返回
Featured image of post Shader入门精要-使用噪声

Shader入门精要-使用噪声

笔记摘自书籍《Shader入门精要》

消融效果

**消融(dissolve)**效果常见于游戏中角色的死亡、地图烧毁等效果。

原理

要实现上图的效果,原理非常简单,概括来说就是噪声纹理+透明度测试。我们使用对噪声纹理采样的结果和某个控制消融程度的阈值比较,如果小于阈值,就使用clip函数把它对应的像素减掉,这些部分对应了图中被“烧毁”的区域。而镂空区域的烧焦效果则是将两种颜色混合,再用pow函数处理后,与原纹理混合后的结果。

实现

  1. 首先,声明消融效果需要的各个属性:
    • _BurnAmount属性用于控制消融程度,当值为0时,物体为正常效果,当值为1时,物体会完全消融。
    • _LineWidth属性用于控制模拟烧焦效果时的线宽,它的值越大,火焰边缘的蔓延范围越广。
    • _MainTex和_BumpMap分别对应了物体原本的漫反射纹理和法线纹理。
    • _BurnFirstColor和_BurnSecondColor对应了火焰边缘的两种颜色值。
    • _BurnMap则是关键的噪声纹理。
  2. 第一个Pass用来计算消融效果,我们在片元着色器中做以下实现
    • 我们首先对噪声纹理进行采样,并将采样结果和用于控制消融程度的属性_BurnAmount相减,
    • 传递给clip函数。当结果小于0时,该像素会被剔除,从而不会显示到屏幕上。如果通过了测试,则进行正常的光照计算。
    • 我们首先根据漫反射纹理得到材质的反射率albedo,并由此计算得到环境光照,进而得到漫反射光照,
    • 然后,我们计算了烧焦颜色burmColor。
    • 我们想要在宽度_LineWidth的范围内模拟一个烧焦的颜色的变化,第一步就使用了smoothstep函数来计算混合系数t。当t值为1时,表明该像素位于消融的边界处,当t值为0时,表明该像素为正常的模型的颜色,而中间的插值则表示需要模拟一个烧焦效果。
    • 我们首先用t来混合两种火焰颜色_BurnFirstColor和 _BurnSecondColor,为了让效果更接近烧焦的痕迹,我们还使用了pow函数对结果进行处理。
    • 然后,我们再次使用t来混合正常的光照颜色(环境光+漫反射)和烧焦颜色。我们这里又使用了step函数来保证当_BurnAmount为0时,不显示任何消融效果。最后,返回混合后的颜色值finalColor。
  3. 第二个Pass用来计算透明物体的阴影投射
    • 我们首先在v2f结构体中利用V2F_SHADOW_CASTER来定义阴影投射需要定义的变量。
    • 随后,在顶点着色器中,我们使用TRANSFER_SHADOW_CASTER_NORMALOFFSET来填充V2F_SHADOW_CASTER在背后声明的一些变量,这是由Unity在背后为我们完成的。
    • 我们需要在顶点着色器中关注自定义的计算部分,这里指的就是我们需要计算噪声纹理的采样坐标uvBurnMap。
    • 在片元着色器中,我们首先按之前的处理方法使用噪声纹理的采样结果来剔除片元,
    • 最后再利用SHADOW_CASTER_FRAGMENT来让Unity为我们完成阴影投射的部分,把结果输出到深度图进而阴影映射纹理中。

具体代码实现见下文

水波效果

在模拟实时水面的过程中,我们往往也会使用噪声纹理。此时,噪声纹理通常会用作一个高度图,以不断修改水面的法线方向。为了模拟水不断的流动效果,我们会使用和时间相关的变量来对噪声纹理进行采样,当得到法线信息后,再进行正常的反射+折射计算,得到最后的水面波动效果。

在本节中,我们将会使用一个由噪声纹理得到的法线贴图,实现一个包含菲涅尔反射的水面效果,如下图所示

