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 <mcanders1@gmail.com>
This commit is contained in:
Carter Anderson 2022-07-15 23:24:42 +00:00
parent c8aa047cca
commit 40d4992401
17 changed files with 472 additions and 114 deletions

View File

@ -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,
}

View File

@ -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(

View File

@ -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<Vec<PointLightAssignmentData>>,
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
mut max_point_lights_warning_emitted: Local<bool>,
@ -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<GlobalTransform>, Changed<DirectionalLight>)>,
>,
@ -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<SpotLight>,
>,
mut visible_entity_query: Query<
(
Entity,
&Visibility,
&mut ComputedVisibility,
Option<&RenderLayers>,
Option<&Aabb>,
Option<&GlobalTransform>,
),
Without<NotShadowCaster>,
(Without<NotShadowCaster>, Without<DirectionalLight>),
>,
) {
// 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);
}
}

View File

@ -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<Res<PointLightShadowMap>>,
directional_light_shadow_map: Extract<Res<DirectionalLightShadowMap>>,
global_point_lights: Extract<Res<GlobalVisiblePointLights>>,
point_lights: Extract<Query<(&PointLight, &CubemapVisibleEntities, &GlobalTransform)>>,
spot_lights: Extract<Query<(&SpotLight, &VisibleEntities, &GlobalTransform)>>,
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<SpotLight>,
>,
@ -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;
}

View File

@ -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?

View File

@ -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" }

View File

@ -200,7 +200,7 @@ fn extract_visible_components<C: ExtractComponent>(
) {
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),)));
}
}

View File

@ -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<T: Component + CameraProjection + Send + Sync + 'static>(
}
}
fn visibility_propagate_system(
mut root_query: Query<
(
Option<&Children>,
&Visibility,
&mut ComputedVisibility,
Entity,
),
Without<Parent>,
>,
mut visibility_query: Query<(&Visibility, &mut ComputedVisibility, &Parent)>,
children_query: Query<&Children, (With<Parent>, With<Visibility>, With<ComputedVisibility>)>,
) {
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<Parent>, With<Visibility>, With<ComputedVisibility>)>,
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<T: Component + CameraProjection + Send + Sync + 'static>(
pub fn check_visibility(
mut thread_queues: Local<ThreadLocal<Cell<Vec<Entity>>>>,
mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With<Camera>>,
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<Aabb>,
>,
) {
// 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::<ComputedVisibility>()
.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"
);
}
}

View File

@ -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"

View File

@ -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<Image>,
/// 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,
}

View File

@ -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();

View File

@ -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<ExtractedSprites>,
texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
sprite_query: Extract<Query<(&Visibility, &Sprite, &GlobalTransform, &Handle<Image>)>>,
sprite_query: Extract<
Query<(
Entity,
&ComputedVisibility,
&Sprite,
&GlobalTransform,
&Handle<Image>,
)>,
>,
atlas_query: Extract<
Query<(
&Visibility,
Entity,
&ComputedVisibility,
&TextureAtlasSprite,
&GlobalTransform,
&Handle<TextureAtlas>,
@ -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<FixedBitSet>,
draw_functions: Res<DrawFunctions<Transparent2d>>,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
@ -346,7 +362,7 @@ pub fn queue_sprites(
gpu_images: Res<RenderAssets<Image>>,
msaa: Res<Msaa>,
mut extracted_sprites: ResMut<ExtractedSprites>,
mut views: Query<&mut RenderPhase<Transparent2d>>,
mut views: Query<(&VisibleEntities, &mut RenderPhase<Transparent2d>)>,
events: Res<SpriteAssetEvents>,
) {
// 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,

View File

@ -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<Res<Assets<TextureAtlas>>>,
text_pipeline: Extract<Res<DefaultTextPipeline>>,
windows: Extract<Res<Windows>>,
text2d_query: Extract<Query<(Entity, &Visibility, &Text, &GlobalTransform, &Text2dSize)>>,
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,

View File

@ -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(),
}
}
}

View File

@ -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`)

View File

@ -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,)));

View File

@ -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(),
);
}
}