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

Related shaders

guest

12 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Keysandough
Keysandough
2 years ago

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

Keysandough
Keysandough
2 years ago
Reply to  Keysandough

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

MuffinInACup
MuffinInACup
2 years 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

lechatonmortel
lechatonmortel
1 year ago
Reply to  MuffinInACup

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;
uniform float max_outline_distance = 100.0; // Distance maximale pour l’outline

// Fonction pour obtenir la profondeur
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;

   // Récupère les données de profondeur
   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;

   // Récupère la normale de l’objet
   vec3 normal = texture(NORMAL_TEXTURE, SCREEN_UV).rgb * 2.0 – 1.0;

   // Vérifie si l’objet est trop éloigné
   if (depth > max_outline_distance) {
       discard; // Ne pas appliquer l’outline si trop loin
   }

   // Parcours les pixels voisins pour déterminer l’outline
   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;

           // Récupère la couleur minimale pour l’outline
           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);
       }
   }

   // Lissage du résultat de profondeur
   depth_diff = smoothstep(0.2, 0.3, depth_diff);

   // Couleur finale de l’outline
   vec3 final = c * shadow_color;
   ALBEDO = final;

   // Applique le masque alpha pour l’outline
   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.);
}

Last edited 1 year ago by LeChatonMortel
Coffandro
Coffandro
2 years ago

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

phearbot
phearbot
2 years 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 2 years ago by phearbot
phearbot
phearbot
2 years 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
2 years 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

Irrwisch
1 year ago

Looks really cool 🙂 Do you have any idea, why the outlines disappear when moving the camera?

Irrwisch
1 year ago
Reply to  Irrwisch

Nvm, I shouldn’t use TAA

pineapple
pineapple
2 months ago

It’s an alright shader, but it has an issue in the current versions of Godot. For anyone having some issue, add:

void vertex() {
  POSITION = vec4(VERTEX.xy, 1.0, 1.0);
}
At the top of your code, so that the mesh will cover the screen.

Try looking through the mesh and toggling flip faces

And if that doesn’t work, I don’t know, but I believe you can solve it, you’ve made it this far after all, so don’t quit yet

edit: also sorry for the confusing grammar, im just tired

Last edited 2 months ago by pineapple