▶ Live demo (requires a WebGPU- or WebGL2-capable browser)
A real-time, GPU-resident particle–mesh spring simulation rendered in the browser with WebGPU compute shaders authored in Three.js TSL (Three Shading Language). A structured lattice of point masses is connected by structural, shear, and bend springs; the entire integration runs on the GPU, and the cloth can be disrupted interactively by clicking and dragging any node. Browsers without WebGPU transparently fall back to a CPU solver rendered through the WebGL2 backend.
This is the browser / "JS view" counterpart to the differentiable spring systems
in jax-spring-sim and Mesh-Based-Physics-Simulator: same mass–spring physics,
here optimized for interactive 60 fps rendering rather than gradients.
pin pin
o-----o-----o-----o-----o
| \ | / | \ | / | structural ──
o-----o-----o-----o-----o shear ╲╱
| / | \ | / | \ | bend ┄┄ (2 apart)
o-----o-----o-----o-----o
↓ drag a node
The solver is small-step Position-Based Dynamics (Müller et al. 2007; Macklin, Small Steps in Physics Simulation, 2019), chosen over explicit springs because it is unconditionally stable — no slider combination can blow it up. Each frame is split into substeps; each substep runs three phases:
1. Predict — integrate external forces (gravity
2. Solve — project every distance constraint toward its rest length
3. Finalize — recover velocity from the constraint-corrected position delta,
with per-substep viscous damping
Springs follow the standard cloth model (Provot, Graphics Interface '95):
4-neighbour structural (
State lives in storage buffers. The constraint solve is ping-ponged each sweep so neighbour reads never alias the write; an even sweep count lands the result back in buffer A, which the render materials read:
per substep (compute passes):
predict ─▶ posA (gravity/wind, advance)
solve ─▶ posA⇄posB (Jacobi distance projection, ×iterations)
finalize─▶ posA (velocity = Δposition / dt, speed for colour)
render: InstancedMesh.positionNode ◀── read-only posA
nodes = icosahedra, coloured by speed
springs = screen-facing ribbons, coloured by signed strain
Each kernel builds its own storage nodes so WebGPU access modes are inferred
per-pipeline (sharing a node across a read- and a write-kernel makes TSL mark it
read-only and reject the write). Picking reads positions back once per grab via
getArrayBufferAsync; dragging then projects the pointer onto a camera-facing
plane through the grabbed node.
| Group | Parameters |
|---|---|
| Physics | gravity, stiffness (0–1), damping, mass |
| Wind | oscillating gust strength and frequency |
| Topology | density (N×N, rebuilds the mesh), pinning mode, floor collision |
| Simulation | substeps per frame, reset, pause |
Append ?cpu to the URL to force the CPU solver even where WebGPU is available.
Drag a node to disrupt · drag the background to orbit · scroll to zoom.
- Vite + TypeScript build.
- three @ r185 via the
three/webgpu+three/tslentry points. - WebGPU compute and rendering, with an automatic WebGL2 + CPU fallback.
- Hand-written CSS control overlay (no UI framework).
Note: this demo uses vanilla Three.js (not React Three Fiber) for tight control over the compute/render loop, and a hand-rolled CSS overlay instead of Tailwind, to keep the bundle to three.js alone. Both are deliberate deviations from the usual Vite + React + Tailwind stack.
webgpu-spring-mesh/
├── index.html
├── package.json
├── vite.config.ts
├── tsconfig.json
├── eslint.config.js
└── src/
├── main.ts # renderer, scene, loop, sim lifecycle
├── style.css # control overlay styling
├── core/
│ ├── params.ts # SimParams + defaults
│ ├── topology.ts # grid + spring graph construction
│ └── types.ts # ClothSim interface
├── sim/
│ ├── ClothSimGPU.ts # TSL compute solver (WebGPU)
│ └── ClothSimCPU.ts # JS solver (WebGL2 fallback)
├── interaction/
│ └── PointerDragger.ts # raycast pick + drag-to-disrupt
└── ui/
└── ControlPanel.ts # glassy DOM control overlay
npm install
npm run dev # http://localhost:5173
npm run build # type-check + production bundle to dist/
npm run lint # eslint
npm run typecheck # tsc --noEmitbase: './' in vite.config.ts keeps the build relocatable, so dist/ can be
dropped into a portfolio sub-path without rewrites.
- M. Müller, B. Heidelberger, M. Hennix, J. Ratcliff, "Position Based Dynamics," J. Visual Communication and Image Representation, 2007.
- M. Macklin, M. Müller, N. Chentanez, "Small Steps in Physics Simulation," ACM SIGGRAPH / Eurographics SCA, 2019.
- X. Provot, "Deformation constraints in a mass-spring model to describe rigid cloth behaviour," Graphics Interface, 1995.
- Three.js Shading Language (TSL) — https://github.com/mrdoob/three.js/wiki/Three.js-Shading-Language
