struct SpriteParticle {
position: vec4<f32>,
velocity: vec4<f32>,
color: vec4<f32>,
size_lifetime: vec4<f32>,
emitter_data: vec4<f32>,
physics: vec4<f32>,
color_start: vec4<f32>,
color_end: vec4<f32>,
}
struct SpriteEmitterData {
position_anchor: vec4<f32>,
velocity_min: vec4<f32>,
velocity_max: vec4<f32>,
lifetime_range: vec4<f32>,
size_start: vec4<f32>,
size_end: vec4<f32>,
gravity_drag: vec4<f32>,
rotation_range: vec4<f32>,
rotation_speed_range: vec4<f32>,
color_start: vec4<f32>,
color_end: vec4<f32>,
uv_min_max: vec4<f32>,
texture_depth: vec4<f32>,
shape_params: vec4<f32>,
spawn_count: u32,
shape_type: u32,
blend_mode: u32,
_padding: u32,
}
struct SimParams {
delta_time: f32,
time: f32,
max_particles: u32,
_padding: u32,
}
struct DrawIndirect {
vertex_count: u32,
instance_count: atomic<u32>,
first_vertex: u32,
first_instance: u32,
}
@group(0) @binding(0)
var<storage, read_write> particles: array<SpriteParticle>;
@group(0) @binding(1)
var<storage, read> emitters: array<SpriteEmitterData>;
@group(0) @binding(2)
var<uniform> params: SimParams;
@group(0) @binding(3)
var<storage, read_write> free_indices: array<u32>;
@group(0) @binding(4)
var<storage, read_write> free_count: atomic<u32>;
@group(0) @binding(5)
var<storage, read_write> alive_indices: array<u32>;
@group(0) @binding(6)
var<storage, read_write> alive_count: atomic<u32>;
@group(0) @binding(7)
var<storage, read_write> draw_indirect: DrawIndirect;
fn hash(seed: u32) -> u32 {
var s = seed;
s = s ^ 2747636419u;
s = s * 2654435769u;
s = s ^ (s >> 16u);
s = s * 2654435769u;
s = s ^ (s >> 16u);
s = s * 2654435769u;
return s;
}
fn random_float(seed: ptr<function, u32>) -> f32 {
*seed = hash(*seed);
return f32(*seed) / 4294967295.0;
}
fn random_range(seed: ptr<function, u32>, min_val: f32, max_val: f32) -> f32 {
return min_val + random_float(seed) * (max_val - min_val);
}
@compute @workgroup_size(256)
fn update(@builtin(global_invocation_id) global_id: vec3<u32>) {
let index = global_id.x;
if (index >= params.max_particles) {
return;
}
var particle = particles[index];
let is_alive = particle.size_lifetime.w > 0.0;
if (!is_alive) {
return;
}
let age = particle.size_lifetime.z;
let lifetime = particle.size_lifetime.w;
let new_age = age + params.delta_time;
if (new_age >= lifetime) {
particle.size_lifetime.w = 0.0;
let free_slot = atomicAdd(&free_count, 1u);
free_indices[free_slot] = index;
particles[index] = particle;
return;
}
let gravity_x = particle.physics.x;
let gravity_y = particle.physics.y;
let drag = particle.physics.z;
let rotation_speed = particle.physics.w;
let drag_factor = 1.0 - drag * params.delta_time;
var velocity = particle.velocity.xy * drag_factor;
velocity = velocity + vec2<f32>(gravity_x, gravity_y) * params.delta_time;
let new_position = particle.position.xy + velocity * params.delta_time;
let new_rotation = particle.position.z + rotation_speed * params.delta_time;
let life_ratio = new_age / lifetime;
let size_start_x = particle.size_lifetime.x;
let size_start_y = particle.size_lifetime.y;
let size_end_x = particle.emitter_data.x;
let size_end_y = particle.emitter_data.y;
let current_size_x = mix(size_start_x, size_end_x, life_ratio);
let current_size_y = mix(size_start_y, size_end_y, life_ratio);
let color = mix(particle.color_start, particle.color_end, life_ratio);
particle.position = vec4<f32>(new_position.x, new_position.y, new_rotation, 1.0);
particle.velocity = vec4<f32>(velocity.x, velocity.y, current_size_x, current_size_y);
particle.color = color;
particle.size_lifetime.z = new_age;
let alive_slot = atomicAdd(&alive_count, 1u);
alive_indices[alive_slot] = index;
atomicAdd(&draw_indirect.instance_count, 1u);
particles[index] = particle;
}
@compute @workgroup_size(1)
fn reset_counters() {
atomicStore(&alive_count, 0u);
atomicStore(&draw_indirect.instance_count, 0u);
}
@compute @workgroup_size(256)
fn spawn(
@builtin(workgroup_id) workgroup_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>
) {
let emitter_index = workgroup_id.x;
let spawn_index = local_id.x;
let emitter = emitters[emitter_index];
if (spawn_index >= emitter.spawn_count) {
return;
}
let old_free_count = atomicSub(&free_count, 1u);
if (old_free_count == 0u) {
atomicAdd(&free_count, 1u);
return;
}
let particle_index = free_indices[old_free_count - 1u];
var seed = hash(particle_index * 1973u + u32(params.time * 10000.0) + spawn_index * 7919u + emitter_index * 6997u);
var spawn_offset = vec2<f32>(0.0);
let shape_type = emitter.shape_type;
if (shape_type == 1u) {
let radius = emitter.shape_params.x;
let angle = random_float(&seed) * 6.28318530718;
let distance = sqrt(random_float(&seed)) * radius;
spawn_offset = vec2<f32>(cos(angle) * distance, sin(angle) * distance);
} else if (shape_type == 2u) {
let half_x = emitter.shape_params.x;
let half_y = emitter.shape_params.y;
spawn_offset = vec2<f32>(
random_range(&seed, -half_x, half_x),
random_range(&seed, -half_y, half_y)
);
}
let position = emitter.position_anchor.xy + spawn_offset;
let velocity = vec2<f32>(
random_range(&seed, emitter.velocity_min.x, emitter.velocity_max.x),
random_range(&seed, emitter.velocity_min.y, emitter.velocity_max.y)
);
let lifetime = random_range(&seed, emitter.lifetime_range.x, emitter.lifetime_range.y);
let rotation = random_range(&seed, emitter.rotation_range.x, emitter.rotation_range.y);
let rotation_speed = random_range(&seed, emitter.rotation_speed_range.x, emitter.rotation_speed_range.y);
var particle: SpriteParticle;
particle.position = vec4<f32>(position.x, position.y, rotation, 1.0);
particle.velocity = vec4<f32>(velocity.x, velocity.y, emitter.size_start.x, emitter.size_start.y);
particle.color = emitter.color_start;
particle.size_lifetime = vec4<f32>(emitter.size_start.x, emitter.size_start.y, 0.0, lifetime);
particle.emitter_data = vec4<f32>(
emitter.size_end.x,
emitter.size_end.y,
emitter.texture_depth.x,
emitter.texture_depth.y
);
particle.physics = vec4<f32>(
emitter.gravity_drag.x,
emitter.gravity_drag.y,
emitter.gravity_drag.z,
rotation_speed
);
particle.color_start = emitter.color_start;
particle.color_end = emitter.color_end;
particles[particle_index] = particle;
}