GameTech/URP 渲染实战

案例 3.5接入雾效入门

目标:让 Shader 响应 Window › Rendering › Lighting › Environment › Fog 的设置,远处的物体逐渐融入雾色。

URP 支持 Built-in 时代的三种雾模式。设物体到相机的深度为 z(沿视线方向的距离),雾因子 f 表示“保留多少原色”(1 = 完全不受雾影响,0 = 完全是雾色):

Linear:f=end−zend−startExponential:f=e−density·zExponential Squared:f=e−(density·z)2最终颜色:color=lerp(fogColor,color,f)
到相机的深度 zf=10 start end Linear Exponential Exp Squared
图 3-8三种雾因子随距离的变化(示意)。线性雾在 start 之前完全清晰、end 之后完全是雾;指数雾从相机处就开始衰减;平方指数雾近处更清晰、到一定距离后迅速变浓

落实到代码只需要三处改动:

Assets/Cookbook/Ch03/C03_05_Fog.shader(节选:pragma)
// 生成 无雾 / FOG_LINEAR / FOG_EXP / FOG_EXP2 四个变体,引擎按 Lighting 设置选用
#pragma multi_compile_fog
// 生成 无雾 / FOG_LINEAR / FOG_EXP / FOG_EXP2 四个变体,引擎按 Lighting 设置选用
// (网页版:雾模式由 uniform unity_FogMode 在运行时选择,不需要变体,这一行留着只为和 HLSL 对照)
#pragma multi_compile_fog

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;  // 每个顶点算一次雾因子,交给插值器
};
VARYING vec2 uv;
VARYING vec4 vColor;
VARYING float fogFactor;  // 每个顶点算一次雾因子,交给插值器
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 实现):指数函数不是线性的,先插值“指数”再求指数,比直接插值指数结果更准确。

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