[Translate]Reverse Engineering the rendering of The Witcher 3 II | [译]巫师3渲染逆向工程 2

原文参见 Reverse engineering the rendering of The Witcher 3: Index

这是第二篇,翻译原文9-12节。由于文章太长,而且废话较多,这里先做个简单的摘要吧

摘要

9-中介绍GBuffer的组成,讲了一些特殊的操作,比如Best Fit Normal,比如降饱和度。另外神奇的是巫师3并没有使用PBR,毕竟是2015年的游戏。

10-中介绍了远景雨幕,其实是一个圆柱上通过噪声滚动做出的

11-介绍了闪电的做法,是一个树状的mesh渲染出的,可以做粗细远近变化,并且加了一些随机效果

12-介绍了天空的渲染,包括大气散射,太阳和星空的渲染,星空是旋转的天空盒,并做了随机闪烁

9 GBuffer

这是我系列的第9部分
这部分我会揭示巫师3的gbuffer的一些细节
假定你了解延迟渲染的基本知识。简单回顾:不立即计算最后光照和着色,而是分成两个阶段:第一阶段(几何pass)填充gbuffer表面数据(颜色,法线,高光颜色等等),第二阶段(光照pass)混合所有并计算光照

延迟渲染很流行,因为它允许在全屏幕空间计算光照,结合tile-based等技术极大地提高了性能

简单地说,gbuffer是包含一系列几何属性贴图的集合,很重要的一点是设计它的组成。比如这个:Crysis3的渲染技术.

之后我们来看看巫师的一帧

gbuffer有三张R8G8B8A8_UNORM格式的rendertarget和一张D24_UNORM_S8_UNIT格式的深度+stentil缓冲

RT0-Albedo
RT0-A 不知道是啥
RT1-法线

RT1-Alpha 像反射度
RT2 像高光颜色
深度缓冲
Stencil

当然这不是gbuffer的全部,光照pass使用反射探针和其它buffer,但不是这篇的主题。在开始之前,我们先讨论一些泛泛的观察

整体观察

  1. 唯一清理的缓冲是深度/蒙版

如果你用帧分析器分析会有点惊讶,除了深度缓冲,其它没有调用clear指令。所以实际上RenderTarget1看上去这样,注意远处模糊的像素。

这是简单实用的优化,因为ClearRenderTargetView调用是有开销的,只有需要时才用

2. 反向Z

很多文章讨论过浮点深度缓冲的精确度,巫师3用了反向z,这是开放世界和较大渲染距离的自然选项。

在DX中这不复杂
a) 用0清理深度缓冲,而不是1远处是0而不是1
b) 计算投影矩阵时,将远近裁切值交换
c) 将深度测试从Less改为Greater
OpenGL中有些复杂,不过是值得的

Pixel Shader

我想展示pixel shader如何给gbuffer传递数据,我们至少存了颜色,法线,高光。但可能不像你想得容易
问题是pixel shader有很多变种,使用的贴图和参数不一样。比如这个桶。

看看他们的贴图:

我们有颜色,法线和高光颜色,很常见。但开始之前,一些碎语:几何体带有位置,UV,法线和tangent属性vertex shader输出uv,归一化的TBN。对于复杂的材质,比如有两张颜色贴图的。vertex shader会输出其他的,这里一个简单的例子:

这个shader有很多步,我分别描述一下。首先是cbuffer的数值

颜色

我们先从难的考试,它不简单是采样贴图颜色,采样之后做了一步降饱和

对于大多数的物体,它就是返回的贴图本色,但适当的材质cbuffer数值,cb4_v1.x如果是1,会导致蒙版为0,会使用lerp混合颜色。

但有一些反例
我发现最高的降饱和系数时4,降饱和颜色取决于材质,可以是(0.2,0.3,0.4),但没有严格规范。我迫不及待重现了,下面是结果,当降饱和颜色为(0.25,0.3,0.45)时

desaturationFactor = 1
desaturationFactor = 2
desaturationFactor = 3
desaturationFactor = 4

我很确定这仅仅应用了材质属性,不是颜色的最终部分。15-20行是最后的几步

