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

OpenGL块系统优化绘制调用:实例化绘制等技术疑问

解决OpenGL体素渲染的Draw Call优化问题

嘿,看来你在体素渲染的性能优化上卡壳了,我来帮你理清楚思路!你的核心需求是用Geometry Shader绘制大量立方体时减少Draw Calls,**实例化绘制(Instanced Drawing)**绝对是你当前场景的最优解,刚好你也提到了正在学习这个,下面结合你的疑问和代码一步步拆解:

核心思路:用实例化绘制替代单实例Draw调用

你现在的DrawVoxel方法每个立方体调用一次glDrawArrays,这在大量立方体场景下会导致Draw Calls爆炸。实例化绘制允许你一次Draw调用渲染成百上千个实例,每个实例可以拥有独立的属性(比如位置、类型),完美适配你的Chunk系统。


逐个解答你的疑问

1. 用Uniform还是VBO传递位置/块数据?

绝对选VBO(实例化数组)!原因有两个:

  • Uniform有严格的容量限制(不同GPU上限不同,但通常单Uniform块最多几KB),根本装不下4096个块的位置/类型数据;
  • 实例化数组是OpenGL专门为多实例渲染设计的机制,数据传输和GPU读取的性能远优于批量更新Uniform。

2. 能否传输uint8_t数组到VBO?

完全可以!OpenGL支持GL_UNSIGNED_BYTE作为顶点数据类型,你可以把Chunk里的块数据(x/y/z索引+类型)打包成紧凑的字节数组。比如每个块用4个uint8_t存储:x(0-15)、y(0-15)、z(0-15)、type(0-255),整个16x16x16的Chunk只需要16*16*16*4=16384字节(16KB),非常节省显存。

3. 能否修改顶点属性接收字节数组?

可以,但GLSL没有原生的uint8_t类型,不过OpenGL会自动把8位无符号字节转换为32位uint传给GLSL。你可以:

  • 要么把4个uint8_t打包成一个uint(用位运算:x<<24 | y<<16 | z<<8 | type),在GLSL里用uint接收;
  • 要么拆成4个独立的顶点属性,每个属性用uint接收(GLSL会自动扩展为32位)。

结合你的代码给出具体实现步骤

第一步:修改Chunk的块数据存储与VBO初始化

把Chunk的三维数组打包成一维的紧凑字节数组,然后创建实例化VBO:

void Chunk::Setup() {
    // 初始化块数据:这里用一维数组存储,每个块占4字节(x,y,z,type)
    uint8_t* blockData = new uint8_t[16*16*16*4];
    int index = 0;
    for (uint8_t x = 0; x < 16; ++x) {
        for (uint8_t y = 0; y < 16; ++y) {
            for (uint8_t z = 0; z < 16; ++z) {
                blockData[index++] = x;
                blockData[index++] = y;
                blockData[index++] = z;
                blockData[index++] = (x+y+z)%2; // 示例:交替空气和泥土
            }
        }
    }

    glGenVertexArrays(1, &m_vao);
    glGenBuffers(1, &m_vbo);
    glGenBuffers(1, &m_instanceVbo); // 新增实例化VBO

    glBindVertexArray(m_vao);

    // 绑定原来的点VBO(单个点,用于Geometry Shader生成立方体)
    glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
    GLfloat verts[] = {0.0f, 0.0f, 0.0f};
    glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

    // 绑定实例化VBO
    glBindBuffer(GL_ARRAY_BUFFER, m_instanceVbo);
    glBufferData(GL_ARRAY_BUFFER, 16*16*16*4, blockData, GL_STATIC_DRAW);

    // 设置实例化属性:x、y、z、type,每个属性占1字节,无符号
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 1, GL_UNSIGNED_BYTE, GL_FALSE, 4, (void*)0);
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 1, GL_UNSIGNED_BYTE, GL_FALSE, 4, (void*)1);
    glEnableVertexAttribArray(3);
    glVertexAttribPointer(3, 1, GL_UNSIGNED_BYTE, GL_FALSE, 4, (void*)2);
    glEnableVertexAttribArray(4);
    glVertexAttribPointer(4, 1, GL_UNSIGNED_BYTE, GL_FALSE, 4, (void*)3);

    // 关键!告诉OpenGL这些属性是每个实例更新一次,而非每个顶点
    glVertexAttribDivisor(1, 1);
    glVertexAttribDivisor(2, 1);
    glVertexAttribDivisor(3, 1);
    glVertexAttribDivisor(4, 1);

    glBindVertexArray(0);
    delete[] blockData;
}

第二步:修改Shader,支持实例化属性

顶点Shader(voxel.glsl)

