Skip to content

Commit 60350c4

Browse files
mlodato517james7132
authored andcommitted
Recalculate entity aabbs when meshes change (bevyengine#4944)
# Objective Update the `calculate_bounds` system to update `Aabb`s for entities who've either: - gotten a new mesh - had their mesh mutated Fixes bevyengine#4294. ## Solution There are two commits here to address the two issues above: ### Commit 1 **This Commit** Updates the `calculate_bounds` system to operate not only on entities without `Aabb`s but also on entities whose `Handle<Mesh>` has changed. **Why?** So if an entity gets a new mesh, its associated `Aabb` is properly recalculated. **Questions** - This type is getting pretty gnarly - should I extract some types? - This system is public - should I add some quick docs while I'm here? ### Commit 2 **This Commit** Updates `calculate_bounds` to update `Aabb`s of entities whose meshes have been directly mutated. **Why?** So if an entity's mesh gets updated, its associated `Aabb` is properly recalculated. **Questions** - I think we should be using `ahash`. Do we want to do that with a direct `hashbrown` dependency or an `ahash` dependency that we configure the `HashMap` with? - There is an edge case of duplicates with `Vec<Entity>` in the `HashMap`. If an entity gets its mesh handle changed and changed back again it'll be added to the list twice. Do we want to use a `HashSet` to avoid that? Or do a check in the list first (assuming iterating over the `Vec` is faster and this edge case is rare)? - There is an edge case where, if an entity gets a new mesh handle and then its old mesh is updated, we'll update the entity's `Aabb` to the new geometry of the _old_ mesh. Do we want to remove items from the `Local<HashMap>` when handles change? Does the `Changed` event give us the old mesh handle? If not we might need to have a `HashMap<Entity, Handle<Mesh>>` or something so we can unlink entities from mesh handles when the handle changes. - I did the `zip()` with the two `HashMap` gets assuming those would be faster than calculating the Aabb of the mesh (otherwise we could do `meshes.get(mesh_handle).and_then(Mesh::compute_aabb).zip(entity_mesh_map...)` or something). Is that assumption way off? ## Testing I originally tried testing this with `bevy_mod_raycast` as mentioned in the original issue but it seemed to work (maybe they are currently manually updating the Aabbs?). I then tried doing it in 2D but it looks like `Handle<Mesh>` is just for 3D. So I took [this example](https://github.com/bevyengine/bevy/blob/main/examples/3d/pbr.rs) and added some systems to mutate/assign meshes: <details> <summary>Test Script</summary> ```rust use bevy::prelude::*; use bevy::render::camera::ScalingMode; use bevy::render::primitives::Aabb; /// Make sure we only mutate one mesh once. #[derive(Eq, PartialEq, Clone, Debug, Default)] struct MutateMeshState(bool); /// Let's have a few global meshes that we can cycle between. /// This way we can be assigned a new mesh, mutate the old one, and then get the old one assigned. #[derive(Eq, PartialEq, Clone, Debug, Default)] struct Meshes(Vec<Handle<Mesh>>); fn main() { App::new() .add_plugins(DefaultPlugins) .init_resource::<MutateMeshState>() .init_resource::<Meshes>() .add_startup_system(setup) .add_system(assign_new_mesh) .add_system(show_aabbs.after(assign_new_mesh)) .add_system(mutate_meshes.after(show_aabbs)) .run(); } fn setup( mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut global_meshes: ResMut<Meshes>, mut materials: ResMut<Assets<StandardMaterial>>, ) { let m1 = meshes.add(Mesh::from(shape::Icosphere::default())); let m2 = meshes.add(Mesh::from(shape::Icosphere { radius: 0.90, ..Default::default() })); let m3 = meshes.add(Mesh::from(shape::Icosphere { radius: 0.80, ..Default::default() })); global_meshes.0.push(m1.clone()); global_meshes.0.push(m2); global_meshes.0.push(m3); // add entities to the world // sphere commands.spawn_bundle(PbrBundle { mesh: m1, material: materials.add(StandardMaterial { base_color: Color::hex("ffd891").unwrap(), ..default() }), ..default() }); // new 3d camera commands.spawn_bundle(Camera3dBundle { projection: OrthographicProjection { scale: 3.0, scaling_mode: ScalingMode::FixedVertical(1.0), ..default() } .into(), ..default() }); // old 3d camera // commands.spawn_bundle(OrthographicCameraBundle { // transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::default(), Vec3::Y), // orthographic_projection: OrthographicProjection { // scale: 0.01, // ..default() // }, // ..OrthographicCameraBundle::new_3d() // }); } fn show_aabbs(query: Query<(Entity, &Handle<Mesh>, &Aabb)>) { for thing in query.iter() { println!("{thing:?}"); } } /// For testing the second part - mutating a mesh. /// /// Without the fix we should see this mutate an old mesh and it affects the new mesh that the /// entity currently has. /// With the fix, the mutation doesn't affect anything until the entity is reassigned the old mesh. fn mutate_meshes( mut meshes: ResMut<Assets<Mesh>>, time: Res<Time>, global_meshes: Res<Meshes>, mut mutate_mesh_state: ResMut<MutateMeshState>, ) { let mutated = mutate_mesh_state.0; if time.seconds_since_startup() > 4.5 && !mutated { println!("Mutating {:?}", global_meshes.0[0]); let m = meshes.get_mut(&global_meshes.0[0]).unwrap(); let mut p = m.attribute(Mesh::ATTRIBUTE_POSITION).unwrap().clone(); use bevy::render::mesh::VertexAttributeValues; match &mut p { VertexAttributeValues::Float32x3(v) => { v[0] = [10.0, 10.0, 10.0]; } _ => unreachable!(), } m.insert_attribute(Mesh::ATTRIBUTE_POSITION, p); mutate_mesh_state.0 = true; } } /// For testing the first part - assigning a new handle. fn assign_new_mesh( mut query: Query<&mut Handle<Mesh>, With<Aabb>>, time: Res<Time>, global_meshes: Res<Meshes>, ) { let s = time.seconds_since_startup() as usize; let idx = s % global_meshes.0.len(); for mut handle in query.iter_mut() { *handle = global_meshes.0[idx].clone_weak(); } } ``` </details> ## Changelog ### Fixed Entity `Aabb`s not updating when meshes are mutated or re-assigned.
1 parent 88aa9f1 commit 60350c4

File tree

1 file changed

+157
-38
lines changed
  • crates/bevy_render/src/view/visibility

1 file changed

+157
-38
lines changed

crates/bevy_render/src/view/visibility/mod.rs

Lines changed: 157 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
mod render_layers;
22

3+
use smallvec::SmallVec;
4+
35
pub use render_layers::*;
46

57
use bevy_app::{CoreStage, Plugin};
6-
use bevy_asset::{Assets, Handle};
8+
use bevy_asset::{AssetEvent, Assets, Handle};
79
use bevy_ecs::prelude::*;
810
use bevy_hierarchy::{Children, Parent};
911
use bevy_reflect::std_traits::ReflectDefault;
1012
use bevy_reflect::Reflect;
1113
use bevy_transform::components::GlobalTransform;
1214
use bevy_transform::TransformSystem;
15+
use bevy_utils::HashMap;
1316
use std::cell::Cell;
1417
use thread_local::ThreadLocal;
1518

@@ -127,6 +130,11 @@ pub struct VisibilityBundle {
127130
#[derive(Component)]
128131
pub struct NoFrustumCulling;
129132

133+
/// Use this component to opt-out of built-in [`Aabb`] updating for Mesh entities (i.e. to opt out
134+
/// of [`update_bounds`]).
135+
#[derive(Component)]
136+
pub struct NoAabbUpdate;
137+
130138
/// Collection of entities visible from the current view.
131139
///
132140
/// This component contains all entities which are visible from the currently
@@ -160,9 +168,59 @@ impl VisibleEntities {
160168
}
161169
}
162170

171+
/// Tracks which [`Entities`](Entity) have which meshes for entities whose [`Aabb`]s are managed by
172+
/// the [`calculate_bounds`] and [`update_bounds`] systems. This is needed because `update_bounds`
173+
/// recomputes `Aabb`s for entities whose mesh has been mutated. These mutations are visible via
174+
/// [`AssetEvent<Mesh>`](AssetEvent) which tells us which mesh was changed but not which entities
175+
/// have that mesh.
176+
#[derive(Debug, Default, Clone)]
177+
pub struct EntityMeshMap {
178+
entities_with_mesh: HashMap<Handle<Mesh>, SmallVec<[Entity; 1]>>,
179+
mesh_for_entity: HashMap<Entity, Handle<Mesh>>,
180+
}
181+
182+
impl EntityMeshMap {
183+
/// Register the passed `entity` as having the passed `mesh_handle`.
184+
fn register(&mut self, entity: Entity, mesh_handle: &Handle<Mesh>) {
185+
// Note that this list can have duplicates if an entity is registered for a mesh multiple
186+
// times. This should be rare and only cause an additional `Aabb.clone()` in
187+
// `update_bounds` so it is preferable to a `HashSet` for now.
188+
self.entities_with_mesh
189+
.entry(mesh_handle.clone_weak())
190+
.or_default()
191+
.push(entity);
192+
self.mesh_for_entity
193+
.insert(entity, mesh_handle.clone_weak());
194+
}
195+
196+
/// Deregisters the mapping between an `Entity` and `Mesh`. Used so [`update_bounds`] can
197+
/// track which mappings are still active so `Aabb`s are updated correctly.
198+
fn deregister(&mut self, entity: Entity) {
199+
let mut inner = || {
200+
let mesh = self.mesh_for_entity.remove(&entity)?;
201+
202+
// This lookup failing is _probably_ an error.
203+
let entities = self.entities_with_mesh.get_mut(&mesh)?;
204+
205+
// There could be duplicate entries in here if an entity was registered with a mesh
206+
// multiple times. It's important to remove all references so that if an entity gets a
207+
// new mesh and its old mesh is mutated, the entity doesn't get its old mesh's new
208+
// `Aabb`. Note that there _should_ only be one entity.
209+
for i in (0..entities.len()).rev() {
210+
if entities[i] == entity {
211+
entities.swap_remove(i);
212+
}
213+
}
214+
Some(())
215+
};
216+
inner();
217+
}
218+
}
219+
163220
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
164221
pub enum VisibilitySystems {
165222
CalculateBounds,
223+
UpdateBounds,
166224
UpdateOrthographicFrusta,
167225
UpdatePerspectiveFrusta,
168226
UpdateProjectionFrusta,
@@ -178,60 +236,121 @@ impl Plugin for VisibilityPlugin {
178236
fn build(&self, app: &mut bevy_app::App) {
179237
use VisibilitySystems::*;
180238

181-
app.add_system_to_stage(
182-
CoreStage::PostUpdate,
183-
calculate_bounds.label(CalculateBounds),
184-
)
185-
.add_system_to_stage(
186-
CoreStage::PostUpdate,
187-
update_frusta::<OrthographicProjection>
188-
.label(UpdateOrthographicFrusta)
189-
.after(TransformSystem::TransformPropagate),
190-
)
191-
.add_system_to_stage(
192-
CoreStage::PostUpdate,
193-
update_frusta::<PerspectiveProjection>
194-
.label(UpdatePerspectiveFrusta)
195-
.after(TransformSystem::TransformPropagate),
196-
)
197-
.add_system_to_stage(
198-
CoreStage::PostUpdate,
199-
update_frusta::<Projection>
200-
.label(UpdateProjectionFrusta)
201-
.after(TransformSystem::TransformPropagate),
202-
)
203-
.add_system_to_stage(
204-
CoreStage::PostUpdate,
205-
visibility_propagate_system.label(VisibilityPropagate),
206-
)
207-
.add_system_to_stage(
208-
CoreStage::PostUpdate,
209-
check_visibility
210-
.label(CheckVisibility)
211-
.after(CalculateBounds)
212-
.after(UpdateOrthographicFrusta)
213-
.after(UpdatePerspectiveFrusta)
214-
.after(UpdateProjectionFrusta)
215-
.after(VisibilityPropagate)
216-
.after(TransformSystem::TransformPropagate),
217-
);
239+
app.init_resource::<EntityMeshMap>()
240+
.add_system_to_stage(
241+
CoreStage::PostUpdate,
242+
calculate_bounds.label(CalculateBounds),
243+
)
244+
.add_system_to_stage(CoreStage::PostUpdate, update_bounds.label(UpdateBounds))
245+
.add_system_to_stage(
246+
CoreStage::PostUpdate,
247+
update_frusta::<OrthographicProjection>
248+
.label(UpdateOrthographicFrusta)
249+
.after(TransformSystem::TransformPropagate),
250+
)
251+
.add_system_to_stage(
252+
CoreStage::PostUpdate,
253+
update_frusta::<PerspectiveProjection>
254+
.label(UpdatePerspectiveFrusta)
255+
.after(TransformSystem::TransformPropagate),
256+
)
257+
.add_system_to_stage(
258+
CoreStage::PostUpdate,
259+
update_frusta::<Projection>
260+
.label(UpdateProjectionFrusta)
261+
.after(TransformSystem::TransformPropagate),
262+
)
263+
.add_system_to_stage(
264+
CoreStage::PostUpdate,
265+
visibility_propagate_system.label(VisibilityPropagate),
266+
)
267+
.add_system_to_stage(
268+
CoreStage::PostUpdate,
269+
check_visibility
270+
.label(CheckVisibility)
271+
.after(CalculateBounds)
272+
.after(UpdateBounds)
273+
.after(UpdateOrthographicFrusta)
274+
.after(UpdatePerspectiveFrusta)
275+
.after(UpdateProjectionFrusta)
276+
.after(VisibilityPropagate)
277+
.after(TransformSystem::TransformPropagate),
278+
);
218279
}
219280
}
220281

282+
/// Calculates [`Aabb`]s for [`Entities`](Entity) with [`Mesh`]es.
221283
pub fn calculate_bounds(
222284
mut commands: Commands,
223285
meshes: Res<Assets<Mesh>>,
224286
without_aabb: Query<(Entity, &Handle<Mesh>), (Without<Aabb>, Without<NoFrustumCulling>)>,
287+
mut entity_mesh_map: ResMut<EntityMeshMap>,
225288
) {
226289
for (entity, mesh_handle) in &without_aabb {
227290
if let Some(mesh) = meshes.get(mesh_handle) {
228291
if let Some(aabb) = mesh.compute_aabb() {
292+
entity_mesh_map.register(entity, mesh_handle);
229293
commands.entity(entity).insert(aabb);
230294
}
231295
}
232296
}
233297
}
234298

299+
/// Updates [`Aabb`]s for [`Entities`](Entity) with [`Mesh`]es. This includes `Entities` that have
300+
/// been assigned new `Mesh`es as well as `Entities` whose `Mesh` has been directly mutated.
301+
///
302+
/// To opt out of bound calculation for an `Entity`, give it the [`NoAabbUpdate`] component.
303+
///
304+
/// NOTE: This system needs to remove entities from their collection in
305+
/// [`EntityMeshMap`] whenever a mesh handle is reassigned or an entity's mesh handle is
306+
/// removed. This may impact performance if meshes with many entities are frequently
307+
/// reassigned/removed.
308+
pub fn update_bounds(
309+
mut commands: Commands,
310+
meshes: Res<Assets<Mesh>>,
311+
mut mesh_reassigned: Query<
312+
(Entity, &Handle<Mesh>, &mut Aabb),
313+
(
314+
Changed<Handle<Mesh>>,
315+
Without<NoFrustumCulling>,
316+
Without<NoAabbUpdate>,
317+
),
318+
>,
319+
mut entity_mesh_map: ResMut<EntityMeshMap>,
320+
mut mesh_events: EventReader<AssetEvent<Mesh>>,
321+
entities_lost_mesh: RemovedComponents<Handle<Mesh>>,
322+
) {
323+
for entity in entities_lost_mesh.iter() {
324+
entity_mesh_map.deregister(entity);
325+
}
326+
327+
for (entity, mesh_handle, mut aabb) in mesh_reassigned.iter_mut() {
328+
entity_mesh_map.deregister(entity);
329+
if let Some(mesh) = meshes.get(mesh_handle) {
330+
if let Some(new_aabb) = mesh.compute_aabb() {
331+
entity_mesh_map.register(entity, mesh_handle);
332+
*aabb = new_aabb;
333+
}
334+
}
335+
}
336+
337+
let to_update = |event: &AssetEvent<Mesh>| {
338+
let handle = match event {
339+
AssetEvent::Modified { handle } => handle,
340+
_ => return None,
341+
};
342+
let mesh = meshes.get(handle)?;
343+
let entities_with_handle = entity_mesh_map.entities_with_mesh.get(handle)?;
344+
let aabb = mesh.compute_aabb()?;
345+
Some((aabb, entities_with_handle))
346+
};
347+
for (aabb, entities_with_handle) in mesh_events.iter().filter_map(to_update) {
348+
for entity in entities_with_handle {
349+
commands.entity(*entity).insert(aabb.clone());
350+
}
351+
}
352+
}
353+
235354
pub fn update_frusta<T: Component + CameraProjection + Send + Sync + 'static>(
236355
mut views: Query<(&GlobalTransform, &T, &mut Frustum)>,
237356
) {

0 commit comments

Comments
 (0)