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