Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fragment Shaders

Fragment shaders control how each particle looks - its shape, glow, color effects, and more.

Basic Usage

#![allow(unused)]
fn main() {
Simulation::<MyParticle>::new()
    .with_fragment_shader(r#"
        let dist = length(in.uv);
        let glow = 1.0 / (dist * dist * 8.0 + 0.3);
        return vec4<f32>(in.color * glow, glow * 0.5);
    "#)
    .run();
}

Available Variables

In your fragment shader snippet, you have access to:

VariableTypeDescription
in.uvvec2<f32>Position within particle quad (-1 to 1, center is 0)
in.colorvec3<f32>Particle's color (from #[color] field)
uniforms.timef32Seconds since simulation start
uniforms.delta_timef32Seconds since last frame
uniforms.*variesAny custom uniforms defined via .with_uniform()

How It Works

Your snippet is injected into a fragment shader that runs for every pixel of every particle. The in.uv coordinates tell you where you are within the particle's billboard quad:

(-1,-1) -------- (1,-1)
   |               |
   |    (0,0)      |
   |               |
(-1,1) --------- (1,1)

The center is (0,0), so length(in.uv) gives distance from center.

Common Patterns

Soft Circle (Default Look)

#![allow(unused)]
fn main() {
.with_fragment_shader(r#"
    let dist = length(in.uv);
    if dist > 1.0 { discard; }
    let alpha = 1.0 - smoothstep(0.0, 1.0, dist);
    return vec4<f32>(in.color, alpha);
"#)
}

Glowing Particle

#![allow(unused)]
fn main() {
.with_fragment_shader(r#"
    let dist = length(in.uv);
    let glow = 1.0 / (dist * dist * 8.0 + 0.3);
    let alpha = clamp(glow * 0.5, 0.0, 1.0);
    return vec4<f32>(in.color * glow, alpha);
"#)
}

Ring

#![allow(unused)]
fn main() {
.with_fragment_shader(r#"
    let dist = length(in.uv);
    let ring = smoothstep(0.6, 0.7, dist) - smoothstep(0.8, 0.9, dist);
    return vec4<f32>(in.color, ring);
"#)
}

Pulsing

#![allow(unused)]
fn main() {
.with_fragment_shader(r#"
    let dist = length(in.uv);
    let pulse = sin(uniforms.time * 4.0) * 0.3 + 0.7;
    let glow = 1.0 / (dist * dist * 8.0 + 0.2);
    return vec4<f32>(in.color * glow * pulse, glow * 0.5);
"#)
}

Animated Interference

#![allow(unused)]
fn main() {
.with_fragment_shader(r#"
    let dist = length(in.uv);

    // Core
    let core = 1.0 - smoothstep(0.0, 0.3, dist);

    // Animated rings
    let rings = sin(dist * 20.0 - uniforms.time * 5.0) * 0.5 + 0.5;
    let ring_fade = exp(-dist * 3.0);

    let intensity = core + rings * ring_fade * 0.5;
    return vec4<f32>(in.color * intensity, intensity * 0.6);
"#)
}

Color Shift Based on Position

#![allow(unused)]
fn main() {
.with_fragment_shader(r#"
    let dist = length(in.uv);
    let glow = 1.0 / (dist * dist * 6.0 + 0.3);

    // Shift hue based on angle
    let angle = atan2(in.uv.y, in.uv.x);
    let hue_shift = angle / 6.28318;

    // Simple hue rotation (approximate)
    let shifted = vec3<f32>(
        in.color.r * cos(hue_shift * 6.28) - in.color.g * sin(hue_shift * 6.28),
        in.color.r * sin(hue_shift * 6.28) + in.color.g * cos(hue_shift * 6.28),
        in.color.b
    );

    return vec4<f32>(shifted * glow, glow * 0.5);
"#)
}

Sharp Core + Soft Halo

#![allow(unused)]
fn main() {
.with_fragment_shader(r#"
    let dist = length(in.uv);

    // Sharp inner core
    let core = 1.0 - smoothstep(0.0, 0.2, dist);

    // Soft outer glow
    let halo = 1.0 / (dist * dist * 4.0 + 0.5);

    let intensity = core * 2.0 + halo * 0.5;
    let alpha = clamp(intensity * 0.4, 0.0, 1.0);

    return vec4<f32>(in.color * intensity, alpha);
"#)
}

Tips

Coordinate System

  • in.uv ranges from -1 to 1
  • length(in.uv) = distance from center (0 at center, 1 at edge, >1 at corners)
  • Use in.uv * 0.5 + 0.5 to get 0-1 range for texture coordinates

Performance

  • Fragment shaders run per-pixel per-particle
  • Keep math simple for thousands of particles
  • Avoid loops if possible

Blending

Fragment shader output interacts with blend mode:

  • Additive: RGB values add together (bright + bright = brighter)
  • Alpha: Standard alpha compositing

For additive blending, the alpha channel still matters for intensity.

Debugging

Set solid colors to debug:

#![allow(unused)]
fn main() {
// Debug: show UV coordinates as colors
.with_fragment_shader(r#"
    return vec4<f32>(in.uv * 0.5 + 0.5, 0.0, 1.0);
"#)
}