Most soft-body simulations are built the same way: put masses on a grid, connect neighbouring ones with springs, add shear and bend springs so the sheet doesn’t collapse, then solve the whole network every frame. It is a well-earned standard, and the first half of this post explains how it works — the spring types, the integrators, and why the naive one explodes.

The second half is about not using it. WobblePic wobbles photographs with a model that has no springs between vertices at all, and the reason it gets away with that says something about when the standard machinery is worth its cost.

A still photo of balloons being dragged and released — the bounce comes entirely from the simulation described below.

First, the standard model

It is worth walking through the conventional approach properly, because it is the right tool for most soft-body problems and because it makes clear what WobblePic is opting out of.

A mass-spring system represents a deformable object as point masses connected by springs. Each mass has a position, a velocity and a mass; each spring has a stiffness k and a rest length L₀, and pulls its two endpoints together when stretched, apart when compressed. That is Hooke’s law, written for a spring between points i and j:

F = -k · (|xᵢ - xⱼ| - L₀) · (xᵢ - xⱼ) / |xᵢ - xⱼ|

Damping usually rides along with it, proportional to the endpoints’ relative velocity along the spring, so that a stretched spring loses energy as it recoils rather than ringing forever.

Mesh Grid and Spring Network diagram

A grid of masses joined only to their immediate horizontal and vertical neighbours turns out to be useless — it folds flat under its own weight, because nothing resists a square becoming a parallelogram. The classic fix, from Provot’s 1995 cloth paper, is three kinds of spring layered on the same grid:

  • Structural springs join adjacent vertices horizontally and vertically. They resist stretching and give the sheet its basic dimensions.
  • Shear springs join diagonal neighbours. They resist the parallelogram collapse that structural springs alone allow.
  • Bend (or flexion) springs join vertices two steps apart. They resist folding, and their stiffness is what separates a stiff canvas from a limp silk.

Every frame then runs the same three phases:

  1. Accumulate forces. For each mass, sum the pull of every spring attached to it, plus gravity, damping, and whatever the user is doing. With three spring types on a grid, that is roughly six springs touching each interior vertex.
  2. Integrate. Convert the accumulated force into motion with F = ma. The choice of integrator matters more than beginners expect: plain explicit Euler — computing the new position from the old velocity — pumps energy into the system, so a cloth simulated with it will visibly gain amplitude and eventually detonate. Semi-implicit (symplectic) Euler, which updates velocity first and moves with the new value, costs the same and is stable across a far wider range. Verlet integration stores the previous position instead of an explicit velocity and is popular in game code for its stability under constraint tweaking. Implicit integration (Baraff and Witkin’s 1998 method) stays stable at very large time steps and stiff spring constants, at the price of solving a linear system each frame.
  3. Solve constraints. Springs alone are springy: pull hard enough and the sheet stretches like rubber. Cloth simulators typically add a post-integration pass that clamps each spring to some maximum strain, resolves collisions with other geometry, and enforces pinned points. Position-based dynamics (Müller et al., 2007) takes this idea to its conclusion and drops forces almost entirely, iterating directly on positions to satisfy constraints — which is why it dominates real-time game physics.

The reason this machinery exists is propagation. In a network, moving one vertex disturbs its neighbours, which disturb theirs; the deformation travels, which is exactly what you want when a character’s cloak catches on a corner and the wrinkle runs across the fabric. That is also the source of the difficulty: the springs are coupled, so stiffness and time step interact, and the whole system has to be solved together rather than vertex by vertex.

What WobblePic does instead: one spring per vertex

The image is laid out on a grid mesh — 512 cells per side by default, so 513 × 513 = 263,169 vertices. Each vertex carries a position, a velocity, and a rest position: the spot it occupied before anyone touched the picture.

There are no springs between vertices. Every vertex is attached by a single spring to its own rest position, and that is the entire model:

acceleration = -k · (position - rest) - c · velocity

The first term is Hooke’s law again, but the spring’s other end is not a neighbour — it is the vertex’s own starting point, which never moves. The second term is damping proportional to velocity, and it is the only damping in the system; with no springs between vertices there is no relative velocity along a spring to damp. Integration is semi-implicit Euler, the same choice as above and for the same reason:

velocity += acceleration · dt
position += velocity · dt

Compare that against the three phases of the standard loop and most of them simply disappear. Force accumulation is one subtraction per vertex instead of a sum over six springs. There is no constraint phase: nothing can exceed maximum strain, because there is no strain between points to measure; nothing can self-intersect, because no vertex is pulled by anything except its own anchor; nothing needs pinning, because vertices that should not move are already anchored where they started. Stiffness and time step stop interacting, since each vertex is an independent one-dimensional oscillator with a known analytic solution rather than a term in a coupled system.

Physically, this is a strange material. Real jelly transmits force between neighbouring bits of itself; here, each point of the photo is deaf to what its neighbours are doing. For cloth, a rope, or a 3D soft body, that would be disqualifying — propagation is the phenomenon.

For a photograph it is not, because the deformation is imposed rather than propagated. When you grab a picture, the shape of the pull is decided at the moment of the grab by the falloff described below: neighbours move because the grab moved them, not because a force travelled through the mesh. The network would be computing an answer that the falloff has already given. What remains for the physics to do is the return trip — and a return trip is exactly one spring’s worth of work.

The payoff is that 263,169 vertices integrate as three whole-array numpy operations instead of a graph traversal, which is what makes a mesh this dense affordable at 60 FPS in Python.

