Origin
zhuanlan.zhihu.com
Tags
风格化
渲染
经验分享
项目
异度之刃
收藏夹
创建时间
收藏类型
Cubox 深度链接
更新时间
原链接
描述
b 站有人上传了 xb3 的 cedec 两个 talk,其中一个是关于角色渲染和升采样的,前几天看了下做个笔记,前半部分 npr 渲染,后半部分是超采样。原视频在此:【CEDEC+KYUSHU2022】异度神剑 3 角色表现与升采样技术官方讲解_哔哩哔哩_bilibili
我不懂日语,而且 npr 部分没有很深入了解过,所以有的地方细节不一定理解正确。我对 xb3 npr 渲染最大感触就是 mio 渲染的水灵灵的。另外建议搭配流朔:Xenoblade3 渲染研究的模拟器截帧阅读。

NPR 角色渲染

我对 NPR 研究不多,这部分就图一乐。
xb3 特点是场景和物品以 pbr 写实为主,角色使用 npr 进行风格化描绘,有点类似荒野之息的感觉。另外用了延迟管线,怎么把 npr 里需要的额外风格化数据存进 gbuffer 里而带宽开销还能保持住也是一个极大的问题,尤其考虑到 ns 那 lpddr3 的内存,就连基础 pbr 需要的 gbuffer 其实也比较吃力。
notion image
不透明物体是标准的延迟渲染 + cluster light culling 管线。常见 gbuffer 比如 albedo,normal,metallic+roughness,depth 都有。

Ramp Map

npr 下主要是对光照做离散化处理,为了提高美术操控性一般都会引入一张 ramp map 对直接光照结果进行映射到对应的亮部和暗部色彩上。在 xb3 里角色不同部位(脸,头发,不同材质的衣物)的质感不同,所以每种材质都有单独的 ramp map。
notion image
但考虑到不同环境光下色调都不太一样,所以在不同的环境光下(比如白天,晚上明亮的地方,晚上黯淡的地方)应该制作不同的 ramp map(美工这么肝的吗,早听说 xb2 的 ramp 复杂无比,不同天气都有不同 ramp)。
notion image

NPR 曝光控制

场景是基于物理的的,但是角色是 npr 的,两者曝光都不上,很容易产生角色过曝的情况,所以类似 Infamous Second Son 通过调整 diffuse(Infamous 里是 albedo)控制曝光。这个假设在基于物理的情况下肯定是错的,直接导致漫反射和曝光互相影响不说,也完全忽视了高光的贡献,所以 Infamous 后来没用了。但是反正 xb3 都 npr 了,管什么物理。
notion image

Shadow Offset?

notion image
看不懂日文,不过看图意思应该是诸如锁骨之类凹陷地方应该比别的部位更容易出阴影,但是纯靠法线很难体现这种效果,所以给了张 shadow offset/bias 的图把这类位置的光照往 shadow 方向偏移,更好的体现轮廓,也是常见操作。

笔触模拟

notion image
诸如衣物之类的材质不像人脸这么平滑,而且也希望模拟出三渲二画笔的笔触感。一般来说是屏幕空间采样一张笔触贴图来处理,但是 monolith 直接把笔触感做到法线贴图里。好处?节省一次采样吧,也不用碰 gbuffer,但是实际上后面也采了一张笔触图(再次把活甩给美工)。

多光源光照合成

notion image
xb2 里点光源和聚光源的光照计算只考虑光源离物体的距离衰减和光源自己的开角,并没有考虑入射光和物体表面的方向关系。这个主要是因为和平行光同时叠加起来的时候,复杂的光照会造成脸部阴影非常杂乱,所以就忽略了脸部法线的影响,角色的点光源光照结果看上去是非常均匀的。所以 xb2 在阴影处,没有了来自平行光的直接光影响后,缺少细节的间接漫反射和局部点光源光照都会角色看起来过于 “平”,有时候甚至有一种莫名 unlit 的感觉。
xb3 里存在大量人造设施的夜晚场景,需要靠点光源打光,所以这个多光源叠加的问题还是得解决。
做法也不复杂。首先比如如果有两个光源,那就分别计算两者的三渲二光照结果,然后根据 点光源在该像素表面的亮度 在平行光 和 点光源的光照结果里插值。实际上就是根据亮度的权重平衡多个光源的贡献,让重合区域的亮度不至于过高而导致和别的地方产生显著差异,又能体现一定的方向性细节。
notion image
具体这个亮度怎么算还有点细节,但不会日语我就扔个截图。
notion image
notion image
notion image
上面是结果图,箭头是我加的,橘色是来自右边的强烈点光源,同时还有左上方的月光,当然这是过场,可以手动打光所以效果比较好。实际游玩的时候白天如果路过营地里的点光源的话,光源本身的黄色会给人物皮肤和衣服都加了一个软软的 tint,观感其实有点奇怪。我觉得亮处其实可以完全忽略点光源的影响或者忽略颜色(就好比白天其实看不出路灯开没开一样)。
当然这方法也不完美,如果两个点光源都很强的话,阴影会很杂乱,而且多个点光源之间的过渡观感也不够协调。
notion image

局部光俯仰角限制

notion image
室内有很多从头上打下来的吊顶灯,直接计算不处理的话,人物的阴影轮廓比较杂乱,所以会把光源的俯仰角限制在水平面上下 40 度。蓝色协议应该也是这么做的。
notion image

次表面散射模拟?

notion image
其实就是用边缘光快速模拟次表面散射,也就是皮肤的透光效果。这里区分了亮部和暗部的散射光,亮部由环境光提供(应该就是指间接高光部分),暗部的颜色是 两盏光里最亮?(存疑)的那个,这个加了之后就很明显感觉皮肤粉嫩很多。

面部渲染

notion image
三渲二都会碰到面部阴影杂乱的问题,所以要修改面部法线进行调整(但也导致游戏里容易产生阴阳脸的情况)
notion image
notion image
游戏角色头部也会频繁的有小幅度的转动动画(比如点头),虽然修改了面部法线以及对光源方向做了钳制,但这些小晃动还是会导致阴影在时域上的不稳定。考虑到头部上下运动普遍较小,所以在计算面部光照的时候我们可以无视一定范围内头部仰角的影响。为了逆推世界信息,需要头部坐标系进行计算。(其实这页 ppt 我就没看懂几个字,我猜他是这个意思)
但是 xb3 里所有不透明物体渲染都走延迟管线,如果要加头部坐标系的话,什么都不压缩也需要额外 float3x3 的带宽,这个开销太高了。为了优化,xb3 假设同屏最多出现 256 个角色(相当富裕了),这样每个角色可以用一个 int8 的索引表示,然后把每个角色的头部坐标系存进一个 256 长度的 uniform array 里,比直接 sbo 访存要快。
notion image
notion image
不过实际站桩动画的时候还是能看到杂鱼 npc 的脸上有没做处理时阴影的杂乱情况,我不知道是超过了设定的 10 度阈值还是别的原因导致的。
之后还得在脸部和颈部之间做一个光照过度,不然接缝处光照会出现对不上的空档,同时眼睛着色也不需要这个(可能高光细节比较重要吧),这里就是给了个 mask 进行过渡。
notion image
解决完仰角了问题后,脸部沿 y 轴旋转在两个角度之间过渡还是会产生阴影杂乱(掰了法线也没法完全解决)。monolith 就加了张模拟画笔的笔触纹理图进行扰乱,尽量让过渡看起来自然一点(我觉得凑活吧)。不过我觉得这个用 sdf 可能就没这么麻烦了,但 monolith 可能想全部走延迟渲染吧。
notion image
面部阴影另一个重点就是伦勃朗光,或者说暗部半边的三角形亮区,比如图里我圈出的位置:
notion image
xb3 的做法是根据头部坐标系相对的位置来判断,但我没太看懂这具体怎么算的?如果用 sdf 做阴影 ramp 的话,这个三角光可以直接在生成 sdf 的时候指定进去。
notion image
我个人认为 xb3 里角色渲染最点睛的地方就是鼻尖油脂的高光,显得人物特别水灵。这就是一个画好的圆片贴图,但是为了在侧脸的时候也体现出效果,诸如嘴唇,鼻尖高光的贴图都会根据视角做一定的偏移来保证可见度,防止从侧面看只剩下薄薄一片了。另外眉毛也需要做一个 z offset 之类的效果,不被刘海遮挡住。
notion image

