3

游戏性能优化杂谈(十七)

 2 years ago
source link: https://zhuanlan.zhihu.com/p/424666857
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

游戏性能优化杂谈(十七)

游戏开发话题下的优秀答主

随着游戏对于真实度的追求不断提高,游戏场景变得越来越复杂,场景内对象越来越多,单个对象的面数越来越高,材质也日趋复杂。这些都对游戏性能提出了更为艰巨的挑战。

首先,场景内对象数量的急剧增加,导致CPU所需要更新维护的场景对象数据结构,也就是所谓的Scene Graph变得十分庞大且复杂。

显然每帧都对整个Scene Graph进行完整的更新,是十分耗费性能的。如何高效快速地识别出最为重要的更新对象,按照对于当前帧的影响程度进行权重区分,是这一部分需要重点考虑的问题。

比如,在开放世界当中,对于画面近处的景物和人物,我们可能需要每帧都去更新动画,但是对于中远景当中的动画,就不见得需要了。尤其对于远景当中的对象,我们甚至可以在shader当中采用一些简单的三角函数或者查表等,来代替其原本的动画序列。

另外一个非常有效的方法,就是在制作场景的时候,对场景对象进行分类分层,也就是打tag。最为基本的,静态(位置不动且属性不变)的对象、半静态(位置不动但是属性会发生变化)以及可移动对象(包括可动态创建或者销毁的对象),应当使用tag区分开来。

甚至我们可以考虑为不同性质的对象创建不同的Scene Graph,并且由不同的Updator按照不同到更新周期进行更新维护。

在更新完成Scene Graph的更新之后,我们会需要确定要渲染的对象,也就是构建Render Graph。一般来说,Render Graph会是Scene Graph的一个子集,可以将其看作是对Scene Graph按照可见性进行裁剪之后的一个缩减版。

同样,因为场景复杂对象数量很高,为了提高这个裁剪的速度,我们会需要导入一些加速结构。用得比较多的依然是诸如场景八叉树或者层次包围盒这些技术,核心思路都是将场景对象进行分组之后,进行渐进式的测试与剔除。最近也有看到利用光追所用的BVH结合光追进行这项工作的尝试。

当Render Graph构建完成之后,一般会将其发送给渲染线程,进行绘制命令的生成。绘制命令的生成可以看作是CPU实时书写需要GPU执行的小程序。需要绘制的对象越多,则CPU所要书写的程序就越长。因为这是一种访存操作,所以相对来说是很慢的。

为了加快这个阶段,我们需要尽量减少CPU在每帧所要书写的内容。这主要有两种方法,一是使用draw instance或者multidraw等现代命令,将多个draw call进行合并。另外一个则是对于支持command buffer到command buffer跳转或者说间接引用的环境,将一些常用的过程单独存储在特定的command buffer当中,作为子过程调用,从而避免每帧重复生成。

此外,对于较长的command buffer,将其分为多个松耦合的子部分,用多线程进行并行生成和异步提交,也可以使得GPU和CPU之间的并行度增加,减少总的帧渲染时间。

事实上,随着indirect draw和multi draw的发展,使得我们几乎有可能将整个渲染管线以一种类似模板的方式事先写好并保存,并将其提交给GPU。而渲染所需的具体数据he参数,是可以在渲染命令提交之后再进行确定的。而且这个工作即可以由CPU来完成,也可以通过GPU自己来完成。

但是如果要GPU来完成,我们就需要找到一种对于GPU来说也方便的,可以高度并行化的Render Graph的描述方式。并且,我们需要在绘制命令当中巧妙安排同步点,以避免GPU在相关参数还未最终确定之前就开始了渲染。

在优化的时候,需要注意对于采用了此种技术的管线,即便GPU出于等待同步的状态,其前端依然会输出繁忙的信号。所以仅仅观察GPU的使用率是不够的,还需要具体观察GPU各个模块,特别是后端的工作状态。

对于复杂高面数场景,传统vs-ps管线的主要问题是在于,vs在执行之前GPU会扫描index buffer,加载vertex attribute,以及开辟相应的片上存储空间用于存储(缓存)vs运行的结果。

由于vs工作量当中一个线程对应一个顶点,每个线程并不能感知和周边其它线程的关系,无法访问顶点间的相对关系(比如,是否属于同一个三角形),导致vs自身并不能实现对于顶点的裁剪。

那么,游戏场景(Render Graph)里面有多少顶点,vs就会实打实地处理多少顶点,包括所有附属在顶点上的属性。即便这个顶点会在后续的culling测试当中被抛弃。

为了改善这种状况下的性能,我们希望在处理顶点的时候,还能获得顶点和其它顶点,或者说面,之间的关系。也唯有这样,我们才能判断一个顶点是否会被cull掉。

这就是所谓的primitive shader。使用primitive shader可以让我们在处理顶点的同时,根据顶点所属的面(三角形),判断这个顶点是否该丢弃。也就是将顶点的剔除前移到shader当中,避免了不必要的固定功能管线资源的开销。

但是如果仅仅是顶点级别的剔除效率依然是比较有限的。因为需要对几乎所有的vertex index进行遍历这一点依然没有得到任何改善,相反,我们还需要额外参照顶点所属的primitive信息。

为了解决这个问题,我们将一定数目的primitive组织起来,形成一个primitive cluster,或者叫meshlet。由于这个构建是离线的,我们在构建的时候还会额外构建一个meshlet的包围盒。

这样的话,我们就可以实现primitive group级别的culling,如果被culling掉,则该group当中的primitive信息是不需要访问的,也就是减少了vertex index的访问量。

而且,如果我们将这个primitive group的尺寸限制在一个相当小的范围,则我们可以使用更短的index。比如,如果一个meshlet只包含64个primitive,则index采用7bit就差不多足够的了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK