Xenoblade3 渲染研究

Posted by FlowingCrescent on 2022-08-14
Estimated Reading Time 14 Minutes
Words 4k In Total
Viewed Times

本文初发布于zhihu
https://zhuanlan.zhihu.com/p/553209338

0. 前言

自两年前通关XBDE与XB2以来,笔者就对这个游戏系列非常着迷,不久前发售的XB3更是系列集大成之作,而它相比前两作也有长足进步,不禁让人感叹“这竟然是NS游戏?”;而笔者作为一名游戏开发者,当然也想更加深入了解其使用的技术方案,在研究之时觉得其内容还挺值得分享,因此便有了这篇粗浅之文。

本文大致目录

  1. 渲染管线总览
  2. 角色渲染相关
  3. 结语与其他

渲染管线总览

image.png

最终渲染图(1920x1080)

PreZ

平常只包括地形、石头与草,无角色
image.png
但特写镜头会有角色
image.png
大概是因为平常角色占屏幕比例较小,PreZ收益低,但人物特写的时候开启收益较大。
通常而言草与各种植物需要有PreZ以防止其Alpha Test破坏硬件EarlyZ机制,而石头与人物依情况开启。


GBuffer Pass

GBuffer 0(R8G8B8A8)

image.png
Albedo(RGB)960x544
image.png
笔触感(A)

image.png
Albedo(RGB) 1280x720

