This is the WebGPU Spring Mesh solver embedded live: a cloth lattice integrated entirely on the GPU with compute shaders, at 60 fps. Drag any node to disturb it, orbit with the mouse, and tune the solver in its panel. The readout in the corner is measured from the running engine, not mocked. Browsers without WebGPU get the same physics on a CPU solver through WebGL2.

Technical Stack

Each frame splits into substeps, and each substep runs three GPU compute passes: predict (gravity plus velocity, damped), solve (the Jacobi distance-constraint sweep below, run twice per substep, ping-ponged between two position buffers), and finalize (velocity from the position delta). The kernel is authored in Three.js TSL and compiles to a WGSL compute shader. This is the actual solve kernel from the repository:

// solve: one Jacobi sweep of distance constraints (TSL → WGSL compute)
const makeSolve = (readAttr: Attr, writeAttr: Attr): Compute =>
  Fn(() => {
    const read  = storage(readAttr,  'vec4', count);
    const write = storage(writeAttr, 'vec4', count);
    const fixedNode = storage(this.fixedAttr, 'float', count);

    const idx  = int(instanceIndex);
    const yy   = idx.div(int(cols));
    const xx   = idx.sub(yy.mul(int(cols)));
    const self = read.element(instanceIndex).toVar();

    If(lockedAt(idx, fixedNode), () => {
      write.element(instanceIndex).assign(self);
    }).Else(() => {
      const p    = self.xyz.toVar();
      const corr = vec3(0, 0, 0).toVar();
      const cnt  = float(0).toVar();

      for (const o of NEIGHBOURS) {          // structural + shear + bend
        const nx = xx.add(int(o.dx));
        const ny = yy.add(int(o.dy));
        const inBounds = nx.greaterThanEqual(int(0)).and(nx.lessThan(int(cols)))
          .and(ny.greaterThanEqual(int(0))).and(ny.lessThan(int(rows)));
        If(inBounds, () => {
          const np   = read.element(ny.mul(int(cols)).add(nx)).xyz;
          const d    = np.sub(p);
          const len  = d.length().max(1e-4);
          const rest = float(o.rest * spacing);
          // Move toward the neighbour proportional to the rest-length error.
          corr.addAssign(d.div(len).mul(len.sub(rest)));
          cnt.addAssign(1);
        });
      }

      const moved = p.add(corr.div(cnt.max(1)).mul(U.stiffness));
      write.element(instanceIndex).assign(vec4(clampFloor(moved), self.w));
    });
  })().compute(count);