渲染引擎开发笔记小总结

代码框架

Posted Kongouuu's Blog on August 17, 2022

前言

这是我人生第一个尝试去开发渲染引擎的经历,说实话从架构上来看开发的问题特别特别的多。之前开发时不成熟的想法记录在这里。因为四月份的时候得去4v的义务军训耽误的四个月,不过也因此有了些时间去沉思问题到底出现在哪里。 讲白了现在的成果就是烂在骨子里,后期补救不回来,只能找时间重构。

这一篇单纯的总结一下我做了什么,哪里好哪里坏,重构怎么重构。

成果

image-20220107132104784

先说结果,开发出来的东西还是有一点点样子的。长相上有 UI, 可以通过 UI 去调整场景的物体、光、阴影设置等,也还算是过得去。 渲染有延迟渲染,阴影有 VSSM ,还算过得去。

基本结构

基本结构由三大部分组成:

  1. Renderer 处理所有绘制以及绘制资源的问题
  2. Engine 调用 Renderer 绘制,也负责处理所有子系统的运行以及资源的加载
  3. Application 上面两个部分会变成一个 DLL, 然后Application 可以调用上面的 DLL 去做场景布置(加载物体及纹理等)

理论上自定义的 Application 类同时也可以在运行时控制不同物体的行动,因此被额外分离开来。当初最开始是跟着 Cherno 的游戏引擎教学做出这个最基础的架构的;不过既然后面想专注于渲染,暂时也没考虑 实时运行游戏 的这种想法,因此也没必要为了个场景加载弄成这样的架构其实…

DX 封装

比起龙书那种每一个章节都要自己手动设置一大堆例如 描述符堆啊 根签名 的结构,我这里还是封装了很大部分提高了自由度。大部分的功能例如堆描述符,Shader, 以及缓冲等等都进行了包装,在增加新功能的时候也会非常的简单泛用。

图形

图形方面其实跟架构就有一些出入了。不过这里的话比较新鲜的是写了一个这个:VSSM阴影实战

反思

大结构问题

其实引擎开发在结构上最大的问题还是我想要开放多少自由度,以及想要做到多独立的一个模块区分吧。最开始进行开发的时候我是希望把图形的底层封死在 DLL 里,并且通过 API 提供大量的自由度给使用这个 API 的。 并且可以让用户像 Unity ShaderLab 那样随意的选择一个渲染的流程以及使用的 Shader。开发中一开始就有这种想法所以留下了不少不合适的设计,也不是说这种想法不对,只是不合适。

如果能重新写一个,我会希望大部分渲染流程上的功能都是写死在引擎内部,就像 UE4 那样。渲染的事情就都写死在引擎内,而用户只负责场景的控制这样,我觉得是一个比较合理的结构。

如果要曝露出很多图形上的功能出来,整体结构会十分耦合,或者说不好写吧。

引擎和图形API 耦合度控制

问题

最开始的时候想要把图形 API 跟引擎方面完全分离开来,觉得这样方便让我们的引擎可以选择使用 DX/OpenGL。 这样的想法是没有问题的,但是试图分离开来这个想法在当时很大的影响了包装 DX 的设计。开发的时候过于想要保持独立,导致出了很多的问题。

资源管理

很多资源他是我们图形 API 要用到的,同时也是我们的引擎方面要去操作的,例如我们物体的顶点以及场景的摄像机啥的。当时为了想要保持模块的独立,所以基本上我没有让引擎侧知道任何 DX12 内部的数据结构什么的,只知道开放的几个函数接口。很显然这样的做法问题很大。在数据需要有多份存储的情况下,还有很多的性能浪费在拷贝数据(从引擎的格式到图形API格式)。

最离谱的是当时为了减少图形API权限提高自由度,让图形API 侧不能知道要作用于 Shader 的全局变量的格式。只有 Shader 和引擎侧知道全局变量的格式,数据的传输也是使用的字节数组。 讲白了这种操作也没啥意义,没什么必要藏数据藏接口…

耦合

说白了就是过分的想要解耦,让引擎和图形分离。但引擎的子系统,例如场景控制等,都是离不开图形 API的。 因为我在控制场景时所有的数据操作最后都要返还到图形 API 那,当时想分离接口却又想解耦导致最后写出来的东西不好看。

解决

实际上开发途中意识到问题后已经解决了好一部分的问题了,也建立了一个公用的 IResource.h 存放不同部件都会用到的数据结构啥的。不过因为重构是个大工程当时并没有去解决根本的问题。

下面谈一下什么样的结构是比较合适的。

  • 我希望整个渲染流程是像 UE4 那样写死的,不提供更改流程这样的操作给最上层。

  • 首先我一样需要一个 引擎 类,来处理所有的子系统的流程和场景的操作,这是一定的
  • 再来我应该放置一个 Renderer 作为中间件,负责管理我们的窗口,然后被引擎调用,以及调用当前使用的图形 API (可以在代码里放OpenGL 以及 DX12两种)
  • 因为不需要考虑我包装的 图形API 能不能和其他的 引擎 去搭配,所以我的图形 API 可以完全是隶属于我的引擎的模块,而不是分离开来 。这一点很重要,因为这样去考虑的话大部分的数据结构类型都可以统一,而且调用的设计也不会过分顾及耦合。

  • 最好再来一个资源管理的子系统让渲染器部分和引擎部分不需要对一个数据存储两个备份

以上的想法是重构的时候会考虑的。

最主要的部分还是中间件的 Renderer, 如果设计的好的话首先可以省下很多多重备份上面的性能,而且流程也会更加的干净。

如果要重构的话应该会从0 开始重新写一遍,把一些独立出来的模块给整合。有很多细节部分都是可以更改增强的只是还没有提到。虽然其实并没有那么重要,例如缓冲的部分我抽象化的并不是很好,不过还够用。