use glam::{Mat4, Vec3};
use slotmap::{DenseSlotMap, KeyData};
use crate::{
bounds::Aabb,
frustum::Frustum,
meshes::MeshKey,
scene_spatial::{
node::{SceneNode, SceneNodeFlags},
query::NodeFilter,
SceneSpatial,
},
};
fn fake_mesh_key(slotmap: &mut DenseSlotMap<MeshKey, ()>) -> MeshKey {
slotmap.insert(())
}
fn node(mesh_key: MeshKey, min: Vec3, max: Vec3) -> SceneNode {
SceneNode {
aabb: Aabb { min, max },
mesh_key,
flags: SceneNodeFlags {
cast_shadows: true,
receive_shadows: true,
hidden: false,
hud: false,
dynamic: false,
},
}
}
#[test]
fn insert_and_envelope_query_finds_node() {
let mut keys: DenseSlotMap<MeshKey, ()> = DenseSlotMap::with_key();
let mut spatial = SceneSpatial::default();
let key = fake_mesh_key(&mut keys);
spatial.insert(node(
key,
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, 1.0, 1.0),
));
let hits: Vec<_> = spatial
.query_envelope(&Aabb {
min: Vec3::splat(-2.0),
max: Vec3::splat(2.0),
})
.map(|n| n.mesh_key)
.collect();
assert_eq!(hits, vec![key]);
}
#[test]
fn update_replaces_old_envelope() {
let mut keys: DenseSlotMap<MeshKey, ()> = DenseSlotMap::with_key();
let mut spatial = SceneSpatial::default();
let key = fake_mesh_key(&mut keys);
spatial.insert(node(
key,
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 1.0, 1.0),
));
spatial.update(
key,
Aabb {
min: Vec3::new(100.0, 100.0, 100.0),
max: Vec3::new(101.0, 101.0, 101.0),
},
);
let origin_hits: Vec<_> = spatial
.query_envelope(&Aabb {
min: Vec3::splat(-2.0),
max: Vec3::splat(2.0),
})
.collect();
assert!(
origin_hits.is_empty(),
"old envelope still resident after update"
);
let far_hits: Vec<_> = spatial
.query_envelope(&Aabb {
min: Vec3::splat(99.0),
max: Vec3::splat(102.0),
})
.map(|n| n.mesh_key)
.collect();
assert_eq!(far_hits, vec![key]);
}
#[test]
fn remove_evicts_the_leaf() {
let mut keys: DenseSlotMap<MeshKey, ()> = DenseSlotMap::with_key();
let mut spatial = SceneSpatial::default();
let key = fake_mesh_key(&mut keys);
spatial.insert(node(key, Vec3::splat(-1.0), Vec3::splat(1.0)));
spatial.remove(key);
assert_eq!(spatial.len(), 0);
let hits: Vec<_> = spatial
.query_envelope(&Aabb {
min: Vec3::splat(-2.0),
max: Vec3::splat(2.0),
})
.collect();
assert!(hits.is_empty());
}
#[test]
fn set_dynamic_moves_node_between_tree_and_sidecar() {
let mut keys: DenseSlotMap<MeshKey, ()> = DenseSlotMap::with_key();
let mut spatial = SceneSpatial::default();
let key = fake_mesh_key(&mut keys);
spatial.insert(node(key, Vec3::splat(-1.0), Vec3::splat(1.0)));
assert!(!spatial.is_dynamic(key));
spatial.set_dynamic(key, true);
assert!(spatial.is_dynamic(key));
let hits: Vec<_> = spatial
.query_envelope(&Aabb {
min: Vec3::splat(-2.0),
max: Vec3::splat(2.0),
})
.map(|n| n.mesh_key)
.collect();
assert_eq!(hits, vec![key]);
spatial.set_dynamic(key, false);
assert!(!spatial.is_dynamic(key));
}
#[test]
fn frustum_query_parity_with_linear_scan() {
let mut keys: DenseSlotMap<MeshKey, ()> = DenseSlotMap::with_key();
let mut spatial = SceneSpatial::default();
let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 30.0), Vec3::ZERO, Vec3::Y);
let proj = Mat4::perspective_rh(45.0_f32.to_radians(), 16.0 / 9.0, 0.1, 100.0);
let frustum = Frustum::from_view_projection(proj * view);
let mut linear_hits = Vec::new();
for i in 0..100 {
let t = i as f32;
let x = (t * 1.3).sin() * 25.0;
let y = (t * 0.7).cos() * 25.0;
let z = ((t * 0.4).sin() * 25.0) + 5.0;
let half = 0.5 + (t * 0.11).fract();
let min = Vec3::new(x - half, y - half, z - half);
let max = Vec3::new(x + half, y + half, z + half);
let key = fake_mesh_key(&mut keys);
let aabb = Aabb { min, max };
if frustum.intersects_aabb(&aabb) {
linear_hits.push(key);
}
spatial.insert(node(key, min, max));
}
let mut tree_hits: Vec<_> = spatial
.query_frustum(&frustum, NodeFilter::default())
.map(|n| n.mesh_key)
.collect();
tree_hits.sort_by_key(slotmap::Key::data);
let mut linear_sorted = linear_hits.clone();
linear_sorted.sort_by_key(slotmap::Key::data);
assert_eq!(
tree_hits, linear_sorted,
"BVH frustum query disagrees with linear scan"
);
}
#[test]
fn flag_filter_excludes_hidden_and_hud() {
let mut keys: DenseSlotMap<MeshKey, ()> = DenseSlotMap::with_key();
let mut spatial = SceneSpatial::default();
let key_a = fake_mesh_key(&mut keys);
let key_b = fake_mesh_key(&mut keys);
let key_c = fake_mesh_key(&mut keys);
let node_a = node(key_a, Vec3::splat(-1.0), Vec3::splat(1.0));
let mut node_b = node(key_b, Vec3::splat(-1.0), Vec3::splat(1.0));
let mut node_c = node(key_c, Vec3::splat(-1.0), Vec3::splat(1.0));
node_b.flags.hidden = true;
node_c.flags.hud = true;
spatial.insert(node_a);
spatial.insert(node_b);
spatial.insert(node_c);
let view = Mat4::look_at_rh(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO, Vec3::Y);
let proj = Mat4::perspective_rh(60.0_f32.to_radians(), 1.0, 0.1, 100.0);
let frustum = Frustum::from_view_projection(proj * view);
let camera_hits: Vec<_> = spatial
.query_frustum(&frustum, NodeFilter::camera_default())
.map(|n| n.mesh_key)
.collect();
assert!(camera_hits.contains(&key_a));
assert!(!camera_hits.contains(&key_b), "hidden node leaked through");
assert!(camera_hits.contains(&key_c), "hud was excluded for camera");
let shadow_hits: Vec<_> = spatial
.query_frustum(&frustum, NodeFilter::shadow_caster())
.map(|n| n.mesh_key)
.collect();
assert!(shadow_hits.contains(&key_a));
assert!(!shadow_hits.contains(&key_b));
assert!(!shadow_hits.contains(&key_c));
}
#[test]
fn cube_face_frustum_prunes_other_face_geometry() {
let mut keys: DenseSlotMap<MeshKey, ()> = DenseSlotMap::with_key();
let mut spatial = SceneSpatial::default();
let mut placed = Vec::new();
for (axis, label) in [
(Vec3::new(10.0, 0.0, 0.0), "+x"),
(Vec3::new(-10.0, 0.0, 0.0), "-x"),
(Vec3::new(0.0, 10.0, 0.0), "+y"),
(Vec3::new(0.0, -10.0, 0.0), "-y"),
(Vec3::new(0.0, 0.0, 10.0), "+z"),
(Vec3::new(0.0, 0.0, -10.0), "-z"),
] {
let key = fake_mesh_key(&mut keys);
spatial.insert(node(key, axis - Vec3::splat(0.5), axis + Vec3::splat(0.5)));
placed.push((key, label));
}
let view = Mat4::look_at_rh(Vec3::ZERO, Vec3::new(1.0, 0.0, 0.0), Vec3::Y);
let proj = Mat4::perspective_rh(90.0_f32.to_radians(), 1.0, 0.1, 100.0);
let frustum = Frustum::from_view_projection(proj * view);
let hits: Vec<_> = spatial
.query_frustum(&frustum, NodeFilter::default())
.map(|n| n.mesh_key)
.collect();
let plus_x_key = placed[0].0;
assert!(hits.contains(&plus_x_key), "+x box should be in +x frustum");
for (key, label) in &placed[1..] {
assert!(
!hits.contains(key),
"{label} box leaked through +x frustum query"
);
}
}
#[test]
fn rebuild_if_needed_preserves_query_results() {
let mut keys: DenseSlotMap<MeshKey, ()> = DenseSlotMap::with_key();
let mut spatial = SceneSpatial::default();
let mut inserted_keys = Vec::new();
for i in 0..10 {
let key = fake_mesh_key(&mut keys);
let p = i as f32 * 2.0;
spatial.insert(node(
key,
Vec3::new(p, p, p),
Vec3::new(p + 1.0, p + 1.0, p + 1.0),
));
inserted_keys.push(key);
}
spatial.mark_rebuild_needed();
spatial.rebuild_if_needed();
let hits: Vec<_> = spatial.iter_all().map(|n| n.mesh_key).collect();
assert_eq!(hits.len(), inserted_keys.len());
}
const _: fn() = || {
fn assert_copy<T: Copy>() {}
fn assert_eq_hash<T: Eq + std::hash::Hash>() {}
assert_copy::<MeshKey>();
assert_eq_hash::<MeshKey>();
let _ = KeyData::default();
};