synvael/crates/renderer/src/frustum.rs

67 lines
2.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 GribbHartmann 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;