pub use gizmo_math::{Aabb, Frustum, Mat4};
#[inline]
pub fn visible_in_frustum(frustum: &Frustum, model_matrix: &Mat4, local_aabb: Aabb) -> bool {
frustum.intersects_aabb(local_aabb.transform(model_matrix))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Visibility {
Camera,
ShadowOnly,
Culled,
}
pub fn classify_visibility(
camera_frustum: &Frustum,
cascade_frusta: &[Frustum],
model: &Mat4,
local_aabb: Aabb,
material_type: crate::components::MaterialType,
is_transparent: bool,
albedo_alpha: f32,
) -> Visibility {
classify_visibility_world(
camera_frustum,
cascade_frusta,
local_aabb.transform(model),
material_type,
is_transparent,
albedo_alpha,
)
}
pub fn classify_visibility_world(
camera_frustum: &Frustum,
cascade_frusta: &[Frustum],
world_aabb: Aabb,
material_type: crate::components::MaterialType,
is_transparent: bool,
albedo_alpha: f32,
) -> Visibility {
if camera_frustum.intersects_aabb(world_aabb) {
return Visibility::Camera;
}
let is_caster = !is_transparent
&& albedo_alpha >= 0.99
&& !matches!(
material_type,
crate::components::MaterialType::Unlit
| crate::components::MaterialType::Skybox
| crate::components::MaterialType::Backdrop
| crate::components::MaterialType::BackdropPlaced
| crate::components::MaterialType::Grid
);
if is_caster
&& cascade_frusta
.iter()
.any(|f| f.intersects_aabb(world_aabb))
{
Visibility::ShadowOnly
} else {
Visibility::Culled
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::MaterialType;
use gizmo_math::Vec3;
fn cam_frustum(eye: Vec3) -> Frustum {
let view = Mat4::look_at_rh(eye, eye + Vec3::new(0.0, 0.0, -1.0), Vec3::Y);
let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 1.0, 0.1, 100.0);
Frustum::from_matrix(&(proj * view))
}
fn unit_aabb() -> Aabb {
Aabb::new(Vec3::splat(-0.5), Vec3::splat(0.5))
}
#[test]
fn visible_in_frustum_tracks_the_transformed_aabb() {
let f = cam_frustum(Vec3::new(0.0, 0.0, 5.0));
let front = Mat4::from_translation(Vec3::new(0.0, 0.0, -10.0));
assert!(visible_in_frustum(&f, &front, unit_aabb()));
let behind = Mat4::from_translation(Vec3::new(0.0, 0.0, 50.0));
assert!(!visible_in_frustum(&f, &behind, unit_aabb()));
let side = Mat4::from_translation(Vec3::new(100.0, 0.0, -10.0));
assert!(!visible_in_frustum(&f, &side, unit_aabb()));
}
#[test]
fn in_camera_frustum_classifies_as_camera_regardless_of_material() {
let cam = cam_frustum(Vec3::new(0.0, 0.0, 5.0));
let front = Mat4::from_translation(Vec3::new(0.0, 0.0, -10.0));
assert_eq!(
classify_visibility(&cam, &[], &front, unit_aabb(), MaterialType::Unlit, true, 0.1),
Visibility::Camera
);
}
#[test]
fn opaque_lit_caster_outside_view_but_in_cascade_is_shadow_only() {
let cam = cam_frustum(Vec3::new(0.0, 0.0, 5.0));
let cascade = cam_frustum(Vec3::new(0.0, 0.0, 60.0));
let behind = Mat4::from_translation(Vec3::new(0.0, 0.0, 50.0));
assert!(!visible_in_frustum(&cam, &behind, unit_aabb()));
assert!(visible_in_frustum(&cascade, &behind, unit_aabb()));
assert_eq!(
classify_visibility(&cam, &[cascade], &behind, unit_aabb(), MaterialType::Pbr, false, 1.0),
Visibility::ShadowOnly
);
assert_eq!(
classify_visibility(&cam, &[cascade], &behind, unit_aabb(), MaterialType::Water, false, 1.0),
Visibility::ShadowOnly
);
}
#[test]
fn caster_with_no_cascade_containing_it_is_culled() {
let cam = cam_frustum(Vec3::new(0.0, 0.0, 5.0));
let behind = Mat4::from_translation(Vec3::new(0.0, 0.0, 50.0));
assert_eq!(
classify_visibility(&cam, &[], &behind, unit_aabb(), MaterialType::Pbr, false, 1.0),
Visibility::Culled
);
let far_cascade = cam_frustum(Vec3::new(500.0, 0.0, 5.0));
assert_eq!(
classify_visibility(&cam, &[far_cascade], &behind, unit_aabb(), MaterialType::Pbr, false, 1.0),
Visibility::Culled
);
}
#[test]
fn transparent_or_faded_or_unlit_objects_are_not_casters() {
let cam = cam_frustum(Vec3::new(0.0, 0.0, 5.0));
let cascade = cam_frustum(Vec3::new(0.0, 0.0, 60.0));
let behind = Mat4::from_translation(Vec3::new(0.0, 0.0, 50.0));
assert_eq!(
classify_visibility(&cam, &[cascade], &behind, unit_aabb(), MaterialType::Pbr, true, 1.0),
Visibility::Culled
);
assert_eq!(
classify_visibility(&cam, &[cascade], &behind, unit_aabb(), MaterialType::Pbr, false, 0.5),
Visibility::Culled
);
for mt in [
MaterialType::Unlit,
MaterialType::Skybox,
MaterialType::Backdrop,
MaterialType::Grid,
] {
assert_eq!(
classify_visibility(&cam, &[cascade], &behind, unit_aabb(), mt, false, 1.0),
Visibility::Culled,
"{mt:?} must not be a shadow caster"
);
}
}
#[test]
fn a_camera_locked_backdrop_must_be_culled_against_its_locked_transform() {
let eye = Vec3::new(0.0, 0.0, 905.0);
let cam = cam_frustum(eye);
let authored = Mat4::from_translation(Vec3::new(0.0, 0.0, -10.0));
let bounds = Aabb::new(Vec3::splat(-2.0), Vec3::splat(2.0));
assert_eq!(
classify_visibility(&cam, &[], &authored, bounds, MaterialType::Backdrop, false, 1.0),
Visibility::Culled,
"premise: the authored transform really is outside this camera's frustum"
);
let drawn = crate::backdrop::camera_locked_model(MaterialType::Backdrop, &authored, eye);
assert_eq!(
classify_visibility(&cam, &[], &drawn, bounds, MaterialType::Backdrop, false, 1.0),
Visibility::Camera,
"a camera-locked backdrop is always in front of the camera; culling it away is \
how 191 backdrop meshes never reach the screen"
);
}
#[test]
fn the_world_space_entry_point_decides_exactly_what_the_local_space_one_does() {
let cam = cam_frustum(Vec3::new(0.0, 0.0, 5.0));
let cascades = [
cam_frustum(Vec3::new(0.0, 0.0, 60.0)),
cam_frustum(Vec3::new(300.0, 0.0, 5.0)),
];
let mats = [
MaterialType::Pbr,
MaterialType::Unlit,
MaterialType::Water,
MaterialType::Backdrop,
MaterialType::Skybox,
MaterialType::Grid,
MaterialType::BakedLit,
];
let models = [
Mat4::from_translation(Vec3::new(0.0, 0.0, -10.0)),
Mat4::from_translation(Vec3::new(0.0, 0.0, 50.0)),
Mat4::from_translation(Vec3::new(305.0, 0.0, 0.0)),
Mat4::from_scale_rotation_translation(
Vec3::new(3.0, 0.5, 2.0),
gizmo_math::Quat::from_rotation_y(0.7),
Vec3::new(0.0, 0.0, 55.0),
),
Mat4::from_scale_rotation_translation(
Vec3::splat(40.0),
gizmo_math::Quat::from_rotation_x(1.1),
Vec3::ZERO,
),
];
for m in &models {
for mt in mats {
for transparent in [false, true] {
for alpha in [1.0f32, 0.99, 0.5] {
let local = classify_visibility(
&cam, &cascades, m, unit_aabb(), mt, transparent, alpha,
);
let world = classify_visibility_world(
&cam,
&cascades,
unit_aabb().transform(m),
mt,
transparent,
alpha,
);
assert_eq!(local, world, "{mt:?} transparent={transparent} alpha={alpha}");
}
}
}
}
}
#[test]
fn caster_opacity_gate_boundary_is_inclusive_at_0_99() {
let cam = cam_frustum(Vec3::new(0.0, 0.0, 5.0));
let cascade = cam_frustum(Vec3::new(0.0, 0.0, 60.0));
let behind = Mat4::from_translation(Vec3::new(0.0, 0.0, 50.0));
assert_eq!(
classify_visibility(&cam, &[cascade], &behind, unit_aabb(), MaterialType::Pbr, false, 0.99),
Visibility::ShadowOnly
);
assert_eq!(
classify_visibility(&cam, &[cascade], &behind, unit_aabb(), MaterialType::Pbr, false, 0.98),
Visibility::Culled
);
}
}