1. 渲染上下文(Rendering Context)
渲染上下文是构建渲染接口时最基础、最关键的一步,它负责绑定图形 API 到窗口系统、管理渲染状态、初始化图形设备等底层资源。
渲染上下文就是你和 GPU 沟通的“通道”,你必须先建立这个通道,才能开始绘制任何东西。
渲染上下文应该满足以下功能:
- 与窗口句柄绑定:将 OpenGL/Vulkan/DirectX 的输出绑定到一个平台窗口(如 Windows HWND、GLFW 窗口等)。
- 初始化图形API设备:加载 OpenGL 函数(如使用 GLAD)、创建 Vulkan 实例与设备。
- 负责上下文生命周期管理:创建、切换、销毁、失效处理。
通用的上下文虚基类:
class GraphicsContext
{
public:
virtual void Init() = 0;
virtual void SwapBuffers() = 0;
};
对于一个渲染接口而言,他需要满足多种不同的图形API需求,然而不同的图形API所提供的创建渲染上下文的方式有所不同,所以对于每个图形API,都要有自己的上下文类。
OpenGL的上下文类:
class OpenGLContext : public GraphicsContext
{
public:
OpenGLContext(GLFWwindow* windowHandle);
virtual void Init() override;
virtual void SwapBuffers() override;
private:
GLFWwindow* m_WindowHandle;
};
实现:
void OpenGLContext::Init()
{
glfwMakeContextCurrent(m_WindowHandle);
int status = gladLoadGLLoader((GLADloadproc)glfwGetProcAddress);
}
void OpenGLContext::SwapBuffers()
{
glfwSwapBuffers(m_WindowHandle);
}
对于OpenGL的上下文类OpenGLContext而言,它需要接收一个已被创建的窗口句柄,然后在初始化函数Init()中设置上下文,并且提供一个使用双缓冲(Double Buffering)渲染的函数SwapBuffers()。
2. Shader
使用一个单独的类Shader对顶点着色器和片段着色器进行编译。
创建着色器并编译:
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
const GLchar* source = vertexSrc.c_str();
glShaderSource(vertexShader, 1, &source, 0);
glCompileShader(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
source = fragmentSrc.c_str();
glShaderSource(fragmentShader, 1, &source, 0);
glCompileShader(fragmentShader);
将顶点着色器和片段着色器编译成功后,将它们链接在一起形成一个程序(Object):
m_RendererID = glCreateProgram();
GLuint program = m_RendererID;
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glDetachShader(program, vertexShader);
glDetachShader(program, fragmentShader);
在类Application中创建类Shader的Unique Pointer变量m_Shader,然后使用函数reset()初始化:
std::unique_ptr<Shader> m_Shader;
m_Shader.reset(new Shader(vertexSrc, fragmentSrc));
3. Buffer
目前为止,我们需要两种缓冲区:顶点缓冲区(Vertex Buffer)和索引缓冲区(Index Buffer)。
顶点缓冲区:存储顶点属性(如位置、颜色、法线、纹理坐标等)的连续内存。
索引缓冲区:存储画图顺序,指示如何使用顶点缓冲区中的数据组成图元(如三角形)。
举个例子:比如我们要画一个正方形,而正方形是由两个三角形组成的,所以画一个正方形就变成了画两个三角形并将它们“拼装”在一起。
我们在数组vertices[]中传入4行顶点,用于表示正方形的顶点位置。然后使用数组indices[]中传入2行顶点,每一行3个数字,表示:每一个三角形使用哪个顶点。
float vertices[] = {
// positions
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.5f, 0.5f, 0.0f,
-0.5f, 0.5f, 0.0f
};
unsigned int indices[] = {
0, 1, 2,
2, 3, 0
};
接下来让我们聊一聊在抽象渲染接口中该如何定义Buffer。
为了让游戏引擎支持多种图形API,仅仅创建一个Buffer类是不够的,因为每一种图形API创建Buffer的方式都不一样,因此我们需要为每一种图形API都创建一个单独的Buffer类,如:类OpenGLBuffer。
OpenGLVertexBuffer::OpenGLVertexBuffer(float* vertices, uint32_t size)
{
glCreateBuffers(1, &m_RendererID);
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
glBufferData(GL_ARRAY_BUFFER, size, vertices, GL_STATIC_DRAW);
}
OpenGLVertexBuffer::~OpenGLVertexBuffer()
{
glDeleteBuffers(1, & m_RendererID);
}
void OpenGLVertexBuffer::Bind() const
{
glBindBuffer(GL_ARRAY_BUFFER, m_RendererID);
}
void OpenGLVertexBuffer::Unbind() const
{
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
在类Application中,依旧使用Unique Pointer来创建Vertex Buffer和Index Buffer的对象,并且使用函数reset()来初始化。
std::unique_ptr<VertexBuffer> m_VertexBuffer;
std::unique_ptr<IndexBuffer> m_IndexBuffer;
m_VertexBuffer.reset(VertexBuffer::Create(vertices, sizeof(vertices)));
m_IndexBuffer.reset(IndexBuffer::Create(indices, sizeof(indices) / sizeof(uint32_t)));
4. Buffer Layout
顶点缓冲布局系统是渲染抽象接口中很重要的部分,它用于描述:一组顶点数据在内存中的布局结构,从而使渲染API(如 OpenGL)能正确解释和读取顶点缓冲数据。
对于一个传入的顶点数据而言,它可能在同一个节点中表达了多个内容,比图:位置,颜色,归一化等。所以使用一个类Layout来解释这个顶点数据的各个数字都代表什么非常重要。
比如,我们定义一个Vertex Array:
float vertices[3 * 7] = {
//X Y Z R G B A
-0.5f, -0.5f, 0.0f, 0.8f, 0.2f, 0.8f, 1.0f,
0.5f, -0.5f, 0.0f, 0.2f, 0.3f, 0.8f, 1.0f,
0.0f, 0.5f, 0.0f, 0.8f, 0.8f, 0.2f, 1.0f
};
如果我们将它直接传入GPU,然后用OpenGL语言来解析它(比如:glVertexAttribPointer),显然它是无法得知这三组数字的含义的,因为它可能代表位置,也可能代表颜色。况且,每一类数据所使用的数据类型也不同:位置需要使用vec3来表示x, y, z三个值,而颜色则需要vec4来表示R, G, B, A。
使用一个Layout类可以模块化地自动解决这个问题,而不再需要程序员手动输入类型。
class BufferLayout
{
public:
BufferLayout() {}
BufferLayout(const std::initializer_list<BufferElement>& elements) : m_Elements(elements)
{
CalculateOffsetsAndStride();
}
private:
void CalculateOffsetsAndStride()
{
uint32_t offset = 0;
m_Stride = 0;
for (auto& element : m_Elements)
{
element.Offset = offset;
offset += element.Size;
m_Stride += element.Size;
}
}
private:
std::vector<BufferElement> m_Elements;
uint32_t m_Stride = 0;
};
在上述代码中,使用数据结构vector来记录结构体Buffer Element,每一个Buffer Element都保存着数据类型和数据名称,以用于步长(stride)等必要信息的计算。
定义并设置一个Vertex Buffer的Layout信息:
BufferLayout layout = {
{ ShaderDataType::Float3, "a_Position" },
{ ShaderDataType::Float4, "a_Color" }
};
m_VertexBuffer->SetLayout(layout);
5. Vertex Array
6. 渲染器抽象(Renderer Abstraction)
渲染器抽象是图形引擎架构中最关键的组成部分之一。由于不同图形 API(如 OpenGL、DirectX、Vulkan、Metal)在渲染流程、资源绑定、命令执行等方面存在显著差异,我们需要对底层图形 API 的调用进行统一封装。通过定义一个通用的、平台无关的渲染接口(例如 RendererAPI),并为每种图形 API 提供具体实现,我们可以在顶层通过一致的逻辑描述渲染任务,底层再根据当前平台自动转换为相应图形 API 的指令,从而实现跨平台、可扩展的渲染架构设计。这种设计不仅提高了代码可维护性与可移植性,也为后续支持多后端渲染器(如从 OpenGL 迁移到 Vulkan)奠定了良好基础。
渲染器类Renderer:
class Renderer
{
public:
static void BeginScene();
static void EndScene();
static void Submit(const std::shared_ptr<VertexArray>& vertexArray);
inline static API GetAPI() { return RendererAPI::GetAPI(); }
};
- 函数BeginScene():每一帧的入口函数,通常会在这里设置摄像机视图投影矩阵;设置全局 shader uniforms,比如光照、时间、屏幕大小等;和初始化帧缓冲、清屏操作等。
- 函数EndScene():结束当前帧渲染的函数,通常用于提交绘制命令队列,或一些收尾工作(比如交换缓冲区)。
- 函数Submit():真正执行渲染的地方,将顶层逻辑转换为底层指令。他会绑定对应的VAO,告诉底层图形API你要画什么并且调用该API的绘图代码。
渲染接口类RendererAPI:
class RendererAPI
{
public:
virtual void SetClearColor(const glm::vec4& color) = 0;
virtual void Clear() = 0;
virtual void DrawIndexed(const std::shared_ptr<VertexArray>& vertexArray) = 0;
inline static API GetAPI() { return s_API; }
private:
static API s_API;
};
这是一个抽象基类,需要建立额外的子类来实现具体逻辑,比如:OpenGLRendererAPI,里面封装的就是基于OpenGL的图形语言,在未来如果支持Vulkan或DirectX等接口,也需要编写相应的RendererAPI类。
在类OpenGLRendererAPI中,可以这样实现底层逻辑:
void OpenGLRendererAPI::SetClearColor(const glm::vec4& color)
{
glClearColor(color.r, color.g, color.b, color.a);
}
void OpenGLRendererAPI::Clear()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void OpenGLRendererAPI::DrawIndexed(const std::shared_ptr<VertexArray>& vertexArray)
{
glDrawElements(GL_TRIANGLES, vertexArray->GetIndexBuffer()->GetCount(), GL_UNSIGNED_INT, nullptr);
}
那么问题来了,类Renderer实现了渲染器基本功能,类Renderer负责实现各个图形API的绘制流程逻辑,游戏引擎怎么知道自己需要使用哪个图形API呢?
这就需要另一个类RenderCommand来负责:
class RenderCommand
{
public:
inline static void SetClearColor(const glm::vec4& color)
{
s_RendererAPI->SetClearColor(color);
}
inline static void Clear()
{
s_RendererAPI->Clear();
}
inline static void DrawIndexed(const std::shared_ptr<VertexArray>& vertexArray)
{
s_RendererAPI->DrawIndexed(vertexArray);
}
private:
static RendererAPI* s_RendererAPI;
};
在这个类中,我们使用多态定义了RendererAPI的具体实现方式,并且调用统一接口来完成背景颜色处理,图形的绘制等功能,从而达到跨平台和高度模块化的要求。

Leave a comment