use glam::Affine3A;
#[cfg(test)]
use unlit_ecs::World;
use unlit_ecs::{Entity, Scope};
use crate::bounds::{Aabb, FrustumPlanes};
use crate::components::{GpuMesh, InstanceVisibility, Transform};
pub fn is_culled(aabb: Aabb, world_from_local: &Affine3A, frustum: &FrustumPlanes) -> bool {
!frustum.test_obb(&aabb.transformed(world_from_local))
}
#[derive(Clone, Copy)]
pub(crate) struct VisibleMesh {
pub(crate) entity: Entity,
pub(crate) world_from_local: Affine3A,
}
pub(crate) fn collect_visible(
scope: &Scope<'_>,
frustum: &FrustumPlanes,
out: &mut Vec<VisibleMesh>,
) {
out.clear();
for (gpu_mesh, option, visibility, entity) in scope.query::<(
&GpuMesh,
Option<&Transform>,
Option<&InstanceVisibility>,
Entity,
)>() {
if visibility.is_some_and(|visibility| !visibility.visible) {
continue;
}
let model = match option {
Some(transform) => transform.compute_matrix(),
None => Affine3A::IDENTITY,
};
if is_culled(gpu_mesh.aabb, &model, frustum) {
continue;
}
out.push(VisibleMesh {
entity,
world_from_local: model,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrayvec::ArrayVec;
use glam::Vec3;
use std::rc::Rc;
use unlit_ecs::bundle;
use unlit_wgpu::resources::{ResourceGraph, Virtual};
use unlit_wgpu::specialize::VertexLayout;
use crate::components::MeshParts;
fn test_perspective() -> glam::Mat4 {
glam::camera::rh::proj::opengl::perspective(1.0, 1.0, 0.1, 100.0)
}
fn frustum_at(eye: Vec3) -> FrustumPlanes {
let view = glam::camera::rh::view::look_at_mat4(eye, Vec3::ZERO, Vec3::Y);
FrustumPlanes::from_clip_from_world(test_perspective() * view)
}
#[test]
fn a_mesh_in_front_of_the_camera_is_visible() {
let frustum = frustum_at(Vec3::new(0.0, 0.0, 5.0));
let aabb = Aabb::new(Vec3::ZERO, Vec3::splat(0.5));
assert!(!is_culled(aabb, &Affine3A::IDENTITY, &frustum));
}
#[test]
fn a_mesh_behind_the_camera_is_culled() {
let frustum = frustum_at(Vec3::new(0.0, 0.0, 5.0));
let aabb = Aabb::new(Vec3::new(0.0, 0.0, 20.0), Vec3::splat(0.5));
assert!(is_culled(aabb, &Affine3A::IDENTITY, &frustum));
}
#[test]
fn a_mesh_moved_out_of_view_by_its_transform_is_culled() {
let frustum = frustum_at(Vec3::new(0.0, 0.0, 5.0));
let aabb = Aabb::new(Vec3::ZERO, Vec3::splat(0.5));
let far = Affine3A::from_translation(Vec3::new(-10.0, 0.0, 0.0));
assert!(is_culled(aabb, &far, &frustum));
}
#[test]
fn collect_visible_keeps_only_the_visible_meshes() {
let mut world = World::new();
let mut graph = ResourceGraph::new();
let root = graph.insert(Virtual, None);
let mesh = GpuMesh {
parts: Rc::new(MeshParts {
root,
vertex_buffers: ArrayVec::new(),
index_buffer: None,
bind_group_id: None,
vertex_allocation: None,
index_allocation: None,
morph_deltas_allocation: None,
metadata_index: 0,
}),
vertex_layout: VertexLayout::default(),
count: 0,
first: 0,
base_vertex: 0,
indexed: false,
aabb: Aabb::new(Vec3::ZERO, Vec3::splat(0.5)),
morph_targets: 0,
skinned: false,
};
let near = world
.root_scope_mut()
.spawn(bundle![mesh.clone(), Transform::default()]);
let far = world.root_scope_mut().spawn(bundle![
mesh,
Transform {
translation: Vec3::new(0.0, 0.0, 20.0),
rotation: glam::Quat::IDENTITY,
scale: Vec3::ONE,
},
]);
let frustum = frustum_at(Vec3::new(0.0, 0.0, 5.0));
let mut out = Vec::new();
collect_visible(&world.root_scope(), &frustum, &mut out);
let found: Vec<_> = out.iter().map(|mesh| mesh.entity).collect();
assert_eq!(found, [near]);
assert_ne!(found, [far]);
}
#[test]
fn a_hidden_entity_is_not_collected() {
let mut world = World::new();
let mut graph = ResourceGraph::new();
let root = graph.insert(Virtual, None);
let mesh = GpuMesh {
parts: Rc::new(MeshParts {
root,
vertex_buffers: ArrayVec::new(),
index_buffer: None,
bind_group_id: None,
vertex_allocation: None,
index_allocation: None,
morph_deltas_allocation: None,
metadata_index: 0,
}),
vertex_layout: VertexLayout::default(),
count: 0,
first: 0,
base_vertex: 0,
indexed: false,
aabb: Aabb::new(Vec3::ZERO, Vec3::splat(0.5)),
morph_targets: 0,
skinned: false,
};
let shown = world.root_scope_mut().spawn(bundle![
mesh.clone(),
Transform::default(),
InstanceVisibility::default(),
]);
let hidden = world.root_scope_mut().spawn(bundle![
mesh,
Transform::default(),
InstanceVisibility { visible: false },
]);
let frustum = frustum_at(Vec3::new(0.0, 0.0, 5.0));
let mut out = Vec::new();
collect_visible(&world.root_scope(), &frustum, &mut out);
let found: Vec<_> = out.iter().map(|mesh| mesh.entity).collect();
assert_eq!(found, [shown]);
assert_ne!(found, [hidden]);
}
#[test]
fn the_default_visibility_is_drawn() {
assert!(InstanceVisibility::default().visible);
}
}