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.);
}
Hey, this looks amazing!! thanks for sharing it.
can I copy the code for a gamejam?
of course, I will put a link to here in the credits
I’m sorry for not replying sooner.
Of course, use as you like!!
No need to credit.
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
Heya, it dosen’t quite seem to work for the HTML export? any idea why this would be?
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.
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.
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
Looks really cool 🙂 Do you have any idea, why the outlines disappear when moving the camera?
Nvm, I shouldn’t use TAA