Skip to content

BVH Broad Phase and Collider Trees#927

Merged
Jondolf merged 97 commits intomainfrom
obvhs
Feb 2, 2026
Merged

BVH Broad Phase and Collider Trees#927
Jondolf merged 97 commits intomainfrom
obvhs

Conversation

@Jondolf
Copy link
Member

@Jondolf Jondolf commented Jan 24, 2026

Objective

Implement a new broad phase algorithm using Bounding Volume Hierarchies (BVH) provided by OBVHS. This replaces our existing broad phase that uses Sweep and Prune (SAP).

The goal is to greatly reduce overhead for large scenes with lots of colliders. Updating and querying the acceleration structures should be efficient, and static or sleeping bodies should have minimal runtime cost. The existing SAP broad phase is very inefficient at this, and scales poorly even with only static geometry.

Additionally, we can reuse these BVHs for accelerating spatial queries. Currently, they just use Parry's BVH, which we update and manage very inefficiently. I will leave migrating spatial queries to the new BVHs for a follow-up however, to keep the diff more manageable.

Huge thanks to @DGriffin91 for implementing incremental leaf insertion and removal, partial rebuilds, and more for OBVHS to better suit our needs <3

Solution

  • ColliderTree type that contains a Bvh2, its collision proxies, and a workspace for reusing allocations
  • ColliderTrees resource, containing a separate ColliderTree for dynamic, kinematic, static, and standalone (=no body) colliders
  • ColliderTreePlugin that manages ColliderTrees for a collider type C
    • Adds/removes colliders to/from trees, and updates proxy data
    • Updates ColliderAabbs and EnlargedAabbs of colliders
    • Manages "moved proxies": colliders that moved past their EnlargedAabb, or otherwise require updating the tree
    • Optimizes trees to maintain good query performance, in an async task that runs concurrently with the narrow phase and solver
      • Full rebuilds, partial rebuilds, or reinsertion based on ColliderTreeOptimization settings (default is an adaptive hybrid that switches between them)
  • BroadPhaseCorePlugin that sets up the resources, system sets, and diagnostics required for the broad phase
  • BvhBroadPhasePlugin that implements a BVH-based broad phase algorithm
    • Traverses each tree for every moved proxy (colliders that don't move beyond their enlarged AABB don't need to check for new overlaps)
    • Creates edges in the contact graph for every new overlap between enlarged AABBs

Testing

Ran examples and tested adding/removing/enabling/disabling colliders and changing various different configurations. Users also did some testing in real-world applications.

Note for Reviewers

The commit history here is a bit borked for some reason, and contains some older commits that are unrelated. The first real commit is "Implement dynamic BVH broad phase with OBVHS (WIP)" (a603c5e).


Showcase

Here is the new bvh example, for stress testing the collider trees with various different configurations.

bvh.mp4

Dynamic Scene

Below is a test scene with 10k colliders with approximately 25% of them being moved each frame. Spatial queries are disabled.

Before, with the SAP broad phase:

Before

After, with the BVH broad phase:

After

Note that while the tree optimization has a fairly large cost here, in real-world applications the cost should often be more hidden, as it is done in parallel with the narrow phase and solver.

Static Scene

Below are the results before and after, for 40k static colliders with no movement. Spatial queries are disabled. Debug rendering is also disabled, as gizmos start to tank performance at this scale.

BVH Performance

