GameTech/URP 渲染实战

案例 7.3三种常见错误的实拍对比入门

目标:把“手写 rgb*2−1”和“Y 方向用反”这两个最常见的错误放在同一张图里,记住它们的长相,以后一眼就能认出来。

Assets/Cookbook/Ch07/NormalMistakes.shader(节选:frag)
half4 frag(Varyings i) : SV_Target
{
    half4 packed = SAMPLE_TEXTURE2D(_BumpMap, sampler_BumpMap, i.uv);
    half3 normalTS;
    if (_Mistake > 0.5 && _Mistake < 1.5)
    {
        // 错误 1:照搬“rgb*2-1”。桌面平台法线贴图默认压成 DXT5nm/BC5,
        // 数据在 A/G(或 R/G)通道里,B 通道是 0,这样解出来的向量完全不对
        normalTS = packed.rgb * 2.0 - 1.0;
        normalTS.xy *= _BumpScale;
    }
    else
    {
        normalTS = UnpackNormalScale(packed, _BumpScale);
        // 错误 2:贴图按 DirectX 约定(Y 朝下)烘焙,却直接给 Unity(OpenGL 约定,Y 朝上)用
        if (_Mistake > 1.5 && _Mistake < 2.5) normalTS.y = -normalTS.y;
    }

    float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz);
    half3 normalWS = TransformTangentToWorld(normalTS, half3x3(i.tangentWS.xyz, bitangent, i.normalWS));
    // 错误 3:忘了归一化(插值后的 TBN 和缩放过的 normalTS 都不是单位长度)
    if (_Mistake < 2.5) normalWS = normalize(normalWS);

    half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb;
    return half4(Ch07Shade(i.positionWS, normalWS, albedo, 0.45, 0.15, 1), 1);
}
#ifdef FRAGMENT
void main()
{
    vec4 packed = texture(_BumpMap, uv);
    vec3 normalTS;
    if (_Mistake > 0.5 && _Mistake < 1.5)
    {
        // 错误 1:照搬“rgb*2-1”。桌面平台法线贴图默认压成 DXT5nm/BC5,
        // 数据在 A/G(或 R/G)通道里,B 通道是 0,这样解出来的向量完全不对。
        // (网页版的法线贴图是未压缩的 RGB,这里先把 B 清零,模拟 Unity 桌面平台 BC5 采样到的数据)
        packed.b = 0.0;
        normalTS = packed.rgb * 2.0 - 1.0;
        normalTS.xy *= _BumpScale;
    }
    else
    {
        normalTS = UnpackNormalScale(packed, _BumpScale);
        // 错误 2:贴图按 DirectX 约定(Y 朝下)烘焙,却直接给 Unity(OpenGL 约定,Y 朝上)用
        if (_Mistake > 1.5 && _Mistake < 2.5) normalTS.y = -normalTS.y;
    }

    vec3 bitangent = tangentWS.w * cross(normalWS, tangentWS.xyz);
    vec3 nWS = TransformTangentToWorld(normalTS, mat3(tangentWS.xyz, bitangent, normalWS));
    // 错误 3:忘了归一化(插值后的 TBN 和缩放过的 normalTS 都不是单位长度)
    if (_Mistake < 2.5) nWS = normalize(nWS);

    vec3 albedo = texture(_BaseMap, uv).rgb;
    fragColor = vec4(Ch07Shade(positionWS, nWS, albedo, 0.45, 0.15, 1.0), 1.0);
}
#endif
图 7-7左:正确解码;中:手写 rgb*2-1,在 DXT5nm 存储下 R 恒为 1、B 恒为 0,解出的法线朝一侧倒并且 z 为负,整体发暗、凹凸几乎消失;右:Y 方向取反,光从左上方照来,砖块却是下沿亮、上沿暗,凸起看起来像凹进墙里。

中图的错误在编辑器里尤其隐蔽:如果你在 Android 上测试(可能走 ASTC 或 UNITY_NO_DXT5nm),手写的 rgb*2-1 有时“看起来是对的”,换到 PC 上就坏掉。永远使用 UnpackNormal 系列函数,让引擎根据平台选择解码路径。右图的错误则常发生在跨软件协作时,解决办法是在源头统一约定,而不是在 Shader 里加一堆开关。第三种错误“忘记归一化”在 _Mistake = 3 时可以看到:强度越大、表面越亮,高光也跟着变形,原理很简单——点积的结果被向量长度放大了。