Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
//! A view [`Frustum`]: six planes used for visibility culling.
|
||||
|
||||
use crate::math::{Aabb, Plane};
|
||||
use glam::{Mat4, Vec3, Vec4, Vec4Swizzles};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A frustum represented by six bounding planes, each with its normal pointing
|
||||
/// *inward*. A point is inside the frustum when it lies in the positive
|
||||
/// half-space of every plane.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Frustum {
|
||||
/// Planes ordered: left, right, bottom, top, near, far.
|
||||
pub planes: [Plane; 6],
|
||||
}
|
||||
|
||||
impl Frustum {
|
||||
/// Extracts the six frustum planes from a combined view-projection matrix
|
||||
/// using the Gribb–Hartmann method. Works for both perspective and
|
||||
/// orthographic projections.
|
||||
pub fn from_view_projection(view_projection: Mat4) -> Self {
|
||||
// Rows of the matrix (glam is column-major, so build rows explicitly).
|
||||
let m = view_projection;
|
||||
let row0 = Vec4::new(m.x_axis.x, m.y_axis.x, m.z_axis.x, m.w_axis.x);
|
||||
let row1 = Vec4::new(m.x_axis.y, m.y_axis.y, m.z_axis.y, m.w_axis.y);
|
||||
let row2 = Vec4::new(m.x_axis.z, m.y_axis.z, m.z_axis.z, m.w_axis.z);
|
||||
let row3 = Vec4::new(m.x_axis.w, m.y_axis.w, m.z_axis.w, m.w_axis.w);
|
||||
|
||||
let plane_from = |v: Vec4| Plane::new(v.xyz(), v.w);
|
||||
|
||||
let planes = [
|
||||
plane_from(row3 + row0), // left
|
||||
plane_from(row3 - row0), // right
|
||||
plane_from(row3 + row1), // bottom
|
||||
plane_from(row3 - row1), // top
|
||||
plane_from(row3 + row2), // near
|
||||
plane_from(row3 - row2), // far
|
||||
];
|
||||
Self { planes }
|
||||
}
|
||||
|
||||
/// Returns `true` if `point` is inside (or on the boundary of) the frustum.
|
||||
pub fn contains_point(&self, point: Vec3) -> bool {
|
||||
self.planes
|
||||
.iter()
|
||||
.all(|plane| plane.signed_distance(point) >= 0.0)
|
||||
}
|
||||
|
||||
/// Returns `true` if any part of `aabb` is inside the frustum.
|
||||
///
|
||||
/// This is a conservative test: it may very rarely report a box as visible
|
||||
/// when it is just outside a corner, but never culls a visible box. That is
|
||||
/// the correct trade-off for rendering.
|
||||
pub fn intersects_aabb(&self, aabb: &Aabb) -> bool {
|
||||
for plane in &self.planes {
|
||||
// The "positive vertex": the AABB corner farthest along the normal.
|
||||
let p = Vec3::new(
|
||||
if plane.normal.x >= 0.0 {
|
||||
aabb.max.x
|
||||
} else {
|
||||
aabb.min.x
|
||||
},
|
||||
if plane.normal.y >= 0.0 {
|
||||
aabb.max.y
|
||||
} else {
|
||||
aabb.min.y
|
||||
},
|
||||
if plane.normal.z >= 0.0 {
|
||||
aabb.max.z
|
||||
} else {
|
||||
aabb.min.z
|
||||
},
|
||||
);
|
||||
// If the farthest corner is behind a plane, the box is fully outside.
|
||||
if plane.signed_distance(p) < 0.0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns `true` if the sphere at `center` with `radius` is at least
|
||||
/// partially inside the frustum.
|
||||
pub fn intersects_sphere(&self, center: Vec3, radius: f32) -> bool {
|
||||
self.planes
|
||||
.iter()
|
||||
.all(|plane| plane.signed_distance(center) >= -radius)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn perspective_vp() -> Mat4 {
|
||||
let proj = Mat4::perspective_rh(60_f32.to_radians(), 1.0, 1.0, 100.0);
|
||||
let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 0.0), Vec3::NEG_Z, Vec3::Y);
|
||||
proj * view
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_in_front_is_inside() {
|
||||
let f = Frustum::from_view_projection(perspective_vp());
|
||||
assert!(f.contains_point(Vec3::new(0.0, 0.0, -10.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_behind_camera_is_outside() {
|
||||
let f = Frustum::from_view_projection(perspective_vp());
|
||||
assert!(!f.contains_point(Vec3::new(0.0, 0.0, 10.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_beyond_far_is_outside() {
|
||||
let f = Frustum::from_view_projection(perspective_vp());
|
||||
assert!(!f.contains_point(Vec3::new(0.0, 0.0, -500.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_way_off_to_side_is_outside() {
|
||||
let f = Frustum::from_view_projection(perspective_vp());
|
||||
assert!(!f.contains_point(Vec3::new(500.0, 0.0, -10.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aabb_in_view_intersects() {
|
||||
let f = Frustum::from_view_projection(perspective_vp());
|
||||
let bb = Aabb::from_center_half_extents(Vec3::new(0.0, 0.0, -10.0), Vec3::splat(1.0));
|
||||
assert!(f.intersects_aabb(&bb));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aabb_behind_camera_is_culled() {
|
||||
let f = Frustum::from_view_projection(perspective_vp());
|
||||
let bb = Aabb::from_center_half_extents(Vec3::new(0.0, 0.0, 50.0), Vec3::splat(1.0));
|
||||
assert!(!f.intersects_aabb(&bb));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sphere_culling() {
|
||||
let f = Frustum::from_view_projection(perspective_vp());
|
||||
assert!(f.intersects_sphere(Vec3::new(0.0, 0.0, -10.0), 1.0));
|
||||
// Just behind the camera but large enough to poke into the near plane.
|
||||
assert!(!f.intersects_sphere(Vec3::new(0.0, 0.0, 50.0), 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orthographic_frustum_works() {
|
||||
let proj = Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, 1.0, 100.0);
|
||||
let view = Mat4::look_at_rh(Vec3::ZERO, Vec3::NEG_Z, Vec3::Y);
|
||||
let f = Frustum::from_view_projection(proj * view);
|
||||
assert!(f.contains_point(Vec3::new(5.0, 5.0, -10.0)));
|
||||
assert!(!f.contains_point(Vec3::new(50.0, 0.0, -10.0)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user