头发渲染

notion image
现实世界里头发的恶高光是各向异性的,一般会有一道圆环一样的高光形状。这种手法最简单的处理就是画死在头发贴图上,但那就缺乏效果了。monolith 意思是用 “球面环境” 表示这个信息来提供一定的动态效果,我没有很清楚这个意思,但我觉得大概和 matcap 差不多?用 Normal 和 View Direction 去查表。
另外这个计算也是在头部坐标系的,避免了人物倾斜时候高光环大致保持和头部平行的效果。
notion image
头发当然也要做次表面散射效果,这个应该就是用 fresnel 模拟的做法。xb3 这里把头发近似成球体表面法线来计算,这样边缘更平滑一点(群友问,如果是马尾怎么办?)同样也是在头部坐标系下计算。
notion image
这样处理完之后头发是类似下图左边那样的。其实我觉得还蛮好的,但是他们嫌和别的材质差别不大(可能有点油了?)所以把头发再抠出来做一个 SNN 的滤波。
notion image
notion image
额发之类的刘海会对脸部造成阴影效果。这个在 xb2 里有一个屏幕空间的 contact shadow 提供角色身上各种短距离阴影细节,但是 xb3 里直接改成暴力半透明贴片(美工:我去)。这个好处是开销很低,精度很高,而且可以实现类似影中影的效果。不过实际头发 mesh 有动画的时候这个需要映射到这个发影面片上,而且由于这个面片没有光照计算,很多时候和周围环境光的颜色不匹配。
notion image

描边

背面外扩法。顶点色里 rgb 存描边颜色,alpha 通道我没看懂但我猜是描边厚度之类的。然后下部分意思好像是说如果描边对应的屏幕空间太小了的话就不显示了,防止严重的锯齿。没什么特殊做法。
notion image

肌肤光晕

notion image
会对皮肤做一个额外的 bloom 强调以达到一张皮肤上的柔光效果,确实非常好,有时候晚上看 mio 身上像发光一样。这合理吗?不合理,但是好看。
这里有一个 bloom 的小优化就是对于 hdr 色彩(也就是颜色范围大于 1 的部分)使用类似 rgbm 的方式表现,这样节省下 lighting buffer 的带宽。

小总结

总的来说大部分都是常规方案,美术比重很大。多光源合成思路挺好的,虽然还有点缺陷,但实际游戏里察觉到的次数并不多(那个过渡问题 ppt 里不放我都没感觉)。游戏里表现的最大问题还是阴阳脸,其他的话,角色表现确实没得说,比 xb2 好了太多,很少见的游戏 3d 效果比 2d 立绘更生动的。monolith 提到了后面会研究 NPR 怎么更好的和全局光融合的问题。

时域升采样

