翻译的第一部分在
这里 。 在这一部分中,我们将讨论锐度,平均亮度,月相和降雨期间大气现象的影响。
第6部分。
在这一部分中,我们将仔细研究The Witcher 3-Sharpen的另一个后期处理效果。
锐化使输出图像更锐利。 我们从Photoshop和其他图形编辑器中就知道了这种效果。
在《巫师3》中,锐化有两个选项:低和高。 我将在下面讨论它们之间的区别,但现在让我们看一下屏幕截图:
选项“低”-最高选项“低”-之后高选项-最高选项“高”-之后如果要查看更详细的(交互式)比较,请查看
《 Nvidia The Witcher 3性能指南》中的部分 。 如您所见,这种效果在草和树叶上尤为明显。
在这篇文章的这一部分中,我们将从游戏一开始就研究框架:我故意选择它,因为在这里我们看到了浮雕(长绘图距离)和天空穹顶。
在输入方面,锐化需要颜色缓冲区
t0 (色调校正和镜头光晕后的LDR)和深度缓冲区
t1 。
让我们检查一下像素着色器的汇编代码:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb3[3], immediateIndexed
dcl_constantbuffer cb12[23], 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_siv v0.xy, position
dcl_output o0.xyzw
dcl_temps 7
0: ftoi r0.xy, v0.xyxx
1: mov r0.zw, l(0, 0, 0, 0)
2: ld_indexable(texture2d)(float,float,float,float) r0.x, r0.xyzw, t1.xyzw
3: mad r0.x, r0.x, cb12[22].x, cb12[22].y
4: mad r0.y, r0.x, cb12[21].x, cb12[21].y
5: max r0.y, r0.y, l(0.000100)
6: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y
7: mad_sat r0.y, r0.y, cb3[1].z, cb3[1].w
8: add r0.z, -cb3[1].x, cb3[1].y
9: mad r0.y, r0.y, r0.z, cb3[1].x
10: add r0.y, r0.y, l(1.000000)
11: ge r0.x, r0.x, l(1.000000)
12: movc r0.x, r0.x, l(0), l(1.000000)
13: mul r0.z, r0.x, r0.y
14: round_z r1.xy, v0.xyxx
15: add r1.xy, r1.xyxx, l(0.500000, 0.500000, 0.000000, 0.000000)
16: div r1.xy, r1.xyxx, cb3[0].zwzz
17: sample_l(texture2d)(float,float,float,float) r2.xyz, r1.xyxx, t0.xyzw, s0, l(0)
18: lt r0.z, l(0), r0.z
19: if_nz r0.z
20: div r3.xy, l(0.500000, 0.500000, 0.000000, 0.000000), cb3[0].zwzz
21: add r0.zw, r1.xxxy, -r3.xxxy
22: sample_l(texture2d)(float,float,float,float) r4.xyz, r0.zwzz, t0.xyzw, s0, l(0)
23: mov r3.zw, -r3.xxxy
24: add r5.xyzw, r1.xyxy, r3.zyxw
25: sample_l(texture2d)(float,float,float,float) r6.xyz, r5.xyxx, t0.xyzw, s0, l(0)
26: add r4.xyz, r4.xyzx, r6.xyzx
27: sample_l(texture2d)(float,float,float,float) r5.xyz, r5.zwzz, t0.xyzw, s0, l(0)
28: add r4.xyz, r4.xyzx, r5.xyzx
29: add r0.zw, r1.xxxy, r3.xxxy
30: sample_l(texture2d)(float,float,float,float) r1.xyz, r0.zwzz, t0.xyzw, s0, l(0)
31: add r1.xyz, r1.xyzx, r4.xyzx
32: mul r3.xyz, r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000)
33: mad r1.xyz, -r1.xyzx, l(0.250000, 0.250000, 0.250000, 0.000000), r2.xyzx
34: max r0.z, abs(r1.z), abs(r1.y)
35: max r0.z, r0.z, abs(r1.x)
36: mad_sat r0.z, r0.z, cb3[2].x, cb3[2].y
37: mad r0.x, r0.y, r0.x, l(-1.000000)
38: mad r0.x, r0.z, r0.x, l(1.000000)
39: dp3 r0.y, l(0.212600, 0.715200, 0.072200, 0.000000), r2.xyzx
40: dp3 r0.z, l(0.212600, 0.715200, 0.072200, 0.000000), r3.xyzx
41: max r0.w, r0.y, l(0.000100)
42: div r1.xyz, r2.xyzx, r0.wwww
43: add r0.y, -r0.z, r0.y
44: mad r0.x, r0.x, r0.y, r0.z
45: max r0.x, r0.x, l(0)
46: mul r2.xyz, r0.xxxx, r1.xyzx
47: endif
48: mov o0.xyz, r2.xyzx
49: mov o0.w, l(1.000000)
50: ret
50行汇编程序代码看起来像是一个可行的任务。 让我们开始解决它。
提升价值创造
第一步是加载深度缓冲区(第1行)。 值得注意的是,“巫师3”使用的是倒置深度(1.0-接近,0.0-接近)。 如您所知,硬件深度是非线性关联的(
有关详细信息,请参阅
本文 )。
第3-6行提供了一种非常有趣的方式来将此硬件深度[1.0-0.0]与[near-far]值相关联(我们将它们设置在MatrixPerspectiveFov阶段)。 考虑常量缓冲区中的值:
具有“关闭”值0.2和“远”值5000,我们可以计算出cb12_v21.xy的值,如下所示:
cb12_v21.y = 1.0 / near
cb12_v21.x = - (1.0 / near) + (1.0 / near) * (near / far)
这段代码在TW3着色器中很常见,因此我认为它只是一个函数。
在获得“可见性金字塔的深度”之后,第7行使用比例尺/变形来创建插值系数(此处,我们使用
saturate将值限制为区间[0-1])。
cb3_v1.xy和cb3_v2.xy-这是近距离和远距离锐化效果的亮度。 我们称它们为SharpenNear和SharpenFar。 这是《巫师3》中此效果的“低”和“高”选项之间的唯一区别。
现在该使用所得比率了。 第8-9行
lerp(sharpenNear, sharpenFar, interpolationCoeff)
。 这是为了什么 由于这一点,我们在Geralt附近和远离他的地方获得了不同的亮度。 看一下:
也许这几乎没有引起注意,但是在这里我们根据距离对插值进行了插值,即靠近播放器的锐化亮度(2.177151),效果亮度非常远(1.91303)。 计算之后,我们将亮度加1.0(第10行)。 为什么需要这个? 假设上面显示的操作lerp给我们0.0。 添加1.0之后,我们自然会得到1.0,并且该值在锐化时不会影响像素。 在下面阅读有关此内容的更多信息。
在锐化时,我们不想影响天空。 这可以通过添加简单的条件检查来实现:
// sharpen
float fSkyboxTest = (fDepth >= 1.0) ? 0 : 1;
在《巫师3》中,天空的像素深度值为1.0,因此我们使用它来获得一种“二进制过滤器”(一个有趣的事实:在这种情况下,
步骤将无法正常工作)。
现在我们可以将插值的亮度乘以“天空滤镜”:
此乘法在第13行执行。
着色器代码示例:
// sharpen
float fSharpenAmount = fSharpenIntensity * fSkyboxTest;
像素采样中心
SV_Position有一个很重要的方面:
半像素偏移量 。 事实证明,就SV_Position.xy而言,左上角(0,0)的此像素的坐标不是(0,0),而是(0.5,0.5)。 哇!
这里我们要在像素的中心进行采样,因此让我们看一下第14-16行。 您可以用HLSL编写它们:
// .
// "" SV_Position.xy.
float2 uvCenter = trunc( Input.Position.xy );
// ,
uvCenter += float2(0.5, 0.5);
uvCenter /= g_Viewport.xy
然后,我们从texcoords“ uvCenter”中采样输入的颜色纹理。 不用担心,采样结果将与“常规”方法(SV_Position.xy / ViewportSize.xy)相同。
锐化或不锐化
是否使用锐化的决定取决于fSharpenAmount。
//
float3 colorCenter = TexColorBuffer.SampleLevel( samplerLinearClamp, uvCenter, 0 ).rgb;
//
float3 finalColor = colorCenter;
if ( fSharpenAmount > 0 )
{
// sharpening...
}
return float4( finalColor, 1 );
锐化
现在是时候看看算法本身的内部了。
本质上,它执行以下操作:
-在像素的四角对输入颜色纹理进行四倍采样,
-添加样本并计算平均值,
-计算“中心”和“ cornerAverage”之间的差,
-找到差异的最大绝对成分,
-更正最大值 腹肌 使用比例+偏差值的组件,
-使用最大值确定效果的大小。 腹肌 组成部分
-计算“ centerColor”和“ averageColor”的亮度值(亮度),
-将colorCenter划分为亮度
-根据效果的大小计算一个新的内插亮度值,
-将colorCenter乘以新的亮度值。
大量的工作,我很难弄清楚,因为我从未尝试过锐化滤镜。
让我们从采样模式开始。 如您在汇编代码中所见,执行了四个纹理读取。
最好以像素图像为例(艺术家的技术水平是
专家 )来展示:
着色器中的所有读取均使用双线性采样(D3D11_FILTER_MIN_MAG_LINEAR_MIP_POINT)。
从中心到每个角度的偏移量为(±0.5,±0.5),具体取决于角度。
看看如何在HLSL上实现? 让我们看看:
float2 uvCorner;
float2 uvOffset = float2( 0.5, 0.5 ) / g_Viewport.xy; // remember about division!
float3 colorCorners = 0;
//
// -0,5, -0.5
uvCorner = uvCenter - uvOffset;
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;
//
// +0.5, -0.5
uvCorner = uvCenter + float2(uvOffset.x, -uvOffset.y);
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;
//
// -0.5, +0.5
uvCorner = uvCenter + float2(-uvOffset.x, uvOffset.y);
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;
//
// +0.5, +0.5
uvCorner = uvCenter + uvOffset;
colorCorners += TexColorBuffer.SampleLevel( samplerLinearClamp, uvCorner, 0 ).rgb;
因此,现在所有四个样本都汇总在“ colorCorners”变量中。 让我们按照以下步骤操作:
//
float3 averageColorCorners = colorCorners / 4.0;
//
float3 diffColor = colorCenter - averageColorCorners;
// . . RGB-
float fDiffColorMaxComponent = max( abs(diffColor.x), max( abs(diffColor.y), abs(diffColor.z) ) );
//
float fDiffColorMaxComponentScaled = saturate( fDiffColorMaxComponent * sharpenLumScale + sharpenLumBias );
// .
// "1.0" - fSharpenIntensity 1.0.
float fPixelSharpenAmount = lerp(1.0, fSharpenAmount, fDiffColorMaxComponentScaled);
// "" .
float lumaCenter = dot( LUMINANCE_RGB, finalColor );
float lumaCornersAverage = dot( LUMINANCE_RGB, averageColorCorners );
// "centerColor"
float3 fColorBalanced = colorCenter / max( lumaCenter, 1e-4 );
//
float fPixelLuminance = lerp(lumaCornersAverage, lumaCenter, fPixelSharpenAmount);
//
finalColor = fColorBalanced * max(fPixelLuminance, 0.0);
}
return float4(finalColor, 1.0);
通过计算最大值来执行边缘识别。 腹肌 差异成分。 聪明的举动! 查看其可视化效果:
可视化最大差异绝对值。太好了 成品HLSL着色器可
在此处获得 。 抱歉,格式化效果很差。 您可以使用我的
HLSLexplorer程序并尝试该代码。
我可以高兴地说,上面的代码创建的代码与游戏中的代码相同!
总结一下:Witcher 3锐度着色器的编写非常好(请注意fPixelSharpenAmount大于1.0!这很有趣...)。 另外,改变效果亮度的主要方法是近/远物体的亮度。 在这个游戏中,它们不是常数。 我已经编译了几个值的示例:
骷髅:
| 在附近锐化 | 锐化远 | sharpenDistanceScale | 锐化距离偏差 | SharpenLumScale | 锐化 |
---|
低 |
高 | 2.0 | 1.8 | 0.025
| -0.25
| -13.33333
| 1.33333 |
Kaer Morhen:
| 在附近锐化
| 锐化远
| sharpenDistanceScale
| 锐化距离偏差
| SharpenLumScale
| 锐化
|
---|
低
| 0.57751
| 0.31303
| 0.06665
| -0.33256
| -1.0
| 2.0
|
高
| 2.17751
| 1.91303
| 0.06665
| -0.33256
| -1.0
| 2.0 |
第7部分。平均亮度
几乎在任何现代视频游戏中都可以找到计算当前帧的平均亮度的操作。 此值通常在以后用于眼睛适应和色调校正的效果(请参阅文章的
上半部分 )。 在简单的解决方案中,将亮度计算用于(例如)纹理512
2 ,然后计算其mip级别并应用后者。 这通常可行,但是极大地限制了可能性。 更复杂的解决方案使用计算着色器执行例如
并行约简 。
让我们找出CD Projekt Red团队如何在The Witcher 3中解决此问题。 在上一部分中,我已经检查了眼睛的色调校正和适应性,因此难题中唯一剩下的部分就是平均亮度。
首先,《巫师3》的平均亮度计算包括两遍。 为了清楚起见,我决定将它们分成几个部分,首先我们看一下第一遍-“亮度分布”(亮度直方图的计算)。
亮度分布
在任何帧分析器中都非常容易找到这两个遍。 这些是在执行眼睛适应之前按顺序进行的
Dispatch调用:
让我们看看此通行证的输入。 他需要两个纹理:
1)HDR彩色缓冲区,其大小缩小为1/4 x 1/4(例如,从1920x1080降至480x270),
2)全屏深度缓冲区
1/4 x 1/4 HDR彩色缓冲区。 请注意棘手的技巧-此缓冲区是较大缓冲区的一部分。 重用缓冲区是一种好习惯。全屏深度缓冲区为什么要缩小颜色缓冲区? 我认为这全都与性能有关。
至于此遍的输出,它是一个结构化的缓冲区。 256个元素,每个元素4个字节。
着色器在这里没有调试信息,因此假设它只是无符号int值的缓冲区。
重要提示:计算平均亮度的第一步是调用
ClearUnorderedAccessViewUint将结构化缓冲区的所有元素重置为零。
让我们研究计算着色器的汇编代码(这是我们整个分析中的第一个计算着色器!)
cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[3], immediateIndexed
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_uav_structured u0, 4
dcl_input vThreadGroupID.x
dcl_input vThreadIDInGroup.x
dcl_temps 6
dcl_tgsm_structured g0, 4, 256
dcl_thread_group 64, 1, 1
0: store_structured g0.x, vThreadIDInGroup.x, l(0), l(0)
1: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)
2: store_structured g0.x, r0.x, l(0), l(0)
3: store_structured g0.x, r0.y, l(0), l(0)
4: store_structured g0.x, r0.z, l(0), l(0)
5: sync_g_t
6: ftoi r1.x, cb0[2].z
7: mov r2.y, vThreadGroupID.x
8: mov r2.zw, l(0, 0, 0, 0)
9: mov r3.zw, l(0, 0, 0, 0)
10: mov r4.yw, l(0, 0, 0, 0)
11: mov r1.y, l(0)
12: loop
13: utof r1.z, r1.y
14: ge r1.z, r1.z, cb0[0].x
15: breakc_nz r1.z
16: iadd r2.x, r1.y, vThreadIDInGroup.x
17: utof r1.z, r2.x
18: lt r1.z, r1.z, cb0[0].x
19: if_nz r1.z
20: ld_indexable(texture2d)(float,float,float,float) r5.xyz, r2.xyzw, t0.xyzw
21: dp3 r1.z, r5.xyzx, l(0.212600, 0.715200, 0.072200, 0.000000)
22: imul null, r3.xy, r1.xxxx, r2.xyxx
23: ld_indexable(texture2d)(float,float,float,float) r1.w, r3.xyzw, t1.yzwx
24: eq r1.w, r1.w, cb0[2].w
25: and r1.w, r1.w, cb0[2].y
26: add r2.x, -r1.z, cb0[2].x
27: mad r1.z, r1.w, r2.x, r1.z
28: add r1.z, r1.z, l(1.000000)
29: log r1.z, r1.z
30: mul r1.z, r1.z, l(88.722839)
31: ftou r1.z, r1.z
32: umin r4.x, r1.z, l(255)
33: atomic_iadd g0, r4.xyxx, l(1)
34: endif
35: iadd r1.y, r1.y, l(64)
36: endloop
37: sync_g_t
38: ld_structured r1.x, vThreadIDInGroup.x, l(0), g0.xxxx
39: mov r4.z, vThreadIDInGroup.x
40: atomic_iadd u0, r4.zwzz, r1.x
41: ld_structured r1.x, r0.x, l(0), g0.xxxx
42: mov r0.w, l(0)
43: atomic_iadd u0, r0.xwxx, r1.x
44: ld_structured r0.x, r0.y, l(0), g0.xxxx
45: atomic_iadd u0, r0.ywyy, r0.x
46: ld_structured r0.x, r0.z, l(0), g0.xxxx
47: atomic_iadd u0, r0.zwzz, r0.x
48: ret
还有一个常量缓冲区:
我们已经知道第一个输入是HDR颜色缓冲区。 使用FullHD时,其分辨率为480x270。 让我们看一下Dispatch调用。
调度(270,1,1)-这意味着我们运行270个线程组。 简而言之,我们在颜色缓冲区的每一行运行一组线程。
每个线程组执行一行HDR颜色缓冲区现在我们有了这个上下文,让我们尝试找出着色器的功能。
每个线程组在X方向上具有64个线程(dcl_thread_group 64、1、1),以及共享内存,256个元素(每个元素4个字节)(dcl_tgsm_structured g0、4、256)。
请注意,在着色器中,我们使用
SV_GroupThreadID (vThreadIDInGroup.x)[0-63]和
SV_GroupID (vThreadGroupID.x)[0-269]。
1)我们首先为共享内存的所有元素分配零值。 由于总内存每个组包含256个元素和64个线程,因此可以通过一个简单的循环方便地完成此操作:
// - .
// 64 , 4 .
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = 0;
}
2)之后,我们使用GroupMemoryBarrierWithGroupSync(sync_g_t)设置障碍。 我们这样做是为了确保在进行下一步之前,组共享内存中的所有线程都重置为零。
3)现在我们正在执行一个循环,大致可以这样写:
// cb0_v0.x - . 1920x1080 1920/4 = 480;
float ViewportSizeX = cb0_v0.x;
[loop] for ( uint PositionX = 0; PositionX < ViewportSizeX; PositionX += 64 )
{
...
这是一个简单的for循环,增量为64(您已经知道为什么吗?)。
下一步是计算加载像素的位置。
让我们考虑一下。
对于Y坐标,我们可以使用SV_GroupID.x,因为我们启动了270个线程组。
对于X坐标,我们...可以利用当前的组流! 让我们尝试去做。
由于每个组中有64个线程,因此这种解决方案将绕过所有像素。
考虑线程组(0、0、0)。
-流(0,0,0)将处理像素(0,0),(64,0),(128,0),(192,0),(256,0),(320,0),(384, 0),(448,0)。
-流(1、0、0)将处理像素(1、0),(65、0),(129、0),(193、0),(257、0),(321、0),(385, 0),(449、0)...
-流(63,0,0)将处理像素(63,0),(127,0),(191,0),(255,0),(319,0),(383,0),(447, 0)
因此,将处理所有像素。
我们还需要确保不从颜色缓冲区外部加载像素:
// X. Y GroupID.
uint CurrentPixelPositionX = PositionX + threadID;
uint CurrentPixelPositionY = groupID;
if ( CurrentPixelPositionX < ViewportSizeX )
{
// HDR- .
// HDR- , .
uint2 colorPos = uint2(CurrentPixelPositionX, CurrentPixelPositionY);
float3 color = texture0.Load( int3(colorPos, 0) ).rgb;
float luma = dot(color, LUMA_RGB);
看吗 很简单!
我还计算了亮度(汇编代码的第21行)。
太好了,我们已经从彩色像素计算出了亮度。 下一步是加载(而不是采样!)相应的深度值。
但是这里存在一个问题,因为我们连接了全分辨率深度缓冲。 怎么办呢?
这非常简单,只需将colorPos乘以某个常数(cb0_v2.z)。 我们将HDR颜色缓冲区缩小了四倍。 因此该值为4!
const int iDepthTextureScale = (int) cb0_v2.z;
uint2 depthPos = iDepthTextureScale * colorPos;
float depth = texture1.Load( int3(depthPos, 0) ).x;
到目前为止一切顺利! 但是...我们到了24-25行...
24: eq r2.x, r2.x, cb0[2].w
25: and r2.x, r2.x, cb0[2].y
所以 首先,我们
对浮点
相等进行比较 ,其结果写在r2.x中,然后紧接着...什么? 按位
和 ?? 真的吗 对于浮点值? 到底是什么???
'eq + and'问题我只想说这对着色器来说是最难的部分。 我什至尝试了怪异的asint / asfloat组合...
如果使用稍微不同的方法? 让我们在HLSL中进行通常的浮点比较。
float DummyPS() : SV_Target0
{
float test = (cb0_v0.x == cb0_v0.y);
return test;
}
这是汇编代码的输出:
0: eq r0.x, cb0[0].y, cb0[0].x
1: and o0.x, r0.x, l(0x3f800000)
2: ret
有趣吧? 我没想到在这里看到“和”。
0x3f800000仅为1.0f ...这是合理的,因为如果比较成功,我们将得到1.0和0.0。
但是,如果我们用其他值“替换” 1.0怎么办? 例如,像这样:
float DummyPS() : SV_Target0
{
float test = (cb0_v0.x == cb0_v0.y) ? cb0_v0.z : 0.0;
return test;
}
我们得到以下结果:
0: eq r0.x, cb0[0].y, cb0[0].x
1: and o0.x, r0.x, cb0[0].z
2: ret
哈! 奏效了。 这只是HLSL编译器的魔力。 注意:如果将0.0替换为其他内容,则只会得到movc。
让我们回到计算着色器。 下一步是验证深度等于cb0_v2.w。 它始终等于0.0-换句话说,我们检查像素是否在远平面上(在天空中)。 如果是这样,那么我们给这个系数分配一个大约0.5的值(我在几帧上检查了一下)。
该计算出的系数用于在颜色的亮度和“天空”的亮度(cb0_v2.x值,通常大约等于0.0)之间进行插值。 我认为这对于控制天空在计算平均亮度中的重要性是必要的。 通常重要性降低。 非常聪明的主意。
// , ( ). , ,
// .
float value = (depth == cb0_v2.w) ? cb0_v2.y : 0.0;
// 'value' 0.0, lerp 'luma'. 'value'
// ( 0.50), luma . (cb0_v2.x 0.0).
float lumaOk = lerp( luma, cb0_v2.x, value );
由于我们有lumaOk,下一步是计算其自然对数以创建良好的分布。 但是,等等,假设lumaOk为0.0。 我们知道log(0)的值是不确定的,所以我们加1.0是因为log(1)= 0.0。
之后,我们将计算的对数缩放为128,以将其分布在256个像元中。 很聪明!
从这里开始,将采用此值88.722839。 这是
128 * (2)
。
这只是HLSL计算对数的方式。
HLSL汇编器代码中只有一个函数可以计算对数:
log ,其底数为2。
// , lumaOk 0.0.
// log(0) undefined
// log(1) = 0.
//
lumaOk = log(lumaOk + 1.0);
// 128
lumaOk *= 128;
最后,我们根据对数分布的亮度计算单元格的索引,并将1加到共享内存中的相应单元格。
// . Uint, 256 ,
// , .
uint uLuma = (uint) lumaOk;
uLuma = min(uLuma, 255);
// 1 .
InterlockedAdd( shared_data[uLuma], 1 );
下一步将再次设置障碍,以确保已处理该行中的所有像素。
最后一步是将共享内存中的值添加到结构化缓冲区中。 通过一个简单的循环以相同的方式完成此操作:
// ,
GroupMemoryBarrierWithGroupSync();
// .
[unroll] for (uint idx = 0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
uint data = shared_data[offset];
InterlockedAdd( g_buffer[offset], data );
}
在线程组中的所有64个线程填充公共数据之后,每个线程将4个值添加到输出缓冲区。
考虑输出缓冲区。 让我们考虑一下。 缓冲区中所有值的总和等于像素总数! (480x270 = 129,600)。 也就是说,我们知道有多少像素具有特定的亮度值。
如果您不熟悉计算着色器(例如我),那么一开始可能不清楚,因此请多读几遍文章,拿着纸和一支铅笔,并尝试了解构建此技术的概念。
仅此而已! 这就是巫师3如何计算亮度的直方图。 就个人而言,在编写此部分时我学到了很多东西。 祝贺CD Projekt Red的出色工作!
如果您对完整的HLSL着色器感兴趣,可以
在此处获得 。 我一直在努力使汇编代码尽可能地接近游戏,并且对我再次成功感到非常高兴!
平均亮度计算
这是《巫师3:狂猎》中中等亮度计算分析的第二部分。
在与另一个计算着色器进行战斗之前,让我们简单地重复上一部分的内容:我们使用缩小到1 / 4x1 / 4的HDR颜色缓冲区。 第一次通过后,我们获得了亮度直方图(256个无符号整数值的结构化缓冲区)。 我们计算了每个像素亮度的对数,将其分布在256个单元中,并将结构化缓冲区的相应值每个像素增加1。 因此,这256个单元中所有值的总和等于像素数。
第一次通过的输出示例。 有256个元素。例如,我们的全屏缓冲区的大小为1920x1080。 缩小后,第一遍使用480x270缓冲区。 缓冲区中所有256个值的总和将等于480 * 270 = 129600。
简短介绍之后,我们准备继续进行下一步:计算。
这次仅使用了一个线程组(调度(1、1、1、1))。
让我们看一下计算着色器的汇编代码:
cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[1], immediateIndexed
dcl_uav_structured u0, 4
dcl_uav_typed_texture2d (float,float,float,float) u1
dcl_input vThreadIDInGroup.x
dcl_temps 4
dcl_tgsm_structured g0, 4, 256
dcl_thread_group 64, 1, 1
0: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, vThreadIDInGroup.x, l(0), u0.xxxx
1: store_structured g0.x, vThreadIDInGroup.x, l(0), r0.x
2: iadd r0.xyz, vThreadIDInGroup.xxxx, l(64, 128, 192, 0)
3: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.w, r0.x, l(0), u0.xxxx
4: store_structured g0.x, r0.x, l(0), r0.w
5: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.y, l(0), u0.xxxx
6: store_structured g0.x, r0.y, l(0), r0.x
7: ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.x, r0.z, l(0), u0.xxxx
8: store_structured g0.x, r0.z, l(0), r0.x
9: sync_g_t
10: if_z vThreadIDInGroup.x
11: mul r0.x, cb0[0].y, cb0[0].x
12: ftou r0.x, r0.x
13: utof r0.y, r0.x
14: mul r0.yz, r0.yyyy, cb0[0].zzwz
15: ftoi r0.yz, r0.yyzy
16: iadd r0.x, r0.x, l(-1)
17: imax r0.y, r0.y, l(0)
18: imin r0.y, r0.x, r0.y
19: imax r0.z, r0.y, r0.z
20: imin r0.x, r0.x, r0.z
21: mov r1.z, l(-1)
22: mov r2.xyz, l(0, 0, 0, 0)
23: loop
24: breakc_nz r2.x
25: ld_structured r0.z, r2.z, l(0), g0.xxxx
26: iadd r3.x, r0.z, r2.y
27: ilt r0.z, r0.y, r3.x
28: iadd r3.y, r2.z, l(1)
29: mov r1.xy, r2.yzyy
30: mov r3.z, r2.x
31: movc r2.xyz, r0.zzzz, r1.zxyz, r3.zxyz
32: endloop
33: mov r0.w, l(-1)
34: mov r1.yz, r2.yyzy
35: mov r1.xw, l(0, 0, 0, 0)
36: loop
37: breakc_nz r1.x
38: ld_structured r2.x, r1.z, l(0), g0.xxxx
39: iadd r1.y, r1.y, r2.x
40: utof r2.x, r2.x
41: utof r2.w, r1.z
42: add r2.w, r2.w, l(0.500000)
43: mul r2.w, r2.w, l(0.011271)
44: exp r2.w, r2.w
45: add r2.w, r2.w, l(-1.000000)
46: mad r3.z, r2.x, r2.w, r1.w
47: ilt r2.x, r0.x, r1.y
48: iadd r2.w, -r2.y, r1.y
49: itof r2.w, r2.w
50: div r0.z, r3.z, r2.w
51: iadd r3.y, r1.z, l(1)
52: mov r0.y, r1.z
53: mov r3.w, r1.x
54: movc r1.xzw, r2.xxxx, r0.wwyz, r3.wwyz
55: endloop
56: store_uav_typed u1.xyzw, l(0, 0, 0, 0), r1.wwww
57: endif
58: ret
有一个常量缓冲区:
让我们快速看一下汇编代码:附加了两个UAV(u0:第一部分的输入缓冲区,u1:1x1 R32_FLOAT格式的输出纹理)。 我们还看到,每个组有64个线程,而4字节共享组内存有256个元素。
我们首先用来自输入缓冲区的数据填充共享内存。 我们有64个线程,因此您将必须执行与以前几乎相同的操作。
为了绝对确保已加载所有数据以进行进一步处理,在此之后,我们设置了障碍。
// - .
// 64 , 4
// .
[unroll] for (uint idx=0; idx < 4; idx++)
{
const uint offset = threadID + idx*64;
shared_data[ offset ] = g_buffer[offset];
}
// , ,
// .
GroupMemoryBarrierWithGroupSync();
所有计算仅在一个线程中执行,所有其他计算仅用于将缓冲区中的值加载到共享内存中。
“计算”流的索引为0。为什么? 从理论上讲,我们可以使用间隔[0-63]中的任何流,但是由于与0进行了比较,因此可以避免其他整数-整数比较(
ieq指令)。
该算法基于在操作中将要考虑的像素间隔的指示。
在第11行中,我们将width * height乘以得到像素总数,然后将它们乘以间隔[0.0f-1.0f]中的两个数字,以指示间隔的开始和结束。 进一步的限制用于确保
0 <= Start <= End <= totalPixels - 1
:
// 0.
[branch] if (threadID == 0)
{
//
uint totalPixels = cb0_v0.x * cb0_v0.y;
// (, , ),
// .
int pixelsToConsiderStart = totalPixels * cb0_v0.z;
int pixelsToConsiderEnd = totalPixels * cb0_v0.w;
int pixelsMinusOne = totalPixels - 1;
pixelsToConsiderStart = clamp( pixelsToConsiderStart, 0, pixelsMinusOne );
pixelsToConsiderEnd = clamp( pixelsToConsiderEnd, pixelsToConsiderStart, pixelsMinusOne );
如您所见,下面有两个循环。 它们(或它们的汇编代码)的问题在于,循环的末尾有奇怪的条件转换。 我很难重新创建它们。 还要看一下第21行。为什么会有“ -1”? 我会在下面解释一下。
第一个循环的任务是删除
pixelToConsiderStart并为我们提供其中存在
pixelToConsiderStart +1的缓冲单元的索引(以及先前单元中所有像素的数量)。
假设
pixelsToConsiderStart大约等于30,000,并且缓冲区中的“零”单元中有37,000个像素(这种情况发生在夜间的游戏中)。 因此,我们要开始分析像素“零”中存在的像素30001的亮度。 在这种情况下,我们立即退出循环,获得起始索引“ 0”和零废弃像素。
看一下HLSL代码:
//
int numProcessedPixels = 0;
// [0-255]
int lumaValue = 0;
//
bool bExitLoop = false;
// - "pixelsToConsiderStart" .
// lumaValue, .
[loop]
while (!bExitLoop)
{
// .
uint numPixels = shared_data[lumaValue];
// , lumaValue
int tempSum = numProcessedPixels + numPixels;
// , pixelsToConsiderStart, .
// , lumaValue.
// , pixelsToConsiderStart - "" , , .
[flatten]
if (tempSum > pixelsToConsiderStart)
{
bExitLoop = true;
}
else
{
numProcessedPixels = tempSum;
lumaValue++;
}
}
汇编代码第21行中的神秘数字“ -1”与用于循环执行的布尔条件相关联(我偶然发现了这一点)。
从
lumaValue单元和
lumaValue本身接收到像素数后,我们可以继续第二个循环。
第二个周期的任务是计算像素和平均亮度的影响。
我们从在第一个循环中计算出的
lumaValue开始。
float finalAvgLuminance = 0.0f;
//
uint numProcessedPixelStart = numProcessedPixels;
// - .
// , , lumaValue.
// [0-255], , , ,
// pixelsToConsiderEnd.
// .
bExitLoop = false;
[loop]
while (!bExitLoop)
{
// .
uint numPixels = shared_data[lumaValue];
//
numProcessedPixels += numPixels;
// , [0-255] (uint)
uint encodedLumaUint = lumaValue;
//
float numberOfPixelsWithCurrentLuma = numPixels;
// , [0-255] (float)
float encodedLumaFloat = encodedLumaUint;
在这一阶段,我们获得了以[0.0f-255.f]间隔编码的亮度值。
解码过程非常简单-您需要反转编码阶段的计算。
编码过程的简短重复:
float luma = dot( hdrPixelColor, float3(0.2126, 0.7152, 0.0722) );
...
float outLuma;
// log(0) undef, log(1) = 0
outLuma = luma + 1.0;
//
outLuma = log( outLuma );
// 128, log(1) * 128 = 0, log(2,71828) * 128 = 128, log(7,38905) * 128 = 256
outLuma = outLuma * 128
// uint
uint outLumaUint = min( (uint) outLuma, 255);
为了解码亮度,我们反转编码过程,例如:
// 0.5f ( , )
float fDecodedLuma = encodedLumaFloat + 0.5;
// :
// 128
fDecodedLuma /= 128.0;
// exp(x), log(x)
fDecodedLuma = exp(fDecodedLuma);
// 1.0
fDecodedLuma -= 1.0;
然后,我们将给定亮度的像素数乘以解码后的亮度,然后求和,直到进行
pixelToConsiderEnd像素处理,
从而计算出分布。
之后,我们将总效果除以分析的像素数。这是循环的其余部分(和着色器):完整的着色器在此处可用。它与我的HLSLexplorer程序完全兼容,没有HLSLexplorer程序,我将无法在The Witcher 3中有效地重新创建平均亮度计算(以及所有其他效果!)。总之,有几点想法。在计算平均亮度方面,很难重建此着色器。主要原因是:1)对循环执行过程进行奇怪的“挂起”检查,比我以前想象的要花费更多的时间。2)在RenderDoc(v。1.2)中调试此计算着色器时出现问题。//
float fCurrentLumaContribution = numberOfPixelsWithCurrentLuma * fDecodedLuma;
// () .
float tempTotalContribution = fCurrentLumaContribution + finalAvgLuminance;
[flatten]
if (numProcessedPixels > pixelsToConsiderEnd )
{
//
bExitLoop = true;
// , .
//
int diff = numProcessedPixels - numProcessedPixelStart;
//
finalAvgLuminance = tempTotalContribution / float(diff);
}
else
{
// lumaValue
finalAvgLuminance = tempTotalContribution;
lumaValue++;
}
}
//
g_avgLuminance[uint2(0,0)] = finalAvgLuminance;
尽管从索引0读取的结果给出了正确的值,但并未完全支持“ ld_structured_indexable”操作,而所有其他操作都返回零,这就是循环无限期继续的原因。尽管我无法实现与原始代码相同的汇编代码(有关差异,请参见下面的屏幕截图),但是使用RenderDoc,我可以将此着色器注入到管道中-结果是相同的!战斗的结果。左边是我的着色器,右边是原始的汇编代码。第8部分。月亮及其相位
在本文的第八部分中,我研究了《巫师3》中的月亮着色器(更具体地说,是《血与酒》的扩展部分)。月亮是夜空的重要元素,很难让人相信它,但是对我来说,在TW3夜间行走确实是一种享受。看看这个场景!在介绍像素着色器之前,我会先谈谈渲染的细微差别。从几何角度来看,月球只是一个球体(见下文),具有纹理坐标,法线和切线向量。顶点着色器计算世界空间中的位置,以及法线的归一化向量(与向量相乘)的两个点的切线和切线,再乘以世界矩阵。为了确保完全月球位于远程平面,字段和MinDepth MAXDEPTH结构D3D11_VIEWPORT分配值0.0(这是用于天空圆顶同一特技)。月亮在天空之后立即渲染。用来吸引月球的球体,嗯,我想,一切都可以继续进行。让我们看一下像素着色器:我从Blood and Wine中选择着色器的主要原因很简单-它更短。首先,我们计算偏移量以对纹理进行采样。cb0 [0] .w用作X轴的偏移量,通过此简单技巧,我们可以模拟月亮绕其轴的旋转。ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[1], immediateIndexed
dcl_constantbuffer cb2[3], immediateIndexed
dcl_constantbuffer cb12[267], immediateIndexed
dcl_sampler s0, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_input_ps linear v1.w
dcl_input_ps linear v2.xyzw
dcl_input_ps linear v3.xy
dcl_input_ps linear v4.xy
dcl_output o0.xyzw
dcl_temps 3
0: mov r0.x, -cb0[0].w
1: mov r0.y, l(0)
2: add r0.xy, r0.xyxx, v2.xyxx
3: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, r0.xyxx, t0.xyzw, s0
4: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
5: log r0.w, r0.w
6: mul r0.w, r0.w, l(2.200000)
7: exp r0.w, r0.w
8: add r0.xyz, r0.xyzx, r0.xyzx
9: dp3 r1.x, r0.xyzx, r0.xyzx
10: rsq r1.x, r1.x
11: mul r0.xyz, r0.xyzx, r1.xxxx
12: mul r1.xy, r0.yyyy, v3.xyxx
13: mad r0.xy, v4.xyxx, r0.xxxx, r1.xyxx
14: mad r0.xy, v2.zwzz, r0.zzzz, r0.xyxx
15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w
16: mul r0.z, r0.z, l(6.283185)
17: sincos r1.x, r2.x, r0.z
18: mov r2.y, r1.x
19: dp2_sat r0.x, r0.xyxx, r2.xyxx
20: mul r0.xyz, r0.xxxx, cb12[266].xyzx
21: mul r0.xyz, r0.xyzx, r0.wwww
22: mul r0.xyz, r0.xyzx, cb2[2].xyzx
23: add_sat r0.w, -v1.w, l(1.000000)
24: mul r0.w, r0.w, cb2[2].w
25: mul o0.xyz, r0.wwww, r0.xyzx
26: mov o0.w, l(0)
27: ret

来自常量缓冲区的值的示例。一个纹理(1024x512)附加为输入。法线贴图在RGB通道中进行编码,而月球表面的颜色在alpha通道中进行编码。聪明!纹理的Alpha通道是月球表面的颜色。纹理RGB通道是一个法线贴图。收到正确的纹理坐标后,我们对RGBA通道进行采样。我们需要解压缩法线贴图并执行表面颜色的伽玛校正。当前,可以这样编写HLSL着色器,例如:下一步是执行常规绑定,但仅在XY组件中。 (在“巫师3”中,Z轴朝上,纹理的整个Z通道为1.0)。我们可以这样进行:现在是时候使用该着色器中我最喜欢的部分了。再看15-16行:这个神秘的0.033864是什么?乍一看似乎没有任何意义,但如果我们计算与之相反的值,我们将得出大约29.53的价格,这等于宗教会议月份的持续时间。float4 MoonPS(in InputStruct IN) : SV_Target0
{
// Texcoords
float2 uvOffsets = float2(-cb0_v0.w, 0.0);
// texcoords
float2 uv = IN.param2.xy + uvOffsets;
//
float4 sampledTexture = texture0.Sample( sampler0, uv);
// - -
float moonColorTex = pow(sampledTexture.a, 2.2 );
// [0,1] [-1,1].
// : sampledTexture.xyz * 2.0 - 1.0
float3 sampledNormal = normalize((sampledTexture.xyz - 0.5) * 2);
//
float3 Tangent = IN.param4.xyz;
float3 Normal = float3(IN.param2.zw, IN.param3.w);
float3 Bitangent = IN.param3.xyz;
// TBN
float3x3 TBN = float3x3(Tangent, Bitangent, Normal);
// XY
// TBN float3x2: 3 , 2
float2 vNormal = mul(sampledNormal, (float3x2)TBN).xy;
15: mad r0.z, cb0[0].y, l(0.033864), cb0[0].w
16: mul r0.z, r0.z, l(6.283185)
在几天之内!这就是我所谓的关注细节!我们可以可靠地假定cb0 [0] .y是游戏过程中经过的天数。此处使用一个附加偏差,用作沿着纹理x轴的偏移。收到该系数后,我们将其乘以2 * Pi。然后使用sincos计算另一个2d向量。通过计算法向矢量和“月球”矢量之间的标量积,可以模拟月球的一个相位。查看不同月相的屏幕截图:// .
// days/29.53 + bias.
float phase = cb0_v0.y * (1.0 / SYNODIC_MONTH_LENGTH) + cb0_v0.w;
// 2*PI. , 29.53
// sin/cos.
phase *= TWOPI;
// .
float outSin = 0.0;
float outCos = 0.0;
sincos(phase, outSin, outCos);
//
float lunarPhase = saturate( dot(vNormal, float2(outCos, outSin)) );
最后一步是执行一系列乘法运算以计算最终颜色。您可能不明白为什么此着色器将alpha值0.0发送到输出。这是因为在启用混合的情况下渲染了月亮:// .
// cb12_v266.xyz , .
// (1.54, 2.82, 4.13)
float3 moonSurfaceGlowColor = cb12_v266.xyz;
float3 moonColor = lunarPhase * moonSurfaceGlowColor;
moonColor = moonColorTex * moonColor;
// cb_v2.xyz - , , , (1.0, 1.0, 1.0)
moonColor *= cb2_v2.xyz;
// , , . - .
// , ,
// .
float paramHorizon = saturate(1.0 - IN.param1.w);
paramHorizon *= cb2_v2.w;
moonColor *= paramHorizon;
//
return float4(moonColor, 0.0);
如果此着色器返回黑色,则可以使用此方法获得背景(天空)颜色。如果您对完整的着色器感兴趣,则可以在此处获取。它具有大的常量缓冲区,应该已经准备好注入RenderDoc而不是原始的着色器(只需将“ MoonPS”重命名为“ EditedShaderPS”)。最后:我想与您分享结果:左边是我的着色器,右边是游戏中的原始着色器。差异很小,不会影响结果。如您所见,此着色器很容易重新创建。第9部分。G缓冲区
在这一部分中,我将揭示The Witcher 3中gbuffer的一些细节。我们将假定您了解延迟着色的基础知识。简短的重复:推迟的想法不是立即计算所有完成的照明和阴影,而是将计算分为两个阶段。在第一个(几何过程)中,我们用表面数据(位置,法线,镜面颜色等)填充GBuffer,在第二个(光照过程)中,我们将所有内容组合起来并计算光照。延迟着色是一种非常流行的方法,因为它使您可以通过平铺延迟着色等技术在单个全屏通道中进行计算,从而大大提高了性能。简而言之,GBuffer是一组具有几何属性的纹理。为它创建正确的结构非常重要。以现实生活中的示例为例,您可以研究《孤岛危机3》渲染技术。简要介绍之后,让我们看一下《巫师3:血与酒》中的一个示例框架:ToussentBasic GBuffer 的众多酒店之一,包含DXGI_FORMAT_R8G8B8A8_UNORM格式的三个全屏渲染目标和DXGI_FORMAT_D24_UNORM_S8_UINT格式的depth +模具缓冲区。这是他们的屏幕截图:渲染目标0-RGB通道,表面颜色渲染目标0-Alpha通道。老实说,我不知道这些信息是什么。渲染目标1-RGB通道。在此记录间隔[0-1]中的法向矢量。渲染目标1-Alpha通道。看起来像反射率!渲染目标2-RGB通道。看起来像镜面的颜色!在此场景中,alpha通道为黑色(但稍后使用)。缓冲区深度。请注意,此处使用倒置深度。用于标记特定类型像素(例如,皮肤,植被等)的模具缓冲区不是整个GBuffer。照明通道还使用照明探针和其他缓冲区,但是在本文中我将不讨论它们。在继续本文的“主要”部分之前,我将给出一般性观察:一般观察
1)唯一要清除的缓冲区是深度/模板缓冲区。如果在一个好的帧分析器中分析上述纹理,您会感到有些惊讶,因为它们不使用“清除”调用,而是“深度” /“模具”除外。也就是说,实际上,RenderTarget1如下所示(请注意,远端平面上的“模糊”像素):这是一个简单而智能的优化。重要的一课:您需要在ClearRenderTargetView调用上花费资源,因此仅在必要时使用它们。2)倒深度-这是酷在许多 文章 已经 写的关于浮点深度缓冲的精确度。巫师3使用反向Z。对于具有较长渲染距离的开放世界游戏,这是自然的选择。切换到DirectX并不困难:a)通过写入“ 0”而不是“ 1”来清除深度缓冲区。在传统方法中,将far值“ 1”用于清除深度缓冲区。深度翻转后,新的“ distance”值变为0,因此您需要更改所有内容。b)在计算投影矩阵时交换近边界和远边界c)将深度检查从“较小”更改为“较大”。对于OpenGL,需要做更多的工作(请参见上述文章),但这是值得的。3)我们不保持自己在世界上的地位,是的,一切都如此简单。在照明的传递中,我们从深处重新建立了在世界上的位置。像素着色器
在这一部分中,我想确切显示向GBuffer提供表面数据的像素着色器。因此,现在我们已经知道如何存储颜色,法线和镜面反射。当然,一切都不如您想像的那么简单。像素着色器的问题在于它具有许多选项。它们的不同之处在于传递给它们的纹理数量以及从常量缓冲区(可能与描述材料的常量缓冲区)使用的参数数量不同。为了进行分析,我决定使用这个漂亮的桶:我们的英勇桶!请欢迎纹理:因此,我们有了反照率,法线贴图和镜面反射颜色。很标准的情况。在开始之前,请先谈谈几何输入:几何是通过位置,texcoords,法线和切线缓冲区传输的。顶点着色器至少输出texcoords,归一化切线/法线/切线向量到两个点,之前已与世界矩阵相乘。对于更复杂的材质(例如,具有两个扩散贴图或两个法线贴图),顶点着色器可以输出其他数据,但我想在此处显示一个简单示例。汇编代码中的像素着色器:着色器包含几个步骤。我将分别描述此着色器的每个主要部分。ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb4[3], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s13, mode_default
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_resource_texture2d (float,float,float,float) t13
dcl_input_ps linear v0.zw
dcl_input_ps linear v1.xyzw
dcl_input_ps linear v2.xyz
dcl_input_ps linear v3.xyz
dcl_input_ps_sgv v4.x, isfrontface
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_output o2.xyzw
dcl_temps 3
0: sample_indexable(texture2d)(float,float,float,float) r0.xyzw, v1.xyxx, t1.xyzw, s0
1: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t0.xyzw, s0
2: add r1.w, r1.y, r1.x
3: add r1.w, r1.z, r1.w
4: mul r2.x, r1.w, l(0.333300)
5: add r2.y, l(-1.000000), cb4[1].x
6: mul r2.y, r2.y, l(0.500000)
7: mov_sat r2.z, r2.y
8: mad r1.w, r1.w, l(-0.666600), l(1.000000)
9: mad r1.w, r2.z, r1.w, r2.x
10: mul r2.xzw, r1.xxyz, cb4[0].xxyz
11: mul_sat r2.xzw, r2.xxzw, l(1.500000, 0.000000, 1.500000, 1.500000)
12: mul_sat r1.w, abs(r2.y), r1.w
13: add r2.xyz, -r1.xyzx, r2.xzwx
14: mad r1.xyz, r1.wwww, r2.xyzx, r1.xyzx
15: max r1.w, r1.z, r1.y
16: max r1.w, r1.w, r1.x
17: lt r1.w, l(0.220000), r1.w
18: movc r1.w, r1.w, l(-0.300000), l(-0.150000)
19: mad r1.w, v0.z, r1.w, l(1.000000)
20: mul o0.xyz, r1.wwww, r1.xyzx
21: add r0.xyz, r0.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
22: add r0.xyz, r0.xyzx, r0.xyzx
23: mov r1.x, v0.w
24: mov r1.yz, v1.zzwz
25: mul r1.xyz, r0.yyyy, r1.xyzx
26: mad r1.xyz, v3.xyzx, r0.xxxx, r1.xyzx
27: mad r0.xyz, v2.xyzx, r0.zzzz, r1.xyzx
28: uge r1.x, l(0), v4.x
29: if_nz r1.x
30: dp3 r1.x, v2.xyzx, r0.xyzx
31: mul r1.xyz, r1.xxxx, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
34: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t2.xyzw, s0
35: max r1.w, r1.z, r1.y
36: max r1.w, r1.w, r1.x
37: lt r1.w, l(0.200000), r1.w
38: movc r2.xyz, r1.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
39: add r2.xyz, -r1.xyzx, r2.xyzx
40: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
41: lt r1.x, r0.w, l(0.330000)
42: mul r1.y, r0.w, l(0.950000)
43: movc r1.x, r1.x, r1.y, l(0.330000)
44: add r1.x, -r0.w, r1.x
45: mad o1.w, v0.z, r1.x, r0.w
46: dp3 r0.w, r0.xyzx, r0.xyzx
47: rsq r0.w, r0.w
48: mul r0.xyz, r0.wwww, r0.xyzx
49: max r0.w, abs(r0.y), abs(r0.x)
50: max r0.w, r0.w, abs(r0.z)
51: lt r1.xy, abs(r0.zyzz), r0.wwww
52: movc r1.yz, r1.yyyy, abs(r0.zzyz), abs(r0.zzxz)
53: movc r1.xy, r1.xxxx, r1.yzyy, abs(r0.yxyy)
54: lt r1.z, r1.y, r1.x
55: movc r1.xy, r1.zzzz, r1.xyxx, r1.yxyy
56: div r1.z, r1.y, r1.x
57: div r0.xyz, r0.xyzx, r0.wwww
58: sample_l(texture2d)(float,float,float,float) r0.w, r1.xzxx, t13.yzwx, s13, l(0)
59: mul r0.xyz, r0.wwww, r0.xyzx
60: mad o1.xyz, r0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000), l(0.500000, 0.500000, 0.500000, 0.000000)
61: mov o0.w, cb4[2].x
62: mov o2.w, l(0)
63: ret
但是首先,像往常一样-屏幕截图包含来自常量缓冲区的值:反照率
我们将从复杂的事情开始。不仅仅是“ OutputColor.rgb = Texture.Sample(uv).rgb”在对RGB颜色纹理进行采样(第1行)之后,接下来的14行被称为“饱和度降低缓冲区”。让我向您展示HLSL代码:对于大多数对象,此代码除了从纹理返回原始颜色外,什么也不做。这是通过相应的“ material cbuffer”值实现的。cb4_v1.x的值为1.0,返回的掩码为0.0,并返回来自lerp指令的输入颜色。但是,也有一些例外。我发现最大的desaturationFactor是4.0(从不小于1.0),并且desaturatedColorfloat3 albedoColorFilter( in float3 color, in float desaturationFactor, in float3 desaturationValue )
{
float sumColorComponents = color.r + color.g + color.b;
float averageColorComponentValue = 0.3333 * sumColorComponents;
float oneMinusAverageColorComponentValue = 1.0 - averageColorComponentValue;
float factor = 0.5 * (desaturationFactor - 1.0);
float avgColorComponent = lerp(averageColorComponentValue, oneMinusAverageColorComponentValue, saturate(factor));
float3 desaturatedColor = saturate(color * desaturationValue * 1.5);
float mask = saturate( avgColorComponent * abs(factor) );
float3 finalColor = lerp( color, desaturatedColor, mask );
return finalColor;
}
取决于材料。可以是(0.2,0.3,0.4); 没有严格的规则。当然,我无法抗拒在自己的DX11框架中实现此功能,这是所有desaturatedColor值均等于float3(0.25,0.3,0.45)的结果desaturationFactor = 1.0(无效)desaturationFactor = 2.0desaturationFactor = 3.0desaturationFactor = 4.0我确定这只是材料参数的应用,而不是在反照率部分的末尾执行。第15-20行添加了最后的接触:v0.z是顶点着色器的输出,它们为零。不要忘记它,因为v0.z将在以后使用两次。看起来这是某种系数,整个代码看起来有点暗淡反照率,但是由于v0.z为0,因此颜色保持不变。HLSL:关于RT0.a,我们可以看到,它是从材质常量缓冲区中获取的,但是由于着色器没有调试信息,因此很难说出它是什么。也许半透明?我们已经完成了第一个渲染目标!15: max r1.w, r1.z, r1.y
16: max r1.w, r1.w, r1.x
17: lt r1.w, l(0.220000), r1.w
18: movc r1.w, r1.w, l(-0.300000), l(-0.150000)
19: mad r1.w, v0.z, r1.w, l(1.000000)
20: mul o0.xyz, r1.wwww, r1.xyzx
/* ALBEDO */
// (?)
float3 albedoColor = albedoColorFilter( colorTex, cb4_v1.x, cb4_v0.rgb );
float albedoMaxComponent = getMaxComponent( albedoColor );
// ,
// "paramZ" 0
float paramZ = Input.out0.z; // , 0
// , 0.70 0.85
// lerp, .
float param = (albedoMaxComponent > 0.22) ? 0.70 : 0.85;
float mulParam = lerp(1, param, paramZ);
//
pout.RT0.rgb = albedoColor * mulParam;
pout.RT0.a = cb4_v2.x;
正常的
让我们首先打开法线贴图的包装,然后像往常一样绑定法线:到目前为止,不足为奇。看一下28-33行:我们可以大致如下编写它们:不知道编写是否正确。如果您知道这个数学运算是什么,请告诉我。我们看到像素着色器使用了SV_IsFrontFace。/* */
float3 sampledNormal = ((normalTex.xyz - 0.5) * 2);
// TBN
float3 Tangent = Input.TangentW.xyz;
float3 Normal = Input.NormalW.xyz;
float3 Bitangent;
Bitangent.x = Input.out0.w;
Bitangent.yz = Input.out1.zw;
// ; , , normal-tbn
// 'mad' 'mov'
Bitangent = saturate(Bitangent);
float3x3 TBN = float3x3(Tangent, Bitangent, Normal);
float3 normal = mul( sampledNormal, TBN );
28: uge r1.x, l(0), v4.x
29: if_nz r1.x
30: dp3 r1.x, v2.xyzx, r0.xyzx
31: mul r1.xyz, r1.xxxx, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
[branch] if (bIsFrontFace <= 0)
{
float cosTheta = dot(Input.NormalW, normal);
float3 invNormal = cosTheta * Input.NormalW;
normal = normal - 2*invNormal;
}
这是什么
该文档可以帮助您(我想写“ msdn”,但是...):确定三角形是否在看着相机。对于线和点,IsFrontFace为true。一个例外是从三角形绘制的线(线框模式),其设置IsFrontFace类似于在实体模式下栅格化三角形。可以通过几何体着色器进行写入,并通过像素着色器进行读取。
我想自己检查一下。实际上,这种效果仅在线框模式下才明显。我相信这段代码是线框模式下正确计算法线(并因此计算照明)所必需的。进行比较:启用/禁用此技巧的完成场景的帧颜色,以及启用/禁用此技巧的gbuffer [0-1]法线的纹理:现场色彩毫不费力色彩特技表演普通[0-1]绝招正常[0-1]的技巧您是否注意到GBuffer中的每个渲染目标都具有R8G8B8A8_UNORM格式?这意味着每个组件有256个可能的值。这足以存储法线吗?在Gbuffer中存储具有足够字节数的高质量法线是一个已知问题,但是幸运的是,可以学习许多 不同的 材料。也许有些人已经知道这里使用了什么技术。我必须说,在几何图形的整个过程中,插槽13附加了一个附加纹理...:哈!
巫师3使用一种称为“ 最佳拟合法线 ” 的技术。在这里,我将不进行详细解释(请参阅演示文稿)。它是Crytek在2009-2010年左右发明的,并且由于CryEngine具有开源功能,因此BFN也是开源的。BFN使法线纹理具有“粒状”外观。使用BFN缩放法线后,我们将其从[-1; 1]间隔重新编码为[0,1]。高光
让我们从第34行开始,对镜面纹理进行采样:如您所见,我们从Albedo那里知道有一个“变暗”滤镜:我们计算出最大的分量。值,然后计算“变暗”的颜色并使用原始镜面反射颜色进行插值,并从顶点着色器中获取参数...为0,因此在输出时,我们从纹理中获取颜色。HLSL:34: sample_indexable(texture2d)(float,float,float,float) r1.xyz, v1.xyxx, t2.xyzw, s0
35: max r1.w, r1.z, r1.y
36: max r1.w, r1.w, r1.x
37: lt r1.w, l(0.200000), r1.w
38: movc r2.xyz, r1.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
39: add r2.xyz, -r1.xyzx, r2.xyzx
40: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
/* SPECULAR */
float3 specularTex = texture2.Sample( samplerAnisoWrap, Texcoords ).rgb;
// , Albedo. . ,
// - "".
// paramZ 0,
// .
float specularMaxComponent = getMaxComponent( specularTex );
float3 specB = (specularMaxComponent > 0.2) ? specularTex : float3(0.12, 0.12, 0.12);
float3 finalSpec = lerp(specularTex, specB, paramZ);
pout.RT2.xyz = finalSpec;
反射率
我不知道这个名称是否适合此参数,因为我不知道它如何影响照明的通过。事实是输入法线贴图的Alpha通道包含其他数据:Alpha通道纹理“法线贴图”。汇编代码:向我们的老朋友问好-v0.z!其含义类似于反照率和镜面反射:41: lt r1.x, r0.w, l(0.330000)
42: mul r1.y, r0.w, l(0.950000)
43: movc r1.x, r1.x, r1.y, l(0.330000)
44: add r1.x, -r0.w, r1.x
45: mad o1.w, v0.z, r1.x, r0.w
/* REFLECTIVITY */
float reflectivity = normalTex.a;
float reflectivity2 = (reflectivity < 0.33) ? (reflectivity * 0.95) : 0.33;
float finalReflectivity = lerp(reflectivity, reflectivity2, paramZ);
pout.RT1.a = finalReflectivity;
太好了!
至此,对像素着色器的第一个版本进行了分析。这是我的着色器(左)与原始着色器(右)的比较:这些差异不会影响计算,因此我在这里的工作已经完成。像素着色器:反照率+普通选项
我决定再显示一个选项,现在仅使用反照率和法线贴图,而没有镜面纹理。汇编代码稍长一些:此选项与之前的选项之间的区别如下:a)第19行:插值参数v0.z乘以常量缓冲区中的cb4 [0] .x,但此乘积仅用于第19行的插值反照率。对于其他输出,使用v0.z的“正常”值。b)第54-55行:o2.w现在的设置条件是(cb4 [7] .x> 0.0)我们已经从亮度直方图的计算中识别出这种模式“某种比较-AND ”。可以这样写:c)34-42行:完全不同的镜面反射计算。ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb4[8], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s13, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t1
dcl_resource_texture2d (float,float,float,float) t13
dcl_input_ps linear v0.zw
dcl_input_ps linear v1.xyzw
dcl_input_ps linear v2.xyz
dcl_input_ps linear v3.xyz
dcl_input_ps_sgv v4.x, isfrontface
dcl_output o0.xyzw
dcl_output o1.xyzw
dcl_output o2.xyzw
dcl_temps 4
0: mul r0.x, v0.z, cb4[0].x
1: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, v1.xyxx, t1.xyzw, s0
2: sample_indexable(texture2d)(float,float,float,float) r0.yzw, v1.xyxx, t0.wxyz, s0
3: add r2.x, r0.z, r0.y
4: add r2.x, r0.w, r2.x
5: add r2.z, l(-1.000000), cb4[2].x
6: mul r2.yz, r2.xxzx, l(0.000000, 0.333300, 0.500000, 0.000000)
7: mov_sat r2.w, r2.z
8: mad r2.x, r2.x, l(-0.666600), l(1.000000)
9: mad r2.x, r2.w, r2.x, r2.y
10: mul r3.xyz, r0.yzwy, cb4[1].xyzx
11: mul_sat r3.xyz, r3.xyzx, l(1.500000, 1.500000, 1.500000, 0.000000)
12: mul_sat r2.x, abs(r2.z), r2.x
13: add r2.yzw, -r0.yyzw, r3.xxyz
14: mad r0.yzw, r2.xxxx, r2.yyzw, r0.yyzw
15: max r2.x, r0.w, r0.z
16: max r2.x, r0.y, r2.x
17: lt r2.x, l(0.220000), r2.x
18: movc r2.x, r2.x, l(-0.300000), l(-0.150000)
19: mad r0.x, r0.x, r2.x, l(1.000000)
20: mul o0.xyz, r0.xxxx, r0.yzwy
21: add r0.xyz, r1.xyzx, l(-0.500000, -0.500000, -0.500000, 0.000000)
22: add r0.xyz, r0.xyzx, r0.xyzx
23: mov r1.x, v0.w
24: mov r1.yz, v1.zzwz
25: mul r1.xyz, r0.yyyy, r1.xyzx
26: mad r0.xyw, v3.xyxz, r0.xxxx, r1.xyxz
27: mad r0.xyz, v2.xyzx, r0.zzzz, r0.xywx
28: uge r0.w, l(0), v4.x
29: if_nz r0.w
30: dp3 r0.w, v2.xyzx, r0.xyzx
31: mul r1.xyz, r0.wwww, v2.xyzx
32: mad r0.xyz, -r1.xyzx, l(2.000000, 2.000000, 2.000000, 0.000000), r0.xyzx
33: endif
34: add r0.w, -r1.w, l(1.000000)
35: log r1.xyz, cb4[3].xyzx
36: mul r1.xyz, r1.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
37: exp r1.xyz, r1.xyzx
38: mad r0.w, r0.w, cb4[4].x, cb4[5].x
39: mul_sat r1.xyz, r0.wwww, r1.xyzx
40: log r1.xyz, r1.xyzx
41: mul r1.xyz, r1.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)
42: exp r1.xyz, r1.xyzx
43: max r0.w, r1.z, r1.y
44: max r0.w, r0.w, r1.x
45: lt r0.w, l(0.200000), r0.w
46: movc r2.xyz, r0.wwww, r1.xyzx, l(0.120000, 0.120000, 0.120000, 0.000000)
47: add r2.xyz, -r1.xyzx, r2.xyzx
48: mad o2.xyz, v0.zzzz, r2.xyzx, r1.xyzx
49: lt r0.w, r1.w, l(0.330000)
50: mul r1.x, r1.w, l(0.950000)
51: movc r0.w, r0.w, r1.x, l(0.330000)
52: add r0.w, -r1.w, r0.w
53: mad o1.w, v0.z, r0.w, r1.w
54: lt r0.w, l(0), cb4[7].x
55: and o2.w, r0.w, l(0.064706)
56: dp3 r0.w, r0.xyzx, r0.xyzx
57: rsq r0.w, r0.w
58: mul r0.xyz, r0.wwww, r0.xyzx
59: max r0.w, abs(r0.y), abs(r0.x)
60: max r0.w, r0.w, abs(r0.z)
61: lt r1.xy, abs(r0.zyzz), r0.wwww
62: movc r1.yz, r1.yyyy, abs(r0.zzyz), abs(r0.zzxz)
63: movc r1.xy, r1.xxxx, r1.yzyy, abs(r0.yxyy)
64: lt r1.z, r1.y, r1.x
65: movc r1.xy, r1.zzzz, r1.xyxx, r1.yxyy
66: div r1.z, r1.y, r1.x
67: div r0.xyz, r0.xyzx, r0.wwww
68: sample_l(texture2d)(float,float,float,float) r0.w, r1.xzxx, t13.yzwx, s13, l(0)
69: mul r0.xyz, r0.wwww, r0.xyzx
70: mad o1.xyz, r0.xyzx, l(0.500000, 0.500000, 0.500000, 0.000000), l(0.500000, 0.500000, 0.500000, 0.000000)
71: mov o0.w, cb4[6].x
72: ret
pout.RT2.w = (cb4_v7.x > 0.0) ? (16.5/255.0) : 0.0;
这里没有镜面反射纹理。让我们看一下负责这部分的汇编代码:请注意,我们在这里使用了(1-反映能力)。幸运的是,用HLSL编写此代码非常简单:我要补充一点,在此版本中,包含材质数据的常量缓冲区会稍大。在这里,这些附加值用于模拟镜面反射颜色。着色器的其余部分与以前的版本相同。在WinMerge中无法显示72行汇编程序代码,因此请相信:我的代码与原始代码几乎相同。或者,您可以下载我的HLSLexplorer并亲自查看!34: add r0.w, -r1.w, l(1.000000)
35: log r1.xyz, cb4[3].xyzx
36: mul r1.xyz, r1.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
37: exp r1.xyz, r1.xyzx
38: mad r0.w, r0.w, cb4[4].x, cb4[5].x
39: mul_sat r1.xyz, r0.wwww, r1.xyzx
40: log r1.xyz, r1.xyzx
41: mul r1.xyz, r1.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000)
42: exp r1.xyz, r1.xyzx
float oneMinusReflectivity = 1.0 - normalTex.a;
float3 specularTex = pow(cb4_v3.rgb, 2.2);
oneMinusReflectivity = oneMinusReflectivity * cb4_v4.x + cb4_v5.x;
specularTex = saturate(specularTex * oneMinusReflectivity);
specularTex = pow(specularTex, 1.0/2.2);
// ...
float specularMaxComponent = getMaxComponent( specularTex );
...
总结一下
...如果您在这里阅读过,那么您可能想更深入一点。在现实生活中看起来简单的事情通常并非如此,并且将数据传输到gbuffer Witcher 3也不例外。我仅向您展示了负责此功能的最简单版本的像素着色器,还提供了与一般延迟着色有关的常规观察结果。对于大多数患者,pastebin中的像素着色器有两个选项:选项1-具有镜面纹理选项2-不具有镜面纹理第10部分。远方的雨帘
在这一部分中,我们将看到我真正喜欢的一种奇妙的大气效果- 地平线附近的远处雨/ 光幕。在游戏中,他们最容易在斯凯利格群岛见面。就个人而言,我真的很喜欢这种大气现象,并且很好奇CD Projekt Red图形程序员是如何实现的。让我们弄清楚!这是应用雨帘前后的两个屏幕截图:到雨帘雨后的窗帘几何形状
首先,我们将重点放在几何上。这个想法是使用一个小圆柱体:从局部空间中的位置来看,圆柱体非常小-位置在(0.0-1.0)范围内。此绘图调用的输入电路如下所示:以下对我们而言很重要:Texcoords和Instance_Transform。Texcoords的包装非常简单:上,下两个碱基的U在[0.02777-1.02734]区间内。较低基数的V为1.0,较高基数的V为0.0。如您所见,您甚至可以在过程上非常简单地创建此网格。在本地空间中收到了这个小圆柱体后,我们将其乘以为输入元素INSTANCE_TRANSFORM的每个实例提供的世界矩阵。让我们检查一下这个矩阵的值:看起来很吓人吧?但请放心,我们将分析此矩阵并查看其隐藏的内容!结果非常有趣:了解相机在此特定帧中的位置很重要:(-116.5338、234.8695、2.09)如您所见,我们缩放了圆柱体以使其在世界空间中相当大(在TW3中Z轴朝上),相对于相机位置移动了圆柱体,然后转身。使用顶点着色器进行转换后,圆柱体的外观如下:XMMATRIX mat( -227.7472, 159.8043, 374.0736, -116.4951,
-194.7577, -173.3836, -494.4982, 238.6908,
-14.16466, -185.4743, 784.564, -1.45565,
0.0, 0.0, 0.0, 1.0 );
mat = XMMatrixTranspose( mat );
XMVECTOR vScale;
XMVECTOR vRotateQuat;
XMVECTOR vTranslation;
XMMatrixDecompose( &vScale, &vRotateQuat, &vTranslation, mat );
// ...
XMMATRIX matRotate = XMMatrixRotationQuaternion( vRotateQuat );
vRotateQuat: (0.0924987569, -0.314900011, 0.883411944, -0.334462732)
vScale: (299.999969, 300.000000, 1000.00012)
vTranslation: (-116.495102, 238.690796, -1.45564997)
顶点着色器转换后的圆柱体。查看它相对于能见度金字塔的位置。顶点着色器
输入几何图形和顶点着色器严格相互依赖。让我们仔细看一下顶点着色器的汇编代码:连同简单传递的Texcoords(第0行)和Instance_LOD_Params(第8行)一起,还需要两个元素来输出:SV_Position(这很明显)和Height(组件.z)在世界上的位置。还记得本地空间在[0-1]范围内吗?因此,在应用世界矩阵之前,顶点着色器将使用比例和偏差来更改局部位置。聪明的举动!在这种情况下,scale = float3(4,4,2),bias = float3(-2,-2,-1)< 第9和28行之间值得注意的模式是两个行主矩阵的乘积。让我们看一下HLSL上完成的顶点着色器:vs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb1[7], immediateIndexed
dcl_constantbuffer cb2[6], immediateIndexed
dcl_input v0.xyz
dcl_input v1.xy
dcl_input v4.xyzw
dcl_input v5.xyzw
dcl_input v6.xyzw
dcl_input v7.xyzw
dcl_output o0.xyz
dcl_output o1.xyzw
dcl_output_siv o2.xyzw, position
dcl_temps 2
0: mov o0.xy, v1.xyxx
1: mul r0.xyzw, v5.xyzw, cb1[6].yyyy
2: mad r0.xyzw, v4.xyzw, cb1[6].xxxx, r0.xyzw
3: mad r0.xyzw, v6.xyzw, cb1[6].zzzz, r0.xyzw
4: mad r0.xyzw, cb1[6].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
5: mad r1.xyz, v0.xyzx, cb2[4].xyzx, cb2[5].xyzx
6: mov r1.w, l(1.000000)
7: dp4 o0.z, r1.xyzw, r0.xyzw
8: mov o1.xyzw, v7.xyzw
9: mul r0.xyzw, v5.xyzw, cb1[0].yyyy
10: mad r0.xyzw, v4.xyzw, cb1[0].xxxx, r0.xyzw
11: mad r0.xyzw, v6.xyzw, cb1[0].zzzz, r0.xyzw
12: mad r0.xyzw, cb1[0].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
13: dp4 o2.x, r1.xyzw, r0.xyzw
14: mul r0.xyzw, v5.xyzw, cb1[1].yyyy
15: mad r0.xyzw, v4.xyzw, cb1[1].xxxx, r0.xyzw
16: mad r0.xyzw, v6.xyzw, cb1[1].zzzz, r0.xyzw
17: mad r0.xyzw, cb1[1].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
18: dp4 o2.y, r1.xyzw, r0.xyzw
19: mul r0.xyzw, v5.xyzw, cb1[2].yyyy
20: mad r0.xyzw, v4.xyzw, cb1[2].xxxx, r0.xyzw
21: mad r0.xyzw, v6.xyzw, cb1[2].zzzz, r0.xyzw
22: mad r0.xyzw, cb1[2].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
23: dp4 o2.z, r1.xyzw, r0.xyzw
24: mul r0.xyzw, v5.xyzw, cb1[3].yyyy
25: mad r0.xyzw, v4.xyzw, cb1[3].xxxx, r0.xyzw
26: mad r0.xyzw, v6.xyzw, cb1[3].zzzz, r0.xyzw
27: mad r0.xyzw, cb1[3].wwww, l(0.000000, 0.000000, 0.000000, 1.000000), r0.xyzw
28: dp4 o2.w, r1.xyzw, r0.xyzw
29: ret
cbuffer cbPerFrame : register (b1)
{
row_major float4x4 g_viewProjMatrix;
row_major float4x4 g_rainShaftsViewProjMatrix;
}
cbuffer cbPerObject : register (b2)
{
float4x4 g_mtxWorld;
float4 g_modelScale;
float4 g_modelBias;
}
struct VS_INPUT
{
float3 PositionW : POSITION;
float2 Texcoord : TEXCOORD;
float3 NormalW : NORMAL;
float3 TangentW : TANGENT;
float4 InstanceTransform0 : INSTANCE_TRANSFORM0;
float4 InstanceTransform1 : INSTANCE_TRANSFORM1;
float4 InstanceTransform2 : INSTANCE_TRANSFORM2;
float4 InstanceLODParams : INSTANCE_LOD_PARAMS;
};
struct VS_OUTPUT
{
float3 TexcoordAndZ : Texcoord0;
float4 LODParams : LODParams;
float4 PositionH : SV_Position;
};
VS_OUTPUT RainShaftsVS( VS_INPUT Input )
{
VS_OUTPUT Output = (VS_OUTPUT)0;
//
Output.TexcoordAndZ.xy = Input.Texcoord;
Output.LODParams = Input.InstanceLODParams;
//
float3 meshScale = g_modelScale.xyz; // float3( 4, 4, 2 );
float3 meshBias = g_modelBias.xyz; // float3( -2, -2, -1 );
float3 PositionL = Input.PositionW * meshScale + meshBias;
// instanceWorld float4s:
float4x4 matInstanceWorld = float4x4(Input.InstanceTransform0, Input.InstanceTransform1,
Input.InstanceTransform2 , float4(0, 0, 0, 1) );
// (.z)
float4x4 matWorldInstanceLod = mul( g_rainShaftsViewProjMatrix, matInstanceWorld );
Output.TexcoordAndZ.z = mul( float4(PositionL, 1.0), transpose(matWorldInstanceLod) ).z;
// SV_Posiiton
float4x4 matModelViewProjection = mul(g_viewProjMatrix, matInstanceWorld );
Output.PositionH = mul( float4(PositionL, 1.0), transpose(matModelViewProjection) );
return Output;
}
我的着色器(左)和原始着色器(右)的比较:差异不影响计算。我将着色器注入框架,一切仍然很好!像素着色器
终于!
首先,我将向您展示输入:此处使用两个纹理:噪声纹理和深度缓冲区:来自常量缓冲区的值:以及像素着色器的汇编代码:ps_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer cb0[8], immediateIndexed
dcl_constantbuffer cb2[3], immediateIndexed
dcl_constantbuffer cb12[23], immediateIndexed
dcl_constantbuffer cb4[8], immediateIndexed
dcl_sampler s0, mode_default
dcl_sampler s15, mode_default
dcl_resource_texture2d (float,float,float,float) t0
dcl_resource_texture2d (float,float,float,float) t15
dcl_input_ps linear v0.xyz
dcl_input_ps linear v1.w
dcl_input_ps_siv v2.xy, position
dcl_output o0.xyzw
dcl_temps 1
0: mul r0.xy, cb0[0].xxxx, cb4[5].xyxx
1: mad r0.xy, v0.xyxx, cb4[4].xyxx, r0.xyxx
2: sample_indexable(texture2d)(float,float,float,float) r0.x, r0.xyxx, t0.xyzw, s0
3: add r0.y, -cb4[2].x, cb4[3].x
4: mad_sat r0.x, r0.x, r0.y, cb4[2].x
5: mul r0.x, r0.x, v0.y
6: mul r0.x, r0.x, v1.w
7: mul r0.x, r0.x, cb4[1].x
8: mul r0.yz, v2.xxyx, cb0[1].zzwz
9: sample_l(texture2d)(float,float,float,float) r0.y, r0.yzyy, t15.yxzw, s15, l(0)
10: mad r0.y, r0.y, cb12[22].x, cb12[22].y
11: mad r0.y, r0.y, cb12[21].x, cb12[21].y
12: max r0.y, r0.y, l(0.000100)
13: div r0.y, l(1.000000, 1.000000, 1.000000, 1.000000), r0.y
14: add r0.y, r0.y, -v0.z
15: mul_sat r0.y, r0.y, cb4[6].x
16: mul_sat r0.x, r0.y, r0.x
17: mad r0.y, cb0[7].y, r0.x, -r0.x
18: mad r0.x, cb4[7].x, r0.y, r0.x
19: mul r0.xyz, r0.xxxx, cb4[0].xyzx
20: log r0.xyz, r0.xyzx
21: mul r0.xyz, r0.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000)
22: exp r0.xyz, r0.xyzx
23: mul r0.xyz, r0.xyzx, cb2[2].xyzx
24: mul o0.xyz, r0.xyzx, cb2[2].wwww
25: mov o0.w, l(0)
26: ret
哇!
数量很多,但实际上,一切还不错。这是怎么回事 首先,我们使用从cbuffer(cb0 [0] .x)开始的时间和缩放/偏移量来计算动画UV。这些texcoords用于从噪声纹理中采样(第2行)。从纹理接收到噪声值后,我们在最小值/最大值之间进行插值(通常为0和1)。然后,我们通过乘以纹理V的坐标(请记住V坐标从1到0?)进行乘法(第5行)。因此,我们计算了“亮度蒙版”,它看起来像这样:请注意,远处的物体(灯塔,山脉...)已消失。发生这种情况是因为圆柱体通过了深度测试-圆柱体不在远平面上,而是绘制在以下对象的顶部:深度测试我们想模拟雨幕更远(但不一定在远平面上)。为此,我们计算了另一个蒙版,即“远处对象的蒙版”。它是通过以下公式计算的:(farObjectsMask = saturate( (FrustumDepth - CylinderWorldSpaceHeight) * 0.001 );
从缓冲区中提取0.001),这为我们提供了所需的掩码:(在有关“锐化”效果的部分中,我已经简要地解释了如何从深度缓冲区中提取可见性金字塔的深度。)就我个人而言,如果不通过将可见性金字塔的深度乘以较小的数字来计算世界空间中的高度,就可以实现这种效果的成本更低。 0.0004。当两个遮罩相乘时,获得最后一个:收到此最终蒙版(第16行)后,我们执行另一次插值,该操作几乎不执行任何操作(至少在测试情况下如此),然后将最终蒙版乘以窗帘的颜色(第19行),执行伽玛校正(第20行) -22)和最终乘法(23-24)。最后,我们返回Alpha值为零的颜色。这是因为在此过程中启用了混合:FinalColor = SourceColor * 1.0 + (1.0 - SourceAlpha) * DestColor
如果您不太了解混合的工作方式,则简要说明一下:SourceColor是像素着色器的RGB输出,DestColor是渲染目标中像素的当前RGB颜色。 。由于SourceAlpha总是等于0.0,上述等式简化为:FinalColor = SourceColor + DestColor
。简而言之,我们在这里进行添加剂混合。如果像素着色器返回(0,0,0),则颜色将保持不变。这是完整的HLSL代码-我认为在解释之后,它会更容易理解:我可以很高兴地说我的像素着色器创建的代码与原始代码相同。希望您喜欢这篇文章。感谢您的阅读!struct VS_OUTPUT
{
float3 TexcoordAndWorldspaceHeight : Texcoord0;
float4 LODParams : LODParams; // float4(1,1,1,1)
float4 PositionH : SV_Position;
};
float getFrustumDepth( in float depth )
{
// from [1-0] to [0-1]
float d = depth * cb12_v22.x + cb12_v22.y;
// special coefficents
d = d * cb12_v21.x + cb12_v21.y;
// return frustum depth
return 1.0 / max(d, 1e-4);
}
float4 EditedShaderPS( in VS_OUTPUT Input ) : SV_Target0
{
// * Input from Vertex Shader
float2 InputUV = Input.TexcoordAndWorldspaceHeight.xy;
float WorldHeight = Input.TexcoordAndWorldspaceHeight.z;
float LODParam = Input.LODParams.w;
// * Inputs
float elapsedTime = cb0_v0.x;
float2 uvAnimation = cb4_v5.xy;
float2 uvScale = cb4_v4.xy;
float minValue = cb4_v2.x; // 0.0
float maxValue = cb4_v3.x; // 1.0
float3 shaftsColor = cb4_v0.rgb; // RGB( 147, 162, 173 )
float3 finalColorFilter = cb2_v2.rgb; // float3( 1.175, 1.296, 1.342 );
float finalEffectIntensity = cb2_v2.w;
float2 invViewportSize = cb0_v1.zw;
float depthScale = cb4_v6.x; // 0.001
// sample noise
float2 uvOffsets = elapsedTime * uvAnimation;
float2 uv = InputUV * uvScale + uvOffsets;
float disturb = texture0.Sample( sampler0, uv ).x;
// * Intensity mask
float intensity = saturate( lerp(minValue, maxValue, disturb) );
intensity *= InputUV.y; // transition from (0, 1)
intensity *= LODParam; // usually 1.0
intensity *= cb4_v1.x; // 1.0
// Sample depth
float2 ScreenUV = Input.PositionH.xy * invViewportSize;
float hardwareDepth = texture15.SampleLevel( sampler15, ScreenUV, 0 ).x;
float frustumDepth = getFrustumDepth( hardwareDepth );
// * Calculate mask covering distant objects behind cylinder.
// Seems that the input really is world-space height (.z component, see vertex shader)
float depth = frustumDepth - WorldHeight;
float distantObjectsMask = saturate( depth * depthScale );
// * calculate final mask
float finalEffectMask = saturate( intensity * distantObjectsMask );
// cb0_v7.y and cb4_v7.x are set to 1.0 so I didn't bother with naming them :)
float paramX = finalEffectMask;
float paramY = cb0_v7.y * finalEffectMask;
float effectAmount = lerp(paramX, paramY, cb4_v7.x);
// color of shafts comes from contant buffer
float3 effectColor = effectAmount * shaftsColor;
// gamma correction
effectColor = pow(effectColor, 2.2);
// final multiplications
effectColor *= finalColorFilter;
effectColor *= finalEffectIntensity;
// return with zero alpha 'cause the blending used here is:
// SourceColor * 1.0 + (1.0 - SrcAlpha) * DestColor
return float4( effectColor, 0.0 );
}