GameTech/URP 渲染实战
源码 / Ch04/C04_01_FunctionPlot.shader
Ch04/C04_01_FunctionPlot.shader
// 案例 4.1:在 Shader 里画函数曲线 —— 调试数学函数最直观的办法。
Shader "Cookbook/Ch04/FunctionPlot"
{
    Properties
    {
        [Enum(Steps,0,Repeat,1,Shapes,2)] _Mode ("Curve Set", Float) = 0
        _View ("View (xMin, xMax, yMin, yMax)", Vector) = (-0.2, 1.2, -0.2, 1.2)
        _LineWidth ("Line Width (px)", Float) = 3
        _ColorA ("Curve A", Color) = (0.91, 0.45, 0.23, 1)
        _ColorB ("Curve B", Color) = (0.12, 0.42, 0.56, 1)
        _ColorC ("Curve C", Color) = (0.18, 0.49, 0.34, 1)
        _Paper ("Paper", Color) = (0.98, 0.98, 0.97, 1)
    }

    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

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

            HLSLPROGRAM
            #pragma vertex ProceduralVert
            #pragma fragment frag
            #include "Assets/Cookbook/Ch04/ProceduralCommon.hlsl"

            CBUFFER_START(UnityPerMaterial)
                float  _Mode;
                float4 _View;
                float  _LineWidth;
                half4  _ColorA, _ColorB, _ColorC, _Paper;
            CBUFFER_END

            // 第 k 条曲线 y = f(x)。改这里就能画任意函数
            float Curve(int mode, int k, float x)
            {
                if (mode == 0)          // 阈值类:step / smoothstep / 线性 step
                {
                    if (k == 0) return step(0.5, x);
                    if (k == 1) return smoothstep(0.2, 0.8, x);
                    return LinearStep(0.2, 0.8, x);
                }
                if (mode == 1)          // 重复类:floor 阶梯 / frac 锯齿 / 三角波
                {
                    if (k == 0) return floor(x * 4.0) / 4.0;
                    if (k == 1) return frac(x * 2.0);
                    return abs(frac(x) - 0.5) * 2.0;
                }
                // 形状类:脉冲(两个 smoothstep 相减)/ 正弦重映射 / 幂函数
                if (k == 0) return smoothstep(0.25, 0.35, x) - smoothstep(0.65, 0.75, x);
                if (k == 1) return 0.5 + 0.5 * sin(x * TWO_PI * 2.0);
                return x * x * x;
            }

            // 抗锯齿画线:把曲线在当前像素附近近似成一小段折线(左右各一个像素),
            // 求到这段线段的距离,再换算成“像素单位”。跳变处线段几乎竖直,竖线也能自然连上
            float PlotCurve(int mode, int k, float2 q, float pixel)
            {
                float2 a = float2(q.x - pixel, Curve(mode, k, q.x - pixel));
                float2 b = float2(q.x + pixel, Curve(mode, k, q.x + pixel));
                float dist = SdSegment(q, a, b);
                return saturate(_LineWidth * 0.5 + 0.5 - dist / pixel);
            }

            // 到 d=0 处一条宽 widthPx 像素的线
            float LineAt(float d, float pixel, float widthPx)
            {
                return saturate(widthPx * 0.5 + 0.5 - abs(d) / pixel);
            }

            // 每隔 spacing 一条网格线:frac 把坐标折回到最近网格线附近
            float GridLine(float v, float spacing, float pixel, float widthPx)
            {
                float d = (frac(v / spacing + 0.5) - 0.5) * spacing;
                return LineAt(d, pixel, widthPx);
            }

            half4 frag (Varyings input) : SV_Target
            {
                // UV [0,1] → 绘图坐标
                float2 q = float2(lerp(_View.x, _View.y, input.uv.x), lerp(_View.z, _View.w, input.uv.y));
                float pixel = fwidth(q.x);                 // 一个像素对应多少绘图单位

                half3 col = _Paper.rgb;
                col = lerp(col, half3(0.82, 0.85, 0.88), max(GridLine(q.x, 0.25, pixel, 1), GridLine(q.y, 0.25, pixel, 1)));
                col = lerp(col, half3(0.35, 0.38, 0.42), max(LineAt(q.x, pixel, 1.5), LineAt(q.y, pixel, 1.5)));  // 坐标轴
                int mode = (int)_Mode;
                col = lerp(col, _ColorC.rgb, PlotCurve(mode, 2, q, pixel));
                col = lerp(col, _ColorB.rgb, PlotCurve(mode, 1, q, pixel));
                col = lerp(col, _ColorA.rgb, PlotCurve(mode, 0, q, pixel));
                return half4(col, 1);
            }
            ENDHLSL
        }
    }
}