最近,我开始处理《巫师3》的渲染。 该游戏具有惊人的渲染技术。 此外,她在情节/音乐/游戏方面很出色。
在本文中,我将讨论用于渲染The Witcher 3的解决方案。至少到目前为止,它不会像Adrian Correger对
GTA V图形的分析那样全面。
我们将从色调校正的逆向工程开始。
第1部分:音调校正
在大多数现代AAA游戏中,渲染步骤之一必然是色调校正。
让我提醒您,在现实生活中,亮度范围相当广,而在计算机屏幕上,亮度范围非常有限(每像素8位,这使我们获得0-255)。 这是抢救色调映射的地方,使您可以在有限的照明间隔内适应更大的范围。 通常,此过程中有两个数据源:一个具有浮点的HDR图像,其颜色值超过1.0,以及场景的平均照度(后者可以通过几种方式计算,甚至可以考虑用眼睛的适应性来模拟人眼的行为,但这并不重要)。
下一步(也是最后一个步骤)是获取快门速度,使用快门速度计算颜色并使用色调校正曲线进行处理。 由于出现了新概念,例如“白点”(white point)和“中灰”(middle grey)(中灰),因此这里的一切都变得混乱。 至少有一些受欢迎的曲线,而其中一些则在Matt Pettineo的
“色调映射的更仔细的观察”中进行了介绍 。
老实说,我总是在自己的代码中正确实现音调校正时遇到问题。 在网上至少有
一些 不同的 例子对我有用……在某种程度上。 其中一些考虑了HDR亮度/白点/中等灰色,而其他则没有考虑-因此它们并没有真正的帮助。 我想找到一个经过“战斗测试”的实现。
我们将在RenderDoc中进行工作,以捕获Novigrad主要任务之一的这一框架。 所有设置均为最大值:
经过一番搜索,我发现了一个调音的抽奖电话! 如上所述,这里有一个HDR颜色(纹理编号0,全分辨率)和场景的平均亮度(纹理编号1,1x1,浮点,由计算着色器较早计算)的缓冲区。
让我们看一下像素着色器的汇编代码:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[17], immediateIndexed 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 4 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.x, r0.x, cb3[4].y 2: min r0.x, r0.x, cb3[4].z 3: max r0.x, r0.x, l(0.000100) 4: mul r0.y, cb3[16].x, l(11.200000) 5: div r0.x, r0.x, r0.y 6: log r0.x, r0.x 7: mul r0.x, r0.x, cb3[16].z 8: exp r0.x, r0.x 9: mul r0.x, r0.y, r0.x 10: div r0.x, cb3[16].x, r0.x 11: ftou r1.xy, v0.xyxx 12: mov r1.zw, l(0, 0, 0, 0) 13: ld_indexable(texture2d)(float,float,float,float) r0.yzw, r1.xyzw, t0.wxyz 14: mul r0.xyz, r0.yzwy, r0.xxxx 15: mad r1.xyz, cb3[7].xxxx, r0.xyzx, cb3[7].yyyy 16: mul r2.xy, cb3[8].yzyy, cb3[8].xxxx 17: mad r1.xyz, r0.xyzx, r1.xyzx, r2.yyyy 18: mul r0.w, cb3[7].y, cb3[7].z 19: mad r3.xyz, cb3[7].xxxx, r0.xyzx, r0.wwww 20: mad r0.xyz, r0.xyzx, r3.xyzx, r2.xxxx 21: div r0.xyz, r0.xyzx, r1.xyzx 22: mad r0.w, cb3[7].x, l(11.200000), r0.w 23: mad r0.w, r0.w, l(11.200000), r2.x 24: div r1.x, cb3[8].y, cb3[8].z 25: add r0.xyz, r0.xyzx, -r1.xxxx 26: max r0.xyz, r0.xyzx, l(0, 0, 0, 0) 27: mul r0.xyz, r0.xyzx, cb3[16].yyyy 28: mad r1.y, cb3[7].x, l(11.200000), cb3[7].y 29: mad r1.y, r1.y, l(11.200000), r2.y 30: div r0.w, r0.w, r1.y 31: add r0.w, -r1.x, r0.w 32: max r0.w, r0.w, l(0) 33: div o0.xyz, r0.xyzx, r0.wwww 34: mov o0.w, l(1.000000) 35: ret
有几点值得注意。 首先,加载的亮度不必等于所使用的亮度,因为它被限制(最大/最小调用)在艺术家(从常量缓冲区)选择的值内。 这很方便,因为它可以避免太高或太低的快门速度。 这一举动似乎很平常,但我从未做过。 其次,熟悉色调校正曲线的人会立即识别出此“ 11.2”值,因为实际上这是John Hable的
Uncharted2色调校正曲线中白点的值。
AF参数是从cbuffer加载的。
因此,我们还有另外三个参数:cb3_v16.x,cb3_v16.y,cb3_v16.z。 我们可以检查它们的含义:
我的预感:
我认为“ x”是一种“白色标度”或中等灰度,因为它乘以11.2(第4行),然后用作计算快门速度设置的分子(第10行)。
“ Y”-我称它为“ u2分子因数”,很快您就会明白为什么。
“ Z”是“幂参数”,因为它在三对数/ mul / exp(实际上是幂)中使用。
但是,请对这些变量名持怀疑态度!
另外:
cb3_v4.yz-允许的亮度的最小值/最大值,
cb3_v7.xyz-Uncharted2曲线的交流参数,
cb3_v8.xyz-Uncharted2曲线的DF参数。
现在让我们开始困难的部分-我们将编写一个HLSL着色器,它将为我们提供完全相同的汇编代码。
这可能非常困难,并且着色器越长,任务越困难。 幸运的是,前一段时间我写了一个工具来快速浏览hlsl-> asm。
女士们,先生们...欢迎D3DShaderDisassembler!
经过对代码的试验,我得到了现成的HLSL
音调校正The Witcher 3 :
cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); avgLuminance = clamp( avgLuminance, cb3_v4.y, cb3_v4.z ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = cb3_v16.x * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, cb3_v16.z ); luma = luma * scaledWhitePoint; luma = cb3_v16.x / luma; float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, luma*HDRColor, cb3_v16.y); return float4(color, 1); }
我的实用程序的屏幕截图,确认了这一点:
瞧!
我认为,至少在汇编代码方面,这是TW3音调校正的相当准确的实现。 我已经在框架中应用了它,效果很好!
我说“足够”是因为我
不知道为什么ToneMapU2Func中的分母为什么变为零。 除以0时,您应该得到未定义?
可以完成此操作,但是几乎偶然地,我在本帧中发现了TW3色调着色器的另一个版本,用于实现美丽的日落(有趣的是,它使用了最少的图形设置!)
让我们来看看。 首先,着色器的汇编代码:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed 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 5 0: ld_indexable(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw 1: max r0.y, r0.x, cb3[9].y 2: max r0.x, r0.x, cb3[4].y 3: min r0.x, r0.x, cb3[4].z 4: min r0.y, r0.y, cb3[9].z 5: max r0.xy, r0.xyxx, l(0.000100, 0.000100, 0.000000, 0.000000) 6: mul r0.z, cb3[17].x, l(11.200000) 7: div r0.y, r0.y, r0.z 8: log r0.y, r0.y 9: mul r0.y, r0.y, cb3[17].z 10: exp r0.y, r0.y 11: mul r0.y, r0.z, r0.y 12: div r0.y, cb3[17].x, r0.y 13: ftou r1.xy, v0.xyxx 14: mov r1.zw, l(0, 0, 0, 0) 15: ld_indexable(texture2d)(float,float,float,float) r1.xyz, r1.xyzw, t0.xyzw 16: mul r0.yzw, r0.yyyy, r1.xxyz 17: mad r2.xyz, cb3[11].xxxx, r0.yzwy, cb3[11].yyyy 18: mul r3.xy, cb3[12].yzyy, cb3[12].xxxx 19: mad r2.xyz, r0.yzwy, r2.xyzx, r3.yyyy 20: mul r1.w, cb3[11].y, cb3[11].z 21: mad r4.xyz, cb3[11].xxxx, r0.yzwy, r1.wwww 22: mad r0.yzw, r0.yyzw, r4.xxyz, r3.xxxx 23: div r0.yzw, r0.yyzw, r2.xxyz 24: mad r1.w, cb3[11].x, l(11.200000), r1.w 25: mad r1.w, r1.w, l(11.200000), r3.x 26: div r2.x, cb3[12].y, cb3[12].z 27: add r0.yzw, r0.yyzw, -r2.xxxx 28: max r0.yzw, r0.yyzw, l(0, 0, 0, 0) 29: mul r0.yzw, r0.yyzw, cb3[17].yyyy 30: mad r2.y, cb3[11].x, l(11.200000), cb3[11].y 31: mad r2.y, r2.y, l(11.200000), r3.y 32: div r1.w, r1.w, r2.y 33: add r1.w, -r2.x, r1.w 34: max r1.w, r1.w, l(0) 35: div r0.yzw, r0.yyzw, r1.wwww 36: mul r1.w, cb3[16].x, l(11.200000) 37: div r0.x, r0.x, r1.w 38: log r0.x, r0.x 39: mul r0.x, r0.x, cb3[16].z 40: exp r0.x, r0.x 41: mul r0.x, r1.w, r0.x 42: div r0.x, cb3[16].x, r0.x 43: mul r1.xyz, r1.xyzx, r0.xxxx 44: mad r2.xyz, cb3[7].xxxx, r1.xyzx, cb3[7].yyyy 45: mul r3.xy, cb3[8].yzyy, cb3[8].xxxx 46: mad r2.xyz, r1.xyzx, r2.xyzx, r3.yyyy 47: mul r0.x, cb3[7].y, cb3[7].z 48: mad r4.xyz, cb3[7].xxxx, r1.xyzx, r0.xxxx 49: mad r1.xyz, r1.xyzx, r4.xyzx, r3.xxxx 50: div r1.xyz, r1.xyzx, r2.xyzx 51: mad r0.x, cb3[7].x, l(11.200000), r0.x 52: mad r0.x, r0.x, l(11.200000), r3.x 53: div r1.w, cb3[8].y, cb3[8].z 54: add r1.xyz, -r1.wwww, r1.xyzx 55: max r1.xyz, r1.xyzx, l(0, 0, 0, 0) 56: mul r1.xyz, r1.xyzx, cb3[16].yyyy 57: mad r2.x, cb3[7].x, l(11.200000), cb3[7].y 58: mad r2.x, r2.x, l(11.200000), r3.y 59: div r0.x, r0.x, r2.x 60: add r0.x, -r1.w, r0.x 61: max r0.x, r0.x, l(0) 62: div r1.xyz, r1.xyzx, r0.xxxx 63: add r0.xyz, r0.yzwy, -r1.xyzx 64: mad o0.xyz, cb3[13].xxxx, r0.xyzx, r1.xyzx 65: mov o0.w, l(1.000000) 66: ret
乍一看,该代码可能看起来令人生畏,但实际上,并非一切都那么糟糕。 简要分析之后,您会注意到对Uncharted2函数的两次调用使用了不同的输入数据集(AF,最小/最大亮度...)。 我以前从未见过这样的决定。
和HLSL:
cbuffer cBuffer : register (b3) { float4 cb3_v0; float4 cb3_v1; float4 cb3_v2; float4 cb3_v3; float4 cb3_v4; float4 cb3_v5; float4 cb3_v6; float4 cb3_v7; float4 cb3_v8; float4 cb3_v9; float4 cb3_v10; float4 cb3_v11; float4 cb3_v12; float4 cb3_v13; float4 cb3_v14; float4 cb3_v15; float4 cb3_v16, cb3_v17; } Texture2D TexHDRColor : register (t0); Texture2D TexAvgLuminance : register (t1); float3 U2Func( float A, float B, float C, float D, float E, float F, float3 x ) { return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F)) - E/F; } float3 ToneMapU2Func( float A, float B, float C, float D, float E, float F, float3 color, float numMultiplier ) { float3 numerator = U2Func( A, B, C, D, E, F, color ); numerator = max( numerator, 0 ); numerator.rgb *= numMultiplier; float3 denominator = U2Func( A, B, C, D, E, F, 11.2 ); denominator = max( denominator, 0 ); return numerator / denominator; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; float getExposure(float avgLuminance, float minLuminance, float maxLuminance, float middleGray, float powParam) { avgLuminance = clamp( avgLuminance, minLuminance, maxLuminance ); avgLuminance = max( avgLuminance, 1e-4 ); float scaledWhitePoint = middleGray * 11.2; float luma = avgLuminance / scaledWhitePoint; luma = pow( luma, powParam); luma = luma * scaledWhitePoint; float exposure = middleGray / luma; return exposure; } float4 ToneMappingPS( VS_OUTPUT_POSTFX Input) : SV_Target0 { float avgLuminance = TexAvgLuminance.Load( int3(0, 0, 0) ); float exposure1 = getExposure( avgLuminance, cb3_v9.y, cb3_v9.z, cb3_v17.x, cb3_v17.z); float exposure2 = getExposure( avgLuminance, cb3_v4.y, cb3_v4.z, cb3_v16.x, cb3_v16.z); float3 HDRColor = TexHDRColor.Load( uint3(Input.Position.xy, 0) ).rgb; float3 color1 = ToneMapU2Func( cb3_v11.x, cb3_v11.y, cb3_v11.z, cb3_v12.x, cb3_v12.y, cb3_v12.z, exposure1*HDRColor, cb3_v17.y); float3 color2 = ToneMapU2Func( cb3_v7.x, cb3_v7.y, cb3_v7.z, cb3_v8.x, cb3_v8.y, cb3_v8.z, exposure2*HDRColor, cb3_v16.y); float3 finalColor = lerp( color2, color1, cb3_v13.x ); return float4(finalColor, 1); }
也就是说,实际上,我们有两组控制参数,我们通过色调校正计算出两种颜色,最后对它们进行插值。 明智的决定!
第2部分:眼睛适应
第二部分将简单得多。
在第一部分中,我展示了如何在TW3中执行音调校正。 在解释理论背景时,我简要提到了眼睛的适应性。 你知道吗? 在这一部分中,我将讨论如何实现眼睛的这种适应。
但是,等等,什么是眼睛适应,为什么我们需要它?
Wikipedia对此一无所知,但我会解释:假设您在黑暗的房间(记住Life is Strange)或在山洞中,然后走到光线充足的地方。 例如,主要的照明源可以是太阳。
在黑暗中,我们的瞳孔会扩张,以便更多的光线通过它们进入视网膜。 当光线变亮时,我们的瞳孔减少,有时我们会闭上眼睛,因为它“受伤”了。
此更改不会立即发生。 眼睛必须适应亮度的变化。 这就是为什么我们在实时渲染中执行眼睛适应。
DirectX SDK的
HDRToneMappingCS11是一个很好的例子,说明何时缺乏眼睛适应能力。 中等亮度的急剧变化是相当不愉快和不自然的。
让我们开始吧! 为了保持一致,我们将分析Novigrad的同一帧。
现在,我们将深入研究帧捕获程序RenderDoc。 眼睛的适应通常是在进行色调校正之前进行的,The Witcher 3也不例外。
让我们看一下像素着色器的状态:
我们有两个输入源-2个纹理R32_FLOAT,1x1(一个像素)。 Texture0包含前一帧场景的平均亮度。 texture1包含当前帧中场景的平均亮度(在此计算着色器之前立即计算-我将其标记为蓝色)。
预期会有一个输出-R32_FLOAT,1x1。 让我们看一下像素着色器。
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[1], immediateIndexed dcl_sampler s0, mode_default dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_resource_texture2d (float,float,float,float) t1 dcl_output o0.xyzw dcl_temps 1 0: sample_l(texture2d)(float,float,float,float) r0.x, l(0, 0, 0, 0), t1.xyzw, s1, l(0) 1: sample_l(texture2d)(float,float,float,float) r0.y, l(0, 0, 0, 0), t0.yxzw, s0, l(0) 2: ge r0.z, r0.y, r0.x 3: add r0.x, -r0.y, r0.x 4: movc r0.z, r0.z, cb3[0].x, cb3[0].y 5: mad o0.xyzw, r0.zzzz, r0.xxxx, r0.yyyy 6: ret
哇,多么简单! 只有7行汇编代码。 这是怎么回事 我将解释每一行:
0)获取当前帧的平均亮度。
1)获取前一帧的平均亮度。
2)执行检查:当前亮度是否小于或等于前一帧的亮度?
如果是,则亮度降低;如果不是,则亮度增加。
3)计算差异:
差异= currentLum-previousLum。4)此条件传输(movc)从常量缓冲区分配速度因子。 根据检查的结果,可以从第2行分配两个不同的值。 这是明智之举,因为通过这种方式,您既可以降低亮度也可以提高亮度,从而获得不同的适应速度。 但是在所研究的框架中,两个值是相同的,并且在0.11到0.3之间变化。
5)适应亮度的最终计算:
adaptedLuminance = speedFactor *差异+ previousLuminance。6)着色器的末端
这在HLSL中非常简单地实现:
// The Witcher 3 eye adaptation shader cbuffer cBuffer : register (b3) { float4 cb3_v0; } struct VS_OUTPUT_POSTFX { float4 Position : SV_Position; }; SamplerState samplerPointClamp : register (s0); SamplerState samplerPointClamp2 : register (s1); Texture2D TexPreviousAvgLuminance : register (t0); Texture2D TexCurrentAvgLuminance : register (t1); float4 TW3_EyeAdaptationPS(VS_OUTPUT_POSTFX Input) : SV_TARGET { // Get current and previous luminance. float currentAvgLuminance = TexCurrentAvgLuminance.SampleLevel( samplerPointClamp2, float2(0.0, 0.0), 0 ); float previousAvgLuminance = TexPreviousAvgLuminance.SampleLevel( samplerPointClamp, float2(0.0, 0.0), 0 ); // Difference between current and previous luminance. float difference = currentAvgLuminance - previousAvgLuminance; // Scale factor. Can be different for both falling down and rising up of luminance. // It affects speed of adaptation. // Small conditional test is performed here, so different speed can be set differently for both these cases. float adaptationSpeedFactor = (currentAvgLuminance <= previousAvgLuminance) ? cb3_v0.x : cb3_v0.y; // Calculate adapted luminance. float adaptedLuminance = adaptationSpeedFactor * difference + previousAvgLuminance; return adaptedLuminance; }
这些行为我们提供了相同的汇编代码。 我只建议用
float将输出类型替换为
float4 。 无需浪费带宽。 这就是巫师3实现眼睛适应的方式。 很简单,对吧?
PS。 非常感谢Baldur Karlsson(Twitter:
@baldurk )的RenderDoc。 该程序是伟大的。
第3部分:色差
色差是一种主要在廉价镜片中发现的效果。 发生这种情况是因为透镜对于不同长度的可见光具有不同的折射率。 结果,出现了可见的失真。 但是,并非每个人都喜欢它。 幸运的是,在《巫师3》中,这种效果非常微妙,因此不会影响游戏玩法(至少对我而言)。 但是您可以根据需要将其关闭。
让我们仔细看一个有色差和没有色差的场景示例:
包括色差色差禁用您是否注意到边缘附近有任何差异? 我也没有 让我们尝试另一个场景:
包括色差。 请注意指示区域中的轻微“红色”变形。是的,好多了! 在这里,暗区和亮区之间的对比度更强,在拐角处我们会看到轻微的失真。 如您所见,这种效果非常弱。 但是,我想知道它是如何实现的。 让我们继续最有趣的部分:代码!
实作首先要做的是使用像素着色器找到正确的绘制调用。 实际上,色差是大型“后处理”像素着色器的一部分,该着色器由色差,渐晕和伽玛校正组成。 所有这些都在单个像素着色器内。 让我们仔细看一下像素着色器的汇编代码:
ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb3[18], immediateIndexed dcl_sampler s1, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v0.xy, position dcl_input_ps linear v1.zw dcl_output o0.xyzw dcl_temps 4 0: mul r0.xy, v0.xyxx, cb3[17].zwzz 1: mad r0.zw, v0.xxxy, cb3[17].zzzw, -cb3[17].xxxy 2: div r0.zw, r0.zzzw, cb3[17].xxxy 3: dp2 r1.x, r0.zwzz, r0.zwzz 4: sqrt r1.x, r1.x 5: add r1.y, r1.x, -cb3[16].y 6: mul_sat r1.y, r1.y, cb3[16].z 7: sample_l(texture2d)(float,float,float,float) r2.xyz, r0.xyxx, t0.xyzw, s1, l(0) 8: lt r1.z, l(0), r1.y 9: if_nz r1.z 10: mul r1.y, r1.y, r1.y 11: mul r1.y, r1.y, cb3[16].x 12: max r1.x, r1.x, l(0.000100) 13: div r1.x, r1.y, r1.x 14: mul r0.zw, r0.zzzw, r1.xxxx 15: mul r0.zw, r0.zzzw, cb3[17].zzzw 16: mad r0.xy, -r0.zwzz, l(2.000000, 2.000000, 0.000000, 0.000000), r0.xyxx 17: sample_l(texture2d)(float,float,float,float) r2.x, r0.xyxx, t0.xyzw, s1, l(0) 18: mad r0.xy, v0.xyxx, cb3[17].zwzz, -r0.zwzz 19: sample_l(texture2d)(float,float,float,float) r2.y, r0.xyxx, t0.xyzw, s1, l(0) 20: endif ...
并为cbuffer值:
因此,让我们尝试了解这里发生的情况。 实际上,cb3_v17.xy是色差的中心,因此第一行计算从纹理像素坐标(cb3_v17.zw =视口大小的倒数)到“色差中心”及其长度的2d向量,然后执行其他计算,验证和分支。 当应用色差时,我们使用恒定缓冲区中的某些值来计算位移,并使通道R和G失真。通常,离屏幕边缘越近,效果越强。 第10行非常有趣,因为它使像素“更近”,尤其是当我们夸大像差时。 我将很高兴与您分享我对效果的认识。 像往常一样,对变量名持怀疑态度。 并且请注意,该效果是
在伽玛校正
之前应用的。
void ChromaticAberration( float2 uv, inout float3 color ) { // User-defined params float2 chromaticAberrationCenter = float2(0.5, 0.5); float chromaticAberrationCenterAvoidanceDistance = 0.2; float fA = 1.25; float fChromaticAbberationIntensity = 30; float fChromaticAberrationDistortionSize = 0.75; // Calculate vector float2 chromaticAberrationOffset = uv - chromaticAberrationCenter; chromaticAberrationOffset = chromaticAberrationOffset / chromaticAberrationCenter; float chromaticAberrationOffsetLength = length(chromaticAberrationOffset); // To avoid applying chromatic aberration in center, subtract small value from // just calculated length. float chromaticAberrationOffsetLengthFixed = chromaticAberrationOffsetLength - chromaticAberrationCenterAvoidanceDistance; float chromaticAberrationTexel = saturate(chromaticAberrationOffsetLengthFixed * fA); float fApplyChromaticAberration = (0.0 < chromaticAberrationTexel); if (fApplyChromaticAberration) { chromaticAberrationTexel *= chromaticAberrationTexel; chromaticAberrationTexel *= fChromaticAberrationDistortionSize; chromaticAberrationOffsetLength = max(chromaticAberrationOffsetLength, 1e-4); float fMultiplier = chromaticAberrationTexel / chromaticAberrationOffsetLength; chromaticAberrationOffset *= fMultiplier; chromaticAberrationOffset *= g_Viewport.zw; chromaticAberrationOffset *= fChromaticAbberationIntensity; float2 offsetUV = -chromaticAberrationOffset * 2 + uv; color.r = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).r; offsetUV = uv - chromaticAberrationOffset; color.g = TexColorBuffer.SampleLevel(samplerLinearClamp, offsetUV, 0).g; } }
顾名思义,我添加了“ fChromaticAberrationIntensity”来增加偏移的大小,从而增加效果的强度(TW3 = 1.0)。 强度= 40:
仅此而已! 希望您喜欢这部分。
第4部分:渐晕
渐晕是游戏中最常见的后处理效果之一。 他在摄影界也很受欢迎。 略微阴影的角落可以创建漂亮的效果。 渐晕有几种类型。 例如,
虚幻引擎4使用自然。 但是回到巫师3。
单击此处以查看有无渐晕的帧的交互式比较。 该比较来自
NVIDIA针对《巫师3》的性能指南 。
启用渐晕效果的“巫师3”的屏幕截图。请注意,左上角(天空)的阴影不及图像其他部分的阴影。 稍后我们将返回至此。
实施细节首先,原始版本的《巫师3》(2015年5月19日发布)和《巫师3:血与酒》中使用的渐晕之间存在细微差异。 在前者中,“反梯度”是在像素着色器内部计算的,而在后者中,它是预先计算为256x256 2D纹理的:
256x256纹理,用作补全“血液和葡萄酒”中的“反向渐变”。
我将使用“ Blood and Wine”(顺便说一句很棒的游戏)中的着色器。 与大多数其他游戏一样,Witcher 3渐晕是在最终后期处理的像素着色器中计算的。 看一下汇编代码:
... 44: log r0.xyz, r0.xyzx 45: mul r0.xyz, r0.xyzx, l(0.454545, 0.454545, 0.454545, 0.000000) 46: exp r0.xyz, r0.xyzx 47: mul r1.xyz, r0.xyzx, cb3[9].xyzx 48: sample_indexable(texture2d)(float,float,float,float) r0.w, v1.zwzz, t2.yzwx, s2 49: log r2.xyz, r1.xyzx 50: mul r2.xyz, r2.xyzx, l(2.200000, 2.200000, 2.200000, 0.000000) 51: exp r2.xyz, r2.xyzx 52: dp3 r1.w, r2.xyzx, cb3[6].xyzx 53: add_sat r1.w, -r1.w, l(1.000000) 54: mul r1.w, r1.w, cb3[6].w 55: mul_sat r0.w, r0.w, r1.w 56: mad r0.xyz, -r0.xyzx, cb3[9].xyzx, cb3[7].xyzx 57: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx ...
有趣! 看起来伽玛(第46行)和线性空格(第51行)都用于计算渐晕。 在第48行,我们对“反梯度”的纹理进行采样。 cb3 [9] .xyz与渐晕无关。 在检查的每个帧中,为其分配值float3(1.0、1.0、1.0),也就是说,它可能是淡入/淡出效果中使用的最终滤波器。 TW3中的渐晕有三个主要参数:
- 不透明度(cb3 [6] .w)-影响渐晕的强度。 0-无渐晕,1-最大渐晕。 根据我的观察,在《 The Witcher 3》中,它大约为1.0,而在《 Blood and Wine》中,它大约在0.15附近波动。
- 颜色(cb3 [7] .xyz)-TW3渐晕的一个出色功能是能够更改其颜色。 它不一定是黑色的,但实际上...通常具有值float3(3.0 / 255.0、4.0 / 255.0、5.0 / 255.0),依此类推-在一般情况下,它们是0.00392156 = 1.0 / 255.0的倍数
- 权重(cb3 [6] .xyz)是一个非常有趣的参数。 我总是看到“扁平”的渐晕,例如:
典型的渐晕面具但是使用权重(第52行),您可以获得非常有趣的结果:
使用权重计算的TW3渐晕遮罩权重接近1.0。 查看“血与酒”(带有彩虹的神奇世界)中一帧的常量缓冲数据:这就是为什么渐晕不影响上方天空的明亮像素的原因。
代号这是我在HLSL上实施TW3渐晕的方法。
GammaToLinear =战俘(颜色2.2)
float3 Vignette_TW3( in float3 gammaColor, in float3 vignetteColor, in float3 vignetteWeights, in float vignetteOpacity, in Texture2D texVignette, in float2 texUV ) { // For coloring vignette float3 vignetteColorGammaSpace = -gammaColor + vignetteColor; // Calculate vignette amount based on color in *LINEAR* color space and vignette weights. float vignetteWeight = dot( GammaToLinear( gammaColor ), vignetteWeights ); // We need to keep vignette weight in [0-1] range vignetteWeight = saturate( 1.0 - vignetteWeight ); // Multiply by opacity vignetteWeight *= vignetteOpacity; // Obtain vignette mask (here is texture; you can also calculate your custom mask here) float sampledVignetteMask = texVignette.Sample( samplerLinearClamp, texUV ).x; // Final (inversed) vignette mask float finalInvVignetteMask = saturate( vignetteWeight * sampledVignetteMask ); // final composite in gamma space float3 Color = vignetteColorGammaSpace * finalInvVignetteMask + gammaColor.rgb; // * uncomment to debug vignette mask: // return 1.0 - finalInvVignetteMask; // Return final color return Color; }
希望您喜欢。您也可以尝试我的HLSLexplorer,它对我了解HLSL汇编程序代码有很大帮助。和以前一样,对变量的名称持怀疑态度-TW3着色器由D3DStripShader处理,因此,实际上,我几乎对它们一无所知,我只能猜测。此外,对于此着色器对您的设备造成的损坏,我不承担任何责任;)奖金:计算梯度在2015年发布的《巫师3》中,反向梯度是在像素着色器中计算的,而不是对预先计算的纹理进行采样。看一下汇编代码: 35: add r2.xy, v1.zwzz, l(-0.500000, -0.500000, 0.000000, 0.000000) 36: dp2 r1.w, r2.xyxx, r2.xyxx 37: sqrt r1.w, r1.w 38: mad r1.w, r1.w, l(2.000000), l(-0.550000) 39: mul_sat r2.w, r1.w, l(1.219512) 40: mul r2.z, r2.w, r2.w 41: mul r2.xy, r2.zwzz, r2.zzzz 42: dp4 r1.w, l(-0.100000, -0.105000, 1.120000, 0.090000), r2.xyzw 43: min r1.w, r1.w, l(0.940000)
对我们来说幸运的是,这很简单。在HLSL上,它将如下所示: float TheWitcher3_2015_Mask( in float2 uv ) { float distanceFromCenter = length( uv - float2(0.5, 0.5) ); float x = distanceFromCenter * 2.0 - 0.55; x = saturate( x * 1.219512 ); // 1.219512 = 100/82 float x2 = x * x; float x3 = x2 * x; float x4 = x2 * x2; float outX = dot( float4(x4, x3, x2, x), float4(-0.10, -0.105, 1.12, 0.09) ); outX = min( outX, 0.94 ); return outX; }
也就是说,我们只需计算从中心到纺织品的距离,并对其进行一些魔术处理(乘法,饱和...),然后...即可计算多项式!太棒了第五部分:陶醉的效果
让我们看看游戏《巫师3:狂猎》如何实现陶醉的效果。如果尚未播放,则放下所有东西,购买并播放,观看视频:晚上:晚上:首先,我们看到双重和旋转的图像,通常是在现实生活中饮酒时出现的。距图像中心的像素越远,旋转效果越强。我特意在夜晚发布了第二个视频,因为您可以清楚地看到星星上的这种旋转(请参见8个单独的点?)醉人的效果的第二部分(可能不会立即注意到)是变焦的微小变化。在中心附近很明显。很明显,这种效果是典型的后处理(像素着色器)。但是,它在渲染管道中的位置可能不太明显。它的出现,立即中毒效果应用于后色调校正和前直运动模糊(«醉酒”图像被输入到运动模糊)。让我们从汇编代码开始游戏: ps_5_0 dcl_globalFlags refactoringAllowed dcl_constantbuffer cb0[2], immediateIndexed dcl_constantbuffer cb3[3], immediateIndexed dcl_sampler s0, mode_default dcl_resource_texture2d (float,float,float,float) t0 dcl_input_ps_siv v1.xy, position dcl_output o0.xyzw dcl_temps 8 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw 17: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r2.xyzw 18: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 19: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 20: add r5.xyzw, r5.xyzw, r6.xyzw 21: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r2.xyzw 22: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 23: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 24: add r5.xyzw, r5.xyzw, r7.xyzw 25: add r5.xyzw, r6.xyzw, r5.xyzw 26: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r2.xyzw 27: mad r2.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r2.xyzw 28: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 29: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 30: add r5.xyzw, r5.xyzw, r7.xyzw 31: add r5.xyzw, r6.xyzw, r5.xyzw 32: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r2.xyxx, t0.xyzw, s0 33: sample_indexable(texture2d)(float,float,float,float) r2.xyzw, r2.zwzz, t0.xyzw, s0 34: add r5.xyzw, r5.xyzw, r6.xyzw 35: add r2.xyzw, r2.xyzw, r5.xyzw 36: mul r2.xyzw, r2.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500) 37: mad r5.xyzw, r4.zwzw, l(1.000000, 0.000000, -1.000000, -0.000000), r3.zwzw 38: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r5.xyxx, t0.xyzw, s0 39: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r5.zwzz, t0.xyzw, s0 40: add r5.xyzw, r5.xyzw, r6.xyzw 41: mad r6.xyzw, r4.zwzw, l(0.707000, 0.707000, -0.707000, -0.707000), r3.zwzw 42: sample_indexable(texture2d)(float,float,float,float) r7.xyzw, r6.xyxx, t0.xyzw, s0 43: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 44: add r5.xyzw, r5.xyzw, r7.xyzw 45: add r5.xyzw, r6.xyzw, r5.xyzw 46: mad r6.xyzw, r4.zwzw, l(0.000000, 1.000000, -0.000000, -1.000000), r3.zwzw 47: mad r3.xyzw, r4.xyzw, l(-0.707000, 0.707000, 0.707000, -0.707000), r3.xyzw 48: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r6.xyxx, t0.xyzw, s0 49: sample_indexable(texture2d)(float,float,float,float) r6.xyzw, r6.zwzz, t0.xyzw, s0 50: add r4.xyzw, r4.xyzw, r5.xyzw 51: add r4.xyzw, r6.xyzw, r4.xyzw 52: sample_indexable(texture2d)(float,float,float,float) r5.xyzw, r3.xyxx, t0.xyzw, s0 53: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.zwzz, t0.xyzw, s0 54: add r4.xyzw, r4.xyzw, r5.xyzw 55: add r3.xyzw, r3.xyzw, r4.xyzw 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret
这里使用两个单独的常量缓冲区。让我们检查一下它们的值:我们对其中的一些感兴趣:cb0_v0.x->经过的时间(以秒为单位)cb0_v1.xyzw-视口大小和视口大小的倒数(又名“像素大小”)cb3_v0.x-围绕像素旋转,其值始终为1.0。cb3_v0.y-中毒作用的大小。启用后,它无法完全发挥作用,但会从0.0逐渐增加到1.0。cv3_v1.xy-像素偏移量(有关此内容,请参见下文)。这是一个sin / cos对,因此您可以根据需要在着色器中使用sincos(时间)。cb3_v2.xy是效果的中心,通常是float2(0.5,0.5)。在这里,我们要专注于了解正在发生的事情,而不仅仅是盲目地重写着色器。我们将从第一行开始: ps_5_0 0: mad r0.x, cb3[0].y, l(-0.100000), l(1.000000) 1: mul r0.yz, cb3[1].xxyx, l(0.000000, 0.050000, 0.050000, 0.000000) 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 4: sqrt r1.z, r0.w
我将第0行称为“缩放比例”,您很快就会明白原因。在它之后(第1行),我们立即计算“旋转位移”。这只是输入sin / cos数据对乘以0.05。2-4行:首先,我们计算从效果中心到纹理的UV坐标的向量。然后我们计算距离(3)和简单距离(4)(从中心到纹理像素)的平方缩放纹理坐标
让我们看下面的汇编代码: 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy
由于它们是以这种方式包装的,因此我们只能分析一对浮标。对于初学者,r0.yz是“旋转偏移”,r1.z是从中心到纹理像素的距离,r1.xy是从中心到纹理像素的矢量,r0.x是“缩放因子”。为了理解这一点,让我们现在假设zoomFactor = 1.0,也就是说,您可以编写以下代码: 8: mul r2.xyzw, r0.yzyz, r1.zzzz 9: mad r2.xyzw, r1.xyxy, r0.xxxx, -r2.xyzw 13: add r2.xyzw, r2.xyzw, cb3[2].xyxy r2.xy = (texel - center) * zoomFactor - rotationOffsets * distanceFromCenter + center;
但是zoomFactor = 1.0: r2.xy = texel - center - rotationOffsets * distanceFromCenter + center; r2.xy = texel - rotationOffsets * distanceFromCenter;
对于r3.xy同样: 10: mul r3.xy, r0.xxxx, r1.xyxx 11: mad r3.xyzw, r0.yzyz, r1.zzzz, r3.xyxy 12: add r3.xyzw, r3.xyzw, cb3[2].xyxy r3.xy = rotationOffsets * distanceFromCenter + zoomFactor * (texel - center) + center
但是zoomFactor = 1.0:太好了。也就是说,目前我们基本上具有当前的TextureUV(texel)±旋转偏移量,但是zoomFactor呢?看一下第0行。实际上,zoomFactor = 1.0-0.1 * drunkAmount。对于最大的drunkAmount,zoomFactor的值应为0.9,并且具有zoom的纹理坐标现在如下计算:r3.xy = rotationOffsets * distanceFromCenter + texel - center + center r3.xy = texel + rotationOffsets * distanceFromCenter
baseTexcoordsA = 0.9 * texel + 0.1 * center + rotationOffsets * distanceFromCenter baseTexcoordsB = 0.9 * texel + 0.1 * center - rotationOffsets * distanceFromCenter
也许这样的解释会更直观:通过归一化纹理坐标和中心之间的某种因素进行简单线性插值。这是“放大”图像。要理解这一点,最好尝试使用这些值。这是Shadertoy 的链接,您可以在其中看到实际效果。纹理偏移
汇编代码中的整个片段: 2: mad r1.xy, v1.xyxx, cb0[1].zwzz, -cb3[2].xyxx 3: dp2 r0.w, r1.xyxx, r1.xyxx 5: mul r0.w, r0.w, l(10.000000) 6: min r0.w, r0.w, l(1.000000) 7: mul r0.w, r0.w, cb3[0].y 14: mul r0.x, r0.w, cb3[0].x 15: mul r0.x, r0.x, l(5.000000) // texcoords offset intensity 16: mul r4.xyzw, r0.xxxx, cb3[0].zwzw // texcoords offset
创建一个特定的渐变,我们称之为“位移强度蒙版”。实际上,它具有两个含义。第一个在r0.w中(我们将在以后使用),第二个在r0.x中强5倍(第15行)。后者实际上是纹理像素大小的一个因素,因此会影响偏压力。旋转相关的采样接下来,执行一系列纹理采样。实际上,使用了2个系列的8个样本,每个“边”一个。在HLSL中,您可以这样编写: static const float2 pointsAroundPixel[8] = { float2(1.0, 0.0), float2(-1.0, 0.0), float2(0.707, 0.707), float2(-0.707, -0.707), float2(0.0, 1.0), float2(0.0, -1.0), float2(-0.707, 0.707), float2(0.707, -0.707) }; float4 colorA = 0; float4 colorB = 0; int i=0; [unroll] for (i = 0; i < 8; i++) { colorA += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsA + texcoordsOffset * pointsAroundPixel[i] ); } colorA /= 16.0; [unroll] for (i = 0; i < 8; i++) { colorB += TexColorBuffer.Sample( samplerLinearClamp, baseTexcoordsB + texcoordsOffset * pointsAroundPixel[i] ); } colorB /= 16.0; float4 rotationPart = colorA + colorB;
诀窍是,我们在baseTexcoordsA / B上添加了一个单位圆上的附加偏移量,乘以前面提到的“纹理坐标偏移强度”。距像素中心越远,围绕像素的圆的半径越大-我们对其进行8次采样,这在星星上清晰可见。PointsAroundPixel值(45度的倍数):单圈缩放相关的采样《巫师3》中醉酒效果的第二部分是放大和缩小。让我们看一下执行此任务的汇编代码: 56: mad r2.xyzw, r3.xyzw, l(0.062500, 0.062500, 0.062500, 0.062500), r2.xyzw // the rotation part is stored in r2 register 57: mul r0.x, cb3[0].y, l(8.000000) 58: mul r0.xy, r0.xxxx, cb3[0].zwzz 59: mad r0.z, cb3[1].y, l(0.020000), l(1.000000) 60: mul r1.zw, r0.zzzz, r1.xxxy 61: mad r1.xy, r1.xyxx, r0.zzzz, cb3[2].xyxx 62: mad r3.xy, r1.zwzz, r0.xyxx, r1.xyxx 63: mul r0.xy, r0.xyxx, r1.zwzz 64: mad r0.xy, r0.xyxx, l(2.000000, 2.000000, 0.000000, 0.000000), r1.xyxx 65: sample_indexable(texture2d)(float,float,float,float) r1.xyzw, r1.xyxx, t0.xyzw, s0 66: sample_indexable(texture2d)(float,float,float,float) r4.xyzw, r0.xyxx, t0.xyzw, s0 67: sample_indexable(texture2d)(float,float,float,float) r3.xyzw, r3.xyxx, t0.xyzw, s0 68: add r1.xyzw, r1.xyzw, r3.xyzw 69: add r1.xyzw, r4.xyzw, r1.xyzw
我们看到有三个单独的纹理调用,即三个不同的纹理坐标。让我们分析如何从中计算纹理坐标。但首先,我们显示此部分的输入: float zoomInOutScalePixels = drunkEffectAmount * 8.0; // line 57 float2 zoomInOutScaleNormalizedScreenCoordinates = zoomInOutScalePixels * texelSize.xy; // line 58 float zoomInOutAmplitude = 1.0 + 0.02*cos(time); // line 59 float2 zoomInOutfromCenterToTexel = zoomInOutAmplitude * fromCenterToTexel; // line 60
关于输入的几句话。我们以像素为单位计算偏移量(例如8.0 *像素大小),然后将其添加到基本uv坐标中。振幅仅在0.98到1.02之间,以提供缩放感,就像执行旋转的部件中的zoomFactor一样。让我们从第一对开始-r1.xy(第61行) r1.xy = fromCenterToTexel * amplitude + center r1.xy = (TextureUV - Center) * amplitude + Center // you can insert here zoomInOutfromCenterToTexel r1.xy = TextureUV * amplitude - Center * amplitude + Center r1.xy = TextureUV * amplitude + Center * 1.0 - Center * amplitude r1.xy = TextureUV * amplitude + Center * (1.0 - amplitude) r1.xy = lerp( TextureUV, Center, amplitude);
那就是: float2 zoomInOutBaseTextureUV = lerp(TextureUV, Center, amplitude)
让我们检查第二对-r3.xy(第62行) r3.xy = (amplitude * fromCenterToTexel) * zoomInOutScaleNormalizedScreenCoordinates + zoomInOutBaseTextureUV
那就是: float2 zoomInOutAddTextureUV0 = zoomInOutBaseTextureUV + zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates;
让我们检查第三对-r0.xy(第63-64行) r0.xy = zoomInOutScaleNormalizedScreenCoordinates * (amplitude * fromCenterToTexel) * 2.0 + zoomInOutBaseTextureUV
那就是: float2 zoomInOutAddTextureUV1 = zoomInOutBaseTextureUV + 2.0*zoomInOutfromCenterToTexel*zoomInOutScaleNormalizedScreenCoordinates
将所有三个纹理查询加在一起,并将结果存储在寄存器r1中。值得注意的是,此像素着色器使用了有限的寻址采样器。全部放在一起因此,目前,我们在r2寄存器中有了旋转的结果,在r1寄存器中有了三个折叠的缩放请求。让我们看一下汇编代码的最后几行: 70: mad r2.xyzw, -r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333), r2.xyzw 71: mul r1.xyzw, r1.xyzw, l(0.333333, 0.333333, 0.333333, 0.333333) 72: mul r0.xyzw, r0.wwww, r2.xyzw 73: mad o0.xyzw, cb3[0].yyyy, r0.xyzw, r1.xyzw 74: ret
关于附加输入:r0.w来自第7行,这是我们的强度遮罩,而cb3 [0] .y是中毒效果的大小。让我们看看它是如何工作的。我的第一种方法是蛮力: float4 finalColor = intensityMask * (rotationPart - zoomingPart); finalColor = drunkIntensity * finalColor + zoomingPart; return finalColor;
但是,该死的,没有人写这样的着色器。我用铅笔用纸写了这个公式: finalColor = effectAmount * [intensityMask * (rotationPart - zoomPart)] + zoomPart finalColor = effectAmount * intensityMask * rotationPart - effectAmount * intensityMask * zoomPart + zooomPart
其中t = effectAmount * strengthMask因此,我们得到: finalColor = t * rotationPart - t * zoomPart + zoomPart finalColor = t * rotationPart + zoomPart - t * zoomPart finalColor = t * rotationPart + (1.0 - t) * zoomPart finalColor = lerp( zoomingPart, rotationPart, t )
我们得出以下结论: finalColor = lerp(zoomingPart, rotationPart, intensityMask * drunkIntensity);
是的,文章的这一部分原来非常详细,但是我们终于完成了!我个人在写作过程中学到了一些东西,希望您也能做到!如果您有兴趣,可以在此处获得完整的HLSL来源。我使用HLSLexplorer对其进行了测试,尽管与原始着色器没有直接的一对一对应关系,但差异是如此之小(少了一行),我可以自信地说它可以工作。感谢您的阅读!