67 lines
2.8 KiB
Rust
67 lines
2.8 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
||
//! CPU-side view-frustum culling for chunk meshes.
|
||
|
||
use glam::{Mat4, Vec3, Vec4};
|
||
|
||
/// Six view-frustum planes in world space.
|
||
///
|
||
/// Each plane is stored as a [`Vec4`] `(a, b, c, d)` where `(a, b, c)` is the inward-facing normal and the plane equation is `a·x + b·y + c·z + d = 0`. The planes are normalised, so evaluating the equation at a point yields the signed distance from that point to the plane; a non-negative result lies on the interior side.
|
||
pub(crate) struct Frustum {
|
||
/// The six planes in the order left, right, bottom, top, near, far.
|
||
planes: [Vec4; 6],
|
||
}
|
||
|
||
impl Frustum {
|
||
/// Builds the frustum from a combined view-projection matrix.
|
||
///
|
||
/// The matrix is expected to map world space into Vulkan clip space, whose depth range is `[0, 1]`. Under that convention the near plane is the third matrix row alone (`r2`), not `r3 + r2` as in the OpenGL `[-1, 1]` range; the OpenGL form would cull geometry directly ahead of the camera. Each plane is normalised by the length of its `(a, b, c)` normal so subsequent evaluations return true signed distances.
|
||
pub(crate) fn from_view_proj(mvp: Mat4) -> Self {
|
||
// glam stores matrices column-major; the Gribb–Hartmann derivation operates on the rows of the combined matrix, so rows are read here rather than columns.
|
||
let r0 = mvp.row(0);
|
||
let r1 = mvp.row(1);
|
||
let r2 = mvp.row(2);
|
||
let r3 = mvp.row(3);
|
||
|
||
let mut planes = [
|
||
r3 + r0, // left
|
||
r3 - r0, // right
|
||
r3 + r1, // bottom
|
||
r3 - r1, // top
|
||
r2, // near (Vulkan depth range [0, 1], hence r2 alone)
|
||
r3 - r2, // far
|
||
];
|
||
|
||
for plane in &mut planes {
|
||
let normal_length = plane.truncate().length();
|
||
*plane /= normal_length;
|
||
}
|
||
|
||
Self { planes }
|
||
}
|
||
|
||
/// Returns whether the axis-aligned box spanning `[min, max]` is at least partially inside the frustum.
|
||
pub(crate) fn intersects_aabb(&self, min: Vec3, max: Vec3) -> bool {
|
||
for plane in &self.planes {
|
||
let normal = plane.truncate();
|
||
|
||
// The "positive vertex" is the box corner farthest along the plane normal: per axis the max component is taken when the normal's component is non-negative, otherwise the min. If even that corner lies behind the plane, the whole box does.
|
||
let positive_vertex = Vec3::new(
|
||
if normal.x >= 0.0 { max.x } else { min.x },
|
||
if normal.y >= 0.0 { max.y } else { min.y },
|
||
if normal.z >= 0.0 { max.z } else { min.z },
|
||
);
|
||
|
||
if plane.dot(positive_vertex.extend(1.0)) < 0.0 {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
true
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
#[path = "tests/frustum.rs"]
|
||
mod tests;
|