shader-patterns · git:20260930.4aecd99 · 2026-09-30 · sha256 5541326a534a6e2f

shader-patterns git:20260930.4aecd99A

Immutable. This exact content never changes and is served at /api/v1/blob/5541326a534a6e2f.

---
name: shader-patterns
description: "Godot shader code: dissolve, 2D outline, fresnel hologram, scrolling water, a pixelate post-process on a SubViewport, and setting shader parameters from GDScript. Use when building visual effects in Godot's shading language."
---

# Shader Patterns

## Dissolve Effect (Godot Spatial)

```glsl
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back;

uniform sampler2D albedo_texture : source_color;
uniform sampler2D noise_texture;
uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
uniform vec4 edge_color : source_color = vec4(1.0, 0.4, 0.0, 1.0);
uniform float edge_width : hint_range(0.0, 0.2) = 0.05;

void fragment() {
    vec4 albedo = texture(albedo_texture, UV);
    float noise = texture(noise_texture, UV).r;

    // Discard fragments below dissolve threshold
    if (noise < dissolve_amount) {
        discard;
    }

    // Glow at dissolve edge
    float edge = smoothstep(dissolve_amount, dissolve_amount + edge_width, noise);
    vec3 emission = mix(edge_color.rgb, vec3(0.0), edge);

    ALBEDO = albedo.rgb;
    EMISSION = emission;
    ALPHA = albedo.a;
}
```

## Outline Shader (Godot CanvasItem / 2D)

```glsl
shader_type canvas_item;

uniform vec4 outline_color : source_color = vec4(0.0, 0.0, 0.0, 1.0);
uniform float outline_width : hint_range(0.0, 10.0) = 2.0;

void fragment() {
    vec2 uv = UV;
    vec2 texel_size = outline_width / vec2(textureSize(TEXTURE, 0));

    float alpha = texture(TEXTURE, uv).a;

    // Sample 8 neighbors
    float neighbor_alpha = 0.0;
    neighbor_alpha += texture(TEXTURE, uv + vec2(texel_size.x, 0.0)).a;
    neighbor_alpha += texture(TEXTURE, uv + vec2(-texel_size.x, 0.0)).a;
    neighbor_alpha += texture(TEXTURE, uv + vec2(0.0, texel_size.y)).a;
    neighbor_alpha += texture(TEXTURE, uv + vec2(0.0, -texel_size.y)).a;
    neighbor_alpha += texture(TEXTURE, uv + vec2(texel_size.x, texel_size.y)).a;
    neighbor_alpha += texture(TEXTURE, uv + vec2(-texel_size.x, texel_size.y)).a;
    neighbor_alpha += texture(TEXTURE, uv + vec2(texel_size.x, -texel_size.y)).a;
    neighbor_alpha += texture(TEXTURE, uv + vec2(-texel_size.x, -texel_size.y)).a;

    // If any neighbor has alpha but this pixel doesn't: draw outline
    if (alpha < 0.1 && neighbor_alpha > 0.1) {
        COLOR = outline_color;
    } else {
        COLOR = texture(TEXTURE, uv);
    }
}
```

## Fresnel / Hologram Effect (Godot Spatial)

```glsl
shader_type spatial;
render_mode blend_add, depth_draw_never, cull_disabled, unshaded;

uniform vec4 hologram_color : source_color = vec4(0.0, 1.0, 0.8, 1.0);
uniform float fresnel_power : hint_range(0.5, 8.0) = 3.0;
uniform float scan_line_density : hint_range(10.0, 200.0) = 60.0;
uniform float flicker_speed : hint_range(0.0, 20.0) = 8.0;
uniform float alpha_intensity : hint_range(0.0, 2.0) = 1.0;

void fragment() {
    // Fresnel: bright at glancing angles
    float fresnel = pow(1.0 - abs(dot(NORMAL, VIEW)), fresnel_power);

    // Scan lines via screen-space Y coordinate
    float scan = step(0.5, fract(FRAGCOORD.y / scan_line_density));

    // Flicker
    float flicker = 0.8 + 0.2 * sin(TIME * flicker_speed + FRAGCOORD.y * 0.1);

    float alpha = fresnel * scan * flicker * alpha_intensity;
    ALBEDO = hologram_color.rgb;
    EMISSION = hologram_color.rgb * fresnel;
    ALPHA = clamp(alpha, 0.0, 1.0);
}
```