原理

  1. 我们曾在前面介绍过如何使用反射和折射来模拟一个透明玻璃效果。本节使用的Shader和前面使用的透明玻璃效果Shader基本相同。

  2. 我们使用一张**立方体纹理(Cubemap)**作为环境纹理,模拟反射。

  3. 为了模拟折射效果,我们使用GrabPass来获取当前屏幕的渲染纹理,

  4. 并使用切线空间下的法线方向对像素的屏幕坐标进行偏移,再使用该坐标对渲染纹理进行屏幕采样,从而模拟近似的折射效果。

  5. 与之前不同的是,水波的法线纹理是由一张噪声纹理生成而得,而且会随着时间变化不断平移,得到波光粼粼的效果。

  6. 除此之外,我们没有使用一个定值来混合反射和折射颜色,而是使用之前提到的菲涅尔系数来动态决定混合系数。我们使用如下公式来计算菲涅尔系数:

    fresnel = pow ( 1 - max ( 0 , v · n ) , 4 )

    其中v和n分别对应了视角方向和法线方向。它们之间的夹角越小,fresnel值越小,反射越弱,折射越强。菲涅尔系数还经常会用于边缘光照的计算中。

本场景中适用的环境纹理如下所示:

实现

  1. 我们首先声明Shader使用的各个属性:
    • _Color用于控制水面颜色;
    • _MainTex是水面波纹材质纹理,默认为白色纹理;
    • _WaveMap是一个由噪声生成的法线纹理;
    • _Cubemap是用于模拟反射的立方体纹理;
    • _Distortion则用于控制模拟折射时图像的扭曲程度;
    • _WaveXSpeed和_WaveYSpeed分别用于控制法线纹理在X和Y方向上的平移速度。
  2. 定义相应的渲染队列,并使用GrabPass来获取屏幕图像:
    • 我们首先在SubShader的标签中将渲染队列设置成Transparent,并把后面的RenderType设置成Opaque。标签Queue设置成Transparent可以保证该物体渲染时,其它所有不透明物体都已经被渲染到屏幕上了,否则就可能无法正确得到“透过水面看到的图像”。而设置RenderType则是为了在使用着色器替换(Shader Replacement)时,该物体可以在需要时被正确渲染。这通常发生在我们需要得到摄像机的深度和法线纹理时。
    • 随后,我们通过关键词GrabPass定义了一个抓取屏幕图像的Pass。在这个Pass中我们定义了一个字符串,该字符串内部的名称决定了抓取得到的屏幕图像将会被存入哪个纹理中。
  3. 定义属性对应的变量,还定义了_RefractionTex和_RefractionTex_TexelSize变量,这对应了在使用GrabPass时,指定的纹理名称。_RefractionTex_TexelSize可以让我们得到该纹理的纹素大小,例如一个大小为256×512的纹理。它的纹素大小为(1/256,1/512)。我们需要在对屏幕图像的采样坐标进行偏移时使用该变量。
  4. 在顶点着色器中,在进行了必要的顶点坐标变换后,我们通过调用ComputeGrabScreenPos来得到对应被抓取屏幕图像的采样坐标。
  5. 我们需要在这里计算该顶点对应的从切线空间到世界空间的变换矩阵,并把该矩阵的每一行存储在TtoW0、TtoW1和TtoW2的xyz分量中。这里面使用的数学方法就是,得到切线空间下的3个坐标轴(x、y、z轴分别对应了切线、副切线和法线的方向)在世界空间下的表示,再把它们依次按列组成一个变换矩阵即可。TtoW0等值的w分量同样被利用起来,用做存储世界空间下的顶点坐标。
  6. 定义片元着色器:
    • 我们首先通过TtoW0等变量的w分量得到世界坐标,并用该值得到该片元对应的视角方向。
    • 我们还使用了内置的_Time.y变量和_WaveXSpeed, _WaveYSpeed属性计算了法线纹理的当前偏移量,并利用该值对法线纹理进行两次采样(这是为了模拟两层交叉的水面波动效果),对两次结果相加并归一化后得到切线空间下的法线方向。
    • 之后,和之前的处理一样,我们使用该值和_Distortion以及_RefractionTex_TexelSize来对屏幕图像的采样坐标进行偏移,模拟折射效果。_Distortion值越大,偏移量越大,水面背后的物体看起来变形程度越大,在这里,我们选择使用切线空间下的法线方向来进行偏移,是因为该空间下的法线可以反应顶点局部空间下的法线方向。需要注意的是,在计算偏移后的屏幕坐标时,我们把偏移量和屏幕坐标的z分量相乘,这是为了模拟深度越大、折射程度越大的效果。
    • 随后,我们对srcPos进行了透视除法,再使用该坐标对抓取的屏幕图像 _RefractionTex进行采样,得到模拟的折射颜色。
    • 之后,我们把法线方向从切线空间变换到了世界空间下(使用变换矩阵的每一行,即TtoW0、TtoW1和TtoW2,分别和法线方向点乘,构成新的法线方向),并据此得到视角方向相对于法线方向的反射方向。
    • 随后,使用反射方向对Cubemap进行采样,并把结果和主纹理颜色相乘后得到反射颜色。我们也对主纹理进行了纹理动画,以模拟水波的效果。
    • 最后为了混合折射和反射颜色,我们随后计算了菲涅耳系数。我们使用之前的公式来计算菲涅耳系数。并据此来混合折射和反射颜色,作为最终的输出颜色。

