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