1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct BodyId(pub u32);
15
16#[derive(Debug, Clone, Copy)]
18pub enum CollisionShape {
19 Sphere { radius: f32 },
20 Plane { normal: [f32; 3], d: f32 },
21 Aabb { half_extents: [f32; 3] },
22 Capsule { radius: f32, half_height: f32 }, Cylinder { radius: f32, half_height: f32 }, Cone { radius: f32, height: f32 }, }
26
27#[derive(Debug, Clone)]
29pub struct RigidBody {
30 pub position: [f32; 3],
31 pub velocity: [f32; 3],
32 pub rotation: [f32; 4], pub angular_velocity: [f32; 3],
34 pub mass: f32,
35 pub inv_mass: f32,
36 pub restitution: f32,
37 pub friction: f32,
38 pub shape: CollisionShape,
39 pub is_static: bool,
40 pub is_active: bool,
41 pub sleep_frames: u16,
42 pub is_sleeping: bool,
43 pub cosmetic_only: bool,
49}
50
51impl RigidBody {
52 pub fn dynamic(mass: f32, shape: CollisionShape) -> Self {
54 Self {
55 position: [0.0; 3],
56 velocity: [0.0; 3],
57 rotation: [0.0, 0.0, 0.0, 1.0],
58 angular_velocity: [0.0; 3],
59 mass,
60 inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
61 restitution: 0.5,
62 friction: 0.3,
63 shape,
64 is_static: false,
65 is_active: true,
66 sleep_frames: 0,
67 is_sleeping: false,
68 cosmetic_only: false,
69 }
70 }
71
72 pub fn fixed(shape: CollisionShape) -> Self {
74 Self {
75 position: [0.0; 3],
76 velocity: [0.0; 3],
77 rotation: [0.0, 0.0, 0.0, 1.0],
78 angular_velocity: [0.0; 3],
79 mass: 0.0,
80 inv_mass: 0.0,
81 restitution: 0.5,
82 friction: 0.5,
83 shape,
84 is_static: true,
85 is_active: true,
86 sleep_frames: 0,
87 is_sleeping: false,
88 cosmetic_only: false,
89 }
90 }
91
92 pub fn as_cosmetic(mut self) -> Self {
95 self.cosmetic_only = true;
96 self
97 }
98
99 pub fn with_position(mut self, pos: [f32; 3]) -> Self {
100 self.position = pos;
101 self
102 }
103
104 pub fn with_restitution(mut self, e: f32) -> Self {
105 self.restitution = e;
106 self
107 }
108}
109
110#[derive(Debug, Clone, Copy)]
112pub struct Contact {
113 pub body_a: usize,
114 pub body_b: usize,
115 pub normal: [f32; 3], pub depth: f32, pub point: [f32; 3], pub cached_impulse: f32,
120}
121
122#[derive(Debug, Clone, Copy)]
124pub struct RayHit {
125 pub position: [f32; 3],
126 pub normal: [f32; 3],
127 pub distance: f32,
128 pub body_index: usize,
129}
130
131#[derive(Debug, Clone, Copy)]
133pub struct SleepConfig {
134 pub linear_threshold: f32,
135 pub angular_threshold: f32,
136 pub frames_to_sleep: u16,
137}
138
139impl Default for SleepConfig {
140 fn default() -> Self {
141 Self {
142 linear_threshold: 0.01,
143 angular_threshold: 0.01,
144 frames_to_sleep: 60,
145 }
146 }
147}
148
149#[derive(Clone, Copy)]
159struct SapEntry {
160 min: [f32; 3],
161 max: [f32; 3],
162 index: usize,
163}
164
165#[derive(Clone)]
169pub struct SweepAndPrune {
170 entries: Vec<SapEntry>,
171 pairs: Vec<(usize, usize)>,
173}
174
175impl SweepAndPrune {
176 pub fn new() -> Self {
177 Self {
178 entries: Vec::with_capacity(1024),
179 pairs: Vec::with_capacity(4096),
180 }
181 }
182
183 #[inline]
185 fn body_aabb(pos: &[f32; 3], shape: &CollisionShape) -> ([f32; 3], [f32; 3]) {
186 match shape {
187 CollisionShape::Sphere { radius } => {
188 let r = *radius;
189 (
190 [pos[0] - r, pos[1] - r, pos[2] - r],
191 [pos[0] + r, pos[1] + r, pos[2] + r],
192 )
193 }
194 CollisionShape::Aabb { half_extents } => (
195 [
196 pos[0] - half_extents[0],
197 pos[1] - half_extents[1],
198 pos[2] - half_extents[2],
199 ],
200 [
201 pos[0] + half_extents[0],
202 pos[1] + half_extents[1],
203 pos[2] + half_extents[2],
204 ],
205 ),
206 CollisionShape::Capsule { radius, half_height } => {
207 let r = *radius;
208 let h = *half_height;
209 (
210 [pos[0] - r, pos[1] - h - r, pos[2] - r],
211 [pos[0] + r, pos[1] + h + r, pos[2] + r],
212 )
213 }
214 CollisionShape::Cylinder { radius, half_height } => {
215 let r = *radius;
216 let h = *half_height;
217 (
218 [pos[0] - r, pos[1] - h, pos[2] - r],
219 [pos[0] + r, pos[1] + h, pos[2] + r],
220 )
221 }
222 CollisionShape::Cone { radius, height } => {
223 let r = *radius;
224 (
225 [pos[0] - r, pos[1], pos[2] - r],
226 [pos[0] + r, pos[1] + height, pos[2] + r],
227 )
228 }
229 CollisionShape::Plane { .. } => {
230 ([-1e6, -1e6, -1e6], [1e6, 1e6, 1e6])
232 }
233 }
234 }
235
236 pub fn populate(&mut self, bodies: &[RigidBody]) {
238 self.entries.clear();
239 self.entries.reserve(bodies.len());
240 for (i, body) in bodies.iter().enumerate() {
241 if !body.is_active {
242 continue;
243 }
244 let (min, max) = Self::body_aabb(&body.position, &body.shape);
245 self.entries.push(SapEntry { min, max, index: i });
246 }
247 #[cfg(feature = "parallel-physics")]
249 {
250 use rayon::slice::ParallelSliceMut;
251 self.entries
252 .par_sort_unstable_by(|a, b| a.min[0].partial_cmp(&b.min[0]).unwrap_or(std::cmp::Ordering::Equal));
253 }
254 #[cfg(not(feature = "parallel-physics"))]
255 self.entries
256 .sort_unstable_by(|a, b| a.min[0].partial_cmp(&b.min[0]).unwrap_or(std::cmp::Ordering::Equal));
257 }
258
259 pub fn query_pairs(&mut self) -> &[(usize, usize)] {
262 self.pairs.clear();
263 let n = self.entries.len();
264 for i in 0..n {
265 let a = &self.entries[i];
266 for j in (i + 1)..n {
268 let b = &self.entries[j];
269 if b.min[0] > a.max[0] {
271 break;
272 }
273 if a.max[1] < b.min[1] || b.max[1] < a.min[1] {
275 continue;
276 }
277 if a.max[2] < b.min[2] || b.max[2] < a.min[2] {
279 continue;
280 }
281 let (lo, hi) = if a.index < b.index {
283 (a.index, b.index)
284 } else {
285 (b.index, a.index)
286 };
287 self.pairs.push((lo, hi));
288 }
289 }
290 &self.pairs
291 }
292}
293
294#[derive(Debug, Clone)]
297pub struct SpatialHashGrid {
298 cell_size: f32,
299 inv_cell_size: f32,
300 entries: Vec<(u64, usize)>, }
302
303impl SpatialHashGrid {
304 pub fn new(cell_size: f32) -> Self {
305 Self {
306 cell_size,
307 inv_cell_size: 1.0 / cell_size,
308 entries: Vec::new(),
309 }
310 }
311
312 pub fn cell_size(&self) -> f32 {
314 self.cell_size
315 }
316
317 pub fn set_cell_size(&mut self, size: f32) {
319 self.cell_size = size;
320 self.inv_cell_size = 1.0 / size;
321 }
322
323 pub fn inv_cell_size(&self) -> f32 {
325 self.inv_cell_size
326 }
327
328 pub fn entries(&self) -> &[(u64, usize)] {
330 &self.entries
331 }
332
333 fn hash_cell(ix: i32, iy: i32, iz: i32) -> u64 {
335 let mut h: u64 = 0xcbf29ce484222325;
336 for byte in ix
337 .to_le_bytes()
338 .iter()
339 .chain(iy.to_le_bytes().iter())
340 .chain(iz.to_le_bytes().iter())
341 {
342 h ^= *byte as u64;
343 h = h.wrapping_mul(0x100000001b3);
344 }
345 h
346 }
347
348 pub fn populate(&mut self, bodies: &[RigidBody]) {
350 self.entries.clear();
351 self.entries.reserve(bodies.len());
352 let inv = self.inv_cell_size;
353 for (i, body) in bodies.iter().enumerate() {
354 if !body.is_active {
355 continue;
356 }
357 let ix = (body.position[0] * inv).floor() as i32;
358 let iy = (body.position[1] * inv).floor() as i32;
359 let iz = (body.position[2] * inv).floor() as i32;
360 let hash = Self::hash_cell(ix, iy, iz);
361 self.entries.push((hash, i));
362 }
363 #[cfg(feature = "parallel-physics")]
365 {
366 use rayon::slice::ParallelSliceMut;
367 self.entries.par_sort_unstable_by_key(|e| e.0);
368 }
369 #[cfg(not(feature = "parallel-physics"))]
370 {
371 self.entries.sort_unstable_by_key(|e| e.0);
372 }
373 }
374
375 pub fn query_pairs(&self, bodies: &[RigidBody]) -> Vec<(usize, usize)> {
395 const FORWARD: [(i32, i32, i32); 14] = [
398 (0, 0, 0),
400 (0, 0, 1),
402 (0, 1, -1),
403 (0, 1, 0),
404 (0, 1, 1),
405 (1, -1, -1),
406 (1, -1, 0),
407 (1, -1, 1),
408 (1, 0, -1),
409 (1, 0, 0),
410 (1, 0, 1),
411 (1, 1, -1),
412 (1, 1, 0),
413 (1, 1, 1),
414 ];
415
416 #[cfg(feature = "parallel-physics")]
417 {
418 use rayon::prelude::*;
419 let entries = &self.entries;
420 let inv = self.inv_cell_size;
421
422 bodies
425 .par_iter()
426 .enumerate()
427 .filter(|(_, body)| body.is_active)
428 .flat_map_iter(|(i, body)| {
429 let cx = (body.position[0] * inv).floor() as i32;
430 let cy = (body.position[1] * inv).floor() as i32;
431 let cz = (body.position[2] * inv).floor() as i32;
432 let mut local = Vec::new();
433 for &(dx, dy, dz) in &FORWARD {
434 let hash = Self::hash_cell(cx + dx, cy + dy, cz + dz);
435 let start = entries.partition_point(|e| e.0 < hash);
436 for entry in &entries[start..] {
437 if entry.0 != hash {
438 break;
439 }
440 let j = entry.1;
441 if dx == 0 && dy == 0 && dz == 0 {
442 if j > i {
444 local.push((i, j));
445 }
446 } else {
447 if j != i {
449 local.push((i.min(j), i.max(j)));
450 }
451 }
452 }
453 }
454 local
455 })
456 .collect()
457 }
459
460 #[cfg(not(feature = "parallel-physics"))]
461 {
462 let mut pairs = Vec::new();
463 for (i, body) in bodies.iter().enumerate() {
464 if !body.is_active {
465 continue;
466 }
467 let cx = (body.position[0] * self.inv_cell_size).floor() as i32;
468 let cy = (body.position[1] * self.inv_cell_size).floor() as i32;
469 let cz = (body.position[2] * self.inv_cell_size).floor() as i32;
470 for &(dx, dy, dz) in &FORWARD {
471 let hash = Self::hash_cell(cx + dx, cy + dy, cz + dz);
472 let start = self.entries.partition_point(|e| e.0 < hash);
473 for entry in &self.entries[start..] {
474 if entry.0 != hash {
475 break;
476 }
477 let j = entry.1;
478 if dx == 0 && dy == 0 && dz == 0 {
479 if j > i {
480 pairs.push((i, j));
481 }
482 } else {
483 if j != i {
484 pairs.push((i.min(j), i.max(j)));
485 }
486 }
487 }
488 }
489 }
490 pairs
492 }
493 }
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
534#[repr(u8)]
535pub enum SuperpositionState {
536 Active = 0,
538 Decohering = 1,
541 Superposed = 2,
543 Dormant = 3,
545}
546
547#[derive(Debug, Clone, Copy)]
557pub struct SuperpositionObserver {
558 pub position: [f32; 3],
559 pub active_radius: f32,
561 pub active_radius_sq: f32,
562 pub decohere_radius: f32,
564 pub decohere_radius_sq: f32,
565 pub decohere_tick_rate: u32,
567 pub tick_index: u32,
569}
570
571impl SuperpositionObserver {
572 pub fn new(position: [f32; 3], active_radius: f32) -> Self {
574 let decohere_radius = active_radius * 1.5;
575 Self {
576 position,
577 active_radius,
578 active_radius_sq: active_radius * active_radius,
579 decohere_radius,
580 decohere_radius_sq: decohere_radius * decohere_radius,
581 decohere_tick_rate: 2,
582 tick_index: 0,
583 }
584 }
585
586 pub fn with_rings(position: [f32; 3], active_radius: f32, decohere_radius: f32, tick_rate: u32) -> Self {
588 Self {
589 position,
590 active_radius,
591 active_radius_sq: active_radius * active_radius,
592 decohere_radius,
593 decohere_radius_sq: decohere_radius * decohere_radius,
594 decohere_tick_rate: tick_rate.max(1),
595 tick_index: 0,
596 }
597 }
598
599 pub fn advance_tick(&mut self) {
601 self.tick_index = self.tick_index.wrapping_add(1);
602 }
603
604 #[inline(always)]
606 pub fn should_decohere_simulate(&self) -> bool {
607 self.tick_index % self.decohere_tick_rate == 0
608 }
609
610 #[inline(always)]
612 pub fn classify(&self, body_pos: &[f32; 3]) -> SuperpositionState {
613 let dx = body_pos[0] - self.position[0];
614 let dy = body_pos[1] - self.position[1];
615 let dz = body_pos[2] - self.position[2];
616 let dist_sq = dx * dx + dy * dy + dz * dz;
617 if dist_sq <= self.active_radius_sq {
618 SuperpositionState::Active
619 } else if dist_sq <= self.decohere_radius_sq {
620 SuperpositionState::Decohering
621 } else {
622 SuperpositionState::Superposed
623 }
624 }
625}
626
627pub struct DreamSpace {
647 cell_size: f32,
648 inv_cell_size: f32,
649 cells: std::collections::HashMap<(i32, i32, i32), Vec<usize>>,
651 body_cells: Vec<(i32, i32, i32)>,
653 dirty_cells: Vec<(i32, i32, i32)>,
655 initialized: bool,
657}
658
659impl DreamSpace {
660 pub fn new(cell_size: f32) -> Self {
661 Self {
662 cell_size,
663 inv_cell_size: 1.0 / cell_size,
664 cells: std::collections::HashMap::new(),
665 body_cells: Vec::new(),
666 dirty_cells: Vec::new(),
667 initialized: false,
668 }
669 }
670
671 #[inline(always)]
672 fn cell_of(&self, pos: &[f32; 3]) -> (i32, i32, i32) {
673 (
674 (pos[0] * self.inv_cell_size).floor() as i32,
675 (pos[1] * self.inv_cell_size).floor() as i32,
676 (pos[2] * self.inv_cell_size).floor() as i32,
677 )
678 }
679
680 pub fn rebuild(&mut self, bodies: &[RigidBody]) {
682 self.cells.clear();
683 self.body_cells.clear();
684 self.body_cells.reserve(bodies.len());
685 self.dirty_cells.clear();
686
687 for (i, body) in bodies.iter().enumerate() {
688 let cell = if body.is_active {
689 self.cell_of(&body.position)
690 } else {
691 (i32::MIN, i32::MIN, i32::MIN)
692 };
693 self.body_cells.push(cell);
694 if body.is_active {
695 self.cells.entry(cell).or_default().push(i);
696 }
697 }
698 self.initialized = true;
699 self.dirty_cells = self.cells.keys().copied().collect();
701 }
702
703 pub fn update(&mut self, bodies: &[RigidBody]) {
707 if self.body_cells.len() != bodies.len() || !self.initialized {
709 self.rebuild(bodies);
710 return;
711 }
712
713 self.dirty_cells.clear();
714
715 for (i, body) in bodies.iter().enumerate() {
716 let new_cell = if body.is_active && !body.is_sleeping {
717 self.cell_of(&body.position)
718 } else if body.is_active {
719 continue;
721 } else {
722 (i32::MIN, i32::MIN, i32::MIN)
723 };
724
725 let old_cell = self.body_cells[i];
726 if new_cell == old_cell {
727 continue; }
729
730 if old_cell != (i32::MIN, i32::MIN, i32::MIN) {
733 if let Some(list) = self.cells.get_mut(&old_cell) {
734 if let Some(pos) = list.iter().position(|&idx| idx == i) {
735 list.swap_remove(pos);
736 }
737 if list.is_empty() {
738 self.cells.remove(&old_cell);
739 }
740 }
741 self.dirty_cells.push(old_cell);
742 }
743
744 if new_cell != (i32::MIN, i32::MIN, i32::MIN) {
746 self.cells.entry(new_cell).or_default().push(i);
747 self.dirty_cells.push(new_cell);
748 }
749
750 self.body_cells[i] = new_cell;
751 }
752
753 self.dirty_cells.sort_unstable();
755 self.dirty_cells.dedup();
756 }
757
758 pub fn query_dirty_pairs(&self, bodies: &[RigidBody]) -> Vec<(usize, usize)> {
761 let mut query_cells: Vec<(i32, i32, i32)> = Vec::with_capacity(self.dirty_cells.len() * 27);
763 for &(cx, cy, cz) in &self.dirty_cells {
764 for dx in -1..=1 {
765 for dy in -1..=1 {
766 for dz in -1..=1 {
767 query_cells.push((cx + dx, cy + dy, cz + dz));
768 }
769 }
770 }
771 }
772 query_cells.sort_unstable();
773 query_cells.dedup();
774
775 let mut pairs = Vec::new();
777 for &cell in &query_cells {
778 let Some(list) = self.cells.get(&cell) else { continue };
779 for &i in list {
781 if !bodies[i].is_active {
782 continue;
783 }
784 let (cx, cy, cz) = self.cell_of(&bodies[i].position);
785 for dx in -1..=1 {
786 for dy in -1..=1 {
787 for dz in -1..=1 {
788 let neighbor = (cx + dx, cy + dy, cz + dz);
789 let Some(nlist) = self.cells.get(&neighbor) else {
790 continue;
791 };
792 for &j in nlist {
793 if j > i && bodies[j].is_active {
794 pairs.push((i, j));
795 }
796 }
797 }
798 }
799 }
800 }
801 }
802 pairs.sort_unstable();
803 pairs.dedup();
804 pairs
805 }
806
807 pub fn query_all_pairs(&self, bodies: &[RigidBody]) -> Vec<(usize, usize)> {
810 let mut pairs = Vec::new();
811 for (i, body) in bodies.iter().enumerate() {
812 if !body.is_active {
813 continue;
814 }
815 let (cx, cy, cz) = self.cell_of(&body.position);
816 for dx in -1..=1 {
817 for dy in -1..=1 {
818 for dz in -1..=1 {
819 let neighbor = (cx + dx, cy + dy, cz + dz);
820 let Some(list) = self.cells.get(&neighbor) else {
821 continue;
822 };
823 for &j in list {
824 if j > i && bodies[j].is_active {
825 pairs.push((i, j));
826 }
827 }
828 }
829 }
830 }
831 }
832 pairs.sort_unstable();
833 pairs.dedup();
834 pairs
835 }
836
837 pub fn dirty_cell_count(&self) -> usize {
838 self.dirty_cells.len()
839 }
840 pub fn total_cell_count(&self) -> usize {
841 self.cells.len()
842 }
843 pub fn is_initialized(&self) -> bool {
844 self.initialized
845 }
846
847 pub fn classify_cells(
852 &self,
853 observer: &SuperpositionObserver,
854 ) -> (Vec<(i32, i32, i32)>, Vec<(i32, i32, i32)>, Vec<(i32, i32, i32)>) {
855 let mut active = Vec::new();
856 let mut decohering = Vec::new();
857 let mut superposed = Vec::new();
858
859 for &cell_key in self.cells.keys() {
860 let cx = cell_key.0 as f32 * self.cell_size + self.cell_size * 0.5;
862 let cy = cell_key.1 as f32 * self.cell_size + self.cell_size * 0.5;
863 let cz = cell_key.2 as f32 * self.cell_size + self.cell_size * 0.5;
864 let cell_center = [cx, cy, cz];
865
866 match observer.classify(&cell_center) {
867 SuperpositionState::Active => active.push(cell_key),
868 SuperpositionState::Decohering => decohering.push(cell_key),
869 _ => superposed.push(cell_key),
870 }
871 }
872
873 (active, decohering, superposed)
874 }
875
876 pub fn bodies_in_cell(&self, cell: &(i32, i32, i32)) -> &[usize] {
878 self.cells.get(cell).map(|v| v.as_slice()).unwrap_or(&[])
879 }
880}
881
882#[repr(C)]
889#[derive(Clone, Copy, Default)]
890struct JacobiAccumulator {
891 velocity: [f32; 3],
893 position: [f32; 3],
895 contact_count: u32,
897}
898
899struct WaveCoherence {
920 baseline_positions: Vec<[f32; 3]>,
922 cached_pairs: Vec<(usize, usize)>,
924 threshold_sq: f32,
926 steps_since_rebuild: u32,
928 rebuild_interval: u32,
930 initialized: bool,
932}
933
934impl WaveCoherence {
935 fn new() -> Self {
936 Self {
937 baseline_positions: Vec::new(),
938 cached_pairs: Vec::with_capacity(4096),
939 threshold_sq: 0.36, steps_since_rebuild: 0,
941 rebuild_interval: 60,
942 initialized: false,
943 }
944 }
945
946 fn set_threshold_from_cell_size(&mut self, cell_size: f32) {
948 let half = cell_size * 0.5;
949 self.threshold_sq = half * half;
950 }
951
952 fn invalidate(&mut self) {
954 self.initialized = false;
955 }
956
957 fn ensure_capacity(&mut self, body_count: usize) {
959 self.baseline_positions.resize(body_count, [0.0; 3]);
960 }
961
962 #[inline]
964 fn is_displaced(&self, index: usize, current_pos: &[f32; 3]) -> bool {
965 let bp = &self.baseline_positions[index];
966 let dx = current_pos[0] - bp[0];
967 let dy = current_pos[1] - bp[1];
968 let dz = current_pos[2] - bp[2];
969 dx * dx + dy * dy + dz * dz > self.threshold_sq
970 }
971}
972
973pub struct PhysicsWorld {
975 bodies: Vec<RigidBody>,
976 body_ids: Vec<BodyId>,
977 contacts: Vec<Contact>,
978 prev_contacts: Vec<Contact>,
980 pub gravity: [f32; 3],
981 next_id: u32,
982 pub sleep_config: SleepConfig,
983 broadphase: SpatialHashGrid,
984 pub dreamspace: DreamSpace,
986 pub superposition: Vec<SuperpositionState>,
988 jacobi_accumulators: Vec<JacobiAccumulator>,
990 _sap: SweepAndPrune,
992 wave: WaveCoherence,
994}
995
996impl Default for PhysicsWorld {
997 fn default() -> Self {
998 Self {
999 bodies: Vec::new(),
1000 body_ids: Vec::new(),
1001 contacts: Vec::new(),
1002 prev_contacts: Vec::new(),
1003 gravity: [0.0, -9.81, 0.0],
1004 next_id: 0,
1005 sleep_config: SleepConfig::default(),
1006 broadphase: SpatialHashGrid::new(2.0),
1007 dreamspace: DreamSpace::new(2.0),
1008 superposition: Vec::new(),
1009 jacobi_accumulators: Vec::new(),
1010 _sap: SweepAndPrune::new(),
1011 wave: WaveCoherence::new(),
1012 }
1013 }
1014}
1015
1016impl PhysicsWorld {
1017 pub fn new() -> Self {
1018 Self::default()
1019 }
1020
1021 pub fn add_body(&mut self, body: RigidBody) -> BodyId {
1023 let id = BodyId(self.next_id);
1024 self.next_id += 1;
1025 self.bodies.push(body);
1026 self.body_ids.push(id);
1027 self.superposition.push(SuperpositionState::Active);
1028 self.wave.invalidate(); id
1030 }
1031
1032 pub fn remove_body(&mut self, id: BodyId) -> bool {
1034 if let Some(pos) = self.body_ids.iter().position(|bid| *bid == id) {
1035 self.bodies.swap_remove(pos);
1036 self.body_ids.swap_remove(pos);
1037 self.wave.invalidate(); true
1039 } else {
1040 false
1041 }
1042 }
1043
1044 pub fn body(&self, id: BodyId) -> Option<&RigidBody> {
1046 self.body_ids
1047 .iter()
1048 .position(|bid| *bid == id)
1049 .and_then(|i| self.bodies.get(i))
1050 }
1051
1052 pub fn body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody> {
1054 self.body_ids
1055 .iter()
1056 .position(|bid| *bid == id)
1057 .and_then(|i| self.bodies.get_mut(i))
1058 }
1059
1060 pub fn body_count(&self) -> usize {
1062 self.bodies.len()
1063 }
1064
1065 pub fn bodies(&self) -> &[RigidBody] {
1067 &self.bodies
1068 }
1069
1070 pub fn set_broadphase_cell_size(&mut self, size: f32) {
1073 self.broadphase.set_cell_size(size);
1074 self.wave.set_threshold_from_cell_size(size);
1075 }
1076
1077 pub fn bodies_mut_slice(&mut self) -> &mut [RigidBody] {
1079 &mut self.bodies
1080 }
1081
1082 pub fn body_ids(&self) -> &[BodyId] {
1084 &self.body_ids
1085 }
1086
1087 pub fn step(&mut self, dt: f32) {
1090 if dt <= 0.0 {
1091 return;
1092 }
1093
1094 let grav = self.gravity;
1097 #[cfg(feature = "parallel-physics")]
1098 {
1099 use rayon::prelude::*;
1100 self.bodies.par_iter_mut().for_each(|body| {
1101 if body.is_static || !body.is_active || body.is_sleeping {
1102 return;
1103 }
1104 body.velocity[0] += grav[0] * dt;
1105 body.velocity[1] += grav[1] * dt;
1106 body.velocity[2] += grav[2] * dt;
1107 });
1108 }
1109 #[cfg(not(feature = "parallel-physics"))]
1110 for body in &mut self.bodies {
1111 if body.is_static || !body.is_active || body.is_sleeping {
1112 continue;
1113 }
1114 body.velocity[0] += grav[0] * dt;
1115 body.velocity[1] += grav[1] * dt;
1116 body.velocity[2] += grav[2] * dt;
1117 }
1118
1119 std::mem::swap(&mut self.prev_contacts, &mut self.contacts);
1126 self.contacts.clear();
1127
1128 self.wave.ensure_capacity(self.bodies.len());
1129 self.wave.steps_since_rebuild += 1;
1130
1131 let mut displaced_count = 0u32;
1133 if self.wave.initialized {
1134 for (i, body) in self.bodies.iter().enumerate() {
1135 if body.is_active && i < self.wave.baseline_positions.len() && self.wave.is_displaced(i, &body.position)
1136 {
1137 displaced_count += 1;
1138 }
1139 }
1140 }
1141
1142 let need_full = !self.wave.initialized
1147 || self.wave.steps_since_rebuild >= self.wave.rebuild_interval
1148 || displaced_count > (self.bodies.len() as u32) / 4;
1149
1150 if need_full {
1151 self.broadphase.populate(&self.bodies);
1152 let fresh = self.broadphase.query_pairs(&self.bodies);
1153 self.wave.cached_pairs.clear();
1154 self.wave.cached_pairs.extend_from_slice(&fresh);
1155 for (i, body) in self.bodies.iter().enumerate() {
1156 if i < self.wave.baseline_positions.len() {
1157 self.wave.baseline_positions[i] = body.position;
1158 }
1159 }
1160 self.wave.initialized = true;
1161 self.wave.steps_since_rebuild = 0;
1162 }
1163 let pairs = &self.wave.cached_pairs;
1167
1168 #[cfg(feature = "parallel-physics")]
1170 {
1171 use rayon::prelude::*;
1172 let bodies = &self.bodies;
1173 let new_contacts: Vec<Contact> = pairs
1174 .par_iter()
1175 .filter_map(|&(i, j)| {
1176 if bodies[i].is_static && bodies[j].is_static {
1177 return None;
1178 }
1179 detect_contact(&bodies[i], &bodies[j], i, j)
1180 })
1181 .collect();
1182 self.contacts.extend(new_contacts);
1183 }
1184 #[cfg(not(feature = "parallel-physics"))]
1185 for &(i, j) in pairs {
1186 if self.bodies[i].is_static && self.bodies[j].is_static {
1187 continue;
1188 }
1189 if let Some(contact) = detect_contact(&self.bodies[i], &self.bodies[j], i, j) {
1190 self.contacts.push(contact);
1191 }
1192 }
1193
1194 for ci in 0..self.contacts.len() {
1196 let contact = self.contacts[ci];
1197 let a = contact.body_a;
1198 let b = contact.body_b;
1199 if self.bodies[a].is_sleeping {
1200 self.bodies[a].is_sleeping = false;
1201 self.bodies[a].sleep_frames = 0;
1202 }
1203 if self.bodies[b].is_sleeping {
1204 self.bodies[b].is_sleeping = false;
1205 self.bodies[b].sleep_frames = 0;
1206 }
1207 }
1208
1209 warm_start_contacts(&mut self.contacts, &self.prev_contacts, &mut self.bodies);
1211
1212 const JACOBI_ITERATIONS: usize = 1;
1238
1239 self.jacobi_accumulators
1241 .resize(self.bodies.len(), JacobiAccumulator::default());
1242
1243 for _iter in 0..JACOBI_ITERATIONS {
1244 for acc in &mut self.jacobi_accumulators {
1246 *acc = JacobiAccumulator::default();
1247 }
1248
1249 #[cfg(feature = "parallel-physics")]
1253 {
1254 use rayon::prelude::*;
1255 use std::sync::atomic::{AtomicU32, Ordering};
1256
1257 let n = self.bodies.len();
1260 let atom_buf: Vec<AtomicU32> = (0..n * 6).map(|_| AtomicU32::new(0f32.to_bits())).collect();
1262 let atom_count: Vec<AtomicU32> = (0..n).map(|_| AtomicU32::new(0)).collect();
1263
1264 let bodies = &self.bodies;
1265 self.contacts.par_iter_mut().for_each(|contact| {
1266 let (a, b) = (contact.body_a, contact.body_b);
1267 if a == b {
1268 return;
1269 }
1270 let ba = &bodies[a];
1271 let bb = &bodies[b];
1272
1273 let n_vec = contact.normal;
1274 let v_rel = [
1275 ba.velocity[0] - bb.velocity[0],
1276 ba.velocity[1] - bb.velocity[1],
1277 ba.velocity[2] - bb.velocity[2],
1278 ];
1279 let v_along_n = vec3_dot(v_rel, n_vec);
1280 if v_along_n > 0.0 {
1281 return;
1282 } let e = ba.restitution.min(bb.restitution);
1285 let inv_mass_sum = ba.inv_mass + bb.inv_mass;
1286 if inv_mass_sum < 1e-8 {
1287 return;
1288 }
1289
1290 let j = -(1.0 + e) * v_along_n / inv_mass_sum;
1291 contact.cached_impulse = j;
1292
1293 let va = [
1295 n_vec[0] * j * ba.inv_mass,
1296 n_vec[1] * j * ba.inv_mass,
1297 n_vec[2] * j * ba.inv_mass,
1298 ];
1299 let vb = [
1300 n_vec[0] * j * bb.inv_mass,
1301 n_vec[1] * j * bb.inv_mass,
1302 n_vec[2] * j * bb.inv_mass,
1303 ];
1304
1305 let correction = (contact.depth - 0.01f32).max(0.0) * 0.8 / inv_mass_sum;
1307 let pa = [
1308 n_vec[0] * correction * ba.inv_mass,
1309 n_vec[1] * correction * ba.inv_mass,
1310 n_vec[2] * correction * ba.inv_mass,
1311 ];
1312 let pb = [
1313 n_vec[0] * correction * bb.inv_mass,
1314 n_vec[1] * correction * bb.inv_mass,
1315 n_vec[2] * correction * bb.inv_mass,
1316 ];
1317
1318 let tangent = [
1320 v_rel[0] - n_vec[0] * v_along_n,
1321 v_rel[1] - n_vec[1] * v_along_n,
1322 v_rel[2] - n_vec[2] * v_along_n,
1323 ];
1324 let tlen = vec3_len(tangent);
1325 let (fta, ftb) = if tlen > 1e-8 {
1326 let t = vec3_scale(tangent, 1.0 / tlen);
1327 let vt = vec3_dot(v_rel, t);
1328 let mu = (ba.friction + bb.friction) * 0.5;
1329 let jt = (-vt / inv_mass_sum).clamp(-j.abs() * mu, j.abs() * mu);
1330 (
1331 [
1332 t[0] * jt * ba.inv_mass,
1333 t[1] * jt * ba.inv_mass,
1334 t[2] * jt * ba.inv_mass,
1335 ],
1336 [
1337 t[0] * jt * bb.inv_mass,
1338 t[1] * jt * bb.inv_mass,
1339 t[2] * jt * bb.inv_mass,
1340 ],
1341 )
1342 } else {
1343 ([0.0; 3], [0.0; 3])
1344 };
1345
1346 for k in 0..3 {
1348 atomic_f32_add(&atom_buf[a * 6 + k], va[k] + fta[k]);
1349 atomic_f32_add(&atom_buf[a * 6 + 3 + k], -pa[k]);
1350 }
1351 atom_count[a].fetch_add(1, Ordering::Relaxed);
1352
1353 for k in 0..3 {
1355 atomic_f32_add(&atom_buf[b * 6 + k], -vb[k] - ftb[k]);
1356 atomic_f32_add(&atom_buf[b * 6 + 3 + k], pb[k]);
1357 }
1358 atom_count[b].fetch_add(1, Ordering::Relaxed);
1359 });
1360
1361 self.bodies.par_iter_mut().enumerate().for_each(|(i, body)| {
1363 if body.is_static || body.is_sleeping {
1364 return;
1365 }
1366 let count = atom_count[i].load(Ordering::Relaxed);
1367 if count == 0 {
1368 return;
1369 }
1370 for k in 0..3 {
1371 body.velocity[k] += f32::from_bits(atom_buf[i * 6 + k].load(Ordering::Relaxed));
1372 body.position[k] += f32::from_bits(atom_buf[i * 6 + 3 + k].load(Ordering::Relaxed));
1373 }
1374 });
1375 }
1376
1377 #[cfg(not(feature = "parallel-physics"))]
1378 {
1379 for acc in &mut self.jacobi_accumulators {
1381 *acc = JacobiAccumulator::default();
1382 }
1383 for ci in 0..self.contacts.len() {
1384 let contact = &mut self.contacts[ci];
1385 let (a, b) = (contact.body_a, contact.body_b);
1386 if a == b {
1387 continue;
1388 }
1389 let ba = &self.bodies[a];
1390 let bb = &self.bodies[b];
1391
1392 let n_vec = contact.normal;
1393 let v_rel = [
1394 ba.velocity[0] - bb.velocity[0],
1395 ba.velocity[1] - bb.velocity[1],
1396 ba.velocity[2] - bb.velocity[2],
1397 ];
1398 let v_along_n = vec3_dot(v_rel, n_vec);
1399 if v_along_n > 0.0 {
1400 continue;
1401 }
1402
1403 let e = ba.restitution.min(bb.restitution);
1404 let inv_mass_sum = ba.inv_mass + bb.inv_mass;
1405 if inv_mass_sum < 1e-8 {
1406 continue;
1407 }
1408
1409 let j = -(1.0 + e) * v_along_n / inv_mass_sum;
1410 contact.cached_impulse = j;
1411
1412 let correction = (contact.depth - 0.01f32).max(0.0) * 0.8 / inv_mass_sum;
1413
1414 self.jacobi_accumulators[a].velocity[0] += n_vec[0] * j * ba.inv_mass;
1416 self.jacobi_accumulators[a].velocity[1] += n_vec[1] * j * ba.inv_mass;
1417 self.jacobi_accumulators[a].velocity[2] += n_vec[2] * j * ba.inv_mass;
1418 self.jacobi_accumulators[a].position[0] -= n_vec[0] * correction * ba.inv_mass;
1419 self.jacobi_accumulators[a].position[1] -= n_vec[1] * correction * ba.inv_mass;
1420 self.jacobi_accumulators[a].position[2] -= n_vec[2] * correction * ba.inv_mass;
1421 self.jacobi_accumulators[a].contact_count += 1;
1422
1423 self.jacobi_accumulators[b].velocity[0] -= n_vec[0] * j * bb.inv_mass;
1425 self.jacobi_accumulators[b].velocity[1] -= n_vec[1] * j * bb.inv_mass;
1426 self.jacobi_accumulators[b].velocity[2] -= n_vec[2] * j * bb.inv_mass;
1427 self.jacobi_accumulators[b].position[0] += n_vec[0] * correction * bb.inv_mass;
1428 self.jacobi_accumulators[b].position[1] += n_vec[1] * correction * bb.inv_mass;
1429 self.jacobi_accumulators[b].position[2] += n_vec[2] * correction * bb.inv_mass;
1430 self.jacobi_accumulators[b].contact_count += 1;
1431
1432 let tangent = [
1434 v_rel[0] - n_vec[0] * v_along_n,
1435 v_rel[1] - n_vec[1] * v_along_n,
1436 v_rel[2] - n_vec[2] * v_along_n,
1437 ];
1438 let tlen = vec3_len(tangent);
1439 if tlen > 1e-8 {
1440 let t = vec3_scale(tangent, 1.0 / tlen);
1441 let vt = vec3_dot(v_rel, t);
1442 let mu = (ba.friction + bb.friction) * 0.5;
1443 let jt = (-vt / inv_mass_sum).clamp(-j.abs() * mu, j.abs() * mu);
1444 self.jacobi_accumulators[a].velocity[0] += t[0] * jt * ba.inv_mass;
1445 self.jacobi_accumulators[a].velocity[1] += t[1] * jt * ba.inv_mass;
1446 self.jacobi_accumulators[a].velocity[2] += t[2] * jt * ba.inv_mass;
1447 self.jacobi_accumulators[b].velocity[0] -= t[0] * jt * bb.inv_mass;
1448 self.jacobi_accumulators[b].velocity[1] -= t[1] * jt * bb.inv_mass;
1449 self.jacobi_accumulators[b].velocity[2] -= t[2] * jt * bb.inv_mass;
1450 }
1451 }
1452
1453 for (i, body) in self.bodies.iter_mut().enumerate() {
1455 if body.is_static || body.is_sleeping {
1456 continue;
1457 }
1458 let acc = &self.jacobi_accumulators[i];
1459 if acc.contact_count == 0 {
1460 continue;
1461 }
1462 for k in 0..3 {
1463 body.velocity[k] += acc.velocity[k];
1464 body.position[k] += acc.position[k];
1465 }
1466 }
1467 }
1468 }
1469
1470 let sleep_cfg = self.sleep_config;
1473 #[cfg(feature = "parallel-physics")]
1474 {
1475 use rayon::prelude::*;
1476 self.bodies.par_iter_mut().for_each(|body| {
1477 if body.is_static || !body.is_active || body.is_sleeping {
1478 return;
1479 }
1480 body.position[0] += body.velocity[0] * dt;
1481 body.position[1] += body.velocity[1] * dt;
1482 body.position[2] += body.velocity[2] * dt;
1483 let lin_speed_sq = body.velocity[0] * body.velocity[0]
1484 + body.velocity[1] * body.velocity[1]
1485 + body.velocity[2] * body.velocity[2];
1486 let ang_speed_sq = body.angular_velocity[0] * body.angular_velocity[0]
1487 + body.angular_velocity[1] * body.angular_velocity[1]
1488 + body.angular_velocity[2] * body.angular_velocity[2];
1489 let lin_thresh_sq = sleep_cfg.linear_threshold * sleep_cfg.linear_threshold;
1490 let ang_thresh_sq = sleep_cfg.angular_threshold * sleep_cfg.angular_threshold;
1491 if lin_speed_sq < lin_thresh_sq && ang_speed_sq < ang_thresh_sq {
1492 body.sleep_frames = body.sleep_frames.saturating_add(1);
1493 if body.sleep_frames >= sleep_cfg.frames_to_sleep {
1494 body.is_sleeping = true;
1495 body.velocity = [0.0; 3];
1496 body.angular_velocity = [0.0; 3];
1497 }
1498 } else {
1499 body.sleep_frames = 0;
1500 }
1501 });
1502 }
1503 #[cfg(not(feature = "parallel-physics"))]
1504 for body in &mut self.bodies {
1505 if body.is_static || !body.is_active || body.is_sleeping {
1506 continue;
1507 }
1508 body.position[0] += body.velocity[0] * dt;
1509 body.position[1] += body.velocity[1] * dt;
1510 body.position[2] += body.velocity[2] * dt;
1511 let lin_speed_sq = body.velocity[0] * body.velocity[0]
1512 + body.velocity[1] * body.velocity[1]
1513 + body.velocity[2] * body.velocity[2];
1514 let ang_speed_sq = body.angular_velocity[0] * body.angular_velocity[0]
1515 + body.angular_velocity[1] * body.angular_velocity[1]
1516 + body.angular_velocity[2] * body.angular_velocity[2];
1517 let lin_thresh_sq = sleep_cfg.linear_threshold * sleep_cfg.linear_threshold;
1518 let ang_thresh_sq = sleep_cfg.angular_threshold * sleep_cfg.angular_threshold;
1519 if lin_speed_sq < lin_thresh_sq && ang_speed_sq < ang_thresh_sq {
1520 body.sleep_frames = body.sleep_frames.saturating_add(1);
1521 if body.sleep_frames >= sleep_cfg.frames_to_sleep {
1522 body.is_sleeping = true;
1523 body.velocity = [0.0; 3];
1524 body.angular_velocity = [0.0; 3];
1525 }
1526 } else {
1527 body.sleep_frames = 0;
1528 }
1529 }
1530 }
1531
1532 pub fn step_dreamspace(&mut self, dt: f32) {
1536 if dt <= 0.0 {
1537 return;
1538 }
1539
1540 for body in &mut self.bodies {
1542 if body.is_static || !body.is_active || body.is_sleeping {
1543 continue;
1544 }
1545 body.velocity[0] += self.gravity[0] * dt;
1546 body.velocity[1] += self.gravity[1] * dt;
1547 body.velocity[2] += self.gravity[2] * dt;
1548 }
1549
1550 std::mem::swap(&mut self.prev_contacts, &mut self.contacts);
1553 self.contacts.clear();
1554 if !self.dreamspace.is_initialized() {
1555 self.dreamspace.rebuild(&self.bodies);
1556 }
1557 let pairs = if self.dreamspace.dirty_cell_count() > self.dreamspace.total_cell_count() / 2 {
1558 self.dreamspace.query_all_pairs(&self.bodies)
1560 } else {
1561 self.dreamspace.query_dirty_pairs(&self.bodies)
1562 };
1563
1564 for (i, j) in pairs {
1566 if self.bodies[i].is_static && self.bodies[j].is_static {
1567 continue;
1568 }
1569 if let Some(contact) = detect_contact(&self.bodies[i], &self.bodies[j], i, j) {
1570 self.contacts.push(contact);
1571 }
1572 }
1573
1574 for ci in 0..self.contacts.len() {
1576 let c = self.contacts[ci];
1577 if self.bodies[c.body_a].is_sleeping {
1578 self.bodies[c.body_a].is_sleeping = false;
1579 self.bodies[c.body_a].sleep_frames = 0;
1580 }
1581 if self.bodies[c.body_b].is_sleeping {
1582 self.bodies[c.body_b].is_sleeping = false;
1583 self.bodies[c.body_b].sleep_frames = 0;
1584 }
1585 }
1586
1587 warm_start_contacts(&mut self.contacts, &self.prev_contacts, &mut self.bodies);
1589
1590 let islands = build_islands(self.bodies.len(), &self.contacts);
1592 for island_contacts in &islands {
1593 for &ci in island_contacts {
1594 let (a_idx, b_idx) = (self.contacts[ci].body_a, self.contacts[ci].body_b);
1595 if a_idx == b_idx {
1596 continue;
1597 }
1598 let (lo, hi) = if a_idx < b_idx { (a_idx, b_idx) } else { (b_idx, a_idx) };
1599 let (left, right) = self.bodies.split_at_mut(hi);
1600 if a_idx < b_idx {
1601 resolve_contact(&mut left[lo], &mut right[0], &mut self.contacts[ci]);
1602 } else {
1603 resolve_contact(&mut right[0], &mut left[lo], &mut self.contacts[ci]);
1604 }
1605 }
1606 }
1607
1608 let sleep_cfg = self.sleep_config;
1610 for body in &mut self.bodies {
1611 if body.is_static || !body.is_active || body.is_sleeping {
1612 continue;
1613 }
1614 body.position[0] += body.velocity[0] * dt;
1615 body.position[1] += body.velocity[1] * dt;
1616 body.position[2] += body.velocity[2] * dt;
1617
1618 let lin_sq = body.velocity[0] * body.velocity[0]
1619 + body.velocity[1] * body.velocity[1]
1620 + body.velocity[2] * body.velocity[2];
1621 let ang_sq = body.angular_velocity[0] * body.angular_velocity[0]
1622 + body.angular_velocity[1] * body.angular_velocity[1]
1623 + body.angular_velocity[2] * body.angular_velocity[2];
1624 if lin_sq < sleep_cfg.linear_threshold * sleep_cfg.linear_threshold
1625 && ang_sq < sleep_cfg.angular_threshold * sleep_cfg.angular_threshold
1626 {
1627 body.sleep_frames = body.sleep_frames.saturating_add(1);
1628 if body.sleep_frames >= sleep_cfg.frames_to_sleep {
1629 body.is_sleeping = true;
1630 body.velocity = [0.0; 3];
1631 body.angular_velocity = [0.0; 3];
1632 }
1633 } else {
1634 body.sleep_frames = 0;
1635 }
1636 }
1637
1638 self.dreamspace.update(&self.bodies);
1640 }
1641
1642 pub fn step_superposition(&mut self, dt: f32, observer: &SuperpositionObserver) {
1652 if dt <= 0.0 {
1653 return;
1654 }
1655
1656 if !self.dreamspace.is_initialized() {
1660 self.dreamspace.rebuild(&self.bodies);
1661 }
1662
1663 let (active_cells, decohere_cells, superposed_cells) = self.dreamspace.classify_cells(observer);
1664
1665 for &cell in &active_cells {
1667 for &idx in self.dreamspace.bodies_in_cell(&cell) {
1668 if idx < self.superposition.len() && self.bodies[idx].is_active {
1669 self.superposition[idx] = SuperpositionState::Active;
1670 }
1671 }
1672 }
1673 for &cell in &decohere_cells {
1674 for &idx in self.dreamspace.bodies_in_cell(&cell) {
1675 if idx < self.superposition.len() && self.bodies[idx].is_active {
1676 if self.superposition[idx] != SuperpositionState::Active {
1678 self.superposition[idx] = SuperpositionState::Decohering;
1679 }
1680 }
1681 }
1682 }
1683 for &cell in &superposed_cells {
1684 for &idx in self.dreamspace.bodies_in_cell(&cell) {
1685 if idx < self.superposition.len() {
1686 if !self.bodies[idx].is_active {
1687 self.superposition[idx] = SuperpositionState::Dormant;
1688 } else if self.bodies[idx].is_sleeping {
1689 self.superposition[idx] = SuperpositionState::Dormant;
1690 } else if self.superposition[idx] != SuperpositionState::Active {
1691 self.superposition[idx] = SuperpositionState::Superposed;
1692 }
1693 }
1694 }
1695 }
1696
1697 let simulate_decohere = observer.should_decohere_simulate();
1698
1699 for (i, body) in self.bodies.iter_mut().enumerate() {
1701 let state = self.superposition[i];
1702 let should_sim =
1703 state == SuperpositionState::Active || (state == SuperpositionState::Decohering && simulate_decohere);
1704 if !should_sim || body.is_static || body.is_sleeping {
1705 continue;
1706 }
1707 body.velocity[0] += self.gravity[0] * dt;
1708 body.velocity[1] += self.gravity[1] * dt;
1709 body.velocity[2] += self.gravity[2] * dt;
1710 }
1711
1712 std::mem::swap(&mut self.prev_contacts, &mut self.contacts);
1715 self.contacts.clear();
1716 let all_pairs = if self.dreamspace.dirty_cell_count() > self.dreamspace.total_cell_count() / 2 {
1717 self.dreamspace.query_all_pairs(&self.bodies)
1718 } else {
1719 self.dreamspace.query_dirty_pairs(&self.bodies)
1720 };
1721
1722 let pairs: Vec<(usize, usize)> = all_pairs
1723 .into_iter()
1724 .filter(|&(i, j)| {
1725 let si = self.superposition[i];
1726 let sj = self.superposition[j];
1727 si == SuperpositionState::Active
1728 || sj == SuperpositionState::Active
1729 || ((si == SuperpositionState::Decohering || sj == SuperpositionState::Decohering)
1730 && simulate_decohere)
1731 })
1732 .collect();
1733
1734 for (i, j) in pairs {
1738 if self.bodies[i].is_static && self.bodies[j].is_static {
1739 continue;
1740 }
1741 if self.bodies[i].cosmetic_only
1743 && self.bodies[j].cosmetic_only
1744 && self.superposition[i] != SuperpositionState::Active
1745 && self.superposition[j] != SuperpositionState::Active
1746 {
1747 continue;
1748 }
1749 if let Some(contact) = detect_contact(&self.bodies[i], &self.bodies[j], i, j) {
1750 self.contacts.push(contact);
1751 }
1752 }
1753
1754 for ci in 0..self.contacts.len() {
1756 let c = self.contacts[ci];
1757 if self.bodies[c.body_a].is_sleeping {
1758 self.bodies[c.body_a].is_sleeping = false;
1759 self.bodies[c.body_a].sleep_frames = 0;
1760 }
1761 if self.bodies[c.body_b].is_sleeping {
1762 self.bodies[c.body_b].is_sleeping = false;
1763 self.bodies[c.body_b].sleep_frames = 0;
1764 }
1765 self.superposition[c.body_a] = SuperpositionState::Active;
1766 self.superposition[c.body_b] = SuperpositionState::Active;
1767 }
1768
1769 warm_start_contacts(&mut self.contacts, &self.prev_contacts, &mut self.bodies);
1771
1772 let islands = build_islands(self.bodies.len(), &self.contacts);
1774 for island_contacts in &islands {
1775 for &ci in island_contacts {
1776 let (a_idx, b_idx) = (self.contacts[ci].body_a, self.contacts[ci].body_b);
1777 if a_idx == b_idx {
1778 continue;
1779 }
1780 let (lo, hi) = if a_idx < b_idx { (a_idx, b_idx) } else { (b_idx, a_idx) };
1781 let (left, right) = self.bodies.split_at_mut(hi);
1782 if a_idx < b_idx {
1783 resolve_contact(&mut left[lo], &mut right[0], &mut self.contacts[ci]);
1784 } else {
1785 resolve_contact(&mut right[0], &mut left[lo], &mut self.contacts[ci]);
1786 }
1787 }
1788 }
1789
1790 let sleep_cfg = self.sleep_config;
1792 for (i, body) in self.bodies.iter_mut().enumerate() {
1793 let state = self.superposition[i];
1794 let should_sim =
1795 state == SuperpositionState::Active || (state == SuperpositionState::Decohering && simulate_decohere);
1796 if !should_sim || body.is_static || body.is_sleeping {
1797 continue;
1798 }
1799
1800 body.position[0] += body.velocity[0] * dt;
1801 body.position[1] += body.velocity[1] * dt;
1802 body.position[2] += body.velocity[2] * dt;
1803
1804 let lin_sq = body.velocity[0] * body.velocity[0]
1805 + body.velocity[1] * body.velocity[1]
1806 + body.velocity[2] * body.velocity[2];
1807 let ang_sq = body.angular_velocity[0] * body.angular_velocity[0]
1808 + body.angular_velocity[1] * body.angular_velocity[1]
1809 + body.angular_velocity[2] * body.angular_velocity[2];
1810 if lin_sq < sleep_cfg.linear_threshold * sleep_cfg.linear_threshold
1811 && ang_sq < sleep_cfg.angular_threshold * sleep_cfg.angular_threshold
1812 {
1813 body.sleep_frames = body.sleep_frames.saturating_add(1);
1814 if body.sleep_frames >= sleep_cfg.frames_to_sleep {
1815 body.is_sleeping = true;
1816 body.velocity = [0.0; 3];
1817 body.angular_velocity = [0.0; 3];
1818 }
1819 } else {
1820 body.sleep_frames = 0;
1821 }
1822 }
1823
1824 self.dreamspace.update(&self.bodies);
1826 }
1827
1828 pub fn superposition_counts(&self) -> (u32, u32, u32, u32) {
1830 let (mut a, mut dec, mut s, mut d) = (0u32, 0u32, 0u32, 0u32);
1831 for &state in &self.superposition {
1832 match state {
1833 SuperpositionState::Active => a += 1,
1834 SuperpositionState::Decohering => dec += 1,
1835 SuperpositionState::Superposed => s += 1,
1836 SuperpositionState::Dormant => d += 1,
1837 }
1838 }
1839 (a, dec, s, d)
1840 }
1841
1842 pub fn raycast(&self, origin: [f32; 3], direction: [f32; 3], max_dist: f32) -> Option<RayHit> {
1844 let dir_len = vec3_len(direction);
1845 if dir_len < 1e-8 {
1846 return None;
1847 }
1848 let dir = vec3_scale(direction, 1.0 / dir_len);
1849
1850 let mut closest: Option<RayHit> = None;
1851
1852 for (i, body) in self.bodies.iter().enumerate() {
1853 if !body.is_active {
1854 continue;
1855 }
1856 let hit = match body.shape {
1857 CollisionShape::Sphere { radius } => ray_sphere(origin, dir, body.position, radius),
1858 CollisionShape::Plane { normal, d } => ray_plane(origin, dir, normal, d),
1859 CollisionShape::Aabb { half_extents } => ray_aabb(origin, dir, body.position, half_extents),
1860 CollisionShape::Capsule { radius, half_height } => {
1861 ray_capsule(origin, dir, body.position, radius, half_height)
1862 }
1863 CollisionShape::Cylinder { radius, half_height } => {
1864 ray_cylinder(origin, dir, body.position, radius, half_height)
1865 }
1866 CollisionShape::Cone { radius, height } => ray_cone(origin, dir, body.position, radius, height),
1867 };
1868
1869 if let Some((dist, normal, point)) = hit {
1870 if dist >= 0.0 && dist <= max_dist {
1871 if closest.as_ref().map_or(true, |c| dist < c.distance) {
1872 closest = Some(RayHit {
1873 position: point,
1874 normal,
1875 distance: dist,
1876 body_index: i,
1877 });
1878 }
1879 }
1880 }
1881 }
1882
1883 closest
1884 }
1885
1886 pub fn contacts(&self) -> &[Contact] {
1888 &self.contacts
1889 }
1890}
1891
1892fn detect_contact(a: &RigidBody, b: &RigidBody, idx_a: usize, idx_b: usize) -> Option<Contact> {
1895 match (&a.shape, &b.shape) {
1896 (CollisionShape::Sphere { radius: ra }, CollisionShape::Sphere { radius: rb }) => {
1897 sphere_sphere(a.position, *ra, b.position, *rb, idx_a, idx_b)
1898 }
1899 (CollisionShape::Sphere { radius }, CollisionShape::Plane { normal, d }) => {
1900 sphere_plane(a.position, *radius, *normal, *d, idx_a, idx_b, false)
1901 }
1902 (CollisionShape::Plane { normal, d }, CollisionShape::Sphere { radius }) => {
1903 sphere_plane(b.position, *radius, *normal, *d, idx_b, idx_a, true)
1904 }
1905 (CollisionShape::Aabb { half_extents: ha }, CollisionShape::Aabb { half_extents: hb }) => {
1906 aabb_aabb(a.position, *ha, b.position, *hb, idx_a, idx_b)
1907 }
1908 (CollisionShape::Sphere { radius }, CollisionShape::Aabb { half_extents }) => {
1909 sphere_aabb(a.position, *radius, b.position, *half_extents, idx_a, idx_b)
1910 }
1911 (CollisionShape::Aabb { half_extents }, CollisionShape::Sphere { radius }) => {
1912 sphere_aabb(b.position, *radius, a.position, *half_extents, idx_b, idx_a)
1913 }
1914 (CollisionShape::Capsule { radius, half_height }, CollisionShape::Sphere { radius: sr }) => {
1916 capsule_sphere_contact(a.position, *radius, *half_height, b.position, *sr, idx_a, idx_b)
1917 }
1918 (CollisionShape::Sphere { radius: sr }, CollisionShape::Capsule { radius, half_height }) => {
1919 capsule_sphere_contact(b.position, *radius, *half_height, a.position, *sr, idx_b, idx_a).map(|c| Contact {
1920 body_a: idx_a,
1921 body_b: idx_b,
1922 normal: [-c.normal[0], -c.normal[1], -c.normal[2]],
1923 ..c
1924 })
1925 }
1926 (CollisionShape::Capsule { radius, half_height }, CollisionShape::Plane { normal, d }) => {
1927 capsule_plane_contact(a.position, *radius, *half_height, *normal, *d, idx_a, idx_b)
1928 }
1929 (CollisionShape::Plane { normal, d }, CollisionShape::Capsule { radius, half_height }) => {
1930 capsule_plane_contact(b.position, *radius, *half_height, *normal, *d, idx_b, idx_a).map(|c| Contact {
1931 body_a: idx_a,
1932 body_b: idx_b,
1933 normal: [-c.normal[0], -c.normal[1], -c.normal[2]],
1934 ..c
1935 })
1936 }
1937 (CollisionShape::Capsule { radius, half_height }, CollisionShape::Aabb { half_extents }) => {
1938 capsule_aabb_contact(
1939 a.position,
1940 *radius,
1941 *half_height,
1942 b.position,
1943 *half_extents,
1944 idx_a,
1945 idx_b,
1946 )
1947 }
1948 (CollisionShape::Aabb { half_extents }, CollisionShape::Capsule { radius, half_height }) => {
1949 capsule_aabb_contact(
1950 b.position,
1951 *radius,
1952 *half_height,
1953 a.position,
1954 *half_extents,
1955 idx_b,
1956 idx_a,
1957 )
1958 .map(|c| Contact {
1959 body_a: idx_a,
1960 body_b: idx_b,
1961 normal: [-c.normal[0], -c.normal[1], -c.normal[2]],
1962 ..c
1963 })
1964 }
1965 (CollisionShape::Cylinder { radius, half_height }, CollisionShape::Sphere { radius: sr }) => {
1967 cylinder_sphere_contact(a.position, *radius, *half_height, b.position, *sr, idx_a, idx_b)
1968 }
1969 (CollisionShape::Sphere { radius: sr }, CollisionShape::Cylinder { radius, half_height }) => {
1970 cylinder_sphere_contact(b.position, *radius, *half_height, a.position, *sr, idx_b, idx_a).map(|c| Contact {
1971 body_a: idx_a,
1972 body_b: idx_b,
1973 normal: [-c.normal[0], -c.normal[1], -c.normal[2]],
1974 ..c
1975 })
1976 }
1977 (CollisionShape::Cylinder { radius, half_height }, CollisionShape::Plane { normal, d }) => {
1978 cylinder_plane_contact(a.position, *radius, *half_height, *normal, *d, idx_a, idx_b)
1979 }
1980 (CollisionShape::Plane { normal, d }, CollisionShape::Cylinder { radius, half_height }) => {
1981 cylinder_plane_contact(b.position, *radius, *half_height, *normal, *d, idx_b, idx_a).map(|c| Contact {
1982 body_a: idx_a,
1983 body_b: idx_b,
1984 normal: [-c.normal[0], -c.normal[1], -c.normal[2]],
1985 ..c
1986 })
1987 }
1988 (CollisionShape::Cone { radius, height }, CollisionShape::Sphere { radius: sr }) => {
1990 cone_sphere_contact(a.position, *radius, *height, b.position, *sr, idx_a, idx_b)
1991 }
1992 (CollisionShape::Sphere { radius: sr }, CollisionShape::Cone { radius, height }) => {
1993 cone_sphere_contact(b.position, *radius, *height, a.position, *sr, idx_b, idx_a).map(|c| Contact {
1994 body_a: idx_a,
1995 body_b: idx_b,
1996 normal: [-c.normal[0], -c.normal[1], -c.normal[2]],
1997 ..c
1998 })
1999 }
2000 (CollisionShape::Cone { radius, height }, CollisionShape::Plane { normal, d }) => {
2001 cone_plane_contact(a.position, *radius, *height, *normal, *d, idx_a, idx_b)
2002 }
2003 (CollisionShape::Plane { normal, d }, CollisionShape::Cone { radius, height }) => {
2004 cone_plane_contact(b.position, *radius, *height, *normal, *d, idx_b, idx_a).map(|c| Contact {
2005 body_a: idx_a,
2006 body_b: idx_b,
2007 normal: [-c.normal[0], -c.normal[1], -c.normal[2]],
2008 ..c
2009 })
2010 }
2011 _ => None, }
2013}
2014
2015fn sphere_sphere(pos_a: [f32; 3], ra: f32, pos_b: [f32; 3], rb: f32, idx_a: usize, idx_b: usize) -> Option<Contact> {
2016 let dx = pos_b[0] - pos_a[0];
2017 let dy = pos_b[1] - pos_a[1];
2018 let dz = pos_b[2] - pos_a[2];
2019 let dist_sq = dx * dx + dy * dy + dz * dz;
2020 let sum_r = ra + rb;
2021 if dist_sq >= sum_r * sum_r {
2022 return None;
2023 }
2024 let dist = dist_sq.sqrt();
2025 let normal = if dist > 1e-8 {
2026 [dx / dist, dy / dist, dz / dist]
2027 } else {
2028 [0.0, 1.0, 0.0]
2029 };
2030 Some(Contact {
2031 body_a: idx_a,
2032 body_b: idx_b,
2033 normal,
2034 depth: sum_r - dist,
2035 point: [
2036 pos_a[0] + normal[0] * ra,
2037 pos_a[1] + normal[1] * ra,
2038 pos_a[2] + normal[2] * ra,
2039 ],
2040 cached_impulse: 0.0,
2041 })
2042}
2043
2044fn sphere_plane(
2045 sphere_pos: [f32; 3],
2046 radius: f32,
2047 plane_n: [f32; 3],
2048 plane_d: f32,
2049 sphere_idx: usize,
2050 plane_idx: usize,
2051 swap: bool,
2052) -> Option<Contact> {
2053 let dist = vec3_dot(plane_n, sphere_pos) + plane_d;
2054 if dist >= radius {
2055 return None;
2056 }
2057 let depth = radius - dist;
2058 let (a, b) = if swap {
2059 (plane_idx, sphere_idx)
2060 } else {
2061 (sphere_idx, plane_idx)
2062 };
2063 let normal = if swap {
2064 [-plane_n[0], -plane_n[1], -plane_n[2]]
2065 } else {
2066 plane_n
2067 };
2068 Some(Contact {
2069 body_a: a,
2070 body_b: b,
2071 normal,
2072 depth,
2073 point: [
2074 sphere_pos[0] - plane_n[0] * dist,
2075 sphere_pos[1] - plane_n[1] * dist,
2076 sphere_pos[2] - plane_n[2] * dist,
2077 ],
2078 cached_impulse: 0.0,
2079 })
2080}
2081
2082fn aabb_aabb(
2083 pos_a: [f32; 3],
2084 ha: [f32; 3],
2085 pos_b: [f32; 3],
2086 hb: [f32; 3],
2087 idx_a: usize,
2088 idx_b: usize,
2089) -> Option<Contact> {
2090 let mut overlap = [0.0f32; 3];
2091 for i in 0..3 {
2092 let gap = (pos_b[i] - pos_a[i]).abs() - (ha[i] + hb[i]);
2093 if gap > 0.0 {
2094 return None;
2095 }
2096 overlap[i] = -gap;
2097 }
2098 let mut min_axis = 0;
2100 for i in 1..3 {
2101 if overlap[i] < overlap[min_axis] {
2102 min_axis = i;
2103 }
2104 }
2105 let sign = if pos_b[min_axis] > pos_a[min_axis] { 1.0 } else { -1.0 };
2106 let mut normal = [0.0f32; 3];
2107 normal[min_axis] = sign;
2108 let mid = [
2109 (pos_a[0] + pos_b[0]) * 0.5,
2110 (pos_a[1] + pos_b[1]) * 0.5,
2111 (pos_a[2] + pos_b[2]) * 0.5,
2112 ];
2113 Some(Contact {
2114 body_a: idx_a,
2115 body_b: idx_b,
2116 normal,
2117 depth: overlap[min_axis],
2118 point: mid,
2119 cached_impulse: 0.0,
2120 })
2121}
2122
2123fn sphere_aabb(
2124 sphere_pos: [f32; 3],
2125 radius: f32,
2126 aabb_pos: [f32; 3],
2127 half: [f32; 3],
2128 sphere_idx: usize,
2129 aabb_idx: usize,
2130) -> Option<Contact> {
2131 let mut closest = [0.0f32; 3];
2133 for i in 0..3 {
2134 closest[i] = sphere_pos[i].clamp(aabb_pos[i] - half[i], aabb_pos[i] + half[i]);
2135 }
2136 let dx = sphere_pos[0] - closest[0];
2137 let dy = sphere_pos[1] - closest[1];
2138 let dz = sphere_pos[2] - closest[2];
2139 let dist_sq = dx * dx + dy * dy + dz * dz;
2140 if dist_sq >= radius * radius {
2141 return None;
2142 }
2143 let dist = dist_sq.sqrt();
2144 let normal = if dist > 1e-8 {
2145 [dx / dist, dy / dist, dz / dist]
2146 } else {
2147 [0.0, 1.0, 0.0]
2148 };
2149 Some(Contact {
2150 body_a: sphere_idx,
2151 body_b: aabb_idx,
2152 normal,
2153 depth: radius - dist,
2154 point: closest,
2155 cached_impulse: 0.0,
2156 })
2157}
2158
2159fn capsule_closest_segment_point(capsule_pos: [f32; 3], half_height: f32, point: [f32; 3]) -> [f32; 3] {
2164 let dy = point[1] - capsule_pos[1];
2165 let clamped_y = dy.clamp(-half_height, half_height);
2166 [capsule_pos[0], capsule_pos[1] + clamped_y, capsule_pos[2]]
2167}
2168
2169fn capsule_sphere_contact(
2170 cap_pos: [f32; 3],
2171 cap_radius: f32,
2172 cap_half_h: f32,
2173 sphere_pos: [f32; 3],
2174 sphere_radius: f32,
2175 cap_idx: usize,
2176 sphere_idx: usize,
2177) -> Option<Contact> {
2178 let closest = capsule_closest_segment_point(cap_pos, cap_half_h, sphere_pos);
2179 sphere_sphere(closest, cap_radius, sphere_pos, sphere_radius, cap_idx, sphere_idx)
2180}
2181
2182fn capsule_plane_contact(
2183 cap_pos: [f32; 3],
2184 cap_radius: f32,
2185 cap_half_h: f32,
2186 plane_n: [f32; 3],
2187 plane_d: f32,
2188 cap_idx: usize,
2189 plane_idx: usize,
2190) -> Option<Contact> {
2191 let top = [cap_pos[0], cap_pos[1] + cap_half_h, cap_pos[2]];
2193 let bot = [cap_pos[0], cap_pos[1] - cap_half_h, cap_pos[2]];
2194 let dist_top = vec3_dot(plane_n, top) + plane_d;
2195 let dist_bot = vec3_dot(plane_n, bot) + plane_d;
2196 let (closest_pt, min_dist) = if dist_top < dist_bot {
2197 (top, dist_top)
2198 } else {
2199 (bot, dist_bot)
2200 };
2201 if min_dist >= cap_radius {
2202 return None;
2203 }
2204 let depth = cap_radius - min_dist;
2205 Some(Contact {
2206 body_a: cap_idx,
2207 body_b: plane_idx,
2208 normal: plane_n,
2209 depth,
2210 point: [
2211 closest_pt[0] - plane_n[0] * min_dist,
2212 closest_pt[1] - plane_n[1] * min_dist,
2213 closest_pt[2] - plane_n[2] * min_dist,
2214 ],
2215 cached_impulse: 0.0,
2216 })
2217}
2218
2219fn capsule_aabb_contact(
2220 cap_pos: [f32; 3],
2221 cap_radius: f32,
2222 cap_half_h: f32,
2223 aabb_pos: [f32; 3],
2224 aabb_half: [f32; 3],
2225 cap_idx: usize,
2226 aabb_idx: usize,
2227) -> Option<Contact> {
2228 let seg_point = capsule_closest_segment_point(cap_pos, cap_half_h, aabb_pos);
2231 sphere_aabb(seg_point, cap_radius, aabb_pos, aabb_half, cap_idx, aabb_idx)
2232}
2233
2234fn cylinder_sphere_contact(
2237 cyl_pos: [f32; 3],
2238 cyl_radius: f32,
2239 cyl_half_h: f32,
2240 sphere_pos: [f32; 3],
2241 sphere_radius: f32,
2242 cyl_idx: usize,
2243 sphere_idx: usize,
2244) -> Option<Contact> {
2245 let dx = sphere_pos[0] - cyl_pos[0];
2247 let dz = sphere_pos[2] - cyl_pos[2];
2248 let radial_dist = (dx * dx + dz * dz).sqrt();
2249 let dy = sphere_pos[1] - cyl_pos[1];
2250 let clamped_y = dy.clamp(-cyl_half_h, cyl_half_h);
2251
2252 let on_axis = [cyl_pos[0], cyl_pos[1] + clamped_y, cyl_pos[2]];
2254 let mut closest = on_axis;
2255
2256 if radial_dist > 1e-8 {
2257 let scale = cyl_radius / radial_dist;
2258 if radial_dist > cyl_radius {
2260 closest[0] = cyl_pos[0] + dx * scale;
2261 closest[2] = cyl_pos[2] + dz * scale;
2262 } else {
2263 closest[0] = sphere_pos[0];
2264 closest[2] = sphere_pos[2];
2265 }
2266 }
2267
2268 let cap_overlap_y = cyl_half_h - dy.abs();
2270 let cap_overlap_r = cyl_radius - radial_dist;
2271
2272 if radial_dist < cyl_radius && dy.abs() < cyl_half_h {
2274 if cap_overlap_y < cap_overlap_r {
2276 let sign = if dy > 0.0 { 1.0 } else { -1.0 };
2278 let depth = cap_overlap_y + sphere_radius;
2279 let normal = [0.0, sign, 0.0];
2280 let point = [sphere_pos[0], cyl_pos[1] + cyl_half_h * sign, sphere_pos[2]];
2281 return Some(Contact {
2282 body_a: cyl_idx,
2283 body_b: sphere_idx,
2284 normal,
2285 depth,
2286 point,
2287 cached_impulse: 0.0,
2288 });
2289 } else {
2290 let depth = cap_overlap_r + sphere_radius;
2292 if radial_dist > 1e-8 {
2293 let nx = dx / radial_dist;
2294 let nz = dz / radial_dist;
2295 let point = [cyl_pos[0] + nx * cyl_radius, on_axis[1], cyl_pos[2] + nz * cyl_radius];
2296 return Some(Contact {
2297 body_a: cyl_idx,
2298 body_b: sphere_idx,
2299 normal: [nx, 0.0, nz],
2300 depth,
2301 point,
2302 cached_impulse: 0.0,
2303 });
2304 } else {
2305 return Some(Contact {
2306 body_a: cyl_idx,
2307 body_b: sphere_idx,
2308 normal: [1.0, 0.0, 0.0],
2309 depth,
2310 point: closest,
2311 cached_impulse: 0.0,
2312 });
2313 }
2314 }
2315 }
2316
2317 let to_sphere = [
2319 sphere_pos[0] - closest[0],
2320 sphere_pos[1] - closest[1],
2321 sphere_pos[2] - closest[2],
2322 ];
2323 let dist = vec3_len(to_sphere);
2324 if dist >= sphere_radius {
2325 return None;
2326 }
2327 let normal = if dist > 1e-8 {
2328 vec3_scale(to_sphere, 1.0 / dist)
2329 } else {
2330 [0.0, 1.0, 0.0]
2331 };
2332 Some(Contact {
2333 body_a: cyl_idx,
2334 body_b: sphere_idx,
2335 normal,
2336 depth: sphere_radius - dist,
2337 point: closest,
2338 cached_impulse: 0.0,
2339 })
2340}
2341
2342fn cylinder_plane_contact(
2343 cyl_pos: [f32; 3],
2344 cyl_radius: f32,
2345 cyl_half_h: f32,
2346 plane_n: [f32; 3],
2347 plane_d: f32,
2348 cyl_idx: usize,
2349 plane_idx: usize,
2350) -> Option<Contact> {
2351 let top_center = [cyl_pos[0], cyl_pos[1] + cyl_half_h, cyl_pos[2]];
2355 let bot_center = [cyl_pos[0], cyl_pos[1] - cyl_half_h, cyl_pos[2]];
2356
2357 let radial_n = [plane_n[0], 0.0, plane_n[2]];
2359 let radial_len = vec3_len(radial_n);
2360 let rim_offset = if radial_len > 1e-8 {
2361 vec3_scale(radial_n, -cyl_radius / radial_len)
2362 } else {
2363 [0.0, 0.0, 0.0]
2364 };
2365
2366 let candidates = [
2368 top_center,
2369 bot_center,
2370 [
2371 top_center[0] + rim_offset[0],
2372 top_center[1],
2373 top_center[2] + rim_offset[2],
2374 ],
2375 [
2376 bot_center[0] + rim_offset[0],
2377 bot_center[1],
2378 bot_center[2] + rim_offset[2],
2379 ],
2380 ];
2381
2382 let mut deepest_dist = f32::MAX;
2383 let mut deepest_pt = cyl_pos;
2384 for pt in &candidates {
2385 let dist = vec3_dot(plane_n, *pt) + plane_d;
2386 if dist < deepest_dist {
2387 deepest_dist = dist;
2388 deepest_pt = *pt;
2389 }
2390 }
2391
2392 if deepest_dist >= 0.0 {
2393 return None;
2394 }
2395 Some(Contact {
2396 body_a: cyl_idx,
2397 body_b: plane_idx,
2398 normal: plane_n,
2399 depth: -deepest_dist,
2400 point: [
2401 deepest_pt[0] - plane_n[0] * deepest_dist,
2402 deepest_pt[1] - plane_n[1] * deepest_dist,
2403 deepest_pt[2] - plane_n[2] * deepest_dist,
2404 ],
2405 cached_impulse: 0.0,
2406 })
2407}
2408
2409fn cone_sphere_contact(
2412 cone_pos: [f32; 3],
2413 cone_radius: f32,
2414 cone_height: f32,
2415 sphere_pos: [f32; 3],
2416 sphere_radius: f32,
2417 cone_idx: usize,
2418 sphere_idx: usize,
2419) -> Option<Contact> {
2420 let dx = sphere_pos[0] - cone_pos[0];
2424 let dy = sphere_pos[1] - cone_pos[1];
2425 let dz = sphere_pos[2] - cone_pos[2];
2426 let radial_dist = (dx * dx + dz * dz).sqrt();
2427
2428 let clamped_y = dy.clamp(0.0, cone_height);
2430 let r_at_y = cone_radius * (1.0 - clamped_y / cone_height);
2432
2433 let mut closest = [cone_pos[0], cone_pos[1] + clamped_y, cone_pos[2]];
2435 if radial_dist > 1e-8 {
2436 let clamp_r = radial_dist.min(r_at_y);
2437 closest[0] += dx / radial_dist * clamp_r;
2438 closest[2] += dz / radial_dist * clamp_r;
2439 }
2440
2441 if dy >= 0.0 && dy <= cone_height && radial_dist < r_at_y {
2443 let base_dist = dy + sphere_radius;
2445 let tip_dist = cone_height - dy; let slant_len = (cone_height * cone_height + cone_radius * cone_radius).sqrt();
2447 let side_normal_y = cone_radius / slant_len;
2448 let side_normal_r = cone_height / slant_len;
2449 let side_dist = (r_at_y - radial_dist) * side_normal_r + sphere_radius;
2450
2451 if base_dist < tip_dist && base_dist < side_dist {
2452 return Some(Contact {
2454 body_a: cone_idx,
2455 body_b: sphere_idx,
2456 normal: [0.0, -1.0, 0.0],
2457 depth: base_dist,
2458 point: [sphere_pos[0], cone_pos[1], sphere_pos[2]],
2459 cached_impulse: 0.0,
2460 });
2461 } else if side_dist < tip_dist {
2462 if radial_dist > 1e-8 {
2464 let nx = dx / radial_dist * side_normal_r;
2465 let nz = dz / radial_dist * side_normal_r;
2466 let normal = vec3_normalize([nx, side_normal_y, nz]);
2467 return Some(Contact {
2468 body_a: cone_idx,
2469 body_b: sphere_idx,
2470 normal,
2471 depth: side_dist,
2472 point: closest,
2473 cached_impulse: 0.0,
2474 });
2475 }
2476 }
2477 return Some(Contact {
2479 body_a: cone_idx,
2480 body_b: sphere_idx,
2481 normal: [0.0, 1.0, 0.0],
2482 depth: sphere_radius,
2483 point: [cone_pos[0], cone_pos[1] + cone_height, cone_pos[2]],
2484 cached_impulse: 0.0,
2485 });
2486 }
2487
2488 let to_sphere = [
2489 sphere_pos[0] - closest[0],
2490 sphere_pos[1] - closest[1],
2491 sphere_pos[2] - closest[2],
2492 ];
2493 let dist = vec3_len(to_sphere);
2494 if dist >= sphere_radius {
2495 return None;
2496 }
2497 let normal = if dist > 1e-8 {
2498 vec3_scale(to_sphere, 1.0 / dist)
2499 } else {
2500 [0.0, 1.0, 0.0]
2501 };
2502 Some(Contact {
2503 body_a: cone_idx,
2504 body_b: sphere_idx,
2505 normal,
2506 depth: sphere_radius - dist,
2507 point: closest,
2508 cached_impulse: 0.0,
2509 })
2510}
2511
2512fn cone_plane_contact(
2513 cone_pos: [f32; 3],
2514 cone_radius: f32,
2515 cone_height: f32,
2516 plane_n: [f32; 3],
2517 plane_d: f32,
2518 cone_idx: usize,
2519 plane_idx: usize,
2520) -> Option<Contact> {
2521 let tip = [cone_pos[0], cone_pos[1] + cone_height, cone_pos[2]];
2523
2524 let base = cone_pos;
2526
2527 let radial_n = [plane_n[0], 0.0, plane_n[2]];
2529 let radial_len = vec3_len(radial_n);
2530 let rim_offset = if radial_len > 1e-8 {
2531 vec3_scale(radial_n, -cone_radius / radial_len)
2532 } else {
2533 [0.0, 0.0, 0.0]
2534 };
2535 let rim_pt = [base[0] + rim_offset[0], base[1], base[2] + rim_offset[2]];
2536
2537 let candidates = [tip, base, rim_pt];
2538 let mut deepest_dist = f32::MAX;
2539 let mut deepest_pt = cone_pos;
2540 for pt in &candidates {
2541 let dist = vec3_dot(plane_n, *pt) + plane_d;
2542 if dist < deepest_dist {
2543 deepest_dist = dist;
2544 deepest_pt = *pt;
2545 }
2546 }
2547
2548 if deepest_dist >= 0.0 {
2549 return None;
2550 }
2551 Some(Contact {
2552 body_a: cone_idx,
2553 body_b: plane_idx,
2554 normal: plane_n,
2555 depth: -deepest_dist,
2556 point: [
2557 deepest_pt[0] - plane_n[0] * deepest_dist,
2558 deepest_pt[1] - plane_n[1] * deepest_dist,
2559 deepest_pt[2] - plane_n[2] * deepest_dist,
2560 ],
2561 cached_impulse: 0.0,
2562 })
2563}
2564
2565fn build_islands(body_count: usize, contacts: &[Contact]) -> Vec<Vec<usize>> {
2571 let mut parent: Vec<usize> = (0..body_count).collect();
2572 let mut rank: Vec<u8> = vec![0; body_count];
2573
2574 fn find(parent: &mut [usize], mut x: usize) -> usize {
2575 while parent[x] != x {
2576 parent[x] = parent[parent[x]]; x = parent[x];
2578 }
2579 x
2580 }
2581
2582 fn union(parent: &mut [usize], rank: &mut [u8], a: usize, b: usize) {
2583 let ra = find(parent, a);
2584 let rb = find(parent, b);
2585 if ra == rb {
2586 return;
2587 }
2588 if rank[ra] < rank[rb] {
2589 parent[ra] = rb;
2590 } else if rank[ra] > rank[rb] {
2591 parent[rb] = ra;
2592 } else {
2593 parent[rb] = ra;
2594 rank[ra] += 1;
2595 }
2596 }
2597
2598 for c in contacts {
2600 union(&mut parent, &mut rank, c.body_a, c.body_b);
2601 }
2602
2603 let mut island_map: std::collections::HashMap<usize, Vec<usize>> = std::collections::HashMap::new();
2605 for (ci, c) in contacts.iter().enumerate() {
2606 let root = find(&mut parent, c.body_a);
2607 island_map.entry(root).or_default().push(ci);
2608 }
2609
2610 island_map.into_values().collect()
2611}
2612
2613fn warm_start_contacts(contacts: &mut [Contact], prev_contacts: &[Contact], bodies: &mut [RigidBody]) {
2617 use std::collections::HashMap;
2620 let mut cache: HashMap<(usize, usize), f32> = HashMap::with_capacity(prev_contacts.len());
2621 for pc in prev_contacts {
2622 let key = if pc.body_a <= pc.body_b {
2623 (pc.body_a, pc.body_b)
2624 } else {
2625 (pc.body_b, pc.body_a)
2626 };
2627 cache.insert(key, pc.cached_impulse);
2628 }
2629
2630 for c in contacts.iter_mut() {
2631 let key = if c.body_a <= c.body_b {
2632 (c.body_a, c.body_b)
2633 } else {
2634 (c.body_b, c.body_a)
2635 };
2636 let cached = cache.get(&key).copied().unwrap_or(0.0);
2637
2638 if cached.abs() > 1e-6 {
2639 c.cached_impulse = cached;
2640 let n = c.normal;
2641 let inv_sum = bodies[c.body_a].inv_mass + bodies[c.body_b].inv_mass;
2642 if inv_sum > 1e-8 {
2643 let j = cached * 0.8;
2644 bodies[c.body_a].velocity[0] += n[0] * j * bodies[c.body_a].inv_mass;
2645 bodies[c.body_a].velocity[1] += n[1] * j * bodies[c.body_a].inv_mass;
2646 bodies[c.body_a].velocity[2] += n[2] * j * bodies[c.body_a].inv_mass;
2647 bodies[c.body_b].velocity[0] -= n[0] * j * bodies[c.body_b].inv_mass;
2648 bodies[c.body_b].velocity[1] -= n[1] * j * bodies[c.body_b].inv_mass;
2649 bodies[c.body_b].velocity[2] -= n[2] * j * bodies[c.body_b].inv_mass;
2650 }
2651 }
2652 }
2653}
2654
2655fn resolve_contact(a: &mut RigidBody, b: &mut RigidBody, contact: &mut Contact) {
2658 let n = contact.normal;
2659
2660 let v_rel = [
2662 a.velocity[0] - b.velocity[0],
2663 a.velocity[1] - b.velocity[1],
2664 a.velocity[2] - b.velocity[2],
2665 ];
2666 let v_along_normal = vec3_dot(v_rel, n);
2667
2668 if v_along_normal > 0.0 {
2670 return;
2671 }
2672
2673 let e = a.restitution.min(b.restitution);
2675
2676 let inv_mass_sum = a.inv_mass + b.inv_mass;
2678 if inv_mass_sum < 1e-8 {
2679 return; }
2681 let j = -(1.0 + e) * v_along_normal / inv_mass_sum;
2682 contact.cached_impulse = j; a.velocity[0] += n[0] * j * a.inv_mass;
2686 a.velocity[1] += n[1] * j * a.inv_mass;
2687 a.velocity[2] += n[2] * j * a.inv_mass;
2688
2689 b.velocity[0] -= n[0] * j * b.inv_mass;
2690 b.velocity[1] -= n[1] * j * b.inv_mass;
2691 b.velocity[2] -= n[2] * j * b.inv_mass;
2692
2693 let correction_pct = 0.8;
2695 let slop = 0.01;
2696 let correction = (contact.depth - slop).max(0.0) * correction_pct / inv_mass_sum;
2697
2698 a.position[0] -= n[0] * correction * a.inv_mass;
2699 a.position[1] -= n[1] * correction * a.inv_mass;
2700 a.position[2] -= n[2] * correction * a.inv_mass;
2701
2702 b.position[0] += n[0] * correction * b.inv_mass;
2703 b.position[1] += n[1] * correction * b.inv_mass;
2704 b.position[2] += n[2] * correction * b.inv_mass;
2705
2706 let tangent = [
2708 v_rel[0] - n[0] * v_along_normal,
2709 v_rel[1] - n[1] * v_along_normal,
2710 v_rel[2] - n[2] * v_along_normal,
2711 ];
2712 let tangent_len = vec3_len(tangent);
2713 if tangent_len > 1e-8 {
2714 let t = vec3_scale(tangent, 1.0 / tangent_len);
2715 let v_along_t = vec3_dot(v_rel, t);
2716 let mu = (a.friction + b.friction) * 0.5;
2717 let jt = (-v_along_t / inv_mass_sum).clamp(-j.abs() * mu, j.abs() * mu);
2718
2719 a.velocity[0] += t[0] * jt * a.inv_mass;
2720 a.velocity[1] += t[1] * jt * a.inv_mass;
2721 a.velocity[2] += t[2] * jt * a.inv_mass;
2722
2723 b.velocity[0] -= t[0] * jt * b.inv_mass;
2724 b.velocity[1] -= t[1] * jt * b.inv_mass;
2725 b.velocity[2] -= t[2] * jt * b.inv_mass;
2726 }
2727}
2728
2729fn ray_sphere(origin: [f32; 3], dir: [f32; 3], center: [f32; 3], radius: f32) -> Option<(f32, [f32; 3], [f32; 3])> {
2732 let oc = [origin[0] - center[0], origin[1] - center[1], origin[2] - center[2]];
2733 let b = vec3_dot(oc, dir);
2734 let c = vec3_dot(oc, oc) - radius * radius;
2735 let disc = b * b - c;
2736 if disc < 0.0 {
2737 return None;
2738 }
2739 let t = -b - disc.sqrt();
2740 if t < 0.0 {
2741 return None;
2742 }
2743 let point = [origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t];
2744 let normal = vec3_normalize([point[0] - center[0], point[1] - center[1], point[2] - center[2]]);
2745 Some((t, normal, point))
2746}
2747
2748fn ray_plane(origin: [f32; 3], dir: [f32; 3], normal: [f32; 3], d: f32) -> Option<(f32, [f32; 3], [f32; 3])> {
2749 let denom = vec3_dot(normal, dir);
2750 if denom.abs() < 1e-8 {
2751 return None;
2752 }
2753 let t = -(vec3_dot(normal, origin) + d) / denom;
2754 if t < 0.0 {
2755 return None;
2756 }
2757 let point = [origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t];
2758 Some((t, normal, point))
2759}
2760
2761fn ray_aabb(origin: [f32; 3], dir: [f32; 3], center: [f32; 3], half: [f32; 3]) -> Option<(f32, [f32; 3], [f32; 3])> {
2762 let mut tmin = f32::NEG_INFINITY;
2763 let mut tmax = f32::INFINITY;
2764 let mut hit_axis = 0usize;
2765 let mut hit_sign = 1.0f32;
2766
2767 for i in 0..3 {
2768 if dir[i].abs() < 1e-8 {
2769 if origin[i] < center[i] - half[i] || origin[i] > center[i] + half[i] {
2770 return None;
2771 }
2772 } else {
2773 let inv_d = 1.0 / dir[i];
2774 let t1 = (center[i] - half[i] - origin[i]) * inv_d;
2775 let t2 = (center[i] + half[i] - origin[i]) * inv_d;
2776 let (t_near, t_far) = if t1 < t2 { (t1, t2) } else { (t2, t1) };
2777 if t_near > tmin {
2778 tmin = t_near;
2779 hit_axis = i;
2780 hit_sign = if dir[i] > 0.0 { -1.0 } else { 1.0 };
2781 }
2782 tmax = tmax.min(t_far);
2783 if tmin > tmax {
2784 return None;
2785 }
2786 }
2787 }
2788 if tmin < 0.0 {
2789 return None;
2790 }
2791 let point = [
2792 origin[0] + dir[0] * tmin,
2793 origin[1] + dir[1] * tmin,
2794 origin[2] + dir[2] * tmin,
2795 ];
2796 let mut normal = [0.0f32; 3];
2797 normal[hit_axis] = hit_sign;
2798 Some((tmin, normal, point))
2799}
2800
2801fn ray_capsule(
2804 origin: [f32; 3],
2805 dir: [f32; 3],
2806 center: [f32; 3],
2807 radius: f32,
2808 half_height: f32,
2809) -> Option<(f32, [f32; 3], [f32; 3])> {
2810 let ox = origin[0] - center[0];
2816 let oz = origin[2] - center[2];
2817 let a = dir[0] * dir[0] + dir[2] * dir[2];
2818 let b = ox * dir[0] + oz * dir[2];
2819 let c = ox * ox + oz * oz - radius * radius;
2820
2821 let mut best: Option<(f32, [f32; 3], [f32; 3])> = None;
2822
2823 if a > 1e-12 {
2824 let disc = b * b - a * c;
2825 if disc >= 0.0 {
2826 let sqrt_disc = disc.sqrt();
2827 for &t in &[(-b - sqrt_disc) / a, (-b + sqrt_disc) / a] {
2828 if t < 0.0 {
2829 continue;
2830 }
2831 let hit_y = origin[1] + dir[1] * t - center[1];
2832 if hit_y >= -half_height && hit_y <= half_height {
2833 let point = [origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t];
2834 let normal = vec3_normalize([point[0] - center[0], 0.0, point[2] - center[2]]);
2835 if best.as_ref().map_or(true, |prev| t < prev.0) {
2836 best = Some((t, normal, point));
2837 }
2838 break; }
2840 }
2841 }
2842 }
2843
2844 let top = [center[0], center[1] + half_height, center[2]];
2846 if let Some((t, n, p)) = ray_sphere(origin, dir, top, radius) {
2847 if p[1] >= top[1] {
2848 if best.as_ref().map_or(true, |prev| t < prev.0) {
2850 best = Some((t, n, p));
2851 }
2852 }
2853 }
2854
2855 let bot = [center[0], center[1] - half_height, center[2]];
2857 if let Some((t, n, p)) = ray_sphere(origin, dir, bot, radius) {
2858 if p[1] <= bot[1] {
2859 if best.as_ref().map_or(true, |prev| t < prev.0) {
2861 best = Some((t, n, p));
2862 }
2863 }
2864 }
2865
2866 best
2867}
2868
2869fn ray_cylinder(
2870 origin: [f32; 3],
2871 dir: [f32; 3],
2872 center: [f32; 3],
2873 radius: f32,
2874 half_height: f32,
2875) -> Option<(f32, [f32; 3], [f32; 3])> {
2876 let mut best: Option<(f32, [f32; 3], [f32; 3])> = None;
2877
2878 let ox = origin[0] - center[0];
2880 let oz = origin[2] - center[2];
2881 let a = dir[0] * dir[0] + dir[2] * dir[2];
2882 let b = ox * dir[0] + oz * dir[2];
2883 let c = ox * ox + oz * oz - radius * radius;
2884
2885 if a > 1e-12 {
2886 let disc = b * b - a * c;
2887 if disc >= 0.0 {
2888 let sqrt_disc = disc.sqrt();
2889 for &t in &[(-b - sqrt_disc) / a, (-b + sqrt_disc) / a] {
2890 if t < 0.0 {
2891 continue;
2892 }
2893 let hit_y = origin[1] + dir[1] * t - center[1];
2894 if hit_y >= -half_height && hit_y <= half_height {
2895 let point = [origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t];
2896 let normal = vec3_normalize([point[0] - center[0], 0.0, point[2] - center[2]]);
2897 if best.as_ref().map_or(true, |prev| t < prev.0) {
2898 best = Some((t, normal, point));
2899 }
2900 break;
2901 }
2902 }
2903 }
2904 }
2905
2906 if dir[1].abs() > 1e-8 {
2908 let t_top = (center[1] + half_height - origin[1]) / dir[1];
2909 if t_top >= 0.0 {
2910 let px = origin[0] + dir[0] * t_top - center[0];
2911 let pz = origin[2] + dir[2] * t_top - center[2];
2912 if px * px + pz * pz <= radius * radius {
2913 if best.as_ref().map_or(true, |prev| t_top < prev.0) {
2914 let point = [
2915 origin[0] + dir[0] * t_top,
2916 origin[1] + dir[1] * t_top,
2917 origin[2] + dir[2] * t_top,
2918 ];
2919 best = Some((t_top, [0.0, 1.0, 0.0], point));
2920 }
2921 }
2922 }
2923 let t_bot = (center[1] - half_height - origin[1]) / dir[1];
2925 if t_bot >= 0.0 {
2926 let px = origin[0] + dir[0] * t_bot - center[0];
2927 let pz = origin[2] + dir[2] * t_bot - center[2];
2928 if px * px + pz * pz <= radius * radius {
2929 if best.as_ref().map_or(true, |prev| t_bot < prev.0) {
2930 let point = [
2931 origin[0] + dir[0] * t_bot,
2932 origin[1] + dir[1] * t_bot,
2933 origin[2] + dir[2] * t_bot,
2934 ];
2935 best = Some((t_bot, [0.0, -1.0, 0.0], point));
2936 }
2937 }
2938 }
2939 }
2940
2941 best
2942}
2943
2944fn ray_cone(
2945 origin: [f32; 3],
2946 dir: [f32; 3],
2947 center: [f32; 3],
2948 radius: f32,
2949 height: f32,
2950) -> Option<(f32, [f32; 3], [f32; 3])> {
2951 let oy = origin[1] - center[1];
2959 let ox = origin[0] - center[0];
2960 let oz = origin[2] - center[2];
2961 let k = radius / height;
2962 let k2 = k * k;
2963
2964 let h0 = height - oy;
2966 let a = dir[0] * dir[0] + dir[2] * dir[2] - k2 * dir[1] * dir[1];
2969 let b = ox * dir[0] + oz * dir[2] + k2 * dir[1] * h0; let c = ox * ox + oz * oz - k2 * h0 * h0;
2971
2972 let mut best: Option<(f32, [f32; 3], [f32; 3])> = None;
2973
2974 let disc = b * b - a * c;
2976 if disc >= 0.0 && a.abs() > 1e-12 {
2977 let sqrt_disc = disc.sqrt();
2978 for &t in &[(-b - sqrt_disc) / a, (-b + sqrt_disc) / a] {
2979 if t < 0.0 {
2980 continue;
2981 }
2982 let hit_y = oy + dir[1] * t;
2983 if hit_y >= 0.0 && hit_y <= height {
2984 let point = [origin[0] + dir[0] * t, origin[1] + dir[1] * t, origin[2] + dir[2] * t];
2985 let px = point[0] - center[0];
2987 let pz = point[2] - center[2];
2988 let py_cone = height - hit_y;
2989 let normal = vec3_normalize([2.0 * px, 2.0 * k2 * py_cone, 2.0 * pz]);
2990 if best.as_ref().map_or(true, |prev| t < prev.0) {
2991 best = Some((t, normal, point));
2992 }
2993 break;
2994 }
2995 }
2996 }
2997
2998 if dir[1].abs() > 1e-8 {
3000 let t_base = -oy / dir[1];
3001 if t_base >= 0.0 {
3002 let px = ox + dir[0] * t_base;
3003 let pz = oz + dir[2] * t_base;
3004 if px * px + pz * pz <= radius * radius {
3005 if best.as_ref().map_or(true, |prev| t_base < prev.0) {
3006 let point = [
3007 origin[0] + dir[0] * t_base,
3008 origin[1] + dir[1] * t_base,
3009 origin[2] + dir[2] * t_base,
3010 ];
3011 best = Some((t_base, [0.0, -1.0, 0.0], point));
3012 }
3013 }
3014 }
3015 }
3016
3017 best
3018}
3019
3020#[cfg(feature = "parallel-physics")]
3025fn atomic_f32_add(atom: &std::sync::atomic::AtomicU32, val: f32) {
3026 use std::sync::atomic::Ordering;
3027 loop {
3028 let bits = atom.load(Ordering::Relaxed);
3029 let current = f32::from_bits(bits);
3030 let new = current + val;
3031 match atom.compare_exchange_weak(bits, new.to_bits(), Ordering::Relaxed, Ordering::Relaxed) {
3032 Ok(_) => break,
3033 Err(_) => {} }
3035 }
3036}
3037
3038fn vec3_dot(a: [f32; 3], b: [f32; 3]) -> f32 {
3039 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
3040}
3041
3042fn vec3_len(v: [f32; 3]) -> f32 {
3043 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
3044}
3045
3046fn vec3_scale(v: [f32; 3], s: f32) -> [f32; 3] {
3047 [v[0] * s, v[1] * s, v[2] * s]
3048}
3049
3050fn vec3_normalize(v: [f32; 3]) -> [f32; 3] {
3051 let len = vec3_len(v);
3052 if len > 1e-8 {
3053 vec3_scale(v, 1.0 / len)
3054 } else {
3055 [0.0, 1.0, 0.0]
3056 }
3057}
3058
3059#[cfg(test)]
3060mod tests {
3061 use super::*;
3062
3063 #[test]
3064 fn sphere_falls_under_gravity() {
3065 let mut world = PhysicsWorld::new();
3066 let id = world
3067 .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 10.0, 0.0]));
3068 world.step(1.0 / 60.0);
3069 let body = world.body(id).unwrap();
3070 assert!(body.position[1] < 10.0, "Sphere should fall");
3071 assert!(body.velocity[1] < 0.0, "Velocity should be negative");
3072 }
3073
3074 #[test]
3075 fn sphere_bounces_on_plane() {
3076 let mut world = PhysicsWorld::new();
3077 let sphere = world.add_body(
3078 RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 })
3079 .with_position([0.0, 0.4, 0.0])
3080 .with_restitution(1.0),
3081 );
3082 world.add_body(RigidBody::fixed(CollisionShape::Plane {
3083 normal: [0.0, 1.0, 0.0],
3084 d: 0.0,
3085 }));
3086 world.body_mut(sphere).unwrap().velocity = [0.0, -5.0, 0.0];
3088 world.step(1.0 / 60.0);
3089 let body = world.body(sphere).unwrap();
3090 assert!(
3092 body.velocity[1] > 0.0,
3093 "Sphere should bounce up, got {}",
3094 body.velocity[1]
3095 );
3096 }
3097
3098 #[test]
3099 fn static_bodies_dont_move() {
3100 let mut world = PhysicsWorld::new();
3101 let id = world.add_body(RigidBody::fixed(CollisionShape::Plane {
3102 normal: [0.0, 1.0, 0.0],
3103 d: 0.0,
3104 }));
3105 world.step(1.0);
3106 let body = world.body(id).unwrap();
3107 assert_eq!(body.position, [0.0, 0.0, 0.0]);
3108 }
3109
3110 #[test]
3111 fn sphere_sphere_collision() {
3112 let pos_a = [0.0f32, 0.0, 0.0];
3114 let pos_b = [0.8f32, 0.0, 0.0];
3115 let contact = sphere_sphere(pos_a, 0.5, pos_b, 0.5, 0, 1);
3116 assert!(
3117 contact.is_some(),
3118 "Spheres at distance 0.8 with radii 0.5+0.5 should overlap"
3119 );
3120 let c = contact.unwrap();
3121 assert!(c.depth > 0.0, "Penetration depth should be positive");
3122 assert!(c.normal[0] > 0.0, "Normal should point from A to B (positive X)");
3123 }
3124
3125 #[test]
3126 fn aabb_collision() {
3127 let mut world = PhysicsWorld::new();
3128 world.gravity = [0.0, 0.0, 0.0];
3129 let a = world.add_body(
3130 RigidBody::dynamic(
3131 1.0,
3132 CollisionShape::Aabb {
3133 half_extents: [0.5, 0.5, 0.5],
3134 },
3135 )
3136 .with_position([0.0, 0.0, 0.0]),
3137 );
3138 let _b = world.add_body(
3139 RigidBody::dynamic(
3140 1.0,
3141 CollisionShape::Aabb {
3142 half_extents: [0.5, 0.5, 0.5],
3143 },
3144 )
3145 .with_position([0.9, 0.0, 0.0]),
3146 );
3147 world.body_mut(a).unwrap().velocity = [1.0, 0.0, 0.0];
3148 world.step(1.0 / 60.0);
3149 assert!(!world.contacts().is_empty(), "Should detect AABB overlap");
3150 }
3151
3152 #[test]
3153 fn raycast_hits_sphere() {
3154 let mut world = PhysicsWorld::new();
3155 world.add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 0.0, -5.0]));
3156 let hit = world.raycast([0.0, 0.0, 0.0], [0.0, 0.0, -1.0], 100.0);
3157 assert!(hit.is_some(), "Ray should hit the sphere");
3158 let h = hit.unwrap();
3159 assert!(
3160 (h.distance - 4.5).abs() < 0.01,
3161 "Distance should be ~4.5, got {}",
3162 h.distance
3163 );
3164 }
3165
3166 #[test]
3167 fn raycast_misses() {
3168 let mut world = PhysicsWorld::new();
3169 world.add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([10.0, 0.0, 0.0]));
3170 let hit = world.raycast([0.0, 0.0, 0.0], [0.0, 0.0, -1.0], 100.0);
3171 assert!(hit.is_none(), "Ray should miss");
3172 }
3173
3174 #[test]
3175 fn remove_body() {
3176 let mut world = PhysicsWorld::new();
3177 let id = world.add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }));
3178 assert_eq!(world.body_count(), 1);
3179 assert!(world.remove_body(id));
3180 assert_eq!(world.body_count(), 0);
3181 assert!(!world.remove_body(id)); }
3183
3184 #[test]
3185 fn zero_dt_is_noop() {
3186 let mut world = PhysicsWorld::new();
3187 let id = world
3188 .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 5.0, 0.0]));
3189 world.step(0.0);
3190 let body = world.body(id).unwrap();
3191 assert_eq!(body.position[1], 5.0);
3192 }
3193
3194 #[test]
3195 fn raycast_plane() {
3196 let mut world = PhysicsWorld::new();
3197 world.add_body(RigidBody::fixed(CollisionShape::Plane {
3198 normal: [0.0, 1.0, 0.0],
3199 d: 0.0,
3200 }));
3201 let hit = world.raycast([0.0, 5.0, 0.0], [0.0, -1.0, 0.0], 100.0);
3202 assert!(hit.is_some());
3203 let h = hit.unwrap();
3204 assert!((h.distance - 5.0).abs() < 0.01);
3205 }
3206
3207 #[test]
3208 fn sphere_aabb_collision() {
3209 let contact = sphere_aabb([0.0, 0.0, 0.0], 0.5, [0.8, 0.0, 0.0], [0.5, 0.5, 0.5], 0, 1);
3210 assert!(contact.is_some(), "Sphere-AABB should detect overlap");
3211 }
3212
3213 #[test]
3216 fn capsule_sphere_contact_overlap() {
3217 let contact = capsule_sphere_contact([0.0, 0.0, 0.0], 0.5, 1.0, [1.0, 0.0, 0.0], 0.6, 0, 1);
3220 assert!(
3221 contact.is_some(),
3222 "Capsule-sphere should overlap at distance 1.0 with radii sum 1.1"
3223 );
3224 let c = contact.unwrap();
3225 assert!(c.depth > 0.0);
3226 assert!(c.normal[0] > 0.0, "Normal should point from capsule to sphere (+X)");
3227 }
3228
3229 #[test]
3230 fn capsule_sphere_contact_miss() {
3231 let contact = capsule_sphere_contact([0.0, 0.0, 0.0], 0.3, 1.0, [5.0, 0.0, 0.0], 0.3, 0, 1);
3232 assert!(
3233 contact.is_none(),
3234 "Should not overlap at distance 5.0 with radii sum 0.6"
3235 );
3236 }
3237
3238 #[test]
3239 fn capsule_sphere_contact_along_axis() {
3240 let contact = capsule_sphere_contact([0.0, 0.0, 0.0], 0.5, 1.0, [0.0, 1.8, 0.0], 0.5, 0, 1);
3242 assert!(contact.is_some(), "Sphere near top cap should overlap");
3243 }
3244
3245 #[test]
3246 fn capsule_plane_contact_resting() {
3247 let contact = capsule_plane_contact([0.0, 0.2, 0.0], 0.3, 0.5, [0.0, 1.0, 0.0], 0.0, 0, 1);
3254 assert!(contact.is_some(), "Capsule bottom should penetrate the ground plane");
3255 let c = contact.unwrap();
3256 assert!(c.depth > 0.0);
3257 }
3258
3259 #[test]
3260 fn capsule_aabb_contact_overlap() {
3261 let contact = capsule_aabb_contact([0.0, 0.0, 0.0], 0.5, 1.0, [0.8, 0.0, 0.0], [0.5, 0.5, 0.5], 0, 1);
3262 assert!(contact.is_some(), "Capsule-AABB should overlap");
3263 }
3264
3265 #[test]
3268 fn cylinder_sphere_contact_radial() {
3269 let contact = cylinder_sphere_contact([0.0, 0.0, 0.0], 0.5, 1.0, [0.8, 0.0, 0.0], 0.5, 0, 1);
3272 assert!(contact.is_some(), "Cylinder-sphere should detect radial overlap");
3273 let c = contact.unwrap();
3274 assert!(c.depth > 0.0);
3275 }
3276
3277 #[test]
3278 fn cylinder_sphere_contact_above() {
3279 let contact = cylinder_sphere_contact([0.0, 0.0, 0.0], 0.5, 1.0, [0.0, 5.0, 0.0], 0.3, 0, 1);
3281 assert!(contact.is_none(), "Sphere far above cylinder should not overlap");
3282 }
3283
3284 #[test]
3285 fn cylinder_plane_contact_resting() {
3286 let contact = cylinder_plane_contact([0.0, 0.5, 0.0], 0.5, 1.0, [0.0, 1.0, 0.0], 0.0, 0, 1);
3289 assert!(contact.is_some(), "Cylinder bottom face should penetrate ground plane");
3290 let c = contact.unwrap();
3291 assert!(c.depth > 0.0);
3292 }
3293
3294 #[test]
3295 fn cylinder_plane_contact_floating() {
3296 let contact = cylinder_plane_contact([0.0, 5.0, 0.0], 0.5, 1.0, [0.0, 1.0, 0.0], 0.0, 0, 1);
3298 assert!(contact.is_none(), "Cylinder above plane should not overlap");
3299 }
3300
3301 #[test]
3304 fn cone_sphere_contact_near_base() {
3305 let contact = cone_sphere_contact([0.0, 0.0, 0.0], 1.0, 2.0, [1.2, 0.0, 0.0], 0.5, 0, 1);
3308 assert!(contact.is_some(), "Sphere near cone base should overlap");
3309 let c = contact.unwrap();
3310 assert!(c.depth > 0.0);
3311 }
3312
3313 #[test]
3314 fn cone_sphere_contact_miss() {
3315 let contact = cone_sphere_contact([0.0, 0.0, 0.0], 0.5, 2.0, [5.0, 0.0, 0.0], 0.3, 0, 1);
3316 assert!(contact.is_none(), "Sphere far from cone should not overlap");
3317 }
3318
3319 #[test]
3320 fn cone_plane_contact_resting_on_base() {
3321 let contact = cone_plane_contact([0.0, -0.1, 0.0], 1.0, 2.0, [0.0, 1.0, 0.0], 0.0, 0, 1);
3325 assert!(contact.is_some(), "Cone base should penetrate ground plane");
3326 let c = contact.unwrap();
3327 assert!(c.depth > 0.0);
3328 }
3329
3330 #[test]
3331 fn cone_plane_contact_floating() {
3332 let contact = cone_plane_contact([0.0, 5.0, 0.0], 0.5, 1.0, [0.0, 1.0, 0.0], 0.0, 0, 1);
3333 assert!(contact.is_none(), "Cone above plane should not overlap");
3334 }
3335
3336 #[test]
3339 fn raycast_capsule_side() {
3340 let mut world = PhysicsWorld::new();
3341 world.add_body(
3342 RigidBody::dynamic(
3343 1.0,
3344 CollisionShape::Capsule {
3345 radius: 0.5,
3346 half_height: 1.0,
3347 },
3348 )
3349 .with_position([0.0, 0.0, -5.0]),
3350 );
3351 let hit = world.raycast([0.0, 0.0, 0.0], [0.0, 0.0, -1.0], 100.0);
3352 assert!(hit.is_some(), "Ray should hit capsule");
3353 let h = hit.unwrap();
3354 assert!(
3355 (h.distance - 4.5).abs() < 0.01,
3356 "Distance should be ~4.5, got {}",
3357 h.distance
3358 );
3359 }
3360
3361 #[test]
3362 fn raycast_capsule_misses() {
3363 let mut world = PhysicsWorld::new();
3364 world.add_body(
3365 RigidBody::dynamic(
3366 1.0,
3367 CollisionShape::Capsule {
3368 radius: 0.5,
3369 half_height: 1.0,
3370 },
3371 )
3372 .with_position([10.0, 0.0, 0.0]),
3373 );
3374 let hit = world.raycast([0.0, 0.0, 0.0], [0.0, 0.0, -1.0], 100.0);
3375 assert!(hit.is_none(), "Ray should miss capsule");
3376 }
3377
3378 #[test]
3379 fn raycast_cylinder_side() {
3380 let mut world = PhysicsWorld::new();
3381 world.add_body(
3382 RigidBody::dynamic(
3383 1.0,
3384 CollisionShape::Cylinder {
3385 radius: 0.5,
3386 half_height: 1.0,
3387 },
3388 )
3389 .with_position([0.0, 0.0, -5.0]),
3390 );
3391 let hit = world.raycast([0.0, 0.0, 0.0], [0.0, 0.0, -1.0], 100.0);
3392 assert!(hit.is_some(), "Ray should hit cylinder barrel");
3393 let h = hit.unwrap();
3394 assert!(
3395 (h.distance - 4.5).abs() < 0.01,
3396 "Distance should be ~4.5, got {}",
3397 h.distance
3398 );
3399 }
3400
3401 #[test]
3402 fn raycast_cylinder_cap() {
3403 let mut world = PhysicsWorld::new();
3404 world.add_body(
3405 RigidBody::dynamic(
3406 1.0,
3407 CollisionShape::Cylinder {
3408 radius: 1.0,
3409 half_height: 0.5,
3410 },
3411 )
3412 .with_position([0.0, -3.0, 0.0]),
3413 );
3414 let hit = world.raycast([0.0, 0.0, 0.0], [0.0, -1.0, 0.0], 100.0);
3416 assert!(hit.is_some(), "Ray should hit cylinder top cap");
3417 let h = hit.unwrap();
3418 assert!(
3419 (h.distance - 2.5).abs() < 0.01,
3420 "Distance to top cap should be ~2.5, got {}",
3421 h.distance
3422 );
3423 }
3424
3425 #[test]
3426 fn raycast_cone_side() {
3427 let mut world = PhysicsWorld::new();
3428 world.add_body(
3430 RigidBody::dynamic(
3431 1.0,
3432 CollisionShape::Cone {
3433 radius: 1.0,
3434 height: 2.0,
3435 },
3436 )
3437 .with_position([0.0, -1.0, -5.0]),
3438 );
3439 let hit = world.raycast([0.0, 0.0, 0.0], [0.0, 0.0, -1.0], 100.0);
3441 assert!(hit.is_some(), "Ray should hit cone side");
3442 }
3443
3444 #[test]
3445 fn raycast_cone_base() {
3446 let mut world = PhysicsWorld::new();
3447 world.add_body(
3449 RigidBody::dynamic(
3450 1.0,
3451 CollisionShape::Cone {
3452 radius: 2.0,
3453 height: 3.0,
3454 },
3455 )
3456 .with_position([0.0, -5.0, 0.0]),
3457 );
3458 let hit = world.raycast([0.0, 0.0, 0.0], [0.0, -1.0, 0.0], 100.0);
3459 assert!(hit.is_some(), "Ray should hit cone base");
3460 }
3461
3462 #[test]
3465 fn spatial_hash_grid_finds_nearby_pair() {
3466 let bodies = vec![
3467 RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 0.0, 0.0]),
3468 RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.5, 0.0, 0.0]),
3469 ];
3470 let mut grid = SpatialHashGrid::new(2.0);
3471 grid.populate(&bodies);
3472 let pairs = grid.query_pairs(&bodies);
3473 assert!(pairs.contains(&(0, 1)), "Nearby bodies should form a pair");
3474 }
3475
3476 #[test]
3477 fn spatial_hash_grid_distant_no_pair() {
3478 let bodies = vec![
3479 RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 0.0, 0.0]),
3480 RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([100.0, 100.0, 100.0]),
3481 ];
3482 let mut grid = SpatialHashGrid::new(2.0);
3483 grid.populate(&bodies);
3484 let pairs = grid.query_pairs(&bodies);
3485 assert!(pairs.is_empty(), "Distant bodies should not form a pair");
3486 }
3487
3488 #[test]
3489 fn spatial_hash_grid_cross_cell_boundary() {
3490 let bodies = vec![
3492 RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([1.9, 0.0, 0.0]),
3493 RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([2.1, 0.0, 0.0]),
3494 ];
3495 let mut grid = SpatialHashGrid::new(2.0);
3496 grid.populate(&bodies);
3497 let pairs = grid.query_pairs(&bodies);
3498 assert!(
3499 pairs.contains(&(0, 1)),
3500 "Bodies across cell boundary should form a pair"
3501 );
3502 }
3503
3504 #[test]
3505 fn spatial_hash_grid_many_bodies() {
3506 let bodies: Vec<RigidBody> = (0..100)
3508 .map(|i| {
3509 RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.3 }).with_position([
3510 i as f32 * 0.5,
3511 0.0,
3512 0.0,
3513 ])
3514 })
3515 .collect();
3516 let mut grid = SpatialHashGrid::new(2.0);
3517 grid.populate(&bodies);
3518 let pairs = grid.query_pairs(&bodies);
3519 assert!(
3521 pairs.len() > 50,
3522 "Should have many pairs for closely-spaced bodies, got {}",
3523 pairs.len()
3524 );
3525 }
3526
3527 #[test]
3530 fn body_falls_asleep_below_threshold() {
3531 let mut world = PhysicsWorld::new();
3532 world.gravity = [0.0, 0.0, 0.0]; world.sleep_config = SleepConfig {
3534 linear_threshold: 0.01,
3535 angular_threshold: 0.01,
3536 frames_to_sleep: 5,
3537 };
3538 let id = world
3539 .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 0.0, 0.0]));
3540 for _ in 0..10 {
3542 world.step(1.0 / 60.0);
3543 }
3544 let body = world.body(id).unwrap();
3545 assert!(
3546 body.is_sleeping,
3547 "Body with zero velocity should be sleeping after enough frames"
3548 );
3549 assert!(body.sleep_frames >= 5);
3550 }
3551
3552 #[test]
3553 fn moving_body_does_not_sleep() {
3554 let mut world = PhysicsWorld::new();
3555 world.gravity = [0.0, 0.0, 0.0];
3556 world.sleep_config = SleepConfig {
3557 linear_threshold: 0.01,
3558 angular_threshold: 0.01,
3559 frames_to_sleep: 5,
3560 };
3561 let id = world
3562 .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 0.0, 0.0]));
3563 world.body_mut(id).unwrap().velocity = [1.0, 0.0, 0.0];
3564 for _ in 0..10 {
3565 world.step(1.0 / 60.0);
3566 }
3567 let body = world.body(id).unwrap();
3568 assert!(!body.is_sleeping, "Body with velocity > threshold should not sleep");
3569 }
3570
3571 #[test]
3572 fn sleeping_body_wakes_on_contact() {
3573 let mut world = PhysicsWorld::new();
3574 world.gravity = [0.0, 0.0, 0.0];
3575 world.sleep_config = SleepConfig {
3576 linear_threshold: 0.1,
3577 angular_threshold: 0.1,
3578 frames_to_sleep: 3,
3579 };
3580 let a = world
3582 .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 0.0, 0.0]));
3583 let b = world
3585 .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([2.0, 0.0, 0.0]));
3586
3587 {
3589 let body_a = world.body_mut(a).unwrap();
3590 body_a.is_sleeping = true;
3591 body_a.sleep_frames = 100;
3592 }
3593
3594 world.body_mut(b).unwrap().velocity = [-10.0, 0.0, 0.0];
3596
3597 for _ in 0..20 {
3599 world.step(1.0 / 60.0);
3600 }
3601 let body_a = world.body(a).unwrap();
3602 assert!(!body_a.is_sleeping, "Sleeping body should wake on contact");
3604 }
3605
3606 #[test]
3607 fn sleeping_body_skips_integration() {
3608 let mut world = PhysicsWorld::new();
3609 world.gravity = [0.0, -9.81, 0.0];
3610 world.sleep_config = SleepConfig {
3611 linear_threshold: 0.01,
3612 angular_threshold: 0.01,
3613 frames_to_sleep: 3,
3614 };
3615 let id = world
3616 .add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.0, 5.0, 0.0]));
3617 {
3619 let body = world.body_mut(id).unwrap();
3620 body.is_sleeping = true;
3621 body.sleep_frames = 100;
3622 }
3623 let pos_before = world.body(id).unwrap().position;
3624 world.step(1.0 / 60.0);
3625 let pos_after = world.body(id).unwrap().position;
3626 assert_eq!(pos_before, pos_after, "Sleeping body should not be integrated");
3627 }
3628
3629 #[test]
3632 fn detect_contact_capsule_sphere_via_world() {
3633 let mut world = PhysicsWorld::new();
3634 world.gravity = [0.0, 0.0, 0.0];
3635 world.add_body(
3636 RigidBody::dynamic(
3637 1.0,
3638 CollisionShape::Capsule {
3639 radius: 0.5,
3640 half_height: 1.0,
3641 },
3642 )
3643 .with_position([0.0, 0.0, 0.0]),
3644 );
3645 world.add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.8, 0.0, 0.0]));
3646 world.step(1.0 / 60.0);
3647 assert!(
3648 !world.contacts().is_empty(),
3649 "World should detect capsule-sphere contact"
3650 );
3651 }
3652
3653 #[test]
3654 fn detect_contact_cylinder_sphere_via_world() {
3655 let mut world = PhysicsWorld::new();
3656 world.gravity = [0.0, 0.0, 0.0];
3657 world.add_body(
3658 RigidBody::dynamic(
3659 1.0,
3660 CollisionShape::Cylinder {
3661 radius: 0.5,
3662 half_height: 1.0,
3663 },
3664 )
3665 .with_position([0.0, 0.0, 0.0]),
3666 );
3667 world.add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([0.8, 0.0, 0.0]));
3668 world.step(1.0 / 60.0);
3669 assert!(
3670 !world.contacts().is_empty(),
3671 "World should detect cylinder-sphere contact"
3672 );
3673 }
3674
3675 #[test]
3676 fn detect_contact_cone_sphere_via_world() {
3677 let mut world = PhysicsWorld::new();
3678 world.gravity = [0.0, 0.0, 0.0];
3679 world.add_body(
3680 RigidBody::dynamic(
3681 1.0,
3682 CollisionShape::Cone {
3683 radius: 1.0,
3684 height: 2.0,
3685 },
3686 )
3687 .with_position([0.0, 0.0, 0.0]),
3688 );
3689 world.add_body(RigidBody::dynamic(1.0, CollisionShape::Sphere { radius: 0.5 }).with_position([1.2, 0.0, 0.0]));
3690 world.step(1.0 / 60.0);
3691 assert!(!world.contacts().is_empty(), "World should detect cone-sphere contact");
3692 }
3693}