《巫师3》渲染的逆向工程:各种天空效果

图片

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

第1部分。卷云


当游戏在开放空间中进行时,决定世界信誉的因素之一就是天空。考虑一下-大多数情况下,天空实际上占据了整个屏幕的40-50%。天空不仅仅是美丽的梯度。它有星星,太阳,月亮,最后还有云。

尽管当前的趋势似乎在于使用光线行进法对云进行体积渲染(请参阅本文),但是《巫师3》中的云完全基于纹理。我之前已经检查过它们,但事实证明,有了它们,一切都比我最初预期的要复杂。如果您关注了我的系列文章,那么您就会知道Blood and Wine DLC与游戏其余部分之间存在差异。而且,您可能会猜到,DLC中使用云的工作有所变化。

巫师3有几层云。根据天气的不同,它只能是卷云高积云,可能是层云家族中的几朵云(例如,暴风雨期间)。最后,可能根本没有云。

某些图层在用于渲染它们的纹理和着色器方面有所不同。显然,这会影响像素着色器的汇编代码的复杂度和长度。

尽管存在所有这些多样性,但在Witcher 3中渲染云时仍可以观察到一些常见的模式。首先,它们都以主动方式进行渲染,这是理想的选择。他们都使用混合(见下文)。这样可以更轻松地控制单独的图层如何覆盖天空-这受像素着色器的alpha值影响。


更有趣的是,某些图层使用相同的参数渲染两次。

看完代码后,我选择了最短的着色器,以便(1)最有可能执行其完整的反向工程,(2)找出所有方面。

我仔细观察了《巫师3:血与酒》中的卷云。

这是一个示例框架:


渲染之前


第一次渲染通过之后


在第二个渲染通道之后。

在此特定帧中,卷云是渲染的第一层。如您所见,它被渲染了两次,从而增加了它的亮度。

几何和顶点着色器


在像素着色器之前,我们将简要讨论使用的几何着色器和顶点着色器。用于显示云的网格有点像普通的穹顶:


所有顶点都在[0-1]区间内,因此要使网格在点(0,0,0)上居中,请在转换为worldViewProj之前使用缩放和偏差(我们已经从系列的先前部分中知道了这种模式)。在云的情况下,网格沿XY平面(Z轴指向上)强烈伸展,以覆盖比可见角金字塔更大的空间。结果如下:


此外,网格具有法线和切向量。顶点着色器还通过矢量乘积计算双切向量-所有三个均以标准化形式显示。还有一个最高级的雾计算(它的颜色和亮度)。

像素着色器


像素着色器汇编代码如下所示:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb12[238], immediateIndexed  
    dcl_constantbuffer cb4[13], immediateIndexed  
    dcl_sampler s0, mode_default  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.w  
    dcl_input_ps linear v3.xyzw  
    dcl_input_ps linear v4.xyz  
    dcl_input_ps linear v5.xyz  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  
   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx  
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx  
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0  
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)  
   8: add r1.xyz, r1.xyzx, r1.xyzx  
   9: dp3 r0.w, r1.xyzx, r1.xyzx  
  10: rsq r0.w, r0.w  
  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  
  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x  
  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x  
  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x  
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x  
  53: mul r0.w, r0.w, r1.x  
  54: mul_sat r0.w, r0.w, cb4[6].x  
  55: mul o0.xyz, r0.wwww, r0.xyzx  
  56: mov o0.w, r0.w  
  57: ret 

输入两个无缝纹理。其中一个包含法线贴图(xyz通道)和云形状(通道a)。第二个是使形状变形的噪音。


法线贴图,CD Projekt红色属性


云形,属性CD项目红色


噪声纹理,CD Projekt Red

属性。具有云参数的常量的主要缓冲区为cb4。对于此框架,它具有以下含义:


另外,使用来自其他cbuffer的其他值。不用担心,我们也会考虑它们。

Z向倒置阳光


着色器中发生的第一件事是计算沿Z轴反转的阳光的归一化方向:

   0: mul r0.xyz, cb0[9].xyzx, l(1.000000, 1.000000, -1.000000, 0.000000)  
   1: dp3 r0.w, r0.xyzx, r0.xyzx  
   2: rsq r0.w, r0.w  
   3: mul r0.xyz, r0.wwww, r0.xyzx  

   float3 invertedSunlightDir = normalize(lightDir * float3(1, 1, -1) );

如前所述,Z轴指向上方,而cb0 [9]是太阳光的方向。该向量针对阳光下-这是很重要的!您可以通过编写运行简单NdotL的简单计算着色器并将其插入延迟的着色器遍历中来验证这一点。

云纹理采样


下一步是计算texcoords以采样云纹理,解压缩法线向量并将其标准化。

   4: mul r1.xy, cb0[0].xxxx, cb4[5].xyxx   
   5: mad r1.xy, v1.xyxx, cb4[4].xyxx, r1.xyxx   
   6: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0   
   7: add r1.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)   
   8: add r1.xyz, r1.xyzx, r1.xyzx   
   9: dp3 r0.w, r1.xyzx, r1.xyzx   
  10: rsq r0.w, r0.w   
   
   
   // Calc sampling coords  
   float2 cloudTextureUV = Texcoords * textureScale + elapsedTime * speedFactors;  
   
   // Sample texture and get data from it  
   float4 cloudTextureValue = texture0.Sample( sampler0, cloudTextureUV ).rgba;  
   float3 normalMap = cloudTextureValue.xyz;  
   float cloudShape = cloudTextureValue.a;  
   
   // Unpack normal and normalize it  
   float3 unpackedNormal = (normalMap - 0.5) * 2.0;  
   unpackedNormal = normalize(unpackedNormal);

让我们逐步解决它。

为了获得云的运动,我们需要经过的时间(秒)(cb [0] .x)乘以速度系数,这会影响云在天空中移动的速度(cb4 [5] .xy)。

正如我之前说的,UV沿着穹顶的几何形状伸展,我们还需要影响云大小的纹理缩放因子(cb4 [4] .xy)。

最终公式为:

samplingUV = Input.TextureUV * textureScale + time * speedMultiplier;

对所有4个通道进行采样后,我们得到一个法线贴图(RGB通道)和一个云形状(通道a)。

从区间[0; 1]在区间[-1; 1]我们使用以下公式:

unpackedNormal = (packedNormal - 0.5) * 2.0;

您还可以使用以下命令:

unpackedNormal = packedNormal * 2.0 - 1.0;

最后,我们将解压缩后的法线向量归一化。

覆盖法线


在具有法线向量,来自顶点着色器的切线和双切线向量以及法线贴图的法线向量的情况下,我们通常会映射法线。

  11: mul r1.xyz, r0.wwww, r1.xyzx  
  12: mul r2.xyz, r1.yyyy, v3.xyzx  
  13: mad r2.xyz, v5.xyzx, r1.xxxx, r2.xyzx  
  14: mov r3.xy, v1.zwzz  
  15: mov r3.z, v3.w  
  16: mad r1.xyz, r3.xyzx, r1.zzzz, r2.xyzx  
    
   // Perform bump mapping  
   float3 SkyTangent = Input.Tangent;  
   float3 SkyNormal = (float3( Input.Texcoords.zw, Input.param3.w ));  
   float3 SkyBitangent = Input.param3.xyz;  
        
   float3x3 TBN = float3x3(SkyTangent, SkyBitangent, SkyNormal);  
   float3 finalNormal = (float3)mul( unpackedNormal, (TBN) );

亮度(1)


在下一步中,将应用NdotL计算,这会影响特定像素的照明量。

考虑以下汇编代码:

  17: dp3_sat r0.x, r0.xyzx, r1.xyzx  
  18: add r0.y, -cb4[2].x, cb4[3].x  
  19: mad r0.x, r0.x, r0.y, cb4[2].x  

这是有关框架上NdotL的可视化:


此标量积(具有饱和度)用于在minIntensity和maxIntensity之间进行插值。因此,阳光照射下的云层会更亮。

   // Calculate cosine between normal and up-inv lightdir  
   float NdotL = saturate( dot(invertedSunlightDir, finalNormal) );  
   
   // Param 1, line 19, r0.x  
   float intensity1 = lerp( param1Min, param1Max, NdotL );

亮度(2)


还有另一个因素会影响云的亮度。

应当更加突出位于太阳所在的天空部分的云。为此,我们基于XY平面计算梯度。

该梯度用于计算最小值/最大值之间的线性插值,类似于在部分(1)中发生的情况。

也就是说,从理论上讲,我们可以要求使位于太阳另一侧的云变暗,但这在此特定帧中不会发生,因为param2Minparam2Maxcb4 [0] .xcb4 [1] .x)设置为1.0f。

  20: dp2 r0.y, -cb0[9].xyxx, -cb0[9].xyxx  
  21: rsq r0.y, r0.y  
  22: mul r0.yz, r0.yyyy, -cb0[9].xxyx  
  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  28: mul r1.xy, r1.zzzz, r1.xyxx  
  29: dp2_sat r0.y, r0.yzyy, r1.xyxx  
  30: add r0.y, r0.y, r0.y  
  31: min r0.y, r0.y, l(1.000000)  
  32: add r0.z, -cb4[0].x, cb4[1].x  
  33: mad r0.z, r0.y, r0.z, cb4[0].x  
  34: mul r0.x, r0.x, r0.z  
  35: log r0.x, r0.x  
  36: mul r0.x, r0.x, l(2.200000)  
  37: exp r0.x, r0.x   
   
   
   // Calculate normalized -lightDir.xy (20-22)  
   float2 lightDirXY = normalize( -lightDir.xy );  
   
   // Calculate world to camera  
   float3 vWorldToCamera = ( CameraPos - WorldPos );  
   float worldToCamera_distance = length(vWorldToCamera);  
        
   // normalize vector  
   vWorldToCamera = normalize( vWorldToCamera );  
        
   
   float LdotV = saturate( dot(lightDirXY, vWorldToCamera.xy) );  
   float highlightedSkySection = saturate( 2*LdotV );  
   float intensity2 = lerp( param2Min, param2Max, highlightedSkySection );  
   
   float finalIntensity = pow( intensity2 *intensity1, 2.2);

最后,我们将两个亮度相乘并将结果提高到2.2的幂。

云色


云的颜色的计算首先从缓冲区常量中获得两个值,这些值指示太阳旁边的云和天空另一侧的云的颜色。在它们之间,基于高亮显示的 SkySection进行线性插值

然后将结果乘以finalIntensity

最后,结果与雾混合(出于性能原因,它是由顶点着色器计算的)。

  38: add r1.xyz, cb12[236].xyzx, -cb12[237].xyzx  
  39: mad r1.xyz, r0.yyyy, r1.xyzx, cb12[237].xyzx  
  40: mul r2.xyz, r0.xxxx, r1.xyzx  
  41: mad r0.xyz, -r1.xyzx, r0.xxxx, v0.xyzx  
  42: mad r0.xyz, v0.wwww, r0.xyzx, r2.xyzx  
   
  float3 cloudsColor = lerp( cloudsColorBack, cloudsColorFront, highlightedSunSection );  
  cloudsColor *= finalIntensity;  
  cloudsColor = lerp( cloudsColor, FogColor, FogAmount );

使卷云在地平线上更清晰可见


这在框架上不是很明显,但是实际上,在靠近地平线的地方比在Geralt的头部上方更容易看到这一层。这是操作方法。

您可能会注意到,在计算第二个亮度时,我们计算了向量worldToCamera的长度

  23: add r1.xyz, -v4.xyzx, cb1[8].xyzx  
  24: dp3 r0.w, r1.xyzx, r1.xyzx  
  25: rsq r1.z, r0.w  
  26: sqrt r0.w, r0.w

让我们在代码中找到以下这种长度的情况:

  26: sqrt r0.w, r0.w  
  27: add r0.w, r0.w, -cb4[7].x  
  ...  
  43: add r1.x, -cb4[7].x, cb4[8].x  
  44: div_sat r0.w, r0.w, r1.x

哇,这是怎么回事?

cb [7] .x和cb [8] .x的值分别为2000.0和7000.0。

事实证明,这是使用linstep函数的结果

她收到三个参数:min / max-间隔和v-值。

它的工作原理如下:如果v在间隔[ min - max ]中,则函数将在间隔[0.0-1.0]中返回线性插值。另一方面,如果v超出范围,则linstep返回0.0或1.0。

一个简单的例子:

linstep( 1000.0, 2000.0, 999.0) = 0.0
linstep( 1000.0, 2000.0, 1500.0) = 0.5
linstep( 1000.0, 2000.0, 2000.0) = 1.0

也就是说,它与HLSL 平滑步非常相似,除了在这种情况下,不是执行Hermitian插值而是执行线性。

Linstep不是HLSL的功能,但它非常有用。值得在您的工具包中使用它。

 // linstep:  
 //  
 // Returns a linear interpolation between 0 and 1 if t is in the range [min, max]   
 // if "v" is <= min, the output is 0  
 // if "v" i >= max, the output is 1  
   
 float linstep( float min, float max, float v )  
 {  
   return saturate( (v - min) / (max - min) );  
 } 

让我们回到巫师3:计算完该指标后,报告天空的特定部分离Geralt有多远,我们用它来减弱云层的亮度:

  45: mul r1.x, r1.w, cb4[9].x  
  46: mad r1.y, -cb4[9].x, r1.w, r1.w  
  47: mad r0.w, r0.w, r1.y, r1.x  
   
   float distanceAttenuation = linstep( fadeDistanceStart, fadeDistanceEnd, worldToCamera_distance );  
    
   float fadedCloudShape = closeCloudsHidingFactor * cloudShape;  
   cloudShape = lerp( fadedCloudShape, cloudShape, distanceAttenuation );

cloudShape是第一个纹理的.a通道,closeCloudsHidingFactor是一个恒定的缓冲区值,用于控制Geralt头顶上方云的可见性。在我测试的所有帧中,它等于0.0,这等于没有云。随着distanceAttenuation接近1.0(从摄像机到天空穹顶的距离增加),云变得更加可见。

噪声纹理采样


采样噪声纹理的坐标计算对云的纹理进行类似的计算,不同之处在于,您使用了不同的textureScalespeedMultiplier集

当然,启用了环绕寻址模式的采样器用于采样所有这些纹理

  48: mul r1.xy, cb0[0].xxxx, cb4[11].xyxx  
  49: mad r1.xy, v1.xyxx, cb4[10].xyxx, r1.xyxx  
  50: sample_indexable(texture2d)(float,float,float,float) r1.x, r1.xyxx, t1.xyzw, s0  
   
   // Calc sampling coords for noise  
   float2 noiseTextureUV = Texcoords * textureScaleNoise + elapsedTime * speedFactorsNoise;  
   
   // Sample texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;

全部放在一起


收到噪声值后,我们必须将其与cloudShape结合使用。

我在理解这些行时遇到了一些问题,这些行中有param2.w(始终为1.0)和noiseMult(值为5.0,取自常量缓冲区)。不管怎样

这里最重要的是generalCloudsVisibility的最终值,该会影响云的可见性。

还要看看噪声的最终值。cloudsColor的输出颜色乘以最终噪声,该噪声也会输出到Alpha通道。

  51: mad r1.x, r1.x, cb4[12].x, -cb4[12].x
  52: mad_sat r1.x, cb4[12].x, v2.w, r1.x
  53: mul r0.w, r0.w, r1.x
  54: mul_sat r0.w, r0.w, cb4[6].x
  55: mul o0.xyz, r0.wwww, r0.xyzx
  56: mov o0.w, r0.w
  57: ret   

   // Sample noise texture and get data from it  
   float noiseTextureValue = texture1.Sample( sampler0, noiseTextureUV ).x;  
   noiseTextureValue = noiseTextureValue * noiseMult - noiseMult;  
     
   float noiseValue = saturate( noiseMult * Input.param2.w + noiseTextureValue);  
   noiseValue *= cloudShape;  
     
   float finalNoise = saturate( noiseValue * generalCloudsVisibility);  
   
   return float4( cloudsColor*finalNoise, finalNoise ); 


最终结果看起来非常可信。

您可以比较。第一张图片是我的着色器,第二张是游戏着色器:


