Sprite3d outline working billboard

———-Add a shader to sprite3d and then add the script in the shader3d description. The texture will always work with the shader, even if the entire graphic is replaced.————-
 
 
@tool
# This class extends Sprite3D to automatically synchronize its texture
# with a shader parameter named “sprite_texture”. This is essential for shaders
# that perform effects based on the sprite’s content, like outlines.
class_name Sprite3dOutlineShader
extends Sprite3D

# Caches the last known material to detect when the entire material has been changed.
# The underscore prefix `_` indicates it’s an internal variable, not meant to be
# modified from outside this script.
var _last_material: ShaderMaterial
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
  # — The core of the solution for flicker-free updates —
  # Connect to the ‘texture_changed’ signal. This signal is automatically emitted
  # by the Sprite3D node whenever its ‘texture’ property is modified.
  # By connecting our function to it, we ensure the shader is updated *immediately*
  # in the same frame the texture changes, which prevents flickering.
  # This is much more efficient than checking for changes every frame in _process().
  texture_changed.connect(_update_shader_texture)
 
  # Enable per-frame processing. We still need this to handle the less frequent
  # case where the entire ‘material_override’ is swapped out.
  set_process(true)
 
  # Perform an initial synchronization when the scene starts.
  # This ensures the shader has the correct texture and material references
  # from the very first frame.
  _update_shader_material()

# Called every frame.
func _process(_delta: float) -> void:
  # Check if the material assigned to ‘material_override’ has changed since the last frame.
  # This can happen if you change it in the editor or via code.
  if material_override != _last_material:
    _update_shader_material()

# This function is called when the material itself is swapped out.
func _update_shader_material() -> void:
  # Update our internal cache with the new material reference.
  _last_material = material_override as ShaderMaterial
 
  # After a new material is assigned, we must immediately update its
  # texture parameter to match the sprite’s current texture.
  _update_shader_texture()

# This function is the central point for updating the shader’s texture.
# It’s called either by the ‘texture_changed’ signal or when the material is swapped.
func _update_shader_texture() -> void:
  # Safely get the material. The ‘as ShaderMaterial’ will result in `null`
  # if the material is not a ShaderMaterial, preventing crashes.
  var mat := material_override as ShaderMaterial
 
  # Only proceed if we have a valid shader material AND a valid texture assigned.
  # This prevents errors if either property is unassigned.
  if mat and texture:
    mat.set_shader_parameter(“sprite_texture”, texture)

# — Public API —
# A helper function to allow other scripts or animations to easily change the line color.
func set_line_color(color: Color) -> void:
  var mat := material_override as ShaderMaterial
  if mat:
    mat.set_shader_parameter(“line_color”, color)
  else:
    # Provide a helpful warning if the user tries to set a color
    # without a valid material assigned.
    push_warning(“Missing ShaderMaterial in material_override – can’t set line_color.”)

func enable_outline(enable: bool) -> void:
  var mat := material_override as ShaderMaterial
  if mat:
    mat.set_shader_parameter(“enable_outline”, enable)
  else:
    # Provide a helpful warning if the user tries to set outline
    # without a valid material assigned.
    push_warning(“Missing ShaderMaterial in material_override – can’t set enable_outline.”)
Shader code
shader_type spatial;
render_mode unshaded, blend_mix, depth_prepass_alpha, cull_disabled, specular_disabled;

uniform sampler2D sprite_texture : source_color, filter_nearest;
uniform bool enable_billboard = true;
uniform bool enable_outline = true;

uniform vec4 line_color : source_color = vec4(1.0,1.0,1.0,1.0);
uniform float glowSize: hint_range(0.0, 300) = 15.0;
uniform int glowDensity: hint_range(0, 30) = 3;
uniform int glowRadialCoverage: hint_range(0, 32) = 4;
uniform float glowAngle: hint_range(0.0, 6.28) = 1.57;
uniform float glowSharpness: hint_range(0.0, 5.0) = 1.0;
uniform float alphaThreshold: hint_range(0.0, 1.0) =  0.2;

void vertex() {
    if (enable_billboard) {
        vec3 world_origin = (MODEL_MATRIX * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
        float scale_x = length(MODEL_MATRIX[0].xyz);
        float scale_y = length(MODEL_MATRIX[1].xyz);
        vec3 cam_right = normalize(INV_VIEW_MATRIX[0].xyz);
        vec3 cam_up    = normalize(INV_VIEW_MATRIX[1].xyz);
        vec2 local = VERTEX.xy;
        vec3 world_billboard = world_origin
            + cam_right * local.x * scale_x
            + cam_up    * local.y * scale_y;
        POSITION = PROJECTION_MATRIX * VIEW_MATRIX * vec4(world_billboard, 1.0);
    } else {
        POSITION = PROJECTION_MATRIX * VIEW_MATRIX * (MODEL_MATRIX * vec4(VERTEX, 1.0));
    }
}

void fragment() {
    vec4 col = texture(sprite_texture, UV);

    ALBEDO = col.rgb;
    ALPHA = col.a;

    if (enable_outline) {
        vec2 pixel_size = 1.0 / vec2(textureSize(sprite_texture, 0));
        float alph = 0.0;
        for (int i = 0; i < glowRadialCoverage; i++) {
            for (int j = 0; j < glowDensity; j++) {
                float radians360 = 6.28;
                float angle = (radians360 / float(glowRadialCoverage)) * float(i+1) + glowAngle;
                float dist = glowSize * float(j + 1) / float(glowDensity);
                vec2 pixel_coor = vec2(sin(angle), cos(angle));
                vec4 tex = texture(sprite_texture, UV + pixel_coor * pixel_size * dist);

                float distFrom = float(glowDensity - j) / float(glowDensity);
                float sharpness = mix(0.0, 1.0, pow(distFrom, glowSharpness));
                alph += (tex.a * line_color.a) * sharpness;
            }
        }

        if (ALPHA < alphaThreshold) {
            ALBEDO = line_color.rgb;
            ALPHA = alph;
        }
    }
}
Tags
billboard, outline, sprite3D
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.

Related shaders

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments