Flexible Toon Shader (Godot 4)

Original shader by CaptainProton42 ported to Godot 4

  🤸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

New:
 ✏️ Experimental toon hatching shader (available as a separate material), available on github or asset store.

Current supported repo: https://github.com/atzuk4451/FlexibleToonShaderGD-4.0

Shader code
shader_type spatial;

//render_mode ambient_light_disabled;


uniform vec4 albedo : source_color = vec4(1.0f);
uniform sampler2D albedo_texture : source_color;
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 : source_color;

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

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

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;
	}
	
	// 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);
		SPECULAR_LIGHT += rim_light * rim_color.rgb * rim_color.a * LIGHT_COLOR / PI; //Changed DIFFUSE_LIGHT to SPECULAR_LIGHT while migrating for similar result
	}
}

Warning: Undefined variable $post in /var/www/godotshaders.com/public_html/wp-content/plugins/code-snippets/php/snippet-ops.php(663) : eval()'d code on line 26

Warning: Attempt to read property "ID" on null in /var/www/godotshaders.com/public_html/wp-content/plugins/code-snippets/php/snippet-ops.php(663) : eval()'d code on line 26
Tags
toon
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.

Related shaders

guest

7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Jacob
Jacob
2 years ago

This is great stuff! I love that it’s so flexible.

MonkeyVocalist
MonkeyVocalist
1 year ago

Really impressive stuff. The best toon shader I’ve seen. I had one problem with the “ramp”. It picked the right most color of the ramp for the dark shadows as well. I altered the clamp to be 0.01f and it solved it for me. I’m using 4.3:
float diffuse_stepped = clamp(diffuse_amount + mod(1.0f – diffuse_amount, cuts_inv), 0.01f, 1.0f);

cake
cake
11 months ago

You made one on the best shaders!!!

PG23
7 months ago

For some reason, objects just outside of a light still receive light, but they have a pixelated ring around them. Is there a fix for this?

Snippyboons
Snippyboons
4 months ago
Reply to  PG23

Hey PG23, it is because you are not using attenuation. ATTENUATION is how much light is hitting the pixel. If use_attenuation is false it will light everything in it’s “behind” the omnilight.

Dan
Dan
6 months ago

When I use two directional lights, is there a way for only one to cast shadows? Even if I selected no shadows on one of the lights, the object using the toon shader still casts shadows on itself.

Edit: I’m an idiot. It’s clamp diffuse.

Last edited 6 months ago by Dan
Pixelipy
Pixelipy
5 months ago

Hey! Wanted to thank you for this shader. Not only it looks great, it also teached me a ton about lighting shaders in godot. Hope you the best!

I’ll probably be adding some depth based outline to it as well, I think it should look great with the cartoonish style