-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mesh AABBs are never updated #4294
Comments
Hi! I'm fairly new to
One proposal for solving One proposal for solving Is that roughly correct? If so, I'm happy to go ahead and try to do |
@mlodato517 Your whole speculation is not just "roughly" correct, it's dead on! I said that the event reading approach might introduce perf issues. For posterity, I think it's going to be a perf issue because, even if you can listen for |
Note that solving only |
Oh I'm glad you said this! I had assumed (silly me!) that the performance hit was something inherent to
I think this makes sense! I'm guessing the lookup would be undesirable in the hot path of collision detection but to avoid the
This would mean that updating the |
@mlodato517 I was rather thinking the following: The |
Hmmm maybe I am a bit confused. I thought originally you were suggesting roughly this pseudocode: pub fn calculate_bounds(
mut commands: Commands,
meshes: Res<Assets<Mesh>>,
without_aabb: Query<(Entity, &Handle<Mesh>), (Without<Aabb>, Without<NoFrustumCulling>)>,
mesh_events: EventReader<AssetEvent<Mesh>>, // new
entities_with_mesh: &HashMap<Handle<Mesh>, Vec<Entity>>, // new
) {
// same old code plus...
let updated_meshes = mesh_events.iter().filter_map(|&event| match event {
Modified { handle } => Some(handle),
_ => None,
});
for mesh_handle in updated_meshes {
if let Some(mesh) = meshes.get(mesh_handle) {
if let Some(aabb) = mesh.compute_aabb() {
if let Some(entities) = entities_with_mesh(mesh_handle) {
commands.entity(entity).insert(aabb); // or do we need to remove + insert?
// lots of closing curlies... Was I way off there? (I also wonder if this should be a different system from If notI think this makes perfect sense. I was just saying that, if we wanted to, instead of storing I think that's undesirable because it impacts the performance of everyone checking for Not that I'm saying we should go this way, just wanted to make sure my suggestion was clear :-) |
I had in mind pretty much what you wrote down yeah. I was thinking of the mut entities_with_mesh: Local<HashMap<Handle<Mesh>, Vec<Entity>>> (and updating the hashmap when necessary). A hint: you could maybe chain the The idea described in the |
Oh, sorry, last thing, did we want this to be a separate system (e.g. " |
@mlodato517 If we want to use a |
Oh, yeah, because we're going to add items to the map when we calculate bounds for them the first time? |
@mlodato517 this is correct. I think you have everything you need now. |
Another way would be to consider that the aabb of a mesh should be an asset rather than a component. That way each entity would have the handle to the same aabb linked to their mesh. Not sure how much that would add when querying though... |
That's an interesting idea! If it were an Asset would it essentially require a Side QuestionWhat is "an Asset"? Kind of from like a semantic/best practice point of view? It seems like there may be situations where you can make something an asset for some benefit but I wonder if there are any docs for when you should (or maybe more importantly shouldn't) make something an asset. It's totally tangential to this thread so we can take the conversation elsewhere but I'd be happy to either update the |
Reopening this to account for #5489 |
# 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.
# 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.
# 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.
Reproduced in Bevy 0.9: great video of the problem here. |
This is super annoying. One can easily circumvent this, but it's still a trap. Can we get this merged in 0.11? |
If anyone is here looking for a quick workaround in the meantime, manually removing the AABB from the entity, after the mesh has been changed, seem to do the trick: commands
.entity(entity_id)
.remove::<bevy::render::primitives::Aabb>(); |
I don't know if this just removes the aabb and disables culling or not. if let Some(aabb) = mesh.compute_aabb() {
commands.entity(entity).try_insert(aabb);
} (from #10801) |
This is documented for Aabb as "It won’t be updated automatically if the space occupied by the entity changes, for example if the vertex positions of a Mesh inside a Handle are updated." It is also documented for Mesh under "Common points of confusion" as "Bevy performs frustum culling based on the Aabb of meshes, which is calculated and added automatically for new meshes only. If a mesh is modified, the entity’s Aabb needs to be updated manually or deleted so that it is re-calculated." I also stumbled over this while implementing terrain generation/modification, but I think the documentation is clear enough. It is subjective if there should already be a system in place to update the Aabbs for modified meshes. But IMHO if someone is modifying vertices by hand, then it can be expected that this someone reads the documentation anyway. I propose to close this issue. |
It's good that this is documented but I don't see a fundamental reason why we need to keep this footgun in place. Is there an unavoidable performance cost? |
Bevy version: 0.6
OS & graphic stack: ArchLinux kernel 5.16.2
Problem
When using https://github.com/aevyrie/bevy_mod_raycast which relies on bevy's Aabb, and I update an entity's mesh, either by (1) changing it's
Handle<Mesh>
or (2) updating the mesh itself in theAssets<Mesh>
, the plugin doesn't account for the change in mesh value.This is because bevy never updates the
Aabb
of an entity after the initial spawn of a mesh.Solution
It is legitimate to expect the
Aabb
s would be updated with the meshes or mesh handles of entities (certainly the documentation doesn't mention that). Either this should be documented as expected behavior or the code amended to updateAabb
s based on the current mesh.Technical solution
It seems the problematic system is
bevy_render::view::visibility::calculate_bounds
. (crates/bevy_render/src/view/visibility/mod.rs
line 112 on 024d984).#5489 has a few discussions around potential fixes.
Current Workaround
Manually update the
Aabb
after updating the mesh.The text was updated successfully, but these errors were encountered: