This note is about upgrading a Built-in RP shader file to the URP way of writing it. Most shader tutorials online are still written for Built-in, so this translation table gets more use than you would expect.
Structure
SubShader
Add the render pipeline tag inside Tags {}, otherwise URP will not consider this SubShader at all.
Plain TextSubShader {
Tags {
"RenderPipeline" = "UniversalPipeline"
}
}
The LightMode values changed too. Built-in's ForwardBase / ForwardAdd collapse into a single UniversalForward, because URP's forward renderer handles every light inside one pass instead of re-running the geometry once per additional light. The ones you actually use:
| Purpose | Built-in | URP |
|---|---|---|
| Main lighting pass | ForwardBase | UniversalForward |
| Additional lights | ForwardAdd | (folded into UniversalForward) |
| Shadow casting | ShadowCaster | ShadowCaster |
| Depth prepass | β | DepthOnly / DepthNormals |
| Unlit extra pass | anything | SRPDefaultUnlit |
A pass whose LightMode URP does not recognize is silently dropped β no error, no rendering. If an extra pass such as an outline refuses to show up, this is the cause nine times out of ten.
CG macros
Replace CGPROGRAM / ENDCG with HLSLPROGRAM / ENDHLSL, and CGINCLUDE with HLSLINCLUDE.
#include changes
The common includes map as follows.
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 pulls in Common.hlsl, SpaceTransforms.hlsl and friends, so most of the time it is the only include you need.
Replacing the built-in helper functions
Vertex transforms
| 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 also gives you a VertexPositionInputs struct that computes the object / world / view / clip positions in one call β less typing than converting each by hand, and fewer instructions:
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;
Texture sampling
Built-in's sampler2D + tex2D becomes an explicit texture/sampler pair in URP, so one sampler can serve several textures and you stop burning through the limited sampler slots.
HLSL// Built-in
sampler2D _MainTex;
half4 c = tex2D(_MainTex, uv);
// URP
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
half4 c = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
Lighting
_LightColor0 and _WorldSpaceLightPos0 are gone. You ask URP for a light object instead:
HLSLLight mainLight = GetMainLight(shadowCoord);
half3 lightDir = mainLight.direction;
half3 lightColor = mainLight.color;
half atten = mainLight.distanceAttenuation * mainLight.shadowAttenuation;
// additional lights have to be iterated yourself
uint count = GetAdditionalLightsCount();
for (uint i = 0; i < count; i++) {
Light light = GetAdditionalLight(i, positionWS);
// ... accumulate
}
The SRP Batcher and CBUFFER
This one is not a syntax requirement, but it decides your performance: every per-material property has to live inside the UnityPerMaterial constant buffer, or the shader gets excluded from the SRP Batcher and falls back to a SetPass per material.
HLSLCBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
half4 _BaseColor;
half _Smoothness;
CBUFFER_END
The rules are strict: the declaration order and types inside the CBUFFER must match across every pass, and it cannot contain the textures themselves β only their companion properties such as _MainTex_ST. Once you are done, expand the shader in the Inspector; the header should read "SRP Batcher: compatible". Only then have you actually passed.
Receiving shadows
URP shadows need their keywords enabled explicitly, otherwise the sample always returns 1 (fully lit).
HLSL#pragma multi_compile _ _MAIN_LIGHT_SHADOWS
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE
#pragma multi_compile _ _SHADOWS_SOFT
#pragma multi_compile _ _ADDITIONAL_LIGHTS
Then fetch the shadow coordinate in the fragment stage:
HLSLfloat4 shadowCoord = TransformWorldToShadowCoord(positionWS);
Light mainLight = GetMainLight(shadowCoord);
half shadow = mainLight.shadowAttenuation;
A migration checklist
- Is the
RenderPipelinetag there? - Are the
LightModevalues ones URP recognizes? CGPROGRAMβHLSLPROGRAM?- Are the includes pointing at URP's ShaderLibrary?
- Are the transform and sampling macros all converted?
- Are the material properties inside
UnityPerMaterial? Confirm SRP Batcher compatibility in the Inspector. - If you need shadows, are the
multi_compilekeywords declared? - If depth or post-processing needs this object, did you add a
DepthOnlypass?
Skip any of the first four and the shader turns magenta or refuses to render. Skip any of the last four and it still runs β but performance or appearance quietly degrades, which is the harder kind of bug to find.