From 938e73988e3b3dcc00c8f43851caca2abae877c9 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Fri, 15 Jul 2022 23:24:42 +0000 Subject: [PATCH] Visibilty Inheritance, universal ComputedVisibility and RenderLayers support (#5310) # Objective Fixes #4907. Fixes #838. Fixes #5089. Supersedes #5146. Supersedes #2087. Supersedes #865. Supersedes #5114 Visibility is currently entirely local. Set a parent entity to be invisible, and the children are still visible. This makes it hard for users to hide entire hierarchies of entities. Additionally, the semantics of `Visibility` vs `ComputedVisibility` are inconsistent across entity types. 3D meshes use `ComputedVisibility` as the "definitive" visibility component, with `Visibility` being just one data source. Sprites just use `Visibility`, which means they can't feed off of `ComputedVisibility` data, such as culling information, RenderLayers, and (added in this pr) visibility inheritance information. ## Solution Splits `ComputedVisibilty::is_visible` into `ComputedVisibilty::is_visible_in_view` and `ComputedVisibilty::is_visible_in_hierarchy`. For each visible entity, `is_visible_in_hierarchy` is computed by propagating visibility down the hierarchy. The `ComputedVisibility::is_visible()` function combines these two booleans for the canonical "is this entity visible" function. Additionally, all entities that have `Visibility` now also have `ComputedVisibility`. Sprites, Lights, and UI entities now use `ComputedVisibility` when appropriate. This means that in addition to visibility inheritance, everything using Visibility now also supports RenderLayers. Notably, Sprites (and other 2d objects) now support `RenderLayers` and work properly across multiple views. Also note that this does increase the amount of work done per sprite. Bevymark with 100,000 sprites on `main` runs in `0.017612` seconds and this runs in `0.01902`. That is certainly a gap, but I believe the api consistency and extra functionality this buys us is worth it. See [this thread](https://github.com/bevyengine/bevy/pull/5146#issuecomment-1182783452) for more info. Note that #5146 in combination with #5114 _are_ a viable alternative to this PR and _would_ perform better, but that comes at the cost of api inconsistencies and doing visibility calculations in the "wrong" place. The current visibility system does have potential for performance improvements. I would prefer to evolve that one system as a whole rather than doing custom hacks / different behaviors for each feature slice. Here is a "split screen" example where the left camera uses RenderLayers to filter out the blue sprite. ![image](https://user-images.githubusercontent.com/2694663/178814868-2e9a2173-bf8c-4c79-8815-633899d492c3.png) Note that this builds directly on #5146 and that @james7132 deserves the credit for the baseline visibility inheritance work. This pr moves the inherited visibility field into `ComputedVisibility`, then does the additional work of porting everything to `ComputedVisibility`. See my [comments here](https://github.com/bevyengine/bevy/pull/5146#issuecomment-1182783452) for rationale. ## Follow up work * Now that lights use ComputedVisibility, VisibleEntities now includes "visible lights" in the entity list. Functionally not a problem as we use queries to filter the list down in the desired context. But we should consider splitting this out into a separate`VisibleLights` collection for both clarity and performance reasons. And _maybe_ even consider scoping `VisibleEntities` down to `VisibleMeshes`?. * Investigate alternative sprite rendering impls (in combination with visibility system tweaks) that avoid re-generating a per-view fixedbitset of visible entities every frame, then checking each ExtractedEntity. This is where most of the performance overhead lives. Ex: we could generate ExtractedEntities per-view using the VisibleEntities list, avoiding the need for the bitset. * Should ComputedVisibility use bitflags under the hood? This would cut down on the size of the component, potentially speed up the `is_visible()` function, and allow us to cheaply expand ComputedVisibility with more data (ex: split out local visibility and parent visibility, add more culling classes, etc). --- ## Changelog * ComputedVisibility now takes hierarchy visibility into account. * 2D, UI and Light entities now use the ComputedVisibility component. ## Migration Guide If you were previously reading `Visibility::is_visible` as the "actual visibility" for sprites or lights, use `ComputedVisibilty::is_visible()` instead: ```rust // before (0.7) fn system(query: Query<&Visibility>) { for visibility in query.iter() { if visibility.is_visible { log!("found visible entity"); } } } // after (0.8) fn system(query: Query<&ComputedVisibility>) { for visibility in query.iter() { if visibility.is_visible() { log!("found visible entity"); } } } ``` Co-authored-by: Carter Anderson --- crates/bevy_pbr/src/bundle.rs | 6 + crates/bevy_pbr/src/lib.rs | 3 + crates/bevy_pbr/src/light.rs | 60 ++-- crates/bevy_pbr/src/render/light.rs | 37 +- crates/bevy_pbr/src/render/mesh.rs | 4 +- crates/bevy_render/Cargo.toml | 1 + crates/bevy_render/src/extract_component.rs | 2 +- crates/bevy_render/src/view/visibility/mod.rs | 339 ++++++++++++++++-- crates/bevy_sprite/Cargo.toml | 1 + crates/bevy_sprite/src/bundle.rs | 7 +- crates/bevy_sprite/src/mesh2d/mesh.rs | 2 +- crates/bevy_sprite/src/render/mod.rs | 72 ++-- crates/bevy_text/src/text2d.rs | 23 +- crates/bevy_ui/src/entity.rs | 15 +- crates/bevy_ui/src/render/mod.rs | 10 +- examples/2d/mesh2d_manual.rs | 2 +- examples/stress_tests/many_cubes.rs | 2 +- 17 files changed, 472 insertions(+), 114 deletions(-) diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs index 2f3a9c63c912e..0bd720d11ea06 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -73,6 +73,8 @@ pub struct PointLightBundle { pub global_transform: GlobalTransform, /// Enables or disables the light pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } /// A component bundle for spot light entities @@ -85,6 +87,8 @@ pub struct SpotLightBundle { pub global_transform: GlobalTransform, /// Enables or disables the light pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } /// A component bundle for [`DirectionalLight`] entities. @@ -97,4 +101,6 @@ pub struct DirectionalLightBundle { pub global_transform: GlobalTransform, /// Enables or disables the light pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index a73ffcc9a4d30..5d190540579ea 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -150,6 +150,7 @@ impl Plugin for PbrPlugin { assign_lights_to_clusters .label(SimulationLightSystems::AssignLightsToClusters) .after(TransformSystem::TransformPropagate) + .after(VisibilitySystems::CheckVisibility) .after(CameraUpdateSystem) .after(ModifiesWindows), ) @@ -157,6 +158,8 @@ impl Plugin for PbrPlugin { CoreStage::PostUpdate, update_directional_light_frusta .label(SimulationLightSystems::UpdateLightFrusta) + // This must run after CheckVisibility because it relies on ComputedVisibility::is_visible() + .after(VisibilitySystems::CheckVisibility) .after(TransformSystem::TransformPropagate), ) .add_system_to_stage( diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 9bc9bb5b08e04..ff505467a0e3e 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -10,7 +10,7 @@ use bevy_render::{ primitives::{Aabb, CubemapFrusta, Frustum, Plane, Sphere}, render_resource::BufferBindingType, renderer::RenderDevice, - view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities}, + view::{ComputedVisibility, RenderLayers, VisibleEntities}, }; use bevy_transform::components::GlobalTransform; use bevy_utils::tracing::warn; @@ -793,8 +793,8 @@ pub(crate) fn assign_lights_to_clusters( &mut Clusters, Option<&mut VisiblePointLights>, )>, - point_lights_query: Query<(Entity, &GlobalTransform, &PointLight, &Visibility)>, - spot_lights_query: Query<(Entity, &GlobalTransform, &SpotLight, &Visibility)>, + point_lights_query: Query<(Entity, &GlobalTransform, &PointLight, &ComputedVisibility)>, + spot_lights_query: Query<(Entity, &GlobalTransform, &SpotLight, &ComputedVisibility)>, mut lights: Local>, mut cluster_aabb_spheres: Local>>, mut max_point_lights_warning_emitted: Local, @@ -811,7 +811,7 @@ pub(crate) fn assign_lights_to_clusters( lights.extend( point_lights_query .iter() - .filter(|(.., visibility)| visibility.is_visible) + .filter(|(.., visibility)| visibility.is_visible()) .map( |(entity, transform, point_light, _visibility)| PointLightAssignmentData { entity, @@ -826,7 +826,7 @@ pub(crate) fn assign_lights_to_clusters( lights.extend( spot_lights_query .iter() - .filter(|(.., visibility)| visibility.is_visible) + .filter(|(.., visibility)| visibility.is_visible()) .map( |(entity, transform, spot_light, _visibility)| PointLightAssignmentData { entity, @@ -1415,7 +1415,7 @@ pub fn update_directional_light_frusta( &GlobalTransform, &DirectionalLight, &mut Frustum, - &Visibility, + &ComputedVisibility, ), Or<(Changed, Changed)>, >, @@ -1424,7 +1424,7 @@ pub fn update_directional_light_frusta( // The frustum is used for culling meshes to the light for shadow mapping // so if shadow mapping is disabled for this light, then the frustum is // not needed. - if !directional_light.shadows_enabled || !visibility.is_visible { + if !directional_light.shadows_enabled || !visibility.is_visible() { continue; } @@ -1541,45 +1541,43 @@ pub fn check_light_mesh_visibility( &Frustum, &mut VisibleEntities, Option<&RenderLayers>, - &Visibility, + &ComputedVisibility, ), Without, >, mut visible_entity_query: Query< ( Entity, - &Visibility, &mut ComputedVisibility, Option<&RenderLayers>, Option<&Aabb>, Option<&GlobalTransform>, ), - Without, + (Without, Without), >, ) { - // Directonal lights - for (directional_light, frustum, mut visible_entities, maybe_view_mask, visibility) in - &mut directional_lights + // Directional lights + for ( + directional_light, + frustum, + mut visible_entities, + maybe_view_mask, + light_computed_visibility, + ) in &mut directional_lights { visible_entities.entities.clear(); // NOTE: If shadow mapping is disabled for the light then it must have no visible entities - if !directional_light.shadows_enabled || !visibility.is_visible { + if !directional_light.shadows_enabled || !light_computed_visibility.is_visible() { continue; } let view_mask = maybe_view_mask.copied().unwrap_or_default(); - for ( - entity, - visibility, - mut computed_visibility, - maybe_entity_mask, - maybe_aabb, - maybe_transform, - ) in &mut visible_entity_query + for (entity, mut computed_visibility, maybe_entity_mask, maybe_aabb, maybe_transform) in + &mut visible_entity_query { - if !visibility.is_visible { + if !computed_visibility.is_visible_in_hierarchy() { continue; } @@ -1595,7 +1593,7 @@ pub fn check_light_mesh_visibility( } } - computed_visibility.is_visible = true; + computed_visibility.set_visible_in_view(); visible_entities.entities.push(entity); } @@ -1631,14 +1629,13 @@ pub fn check_light_mesh_visibility( for ( entity, - visibility, mut computed_visibility, maybe_entity_mask, maybe_aabb, maybe_transform, ) in &mut visible_entity_query { - if !visibility.is_visible { + if !computed_visibility.is_visible_in_hierarchy() { continue; } @@ -1660,12 +1657,12 @@ pub fn check_light_mesh_visibility( .zip(cubemap_visible_entities.iter_mut()) { if frustum.intersects_obb(aabb, &model_to_world, true) { - computed_visibility.is_visible = true; + computed_visibility.set_visible_in_view(); visible_entities.entities.push(entity); } } } else { - computed_visibility.is_visible = true; + computed_visibility.set_visible_in_view(); for visible_entities in cubemap_visible_entities.iter_mut() { visible_entities.entities.push(entity); } @@ -1695,14 +1692,13 @@ pub fn check_light_mesh_visibility( for ( entity, - visibility, mut computed_visibility, maybe_entity_mask, maybe_aabb, maybe_transform, ) in visible_entity_query.iter_mut() { - if !visibility.is_visible { + if !computed_visibility.is_visible_in_hierarchy() { continue; } @@ -1720,11 +1716,11 @@ pub fn check_light_mesh_visibility( } if frustum.intersects_obb(aabb, &model_to_world, true) { - computed_visibility.is_visible = true; + computed_visibility.set_visible_in_view(); visible_entities.entities.push(entity); } } else { - computed_visibility.is_visible = true; + computed_visibility.set_visible_in_view(); visible_entities.entities.push(entity); } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 27295455f0d4c..8a9c037b1bdc6 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -26,7 +26,8 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice, RenderQueue}, texture::*, view::{ - ExtractedView, ViewUniform, ViewUniformOffset, ViewUniforms, Visibility, VisibleEntities, + ComputedVisibility, ExtractedView, ViewUniform, ViewUniformOffset, ViewUniforms, + VisibleEntities, }, Extract, }; @@ -411,8 +412,22 @@ pub fn extract_lights( point_light_shadow_map: Extract>, directional_light_shadow_map: Extract>, global_point_lights: Extract>, - point_lights: Extract>, - spot_lights: Extract>, + point_lights: Extract< + Query<( + &PointLight, + &CubemapVisibleEntities, + &GlobalTransform, + &ComputedVisibility, + )>, + >, + spot_lights: Extract< + Query<( + &SpotLight, + &VisibleEntities, + &GlobalTransform, + &ComputedVisibility, + )>, + >, directional_lights: Extract< Query< ( @@ -420,7 +435,7 @@ pub fn extract_lights( &DirectionalLight, &VisibleEntities, &GlobalTransform, - &Visibility, + &ComputedVisibility, ), Without, >, @@ -447,7 +462,12 @@ pub fn extract_lights( let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); for entity in global_point_lights.iter().copied() { - if let Ok((point_light, cubemap_visible_entities, transform)) = point_lights.get(entity) { + if let Ok((point_light, cubemap_visible_entities, transform, visibility)) = + point_lights.get(entity) + { + if !visibility.is_visible() { + continue; + } // TODO: This is very much not ideal. We should be able to re-use the vector memory. // However, since exclusive access to the main world in extract is ill-advised, we just clone here. let render_cubemap_visible_entities = cubemap_visible_entities.clone(); @@ -481,7 +501,10 @@ pub fn extract_lights( let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); for entity in global_point_lights.iter().copied() { - if let Ok((spot_light, visible_entities, transform)) = spot_lights.get(entity) { + if let Ok((spot_light, visible_entities, transform, visibility)) = spot_lights.get(entity) { + if !visibility.is_visible() { + continue; + } // TODO: This is very much not ideal. We should be able to re-use the vector memory. // However, since exclusive access to the main world in extract is ill-advised, we just clone here. let render_visible_entities = visible_entities.clone(); @@ -522,7 +545,7 @@ pub fn extract_lights( for (entity, directional_light, visible_entities, transform, visibility) in directional_lights.iter() { - if !visibility.is_visible { + if !visibility.is_visible() { continue; } diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 3bf3fb2a0a501..0e33b5caaabd7 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -139,7 +139,7 @@ pub fn extract_meshes( ) { let mut caster_commands = Vec::with_capacity(*prev_caster_commands_len); let mut not_caster_commands = Vec::with_capacity(*prev_not_caster_commands_len); - let visible_meshes = meshes_query.iter().filter(|(_, vis, ..)| vis.is_visible); + let visible_meshes = meshes_query.iter().filter(|(_, vis, ..)| vis.is_visible()); for (entity, _, transform, handle, not_receiver, not_caster) in visible_meshes { let transform = transform.compute_matrix(); @@ -224,7 +224,7 @@ pub fn extract_skinned_meshes( let mut last_start = 0; for (entity, computed_visibility, skin) in query.iter() { - if !computed_visibility.is_visible { + if !computed_visibility.is_visible() { continue; } // PERF: This can be expensive, can we move this to prepare? diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index a773d01affac5..9c30218200b9b 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -34,6 +34,7 @@ bevy_core = { path = "../bevy_core", version = "0.8.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.8.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.8.0-dev" } bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.8.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.8.0-dev" } bevy_log = { path = "../bevy_log", version = "0.8.0-dev" } bevy_math = { path = "../bevy_math", version = "0.8.0-dev" } bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.8.0-dev" } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index ffedd60c32167..25a9384aa7c42 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -200,7 +200,7 @@ fn extract_visible_components( ) { let mut values = Vec::with_capacity(*previous_len); for (entity, computed_visibility, query_item) in query.iter_mut() { - if computed_visibility.is_visible { + if computed_visibility.is_visible() { values.push((entity, (C::extract_component(query_item),))); } } diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 91a733135d3bd..771151630fbae 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -6,6 +6,7 @@ pub use render_layers::*; use bevy_app::{CoreStage, Plugin}; use bevy_asset::{Assets, Handle}; use bevy_ecs::prelude::*; +use bevy_hierarchy::{Children, Parent}; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_transform::components::GlobalTransform; @@ -19,10 +20,16 @@ use crate::{ primitives::{Aabb, Frustum, Sphere}, }; -/// User indication of whether an entity is visible +/// User indication of whether an entity is visible. Propagates down the entity hierarchy. + +/// If an entity is hidden in this way, all [`Children`] (and all of their children and so on) will also be hidden. +/// This is done by setting the values of their [`ComputedVisibility`] component. #[derive(Component, Clone, Reflect, Debug)] #[reflect(Component, Default)] pub struct Visibility { + /// Indicates whether this entity is visible. Hidden values will propagate down the entity hierarchy. + /// If this entity is hidden, all of its descendants will be hidden as well. See [`Children`] and [`Parent`] for + /// hierarchy info. pub is_visible: bool, } @@ -33,15 +40,51 @@ impl Default for Visibility { } /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering -#[derive(Component, Clone, Reflect, Debug)] +#[derive(Component, Clone, Reflect, Debug, Eq, PartialEq, Default)] #[reflect(Component)] pub struct ComputedVisibility { - pub is_visible: bool, + is_visible_in_hierarchy: bool, + is_visible_in_view: bool, } -impl Default for ComputedVisibility { - fn default() -> Self { - Self { is_visible: true } +impl ComputedVisibility { + /// Whether this entity is visible to something this frame. This is true if and only if [`Self::is_visible_in_hierarchy`] and [`Self::is_visible_in_view`] + /// are true. This is the canonical method to call to determine if an entity should be drawn. + /// This value is updated in [`CoreStage::PostUpdate`] during the [`VisibilitySystems::CheckVisibility`] system label. Reading it from the + /// [`CoreStage::Update`] stage will yield the value from the previous frame. + #[inline] + pub fn is_visible(&self) -> bool { + self.is_visible_in_hierarchy && self.is_visible_in_view + } + + /// Whether this entity is visible in the entity hierarchy, which is determined by the [`Visibility`] component. + /// This takes into account "visibility inheritance". If any of this entity's ancestors (see [`Parent`]) are hidden, this entity + /// will be hidden as well. This value is updated in the [`CoreStage::PostUpdate`] stage in the + /// [`VisibilitySystems::VisibilityPropagate`] system label. + #[inline] + pub fn is_visible_in_hierarchy(&self) -> bool { + self.is_visible_in_hierarchy + } + + /// Whether this entity is visible in _any_ view (Cameras, Lights, etc). Each entity type (and view type) should choose how to set this + /// value. For cameras and drawn entities, this will take into account [`RenderLayers`]. + /// + /// This value is reset to `false` every frame in [`VisibilitySystems::VisibilityPropagate`] during [`CoreStage::PostUpdate`]. + /// Each entity type then chooses how to set this field in the [`CoreStage::PostUpdate`] stage in the + /// [`VisibilitySystems::CheckVisibility`] system label. Meshes might use frustum culling to decide if they are visible in a view. + /// Other entities might just set this to `true` every frame. + #[inline] + pub fn is_visible_in_view(&self) -> bool { + self.is_visible_in_view + } + + /// Sets `is_visible_in_view` to `true`. This is not reversible for a given frame, as it encodes whether or not this is visible in + /// _any_ view. This will be automatically reset to `false` every frame in [`VisibilitySystems::VisibilityPropagate`] and then set + /// to the proper value in [`VisibilitySystems::CheckVisibility`]. This should _only_ be set in systems with the [`VisibilitySystems::CheckVisibility`] + /// label. Don't call this unless you are defining a custom visibility system. For normal user-defined entity visibility, see [`Visibility`]. + #[inline] + pub fn set_visible_in_view(&mut self) { + self.is_visible_in_view = true; } } @@ -88,6 +131,7 @@ pub enum VisibilitySystems { UpdateOrthographicFrusta, UpdatePerspectiveFrusta, UpdateProjectionFrusta, + VisibilityPropagate, /// Label for the [`check_visibility()`] system updating each frame the [`ComputedVisibility`] /// of each entity and the [`VisibleEntities`] of each view. CheckVisibility, @@ -121,6 +165,10 @@ impl Plugin for VisibilityPlugin { .label(UpdateProjectionFrusta) .after(TransformSystem::TransformPropagate), ) + .add_system_to_stage( + CoreStage::PostUpdate, + visibility_propagate_system.label(VisibilityPropagate), + ) .add_system_to_stage( CoreStage::PostUpdate, check_visibility @@ -129,6 +177,7 @@ impl Plugin for VisibilityPlugin { .after(UpdateOrthographicFrusta) .after(UpdatePerspectiveFrusta) .after(UpdateProjectionFrusta) + .after(VisibilityPropagate) .after(TransformSystem::TransformPropagate), ); } @@ -163,6 +212,68 @@ pub fn update_frusta( } } +fn visibility_propagate_system( + mut root_query: Query< + ( + Option<&Children>, + &Visibility, + &mut ComputedVisibility, + Entity, + ), + Without, + >, + mut visibility_query: Query<(&Visibility, &mut ComputedVisibility, &Parent)>, + children_query: Query<&Children, (With, With, With)>, +) { + for (children, visibility, mut computed_visibility, entity) in root_query.iter_mut() { + computed_visibility.is_visible_in_hierarchy = visibility.is_visible; + // reset "view" visibility here ... if this entity should be drawn a future system should set this to true + computed_visibility.is_visible_in_view = false; + if let Some(children) = children { + for child in children.iter() { + let _ = propagate_recursive( + computed_visibility.is_visible_in_hierarchy, + &mut visibility_query, + &children_query, + *child, + entity, + ); + } + } + } +} + +fn propagate_recursive( + parent_visible: bool, + visibility_query: &mut Query<(&Visibility, &mut ComputedVisibility, &Parent)>, + children_query: &Query<&Children, (With, With, With)>, + entity: Entity, + expected_parent: Entity, + // BLOCKED: https://github.com/rust-lang/rust/issues/31436 + // We use a result here to use the `?` operator. Ideally we'd use a try block instead +) -> Result<(), ()> { + let is_visible = { + let (visibility, mut computed_visibility, child_parent) = + visibility_query.get_mut(entity).map_err(drop)?; + assert_eq!( + child_parent.get(), expected_parent, + "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" + ); + computed_visibility.is_visible_in_hierarchy = visibility.is_visible && parent_visible; + // reset "view" visibility here ... if this entity should be drawn a future system should set this to true + computed_visibility.is_visible_in_view = false; + computed_visibility.is_visible_in_hierarchy + }; + + for child in children_query.get(entity).map_err(drop)?.iter() { + let _ = propagate_recursive(is_visible, visibility_query, children_query, *child, entity); + } + Ok(()) +} + +// the batch size used for check_visibility, chosen because this number tends to perform well +const VISIBLE_ENTITIES_QUERY_BATCH_SIZE: usize = 1024; + /// System updating the visibility of entities each frame. /// /// The system is labelled with [`VisibilitySystems::CheckVisibility`]. Each frame, it updates the @@ -171,39 +282,35 @@ pub fn update_frusta( pub fn check_visibility( mut thread_queues: Local>>>, mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With>, - mut visible_entity_query: ParamSet<( - Query<&mut ComputedVisibility>, - Query<( - Entity, - &Visibility, - &mut ComputedVisibility, - Option<&RenderLayers>, - Option<&Aabb>, - Option<&NoFrustumCulling>, - Option<&GlobalTransform>, - )>, + mut visible_aabb_query: Query<( + Entity, + &mut ComputedVisibility, + Option<&RenderLayers>, + &Aabb, + &GlobalTransform, + Option<&NoFrustumCulling>, )>, + mut visible_no_aabb_query: Query< + (Entity, &mut ComputedVisibility, Option<&RenderLayers>), + Without, + >, ) { - // Reset the computed visibility to false - for mut computed_visibility in visible_entity_query.p0().iter_mut() { - computed_visibility.is_visible = false; - } - for (mut visible_entities, frustum, maybe_view_mask) in &mut view_query { let view_mask = maybe_view_mask.copied().unwrap_or_default(); visible_entities.entities.clear(); - visible_entity_query.p1().par_for_each_mut( - 1024, + visible_aabb_query.par_for_each_mut( + VISIBLE_ENTITIES_QUERY_BATCH_SIZE, |( entity, - visibility, mut computed_visibility, maybe_entity_mask, - maybe_aabb, + model_aabb, + transform, maybe_no_frustum_culling, - maybe_transform, )| { - if !visibility.is_visible { + // skip computing visibility for entities that are configured to be hidden. is_visible_in_view has already been set to false + // in visibility_propagate_system + if !computed_visibility.is_visible_in_hierarchy() { return; } @@ -213,9 +320,7 @@ pub fn check_visibility( } // 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) - { + if maybe_no_frustum_culling.is_none() { let model = transform.compute_matrix(); let model_sphere = Sphere { center: model.transform_point3a(model_aabb.center), @@ -231,7 +336,29 @@ pub fn check_visibility( } } - computed_visibility.is_visible = true; + computed_visibility.is_visible_in_view = true; + let cell = thread_queues.get_or_default(); + let mut queue = cell.take(); + queue.push(entity); + cell.set(queue); + }, + ); + + visible_no_aabb_query.par_for_each_mut( + VISIBLE_ENTITIES_QUERY_BATCH_SIZE, + |(entity, mut computed_visibility, maybe_entity_mask)| { + // skip computing visibility for entities that are configured to be hidden. is_visible_in_view has already been set to false + // in visibility_propagate_system + if !computed_visibility.is_visible_in_hierarchy() { + return; + } + + let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); + if !view_mask.intersects(&entity_mask) { + return; + } + + computed_visibility.is_visible_in_view = true; let cell = thread_queues.get_or_default(); let mut queue = cell.take(); queue.push(entity); @@ -244,3 +371,151 @@ pub fn check_visibility( } } } + +#[cfg(test)] +mod test { + use bevy_app::prelude::*; + use bevy_ecs::prelude::*; + + use super::*; + + use bevy_hierarchy::BuildWorldChildren; + + #[test] + fn visibility_propagation() { + let mut app = App::new(); + app.add_system(visibility_propagate_system); + + let root1 = app + .world + .spawn() + .insert_bundle(( + Visibility { is_visible: false }, + ComputedVisibility::default(), + )) + .id(); + let root1_child1 = app + .world + .spawn() + .insert_bundle((Visibility::default(), ComputedVisibility::default())) + .id(); + let root1_child2 = app + .world + .spawn() + .insert_bundle(( + Visibility { is_visible: false }, + ComputedVisibility::default(), + )) + .id(); + let root1_child1_grandchild1 = app + .world + .spawn() + .insert_bundle((Visibility::default(), ComputedVisibility::default())) + .id(); + let root1_child2_grandchild1 = app + .world + .spawn() + .insert_bundle((Visibility::default(), ComputedVisibility::default())) + .id(); + + app.world + .entity_mut(root1) + .push_children(&[root1_child1, root1_child2]); + app.world + .entity_mut(root1_child1) + .push_children(&[root1_child1_grandchild1]); + app.world + .entity_mut(root1_child2) + .push_children(&[root1_child2_grandchild1]); + + let root2 = app + .world + .spawn() + .insert_bundle((Visibility::default(), ComputedVisibility::default())) + .id(); + let root2_child1 = app + .world + .spawn() + .insert_bundle((Visibility::default(), ComputedVisibility::default())) + .id(); + let root2_child2 = app + .world + .spawn() + .insert_bundle(( + Visibility { is_visible: false }, + ComputedVisibility::default(), + )) + .id(); + let root2_child1_grandchild1 = app + .world + .spawn() + .insert_bundle((Visibility::default(), ComputedVisibility::default())) + .id(); + let root2_child2_grandchild1 = app + .world + .spawn() + .insert_bundle((Visibility::default(), ComputedVisibility::default())) + .id(); + + app.world + .entity_mut(root2) + .push_children(&[root2_child1, root2_child2]); + app.world + .entity_mut(root2_child1) + .push_children(&[root2_child1_grandchild1]); + app.world + .entity_mut(root2_child2) + .push_children(&[root2_child2_grandchild1]); + + app.update(); + + let is_visible = |e: Entity| { + app.world + .entity(e) + .get::() + .unwrap() + .is_visible_in_hierarchy + }; + assert!( + !is_visible(root1), + "invisibility propagates down tree from root" + ); + assert!( + !is_visible(root1_child1), + "invisibility propagates down tree from root" + ); + assert!( + !is_visible(root1_child2), + "invisibility propagates down tree from root" + ); + assert!( + !is_visible(root1_child1_grandchild1), + "invisibility propagates down tree from root" + ); + assert!( + !is_visible(root1_child2_grandchild1), + "invisibility propagates down tree from root" + ); + + assert!( + is_visible(root2), + "visibility propagates down tree from root" + ); + assert!( + is_visible(root2_child1), + "visibility propagates down tree from root" + ); + assert!( + !is_visible(root2_child2), + "visibility propagates down tree from root, but local invisibility is preserved" + ); + assert!( + is_visible(root2_child1_grandchild1), + "visibility propagates down tree from root" + ); + assert!( + !is_visible(root2_child2_grandchild1), + "child's invisibility propagates down to grandchild" + ); + } +} diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 238fe048b2f37..c70f0b980f434 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -25,6 +25,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.8.0-dev" } # other bytemuck = { version = "1.5", features = ["derive"] } +fixedbitset = "0.4" guillotiere = "0.6.0" thiserror = "1.0" rectangle-pack = "0.4" diff --git a/crates/bevy_sprite/src/bundle.rs b/crates/bevy_sprite/src/bundle.rs index 82fdcd9db6292..d97e1857ded21 100644 --- a/crates/bevy_sprite/src/bundle.rs +++ b/crates/bevy_sprite/src/bundle.rs @@ -6,7 +6,7 @@ use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; use bevy_render::{ texture::{Image, DEFAULT_IMAGE_HANDLE}, - view::Visibility, + view::{ComputedVisibility, Visibility}, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -18,6 +18,8 @@ pub struct SpriteBundle { pub texture: Handle, /// User indication of whether an entity is visible pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } impl Default for SpriteBundle { @@ -28,6 +30,7 @@ impl Default for SpriteBundle { global_transform: Default::default(), texture: DEFAULT_IMAGE_HANDLE.typed(), visibility: Default::default(), + computed_visibility: Default::default(), } } } @@ -44,4 +47,6 @@ pub struct SpriteSheetBundle { pub global_transform: GlobalTransform, /// User indication of whether an entity is visible pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 0c858e17eaca1..41188cd48679f 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -128,7 +128,7 @@ pub fn extract_mesh2d( ) { let mut values = Vec::with_capacity(*previous_len); for (entity, computed_visibility, transform, handle) in query.iter() { - if !computed_visibility.is_visible { + if !computed_visibility.is_visible() { continue; } let transform = transform.compute_matrix(); diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 6d739dd029560..bd95a06d8e637 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -22,7 +22,9 @@ use bevy_render::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, Image}, - view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, Visibility}, + view::{ + ComputedVisibility, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities, + }, Extract, }; use bevy_transform::components::GlobalTransform; @@ -30,6 +32,7 @@ use bevy_utils::FloatOrd; use bevy_utils::HashMap; use bytemuck::{Pod, Zeroable}; use copyless::VecHelper; +use fixedbitset::FixedBitSet; pub struct SpritePipeline { view_layout: BindGroupLayout, @@ -172,6 +175,7 @@ impl SpecializedRenderPipeline for SpritePipeline { #[derive(Component, Clone, Copy)] pub struct ExtractedSprite { + pub entity: Entity, pub transform: GlobalTransform, pub color: Color, /// Select an area of the texture @@ -222,10 +226,19 @@ pub fn extract_sprite_events( pub fn extract_sprites( mut extracted_sprites: ResMut, texture_atlases: Extract>>, - sprite_query: Extract)>>, + sprite_query: Extract< + Query<( + Entity, + &ComputedVisibility, + &Sprite, + &GlobalTransform, + &Handle, + )>, + >, atlas_query: Extract< Query<( - &Visibility, + Entity, + &ComputedVisibility, &TextureAtlasSprite, &GlobalTransform, &Handle, @@ -233,12 +246,13 @@ pub fn extract_sprites( >, ) { extracted_sprites.sprites.clear(); - for (visibility, sprite, transform, handle) in sprite_query.iter() { - if !visibility.is_visible { + for (entity, visibility, sprite, transform, handle) in sprite_query.iter() { + if !visibility.is_visible() { continue; } // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive extracted_sprites.sprites.alloc().init(ExtractedSprite { + entity, color: sprite.color, transform: *transform, // Use the full texture @@ -251,13 +265,14 @@ pub fn extract_sprites( anchor: sprite.anchor.as_vec(), }); } - for (visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { - if !visibility.is_visible { + for (entity, visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { + if !visibility.is_visible() { continue; } if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { let rect = Some(texture_atlas.textures[atlas_sprite.index as usize]); extracted_sprites.sprites.alloc().init(ExtractedSprite { + entity, color: atlas_sprite.color, transform: *transform, // Select the area in the texture atlas @@ -334,6 +349,7 @@ pub struct ImageBindGroups { #[allow(clippy::too_many_arguments)] pub fn queue_sprites( mut commands: Commands, + mut view_entities: Local, draw_functions: Res>, render_device: Res, render_queue: Res, @@ -346,7 +362,7 @@ pub fn queue_sprites( gpu_images: Res>, msaa: Res, mut extracted_sprites: ResMut, - mut views: Query<&mut RenderPhase>, + mut views: Query<(&VisibleEntities, &mut RenderPhase)>, events: Res, ) { // If an image has changed, the GpuImage has (probably) changed @@ -388,26 +404,27 @@ pub fn queue_sprites( let mut index = 0; let mut colored_index = 0; - // FIXME: VisibleEntities is ignored - for mut transparent_phase in &mut views { - let extracted_sprites = &mut extracted_sprites.sprites; - let image_bind_groups = &mut *image_bind_groups; + let extracted_sprites = &mut extracted_sprites.sprites; + // Sort sprites by z for correct transparency and then by handle to improve batching + // NOTE: This can be done independent of views by reasonably assuming that all 2D views look along the negative-z axis in world space + extracted_sprites.sort_unstable_by(|a, b| { + match a + .transform + .translation + .z + .partial_cmp(&b.transform.translation.z) + { + Some(Ordering::Equal) | None => a.image_handle_id.cmp(&b.image_handle_id), + Some(other) => other, + } + }); + let image_bind_groups = &mut *image_bind_groups; + for (visible_entities, mut transparent_phase) in &mut views { + view_entities.clear(); + view_entities.extend(visible_entities.entities.iter().map(|e| e.id() as usize)); transparent_phase.items.reserve(extracted_sprites.len()); - // Sort sprites by z for correct transparency and then by handle to improve batching - extracted_sprites.sort_unstable_by(|a, b| { - match a - .transform - .translation - .z - .partial_cmp(&b.transform.translation.z) - { - Some(Ordering::Equal) | None => a.image_handle_id.cmp(&b.image_handle_id), - Some(other) => other, - } - }); - // Impossible starting values that will be replaced on the first iteration let mut current_batch = SpriteBatch { image_handle_id: HandleId::Id(Uuid::nil(), u64::MAX), @@ -420,7 +437,10 @@ pub fn queue_sprites( // Compatible items share the same entity. // Batches are merged later (in `batch_phase_system()`), so that they can be interrupted // by any other phase item (and they can interrupt other items from batching). - for extracted_sprite in extracted_sprites { + for extracted_sprite in extracted_sprites.iter() { + if !view_entities.contains(extracted_sprite.entity.id() as usize) { + continue; + } let new_batch = SpriteBatch { image_handle_id: extracted_sprite.image_handle_id, colored: extracted_sprite.color != Color::WHITE, diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 3dfb0a041b3f9..9209fd8d32a3e 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -10,7 +10,11 @@ use bevy_ecs::{ }; use bevy_math::{Vec2, Vec3}; use bevy_reflect::Reflect; -use bevy_render::{texture::Image, view::Visibility, Extract}; +use bevy_render::{ + texture::Image, + view::{ComputedVisibility, Visibility}, + Extract, +}; use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlas}; use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; @@ -58,6 +62,7 @@ pub struct Text2dBundle { pub text_2d_size: Text2dSize, pub text_2d_bounds: Text2dBounds, pub visibility: Visibility, + pub computed_visibility: ComputedVisibility, } pub fn extract_text2d_sprite( @@ -65,11 +70,20 @@ pub fn extract_text2d_sprite( texture_atlases: Extract>>, text_pipeline: Extract>, windows: Extract>, - text2d_query: Extract>, + text2d_query: Extract< + Query<( + Entity, + &ComputedVisibility, + &Text, + &GlobalTransform, + &Text2dSize, + )>, + >, ) { let scale_factor = windows.scale_factor(WindowId::primary()) as f32; - for (entity, visibility, text, transform, calculated_size) in text2d_query.iter() { - if !visibility.is_visible { + + for (entity, computed_visibility, text, transform, calculated_size) in text2d_query.iter() { + if !computed_visibility.is_visible() { continue; } let (width, height) = (calculated_size.size.x, calculated_size.size.y); @@ -108,6 +122,7 @@ pub fn extract_text2d_sprite( let transform = text_transform.mul_transform(glyph_transform); extracted_sprites.sprites.push(ExtractedSprite { + entity, transform, color, rect, diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index a78d4a64fb656..5345ee3dd6726 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.rs @@ -9,7 +9,10 @@ use bevy_ecs::{ prelude::{Component, With}, query::QueryItem, }; -use bevy_render::{camera::Camera, extract_component::ExtractComponent, view::Visibility}; +use bevy_render::{ + camera::Camera, extract_component::ExtractComponent, prelude::ComputedVisibility, + view::Visibility, +}; use bevy_text::Text; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -32,6 +35,8 @@ pub struct NodeBundle { pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } /// A UI node that is an image @@ -57,6 +62,8 @@ pub struct ImageBundle { pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } /// A UI node that is text @@ -78,6 +85,8 @@ pub struct TextBundle { pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } impl Default for TextBundle { @@ -91,6 +100,7 @@ impl Default for TextBundle { transform: Default::default(), global_transform: Default::default(), visibility: Default::default(), + computed_visibility: Default::default(), } } } @@ -118,6 +128,8 @@ pub struct ButtonBundle { pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } impl Default for ButtonBundle { @@ -133,6 +145,7 @@ impl Default for ButtonBundle { transform: Default::default(), global_transform: Default::default(), visibility: Default::default(), + computed_visibility: Default::default(), } } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 93abfb14b38bb..08b25a42f08ba 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -20,7 +20,7 @@ use bevy_render::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::Image, - view::{ExtractedView, ViewUniforms, Visibility}, + view::{ComputedVisibility, ExtractedView, ViewUniforms}, Extract, RenderApp, RenderStage, }; use bevy_sprite::{Rect, SpriteAssetEvents, TextureAtlas}; @@ -182,14 +182,14 @@ pub fn extract_uinodes( &GlobalTransform, &UiColor, &UiImage, - &Visibility, + &ComputedVisibility, Option<&CalculatedClip>, )>, >, ) { extracted_uinodes.uinodes.clear(); for (uinode, transform, color, image, visibility, clip) in uinode_query.iter() { - if !visibility.is_visible { + if !visibility.is_visible() { continue; } let image = image.0.clone_weak(); @@ -277,14 +277,14 @@ pub fn extract_text_uinodes( &Node, &GlobalTransform, &Text, - &Visibility, + &ComputedVisibility, Option<&CalculatedClip>, )>, >, ) { let scale_factor = windows.scale_factor(WindowId::primary()) as f32; for (entity, uinode, transform, text, visibility, clip) in uinode_query.iter() { - if !visibility.is_visible { + if !visibility.is_visible() { continue; } // Skip if size is set to zero (e.g. when a parent is set to `Display::None`) diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index e1828d92e6fb0..d5c245f7bd728 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -292,7 +292,7 @@ pub fn extract_colored_mesh2d( ) { let mut values = Vec::with_capacity(*previous_len); for (entity, computed_visibility) in query.iter() { - if !computed_visibility.is_visible { + if !computed_visibility.is_visible() { continue; } values.push((entity, (ColoredMesh2d,))); diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index f4b119b61d4f6..ae85fa92cca0f 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -173,7 +173,7 @@ fn print_mesh_count( info!( "Meshes: {} - Visible Meshes {}", sprites.iter().len(), - sprites.iter().filter(|(_, cv)| cv.is_visible).count(), + sprites.iter().filter(|(_, cv)| cv.is_visible()).count(), ); } }