CEDEC2016 生化危机7的渲染技术
本次演讲是Capcom的渲染组组长三嶋仁和渲染组的同事清水昭尋,以实现生化危机7的渲染技术为主题来做分享。
为了生化危机7Capcom开发的了新游戏引擎RE ENGINE,是是为了可以高速游戏开发而提升迭代效率,以及游戏中的运行效率两点而开发的。另外高速游戏开发方面,是在下午在石田的演讲中介绍。本讲座主要是运行效率方面。引擎的开发和游戏开发是并行进行的,经过了2年半的时间。最终目标是1080p分辨率,60FPS,为了实现这个目标而进行了各种的优化。这次来说说渲染相关的话题。
这次要讲的,主要是关于实现部分的。关于渲染的GPU的设计概念的内容,可以参考今年春田GCC上的资料。http://www.slideshare.net/capcom_rd/gpu-59175056 。 这次的分享,三嶋主讲光照部分,后面的模型和特效部分由清水来介绍。
首先,先看下Frame和Cost,也就是现在的RenderPass的状况,在开始的Common中,进行褶皱的计算,角色的印章,法线的再计算以及水面的计算。再根据情况,进行阴影的Cache拷贝在Gbuffer里进行Decal和水面的操作,这点也没什么特别的。根据场景Cost有些变化,不过大概是这样。Cost比较重的,主要是阴影,光照和半透明。增减特别大的是阴影和半透明部分,在RE引擎里,使用的是异步的计算。
那么,从光照开始讲。这个画面和6月在E3上发表的体验版是相同的场景。
在生化危机7的光照上,为了性能考虑,以尽可能简单的构造为目标,首先是只实现了延迟光照,排除了前向光照,引擎也支持前向光照,但生化危机7里没有使用。前向光照的自由使用,在下一款游戏中会使用。然后是有一体感的光照。也就是动态物体和背景使用同样的照明,为了实现这个目标使用了IrradianceVolume和Local Cube Map,来配置空间的间接照明。之前在生化危机6中使用的光照贴图也被排了。通过这个方法,运动的稍微移动,环境变化也可以对应。
直接光照上,支持标准的光源,各种光源可以进行阴影计算。另外,光源的剔除是两个阶段进行的。第1个阶段是判断阴影是否显示的Occlusion Cull,这个是有帧延迟的第2个阶段是,确认实际的光源在屏幕的位置是否可见,这里是利用了深度信息的Clustered Shading,尽可能的把不需要的光照剔除掉。
首先是想对漫反射(Diffuse)做一下说明,先要说明的是,使用了全部一样的光照(着色)模型。因为也需要叶子和窗帘、灯罩的表现。这里为了能够用最简单的计算,把Lambert和Isotropic Scattering用半透明率来做混合。得到了下图的结果。图的右侧是透明状态,左侧是不透明状态。本来是需要沿着深度方向做衰减计算的,但这样也可以调的差不多,就采用了计算消耗低的方法了。
然后是镜面反射,基本上采用了和OptimizeGGX近似的算法。下面的图中,左侧是优化的计算模型,右侧是原有的计算。脸颊部分的亮度多少有些不同,这个是因为光源和视线角度相对时(Grazingangle 掠射角)的颜色会变浅。因为整体的一眼看上去没什么问题,就采用了。
在之前,简单的介绍了直接光照。这里相对显示直接光照上非常重要的Lightculling进行说明。在引擎中,是把32x32的像素作为一个Tile,深度方向进行16等分的3D纹理上保存LightCulling的信息。把Tile中深度的最大值和最小值沿着视锥体来作出AABB。中间的图中,三角形的圆锥是视锥体,蓝色部分是深度方向的Cluster单位的AABB。生成的AABB,可以与聚光灯的形状进行交叉判断。这时使用的形状平面,用AABB进行交叉判断。 这种处理,可以的话会用异步计算来执行。
LightCull的Cluster的构造,是3D Texture的ClusterGrid,用线性列表的Light List的两个层来制作。第1层,是称作ClusterGrid的视锥体分割的部分。 1080p分辨率下,使用了60x34x16的3DTexture。每个纹素,保存了32bit的数据,其中的24bit是LightList的偏移地址。而剩下的8bit是实际使用的LightList的信息,作为MaskBit来使用。是否使用Mask Bit是根据实现的情况而定,为了减少内存访问可以加入。第2层的每个Lightlist,各个纹素最多8个连续的32bit的数据。为了访问,需要Cluster Grid的偏移,每个Bit单位,保存了LightIndex的显示状态。因为是用32bit单位来保存light index(2014deep down里介绍里,每个光源用32bit来保存他影像的Cluster的信息),在光源数量增大时会有增加内存使用的问题,用bit单位来管理限制内存的消耗。另外,使用的光源也有上限的,现在是超过512个就不对应了。
看一下实际的光照数据的引用的代码。这个是基于avalancestudio的humus提出的ClusterShading实现方法的改良版。因为也不清楚会利用到什么程度的光源来实现的,上限设置了512个。但这种方法因为使用了两重循环,在寄存器方法稍微有些微秒。如果光源数不到128个,LightClusterGrid是128bit的格式,一次加载就可以取得所有值。这种以Bit单位来处理的好处,在Light Culling方面也有好处。把光照的Culling的判断结果保存在LDS(LocalData Storage)上,消耗也能更少。
那么,接下里是对LightCulling使用的深度方向的分割的说明,最初是按深度方向线性分割的,虽然可以使用,但是线性分割的话,近处的分辨率会有偏重产生,结果近处的光源可以很好的Culling,而远处的Culling的就成问题了。这样,对深度方向做对数分割,多少有些线性。用这个方面,远处和近处必要的光源都可以很好的Culling。和左侧相比,右侧对整体近景的分辨率更强,尽可能的让远景集中一些。
那么光照方面的进展,这个是6月E3发表的体验版相同的场景。
内存访问这部分可以高速化么? 今年的SIGGRAPH上的演讲,通过内存方访的技术可以实现高速化。GPU是多线程来运作的,以Thread 块的单位来进行图形处理,这个块可以叫做wave(也叫做wavefront,是AMD的叫法,Nvidia里叫warp),在wave里运行的是同一个的ProgramCounter。在不运行的情况下,用mask来让线程休眠,必要运行完成后再继续运行。右下的图中,把一个Wave的内部的Light index是一样还是不一样的做可视化显示。红色部分,是Wave里获得相同光源信息的部分,绿色是Wave里取得不同光源的位置。通过这个图,光源边缘附近不同的光照也可以很方便的看出,这之外大体上是同一个光源。这样,根据GPU的不同,wave是读取相同内存的情况,除了通常的各线程单位的加载外,wave单位的加载也是可以实现的。如果可以这样做,就可以高速的获取GPU的值了。左下的图是这个代码的示例。因为有ShaderModel6.0,通过这种方法可以进行高速的数据加载。优化后,至少也是0.5ms的速度提升。
接下来要说明的是阴影的制作。在生化危机7中,因为没有lightmap,所以Shadowmap变得重要起来。在游戏中,最大可以同时使用32个阴影,使用Texture2DArray来管理。但是,因为所有阴影的更新会影响性能,在光源不动,且物体也不动的情况下,用Cache来渲染。
关于阴影的Cache,大概是下面这样实现的。用比程序更好理解的文字方式来表达。
最后说下阴影拷贝的策略。根据平台的不同,在可以使用ASyncCompute或者DMA的环境,是在绘制处理开始的地方开始拷贝,和后面阴影的绘制同步进行。其他更主动的优化方法,可以在前一帧安全的位置进行阴影cache。不能使用上面特性的场景,就要在绘制阴影前在拷贝Cache了。
阴影的拷贝,并不是没有消耗的。假设一个拷贝要0.1ms,32个全部拷贝就是3.2ms。实际上还有也很多更恶劣的情况。所以,也要有不做拷贝的选项。
只有阴影的视锥里有动态物体存在时才做cache拷贝。减少不需要的拷贝,提升整体的性能。
接下来是关于间接照明的说明。间接照明是IrradianceVolume和Local Cube Map的组合使用来解决。这个是在compute shader里在同一个shader里处理的
IrradianceVolume上,使用的是内部数据结构分离的方法。把保存Probe之间的链接关系的网格网络,和Probe自身的Irradiance信息分离的。这样要追加照明状态的变化也很容易。
Probe之间的连结关系是用四面体结构来管理的称作Probe Network的技术,是参考的[Cupisz 2012]Light Probe InterpolationUsing Tetrahedral Tessellations 和 [Valient2014]Taking Killzone: Shadow Fall Image Quality Into the Next Generation实现的。以前有用过Volume Texture来保存,但因为是离线的制作的,这次采用了新的方法。Probed 的位置生成,是在引擎中把背景的多边形数据体素化(voxel),来生成probe的位置然后,用Mipmap把稀疏的Probe做成两个层级。用Probe的位置做成四面体集。最后把四面体的空间构造变换成BSP Tree,用变换成适用GPU的Stacklesstree(无栈树)的形状来保存。
Probe的network大概就是这个样子
那么Probe网络生成后,接下来就是在probe里生成Irradiance。这个也是在引擎里制作的。按「2012]Deferred Radiance Transfer Volumes:Global Illumination in Far Cry 3的方式来保存数据 引擎中把4个方向的颜色信息保存在R11G11B10Float里。把这个在实际的shader里用低精度来复原使用。
这样使用,外观多少有些差别,但性能更好一些。
因为light probe的位置信息和颜色信息是分离的,所以可以比较简单的进行调整。例如,傍晚和夜晚照明的变更,替换少量的数据交换就可以了。
可以看下Probe的结果,整体很平台的效果。
使用直接照明的效果。因为透明度是最高的印象可能会有些不同。
最终画面。
虽然可以就这样实现,但像素单位的搜索消费还是太高了。这里进行了3个优化,首先是增加了uniform grid,把现在的位置加入到BSP-Tree的Shortcut里,然后,使用Reprojection,这样可以复用前一帧的结果。最后,是异步的Overlap操作。
首先是Uniform Grid。 把现在的位置转到写明的Grid位置来判断,实现消耗也会少。把空间分割为32x32x32的Grid,利用格子内的四面体来做成BPS树,通过这种方式,把集中在一起的四面体,转换成了大型的树结构,实现了内存的消减的运行速度的改善。
接下来解决的是Reprojection。在前一帧的间接照明计算时,保存使用了的四面体ID,通过当前帧的世界坐标,来取得前一帧的屏幕坐标的四面体的ID,在当前帧的位置上再次使用。如果不能再次使用,就要在BPS树里搜索了。下面的图像中,红色的部分表示再次使用前一帧结果的像素。左图是低速摇头时,再使用率的显示。右图是降低游戏控制器的最大速度时的显示。
可以看到有不到一半的再使用率。这种技术,如果两帧之间视差越大也越容易出错。
通过这种优化,提升了0.5ms速度。
最后的对策是在阴影计算的时候在底层进行的方法。因为这个计算是要在SSAO一类的优先级来计算,在异步上用适当的优先级来运行
使用了Getting MorePhysical in Call of Duty: Black Ops II 的想法,对Cubemap的亮度做再归一化使用了下面的计算公式,修正后游戏中的不协调感得到了很大的解决。
这张是修正前的画面。一部分曝光过度了,感觉是被外包的IBL影响了。虽然整体的不协调感很少,但到了冰箱前多少会觉得像是垃圾箱。
修正后的画面,整体平整的感觉,副作用是橱柜变亮了,不过应该没有不协调感觉。
把之前的做大致的说明,间接照明大概是1.5ms的程度。问题主要是以下这些,Network的构筑太慢,品质还差点意思,Irradiance Volume等的重新光照的引擎烘培时间,等各种问题。今后会考虑Light Leak(漏光)的对应和Relighting(重新打光)的优化。
最后是整体照明的最优化做说明。在引起里,根据不同的功能来组合来改变参数输出模式。使用次级表面山色情况输出模式增加到3个参数,再加入屏幕空间反射。
接下来网格渲染部分的介绍
在当初生化危机7引擎开发时,也探索了新的网格管理方法,以减少DrawCall数量,和GPU上高效Culling使用为目标的IndirectDraw的使用也很早就进行了验证。另外还有适合IndirectDraw的统一缓冲的管理方法也验证了。这些概要可以参考Capcom之前分享的资料。
Drawcall使用的参数结构体是20byte,保存Mesh的Instance,TRS和Joint的信息结构体是112byte。以前的mesh是把参数信息分别来管理了,这次的引擎中是由Render汇总管理。每个固定长的缓冲使用了数MB的样子。
操作Indirect 参数的Instance数就可以操作实际绘制的数量。Culling是在同一帧内,相机和HiZ生成后在绘制前的阶段异步来进行的。
Culling方案之一的Cluster Culling。所有的法线汇集到平面的一侧方向的backface Culling也是可以的
对Cluster进行分隔。
三角形单位做Cluster画的结果,因为Mesh做了索引优化,在一定程度解决了,Mesh边界大的话,Cluster也会多。三角形数 24109,Cluster数 98,有效Cluster16个,根据排序来优化绘制顺序。
最后是Mesh部分的问题和展望,首先旧的想法的限制下无法进行大胆的设计变更,缓冲相关是一次DrawCall只能使用GPU可见的部分,尽量的整合是很重要的。还有就是因为材质参数和纹理资源的差异的缘故也要额外使用DrawCall,设计必须要进行改良。 考虑最好是常量缓冲(Constant Buffer)使用StructuredBuffer,纹理使用BindlessTexture。生化危机7中Culling相关并没有做到大幅度的处理削减。因为GPU Culling的不会改变Draw Call,只是纯粹的减少顶点处理,所以要配合CPU上的DrawCall削减进一步的有效使用。还有就是进行ClusterCulling时,距离顶点位置越近越不好,法线方向也尽可能的同时整理必须进行事先的优化。
然后是流体模拟部分的讲解
移流計算使用了2次精度的MacCormack方法,1次精度细节会有问题。
分辨率是64x64程度的Jacobi,缓冲因为PingPong的缘故会有Stall发生。用多个模拟器同时运行的方法来回避。
对模拟器增的交互和外力的追加。
调试绘制中显示速度,用速度发射器生成速度,多个发射器也可以对应,用障碍物给予速度影响。发射器配置在3D空间与Billboard的碰撞判断。通过移动模拟网格和惯性来产生速度。密度发射器产生的密度通过速度缓冲进行移流。
视频1 模拟器外力的动画
模拟分辨率是低(64x64多),Sprite分辨率是4^2=16倍(256x256很多)。负荷并不是线性增长的,要通过异步的计算来混合。
视频2 流体实例
接下来,介绍流体水面。
绘制可以用Gbuffer Pass和半透明 Pass来支持的。生化危机7中是负荷的考虑,使用了Gbuffer的绘制。
利用LDS(Local Data Store)来共享内存。
在Culling的Shader里构筑IndexBuffer。
实际配置水面的状态,感觉是稀释顶点,再用控制高度的贴图来混合。交互的是胶囊形状。
视频3 水面的美术控制
水面支持Gbuffer Pass和半透明Pass 绘制,生化危机7使用了Gbuffer绘制。在Gbuffer里复原位置需要读取的Z,所以用了两个Pass来绘制。
把混合系数的实际值替换成更容易看明白的数值。
模拟使用的临时缓冲的是用Pool的方式来优化。美术对消耗的理解方面很难的技术,还是要进行适当的分辨率自动化。关于水面的高频的Artifact很容易出现的问题,漂亮稳定的模拟对参数很敏感的。
没有Specular参数,以后追加。