2D Cel / Toon Shader v2 +Plus

This is the advanced version of the shader that has extra features. Those features consist of:

 

This shader will restrict light values giving a type of cel or toon shaded look, as well as give you options to fake light depth. It requires sprites to have normal maps. This shader will NOT create the drawn lines, but you could combine with a border shader to give an outline around edges of your sprites. To get extra glow-y light effects, you need to use the WorldEnvironment node. 

Shader Param:

  • First Stage – Sets the threshold for the darkest light. 

  • First Smooth – Smooths the transition between darkest and the next stage.

  • Second Stage – Sets a second light threshold for a mid value. If set to 0 this setting and Second Smooth is ignored and only two light stages are used.

  • Second Smooth – Smooths from the second stage to the lightest. 

  • Rim Light – Activates a border that reacts brightly to light. This border is based off of the outline shader code from GDQuest.

    • Rim Thickness – Changes the line thickness.

    • Rim Intense – Increase or decrease the brightness or intensity.

    • Rim Extra Thick – Adds another pass that thickens the border.

  • Min Light – Sets minimum allowed light. 0 is complete darkness. Increasing from 0 may have a gradient effect due to the way I implement the ability to add light where there was none.

  • Mid Light – Sets the light value of the Second Stage. If Second Stage is set to 0, this setting is ignored.

  • Max Light – Sets the maximum light value allowed. 

  • Obj Light Add – Adds the original light values to the cel values. 

  • Fake Light Depth – Enables the fading or shrinking of light at a set height distance.

    • Obj Height – Sets the light height for this object.

    • Min Scale – A value for use outside of the shader. See “Modifying Scale and Z-index with Height Example.”

    • Max Scale – A value for use outside of the shader. See “Modifying Scale and Z-index with Height Example.”

    • Light Change Thresh – Sets the threshold for where the light is to start being affected by depth. For example: If the Light2D range_height is 500, the Obj Height is 0, and the Light Change Thresh is 500; then the fake depth activates if Light2D height increases any further.

    • Light Fade – Activates fading or dimming of light values.

      • Light Fade End – Sets the distance for the transition from lit to unlit.

    • Fake Spot Light – Activates the ability to shrink or grow the Stage values once the Light Change Thresh is reached. The best way to use these speed values is to set your Obj Height to the height you want it to be dark and change the speed values to your desired effect. Then return your Obj Height to where you had it last.

      • First Shrink Speed – Sets the shrink speed value of the First Stage.

      • Second Shrink Speed – Sets the shrink speed value of the Second Stage.

  • Before Light Modulate – Alters the color of the object before the light pass, preserving luminosity. The standard Modulate setting will change the color of the object and the light after the light has been applied. 

Modifying Scale and Z-Index with Height Example

You can modify the shader parameters from gdscript to accomplish all sorts of effects. For my test project I wanted to modify the scale and z-index of objects based on their Obj Height or Light2D range_height. I also wanted to be able to view it in real time as I set up the scene so I coded it to be a tool. The code below taps the Min and Max Scale variables and compares them to the maximum light range of -2048 to 2048 to calculate the relative scale. If you want to have more control over the scale you can apply this code to a parent. Make sure you modify it to suit your needs, may be error prone.

 

tool
extends Node2D

var change_compare = 0.0
export var scale_with_depth:bool = true
export(NodePath) var lit_node
onready var scaled_node:Node2D = get_node(lit_node)


func _process(delta):
	pass
	if (scale_with_depth):
		if (scaled_node == null):
			print("setting node")
			scaled_node = get_node(lit_node)
		if (scaled_node.material.get_shader_param("fake_light_depth") && scaled_node != null):
			var scale_max = scaled_node.material.get_shader_param("max_scale");
			var scale_min = scaled_node.material.get_shader_param("min_scale");
			var safe_range_height = scaled_node.material.get_shader_param("obj_height");
			if (safe_range_height == -2048.0): 
				safe_range_height = -2047.99
			self.z_index = int(safe_range_height)
			if (change_compare != safe_range_height):
				change_compare = safe_range_height
				var perc_diff:float = (safe_range_height + 2048) / 4096
				var new_scale = (scale_max * perc_diff) + abs((scale_min * perc_diff) - scale_min)
				self.scale.x = new_scale
				self.scale.y = new_scale

Thanks

This shader is a combination of approaches I found on the internet, help from community members, and my own experimentation and development. 

 

Big thanks and shout outs to these people:

  • Azagaya from Laigter – Provided initial cel shading code that got the first version of the toon shader working and jump started me on this. His method for smoothing got carried over to this version and I utilized the VEC calculation to allow setting the Min Light

  • Toasteater on Github – In my searching I found this issue on Github where toasteater outlined his method for cel shading. I learned from their method to solve the gradient issues from version 1 of the toon shader. 

  • cybereality on Reddit – Provided the super helpful solution for Before Light Modulate.

  • GDQuest – The Rim Light is based off of their outline shader. I also generally have learned a lot from their open source contributions and online lessons.

Use it, Make it better!

If you use this in a project let me know so I can check it out. I’m not a programmer by trade so if you know how to make this better, please do so I can use a better version!

Fork it! : https://github.com/mightymochi/2D-Cel-Toon-Shader-v2-Plus

Shader code
shader_type canvas_item;

uniform float first_stage : hint_range(0.0, 1.0) = 0.5; 
uniform float first_smooth : hint_range(0.0, 1.0) = 0.0; // Lengthens the color transition
uniform float second_stage : hint_range(0.0, 1.0) = 0.0;   // If left at 0, only level 1 is used.
uniform float second_smooth : hint_range(0.0, 1.0) = 0.0;
uniform bool rim_light = false;
uniform float rim_thickness : hint_range(0, 40) = 5.0;
uniform float rim_intense : hint_range(0, 1) = 1.0;
uniform bool rim_extra_thick = false;
uniform float min_light : hint_range(0.0, 1.0) = 0.0;
uniform float mid_light : hint_range(0.0, 1.0) = 0.0;
uniform float max_light : hint_range(0.0, 1.0) = 1.0;
uniform float obj_light_add : hint_range(0.0, 1.0) = 0.0;
// Light height variables
uniform bool fake_light_depth = false;
uniform float obj_height : hint_range(-2048.0, 2048.0) = 0.0; 
uniform float min_scale : hint_range(0.0, 10.0) = 0.2;
uniform float max_scale : hint_range(0.0, 10.0) = 2.0; 
uniform float light_change_thresh : hint_range(0.0, 4080.0) = 0.0;
uniform bool light_fade = false;
uniform float light_fade_end : hint_range(0.0, 4080.0) = 0.0;
uniform bool fake_spot_light = false;
uniform float first_shrink_speed : hint_range(0.0, 120.0) = 0.0; 
uniform float second_shrink_speed : hint_range(0.0, 120.0) = 0.0; 
//---------------Color Override
uniform vec4 before_light_modulate : hint_color = vec4(1.0,1.0,1.0,1.0);

void fragment() {
	vec4 texture_color = texture(TEXTURE, UV);
	if (AT_LIGHT_PASS) {
		COLOR = texture_color;
	} else {
		COLOR = texture_color * before_light_modulate;
	}
}
float light_calc(float light_strength, float would_be_strength) {
	float target_strength = light_strength + would_be_strength * obj_light_add;
	if (target_strength == 0.0) {target_strength = 0.000001;}
	if (would_be_strength == 0.0) {would_be_strength = 1.0;}
	return(target_strength / would_be_strength);
}

