一、前言

什么是Batch Rendering

打个比方,一个人骑一辆摩托车上路,10个人就需要骑10辆摩托车,这是一般的烘托方法,每个人上路都要消耗一次摩托车资源(GPU资源)

游戏引擎从零开始(35)-Batch Rendering(1)

假如10个人都挤上1辆摩托车,一起上路,就相当于兼并了10次操作,却只占用了1/10的资源,这便是批烘托的核心思维。

游戏引擎从零开始(35)-Batch Rendering(1)

实践工程中,由于GPU内存不是无限大的,且GPU的并发也是有上限的,不能一次存储一个十分大的极点数据,所以合批也是有上限的。那么问题来了,一次draw中,究竟制作多少极点数据才适宜呢?不知道!!得依据每台机器的硬件资源、图形驱动实测才能大致承认。依据笔者的经历,这个值仍是挺大的,至少是十几万的量级。

简略总结下批烘托的知识点:

Batch Rendering的优势

  • 削减极点数据传输开销
  • 削减状态变更开销

批烘托应留意的地方

  • 相同的极点格局
  • 相同的shader及参数
  • 相同的烘托状态(混合形式、深度测试、除掉形式)

二、批烘托完成

这篇文章,我们仅完成最根底的矩形烘托合批,旋转和纹路切换在下一篇文章介绍。

修改点如下两张图所示,批烘托流程中,draw仅仅新增加数据,在EndScene()中调用Flush()才真的去制作。

一般烘托:

游戏引擎从零开始(35)-Batch Rendering(1)

批烘托:

游戏引擎从零开始(35)-Batch Rendering(1)

Renderer2D支撑批烘托

主要改造在Renderer2D类中,捋清楚了Renderer2D的代码逻辑,就能理解批烘托的完成流程。

改造DrawQuad,仅增加数据,不调draw指令,比及调用flush时,才真实的调用drawElement,一次制作一切的数据。

Sandbox/Hazel/src/Hazel/Renderer/Renderer2D.h

  1. Renderer2D增加Flush接口
static void Flush();

Sandbox/Hazel/src/Hazel/Renderer/Renderer2D.h

  1. 增加QuadVertex数据结构,描绘单点的数据特点
struct QuadVertex
{
    glm::vec3 Position;
    glm::vec4 Color;
    glm::vec2 TexCoord;
};
  1. Renderer2DStorage更名为Renderer2DData,增加了批烘托需要的特点

struct Renderer2DData{
    // 一个批次最多制作10000个矩形
    const uint32_t MaxQuads = 10000;
    // 最多处理MaxQuads * 4个极点
    const uint32_t MaxVertices = MaxQuads * 4;
    // 最多处理MaxQuads * 6个索引
    // 1个矩形=2个三角形=6个索引
    const uint32_t MaxIndices = MaxQuads * 6;
    // 极点数组
    Ref<VertexArray> QuadVertexArray;
    // 极点缓冲
    Ref<VertexBuffer> QuadVertexBuffer;
    // shader
    Ref<Shader> TextureShader;
    // 纹路
    Ref<Texture2D> WhiteTexture;
    // 索引总数量
    uint32_t QuadIndexCount = 0;
    // 极点数据的起始地址,即第一个极点的指针
    QuadVertex* QuadVertexBufferBase = nullptr;
    // 动态更新,以符号当前要处理的数据
    QuadVertex* QuadVertexBufferPtr = nullptr;
};
  1. 初始化Renderer2D

依照预设的最大值10000来初始化GPU中的极点数组内存

void Renderer2D::Init() {
    HZ_PROFILE_FUNCTION();
    s_Data = new Renderer2DData();
    s_Data->QuadVertexArray = Hazel::VertexArray::Create();
    // 创立极点缓冲,依照预设的最大值MaxVertices来请求空间
    s_Data->QuadVertexBuffer = VertexBuffer::Create(s_Data->MaxVertices * sizeof(QuadVertex));
    // 设置极点数据的布局特点,依照position、color、texCoord的顺序排列
    s_Data->QuadVertexBuffer->SetLayout(
            {
                    {ShaderDataType::Float3, "a_Position"},
                    {ShaderDataType::Float4, "a_Color"},
                    {ShaderDataType::Float2, "a_TexCoord"}
            }
            );
    // 极点缓冲绑定到极点数组中,s_Data->QuadVertexBuffer在GPU内存中,现在只有内存占用无数据
    s_Data->QuadVertexArray->AddVertexBuffer(s_Data->QuadVertexBuffer);
    // 创立CPU空间的极点数据,也是依照最大预设置来创立
    s_Data->QuadVertexBufferBase = new QuadVertex[s_Data->MaxVertices];
    // 创立索引数组
    uint32_t* quadIndices = new uint32_t[s_Data->MaxIndices];
    uint32_t  offset = 0;
    // 1个矩形对应4个极点,对应6个索引值,所以offset距离为4,indice距离为6
    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;
    // 创立1*1的纯色纹路
    s_Data->WhiteTexture = Texture2D::Create(1, 1);
    // 纹路颜色为白色
    uint32_t whiteTextureData = 0xffffffff;
    s_Data->WhiteTexture->SetData(&whiteTextureData, sizeof(uint32_t));
    s_Data->TextureShader = Shader::Create("../assets/shaders/Texture.glsl");
    s_Data->TextureShader->Bind();
    s_Data->TextureShader->SetInt("u_Texture", 0);
}
  1. 新的DrawQuad方法,draw的时候只增加数据,真实的调用在Flush()函数中
