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:
Homer Simpson
2026-07-05 20:41:02 +02:00
parent 2afb56b329
commit f56a1eea3b
128 changed files with 40493 additions and 2 deletions
+154
View File
@@ -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 GribbHartmann 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)));
}
}