Tight-Fit Assembly in Newton: Solvers & Tunnelling

Bring-up and solver investigation for two contact-rich insertion tasks — a gear-on-shaft assembly and a DisplayPort cable insertion — on a Flexiv Rizon 4s + Grav gripper in Isaac Lab / Newton. The goal is to run both tasks at a much larger time step than the current 2 kHz soft-contact regime by moving to Newton's constraint-based solvers (SolverVBD, SolverKamino), while quantifying throughput and detecting tunnelling before committing to a two-day training run.

Generated 2026-07-28 · Newton 1.4.0.dev0 (ships SolverVBD + SolverKamino) · Isaac Lab worktree newton/gear-dp-combined · NVIDIA RTX PRO 6000 Blackwell (MIG 1g.24 GB) · runs are Kit-free (no Isaac Sim).

● Live — updated as the investigation progresses
Tasks combined
2
gear + DisplayPort, one branch
Solvers selectable
5
MuJoCo · hydroelastic · XPBD · VBD · Kamino
Isolated gear train, exact meshes
0.1 %
slip vs the tooth-count ratio
Same train, convex-hull colliders
100 %
slip — what the training preset simulates
Headline (2026-07-28): the gears were never being simulated as gears. An isolated three-gear rig — no robot, no task, the task's own meshes — meshes and transmits torque at the exact tooth-count ratios (0.1 % slip, <0.5° wobble) under XPBD and under MuJoCo when Newton's collision pipeline supplies the contacts. Swap only the collider representation to convex hulls and the same rig goes to 100 % slip. The task's default preset runs use_mujoco_contacts=True, and MuJoCo "compiles every mesh geom through its convex-hull path" — so the teeth, and the shafts, are hulled away before the solver ever runs. This is the wobbling, the non-meshing and the gear sliding off the fixture, and no solver setting could have fixed it. Full study below.

Where things stand

Rollouts

All videos are rendered fully headless through Newton's ViewerGL.get_frame() into mp4 (no Isaac Sim / Kit). The close-ups use the forced-insertion proxy — a scripted Jacobian push that presses the grasped part straight down into the socket / onto the shaft.

DisplayPort insertion — close-up. The gripper presses the plug (dark connector) down toward the socket. Shown for scale only: DisplayPort contact is still broken (the plug passes through the socket), so nothing physical should be read from this clip.

Gear assembly — the gear threaded onto its shaft and resting on the plate, 0.03 mm from its designed rest height. Earlier versions of this clip showed the gear sliding past the fixture; they were wrong and have been replaced rather than kept.

Wide overview (4 environments) of the DisplayPort task — the Flexiv Rizon 4s arms holding the plug at the grasp pose, for scale and context.

The two tasks (as shipped)

SettingGear assemblyDisplayPort insertion
Physics dt / decimation1 ms · 331 ms · 33
Effective solver rate1 kHz × 2 substeps = 2 kHz, control at 30.3 Hz
Default solverNewton + MuJoCo internal contacts (soft/penalty), implicit-fast, elliptic cone
Envs · episode4096 · 6.66 s
Held partgear (small/medium/large)DisplayPort plug (30 g), ~0.27 mm blade clearance

The soft-contact model is what forces the tiny time step: stiff penalty contacts need ~2 kHz to stay stable and avoid tunnelling. Newton's SolverVBD and SolverKamino instead solve contacts as constraints, so they should tolerate 100–200 Hz collision handling — the premise of this investigation.

Key finding: contact count is the bottleneck

With collisions left unfiltered, the DisplayPort scene generates a huge number of contacts per body — the plug against the socket's full SDF cavity, plus robot links. Running VBD surfaces this immediately as per-body contact-buffer overflow; Kamino cannot even build.

Per-body rigid contact counts reported during a VBD rollout of the DisplayPort task (n = 578 overflow events, 4 envs). Median 681, max 820 — all far above the 256-contact per-body VBD buffer (dashed line). Every overflow silently drops contacts, which is exactly how tunnelling creeps in.

