T.TAO
Back to Blog
/4 min read/Technical Art

Unity Shader #3 URP Upgrade

#Unity#Shader#TechnicalArt

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:

PurposeBuilt-inURP
Main lighting passForwardBaseUniversalForward
Additional lightsForwardAdd(folded into UniversalForward)
Shadow castingShadowCasterShadowCaster
Depth prepassβ€”DepthOnly / DepthNormals
Unlit extra passanythingSRPDefaultUnlit

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-inURP
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

  1. Is the RenderPipeline tag there?
  2. Are the LightMode values ones URP recognizes?
  3. CGPROGRAM β†’ HLSLPROGRAM?
  4. Are the includes pointing at URP's ShaderLibrary?
  5. Are the transform and sampling macros all converted?
  6. Are the material properties inside UnityPerMaterial? Confirm SRP Batcher compatibility in the Inspector.
  7. If you need shadows, are the multi_compile keywords declared?
  8. If depth or post-processing needs this object, did you add a DepthOnly pass?

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.

In this series

Unity Shader β†’
  1. 01Unity Shader #0 Maths Basics
  2. 02Unity Shader #1 Render Pipeline
  3. 03Unity Shader #2 Code Basics
  4. 04Unity Shader #2.1 Basic Commands
  5. 05Unity Shader #3 URP Upgrade
  6. 06Unity Shader #4 Drawing Shapes on UV
  7. 07Unity Shader #5 Rim Light
  8. 08Unity Shader #6 Scan & Hologram
  9. 09Unity Shader #7 Anime Toon Shader
  10. 10Unity Shader #8 Dissolve
  11. 11Unity Shader #9 Raindrops on Lens
  12. 12Unity Shader #13 Post-Processing
  13. 13URP #1 Universal Lit & URP ShaderLab