euv_engine/physics/impl.rs
1use super::*;
2
3/// Implements `Default` for `BodyCollider`, returning an AABB collider with default values.
4impl Default for BodyCollider {
5 /// Constructs a default [`BodyCollider`] value.
6 ///
7 /// # Returns
8 ///
9 /// - `BodyCollider` - A default-constructed instance with the documented initial state.
10 fn default() -> BodyCollider {
11 BodyCollider::Aabb(AabbCollider::default())
12 }
13}
14
15/// Implements `Default` for `BodyCollider3D`, returning a 3D AABB collider with default values.
16impl Default for BodyCollider3D {
17 /// Constructs a default [`BodyCollider3D`] value.
18 ///
19 /// # Returns
20 ///
21 /// - `BodyCollider3D` - A default-constructed instance with the documented initial state.
22 fn default() -> BodyCollider3D {
23 BodyCollider3D::Aabb(AabbCollider3D::default())
24 }
25}
26
27/// Implements default configuration for `PhysicsConfig`.
28impl Default for PhysicsConfig {
29 /// Constructs a default [`PhysicsConfig`] value.
30 ///
31 /// # Returns
32 ///
33 /// - `PhysicsConfig` - A default-constructed instance with the documented initial state.
34 fn default() -> PhysicsConfig {
35 PhysicsConfig::new(
36 Vector2D::new(0.0, DEFAULT_GRAVITY),
37 DEFAULT_LINEAR_DAMPING,
38 DEFAULT_ANGULAR_DAMPING,
39 )
40 }
41}
42
43/// Implements body creation and force management for `RigidBody2D`.
44impl RigidBody2D {
45 /// Creates a new dynamic rigid body with default mass and the given position.
46 ///
47 /// # Arguments
48 ///
49 /// - `u64` - The unique ID.
50 /// - `Vector2D` - The initial position.
51 ///
52 /// # Returns
53 ///
54 /// - `RigidBody2D` - The new body.
55 pub fn new_dynamic(id: u64, position: Vector2D) -> RigidBody2D {
56 let mass: f64 = PHYSICS_DEFAULT_MASS;
57 RigidBody2D::new(
58 id,
59 position,
60 mass,
61 1.0 / mass,
62 DEFAULT_RESTITUTION,
63 DEFAULT_FRICTION,
64 BodyType::Dynamic,
65 )
66 }
67
68 /// Creates a new static rigid body at the given position with infinite mass.
69 ///
70 /// # Arguments
71 ///
72 /// - `u64` - The unique ID.
73 /// - `Vector2D` - The position.
74 ///
75 /// # Returns
76 ///
77 /// - `RigidBody2D` - The new static body.
78 pub fn new_static(id: u64, position: Vector2D) -> RigidBody2D {
79 RigidBody2D::new(
80 id,
81 position,
82 PHYSICS_STATIC_MASS,
83 0.0,
84 DEFAULT_RESTITUTION,
85 DEFAULT_FRICTION,
86 BodyType::Static,
87 )
88 }
89
90 /// Applies a force to the body's force accumulator.
91 ///
92 /// # Arguments
93 ///
94 /// - `Vector2D` - The force vector.
95 pub fn apply_force(&mut self, force: Vector2D) {
96 *self.get_mut_force_accumulator() += force;
97 }
98
99 /// Applies an instantaneous impulse, directly changing velocity.
100 ///
101 /// # Arguments
102 ///
103 /// - `Vector2D` - The impulse vector.
104 pub fn apply_impulse(&mut self, impulse: Vector2D) {
105 let inverse_mass: f64 = self.get_inverse_mass();
106 if inverse_mass == 0.0 {
107 return;
108 }
109 *self.get_mut_velocity() += impulse.scaled(inverse_mass);
110 }
111
112 /// Sets the mass of the body, updating the inverse mass.
113 /// A mass of 0 makes the body static (infinite mass).
114 ///
115 /// # Arguments
116 ///
117 /// - `f64` - The new mass.
118 pub fn update_mass(&mut self, mass: f64) {
119 self.set_mass(mass);
120 self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
121 }
122
123 /// Returns `true` if this body is affected by forces and collisions.
124 ///
125 /// # Returns
126 ///
127 /// - `bool` - True if the body is dynamic.
128 pub fn is_dynamic(&self) -> bool {
129 self.get_body_type() == BodyType::Dynamic
130 }
131
132 /// Attaches a collider shape to this body.
133 ///
134 /// # Arguments
135 ///
136 /// - `BodyCollider` - The collider to attach.
137 pub fn update_collider(&mut self, collider: BodyCollider) {
138 self.set_collider(Some(collider));
139 }
140
141 /// Returns the world-space bounding box of the attached collider, if any.
142 ///
143 /// # Returns
144 ///
145 /// - `Option<Rect>` - The bounding box, or `None` if no collider is attached.
146 pub fn bounding_box(&self) -> Option<Rect> {
147 let collider: Option<BodyCollider> = self.get_collider();
148 match collider? {
149 BodyCollider::Aabb(aabb) => {
150 let aabb_rect: Rect = aabb.get_rect();
151 let mut offset_rect: Rect = aabb_rect;
152 offset_rect.set_x(
153 offset_rect.get_x() + self.get_position().get_x() - aabb_rect.get_width() * 0.5,
154 );
155 offset_rect.set_y(
156 offset_rect.get_y() + self.get_position().get_y()
157 - aabb_rect.get_height() * 0.5,
158 );
159 Some(offset_rect)
160 }
161 BodyCollider::Circle(circle) => {
162 let diameter: f64 = circle.get_circle().get_radius() * 2.0;
163 Some(Rect::from_center(self.get_position(), diameter, diameter))
164 }
165 }
166 }
167}
168
169/// Implements body management and simulation for `PhysicsWorld2D`.
170impl PhysicsWorld2D {
171 /// Creates a new physics world with the given configuration.
172 ///
173 /// # Arguments
174 ///
175 /// - `PhysicsConfig` - The simulation configuration.
176 ///
177 /// # Returns
178 ///
179 /// - `PhysicsWorld2D` - The new world.
180 pub fn with_config(config: PhysicsConfig) -> PhysicsWorld2D {
181 let mut world: PhysicsWorld2D = PhysicsWorld2D::new(config);
182 world.set_grid(SpatialHashGrid2D::with_default_size());
183 world
184 }
185
186 /// Adds a rigid body to the world.
187 ///
188 /// # Arguments
189 ///
190 /// - `RigidBody2D` - The body to add.
191 pub fn add_body(&mut self, body: RigidBody2D) {
192 self.get_mut_bodies().push(body);
193 }
194
195 /// Removes the body with the given ID.
196 ///
197 /// # Arguments
198 ///
199 /// - `u64` - The ID of the body to remove.
200 pub fn remove_body(&mut self, id: u64) {
201 self.get_mut_bodies()
202 .retain(|body: &RigidBody2D| body.get_id() != id);
203 }
204
205 /// Returns a reference to the body with the given ID.
206 ///
207 /// # Arguments
208 ///
209 /// - `u64` - The body ID.
210 ///
211 /// # Returns
212 ///
213 /// - `Option<&RigidBody2D>` - The body reference, if found.
214 pub fn get_body(&self, id: u64) -> Option<&RigidBody2D> {
215 self.get_bodies()
216 .iter()
217 .find(|body: &&RigidBody2D| body.get_id() == id)
218 }
219
220 /// Returns a mutable reference to the body with the given ID.
221 ///
222 /// # Arguments
223 ///
224 /// - `u64` - The body ID.
225 ///
226 /// # Returns
227 ///
228 /// - `Option<&mut RigidBody2D>` - The mutable body reference, if found.
229 pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody2D> {
230 self.get_mut_bodies()
231 .iter_mut()
232 .find(|body: &&mut RigidBody2D| body.get_id() == id)
233 }
234}
235
236/// Implements `Default` for `PhysicsWorld2D` as an empty world.
237impl Default for PhysicsWorld2D {
238 /// Constructs a default [`PhysicsWorld2D`] value.
239 ///
240 /// # Returns
241 ///
242 /// - `PhysicsWorld2D` - A default-constructed instance with the documented initial state.
243 fn default() -> PhysicsWorld2D {
244 PhysicsWorld2D::with_config(PhysicsConfig::default())
245 }
246}
247
248/// Implements collision detection and resolution for `RigidBody2D`.
249impl RigidBody2D {
250 /// Checks collision with another body based on both bodies' collider shapes.
251 ///
252 /// # Arguments
253 ///
254 /// - `&RigidBody2D` - The other body to check against.
255 ///
256 /// # Returns
257 ///
258 /// - `Option<CollisionResult>` - The collision result, or `None`.
259 fn check_collision_with(&self, other: &RigidBody2D) -> Option<CollisionResult> {
260 let a_bbox: Rect = self.bounding_box()?;
261 let b_bbox: Rect = other.bounding_box()?;
262 if !Rect::broad_phase_alias(a_bbox, b_bbox) {
263 return None;
264 }
265 let self_collider: Option<BodyCollider> = self.get_collider();
266 let other_collider: Option<BodyCollider> = other.get_collider();
267 let position_delta: Vector2D = other.get_position() - self.get_position();
268 match (self_collider, other_collider) {
269 (Some(BodyCollider::Aabb(aabb_a)), Some(BodyCollider::Aabb(aabb_b))) => {
270 let aabb_b_rect: Rect = aabb_b.get_rect();
271 let offset_aabb_b: AabbCollider = AabbCollider::new(Rect::new(
272 aabb_b_rect.get_x() + position_delta.get_x(),
273 aabb_b_rect.get_y() + position_delta.get_y(),
274 aabb_b_rect.get_width(),
275 aabb_b_rect.get_height(),
276 ));
277 aabb_a.collide_with_aabb(&offset_aabb_b)
278 }
279 (Some(BodyCollider::Circle(circle_a)), Some(BodyCollider::Circle(circle_b))) => {
280 let circle_b_inner: Circle = circle_b.get_circle();
281 let offset_circle_b: CircleCollider = CircleCollider::new(Circle::new(
282 circle_b_inner.get_center() + position_delta,
283 circle_b_inner.get_radius(),
284 ));
285 circle_a.collide_with_circle(&offset_circle_b)
286 }
287 (Some(BodyCollider::Aabb(aabb)), Some(BodyCollider::Circle(circle))) => {
288 let circle_inner: Circle = circle.get_circle();
289 let offset_circle: CircleCollider = CircleCollider::new(Circle::new(
290 circle_inner.get_center() + position_delta,
291 circle_inner.get_radius(),
292 ));
293 aabb.collide_with_circle(&offset_circle)
294 }
295 (Some(BodyCollider::Circle(circle)), Some(BodyCollider::Aabb(aabb))) => {
296 let aabb_rect: Rect = aabb.get_rect();
297 let offset_aabb: AabbCollider = AabbCollider::new(Rect::new(
298 aabb_rect.get_x() + position_delta.get_x(),
299 aabb_rect.get_y() + position_delta.get_y(),
300 aabb_rect.get_width(),
301 aabb_rect.get_height(),
302 ));
303 offset_aabb
304 .collide_with_circle(&circle)
305 .map(|mut result: CollisionResult| {
306 result.set_normal(-result.get_normal());
307 result
308 })
309 }
310 _ => None,
311 }
312 }
313
314 /// Resolves a collision with another body using impulse-based response
315 /// and position correction.
316 ///
317 /// # Arguments
318 ///
319 /// - `&mut RigidBody2D` - The other body involved in the collision.
320 /// - `&CollisionResult` - The collision data.
321 fn resolve_collision_with(&mut self, other: &mut RigidBody2D, result: &CollisionResult) {
322 let self_inverse_mass: f64 = self.get_inverse_mass();
323 let other_inverse_mass: f64 = other.get_inverse_mass();
324 let relative_velocity: Vector2D = other.get_velocity() - self.get_velocity();
325 let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
326 if velocity_along_normal > 0.0 {
327 return;
328 }
329 let restitution: f64 = self.get_restitution().min(other.get_restitution());
330 let inverse_mass_sum: f64 = self_inverse_mass + other_inverse_mass;
331 if inverse_mass_sum == 0.0 {
332 return;
333 }
334 let impulse_magnitude: f64 =
335 -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum;
336 let impulse: Vector2D = result.get_normal().scaled(impulse_magnitude);
337 *self.get_mut_velocity() -= impulse.scaled(self_inverse_mass);
338 *other.get_mut_velocity() += impulse.scaled(other_inverse_mass);
339 let correction: Vector2D = result
340 .get_normal()
341 .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
342 *self.get_mut_position() -= correction.scaled(self_inverse_mass);
343 *other.get_mut_position() += correction.scaled(other_inverse_mass);
344 }
345}
346
347/// Implements simulation stepping and collision resolution for `PhysicsWorld2D`.
348impl PhysicsWorld2D {
349 /// Performs one physics simulation step using semi-implicit Euler integration.
350 ///
351 /// Applies gravity to dynamic bodies, integrates velocity from accumulated forces,
352 /// applies damping, integrates position, and resolves collisions.
353 ///
354 /// # Arguments
355 ///
356 /// - `f64` - The fixed delta time in seconds.
357 pub fn step(&mut self, delta_time: f64) {
358 let config: PhysicsConfig = self.get_config();
359 // Hoist loop-invariant damping factors out of the per-body loop.
360 let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
361 let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
362 let gravity: Vector2D = config.get_gravity();
363 for body in self.get_mut_bodies() {
364 if !body.is_dynamic() {
365 continue;
366 }
367 let body_mass: f64 = body.get_mass();
368 let body_inverse_mass: f64 = body.get_inverse_mass();
369 *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
370 let force: Vector2D = body.get_force_accumulator();
371 *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
372 // In-place damping and integration avoid temporary vector copies.
373 *body.get_mut_velocity() *= damping_factor;
374 let current_velocity: Vector2D = body.get_velocity();
375 *body.get_mut_position() += current_velocity.scaled(delta_time);
376 body.set_force_accumulator(Vector2D::zero());
377 *body.get_mut_angular_velocity() *= angular_damping;
378 let current_angular_velocity: f64 = body.get_angular_velocity();
379 *body.get_mut_rotation() += current_angular_velocity * delta_time;
380 }
381 self.resolve_collisions();
382 }
383
384 /// Detects and resolves all collisions between bodies in the world.
385 ///
386 /// Uses a spatial hash grid for broad-phase culling followed by narrow-phase
387 /// shape-specific collision detection, then applies impulse-based resolution.
388 /// This reduces the broad-phase from O(n²) to near O(n) for typical scenes.
389 fn resolve_collisions(&mut self) {
390 let body_count: usize = self.get_bodies().len();
391 if body_count < 2 {
392 return;
393 }
394 // Rebuild the persistent grid once per step and collect the candidate
395 // pair list once; every solver iteration then reuses both (the grid is
396 // unchanged between iterations), eliminating per-iteration re-queries and
397 // per-query allocations.
398 // OPT 33: the candidate pair list is now backed by `self.pair_buffer`,
399 // a persistent `Vec<(usize, usize)>` field on `PhysicsWorld2D`. Cleared
400 // at the top of each step instead of allocating a fresh `Vec` — across
401 // thousands of physics steps per app run the heap churn adds up.
402 self.pair_buffer.clear();
403 {
404 let (bodies, grid, query_buffer, query_seen) = (
405 &self.bodies,
406 &mut self.grid,
407 &mut self.query_buffer,
408 &mut self.query_seen,
409 );
410 grid.clear();
411 for (index, body) in bodies.iter().enumerate() {
412 if let Some(bbox) = body.bounding_box() {
413 grid.insert(index, bbox.min(), bbox.max());
414 }
415 }
416 let pairs: &mut Vec<(usize, usize)> = &mut self.pair_buffer;
417 for (i, body) in bodies.iter().enumerate() {
418 let Some(bbox) = body.bounding_box() else {
419 continue;
420 };
421 grid.query_into(bbox.min(), bbox.max(), query_buffer, query_seen);
422 for &j in query_buffer.iter() {
423 if j > i {
424 pairs.push((i, j));
425 }
426 }
427 }
428 }
429 // OPT 33: copy out the pairs slice into a stack-local binding once
430 // per step so the immutable borrow on `self.pair_buffer` ends before
431 // the `self.get_mut_bodies()` mutable borrow below. Method-call-
432 // based disjoint borrow rules in Rust 2024 are not fine-grained
433 // enough to allow `self.pair_buffer.iter()` alongside
434 // `self.get_mut_bodies()` for a method that takes `&mut self`.
435 // The clone happens once per step (not per iteration) — `pairs` is
436 // typically O(n) short, much smaller than the per-pair body work.
437 let pairs_snapshot: Vec<(usize, usize)> = self.pair_buffer.clone();
438 for iteration in 0..PHYSICS_MAX_ITERATIONS {
439 let mut any_collision: bool = false;
440 for &(i, j) in pairs_snapshot.iter() {
441 let (left, right) = self.get_mut_bodies().split_at_mut(j);
442 let body_a: &mut RigidBody2D = &mut left[i];
443 let body_b: &mut RigidBody2D = &mut right[0];
444 if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
445 continue;
446 }
447 if let Some(result) = body_a.check_collision_with(body_b) {
448 body_a.resolve_collision_with(body_b, &result);
449 any_collision = true;
450 }
451 }
452 if !any_collision {
453 break;
454 }
455 let _: u32 = iteration;
456 }
457 }
458}
459
460/// Forwards `PhysicsWorld2D::step` through the [`Updatable`] trait so that
461/// physics worlds participate in the same update loop as entities, animators,
462/// and scene managers. The inherent [`PhysicsWorld2D::step`] method is the
463/// canonical implementation; this impl exists purely for trait dispatch.
464/// The inherent call resolves first when both are in scope, so there is no
465/// recursion.
466impl Updatable for PhysicsWorld2D {
467 /// Advances the simulation by `delta_time` seconds.
468 ///
469 /// # Arguments
470 ///
471 /// - `f64` - Seconds elapsed since the previous update.
472 fn update(&mut self, delta_time: f64) {
473 PhysicsWorld2D::step(self, delta_time);
474 }
475}
476
477/// Implements default configuration for `PhysicsConfig3D`.
478impl Default for PhysicsConfig3D {
479 /// Constructs a default [`PhysicsConfig3D`] value.
480 ///
481 /// # Returns
482 ///
483 /// - `PhysicsConfig3D` - A default-constructed instance with the documented initial state.
484 fn default() -> PhysicsConfig3D {
485 PhysicsConfig3D::new(
486 Vector3D::new(0.0, DEFAULT_GRAVITY_3D, 0.0),
487 DEFAULT_LINEAR_DAMPING,
488 DEFAULT_ANGULAR_DAMPING,
489 )
490 }
491}
492
493/// Implements body creation and force management for `RigidBody3D`.
494impl RigidBody3D {
495 /// Creates a new dynamic 3D rigid body with default mass and the given position.
496 ///
497 /// # Arguments
498 ///
499 /// - `u64` - The unique ID.
500 /// - `Vector3D` - The initial position.
501 ///
502 /// # Returns
503 ///
504 /// - `RigidBody3D` - The new body.
505 pub fn new_dynamic(id: u64, position: Vector3D) -> RigidBody3D {
506 let mass: f64 = PHYSICS_DEFAULT_MASS;
507 let mut body: RigidBody3D = RigidBody3D::new(
508 id,
509 position,
510 mass,
511 1.0 / mass,
512 DEFAULT_RESTITUTION,
513 DEFAULT_FRICTION,
514 BodyType::Dynamic,
515 );
516 body.update_inertia(mass);
517 body
518 }
519
520 /// Creates a new static 3D rigid body at the given position with infinite mass.
521 ///
522 /// # Arguments
523 ///
524 /// - `u64` - The unique ID.
525 /// - `Vector3D` - The position.
526 ///
527 /// # Returns
528 ///
529 /// - `RigidBody3D` - The new static body.
530 pub fn new_static(id: u64, position: Vector3D) -> RigidBody3D {
531 RigidBody3D::new(
532 id,
533 position,
534 PHYSICS_STATIC_MASS,
535 0.0,
536 DEFAULT_RESTITUTION,
537 DEFAULT_FRICTION,
538 BodyType::Static,
539 )
540 }
541
542 /// Applies a force to the body's force accumulator.
543 ///
544 /// # Arguments
545 ///
546 /// - `Vector3D` - The force vector.
547 pub fn apply_force(&mut self, force: Vector3D) {
548 *self.get_mut_force_accumulator() += force;
549 }
550
551 /// Applies a torque to the body's torque accumulator.
552 ///
553 /// # Arguments
554 ///
555 /// - `Vector3D` - The torque vector.
556 pub fn apply_torque(&mut self, torque: Vector3D) {
557 *self.get_mut_torque_accumulator() += torque;
558 }
559
560 /// Applies an instantaneous impulse, directly changing velocity.
561 ///
562 /// # Arguments
563 ///
564 /// - `Vector3D` - The impulse vector.
565 pub fn apply_impulse(&mut self, impulse: Vector3D) {
566 let inverse_mass: f64 = self.get_inverse_mass();
567 if inverse_mass == 0.0 {
568 return;
569 }
570 *self.get_mut_velocity() += impulse.scaled(inverse_mass);
571 }
572
573 /// Sets the mass of the body, updating the inverse mass.
574 /// A mass of 0 makes the body static (infinite mass).
575 ///
576 /// # Arguments
577 ///
578 /// - `f64` - The new mass.
579 pub fn update_mass(&mut self, mass: f64) {
580 self.set_mass(mass);
581 self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
582 }
583
584 /// Sets the moment of inertia of the body, updating the inverse inertia.
585 /// An inertia of 0 makes the body non-rotatable (used for static bodies).
586 ///
587 /// # Arguments
588 ///
589 /// - `f64` - The new moment of inertia.
590 pub fn update_inertia(&mut self, inertia: f64) {
591 self.set_inverse_inertia(if inertia > 0.0 { 1.0 / inertia } else { 0.0 });
592 }
593
594 /// Returns `true` if this body is affected by forces and collisions.
595 ///
596 /// # Returns
597 ///
598 /// - `bool` - True if this body is dynamic.
599 pub fn is_dynamic(&self) -> bool {
600 self.get_body_type() == BodyType::Dynamic
601 }
602
603 /// Attaches a 3D collider shape to this body.
604 ///
605 /// # Arguments
606 ///
607 /// - `BodyCollider3D` - The collider to attach.
608 pub fn update_collider(&mut self, collider: BodyCollider3D) {
609 self.set_collider(Some(collider));
610 }
611
612 /// Returns the world-space 3D bounding box of the attached collider, if any.
613 ///
614 /// # Returns
615 ///
616 /// - `Option<AABB3D>` - The bounding box, or `None` if no collider is attached.
617 pub fn bounding_box(&self) -> Option<AABB3D> {
618 let collider: Option<BodyCollider3D> = self.get_collider();
619 let position: Vector3D = self.get_position();
620 match collider? {
621 BodyCollider3D::Aabb(aabb) => {
622 let center: Vector3D = aabb.get_aabb().center();
623 let size: Vector3D = aabb.get_aabb().size();
624 Some(AABB3D::from_center(
625 position + center,
626 size.get_x(),
627 size.get_y(),
628 size.get_z(),
629 ))
630 }
631 BodyCollider3D::Sphere(sphere) => {
632 let sphere_inner: Sphere = sphere.get_sphere();
633 let diameter: f64 = sphere_inner.get_radius() * 2.0;
634 Some(AABB3D::from_center(
635 position + sphere_inner.get_center(),
636 diameter,
637 diameter,
638 diameter,
639 ))
640 }
641 }
642 }
643}
644
645/// Implements body management and simulation for `PhysicsWorld3D`.
646impl PhysicsWorld3D {
647 /// Creates a new 3D physics world with the given configuration.
648 ///
649 /// # Arguments
650 ///
651 /// - `PhysicsConfig3D` - The simulation configuration.
652 ///
653 /// # Returns
654 ///
655 /// - `PhysicsWorld3D` - The new world.
656 pub fn with_config(config: PhysicsConfig3D) -> PhysicsWorld3D {
657 let mut world: PhysicsWorld3D = PhysicsWorld3D::new(config);
658 world.set_grid(SpatialHashGrid3D::with_default_size());
659 world
660 }
661
662 /// Adds a rigid body to the world.
663 ///
664 /// # Arguments
665 ///
666 /// - `RigidBody3D` - The body to add.
667 pub fn add_body(&mut self, body: RigidBody3D) {
668 self.get_mut_bodies().push(body);
669 }
670
671 /// Removes the body with the given ID.
672 ///
673 /// # Arguments
674 ///
675 /// - `u64` - The ID of the body to remove.
676 pub fn remove_body(&mut self, id: u64) {
677 self.get_mut_bodies()
678 .retain(|body: &RigidBody3D| body.get_id() != id);
679 }
680
681 /// Returns a reference to the body with the given ID.
682 ///
683 /// # Arguments
684 ///
685 /// - `u64` - The body ID.
686 ///
687 /// # Returns
688 ///
689 /// - `Option<&RigidBody3D>` - The body reference, if found.
690 pub fn get_body(&self, id: u64) -> Option<&RigidBody3D> {
691 self.get_bodies()
692 .iter()
693 .find(|body: &&RigidBody3D| body.get_id() == id)
694 }
695
696 /// Returns a mutable reference to the body with the given ID.
697 ///
698 /// # Arguments
699 ///
700 /// - `u64` - The body ID.
701 ///
702 /// # Returns
703 ///
704 /// - `Option<&mut RigidBody3D>` - The mutable body reference, if found.
705 pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody3D> {
706 self.get_mut_bodies()
707 .iter_mut()
708 .find(|body: &&mut RigidBody3D| body.get_id() == id)
709 }
710
711 /// Performs one physics simulation step using semi-implicit Euler integration.
712 ///
713 /// Applies gravity to dynamic bodies, integrates velocity from accumulated forces,
714 /// applies damping, integrates position, and resolves collisions.
715 ///
716 /// # Arguments
717 ///
718 /// - `f64` - The fixed delta time in seconds.
719 pub fn step(&mut self, delta_time: f64) {
720 let config: PhysicsConfig3D = self.get_config();
721 // Hoist loop-invariant damping factors out of the per-body loop.
722 let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
723 let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
724 let gravity: Vector3D = config.get_gravity();
725 for body in self.get_mut_bodies() {
726 if !body.is_dynamic() {
727 continue;
728 }
729 let body_mass: f64 = body.get_mass();
730 let body_inverse_mass: f64 = body.get_inverse_mass();
731 *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
732 let force: Vector3D = body.get_force_accumulator();
733 *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
734 // In-place damping and integration avoid temporary vector copies.
735 *body.get_mut_velocity() *= damping_factor;
736 let current_velocity: Vector3D = body.get_velocity();
737 *body.get_mut_position() += current_velocity.scaled(delta_time);
738 body.set_force_accumulator(Vector3D::zero());
739 *body.get_mut_angular_velocity() *= angular_damping;
740 let body_inverse_inertia: f64 = body.get_inverse_inertia();
741 let torque: Vector3D = body.get_torque_accumulator();
742 *body.get_mut_angular_velocity() += torque.scaled(body_inverse_inertia * delta_time);
743 let angular_velocity: Vector3D = body.get_angular_velocity();
744 let rotation_delta: Quaternion = Quaternion::new(
745 angular_velocity.get_x() * delta_time * 0.5,
746 angular_velocity.get_y() * delta_time * 0.5,
747 angular_velocity.get_z() * delta_time * 0.5,
748 1.0,
749 );
750 body.set_rotation((rotation_delta * body.get_rotation()).normalized());
751 body.set_torque_accumulator(Vector3D::zero());
752 }
753 self.resolve_collisions();
754 }
755
756 /// Detects and resolves all collisions between bodies in the 3D world.
757 ///
758 /// Uses a spatial hash grid for broad-phase culling followed by narrow-phase
759 /// shape-specific collision detection, then applies impulse-based resolution.
760 /// This reduces the broad-phase from O(n²) to near O(n) for typical scenes.
761 fn resolve_collisions(&mut self) {
762 let body_count: usize = self.get_bodies().len();
763 if body_count < 2 {
764 return;
765 }
766 // Rebuild the persistent grid once per step and collect the candidate
767 // pair list once; every solver iteration then reuses both (the grid is
768 // unchanged between iterations), eliminating per-iteration re-queries and
769 // per-query allocations.
770 // OPT 33: candidate pair list backed by `self.pair_buffer`, a persistent
771 // field on `PhysicsWorld3D`. See `PhysicsWorld2D::resolve_collisions`
772 // for the rationale.
773 self.pair_buffer.clear();
774 // Collect bboxes first (immutable borrow of bodies) then drain the
775 // spatial grid (mutable borrow). Splitting avoids the split-borrow
776 // limitation that method-call-based accessors introduce.
777 // OPT 33: pre-size the bboxes scratch Vec to the current body count
778 // so the first allocation does not double-grow on subsequent frames
779 // (each body's bbox is `O(1)` and the Vec is rebuilt every step).
780 // The actual allocation still happens here — the persistent
781 // `bbox_buffer` field idea was rejected because the immutable-then-
782 // mutable borrow split on `self.bodies` cannot hold both a `&mut`
783 // borrow on `bbox_buffer` and the source `iter()` simultaneously
784 // even with edition 2024 split-borrow rules.
785 let body_count: usize = self.get_bodies().len();
786 let mut bboxes: Vec<(usize, AABB3D)> = Vec::with_capacity(body_count);
787 bboxes.extend(
788 self.get_bodies()
789 .iter()
790 .enumerate()
791 .filter_map(|(index, body)| body.bounding_box().map(|bbox| (index, bbox))),
792 );
793 {
794 let Self {
795 grid,
796 query_buffer,
797 query_seen,
798 ..
799 } = self;
800 let grid: &mut SpatialHashGrid3D = grid;
801 let query_buffer: &mut Vec<usize> = query_buffer;
802 let query_seen: &mut HashSet<usize> = query_seen;
803 grid.clear();
804 let pairs: &mut Vec<(usize, usize)> = &mut self.pair_buffer;
805 for (index, bbox) in bboxes.iter() {
806 grid.insert(*index, bbox.get_min(), bbox.get_max());
807 }
808 for (i, (_, bbox)) in bboxes.iter().enumerate() {
809 grid.query_into(bbox.get_min(), bbox.get_max(), query_buffer, query_seen);
810 for &j in query_buffer.iter() {
811 if j > i {
812 pairs.push((i, j));
813 }
814 }
815 }
816 }
817 // OPT 33: copy out the pairs slice into a stack-local binding once
818 // per step so the immutable borrow on `self.pair_buffer` ends before
819 // the `self.get_mut_bodies()` mutable borrow below. See the matching
820 // comment in `PhysicsWorld2D::resolve_collisions` for rationale.
821 let pairs_snapshot: Vec<(usize, usize)> = self.pair_buffer.clone();
822 for iteration in 0..PHYSICS_MAX_ITERATIONS {
823 let mut any_collision: bool = false;
824 for &(i, j) in pairs_snapshot.iter() {
825 let (left, right) = self.get_mut_bodies().split_at_mut(j);
826 let body_a: &mut RigidBody3D = &mut left[i];
827 let body_b: &mut RigidBody3D = &mut right[0];
828 if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
829 continue;
830 }
831 if let Some(result) = Self::check_collision_3d(body_a, body_b) {
832 Self::resolve_collision_3d(body_a, body_b, &result);
833 any_collision = true;
834 }
835 }
836 if !any_collision {
837 break;
838 }
839 let _: u32 = iteration;
840 }
841 }
842
843 /// Checks collision between two 3D bodies based on both bodies' collider shapes.
844 ///
845 /// # Arguments
846 ///
847 /// - `&RigidBody3D` - The first body.
848 /// - `&RigidBody3D` - The second body.
849 ///
850 /// # Returns
851 ///
852 /// - `Option<CollisionResult3D>` - The collision result, or `None`.
853 fn check_collision_3d(a: &RigidBody3D, b: &RigidBody3D) -> Option<CollisionResult3D> {
854 let a_bbox: AABB3D = a.bounding_box()?;
855 let b_bbox: AABB3D = b.bounding_box()?;
856 if !AABB3D::broad_phase(a_bbox, b_bbox) {
857 return None;
858 }
859 let a_collider: Option<BodyCollider3D> = a.get_collider();
860 let b_collider: Option<BodyCollider3D> = b.get_collider();
861 let position_delta: Vector3D = b.get_position() - a.get_position();
862 match (a_collider, b_collider) {
863 (Some(BodyCollider3D::Aabb(aabb_a)), Some(BodyCollider3D::Aabb(aabb_b))) => {
864 let aabb_b_inner: AABB3D = aabb_b.get_aabb();
865 let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
866 aabb_b_inner.get_min() + position_delta,
867 aabb_b_inner.get_max() + position_delta,
868 ));
869 aabb_a.collide_with_aabb(&offset_aabb)
870 }
871 (Some(BodyCollider3D::Sphere(sphere_a)), Some(BodyCollider3D::Sphere(sphere_b))) => {
872 let sphere_b_inner: Sphere = sphere_b.get_sphere();
873 let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
874 sphere_b_inner.get_center() + position_delta,
875 sphere_b_inner.get_radius(),
876 ));
877 sphere_a.collide_with_sphere(&offset_sphere)
878 }
879 (Some(BodyCollider3D::Aabb(aabb)), Some(BodyCollider3D::Sphere(sphere))) => {
880 let sphere_inner: Sphere = sphere.get_sphere();
881 let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
882 sphere_inner.get_center() + position_delta,
883 sphere_inner.get_radius(),
884 ));
885 aabb.collide_with_sphere(&offset_sphere)
886 }
887 (Some(BodyCollider3D::Sphere(sphere)), Some(BodyCollider3D::Aabb(aabb))) => {
888 let aabb_inner: AABB3D = aabb.get_aabb();
889 let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
890 aabb_inner.get_min() + position_delta,
891 aabb_inner.get_max() + position_delta,
892 ));
893 offset_aabb
894 .collide_with_sphere(&sphere)
895 .map(|mut result: CollisionResult3D| {
896 result.set_normal(-result.get_normal());
897 result
898 })
899 }
900 _ => None,
901 }
902 }
903
904 /// Resolves a collision between two 3D bodies using impulse-based response
905 /// and position correction.
906 ///
907 /// # Arguments
908 ///
909 /// - `&mut RigidBody3D` - The first body.
910 /// - `&mut RigidBody3D` - The second body.
911 /// - `&CollisionResult3D` - The collision data.
912 fn resolve_collision_3d(a: &mut RigidBody3D, b: &mut RigidBody3D, result: &CollisionResult3D) {
913 let a_inverse_mass: f64 = a.get_inverse_mass();
914 let b_inverse_mass: f64 = b.get_inverse_mass();
915 let relative_velocity: Vector3D = b.get_velocity() - a.get_velocity();
916 let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
917 if velocity_along_normal > 0.0 {
918 return;
919 }
920 let restitution: f64 = a.get_restitution().min(b.get_restitution());
921 let inverse_mass_sum: f64 = a_inverse_mass + b_inverse_mass;
922 if inverse_mass_sum == 0.0 {
923 return;
924 }
925 let impulse_magnitude: f64 =
926 -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum;
927 let impulse: Vector3D = result.get_normal().scaled(impulse_magnitude);
928 *a.get_mut_velocity() -= impulse.scaled(a_inverse_mass);
929 *b.get_mut_velocity() += impulse.scaled(b_inverse_mass);
930 let correction: Vector3D = result
931 .get_normal()
932 .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
933 *a.get_mut_position() -= correction.scaled(a_inverse_mass);
934 *b.get_mut_position() += correction.scaled(b_inverse_mass);
935 }
936}
937
938/// Forwards `PhysicsWorld3D::step` through the [`Updatable`] trait so that
939/// 3D physics worlds participate in the same update loop as their 2D
940/// counterparts, entities, animators, and scene managers. The inherent
941/// [`PhysicsWorld3D::step`] method is the canonical implementation; this impl
942/// exists purely for trait dispatch. The inherent call resolves first when
943/// both are in scope, so there is no recursion.
944impl Updatable for PhysicsWorld3D {
945 /// Advances the simulation by `delta_time` seconds.
946 ///
947 /// # Arguments
948 ///
949 /// - `f64` - Seconds elapsed since the previous update.
950 fn update(&mut self, delta_time: f64) {
951 PhysicsWorld3D::step(self, delta_time);
952 }
953}
954
955/// Implements `Default` for `PhysicsWorld3D` as an empty world.
956impl Default for PhysicsWorld3D {
957 /// Constructs a default [`PhysicsWorld3D`] value.
958 ///
959 /// # Returns
960 ///
961 /// - `PhysicsWorld3D` - A default-constructed instance with the documented initial state.
962 fn default() -> PhysicsWorld3D {
963 PhysicsWorld3D::with_config(PhysicsConfig3D::default())
964 }
965}