WAYNETS.ORG

Game and Program

2D 渲染器

作者:

发表于

Context Polling System Post-Processing Renderer

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):

  1. 创建 VAO(顶点数组对象)
    • 用于“记录”绑定关系。
  2. 创建并绑定 VBO(顶点缓冲对象)
    • 用来存储顶点数据(如位置、颜色、UV 等)
    • 设置顶点布局(告诉 GPU 每个顶点的数据结构)
  3. 将 VBO 添加到 VAO 中
    • 这一步建立“VAO 记录下当前绑定的 VBO 与其布局”的关系
  4. 创建并绑定 IBO(索引缓冲对象)
    • 用来描述顶点的绘制顺序,提升效率,避免顶点重复
  5. 把 IBO 设置到 VAO 上
    • 同样是为了让 VAO 记录这个“索引缓冲”信息

Leave a comment