如果您好奇,可以在此处使用着色器

第2部分。雾


雾可以以多种方式实现。但是,我们可以应用简单的与距离相关的雾并消除它的时代在过去是永远的(最有可能)。生活在可编程着色器世界中的人们为疯狂的,但更重要的是物理上精确的和视觉上逼真的解决方案打开了大门。

目前在雾渲染趋势是基于计算着色器(有关详细信息,请参阅演示文稿由Bart Wronsky)。

尽管该演示文稿于2014年出现,《巫师3》于2015/2016年发布,但Geralt冒险的最后一部分中的迷雾完全取决于屏幕,并作为典型的后处理来实现。

在我们开始下一个逆向工程会议之前,我必须说,在过去的一年中,我试图找出巫师3的迷雾至少五次,但每次都失败了。正如您将很快看到的,汇编代码非常复杂,这使得在HLSL上创建可读的雾着色器的过程几乎变得不可能。

但是,我设法在Internet上找到了一个雾着色器,因为它在变量名和一般指令顺序方面与Witcher 3 fog相似,因此立即引起了我的注意。该着色器与游戏中的着色器并不完全相同,因此我不得不对其进行一些重新设计。我想说的是,您看到的HLSL代码的主要部分是由我创建/分析的,只有两个例外。记住这一点。

这是像素雾着色器的汇编代码-值得注意的是,整个游戏(2015年的主要部分和两个DLC)都相同:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb3[2], immediateIndexed  
    dcl_constantbuffer cb12[214], immediateIndexed  
    dcl_resource_texture2d (float,float,float,float) t0  
    dcl_resource_texture2d (float,float,float,float) t1  
    dcl_resource_texture2d (float,float,float,float) t2  
    dcl_input_ps_siv v0.xy, position  
    dcl_output o0.xyzw  
    dcl_temps 7  
   0: ftou r0.xy, v0.xyxx  
   1: mov r0.zw, l(0, 0, 0, 0)  
   2: ld_indexable(texture2d)(float,float,float,float) r1.x, r0.xyww, t0.xyzw  
   3: mad r1.y, r1.x, cb12[22].x, cb12[22].y  
   4: lt r1.y, r1.y, l(1.000000)  
   5: if_nz r1.y  
   6:  utof r1.yz, r0.xxyx  
   7:  mul r2.xyzw, r1.zzzz, cb12[211].xyzw  
   8:  mad r2.xyzw, cb12[210].xyzw, r1.yyyy, r2.xyzw  
   9:  mad r1.xyzw, cb12[212].xyzw, r1.xxxx, r2.xyzw  
  10:  add r1.xyzw, r1.xyzw, cb12[213].xyzw  
  11:  div r1.xyz, r1.xyzx, r1.wwww  
  12:  ld_indexable(texture2d)(float,float,float,float) r2.xyz, r0.xyww, t1.xyzw  
  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)  
  24:  div r0.yzw, r0.yyzw, r1.xxxx  
  25:  mad r1.y, r0.w, cb12[22].z, cb12[0].z  
  26:  add r1.x, r1.x, -cb12[22].z  
  27:  max r1.x, r1.x, l(0)  
  28:  min r1.x, r1.x, cb12[42].z  
  29:  mul r1.z, r0.w, r1.x  
  30:  mul r1.w, r1.x, cb12[43].x  
  31:  mul r1.zw, r1.zzzw, l(0.000000, 0.000000, 0.062500, 0.062500)  
  32:  dp3 r0.y, cb12[38].xyzx, r0.yzwy  
  33:  add r0.z, r0.y, cb12[42].x  
  34:  add r0.w, cb12[42].x, l(1.000000)  
  35:  div_sat r0.z, r0.z, r0.w  
  36:  add r0.w, -cb12[43].z, cb12[43].y  
  37:  mad r0.z, r0.z, r0.w, cb12[43].z  
  38:  mul r0.w, abs(r0.y), abs(r0.y)  
  39:  mad_sat r2.w, r1.x, l(0.002000), l(-0.300000)  
  40:  mul r0.w, r0.w, r2.w  
  41:  lt r0.y, l(0), r0.y  
  42:  movc r3.xyz, r0.yyyy, cb12[39].xyzx, cb12[41].xyzx  
  43:  add r3.xyz, r3.xyzx, -cb12[40].xyzx  
  44:  mad r3.xyz, r0.wwww, r3.xyzx, cb12[40].xyzx  
  45:  movc r4.xyz, r0.yyyy, cb12[45].xyzx, cb12[47].xyzx  
  46:  add r4.xyz, r4.xyzx, -cb12[46].xyzx  
  47:  mad r4.xyz, r0.wwww, r4.xyzx, cb12[46].xyzx  
  48:  ge r0.y, r1.x, cb12[48].y  
  49:  if_nz r0.y  
  50:   add r0.y, r1.y, cb12[42].y  
  51:   mul r0.w, r0.z, r0.y  
  52:   mul r1.y, r0.z, r1.z  
  53:   mad r5.xyzw, r1.yyyy, l(16.000000, 15.000000, 14.000000, 13.000000), r0.wwww  
  54:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  55:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  56:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  57:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  58:   mul r1.z, r5.y, r5.x  
  59:   mul r1.z, r5.z, r1.z  
  60:   mul r1.z, r5.w, r1.z  
  61:   mad r5.xyzw, r1.yyyy, l(12.000000, 11.000000, 10.000000, 9.000000), r0.wwww  
  62:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  63:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  64:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  65:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:   mul r1.z, r1.z, r5.x  
  67:   mul r1.z, r5.y, r1.z  
  68:   mul r1.z, r5.z, r1.z  
  69:   mul r1.z, r5.w, r1.z  
  70:   mad r5.xyzw, r1.yyyy, l(8.000000, 7.000000, 6.000000, 5.000000), r0.wwww  
  71:   max r5.xyzw, r5.xyzw, l(0, 0, 0, 0)  
  72:   add r5.xyzw, r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  73:   div_sat r5.xyzw, r1.wwww, r5.xyzw  
  74:   add r5.xyzw, -r5.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  75:   mul r1.z, r1.z, r5.x  
  76:   mul r1.z, r5.y, r1.z  
  77:   mul r1.z, r5.z, r1.z  
  78:   mul r1.z, r5.w, r1.z  
  79:   mad r5.xy, r1.yyyy, l(4.000000, 3.000000, 0.000000, 0.000000), r0.wwww  
  80:   max r5.xy, r5.xyxx, l(0, 0, 0, 0)  
  81:   add r5.xy, r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  82:   div_sat r5.xy, r1.wwww, r5.xyxx  
  83:   add r5.xy, -r5.xyxx, l(1.000000, 1.000000, 0.000000, 0.000000)  
  84:   mul r1.z, r1.z, r5.x  
  85:   mul r1.z, r5.y, r1.z  
  86:   mad r0.w, r1.y, l(2.000000), r0.w  
  87:   max r0.w, r0.w, l(0)  
  88:   add r0.w, r0.w, l(1.000000)  
  89:   div_sat r0.w, r1.w, r0.w  
  90:   add r0.w, -r0.w, l(1.000000)  
  91:   mul r0.w, r0.w, r1.z  
  92:   mad r0.y, r0.y, r0.z, r1.y  
  93:   max r0.y, r0.y, l(0)  
  94:   add r0.y, r0.y, l(1.000000)  
  95:   div_sat r0.y, r1.w, r0.y  
  96:   add r0.y, -r0.y, l(1.000000)  
  97:   mad r0.y, -r0.w, r0.y, l(1.000000)  
  98:   add r0.z, r1.x, -cb12[48].y  
  99:   mul_sat r0.z, r0.z, cb12[48].z  
  100:  else  
  101:   mov r0.yz, l(0.000000, 1.000000, 0.000000, 0.000000)  
  102:  endif  
  103:  log r0.y, r0.y  
  104:  mul r0.w, r0.y, cb12[42].w  
  105:  exp r0.w, r0.w  
  106:  mul r0.y, r0.y, cb12[48].x  
  107:  exp r0.y, r0.y  
  108:  mul r0.yw, r0.yyyw, r0.zzzz  
  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, cb12[188].w, l(-1.000000)  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.z, r0.w  
  115:  lt r0.z, l(0), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, cb12[190].w, l(-1.000000)  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.z, r0.w  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif  
  126:  mul r0.z, r0.x, r5.w  
  127:  mul r0.x, r0.x, r0.y  
  128:  dp3 r0.y, l(0.333000, 0.555000, 0.222000, 0.000000), r2.xyzx  
  129:  mad r1.xyz, r0.yyyy, r4.xyzx, -r2.xyzx  
  130:  mad r0.xyw, r0.xxxx, r1.xyxz, r2.xyxz  
  131:  add r1.xyz, -r0.xywx, r5.xyzx  
  132:  mad r0.xyz, r0.zzzz, r1.xyzx, r0.xywx  
  133: else  
  134:  mov r0.xyz, l(0, 0, 0, 0)  
  135: endif  
  136: mov o0.xyz, r0.xyzx  
  137: mov o0.w, l(1.000000)  
  138: ret 

老实说,着色器很长。对于有效的反向工程过程而言,可能太长了。

这是一个有雾的日落场景的示例:


让我们看一下输入:

至于纹理,我们有一个深度缓冲区,环境光遮挡和一个HDR颜色缓冲区。


入站深度缓冲区


传入环境光遮挡


传入的HDR颜色缓冲区

...以及在此场景中应用雾着色器的结果如下所示:


施加雾后的HDR纹理

深度缓冲区用于重新创建世界中的位置。这是Witcher 3着色器的标准模式,

具有环境光遮挡数据(如果启用)可使我们遮住雾。一个非常聪明的主意,也许是一个显而易见的主意,但我从未想到过。稍后我将回到这方面。

着色器从确定像素是否在天空中开始。如果像素位于天空中(深度== 1.0),则着色器将返回黑色。如果像素在场景中(深度<1.0),则我们使用深度缓冲区(7-11行)在世界上重新创建位置,然后继续计算雾。

延迟遮蔽过程后不久,雾就会通过。您可能会注意到与向前运行相关的某些元素尚不可用。在此特定场景中,应用了延迟的照明量,然后渲染Geralt的头发/脸部/眼睛。

关于“巫师3”中的雾,您需要了解的第一件事:它由两部分组成:“雾的颜色”和“大气的颜色”。

 struct FogResult  
 {  
    float4 paramsFog;     // RGB: color, A: influence  
    float4 paramsAerial;  // RGB: color, A: influence  
 };

每个部分都有三种颜色:正面,中间和背面。也就是说,在常量缓冲区中有诸如“ FogColorFront”,“ FogColorMiddle”,“ AerialColorBack”等数据。...让我们看一下传入的数据:


   // *** Inputs *** //  
   float3 FogSunDir = cb12_v38.xyz;  
   float3 FogColorFront = cb12_v39.xyz;  
   float3 FogColorMiddle = cb12_v40.xyz;  
   float3 FogColorBack = cb12_v41.xyz;  
     
   float4 FogBaseParams = cb12_v42;  
   float4 FogDensityParamsScene = cb12_v43;  
   float4 FogDensityParamsSky = cb12_v44;  
     
   float3 AerialColorFront = cb12_v45.xyz;  
   float3 AerialColorMiddle = cb12_v46.xyz;  
   float3 AerialColorBack = cb12_v47.xyz;  
   float4 AerialParams = cb12_v48;

在计算最终颜色之前,我们需要计算向量和标量积。着色器可以访问世界上的像素位置,相机位置(cb12 [0] .xyz)和雾/光照方向(cb12 [38] .xyz)。这使我们能够计算雾的形式和方向的向量的标量积。

   float3 frag_vec = fragPosWorldSpace.xyz - customCameraPos.xyz;  
   float frag_dist = length(frag_vec);  
     
   float3 frag_dir = frag_vec / frag_dist;  
   
   float dot_fragDirSunDir = dot(GlobalLightDirection.xyz, frag_dir);

要计算混合梯度,您需要使用绝对标量积的平方,然后再次将结果乘以取决于距离的某个参数:

   float3 curr_col_fog;  
   float3 curr_col_aerial;  
   {  
     float _dot = dot_fragDirSunDir;  
   
     float _dd = _dot;  
     {  
       const float _distOffset = -150;  
       const float _distRange = 500;  
       const float _mul = 1.0 / _distRange;  
       const float _bias = _distOffset * _mul;  
   
       _dd = abs(_dd);  
       _dd *= _dd;  
       _dd *= saturate( frag_dist * _mul + _bias );  
     }  
   
     curr_col_fog = lerp( FogColorMiddle.xyz, (_dot>0.0f ? FogColorFront.xyz : FogColorBack.xyz), _dd );  
     curr_col_aerial = lerp( AerialColorMiddle.xyz, (_dot>0.0f ? AerialColorFront.xyz : AerialColorBack.xyz), _dd );  
   }

该代码块使我们清楚地知道这0.002和-0.300的来源。正如我们所看到的,视图矢量和照明矢量之间的标量积负责“前”和“后”颜色之间的选择。聪明!

这是最终梯度(_dd)的可视化。


但是,计算大气/雾气的影响要复杂得多。如您所见,我们不仅提供rgb颜色,还有更多选择。它们包括例如场景密度。我们使用raymarching(16步,这就是可以扩展循环的原因)来确定雾的大小和比例因子:

有了向量[camera ---> world],我们可以将其所有分量划分为16-这将是raymarching的一步。如下所示,计算中仅涉及.z(高度)分量(curr_pos_z_step)。

例如,在此处了解有关通过射线行进实现的雾的更多信息

   float fog_amount = 1;  
   float fog_amount_scale = 0;  
   [branch]  
   if ( frag_dist >= AerialParams.y )  
   {  
     float curr_pos_z_base = (customCameraPos.z + FogBaseParams.y) * density_factor;  
     float curr_pos_z_step = frag_step.z * density_factor;  
   
     [unroll]  
     for ( int i=16; i>0; --i )  
     {  
       fog_amount *= 1 - saturate( density_sample_scale / (1 + max( 0.0, curr_pos_z_base + (i) * curr_pos_z_step ) ) );  
     }  
   
     fog_amount = 1 - fog_amount;  
     fog_amount_scale = saturate( (frag_dist - AerialParams.y) * AerialParams.z );  
   }  
   
   FogResult ret;  
   
   ret.paramsFog = float4 ( curr_col_fog, fog_amount_scale * pow( abs(fog_amount), final_exp_fog ) );  
   ret.paramsAerial = float4 ( curr_col_aerial, fog_amount_scale * pow( abs(fog_amount), final_exp_aerial ) );

雾的数量显然取决于高度(分量.z),最后雾的数量增加到雾/大气压的程度。

final_exp_fogfinal_exp_aerial是从常量缓冲区中获取的;它们使您可以控制雾和大气的颜色如何随着海拔的升高影响世界。

防雾


我发现的着色器没有以下汇编代码片段:

  109:  mad_sat r1.xy, r0.wwww, cb12[189].xzxx, cb12[189].ywyy  
  110:  add r5.xyz, -r3.xyzx, cb12[188].xyzx  
  111:  mad r5.xyz, r1.xxxx, r5.xyzx, r3.xyzx  
  112:  add r0.z, l(-1.000000), cb12[188].w  
  113:  mad r0.z, r1.y, r0.z, l(1.000000)  
  114:  mul_sat r5.w, r0.w, r0.z  
  115:  lt r0.z, l(0.000000), cb12[192].x  
  116:  if_nz r0.z  
  117:   mad_sat r1.xy, r0.wwww, cb12[191].xzxx, cb12[191].ywyy  
  118:   add r6.xyz, -r3.xyzx, cb12[190].xyzx  
  119:   mad r3.xyz, r1.xxxx, r6.xyzx, r3.xyzx  
  120:   add r0.z, l(-1.000000), cb12[190].w  
  121:   mad r0.z, r1.y, r0.z, l(1.000000)  
  122:   mul_sat r3.w, r0.w, r0.z  
  123:   add r1.xyzw, -r5.xyzw, r3.xyzw  
  124:   mad r5.xyzw, cb12[192].xxxx, r1.xyzw, r5.xyzw  
  125:  endif

