<2021SC@SDUSC> 开源游戏引擎 Overload 代码模块分析 之 OvGame(三)—— Core(二)GameRenderer

<2021SC@SDUSC> 开源游戏引擎 Overload 代码模块分析 之 OvGame(三)—— Core(二)GameRenderer,第1张

<2021SC@SDUSC> 开源游戏引擎 Overload 代码模块分析 之 OvGame(三)—— Core(二)GameRenderer

2021SC@SDUSC
开源游戏引擎 Overload 代码模块分析 之 OvGame(三)—— Core(二)GameRenderer

目录
  • 前言
  • GameRenderer
    • 1、GameRenderer.h
      • 1.1 头文件
      • 1.2 主体代码
    • 2、GameRenderer.cpp
      • 2.1 头文件
      • 2.2 主体代码
        • GameRenderer() 函数
        • UpdateEngineUBO() 函数
        • UpdateLights() 函数
        • UpdateLightsInFrustum() 函数
        • RenderScene() 函数
  • 总结

前言

本篇是 OvGame 的 Core 的第二篇,将探究其倒二层引用的文件:GameRenderer。该文件会包含 Core 的另一个部分 —— Context,所以如果读者已经有些遗忘、或是尚未了解过,请前往 Core(一)阅读。

另外,若想先大致了解该引擎各个大模块,可前往笔者这篇相关文章查看。
若想了解 OvGame 大纲,可前往笔者这篇文章。

GameRenderer 1、GameRenderer.h 1.1 头文件
#include 
#include 
#include 

#include "OvGame/Core/Context.h"

这里引入了 Overload 的 OvCore 模块的部分文件,在此不多讲述,遇到再简单探究;还引入了上一篇探究的 Context 文件。

1.2 主体代码

该文件的主体代码包含了一个 GameRenderer 类,负责游戏界面的渲染工作,定义如下:

class GameRenderer
	{
	public:
		
		GameRenderer(Context& p_context);

		
		void RenderScene();

		
		void UpdateEngineUBO(OvCore::ECS::Components::CCamera& p_mainCamera);

		
		void UpdateLights(OvCore::SceneSystem::Scene& p_scene);

		
		void UpdateLightsInFrustum(OvCore::SceneSystem::Scene& p_scene, const OvRendering::Data::Frustum& p_frustum);

	private:
		Context& m_context;
		OvCore::Resources::Material m_emptyMaterial;
	};

该类包含了两个私有类,类型分别是上篇文章的 Context.h 文件中定义的 Context 类与包含材质设置以及 Shader 的一个类 Material;另一方面,GameRenderer 的函数功能已有注释,不多赘述,我们来 cpp 文件中进一步探究:

2、GameRenderer.cpp 2.1 头文件
#include "OvGame/Core/GameRenderer.h"

#include 

#include 
#include 
#include 
#include 
#include 
#include 
#include 

该文件一样是引入了多个 Overload 几个模块的文件,涉及到再作分析。

2.2 主体代码

主体代码先声明了一些命名空间,并又多包含了两模块 OvMaths 与 OvRendering:

using namespace OvMaths;
using namespace OvRendering::Resources;
using namespace OvCore::Resources;

接下来依次探究 GameRenderer 的函数:

GameRenderer() 函数

该函数是类的构造函数,咱们分两段代码看:

OvGame::Core::GameRenderer::GameRenderer(Context & p_context) :
	m_context(p_context)
{
	
	m_emptyMaterial.SetShader(m_context.shaderManager[":Shaders\Unlit.glsl"]);
	m_emptyMaterial.Set("u_Diffuse", FVector3(1.f, 0.f, 1.f));
	m_emptyMaterial.Set("u_DiffuseMap", nullptr);

首先,参数初始化表初始化 Context 类的 m_context;然后,m_context 的变量 shaderManager 调用运算符重载函数 “ [ ] ” 获取目标路径的文件(此处为 OpenGL 的 glsl 文件),并传入 Material 类的 m_emptyMaterial 调用 SetShader() 进行设置,绑定为 EngineUBO;最后,m_emptyMaterial 调用 Set() 函数(该函数的两个参数会分别作为 key 和 value 存入哈希表)设置漫反射初值、以及设置漫反射贴图为空。

这样 m_emptyMaterial 就初始化完成了,接着处理 m_context:

	m_context.renderer->RegisterModelMatrixSender([this](const OvMaths::FMatrix4 & p_modelMatrix)
	{
		m_context.engineUBO->SetSubData(OvMaths::FMatrix4::Transpose(p_modelMatrix), 0);
	});

	m_context.renderer->RegisterUserMatrixSender([this](const OvMaths::FMatrix4 & p_userMatrix)
	{
		m_context.engineUBO->SetSubData
		(
			p_userMatrix,

			// UBO layout offset
			sizeof(OvMaths::FMatrix4) +
			sizeof(OvMaths::FMatrix4) +
			sizeof(OvMaths::FMatrix4) +
			sizeof(OvMaths::FVector3) +
			sizeof(float)
		);
	});
}

