The Problem
I wanted to see how fabric moves before I cut it. Not a 2D overlay — actual physics. The kind where switching from silk to denim changes the fold radius, the cling, the way gravity pulls through the weave.
Professional tools exist (CLO3D, Marvelous Designer), but they're $500/year CAD packages designed for production pipelines. I wanted something that runs in a browser tab, reacts instantly to parameter changes, and lets you develop intuition for how different fabrics behave on different body shapes.
The core question: can you get physically plausible cloth simulation at 60fps in JavaScript?
The Approach
I chose Position-Based Dynamics over traditional mass-spring simulation. In a force-based system, you compute forces from spring stretching, integrate accelerations, and hope the timestep is small enough that nothing explodes. PBD flips this — you predict positions with Verlet integration, then iteratively correct them to satisfy constraints. It's less physically accurate but dramatically more stable, which matters when you're targeting real-time performance in a browser.
The cloth is a 70×70 particle grid (4,900 particles) connected by three types of constraints:
Structural springs connect adjacent particles horizontally and vertically. These resist stretching — they're what gives fabric its shape. Stiffness is boosted 1.2× from the base parameter (structuralK = min(1.0, stiffness * 1.2)) because structural integrity is non-negotiable.
Shear springs connect diagonal neighbors at restLength * sqrt(2). They resist skewing — without them, the cloth deforms like a parallelogram under gravity. Stiffness is 35% of structural.
Bend springs skip one particle, connecting every-other neighbor. These control fold radius — low bend stiffness means soft, flowing folds (silk at 0.008); high means stiff creases (denim at 0.12). Only 6% of structural stiffness by default, and I solve them every other iteration to save compute.
6 substeps × 10 iterations each = 60 solver passes/frame
For each substep:
1. Verlet predict: vel = (pos - prevPos) * damping
pos += vel + gravity * dt²
2. Solve constraints (10 iterations):
→ Structural springs (every iteration)
→ Shear springs (every iteration)
→ Bend springs (every 2nd iteration)
→ Collision response (every 3rd iteration)
3. Hard-clamp stretch limiting (1.12× max)
4. Final collision + velocity friction
5. Floor collision
6. Recompute vertex normalsFor collision, I approximate the torso with 28 overlapping spheres generated from a parametric body profile. Each height slice of the mannequin defines an elliptical cross-section (wider side-to-side than front-to-back), and I fill it with a center sphere plus left/right offset spheres. Sphere-point distance checks are O(1) per particle per sphere — fast enough to interleave with the constraint solver.
Rendering uses Three.js with a critical optimization: the simulation writes directly into THREE.BufferAttribute arrays. No intermediate copies. Each frame I mark posAttr.needsUpdate = true and the GPU picks up the new data. The cloth material uses MeshPhysicalMaterial with sheen parameters that simulate fabric luster — sheen color lerps toward white by 30%, with roughness varying per fabric preset.
The Hard Parts
Stretch limiting without killing natural motion. PBD with limited iterations means springs can extend beyond rest length before the solver converges. With only 10 iterations, I was seeing rubber-band artifacts — fabric stretching 20–30% then snapping back. The fix: a post-solve hard clamp in limitStretch() that enforces a MAX_STRETCH_RATIO of 1.12× on structural springs. One pass, negligible cost, eliminates the worst visual artifacts. The 12% threshold is a sweet spot — tight enough to prevent visible stretching, loose enough that the solver doesn't fight itself.
Collision that doesn't let cloth pop through the body. If you only resolve collisions at the end of the frame, the constraint solver can push particles through collision surfaces during its iterations. My solution: interleave collision every 3rd constraint iteration. This keeps cloth on the body surface during the solve, not just after.
The subtler problem is velocity. Position-only correction makes cloth bounce off the body unnaturally. In resolveCollisionsWithFriction(), I decompose the implicit velocity (position minus previous position) into normal and tangential components, kill the normal component entirely, and damp tangential by a friction coefficient. This single decomposition is what makes silk slide and denim grip.
v = pos - prevPos // implicit velocity
vn = dot(v, normal) // normal component
vt = v - vn * normal // tangential component
pos = sphereSurface // push outside
prevPos = pos - vt * (1 - friction) // encode corrected velMaking fabrics actually feel different. Six parameters control behavior: stiffness, weight, damping, bend stiffness, friction, and wind response. Silk at 0.4 stiffness and 0.008 bend stiffness flows and catches light; denim at 0.85 stiffness and 0.12 bend creates visible creases with stiff folds. The wind model uses layered sine waves across both time and position — sin(t*2.1 + py*0.15)*0.6 + sin(t*0.7 + px*0.08)*0.4 — to create organic turbulence. Chiffon at wind coefficient 0.5 billows dramatically; denim at 0.05 barely moves.
Getting the presets right meant iterating until each fabric felt intuitively correct — you should be able to tell silk from cotton at a glance without reading the label.
What I'd Do Differently
The solver is CPU-bound. All 60 solver passes per frame run single-threaded in JavaScript. WebGPU compute shaders could parallelize the constraint solver — each spring correction is independent within a color-grouped iteration, making it suitable for GPU dispatch. I'd also move normal computation to a compute shader; accumulating face normals to vertices is a classic parallel reduction.
I'd replace the skip-one bend springs with proper dihedral angle constraints. The current approach approximates fold resistance with distance constraints between particles two apart, which is fast but doesn't capture the actual angular relationship between adjacent triangles. Real dihedral constraints would give more physically accurate folds at steep angles — the difference would be most visible in heavy fabrics like wool where fold shape matters.
The 28-sphere collision body works, but it has blind spots — thin gaps between spheres where particles can slip through at high velocities. A spatial hash with continuous collision detection (computing time-of-impact rather than per-frame position snap) would close these gaps entirely and enable cloth-on-cloth interaction, which the current system can't do.