Resting contact on a pinned cloth table: elastic cube, stacked sheets, fast impact

Vanilla VBD contact (penalty_dat) against the ALM-DAT contact row (alm_dat, with Coulomb friction and gap-rate damping) on three scenes: a dropped elastic cube, four stacked sheets (python -m newton.examples vbd_rest_on_contact), and five sheets falling freely at 1 m/s with no table. Branch ankac/alm-dat-fable-review, snapshot 986b7c92 plus the example commit, CPU run, Warp 1.17.0, 2026-10-05.

1. Simplest test: an elastic cube dropped on the pinned cloth plane

z trajectory of the cube on both contact methods

A 4x4x4 cm tetrahedral cube (density 1 g/cm3, 125 g lumped, k_mu = k_lambda = 1e+05 dyn/cm2, no material damping) starts with its bottom face 1 cm above the pinned 16x16 cm cloth table and falls under gravity. Contact settings shared by every scene on this page (margin 0.2 cm, gap 0.1 cm, ke 1e+04, kd 1e+02, mu 0.5), 5 iterations, 10 substeps, 60 fps. Curves are the lowest cube vertex above the table and the largest vertex speed; the dashed line is the margin.

penalty_dat (vanilla)alm_dat
first contact (lowest vertex reaches the margin)0.05 s0.05 s
highest rebound of the lowest vertex after contact [cm]0.2770.274
settled (every vertex below 1 cm/s from then on)0.27 snever
rest height of the lowest vertex, mean over the last second [cm]0.18600.1318
rest jitter of the lowest vertex, std / range over the last second [cm]7.2e-05 / 4.1e-041.3e-03 / 6.2e-03
largest vertex speed over the last second [cm/s]0.2403.277
lowest vertex height over the whole run [cm] (0 is the table)0.16620.1300
detector coverage flag true on every frameTrueTrue

Both paths land and rebound alike. The vanilla path settles within a quarter second and rests one cushion deflection below the margin with sub-micron jitter. The ALM path rests lower because its linear cushion (K = 1e4 dyn/cm per row) is softer than the penalty barrier near the rest point, and it keeps a few cm/s of elastic ringing: the cube has no material damping and the contact damping only acts on closing motion. Before friction and damping were added the ALM cube rested at 0.115 cm with five times the jitter and was still drifting down at 3 s.

2. Stacked sheets: default path versus alm_dat, both with friction 0.5

