渲染工作流
完整的游戏引擎中,渲染流程一般拆分为两大部分,一部分在业务层,生成渲染的数据、更新状态,另一部分在真正的渲染层,和图形API打交道。
第一部分称为Renderer,第二部分称为RenderCommand,为了跨平台,对图形API还封装了一层RendererAPI,调用关系如下图:
Renderer属于上层的API,已经看不到图形API的影子了,一般在主线程调用,RendererCommand在子线程中循环调用,这就是最基本的"多线程渲染"实现,如下图所示:
考虑分层解耦,成熟的引擎中至少有两个线程,一个主线程,也叫游戏线程,更新场景状态(光照、camera、gameObject),另一个是渲染线程,死循环中执行图形命令。
分层有另一个重要的原因,OpenGL的API调用是阻塞式的,放到子线程中,是基于性能的考虑。实际上有些商业引擎中,渲染线程会再拆分成两种线程,一种用于生成渲染的指令,另一种才是真正的调用图形API。注意!这里我说的是"种",可能会存在线程池,更细粒度拆分每个阶段的任务。
按照RendererAPI->RenderCommand->Renderer的顺序,我们从下往上讲代码的实现。
接口RendererAPI
RendererAPI这层,是为了隔离图形API,实现平台切换。暂时仅封装了Application中的用到的API,后续逐步完善。
声明接口RendererAPI
Renderer/RendererAPI.h
c++
#pragma once
#include <memory>
#include <glm/glm.hpp>
#include "VertexArray.h"
namespace Hazel {
class RendererAPI {
public:
enum class API {
None = 0, OpenGL = 1
};
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;
};
}
RendererAPI.cpp中定义RendererAPI::API
c++
#include "RendererAPI.h"
namespace Hazel {
RendererAPI::API RendererAPI::s_API = RendererAPI::API::OpenGL;
}
实现-OpenGLRendererAPI
RendererAPI接口的实现: Platform/OpenGL/OpenGLRendererAPI.h
c++
#pragma once
#include "Renderer/RendererAPI.h"
namespace Hazel {
class OpenGLRendererAPI : public RendererAPI{
virtual void SetClearColor(const glm::vec4& color) override;
virtual void Clear() override;
virtual void DrawIndexed(const std::shared_ptr<VertexArray>& vertexArray) override;
};
}
注意,RendererAPI类中的函数定义为纯虚函数,否则编译会报错找不到定义,必须在末尾加上"=0"指明为纯虚函数。也有可能在其他平台的编译器上不会报错。
OpenGLRendererAPI.cpp
c++
#include "OpenGLRendererAPI.h"
#include <glad/glad.h>
namespace Hazel{
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);
}
}
渲染命令(RenderCommand)
平台API的切换由RenderCommand控制,因为按照引擎的设计,在RenderCommand层才真正与图形驱动打交道,是一个多态的实现。
Renderer/RenderCommand.h
c++
#pragma once
#include <glm/glm.hpp>
#include "Renderer/RendererAPI.h"
#include "VertexArray.h"
namespace Hazel {
class RenderCommand {
public:
static inline void SetClearColor(const glm::vec4& color) {
s_RendererAPI->SetClearColor(color);
} ;
static inline void Clear() {
s_RendererAPI->Clear();
}
static inline void DrawIndexed(const std::shared_ptr<VertexArray>& vertexArray) {
s_RendererAPI->DrawIndexed(vertexArray);
}
private:
static RendererAPI* s_RendererAPI;
};
}
Renderer/RenderCommand.cpp
c++
#include "RenderCommand.h"
#include "Platform/OpenGL/OpenGLRendererAPI.h"
namespace Hazel{
RendererAPI* RenderCommand::s_RendererAPI = new OpenGLRendererAPI();
}
新的渲染器(Renderer)
这里需要重点说明下,主流的渲染器接口设计中,BeginScene()、EndScene()成对出现,每个渲染循环的开始和结束成对调用,和线程锁一样, 当然每个引擎中的叫法、名称会不一样。
Begin和End的调用一般会处理锁同步,防止多线程渲染状态异常,另外,Begin还用于check渲染器状态,如果上一帧渲染线程任务还没处理完,那么当前帧就要考虑跳过一帧,等一等渲染线程。
Renderer类中删掉了RendererAPI的逻辑,新增加了三个函数接口,其中submit用于提交渲染指令,底层直接调用了OpenGL的API,完整的实现应该是提交一个包含数据的渲染指令到队列中。
现在BeginScene、EndScene只是一个空实现,后续涉及到相关逻辑会逐步完善。
Hazel/Renderer/Renderer.h
c++
#pragma once
#include "RenderCommand.h"
namespace Hazel {
class Renderer
{
public:
static void BeginScene();
static void EndScene();
static void Submit(const std::shared_ptr<VertexArray>& vertexArray);
inline static RendererAPI::API GetAPI() { return RendererAPI::GetAPI(); }
};
}
Renderer.cpp中预留了BeginScene()和EndScene()的位置,暂时为空实现。 Hazel/Renderer/Renderer.cpp
c++
namespace Hazel {
void Renderer::BeginScene()
{
}
void Renderer::EndScene()
{
}
void Renderer::Submit(const std::shared_ptr<VertexArray>& vertexArray)
{
vertexArray->Bind();
RenderCommand::DrawIndexed(vertexArray);
}
}
调整代码
调整代码:Buffer.cpp、VertexArray中有RendererAPI相关的调用,换到了RendererAPI的类中,需要替换。
按照新的Renderer设计,替换Application中的渲染的逻辑
c++
#include "Hazel/Renderer/Renderer.h"
#include "Input.h"
...
while (m_Running)
{
RenderCommand::SetClearColor({0.45f, 0.55f, 0.60f, 1.00f});
RenderCommand::Clear();
Renderer::BeginScene();
m_BlueShader->Bind();
Renderer::Submit(m_SquareVA);
m_Shader->Bind();
Renderer::Submit(m_VertexArray);
Renderer::EndScene();
...
...
注意,此时,在Application这一层,去掉了glad(OpenGL)的直接引用,整个OpenGL已经下沉到platform的实现类中了。
如果代码没有问题,应该能正常渲染出一个三角形,和上一节一样,没有变化
完整代码&总结
网络有点问题,代码暂时提交不了,下一篇文章中会附上这次的修改。
这次的代码不多但是涉及到游戏引擎中很重要的概念:多线程渲染。理解了Renderer、RenderCommand、RendererAPI的分层理念,以后你看其他游戏引擎,都可以参考这个思路去捋代码,从入口开始,找到游戏线程、渲染线程以及驱动线程,理解了数据的传递流程,就理解了游戏引擎的渲染逻辑。