Photorealistic PBR is the default, but toon and cel shading convert a game's visual identity into something instantly recognizable — and they're far less texture-heavy than physically based workflows.
If you've ever shipped a PBR asset into an engine and wondered why it looks flat, you've already run into the reason most indie studios choose stylized rendering: PBR demands lighting rigs, HDRI environments, and hours of calibration. Toon shading gives you bold results with fewer moving parts.
This guide covers the complete pipeline: shader setup in Blender for previewing, implementation in Unity URP, Unreal Engine 5, and Godot 4, plus outline techniques across all three engines.
Setting Up a Toon Shader in Blender's Shader Editor
Blender doesn't render toon-shaded output for game engines — Unity and Unreal handle that at runtime — but you can prototype the visual language directly in the viewport before exporting.
The core of a Blender toon shader is a Diffuse BSDF node with Smooth set to 0 (Ramp mode). For more control, build it manually with a ColorRamp node driving the diffuse light intensity.
Node setup:
1. Add a Diffuse BSDF → feed its BSDF into a Shader to RGB node
2. Plug the Color output of Shader to RGB into a ColorRamp
3. Set the ColorRamp to Constant interpolation
4. Place two color stops: one dark at position ~0.4, one light at ~0.41 — this creates the hard shadow edge
5. Feed ColorRamp into Emission (or a final Mix Shader with your base color)
```python
# Blender Python — build a basic toon shader on the active material
import bpy
mat = bpy.context.active_object.active_material
nodes = mat.node_tree.nodes
links = mat.node_tree.links
nodes.clear()
output = nodes.new("ShaderNodeOutputMaterial")
emission = nodes.new("ShaderNodeEmission")
ramp = nodes.new("ShaderNodeValToRGB")
rgb = nodes.new("ShaderNodeShaderToRGB")
diffuse = nodes.new("ShaderNodeBsdfDiffuse")
ramp.color_ramp.interpolation = 'CONSTANT'
ramp.color_ramp.elements[0].position = 0.4
ramp.color_ramp.elements[1].position = 0.41
links.new(diffuse.outputs["BSDF"], rgb.inputs["Shader"])
links.new(rgb.outputs["Color"], ramp.inputs["Fac"])
links.new(ramp.outputs["Color"], emission.inputs["Color"])
links.new(emission.outputs["Emission"], output.inputs["Surface"])
```
This gives you a hard two-tone shadow split. For multi-band cel shading (highlight + midtone + shadow), add more stops to the ColorRamp.
![]()
Cel Shading in Unity URP with Shader Graph
Unity URP's Shader Graph handles toon shading cleanly via a Step or Smoothstep node on the diffuse light value.
Step-Based Approach:
1. In Shader Graph, add a Main Light Direction node
2. Dot-product it with the mesh Normal Vector (World space)
3. Pass the result through a Step node (Edge = 0.3 for a roughly 30° terminator)
4. Multiply by your Base Color texture
5. Optionally add a second Step pass at 0.7 to get a highlight band
| Band | Step Threshold | Typical Color Modifier |
|------|---------------|----------------------|
| Shadow | 0.0–0.3 | Base Color × 0.4 |
| Midtone | 0.3–0.7 | Base Color × 1.0 |
| Highlight | 0.7–1.0 | Base Color × 1.5 |
For outlines in URP, use the Inverted Hull method: duplicate the mesh, flip normals via `Cull Front` in a second pass shader, and scale by 1.02–1.05 using the vertex normal offset trick.
```hlsl
// Unity URP outline pass — vertex shader snippet
VertexOutput vert(VertexInput v)
{
VertexOutput o;
float3 worldNorm = TransformObjectToWorldNormal(v.normalOS);
float3 worldPos = TransformObjectToWorld(v.positionOS.xyz);
worldPos += worldNorm * _OutlineWidth;
o.positionCS = TransformWorldToHClip(worldPos);
o.color = _OutlineColor;
return o;
}
```
Set `_OutlineWidth` to 0.003–0.008 world units for most character scales. Too high and it clips geometry; too low and it disappears at distance.
Toon Materials in Unreal Engine 5
UE5's Material system handles toon shading through custom expressions on the Diffuse channel. The key is bypassing Lumen and using Unlit or a custom Lighting Model shading mode.
Fastest approach — Unlit + baked lighting:
1. Set Material Shading Model to Unlit
2. Feed a stepped light value into Emissive Color
3. Capture lighting with a Render Target via Blueprint at bake time
Better approach — Custom Shading Model:
1. Enable `r.CustomDepth 3` in Project Settings
2. Write a Material Function that quantizes the `DotProduct(N, L)` result into 2–4 discrete steps
3. Plug the function into a Material Layer for reusability across all toon assets
4. Use a Post Process Material with Custom Depth/Stencil to draw outlines on tagged meshes only
For outlines specifically, the Post Process approach gives you engine-wide control:
```cpp
// PostProcess Material — edge detection via Sobel on custom depth
float depth = CalcSceneDepth(UV);
float dx = ddx(depth);
float dy = ddy(depth);
float edge = sqrt(dx*dx + dy*dy);
return edge > _EdgeThreshold ? _OutlineColor : float4(0,0,0,0);
```
![]()
Toon Shading in Godot 4 Spatial Shaders
Godot 4's `spatial` shader type gives direct access to the light model via the `light()` function — which makes toon shading exceptionally clean.
```glsl
shader_type spatial;
render_mode unshaded;
uniform vec4 base_color : source_color = vec4(1.0);
uniform vec4 shadow_color : source_color = vec4(0.3, 0.3, 0.4, 1.0);
uniform float shadow_threshold : hint_range(0.0, 1.0) = 0.4;
uniform float highlight_threshold : hint_range(0.0, 1.0) = 0.8;
void light() {
float NdotL = dot(NORMAL, LIGHT);
vec4 color;
if (NdotL < shadow_threshold) {
color = shadow_color;
} else if (NdotL > highlight_threshold) {
color = base_color * 1.4;
} else {
color = base_color;
}
DIFFUSE_LIGHT += color.rgb * ATTENUATION * LIGHT_COLOR;
}
```
For outlines in Godot 4, use a WorldEnvironment post-process pass or a MeshInstance3D with a second surface using `CULL_FRONT` and a normal-offset vertex shader — the same inverted hull technique as URP.
Outlines: Inverted Hull vs. Post-Process Edge Detection
Two main outline techniques, each with clear trade-offs:
| Technique | Inverted Hull | Post-Process Edge Detection |
|-----------|--------------|------------------------------|
| Works on | Single mesh | Entire scene |
| Cost | Extra draw call per outlined mesh | Single fullscreen pass |
| Crease handling | Poor on convex gaps | Handles depth edges automatically |
| Animated meshes | Works cleanly | Flickers if motion blur is on |
| Best for | Characters, hero props | Environments, batch outlines |
For characters sourced from BitSoul's marketplace, inverted hull is usually correct — it ships as a second material slot and works in any engine without post-process dependencies. For environment tilesets, edge detection scales better.
A common pitfall: inverted hull outlines disappear at silhouette edges where the normal is nearly perpendicular to the camera. Fix this by offsetting along the screen-space normal rather than the world-space normal in your vertex shader.
Getting Toon-Ready Assets into Production
Toon shading removes the need for elaborate PBR texture sets — you can often ship with just a flat color texture and a normal map for the light model. This means:
- Texture memory drops by 50–70% versus a full PBR set (no roughness, metallic, or AO maps required)
- Draw call count decreases if you atlas multiple toon assets onto one flat-color sheet
- Artist iteration time shortens since you're adjusting color ramps, not baking light maps
Browse the stylized and low-poly asset libraries at BitSoul's marketplace for character meshes, props, and environment kits built with toon workflows in mind — most come with clean UV layouts and separate material slots ready for the inverted hull pass.
Start with the Blender Shader Editor preview to lock down your color palette, then carry that palette into your engine's toon shader as explicit ramp values. Consistency across assets matters more than individual shader complexity in stylized games.