image.png
笔触感(A)
画脸的时候还特意加了一个类似笔触感的贴图
![[5[@J4PJ{EO6Z%X7NUIEZH.png

image.png
image.png
脸部延迟Pass结果
效果确实是有……但高桥似乎忘了自己是在开发NS游戏
image.png
该值为1
image.png
该值为0
总之看上去大概是个Ramp Offset或者扰动法线的什么东西。

image.png
在SD里验证了下这笔触图甚至不是四方连续的,高桥,你到底想不想好好做啊!?


GBuffer 1(R8G8B8A8)

image.png
960x544

image.png
R 猜测为金属度

image.png
G 猜测为光滑度

image.png
B
对草而言大概是“某值的相加/乘程度”,总之增大之后显得像自发光一样。
image.png
原值
image.png
原值*100

对角色而言应当是用于决定Ramp图中Y轴取值,可能也趁此区分了不同部位:
image.png
B
image.jpg
原值渲染结果
image.jpg
原值*2渲染结果

image.png
A 不明确,只能猜测
草的延迟Pass根本没有取A通道,还画个值上去干啥呢,可能又在哪个角落里用到了吧。
image.png
A
image.png
原值渲染结果
image.png
原值x10
image.png
原值x100
image.png
原值x1000
有些类似“值越大则越倾向于原Albedo值”,但似乎也不完全如此。

对头发而言似乎是高光强度。
image.png
原值
image.png
Gbuffer1 A通道为0


GBuffer 2(R10G10B10A2)

image.png
乍一看好像是个比较普通的屏幕空间法线Gbuffer,其实不然。
它B通道存的是AO
image.png

而A通道内容较不明确。
由于法线是一个三维单位向量,有其三维中其中两个值以及另一个值的符号值(正或负)即可还原出法线值;但譬如Spheremap Transform之类的法线编码算法其实是不需要额外存符号位的,xb3这算法可能是将半个球面进行投影,而剩下半球用符号位表示,提升一点精度?笔者对这方面内容实在算不上熟悉。
image.png
A作为一个符号位存储
image.png
A为原值

image.png
A为1 - 原值
image.png
A为原值
image.png
A为1 - 原值(这一张效果还是比较明显的)


GBuffer 3(R8G8B8A8)

image.png
RGB 960x544
乍看可说是极其诡异的一个GBuffer。
image.png
R
image.png
G
image.png
B
这张GBuffer的RGB通道单独看都不知道是什么,笔者一位不愿透露姓名的友人调试后发现这rgb三个通道为编码后的线性深度,也就是-PosVS.z,还原后如下图:
_H}NPV55I[]N7@P18%R1M5M.png
或许是被映射到0-1的非线性深度无法满足雾、后处理等算法的精度要求,才需要出此下策……这就是超远视距的后果吗?
float和rgb24转换的类似算法可参考这篇ShaderToy:
https://www.shadertoy.com/view/WsVGzm

那么接下来是A通道:
image.png
A
image.png
人身上大多数区域的值为0.5
image.png
A为0(还有部分亮部应当是边缘光)延迟Pass输出结果
image.png
A为1时延迟Pass输出结果
基本可敲定对角色而言便是影响Ramp的取值,可以理解为类似AO,为0就必为暗部,而为1就更容易为亮部(但不一定为亮部,大概只是把halfLamber的值给乘2了而已)。

于草而言,似乎也是相加了某值,
image.png
原值

值为0

值为1
感觉变化实在不明显,或许草也就是顺便蹭一下这个通道而已,不用白不用;比较重要的还是角色那个类似AO的用法。


GBuffer 4 (R11G11B10)

image.png
Emissive 960x544


Velocity Buffer(R10G10B10A2)

之后用于TAA的Velocity Buffer。
image.png
RGB 960x544
image.png
RG调整映射范围后

image.png
B
又在标记头发和脸,或许是让脸和头发受TAA影响小些?但为何不同人的值不一样呢,姑且懒得计较了。

image.png
A
应当与法线类似同样是记录正负符号位,用两个通道分别给R与G通道记录了符号。
虽说算上这个符号位,每个通道也就11位……作为Velocity Buffer勉强能用。


Specular Mask(R8G8B8A8)

在头发的GBuffer Pass时直接输出到一张RT上
image.png
960x544
image.png
1280x720
似乎对于卡通渲染中角色的风格化高光必不可少,在蓝色协议的技术分享中也有见到类似的GBuffer:
image.png
大伙真是拼了老命也要让角色走延迟。
540p也确实是有理由的。


Depth/Stencil(D24/S8)

image.png
经过重新映射后的深度值
image.png
同时也用模板值区分各个不同的部位,如普通PBR场景物件、角色身体、头发等。


GI Pass

根据之前的GBuffer以及反射探针计算GI
image.png
其中tex3即为反射探针集,共192面,而根据一个反射探针有6面计算可得一共用了32个反射探针……不过每个的分辨率都极小,单面64x64而已。

会输出两张RT,看上去一个是Diffuse GI,一个是Specular GI
image.png
Diffuse GI
image.png
Specular GI


Shadow Caster Pass

执行近处动态物体(角色和怪物)的Shadow Caster Pass
image.png
1024x1024
没错,只有近处动态物体(人物)执行了Shadow Caster。


Screen Space Shadow Pass

经 @GuardHei 点拨,XB3的阴影方案应当与XBX所使用的相差无几,Monolith将其称之为“Shadowmap Cache化”。
用简单而不太准确的话概括,就是将Shadowmap每级联都划分为3x3的9份,当触发shadow更新时才会更新其中一小块。
当然其中实现起来有许多的细节要考虑,看上去也是很吃显存。
在此只展示XBX在cedec2015的其中部分分享:
image.png
Shadowmap Cache化
image.png
以当前位置为中心渲染Cache Buffer
image.png
划分为9块
image.png
生成另外两个级联
image.png
每个CacheBuffer使用的相机高度(Y)相同,仅移动XZ
image.png
从cache中取用部分作为Shadowmap

那么xbx的阴影内容先告一段落,我们回到xb3

image.png

输入了3张场景级联ShadowMap,然后再结合上一步角色的ShadowMap进行阴影判断。
image.png
image.png
image.png
三张预烘焙的Shadowmap
三张Shadowmap均为1024x1024,格式为D16

image.png
阴影的RT格式是R8G8B8A8,但A通道没有内容.
R通道放的是角色以及场景的阴影结果,G通道放的是只有角色而无场景的阴影结果,B放的是只有场景无角色的阴影结果,大概之后也会对不同的阴影做什么操作吧,不过感觉也无关紧要就是了。
这也解释了为什么场景阴影不会随着时间变化而变化,因为NS的机能无法支持高桥给场景搞实时ShadowCaster,说到底这种超大场景还实时级联阴影的话开销极其爆炸。
而各个大时间段之间不切阴影方向大概也是为了不卡顿而考虑。


AO Pass

下一个Pass应当是AO Pass,在此之前似乎根据ddx以及ddy算了个未平滑的的屏幕空间法线,但这RT也并没有出现在后续的AO Pass Input中,不知是不是截帧有问题。
image.png
于是它Input只有深度图,而输出如下图
image.png
240x136
看Shader中只采样了5次深度图,第一反应是这采样数也太少了,还有这四分之一分辨率输出也太糊了。
具体是HBAO抑或GTAO,因为真不想看汇编shader,不得而知 。

image.png
之后会做模糊后再与前一帧的AO进行混合去噪,最后输出480x272。


Deferred Lighting Pass

于是终于来到了延迟Pass,把之前计算所得的各个GBuffer、阴影RT、AO RT、GI RT等全部输入。
带宽芜湖起飞。
image.png
image.png
PBR Pass

image.png
image.png
人物Pass(不包括头发)

image.png
image.png
植被Pass

image.png
image.png
头发Pass
于是头发刚画完,又抠出来加个后处理:
image.png
image.png
应该是用了类似SNN的算法以模拟“水彩”效果,xb3中还有兰兹大衣的绒毛也经过了该处理,在破晓传说中类似这种后处理更是被推广至整个游戏场景,被宣传为“Atmos Shader”,或许也值得其他游戏借鉴学习。
SNN算法可参考:
www.shadertoy.com/view/MlyfWd


Skybox/Cloud Pass

于是之后开始绘制天空盒、体积云等
image.png
普通的面片云

image.png
1/4分辨率体积云RT(240x136),似乎是作为最远景
image.png
并入Frame Buffer后

image.png
另一张体积云RT(240x136)
image.png
并入Frame Buffer后


Transparent/Forward Pass

这个队列不仅渲染半透明物体,甚至远景的不透明物体也在这时候绘制,如远景的天涯巨剑,大概跟Additional Light无关的玩意都可以扔这里。

Instance的半透明发光面片

image.png
天涯大剑,输入Albedo、法线后直接前向绘制到FrameBuffer上
image.png
顺便看了下这个大剑的PosCS输出,其中w便是深度.
我们仍未知道那天xb3所使用的相机视距数值。
当然,这种独特的远景物体,大概也可以重新设置相机视距后再提交渲染。

在另一个截帧中可发现半透明粒子在半分辨率的另一张RT上渲染,倒也是个比较常见的优化overdraw的手段。
image.png


TAA/Post Process

之后便是TAA以及各种后处理,其中涉及多个RT的创建与Blit等,截帧的内容也开始愈发混乱起来,比如其中某个看似是LUT调色的Input与Output:

总之,笔者难以保证之后的内容的准确度,只能尽量从正常的截帧中选取内容。

TAA

image.png
当前帧
image.png
历史帧
image.png
混合帧
当前帧清晰但是锯齿也清晰,而混合帧整体显模糊但也切实消去了锯齿……

DOF 景深

image.png
一个半分辨率且模糊后的RT

image.png
CoC Buffer,但是其中黑色区域没有存负值,也就是没有区分“前后景”。不知是截帧问题还是本就没做区分?


DOF结果

Bloom

image.png
image.png
提取亮部后进行数次Downsample

之后截帧的内容极其混乱,几乎只能看出做了个LUT。

总之最终本帧输出如下:
image.png

image.png
TAA及后处理前
image.png
后处理后,绘制UI前
同时这个草原的TAA会将本帧进行升采样,也就是TAAU,而播片时的mio截帧中TAA似乎并没有做升采样这一步,因此草原帧输出为1080p,mio那一帧为720p。也是笔者感到非常迷惑的一处,也无法排除是截帧问题……
总之,渲染管线至此终于是大致过了一遍。
其实有些内容此次并没有看出来,如笔者在游玩时发现似乎存在自动曝光以及体积光。


角色渲染相关

相信各位玩家都看得出来,此次xb3相比xb2与xbde,进步最为明显的就是它的角色渲染。

示例图

image.png
image.png
image.png
image.png
image.png
image.png

较有趣的部分

就个人而言,截帧前最关心的几个细节是脸部的明暗计算方法、“厚涂”风的头发、脸上的高光点、刘海阴影。
关于头发的独特风格实现在前文已有提及,是做了类似SNN的后处理,当然实际上是不是SNN并不清楚。

一开始以为脸部的明暗是类似sdf或者赛马娘的那种trick方法,但想了想这是延迟渲染,想太trick也有点难,一截帧发现它硬是整了张法线贴图……
image.png
脸部使用的法线贴图
image.png
法线输出结果
倒也能理解为什么感觉游戏中角色很容易就“阴阳脸”了,毕竟法线图能力有限,做不了很平滑的过渡。至于是烘的还是画的,反正国内应该没人用这种方案,姑且不关心了。

image.png
角色渲染使用的ramp图(256x256),可以说是相当复杂
应当是为了兼容各种身体部件、不同角色、各个场景的早、午、暮、夜四个时间段,甚至不同天气,最终导致ramp图如此复杂,也算是延迟渲染卡渲之痛吧。
而国内目前流行的角色走前向渲染就避免了这一点。

image.png
xb2时的ramp图就已经趋向复杂,没想到xb3更是夸张(图出自CGWORLD2018年8月号)

角色的衣服、首饰等应当是魔改PBR,让漫反射经过了Ramp图映射,其他大概与标准PBR相差不多,不过这也只是没看代码的纯猜测,可信度不高。


刘海阴影是用面片做的,在GBufferPass之后作为半透明物体前向绘制:
image.png
image.png
上一次看到这种做法好像还是在MMD的模型里,果然还是工匠克算法。


而在人物延迟光照Pass之后,又加了一些半透明物体以“画龙点睛”
image.png
延迟光照结束时的人物效果

image.pngimage.png
增添脸部高光点
直接用半透明面片点了几个高光点,个人感觉它还会根据视角做一点偏移:
image.pngimage.png
比如从左看脸时高光也会偏左,下嘴唇高光位置变化尤其明显。
image.png
这一张侧脸特写中,鼻尖高光的偏移与“浮空”十分明显

image.png
image.png
增添虹膜细节

image.png
image.png
增加反射
image.png
而这个反射真的使用了反射探针,虽然笔者在游玩期间根本没看出这个反射的变化……

image.pngimage.png
最后点上眼睛高光

那么至此也大致将本作角色渲染中比较独特有趣的部分聊过了。

结语与其他

客观而言,这些技术方案实在不像一款NS游戏应该拥有的:超远视距、完整的延迟渲染管线、常增加管线负担的风格化角色渲染,xb3这实在是戴着脚镣跳舞;同样作为一名游戏开发者,我不禁心生敬意。

一开始本是单纯想研究一下角色渲染才截帧,结果因为其他玩家朋友强烈要求让笔者分析一下整个游戏的渲染(顺便问问到底为什么画质这么糊),索性就趁此机会学习一下这个最不像NS游戏的NS游戏所使用的渲染方案,但笔者既非图形程序也非引擎开发,只是一个写写破shader的初出茅庐TA罢了,个人能力所限,能分析的技术只是这个游戏的冰山一角,而文中许多基于猜测的内容很可能错误甚至牛头不对马嘴,还请多包涵。
也希望各位不会被笔者误导,可以亲自看看xb3的渲染流程,因此将截帧分享出来(使用yuzu模拟器OpenGL模式,Xenoblade3 1.00 ver):
链接:https://pan.baidu.com/s/18dbrPCAXGrg_fsZF9FiF8g
提取码:xbnb

参考资料:

  1. Hallucinations re: the rendering of Cyberpunk 2077:http://c0de517e.blogspot.com/2020/12/hallucinations-re-rendering-of.html
  2. CGWORLD (シージーワールド) 2018年 08月号
  3. float to RGB to float :https://www.shadertoy.com/view/WsVGzm
  4. SNN/Kuwahara Filter :https://www.shadertoy.com/view/MlyfWd

感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。