1. 前言
本章节是在整个项目完成结束之后,回来对修改的细节进行订正。
2. 顶点数据
在游戏引擎中,我们实现了三种基本的图形,分别是:线段,三角形,圆形。在渲染器类中,我们定义了三个结构体,用来存储图形的顶点数据,之后会被上传到GPU的顶点缓冲区。
线段顶点数据:
struct LineVertex
{
glm::vec3 Position;
glm::vec4 Color;
// Editor-only
int EntityID;
};
圆形顶点数据:
struct CircleVertex
{
glm::vec3 WorldPosition; // 世界空间位置
glm::vec3 LocalPosition; // 局部坐标位置
glm::vec4 Color;
float Thickness;
float Fade;
// Editor-only
int EntityID;
};
四边形顶点数据:
struct QuadVertex
{
glm::vec3 Position;
glm::vec4 Color;
glm::vec2 TexCoord;
float TexIndex;
float TilingFactor;
// Editor-only
int EntityID;
};
3. 渲染器数据
(1)预设常量
规定了最大支持的四边形数量,最大顶点数量,最大索引数量和每个批次可绑定的最大纹理数。
static const uint32_t MaxQuads = 20000;
static const uint32_t MaxVertices = MaxQuads * 4;
static const uint32_t MaxIndices = MaxQuads * 6;
static const uint32_t MaxTextureSlots = 32;
(2)四边形数据
Ref<VertexArray> QuadVertexArray; // 四边形VAO
Ref<VertexBuffer> QuadVertexBuffer; // 四边形VBO
Ref<Shader> QuadShader; // 着色器
Ref<Texture2D> WhiteTexture; // 白色纹理
uint32_t QuadIndexCount = 0; // 批处理:当前批次已使用索引数量
QuadVertex* QuadVertexBufferBase = nullptr; // 批处理:指向顶点缓冲区起始位置
QuadVertex* QuadVertexBufferPtr = nullptr; // 批处理:指向当前写入顶点的位置
(3)圆形数据
Ref<VertexArray> CircleVertexArray;
Ref<VertexBuffer> CircleVertexBuffer;
Ref<Shader> CircleShader;
uint32_t CircleIndexCount = 0;
CircleVertex* CircleVertexBufferBase = nullptr;
CircleVertex* CircleVertexBufferPtr = nullptr;
(4)线段数据
Ref<VertexArray> LineVertexArray;
Ref<VertexBuffer> LineVertexBuffer;
Ref<Shader> LineShader;
LineVertex* LineVertexBufferBase = nullptr;
LineVertex* LineVertexBufferPtr = nullptr;
uint32_t LineVertexCount = 0;
float LineWidth = 2.0f;
4. 初始化渲染器
(1)创建一个公共IBO
用于存储所有四边形的索引,因为每个四边形是由两个三角形组成的,两个三角形会有六个点,其中有四个点是可以优化成两个点的,所以我们需要一个IBO绑定到VAO去实现复用。
首先开辟一个最大索引数长度的数组,对数组中的每一个元素进行初始化,然后调用类IndexBuffer(文件OpenGLBuffer.cpp)的接口Create(),传入数组指针和要创建的索引数量,创建IBO。
uint32_t* quadIndices = new uint32_t[s_Data.MaxIndices];
uint32_t offset = 0;
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);
delete[] quadIndices;
(2)创建VAO和VBO
s_Data.QuadVertexArray = VertexArray::Create();
s_Data.QuadVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(QuadVertex));
s_Data.QuadVertexBuffer->SetLayout({
{ ShaderDataType::Float3, "a_Position" },
{ ShaderDataType::Float4, "a_Color" },
{ ShaderDataType::Float2, "a_TexCoord" },
{ ShaderDataType::Float, "a_TexIndex" },
{ ShaderDataType::Float, "a_TilingFactor" },
{ ShaderDataType::Int, "a_EntityID" }
});
s_Data.QuadVertexArray->AddVertexBuffer(s_Data.QuadVertexBuffer);
s_Data.QuadVertexArray->SetIndexBuffer(quadIB);
s_Data.QuadVertexBufferBase = new QuadVertex[s_Data.MaxVertices];
无需担心有大量顶点的3D模型
什么是纹理slot?
批处理渲染器(Batch Renderer)
动画系统
UI Layout
为什么渲染器通常是静态的?
渲染器不是资源本身,而是对 GPU 命令的一组全局化封装。
渲染器(Renderer)本质上是:
- 一个 功能模块,不需要拥有状态(或只拥有一个全局状态)
- 用于 发出绘制命令(Draw Call),而非描述场景数据
- 不需要创建多个实例(一次场景渲染只有一个流程)
因此,它被设计成一个静态类,或被包装进一个全局访问的 API(如 Renderer2D::DrawQuad(...)),符合“只需一次全局设置”的设计模式。
事实上,完全可以将2D渲染器看作是执行GPU渲染指令的包装器,它是对底层图形API的封装,负责调用其中的绘制函数,而自己则不拥有纹理或网格,它只负责调用命令。
它拥有的数据存储必须是单例的,因为一次只能由一个场景,一次只能进行一个2D渲染。它并不承载OpenGL实例
渲染器初始化流程顺序(VAO + VBO + IBO):
- 创建 VAO(顶点数组对象)
- 用于“记录”绑定关系。
- 创建并绑定 VBO(顶点缓冲对象)
- 用来存储顶点数据(如位置、颜色、UV 等)
- 设置顶点布局(告诉 GPU 每个顶点的数据结构)
- 将 VBO 添加到 VAO 中
- 这一步建立“VAO 记录下当前绑定的 VBO 与其布局”的关系
- 创建并绑定 IBO(索引缓冲对象)
- 用来描述顶点的绘制顺序,提升效率,避免顶点重复
- 把 IBO 设置到 VAO 上
- 同样是为了让 VAO 记录这个“索引缓冲”信息

Leave a comment