agentleFS
IceMasterTIceMasterT/Unity-6.3_SKILLS/unity-shaders-hlsl/SKILL.md

unity-shaders-hlsl

Skill0 starsChanged 3 months agoMITView on GitHub

Use when hand-authoring Unity shaders in HLSL/ShaderLab rather than Shader Graph — writing .shader files, custom lighting models, or shader passes/variants. Grounds answers in the local Unity 6.3 docs over pretrained knowledge.

The file

Files
SKILL.md

Unity Shaders (HLSL/ShaderLab)

Hand-written shader syntax, includes, and pragmas differ sharply between Built-in Render Pipeline (BIRP) and URP, and between Unity versions. Prefer the local docs over pre-trained knowledge for block structure, include paths, and pragma directives — Unity has renamed and moved shader library files across versions, and BIRP/URP syntax is easy to cross-contaminate from memory. This skill covers hand-written .shader files only — Shader Graph and render-pipeline selection are owned by the sibling unity-rendering-pipelines skill.

Retrieval Sources

Docs root: /media/artiq/FRESH_DRIVE/Books/Unity6/Documentation/en/Manual/

SourcePathUse for
Shader/SubShader/Pass block structureSL-Shader.html, SL-SubShader.html, SL-Pass.htmlOuter nested-brace syntax; what each block can contain
Properties block & material propertiesSL-Properties.html, writing-shader-material-properties.html, SL-PropertiesInPrograms.htmlProperty syntax by type, reserved names, attributes, HLSL variable mapping
Shader/SubShader/Pass TagsSL-PassTags.html, SL-SubShaderTags.html, writing-shader-tags.htmlLightMode, RenderPipeline, Queue, RenderType, DisableBatching, etc.
LOD, UsePass, GrabPassSL-ShaderLOD.html, SL-UsePass.html, SL-GrabPass.htmlSubShader-level directives: LOD selection, reusing another shader's Pass, screen-grab textures
Fallback & PackageRequirementsSL-Fallback.html, SL-PackageRequirements.htmlFallback shader selection; gating a SubShader/Pass on installed packages/Unity version
Render state commands overviewwriting-shader-render-state-commands.htmlIndex of all GPU render-state commands and the Category block
Cull / Offset / ConservativeSL-Cull.html, SL-Offset.html, SL-Conservative.htmlBackface culling, depth bias, conservative rasterization
ZWrite / ZTest / ZClipSL-ZWrite.html, SL-ZTest.html, SL-ZClip.htmlDepth buffer writes, depth comparison, near/far clip mode
Blend / BlendOp / ColorMask / AlphaToMaskSL-Blend.html, SL-BlendOp.html, SL-ColorMask.html, SL-AlphaToMask.htmlTransparency blending factors/equations, channel write masks, alpha-to-coverage
Stencil bufferSL-Stencil.htmlStencil ref/read/write masks, comparison and stencil ops, front/back face variants
Adding HLSL program blockswriting-shader-add-shader-program.html, writing-shader-writing-shader-programs-hlsl.html, writing-shader-programs-introduction.htmlWrapping HLSL in HLSLPROGRAM/ENDHLSL, vertex/fragment program basics
HLSL semantics referenceSL-HLSLSemantics.htmlPOSITION, NORMAL, TEXCOORD0-3, TANGENT, COLOR, VPOS, VFACE, SV_VertexID
Vertex input structures (BIRP)SL-VertexProgramInputs.html, SL-BuiltinIncludes-Structures.htmlCustom vertex structs; prebuilt appdata_base/appdata_full/appdata_tan/appdata_img
Pragma directives overviewSL-PragmaDirectives.html, writing-shader-programs-pragma-directives.htmlFull directive list: stages, variants, GPU requirements, graphics APIs, debug flags
Pragma target / require (shader models)SL-Pragma-target.html, SL-Pragma-require.html, SL-ShaderCompileTargets.htmlShader model levels (2.0–5.0), individual GPU feature requirements, per-variant gating
Shader keywords & variantsSL-MultipleProgramVariants.html, SL-MultipleProgramVariants-declare.html, SL-MultipleProgramVariants-make-conditionals.html, SL-MultipleProgramVariants-shortcuts.htmlDeclaring keyword sets, branching on them, built-in shortcut macros (multi_compile_fog, etc.)
Built-in include files (BIRP .cginc)SL-BuiltinIncludes.html, SL-BuiltinIncludes-Lighting.html, SL-BuiltinIncludes-PassTags.htmlUnityCG.cginc, Lighting.cginc, AutoLight.cginc; BIRP lighting variables and LightMode values
Built-in shader variables (BIRP)SL-UnityShaderVariables.htmlTransform matrices, camera/screen/time globals, inline sampler-state suffixes
ShaderLab language overviewSL-landing.html, SL-Reference.html, SL-ShadingLanguage.htmlTable-of-contents pages tying the whole ShaderLab/HLSL reference together
Rendering paths & Surface Shaders (BIRP)SL-RenderPipeline.html, writing-shaders-birp.htmlForwardBase/ForwardAdd/Deferred/Vertex pass selection; Surface Shader entry points
URP basic shader structure & worked examplesurp/writing-shaders-urp-basic-unlit-structure.html, urp/writing-shaders-urp-unlit-color.html, urp/writing-shaders-urp-unlit-texture.htmlMinimal compiling URP unlit shader, then adding a color property, then a texture
URP Pass tags referenceurp/urp-shaders/urp-shaderlab-pass-tags.htmlLightMode values URP recognizes (UniversalForward, ShadowCaster, DepthOnly, etc.)
URP shader library methodsurp/use-built-in-shader-methods.html, urp/use-built-in-shader-methods-transformations.html, urp/use-built-in-shader-methods-lighting.html, urp/use-built-in-shader-methods-shadows.html, urp/use-built-in-shader-methods-additional-lights-fplus.htmlTransformObjectToHClip, GetVertexPositionInputs, GetMainLight, shadow coordinate/attenuation functions, Forward+ light loop
URP keywords & macros referenceurp/urp-shaders/shader-keywords-macros.html_ADDITIONAL_LIGHTS, _CLUSTER_LIGHT_LOOP, LIGHT_LOOP_BEGIN/END
URP custom lightingurp/lighting/custom-lighting-introduction.htmlWhen/how to hand-write a lighting model vs. Shader Graph custom function nodes
URP shader variant strippingurp/shader-stripping-landing.html, urp/shader-stripping-features.html, urp/shader-stripping-check.htmlReducing build size/compile time; logging and exporting variant counts