m_context 的变量 renderer 使用了一个特殊结构 [ this ] ( ) { }。这是 C++11 引入的 Lambda 表达式,可以简单理解为是一种未命名也无需命名的内联函数,可以使得代码更为简洁。此处嵌套在了 RegisterModelMatrixSender() 与 RegisterUserMatrixSender() 的参数内。

RegisterModelMatrixSender() 函数可以记录模型矩阵,并能通过它传给 GPU,所以传入的参数是一个四维矩阵。该函数的 Lambda 表达式是 m_context 的变量 engineUBO 调用 SetSubData() 函数,其有两个参数依次为 p_data、p_offsetInOut,函数能将位于位置 p_offsetInOut 的 UBO(缓冲对象,OpenGL 的数据结构)设置其值为 p_data。此处,是在位置 0 将 UBO 设置为 Transpose() 转置后的矩阵。

最后是 RegisterUserMatrixSender() 函数,可以记录的是用户矩阵,也能传给 GPU。该函数的 Lambda 表达式也是调用 SetSubData(),不过位置变了。

现在先探究三个较短的函数,因为它们会被另一个函数 RenderScene() 调用中:

UpdateEngineUBO() 函数
void OvGame::Core::GameRenderer::UpdateEngineUBO(OvCore::ECS::Components::CCamera& p_mainCamera)
{
	size_t offset = sizeof(OvMaths::FMatrix4); // We skip the model matrix (Which is a special case, modified every draw calls)
	auto& camera = p_mainCamera.GetCamera();

	m_context.engineUBO->SetSubData(OvMaths::FMatrix4::Transpose(camera.GetViewMatrix()), std::ref(offset));
	m_context.engineUBO->SetSubData(OvMaths::FMatrix4::Transpose(camera.GetProjectionMatrix()), std::ref(offset));
	m_context.engineUBO->SetSubData(p_mainCamera.owner.transform.GetWorldPosition(), std::ref(offset));
}

由于构造函数中已经记录了模型矩阵,待会设置时需要跳过,所以这里 sizeof() 取了一个四维矩阵长度;然后 GetCamera() 获得的是相机的指针;接着,和构造函数中的 *** 作一样,调用 SetSubData() 设置 UBO,其中依次传入的值是转置的视角矩阵、转置的投影矩阵、相机类的组件集合类对象 owner 的实体组件系统类 ECS 调用 GetWorldPosition() 获得相机在世界中的位置,这些值存的 UBO 位置就是模型矩阵之后。简单一提,这里的 std::ref 是 C++11 的引用包装器 reference_wrapper。

UpdateLights() 函数
void OvGame::Core::GameRenderer::UpdateLights(OvCore::SceneSystem::Scene& p_scene)
{
	PROFILER_SPY("Light SSBO Update");
	auto lightMatrices = m_context.renderer->FindLightMatrices(p_scene);
	m_context.lightSSBO->SendBlocks(lightMatrices.data(), lightMatrices.size() * sizeof(FMatrix4));
}

该函数先是使用 OvAnalytics/Profiling/Profiler.h 中定义的 PROFILER_SPY,这个宏允许创造 profiler spy(笔者不知道中文该如何翻译),使用的就是给出的 name,这里是 SSBO(GLSL 着色器的缓冲对象);然后就简单了,FindLightMatrices() 获得光的矩阵信息,m_context.lightSSBO 调用 SendBlocks() 发送数据块给 OpenGL 的接口,完成 Shader 的绑定等。

UpdateLightsInFrustum() 函数
void OvGame::Core::GameRenderer::UpdateLightsInFrustum(OvCore::SceneSystem::Scene& p_scene, const OvRendering::Data::Frustum& p_frustum)
{
	PROFILER_SPY("Light SSBO Update (Frustum culled)");
	auto lightMatrices = m_context.renderer->FindLightMatricesInFrustum(p_scene, p_frustum);
	m_context.lightSSBO->SendBlocks(lightMatrices.data(), lightMatrices.size() * sizeof(FMatrix4));
}

该函数和 UpdateLights() 原理一样,只是换为 FindLightMatricesInFrustum(),获取的是 Frustum(视截体)内的光矩阵信息。

了解了上面的三个函数,咱们就可以继续探究最后一个 RenderScene() 函数了:

RenderScene() 函数

代码比较长,咱们分段看:

