第1部分。拉链
在这一部分中,我们将研究《巫师3:狂猎》中的闪电渲染过程。
闪电渲染的执行时间比
雨幕效果晚一些,但仍在直接渲染过程中进行。 可以在此视频中看到闪电:
它们会很快消失,因此最好以0.25的速度观看视频。
您会看到这些不是静态图像; 随着时间的流逝,它们的亮度会略有变化。
从细微差别的角度来看,在远处画雨帘有许多相似之处,例如,混合状态(加法混合)和深度相同(启用检查,不进行深度记录)。
没有闪电的场景闪电场景就闪电的几何形状而言,巫师3是一个树状网格。 此闪电示例由以下网格表示:
它具有UV坐标和法线向量。 所有这些在顶点着色器阶段都派上用场。
顶点着色器
让我们看一下组装好的顶点着色器代码:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[9], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_input v2.xyz dcl_input v4.xyzw dcl_input v5.xyzw dcl_input v6.xyzw dcl_input v7.xyzw dcl_output o0.xy dcl_output o1.xyzw dcl_output_siv o2.xyzw, position dcl_temps 3 0: mov o0.xy, v1.xyxx 1: mov o1.xyzw, v7.xyzw 2: mul r0.xyzw, v5.xyzw, cb1[0].yyyy 3: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw 4: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw 5: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 6: mov r1.w, l(1.000000) 7: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 8: dp4 r2.x, r1.xyzw, v4.xyzw 9: dp4 r2.y, r1.xyzw, v5.xyzw 10: dp4 r2.z, r1.xyzw, v6.xyzw 11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw 20: mul r0.xyzw, v5.xyzw, cb1[1].yyyy 21: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw 22: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw 23: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 24: dp4 o2.y, r1.xyzw, r0.xyzw 25: mul r0.xyzw, v5.xyzw, cb1[2].yyyy 26: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw 27: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw 28: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 29: dp4 o2.z, r1.xyzw, r0.xyzw 30: mul r0.xyzw, v5.xyzw, cb1[3].yyyy 31: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw 32: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw 33: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw 34: dp4 o2.w, r1.xyzw, r0.xyzw 35: ret
顶点着色器防雨帘有很多相似之处,因此我不再重复。 我想向您展示第11-18行的重要区别:
11: add r2.xyz, r2.xyzx, -cb1[8].xyzx 12: dp3 r1.w, r2.xyzx, r2.xyzx 13: rsq r1.w, r1.w 14: div r1.w, l(1.000000, 1.000000, 1.000000, 1.000000), r1.w 15: mul r1.w, r1.w, l(0.000001) 16: mad r2.xyz, v2.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), l(-1.000000, -1.000000, -1.000000, 0.000000) 17: mad r1.xyz, r2.xyzx, r1.wwww, r1.xyzx 18: mov r1.w, l(1.000000) 19: dp4 o2.x, r1.xyzw, r0.xyzw
首先,cb1 [8] .xyz是摄像机的位置,而r2.xyz是世界空间中的位置,也就是说,第11行计算从摄像机到世界位置的矢量。 然后第12-15行计算
长度(worldPos-cameraPos)* 0.000001。v2.xyz是传入几何的法线向量。 第16行将其从间隔[0-1]扩展到间隔[-1; 1]。
然后计算世界上的最终位置:
finalWorldPos = worldPos +长度(worldPos-cameraPos)* 0.000001 * normalVector此操作的HLSL代码段将如下所示:
...
此操作导致网格的较小“爆发”(沿法向矢量的方向)。 我尝试用其他几个值替换0.000001。 结果如下:
0.0000020.0000050.000010.000025像素着色器
好了,我们找到了顶点着色器,现在是时候开始讨论像素着色器的汇编代码了!
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[1], immediateIndexed dcl_constantbuffer cb2[3], immediateIndexed dcl_constantbuffer cb4[5], immediateIndexed dcl_input_ps linear v0.x dcl_input_ps linear v1.w dcl_output o0.xyzw dcl_temps 1 0: mad r0.x, cb0[0].x, cb4[4].x, v0.x 1: add r0.y, r0.x, l(-1.000000) 2: round_ni r0.y, r0.y 3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff) 9: round_ni r0.z, r0.x 10: frc r0.x, r0.x 11: add r0.x, -r0.x, l(1.000000) 12: ishr r0.w, r0.z, l(13) 13: xor r0.z, r0.z, r0.w 14: imul null, r0.w, r0.z, r0.z 15: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 16: imad r0.z, r0.z, r0.w, l(146956042240.000000) 17: and r0.z, r0.z, l(0x7fffffff) 18: itof r0.yz, r0.yyzy 19: mul r0.z, r0.z, l(0.000000001) 20: mad r0.y, r0.y, l(0.000000001), -r0.z 21: mul r0.w, r0.x, r0.x 22: mul r0.x, r0.x, r0.w 23: mul r0.w, r0.w, l(3.000000) 24: mad r0.x, r0.x, l(-2.000000), r0.w 25: mad r0.x, r0.x, r0.y, r0.z 26: add r0.y, -cb4[2].x, cb4[3].x 27: mad_sat r0.x, r0.x, r0.y, cb4[2].x 28: mul r0.x, r0.x, v1.w 29: mul r0.yzw, cb4[0].xxxx, cb4[1].xxyz 30: mul r0.xyzw, r0.xyzw, cb2[2].wxyz 31: mul o0.xyz, r0.xxxx, r0.yzwy 32: mov o0.w, r0.x 33: ret
好消息:代码并不长。
坏消息:
3: ishr r0.z, r0.y, l(13) 4: xor r0.y, r0.y, r0.z 5: imul null, r0.z, r0.y, r0.y 6: imad r0.z, r0.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 7: imad r0.y, r0.y, r0.z, l(146956042240.000000) 8: and r0.y, r0.y, l(0x7fffffff)
...这是怎么回事?
老实说,这不是我第一次在Witcher 3着色器中看到这样的汇编代码。 但是当我第一次见到他时,我想:“这到底是什么?”
在其他一些TW3着色器中也可以找到类似的内容。 我不会用这个片段来描述我的冒险经历,而只是说答案在于
整数噪声 :
如您所见,在像素着色器中它被调用了两次。 使用该网站上的指南,我们可以了解如何正确实现平滑噪声。 一分钟后,我会再谈这个。
看一下第0行-我们在此基于以下公式进行动画处理:
动画=经过时间*动画速度+ TextureUV.x这些值在将来四舍五入到下侧(
floor )(指令
round_ni )后,将成为整数噪声的输入点。 通常,我们计算两个整数的噪声值,然后计算它们之间的最终内插值(有关详细信息,请参见libnoise网站)。
好吧,这是
整数噪声,但毕竟,所有前面提到的值(也四舍五入)都是浮点的!
请注意,此处没有
ftoi指令。 我假设CD Projekt Red的程序员
在这里使用了HLSL
asint内部函数,该函数执行“ reinterpret_cast”浮点值的转换并将它们视为整数模式。
这两个值的插值权重在第10-11行中计算。
插值权重= 1.0-压裂(动画);这种方法使我们可以随着时间在值之间进行插值。
为了产生平滑噪声,将此内插器传递给
SCurve函数:
float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x;
平滑步函数[libnoise.sourceforge.net]此功能称为“平滑步伐”。 但是,从汇编代码中可以看到,这
不是 HLSL
的内部
平滑步函数。 内部函数会应用限制,以使值均为true。 但是由于我们知道
插值权重将始终在[0-1]范围内,因此可以安全地跳过这些检查。
在计算最终值时,将使用几个乘法运算。 了解最终的alpha输出如何根据噪声值而变化。 这很方便,因为它会影响渲染的闪电的不透明度,就像在现实生活中一样。
准备好像素着色器:
cbuffer cbPerFrame : register (b0) { float4 cb0_v0; float4 cb0_v1; float4 cb0_v2; float4 cb0_v3; } cbuffer cbPerFrame : register (b2) { float4 cb2_v0; float4 cb2_v1; float4 cb2_v2; float4 cb2_v3; } cbuffer cbPerFrame : register (b4) { float4 cb4_v0; float4 cb4_v1; float4 cb4_v2; float4 cb4_v3; float4 cb4_v4; } struct VS_OUTPUT { float2 Texcoords : Texcoord0; float4 InstanceLODParams : INSTANCE_LOD_PARAMS; float4 PositionH : SV_Position; };
总结一下
在这一部分中,我描述了一种在《巫师3》中渲染闪电的方法。
我很高兴我的着色器产生的汇编代码与原始代码完全匹配!
第2部分。愚蠢的天空技巧
这部分将与之前的部分略有不同。 在其中,我想向您展示Sky Shader Witcher 3的某些方面。
为什么使用“愚蠢的把戏”而不是整个着色器? 好吧,有几个原因。 首先,《巫师3》的天空着色器是相当复杂的野兽。 2015版的像素着色器包含267行汇编代码,Blood and Wine DLC的着色器包含385行。
而且,它们接收到很多输入,这不利于对完整(且可读!)的HLSL代码进行逆向工程。
因此,我决定只展示这些着色器的部分技巧。 如果我发现新的东西,我将补充该职位。
2015版和DLC(2016)之间的差异非常明显。 尤其是,它们包括计算星星和闪烁的差异,以及渲染太阳的不同方法。
血液和葡萄酒着色器甚至可以计算夜间的银河系。
我将从基础知识开始,然后讨论愚蠢的把戏。
基础知识
像大多数现代游戏一样,《巫师3》使用skydome建模天空。 请看《巫师3》(2015)中用于此目的的半球。 注意:在这种情况下,此网格的边界框在[0,0,0]到[1,1,1]的范围内(Z是指向上方的轴),并且UV平滑分布。 以后我们使用它们。
skydome背后的想法类似于
skybox的想法(唯一的区别是所使用的网格)。 在顶点着色器阶段,我们相对于观察者(通常根据摄影机的位置)对天穹进行转换,从而产生一种幻象,即天空实际上距离很远-我们将永远无法到达天空。
如果您阅读了本系列文章的前几部分,那么您就会知道“巫师3”使用的是反深度,即远平面为0.0f,最近的平面为1.0f。 为了在远平面上完成skydome输出,在浏览窗口的参数中,我们将
MinDepth设置
为与
MaxDepth相同的值:
若要了解在浏览窗口转换期间如何使用
MinDepth和
MaxDepth字段 ,请单击
此处 (docs.microsoft.com)。
顶点着色器
让我们从顶点着色器开始。 在Witcher 3(2015)中,汇编器着色器代码如下:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb1[4], immediateIndexed dcl_constantbuffer cb2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 o1.x, r0.xyzw, cb2[0].xyzw 4: dp4 o1.y, r0.xyzw, cb2[1].xyzw 5: dp4 o1.z, r0.xyzw, cb2[2].xyzw 6: mul r1.xyzw, cb1[0].yyyy, cb2[1].xyzw 7: mad r1.xyzw, cb2[0].xyzw, cb1[0].xxxx, r1.xyzw 8: mad r1.xyzw, cb2[2].xyzw, cb1[0].zzzz, r1.xyzw 9: mad r1.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 10: dp4 o2.x, r0.xyzw, r1.xyzw 11: mul r1.xyzw, cb1[1].yyyy, cb2[1].xyzw 12: mad r1.xyzw, cb2[0].xyzw, cb1[1].xxxx, r1.xyzw 13: mad r1.xyzw, cb2[2].xyzw, cb1[1].zzzz, r1.xyzw 14: mad r1.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 15: dp4 o2.y, r0.xyzw, r1.xyzw 16: mul r1.xyzw, cb1[2].yyyy, cb2[1].xyzw 17: mad r1.xyzw, cb2[0].xyzw, cb1[2].xxxx, r1.xyzw 18: mad r1.xyzw, cb2[2].xyzw, cb1[2].zzzz, r1.xyzw 19: mad r1.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 20: dp4 o2.z, r0.xyzw, r1.xyzw 21: mul r1.xyzw, cb1[3].yyyy, cb2[1].xyzw 22: mad r1.xyzw, cb2[0].xyzw, cb1[3].xxxx, r1.xyzw 23: mad r1.xyzw, cb2[2].xyzw, cb1[3].zzzz, r1.xyzw 24: mad r1.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r1.xyzw 25: dp4 o2.w, r0.xyzw, r1.xyzw 26: ret
在这种情况下,顶点着色器仅将texcoords和世界空间中的位置传输到输出。 在
Blood and Wine中,他还显示了归一化的法线向量。 我会考虑使用2015版,因为它更简单。
查看指定为
cb2的常量缓冲区:
在这里,我们有一个世界矩阵(按100均匀缩放并相对于摄像机位置进行传输)。 没什么复杂的。 cb2_v4和cb2_v5是用于将顶点位置从间隔[0-1]转换为间隔[-1; 1]的比例/偏差因子。 但是在这里,这些系数“压缩” Z轴(向上)。
在本系列的前几部分中,我们有类似的顶点着色器。 通用算法是进一步传递texcoords,然后考虑比例尺/偏差系数
来计算
Position ,然后在世界空间中计算
PositionW ,然后通过将
matWorld和
matViewProj相乘来计算裁剪空间的最终位置->使用它们的乘积乘以
Position以获得最终的SV_Position 。
因此,此顶点着色器的HLSL应该是这样的:
struct InputStruct { float3 param0 : POSITION; float2 param1 : TEXCOORD; float3 param2 : NORMAL; float4 param3 : TANGENT; }; struct OutputStruct { float2 param0 : TEXCOORD0; float3 param1 : TEXCOORD1; float4 param2 : SV_Position; }; OutputStruct EditedShaderVS(in InputStruct IN) { OutputStruct OUT = (OutputStruct)0;
我的着色器(左)和原始着色器(右)的比较:
RenderDoc的一个出色特性是它允许我们注入自己的着色器,而不是原始的着色器,并且这些更改将影响到帧末尾的管道。 从HLSL代码可以看到,我提供了几个用于缩放和变换最终几何图形的选项。 您可以对它们进行试验,并获得非常有趣的结果:
顶点着色器优化
您是否注意到原始顶点着色器的问题? 矩阵的顶点乘以矩阵是完全多余的! 我至少在一些顶点着色器中发现了这一点(例如,在着色器
中远处有
雨帘 )。 我们可以通过立即将
PositionW乘以
matViewProj来优化它!
因此,我们可以将代码替换为HLSL:
如下:
优化版本为我们提供了以下汇编代码:
vs_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer CB1[4], immediateIndexed dcl_constantbuffer CB2[6], immediateIndexed dcl_input v0.xyz dcl_input v1.xy dcl_output o0.xy dcl_output o1.xyz dcl_output_siv o2.xyzw, position dcl_temps 2 0: mov o0.xy, v1.xyxx 1: mad r0.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx 2: mov r0.w, l(1.000000) 3: dp4 r1.x, r0.xyzw, cb2[0].xyzw 4: dp4 r1.y, r0.xyzw, cb2[1].xyzw 5: dp4 r1.z, r0.xyzw, cb2[2].xyzw 6: mov o1.xyz, r1.xyzx 7: mov r1.w, l(1.000000) 8: dp4 o2.x, cb1[0].xyzw, r1.xyzw 9: dp4 o2.y, cb1[1].xyzw, r1.xyzw 10: dp4 o2.z, cb1[2].xyzw, r1.xyzw 11: dp4 o2.w, cb1[3].xyzw, r1.xyzw 12: ret
如您所见,我们将指令的数量从26条减少到12条-相当重要的变化。 我不知道这个问题在游戏中的普及程度,但是为了上帝的缘故,CD Projekt Red也许发布了补丁? :)
而且我不是在开玩笑。 您可以插入优化的着色器,而不是原始的RenderDoc,您会看到此优化在视觉上没有任何影响。 老实说,我不明白为什么CD Projekt Red决定执行矩阵与矩阵的顶点乘法...
太阳
在《巫师3》(2015年)中,大气散射和太阳的计算包括两个单独的绘图调用:
巫师3(2015)-直到巫师3(2015)-天空巫师3(2015)-天空+太阳就几何形状和混合/深度状态而言,2015年版本中太阳的
渲染与月亮的
渲染非常相似。
另一方面,在
“血液和葡萄酒”中,一次渲染有太阳
的天空:
巫师3:血与酒(2016)-通往天堂《巫师3:鲜血与美酒》(2016)-《天堂与太阳》无论您如何渲染太阳,在某个阶段您仍然需要阳光的(规范化)方向。 获得此向量的最合乎逻辑的方法是使用
球坐标 。 实际上,我们只需要两个表示两个角度(以弧度表示)的值即可:
Phi和
theta 。 收到它们后,我们可以假定
r = 1 ,从而将其减小。 然后,对于Y轴指向上方的直角坐标,可以在HLSL中编写以下代码:
float3 vSunDir; vSunDir.x = sin(fTheta)*cos(fPhi); vSunDir.y = sin(fTheta)*sin(fPhi); vSunDir.z = cos(fTheta); vSunDir = normalize(vSunDir);
通常,在应用程序中计算日光的方向,然后将其传递给常量缓冲区以备将来使用。
接收到阳光的方向后,我们可以更深入地研究
“ Blood and Wine”像素着色器的汇编代码...
... 100: add r1.xyw, -r0.xyxz, cb12[0].xyxz 101: dp3 r2.x, r1.xywx, r1.xywx 102: rsq r2.x, r2.x 103: mul r1.xyw, r1.xyxw, r2.xxxx 104: mov_sat r2.xy, cb12[205].yxyy 105: dp3 r2.z, -r1.xywx, -r1.xywx 106: rsq r2.z, r2.z 107: mul r1.xyw, -r1.xyxw, r2.zzzz ...
因此,首先,
cb12 [0] .xyz是摄像机位置,在
r0.xyz中,我们存储顶点位置(这是顶点着色器的输出)。 因此,第100行计算向量
worldToCamera 。 但是,请看第105-107行。 我们可以将它们写为
归一化(-worldToCamera) ,即我们计算归一化的
cameraToWorld向量。
120: dp3_sat r1.x, cb12[203].yzwy, r1.xywx
然后,我们计算
cameraToWorld和
sunDirection向量的标量积! 请记住,必须将它们标准化。 我们还使该完整表达式饱和,以将其限制为间隔[0-1]。
太好了! 该标量积存储在r1.x中。 让我们看看它的下一步应用...
152: log r1.x, r1.x 153: mul r1.x, r1.x, cb12[203].x 154: exp r1.x, r1.x 155: mul r1.x, r2.y, r1.x
三位一体“ log,mul,exp”是幂。 如您所见,我们将余弦(归一化向量的标量积)提高到一定程度。 您可能会问为什么。 这样,我们可以创建模仿太阳的渐变。 (并且第155行会影响此渐变的不透明度,因此,例如,我们将其重置为完全隐藏太阳)。 以下是一些示例:
指数= 54指数= 2400有了这个渐变,我们就可以使用它在
skyColor和
sunColor之间进行插值! 为避免出现伪影,您需要使第120行的值饱和。
值得一提的是,该技巧可用于模拟月球
冠 (在低指数值下)。 为此,我们需要
moonDirection向量,可以使用球坐标轻松计算出该向量。
现成的HLSL代码可能类似于以下代码段:
float3 vCamToWorld = normalize( PosW – CameraPos ); float cosTheta = saturate( dot(vSunDir, vCamToWorld) ); float sunGradient = pow( cosTheta, sunExponent ); float3 color = lerp( skyColor, sunColor, sunGradient );
星星运动
如果您将“巫师3”晴朗的夜空延时拍摄,您会发现星星不是静止的-它们在天空中移动一些! 我几乎偶然地注意到了这一点,并想知道它是如何实现的。
让我们从以下事实开始:巫师3中的恒星以大小为1024x1024x6的立方图表示。 如果您考虑一下,您将理解这是一个非常方便的解决方案,可让您轻松地为采样立方图确定方向。
让我们看下面的汇编代码:
159: add r1.xyz, -v1.xyzx, cb1[8].xyzx 160: dp3 r0.w, r1.xyzx, r1.xyzx 161: rsq r0.w, r0.w 162: mul r1.xyz, r0.wwww, r1.xyzx 163: mul r2.xyz, cb12[204].zwyz, l(0.000000, 0.000000, 1.000000, 0.000000) 164: mad r2.xyz, cb12[204].yzwy, l(0.000000, 1.000000, 0.000000, 0.000000), -r2.xyzx 165: mul r4.xyz, r2.xyzx, cb12[204].zwyz 166: mad r4.xyz, r2.zxyz, cb12[204].wyzw, -r4.xyzx 167: dp3 r4.x, r1.xyzx, r4.xyzx 168: dp2 r4.y, r1.xyxx, r2.yzyy 169: dp3 r4.z, r1.xyzx, cb12[204].yzwy 170: dp3 r0.w, r4.xyzx, r4.xyzx 171: rsq r0.w, r0.w 172: mul r2.xyz, r0.wwww, r4.xyzx 173: sample_indexable(texturecube)(float,float,float,float) r4.xyz, r2.xyzx, t0.xyzw, s0
为了计算最终采样向量(第173行),我们首先计算归一化的
worldToCamera向量(第159-162行)。
然后我们使用
moonDirection计算两个向量乘积(
163-164,165-166) ,然后我们计算三个标量乘积以获得最终采样向量。 HLSL代码:
float3 vWorldToCamera = normalize( g_CameraPos.xyz - Input.PositionW.xyz ); float3 vMoonDirection = cb12_v204.yzw; float3 vStarsSamplingDir = cross( vMoonDirection, float3(0, 0, 1) ); float3 vStarsSamplingDir2 = cross( vStarsSamplingDir, vMoonDirection ); float dirX = dot( vWorldToCamera, vStarsSamplingDir2 ); float dirY = dot( vWorldToCamera, vStarsSamplingDir ); float dirZ = dot( vWorldToCamera, vMoonDirection); float3 dirXYZ = normalize( float3(dirX, dirY, dirZ) ); float3 starsColor = texNightStars.Sample( samplerAnisoWrap, dirXYZ ).rgb;
请注意:这是一个设计良好的代码,我应该对其进行详细研究。
读者注意:如果您对此操作了解更多,请告诉我!
闪烁的星星
我想更详细地探讨的另一个有趣的技巧是星星的闪烁。
例如,如果您在晴朗的天气中在诺维格勒(Novigrad)周围徘徊,您会注意到星星闪烁。我很好奇这是如何实现的。事实证明,2015版与“血液与葡萄酒”之间的差异非常大。为简单起见,我将考虑2015版。因此,我们从上一节中的starsColor采样之后开始: 174: mul r0.w, v0.x, l(100.000000) 175: round_ni r1.w, r0.w 176: mad r2.w, v0.y, l(50.000000), cb0[0].x 177: round_ni r4.w, r2.w 178: bfrev r4.w, r4.w 179: iadd r5.x, r1.w, r4.w 180: ishr r5.y, r5.x, l(13) 181: xor r5.x, r5.x, r5.y 182: imul null, r5.y, r5.x, r5.x 183: imad r5.y, r5.y, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 184: imad r5.x, r5.x, r5.y, l(146956042240.000000) 185: and r5.x, r5.x, l(0x7fffffff) 186: itof r5.x, r5.x 187: mad r5.y, v0.x, l(100.000000), l(-1.000000) 188: round_ni r5.y, r5.y 189: iadd r4.w, r4.w, r5.y 190: ishr r5.z, r4.w, l(13) 191: xor r4.w, r4.w, r5.z 192: imul null, r5.z, r4.w, r4.w 193: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 194: imad r4.w, r4.w, r5.z, l(146956042240.000000) 195: and r4.w, r4.w, l(0x7fffffff) 196: itof r4.w, r4.w 197: add r5.z, r2.w, l(-1.000000) 198: round_ni r5.z, r5.z 199: bfrev r5.z, r5.z 200: iadd r1.w, r1.w, r5.z 201: ishr r5.w, r1.w, l(13) 202: xor r1.w, r1.w, r5.w 203: imul null, r5.w, r1.w, r1.w 204: imad r5.w, r5.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 205: imad r1.w, r1.w, r5.w, l(146956042240.000000) 206: and r1.w, r1.w, l(0x7fffffff) 207: itof r1.w, r1.w 208: mul r1.w, r1.w, l(0.000000001) 209: iadd r5.y, r5.z, r5.y 210: ishr r5.z, r5.y, l(13) 211: xor r5.y, r5.y, r5.z 212: imul null, r5.z, r5.y, r5.y 213: imad r5.z, r5.z, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 214: imad r5.y, r5.y, r5.z, l(146956042240.000000) 215: and r5.y, r5.y, l(0x7fffffff) 216: itof r5.y, r5.y 217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z 229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 237: log r4.xyz, r4.xyzx 238: mul r4.xyz, r4.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 239: exp r4.xyz, r4.xyzx 240: log r2.xyz, r2.xyzx 241: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 242: exp r2.xyz, r2.xyzx 243: mul r2.xyz, r2.xyzx, r4.xyzx
嗯让我们看一下这段相当长的汇编代码的结尾。在第173行上对starsColor进行采样后,我们计算出某种偏移值。该偏移量用于使采样的第一个方向失真(r2.xyz,第235行),然后再次对恒星的三次方图进行采样,对这两个值进行伽玛校正(237-242)并将它们相乘(243)。简单吧?好吧,不是真的。让我们考虑一下这个偏移量。在整个穹顶中,此值应有所不同-闪烁的星星看起来非常不现实。以抵消如果UV尽可能多样化,我们将利用UV延伸到天穹(v0.xy)并应用存储在常量缓冲区(cb [0] .x)中的经过时间这一事实。如果您不熟悉这些令人恐惧的ishr / xor /,那么在有关闪电效果的部分中,请阅读有关整数噪声的内容。如您所见,这里会产生四次整数噪声,但它不同于雷电。为了使结果更加随机,噪声的输入整数是总和(iadd),并且将其与之取反(内部函数reversebits;bfrev指令)。所以,现在慢下来。让我们从头开始。4 «» . , 4 :
int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); }
4 ( ,
itof ):
1 — r5.x,
2 — r4.w,
3 — r1.w,
4 — r5.y
itof ( 216) :
217: frc r0.w, r0.w 218: add r0.w, -r0.w, l(1.000000) 219: mul r5.z, r0.w, r0.w 220: mul r0.w, r0.w, r5.z 221: mul r5.xz, r5.xxzx, l(0.000000001, 0.000000, 3.000000, 0.000000) 222: mad r0.w, r0.w, l(-2.000000), r5.z 223: frc r2.w, r2.w 224: add r2.w, -r2.w, l(1.000000) 225: mul r5.z, r2.w, r2.w 226: mul r2.w, r2.w, r5.z 227: mul r5.z, r5.z, l(3.000000) 228: mad r2.w, r2.w, l(-2.000000), r5.z
S- UV, . 因此:
float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x;
, :
229: mad r4.w, r4.w, l(0.000000001), -r5.x 230: mad r4.w, r0.w, r4.w, r5.x float noise0 = lerp( fStarsNoise1, fStarsNoise2, weightX ); 231: mad r5.x, r5.y, l(0.000000001), -r1.w 232: mad r0.w, r0.w, r5.x, r1.w float noise1 = lerp( fStarsNoise3, fStarsNoise4, weightX ); 233: add r0.w, -r4.w, r0.w 234: mad r0.w, r2.w, r0.w, r4.w float offset = lerp( noise0, noise1, weightY ); 235: mad r2.xyz, r0.wwww, l(0.000500, 0.000500, 0.000500, 0.000000), r2.xyzx 236: sample_indexable(texturecube)(float,float,float,float) r2.xyz, r2.xyzx, t0.xyzw, s0 float3 starsPerturbedDir = dirXYZ + offset * 0.0005; float3 starsColorDisturbed = texNightStars.Sample( samplerAnisoWrap, starsPerturbedDir ).rgb;
offset :
在计算了starsColorDisturbed之后,最难的部分就完成了。万岁!
下一步是对starsColor和starsColorDisturbed进行伽玛校正,然后将它们相乘: starsColor = pow( starsColor, 2.2 ); starsColorDisturbed = pow( starsColorDisturbed, 2.2 ); float3 starsFinal = starsColor * starsColorDisturbed;
星星-画龙点睛
我们有starsFinal在r1.xyz。在星形处理结束时,将发生以下情况: 256: log r1.xyz, r1.xyzx 257: mul r1.xyz, r1.xyzx, l(2.500000, 2.500000, 2.500000, 0.000000) 258: exp r1.xyz, r1.xyzx 259: min r1.xyz, r1.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 260: add r0.w, -cb0[9].w, l(1.000000) 261: mul r1.xyz, r0.wwww, r1.xyzx 262: mul r1.xyz, r1.xyzx, l(10.000000, 10.000000, 10.000000, 0.000000)
与闪烁和移动的恒星相比,这要容易得多。因此,我们首先将starsFinal提高到2.5的幂-这使我们可以控制星星的密度。很聪明 然后,使星星的最大颜色等于float3(1、1、1)。cb0 [9] .w用于控制恒星的整体可见性。因此,我们可以预期在白天该值为1.0(乘以零),而在晚上为0.0。最后,我们将恒星的能见度提高了10。就这样!第3部分。巫师天才(对象和亮度图)
几乎所有先前描述的效果和技术都与巫师3并没有真正的联系,诸如色调校正,渐晕或计算平均亮度之类的事情几乎出现在每个现代游戏中。甚至中毒的影响也相当普遍。这就是为什么我决定仔细研究“维克特本能”的渲染机制。杰拉特(Geralt)是个巫师,因此他的感情比普通人要敏锐得多。因此,他比其他人可以看到和听到更多的声音,这对他的调查非常有帮助。巫师的天赋机制允许玩家可视化此类痕迹。这是效果的演示:还有一个,具有更好的照明:如您所见,有两种类型的对象:Geralt可以与之交互的对象(黄色轮廓)和与调查相关联的痕迹(红色轮廓)。杰拉特(Geralt)检查了红色足迹后,它可能变成黄色(第一个视频)。请注意,整个屏幕变为灰色,并添加了鱼眼效果(第二个视频)。这种影响相当复杂,因此我决定将他的研究分为三个部分。在第一篇中,我将讨论对象的选择,第二篇中,将讨论轮廓的生成,第三篇中,将所有这些元素最终统一为一个整体。选择对象
正如我所说,有两种类型的对象,我们需要对其进行区分。在Witcher 3中,这是使用模板缓冲区实现的。生成应标记为“迹线”(红色)的GBuffer网格物体时,将以stencil = 8渲染它们。以黄色标记为“有趣”对象的网格物体将以stencil = 4渲染。例如,以下两个纹理显示了一个示例框架,其中包含可见的巫师本能和相应的模具缓冲区:模板缓冲简介
模板缓冲区通常在游戏中用于标记网格。某些类别的网格被分配了相同的ID。想法是,如果模板测试成功,则将Always函数与Replace运算符一起使用,而在所有其他情况下,与Keep运算符一起使用。这是使用D3D11实施的方法: D3D11_DEPTH_STENCIL_DESC depthstencilState;
要写入缓冲区的stensil值在API调用中作为StencilRef传递:
渲染亮度
从实现的角度来看,这段代码以R11G11B10_FLOAT格式提供了一个全屏纹理,在通道R和G中存储了有趣的对象和迹线。为什么在亮度方面需要这样做?事实证明,Geralt的本能只有有限的半径,因此,只有当玩家离他们足够近时,对象才会获得轮廓。看看实际的这方面:我们首先清洁亮度纹理,然后用黑色填充。然后进行两个全屏绘制调用:第一个用于跟踪,第二个用于有趣的对象:第一次进行描画调用-绿色通道:第二个调用是针对有趣的对象-红色通道:好吧,但是我们如何确定要考虑的像素呢?我们将不得不使用模板缓冲区!对于这些调用中的每个调用,都将进行模板测试,并且仅接受先前标记为“ 8”(第一次绘制调用)或“ 4”的那些像素。模板测试痕迹的可视化:...以及有趣的对象:在这种情况下如何进行测试?您可以在一篇不错的文章中了解模板测试的基础知识。通常,模板测试公式具有以下形式: if (StencilRef & StencilReadMask OP StencilValue & StencilReadMask) accept pixel else discard pixel
其中:StencilRef是API调用传递的值,StencilReadMask是用于读取Stensil值的掩码(请注意,它同时出现在左右两侧),OP是通过API设置的比较运算符,StencilValue是模板缓冲区的值在当前正在处理的像素中。重要的是要理解我们使用二进制AND来计算操作数。熟悉基础知识之后,让我们看看如何在以下绘制调用中使用这些参数:迹线的模具条件模具状态为有趣的物体哈!如我们所见,唯一的区别是ReadMask。让我们看看吧!将这些值替换为模板测试方程式: Let StencilReadMask = 0x08 and StencilRef = 0: For a pixel with stencil = 8: 0 & 0x08 < 8 & 0x08 0 < 8 TRUE For a pixel with stencil = 4: 0 & 0x08 < 4 & 0x08 0 < 0 FALSE
聪明地 如您所见,在这种情况下,我们不比较模板值,而是检查模板缓冲区的某个位是否已设置。模板缓冲区的每个像素均具有uint8格式,因此值的间隔为[0-255]。注意:所有DrawIndexed(36)调用都与将脚印渲染为脚印有关,因此在此特定帧中,亮度贴图具有以下最终形式:但是在模板测试之前,有一个像素着色器。28738和28748都使用相同的像素着色器: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[8], immediateIndexed dcl_constantbuffer cb12[214], immediateIndexed dcl_sampler s15, mode_default dcl_resource_texture2d (float,float,float,float) t15 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_output o1.xyzw dcl_output o2.xyzw dcl_output o3.xyzw dcl_temps 2 0: mul r0.xy, v0.xyxx, cb0[1].zwzz 1: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t15.xyzw, s15 2: mul r1.xyzw, v0.yyyy, cb12[211].xyzw 3: mad r1.xyzw, cb12[210].xyzw, v0.xxxx, r1.xyzw 4: mad r0.xyzw, cb12[212].xyzw, r0.xxxx, r1.xyzw 5: add r0.xyzw, r0.xyzw, cb12[213].xyzw 6: div r0.xyz, r0.xyzx, r0.wwww 7: add r0.xyz, r0.xyzx, -cb3[7].xyzx 8: dp3 r0.x, r0.xyzx, r0.xyzx 9: sqrt r0.x, r0.x 10: mul r0.y, r0.x, l(0.120000) 11: log r1.x, abs(cb3[6].y) 12: mul r1.xy, r1.xxxx, l(2.800000, 0.800000, 0.000000, 0.000000) 13: exp r1.xy, r1.xyxx 14: mad r0.zw, r1.xxxy, l(0.000000, 0.000000, 120.000000, 120.000000), l(0.000000, 0.000000, 1.000000, 1.000000) 15: lt r1.x, l(0.030000), cb3[6].y 16: movc r0.xy, r1.xxxx, r0.yzyy, r0.xwxx 17: div r0.x, r0.x, r0.y 18: log r0.x, r0.x 19: mul r0.x, r0.x, l(1.600000) 20: exp r0.x, r0.x 21: add r0.x, -r0.x, l(1.000000) 22: max r0.x, r0.x, l(0) 23: mul o0.xyz, r0.xxxx, cb3[0].xyzx 24: mov o0.w, cb3[0].w 25: mov o1.xyzw, cb3[1].xyzw 26: mov o2.xyzw, cb3[2].xyzw 27: mov o3.xyzw, cb3[3].xyzw 28: ret
此像素着色器仅写入一个渲染目标,因此第24-27行是多余的。在这里发生的第一件事是深度采样(使用具有值限制的点采样器),第1行。该值用于通过乘以特殊矩阵再进行透视划分(第2-6行)来重新创建世界上的位置。考虑Geralt的位置(cb3 [7] .xyz-注意,这不是相机的位置!),我们计算出Geralt到该特定点的距离(第7-9行)。以下输入在此着色器中很重要:-cb3 [0] .rgb-输出颜色。它的格式可以为float3(0,1,0)(迹线)或float3(1,0,0)(有趣的对象),-cb3 [6] .y-距离比例因子。直接影响最终输出的半径和亮度。后来,我们有了相当复杂的公式来计算亮度,具体取决于Geralt和物体之间的距离。我可以假设所有系数都是通过实验选择的。最终输出是颜色 * 强度。HLSL代码如下所示: struct FSInput { float4 param0 : SV_Position; }; struct FSOutput { float4 param0 : SV_Target0; float4 param1 : SV_Target1; float4 param2 : SV_Target2; float4 param3 : SV_Target3; }; float3 getWorldPos( float2 screenPos, float depth ) { float4 worldPos = float4(screenPos, depth, 1.0); worldPos = mul( worldPos, screenToWorld ); return worldPos.xyz / worldPos.w; } FSOutput EditedShaderPS(in FSInput IN) {
原始(左)和我的(右)汇编器着色器代码的比较。这是巫婆天赋作用的第一步。实际上,这是最简单的。第4部分。巫师的天才(轮廓图)
再次看一下我们正在探索的场景:在分析女巫的本能的第一部分中,我展示了如何生成“亮度图”。我们有一个R11G11B10_FLOAT格式的全屏纹理,看起来可能像这样:绿色通道表示“足迹”,红色是Geralt可以与之交互的有趣对象。收到此纹理后,我们可以进入下一个阶段-我将其称为“轮廓图”。这是512x512 R16G16_FLOAT格式的怪异纹理。以“乒乓”的方式实现它很重要。前一帧的轮廓图是输入数据(连同亮度图),以在当前帧中生成新的轮廓图。乒乓缓冲区可以通过多种方式实现,但我个人最喜欢以下内容(伪代码):
这种方法的输入始终为[m_outlineIndex],而输出始终为[!M_outlineIndex],为使用其他后期效果提供了灵活性。让我们看一下像素着色器: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_input_ps linear v2.xy dcl_output o0.xyzw dcl_temps 4 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0) 15: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r0.zwzz, t1.xyzw, s1 16: dp4 r1.x, r1.xyzw, r2.xyzw 17: add r2.xyzw, r0.zwzw, l(0.003906, 0.000000, -0.003906, 0.000000) 18: add r0.xyzw, r0.xyzw, l(0.000000, 0.003906, 0.000000, -0.003906) 19: sample_indexable(texture2d)(float,float,float,float) r1.yz, r2.xyxx, t1.zxyw, s1 20: sample_indexable(texture2d)(float,float,float,float) r2.xy, r2.zwzz, t1.xyzw, s1 21: add r1.yz, r1.yyzy, -r2.xxyx 22: sample_indexable(texture2d)(float,float,float,float) r0.xy, r0.xyxx, t1.xyzw, s1 23: sample_indexable(texture2d)(float,float,float,float) r0.zw, r0.zwzz, t1.zwxy, s1 24: add r0.xy, -r0.zwzz, r0.xyxx 25: max r0.xy, abs(r0.xyxx), abs(r1.yzyy) 26: min r0.xy, r0.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 27: mul r0.xy, r0.xyxx, r1.xxxx 28: sample_indexable(texture2d)(float,float,float,float) r0.zw, v2.xyxx, t0.zwxy, s0 29: mad r0.w, r1.x, l(0.150000), r0.w 30: mad r0.x, r0.x, l(0.350000), r0.w 31: mad r0.x, r0.y, l(0.350000), r0.x 32: mul r0.yw, cb3[0].zzzw, l(0.000000, 300.000000, 0.000000, 300.000000) 33: mad r0.yw, v2.xxxy, l(0.000000, 150.000000, 0.000000, 150.000000), r0.yyyw 34: ftoi r0.yw, r0.yyyw 35: bfrev r0.w, r0.w 36: iadd r0.y, r0.w, r0.y 37: ishr r0.w, r0.y, l(13) 38: xor r0.y, r0.y, r0.w 39: imul null, r0.w, r0.y, r0.y 40: imad r0.w, r0.w, l(0x0000ec4d), l(0.0000000000000000000000000000000000001) 41: imad r0.y, r0.y, r0.w, l(146956042240.000000) 42: and r0.y, r0.y, l(0x7fffffff) 43: itof r0.y, r0.y 44: mad r0.y, r0.y, l(0.000000001), l(0.650000) 45: add_sat r1.xyzw, v2.xyxy, l(0.001953, 0.000000, -0.001953, 0.000000) 46: sample_indexable(texture2d)(float,float,float,float) r0.w, r1.xyxx, t0.yzwx, s0 47: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.zwzz, t0.xyzw, s0 48: add r0.w, r0.w, r1.x 49: add_sat r1.xyzw, v2.xyxy, l(0.000000, 0.001953, 0.000000, -0.001953) 50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t0.xyzw, s0 51: sample_indexable(texture2d)(float,float,float,float) r1.y, r1.zwzz, t0.yxzw, s0 52: add r0.w, r0.w, r1.x 53: add r0.w, r1.y, r0.w 54: mad r0.w, r0.w, l(0.250000), -r0.z 55: mul r0.w, r0.y, r0.w 56: mul r0.y, r0.y, r0.z 57: mad r0.x, r0.w, l(0.900000), r0.x 58: mad r0.y, r0.y, l(-0.240000), r0.x 59: add r0.x, r0.y, r0.z 60: mov_sat r0.z, cb3[0].x 61: log r0.z, r0.z 62: mul r0.z, r0.z, l(100.000000) 63: exp r0.z, r0.z 64: mad r0.z, r0.z, l(0.160000), l(0.700000) 65: mul o0.xy, r0.zzzz, r0.xyxx 66: mov o0.zw, l(0, 0, 0, 0) 67: ret
如您所见,输出轮廓图被分为四个相等的正方形,这是我们需要研究的第一件事: 0: add r0.xyzw, v2.xyxy, v2.xyxy 1: round_ni r1.xy, r0.zwzz 2: frc r0.xyzw, r0.xyzw 3: add r1.zw, r1.xxxy, l(0.000000, 0.000000, -1.000000, -1.000000) 4: dp2 r1.z, r1.zwzz, r1.zwzz 5: add r1.z, -r1.z, l(1.000000) 6: max r2.w, r1.z, l(0) 7: dp2 r1.z, r1.xyxx, r1.xyxx 8: add r3.xyzw, r1.xyxy, l(-1.000000, -0.000000, -0.000000, -1.000000) 9: add r1.x, -r1.z, l(1.000000) 10: max r2.x, r1.x, l(0) 11: dp2 r1.x, r3.xyxx, r3.xyxx 12: dp2 r1.y, r3.zwzz, r3.zwzz 13: add r1.xy, -r1.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000) 14: max r2.yz, r1.xxyx, l(0, 0, 0, 0)
我们首先计算地板(TextureUV * 2.0),这将为我们提供以下信息:为了确定各个正方形,使用了一个小函数: float getParams(float2 uv) { float d = dot(uv, uv); d = 1.0 - d; d = max( d, 0.0 ); return d; }
请注意,该函数使用输入float2(0.0,0.0)返回1.0。这种情况发生在左上角。要在右上角得到相同的情况,请从四舍五入的texcoords中减去float2(1,0),为绿色正方形减去float2(0,1),为黄色正方形减去float2(1.0,1.0)。因此:
float2 flooredTextureUV = floor( 2.0 * TextureUV ); ... float2 uv1 = flooredTextureUV; float2 uv2 = flooredTextureUV + float2(-1.0, -0.0); float2 uv3 = flooredTextureUV + float2( -0.0, -1.0); float2 uv4 = flooredTextureUV + float2(-1.0, -1.0); float4 mask; mask.x = getParams( uv1 ); mask.y = getParams( uv2 ); mask.z = getParams( uv3 ); mask.w = getParams( uv4 );
每个蒙版成分要么为零,要么为一,并负责纹理的一平方。例如,mask.r和mask.w:遮罩mask.w我们有mask,让我们继续。第15行采样亮度图。请注意,尽管我们采样了所有rgba分量,但亮度纹理的格式为R11G11B10_FLOAT。在这种情况下,假定.a为1.0f。可以将用于此操作的Texcoords计算为frac(TextureUV * 2.0)。因此,此操作的结果可能例如如下所示:看到相似之处吗?下一步非常明智-执行四分量标量积(dp4): 16: dp4 r1.x, r1.xyzw, r2.xyzw
因此,只有红色通道(即只有有趣的物体)保留在左上角,只有绿色通道在右上角(仅是迹线),并且所有在右下角都有(因为亮度分量.w被间接设置为1.0)。好主意。标量积的结果如下所示:收到此masterFilter之后,我们就可以确定对象的轮廓了。它并不像看起来那样困难。该算法与用于获得清晰度的算法非常相似-我们需要获取值的最大绝对差。这就是发生的情况:我们在要处理的当前纹理像素旁边采样四个纹理像素(重要的是:在这种情况下,纹理像素大小为1.0 / 256.0!),并计算红色和绿色通道的最大绝对差: float fTexel = 1.0 / 256; float2 sampling1 = TextureUV + float2( fTexel, 0 ); float2 sampling2 = TextureUV + float2( -fTexel, 0 ); float2 sampling3 = TextureUV + float2( 0, fTexel ); float2 sampling4 = TextureUV + float2( 0, -fTexel ); float2 intensity_x0 = texIntensityMap.Sample( sampler1, sampling1 ).xy; float2 intensity_x1 = texIntensityMap.Sample( sampler1, sampling2 ).xy; float2 intensity_diff_x = intensity_x0 - intensity_x1; float2 intensity_y0 = texIntensityMap.Sample( sampler1, sampling3 ).xy; float2 intensity_y1 = texIntensityMap.Sample( sampler1, sampling4 ).xy; float2 intensity_diff_y = intensity_y0 - intensity_y1; float2 maxAbsDifference = max( abs(intensity_diff_x), abs(intensity_diff_y) ); maxAbsDifference = saturate(maxAbsDifference);
现在,如果我们乘的过滤器上maxAbsDifference ...非常简单高效。接收到轮廓后,我们从前一帧中采样轮廓图。然后,为了获得“幽灵般”的效果,我们采用了当前通过计算出的部分参数以及等高线图中的值。向我们的老朋友问好-整数噪音。他在这里。动画参数(cb3 [0] .zw)从常量缓冲区中获取,并随时间变化。 float2 outlines = masterFilter * maxAbsDifference;
注意:如果您想自己实现巫婆的本能,那么我建议将整数噪声限制为[-1; 1]区间(如其网站上所述)。原始的TW3着色器没有任何限制,但是如果没有它,我将得到可怕的伪影,整个轮廓图不稳定。然后,我们以与之前的亮度图相同的方式对轮廓图进行采样(这次,纹理像素的大小为1.0 / 512.0),并计算.x分量的平均值:
然后,通过汇编代码判断,计算该特定像素的平均值与值之差,然后执行整数噪声引起的失真:
下一步是使用噪声使“旧”轮廓图的值失真-这是使输出纹理具有块状感觉的主线。然后还有其他计算,然后在最后计算“衰减”。
这是一个简短的视频,演示了实际的轮廓图:如果您对全像素着色器感兴趣,则可以在此处获得。Shader与RenderDoc兼容。有趣的是(老实说,有点烦人),尽管汇编代码与Witcher 3的原始着色器相同,但RenderDoc中轮廓图的最终外观仍在改变!注意:在最后一遍(请参阅下一部分)中,您将看到仅使用等高线图的.r通道。为什么我们需要.g通道?我认为这是一种纹理中的乒乓缓冲区-请注意.r包含.g通道+一些新值。第五部分:巫师的天才(鱼眼和最终结果)
我们将简要列出我们已经拥有的东西:在第一部分中,专门针对女巫的直觉,将生成一个全屏亮度图,该图表明效果取决于距离的明显程度。在第二部分中,我更详细地研究了轮廓图,该轮廓图负责完成效果的轮廓和动画。我们到了最后阶段。所有这些都需要结合起来!最后一遍是全屏四边形。输入:颜色缓冲区,轮廓图和亮度图。至:之后:
我将再次展示具有所应用效果的视频:如您所见,除了将轮廓应用于Geralt可以看到或听到的对象之外,鱼眼效果还应用于整个屏幕,并且整个屏幕(尤其是角落)都变灰,以传达真正的怪物猎人的感觉。完全组装的像素着色器代码: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[3], immediateIndexed dcl_constantbuffer cb3[7], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s2, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t2 dcl_resource_texture2d (float,float,float,float) t3 dcl_input_ps_siv v0.xy, position dcl_output o0.xyzw dcl_temps 7 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000) 14: mov_sat r0.w, cb3[6].x 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw 34: sample_indexable(texture2d)(float,float,float,float) r2.xyz, r1.zwzz, t0.xyzw, s0 35: mul r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 36: sample_indexable(texture2d)(float,float,float,float) r0.y, r3.xyxx, t2.yxzw, s2 37: mad r3.xy, r1.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000), l(0.500000, 0.000000, 0.000000, 0.000000) 38: sample_indexable(texture2d)(float,float,float,float) r2.w, r3.xyxx, t2.yzwx, s2 39: mul r2.w, r2.w, l(0.125000) 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx 69: dp3 r1.x, r3.yzwy, l(0.300000, 0.300000, 0.300000, 0.000000) 70: add r1.yzw, -r1.xxxx, r3.yyzw 71: mad r1.xyz, r0.zzzz, r1.yzwy, r1.xxxx 72: mad r1.xyz, r1.xyzx, l(0.600000, 0.600000, 0.600000, 0.000000), -r2.xyzx 73: mad r1.xyz, r0.wwww, r1.xyzx, r2.xyzx 74: mul r0.yzw, r0.yyyy, cb3[4].xxyz 75: mul r2.xyz, r0.xxxx, cb3[5].xyzx 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret
82行-所以我们要做很多工作!首先,看一下输入数据:
影响效果大小的主要值是fisheyeAmount。我认为Geralt开始使用本能时,它会从0.0逐渐上升到1.0。其余的值变化不大,但是我怀疑如果用户在选项中禁用了鱼眼效果(我没有对此进行检查),其中的一些值会有所不同。发生的第一件事是,着色器计算造成灰度角度的蒙版: 0: div r0.xy, v0.xyxx, cb0[2].xyxx 1: mad r0.zw, r0.xxxy, l(0.000000, 0.000000, 2.000000, 2.000000), l(0.000000, 0.000000, -1.000000, -1.000000) 2: mov r1.yz, abs(r0.zzwz) 3: div r0.z, cb0[2].x, cb0[2].y 4: mul r1.x, r0.z, r1.y 5: add r0.zw, r1.xxxz, -cb3[2].xxxy 6: mul_sat r0.zw, r0.zzzw, l(0.000000, 0.000000, 0.555556, 0.555556) 7: log r0.zw, r0.zzzw 8: mul r0.zw, r0.zzzw, l(0.000000, 0.000000, 2.500000, 2.500000) 9: exp r0.zw, r0.zzzw 10: dp2 r0.z, r0.zwzz, r0.zwzz 11: sqrt r0.z, r0.z 12: min r0.z, r0.z, l(1.000000) 13: add r0.z, -r0.z, l(1.000000)
在HLSL中,我们可以这样编写:
首先,区间[-1;1] UV及其绝对值。然后是一个棘手的“挤压”。成品口罩如下:稍后我将返回此蒙版。现在,我将有意跳过几行代码,并仔细研究负责缩放效果的代码。 22: add r1.xy, v0.xyxx, v0.xyxx 23: div r1.xy, r1.xyxx, cb0[2].xyxx 24: add r1.xy, r1.xyxx, l(-1.000000, -1.000000, 0.000000, 0.000000) 25: dp2 r0.y, r1.xyxx, r1.xyxx 26: mul r1.xy, r0.yyyy, r1.xyxx 27: mul r0.y, r0.w, l(0.100000) 28: mul r1.xy, r0.yyyy, r1.xyxx 29: max r1.xy, r1.xyxx, l(-0.400000, -0.400000, 0.000000, 0.000000) 30: min r1.xy, r1.xyxx, l(0.400000, 0.400000, 0.000000, 0.000000) 31: mul r1.xy, r1.xyxx, cb3[1].xxxx 32: mul r1.zw, r1.xxxy, cb0[2].zzzw 33: mad r1.zw, v0.xxxy, cb0[1].zzzw, -r1.zzzw
首先,计算“加倍”的纹理坐标,并执行减法float2(1、1): float2 uv4 = 2 * PosH.xy; uv4 /= cb0_v2.xy; uv4 -= float2(1.0, 1.0);
这样的texcoord可以如下显示:然后计算标量积点(uv4,uv4),这给了我们掩码:用于乘以上面的texcoords:重要提示:在左上角(黑色像素),该值为负。由于R11G11B10_FLOAT格式的准确性有限,它们以黑色(0.0)显示。它没有符号位,因此不能在其中存储负值。然后计算衰减系数(如上所述,fisheyeAmount在0.0到1.0 之间变化)。 float attenuation = fisheyeAmount * 0.1; uv4 *= attenuation;
然后执行限制(最大/最小)和一个乘法。因此,计算出偏移量。要计算最终的uv(将用于对颜色纹理进行采样),我们只需执行减法操作即可:float2 colorUV = mainUv-offset;通过对输入的colorUV颜色纹理进行采样,我们在拐角处得到了失真的图像:概述
下一步是对等高线图进行采样以找到等高线。这很简单,首先我们找到texcoords来采样有趣物体的轮廓,然后对轨道进行相同的处理:
等高线图中有趣的物体轮廓图上的迹线值得注意的是,我们仅从轮廓图上采样.x通道,并且仅考虑了上方的正方形。机芯
为了实现轨道的运动,使用了与醉酒效果几乎相同的技巧。添加了一个单位大小的圆圈,我们对轮廓图进行了8倍采样,以获取有趣的对象和迹线以及颜色纹理。请注意,我们仅将找到的路径除以8.0。由于我们处于纹理坐标[0-1] 2的空间中,因此存在一个半径为1的圆以使单个像素环绕的操作将产生不可接受的伪像:因此,在继续之前,让我们先了解一下如何计算此半径。为此,我们需要返回到缺少的第15-21行。计算此半径的一个小问题是其计算分散在着色器周围(可能是由于编译器优化了着色器)。因此,这是第一部分(15-21)和第二部分(41-42): 15: add_sat r1.xy, -r0.xyxx, l(0.030000, 0.030000, 0.000000, 0.000000) 16: add r1.x, r1.y, r1.x 17: add_sat r0.xy, r0.xyxx, l(-0.970000, -0.970000, 0.000000, 0.000000) 18: add r0.x, r0.x, r1.x 19: add r0.x, r0.y, r0.x 20: mul r0.x, r0.x, l(20.000000) 21: min r0.x, r0.x, l(1.000000) ... 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000)
如您所见,我们仅考虑每个表面旁边的[0.00-0.03]中的纹素,汇总其值,乘以20并使其饱和。这是第15-21行之后的样子:这是第41行之后的方式:在第42行,我们将其乘以0.03,该值是整个屏幕的圆的半径。如您所见,半径越靠近屏幕边缘,半径就越小。现在我们来看一下负责该运动的汇编代码: 40: mul r3.x, cb0[0].x, l(0.100000) 41: add r0.x, -r0.x, l(1.000000) 42: mul r0.xy, r0.xyxx, l(0.030000, 0.125000, 0.000000, 0.000000) 43: mov r3.yzw, l(0, 0, 0, 0) 44: mov r4.x, r0.y 45: mov r4.y, r2.w 46: mov r4.z, l(0) 47: loop 48: ige r4.w, r4.z, l(8) 49: breakc_nz r4.w 50: itof r4.w, r4.z 51: mad r4.w, r4.w, l(0.785375), -r3.x 52: sincos r5.x, r6.x, r4.w 53: mov r6.y, r5.x 54: mul r5.xy, r0.xxxx, r6.xyxx 55: mad r5.zw, r5.xxxy, l(0.000000, 0.000000, 0.125000, 0.125000), r1.zzzw 56: mul r6.xy, r5.zwzz, l(0.500000, 0.500000, 0.000000, 0.000000) 57: sample_indexable(texture2d)(float,float,float,float) r4.w, r6.xyxx, t2.yzwx, s2 58: mad r4.x, r4.w, l(0.125000), r4.x 59: mad r5.zw, r5.zzzw, l(0.000000, 0.000000, 0.500000, 0.500000), l(0.000000, 0.000000, 0.500000, 0.000000) 60: sample_indexable(texture2d)(float,float,float,float) r4.w, r5.zwzz, t2.yzwx, s2 61: mad r4.y, r4.w, l(0.125000), r4.y 62: mad r5.xy, r5.xyxx, r1.xyxx, r1.zwzz 63: sample_indexable(texture2d)(float,float,float,float) r5.xyz, r5.xyxx, t0.xyzw, s0 64: mad r3.yzw, r5.xxyz, l(0.000000, 0.125000, 0.125000, 0.125000), r3.yyzw 65: iadd r4.z, r4.z, l(1) 66: endloop
让我们在这里待一分钟。在第40行上,我们获得了时间系数-elapsedTime * 0.1。在第43行中,我们为循环内获得的颜色纹理提供了一个缓冲区。众所周知,r0.x(第41-42行)是圆的半径。r4.x(第44行)是感兴趣对象的轮廓,r4.y(第45行)是轨道的轮廓(以前被8!除),r4.z(第46行)是循环计数器。如您所料,循环有8次迭代。我们首先以弧度i * PI_4计算角度,这将使我们获得2 * PI-一个完整的圆。角度会随着时间而变形。使用sincos,我们确定采样点(单位圆),并使用乘法更改半径(第54行)。在那之后,我们绕一个像素绕圈,并采样轮廓和颜色。循环之后,我们获得轮廓和颜色的平均值(由于除以8)。 float timeParam = time * 0.1;
颜色采样将以几乎相同的方式执行,但是我们将向基础colorUV添加偏移量乘以“单个”圆。亮度调节
在循环之后,我们对亮度图进行采样并更改最终的亮度值(因为亮度图对轮廓一无所知): 67: sample_indexable(texture2d)(float,float,float,float) r0.xy, r1.zwzz, t3.xyzw, s0 68: mad_sat r0.xy, -r0.xyxx, l(0.800000, 0.750000, 0.000000, 0.000000), r4.xyxx
HLSL代码:
灰色的角落和一切的最终统一
使用标量乘积(汇编器行69)计算更靠近拐角的灰色:
然后进行两个插值。第一个使用我描述的第一个蒙版将灰色与“圆圈中的颜色”结合在一起,因此拐角变为灰色。此外,系数为0.6,可降低最终图像的饱和度:第二种使用fisheyeAmount将第一种颜色与上述颜色结合在一起。这意味着屏幕逐渐变暗(由于乘以0.6),角落处逐渐变灰!精巧。HLSL:
现在我们可以继续添加对象的轮廓。颜色(红色和黄色)来自常量缓冲区。
!我们快到终点了!我们有最终的颜色,有女巫本能的颜色……它仍然需要以某种方式结合起来!为此,简单的添加是不合适的。首先,我们计算标量积: 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) );
看起来像这样:最后,这些值用于在颜色和(饱和的)女巫的天赋之间进行插值: 76: mad r0.xyz, r0.yzwy, l(1.200000, 1.200000, 1.200000, 0.000000), r2.xyzx 77: mov_sat r2.xyz, r0.xyzx 78: dp3_sat r0.x, r0.xyzx, l(1.000000, 1.000000, 1.000000, 0.000000) 79: add r0.yzw, -r1.xxyz, r2.xxyz 80: mad o0.xyz, r0.xxxx, r0.yzwy, r1.xyzx 81: mov o0.w, l(1.000000) 82: ret float3 senses_total = 1.2 * senses_traces + senses_interesting;
仅此而已。完整的着色器可在此处获得。我的(左)着色器和原始(右)着色器的比较:希望您喜欢这篇文章!“维克本能”的机制中有许多绝妙的主意,而且最终的结果很合理。[分析的先前部分:第一和第二。]