What does Procedural Cloth (No Simulation) do?
Source: room-at-dawn (MIT), src/scene/parts/bed.js for the quilt, blanket, and pillow. This separates it into a reusable library with a stage-by-stage example viewer.
Core idea: Cloth is not "simulated", it's "designed".
Define "how much higher each point of the cloth is above its support" on a flat coordinate plane, then wrap that plane onto the support.
The benefit is that each step is controllable and reproducible (same seed always gives the same result), builds in under 0.5 seconds,
and other objects (books, phones, another blanket) can directly query the same height function, guaranteeing they sit on the fabric's surface.
The trade-off is the cloth is static: it won't be pushed by characters (for interactivity, see references/engines.md).
Files
| Path | Content |
|---|---|
scripts/cloth.js | Library (ES module, depends only on three): RNG/noise, Field, Lobes, FoldFamily, CreaseSet, support drape, curl, grid, UV flattening, wrinkle textures, prints, cotton material, exportableMesh (pre-GLB conversion) |
example/index.html + main.js | An unmade quilt on a bed, ?stage=1..5 to incrementally add each effect |
scripts/curtain.js | Curtain: curtainPanel (cross-section integration without stretch), auditPanel, linenTextures, curtainMaterial (stitching, hem, storage creases, backlight transmission, micro-sway), curtainRing |
example/curtain.html | A pair of curtains hanging in front of a window, ?cam= (front, angle, top, hem, stitch, above), ?tied=1, ?fullness=, ?loops=, ?tex=checker |
example/preview-*.jpg | Finished and stage-by-stage preview images of the example (preview-curtain.jpg is the curtain) |
references/shape.md | Height field design: lobes, directional folds, peak erosion, creases, flat zones |
references/drape-curl.md | Supports, toppling, planar distortion, corner curl, hems, placing objects on cloth, adding a second layer |
references/uv-material.md | UV flattening, fine wrinkle normal map + cavity, prints, materials |
references/verification.md | What data to check, which camera angles to use, common symptom troubleshooting |
references/curtain.md | Curtains: cross-section integration and width solving, composition of θ(s), materials, parameters, verification, known issues |
references/engines.md | Porting to Godot/Unreal: exporting GLB, rewriting in GDScript/C++, integrating interactive cloth |
Running the Example
cd <skill folder> && python3 -m http.server 5420 --bind 127.0.0.1
# Open http://127.0.0.1:5420/example/
(<skill folder> is the Base directory shown when loading this skill; when installed as a plugin it is under ~/.claude/plugins/cache/)
URL parameters:
stage=1..5: respectively, lobes, add directional folds, add peak erosion, add creases, add corner curl and toppling.cam=wide|close|top|side|left|foot: camera presets.tex=checker: switch to a 10 cm checkerboard to check UV stretching.relax=0: turn off UV flattening for comparison.ui=0: hide the data panel, useful for screenshots.
When finished, window.__cloth exposes verification data, and body[data-ready="1"] indicates screenshots can be taken.
The "Download GLB" button can export the cloth for use in other engines.
Process
| Step | What it does | Why | Library functions |
|---|---|---|---|
| 1 | Define the outline (four corners) and "distance to hem" in flat coordinates | Used for hems, crease fading, corner curls | quadEdge |
| 2 | Lobes: manually place 5–10 ellipses, each with their own height and angle, combined with smooth-max | The large shape defines "what state is the quilt in"—noise can't achieve this. | makeLobes, lobesAt |
| 3 | Directional folds: 2–3 sets of billow noise with direction, switching direction per region | Folds in a quilt run along the direction of tension. | foldFamily |
| 4 | Vertical waves on the draping part; overall domain warp; soft cap to limit height | Draping fabric isn't flat; avoid ruler-straight lines; keep the tallest lobe from being excessive. | softCap |
| 5 | Peak erosion: each point is at most ~1.2 cm higher than its surroundings (after blurring twice), valleys stay sharp | Filled quilts don't have sharp peaks; this turns it from "mountains" into "a quilt". | Field.erodePeaks |
| 6 | Creases drawn one by one: fan out from pinch points, plus spread elsewhere following the tension field, attenuating near flat zones and hems | Creases made with noise look like marble; use "tent-shaped profile" line segments. | CreaseSet, crease |
| 7 | Hem: flat band, stitch groove, puckering | The edge is real geometry; it must be convincing up close. | example hemElev |
| 8 | Wrap onto support: flat area + rounded corners + vertical drop | A single plane can represent both the on-bed and off-bed parts. | boxSupport |
| 9 | High folds topple to one side: the tipping distance depends on the local height | A bulge becomes steep on one side and gentle on the other, not like a symmetrical dome. | example place |
| 10 | Corner curl: parts beyond the fold line are first rolled up along a cylinder, then laid flat back on the quilt; the flattened area is marked as a flat zone | Adds a storytelling detail. | curl |
| 11 | Flatten UV based on the 3D surface (multi-resolution mesh, edge-length constraints) | Prints aren't stretched on steep folds. | relaxUV, uvStretch |
| 12 | Material: tiled fine-wrinkle normal map (alpha stores cavity) + print + sheen | Geometry handles centimeter scale; textures handle millimeter scale. | wrinkleTexture, sprigPrintTexture, cottonMaterial |
Rules to Follow
- Random must use
RNG(seed)/pn(...,seed), neverMath.random(). Screenshot regression, baked lighting, and multi-user sync all rely on it. - Sample large shapes with cubic interpolation (
Field.cubic), creases with bilinear (Field.bil). If large shapes use bilinear, 5 mm grid facets become visible on the mesh. - Do not make creases with noise. Noise produces blobs; real creases are directional, tapering lines.
- Erode peaks first, then add creases. If the order is reversed, the ridge lines of creases will also be eroded.
- The flat zone under the curled corner must cover the entire folded range (the example uses
QMAXto compute it automatically). If it's not fully covered, the flipped part will stack on top of high lobes, and the pattern will stretch into stripes (this actually happened; seereferences/verification.md). - UV flattening uses the "unflipped" shape U; stretching of the flipped part must be checked separately.
relaxUVrequires the grid count to be a multiple of 8. - Draping cloth must not collide with objects underneath: the example makes the bed frame narrower than the mattress. Collision with anything other than the support is outside this method's scope.
- When two adjacent sides both drape, the corner needs a conical or triangular gore;
boxSupportdoes not handle this. Only allow two opposite sides to drape.
Numerical Starting Points (Measured in Example)
- Quilt flat thickness: 5 cm; lobe height ×
LOBE_SCALE(0.3 is normally slept on, 1 is piled into big rolls); maximum soft cap 23 cm. - Directional fold spacing ~12 cm, length ~60 cm, amplitude 3–4 cm; erosion radius 5 grid cells (2.5 cm), upper limit 1.2 cm.
- Creases: width 2.4–4 cm, height ~0.16–0.3× width, 30% are grooves; each pinch point 4–7 creases, other areas have ~200 scattered groups.
- Grid size ~7 mm (288×192), ~130k triangles, modeling time ~0.45 s (UV flattening ~0.15 s), zero interpenetration, UV stretch p5/p95 ~0.92/1.08.
Curtains (Hanging Fabric)
The quilt method is "height field wrapped onto a support"—not suitable for curtains. Curtains use: each horizontal cross-section is a fixed-length line segment,
drawn by integrating the travel angle θ(s) along the cloth, then using binary search to adjust the multiplier of θ so that the column exactly equals the required width at that height.
The cloth doesn't stretch, so UV is directly in meters, and textures don't deform. See references/curtain.md for details.
import { curtainPanel, auditPanel, linenTextures, curtainMaterial, curtainRing, CU } from './scripts/curtain.js';
const p = curtainPanel({ x0t: 0.27, x1t: 0.95, x0h: 0.25, x1h: 0.97, zc: 0.12, yTop: 2.29, yHem: 0.02, loops: 7, seed: 4101 });
const lin = linenTextures();
scene.add(new THREE.Mesh(p.geo, curtainMaterial({ map: lin.map, normalMap: lin.normalMap, yTop: 2.29 })));
console.log(auditPanel(p)); // crossings must be 0; stretchP95Pct should be < 1
- Measured in example: one panel ~100k triangles, built in 0.2 s, stretch p95 0.4–0.7 %, zero crossings.
- Pitfall: if
depthis too tight, the cloth loses 5–15 % width. If space is limited, lowerfullness; don't compress depth. - Pitfall: if the hem is too narrow, folds interpenetrate; the threshold depends on seed (the right panel in the example still had crossings at ~0.67×, only passed at 0.79×). Check
crossings; change seed if needed. - Default
depth[-0.11, 0.16] doesn't shorten the cloth, but the rod must be ~12 cm from the wall.