Spring Mesh
Warp a texture with a jiggly spring mesh (it's a WIP, mind the edge)
- texture
- motion_map
- mask
- images
- mask
Ever wanted your image to wobble like it's made of jelly? That's the idea behind Spring Mesh: it lays a grid of points over your texture, treats each point as a mass connected to its neighbors by springs, nudges the whole thing with a motion map, and renders the texture through the deformed grid. The result is a fluid, physics-y distortion - flags rippling, water wobbling, surfaces breathing - that's a completely different animal from a static warp.
Fair warning up front: the README calls this "still very much work in progress," and it shows. It's the roughest node in the pack - but for a particular kind of organic deformation there isn't much else that does this in ComfyUI, so if that's your use case, it's worth the tinkering.
How it works
Two distinct phases. First, a grid of cols × rows points is built. Each point is connected to its neighbors, and each has a spring pulling it back toward its original grid position. Two spring constants control these forces: spring_constant_nb (neighbor-to-neighbor) and spring_constant_grid (pull back to rest). Then, if a motion_map is connected, its pixel values - selected by offset_sampling - displace the points, scaled by offset_scale_x/offset_scale_y and centered on offset_zero. One code-level gotcha: the channel-to-axis mapping is reversed from how the pair reads - with RG, the G channel drives X and R drives Y. If your warp looks transposed, that's why.
The physics is an Euler integration run for t_steps per output frame, with time_step controlling integration size. The interesting mode is cumulative (default on): displacements from the motion map accumulate across frames, so the mesh keeps building momentum - that's what produces the jiggly, springy motion. Turn it off and the mesh re-reads the motion map fresh each frame.
Then the deformed mesh is rendered through OpenGL (via moderngl) with MSAA anti-aliasing, sampling your texture. show_vertices is a debug toggle that renders the wireframe mesh instead of the texture - invaluable while tuning.
The inputs that matter
texture- what gets warped.motion_map(optional) - what drives the warp. This is the heart of the effect; without it you just get a springy grid settling to rest.cols/rows- mesh density. 64×64 default is a good balance; higher = finer deformation, slower.offset_scale_x/offset_scale_y- how hard the motion map pushes the mesh. Start small (0.1 default) - this is the "amplitude" dial and it's easy to overshoot into chaos.spring_constant_nb/spring_constant_grid- stiffness. High values = tight, springy; low = floppy.t_steps/time_step- simulation fidelity per frame.cumulative- accumulate (jelly) vs reset (read motion map fresh) per frame.
Outputs: images and a mask (the texture's alpha, or the deformed mask if you connected one).
Install
From the Quasimondo pack. ComfyUI Manager → search ComfyUI-QuasimondoNodes, or:
cd ComfyUI/custom_nodes
git clone https://github.com/Quasimondo/ComfyUI-QuasimondoNodes
cd ComfyUI-QuasimondoNodes
pip install -r requirements.txt
This is one of the two nodes (with Custom Shader) that genuinely needs moderngl, plus OpenCV for image handling. A working OpenGL/EGL context is required - the same "no context, no render" wall as the shader node. No models to download.
Where people get burned
It's a WIP, so: instability. The Euler simulation can blow up - crank spring_constant_nb, t_steps, or time_step too far and the mesh explodes into wild oscillation or NaN garbage. If you see chaos, dial the physics back, don't push through it.
Second, the multi-frame path expects a motion_map: the code touches motion_map inside the frame loop, so animating with frames > 1 and no motion map is where the WIP-ness bites. Give it a motion map when you animate. And it's slow - the physics is a Python/numpy loop over all connections per step, so a dense mesh over many frames is genuinely expensive. Start coarse, confirm the look, then refine.
Inputs (22)
| Name | Type | Default | Description |
|---|---|---|---|
| texture | IMAGE | — | |
| frames | INT | 11–9999 | — |
| output_size | COMBO | 3 options: Custom, texture, motion_map | |
| width | INT | 10241–65536 | — |
| height | INT | 10241–65536 | — |
| cols | INT | 641–2048 | — |
| rows | INT | 641–2048 | — |
| offset_sampling | COMBO | 6 options: RG, GR, BG, GB, RB, BR | |
| offset_normalize | BOOLEAN | true | — |
| offset_zero | FLOAT | 0.000-1–1 | — |
| offset_scale_x | FLOAT | 0.1000-10–10 | — |
| offset_scale_y | FLOAT | 0.1000-10–10 | — |
| spring_constant_nb | FLOAT | 0.333-65536–65536 | — |
| spring_constant_grid | FLOAT | 0.333-65536–65536 | — |
| time_step | FLOAT | 0.5000.000001–1 | — |
| t_steps | INT | 11–500 | — |
| cumulative | BOOLEAN | true | — |
| sampling | COMBO | 2 options: LINEAR, NEAREST | |
| border | COMBO | 4 options: REPEAT/REPEAT, CLAMP/CLAMP, CLAMP/REPEAT, REPEAT/CLAMP | |
| show_vertices | BOOLEAN | false | — |
| motion_mapopt | IMAGE | — | |
| maskopt | MASK | — |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| images | IMAGE | — |
| mask | MASK | — |