GameTech/URP 渲染实战
源码 / shaders/Ch07/HeightToNormal.glsl
shaders/Ch07/HeightToNormal.glsl
// 不用法线贴图,直接从高度图算法线:①中心差分(多采样 4 次)②屏幕导数 dFdx/dFdy(只采样 1 次)。(GLSL 版,对应 Ch07/HeightToNormal.shader)
// 模式 0 是烘焙好的法线贴图,作为参照。
Shader "Cookbook/Ch07/HeightToNormal"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        GLSLINCLUDE
        uniform vec4  _BaseMap_ST;
        uniform vec4  _HeightMap_TexelSize;   // 引擎自动填充:(1/宽, 1/高, 宽, 高)
        uniform float _HeightStrength;
        uniform float _Mode;
        ENDGLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }
            GLSLPROGRAM
            #include "Ch07/Ch07Common.glsl"

            uniform sampler2D _BaseMap;
            uniform sampler2D _BumpMap;
            uniform sampler2D _HeightMap;

            VARYING vec2 uv;
            VARYING vec3 positionWS;
            VARYING vec3 normalWS;
            VARYING vec4 tangentWS;

            #ifdef VERTEX
            void main()
            {
                VertexPositionInputs pos = GetVertexPositionInputs(positionOS.xyz);
                VertexNormalInputs nrm = GetVertexNormalInputs(normalOS, tangentOS);
                gl_Position = pos.positionCS;
                positionWS = pos.positionWS;
                uv = TransformTex(texcoord0.xy, _BaseMap_ST);
                normalWS = nrm.normalWS;
                tangentWS = vec4(nrm.tangentWS, tangentOS.w * GetOddNegativeScale());
            }
            #endif

            #ifdef FRAGMENT
            float SampleHeight(vec2 uv) { return texture(_HeightMap, uv).r; }

            // 中心差分:在 U、V 方向各取左右两个邻居。结果与离线烘焙法线贴图的算法一致。
            vec3 NormalFromHeightCentral(vec2 uv)
            {
                vec2 t = _HeightMap_TexelSize.xy;
                float hL = SampleHeight(uv - vec2(t.x, 0));
                float hR = SampleHeight(uv + vec2(t.x, 0));
                float hD = SampleHeight(uv - vec2(0, t.y));
                float hU = SampleHeight(uv + vec2(0, t.y));
                // 每个纹素的高度差 × 强度 = 斜率;法线 = normalize(-dh/du, -dh/dv, 1)
                float dhdu = (hR - hL) * 0.5 * _HeightStrength;
                float dhdv = (hU - hD) * 0.5 * _HeightStrength;
                return normalize(vec3(-dhdu, -dhdv, 1));
            }

            // 屏幕导数:dFdx/dFdy 给出“相邻像素之间”h 和 uv 的变化量,解一个 2×2 线性方程
            // 就能把屏幕空间的变化换算成 dh/du、dh/dv。只采样一次,但精度受 2×2 像素块限制。
            vec3 NormalFromHeightDerivative(vec2 uv)
            {
                float h = SampleHeight(uv);
                vec2 duvdx = dFdx(uv), duvdy = dFdy(uv);
                float dhdx = dFdx(h), dhdy = dFdy(h);
                // [duvdx; duvdy] · (dh/du, dh/dv) = (dhdx, dhdy),用克拉默法则求解
                float det = duvdx.x * duvdy.y - duvdx.y * duvdy.x;
                vec2 grad = vec2(dhdx * duvdy.y - dhdy * duvdx.y,
                                 duvdx.x * dhdy - duvdy.x * dhdx) / (abs(det) > 1e-12 ? det : 1e-12);
                grad *= _HeightMap_TexelSize.xy * _HeightStrength;   // 每 UV 单位 → 每纹素
                return normalize(vec3(-grad, 1));
            }

            void main()
            {
                vec3 normalTS;
                if (_Mode < 0.5)      normalTS = UnpackNormal(texture(_BumpMap, uv));
                else if (_Mode < 1.5) normalTS = NormalFromHeightCentral(uv);
                else                  normalTS = NormalFromHeightDerivative(uv);

                vec3 bitangent = tangentWS.w * cross(normalWS, tangentWS.xyz);
                vec3 nWS = NormalizeNormalPerPixel(
                    TransformTangentToWorld(normalTS, mat3(tangentWS.xyz, bitangent, normalWS)));
                vec3 albedo = texture(_BaseMap, uv).rgb;
                fragColor = vec4(Ch07Shade(positionWS, nWS, albedo, 0.45, 0.15, 1.0), 1.0);
            }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            GLSLPROGRAM
            #define CH07_SHADOW_PASS
            #include "Ch07/Ch07Common.glsl"
            ENDGLSL
        }
        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            GLSLPROGRAM
            #define CH07_DEPTH_PASS
            #include "Ch07/Ch07Common.glsl"
            ENDGLSL
        }
    }
}