OpenGL:不使用实例化将多物体从VBO转为EBO的实现方案
不使用实例化,通过EBO高效绘制多个立方体(网格合并方案)
问题背景
我需要在不使用实例化(Instancing)的前提下,用元素缓冲对象(EBO)绘制多个立方体(演示用三角形绘制四边形),同时避免VBO方案中的大量重复数据浪费——比如每个立方体的顶点位置、平移参数重复存储,对复杂模型来说内存开销极大。这类场景参考简易Minecraft克隆项目,此时无效面的绘制成本过高,实例化不再适用,需通过网格合并优化。
先看VBO的低效实现:
float cubeVertices[] = { //FRONT translate(x, y, z) -0.5f, 0.5f, 0.5f, 0, 0, 0,//front top left -0.5f, -0.5f, 0.5f, 0, 0, 0,//front bottom left 0.5f, 0.5f, 0.5f, 0, 0, 0,//front top right 0.5f, 0.5f, 0.5f, 0, 0, 0,//front top right -0.5f, -0.5f, 0.5f, 0, 0, 0,//front bottom left 0.5f, -0.5f, 0.5f, 0, 0, 0,//front bottom right //FRONT translate(x, y, z) -0.5f, 0.5f, 0.5f, 1, 1, 1,//front top left -0.5f, -0.5f, 0.5f, 1, 1, 1,//front bottom left 0.5f, 0.5f, 0.5f, 1, 1, 1,//front top right 0.5f, 0.5f, 0.5f, 1, 1, 1,//front top right -0.5f, -0.5f, 0.5f, 1, 1, 1,//front bottom left 0.5f, -0.5f, 0.5f, 1, 1, 1,//front bottom right };
这种方式每个立方体的顶点、平移参数都重复存储,内存浪费严重。我期望的高效逻辑(伪代码)如下:
float cubeVertices[] = { -0.5f, 0.5f, 0.5f, //front top left 0.5f, 0.5f, 0.5f, //front top right -0.5f, -0.5f, 0.5f, //front bottom left 0.5f, -0.5f, 0.5f //front bottom right }; unsigned int cubeIndices[] = { 0, 2, 1, //FRONT 1, 2, 3 }; float cubeLocations[] = { 0, 0, 0, 1, 1, 1 }; // 期望用单次绘制调用完成,而非循环多次 for (int loc = 0; loc < 2; ++loc) { for (int i = 0; i < 6; ++i) { //VERTEX COORDS //MODEL LOCATION Draw(cubeVertices[cubeIndices[i]], cubeLocations[loc]); } }
实现方案:网格合并(Mesh Merging)
核心思路是在CPU端预先合并所有立方体的顶点数据,生成一个包含所有唯一顶点(带位置偏移)的大VBO,以及对应的大EBO,然后用一次glDrawElements调用完成绘制。
步骤1:构建合并后的顶点数组和索引数组
复用原始顶点的局部坐标,为每个立方体的位置计算偏移后的世界坐标,同时复用原始索引模式:
#include <vector> #include <glm/glm.hpp> // 原始立方体单个面的局部顶点 std::vector<glm::vec3> baseVertices = { {-0.5f, 0.5f, 0.5f}, // 0: front top left {0.5f, 0.5f, 0.5f}, // 1: front top right {-0.5f, -0.5f, 0.5f}, // 2: front bottom left {0.5f, -0.5f, 0.5f} // 3: front bottom right }; // 原始单个面的索引 std::vector<unsigned int> baseIndices = { 0, 2, 1, 1, 2, 3 }; // 所有立方体的世界位置 std::vector<glm::vec3> cubePositions = { {0.0f, 0.0f, 0.0f}, {1.0f, 1.0f, 1.0f} }; // 合并后的顶点和索引容器 std::vector<glm::vec3> mergedVertices; std::vector<unsigned int> mergedIndices; for (const auto& pos : cubePositions) { // 记录当前合并顶点数组的起始索引 unsigned int baseIndex = mergedVertices.size(); // 添加偏移后的世界顶点 for (const auto& vert : baseVertices) { mergedVertices.push_back(vert + pos); } // 添加偏移后的索引 for (unsigned int idx : baseIndices) { mergedIndices.push_back(idx + baseIndex); } }
步骤2:创建VBO/EBO并完成单次绘制
将合并后的数组上传到GPU,用一次绘制调用完成所有立方体的渲染:
// 创建VAO、VBO、EBO unsigned int VAO, VBO, EBO; glGenVertexArrays(1, &VAO); glGenBuffers(1, &VBO); glGenBuffers(1, &EBO); glBindVertexArray(VAO); // 绑定VBO并上传合并后的顶点数据 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, mergedVertices.size() * sizeof(glm::vec3), mergedVertices.data(), GL_STATIC_DRAW); // 绑定EBO并上传合并后的索引数据 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, mergedIndices.size() * sizeof(unsigned int), mergedIndices.data(), GL_STATIC_DRAW); // 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0); glEnableVertexAttribArray(0); // 单次绘制所有立方体 glDrawElements(GL_TRIANGLES, mergedIndices.size(), GL_UNSIGNED_INT, 0); // 解绑资源(可选) glBindVertexArray(0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
进阶优化:剔除重复顶点
如果多个立方体存在共用面(比如相邻方块的接触面),可以用哈希表缓存已存在的世界顶点,只添加唯一顶点,进一步减少内存占用:
#include <unordered_map> std::unordered_map<glm::vec3, unsigned int> vertexMap; std::vector<glm::vec3> mergedVertices; std::vector<unsigned int> mergedIndices; for (const auto& pos : cubePositions) { std::vector<unsigned int> localIndices; for (const auto& vert : baseVertices) { glm::vec3 worldVert = vert + pos; // 检查顶点是否已存在,不存在则添加 if (vertexMap.find(worldVert) == vertexMap.end()) { vertexMap[worldVert] = mergedVertices.size(); mergedVertices.push_back(worldVert); } localIndices.push_back(vertexMap[worldVert]); } // 转换并添加索引 for (unsigned int idx : baseIndices) { mergedIndices.push_back(localIndices[idx]); } }
这种优化在Minecraft类场景中效果显著,能大量减少重复的共用面顶点,降低内存开销和绘制压力。
为什么不用实例化?
在Minecraft这类场景中,大量立方体存在不可见面(被其他方块遮挡的面),实例化会强制绘制所有面,造成不必要的性能浪费。而网格合并可以在合并前预先剔除不可见面,只保留需要绘制的面,大幅降低绘制的三角形数量,这是实例化无法实现的。
内容的提问来源于stack exchange,提问作者confused cmake user
相关产品推荐
相关产品推荐

