从零开始游戏开发——3.1 引擎架构和渲染管线
第三章的主要目标是完成渲染器的实现,因为进入到了主要引擎核心部分的实现,我们首先需要对引擎进行框架搭建,之后的所有内容将是在引擎框架上进行扩展。下图为引擎所包含的模块,由下向上代表了整个引擎的层次结构,上层模块依赖下次模块的实现,而下层模块则不依赖上层模块。
Dependency模块是外部依赖项,如渲染包含的图形API接口、物理引擎、模型库、声音等外部依赖项。Dependency中的内容是外部导入的,因此我们的引擎对这个部分产生依赖关系。
Platform模块是各平台相关实现,如窗口的显示、游戏主循环的启动、输入输出、渲染驱动的初始化等,都依赖各平台的不同实现。
Foundation模块是引擎中用到的基础功能,如第二章介绍的数学库、内存管理、IO、线程、Debugger等内容,上层引擎实现会依赖这些基础内容库。
ResourceMgr模块是对引擎中的贴图、材质、模型、音视频、动画、配置文件等资源的管理,上层模块通过这个ResourceMgr可以方便的对各种引擎资源进行访问。
Graphic模块是图形相关功能的实现,最下面的Renderer实现了不同各类的渲染器,如软件渲染器、Vulkan渲染器、GL渲染器等都是在这个里实现的。Rendering部分提供了渲染相关的外部接口。Effect、PostProcess、Illumination等则是提供如特效、光照等渲染效果。
AI、Net、HID(Human Interface Device)、Animation System、Physic、Audio从名字可以看出其对应了游戏中会用的到各相关模块。
Application层是应用程序层,这层封装了引擎的启动、初始化、主循环等逻辑, 用户使用引擎时,通过实现模块提供的CApplication类,可以在不关心底层逻辑的基础上,快速启动一个窗口并在上面绘制图形。
当需要渲染一个三角形时,具体代码类的定义如下面的结构:
1 class RendererApplication : public CApplication 2 { 3 DECLARE_INITIALIZE; 4 5 public: 6 virtual bool OnInitialize() override; 7 virtual bool OnTerminate() override; 8 virtual void OnUpdate() override; 9 10 private: 11 CRenderInput _RenderInput; 12 CGeometry *_Geometry; 13 }; 14 15 REGISTER_INITIALIZE(RendererApplication);
namespace Magic { void VertexShader(const void *globalUniforms, const void *uniforms, const void *datas, unsigned char*out) { UniformMap globalU= *((UniformMap*)globalUniforms); UniformMap u = *((UniformMap*)uniforms); Matrix4x4f mvpMat4(globalU["mvpMat"].data()); Vector3f inPosition; Vector3f inColor; memcpy(inPosition.v, datas, sizeof(inPosition.v)); memcpy(inColor.v, (unsigned char *)datas + sizeof(Vector3f), sizeof(inColor.v)); Vector4f outPosition = mvpMat4 * Vector4f(inPosition.x, inPosition.y, inPosition.z, 1.0f); memcpy(out, &outPosition, sizeof(outPosition)); memcpy(out + sizeof(outPosition), &inColor, sizeof(inColor)); } Color FragmentShader(const void *uniforms, ISampler **, const void *datas) { Vector3f inColor; memcpy(inColor.v, datas, sizeof(inColor.v)); return Color(1.0f, inColor.x, inColor.y, inColor.z); } IMPLEMENT_APP_INITIALIZE(RendererApplication); bool RendererApplication::OnInitialize() { cout << "RendererApplication::OnInitialize" << endl; IVertexBuffer *vertexBuffer = Renderer->CreateVertexBuffer(); IIndexBuffer *indexBuffer = Renderer->CreateIndexBuffer(); float triangle[] = { -1.f, -1.f, -5.f, 1.f, 0.f, 0.f, 1.f, -1.f, -5.f, 0.0f, 1.0f, 0.0f, 0.f, 1.f, -5.f, 0.0f, 0.0f, 1.0f, }; vertexBuffer->BufferData(triangle, sizeof(triangle), sizeof(triangle) / (6 * sizeof(float))); vertexBuffer->GetAttribute()->SetPositionAttr(0, sizeof(float) * 6); vertexBuffer->GetAttribute()->SetColorAttr(sizeof(float) * 3, sizeof(float) * 6); _Geometry = NEW CGeometry(vertexBuffer, nullptr); _RenderInput.SetGeometry(_Geometry); IShaderProgram *shaderProgram = nullptr; if (Renderer->GetRendererType() == RendererType::Software) { shaderProgram = NEW CSoftShaderProgram(VertexShader, FragmentShader); } _RenderInput.SetShaderProgram(shaderProgram); _RenderInput.SetTexture(0, nullptr); Renderer->SetClearColor(0, 0, 0); Matrix4x4f vMat, proMat; vMat.BuildCameraLookAtMatrix(Vector3f(0, 0, 0), Vector3f(0, 0, -1), Vector3f(0, 1, 0)); proMat.BuildProjectionMatrixPerspectiveFovRH(PI / 6.f, 1.0f * GetWindowHeight() / GetWindowWidth(), 1.0f, 1000.f); Renderer->SetGlobalUniform("mvpMat", (proMat * vMat).m, sizeof(Matrix4x4f)); return true; } bool RendererApplication::OnTerminate() { cout << "RendererApplication::OnTerminate" << endl; SAFE_DELETE(_Geometry); return true; } void RendererApplication::OnUpdate() { Renderer->AddRenderInput(&_RenderInput); } }
我们在OnInitialize()、OnTerminate()、OnUpdate()分别进行游戏启动时的初始化、关闭游戏时的清理和每帧的更新操作。这里初始化OnInitialize()时创建了一个三角形并构建渲染需要的RenderInput数据,在OnUpdate时将RenderInput数据传递给渲染器,OnTerminate()中销毁几何数据。VertexShader和FragmentShader是软件渲染器使用的着色器程序,分别对顶点和要绘制的像素进行操作。上面代码会绘制出我们熟悉的三角形,这个简单的实现没有涉及到场景管理,因此通过手动生成RenderInput的方式将数据传递给需要器,因为这章我们的重点是渲染器的实现。
这章我们主要目标是实现上面引擎架构Graphics中 Renderer部分,首先我们要实现的是一个软件渲染器,之所以叫软件渲染器,是因为没有利用GPU的硬件加速,所有操作都基于CPU处理,这对于理解如OpenGL、DirectX、Vulkan等图形API的原理十分重要,当我们通过CPU完成了整个渲染流程,再去看这些晦涩的图形API时,一切都变的柳暗花明。之后我们实现一个基于Vulkan的硬件渲染器,通过GPU加速,实时渲染速度会得到非常大的提升。在这之前,首先要了解现代实时渲染过程是如何将物体绘制到屏幕上的。
在《从零开始游戏开发——2.5 第二个三角形》节中,我们利用射线实现了在屏幕上绘制三角形,但在实时渲染中这种方式太慢了,现在主流方式主要包括以下几个步骤:
应用程序处理段阶主要是剔除不可见三角形然后将数据提交给渲染器,应用程序通常会通过摄像机的视锥体(可见范围)、BSP、PVS等技术裁剪掉不可见的模型,这个阶段也会将三角形进行合批操作,将相同材质的模型合并成一个整体一次性提交给渲染器,以减少每次drawcall渲染器需要的状态切换。几何处理阶段是对三角形和顶点的处理,这个阶段会将应用程序阶段提交的三角形进行空间变换,由模型空间——世界空间——摄像机空间—— 裁剪空间——屏幕空间,这个过程会在后面详细介绍,需要知道的是这个阶段的目的是将处理输入的世界空间中的几何图形输出为屏幕空间中的几何图形, 为屏幕上绘制提供数据。几何处理阶段输出的是由点构成的屏幕空间几何图形,我们最终要在屏幕上显示的是实心的三角形面而不是点或线框,光栅化阶段对三角形顶点数据进行插值,计算屏幕上三角形内部每个像素绘制所需要的数据。像素处理阶段通过深度测试、模板测试、Alpha测试、颜色混合等操作对最终屏幕上的像素进行着色操作。软件渲染器中,上述阶段都是在CPU上执行的,硬件渲染器中应用程序阶段主要是在CPU中执行的,也有通过编写Compute Shder利用GPU加速数据处理,剩下的几个阶段都是在GPU上运行,几何阶段提供了必需要的Vertex Shader和可选的Tessellation Shader和Geometry Shader由我们控制几何数据的生成,像素处理阶段提供了Fragment Shader(或叫Pixel Shader)来处理最终着色效果和可配的模板操作来达到一些特殊效果。
在实现渲染器之前,有几个概念是比较重要的,第一个要明确的是三角形面的正面是通过顺时针方向还是逆时针方向定义的。在渲染过程中,我们告诉渲染器需要渲染三角形的哪个面,因此这需要告诉渲染器第二件事情剔除面的模式是正面剔除、背景剔除还是都不剔除。一次渲染过程我们通常需要颜色Buffer、深度Buffer、模板Buffer,这些都定义在叫做帧缓存的对象上,我们的引擎定义名字为RenderTarget,第三个要告诉渲染器的是否需要写深度、写模板、需要进行深度测试、模板测试。最后我们要设置每次渲染开始前是否要清除颜色Buffer、深度Buffer、模板Buffer,同时可以设置Clear的数值。这些常用状态没有设置的时候,我们提供默认的状态进行渲染。
本文来自博客园,作者:毅安,转载请注明原文链接:https://www.cnblogs.com/primarycode/p/16536536.html,文章内容同步更新微信公众号:“游戏开发实战”或“GamePrimaryCode”