《巫师3》的渲染方式是如何实现的:闪电,巫师的天赋和其他效果

图片

第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代码段将如下所示:

  ... // final world-space position float3 vNormal = Input.NormalW * 2.0 - 1.0; float lencameratoworld = length( PositionL - g_cameraPos.xyz) * 0.000001; PositionL += vNormal*lencameratoworld; // SV_Posiiton float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld ); Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) ); return Output; 

此操作导致网格的较小“爆发”(沿法向矢量的方向)。 我尝试用其他几个值替换0.000001。 结果如下:


0.000002


0.000005


0.00001


0.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着色器中也可以找到类似的内容。 我不会用这个片段来描述我的冒险经历,而只是说答案在于整数噪声

  // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } 

如您所见,在像素着色器中它被调用了两次。 使用该网站上的指南,我们可以了解如何正确实现平滑噪声。 一分钟后,我会再谈这个。

看一下第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; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } 


平滑步函数[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; }; // Shaders in TW3 use integer noise. // For more details see: http://libnoise.sourceforge.net/noisegen/ float integerNoise( int n ) { n = (n >> 13) ^ n; int nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7fffffff; return ((float)nn / 1073741824.0); } float s_curve( float x ) { float x2 = x * x; float x3 = x2 * x; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } float4 Lightning_TW3_PS( in VS_OUTPUT Input ) : SV_Target { // * Inputs float elapsedTime = cb0_v0.x; float animationSpeed = cb4_v4.x; float minAmount = cb4_v2.x; float maxAmount = cb4_v3.x; float colorMultiplier = cb4_v0.x; float3 colorFilter = cb4_v1.xyz; float3 lightningColorRGB = cb2_v2.rgb; // Animation using time and X texcoord float animation = elapsedTime * animationSpeed + Input.Texcoords.x; // Input parameters for Integer Noise. // They are floored and please note there are using asint. // That might be an optimization to avoid "ftoi" instructions. int intX0 = asint( floor(animation) ); int intX1 = asint( floor(animation-1.0) ); float n0 = integerNoise( intX0 ); float n1 = integerNoise( intX1 ); // We interpolate "backwards" here. float weight = 1.0 - frac(animation); // Following the instructions from libnoise, we perform // smooth interpolation here with cubic s-curve function. float noise = lerp( n0, n1, s_curve(weight) ); // Make sure we are in [0.0 - 1.0] range. float lightningAmount = saturate( lerp(minAmount, maxAmount, noise) ); lightningAmount *= Input.InstanceLODParams.w; // 1.0 lightningAmount *= cb2_v2.w; // 1.0 // Calculate final lightning color float3 lightningColor = colorMultiplier * colorFilter; lightningColor *= lighntingColorRGB; float3 finalLightningColor = lightningColor * lightningAmount; return float4( finalLightningColor, lightningAmount ); } 

总结一下


在这一部分中,我描述了一种在《巫师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相同的值:


若要了解在浏览窗口转换期间如何使用MinDepthMaxDepth字段 ,请单击此处 (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 ,然后通过将matWorldmatViewProj相乘来计算裁剪空间的最终位置->使用它们的乘积乘以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; // Simple texcoords passing OUT.param0 = IN.param1; // * Manually construct world and viewProj martices from float4s: row_major matrix matWorld = matrix(cb2_v0, cb2_v1, cb2_v2, float4(0,0,0,1) ); matrix matViewProj = matrix(cb1_v0, cb1_v1, cb1_v2, cb1_v3); // * Some optional fun with worldMatrix // a) Scale //matWorld._11 = matWorld._22 = matWorld._33 = 0.225f; // b) Translate // XYZ //matWorld._14 = 520.0997; //matWorld._24 = 74.4226; //matWorld._34 = 113.9; // Local space - note the scale+bias here! //float3 meshScale = float3(2.0, 2.0, 2.0); //float3 meshBias = float3(-1.0, -1.0, -0.4); float3 meshScale = cb2_v4.xyz; float3 meshBias = cb2_v5.xyz; float3 Position = IN.param0 * meshScale + meshBias; // World space float4 PositionW = mul(float4(Position, 1.0), transpose(matWorld) ); OUT.param1 = PositionW.xyz; // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); return OUT; } 

我的着色器(左)和原始着色器(右)的比较:


RenderDoc的一个出色特性是它允许我们注入自己的着色器,而不是原始的着色器,并且这些更改将影响到帧末尾的管道。 从HLSL代码可以看到,我提供了几个用于缩放和变换最终几何图形的选项。 您可以对它们进行试验,并获得非常有趣的结果:


顶点着色器优化


您是否注意到原始顶点着色器的问题? 矩阵的顶点乘以矩阵是完全多余的! 我至少在一些顶点着色器中发现了这一点(例如,在着色器中远处雨帘 )。 我们可以通过立即将PositionW乘以matViewProj来优化它!

因此,我们可以将代码替换为HLSL:

  // Clip space - original approach from The Witcher 3 matrix matWorldViewProj = mul(matViewProj, matWorld); OUT.param2 = mul( float4(Position, 1.0), transpose(matWorldViewProj) ); 

如下:

  // Clip space - optimized version OUT.param2 = mul( matViewProj, PositionW ); 

优化版本为我们提供了以下汇编代码:

  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)-《天堂与太阳》

无论您如何渲染太阳,在某个阶段您仍然需要阳光的(规范化)方向。 获得此向量的最合乎逻辑的方法是使用球坐标 。 实际上,我们只需要两个表示两个角度(以弧度表示)的值即可: Phitheta 。 收到它们后,我们可以假定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 

然后,我们计算cameraToWorldsunDirection向量的标量积! 请记住,必须将它们标准化。 我们还使该完整表达式饱和,以将其限制为间隔[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

有了这个渐变,我们就可以使用它在skyColorsunColor之间进行插值! 为避免出现伪影,您需要使第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),并且将其与之取反(内部函数reversebitsbfrev指令)。

所以,现在慢下来。让我们从头开始。

4 «» . , 4 :

  int getInt( float x ) { return asint( floor(x) ); } int getReverseInt( float x ) { return reversebits( getInt(x) ); } // * Inputs - UV and elapsed time in seconds float2 starsUV; starsUV.x = 100.0 * Input.TextureUV.x; starsUV.y = 50.0 * Input.TextureUV.y + g_fTime; // * Iteration 1 int iStars1_A = getReverseInt( starsUV.y ); int iStars1_B = getInt( starsUV.x ); float fStarsNoise1 = integerNoise( iStars1_A + iStars1_B ); // * Iteration 2 int iStars2_A = getReverseInt( starsUV.y ); int iStars2_B = getInt( starsUV.x - 1.0 ); float fStarsNoise2 = integerNoise( iStars2_A + iStars2_B ); // * Iteration 3 int iStars3_A = getReverseInt( starsUV.y - 1.0 ); int iStars3_B = getInt( starsUV.x ); float fStarsNoise3 = integerNoise( iStars3_A + iStars3_B ); // * Iteration 4 int iStars4_A = getReverseInt( starsUV.y - 1.0 ); int iStars4_B = getInt( starsUV.x - 1.0 ); float fStarsNoise4 = integerNoise( iStars4_A + iStars4_B ); 

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; // -2x^3 + 3x^2 return -2.0*x3 + 3.0*x2; } ... // lines 217-222 float weightX = 1.0 - frac( starsUV.x ); weightX = s_curve( weightX ); // lines 223-228 float weightY = 1.0 - frac( starsUV.y ); weightY = s_curve( weightY ); 

, :

  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之后,最难的部分就完成了。万岁!

下一步是对starsColorstarsColorDisturbed进行伽玛校正,然后将它们相乘:

  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; // Set depth parameters.... // Enable stencil depthstencilState.StencilEnable = TRUE; // Read & write all bits depthstencilState.StencilReadMask = 0xFF; depthstencilState.StencilWriteMask = 0xFF; // Stencil operator for front face depthstencilState.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; // Stencil operator for back face. depthstencilState.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS; depthstencilState.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP; depthstencilState.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE; pDevice->CreateDepthStencilState( &depthstencilState, &m_pDS_AssignValue ); 

