In a VBD entry with self contact (add_cloth's default, a cloth card with features.self_contact, two or more
cloth or soft-solid objects), nothing moves faster than a cap. A sheet dropped from 1 m reaches 3 m/s and then falls
at a constant 3 m/s instead of 4.4 m/s at the floor.
On fine meshes the cap is lower (see Number).
Before 2026-11-08 the cap was much lower: 0.82 m/s for the 0.6 mm towel card and 2.72 m/s for a 2 mm sheet at the
1.25 ms step.
Rule: cap = 0.85 (margin + gap) / step, where margin is the contact thickness h and the step is the cloth's
physics step. The route (alloy.solvers.vbd.reach) picks the gap from the step so that the cap is 3 m/s:
gap = max(0, min(3 step / 0.85, 0.75 ℓ) - h), with ℓ the median rest edge length. A finer step shrinks the gap and
the cap stays 3 m/s.
The cap rises above 3 m/s only when the gap is 0, that is when the step is under 0.85 h / 3 m/s: 0.57 ms for a
2 mm sheet. Below that it is 0.85 h / step.
On fine meshes, where 0.75 ℓ binds, it is lower than 3 m/s: at the 1.25 ms step that is a median edge under
5.9 mm.
A control dt above 1.25 ms does not change the step:Scene(dt=...) of 10, 5 or 2.5 ms still steps the cloth at
1.25 ms (800 steps/s).
Measured (CPU, 0.3 m sheet, resolution 16, 0.03 kg, dropped from 1.6 m, fall speed fitted over 0.35 to 0.4 s;
free fall is 3.68 m/s at the middle of that window, 3.92 m/s at 0.4 s):
2 mm sheet, Scene dt 10 ms, step 1.25 ms: 3.00 m/s (cap 3);
2 mm, dt 1.25 ms, step 1.25 ms: 3.00 m/s (cap 3);
2 mm, dt 0.625 ms, step 0.625 ms: 3.00 m/s (cap 3, gap 0.2 mm);
2 mm, dt 0.5 ms, step 0.5 ms: 3.40 m/s (cap 3.4, gap 0);
2 mm, dt 0.25 ms, step 0.25 ms: 3.58 m/s (cap 6.8, so not capped; 3 % under free fall's 3.68, which is
float32 rounding of the velocity at fine steps);
4 mm, dt 1.25 ms: 3.00 m/s (cap 3); 4 mm, dt 0.625 ms: 3.67 m/s (cap 5.4: free fall);
2 mm, 5 mm cells (resolution 61, median edge 7.5 mm), dt 1.25 ms: 3.00 m/s (cap 3; 0.75 ℓ does not bind);
self_contact=False, dt 10 ms: 3.70 m/s (free fall).
Earlier numbers: with the old reach the cap was 0.816 and 2.72 m/s (development scripts, not shipped).
Newton's penetration-free truncation (DAT) caps every particle's motion between self-contact detections at
0.5 · 0.85 · (margin + gap). A step detects twice.
A larger gap lifts the cap, but it lists more candidate pairs: the cost grows with the reach.
Past about a cell, the pairs overflow Newton's fixed per-vertex buffers, and the overflowing pairs are dropped
(docs/known_gaps/vbd-self-contact-buffer-overflow.md).
The 3 m/s bound and the 0.75-edge cap were chosen by measurement:
at 5 m/s a towel's fast pinch ran 9x slower and its half fold failed;
at 3 m/s, 0 crossings, the same fold quality, 1.5-2.8x the old cost on towels, and 3.35x faster on WP2's 4 mm drape.
Scene(dt=...) alone does not raise the cap. Halving the control dt does not double it: at the route's reach the cap
is 3 m/s at every step down to 0.57 ms (2 mm sheet).
A thicker sheet with a fine enough step: a 4 mm sheet at a 0.625 ms step (dt=0.000625) fell freely (3.67 m/s,
cap 5.4). A 4 mm sheet at 1.25 ms stayed at 3.00 m/s.
A 2 mm sheet needs a step under 0.57 ms: 3.40 m/s at 0.5 ms, not capped at 0.25 ms.
A finer step takes more steps per simulated second: 2 x at 0.625 ms and 5 x at 0.25 ms, against 1.25 ms.
Self contact off (self_contact=False) removes the cap (3.70 m/s, free fall), and with it layer-on-layer contact.
Engine layer, not public API: an explicit particle_self_contact_gap in the VBD entry's kwargs wins over the rule
(keep margin + gap under one rest edge length, or pairs overflow), and alloy_self_contact_speed sets the rule's
motion bound. No speed was measured for either.
A detection inside the iterations would lift the cap at less cost than a larger reach. Newton's ITERATIONS schedule
does that; it is not measured yet (WP11).