use glam::Vec3;
use std::ops::Range;
#[derive(Clone, Debug)]
pub enum SpawnTrigger {
OnDeath,
OnCondition(String),
}
impl Default for SpawnTrigger {
fn default() -> Self {
Self::OnDeath
}
}
#[derive(Clone, Debug)]
pub struct SubEmitter {
pub parent_type: u32,
pub child_type: u32,
pub count: u32,
pub speed_min: f32,
pub speed_max: f32,
pub spread: f32,
pub inherit_velocity: f32,
pub child_lifetime: Option<f32>,
pub child_color: Option<Vec3>,
pub spawn_radius: f32,
pub trigger: SpawnTrigger,
}
impl SubEmitter {
pub fn new(parent_type: u32, child_type: u32) -> Self {
Self {
parent_type,
child_type,
count: 10,
speed_min: 0.5,
speed_max: 1.5,
spread: std::f32::consts::PI, inherit_velocity: 0.3,
child_lifetime: None,
child_color: None,
spawn_radius: 0.0,
trigger: SpawnTrigger::OnDeath,
}
}
pub fn on_condition(mut self, condition: impl Into<String>) -> Self {
self.trigger = SpawnTrigger::OnCondition(condition.into());
self
}
pub fn trigger(mut self, trigger: SpawnTrigger) -> Self {
self.trigger = trigger;
self
}
pub fn count(mut self, n: u32) -> Self {
self.count = n;
self
}
pub fn speed(mut self, range: Range<f32>) -> Self {
self.speed_min = range.start;
self.speed_max = range.end;
self
}
pub fn spread(mut self, radians: f32) -> Self {
self.spread = radians;
self
}
pub fn inherit_velocity(mut self, factor: f32) -> Self {
self.inherit_velocity = factor.clamp(0.0, 1.0);
self
}
pub fn child_lifetime(mut self, seconds: f32) -> Self {
self.child_lifetime = Some(seconds);
self
}
pub fn child_color(mut self, color: Vec3) -> Self {
self.child_color = Some(color);
self
}
pub fn spawn_radius(mut self, radius: f32) -> Self {
self.spawn_radius = radius;
self
}
pub(crate) fn child_spawning_wgsl(&self, emitter_index: usize) -> String {
let child_color_code = if let Some(color) = self.child_color {
format!(
"child.color = vec3<f32>({}, {}, {});",
color.x, color.y, color.z
)
} else {
"child.color = death.color;".to_string()
};
let child_lifetime_code = if let Some(lifetime) = self.child_lifetime {
format!("// Child lifetime set to {}", lifetime)
} else {
"// Child uses normal lifecycle".to_string()
};
format!(
r#"
// Sub-emitter {emitter_index}: Spawn children for parent type {parent_type}
if death.parent_type == {parent_type}u {{
let num_children = {count}u;
let speed_min = {speed_min:.6};
let speed_max = {speed_max:.6};
let spread = {spread:.6};
let inherit_vel = {inherit_velocity:.6};
let spawn_radius = {spawn_radius:.6};
// Spawn each child
for (var child_i = 0u; child_i < num_children; child_i++) {{
// Find a dead slot using atomic counter
let slot = atomicAdd(&next_child_slot, 1u);
if slot >= arrayLength(&particles) {{
break;
}}
// Search for actual dead particle starting from slot
var actual_slot = slot;
var found = false;
for (var search = 0u; search < 100u; search++) {{
let check_slot = (slot + search) % arrayLength(&particles);
if particles[check_slot].alive == 0u {{
actual_slot = check_slot;
found = true;
break;
}}
}}
if !found {{
continue;
}}
var child = particles[actual_slot];
// Random direction within spread cone
let seed = death_idx * 1000u + child_i * 7u + {emitter_index}u;
let theta = rand(seed) * 6.28318;
let phi = rand(seed + 1u) * spread;
let dir = vec3<f32>(
sin(phi) * cos(theta),
cos(phi),
sin(phi) * sin(theta)
);
// Random speed within range
let speed = speed_min + rand(seed + 2u) * (speed_max - speed_min);
// Random offset within spawn radius
let offset = rand_sphere(seed + 3u) * spawn_radius;
// Set child properties
child.position = death.position + offset;
child.velocity = death.velocity * inherit_vel + dir * speed;
child.particle_type = {child_type}u;
child.age = 0.0;
child.alive = 1u;
child.scale = 1.0;
{child_color_code}
{child_lifetime_code}
particles[actual_slot] = child;
}}
}}
"#,
emitter_index = emitter_index,
parent_type = self.parent_type,
child_type = self.child_type,
count = self.count,
speed_min = self.speed_min,
speed_max = self.speed_max,
spread = self.spread,
inherit_velocity = self.inherit_velocity,
spawn_radius = self.spawn_radius,
child_color_code = child_color_code,
child_lifetime_code = child_lifetime_code,
)
}
}
pub const MAX_DEATH_EVENTS: u32 = 4096;
pub const DEATH_EVENT_WGSL: &str = r#"
struct DeathEvent {
position: vec3<f32>,
parent_type: u32,
velocity: vec3<f32>,
_pad0: u32,
color: vec3<f32>,
_pad1: u32,
};
"#;
pub const DEATH_BUFFER_BINDINGS_WGSL: &str = r#"
@group(3) @binding(0)
var<storage, read_write> death_buffer: array<DeathEvent>;
@group(3) @binding(1)
var<storage, read_write> death_count: atomic<u32>;
@group(3) @binding(2)
var<storage, read_write> next_child_slot: atomic<u32>;
"#;
pub const RECORD_DEATH_WGSL: &str = r#"
// Record a particle death for sub-emitter processing
fn record_death(pos: vec3<f32>, vel: vec3<f32>, col: vec3<f32>, ptype: u32) {
let idx = atomicAdd(&death_count, 1u);
if idx < arrayLength(&death_buffer) {
death_buffer[idx].position = pos;
death_buffer[idx].velocity = vel;
death_buffer[idx].color = col;
death_buffer[idx].parent_type = ptype;
}
}
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sub_emitter_builder() {
let se = SubEmitter::new(0, 1)
.count(50)
.speed(1.0..3.0)
.spread(std::f32::consts::PI)
.inherit_velocity(0.5)
.child_lifetime(2.0)
.child_color(Vec3::new(1.0, 0.5, 0.0));
assert_eq!(se.parent_type, 0);
assert_eq!(se.child_type, 1);
assert_eq!(se.count, 50);
assert_eq!(se.speed_min, 1.0);
assert_eq!(se.speed_max, 3.0);
assert_eq!(se.inherit_velocity, 0.5);
assert!(se.child_lifetime.is_some());
assert!(se.child_color.is_some());
}
#[test]
fn test_inherit_velocity_clamping() {
let se = SubEmitter::new(0, 1).inherit_velocity(2.0);
assert_eq!(se.inherit_velocity, 1.0);
let se = SubEmitter::new(0, 1).inherit_velocity(-0.5);
assert_eq!(se.inherit_velocity, 0.0);
}
}