一、前言
什么是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接口
static void Flush();
Sandbox/Hazel/src/Hazel/Renderer/Renderer2D.h
- 增加QuadVertex数据结构,描绘单点的数据特点
struct QuadVertex
{
glm::vec3 Position;
glm::vec4 Color;
glm::vec2 TexCoord;
};
- 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;
};
- 初始化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);
}
- 新的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;
}
- 增加Flush环节
void Renderer2D::Flush()
{
RenderCommand::DrawIndexed(s_Data->QuadVertexArray, s_Data->QuadIndexCount);
}
- 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…
假如运转正常能看到两个矩形图画,一红一蓝。
三、代码 & 总结
本次代码修改参阅: github.com/summer-go/H…
批烘托无论是实践开发,仍是面试中,都是十分根底且重要的技术点。笔者本年求职中大部分图形岗位都问到了这个点,比如会问,合批有哪些限制条件?