v0.z是vertex shader出来的,结果是0.记住它,因为vo.z后面会多次用到。
看上去一些系数和代码会让颜色变暗,但如果v0.z是0的话,颜色就不会变,

关于RT0.a,如你所见,材质材质cbuffer,但是因为没有debug信息,我们也不知道是什么,或许是透明度?

法线

首先解压法线,然后正常做,没什么特别的

看看28-33行,

大概可以写成

我不确定这是合适的写法,如果你知道这是什么数学计算,请告诉我。(译者注:像是一个reflect操作。。。但是为啥对模型反面这么干我是头一回见)
我们看到pixel shader使用了SV_IsFrontFace

对于线和点,始终为True。特殊情况是三角面的线(线框模式),和实体模式一致。可以用geometry shader设置并被pixel shader读取

我想自己检查下,的确他只在线框模式可见。我相信目的是线框模式下正确计算法线。下面是比较(译者注:也有可能是开启双面渲染的材质,背面有特殊的计算法线方式,trick地修正光照)

没用trick的场景颜色
用了这个trick的场景颜色
法线0-1,没使用上述trick
法线0-1,使用了trick

你注意到rendertarget地格式是R8G8B8A8_UNORM了嘛?它意味着每个组件有256种可能,但它够用吗?

用有限地资源储存高质量地法线是一个已知问题,但是幸运的是我们有很多材料可以学习

也许你注意到这个技术在这里使用了,我要说整个几何pass种确实有一个附加贴图:

巫师3使用了Best Fit Normal的技术,我不会详细解释,它是2009-2010时Crytek开发的,随着CryEngine开源

BFN造成了法线贴图地颗粒感在缩放法线获得最佳后,我们把它从[-1,1]区间映射到[0,1]区间

高光

我们从34行开始,采样高光贴图

如你所见他也有类似的变暗过滤,计算最大值,然后计算变暗值,然后和本色lerp。

Reflectivity ?

我也不知道合适的名字是什么,我不知道他如何影响光照。它仅仅是法线图的alpha通道

汇编:

和我们的老朋友v0.z打招呼,类似颜色和高光

好了这是第一个pixel shader的变种

pixel shader – 颜色+法线变种

我给你展示另一个变种,这次是颜色和法线。没有高光贴图

和前面的区别:

a) 1,19行,插值参数v0.z乘以了cbuffer系数cb4[0].x,被用于19行插值颜色,其他地方还是用的v0.z
b) 54-55行,o2.w设定条件是cb4[7].x > 0.0我们已经知道这是计算明度的地方

c) 34-42行,完全不同的计算高光的方法
没有高光贴图

我们使用了1-reflectivity

这个变种中cbuffer稍微大一些,这些参数用来模拟高光颜色。
其它的变种跟之前一样

总结,看上去简单的在实际应用中都会很复杂,巫师3的gbufer也不例外,我展示了pixel shader的一些简单变种,和一些总体观察。

10 远景雨

这次我们看一个有趣的气象现象-靠近地平线的远景雨。最容易的与它相遇的方法是拜访Skellige岛

我个人很喜欢这个现象并且好奇CDPR的图形程序如何实现它。让我们看看吧

这里是两张前后对比图

模型

第一步是几何,用了一个小圆柱
局部坐标系位置很小,在0-1之前drawcall的内容如下:

这里重要的是:Texcoord和Instance_Transformuv很简单,甚至可以程序化生成这个模型

有了局部坐标系的圆柱,乘以Instance_Transform提供的世界矩阵

看上去很吓人,我们解析一下

有意思:

旋转四元数:0.0925, -0.3149, 0.883412, -0.33446

缩放:299.99, 300, 1000

位移:-116.495, 238.691, -1.456
重要的是了解到相机位置是-116.533, 234.869, 2.09

如你所见,我们缩放到很大,移动到相机位置并且旋转

Vertex Shader

输入几何体和vertex shader互相依赖:

除了传递坐标和instance_lod_param,另外输出了两个:SV_Position和高度(z分量)这里,缩放为4,4,2而bias为-2,-2,-1
你可以注意到9和28行成了两个行矩阵

Pixel Shader

有两个贴图:一张噪声和一张深度

cbuffer的数值

Pixel Shader:

看上去很多,实际不差

发生了什么?首先计算了动画UV,从cbuffer获取到逝去的时间来计算的。这个uv用来采样噪声贴图

有了噪声后,用它插值最大最小值,做了一些乘法,计算了强度蒙版

注意到远景都没了,因为圆柱执行了深度测试(没有写入)。之后计算了“远景物体蒙版”

可以这么计算:

个人觉得这可以更廉价,不计算世界坐标高度,而是将视锥深度乘以一个更小的数值。

两个乘起来的到最后蒙版

有了这个最后的蒙版,我们做了另一个插值(其实没做啥)然后乘上了shaft颜色,计算gamma矫正并输出最后输出用了0alpha

11 闪电

这一节我介绍闪电在巫师3中是如何渲染的
在雨幕之后,闪电也是在前向渲染pass中。

它持续很短的一段时间,因此最好的方式是0.25倍速播放,你可以看到它不是静态图像,强度随着时间变化微微增强。

和雨幕有很多相似的地方,比如混合方式和深度测试。

它的几何用了一个树状的模型,这个闪电用的是这个模型。它有uv和法线,并会在vertex shader中用到

Vertex Shader

它有雨幕相比有很多相似的地方,我就不重复了。主要不同在11-18行

cb1[8].xyz是相机位置,而r2.xyz是世界坐标。因此11行计算了视向量,之后12-15行计算了长度

v2.xyz是法线,16行将它从0-1解压到[-1,1],最后计算了世界坐标

这样,模型会沿法线稍微膨胀。这里我将0.000001替换成了几个不同数值

Pixel shader

好事是它不长,坏事是这在干嘛?

实话说,这不是我一次看到它,但我第一次看到它很懵逼。
实际上你在好几个shader里都能看到它,我的答案是他是个整数随机数

若你所见,它在pixelshader中执行两次。从那个网站中你可以找到更多的实现光滑噪声的方法。
看看这行,这里计算了uv动画

在做了floor之后,用于计算随机数。总体上而言我们计算了两个随机数,计算最终结果,并做了插值。

好了,这是个整数噪声,但是前面全是float,也没用过ftoi指令。我猜测CDPR的程序员用了asint之类的函数。
两个值的插值权重在10-11行计算

这让我们可以用时间插值。为了产生光滑函数,后面用了SCurve函数

这就是smoothstep,但你从汇编中可以看到,他不是HLSL内置的smoothstep,内置的做了些clamp保证计算结果正确。但我们已经知道权重在0-1之间,所以可以安全跳过这些检查

计算最终结果包含几个乘法,注意最后输出的透明度可能变化,取决于噪声。这很省事,因为它会影响到闪电的透明度,就像实际的那样。

最后的pixel shader

12- 愚蠢的天空trick

这部分和之前稍有不同,我会展示天空shader的一部分

为什么是愚蠢的trick而不是整个shader?首先一些原因,天空shader很大,2015版本有267行而血与酒有385行

其次,有很多参数对于逆向工程没必要但很艰难
因此我决定只展示一些trick,如果我发现了更多,我会补充在后面。

2015版本和血与酒的区别相当显著。比如,计算星星和闪烁,不同计算太阳的方式。血与酒还计算了夜间的银河。

我们从一些基本的开始。 一个晚上晴朗的天空

基础

像很多当代游戏一样,巫师3用天空穹顶。看一下这个半球,它的包围和是[0,0,0]到[1,1,1],有平滑的uv

天空穹顶和天空盒很相似,vertexshader中我们将穹顶随着观察者移动,产生很远的假象。

如果你读了整个系列了解巫师3用了反向深度,最远为0。为了让天空穹顶位于远平面,我们将视口参数的最远最近设为一样。

Vertex Shader

巫师3(2015)的shader是这样的:

在这个情况下,vertexshader仅仅输出了uv和世界坐标。血与酒中还有法线。简单起见我们用2015的版本。
看看cbuffer

这里有世界矩阵。没什么特别的,cb2_v4和cb2_v5是缩放/偏移系数,将坐标从[0,1]变换到[-1,1]。但这里z(上)方向系数会压缩他。

