Wide Contacts vs Manifold Constraints in Box3D's Narrow-Phase
Wide contacts batch process collision constraints using SIMD vector lanes for parallel solver execution, while manifold constraints store the precise geometric contact data—normals, penetration depths, and up to four contact points—generated by narrow-phase collision detection.
The erincatto/box3d physics engine employs a dual-representation strategy to balance geometric fidelity with computational throughput. Understanding the distinction between wide contacts and manifold constraints is critical for optimizing collision response, as these structures serve distinct roles in converting raw intersection data into solver-ready constraints.
Manifold Constraints: Geometric Collision Data
Manifold constraints represent the raw output of the narrow-phase collision detection system. When two convex shapes intersect, the engine computes a detailed geometric description of the contact region required for accurate impulse calculation and friction modeling.
The b3Manifold Structure
Defined in src/manifold.h, the b3Manifold structure stores contact geometry in scalar form:
typedef struct b3Manifold {
b3Vec3 normal;
float depth;
int pointCount;
b3Vec3 points[4];
} b3Manifold;
This structure captures the contact normal, penetration depth, and up to four contact points. Each manifold persists in the constraint graph (b3ContactConstraintManifold) across simulation steps, enabling temporal coherence and warm-starting for the solver.
Narrow-Phase Generation
The narrow-phase populates these structures during shape intersection tests. In src/manifold.c, collision functions such as b3ComputeManifoldConvexConvex clip incident faces against reference faces to fill the manifold:
b3Manifold manifold;
b3ComputeManifoldConvexConvex(shapeA, shapeB, &manifold);
Manifolds preserve per-point data in scalar arrays because geometric algorithms require sequential, branching logic that does not vectorize efficiently.
Wide Contacts: SIMD Solver Optimization
Wide contacts transform scalar manifold data into a vectorized format optimized for the constraint solver. This representation allows the engine to process multiple contacts simultaneously using single-instruction-multiple-data (SIMD) operations.
The b3ContactConstraintWide Architecture
Declared in src/solver.h (lines 92-222), the b3ContactConstraintWide structure packs scalar contact values into vector types—typically 4-wide floats—enabling the solver to process four constraints in parallel:
// Block for iterating across wide contacts. For prepare and store.
b3_wideContactBlock,
...
// Flat view of the wide contact constraint array used by prepare and store.
struct b3ContactConstraintWide* wideConstraints;
b3WidePrepareSpan* widePrepareSpans;
int wideContactCount;
Stack Allocation and Block Processing
During the solver step in src/solver.c, wide constraints are allocated as a contiguous array on the physics world's stack:
b3ContactConstraintWide* wideConstraints =
(b3ContactConstraintWide*)b3StackAlloc(&world->stack,
wideContactCount * b3GetWideContactConstraintByteCount(),
"wide contacts");
...
stepContext->wideConstraints = wideConstraints;
The solver treats this array as a flat parallel-for workload using b3InitBlocks and b3_wideContactBlock, maximizing cache locality and minimizing branch divergence. Unlike manifolds, wide contacts are temporary structures cleared after each simulation step.
From Geometry to Execution: The Conversion Pipeline
The transition from geometric manifolds to solver constraints occurs during the prepare phase. The engine extracts data from b3Manifold instances and widens the values into b3ContactConstraintWide structures:
/* Example: converting a manifold into a wide contact constraint */
b3Manifold manifold;
b3CollideConvexConvex(shapeA, shapeB, &manifold);
/* Allocate a wide constraint for the pair */
b3ContactConstraintWide *wide = ctx->wideConstraints + wideIndex;
wide->normal = b3Vec3Widen(manifold.normal);
wide->depth = b3FloatWiden(manifold.depth);
wide->point0 = b3Vec3Widen(manifold.points[0]);
/* … repeat for up to 4 points … */
During the solve phase, the engine processes these blocks in SIMD-friendly loops:
/* Example: processing a wide contact in the solver */
for (int i = 0; i < wideContactCount; ++i) {
b3ContactConstraintWide *c = wideConstraints + i;
b3Vec3 normal = b3Vec3Narrow(c->normal);
float depth = b3FloatNarrow(c->depth);
/* SIMD-friendly impulse solve … */
}
Performance Characteristics and Design Rationale
The coexistence of these structures addresses competing optimization constraints:
- Manifold constraints prioritize geometric accuracy. They store per-point positions in scalar form because collision detection requires precise world-space coordinates and sequential clipping algorithms.
- Wide contacts prioritize execution throughput. By packing data into vector registers, the solver achieves high performance on modern CPUs where SIMD width often matches or exceeds the typical contact point count between simple shapes.
According to the implementation in src/contact_solver.c, the solver consumes both representations: manifolds provide the persistent geometric truth for friction and restitution calculations, while wide contacts provide the cache-friendly layout necessary for impulse resolution at scale.
Summary
- Manifold constraints (
b3Manifold) store precise collision geometry including contact normals, penetration depths, and up to four contact points in scalar form according to src/manifold.h - Wide contacts (
b3ContactConstraintWide) repackage this data into 4-wide SIMD vectors for parallel processing across multiple constraints simultaneously - Manifolds persist in the constraint graph (
b3ContactConstraintManifold) for temporal coherence, while wide contacts are temporary stack allocations created per solver step in src/solver.c - The conversion from manifolds to wide contacts occurs during the prepare/store phase, enabling the contact solver to achieve high throughput via contiguous memory layouts and
b3_wideContactBlockiteration
Frequently Asked Questions
Why does Box3D maintain two separate contact representations?
Box3D uses dual representations because manifolds preserve the exact geometric detail needed for accurate collision response, friction modeling, and warm-starting across frames, while wide contacts provide the SIMD throughput necessary for real-time physics. This separation allows the narrow-phase to focus on geometric correctness using scalar algorithms in src/manifold.c, while the solver optimizes for parallel execution using vectorized blocks defined in src/solver.h.
How many contact points can a manifold store?
Each b3Manifold structure defined in src/manifold.h can store up to four contact points (b3Vec3 points[4]). This accommodates the maximum contact count typically generated between intersecting convex shapes, such as face-face contacts between boxes or box-cylinder collisions.
Where are wide contacts allocated during the simulation step?
Wide contacts are allocated on the physics world's stack in src/solver.c using b3StackAlloc. They exist only for the duration of the solver step and are automatically cleared afterward, unlike manifold constraints which persist in the constraint graph across simulation steps for temporal coherence.
Can the solver process different collision types simultaneously using wide contacts?
Yes. The b3ContactConstraintWide structure uses vectorized data types that allow the solver to process four contact constraints simultaneously regardless of the underlying shape types. The b3_wideContactBlock iteration system groups constraints into contiguous blocks, maximizing SIMD lane utilization even when processing heterogeneous collision pairs (e.g., sphere-box and cylinder-cylinder contacts) within the same parallel-for loop.
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 →