void OvGame::Core::GameRenderer::RenderScene()
{
	if (auto currentScene = m_context.sceneManager.GetCurrentScene())
	{
		if (OvCore::ECS::Components::CCamera* mainCameraComponent = m_context.renderer->FindMainCamera(*currentScene))
		{
			if (mainCameraComponent->HasFrustumLightCulling())
			{
				UpdateLightsInFrustum(*currentScene, mainCameraComponent->GetCamera().GetFrustum());
			}
			else
			{
				UpdateLights(*currentScene);
			}

首先,第一个判断里调用了函数 GetCurrentScene(),从 m_context 的变量对象 sceneManager 场景管理者获得当前的场景后继续;第二个判断将第一个判断获得的场景的指针传入 m_context 的变量对象 renderer 渲染器的函数 FindMainCamera() ,查找激活的主摄像头、返回指针并继续,没有则返回 nulllptr。

第三个判断是第二个判断获得的相机指针 mainCameraComponent 调用 HasFrustumLightCulling(),返回真假值来判断是否已经做过 Frustum Light Culling(视截体内的光的裁剪);返回 True,则调用上述的函数 UpdateLightsInFrustum() ,传入当前场景的指针、当前主相机的视截体的引用,依此来更新 SSBO 为视截体内;否则,调用上述的函数 UpdateLights() ,传入当前场景的指针设置 SSBO。

紧接着进行下列处理:

			auto [winWidth, winHeight] = m_context.window->GetSize();
			const auto& cameraPosition = mainCameraComponent->owner.transform.GetWorldPosition();
			const auto& cameraRotation = mainCameraComponent->owner.transform.GetWorldRotation();
			auto& camera = mainCameraComponent->GetCamera();

			camera.CacheMatrices(winWidth, winHeight, cameraPosition, cameraRotation);

			UpdateEngineUBO(*mainCameraComponent);

			m_context.renderer->Clear(camera, true, true, false);

			uint8_t glState = m_context.renderer->FetchGLState();
			m_context.renderer->ApplyStateMask(glState);
			m_context.renderer->RenderScene(*currentScene, cameraPosition, camera, nullptr, &m_emptyMaterial);
			m_context.renderer->ApplyStateMask(glState);
		}

首先进行几个变量声明:m_context.window->GetSize() 获得窗口大小,记录在 winWidth、winHeight;和 UpdateEngineUBO() 里的 *** 作一样,mainCameraComponent 分别调用 GetWorldPosition() 获得相机在世界中的位置赋值引用 cameraPosition、GetWorldRotation() 获得相机在世界中的旋转情况赋值引用 cameraRotation;mainCameraComponent 又调用 GetCamera() 获得相机指针赋值引用 camera。

接着,camera 调用 CacheMatrices() 缓存信息,包括窗口大小、相机位置与转角;然后调用上述的函数 UpdateEngineUBO 更新 UBO;

最后是 m_context.renderer 的一些 *** 作:先调用 Clear() 函数用设置的颜色清除屏幕,其后三个真值参数的意义依次为是否清空颜色缓冲、深度缓冲、模板缓冲,其中的接口使用的是 OpenGL;然后调用 FetchGLState(),赋值变量 glState 获得 当前 OpenGL 的状态;接着第一次调用 ApplyStateMask(),传入 glState 修改并应用当前 OpenGL 状态,之后调用 RenderScene(),依次传入上述的当前场景指针 *currentScene、相机位置、相机、nullptr(参数意义是视截体)、GameRenderer 类自己的私有对象 m_emptyMaterial 来绘制场景,最后第二次调用 ApplyStateMask() 再次修改状态。

不过,如果第二个判断没通过,即没有找到激活的相机,则进行下列 *** 作:

		else
		{
			m_context.renderer->SetClearColor(0.0f, 0.0f, 0.0f);
			m_context.renderer->Clear(true, true, false);
		}
	}
}

renderer 渲染器 SetClearColor() 设置一个颜色(这里是黑色),再 Clear(),用设置的颜色清屏,其三个真值参数的意义依次为是否清空颜色缓冲、深度缓冲、模板缓冲。简单一提,细心的读者或许已经发现,这里的 Clear() 和上一段代码里的 Clear() 确实是函数的重载,并且后者调用了前者来完成清屏工作。

总结

至此,我们就大致了解了整个游戏场景的渲染器和渲染方法。其中涉及了图形学,但是对于涉足渲染的人来说都是一些简单的基础知识,就不特别讲解了,还请其它领域的读者自行搜索了解。

由于 Core 文件剩下的两部分里,Application 会包含 Game;另外,Game 还用到了同属 OvGame 模块的 Utils 文件以及 Debug 文件。所以接下来的两篇,我们将暂停对 Core 文件的探究;下一篇,先探究 Utils 的 FPSCounter 以及 Debug 的 DriverInfo。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5665846.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存