Key Guidelines

ShaderLab Structure (Properties/SubShader/Pass/Tags)

ShaderLab is the outer declarative wrapper — Shader, Properties, SubShader, Pass, Tags — around one or more inner HLSL program blocks; ShaderLab itself has no math, just structure, state, and metadata. A Shader block can contain a Properties block, one or more SubShader blocks, an optional custom editor assignment, and an optional Fallback. Unity picks the first SubShader that is compatible with the current GPU/pipeline, so put your most-capable SubShader first and simpler fallbacks after it. Inside a SubShader, an optional LOD value and a Tags block (commonly RenderPipeline, Queue, RenderType) scope which pipeline and render order the SubShader applies to; a SubShader then contains one or more Pass blocks, each of which can have its own Name, its own Tags (most importantly LightMode, which controls when the pipeline runs the pass), render-state commands, and a shader code block.

Shader "Examples/StructureDemo"
{
    Properties
    {
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        // SubShader Tags scope this whole SubShader to URP and the opaque queue.
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "Queue" = "Geometry" }
        LOD 100

        Pass
        {
            Name "ForwardUnlit"
            // Pass Tags control when this specific Pass runs.
            Tags { "LightMode" = "SRPDefaultUnlit" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes { float4 positionOS : POSITION; };
            struct Varyings   { float4 positionHCS : SV_POSITION; };

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseColor;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }

            half4 frag() : SV_Target { return _BaseColor; }
            ENDHLSL
        }
    }

    // Used only if no SubShader above is compatible with the current hardware/pipeline.
    Fallback "Universal Render Pipeline/Unlit"
}

HLSLPROGRAM vs CGPROGRAM