Drag and Release: Gaussian Falloff and Spring Restoration

The grab is a Gaussian

Dragging displaces vertices by a weight that falls off with distance from the point you grabbed:

weight = exp(-d² / 2r²)

d is measured from each vertex’s rest position, not its current one — measuring from the deformed positions would let the grip wander as the picture stretches under it.

The radius r is where a small design decision turned out to matter. It was originally a fixed value in normalized device coordinates, which meant the grip stayed the same absolute size no matter how large the picture was on screen: fine in a small window, uselessly tight once the image filled a 4K display. It is now a fraction of the image’s shorter edge — one sixth, tuned by feel, with a quarter coming out too loose and a ninth too stiff. When a segment is active the radius is derived from the object instead, so grabbing a balloon feels like grabbing a balloon and grabbing a cake feels like grabbing a cake.

Pull far enough and the grip lets go rather than stretching indefinitely. With up to four pins placed, each pin damps the drag around itself, and the pins combine as independent probabilities:

pin_weight = 1 - Π (1 - gᵢ)

Summing the pins and clipping would work too, but it saturates with a hard edge where two pins overlap. This form keeps each pin at full strength on its own and lets overlapping pins fade into each other smoothly.

Segmentation is a weight, not a second mesh

Clicking an object runs SAM2 and produces a mask, and it is tempting to assume the segmented object becomes its own mesh. It does not — there is still exactly one mesh. The mask becomes a per-vertex weight that multiplies the Gaussian above, so vertices outside the object are simply never displaced, and any that still hold leftover displacement are snapped back to rest and held there.

The background you see behind the wobbling object is a second render pass over the same vertex buffer, drawn from the rest positions. So “only the cake wobbles, the plate stays still” is not two simulations. It is one simulation, one texture, and a weight deciding who is allowed to move.

Damping: How Wobble Settles Down

Two sliders, one of which is the damping ratio

Elasticity sets the spring constant k, mapped exponentially so the ends of the slider are half and double the default. Stiffer springs snap back faster and let you pull further before the grip releases.

Bounce is more interesting: it sets the damping ratio ζ directly, and the damping coefficient is derived as c = ζ · 2√k. That 2√k is the critical damping value for the system, so ζ = 1 means exactly no oscillation — the picture returns to rest in one motion and stops. The slider runs from there down to ζ ≈ 0.16 at the top end. Deriving c from k rather than setting it independently is what keeps the bounce feeling consistent when you change Elasticity; otherwise every stiffness change would silently retune the bounce.

Three presets name the useful corners of that space, chosen by integrating the spring equation and reading off settling time and oscillation count rather than by eye:

PresetPeriodOscillationsSettles in
Spring0.40 s~61.38 s
Rubber0.51 s~20.80 s
Jelly0.63 s~51.87 s

Spring and Jelly both ring for a while, but they are not two points on one axis: Spring is fast and tight, Jelly is slow and wide.

Stopping is a feature

The physics is cheap. What was expensive, for a long time, was not doing the physics.

The simulation used to run whenever the image had been touched — meaning that after a single drag, every frame you spent looking at a settled picture still integrated all 263,169 vertices. At grid 512 that was 6.5 ms per frame, 39% of a 60 FPS budget, spent on a photograph standing perfectly still.

Fixing it needed the right question. “Has the user interacted?” is not the same as “is anything still moving?”, and only the second one belongs in the update loop. The check itself has to be cheap or it eats the savings — scanning displacement and velocity across both arrays costs 0.39 ms, half of the integration it is trying to avoid, so velocity is tested first and the check short-circuits at 0.09 ms in the common case where the picture is clearly still moving.

Then there was a surprise underneath. Most of that 6.5 ms was not arithmetic at all — it was denormal floats. As a wobble damps out, the values approach zero, and once they fall below the smallest normal float32 (1.18 × 10⁻³⁸) x86 drops into a microcode fallback that runs the same integration 7.9× slower. It appeared about ten seconds after the wobble settled and saturated around fifteen, so the app was at its slowest during precisely the quiet stretch when nothing was happening. A control run confirmed it: zeroing only the denormal entries in a settled mesh, touching nothing else, took the frame from 6.497 ms to 0.825 ms.

The fix is one line of intent — when the mesh settles, snap the positions to the rest array exactly rather than approximately. The denormals never get a chance to form, and is_moving becomes an assertion about the array rather than a threshold. Still frames now cost 0.0001 ms.

What it does not do

  • No gravity. A photo has no down.
  • No collision detection or self-intersection handling. With displacement imposed by a falloff rather than propagated through a network, the mesh has no way to fold through itself in the first place.
  • No level of detail. The grid is a fixed 512, and after the frame-budget work there was nothing left to buy by reducing it.
  • No spring network, as above — which is the reason the other three lines are so short.

The browser demo runs the same model in TypeScript, down to the same settle threshold, which is a decent argument that the model is small: the entire thing ports to a different language and a different renderer without a redesign.

None of which is an argument against spring networks. Hang cloth off a character, drape a rope over an edge, squash a 3D body against a floor, and the propagation you paid for is the whole effect — the falloff trick has nothing to say about any of them. The claim is narrower: a still photograph under a mouse is a case where the deformation is known in advance, and knowing that turns a coupled system into 263,169 independent ones.

If you want the layer above this one — how the deformed frames get captured without stalling the render loop — that is Recording Real-Time Graphics. For more on deformable materials in general, see What Is Soft-Body Physics?.