use glam::Affine3A;
use unlit_ecs::{Entity, World};
use crate::bounds::{Aabb, FrustumPlanes, Obb};
use crate::components::{GpuMesh, Transform};
pub fn is_culled(aabb: Aabb, world_from_local: &Affine3A, frustum: &FrustumPlanes) -> bool {
!frustum.test_obb(&Obb::from_aabb(aabb, 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(world: &World, frustum: &FrustumPlanes, out: &mut Vec<VisibleMesh>) {
out.clear();
for (entity, (mesh, transform)) in world.query::<(&GpuMesh, Option<&Transform>)>() {
let model = match transform {
Some(transform) => transform.compute_matrix(),
None => Affine3A::IDENTITY,
};
if is_culled(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_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.spawn((mesh.clone(), Transform::default()));
let far = world.spawn((
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, &frustum, &mut out);
let found: Vec<_> = out.iter().map(|mesh| mesh.entity).collect();
assert_eq!(found, [near]);
assert_ne!(found, [far]);
}
}