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 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 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#[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 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 pub fn active_pairs_snapshot(&self) -> Vec<(ComponentId, ComponentId)> {
108 self.active_pairs.iter().copied().collect()
109 }
110
111 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 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 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 }
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 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 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 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(¤t_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 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 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 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 }
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 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 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 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 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 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}