## Scrolling Water (Godot Spatial)

```glsl
shader_type spatial;
render_mode blend_mix, depth_draw_opaque;

uniform sampler2D normal_map_a : hint_normal;
uniform sampler2D normal_map_b : hint_normal;
uniform vec4 shallow_color : source_color = vec4(0.1, 0.6, 0.8, 0.9);
uniform vec4 deep_color : source_color = vec4(0.0, 0.15, 0.4, 1.0);
uniform float tiling : hint_range(1.0, 20.0) = 5.0;
uniform float scroll_speed : hint_range(0.0, 2.0) = 0.3;
uniform float wave_height : hint_range(0.0, 1.0) = 0.1;
uniform float fresnel_power : hint_range(0.5, 8.0) = 4.0;

void vertex() {
    // Sine wave vertex displacement
    float wave = sin(VERTEX.x * 2.0 + TIME * scroll_speed * 3.0)
               * cos(VERTEX.z * 2.5 + TIME * scroll_speed * 2.0);
    VERTEX.y += wave * wave_height;
}

void fragment() {
    vec2 uv1 = UV * tiling + vec2(TIME * scroll_speed, TIME * scroll_speed * 0.7);
    vec2 uv2 = UV * tiling * 0.7 + vec2(-TIME * scroll_speed * 0.8, TIME * scroll_speed * 0.5);

    vec3 n1 = texture(normal_map_a, uv1).rgb;
    vec3 n2 = texture(normal_map_b, uv2).rgb;
    NORMAL_MAP = normalize(n1 + n2);  // Blend two normal maps

    float fresnel = pow(1.0 - dot(NORMAL, VIEW), fresnel_power);
    ALBEDO = mix(deep_color.rgb, shallow_color.rgb, fresnel);
    ROUGHNESS = 0.05;
    METALLIC = 0.0;
    ALPHA = mix(deep_color.a, shallow_color.a, fresnel);
}
```

## Pixelate Post-Process (CanvasItem on SubViewport)

```glsl
shader_type canvas_item;

uniform float pixel_size : hint_range(1.0, 64.0) = 4.0;

void fragment() {
    // Snap UV to pixel grid
    vec2 uv = UV;
    vec2 screen_size = vec2(textureSize(TEXTURE, 0));
    vec2 pixels = screen_size / pixel_size;
    uv = floor(uv * pixels) / pixels;
    COLOR = texture(TEXTURE, uv);
}
```

## Setting Shader Parameters from GDScript

```gdscript
# Set uniform parameters on a ShaderMaterial at runtime
class_name DissolveController extends Node3D
@export var mesh_instance: MeshInstance3D
@export var dissolve_duration: float = 1.5

var _dissolve_tween: Tween

func start_dissolve() -> void:
    var mat := mesh_instance.get_active_material(0) as ShaderMaterial
    if not mat:
        push_error("DissolveController: no ShaderMaterial on mesh")
        return

    _dissolve_tween = create_tween()
    _dissolve_tween.tween_method(
        func(v: float): mat.set_shader_parameter("dissolve_amount", v),
        0.0,
        1.0,
        dissolve_duration
    )
    _dissolve_tween.tween_callback(func(): mesh_instance.visible = false)
```

## Anti-Patterns

- **Branching in fragment shader**: `if (condition)` on GPU executes both branches. Use `mix(a, b, step(0.5, condition))` instead.
- **Texture samples in vertex shader for per-fragment detail**: Vertex shader has limited texture precision (depends on mesh density). Sample detail textures in fragment.
- **SCREEN_TEXTURE without hint**: `uniform sampler2D screen_texture` requires `hint_screen_texture` annotation in Godot 4, otherwise it binds incorrectly.
- **Complex post-process as canvas_item on every sprite**: One expensive shader on a full-screen quad costs the same as on a tiny sprite but covers far fewer pixels. Post-process goes on screen-covering quad.
- **`discard` for alpha blending**: `discard` prevents early-z optimization. For gradual transparency, use `ALPHA` with `blend_mix`. Reserve `discard` for binary alpha (foliage, windows) where the expense is worth the sorting simplicity.