游戏引擎从零开始(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...

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

相关推荐
一名用户15 小时前
unity实现自定义粒子系统
c#·unity3d·游戏开发
技术小甜甜3 天前
【Blender Texture】【游戏开发】高质感 Blender 4K 材质资源推荐合集 —— 提升场景真实感与美术表现力
blender·游戏开发·材质·texture
Thomas游戏开发3 天前
Unity3D TextMeshPro终极使用指南
前端·unity3d·游戏开发
Thomas游戏开发4 天前
Unity3D 逻辑代码性能优化策略
前端框架·unity3d·游戏开发
byxdaz4 天前
Qt OpenGL 3D 编程入门
qt·opengl
Thomas游戏开发5 天前
Unity3D HUD高性能优化方案
前端框架·unity3d·游戏开发
陈哥聊测试6 天前
游戏公司如何同时管好上百个游戏项目?
游戏·程序员·游戏开发
byxdaz6 天前
Qt OpenGL 相机实现
opengl
一名用户7 天前
unity随机生成未知符号教程
c#·unity3d·游戏开发
Be_Somebody12 天前
计算机图形学——Games101深度解析_第二章
游戏开发·计算机图形学·games101