1#[derive(Debug, Clone, Copy)]
7pub enum EmitterShape {
8 Point,
10 Cone {
12 half_angle: f32,
14 },
15 Sphere {
17 radius: f32,
19 },
20 Box {
22 half_extents: [f32; 3],
24 },
25}
26pub struct GridParticleCollision {
30 pub cell_size: f32,
32 pub particle_radius: f32,
34 pub restitution: f32,
36}
37impl GridParticleCollision {
38 pub fn new(cell_size: f32, particle_radius: f32, restitution: f32) -> Self {
40 Self {
41 cell_size,
42 particle_radius,
43 restitution,
44 }
45 }
46 pub fn resolve(&self, buffer: &mut ParticleBuffer) {
51 let n = buffer.count;
52 let alive: Vec<usize> = (0..n).filter(|&i| buffer.is_alive(i)).collect();
53 let na = alive.len();
54 if na < 2 {
55 return;
56 }
57 let mut grid: std::collections::HashMap<(i32, i32, i32), Vec<usize>> =
58 std::collections::HashMap::new();
59 for &i in &alive {
60 let cx = (buffer.positions_x[i] / self.cell_size).floor() as i32;
61 let cy = (buffer.positions_y[i] / self.cell_size).floor() as i32;
62 let cz = (buffer.positions_z[i] / self.cell_size).floor() as i32;
63 grid.entry((cx, cy, cz)).or_default().push(i);
64 }
65 let diameter = 2.0 * self.particle_radius;
66 let mut dvx = vec![0.0f32; n];
67 let mut dvy = vec![0.0f32; n];
68 let mut dvz = vec![0.0f32; n];
69 for (&(cx, cy, cz), cell) in &grid {
70 for dx in -1i32..=1 {
71 for dy in -1i32..=1 {
72 for dz in -1i32..=1 {
73 let nb_key = (cx + dx, cy + dy, cz + dz);
74 if let Some(nb_cell) = grid.get(&nb_key) {
75 for &i in cell {
76 for &j in nb_cell {
77 if j <= i {
78 continue;
79 }
80 let dx_p = buffer.positions_x[j] - buffer.positions_x[i];
81 let dy_p = buffer.positions_y[j] - buffer.positions_y[i];
82 let dz_p = buffer.positions_z[j] - buffer.positions_z[i];
83 let dist = (dx_p * dx_p + dy_p * dy_p + dz_p * dz_p).sqrt();
84 if dist < diameter && dist > 1e-6 {
85 let overlap = diameter - dist;
86 let nx = dx_p / dist;
87 let ny = dy_p / dist;
88 let nz = dz_p / dist;
89 let rvx = buffer.velocities_x[j] - buffer.velocities_x[i];
90 let rvy = buffer.velocities_y[j] - buffer.velocities_y[i];
91 let rvz = buffer.velocities_z[j] - buffer.velocities_z[i];
92 let rv_n = rvx * nx + rvy * ny + rvz * nz;
93 if rv_n < 0.0 {
94 let j_impulse = -(1.0 + self.restitution) * rv_n
95 / (1.0 / buffer.masses[i] + 1.0 / buffer.masses[j]);
96 let inv_mi = 1.0 / buffer.masses[i];
97 let inv_mj = 1.0 / buffer.masses[j];
98 dvx[i] -= j_impulse * inv_mi * nx;
99 dvy[i] -= j_impulse * inv_mi * ny;
100 dvz[i] -= j_impulse * inv_mi * nz;
101 dvx[j] += j_impulse * inv_mj * nx;
102 dvy[j] += j_impulse * inv_mj * ny;
103 dvz[j] += j_impulse * inv_mj * nz;
104 }
105 let push = overlap * 0.5;
106 buffer.positions_x[i] -= push * nx;
107 buffer.positions_y[i] -= push * ny;
108 buffer.positions_z[i] -= push * nz;
109 buffer.positions_x[j] += push * nx;
110 buffer.positions_y[j] += push * ny;
111 buffer.positions_z[j] += push * nz;
112 }
113 }
114 }
115 }
116 }
117 }
118 }
119 }
120 for &i in &alive {
121 buffer.velocities_x[i] += dvx[i];
122 buffer.velocities_y[i] += dvy[i];
123 buffer.velocities_z[i] += dvz[i];
124 }
125 }
126}
127#[derive(Debug, Clone)]
129pub struct ParticleStats {
130 pub active: usize,
132 pub min_pos: [f32; 3],
134 pub max_pos: [f32; 3],
136 pub avg_speed: f32,
138 pub total_kinetic_energy: f32,
140}
141impl ParticleStats {
142 pub fn compute(buffer: &ParticleBuffer) -> Self {
144 let mut active = 0usize;
145 let mut min_pos = [f32::MAX; 3];
146 let mut max_pos = [f32::MIN; 3];
147 let mut sum_speed = 0.0f32;
148 let mut total_ke = 0.0f32;
149 for i in 0..buffer.count {
150 if !buffer.is_alive(i) {
151 continue;
152 }
153 active += 1;
154 let x = buffer.positions_x[i];
155 let y = buffer.positions_y[i];
156 let z = buffer.positions_z[i];
157 min_pos[0] = min_pos[0].min(x);
158 min_pos[1] = min_pos[1].min(y);
159 min_pos[2] = min_pos[2].min(z);
160 max_pos[0] = max_pos[0].max(x);
161 max_pos[1] = max_pos[1].max(y);
162 max_pos[2] = max_pos[2].max(z);
163 let vx = buffer.velocities_x[i];
164 let vy = buffer.velocities_y[i];
165 let vz = buffer.velocities_z[i];
166 let speed = (vx * vx + vy * vy + vz * vz).sqrt();
167 sum_speed += speed;
168 total_ke += 0.5 * buffer.masses[i] * speed * speed;
169 }
170 let avg_speed = if active > 0 {
171 sum_speed / active as f32
172 } else {
173 0.0
174 };
175 if active == 0 {
176 min_pos = [0.0; 3];
177 max_pos = [0.0; 3];
178 }
179 Self {
180 active,
181 min_pos,
182 max_pos,
183 avg_speed,
184 total_kinetic_energy: total_ke,
185 }
186 }
187}
188pub struct ParticleIntegrator;
190impl ParticleIntegrator {
191 pub fn integrate(buffer: &mut ParticleBuffer, dt: f32) {
197 for i in 0..buffer.count {
198 if !buffer.is_alive(i) {
199 continue;
200 }
201 buffer.positions_x[i] += buffer.velocities_x[i] * dt;
202 buffer.positions_y[i] += buffer.velocities_y[i] * dt;
203 buffer.positions_z[i] += buffer.velocities_z[i] * dt;
204 buffer.ages[i] += dt;
205 buffer.lifetimes[i] -= dt;
206 }
207 }
208}
209pub struct ParticleRepulsion {
211 pub strength: f32,
213 pub radius: f32,
215}
216impl ParticleRepulsion {
217 pub fn apply(&self, buffer: &mut ParticleBuffer, dt: f32) {
221 let n = buffer.count;
222 let alive: Vec<usize> = (0..n).filter(|&i| buffer.is_alive(i)).collect();
223 let mut fx = vec![0.0f32; n];
224 let mut fy = vec![0.0f32; n];
225 let mut fz = vec![0.0f32; n];
226 for (ai, &i) in alive.iter().enumerate() {
227 for &j in alive.iter().skip(ai + 1) {
228 let dx = buffer.positions_x[j] - buffer.positions_x[i];
229 let dy = buffer.positions_y[j] - buffer.positions_y[i];
230 let dz = buffer.positions_z[j] - buffer.positions_z[i];
231 let dist2 = dx * dx + dy * dy + dz * dz;
232 let dist = dist2.sqrt();
233 if dist >= self.radius || dist < 1e-6 {
234 continue;
235 }
236 let overlap = self.radius - dist;
237 let f = self.strength * overlap / dist;
238 fx[i] -= f * dx;
239 fy[i] -= f * dy;
240 fz[i] -= f * dz;
241 fx[j] += f * dx;
242 fy[j] += f * dy;
243 fz[j] += f * dz;
244 }
245 }
246 for &i in &alive {
247 buffer.velocities_x[i] += fx[i] * dt / buffer.masses[i];
248 buffer.velocities_y[i] += fy[i] * dt / buffer.masses[i];
249 buffer.velocities_z[i] += fz[i] * dt / buffer.masses[i];
250 }
251 }
252}
253#[derive(Debug, Clone, Copy)]
255pub enum EmissionMode {
256 Burst {
258 count: usize,
260 },
261 Continuous {
263 rate: f32,
265 },
266}
267#[derive(Debug, Clone)]
269pub struct SortedParticleRenderData {
270 pub render_data: ParticleRenderData,
272 pub sort_key: f32,
274 pub buffer_index: usize,
276}
277pub struct DragForce {
279 pub coefficient: f32,
281}
282impl DragForce {
283 pub fn apply(&self, buffer: &mut ParticleBuffer, dt: f32) {
285 let factor = (1.0 - self.coefficient * dt).max(0.0);
286 for i in 0..buffer.count {
287 if buffer.is_alive(i) {
288 buffer.velocities_x[i] *= factor;
289 buffer.velocities_y[i] *= factor;
290 buffer.velocities_z[i] *= factor;
291 }
292 }
293 }
294}
295pub struct GpuParticleLayout;
298impl GpuParticleLayout {
299 pub fn stride() -> usize {
303 8
304 }
305 pub fn to_f32_buffer(buffer: &ParticleBuffer) -> Vec<f32> {
307 let stride = Self::stride();
308 let mut out = Vec::with_capacity(buffer.count * stride);
309 for i in 0..buffer.count {
310 out.push(buffer.positions_x[i]);
311 out.push(buffer.positions_y[i]);
312 out.push(buffer.positions_z[i]);
313 out.push(buffer.velocities_x[i]);
314 out.push(buffer.velocities_y[i]);
315 out.push(buffer.velocities_z[i]);
316 out.push(buffer.masses[i]);
317 out.push(buffer.lifetimes[i]);
318 }
319 out
320 }
321 pub fn from_f32_buffer(data: &[f32], count: usize) -> ParticleBuffer {
325 let stride = Self::stride();
326 assert_eq!(data.len(), count * stride, "data length mismatch");
327 let mut buf = ParticleBuffer::new(count);
328 for i in 0..count {
329 let base = i * stride;
330 buf.positions_x[i] = data[base];
331 buf.positions_y[i] = data[base + 1];
332 buf.positions_z[i] = data[base + 2];
333 buf.velocities_x[i] = data[base + 3];
334 buf.velocities_y[i] = data[base + 4];
335 buf.velocities_z[i] = data[base + 5];
336 buf.masses[i] = data[base + 6];
337 buf.lifetimes[i] = data[base + 7];
338 }
339 buf
340 }
341}
342#[derive(Debug, Clone)]
344pub struct ParticleSystemStats {
345 pub basic: ParticleStats,
347 pub capacity: usize,
349 pub fill_ratio: f32,
351 pub total_kinetic_energy: f32,
353 pub mean_age: f32,
355 pub max_age: f32,
357 pub velocity_std_dev: f32,
359}
360impl ParticleSystemStats {
361 pub fn compute_extended(buffer: &ParticleBuffer) -> Self {
363 let basic = ParticleStats::compute(buffer);
364 let capacity = buffer.count;
365 let fill_ratio = if capacity > 0 {
366 basic.active as f32 / capacity as f32
367 } else {
368 0.0
369 };
370 let mut sum_age = 0.0f32;
371 let mut max_age = 0.0f32;
372 let mut sum_v2 = 0.0f32;
373 let active = basic.active;
374 for i in 0..buffer.count {
375 if !buffer.is_alive(i) {
376 continue;
377 }
378 sum_age += buffer.ages[i];
379 max_age = max_age.max(buffer.ages[i]);
380 let vx = buffer.velocities_x[i];
381 let vy = buffer.velocities_y[i];
382 let vz = buffer.velocities_z[i];
383 sum_v2 += vx * vx + vy * vy + vz * vz;
384 }
385 let mean_age = if active > 0 {
386 sum_age / active as f32
387 } else {
388 0.0
389 };
390 let mean_v2 = if active > 0 {
391 sum_v2 / active as f32
392 } else {
393 0.0
394 };
395 let velocity_std_dev = (mean_v2 - basic.avg_speed * basic.avg_speed)
396 .max(0.0)
397 .sqrt();
398 Self {
399 total_kinetic_energy: basic.total_kinetic_energy,
400 basic,
401 capacity,
402 fill_ratio,
403 mean_age,
404 max_age,
405 velocity_std_dev,
406 }
407 }
408 pub fn is_near_capacity(&self, threshold: f32) -> bool {
410 self.fill_ratio >= threshold
411 }
412}
413pub struct VortexForceField {
415 pub center: [f32; 2],
417 pub angular_velocity: f32,
419 pub radius: f32,
421}
422impl VortexForceField {
423 pub fn apply(&self, buffer: &mut ParticleBuffer, dt: f32) {
425 for i in 0..buffer.count {
426 if !buffer.is_alive(i) {
427 continue;
428 }
429 let dx = buffer.positions_x[i] - self.center[0];
430 let dz = buffer.positions_z[i] - self.center[1];
431 let dist = (dx * dx + dz * dz).sqrt();
432 if dist > self.radius || dist < 1e-6 {
433 continue;
434 }
435 let factor = self.angular_velocity * (1.0 - dist / self.radius) * dt;
436 buffer.velocities_x[i] += -dz / dist * factor;
437 buffer.velocities_z[i] += dx / dist * factor;
438 }
439 }
440}
441pub struct FloorCollision {
443 pub y: f32,
445 pub restitution: f32,
447}
448impl FloorCollision {
449 pub fn apply(&self, buffer: &mut ParticleBuffer) {
451 for i in 0..buffer.count {
452 if !buffer.is_alive(i) {
453 continue;
454 }
455 if buffer.positions_y[i] < self.y {
456 buffer.positions_y[i] = self.y;
457 if buffer.velocities_y[i] < 0.0 {
458 buffer.velocities_y[i] = -buffer.velocities_y[i] * self.restitution;
459 }
460 }
461 }
462 }
463}
464pub struct SimpleRng {
466 pub(super) state: u64,
467}
468impl SimpleRng {
469 pub fn new(seed: u64) -> Self {
471 Self {
472 state: seed ^ 0x853c_49e6_748f_ea9b,
473 }
474 }
475 pub fn next_u64(&mut self) -> u64 {
477 self.state = self
478 .state
479 .wrapping_mul(6_364_136_223_846_793_005)
480 .wrapping_add(1_442_695_040_888_963_407);
481 self.state
482 }
483 pub fn next_f32(&mut self) -> f32 {
485 let bits = (self.next_u64() >> 40) as u32;
486 (bits as f32) / (1u32 << 24) as f32
487 }
488 pub fn next_f32_range(&mut self, min: f32, max: f32) -> f32 {
490 min + self.next_f32() * (max - min)
491 }
492 pub fn next_unit_sphere(&mut self) -> [f32; 3] {
494 loop {
495 let x = self.next_f32_range(-1.0, 1.0);
496 let y = self.next_f32_range(-1.0, 1.0);
497 let z = self.next_f32_range(-1.0, 1.0);
498 let len2 = x * x + y * y + z * z;
499 if len2 > 1e-10 && len2 <= 1.0 {
500 let inv = 1.0 / len2.sqrt();
501 return [x * inv, y * inv, z * inv];
502 }
503 }
504 }
505}
506pub struct BoundingBoxKill {
508 pub min: [f32; 3],
510 pub max: [f32; 3],
512}
513impl BoundingBoxKill {
514 pub fn apply(&self, buffer: &mut ParticleBuffer) {
516 for i in 0..buffer.count {
517 if !buffer.is_alive(i) {
518 continue;
519 }
520 let x = buffer.positions_x[i];
521 let y = buffer.positions_y[i];
522 let z = buffer.positions_z[i];
523 if x < self.min[0]
524 || x > self.max[0]
525 || y < self.min[1]
526 || y > self.max[1]
527 || z < self.min[2]
528 || z > self.max[2]
529 {
530 buffer.kill(i);
531 }
532 }
533 }
534}
535pub struct ParticleSystem {
537 pub buffer: ParticleBuffer,
539 pub emitters: Vec<ParticleEmitter>,
541 pub gravity: GravityForce,
543 pub drag: DragForce,
545 pub floor: Option<FloorCollision>,
547 pub rng: SimpleRng,
549 pub time: f32,
551}
552impl ParticleSystem {
553 pub fn new(capacity: usize) -> Self {
555 Self {
556 buffer: ParticleBuffer::new(capacity),
557 emitters: Vec::new(),
558 gravity: GravityForce {
559 g: [0.0, -9.81, 0.0],
560 },
561 drag: DragForce { coefficient: 0.01 },
562 floor: None,
563 rng: SimpleRng::new(12345),
564 time: 0.0,
565 }
566 }
567 pub fn add_emitter(&mut self, emitter: ParticleEmitter) -> usize {
569 let idx = self.emitters.len();
570 self.emitters.push(emitter);
571 idx
572 }
573 pub fn step(&mut self, dt: f32) {
577 let seed_base = self.rng.next_u64();
578 for (idx, emitter) in self.emitters.iter_mut().enumerate() {
579 let seed = seed_base ^ (idx as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
580 emitter.emit(&mut self.buffer, dt, seed);
581 }
582 self.gravity.apply(&mut self.buffer, dt);
583 self.drag.apply(&mut self.buffer, dt);
584 if let Some(ref floor) = self.floor {
585 floor.apply(&mut self.buffer);
586 }
587 ParticleIntegrator::integrate(&mut self.buffer, dt);
588 self.time += dt;
589 }
590}
591pub struct RadialForceField {
593 pub center: [f32; 3],
595 pub strength: f32,
597 pub falloff: f32,
599 pub min_distance: f32,
601}
602impl RadialForceField {
603 pub fn apply(&self, buffer: &mut ParticleBuffer, dt: f32) {
605 for i in 0..buffer.count {
606 if !buffer.is_alive(i) {
607 continue;
608 }
609 let dx = self.center[0] - buffer.positions_x[i];
610 let dy = self.center[1] - buffer.positions_y[i];
611 let dz = self.center[2] - buffer.positions_z[i];
612 let dist = (dx * dx + dy * dy + dz * dz).sqrt().max(self.min_distance);
613 let force = self.strength / dist.powf(self.falloff);
614 let inv_dist = 1.0 / dist;
615 buffer.velocities_x[i] += force * dx * inv_dist * dt;
616 buffer.velocities_y[i] += force * dy * inv_dist * dt;
617 buffer.velocities_z[i] += force * dz * inv_dist * dt;
618 }
619 }
620}
621#[derive(Debug, Clone)]
623pub struct ParticleRenderData {
624 pub position: [f32; 3],
626 pub color: [f32; 4],
628 pub size: f32,
630 pub age_normalized: f32,
632}
633pub struct GravityForce {
635 pub g: [f32; 3],
637}
638impl GravityForce {
639 pub fn apply(&self, buffer: &mut ParticleBuffer, dt: f32) {
641 for i in 0..buffer.count {
642 if buffer.is_alive(i) {
643 buffer.velocities_x[i] += self.g[0] * dt;
644 buffer.velocities_y[i] += self.g[1] * dt;
645 buffer.velocities_z[i] += self.g[2] * dt;
646 }
647 }
648 }
649}
650pub struct ParticleBuffer {
652 pub positions_x: Vec<f32>,
654 pub positions_y: Vec<f32>,
656 pub positions_z: Vec<f32>,
658 pub velocities_x: Vec<f32>,
660 pub velocities_y: Vec<f32>,
662 pub velocities_z: Vec<f32>,
664 pub masses: Vec<f32>,
666 pub lifetimes: Vec<f32>,
668 pub ages: Vec<f32>,
670 pub count: usize,
672}
673impl ParticleBuffer {
674 pub fn new(capacity: usize) -> Self {
676 Self {
677 positions_x: vec![0.0; capacity],
678 positions_y: vec![0.0; capacity],
679 positions_z: vec![0.0; capacity],
680 velocities_x: vec![0.0; capacity],
681 velocities_y: vec![0.0; capacity],
682 velocities_z: vec![0.0; capacity],
683 masses: vec![1.0; capacity],
684 lifetimes: vec![-1.0; capacity],
685 ages: vec![0.0; capacity],
686 count: capacity,
687 }
688 }
689 pub fn add_particle(
692 &mut self,
693 pos: [f32; 3],
694 vel: [f32; 3],
695 mass: f32,
696 lifetime: f32,
697 ) -> Option<usize> {
698 for i in 0..self.count {
699 if self.lifetimes[i] < 0.0 {
700 self.positions_x[i] = pos[0];
701 self.positions_y[i] = pos[1];
702 self.positions_z[i] = pos[2];
703 self.velocities_x[i] = vel[0];
704 self.velocities_y[i] = vel[1];
705 self.velocities_z[i] = vel[2];
706 self.masses[i] = mass;
707 self.lifetimes[i] = lifetime;
708 self.ages[i] = 0.0;
709 return Some(i);
710 }
711 }
712 None
713 }
714 pub fn get_position(&self, i: usize) -> [f32; 3] {
716 [
717 self.positions_x[i],
718 self.positions_y[i],
719 self.positions_z[i],
720 ]
721 }
722 pub fn get_velocity(&self, i: usize) -> [f32; 3] {
724 [
725 self.velocities_x[i],
726 self.velocities_y[i],
727 self.velocities_z[i],
728 ]
729 }
730 pub fn set_position(&mut self, i: usize, p: [f32; 3]) {
732 self.positions_x[i] = p[0];
733 self.positions_y[i] = p[1];
734 self.positions_z[i] = p[2];
735 }
736 pub fn set_velocity(&mut self, i: usize, v: [f32; 3]) {
738 self.velocities_x[i] = v[0];
739 self.velocities_y[i] = v[1];
740 self.velocities_z[i] = v[2];
741 }
742 pub fn is_alive(&self, i: usize) -> bool {
744 self.lifetimes[i] >= 0.0
745 }
746 pub fn kill(&mut self, i: usize) {
748 self.lifetimes[i] = -1.0;
749 }
750 pub fn active_count(&self) -> usize {
752 (0..self.count).filter(|&i| self.is_alive(i)).count()
753 }
754}
755pub struct ParticleLifetimeManager {
757 pub total_spawned: usize,
759 pub total_expired: usize,
761 pub min_observed_lifetime: f32,
763 pub max_observed_lifetime: f32,
765}
766impl ParticleLifetimeManager {
767 pub fn new() -> Self {
769 Self {
770 total_spawned: 0,
771 total_expired: 0,
772 min_observed_lifetime: f32::MAX,
773 max_observed_lifetime: 0.0,
774 }
775 }
776 pub fn record_spawn(&mut self, lifetime: f32) {
778 self.total_spawned += 1;
779 self.min_observed_lifetime = self.min_observed_lifetime.min(lifetime);
780 self.max_observed_lifetime = self.max_observed_lifetime.max(lifetime);
781 }
782 pub fn record_expiration(&mut self) {
784 self.total_expired += 1;
785 }
786 pub fn retire_expired(&mut self, buffer: &mut ParticleBuffer) -> usize {
789 let mut count = 0;
790 for i in 0..buffer.count {
791 if buffer.lifetimes[i] < 0.0 && buffer.ages[i] > 0.0 {
792 let _ = i;
793 }
794 if buffer.is_alive(i) && buffer.lifetimes[i] < 0.0 {
795 count += 1;
796 self.record_expiration();
797 }
798 }
799 count
800 }
801 pub fn alive_fraction(&self, buffer: &ParticleBuffer) -> f32 {
803 if self.total_spawned == 0 {
804 return 0.0;
805 }
806 buffer.active_count() as f32 / self.total_spawned as f32
807 }
808}
809pub struct ParticleEmitter {
811 pub position: [f32; 3],
813 pub emit_rate: f32,
815 pub emit_accumulator: f32,
817 pub initial_velocity: [f32; 3],
819 pub velocity_spread: f32,
821 pub lifetime_min: f32,
823 pub lifetime_max: f32,
825 pub mass: f32,
827 pub active: bool,
829}
830impl ParticleEmitter {
831 pub fn new(pos: [f32; 3], rate: f32, vel: [f32; 3], lifetime: f32) -> Self {
833 Self {
834 position: pos,
835 emit_rate: rate,
836 emit_accumulator: 0.0,
837 initial_velocity: vel,
838 velocity_spread: 0.0,
839 lifetime_min: lifetime,
840 lifetime_max: lifetime,
841 mass: 1.0,
842 active: true,
843 }
844 }
845 pub fn emit(&mut self, buffer: &mut ParticleBuffer, dt: f32, rng_seed: u64) -> usize {
847 if !self.active {
848 return 0;
849 }
850 let mut rng = SimpleRng::new(rng_seed);
851 self.emit_accumulator += self.emit_rate * dt;
852 let to_emit = self.emit_accumulator.floor() as usize;
853 self.emit_accumulator -= to_emit as f32;
854 let mut spawned = 0usize;
855 for _ in 0..to_emit {
856 let spread_dir = rng.next_unit_sphere();
857 let vel = [
858 self.initial_velocity[0] + spread_dir[0] * self.velocity_spread,
859 self.initial_velocity[1] + spread_dir[1] * self.velocity_spread,
860 self.initial_velocity[2] + spread_dir[2] * self.velocity_spread,
861 ];
862 let lt = rng.next_f32_range(self.lifetime_min, self.lifetime_max);
863 if buffer
864 .add_particle(self.position, vel, self.mass, lt)
865 .is_some()
866 {
867 spawned += 1;
868 }
869 }
870 spawned
871 }
872}
873pub struct GpuParticleEmitter {
875 pub position: [f32; 3],
877 pub shape: EmitterShape,
879 pub mode: EmissionMode,
881 pub initial_velocity: [f32; 3],
883 pub lifetime: f32,
885 pub mass: f32,
887 pub active: bool,
889 pub accumulator: f32,
891 pub(super) rng: SimpleRng,
893}
894impl GpuParticleEmitter {
895 pub fn new_continuous(position: [f32; 3], rate: f32, lifetime: f32) -> Self {
897 Self {
898 position,
899 shape: EmitterShape::Point,
900 mode: EmissionMode::Continuous { rate },
901 initial_velocity: [0.0, 1.0, 0.0],
902 lifetime,
903 mass: 1.0,
904 active: true,
905 accumulator: 0.0,
906 rng: SimpleRng::new(0xdeadbeef),
907 }
908 }
909 pub fn new_burst(position: [f32; 3], count: usize, lifetime: f32) -> Self {
911 Self {
912 position,
913 shape: EmitterShape::Point,
914 mode: EmissionMode::Burst { count },
915 initial_velocity: [0.0, 1.0, 0.0],
916 lifetime,
917 mass: 1.0,
918 active: true,
919 accumulator: 0.0,
920 rng: SimpleRng::new(0xcafebabe),
921 }
922 }
923 pub fn emit(&mut self, buffer: &mut ParticleBuffer, dt: f32) -> usize {
925 if !self.active {
926 return 0;
927 }
928 let to_emit = match self.mode {
929 EmissionMode::Burst { count } => {
930 self.active = false;
931 count
932 }
933 EmissionMode::Continuous { rate } => {
934 self.accumulator += rate * dt;
935 let n = self.accumulator.floor() as usize;
936 self.accumulator -= n as f32;
937 n
938 }
939 };
940 let mut spawned = 0;
941 for _ in 0..to_emit {
942 let pos = self.sample_position();
943 let vel = self.initial_velocity;
944 if buffer
945 .add_particle(pos, vel, self.mass, self.lifetime)
946 .is_some()
947 {
948 spawned += 1;
949 }
950 }
951 spawned
952 }
953 fn sample_position(&mut self) -> [f32; 3] {
955 match self.shape {
956 EmitterShape::Point => self.position,
957 EmitterShape::Cone { half_angle } => {
958 let _ = half_angle;
959 self.position
960 }
961 EmitterShape::Sphere { radius } => {
962 let dir = self.rng.next_unit_sphere();
963 [
964 self.position[0] + dir[0] * radius,
965 self.position[1] + dir[1] * radius,
966 self.position[2] + dir[2] * radius,
967 ]
968 }
969 EmitterShape::Box { half_extents } => {
970 let x = self.rng.next_f32_range(-half_extents[0], half_extents[0]);
971 let y = self.rng.next_f32_range(-half_extents[1], half_extents[1]);
972 let z = self.rng.next_f32_range(-half_extents[2], half_extents[2]);
973 [
974 self.position[0] + x,
975 self.position[1] + y,
976 self.position[2] + z,
977 ]
978 }
979 }
980 }
981 pub fn burst_count(&self) -> usize {
983 match self.mode {
984 EmissionMode::Burst { count } => count,
985 EmissionMode::Continuous { .. } => 0,
986 }
987 }
988}