Future Work

  • Make spatial queries use the new BVH (adopt Draft: Make spatial queries generic over colliders #810)
  • Explore making collider trees more flexible, ex: allow users to easily create their own trees and perform queries on them
  • Try doing a small amount of reinsertion on the static tree at each step to improve its query performance
  • Optimize partial rebuilds further

- The broad phase now emits new collision pairs and stores all pairs in a HashSet

- Contact status and kind is now tracked with `ContactPairFlags` instead of booleans

- The narrow phase adds new collision pairs, updates existing pairs, and responds to state changes separately instead of overwriting and doing extra work for persistent contact

- State changes are tracked with bit vectors (bit sets), which are fast to iterate serially

- The narrow phase is responsible for collision events instead of the `ContactReportingPlugin`
- Renamed `BroadCollisionPairs` to `BroadPhasePairSet`
- Added `BroadPhasePairSet` for fast pair lookup with new `PairKey`
- Improve broad phase docs
…pt-in

- Removed `BroadPhaseAddedPairs`
- Renamed `BroadPhasePairSet` to `BroadPhasePairs`
- Moved contact creation to broad phase to improve persistence
- Removed some graph querying overhead from contact pair removal by using the `EdgeIndex` directly
- Made collision events opt-in with `CollisionEventsEnabled` component
- Improved a lot of docs
@eswartz
Copy link

eswartz commented Jan 30, 2026

I think there is a new issue on this branch (doesn't exist on main). I have a "noclip" toggle feature, which changes the player's RigidBody::Kinematic to RigidBody::Static. It seems this does not reset state properly, since it results in this panic when the player first intersects world geometry:

(Here it's overflowed a signed to unsigned u32 The index is sometimes positive, sometimes equal to the length.)

thread 'Async Compute Task Pool (3)' (2094163) panicked at /home/ejs/.cargo/git/checkouts/obvhs-910b4198c23e914d/4735d72/src/ploc/partial_rebuild.rs:31:32:
index out of bounds: the len is 9 but the index is 4294967295
stack backtrace:
   0: __rustc::rust_begin_unwind
             at /rustc/0a3cd3b6b6e1fa8fd3c75c1d13d2e22e64273f49/library/std/src/panicking.rs:689:5
   1: core::panicking::panic_fmt
             at /rustc/0a3cd3b6b6e1fa8fd3c75c1d13d2e22e64273f49/library/core/src/panicking.rs:80:14
   2: core::panicking::panic_bounds_check
             at /rustc/0a3cd3b6b6e1fa8fd3c75c1d13d2e22e64273f49/library/core/src/panicking.rs:271:5
   3: index<obvhs::bvh2::node::Bvh2Node>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/index.rs:272:10
   4: index<obvhs::bvh2::node::Bvh2Node, usize>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/index.rs:19:15
   5: index<obvhs::bvh2::node::Bvh2Node, usize, alloc::alloc::Global>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3740:9
   6: compute_rebuild_path_flags<&[u32], &u32>
             at /home/ejs/.cargo/git/checkouts/obvhs-910b4198c23e914d/4735d72/src/ploc/partial_rebuild.rs:31:32
   7: rebuild_partial
             at /home/ejs/.cargo/git/checkouts/avian-5a22c167119f3550/21c1e15/crates/avian3d/../../src/collider_tree/tree.rs:313:9
   8: {closure#3}
             at /home/ejs/.cargo/git/checkouts/avian-5a22c167119f3550/21c1e15/crates/avian3d/../../src/collider_tree/optimization.rs:207:22
   9: {async_block#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>
             at /home/ejs/.cargo/git/checkouts/avian-5a22c167119f3550/21c1e15/crates/avian3d/../../src/collider_tree/optimization.rs:230:9
  10: poll<avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>, async_executor::{impl#5}::spawn_inner::{closure_env#0}<bevy_ecs::world::command_queue::CommandQueue, avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>>>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.3/src/lib.rs:1197:31
  11: {closure#1}<async_executor::AsyncCallOnDrop<avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>, async_executor::{impl#5}::spawn_inner::{closure_env#0}<bevy_ecs::world::command_queue::CommandQueue, avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>>>, bevy_ecs::world::command_queue::CommandQueue, async_executor::{impl#5}::schedule::{closure_env#0}, ()>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/raw.rs:550:21
  12: call_once<async_task::raw::{impl#3}::run::{closure_env#1}<async_executor::AsyncCallOnDrop<avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>, async_executor::{impl#5}::spawn_inner::{closure_env#0}<bevy_ecs::world::command_queue::CommandQueue, avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>>>, bevy_ecs::world::command_queue::CommandQueue, async_executor::{impl#5}::schedule::{closure_env#0}, ()>, ()>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
  13: call_once<core::task::poll::Poll<bevy_ecs::world::command_queue::CommandQueue>, async_task::raw::{impl#3}::run::{closure_env#1}<async_executor::AsyncCallOnDrop<avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>, async_executor::{impl#5}::spawn_inner::{closure_env#0}<bevy_ecs::world::command_queue::CommandQueue, avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>>>, bevy_ecs::world::command_queue::CommandQueue, async_executor::{impl#5}::schedule::{closure_env#0}, ()>>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/panic/unwind_safe.rs:274:9
  14: do_call<core::panic::unwind_safe::AssertUnwindSafe<async_task::raw::{impl#3}::run::{closure_env#1}<async_executor::AsyncCallOnDrop<avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>, async_executor::{impl#5}::spawn_inner::{closure_env#0}<bevy_ecs::world::command_queue::CommandQueue, avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>>>, bevy_ecs::world::command_queue::CommandQueue, async_executor::{impl#5}::schedule::{closure_env#0}, ()>>, core::task::poll::Poll<bevy_ecs::world::command_queue::CommandQueue>>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
  15: catch_unwind<core::task::poll::Poll<bevy_ecs::world::command_queue::CommandQueue>, core::panic::unwind_safe::AssertUnwindSafe<async_task::raw::{impl#3}::run::{closure_env#1}<async_executor::AsyncCallOnDrop<avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>, async_executor::{impl#5}::spawn_inner::{closure_env#0}<bevy_ecs::world::command_queue::CommandQueue, avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>>>, bevy_ecs::world::command_queue::CommandQueue, async_executor::{impl#5}::schedule::{closure_env#0}, ()>>>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
  16: catch_unwind<core::panic::unwind_safe::AssertUnwindSafe<async_task::raw::{impl#3}::run::{closure_env#1}<async_executor::AsyncCallOnDrop<avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>, async_executor::{impl#5}::spawn_inner::{closure_env#0}<bevy_ecs::world::command_queue::CommandQueue, avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>>>, bevy_ecs::world::command_queue::CommandQueue, async_executor::{impl#5}::schedule::{closure_env#0}, ()>>, core::task::poll::Poll<bevy_ecs::world::command_queue::CommandQueue>>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
  17: run<async_executor::AsyncCallOnDrop<avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>, async_executor::{impl#5}::spawn_inner::{closure_env#0}<bevy_ecs::world::command_queue::CommandQueue, avian3d::collider_tree::optimization::spawn_optimization_task::{async_block_env#0}<avian3d::collider_tree::optimization::optimize_trees::{closure_env#3}>>>, bevy_ecs::world::command_queue::CommandQueue, async_executor::{impl#5}::schedule::{closure_env#0}, ()>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-task-4.7.1/src/raw.rs:549:23
  18: {async_block#0}<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.3/src/lib.rs:751:30
  19: poll<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>, async_executor::{impl#13}::run::{async_fn#0}::{async_block_env#0}<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>>>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-lite-2.6.1/src/future.rs:454:46
  20: {async_fn#0}<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.3/src/lib.rs:758:32
  21: {async_fn#0}<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-executor-1.13.3/src/lib.rs:344:34
  22: {closure#1}<core::result::Result<(), async_channel::RecvError>, async_executor::{impl#5}::run::{async_fn_env#0}<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>>>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/src/driver.rs:204:53
  23: try_with<core::cell::RefCell<(parking::Parker, core::task::wake::Waker, alloc::sync::Arc<core::sync::atomic::AtomicBool, alloc::alloc::Global>)>, async_io::driver::block_on::{closure_env#1}<core::result::Result<(), async_channel::RecvError>, async_executor::{impl#5}::run::{async_fn_env#0}<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>>>, core::result::Result<(), async_channel::RecvError>>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:513:12
  24: with<core::cell::RefCell<(parking::Parker, core::task::wake::Waker, alloc::sync::Arc<core::sync::atomic::AtomicBool, alloc::alloc::Global>)>, async_io::driver::block_on::{closure_env#1}<core::result::Result<(), async_channel::RecvError>, async_executor::{impl#5}::run::{async_fn_env#0}<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>>>, core::result::Result<(), async_channel::RecvError>>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:477:20
  25: block_on<core::result::Result<(), async_channel::RecvError>, async_executor::{impl#5}::run::{async_fn_env#0}<core::result::Result<(), async_channel::RecvError>, futures_lite::future::Or<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure#0}::{async_block_env#0}, async_channel::Recv<()>>>>
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/async-io-2.6.0/src/driver.rs:180:11
  26: {closure#0}
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy_tasks-0.18.0/src/task_pool.rs:203:37
  27: do_call<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure_env#0}, core::result::Result<(), async_channel::RecvError>>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:581:40
  28: catch_unwind<core::result::Result<(), async_channel::RecvError>, bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure_env#0}>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panicking.rs:544:19
  29: catch_unwind<bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure#0}::{closure_env#0}, core::result::Result<(), async_channel::RecvError>>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/panic.rs:359:14
  30: {closure#0}
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy_tasks-0.18.0/src/task_pool.rs:197:43
  31: try_with<bevy_tasks::executor::LocalExecutor, bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure_env#0}, ()>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:513:12
  32: with<bevy_tasks::executor::LocalExecutor, bevy_tasks::task_pool::{impl#2}::new_internal::{closure#0}::{closure#0}::{closure_env#0}, ()>
             at /home/ejs/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/thread/local.rs:477:20
  33: {closure#0}
             at /home/ejs/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy_tasks-0.18.0/src/task_pool.rs:190:50
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Encountered a panic in system `avian3d::collider_tree::optimization::block_on_optimize_trees`!
Encountered a panic in system `avian3d::schedule::run_physics_schedule`!
Encountered a panic in system `bevy_app::main_schedule::FixedMain::run_fixed_main`!
Encountered a panic in system `bevy_time::fixed::run_fixed_main_schedule`!
Encountered a panic in system `bevy_app::main_schedule::Main::run_main`!

This is the kind of code I use:


# ...
                        if *on {
                            let layers = CollisionLayers::new(LayerMask::NONE, layers.filters);
                            commands.entity(player_ent).insert((
                                layers,
                                RigidBody::Kinematic,
                            ));
                            // commands.entity(player_ent).remove::<RigidBodyColliders>();   // this works around the panic *mostly*
                            cheats.0.insert(*cheat);
                            warn!("Player {} is noclipping", player_ent);
                        } else {
                            commands.entity(player_ent).insert((
                                CollisionLayers::new(GameLayer::Player, [
                                    GameLayer::Default, GameLayer::World,
                                    GameLayer::Projectiles,
                                ]),
                               RigidBody::Dynamic,
                         ));

I can work around this by also removing the RigidBodyColliders component, though not always.

@Jondolf
Copy link
Member Author

Jondolf commented Jan 31, 2026

On main, the order of colliders/bodies is switched -- though I deal with that already -- but the body1/body2 entities are the same as collider1/collider2, given that I have the mesh and colliders on the same entity throughout. Not sure if that's an intentional change.

It's intentional that body1 and body2 are None if the colliders are not attached to a body (i.e. they are "standalone" colliders). This was apparently not the case before, but I believe that was partially an oversight, or maybe done to work around some old problem, I'm not entirely sure. Either way I would personally expect the bodies to be None if there is no actual RigidBody involved. I do need to add this to the migration guide though!

I think there is a new issue on this branch (doesn't exist on main). I have a "noclip" toggle feature, which changes the player's RigidBody::Kinematic to RigidBody::Static. It seems this does not reset state properly, since it results in this panic when the player first intersects world geometry:

Hmm 🤔 I have a test scene that is changing RigidBody type between dynamic, kinematic, and static for some colliding entities, but I'm not getting this panic. Any chance you could get a more minimal reproduction of this? Also make sure you have the latest version with a quick cargo update

@eswartz
Copy link

eswartz commented Jan 31, 2026

It's intentional that body1 and body2 are None if the colliders are not attached to a body (i.e. they are "standalone" colliders). This was apparently not the case before, but I believe that was partially an oversight, or maybe done to work around some old problem, I'm not entirely sure. Either way I would personally expect the bodies to be None if there is no actual RigidBody involved. I do need to add this to the migration guide though!

I see, that makes sense.

Hmm 🤔 I have a test scene that is changing RigidBody type between dynamic, kinematic, and static for some colliding entities, but I'm not getting this panic. Any chance you could get a more minimal reproduction of this? Also make sure you have the latest version with a quick cargo update

I need to update the purported cause -- it apparently has nothing directly to do with swapping RigidBody types. I have seen the same panic a few more times over the past few days without this. IIRC it usually happens when my player mesh/KinematicBody is jumping and landing on the static RigidBody parts of the world. The similarities to the original "noclip" situation are that the player and world penetrate.

I will work on a smaller reproducible case.

@eswartz
Copy link

eswartz commented Jan 31, 2026

OK, I found an easy reproducible, though likely it means painful debugging. The actual cause is, adding and removing colliders during a simulation. I just modified the conveyor_belt example to add two systems spawn and check_despawners to continuously add boxes and despawn the boxes. It leads to the panic pretty quickly.

conveyor_belt_repro.rs.txt

@Jondolf
Copy link
Member Author

Jondolf commented Jan 31, 2026

Thanks, I'll try that! I did also get a repro for panics when changing RigidBody type, it often happens when you switch some dynamic bodies to kinematic or vice versa, but not all:

thread 'main' (77583) panicked at /home/joona/.cargo/git/checkouts/obvhs-910b4198c23e914d/4735d72/src/bvh2/mod.rs:609:35:
index out of bounds: the len is 60 but the index is 89
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Encountered a panic in system `avian2d::collider_tree::update::update_dynamic_kinematic_aabbs<avian2d::collision::collider::parry::Collider>`!
Encountered a panic in system `avian2d::schedule::run_physics_schedule`!
Encountered a panic in system `bevy_app::main_schedule::FixedMain::run_fixed_main`!
Encountered a panic in system `bevy_time::fixed::run_fixed_main_schedule`!
Encountered a panic in system `bevy_app::main_schedule::Main::run_main`!

This is slightly different from the panic you got though. Either way, I'll look into these and see if I can fix them...

Edit: ah this actually looks like the same panic as your new repro

@Jondolf
Copy link
Member Author

Jondolf commented Jan 31, 2026

@eswartz I fixed some issues (this involved DGriffin91/obvhs#9) and it seems to work for me now. Could you try again when you have time, and check if there are any problems remaining? Remember to do a cargo update again :)

@eswartz
Copy link

eswartz commented Jan 31, 2026

@eswartz I fixed some issues (this involved DGriffin91/obvhs#9) and it seems to work for me now. Could you try again when you have time, and check if there are any problems remaining? Remember to do a cargo update again :)

Yup, I saw the commits and the panics are gone for me too. Thanks!

@Jondolf Jondolf removed the S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged label Jan 31, 2026
@Jondolf
Copy link
Member Author

Jondolf commented Feb 2, 2026

Merging! If someone notices unexpected regressions, please open an issue :)

@Jondolf Jondolf merged commit 17d868e into main Feb 2, 2026
6 checks passed
@Jondolf Jondolf deleted the obvhs branch February 2, 2026 14:14
Jondolf added a commit that referenced this pull request Feb 2, 2026
# Objective

#927 broke wasm! It useds async tasks, and apparently you cannot use
`block_on` there.

## Solution

Perform tree optimization synchronously on wasm. I also added a
`use_async_tasks` setting in case you want to disable it even in native
contexts.

## Testing

`bevy run --example bvh web`
Jondolf added a commit that referenced this pull request Feb 7, 2026
# Objective

Fixes #403.

#927 added `ColliderTrees` for the new BVH broad phase. We should reuse
them for spatial queries instead of maintaining and using a separate BVH
from Parry.

## Solution

In short:

- Add more traversal methods on `Bvh2` via extension traits (we should
probably upstream these)
- `sweep_traverse`, `sweep_traverse_miss`, `sweep_traverse_anyhit`, and
`sweep_traverse_dynamic`
  - `squared_distance_traverse` and `squared_distance_traverse_dynamic`
- Add methods for BVH traversal on `ColliderTree`
  - `ray_traverse_closest` and `ray_traverse_all`
  - `sweep_traverse_closest` and `sweep_traverse_all`
  - `squared_distance_traverse_closest`
  - `point_traverse`
  - `aabb_traverse`
- Remove `SpatialQueryPipeline`, and use the `ColliderTrees` traversal
methods for `SpatialQuery`

This involved some other miscellaneous changes:

- Shape casts now returns hits in arbitrary order when `max_hits > 1`,
similar to ray casts.
- `point2` and `normal2` were previously in local space, despite what
the docs state. They are now in world space.

## Testing

Tested different spatial queries in examples.

---

## Showcase

Before, updating the spatial query pipeline was extremely expensive for
large scenes with a lot of colliders:

<img width="263" height="439" alt="Before"
src="https://github.com/user-attachments/assets/0cc11950-cd69-434a-90f6-1fa517d255f9"
/>

(note that the tree optimization cost is partially hidden here, as it is
run in parallel with the spatial query pipeline update)

Now, using the much more optimized `ColliderTrees`, that overhead is
gone:

<img width="263" height="439" alt="After"
src="https://github.com/user-attachments/assets/e39091f1-3a6f-4cfd-b22d-c8ac9c646b5f"
/>

## Future Work

- Generic collider types for spatial queries (#810)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Collision Relates to the broad phase, narrow phase, colliders, or other collision functionality C-Performance Improvements or questions related to performance D-Complex Challenging from a design or technical perspective. Ask for help if you'd like to tackle this! M-Migration-Guide A breaking change to Avian's public API that needs to be noted in a migration guide

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants