Skip to content

Commit 86d4abc

Browse files
zlyfunctionclaude
andcommitted
Give the snow a fluffy clumpy texture and a snow ground, Disney-style
Follows the reference look (Stomakhin snow slab/snowball) the user pointed at. Three changes: - Baked clumpy micro-relief: after decimation+Taubin, the surface is pushed along its normal by a fixed 3D value-noise field (CLUMP=0.42 cell, CLUMP_FREQ=3/cell ~ 1.5cm clods). Fixed in space, so settled snow doesn't shimmer; applied after smoothing so the high-frequency part survives. - The ground is now packed snow (bright bluish-white, strong bump grain), not a bare pale floor -- matching the reference where the slab itself is snow, and giving the thin debris carpet a snow bed to sit on. - Hemisphere light now reads as an overcast sky (white above, cool snow-blue bounce below) so shadowed snow picks up the bluish tint; sun is slightly warm. Known limit, stated honestly: the settled carpet is still physically 1-2 particles (~5mm) thick, so a straight top-down view still reads as texture plus terrain rather than volume -- no renderer can give a 5mm sheet side area from above. Making the floor a real simulated snow bed the letters crater into would fix that, at the cost of a re-sim and ~2x payload. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d3dba4d commit 86d4abc

3 files changed

Lines changed: 64 additions & 8 deletions

File tree

2.29 KB
Binary file not shown.

mpm_demo/gen/bake_surface.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,41 @@
113113

114114

115115
SMOOTH_ITERS = 3 # Taubin smoothing passes on the decimated mesh
116+
CLUMP = 0.42 # baked clumpy micro-relief: fraction of a cell that the
117+
# decimated surface is pushed in/out along its normal by
118+
# a fixed 3D value-noise field. The Disney renders get
119+
# their "fluffy clods" from real grain-scale geometry;
120+
# at our 5mm cells that scale is far below the mesh, so
121+
# the nearest achievable thing is to dither the surface
122+
# with a fixed ~1-2cm noise. Fixed in space, not per
123+
# frame, so settled snow shimmers no more than before.
124+
CLUMP_FREQ = 3.0 # noise frequency, cycles per cell (above the 5mm mesh
125+
# scale, so it reads as grain rather than as lumps)
126+
_NOISE = None # lazily built fixed permutation + value table
127+
128+
129+
def _noise3(p):
130+
"""Deterministic trilinear value noise, p in cell units, out in [-1,1].
131+
Fixed table -> the same world point always gets the same value, so the
132+
relief is stable frame to frame even as the mesh is rebuilt."""
133+
global _NOISE
134+
if _NOISE is None:
135+
rng = np.random.default_rng(11)
136+
_NOISE = rng.random((64, 64, 64))
137+
i = np.floor(p).astype(np.int64)
138+
f = p - i
139+
f = f * f * (3 - 2 * f) # smoothstep
140+
i0 = i % 64
141+
i1 = (i + 1) % 64
142+
n = _NOISE
143+
c000 = n[i0[:, 0], i0[:, 1], i0[:, 2]]; c100 = n[i1[:, 0], i0[:, 1], i0[:, 2]]
144+
c010 = n[i0[:, 0], i1[:, 1], i0[:, 2]]; c110 = n[i1[:, 0], i1[:, 1], i0[:, 2]]
145+
c001 = n[i0[:, 0], i0[:, 1], i1[:, 2]]; c101 = n[i1[:, 0], i0[:, 1], i1[:, 2]]
146+
c011 = n[i0[:, 0], i1[:, 1], i1[:, 2]]; c111 = n[i1[:, 0], i1[:, 1], i1[:, 2]]
147+
x00 = c000 + f[:, 0] * (c100 - c000); x10 = c010 + f[:, 0] * (c110 - c010)
148+
x01 = c001 + f[:, 0] * (c101 - c001); x11 = c011 + f[:, 0] * (c111 - c011)
149+
y0 = x00 + f[:, 1] * (x10 - x00); y1 = x01 + f[:, 1] * (x11 - x01)
150+
return (y0 + f[:, 2] * (y1 - y0)) * 2 - 1
116151

117152

118153
def taubin_smooth(verts, faces, iters=SMOOTH_ITERS, lam=0.5, mu=-0.53):
@@ -196,6 +231,22 @@ def reconstruct(points, tree_colors_f32, cell, y_floor, iso):
196231
if SMOOTH_ITERS and len(faces):
197232
verts = taubin_smooth(verts, faces.astype(np.int64))
198233

