Skip to main content

arcane_core/physics/
resolve.rs

1use super::types::{BodyType, ContactManifold, ManifoldPoint, RigidBody};
2
3/// Transform body-local point to world space
4fn local_to_world(body: &RigidBody, local: (f32, f32)) -> (f32, f32) {
5    let cos = body.angle.cos();
6    let sin = body.angle.sin();
7    (
8        local.0 * cos - local.1 * sin + body.x,
9        local.0 * sin + local.1 * cos + body.y,
10    )
11}
12
13/// Initialize manifolds: pre-compute velocity bias and tangent for all contact points.
14pub fn initialize_manifolds(
15    bodies: &[Option<RigidBody>],
16    manifolds: &mut [ContactManifold],
17    restitution_threshold: f32,
18) {
19    for manifold in manifolds.iter_mut() {
20        let id_a = manifold.body_a as usize;
21        let id_b = manifold.body_b as usize;
22
23        let a = match &bodies[id_a] {
24            Some(b) => b,
25            None => continue,
26        };
27        let b = match &bodies[id_b] {
28            Some(b) => b,
29            None => continue,
30        };
31
32        let (nx, ny) = manifold.normal;
33
34        // Compute tangent (perpendicular to normal)
35        manifold.tangent = (-ny, nx);
36
37        // Compute velocity bias from average of all contact point velocities
38        let mut total_vn = 0.0;
39        for point in &manifold.points {
40            // World-space contact points from local anchors
41            let wa = local_to_world(a, point.local_a);
42            let wb = local_to_world(b, point.local_b);
43
44            // Lever arms
45            let ra_x = wa.0 - a.x;
46            let ra_y = wa.1 - a.y;
47            let rb_x = wb.0 - b.x;
48            let rb_y = wb.1 - b.y;
49
50            // Relative velocity at contact point
51            let rel_vx = (b.vx + (-b.angular_velocity * rb_y)) - (a.vx + (-a.angular_velocity * ra_y));
52            let rel_vy = (b.vy + (b.angular_velocity * rb_x)) - (a.vy + (a.angular_velocity * ra_x));
53
54            let vn = rel_vx * nx + rel_vy * ny;
55            total_vn += vn;
56        }
57
58        let avg_vn = if manifold.points.is_empty() {
59            0.0
60        } else {
61            total_vn / manifold.points.len() as f32
62        };
63
64        // Restitution bias: only apply bounce for approach speeds above threshold
65        let e = if -avg_vn < restitution_threshold {
66            0.0
67        } else {
68            a.material.restitution.max(b.material.restitution)
69        };
70        manifold.velocity_bias = e * (-avg_vn).max(0.0);
71    }
72}
73
74/// Warm start manifolds: apply cached impulses from previous frame.
75pub fn warm_start_manifolds(
76    bodies: &mut [Option<RigidBody>],
77    manifolds: &[ContactManifold],
78) {
79    for manifold in manifolds {
80        let id_a = manifold.body_a as usize;
81        let id_b = manifold.body_b as usize;
82
83        let (nx, ny) = manifold.normal;
84        let (tx, ty) = manifold.tangent;
85
86        for point in &manifold.points {
87            let jn = point.accumulated_jn;
88            let jt = point.accumulated_jt;
89
90            if jn == 0.0 && jt == 0.0 {
91                continue;
92            }
93
94            // Combined impulse
95            let px = jn * nx + jt * tx;
96            let py = jn * ny + jt * ty;
97
98            // Get world contact point (use average of both anchors)
99            let (inv_ma, inv_ia, type_a, xa, ya, cos_a, sin_a) = match &bodies[id_a] {
100                Some(a) => (a.inv_mass, a.inv_inertia, a.body_type, a.x, a.y, a.angle.cos(), a.angle.sin()),
101                None => continue,
102            };
103            let (inv_mb, inv_ib, type_b, xb, yb, cos_b, sin_b) = match &bodies[id_b] {
104                Some(b) => (b.inv_mass, b.inv_inertia, b.body_type, b.x, b.y, b.angle.cos(), b.angle.sin()),
105                None => continue,
106            };
107
108            // Transform local anchors to world
109            let wa_x = point.local_a.0 * cos_a - point.local_a.1 * sin_a + xa;
110            let wa_y = point.local_a.0 * sin_a + point.local_a.1 * cos_a + ya;
111            let wb_x = point.local_b.0 * cos_b - point.local_b.1 * sin_b + xb;
112            let wb_y = point.local_b.0 * sin_b + point.local_b.1 * cos_b + yb;
113
114            // Lever arms
115            let ra_x = wa_x - xa;
116            let ra_y = wa_y - ya;
117            let rb_x = wb_x - xb;
118            let rb_y = wb_y - yb;
119
120            if let Some(a) = &mut bodies[id_a] {
121                if type_a == BodyType::Dynamic {
122                    a.vx -= px * inv_ma;
123                    a.vy -= py * inv_ma;
124                    let ra_cross_p = ra_x * py - ra_y * px;
125                    a.angular_velocity -= ra_cross_p * inv_ia;
126                }
127            }
128            if let Some(b) = &mut bodies[id_b] {
129                if type_b == BodyType::Dynamic {
130                    b.vx += px * inv_mb;
131                    b.vy += py * inv_mb;
132                    let rb_cross_p = rb_x * py - rb_y * px;
133                    b.angular_velocity += rb_cross_p * inv_ib;
134                }
135            }
136        }
137    }
138}
139
140/// One iteration of velocity solving for all manifolds.
141/// `sub_dt` is the sub-step time for speculative contact bias calculation.
142pub fn resolve_manifolds_velocity_iteration(
143    bodies: &mut [Option<RigidBody>],
144    manifolds: &mut [ContactManifold],
145    reverse: bool,
146    sub_dt: f32,
147) {
148    let len = manifolds.len();
149    if reverse {
150        for i in (0..len).rev() {
151            resolve_manifold_velocity(bodies, &mut manifolds[i], sub_dt);
152        }
153    } else {
154        for i in 0..len {
155            resolve_manifold_velocity(bodies, &mut manifolds[i], sub_dt);
156        }
157    }
158}
159
160/// Solve velocity constraints for a single manifold.
161/// `sub_dt` is used to compute speculative contact bias for negative penetration.
162fn resolve_manifold_velocity(bodies: &mut [Option<RigidBody>], manifold: &mut ContactManifold, sub_dt: f32) {
163    let id_a = manifold.body_a as usize;
164    let id_b = manifold.body_b as usize;
165
166    // Extract body data
167    let (inv_ma, inv_ia, fric_a, type_a, xa, ya, cos_a, sin_a) = {
168        let a = match &bodies[id_a] {
169            Some(b) => b,
170            None => return,
171        };
172        (
173            a.inv_mass, a.inv_inertia,
174            a.material.friction, a.body_type, a.x, a.y, a.angle.cos(), a.angle.sin(),
175        )
176    };
177    let (inv_mb, inv_ib, fric_b, type_b, xb, yb, cos_b, sin_b) = {
178        let b = match &bodies[id_b] {
179            Some(b) => b,
180            None => return,
181        };
182        (
183            b.inv_mass, b.inv_inertia,
184            b.material.friction, b.body_type, b.x, b.y, b.angle.cos(), b.angle.sin(),
185        )
186    };
187
188    if type_a != BodyType::Dynamic && type_b != BodyType::Dynamic {
189        return;
190    }
191
192    let (nx, ny) = manifold.normal;
193    let (tx, ty) = manifold.tangent;
194    let velocity_bias = manifold.velocity_bias;
195    let mu = (fric_a * fric_b).sqrt();
196    let num_points = manifold.points.len() as f32;
197
198    // Solve each contact point
199    for point in &mut manifold.points {
200        // Transform local anchors to world
201        let wa_x = point.local_a.0 * cos_a - point.local_a.1 * sin_a + xa;
202        let wa_y = point.local_a.0 * sin_a + point.local_a.1 * cos_a + ya;
203        let wb_x = point.local_b.0 * cos_b - point.local_b.1 * sin_b + xb;
204        let wb_y = point.local_b.0 * sin_b + point.local_b.1 * cos_b + yb;
205
206        // Lever arms
207        let ra_x = wa_x - xa;
208        let ra_y = wa_y - ya;
209        let rb_x = wb_x - xb;
210        let rb_y = wb_y - yb;
211
212        // Re-read current velocities (they may have changed from previous point solve)
213        let (vax, vay, ava) = bodies[id_a].as_ref().map_or((0.0, 0.0, 0.0), |b| (b.vx, b.vy, b.angular_velocity));
214        let (vbx, vby, avb) = bodies[id_b].as_ref().map_or((0.0, 0.0, 0.0), |b| (b.vx, b.vy, b.angular_velocity));
215
216        // Relative velocity at contact point
217        let rel_vx = (vbx + (-avb * rb_y)) - (vax + (-ava * ra_y));
218        let rel_vy = (vby + (avb * rb_x)) - (vay + (ava * ra_x));
219
220        let vn = rel_vx * nx + rel_vy * ny;
221
222        // Effective mass
223        let ra_cross_n = ra_x * ny - ra_y * nx;
224        let rb_cross_n = rb_x * ny - rb_y * nx;
225        let inv_mass_sum = inv_ma + inv_mb
226            + ra_cross_n * ra_cross_n * inv_ia
227            + rb_cross_n * rb_cross_n * inv_ib;
228
229        if inv_mass_sum == 0.0 {
230            continue;
231        }
232
233        // Bias computation: speculative vs restitution are mutually exclusive.
234        // - Speculative (penetration < 0): not touching yet, just limit approach
235        //   velocity so bodies arrive just-touching. No bounce.
236        // - Touching (penetration >= 0): apply restitution for bounce.
237        let per_point_bias = if point.penetration < 0.0 && sub_dt > 0.0 {
238            // Speculative: allow approach up to separation/dt (negative bias)
239            point.penetration / sub_dt
240        } else {
241            // Actual contact: apply restitution bounce
242            velocity_bias / num_points
243        };
244        let j_new = -(vn - per_point_bias) / inv_mass_sum;
245
246        let old_accumulated = point.accumulated_jn;
247        point.accumulated_jn = (old_accumulated + j_new).max(0.0);
248        let j = point.accumulated_jn - old_accumulated;
249
250        if j.abs() > 1e-8 {
251            let impulse_x = j * nx;
252            let impulse_y = j * ny;
253
254            if let Some(a) = &mut bodies[id_a] {
255                if a.body_type == BodyType::Dynamic {
256                    a.vx -= impulse_x * inv_ma;
257                    a.vy -= impulse_y * inv_ma;
258                    a.angular_velocity -= ra_cross_n * j * inv_ia;
259                }
260            }
261            if let Some(b) = &mut bodies[id_b] {
262                if b.body_type == BodyType::Dynamic {
263                    b.vx += impulse_x * inv_mb;
264                    b.vy += impulse_y * inv_mb;
265                    b.angular_velocity += rb_cross_n * j * inv_ib;
266                }
267            }
268        }
269
270        // Friction solve with friction anchors
271        let ra_cross_t = ra_x * ty - ra_y * tx;
272        let rb_cross_t = rb_x * ty - rb_y * tx;
273        let inv_mass_sum_t = inv_ma + inv_mb
274            + ra_cross_t * ra_cross_t * inv_ia
275            + rb_cross_t * rb_cross_t * inv_ib;
276
277        if inv_mass_sum_t > 0.0 {
278            // Re-read velocities after normal solve
279            let (vax, vay, ava) = bodies[id_a].as_ref().map_or((0.0, 0.0, 0.0), |b| (b.vx, b.vy, b.angular_velocity));
280            let (vbx, vby, avb) = bodies[id_b].as_ref().map_or((0.0, 0.0, 0.0), |b| (b.vx, b.vy, b.angular_velocity));
281
282            let rel_vx = (vbx + (-avb * rb_y)) - (vax + (-ava * ra_y));
283            let rel_vy = (vby + (avb * rb_x)) - (vay + (ava * ra_x));
284
285            let vt = rel_vx * tx + rel_vy * ty;
286
287            // Friction anchor: compute tangent position correction
288            // Current world-space contact position (midpoint of both anchors)
289            let current_pos = ((wa_x + wb_x) * 0.5, (wa_y + wb_y) * 0.5);
290
291            // Initialize friction anchor if not set
292            if point.friction_anchor.is_none() {
293                point.friction_anchor = Some(current_pos);
294            }
295
296            // Compute tangent drift from anchor
297            let anchor = point.friction_anchor.unwrap();
298            let drift_x = current_pos.0 - anchor.0;
299            let drift_y = current_pos.1 - anchor.1;
300            let tangent_drift = drift_x * tx + drift_y * ty;
301
302            // Baumgarte correction velocity toward anchor (factor = 0.1)
303            const FRICTION_BAUMGARTE: f32 = 0.1;
304            let correction_velocity = if sub_dt > 0.0 {
305                tangent_drift * FRICTION_BAUMGARTE / sub_dt
306            } else {
307                0.0
308            };
309
310            // Total tangent velocity to correct: actual velocity + anchor drift correction
311            let vt_corrected = vt + correction_velocity;
312            let jt_new = -vt_corrected / inv_mass_sum_t;
313
314            // Coulomb friction
315            let max_friction = point.accumulated_jn * mu;
316            let old_jt = point.accumulated_jt;
317            let new_jt_accumulated = (old_jt + jt_new).clamp(-max_friction, max_friction);
318
319            // Check if we hit the friction limit (sliding)
320            let requested_jt = old_jt + jt_new;
321            let is_sliding = (new_jt_accumulated - requested_jt).abs() > 1e-8;
322
323            if is_sliding {
324                // Reset friction anchor when sliding
325                point.friction_anchor = Some(current_pos);
326            }
327
328            point.accumulated_jt = new_jt_accumulated;
329            let jt = point.accumulated_jt - old_jt;
330
331            if jt.abs() > 1e-8 {
332                let friction_x = jt * tx;
333                let friction_y = jt * ty;
334
335                if let Some(a) = &mut bodies[id_a] {
336                    if a.body_type == BodyType::Dynamic {
337                        a.vx -= friction_x * inv_ma;
338                        a.vy -= friction_y * inv_ma;
339                        a.angular_velocity -= ra_cross_t * jt * inv_ia;
340                    }
341                }
342                if let Some(b) = &mut bodies[id_b] {
343                    if b.body_type == BodyType::Dynamic {
344                        b.vx += friction_x * inv_mb;
345                        b.vy += friction_y * inv_mb;
346                        b.angular_velocity += rb_cross_t * jt * inv_ib;
347                    }
348                }
349            }
350        }
351    }
352}
353
354/// Position correction for manifolds.
355pub fn resolve_manifolds_position(
356    bodies: &mut [Option<RigidBody>],
357    manifolds: &[ContactManifold],
358    reverse: bool,
359) {
360    if reverse {
361        for manifold in manifolds.iter().rev() {
362            for point in &manifold.points {
363                position_correction_manifold_point(bodies, manifold, point);
364            }
365        }
366    } else {
367        for manifold in manifolds {
368            for point in &manifold.points {
369                position_correction_manifold_point(bodies, manifold, point);
370            }
371        }
372    }
373}
374
375fn position_correction_manifold_point(
376    bodies: &mut [Option<RigidBody>],
377    manifold: &ContactManifold,
378    point: &ManifoldPoint,
379) {
380    let id_a = manifold.body_a as usize;
381    let id_b = manifold.body_b as usize;
382
383    let (inv_ma, type_a) = match &bodies[id_a] {
384        Some(b) => (b.inv_mass, b.body_type),
385        None => return,
386    };
387    let (inv_mb, type_b) = match &bodies[id_b] {
388        Some(b) => (b.inv_mass, b.body_type),
389        None => return,
390    };
391
392    if type_a != BodyType::Dynamic && type_b != BodyType::Dynamic {
393        return;
394    }
395
396    let slop = 0.005;
397    let max_correction = 0.2;
398    let baumgarte = 0.2;
399
400    let pen = (point.penetration - slop).max(0.0);
401    let inv_total = inv_ma + inv_mb;
402    if inv_total == 0.0 {
403        return;
404    }
405
406    let correction = (pen * baumgarte).min(max_correction) / inv_total;
407
408    let (nx, ny) = manifold.normal;
409
410    if let Some(a) = &mut bodies[id_a] {
411        if a.body_type == BodyType::Dynamic {
412            a.x -= correction * inv_ma * nx;
413            a.y -= correction * inv_ma * ny;
414        }
415    }
416    if let Some(b) = &mut bodies[id_b] {
417        if b.body_type == BodyType::Dynamic {
418            b.x += correction * inv_mb * nx;
419            b.y += correction * inv_mb * ny;
420        }
421    }
422}