1use glam::{Vec2, Vec3};
18
19pub mod fluid;
20pub mod soft_body;
21pub mod rigid_body;
22pub mod constraints;
23pub mod joints;
24pub mod fluids;
25
26pub use joints::{Joint, JointType, Ragdoll, RagdollBone, CharacterController, JointSolver};
27
28pub use fluid::FluidGrid;
29pub use soft_body::SoftBody;
30
31#[derive(Debug, Clone)]
35pub struct RigidBody {
36 pub id: BodyId,
37 pub position: Vec2,
38 pub velocity: Vec2,
39 pub acceleration: Vec2,
40 pub mass: f32,
41 pub inv_mass: f32, pub restitution: f32, pub friction: f32, pub damping: f32, pub angle: f32, pub angular_vel: f32,
47 pub angular_damp: f32,
48 pub inertia: f32, pub inv_inertia: f32,
50 pub force_accum: Vec2, pub torque_accum: f32,
52 pub is_sleeping: bool,
53 pub collider: Collider,
54 pub tag: u32,
56 pub layer: u8,
57 pub mask: u8, }
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct BodyId(pub u64);
63
64impl RigidBody {
65 pub fn dynamic(position: Vec2, mass: f32, collider: Collider) -> Self {
67 let inertia = collider.compute_inertia(mass);
68 Self {
69 id: BodyId(0),
70 position,
71 velocity: Vec2::ZERO,
72 acceleration: Vec2::ZERO,
73 mass,
74 inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
75 restitution: 0.3,
76 friction: 0.5,
77 damping: 0.01,
78 angle: 0.0,
79 angular_vel: 0.0,
80 angular_damp: 0.01,
81 inertia,
82 inv_inertia: if inertia > 0.0 { 1.0 / inertia } else { 0.0 },
83 force_accum: Vec2::ZERO,
84 torque_accum: 0.0,
85 is_sleeping: false,
86 collider,
87 tag: 0,
88 layer: 1,
89 mask: 0xFF,
90 }
91 }
92
93 pub fn static_body(position: Vec2, collider: Collider) -> Self {
95 Self {
96 inv_mass: 0.0,
97 inv_inertia: 0.0,
98 mass: f32::INFINITY,
99 inertia: f32::INFINITY,
100 ..Self::dynamic(position, 1.0, collider)
101 }
102 }
103
104 pub fn apply_force(&mut self, force: Vec2) {
106 if self.inv_mass > 0.0 {
107 self.force_accum += force;
108 }
109 }
110
111 pub fn apply_force_at_point(&mut self, force: Vec2, point: Vec2) {
113 self.apply_force(force);
114 let r = point - self.position;
115 self.torque_accum += r.x * force.y - r.y * force.x;
116 }
117
118 pub fn apply_impulse(&mut self, impulse: Vec2) {
120 self.velocity += impulse * self.inv_mass;
121 }
122
123 pub fn clear_forces(&mut self) {
125 self.force_accum = Vec2::ZERO;
126 self.torque_accum = 0.0;
127 }
128
129 pub fn is_static(&self) -> bool { self.inv_mass == 0.0 }
131
132 pub fn kinetic_energy(&self) -> f32 {
134 0.5 * self.mass * self.velocity.length_squared()
135 + 0.5 * self.inertia * self.angular_vel * self.angular_vel
136 }
137
138 fn integrate(&mut self, dt: f32, gravity: Vec2) {
140 if self.is_static() || self.is_sleeping { return; }
141
142 let total_force = self.force_accum + gravity * self.mass;
144 self.acceleration = total_force * self.inv_mass;
145 self.velocity += self.acceleration * dt;
146 self.velocity *= 1.0 - self.damping * dt;
147 self.position += self.velocity * dt;
148
149 let alpha = self.torque_accum * self.inv_inertia;
151 self.angular_vel += alpha * dt;
152 self.angular_vel *= 1.0 - self.angular_damp * dt;
153 self.angle += self.angular_vel * dt;
154
155 self.clear_forces();
156 }
157
158 fn try_sleep(&mut self, sleep_threshold: f32) {
160 if self.velocity.length_squared() < sleep_threshold * sleep_threshold
161 && self.angular_vel.abs() < sleep_threshold
162 {
163 self.is_sleeping = true;
164 self.velocity = Vec2::ZERO;
165 self.angular_vel = 0.0;
166 }
167 }
168
169 pub fn wake(&mut self) { self.is_sleeping = false; }
171}
172
173#[derive(Debug, Clone, Copy)]
177pub enum Collider {
178 Circle { radius: f32 },
179 Box { half_w: f32, half_h: f32 },
180 Capsule { radius: f32, half_height: f32 },
181 Point,
183}
184
185impl Collider {
186 pub fn circle(radius: f32) -> Self { Self::Circle { radius } }
187 pub fn box_shape(w: f32, h: f32) -> Self { Self::Box { half_w: w * 0.5, half_h: h * 0.5 } }
188 pub fn capsule(radius: f32, height: f32) -> Self { Self::Capsule { radius, half_height: height * 0.5 } }
189
190 pub fn compute_inertia(&self, mass: f32) -> f32 {
192 match self {
193 Self::Circle { radius } => 0.5 * mass * radius * radius,
194 Self::Box { half_w, half_h } => mass * (half_w * half_w + half_h * half_h) / 3.0,
195 Self::Capsule { radius, half_height } => {
196 let r2 = radius * radius;
197 let h2 = half_height * half_height;
198 mass * (r2 + h2) / 3.0
199 }
200 Self::Point => 0.0,
201 }
202 }
203
204 pub fn aabb(&self) -> (Vec2, Vec2) {
206 match self {
207 Self::Circle { radius } => (Vec2::splat(-radius), Vec2::splat(*radius)),
208 Self::Box { half_w, half_h } => (Vec2::new(-half_w, -half_h), Vec2::new(*half_w, *half_h)),
209 Self::Capsule { radius, half_height } => {
210 (Vec2::new(-radius, -half_height - radius),
211 Vec2::new(*radius, *half_height + radius))
212 }
213 Self::Point => (Vec2::ZERO, Vec2::ZERO),
214 }
215 }
216
217 pub fn bounding_radius(&self) -> f32 {
219 match self {
220 Self::Circle { radius } => *radius,
221 Self::Box { half_w, half_h } => half_w.hypot(*half_h),
222 Self::Capsule { radius, half_height } => radius + half_height,
223 Self::Point => 0.0,
224 }
225 }
226}
227
228#[derive(Debug, Clone)]
232pub struct ContactManifold {
233 pub body_a: BodyId,
234 pub body_b: BodyId,
235 pub depth: f32,
237 pub normal: Vec2,
239 pub point: Vec2,
241}
242
243pub fn circle_circle(
245 pos_a: Vec2, ra: f32,
246 pos_b: Vec2, rb: f32,
247 id_a: BodyId, id_b: BodyId,
248) -> Option<ContactManifold> {
249 let delta = pos_b - pos_a;
250 let dist = delta.length();
251 let sum_r = ra + rb;
252 if dist >= sum_r || dist < 1e-8 { return None; }
253 let normal = if dist > 0.0 { delta / dist } else { Vec2::X };
254 Some(ContactManifold {
255 body_a: id_a, body_b: id_b,
256 depth: sum_r - dist,
257 normal,
258 point: pos_a + normal * ra,
259 })
260}
261
262pub fn aabb_aabb(
264 pos_a: Vec2, (min_a, max_a): (Vec2, Vec2),
265 pos_b: Vec2, (min_b, max_b): (Vec2, Vec2),
266 id_a: BodyId, id_b: BodyId,
267) -> Option<ContactManifold> {
268 let a_min = pos_a + min_a;
269 let a_max = pos_a + max_a;
270 let b_min = pos_b + min_b;
271 let b_max = pos_b + max_b;
272
273 if a_max.x < b_min.x || b_max.x < a_min.x { return None; }
274 if a_max.y < b_min.y || b_max.y < a_min.y { return None; }
275
276 let dx = (a_max.x.min(b_max.x) - a_min.x.max(b_min.x)).abs();
278 let dy = (a_max.y.min(b_max.y) - a_min.y.max(b_min.y)).abs();
279 let (depth, normal) = if dx < dy {
280 let sign = if pos_a.x < pos_b.x { -1.0 } else { 1.0 };
281 (dx, Vec2::new(sign, 0.0))
282 } else {
283 let sign = if pos_a.y < pos_b.y { -1.0 } else { 1.0 };
284 (dy, Vec2::new(0.0, sign))
285 };
286
287 Some(ContactManifold {
288 body_a: id_a, body_b: id_b, depth, normal,
289 point: (pos_a + pos_b) * 0.5,
290 })
291}
292
293pub fn resolve_contact(a: &mut RigidBody, b: &mut RigidBody, manifold: &ContactManifold) {
295 let rel_vel = b.velocity - a.velocity;
297 let vel_along_normal = rel_vel.dot(manifold.normal);
298
299 if vel_along_normal > 0.0 { return; }
301
302 let e = a.restitution.min(b.restitution);
303 let j_scalar = -(1.0 + e) * vel_along_normal / (a.inv_mass + b.inv_mass);
304 let impulse = manifold.normal * j_scalar;
305
306 a.velocity -= impulse * a.inv_mass;
307 b.velocity += impulse * b.inv_mass;
308
309 const SLOP: f32 = 0.01;
311 const BIAS: f32 = 0.2;
312 let correction = (manifold.depth - SLOP).max(0.0) * BIAS
313 / (a.inv_mass + b.inv_mass);
314 let correction_v = manifold.normal * correction;
315 if !a.is_static() { a.position -= correction_v * a.inv_mass; }
316 if !b.is_static() { b.position += correction_v * b.inv_mass; }
317}
318
319#[derive(Debug, Clone)]
323pub enum Constraint {
324 Distance {
326 body_a: BodyId,
327 body_b: BodyId,
328 rest_length: f32,
329 stiffness: f32,
330 damping: f32,
331 },
332 Spring {
334 body_a: BodyId,
335 body_b: BodyId,
336 rest_len: f32,
337 k: f32, c: f32, },
340 Pin {
342 body: BodyId,
343 target: Vec2,
344 stiffness: f32,
345 },
346 Boundary {
348 body: BodyId,
349 min: Vec2,
350 max: Vec2,
351 },
352 AngleLimit {
354 body: BodyId,
355 min_angle: f32,
356 max_angle: f32,
357 stiffness: f32,
358 },
359}
360
361impl Constraint {
362 pub fn apply(&self, bodies: &mut [RigidBody]) {
364 match self {
365 Constraint::Spring { body_a, body_b, rest_len, k, c } => {
366 let (idx_a, idx_b) = find_pair_indices(bodies, *body_a, *body_b);
367 if let (Some(ia), Some(ib)) = (idx_a, idx_b) {
368 let (left, right) = bodies.split_at_mut(ia.max(ib));
370 let (a, b) = if ia < ib {
371 (&mut left[ia], &mut right[0])
372 } else {
373 (&mut right[0], &mut left[ib])
374 };
375 let delta = b.position - a.position;
376 let dist = delta.length();
377 if dist < 1e-6 { return; }
378 let dir = delta / dist;
379 let spring = *k * (dist - rest_len);
380 let damp = *c * (b.velocity - a.velocity).dot(dir);
381 let force = (spring + damp) * dir;
382 a.apply_force(force);
383 b.apply_force(-force);
384 }
385 }
386 Constraint::Distance { body_a, body_b, rest_length, stiffness, damping } => {
387 let (idx_a, idx_b) = find_pair_indices(bodies, *body_a, *body_b);
388 if let (Some(ia), Some(ib)) = (idx_a, idx_b) {
389 let (left, right) = bodies.split_at_mut(ia.max(ib));
390 let (a, b) = if ia < ib {
391 (&mut left[ia], &mut right[0])
392 } else {
393 (&mut right[0], &mut left[ib])
394 };
395 let delta = b.position - a.position;
396 let dist = delta.length();
397 if dist < 1e-6 { return; }
398 let dir = delta / dist;
399 let err = dist - rest_length;
400 let damp = *damping * (b.velocity - a.velocity).dot(dir);
401 let force = (err * stiffness + damp) * dir;
402 a.apply_force( force);
403 b.apply_force(-force);
404 }
405 }
406 Constraint::Pin { body, target, stiffness } => {
407 if let Some(b) = bodies.iter_mut().find(|b| b.id == *body) {
408 let delta = *target - b.position;
409 b.apply_force(delta * *stiffness);
410 }
411 }
412 Constraint::Boundary { body, min, max } => {
413 if let Some(b) = bodies.iter_mut().find(|b| b.id == *body) {
414 if b.position.x < min.x { b.position.x = min.x; b.velocity.x = b.velocity.x.abs(); }
415 if b.position.x > max.x { b.position.x = max.x; b.velocity.x = -b.velocity.x.abs(); }
416 if b.position.y < min.y { b.position.y = min.y; b.velocity.y = b.velocity.y.abs(); }
417 if b.position.y > max.y { b.position.y = max.y; b.velocity.y = -b.velocity.y.abs(); }
418 }
419 }
420 Constraint::AngleLimit { body, min_angle, max_angle, stiffness } => {
421 if let Some(b) = bodies.iter_mut().find(|b| b.id == *body) {
422 if b.angle < *min_angle {
423 let err = min_angle - b.angle;
424 b.torque_accum += err * stiffness;
425 } else if b.angle > *max_angle {
426 let err = max_angle - b.angle;
427 b.torque_accum += err * stiffness;
428 }
429 }
430 }
431 }
432 }
433}
434
435fn find_pair_indices(bodies: &[RigidBody], a: BodyId, b: BodyId) -> (Option<usize>, Option<usize>) {
436 let ia = bodies.iter().position(|bod| bod.id == a);
437 let ib = bodies.iter().position(|bod| bod.id == b);
438 (ia, ib)
439}
440
441pub struct PhysicsWorld {
448 bodies: Vec<RigidBody>,
449 constraints: Vec<Constraint>,
450 pub gravity: Vec2,
451 pub substeps: u32,
452 pub sleep_threshold: f32,
453 next_id: u64,
454 contacts: Vec<ContactManifold>,
456 accumulator: f32,
458 pub fixed_dt: f32,
459}
460
461impl PhysicsWorld {
462 pub fn new() -> Self {
463 Self {
464 bodies: Vec::new(),
465 constraints: Vec::new(),
466 gravity: Vec2::new(0.0, -9.81),
467 substeps: 4,
468 sleep_threshold: 0.01,
469 next_id: 1,
470 contacts: Vec::new(),
471 accumulator: 0.0,
472 fixed_dt: 1.0 / 120.0,
473 }
474 }
475
476 pub fn add_body(&mut self, mut body: RigidBody) -> BodyId {
478 let id = BodyId(self.next_id);
479 self.next_id += 1;
480 body.id = id;
481 self.bodies.push(body);
482 id
483 }
484
485 pub fn remove_body(&mut self, id: BodyId) {
487 self.bodies.retain(|b| b.id != id);
488 }
489
490 pub fn add_constraint(&mut self, c: Constraint) {
492 self.constraints.push(c);
493 }
494
495 pub fn body(&self, id: BodyId) -> Option<&RigidBody> {
497 self.bodies.iter().find(|b| b.id == id)
498 }
499
500 pub fn body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody> {
502 self.bodies.iter_mut().find(|b| b.id == id)
503 }
504
505 pub fn set_gravity_3d(&mut self, g: Vec3) {
507 self.gravity = Vec2::new(g.x, g.y);
508 }
509
510 pub fn step(&mut self, dt: f32) {
512 self.accumulator += dt;
513 while self.accumulator >= self.fixed_dt {
514 let step_dt = self.fixed_dt / self.substeps as f32;
515 for _ in 0..self.substeps {
516 self.fixed_step(step_dt);
517 }
518 self.accumulator -= self.fixed_dt;
519 }
520 }
521
522 fn fixed_step(&mut self, dt: f32) {
523 let constraints = self.constraints.clone();
525 for c in &constraints {
526 c.apply(&mut self.bodies);
527 }
528
529 let gravity = self.gravity;
531 for body in &mut self.bodies {
532 body.integrate(dt, gravity);
533 }
534
535 self.contacts.clear();
537 let n = self.bodies.len();
538 for i in 0..n {
539 for j in (i + 1)..n {
540 if self.bodies[i].layer & self.bodies[j].mask == 0
542 && self.bodies[j].layer & self.bodies[i].mask == 0 {
543 continue;
544 }
545 if self.bodies[i].is_static() && self.bodies[j].is_static() { continue; }
547
548 let pa = self.bodies[i].position;
549 let pb = self.bodies[j].position;
550 let ca = self.bodies[i].collider;
551 let cb = self.bodies[j].collider;
552 let id_a = self.bodies[i].id;
553 let id_b = self.bodies[j].id;
554
555 let contact = match (ca, cb) {
557 (Collider::Circle { radius: ra }, Collider::Circle { radius: rb }) => {
558 circle_circle(pa, ra, pb, rb, id_a, id_b)
559 }
560 _ => {
561 aabb_aabb(pa, ca.aabb(), pb, cb.aabb(), id_a, id_b)
563 }
564 };
565
566 if let Some(manifold) = contact {
567 self.contacts.push(manifold);
568 }
569 }
570 }
571
572 let contacts = self.contacts.clone();
574 for contact in &contacts {
575 let ia = self.bodies.iter().position(|b| b.id == contact.body_a);
576 let ib = self.bodies.iter().position(|b| b.id == contact.body_b);
577 if let (Some(ia), Some(ib)) = (ia, ib) {
578 let (left, right) = self.bodies.split_at_mut(ia.max(ib));
579 let (a, b) = if ia < ib {
580 (&mut left[ia], &mut right[0])
581 } else {
582 (&mut right[0], &mut left[ib])
583 };
584 resolve_contact(a, b, contact);
585 }
586 }
587
588 let threshold = self.sleep_threshold;
590 for body in &mut self.bodies {
591 body.try_sleep(threshold);
592 }
593 }
594
595 pub fn contacts(&self) -> &[ContactManifold] {
597 &self.contacts
598 }
599
600 pub fn positions(&self) -> Vec<(BodyId, Vec2)> {
602 self.bodies.iter().map(|b| (b.id, b.position)).collect()
603 }
604
605 pub fn body_count(&self) -> usize { self.bodies.len() }
606}
607
608impl Default for PhysicsWorld {
609 fn default() -> Self { Self::new() }
610}
611
612pub fn verlet_step(pos: Vec2, vel: Vec2, acc: Vec2, dt: f32) -> (Vec2, Vec2) {
617 let new_pos = pos + vel * dt + acc * (0.5 * dt * dt);
618 let new_vel = vel + acc * dt;
620 (new_pos, new_vel)
621}
622
623pub fn rk4_2d<F>(pos: Vec2, vel: Vec2, mass: f32, dt: f32, force_fn: F) -> (Vec2, Vec2)
625where
626 F: Fn(Vec2, Vec2) -> Vec2
627{
628 let inv_m = 1.0 / mass.max(1e-8);
629 let k1_v = force_fn(pos, vel) * inv_m;
630 let k1_p = vel;
631
632 let k2_v = force_fn(pos + k1_p * (dt * 0.5), vel + k1_v * (dt * 0.5)) * inv_m;
633 let k2_p = vel + k1_v * (dt * 0.5);
634
635 let k3_v = force_fn(pos + k2_p * (dt * 0.5), vel + k2_v * (dt * 0.5)) * inv_m;
636 let k3_p = vel + k2_v * (dt * 0.5);
637
638 let k4_v = force_fn(pos + k3_p * dt, vel + k3_v * dt) * inv_m;
639 let k4_p = vel + k3_v * dt;
640
641 let new_vel = vel + (k1_v + k2_v * 2.0 + k3_v * 2.0 + k4_v) * (dt / 6.0);
642 let new_pos = pos + (k1_p + k2_p * 2.0 + k3_p * 2.0 + k4_p) * (dt / 6.0);
643 (new_pos, new_vel)
644}
645
646#[cfg(test)]
649mod tests {
650 use super::*;
651
652 #[test]
653 fn body_falls_under_gravity() {
654 let mut world = PhysicsWorld::new();
655 let id = world.add_body(RigidBody::dynamic(Vec2::ZERO, 1.0, Collider::circle(0.5)));
656 world.step(0.5);
657 let body = world.body(id).unwrap();
658 assert!(body.position.y < 0.0, "body should fall: y={}", body.position.y);
659 }
660
661 #[test]
662 fn static_body_does_not_move() {
663 let mut world = PhysicsWorld::new();
664 let id = world.add_body(RigidBody::static_body(Vec2::new(0.0, -5.0), Collider::box_shape(10.0, 1.0)));
665 world.step(1.0);
666 let body = world.body(id).unwrap();
667 assert_eq!(body.position, Vec2::new(0.0, -5.0));
668 }
669
670 #[test]
671 fn spring_constraint_attracts() {
672 let mut world = PhysicsWorld::new();
673 let id_a = world.add_body(RigidBody::dynamic(Vec2::new(-2.0, 0.0), 1.0, Collider::circle(0.1)));
674 let id_b = world.add_body(RigidBody::dynamic(Vec2::new( 2.0, 0.0), 1.0, Collider::circle(0.1)));
675 world.gravity = Vec2::ZERO;
676 world.add_constraint(Constraint::Spring { body_a: id_a, body_b: id_b, rest_len: 1.0, k: 10.0, c: 1.0 });
677 let dist_before = 4.0;
678 world.step(0.1);
679 let pa = world.body(id_a).unwrap().position;
680 let pb = world.body(id_b).unwrap().position;
681 let dist_after = (pb - pa).length();
682 assert!(dist_after < dist_before, "spring should pull bodies closer");
683 }
684
685 #[test]
686 fn circle_collision_detected() {
687 let a = circle_circle(Vec2::ZERO, 1.0, Vec2::new(1.5, 0.0), 1.0, BodyId(1), BodyId(2));
688 assert!(a.is_some(), "circles should overlap");
689 let b = circle_circle(Vec2::ZERO, 1.0, Vec2::new(3.0, 0.0), 1.0, BodyId(1), BodyId(2));
690 assert!(b.is_none(), "circles should not overlap");
691 }
692}