// Geometric primitives that live *in* the coordinate spaces: rays, planes, boxes, frusta.
///|
#valtype
pub(all) struct Ray {
origin : Vec3
/// Unit length for the intersection routines to return true distances.
dir : Vec3
} derive(Eq, Debug)
///|
pub fn Ray::new(origin : Vec3, dir : Vec3) -> Ray {
{ origin, dir: dir.normalize(), }
}
///|
pub fn Ray::at(self : Ray, t : Double) -> Vec3 {
self.origin + self.dir.scale(t)
}
///|
/// Apply a rigid / affine matrix to the ray (dir re-normalised; distances t scale by the
/// returned factor if the matrix scales).
pub fn Ray::transform(self : Ray, m : Mat4) -> Ray {
{ origin: m.mul_point(self.origin), dir: m.mul_dir(self.dir).normalize(), }
}
///|
/// Plane `dot(normal, p) + d = 0`; `normal` should be unit for `signed_distance` to be metric.
#valtype
pub(all) struct Plane {
normal : Vec3
d : Double
} derive(Eq, Debug)
///|
pub fn Plane::from_point_normal(point : Vec3, normal : Vec3) -> Plane {
let n = normal.normalize()
{ normal: n, d: -n.dot(point), }
}
///|
/// Plane through three points (normal = (b-a) x (c-a), counter-clockwise => facing viewer).
pub fn Plane::from_points(a : Vec3, b : Vec3, c : Vec3) -> Plane {
Plane::from_point_normal(a, (b - a).cross(c - a))
}
///|
pub fn Plane::signed_distance(self : Plane, p : Vec3) -> Double {
self.normal.dot(p) + self.d
}
///|
/// Distance t >= 0 along the ray to the plane; `None` if parallel or behind the origin.
pub fn Ray::intersect_plane(self : Ray, pl : Plane) -> Double? {
let denom = pl.normal.dot(self.dir)
if denom == 0.0 {
return None
}
let t = -(pl.normal.dot(self.origin) + pl.d) / denom
if t >= 0.0 {
Some(t)
} else {
None
}
}
///|
/// Nearest distance t >= 0 to a sphere surface hit (entry, or exit if starting inside).
pub fn Ray::intersect_sphere(
self : Ray,
center : Vec3,
radius : Double,
) -> Double? {
let oc = self.origin - center
let b = oc.dot(self.dir)
let c = oc.dot(oc) - radius * radius
let disc = b * b - c
if disc < 0.0 {
return None
}
let s = disc.sqrt()
let t0 = -b - s
let t1 = -b + s
if t0 >= 0.0 {
Some(t0)
} else if t1 >= 0.0 {
Some(t1)
} else {
None
}
}
///|
/// Axis-aligned bounding box.
#valtype
pub(all) struct Aabb {
min : Vec3
max : Vec3
} derive(Eq, Debug)
///|
pub fn Aabb::new(min : Vec3, max : Vec3) -> Aabb {
{ min, max, }
}
///|
pub fn Aabb::from_center_half(center : Vec3, half : Vec3) -> Aabb {
{ min: center - half, max: center + half, }
}
///|
pub fn Aabb::center(self : Aabb) -> Vec3 {
(self.min + self.max).scale(0.5)
}
///|
pub fn Aabb::half_extents(self : Aabb) -> Vec3 {
(self.max - self.min).scale(0.5)
}
///|
pub fn Aabb::union(self : Aabb, o : Aabb) -> Aabb {
{ min: self.min.min(o.min), max: self.max.max(o.max), }
}
///|
pub fn Aabb::expand(self : Aabb, p : Vec3) -> Aabb {
{ min: self.min.min(p), max: self.max.max(p), }
}
///|
pub fn Aabb::contains_point(self : Aabb, p : Vec3) -> Bool {
p.x >= self.min.x &&
p.x <= self.max.x &&
p.y >= self.min.y &&
p.y <= self.max.y &&
p.z >= self.min.z &&
p.z <= self.max.z
}
///|
/// Corner `i` in 0..7 (bit0 -> x, bit1 -> y, bit2 -> z select max).
pub fn Aabb::corner(self : Aabb, i : Int) -> Vec3 {
{
x: if (i & 1) != 0 {
self.max.x
} else {
self.min.x
},
y: if (i & 2) != 0 {
self.max.y
} else {
self.min.y
},
z: if (i & 4) != 0 {
self.max.z
} else {
self.min.z
},
}
}
///|
/// Exact world-space AABB of this box under an affine matrix (Arvo): cost of one 3x3
/// multiply, no corner enumeration.
pub fn Aabb::transform(self : Aabb, m : Mat4) -> Aabb {
let c = self.center()
let h = self.half_extents()
let nc = m.mul_point(c)
let nh : Vec3 = {
x: m.m00.abs() * h.x + m.m01.abs() * h.y + m.m02.abs() * h.z,
y: m.m10.abs() * h.x + m.m11.abs() * h.y + m.m12.abs() * h.z,
z: m.m20.abs() * h.x + m.m21.abs() * h.y + m.m22.abs() * h.z,
}
{ min: nc - nh, max: nc + nh, }
}
///|
/// Slab test. Returns (t_enter, t_exit) with t_exit >= max(t_enter, 0); t_enter is clamped to 0
/// when the origin is inside. `None` on a miss.
pub fn Ray::intersect_aabb(self : Ray, b : Aabb) -> (Double, Double)? {
let mut t0 = 0.0
let mut t1 = @double.infinity
for i in 0..<3 {
let o = self.origin.get(i)
let d = self.dir.get(i)
let lo = b.min.get(i)
let hi = b.max.get(i)
if d == 0.0 {
if o < lo || o > hi {
return None
}
} else {
let inv = 1.0 / d
let mut a = (lo - o) * inv
let mut c = (hi - o) * inv
if a > c {
let tmp = a
a = c
c = tmp
}
if a > t0 {
t0 = a
}
if c < t1 {
t1 = c
}
if t0 > t1 {
return None
}
}
}
Some((t0, t1))
}
///|
pub(all) enum Containment {
Outside
Intersecting
Inside
} derive(Eq, Debug)
///|
/// The six planes of a view frustum with normals pointing INSIDE.
#valtype
pub(all) struct Frustum {
left : Plane
right : Plane
bottom : Plane
top : Plane
near : Plane
far : Plane
} derive(Eq, Debug)
///|
fn plane_from_vec4(v : Vec4) -> Plane {
let n = Vec3::new(v.x, v.y, v.z)
let l = n.length()
if l > 0.0 {
{ normal: n.scale(1.0 / l), d: v.w / l, }
} else {
// degenerate (infinite far plane): constant, always satisfied when d >= 0
{ normal: n, d: v.w, }
}
}
///|
/// Extract frustum planes from `clip = view_proj * world`; handles every `DepthMode`.
/// The planes are expressed in the space that `view_proj` takes as INPUT (world space when
/// `view_proj = P * V`; model space when `P * V * M`).
pub fn Frustum::from_view_proj(m : Mat4, depth : DepthMode) -> Frustum {
let r0 = m.row(0)
let r1 = m.row(1)
let r2 = m.row(2)
let r3 = m.row(3)
let (near, far) = match depth {
ZeroToOne => (r2, r3 - r2)
NegOneToOne => (r3 + r2, r3 - r2)
ReversedZ => (r3 - r2, r2)
}
{
left: plane_from_vec4(r3 + r0),
right: plane_from_vec4(r3 - r0),
bottom: plane_from_vec4(r3 + r1),
top: plane_from_vec4(r3 - r1),
near: plane_from_vec4(near),
far: plane_from_vec4(far),
}
}
///|
pub fn Frustum::contains_point(self : Frustum, p : Vec3) -> Bool {
self.left.signed_distance(p) >= 0.0 &&
self.right.signed_distance(p) >= 0.0 &&
self.bottom.signed_distance(p) >= 0.0 &&
self.top.signed_distance(p) >= 0.0 &&
self.near.signed_distance(p) >= 0.0 &&
self.far.signed_distance(p) >= 0.0
}
///|
/// Conservative sphere test (never a false negative; rare false positives near frustum edges).
pub fn Frustum::intersects_sphere(self : Frustum, c : Vec3, r : Double) -> Bool {
self.left.signed_distance(c) >= -r &&
self.right.signed_distance(c) >= -r &&
self.bottom.signed_distance(c) >= -r &&
self.top.signed_distance(c) >= -r &&
self.near.signed_distance(c) >= -r &&
self.far.signed_distance(c) >= -r
}
///|
fn classify_plane(pl : Plane, b : Aabb) -> Int {
// 0 = fully outside, 1 = straddles, 2 = fully inside
let pv : Vec3 = {
x: if pl.normal.x >= 0.0 {
b.max.x
} else {
b.min.x
},
y: if pl.normal.y >= 0.0 {
b.max.y
} else {
b.min.y
},
z: if pl.normal.z >= 0.0 {
b.max.z
} else {
b.min.z
},
}
if pl.signed_distance(pv) < 0.0 {
return 0
}
let nv : Vec3 = {
x: if pl.normal.x >= 0.0 {
b.min.x
} else {
b.max.x
},
y: if pl.normal.y >= 0.0 {
b.min.y
} else {
b.max.y
},
z: if pl.normal.z >= 0.0 {
b.min.z
} else {
b.max.z
},
}
if pl.signed_distance(nv) < 0.0 {
1
} else {
2
}
}
///|
/// Box classification. `Outside` is exact-safe (box is certainly invisible); `Inside` is
/// certain; `Intersecting` may include a few boxes that are actually just outside a corner.
pub fn Frustum::classify_aabb(self : Frustum, b : Aabb) -> Containment {
let mut all_in = true
for pl in [self.left, self.right, self.bottom, self.top, self.near, self.far] {
match classify_plane(pl, b) {
0 => return Outside
1 => all_in = false
_ => ()
}
}
if all_in {
Inside
} else {
Intersecting
}
}