Skip to main content

arcane_engine/physics/
resolve.rs

1use super::types::{BodyType, Contact, RigidBody};
2
3/// Pre-compute solver data for each contact: velocity bias (restitution) and tangent direction.
4/// Must be called ONCE per frame before warm starting and solver iterations.
5/// This follows the Box2D approach: bias and tangent are fixed for the entire solve.
6pub fn initialize_contacts(
7    bodies: &[Option<RigidBody>],
8    contacts: &mut [Contact],
9    restitution_threshold: f32,
10) {
11    for contact in contacts.iter_mut() {
12        let id_a = contact.body_a as usize;
13        let id_b = contact.body_b as usize;
14
15        let a = match &bodies[id_a] {
16            Some(b) => b,
17            None => continue,
18        };
19        let b = match &bodies[id_b] {
20            Some(b) => b,
21            None => continue,
22        };
23
24        let (nx, ny) = contact.normal;
25        let (cpx, cpy) = contact.contact_point;
26
27        let ra_x = cpx - a.x;
28        let ra_y = cpy - a.y;
29        let rb_x = cpx - b.x;
30        let rb_y = cpy - b.y;
31
32        // Relative velocity at contact point
33        let rel_vx = (b.vx + (-b.angular_velocity * rb_y)) - (a.vx + (-a.angular_velocity * ra_y));
34        let rel_vy = (b.vy + (b.angular_velocity * rb_x)) - (a.vy + (a.angular_velocity * ra_x));
35
36        let vn = rel_vx * nx + rel_vy * ny;
37
38        // Restitution bias: only apply bounce for approach speeds above threshold
39        let e = if -vn < restitution_threshold {
40            0.0
41        } else {
42            a.material.restitution.min(b.material.restitution)
43        };
44        contact.velocity_bias = e * (-vn).max(0.0);
45
46        // Tangent direction from initial relative velocity (fixed for all iterations)
47        let tx = rel_vx - vn * nx;
48        let ty = rel_vy - vn * ny;
49        let t_len = (tx * tx + ty * ty).sqrt();
50        if t_len > 1e-8 {
51            contact.tangent = (tx / t_len, ty / t_len);
52        } else {
53            // Default tangent: 90° rotation of normal
54            contact.tangent = (-ny, nx);
55        }
56    }
57}
58
59/// Pre-apply cached impulses from previous frame (warm starting).
60/// This gives the iterative solver a head start, dramatically improving convergence for stacks.
61pub fn warm_start_contacts(
62    bodies: &mut [Option<RigidBody>],
63    contacts: &[Contact],
64) {
65    for contact in contacts {
66        let jn = contact.accumulated_jn;
67        let jt = contact.accumulated_jt;
68        if jn == 0.0 && jt == 0.0 {
69            continue;
70        }
71
72        let id_a = contact.body_a as usize;
73        let id_b = contact.body_b as usize;
74        let (nx, ny) = contact.normal;
75        let (tx, ty) = contact.tangent;
76
77        // Combined impulse: normal + friction
78        let px = jn * nx + jt * tx;
79        let py = jn * ny + jt * ty;
80
81        let (cpx, cpy) = contact.contact_point;
82
83        if let Some(a) = &mut bodies[id_a] {
84            if a.body_type == BodyType::Dynamic {
85                let ra_x = cpx - a.x;
86                let ra_y = cpy - a.y;
87                a.vx -= px * a.inv_mass;
88                a.vy -= py * a.inv_mass;
89                let ra_cross_p = ra_x * py - ra_y * px;
90                a.angular_velocity -= ra_cross_p * a.inv_inertia;
91            }
92        }
93        if let Some(b) = &mut bodies[id_b] {
94            if b.body_type == BodyType::Dynamic {
95                let rb_x = cpx - b.x;
96                let rb_y = cpy - b.y;
97                b.vx += px * b.inv_mass;
98                b.vy += py * b.inv_mass;
99                let rb_cross_p = rb_x * py - rb_y * px;
100                b.angular_velocity += rb_cross_p * b.inv_inertia;
101            }
102        }
103    }
104}
105
106/// One iteration of velocity-level contact resolution with accumulated impulse clamping.
107/// `reverse`: if true, iterate contacts in reverse order (for alternating Gauss-Seidel).
108pub fn resolve_contacts_velocity_iteration(
109    bodies: &mut [Option<RigidBody>],
110    contacts: &mut [Contact],
111    reverse: bool,
112) {
113    let len = contacts.len();
114    if reverse {
115        for i in (0..len).rev() {
116            resolve_single_accumulated(bodies, &mut contacts[i]);
117        }
118    } else {
119        for i in 0..len {
120            resolve_single_accumulated(bodies, &mut contacts[i]);
121        }
122    }
123}
124
125/// Position correction (Baumgarte stabilization) for all contacts.
126/// `reverse`: if true, iterate contacts in reverse order.
127pub fn resolve_contacts_position(
128    bodies: &mut [Option<RigidBody>],
129    contacts: &[Contact],
130    reverse: bool,
131) {
132    if reverse {
133        for contact in contacts.iter().rev() {
134            position_correction(bodies, contact);
135        }
136    } else {
137        for contact in contacts {
138            position_correction(bodies, contact);
139        }
140    }
141}
142
143/// Sequential impulse solver for contact resolution (backward compat).
144pub fn resolve_contacts(
145    bodies: &mut [Option<RigidBody>],
146    contacts: &mut [Contact],
147    iterations: usize,
148) {
149    let iterations = if iterations == 0 { 6 } else { iterations };
150
151    // Pre-compute bias and tangent (no restitution threshold for standalone use)
152    initialize_contacts(bodies, contacts, 0.0);
153
154    for i in 0..iterations {
155        resolve_contacts_velocity_iteration(bodies, contacts, i % 2 == 1);
156    }
157
158    resolve_contacts_position(bodies, contacts, false);
159}
160
161/// Accumulated impulse solver — the key to warm starting (Box2D approach).
162/// Uses pre-computed velocity_bias and tangent direction from initialize_contacts().
163/// Tracks total accumulated impulse and only applies the DELTA each iteration.
164/// Normal impulse clamped non-negative (can't pull). Friction clamped to Coulomb cone.
165fn resolve_single_accumulated(bodies: &mut [Option<RigidBody>], contact: &mut Contact) {
166    let id_a = contact.body_a as usize;
167    let id_b = contact.body_b as usize;
168
169    let (inv_ma, inv_ia, vax, vay, ava, fric_a, type_a, xa, ya) = {
170        let a = match &bodies[id_a] {
171            Some(b) => b,
172            None => return,
173        };
174        (
175            a.inv_mass, a.inv_inertia, a.vx, a.vy, a.angular_velocity,
176            a.material.friction, a.body_type, a.x, a.y,
177        )
178    };
179    let (inv_mb, inv_ib, vbx, vby, avb, fric_b, type_b, xb, yb) = {
180        let b = match &bodies[id_b] {
181            Some(b) => b,
182            None => return,
183        };
184        (
185            b.inv_mass, b.inv_inertia, b.vx, b.vy, b.angular_velocity,
186            b.material.friction, b.body_type, b.x, b.y,
187        )
188    };
189
190    if type_a != BodyType::Dynamic && type_b != BodyType::Dynamic {
191        return;
192    }
193
194    let (nx, ny) = contact.normal;
195    let (cpx, cpy) = contact.contact_point;
196
197    let ra_x = cpx - xa;
198    let ra_y = cpy - ya;
199    let rb_x = cpx - xb;
200    let rb_y = cpy - yb;
201
202    // Relative velocity at contact point
203    let rel_vx = (vbx + (-avb * rb_y)) - (vax + (-ava * ra_y));
204    let rel_vy = (vby + (avb * rb_x)) - (vay + (ava * ra_x));
205
206    let vn = rel_vx * nx + rel_vy * ny;
207
208    let ra_cross_n = ra_x * ny - ra_y * nx;
209    let rb_cross_n = rb_x * ny - rb_y * nx;
210
211    let inv_mass_sum = inv_ma + inv_mb
212        + ra_cross_n * ra_cross_n * inv_ia
213        + rb_cross_n * rb_cross_n * inv_ib;
214
215    if inv_mass_sum == 0.0 {
216        return;
217    }
218
219    // Normal impulse with accumulated clamping.
220    // Uses pre-computed velocity_bias for restitution (fixed for all iterations).
221    // velocity_bias is the target rebound speed (positive). We want vn >= velocity_bias.
222    let j_new = -(vn - contact.velocity_bias) / inv_mass_sum;
223
224    // Clamp accumulated impulse: can't pull (impulse must be non-negative)
225    let old_accumulated = contact.accumulated_jn;
226    contact.accumulated_jn = (old_accumulated + j_new).max(0.0);
227    let j = contact.accumulated_jn - old_accumulated; // Apply only the delta
228
229    if j.abs() > 1e-8 {
230        let impulse_x = j * nx;
231        let impulse_y = j * ny;
232
233        if let Some(a) = &mut bodies[id_a] {
234            if a.body_type == BodyType::Dynamic {
235                a.vx -= impulse_x * inv_ma;
236                a.vy -= impulse_y * inv_ma;
237                a.angular_velocity -= ra_cross_n * j * inv_ia;
238            }
239        }
240        if let Some(b) = &mut bodies[id_b] {
241            if b.body_type == BodyType::Dynamic {
242                b.vx += impulse_x * inv_mb;
243                b.vy += impulse_y * inv_mb;
244                b.angular_velocity += rb_cross_n * j * inv_ib;
245            }
246        }
247    }
248
249    // Friction impulse with accumulated clamping.
250    // Uses pre-computed tangent direction (fixed for all iterations).
251    let (tx, ty) = contact.tangent;
252
253    // Recompute relative velocity (bodies changed from normal impulse)
254    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));
255    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));
256
257    let rel_vx = (vbx + (-avb * rb_y)) - (vax + (-ava * ra_y));
258    let rel_vy = (vby + (avb * rb_x)) - (vay + (ava * ra_x));
259
260    let ra_cross_t = ra_x * ty - ra_y * tx;
261    let rb_cross_t = rb_x * ty - rb_y * tx;
262
263    let inv_mass_sum_t = inv_ma + inv_mb
264        + ra_cross_t * ra_cross_t * inv_ia
265        + rb_cross_t * rb_cross_t * inv_ib;
266
267    if inv_mass_sum_t > 0.0 {
268        let vt = rel_vx * tx + rel_vy * ty;
269        let jt_new = -vt / inv_mass_sum_t;
270
271        // Coulomb friction: clamp accumulated friction to friction cone
272        let mu = (fric_a * fric_b).sqrt();
273        let max_friction = contact.accumulated_jn * mu;
274        let old_jt = contact.accumulated_jt;
275        contact.accumulated_jt = (old_jt + jt_new).clamp(-max_friction, max_friction);
276        let jt = contact.accumulated_jt - old_jt;
277
278        if jt.abs() > 1e-8 {
279            let friction_x = jt * tx;
280            let friction_y = jt * ty;
281
282            if let Some(a) = &mut bodies[id_a] {
283                if a.body_type == BodyType::Dynamic {
284                    a.vx -= friction_x * inv_ma;
285                    a.vy -= friction_y * inv_ma;
286                    a.angular_velocity -= ra_cross_t * jt * inv_ia;
287                }
288            }
289            if let Some(b) = &mut bodies[id_b] {
290                if b.body_type == BodyType::Dynamic {
291                    b.vx += friction_x * inv_mb;
292                    b.vy += friction_y * inv_mb;
293                    b.angular_velocity += rb_cross_t * jt * inv_ib;
294                }
295            }
296        }
297    }
298}
299
300fn position_correction(bodies: &mut [Option<RigidBody>], contact: &Contact) {
301    let id_a = contact.body_a as usize;
302    let id_b = contact.body_b as usize;
303
304    let (inv_ma, type_a) = match &bodies[id_a] {
305        Some(b) => (b.inv_mass, b.body_type),
306        None => return,
307    };
308    let (inv_mb, type_b) = match &bodies[id_b] {
309        Some(b) => (b.inv_mass, b.body_type),
310        None => return,
311    };
312
313    if type_a != BodyType::Dynamic && type_b != BodyType::Dynamic {
314        return;
315    }
316
317    let slop = 0.005;
318    let baumgarte = 0.4;
319
320    let correction = ((contact.penetration - slop).max(0.0) * baumgarte) / (inv_ma + inv_mb);
321
322    let (nx, ny) = contact.normal;
323
324    if let Some(a) = &mut bodies[id_a] {
325        if a.body_type == BodyType::Dynamic {
326            a.x -= correction * inv_ma * nx;
327            a.y -= correction * inv_ma * ny;
328        }
329    }
330    if let Some(b) = &mut bodies[id_b] {
331        if b.body_type == BodyType::Dynamic {
332            b.x += correction * inv_mb * nx;
333            b.y += correction * inv_mb * ny;
334        }
335    }
336}