Two-band anime
A lit color and a tinted shadow color, split by one crisp line.
- 01GoalStylize
- 02TechniqueToon / cel shading
- 03ExecutionTwo-band anime
Parameters
Copy for your platform
- GLSLCopy
- GodotCopy
Node chain
Built step by step, in GLSL style, with its node names.
Built step by step, in Godot style, with its node names.
No Unreal Engine 5 version of this card yet, so the nodes show the GLSL names.
No Unity version of this card yet, so the nodes show the GLSL names.
No Blender version of this card yet, so the nodes show the GLSL names.
dotDotProductdotdotdot Normal01 Light facing
smoothstepSmoothStepsmoothstepsmoothstepsmoothstep In02 Two bands
*Multiply*** In03 Shadow color
mixMixmixmixmix In04 Pick a band's color
How it works
The card lights a sphere with one sun and then refuses to let the light fade. Each pixel is either lit or in shadow, and the only thing left of the gradient is where the line between them falls.
1. How much the surface faces the light. The dot productMultiplies two vectors component by component and adds the results. For unit vectors it is the cosine of the angle between them, from 1 (same direction) to -1 (opposite). Dot product Multiplies two vectors component by component and adds the results. For unit vectors it is the cosine of the angle between them, from 1 (same direction) to -1 (opposite). Normal A unit vector perpendicular to a surface, pointing the way it faces. Lighting and Fresnel effects compare it with other directions.dot(a, b) = |a| |b| cos θ
2. Two bands. A smoothstepA soft threshold. It is 0 below the first edge, 1 above the second, and follows an S-shaped curve with flat ends in between. Smoothstep A soft threshold. It is 0 below the first edge, 1 above the second, and follows an S-shaped curve with flat ends in between.t = clamp((x − e0) / (e1 − e0), 0, 1); s = t² (3 − 2t)threshold - softness it returns 0, above threshold + softness it returns 1, and only the thin window between gets a
blend. A window of zero would be a hard step, and its edge would stair-step along the pixels; a few hundredths keeps it
clean without looking soft. Moving the threshold above 0 pulls the shadow toward the light, shrinking the lit
side to a smaller patch facing it and giving more of the form to shadow, for a more dramatic look.
3. Shadow color. The shadow isn’t black or a darker base color. It is the base color multiplied by a tint, a cool violet by default. Multiplying keeps some of the base color’s hue in the shadow, so an orange object gets a dusky rose shadow instead of a brown one. This is how cel animators paint shadows, and it is most of what makes the result read as anime rather than as a broken light.
4. Pick a band’s color. A mix between the shadow color and the base color, driven by the band value, gives the
final color: the base where the band is 1, the shadow where it is 0.
Why the sun is inside the shader
The light direction is a constant, so the bands don’t move when the camera orbits, only when the object turns. That keeps
the card portable: the same code runs as a plain unlit material in any engine. The cost is that scene lights don’t
affect it. In Godot, the same four steps can move into a light() function, where the direction and color come from
each real light; the Godot guide says how.
Making it look intentional
Hand-drawn shadows are designed, and lighting math doesn’t know that. Studios that ship this style correct the shading per vertex or with a painted threshold map, so the shadow on a face falls where an artist wants it. The card’s threshold slider is the simplest form of that control.
Make it in GLSL
The copy button gives a complete GLSL ES 3.00 fragment shader. It expects the world-space normal in v_normal and the four params as uniforms, with the two colors uploaded decoded to linear; it encodes its own output to sRGB.
Draw your mesh with the shader as its fragment stage. Its vertex stage has to pass the world-space normal in v_normal;
the other varyings the shader declares (v_uv, v_position, v_view) must still be written, but this card never
reads them, so any value works as long as v_view isn’t zero (main() normalizes it).
Set p_base and p_shadowTint from a color picker, converted from sRGB to linear first, and p_threshold and
p_softness as plain floats. The shader turns its result back into sRGB at the end of main(), so draw it to the
default framebuffer with no further conversion.
To light it with your scene’s sun instead of the fixed one, add a uniform vec3 u_sunDirection (normalized, pointing
toward the sun, world space) and use it in place of the constant in the lambert line.
Make it in Godot
The shader is unshaded, so scene lights and shadows don't touch it, exactly like the preview. StandardMaterial3D also has a built-in toon mode (Diffuse Mode set to Toon), which bands Godot's own lighting but gives no control over the shadow's color.
Create a MeshInstance3D, give it a new ShaderMaterial, and paste the shader into a new Shader for it. The four params show up in the inspector under Shader Parameters, with the colors as color pickers.
The fixed sun inside effect() keeps the material identical in every scene. To make the bands follow Godot’s lights
instead, remove unshaded from the render mode, write the base color to ALBEDO, and add a light() function: there
dot(NORMAL, LIGHT) is the lambert term for each light, and you add the band’s color, the shadow tint where it’s below
the threshold and white above it, times LIGHT_COLOR * ATTENUATION, to DIFFUSE_LIGHT. Godot calls light() once per
light, so each one draws its own bands.
In VisualShader, the same chain is the Input normal turned to world space with a TransformVecMult (Operator set to A x B (3x3), so the camera’s position isn’t added) by the Input inv_view_matrix, a DotProduct with a Vec3Constant, a SmoothStep, a Multiply for the shadow color and a Mix, wired into Albedo, with the Unshaded flag turned on in the shader’s Flags.
No Unreal Engine 5 tutorial for this card yet.
No Unity tutorial for this card yet.
No Blender tutorial for this card yet.
Code
#version 300 es// Toon / cel shading → Two-band anime, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform vec3 p_base; // Base color: vec3(0.96, 0.62, 0.42), sRGB; upload it decoded to linearuniform vec3 p_shadowTint; // Shadow tint: vec3(0.62, 0.52, 0.78), sRGB; upload it decoded to linearuniform float p_threshold; // Shadow line: 0.1 (-0.6 to 0.8)uniform float p_softness; // Edge softness: 0.02 (0.005 to 0.3)in vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// A fixed sun, the same for every pixel, so the bands stay put while the camera orbits.vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(p_threshold - p_softness, p_threshold + p_softness, lambert); // Anime shadows are a tint of the base color, not a darker gray of it, so the shadow side keeps its hue vec3 shadow = p_base * p_shadowTint; vec3 color = mix(shadow, p_base, lit); return vec4(color, 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Toon / cel shading → Two-band anime, Godot 4. From Creative Center, MIT license.shader_type spatial;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform vec3 base : source_color = vec3(0.96, 0.62, 0.42);uniform vec3 shadowTint : source_color = vec3(0.62, 0.52, 0.78);uniform float threshold : hint_range(-0.6, 0.8, 0.01) = 0.1;uniform float softness : hint_range(0.005, 0.3, 0.005) = 0.02;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// A fixed sun, the same for every pixel, so the bands stay put while the camera orbits.vec4 effect(Inputs i) { float lambert = dot(i.normal, normalize(vec3(-0.4, 0.8, 0.45))); float lit = smoothstep(threshold - softness, threshold + softness, lambert); // Anime shadows are a tint of the base color, not a darker gray of it, so the shadow side keeps its hue vec3 shadow = base * shadowTint; vec3 color = mix(shadow, base, lit); return vec4(color, 1.0);}void fragment() { Inputs i; // Godot's UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // VERTEX, NORMAL and VIEW are in view space here; the effect works in world space i.position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; i.normal = normalize((INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); i.view = normalize((INV_VIEW_MATRIX * vec4(VIEW, 0.0)).xyz); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = VIEWPORT_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); ALBEDO = frag_out.rgb;}No Unreal Engine 5 code for this card yet.
No Unity code for this card yet.
No Blender code for this card yet.