游戏引擎从零开始(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的分层理念,以后你看其他游戏引擎,都可以参考这个思路去捋代码,从入口开始,找到游戏线程、渲染线程以及驱动线程,理解了数据的传递流程,就理解了游戏引擎的渲染逻辑。

相关推荐
AlexMercer10121 小时前
【C++】二、数据类型 (同C)
c语言·开发语言·数据结构·c++·笔记·算法
小灰灰爱代码2 小时前
C++——求3个数中最大的数(分别考虑整数、双精度数、长整数的情况),用函数模板来实现。
开发语言·c++·算法
BeyondESH3 小时前
Linux线程同步—竞态条件和互斥锁(C语言)
linux·服务器·c++
豆浩宇3 小时前
Halcon OCR检测 免训练版
c++·人工智能·opencv·算法·计算机视觉·ocr
WG_173 小时前
C++多态
开发语言·c++·面试
Charles Ray5 小时前
C++学习笔记 —— 内存分配 new
c++·笔记·学习
重生之我在20年代敲代码5 小时前
strncpy函数的使用和模拟实现
c语言·开发语言·c++·经验分享·笔记
迷迭所归处10 小时前
C++ —— 关于vector
开发语言·c++·算法
CV工程师小林11 小时前
【算法】BFS 系列之边权为 1 的最短路问题
数据结构·c++·算法·leetcode·宽度优先
white__ice12 小时前
2024.9.19
c++