URP/SRP shaders use HLSLPROGRAM/ENDHLSL and include files from Packages/com.unity.render-pipelines.*; legacy Built-in Render Pipeline shaders use CGPROGRAM/ENDCG and include .cginc files such as UnityCG.cginc. These are not interchangeable — mixing CGPROGRAM blocks with URP includes, or HLSLPROGRAM blocks with UnityCG.cginc, produces broken output (often solid pink/magenta) or compile errors, because the macro and function names in each library conflict or are simply absent from the other. A single Shader file can legitimately contain one SubShader targeting URP (tagged "RenderPipeline" = "UniversalPipeline") and a second SubShader with no RenderPipeline tag as a BIRP-compatible fallback, each using its own program-block type and includes — this is the standard way to ship one shader that degrades gracefully across pipelines.

Shader "Examples/DualPipelineUnlit"
{
    Properties
    {
        [MainColor] _BaseColor("Base Color", Color) = (1, 0, 0, 1)
    }

    // Picked when the active pipeline asset is a UniversalRenderPipelineAsset.
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes { float4 positionOS : POSITION; };
            struct Varyings   { float4 positionHCS : SV_POSITION; };

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseColor;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }
            half4 frag() : SV_Target { return _BaseColor; }
            ENDHLSL
        }
    }

    // No RenderPipeline tag = Built-in-only. Unity falls through to this SubShader
    // when no SRP is active.
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            fixed4 _BaseColor;

            struct appdata { float4 vertex : POSITION; };
            struct v2f      { float4 pos : SV_POSITION; };

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                return o;
            }
            fixed4 frag(v2f i) : SV_Target { return _BaseColor; }
            ENDCG
        }
    }
}

Vertex/Fragment Function Structure

Structure vertex/fragment stages as input/output structs whose fields carry explicit HLSL semantics connecting them to mesh data — Unity's URP convention names these Attributes (vertex input) and Varyings (vertex-to-fragment output), while legacy BIRP code and tutorials often use appdata/v2f. Every Varyings-equivalent struct must include a float4 field with the SV_POSITION semantic (clip-space position); everything else is up to you, but interpolator counts are limited by target platform, so pack related data into single float4s where possible (e.g. two float2 UV sets into one float4). BIRP additionally ships prebuilt vertex-input structs in UnityCG.cginc (appdata_base, appdata_tan, appdata_full, appdata_img) so you don't have to hand-declare common combinations of position/normal/tangent/UV/color.

Shader "Examples/VertexFragmentStructure"
{
    Properties { _BaseMap("Base Map", 2D) = "white" {} }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float2 uv         : TEXCOORD0;
                float4 color      : COLOR;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;  // Required: clip-space position.
                float3 normalWS    : TEXCOORD0;    // Reuse TEXCOORD slots for custom data.
                float2 uv          : TEXCOORD1;
                float4 color       : COLOR;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.normalWS    = TransformObjectToWorldNormal(IN.normalOS);
                OUT.uv          = TRANSFORM_TEX(IN.uv, _BaseMap);
                OUT.color       = IN.color;
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 tex = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv);
                return tex * IN.color;
            }
            ENDHLSL
        }
    }
}

Render State (Cull/ZWrite/Blend/ZTest/Stencil)

Render-state commands (Cull, ZWrite, ZTest, ZClip, Blend, BlendOp, ColorMask, AlphaToMask, Offset, Stencil, Conservative) configure fixed-function GPU state before your shader program runs for that Pass. Placed inside a Pass block they apply to that Pass only; placed directly inside a SubShader block (outside any Pass) they apply to every Pass within it unless a Pass overrides them locally. Blending defaults to Off; when enabled without an explicit BlendOp, the operation defaults to Add, and the blend equation is finalValue = sourceFactor * sourceValue OP destinationFactor * destinationValue. ZWrite is conventionally On for opaque geometry and Off for transparent geometry (since transparent surfaces normally shouldn't occlude what's behind them); disabling it means you may need to sort geometry yourself to avoid ordering artifacts. The Stencil block's Ref/ReadMask/WriteMask/Comp/Pass/Fail/ZFail fields are all optional and independently overridable per front/back face via *Front/*Back suffixes.

