GameTech/URP 渲染实战
源码 / Ch15/FlatShading.shader
Ch15/FlatShading.shader
// 低多边形(Low Poly)flat shading:不改网格,在片元里用 ddx/ddy 求三角形的面法线。
// 演示场景是一块在顶点着色器里用噪声抬高的地形,可以在“平滑法线 / 面法线”之间切换。
Shader "Cookbook/Ch15/FlatShading"
{
    Properties
    {
        _BaseColor ("Base Color (non-terrain)", Color) = (0.9, 0.55, 0.3, 1)
        [Toggle] _Flat ("Flat Shading (ddx/ddy)", Float) = 1
        [Toggle] _Terrain ("Terrain Mode", Float) = 0
        [NoScaleOffset] _HeightTex ("Height Noise", 2D) = "gray" {}
        _HeightScale ("Height", Range(0, 4)) = 1.6
        _NoiseTiling ("Noise Tiling", Float) = 0.08
        _WaterLevel ("Water Level", Float) = -0.6
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        CBUFFER_START(UnityPerMaterial)
            half4 _BaseColor;
            float _Flat, _Terrain, _HeightScale, _NoiseTiling, _WaterLevel;
        CBUFFER_END
        TEXTURE2D(_HeightTex); SAMPLER(sampler_HeightTex);

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

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

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForwardOnly" }
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
            #include "Assets/Cookbook/Ch15/NPRCommon.hlsl"

            NprVaryings vert(NprAttributes v)
            {
                NprVaryings o;
                float3 positionWS = TransformObjectToWorld(v.positionOS.xyz);
                float3 normalWS = TransformObjectToWorldNormal(v.normalOS);
                Deform(positionWS, normalWS);
                o.positionWS = positionWS;
                o.normalWS = normalWS;
                o.positionCS = TransformWorldToHClip(positionWS);
                o.uv = v.uv; o.color = v.color; o.fogFactor = 0;
                return o;
            }

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

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

            half4 frag(NprVaryings i) : SV_Target
            {
                float3 N = _Flat > 0.5 ? FaceNormal(i.positionWS) : normalize(i.normalWS);
                NprLight L = GetNprLight(i.positionWS, N);
                half3 albedo = _Terrain > 0.5 ? TerrainColor(i.positionWS.y, N) : _BaseColor.rgb;
                half3 col = albedo * (saturate(L.ndotl) * L.shadow * L.color + SampleSH(N));
                return half4(col, 1);
            }
            ENDHLSL
        }

        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            ZWrite On ZTest LEqual ColorMask 0
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment NprNullFrag
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            #include "Assets/Cookbook/Ch15/NPRCommon.hlsl"
            float4 vert(NprShadowAttributes v) : SV_POSITION
            {
                float3 positionWS = TransformObjectToWorld(v.positionOS.xyz);
                float3 normalWS = TransformObjectToWorldNormal(v.normalOS);
                Deform(positionWS, normalWS);   // 阴影要和颜色 Pass 用同样的变形
                float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, _LightDirection));
            #if UNITY_REVERSED_Z
                positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #else
                positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #endif
                return positionCS;
            }
            ENDHLSL
        }

        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode"="DepthOnly" }
            ZWrite On ColorMask R
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment NprNullFrag
            #include "Assets/Cookbook/Ch15/NPRCommon.hlsl"
            float4 vert(NprShadowAttributes v) : SV_POSITION
            {
                float3 positionWS = TransformObjectToWorld(v.positionOS.xyz);
                float3 normalWS = TransformObjectToWorldNormal(v.normalOS);
                Deform(positionWS, normalWS);
                return TransformWorldToHClip(positionWS);
            }
            ENDHLSL
        }
    }
}