1use crate::math::{Fix128, Vec3Fix};
7use crate::solver::RigidBody;
8
9#[cfg(not(feature = "std"))]
10use alloc::vec::Vec;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum ForceField {
15 Directional {
17 direction: Vec3Fix,
19 strength: Fix128,
21 },
22
23 Point {
25 center: Vec3Fix,
27 strength: Fix128,
29 repulsive: bool,
31 max_force: Fix128,
33 },
34
35 Drag {
37 coefficient: Fix128,
39 },
40
41 Buoyancy {
43 surface_y: Fix128,
45 density: Fix128,
47 drag: Fix128,
49 },
50
51 Vortex {
53 center: Vec3Fix,
55 axis: Vec3Fix,
57 strength: Fix128,
59 falloff_radius: Fix128,
61 },
62
63 Explosion {
68 center: Vec3Fix,
70 strength: Fix128,
72 radius: Fix128,
74 falloff_power: Fix128,
76 },
77
78 Magnetic {
86 position: Vec3Fix,
88 moment: Vec3Fix,
90 strength: Fix128,
92 },
93}
94
95#[derive(Clone, Debug)]
97pub struct ForceFieldInstance {
98 pub field: ForceField,
100 pub affected_bodies: Option<Vec<usize>>,
102 pub enabled: bool,
104}
105
106impl ForceFieldInstance {
107 #[inline]
109 #[must_use]
110 pub const fn new(field: ForceField) -> Self {
111 Self {
112 field,
113 affected_bodies: None,
114 enabled: true,
115 }
116 }
117
118 #[must_use]
120 pub fn with_affected_bodies(mut self, bodies: Vec<usize>) -> Self {
121 self.affected_bodies = Some(bodies);
122 self
123 }
124
125 #[inline]
127 fn affects(&self, body_index: usize) -> bool {
128 if !self.enabled {
129 return false;
130 }
131 self.affected_bodies
132 .as_ref()
133 .is_none_or(|list| list.contains(&body_index))
134 }
135}
136
137fn point_force_beyond_square_range(
145 delta: Vec3Fix,
146 strength: Fix128,
147 repulsive: bool,
148 max_force: Fix128,
149) -> Vec3Fix {
150 let Some(direction) = delta.try_normalize_scaled() else {
151 return Vec3Fix::ZERO;
152 };
153 let force_mag = match delta.checked_length_scaled() {
154 Some(r) => (strength / r) / r,
155 None => {
156 let two = Fix128::from_int(2);
157 let half = Vec3Fix::new(delta.x / two, delta.y / two, delta.z / two);
158 match half.checked_length_scaled() {
159 Some(h) => ((strength / h) / h) / Fix128::from_int(4),
160 None => Fix128::ZERO,
162 }
163 }
164 };
165 let clamped = if force_mag > max_force {
168 max_force
169 } else {
170 force_mag
171 };
172 if repulsive {
173 -direction * clamped
174 } else {
175 direction * clamped
176 }
177}
178
179#[must_use]
184pub fn compute_force(field: &ForceField, body: &RigidBody) -> Vec3Fix {
185 field_force_at(field, body.position, body.velocity)
186}
187
188#[must_use]
195pub fn field_force_at(field: &ForceField, position: Vec3Fix, velocity: Vec3Fix) -> Vec3Fix {
196 match field {
197 ForceField::Directional {
198 direction,
199 strength,
200 } => {
201 if direction.length_squared() == Fix128::ONE {
204 *direction * *strength
205 } else {
206 direction.normalize() * *strength
207 }
208 }
209
210 ForceField::Point {
211 center,
212 strength,
213 repulsive,
214 max_force,
215 } => {
216 let delta = *center - position;
217
218 if delta == Vec3Fix::ZERO {
219 return Vec3Fix::ZERO;
220 }
221
222 let Some(dist_sq) = delta.checked_length_squared() else {
226 return point_force_beyond_square_range(delta, *strength, *repulsive, *max_force);
227 };
228
229 let direction = if dist_sq < Fix128::ONE {
234 let mut scaled = delta;
242 let mut scaled_dist_sq = dist_sq;
243 for _ in 0..128 {
244 if scaled_dist_sq >= Fix128::ONE {
245 break;
246 }
247 scaled = scaled + scaled;
248 scaled_dist_sq = scaled.length_squared();
249 }
250 scaled / scaled_dist_sq.sqrt()
251 } else {
252 delta / dist_sq.sqrt()
253 };
254
255 let dist_sq_for_force = if max_force.is_zero() {
262 dist_sq
263 } else {
264 let floor = (*strength / *max_force).abs();
265 if dist_sq < floor {
266 floor
267 } else {
268 dist_sq
269 }
270 };
271
272 let force_mag = *strength / dist_sq_for_force;
274 let clamped = if force_mag > *max_force {
275 *max_force
276 } else {
277 force_mag
278 };
279
280 if *repulsive {
281 -direction * clamped
282 } else {
283 direction * clamped
284 }
285 }
286
287 ForceField::Drag { coefficient } => {
288 let speed_sq = velocity.length_squared();
289 if speed_sq.is_zero() {
290 return Vec3Fix::ZERO;
291 }
292 let speed = speed_sq.sqrt();
293 let drag_dir = velocity / speed;
294 -drag_dir * (*coefficient * speed)
295 }
296
297 ForceField::Buoyancy {
298 surface_y,
299 density,
300 drag,
301 } => {
302 let depth = *surface_y - position.y;
303 if depth <= Fix128::ZERO {
304 return Vec3Fix::ZERO;
305 }
306
307 let buoyancy = Vec3Fix::new(Fix128::ZERO, *density * depth, Fix128::ZERO);
309
310 let water_drag = if velocity.length_squared().is_zero() {
312 Vec3Fix::ZERO
313 } else {
314 velocity * (-*drag)
315 };
316
317 buoyancy + water_drag
318 }
319
320 ForceField::Vortex {
321 center,
322 axis,
323 strength,
324 falloff_radius,
325 } => {
326 let delta = position - *center;
327
328 let axis_norm = axis.normalize();
330 let along_axis = axis_norm * delta.dot(axis_norm);
331 let radial = delta - along_axis;
332 let dist = radial.length();
333
334 if dist.is_zero() || falloff_radius.is_zero() {
335 return Vec3Fix::ZERO;
336 }
337
338 let tangent = axis_norm.cross(radial.normalize());
340
341 let falloff = if dist < *falloff_radius {
343 Fix128::ONE
344 } else {
345 *falloff_radius / dist
346 };
347
348 tangent * (*strength * falloff)
349 }
350
351 ForceField::Explosion {
352 center,
353 strength,
354 radius,
355 falloff_power,
356 } => {
357 let delta = position - *center;
358 let dist_sq = delta.length_squared();
359
360 if dist_sq.is_zero() {
361 return Vec3Fix::ZERO;
362 }
363
364 let dist = dist_sq.sqrt();
365
366 if dist >= *radius {
368 return Vec3Fix::ZERO;
369 }
370
371 let direction = delta / dist;
372
373 let ratio = Fix128::ONE - dist / *radius;
375
376 let power = if falloff_power.is_negative() {
383 Fix128::ZERO
384 } else {
385 *falloff_power
386 };
387 let falloff = ratio.powf_pos(power);
388
389 direction * (*strength * falloff)
390 }
391
392 ForceField::Magnetic {
393 position: dipole,
394 moment,
395 strength,
396 } => {
397 let delta = position - *dipole;
398 let dist_sq = delta.length_squared();
399
400 if delta == Vec3Fix::ZERO {
401 return Vec3Fix::ZERO;
402 }
403
404 let dist = dist_sq.sqrt();
405
406 let r_cubed = dist_sq * dist;
424 let saturation_cap = Fix128::from_raw(i64::MAX, u64::MAX) / Fix128::from_int(1024);
425 let r_cubed_floor = strength.abs() / saturation_cap;
426 let r_cubed = if r_cubed < r_cubed_floor {
427 r_cubed_floor
428 } else {
429 r_cubed
430 };
431
432 let force_mag = *strength / r_cubed;
433
434 let moment_dir = moment.normalize();
436
437 let (delta_p, dist_sq_p) = if dist_sq < Fix128::ONE {
451 let mut scaled = delta;
452 let mut scaled_dist_sq = dist_sq;
453 for _ in 0..128 {
454 if scaled_dist_sq >= Fix128::ONE {
455 break;
456 }
457 scaled = scaled + scaled;
458 scaled_dist_sq = scaled.length_squared();
459 }
460 (scaled, scaled_dist_sq)
461 } else {
462 (delta, dist_sq)
463 };
464 let cos_theta = delta_p.dot(moment_dir) / dist_sq_p.sqrt();
465
466 let signed_mag = -(force_mag * cos_theta);
478
479 moment_dir * signed_mag
480 }
481 }
482}
483
484pub fn apply_force_fields(fields: &[ForceFieldInstance], bodies: &mut [RigidBody], dt: Fix128) {
486 for (body_idx, body) in bodies.iter_mut().enumerate() {
487 if body.is_static() {
488 continue;
489 }
490
491 body.velocity = force_field_velocity(fields, body_idx, body, dt);
492 }
493}
494
495#[must_use]
498pub(crate) fn force_field_velocity(
499 fields: &[ForceFieldInstance],
500 body_idx: usize,
501 body: &RigidBody,
502 dt: Fix128,
503) -> Vec3Fix {
504 let mut total_force = Vec3Fix::ZERO;
505
506 for field_inst in fields {
507 if !field_inst.affects(body_idx) {
508 continue;
509 }
510 total_force = total_force + compute_force(&field_inst.field, body);
511 }
512
513 let acceleration = total_force * body.inv_mass;
515 body.velocity + acceleration * dt
516}
517
518#[cfg(all(test, feature = "std"))]
519mod tests {
520 use super::*;
521
522 #[test]
523 fn test_directional_force() {
524 let field = ForceField::Directional {
525 direction: Vec3Fix::UNIT_Y,
526 strength: Fix128::from_int(10),
527 };
528 let body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
529 let force = compute_force(&field, &body);
530 assert_eq!(force.y.hi, 10);
531 }
532
533 #[test]
534 fn test_drag_force() {
535 let field = ForceField::Drag {
536 coefficient: Fix128::from_int(1),
537 };
538 let mut body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
539 body.velocity = Vec3Fix::from_int(10, 0, 0);
540
541 let force = compute_force(&field, &body);
542 assert!(force.x < Fix128::ZERO, "Drag should oppose motion");
544 }
545
546 #[test]
547 fn test_point_gravity() {
548 let field = ForceField::Point {
549 center: Vec3Fix::ZERO,
550 strength: Fix128::from_int(100),
551 repulsive: false,
552 max_force: Fix128::from_int(1000),
553 };
554 let body = RigidBody::new(Vec3Fix::from_int(10, 0, 0), Fix128::ONE);
555 let force = compute_force(&field, &body);
556
557 assert!(
559 force.x < Fix128::ZERO,
560 "Point gravity should pull toward center"
561 );
562 }
563
564 #[test]
565 fn test_buoyancy() {
566 let field = ForceField::Buoyancy {
567 surface_y: Fix128::from_int(5),
568 density: Fix128::from_int(10),
569 drag: Fix128::ONE,
570 };
571
572 let body = RigidBody::new(Vec3Fix::from_int(0, 2, 0), Fix128::ONE);
574 let force = compute_force(&field, &body);
575 assert!(force.y > Fix128::ZERO, "Buoyancy should push up");
576
577 let body_above = RigidBody::new(Vec3Fix::from_int(0, 10, 0), Fix128::ONE);
579 let force_above = compute_force(&field, &body_above);
580 assert!(
581 force_above.y.is_zero() && force_above.x.is_zero(),
582 "No buoyancy above surface"
583 );
584 }
585
586 #[test]
587 fn test_apply_force_fields() {
588 let fields = vec![ForceFieldInstance::new(ForceField::Directional {
589 direction: Vec3Fix::UNIT_X,
590 strength: Fix128::from_int(10),
591 })];
592
593 let mut bodies = vec![RigidBody::new(Vec3Fix::ZERO, Fix128::ONE)];
594
595 let dt = Fix128::from_ratio(1, 60);
596 apply_force_fields(&fields, &mut bodies, dt);
597
598 assert!(
600 bodies[0].velocity.x > Fix128::ZERO,
601 "Force should accelerate body"
602 );
603 }
604
605 #[test]
606 fn test_affected_bodies_filter() {
607 let field = ForceFieldInstance::new(ForceField::Directional {
608 direction: Vec3Fix::UNIT_X,
609 strength: Fix128::from_int(100),
610 })
611 .with_affected_bodies(vec![0]); assert!(field.affects(0));
614 assert!(!field.affects(1));
615 }
616
617 #[test]
618 fn test_vortex() {
619 let field = ForceField::Vortex {
620 center: Vec3Fix::ZERO,
621 axis: Vec3Fix::UNIT_Y,
622 strength: Fix128::from_int(10),
623 falloff_radius: Fix128::from_int(100),
624 };
625
626 let body = RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE);
627 let force = compute_force(&field, &body);
628
629 assert!(
631 force.z.abs() > Fix128::ZERO,
632 "Vortex should produce tangential force"
633 );
634 }
635
636 #[test]
639 fn test_explosion_pushes_outward() {
640 let field = ForceField::Explosion {
641 center: Vec3Fix::ZERO,
642 strength: Fix128::from_int(100),
643 radius: Fix128::from_int(20),
644 falloff_power: Fix128::ONE,
645 };
646 let body = RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE);
647 let force = compute_force(&field, &body);
648
649 assert!(
651 force.x > Fix128::ZERO,
652 "Explosion should push body away from center"
653 );
654 }
655
656 #[test]
657 fn test_explosion_zero_beyond_radius() {
658 let field = ForceField::Explosion {
659 center: Vec3Fix::ZERO,
660 strength: Fix128::from_int(100),
661 radius: Fix128::from_int(10),
662 falloff_power: Fix128::ONE,
663 };
664 let body = RigidBody::new(Vec3Fix::from_int(15, 0, 0), Fix128::ONE);
666 let force = compute_force(&field, &body);
667
668 assert!(
669 force.x.is_zero() && force.y.is_zero() && force.z.is_zero(),
670 "No explosion force beyond radius"
671 );
672 }
673
674 #[test]
675 fn test_explosion_at_center() {
676 let field = ForceField::Explosion {
677 center: Vec3Fix::ZERO,
678 strength: Fix128::from_int(100),
679 radius: Fix128::from_int(10),
680 falloff_power: Fix128::ONE,
681 };
682 let body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
683 let force = compute_force(&field, &body);
684 assert!(
685 force.x.is_zero() && force.y.is_zero() && force.z.is_zero(),
686 "No force at explosion center (zero distance)"
687 );
688 }
689
690 #[test]
691 fn test_explosion_falloff() {
692 let field_linear = ForceField::Explosion {
693 center: Vec3Fix::ZERO,
694 strength: Fix128::from_int(100),
695 radius: Fix128::from_int(20),
696 falloff_power: Fix128::ONE,
697 };
698 let field_quadratic = ForceField::Explosion {
699 center: Vec3Fix::ZERO,
700 strength: Fix128::from_int(100),
701 radius: Fix128::from_int(20),
702 falloff_power: Fix128::from_int(2),
703 };
704
705 let body = RigidBody::new(Vec3Fix::from_int(10, 0, 0), Fix128::ONE);
706 let force_linear = compute_force(&field_linear, &body);
707 let force_quadratic = compute_force(&field_quadratic, &body);
708
709 assert!(
713 force_quadratic.x < force_linear.x,
714 "Quadratic falloff should produce weaker force than linear at same distance"
715 );
716 }
717
718 #[test]
719 fn test_explosion_apply_to_body() {
720 let fields = vec![ForceFieldInstance::new(ForceField::Explosion {
721 center: Vec3Fix::ZERO,
722 strength: Fix128::from_int(1000),
723 radius: Fix128::from_int(50),
724 falloff_power: Fix128::ONE,
725 })];
726
727 let mut bodies = vec![RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE)];
728 let dt = Fix128::from_ratio(1, 60);
729 apply_force_fields(&fields, &mut bodies, dt);
730
731 assert!(
732 bodies[0].velocity.x > Fix128::ZERO,
733 "Explosion should accelerate body away"
734 );
735 }
736
737 #[test]
740 fn test_magnetic_along_dipole_axis() {
741 let field = ForceField::Magnetic {
742 position: Vec3Fix::ZERO,
743 moment: Vec3Fix::UNIT_X,
744 strength: Fix128::from_int(1000),
745 };
746 let body = RigidBody::new(Vec3Fix::from_int(2, 0, 0), Fix128::ONE);
748 let force = compute_force(&field, &body);
749
750 assert!(
752 force.x.abs() > Fix128::ZERO,
753 "Magnetic force should exist along dipole axis"
754 );
755 }
756
757 #[test]
758 fn test_magnetic_perpendicular_to_dipole() {
759 let field = ForceField::Magnetic {
760 position: Vec3Fix::ZERO,
761 moment: Vec3Fix::UNIT_X,
762 strength: Fix128::from_int(1000),
763 };
764 let body = RigidBody::new(Vec3Fix::from_int(0, 5, 0), Fix128::ONE);
766 let force = compute_force(&field, &body);
767
768 assert!(
770 force.x.is_zero() && force.y.is_zero() && force.z.is_zero(),
771 "No magnetic force perpendicular to dipole axis"
772 );
773 }
774
775 #[test]
776 fn test_magnetic_at_dipole_position() {
777 let field = ForceField::Magnetic {
778 position: Vec3Fix::ZERO,
779 moment: Vec3Fix::UNIT_Z,
780 strength: Fix128::from_int(100),
781 };
782 let body = RigidBody::new(Vec3Fix::ZERO, Fix128::ONE);
783 let force = compute_force(&field, &body);
784 assert!(
785 force.x.is_zero() && force.y.is_zero() && force.z.is_zero(),
786 "No force at the dipole location"
787 );
788 }
789
790 #[test]
791 fn test_magnetic_inverse_cube_falloff() {
792 let field = ForceField::Magnetic {
793 position: Vec3Fix::ZERO,
794 moment: Vec3Fix::UNIT_X,
795 strength: Fix128::from_int(10000),
796 };
797
798 let body_near = RigidBody::new(Vec3Fix::from_int(2, 0, 0), Fix128::ONE);
799 let body_far = RigidBody::new(Vec3Fix::from_int(4, 0, 0), Fix128::ONE);
800
801 let force_near = compute_force(&field, &body_near);
802 let force_far = compute_force(&field, &body_far);
803
804 assert!(
807 force_near.x.abs() > force_far.x.abs(),
808 "Magnetic force should be stronger at closer distance (1/r^3 falloff)"
809 );
810 }
811
812 #[test]
813 fn test_magnetic_apply_to_body() {
814 let fields = vec![ForceFieldInstance::new(ForceField::Magnetic {
815 position: Vec3Fix::ZERO,
816 moment: Vec3Fix::UNIT_X,
817 strength: Fix128::from_int(10000),
818 })];
819
820 let mut bodies = vec![RigidBody::new(Vec3Fix::from_int(3, 0, 0), Fix128::ONE)];
821 let dt = Fix128::from_ratio(1, 60);
822 apply_force_fields(&fields, &mut bodies, dt);
823
824 assert!(
825 bodies[0].velocity.x.abs() > Fix128::ZERO,
826 "Magnetic field should accelerate body"
827 );
828 }
829
830 fn pow2(e: i32) -> Fix128 {
831 if e >= 0 {
832 Fix128::from_int(1_i64 << e)
833 } else {
834 Fix128::ONE / Fix128::from_int(1_i64 << -e)
835 }
836 }
837
838 fn body_at(x: Fix128) -> RigidBody {
839 RigidBody::new(Vec3Fix::new(x, Fix128::ZERO, Fix128::ZERO), Fix128::ONE)
840 }
841
842 fn point(strength: Fix128, repulsive: bool, max_force: Fix128) -> ForceField {
843 ForceField::Point {
844 center: Vec3Fix::ZERO,
845 strength,
846 repulsive,
847 max_force,
848 }
849 }
850
851 #[test]
855 fn point_force_beyond_the_square_range() {
856 let b = body_at(pow2(32));
857 let s = pow2(62);
858 let quarter = Fix128::from_ratio(1, 4);
859 assert_eq!(
860 compute_force(&point(s, false, pow2(10)), &b),
861 Vec3Fix::new(-quarter, Fix128::ZERO, Fix128::ZERO)
862 );
863 assert_eq!(
864 compute_force(&point(s, true, pow2(10)), &b),
865 Vec3Fix::new(quarter, Fix128::ZERO, Fix128::ZERO)
866 );
867 assert_eq!(
868 compute_force(&point(s, false, Fix128::from_ratio(1, 8)), &b),
869 Vec3Fix::new(-Fix128::from_ratio(1, 8), Fix128::ZERO, Fix128::ZERO)
870 );
871 }
872
873 #[test]
877 fn point_force_when_the_distance_does_not_fit() {
878 let c = Fix128::from_int(3_i64 << 61);
879 let field = ForceField::Point {
880 center: Vec3Fix::new(c, c, c),
881 strength: pow2(62),
882 repulsive: false,
883 max_force: pow2(10),
884 };
885 let f = compute_force(&field, &body_at(Fix128::ZERO));
886 for v in [f.x, f.y, f.z] {
887 assert!(v >= Fix128::ZERO && v < pow2(-60), "{f:?}");
888 }
889 }
890
891 #[test]
897 fn point_force_near_the_centre() {
898 let b = body_at(pow2(-20));
899 let on_x = |v: Fix128| Vec3Fix::new(v, Fix128::ZERO, Fix128::ZERO);
900 assert_eq!(
901 compute_force(&point(Fix128::ONE, false, pow2(50)), &b),
902 on_x(-pow2(40))
903 );
904 assert_eq!(
905 compute_force(&point(Fix128::ONE, false, pow2(10)), &b),
906 on_x(-pow2(10))
907 );
908 assert_eq!(
909 compute_force(&point(Fix128::ONE, false, Fix128::ZERO), &b),
910 Vec3Fix::ZERO
911 );
912 assert_eq!(
913 compute_force(&point(Fix128::ONE, false, pow2(10)), &body_at(Fix128::ZERO)),
914 Vec3Fix::ZERO
915 );
916 }
917
918 #[test]
923 fn buoyancy_drag_vortex_axis_and_magnet_centre() {
924 let mut b = RigidBody::new(Vec3Fix::from_int(0, -2, 0), Fix128::ONE);
925 b.velocity = Vec3Fix::from_int(1, 0, 2);
926 let water = ForceField::Buoyancy {
927 surface_y: Fix128::ZERO,
928 density: Fix128::from_int(3),
929 drag: Fix128::from_ratio(1, 2),
930 };
931 assert_eq!(
932 compute_force(&water, &b),
933 Vec3Fix::new(-Fix128::from_ratio(1, 2), Fix128::from_int(6), -Fix128::ONE)
934 );
935 let vortex = ForceField::Vortex {
936 center: Vec3Fix::ZERO,
937 axis: Vec3Fix::UNIT_Y,
938 strength: Fix128::ONE,
939 falloff_radius: Fix128::ONE,
940 };
941 assert_eq!(compute_force(&vortex, &b), Vec3Fix::ZERO);
942 let magnet = ForceField::Magnetic {
943 position: b.position,
944 moment: Vec3Fix::UNIT_X,
945 strength: Fix128::ONE,
946 };
947 assert_eq!(compute_force(&magnet, &b), Vec3Fix::ZERO);
948 }
949
950 #[test]
954 fn magnetic_force_near_the_dipole() {
955 let magnet = ForceField::Magnetic {
956 position: Vec3Fix::ZERO,
957 moment: Vec3Fix::UNIT_X,
958 strength: Fix128::ONE,
959 };
960 let f = compute_force(&magnet, &body_at(pow2(-10)));
961 assert!(
962 (f.x.to_f64() / -1_073_741_824.0 - 1.0).abs() < 1e-12,
963 "{f:?}"
964 );
965 assert!(f.y.is_zero() && f.z.is_zero());
966 }
967}