1use glam::{Vec2, Vec3, Quat, Mat3};
16use std::collections::HashMap;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
22pub struct RigidBodyHandle(pub u32);
23
24#[derive(Debug, Clone, Copy, PartialEq)]
27pub enum SleepState {
28 Awake,
29 Drowsy { timer: f32 },
30 Asleep,
31}
32
33#[derive(Debug, Clone)]
37pub struct RigidBody {
38 pub position: Vec3,
40 pub velocity: Vec3,
42 pub angular_vel: Vec3,
44 pub orientation: Quat,
46 pub mass: f32,
48 pub inv_mass: f32,
50 pub inv_inertia_local: Mat3,
52 pub force_accum: Vec3,
54 pub torque_accum: Vec3,
56 pub restitution: f32,
58 pub friction: f32,
60 pub linear_damping: f32,
62 pub angular_damping: f32,
64 pub sleep_state: SleepState,
66 pub collision_group: u32,
68 pub collision_mask: u32,
70 pub tag: u32,
72 pub shape: CollisionShape,
74}
75
76impl RigidBody {
77 pub fn dynamic(position: Vec3, mass: f32, shape: CollisionShape) -> Self {
79 let inv_inertia_local = shape.compute_inverse_inertia_tensor(mass);
80 Self {
81 position,
82 velocity: Vec3::ZERO,
83 angular_vel: Vec3::ZERO,
84 orientation: Quat::IDENTITY,
85 mass,
86 inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
87 inv_inertia_local,
88 force_accum: Vec3::ZERO,
89 torque_accum: Vec3::ZERO,
90 restitution: 0.3,
91 friction: 0.5,
92 linear_damping: 0.01,
93 angular_damping: 0.05,
94 sleep_state: SleepState::Awake,
95 collision_group: 1,
96 collision_mask: u32::MAX,
97 tag: 0,
98 shape,
99 }
100 }
101
102 pub fn static_body(position: Vec3, shape: CollisionShape) -> Self {
104 Self {
105 inv_mass: 0.0,
106 mass: f32::INFINITY,
107 inv_inertia_local: Mat3::ZERO,
108 ..Self::dynamic(position, 1.0, shape)
109 }
110 }
111
112 pub fn is_static(&self) -> bool { self.inv_mass < 1e-12 }
113 pub fn is_sleeping(&self) -> bool { matches!(self.sleep_state, SleepState::Asleep) }
114
115 pub fn wake(&mut self) {
117 self.sleep_state = SleepState::Awake;
118 }
119
120 pub fn apply_force(&mut self, force: Vec3) {
122 if !self.is_static() { self.force_accum += force; }
123 }
124
125 pub fn apply_torque(&mut self, torque: Vec3) {
127 if !self.is_static() { self.torque_accum += torque; }
128 }
129
130 pub fn apply_force_at_point(&mut self, force: Vec3, world_point: Vec3) {
132 self.apply_force(force);
133 let r = world_point - self.position;
134 self.apply_torque(r.cross(force));
135 }
136
137 pub fn apply_linear_impulse(&mut self, impulse: Vec3) {
139 self.velocity += impulse * self.inv_mass;
140 }
141
142 pub fn apply_angular_impulse(&mut self, impulse: Vec3) {
144 let iw = self.inv_inertia_world();
145 self.angular_vel += iw * impulse;
146 }
147
148 pub fn apply_impulse_at_point(&mut self, impulse: Vec3, world_point: Vec3) {
150 self.apply_linear_impulse(impulse);
151 let r = world_point - self.position;
152 self.apply_angular_impulse(r.cross(impulse));
153 }
154
155 pub fn clear_forces(&mut self) {
157 self.force_accum = Vec3::ZERO;
158 self.torque_accum = Vec3::ZERO;
159 }
160
161 pub fn inv_inertia_world(&self) -> Mat3 {
163 let rot = Mat3::from_quat(self.orientation);
164 let rot_t = rot.transpose();
165 rot * self.inv_inertia_local * rot_t
166 }
167
168 pub fn velocity_at_point(&self, world_point: Vec3) -> Vec3 {
170 let r = world_point - self.position;
171 self.velocity + self.angular_vel.cross(r)
172 }
173
174 pub fn integrate(&mut self, dt: f32, gravity: Vec3) {
176 if self.is_static() || self.is_sleeping() { return; }
177
178 let accel = self.force_accum * self.inv_mass + gravity;
180 self.velocity += accel * dt;
181 self.velocity *= (1.0 - self.linear_damping * dt).max(0.0);
182 self.position += self.velocity * dt;
183
184 let iw = self.inv_inertia_world();
186 let alpha = iw * self.torque_accum;
187 self.angular_vel += alpha * dt;
188 self.angular_vel *= (1.0 - self.angular_damping * dt).max(0.0);
189
190 let half_omega = self.angular_vel * 0.5;
192 let dq = Quat::from_xyzw(half_omega.x, half_omega.y, half_omega.z, 0.0) * self.orientation;
193 self.orientation = (self.orientation + dq * dt).normalize();
194
195 self.clear_forces();
196 }
197
198 pub fn kinetic_energy(&self) -> f32 {
200 let linear = 0.5 * self.mass * self.velocity.length_squared();
201 let rot_mat = Mat3::from_quat(self.orientation);
202 let inertia_local = pseudo_inverse_mat3(self.inv_inertia_local);
203 let ang_local = rot_mat.transpose() * self.angular_vel;
204 let rotational = 0.5 * ang_local.dot(inertia_local * ang_local);
205 linear + rotational
206 }
207}
208
209fn pseudo_inverse_mat3(m: Mat3) -> Mat3 {
211 let d = Vec3::new(m.x_axis.x, m.y_axis.y, m.z_axis.z);
213 Mat3::from_diagonal(Vec3::new(
214 if d.x.abs() > 1e-10 { 1.0 / d.x } else { 0.0 },
215 if d.y.abs() > 1e-10 { 1.0 / d.y } else { 0.0 },
216 if d.z.abs() > 1e-10 { 1.0 / d.z } else { 0.0 },
217 ))
218}
219
220#[derive(Debug, Clone)]
224pub enum CollisionShape {
225 Sphere { radius: f32 },
226 Box { half_extents: Vec3 },
227 Capsule { radius: f32, half_height: f32 },
228 ConvexHull(Vec<Vec3>),
229}
230
231impl CollisionShape {
232 pub fn sphere(radius: f32) -> Self { Self::Sphere { radius } }
233 pub fn box_shape(half: Vec3) -> Self { Self::Box { half_extents: half } }
234 pub fn capsule(radius: f32, half_height: f32) -> Self { Self::Capsule { radius, half_height } }
235
236 pub fn compute_inverse_inertia_tensor(&self, mass: f32) -> Mat3 {
238 if mass < 1e-12 { return Mat3::ZERO; }
239 let inv_m = 1.0 / mass;
240 match self {
241 Self::Sphere { radius } => {
242 let i = 2.0 / 5.0 * mass * radius * radius;
243 Mat3::from_diagonal(Vec3::splat(1.0 / i))
244 }
245 Self::Box { half_extents: h } => {
246 let ix = mass * (h.y * h.y + h.z * h.z) / 3.0;
247 let iy = mass * (h.x * h.x + h.z * h.z) / 3.0;
248 let iz = mass * (h.x * h.x + h.y * h.y) / 3.0;
249 Mat3::from_diagonal(Vec3::new(
250 if ix > 1e-10 { 1.0 / ix } else { 0.0 },
251 if iy > 1e-10 { 1.0 / iy } else { 0.0 },
252 if iz > 1e-10 { 1.0 / iz } else { 0.0 },
253 ))
254 }
255 Self::Capsule { radius, half_height } => {
256 let r2 = radius * radius;
257 let h2 = half_height * half_height;
258 let iy = mass * (r2 * 2.0 / 5.0 + h2 / 3.0 + r2 / 2.0);
259 let ixz = mass * (r2 * 2.0 / 5.0 + h2 / 3.0);
260 Mat3::from_diagonal(Vec3::new(
261 if ixz > 1e-10 { 1.0 / ixz } else { 0.0 },
262 if iy > 1e-10 { 1.0 / iy } else { 0.0 },
263 if ixz > 1e-10 { 1.0 / ixz } else { 0.0 },
264 ))
265 }
266 Self::ConvexHull(pts) => {
267 if pts.is_empty() { return Mat3::ZERO; }
268 let mut mn = Vec3::splat(f32::INFINITY);
270 let mut mx = Vec3::splat(f32::NEG_INFINITY);
271 for p in pts {
272 mn = mn.min(*p);
273 mx = mx.max(*p);
274 }
275 let h = (mx - mn) * 0.5;
276 let ix = mass * (h.y * h.y + h.z * h.z) / 3.0;
277 let iy = mass * (h.x * h.x + h.z * h.z) / 3.0;
278 let iz = mass * (h.x * h.x + h.y * h.y) / 3.0;
279 Mat3::from_diagonal(Vec3::new(
280 if ix > 1e-10 { 1.0 / ix } else { 0.0 },
281 if iy > 1e-10 { 1.0 / iy } else { 0.0 },
282 if iz > 1e-10 { 1.0 / iz } else { 0.0 },
283 ))
284 }
285 }
286 }
287
288 pub fn bounding_radius(&self) -> f32 {
290 match self {
291 Self::Sphere { radius } => *radius,
292 Self::Box { half_extents } => half_extents.length(),
293 Self::Capsule { radius, half_height } => radius + half_height,
294 Self::ConvexHull(pts) => pts.iter().map(|p| p.length()).fold(0.0_f32, f32::max),
295 }
296 }
297
298 pub fn aabb_half_extents(&self) -> Vec3 {
300 match self {
301 Self::Sphere { radius } => Vec3::splat(*radius),
302 Self::Box { half_extents } => *half_extents,
303 Self::Capsule { radius, half_height } => Vec3::new(*radius, half_height + radius, *radius),
304 Self::ConvexHull(pts) => {
305 let mut mx = Vec3::ZERO;
306 for p in pts { mx = mx.max(p.abs()); }
307 mx
308 }
309 }
310 }
311}
312
313#[derive(Debug, Clone, Copy)]
317pub struct ContactPoint {
318 pub point: Vec3,
320 pub normal: Vec3,
322 pub depth: f32,
324 pub cached_normal: f32,
326 pub cached_tangent: [f32; 2],
328}
329
330impl ContactPoint {
331 pub fn new(point: Vec3, normal: Vec3, depth: f32) -> Self {
332 Self { point, normal, depth, cached_normal: 0.0, cached_tangent: [0.0; 2] }
333 }
334}
335
336#[derive(Debug, Clone)]
338pub struct ContactManifold3D {
339 pub handle_a: RigidBodyHandle,
340 pub handle_b: RigidBodyHandle,
341 pub contacts: Vec<ContactPoint>,
343}
344
345impl ContactManifold3D {
346 pub fn new(a: RigidBodyHandle, b: RigidBodyHandle) -> Self {
347 Self { handle_a: a, handle_b: b, contacts: Vec::with_capacity(4) }
348 }
349
350 pub fn add_contact(&mut self, pt: ContactPoint) {
351 if self.contacts.len() < 4 {
352 self.contacts.push(pt);
353 }
354 }
355}
356
357pub struct CollisionDetector;
361
362impl CollisionDetector {
363 pub fn sphere_sphere(
365 pos_a: Vec3, ra: f32, ha: RigidBodyHandle,
366 pos_b: Vec3, rb: f32, hb: RigidBodyHandle,
367 ) -> Option<ContactManifold3D> {
368 let delta = pos_b - pos_a;
369 let dist = delta.length();
370 let sum_r = ra + rb;
371 if dist >= sum_r || dist < 1e-8 { return None; }
372 let normal = if dist > 1e-7 { delta / dist } else { Vec3::Y };
373 let mut m = ContactManifold3D::new(ha, hb);
374 m.add_contact(ContactPoint::new(
375 pos_a + normal * ra,
376 normal,
377 sum_r - dist,
378 ));
379 Some(m)
380 }
381
382 pub fn sphere_box(
384 sphere_pos: Vec3, sphere_r: f32, hs: RigidBodyHandle,
385 box_pos: Vec3, box_rot: Quat, box_half: Vec3, hb: RigidBodyHandle,
386 ) -> Option<ContactManifold3D> {
387 let box_inv_rot = box_rot.inverse();
389 let local_sphere = box_inv_rot * (sphere_pos - box_pos);
390
391 let clamped = local_sphere.clamp(-box_half, box_half);
393 let local_delta = local_sphere - clamped;
394 let dist = local_delta.length();
395
396 if dist >= sphere_r { return None; }
397
398 if dist < 1e-8 {
400 let overlap = box_half - local_sphere.abs();
402 let min_overlap_axis = if overlap.x < overlap.y && overlap.x < overlap.z {
403 Vec3::X * local_sphere.x.signum()
404 } else if overlap.y < overlap.z {
405 Vec3::Y * local_sphere.y.signum()
406 } else {
407 Vec3::Z * local_sphere.z.signum()
408 };
409 let world_normal = box_rot * min_overlap_axis;
410 let penetration = overlap.min_element() + sphere_r;
411 let mut m = ContactManifold3D::new(hs, hb);
412 m.add_contact(ContactPoint::new(sphere_pos - world_normal * sphere_r, world_normal, penetration));
413 return Some(m);
414 }
415
416 let local_normal = local_delta / dist;
417 let world_normal = box_rot * local_normal;
418 let depth = sphere_r - dist;
419 let contact_point = sphere_pos - world_normal * sphere_r;
420 let mut m = ContactManifold3D::new(hs, hb);
421 m.add_contact(ContactPoint::new(contact_point, world_normal, depth));
422 Some(m)
423 }
424
425 pub fn box_box(
427 pos_a: Vec3, rot_a: Quat, half_a: Vec3, ha: RigidBodyHandle,
428 pos_b: Vec3, rot_b: Quat, half_b: Vec3, hb: RigidBodyHandle,
429 ) -> Option<ContactManifold3D> {
430 let axes_a = [
431 rot_a * Vec3::X, rot_a * Vec3::Y, rot_a * Vec3::Z,
432 ];
433 let axes_b = [
434 rot_b * Vec3::X, rot_b * Vec3::Y, rot_b * Vec3::Z,
435 ];
436
437 let t = pos_b - pos_a;
438
439 let mut min_depth = f32::MAX;
440 let mut best_normal = Vec3::X;
441
442 let test_axes: Vec<Vec3> = {
444 let mut v: Vec<Vec3> = Vec::with_capacity(15);
445 v.extend_from_slice(&axes_a);
446 v.extend_from_slice(&axes_b);
447 for &aa in &axes_a {
448 for &ab in &axes_b {
449 let cross = aa.cross(ab);
450 if cross.length_squared() > 1e-10 {
451 v.push(cross.normalize());
452 }
453 }
454 }
455 v
456 };
457
458 for axis in test_axes {
459 let proj_a = project_box_onto_axis(half_a, axes_a, axis);
460 let proj_b = project_box_onto_axis(half_b, axes_b, axis);
461 let t_proj = t.dot(axis).abs();
462 let depth = proj_a + proj_b - t_proj;
463 if depth < 0.0 { return None; } if depth < min_depth {
465 min_depth = depth;
466 best_normal = if t.dot(axis) < 0.0 { -axis } else { axis };
468 }
469 }
470
471 let mut m = ContactManifold3D::new(ha, hb);
472 let contact = pos_a + best_normal * (min_depth * 0.5);
474 m.add_contact(ContactPoint::new(contact, best_normal, min_depth));
475 Some(m)
476 }
477
478 pub fn capsule_capsule(
480 pos_a: Vec3, axis_a: Vec3, r_a: f32, hh_a: f32, ha: RigidBodyHandle,
481 pos_b: Vec3, axis_b: Vec3, r_b: f32, hh_b: f32, hb: RigidBodyHandle,
482 ) -> Option<ContactManifold3D> {
483 let p1 = pos_a - axis_a * hh_a;
485 let p2 = pos_a + axis_a * hh_a;
486 let p3 = pos_b - axis_b * hh_b;
487 let p4 = pos_b + axis_b * hh_b;
488
489 let (cp_a, cp_b) = closest_points_on_segments(p1, p2, p3, p4);
490 let delta = cp_b - cp_a;
491 let dist = delta.length();
492 let sum_r = r_a + r_b;
493 if dist >= sum_r { return None; }
494
495 let normal = if dist > 1e-7 { delta / dist } else { Vec3::Y };
496 let depth = sum_r - dist;
497 let contact_point = cp_a + normal * r_a;
498
499 let mut m = ContactManifold3D::new(ha, hb);
500 m.add_contact(ContactPoint::new(contact_point, normal, depth));
501 Some(m)
502 }
503
504 pub fn detect(
506 body_a: &RigidBody, ha: RigidBodyHandle,
507 body_b: &RigidBody, hb: RigidBodyHandle,
508 ) -> Option<ContactManifold3D> {
509 match (&body_a.shape, &body_b.shape) {
510 (CollisionShape::Sphere { radius: ra }, CollisionShape::Sphere { radius: rb }) => {
511 Self::sphere_sphere(body_a.position, *ra, ha, body_b.position, *rb, hb)
512 }
513 (CollisionShape::Sphere { radius: rs }, CollisionShape::Box { half_extents }) => {
514 Self::sphere_box(body_a.position, *rs, ha, body_b.position, body_b.orientation, *half_extents, hb)
515 }
516 (CollisionShape::Box { half_extents }, CollisionShape::Sphere { radius: rs }) => {
517 let m = Self::sphere_box(body_b.position, *rs, hb, body_a.position, body_a.orientation, *half_extents, ha)?;
518 let mut flipped = ContactManifold3D::new(ha, hb);
520 for c in m.contacts {
521 flipped.add_contact(ContactPoint::new(c.point, -c.normal, c.depth));
522 }
523 Some(flipped)
524 }
525 (CollisionShape::Box { half_extents: ha_ext }, CollisionShape::Box { half_extents: hb_ext }) => {
526 Self::box_box(body_a.position, body_a.orientation, *ha_ext, ha, body_b.position, body_b.orientation, *hb_ext, hb)
527 }
528 (CollisionShape::Capsule { radius: ra, half_height: hha }, CollisionShape::Capsule { radius: rb, half_height: hhb }) => {
529 let axis_a = body_a.orientation * Vec3::Y;
530 let axis_b = body_b.orientation * Vec3::Y;
531 Self::capsule_capsule(body_a.position, axis_a, *ra, *hha, ha, body_b.position, axis_b, *rb, *hhb, hb)
532 }
533 _ => {
534 let ra = body_a.shape.bounding_radius();
536 let rb = body_b.shape.bounding_radius();
537 Self::sphere_sphere(body_a.position, ra, ha, body_b.position, rb, hb)
538 }
539 }
540 }
541}
542
543fn project_box_onto_axis(half: Vec3, axes: [Vec3; 3], axis: Vec3) -> f32 {
545 half.x * axes[0].dot(axis).abs()
546 + half.y * axes[1].dot(axis).abs()
547 + half.z * axes[2].dot(axis).abs()
548}
549
550fn closest_points_on_segments(p1: Vec3, p2: Vec3, p3: Vec3, p4: Vec3) -> (Vec3, Vec3) {
552 let d1 = p2 - p1;
553 let d2 = p4 - p3;
554 let r = p1 - p3;
555 let a = d1.dot(d1);
556 let e = d2.dot(d2);
557 let f = d2.dot(r);
558
559 let (s, t) = if a < 1e-10 && e < 1e-10 {
560 (0.0, 0.0)
561 } else if a < 1e-10 {
562 (0.0, (f / e).clamp(0.0, 1.0))
563 } else {
564 let c = d1.dot(r);
565 if e < 1e-10 {
566 ((-c / a).clamp(0.0, 1.0), 0.0)
567 } else {
568 let b = d1.dot(d2);
569 let denom = a * e - b * b;
570 let s = if denom.abs() > 1e-10 { ((b * f - c * e) / denom).clamp(0.0, 1.0) } else { 0.0 };
571 let t = (b * s + f) / e;
572 if t < 0.0 {
573 ((-c / a).clamp(0.0, 1.0), 0.0)
574 } else if t > 1.0 {
575 (((b - c) / a).clamp(0.0, 1.0), 1.0)
576 } else {
577 (s, t)
578 }
579 }
580 };
581
582 (p1 + d1 * s, p3 + d2 * t)
583}
584
585#[derive(Debug, Clone)]
589pub struct BroadphaseEntry {
590 pub handle: RigidBodyHandle,
591 pub min_x: f32,
592 pub max_x: f32,
593 pub min_y: f32,
594 pub max_y: f32,
595 pub min_z: f32,
596 pub max_z: f32,
597}
598
599pub struct Broadphase {
601 entries: Vec<BroadphaseEntry>,
602}
603
604impl Broadphase {
605 pub fn new() -> Self { Self { entries: Vec::new() } }
606
607 pub fn rebuild(&mut self, bodies: &HashMap<RigidBodyHandle, RigidBody>) {
609 self.entries.clear();
610 for (h, b) in bodies {
611 let half = b.shape.aabb_half_extents();
612 self.entries.push(BroadphaseEntry {
613 handle: *h,
614 min_x: b.position.x - half.x,
615 max_x: b.position.x + half.x,
616 min_y: b.position.y - half.y,
617 max_y: b.position.y + half.y,
618 min_z: b.position.z - half.z,
619 max_z: b.position.z + half.z,
620 });
621 }
622 let n = self.entries.len();
624 for i in 1..n {
625 let mut j = i;
626 while j > 0 && self.entries[j - 1].min_x > self.entries[j].min_x {
627 self.entries.swap(j - 1, j);
628 j -= 1;
629 }
630 }
631 }
632
633 pub fn overlapping_pairs(&self) -> Vec<(RigidBodyHandle, RigidBodyHandle)> {
635 let mut pairs = Vec::new();
636 let n = self.entries.len();
637 for i in 0..n {
638 for j in (i + 1)..n {
639 let a = &self.entries[i];
640 let b = &self.entries[j];
641 if b.min_x > a.max_x { break; }
643 if a.min_y > b.max_y || b.min_y > a.max_y { continue; }
645 if a.min_z > b.max_z || b.min_z > a.max_z { continue; }
646 pairs.push((a.handle, b.handle));
647 }
648 }
649 pairs
650 }
651
652 pub fn entry_count(&self) -> usize { self.entries.len() }
653}
654
655impl Default for Broadphase {
656 fn default() -> Self { Self::new() }
657}
658
659#[derive(Debug, Clone, Copy)]
663pub struct Ray {
664 pub origin: Vec3,
665 pub direction: Vec3, }
667
668impl Ray {
669 pub fn new(origin: Vec3, direction: Vec3) -> Self {
670 Self { origin, direction: direction.normalize_or_zero() }
671 }
672
673 pub fn at(&self, t: f32) -> Vec3 { self.origin + self.direction * t }
674}
675
676#[derive(Debug, Clone, Copy)]
678pub struct RayHit {
679 pub handle: RigidBodyHandle,
680 pub t: f32, pub point: Vec3,
682 pub normal: Vec3,
683}
684
685pub struct RayCast;
687
688impl RayCast {
689 pub fn ray_sphere(ray: &Ray, center: Vec3, radius: f32) -> Option<f32> {
691 let oc = ray.origin - center;
692 let b = oc.dot(ray.direction);
693 let c = oc.dot(oc) - radius * radius;
694 let disc = b * b - c;
695 if disc < 0.0 { return None; }
696 let sqrt_disc = disc.sqrt();
697 let t1 = -b - sqrt_disc;
698 let t2 = -b + sqrt_disc;
699 if t1 > 1e-4 { Some(t1) }
700 else if t2 > 1e-4 { Some(t2) }
701 else { None }
702 }
703
704 pub fn ray_box(ray: &Ray, box_pos: Vec3, box_rot: Quat, box_half: Vec3) -> Option<f32> {
706 let inv_rot = box_rot.inverse();
707 let local_origin = inv_rot * (ray.origin - box_pos);
708 let local_dir = inv_rot * ray.direction;
709
710 let t_min_arr = (-box_half - local_origin) / local_dir;
711 let t_max_arr = ( box_half - local_origin) / local_dir;
712
713 let t_min_v = t_min_arr.min(t_max_arr);
714 let t_max_v = t_min_arr.max(t_max_arr);
715
716 let t_enter = t_min_v.x.max(t_min_v.y).max(t_min_v.z);
717 let t_exit = t_max_v.x.min(t_max_v.y).min(t_max_v.z);
718
719 if t_enter > t_exit || t_exit < 1e-4 { return None; }
720 let t = if t_enter > 1e-4 { t_enter } else { t_exit };
721 Some(t)
722 }
723
724 pub fn ray_capsule(ray: &Ray, cap_pos: Vec3, cap_axis: Vec3, radius: f32, half_height: f32) -> Option<f32> {
726 let p = ray.origin - cap_pos;
728 let axis = cap_axis.normalize_or_zero();
729 let d = ray.direction - ray.direction.dot(axis) * axis;
730 let e = p - p.dot(axis) * axis;
731
732 let a = d.dot(d);
733 let b = 2.0 * d.dot(e);
734 let c = e.dot(e) - radius * radius;
735
736 let mut best_t = f32::MAX;
737
738 if a > 1e-10 {
739 let disc = b * b - 4.0 * a * c;
740 if disc >= 0.0 {
741 let sqrt_d = disc.sqrt();
742 for t in [(-b - sqrt_d) / (2.0 * a), (-b + sqrt_d) / (2.0 * a)] {
743 if t > 1e-4 {
744 let pt = ray.at(t) - cap_pos;
745 let h = pt.dot(axis);
746 if h.abs() <= half_height && t < best_t {
747 best_t = t;
748 }
749 }
750 }
751 }
752 }
753
754 for sign in [-1.0_f32, 1.0] {
756 let cap_center = cap_pos + axis * (sign * half_height);
757 if let Some(t) = Self::ray_sphere(ray, cap_center, radius) {
758 if t < best_t { best_t = t; }
759 }
760 }
761
762 if best_t < f32::MAX { Some(best_t) } else { None }
763 }
764
765 pub fn ray_vs_body(ray: &Ray, body: &RigidBody) -> Option<f32> {
767 match &body.shape {
768 CollisionShape::Sphere { radius } =>
769 Self::ray_sphere(ray, body.position, *radius),
770 CollisionShape::Box { half_extents } =>
771 Self::ray_box(ray, body.position, body.orientation, *half_extents),
772 CollisionShape::Capsule { radius, half_height } => {
773 let axis = body.orientation * Vec3::Y;
774 Self::ray_capsule(ray, body.position, axis, *radius, *half_height)
775 }
776 CollisionShape::ConvexHull(_) => {
777 let r = body.shape.bounding_radius();
779 Self::ray_sphere(ray, body.position, r)
780 }
781 }
782 }
783}
784
785pub struct ImpulseResolver {
790 pub iterations: u32,
792 pub baumgarte: f32,
794 pub slop: f32,
796 pub warm_start: bool,
798}
799
800impl Default for ImpulseResolver {
801 fn default() -> Self {
802 Self { iterations: 10, baumgarte: 0.2, slop: 0.005, warm_start: true }
803 }
804}
805
806impl ImpulseResolver {
807 pub fn new() -> Self { Self::default() }
808
809 pub fn with_iterations(mut self, n: u32) -> Self { self.iterations = n; self }
810
811 pub fn resolve_contact(
813 &self,
814 a: &mut RigidBody, b: &mut RigidBody,
815 contact: &mut ContactPoint,
816 dt: f32,
817 ) {
818 let ra = contact.point - a.position;
819 let rb = contact.point - b.position;
820 let n = contact.normal;
821
822 let va = a.velocity_at_point(contact.point);
823 let vb = b.velocity_at_point(contact.point);
824 let rel_vel = vb - va;
825 let vn = rel_vel.dot(n);
826
827 if vn > 0.0 { return; }
829
830 let e = a.restitution.min(b.restitution);
831 let restitution_term = if vn.abs() > 1.0 { -e * vn } else { 0.0 };
832 let baumgarte_bias = self.baumgarte / dt * (contact.depth - self.slop).max(0.0);
833
834 let iwa = a.inv_inertia_world();
836 let iwb = b.inv_inertia_world();
837 let k_n = a.inv_mass + b.inv_mass
838 + (iwa * ra.cross(n)).cross(ra).dot(n)
839 + (iwb * rb.cross(n)).cross(rb).dot(n);
840
841 if k_n < 1e-10 { return; }
842
843 let lambda_n = -(vn + restitution_term + baumgarte_bias) / k_n;
844 let prev_n = contact.cached_normal;
845 let new_n = (prev_n + lambda_n).max(0.0);
846 let actual_n = new_n - prev_n;
847 contact.cached_normal = new_n;
848
849 let impulse_n = n * actual_n;
850 a.apply_impulse_at_point(-impulse_n, contact.point);
851 b.apply_impulse_at_point( impulse_n, contact.point);
852
853 let rel_vel2 = b.velocity_at_point(contact.point) - a.velocity_at_point(contact.point);
855 let tangential = rel_vel2 - n * rel_vel2.dot(n);
856 let tangential_len = tangential.length();
857 if tangential_len < 1e-7 { return; }
858 let t1 = tangential / tangential_len;
859 let t2 = n.cross(t1);
860
861 let mu = (a.friction + b.friction) * 0.5;
862 let max_friction = mu * contact.cached_normal;
863
864 for (ti, tangent) in [t1, t2].iter().enumerate() {
865 let k_t = a.inv_mass + b.inv_mass
866 + (iwa * ra.cross(*tangent)).cross(ra).dot(*tangent)
867 + (iwb * rb.cross(*tangent)).cross(rb).dot(*tangent);
868 if k_t < 1e-10 { continue; }
869 let vt = rel_vel2.dot(*tangent);
870 let lambda_t = -vt / k_t;
871 let prev_t = contact.cached_tangent[ti];
872 let new_t = (prev_t + lambda_t).clamp(-max_friction, max_friction);
873 let actual_t = new_t - prev_t;
874 contact.cached_tangent[ti] = new_t;
875 let imp_t = *tangent * actual_t;
876 a.apply_impulse_at_point(-imp_t, contact.point);
877 b.apply_impulse_at_point( imp_t, contact.point);
878 }
879 }
880
881 pub fn resolve_manifold(
883 &self,
884 a: &mut RigidBody, b: &mut RigidBody,
885 manifold: &mut ContactManifold3D,
886 dt: f32,
887 ) {
888 if self.warm_start {
890 for contact in &manifold.contacts {
891 let impulse_n = contact.normal * contact.cached_normal * 0.8;
892 a.apply_impulse_at_point(-impulse_n, contact.point);
893 b.apply_impulse_at_point( impulse_n, contact.point);
894 }
895 }
896
897 for _ in 0..self.iterations {
898 for contact in &mut manifold.contacts {
899 self.resolve_contact(a, b, contact, dt);
900 }
901 }
902 }
903}
904
905pub struct SleepSystem {
909 pub energy_threshold: f32,
911 pub sleep_delay: f32,
913 drowsy_timers: HashMap<RigidBodyHandle, f32>,
915}
916
917impl SleepSystem {
918 pub fn new(energy_threshold: f32, sleep_delay: f32) -> Self {
919 Self { energy_threshold, sleep_delay, drowsy_timers: HashMap::new() }
920 }
921
922 pub fn update(
924 &mut self,
925 bodies: &mut HashMap<RigidBodyHandle, RigidBody>,
926 dt: f32,
927 ) -> Vec<RigidBodyHandle> {
928 let mut newly_slept = Vec::new();
929
930 for (h, body) in bodies.iter_mut() {
931 if body.is_static() { continue; }
932
933 let ke = body.kinetic_energy();
934 if ke < self.energy_threshold {
935 let timer = self.drowsy_timers.entry(*h).or_insert(0.0);
936 *timer += dt;
937 if *timer >= self.sleep_delay {
938 body.sleep_state = SleepState::Asleep;
939 body.velocity = Vec3::ZERO;
940 body.angular_vel = Vec3::ZERO;
941 newly_slept.push(*h);
942 } else {
943 body.sleep_state = SleepState::Drowsy { timer: *timer };
944 }
945 } else {
946 self.drowsy_timers.remove(h);
947 body.sleep_state = SleepState::Awake;
948 }
949 }
950
951 newly_slept
952 }
953
954 pub fn wake_body(
956 &mut self,
957 handle: RigidBodyHandle,
958 bodies: &mut HashMap<RigidBodyHandle, RigidBody>,
959 manifolds: &[ContactManifold3D],
960 ) {
961 let mut to_wake = vec![handle];
962 let mut visited = std::collections::HashSet::new();
963
964 while let Some(h) = to_wake.pop() {
965 if visited.contains(&h) { continue; }
966 visited.insert(h);
967
968 if let Some(body) = bodies.get_mut(&h) {
969 body.wake();
970 self.drowsy_timers.remove(&h);
971 }
972
973 for manifold in manifolds {
975 if manifold.handle_a == h && !visited.contains(&manifold.handle_b) {
976 to_wake.push(manifold.handle_b);
977 }
978 if manifold.handle_b == h && !visited.contains(&manifold.handle_a) {
979 to_wake.push(manifold.handle_a);
980 }
981 }
982 }
983 }
984
985 pub fn wake_in_region(
987 &mut self,
988 region_min: Vec3, region_max: Vec3,
989 bodies: &mut HashMap<RigidBodyHandle, RigidBody>,
990 manifolds: &[ContactManifold3D],
991 ) {
992 let to_wake: Vec<RigidBodyHandle> = bodies.iter()
993 .filter(|(_, b)| {
994 let h = b.shape.aabb_half_extents();
995 b.position.x + h.x >= region_min.x && b.position.x - h.x <= region_max.x &&
996 b.position.y + h.y >= region_min.y && b.position.y - h.y <= region_max.y &&
997 b.position.z + h.z >= region_min.z && b.position.z - h.z <= region_max.z
998 })
999 .map(|(h, _)| *h)
1000 .collect();
1001
1002 for h in to_wake {
1003 self.wake_body(h, bodies, manifolds);
1004 }
1005 }
1006
1007 pub fn sleeping_count(&self, bodies: &HashMap<RigidBodyHandle, RigidBody>) -> usize {
1008 bodies.values().filter(|b| b.is_sleeping()).count()
1009 }
1010}
1011
1012pub struct PhysicsWorld3D {
1016 pub bodies: HashMap<RigidBodyHandle, RigidBody>,
1017 pub gravity: Vec3,
1018 pub fixed_dt: f32,
1019 pub substeps: u32,
1020 accumulator: f32,
1021 next_id: u32,
1022 broadphase: Broadphase,
1023 resolver: ImpulseResolver,
1024 pub sleep_system: SleepSystem,
1025 manifolds: Vec<ContactManifold3D>,
1026}
1027
1028impl PhysicsWorld3D {
1029 pub fn new() -> Self {
1030 Self {
1031 bodies: HashMap::new(),
1032 gravity: Vec3::new(0.0, -9.81, 0.0),
1033 fixed_dt: 1.0 / 60.0,
1034 substeps: 2,
1035 accumulator: 0.0,
1036 next_id: 1,
1037 broadphase: Broadphase::new(),
1038 resolver: ImpulseResolver::default(),
1039 sleep_system: SleepSystem::new(0.1, 0.5),
1040 manifolds: Vec::new(),
1041 }
1042 }
1043
1044 pub fn add_body(&mut self, body: RigidBody) -> RigidBodyHandle {
1046 let h = RigidBodyHandle(self.next_id);
1047 self.next_id += 1;
1048 self.bodies.insert(h, body);
1049 h
1050 }
1051
1052 pub fn remove_body(&mut self, h: RigidBodyHandle) {
1054 self.bodies.remove(&h);
1055 }
1056
1057 pub fn body(&self, h: RigidBodyHandle) -> Option<&RigidBody> { self.bodies.get(&h) }
1058 pub fn body_mut(&mut self, h: RigidBodyHandle) -> Option<&mut RigidBody> { self.bodies.get_mut(&h) }
1059
1060 pub fn body_count(&self) -> usize { self.bodies.len() }
1061
1062 pub fn raycast(&self, ray: &Ray) -> Option<RayHit> {
1064 let mut best: Option<RayHit> = None;
1065 for (h, body) in &self.bodies {
1066 if let Some(t) = RayCast::ray_vs_body(ray, body) {
1067 let is_better = best.as_ref().map(|b| t < b.t).unwrap_or(true);
1068 if is_better {
1069 let point = ray.at(t);
1070 let normal = (point - body.position).normalize_or_zero();
1071 best = Some(RayHit { handle: *h, t, point, normal });
1072 }
1073 }
1074 }
1075 best
1076 }
1077
1078 pub fn step(&mut self, dt: f32) {
1080 self.accumulator += dt;
1081 let fixed = self.fixed_dt;
1082 while self.accumulator >= fixed {
1083 let sub_dt = fixed / self.substeps as f32;
1084 for _ in 0..self.substeps {
1085 self.fixed_step(sub_dt);
1086 }
1087 self.accumulator -= fixed;
1088 }
1089 }
1090
1091 fn fixed_step(&mut self, dt: f32) {
1092 let gravity = self.gravity;
1093
1094 for body in self.bodies.values_mut() {
1096 body.integrate(dt, gravity);
1097 }
1098
1099 self.broadphase.rebuild(&self.bodies);
1101 let pairs = self.broadphase.overlapping_pairs();
1102
1103 self.manifolds.clear();
1105 for (ha, hb) in pairs {
1106 let (a_static, b_static) = {
1108 let a = self.bodies.get(&ha);
1109 let b = self.bodies.get(&hb);
1110 (a.map(|b| b.is_static()).unwrap_or(true), b.map(|b| b.is_static()).unwrap_or(true))
1111 };
1112 if a_static && b_static { continue; }
1113
1114 let (a_group, a_mask, b_group, b_mask) = {
1116 let a = self.bodies.get(&ha);
1117 let b = self.bodies.get(&hb);
1118 (
1119 a.map(|b| b.collision_group).unwrap_or(0),
1120 a.map(|b| b.collision_mask).unwrap_or(0),
1121 b.map(|b| b.collision_group).unwrap_or(0),
1122 b.map(|b| b.collision_mask).unwrap_or(0),
1123 )
1124 };
1125 if a_group & b_mask == 0 && b_group & a_mask == 0 { continue; }
1126
1127 let body_a_copy = self.bodies.get(&ha).cloned();
1128 let body_b_copy = self.bodies.get(&hb).cloned();
1129
1130 if let (Some(ba), Some(bb)) = (body_a_copy, body_b_copy) {
1131 if let Some(mut manifold) = CollisionDetector::detect(&ba, ha, &bb, hb) {
1132 if ba.is_sleeping() || bb.is_sleeping() {
1134 if let Some(b) = self.bodies.get_mut(&ha) { b.wake(); }
1135 if let Some(b) = self.bodies.get_mut(&hb) { b.wake(); }
1136 }
1137
1138 let body_a = self.bodies.get_mut(&ha).unwrap() as *mut RigidBody;
1140 let body_b = self.bodies.get_mut(&hb).unwrap() as *mut RigidBody;
1141 unsafe {
1143 self.resolver.resolve_manifold(&mut *body_a, &mut *body_b, &mut manifold, dt);
1144 }
1145
1146 self.manifolds.push(manifold);
1147 }
1148 }
1149 }
1150
1151 self.sleep_system.update(&mut self.bodies, dt);
1153 }
1154
1155 pub fn manifolds(&self) -> &[ContactManifold3D] { &self.manifolds }
1156
1157 pub fn sleeping_count(&self) -> usize {
1158 self.sleep_system.sleeping_count(&self.bodies)
1159 }
1160
1161 pub fn apply_global_force(&mut self, force: Vec3) {
1163 for body in self.bodies.values_mut() {
1164 if !body.is_static() && !body.is_sleeping() {
1165 body.apply_force(force);
1166 }
1167 }
1168 }
1169}
1170
1171impl Default for PhysicsWorld3D {
1172 fn default() -> Self { Self::new() }
1173}
1174
1175#[derive(Debug, Clone, Copy, PartialEq)]
1178pub enum JointType {
1179 Distance, Revolute, Prismatic, Fixed, Spring, BallSocket, Weld, Pulley, Gear,
1180}
1181
1182#[derive(Debug, Clone, Copy)]
1183pub struct JointAnchor {
1184 pub local_a: Vec2,
1185 pub local_b: Vec2,
1186}
1187
1188impl JointAnchor {
1189 pub fn at_origins() -> Self { Self { local_a: Vec2::ZERO, local_b: Vec2::ZERO } }
1190 pub fn new(local_a: Vec2, local_b: Vec2) -> Self { Self { local_a, local_b } }
1191}
1192
1193#[derive(Debug, Clone, Copy)]
1194pub struct JointLimits {
1195 pub min: f32,
1196 pub max: f32,
1197 pub motor_speed: f32,
1198 pub max_motor_force: f32,
1199 pub motor_enabled: bool,
1200}
1201
1202impl JointLimits {
1203 pub fn none() -> Self {
1204 Self { min: f32::NEG_INFINITY, max: f32::INFINITY, motor_speed: 0.0, max_motor_force: 0.0, motor_enabled: false }
1205 }
1206
1207 pub fn range(min: f32, max: f32) -> Self {
1208 Self { min, max, motor_speed: 0.0, max_motor_force: 0.0, motor_enabled: false }
1209 }
1210
1211 pub fn with_motor(mut self, speed: f32, max_force: f32) -> Self {
1212 self.motor_speed = speed; self.max_motor_force = max_force; self.motor_enabled = true; self
1213 }
1214
1215 pub fn clamp(&self, value: f32) -> f32 { value.clamp(self.min, self.max) }
1216}
1217
1218#[derive(Debug, Clone)]
1219pub struct Joint {
1220 pub id: u32,
1221 pub kind: JointType,
1222 pub body_a: u32,
1223 pub body_b: Option<u32>,
1224 pub anchor: JointAnchor,
1225 pub limits: JointLimits,
1226 pub stiffness: f32,
1227 pub damping: f32,
1228 pub rest_length: f32,
1229 pub frequency: f32,
1230 pub collide_connected: bool,
1231 pub break_force: f32,
1232 pub broken: bool,
1233 pub gear_ratio: f32,
1234 pub pulley_ratio: f32,
1235 pub pulley_anchor_a: Vec2,
1236 pub pulley_anchor_b: Vec2,
1237}
1238
1239impl Joint {
1240 pub fn distance(id: u32, body_a: u32, body_b: u32, local_a: Vec2, local_b: Vec2, length: f32) -> Self {
1241 Self {
1242 id, kind: JointType::Distance, body_a, body_b: Some(body_b),
1243 anchor: JointAnchor::new(local_a, local_b),
1244 limits: JointLimits::range(length * 0.9, length * 1.1),
1245 stiffness: 1.0, damping: 0.1, rest_length: length,
1246 frequency: 10.0, collide_connected: false,
1247 break_force: 0.0, broken: false, gear_ratio: 1.0,
1248 pulley_ratio: 1.0, pulley_anchor_a: Vec2::ZERO, pulley_anchor_b: Vec2::ZERO,
1249 }
1250 }
1251
1252 pub fn spring(id: u32, body_a: u32, body_b: u32, local_a: Vec2, local_b: Vec2, rest_len: f32, stiffness: f32, damping: f32) -> Self {
1253 Self {
1254 id, kind: JointType::Spring, body_a, body_b: Some(body_b),
1255 anchor: JointAnchor::new(local_a, local_b),
1256 limits: JointLimits::none(),
1257 stiffness, damping, rest_length: rest_len,
1258 frequency: 5.0, collide_connected: true,
1259 break_force: 0.0, broken: false, gear_ratio: 1.0,
1260 pulley_ratio: 1.0, pulley_anchor_a: Vec2::ZERO, pulley_anchor_b: Vec2::ZERO,
1261 }
1262 }
1263
1264 pub fn revolute(id: u32, body_a: u32, body_b: u32, anchor: Vec2) -> Self {
1265 Self {
1266 id, kind: JointType::Revolute, body_a, body_b: Some(body_b),
1267 anchor: JointAnchor { local_a: anchor, local_b: anchor },
1268 limits: JointLimits::none(),
1269 stiffness: 1.0, damping: 0.05, rest_length: 0.0,
1270 frequency: 10.0, collide_connected: false,
1271 break_force: 0.0, broken: false, gear_ratio: 1.0,
1272 pulley_ratio: 1.0, pulley_anchor_a: Vec2::ZERO, pulley_anchor_b: Vec2::ZERO,
1273 }
1274 }
1275
1276 pub fn fixed(id: u32, body_a: u32, body_b: u32) -> Self {
1277 Self {
1278 id, kind: JointType::Fixed, body_a, body_b: Some(body_b),
1279 anchor: JointAnchor::at_origins(),
1280 limits: JointLimits::none(),
1281 stiffness: 1.0, damping: 1.0, rest_length: 0.0,
1282 frequency: 60.0, collide_connected: false,
1283 break_force: 0.0, broken: false, gear_ratio: 1.0,
1284 pulley_ratio: 1.0, pulley_anchor_a: Vec2::ZERO, pulley_anchor_b: Vec2::ZERO,
1285 }
1286 }
1287
1288 pub fn with_limits(mut self, min: f32, max: f32) -> Self { self.limits = JointLimits::range(min, max); self }
1289 pub fn with_motor(mut self, speed: f32, max_force: f32) -> Self { self.limits = self.limits.with_motor(speed, max_force); self }
1290 pub fn with_break_force(mut self, force: f32) -> Self { self.break_force = force; self }
1291 pub fn collide_connected(mut self) -> Self { self.collide_connected = true; self }
1292 pub fn is_active(&self) -> bool { !self.broken }
1293}
1294
1295#[derive(Debug, Clone, Default)]
1296pub struct JointImpulse {
1297 pub impulse: Vec2,
1298 pub angular_impulse: f32,
1299 pub lambda: f32,
1300}
1301
1302#[derive(Debug, Clone)]
1304pub struct JointSolver {
1305 pub iterations: u32,
1306 pub position_slop: f32,
1307 pub baumgarte: f32,
1308}
1309
1310impl JointSolver {
1311 pub fn new() -> Self { Self { iterations: 10, position_slop: 0.005, baumgarte: 0.2 } }
1312
1313 pub fn solve_distance(
1314 &self,
1315 pos_a: &mut Vec2, vel_a: &mut Vec2, inv_mass_a: f32,
1316 pos_b: &mut Vec2, vel_b: &mut Vec2, inv_mass_b: f32,
1317 rest_len: f32, stiffness: f32, damping: f32, dt: f32,
1318 ) {
1319 let delta = *pos_b - *pos_a;
1320 let dist = delta.length();
1321 if dist < 1e-6 { return; }
1322 let dir = delta / dist;
1323 let stretch = dist - rest_len;
1324 let relative_vel = (*vel_b - *vel_a).dot(dir);
1325 let force_mag = -stiffness * stretch - damping * relative_vel;
1326 let total_inv_mass = inv_mass_a + inv_mass_b;
1327 if total_inv_mass < 1e-6 { return; }
1328 let impulse = dir * (-force_mag * dt / total_inv_mass);
1329 *vel_a -= impulse * inv_mass_a;
1330 *vel_b += impulse * inv_mass_b;
1331 }
1332
1333 pub fn solve_rigid_distance(
1334 &self,
1335 pos_a: &mut Vec2, vel_a: &mut Vec2, inv_mass_a: f32,
1336 pos_b: &mut Vec2, vel_b: &mut Vec2, inv_mass_b: f32,
1337 target_dist: f32, dt: f32,
1338 ) {
1339 for _ in 0..self.iterations {
1340 let delta = *pos_b - *pos_a;
1341 let dist = delta.length();
1342 if dist < 1e-6 { continue; }
1343 let dir = delta / dist;
1344 let error = dist - target_dist;
1345 let total_inv_mass = inv_mass_a + inv_mass_b;
1346 if total_inv_mass < 1e-6 { break; }
1347 let correction = dir * (error * self.baumgarte / (total_inv_mass * dt));
1348 *pos_a += correction * inv_mass_a;
1349 *pos_b -= correction * inv_mass_b;
1350 let vel_along = (*vel_b - *vel_a).dot(dir);
1351 let lambda = -vel_along / total_inv_mass;
1352 *vel_a -= dir * (lambda * inv_mass_a);
1353 *vel_b += dir * (lambda * inv_mass_b);
1354 }
1355 }
1356}
1357
1358impl Default for JointSolver {
1359 fn default() -> Self { Self::new() }
1360}
1361
1362#[derive(Debug, Clone)]
1365pub struct RagdollBone {
1366 pub name: String,
1367 pub body_id: u32,
1368 pub position: Vec2,
1369 pub velocity: Vec2,
1370 pub angle: f32,
1371 pub angular_vel: f32,
1372 pub mass: f32,
1373 pub inv_mass: f32,
1374 pub half_extents: Vec2,
1375 pub damping: f32,
1376 pub restitution: f32,
1377 pub friction: f32,
1378}
1379
1380impl RagdollBone {
1381 pub fn new(name: impl Into<String>, id: u32, pos: Vec2, mass: f32, half: Vec2) -> Self {
1382 Self {
1383 name: name.into(), body_id: id, position: pos,
1384 velocity: Vec2::ZERO, angle: 0.0, angular_vel: 0.0,
1385 mass, inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
1386 half_extents: half, damping: 0.1, restitution: 0.3, friction: 0.5,
1387 }
1388 }
1389
1390 pub fn apply_impulse(&mut self, impulse: Vec2, point: Vec2) {
1391 self.velocity += impulse * self.inv_mass;
1392 let r = point - self.position;
1393 let inertia_inv = 1.0 / (self.mass * (self.half_extents.x.powi(2) + self.half_extents.y.powi(2)) / 3.0).max(1e-6);
1394 self.angular_vel += (r.x * impulse.y - r.y * impulse.x) * inertia_inv;
1395 }
1396
1397 pub fn integrate(&mut self, dt: f32, gravity: Vec2) {
1398 if self.inv_mass <= 0.0 { return; }
1399 self.velocity += gravity * dt;
1400 self.velocity *= (1.0 - self.damping * dt).max(0.0);
1401 self.position += self.velocity * dt;
1402 self.angle += self.angular_vel * dt;
1403 self.angular_vel *= (1.0 - self.damping * dt).max(0.0);
1404 }
1405
1406 pub fn world_point(&self, local: Vec2) -> Vec2 {
1407 let (sin, cos) = self.angle.sin_cos();
1408 self.position + Vec2::new(local.x * cos - local.y * sin, local.x * sin + local.y * cos)
1409 }
1410}
1411
1412#[derive(Debug, Clone)]
1415pub struct Ragdoll {
1416 pub bones: Vec<RagdollBone>,
1417 pub joints: Vec<Joint>,
1418 pub active: bool,
1419 pub gravity: Vec2,
1420 solver: JointSolver,
1421}
1422
1423impl Ragdoll {
1424 pub fn new() -> Self {
1425 Self { bones: Vec::new(), joints: Vec::new(), active: false, gravity: Vec2::new(0.0, -9.81), solver: JointSolver::new() }
1426 }
1427
1428 pub fn add_bone(&mut self, bone: RagdollBone) -> u32 {
1429 let id = self.bones.len() as u32;
1430 self.bones.push(bone);
1431 id
1432 }
1433
1434 pub fn add_joint(&mut self, joint: Joint) { self.joints.push(joint); }
1435
1436 pub fn activate(&mut self, impulse: Vec2, contact_bone: usize) {
1437 self.active = true;
1438 if let Some(bone) = self.bones.get_mut(contact_bone) {
1439 bone.apply_impulse(impulse, bone.position);
1440 }
1441 }
1442
1443 pub fn deactivate(&mut self) {
1444 self.active = false;
1445 for bone in &mut self.bones {
1446 bone.velocity = Vec2::ZERO;
1447 bone.angular_vel = 0.0;
1448 }
1449 }
1450
1451 pub fn update(&mut self, dt: f32) {
1452 if !self.active { return; }
1453 for bone in &mut self.bones { bone.integrate(dt, self.gravity); }
1454
1455 let joint_snapshot: Vec<Joint> = self.joints.iter().filter(|j| j.is_active()).cloned().collect();
1456 for joint in &joint_snapshot {
1457 let Some(body_b_id) = joint.body_b else { continue };
1458 let a_idx = self.bones.iter().position(|b| b.body_id == joint.body_a);
1459 let b_idx = self.bones.iter().position(|b| b.body_id == body_b_id);
1460 if a_idx.is_none() || b_idx.is_none() { continue; }
1461 let ai = a_idx.unwrap();
1462 let bi = b_idx.unwrap();
1463 match joint.kind {
1464 JointType::Distance | JointType::Spring => {
1465 let (left, right) = self.bones.split_at_mut(ai.max(bi));
1466 let (bone_a, bone_b) = if ai < bi { (&mut left[ai], &mut right[0]) } else { (&mut right[0], &mut left[bi]) };
1467 self.solver.solve_distance(
1468 &mut bone_a.position, &mut bone_a.velocity, bone_a.inv_mass,
1469 &mut bone_b.position, &mut bone_b.velocity, bone_b.inv_mass,
1470 joint.rest_length, joint.stiffness, joint.damping, dt,
1471 );
1472 }
1473 JointType::Fixed | JointType::Weld => {
1474 let (left, right) = self.bones.split_at_mut(ai.max(bi));
1475 let (bone_a, bone_b) = if ai < bi { (&mut left[ai], &mut right[0]) } else { (&mut right[0], &mut left[bi]) };
1476 self.solver.solve_rigid_distance(
1477 &mut bone_a.position, &mut bone_a.velocity, bone_a.inv_mass,
1478 &mut bone_b.position, &mut bone_b.velocity, bone_b.inv_mass,
1479 joint.rest_length, dt,
1480 );
1481 }
1482 _ => {}
1483 }
1484 }
1485
1486 for joint in &mut self.joints {
1487 if joint.broken || joint.break_force <= 0.0 { continue; }
1488 let Some(body_b_id) = joint.body_b else { continue };
1489 let a_pos = self.bones.iter().find(|b| b.body_id == joint.body_a).map(|b| b.position);
1490 let b_pos = self.bones.iter().find(|b| b.body_id == body_b_id).map(|b| b.position);
1491 if let (Some(pa), Some(pb)) = (a_pos, b_pos) {
1492 let stretch = ((pb - pa).length() - joint.rest_length).abs();
1493 if stretch * joint.stiffness > joint.break_force {
1494 joint.broken = true;
1495 }
1496 }
1497 }
1498 }
1499
1500 pub fn bone_by_name(&self, name: &str) -> Option<&RagdollBone> {
1501 self.bones.iter().find(|b| b.name == name)
1502 }
1503
1504 pub fn bone_by_name_mut(&mut self, name: &str) -> Option<&mut RagdollBone> {
1505 self.bones.iter_mut().find(|b| b.name == name)
1506 }
1507
1508 pub fn humanoid(position: Vec2) -> Self {
1509 let mut r = Ragdoll::new();
1510 let (tw, th) = (Vec2::new(0.2, 0.3), Vec2::new(0.15, 0.15));
1511 let (uh, lh) = (Vec2::new(0.08, 0.25), Vec2::new(0.07, 0.22));
1512 let (fh, ua, la) = (Vec2::new(0.10, 0.06), Vec2::new(0.07, 0.22), Vec2::new(0.06, 0.18));
1513 r.add_bone(RagdollBone::new("torso", 0, position + Vec2::new( 0.0, 0.0), 15.0, tw));
1514 r.add_bone(RagdollBone::new("head", 1, position + Vec2::new( 0.0, 0.55), 5.0, th));
1515 r.add_bone(RagdollBone::new("upper_arm_l",2, position + Vec2::new(-0.35, 0.2), 3.0, ua));
1516 r.add_bone(RagdollBone::new("lower_arm_l",3, position + Vec2::new(-0.35,-0.1), 2.0, la));
1517 r.add_bone(RagdollBone::new("upper_arm_r",4, position + Vec2::new( 0.35, 0.2), 3.0, ua));
1518 r.add_bone(RagdollBone::new("lower_arm_r",5, position + Vec2::new( 0.35,-0.1), 2.0, la));
1519 r.add_bone(RagdollBone::new("upper_leg_l",6, position + Vec2::new(-0.12,-0.5), 5.0, uh));
1520 r.add_bone(RagdollBone::new("lower_leg_l",7, position + Vec2::new(-0.12,-0.9), 4.0, lh));
1521 r.add_bone(RagdollBone::new("foot_l", 8, position + Vec2::new(-0.12,-1.2), 1.5, fh));
1522 r.add_bone(RagdollBone::new("upper_leg_r",9, position + Vec2::new( 0.12,-0.5), 5.0, uh));
1523 r.add_bone(RagdollBone::new("lower_leg_r",10, position + Vec2::new( 0.12,-0.9), 4.0, lh));
1524 r.add_bone(RagdollBone::new("foot_r", 11, position + Vec2::new( 0.12,-1.2), 1.5, fh));
1525 r.add_joint(Joint::revolute(0, 0, 1, Vec2::new(0.0, 0.45)).with_limits(-0.5, 0.5));
1526 r.add_joint(Joint::revolute(1, 0, 2, Vec2::new(-0.25, 0.25)).with_limits(-2.0, 0.5));
1527 r.add_joint(Joint::revolute(2, 2, 3, Vec2::new(-0.35,-0.05)).with_limits(-2.2, 0.0));
1528 r.add_joint(Joint::revolute(3, 0, 4, Vec2::new( 0.25, 0.25)).with_limits(-0.5, 2.0));
1529 r.add_joint(Joint::revolute(4, 4, 5, Vec2::new( 0.35,-0.05)).with_limits(0.0, 2.2));
1530 r.add_joint(Joint::revolute(5, 0, 6, Vec2::new(-0.12,-0.35)).with_limits(-0.5, 1.5));
1531 r.add_joint(Joint::revolute(6, 6, 7, Vec2::new(-0.12,-0.75)).with_limits(0.0, 2.5));
1532 r.add_joint(Joint::revolute(7, 7, 8, Vec2::new(-0.12,-1.12)).with_limits(-0.8, 0.8));
1533 r.add_joint(Joint::revolute(8, 0, 9, Vec2::new( 0.12,-0.35)).with_limits(-0.5, 1.5));
1534 r.add_joint(Joint::revolute(9, 9, 10,Vec2::new( 0.12,-0.75)).with_limits(0.0, 2.5));
1535 r.add_joint(Joint::revolute(10,10,11,Vec2::new( 0.12,-1.12)).with_limits(-0.8, 0.8));
1536 r
1537 }
1538
1539 pub fn center_of_mass(&self) -> Vec2 {
1540 let (sm, sp) = self.bones.iter().fold((0.0f32, Vec2::ZERO), |(tm, tp), b| (tm + b.mass, tp + b.position * b.mass));
1541 if sm > 0.0 { sp / sm } else { Vec2::ZERO }
1542 }
1543
1544 pub fn is_at_rest(&self) -> bool {
1545 const T: f32 = 0.05;
1546 self.bones.iter().all(|b| b.velocity.length() < T && b.angular_vel.abs() < T)
1547 }
1548}
1549
1550impl Default for Ragdoll {
1551 fn default() -> Self { Self::new() }
1552}
1553
1554#[derive(Debug, Clone)]
1557pub struct CharacterController {
1558 pub position: Vec2,
1559 pub velocity: Vec2,
1560 pub size: Vec2,
1561 pub speed: f32,
1562 pub jump_force: f32,
1563 pub gravity: f32,
1564 pub on_ground: bool,
1565 pub on_slope: bool,
1566 pub max_slope_angle: f32,
1567 pub step_height: f32,
1568 pub coyote_time: f32,
1569 pub coyote_timer: f32,
1570 pub jump_buffer: f32,
1571 pub jump_buffer_timer: f32,
1572 pub wall_stick_time: f32,
1573 pub wall_stick_timer: f32,
1574 pub is_wall_sliding: bool,
1575 pub wall_normal: Vec2,
1576 pub wall_jump_force: Vec2,
1577 pub move_input: f32,
1578 pub jump_requested: bool,
1579 pub crouch: bool,
1580 pub dash_velocity: Vec2,
1581 pub dash_timer: f32,
1582 pub dash_cooldown: f32,
1583 pub dash_speed: f32,
1584 pub dashes_remaining: u32,
1585 pub max_dashes: u32,
1586}
1587
1588impl CharacterController {
1589 pub fn new(position: Vec2, size: Vec2) -> Self {
1590 Self {
1591 position, velocity: Vec2::ZERO, size,
1592 speed: 6.0, jump_force: 12.0, gravity: -20.0,
1593 on_ground: false, on_slope: false, max_slope_angle: 0.7,
1594 step_height: 0.25,
1595 coyote_time: 0.15, coyote_timer: 0.0,
1596 jump_buffer: 0.12, jump_buffer_timer: 0.0,
1597 wall_stick_time: 0.3, wall_stick_timer: 0.0,
1598 is_wall_sliding: false, wall_normal: Vec2::ZERO, wall_jump_force: Vec2::new(6.0, 10.0),
1599 move_input: 0.0, jump_requested: false, crouch: false,
1600 dash_velocity: Vec2::ZERO, dash_timer: 0.0, dash_cooldown: 0.8,
1601 dash_speed: 15.0, dashes_remaining: 2, max_dashes: 2,
1602 }
1603 }
1604
1605 pub fn move_input(&mut self, x: f32) { self.move_input = x.clamp(-1.0, 1.0); }
1606 pub fn request_jump(&mut self) { self.jump_requested = true; self.jump_buffer_timer = self.jump_buffer; }
1607
1608 pub fn request_dash(&mut self) {
1609 if self.dashes_remaining > 0 && self.dash_timer <= 0.0 {
1610 self.dash_velocity = Vec2::new(self.move_input * self.dash_speed, 0.0);
1611 if self.move_input == 0.0 { self.dash_velocity.x = self.dash_speed; }
1612 self.dash_timer = 0.2; self.dash_cooldown = 0.8;
1613 self.dashes_remaining -= 1;
1614 }
1615 }
1616
1617 pub fn update(&mut self, dt: f32, is_grounded: bool, slope_normal: Option<Vec2>) {
1618 if self.on_ground && !is_grounded { self.coyote_timer = self.coyote_time; }
1619 self.on_ground = is_grounded;
1620 if self.coyote_timer > 0.0 { self.coyote_timer -= dt; }
1621 if self.jump_buffer_timer > 0.0 { self.jump_buffer_timer -= dt; }
1622 if self.dash_cooldown > 0.0 { self.dash_cooldown -= dt; }
1623 if self.dash_timer > 0.0 { self.dash_timer -= dt; self.velocity.x = self.dash_velocity.x; }
1624 if is_grounded { self.dashes_remaining = self.max_dashes; }
1625
1626 if !is_grounded {
1627 let grav = if self.is_wall_sliding { self.gravity * 0.3 } else { self.gravity };
1628 self.velocity.y += grav * dt;
1629 } else if self.velocity.y < 0.0 {
1630 self.velocity.y = 0.0;
1631 }
1632
1633 if self.dash_timer <= 0.0 {
1634 let target = self.move_input * self.speed * if self.crouch { 0.5 } else { 1.0 };
1635 let accel = if is_grounded { 20.0 } else { 8.0 };
1636 self.velocity.x += (target - self.velocity.x) * (accel * dt).min(1.0);
1637 }
1638
1639 let can_jump = is_grounded || self.coyote_timer > 0.0;
1640 let wants_jump = self.jump_requested || self.jump_buffer_timer > 0.0;
1641 if can_jump && wants_jump {
1642 self.velocity.y = self.jump_force;
1643 self.coyote_timer = 0.0; self.jump_buffer_timer = 0.0; self.jump_requested = false;
1644 } else if !is_grounded && self.is_wall_sliding && wants_jump {
1645 self.velocity = self.wall_normal * self.wall_jump_force.x + Vec2::Y * self.wall_jump_force.y;
1646 self.is_wall_sliding = false; self.jump_buffer_timer = 0.0; self.jump_requested = false;
1647 }
1648 self.jump_requested = false;
1649
1650 if let Some(normal) = slope_normal {
1651 let slope_angle = normal.dot(Vec2::Y).acos();
1652 self.on_slope = slope_angle > 0.1;
1653 if self.on_slope && is_grounded && self.dash_timer <= 0.0 {
1654 let right = Vec2::new(normal.y, -normal.x);
1655 self.velocity = right * self.velocity.dot(right);
1656 }
1657 }
1658 self.position += self.velocity * dt;
1659 }
1660
1661 pub fn is_moving(&self) -> bool { self.velocity.length() > 0.1 }
1662 pub fn is_falling(&self) -> bool { self.velocity.y < -0.5 && !self.on_ground }
1663 pub fn is_jumping(&self) -> bool { self.velocity.y > 0.5 }
1664 pub fn facing_right(&self) -> bool { self.velocity.x >= 0.0 }
1665 pub fn aabb_min(&self) -> Vec2 { self.position - self.size }
1666 pub fn aabb_max(&self) -> Vec2 { self.position + self.size }
1667}
1668
1669#[cfg(test)]
1672mod tests {
1673 use super::*;
1674
1675 #[test]
1676 fn rigid_body_falls_under_gravity() {
1677 let mut world = PhysicsWorld3D::new();
1678 let h = world.add_body(RigidBody::dynamic(Vec3::new(0.0, 10.0, 0.0), 1.0, CollisionShape::sphere(0.5)));
1679 world.step(0.5);
1680 let body = world.body(h).unwrap();
1681 assert!(body.position.y < 10.0, "body should fall: y={}", body.position.y);
1682 }
1683
1684 #[test]
1685 fn static_body_does_not_move() {
1686 let mut world = PhysicsWorld3D::new();
1687 let h = world.add_body(RigidBody::static_body(Vec3::ZERO, CollisionShape::sphere(1.0)));
1688 world.step(1.0);
1689 assert_eq!(world.body(h).unwrap().position, Vec3::ZERO);
1690 }
1691
1692 #[test]
1693 fn sphere_sphere_collision_detected() {
1694 let m = CollisionDetector::sphere_sphere(
1695 Vec3::ZERO, 1.0, RigidBodyHandle(1),
1696 Vec3::new(1.5, 0.0, 0.0), 1.0, RigidBodyHandle(2),
1697 );
1698 assert!(m.is_some(), "spheres should overlap");
1699 let m = CollisionDetector::sphere_sphere(
1700 Vec3::ZERO, 1.0, RigidBodyHandle(1),
1701 Vec3::new(3.0, 0.0, 0.0), 1.0, RigidBodyHandle(2),
1702 );
1703 assert!(m.is_none(), "spheres should not overlap");
1704 }
1705
1706 #[test]
1707 fn sphere_box_collision() {
1708 let m = CollisionDetector::sphere_box(
1709 Vec3::new(0.0, 1.4, 0.0), 0.6, RigidBodyHandle(1),
1710 Vec3::ZERO, Quat::IDENTITY, Vec3::splat(1.0), RigidBodyHandle(2),
1711 );
1712 assert!(m.is_some(), "sphere should touch box");
1713 }
1714
1715 #[test]
1716 fn box_box_collision_sat() {
1717 let m = CollisionDetector::box_box(
1718 Vec3::ZERO, Quat::IDENTITY, Vec3::splat(1.0), RigidBodyHandle(1),
1719 Vec3::new(1.5, 0.0, 0.0), Quat::IDENTITY, Vec3::splat(1.0), RigidBodyHandle(2),
1720 );
1721 assert!(m.is_some(), "boxes should overlap");
1722 }
1723
1724 #[test]
1725 fn capsule_capsule_collision() {
1726 let m = CollisionDetector::capsule_capsule(
1727 Vec3::ZERO, Vec3::Y, 0.5, 1.0, RigidBodyHandle(1),
1728 Vec3::new(0.8, 0.0, 0.0), Vec3::Y, 0.5, 1.0, RigidBodyHandle(2),
1729 );
1730 assert!(m.is_some(), "capsules should overlap");
1731 }
1732
1733 #[test]
1734 fn broadphase_finds_overlapping_pairs() {
1735 let mut bp = Broadphase::new();
1736 let mut bodies = HashMap::new();
1737 bodies.insert(RigidBodyHandle(1), RigidBody::dynamic(Vec3::ZERO, 1.0, CollisionShape::sphere(1.0)));
1738 bodies.insert(RigidBodyHandle(2), RigidBody::dynamic(Vec3::new(1.5, 0.0, 0.0), 1.0, CollisionShape::sphere(1.0)));
1739 bodies.insert(RigidBodyHandle(3), RigidBody::dynamic(Vec3::new(100.0, 0.0, 0.0), 1.0, CollisionShape::sphere(1.0)));
1740 bp.rebuild(&bodies);
1741 let pairs = bp.overlapping_pairs();
1742 assert!(!pairs.is_empty(), "should find at least one overlapping pair");
1743 let has_far = pairs.iter().any(|(a, b)| *a == RigidBodyHandle(3) || *b == RigidBodyHandle(3));
1744 assert!(!has_far, "far body should not be in any pair");
1745 }
1746
1747 #[test]
1748 fn raycast_hits_sphere() {
1749 let mut world = PhysicsWorld3D::new();
1750 let h = world.add_body(RigidBody::dynamic(Vec3::new(0.0, 0.0, 5.0), 1.0, CollisionShape::sphere(1.0)));
1751 let ray = Ray::new(Vec3::ZERO, Vec3::Z);
1752 let hit = world.raycast(&ray);
1753 assert!(hit.is_some(), "ray should hit sphere");
1754 assert_eq!(hit.unwrap().handle, h);
1755 }
1756
1757 #[test]
1758 fn raycast_misses_sphere() {
1759 let mut world = PhysicsWorld3D::new();
1760 world.add_body(RigidBody::dynamic(Vec3::new(0.0, 5.0, 0.0), 1.0, CollisionShape::sphere(0.5)));
1761 let ray = Ray::new(Vec3::ZERO, Vec3::Z);
1762 let hit = world.raycast(&ray);
1763 assert!(hit.is_none(), "ray going Z should miss sphere at Y=5");
1764 }
1765
1766 #[test]
1767 fn sleep_system_sleeps_body() {
1768 let mut bodies = HashMap::new();
1769 let h = RigidBodyHandle(1);
1770 bodies.insert(h, RigidBody::dynamic(Vec3::ZERO, 1.0, CollisionShape::sphere(0.5)));
1771 let mut sys = SleepSystem::new(10.0, 0.1); for _ in 0..10 {
1773 sys.update(&mut bodies, 0.05);
1774 }
1775 assert!(bodies[&h].is_sleeping(), "body should have slept");
1776 }
1777
1778 #[test]
1779 fn sleep_wake_propagation() {
1780 let mut bodies = HashMap::new();
1781 let h1 = RigidBodyHandle(1);
1782 let h2 = RigidBodyHandle(2);
1783 let mut b1 = RigidBody::dynamic(Vec3::ZERO, 1.0, CollisionShape::sphere(0.5));
1784 b1.sleep_state = SleepState::Asleep;
1785 let mut b2 = RigidBody::dynamic(Vec3::new(0.5, 0.0, 0.0), 1.0, CollisionShape::sphere(0.5));
1786 b2.sleep_state = SleepState::Asleep;
1787 bodies.insert(h1, b1);
1788 bodies.insert(h2, b2);
1789 let manifolds = vec![ContactManifold3D { handle_a: h1, handle_b: h2, contacts: vec![] }];
1790 let mut sys = SleepSystem::new(0.01, 0.5);
1791 sys.wake_body(h1, &mut bodies, &manifolds);
1792 assert!(!bodies[&h1].is_sleeping(), "h1 should be awake");
1793 assert!(!bodies[&h2].is_sleeping(), "h2 should have been woken by propagation");
1794 }
1795
1796 #[test]
1797 fn joint_spring_velocity_change() {
1798 let solver = JointSolver::new();
1799 let mut pa = Vec2::ZERO;
1800 let mut va = Vec2::ZERO;
1801 let mut pb = Vec2::new(5.0, 0.0);
1802 let mut vb = Vec2::ZERO;
1803 solver.solve_distance(&mut pa, &mut va, 1.0, &mut pb, &mut vb, 1.0, 2.0, 100.0, 0.5, 0.016);
1804 assert!(va.length() > 0.0 || vb.length() > 0.0, "velocities should change");
1805 }
1806
1807 #[test]
1808 fn ragdoll_humanoid_bone_count() {
1809 let r = Ragdoll::humanoid(Vec2::new(0.0, 5.0));
1810 assert_eq!(r.bones.len(), 12);
1811 assert!(!r.joints.is_empty());
1812 assert!(r.bone_by_name("torso").is_some());
1813 }
1814
1815 #[test]
1816 fn ragdoll_integrates() {
1817 let mut r = Ragdoll::humanoid(Vec2::ZERO);
1818 r.activate(Vec2::new(5.0, 8.0), 0);
1819 r.update(0.016);
1820 assert!(r.active);
1821 }
1822
1823 #[test]
1824 fn character_controller_moves() {
1825 let mut cc = CharacterController::new(Vec2::ZERO, Vec2::new(0.4, 0.8));
1826 cc.move_input(1.0);
1827 cc.update(0.016, true, None);
1828 assert!(cc.position.x > 0.0, "controller should move right");
1829 }
1830
1831 #[test]
1832 fn character_controller_jumps() {
1833 let mut cc = CharacterController::new(Vec2::ZERO, Vec2::new(0.4, 0.8));
1834 cc.request_jump();
1835 cc.update(0.016, true, None);
1836 assert!(cc.velocity.y > 0.0, "should be jumping");
1837 }
1838
1839 #[test]
1840 fn collision_shape_bounding_radius() {
1841 let s = CollisionShape::sphere(2.0);
1842 assert!((s.bounding_radius() - 2.0).abs() < 1e-5);
1843 let b = CollisionShape::box_shape(Vec3::splat(1.0));
1844 assert!(b.bounding_radius() > 1.0);
1845 }
1846
1847 #[test]
1848 fn rigid_body_kinetic_energy() {
1849 let mut b = RigidBody::dynamic(Vec3::ZERO, 2.0, CollisionShape::sphere(1.0));
1850 b.velocity = Vec3::new(3.0, 0.0, 0.0);
1851 let ke = b.kinetic_energy();
1852 assert!((ke - 9.0).abs() < 0.1, "KE should be ~9, got {ke}");
1854 }
1855}