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
	}
}
Live Preview
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

Comments 7

  1. Jacob

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

    1
  2. 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);

    2
  3. cake

    You made one on the best shaders!!!

    1
  4. 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?

    1
    1. Snippyboons

      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.

      0
  5. 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.

    2
  6. Pixelipy

    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

    1