Toon Lit Shader with Outline for Unity URP

A URP toon shader for Unity 6: two-band cel shading that receives shadows, a hard specular highlight, a rim light and an inverted-hull outline, in one .shader file.

Also called: cel shader, anime shader, cartoon shader, toon outline shader.

Toon & Stylized URP, Unity 6000.0+ beginner MIT licensed

Use it for: Characters, props and environments in a cartoon, anime or low-poly look, where light should read as clear bands rather than smooth gradients.

Inspector Try the properties
Shader Scripts For Unity/Toon Lit
None (Texture)
HDR
HDR
ToonLit Drag to rotate

The preview is a WebGL copy of the shader's maths with one fixed light, so it shows no shadows, Bloom or textures.

What it does

  • Two light bands with an adjustable threshold and edge softness
  • Receives main light shadows into the dark band, with soft shadow support
  • Hard-edged Blinn-Phong highlight and a lit-side rim light
  • Inverted-hull outline pass with colour and width
  • Ambient light from the scene's environment lighting and light probes
  • Shadow caster, depth and depth-normals passes, so it casts shadows and works with SSAO
  • SRP Batcher compatible

How it works

The main pass compares the surface normal with the main light direction. A smoothstep around Shadow Threshold turns that into two bands, lit and shadowed, and multiplies in the light's shadow attenuation, so cast shadows land in the dark band instead of making a third tone.

The outline is a second pass that draws the mesh again, pushed out along its normals and with front faces culled. Only the back faces of that slightly larger shell show, as a line around the silhouette.

Setup

  1. Save the file as ToonLit.shader anywhere under Assets.
  2. Create a material (Assets > Create > Material) and pick Scripts For Unity > Toon Lit as its shader.
  3. Assign the material to a mesh and set Base Color, Shadow Color and the band settings.
  4. Raise Outline Width until the line reads at your camera distance; 0.01 to 0.03 suits most characters at scale 1.

Common mistakes

  • Outline splits on hard edges: cubes and flat-shaded models have separate normals per face, so the pushed-out shell breaks apart at corners. Use smoothed normals for the outline, or a smooth-shaded mesh.
  • Outline thickness changes with scale: the width is in object space. A model imported at scale 100 needs a width about a hundred times smaller.
  • Only the sun lights it: the shader uses the main directional light only. Point and spot lights do not affect it.

Questions

Does it work with the Built-in Render Pipeline?
No. It is written for the Universal Render Pipeline in Unity 6 and uses URP's lighting library. Unity deprecated the Built-in Render Pipeline in 6.5; see moving a project from Built-in to URP.
Do I need Shader Graph?
No. It is a plain .shader file in ShaderLab and HLSL. Drop it into Assets and pick it in a material's Shader menu.
How do I make the outline thicker or thinner?
Change Outline Width on the material. The width is in object space, so a model imported at a large scale needs a smaller value. For an outline that stays the same thickness on screen, use the Outline Highlight shader as an extra material.

Source

ToonLit.shader · 314 lines · ShaderLab and HLSL for the Universal Render Pipeline 17 (Unity 6)

ToonLit.shader
cpp
// Toon Lit: two-band cel shading with a hard specular, a rim light and
// an inverted-hull outline. URP 17 (Unity 6). Main directional light only.
// Scripts For Unity, MIT licensed.
Shader "Scripts For Unity/Toon Lit"
{
    Properties
    {
        [MainTexture] _BaseMap ("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor ("Base Color", Color) = (1, 0.62, 0.38, 1)
        _ShadowColor ("Shadow Color", Color) = (0.42, 0.33, 0.55, 1)
        _ShadowThreshold ("Shadow Threshold", Range(-1, 1)) = 0
        _BandSoftness ("Band Softness", Range(0.001, 0.2)) = 0.02
        [HDR] _SpecularColor ("Specular Color", Color) = (0.9, 0.9, 0.9, 1)
        _Glossiness ("Specular Size", Range(4, 256)) = 48
        [HDR] _RimColor ("Rim Color", Color) = (1, 1, 1, 1)
        _RimAmount ("Rim Amount", Range(0, 1)) = 0.72
        _RimThreshold ("Rim Threshold", Range(0.01, 1)) = 0.12
        _AmbientStrength ("Ambient Strength", Range(0, 1)) = 0.35
        _OutlineColor ("Outline Color", Color) = (0.09, 0.07, 0.11, 1)
        _OutlineWidth ("Outline Width", Range(0, 0.1)) = 0.02
    }

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

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

        CBUFFER_START(UnityPerMaterial)
            float4 _BaseMap_ST;
            half4 _BaseColor;
            half4 _ShadowColor;
            float _ShadowThreshold;
            float _BandSoftness;
            half4 _SpecularColor;
            float _Glossiness;
            half4 _RimColor;
            float _RimAmount;
            float _RimThreshold;
            float _AmbientStrength;
            half4 _OutlineColor;
            float _OutlineWidth;
        CBUFFER_END

        TEXTURE2D(_BaseMap);
        SAMPLER(sampler_BaseMap);
        ENDHLSL

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

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH
            #pragma multi_compile_fog
            #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;
                float2 uv : TEXCOORD0;
                float3 normalWS : TEXCOORD1;
                float3 positionWS : TEXCOORD2;
                float fogFactor : TEXCOORD3;
            };

            float4 SfuShadowCoord(float3 positionWS)
            {
            #if defined(_MAIN_LIGHT_SHADOWS_SCREEN)
                return ComputeScreenPos(TransformWorldToHClip(positionWS));
            #else
                return TransformWorldToShadowCoord(positionWS);
            #endif
            }

            Varyings vert(Attributes input)
            {
                Varyings output;
                VertexPositionInputs positions = GetVertexPositionInputs(input.positionOS.xyz);
                output.positionCS = positions.positionCS;
                output.positionWS = positions.positionWS;
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
                output.fogFactor = ComputeFogFactor(positions.positionCS.z);
                return output;
            }

            half4 frag(Varyings input) : SV_Target
            {
                half4 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv) * _BaseColor;
                float3 normalWS = normalize(input.normalWS);
                float3 viewDirWS = GetWorldSpaceNormalizeViewDir(input.positionWS);

                Light mainLight = GetMainLight(SfuShadowCoord(input.positionWS));
                float NdotL = dot(normalWS, mainLight.direction);

                // Two bands with a soft edge; shadowed areas fall into the dark band
                float lightBand = smoothstep(_ShadowThreshold - _BandSoftness, _ShadowThreshold + _BandSoftness, NdotL);
                lightBand *= mainLight.shadowAttenuation * mainLight.distanceAttenuation;
                float3 color = lerp(albedo.rgb * _ShadowColor.rgb, albedo.rgb * mainLight.color, lightBand);

                // Hard-edged Blinn-Phong highlight
                float3 halfDirWS = normalize(mainLight.direction + viewDirWS);
                float specular = pow(saturate(dot(normalWS, halfDirWS)), _Glossiness) * lightBand;
                color += smoothstep(0.48, 0.52, specular) * _SpecularColor.rgb;

                // Rim light on the lit side of the silhouette
                float rim = (1.0 - saturate(dot(viewDirWS, normalWS))) * pow(saturate(NdotL), _RimThreshold);
                color += smoothstep(_RimAmount - 0.01, _RimAmount + 0.01, rim) * _RimColor.rgb;

                // Ambient light from the scene's environment lighting and probes
                color += albedo.rgb * SampleSH(normalWS) * _AmbientStrength;

                color = MixFog(color, input.fogFactor);
                return half4(color, albedo.a);
            }
            ENDHLSL
        }

        // Inverted hull: the mesh pushed out along its normals, front faces culled
        Pass
        {
            Name "Outline"
            Tags { "LightMode" = "SRPDefaultUnlit" }
            Cull Front

            HLSLPROGRAM
            #pragma vertex OutlineVert
            #pragma fragment OutlineFrag
            #pragma multi_compile_fog

            struct OutlineAttributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
            };

            struct OutlineVaryings
            {
                float4 positionCS : SV_POSITION;
                float fogFactor : TEXCOORD0;
            };

            OutlineVaryings OutlineVert(OutlineAttributes input)
            {
                OutlineVaryings output;
                float3 positionOS = input.positionOS.xyz + normalize(input.normalOS) * _OutlineWidth;
                output.positionCS = TransformObjectToHClip(positionOS);
                output.fogFactor = ComputeFogFactor(output.positionCS.z);
                return output;
            }

            half4 OutlineFrag(OutlineVaryings input) : SV_Target
            {
                return half4(MixFog(_OutlineColor.rgb, input.fogFactor), 1.0);
            }
            ENDHLSL
        }

        // Casts shadows from the main and additional lights
        Pass
        {
            Name "ShadowCaster"
            Tags { "LightMode" = "ShadowCaster" }
            ZWrite On
            ZTest LEqual
            ColorMask 0
            Cull Back

            HLSLPROGRAM
            #pragma vertex ShadowVert
            #pragma fragment ShadowFrag
            #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"

            float3 _LightDirection;
            float3 _LightPosition;

            struct ShadowAttributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
            };

            struct ShadowVaryings
            {
                float4 positionCS : SV_POSITION;
            };

            ShadowVaryings ShadowVert(ShadowAttributes input)
            {
                ShadowVaryings output;
                float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
                float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
            #if _CASTING_PUNCTUAL_LIGHT_SHADOW
                float3 lightDirectionWS = normalize(_LightPosition - positionWS);
            #else
                float3 lightDirectionWS = _LightDirection;
            #endif
                float4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirectionWS));
            #if UNITY_REVERSED_Z
                positionCS.z = min(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #else
                positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE);
            #endif
                output.positionCS = positionCS;
                return output;
            }

            half4 ShadowFrag(ShadowVaryings input) : SV_Target
            {
                return 0;
            }
            ENDHLSL
        }

        // Depth prepass, used by depth priming and the depth texture
        Pass
        {
            Name "DepthOnly"
            Tags { "LightMode" = "DepthOnly" }
            ZWrite On
            ColorMask R
            Cull Back

            HLSLPROGRAM
            #pragma vertex DepthVert
            #pragma fragment DepthFrag

            struct DepthAttributes
            {
                float4 positionOS : POSITION;
            };

            struct DepthVaryings
            {
                float4 positionCS : SV_POSITION;
            };

            DepthVaryings DepthVert(DepthAttributes input)
            {
                DepthVaryings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                return output;
            }

            half DepthFrag(DepthVaryings input) : SV_Target
            {
                return input.positionCS.z;
            }
            ENDHLSL
        }

        // Normals for SSAO and other effects that read the normals texture
        Pass
        {
            Name "DepthNormals"
            Tags { "LightMode" = "DepthNormals" }
            ZWrite On
            Cull Back

            HLSLPROGRAM
            #pragma vertex NormalsVert
            #pragma fragment NormalsFrag

            struct NormalsAttributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
            };

            struct NormalsVaryings
            {
                float4 positionCS : SV_POSITION;
                float3 normalWS : TEXCOORD0;
            };

            NormalsVaryings NormalsVert(NormalsAttributes input)
            {
                NormalsVaryings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.normalWS = TransformObjectToWorldNormal(input.normalOS);
                return output;
            }

            half4 NormalsFrag(NormalsVaryings input) : SV_Target
            {
                return half4(NormalizeNormalPerPixel(input.normalWS), 0.0);
            }
            ENDHLSL
        }
    }

    FallBack "Hidden/Universal Render Pipeline/FallbackError"
}

More shaders