Skip to main content

mittens_engine/engine/ecs/system/
collision_system.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::EventSignal;
3use crate::engine::ecs::RxWorld;
4use crate::engine::ecs::World;
5use crate::engine::ecs::component::{
6    CollisionComponent, CollisionShapeComponent, RenderableComponent,
7};
8use crate::engine::ecs::system::System;
9use crate::engine::ecs::system::TransformSystem;
10use crate::engine::graphics::VisualWorld;
11use crate::engine::user_input::InputState;
12use bvh::Point3;
13use bvh::aabb::{AABB, Bounded};
14use bvh::bounding_hierarchy::BHShape;
15use bvh::bvh::{BVH, BVHNode};
16use slotmap::Key;
17use slotmap::{SlotMap, new_key_type};
18use std::collections::{HashMap, HashSet};
19use std::sync::mpsc;
20use std::thread;
21
22pub type CollisionShape = crate::engine::ecs::system::model::collision_types::CollisionShape;
23pub type CollisionMode = crate::engine::ecs::system::model::collision_types::CollisionMode;
24
25new_key_type! {
26    pub struct StaticCollisionKey;
27    pub struct KinematicCollisionKey;
28    pub struct RiggedCollisionKey;
29}
30
31#[derive(Debug, Clone, Copy)]
32pub enum CollisionHandle {
33    Static(StaticCollisionKey),
34    Kinematic(KinematicCollisionKey),
35    Rigged(RiggedCollisionKey),
36}
37
38#[derive(Debug, Clone)]
39pub enum CollisionMessage {
40    // to worker
41    Tick,
42    AddObject {
43        component: ComponentId,
44        guid: uuid::Uuid,
45        mode: CollisionMode,
46        shape: CollisionShape,
47        position_world: [f32; 3],
48    },
49    RemoveObject {
50        component: ComponentId,
51    },
52    UpdateObject {
53        component: ComponentId,
54        guid: uuid::Uuid,
55        mode: CollisionMode,
56        shape: CollisionShape,
57        position_world: [f32; 3],
58    },
59    Shutdown,
60
61    // from worker
62    CollisionDetected {
63        a_component: ComponentId,
64        a_guid: uuid::Uuid,
65        a_mode: CollisionMode,
66        b_component: ComponentId,
67        b_guid: uuid::Uuid,
68        b_mode: CollisionMode,
69    },
70}
71
72/// Placeholder collision object record.
73///
74/// This will likely evolve into a more event-driven structure later (e.g. pairs,
75/// contact manifolds, triggers).
76#[derive(Debug, Clone)]
77pub struct CollisionObject {
78    pub component: ComponentId,
79    pub guid: uuid::Uuid,
80    pub mode: CollisionMode,
81    pub shape: CollisionShape,
82
83    /// Cached world-space position (translation).
84    pub position_world: [f32; 3],
85}
86
87#[derive(Debug, Default)]
88pub struct CollisionSystem {
89    to_worker: Option<mpsc::Sender<CollisionMessage>>,
90    from_worker: Option<mpsc::Receiver<CollisionMessage>>,
91    worker: Option<thread::JoinHandle<()>>,
92
93    known: HashSet<ComponentId>,
94
95    active_pairs: HashSet<(ComponentId, ComponentId)>,
96    active_pair_deltas: HashMap<(ComponentId, ComponentId), [f32; 3]>,
97}
98
99impl CollisionSystem {
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Snapshot of the currently-active overlap pairs (normalized ordering).
105    ///
106    /// Note: this is updated when `tick_with_rx` drains worker messages.
107    pub fn active_pairs_snapshot(&self) -> Vec<(ComponentId, ComponentId)> {
108        self.active_pairs.iter().copied().collect()
109    }
110
111    /// Snapshot of active overlap pairs with `delta = pos(b) - pos(a)` in world space.
112    pub fn active_pairs_with_delta_snapshot(&self) -> Vec<(ComponentId, ComponentId, [f32; 3])> {
113        self.active_pairs
114            .iter()
115            .copied()
116            .map(|(a, b)| {
117                let delta = self
118                    .active_pair_deltas
119                    .get(&(a, b))
120                    .copied()
121                    .unwrap_or([0.0, 0.0, 0.0]);
122                (a, b, delta)
123            })
124            .collect()
125    }
126
127    pub fn register_collision(
128        &mut self,
129        world: &mut World,
130        _visuals: &mut VisualWorld,
131        component: ComponentId,
132    ) {
133        self.ensure_worker();
134        self.upsert_component(world, component);
135    }
136
137    /// Update a collision object when its parent transform changes.
138    ///
139    /// Intended to be called by TransformSystem when `transform_component` has `component`
140    /// as a direct child.
141    pub fn update_from_transform(
142        &mut self,
143        world: &mut World,
144        component: ComponentId,
145        transform_component: ComponentId,
146    ) {
147        self.ensure_worker();
148
149        let position_world = match world
150            .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(
151                transform_component,
152            )
153            .map(|t| t.transform.matrix_world)
154        {
155            Some(m) => {
156                let p = m[3];
157                [p[0], p[1], p[2]]
158            }
159            None => TransformSystem::world_position(world, component).unwrap_or([0.0, 0.0, 0.0]),
160        };
161
162        self.upsert_component_with_position(world, component, position_world);
163    }
164
165    pub fn remove_collision(
166        &mut self,
167        _world: &mut World,
168        _visuals: &mut VisualWorld,
169        component: ComponentId,
170    ) {
171        self.ensure_worker();
172        if let Some(tx) = self.to_worker.as_ref() {
173            let _ = tx.send(CollisionMessage::RemoveObject { component });
174        }
175        self.known.remove(&component);
176    }
177
178    fn upsert_component(&mut self, world: &mut World, component: ComponentId) {
179        let position_world =
180            TransformSystem::world_position(world, component).unwrap_or([0.0, 0.0, 0.0]);
181        self.upsert_component_with_position(world, component, position_world);
182    }
183
184    fn upsert_component_with_position(
185        &mut self,
186        world: &mut World,
187        component: ComponentId,
188        position_world: [f32; 3],
189    ) {
190        // Semantics: a CollisionComponent only has behavior when it is a direct child of a
191        // TransformComponent. Otherwise, it should not participate in collision at all.
192        let has_transform_parent = world
193            .parent_of(component)
194            .and_then(|p| {
195                world
196                    .get_component_by_id_as::<crate::engine::ecs::component::TransformComponent>(p)
197                    .map(|_| p)
198            })
199            .is_some();
200
201        if !has_transform_parent {
202            if self.known.remove(&component) {
203                if let Some(tx) = self.to_worker.as_ref() {
204                    let _ = tx.send(CollisionMessage::RemoveObject { component });
205                }
206            }
207            return;
208        }
209
210        let Some(collision_comp) = world.get_component_by_id_as::<CollisionComponent>(component)
211        else {
212            return;
213        };
214
215        let Some(tx) = self.to_worker.as_ref() else {
216            return;
217        };
218
219        let guid = match world.get_component_record(component) {
220            Some(node) => node.guid,
221            None => return,
222        };
223
224        let mode = collision_comp.mode;
225
226        let shape = resolve_shape(world, component).unwrap_or_else(|| {
227            crate::engine::ecs::system::model::collision_types::CollisionShape::CUBE()
228        });
229
230        let msg = if self.known.contains(&component) {
231            CollisionMessage::UpdateObject {
232                component,
233                guid,
234                mode,
235                shape,
236                position_world,
237            }
238        } else {
239            CollisionMessage::AddObject {
240                component,
241                guid,
242                mode,
243                shape,
244                position_world,
245            }
246        };
247
248        let _ = tx.send(msg);
249        self.known.insert(component);
250    }
251
252    fn ensure_worker(&mut self) {
253        if self.to_worker.is_some() {
254            return;
255        }
256
257        let (to_worker_tx, to_worker_rx) = mpsc::channel::<CollisionMessage>();
258        let (from_worker_tx, from_worker_rx) = mpsc::channel::<CollisionMessage>();
259
260        let handle = thread::Builder::new()
261            .name("CollisionSystemWorker".to_string())
262            .spawn(move || collision_worker_loop(to_worker_rx, from_worker_tx))
263            .expect("failed to spawn CollisionSystemWorker thread");
264
265        self.to_worker = Some(to_worker_tx);
266        self.from_worker = Some(from_worker_rx);
267        self.worker = Some(handle);
268    }
269}
270
271impl Drop for CollisionSystem {
272    fn drop(&mut self) {
273        if let Some(tx) = self.to_worker.take() {
274            let _ = tx.send(CollisionMessage::Shutdown);
275        }
276        if let Some(h) = self.worker.take() {
277            let _ = h.join();
278        }
279    }
280}
281
282impl System for CollisionSystem {
283    fn tick(
284        &mut self,
285        _world: &mut World,
286        _visuals: &mut VisualWorld,
287        _input: &InputState,
288        _dt_sec: f32,
289    ) {
290        // Driven via SystemWorld::tick_with_rx.
291    }
292}
293
294impl CollisionSystem {
295    pub fn tick_with_rx(
296        &mut self,
297        world: &mut World,
298        _visuals: &mut VisualWorld,
299        _input: &InputState,
300        _dt_sec: f32,
301        rx: &mut RxWorld,
302    ) {
303        self.ensure_worker();
304
305        let Some(tx) = self.to_worker.as_ref() else {
306            return;
307        };
308
309        // Drain worker -> current overlap set.
310        let mut current_pairs: HashSet<(ComponentId, ComponentId)> = HashSet::new();
311        let mut current_deltas: HashMap<(ComponentId, ComponentId), [f32; 3]> = HashMap::new();
312        if let Some(from_worker) = self.from_worker.as_ref() {
313            while let Ok(msg) = from_worker.try_recv() {
314                let CollisionMessage::CollisionDetected {
315                    a_component,
316                    a_guid: _,
317                    a_mode: _,
318                    b_component,
319                    b_guid: _,
320                    b_mode: _,
321                } = msg
322                else {
323                    continue;
324                };
325
326                // Normalize ordering so (a,b) and (b,a) map to the same pair.
327                let a_key = a_component.data().as_ffi();
328                let b_key = b_component.data().as_ffi();
329                let (lo, hi) = if a_key <= b_key {
330                    (a_component, b_component)
331                } else {
332                    (b_component, a_component)
333                };
334                if lo != hi {
335                    if current_pairs.insert((lo, hi)) {
336                        let a_pos =
337                            TransformSystem::world_position(world, lo).unwrap_or([0.0, 0.0, 0.0]);
338                        let b_pos =
339                            TransformSystem::world_position(world, hi).unwrap_or([0.0, 0.0, 0.0]);
340                        current_deltas.insert(
341                            (lo, hi),
342                            [
343                                b_pos[0] - a_pos[0],
344                                b_pos[1] - a_pos[1],
345                                b_pos[2] - a_pos[2],
346                            ],
347                        );
348                    }
349                }
350            }
351        }
352
353        // Emit started/ended based on set diffs.
354        for &(a, b) in current_pairs.difference(&self.active_pairs) {
355            let delta = current_deltas
356                .get(&(a, b))
357                .copied()
358                .unwrap_or([0.0, 0.0, 0.0]);
359            rx.push_event(a, EventSignal::CollisionStarted { a, b, delta });
360            rx.push_event(b, EventSignal::CollisionStarted { a, b, delta });
361        }
362
363        for &(a, b) in self.active_pairs.difference(&current_pairs) {
364            let delta = self
365                .active_pair_deltas
366                .get(&(a, b))
367                .copied()
368                .unwrap_or([0.0, 0.0, 0.0]);
369            rx.push_event(a, EventSignal::CollisionEnded { a, b, delta });
370            rx.push_event(b, EventSignal::CollisionEnded { a, b, delta });
371        }
372
373        self.active_pairs = current_pairs;
374        self.active_pair_deltas = current_deltas;
375
376        let _ = tx.send(CollisionMessage::Tick);
377    }
378}
379
380fn resolve_shape(world: &World, collision_cid: ComponentId) -> Option<CollisionShape> {
381    // 1) Child CollisionShapeComponent.
382    for child in world.children_of(collision_cid) {
383        if let Some(s) = world.get_component_by_id_as::<CollisionShapeComponent>(*child) {
384            return Some(s.shape);
385        }
386    }
387
388    // 2) Sibling RenderableComponent with built-in mesh handles (cube only for now).
389    let parent = world.parent_of(collision_cid)?;
390    for sib in world.children_of(parent) {
391        if *sib == collision_cid {
392            continue;
393        }
394        let Some(r) = world.get_component_by_id_as::<RenderableComponent>(*sib) else {
395            continue;
396        };
397
398        // Built-in mesh handles (stable ids).
399        if r.renderable.base_mesh == crate::engine::graphics::primitives::CpuMeshHandle::CUBE {
400            return Some(CollisionShape::CUBE());
401        }
402
403        if r.renderable.base_mesh == crate::engine::graphics::primitives::CpuMeshHandle::SPHERE {
404            return Some(CollisionShape::SPHERE());
405        }
406    }
407
408    None
409}
410
411#[derive(Debug, Clone)]
412struct StoredObject {
413    component: ComponentId,
414    guid: uuid::Uuid,
415    mode: CollisionMode,
416    shape: CollisionShape,
417    position_world: [f32; 3],
418}
419
420struct WorkerState {
421    static_objects: SlotMap<StaticCollisionKey, StoredObject>,
422    kinematic_objects: SlotMap<KinematicCollisionKey, StoredObject>,
423    rigged_objects: SlotMap<RiggedCollisionKey, StoredObject>,
424
425    by_component: HashMap<ComponentId, CollisionHandle>,
426}
427
428impl Default for WorkerState {
429    fn default() -> Self {
430        Self {
431            static_objects: SlotMap::with_key(),
432            kinematic_objects: SlotMap::with_key(),
433            rigged_objects: SlotMap::with_key(),
434            by_component: HashMap::new(),
435        }
436    }
437}
438
439fn collision_worker_loop(rx: mpsc::Receiver<CollisionMessage>, tx: mpsc::Sender<CollisionMessage>) {
440    let mut state = WorkerState::default();
441    while let Ok(msg) = rx.recv() {
442        match msg {
443            CollisionMessage::Shutdown => break,
444            CollisionMessage::AddObject {
445                component,
446                guid,
447                mode,
448                shape,
449                position_world,
450            } => {
451                worker_upsert(&mut state, component, guid, mode, shape, position_world);
452            }
453            CollisionMessage::UpdateObject {
454                component,
455                guid,
456                mode,
457                shape,
458                position_world,
459            } => {
460                worker_upsert(&mut state, component, guid, mode, shape, position_world);
461            }
462            CollisionMessage::RemoveObject { component } => {
463                worker_remove(&mut state, component);
464            }
465            CollisionMessage::Tick => {
466                worker_tick(&state, &tx);
467            }
468            CollisionMessage::CollisionDetected { .. } => {
469                // main->worker never sends this
470            }
471        }
472    }
473}
474
475fn worker_remove(state: &mut WorkerState, component: ComponentId) {
476    let Some(handle) = state.by_component.remove(&component) else {
477        return;
478    };
479
480    match handle {
481        CollisionHandle::Static(k) => {
482            let _ = state.static_objects.remove(k);
483        }
484        CollisionHandle::Kinematic(k) => {
485            let _ = state.kinematic_objects.remove(k);
486        }
487        CollisionHandle::Rigged(k) => {
488            let _ = state.rigged_objects.remove(k);
489        }
490    }
491}
492
493fn worker_upsert(
494    state: &mut WorkerState,
495    component: ComponentId,
496    guid: uuid::Uuid,
497    mode: CollisionMode,
498    shape: CollisionShape,
499    position_world: [f32; 3],
500) {
501    // If mode changed, remove from old store.
502    if let Some(existing) = state.by_component.get(&component).copied() {
503        let existing_mode = match existing {
504            CollisionHandle::Static(_) => CollisionMode::Static,
505            CollisionHandle::Kinematic(_) => CollisionMode::Kinematic,
506            CollisionHandle::Rigged(_) => CollisionMode::Rigged,
507        };
508        if existing_mode != mode {
509            worker_remove(state, component);
510        }
511    }
512
513    let obj = StoredObject {
514        component,
515        guid,
516        mode,
517        shape,
518        position_world,
519    };
520
521    match state.by_component.get(&component).copied() {
522        Some(CollisionHandle::Static(k)) => {
523            if let Some(stored) = state.static_objects.get_mut(k) {
524                *stored = obj;
525            }
526        }
527        Some(CollisionHandle::Kinematic(k)) => {
528            if let Some(stored) = state.kinematic_objects.get_mut(k) {
529                *stored = obj;
530            }
531        }
532        Some(CollisionHandle::Rigged(k)) => {
533            if let Some(stored) = state.rigged_objects.get_mut(k) {
534                *stored = obj;
535            }
536        }
537        None => {
538            let handle = match mode {
539                CollisionMode::Static => {
540                    let k = state.static_objects.insert(obj);
541                    CollisionHandle::Static(k)
542                }
543                CollisionMode::Kinematic => {
544                    let k = state.kinematic_objects.insert(obj);
545                    CollisionHandle::Kinematic(k)
546                }
547                CollisionMode::Rigged => {
548                    let k = state.rigged_objects.insert(obj);
549                    CollisionHandle::Rigged(k)
550                }
551            };
552            state.by_component.insert(component, handle);
553        }
554    }
555}
556
557fn worker_tick(state: &WorkerState, tx: &mpsc::Sender<CollisionMessage>) {
558    let mut all: Vec<&StoredObject> = Vec::new();
559    all.extend(state.static_objects.values());
560    all.extend(state.kinematic_objects.values());
561    all.extend(state.rigged_objects.values());
562
563    if all.len() < 2 {
564        return;
565    }
566
567    // Broadphase: build a BVH over world-space AABBs for the collision objects.
568    // This reduces the candidate set for narrowphase `intersects()`.
569    let mut shapes: Vec<CollisionAabbShape> = all
570        .iter()
571        .enumerate()
572        .filter_map(|(index, obj)| {
573            let (min, max) = world_aabb_for_collision_object(obj);
574            Some(CollisionAabbShape::new(index, min, max))
575        })
576        .collect();
577
578    // If any shapes failed to produce AABBs, fall back to brute force.
579    if shapes.len() != all.len() {
580        for i in 0..all.len() {
581            for j in (i + 1)..all.len() {
582                let a = all[i];
583                let b = all[j];
584
585                if a.mode == CollisionMode::Static && b.mode == CollisionMode::Static {
586                    continue;
587                }
588
589                if intersects(a, b) {
590                    let _ = tx.send(CollisionMessage::CollisionDetected {
591                        a_component: a.component,
592                        a_guid: a.guid,
593                        a_mode: a.mode,
594                        b_component: b.component,
595                        b_guid: b.guid,
596                        b_mode: b.mode,
597                    });
598                }
599            }
600        }
601        return;
602    }
603
604    let bvh = BVH::build(&mut shapes);
605
606    // Only query from non-static objects.
607    // Static-static collisions are ignored, and static objects don't need to initiate queries.
608    for i in 0..all.len() {
609        let a = all[i];
610        if a.mode == CollisionMode::Static {
611            continue;
612        }
613
614        let query = shapes[i].aabb;
615        let candidates = bvh_query_aabb_indices(&bvh, &shapes, &query);
616
617        for j in candidates {
618            if j == i || j >= all.len() {
619                continue;
620            }
621
622            let b = all[j];
623
624            // Avoid double-reporting dynamic-dynamic pairs, but always test dynamic-static.
625            if b.mode != CollisionMode::Static && j <= i {
626                continue;
627            }
628
629            if intersects(a, b) {
630                let _ = tx.send(CollisionMessage::CollisionDetected {
631                    a_component: a.component,
632                    a_guid: a.guid,
633                    a_mode: a.mode,
634                    b_component: b.component,
635                    b_guid: b.guid,
636                    b_mode: b.mode,
637                });
638            }
639        }
640    }
641}
642
643#[derive(Debug, Clone)]
644struct CollisionAabbShape {
645    index: usize,
646    aabb: AABB,
647    node_index: usize,
648}
649
650impl CollisionAabbShape {
651    fn new(index: usize, min: [f32; 3], max: [f32; 3]) -> Self {
652        Self {
653            index,
654            aabb: AABB::with_bounds(
655                Point3::new(min[0], min[1], min[2]),
656                Point3::new(max[0], max[1], max[2]),
657            ),
658            node_index: 0,
659        }
660    }
661}
662
663impl Bounded for CollisionAabbShape {
664    fn aabb(&self) -> AABB {
665        self.aabb
666    }
667}
668
669impl BHShape for CollisionAabbShape {
670    fn set_bh_node_index(&mut self, index: usize) {
671        self.node_index = index;
672    }
673
674    fn bh_node_index(&self) -> usize {
675        self.node_index
676    }
677}
678
679fn bvh_query_aabb_indices(bvh: &BVH, shapes: &[CollisionAabbShape], query: &AABB) -> Vec<usize> {
680    if bvh.nodes.is_empty() {
681        return Vec::new();
682    }
683
684    let mut out = Vec::new();
685    let mut stack = vec![0usize];
686    while let Some(node_index) = stack.pop() {
687        match bvh.nodes[node_index] {
688            BVHNode::Node {
689                child_l_index,
690                child_l_aabb,
691                child_r_index,
692                child_r_aabb,
693                ..
694            } => {
695                if aabb_overlap_bvh(query, &child_l_aabb) {
696                    stack.push(child_l_index);
697                }
698                if aabb_overlap_bvh(query, &child_r_aabb) {
699                    stack.push(child_r_index);
700                }
701            }
702            BVHNode::Leaf { shape_index, .. } => {
703                if let Some(s) = shapes.get(shape_index) {
704                    if aabb_overlap_bvh(query, &s.aabb) {
705                        out.push(s.index);
706                    }
707                }
708            }
709        }
710    }
711
712    out
713}
714
715fn aabb_overlap_bvh(a: &AABB, b: &AABB) -> bool {
716    !(a.max.x < b.min.x
717        || a.min.x > b.max.x
718        || a.max.y < b.min.y
719        || a.min.y > b.max.y
720        || a.max.z < b.min.z
721        || a.min.z > b.max.z)
722}
723
724fn world_aabb_for_collision_object(obj: &StoredObject) -> ([f32; 3], [f32; 3]) {
725    match obj.shape {
726        CollisionShape::Sphere { radius } => (
727            [
728                obj.position_world[0] - radius,
729                obj.position_world[1] - radius,
730                obj.position_world[2] - radius,
731            ],
732            [
733                obj.position_world[0] + radius,
734                obj.position_world[1] + radius,
735                obj.position_world[2] + radius,
736            ],
737        ),
738        CollisionShape::Cube { half_extents } => world_aabb_cube(obj.position_world, half_extents),
739    }
740}
741
742fn intersects(a: &StoredObject, b: &StoredObject) -> bool {
743    match (a.shape, b.shape) {
744        (CollisionShape::Sphere { radius: ra }, CollisionShape::Sphere { radius: rb }) => {
745            let dx = a.position_world[0] - b.position_world[0];
746            let dy = a.position_world[1] - b.position_world[1];
747            let dz = a.position_world[2] - b.position_world[2];
748            let r = ra + rb;
749            dx * dx + dy * dy + dz * dz <= r * r
750        }
751        (CollisionShape::Cube { half_extents: ea }, CollisionShape::Cube { half_extents: eb }) => {
752            aabb_overlap(
753                world_aabb_cube(a.position_world, ea),
754                world_aabb_cube(b.position_world, eb),
755            )
756        }
757        (CollisionShape::Cube { half_extents }, CollisionShape::Sphere { radius })
758        | (CollisionShape::Sphere { radius }, CollisionShape::Cube { half_extents }) => {
759            let (cube_center, sphere_center) = if matches!(a.shape, CollisionShape::Cube { .. }) {
760                (a.position_world, b.position_world)
761            } else {
762                (b.position_world, a.position_world)
763            };
764            cube_sphere_intersect(cube_center, half_extents, sphere_center, radius)
765        }
766    }
767}
768
769fn world_aabb_cube(center: [f32; 3], half_extents: [f32; 3]) -> ([f32; 3], [f32; 3]) {
770    let min = [
771        center[0] - half_extents[0],
772        center[1] - half_extents[1],
773        center[2] - half_extents[2],
774    ];
775    let max = [
776        center[0] + half_extents[0],
777        center[1] + half_extents[1],
778        center[2] + half_extents[2],
779    ];
780    (min, max)
781}
782
783fn aabb_overlap(a: ([f32; 3], [f32; 3]), b: ([f32; 3], [f32; 3])) -> bool {
784    let (amin, amax) = a;
785    let (bmin, bmax) = b;
786    !(amax[0] < bmin[0]
787        || amin[0] > bmax[0]
788        || amax[1] < bmin[1]
789        || amin[1] > bmax[1]
790        || amax[2] < bmin[2]
791        || amin[2] > bmax[2])
792}
793
794fn cube_sphere_intersect(
795    cube_center: [f32; 3],
796    half_extents: [f32; 3],
797    sphere_center: [f32; 3],
798    radius: f32,
799) -> bool {
800    let (min, max) = world_aabb_cube(cube_center, half_extents);
801
802    let cx = sphere_center[0].clamp(min[0], max[0]);
803    let cy = sphere_center[1].clamp(min[1], max[1]);
804    let cz = sphere_center[2].clamp(min[2], max[2]);
805
806    let dx = sphere_center[0] - cx;
807    let dy = sphere_center[1] - cy;
808    let dz = sphere_center[2] - cz;
809    dx * dx + dy * dy + dz * dz <= radius * radius
810}