book/09-writing-urp-compatible-shaders.md

09. Guide to writing shaders that are fully compatible with URP standards

ShaderLab/HLSL checklist that matches Pass tag/keyword/CBUFFER (SRP Batcher)/Forward+·Deferred·MotionVectors contracts based on Unity 6.3 (6000.3) / URP 17.3.0

09. Guide to writing “fully compatible” shaders based on URP (HLSL)

The goal of this chapter is to provide a checklist for writing custom HLSL shaders that are compatible with URP's existing shaders/pipeline.

Compatibility is broadly divided into four categories.

  1. Keyword/Variant Compatibility: URP expects #pragma multi_compile/@shader_feature
  2. Pass Tag/Light Mode: Rules for URP to find passes
  3. Constant buffer layout (SRP Batcher): CBUFFER structure/sorting rules
  4. Forward+/Deferred etc path compatible: light loop branching

If these four conditions are met simultaneously, the custom shader will operate stably “in an environment similar to the URP built-in Lit series.”

9.0 Accurate Reference (Generated, URP 17.3.0)

This chapter is intended to be read in conjunction with references automatically extracted from local URP/Core sources, so as not to end up as a “concept checklist”.

In particular, Pass contracts (which LightMode is consumed and when) are fixed in a separate chapter.

Precedence: 08. URP HLSL 라이브러리 지도, 07. Forward/Forward+/Lights

9.1 Pass tag: How URP recognizes passes

URP distinguishes “which pass is for what purpose” with tags such as LightMode.

If you want your custom shader to participate in a specific rendering phase (e.g. DepthOnly, ShadowCaster), You need to match the LightMode tag and pass configuration used by URP.

Practical tip: The safest way is to duplicate (structure only) the Pass section of URP's existing shader (Lit/SimpleLit/Unlit), Start by replacing only the internal HLSL.

9.1.1 There are two types of “passes”: (A) Shader pass vs (B) Render pass

Isolate the points of confusion first.

  • (A) ShaderLab Pass: Pass blocks defined within one shader (ForwardLit, ShadowCaster, etc.)
  • (B) URP Render Pass(ScriptableRenderPass): Render step inserted into the URP pipeline.

URP's Render Pass selects and uses a specific ShaderLab Pass (=LightMode) of the shader when drawing a specific cue at a specific time.

In other words, “compatible” means:

  • The LightMode Pass that URP expects exists and
  • The pass satisfies the input/keyword/buffer layout expected by URP.

It means a state of being.

9.1.2 LightMode tag is “Contract” (as of URP 17.3.0 Lit)

URP selects a ShaderLab Pass based on LightMode from the Render Pass (pipeline stage).

So “fully compatible” means:

  • Provides a LightMode Pass consumed by the project,
  • Satisfies the output/keyword/input structure required by the pass.

means.

The 9 LightModes actually included by Lit.shader in URP 17.3.0 (as of Generated):

LightMode Recommended classification Main consumption stages (summary) Remarks
UniversalForward Essential level Forward color Includes Forward+ branches
ShadowCaster Essential level shadow map Alpha Clip/Bias
DepthOnly Usually required DepthTexture/Depth prepass ColorMask Setting Caution
DepthNormals Feature Dependency DepthNormalsTexture Based on SSAO/Outline/SSR type
Meta Feature Dependency Lightmap Baking Baking is virtually essential
UniversalGBuffer Feature Dependency Deferred(GBuffer) Essential for deferred projects
MotionVectors Feature Dependency velocity TAA/Motion Blur/Reprojection
XRMotionVectors XR Dependency XR velocity Stencil Contract Included
Universal2D 2D dependence 2D Renderer 2D pipeline contract

Exact include/entry/definition location:

Contract (when/why consumed) details:- @@TOK_0_b865c9ce@@

9.2 Basic framework of URP shader (ShaderLab)

When creating a URP-compatible shader, it is safe to use the following form as the basic framework “including the Pass contract”.

ShaderLab
Shader "Custom/URP/ExampleLit"
{
    Properties
    {
        _BaseMap ("Base Map", 2D) = "white" {}
        _BaseColor ("Base Color", Color) = (1,1,1,1)
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline"="UniversalPipeline"
            "RenderType"="Opaque"
            "Queue"="Geometry"
        }

        // Pass: Forward(컬러)
        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode"="UniversalForward" }

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag

            // (A) URP 키워드/variant
            // (B) include/CBUFFER
            // (C) 엔트리 함수
            ENDHLSL
        }

        // Pass: ShadowCaster(그림자)
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode"="ShadowCaster" }
            HLSLPROGRAM
            #pragma vertex VertShadow
            #pragma fragment FragShadow
            ENDHLSL
        }

        // Pass: DepthOnly(깊이)
        Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } }

        // Pass: DepthNormals(깊이+노말)
        Pass { Name "DepthNormals" Tags { "LightMode"="DepthNormals" } }

        // Pass: Meta(베이킹)
        Pass { Name "Meta" Tags { "LightMode"="Meta" } }

        // (옵션) Deferred를 목표로 한다면:
        Pass { Name "GBuffer" Tags { "LightMode"="UniversalGBuffer" } }

        // (옵션) TAA/모션블러/리프로젝션을 목표로 한다면:
        Pass { Name "MotionVectors" Tags { "LightMode"="MotionVectors" } }

        // (옵션) XR에서 모션 벡터를 쓰면:
        Pass { Name "XRMotionVectors" Tags { "LightMode"="XRMotionVectors" } }

        // (옵션) 2D Renderer 호환이 필요하면:
        Pass { Name "Universal2D" Tags { "LightMode"="Universal2D" } }
    }
}