234+
# bake the fluffy clods: push the decimated surface along its own normal
235+
# by the fixed noise field. Done *after* smoothing so the high-frequency
236+
# component survives (Taubin would otherwise iron exactly this out).
237+
if CLUMP and len(faces):
238+
v = verts.astype(np.float64)
239+
n = np.zeros_like(v)
240+
tris = v[faces.astype(np.int64)]
241+
fn = np.cross(tris[:, 1] - tris[:, 0], tris[:, 2] - tris[:, 0])
242+
for c in range(3):
243+
np.add.at(n, faces[:, c].astype(np.int64), fn)
244+
ln = np.linalg.norm(n, axis=1)
245+
ln[ln == 0] = 1.0
246+
n /= ln[:, None]
247+
disp = CLUMP * cell * _noise3(verts * (CLUMP_FREQ / cell))
248+
verts = (v + n * disp[:, None]).astype(np.float32)
249+
199250
tree = cKDTree(points)
200251
dist, nn = tree.query(verts, k=COLOR_K, workers=-1)
201252
# inverse-cube distance weighting: smooths noise among same-letter

mpm_demo/index.html

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,11 @@ <h2>WHAT IS THIS?</h2>
219219
controls.minDistance = 0.3;
220220
controls.maxDistance = 12;
221221

222-
const hemi = new THREE.HemisphereLight(0xffffff, 0x445566, 0.6);
222+
// hemisphere light like an overcast sky: white from above, cool snow-blue
223+
// bounce from below -- this is where snow's bluish shadow tint comes from
224+
const hemi = new THREE.HemisphereLight(0xffffff, 0x8fa8cc, 0.6);
223225
scene.add(hemi);
224-
const sun = new THREE.DirectionalLight(0xffffff, 2.4);
226+
const sun = new THREE.DirectionalLight(0xfff2e0, 2.4);
225227
sun.castShadow = true;
226228
sun.shadow.mapSize.set(2048, 2048);
227229
sun.shadow.bias = -0.0006;
@@ -246,6 +248,7 @@ <h2>WHAT IS THIS?</h2>
246248
ssaoPass.kernelRadius = 0.008;
247249
ssaoPass.minDistance = 0.00003;
248250
ssaoPass.maxDistance = 0.003;
251+
ssaoPass.output = SSAOPass.OUTPUT.Default;
249252
composer.addPass(ssaoPass);
250253
composer.addPass(new OutputPass());
251254

@@ -423,11 +426,13 @@ <h2>WHAT IS THIS?</h2>
423426
groundGeo.setAttribute('position', new THREE.BufferAttribute(gpos, 3));
424427
groundGeo.setIndex(g.indices);
425428
groundGeo.computeVertexNormals();
429+
// the ground *is* packed snow, like the slab in the Disney reference
430+
// footage -- bright, granular-textured, slightly bluish in shadow
426431
const groundMat = new THREE.MeshPhysicalMaterial({
427-
color: 0xc7d2e0, roughness: 0.9, metalness: 0.0, side: THREE.DoubleSide,
428-
sheen: 1.0, sheenRoughness: 0.75, sheenColor: 0xffffff,
429-
bumpMap: noiseTex, bumpScale: 0.0015,
430-
envMapIntensity: 0.3,
432+
color: 0xe8edf4, roughness: 0.95, metalness: 0.0, side: THREE.DoubleSide,
433+
sheen: 1.0, sheenRoughness: 0.85, sheenColor: 0xffffff,
434+
bumpMap: noiseTex, bumpScale: 0.006,
435+
envMapIntensity: 0.35,
431436
});
432437
const groundMesh = new THREE.Mesh(groundGeo, groundMat);
433438
groundMesh.receiveShadow = true;
@@ -531,12 +536,12 @@ <h2>WHAT IS THIS?</h2>
531536
surfGeo.setIndex(new THREE.BufferAttribute(new Uint16Array(surfMeta.maxTris * 3), 1));
532537
const surfMat = new THREE.MeshPhysicalMaterial({
533538
vertexColors: true, // geometry really has a `color` attribute here
534-
color: 0xffffff, roughness: 0.8, metalness: 0.0,
539+
color: 0xffffff, roughness: 0.85, metalness: 0.0,
535540
// sheen kept moderate: a full white sheen layer washes the
536541
// letter colors out to pastel
537542
sheen: 0.55, sheenRoughness: 0.5, sheenColor: 0xffffff,
538543
clearcoat: 0.1, clearcoatRoughness: 0.6,
539-
bumpMap: noiseTex, bumpScale: 0.0014,
544+
bumpMap: noiseTex, bumpScale: 0.0022,
540545
envMapIntensity: 0.3,
541546
});
542547
const surfMesh = new THREE.Mesh(surfGeo, surfMat);

0 commit comments

Comments
 (0)