Calculate AABBs to enable text2d culling (#11663)
# Objective - Cull 2D text outside the view frustum. - Part of #11081. ## Solution - Compute AABBs for entities with a `Text2DBundle` to enable culling them. `text2d` example with AABB gizmos on the text entities: https://github.com/bevyengine/bevy/assets/18357657/52ed3ddc-2274-4480-835b-a7cf23338931 --- ## Changelog ### Added - 2D text outside the view are now culled with the `calculate_bounds_text2d` system adding the necessary AABBs.
This commit is contained in:
parent
21adeb6842
commit
58ee3e8908
@ -190,7 +190,7 @@ impl VisibleEntities {
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||
pub enum VisibilitySystems {
|
||||
/// Label for the [`calculate_bounds`] and `calculate_bounds_2d` systems,
|
||||
/// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems,
|
||||
/// calculating and inserting an [`Aabb`] to relevant entities.
|
||||
CalculateBounds,
|
||||
/// Label for the [`update_frusta<OrthographicProjection>`] system.
|
||||
|
@ -33,5 +33,8 @@ glyph_brush_layout = "0.2.1"
|
||||
thiserror = "1.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
approx = "0.5.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
@ -31,7 +31,9 @@ use bevy_asset::AssetApp;
|
||||
#[cfg(feature = "default_font")]
|
||||
use bevy_asset::{load_internal_binary_asset, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_render::{camera::CameraUpdateSystem, ExtractSchedule, RenderApp};
|
||||
use bevy_render::{
|
||||
camera::CameraUpdateSystem, view::VisibilitySystems, ExtractSchedule, RenderApp,
|
||||
};
|
||||
use bevy_sprite::SpriteSystem;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
@ -87,6 +89,9 @@ impl Plugin for TextPlugin {
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
calculate_bounds_text2d
|
||||
.in_set(VisibilitySystems::CalculateBounds)
|
||||
.after(update_text2d_layout),
|
||||
update_text2d_layout
|
||||
.after(font_atlas_set::remove_dropped_font_atlas_sets)
|
||||
// Potential conflict: `Assets<Image>`
|
||||
|
@ -10,6 +10,7 @@ use bevy_ecs::{
|
||||
entity::Entity,
|
||||
event::EventReader,
|
||||
prelude::With,
|
||||
query::{Changed, Without},
|
||||
reflect::ReflectComponent,
|
||||
system::{Commands, Local, Query, Res, ResMut},
|
||||
};
|
||||
@ -17,8 +18,9 @@ use bevy_math::Vec2;
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::{
|
||||
prelude::LegacyColor,
|
||||
primitives::Aabb,
|
||||
texture::Image,
|
||||
view::{InheritedVisibility, ViewVisibility, Visibility},
|
||||
view::{InheritedVisibility, NoFrustumCulling, ViewVisibility, Visibility},
|
||||
Extract,
|
||||
};
|
||||
use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlasLayout};
|
||||
@ -226,3 +228,152 @@ pub fn update_text2d_layout(
|
||||
pub fn scale_value(value: f32, factor: f32) -> f32 {
|
||||
value * factor
|
||||
}
|
||||
|
||||
/// System calculating and inserting an [`Aabb`] component to entities with some
|
||||
/// [`TextLayoutInfo`] and [`Anchor`] components, and without a [`NoFrustumCulling`] component.
|
||||
///
|
||||
/// Used in system set [`VisibilitySystems::CalculateBounds`](bevy_render::view::VisibilitySystems::CalculateBounds).
|
||||
pub fn calculate_bounds_text2d(
|
||||
mut commands: Commands,
|
||||
mut text_to_update_aabb: Query<
|
||||
(Entity, &TextLayoutInfo, &Anchor, Option<&mut Aabb>),
|
||||
(Changed<TextLayoutInfo>, Without<NoFrustumCulling>),
|
||||
>,
|
||||
) {
|
||||
for (entity, layout_info, anchor, aabb) in &mut text_to_update_aabb {
|
||||
// `Anchor::as_vec` gives us an offset relative to the text2d bounds, by negating it and scaling
|
||||
// by the logical size we compensate the transform offset in local space to get the center.
|
||||
let center = (-anchor.as_vec() * layout_info.logical_size)
|
||||
.extend(0.0)
|
||||
.into();
|
||||
// Distance in local space from the center to the x and y limits of the text2d bounds.
|
||||
let half_extents = (layout_info.logical_size / 2.0).extend(0.0).into();
|
||||
if let Some(mut aabb) = aabb {
|
||||
*aabb = Aabb {
|
||||
center,
|
||||
half_extents,
|
||||
};
|
||||
} else {
|
||||
commands.entity(entity).try_insert(Aabb {
|
||||
center,
|
||||
half_extents,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use bevy_app::{App, Update};
|
||||
use bevy_asset::{load_internal_binary_asset, Handle};
|
||||
use bevy_ecs::{event::Events, schedule::IntoSystemConfigs};
|
||||
use bevy_utils::default;
|
||||
|
||||
use super::*;
|
||||
|
||||
const FIRST_TEXT: &str = "Sample text.";
|
||||
const SECOND_TEXT: &str = "Another, longer sample text.";
|
||||
|
||||
fn setup() -> (App, Entity) {
|
||||
let mut app = App::new();
|
||||
app.init_resource::<Assets<Font>>()
|
||||
.init_resource::<Assets<Image>>()
|
||||
.init_resource::<Assets<TextureAtlasLayout>>()
|
||||
.init_resource::<TextSettings>()
|
||||
.init_resource::<FontAtlasSets>()
|
||||
.init_resource::<Events<WindowScaleFactorChanged>>()
|
||||
.insert_resource(TextPipeline::default())
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
update_text2d_layout,
|
||||
calculate_bounds_text2d.after(update_text2d_layout),
|
||||
),
|
||||
);
|
||||
|
||||
// A font is needed to ensure the text is laid out with an actual size.
|
||||
load_internal_binary_asset!(
|
||||
app,
|
||||
Handle::default(),
|
||||
"FiraMono-subset.ttf",
|
||||
|bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() }
|
||||
);
|
||||
|
||||
let entity = app
|
||||
.world
|
||||
.spawn((Text2dBundle {
|
||||
text: Text::from_section(FIRST_TEXT, default()),
|
||||
..default()
|
||||
},))
|
||||
.id();
|
||||
|
||||
(app, entity)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_bounds_text2d_create_aabb() {
|
||||
let (mut app, entity) = setup();
|
||||
|
||||
assert!(!app
|
||||
.world
|
||||
.get_entity(entity)
|
||||
.expect("Could not find entity")
|
||||
.contains::<Aabb>());
|
||||
|
||||
// Creates the AABB after text layouting.
|
||||
app.update();
|
||||
|
||||
let aabb = app
|
||||
.world
|
||||
.get_entity(entity)
|
||||
.expect("Could not find entity")
|
||||
.get::<Aabb>()
|
||||
.expect("Text should have an AABB");
|
||||
|
||||
// Text2D AABB does not have a depth.
|
||||
assert_eq!(aabb.center.z, 0.0);
|
||||
assert_eq!(aabb.half_extents.z, 0.0);
|
||||
|
||||
// AABB has an actual size.
|
||||
assert!(aabb.half_extents.x > 0.0 && aabb.half_extents.y > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_bounds_text2d_update_aabb() {
|
||||
let (mut app, entity) = setup();
|
||||
|
||||
// Creates the initial AABB after text layouting.
|
||||
app.update();
|
||||
|
||||
let first_aabb = *app
|
||||
.world
|
||||
.get_entity(entity)
|
||||
.expect("Could not find entity")
|
||||
.get::<Aabb>()
|
||||
.expect("Could not find initial AABB");
|
||||
|
||||
let mut entity_ref = app
|
||||
.world
|
||||
.get_entity_mut(entity)
|
||||
.expect("Could not find entity");
|
||||
*entity_ref
|
||||
.get_mut::<Text>()
|
||||
.expect("Missing Text on entity") = Text::from_section(SECOND_TEXT, default());
|
||||
|
||||
// Recomputes the AABB.
|
||||
app.update();
|
||||
|
||||
let second_aabb = *app
|
||||
.world
|
||||
.get_entity(entity)
|
||||
.expect("Could not find entity")
|
||||
.get::<Aabb>()
|
||||
.expect("Could not find second AABB");
|
||||
|
||||
// Check that the height is the same, but the width is greater.
|
||||
approx::assert_abs_diff_eq!(first_aabb.half_extents.y, second_aabb.half_extents.y);
|
||||
assert!(FIRST_TEXT.len() < SECOND_TEXT.len());
|
||||
assert!(first_aabb.half_extents.x < second_aabb.half_extents.x);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user