Skip to main content

arcane_core/physics/
world.rs

1use std::collections::{HashMap, HashSet};
2
3use super::broadphase::SpatialHash;
4use super::constraints::{solve_constraints, solve_constraints_position};
5use super::integrate::integrate;
6use super::broadphase::SPECULATIVE_MARGIN;
7use super::narrowphase::test_collision_manifold_speculative;
8use super::resolve::{
9    initialize_manifolds, resolve_manifolds_position,
10    resolve_manifolds_velocity_iteration, warm_start_manifolds,
11};
12use super::sleep::update_sleep;
13use super::types::*;
14
15pub struct PhysicsWorld {
16    bodies: Vec<Option<RigidBody>>,
17    free_ids: Vec<BodyId>,
18    next_id: BodyId,
19    constraints: Vec<Constraint>,
20    next_constraint_id: ConstraintId,
21    gravity: (f32, f32),
22    fixed_dt: f32,
23    accumulator: f32,
24    contacts: Vec<Contact>,
25    /// Contact manifolds (TGS Soft): proper 2-point contacts with local anchors
26    manifolds: Vec<ContactManifold>,
27    /// Contacts accumulated across all sub-steps within a frame.
28    /// Game code reads this via get_contacts() to see every collision that
29    /// occurred during the frame, not just the last sub-step's contacts.
30    /// De-duplicated by body pair (first contact per pair wins).
31    frame_contacts: Vec<Contact>,
32    /// Tracks which body pairs already have a contact in frame_contacts.
33    frame_contact_pairs: HashSet<(BodyId, BodyId)>,
34    broadphase: SpatialHash,
35    solver_iterations: usize,
36    /// Warm-start cache for manifolds: maps (body_a, body_b, ContactID) → (jn, jt)
37    manifold_warm_cache: HashMap<(BodyId, BodyId, ContactID), (f32, f32)>,
38}
39
40impl PhysicsWorld {
41    pub fn new(gravity_x: f32, gravity_y: f32) -> Self {
42        Self {
43            bodies: Vec::new(),
44            free_ids: Vec::new(),
45            next_id: 0,
46            constraints: Vec::new(),
47            next_constraint_id: 0,
48            gravity: (gravity_x, gravity_y),
49            fixed_dt: 1.0 / 60.0,
50            accumulator: 0.0,
51            contacts: Vec::new(),
52            manifolds: Vec::new(),
53            frame_contacts: Vec::new(),
54            frame_contact_pairs: HashSet::new(),
55            broadphase: SpatialHash::new(64.0),
56            // Increased from 6 to 10 for better constraint convergence (ropes/chains).
57            // Box2D uses 4 velocity + 2 position with sub-stepping; we use more iterations.
58            solver_iterations: 10,
59            manifold_warm_cache: HashMap::new(),
60        }
61    }
62
63    /// Fixed-timestep physics step. Accumulates dt and runs sub-steps as needed.
64    /// Uses 4 sub-steps per fixed step (Box2D v3 approach) for improved stack
65    /// stability: sub-stepping is more effective than extra solver iterations.
66    ///
67    /// TGS Soft Phase 4: Narrowphase runs ONCE per frame, sub-steps use
68    /// analytical contact updating (O(contacts) vs O(contacts × geometry)).
69    ///
70    /// Contacts are accumulated across all sub-steps (de-duplicated by body pair)
71    /// so that game code can see every collision via get_contacts().
72    pub fn step(&mut self, dt: f32) {
73        self.accumulator += dt;
74
75        // Clear frame-level contact accumulator at the start of each step call
76        self.frame_contacts.clear();
77        self.frame_contact_pairs.clear();
78
79        while self.accumulator >= self.fixed_dt {
80            self.step_manifolds(self.fixed_dt);
81            self.accumulator -= self.fixed_dt;
82        }
83    }
84
85    /// TGS Soft Phase 4: Run narrowphase once per sub-step, but use analytical updating
86    /// for position correction phase. This reduces narrowphase calls from 16x to 4x per frame.
87    fn step_manifolds(&mut self, fixed_dt: f32) {
88        let sub_dt = fixed_dt / 4.0;
89
90        for sub_step in 0..4 {
91            // 1. Integrate
92            for body in self.bodies.iter_mut().flatten() {
93                integrate(body, self.gravity.0, self.gravity.1, sub_dt);
94            }
95
96            // 2. Broadphase with speculative expansion
97            self.broadphase.clear();
98            for body in self.bodies.iter().flatten() {
99                let (min_x, min_y, max_x, max_y) = get_shape_aabb(body);
100                self.broadphase.insert_speculative(
101                    body.id, min_x, min_y, max_x, max_y,
102                    body.vx, body.vy, sub_dt,
103                );
104            }
105            let pairs = self.broadphase.get_pairs();
106
107            // 3. Narrowphase - generate contact manifolds
108            self.manifolds.clear();
109            self.contacts.clear();
110            for (id_a, id_b) in &pairs {
111                let a_idx = *id_a as usize;
112                let b_idx = *id_b as usize;
113
114                let (layer_a, mask_a, sleeping_a) = match &self.bodies[a_idx] {
115                    Some(b) => (b.layer, b.mask, b.sleeping),
116                    None => continue,
117                };
118                let (layer_b, mask_b, sleeping_b) = match &self.bodies[b_idx] {
119                    Some(b) => (b.layer, b.mask, b.sleeping),
120                    None => continue,
121                };
122
123                if (layer_a & mask_b) == 0 || (layer_b & mask_a) == 0 {
124                    continue;
125                }
126
127                if sleeping_a && sleeping_b {
128                    continue;
129                }
130
131                let body_a = self.bodies[a_idx].as_ref().unwrap();
132                let body_b = self.bodies[b_idx].as_ref().unwrap();
133
134                let speculative_margin = SPECULATIVE_MARGIN + (body_a.vx.abs() + body_a.vy.abs() + body_b.vx.abs() + body_b.vy.abs()) * sub_dt;
135                if let Some(manifold) = test_collision_manifold_speculative(body_a, body_b, speculative_margin) {
136                    if !manifold.points.is_empty() {
137                        let point = &manifold.points[0];
138                        let cos_a = body_a.angle.cos();
139                        let sin_a = body_a.angle.sin();
140                        let cpx = point.local_a.0 * cos_a - point.local_a.1 * sin_a + body_a.x;
141                        let cpy = point.local_a.0 * sin_a + point.local_a.1 * cos_a + body_a.y;
142                        self.contacts.push(Contact {
143                            body_a: manifold.body_a,
144                            body_b: manifold.body_b,
145                            normal: manifold.normal,
146                            penetration: point.penetration,
147                            contact_point: (cpx, cpy),
148                            accumulated_jn: 0.0,
149                            accumulated_jt: 0.0,
150                            velocity_bias: 0.0,
151                            tangent: manifold.tangent,
152                        });
153                    }
154                    self.manifolds.push(manifold);
155                }
156            }
157
158            // Accumulate contacts to frame_contacts (first sub-step only to avoid duplicates)
159            if sub_step == 0 {
160                for contact in &self.contacts {
161                    let key = (contact.body_a.min(contact.body_b), contact.body_a.max(contact.body_b));
162                    if self.frame_contact_pairs.insert(key) {
163                        self.frame_contacts.push(contact.clone());
164                    }
165                }
166            }
167
168            // 3b. Sort manifolds bottom-up for stack stability
169            self.manifolds.sort_by(|a, b| {
170                let ay = self.bodies[a.body_a as usize]
171                    .as_ref()
172                    .map_or(0.0f32, |body| body.y)
173                    .max(
174                        self.bodies[a.body_b as usize]
175                            .as_ref()
176                            .map_or(0.0f32, |body| body.y),
177                    );
178                let by = self.bodies[b.body_a as usize]
179                    .as_ref()
180                    .map_or(0.0f32, |body| body.y)
181                    .max(
182                        self.bodies[b.body_b as usize]
183                            .as_ref()
184                            .map_or(0.0f32, |body| body.y),
185                    );
186                by.partial_cmp(&ay).unwrap_or(std::cmp::Ordering::Equal)
187            });
188
189            // 3c. Pre-compute velocity bias
190            let gravity_mag = (self.gravity.0 * self.gravity.0 + self.gravity.1 * self.gravity.1).sqrt();
191            let restitution_threshold = gravity_mag * sub_dt * 1.5;
192            initialize_manifolds(&self.bodies, &mut self.manifolds, restitution_threshold);
193
194            // 3d. Warm start from cache using ContactID
195            for manifold in &mut self.manifolds {
196                let pair_key = (
197                    manifold.body_a.min(manifold.body_b),
198                    manifold.body_a.max(manifold.body_b),
199                );
200                for point in &mut manifold.points {
201                    let key = (pair_key.0, pair_key.1, point.id);
202                    if let Some(&(jn, jt)) = self.manifold_warm_cache.get(&key) {
203                        point.accumulated_jn = jn * 0.95;
204                        point.accumulated_jt = jt * 0.95;
205                    }
206                }
207            }
208            warm_start_manifolds(&mut self.bodies, &self.manifolds);
209
210            // 3e. Reset soft constraint accumulated impulses
211            for constraint in &mut self.constraints {
212                match constraint {
213                    Constraint::Distance { soft: Some(_), accumulated_impulse, .. } => {
214                        *accumulated_impulse = 0.0;
215                    }
216                    Constraint::Revolute { soft: Some(_), accumulated_impulse, .. } => {
217                        *accumulated_impulse = (0.0, 0.0);
218                    }
219                    _ => {}
220                }
221            }
222
223            // 4. Velocity solve
224            for i in 0..self.solver_iterations {
225                let reverse = i % 2 == 1;
226                resolve_manifolds_velocity_iteration(&mut self.bodies, &mut self.manifolds, reverse, sub_dt);
227                solve_constraints(&mut self.bodies, &mut self.constraints, sub_dt);
228            }
229
230            // 4b. Save accumulated impulses to warm cache
231            self.manifold_warm_cache.clear();
232            for manifold in &self.manifolds {
233                let pair_key = (
234                    manifold.body_a.min(manifold.body_b),
235                    manifold.body_a.max(manifold.body_b),
236                );
237                for point in &manifold.points {
238                    let key = (pair_key.0, pair_key.1, point.id);
239                    self.manifold_warm_cache.insert(key, (point.accumulated_jn, point.accumulated_jt));
240                }
241            }
242
243            // 5. Position correction - re-run narrowphase for accurate penetration
244            // Note: We tried analytical updating but it doesn't converge well for
245            // position correction. Keeping narrowphase here maintains correctness.
246            for i in 0..3 {
247                for manifold in &mut self.manifolds {
248                    let a = &self.bodies[manifold.body_a as usize];
249                    let b = &self.bodies[manifold.body_b as usize];
250                    if let (Some(a), Some(b)) = (a, b) {
251                        if let Some(fresh) = test_collision_manifold_speculative(a, b, SPECULATIVE_MARGIN) {
252                            manifold.normal = fresh.normal;
253                            manifold.tangent = fresh.tangent;
254                            for (point, fresh_point) in manifold.points.iter_mut().zip(fresh.points.iter()) {
255                                point.penetration = fresh_point.penetration;
256                                point.local_a = fresh_point.local_a;
257                                point.local_b = fresh_point.local_b;
258                            }
259                            if manifold.points.len() != fresh.points.len() {
260                                manifold.points = fresh.points;
261                            }
262                        } else {
263                            for point in &mut manifold.points {
264                                point.penetration = 0.0;
265                            }
266                        }
267                    }
268                }
269                resolve_manifolds_position(&mut self.bodies, &self.manifolds, i % 2 == 1);
270                solve_constraints_position(&mut self.bodies, &self.constraints);
271            }
272
273            // 6. Post-correction velocity clamping
274            for manifold in &self.manifolds {
275                let a_idx = manifold.body_a as usize;
276                let b_idx = manifold.body_b as usize;
277
278                let still_penetrating = manifold.points.iter().any(|p| p.penetration > 0.01);
279                if !still_penetrating {
280                    continue;
281                }
282
283                let (nx, ny) = manifold.normal;
284
285                if let Some(a) = &mut self.bodies[a_idx] {
286                    if a.body_type == BodyType::Dynamic {
287                        let vn = a.vx * nx + a.vy * ny;
288                        if vn > 0.0 {
289                            a.vx -= vn * nx;
290                            a.vy -= vn * ny;
291                        }
292                    }
293                }
294                if let Some(b) = &mut self.bodies[b_idx] {
295                    if b.body_type == BodyType::Dynamic {
296                        let vn = b.vx * nx + b.vy * ny;
297                        if vn < 0.0 {
298                            b.vx -= vn * nx;
299                            b.vy -= vn * ny;
300                        }
301                    }
302                }
303            }
304        }
305
306        // Sleep update (once per frame)
307        update_sleep(&mut self.bodies, &self.contacts, fixed_dt);
308    }
309
310    pub fn add_body(
311        &mut self,
312        body_type: BodyType,
313        shape: Shape,
314        x: f32,
315        y: f32,
316        mass: f32,
317        material: Material,
318        layer: u16,
319        mask: u16,
320    ) -> BodyId {
321        let id = if let Some(recycled) = self.free_ids.pop() {
322            recycled
323        } else {
324            let id = self.next_id;
325            self.next_id += 1;
326            id
327        };
328
329        let (inv_mass, inertia, inv_inertia) = compute_mass_and_inertia(&shape, mass, body_type);
330
331        let body = RigidBody {
332            id,
333            body_type,
334            shape,
335            material,
336            x,
337            y,
338            angle: 0.0,
339            vx: 0.0,
340            vy: 0.0,
341            angular_velocity: 0.0,
342            fx: 0.0,
343            fy: 0.0,
344            torque: 0.0,
345            mass,
346            inv_mass,
347            inertia,
348            inv_inertia,
349            layer,
350            mask,
351            sleeping: false,
352            sleep_timer: 0.0,
353        };
354
355        let idx = id as usize;
356        if idx >= self.bodies.len() {
357            self.bodies.resize_with(idx + 1, || None);
358        }
359        self.bodies[idx] = Some(body);
360        id
361    }
362
363    pub fn remove_body(&mut self, id: BodyId) {
364        let idx = id as usize;
365        if idx < self.bodies.len() {
366            self.bodies[idx] = None;
367            self.free_ids.push(id);
368        }
369    }
370
371    pub fn get_body(&self, id: BodyId) -> Option<&RigidBody> {
372        self.bodies.get(id as usize)?.as_ref()
373    }
374
375    pub fn get_body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody> {
376        self.bodies.get_mut(id as usize)?.as_mut()
377    }
378
379    pub fn set_velocity(&mut self, id: BodyId, vx: f32, vy: f32) {
380        if let Some(body) = self.get_body_mut(id) {
381            body.vx = vx;
382            body.vy = vy;
383            body.sleeping = false;
384            body.sleep_timer = 0.0;
385        }
386    }
387
388    pub fn set_angular_velocity(&mut self, id: BodyId, av: f32) {
389        if let Some(body) = self.get_body_mut(id) {
390            body.angular_velocity = av;
391            body.sleeping = false;
392            body.sleep_timer = 0.0;
393        }
394    }
395
396    pub fn apply_force(&mut self, id: BodyId, fx: f32, fy: f32) {
397        if let Some(body) = self.get_body_mut(id) {
398            body.fx += fx;
399            body.fy += fy;
400            body.sleeping = false;
401            body.sleep_timer = 0.0;
402        }
403    }
404
405    pub fn apply_impulse(&mut self, id: BodyId, ix: f32, iy: f32) {
406        if let Some(body) = self.get_body_mut(id) {
407            body.vx += ix * body.inv_mass;
408            body.vy += iy * body.inv_mass;
409            body.sleeping = false;
410            body.sleep_timer = 0.0;
411        }
412    }
413
414    pub fn set_position(&mut self, id: BodyId, x: f32, y: f32) {
415        if let Some(body) = self.get_body_mut(id) {
416            body.x = x;
417            body.y = y;
418            body.sleeping = false;
419            body.sleep_timer = 0.0;
420        }
421    }
422
423    pub fn set_collision_layers(&mut self, id: BodyId, layer: u16, mask: u16) {
424        if let Some(body) = self.get_body_mut(id) {
425            body.layer = layer;
426            body.mask = mask;
427        }
428    }
429
430    pub fn add_constraint(&mut self, constraint: Constraint) -> ConstraintId {
431        let id = self.next_constraint_id;
432        self.next_constraint_id += 1;
433
434        let constraint = match constraint {
435            Constraint::Distance {
436                body_a,
437                body_b,
438                distance,
439                anchor_a,
440                anchor_b,
441                soft,
442                ..
443            } => Constraint::Distance {
444                id,
445                body_a,
446                body_b,
447                distance,
448                anchor_a,
449                anchor_b,
450                soft,
451                accumulated_impulse: 0.0,
452            },
453            Constraint::Revolute {
454                body_a,
455                body_b,
456                anchor_a,
457                anchor_b,
458                soft,
459                ..
460            } => Constraint::Revolute {
461                id,
462                body_a,
463                body_b,
464                anchor_a,
465                anchor_b,
466                soft,
467                accumulated_impulse: (0.0, 0.0),
468            },
469        };
470        self.constraints.push(constraint);
471        id
472    }
473
474    pub fn remove_constraint(&mut self, id: ConstraintId) {
475        self.constraints.retain(|c| c.id() != id);
476    }
477
478    pub fn query_aabb(&self, min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> Vec<BodyId> {
479        let mut result = Vec::new();
480        for body in self.bodies.iter().flatten() {
481            let (bmin_x, bmin_y, bmax_x, bmax_y) = get_shape_aabb(body);
482            if bmax_x >= min_x && bmin_x <= max_x && bmax_y >= min_y && bmin_y <= max_y {
483                result.push(body.id);
484            }
485        }
486        result
487    }
488
489    pub fn raycast(
490        &self,
491        ox: f32,
492        oy: f32,
493        dx: f32,
494        dy: f32,
495        max_dist: f32,
496    ) -> Option<(BodyId, f32, f32, f32)> {
497        let dir_len = (dx * dx + dy * dy).sqrt();
498        if dir_len < 1e-8 {
499            return None;
500        }
501        let ndx = dx / dir_len;
502        let ndy = dy / dir_len;
503
504        let mut closest: Option<(BodyId, f32, f32, f32)> = None;
505
506        for body in self.bodies.iter().flatten() {
507            let t = match &body.shape {
508                Shape::Circle { radius } => {
509                    ray_vs_circle(ox, oy, ndx, ndy, body.x, body.y, *radius)
510                }
511                Shape::AABB { half_w, half_h } => {
512                    ray_vs_aabb(ox, oy, ndx, ndy, body.x, body.y, *half_w, *half_h)
513                }
514                Shape::Polygon { vertices } => {
515                    ray_vs_polygon(ox, oy, ndx, ndy, body, vertices)
516                }
517            };
518
519            if let Some(t) = t {
520                if t >= 0.0 && t <= max_dist {
521                    let hit_x = ox + ndx * t;
522                    let hit_y = oy + ndy * t;
523                    if closest.is_none() || t < closest.unwrap().3 {
524                        closest = Some((body.id, hit_x, hit_y, t));
525                    }
526                }
527            }
528        }
529        closest
530    }
531
532    /// Return all contacts that occurred during the last step() call.
533    /// Accumulated across all sub-steps, de-duplicated by body pair.
534    /// This ensures game code sees every collision, even if the bodies
535    /// separated during a later sub-step.
536    pub fn get_contacts(&self) -> &[Contact] {
537        &self.frame_contacts
538    }
539
540    /// Return all contact manifolds from the last sub-step.
541    /// Each manifold contains up to 2 contact points with feature-based IDs.
542    /// Useful for debugging and visualization.
543    pub fn get_manifolds(&self) -> &[ContactManifold] {
544        &self.manifolds
545    }
546
547    /// Return all active (non-None) bodies.
548    pub fn all_bodies(&self) -> Vec<&RigidBody> {
549        self.bodies.iter().filter_map(|b| b.as_ref()).collect()
550    }
551
552    /// Return the world gravity.
553    pub fn gravity(&self) -> (f32, f32) {
554        self.gravity
555    }
556
557    /// Return the number of active bodies.
558    pub fn body_count(&self) -> usize {
559        self.bodies.iter().filter(|b| b.is_some()).count()
560    }
561}
562
563fn ray_vs_circle(
564    ox: f32, oy: f32,
565    dx: f32, dy: f32,
566    cx: f32, cy: f32,
567    radius: f32,
568) -> Option<f32> {
569    let fx = ox - cx;
570    let fy = oy - cy;
571    let a = dx * dx + dy * dy;
572    let b = 2.0 * (fx * dx + fy * dy);
573    let c = fx * fx + fy * fy - radius * radius;
574    let discriminant = b * b - 4.0 * a * c;
575    if discriminant < 0.0 {
576        return None;
577    }
578    let sqrt_d = discriminant.sqrt();
579    let t1 = (-b - sqrt_d) / (2.0 * a);
580    let t2 = (-b + sqrt_d) / (2.0 * a);
581    if t1 >= 0.0 {
582        Some(t1)
583    } else if t2 >= 0.0 {
584        Some(t2)
585    } else {
586        None
587    }
588}
589
590fn ray_vs_aabb(
591    ox: f32, oy: f32,
592    dx: f32, dy: f32,
593    cx: f32, cy: f32,
594    hw: f32, hh: f32,
595) -> Option<f32> {
596    let min_x = cx - hw;
597    let max_x = cx + hw;
598    let min_y = cy - hh;
599    let max_y = cy + hh;
600
601    let (mut tmin, mut tmax) = if dx.abs() < 1e-8 {
602        if ox < min_x || ox > max_x {
603            return None;
604        }
605        (f32::MIN, f32::MAX)
606    } else {
607        let inv_dx = 1.0 / dx;
608        let t1 = (min_x - ox) * inv_dx;
609        let t2 = (max_x - ox) * inv_dx;
610        (t1.min(t2), t1.max(t2))
611    };
612
613    let (tymin, tymax) = if dy.abs() < 1e-8 {
614        if oy < min_y || oy > max_y {
615            return None;
616        }
617        (f32::MIN, f32::MAX)
618    } else {
619        let inv_dy = 1.0 / dy;
620        let t1 = (min_y - oy) * inv_dy;
621        let t2 = (max_y - oy) * inv_dy;
622        (t1.min(t2), t1.max(t2))
623    };
624
625    tmin = tmin.max(tymin);
626    tmax = tmax.min(tymax);
627
628    if tmin > tmax || tmax < 0.0 {
629        return None;
630    }
631
632    Some(if tmin >= 0.0 { tmin } else { tmax })
633}
634
635fn ray_vs_polygon(
636    ox: f32, oy: f32,
637    dx: f32, dy: f32,
638    body: &RigidBody,
639    vertices: &[(f32, f32)],
640) -> Option<f32> {
641    let cos = body.angle.cos();
642    let sin = body.angle.sin();
643    let n = vertices.len();
644    if n < 3 {
645        return None;
646    }
647
648    let mut closest_t: Option<f32> = None;
649
650    for i in 0..n {
651        let (vx0, vy0) = vertices[i];
652        let (vx1, vy1) = vertices[(i + 1) % n];
653
654        // Transform to world space
655        let ax = vx0 * cos - vy0 * sin + body.x;
656        let ay = vx0 * sin + vy0 * cos + body.y;
657        let bx = vx1 * cos - vy1 * sin + body.x;
658        let by = vx1 * sin + vy1 * cos + body.y;
659
660        if let Some(t) = ray_vs_segment(ox, oy, dx, dy, ax, ay, bx, by) {
661            if closest_t.is_none() || t < closest_t.unwrap() {
662                closest_t = Some(t);
663            }
664        }
665    }
666    closest_t
667}
668
669fn ray_vs_segment(
670    ox: f32, oy: f32,
671    dx: f32, dy: f32,
672    ax: f32, ay: f32,
673    bx: f32, by: f32,
674) -> Option<f32> {
675    let ex = bx - ax;
676    let ey = by - ay;
677    let denom = dx * ey - dy * ex;
678    if denom.abs() < 1e-8 {
679        return None;
680    }
681    let t = ((ax - ox) * ey - (ay - oy) * ex) / denom;
682    let u = ((ax - ox) * dy - (ay - oy) * dx) / denom;
683    if t >= 0.0 && u >= 0.0 && u <= 1.0 {
684        Some(t)
685    } else {
686        None
687    }
688}