一、前言
什么是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
- Renderer2D增加Flush接口
c++
static void Flush();
Sandbox/Hazel/src/Hazel/Renderer/Renderer2D.h
- 增加QuadVertex数据结构,描述单点的数据属性
c++
struct QuadVertex
{
glm::vec3 Position;
glm::vec4 Color;
glm::vec2 TexCoord;
};
- 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;
};
- 初始化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);
}
- 新的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;
}
- 增加Flush环节
c++
void Renderer2D::Flush()
{
RenderCommand::DrawIndexed(s_Data->QuadVertexArray, s_Data->QuadIndexCount);
}
- 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...
批渲染无论是实际开发,还是面试中,都是非常基础且重要的技术点。笔者今年求职中大部分图形岗位都问到了这个点,比如会问,合批有哪些限制条件?