Ray Casting for Primitive Shapes #15724
Draft
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Objective
Add ray casting support for Bevy's primitive 2D and 3D shapes.
The scope here is to support the following:
These should be supported for all of Bevy's primitive shapes, except where unreasonable, such as for 3D lines, which are infinitely thin.
The following are not in scope here:
The goal is purely to provide the core tools and implementations for performing efficient ray casts on individual shapes. Abstractions can be built on top by users and third party crates, and eventually Bevy itself once it has first-party colliders and physics.
Solution
Add
PrimitiveRayCast2d
andPrimitiveRayCast3d
traits with the following methods:where
RayHit2d
looks like this:Usage then looks like this:
Names are open for bikeshedding. I chose
PrimitiveRayCastNd
because we already haveRayCastNd
structs, and these traits are intended to return only the most minimal data required to represent an intersection and its geometry efficiently. Other APIs could be built on top to return more data if desired.Let's go over a few relevant features and implementation details.
Solid and Hollow Shapes
The ray casting methods (excluding intersection tests) have a
solid
boolean argument. It controls how rays cast from the interior of a shape behave. Iftrue
, the ray cast will terminate with a distance of zero as soon as the ray origin is detected to be inside of the shape. Otherwise, the ray will travel until it hits the boundary.This feature has somewhat unclear utility. One valid use case is determining how far a shape extends in some given direction, which could be used to figure out e.g. how far away an object picked up by the player should be held. Or maybe you have a circular room, and want to cast rays against its walls from the inside without discretizing the circle to be formed out of multiple separate shapes.
Some hollow shapes can actually be handled in most cases without this built-in support, by simply performing another ray cast in the opposite direction from outside the shape if the ray origin is detected to be inside of the shape. However, for shapes like annuli and tori, the amount by which to offset the ray origin isn't obvious, and doing two ray casts is also more expensive than just having built-in support.
For prior art, Parry has the same boolean argument, and Box2D also supports hollow shapes, although in Box2D's case these are just handled by using chain (i.e. polyline) shapes.
Local and World Space Ray Casts
Each method has a local and world space variant. Practically all ray casts should be performed in local space, since it makes the algorithms significantly more straightforward.
The world-space versions are primarily a user-facing abstraction, which actually just transforms the ray into local space and then transform the results back to world space.
Discussion
Do we want this yet?
Bevy doesn't have built-in physics or collision detection yet. Should we have something like ray casting?
I believe having ray casting support for Bevy's primitive shapes could be amazing for users, even without first-party colliders. So far, the answer to "how do I do ray casts?" has basically been "try to use Parry, or just use Avian or bevy_rapier for nicer ergonomics and to avoid having to deal with Nalgebra". While this PR doesn't add APIs or tools to cast rays into the world like physics engines do, it does provide a foundation on top of which both users and crates could build their own lightweight APIs. We could even extend this to perform ray casts on meshes in the future (and as part of a mesh picking backend).
I don't think we should necessarily build an entire collision detection and geometry library in the background and upstream it all at once, but rather build it out incrementally in logical chunks. I think ray casting is a perfect candidate for this. This could be released in a third party crate too, but I think it's something that could have immediate upstream value to users.
Does it make sense to have this in
bevy_math
?We don't have a better place yet. I expect us to add more similar queries (like point queries) over time. I think we should add this in
bevy_math
for now, and split things out as we reach critical mass for geometry functionality.Naming
One potentially contentious part is the naming. Should we go for
RayCast
orRaycast
?If we do a little statistical research on popular physics and game engines for prior art (only ones I found clearly):
RayCast
RayCast
RayCast
RayCast
RayCast
Raycast
Raycast
Raycast
There is no clear consensus, but in my experience, I've seen
RayCast
a lot more. That may be because I've seen Box2D, Rapier, and Godot more however. Many other people may be more familiar withRaycast
because of Unity and its popularity.Personally, I prefer
RayCast
especially in the context of other types of casts existing. In my opinion, shape casts look more strange when combined as one word,Shapecast
, as opposed toShapeCast
. Bevy will eventually have shape casts as well, and I think we should be consistent in naming there.It is also worth noting that the usage of "ray cast" vs. "raycast" vs. "ray-cast" is wildly inconsistent in many engines and literature, even if code uses one form consistently. Godot's docs have all three forms on one page, and so does Wikipedia. I would personally prefer if we followed Box2D's example here: code uses
RayCast
andShapeCast
, and documentation also uses this naming consistently as "ray cast" and "shape cast".Split into smaller PRs
This is a very large PR with quite a lot of math-heavy code. I could split this up into several smaller PRs if it would help reviewers.
Maybe something like:
Circle
,Sphere
,Ellipse
, andAnnulus
Arc2d
,CircularSector
, andCircularSegment
Rectangle
andCuboid
Capsule2d
andCapsule3d
Line2d
andSegment2d
Triangle2d
,Rhombus
, polygons, and polylinesTriangle3d
Tetrahedron
Cone
,ConicalFrustum
, andCylinder
Torus
That would be 11 PRs 😬
Or, I can just have this one mega PR. I'm fine with whatever reviewers would prefer.
Testing
Every primitive shape that supports ray casting has basic tests for various different cases like outside hits, inside hits, and missed hits. The shared world-space versions of the methods also have basic tests.
There are also new
ray_cast_2d
andray_cast_3d
examples to showcase ray casting for the various primitive shapes. These function as good hands-on tests. You can see videos of these in the "Showcase" section.Performance
Below are mean times for 100,000 ray casts, with randomized shape definitions (within specified ranges) and rays. I am using the Parry collision detection library for comparison, as it is currently the most popular and feature-complete option in the Rust ecosystem. Pay attention to whether units are microseconds or milliseconds!
Shapes marked as "-" don't have a built-in ray casting implementation in Parry, and would require using e.g. convex hulls or compound shapes.
2D:
3D:
As you can see, all of our implementations outperform those found in Parry, and we also have many more supported shapes. Of course Parry also has its own shapes that we don't have yet, like triangle meshes, heightfields, and half-spaces.
Why so much faster? For shapes like capsules, cylinders, and cones, Parry actually doesn't have custom analytic solutions. Instead, it uses a form of GJK-based ray casting. This works for arbitrary shapes with support maps, but is less efficient and robust than the analytic solutions I implemented. Sometimes Parry's approach even completely misses ray hits for shapes like capsules because of the algorithm failing to converge with the way it's configured in Parry.
For other shapes like balls and cuboids, it's harder to say, but I did find several simplifications and ways to make CPU go brrr in comparison to Parry, and spent time micro-optimizing common shapes like
Triangle3d
in particular. It could also just be that Glam is simply faster in some cases here.Showcase
Below are the new
ray_cast_2d
andray_cast_3d
examples. Keep in mind that this does not involve mesh picking of any kind: each object here stores a component that stores the underlying primitive shape, and then a system performs ray casts.ray_cast_2d.mp4
ray_cast_3d.mp4
Acknowledgments
Thank you @Olle-Lukowski for creating initial versions of a lot of the 2D ray casting implementations in bevy_contrib_raycast! They worked wonderfully as a base to build on top of.