Edge Detection Shader

This shader is actually not, well, one shader. It’s two!

The shader code below contains two shaders separated by a comment – copy them into separate shaders files and add shader pass 1 to a material, in that material, select “next pass” and add the shader pass 2.

Shader code
//---------------------------------------------
// Edge-Detection Shader Pass 1
//
// Here we simply pass the vertex normals to the albedo
// so we can access it through the SCREEN_TEXTURE in our Shader Pass 2
// LICENSE: MIT

shader_type spatial;
render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_toon,specular_disabled,shadows_disabled;

varying vec3 world_normal;

void vertex() {
	world_normal = NORMAL;
}

void fragment() {
	ALBEDO = world_normal.rgb;
}

// END OF SHADER PASS 1
//---------------------------------------------



//---------------------------------------------
// Edge-Detection Shader Pass 2
//
// Here's our fully lit and shaded model,
// but through the SCREEN_TEXTURE, we also have the world normals
// of all the visible parts of our model that the first pass gives us.
// LICENSE: MIT

shader_type spatial;
render_mode blend_mix,depth_draw_alpha_prepass,cull_back,diffuse_lambert,specular_disabled;
uniform vec4 albedo : hint_color;
uniform sampler2D texture_albedo : hint_albedo;
uniform float specular;
uniform float metallic;
uniform float roughness : hint_range(0,1);
uniform float edge_strength : hint_range(0,1) = 0.2;
uniform vec4 edge_color : hint_color = vec4(0.5, 0.5, 0.5, 1.0);

// essentially a cheap "lightness" function
// returns the average of red, green and blue color channels
float vec3_avg(vec3 color) {
	return (color.r + color.g + color.b) / 3.0;
} 

// transform a pixel coordinate to screen UV
vec2 pixel_to_screen_uv(vec2 viewport_size, vec2 pixel) {
	return vec2(pixel.x / viewport_size.x, pixel.y / viewport_size.y);
}

void fragment() {
	vec4 albedo_tex = texture(texture_albedo, UV);
	
	vec2 iuv = vec2(SCREEN_UV.x * VIEWPORT_SIZE.x, SCREEN_UV.y * VIEWPORT_SIZE.y);
	
	vec3 neighbour_left = texture(SCREEN_TEXTURE, pixel_to_screen_uv(VIEWPORT_SIZE, iuv + vec2(0, 0))).rgb;
	vec3 neighbour_right = texture(SCREEN_TEXTURE, pixel_to_screen_uv(VIEWPORT_SIZE, iuv + vec2(0.5, 0))).rgb;
	
	vec3 neighbour_top = texture(SCREEN_TEXTURE, pixel_to_screen_uv(VIEWPORT_SIZE, iuv + vec2(0, 0.0))).rgb;
	vec3 neighbour_bottom = texture(SCREEN_TEXTURE, pixel_to_screen_uv(VIEWPORT_SIZE, iuv + vec2(0, 0.5))).rgb;
	
	ALBEDO = albedo.rgb * texture(texture_albedo, UV).rgb;
	
	// compare normals: if they differ, we draw an edge
	// by mixing in the edge_color, by edge_strength amount
	// feel free to try other ways to mix, such as multiply for more textured objects.
	if (abs(vec3_avg(neighbour_left) - vec3_avg(neighbour_right)) > 0.0) {
		ALBEDO = mix(ALBEDO, edge_color.rgb, edge_strength);
	}else if (abs(vec3_avg(neighbour_top) - vec3_avg(neighbour_bottom)) > 0.0) {
		ALBEDO = mix(ALBEDO, edge_color.rgb, edge_strength);
	}
	
	METALLIC = metallic;
	ROUGHNESS = roughness;
	SPECULAR = specular;
}

// END OF SHADER PASS 2
//---------------------------------------------
Tags
edge, edge detection, pixel-art
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 kostilus

2D Highlight Improved – Pixel Perfect

Flashing Overlay Shader

2D Highlight Improved

Related shaders

3D Edge Detection Shader (Borderlands-Style)

Screen-Space Edge Detection Outline Shader

Edge Detection (Sobel Filter and Gaussian Blur)

Subscribe
Notify of
guest

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
beeb
1 year ago

This will have issues due to the vec3 avg color from the normals. For example a normal of (1, 0, 0) and (0, 1, 0) and (0, 0, 1) will all have the resulting avg value of 0.33, even though they are vastly different normals, they are considered the same in comparison.

Bonbon
Bonbon
1 year ago

Thanks!

Last edited 1 year ago by Bonbon
QuibblingComet
QuibblingComet
1 year ago

I don’t think this works anymore in Godot 4, either that or I’m missing something haha. Anyone know how this is implemented properly?