use glam::DVec3;
pub fn basis_for(dir: DVec3) -> (DVec3, DVec3) {
let helper = if dir.x.abs() < 0.9 {
DVec3::X
} else {
DVec3::Y
};
let u = helper.cross(dir).normalize();
let v = dir.cross(u);
(u, v)
}
pub fn oriented_against(n: DVec3, d: DVec3) -> DVec3 {
if d.dot(n) > 0.0 {
-n
} else {
n
}
}
pub fn reflect(d: DVec3, n: DVec3) -> DVec3 {
(d - n * (2.0 * d.dot(n))).normalize()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_basis_is_orthonormal_everywhere() {
let directions = [
DVec3::X,
DVec3::Y,
DVec3::Z,
-DVec3::X,
-DVec3::Z,
DVec3::new(1.0, 1.0, 1.0).normalize(),
DVec3::new(0.9999, 0.01, 0.0).normalize(),
DVec3::new(-0.7, 0.2, 0.68).normalize(),
];
for d in directions {
let (u, v) = basis_for(d);
assert!((u.length() - 1.0).abs() < 1e-12, "u not unit for {d}");
assert!((v.length() - 1.0).abs() < 1e-12, "v not unit for {d}");
assert!(u.dot(d).abs() < 1e-12, "u not perpendicular to {d}");
assert!(v.dot(d).abs() < 1e-12, "v not perpendicular to {d}");
assert!(u.dot(v).abs() < 1e-12, "u and v not perpendicular for {d}");
assert!((u.cross(v) - d).length() < 1e-12, "left-handed for {d}");
}
}
#[test]
fn reflection_preserves_the_angle_and_is_its_own_inverse() {
let n = DVec3::new(0.0, 1.0, 0.0);
let d = DVec3::new(1.0, -1.0, 0.0).normalize();
let r = reflect(d, n);
assert!((r - DVec3::new(1.0, 1.0, 0.0).normalize()).length() < 1e-12);
assert!((d.dot(n).abs() - r.dot(n).abs()).abs() < 1e-12);
assert!((reflect(r, n) - d).length() < 1e-12);
assert!((reflect(-n, n) - n).length() < 1e-12);
}
#[test]
fn a_normal_is_oriented_per_hit_not_per_surface() {
let n = DVec3::Z;
assert_eq!(oriented_against(n, -DVec3::Z), n);
assert_eq!(oriented_against(n, DVec3::Z), -n);
for d in [DVec3::Z, -DVec3::Z, DVec3::new(0.3, 0.1, 0.9).normalize()] {
assert!(oriented_against(n, d).dot(d) <= 0.0);
}
}
}