You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 21:35:24