Skip to main content

box3d_rust/shape/
toi.rs

1//! Shape time-of-impact dispatch, including mesh / height / compound CCD.
2//!
3//! Port of `b3ShapeTimeOfImpact` and helpers from
4//! `box3d-cpp-reference/src/shape.c`.
5//!
6//! SPDX-FileCopyrightText: 2025 Erin Catto
7//! SPDX-License-Identifier: MIT
8
9use super::{make_shape_proxy, Shape, ShapeGeometry};
10use crate::compound::{
11    get_compound_child, make_compound_child_sweep, query_compound, ChildGeometry,
12};
13use crate::constants::{linear_slop, speculative_distance};
14use crate::core::NULL_INDEX;
15use crate::distance::{
16    get_sweep_transform, make_proxy, time_of_impact, ShapeProxy, Sweep, ToiInput, ToiOutput,
17};
18use crate::geometry::{compute_swept_capsule_aabb, compute_swept_sphere_aabb, ShapeType};
19use crate::height_field::query_height_field;
20use crate::hull::{compute_swept_hull_aabb, get_hull_points};
21use crate::math_functions::{
22    aabb_transform, cross, dot, inv_transform_point, invert_transform, max_float, mul_transforms,
23    normalize, rotate_vector, sub, transform_point, Aabb, Transform, Vec3, VEC3_ZERO,
24};
25use crate::mesh::{query_mesh, Mesh};
26
27/// Swept AABB of a convex shape along a sweep at time in [0, 1].
28/// (b3ComputeSweptShapeAABB)
29pub fn compute_swept_shape_aabb(shape: &Shape, sweep: &Sweep, time: f32) -> Aabb {
30    debug_assert!((0.0..=1.0).contains(&time));
31    let xf1 = Transform {
32        p: sub(sweep.c1, rotate_vector(sweep.q1, sweep.local_center)),
33        q: sweep.q1,
34    };
35    let xf2 = get_sweep_transform(sweep, time);
36
37    match &shape.geometry {
38        ShapeGeometry::Capsule(capsule) => compute_swept_capsule_aabb(capsule, xf1, xf2),
39        ShapeGeometry::Hull(hull) => compute_swept_hull_aabb(hull, xf1, xf2),
40        ShapeGeometry::Sphere(sphere) => compute_swept_sphere_aabb(sphere, xf1, xf2),
41        _ => {
42            debug_assert!(false, "compute_swept_shape_aabb is for convex shapes only");
43            Aabb {
44                lower_bound: xf1.p,
45                upper_bound: xf1.p,
46            }
47        }
48    }
49}
50
51/// Context for mesh / height-field triangle TOI queries. (b3MeshImpactContext)
52struct MeshImpactContext {
53    toi_input: ToiInput,
54    toi_output: ToiOutput,
55    /// Centroid of shape B in body B local space
56    local_centroid_b: Vec3,
57    /// Centroid of shape B at start of sweep, in mesh local space
58    mesh_local_centroid_b1: Vec3,
59    /// Centroid of shape B at end of sweep, in mesh local space
60    mesh_local_centroid_b2: Vec3,
61    fallback_radius: f32,
62    is_sensor: bool,
63}
64
65/// Per-triangle callback for mesh / height TOI. (b3MeshTimeOfImpactFcn)
66fn mesh_time_of_impact_fcn(a: Vec3, b: Vec3, c: Vec3, context: &mut MeshImpactContext) -> bool {
67    // Early out for parallel movement
68    let c1 = context.mesh_local_centroid_b1;
69    let c2 = context.mesh_local_centroid_b2;
70
71    let n = normalize(cross(sub(b, a), sub(c, a)));
72    let offset1 = dot(n, sub(c1, a));
73    let offset2 = dot(n, sub(c2, a));
74
75    if offset1 < 0.0 {
76        // Started behind or finished in front
77        return true;
78    }
79
80    if !context.is_sensor
81        && offset1 - offset2 < context.fallback_radius
82        && offset2 > context.fallback_radius
83    {
84        // Finished in front
85        return true;
86    }
87
88    let triangle = [a, b, c];
89    context.toi_input.proxy_a = make_proxy(&triangle, 0.0);
90
91    let mut output = time_of_impact(&context.toi_input);
92
93    // It is possible for a hit at fraction == 0
94
95    if 0.0 < output.fraction && output.fraction < context.toi_input.max_fraction {
96        context.toi_output = output;
97        context.toi_input.max_fraction = output.fraction;
98    } else if output.fraction == 0.0 {
99        // Fallback to TOI of a small circle around the fast shape centroid
100        let mut fallback_input = context.toi_input;
101        fallback_input.proxy_b = make_proxy(
102            &[context.local_centroid_b],
103            context.fallback_radius + linear_slop(),
104        );
105        output = time_of_impact(&fallback_input);
106
107        if 0.0 < output.fraction && output.fraction < context.toi_input.max_fraction {
108            context.toi_output = output;
109            context.toi_input.max_fraction = output.fraction;
110            context.toi_output.used_fallback = true;
111        }
112    }
113
114    // Continue the query
115    true
116}
117
118/// Context for compound child TOI queries. (b3CompoundImpactContext)
119struct CompoundImpactContext {
120    toi_input: ToiInput,
121    toi_output: ToiOutput,
122    compound_transform: Transform,
123    /// Bounds local to compound
124    local_sweep_bounds_b: Aabb,
125    /// Centroid of shape B in body B local space
126    local_centroid_b: Vec3,
127    fallback_radius: f32,
128}
129
130/// Per-child callback for compound TOI. (b3CompoundTimeOfImpactFcn)
131fn compound_time_of_impact_fcn(
132    compound: &crate::compound::CompoundData,
133    child_index: i32,
134    context: &mut CompoundImpactContext,
135) -> bool {
136    let child = get_compound_child(compound, child_index);
137
138    context.toi_input.sweep_a =
139        make_compound_child_sweep(context.compound_transform, child.transform);
140
141    let output = match child.geometry {
142        ChildGeometry::Capsule(capsule) => {
143            context.toi_input.proxy_a =
144                make_proxy(&[capsule.center1, capsule.center2], capsule.radius);
145            time_of_impact(&context.toi_input)
146        }
147        ChildGeometry::Hull(hull) => {
148            context.toi_input.proxy_a = make_proxy(get_hull_points(hull), 0.0);
149            time_of_impact(&context.toi_input)
150        }
151        ChildGeometry::Mesh(mesh) => {
152            let mut mesh_context = MeshImpactContext {
153                toi_input: context.toi_input,
154                toi_output: ToiOutput::default(),
155                local_centroid_b: context.local_centroid_b,
156                mesh_local_centroid_b1: VEC3_ZERO,
157                mesh_local_centroid_b2: VEC3_ZERO,
158                fallback_radius: context.fallback_radius,
159                is_sensor: false,
160            };
161
162            let mesh_world_transform = mul_transforms(context.compound_transform, child.transform);
163
164            let sweep_b = &context.toi_input.sweep_b;
165            let xf_b1 = Transform {
166                p: sub(sweep_b.c1, rotate_vector(sweep_b.q1, sweep_b.local_center)),
167                q: sweep_b.q1,
168            };
169            let xf_b2 = Transform {
170                p: sub(sweep_b.c2, rotate_vector(sweep_b.q2, sweep_b.local_center)),
171                q: sweep_b.q2,
172            };
173
174            mesh_context.mesh_local_centroid_b1 = inv_transform_point(
175                mesh_world_transform,
176                transform_point(xf_b1, mesh_context.local_centroid_b),
177            );
178            mesh_context.mesh_local_centroid_b2 = inv_transform_point(
179                mesh_world_transform,
180                transform_point(xf_b2, mesh_context.local_centroid_b),
181            );
182
183            // Bounds local to mesh
184            let local_bounds = aabb_transform(
185                invert_transform(child.transform),
186                context.local_sweep_bounds_b,
187            );
188
189            query_mesh(&mesh, local_bounds, |a, b, c, _triangle_index| {
190                mesh_time_of_impact_fcn(a, b, c, &mut mesh_context)
191            });
192
193            mesh_context.toi_output
194        }
195        ChildGeometry::Sphere(sphere) => {
196            context.toi_input.proxy_a = make_proxy(&[sphere.center], sphere.radius);
197            time_of_impact(&context.toi_input)
198        }
199    };
200
201    if 0.0 < output.fraction && output.fraction < context.toi_input.max_fraction {
202        context.toi_output = output;
203        context.toi_input.max_fraction = output.fraction;
204    }
205
206    // Clear this to be safe (C zeroes proxyA)
207    context.toi_input.proxy_a = ShapeProxy::default();
208
209    // Continue the query
210    true
211}
212
213/// Time of impact between two shapes along sweeps.
214/// Convex vs convex uses GJK TOI; mesh/height/compound use triangle/child queries.
215/// (b3ShapeTimeOfImpact)
216pub fn shape_time_of_impact(
217    shape_a: &Shape,
218    shape_b: &Shape,
219    sweep_a: &Sweep,
220    sweep_b: &Sweep,
221    max_fraction: f32,
222) -> ToiOutput {
223    use super::{compute_shape_extent, get_shape_centroid};
224
225    let is_sensor = shape_a.sensor_index != NULL_INDEX;
226    let type_a = shape_a.shape_type();
227
228    if type_a == ShapeType::Compound {
229        let ShapeGeometry::Compound(compound) = &shape_a.geometry else {
230            unreachable!();
231        };
232
233        let local_centroid_b = get_shape_centroid(shape_b);
234        let extents = compute_shape_extent(shape_b, local_centroid_b);
235        let fallback_radius = max_float(0.75 * extents.min_extent, speculative_distance());
236
237        let compound_transform = Transform {
238            p: sweep_a.c1,
239            q: sweep_a.q1,
240        };
241
242        // Swept bounds of shapeB, then local to compound
243        let bounds = compute_swept_shape_aabb(shape_b, sweep_b, max_fraction);
244        let local_bounds = aabb_transform(invert_transform(compound_transform), bounds);
245
246        let mut context = CompoundImpactContext {
247            toi_input: ToiInput {
248                proxy_a: ShapeProxy::default(),
249                proxy_b: make_shape_proxy(shape_b),
250                sweep_a: Sweep::default(),
251                sweep_b: *sweep_b,
252                max_fraction,
253            },
254            toi_output: ToiOutput::default(),
255            compound_transform,
256            local_sweep_bounds_b: local_bounds,
257            local_centroid_b,
258            fallback_radius,
259        };
260
261        query_compound(compound, local_bounds, |compound, child_index| {
262            compound_time_of_impact_fcn(compound, child_index, &mut context)
263        });
264
265        return context.toi_output;
266    }
267
268    if type_a == ShapeType::Height || type_a == ShapeType::Mesh {
269        // Note: assuming mesh is static
270        let local_centroid_b = get_shape_centroid(shape_b);
271
272        // Assume mesh is static
273        let xf_a = Transform {
274            p: sub(sweep_a.c1, rotate_vector(sweep_a.q1, sweep_a.local_center)),
275            q: sweep_a.q1,
276        };
277        let xf_b1 = Transform {
278            p: sub(sweep_b.c1, rotate_vector(sweep_b.q1, sweep_b.local_center)),
279            q: sweep_b.q1,
280        };
281        let xf_b2 = Transform {
282            p: sub(sweep_b.c2, rotate_vector(sweep_b.q2, sweep_b.local_center)),
283            q: sweep_b.q2,
284        };
285
286        let extents = compute_shape_extent(shape_b, local_centroid_b);
287        let fallback_radius = max_float(0.5 * extents.min_extent, linear_slop());
288
289        // Swept bounds of shapeB, then local to mesh
290        let bounds = compute_swept_shape_aabb(shape_b, sweep_b, max_fraction);
291        let local_bounds = aabb_transform(invert_transform(xf_a), bounds);
292
293        let mut context = MeshImpactContext {
294            toi_input: ToiInput {
295                proxy_a: ShapeProxy {
296                    count: 3,
297                    ..ShapeProxy::default()
298                },
299                proxy_b: make_shape_proxy(shape_b),
300                sweep_a: *sweep_a,
301                sweep_b: *sweep_b,
302                max_fraction,
303            },
304            toi_output: ToiOutput::default(),
305            local_centroid_b,
306            mesh_local_centroid_b1: inv_transform_point(
307                xf_a,
308                transform_point(xf_b1, local_centroid_b),
309            ),
310            mesh_local_centroid_b2: inv_transform_point(
311                xf_a,
312                transform_point(xf_b2, local_centroid_b),
313            ),
314            fallback_radius,
315            is_sensor,
316        };
317
318        match &shape_a.geometry {
319            ShapeGeometry::Mesh { data, scale } => {
320                let mesh = Mesh {
321                    data,
322                    scale: *scale,
323                };
324                query_mesh(&mesh, local_bounds, |a, b, c, _triangle_index| {
325                    mesh_time_of_impact_fcn(a, b, c, &mut context)
326                });
327            }
328            ShapeGeometry::HeightField(hf) => {
329                query_height_field(hf, local_bounds, |a, b, c, _triangle_index| {
330                    mesh_time_of_impact_fcn(a, b, c, &mut context)
331                });
332            }
333            _ => unreachable!(),
334        }
335
336        return context.toi_output;
337    }
338
339    debug_assert!(
340        shape_b.shape_type() != ShapeType::Compound
341            && shape_b.shape_type() != ShapeType::Mesh
342            && shape_b.shape_type() != ShapeType::Height
343    );
344
345    let input = ToiInput {
346        proxy_a: make_shape_proxy(shape_a),
347        proxy_b: make_shape_proxy(shape_b),
348        sweep_a: *sweep_a,
349        sweep_b: *sweep_b,
350        max_fraction,
351    };
352    time_of_impact(&input)
353}