Skip to main content

box3d_rust/contact/
collide.rs

1// Narrow-phase collide pass and contact state machine from physics_world.c.
2//
3// Serial path only (worker_count == 1).
4//
5// SPDX-FileCopyrightText: 2025 Erin Catto
6// SPDX-License-Identifier: MIT
7
8use super::contact_flags;
9use super::{destroy_contact, update_contact};
10use crate::bitset::BitSet;
11use crate::body::body_flags;
12use crate::constants::{
13    speculative_distance, CONTACT_MANIFOLD_COUNT_BUCKETS, CONTACT_RECYCLE_ANGULAR_DISTANCE,
14    GRAPH_COLOR_COUNT,
15};
16use crate::constraint_graph::{add_contact_to_graph, remove_contact_from_graph};
17use crate::core::{ctz64, NULL_INDEX};
18use crate::events::{ContactBeginTouchEvent, ContactEndTouchEvent};
19use crate::id::{ContactId, ShapeId};
20use crate::island::{link_contact, unlink_contact};
21use crate::math_functions::{
22    aabb_overlaps, abs, conjugate, distance_squared, dot, dot_quat, inv_mul_quat,
23    inv_mul_world_transforms, length_squared, make_matrix_from_quat, max, min_float, min_int,
24    mul_mv, mul_quat, sub, sub_pos, Vec3,
25};
26use crate::solver_set::{AWAKE_SET, STATIC_SET};
27use crate::types::BodyType;
28use crate::world::World;
29
30/// Modified cross used by contact recycling (simd.h b3ModifiedCross scalar).
31fn modified_cross(a: Vec3, b: Vec3) -> Vec3 {
32    Vec3 {
33        x: a.y * b.z + a.z * b.y,
34        y: a.z * b.x + a.x * b.z,
35        z: a.x * b.y + a.y * b.x,
36    }
37}
38
39/// (b3AddNonTouchingContact)
40fn add_non_touching_contact(world: &mut World, contact_id: i32) {
41    debug_assert!(world.contacts[contact_id as usize].set_index == AWAKE_SET);
42    let local_index = world.solver_sets[AWAKE_SET as usize].contact_indices.len() as i32;
43    {
44        let contact = &mut world.contacts[contact_id as usize];
45        contact.color_index = NULL_INDEX;
46        contact.local_index = local_index;
47        contact.body_sim_index_a = NULL_INDEX;
48        contact.body_sim_index_b = NULL_INDEX;
49    }
50    world.solver_sets[AWAKE_SET as usize]
51        .contact_indices
52        .push(contact_id);
53}
54
55/// (b3RemoveNonTouchingContact)
56fn remove_non_touching_contact(world: &mut World, set_index: i32, local_index: i32) {
57    let set = &mut world.solver_sets[set_index as usize];
58    let moved_index = set.contact_indices.len() as i32 - 1;
59    set.contact_indices.swap_remove(local_index as usize);
60    if moved_index != local_index {
61        let moved_contact_id = set.contact_indices[local_index as usize];
62        let moved = &mut world.contacts[moved_contact_id as usize];
63        debug_assert!(moved.set_index == set_index);
64        debug_assert!(moved.color_index == NULL_INDEX);
65        debug_assert!(moved.local_index == moved_index);
66        moved.local_index = local_index;
67    }
68}
69
70/// Per-contact narrow phase. (b3CollideTask — serial over full range)
71fn collide_task(world: &mut World, contact_indices: &[i32], worker_index: i32) {
72    let recycle_distance = world.contact_recycle_distance;
73    let speculative = speculative_distance();
74    let recycle_distance_non_touching = min_float(recycle_distance, speculative);
75
76    for &contact_index in contact_indices {
77        debug_assert!((contact_index as usize) < world.contacts.len());
78        debug_assert!(world.contacts[contact_index as usize].contact_id == contact_index);
79
80        let shape_id_a = world.contacts[contact_index as usize].shape_id_a;
81        let shape_id_b = world.contacts[contact_index as usize].shape_id_b;
82
83        let overlap = aabb_overlaps(
84            world.shapes[shape_id_a as usize].fat_aabb,
85            world.shapes[shape_id_b as usize].fat_aabb,
86        );
87        if !overlap {
88            world.contacts[contact_index as usize].flags |= contact_flags::SIM_DISJOINT;
89            world.contacts[contact_index as usize].flags &= !contact_flags::SIM_TOUCHING;
90            world.task_contexts[worker_index as usize]
91                .contact_state_bit_set
92                .set_bit(contact_index as u32);
93            continue;
94        }
95
96        let body_id_a = world.shapes[shape_id_a as usize].body_id;
97        let body_id_b = world.shapes[shape_id_b as usize].body_id;
98        let is_static_a = world.bodies[body_id_a as usize].type_ == BodyType::Static;
99        let is_static_b = world.bodies[body_id_b as usize].type_ == BodyType::Static;
100        let was_touching =
101            (world.contacts[contact_index as usize].flags & contact_flags::SIM_TOUCHING) != 0;
102        let is_mesh_contact =
103            (world.contacts[contact_index as usize].flags & contact_flags::SIM_MESH_CONTACT) != 0;
104
105        let set_a = world.bodies[body_id_a as usize].set_index;
106        let set_b = world.bodies[body_id_b as usize].set_index;
107        let local_a = world.bodies[body_id_a as usize].local_index;
108        let local_b = world.bodies[body_id_b as usize].local_index;
109
110        if was_touching {
111            debug_assert!(set_a == AWAKE_SET || set_a == STATIC_SET);
112            debug_assert!(set_b == AWAKE_SET || set_b == STATIC_SET);
113        }
114
115        let sim_a = world.solver_sets[set_a as usize].body_sims[local_a as usize];
116        let sim_b = world.solver_sets[set_b as usize].body_sims[local_b as usize];
117        let transform_a = sim_a.transform;
118        let transform_b = sim_b.transform;
119        let is_fast =
120            (sim_a.flags & body_flags::IS_FAST) != 0 || (sim_b.flags & body_flags::IS_FAST) != 0;
121
122        {
123            let contact = &mut world.contacts[contact_index as usize];
124            contact.body_sim_index_a = if is_static_a { NULL_INDEX } else { local_a };
125            contact.body_sim_index_b = if is_static_b { NULL_INDEX } else { local_b };
126        }
127
128        let recycle_tolerance = if was_touching {
129            recycle_distance
130        } else {
131            recycle_distance_non_touching
132        };
133
134        let flags = world.contacts[contact_index as usize].flags;
135        if (!is_fast || !is_mesh_contact)
136            && recycle_distance > 0.0
137            && (flags & contact_flags::RELATIVE_TRANSFORM_VALID) != 0
138            && (flags & contact_flags::RECYCLE) != 0
139        {
140            let contact = &world.contacts[contact_index as usize];
141            let angle_a = dot_quat(transform_a.q, contact.cached_rotation_a);
142            let angle_b = dot_quat(transform_b.q, contact.cached_rotation_b);
143            let angular_distance = min_float(angle_a * angle_a, angle_b * angle_b);
144
145            let xf = inv_mul_world_transforms(transform_a, transform_b);
146            let xfc = contact.cached_relative_pose;
147            let max_extent_a = if is_static_a {
148                Vec3 {
149                    x: 0.0,
150                    y: 0.0,
151                    z: 0.0,
152                }
153            } else {
154                sim_a.max_extent
155            };
156            let max_extent_b = if is_static_b {
157                Vec3 {
158                    x: 0.0,
159                    y: 0.0,
160                    z: 0.0,
161                }
162            } else {
163                sim_b.max_extent
164            };
165            let max_extent = max(max_extent_a, max_extent_b);
166            let dist_squared = distance_squared(xf.p, xfc.p);
167
168            if angular_distance > CONTACT_RECYCLE_ANGULAR_DISTANCE
169                && dist_squared < recycle_tolerance * recycle_tolerance
170            {
171                let distance = dist_squared.sqrt();
172                let slack = recycle_tolerance - distance;
173                let qr = inv_mul_quat(xfc.q, xf.q);
174                let arc = modified_cross(abs(qr.v), max_extent);
175                let arc_sq = 4.0 * length_squared(arc);
176                if arc_sq < slack * slack {
177                    let dq_a = mul_quat(transform_a.q, conjugate(contact.cached_rotation_a));
178                    let dq_b = mul_quat(transform_b.q, conjugate(contact.cached_rotation_b));
179                    let matrix_a = make_matrix_from_quat(dq_a);
180                    let matrix_b = make_matrix_from_quat(dq_b);
181                    let dc = sub_pos(sim_b.center, sim_a.center);
182
183                    let manifold_count = contact.manifold_count();
184                    for manifold_index in 0..manifold_count as usize {
185                        let normal =
186                            world.contacts[contact_index as usize].manifolds[manifold_index].normal;
187                        let point_count = world.contacts[contact_index as usize].manifolds
188                            [manifold_index]
189                            .point_count;
190                        for point_index in 0..point_count as usize {
191                            let mp = &mut world.contacts[contact_index as usize].manifolds
192                                [manifold_index]
193                                .points[point_index];
194                            let r_a = mul_mv(matrix_a, mp.anchor_a);
195                            let r_b = mul_mv(matrix_b, mp.anchor_b);
196                            let dp = crate::math_functions::add(dc, sub(r_b, r_a));
197                            mp.separation = mp.base_separation + dot(dp, normal);
198                            mp.persisted = true;
199                        }
200                    }
201
202                    world.task_contexts[worker_index as usize].recycled_contact_count += 1;
203                    let bucket_index =
204                        min_int(manifold_count, CONTACT_MANIFOLD_COUNT_BUCKETS as i32 - 1);
205                    if bucket_index > 0 {
206                        world.task_contexts[worker_index as usize].manifold_counts
207                            [(bucket_index - 1) as usize] += 1;
208                    }
209                    continue;
210                }
211            }
212        }
213
214        {
215            let contact = &mut world.contacts[contact_index as usize];
216            contact.cached_rotation_a = transform_a.q;
217            contact.cached_rotation_b = transform_b.q;
218            contact.cached_relative_pose = inv_mul_world_transforms(transform_a, transform_b);
219            contact.flags |= contact_flags::RELATIVE_TRANSFORM_VALID;
220        }
221
222        let touching = update_contact(
223            world,
224            worker_index,
225            contact_index,
226            shape_id_a,
227            sim_a.local_center,
228            transform_a,
229            shape_id_b,
230            sim_b.local_center,
231            transform_b,
232            is_fast,
233        );
234
235        let manifold_count = world.contacts[contact_index as usize].manifold_count();
236        let bucket_index = min_int(manifold_count, CONTACT_MANIFOLD_COUNT_BUCKETS as i32 - 1);
237        if bucket_index > 0 {
238            world.task_contexts[worker_index as usize].manifold_counts
239                [(bucket_index - 1) as usize] += 1;
240        }
241
242        if touching
243            && was_touching
244            && (world.contacts[contact_index as usize].flags & contact_flags::SIM_MESH_CONTACT) != 0
245        {
246            let color_index = world.contacts[contact_index as usize].color_index;
247            let local_index = world.contacts[contact_index as usize].local_index;
248            debug_assert!(color_index != NULL_INDEX);
249            debug_assert!(0 <= color_index && color_index < GRAPH_COLOR_COUNT);
250            world.constraint_graph.colors[color_index as usize].contacts[local_index as usize]
251                .manifold_count = manifold_count as u16;
252        }
253
254        if touching && !was_touching {
255            world.contacts[contact_index as usize].flags |= contact_flags::SIM_STARTED_TOUCHING;
256            world.task_contexts[worker_index as usize]
257                .contact_state_bit_set
258                .set_bit(contact_index as u32);
259        } else if !touching && was_touching {
260            world.contacts[contact_index as usize].flags |= contact_flags::SIM_STOPPED_TOUCHING;
261            world.task_contexts[worker_index as usize]
262                .contact_state_bit_set
263                .set_bit(contact_index as u32);
264        }
265
266        let manifold_count = world.contacts[contact_index as usize].manifold_count();
267        for manifold_index in 0..manifold_count as usize {
268            let point_count =
269                world.contacts[contact_index as usize].manifolds[manifold_index].point_count;
270            for point_index in 0..point_count as usize {
271                let mp = &mut world.contacts[contact_index as usize].manifolds[manifold_index]
272                    .points[point_index];
273                mp.base_separation = mp.separation;
274            }
275        }
276    }
277}
278
279/// Narrow-phase collision. (b3Collide)
280pub fn collide(world: &mut World, dt: f32) {
281    debug_assert!(world.worker_count > 0);
282
283    let mut touching_count = 0;
284    for i in 0..GRAPH_COLOR_COUNT as usize {
285        let color = &world.constraint_graph.colors[i];
286        touching_count += color.convex_contacts.len() + color.contacts.len();
287    }
288
289    let non_touching_count = world.solver_sets[AWAKE_SET as usize].contact_indices.len();
290    let contact_count = touching_count + non_touching_count;
291    if contact_count == 0 {
292        return;
293    }
294
295    let mut contact_indices = Vec::with_capacity(contact_count);
296    for i in 0..GRAPH_COLOR_COUNT as usize {
297        let color = &world.constraint_graph.colors[i];
298        contact_indices.extend_from_slice(&color.convex_contacts);
299        for spec in &color.contacts {
300            contact_indices.push(spec.contact_id);
301        }
302    }
303    debug_assert!(contact_indices.len() == touching_count);
304    if non_touching_count > 0 {
305        contact_indices.extend_from_slice(&world.solver_sets[AWAKE_SET as usize].contact_indices);
306    }
307
308    let contact_id_capacity = world.contact_id_pool.id_capacity();
309    for task in &mut world.task_contexts {
310        task.contact_state_bit_set
311            .set_bit_count_and_clear(contact_id_capacity as u32);
312        task.sat_call_count = 0;
313        task.sat_cache_hit_count = 0;
314        task.recycled_contact_count = 0;
315        task.manifold_counts = [0; CONTACT_MANIFOLD_COUNT_BUCKETS];
316    }
317
318    // Serial: one worker covers the full range.
319    collide_task(world, &contact_indices, 0);
320
321    let sat_multiplier = if dt > 0.0 { 1 } else { 0 };
322    world.sat_call_count = sat_multiplier * world.task_contexts[0].sat_call_count;
323    world.sat_cache_hit_count = sat_multiplier * world.task_contexts[0].sat_cache_hit_count;
324    world.manifold_counts = world.task_contexts[0].manifold_counts;
325
326    // Union worker bitsets (serial: only worker 0).
327    let bit_set: BitSet = world.task_contexts[0].contact_state_bit_set.clone();
328    let end_event_array_index = world.end_event_array_index;
329    let world_id = world.world_id;
330
331    for k in 0..bit_set.block_count() {
332        let mut bits = bit_set.block(k);
333        while bits != 0 {
334            let ctz = ctz64(bits);
335            let contact_id = (64 * k + ctz) as i32;
336
337            let (shape_id_a, shape_id_b, flags, generation) = {
338                let contact = &world.contacts[contact_id as usize];
339                debug_assert!(contact.set_index == AWAKE_SET);
340                (
341                    contact.shape_id_a,
342                    contact.shape_id_b,
343                    contact.flags,
344                    contact.generation,
345                )
346            };
347
348            let shape_a = &world.shapes[shape_id_a as usize];
349            let shape_b = &world.shapes[shape_id_b as usize];
350            let shape_id_a_full = ShapeId {
351                index1: shape_a.id + 1,
352                world0: world_id,
353                generation: shape_a.generation,
354            };
355            let shape_id_b_full = ShapeId {
356                index1: shape_b.id + 1,
357                world0: world_id,
358                generation: shape_b.generation,
359            };
360            let contact_full_id = ContactId {
361                index1: contact_id + 1,
362                world0: world_id,
363                padding: 0,
364                generation,
365            };
366
367            if (flags & contact_flags::SIM_DISJOINT) != 0 {
368                destroy_contact(world, contact_id, false);
369            } else if (flags & contact_flags::SIM_STARTED_TOUCHING) != 0 {
370                debug_assert!(world.contacts[contact_id as usize].island_id == NULL_INDEX);
371
372                if (flags & contact_flags::ENABLE_CONTACT_EVENTS) != 0 {
373                    world.contact_begin_events.push(ContactBeginTouchEvent {
374                        shape_id_a: shape_id_a_full,
375                        shape_id_b: shape_id_b_full,
376                        contact_id: contact_full_id,
377                    });
378                }
379
380                debug_assert!(world.contacts[contact_id as usize].manifold_count() > 0);
381                debug_assert!(world.contacts[contact_id as usize].set_index == AWAKE_SET);
382
383                world.contacts[contact_id as usize].flags &= !contact_flags::SIM_STARTED_TOUCHING;
384                world.contacts[contact_id as usize].flags |= contact_flags::TOUCHING;
385                link_contact(world, contact_id);
386
387                debug_assert!(world.contacts[contact_id as usize].color_index == NULL_INDEX);
388
389                let old_local_index = world.contacts[contact_id as usize].local_index;
390                add_contact_to_graph(world, contact_id);
391                remove_non_touching_contact(world, AWAKE_SET, old_local_index);
392            } else if (flags & contact_flags::SIM_STOPPED_TOUCHING) != 0 {
393                world.contacts[contact_id as usize].flags &= !contact_flags::SIM_STOPPED_TOUCHING;
394                world.contacts[contact_id as usize].flags &= !contact_flags::TOUCHING;
395
396                if (world.contacts[contact_id as usize].flags
397                    & contact_flags::ENABLE_CONTACT_EVENTS)
398                    != 0
399                {
400                    world.contact_end_events[end_event_array_index as usize].push(
401                        ContactEndTouchEvent {
402                            shape_id_a: shape_id_a_full,
403                            shape_id_b: shape_id_b_full,
404                            contact_id: contact_full_id,
405                        },
406                    );
407                }
408
409                debug_assert!(world.contacts[contact_id as usize].manifolds.is_empty());
410
411                let color_index = world.contacts[contact_id as usize].color_index;
412                let local_index = world.contacts[contact_id as usize].local_index;
413                let body_id_a = world.contacts[contact_id as usize].edges[0].body_id;
414                let body_id_b = world.contacts[contact_id as usize].edges[1].body_id;
415                let is_mesh = (world.contacts[contact_id as usize].flags
416                    & contact_flags::SIM_MESH_CONTACT)
417                    != 0;
418
419                unlink_contact(world, contact_id);
420                add_non_touching_contact(world, contact_id);
421                remove_contact_from_graph(
422                    world,
423                    body_id_a,
424                    body_id_b,
425                    color_index,
426                    local_index,
427                    is_mesh,
428                );
429            }
430
431            bits &= bits - 1;
432        }
433    }
434
435    world.validate_solver_sets();
436}