Skip to content

Commit d3dba4d

Browse files
zlyfunctionclaude
andcommitted
Fix snow vanishing as it spreads: lower iso, deepen mirror, smaller min-chunk
The "snow suddenly disappears as it spreads" complaint was a rendering bug, not a simulation one. Offline replay of the splat/threshold pipeline showed only ~74% of the 340k particles were meshed at late times and ~25.5% were invisible by construction, because the deposit relaxes into a carpet 1-2 particles thick whose density mostly sat below the isosurface, and the anti-flicker dead band then drew nothing there. Spreading *was* disappearing. bake_surface.py: - ISO_FRAC 0.40 -> 0.12 (meshed fraction 74% -> 96.4% at late frames) - MIRROR_BAND 3 -> 12 cells (mirror the whole ~3-cell Gaussian foot of the carpet, not just its bottom skin) - MIN_COMPONENT 500 -> 120 cells (~38mm -> ~24mm; 500 was reclassifying real rubble an eighth of a letter tall as dust) Also sim.py --dump-jp, used to confirm the material is fully torn and stationary after ~0.7s (no sim-side cause). REPORT.md gains the full disappearance postmortem. No re-simulation; same raw data, rebaked. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3b82b09 commit d3dba4d

5 files changed

Lines changed: 62 additions & 5 deletions

File tree

mpm_demo/REPORT.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,39 @@ dusting. Spray identity is also keyed to particle id rather than resampled per
200200
frame — an independent random subset each keyframe made the powder strobe
201201
(consecutive-keyframe identity overlap: ~0.1 → 0.97).
202202

203+
### 4.1 The disappearing-spread postmortem
204+
205+
The one remaining complaint — snow visibly vanishing as it spread, and the
206+
final deposit looking barren rather than like a fluffy carpet — was diagnosed
207+
by replaying the exact splat/threshold pipeline offline and counting, per
208+
frame, how many of the 340k simulated particles land in each renderable
209+
bucket. The answer was unambiguous: **it was a rendering bug, not a
210+
simulation bug.** The sim conserves mass (the point-cloud truth view shows a
211+
wide, intact debris field to the last frame), but the surface path hid it:
212+
213+
- After the burst the deposit relaxes into a carpet only **1–2 particles
214+
thick** over most of its footprint (measured: ~2.5 particles per bake cell,
215+
spread stops growing ~0.5 s after impact). The isosurface at `ISO_FRAC=0.40`
216+
therefore sat above most of the carpet's density, and the "dead band" of
217+
anti-flicker logic deliberately rendered nothing there. Net effect: **only
218+
~74% of all particles were meshed at late times; ~25.5% of the snow was
219+
invisible by construction**, and it was precisely the spread-out part — so
220+
spreading *was* disappearing.
221+
- The Gaussian skirt of the carpet is ~3 cells thick, but floor mirroring only
222+
doubled the bottom 3 cells (`MIRROR_BAND=3`), leaving the rest of the foot
223+
below iso.
224+
- `MIN_COMPONENT=500` at the coarser 2.1-spacing cell silently means "drop any
225+
chunk under ~38 mm across" — an eighth of a 300 mm letter — so genuine
226+
rubble was being reclassified as dust.
227+
228+
The fix is three constants: `ISO_FRAC` 0.40 → **0.12**, `MIRROR_BAND` 3 →
229+
**12** cells, `MIN_COMPONENT` 500 → **120** (~24 mm, still well above the
230+
noise scale). Meshed fraction at late frames goes **74% → 96.4%**, the dead
231+
band shrinks from ~4.5% to ~1.2%, and the residue is a real fluff halo of
232+
~180 one-millimetre grains instead of 2500 invisible ones. No re-simulation
233+
was needed; the same raw particle data now renders as a continuous snow
234+
carpet with chunks sitting on it.
235+
203236
---
204237

