PAYATHON 2026

OpenGL batch renderer 仅显示 clear color 且无报错,如何排查?

排障赵师傅

IndexCount 始终为 0,所以当前绘制调用不会生成任何三角形。代码中还有几处需要修正:索引缓冲没有绑定到 VAO 的 GL_ELEMENT_ARRAY_BUFFER,投影矩阵使用了整数参数,矩形的位置和尺寸也不符合在屏幕中央显示的要求。

DrawQuad() 写入了四个顶点,却没有增加索引数量,实际提交的调用是:

glDrawElements(GL_TRIANGLES, 0, GL_UNSIGNED_INT, nullptr);

这就解释了为什么顶点数据看起来正常,窗口里却只有清屏颜色。没有 OpenGL 错误,也不能说明其余状态都正确。

1. 每添加一个矩形,都要增加六个索引

主循环每帧执行:

VertexCount = 0;
IndexCount = 0;

随后,DrawQuad() 只增加了 VertexCount。即使已经生成并上传索引缓冲,CPU 端的 IndexCount 也需要单独更新。

在 DrawQuad() 写完四个顶点后加上:

IndexCount += 6;

绘制一个矩形时,计数应为:

VertexCount = 4
IndexCount  = 6

glDrawElements() 的第二个参数是本次读取的索引数量,不是顶点数、三角形数或缓冲区字节数。

2. 把索引缓冲绑定到 VAO

原代码使用:

glBindBuffer(GL_ARRAY_BUFFER, IndexBuffer);
glBufferData(GL_ARRAY_BUFFER, ..., quadIndices, GL_STATIC_DRAW);

这样可以上传数据,但绘制时还没有指定这个缓冲对象作为索引缓冲。

OpenGL 缓冲对象本身没有固定的“顶点缓冲”或“索引缓冲”类型。这里需要修正的是绑定状态:glDrawElements() 使用当前 VAO 关联的 GL_ELEMENT_ARRAY_BUFFER。

将索引缓冲的初始化代码改为:

glBindVertexArray(VAO);

glCreateBuffers(1, &IndexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IndexBuffer);
glBufferData(
    GL_ELEMENT_ARRAY_BUFFER,
    RendererMaxIndices * sizeof(uint32_t),
    quadIndices,
    GL_STATIC_DRAW
);

delete[] quadIndices;

GL_ELEMENT_ARRAY_BUFFER 的绑定属于 VAO 状态,设置时需要先绑定目标 VAO。在这个 VAO 仍处于绑定状态时,执行下面的调用会移除它的索引缓冲关联:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

本例请求的是 OpenGL 4.5,也可以用下面的调用显式设置关联:

glVertexArrayElementBuffer(VAO, IndexBuffer);

两种方式选一种即可。绘制调用中的 nullptr 表示从已绑定索引缓冲的字节偏移 0 开始读取。

3. 使用浮点投影参数,并统一坐标尺度

原代码中的调用是:

glm::ortho(0, 800, 0, 600, -1, 1)

实参全是整数,模板参数也会推导为整数类型。具体结果取决于 GLM 版本:有的版本会通过静态断言拒绝整数类型;允许这种实例化的实现则可能发生整数除法,产生错误的矩阵。这里应明确使用浮点数:

glm::ortho(
    0.0f, 800.0f,
    0.0f, 600.0f,
    -1.0f, 1.0f
)

按通常的 OpenGL 坐标约定,这个投影覆盖 x ∈ [0, 800]、y ∈ [0, 600],中心为 (400, 300)。

原来的绘制调用是:

DrawQuad(
    { 0.0f, 0.0f, 0.0f },
    { 1.0f, 1.0f },
    { 1.0f, 0.0f, 0.0f, 1.0f }
);

DrawQuad() 用 position ± size 构造顶点,因此 size 表示半宽和半高。这个矩形覆盖 [-1, 1] × [-1, 1],大部分在可见范围外,可见部分也靠近左下角。

要在上述投影的中央绘制一个宽 200、高 120 的矩形,可以改为:

DrawQuad(
    { 400.0f, 300.0f, 0.0f },
    { 100.0f, 60.0f },
    { 1.0f, 0.0f, 0.0f, 1.0f }
);

如果希望画面中心是 (0, 0),也可以使用以原点为中心的浮点正交投影,再按投影尺度设置矩形大小。

4. 修正后的关键绘制代码

保留原来的顶点结构和索引排列,DrawQuad() 可以写成:

void DrawQuad(glm::vec3 position, glm::vec2 size, glm::vec4 color)
{
    if (VertexCount + 4 > RendererMaxVertexCount ||
        IndexCount + 6 > RendererMaxIndices)
    {
        Flush();
        VertexCount = 0;
        IndexCount = 0;
        TextureSlotIndex = 1;
    }

    Verticies[VertexCount++] = Vertex{
        position + glm::vec3(size.x, size.y, 0.0f),
        color, {1.0f, 1.0f}, 0.0f, 1.0f
    };
    Verticies[VertexCount++] = Vertex{
        position + glm::vec3(-size.x, size.y, 0.0f),
        color, {0.0f, 1.0f}, 0.0f, 1.0f
    };
    Verticies[VertexCount++] = Vertex{
        position + glm::vec3(size.x, -size.y, 0.0f),
        color, {1.0f, 0.0f}, 0.0f, 1.0f
    };
    Verticies[VertexCount++] = Vertex{
        position + glm::vec3(-size.x, -size.y, 0.0f),
        color, {0.0f, 0.0f}, 0.0f, 1.0f
    };

    IndexCount += 6;
}

对应的 Flush():

void Flush()
{
    if (IndexCount == 0)
        return;

    TextureShader->use();
    TextureShader->setUniformMat4(
        "u_ViewProjection",
        glm::ortho(
            0.0f, 800.0f,
            0.0f, 600.0f,
            -1.0f, 1.0f
        )
    );

    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferSubData(
        GL_ARRAY_BUFFER,
        0,
        VertexCount * sizeof(Vertex),
        Verticies.data()
    );

    for (uint32_t i = 0; i < TextureSlotIndex; ++i)
        TextureSlots[i]->bind(i);

    glDrawElements(
        GL_TRIANGLES,
        static_cast<GLsizei>(IndexCount),
        GL_UNSIGNED_INT,
        nullptr
    );
}

使用这段代码前,需要在初始化时将 IndexBuffer 正确关联到 VAO。这里的 Verticies.data() 直接返回顶点元素的指针,比传入整个 std::array 对象的地址更清楚。

5. 为什么没有报错,以及如何定位

OpenGL 无法判断应用打算提交多少几何体,也不知道矩形是否应该位于屏幕中央。绘制数量为零、坐标尺度错误或物体位于可见范围外,都可能不产生 API 错误。

缺少索引缓冲则需要单独检查。在本例使用的 core profile 中,索引绘制需要正确的缓冲关联。count == 0 合法,并不代表调用所需的其他状态也都正确。

排查时,可以从最终的绘制调用向前检查:

  1. 在 glDrawElements() 前设置断点,查看 IndexCount。这里会发现它一直为 0。
  2. 搜索所有对 IndexCount 的赋值,确认它只被初始化和重置,从未增加。
  3. 检查 IndexBuffer 的创建和绑定,确认它只绑定到了 GL_ARRAY_BUFFER。
  4. 检查顶点经过 u_ViewProjection 变换后的坐标,确认矩形的位置和大小。

也可以在绘制前临时查询 VAO 的索引缓冲绑定:

glBindVertexArray(VAO);

GLint elementBuffer = 0;
glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &elementBuffer);

std::cout << "IndexCount = " << IndexCount
          << ", element buffer = " << elementBuffer
          << '\n';

修复后,单个矩形应使用六个索引,elementBuffer 应对应创建的 IndexBuffer。

使用 RenderDoc 时,抓取一帧并找到对应的绘制事件,先检查绘制数量,再查看 Pipeline State 中的索引缓冲和顶点输入。原来的零数量调用可能没有可供分析的有效网格。修复计数和绑定后,再用 Mesh Viewer 检查输入顶点及顶点着色器输出,然后继续排查纹理采样、深度测试和最终颜色输出。

6. 还需要补上的检查

检查 shader 编译和 program 链接结果

原来的 Shader 构造函数没有查询编译或链接状态。glCompileShader() 返回不代表编译成功,编译是否失败也不能只靠 glGetError() 判断。

至少需要查询:

GLint compiled = GL_FALSE;
glGetShaderiv(vertex, GL_COMPILE_STATUS, &compiled);
// 失败时使用 glGetShaderInfoLog() 读取日志。

glGetShaderiv(fragment, GL_COMPILE_STATUS, &compiled);
// 同样检查 fragment。

GLint linked = GL_FALSE;
glGetProgramiv(m_rendererID, GL_LINK_STATUS, &linked);
// 失败时使用 glGetProgramInfoLog() 读取日志。

片元着色器使用了 sampler2D u_Textures[32]。需要查询当前实现的 GL_MAX_TEXTURE_IMAGE_UNITS,确认支持所需数量,并检查链接日志。请求 OpenGL 4.5 并不能保证所有实现都支持这段资源配置。

确认调试输出确实启用

贴出的代码没有展示 debug context 请求和消息回调注册。通常需要在创建窗口前请求:

glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);

创建上下文并加载函数后,再启用 GL_DEBUG_OUTPUT,注册 glDebugMessageCallback()。如果未贴出的代码已经完成这些配置,还需要检查消息过滤设置。矩形位置不符合应用预期这类问题,调试输出无法判断。

显式初始化 viewport

GLAD 初始化完成后,可以立即按 framebuffer 的实际尺寸设置 viewport:

int framebufferWidth = 0;
int framebufferHeight = 0;

glfwGetFramebufferSize(
    Window, &framebufferWidth, &framebufferHeight
);
glViewport(0, 0, framebufferWidth, framebufferHeight);

原来的 resize callback 可以保留。如果希望窗口缩放后,投影坐标也随 framebuffer 尺寸变化,还需要同步更新投影矩阵。

本例没有启用 GL_CULL_FACE,可以先排查绘制数量、VAO 的索引缓冲关联和变换后的坐标,再考虑三角形绕序。这些检查更有助于定位当前没有几何体显示的问题。

备注:内容仅供参考。