我们已经见到了相似的vertex shader,通用算法是传递uv,然后用缩放/偏移系数计算坐标,然后计算PositionW,最后用世界矩阵和投影矩阵计算裁剪空间坐标

Rendoc的好处是它可以注入你自己的shader而不影响管线,如你所见我改变了几何体的缩放和位移得到了一些有意思的结果

优化vertex shader

你发现vertex shader的问题了吗?逐顶点矩阵乘法没必要。我发现很多shader里都有了。我们可以直接用PositionW乘以投影矩阵把这个

换成

优化版本是这样:

如你所见从26行变成了12行。我不明白为什么这个问题如此普遍。你可以在renderdoc中注入我优化过的shader,一点没有改变视觉效果。实话说我不明白为什么CDPR要做矩阵-矩阵乘法

太阳

巫师3计算大气散射和太阳用了两个drawcall

太阳的渲染和月亮很像,在几何体和混合/状态上。

另一方面血与酒的太阳和天空是一个pass渲染的

无论你如何渲染太阳,你都需要阳光方向。一个直觉的方法是用球面坐标。你只需要两个参数:phi和theta。可以假设r是1,我们可以计算为:

正常来讲你也可以在应用阶段计算太阳方向然后传给cbuffer。有了方向我们就可以深入pixel shader

首先cb12[0].xyz是相机的方向。在r0.xyz中我们储存了顶点坐标。因此100行计算了世界到相机向量。看看105-107行,计算了相机到世界的归一化向量

然后计算了相机方向和太阳方向的点积,记得归一化它们。并且我们要clamp到0-1之间
我们有了点积之后:

log,mul,exp做了指数运算。我们计算了点积的指数,原因是产生了模拟太阳的渐变光晕。有了渐变,就可以在天空颜色和太阳颜色间插值了。

注意到这个也可以用来模拟日食现象,然后也需要月亮方向的向量。
最后的代码:

移动的星星

如果你在晴朗夜空上做一个延时,会发现星星不是静止的,而是慢慢再天空移动的。

我们先从星星开始,它是一张1024*1024*6的cubemap,如果你想下会发现有一个很简单的方式使用方向采样cubemap

为了计算方向向量,首先从世界相机向量开始。用月亮方向乘了两个叉积,最后执行了三个点积获得了最后的向量

我需要更严谨地调查,读者们,如果你了解的话请让我知道(译者注:换了一套基座标。首先重建了一个月亮为天顶方向的坐标系,然后将视方向投影到这个坐标系采样星星。意义是星星贴图是相对月亮的坐标系的。)

闪烁星星

一个trick是闪烁的星星,如果你在novigrad的郊野漫步会发现星星在闪。我很好奇如何实现的,2015版本和血与酒有很多大差异,简化起见我们看2015版本

我们来看看这段汇编

173行采样完starsColor,我们计算了一定的偏移数值。这个数值用于扰动采样方向,然后又采样了cubemap,做了gamma矫正乘了起来。

如此简单?考虑这个便宜数值,必须在天穹上很不一样,否则星星会一样的闪烁。

为了让偏移尽可能多样,我们需要用天穹的uv坐标和当前时间。如果你对那个吓人的ishr/xor/and不熟悉的话,看一下闪电那篇,会了解到更多整数噪声。如你所见,这里用了四次,不过和闪电不太一样的是,为了让结果更随机,使用的整数做了加和,反序。

好了我们开始

4次的最后结果是:第一次 r5.x第二次 r4.w第三次 r1.w第四次 r5.y
最后我们有

这部分计算了权重的s曲线,基于uv的小数部分

如你期待那样,这个系数用来插值噪声并产生最后的结果

一个对偏移计算简短的可视化

一旦我们有了starsColorDisturbed,最难的部分就结束了。
下一步是计算gamma教程并相乘

星星,最后

我们有了starsFinal在r1.xyz中,最后做的处理是

这比前面简单多了,颜色做了幂运算,控制星星密度,最后保证颜色在1,1,1之间。
cb0[9].w用来控制可见性,因此白天是1,晚上是0.

参考资料

Reverse engineering the rendering of The Witcher 3: Index 9-12节

发表评论

邮箱地址不会被公开。 必填项已用*标注