从我的理解来看,这就像重新定义颜色和雾的效果一样:

大多数时候,仅执行一次重新定义(cb12_v192.x为0.0),但是在这种情况下,其值为〜0.22,因此我们进行第二次覆盖。


 #ifdef OVERRIDE_FOG  
     
   // Override  
   float fog_influence = ret.paramsFog.w; // r0.w  
   
   float override1ColorScale = cb12_v189.x;  
   float override1ColorBias = cb12_v189.y;  
   float3 override1Color = cb12_v188.rgb;  
     
   float override1InfluenceScale = cb12_v189.z;  
   float override1InfluenceBias = cb12_v189.w;  
   float override1Influence = cb12_v188.w;  
     
   float override1ColorAmount = saturate(fog_influence * override1ColorScale + override1ColorBias);  
   float override1InfluenceAmount = saturate(fog_influence * override1InfluenceScale + override1InfluenceBias);    
     

   float4 paramsFogOverride;  
   paramsFogOverride.rgb = lerp(curr_col_fog, override1Color, override1ColorAmount ); // ***r5.xyz   
     
   float param1 = lerp(1.0, override1Influence, override1InfluenceAmount); // r0.x  
   paramsFogOverride.w = saturate(param1 * fog_influence ); // ** r5.w  
   
     
   const float extraFogOverride = cb12_v192.x;  
     
   [branch]   
   if (extraFogOverride > 0.0)  
   {  
     float override2ColorScale = cb12_v191.x;  
     float override2ColorBias = cb12_v191.y;  
     float3 override2Color = cb12_v190.rgb;  
     
     float override2InfluenceScale = cb12_v191.z;  
     float override2InfluenceBias = cb12_v191.w;  
     float override2Influence = cb12_v190.w;  
       
     float override2ColorAmount = saturate(fog_influence * override2ColorScale + override2ColorBias);  
     float override2InfluenceAmount = saturate(fog_influence * override2InfluenceScale + override2InfluenceBias);  
      

     float4 paramsFogOverride2;  
     paramsFogOverride2.rgb = lerp(curr_col_fog, override2Color, override2ColorAmount); // r3.xyz   
           
     float ov_param1 = lerp(1.0, override2Influence, override2InfluenceAmount); // r0.z  
     paramsFogOverride2.w = saturate(ov_param1 * fog_influence); // r3.w  
   
     paramsFogOverride = lerp(paramsFogOverride, paramsFogOverride2, extraFogOverride);  
   
   }  
   ret.paramsFog = paramsFogOverride;  
     
 #endif

这是我们没有重新定义雾(第一张图片),一次重新定义(第二张图片)和两次重新定义(第三张图片,最终结果)的最终价格:




调节环境光遮挡


我发现的着色器也根本不使用环境光遮挡。让我们再次看一下AO的纹理和我们感兴趣的代码:


  13:  ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t2.xyzw  
  14:  max r0.x, r0.x, cb3[1].x  
  15:  add r0.yzw, r1.xxyz, -cb12[0].xxyz  
  16:  dp3 r1.x, r0.yzwy, r0.yzwy  
  17:  sqrt r1.x, r1.x  
  18:  add r1.y, r1.x, -cb3[0].x  
  19:  add r1.zw, -cb3[0].xxxz, cb3[0].yyyw  
  20:  div_sat r1.y, r1.y, r1.z  
  21:  mad r1.y, r1.y, r1.w, cb3[0].z  
  22:  add r0.x, r0.x, l(-1.000000)  
  23:  mad r0.x, r1.y, r0.x, l(1.000000)

也许这个场景不是最好的例子,因为我们在遥远的岛屿上看不到细节。但是,让我们看一下常量缓冲区,该缓冲区用于设置环境光遮挡值:


我们首先从纹理加载AO,然后执行max指令。在此场景中,cb3_v1.x非常高(0.96888),这使AO非常弱。

该代码的下一部分将计算照相机的位置与世界中像素之间的距离。

我相信代码有时可以说明一切,因此让我们看一下HLSL,它完成了大部分设置:

 float AdjustAmbientOcclusion(in float inputAO, in float worldToCameraDistance)  
 {  
   // *** Inputs *** //  
   const float aoDistanceStart = cb3_v0.x;  
   const float aoDistanceEnd = cb3_v0.y;  
   const float aoStrengthStart = cb3_v0.z;  
   const float aoStrengthEnd = cb3_v0.w;  
      
   // * Adjust AO  
   float aoDistanceIntensity = linstep( aoDistanceStart, aoDistanceEnd, worldToCameraDistance );  
   float aoStrength = lerp(aoStrengthStart, aoStrengthEnd, aoDistanceIntensity);   
   float adjustedAO = lerp(1.0, inputAO, aoStrength);  
     
   return adjustedAO;   
 }

所计算出的相机与世界之间的距离用于linstep函数。我们已经知道此功能,它出现在卷云着色器中。

如您所见,在常量缓冲区中,我们具有AO开始/结束距离值。linstep的输出会影响AO的强度(以及来自cbuffer的强度),而强度会影响AO的输出。

一个简单的例子:像素很远,例如,距离是

500。linstep返回1.0;
aoStrength等于aoStrengthEnd;

这会导致AO返回,大约是输入值的77%(最终力)。

此功能的传入AO之前已接受max操作。

放在一起


收到雾的颜色和气氛的颜色和效果后,您终于可以将它们组合起来了。

我们首先从产生的AO衰减效果开始:

   ...
   FogResult fog = CalculateFog( worldPos, CameraPosition, fogStart, ao, false );  
      
   // Apply AO to influence  
   fog.paramsFog.w *= ao;  
   fog.paramsAerial.w *= ao; 
      
   // Mix fog with scene color  
   outColor = ApplyFog(fog, colorHDR);

所有神奇的事情都发生ApplyFog函数中

 float3 ApplyFog(FogResult fog, float3 color)  
 {  
   const float3 LuminanceFactors = float3(0.333f, 0.555f, 0.222f);  
   
   float3 aerialColor = dot(LuminanceFactors, color) * fog.paramsAerial.xyz;  
   color = lerp(color, aerialColor, fog.paramsAerial.w);  
   color = lerp(color, fog.paramsFog.xyz, fog.paramsFog.w);  
    
   return color.xyz;  
 }

首先,我们计算像素的亮度:


然后我们将其乘以大气的颜色:


然后,我们将HDR颜色与气氛颜色结合起来:


最后一步是将中间结果与雾的颜色结合起来:


就这样!

一些调试屏幕截图



大气效应


大气色


雾效果


雾色


成品场景无雾


现成的场景只有雾


完成的场景只是主雾


现成的场景再次充满雾气,便于比较


我想您可以理解上面的许多内容,如果您看一下着色器,它就在这里

我可以高兴地说该着色器与原始着色器完全相同 -它使我非常高兴。

通常,最终结果高度依赖传递给着色器的值。这不是在输出中提供完美色彩的“神奇”解决方案;它需要大量迭代和美工才能使最终结果看起来不错。我认为这可能是一个漫长的过程,但是完成此过程之后,结果将非常令人信服,就像这个日落场景一样。

Witcher 3 Sky Shader还使用雾计算在地平线附近创建平滑的颜色过渡。但是,一组不同的密度系数将传递给天空着色器。

让我提醒您-大多数着色器不是我创建/分析的。所有确认应发送至CD PROJEKT RED。支持他们,他们做得很好。

第3部分。流星


在《巫师3》中,有一个小而有趣的细节-流星。有趣的是,它们似乎不在Blood and Wine DLC中。

在视频中,您可以看到它们的外观:


让我们看看我们如何获得这种效果。

如您所见,流星的身体比尾巴亮得多。这是一个重要属性,稍后我们将使用。

我们的议程非常熟悉:首先,我将描述常规属性,然后再讨论与几何相关的主题,最后,我们将转到发生最有趣的事情的像素着色器。

1.概述


简要描述正在发生的事情。

在天空,天空和月亮的穹顶之后,流星在主动通道中被吸引:



DrawIndexed(720)-天空的圆顶,
DrawIndexed(2160)-天空/月亮的球体,
DrawIndexed(36)-无关紧要,看起来像是太阳的平行六面体(?)
DrawIndexed(12)-流星
DrawIndexedInstanced(1116,1) -卷云

