URP 渲染实战 / 第 3 章 第一个 URP Shader / 案例 3.1
案例 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 } } }
从上往下逐行看:
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" "Queue"="Geometry" }:这是一个只给 URP 用的、不透明的、在 2000 队列绘制的 SubShader。Name "Unlit":Pass 的名字,Frame Debugger 里能看到;UsePass引用别的 Shader 的 Pass 时也靠它。不影响渲染。#pragma vertex vert/#pragma fragment frag:告诉编译器顶点、片元着色器的入口函数叫什么。名字随意,URP 自带 Shader 用UnlitPassVertex这样的长名字。#include ".../ShaderLibrary/Core.hlsl":URP Shader 的“地基”。它会依次引入 core 包的Common.hlsl(数学常量、平台宏、half/real精度定义)、URP 的Input.hlsl(相机、时间、矩阵等全局变量)、SpaceTransforms.hlsl(坐标变换函数)等。注意路径以Packages/开头,这是 Unity 对包内文件的虚拟路径,不是硬盘上的真实路径。
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,裁剪空间 };
两个结构体对应图 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; }
顶点着色器只做一件事:把模型空间坐标变换到裁剪空间。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 表示“写到当前渲染目标”。

还有一处故意的“不规范”: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 的任何阶段选中,也不会报错。