How to Perform Spatial Queries Using Ray Casts and Shape Casts in Box3D's Broad-Phase
Box3D's broad-phase accelerates spatial queries by walking three dynamic trees, testing AABB overlaps before invoking shape-specific intersection routines for both ray casts and shape casts.
The erincatto/box3d physics engine provides high-performance spatial query capabilities through its dynamic tree-based broad-phase architecture. Understanding how to perform spatial queries using ray casts and shape casts in Box3D's broad-phase is essential for implementing gameplay mechanics like weapon traces, visibility checks, and predictive collision sweeps.
Ray Cast Queries
Ray casts test intersection between a line segment and the world geometry. Box3D implements this by first pruning candidates using the broad-phase dynamic trees, then performing precise shape-level intersection tests.
World-Level Entry Points
The primary API for ray casting is b3World_CastRay and its convenience variant b3World_CastRayClosest, both defined in src/physics_world.c at lines 2892 and 2973 respectively. These functions accept a ray origin, direction vector, and collision filter, then return intersection results containing the hit fraction, normal, and shape identifier.
Broad-Phase Traversal
When b3World_CastRay executes, it constructs a b3RayCastInput structure containing the origin, direction, and maximum fraction. The function then invokes b3DynamicTree_RayCast on each of the three internal dynamic trees (one per body type).
The callback function RayCastCallback, located around line 2841 in src/physics_world.c, receives every proxy whose AABB overlaps the ray. This early pruning prevents expensive shape intersection tests on distant objects.
Shape-Level Dispatch
Inside RayCastCallback, the engine creates a local copy of the ray input and dispatches to b3RayCastShape at src/shape.c line 794. This dispatcher selects the appropriate intersection routine based on the shape type (sphere, capsule, convex hull, or mesh).
Per-Shape Implementation
Each concrete shape implements its own ray intersection routine:
- Sphere:
b3RayCastSphereinsrc/sphere.cat line 60 - Capsule:
b3RayCastCapsuleinsrc/capsule.cat line 99 - Convex Hull:
b3RayCastHullinsrc/hull.c - Mesh:
b3RayCastMeshinsrc/mesh.c
These routines perform exact ray-versus-shape intersection tests and return the hit fraction and surface normal.
Result Handling
The b3World_CastRay function calls a user-provided callback for every hit encountered along the ray, while b3World_CastRayClosest returns only the nearest intersection. Results include the hit point, surface normal, fraction along the ray, and the b3ShapeId of the colliding shape.
Shape Cast Queries
Shape casts (also called sweep tests or box casts) determine whether a moving shape collides with the world. Unlike ray casts, shape casts account for the volume of the querying geometry.
World-Level Entry Point
Shape casts are exposed via b3World_ShapeCast, implemented at line 3029 in src/physics_world.c. This function uses the WorldShapeCastContext structure and the b3ShapeCastCallback internal callback to process potential intersections.
Broad-Phase Traversal
The world constructs a b3BoxCastInput containing the initial transform, sweep translation, and maximum fraction. It then calls b3DynamicTree_BoxCast on each dynamic tree. The callback receives proxies whose AABBs overlap the swept shape's bounding volume.
Shape-Level Dispatch
The callback creates a local copy of the cast input and forwards it to b3ShapeCastShape at src/shape.c line 830. This dispatcher prepares the cast shape and static shape pair for intersection testing.
Per-Shape Implementation
Each shape type implements a specialized shape-cast routine that reuses the generic b3Shape collision framework:
- Capsule:
b3ShapeCastCapsuleinsrc/capsule.cat line 342 - Convex Hull:
b3ShapeCastHullinsrc/hull.cat line 2616 - Sphere:
b3ShapeCastSphereinsrc/sphere.c
These functions internally create a pair of the static target shape and the moving cast shape, then invoke the appropriate contact generation routine to determine the time of impact.
Result Handling
Shape casts return a b3CastOutput structure containing the hit fraction, contact normal, contact point, and the identifier of the struck shape. A fraction less than 1.0 indicates a collision occurred before the sweep completed.
Broad-Phase Architecture Benefits
Box3D maintains three separate dynamic trees—one for static bodies, one for kinematic bodies, and one for dynamic bodies—implemented in src/dynamic_tree.c and managed by src/broad_phase.c.
The functions b3DynamicTree_RayCast and b3DynamicTree_BoxCast perform conservative AABB tests before invoking expensive per-shape intersection routines. This hierarchical pruning reduces the query complexity from O(n) to O(log n) in sparse worlds, making large simulations feasible.
Additionally, the broad-phase automatically filters queries using categoryBits and maskBits stored in each proxy, ensuring spatial queries respect collision layer configurations without additional user-level filtering.
Practical Implementation Examples
The following examples demonstrate how to perform spatial queries against a Box3D world.
Ray Cast Closest Hit
// Cast a ray downward from (0,5,0) to find the nearest intersection
b3WorldId world = b3World_Create();
b3Pos origin = { .x = 0.0f, .y = 5.0f, .z = 0.0f };
b3Vec3 direction = { .x = 0.0f, .y = -1.0f, .z = 0.0f };
b3QueryFilter filter = {
.maskBits = 0xffffffffu,
.categoryBits = 0xffffffffu
};
b3RayResult hit = b3World_CastRayClosest(world, origin, direction, filter);
if (hit.fraction < 1.0f) {
printf("Hit shape %d at distance %.3f\n", hit.shapeId, hit.fraction);
}
Capsule Shape Cast
// Sweep a capsule downward to detect collision before moving
b3Capsule capsule = { .radius = 0.5f, .halfHeight = 2.0f };
b3ShapeId capsuleId = b3World_CreateCapsuleShape(world, &capsule);
b3ShapeCastInput sweep = {
.shape = capsuleId,
.transform = b3Transform_Translate(b3Pos_zero, (b3Vec3){0,0,0}),
.translation = (b3Vec3){0, -3, 0},
.maxFraction = 1.0f
};
b3CastOutput result = b3World_ShapeCast(world, &sweep, filter, NULL, NULL);
if (result.fraction < 1.0f) {
printf("Capsule hit shape %d after moving %.2f units\n",
result.shapeId,
result.fraction * b3Vec3_Length(sweep.translation));
}
Summary
- Box3D's broad-phase uses three dynamic trees to accelerate spatial queries, separating static, kinematic, and dynamic bodies for optimal traversal.
- Ray casts enter through
b3World_CastRayorb3World_CastRayClosest, traverse trees viab3DynamicTree_RayCast, and dispatch to shape-specific routines likeb3RayCastSphere. - Shape casts use
b3World_ShapeCast, traverse viab3DynamicTree_BoxCast, and delegate to implementations such asb3ShapeCastCapsulefor precise swept-volume tests. - AABB pruning in the broad-phase prevents expensive shape intersection tests on distant objects, maintaining O(log n) query performance.
- Collision filtering is automatically applied using proxy category and mask bits during tree traversal.
Frequently Asked Questions
What is the difference between b3World_CastRay and b3World_CastRayClosest?
b3World_CastRay reports every intersection along the ray to a user-provided callback function, allowing you to process multiple hits. b3World_CastRayClosest returns only the nearest intersection and is optimized for queries where you only need the first collision point, such as ground checks or line-of-sight tests.
How does Box3D filter which objects are tested during spatial queries?
Box3D uses bit masks stored in each broad-phase proxy to filter collisions efficiently. The b3QueryFilter structure contains categoryBits (what layers the query belongs to) and maskBits (what layers the query can hit). During tree traversal in b3DynamicTree_RayCast and b3DynamicTree_BoxCast, the engine skips proxies that fail the bitwise layer check, preventing unnecessary intersection tests against filtered bodies.
Can I perform shape casts with any shape type in Box3D?
Yes, Box3D supports shape casts for all primitive types. Each shape implements a specific routine—such as b3ShapeCastCapsule in src/capsule.c and b3ShapeCastHull in src/hull.c—that handles the swept-volume intersection. The dispatcher b3ShapeCastShape in src/shape.c automatically routes the query to the appropriate implementation based on the shape type identifier.
Why does Box3D use three separate dynamic trees for the broad-phase?
Box3D maintains separate dynamic trees for static, kinematic, and dynamic bodies to optimize query patterns. Static bodies rarely move, allowing their tree to be highly optimized and rarely rebalanced. Dynamic bodies move frequently and require frequent updates. This separation allows ray casts against static geometry to traverse a stable, optimized structure while dynamic objects are handled separately, improving cache coherence and traversal speed.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →