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.
		DIFFUSE_LIGHT = max(DIFFUSE_LIGHT, diffuse);
	} 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;
	}
}
Tags
cartoon
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

Complete Toon Shader

Toon Shader inspired by Genshin Impact

Toon Hatching Shader

guest
5 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
zmcn
zmcn
6 months ago

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

edge < 1.0f

should be

edge < 1.0f
Last edited 6 months ago by zmcn
admin
Admin
admin
6 months 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.

ariorick
ariorick
4 months 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;
ariorick
ariorick
4 months ago
Reply to  ariorick

Beautiful shader btw, thank you