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: `mem::take` moves the pairs out (leaving an empty Vec
430 // behind) so the immutable borrow on `self.pair_buffer` ends before
431 // the `self.get_mut_bodies()` mutable borrow below — the buffer keeps
432 // its allocation across steps instead of paying one Vec clone
433 // (alloc + memcpy) per step per world. It is restored after the
434 // iteration loop.
435 let pairs_snapshot: Vec<(usize, usize)> = std::mem::take(&mut self.pair_buffer);
436 for iteration in 0..PHYSICS_MAX_ITERATIONS {
437 let mut any_collision: bool = false;
438 for &(i, j) in pairs_snapshot.iter() {
439 let (left, right) = self.get_mut_bodies().split_at_mut(j);
440 let body_a: &mut RigidBody2D = &mut left[i];
441 let body_b: &mut RigidBody2D = &mut right[0];
442 if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
443 continue;
444 }
445 if let Some(result) = body_a.check_collision_with(body_b) {
446 body_a.resolve_collision_with(body_b, &result);
447 any_collision = true;
448 }
449 }
450 if !any_collision {
451 break;
452 }
453 let _: u32 = iteration;
454 }
455 self.pair_buffer = pairs_snapshot;
456 }
457}
458
459/// Forwards `PhysicsWorld2D::step` through the [`Updatable`] trait so that
460/// physics worlds participate in the same update loop as entities, animators,
461/// and scene managers. The inherent [`PhysicsWorld2D::step`] method is the
462/// canonical implementation; this impl exists purely for trait dispatch.
463/// The inherent call resolves first when both are in scope, so there is no
464/// recursion.
465impl Updatable for PhysicsWorld2D {
466 /// Advances the simulation by `delta_time` seconds.
467 ///
468 /// # Arguments
469 ///
470 /// - `f64` - Seconds elapsed since the previous update.
471 fn update(&mut self, delta_time: f64) {
472 PhysicsWorld2D::step(self, delta_time);
473 }
474}
475
476/// Implements default configuration for `PhysicsConfig3D`.
477impl Default for PhysicsConfig3D {
478 /// Constructs a default [`PhysicsConfig3D`] value.
479 ///
480 /// # Returns
481 ///
482 /// - `PhysicsConfig3D` - A default-constructed instance with the documented initial state.
483 fn default() -> PhysicsConfig3D {
484 PhysicsConfig3D::new(
485 Vector3D::new(0.0, DEFAULT_GRAVITY_3D, 0.0),
486 DEFAULT_LINEAR_DAMPING,
487 DEFAULT_ANGULAR_DAMPING,
488 )
489 }
490}
491
492/// Implements body creation and force management for `RigidBody3D`.
493impl RigidBody3D {
494 /// Creates a new dynamic 3D rigid body with default mass and the given position.
495 ///
496 /// # Arguments
497 ///
498 /// - `u64` - The unique ID.
499 /// - `Vector3D` - The initial position.
500 ///
501 /// # Returns
502 ///
503 /// - `RigidBody3D` - The new body.
504 pub fn new_dynamic(id: u64, position: Vector3D) -> RigidBody3D {
505 let mass: f64 = PHYSICS_DEFAULT_MASS;
506 let mut body: RigidBody3D = RigidBody3D::new(
507 id,
508 position,
509 mass,
510 1.0 / mass,
511 DEFAULT_RESTITUTION,
512 DEFAULT_FRICTION,
513 BodyType::Dynamic,
514 );
515 body.update_inertia(mass);
516 body
517 }
518
519 /// Creates a new static 3D rigid body at the given position with infinite mass.
520 ///
521 /// # Arguments
522 ///
523 /// - `u64` - The unique ID.
524 /// - `Vector3D` - The position.
525 ///
526 /// # Returns
527 ///
528 /// - `RigidBody3D` - The new static body.
529 pub fn new_static(id: u64, position: Vector3D) -> RigidBody3D {
530 RigidBody3D::new(
531 id,
532 position,
533 PHYSICS_STATIC_MASS,
534 0.0,
535 DEFAULT_RESTITUTION,
536 DEFAULT_FRICTION,
537 BodyType::Static,
538 )
539 }
540
541 /// Applies a force to the body's force accumulator.
542 ///
543 /// # Arguments
544 ///
545 /// - `Vector3D` - The force vector.
546 pub fn apply_force(&mut self, force: Vector3D) {
547 *self.get_mut_force_accumulator() += force;
548 }
549
550 /// Applies a torque to the body's torque accumulator.
551 ///
552 /// # Arguments
553 ///
554 /// - `Vector3D` - The torque vector.
555 pub fn apply_torque(&mut self, torque: Vector3D) {
556 *self.get_mut_torque_accumulator() += torque;
557 }
558
559 /// Applies an instantaneous impulse, directly changing velocity.
560 ///
561 /// # Arguments
562 ///
563 /// - `Vector3D` - The impulse vector.
564 pub fn apply_impulse(&mut self, impulse: Vector3D) {
565 let inverse_mass: f64 = self.get_inverse_mass();
566 if inverse_mass == 0.0 {
567 return;
568 }
569 *self.get_mut_velocity() += impulse.scaled(inverse_mass);
570 }
571
572 /// Sets the mass of the body, updating the inverse mass.
573 /// A mass of 0 makes the body static (infinite mass).
574 ///
575 /// # Arguments
576 ///
577 /// - `f64` - The new mass.
578 pub fn update_mass(&mut self, mass: f64) {
579 self.set_mass(mass);
580 self.set_inverse_mass(if mass > 0.0 { 1.0 / mass } else { 0.0 });
581 }
582
583 /// Sets the moment of inertia of the body, updating the inverse inertia.
584 /// An inertia of 0 makes the body non-rotatable (used for static bodies).
585 ///
586 /// # Arguments
587 ///
588 /// - `f64` - The new moment of inertia.
589 pub fn update_inertia(&mut self, inertia: f64) {
590 self.set_inverse_inertia(if inertia > 0.0 { 1.0 / inertia } else { 0.0 });
591 }
592
593 /// Returns `true` if this body is affected by forces and collisions.
594 ///
595 /// # Returns
596 ///
597 /// - `bool` - True if this body is dynamic.
598 pub fn is_dynamic(&self) -> bool {
599 self.get_body_type() == BodyType::Dynamic
600 }
601
602 /// Attaches a 3D collider shape to this body.
603 ///
604 /// # Arguments
605 ///
606 /// - `BodyCollider3D` - The collider to attach.
607 pub fn update_collider(&mut self, collider: BodyCollider3D) {
608 self.set_collider(Some(collider));
609 }
610
611 /// Returns the world-space 3D bounding box of the attached collider, if any.
612 ///
613 /// # Returns
614 ///
615 /// - `Option<AABB3D>` - The bounding box, or `None` if no collider is attached.
616 pub fn bounding_box(&self) -> Option<AABB3D> {
617 let collider: Option<BodyCollider3D> = self.get_collider();
618 let position: Vector3D = self.get_position();
619 match collider? {
620 BodyCollider3D::Aabb(aabb) => {
621 let center: Vector3D = aabb.get_aabb().center();
622 let size: Vector3D = aabb.get_aabb().size();
623 Some(AABB3D::from_center(
624 position + center,
625 size.get_x(),
626 size.get_y(),
627 size.get_z(),
628 ))
629 }
630 BodyCollider3D::Sphere(sphere) => {
631 let sphere_inner: Sphere = sphere.get_sphere();
632 let diameter: f64 = sphere_inner.get_radius() * 2.0;
633 Some(AABB3D::from_center(
634 position + sphere_inner.get_center(),
635 diameter,
636 diameter,
637 diameter,
638 ))
639 }
640 }
641 }
642}
643
644/// Implements body management and simulation for `PhysicsWorld3D`.
645impl PhysicsWorld3D {
646 /// Creates a new 3D physics world with the given configuration.
647 ///
648 /// # Arguments
649 ///
650 /// - `PhysicsConfig3D` - The simulation configuration.
651 ///
652 /// # Returns
653 ///
654 /// - `PhysicsWorld3D` - The new world.
655 pub fn with_config(config: PhysicsConfig3D) -> PhysicsWorld3D {
656 let mut world: PhysicsWorld3D = PhysicsWorld3D::new(config);
657 world.set_grid(SpatialHashGrid3D::with_default_size());
658 world
659 }
660
661 /// Adds a rigid body to the world.
662 ///
663 /// # Arguments
664 ///
665 /// - `RigidBody3D` - The body to add.
666 pub fn add_body(&mut self, body: RigidBody3D) {
667 self.get_mut_bodies().push(body);
668 }
669
670 /// Removes the body with the given ID.
671 ///
672 /// # Arguments
673 ///
674 /// - `u64` - The ID of the body to remove.
675 pub fn remove_body(&mut self, id: u64) {
676 self.get_mut_bodies()
677 .retain(|body: &RigidBody3D| body.get_id() != id);
678 }
679
680 /// Returns a reference to the body with the given ID.
681 ///
682 /// # Arguments
683 ///
684 /// - `u64` - The body ID.
685 ///
686 /// # Returns
687 ///
688 /// - `Option<&RigidBody3D>` - The body reference, if found.
689 pub fn get_body(&self, id: u64) -> Option<&RigidBody3D> {
690 self.get_bodies()
691 .iter()
692 .find(|body: &&RigidBody3D| body.get_id() == id)
693 }
694
695 /// Returns a mutable reference to the body with the given ID.
696 ///
697 /// # Arguments
698 ///
699 /// - `u64` - The body ID.
700 ///
701 /// # Returns
702 ///
703 /// - `Option<&mut RigidBody3D>` - The mutable body reference, if found.
704 pub fn get_body_mut(&mut self, id: u64) -> Option<&mut RigidBody3D> {
705 self.get_mut_bodies()
706 .iter_mut()
707 .find(|body: &&mut RigidBody3D| body.get_id() == id)
708 }
709
710 /// Performs one physics simulation step using semi-implicit Euler integration.
711 ///
712 /// Applies gravity to dynamic bodies, integrates velocity from accumulated forces,
713 /// applies damping, integrates position, and resolves collisions.
714 ///
715 /// # Arguments
716 ///
717 /// - `f64` - The fixed delta time in seconds.
718 pub fn step(&mut self, delta_time: f64) {
719 let config: PhysicsConfig3D = self.get_config();
720 // Hoist loop-invariant damping factors out of the per-body loop.
721 let damping_factor: f64 = (1.0 - config.get_linear_damping() * delta_time).max(0.0);
722 let angular_damping: f64 = (1.0 - config.get_angular_damping() * delta_time).max(0.0);
723 let gravity: Vector3D = config.get_gravity();
724 for body in self.get_mut_bodies() {
725 if !body.is_dynamic() {
726 continue;
727 }
728 let body_mass: f64 = body.get_mass();
729 let body_inverse_mass: f64 = body.get_inverse_mass();
730 *body.get_mut_force_accumulator() += gravity.scaled(body_mass);
731 let force: Vector3D = body.get_force_accumulator();
732 *body.get_mut_velocity() += force.scaled(body_inverse_mass * delta_time);
733 // In-place damping and integration avoid temporary vector copies.
734 *body.get_mut_velocity() *= damping_factor;
735 let current_velocity: Vector3D = body.get_velocity();
736 *body.get_mut_position() += current_velocity.scaled(delta_time);
737 body.set_force_accumulator(Vector3D::zero());
738 *body.get_mut_angular_velocity() *= angular_damping;
739 let body_inverse_inertia: f64 = body.get_inverse_inertia();
740 let torque: Vector3D = body.get_torque_accumulator();
741 *body.get_mut_angular_velocity() += torque.scaled(body_inverse_inertia * delta_time);
742 let angular_velocity: Vector3D = body.get_angular_velocity();
743 let rotation_delta: Quaternion = Quaternion::new(
744 angular_velocity.get_x() * delta_time * 0.5,
745 angular_velocity.get_y() * delta_time * 0.5,
746 angular_velocity.get_z() * delta_time * 0.5,
747 1.0,
748 );
749 body.set_rotation((rotation_delta * body.get_rotation()).normalized());
750 body.set_torque_accumulator(Vector3D::zero());
751 }
752 self.resolve_collisions();
753 }
754
755 /// Detects and resolves all collisions between bodies in the 3D world.
756 ///
757 /// Uses a spatial hash grid for broad-phase culling followed by narrow-phase
758 /// shape-specific collision detection, then applies impulse-based resolution.
759 /// This reduces the broad-phase from O(n²) to near O(n) for typical scenes.
760 fn resolve_collisions(&mut self) {
761 let body_count: usize = self.get_bodies().len();
762 if body_count < 2 {
763 return;
764 }
765 // Rebuild the persistent grid once per step and collect the candidate
766 // pair list once; every solver iteration then reuses both (the grid is
767 // unchanged between iterations), eliminating per-iteration re-queries and
768 // per-query allocations.
769 // OPT 33: candidate pair list backed by `self.pair_buffer`, a persistent
770 // field on `PhysicsWorld3D`. See `PhysicsWorld2D::resolve_collisions`
771 // for the rationale.
772 self.pair_buffer.clear();
773 // Collect bboxes first (immutable borrow of bodies) then drain the
774 // spatial grid (mutable borrow). Splitting avoids the split-borrow
775 // limitation that method-call-based accessors introduce.
776 // OPT 33: pre-size the bboxes scratch Vec to the current body count
777 // so the first allocation does not double-grow on subsequent frames
778 // (each body's bbox is `O(1)` and the Vec is rebuilt every step).
779 // The actual allocation still happens here — the persistent
780 // `bbox_buffer` field idea was rejected because the immutable-then-
781 // mutable borrow split on `self.bodies` cannot hold both a `&mut`
782 // borrow on `bbox_buffer` and the source `iter()` simultaneously
783 // even with edition 2024 split-borrow rules.
784 let body_count: usize = self.get_bodies().len();
785 let mut bboxes: Vec<(usize, AABB3D)> = Vec::with_capacity(body_count);
786 bboxes.extend(
787 self.get_bodies()
788 .iter()
789 .enumerate()
790 .filter_map(|(index, body)| body.bounding_box().map(|bbox| (index, bbox))),
791 );
792 {
793 let Self {
794 grid,
795 query_buffer,
796 query_seen,
797 ..
798 } = self;
799 let grid: &mut SpatialHashGrid3D = grid;
800 let query_buffer: &mut Vec<usize> = query_buffer;
801 let query_seen: &mut HashSet<usize> = query_seen;
802 grid.clear();
803 let pairs: &mut Vec<(usize, usize)> = &mut self.pair_buffer;
804 for (index, bbox) in bboxes.iter() {
805 grid.insert(*index, bbox.get_min(), bbox.get_max());
806 }
807 for (i, (_, bbox)) in bboxes.iter().enumerate() {
808 grid.query_into(bbox.get_min(), bbox.get_max(), query_buffer, query_seen);
809 for &j in query_buffer.iter() {
810 if j > i {
811 pairs.push((i, j));
812 }
813 }
814 }
815 }
816 // `mem::take` moves the pairs out (leaving an empty Vec behind) so
817 // the immutable borrow on `self.pair_buffer` ends before the
818 // `self.get_mut_bodies()` mutable borrow below — the buffer keeps
819 // its allocation across steps instead of paying one Vec clone
820 // (alloc + memcpy) per step per world. It is restored after the
821 // iteration loop.
822 let pairs_snapshot: Vec<(usize, usize)> = std::mem::take(&mut self.pair_buffer);
823 for iteration in 0..PHYSICS_MAX_ITERATIONS {
824 let mut any_collision: bool = false;
825 for &(i, j) in pairs_snapshot.iter() {
826 let (left, right) = self.get_mut_bodies().split_at_mut(j);
827 let body_a: &mut RigidBody3D = &mut left[i];
828 let body_b: &mut RigidBody3D = &mut right[0];
829 if body_a.get_inverse_mass() == 0.0 && body_b.get_inverse_mass() == 0.0 {
830 continue;
831 }
832 if let Some(result) = Self::check_collision_3d(body_a, body_b) {
833 Self::resolve_collision_3d(body_a, body_b, &result);
834 any_collision = true;
835 }
836 }
837 if !any_collision {
838 break;
839 }
840 let _: u32 = iteration;
841 }
842 self.pair_buffer = pairs_snapshot;
843 }
844
845 /// Checks collision between two 3D bodies based on both bodies' collider shapes.
846 ///
847 /// # Arguments
848 ///
849 /// - `&RigidBody3D` - The first body.
850 /// - `&RigidBody3D` - The second body.
851 ///
852 /// # Returns
853 ///
854 /// - `Option<CollisionResult3D>` - The collision result, or `None`.
855 fn check_collision_3d(a: &RigidBody3D, b: &RigidBody3D) -> Option<CollisionResult3D> {
856 let a_bbox: AABB3D = a.bounding_box()?;
857 let b_bbox: AABB3D = b.bounding_box()?;
858 if !AABB3D::broad_phase(a_bbox, b_bbox) {
859 return None;
860 }
861 let a_collider: Option<BodyCollider3D> = a.get_collider();
862 let b_collider: Option<BodyCollider3D> = b.get_collider();
863 let position_delta: Vector3D = b.get_position() - a.get_position();
864 match (a_collider, b_collider) {
865 (Some(BodyCollider3D::Aabb(aabb_a)), Some(BodyCollider3D::Aabb(aabb_b))) => {
866 let aabb_b_inner: AABB3D = aabb_b.get_aabb();
867 let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
868 aabb_b_inner.get_min() + position_delta,
869 aabb_b_inner.get_max() + position_delta,
870 ));
871 aabb_a.collide_with_aabb(&offset_aabb)
872 }
873 (Some(BodyCollider3D::Sphere(sphere_a)), Some(BodyCollider3D::Sphere(sphere_b))) => {
874 let sphere_b_inner: Sphere = sphere_b.get_sphere();
875 let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
876 sphere_b_inner.get_center() + position_delta,
877 sphere_b_inner.get_radius(),
878 ));
879 sphere_a.collide_with_sphere(&offset_sphere)
880 }
881 (Some(BodyCollider3D::Aabb(aabb)), Some(BodyCollider3D::Sphere(sphere))) => {
882 let sphere_inner: Sphere = sphere.get_sphere();
883 let offset_sphere: SphereCollider3D = SphereCollider3D::new(Sphere::new(
884 sphere_inner.get_center() + position_delta,
885 sphere_inner.get_radius(),
886 ));
887 aabb.collide_with_sphere(&offset_sphere)
888 }
889 (Some(BodyCollider3D::Sphere(sphere)), Some(BodyCollider3D::Aabb(aabb))) => {
890 let aabb_inner: AABB3D = aabb.get_aabb();
891 let offset_aabb: AabbCollider3D = AabbCollider3D::new(AABB3D::new(
892 aabb_inner.get_min() + position_delta,
893 aabb_inner.get_max() + position_delta,
894 ));
895 offset_aabb
896 .collide_with_sphere(&sphere)
897 .map(|mut result: CollisionResult3D| {
898 result.set_normal(-result.get_normal());
899 result
900 })
901 }
902 _ => None,
903 }
904 }
905
906 /// Resolves a collision between two 3D bodies using impulse-based response
907 /// and position correction.
908 ///
909 /// # Arguments
910 ///
911 /// - `&mut RigidBody3D` - The first body.
912 /// - `&mut RigidBody3D` - The second body.
913 /// - `&CollisionResult3D` - The collision data.
914 fn resolve_collision_3d(a: &mut RigidBody3D, b: &mut RigidBody3D, result: &CollisionResult3D) {
915 let a_inverse_mass: f64 = a.get_inverse_mass();
916 let b_inverse_mass: f64 = b.get_inverse_mass();
917 let relative_velocity: Vector3D = b.get_velocity() - a.get_velocity();
918 let velocity_along_normal: f64 = relative_velocity.dot(result.get_normal());
919 if velocity_along_normal > 0.0 {
920 return;
921 }
922 let restitution: f64 = a.get_restitution().min(b.get_restitution());
923 let inverse_mass_sum: f64 = a_inverse_mass + b_inverse_mass;
924 if inverse_mass_sum == 0.0 {
925 return;
926 }
927 let impulse_magnitude: f64 =
928 -(1.0 + restitution) * velocity_along_normal / inverse_mass_sum;
929 let impulse: Vector3D = result.get_normal().scaled(impulse_magnitude);
930 *a.get_mut_velocity() -= impulse.scaled(a_inverse_mass);
931 *b.get_mut_velocity() += impulse.scaled(b_inverse_mass);
932 let correction: Vector3D = result
933 .get_normal()
934 .scaled((result.get_depth() * PHYSICS_POSITION_PERCENT / inverse_mass_sum).max(0.0));
935 *a.get_mut_position() -= correction.scaled(a_inverse_mass);
936 *b.get_mut_position() += correction.scaled(b_inverse_mass);
937 }
938}
939
940/// Forwards `PhysicsWorld3D::step` through the [`Updatable`] trait so that
941/// 3D physics worlds participate in the same update loop as their 2D
942/// counterparts, entities, animators, and scene managers. The inherent
943/// [`PhysicsWorld3D::step`] method is the canonical implementation; this impl
944/// exists purely for trait dispatch. The inherent call resolves first when
945/// both are in scope, so there is no recursion.
946impl Updatable for PhysicsWorld3D {
947 /// Advances the simulation by `delta_time` seconds.
948 ///
949 /// # Arguments
950 ///
951 /// - `f64` - Seconds elapsed since the previous update.
952 fn update(&mut self, delta_time: f64) {
953 PhysicsWorld3D::step(self, delta_time);
954 }
955}
956
957/// Implements `Default` for `PhysicsWorld3D` as an empty world.
958impl Default for PhysicsWorld3D {
959 /// Constructs a default [`PhysicsWorld3D`] value.
960 ///
961 /// # Returns
962 ///
963 /// - `PhysicsWorld3D` - A default-constructed instance with the documented initial state.
964 fn default() -> PhysicsWorld3D {
965 PhysicsWorld3D::with_config(PhysicsConfig3D::default())
966 }
967}