1use glam::{Vec3, Vec4};
5use super::emitter::{Particle, ParticleTag, lcg_next};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct ForceFieldId(pub u32);
11
12#[derive(Debug, Clone, Copy)]
16pub enum FalloffMode {
17 Constant,
19 Linear,
21 InverseSquare { min_dist: f32 },
23 SmoothStep,
25 Annular { inner_radius: f32 },
27}
28
29impl FalloffMode {
30 pub fn factor(&self, t: f32, dist: f32, _radius: f32) -> f32 {
32 match self {
33 FalloffMode::Constant => 1.0,
34 FalloffMode::Linear => (1.0 - t).max(0.0),
35 FalloffMode::InverseSquare { min_dist } => {
36 let d = dist.max(*min_dist);
37 1.0 / (d * d)
38 }
39 FalloffMode::SmoothStep => {
40 let t = t.clamp(0.0, 1.0);
41 1.0 - (3.0 * t * t - 2.0 * t * t * t)
42 }
43 FalloffMode::Annular { inner_radius } => {
44 if dist < *inner_radius { 0.0 } else { (1.0 - t).max(0.0) }
45 }
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
54pub enum TagMask {
55 All,
57 Include(ParticleTag),
59 Exclude(ParticleTag),
61}
62
63impl TagMask {
64 pub fn matches(&self, tag: ParticleTag) -> bool {
65 match self {
66 TagMask::All => true,
67 TagMask::Include(m) => tag.contains(*m) || m.0 == 0,
68 TagMask::Exclude(m) => !tag.contains(*m),
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
77pub struct GravityWell {
78 pub position: Vec3,
79 pub strength: f32,
80 pub radius: f32,
81 pub falloff: FalloffMode,
82 pub repulsive: bool,
84 pub absorb_radius: f32,
86}
87
88impl GravityWell {
89 pub fn new(position: Vec3, strength: f32, radius: f32) -> Self {
90 Self { position, strength, radius, falloff: FalloffMode::InverseSquare { min_dist: 0.1 }, repulsive: false, absorb_radius: 0.0 }
91 }
92
93 pub fn repulsor(position: Vec3, strength: f32, radius: f32) -> Self {
94 Self { repulsive: true, ..Self::new(position, strength, radius) }
95 }
96
97 pub fn acceleration(&self, p_pos: Vec3) -> Vec3 {
98 let delta = self.position - p_pos;
99 let dist = delta.length();
100 if dist > self.radius || dist < 1e-6 { return Vec3::ZERO; }
101 let dir = delta / dist;
102 let t = dist / self.radius;
103 let factor = self.falloff.factor(t, dist, self.radius);
104 let sign = if self.repulsive { -1.0 } else { 1.0 };
105 dir * (self.strength * factor * sign)
106 }
107
108 pub fn should_absorb(&self, p_pos: Vec3) -> bool {
109 self.absorb_radius > 0.0 && (self.position - p_pos).length() <= self.absorb_radius
110 }
111}
112
113#[derive(Debug, Clone)]
117pub struct VortexField {
118 pub position: Vec3,
119 pub axis: Vec3,
121 pub strength: f32,
122 pub radius: f32,
123 pub falloff: FalloffMode,
124 pub radial_pull: f32,
126 pub axial_pull: f32,
128}
129
130impl VortexField {
131 pub fn new(position: Vec3, axis: Vec3, strength: f32, radius: f32) -> Self {
132 Self {
133 position, axis: axis.normalize_or_zero(),
134 strength, radius, falloff: FalloffMode::Linear,
135 radial_pull: 0.0, axial_pull: 0.0,
136 }
137 }
138
139 pub fn tornado(position: Vec3, strength: f32, radius: f32) -> Self {
140 Self { radial_pull: -strength * 0.3, axial_pull: strength * 0.5, ..Self::new(position, Vec3::Y, strength, radius) }
141 }
142
143 pub fn acceleration(&self, p_pos: Vec3) -> Vec3 {
144 let to_axis_origin = p_pos - self.position;
145 let along_axis = self.axis * to_axis_origin.dot(self.axis);
147 let radial_vec = to_axis_origin - along_axis;
148 let dist = radial_vec.length();
149
150 if dist > self.radius || dist < 1e-6 { return Vec3::ZERO; }
151
152 let radial_dir = radial_vec / dist;
153 let tangent_dir = self.axis.cross(radial_dir).normalize_or_zero();
154 let t = dist / self.radius;
155 let factor = self.falloff.factor(t, dist, self.radius);
156
157 let tangent = tangent_dir * (self.strength * factor);
158 let radial = radial_dir * (self.radial_pull * factor);
159 let axial = self.axis * (self.axial_pull * factor);
160
161 tangent + radial + axial
162 }
163}
164
165#[derive(Debug, Clone)]
169pub struct TurbulenceField {
170 pub strength: f32,
171 pub frequency: f32, pub time_speed: f32, pub octaves: u32, pub lacunarity: f32, pub persistence: f32, pub radius: f32, pub position: Vec3,
178 time: f32,
179}
180
181impl TurbulenceField {
182 pub fn new(strength: f32, frequency: f32) -> Self {
183 Self {
184 strength, frequency, time_speed: 0.5,
185 octaves: 3, lacunarity: 2.0, persistence: 0.5,
186 radius: 0.0, position: Vec3::ZERO, time: 0.0,
187 }
188 }
189
190 pub fn tick(&mut self, dt: f32) { self.time += dt * self.time_speed; }
191
192 pub fn acceleration(&self, p_pos: Vec3) -> Vec3 {
193 if self.radius > 0.0 && (p_pos - self.position).length() > self.radius {
194 return Vec3::ZERO;
195 }
196
197 let t = self.time;
199 let nx = self.fbm(p_pos + Vec3::new(0.0, 100.5, 300.2), t);
200 let ny = self.fbm(p_pos + Vec3::new(100.1, 0.0, 200.3), t);
201 let nz = self.fbm(p_pos + Vec3::new(200.8, 300.1, 0.0), t);
202
203 Vec3::new(nx, ny, nz) * self.strength
204 }
205
206 fn fbm(&self, pos: Vec3, time: f32) -> f32 {
207 let mut value = 0.0_f32;
208 let mut amplitude = 1.0_f32;
209 let mut frequency = self.frequency;
210 let mut max_value = 0.0_f32;
211
212 for _ in 0..self.octaves {
213 value += self.value_noise_3d(pos * frequency, time) * amplitude;
214 max_value += amplitude;
215 amplitude *= self.persistence;
216 frequency *= self.lacunarity;
217 }
218
219 if max_value > 0.0 { value / max_value } else { 0.0 }
220 }
221
222 fn value_noise_3d(&self, pos: Vec3, time: f32) -> f32 {
223 let ix = pos.x.floor() as i32;
225 let iy = pos.y.floor() as i32;
226 let iz = pos.z.floor() as i32;
227 let it = time.floor() as i32;
228
229 let fx = pos.x - ix as f32;
231 let fy = pos.y - iy as f32;
232 let fz = pos.z - iz as f32;
233
234 let ux = fx * fx * (3.0 - 2.0 * fx);
236 let uy = fy * fy * (3.0 - 2.0 * fy);
237 let uz = fz * fz * (3.0 - 2.0 * fz);
238
239 let h = |x: i32, y: i32, z: i32, t: i32| -> f32 {
241 let mut s = (x as u64).wrapping_mul(1619)
242 .wrapping_add((y as u64).wrapping_mul(31337))
243 .wrapping_add((z as u64).wrapping_mul(6971))
244 .wrapping_add((t as u64).wrapping_mul(1013904223))
245 ^ 0x5851F42D4C957F2D;
246 s ^= s >> 33;
247 s = s.wrapping_mul(0xFF51AFD7ED558CCD);
248 s ^= s >> 33;
249 (s as f32 / u64::MAX as f32) * 2.0 - 1.0
250 };
251
252 let v000 = h(ix, iy, iz, it);
253 let v100 = h(ix+1, iy, iz, it);
254 let v010 = h(ix, iy+1, iz, it);
255 let v110 = h(ix+1, iy+1, iz, it);
256 let v001 = h(ix, iy, iz+1, it);
257 let v101 = h(ix+1, iy, iz+1, it);
258 let v011 = h(ix, iy+1, iz+1, it);
259 let v111 = h(ix+1, iy+1, iz+1, it);
260
261 let lerp = |a: f32, b: f32, t: f32| a + t * (b - a);
262
263 lerp(
264 lerp(lerp(v000, v100, ux), lerp(v010, v110, ux), uy),
265 lerp(lerp(v001, v101, ux), lerp(v011, v111, ux), uy),
266 uz,
267 )
268 }
269}
270
271#[derive(Debug, Clone)]
275pub struct WindZone {
276 pub direction: Vec3, pub speed: f32,
278 pub gust_strength: f32, pub gust_frequency: f32, pub bounds_min: Option<Vec3>,
281 pub bounds_max: Option<Vec3>,
282 time: f32,
283 gust_phase: f32,
284}
285
286impl WindZone {
287 pub fn new(direction: Vec3, speed: f32) -> Self {
288 Self {
289 direction: direction.normalize_or_zero(), speed,
290 gust_strength: speed * 0.3, gust_frequency: 0.5,
291 bounds_min: None, bounds_max: None,
292 time: 0.0, gust_phase: 0.0,
293 }
294 }
295
296 pub fn global(direction: Vec3, speed: f32) -> Self { Self::new(direction, speed) }
297
298 pub fn bounded(mut self, min: Vec3, max: Vec3) -> Self {
299 self.bounds_min = Some(min);
300 self.bounds_max = Some(max);
301 self
302 }
303
304 pub fn tick(&mut self, dt: f32) {
305 self.time += dt;
306 self.gust_phase = self.time * self.gust_frequency * std::f32::consts::TAU;
307 }
308
309 pub fn acceleration(&self, p_pos: Vec3) -> Vec3 {
310 if let (Some(bmin), Some(bmax)) = (self.bounds_min, self.bounds_max) {
311 if p_pos.x < bmin.x || p_pos.x > bmax.x ||
312 p_pos.y < bmin.y || p_pos.y > bmax.y ||
313 p_pos.z < bmin.z || p_pos.z > bmax.z {
314 return Vec3::ZERO;
315 }
316 }
317 let gust = self.gust_phase.sin() * self.gust_strength;
318 self.direction * (self.speed + gust)
319 }
320}
321
322#[derive(Debug, Clone)]
326pub struct AttractorRepulsor {
327 pub position: Vec3,
328 pub strength: f32,
329 pub radius: f32,
330 pub mode: AttractorMode,
331 pub falloff: FalloffMode,
332}
333
334#[derive(Debug, Clone, Copy, PartialEq)]
335pub enum AttractorMode {
336 Attract,
337 Repel,
338 Orbit,
339}
340
341impl AttractorRepulsor {
342 pub fn attractor(position: Vec3, strength: f32, radius: f32) -> Self {
343 Self { position, strength, radius, mode: AttractorMode::Attract, falloff: FalloffMode::Linear }
344 }
345
346 pub fn repulsor(position: Vec3, strength: f32, radius: f32) -> Self {
347 Self { position, strength, radius, mode: AttractorMode::Repel, falloff: FalloffMode::Linear }
348 }
349
350 pub fn orbit(position: Vec3, strength: f32, radius: f32) -> Self {
351 Self { position, strength, radius, mode: AttractorMode::Orbit, falloff: FalloffMode::SmoothStep }
352 }
353
354 pub fn acceleration(&self, p_pos: Vec3) -> Vec3 {
355 let delta = self.position - p_pos;
356 let dist = delta.length();
357 if dist > self.radius || dist < 1e-6 { return Vec3::ZERO; }
358 let dir = delta / dist;
359 let t = dist / self.radius;
360 let factor = self.falloff.factor(t, dist, self.radius);
361
362 match self.mode {
363 AttractorMode::Attract => dir * (self.strength * factor),
364 AttractorMode::Repel => -dir * (self.strength * factor),
365 AttractorMode::Orbit => {
366 let up = Vec3::Y;
367 let tangent = dir.cross(up).normalize_or_zero();
368 tangent * (self.strength * factor)
369 }
370 }
371 }
372}
373
374#[derive(Debug, Clone)]
378pub struct DragField {
379 pub coefficient: f32, pub quadratic: bool, pub bounds_min: Option<Vec3>,
382 pub bounds_max: Option<Vec3>,
383}
384
385impl DragField {
386 pub fn new(coefficient: f32) -> Self {
387 Self { coefficient, quadratic: false, bounds_min: None, bounds_max: None }
388 }
389
390 pub fn quadratic(coefficient: f32) -> Self {
391 Self { quadratic: true, ..Self::new(coefficient) }
392 }
393
394 pub fn acceleration(&self, p_pos: Vec3, velocity: Vec3) -> Vec3 {
395 if let (Some(bmin), Some(bmax)) = (self.bounds_min, self.bounds_max) {
396 if p_pos.x < bmin.x || p_pos.x > bmax.x ||
397 p_pos.y < bmin.y || p_pos.y > bmax.y ||
398 p_pos.z < bmin.z || p_pos.z > bmax.z {
399 return Vec3::ZERO;
400 }
401 }
402 if self.quadratic {
403 -velocity * velocity.length() * self.coefficient
404 } else {
405 -velocity * self.coefficient
406 }
407 }
408}
409
410#[derive(Debug, Clone)]
414pub struct BuoyancyField {
415 pub up: Vec3,
417 pub fluid_density: f32,
419 pub gravity: f32,
421 pub surface_height: Option<f32>,
423}
424
425impl BuoyancyField {
426 pub fn air(gravity: f32) -> Self {
427 Self { up: Vec3::Y, fluid_density: 1.2, gravity, surface_height: None }
428 }
429
430 pub fn water(gravity: f32, surface_y: f32) -> Self {
431 Self { up: Vec3::Y, fluid_density: 1000.0, gravity, surface_height: Some(surface_y) }
432 }
433
434 pub fn smoke_in_air() -> Self {
435 Self { up: Vec3::Y, fluid_density: 1.8, gravity: 9.81, surface_height: None }
437 }
438
439 pub fn acceleration(&self, p_pos: Vec3, mass: f32, size: f32) -> Vec3 {
441 if let Some(sy) = self.surface_height {
442 if p_pos.y > sy { return Vec3::ZERO; }
443 }
444 let radius = size * 0.5;
446 let volume = (4.0 / 3.0) * std::f32::consts::PI * radius * radius * radius;
447 let buoyant_force = self.fluid_density * volume * self.gravity;
448 let weight = mass * self.gravity;
449 let net_force = buoyant_force - weight;
450 self.up * (net_force / mass.max(1e-6))
451 }
452}
453
454#[derive(Debug, Clone)]
458pub struct ForceField {
459 pub id: ForceFieldId,
460 pub enabled: bool,
461 pub strength_scale: f32,
462 pub tag_mask: TagMask,
463 pub kind: ForceFieldKind,
464 pub priority: i32,
465}
466
467#[derive(Debug, Clone)]
469pub enum ForceFieldKind {
470 GravityWell(GravityWell),
471 Vortex(VortexField),
472 Turbulence(TurbulenceField),
473 Wind(WindZone),
474 Attractor(AttractorRepulsor),
475 Drag(DragField),
476 Buoyancy(BuoyancyField),
477 Gravity { acceleration: Vec3 },
479 Spline {
481 position: Vec3,
482 axis: Vec3,
483 radius: f32,
484 curve: Vec<(f32, f32)>,
486 },
487}
488
489impl ForceField {
490 pub fn new(id: ForceFieldId, kind: ForceFieldKind) -> Self {
491 Self { id, enabled: true, strength_scale: 1.0, tag_mask: TagMask::All, kind, priority: 0 }
492 }
493
494 pub fn with_tag_mask(mut self, mask: TagMask) -> Self { self.tag_mask = mask; self }
495 pub fn with_scale(mut self, s: f32) -> Self { self.strength_scale = s; self }
496 pub fn with_priority(mut self, p: i32) -> Self { self.priority = p; self }
497 pub fn disabled(mut self) -> Self { self.enabled = false; self }
498
499 pub fn apply(&self, particle: &Particle) -> Vec3 {
501 if !self.enabled { return Vec3::ZERO; }
502 if !self.tag_mask.matches(particle.tag) { return Vec3::ZERO; }
503
504 let raw = match &self.kind {
505 ForceFieldKind::GravityWell(gw) => gw.acceleration(particle.position),
506 ForceFieldKind::Vortex(vx) => vx.acceleration(particle.position),
507 ForceFieldKind::Turbulence(tb) => tb.acceleration(particle.position),
508 ForceFieldKind::Wind(wz) => wz.acceleration(particle.position),
509 ForceFieldKind::Attractor(at) => at.acceleration(particle.position),
510 ForceFieldKind::Drag(dr) => dr.acceleration(particle.position, particle.velocity),
511 ForceFieldKind::Buoyancy(by) => by.acceleration(particle.position, particle.mass, particle.size),
512 ForceFieldKind::Gravity { acceleration } => *acceleration,
513 ForceFieldKind::Spline { position, axis, radius, curve } => {
514 let delta = particle.position - *position;
515 let dist = delta.length();
516 if dist > *radius || dist < 1e-6 || curve.is_empty() {
517 Vec3::ZERO
518 } else {
519 let t = dist / radius;
520 let mag = sample_curve(curve, t);
521 let dir = (*axis).normalize_or_zero();
522 dir * mag
523 }
524 }
525 };
526
527 raw * self.strength_scale
528 }
529
530 pub fn tick(&mut self, dt: f32) {
531 match &mut self.kind {
532 ForceFieldKind::Turbulence(tb) => tb.tick(dt),
533 ForceFieldKind::Wind(wz) => wz.tick(dt),
534 _ => {}
535 }
536 }
537
538 pub fn marks_for_death(&self, particle: &Particle) -> bool {
539 if let ForceFieldKind::GravityWell(gw) = &self.kind {
540 return gw.should_absorb(particle.position);
541 }
542 false
543 }
544}
545
546fn sample_curve(curve: &[(f32, f32)], t: f32) -> f32 {
547 if curve.len() == 1 { return curve[0].1; }
548 let i = curve.partition_point(|(ct, _)| *ct <= t);
549 if i == 0 { return curve[0].1; }
550 if i >= curve.len() { return curve.last().unwrap().1; }
551 let (t0, v0) = curve[i - 1];
552 let (t1, v1) = curve[i];
553 let f = (t - t0) / (t1 - t0).max(1e-6);
554 v0 + f * (v1 - v0)
555}
556
557#[derive(Debug, Clone, Copy, PartialEq)]
561pub enum ForceBlendMode {
562 Additive,
564 Override,
566 Multiply,
568 Average,
570}
571
572#[derive(Debug, Clone)]
574pub struct ForceComposite {
575 pub fields: Vec<ForceField>,
576 pub blend_mode: ForceBlendMode,
577 pub global_scale: f32,
578}
579
580impl ForceComposite {
581 pub fn new() -> Self {
582 Self { fields: Vec::new(), blend_mode: ForceBlendMode::Additive, global_scale: 1.0 }
583 }
584
585 pub fn with_blend(mut self, mode: ForceBlendMode) -> Self { self.blend_mode = mode; self }
586
587 pub fn add(&mut self, field: ForceField) { self.fields.push(field); }
588 pub fn remove(&mut self, id: ForceFieldId) { self.fields.retain(|f| f.id != id); }
589 pub fn get_mut(&mut self, id: ForceFieldId) -> Option<&mut ForceField> {
590 self.fields.iter_mut().find(|f| f.id == id)
591 }
592
593 pub fn tick(&mut self, dt: f32) {
594 for f in &mut self.fields { f.tick(dt); }
595 }
596
597 pub fn net_acceleration(&self, particle: &Particle) -> Vec3 {
599 let enabled: Vec<&ForceField> = self.fields.iter().filter(|f| f.enabled).collect();
600 if enabled.is_empty() { return Vec3::ZERO; }
601
602 let result = match self.blend_mode {
603 ForceBlendMode::Additive => {
604 enabled.iter().map(|f| f.apply(particle)).fold(Vec3::ZERO, |a, b| a + b)
605 }
606 ForceBlendMode::Override => {
607 enabled.iter()
608 .map(|f| f.apply(particle))
609 .max_by(|a, b| a.length_squared().partial_cmp(&b.length_squared()).unwrap())
610 .unwrap_or(Vec3::ZERO)
611 }
612 ForceBlendMode::Multiply => {
613 enabled.iter().map(|f| f.apply(particle))
614 .fold(Vec3::ONE, |a, b| Vec3::new(a.x * b.x, a.y * b.y, a.z * b.z))
615 }
616 ForceBlendMode::Average => {
617 let sum = enabled.iter().map(|f| f.apply(particle)).fold(Vec3::ZERO, |a, b| a + b);
618 sum / enabled.len() as f32
619 }
620 };
621
622 result * self.global_scale
623 }
624
625 pub fn apply_to_particle(&self, particle: &mut Particle) {
627 particle.acceleration += self.net_acceleration(particle);
628 }
629
630 pub fn apply_and_cull(&self, particles: &mut Vec<Particle>) {
632 for p in particles.iter_mut() {
633 p.acceleration += self.net_acceleration(p);
634 }
635 particles.retain(|p| {
637 !self.fields.iter().any(|f| f.marks_for_death(p))
638 });
639 }
640}
641
642impl Default for ForceComposite {
643 fn default() -> Self { Self::new() }
644}
645
646pub struct ForceFieldWorld {
650 pub composite: ForceComposite,
651 next_id: u32,
652 pub gravity: Vec3,
654 pub gravity_enabled: bool,
655}
656
657impl ForceFieldWorld {
658 pub fn new() -> Self {
659 Self {
660 composite: ForceComposite::new(),
661 next_id: 1,
662 gravity: Vec3::new(0.0, -9.81, 0.0),
663 gravity_enabled: true,
664 }
665 }
666
667 fn alloc_id(&mut self) -> ForceFieldId {
668 let id = ForceFieldId(self.next_id);
669 self.next_id += 1;
670 id
671 }
672
673 pub fn add_gravity_well(&mut self, pos: Vec3, strength: f32, radius: f32) -> ForceFieldId {
674 let id = self.alloc_id();
675 self.composite.add(ForceField::new(id, ForceFieldKind::GravityWell(GravityWell::new(pos, strength, radius))));
676 id
677 }
678
679 pub fn add_vortex(&mut self, pos: Vec3, axis: Vec3, strength: f32, radius: f32) -> ForceFieldId {
680 let id = self.alloc_id();
681 self.composite.add(ForceField::new(id, ForceFieldKind::Vortex(VortexField::new(pos, axis, strength, radius))));
682 id
683 }
684
685 pub fn add_turbulence(&mut self, strength: f32, frequency: f32) -> ForceFieldId {
686 let id = self.alloc_id();
687 self.composite.add(ForceField::new(id, ForceFieldKind::Turbulence(TurbulenceField::new(strength, frequency))));
688 id
689 }
690
691 pub fn add_wind(&mut self, direction: Vec3, speed: f32) -> ForceFieldId {
692 let id = self.alloc_id();
693 self.composite.add(ForceField::new(id, ForceFieldKind::Wind(WindZone::new(direction, speed))));
694 id
695 }
696
697 pub fn add_attractor(&mut self, pos: Vec3, strength: f32, radius: f32) -> ForceFieldId {
698 let id = self.alloc_id();
699 self.composite.add(ForceField::new(id, ForceFieldKind::Attractor(AttractorRepulsor::attractor(pos, strength, radius))));
700 id
701 }
702
703 pub fn add_repulsor(&mut self, pos: Vec3, strength: f32, radius: f32) -> ForceFieldId {
704 let id = self.alloc_id();
705 self.composite.add(ForceField::new(id, ForceFieldKind::Attractor(AttractorRepulsor::repulsor(pos, strength, radius))));
706 id
707 }
708
709 pub fn add_drag(&mut self, coefficient: f32) -> ForceFieldId {
710 let id = self.alloc_id();
711 self.composite.add(ForceField::new(id, ForceFieldKind::Drag(DragField::new(coefficient))));
712 id
713 }
714
715 pub fn add_buoyancy(&mut self, gravity: f32) -> ForceFieldId {
716 let id = self.alloc_id();
717 self.composite.add(ForceField::new(id, ForceFieldKind::Buoyancy(BuoyancyField::air(gravity))));
718 id
719 }
720
721 pub fn add_field(&mut self, kind: ForceFieldKind) -> ForceFieldId {
722 let id = self.alloc_id();
723 self.composite.add(ForceField::new(id, kind));
724 id
725 }
726
727 pub fn remove_field(&mut self, id: ForceFieldId) {
728 self.composite.remove(id);
729 }
730
731 pub fn get_mut(&mut self, id: ForceFieldId) -> Option<&mut ForceField> {
732 self.composite.get_mut(id)
733 }
734
735 pub fn tick(&mut self, dt: f32) {
736 self.composite.tick(dt);
737 }
738
739 pub fn apply_to_particles(&self, particles: &mut Vec<Particle>) {
740 if self.gravity_enabled {
742 for p in particles.iter_mut() {
743 p.acceleration += self.gravity;
744 }
745 }
746 self.composite.apply_and_cull(particles);
747 }
748
749 pub fn field_count(&self) -> usize { self.composite.fields.len() }
750}
751
752impl Default for ForceFieldWorld {
753 fn default() -> Self { Self::new() }
754}
755
756pub struct ForcePresets;
760
761impl ForcePresets {
762 pub fn earth_gravity() -> ForceFieldKind {
764 ForceFieldKind::Gravity { acceleration: Vec3::new(0.0, -9.81, 0.0) }
765 }
766
767 pub fn moon_gravity() -> ForceFieldKind {
769 ForceFieldKind::Gravity { acceleration: Vec3::new(0.0, -1.62, 0.0) }
770 }
771
772 pub fn anti_gravity(strength: f32) -> ForceFieldKind {
774 ForceFieldKind::Gravity { acceleration: Vec3::new(0.0, strength, 0.0) }
775 }
776
777 pub fn explosion_blast(center: Vec3, strength: f32, radius: f32) -> ForceFieldKind {
779 ForceFieldKind::Attractor(AttractorRepulsor::repulsor(center, strength, radius))
780 }
781
782 pub fn black_hole(center: Vec3, strength: f32, event_horizon: f32) -> ForceFieldKind {
784 ForceFieldKind::GravityWell(GravityWell {
785 position: center, strength, radius: strength * 5.0,
786 falloff: FalloffMode::InverseSquare { min_dist: 0.01 },
787 repulsive: false,
788 absorb_radius: event_horizon,
789 })
790 }
791
792 pub fn heat_shimmer() -> ForceFieldKind {
794 ForceFieldKind::Turbulence(TurbulenceField {
795 strength: 0.8, frequency: 0.5, time_speed: 0.3,
796 octaves: 2, lacunarity: 2.0, persistence: 0.4,
797 radius: 0.0, position: Vec3::ZERO, time: 0.0,
798 })
799 }
800
801 pub fn outdoor_wind(direction: Vec3, base_speed: f32) -> ForceFieldKind {
803 ForceFieldKind::Wind(WindZone {
804 direction: direction.normalize_or_zero(), speed: base_speed,
805 gust_strength: base_speed * 0.4, gust_frequency: 0.3,
806 bounds_min: None, bounds_max: None,
807 time: 0.0, gust_phase: 0.0,
808 })
809 }
810
811 pub fn air_resistance() -> ForceFieldKind {
813 ForceFieldKind::Drag(DragField::quadratic(0.05))
814 }
815
816 pub fn tornado(center: Vec3, strength: f32, radius: f32) -> ForceFieldKind {
818 ForceFieldKind::Vortex(VortexField::tornado(center, strength, radius))
819 }
820
821 pub fn water_current(direction: Vec3, speed: f32, bounds_min: Vec3, bounds_max: Vec3) -> ForceFieldKind {
823 ForceFieldKind::Wind(WindZone {
824 direction: direction.normalize_or_zero(), speed,
825 gust_strength: 0.0, gust_frequency: 0.0,
826 bounds_min: Some(bounds_min), bounds_max: Some(bounds_max),
827 time: 0.0, gust_phase: 0.0,
828 })
829 }
830
831 pub fn smoke_buoyancy() -> ForceFieldKind {
833 ForceFieldKind::Buoyancy(BuoyancyField::smoke_in_air())
834 }
835}
836
837#[derive(Debug, Clone)]
841pub struct ForceDebugSample {
842 pub position: Vec3,
843 pub total_force: Vec3,
844 pub per_field: Vec<(ForceFieldId, Vec3)>,
845}
846
847impl ForceFieldWorld {
848 pub fn debug_sample(&self, pos: Vec3) -> ForceDebugSample {
850 let test = Particle {
851 id: 0, position: pos, velocity: Vec3::ZERO, acceleration: Vec3::ZERO,
852 color: Vec4::ONE, size: 0.1, rotation: 0.0, angular_vel: 0.0,
853 age: 0.0, lifetime: 1.0, mass: 1.0,
854 tag: ParticleTag::ALL,
855 emitter_id: 0, custom: [0.0; 4],
856 };
857
858 let mut per_field = Vec::new();
859 let mut total = Vec3::ZERO;
860
861 if self.gravity_enabled {
862 total += self.gravity;
863 }
864
865 for f in &self.composite.fields {
866 let acc = f.apply(&test);
867 per_field.push((f.id, acc));
868 total += acc;
869 }
870
871 ForceDebugSample { position: pos, total_force: total, per_field }
872 }
873}