Skip to main content

PARTICLE_SHADER

Constant PARTICLE_SHADER 

Source
const PARTICLE_SHADER: &str = r#"
struct View {
    clip_from_world: mat4x4<f32>,
    view_from_world: mat4x4<f32>,
    camera_position: vec4<f32>,
};

struct Globals {
    time: f32,
    delta_time: f32,
    frame_count: u32,
    pad0: u32,
};

struct ParticleUniforms {
    clock: vec4<f32>,
};

@group(0) @binding(0) var<uniform> view: View;
@group(0) @binding(1) var<uniform> frame: Globals;
@group(1) @binding(0) var<uniform> particles: ParticleUniforms;

struct ParticleInput {
    @builtin(vertex_index) vertex_index: u32,
    @location(2) origin: vec4<f32>,
    @location(3) motion: vec4<f32>,
    @location(4) tint: vec4<f32>,
};

struct ParticleOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) colour: vec3<f32>,
    @location(1) corner: vec2<f32>,
    @location(2) @interpolate(flat) shape: u32,
};

@vertex
fn vs_main(input: ParticleInput) -> ParticleOutput {
    var corners = array<vec2<f32>, 6>(
        vec2<f32>(-1.0, -1.0),
        vec2<f32>( 1.0, -1.0),
        vec2<f32>(-1.0,  1.0),
        vec2<f32>(-1.0,  1.0),
        vec2<f32>( 1.0, -1.0),
        vec2<f32>( 1.0,  1.0),
    );
    let corner = corners[input.vertex_index];

    let phase = input.origin.w + particles.clock.x * input.motion.y;
    // The particle drifts around its own centre rather than spinning in place,
    // so the cloud reshapes instead of only rotating.
    let centre = vec3<f32>(
        input.origin.x + cos(phase) * input.motion.x,
        input.origin.y + sin(phase * 1.7) * input.motion.w,
        input.origin.z + sin(phase) * input.motion.x,
    );
    let size = input.motion.z * (0.85 + 0.15 * sin(phase * 3.0));

    // The camera's world-space axes are the view matrix's rows, which is what
    // keeps the sprite facing the viewer whichever way the camera is turned.
    let right = vec3<f32>(
        view.view_from_world[0][0],
        view.view_from_world[1][0],
        view.view_from_world[2][0],
    );
    let up = vec3<f32>(
        view.view_from_world[0][1],
        view.view_from_world[1][1],
        view.view_from_world[2][1],
    );
    let world = centre + right * (corner.x * size) + up * (corner.y * size);

    var out: ParticleOutput;
    out.position = view.clip_from_world * vec4<f32>(world, 1.0);
    out.corner = corner;
    // A flat fill: the sprite is one colour with no shading, and the spin is
    // the whole of its motion.
    out.colour = input.tint.rgb;
    out.shape = u32(input.tint.a + 0.5);
    return out;
}

@fragment
fn fs_main(input: ParticleOutput) -> @location(0) vec4<f32> {
    // Each sprite is one flat shape with a hard edge, drawn over the frame
    // rather than added to it, so its colour is exactly the tint it was given.
    if input.shape == 0u {
        if dot(input.corner, input.corner) > 1.0 {
            discard;
        }
    } else if input.shape == 1u {
        let point = input.corner;
        if length(point) > 1.0 || length(point - vec2<f32>(0.0, -0.62)) < 0.42 {
            discard;
        }
    } else {
        let radius = length(input.corner);
        if radius > 1.0 || radius < 0.55 {
            discard;
        }
    }
    return vec4<f32>(input.colour, 1.0);
}"#;
Expand description

The particle shader.

The mesh carries no vertex attributes at all: the six vertices of each sprite come from the vertex index, and the sprite’s own placement comes from the instance stream. The global group is the renderer’s own — the camera’s view and projection matrices — while the clock lives in the family’s own uniform block at the material group.