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 合法,并不代表调用所需的其他状态也都正确。
排查时,可以从最终的绘制调用向前检查:
- 在
glDrawElements()前设置断点,查看IndexCount。这里会发现它一直为0。 - 搜索所有对
IndexCount的赋值,确认它只被初始化和重置,从未增加。 - 检查
IndexBuffer的创建和绑定,确认它只绑定到了GL_ARRAY_BUFFER。 - 检查顶点经过
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 的索引缓冲关联和变换后的坐标,再考虑三角形绕序。这些检查更有助于定位当前没有几何体显示的问题。
备注:内容仅供参考。