// 每个矩形对应4个极点,即每制作一个矩形,要增加4个极点到s_Data中,用s_Data->QuadVertexBufferPtr符号end的地址
void Renderer2D::DrawQuad(const glm::vec3 &position, const glm::vec2 &size, const glm::vec4 &color) {
    HZ_PROFILE_FUNCTION();
    s_Data->QuadVertexBufferPtr->Position = position;
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {0.0f, 0.0f};
    s_Data->QuadVertexBufferPtr++;
    s_Data->QuadVertexBufferPtr->Position = {position.x + size.x, position.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {1.0f, 0.0f};
    s_Data->QuadVertexBufferPtr++;
    s_Data->QuadVertexBufferPtr->Position = {position.x+size.x, position.y+size.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {1.0f, 1.0f};
    s_Data->QuadVertexBufferPtr++;
    s_Data->QuadVertexBufferPtr->Position = {position.x, position.y + size.y, 0.0f};
    s_Data->QuadVertexBufferPtr->Color = color;
    s_Data->QuadVertexBufferPtr->TexCoord = {0.0f, 1.0f};
    s_Data->QuadVertexBufferPtr++;
    s_Data->QuadIndexCount += 6;
}
  1. 增加Flush环节
void Renderer2D::Flush()
{
    RenderCommand::DrawIndexed(s_Data->QuadVertexArray, s_Data->QuadIndexCount);
}
  1. EndScene()结尾增加Flush()

GL相关的操作滞后在EndScene()中合批处理。这个机遇的选择要放到一切的数据增加完毕之后。

void Renderer2D::EndScene() {
    HZ_PROFILE_FUNCTION();
    // 一次性设置一切极点数据
    uint32_t dataSize = (uint8_t*)s_Data->QuadVertexBufferPtr - (uint8_t*)s_Data->QuadVertexBufferBase;
    s_Data->QuadVertexBuffer->SetData(s_Data->QuadVertexBufferBase, dataSize);
    // 一切的draw完毕后,调用Flush(),触发真实的GL制作
    Flush();
}

OpenGLBuffer支撑动态增加数据

Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLBuffer.h

// 新增结构函数中,不需要设置数据指针
OpenGLVertexBuffer(uint32_t size);
// 支撑动态的设置极点数据
virtual void SetData(const void* data, uint32_t size) override;

Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLBuffer.cpp

OpenGLVertexBuffer::OpenGLVertexBuffer(uint32_t size) {
    HZ_PROFILE_FUNCTION();
    glGenBuffers(1, &m_RendererID);
    glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
    glBufferData(GL_ARRAY_BUFFER, size, nullptr, GL_DYNAMIC_DRAW);
}
void OpenGLVertexBuffer::SetData(const void *data, uint32_t size) {
    glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
    glBufferSubData(GL_ARRAY_BUFFER, 0, size, data);
}

gl_dynamic_draw和gl_static_draw的差异参阅:
computergraphics.stackexchange.com/questions/5…

Texture.glsl适配

这一章节中还不支撑旋转和纹路的处理,先注掉了u_Transform、u_Texture等特点

Sandbox/assets/shaders/Texture.glsl

// Basic Texture Shader
#type vertex
#version 330 core
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;
layout(location = 2) in vec2 a_TexCoord;
uniform mat4 u_ViewProjection;
out vec4 v_Color;
out vec2 v_TexCoord;
void main()
{
    v_Color = a_Color;
	v_TexCoord = a_TexCoord;
//	 gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0);
	gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
}
#type fragment
#version 330 core
layout(location = 0) out vec4 color;
in vec4 v_Color;
in vec2 v_TexCoord;
uniform vec4 u_Color;
uniform float u_TilingFactor;
uniform sampler2D u_Texture;
void main()
{
	// color = texture(u_Texture, v_TexCoord * u_TilingFactor) * u_Color;
	color = v_Color;
}

切换Layer

我们根据Sandbox2D来完成demo,Layer切换到Sandbox2D

Sandbox/src/SandBoxApp.cpp

Sandbox(){
//        PushOverlay(new ExampleLayer());
//        PushLayer(new GameLayer());
      PushOverlay(new Sandbox2D());
}

Sandbox2D中更新制作的逻辑,制作两个矩形,一个偏赤色,一个偏蓝色

Sandbox/src/Sandbox2D.cpp

void Sandbox2D::OnUpdate(Hazel::Timestep ts) {
    HZ_PROFILE_FUNCTION();
    // Update
    ...
    {
        HZ_PROFILE_SCOPE("Renderer Draw");
        Hazel::Renderer2D::BeginScene(m_CameraController.GetCamera());
//        Hazel::Renderer2D::DrawQuad({-1.0f, 0.0f}, {0.8f, 0.8f}, glm::radians(-45.0f), {0.8f, 0.2f, 0.3f, 1.0f});
        Hazel::Renderer2D::DrawQuad({-1.0f, 0.0f}, {0.8f, 0.8f}, {0.8f, 0.2f, 0.3f, 1.0f});
        Hazel::Renderer2D::DrawQuad({0.5f, -0.5f}, {0.5f, 0.75f}, {0.2f, 0.3f, 0.8f, 1.0f});
        Hazel::Renderer2D::EndScene();
    }

其他的代码修改

为了适配批烘托,还有一些小的代码修改,纷歧一讲解了,参阅:github.com/summer-go/H…

假如运转正常能看到两个矩形图画,一红一蓝。

游戏引擎从零开始(35)-Batch Rendering(1)

三、代码 & 总结

本次代码修改参阅: github.com/summer-go/H…

批烘托无论是实践开发,仍是面试中,都是十分根底且重要的技术点。笔者本年求职中大部分图形岗位都问到了这个点,比如会问,合批有哪些限制条件?