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

A shader that adds thick outlines to your 3D renders. Made for Godot 4.

It’s very much tailored to my project’s requirements, but if it’s usefull to you in any way, then use it as you like.

Also check out the inspiration for this shader !!
https://godotshaders.com/shader/3d-pixel-art-outline-highlight-post-processing-shader/

Apply to a quad in front of your camera, similar to:
https://docs.godotengine.org/en/stable/tutorials/shaders/advanced_postprocessing.html

Shader code
shader_type spatial;
render_mode unshaded, depth_draw_opaque, depth_prepass_alpha;

// Inspired by https://godotshaders.com/shader/3d-pixel-art-outline-highlight-post-processing-shader/

uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap;
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;
uniform sampler2D NORMAL_TEXTURE : hint_normal_roughness_texture, filter_nearest;


uniform vec3 shadow_color : source_color = vec3(0.0);
uniform float shadow_thickness = 2.0;

vec2 getDepth(vec2 screen_uv, sampler2D depth_texture, mat4 inv_projection_matrix){
	float raw_depth = texture(depth_texture, screen_uv)[0];
	vec3 normalized_device_coordinates = vec3(screen_uv * 2.0 - 1.0, raw_depth);
    vec4 view_space = inv_projection_matrix * vec4(normalized_device_coordinates, 1.0);	
	view_space.xyz /= view_space.w;	
	return vec2(-view_space.z, raw_depth);
}


void fragment() {
	vec2 e = vec2(1./VIEWPORT_SIZE.xy)*1.0;

	float depth_diff = 0.0;
	float neg_depth_diff = .5;
	
	vec2 depth_data = getDepth(SCREEN_UV, DEPTH_TEXTURE, INV_PROJECTION_MATRIX);
	float depth = depth_data.x;
	vec3 color = texture(SCREEN_TEXTURE, SCREEN_UV).rgb;
	vec3 c = vec3(0.0);
	
	vec2 min_depth_data = depth_data;
	float min_depth = 9999999.9;
	

	vec3 normal = texture(NORMAL_TEXTURE, SCREEN_UV).rgb * 2.0 - 1.0;
	
	for (float x = -shadow_thickness; x <= shadow_thickness;x += 1.0){
		for (float y = -shadow_thickness; y <= shadow_thickness; y += 1.0){
			if ((x == 0.0 && y == 0.0) || (shadow_thickness*shadow_thickness < (x*x + y*y))){
				continue;
			}
			
			vec2 du_data = getDepth(SCREEN_UV+1.0*vec2(x, y)*e, DEPTH_TEXTURE, INV_PROJECTION_MATRIX);
			vec2 dd_data = getDepth(SCREEN_UV+0.5*vec2(x, y)*e, DEPTH_TEXTURE, INV_PROJECTION_MATRIX);
			
			float du = du_data.x;
			float dd = dd_data.x;
			
			float dd_diff = clamp(abs((depth - dd) - (dd - du)), 0.0, 1.0);

			float val = clamp(abs(depth - du), 0., 1.)/(x*x + y*y)*dd_diff*dd_diff*5000.0;
			
			val = clamp(val, 0.0, 1.0);

			depth_diff += val;

			if (du < min_depth){
				min_depth = du;
				min_depth_data = du_data;
				c = texture(SCREEN_TEXTURE, SCREEN_UV+vec2(x, y)*e).rgb;
				
				c *= clamp(0.5+ 0.5*dot(normalize(vec2(x, y)), (vec2(0.0, 1.0))), 0.0, 1.0);
				
			}
			
			vec3 nu = texture(NORMAL_TEXTURE, SCREEN_UV+vec2(x, y)*e).rgb * 2.0 - 1.0;
			
			depth_diff += (1.0-abs(dot(nu, normal)))/max(min(dd, depth), 2.0);
		}
	}


	depth_diff = smoothstep(0.2, 0.3, depth_diff);

	vec3 final = c*shadow_color;
	ALBEDO = final;

	float alpha_mask = depth_diff;
	DEPTH = min_depth_data.y*alpha_mask + depth_data.y*(1.0-alpha_mask);
	ALPHA = clamp((alpha_mask) * 5., 0., 1.);

}
Tags
Outline Shader
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 OlisUnfinishedProjects

Decal shader 4.0 port

Related shaders

Depth Modulated Pixel Outline in Screen Space

Screen-Space Edge Detection Outline Shader

Depth-Based Outline

Subscribe
Notify of
guest

8 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Keysandough
Keysandough
7 months ago

Hey, this looks amazing!! thanks for sharing it.
can I copy the code for a gamejam?

Keysandough
Keysandough
7 months ago
Reply to  Keysandough

of course, I will put a link to here in the credits

MuffinInACup
MuffinInACup
6 months ago

Thanks for sharing this!
Could you recommend a way to adapt it for a ‘first-person’ perspective camera? I assume this was made for a top-down perspective, and with a different perspective flat, far away surfaces get colored black. I assume its because of the depth pass noticing a significant difference in depth at that point

Coffandro
Coffandro
5 months ago

Heya, it dosen’t quite seem to work for the HTML export? any idea why this would be?

phearbot
phearbot
5 months ago
Reply to  Coffandro

From testing, when attempting to run it in an HTML build the console throws:

USER SHADER ERROR: ‘hint_normal_roughness_texture’ is only available when using the Forward+ backend.
Going into the editor and changing from Forward+ renderer to Compatibility (used for HTML builds) confirms that the ‘hint_normal_roughness_texture’ requires the Forward+ renderer:

‘hint_normal_roughness_texture’ is only available when using the Forward+ backend.
Looking around, there’s an issue filed where the comments essentially say that they “don’t expect it to be implemented in the future due to the performance cost it would have on mobile devices”.

Hope this is helpful to the next person, and maybe they know more about shaders and can say if/how the shader can be modified to be compatible.

Edit to add:
The documentation actually explicitly states it is only supported in Forward+ as well.

Last edited 5 months ago by phearbot
phearbot
phearbot
5 months ago
Reply to  phearbot

Updating, I found the person smarter than me and she posted a shader here that solves the problem for mobile users:

https://godotshaders.com/shader/high-quality-post-process-outline/

I found a thread here that basically said you could probably use the depth values to derive normal data. Someone even linked to a very thorough write-up on a wicked engine blog.

Fast-forward a weekend of reading on the subject and I found the shader Embyr wrote, which basically does exactly what is described in the thread above.

lemon
lemon
4 months ago

Hi! In one of your screenshots, the grass doesn’t have the shader applied, how can I do that?
I’ve been trying to apply a screen space shader to only selected objects, but I can’t figure it out :c