Skip to main content

alice_physics/
shape_raycast.rs

1//! Ray queries against the geometry a [`PhysicsWorld`] actually collides with.
2//!
3//! [`PhysicsWorld::raycast`] and [`crate::query::batch_raycast`] test a ray against
4//! each body's **bounding sphere** (its collision radius): a ray that passes the
5//! corner gap of a box is reported as a hit, the distance to a long box is the
6//! distance to its sphere, and planes, height fields, triangle meshes and SDF
7//! colliders are not seen at all. The queries here test the real geometry:
8//!
9//! | geometry | where it comes from | method |
10//! |---|---|---|
11//! | sphere | a body with a collision radius and no shape | quadratic |
12//! | box | [`Shape::Box`], compound box child | slab clipping of 6 planes |
13//! | cylinder | [`Shape::Cylinder`] | side quadratic + two caps |
14//! | cone | [`Shape::Cone`] | lateral quadratic + base cap |
15//! | ellipsoid | [`Shape::Ellipsoid`] | quadratic in scaled coordinates |
16//! | wedge | [`Shape::Wedge`] | clipping of its 5 planes |
17//! | torus | [`Shape::Torus`] | sphere tracing of the exact distance, then Newton on the quartic |
18//! | sphere, capsule | compound children | quadratic (capsule: side + two spheres) |
19//! | convex hull | compound children | clipping of the hull's face planes |
20//! | plane | [`StaticCollider::Plane`] | [`crate::raycast::ray_plane`] |
21//! | height field | [`StaticCollider::HeightField`] | cell walk, exact quadratic on each bilinear cell |
22//! | triangle mesh | [`StaticCollider::TriMesh`] | [`TriMesh::raycast`](crate::trimesh::TriMesh::raycast) (BVH + Möller–Trumbore) |
23//! | SDF | [`PhysicsWorld::sdf_colliders`] (`std` only) | [`crate::sdf_ccd::ray_march_sdf`] |
24//!
25//! Bodies are culled with a BVH of their collider boxes, walked by the ray itself
26//! (a node is entered only when the ray segment passes through its box), so a
27//! query touches the bodies along the ray and not every body in the world.
28//!
29//! # Conventions
30//!
31//! - The direction need not be unit length; it is normalized, and `t` is a
32//!   **distance** along it. A direction whose squared length is zero in `Fix128`
33//!   (every component below about `2⁻³²`) gives no hit.
34//! - Hits have `0 ≤ t ≤ max_t`. `max_t ≤ 0` is an empty segment: no hit.
35//! - **Solids** (every body shape, compound children, SDFs): a ray whose origin is
36//!   strictly inside reports a hit at `t = 0`, at the origin, with normal
37//!   `−direction`. A ray that starts outside reports its entry point with the
38//!   outward normal there.
39//! - **Surfaces** (plane, height field, triangle mesh) have no inside: the first
40//!   crossing is reported from either side, and the normal is turned to face the
41//!   incoming ray.
42//! - A ray lying in a flat face (parallel to it within `2⁻³²`) does not hit that
43//!   face; it can still enter through an adjacent face.
44//! - Each collider reports at most one hit, its first crossing; a compound body
45//!   reports its nearest child.
46//! - Ties in `t` are broken by target: bodies (by index), then static colliders
47//!   (by index), then SDF colliders (by index). The result does not depend on the
48//!   order the BVH visits bodies in.
49//!
50//! # Precision
51//!
52//! The analytic shapes are exact up to `Fix128` truncation (`2⁻⁶⁴` per operation)
53//! and the CORDIC rotations: distances agree with closed forms to about `1e-12`
54//! for scenes of size 1–100 (`tests/analytic_shape_raycast.rs`). The torus stops
55//! sphere tracing at `2⁻³²` and refines with Newton on its quartic. An SDF hit is
56//! where the field drops below [`SdfCcdConfig::tolerance`] (an `f32` field), so it
57//! is up to that tolerance (divided by the cosine of incidence) short of the
58//! surface.
59//!
60//! # Height fields
61//!
62//! The surface is the one [`HeightField::sample_height`] describes: bilinear over
63//! each cell, with the stored heights as absolute world `Y` (`origin.y` is not
64//! read, as by every other height-field query). The ray is clipped to the grid's
65//! `XZ` footprint `[origin, origin + (n − 1)·spacing]`; a grid with fewer than two
66//! points along either axis has no surface. The normal is
67//! [`HeightField::sample_normal`] at the hit point.
68//!
69//! Author: Moroya Sakamoto
70
71use crate::body_collider::BodyCollider;
72use crate::bvh::{BvhPrimitive, LinearBvh};
73use crate::collider::{Capsule, ConvexHull, AABB};
74use crate::compound::{CompoundChild, ShapeRef};
75use crate::filter::CollisionFilter;
76use crate::heightfield::HeightField;
77use crate::math::{Fix128, QuatFix, Vec3Fix};
78use crate::raycast::{ray_plane, Ray};
79use crate::sdf_ccd::SdfCcdConfig;
80use crate::shape::Shape;
81use crate::solver::PhysicsWorld;
82use crate::static_collider::StaticCollider;
83
84#[cfg(not(feature = "std"))]
85use alloc::{vec, vec::Vec};
86
87/// Denominators below this are treated as zero (a ray parallel to a face): `2⁻³²`.
88pub(crate) const PARALLEL_EPSILON: Fix128 = Fix128 {
89    hi: 0,
90    lo: 0x0000_0001_0000_0000,
91};
92
93/// Sphere tracing of a torus stops when the distance is below this: `2⁻³²`.
94const TORUS_TOLERANCE: Fix128 = Fix128 {
95    hi: 0,
96    lo: 0x0000_0001_0000_0000,
97};
98
99/// Sphere tracing steps for a torus before giving up (a grazing ray).
100const TORUS_MAX_STEPS: usize = 512;
101
102/// Newton steps on the torus quartic after sphere tracing.
103const TORUS_NEWTON_STEPS: usize = 4;
104
105/// The first crossing of a ray with one piece of geometry: the distance along the
106/// unit direction and the normal there (unit, see the module conventions).
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub(crate) struct LocalHit {
109    pub(crate) t: Fix128,
110    pub(crate) normal: Vec3Fix,
111}
112
113/// The hit at `t = 0` of a ray that starts inside a solid.
114fn inside_hit(direction: Vec3Fix) -> LocalHit {
115    LocalHit {
116        t: Fix128::ZERO,
117        normal: -direction,
118    }
119}
120
121/// Keep the nearer of two hits; on a tie the first one.
122pub(crate) fn nearer(a: Option<LocalHit>, b: Option<LocalHit>) -> Option<LocalHit> {
123    match (a, b) {
124        (Some(x), Some(y)) => Some(if y.t < x.t { y } else { x }),
125        (x, None) => x,
126        (None, y) => y,
127    }
128}
129
130/// `t` within the segment `[0, max_t]`.
131fn in_segment(t: Fix128, max_t: Fix128) -> bool {
132    t >= Fix128::ZERO && t <= max_t
133}
134
135/// The real roots of `a·t² + 2·half_b·t + c = 0`, ascending; a linear equation when
136/// `|a|` is below [`PARALLEL_EPSILON`], none when that is degenerate too.
137fn quadratic_roots(a: Fix128, half_b: Fix128, c: Fix128) -> [Option<Fix128>; 2] {
138    if a.abs() < PARALLEL_EPSILON {
139        if half_b.abs() < PARALLEL_EPSILON {
140            return [None, None];
141        }
142        return [Some(-c / half_b.double()), None];
143    }
144    let disc = half_b * half_b - a * c;
145    if disc.is_negative() {
146        return [None, None];
147    }
148    let s = disc.sqrt();
149    let r0 = (-half_b - s) / a;
150    let r1 = (-half_b + s) / a;
151    if r0 <= r1 {
152        [Some(r0), Some(r1)]
153    } else {
154        [Some(r1), Some(r0)]
155    }
156}
157
158/// A ray moved into a solid's frame: `(origin, direction)` in local coordinates.
159pub(crate) fn to_local(
160    origin: Vec3Fix,
161    direction: Vec3Fix,
162    center: Vec3Fix,
163    rotation: QuatFix,
164) -> (Vec3Fix, Vec3Fix) {
165    let inv = rotation.conjugate();
166    (inv.rotate_vec(origin - center), inv.rotate_vec(direction))
167}
168
169/// A local hit turned back into the world.
170pub(crate) fn to_world(hit: Option<LocalHit>, rotation: QuatFix) -> Option<LocalHit> {
171    hit.map(|h| LocalHit {
172        t: h.t,
173        normal: rotation.rotate_vec(h.normal),
174    })
175}
176
177/// A ray against the convex region `n·x ≤ e` of every `(n, e)` (unit `n`), by
178/// clipping the segment `[0, max_t]` plane by plane (Cyrus–Beck). The entry
179/// normal is the plane the segment enters last.
180pub(crate) fn ray_planes(
181    origin: Vec3Fix,
182    direction: Vec3Fix,
183    planes: &[(Vec3Fix, Fix128)],
184    max_t: Fix128,
185) -> Option<LocalHit> {
186    let mut enter: Option<(Fix128, Vec3Fix)> = None;
187    let mut exit = max_t;
188    for &(n, e) in planes {
189        let denom = n.dot(direction);
190        let num = e - n.dot(origin);
191        if denom.abs() < PARALLEL_EPSILON {
192            // Parallel: inside this slab for every t, or outside for every t.
193            if num.is_negative() {
194                return None;
195            }
196            continue;
197        }
198        let t = num / denom;
199        if denom.is_negative() {
200            if enter.is_none_or(|(te, _)| t > te) {
201                enter = Some((t, n));
202            }
203        } else if t < exit {
204            exit = t;
205        }
206    }
207    match enter {
208        // No plane is entered: the origin is on the inside of every one.
209        None => (exit >= Fix128::ZERO).then(|| inside_hit(direction)),
210        Some((t, n)) => {
211            if t > exit || exit.is_negative() {
212                None
213            } else if t.is_negative() {
214                Some(inside_hit(direction))
215            } else if t > max_t {
216                None
217            } else {
218                Some(LocalHit { t, normal: n })
219            }
220        }
221    }
222}
223
224/// A solid sphere.
225pub(crate) fn ray_solid_sphere(
226    origin: Vec3Fix,
227    direction: Vec3Fix,
228    center: Vec3Fix,
229    radius: Fix128,
230    max_t: Fix128,
231) -> Option<LocalHit> {
232    let oc = origin - center;
233    let c = oc.dot(oc) - radius * radius;
234    if c.is_negative() {
235        return Some(inside_hit(direction));
236    }
237    let a = direction.dot(direction);
238    let half_b = oc.dot(direction);
239    let [first, _] = quadratic_roots(a, half_b, c);
240    let t = first?;
241    if !in_segment(t, max_t) {
242        return None;
243    }
244    let p = oc + direction * t;
245    Some(LocalHit {
246        t,
247        normal: p.normalize(),
248    })
249}
250
251/// A solid capsule (segment `a`–`b`, radius `r`): the side of the cylinder between
252/// the end planes, and the two end spheres.
253pub(crate) fn ray_solid_capsule(
254    origin: Vec3Fix,
255    direction: Vec3Fix,
256    capsule: &Capsule,
257    max_t: Fix128,
258) -> Option<LocalHit> {
259    let axis = capsule.b - capsule.a;
260    let (u, len) = axis.normalize_with_length();
261    if len.is_zero() {
262        return ray_solid_sphere(origin, direction, capsule.a, capsule.radius, max_t);
263    }
264    let ao = origin - capsule.a;
265    let s0 = ao.dot(u);
266    let closest = capsule.a + u * clamp(s0, Fix128::ZERO, len);
267    let r2 = capsule.radius * capsule.radius;
268    if (origin - closest).length_squared() < r2 {
269        return Some(inside_hit(direction));
270    }
271    let d_perp = direction - u * direction.dot(u);
272    let ao_perp = ao - u * s0;
273    let mut side = None;
274    for t in quadratic_roots(
275        d_perp.dot(d_perp),
276        d_perp.dot(ao_perp),
277        ao_perp.dot(ao_perp) - r2,
278    )
279    .into_iter()
280    .flatten()
281    {
282        if !in_segment(t, max_t) {
283            continue;
284        }
285        let s = s0 + direction.dot(u) * t;
286        if s >= Fix128::ZERO && s <= len {
287            side = Some(LocalHit {
288                t,
289                normal: (ao_perp + d_perp * t).normalize(),
290            });
291            break;
292        }
293    }
294    let cap_a = ray_solid_sphere(origin, direction, capsule.a, capsule.radius, max_t);
295    let cap_b = ray_solid_sphere(origin, direction, capsule.b, capsule.radius, max_t);
296    nearer(nearer(side, cap_a), cap_b)
297}
298
299fn clamp(x: Fix128, lo: Fix128, hi: Fix128) -> Fix128 {
300    if x < lo {
301        lo
302    } else if x > hi {
303        hi
304    } else {
305        x
306    }
307}
308
309/// Unit axis vectors and their negatives as the 6 planes of a box of half-extents
310/// `h` centred at the origin.
311pub(crate) fn box_planes(h: Vec3Fix) -> [(Vec3Fix, Fix128); 6] {
312    [
313        (Vec3Fix::UNIT_X, h.x),
314        (-Vec3Fix::UNIT_X, h.x),
315        (Vec3Fix::UNIT_Y, h.y),
316        (-Vec3Fix::UNIT_Y, h.y),
317        (Vec3Fix::UNIT_Z, h.z),
318        (-Vec3Fix::UNIT_Z, h.z),
319    ]
320}
321
322/// A solid oriented box.
323fn ray_box(
324    origin: Vec3Fix,
325    direction: Vec3Fix,
326    center: Vec3Fix,
327    half_extents: Vec3Fix,
328    rotation: QuatFix,
329    max_t: Fix128,
330) -> Option<LocalHit> {
331    let (o, d) = to_local(origin, direction, center, rotation);
332    to_world(ray_planes(o, d, &box_planes(half_extents), max_t), rotation)
333}
334
335/// A solid cylinder along local `Y`, in local coordinates.
336pub(crate) fn ray_local_cylinder(
337    o: Vec3Fix,
338    d: Vec3Fix,
339    radius: Fix128,
340    half_height: Fix128,
341    max_t: Fix128,
342) -> Option<LocalHit> {
343    let r2 = radius * radius;
344    if o.x * o.x + o.z * o.z < r2 && o.y.abs() < half_height {
345        return Some(inside_hit(d));
346    }
347    let mut best = None;
348    for t in quadratic_roots(
349        d.x * d.x + d.z * d.z,
350        o.x * d.x + o.z * d.z,
351        o.x * o.x + o.z * o.z - r2,
352    )
353    .into_iter()
354    .flatten()
355    {
356        if in_segment(t, max_t) && (o.y + d.y * t).abs() <= half_height {
357            let p = o + d * t;
358            best = Some(LocalHit {
359                t,
360                normal: Vec3Fix::new(p.x, Fix128::ZERO, p.z).normalize(),
361            });
362            break;
363        }
364    }
365    if d.y.abs() >= PARALLEL_EPSILON {
366        for (y, ny) in [(half_height, Fix128::ONE), (-half_height, Fix128::NEG_ONE)] {
367            let t = (y - o.y) / d.y;
368            if !in_segment(t, max_t) {
369                continue;
370            }
371            let p = o + d * t;
372            if p.x * p.x + p.z * p.z <= r2 {
373                best = nearer(
374                    best,
375                    Some(LocalHit {
376                        t,
377                        normal: Vec3Fix::new(Fix128::ZERO, ny, Fix128::ZERO),
378                    }),
379                );
380            }
381        }
382    }
383    best
384}
385
386/// A solid cone along local `Y`, apex at `+half_height`, base of `radius` at
387/// `−half_height`, in local coordinates (centred on its geometric centre).
388fn ray_local_cone(
389    o: Vec3Fix,
390    d: Vec3Fix,
391    radius: Fix128,
392    half_height: Fix128,
393    max_t: Fix128,
394) -> Option<LocalHit> {
395    // Lateral surface: x² + z² = k²·(h − y)², k = r / 2h, for −h ≤ y ≤ h.
396    let height = half_height.double();
397    let k = radius / height;
398    let k2 = k * k;
399    let apex_gap = half_height - o.y; // Y = h − y at the origin
400    let rr = o.x * o.x + o.z * o.z;
401    if apex_gap > Fix128::ZERO && apex_gap < height && rr < k2 * apex_gap * apex_gap {
402        return Some(inside_hit(d));
403    }
404    let mut best = None;
405    for t in quadratic_roots(
406        d.x * d.x + d.z * d.z - k2 * d.y * d.y,
407        o.x * d.x + o.z * d.z + k2 * apex_gap * d.y,
408        rr - k2 * apex_gap * apex_gap,
409    )
410    .into_iter()
411    .flatten()
412    {
413        if !in_segment(t, max_t) {
414            continue;
415        }
416        let gap = apex_gap - d.y * t;
417        if gap >= Fix128::ZERO && gap <= height {
418            let p = o + d * t;
419            let normal = Vec3Fix::new(p.x, k2 * gap, p.z).normalize();
420            best = Some(LocalHit {
421                t,
422                normal: if normal == Vec3Fix::ZERO {
423                    Vec3Fix::UNIT_Y
424                } else {
425                    normal
426                },
427            });
428            break;
429        }
430    }
431    if d.y.abs() >= PARALLEL_EPSILON {
432        let t = (-half_height - o.y) / d.y;
433        if in_segment(t, max_t) {
434            let p = o + d * t;
435            if p.x * p.x + p.z * p.z <= radius * radius {
436                best = nearer(
437                    best,
438                    Some(LocalHit {
439                        t,
440                        normal: -Vec3Fix::UNIT_Y,
441                    }),
442                );
443            }
444        }
445    }
446    best
447}
448
449/// A solid ellipsoid of semi-axes `radii`, in local coordinates.
450fn ray_local_ellipsoid(o: Vec3Fix, d: Vec3Fix, radii: Vec3Fix, max_t: Fix128) -> Option<LocalHit> {
451    let os = Vec3Fix::new(o.x / radii.x, o.y / radii.y, o.z / radii.z);
452    let ds = Vec3Fix::new(d.x / radii.x, d.y / radii.y, d.z / radii.z);
453    let c = os.dot(os) - Fix128::ONE;
454    if c.is_negative() {
455        return Some(inside_hit(d));
456    }
457    let [first, _] = quadratic_roots(ds.dot(ds), os.dot(ds), c);
458    let t = first?;
459    if !in_segment(t, max_t) {
460        return None;
461    }
462    let p = o + d * t;
463    Some(LocalHit {
464        t,
465        normal: Vec3Fix::new(
466            p.x / (radii.x * radii.x),
467            p.y / (radii.y * radii.y),
468            p.z / (radii.z * radii.z),
469        )
470        .normalize(),
471    })
472}
473
474/// The 5 planes of a wedge (see [`crate::wedge`]) about its geometric centre.
475fn wedge_planes(width: Fix128, height: Fix128, depth: Fix128) -> [(Vec3Fix, Fix128); 5] {
476    let hw = width.half();
477    let hh = height.half();
478    let hd = depth.half();
479    // A slanted face runs from the base corner (±w/2, −h/2) to the apex (0, h/2):
480    // its outward normal is (±h, w/2, 0), through the apex.
481    let right = Vec3Fix::new(height, hw, Fix128::ZERO).normalize();
482    let left = Vec3Fix::new(-height, hw, Fix128::ZERO).normalize();
483    [
484        (-Vec3Fix::UNIT_Y, hh),
485        (Vec3Fix::UNIT_Z, hd),
486        (-Vec3Fix::UNIT_Z, hd),
487        (right, right.y * hh),
488        (left, left.y * hh),
489    ]
490}
491
492/// The exact signed distance of a torus (ring of `major` in the local `XZ` plane,
493/// tube of `minor`).
494pub(crate) fn torus_distance(p: Vec3Fix, major: Fix128, minor: Fix128) -> Fix128 {
495    let ring = (p.x * p.x + p.z * p.z).sqrt() - major;
496    (ring * ring + p.y * p.y).sqrt() - minor
497}
498
499/// A solid torus, in local coordinates.
500pub(crate) fn ray_local_torus(
501    o: Vec3Fix,
502    d: Vec3Fix,
503    major: Fix128,
504    minor: Fix128,
505    max_t: Fix128,
506) -> Option<LocalHit> {
507    if torus_distance(o, major, minor).is_negative() {
508        return Some(inside_hit(d));
509    }
510    // Start where the ray enters the bounding sphere (no surface before it).
511    let bound = ray_solid_sphere(o, d, Vec3Fix::ZERO, major + minor, max_t)?;
512    let mut t = bound.t;
513    let mut converged = false;
514    for _ in 0..TORUS_MAX_STEPS {
515        let dist = torus_distance(o + d * t, major, minor);
516        if dist < TORUS_TOLERANCE {
517            converged = true;
518            break;
519        }
520        t = t + dist;
521        if t > max_t {
522            return None;
523        }
524    }
525    if !converged {
526        return None;
527    }
528    // Newton on F(t) = (|p|² + R² − r²)² − 4R²(x² + z²).
529    let r2 = major * major;
530    let k = r2 - minor * minor;
531    let four_r2 = r2.double().double();
532    for _ in 0..TORUS_NEWTON_STEPS {
533        let p = o + d * t;
534        let s = p.dot(p) + k;
535        let f = s * s - four_r2 * (p.x * p.x + p.z * p.z);
536        let df = (s * p.dot(d)).double().double() - (four_r2 * (p.x * d.x + p.z * d.z)).double();
537        if df.abs() < PARALLEL_EPSILON {
538            break;
539        }
540        t = t - f / df;
541    }
542    if !in_segment(t, max_t) {
543        return None;
544    }
545    let p = o + d * t;
546    let (ring_dir, _) = Vec3Fix::new(p.x, Fix128::ZERO, p.z).normalize_with_length();
547    Some(LocalHit {
548        t,
549        normal: (p - ring_dir * major).normalize(),
550    })
551}
552
553/// A solid [`Shape`] whose centre of mass is at `position`, turned by `rotation`
554/// (see [`crate::shape`] for the frames).
555fn ray_posed_shape(
556    origin: Vec3Fix,
557    direction: Vec3Fix,
558    shape: &Shape,
559    position: Vec3Fix,
560    rotation: QuatFix,
561    max_t: Fix128,
562) -> Option<LocalHit> {
563    let rotation = rotation.unit_rotation();
564    let center = position - rotation.rotate_vec(shape.center_of_mass_offset());
565    let (o, d) = to_local(origin, direction, center, rotation);
566    let hit = match *shape {
567        Shape::Box { half_extents } => ray_planes(o, d, &box_planes(half_extents), max_t),
568        Shape::Cylinder {
569            radius,
570            half_height,
571        } => ray_local_cylinder(o, d, radius, half_height, max_t),
572        Shape::Cone {
573            radius,
574            half_height,
575        } => ray_local_cone(o, d, radius, half_height, max_t),
576        Shape::Ellipsoid { radii } => ray_local_ellipsoid(o, d, radii, max_t),
577        Shape::Wedge {
578            width,
579            height,
580            depth,
581        } => ray_planes(o, d, &wedge_planes(width, height, depth), max_t),
582        Shape::Torus {
583            major_radius,
584            minor_radius,
585        } => ray_local_torus(o, d, major_radius, minor_radius, max_t),
586    };
587    to_world(hit, rotation)
588}
589
590/// The face planes of a convex hull (unit outward normals), or `None` when its
591/// points span no volume.
592pub(crate) fn hull_planes(hull: &ConvexHull) -> Option<Vec<(Vec3Fix, Fix128)>> {
593    let mesh = crate::convex_mesh_builder::build_hull_mesh(&hull.vertices)?;
594    Some(
595        mesh.faces
596            .iter()
597            .filter_map(|&[a, b, c]| {
598                let (va, vb, vc) = (mesh.vertices[a], mesh.vertices[b], mesh.vertices[c]);
599                let n = (vb - va).cross(vc - va).try_normalize()?;
600                Some((n, n.dot(va)))
601            })
602            .collect(),
603    )
604}
605
606/// One compound child of a body at `body_pos` turned by `body_rot`, placed the way
607/// [`CompoundChild::support_world`] places it.
608fn ray_compound_child(
609    origin: Vec3Fix,
610    direction: Vec3Fix,
611    child: &CompoundChild,
612    body_pos: Vec3Fix,
613    body_rot: QuatFix,
614    max_t: Fix128,
615) -> Option<LocalHit> {
616    let body_rot = body_rot.unit_rotation();
617    let child_rot = body_rot.mul(child.local_rotation);
618    let child_pos = body_pos + body_rot.rotate_vec(child.local_position);
619    match &child.shape {
620        ShapeRef::Sphere(s) => ray_solid_sphere(
621            origin,
622            direction,
623            child_pos + child_rot.rotate_vec(s.center),
624            s.radius,
625            max_t,
626        ),
627        ShapeRef::Capsule(c) => ray_solid_capsule(
628            origin,
629            direction,
630            &Capsule::new(
631                child_pos + child_rot.rotate_vec(c.a),
632                child_pos + child_rot.rotate_vec(c.b),
633                c.radius,
634            ),
635            max_t,
636        ),
637        ShapeRef::Box(b) => ray_box(
638            origin,
639            direction,
640            child_pos + child_rot.rotate_vec(b.center),
641            b.half_extents,
642            child_rot.mul(b.rotation),
643            max_t,
644        ),
645        ShapeRef::ConvexHull(hull) => {
646            let planes = hull_planes(hull)?;
647            let (o, d) = to_local(origin, direction, child_pos, child_rot);
648            to_world(ray_planes(o, d, &planes, max_t), child_rot)
649        }
650    }
651}
652
653/// The surface of a height field (see the module doc).
654fn ray_heightfield(
655    origin: Vec3Fix,
656    direction: Vec3Fix,
657    field: &HeightField,
658    max_t: Fix128,
659) -> Option<LocalHit> {
660    if field.width < 2 || field.depth < 2 || field.spacing <= Fix128::ZERO {
661        return None;
662    }
663    let s = field.spacing;
664    let x0 = field.origin.x;
665    let z0 = field.origin.z;
666    let x1 = x0 + s * Fix128::from_int(i64::from(field.width - 1));
667    let z1 = z0 + s * Fix128::from_int(i64::from(field.depth - 1));
668    // Clip the segment to the footprint in XZ.
669    let mut t_lo = Fix128::ZERO;
670    let mut t_hi = max_t;
671    for (o, d, lo, hi) in [
672        (origin.x, direction.x, x0, x1),
673        (origin.z, direction.z, z0, z1),
674    ] {
675        if d.abs() < PARALLEL_EPSILON {
676            if o < lo || o > hi {
677                return None;
678            }
679            continue;
680        }
681        let (mut a, mut b) = ((lo - o) / d, (hi - o) / d);
682        if a > b {
683            core::mem::swap(&mut a, &mut b);
684        }
685        if a > t_lo {
686            t_lo = a;
687        }
688        if b < t_hi {
689            t_hi = b;
690        }
691    }
692    if t_lo > t_hi {
693        return None;
694    }
695    let last_x = field.width - 2;
696    let last_z = field.depth - 2;
697    let cell_of = |w: Fix128, w0: Fix128, last: u32| -> u32 {
698        let g = (w - w0) / s;
699        if g.is_negative() {
700            0
701        } else {
702            (g.hi.min(i64::from(last))) as u32
703        }
704    };
705    let p_start = origin + direction * t_lo;
706    let mut cx = cell_of(p_start.x, x0, last_x);
707    let mut cz = cell_of(p_start.z, z0, last_z);
708    let mut t_cell = t_lo;
709    // Each step leaves through at least one cell wall, so the walk is bounded by
710    // the number of cells the footprint has along X plus along Z.
711    let max_steps = (field.width as usize) + (field.depth as usize) + 2;
712    for _ in 0..max_steps {
713        let cell_x0 = x0 + s * Fix128::from_int(i64::from(cx));
714        let cell_z0 = z0 + s * Fix128::from_int(i64::from(cz));
715        // Where the ray leaves this cell (or the clipped segment).
716        let mut t_exit = t_hi;
717        let mut step_x = 0i32;
718        let mut step_z = 0i32;
719        if direction.x.abs() >= PARALLEL_EPSILON {
720            let wall = if direction.x.is_negative() {
721                cell_x0
722            } else {
723                cell_x0 + s
724            };
725            let t = (wall - origin.x) / direction.x;
726            if t < t_exit {
727                t_exit = t;
728                step_x = if direction.x.is_negative() { -1 } else { 1 };
729                step_z = 0;
730            } else if t == t_exit {
731                step_x = if direction.x.is_negative() { -1 } else { 1 };
732            }
733        }
734        if direction.z.abs() >= PARALLEL_EPSILON {
735            let wall = if direction.z.is_negative() {
736                cell_z0
737            } else {
738                cell_z0 + s
739            };
740            let t = (wall - origin.z) / direction.z;
741            if t < t_exit {
742                t_exit = t;
743                step_z = if direction.z.is_negative() { -1 } else { 1 };
744                step_x = 0;
745            } else if t == t_exit {
746                step_z = if direction.z.is_negative() { -1 } else { 1 };
747            }
748        }
749        if t_exit < t_cell {
750            t_exit = t_cell;
751        }
752        if let Some(hit) = ray_bilinear_cell(
753            origin,
754            direction,
755            field,
756            (cx, cz),
757            (cell_x0, cell_z0),
758            t_cell,
759            t_exit,
760        ) {
761            return Some(hit);
762        }
763        if t_exit >= t_hi {
764            return None;
765        }
766        let nx = i64::from(cx) + i64::from(step_x);
767        let nz = i64::from(cz) + i64::from(step_z);
768        if nx < 0 || nz < 0 || nx > i64::from(last_x) || nz > i64::from(last_z) {
769            return None;
770        }
771        cx = nx as u32;
772        cz = nz as u32;
773        t_cell = t_exit;
774    }
775    None
776}
777
778/// The first crossing in `[t0, t1]` of the ray with the bilinear patch of one cell.
779#[allow(clippy::too_many_arguments)]
780fn ray_bilinear_cell(
781    origin: Vec3Fix,
782    direction: Vec3Fix,
783    field: &HeightField,
784    (cx, cz): (u32, u32),
785    (cell_x0, cell_z0): (Fix128, Fix128),
786    t0: Fix128,
787    t1: Fix128,
788) -> Option<LocalHit> {
789    let h00 = field.get_height(cx, cz);
790    let h10 = field.get_height(cx + 1, cz);
791    let h01 = field.get_height(cx, cz + 1);
792    let h11 = field.get_height(cx + 1, cz + 1);
793    let b = h10 - h00;
794    let c = h01 - h00;
795    let dd = h00 - h10 - h01 + h11;
796    // Parametrize by u = t − t0 so the numbers stay cell-sized.
797    let p0 = origin + direction * t0;
798    let inv_s = Fix128::ONE / field.spacing;
799    let ax = (p0.x - cell_x0) * inv_s;
800    let az = (p0.z - cell_z0) * inv_s;
801    let bx = direction.x * inv_s;
802    let bz = direction.z * inv_s;
803    // f(u) = y(u) − h(u) = q0 + q1·u + q2·u²
804    let q0 = p0.y - h00 - b * ax - c * az - dd * ax * az;
805    let q1 = direction.y - b * bx - c * bz - dd * (ax * bz + bx * az);
806    let q2 = -(dd * bx * bz);
807    let span = t1 - t0;
808    if q0.is_zero() && q1.is_zero() && q2.is_zero() {
809        // The ray lies in the cell's surface (a flat cell, ray in its plane).
810        return None;
811    }
812    let root = if q0.is_zero() {
813        Some(Fix128::ZERO)
814    } else {
815        quadratic_roots(q2, q1.half(), q0)
816            .into_iter()
817            .flatten()
818            .find(|&u| u >= Fix128::ZERO && u <= span)
819    }?;
820    let t = t0 + root;
821    let p = origin + direction * t;
822    let n = field.sample_normal(p.x, p.z);
823    Some(LocalHit {
824        t,
825        normal: if n.dot(direction) > Fix128::ZERO {
826            -n
827        } else {
828            n
829        },
830    })
831}
832
833/// A static surface.
834fn ray_static(
835    origin: Vec3Fix,
836    direction: Vec3Fix,
837    collider: &StaticCollider,
838    max_t: Fix128,
839) -> Option<LocalHit> {
840    let ray = Ray { origin, direction };
841    match collider {
842        StaticCollider::Plane(plane) => {
843            ray_plane(&ray, plane.normal, plane.offset, max_t).map(|h| LocalHit {
844                t: h.t,
845                normal: h.normal,
846            })
847        }
848        StaticCollider::HeightField(field) => ray_heightfield(origin, direction, field, max_t),
849        StaticCollider::TriMesh(mesh) => mesh.raycast(&ray, max_t).map(|h| LocalHit {
850            t: h.t,
851            normal: h.normal,
852        }),
853    }
854}
855
856/// An SDF collider: inside (negative distance at the origin) is a hit at `t = 0`;
857/// otherwise [`crate::sdf_ccd::ray_march_sdf`].
858#[cfg(feature = "std")]
859fn ray_sdf(
860    origin: Vec3Fix,
861    direction: Vec3Fix,
862    sdf: &crate::sdf_collider::SdfCollider,
863    config: &SdfCcdConfig,
864    max_t: Fix128,
865) -> Option<LocalHit> {
866    let (lx, ly, lz) = sdf.world_to_local(origin);
867    if sdf.field.distance(lx, ly, lz) * sdf.scale_f32 < 0.0 {
868        return Some(inside_hit(direction));
869    }
870    let toi = crate::sdf_ccd::ray_march_sdf(origin, direction, max_t, sdf, config)?;
871    in_segment(toi.t, max_t).then_some(LocalHit {
872        t: toi.t,
873        normal: toi.normal,
874    })
875}
876
877// ============================================================================
878// Public types
879// ============================================================================
880
881/// What a ray hit.
882#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
883#[non_exhaustive]
884pub enum RayTarget {
885    /// A body, by index: its shape, its compound children, or its collision sphere.
886    Body(usize),
887    /// A static collider ([`PhysicsWorld::add_static_collider`]), by index.
888    StaticCollider(usize),
889    /// An SDF collider ([`PhysicsWorld::sdf_colliders`]), by index.
890    Sdf(usize),
891}
892
893/// A hit of [`PhysicsWorld::cast_ray`] and its variants.
894#[derive(Clone, Copy, Debug, PartialEq, Eq)]
895#[non_exhaustive]
896pub struct WorldRayHit {
897    /// Distance along the (normalized) direction.
898    pub t: Fix128,
899    /// World-space hit point, `origin + direction·t`.
900    pub point: Vec3Fix,
901    /// Unit normal at the hit (see the module conventions for its side).
902    pub normal: Vec3Fix,
903    /// The collider hit.
904    pub target: RayTarget,
905    /// The body the hit belongs to: the body for [`RayTarget::Body`], the body an
906    /// SDF collider is attached to (if any) for [`RayTarget::Sdf`], `None` for a
907    /// static collider.
908    pub body: Option<usize>,
909}
910
911/// Which colliders a ray query sees.
912///
913/// The default sees every body on any layer except sensors, every static collider
914/// and every SDF collider. A body is seen when `layer_mask & body_filter.layer != 0`
915/// ([`PhysicsWorld::body_filter`]); an SDF collider attached to a body is seen only
916/// when that body is.
917#[derive(Clone, Copy, Debug, PartialEq)]
918#[non_exhaustive]
919pub struct RayFilter {
920    /// Layers the ray sees (bitmask against [`CollisionFilter::layer`]).
921    pub layer_mask: u32,
922    /// A body the ray ignores (typically the one carrying the sensor).
923    pub exclude_body: Option<usize>,
924    /// Whether sensor bodies are seen.
925    pub include_sensors: bool,
926    /// Whether static colliders are seen.
927    pub include_static: bool,
928    /// Whether SDF colliders are seen.
929    pub include_sdf: bool,
930    /// Sphere-tracing settings for SDF colliders.
931    pub sdf: SdfCcdConfig,
932}
933
934impl Default for RayFilter {
935    fn default() -> Self {
936        Self {
937            layer_mask: u32::MAX,
938            exclude_body: None,
939            include_sensors: false,
940            include_static: true,
941            include_sdf: true,
942            sdf: SdfCcdConfig::default(),
943        }
944    }
945}
946
947impl RayFilter {
948    /// The default filter (see the type doc).
949    #[must_use]
950    pub fn new() -> Self {
951        Self::default()
952    }
953
954    /// Only bodies on these layers.
955    #[must_use]
956    pub const fn with_layer_mask(mut self, mask: u32) -> Self {
957        self.layer_mask = mask;
958        self
959    }
960
961    /// Ignore this body (and any SDF collider attached to it).
962    #[must_use]
963    pub const fn excluding_body(mut self, body: usize) -> Self {
964        self.exclude_body = Some(body);
965        self
966    }
967
968    /// Whether sensor bodies are seen.
969    #[must_use]
970    pub const fn with_sensors(mut self, include: bool) -> Self {
971        self.include_sensors = include;
972        self
973    }
974
975    /// Whether static colliders are seen.
976    #[must_use]
977    pub const fn with_static(mut self, include: bool) -> Self {
978        self.include_static = include;
979        self
980    }
981
982    /// Whether SDF colliders are seen.
983    #[must_use]
984    pub const fn with_sdf(mut self, include: bool) -> Self {
985        self.include_sdf = include;
986        self
987    }
988
989    /// Sphere-tracing settings for SDF colliders.
990    #[must_use]
991    pub const fn with_sdf_config(mut self, config: SdfCcdConfig) -> Self {
992        self.sdf = config;
993        self
994    }
995
996    pub(crate) fn sees_body(&self, world: &PhysicsWorld, i: usize) -> bool {
997        let Some(body) = world.bodies.get(i) else {
998            return false;
999        };
1000        if self.exclude_body == Some(i) || (body.is_sensor && !self.include_sensors) {
1001            return false;
1002        }
1003        let filter: CollisionFilter = world.body_filter(i);
1004        filter.layer & self.layer_mask != 0
1005    }
1006}
1007
1008/// A ray query against one world, with the body BVH built once: cast many rays
1009/// (a lidar scan) without rebuilding it. Made by [`PhysicsWorld::ray_caster`].
1010pub struct WorldRayCaster<'w> {
1011    world: &'w PhysicsWorld,
1012    filter: RayFilter,
1013    bvh: LinearBvh,
1014    /// Each body's collider box, by body index (unused for bodies not in the tree).
1015    boxes: Vec<AABB>,
1016}
1017
1018impl PhysicsWorld {
1019    /// A ray caster over this world's current state with `filter`; the body BVH is
1020    /// built here, once.
1021    #[must_use]
1022    pub fn ray_caster(&self, filter: RayFilter) -> WorldRayCaster<'_> {
1023        let (bvh, boxes) = self.query_body_bvh(&filter);
1024        WorldRayCaster {
1025            world: self,
1026            filter,
1027            bvh,
1028            boxes,
1029        }
1030    }
1031
1032    /// The BVH of the collider boxes of the bodies `filter` sees, and each body's
1033    /// box by body index (unused for bodies not in the tree).
1034    pub(crate) fn query_body_bvh(&self, filter: &RayFilter) -> (LinearBvh, Vec<AABB>) {
1035        let mut primitives = Vec::new();
1036        let mut boxes = vec![AABB::new(Vec3Fix::ZERO, Vec3Fix::ZERO); self.bodies.len()];
1037        for (i, slot) in boxes.iter_mut().enumerate() {
1038            if !filter.sees_body(self, i) {
1039                continue;
1040            }
1041            let Some((radius, collider)) = self.ray_geometry(i) else {
1042                continue;
1043            };
1044            let body = &self.bodies[i];
1045            let aabb = match collider {
1046                Some(c) => c.world_aabb(body.position, body.rotation),
1047                None => AABB::from_center_half(body.position, Vec3Fix::new(radius, radius, radius)),
1048            };
1049            *slot = aabb;
1050            primitives.push(BvhPrimitive {
1051                aabb,
1052                index: i as u32,
1053                morton: 0,
1054            });
1055        }
1056        (LinearBvh::build(primitives), boxes)
1057    }
1058
1059    /// The nearest hit of a ray with the world's geometry (see [`crate::shape_raycast`]).
1060    #[must_use]
1061    pub fn cast_ray(
1062        &self,
1063        origin: Vec3Fix,
1064        direction: Vec3Fix,
1065        max_t: Fix128,
1066        filter: &RayFilter,
1067    ) -> Option<WorldRayHit> {
1068        self.ray_caster(*filter).closest(origin, direction, max_t)
1069    }
1070
1071    /// Every collider the ray hits, one hit each, nearest first.
1072    #[must_use]
1073    pub fn cast_ray_all(
1074        &self,
1075        origin: Vec3Fix,
1076        direction: Vec3Fix,
1077        max_t: Fix128,
1078        filter: &RayFilter,
1079    ) -> Vec<WorldRayHit> {
1080        self.ray_caster(*filter).all(origin, direction, max_t)
1081    }
1082
1083    /// Whether the ray hits anything (stops at the first hit found).
1084    #[must_use]
1085    pub fn cast_ray_any(
1086        &self,
1087        origin: Vec3Fix,
1088        direction: Vec3Fix,
1089        max_t: Fix128,
1090        filter: &RayFilter,
1091    ) -> bool {
1092        self.ray_caster(*filter).any(origin, direction, max_t)
1093    }
1094}
1095
1096/// The unit direction of a query, or `None` for an empty one.
1097fn query_direction(direction: Vec3Fix, max_t: Fix128) -> Option<Vec3Fix> {
1098    if max_t <= Fix128::ZERO {
1099        return None;
1100    }
1101    direction.try_normalize_scaled()
1102}
1103
1104/// Whether the ray segment `[0, max_t]` passes through `aabb`.
1105fn segment_meets_box(origin: Vec3Fix, direction: Vec3Fix, aabb: &AABB, max_t: Fix128) -> bool {
1106    let mut t0 = Fix128::ZERO;
1107    let mut t1 = max_t;
1108    for (o, d, lo, hi) in [
1109        (origin.x, direction.x, aabb.min.x, aabb.max.x),
1110        (origin.y, direction.y, aabb.min.y, aabb.max.y),
1111        (origin.z, direction.z, aabb.min.z, aabb.max.z),
1112    ] {
1113        if d.is_zero() {
1114            if o < lo || o > hi {
1115                return false;
1116            }
1117            continue;
1118        }
1119        let (mut a, mut b) = ((lo - o) / d, (hi - o) / d);
1120        if a > b {
1121            core::mem::swap(&mut a, &mut b);
1122        }
1123        if a > t0 {
1124            t0 = a;
1125        }
1126        if b < t1 {
1127            t1 = b;
1128        }
1129        if t0 > t1 {
1130            return false;
1131        }
1132    }
1133    true
1134}
1135
1136impl WorldRayCaster<'_> {
1137    /// Bodies whose collider box the ray segment passes through, found by walking
1138    /// the BVH (a subtree is skipped when the segment misses its box), in the order
1139    /// the walk visits them: the only bodies the narrow phase tests. Empty for an
1140    /// empty query (zero direction, `max_t ≤ 0`).
1141    #[must_use]
1142    pub fn candidates(&self, origin: Vec3Fix, direction: Vec3Fix, max_t: Fix128) -> Vec<usize> {
1143        let mut out = Vec::new();
1144        let Some(dir) = query_direction(direction, max_t) else {
1145            return out;
1146        };
1147        let nodes = &self.bvh.nodes;
1148        let mut idx = 0u32;
1149        while (idx as usize) < nodes.len() {
1150            let node = &nodes[idx as usize];
1151            if segment_meets_box(origin, dir, &node.get_aabb(), max_t) {
1152                if node.is_leaf() {
1153                    let start = node.first_child_or_prim as usize;
1154                    let end = start + node.prim_count() as usize;
1155                    for &p in self.bvh.primitives.get(start..end).unwrap_or(&[]) {
1156                        let i = p as usize;
1157                        if self
1158                            .boxes
1159                            .get(i)
1160                            .is_some_and(|b| segment_meets_box(origin, dir, b, max_t))
1161                        {
1162                            out.push(i);
1163                        }
1164                    }
1165                    idx = node.escape_idx();
1166                } else {
1167                    idx = node.first_child_or_prim;
1168                }
1169            } else {
1170                idx = node.escape_idx();
1171            }
1172        }
1173        out
1174    }
1175
1176    /// One body's geometry.
1177    fn body_hit(&self, i: usize, origin: Vec3Fix, dir: Vec3Fix, max_t: Fix128) -> Option<LocalHit> {
1178        let (radius, collider) = self.world.ray_geometry(i)?;
1179        let body = &self.world.bodies[i];
1180        match collider {
1181            None => ray_solid_sphere(origin, dir, body.position, radius, max_t),
1182            Some(BodyCollider::Shape(shape)) => {
1183                ray_posed_shape(origin, dir, shape, body.position, body.rotation, max_t)
1184            }
1185            Some(BodyCollider::Compound(compound)) => {
1186                let mut best = None;
1187                for child in &compound.children {
1188                    best = nearer(
1189                        best,
1190                        ray_compound_child(origin, dir, child, body.position, body.rotation, max_t),
1191                    );
1192                }
1193                best
1194            }
1195        }
1196    }
1197
1198    /// Visit every hit (any order); `visit` returns `false` to stop.
1199    fn for_each_hit(
1200        &self,
1201        origin: Vec3Fix,
1202        direction: Vec3Fix,
1203        max_t: Fix128,
1204        mut visit: impl FnMut(WorldRayHit) -> bool,
1205    ) {
1206        let Some(dir) = query_direction(direction, max_t) else {
1207            return;
1208        };
1209        let make = |h: LocalHit, target: RayTarget, body: Option<usize>| WorldRayHit {
1210            t: h.t,
1211            point: origin + dir * h.t,
1212            normal: h.normal,
1213            target,
1214            body,
1215        };
1216        for i in self.candidates(origin, dir, max_t) {
1217            if let Some(h) = self.body_hit(i, origin, dir, max_t) {
1218                if !visit(make(h, RayTarget::Body(i), Some(i))) {
1219                    return;
1220                }
1221            }
1222        }
1223        if self.filter.include_static {
1224            for (j, collider) in self.world.static_colliders_slice().iter().enumerate() {
1225                if let Some(h) = ray_static(origin, dir, collider, max_t) {
1226                    if !visit(make(h, RayTarget::StaticCollider(j), None)) {
1227                        return;
1228                    }
1229                }
1230            }
1231        }
1232        #[cfg(feature = "std")]
1233        if self.filter.include_sdf {
1234            for (k, sdf) in self.world.sdf_colliders.iter().enumerate() {
1235                let body = (sdf.body_index < self.world.bodies.len()).then_some(sdf.body_index);
1236                if body.is_some_and(|b| !self.filter.sees_body(self.world, b)) {
1237                    continue;
1238                }
1239                if let Some(h) = ray_sdf(origin, dir, sdf, &self.filter.sdf, max_t) {
1240                    if !visit(make(h, RayTarget::Sdf(k), body)) {
1241                        return;
1242                    }
1243                }
1244            }
1245        }
1246    }
1247
1248    /// The nearest hit (ties: see the module conventions).
1249    #[must_use]
1250    pub fn closest(
1251        &self,
1252        origin: Vec3Fix,
1253        direction: Vec3Fix,
1254        max_t: Fix128,
1255    ) -> Option<WorldRayHit> {
1256        let mut best: Option<WorldRayHit> = None;
1257        self.for_each_hit(origin, direction, max_t, |h| {
1258            if best.is_none_or(|b| (h.t, h.target) < (b.t, b.target)) {
1259                best = Some(h);
1260            }
1261            true
1262        });
1263        best
1264    }
1265
1266    /// Every hit, one per collider, sorted by `t` (ties: see the module conventions).
1267    #[must_use]
1268    pub fn all(&self, origin: Vec3Fix, direction: Vec3Fix, max_t: Fix128) -> Vec<WorldRayHit> {
1269        let mut hits = Vec::new();
1270        self.for_each_hit(origin, direction, max_t, |h| {
1271            hits.push(h);
1272            true
1273        });
1274        hits.sort_by_key(|h| (h.t, h.target));
1275        hits
1276    }
1277
1278    /// Whether anything is hit.
1279    #[must_use]
1280    pub fn any(&self, origin: Vec3Fix, direction: Vec3Fix, max_t: Fix128) -> bool {
1281        let mut found = false;
1282        self.for_each_hit(origin, direction, max_t, |_| {
1283            found = true;
1284            false
1285        });
1286        found
1287    }
1288}