今天分享的是《神海4》的延迟渲染管线实现细节,原文链接在参考文献中有给出。
个人总结
在这个分享中,《神海4》团队分享了他们延迟管线的实现细节,通过材质ID的bitmask实现了众多复杂shader的管理与性能优化,此外,还针对近景物件对于specular的影响提出了一种bent cone解决方案,可以有效解决specular reflection被近景物件遮挡却没有相关遮蔽效果的问题。
正文内容
现有的需求是,很多的绘制需要读取材质数据
这边考虑使用一个简易的延迟Pass+一个前向pass来实现(没看出来这种管线为啥可以读取材质?不过Deferred Lighting管线是会有一遍额外的Geometry Pass的,相当于同一个物件绘制两遍,第一遍计算Ligiting所需要的GBuffer,后一遍完成Shading,在后一遍中是可以拿到Material数据的),不过由于《神海4》中具有顶点十分密集的模型,因此Geometry Pass消耗会比较高。
除此之外,前向Pass中的Lighting处理部分(Lighting不是延迟处理吗?!这里难道说的是GBuffer生成之后,渲染光源Source时的前向Pass?)比较复杂,需要占用较多的寄存器,从而进一步导致Geometry的处理时间(为什么?是因为前向Lighting Pass与前向Geometry Shading Pass同时进行,导致Geometry Shading变慢?)
是否有更好的方案呢?《神海4》这里的做法是完全走延迟管线(fully deferred),放弃前向Pass,下面来看一下具体做法。
而完全走延迟管线的问题在于,GBuffer输出的数据要能够支持项目中的所有材质,好在《神海4》针对的目标机型在内存与带宽上不存在瓶颈(换成其他平台就得好好考虑这个问题)。
这里是最终使用的GBuffer格式,可以看到,一共使用了两个RT,每个RT中每个像素格式为16位无符号整数。
在开发过程中,GBuffer的格式发生了多次的变化,最终才给出上面的数据布局,其中对GCN parameter Packing(没找到相关资料,从名字推测应该是某种shader底层优化策略,通过缩减参数的数目来提升性能)
前面的两个RT是所有材质都需要用到的,除此之外,一些特殊的材质(纺织物、头发、皮肤、丝绸等)还需要一些额外的信息,这些信息是放在第三个RT中的,这个RT中的数据是不固定的,根据材质的不同其含义也有所不同,一般材质是不会用到这个RT的。
由于材质种类多样,同一种材质也有大量的变体,因此Deferred Shader的数目膨胀得很快,为了对shader进行管理,这里使用的材质ID贴图逻辑。
这里的材质ID并不是真的是一个ID,而是一个bitmask,这个mask标注了(对应tile)用到了哪些shader,考虑到feature之间的互斥,这里可以将12位压缩到8位。
材质ID的覆盖单位是16x16像素组成的tile,也就是一个bitmask对应于一个tile中使用到的所有shader(应该会有一个索引转换,不然bitmask尺寸会很大)的索引,根据索引就可以从LUT中拿到对应的shader,而LUT是提前计算的,存储的是支持当前tile上的所有feature(所有材质分支)的最简单的shader。
自动将tile的坐标塞入到参与到tile渲染的shader的tile列表(上图伪代码中的tileBuffers)中,最后统计出来每个shader的tiles数目,之后dispatchindirect会使用这个数目来创建对应CS线程完成相关处理。
根据上述方法,可以很方便的统计出每个shader影响到的tile,之后对每个tile发起一个CS shader 线程就能够完成shader对这个tile的处理。
下面来介绍下《神海4》对这个算法的一些优化处理。以布料shader为例,如果某个tile中的所有像素都是布料,那么上图右边的分支判断逻辑就是一种浪费了。
为了消除分支导致的浪费,这里添加了另一个LUT,这个LUT中的shader只包含一种纯粹的执行逻辑,不包含多种不同材质的shader分支逻辑,之后在tile shader绑定中添加相关判断,将只被一种shader所覆盖的tile绑定到这个LUT中。这种做法不但消除了分支,同时还为后面进行全局的编译优化预留了空间(从何说起?)
这里给出了各个tile上的shader index/material ID的图形化展示。
从效果对比上来看,Material ID可以降低大约15%的消耗,而添加无分支的shader LUT后,还可以进一步降低消耗。
而这种做法在保证基础性能不受影响的前提下支持尽可能多的材质与变种,且针对某种shader增加复杂度,不会导致其他物件的消耗上升(被分支或者无分支分流了),此外,由于shader分类是运行在async CS上的,因此不会有额外的消耗。
这里给出了一些其他的优化思路。
下面介绍一下Specular Occlusion的相关内容。
由于Cubemap存储的是远景信息,因此近景的遮挡信息通过cubemap是拿不到的,从而导致specular反射效果不真实。
常用的一种解决方案是添加更多的cubemap(比如为室内环境添加一个cubemap),但是这种做法有其不足之处:
- 有些区域的cubemap可能没有办法覆盖所有的场景内容的使用
- 性能与内存也会受到影响
另一种思路是借鉴寒霜的specular occlusion,在采样点添加一些AO来提升真实度,但是其不足在于没有办法根据视线方向调整效果。
比如这里,当观察者采用俯视角时,右边的遮挡物应该不会对specular造成影响才是,但是寒霜的方案却依然会为之添加AO,所以不符合需求。
为了得到更为真实的specular occlusion,《神海4》的做法是将当前采样点的遮挡数据以及遮挡方向编码到一个结构中并在渲染的时候读取。
这里给出一个示意图,左边是specular反射的lobe cone,右边则是通过后面所谓的bent cone编码的遮挡信息。
bent cone主要包含两项内容,一个是方向,一个是cone angle。这个处理与计算是离线完成的,计算过程比较简单,就是通过多条射线进行碰撞检测,这里就不展开了。
reflection cone的尺寸则是使用了类似Drobot的Phong/GGX lobe拟合策略,cone angle只需要覆盖90%左右的lobe energe就可以了,上图给出了GGX的一个简单拟合公式。
拿到相关数据之后,下一步就是判断当前采样点的reflection cone与bent cone之间的相交范围(solid angle)。
这里给出了相交solid angle的计算公式,下面给出了实现代码。
Reference the original paper for more detailed notes, and numerical precision issues, etc..
上面给出了最终的表现,可以看到specular表现有了比较正确的遮挡信息,结果更为真实。
另外,如果当前采样点已经基本上被完全遮挡了,这个时候会直接使用方向光的lightmap数据来计算specular,虽然细节丢失了,但是能做到能量守恒,绝大部分时候表现还可以。
这种方案(相交检测)计算的消耗比较高,但是如果将函数中的某个参数(比如粗糙度)固定,可以降低一些消耗;此外,这种方案并不能用在动态物件上(遮挡信息离线生成),另外其结果也不是物理正确的。
虽然有这么多的缺点,但是总归是解决了遮挡对于specular的问题,后面可以考虑通过其他方式来提升精度与质量。
参考文献
[1] Deferred Lighting in Uncharted 4