Left: the shipped behavior, penalty contact with Coulomb friction mu = 0.5, gap-rate damping kd = 100 and per-color Divide-and-Truncate. Right: the ALM contact row with stored division planes, now with Coulomb friction (a tangential multiplier capped at mu times the row's normal force) and gap-rate damping of the closing motion inside the margin. Both stacks land, settle and rest one margin apart; the mean sheet heights agree to 0.002 cm (0.195 / 0.391 / 0.563 / 0.771 versus 0.196 / 0.393 / 0.565 / 0.773 cm), the vertical jitter over the last two seconds is of the same size on both (0.010 to 0.015 versus 0.004 to 0.016 cm), and the largest sheet speed over the last two seconds is 5.4 cm/s on the default path and 8.7 cm/s on alm_dat. No vertex touches the table on either path and the detector's coverage flag stayed true on alm_dat for the whole run.

3. Why friction and damping were needed: the first alm_dat version

Left: the alm_dat row as first rendered, without friction or damping. The slightly rotated sheets slide apart at 20 to 25 cm/s, the first one reaches the 30 cm table edge at 0.85 s and tips over it, and the others follow; once off the table a sheet falls at exactly 76.5 cm/s, the per-substep DAT motion budget. Nothing interpenetrates, it is sliding. Right: the same row with friction and damping.

Friction-matched control: the default path with mu = 0 (left) against the first alm_dat version (right). Both frictionless runs slide off (first sheet vertex below the table surface at 1.12 s versus 0.85 s; peak sliding speed before leaving the table 24.2 versus 27.0 cm/s), so the dispersal was frictionless physics, not a contact-method defect.

Stacked sheets, timeline (max sheet speed [cm/s] / lowest sheet vertex above the table [cm])

timepenalty_dat, mu = 0.5penalty_dat, mu = 0alm_dat, first version (no friction, no damping)alm_dat, mu = 0.5 (friction + damping)
0.25 s3.08 / +0.16110.92 / +0.17816.17 / +0.1944.97 / +0.180
0.50 s3.15 / +0.16815.68 / +0.19523.30 / +0.1943.45 / +0.187
0.75 s1.56 / +0.17619.69 / +0.19425.31 / +0.1924.51 / +0.186
1.00 s2.38 / +0.16918.73 / +0.19443.05 / -4.2053.90 / +0.187
1.50 s3.01 / +0.17187.07 / -16.31376.50 / -35.4075.96 / +0.189
2.00 s4.02 / +0.18682.45 / -56.28776.50 / -73.6576.25 / +0.178
3.00 s1.75 / +0.17288.50 / -136.31676.50 / -150.1585.22 / +0.187
4.00 s3.31 / +0.16484.95 / -216.30976.50 / -226.6596.07 / +0.187
4.98 s3.54 / +0.17681.59 / -295.25076.50 / -301.8868.70 / +0.186
penalty_dat, mu = 0.5penalty_dat, mu = 0alm_dat, first version (no friction, no damping)alm_dat, mu = 0.5 (friction + damping)
first sheet vertex below the table surfacenever1.12 s0.85 snever
largest sheet speed over the last 2 s [cm/s]5.41151.5476.508.70
render wall time (CPU, 300 frames)365 s353 s273 s375 s

Stacked sheets, rest metrics over the last 2 s (mean height [cm] / max vertical std [cm])

Only meaningful for runs whose sheets are still on the table; falling sheets show large negative heights.

sheetpenalty_dat, mu = 0.5penalty_dat, mu = 0alm_dat, first version (no friction, no damping)alm_dat, mu = 0.5 (friction + damping)
sheet 00.1948 / 0.010010.1997 / 0.00006-171.0260 / 44.166610.1958 / 0.00362
sheet 10.3907 / 0.01353-10.2292 / 19.11110-219.9434 / 44.166610.3925 / 0.00683
sheet 20.5631 / 0.01474-210.6022 / 46.40108-220.8856 / 44.166610.5649 / 0.01438
sheet 30.7710 / 0.01446-207.4097 / 45.60289-119.5769 / 44.166610.7731 / 0.01581

4. Free fall: five sheets 2.5 mm apart, all at 100 cm/s, no table

stack velocity against exact free fall, kept fraction, and sheet spacing for the four runs

Five sheets start 0.25 cm apart with the same downward velocity of 100 cm/s (3.3 mm per step at dt = 1/300 s, more than their spacing) and fall under gravity with nothing below them. Every sheet should accelerate at g and the spacing should stay 2.5 mm; there is no physical reason for any contact force, so whatever slows the stack is artificial damping of the contact method. The division planes between neighbouring sheets sit at the midpoints (nobody approaches), 1.25 mm below each sheet, while every sheet wants to move 3.3 mm per step: a single truncation round can admit at most 0.85 x 1.25 mm, a second round re-slides the planes with the committed motion and admits the same fraction of the remainder, and so on. Video at 5x slow motion (one simulation step per frame), static camera; margin 0.2 cm, detection gap 3 cm, friction 0.5, damping 100, 5 iterations.

penalty_dat (vanilla)alm_dat, 1 roundalm_dat, 2 roundsalm_dat, 5 rounds
stack velocity at 0.1 s [cm/s] (free fall -198.1); kept fraction-36.8 (19%)-198.1 (100%)-198.1 (100%)-198.1 (100%)
stack velocity at 0.2 s [cm/s] (free fall -296.2); kept fraction-66.2 (22%)-296.2 (100%)-296.2 (100%)-296.2 (100%)
stack velocity at 0.3 s [cm/s] (free fall -394.3); kept fraction-82.3 (21%)-394.3 (100%)-394.3 (100%)-394.3 (100%)
distance fallen by the lowest sheet [cm] (free fall 74.1)74.374.374.374.3
adjacent spacing over the run, min / max [mm] (start 2.5)-4.58 / 679.472.50 / 2.502.50 / 2.502.50 / 2.50

The final truncation count on its own

the same free fall with the in-iteration plane refresh switched off: 1, 2 and 5 final rounds

Same scene, but the alm_dat runs skip the in-iteration plane refresh (--dat-plane-update-interval 0), so the only truncations are the initial one and the 1, 2 or 5 final rounds (--dat-truncation-iterations). With the default refresh (one round after each of the 5 iterations, the row above) every alm_dat run already resolves the whole stack within the step, which is why the three counts were indistinguishable there.

penalty_dat (vanilla)alm_dat, 1 roundalm_dat, 2 roundsalm_dat, 5 rounds
stack velocity at 0.1 s [cm/s] (free fall -198.1); kept fraction-36.8 (19%)-196.8 (99%)-198.1 (100%)-198.1 (100%)
stack velocity at 0.2 s [cm/s] (free fall -296.2); kept fraction-66.2 (22%)-291.3 (98%)-296.2 (100%)-296.2 (100%)
stack velocity at 0.3 s [cm/s] (free fall -394.3); kept fraction-82.3 (21%)-349.5 (89%)-394.2 (100%)-394.3 (100%)
distance fallen by the lowest sheet [cm] (free fall 74.1)74.374.374.374.3
adjacent spacing over the run, min / max [mm] (start 2.5)-4.58 / 679.47-10.45 / 36.412.28 / 2.722.50 / 2.50

Why the detection gap had to be widened

With the example's default detection gap of 0.1 cm the per-step motion budget 0.5 x 0.85 x (0.2 + 0.1) cm = 1.275 mm caps the committed speed at 38.2 cm/s on both paths, so every run fell at that speed and the truncation rounds could not matter (alm_dat kept the spacing at exactly 2.50 mm; vanilla spread it to 2.47 to 3.97 mm). The variant above widens the gap to 3 cm so the budget (408 cm/s) does not bind during the 0.3 s shown and only the plane truncation acts.

the same free fall with the default 0.1 cm detection gap: every run capped at the motion budget
penalty_dat (vanilla)alm_dat, 1 roundalm_dat, 2 roundsalm_dat, 5 rounds
stack velocity at 0.25 s [cm/s] (free fall -328.9); kept fraction-39.2 (12%)-38.2 (12%)-38.2 (12%)-38.2 (12%)
stack velocity at 0.5 s [cm/s] (free fall -574.1); kept fraction-39.1 (7%)-38.3 (7%)-38.3 (7%)-38.3 (7%)
stack velocity at 1 s [cm/s] (free fall -1064.7); kept fraction-39.1 (4%)-38.2 (4%)-38.2 (4%)-38.2 (4%)
distance fallen by the lowest sheet [cm] (free fall 1227.6)58.356.756.756.7
adjacent spacing over the run, min / max [mm] (start 2.5)2.47 / 3.972.50 / 2.502.50 / 2.502.50 / 2.50

Scene

Four 10x10 cm cloth sheets (1 cm cells, 0.02 g/cm2, tri_ke = tri_ka = 1e4, edge_ke = 5, absolute damping tri_kd 0.015 and edge_kd 0.05, contact ke 1e4 and kd 100) start 0.5 cm apart above a pinned 30x30 cm cloth sheet that acts as the table, each shifted by up to 0.2 cm and rotated by up to 0.1 rad. The table is cloth with zero mass, so sheet-on-table and sheet-on-sheet contacts both run through the soft self-contact pipeline and the contact method is the only difference between runs. Self-contact margin 0.2 cm, detection gap 0.1 cm, the table itself has no thickness. 60 fps, 10 substeps, 5 VBD iterations, 300 frames.

Old path (penalty_dat): penalty force along the closest-point direction with friction and damping; the accumulated displacement is clipped against freshly built division planes after every color. New path (alm_dat): one compliant ALM contact row per detected pair with its multiplier carried across steps, division planes stored and rebuilt only at certified anchors, and the committed state is the truncated anchor. The ALM row has no friction or damping yet.

How it was produced

python -m newton.examples vbd_rest_on_contact
python -m newton.examples vbd_rest_on_contact --contact-method alm_dat
python -m newton.examples vbd_rest_on_contact --contact-mu 0.0
notes/vbd-rest-on-contact/render_comparison.py render --method ... --frames 300   (headless ViewerGL under Xvfb, CPU)
notes/vbd-rest-on-contact/render_comparison.py compose --left penalty_dat --right alm_dat --name comparison
notes/vbd-rest-on-contact/render_comparison.py compose --left penalty_dat_mu0 --right alm_dat --name comparison_frictionless