URP 渲染实战 / 第 3 章 第一个 URP Shader / 案例 3.5
案例 3.5接入雾效入门
目标:让 Shader 响应 的设置,远处的物体逐渐融入雾色。
URP 支持 Built-in 时代的三种雾模式。设物体到相机的深度为 z(沿视线方向的距离),雾因子 f 表示“保留多少原色”(1 = 完全不受雾影响,0 = 完全是雾色):
落实到代码只需要三处改动:
Assets/Cookbook/Ch03/C03_05_Fog.shader(节选:pragma)
multi_compile_fog 是 Unity 提供的快捷写法,等价于 #pragma multi_compile _ FOG_LINEAR FOG_EXP FOG_EXP2,编译出“无雾 + 三种雾”四个变体,运行时引擎根据 Lighting 窗口的设置自动启用对应的关键字。“关键字”与“变体”的完整解释在案例 3.8,这里先知道它在起作用即可。
Assets/Cookbook/Ch03/C03_05_Fog.shader(节选:varyings)
struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; half4 color : COLOR; float fogFactor : TEXCOORD1; // 每个顶点算一次雾因子,交给插值器 };
Assets/Cookbook/Ch03/C03_05_Fog.shader(节选:vertfrag)
Varyings vert (Attributes input) { Varyings output; output.positionCS = TransformObjectToHClip(input.positionOS.xyz); output.uv = TRANSFORM_TEX(input.uv, _BaseMap); output.color = input.color; // 用裁剪空间 z 换算出“离相机多远”,再按雾模式算出因子 output.fogFactor = ComputeFogFactor(output.positionCS.z); return output; } half4 frag (Varyings input) : SV_Target { half4 col = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor * input.color; clip(col.a - _Cutoff); // 按因子把颜色往 unity_FogColor 方向混;没开雾时这一行什么都不做 col.rgb = MixFog(col.rgb, input.fogFactor); return half4(col.rgb, 1); }
#ifdef VERTEX void main() { gl_Position = TransformObjectToHClip(positionOS.xyz); uv = TransformTex(texcoord0.xy, _BaseMap_ST); vColor = color; // 用裁剪空间 z 换算出“离相机多远”,再按雾模式算出因子 fogFactor = ComputeFogFactor(gl_Position.z); } #endif #ifdef FRAGMENT void main() { vec4 col = texture(_BaseMap, uv) * _BaseColor * vColor; if (col.a - _Cutoff < 0.0) discard; // 按因子把颜色往 unity_FogColor 方向混;没开雾时这一行什么都不做 col.rgb = MixFog(col.rgb, fogFactor); fragColor = vec4(col.rgb, 1); } #endif
ComputeFogFactor 和 MixFog 都在 URP 的 ShaderVariablesFunctions.hlsl:
// 摘自 URP 14.0.12 ShaderLibrary/ShaderVariablesFunctions.hlsl(节选) real ComputeFogFactorZ0ToFar(float z) { #if defined(FOG_LINEAR) // factor = (end-z)/(end-start) = z * (-1/(end-start)) + (end/(end-start)) float fogFactor = saturate(z * unity_FogParams.z + unity_FogParams.w); return real(fogFactor); #elif defined(FOG_EXP) || defined(FOG_EXP2) return real(unity_FogParams.x * z); #else return real(0.0); #endif } real ComputeFogFactor(float zPositionCS) { float clipZ_0Far = UNITY_Z_0_FAR_FROM_CLIPSPACE(zPositionCS); return ComputeFogFactorZ0ToFar(clipZ_0Far); } half3 MixFog(half3 fragColor, half fogFactor) { return MixFogColor(fragColor, unity_FogColor.rgb, fogFactor); }
读这段源码能学到几件事。第一,UNITY_Z_0_FAR_FROM_CLIPSPACE 把各平台不同的裁剪空间 z(D3D/Metal 是反向 Z,OpenGL 是 [-w, w])统一换算成“0 到远平面”的距离,这类平台差异宏在第 1 章介绍过。第二,Unity 把雾的起止距离、密度预先折算进 unity_FogParams,线性雾被改写成一次乘加,省掉了除法。第三,指数雾在顶点里只算到 density·z 这一步,真正的 exp 放在片元的 MixFog 里(内部用 exp2 实现):指数函数不是线性的,先插值“指数”再求指数,比直接插值指数结果更准确。

URP 14 自己怎么做细心的读者会发现,URP 14.0.12 的
ShaderVariablesFunctions.hlsl 里有一行 #define _FOG_FRAGMENT 1,URP 自带的 Unlit/Lit 因此会把观察空间深度传到片元、在片元里逐像素计算雾因子(InitializeInputDataFog)。逐像素更精确,尤其是对很大的三角形(比如一整块地面只有几个顶点时);逐顶点更省。本书的 Unlit 采用逐顶点因子,它对普通密度的网格足够了;如果你在大面片上看到雾的“条带”,就换成逐像素的写法。天空盒不受雾影响URP 不会给天空盒加雾。雾色和天空差别大时,远处物体会融成一片与天空不同的颜色,所以图 3-9 用了与雾色相同的纯色背景。实际项目里通常让雾色接近地平线天空色,或使用带高度雾的天空方案。