GameTech/URP 渲染实战

案例 3.1最小可运行:纯色 Unlit入门

目标:写出能在 URP 下编译、把物体涂成一个纯色的 Shader,并看懂每一行。

Assets/Cookbook/Ch03/C03_01_SolidColor.shader
// 第 1 步:最小可运行的 URP Shader —— 把物体涂成一个纯色。
// 故意保持最少的代码:一个 Pass、一个颜色属性、顶点变换 + 输出颜色。
Shader "Cookbook/Ch03/SolidColor"
{
    Properties
    {
        // 名字 ("Inspector 显示名", 类型) = 默认值
        _BaseColor ("Base Color", Color) = (1, 0.5, 0.2, 1)
    }

    SubShader
    {
        // RenderPipeline 标签:只有 URP 激活时才使用这个 SubShader
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        Pass
        {
            Name "Unlit"
            // UniversalForward:URP 前向渲染的主 Pass(不写 LightMode 时按 SRPDefaultUnlit 处理)
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // URP 的“地基”:矩阵、坐标变换函数、平台宏都在这里
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            // 顶点着色器的输入:由网格提供,语义 POSITION 告诉 GPU 取哪一路顶点数据
            struct Attributes
            {
                float4 positionOS : POSITION;   // OS = Object Space,模型空间
            };

            // 顶点 → 片元的数据:SV_POSITION 是光栅化必须的齐次裁剪空间坐标
            struct Varyings
            {
                float4 positionCS : SV_POSITION; // CS = Clip Space,裁剪空间
            };

            // 第 1 步先不放 CBUFFER,第 6 步再讲为什么必须放进去
            half4 _BaseColor;

            Varyings vert (Attributes input)
            {
                Varyings output;
                // 模型空间 → 世界空间 → 观察空间 → 裁剪空间,一次做完
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                return output;
            }

            half4 frag (Varyings input) : SV_Target
            {
                return _BaseColor;
            }
            ENDHLSL
        }
    }
}
// 第 1 步:最小可运行的 URP Shader —— 把物体涂成一个纯色。
// 故意保持最少的代码:一个 Pass、一个颜色属性、顶点变换 + 输出颜色。
Shader "Cookbook/Ch03/SolidColor"
{
    SubShader
    {
        // RenderPipeline 标签:只有 URP 激活时才使用这个 SubShader
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }

        Pass
        {
            Name "Unlit"
            // UniversalForward:URP 前向渲染的主 Pass(不写 LightMode 时按 SRPDefaultUnlit 处理)
            Tags { "LightMode"="UniversalForward" }

            GLSLPROGRAM
            // Core.glsl(URP 的“地基”:矩阵、坐标变换函数)网页引擎会自动包含

            // 顶点着色器的输入:由网格提供,GLSL 里是固定名字的 in 变量 positionOS(OS = Object Space,模型空间)
            // 顶点 → 片元的数据:光栅化必须的齐次裁剪空间坐标写进内置变量 gl_Position(CS = Clip Space,裁剪空间)

            // 第 1 步先不放 CBUFFER,第 6 步再讲为什么必须放进去
            uniform vec4 _BaseColor;

            #ifdef VERTEX
            void main()
            {
                // 模型空间 → 世界空间 → 观察空间 → 裁剪空间,一次做完
                gl_Position = TransformObjectToHClip(positionOS.xyz);
            }
            #endif

            #ifdef FRAGMENT
            void main()
            {
                fragColor = _BaseColor;
            }
            #endif
            ENDGLSL
        }
    }
}

从上往下逐行看:

Assets/Cookbook/Ch03/C03_01_SolidColor.shader(节选:io)
// 顶点着色器的输入:由网格提供,语义 POSITION 告诉 GPU 取哪一路顶点数据
struct Attributes
{
    float4 positionOS : POSITION;   // OS = Object Space,模型空间
};

// 顶点 → 片元的数据:SV_POSITION 是光栅化必须的齐次裁剪空间坐标
struct Varyings
{
    float4 positionCS : SV_POSITION; // CS = Clip Space,裁剪空间
};
// 顶点着色器的输入:由网格提供,GLSL 里是固定名字的 in 变量 positionOS(OS = Object Space,模型空间)
// 顶点 → 片元的数据:光栅化必须的齐次裁剪空间坐标写进内置变量 gl_Position(CS = Clip Space,裁剪空间)

两个结构体对应图 3-2 的左右两半。positionOS 用 float4 接收,网格提供的是三维坐标,GPU 会自动把 w 补成 1。

Assets/Cookbook/Ch03/C03_01_SolidColor.shader(节选:vertfrag)
Varyings vert (Attributes input)
{
    Varyings output;
    // 模型空间 → 世界空间 → 观察空间 → 裁剪空间,一次做完
    output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
    return output;
}

half4 frag (Varyings input) : SV_Target
{
    return _BaseColor;
}
#ifdef VERTEX
void main()
{
    // 模型空间 → 世界空间 → 观察空间 → 裁剪空间,一次做完
    gl_Position = TransformObjectToHClip(positionOS.xyz);
}
#endif

#ifdef FRAGMENT
void main()
{
    fragColor = _BaseColor;
}
#endif

顶点着色器只做一件事:把模型空间坐标变换到裁剪空间。TransformObjectToHClip 来自 core 包的 SpaceTransforms.hlsl:

// 摘自 core 14.0.12 ShaderLibrary/SpaceTransforms.hlsl
float4 TransformObjectToHClip(float3 positionOS)
{
    // More efficient than computing M*VP matrix product
    return mul(GetWorldToHClipMatrix(), mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)));
}

它先乘模型矩阵(Object→World),再乘观察投影矩阵(World→Clip)。源码注释说明了为什么不先把 M 和 VP 乘成一个矩阵:那样每个顶点要多做一次矩阵乘矩阵,而现在只是两次矩阵乘向量。片元着色器直接返回颜色,返回值上的 SV_Target 表示“写到当前渲染目标”。

图 3-3案例 3.1 的效果:左边三个物体用 SolidColor,右边是同色的 URP Lit 球作对照。纯色 Unlit 没有任何明暗,立体感完全消失——光照要到第 5 章才登场

还有一处故意的“不规范”:half4 _BaseColor; 直接声明在全局,没有放进 CBUFFER_START(UnityPerMaterial)。它能正常工作,但不兼容 SRP Batcher。我们先把功能做出来,到案例 3.6 再集中解决这个问题,那时你会更清楚 CBUFFER 到底解决了什么。

动手把 return _BaseColor; 改成 return half4(input.positionCS.xy / _ScreenParams.xy, 0, 1);,保存后物体会显示一个屏幕空间的红绿渐变——这验证了“片元着色器里的 SV_POSITION 是像素坐标”。_ScreenParams 是 URP 提供的全局变量,xy 是渲染目标的宽高。
粉色 / 不显示如果材质是洋红色,说明 Shader 编译失败或没有可用的 SubShader(看 Console 里的报错,或选中 Shader 资源在 Inspector 里看错误列表);如果物体完全不显示,先检查 Pass 的 LightMode 是否拼错——拼错的 Pass 不会被 URP 的任何阶段选中,也不会报错。