渲染引擎开发笔记4

绘制物体设计思路

Posted Kongouuu's Blog on December 16, 2021

前言

原本想要一点进度一点进度的去写这系列笔记。不过在当前的一个架构下去绘制一个方块的整体开发流程特别的繁琐。我整个开发的流程是先从渲染器侧去先渲染一个方块,然后再一步一步把建立一个物体的操作移动到引擎那边。

这里首先是要对DX12本身用到的很多东西去包装,比如说Shader,PSO,DescriptorHeap等。由于我们要考虑到在继承自引擎的不同程序里,会进行不同数量的物体和贴图的加载,所以很多东西他没有办法像龙书那样写死。而且还好管理好整个代码的整洁,和足够完善的管线让很多的数据(例如Srv Heap)可以在渲染器内部根据引擎提交的物品数量去自行判断生成多少。

降低耦合度也是这一部分的一个大挑战。这个地方没有办法在一个文章内解释完怎么绘制出一个方块的,主要是设计上比较的繁琐,所以这一篇先抛开实际渲染API封装去讨论一下怎么设计建立一个物体时引擎和渲染器之间的互动。

先上结果,目前的进度是能绘制出一个这样的箱子,颜色使用的是法线:

设计思路

从整体上来看,我的代码目前主要分为三个部分

  1. 继承自引擎的自定义程序,负责加载物体
  2. 引擎本体
  3. 渲染API

并且引擎对渲染API的所有操作都要通过一个Renderer接口去进行(解耦)

期望

我这里希望的是我能在引擎本体上曝露出一个函数 LoadScene(), 然后我的自定义程序可以覆写这个函数来决定去加载什么样的物体,以及物体的属性。

总的来说我希望能看到Application侧能够通过这样的调用去在画面里添加物体:

1
2
3
4
5
6
7
8
void LoadScene()
{
	Object obj;
	obj = AddObject("object 1");
	obj.SetMesh(box);
	obj.SetOtherData();
	renderer->AddObject(Object)
}

物体

物体以及几何信息是毫无疑问应该要存储在图形API外面的。因为我的引擎是负责加载这些模型的,在加载和赋予属性的过程中,DX12是不应该参与其中任何操作的。不过这本身是一个挺具有挑战性的一次经验,因为在龙书里面我们并没有把程序和图形API进行一个分离,放在一起操作的话难度确实会比较低。但如果我们考虑,有那么万分之一的可能性我的引擎应该要支持不同的图形API,那么这些信息的搭建以及我对他们的位置的操作都不应该有DX12的参与。

同时,DX12又需要拥有所有渲染物体的信息,这是一定的,因为在绘制环节的话我们需要把所有的物体使用DX12去画出来。经过比较繁琐的思考后,我是这样子设计两方面对物体的管理类的

程序侧的RenderObject

在程序这里主要是提供一些可以自定义的信息:

  1. 名字
  2. Mesh信息
  3. 位置,缩放,旋转信息
  4. 材质信息
  5. 使用的贴图的id
  6. 对应在图形API内部的RenderItemID

这个RenderItemID是最为关键的,这是我们在引擎侧可以去操作在渲染器内部的该物体的数据的途径。

因为还没有去加载模型,所以写作了含有单一的Mesh以及贴图,不过考虑到拓展性每个RenderObject都应该可以持有多个Mesh和贴图。

渲染器的RenderItem

这里我更想让渲染器使用的RenderItem是一个单纯储存原始信息的一个类,就是说它并不需要自己储存的顶点到底是什么格式。如果我让RenderItem去储存Mesh的信息的话,那么相当于为每一份物体的格式完全一致的几何数据我都要长期在CPU侧进行两倍的存储,这完全是没有必要的操作。

因此它含有下面这些信息:

  1. RenderItemID: 提供给外侧进行查找用
  2. RI_Type: 渲染分类(半透明,或者不透明),因为在实际渲染流程中他们需要使用不同的PSO在不同阶段绘制
  3. Vertex Buffer 的原始数据,以及GPU里面的位置
  4. Index Buffer 的原始数据,以及GPU里面的位置
  5. Object CB 的数据,以及在GPU的位置, 这里的话数据本身其实是可以被移除的,单先不考虑
  6. Material的数据,跟ObjectCB一样其实只需要储存索引
  7. 绘制类型(默认三角形)

这样的设计让两侧的依赖性比较少,并且渲染器这边完全不需要去持有引擎那边的数据类型信息也可以正常的工作。

RenderItem后续发展

上面的设计有个地方就是我很多数据实际上还是多存了,也就是变换信息(ObjectCB)和材质信息。因为这两侧储存的方法他并不会特别的耦合。

也就是说我如果引擎搭配了别的渲染器,我一样可以给其他渲染器提供变换和材质信息。如果我的渲染器搭配了别的引擎,我其他的引擎也不需要知道渲染器内部的Object CB的结构,也可以用曝露出来的接口把位置,材质,旋转等信息喂进去。现在在引擎侧写起来是这样的:

实际上目前都是以固定Shader的方式去考虑,所有没有任何问题。参考UE4引擎的话他其实也是固定的Shader,写材质的时候去控制不同的材质参数罢了。这样是够用的。

如果万一想要支持自定义的Shader,那么意思就是说我的参数也得在引擎侧去决定,然后渲染器不能知道参数的格式。这样是可以的,在每次更改信息的时候我们调用渲染器把原始数据给塞到GPU里面就可以少一份储存。不过因为现在这引擎为了提高效率使用了多个帧资源的设计,并不是很方便进行这样的操作。

应该用不到自定义添加不同格式的Shader这样的功能,只是说说罢了。

大致思路

简单来说就是尽量减少引擎和渲染器的互相依赖性。大部分操作在引擎侧处理好后把简单的数据喂给渲染器就可以了。流程如下图:

总结

之前用过一个比较简单的2d小渲染器叫SDL2,在使用过程中我是几乎不需要知道它里面的构造,只需要让我的程序把该喂给它的数据喂进去然后让它渲染就可以了。所以我想的是要尽量把渲染器的功能独立开,然后曝露出几个接口让引擎有一定程度上对它的控制。就是说我要减少依赖性然后让他们互相比较的,独立。

这个的过程特别的繁琐,可能因为经验不够多。在写代码的时候我也会经常在想如果我放置一个引擎和渲染器都能看到的类,然后在里面去定义什么顶点信息,变换信息,材质信息等,会不会很方便。那肯定是方便的,但总觉得不美观,而且有一定程度的依赖性。