有关SimpleEngine
SimpleEngine(SE)是自己造的一个游戏引擎轮子,这篇文章用来记录SE的开发思路
渲染
数据结构
渲染器对多个渲染层进行渲染,每个层根据功能包括三个渲染队列:不透明队列、阴影队列、透明队列。三个队列各自使用一棵场景树对物体进行管理,各个队列具体情况如下:
- 不透明队列:用于渲染不透明物体。
- 阴影队列:用于渲染阴影贴图,在这一队列中的物体需要加入不透明队列才能正常渲染,这一队列不需要进行光照剔除,但需要根据光源进行视锥体剔除。
- 透明队列:用于渲染半透明物体,这一队列中的物体需要在视锥体剔除后按由远到近的顺序进行渲染。
场景管理
引擎使用稀疏八叉树管理一个渲染队列中的物体和光源(目前只有在阴影队列中的点光源和聚光灯有意义)。这一八叉树有一个最大高度,物体和光源根据其在世界坐标中的作用范围(物体使用AABB包围盒,光源可能使用球体)插入八叉树的某一结点。物体需要插入能包含它的最细粒度的节点,而光源需要插入到能与其唯一相交的最细粒度节点(即这一节点的兄弟与光源都是相离的关系)物体位置移动时需要更新其在八叉树中的位置,若物体仍被包含在原节点,则需要在该节点及其子节点中找到一个新位置插入物体,否则就将物体删除后重新插入树中。对光源位置的更新也是删除重新插入即可。在将物体删除后,需要合并空节点,若被合并节点有灯光,需要将灯光提高至更高层节点,并在此后节点分裂时重新下推。
为了加速对象的删除和更新,需要维护对光源和物体在树中位置的索引。对于物体,只需简单记录物体所在节点及其在节点中的物体链表对应的指针即可。而对于光源,由于在合并空节点时需要将光源提高,因此需要支持查找某一子树中的所有光源的操作。这一操作一种可能的实现方法:首先对八叉树节点进行编号(可能利用中序遍历的模式编一个三维的向量),光源记录其所在节点的号,所有光源根据号的大小串成一个链表,这一过程可以用一棵线索二叉树实现,一个新光源插入时查找其前驱和后继确定其在链表中的位置,并将其加入链表和二叉树。一棵子树对应的编号是连续的,因此操作子树时只需要在二叉树中定位到链表的头尾即可。
对于开放世界游戏,对象在树中的深度会随对象远离原点而增加,另一方面,场景中出现的对象只占据整个世界的一小部分连续区域,也就是说八叉树中只有一部分子树中存在对象,而这一子树到树根的路径都是空的,因而不需要实际维护这一路径,这种空闲现象伴随物体删除产生,因此在物体删除导致出现一条唯一路径指向一棵子树时,可以删除这一路径只保留根;而在插入物体时,如果发现根节点不能包含物体,需要重新生成根节点的父亲并将其作为新的根节点直到其包含物体。
对场景进行渲染时,从树根处出发对八叉树节点进行视锥体剔除。对光源阴影渲染时,从光源索引处出发对子树中节点进行光源视锥体剔除。
这一数据结构起到以下几个作用:
- 利用八叉树加速视锥体剔除的过程
- 利用八叉树快速筛选出渲染某一光源时需要提交Drawcall的物体
这一数据结构的设计思路和依据在于,物体和光源的变化具有一定的局部性,因此采用动态的八叉树结构可以将对象插入删除的开销分摊到每一帧。对于较少物体,八叉树可以收缩不必要的节点,因此遍历八叉树进行剔除的开销和遍历物体列表全部提交drawcall的(遍历)开销规模是近似的,而八叉树可以轻松剔除视锥外的物体减少drawcall使得实际性能得到提升。场景需要用到实时阴影的动态光源较少,因此即使暴力插入删除开销也是可以接受的,而其在渲染shadowmap时可以根据光源作用范围快速剔除大量无关物体,相较此前需要渲染所有物体性能大大提升。
场景管理的思路在过去几周一直在变,从最开始为每一个队列维护一棵树和所有光源,到所有物体和光源共享一棵树,再到现在的每个队列维护一棵树,只把投射阴影的光源加入阴影队列再用GPU中的数据结构管理光源。在各种细枝末节的事情上卡了好长时间才发现问题的根源,之前一直把做Shading的光源和渲染Shadowmap的光源混为一谈,为了优化巨量光源在树上进行各种插入删除煞费苦心,但实际上八叉树只需要对Shadowmap的渲染做剔除就可以了。
到这里就需要一种统一的管理大世界场景的框架,这一框架可以根据主摄像机位置动态按区块加载和移除各种LOD级别的资源,在加载灯光和物体后通知场景管理的数据结构进行更新,并且和未来的地形系统对接。其中的耦合是很严重的,尤其是这一框架需要根据地形系统的某些几何特性选择数据结构,这也是为什么这个项目酝酿了这么久。
渲染管线
目前实现了Cluster based Forward+管线,在实现的过程里遇到一些坑,一个是Shader里的Buffer布局有std140
和std430
,140的数组要对齐成vec4
,还有一个问题是所有单元的光源最大数量乘以单元数要小于等于全局光源索引表的大小,现在的Cluster是8*8*16
,每个单元最多64盏灯光,一共65536个索引。在实现Depth prepass的过程中遇到两个问题,一个是在实际着色前关掉了深度写入,但是在下一帧清除前要开启深度写入,不然没法清除;还有一个是着色Shader和深度Shader用同一几何数据算出来的片元深度可能不一致,导致绘制出来存在闪烁现象,具体解决方法参见这里
管线的实现还存在以下几个问题:
- Cluster对于z的线性划分不合理
- 光源范围计算存在问题,目前是用衰减到0.1代入计算,但是画面会出现分块现象,但是从视觉效果上看光源影响范围要小
全局光照
全局光照系统的实现细节还没有确定,有几个大概思路:
- 采用VXGI
- 每个Voxel只保存Phong漫反射颜色的均值
- Diffuse Cone实现参考IBL的Diffuse部分
- 需要针对GGX BRDF确定Glossy Cone的实现细节
有一个可以优化的点,之前的VXGI实现中Voxel中存储的是材质与光源作用后的数据,如果能将二者分开可能会获得性能提升,尤其是在地形系统中,处理场景破坏时顺便就可以更新Voxel数据。一种暴力的实现方式是将一个Voxel看作一个面,存储面的平均颜色和法线以及可以对其造成影响的光源列表,在做ConeTracing的时候进行漫反射颜色计算,对于每个片元发出的每个Cone,造成的开销是能够对其产生影响的光源数量的点乘,但是问题在于如何高效维护Voxel的光源列表。