-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Improve frustum culling of skinned meshes through per-joint bounds #21837
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
base: main
Are you sure you want to change the base?
Conversation
…ional. This is arguably unnecessary as the `Mesh3d` component currently requires a transform, but maybe that changes in future.
…nent + asset. So `SkinnedMeshBounds` is now a component with a `Handle<SkinnedMeshBoundsAsset>`. This is annoying, but fixes render-world-only meshes breaking, and means `update_skinned_mesh_bounds` can efficiently ignore skinned meshes without skinned bounds.
…added bounds rendering for testing, but might remove it later.
…joint indices rather than keeping them separate. This is a little bit wasteful (two bytes per joint), but much simpler.
…e `SkinnedMeshBoundsAsset::from_mesh`.
|
I very much like the approach presented here and think it's a very reasonable default. The policy enum should be expanded to include @pcwalton's approach: don't generate any AABB, but keep frustum culling, since all AABBs will be artist-authored and inserted manually |
|
Oh nice, I just filed #21845 for the rest pose approach. |
I'd like to consider some different approaches: enum GltfSkinnedMeshBoundsPolicy {
...
/// Skinned meshes are assigned this `Aabb` component.
Fixed { aabb: Aabb },
/// Skinned meshes are assigned a `SkinnedMeshBounds` component, which will
/// be used by the `bevy_camera` plugin to update the `Aabb` component.
///
/// The `Aabb` component will be updated to enclose the given AABB relative to the named joint.
/// If the joint name is `None`, the AABB is relative to the root joint.
FixedJointRelative { aabb: Aabb3d, joint_name: Option<String> },
}So I'm not sure if I'll try adding these options in this PR as it's already a bit chonky. |
|
awesome work, really happy to see a solution to such a long-standing issue.
|
I briefly looked at this, but concluded that joint transforms being affine kinda spoils things - bounding spheres would end up only slightly cheaper. But I didn't work it through in detail or benchmark. There could also be an option where scale is ignored and the bounding sphere is always at zero in joint-space. That would be significantly cheaper but requires a sophisticated user to understand the risks.
Sounds good to me. I think I'd end up adding a
The catch there is that multiple meshes can bind to one joint, so whatever's setting up the joints has to be careful to track the exact set of meshes that are bound to each one and merge the AABBs. This is fairly straightforward if done once in the glTF importer and the user leaves it untouched. But rapidly gets messy if the user starts changing stuff - e.g. swapping clothes/accessories on a character. |
|
So my current state is:
|
|
if you're eager or 21732 stalls you could go ahead with the mesh embedding knowing it will only work for MAIN_WORLD assets initially. i can amend that to store the joint bounds as well if this lands first. |
…hat bevyengine#21732 or similar will land, so we don't lose the bounds if the mesh doesn't have `RenderAssetUsages::MAIN_WORLD`.
…ark of `transform_aabb` this is 1.1x faster on SSE, 1.3x on AVX2 with FMA.
…unds` to return any errors.
Objective
Mostly fix #4971 by adding a new option for updating skinned mesh
Aabbcomponents from joint transforms.skinned_bounds_pr.mp4
This fixes cases where vertex positions are only modified through skinning. It doesn't fix other cases like morph targets and vertex shaders.
The PR kind of upstreams
bevy_mod_skinned_aabb, but with some changes to make it simpler and more reliable.Why A Draft?
I'm fishing for design feedback and any other concerns before I finish up the PR. The current state is functional and mostly tested. The remaining work is documentation, error handling, final testing and release notes. There also needs to be a decision on whether the new option is enabled by default in the glTF importer.
Background
If a main world entity has a
Mesh3dcomponent then it's automatically assigned anAabbcomponent. This is done bybevy_cameraorbevy_gltf. TheAabbis used bybevy_camerafor frustum culling. It can also be used bybevy_pickingas an optimization, and by third party crates.But there's a problem - the
Aabbcan be wrong if something changes the mesh's vertex positions after theAabbis calculated. The most common culprits are skinning or morph targets. Vertex positions can also be changed by mutating theMeshasset (#4294), or by vertex shaders.The most common solution has been to disable frustum culling via the
NoFrustumCullingcomponent. This is simple, and might even be the most efficient approach for apps where meshes tend to stay on-screen. But it's annoying to implement, bad for apps where meshes are often off-screen, and it only fixes frustum culling - it doesn't help other systems that use theAabb.Solution
This PR adds a reliable and reasonably efficient option for updating the
Aabbof a skinned mesh from its animated joint transforms. See the "How does it work" section for more detail.The glTF loader can add skinned bounds automatically if a new
GltfSkinnedMeshBoundsPolicysetting is enabled inGltfPluginorGltfLoaderSettings:This option is currently enabled by default, although that's open to debate.
For non-glTF cases, the user must ask
Meshto generate the skinned bounds, and then add theDynamicSkinnedMeshBoundsmarker component to their mesh entity.See the
custom_skinned_meshexample for real code.There's two downsides to the current approach:
RenderAssetUsages::RENDER_WORLD. I'm assuming that Retain asset without data for RENDER_WORLD-only assets #21732 will fix this.Bonus Features
GltfSkinnedMeshBoundsPolicy::NoFrustumCullingThis is a convenience for users who prefer the
NoFrustumCullingworkaround, but want to avoid the hassle of adding it after a glTF scene has been spawned.Note that PR #21845 is also adding an option related to skinned mesh bounds. I'm fine if that PR lands first - I'll update this PR to include the option.
Gizmos
bevy_gizmos::SkinnedMeshBoundsGizmoPlugincan draw the per-joint AABBs.The name is debatable. It's not technically drawing the bounds of the skinned mesh - it's drawing the per-joint bounds that contribute to the bounds of the skinned mesh.
Testing
I also hacked
custom_skinned_meshto simulate awkward cases like rotated and off-screen entities.How Does It Work?
Click to expand
Summary
Mesh::generated_skinned_mesh_boundscalculates an AABB for each joint in the mesh - the AABB encloses all the vertices skinned to that joint. Then every frame,bevy_camera::update_skinned_mesh_boundsuses the current joint transforms to calculate anAabbthat encloses all the joint AABBs.This approach is reliable, in that the final
Aabbwill always enclose the skinned vertices. But it can be larger than necessary. In practice it's tight enough to be useful, and rarely more than 50% bigger.This approach works even with non-rigid transforms and soft skinning. If there's any doubt then I can add more detail.
Awkward Bits
There's a few issues that stop the solution being as simple and efficient as it could be.
Problem 1: Joint transforms are world-space,
Aabbis entity-space.Aabb, but that's not possible.Aabbis directly calculated in entity-space.Aabbin world-space and then transforms it to entity-space.Aabb.Problem 2: Joint AABBs are in a surprising(?) space.
MeshandSkinnedMeshInverseBindposes- and those assets can be mixed and matched.SkinnedMeshBoundsAssetfor each combination ofMeshandSkinnedMeshInverseBindposes.bevy_mod_skinned_aabbuses this approach - it's slow and fragile.)Meshasset.Pseudo-Code
Here's a pseudo-code version of the update so we can compare against other options.
Future Options
For frustum culling there's a cheeky way to optimize and simplify skinned bounds - put frustum culling in the renderer and calculate a world-space AABB during
extract_skins. It's a good fit because skin extraction has already grabbed the joint transform and calculatedworld_from_mesh. I estimate this would make skinned bounds 3x faster.Another option is to change main world frustum culling to use a world-space AABB. So there would be a new
GlobalAabbcomponent that gets updated each frame fromAabband the entity transform (which is basically the same as transform propagation and the relationship betweenTransformandGlobalTransform). This has some advantages and disadvantages but I won't get into them here - I think putting frustum culling into the renderer is a better option.(Note that putting frustum culling into the renderer doesn't mean removing the current main world visibility system - it just means the main world system would be separate opt-in system)
Performance
Click to expand
Initialization
Creating the skinned bounds asset for
Fox.glb(576 verts, 22 skinned joints) takes 0.03ms. Loading the whole glTF takes 8.7ms, so this is a <1% increase.Per-Frame
The
many_foxesexample has 1000 skinned meshes, each with 22 skinned joints. Updating the skinned bounds takes 0.086ms. This is a throughput of roughly 250,000 joints per millisecond, using two threads.The whole animation update takes 3.67ms (where "animation update" = advancing players + graph evaluation + transform propagation). So we can kinda sorta claim that this PR increases the cost of skinned animation by roughly 3%. But that's very hand-wavey and situation dependent.
This was tested on an AMD Ryzen 7900 but with
TaskPoolOptions::with_num_threads(6)to simulate a lower spec CPU. Comparing against a few other threading options:So the parallel iterator is better but quickly hits diminishing returns as the number of threads increases.
Future Options
The "How Does It Work" section mentions moving skinned mesh bounds into the renderer's skin extraction. Based on some microbenchmarks, I estimate this would reduce non-parallel
many_foxesfrom 0.141ms to 0.049ms, so roughly 3x faster. Requiring AVX2 (to enable broadcast loads) or pre-splatting (to fake broadcast loads for SSE) would knock off another 25%. And fancier SIMD approaches could do better again.There's also approaches that trade reliability for performance. For character rigs, an effective optimization is to fold face and finger joints into a single bound on the head and hand joints. This can reduce the number of joints required by 50-80%. Another option is to use bounding spheres instead of AABBs.
FAQ
Click to expand
Should the glTF importer enable this feature by default?
I think so. Bugs caused by skinned mesh culling have been a regular pain for both new and experienced users. I'm guessing most people would pay the CPU cost to have skinned meshes Just Work(tm). Sophisticated users in search of performance can choose to disable the option and use a fixed AABB.
Why can't it be automatically added to any mesh? Then the glTF importer and custom mesh generators wouldn't need special logic.
bevy_mod_skinned_aabbtook the automatic approach, and I don't think the outcome was good. It needs some surprisingly fiddly and fragile logic to decide when an entity has the right combination of assets in the right loaded state. And it can never work withRenderAssetUsages::RENDER_WORLD.So this PR takes a more modest and manual approach. I think there's plenty of scope to generalise and automate as the asset pipeline matures. If the glTF importer becomes a purer glTF -> BSN transform, then adding skinned bounds could be a general scene/asset transform that's shared with other importers and custom mesh generators.
Why is the data in
Mesh? Shouldn't it go inSkinnedMeshorSkinnedMeshInverseBindposes?That might seem intuitive, but it wouldn't work in practice - the data is derived from
Meshalone.SkinnedMeshdoesn't work because it's per mesh instance, so the data would be duplicated.SkinnedMeshInverseBindposesdoesn't work because it can be shared between multiple meshes.The names are a bit misleading -
Meshcontains the per-vertex skinning data, whileSkinnedMeshandSkinnedMeshInverseBindposesare more like joint bindings one step removed from the vertex data.Why not put the bounds on the joint entities?
This is surprisingly tricky in practice because multiple meshes can be bound to the same joint entity. So there would need to be logic that tracks the bindings and updates the bounds as meshes are added and removed.
Why
SkinnedMeshBounds? Shouldn't it beSkinnedMeshAabbs?Playing it safe in case things change, e.g. bounding spheres might become an an option.
Why is the bounds update system in
bevy_camera? Shouldn't it be inbevy_mesh?bevy_camerais the owner and main user ofAabb, and already has some mesh related logic (calculate_boundsautomatically adds anAabbto mesh entities). So putting it inbevy_camerais consistent with the current structure, but that could change.What Do Other Engines Do?
Click to expand
An approach that's been proposed several times for Bevy is copying Unity's "fixed AABB from animation poses". I think this is more complicated and less reliable than many people expect. More complicated because linking animations to meshes can often be difficult. Less reliable because it doesn't account for ragdolls and procedural animation. But it could still be viable for for simple cases like a single self-contained glTF with basic animation.