GameTech/URP 渲染实战
源码 / shaders/Ch15/FlatShading.glsl
shaders/Ch15/FlatShading.glsl
// 低多边形(Low Poly)flat shading:不改网格,在片元里用 ddx/ddy 求三角形的面法线。(GLSL 版,对应 Ch15/FlatShading.shader)
// 演示场景是一块在顶点着色器里用噪声抬高的地形,可以在“平滑法线 / 面法线”之间切换。
Shader "Cookbook/Ch15/FlatShading"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        GLSLINCLUDE
        uniform vec4 _BaseColor;
        uniform float _Flat, _Terrain, _HeightScale, _NoiseTiling, _WaterLevel;
        uniform sampler2D _HeightTex;

        // 地形高度:世界 xz 采样噪声。顶点着色器里必须用 LOD 版本采样(没有导数可用)
        float TerrainHeight(vec2 xz)
        {
            float h = textureLod(_HeightTex, xz * _NoiseTiling, 0.0).r;
            h = (h - 0.45) * _HeightScale * 2.0;
            return max(h, _WaterLevel);     // 低于水位的部分压平成水面
        }

        // 所有 Pass 共用的顶点变形:地形模式下抬高顶点,并用有限差分算出平滑法线
        void Deform(inout vec3 posWS, inout vec3 nrmWS)
        {
            if (_Terrain < 0.5) return;
            const float e = 0.05;
            float h  = TerrainHeight(posWS.xz);
            float hx = TerrainHeight(posWS.xz + vec2(e, 0));
            float hz = TerrainHeight(posWS.xz + vec2(0, e));
            posWS.y += h;
            nrmWS = normalize(vec3(h - hx, e, h - hz));
        }
        ENDGLSL

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }
            GLSLPROGRAM
            #define NPR_VARYINGS
            #include "Ch15/NPRCommon.glsl"

            #ifdef VERTEX
            void main()
            {
                vec3 posWS = TransformObjectToWorld(positionOS.xyz);
                vec3 nrmWS = TransformObjectToWorldNormal(normalOS);
                Deform(posWS, nrmWS);
                positionWS = posWS;
                normalWS = nrmWS;
                gl_Position = TransformWorldToHClip(posWS);
                uv = texcoord0.xy; vertexColor = color; fogFactor = 0.0;
            }
            #endif

            #ifdef FRAGMENT
            vec3 FaceNormal(vec3 posWS)
            {
                // ddx/ddy:相邻像素之间世界坐标的差,两者都躺在三角形平面内,叉乘就是面法线。
                // 同一个 2×2 像素块里共享导数,而三角形内部世界坐标是线性的,所以结果是“整面常量”。
                vec3 n = normalize(cross(dFdy(posWS), dFdx(posWS)));
                // 屏幕 y 轴朝上还是朝下因平台/是否渲染到 RT 而异,叉乘方向可能反过来;
                // 正面一定朝向相机,所以用视线方向把它翻正,最省心
                vec3 V = _WorldSpaceCameraPos - posWS;
                return dot(n, V) < 0.0 ? -n : n;
            }

            vec3 TerrainColor(float y, vec3 N)
            {
                vec3 c = vec3(0.10, 0.32, 0.62);                                           // 水
                c = mix(c, vec3(0.80, 0.68, 0.42), step(_WaterLevel + 0.02, y));           // 沙
                c = mix(c, vec3(0.30, 0.55, 0.18), step(_WaterLevel + 0.25, y));           // 草
                c = mix(c, vec3(0.42, 0.37, 0.33), step(0.3, y) * step(N.y, 0.9));        // 陡坡变岩石
                c = mix(c, vec3(0.92, 0.93, 0.96), step(1.3, y));                          // 雪
                // 上面这些常量是按“显示器上看到的颜色”(sRGB)挑的,线性空间工程里要先转成线性值
                return SRGBToLinear(c);
            }

            vec4 frag()
            {
                vec3 N = _Flat > 0.5 ? FaceNormal(positionWS) : normalize(normalWS);
                NprLight L = GetNprLight(positionWS, N);
                vec3 albedo = _Terrain > 0.5 ? TerrainColor(positionWS.y, N) : _BaseColor.rgb;
                vec3 col = albedo * (saturate(L.ndotl) * L.shadow * L.color + SampleSH(N));
                return vec4(col, 1.0);
            }
            void main() { fragColor = frag(); }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            GLSLPROGRAM
            #include "Ch15/NPRCommon.glsl"
            #ifdef VERTEX
            void main()
            {
                vec3 posWS = TransformObjectToWorld(positionOS.xyz);
                vec3 nrmWS = TransformObjectToWorldNormal(normalOS);
                Deform(posWS, nrmWS);   // 阴影要和颜色 Pass 用同样的变形
                vec4 positionCS = TransformWorldToHClip(ApplyShadowBias(posWS, nrmWS, _LightDirection));
                positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE * positionCS.w);   // WebGL 不反转深度
                gl_Position = positionCS;
            }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = NprNullFrag(); }
            #endif
            ENDGLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            GLSLPROGRAM
            #include "Ch15/NPRCommon.glsl"
            #ifdef VERTEX
            void main()
            {
                vec3 posWS = TransformObjectToWorld(positionOS.xyz);
                vec3 nrmWS = TransformObjectToWorldNormal(normalOS);
                Deform(posWS, nrmWS);
                gl_Position = TransformWorldToHClip(posWS);
            }
            #endif
            #ifdef FRAGMENT
            void main() { fragColor = vec4(gl_FragCoord.z); }
            #endif
            ENDGLSL
        }
    }
}