Solver presetBuilds?Steps?Observation
default (MuJoCo)yesyesBaseline; soft contacts absorb the large contact set.
newton_vbdyesyesRuns, but floods contact buffer overflowed 820 > 256 → dropped contacts.
newton_kaminonoCrashes at construction: contact-sizing integer overflow (array shape −101886780).

Both failure modes share one root cause and one fix: collision filtering. Restricting collisions to the pairs that matter — plug ↔ socket and fingers ↔ plug, with robot self-collision, robot-link↔socket, and cross-environment contacts switched off — should cut the contact count by an order of magnitude, stop VBD dropping contacts, and let Kamino build. Only then are the dt sweep and tunnelling evaluation meaningful.

Per-step throughput at 4 environments after collision filtering (higher is better), with the pre-filter number in parentheses. VBD jumps ~10× once it stops dropping contacts; Kamino now runs at all (it previously crashed at construction). Absolute numbers are small at 4 envs — the real comparison happens at scale.

Collision filtering: the fix

The fix is a small, general mechanism: a NewtonCollisionFilterCfg that assigns Newton collision groups per body before the model is finalized. Newton's group rule — a body in a negative "hub" group collides with any positive group, but positive groups don't collide with each other — expresses the desired whitelist exactly: the plug is the hub (group −1), the socket (group +1) and the gripper fingers (group +2) each collide with the plug but not with one another, and everything else (robot arm, links, ground) is disabled (group 0). It is O(number of shapes) and never pairs shapes across environments.

Solver presetBefore filterAfter filter (plug↔socket + plug↔finger)
newton_vbdruns, but 578 buffer-overflow events/rollout (drops contacts); ~2 env-steps/s0 overflow, ~21 env-steps/s (~10×)
newton_kaminobuild crash — contact-sizing integer overflowbuilds & steps (~1 env-step/s; P-ADMM is heavy)
Reading of the finding. Using Newton collision filters to remove unnecessary contacts — the original intuition — turned out to be a hard prerequisite for the constraint-based solvers, not just a speed-up. With the whitelist in place, both VBD and Kamino work, and the dt sweep / tunnelling study can proceed on a scene that no longer silently drops contacts.
Gear-task false reset (fixed). The gear close-up initially flickered — the task was resetting roughly every two steps. It was not penetration: the grasp holds fine (the active gear stays ~27 mm from the fingertip center). The culprit is the gear_orientation_exceeded termination, which measures the gear's absolute orientation relative to the link7 wrist frame. Under Newton the gripper settles the grasped gear at ~23° relative roll, so it crossed the tight 30° roll/pitch threshold one step after every reset — in both the play and the training environment (i.e. it would have quietly broken a real training run). Widening the threshold to 60° clears the stable grasp orientation and drops the gear to the same sporadic, workable reset rate as DisplayPort. The scripted demo also disables episode terminations, so the rollout videos never reset mid-push.

The gear task gets the same collision treatment (gears are the hub; they collide with the base / shafts and finger tips, not with one another). The gear teeth are intrinsically contact-dense — ~500 contacts per environment even after filtering, across the three shaft-pinned gears — which initially overflowed Newton's auto-sized global contact buffer. For sub-mm-precision assembly the answer is to keep every contact, not thin them out, so we over-provision the buffer instead: a new rigid_contact_max_scale multiplier on Newton's auto-estimate (which itself scales with the environment count). At 3× the gear runs VBD and Kamino with zero dropped contacts — full contact fidelity, and still env-count-scalable.

Forced insertion: measuring tunnelling (retracted)

These results are withdrawn. They were measured before the collision-filter bugs above were found, so gear-to-base contact was disabled throughout: what the run actually measured was the gear against the gripper, not against its shaft. The method (depth tracking, the travel column, firing the part at a set speed) stands and is reused above, but the solver comparison itself has to be redone on the corrected scene. The charts below are kept only so the correction is auditable.

Tunnelling — the grasped part passing through the socket instead of seating against it — is the failure that forces these tasks down to a 1 ms time step. To measure it rather than argue about it, the probe tracks the signed insertion depth: the distance from the seat plane to the part's tip along the insertion axis. A correct insertion drives that depth toward 0 and stops — solid parts cannot go further. Two signals are flagged: through-penetration (depth ends >2 mm below the seat) and a fly-through jump (>5 mm of depth change in one control step, ending below the seat).