Shader "Examples/RenderStateDemo"
{
    Properties
    {
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 0.5)
    }
    SubShader
    {
        Tags { "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" "Queue" = "Transparent" }

        Pass
        {
            Cull Back
            ZWrite Off
            ZTest LEqual
            Blend SrcAlpha OneMinusSrcAlpha
            BlendOp Add
            ColorMask RGB

            Stencil
            {
                Ref 1
                Comp Always
                Pass Replace
            }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes { float4 positionOS : POSITION; };
            struct Varyings   { float4 positionHCS : SV_POSITION; };

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseColor;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }
            half4 frag() : SV_Target { return _BaseColor; }
            ENDHLSL
        }
    }
}

#pragma Directives & Variants/Keywords

#pragma vertex <name> and #pragma fragment <name> are required in any regular graphics shader and name the entry-point functions; #pragma target <model> and #pragma require <features> declare the minimum shader model / GPU features the program needs (Unity warns and can fail to compile on platforms below the target). Shader keywords let one HLSL program compile into multiple variants: #pragma multi_compile <keywords> keeps every listed variant in the build regardless of whether any material currently uses it (good for things toggled at runtime by script); #pragma shader_feature <keywords> behaves the same at compile time but Unity strips variants that no material in the project actually references, shrinking the build (good for material-authored toggles). Add a leading underscore (_) as a keyword in the set to create the "all keywords in this set disabled" variant explicitly — without it, that all-off state isn't a distinct compiled variant for shader_feature/multi_compile sets of two or more keywords. Branch on keywords with a plain HLSL if (Unity compiles it to the right static branch or dynamic branch depending on the directive used) or with #if defined(...).

Shader "Examples/KeywordVariants"
{
    Properties
    {
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        [Toggle(_RIM_ON)] _UseRim("Enable Rim Light", Float) = 0
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.5

            // shader_feature: variants no material uses are stripped from the build.
            #pragma shader_feature_local _RIM_ON
            // multi_compile: every listed variant is always kept (e.g. toggled at
            // runtime via Shader.EnableKeyword / material.EnableKeyword).
            #pragma multi_compile _ _ADDITIONAL_LIGHTS

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

            struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; };
            struct Varyings   { float4 positionHCS : SV_POSITION; float3 normalWS : TEXCOORD0; };

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseColor;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 color = _BaseColor;
            #if defined(_RIM_ON)
                half rim = 1 - saturate(dot(normalize(IN.normalWS), half3(0, 0, 1)));
                color.rgb += rim;
            #endif
                return color;
            }
            ENDHLSL
        }
    }
}

URP Includes vs Built-in cginc Includes

URP shaders include library files from Packages/com.unity.render-pipelines.universal/ShaderLibrary/ (and .../render-pipelines.core/ShaderLibrary/); BIRP shaders include .cginc files that ship inside the Unity Editor install itself and are found on the compiler's built-in include path (no package path needed). The two hierarchies are not compatible with each other and must never both be included in the same program block.