由于在本例中,我们需要的是一张法线纹理,因此我们可以从该噪声纹理的灰度值中生成需要的法线信息,这是通过在它的面板中把纹理类型设置为Normal map,并选中Create from grayscale来完成的。最后生成的法线纹理如上图的右图所示。我们把生成的法线纹理拖拽到材质的_WaveMap属性上,再单击运行后,就可以看到水面波动的效果了。

具体代码实现见下文

再谈全局雾效

在前面我们讲到了如何使用深度纹理来实现一种基于屏幕后处理的全局雾效。我们由深度纹理重建每个像素在世界空间下的位置,再使用一个基于高度的公式来计算雾效的混合系数,最后使用该系数来混合雾的颜色和原屏幕的颜色。所以实现的效果是一个基于高度的均匀雾效,即在同一个高度上,雾的浓度是相同的。如下图的左图所示:

然而,一些时候我们希望可以模拟一种不均匀的雾效,同时让雾不断飘动,使雾看起来更加缥缈,如上图右图所示。这样就可以通过使用一张噪声纹理来实现。 本节的实现非常简单,绝大多数的代码和前面实现的全局雾效一样,我们只是添加了噪声相关的参数和属性,并在Shader的片元着色器中对高度的计算添加了噪声的影响。

实现

  1. 复制全局雾效使用的C#脚本和Shader
  2. 在脚本中我们增添四个参数:
    • noiseTexture是我们使用的噪声纹理,
    • fogXSpeed和fogYSpeed分别对应了噪声纹理在X和Y方向上的移动速度,以此来模拟雾的飘动效果。
    • 最后,noiseAmount用于控制噪声程度,当noiseAmount为0时,表示不应用任何噪声,即得到一个均匀的基于高度的全局雾效。
  3. 在Shader中,我们做以下修改
    • 增添四个属性,用于接收上一步脚本创建的参数,也用于后续计算
    • 在片元着色器中,我们首先根据深度纹理来重建该像素在世界空间中的位置。
    • 然后,我们利用内置的_Time.y变量和_FogXSpeed、 _FogYSpeed属性计算出当前噪声纹理的偏移量,并据此对噪声纹理进行采样,得到噪声值。我们把该值减去0.5,再乘以控制噪声程度的属性_NoiseAmount,得到最终的噪声值。
    • 随后,我们把该噪声值添加到雾效浓度的计算中,得到应用噪声后的雾效混合系数fogDensity。
    • 最后,我们使用该系数将雾的颜色和原始颜色进行混合后返回。

具体代码实现见下文

代码实现

消融效果

