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