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.
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
- Save the file as ToonLit.shader anywhere under Assets.
- Create a material (Assets > Create > Material) and pick Scripts For Unity > Toon Lit as its shader.
- Assign the material to a mesh and set Base Color, Shadow Color and the band settings.
- 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)
// 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
- Dissolve ShaderA URP dissolve shader for Unity 6 that burns a mesh away along procedural noise with a glowing HDR edge. No noise texture needed, and its shadow dissolves with it.
- Hologram ShaderA URP hologram shader for Unity 6: additive fresnel glow, scrolling world-space scanlines and random flicker, for sci-fi displays, ghosts and selection previews.
- Stylized Water ShaderA URP stylized water shader for Unity 6: rolling vertex waves, a deep-to-horizon colour ramp, crest foam, a sun glint and optional shore foam from the depth texture.
- Wind Foliage ShaderA URP grass and leaves shader for Unity 6: alpha-cut foliage that sways in gusts of wind, with a root-to-tip colour gradient. Shadows and depth sway with it.