Flexible Toon Shader

A flexible toon shader for the Godot Engine with many features:

  • 🤸Flexibility through parameters like number of cuts/bands, steepness, and wrap  
  • 🎨 Supports custom color ramps  
  • 🌈 Affected by the colors of light sources and ambient light in the scene  
  • 💡 Allows for multiple light sources  
  • ⛱️ Supports shadows and attenuation  
  • ✨ Visual extras like specular reflections and rim lighting  
  • 🖼️ Supports textures for albedo and specular  

Full documentation at https://github.com/CaptainProton42/FlexibleToonShaderGD

Shader code
shader_type spatial;

//render_mode ambient_light_disabled;

const float PI = 3.1415926536f;

uniform vec4 albedo : hint_color = vec4(1.0f);
uniform sampler2D albedo_texture : hint_albedo;
uniform bool clamp_diffuse_to_max = false;

uniform int cuts : hint_range(1, 8) = 3;
uniform float wrap : hint_range(-2.0f, 2.0f) = 0.0f;
uniform float steepness : hint_range(1.0f, 8.0f) = 1.0f;

uniform bool use_attenuation = true;

uniform bool use_specular = true;
uniform float specular_strength : hint_range(0.0f, 1.0f) = 1.0f;
uniform float specular_shininess : hint_range(0.0f, 32.0f) = 16.0f;
uniform sampler2D specular_map : hint_albedo;

uniform bool use_rim = true;
uniform float rim_width : hint_range(0.0f, 16.0f) = 8.0f;
uniform vec4 rim_color : hint_color = vec4(1.0f);

uniform bool use_ramp = false;
uniform sampler2D ramp : hint_albedo;

uniform bool use_borders = false;
uniform float border_width = 0.01f;

varying vec3 vertex_pos;
varying vec3 normal;

float split_specular(float specular) {
	return step(0.5f, specular);

void vertex() {
	vertex_pos = VERTEX;
	normal = NORMAL;

void fragment() {
	ALBEDO = albedo.rgb * texture(albedo_texture, UV).rgb;

void light() {
	// Attenuation.
	float attenuation = 1.0f;
	if (use_attenuation) {
		attenuation = ATTENUATION.x;
	// Diffuse lighting.
	float NdotL = dot(NORMAL, LIGHT);
	float diffuse_amount = NdotL + (attenuation - 1.0) + wrap;
	//float diffuse_amount = NdotL * attenuation + wrap;
	diffuse_amount *= steepness;
	float cuts_inv = 1.0f / float(cuts);
	float diffuse_stepped = clamp(diffuse_amount + mod(1.0f - diffuse_amount, cuts_inv), 0.0f, 1.0f);

	// Calculate borders.
	float border = 0.0f;
	if (use_borders) {
		float corr_border_width = length(cross(NORMAL, LIGHT)) * border_width * steepness;
		border = step(diffuse_stepped - corr_border_width, diffuse_amount)
				 - step(1.0 - corr_border_width, diffuse_amount);
	// Apply diffuse result to different styles.
	vec3 diffuse = ALBEDO.rgb * LIGHT_COLOR / PI;
	if (use_ramp) {
		diffuse *= texture(ramp, vec2(diffuse_stepped * (1.0f - border), 0.0f)).rgb;
	} else {
		diffuse *= diffuse_stepped * (1.0f - border);
	if (clamp_diffuse_to_max) {
		// Clamp diffuse to max for multiple light sources.
	} else {
		DIFFUSE_LIGHT += diffuse;
	// Specular lighting.
	if (use_specular) {
		vec3 H = normalize(LIGHT + VIEW);
		float NdotH = dot(NORMAL, H);
		float specular_amount = max(pow(NdotH, specular_shininess*specular_shininess), 0.0f)
							    * texture(specular_map, UV).r
								* attenuation;
		specular_amount = split_specular(specular_amount);
		SPECULAR_LIGHT += specular_strength * specular_amount * LIGHT_COLOR;
	// Simple rim lighting.
	if (use_rim) {
		float NdotV = dot(NORMAL, VIEW);
		float rim_light = pow(1.0 - NdotV, rim_width);
		DIFFUSE_LIGHT += rim_light * rim_color.rgb * rim_color.a * LIGHT_COLOR / PI;
The shader code and all code snippets in this post are under MIT license and can be used freely. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from CaptainProton42

Toon Hatching Shader

Related shaders

Flexible Toon Shader (Godot 4)

Toon Shader for Godot 4

Toon Water Shader

Notify of

Newest Most Voted
Inline Feedbacks
View all comments
3 years ago

I think there’s a typo on line 32 in the snippet

edge < 1.0f

should be

edge < 1.0f
Last edited 3 years ago by zmcn
3 years ago
Reply to  zmcn

Thank you for pointing this out. This is a result of an issue we had before where some code was parsed wrong. The code snippet has been updated now. Sorry for this.

3 years ago

I think steepness is supposed to have a default value. I got really confused when effect didn’t work correctly until I changed this value, although It’s displayed as 1 in editor by default

uniform float steepness : hint_range(1.0f, 8.0f) = 1.0f;
3 years ago
Reply to  ariorick

Beautiful shader btw, thank you

2 years ago

An incredible shader and learning resource for those looking to get into making their own. Thank you for this!

1 year ago

I really love this shader! You did a fantastic job, thank you for sharing it with the community!

Has anyone managed to properly import it into Godot4? I gave it a shot with my limited knowledge, but I can’t get some of the proprieties to work properly (like the Ramp).

I’d appreciate some help!

6 months ago


Last edited 6 months ago by steve