Object Outline Shader
Uses sobel edge detection on normal and depth textures to add an outline to objects.
Shader code
shader_type spatial;
render_mode unshaded;
/*
Normal/Depth outline shader. Apply to nodes as a next pass shader texture.
Inspired by Yui Kinomoto @arlez80, lukky_nl (YT), Robin Seibold (YT)
Uses Sobel Edge detection on a normal and depth texture
Written by William Li (LoudFlameLava)
MIT License
*/
// Might create an outline at the edge of the viewport
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;
uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap;
uniform sampler2D NORMAL_TEXTURE : hint_normal_roughness_texture, filter_linear_mipmap;
uniform float normal_threshold = 0.1;
uniform float depth_threshold = 0.05;
uniform float depth_artifact_correction_coef = 2;
uniform vec3 outline_color: source_color;
const mat3 sobel_y = mat3(
vec3(1.0, 0.0, -1.0),
vec3(2.0, 0.0, -2.0),
vec3(1.0, 0.0, -1.0)
);
const mat3 sobel_x = mat3(
vec3(1.0, 2.0, 1.0),
vec3(0.0, 0.0, 0.0),
vec3(-1.0, -2.0, -1.0)
);
float edge_value_normal(sampler2D normal_tex, vec2 uv, vec2 pixel_size, mat3 sobel) {
float output = 0.0;
vec3 normal = texture(normal_tex, uv).rgb;
vec3 n = texture(NORMAL_TEXTURE, uv + vec2(0.0, -pixel_size.y)).rgb;
vec3 s = texture(NORMAL_TEXTURE, uv + vec2(0.0, pixel_size.y)).rgb;
vec3 e = texture(NORMAL_TEXTURE, uv + vec2(pixel_size.x, 0.0)).rgb;
vec3 w = texture(NORMAL_TEXTURE, uv + vec2(-pixel_size.x, 0.0)).rgb;
vec3 nw = texture(NORMAL_TEXTURE, uv + vec2(-pixel_size.x, -pixel_size.y)).rgb;
vec3 ne = texture(NORMAL_TEXTURE, uv + vec2(pixel_size.x, -pixel_size.y)).rgb;
vec3 sw = texture(NORMAL_TEXTURE, uv + vec2(-pixel_size.x, pixel_size.y)).rgb;
vec3 se = texture(NORMAL_TEXTURE, uv + vec2(pixel_size.x, pixel_size.y)).rgb;
mat3 error_mat = mat3(
vec3(length(normal - nw), length(normal - n), length(normal - ne)),
vec3(length(normal - w), 0.0, length(normal - e)),
vec3(length(normal - sw), length(normal - s), length(normal - se))
);
output += dot(sobel[0], error_mat[0]);
output += dot(sobel[1], error_mat[1]);
output += dot(sobel[2], error_mat[2]);
return abs(output);
}
float get_depth(sampler2D depth_tex, vec2 uv, mat4 inv_projection_matrix) {
float depth_raw = texture(depth_tex, uv).x;
vec3 ndc = vec3(uv * 2.0 - 1.0, depth_raw);
vec4 view = inv_projection_matrix * vec4(ndc, 1.0);
view.xyz /= view.w;
float depth_linear = -view.z;
return depth_linear;
}
float edge_value_depth(sampler2D depth_tex, vec2 uv, vec2 pixel_size, mat3 sobel, mat4 inv_projection_matrix){
float output = 0.0;
float depth = get_depth(depth_tex, uv, inv_projection_matrix);
float n = get_depth(depth_tex, uv + vec2(0.0, -pixel_size.y), inv_projection_matrix);
float s = get_depth(depth_tex, uv + vec2(0.0, pixel_size.y), inv_projection_matrix);
float e = get_depth(depth_tex, uv + vec2(pixel_size.x, 0.0), inv_projection_matrix);
float w = get_depth(depth_tex, uv + vec2(-pixel_size.x, 0.0), inv_projection_matrix);
float ne = get_depth(depth_tex, uv + vec2(pixel_size.x, -pixel_size.y), inv_projection_matrix);
float nw = get_depth(depth_tex, uv + vec2(-pixel_size.x, -pixel_size.y), inv_projection_matrix);
float se = get_depth(depth_tex, uv + vec2(pixel_size.x, pixel_size.y), inv_projection_matrix);
float sw = get_depth(depth_tex, uv + vec2(-pixel_size.x, pixel_size.y), inv_projection_matrix);
mat3 error_mat = mat3(
vec3((depth - nw)/depth, (depth - n)/depth, (depth - ne)/depth),
vec3((depth - w)/depth, 0.0, (depth - e)/depth),
vec3((depth - sw)/depth, (depth - s)/depth, (depth - se)/depth)
);
output += dot(sobel[0], error_mat[0]);
output += dot(sobel[1], error_mat[1]);
output += dot(sobel[2], error_mat[2]);
return abs(output);
}
void fragment() {
float has_outline = 0.0;
vec2 pixel_size = vec2(1.0) / VIEWPORT_SIZE;
ALBEDO = texture(SCREEN_TEXTURE, SCREEN_UV).xyz;
//ALBEDO = vec3(get_depth(DEPTH_TEXTURE, SCREEN_UV, INV_PROJECTION_MATRIX));
if (edge_value_normal(NORMAL_TEXTURE, SCREEN_UV, pixel_size, sobel_x) + edge_value_normal(NORMAL_TEXTURE, SCREEN_UV, pixel_size, sobel_y) > normal_threshold){
ALBEDO = outline_color;
has_outline += 1.0;
}
vec3 normal = texture(NORMAL_TEXTURE, SCREEN_UV).rgb;
float angle = 1.0 - dot(normalize(normal-vec3(0.5)), vec3(0.0,0.0,1.0));
if (edge_value_depth(DEPTH_TEXTURE, SCREEN_UV, pixel_size, sobel_x, INV_PROJECTION_MATRIX) + edge_value_depth(DEPTH_TEXTURE, SCREEN_UV, pixel_size, sobel_y, INV_PROJECTION_MATRIX) > depth_threshold + angle * depth_artifact_correction_coef){
ALBEDO = outline_color;
has_outline += 1.0;
}
if (has_outline < 0.1){
ALPHA = 0.0;
}
}
The king of kings, the lord of lords, the one above all.
Don’t understand a lick of it, but appreciate it, just what I needed.
for anyone interested, I believe you can prevent the outline at the edge of the viewport by replacing lines 15-17 with
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap, repeat_disable;
uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap, repeat_disable;
uniform sampler2D NORMAL_TEXTURE : hint_normal_roughness_texture, filter_linear_mipmap, repeat_disable;
the reason this works is:
at the edge of the viewport, there is a very large change in the edge_value_normal and the edge_value_depth since godot by default repeats the screen_texture ie it tiles it. So when we try to get any pixels from eg to the left of the left edge of the screen, we are actually sampling the pixels from right edge of the screen (I believe) – which leads to this large change in edge values created from the sobel matrix, and the shader detects this as being an edge which needs an outline on it.
If we disable the repeating texture by passing in repeat_disable, the issue is fixed, I assume its because now if we sample outside of the screen, the texture will return the nearest visible pixel (ie a pixel on the nearest edge), and hence any sobel filter will just return 0 (corresponding to no edge) at the borders of the screen.
Feel free to correct me if i misunderstood anything!
Also, thanks for the great shader, it helped me make something that looked a little like antichamber which is the look im going for in my game 🙂