How Box3D's Soft Step Solver Ensures Stable Stacking Without Iterative Divergence

Box3D prevents the "exploding Jenga stack" phenomenon by treating contact constraints as damped springs through the b3Softness parameters—biasRate, massScale, and impulseScale—which limit corrective impulses and decay accumulated energy across substeps.

The erincatto/box3d physics engine solves the classic rigid body stacking instability—where tall towers of objects diverge numerically and explode—through a novel Soft Step contact solver. Unlike traditional impulse-based methods that inject aggressive corrective velocities when resolving penetration, Box3D's approach blends speculative separation bias with soft penetration constraints. This architecture maintains stable stacking even under high mass ratios and deep contact manifolds.

The Three Softness Parameters

At the core of the stability mechanism lies the b3Softness struct, which packs three coefficients that convert hard constraints into compliant springs:

  • biasRate – Controls how much positional error (penetration) feeds into the velocity correction
  • massScale – Scales the effective mass of the contact, preventing large impulse responses
  • impulseScale – Damps the accumulated impulse from previous iterations, acting as a velocity-proportional damper

During the preparation phase in src/contact_solver.c, the solver assigns softness to each contact constraint. In b3PrepareContacts_Mesh, the code selects between global contact softness and a stiffer variant for static bodies:

contactConstraint->softness = (contact->flags & b3_contactStaticFlag) 
    ? context->staticSoftness 
    : context->contactSoftness;

Reference: src/contact_solver.c lines 161-162

Static contacts receive staticSoftness with a higher biasRate and lower massScale, ensuring ground bodies remain immovable and eliminating the "pushing-through-floor" failure mode.

Dual Bias Strategy: Speculative vs. Soft

The solver distinguishes between separating contacts and penetrating contacts, applying different bias strategies to each to prevent divergence.

Speculative Bias for Separation (s > 0)

When the current separation s is positive—meaning the bodies are not penetrating—the solver applies a speculative bias proportional to the inverse time step:

velocityBias = s * inv_h;

Reference: src/contact_solver.c lines 447-450

This soft bias prevents tunneling and drift without injecting large corrective impulses. By scaling with the time step, it keeps the velocity prediction modest and stable.

Soft Bias for Penetration (s ≤ 0)

When penetration occurs (s ≤ 0) and the solver operates in bias mode (useBias == true), the solver computes a soft bias using the compliance parameters:

velocityBias = max(softness.massScale * softness.biasRate * s, -contactSpeed);
massScale    = softness.massScale;
impulseScale = softness.impulseScale;

Reference: src/contact_solver.c lines 452-456

The bias is explicitly limited by contactSpeed and scaled by massScale, ensuring the corrective velocity never overshoots. This transforms a hard inequality constraint into a damped spring that naturally converges.

The Iteration Flow in b3SolveContacts_Mesh

The Soft Step solver follows a disciplined six-phase flow that prevents impulse explosions through progressive damping:

  1. Prepare contacts – Create b3ContactConstraint objects and assign b3Softness from the world context. Static contacts receive staticSoftness for additional rigidity.

  2. Compute separation – Evaluate the current distance between anchors, incorporating the pre-computed baseSeparation:

    // From lines 438-442
    float s = b3Dot(cp->normal, pB - pA) + cp->baseSeparation;
  3. Select bias strategy – Choose between speculative bias (s > 0) or soft bias (s ≤ 0) based on the separation sign.

  4. Solve normal impulse – Update impulses using a damped formulation that blends relative velocity, bias, and historical impulse decay:

    float deltaImpulse = -cp->normalMass * 
        (massScale * vn + velocityBias) - 
        impulseScale * cp->normalImpulse;

    Reference: src/contact_solver.c lines 664-666

  5. Clamp and apply – Enforce non-negativity constraints while preserving the damping characteristics:

    float newImpulse = max(cp->normalImpulse + deltaImpulse, 0.0f);
    deltaImpulse = newImpulse - cp->normalImpulse;
    // Apply to linear/angular velocities...

    Reference: src/contact_solver.c lines 667-672

  6. Solve friction – Compute tangential impulses (central friction, twist friction, and rolling resistance) after the normal solve, sharing the same softness framework to prevent tangential destabilization. Reference: src/contact_solver.c lines 891-911

