今天大家在讨论GDC 2016中育碧在《彩虹六号(Rainbow Six Siege)》中所介绍的checkerboard rendering方法,认真看了看其中的实现原理,现在分享给大家,这里是原文链接。
看下这里对60FPS的理解:
- 非战斗模式下能达到60fps(针对的是渲染层面)
- CPU侧则是不能超过38ms的linear time(简单搜了下,没发现针对这个的详细介绍)
这里有两个问题:
- CPU这个38是怎么来的
- 38ms的CPU消耗是无法保障真正的60fps体验的,不知道这里的target要如何达到呢?
看后面的描述,给CPU的预算还是14ms,这里可以暂时先不纠结38这个数字。
联机游戏的好处与注意事项,如上所示:
- 不需要在发布的时候将特性做到极致,只要够看即可,上线后还有足够的事件慢慢调优
- 不过就是要小心改动会不会引起现有功能的表现或者品质问题
接着之前60fps给GPU设定的14ms的预算来看下具体如何分配:
- 5ms用于几何体的绘制,重点需要介绍的优化点:
- 剔除算法的极致优化(无损+有损的)
- Shadow Cache
- 5ms用于光照计算(包括SSR)
- 采用Checkboard Rendering
- SSR ray trace pass跟SSAO放在async(compute shader?)中完成,不占用Graphics Pipeline的资源
- 4ms用于后处理(包括各种可以看成是后处理的全屏计算)
CPU这边预算的划分则是(从细节描述推测,这里说的是渲染线程,而非游戏线程):
- 10ms分配给关键路径
- 通过fork、join等方式来缩短关键路径
- 同样借助shadow cache来规避CPU侧的时间消耗
- 4ms分配给不透明pass
- 按照从前往后进行绘制
- 遮挡剔除用的HZB算法,遮挡体是手动设置的,每帧最多选用400+
- 不知道这里的剔除是在CPU还是GPU完成,CPU的话,还得将HZB数据readback一次,GPU的话,就走indirect draw,不过drawbuffer数据同样也是需要从GPU回到CPU(?)
- 阴影同样做了HZB Culling以降低GPU压力。
- 局部光源则针对其贴图分辨率做了降低
- 方向光阴影是在地图加载的时候构建并cache的(构建一张大的覆盖全图的)
- 采用的是ESM的计算方案
- 构建的时候还会构建HiZ,用于实现动态数据的剔除
方向光用的还是CSM的设置,这里做了稍微详细的设计描述:
- 烘焙的shadowmap将与运行时的CSM(负责动态数据)一起工作
- 通过XBox的各级是如何配置的,大致可以知道两者是如何结合的:
- 第一级只考虑动态的
- 第二三级则动态(CSM)跟静态结合
- 第四级只用静态的
一个遗留问题是,CSM是相机视角的,而静态的阴影跟frustum的覆盖范围不见得能正好吻合,难道某个cascade需要同时获取到覆盖范围的多张shadowmap吗?
前面其实说过,这是一张覆盖全图的阴影贴图,所以不用考虑这个问题,不过这里带来的另一个问题是,内存会不会有点浪费?而且如果地图尺寸过大,这里也会有问题吧?
局部光源最多只支持8盏灯产生投影,超出的部分呢?怎么选择?
采用的是clustered rendering管线:
- z方向的划分是指数分布算法计算得到的
- 会将局部的cubemap(IBL)看成是灯来做统一处理
- 阴影、cubemap以及gobos等数据统一放到texture array中,方便合批
破坏系统:
- 支持墙体跟地面,需要通过程序化的方式来完成unique几何体的生成
- 挑战在于当我们把物件破坏后,可能会导致遮挡剔除效率受影响
- 破坏后的残留数据,要么是实例化的,要么是作为单一对象绘制,需要限制后者数量
- 早期的设计方案在渲染上会存在很大的瓶颈,不论是CPU还是GPU
- 基于材质来调用DrawCall,通过材质的复用来约束DrawCall
- 控制对破坏数据裁剪的粒度
这里对基于材质来优化DrawCall做了更进一步展开介绍。
裁剪这里做了分层处理(类似BVH),以提高裁剪效率。
前两层剔除逻辑是一致的,最后一层只考虑法线因素(背面剔除)。
通过这种方式可以在上图所示的场景中,只额外增加了5个DP就完成了破坏后的渲染效果。
下面来看看checkerboard rendering框架(这个方案,Intel有专门的页面进行介绍,还提供了相应的源码,这里是原文传送)介绍。
《彩虹六号》的目标是实现60FPS,这里准备从GDC 2014分享的《Killzone Shadow Fall》中的interlaced rendering方案开始进行尝试。
这个方案的基本思路是将纵向分辨率缩减为原始分辨率的一半,之后通过修改投影矩阵(由于纵向覆盖范围维持不变,因此一个像素在纵向上覆盖的尺寸变为原始覆盖尺寸的两倍,之后单帧投影矩阵与双帧投影矩阵需要各自对应当前像素的前半个像素与后半个像素(也就是此前未降分辨率时奇像素与偶像素),因此需要通过投影矩阵添加半像素偏移)来实现奇偶帧结果的互补。
简单总结一下,其实就是在空间上做了分辨率减半处理,但是为了避免效果减损过于严重,在时间上则通过前后两帧数据的复用来掩盖瑕疵,而为了实现复用,前后两帧的投影矩阵需要做一个偏移,使得正好覆盖奇数跟偶数行的像素。
由于这里对投影矩阵进行了偏移与纵轴缩放,因此在处理梯度的时候需要做相应处理(其实这个还挺麻烦的,所有后处理中需要用到梯度计算的部分都要做处理)。
方案实施过程中遇到的问题,纵轴上的锯齿是一个大问题。
尝试通过checkerboard rendering来修复锯齿,最终发现这个方法比interlaced rendering有着更高的质量,且通过硬件MSAA 2X,可以不需要对渲染做太多修改即可实现。
从质量上来说,很显然checkerboard rendering要更胜一筹,下面看下具体方案。
checkerboard rendering的思路是,直接采用1/2 x 1/2分辨率进行渲染,之后通过MSAA + SV_SampleIndex之类的强制Separate Sample Shading方案实现半分辨率shading。那既然每个sample都渲染一次,这种做法相对于全分辨率不加MSAA渲染有什么优势呢?因为使用的是2x MSAA,因此相对于全分辨率渲染,计算次数还是少了一半,其消耗跟interlaced rendering类似,不过相对而言,具有更好的显示质量,更低的锯齿感(此外,猜测借用了硬件自带的MSAA算法来实现,性能会不会更好一些)。
通常MSAA中只对pixel进行shading,将结果写入多个sample中,开启了SV_SampleIndex的渲染则会强制对每个sample执行一次shading,结果等同于SSAA(Super Sampling AA)
这个就是checkerboard rendering的基本思路。
这里给出了checkerboard rendering的优势,说到不需要在shader中为梯度计算做特殊处理。
为什么不用呢,从输出结果来看,MSAA输出的raw buffer相邻像素都是空的,直接使用原始梯度计算方案肯定会得到错误结果才是。这里给出的说法是,通过LOD bias对像素添加一个偏移,就可以直接对下一个像素进行采样,通过这种做法就可以跳过相邻的空白像素数据,得到正常的梯度结果。
后面给出了checkerboard rendering的实施细节,这里就不再赘述了,有兴趣的同学自行翻阅吧。
跟前面的方案类似,checkboard rendering也需要每帧调整相机的投影偏移,使得相邻帧能够覆盖全屏的像素。
比如上图,我们奇偶两帧就可以实现全屏幕所有像素的覆盖。
这里以像素P跟Q为例,介绍了我们怎么基于当前帧绘制的数据(缺失了PQ)以及前一帧(包含PQ,不过是前一帧的PQ)来得到当前帧的PQ数据。
采用了跟TAA类似的复用策略:
- 借用了Motion Vector来实现reprojection
- 通过相邻数据来进行数据验证
- 基于深度差异来对reproject的数据做blend
颜色的计算现在有两个输入来源:
- reprojected color
- interpolated color
最终的颜色就是两者的插值,权重则主要基于下面两个变量计算得到:
- 颜色的一致性(对于Q来说,就是跟ABEF之间的颜色相似性)
- 运动向量速度的幅度
这里给了整个计算逻辑,针对彩虹六号的场景做了比较多的调制(所以其他项目就不能直接拿来用,得做些适配才能达到最佳状态),总体划分为1.4ms,相比起来,可以节省8~10ms。
这里就不对具体的逻辑做展开了,后续有需要再做深入分析整理。
TAA:
- 可以跟checkboard相辅相成,并且可以直接服用resolve shader
- 在这里可以只作用在sub-sample级别,针对MSAA的采样pattern进行抖动(听起来像是不用再调整相机的投影矩阵了?)
- Reproject逻辑也是完全复用的
- 唯一特别的是,需要增加一个额外的unteething逻辑
在checkboard rendering作用下,开启TAA会产生明显的锯齿形状的瑕疵,需要做一遍filtering来消除。
整个filtering只需要采样5次neighbor pixel(貌似都是一条轴上的),通过一个阈值d(颜色差值?深度差值?)来将相邻像素划分为两个阵营[0, d] & [1-d, 1],满足前者的为0, 满足后者的为1,其余为X。
如上图所示,这里是按照垂直方向进行划分的(但是像素的位次却是从左往右水平排列的),按照图中的叙述:
- 红色方框的像素是不会有teeth存在的,绿色的则会有,为啥?
- 这里介绍说,只有在01010跟10101这两种模式下才会存在问题
还是回到最开始的问题,这里的d是针对的到底是哪个变量?
另外,这里也没有介绍具体是怎么混合的。
后续的优化点可以考虑:
- 继续提升blend得到的像素的质量
- 调整权重与插值算法,使得其变得更为科学。
Bonus Slides
GBuffer的布局也给了介绍
GI是基于刺客信条Unity的GI简化而来,是一套烘焙方案:
- 用一套低分辨率的volume来覆盖整个场景,Sky Visibility被烘焙成一个SH,每个voxel覆盖1~2m
- 对于可玩区域,再叠加一套高分辨率的volume:
- 同样需要Sky Visibility的SH
- 叠加反射颜色的SH
- 每个voxel覆盖25cm
直接光的补充
SSR的一些细节:
- 1/2 x 1/2分辨率上渲染
- 基于face normal来实施tracing
- 通过随机调整起始位置和方向,借用TAA降噪
- 在相机移动的时候,将此前的数据重置为无效
反射方案:
- 采用cubemap
- 通过视差贴图来矫正
- 被当成光源参与Clustered Rendering
- 数据则是直接用array存储方便读取
- 在SSR中也会采用cubemap
- tracing失败之后,优先使用cubemap作为fallback
- local cubemap优先级高于global cubemap