就像卷云一样,每个流星连续绘制两次。


在第一次抽奖电话之前


第一次抽奖的结果


第二次抽奖的结果

此外,与该游戏先发制人的通行证中的许多要素一样,使用了以下混合状态:


2.几何


在几何方面,首先要提到的是,每个流星都由一个带有texcoords的细四边形表示:4个顶点,6个索引。这是最简单的四边形。


一颗流星


的近似四分之一。您可以看到表示两个三角形的线的线框显示。

等一下,但是有DrawIndexed(12)这是否意味着我们同时画了两个流星?

是。


在此框架中,其中一颗流星完全在能见度金字塔之外。

让我们看一下顶点着色器的汇编代码:

 vs_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb1[9], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb12[193], immediateIndexed  
    dcl_input v0.xyz  
    dcl_input v1.xyzw  
    dcl_input v2.xy  
    dcl_input v3.xy  
    dcl_input v4.xy  
    dcl_input v5.xyz  
    dcl_input v6.x  
    dcl_input v7.x  
    dcl_output o0.xyzw  
    dcl_output o1.xyzw  
    dcl_output o2.xy  
    dcl_output o3.xyzw  
    dcl_output_siv o4.xyzw, position  
    dcl_temps 5  
   0: mov r0.xyz, v0.xyzx  
   1: mov r0.w, l(1.000000)  
   2: dp4 r1.x, r0.xyzw, cb2[0].xyzw  
   3: dp4 r1.y, r0.xyzw, cb2[1].xyzw  
   4: dp4 r1.z, r0.xyzw, cb2[2].xyzw  
   5: add r0.x, v2.x, v2.y  
   6: add r0.y, -v2.y, v2.x  
   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx  
  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx  
  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx  
  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000)  
  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  
  30: add r0.xyz, r0.xyzx, -cb12[0].xyzx  
  31: dp3 r0.w, r0.xyzx, r0.xyzx  
  32: sqrt r0.w, r0.w  
  33: div r0.xyz, r0.xyzx, r0.wwww  
  34: add r0.w, r0.w, -cb12[22].z  
  35: max r0.w, r0.w, l(0)  
  36: min r0.w, r0.w, cb12[42].z  
  37: dp3 r0.x, cb12[38].xyzx, r0.xyzx  
  38: mul r0.y, abs(r0.x), abs(r0.x)  
  39: mad_sat r1.x, r0.w, l(0.002000), l(-0.300000)  
  40: mul r0.y, r0.y, r1.x  
  41: lt r1.x, l(0), r0.x  
  42: movc r1.yzw, r1.xxxx, cb12[39].xxyz, cb12[41].xxyz  
  43: add r1.yzw, r1.yyzw, -cb12[40].xxyz  
  44: mad r1.yzw, r0.yyyy, r1.yyzw, cb12[40].xxyz  
  45: movc r2.xyz, r1.xxxx, cb12[45].xyzx, cb12[47].xyzx  
  46: add r2.xyz, r2.xyzx, -cb12[46].xyzx  
  47: mad o0.xyz, r0.yyyy, r2.xyzx, cb12[46].xyzx  
  48: ge r0.y, r0.w, cb12[48].y  
  49: if_nz r0.y  
  50:  mad r0.y, r0.z, cb12[22].z, cb12[0].z  
  51:  mul r0.z, r0.w, r0.z  
  52:  mul r0.z, r0.z, l(0.062500)  
  53:  mul r1.x, r0.w, cb12[43].x  
  54:  mul r1.x, r1.x, l(0.062500)  
  55:  add r0.x, r0.x, cb12[42].x  
  56:  add r2.x, cb12[42].x, l(1.000000)  
  57:  div_sat r0.x, r0.x, r2.x  
  58:  add r2.x, -cb12[43].z, cb12[43].y  
  59:  mad r0.x, r0.x, r2.x, cb12[43].z  
  60:  add r0.y, r0.y, cb12[42].y  
  61:  mul r2.x, r0.x, r0.y  
  62:  mul r0.z, r0.x, r0.z  
  63:  mad r3.xyzw, r0.zzzz, l(16.000000, 15.000000, 14.000000, 13.000000), r2.xxxx  
  64:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  65:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  66:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  67:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  68:  mul r2.y, r3.y, r3.x  
  69:  mul r2.y, r3.z, r2.y  
  70:  mul r2.y, r3.w, r2.y  
  71:  mad r3.xyzw, r0.zzzz, l(12.000000, 11.000000, 10.000000, 9.000000), r2.xxxx  
  72:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  73:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  74:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  75:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  76:  mul r2.y, r2.y, r3.x  
  77:  mul r2.y, r3.y, r2.y  
  78:  mul r2.y, r3.z, r2.y  
  79:  mul r2.y, r3.w, r2.y  
  80:  mad r3.xyzw, r0.zzzz, l(8.000000, 7.000000, 6.000000, 5.000000), r2.xxxx  
  81:  max r3.xyzw, r3.xyzw, l(0, 0, 0, 0)  
  82:  add r3.xyzw, r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  83:  div_sat r3.xyzw, r1.xxxx, r3.xyzw  
  84:  add r3.xyzw, -r3.xyzw, l(1.000000, 1.000000, 1.000000, 1.000000)  
  85:  mul r2.y, r2.y, r3.x  
  86:  mul r2.y, r3.y, r2.y  
  87:  mul r2.y, r3.z, r2.y  
  88:  mul r2.y, r3.w, r2.y  
  89:  mad r2.zw, r0.zzzz, l(0.000000, 0.000000, 4.000000, 3.000000), r2.xxxx  
  90:  max r2.zw, r2.zzzw, l(0, 0, 0, 0)  
  91:  add r2.zw, r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  92:  div_sat r2.zw, r1.xxxx, r2.zzzw  
  93:  add r2.zw, -r2.zzzw, l(0.000000, 0.000000, 1.000000, 1.000000)  
  94:  mul r2.y, r2.z, r2.y  
  95:  mul r2.y, r2.w, r2.y  
  96:  mad r2.x, r0.z, l(2.000000), r2.x  
  97:  max r2.x, r2.x, l(0)  
  98:  add r2.x, r2.x, l(1.000000)  
  99:  div_sat r2.x, r1.x, r2.x  
  100:  add r2.x, -r2.x, l(1.000000)  
  101:  mul r2.x, r2.x, r2.y  
  102:  mad r0.x, r0.y, r0.x, r0.z  
  103:  max r0.x, r0.x, l(0)  
  104:  add r0.x, r0.x, l(1.000000)  
  105:  div_sat r0.x, r1.x, r0.x  
  106:  add r0.x, -r0.x, l(1.000000)  
  107:  mad r0.x, -r2.x, r0.x, l(1.000000)  
  108:  add r0.y, r0.w, -cb12[48].y  
  109:  mul_sat r0.y, r0.y, cb12[48].z  
  110: else  
  111:  mov r0.xy, l(1.000000, 0.000000, 0.000000, 0.000000)  
  112: endif  
  113: log r0.x, r0.x  
  114: mul r0.z, r0.x, cb12[42].w  
  115: exp r0.z, r0.z  
  116: mul r0.z, r0.z, r0.y  
  117: mul r0.x, r0.x, cb12[48].x  
  118: exp r0.x, r0.x  
  119: mul o0.w, r0.x, r0.y  
  120: mad_sat r0.xy, r0.zzzz, cb12[189].xzxx, cb12[189].ywyy  
  121: add r2.xyz, -r1.yzwy, cb12[188].xyzx  
  122: mad r2.xyz, r0.xxxx, r2.xyzx, r1.yzwy  
  123: add r0.x, cb12[188].w, l(-1.000000)  
  124: mad r0.x, r0.y, r0.x, l(1.000000)  
  125: mul_sat r2.w, r0.x, r0.z  
  126: lt r0.x, l(0), cb12[192].x  
  127: if_nz r0.x  
  128:  mad_sat r0.xy, r0.zzzz, cb12[191].xzxx, cb12[191].ywyy  
  129:  add r3.xyz, -r1.yzwy, cb12[190].xyzx  
  130:  mad r1.xyz, r0.xxxx, r3.xyzx, r1.yzwy  
  131:  add r0.x, cb12[190].w, l(-1.000000)  
  132:  mad r0.x, r0.y, r0.x, l(1.000000)  
  133:  mul_sat r1.w, r0.x, r0.z  
  134:  add r0.xyzw, -r2.xyzw, r1.xyzw  
  135:  mad o1.xyzw, cb12[192].xxxx, r0.xyzw, r2.xyzw  
  136: else  
  137:  mov o1.xyzw, r2.xyzw  
  138: endif  
  139: mov o3.xyzw, v1.xyzw  
  140: mov o2.xy, v4.yxyy  
  141: ret