Why This Prevents Iterative Divergence

The Soft Step solver guarantees stability through three mathematical mechanisms that work in concert:

Damped Spring Behavior – The combination of biasRate (spring stiffness) and massScale (effective mass reduction) models each contact as a damped spring. Such systems naturally converge to equilibrium without the oscillations that plague hard constraint solvers.

Impulse Decay – The impulseScale term reduces the contribution of accumulated impulses each iteration (deltaImpulse -= impulseScale * cp->normalImpulse). This acts as an energy dissipation mechanism that prevents the exponential growth of correction terms across substeps.

Static Contact Stiffening – By applying staticSoftness to ground contacts, the solver ensures immovable bodies truly remain static. This eliminates the feedback loop where dynamic bodies push static bodies, causing the entire stack to drift and diverge.

Implementation Example

To configure the Soft Step solver in your simulation, adjust the world softness parameters before stepping:

// Configure world softness in src/physics_world.c via b3MakeSoft
b3World* world = b3CreateWorld();
world->contactHertz = 30.0f;        // Frequency for dynamic contacts
world->contactDampingRatio = 0.7f;  // Damping ratio (0 = undamped, 1 = critical)

// Step the simulation - solver automatically applies soft-step logic
float dt = 1.0f / 60.0f;
b3Step(world, dt);

Reference: src/physics_world.c lines 1120-1123

For specific contacts requiring custom compliance, override the default softness:

b3Contact* contact = b3GetContact(world, contactId);
if (contact) {
    // Stiffer contacts for critical stability points
    contact->softness = b3MakeSoft(60.0f, 0.9f, world->stepContext.h);
}

Summary

  • Box3D's Soft Step solver treats contacts as damped springs using the b3Softness struct (biasRate, massScale, impulseScale) to limit impulse magnitudes.
  • Dual bias strategy applies speculative bias for separating contacts and soft bias for penetration, preventing aggressive velocity injections.
  • Impulse damping via impulseScale decays accumulated corrections across iterations, eliminating the energy growth that causes divergence.
  • Static stiffening assigns higher compliance to ground contacts, ensuring immovable anchors remain stable during deep stacking.
  • The implementation in src/contact_solver.c (b3PrepareContacts_Mesh and b3SolveContacts_Mesh) enables stable Jenga-style stacks without iterative explosions.

Frequently Asked Questions

What causes iterative divergence in physics solvers?

Iterative divergence occurs when constraint solvers inject increasingly large corrective impulses to resolve penetration errors, creating a positive feedback loop. In tall stacks, small errors at the bottom compound as the solver propagates corrections upward, eventually producing velocity explosions that numerically "explode" the simulation. Box3D's Soft Step solver prevents this by capping impulse magnitudes through mass scaling and damping historical contributions.

How does the b3Softness struct prevent impulse explosions?

The b3Softness struct converts rigid constraints into compliant contacts. By scaling the effective mass (massScale) and damping previous impulses (impulseScale), it ensures that each iteration adds only a fraction of the corrective force needed. This creates a convergent series where impulses decay geometrically rather than growing exponentially, as implemented in the deltaImpulse calculation at lines 664-666 of src/contact_solver.c.

What is the difference between speculative bias and soft bias?

Speculative bias applies when bodies are separating (s > 0), calculating velocityBias = s * inv_h to prevent future tunneling without harsh corrections. Soft bias applies during penetration (s ≤ 0), using velocityBias = max(softness.massScale * softness.biasRate * s, -contactSpeed) to limit the correction velocity. The speculative approach keeps separating objects stable, while the soft approach gently pushes penetrating objects apart without overshoot.

When should I use staticSoftness versus contactSoftness?

Use staticSoftness for contacts involving immovable bodies (ground, walls) to prevent dynamic objects from pushing through static geometry—this uses higher stiffness and lower mass scaling. Use contactSoftness for dynamic-dynamic interactions where some compliance prevents jitter. The solver automatically selects the appropriate softness in b3PrepareContacts_Mesh based on the b3_contactStaticFlag, ensuring optimal stability for each contact type.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →