Post-Process Outline (Depth/Normal)

This is an outline shader that was ported/created based on this Unity tutorial by Roystan.


Note: This shader will only work in Godot 4.x, as the normal buffer used by the shader is not available in 3.x versions of the engine.


To set up the shader apply it to a screen-space quad mesh in the scene, as described here.

*Note: The creation/port of this shader is still a WIP, but I will update with any improvements.


Parameter Descriptions:

  • Depth Threshold: The threshold used to detect edges via depth texture sampling
  • Normal Threshold: The threshold used to detect edges via normal texture sampling
  • Depth Normal Threshold Scale: Modulation of the depth detection based on the normal angle to the camera
  • Edge Color: The final edge color
Shader code
// Ported from by ERInteractive
shader_type spatial;
render_mode unshaded;

uniform float depth_threshold : hint_range(0.0,10.0,0.01) = 0.2f;
uniform float depth_normal_threshold_scale : hint_range(0.0,10.0,0.01) = 0.2f;
uniform float normal_threshold : hint_range(0.0,10.0,0.01) = 0.2f;
uniform vec4 edge_color: source_color;

uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;
uniform sampler2D NORMAL_TEXUTRE : hint_normal_roughness_texture, filter_linear_mipmap;
uniform sampler2D DEPTH_TEXUTRE : hint_depth_texture, filter_linear_mipmap;

uniform float scale = 1.0;
void vertex()
	POSITION = vec4(VERTEX, 1.0);

void fragment() {
	float half_scale_floor = floor(scale * 0.5);
	float half_scale_ceil = ceil(scale * 0.5);
	// Flip UV on Y axis
	vec2 uv = UV * vec2(1.0,-1.0);
	vec2 bottom_left_UV = uv + vec2(SCREEN_PIXEL_SIZE.x, SCREEN_PIXEL_SIZE.y) * half_scale_floor;
	vec2 top_right_UV =  uv + vec2(SCREEN_PIXEL_SIZE.x, SCREEN_PIXEL_SIZE.y) * half_scale_ceil;  
	vec2 bottom_right_UV =  uv + vec2(SCREEN_PIXEL_SIZE.x * half_scale_ceil, -SCREEN_PIXEL_SIZE.y * half_scale_floor);
	vec2 top_left_UV =  uv + vec2(-SCREEN_PIXEL_SIZE.x * half_scale_floor, SCREEN_PIXEL_SIZE.y * half_scale_ceil);
	// Sample Normals
	vec3 normal0 = texture(NORMAL_TEXUTRE, bottom_left_UV).rgb;
	vec3 normal1 = texture(NORMAL_TEXUTRE, top_right_UV).rgb;
	vec3 normal2 = texture(NORMAL_TEXUTRE, bottom_right_UV).rgb;
	vec3 normal3 = texture(NORMAL_TEXUTRE, top_left_UV).rgb;
	vec3 normal_finite_difference_0 = normal1 - normal0;
	vec3 normal_finite_difference_1 = normal3 - normal2;
	vec3 view_normal = normal0 * 2.0 - 1.0;
	float n_dot_v = 1.0 - dot(view_normal, CAMERA_DIRECTION_WORLD);
	float normal_threshold_01 = (n_dot_v - depth_threshold) / (1.0 - depth_threshold);
	float new_normal_threshold = normal_threshold_01 * depth_normal_threshold_scale + 1.0;
	float edge_normal = sqrt(dot(normal_finite_difference_0, normal_finite_difference_0) + dot(normal_finite_difference_1, normal_finite_difference_1));
	edge_normal = edge_normal > normal_threshold ? 1.0 : 0.0;
	// Sample depth

	float depth0 = texture(DEPTH_TEXUTRE, bottom_left_UV).r;
	float depth1 = texture(DEPTH_TEXUTRE, top_right_UV).r;
	float depth2 = texture(DEPTH_TEXUTRE, bottom_right_UV).r;
	float depth3 = texture(DEPTH_TEXUTRE, top_left_UV).r;
	float depth_finite_difference_0 = depth1 - depth0;
	float depth_finite_difference_1 = depth3 - depth2;
	float edge_depth = sqrt(pow(depth_finite_difference_0, 2) + pow(depth_finite_difference_1, 2)) * 100.0;
	edge_depth = edge_depth > depth_threshold * depth0 * new_normal_threshold ? 1.0 : 0.0;	
	// Combination
	float edge = max(edge_depth, edge_normal);
	vec4 col = vec4(edge_color.rgb, edge_color.a * edge);
	// Apply to screen
	ALBEDO = mix(texture(SCREEN_TEXTURE, uv), col, col.a).rgb;
depth, godot4, outline, post process, ScreenSpace
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.

Related shaders

Thick 3D Screen Space – Depth – & Normal – Based Outline Shader.

Linear Depth/Depth Fog

Depth Modulated Pixel Outline in Screen Space

Notify of

Newest Most Voted
Inline Feedbacks
View all comments
1 year ago

It seems to be flipped upside down?

8 months ago

Hey, thanks for sharing this!
One issue with this shader- when applied to a quad per the docs, it seems to make sprites invisible after a certain distance. When shader isnt enabled, or when the quad is out of view, everything is normal. Any idea what’s cauing this?

8 months ago
Reply to  MuffinInACup

Never mind, this is an issue with godot in general, not this specific shader; solved by setting sprite render priority to 1

One other note – if statements here can be optimised away using smt like

max(sign(x - y), 0.0)

where x and y are the two things you want to compare, it will output 1 if x is greater than y and 0 if not

5 months ago

Error: gdshader:47 – Unknown identifier in expression: ‘gt’.
What is gt? Is it a deprecated variable I cant find info about it anywhere?

4 months ago

Thank you for sharing this. It looks really useful.

I’m having some difficulty on setting up the quad mesh in front of the camera. It doesn’t work for me for some reason. Do you have an example/simple scene that you can share? Maybe I can figure out from there.