在这里,雾的计算可以立即引起注意(第30-138行)。出于性能原因,计算雾顶是有意义的。此外,我们不需要如此精确的雾-流星体通常飞过Geralt的头顶并且不会到达地平线。

大气参数(rgb =颜色,a =影响)存储在o0.xyzw中,雾参数存储在o1.xyzw中。

o2.xy(第140行)只是texcoords。
o3.xyzw(第139行)无关紧要。

现在让我们谈谈计算世界位置的几句话。顶点着色器执行广告牌。首先,广告牌的传入数据来自顶点缓冲区-让我们来看看它们。

第一个数据是排名:


如上所述,这里有2个四边形:8个顶点,12个索引。

但是为什么每个四边形的位置都相同?非常简单-这是四边形中心的位置。

此外,每个顶点从四边形的中心到边缘都有一个偏移量:


这意味着每个流星在世界空间中的大小为(400,3)个单位。(在Witcher 3中,在XY平面上,Z轴指向上)

每个顶点具有的最后一个元素是世界空间中控制流星运动的单位方向向量:


由于数据来自CPU,因此很难理解其计算方式。

现在让我们继续看广告牌代码。这个想法很简单-首先,您从四边形的中心到相机获取一个单位向量:

   7: add r2.xyz, -r1.zxyz, cb1[8].zxyz  
   8: dp3 r0.z, r2.xyzx, r2.xyzx  
   9: rsq r0.z, r0.z  
  10: mul r2.xyz, r0.zzzz, r2.xyzx

然后我们得到一个控制切星运动的单切向量。

假设此向量已在CPU端进行了标准化,则该标准化是多余的。

  11: dp3 r0.z, v5.xyzx, v5.xyzx  
  12: rsq r0.z, r0.z  
  13: mul r3.xyz, r0.zzzz, v5.xyzx

如果有两个向量,则使用向量乘积确定垂直于两个传入向量的双切向量。

  14: mul r4.xyz, r2.xyzx, r3.yzxy  
  15: mad r2.xyz, r2.zxyz, r3.zxyz, -r4.xyzx  
  16: dp3 r0.z, r2.xyzx, r2.xyzx  
  17: rsq r0.z, r0.z  
  18: mul r2.xyz, r0.zzzz, r2.xyzx

现在,我们已将切线(r3.xyz)和切线(r2.xyz)归一化

让我们介绍传入元素TEXCOORD1相对应XsizeYsize,例如(-200,1.50)。

对世界空间位置的最终计算如下:

  19: mad r0.z, v7.x, v6.x, l(1.000000)  
  20: mul r3.xyz, r0.zzzz, r3.xyzx  
  21: mul r3.xyz, r3.xyzx, v3.xxxx  
  22: mul r2.xyz, r2.xyzx, v3.yyyy  
  23: mad r0.xzw, r3.xxyz, r0.xxxx, r1.xxyz  
  24: mad r0.xyz, r2.xyzx, r0.yyyy, r0.xzwx  
  25: mov r0.w, l(1.000000) 

假设r0.x,r0.y和r0.z等于1.0,则简化了最终计算:

worldSpacePosition = quadCenter + tangent * Xsize + bitangent * Ysize

最后一部分是将世界空间中的位置与视图投影矩阵简单相乘以获得SV_Position:

  26: dp4 o4.x, r0.xyzw, cb1[0].xyzw  
  27: dp4 o4.y, r0.xyzw, cb1[1].xyzw  
  28: dp4 o4.z, r0.xyzw, cb1[2].xyzw  
  29: dp4 o4.w, r0.xyzw, cb1[3].xyzw  

3.像素着色器


如“概述”部分所述,使用以下混合状态: 其中SrcColorSrcAlpha分别是像素着色器中的.rgb和.a分量,而DestColor是当前在rendertarget中的.rgb颜色。 控制透明度的主要指标是SrcAlpha。许多主动型游戏着色器将其计算为不透明度,并最终将其应用如下: 坠星着色器也不例外。按照这种模式,我们考虑不透明度为1.0、0.1和0.0的三种情况

FinalColor = SrcColor * One + DestColor * (1.0 - SrcAlpha) =
FinalColor = SrcColor + DestColor * (1.0 - SrcAlpha)






return float4( color * opacity, opacity )



a) opacity = 1.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = color = SrcColor



b) opacity = 0.1

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = 0.1 * color + 0.9 * DestColor



c) opacity = 0.0

FinalColor = color * opacity + DestColor * (1.0 - opacity) =
FinalColor = DestColor



此着色器的基本思想是建模和使用不透明度函数opacity(x),该函数控制流星沿一个像素的不透明度。主要要求是不透明度应在恒星末端(其``身体'')达到最大值并平滑渐变为0.0(至其``尾巴'')。

当我们开始理解像素着色器的汇编代码时,这变得显而易见:

 ps_5_0  
    dcl_globalFlags refactoringAllowed  
    dcl_constantbuffer cb0[10], immediateIndexed  
    dcl_constantbuffer cb2[3], immediateIndexed  
    dcl_constantbuffer cb4[2], immediateIndexed  
    dcl_input_ps linear v0.xyzw  
    dcl_input_ps linear v1.xyzw  
    dcl_input_ps linear v2.y  
    dcl_input_ps linear v3.w  
    dcl_output o0.xyzw  
    dcl_temps 4  
   0: mov_sat r0.x, v2.y  
   1: ge r0.y, r0.x, l(0.052579)  
   2: ge r0.z, l(0.965679), r0.x  
   3: and r0.y, r0.z, r0.y  
   4: if_nz r0.y  
   5:  ge r0.y, l(0.878136), r0.x  
   6:  add r0.z, r0.x, l(-0.052579)  
   7:  mul r1.w, r0.z, l(1.211303)  
   8:  mov_sat r0.z, r1.w  
   9:  mad r0.w, r0.z, l(-2.000000), l(3.000000)  
  10:  mul r0.z, r0.z, r0.z  
  11:  mul r0.z, r0.z, r0.w  
  12:  mul r2.x, r0.z, l(0.084642)  
  13:  mov r1.yz, l(0.000000, 0.000000, 0.084642, 0.000000)  
  14:  movc r2.yzw, r0.yyyy, r1.yyzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  15:  not r0.z, r0.y  
  16:  if_z r0.y  
  17:   ge r0.y, l(0.924339), r0.x  
  18:   add r0.w, r0.x, l(-0.878136)  
  19:   mul r1.w, r0.w, l(21.643608)  
  20:   mov_sat r0.w, r1.w  
  21:   mad r3.x, r0.w, l(-2.000000), l(3.000000)  
  22:   mul r0.w, r0.w, r0.w  
  23:   mul r0.w, r0.w, r3.x  
  24:   mad r1.x, r0.w, l(0.889658), l(0.084642)  
  25:   mov r1.yz, l(0.000000, 0.084642, 0.974300, 0.000000)  
  26:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  27:  else  
  28:   mov r2.y, l(0)  
  29:   mov r0.y, l(-1)  
  30:  endif  
  31:  not r0.w, r0.y  
  32:  and r0.z, r0.w, r0.z  
  33:  if_nz r0.z  
  34:   ge r0.y, r0.x, l(0.924339)  
  35:   add r0.x, r0.x, l(-0.924339)  
  36:   mul r1.w, r0.x, l(24.189651)  
  37:   mov_sat r0.x, r1.w  
  38:   mad r0.z, r0.x, l(-2.000000), l(3.000000)  
  39:   mul r0.x, r0.x, r0.x  
  40:   mul r0.x, r0.x, r0.z  
  41:   mad r1.x, r0.x, l(-0.974300), l(0.974300)  
  42:   mov r1.yz, l(0.000000, 0.974300, 0.000000, 0.000000)  
  43:   movc r2.xyzw, r0.yyyy, r1.xyzw, r2.xyzw  
  44:  endif  
  45: else  
  46:  mov r2.yzw, l(0.000000, 0.000000, 0.000000, 0.500000)  
  47:  mov r0.y, l(0)  
  48: endif  
  49: mov_sat r2.w, r2.w  
  50: mad r0.x, r2.w, l(-2.000000), l(3.000000)  
  51: mul r0.z, r2.w, r2.w  
  52: mul r0.x, r0.z, r0.x  
  53: add r0.z, -r2.y, r2.z  
  54: mad r0.x, r0.x, r0.z, r2.y  
  55: movc r0.x, r0.y, r2.x, r0.x  
  56: mad r0.y, cb4[1].x, -cb0[9].w, l(1.000000)  
  57: mul_sat r0.y, r0.y, v3.w  
  58: mul r0.x, r0.y, r0.x  
  59: mul r0.yzw, cb2[2].xxyz, cb4[0].xxxx  
  60: mul r0.x, r0.x, cb2[2].w  
  61: dp3 r1.x, l(0.333000, 0.555000, 0.222000, 0.000000), r0.yzwy  
  62: mad r1.xyz, r1.xxxx, v0.xyzx, -r0.yzwy  
  63: mad r0.yzw, v0.wwww, r1.xxyz, r0.yyzw  
  64: add r1.xyz, -r0.yzwy, v1.xyzx  
  65: mad r0.yzw, v1.wwww, r1.xxyz, r0.yyzw  
  66: mul o0.xyz, r0.xxxx, r0.yzwy  
  67: mov o0.w, r0.x  
  68: ret