205238
## 5. Reproducing
-495 KB
Binary file not shown.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"fps":60.0,"numFrames":90,"bounds":{"min":[0.01296062159177898,0.046875224884721026,0.011321402590843426],"max":[2.5162489028013026,0.9115983457601344,0.5174074978864467]},"maxVerts":18646,"maxTris":36000,"maxSpray":1846,"sprayRadius":0.0011250472640740034,"frames":[{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18098,"t":35999,"s":0},{"v":18245,"t":35998,"s":10},{"v":18223,"t":36000,"s":380},{"v":18284,"t":35999,"s":875},{"v":18378,"t":36000,"s":1049},{"v":18476,"t":36000,"s":1166},{"v":18532,"t":35999,"s":1288},{"v":18535,"t":35999,"s":1378},{"v":18541,"t":36000,"s":1485},{"v":18507,"t":35999,"s":1555},{"v":18509,"t":35998,"s":1608},{"v":18490,"t":36000,"s":1635},{"v":18501,"t":35998,"s":1641},{"v":18519,"t":35999,"s":1656},{"v":18543,"t":36000,"s":1671},{"v":18550,"t":36000,"s":1674},{"v":18534,"t":36000,"s":1682},{"v":18558,"t":35999,"s":1692},{"v":18560,"t":36000,"s":1705},{"v":18545,"t":36000,"s":1719},{"v":18570,"t":36000,"s":1725},{"v":18563,"t":36000,"s":1736},{"v":18590,"t":35999,"s":1747},{"v":18578,"t":35999,"s":1761},{"v":18585,"t":36000,"s":1757},{"v":18579,"t":35999,"s":1762},{"v":18607,"t":35998,"s":1763},{"v":18609,"t":36000,"s":1780},{"v":18577,"t":35998,"s":1776},{"v":18591,"t":35999,"s":1775},{"v":18579,"t":35999,"s":1789},{"v":18590,"t":36000,"s":1784},{"v":18598,"t":36000,"s":1798},{"v":18624,"t":35999,"s":1786},{"v":18615,"t":35999,"s":1807},{"v":18610,"t":36000,"s":1808},{"v":18624,"t":35998,"s":1805},{"v":18603,"t":35999,"s":1811},{"v":18585,"t":36000,"s":1805},{"v":18613,"t":35999,"s":1823},{"v":18626,"t":36000,"s":1822},{"v":18598,"t":36000,"s":1825},{"v":18613,"t":36000,"s":1829},{"v":18604,"t":35999,"s":1837},{"v":18620,"t":36000,"s":1837},{"v":18608,"t":35998,"s":1831},{"v":18613,"t":35999,"s":1830},{"v":18635,"t":35999,"s":1833},{"v":18635,"t":35999,"s":1829},{"v":18637,"t":35999,"s":1827},{"v":18645,"t":36000,"s":1829},{"v":18633,"t":35999,"s":1833},{"v":18630,"t":36000,"s":1830},{"v":18622,"t":36000,"s":1844},{"v":18640,"t":35999,"s":1834},{"v":18641,"t":36000,"s":1834},{"v":18646,"t":36000,"s":1846},{"v":18646,"t":35998,"s":1833},{"same":1},{"v":18640,"t":35999,"s":1833},{"same":1},{"v":18625,"t":35999,"s":1838},{"same":1},{"same":1},{"same":1},{"same":1},{"same":1},{"same":1},{"same":1},{"v":18626,"t":36000,"s":1839},{"same":1}]}
1+
{"fps":60.0,"numFrames":90,"bounds":{"min":[0.012965201506661337,0.04684784367148773,0.011321663031624716],"max":[2.516256312733246,0.9115974706464539,0.5174065631681214]},"maxVerts":18345,"maxTris":36000,"maxSpray":184,"sprayRadius":0.00112504952073961,"frames":[{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18007,"t":35998,"s":0},{"v":18007,"t":35998,"s":0},{"v":18007,"t":35998,"s":0},{"v":18008,"t":36000,"s":0},{"v":18008,"t":36000,"s":0},{"v":18116,"t":36000,"s":0},{"v":18185,"t":35999,"s":0},{"v":18264,"t":35999,"s":1},{"v":18345,"t":35999,"s":18},{"v":18290,"t":35998,"s":32},{"v":18283,"t":35999,"s":39},{"v":18278,"t":35998,"s":48},{"v":18232,"t":35999,"s":66},{"v":18208,"t":35999,"s":82},{"v":18243,"t":36000,"s":107},{"v":18193,"t":36000,"s":120},{"v":18223,"t":35999,"s":133},{"v":18218,"t":35999,"s":145},{"v":18205,"t":35999,"s":149},{"v":18217,"t":36000,"s":151},{"v":18213,"t":36000,"s":151},{"v":18193,"t":36000,"s":166},{"v":18190,"t":36000,"s":165},{"v":18181,"t":35999,"s":166},{"v":18187,"t":36000,"s":168},{"v":18184,"t":35999,"s":170},{"v":18184,"t":35998,"s":170},{"v":18170,"t":35999,"s":170},{"v":18165,"t":35998,"s":175},{"v":18172,"t":36000,"s":173},{"v":18168,"t":35999,"s":180},{"v":18172,"t":36000,"s":173},{"v":18156,"t":36000,"s":174},{"v":18182,"t":35999,"s":173},{"v":18174,"t":35998,"s":176},{"v":18156,"t":35999,"s":171},{"v":18169,"t":35998,"s":180},{"v":18152,"t":35999,"s":169},{"v":18128,"t":35999,"s":177},{"v":18108,"t":36000,"s":169},{"v":18146,"t":35999,"s":167},{"v":18122,"t":35998,"s":181},{"v":18119,"t":36000,"s":177},{"v":18136,"t":36000,"s":175},{"v":18150,"t":36000,"s":176},{"v":18151,"t":35999,"s":176},{"v":18107,"t":36000,"s":182},{"v":18137,"t":36000,"s":181},{"v":18116,"t":36000,"s":179},{"v":18128,"t":35998,"s":178},{"v":18101,"t":36000,"s":184},{"v":18108,"t":35999,"s":168},{"v":18120,"t":35999,"s":173},{"v":18143,"t":35999,"s":179},{"v":18103,"t":35998,"s":184},{"v":18123,"t":35998,"s":175},{"v":18117,"t":35998,"s":179},{"v":18109,"t":35999,"s":179},{"v":18119,"t":35999,"s":177},{"v":18131,"t":36000,"s":170},{"v":18121,"t":35999,"s":178},{"v":18113,"t":36000,"s":178},{"v":18131,"t":35999,"s":177},{"v":18127,"t":35999,"s":177},{"v":18116,"t":35998,"s":176},{"same":1},{"v":18135,"t":36000,"s":182},{"same":1},{"same":1},{"v":18122,"t":35999,"s":181},{"same":1},{"same":1},{"same":1},{"same":1},{"same":1},{"same":1}]}