Shader "Unity Shaders Book/Chapter 15/Dissolve"
{
    Properties
    {
        // 消融度
        _BurnAmount ("Burn Amount", Range(0.0, 1.0)) = 0.0
        // 烧焦效果的线宽,值越大,火焰边缘的蔓延范围越广
        _LineWidth ("Burn Line Width", Range(0.0, 0.2)) = 0.1
        // 漫反射纹理
        _MainTex ("Base (RGB)", 2D) = "white" { }
        // 法线纹理
        _BumpMap ("Normal Map", 2D) = "bump" { }
        // 火焰边缘两种颜色
        _BurnFirstColor ("Burn First Color", Color) = (1, 0, 0, 1)
        _BurnSecondColor ("Burn Second Color", Color) = (1, 0, 0, 1)
        // 噪声纹理
        _BurnMap ("Burn Map", 2D) = "white" { }
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
        
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            // 关闭遮挡剔除
            Cull Off
            
            CGPROGRAM
            
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            
            #pragma multi_compile_fwdbase
            
            #pragma vertex vert
            #pragma fragment frag
            
            fixed _BurnAmount;
            fixed _LineWidth;
            sampler2D _MainTex;
            sampler2D _BumpMap;
            fixed4 _BurnFirstColor;
            fixed4 _BurnSecondColor;
            sampler2D _BurnMap;
            
            float4 _MainTex_ST;
            float4 _BumpMap_ST;
            float4 _BurnMap_ST;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 tangent: TANGENT;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float2 uvMainTex: TEXCOORD0;
                float2 uvBumpMap: TEXCOORD1;
                float2 uvBurnMap: TEXCOORD2;
                float3 lightDir: TEXCOORD3;
                float3 worldPos: TEXCOORD4;
                SHADOW_COORDS(5)
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                // 计算纹理坐标
                o.uvMainTex = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uvBumpMap = TRANSFORM_TEX(v.texcoord, _BumpMap);
                o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap);
                
                // 实现从模型空间到切线空间的变换矩阵
                TANGENT_SPACE_ROTATION;
                // 计算世界空间下光线方向
                o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex)).xyz;
                // 世界空间下顶点位置
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                // 计算阴影纹理采样坐标
                TRANSFER_SHADOW(o);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                // 对噪声纹理采样
                fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb;
                
                // 剔除小于消融度的像素
                clip(burn.r - _BurnAmount);
                
                // 切线空间下的光线方向
                float3 tangentLightDir = normalize(i.lightDir);
                // 切线空间下的法线
                fixed3 tangentNormal = UnpackNormal(tex2D(_BumpMap, i.uvBumpMap));
                // 反射率
                fixed3 albedo = tex2D(_MainTex, i.uvMainTex).rgb;
                // 环境光照
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
                // 漫反射光照
                fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(tangentNormal, tangentLightDir));
                
                // 使用smoothstep计算混合系数t
                fixed t = 1 - smoothstep(0.0, _LineWidth, burn.r - _BurnAmount);
                // 使用混合系数,插值当前的两种颜色
                fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t);
                // 对结果进行5次方处理,使结果更接近烧焦效果
                burnColor = pow(burnColor, 5);
                
                // 计算阴影纹理
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
                
                // 最终结果 = 插值(正常光照颜色(漫反射加环境光*阴影),消融颜色,混合系数)		step:a<=b?1:0;
                fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount));
                
                return fixed4(finalColor, 1);
            }
            
            ENDCG
            
        }
        
        // Pass 以投射阴影的方式渲染物体
        Pass
        {
            // 渲染通道 = 阴影映射纹理
            Tags { "LightMode" = "ShadowCaster" }
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            // 阴影映射预编译指令
            #pragma multi_compile_shadowcaster
            
            #include "UnityCG.cginc"
            
            fixed _BurnAmount;
            sampler2D _BurnMap;
            float4 _BurnMap_ST;
            
            struct v2f
            {
                // 阴影投射所需变量
                V2F_SHADOW_CASTER;
                float2 uvBurnMap: TEXCOORD1;
            };
            
            v2f vert(appdata_base v)
            {
                v2f o;
                // 填充阴影投射变量
                TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
                
				// 计算噪声纹理坐标
                o.uvBurnMap = TRANSFORM_TEX(v.texcoord, _BurnMap);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
				// 对噪声纹理进行采样
                fixed3 burn = tex2D(_BurnMap, i.uvBurnMap).rgb;
                
				// 剔除噪声纹理小于消融度像素
                clip(burn.r - _BurnAmount);
                
				// 计算阴影投射
                SHADOW_CASTER_FRAGMENT(i)
            }
            ENDCG
            
        }
    }
    FallBack "Diffuse"
}

水波效果