BIRP include (.cginc)ContentsURP include (ShaderLibrary/*.hlsl)Contents
HLSLSupport.cgincCross-platform preprocessor macros (auto-included by CGPROGRAM)Core.hlslPulls in Common.hlsl, SpaceTransforms.hlsl, Packing.hlsl, texture/CBUFFER macros
UnityShaderVariables.cgincBuilt-in global variables (auto-included by CGPROGRAM)Core.hlsl (via ShaderVariablesFunction.hlsl)Transform/camera/screen helper functions
UnityCG.cgincCommon helper functions & appdata_* structsCore.hlslTransformObjectToHClip, GetVertexPositionInputs, etc.
AutoLight.cgincLighting/shadowing support used internally by Surface ShadersLighting.hlslGetMainLight, GetAdditionalLight, shadow sampling
Lighting.cgincStandard Surface Shader lighting models (auto-included when writing Surface Shaders)Lighting.hlslLightingLambert, LightingSpecular
TerrainEngine.cgincTerrain/vegetation shader helpers— (URP terrain shaders use their own package includes)—
Shader "Examples/IncludeChainDemo"
{
    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // Core.hlsl pulls in Common.hlsl, SpaceTransforms.hlsl, Packing.hlsl, etc.
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            // Lighting.hlsl pulls in RealtimeLights.hlsl, AmbientOcclusion.hlsl, and
            // Shadows.hlsl. It's safe to include alongside Core.hlsl — URP headers
            // use include guards.
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

            struct Attributes { float4 positionOS : POSITION; };
            struct Varyings   { float4 positionHCS : SV_POSITION; };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                return OUT;
            }

            half4 frag() : SV_Target
            {
                Light mainLight = GetMainLight();
                return half4(mainLight.color, 1);
            }
            ENDHLSL
        }
    }
}

Writing a Basic Lit Shader

A hand-written lit shader means reimplementing the lighting model yourself — sampling the main light, applying shadow attenuation, and (if needed) looping over additional lights — that Shader Graph's Lit target or BIRP Surface Shaders normally generate for you. In URP, GetMainLight() returns a Light struct with direction/color/distanceAttenuation/shadowAttenuation, but shadowAttenuation only reflects real shadowing if you instead call the shadow-coordinate overload GetMainLight(shadowCoords), where shadowCoords comes from GetShadowCoord(VertexPositionInputs); you also need a multi_compile for the main-light shadow keywords so the correct shadow-sampling variant compiles in. Critically, the object also needs a ShadowCaster pass so other objects can receive shadows cast by it — rather than hand-writing one, UsePass can pull in the built-in Lit shader's, referencing it by uppercased Pass name.

Shader "Examples/BasicLit"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
    }

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

        Pass
        {
            Name "ForwardLit"
            Tags { "LightMode" = "UniversalForward" }

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            // Required so this shader can access the main light's shadow map.
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile _ _ADDITIONAL_LIGHTS

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

            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS   : NORMAL;
                float2 uv         : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS   : SV_POSITION;
                float3 normalWS     : TEXCOORD0;
                float2 uv           : TEXCOORD1;
                float4 shadowCoords : TEXCOORD2;
            };

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                half4 _BaseColor;
            CBUFFER_END

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                VertexPositionInputs posInputs = GetVertexPositionInputs(IN.positionOS.xyz);
                VertexNormalInputs   nrmInputs = GetVertexNormalInputs(IN.normalOS);

                OUT.positionCS   = posInputs.positionCS;
                OUT.normalWS     = nrmInputs.normalWS;
                OUT.uv           = TRANSFORM_TEX(IN.uv, _BaseMap);
                OUT.shadowCoords = GetShadowCoord(posInputs);
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv) * _BaseColor;

                // The shadowCoords overload sets Light.shadowAttenuation from the
                // main light's shadow map instead of always returning 1.
                Light mainLight = GetMainLight(IN.shadowCoords);
                half3 diffuse = LightingLambert(mainLight.color, mainLight.direction, normalize(IN.normalWS))
                                 * mainLight.distanceAttenuation * mainLight.shadowAttenuation;

                return half4(albedo.rgb * diffuse, albedo.a);
            }
            ENDHLSL
        }

        // Reuse the built-in Lit shader's ShadowCaster pass rather than hand-writing
        // one — this is what lets OTHER objects receive shadows cast by this shader.
        UsePass "Universal Render Pipeline/Lit/SHADOWCASTER"
    }
}

Common Mistakes

MistakeWhy / fix
Pink/magenta or fully broken output in URPCGPROGRAM/UnityCG.cginc (BIRP) mixed into a URP shader; use HLSLPROGRAM + Core.hlsl/Lighting.hlsl instead
Shader compiles but doesn't render in URPMissing/incorrect Tags { "RenderPipeline" = "UniversalPipeline" } on the SubShader; see urp/urp-shaders/urp-shaderlab-pass-tags.html
Build/compile times balloonUnbounded multi_compile combinations; audit keyword count, switch rarely-used axes to shader_feature
Lighting looks flat/wrong in a hand-rolled lit shaderCustom lighting model omits GI/shadow terms Shader Graph's Lit target normally supplies; check urp/lighting/custom-lighting-introduction.html
Shader errors only on some platforms#pragma target too low for the HLSL features used, or platform-specific syntax; check SL-Pragma-target.html / SL-ShaderCompileTargets.html
Material properties silently ignored, SRP Batcher disabledPer-material properties not declared inside a single CBUFFER_START(UnityPerMaterial)/CBUFFER_END block in URP; see SL-VertexProgramInputs.html "Add a custom buffer"
Texture Tiling/Offset fields do nothingForgot to declare the matching _ST suffix property (e.g. _BaseMap_ST) that TRANSFORM_TEX depends on
sampler2D _MyTex; tex2D(_MyTex, uv) used inside a URP HLSLPROGRAMThat's BIRP/Cg legacy syntax; URP requires TEXTURE2D(_MyTex) / SAMPLER(sampler_MyTex) / SAMPLE_TEXTURE2D(...) macros
"vertex/fragment shader not found" compile error#pragma vertex <name> / #pragma fragment <name> doesn't match the actual HLSL function name, or the function isn't in scope in that program block
Shader fails to parse / mismatched block errorHLSLPROGRAM closed with ENDCG (or vice versa) instead of its matching ENDHLSL/ENDCG
Object renders as the pink error shader on some hardwareNo Fallback set (or Fallback Off) and no compatible SubShader for that hardware/pipeline; see SL-Fallback.html
shader_feature/multi_compile set can't represent "nothing enabled"Missing the leading _ entry in a multi-keyword set declaration (e.g. shader_feature _ RED GREEN BLUE); without it there's no compiled variant for the all-off state
GrabPass used in a URP or HDRP shaderGrabPass is BIRP-only (see SL-GrabPass.html render-pipeline compatibility table); use the Opaque Texture / Camera Color URP feature instead
Shipped build is bloated and slow#pragma enable_d3d11_debug_symbols (or similar debug pragmas) left in the shader for a final build; strip debug pragmas before shipping
Transparent object depth-sorts incorrectly against itself/other transparentsZWrite On left enabled on a Blend-enabled transparent Pass, or ZWrite Off used but geometry isn't sorted back-to-front on the CPU

Quick Reference

Block / DirectivePurpose
Shader "path" { }Top-level container: Properties, one or more SubShaders, custom editor, Fallback
Properties { }Material-exposed inputs (textures, colors, floats, ints, vectors)
SubShader { }A renderable variant set, picked per-pipeline/LOD; first compatible one wins
Pass { }One draw submission; holds Name, Tags, render state, and the HLSL program
Tags { "K"="V" }Key/value metadata on Shader/SubShader/Pass (RenderPipeline, Queue, RenderType, LightMode, DisableBatching, …)
LOD <n>Assigns a level-of-detail value to a SubShader for Shader.maximumLOD/LODGroup selection
UsePass "Shader/PASS NAME"Inserts a named Pass (uppercase) from another Shader object
GrabPass { } / GrabPass { "Name" }(BIRP only) Grabs the frame buffer into a texture for later Passes
Fallback "Shader" / Fallback OffBackup shader when no SubShader matches; Off shows the error shader instead
PackageRequirements { }Gates a SubShader/Pass on installed package or Unity version ranges
Category { }Groups render-state commands so contained SubShaders/Passes inherit them
Cull Back / Front / OffBackface/frontface culling (default Back)
ZWrite On / OffEnables/disables writes to the depth buffer
ZTest <op>Depth comparison mode: Less, LEqual (default), Greater, Always, etc.
ZClip True / FalseClip vs. clamp fragments outside near/far planes
Offset <factor>, <units>Depth bias to avoid z-fighting
Blend <src> <dst>Enables blending, sets RGBA (or separate RGB/alpha) blend factors
BlendOp <op>Blend equation: Add (default), Sub, RevSub, Min, Max, logical/advanced ops
ColorMask <RGBA/0>Restricts which color channels the GPU writes
AlphaToMask On / OffEnables alpha-to-coverage (MSAA)
Conservative True / FalseEnables conservative rasterization
Stencil { }Configures stencil ref/masks/comparison/pass/fail/zfail ops
HLSLPROGRAM / ENDHLSLSRP (URP/HDRP) shader code block
CGPROGRAM / ENDCGLegacy BIRP shader code block (also emits HLSL under the hood)
CGINCLUDE / ENDCGBIRP-only: HLSL shared across all Passes/SubShaders in the file
#pragma vertex <fn>Declares the vertex shader entry function (required)
#pragma fragment <fn>Declares the fragment shader entry function (required)
#pragma geometry / hull / domain <fn>Optional geometry/tessellation stage entry functions
#pragma multi_compile <kw...>Keeps every listed variant in the build
#pragma shader_feature <kw...>Strips variants unused by any material at build time
#pragma dynamic_branch <kw...>One compiled program, runtime branch instead of separate variants
#pragma skip_variants <kw...>Removes specific keywords/variants from compilation
#pragma target <model>Minimum shader model (2.0…5.0, 4.5, 4.6, gl4.1)
#pragma require <feature...>Minimum individual GPU features (geometry, tessellation, compute, interpolators15, …)
#pragma only_renderers / exclude_renderers <api...>Include/exclude compilation for specific graphics APIs
#pragma surface <fn> <lightModel>(BIRP only) Declares a Surface Shader entry point
CBUFFER_START(name) / CBUFFER_ENDGroups uniform variables into a constant buffer (use UnityPerMaterial for SRP Batcher compatibility)
TEXTURE2D(tex) / SAMPLER(s) / SAMPLE_TEXTURE2D(tex,s,uv)URP/SRP texture declaration and sampling macros
sampler2D tex / tex2D(tex, uv)Legacy BIRP/Cg texture declaration and sampling
TRANSFORM_TEX(uv, tex)Applies a texture's Tiling/Offset (_ST property) to a UV coordinate
SemanticMeaning
POSITIONObject-space vertex position (float3/float4)
NORMALVertex normal (float3)
TANGENTTangent vector for normal mapping (float4)
COLORPer-vertex color (float4)
TEXCOORD0…TEXCOORD3UV/custom interpolator channels (float2/float3/float4)
SV_POSITIONRequired output semantic for clip-space position from the vertex stage
SV_TargetFragment shader color output
SV_VertexIDPer-vertex index (uint; not available in Surface Shaders)
VPOS / UNITY_VPOS_TYPEFragment-stage screen-space pixel position
VFACEFragment-stage front/back facing indicator (float)

Advanced Notes

Shader variant stripping for build size. URP shaders lean heavily on keywords, so an unaudited shader can compile into thousands of variants, each adding to build size and build time. To check actual variant counts, set a non-Disabled Shader Variant Log Level under Project Settings > Graphics > URP > Additional Shader Stripping Settings, enable Development Build, and build — Unity logs a per-shader Total=<kept>/<total>(<percent>) breakdown to the Console and Editor.log (search for Shader Stripping); enabling Export Shader Variants additionally writes Temp/graphics-settings-stripping.json and Temp/shader-stripping.json. To actually reduce the count: enable Strip Unused Variants in the same settings panel (URP then strips variants for features disabled across every URP/Universal Renderer asset in the build); disable unused features (post-processing effects, XR/VR modules) at the project-settings level rather than per-shader, since that's what lets the stripper prove a variant is unreachable; and disable individual shader keywords directly via Shader Build Settings in Graphics settings when you know a feature is unused project-wide. Also strip debug-only pragmas (#pragma enable_d3d11_debug_symbols) before a final build — they disable optimizations and bloat every affected variant. See urp/shader-stripping-features.html and urp/shader-stripping-check.html.

multi_compile vs shader_feature, precisely. Both declare a set of mutually-related keywords and compile one shader variant per combination, but they differ in when unused variants get discarded. multi_compile variants are always included in the build — use it for keywords toggled by script/runtime logic (quality tiers, platform capability keywords like UNITY_DEVICE_SUPPORTS_NATIVE_16BIT) where you can't know ahead of time which variant a given build needs. shader_feature variants are stripped at build time unless some material in the build actually references that keyword combination — use it for keywords exposed as material-authored toggles (e.g. a [Toggle] property), since most projects only ever use a handful of the possible combinations. Both directives support _local suffixes (e.g. shader_feature_local) to scope the keyword to the containing shader only rather than declaring it globally, which further reduces global keyword pressure — a shader is limited to roughly 128 total keywords (4 of which Unity reserves) before performance in the Editor degrades. dynamic_branch is a third option that compiles a single program with a true runtime branch instead of separate variants — no variant explosion at all, at the cost of a per-fragment/vertex branch; prefer it for keywords that change value frequently per-frame or per-object where variant-count reduction matters more than avoiding a branch. Use #pragma skip_variants <keyword...> to prune specific variants out of a built-in keyword shortcut set (e.g. multi_compile_fwdadd plus skip_variants POINT POINT_COOKIE) when you know some lighting combinations never occur in your project.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.