mpm_demo/gen/bake_surface.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,22 @@
5555
# the particles so the splatted field is a connected
5656
# solid, not a cloud of separate per-particle blobs
5757
SIGMA = 1.5 # gaussian blur of the splat field, in cells
58-
ISO_FRAC = 0.40 # isolevel as a fraction of interior (median) density
58+
ISO_FRAC = 0.12 # isolevel as a fraction of interior (median) density.
59+
# Lower than intuition suggests: after the burst the
60+
# deposit settles into a 1-2-particle-thick carpet
61+
# (~2.5 particles per bake cell over the footprint),
62+
# and at 0.40 nearly a third of the spread snow sits
63+
# below the isolevel and silently vanishes (see the
64+
# disappearance analysis in REPORT.md). 0.12 keeps
65+
# ~98% of the carpet meshed at every frame.
5966
TARGET_TRIS = 36000 # decimation target per frame
60-
MIN_COMPONENT = 500 # drop blobs smaller than this many cells (their
61-
# particles land in the spray pass instead)
67+
MIN_COMPONENT = 120 # drop blobs smaller than this many cells (their
68+
# particles land in the spray pass instead). At the
69+
# coarser 2.1-spacing cell, 500 cells is a ~38mm chunk --
70+
# an eighth of a letter -- so real debris was being
71+
# classified as dust. 120 cells ~ 24mm: still well above
72+
# the numerical-noise scale, small enough to keep
73+
# genuine rubble.
6274
# Those four are really one decision: how hard the decimator has to work.
6375
# Triangle *size* is what produces flat-shard artifacts -- a debris chunk
6476
# that survives decimation as only 2-3 triangles reads as a floating polygon
@@ -74,7 +86,12 @@
7486
# in the viewer washes colors out; k-NN averaging also
7587
# pulls them toward the frosty mean)
7688
PAD = 6 # grid padding, cells
77-
MIRROR_BAND = 3.0 # mirror particles within this many cells of the floor
89+
MIRROR_BAND = 12.0 # mirror particles within this many cells of the floor.
90+
# The carpet's density foot is ~3 cells thick (Gaussian
91+
# skirt), so particles up to ~12 cells above the floor
92+
# are still part of the surface that benefits from the
93+
# doubled below-floor density; 3 cells only mirrored the
94+
# bottom skin and left the rest of the foot thin.
7895
SPRAY_FRAC = 0.028 # fraction of particles *eligible* to render as spray.
7996
# Chosen once per particle id, NOT per frame: an
8097
# independent random subsample each frame hands every

mpm_demo/gen/sim.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
ap.add_argument("--letters", type=int, default=0, help="sim only the first N letters (0 = whole word)")
7777
ap.add_argument("--out", default=None)
7878
ap.add_argument("--debug-plastic", action="store_true", help="print Jp (plastic state) stats per frame")
79+
ap.add_argument("--dump-jp", default=None, help="also save per-frame Jp history to this .npz path")
7980
args = ap.parse_args()
8081

8182
n_particles_target = args.particles
@@ -427,11 +428,14 @@ def main():
427428
ground_verts, ground_tris = build_ground_mesh()
428429

429430
frames = np.zeros((n_frames, n_particles, 3), dtype=np.float32)
431+
jp_frames = np.zeros((n_frames, n_particles), dtype=np.float32) if args.dump_jp else None
430432
t0 = time.time()
431433
for f in range(n_frames):
432434
for _ in range(substeps):
433435
substep()
434436
frames[f] = x.to_numpy()
437+
if jp_frames is not None:
438+
jp_frames[f] = Jp.to_numpy()
435439
if args.debug_plastic and (f % 4 == 0 or f == n_frames - 1):
436440
jp = Jp.to_numpy()
437441
print(f" [Jp] f{f:3d} min={jp.min():.4f} max={jp.max():.4f} "
@@ -454,6 +458,9 @@ def main():
454458
domain=np.array([Lx, Ly, Lz], dtype=np.float32),
455459
)
456460
print(f"saved {out_path} ({os.path.getsize(out_path) / 1e6:.1f} MB)")
461+
if jp_frames is not None:
462+
np.savez_compressed(args.dump_jp, jp=jp_frames)
463+
print(f"saved {args.dump_jp}")
457464

458465

459466
if __name__ == "__main__":

0 commit comments

Comments
 (0)