Shader "Unity Shaders Book/Chapter 15/Water Wave"
{
    Properties
    {
        // 水面颜色
        _Color ("Main Color", Color) = (0, 0.15, 0.115, 1)
        // 水面波纹材质纹理
        _MainTex ("Base (RGB)", 2D) = "white" { }
        // 噪声生成的法线纹理
        _WaveMap ("Wave Map", 2D) = "bump" { }
        // 模拟反射的立方体纹理
        _Cubemap ("Environment Cubemap", Cube) = "_Skybox" { }
        // 控制法线在X、Y方向的平移速度
        _WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01
        _WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01
        // 用于模拟折射时图像的扭曲程度
        _Distortion ("Distortion", Range(0, 100)) = 10
    }
    SubShader
    {
        //  渲染队列 = 透明物体				渲染类型 = 不透明物体
        Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }
        
        // 这个pass将物体背后的屏幕抓取成纹理。
        // 我们可以在下一Pass中以_RefractionTex的形式访问结果
        GrabPass
        {
            "_RefractionTex"
        }
        
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            
            CGPROGRAM
            
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            
            #pragma multi_compile_fwdbase
            
            #pragma vertex vert
            #pragma fragment frag
            
            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D _WaveMap;
            float4 _WaveMap_ST;
            samplerCUBE _Cubemap;
            fixed _WaveXSpeed;
            fixed _WaveYSpeed;
            float _Distortion;
            // 屏幕抓取的纹理
            sampler2D _RefractionTex;
            // 屏幕抓取纹理的纹素大小
            float4 _RefractionTex_TexelSize;
            
            struct a2v
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL;
                float4 tangent: TANGENT;
                float4 texcoord: TEXCOORD0;
            };
            
            struct v2f
            {
                float4 pos: SV_POSITION;
                float4 scrPos: TEXCOORD0;
                float4 uv: TEXCOORD1;
                
                // 用于存储从切线空间到世界空间的变换矩阵
                float4 TtoW0: TEXCOORD2;
                float4 TtoW1: TEXCOORD3;
                float4 TtoW2: TEXCOORD4;
            };
            
            v2f vert(a2v v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                // 获取被抓去屏幕图像的采样坐标
                o.scrPos = ComputeGrabScreenPos(o.pos);
                
                // 计算纹理的坐标
                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _WaveMap);
                
                // 计算世界坐标下的顶点,法线,切线,副法线
                float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
                fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w;
                
                // 计算从切线空间到世界空间的方向变换矩阵
                // 按列摆放得到从切线转世界空间的变换矩阵
                o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
                o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
                o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
                
                return o;
            }
            
            fixed4 frag(v2f i): SV_Target
            {
                // 得到世界空间中的位置、视角方向
                float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
                
                // 通过时间参数与法线偏移属性计算出当前偏移量
                float2 speed = _Time.y * float2(_WaveXSpeed, _WaveYSpeed);
                
                // 计算切线空间的法线
                // 通过两次采样模拟两层交叉的水波效果
                fixed3 bump1 = UnpackNormal(tex2D(_WaveMap, i.uv.zw + speed)).rgb;
                fixed3 bump2 = UnpackNormal(tex2D(_WaveMap, i.uv.zw - speed)).rgb;
                // 将采样结果相加并归一化,得到切线空间下的法线方向
                fixed3 bump = normalize(bump1 + bump2);
                
                // 计算切线空间的偏移量
                // 通过法线与扭曲强度对屏幕图像采样坐标进行偏移
                float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
                // 对深度进行偏移,用于模拟深度越大,折射程度越大的效果
                i.scrPos.xy = offset * i.scrPos.z + i.scrPos.xy;
                // 折射光 = 对偏移坐标使用透视除法后采样,得到模拟的折射颜色
                fixed3 refrCol = tex2D(_RefractionTex, i.scrPos.xy / i.scrPos.w).rgb;
                
                // 将法线转换为世界空间
                bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump)));                
                // 反射率 = 对纹理坐标偏移,然后进行纹理采样
                fixed4 texColor = tex2D(_MainTex, i.uv.xy + speed);                
                // 反射光方向
                fixed3 reflDir = reflect(-viewDir, bump);
                // 反射光 = 立方体纹理采样结果 * 反射率 * 叠加颜色
                fixed3 reflCol = texCUBE(_Cubemap, reflDir).rgb * texColor.rgb * _Color.rgb;
                
				// 计算菲涅耳系数 = (1 - 取正数(视角方向 · 法线方向))的4次方
                fixed fresnel = pow(1 - saturate(dot(viewDir, bump)), 4);
				// 使用菲涅耳系数混合折射与反射颜色
                fixed3 finalColor = reflCol * fresnel + refrCol * (1 - fresnel);
                
                return fixed4(finalColor, 1);
            }
            
            ENDCG
            
        }
    }
    // Do not cast shadow
    FallBack Off
}

再谈全局雾效

