pub(super) mod field;
pub(super) mod gjk;
mod ray;
mod simplex;
pub(super) mod sweep;
use concinnity_memory::Pool;
use crate::{BodyHandle, ColliderShape, LayerMask, RayHit};
use super::aabb::shape_bounds;
use super::body::Body;
use super::collide::Pose;
use super::math::{Quat, Vec3};
use super::scene::Scene;
use gjk::Support;
use ray::{BoundsProbe, Ray};
#[derive(Debug, Clone, Copy)]
pub struct ShapeCast {
pub shape: ColliderShape,
pub origin: [f32; 3],
pub euler_deg: [f32; 3],
pub motion: [f32; 3],
pub exclude: Option<BodyHandle>,
pub mask: LayerMask,
}
impl ShapeCast {
pub fn new(shape: ColliderShape, origin: [f32; 3], motion: [f32; 3]) -> Self {
ShapeCast {
shape,
origin,
euler_deg: [0.0; 3],
motion,
exclude: None,
mask: LayerMask::ALL,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ShapeCastHit {
pub body: BodyHandle,
pub toi: f32,
pub point: [f32; 3],
pub normal: [f32; 3],
pub gap: f32,
pub started_touching: bool,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct RayQuery {
pub(crate) origin: [f32; 3],
pub(crate) dir: [f32; 3],
pub(crate) max_dist: f32,
pub(crate) exclude: Option<BodyHandle>,
pub(crate) mask: LayerMask,
}
pub(crate) fn raycast(scene: Scene<'_>, ray_query: &RayQuery) -> Option<RayHit> {
let max_dist = ray_query.max_dist;
if !(max_dist.is_finite() && max_dist > 0.0) {
return None;
}
let direction = Vec3::from_array(ray_query.dir);
let length = direction.length();
if !(length.is_finite() && length > 0.0) {
return None;
}
let origin = Vec3::from_array(ray_query.origin);
if !origin.is_finite() {
return None;
}
let ray = Ray {
origin,
direction: direction * (1.0 / length),
};
let far = origin + ray.direction * max_dist;
let axis = scene.broadphase.axis();
let (low, high) = (
origin.get(axis).min(far.get(axis)),
origin.get(axis).max(far.get(axis)),
);
let probe = BoundsProbe::new(ray);
let mut reach = max_dist;
let mut best: Option<(u32, RayHit)> = None;
for &slot in scene.broadphase.slab_window(low, high) {
let proxy = scene.broadphase.proxy(slot);
if !ray_query.mask.interacts_with(proxy.mask) || !probe.reaches(proxy.bounds, reach) {
continue;
}
let Some((_, body)) = candidate(scene.bodies, slot, ray_query.exclude) else {
continue;
};
let found = match body.terrain_index() {
Some(index) => field::raycast(scene.fields, index, ray, reach),
None => body
.convex()
.and_then(|shape| ray::cast(ray, shape, pose_of(body), reach)),
};
let Some(impact) = found else {
continue;
};
if nearer(
best.map(|(kept, hit)| (kept, hit.distance)),
slot,
impact.distance,
) {
reach = impact.distance;
best = Some((
slot,
RayHit {
point: (origin + ray.direction * impact.distance).to_array(),
normal: impact.normal.to_array(),
distance: impact.distance,
},
));
}
}
best.map(|(_, hit)| hit)
}
pub(crate) fn shape_cast(scene: Scene<'_>, cast: &ShapeCast) -> Option<ShapeCastHit> {
let origin = Vec3::from_array(cast.origin);
let motion = Vec3::from_array(cast.motion);
if !origin.is_finite() || !motion.is_finite() {
return None;
}
let rotation = Quat::from_euler_deg(cast.euler_deg);
let start = Pose {
position: origin,
rotation,
};
let moving = Support::new(&cast.shape, start);
let start_bounds = shape_bounds(&cast.shape, origin, rotation);
let travel =
|toi: f32| start_bounds.union(shape_bounds(&cast.shape, origin + motion * toi, rotation));
let swept = travel(1.0);
let axis = scene.broadphase.axis();
let mut reach = swept;
let mut best: Option<(u32, ShapeCastHit)> = None;
for &slot in scene
.broadphase
.slab_window(swept.min.get(axis), swept.max.get(axis))
{
let proxy = scene.broadphase.proxy(slot);
if !cast.mask.interacts_with(proxy.mask) || !reach.overlaps(proxy.bounds) {
continue;
}
let Some((handle, body)) = candidate(scene.bodies, slot, cast.exclude) else {
continue;
};
let found = match body.terrain_index() {
Some(index) => field::sweep(scene.fields, index, &cast.shape, start, motion, reach),
None => body.convex().and_then(|shape| {
sweep::sweep(&moving, motion, &Support::new(shape, pose_of(body)))
}),
};
let Some(impact) = found else {
continue;
};
if nearer(best.map(|(kept, hit)| (kept, hit.toi)), slot, impact.toi) {
reach = travel(impact.toi);
best = Some((
slot,
ShapeCastHit {
body: handle,
toi: impact.toi,
point: impact.point.to_array(),
normal: impact.normal.to_array(),
gap: impact.gap,
started_touching: impact.started_touching,
},
));
}
}
best.map(|(_, hit)| hit)
}
fn candidate(
bodies: &Pool<Body>,
slot: u32,
exclude: Option<BodyHandle>,
) -> Option<(BodyHandle, &Body)> {
let handle = super::world::handle_at(bodies, slot)?;
if exclude == Some(handle) {
return None;
}
let body = bodies.get_at(slot as usize)?;
if body.is_sensor() {
return None;
}
Some((handle, body))
}
fn nearer(best: Option<(u32, f32)>, slot: u32, measure: f32) -> bool {
match best {
None => true,
Some((kept, held)) => measure < held || (measure == held && slot < kept),
}
}
fn pose_of(body: &Body) -> Pose {
Pose {
position: body.position,
rotation: body.orientation,
}
}