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}