Shader "Unity Shaders Book/Chapter 15/Fog With Noise"
{
    Properties
    {
        _MainTex ("Base (RGB)", 2D) = "white" { }
        // 雾效浓度
        _FogDensity ("Fog Density", Float) = 1.0
        // 雾效颜色
        _FogColor ("Fog Color", Color) = (1, 1, 1, 1)
        // 雾效起始高度
        _FogStart ("Fog Start", Float) = 0.0
        // 雾效终止高度
        _FogEnd ("Fog End", Float) = 1.0
        
        // 噪声纹理
        _NoiseTex ("Noise Texture", 2D) = "white" { }
        // 噪声纹理在X、Y轴上的移动速度
        _FogXSpeed ("Fog Horizontal Speed", Float) = 0.1
        _FogYSpeed ("Fog Vertical Speed", Float) = 0.1
        // 用于控制噪声程度
        _NoiseAmount ("Noise Amount", Float) = 1
    }
    SubShader
    {
        CGINCLUDE
        
        #include "UnityCG.cginc"
        
        // 近裁剪平面的四个角对应的向量
        float4x4 _FrustumCornersRay;
        
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        // 深度纹理,用于Unity背后传递该值
        sampler2D _CameraDepthTexture;
        half _FogDensity;
        fixed4 _FogColor;
        float _FogStart;
        float _FogEnd;
        
        sampler2D _NoiseTex;
        half _FogXSpeed;
        half _FogYSpeed;
        half _NoiseAmount;
        
        struct v2f
        {
            float4 pos: SV_POSITION;
            float2 uv: TEXCOORD0;
            float2 uv_depth: TEXCOORD1;
            // 用于存储插值后的像素向量
            float4 interpolatedRay: TEXCOORD2;
        };
        
        v2f vert(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            
            o.uv = v.texcoord;
            o.uv_depth = v.texcoord;
            
            // 检测DirectX平台
            #if UNITY_UV_STARTS_AT_TOP
                // 检测Unity是否已自动翻转了坐标
                if (_MainTex_TexelSize.y < 0)
                    o.uv_depth.y = 1 - o.uv_depth.y;
            #endif
            
            int index = 0;
            if(v.texcoord.x < 0.5 && v.texcoord.y < 0.5)
            {
                index = 0;
            }
            else if(v.texcoord.x > 0.5 && v.texcoord.y < 0.5)
            {
                index = 1;
            }
            else if(v.texcoord.x > 0.5 && v.texcoord.y > 0.5)
            {
                index = 2;
            }
            else
            {
                index = 3;
            }
            
            // 检测DirectX平台
            #if UNITY_UV_STARTS_AT_TOP
                // 检测Unity是否已自动翻转了坐标
                if (_MainTex_TexelSize.y < 0)
                    index = 3 - index;
            #endif
            
            // 使用索引值获取四个顶点变量中对应顶点作为插值后的像素向量
            o.interpolatedRay = _FrustumCornersRay[index];
            
            return o;
        }
        
        fixed4 frag(v2f i): SV_Target
        {
            // 视角空间下的线性深度值	LinearEyeDepth:线性转换	SAMPLE_DEPTH_TEXTURE:深度纹理采样
            float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth));
            // 世界空间下的深度值	_WorldSpaceCameraPos:世界空间下相机位置
            float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
            
			// 使用时间值与纹理偏移变量计算噪声纹理的当前偏移量
            float2 speed = _Time.y * float2(_FogXSpeed, _FogYSpeed);
			// 噪声值 = 使用偏移量对噪声纹理进行采样,将采样结果减去0.5再乘以噪声程度属性得到最终的噪声值
            float noise = (tex2D(_NoiseTex, i.uv + speed).r - 0.5) * _NoiseAmount;
            
            // 高度雾效系数 = (终止高度 - 当前像素高度)/(终止高度 - 起始高度)
            float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
            // 计算浓度 = 高度雾系数 * 雾浓度 * 噪声值		saturate:截取到0-1
            fogDensity = saturate(fogDensity * _FogDensity * (1 + noise));
            
            // 原始颜色
            fixed4 finalColor = tex2D(_MainTex, i.uv);
            // 插值原始颜色,与雾效颜色,使用雾效系数作为参数
            finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);
            
            return finalColor;
        }
        
        ENDCG
        
        Pass
        {
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag
            
            ENDCG
            
        }
    }
    FallBack Off
}