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.