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