The depth zero-point is verified against the task's own mate helpers: the authored mated pose gives depth 0.0000 m, the authored reset pose (68 mm clearance) gives +0.0680 m. To keep the measurement independent of grasp quality, the part is fired at the shaft from a known 30 mm gap at a set impact speed — the same approach as Newton's RJ45 demo, which drives its plug directly rather than through a gripper. Gear task, 4 environments, dt = 1 ms.

Environments that tunnelled, by solver and impact speed. MuJoCo shows a clean onset — it holds the gear at 0.5 m/s, loses half the environments at 2 m/s, and all of them at 8 m/s. The hydroelastic preset fails at every speed. VBD's 0% is hatched because it is not a pass: see the travel column below.

Insertion depth of the deepest environment during the 2 m/s impact. Zero is the seat plane; below it (shaded) is inside solid material. MuJoCo descends, chatters around the seat and dips ~9 mm into it; hydroelastic sails straight through and keeps going (off-scale, reaching −5.3 m); VBD never moves at all.

Full results

SolverImpact speedTunnelledMin depthTravelVerdict
Hydroelastic (SDF)0.5 m/s4/4-4012.0 mm4020.1 mmno contact at all
MuJoCo (default)0.5 m/s0/417.2 mm7.7 mmcontact held the gear
VBD0.5 m/s0/430.0 mm0.0 mminvalid — part never moved
Hydroelastic (SDF)2.0 m/s4/4-5299.0 mm5257.6 mmno contact at all
MuJoCo (default)2.0 m/s2/4-8.9 mm12.6 mmhalf the envs pushed ~9 mm into the shaft
VBD2.0 m/s0/430.0 mm0.0 mminvalid — part never moved
Kamino2.0 m/s4/4-7.5 mm10.6 mmheld the gear, but 7.5 mm inside the shaft
Hydroelastic (SDF)8.0 m/s4/4-10446.8 mm10207.4 mmno contact at all
MuJoCo (default)8.0 m/s4/4-7062.2 mm6936.6 mmblasted straight through
VBD8.0 m/s0/430.0 mm0.0 mminvalid — part never moved

Travel is how far the part actually moved. It is the honesty check that separates "contact stopped the part" from "the solver never moved it" — without it, a frozen body looks like perfect tunnelling immunity.

Two blockers this measurement exposed.
1. VBD state handling was wrong — fixed — but VBD still diverges. Under newton_vbd the whole scene read back frozen at its initial pose. The cause was an architectural mismatch: VBD, XPBD and Kamino are maximal-coordinate solvers — they integrate body_q/body_qd directly and never touch joint_q — yet forward kinematics was re-run from those stale joint coordinates on every state refresh, overwriting the solver's own result. The fix is to treat body state as authoritative: seed it from joint coordinates once, then re-run FK only for articulations whose joints were explicitly rewritten (a reset), and read observations and rewards from body coordinates. (The second state buffer was also left uninitialized for double-buffered solvers; it is seeded now too.) Generalized-coordinate solvers are bit-identical after the change. VBD now integrates — but the articulated scene diverges (bodies reach kilometre scale within a few steps), so VBD numbers are still not comparable; solver-parameter work is next.
2. The hydroelastic preset generates no contact here, letting the gear free-fall through the base at every speed.
DisplayPort contact is broken at the asset level. A correctly aligned plug dropped from 15–20 mm above the seat falls straight through the socket and never stops — at zero impact speed, and also when dropped 12 mm off-centre onto the socket body. Collision flags are correct (plug, socket colliders and finger tips all report collide=True in the right groups), and it happens with the collision filter both on and off. The plug also is never really gripped: the task closes the hand to hand_close_width = -0.1, and the fingers settle at exactly that target with zero deviation — nothing between them. So the DisplayPort task cannot currently produce a meaningful insertion, which is why the quantitative comparison above runs on the gear.

Getting the gear scene physically right