其实我写这个笔记主要就是为了这部分( 一开始我以为 xb3 没用时域升采样的方法,为啥呢?因为群友截帧发现就是内部渲染 720p+taa,我特地问了有没有 taau,结果说没看到。后来才发现 monolith 玩的比较花。在过场动画的时候由于可以精确手调各种模型光源位置,所以优化的好,分辨率能稳在比较高的地方,因此 monolith 这时候会把分辨率锁死在 720p(主机下),然后再做个 taa 抗锯齿。但实际游玩操控部分,没有办法固定机位优化,所以分辨率会低很多。主机下常见内部分辨率 544p,实际战斗激烈特效翻飞的时候还会再掉。这时候 monolith 用了一个棋盘渲染把分辨率拉到 1080p 去输出,所以差不多是一个 1:4 的升采样。
notion image
先看下 xb2 的数据,主机下最高 720p,掌机最高 540p,动态分辨率 + taa,半透明分辨率再砍一半。由于 xb 系列在 ns 上画面堆的比较激进,视距远,阴影远,怪物多,ssao,ssr,体积云啥的都上了所以分辨率目标经常很低(和 wiiu 上的 xbx 形成了鲜明的对比,15 年了结果材质还不是 pbr)。m 社想,既然如此那干脆就分辨率整低一点,然后靠升采样把清晰度来回来算了。
xb3 的 taau 方法基本就是 call of duty 在 2017 年的分享,部分借用了 horizon zero dawn 棋盘渲染那篇的思想(Decima Engine: Advances in Lighting and AA),详情可看此:
这里我就不按他 ppt 顺序放了,按我个人总结的思路大纲。

渲染管线

首先看下整体管线:
notion image
低分辨率渲染不透明和半透明物体(半透明物体分辨率还要是这个 “低分辨率” 的 1/2 大小)以及各种后处理。然后应用 taau 升采样到高分辨率,再跑一个 fxaa pass(但这里分辨率有特殊),最后绘制 hud。演讲者表示 hud 除了字体都是按 720p 准备的资产的,我不懂啊,你输出都 1080p 了,hud 整成 1080p 很难吗。
碰上动态分辨率调整,内部分辨率也降低的时候,升采样的输出分辨率也会随之降低。主机下输出 1080p,掌机下输出 720p,但实际上掌机下内部渲染分辨率最高能在 440p 左右,升采样完 880p 再降回 720p 反而有超采样抗锯齿的效果。
notion image
放个对比图,左侧是 720p + taa 抗锯齿,也就是没有升采样的效果,右侧是 544p taau 升采样到 1080p 的效果。这对比有点怪怪的,也没控制变量而且我觉得效果一般。
notion image

棋盘分布

notion image
xb3 的超采样是 1:4,也就是说每 2x2 的 4 个像素里,我们每帧实际渲染的只有 1 个像素,其他三个像素都是重构建出来的。在这 3 个像素里,其中 1 个是时域重构,也就是重投影到上一帧实际渲染的像素内容来当作这一帧的额外像素,这就需要我们对实际绘制的像素做一个抖动 jitter 操作。一般的 taa 之类的会用 halton 啥的,但这里因为是和 cod 那套一样只做两帧 jitter,所以是在对角线上 jitter。参考上图右上角的话,每两帧间隔的绘制 2x2 像素里左下角(蓝色)和右上角(黄色)的像素,形成对角线。如果第 N 帧绘制蓝色像素,那么他当前右上角的黄色像素就来自于重投影后上一帧的黄色像素。这就是时域超采样的部分。
temporal reprojection 完了之后我们才做到 2 倍超分,还剩下 2 个像素没填,这就需要做一个空间性的超分,也就是根据邻近的像素来填补空隙。这里 xb3 用的是 differential blend。

Differential Blend

xb3 这个 talk 没具体说 differential blend,这个操作来自 ps4 pro 官方棋盘渲染的 demo。我这里补充下做法,其实非常简单,以下截图大部分来自 cod2017 的分享。
notion image
differential blend 适用于已有 2x2 总共 4 个像素里斜角 2 个像素的情况。在上图里所有白色和黑色的像素都是已知信息,绿色像素是待填补的空缺。这里我们拿中心的绿色像素作为例子进行填补(有橘色小点的那个):
已知上下左右 4 个像素的颜色,怎么推断中心像素的颜色?最简单的做法就是对这 4 个颜色取平均,但是这样会导致重建出来的像素信息过于平均,视觉上显示的结果就是会比较模糊,缺少边缘细节(都被平均过渡了)。
为了解决这个问题,differential blend 先分别计算垂直和水平像素颜色之间的差距,然后选择差距最小的方向进行混合。在这个例子,垂直方向是纯白和纯黑像素,水平方向是两个纯黑像素,所以水平方向差距最小,因为我们混合这两个像素并把结果颜色(黑色)填补到中心像素里。
notion image
这套想法的目的是找出邻近像素里应该来自同一几何表面的像素,并对他们进行线性插值,而对于不同几何表面的像素我们不去混合而保留边缘细节。而像素如果来自同一几何表面的话,他们之间的颜色变化应该比较小,所以我们找到差最小的方向去混合。但高光计算也可能导致在同一表面也有巨大的亮度差异,这时候这套方法就失效了。黑暗之魂重置版里改进了下这套方法,利用每个像素的 triangle+instance id 去判断邻居像素是否来自于同一个表面。

历史帧信息校准

对于时域升采样而言,如果直接使用历史帧信息,很容易出现鬼影的情况。比如画面里物体移动了,显示出了之前被遮挡的区域,但是错误的前一帧还未移动的物体表面进行了混合;亦或者是对于很多半透明特效,他没办法写入运动矢量,所以历史帧重投影后的位置是错误的。当然还有其他 n 类原因,这里就不细展开了。xb3 这里举了半透明特效鬼影的例子,剑头蓝色的特效因为是半透明,不写入深度和运动矢量信息,所以很难判断出鬼影:
notion image
在这种情况下就只能靠邻近颜色取进行钳制,也就是所谓的 neighboring clamp。核心假设历史帧的像素颜色不应该超过当前像素和周围一圈像素形成的色彩包围盒的范围,如果超过了我们就认为这是因为画面产生了变化。这时候我们不直接舍弃这个历史帧像素(舍弃完了就等于没信息了),而是把他 clamp 到这个包围盒里进行一个 temporal filtering,提高时域的稳定性。当然 neighboring clamp 也有很多做法,比如不 clamp 而是 clip,基于 YCoCg 色彩空间做包围盒,或者用 variance 做包围盒等等,这里不展开了。
但这里比较纠结的是,这个邻近 clamp 的力度到底应该是多大?如果太大了,鬼影是消失了,但是很容易 “误伤无辜”,尤其是基础分辨率非常低的情况下,我们本来就要抖动像素位置,而邻近像素之间距离很大,信息量不够,配上基于物理的材质高光很容易造成两帧亮度包围盒不一样,每帧都会发生 clamp,从而导致剧烈的颜色闪烁,也就是时域上并不稳定。如果小了,会引入更多鬼影,与此同时由于高光的亮度很大,就算降小一点也未必能缓解太多闪烁。cod 原分享里提到了就是因为这个原因所以他们放弃了更紧的基于 variance 的包围盒。xb2 里鬼影就还满严重的,尤其是很多环境和动态人物之间明度差不明显,包围盒很难发挥应有的作用。
这个抉择是很难的,当色彩变了的时候,我们得去判断它到底是由于本身处于高频变化的像素位置,还是因为画面 / 物体移动 / 光照变化的原因导致的历史像素失效。。我之前写过一篇回答就聊过这类问题:
monolith 这里用的就是 cod 2016 filmic smaa t2x 的提到的做法(但动视表示这个方法抄自育碧,育碧表示这个方法抄自游骑兵)。比起直接用当前帧的包围盒去判断上一帧像素的颜色是否在其内部,我们去判断上一个 jitter sequence 之前同一位置的像素颜色。
具体例子就是,如果当前帧是 N,我们不直接比较 N 和 N-1 帧的颜色,而是 N 和 N-2 帧的颜色。这里我们对像素的抖动是 2 次一个完整的序列,所以 2 帧之前我们找到像素的位置和当前帧像素对应的位置应该是一致的(因为不存在抖动了)。这时候,如果 N-2 的颜色也没能通过包围盒的检验,那我们确实认为这就是错误的历史信息,而不是抖动带来的超采样差异。为了加强去鬼影效果,xb3 和 cod 一样,不仅比较 N 和 N-2 帧,也比较 N-1 和 N-3 帧(这两个之间同样差一整个 jitter sequence)。
notion image
notion image
在上面这张图里我们能明显看出这张操作的优势。如果只比较 N 和 N-1 的话就是上排的情况,可以看到 N 帧里的高光像素因为抖动的原因在 N-1 帧里是看不到的;对比之下,下排里 N 和 N-2 里因为 jitter 位置相同,所以高光情况也应该相同,不会错误的产生了 clamp。(当然他这例子不够好,理论上当前帧有高光前一帧没有这个其实不会闪,因为前一帧还是在当前帧包围盒内,这得反过来才对吧。。。)
但用了这套方案的话,需要额外存好多张历史帧,带来更高的显存开销。jitter sequence 长度也基本保持在 2,所以理论上最多也就 2x 超分;还不能多帧持续积累,只能前后两帧积累。这对于高光锯齿其实信息不太够。如果要更长的话,比如常见的 8 jitter 的 halton,因为帧数和相机本身的运动,很难保证 8 帧之前的像素还是一致的,容易触发 “误伤”。

低分辨率后处理瑕疵

notion image
xb3 的后处理也应该是低分辨率计算的(不然他这个带 bokeh 的景深效果我想象不出怎么优化在 1080p 下跑),被升采样一起拉到目标分辨率。像景深,bloom,光斑之类的后处理效果,对深度的差异或者像素亮度的变化非常敏感,因为时域抖动的原因极其容易产生闪烁和不稳定。xb3 的做法是输出一张 mask 标记所有应用了景深,bloom 之类后处理的区域,然后升采样 resolve 的时候对邻近的 4 个像素进行平均化混合,非常简单粗暴。
notion image

FXAA

由于基础分辨率还是太低了,碰到诸如草地或者高频纹理闪烁是必然的,所以还得额外加一层抗锯齿。cod 原分享里用的是 smaa,但是 switch 跑不动所以改成 fxaa 了。
notion image
switch 跑 1080p 的 fxaa 其实也很耗,所以这个虽然是在升采样合成之后,但实际上只对当前帧像素有效。那为什么要放在升采样之后呢?这是个好问题,我以前合并 fxaa 和 taa 的时候,也纠结过一整。cod 的做法是把 smaa 放在合成之前,这样 temporal reprojection 得到的信息已经是去了一次锯齿的。但是考虑到这个 fxaa 跑的分辨率低而且质量不一定很高,抗完锯齿的结果可能会过于糊了,如果放在升采样前的话,这个额外错误的模糊还会被积累到下一帧去。但 cod 可能对他们那个 smaa 质量比较自信,所以就塞在前面了。
这里还有个优化的细节。由于 jitter 的原因,当前帧像素的实际上是对角线分布。做 fxaa 的时候如果要去取规则网格上的像素的话,需要去计算重投影,有额外的性能开销。这里 monolith 参考 horizon zero dawn 的做法,把整个 framebuffer 旋转 45 度,这样原来的对角线像素就变成正交格子了,可以直接硬件 load/bilinear 拿到。hzd 还做了进一步处理把整个旋转完的 framebuffer 裁剪下放在更小的矩形里以减小显存开销,xb3 不知道有没有做。
notion image
顺便提一嘴,像 fxaa/smaa 这种搜边缘的抗锯齿操作,很多时候 quad occupancy 其实不高,可以参考微软 DX 样例引擎的操作,先提取边缘,把边缘像素添加到一个 compute buffer 里,然后在 compute shader 里做抗锯齿操作,避免出线哪怕 2x2 的 pixel quad 里只有一个像素要做 fxaa 也会连带 4 个像素一起做的额外开销。

总结

先看下开销吧
notion image
前作 xb2 里的 taa 主机模式在 720p 下花了 2ms,掌机 540p 下也差不多 2ms,这个差不多开销控制的还可以接受。xb2 的 taa 还包含了一个锐化 pass 来强化下因为 taa 模糊掉的细节(虽然掌机下明显过锐,ringing 瑕疵非常明显)。对比下 xb3 里主机模式升采样 + fxaa 接近 8ms,就算 xb3 是 30fps,这都快接近一帧里 1/3 的时间占用了,这个我觉得性能开销挺大的。monolith 表示本来想加个锐化 pass,但是性能不允许。我觉得总体还有待改善吧,主要这个升采样很多问题还是没法解决,比如角色描边这种细小物体的锯齿因为内部极低的分辨率 + 抖动,还是很难 snap 住。
notion image
实际玩的时候,草地之类的地方噪点还是很明显,这个我觉得 fxaa 没法解决,因为这种噪点很多来自高光或者更低分辨率的屏幕空间环境光遮蔽噪点。而且像体积云,屏幕空间反射,屏幕空间环境光遮蔽这类还要低于内部分辨率的特效,在碰上这种棋盘渲染的时候如果碰到深度不匹配的物体边缘,瑕疵会很明显,xb3 里体积云的和近景交叉的游动黑边还是挺明显的。
notion image
可惜升采样分享感觉没什么有新意的内容吧。类似 ddx/ddy 怎么处理,jitter 大小调整,历史帧重采样处理其实还有挺多花样可以搞的。大概还是 ns 的带宽比较吃紧,要实现较好的升采样效果,需要用于记录和判断的数据还是比较多的,纹理采样性能这块打不住,现阶段也就这样了。
另外啊,xb3 这个相机跑起来能感觉画面有扭曲的情况,我不知道是大世界坐标精度问题还是升采样重投影时候哪一步有问题,这还是相对明显的,稍微注意点就能发现。 > 本文由简悦 SimpRead 转码