本文最后更新于:2025年4月17日 下午
消融效果
**消融(dissolve)**效果常见于游戏中的角色死亡、地图烧毁等效果。在这些效果中,消融往往从不同的区域开始,并向看似随机的方向扩张,最后整个物体都将消失不见。我们将学习如何在 Unity 中实现这种效果,并得到类似下图中的效果。
要实现上图中的效果,原理非常简单,概括来说就是噪声纹理+透明度测试。我们使用对噪声纹理采样的结果和某个控制消融程度的阈值比较,如果小于阈值,就使用 clip 函数把它对应的像素裁剪掉,这些部分就对应了图中被“烧毁”的区域。而镂空区域边缘的烧焦效果则是将两种颜色混合,再用 pow 函数处理后,与原纹理颜色混合后的结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 // Upgrade NOTE: replaced '_Object2World' with 'unity_ObjectToWorld' // Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Custom/Chapter15/Chapter15-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 { 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)); fixed t = 1 - smoothstep(0.0, _LineWidth, burn.r - _BurnAmount); fixed3 burnColor = lerp(_BurnFirstColor, _BurnSecondColor, t); burnColor = pow(burnColor, 5); UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); fixed3 finalColor = lerp(ambient + diffuse * atten, burnColor, t * step(0.0001, _BurnAmount)); return fixed4(finalColor, 1); } ENDCG } // Pass to render object as a shadow caster 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" }
_BurnAmount 属性用于控制消融程度,当值为 0 时,物体为正常效果,当值为 1 时,物体会完全消融。_LineWidth 属性用于控制模拟烧焦效果时的线宽,它的值越大,火焰边缘的蔓延范围越广。_MainTex 和 _BumpMap 分别对应了物体原本的漫反射纹理和法线纹理。_BurnFirstColor 和 _BurnSecondColor 对应了火焰边缘的两种颜色值。_BurnMap 则是关键的噪声纹理。
为了得到正确的光照,我们设置了 Pass 的 LightMode 和 multi_compile_fwdbase 的编译指令。值得注意的是,我们还使用 Cull 命令关闭了该 Shader 的面片剔除,也就是说,模型的正面和背面都会被渲染。这是因为,消融会导致裸露模型内部的构造,如果只渲染正面会出现错误的结果。
顶点着色器的代码很常规。我们使用宏 TRANSFORM_TEX 计算了三张纹理对应的纹理坐标,再把光源方向从模型空间变换到了切线空间。最后,为了得到阴影信息,计算了世界空间下的顶点位置和阴影纹理的采样坐标(使用了 TRANSFER_SHADOW 宏)。
在片元着色器中,我们首先对噪声纹理进行采样,并将采样结果和用于控制消融程度的属性 _BurnAmount 相减,传递给 clip 函数。当结果小于 0 时,该像素将会被剔除,从而不会显示到屏幕上。如果通过了测试,则进行正常的光照计算。我们首先根据漫反射纹理得到材质的反射率 albedo,并由此计算得到环境光照,进而得到漫反射光照。然后,我们计算了烧焦颜色 burnColor。我们想要在宽度为 _LineWidth 的范围内模拟一个烧焦的颜色变化,第一步就使用了 smoothstep 函数来计算混合系数 t。当 t 值为 1 时,表明该像素位于消融的边界处,当 t 值为 0 时,表明该像素为正常的模型颜色,而中间的插值则表示需要模拟一个烧焦效果。我们首先用 t 来混合两种火焰颜色 _BurnFirstColor 和 _BurnSecondColor,为了让效果更接近烧焦的痕迹,我们还使用 pow 函数对结果进行处理。然后,我们再次使用 t 来混合正常的光照颜色(环境光+漫反射)和烧焦颜色。我们这里又使用了 step 函数来保证当 _BurnAmount 为 0 时,不显示任何消融效果。最后,返回混合后的颜色值 finalColor。
使用透明度测试的物体的阴影需要特别处理,如果仍然使用普通的阴影 Pass,那么被剔除的区域仍然会向其他物体投射阴影,造成“穿帮”。为了让物体的阴影也能配合透明度测试产生正确的效果,我们自定义了一个投射阴影的 Pass。
在 Unity 中,用于投射阴影的 Pass 的 LightMode 需要被设置为 ShadowCaster,同时,还需要使用 #pragma multi_compile_shadowcaster 指明它需要的编译指令。
阴影投射的重点在于我们需要按正常 Pass 的处理来剔除片元或进行顶点动画,以便阴影可以和物体正常渲染的结果相匹配。在自定义的阴影投射的 Pass 中,我们通常会使用 Unity 提供的内置宏 V2F_SHADOW_CASTER、TRANSFER_SHADOW_CASTER_NORMALOFFSET(旧版本中会使用 TRANSFER_SHADOW_CASTER)和 SHADOW_CASTER_FRAGMENT 来帮助我们计算阴影投射时需要的各种变量,而我们可以只关注自定义计算的部分。在上面的代码中,我们首先在 v2f 结构体中利用 V2F_SHADOW_CASTER 来定义阴影投射需要定义的变量。随后,在顶点着色器中,我们使用 TRANSFER_SHADOW_CASTER_NORMALOFFSET 来填充 V2F_SHADOW_CASTER 在背后声明的一些变量,这是由 Unity 在背后为我们完成的。我们需要在顶点着色器中关注自定义的计算部分,这里指的就是我们需要计算噪声纹理的采样坐标 uvBurnMap。在片元着色器中,我们首先按之前的处理方法使用噪声纹理的采样结果来剔除片元,最后再利用 SHADOW_CASTER_FRAGMENT 来让 Unity 为我们完成阴影投射的部分,把结果输出到深度图和阴影映射纹理中。
通过 Unity 提供的这三个内置宏(在 UnityCG.cginc 文件中被定义),我们可以方便地自定义需要的阴影投射的 Pass,但由于这些宏需要使用一些特定的输入变量,因此我们需要保证为它们提供了这些变量。例如 TRANSFER_SHADOW_CASTER_NORMALOFFSET 会使用名称 v 作为输入结构体,v 中需要包含顶点位置 v.vertex 和顶点法线 v.normal 的信息,我们可以直接使用内置的 appdata_base 结构体,它包含了这些必须的顶点变量。如果我们需要进行顶点动画,可以在顶点着色器中直接修改 v.vertex,再传递给 TRANSFER_SHADOW_CASTER_NORMALOFFSET 即可。
水波效果
在模拟实时水面的过程中,我们往往也会使用噪声纹理,此时,噪声纹理通常会用作一个高度图,以不断修改水面的法线方向。为了模拟水不断流动的效果,我们会使用和时间相关的变量来对噪声纹理进行采样,当得到法线信息后,再进行正常的反射+折射计算,得到最后的水面波动效果。
下面,我们将会使用一个由噪声纹理得到的法线贴图,实现一个包含菲涅耳反射的水面效果,如下图所示
我们使用一张立方体纹理(Cubemap)作为环境纹理,模拟反射。为了模拟折射效果,我们使用 GrabPass 来获取当前屏幕的渲染纹理,并使用切线空间下的法线方向对像素的屏幕坐标进行偏移,再使用该坐标对渲染纹理进行屏幕采样,从而模拟近似的折射效果。水波的法线纹理由一张噪声纹理生成而得,而且会随着时间变化不断平移,模拟波光粼粼的效果。我们没有使用一个定值来混和反射和折射颜色,而是使用之前提到的菲涅耳系数来动态决定混合系数。我们使用如下公式来计算菲涅耳系数:
$$
fresnel = pow(1 - max(0, v \cdot n), 4)
$$
其中,$v$ 和 $n$ 分别对应了视角方向和法线方向。它们之间的夹角越小,$fresnel$ 值越小,反射越弱,折射越强。菲涅耳系数还经常会用于边缘光照的计算中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 Shader "Custom/Chapter15/Chapter15-WaterWave" { 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" {} _WaveXSpeed ("Wave Horizontal Speed", Range(-0.1, 0.1)) = 0.01 _WaveYSpeed ("Wave Vertical Speed", Range(-0.1, 0.1)) = 0.01 } SubShader { // We must be transparent, so other objects are drawn before this one. Tags { "Queue"="Transparent" "RenderType"="Opaque" } // This pass grabs the screen behind the object into a texture. // We can access the result in the next pass as _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); // Get the normal in tangent space 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); // Compute the offset in tangent space 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; // Convert the normal to world space 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; fixed fresnel = pow(1 - saturate(dot(viewDir, bump)), 4); fixed3 finalColor = reflCol * fresnel + refrCol * (1 - fresnel); return fixed4(finalColor, 1); } ENDCG } } FallBack "Diffuse" }
其中,_Color 用于控制水面颜色;_MainTex 是水面波纹材质纹理,默认为白色纹理;_WaveMap 是一个由噪声纹理生成的法线纹理;_Cubemap 是用于模拟反射的立方体纹理;_Distortion 则用于控制模拟折射时图像的扭曲程度;_WaveXSpeed 和 _WaveYSpeed 分别用于控制法线纹理在 X 和 Y 方向上的平移速度。
我们首先在 SubShader 的标签中将渲染队列设置成 Transparent,并把后面的 RenderType 设置为 Opaque。把 Queue 设置成 Transparent 可以确保该物体渲染时,其他所有不透明物体都已经被渲染到屏幕上了,否则就可能无法正确得到“透过水面看到的图像”。而设置 RenderType 则是为了在使用着色器替换(Shader Replacement)时,该物体可以在需要时被正确渲染。这通常发生在我们需要得到摄像机的深度和法线纹理时。随后,我们通过关键词 GrabPass 定义了一个抓取屏幕图像的 Pass。在这个 Pass 中我们定义了一个字符串,该字符串内部的名称决定了抓取得到的屏幕图像将会被存入哪个纹理中。
需要注意的是,我们还定义了 _RefractionTex 和 _RefractionTex_TexelSize 变量,这对应了在使用 GrabPass 时,指定的纹理名称。_RefractionTex_TexelSize 可以让我们得到该纹理的纹素大小,例如一个大小为 256 $\times$ 512 的纹理,它的纹素大小为(1/256, 1/512)。我们需要在对屏幕图像的采样坐标进行偏移时使用该变量。
在顶点着色器中,进行了必要的顶点坐标变换后,我们通过调用 ComputeGrabScreenPos 来得到对应被抓取屏幕图像的采样坐标。我们可以在 UnityCG.cginc 文件中找到它的声明,它的主要代码和 ComputeScreenPos 基本类似,最大的不同时针对平台差异造成的采样坐标问题进行了处理。接着,我们计算了 _MainTex 和 _BumpMap 的采样坐标,并把它们分别存储在一个 float4 类型变量的 xy 和 zw 分量中。由于我们需要在片元着色器中把法线方向从切线空间(由法线纹理采样得到)变换到世界空间下,以便对 Cubemap 进行采样,因此,我们需要在这里计算该顶点对应的从切线空间到世界空间的变换矩阵,并把该矩阵的每一行分别存储在 TtoW0、TtoW1 和 TtoW2 的 xyz 分量中。这里面使用的数学方法就是,得到切线空间下的 3 个坐标轴(x、y、z轴分别对应了切线、副切线和法线的方向)在世界空间下的表示,再把它们依次按列组成一个变换矩阵即可。TtoW0 等值的 w 分量同样被利用起来,用于存储世界空间下的顶点坐标。
在片元着色器中,我们首先通过 TtoW0 等变量的 w 分量得到世界坐标,并用该值得到该片元对应的视角方向。除此之外,我们还使用内置的 _Time.y 变量和 _WaveXSpeed、_WaveYSpeed 属性计算了法线纹理的当前偏移量,并利用该值对法线纹理进行两次采样(这是为了模拟两层交叉的水面波动的效果),对两次结果相加并归一化后得到切线空间下的法线方向。然后,我们使用该值和 _Distortion 属性以及 _RefractionTex_TexelSize 来对屏幕图像的采样坐标进行偏移,模拟折射效果。_Distortion 值越大,偏移量越大,水面背后的物体看起来变形程度越大。在这里,我们选择使用切线空间下的法线方向来进行偏移,是因为该空间下的法线可以反应顶点局部空间下的法线方向。需要注意的是,在计算偏移后的屏幕坐标时,我们把偏移量和屏幕坐标的 z 分量相乘,这是为了模拟深度越大、折射程度越大的效果。如果我们不希望产生这样的效果,可以直接把偏移值叠加到屏幕坐标上。随后,我们对 scrPos 进行了透视除法,再使用该坐标对抓取的屏幕图像 _RefractionTex 进行采样,得到模拟的折射颜色。
之后,我们把法线方向从切线空间变换到了世界空间下(使用变换矩阵的每一行,即 TtoW0、TtoW1 和 TtoW2,分别和法线方向点乘,构成新的法线方向),并据此得到视角方向相对于法线方向的反射方向。随后,使用反射方向对 Cubemap 进行采样,并把结果和主纹理颜色相乘后得到反射颜色。我们也对主文理进行了纹理动画,以模拟水波的效果。
为了混合折射和反射颜色,我们随后计算了菲涅耳系数。我们使用之前的公式来计算菲涅耳系数,并据此来混合折射和反射颜色,作为最终的输出颜色。
我们使用的噪声纹理如下图所示。我们需要的是一张法线纹理,因此我们可以从该噪声纹理的灰度值中生成需要的法线信息,这是通过在它的纹理面板中把纹理类型设置为 Normal map ,并选中 Create from grayscale 来完成的。最后生成的法线纹理如下图的右图所示。
再谈全局雾效
我们在之前讲到了如何使用深度纹理来实现一种基于屏幕后处理的全局雾效。我们由深度纹理重建每个像素在世界空间下的位置,再使用一个基于高度的公式来计算雾效的混合系数,最后使用该系数来混合雾的颜色和原屏幕颜色。上次实现的效果是一个基于高度的均匀雾效,即在同一个高度上,雾的浓度是相同的,如下图的左图所示,然而,一些时候我们希望可以模拟一种不均匀的雾效,同时让雾不断飘动,使雾看起来更加飘渺,如下图的右图所示。而这就可以通过使用一张噪声纹理来实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 using UnityEngine;using System.Collections;public class FogWithNoise : PostEffectsBase { public Shader fogShader; private Material fogMaterial = null ; public Material material { get { fogMaterial = CheckShaderAndCreateMaterial(fogShader, fogMaterial); return fogMaterial; } } private Camera myCamera; public Camera camera { get { if (myCamera == null ) { myCamera = GetComponent<Camera>(); } return myCamera; } } private Transform myCameraTransform; public Transform cameraTransform { get { if (myCameraTransform == null ) { myCameraTransform = camera.transform; } return myCameraTransform; } } [Range(0.1f, 3.0f) ] public float fogDensity = 1.0f ; public Color fogColor = Color.white; public float fogStart = 0.0f ; public float fogEnd = 2.0f ; public Texture noiseTexture; [Range(-0.5f, 0.5f) ] public float fogXSpeed = 0.1f ; [Range(-0.5f, 0.5f) ] public float fogYSpeed = 0.1f ; [Range(0.0f, 3.0f) ] public float noiseAmount = 1.0f ; void OnEnable () { GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth; } void OnRenderImage (RenderTexture src, RenderTexture dest ) { if (material != null ) { Matrix4x4 frustumCorners = Matrix4x4.identity; float fov = camera.fieldOfView; float near = camera.nearClipPlane; float aspect = camera.aspect; float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad); Vector3 toRight = cameraTransform.right * halfHeight * aspect; Vector3 toTop = cameraTransform.up * halfHeight; Vector3 topLeft = cameraTransform.forward * near + toTop - toRight; float scale = topLeft.magnitude / near; topLeft.Normalize(); topLeft *= scale; Vector3 topRight = cameraTransform.forward * near + toRight + toTop; topRight.Normalize(); topRight *= scale; Vector3 bottomLeft = cameraTransform.forward * near - toTop - toRight; bottomLeft.Normalize(); bottomLeft *= scale; Vector3 bottomRight = cameraTransform.forward * near + toRight - toTop; bottomRight.Normalize(); bottomRight *= scale; frustumCorners.SetRow(0 , bottomLeft); frustumCorners.SetRow(1 , bottomRight); frustumCorners.SetRow(2 , topRight); frustumCorners.SetRow(3 , topLeft); material.SetMatrix("_FrustumCornersRay" , frustumCorners); material.SetFloat("_FogDensity" , fogDensity); material.SetColor("_FogColor" , fogColor); material.SetFloat("_FogStart" , fogStart); material.SetFloat("_FogEnd" , fogEnd); material.SetTexture("_NoiseTex" , noiseTexture); material.SetFloat("_FogXSpeed" , fogXSpeed); material.SetFloat("_FogYSpeed" , fogYSpeed); material.SetFloat("_NoiseAmount" , noiseAmount); Graphics.Blit (src, dest, material); } else { Graphics.Blit(src, dest); } } }
fogDensity 用于控制雾的浓度,fogColor 用于控制雾的颜色。我们使用的雾效模拟函数是基于高度的,因此参数 fogStart 用于控制雾效的起始高度,fogEnd 用于控制雾效的终止高度。noiseTexture 是我们使用的噪声纹理,fogXSpeed 和 fogYSpeed 分别对应了噪声纹理在 X 和 Y 方向上的移动速度,以此来模拟雾的飘动效果。最后,noiseAmount 用于控制噪声程度,当 noiseAmount 为 0 时,表示不应用任何噪声,即得到一个均匀的基于高度的全局雾效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 Shader "Custom/Chapter15/Chapter15-FogWithNoise" { 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" {} _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; 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; #if UNITY_UV_STARTS_AT_TOP 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; } #if UNITY_UV_STARTS_AT_TOP if (_MainTex_TexelSize.y < 0) index = 3 - index; #endif o.interpolatedRay = _FrustumCornersRay[index]; return o; } fixed4 frag(v2f i) : SV_Target { float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth)); float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz; float2 speed = _Time.y * float2(_FogXSpeed, _FogYSpeed); float noise = (tex2D(_NoiseTex, i.uv + speed).r - 0.5) * _NoiseAmount; float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart); 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 }
在片元着色器中,我们首先根据深度纹理来重建该像素在世界空间中的位置。然后,我们利用内置的 _Time.y 变量和 _FogXSpeed、_FogYSpeed 属性计算出当前噪声纹理的偏移量,并据此对噪声纹理进行采样,得到噪声值。随后,我们把该噪声值添加到雾效浓度的计算中,得到应用噪声后的雾效混合系数 fogDensity。最后,我们使用该系数将雾的颜色和原始颜色进行混合后返回。