Second correction. The previous clips here were still wrong: the gear slid down past the fixture instead of seating on its shaft, so it could never mesh with its neighbour. Two things caused it, one mine and one the scene's, and both are measured below. Everything in this section is re-run on the corrected setup, and a new functional test now checks the assembly actually works rather than merely coming to rest.

The geometry, measured rather than assumed

Reading the collider meshes directly settles what the scene is supposed to do:

FeatureMeasured
Base platez ∈ [−18.8, −11.0] mm → 7.8 mm thick, top face at −11.0 mm
Shaftsz ∈ [−11.3, +18.8] mm, radius 7.1 mm (30 mm tall)
Gear boreradius 7.9 mm → 0.8 mm radial clearance on the shaft
Gear teethsmall 20T · medium 40T · large 60T, circular pitch ≈ 4.9–5.2 mm (module ≈ 1.6 mm)
Centre distanceslarge↔medium 75.7 mm, medium↔small 45.8 mm → ≈ 1 mm backlash

So a correctly seated gear has its centre at +7.5 mm (bottom face resting on the plate), exactly the task's own seated_gear_z_offset, and the three gears are a matched, meshable set. Any failure to mesh is therefore a simulation problem, not a modelling one.

Why the gear went through the floor

  1. My drop height was above the shaft. The shaft tips end at +18.8 mm and a seated gear centre sits at +7.5 mm, so "drop it 15 mm above the seat" put the bore above the shaft tip entirely. The gear was never threaded, and simply fell past the fixture. Released at its seated pose, or dropped ≤5 mm, it threads on and stays: tilt 0.00°, resting 0.03 mm from the seat, 0.05 mm/s.
  2. The plate is penetrable under modest impact. Dropped 8 mm the gear ends 9.5 mm below its seat; dropped 11 mm, 22.6 mm below — it passes clean through a 7.8 mm plate. Raising the hydroelastic SDF resolution helps only marginally (−22.6 → −21.9 → −17.4 mm for 64 → 128 → 256), and 512 exhausts the GPU. This is a genuine contact-fidelity limit of the current setup, not a placement mistake.

Correctly seated. The gear descends over the shaft — the shaft tip is visible in the bore — and rests flat on the plate, where it stays.

Gear-train drive test. The large gear is spun at 3 rad/s while its neighbours are left free. They barely move — see below.

Does the assembly actually work?

Settling is far too weak a check: a gear lying under the fixture is also at rest. The real question is whether the thing functions as a gear train, so gear_mesh_test.py seats all three gears, spins one, and measures whether the others turn — checking direction and the ratio their radii imply, not merely that something moved. It also fails when the driven gear itself does not turn, so a dead drive cannot quietly pass.

GearMeasured ωExpected ωStayed on its shaft
large (driven, 60T)+2.14 rad/s— (input)within 1.5 mm
medium (40T)+0.13 rad/s−3.2 rad/swithin 1.5 mm
small (20T)−0.04 rad/s+6.4 rad/swithin 2.2 mm

Where the meshing actually breaks

It would be easy to blame SDF resolution and stop. Measuring each link in the chain separately shows that is not it:

QuestionMeasurementVerdict
Are the gears a meshable set?20/40/60 teeth, circular pitch 4.9–5.2 mm, centre distances give ~1 mm backlashyes
Does the SDF represent the teeth?sampling the built SDF around the rim gives a clean 60 cycles/rev, 0.49 mm amplitude (1.53 mm peak-to-peak)yes
Do the teeth actually touch?driven-vs-neighbour overlap peaks at 0.97 mm, in contact 68 of 80 stepsyes
Is tangential load transmitted?neighbour reaches 4 % of the meshing rate, in the wrong directionno

A caution on that SDF check: sampled at the gear's mid-height it looks featureless, because that height is the hub. The toothed rim sits at z ≈ −17 mm, and sampled there the teeth are unmistakable. An earlier reading of this as "the SDF has no teeth" was simply the probe in the wrong place.

Every lever on the contact stack was tried:

ChangeNeighbour ω (expect −3.2)Effect
SDF resolution 64 → 128 → 2560.21 → 0.41 → 0.64 rad/simproves, then stalls; 512 exhausts the GPU
normal_matching=False+0.02overlap rises to 2.6 mm, still no drive
reduce_contacts=False+0.04gears explode (53 m)
Plain SDF contacts (no hydroelastic)+0.01unstable; gears leave the fixture
Applied torque instead of prescribed spin0.00nothing moves at all
The gear train does not transmit, and it is the contact model rather than the geometry. The gears are a matched set, the SDF resolves their teeth, and the teeth are in contact for ~85 % of steps with about a millimetre of overlap — yet the neighbour turns at a few percent of the meshing rate. Hydroelastic contact merges a pair's overlap into a pressure patch with an averaged normal, which is exactly the information a tooth flank needs to push its neighbour tangentially. Disabling that averaging removes the stability instead of fixing the drive.

The practical consequence for training: a policy here can seat a gear on its shaft, and that part is now sound, but nothing in the physics rewards or penalises whether the teeth engage. Insertion is simulated; meshing is not. Newton's threaded nut-and-bolt example is the closest working reference and runs plain SDF contacts under XPBD rather than MuJoCo, which is the next configuration to try — the option to build mesh SDFs without hydroelastic is now in place for it.

XPBD and VBD — and a bug that hid everything

Trying XPBD and VBD on this scene turned up something more serious than the meshing question: maximal-coordinate solvers were never being read back at all.

They advance body_q/body_qd directly. But Isaac Lab reads a floating base's pose out of joint_q — literally get_root_transforms() → joint_q[0:7] — which those solvers never write. So every observation, reward and sensor reported the pose the scene started in, regardless of what the solver did. Measured side by side, Newton's own body_q moved ~40 mm per step while the read-back sat frozen to five decimal places:

StepNewton body_q zIsaac Lab read-back z
0+0.07065+0.06250
2+0.07738+0.06250
5+0.07340+0.06250

Recovering the joint coordinates from the body state after each step (inverse kinematics, the companion to the forward pass) closes the gap exactly — body_q, the bound array and the read-back now agree to every digit. MuJoCo is untouched and still bit-identical.

The uncomfortable part. This bug did not just hide motion — it manufactured reassuring results. A frozen scene scores perfectly on a settling check: zero velocity, zero tilt drift, zero displacement. Earlier VBD runs were reported as stable on exactly that basis. With the read-back fixed, the same configurations are visibly divergent. Any solver comparison run before this fix is worthless, and passive settling alone can never be the gate again.

What the two solvers actually do

SolverPassive seatingGear train
MuJoCo (hydroelastic SDF)seats, 0.00° tilt, 0.03 mm from designed heightteeth touch but transmit ~4 %
XPBD + plain SDF contactsdiverges — gear leaves the fixture (74 m)gears fly apart; no contact registered
VBD + plain SDF contactsdiverges hard — 380 m/s, 2660 °/sgears fly apart
Kaminotoo slow to evaluate here (~500× MuJoCo)

So the honest position on the original question — can a constraint-based solver run this task at a larger time step? — is that neither XPBD nor VBD is usable on this scene yet. Their configuration is now the open problem rather than their plumbing: both mirror Newton's threaded nut-and-bolt example, which is stable in Newton's own harness, so the difference is in this scene's mass ratios, contact parameters and the articulated arm sharing the solve. MuJoCo remains the only configuration that seats a gear correctly, and it still does not turn one.

Measuring the overlap

Overlap is measured directly: the moving mesh's vertices are transformed into the static shape's signed-distance field and sampled, so the most negative sample is the penetration depth. The SDF is built on demand and disk-cached, and one lookup per vertex keeps the query at ~0.04 ms per step — cheap enough to drive a reward penalty or a reset, both exposed as MDP terms.

Peak gear-in-plate overlap against impact speed, on the corrected scene with the gear properly threaded on its shaft. Hatched bars are runs where the gear did not stay seated.

