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

一、前言

什么是Batch Rendering

打个比方,一个人骑一辆摩托车上路,10个人就需要骑10辆摩托车,这是普通的渲染方式,每个人上路都要消耗一次摩托车资源(GPU资源)

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

实际工程中,因为GPU内存不是无限大的,且GPU的并发也是有上限的,不能一次存储一个非常大的顶点数据,所以合批也是有上限的。那么问题来了,一次draw中,到底绘制多少顶点数据才合适呢?不知道!!得根据每台机器的硬件资源、图形驱动实测才能大致确认。根据笔者的经验,这个值还是挺大的,至少是十几万的量级。

简单总结下批渲染的知识点:

Batch Rendering的优势

  • 减少顶点数据传输开销
  • 减少状态变更开销

批渲染应注意的地方

  • 相同的顶点格式
  • 相同的shader及参数
  • 相同的渲染状态(混合模式、深度测试、剔除模式)

二、批渲染实现

这篇文章,我们仅实现最基础的矩形渲染合批,旋转和纹理切换在下一篇文章介绍。

修改点如下两张图所示,批渲染流程中,draw只是新增加数据,在EndScene()中调用Flush()才真的去绘制。

普通渲染:

批渲染:

Renderer2D支持批渲染

主要改造在Renderer2D类中,捋清楚了Renderer2D的代码逻辑,就能理解批渲染的实现流程。

改造DrawQuad,仅增加数据,不调draw命令,等到调用flush时,才真正的调用drawElement,一次绘制所有的数据。

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

  1. Renderer2D增加Flush接口
c++ 复制代码
static void Flush();

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

  1. 增加QuadVertex数据结构,描述单点的数据属性
c++ 复制代码
struct QuadVertex
{
    glm::vec3 Position;
    glm::vec4 Color;
    glm::vec2 TexCoord;
};
  1. Renderer2DStorage更名为Renderer2DData,增加了批渲染需要的属性
c++ 复制代码
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中的顶点数组内存

c++ 复制代码
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()函数中
c++ 复制代码
// 每个矩形对应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环节
c++ 复制代码
void Renderer2D::Flush()
{
    RenderCommand::DrawIndexed(s_Data->QuadVertexArray, s_Data->QuadIndexCount);
}
  1. EndScene()末尾增加Flush()

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

c++ 复制代码
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

c++ 复制代码
// 新增构造函数中,不需要设置数据指针
OpenGLVertexBuffer(uint32_t size);

// 支持动态的设置顶点数据
virtual void SetData(const void* data, uint32_t size) override;

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

c++ 复制代码
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

c++ 复制代码
// 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

c++ 复制代码
Sandbox(){
//        PushOverlay(new ExampleLayer());
//        PushLayer(new GameLayer());
      PushOverlay(new Sandbox2D());

}

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

Sandbox/src/Sandbox2D.cpp

c++ 复制代码
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...

如果运行正常能看到两个矩形图案,一红一蓝。

三、代码 & 总结

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

批渲染无论是实际开发,还是面试中,都是非常基础且重要的技术点。笔者今年求职中大部分图形岗位都问到了这个点,比如会问,合批有哪些限制条件?

相关推荐
凌云行者3 天前
OpenGL入门004——使用EBO绘制矩形
c++·cmake·opengl
Thomas_YXQ3 天前
Unity3D中管理Shader效果详解
开发语言·游戏·unity·unity3d·游戏开发
闲暇部落3 天前
Android OpenGL ES详解——模板Stencil
android·kotlin·opengl·模板测试·stencil·模板缓冲·物体轮廓
凌云行者5 天前
OpenGL入门003——使用Factory设计模式简化渲染流程
c++·cmake·opengl
凌云行者6 天前
OpenGL入门002——顶点着色器和片段着色器
c++·cmake·opengl
Ljw...6 天前
C++游戏开发
c++·c·游戏开发
闲暇部落7 天前
Android OpenGL ES详解——裁剪Scissor
android·kotlin·opengl·窗口·裁剪·scissor·视口
彭祥.12 天前
点云标注工具开发记录(四)之点云根据类别展示与加速渲染
pyqt·opengl
闲暇部落15 天前
android openGL ES详解——缓冲区VBO/VAO/EBO/FBO
kotlin·opengl·缓冲区·fbo·vbo·vao·ebo
北冥没有鱼啊16 天前
ue5 扇形射线检测和鼠标拖拽物体
游戏·ue5·ue4·游戏开发·虚幻