
# Objective - Reduce time spent in the `check_visibility` system ## Solution - Use `Vec3A` for all bounding volume types to leverage SIMD optimisations and to avoid repeated runtime conversions from `Vec3` to `Vec3A` - Inline all bounding volume intersection methods - Add on-the-fly calculated `Aabb` -> `Sphere` and do `Sphere`-`Frustum` intersection tests before `Aabb`-`Frustum` tests. This is faster for `many_cubes` but could be slower in other cases where the sphere test gives a false-positive that the `Aabb` test discards. Also, I tested precalculating the `Sphere`s and inserting them alongside the `Aabb` but this was slower. - Do not test meshes against the far plane. Apparently games don't do this anymore with infinite projections, and it's one fewer plane to test against. I made it optional and still do the test for culling lights but that is up for discussion. - These collectively reduce `check_visibility` execution time in `many_cubes -- sphere` from 2.76ms to 1.48ms and increase frame rate from ~42fps to ~44fps
211 lines
6.2 KiB
Rust
211 lines
6.2 KiB
Rust
mod render_layers;
|
|
|
|
use bevy_math::Vec3A;
|
|
pub use render_layers::*;
|
|
|
|
use bevy_app::{CoreStage, Plugin};
|
|
use bevy_asset::{Assets, Handle};
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_reflect::Reflect;
|
|
use bevy_transform::components::GlobalTransform;
|
|
use bevy_transform::TransformSystem;
|
|
|
|
use crate::{
|
|
camera::{Camera, CameraProjection, OrthographicProjection, PerspectiveProjection},
|
|
mesh::Mesh,
|
|
primitives::{Aabb, Frustum, Sphere},
|
|
};
|
|
|
|
/// User indication of whether an entity is visible
|
|
#[derive(Component, Clone, Reflect, Debug)]
|
|
#[reflect(Component)]
|
|
pub struct Visibility {
|
|
pub is_visible: bool,
|
|
}
|
|
|
|
impl Default for Visibility {
|
|
fn default() -> Self {
|
|
Self { is_visible: true }
|
|
}
|
|
}
|
|
|
|
/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering
|
|
#[derive(Component, Clone, Reflect, Debug)]
|
|
#[reflect(Component)]
|
|
pub struct ComputedVisibility {
|
|
pub is_visible: bool,
|
|
}
|
|
|
|
impl Default for ComputedVisibility {
|
|
fn default() -> Self {
|
|
Self { is_visible: true }
|
|
}
|
|
}
|
|
|
|
/// Use this component to opt-out of built-in frustum culling for Mesh entities
|
|
#[derive(Component)]
|
|
pub struct NoFrustumCulling;
|
|
|
|
#[derive(Clone, Component, Default, Debug, Reflect)]
|
|
#[reflect(Component)]
|
|
pub struct VisibleEntities {
|
|
#[reflect(ignore)]
|
|
pub entities: Vec<Entity>,
|
|
}
|
|
|
|
impl VisibleEntities {
|
|
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
|
|
self.entities.iter()
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.entities.len()
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.entities.is_empty()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
|
|
pub enum VisibilitySystems {
|
|
CalculateBounds,
|
|
UpdateOrthographicFrusta,
|
|
UpdatePerspectiveFrusta,
|
|
CheckVisibility,
|
|
}
|
|
|
|
pub struct VisibilityPlugin;
|
|
|
|
impl Plugin for VisibilityPlugin {
|
|
fn build(&self, app: &mut bevy_app::App) {
|
|
use VisibilitySystems::*;
|
|
|
|
app.add_system_to_stage(
|
|
CoreStage::PostUpdate,
|
|
calculate_bounds.label(CalculateBounds),
|
|
)
|
|
.add_system_to_stage(
|
|
CoreStage::PostUpdate,
|
|
update_frusta::<OrthographicProjection>
|
|
.label(UpdateOrthographicFrusta)
|
|
.after(TransformSystem::TransformPropagate),
|
|
)
|
|
.add_system_to_stage(
|
|
CoreStage::PostUpdate,
|
|
update_frusta::<PerspectiveProjection>
|
|
.label(UpdatePerspectiveFrusta)
|
|
.after(TransformSystem::TransformPropagate),
|
|
)
|
|
.add_system_to_stage(
|
|
CoreStage::PostUpdate,
|
|
check_visibility
|
|
.label(CheckVisibility)
|
|
.after(CalculateBounds)
|
|
.after(UpdateOrthographicFrusta)
|
|
.after(UpdatePerspectiveFrusta)
|
|
.after(TransformSystem::TransformPropagate),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn calculate_bounds(
|
|
mut commands: Commands,
|
|
meshes: Res<Assets<Mesh>>,
|
|
without_aabb: Query<(Entity, &Handle<Mesh>), (Without<Aabb>, Without<NoFrustumCulling>)>,
|
|
) {
|
|
for (entity, mesh_handle) in without_aabb.iter() {
|
|
if let Some(mesh) = meshes.get(mesh_handle) {
|
|
if let Some(aabb) = mesh.compute_aabb() {
|
|
commands.entity(entity).insert(aabb);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn update_frusta<T: Component + CameraProjection + Send + Sync + 'static>(
|
|
mut views: Query<(&GlobalTransform, &T, &mut Frustum)>,
|
|
) {
|
|
for (transform, projection, mut frustum) in views.iter_mut() {
|
|
let view_projection =
|
|
projection.get_projection_matrix() * transform.compute_matrix().inverse();
|
|
*frustum = Frustum::from_view_projection(
|
|
&view_projection,
|
|
&transform.translation,
|
|
&transform.back(),
|
|
projection.far(),
|
|
);
|
|
}
|
|
}
|
|
|
|
pub fn check_visibility(
|
|
mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With<Camera>>,
|
|
mut visible_entity_query: QuerySet<(
|
|
QueryState<&mut ComputedVisibility>,
|
|
QueryState<(
|
|
Entity,
|
|
&Visibility,
|
|
&mut ComputedVisibility,
|
|
Option<&RenderLayers>,
|
|
Option<&Aabb>,
|
|
Option<&NoFrustumCulling>,
|
|
Option<&GlobalTransform>,
|
|
)>,
|
|
)>,
|
|
) {
|
|
// Reset the computed visibility to false
|
|
for mut computed_visibility in visible_entity_query.q0().iter_mut() {
|
|
computed_visibility.is_visible = false;
|
|
}
|
|
|
|
for (mut visible_entities, frustum, maybe_view_mask) in view_query.iter_mut() {
|
|
visible_entities.entities.clear();
|
|
let view_mask = maybe_view_mask.copied().unwrap_or_default();
|
|
|
|
for (
|
|
entity,
|
|
visibility,
|
|
mut computed_visibility,
|
|
maybe_entity_mask,
|
|
maybe_aabb,
|
|
maybe_no_frustum_culling,
|
|
maybe_transform,
|
|
) in visible_entity_query.q1().iter_mut()
|
|
{
|
|
if !visibility.is_visible {
|
|
continue;
|
|
}
|
|
|
|
let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
|
|
if !view_mask.intersects(&entity_mask) {
|
|
continue;
|
|
}
|
|
|
|
// If we have an aabb and transform, do frustum culling
|
|
if let (Some(model_aabb), None, Some(transform)) =
|
|
(maybe_aabb, maybe_no_frustum_culling, maybe_transform)
|
|
{
|
|
let model = transform.compute_matrix();
|
|
let model_sphere = Sphere {
|
|
center: model.transform_point3a(model_aabb.center),
|
|
radius: (Vec3A::from(transform.scale) * model_aabb.half_extents).length(),
|
|
};
|
|
// Do quick sphere-based frustum culling
|
|
if !frustum.intersects_sphere(&model_sphere, false) {
|
|
continue;
|
|
}
|
|
// If we have an aabb, do aabb-based frustum culling
|
|
if !frustum.intersects_obb(model_aabb, &model, false) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
computed_visibility.is_visible = true;
|
|
visible_entities.entities.push(entity);
|
|
}
|
|
|
|
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize
|
|
// to prevent holding unneeded memory
|
|
}
|
|
}
|