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 https://roystan.net/articles/outline-shader/ 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);
vec2 SCREEN_PIXEL_SIZE = 1.0 / VIEWPORT_SIZE;
// 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;
}
It seems to be flipped upside down?
Ah yeah, still working on that. If you set the quad’s Y scale to -2.0 it and uncheck “flip faces” that should resolve it in the meantime.Shader code has been updated with a fix. Feel free to let me know if you run into any other issues! 🙂
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?
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
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
Error: gdshader:47 – Unknown identifier in expression: ‘gt’.
What is gt? Is it a deprecated variable I cant find info about it anywhere?
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.
Thanks!!