1use 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
87pub(crate) const PARALLEL_EPSILON: Fix128 = Fix128 {
89 hi: 0,
90 lo: 0x0000_0001_0000_0000,
91};
92
93const TORUS_TOLERANCE: Fix128 = Fix128 {
95 hi: 0,
96 lo: 0x0000_0001_0000_0000,
97};
98
99const TORUS_MAX_STEPS: usize = 512;
101
102const TORUS_NEWTON_STEPS: usize = 4;
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108pub(crate) struct LocalHit {
109 pub(crate) t: Fix128,
110 pub(crate) normal: Vec3Fix,
111}
112
113fn inside_hit(direction: Vec3Fix) -> LocalHit {
115 LocalHit {
116 t: Fix128::ZERO,
117 normal: -direction,
118 }
119}
120
121pub(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
130fn in_segment(t: Fix128, max_t: Fix128) -> bool {
132 t >= Fix128::ZERO && t <= max_t
133}
134
135fn 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
158pub(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
169pub(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
177pub(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 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 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
224pub(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
251pub(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
309pub(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
322fn 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
335pub(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
386fn ray_local_cone(
389 o: Vec3Fix,
390 d: Vec3Fix,
391 radius: Fix128,
392 half_height: Fix128,
393 max_t: Fix128,
394) -> Option<LocalHit> {
395 let height = half_height.double();
397 let k = radius / height;
398 let k2 = k * k;
399 let apex_gap = half_height - o.y; 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
449fn 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
474fn 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 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
492pub(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
499pub(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 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 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
553fn 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
590pub(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
606fn 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
653fn 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 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 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 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#[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 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 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 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
833fn 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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
883#[non_exhaustive]
884pub enum RayTarget {
885 Body(usize),
887 StaticCollider(usize),
889 Sdf(usize),
891}
892
893#[derive(Clone, Copy, Debug, PartialEq, Eq)]
895#[non_exhaustive]
896pub struct WorldRayHit {
897 pub t: Fix128,
899 pub point: Vec3Fix,
901 pub normal: Vec3Fix,
903 pub target: RayTarget,
905 pub body: Option<usize>,
909}
910
911#[derive(Clone, Copy, Debug, PartialEq)]
918#[non_exhaustive]
919pub struct RayFilter {
920 pub layer_mask: u32,
922 pub exclude_body: Option<usize>,
924 pub include_sensors: bool,
926 pub include_static: bool,
928 pub include_sdf: bool,
930 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 #[must_use]
950 pub fn new() -> Self {
951 Self::default()
952 }
953
954 #[must_use]
956 pub const fn with_layer_mask(mut self, mask: u32) -> Self {
957 self.layer_mask = mask;
958 self
959 }
960
961 #[must_use]
963 pub const fn excluding_body(mut self, body: usize) -> Self {
964 self.exclude_body = Some(body);
965 self
966 }
967
968 #[must_use]
970 pub const fn with_sensors(mut self, include: bool) -> Self {
971 self.include_sensors = include;
972 self
973 }
974
975 #[must_use]
977 pub const fn with_static(mut self, include: bool) -> Self {
978 self.include_static = include;
979 self
980 }
981
982 #[must_use]
984 pub const fn with_sdf(mut self, include: bool) -> Self {
985 self.include_sdf = include;
986 self
987 }
988
989 #[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
1008pub struct WorldRayCaster<'w> {
1011 world: &'w PhysicsWorld,
1012 filter: RayFilter,
1013 bvh: LinearBvh,
1014 boxes: Vec<AABB>,
1016}
1017
1018impl PhysicsWorld {
1019 #[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 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 #[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 #[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 #[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
1096fn 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
1104fn 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 #[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 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 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 #[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 #[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 #[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}