今天学习的时Far Cry 5的大世界渲染方案,包含如下几个专题:
- 水体
- 物理真实的TOD
- 局部tonemapping效果
- 美术生产
Far Cry 5的基本信息,基于延迟渲染实现,引擎是沿用此前版本的引擎,并根据需要做了优化
图形程序团队分散在各个地方
视觉提升主要包含如下几个大的方面:
- 地形部分,在Moore GDC 2018的分享中有详细介绍
- 水体部分,在Grujic GDC 2018的分享中有详细介绍
- 全局光照
- 基于物理的光照
然而本文重点关注的不是一个个的特性,而是在实现开放世界游戏渲染中的一些挑战,尤其侧重的是,如何保障方案在各种情况下的适用。
主要包含上述四个部分
先来看看水体
水体有啥问题呢?
一般来说,我们渲染的顺序是如上图所示的,先不透+延迟光照,之后半透通过前向渲染完成,最后叠加后处理
那么这里水体是否是需要作为半透来完成渲染?
按照这种做法,会导致几个问题:
- 半透物件跟水体存在穿插时,不管谁前谁后效果都不对
- 折射效果(需要拿到水体的深度,这里说的是水深的深,而不是depthmap那个深度,这个深度可以通过两个depth相减后运算得到)以及水下的光照计算效果也会有问题(?)
这个是目标效果
一个常见的思路是在半透之前绘制水体,同时水体绘制的时候会写深度,从而拿到水深之后用于实现水体的折射效果。
不过这样一来,水下的半透物体效果就可能有问题了?
另一个问题是,水体表面不一定是平面(有可能有河流带来的斜坡),那么传统的平面反射(PR或者PPR)方案就不能用,这里考虑使用SSR来计算,而SSR需要采样深度图,并从Framebuffer中获取颜色。
之前的SSR是在延迟光照环节计算的,那水体自然就享受不到这个作用的效果了。
这里的方案是在延迟光照前,先加个water pre pass(只绘制深度),之后流程跟前面一样,从而可以将SSR作用到水体之上。
带来的新问题是,我们现在有了两张深度图,一张有水体,一张没有;其中部分效果需要使用前者(阴影、SSAO、光照等),部分效果则需要使用后者(SSR)
这里对具体的效果以及对应的输入深度图做了总结,简单来说,就是SSR需要带水体的深度,其他不用。
这里特意提到说,水体的阴影、光照以及大气散射等部分,是希望在水体绘制的前向管线中完成计算的,虽然这种做法会带来更高的性能开销(为啥要这么做呢?)
说到深度贴图,跟其他项目一样,深度贴图并不是一张贴图,而是一套贴图,各个特性需要的分辨率、线性与否等可能都不一样,这里设计了一个专门的pass来完成这些数据的生成。
这个pass作用如上图,一般是在GBuffer生成完成之后,通过异步compute shader来完成计算。
这里展示了可能用到的各种深度数据,其中线性深度贴图mipmap还针对需要做了拆分处理,因为绝大多数情况下,可能是只需要一张全分辨率贴图,不需要后面的mipmap的,所以拆分开来有助于节省带宽消耗。
给出了各个效果与上述各种深度贴图的连接关系。
给出这个pass的一些执行细节
执行两次会有两次消耗,最终通过分析认为,可以只对带水体的深度执行一次计算,这种选择的好处是:
- 可以保留水体的SSAO、SSS效果
- 可以在水体上通过延迟光照逻辑,实现水体上的阴影、大气散射效果以及雾效
带来的问题在于:
- 延迟阴影发生在水面上,水下部分阴影缺失?
- 光照的剔除逻辑可能会把水下的光源剔除掉,导致效果异常
不过貌似没有测试同学反馈bug,所以看起来还好。。。。
前面担忧的水下半透物件绘制异常的问题这里也考虑到了
这里的做法是将这些物件分成两部分,水上的跟水下的(是否还有穿插的部分):
- 水下的在水体绘制之前绘制(需要将水体设置为近裁平面剔除掉水上部分)
- 水上的在水体之后绘制(需要将水体设置为远裁平面,剔除掉水下部分)
- 穿插的应该需要绘制两次
伪代码给出如上
水上部分,直接用深度剔除就行,不用额外的剔除逻辑
水下部分则需要将水体设置为剔除平面,在vs中进行剔除
常用的做法是借用SV_ClipDistance的语法,但是实测发现这个是有较大副作用的,所以用了vs剔除的逻辑
前面是假设相机在水上的,那如果相机处于水下呢,上面的方案还能否生效?
那前面的方案在很多特性上表现就是不正常的,这里的解题思路是:
- 使用不带水体的深度来实现延迟效果的计算
- 使用水体表面作为近裁、远裁平面来实现水下、水上物件的绘制剔除逻辑
- 屏蔽掉SSR效果
做一个总结:
- 水体跟各种效果具有非常密切的关联,不能作为一个独立的问题来看待
- 实现的时候不要只考虑方案本身的实现时间,还得预留充足的问题解决与处理事件
- 很多问题并没有完美答案
后续的一些优化思路:
- 将水体clip plane的裁剪逻辑优化掉,改成逐像素的深度比对检测,从而适配倾斜水体的效果
- 考虑将部分剔除逻辑从CPU搬迁到GPU
- 对深度贴图处理pass做进一步优化,按需输出深度贴图
接下来看看基于物理的TOD的实现
- 太阳跟月亮的位置(以及月相)可以根据相机当前所在的经纬度来计算得到
- 之后从前人的数据中拿到太阳跟月亮的光谱数据
- 月亮的光照会基于跟太阳的位置来得到,月相则自动基于月亮的brdf得到(?)
- 太阳跟月亮的数据会被用在Bruneton的天空模型来计算大气散射
- 为了实现TOD效果,这里给太阳月亮k了11个关键帧(不是连续的?)
- 局部光跟天光遮蔽则只有一个关键帧(不考虑开关?)
这里带来的三个问题:
- 每天的表现都不一样(什么原因导致?)
- 夜晚看起来跟白天一样
- 基于物理的光照会导致对比度过高
先来看第一个问题
因为采用的是模拟真实天体的运转方式(真实物理模型也不一定好啊),所以随着时间的推移,太阳跟月亮之间的相对关系在每一天都是不同的
月亮的相位也会导致月亮光强的变化,此外,有的时候晚上是没有月亮的,光照效果可能就无法接受。
上述这些不一致就会导致前面说的太阳、月亮关键帧的结果跟预期效果不匹配,因为按照关键帧的逻辑,其中暗含的意思就是,每一天都是一样的,不然烘焙的数据表现就会失控
为了规避这个问题,部分效果采用了太阳的高度作为输入来保障效果
同样的,bake的GI数据只有正好匹配bake时的太阳、月亮让位置,才会有正确的表现
另外,这里还有一些新的问题:
- 美术同学希望任何时候都只有一个方向光(太阳、月亮二选一)
- 在时刻变化的情景下,光照效果就不可控
- 尤其是夜景
解决方案就是挑选某一天,之后基于这一天来循环,放弃了真实物理模拟方案
没太看懂这里的blend说的是啥意思
不过月亮有月相变化,这个要怎么处理:
- 计算的时候按照满月来
- 不过视觉上需要做处理(样子货)
- 需要根据月相调整月光方向,避免穿帮
再来看第二个问题
白天跟晚上效果一样(自适应曝光)
为了避免晚上太亮,需要对低照明度的区域做曝光降低处理
如图所示,加了一条曲线来实现这个控制
结果看着就好多了
Purkinje效应:长话短说,就是人眼在白天对低频光(长波,红色等)敏感,晚上对高频光(短波,蓝色等)敏感
要想实现这个效果,这里给了三种方案
从性能与效果来看,最合适的方式是给月光做蓝色染色,唯一的问题,中间视觉(mesopic vision)的玩家可能就没法被兼顾了
实现效果如上图所示
给出对比图
最后一个问题
这里给了几个例子
不同区域的光强量化数值如上图所示
一个想法是对日光的亮度做降低处理
比如统一下降4个单位
效果看起来还行
那如果调整局部光,是否也能降低对比度
出于对实际情况的模拟,白天一般是会关掉局部光的
且按照物理光照来看,局部光的效果也不明显
这里给了光源布点位置与亮度的效果对比图,左边放灯具之下,后者放在灯具里,左边是50流明,右边是500.
如果把GI去掉,效果更糟
先看看晚上光源的对比度问题
不同情景下的光强数据
前面两种光强相差4阶(即比值为2^4=16)
如果一个场景只受月光影响,那么其(什么东西的)直方图大概是上面这个样子,我们来看看四阶的对比度是否够用
这里先来说明一下这张图的设定
tonemapping曲线可以用橙色区域表示
假设每一个材料都是100%的diffuse reflector,那么我们可以用红色表示diffuse luminance(diffuse lighting * diffuse albedo)对应的直方图(场景里不同光强区域的占比?)
灰色部分则是最终的场景luminance(叠加了diffuse、specular、emissive等)
红色三角箭头代表自动曝光的计算逻辑(?),看了下,只有轻微区别
蓝色则是目标的曝光度,绿色则是当前的曝光度。
由于是静态的图片,所以这里两者是相同的,在动态的场景下,两者是有区别的,运行时会动态调整曝光值,使之逼近目标曝光
晚上的时候,会降低曝光度,以使场景变暗,通过这种方式来压缩月光跟天光的对比度,如红色箭头指示,只保留尾部的,右边更高数值就clip到最高值。
白天的对比度不变,但是调整曝光度,加大光照范围。
但是发现,情况变得更糟了。。。(?)
这里给出了参考的范围,最多只有13个stop,而夜景的范围是超出这个的,因此有些细节就会丢失。
转到HDR,22个stops就够用了
加大(显示屏?)亮度,还能实现更大范围
另外一个问题是,光照的范围应该怎么设置(半径),100米的局部光(500流明)其强度跟月光接近
1km外的强度则跟天光接近
这个场景中,如果在相机背后放置一个5000流明的灯光,还是能看得出来明显的照明效果的(?)
基于上述分析,我们有如下的结论:
- 采用物理灯光强度的设置,夜景下不能完全支持上这些光源的对比度
- 如果约束光源的半径,只会导致需要的对比度范围被进一步扩大(?)
- 夜景下常用的方案是调暗光源的强度,但这种做法又会引起其他的问题
上述问题的解决方案,虽然写的详细,但还是有些不明所以:
- 参考电影业的做法,通过lighting rigs来模拟月光(具体?)
- far cry5的做法是增强月光的亮度(调高下限?),同时叠加蓝色以掩盖光强过亮的问题(需要注意,晨昏效果可能会过曝)
可以看到,月光曝光度调高之后,跟局部光源就比较匹配了,同时叠加了蓝色之后看起来更真实了。
晚上的曝光度范围是-11到1
白天则是7~20
在上述设定下,如果放一个自发光(2~6EV)物体到场景里,就会在白天全黑,白天全白,这种情况下,粒子效果(自发光)就很难看了。
出现这种情况的原因是自发光物体的处理逻辑跟光源的处理逻辑不相同,光源会照亮环境,会改变直方图,从而使得自身的效果能够在一系列的计算之后能够兼顾白天、晚上的表现,而自发光则没有这个特点,而我们又不太可能将所有的粒子都转成光源。
解决方案是为自发光物件增加一条额外的bias曲线,在不同的环境(白天、黑夜)下通过添加bias来保证效果。
伪代码给出如上
总结:物理光照从思路到落地还有很多实现层面的坑点,需要通过各种trick来攻克
接下来看看局部tonemapping
经过前面的一系列处理之后,室内外的光照对比度还依然过强
针对这个问题,研发团队不希望通过hack光照强度比例的方式来解决,因为这个会破坏物理光照的正确性。
这里的一个解决思路是采用局部tonemapping方案,简单来说,就是将传统的应用于全屏幕的tonemapping改成局部生效,不同区域采用不同的tonemapping曲线,这里需要解决的几个问题:
- 如何分区
- 每个区域的tonemapping该如何设计
- 如何保障画面效果看起来是和谐的,区域边界是平滑的
这个话题bart wronski写过两篇blog来介绍其具体方式。
先基于Bart Wronski的阴影高光方案,在后处理中完成这个过程
我们可以计算出全局的曝光度跟局部的曝光度(先抛开分区的策略不谈)
大致实现思路,下面是理想情况:
- 在全分辨率的buffer中计算场景的log luminance(怎么计算?)
- 对这个buffer采用一个大尺寸的双边模糊算子来得到一个模糊(基于颜色,而非深度,说的是啥意思?)
- 基于上一步的结果来得到局部曝光度(具体怎么做?)
但实际上消耗太高,需要调整一下:
- 第一步计算log luminance的时候改为低分辨率buffer中进行
- 因为第一步采用了低分辨率,所以第二步同样也就不用在全分辨率下进行了
示例场景
得到其luminance
得到低分辨率版本的luminance的log结果
做低分辨率的模糊
将原图与模糊后的明度图做减法(?),就得到了高光部分减弱的版本
但是这种做法,由于减去的部分是模糊过的,因此会在一些区域留下halo效果,导致部分本应该是明亮的区域结果没有明亮,本应该是暗淡的区域没有暗淡。
这里做了对比,上述红圈标注的区域都变暗了, 美术同学认为这个是不能接受的。
这里对这个方案做一个总结:
- 实施快速,非侵入式
- 会有halo瑕疵
这里直接针对问题着手进行解决:问题出现的原因为室内过暗、室外过亮。
一个思路是将两者区别出来,将算法只应用于室外部分。
通过two-side特性来判断是否是室内建筑,下面看看这个方案的实现效果
无曝光处理的效果
对portal添加曝光处理的效果
用红色标注portal范围
这里有一些区域处理起来会有问题
根据到相机的区域添加一个fade效果,看起来会更好一点(?)
做一个总结:
- 即使做了相机距离的fade,还是有些情况处理起来不能令人满意
- 会增加美术同学摆放portal的工作量
- 会跟其他blend物件产生排序问题
- 计算复杂度高
一个新的思路是借鉴刺客信条Origin的方案
前面之所以要做一个双边模糊,主要是为了实现对屏幕空间像素的区分,找出那些在3D空间比较接近的区域,并将之在2D屏幕空间区分开,之后基于3D空间的距离来得到平均的亮度。
如果我们本身就能拿到3D空间的平均亮度,那这个过程就不需要了。
而这个信息在Far Cry5的GI系统中是可以拿到的,GI中存储了:
- 局部光源跟太阳光的间接光照
- 天光遮蔽信息
算法修正为:
- 基于当前场景的曝光度,计算得到一个参考的middle grey
- 基于天光跟间接光,计算得到当前像素的平均luminance(一个kernel覆盖范围内的平均值),这里需要忽略直接光(因为要的是平均值)
- 基于上述两个数值的比对来对光照结果进行调整
看下实现伪代码,下面看下实现效果
计算GI的平均luminance(移除直接光)
基于上述数值,得到tonemapping的factor,这里可以看到,室外的factor是低于室内的
看起来似乎没有太大区别,但实际上也并不需要有多大区别,只是解决了前面的几个问题:
- 室内跟室外有了一个较好的对比度
- gameplay & 美术同学的问题都得到了解决
总结
还有一些问题希望在后续的工作中被覆盖。
最后看下美术生产管线。
背景:
- 资源生产量大
- 美术同学期望提供较多种类的资产,避免重复感
- FPV对贴图精度要求高:6 texels/cm
- 贴图还需要做重用
- 为了得到较好的效果,材质之间还需要支持混合
下面用两种材质(white wood跟bare wood)举例来介绍材质混合
公式给出如上,实际上是各种类型的贴图的混合,不过mask要怎么得到呢?
对于左边的建筑,期望用右边的一张贴图来作为mask,贴图的好处是美术同学控制起来比较方便,就是分辨率不能太高(内存、带宽?),导致精度有限。
这里的解决方案,是叠加一层可以tiling的detail mask
两层mask带来的问题是,如何实现两者的组合
首先想到的是直接相乘
按照相乘来看,如果unique mask是0,得到的结果就是材质1,而如果unique mask为1,得到的就是lerp公式,那么要想得到材质2,unique mask得等于多少呢?
针对上面的问题,如上图所示:假设以顶点色为unique mask,detail贴图为detail mask,按照乘法逻辑,我们永远没有办法只通过控制unique mask来得到100%的贴图1的效果。
我们期望的结果是这样,当unique mask为0的时候,得到材质1,为1的时候得到材质2,为0.5的时候,才是lerp。
再把sharpness参数放进来
通过sharpness来控制detail mask的最大幅度
unique mask将可以用于实现detail mask的滑动控制
根据unique mask的数值是0,还是1,detail mask的符号会有所不同
当unique mask处于0,1之间时,detail mask则落在上述范围里
所以我们最终期望的公式形式应该是这样的
根据梯度、偏移来对上述公式进行代入求解
得到这两个参数的表达式
代入得到最后的公式
注意上述代码中的mask offset参数,通过这个参数可以调整unique mask的bias,从而用于:
- 固定开启或关闭某个材质
- 用于实现材质的动画混合效果,比如用于实现潮湿效果
按照上面公式得到的最终mask结果
集合混合结果
以及最终混合结果,看着还是挺厉害的
材质混合做一个总结:
- 可以实现多种丰富且独特效果的材质
- 可以支持高精度的材质表现
- mask方案的内存消耗相比于其他方案在内存、带宽上消耗更低
- 代价是需要额外的几次贴图采样(PC上影响不大,移动端会有点危险)
还会增加ALU消耗,以贴图旋转为例
需要成本高昂的sincos指令,一个想法是能否将这个指令提前预计算,运行时直接取用?
每个材质类型,都会有一个与之匹配的描述文件,记录了:
- 材质的shader
- 材质的参数
- 用户界面的一些数据
而在这个文件里,我们可以将一些数值提前预计算好,作为shader参数传入以降低运行时的shader计算消耗
这里是处理流程
角度参数预计算变成shader参数
这里还有几个更为复杂的案例
这些都可以通过描述文件来做设置(只看到了部分数值提前预计算的优点,似乎可以将这个能力集成到UE的蓝图中,允许美术同学将部分计算转换成shader参数来避免运行时的高频运算)
开放世界研发的一些感受:
- 研发困难多
- 很多效果是彼此连接的,不能独立看待,一个特性的改动需要考虑对其他模块的影响
- 会出现很多预想不到的问题,方案与落地之间,存在巨大的鸿沟
- 在方案选型的时候,更倾向于选择那些普适程度高的,相对简洁的,维护与适配成本会低很多
- 还有很多问题可能没有一个非常好的解决方案,经常需要通过一些trick手段来解决