通常,着色器有点复杂,我很难弄清楚其中发生了什么。例如,所有值如1.211303、21.643608和24.189651都来自哪里?

如果我们在谈论不透明度函数,那么我们需要一个输入值。这非常简单-在[0,1](第0行)范围内的texcoord在这里很有用,因此我们可以将该功能应用于流星体的整个长度。

不透明度功能具有由四个控制点定义的三个段/间隔:

   // current status: no idea how these are generated  
   const float controlPoint0 = 0.052579;  
   const float controlPoint1 = 0.878136;  
   const float controlPoint2 = 0.924339;  
   const float controlPoint3 = 0.965679;

我不知道如何选择/计算它们。

从汇编代码中可以看到,第一个条件只是检查输入值是否在[controlPoint0-controlPoint3]范围内。如果不是,则不透明度仅为0.0。

   // Input for the opacity function
   float y = saturate(Input.Texcoords.y);  // r0.x
     
   // Value of opacity function.  
   // 0 - no change  
   // 1 - full color  
   float opacity = 0.0;  
     
   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
      ...

如果我们想了解不透明度函数的工作原理,需要对以下汇编代码进行解密

   6: add r0.z, r0.x, l(-0.052579)   
   7: mul r1.w, r0.z, l(1.211303)   
   8: mov_sat r0.z, r1.w   
   9: mad r0.w, r0.z, l(-2.000000), l(3.000000)   
  10: mul r0.z, r0.z, r0.z   
  11: mul r0.z, r0.z, r0.w   
  12: mul r2.x, r0.z, l(0.084642)

第9行的系数为“ -2.0”和“ 3.0”,这暗示着使用了stepsstep函数。是的,这是一个很好的猜测。

带有原型的HLSL平滑步函数:ret平滑步(最小,最大,x)始终将x限制 [ 最小-最大 ]。从汇编程序的角度来看,这会从输入值中减去最小值(即从第9行的r0.z中减去),但是代码中没有类似的东西。对于max,这意味着输入值的乘积,但是代码中没有像“ mul_sat”这样的内容。而是有“ mov_sat”。这告诉我们,平滑步长最小值最大值函数分别为0和1。

现在我们知道x必须在[0,1]之间。如上所述,不透明度函数中包含三个部分。这显然暗示代码正在寻找[segmentStart-segmentEnd]区间中的位置。

答案是Linstep函数!

 float linstep(float min, float max, float v)  
 {  
   return ( (v-min) / (max-min) );  
 }

例如,让我们来看第一段:[0.052579-0.878136]。减法在第6行。如果我们用乘法替换除法-> 1.0 /(0.878136-0.052579)= 1.0 / 0.825557 =〜1.211303。

平滑步的结果在[0,1]范围内。第12行的乘法是段的权重。每个段都有自己的权重,使您可以控制此特定段的最大不透明度。

这意味着对于第一段[0.052579-0.878136],不透明度在[0-0.084642]范围内。

计算任意段的不透明度的HLSL函数可以编写如下:

 float getOpacityFunctionValue(float x, float cpLeft, float cpRight, float weight)  
 {  
   float val = smoothstep( 0, 1, linstep(cpLeft, cpRight, x) );  
   return val * weight;  
 }

因此,重点仅在于为相应的段调用此函数。

看一下权重:

   const float weight0 = 0.084642;  
   const float weight1 = 0.889658;  
   const float weight2 = 0.974300; // note: weight0+weight1 = weight2

根据汇编代码,不透明度(x)函数的计算如下:

   float opacity = 0.0;

   [branch]   
   if (y >= controlPoint0 && y <= controlPoint3)  
   {  
     // Range of v: [0, weight0]  
     float v = getOpacityFunctionValue(y, controlPoint0, controlPoint1, weight0);  
     opacity = v;  
     
     [branch]  
     if ( y >= controlPoint1 )  
     {  
       // Range of v: [0, weight1]  
       float v = getOpacityFunctionValue(y, controlPoint1, controlPoint2, weight1);  
       opacity = weight0 + v;  
   
       [branch]  
       if (y >= controlPoint2)  
       {  
         // Range of v: [0, weight2]  
         float v = getOpacityFunctionValue(y, controlPoint2, controlPoint3, weight2);
         opacity = weight2 - v;          
       }  
     }  
   }

这是不透明度函数的图形。您可以轻松地看到不透明性急剧增加,表明流星的开始:


图形不透明度函数。

红色通道-不透明度值;
绿色通道-控制点;
蓝色通道-权重;


计算了不透明度后,其他所有内容都只是画龙点睛。然后还有其他倍增:星星的不透明度,流星的颜色和雾的影响。像在TW3着色器中一样,您还可以在这里找到1.0的冗余乘法:

   // cb4_v1.x = 1.0  
   float starsOpacity = 1.0 - cb0_v9.w * cb4_v1.x;    
   opacity *= starsOpacity;  

   // Calculate color of a shooting star  
   // cb4_v0.x = 10.0
   // cb2_v2.rgb = (1.0, 1.0, 1.0)
   float3 color = cb2_v2.rgb * cb4_v0.x;
     
   // cb2_v2.w = 1  
   opacity *= cb2_v2.w;
     
   FogResult fr = { Input.FogParams, Input.AerialParams };  
   color = ApplyFog(fr, color);
     
   return float4( color*opacity, opacity);  
 }

4.总结


主要困难在于具有不透明度功能的部分。解码后,其他所有内容都很容易理解。

我在上面说过,像素着色器有些复杂。实际上,我们只关心不透明度(x)函数的值,该值存储在r2.x中(从第49行开始)。但是,汇编代码中的不透明度函数会创建另外三个变量:minRange(r2.y),maxRange(r2.z)和value(r2.w)。当使用不透明度(x)时,所有这些参数都是用于计算不透明度的参数

lerp( minRange, maxRange, smoothstep(0, 1, value) );

实际上,如果输入值为x,则最终的不透明度值是在第55行的条件分支中获得的在[controlPoint0-controlPoint3]范围内,这意味着使用了不透明度功能,因此选择了r2.x。否则,当x在间隔之外,不透明度是根据r0.x计算的,即根据上面的公式。

我在间隔[controlPoint0-controlPoint3]之外调试了几个像素,最终的不透明度始终为零。

今天就这些。和往常一样,感谢您的阅读。

All Articles