GameTech/URP 渲染实战
源码 / Ch01/SpaceViz.shader
Ch01/SpaceViz.shader
// 第 1 章 案例 1.1:坐标空间可视化
// 同一个顶点,在不同空间里坐标不同。这个调试 Shader 把各空间的坐标“涂”在物体表面,
// 用 _Mode 切换:物体空间 / 世界空间 / 观察空间 / NDC / 裁剪空间 w / 屏幕像素坐标。
Shader "Cookbook/Ch01/SpaceViz"
{
    Properties
    {
        [Enum(Object,0,World,1,View,2,NDC,3,ClipW,4,Pixel,5)] _Mode ("显示哪个空间", Float) = 1
        _GridSize ("网格间距(该空间的单位)", Float) = 1
        _DepthScale ("深度着色范围(米)", Float) = 12
        _PixelCell ("像素棋盘格大小(像素)", Float) = 40
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        Pass
        {
            Name "SpaceViz"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float _Mode;
                float _GridSize;
                float _DepthScale;
                float _PixelCell;
            CBUFFER_END

            struct Attributes
            {
                float4 positionOS : POSITION;
            };

            struct Varyings
            {
                float4 positionCS  : SV_POSITION; // 交给光栅化器:裁剪空间坐标(到了片元里会变成像素坐标)
                float3 positionOS  : TEXCOORD0;   // 物体空间
                float3 positionWS  : TEXCOORD1;   // 世界空间
                float3 positionVS  : TEXCOORD2;   // 观察空间(右手系,相机看向 -Z)
                float4 positionHCS : TEXCOORD3;   // 裁剪空间的一份拷贝:SV_POSITION 在片元阶段已被改写,想要原值得自己传
            };

            Varyings vert(Attributes input)
            {
                Varyings output;
                // URP 一次性算好各个空间的位置(ShaderVariablesFunctions.hlsl)
                VertexPositionInputs pos = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS  = pos.positionCS;
                output.positionOS  = input.positionOS.xyz;
                output.positionWS  = pos.positionWS;
                output.positionVS  = pos.positionVS;
                output.positionHCS = pos.positionCS;
                return output;
            }

            // 抗锯齿网格线:p 落在 size 的整数倍附近时返回 1。
            // fwidth 给出 p 在相邻像素间的变化量,用它做线宽,线在远处也保持约 1 像素宽。
            // 在某个表面上恒定不变的分量(如地面的 y)fwidth 为 0,要排除,否则整面都会被当成“线上”。
            float GridLine(float3 p, float size)
            {
                float3 q = p / size;
                float3 fw = fwidth(q);
                float3 d = abs(frac(q - 0.5) - 0.5) / max(fw, 1e-5);
                d += step(fw, 1e-6) * 1e6;
                return 1.0 - saturate(min(min(d.x, d.y), d.z) - 0.5);
            }

            float GridLine2(float2 p, float size)
            {
                float2 q = p / size;
                float2 d = abs(frac(q - 0.5) - 0.5) / max(fwidth(q), 1e-5);
                return 1.0 - saturate(min(d.x, d.y) - 0.5);
            }

            half4 frag(Varyings input) : SV_Target
            {
                float3 col = 0;
                int mode = (int)_Mode;

                if (mode == 0)          // 物体空间:坐标跟着物体走,旋转缩放物体,图案跟着转
                {
                    col = saturate(input.positionOS + 0.5);
                    col = lerp(col, 0.05, GridLine(input.positionOS, 0.25 * _GridSize));
                }
                else if (mode == 1)     // 世界空间:所有物体共用一套坐标,网格线在物体之间连续
                {
                    float3 cell = frac(input.positionWS / _GridSize);
                    col = 0.25 + 0.6 * cell;
                    col = lerp(col, 0.05, GridLine(input.positionWS, _GridSize));
                    // 世界原点处的 X 轴(红)、Z 轴(蓝)
                    float ax = 1.0 - saturate(abs(input.positionWS.z) / (2.0 * fwidth(input.positionWS.z)));
                    float az = 1.0 - saturate(abs(input.positionWS.x) / (2.0 * fwidth(input.positionWS.x)));
                    col = lerp(col, float3(1, 0.1, 0.1), ax);
                    col = lerp(col, float3(0.1, 0.3, 1), az);
                }
                else if (mode == 2)     // 观察空间:x 向右、y 向上,深度 = -z
                {
                    float depth = -input.positionVS.z;
                    col.rg = saturate(input.positionVS.xy * 0.25 + 0.5);
                    col.b  = saturate(depth / _DepthScale);
                    // 等深线:每 1 米一条,它们都与相机的成像平面平行
                    float iso = abs(frac(depth - 0.5) - 0.5) / max(fwidth(depth), 1e-5);
                    col = lerp(col, 1.0, 1.0 - saturate(iso - 0.5));
                }
                else if (mode == 3)     // NDC:透视除法之后,可见范围 x、y 都在 [-1, 1]
                {
                    float2 ndc = input.positionHCS.xy / input.positionHCS.w;
                    ndc.y *= _ProjectionParams.x;      // 渲染到 RT 时投影矩阵被上下翻转过,这里翻回来,让 +y 永远朝上
                    col = float3(saturate(ndc.x), saturate(ndc.y), saturate(-ndc.x) * 0.7 + saturate(-ndc.y) * 0.3);
                    col = lerp(col, 0.0, GridLine2(ndc, 0.25));
                    col = lerp(col, 1.0, GridLine2(ndc, 1.0));   // 中心十字(x=0、y=0)
                }
                else if (mode == 4)     // 裁剪空间的 w:透视投影下它就是观察空间深度
                {
                    float w = input.positionHCS.w;
                    col = saturate(w / _DepthScale).xxx;
                    float iso = abs(frac(w - 0.5) - 0.5) / max(fwidth(w), 1e-5);
                    col = lerp(col, float3(1, 0.45, 0.2), 1.0 - saturate(iso - 0.5));
                }
                else                    // 片元阶段的 SV_POSITION.xy:像素中心坐标(如 100.5, 37.5)
                {
                    float2 cell = floor(input.positionCS.xy / _PixelCell);
                    float k = fmod(cell.x + cell.y, 2.0);
                    col = lerp(float3(0.9, 0.9, 0.92), float3(0.25, 0.45, 0.6), k);
                }
                return half4(col, 1);
            }
            ENDHLSL
        }
    }
}