WAYNETS.ORG

Game and Program

批处理渲染

作者:

发表于

Context Polling System Post-Processing Renderer

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都要 glBufferDataDrawArrays

相反,批处理渲染的方式则是:只给缓冲大小,自己去写入。
在批处理中,提前在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