use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Ray {
pub origin: Vec3,
pub direction: Vec3,
}
impl Ray {
#[inline]
pub fn new(origin: Vec3, direction: Vec3) -> Result<Self, crate::HisabError> {
let len_sq = direction.length_squared();
if len_sq < crate::EPSILON_F32 {
return Err(crate::HisabError::InvalidInput(
"ray direction must be non-zero".into(),
));
}
Ok(Self {
origin,
direction: direction.normalize(),
})
}
#[must_use]
#[inline]
pub fn at(&self, t: f32) -> Vec3 {
self.origin + self.direction * t
}
}
impl fmt::Display for Ray {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let p = f.precision();
let o = self.origin;
let d = self.direction;
match p {
Some(p) => write!(
f,
"Ray({:.p$}, {:.p$}, {:.p$} -> {:.p$}, {:.p$}, {:.p$})",
o.x, o.y, o.z, d.x, d.y, d.z
),
None => write!(
f,
"Ray({}, {}, {} -> {}, {}, {})",
o.x, o.y, o.z, d.x, d.y, d.z
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Plane {
pub normal: Vec3,
pub distance: f32,
}
impl Plane {
#[inline]
pub fn from_point_normal(point: Vec3, normal: Vec3) -> Result<Self, crate::HisabError> {
let len_sq = normal.length_squared();
if len_sq < crate::EPSILON_F32 {
return Err(crate::HisabError::InvalidInput(
"plane normal must be non-zero".into(),
));
}
let n = normal * len_sq.sqrt().recip();
Ok(Self {
normal: n,
distance: n.dot(point),
})
}
#[must_use]
#[inline]
pub fn signed_distance(&self, point: Vec3) -> f32 {
self.normal.dot(point) - self.distance
}
}
impl fmt::Display for Plane {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let p = f.precision();
let n = self.normal;
match p {
Some(p) => write!(
f,
"Plane(n=({:.p$}, {:.p$}, {:.p$}), d={:.p$})",
n.x, n.y, n.z, self.distance
),
None => write!(
f,
"Plane(n=({}, {}, {}), d={})",
n.x, n.y, n.z, self.distance
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Aabb {
pub min: Vec3,
pub max: Vec3,
}
impl Aabb {
#[must_use]
#[inline]
pub fn new(a: Vec3, b: Vec3) -> Self {
Self {
min: a.min(b),
max: a.max(b),
}
}
#[must_use]
#[inline]
pub fn contains(&self, point: Vec3) -> bool {
point.cmpge(self.min).all() && point.cmple(self.max).all()
}
#[must_use]
#[inline]
pub fn center(&self) -> Vec3 {
(self.min + self.max) * 0.5
}
#[must_use]
#[inline]
pub fn size(&self) -> Vec3 {
self.max - self.min
}
#[must_use]
#[inline]
pub fn merge(&self, other: &Aabb) -> Aabb {
Aabb {
min: self.min.min(other.min),
max: self.max.max(other.max),
}
}
#[must_use]
#[inline]
pub fn transformed(&self, transform: glam::Mat4) -> Aabb {
let col = [
transform.x_axis.truncate(), transform.y_axis.truncate(), transform.z_axis.truncate(), ];
let translation = transform.w_axis.truncate();
let old_min = self.min.to_array();
let old_max = self.max.to_array();
let mut new_min = translation;
let mut new_max = translation;
let new_min_arr = new_min.as_mut();
let new_max_arr = new_max.as_mut();
for j in 0..3 {
let col_arr = col[j].to_array();
for i in 0..3 {
let lo = col_arr[i] * old_min[j];
let hi = col_arr[i] * old_max[j];
if lo < hi {
new_min_arr[i] += lo;
new_max_arr[i] += hi;
} else {
new_min_arr[i] += hi;
new_max_arr[i] += lo;
}
}
}
Aabb {
min: new_min,
max: new_max,
}
}
}
impl fmt::Display for Aabb {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let p = f.precision();
match p {
Some(p) => write!(
f,
"Aabb(({:.p$}, {:.p$}, {:.p$})..({:.p$}, {:.p$}, {:.p$}))",
self.min.x, self.min.y, self.min.z, self.max.x, self.max.y, self.max.z
),
None => write!(
f,
"Aabb(({}, {}, {})..({}, {}, {}))",
self.min.x, self.min.y, self.min.z, self.max.x, self.max.y, self.max.z
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Sphere {
pub center: Vec3,
pub radius: f32,
}
impl Sphere {
#[inline]
pub fn new(center: Vec3, radius: f32) -> Result<Self, crate::HisabError> {
if radius < 0.0 {
return Err(crate::HisabError::InvalidInput(
"sphere radius must be non-negative".into(),
));
}
Ok(Self { center, radius })
}
#[must_use]
#[inline]
pub fn contains_point(&self, point: Vec3) -> bool {
(point - self.center).length_squared() <= self.radius * self.radius
}
}
impl fmt::Display for Sphere {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let p = f.precision();
let c = self.center;
match p {
Some(p) => write!(
f,
"Sphere(({:.p$}, {:.p$}, {:.p$}), r={:.p$})",
c.x, c.y, c.z, self.radius
),
None => write!(f, "Sphere(({}, {}, {}), r={})", c.x, c.y, c.z, self.radius),
}
}
}
#[must_use]
#[inline]
pub fn ray_plane(ray: &Ray, plane: &Plane) -> Option<f32> {
let denom = plane.normal.dot(ray.direction);
if denom.abs() < crate::EPSILON_F32 {
return None; }
let t = (plane.distance - plane.normal.dot(ray.origin)) / denom;
if t >= 0.0 { Some(t) } else { None }
}
#[must_use]
#[inline]
pub fn ray_sphere(ray: &Ray, sphere: &Sphere) -> Option<f32> {
let oc = ray.origin - sphere.center;
let half_b = oc.dot(ray.direction);
let c = oc.dot(oc) - sphere.radius * sphere.radius;
let discriminant = half_b * half_b - c;
if discriminant < 0.0 {
return None;
}
let sqrt_d = discriminant.sqrt();
let t1 = -half_b - sqrt_d;
let t2 = -half_b + sqrt_d;
if t1 >= 0.0 {
Some(t1)
} else if t2 >= 0.0 {
Some(t2)
} else {
None
}
}
#[must_use]
#[inline]
pub fn ray_aabb(ray: &Ray, aabb: &Aabb) -> Option<f32> {
let origin = ray.origin.to_array();
let dir = ray.direction.to_array();
let bb_min = aabb.min.to_array();
let bb_max = aabb.max.to_array();
let mut t_min = f32::NEG_INFINITY;
let mut t_max = f32::INFINITY;
for i in 0..3 {
if dir[i].abs() < crate::EPSILON_F32 {
if origin[i] < bb_min[i] || origin[i] > bb_max[i] {
return None;
}
} else {
let inv_d = 1.0 / dir[i];
let mut t1 = (bb_min[i] - origin[i]) * inv_d;
let mut t2 = (bb_max[i] - origin[i]) * inv_d;
if t1 > t2 {
std::mem::swap(&mut t1, &mut t2);
}
t_min = t_min.max(t1);
t_max = t_max.min(t2);
if t_min > t_max {
return None;
}
}
}
if t_min >= 0.0 {
Some(t_min)
} else if t_max >= 0.0 {
Some(t_max)
} else {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Obb {
pub center: Vec3,
pub half_extents: Vec3,
pub rotation: glam::Quat,
}
impl Obb {
#[must_use]
#[inline]
pub fn new(center: Vec3, half_extents: Vec3, rotation: glam::Quat) -> Self {
Self {
center,
half_extents,
rotation,
}
}
#[must_use]
#[inline]
pub fn axes(&self) -> [Vec3; 3] {
let m = glam::Mat3::from_quat(self.rotation);
[m.x_axis, m.y_axis, m.z_axis]
}
#[must_use]
#[inline]
pub fn contains_point(&self, point: Vec3) -> bool {
let d = point - self.center;
let axes = self.axes();
let he = self.half_extents.to_array();
for (i, axis) in axes.iter().enumerate() {
if d.dot(*axis).abs() > he[i] + crate::EPSILON_F32 {
return false;
}
}
true
}
#[must_use]
#[inline]
pub fn closest_point(&self, point: Vec3) -> Vec3 {
let d = point - self.center;
let axes = self.axes();
let he = self.half_extents.to_array();
let mut result = self.center;
for (i, axis) in axes.iter().enumerate() {
let dist = d.dot(*axis).clamp(-he[i], he[i]);
result += *axis * dist;
}
result
}
}
#[must_use]
#[inline]
pub fn ray_obb(ray: &Ray, obb: &Obb) -> Option<f32> {
let d = obb.center - ray.origin;
let axes = obb.axes();
let he = obb.half_extents.to_array();
let mut t_min = f32::NEG_INFINITY;
let mut t_max = f32::INFINITY;
for i in 0..3 {
let e = axes[i].dot(d);
let f = axes[i].dot(ray.direction);
if f.abs() > crate::EPSILON_F32 {
let inv_f = 1.0 / f;
let mut t1 = (e - he[i]) * inv_f;
let mut t2 = (e + he[i]) * inv_f;
if t1 > t2 {
std::mem::swap(&mut t1, &mut t2);
}
t_min = t_min.max(t1);
t_max = t_max.min(t2);
if t_min > t_max {
return None;
}
} else if (-e - he[i]) > 0.0 || (-e + he[i]) < 0.0 {
return None;
}
}
if t_min >= 0.0 {
Some(t_min)
} else if t_max >= 0.0 {
Some(t_max)
} else {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Capsule {
pub start: Vec3,
pub end: Vec3,
pub radius: f32,
}
impl Capsule {
#[inline]
pub fn new(start: Vec3, end: Vec3, radius: f32) -> Result<Self, crate::HisabError> {
if radius < 0.0 {
return Err(crate::HisabError::InvalidInput(
"capsule radius must be non-negative".into(),
));
}
Ok(Self { start, end, radius })
}
#[must_use]
#[inline]
pub fn contains_point(&self, point: Vec3) -> bool {
let seg = Segment::new(self.start, self.end);
seg.distance_to_point(point) <= self.radius + crate::EPSILON_F32
}
#[must_use]
#[inline]
pub fn axis_length(&self) -> f32 {
(self.end - self.start).length()
}
}
#[must_use]
pub fn ray_capsule(ray: &Ray, capsule: &Capsule) -> Option<f32> {
let ab = capsule.end - capsule.start;
let ab_len_sq = ab.dot(ab);
if ab_len_sq < crate::EPSILON_F32 {
let sphere = Sphere {
center: capsule.start,
radius: capsule.radius,
};
return ray_sphere(ray, &sphere);
}
let ao = ray.origin - capsule.start;
let d_par = ray.direction.dot(ab) / ab_len_sq;
let o_par = ao.dot(ab) / ab_len_sq;
let d_perp = ray.direction - ab * d_par;
let o_perp = ao - ab * o_par;
let a = d_perp.dot(d_perp);
let b = 2.0 * d_perp.dot(o_perp);
let c = o_perp.dot(o_perp) - capsule.radius * capsule.radius;
let disc = b * b - 4.0 * a * c;
if disc < 0.0 {
let s1 = Sphere {
center: capsule.start,
radius: capsule.radius,
};
let s2 = Sphere {
center: capsule.end,
radius: capsule.radius,
};
let t1 = ray_sphere(ray, &s1);
let t2 = ray_sphere(ray, &s2);
return match (t1, t2) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) | (None, Some(a)) => Some(a),
_ => None,
};
}
let inv_2a = 0.5 / a;
let sqrt_disc = disc.sqrt();
let t1 = (-b - sqrt_disc) * inv_2a;
let t2 = (-b + sqrt_disc) * inv_2a;
let mut best: Option<f32> = None;
let mut check = |t: f32| {
if t >= 0.0 {
let p = ray.at(t);
let proj = (p - capsule.start).dot(ab) / ab_len_sq;
if (0.0..=1.0).contains(&proj) {
best = Some(best.map_or(t, |b: f32| b.min(t)));
}
}
};
check(t1);
check(t2);
let s1 = Sphere {
center: capsule.start,
radius: capsule.radius,
};
let s2 = Sphere {
center: capsule.end,
radius: capsule.radius,
};
if let Some(t) = ray_sphere(ray, &s1) {
let p = ray.at(t);
if (p - capsule.start).dot(ab) <= 0.0 {
best = Some(best.map_or(t, |b: f32| b.min(t)));
}
}
if let Some(t) = ray_sphere(ray, &s2) {
let p = ray.at(t);
if (p - capsule.end).dot(ab) >= 0.0 {
best = Some(best.map_or(t, |b: f32| b.min(t)));
}
}
best
}
#[cfg(test)]
mod tests {
use super::*;
use glam::{Mat4, Vec3};
const EPS: f32 = 1e-5;
fn approx_vec3(a: Vec3, b: Vec3) -> bool {
(a - b).length() < EPS
}
#[test]
fn transformed_identity_unchanged() {
let aabb = Aabb::new(Vec3::new(-1.0, -2.0, -3.0), Vec3::new(1.0, 2.0, 3.0));
let result = aabb.transformed(Mat4::IDENTITY);
assert!(approx_vec3(result.min, aabb.min));
assert!(approx_vec3(result.max, aabb.max));
}
#[test]
fn transformed_translation_only() {
let aabb = Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
let t = Mat4::from_translation(Vec3::new(3.0, 5.0, -2.0));
let result = aabb.transformed(t);
assert!(approx_vec3(result.min, Vec3::new(2.0, 4.0, -3.0)));
assert!(approx_vec3(result.max, Vec3::new(4.0, 6.0, -1.0)));
}
#[test]
fn transformed_uniform_scale() {
let aabb = Aabb::new(Vec3::new(-1.0, -1.0, -1.0), Vec3::new(1.0, 1.0, 1.0));
let s = Mat4::from_scale(Vec3::splat(2.0));
let result = aabb.transformed(s);
assert!(approx_vec3(result.min, Vec3::splat(-2.0)));
assert!(approx_vec3(result.max, Vec3::splat(2.0)));
}
#[test]
fn transformed_90_deg_rotation() {
let aabb = Aabb::new(Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 1.0));
let r = Mat4::from_rotation_z(std::f32::consts::FRAC_PI_2);
let result = aabb.transformed(r);
assert!(
(result.min.x - (-1.0)).abs() < 1e-5,
"min.x = {}",
result.min.x
);
assert!(
(result.max.x - 0.0).abs() < 1e-5,
"max.x = {}",
result.max.x
);
assert!(
(result.min.y - 0.0).abs() < 1e-5,
"min.y = {}",
result.min.y
);
assert!(
(result.max.y - 1.0).abs() < 1e-5,
"max.y = {}",
result.max.y
);
}
#[test]
fn transformed_result_min_le_max() {
let aabb = Aabb::new(Vec3::new(-2.0, -3.0, -1.0), Vec3::new(2.0, 3.0, 1.0));
let m = Mat4::from_cols_array(&[
-1.0, 0.5, 0.0, 0.0, 0.3, 2.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 2.0, 3.0, 1.0,
]);
let result = aabb.transformed(m);
assert!(result.min.x <= result.max.x);
assert!(result.min.y <= result.max.y);
assert!(result.min.z <= result.max.z);
}
}