void light() {
	float level_1 = first_stage;
	float level_1_smooth = first_smooth;
	float level_2 = second_stage;
	float level_2_smooth = second_smooth;
	//---- Light height calc start ------------------------------------
	//-----------------------------------------------------------------
	if (fake_light_depth) {
		float base_height = LIGHT_HEIGHT;
		float new_height = base_height - obj_height;
		LIGHT_HEIGHT = new_height;
		if (fake_spot_light && obj_height < base_height && light_change_thresh < new_height ){
			if (level_1 != 1.0) {
				level_1 -= (light_change_thresh - new_height) * (first_shrink_speed * .0001);
				if (level_2 != 0.0 && level_2 != 1.0) {
					level_2 -= (light_change_thresh - new_height) * (second_shrink_speed * .0001);
				}
			}
		}
		if (light_fade && new_height > light_change_thresh) {
			float n_height_safety = new_height;
			if (n_height_safety == 0.0) { n_height_safety += 0.01; }
			float light_dist_safety = light_change_thresh;
			if (light_dist_safety == 0.0) { light_dist_safety += 0.001; }
			float new_intens = 1.0;
			float dark_distance = light_fade_end;
			if (dark_distance == 0.0) {dark_distance = 1.0;}
			new_intens = 1.0 - abs(abs(light_dist_safety) - abs(n_height_safety)) / dark_distance;
			float light_drop_a = clamp(LIGHT_COLOR.a * new_intens, 0.0, 1.0);
			LIGHT_COLOR *= light_drop_a;
		}
	}
	//---- Light height calc end --------------------------------------
	
	float mid_range_light = mid_light;
	if (mid_light == 0.0) { mid_range_light = max_light * 0.5; }
	vec3 light_normal = normalize(vec3(LIGHT_VEC, -LIGHT_HEIGHT));
	float would_be_strength = max(dot(-light_normal, NORMAL), 0.0);
//-----Light Rim start------------------------------------------------------
	if (rim_light) {
		vec2 size = TEXTURE_PIXEL_SIZE * rim_thickness;
		float outline = texture(TEXTURE, UV + vec2(-size.x, 0)).a;
		outline *= texture(TEXTURE, UV + vec2(0, size.y)).a;
		outline *= texture(TEXTURE, UV + vec2(size.x, 0)).a;
		outline *= texture(TEXTURE, UV + vec2(0, -size.y)).a;
		if (rim_extra_thick) {
			outline *= texture(TEXTURE, UV + vec2(-size.x, size.y)).a;
			outline *= texture(TEXTURE, UV + vec2(size.x, size.y)).a;
			outline *= texture(TEXTURE, UV + vec2(-size.x, -size.y)).a;
			outline *= texture(TEXTURE, UV + vec2(size.x, -size.y)).a;
		}
		outline = 1.0 - outline;

		vec4 color = texture(TEXTURE, UV);
		float rim_cap = outline * color.a * rim_intense * (max_light - min_light);
		LIGHT += rim_cap;
	}
	//-----Light Rim end------------------------------------------------------
	if (would_be_strength > level_1 && level_2 == 0.0 ) {
		float diff = smoothstep(level_1, (level_1 + level_1_smooth), would_be_strength) + min_light;
		if (diff >= max_light) {diff = max_light;}
		LIGHT *= light_calc(diff, would_be_strength);
	} else if (would_be_strength > level_1 && would_be_strength < level_2 && level_2 != 0.0 ) {
		float diff = smoothstep(level_1, (level_1 + level_1_smooth), would_be_strength) + min_light;
		if (diff >= mid_range_light ) {diff = mid_range_light;}
		LIGHT *= light_calc(diff, would_be_strength);
	} else if (would_be_strength >= level_2 && level_2 != 0.0 ) {
		float diff = smoothstep(level_2, (level_2 + level_2_smooth), would_be_strength) + mid_range_light;
		if (diff < mid_range_light ) {diff = mid_range_light;}
		if (diff >= max_light) {diff = max_light;}
		LIGHT *= light_calc(diff, would_be_strength);
	} else { 
		if (min_light != 0.0) { 
			LIGHT_VEC = -NORMAL.xy*length(LIGHT_VEC); 
		}
		LIGHT *= min_light;                                                                                                                                  
	}
}
Tags
2d, canvas, cel, shader, toon
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from MightyMochiGames

Toon Shading for 2D Sprites v1

2D Rim Light

2D Cel / Toon Shader v2 (Basic)

Related shaders

2D Cel / Toon Shader v2 (Basic)

Flexible Toon Shader

Complete Toon Shader

guest
5 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Alexis von Blumenthal
Alexis von Blumenthal
4 months ago

Hi, can you provide a youtube tutorial for full implementation of this please?

Sousio
Sousio
4 months ago

Looks amazing! Cograts on making this fabulous and laborious work done! As others have mentioned, any video tutorial is highly appreciated. Thanks for sharing