Pixel Perfect outline Shader

Uploading this because couldn’t find one on this site.

 

Same outline size from any distance, perfect for interactable objects.
Mode for Godot 4.

 

To use in Godot 3 replace “source_color” with “hint_color”

Shader code
shader_type spatial;
render_mode cull_front, unshaded;

uniform vec4 outline_color : source_color;
uniform float outline_width = 1.0;

void vertex() {
	vec4 clip_position = PROJECTION_MATRIX * (MODELVIEW_MATRIX * vec4(VERTEX, 1.0));
	vec3 clip_normal = mat3(PROJECTION_MATRIX) * (mat3(MODELVIEW_MATRIX) * NORMAL);
	
	vec2 offset = normalize(clip_normal.xy) / VIEWPORT_SIZE * clip_position.w * outline_width * 2.0;
	
	clip_position.xy += offset;
	
	POSITION = clip_position;
}

void fragment() {
	ALBEDO = outline_color.rgb;
	if (outline_color.a < 1.0) {
		ALPHA = outline_color.a;
	}
}
Live Preview
Tags
outline, pixel perfect outline
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from axilirate

Related shaders

Comments 13

  1. Looks really nice, but I can't seem to get it working. On a simple cube there is gaps between the corners, any idea why this may be? I'm using Godot v4.0.beta1

    4
      1. FreyaArtoria

        I was able to get this working by making a shader with this (as a material) set on the "next pass" after a generic blank material - it renders the base material, and then the outline over it.

        2
        1. Kevin Fajardo

          Thank you! It seems that settiing it in Geometry -> Material Overlay works too

          0
  2. Very nice! Did a little digging, here's an annotated version with some comments on why things are happening and where to read more. Also do we really need the if block in the fragment? Seems cleaner to just always assign ALPHA?

    // From https://godotshaders.com/shader/pixel-perfect-outline-shader/
    // Based on https://www.videopoetics.com/tutorials/pixel-perfect-outline-shaders-unity/
    // In turn based on https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/#the-model-view-and-projection-matrices    
    // Shader presumes smooth shading: discontinous vertex normals (e.g. BoxMesh) will "pull apart" the outline
    // A flat shaded mesh with hard edges can use a smooth shaded mesh for its outline, giving good outline while keeping hard edges
    
    shader_type spatial;
    render_mode cull_front, unshaded;
    
    uniform vec4 outline_color : source_color;
    uniform float outline_width = 1.0;
    
    void vertex() {
        // Model -> World -> View/Camera -> (Homogenous) Clip space
        // Working in clip space is closer to what we see on screen, easier math
        vec4 clip_position = PROJECTION_MATRIX * (MODELVIEW_MATRIX * vec4(VERTEX, 1.0));
        vec3 clip_normal = mat3(PROJECTION_MATRIX) * (mat3(MODELVIEW_MATRIX) * NORMAL);
        
        // Offset clip space vertices along normals in the xy (screen) plane: "normalize(clip_normal.xy) * outline_width"
        // Counteract perspective division (true screen space): "*clip_position.w"
        // Clip Space is 2x2 cube, outline_width 1 is 50% of screen - normalize width to screen pixels: "/ VIEWPORT_SIZE * 2.0"    
        vec2 offset = normalize(clip_normal.xy)* outline_width * clip_position.w / VIEWPORT_SIZE * 2.0;    
        clip_position.xy += offset;    
        POSITION = clip_position;
    }
    
    void fragment() {
        ALBEDO = outline_color.rgb;    
        ALPHA = outline_color.a;    
    }
    
    1