1. 简介
批处理渲染(Batch Rendering) 是一种将多个绘制命令合并为一个绘制调用(Draw Call)来提交给 GPU 的技术。它的目标是:减少CPU → GPU 的Draw Call次数,以提升渲染性能。
- 传统渲染方式:每画一个四边形(Sprite),就一次 Draw Call。如果是这样的话,如果如果你画 1000 个 Quad,每个都调用一次
glDrawElements,就会有 1000 次 Draw Call,CPU 开销爆炸。 - 批处理渲染:将多个四边形的数据一次性打包上传到 GPU,然后只执行一次 Draw Call。
2. 批处理渲染的原理
- 在CPU和GPU端分别预分配内存区域用于存储顶点缓存数据。
- 所有的绘图指令(比如DrawSqad)都会将顶点数据写入这个缓存中。
- 每帧只进行一次提交,调用一次draw call,从而大幅提高效率。
3. 动态顶点缓冲
在过去,我们设置顶点缓冲数据的方式是这样的,我们要自己编写好一个顶点数组,然后把这个数组传给GPU中已经绑定好的顶点缓存区(Vertex Buffer):
float vertices[] = {
// 顶点位置 + 颜色
-0.5f, -0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f, 1.0f,
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
这就意味着顶点数据都是一次性生成好的,且每次执行Draw Call都要 glBufferData 或 DrawArrays
相反,批处理渲染的方式则是:只给缓冲大小,自己去写入。
在批处理中,提前在GPU中分配一大块内存(动态 Vertex Buffer):
// 分配最大缓存大小,比如支持最多 1000 个 Quad(4000 个顶点)
glBufferData(GL_ARRAY_BUFFER, MaxVertices * sizeof(Vertex), nullptr, GL_DYNAMIC_DRAW);
预分配好内存后,我们每帧 CPU 代码在运行时调用函数SetData(),手动写入这些数据:
void OpenGLVertexBuffer::SetData(const void* data, uint32_t size)
{
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
}
在OpenGL中,通常使用glBufferSubData(),它与函数glBufferData()的区别在于:
glBufferData是重新分配整个GPU缓冲(有分配成本,性能低)glBufferSubData是往已有缓冲中写入数据(高效、适合实时更新)
5. 批处理渲染流程
Renderer2D::Init
→ 设置 VAO / VBO / IBO
→ 初始化着色器 / 默认纹理
每帧:
BeginScene → StartBatch
多次 DrawQuad()
→ 写入 VBO(顶点)
→ 填充 TextureSlot(最多 32)
→ 超过限制自动 Flush() + StartBatch()
EndScene → Flush() → DrawIndexed
(1) 核心数据结构
定义一个数据结构用于保存2D渲染器的关键信息,包括:
- 批处理最大支持 10,000 个四边形
- 每个 Quad 有 4 个顶点、6 个索引
- 一个 Frame 中最多支持绑定 32 个纹理
struct Renderer2DData
{
static const uint32_t MaxQuads = 10000;
static const uint32_t MaxVertices = MaxQuads * 4;
static const uint32_t MaxIndices = MaxQuads * 6;
static const uint32_t MaxTextureSlots = 32;
Ref<VertexArray> QuadVertexArray;
Ref<VertexBuffer> QuadVertexBuffer;
Ref<Shader> QuadShader;
uint32_t QuadIndexCount = 0;
QuadVertex* QuadVertexBufferBase = nullptr;
QuadVertex* QuadVertexBufferPtr = nullptr;
Ref<Texture2D> TextureSlots[MaxTextureSlots];
uint32_t TextureSlotIndex = 1; // 0 是默认白纹理
glm::vec4 QuadVertexPositions[4];
};
(2) 初始化2D渲染器信息,构建VAO + VBO + IBO
s_Data.QuadVertexArray = VertexArray::Create();
s_Data.QuadVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(QuadVertex));
s_Data.QuadVertexBuffer->SetLayout({
{ ShaderDataType::Float3, "a_Position" },
{ ShaderDataType::Float4, "a_Color" },
{ ShaderDataType::Float2, "a_TexCoord" },
{ ShaderDataType::Float, "a_TexIndex" },
{ ShaderDataType::Float, "a_TilingFactor" },
});
s_Data.QuadVertexArray->AddVertexBuffer(s_Data.QuadVertexBuffer);
// Index buffer(提前生成所有可能的 index)
uint32_t* quadIndices = new uint32_t[s_Data.MaxIndices];
uint32_t offset = 0;
for (uint32_t i = 0; i < s_Data.MaxIndices; i += 6)
{
quadIndices[i + 0] = offset + 0;
quadIndices[i + 1] = offset + 1;
quadIndices[i + 2] = offset + 2;
quadIndices[i + 3] = offset + 2;
quadIndices[i + 4] = offset + 3;
quadIndices[i + 5] = offset + 0;
offset += 4;
}
Ref<IndexBuffer> quadIB = IndexBuffer::Create(quadIndices, s_Data.MaxIndices);
s_Data.QuadVertexArray->SetIndexBuffer(quadIB);
delete[] quadIndices;
(3) 开始渲染
void Renderer2D::BeginScene(const Camera& camera)
{
s_Data.TextureShader->Bind();
s_Data.TextureShader->SetMat4("u_ViewProjection", camera.GetViewProjection());
StartBatch();
}
初始化批处理:
void StartBatch()
{
s_Data.QuadIndexCount = 0;
s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;
s_Data.TextureSlotIndex = 1;
}
(4) 绘制四边形
void Renderer2D::DrawQuad(const glm::vec2& position, const glm::vec4& color)
{
if (s_Data.QuadIndexCount >= Renderer2DData::MaxIndices)
NextBatch();
float textureIndex = 0.0f; // 白纹理
float tilingFactor = 1.0f;
s_Data.QuadVertexBufferPtr->Position = position3D;
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = {0.0f, 0.0f}; // 左下
s_Data.QuadVertexBufferPtr->TexIndex = textureIndex;
s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
s_Data.QuadVertexBufferPtr++;
// 同样为其他三个顶点设置...
s_Data.QuadIndexCount += 6;
}
如果当前Batch已满,则调用函数NextBatch():
void NextBatch()
{
Flush();
StartBatch();
}
(5) 提交数据并绘制
void Flush()
{
uint32_t dataSize = (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;
s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, dataSize);
for (uint32_t i = 0; i < s_Data.TextureSlotIndex; i++)
s_Data.TextureSlots[i]->Bind(i);
RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
s_Data.Stats.DrawCalls++;
}

Leave a comment