The above code is a framework to show the “Pass contract list”.
It is safest for a practical implementation to use the URP Lit Pass configuration as a starting point:

9.3 URP include configuration (minimum practical set)

URP shaders usually start based on the following include:

HLSL
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

In URP 17.3.0, Core.hlsl is effectively a “root include”.

  • Forward+ branch switch: _CLUSTER_LIGHT_LOOPUSE_CLUSTER_LIGHT_LOOP
  • Pull SRP Core (common) include (Common.hlsl, Packing.hlsl, etc.)
  • Include URP Input.hlsl (global constant/struct/util base)

Related (exact definition location is based on generated):

  • USE_CLUSTER_LIGHT_LOOP: <URP>/ShaderLibrary/Core.hlsl
  • _CLUSTER_LIGHT_LOOP: <URP>/ShaderLibrary/ForwardPlusKeyword.deprecated.hlsl

For your purposes here:

  • If lighting is needed, include Lighting/RealtimesLights series
  • If depth sampling is needed, include the DeclareDepthTexture series.
  • If you want to do surface texture sampling in URP style, include the SurfaceInput series.

Add . The specific “which files should be included” may vary depending on the URP version. We recommend keeping track of the list that URP Lit includes. (Related: 08. URP HLSL 라이브러리 지도)

Note: If you need #include_with_pragmas
URP includes some files (DOTS, ObjectMotionVectors, etc.) as #include_with_pragmas.
If that file contains #pragma inside (e.g. MotionVectors pass), the plain #include alone may be “missing a necessary pragma”.
For detailed pass contracts/examples, refer to @@TOK_14_b865c9ce@@.

9.4 Keywords/Variants: “What needs to be adjusted” and “What needs to be reduced”

When matching keywords for URP compatibility, it is easy for variants to explode.

  • Advantages: Naturally combined with URP features (additional lights, shadows, soft shadows, lightmaps, fog…)
  • Disadvantage: Increased build time/memory/loading time

Commonly seen in shaders containing Forward(+)/shadow:

  • Additional Lights: _ADDITIONAL_LIGHTS, _ADDITIONAL_LIGHTS_VERTEX
  • Additional Light Shadows: _ADDITIONAL_LIGHT_SHADOWS
  • Forward+ loop: _CLUSTER_LIGHT_LOOP
  • Soft Shadows: _SHADOWS_SOFT (or similar keyword)
  • Main Light Shadows/Cascade: Keyword composition varies depending on URP version/shader

Recommended: Turn on “only what you really need” for your first implementation, and verify by increasing keywords each time you add a feature.

9.5 SRP Batcher Compatibility: CBUFFER Rule (Very Important)

SRP Batcher optimizes based on the Material Constant Buffer Layout to reduce material constant upload costs.

So, to bring SRP Batcher to life in a custom shader:

  • Put the material property in CBUFFER_START(UnityPerMaterial) ... CBUFFER_END
  • Prevent layout (type/order) from changing with conditional compilation
  • Avoid placing material values in global (non-CBUFFER) variables unnecessarily.

This is the key.

HLSL
CBUFFER_START(UnityPerMaterial)
float4 _BaseColor;
float4 _BaseMap_ST;
CBUFFER_END

9.5.1 Patterns that frequently break SRP Batcher

  • CBUFFER layout changes by removing the property or changing the type with #if
  • CBUFFER content/order varies for each pass
  • It is a material value, but is scattered outside the global variable/constant buffer.

Related official documents:

9.6 Forward+ Compatible: Light Loop Branching

Supporting Forward+ requires the keywords and branching patterns described in 07. Forward/Forward+/Lights.

In Forward+, there are cases where the shader side should not “simply loop additional lights in the traditional way”. Be sure to check the cluster loop path using URP provided macros/functions.

Key Checks:

  • Does _CLUSTER_LIGHT_LOOP variant exist?
  • Did you know that GetAdditionalLightsCount() in Forward+ can return 0?
    • Solved: Use LIGHT_LOOP_BEGIN/END pattern (unify Forward/Forward+ loops within URP into macro)

The exact definition/reference location is fixed by the generated xref:

Related official documents:

9.7 “Fully Compatible” Checklist (Summary)

A) Pipeline/Pass

  • Is there Tags { "RenderPipeline"="UniversalPipeline" }
  • Is the necessary LightMode Pass (Forward/ShadowCaster/DepthOnly/DepthNormals/Meta) present?

B) Keyword/Variant

  • Are the keywords required for the URP functions (Additional Lights, Shadows, Soft Shadows, Fog…) used in the project included?
  • When using Forward+, is _CLUSTER_LIGHT_LOOP included?

C) Constant buffer (SRP Batcher)

  • Are the material properties gathered in UnityPerMaterial CBUFFER?
  • Is the CBUFFER layout consistent between passes?

D) Resource input (Depth/Normals, etc.)

  • If you need Depth/Normal/History, have you requested/configured them to actually be created in the pipeline?
  • Did you do the correct include/sampling/linearization in the shader?

Further reading (official/authoritative sources)