rdpe/
sub_emitter.rs

1//! Sub-emitter system for spawning particles on particle death.
2//!
3//! Sub-emitters allow particles to spawn child particles when they die,
4//! enabling effects like fireworks, explosions with debris, chain reactions,
5//! and biological reproduction.
6//!
7//! # How It Works
8//!
9//! 1. Parent particles are marked with a specific type
10//! 2. When a parent dies (via `Rule::Lifetime` or `kill_particle()`), a death event is recorded
11//! 3. A secondary compute pass reads death events and spawns children
12//! 4. Children inherit position from parent, with configurable velocity spread
13//!
14//! # Example
15//!
16//! ```ignore
17//! #[derive(ParticleType)]
18//! enum Firework {
19//!     Rocket,
20//!     Spark,
21//! }
22//!
23//! Simulation::<Particle>::new()
24//!     .with_particle_count(10_000)
25//!     .with_lifecycle(|l| l.lifetime(2.0))  // Rockets live 2 seconds
26//!     .with_sub_emitter(SubEmitter::new(Firework::Rocket, Firework::Spark)
27//!         .count(50)
28//!         .speed(1.0..3.0)
29//!         .spread(std::f32::consts::PI)  // Full sphere
30//!         .inherit_velocity(0.2)
31//!         .child_lifetime(1.0))
32//!     .run();
33//! ```
34//!
35//! # Multiple Sub-Emitters
36//!
37//! You can chain multiple sub-emitters for complex effects:
38//!
39//! ```ignore
40//! // Rockets spawn sparks, sparks spawn embers
41//! .with_sub_emitter(SubEmitter::new(Rocket, Spark).count(30))
42//! .with_sub_emitter(SubEmitter::new(Spark, Ember).count(5))
43//! ```
44
45use glam::Vec3;
46use std::ops::Range;
47
48/// Trigger condition for when a sub-emitter spawns children.
49#[derive(Clone, Debug)]
50pub enum SpawnTrigger {
51    /// Spawn when the parent particle dies (default).
52    /// This is the classic sub-emitter behavior for effects like fireworks.
53    OnDeath,
54
55    /// Spawn when a custom WGSL condition evaluates to true.
56    /// The condition has access to `p` (current particle) and `uniforms`.
57    ///
58    /// # Example
59    ///
60    /// ```ignore
61    /// // Spawn when energy is high
62    /// SpawnTrigger::OnCondition("p.energy > 0.9".into())
63    ///
64    /// // Spawn periodically (every ~1 second)
65    /// SpawnTrigger::OnCondition("fract(p.age) < uniforms.delta_time".into())
66    /// ```
67    OnCondition(String),
68}
69
70impl Default for SpawnTrigger {
71    fn default() -> Self {
72        Self::OnDeath
73    }
74}
75
76/// Configuration for a sub-emitter that spawns children when triggered.
77///
78/// # Example
79///
80/// ```ignore
81/// SubEmitter::new(ParentType, ChildType)
82///     .count(20)                    // Spawn 20 children per death
83///     .speed(1.0..2.0)              // Random speed in range
84///     .spread(PI / 4.0)             // 45-degree cone
85///     .inherit_velocity(0.5)        // 50% of parent velocity
86///     .child_lifetime(1.5)          // Children live 1.5 seconds
87///     .child_color(Vec3::new(1.0, 0.5, 0.0))  // Orange children
88/// ```
89#[derive(Clone, Debug)]
90pub struct SubEmitter {
91    /// Parent particle type that triggers sub-emission.
92    pub parent_type: u32,
93    /// Child particle type to spawn.
94    pub child_type: u32,
95    /// Number of children to spawn per trigger event.
96    pub count: u32,
97    /// Speed range for children (random within range).
98    pub speed_min: f32,
99    pub speed_max: f32,
100    /// Spread angle in radians (0 = laser, PI = hemisphere, TAU = full sphere).
101    pub spread: f32,
102    /// How much of parent's velocity children inherit (0.0 - 1.0).
103    pub inherit_velocity: f32,
104    /// Optional fixed lifetime for children (overrides any lifecycle rules).
105    pub child_lifetime: Option<f32>,
106    /// Optional fixed color for children.
107    pub child_color: Option<Vec3>,
108    /// Spawn radius around parent position.
109    pub spawn_radius: f32,
110    /// What triggers this sub-emitter (death, condition, etc.).
111    pub trigger: SpawnTrigger,
112}
113
114impl SubEmitter {
115    /// Create a new sub-emitter configuration.
116    ///
117    /// By default, triggers on parent death. Use `.on_condition()` to trigger
118    /// on a custom WGSL condition instead.
119    ///
120    /// # Arguments
121    ///
122    /// * `parent_type` - Type of particle that triggers sub-emission
123    /// * `child_type` - Type of particle to spawn as children
124    ///
125    /// # Example
126    ///
127    /// ```ignore
128    /// // Classic death-triggered spawning (fireworks)
129    /// SubEmitter::new(Firework::Rocket.into(), Firework::Spark.into())
130    ///
131    /// // Condition-triggered spawning (cell division)
132    /// SubEmitter::new(Cell::Parent.into(), Cell::Child.into())
133    ///     .on_condition("p.energy > 1.5")
134    ///     .count(2)
135    /// ```
136    pub fn new(parent_type: u32, child_type: u32) -> Self {
137        Self {
138            parent_type,
139            child_type,
140            count: 10,
141            speed_min: 0.5,
142            speed_max: 1.5,
143            spread: std::f32::consts::PI, // Hemisphere by default
144            inherit_velocity: 0.3,
145            child_lifetime: None,
146            child_color: None,
147            spawn_radius: 0.0,
148            trigger: SpawnTrigger::OnDeath,
149        }
150    }
151
152    /// Set the trigger to a custom WGSL condition.
153    ///
154    /// The condition is a WGSL boolean expression with access to:
155    /// - `p` - The current particle
156    /// - `uniforms.time` - Total elapsed time
157    /// - `uniforms.delta_time` - Frame delta time
158    ///
159    /// When the condition evaluates to true, spawn events are recorded
160    /// and children are spawned in a secondary pass.
161    ///
162    /// # Example
163    ///
164    /// ```ignore
165    /// // Spawn when energy exceeds threshold
166    /// SubEmitter::new(Parent, Child)
167    ///     .on_condition("p.energy > 0.9")
168    ///     .count(3)
169    ///
170    /// // Periodic spawning (approximately every second)
171    /// SubEmitter::new(Parent, Child)
172    ///     .on_condition("fract(p.age) < uniforms.delta_time")
173    ///     .count(1)
174    ///
175    /// // Spawn near the origin
176    /// SubEmitter::new(Parent, Child)
177    ///     .on_condition("length(p.position) < 0.2")
178    ///     .count(2)
179    /// ```
180    pub fn on_condition(mut self, condition: impl Into<String>) -> Self {
181        self.trigger = SpawnTrigger::OnCondition(condition.into());
182        self
183    }
184
185    /// Set the trigger type explicitly.
186    ///
187    /// # Example
188    ///
189    /// ```ignore
190    /// SubEmitter::new(Parent, Child)
191    ///     .trigger(SpawnTrigger::OnDeath)
192    /// ```
193    pub fn trigger(mut self, trigger: SpawnTrigger) -> Self {
194        self.trigger = trigger;
195        self
196    }
197
198    /// Set the number of children to spawn per parent death.
199    ///
200    /// # Example
201    ///
202    /// ```ignore
203    /// SubEmitter::new(Rocket, Spark).count(50)
204    /// ```
205    pub fn count(mut self, n: u32) -> Self {
206        self.count = n;
207        self
208    }
209
210    /// Set the speed range for children.
211    ///
212    /// Children will have a random speed within this range.
213    ///
214    /// # Example
215    ///
216    /// ```ignore
217    /// SubEmitter::new(Rocket, Spark).speed(1.0..3.0)
218    /// ```
219    pub fn speed(mut self, range: Range<f32>) -> Self {
220        self.speed_min = range.start;
221        self.speed_max = range.end;
222        self
223    }
224
225    /// Set the spread angle in radians.
226    ///
227    /// - `0` = All children move in same direction (laser)
228    /// - `PI/4` = 45-degree cone
229    /// - `PI/2` = 90-degree cone (hemisphere)
230    /// - `PI` = Full hemisphere
231    /// - `TAU` = Full sphere (all directions)
232    ///
233    /// # Example
234    ///
235    /// ```ignore
236    /// SubEmitter::new(Rocket, Spark).spread(std::f32::consts::PI)
237    /// ```
238    pub fn spread(mut self, radians: f32) -> Self {
239        self.spread = radians;
240        self
241    }
242
243    /// Set how much of parent's velocity children inherit.
244    ///
245    /// - `0.0` = Children ignore parent velocity
246    /// - `0.5` = Children inherit half of parent velocity
247    /// - `1.0` = Children inherit full parent velocity
248    ///
249    /// # Example
250    ///
251    /// ```ignore
252    /// SubEmitter::new(Rocket, Spark).inherit_velocity(0.5)
253    /// ```
254    pub fn inherit_velocity(mut self, factor: f32) -> Self {
255        self.inherit_velocity = factor.clamp(0.0, 1.0);
256        self
257    }
258
259    /// Set a fixed lifetime for children.
260    ///
261    /// This overrides any `Rule::Lifetime` for child particles.
262    /// If not set, children follow normal lifecycle rules.
263    ///
264    /// # Example
265    ///
266    /// ```ignore
267    /// SubEmitter::new(Rocket, Spark).child_lifetime(1.5)
268    /// ```
269    pub fn child_lifetime(mut self, seconds: f32) -> Self {
270        self.child_lifetime = Some(seconds);
271        self
272    }
273
274    /// Set a fixed color for children.
275    ///
276    /// If not set, children inherit parent's color.
277    ///
278    /// # Example
279    ///
280    /// ```ignore
281    /// SubEmitter::new(Rocket, Spark).child_color(Vec3::new(1.0, 0.8, 0.0))
282    /// ```
283    pub fn child_color(mut self, color: Vec3) -> Self {
284        self.child_color = Some(color);
285        self
286    }
287
288    /// Set spawn radius around parent position.
289    ///
290    /// Children spawn at random positions within this radius of the parent.
291    ///
292    /// # Example
293    ///
294    /// ```ignore
295    /// SubEmitter::new(Rocket, Spark).spawn_radius(0.1)
296    /// ```
297    pub fn spawn_radius(mut self, radius: f32) -> Self {
298        self.spawn_radius = radius;
299        self
300    }
301
302    /// Generate WGSL code for spawning children from death events.
303    ///
304    /// This code runs in a secondary compute pass.
305    pub(crate) fn child_spawning_wgsl(&self, emitter_index: usize) -> String {
306        let child_color_code = if let Some(color) = self.child_color {
307            format!(
308                "child.color = vec3<f32>({}, {}, {});",
309                color.x, color.y, color.z
310            )
311        } else {
312            "child.color = death.color;".to_string()
313        };
314
315        let child_lifetime_code = if let Some(lifetime) = self.child_lifetime {
316            format!("// Child lifetime set to {}", lifetime)
317        } else {
318            "// Child uses normal lifecycle".to_string()
319        };
320
321        format!(
322            r#"
323    // Sub-emitter {emitter_index}: Spawn children for parent type {parent_type}
324    if death.parent_type == {parent_type}u {{
325        let num_children = {count}u;
326        let speed_min = {speed_min:.6};
327        let speed_max = {speed_max:.6};
328        let spread = {spread:.6};
329        let inherit_vel = {inherit_velocity:.6};
330        let spawn_radius = {spawn_radius:.6};
331
332        // Spawn each child
333        for (var child_i = 0u; child_i < num_children; child_i++) {{
334            // Find a dead slot using atomic counter
335            let slot = atomicAdd(&next_child_slot, 1u);
336            if slot >= arrayLength(&particles) {{
337                break;
338            }}
339
340            // Search for actual dead particle starting from slot
341            var actual_slot = slot;
342            var found = false;
343            for (var search = 0u; search < 100u; search++) {{
344                let check_slot = (slot + search) % arrayLength(&particles);
345                if particles[check_slot].alive == 0u {{
346                    actual_slot = check_slot;
347                    found = true;
348                    break;
349                }}
350            }}
351
352            if !found {{
353                continue;
354            }}
355
356            var child = particles[actual_slot];
357
358            // Random direction within spread cone
359            let seed = death_idx * 1000u + child_i * 7u + {emitter_index}u;
360            let theta = rand(seed) * 6.28318;
361            let phi = rand(seed + 1u) * spread;
362            let dir = vec3<f32>(
363                sin(phi) * cos(theta),
364                cos(phi),
365                sin(phi) * sin(theta)
366            );
367
368            // Random speed within range
369            let speed = speed_min + rand(seed + 2u) * (speed_max - speed_min);
370
371            // Random offset within spawn radius
372            let offset = rand_sphere(seed + 3u) * spawn_radius;
373
374            // Set child properties
375            child.position = death.position + offset;
376            child.velocity = death.velocity * inherit_vel + dir * speed;
377            child.particle_type = {child_type}u;
378            child.age = 0.0;
379            child.alive = 1u;
380            child.scale = 1.0;
381            {child_color_code}
382            {child_lifetime_code}
383
384            particles[actual_slot] = child;
385        }}
386    }}
387"#,
388            emitter_index = emitter_index,
389            parent_type = self.parent_type,
390            child_type = self.child_type,
391            count = self.count,
392            speed_min = self.speed_min,
393            speed_max = self.speed_max,
394            spread = self.spread,
395            inherit_velocity = self.inherit_velocity,
396            spawn_radius = self.spawn_radius,
397            child_color_code = child_color_code,
398            child_lifetime_code = child_lifetime_code,
399        )
400    }
401}
402
403/// Maximum number of death events that can be recorded per frame.
404pub const MAX_DEATH_EVENTS: u32 = 4096;
405
406/// WGSL struct definition for death events.
407pub const DEATH_EVENT_WGSL: &str = r#"
408struct DeathEvent {
409    position: vec3<f32>,
410    parent_type: u32,
411    velocity: vec3<f32>,
412    _pad0: u32,
413    color: vec3<f32>,
414    _pad1: u32,
415};
416"#;
417
418/// WGSL bindings for death buffer system.
419pub const DEATH_BUFFER_BINDINGS_WGSL: &str = r#"
420@group(3) @binding(0)
421var<storage, read_write> death_buffer: array<DeathEvent>;
422
423@group(3) @binding(1)
424var<storage, read_write> death_count: atomic<u32>;
425
426@group(3) @binding(2)
427var<storage, read_write> next_child_slot: atomic<u32>;
428"#;
429
430/// WGSL helper function to record a death event.
431pub const RECORD_DEATH_WGSL: &str = r#"
432// Record a particle death for sub-emitter processing
433fn record_death(pos: vec3<f32>, vel: vec3<f32>, col: vec3<f32>, ptype: u32) {
434    let idx = atomicAdd(&death_count, 1u);
435    if idx < arrayLength(&death_buffer) {
436        death_buffer[idx].position = pos;
437        death_buffer[idx].velocity = vel;
438        death_buffer[idx].color = col;
439        death_buffer[idx].parent_type = ptype;
440    }
441}
442"#;
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn test_sub_emitter_builder() {
450        let se = SubEmitter::new(0, 1)
451            .count(50)
452            .speed(1.0..3.0)
453            .spread(std::f32::consts::PI)
454            .inherit_velocity(0.5)
455            .child_lifetime(2.0)
456            .child_color(Vec3::new(1.0, 0.5, 0.0));
457
458        assert_eq!(se.parent_type, 0);
459        assert_eq!(se.child_type, 1);
460        assert_eq!(se.count, 50);
461        assert_eq!(se.speed_min, 1.0);
462        assert_eq!(se.speed_max, 3.0);
463        assert_eq!(se.inherit_velocity, 0.5);
464        assert!(se.child_lifetime.is_some());
465        assert!(se.child_color.is_some());
466    }
467
468    #[test]
469    fn test_inherit_velocity_clamping() {
470        let se = SubEmitter::new(0, 1).inherit_velocity(2.0);
471        assert_eq!(se.inherit_velocity, 1.0);
472
473        let se = SubEmitter::new(0, 1).inherit_velocity(-0.5);
474        assert_eq!(se.inherit_velocity, 0.0);
475    }
476}