要写入缓冲区的stensil值在API调用中作为StencilRef传递

  // from now on set stencil buffer values to 8 pDevCon->OMSetDepthStencilState( m_pDS_AssignValue, 8 ); ... pDevCon->DrawIndexed( ... ); 

渲染亮度


从实现的角度来看,这段代码以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) { // * Inputs // Directly affects radius of the effect float distanceScaling = cb3_v6.y; // Color of output at the end float3 color = cb3_v0.rgb; // Sample depth float2 uv = IN.param0.xy * cb0_v1.zw; float depth = texture15.Sample( sampler15, uv ).x; // Reconstruct world position float3 worldPos = getWorldPos( IN.param0.xy, depth ); // Calculate distance from Geralt to world position of particular object float dist_geraltToWorld = length( worldPos - cb3_v7.xyz ); // Calculate two squeezing params float t0 = 1.0 + 120*pow( abs(distanceScaling), 2.8 ); float t1 = 1.0 + 120*pow( abs(distanceScaling), 0.8 ); // Determine nominator and denominator float2 params; params = (distanceScaling > 0.03) ? float2(dist_geraltToWorld * 0.12, t0) : float2(dist_geraltToWorld, t1); // Distance Geralt <-> Object float nominator = params.x; // Hiding factor float denominator = params.y; // Raise to power of 1.6 float param = pow( params.x / params.y, 1.6 ); // Calculate final intensity float intensity = max(0.0, 1.0 - param ); // * Final outputs. // * // * This PS outputs only one color, the rest // * is redundant. I just added this to keep 1-1 ratio with // * original assembly. FSOutput OUT = (FSOutput)0; OUT.param0.xyz = color * intensity; // == redundant == OUT.param0.w = cb3_v0.w; OUT.param1 = cb3_v1; OUT.param2 = cb3_v2; OUT.param3 = cb3_v3; // =============== return OUT; } 

原始(左)和我的(右)汇编器着色器代码的比较。


这是巫婆天赋作用的第一步实际上,这是最简单的。

第4部分。巫师的天才(轮廓图)


再次看一下我们正在探索的场景:


在分析女巫的本能的第一部分中,我展示了如何生成“亮度图”。

我们有一个R11G11B10_FLOAT格式的全屏纹理,看起来可能像这样:


绿色通道表示“足迹”,红色是Geralt可以与之交互的有趣对象。

收到此纹理后,我们可以进入下一个阶段-我将其称为“轮廓图”。


这是512x512 R16G16_FLOAT格式的怪异纹理。以“乒乓”的方式实现它很重要。前一帧的轮廓图是输入数据(连同亮度图),以在当前帧中生成新的轮廓图。

乒乓缓冲区可以通过多种方式实现,但我个人最喜欢以下内容(伪代码):

  // Declarations Texture2D m_texOutlineMap[2]; uint m_outlineIndex = 0; // Rendering void Render() { pDevCon->SetInputTexture( m_texOutlineMap[m_outlineIndex] ); pDevCon->SetOutputTexture( m_texOutlineMap[!m_outlineIndex] ); ... pDevCon->Draw(...); // after draw m_outlineIndex = !m_outlineIndex; } 

这种方法的输入始终为[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.rmask.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; // Sample outline map float2 outlineMap = texOutlineMap.Sample( samplerLinearWrap, uv ).xy; // I guess it's related with ghosting float paramOutline = masterFilter*0.15 + outlineMap.y; paramOutline += 0.35 * outlines.r; paramOutline += 0.35 * outlines.g; // input for integer noise float2 noiseWeights = cb3_v0.zw; float2 noiseInputs = 150.0*uv + 300.0*noiseWeights; int2 iNoiseInputs = (int2) noiseInputs; float noise0 = clamp( integerNoise( iNoiseInputs.x + reversebits(iNoiseInputs.y) ), -1, 1 ) + 0.65; // r0.y 

注意:如果您想自己实现巫婆的本能,那么我建议将整数噪声限制为[-1; 1]区间(如其网站上所述)。原始的TW3着色器没有任何限制,但是如果没有它,我将得到可怕的伪影,整个轮廓图不稳定。

然后,我们以与之前的亮度图相同的方式对轮廓图进行采样(这次,纹理像素的大小为1.0 / 512.0),并计算.x分量的平均值:

  // sampling of outline map fTexel = 1.0 / 512.0; sampling1 = saturate( uv + float2( fTexel, 0 ) ); sampling2 = saturate( uv + float2( -fTexel, 0 ) ); sampling3 = saturate( uv + float2( 0, fTexel ) ); sampling4 = saturate( uv + float2( 0, -fTexel ) ); float outline_x0 = texOutlineMap.Sample( sampler0, sampling1 ).x; float outline_x1 = texOutlineMap.Sample( sampler0, sampling2 ).x; float outline_y0 = texOutlineMap.Sample( sampler0, sampling3 ).x; float outline_y1 = texOutlineMap.Sample( sampler0, sampling4 ).x; float averageOutline = (outline_x0+outline_x1+outline_y0+outline_y1) / 4.0; 

然后,通过汇编代码判断,计算该特定像素的平均值与值之差,然后执行整数噪声引起的失真:

  // perturb with noise float frameOutlineDifference = averageOutline - outlineMap.x; frameOutlineDifference *= noise0; 

下一步是使用噪声使“旧”轮廓图的值失真-这是使输出纹理具有块状感觉的主线。

然后还有其他计算,然后在最后计算“衰减”。

  // the main place with gives blocky look of texture float newNoise = outlineMap.x * noise0; float newOutline = frameOutlineDifference * 0.9 + paramOutline; newOutline -= 0.24*newNoise; // 59: add r0.x, r0.y, r0.z float2 finalOutline = float2( outlineMap.x + newOutline, newOutline); // * calculate damping float dampingParam = saturate( cb3_v0.x ); dampingParam = pow( dampingParam, 100 ); float damping = 0.7 + 0.16*dampingParam; // * final multiplication float2 finalColor = finalOutline * damping; return float4(finalColor, 0, 0); 

这是一个简短的视频,演示了实际的轮廓图:


如果您对全像素着色器感兴趣,则可以在此处获得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行-所以我们要做很多工作!

首先,看一下输入数据:

  // *** Inputs // * Zoom amount, always 1 float zoomAmount = cb3_v1.x; // Another value which affect fisheye effect // but always set to float2(1.0, 1.0). float2 amount = cb0_v2.zw; // Elapsed time in seconds float time = cb0_v0.x; // Colors of witcher senses float3 colorInteresting = cb3_v5.rgb; float3 colorTraces = cb3_v4.rgb; // Was always set to float2(0.0, 0.0). // Setting this to higher values // makes "grey corners" effect weaker. float2 offset = cb3_v2.xy; // Dimensions of fullscreen float2 texSize = cb0_v2.xy; float2 invTexSize = cb0_v1.zw; // Main value which causes fisheye effect [0-1] const float fisheyeAmount = saturate( cb3_v6.x ); 

影响效果大小的主要值是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中,我们可以这样编写:

  // Main uv float2 uv = PosH.xy / texSize; // Scale at first from [0-1] to [-1;1], then calculate abs float2 uv3 = abs( uv * 2.0 - 1.0); // Aspect ratio float aspectRatio = texSize.x / texSize.y; // * Mask used to make corners grey float mask_gray_corners; { float2 newUv = float2( uv3.x * aspectRatio, uv3.y ) - offset; newUv = saturate( newUv / 1.8 ); newUv = pow(newUv, 2.5); mask_gray_corners = 1-min(1.0, length(newUv) ); } 

首先,区间[-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来采样有趣物体的轮廓,然后对轨道进行相同的处理:

  // * Sample outline map // interesting objects (upper left square) float2 outlineUV = colorUV * 0.5; float outlineInteresting = texture2.Sample( sampler2, outlineUV ).x; // r0.y // traces (upper right square) outlineUV = colorUV * 0.5 + float2(0.5, 0.0); float outlineTraces = texture2.Sample( sampler2, outlineUV ).x; // r2.w outlineInteresting /= 8.0; // r4.x outlineTraces /= 8.0; // r4.y 


等高线图中有趣的物体


轮廓图上的迹线

值得注意的是,我们仅从轮廓图上采样.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; // adjust circle radius circle_radius = 1.0 - circle_radius; circle_radius *= 0.03; float3 color_circle_main = float3(0.0, 0.0, 0.0); [loop] for (int i=0; 8 > i; i++) { // full 2*PI = 360 angles cycle const float angleRadians = (float) i * PI_4 - timeParam; // unit circle float2 unitCircle; sincos(angleRadians, unitCircle.y, unitCircle.x); // unitCircle.x = cos, unitCircle.y = sin // adjust radius unitCircle *= circle_radius; // * base texcoords (circle) - note we also scale radius here by 8 // * probably because of dimensions of outline map. // line 55 float2 uv_outline_base = colorUV + unitCircle / 8.0; // * interesting objects (circle) float2 uv_outline_interesting_circle = uv_outline_base * 0.5; float outline_interesting_circle = texture2.Sample( sampler2, uv_outline_interesting_circle ).x; outlineInteresting += outline_interesting_circle / 8.0; // * traces (circle) float2 uv_outline_traces_circle = uv_outline_base * 0.5 + float2(0.5, 0.0); float outline_traces_circle = texture2.Sample( sampler2, uv_outline_traces_circle ).x; outlineTraces += outline_traces_circle / 8.0; // * sample color texture (zooming effect) with perturbation float2 uv_color_circle = colorUV + unitCircle * offsetUV; float3 color_circle = texture0.Sample( sampler0, uv_color_circle ).rgb; color_circle_main += color_circle / 8.0; } 

颜色采样将以几乎相同的方式执行,但是我们将向基础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代码:

  // * Sample intensity map float2 intensityMap = texture3.Sample( sampler0, colorUV ).xy; float intensityInteresting = intensityMap.r; float intensityTraces = intensityMap.g; // * Adjust outlines float mainOutlineInteresting = saturate( outlineInteresting - 0.8*intensityInteresting ); float mainOutlineTraces = saturate( outlineTraces - 0.75*intensityTraces ); 

灰色的角落和一切的最终统一


使用标量乘积(汇编器行69)计算更靠近拐角的灰色:

  // * Greyish color float3 color_greyish = dot( color_circle_main, float3(0.3, 0.3, 0.3) ).xxx; 


然后进行两个插值。第一个使用我描述的第一个蒙版将灰色与“圆圈中的颜色”结合在一起,因此拐角变为灰色。此外,系数为0.6,可降低最终图像的饱和度:


第二种使用fisheyeAmount将第一种颜色与上述颜色结合在一起这意味着屏幕逐渐变暗(由于乘以0.6),角落处逐渐变灰!精巧。

HLSL:

  // * Determine main color. // (1) At first, combine "circled" color with gray one. // Now we have have greyish corners here. float3 mainColor = lerp( color_greyish, color_circle_main, mask_gray_corners ) * 0.6; // (2) Then mix "regular" color with the above. // Please note this operation makes corners gradually gray (because fisheyeAmount rises from 0 to 1) // and gradually darker (because of 0.6 multiplier). mainColor = lerp( color, mainColor, fisheyeAmount ); 

现在我们可以继续添加对象的轮廓。

颜色(红色和黄色)来自常量缓冲区。

  // * Determine color of witcher senses float3 senses_traces = mainOutlineTraces * colorTraces; float3 senses_interesting = mainOutlineInteresting * colorInteresting; float3 senses_total = 1.2 * senses_traces + senses_interesting; 


我们快到终点了!

我们有最终的颜色,有女巫本能的颜色……它仍然需要以某种方式结合起来!

为此,简单的添加是不合适的。首先,我们计算标量积:

  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; // * Final combining float3 senses_total_sat = saturate(senses_total); float dot_senses_total = saturate( dot(senses_total, float3(1.0, 1.0, 1.0) ) ); float3 finalColor = lerp( mainColor, senses_total_sat, dot_senses_total ); return float4( finalColor, 1.0 ); 


仅此而已。

完整的着色器可在此处获得

我的(左)着色器和原始(右)着色器的比较:


希望您喜欢这篇文章!“维克本能”的机制中有许多绝妙的主意,而且最终的结果很合理。

[分析的先前部分:第一第二。]

Source: https://habr.com/ru/post/zh-CN450332/


All Articles