How Box3D Handles Height Field Collision for Terrain in Games
Box3D treats terrain as a compressed grid of height samples, generating collision triangles on-demand during narrow-phase queries while reusing the engine's mesh-contact pipeline for static geometry.
Box3D, the 3D physics engine maintained by Erin Catto, implements height field collision through a specialized static shape that balances memory efficiency with robust collision detection. Instead of storing explicit triangles, the engine keeps terrain data in a compact grid structure and defers geometry instantiation until the narrow-phase demands it. This approach allows game developers to create expansive terrains that interact reliably with dynamic rigid bodies without the memory overhead of full triangle meshes.
Height Field Data Structure and Compression
The foundation of Box3D’s terrain system is the b3HeightFieldData structure defined in src/height_field.c. This container stores terrain as a regular grid with the following key properties:
- Grid dimensions:
rowCountandcolumnCountdefine the resolution, whilescaleXandscaleZspecify the horizontal spacing between samples. - Compressed storage: Heights are stored as integers in the
compressedHeightsarray and reconstructed to floating-point values usingminHeightandheightScaleoffsets. - Optional attributes: Per-cell material IDs and flag bits enable surface-specific properties such as friction and collision filtering.
This compression scheme drastically reduces memory footprint for large worlds compared to raw floating-point vertex buffers.
Shape Registration and Static Body Constraints
Height fields can only be attached to static bodies, enforced by validation logic in src/shape.c (lines 276–278). The creation flow proceeds as follows:
- Define a
b3HeightFieldDefstructure populated with grid dimensions, scale factors, and the compressed height array. - Call
b3CreateHeightFieldShape, which instantiates ab3Shapewithtypeset tob3_heightShapeand stores a pointer to theb3HeightFieldData.
Attempting to attach this shape to a dynamic or kinematic body triggers an assertion failure, ensuring terrain remains immovable in the simulation.
Broad-Phase Optimization
For efficient culling, Box3D computes an axis-aligned bounding box on-the-fly via b3ComputeHeightFieldAABB. This function is invoked from b3ShapeAABB in src/shape.c (lines 574–576) and returns an AABB that encloses the entire grid from the minimum to maximum reconstructed heights. The broad-phase uses this bounding volume to quickly eliminate non-intersecting pairs before expensive narrow-phase work begins.
Narrow-Phase Triangle Generation
When a query reaches the narrow-phase, Box3D avoids building a permanent triangle mesh. Instead, it employs lazy evaluation through two key functions in src/mesh.c:
b3QueryHeightField(lines 73–79): Enumerates grid cells whose bounding boxes intersect the query region.b3GetHeightFieldTriangle(lines 591–596): Generates the two triangles for a specific cell on-demand using the reconstructed heights.
These transient triangles are fed into the generic mesh-contact code in src/mesh_contact.c, which calculates contact points, normals, and penetration depths using the same algorithms applied to static triangle meshes. This design keeps memory usage constant regardless of query complexity.
Collision Query Implementations
Box3D supports three primary query types against height fields, each optimized to walk the grid efficiently:
Ray Casting
The b3RayCastHeightField function in src/height_field.c (lines 594–608) performs a 2-D Digital Differential Analyzer (DDA) traversal through the grid. It identifies intersected cells and tests the ray against the two constituent triangles, returning the nearest hit fraction and surface normal.
Shape Casting and Overlap Testing
b3ShapeCastHeightField(lines 856–862 insrc/shape.c): Sweeps a convex shape through the terrain by sampling cells along the sweep path and testing against generated triangles.b3OverlapHeightField(lines 891–894 insrc/shape.c): Performs broad-phase-aware overlap tests by querying cells intersecting the probe AABB and checking each triangle for intersection.
Contact Resolution and Surface Properties
Once the narrow-phase identifies contacts, the solver in src/contact_solver.c processes them identically to standard mesh collisions, applying impulse resolution, friction, and restitution. For per-cell material variation, Box3D provides b3GetHeightFieldMaterial (accessed in src/shape.c line 2390), allowing distinct surface properties (e.g., low friction for ice, high friction for rock) based on the cell index.
Practical Implementation Example
The following example demonstrates creating a 10×10 terrain, attaching it to a static body, and performing a vertical ray-cast:
/* 1. Define the height field */
int rowCount = 10, columnCount = 10;
float heights[100] = { /* ... height samples ... */ };
b3HeightFieldDef hfDef = {0};
hfDef.rowCount = rowCount;
hfDef.columnCount = columnCount;
hfDef.heights = heights;
hfDef.minHeight = 0.0f;
hfDef.heightScale = 0.2f; // Each integer step = 0.2 meters
hfDef.scaleX = 1.0f;
hfDef.scaleZ = 1.0f;
/* 2. Create static body with height field shape */
b3BodyId ground = b3CreateBody(world, &(b3BodyDef){ .type = b3_staticBody });
b3ShapeId terrain = b3CreateHeightFieldShape(ground,
&(b3ShapeDef){ .friction = 0.6f },
&hfDef);
/* 3. Ray-cast against the terrain */
b3RayCastInput input = {
.origin = {0.0f, 10.0f, 0.0f},
.direction = {0.0f, -1.0f, 0.0f},
.maxFraction = 1.0f
};
b3CastOutput output = b3RayCastHeightField(&hfDef, &input);
if (output.hit) {
printf("Hit at fraction %f, normal (%.2f, %.2f, %.2f)\n",
output.fraction,
output.normal.x, output.normal.y, output.normal.z);
}
Summary
- Compact storage: Box3D stores terrain in
b3HeightFieldDatausing compressed integers rather than full vertex buffers, minimizing memory for large worlds. - Static-only restriction: Height field shapes are strictly validated for static bodies in
src/shape.c, ensuring immovable terrain. - On-demand geometry: The engine generates collision triangles lazily via
b3GetHeightFieldTriangleonly when queried, avoiding preprocessing overhead. - Unified pipeline: Height field collisions flow through the same
src/mesh_contact.cnarrow-phase and contact solver used for static meshes, ensuring consistent physics behavior. - Material support: Per-cell surface properties are accessible via
b3GetHeightFieldMaterial, enabling diverse terrain interactions.
Frequently Asked Questions
Can height fields be attached to dynamic bodies in Box3D?
No. The shape creation logic in src/shape.c explicitly validates that height fields are attached only to static bodies. Attempting to use them with dynamic or kinematic bodies will trigger an assertion error, as the engine assumes terrain remains fixed in world space.
How does Box3D minimize memory usage for large terrains?
Box3D compresses height data into integer arrays using minHeight and heightScale parameters for reconstruction. Additionally, it avoids storing explicit triangle meshes; instead, b3QueryHeightField generates triangles on-the-fly during collision queries, keeping memory overhead proportional to grid resolution rather than triangle count.
What collision queries are supported against height fields?
Box3D supports ray casts via b3RayCastHeightField, shape casts via b3ShapeCastHeightField, and overlap tests via b3OverlapHeightField. All three use efficient grid-traversal algorithms (DDA for rays) to test only relevant cells rather than the entire terrain.
How are different terrain materials handled in collision response?
Each cell can store a material ID within the b3HeightFieldData structure. During contact generation, b3GetHeightFieldMaterial (referenced in src/shape.c) retrieves the specific material for the colliding triangle, allowing per-cell customization of friction and restitution properties (e.g., slippery ice versus rough gravel).
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 →