游戏引擎从零开始(18)-渲染工作流与任务提交

渲染工作流

完整的游戏引擎中,渲染流程一般拆分为两大部分,一部分在业务层,生成渲染的数据、更新状态,另一部分在真正的渲染层,和图形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的分层理念,以后你看其他游戏引擎,都可以参考这个思路去捋代码,从入口开始,找到游戏线程、渲染线程以及驱动线程,理解了数据的传递流程,就理解了游戏引擎的渲染逻辑。

相关推荐
whoarethenext1 分钟前
C++ OpenCV 学习路线图
c++·opencv·学习
闻缺陷则喜何志丹11 分钟前
【强连通分量 缩点 拓扑排序】P3387 【模板】缩点|普及+
c++·算法·拓扑排序·洛谷·强连通分量·缩点
hutaotaotao43 分钟前
c++中的输入输出流(标准IO,文件IO,字符串IO)
c++·io·fstream·sstream·iostream
AL流云。1 小时前
【优选算法】C++滑动窗口
数据结构·c++·算法
qq_429879672 小时前
省略号和可变参数模板
开发语言·c++·算法
CodeWithMe3 小时前
【C/C++】std::vector成员函数清单
开发语言·c++
uyeonashi3 小时前
【QT控件】输入类控件详解
开发语言·c++·qt
zh_xuan7 小时前
c++ 单例模式
开发语言·c++·单例模式
利刃大大10 小时前
【在线五子棋对战】二、websocket && 服务器搭建
服务器·c++·websocket·网络协议·项目
喜欢吃燃面10 小时前
C++刷题:日期模拟(1)
c++·学习·算法