#shader vertex
#version 330 core
layout (location = 0) in vec3 position;
// 实例化属性:每个实例的x/y/z索引和块类型
layout (location = 1) in uint block_x;
layout (location = 2) in uint block_y;
layout (location = 3) in uint block_z;
layout (location = 4) in uint block_type;

// 输出给几何Shader的数据
out uint v_block_type;
out vec3 v_block_pos;

uniform vec3 u_chunk_position; // Chunk的世界位置
const float BLOCK_SIZE = 1.0f;

void main() {
    // 计算块的世界位置:Chunk位置 + 块在Chunk内的索引
    v_block_pos = u_chunk_position + vec3(block_x, block_y, block_z) * BLOCK_SIZE;
    v_block_type = block_type;
    // 顶点位置直接传,几何Shader会用它作为生成立方体的基准点
    gl_Position = vec4(position, 1.0f);
}

几何Shader(voxel.glsl)

修改开头,接收顶点Shader的输出,并且过滤空气块:

#shader geometry
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 24) out;

// 从顶点Shader接收的实例化数据
in uint v_block_type[];
in vec3 v_block_pos[];

uniform mat4 u_vp;
const float textCount = 3.0f;
const vec3 cubeVertex[8] = vec3[8] (
    /* Z+ (Front) */
    vec3(-0.5f, -0.5f, +0.5f), // 0
    vec3(-0.5f, +0.5f, +0.5f), // 1
    vec3(+0.5f, -0.5f, +0.5f), // 2
    vec3(+0.5f, +0.5f, +0.5f), // 3
    /* Z- (Back) */
    vec3(-0.5f, -0.5f, -0.5f), // 4
    vec3(-0.5f, +0.5f, -0.5f), // 5
    vec3(+0.5f, -0.5f, -0.5f), // 6
    vec3(+0.5f, +0.5f, -0.5f) // 7
);
const int cubeIndices[24] = int[24] (
    1,0,3,2, // Z+ (Front)
    3,2,7,6, // X+ (Right)
    7,6,5,4, // Z- (Back)
    5,4,1,0, // X- (Left)
    0,4,2,6, // Y- (Bottom)
    5,1,7,3 // Y+ (Top)
);
const vec2 textCoords[24] = vec2[24] (
    vec2(1/textCount, 1), vec2(1/textCount, 0), vec2(2/textCount, 1), vec2(2/textCount, 0), // Front
    vec2(1/textCount, 1), vec2(1/textCount, 0), vec2(2/textCount, 1), vec2(2/textCount, 0), // Right
    vec2(1/textCount, 1), vec2(1/textCount, 0), vec2(2/textCount, 1), vec2(2/textCount, 0), // Back
    vec2(1/textCount, 1), vec2(1/textCount, 0), vec2(2/textCount, 1), vec2(2/textCount, 0), // Left
    vec2(2/textCount, 1), vec2(2/textCount, 0), vec2(1, 1), vec2(1, 0), // Bottom
    vec2(0, 1), vec2(0, 0), vec2(1/textCount, 1), vec2(1/textCount, 0) // Top
);

void main() {
    // 跳过空气块(type=0),避免无效渲染
    if (v_block_type[0] == 0) {
        return;
    }

    // 遍历立方体的24个顶点,生成三角形带
    for(int i = 0; i < 24; i++){
        vec3 worldPos = v_block_pos[0] + cubeVertex[cubeIndices[i]];
        gl_Position = u_vp * vec4(worldPos, 1.0f);
        // 这里可以传递纹理坐标等数据给片段Shader
        EmitVertex();
    }
    EndPrimitive();
}

第三步:修改Draw方法,使用实例化绘制

void Chunk::Draw(Shader& p_shader) {
    p_shader.SetUniformVec3("u_chunk_position", m_chunkPosition); // 设置Chunk的世界位置
    glBindVertexArray(m_vao);
    // 一次渲染4096个实例(16*16*16)
    glDrawArraysInstanced(GL_POINTS, 0, 1, 16*16*16);
    glBindVertexArray(0);
}

额外优化建议

  1. 剔除不可见块:在Chunk初始化时,只保留可见的块(比如周围不是空气的块),减少实例数量,进一步提升性能;
  2. 合并Chunk:如果场景中有很多Chunk,可以把多个Chunk合并成一个大的实例数组,进一步减少Draw Calls;
  3. 避免Geometry Shader循环:你之前考虑的在Geometry Shader里循环生成4096个立方体的思路不可取,因为Geometry Shader是单线程处理每个顶点,循环会严重拖慢性能,实例化绘制是GPU并行处理每个实例,效率高得多。

内容的提问来源于stack exchange,提问作者Adrien Givry

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 06:42:28