美文网首页
Android OpenGLES3绘图 - 鱼群模拟

Android OpenGLES3绘图 - 鱼群模拟

作者: rome753 | 来源:发表于2022-03-28 13:50 被阅读0次
    截屏2022-04-01 下午5.54.46.png

    在B站上偶然看到一个这样的鱼群模拟的视频,很有意思。大自然很多东西都是类似分形、群体涌现的模式。我们可以用OpenGL简单地模拟一下,先实现一个二维模型,再尝试扩展到三维。

    相关资料:
    https://blog.csdn.net/liweizhao/article/details/82106886
    介绍了简单的集群算法原理
    https://www.jianshu.com/p/9881630fb542
    用计算着色器实现的集群算法,非常强大

    原理分析:每条鱼可以先用点代替,在二维平面上生成一些随机点,它们有坐标和速度,每个点会寻找周围的点,跟随群体移动。关键在于寻找周围的点,每个点不能去遍历所有点的位置,那样时间复杂度是O(N2)。需要对空间进行划分,有四叉树、希尔伯特曲线等方法。感觉在这个问题上将空间均匀划分更好搜索相邻的点,可以划分成8x8或16x16的矩形块,每次搜索3x3的区块,这样时间复杂度就下降到了O(N)。当然极端情况下如果所有点都集中到了3x3的空间里,时间复杂度就会回到O(N2),这是需要优化的地方。

    1 模拟算法

    1.1 初始化所有点

    用一个数组保存所有点的位置,数组按[x0,y0,x1,y1...]方式保存;另一个数组保存所有点的速度,数组按[vx0,vy0,vx1,vy1...]方式保存;再用一个数组保存所有点的临时新位置,因为要先用老位置计算,全部计算完才能刷新位置,不能用新老位置夹杂计算。点的速度控制在一定范围内。

        float[] pos = new float[MAX_COUNT * 2];
        float[] vel = new float[MAX_COUNT * 2];
    
        float[] tempPos = new float[pos.length];
    
        void controlSpeed(int i) {
            float vx = vel[i];
            float vy = vel[i + 1];
            float vv = (float) Math.sqrt(vx * vx + vy * vy);
            float mul = 1f;
            if (vv >= vmax) {
                mul = vmax / vv;
            } else if (vv < vmin) {
                mul = vmin / vv;
            }
            vel[i] *= mul;
            vel[i + 1] *= mul;
        }
    

    1.2 划分区块

    划分成16x16个空间区块。用Part表示区块,实际上里面是一个HashSet,保存了区块里面的点;再用一个HashMap保存所有的区块,key是区块的索引,也就是从0~16x16-1顺序排列。每个区块第一次用到时才创建,防止初始化太慢。

        // PART index -> Part
    
        static int PARTS = 16; // 8 * 8
        static int partW = BOUND / PARTS;
        HashMap<Integer, Part> partMap = new HashMap<>();
    
        static class Part {
    
            public Part() {
                set = new HashSet<>();
            }
    
            HashSet<Integer> set;
    
        }
    

    查找一个点所在区块

    
        Part findPart(int posIndex) {
            float x = pos[posIndex];
            float y = pos[posIndex + 1];
            int px = (int)x / partW;
            int py = (int)y / partW;
            int partIndex = px + py * PARTS;
            Part p = partMap.get(partIndex);
            if (p == null) {
                p = new Part();
                partMap.put(partIndex, p);
                Log.d("chao", "create part " + partIndex);
            }
            return p;
        }
    
    

    查找3x3相邻区块

    
        int[] neighborsDir = {
                -1, -1, 0, -1, 1, -1,
                -1,  0, 0,  0, 1,  0,
                -1,  1, 0,  1, 1,  1,
        };
        void findNeighborParts(int posIndex) {
            float x = pos[posIndex];
            float y = pos[posIndex + 1];
            int px = (int)x / partW;
            int py = (int)y / partW;
            int iNeighbor = 0;
            for (int i = 0; i < neighborsDir.length - 1; i+=2) {
                int rx = px + neighborsDir[i];
                int ry = py + neighborsDir[i + 1];
                if (rx >= 0 && rx < PARTS && ry >= 0 && ry < PARTS) {
                    int partIndex = rx + ry * PARTS;
                    Part p = partMap.get(partIndex);
                    if (p != null) {
                        for (Integer iPos: p.set) {
                            neighbors[iNeighbor++] = iPos;
                        }
                    }
                }
            }
            neighbors[iNeighbor] = -1; // 结尾标志
        }
    

    1.3 核心算法:更新位置

    遍历一遍所有点,寻找每个点在相邻区块内的相邻点,保存到neighbors数组里,计算出它们的平均速度,应用到当前点,用该速度计算出新位置,保存到临时列表中。
    再次遍历所有点,应用新位置。并比较新老位置,如果新老位置在不同区块中,那么更新这两个区块。

            long time = System.currentTimeMillis();
    
            for (int i = 0; i < pos.length - 1; i+=2) {
    
                findNeighborParts(i);
    
                // 计算群速度:可见的别的点的速度和
                float vxSum = 0;
                float vySum = 0;
                float count = 0;
                for (Integer j : neighbors) {
                    if (j == -1) {
                        break;
                    }
                    if (j == i) {
                        continue;
                    }
                    float dx = pos[i] - pos[j];
                    float dy = pos[i + 1] - pos[j + 1];
                    float dis = dx * dx + dy * dy;
                    if (dis < visiDis) {
                        vxSum += -dx;
                        vySum += -dy;
                        count++;
                    } else if (dis < closeDis) {
                        vxSum -= dx;
                        vySum -= dy;
                        count++;
                    }
                }
    
                // 改变速度
                if (count > 0) {
                    float vxAve = vxSum / count;
                    float vyAve = vySum / count;
                    vel[i] = vel[i] * 0.8f + vxAve * 0.2f;
                    vel[i + 1] = vel[i + 1] * 0.8f + vyAve * 0.2f;
                    controlSpeed(i);
                }
    
                // 更新位置,保存到temp里
                float x1 = pos[i] + dt * vel[i];
                float y1 = pos[i + 1] + dt * vel[i + 1];
                boolean out = false;
                if (x1 < 0 || x1 >= BOUND) {
                    vel[i] = -vel[i];
                    out = true;
                }
                if (y1 < 0 || y1 >= BOUND) {
                    vel[i + 1] = -vel[i + 1];
                    out = true;
                }
                if (out) {
                    tempPos[i] = pos[i];
                    tempPos[i + 1] = pos[i + 1];
                } else {
                    tempPos[i] = x1;
                    tempPos[i + 1] = y1;
                }
            }
    
            // 更新Part,写入新速度
            for (int i = 0; i < pos.length - 1; i+=2) {
                Part p0 = findPart(i);
                pos[i] = tempPos[i];
                pos[i + 1] = tempPos[i + 1];
                Part p1 = findPart(i);
                if (p0 == p1) {
                    continue;
                }
                p0.set.remove(i);
                p1.set.add(i);
            }
    
            posBuffer.position(0);
            posBuffer.asFloatBuffer().put(pos);
    
            time = System.currentTimeMillis() - time;
            Log.d("chao", "updae time " + time);
        }
    
    

    2 OpenGL绘制

    2.1 创建顶点数组

    顶点数组ByteBuffer posBuffer大小跟位置数组相同,

        int[] vao;
        int[] vbo;
    
        ByteBuffer posBuffer;
    
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    
    //        initPos();
            initAll();
    
            program = ShaderUtils.loadProgramGroup();
    
    //        //分配内存空间,每个浮点型占4字节空间
            posBuffer = ByteBuffer
                    .allocateDirect(2 * 4 * MAX_COUNT)
                    .order(ByteOrder.nativeOrder());
    
            vao = new int[1];
            glGenVertexArrays(1, vao, 0);
            glBindVertexArray(vao[0]);
    
            vbo = new int[1];
            glGenBuffers(1, vbo, 0);
            glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
    //        glBufferData(GL_ARRAY_BUFFER, vertices.length * 4, vertexBuffer, GL_STATIC_DRAW);
            glBufferData(GL_ARRAY_BUFFER, MAX_COUNT * 4 * 2, posBuffer, GL_STREAM_DRAW);
    
            // Load the vertex data
            glVertexAttribPointer(0, 2, GL_FLOAT, false, 2 * 4, 0);
            glEnableVertexAttribArray(0);
    
            glBindBuffer(GL_ARRAY_BUFFER, 0);
            glBindVertexArray(0);
    
            glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    
    //        glCullFace(GL_BACK);
    //        glEnable(GL_CULL_FACE);
        }
    

    2.2 刷新和绘制

    计算完位置后将位置数组传给posBuffer,再将posBuffer传给vbo,用glDrawArrays(GL_POINTS, 0, MAX_COUNT)方式绘制点精灵。

    
        void updateAll() {
            ...
            posBuffer.position(0);
            posBuffer.asFloatBuffer().put(pos);
        }
    
        @Override
        public void onDrawFrame(GL10 gl) {
            super.onDrawFrame(gl);
    
    //        update();
            updateAll();
    
            // Clear the color buffer
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            // 刷新vbo数据
            glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
            glBufferSubData(GL_ARRAY_BUFFER, 0, MAX_COUNT * 4 * 2,  posBuffer);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    
            // Use the program object
            glUseProgram(program);
            glBindVertexArray(vao[0]);
    
            glDrawArrays(GL_POINTS, 0, MAX_COUNT);
        }
    

    2.3 着色器

    顶点着色器将传入的位置转换一下,设置点的大小
    shader_group_v.glsl

    #version 300 es
    layout (location = 0) in vec2 vPosition;
    
    out vec2 vPos;
    
    void main() {
         vPos = (vPosition / 1024.0f - 0.5f) * 2.0f;
         gl_PointSize = 10.0f;
         gl_Position  = vec4(vPos, 0.0f, 1.0f);
    }
    

    片段着色器根据位置选择不同颜色

    #version 300 es
    precision mediump float;
    
    in vec2 vPos;
    
    out vec4 fragColor;
    
    void main() {
         fragColor = vec4((vPos + 1.0f) * 0.5f, 0.5f, 1.0f);
    }
    

    这样就完成了!

    3 扩展到三维

    在二维的基础上修改,主要是位置和速度都增加了z方向,空间划分的区块成了16x16x16个,每个点需要寻找周围3x3x3=27个区块。其实算法的主要时间和空间的复杂度基本上是线性增长,变成原来的1.5倍,是完全没问题的。

    
        float[] pos = new float[MAX_COUNT * 3];
        float[] vel = new float[MAX_COUNT * 3];
    
        // 空间中27个方块的三维坐标偏移
        int[] neighborsDir = {-1, -1, -1, -1, -1, 0, -1, -1, 1, -1, 0, -1, -1, 0, 0, -1, 0, 1, -1, 1, -1, -1, 1, 0, -1, 1, 1, 0, -1, -1, 0, -1, 0, 0, -1, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1, 0, 1, -1, 0, 1, 0, 0, 1, 1, 1, -1, -1, 1, -1, 0, 1, -1, 1, 1, 0, -1, 1, 0, 0, 1, 0, 1, 1, 1, -1, 1, 1, 0, 1, 1, 1};
        int[] neighbors = new int[pos.length / 3 + 1];
    
        Part findPart(int posIndex) {
            float x = pos[posIndex];
            float y = pos[posIndex + 1];
            float z = pos[posIndex + 2];
            int px = (int)x / partW;
            int py = (int)y / partW;
            int pz = (int)z / partW;
            int partIndex = px + py * PARTS + pz * PARTS * PARTS;
            Part p = partMap.get(partIndex);
            if (p == null) {
                p = new Part();
                partMap.put(partIndex, p);
                Log.d("chao", "create part " + partIndex);
            }
            return p;
        }
    
        void findNeighborParts(int posIndex) {
            float x = pos[posIndex];
            float y = pos[posIndex + 1];
            float z = pos[posIndex + 2];
            int px = (int)x / partW;
            int py = (int)y / partW;
            int pz = (int)z / partW;
            int iNeighbor = 0;
            for (int i = 0; i < neighborsDir.length - 2; i+=3) {
                int rx = px + neighborsDir[i];
                int ry = py + neighborsDir[i + 1];
                int rz = pz + neighborsDir[i + 2];
                if (rx >= 0 && rx < PARTS && ry >= 0 && ry < PARTS && rz >= 0 && rz < PARTS) {
                    int partIndex = rx + ry * PARTS + rz * PARTS * PARTS;
                    Part p = partMap.get(partIndex);
                    if (p != null) {
                        for (Integer iPos: p.set) {
                            neighbors[iNeighbor++] = iPos;
                        }
                    }
                }
            }
            neighbors[iNeighbor] = -1; // 结尾标志
        }
    

    OpenGL绘制上有一些增加,变成三维后需要一些空间矩阵变换

    
        float[] modelMat = new float[16];
        float[] viewMat = new float[16];
        float[] projectionMat = new float[16];
    
        @Override
        public void onDrawFrame(GL10 gl) {
            super.onDrawFrame(gl);
    //        update();
            updateAll();
    
            // Clear the color buffer
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            // 刷新vbo数据
            glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
            glBufferSubData(GL_ARRAY_BUFFER, 0, MAX_COUNT * 4 * 3,  posBuffer);
            glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    
            // Use the program object
            glUseProgram(program);
    
    //        Matrix.setIdentityM(modelMat, 0);
            Matrix.setIdentityM(viewMat, 0);
            Matrix.setIdentityM(projectionMat, 0);
    
            Matrix.perspectiveM(projectionMat, 0, OurCamera.radians(ourCamera.Zoom), width / height, 0.1f, 100.0f);
            ourCamera.GetViewMatrix(viewMat);
    
            int loc1 = glGetUniformLocation(program, "view");
            glUniformMatrix4fv(loc1, 1, false, viewMat, 0);
            int loc2 = glGetUniformLocation(program, "projection");
            glUniformMatrix4fv(loc2, 1, false, projectionMat, 0);
            Matrix.setIdentityM(modelMat, 0);
            Matrix.translateM(modelMat, 0, 0, 0, 0);
            Matrix.rotateM(modelMat, 0, rx, 0.0f, 1.0f, 0.0f);
            Matrix.rotateM(modelMat, 0, ry, 1.0f, 0.0f, 0.0f);
            int loc3 = glGetUniformLocation(program, "model");
            glUniformMatrix4fv(loc3, 1, false, modelMat, 0);
    
    
            glBindVertexArray(vao[0]);
            glDrawArrays(GL_POINTS, 0, MAX_COUNT);
    
            // 先画不透明的物体
            // 设置标志位
            glDepthMask(false);
            // 再画半透明物体
            boxShader.draw(modelMat, viewMat, projectionMat);
            // 重置标志位
            glDepthMask(true);
        }
    
    

    顶点着色器
    shader_group3d_v.glsl

    #version 300 es
    layout (location = 0) in vec3 vPosition;
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    out vec3 vPos;
    
    void main() {
         vPos = (vPosition / 1024.0f - 0.5f) * 2.0f;
         gl_PointSize = 10.0f;
         gl_Position  = projection * view * model * vec4(vPos, 1.0f);
    }
    

    相关文章

      网友评论

          本文标题:Android OpenGLES3绘图 - 鱼群模拟

          本文链接:https://www.haomeiwen.com/subject/lrlrjrtx.html