ImpactOutcomeFinal tiltResting offset from seatPeak overlap
0 m/s (released)seats correctly0.00°−0.03 mm1.81 mm
0.2 m/sseats correctly0.01°−0.03 mm3.32 mm
0.5 m/spunches through the plate0.54°−30.0 mm1.10 mm
1.0 m/sknocked off entirely46°fell away1.13 mm
Reading it. Up to 0.2 m/s the gear seats to within 0.03 mm of its designed rest height, but with 1.8–3.3 mm of interpenetration — already coarse against a 0.8 mm shaft clearance. At 0.5 m/s it passes straight through a 7.8 mm plate. And the low overlap on the failing rows is the same trap as before: once the part is moving fast it is never sampled while inside, so overlap must always be read together with the outcome.

The isolated gear-train study

Everything above was measured on the full task: an articulated arm, a gripper, resets, an observation stack and three gears sharing one solve. Any of those can cause the wobble, and none of them can be ruled out from inside. So the scene was rebuilt from nothing — scripts/benchmarks/gear_solver_study.py — as plain Newton: a static base carrying the three shafts, the three gears seated on them, and nothing else. Same USD collision meshes the task ships.

The 60-tooth gear is driven by a velocity servo rather than a bare torque. That matters: the train is friction-limited, so a fixed torque lands anywhere between stuck and 300 rad/s depending on stiction, and no two configurations measure the same thing. Holding the input speed fixed makes slip — how far the followers fall short of the ratio their tooth counts demand — directly comparable across every run.

The geometry, measured from the meshes

Slicing the collision meshes with a plane (not sampling their vertices — a cylinder tessellated only at its ends has no vertices at mid-height, so a vertex probe cannot tell a toothed profile from a smooth one) gives the numbers every threshold below is built on:

QuantityValueConsequence
Module / tooth counts1.5 mm · 20 / 40 / 60 Ttip radii 16.50 / 31.50 / 46.50 mm
Tooth depth (tip − root)3.37 mm, 60 cycles/rev confirmed by FFTthe teeth are really there, at every height of the 13.5 mm toothed band
Centre distances75.75 / 45.75 mm vs 75 / 45 theoretical0.75 mm loose → ≈0.55 mm backlash
Bore vs shaft radius7.875 mm vs 7.125 mm0.75 mm radial slop; ~3° of tilt is physically real, not numerical
Seatplate top −11.25 mm, gear bottom face −18.75 mmseated gear centre at +7.5 mm

Outer radius against angle for the 60-tooth gear, taken from a plane slice of its collision mesh at mid-tooth height. Green is the shipped mesh; the dashed line is its convex hull — the representation MuJoCo's own contact path uses.

With the real meshes, the train works

XPBD at 1 kHz with a single substep, 10 iterations, a 256³ SDF and a 0.5 mm contact gap drives all three gears at the ratios the tooth counts demand:

GearMeasured ωRatio demanded by tooth countsSlipPeak-to-peak tilt
large 60T (driven)+5.24 rad/s— (input)0.19°
medium 40T−7.85 rad/s−7.86 rad/s0.1 %0.36°
small 20T+15.71 rad/s+15.72 rad/s0.1 %0.43°

MuJoCo reaches the same answer when Newton's collision pipeline supplies the contacts (use_mujoco_contacts=False): 0.3 % and 1.5 % slip, 0.27–1.68° of tilt. The sign alternates correctly along the chain, and every gear stays within 0.6 mm of its shaft.

Exact meshes + SDF contacts, XPBD. The teeth mesh and the train turns.

Identical run, gears convex-hulled. The driven gear spins; its neighbours never move.

The one change that breaks it

Same rig, same solver, same time step — only the collider representation changes:

CollidersXPBDMuJoCo (Newton contacts)
Exact meshes + SDF0.1 % slip, 0.2–0.5° tilt0.3–1.5 % slip, 0.3–1.7° tilt
Gears convex-hulled, base exactdiverges100 % slip — followers dead still
Everything convex-hulleddiverges (4529 rad/s)100 % slip, 94–167° tumbling

A convex hull of a spur gear is a plain disc of the tip radius with the bore filled in. Two such discs seated at the design centre distance overlap permanently by 2.25 mm — because that overlap is the tooth engagement, and with the teeth gone there is nothing to interlock, only two cylinders pressing on each other. The hull of the base is worse: it is a tent stretched over the three shafts, so the gears have no bore to thread and slide down its slope. That is precisely the "right gear slides down to the floor below the fixture" seen earlier.

And this is what the task runs. The default preset — the one sized for the two-day training run — sets use_mujoco_contacts=True. Newton's MuJoCo exporter says it plainly in solver_mujoco.py: "MuJoCo compiles every mesh geom through its convex-hull path." So under default the gear teeth and the shafts do not exist as far as contact is concerned. The 2 kHz time step was paying for a scene that is permanently 2.25 mm interpenetrated.

What the task scene actually hands to the solver

Dumping the finalized Newton model per preset (gear_scene_audit.py) turns up three more problems that no amount of solver tuning would have reached:

PresetGear collider Newton receivesContact gapSDF
defaultthe gears' visual meshes — the authored collisions/ prims are absent from the model entirely100 mmnone
newton_xpbdcorrect meshes, but the medium gear gets two coincident colliders (its visual mesh and its collision mesh)10 mmyes
newton_hydroelasticone collision mesh per gear, hydroelastic flagged5 mmyes

Encouragingly, the isolated rig is insensitive to the gap on its own — it still transmits with a 20 mm gap — so the gap is a cost and a contact-count problem (it is where the ~680 contacts/body came from) rather than the cause of the wobble. The cause is the hull.

How slow can the loop run?

With the real colliders, the original question finally has an answer. Sweeping the outer step from 1 kHz to 100 Hz, 22 of 24 configurations transmit within 5 % of the exact ratio:

Worst-case slip against outer step rate. XPBD holds the ratio down to 100 Hz with a single substep; MuJoCo needs 4 substeps below 200 Hz. Only two configurations miss: both at 100 Hz with a single substep.

The rest of the sweep is almost boring, which is the point — with the geometry represented correctly the problem is not delicate: 9/9 SDF-resolution × gap combinations transmit (128–512³, 0.2–2 mm gap), 8/8 iteration counts (5–40), and 9/9 friction × stiffness combinations (µ 0.05–1.0, ke 10⁵–10⁷).

Wobble is flat across SDF resolution — 128³ is already enough for a 3.37 mm tooth on a 93 mm gear. Resolution was never the limiting factor.

VBD: one parameter away from finite

VBD failed differently and more interestingly. On this scene it goes non-finite about 25 ms after the gears touch — not by exploding, but by producing NaN while every velocity is still under 2 cm/s. It is not a contact-buffer overflow (51–91 contacts per body against a 4096 buffer), and it survives none of the usual levers: frictionless, hard or soft contacts, 8 substeps, 5× smaller dt, stiffness from 10⁵ to 10⁷, density up to 10⁵.

The one setting that changes the outcome is the AVBD penalty ramp. With the default rigid_avbd_beta = 0 ("fixed-k") the solve goes NaN at step 25 in every one of the sixteen configurations swept; with rigid_avbd_beta = 1000 it runs indefinitely and the gears settle within 0.2 mm of their designed seat.

Finite is not the same as correct. Driven by the same velocity servo that takes XPBD to 5.2 rad/s, VBD's input gear reaches only 0.5–0.7 rad/s — its contact is heavily dissipative here — and the followers slip 34–56 %. Raising the drive torque to overcome the damping makes it unstable again (86–180° of tumbling). So VBD is not usable on this scene yet, but the failure is now specific enough to report upstream with a minimal repro: three meshes, one static, no articulation.

rigid_avbd_betaOutcomeInput ω reachedFollower slip
0 (default)non-finite at ~25 ms
10 – 100finite but tumbles (14–86°)4.8–6.0 rad/s64–100 %
1000stable, seated to 0.2 mm0.5–0.7 rad/s34–56 %
10000tumbles (162°)5.7 rad/s~100 %
Corrections to earlier sections of this report.

What's next

The scene is physical now, which resets the order of work: the solver comparison has to be re-run before any of it means anything.