这篇笔记主要写一下怎么把 Built-in RP 的 shader 文件升级成 URP 的写法。网上大多数 Shader 教程仍然是 Built-in 的,所以这套翻译规则实际上比想象中用得多。
结构
SubShader
在 Tags {} 中添加新的渲染管线 Tag,URP 才会认这个 SubShader。
Plain TextSubShader {
Tags {
"RenderPipeline" = "UniversalPipeline"
}
}
LightMode 的取值也换了一套。Built-in 的 ForwardBase / ForwardAdd 在 URP 中统一成一个 UniversalForward,因为 URP 的前向渲染在同一个 Pass 内处理所有光源,不再为每盏额外光源跑一遍几何。常用的还有:
| 用途 | Built-in | URP |
|---|---|---|
| 主光照 Pass | ForwardBase | UniversalForward |
| 额外光源 | ForwardAdd | (并入 UniversalForward) |
| 投射阴影 | ShadowCaster | ShadowCaster |
| 深度预通道 | — | DepthOnly / DepthNormals |
| 无光照附加 Pass | 任意 | SRPDefaultUnlit |
没有匹配到已知 LightMode 的 Pass 会被 URP 直接丢弃,不会报错也不会渲染。描边这类附加 Pass 找不到原因不出现,九成是卡在这里。
CG Macro
将 CGPROGRAM / ENDCG 改为 HLSLPROGRAM / ENDHLSL,CGINCLUDE 改为 HLSLINCLUDE。
#include 文件的改动
常用的几个 #include 现在做如下改动。
Plain Text#include "UnityCG.cginc"
=> #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Lighting.cginc"
=> #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "AutoLight.cginc"
=> #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
Core.hlsl 会把 Common.hlsl、SpaceTransforms.hlsl 这些一起带进来,所以大多数时候只 include 它就够了。
内置辅助函数的替换
顶点坐标变换
| Built-in | URP |
|---|---|
UnityObjectToClipPos(v) | TransformObjectToHClip(v) |
UnityObjectToWorldDir(v) | TransformObjectToWorldDir(v) |
UnityObjectToWorldNormal(v) | TransformObjectToWorldNormal(v) |
mul(unity_ObjectToWorld, v) | TransformObjectToWorld(v.xyz) |
UnityObjectToViewPos(v) | TransformWorldToView(TransformObjectToWorld(v)) |
UnityWorldSpaceViewDir(p) | GetWorldSpaceViewDir(p) |
URP 还提供了一个 VertexPositionInputs 结构,一次性把 object / world / view / clip 四个空间的位置都算出来,比手动逐个转换省事也省指令:
HLSLVertexPositionInputs positions = GetVertexPositionInputs(IN.positionOS.xyz);
VertexNormalInputs normals = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
OUT.positionCS = positions.positionCS;
OUT.positionWS = positions.positionWS;
OUT.normalWS = normals.normalWS;
纹理采样
Built-in 的 sampler2D + tex2D 在 URP 里换成显式的纹理/采样器对,这样同一个 sampler 可以被多张贴图复用,省下宝贵的 sampler 槽位。
HLSL// Built-in
sampler2D _MainTex;
half4 c = tex2D(_MainTex, uv);
// URP
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
half4 c = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
光照
_LightColor0 和 _WorldSpaceLightPos0 都没有了,改为向 URP 索要光源对象:
HLSLLight mainLight = GetMainLight(shadowCoord);
half3 lightDir = mainLight.direction;
half3 lightColor = mainLight.color;
half atten = mainLight.distanceAttenuation * mainLight.shadowAttenuation;
// 额外光源需要自己遍历
uint count = GetAdditionalLightsCount();
for (uint i = 0; i < count; i++) {
Light light = GetAdditionalLight(i, positionWS);
// ... 累加
}
SRP Batcher 与 CBUFFER
这一条不是语法要求,但会直接决定性能:所有 per-material 的属性必须放进 UnityPerMaterial 常量缓冲区,否则这个 Shader 会被 SRP Batcher 排除,退回到逐材质 SetPass。
HLSLCBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
half4 _BaseColor;
half _Smoothness;
CBUFFER_END
要求也很死:CBUFFER 里变量的声明顺序和类型必须在所有 Pass 中完全一致,而且不能包含贴图本身(只能放 _MainTex_ST 这类伴随属性)。改完之后在 Inspector 里把 Shader 展开,顶部会显示 "SRP Batcher: compatible",那才算过关。
接收阴影
URP 的阴影需要显式开启关键字,否则采样出来永远是 1(全亮)。
HLSL#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT
#pragma multi_compile _ _ADDITIONAL_LIGHTS
然后在片元里取阴影坐标:
HLSLfloat4 shadowCoord = TransformWorldToShadowCoord(positionWS);
Light mainLight = GetMainLight(shadowCoord);
half shadow = mainLight.shadowAttenuation;
一份迁移检查清单
RenderPipelineTag 加了吗?LightMode换成 URP 认识的值了吗?CGPROGRAM→HLSLPROGRAM?- include 换成 URP 的 ShaderLibrary 了吗?
- 坐标变换和纹理采样的宏都换过来了吗?
- 材质属性进
UnityPerMaterial了吗?Inspector 里确认 SRP Batcher 兼容。 - 需要阴影的话,
multi_compile关键字加了吗? - 需要被后处理/深度用到的话,
DepthOnlyPass 补了吗?
前四条不做,Shader 会直接变紫或不渲染;后四条不做,Shader 能跑,但性能或表现会悄悄退化——这类问题反而更难查。