Below is a clean Markdown blog post you can publish directly on your site. I wrote it assuming your audience is Houdini artists and TDs, so it explains the problem clearly but still stays technical.
Keeping FLIP Bubbles Inside Geometry in Houdini
A Hybrid Move + Shrink Constraint Using VEX
When working with FLIP simulations and bubble particles in Houdini, one common problem appears quickly:
Bubble particles intersect the container surface.
This causes several downstream issues:
- Bubble spheres stick out of the mesh
- High-curvature areas cause particles to be pushed too far
- Particle meshing produces ugly bulges or crushed bubbles
A typical approach is to push particles inward along the SDF gradient, but this can produce artifacts near curved surfaces.
A better solution is to combine two constraints:
- Move the particle inward slightly
- Shrink its
pscaleradius to resolve the rest
This hybrid method keeps bubbles visually correct while preventing excessive positional correction.
This article explains how to implement this in Houdini using a Point Wrangle and an SDF surface.
The Concept
Each bubble particle has:
- a position
@P - a radius stored in
@pscale
To keep the particle fully inside a container, we need to ensure:
distance_to_surface <= -(pscale + padding)
Where:
- distance_to_surface = signed distance from the SDF
- positive = outside
- negative = inside
If a bubble violates this rule, we apply corrections:
- Move the particle inward
- Shrink its radius
Instead of forcing a full position correction, we split the correction between movement and shrinkage, giving us better stability and nicer meshing.
Preparing the Reference Surface
First we need an SDF representation of the container geometry.
Add a node:
VDB From Polygons
Settings:
Output Type: Signed Distance Field
Name: surface
Your node setup should look like:
container_geo
│
VDB From Polygons
│
Point Wrangle (bubble constraint)
Connect the particles to input 1 and the SDF VDB to input 2.
The VEX Code
Drop a Point Wrangle and paste the following code:
// Hybrid "keep inside" constraint using SDF
// Input 1: particles
// Input 2: SDF VDB named "surface"
float pad = chf("padding"); // extra margin
float move_ratio = chf("move_ratio"); // portion handled by movement
float max_move = chf("max_move"); // max movement per iteration
float shrink_ratio = chf("shrink_ratio"); // portion handled by shrinking
float min_ps = chf("min_pscale"); // clamp for pscale
int iters = chi("iters");
float r = max(@pscale, 0.0);
float target = -(r + pad);
for (int i = 0; i < iters; i++)
{
float d = volumesample(1, "surface", @P);
if (d <= target) break;
float need = d - target;
vector g = volumegradient(1, "surface", @P);
float gl = length(g);
if (gl < 1e-8) break;
vector outward = g / gl;
// Move inward
float move_amt = need * clamp(move_ratio, 0.0, 1.0);
if (max_move > 0)
move_amt = min(move_amt, max_move);
@P -= outward * move_amt;
// Recompute distance
d = volumesample(1, "surface", @P);
r = max(@pscale, 0.0);
target = -(r + pad);
float remain = max(0.0, d - target);
// Shrink bubble radius
float sr = clamp(shrink_ratio, 0.0, 1.0);
float dr = remain * sr;
if (dr > 0)
@pscale = max(min_ps, r - dr);
r = max(@pscale, 0.0);
target = -(r + pad);
if (move_ratio <= 0 && shrink_ratio <= 0)
break;
}
Recommended Parameters
Add the following parameters to the wrangle:
| Parameter | Suggested Value | Description |
|---|---|---|
| padding | 0.001 | safety margin from surface |
| move_ratio | 0.2 | portion handled by movement |
| max_move | 0.02 | limits movement to prevent overshoot |
| shrink_ratio | 1.0 | remaining correction handled by shrinking |
| min_pscale | 0.0005 | prevents bubbles disappearing |
| iters | 2 | improves stability |
Typical behavior:
| Setting | Result |
|---|---|
| More movement | bubbles slide along surface |
| More shrink | bubbles maintain stable distribution |
| Balanced | best results for meshing |
Optional: Bubble Regrowth
If bubbles shrink permanently, the simulation can lose volume.
A simple fix is to store the original bubble size and allow gradual regrowth.
Store the rest size
Run once before simulation:
if(@Frame == 1)
f@pscale_rest = @pscale;
Regrow slowly
float grow_rate = chf("grow_rate");
@pscale = min(f@pscale_rest, lerp(@pscale, f@pscale_rest, grow_rate));
This allows bubbles to recover their size when they move away from the surface.
Why This Works Better
Pure positional correction can cause:
- particle clustering
- exaggerated movement near curvature
- unstable meshing
The hybrid approach:
✔ keeps particles inside ✔ preserves a natural distribution ✔ produces cleaner particle meshes ✔ prevents curvature artifacts
When To Use This
This technique is useful for:
- FLIP bubble particles
- foam systems
- particle-based liquid meshing
- particle collisions with containers
It works particularly well with:
Particle Fluid Surface
VDB From Particles
custom metaball meshing
Final Result
Using this constraint you get:
- stable bubble motion
- cleaner fluid meshes
- particles that naturally conform to container surfaces
Without the ugly artifacts that often appear near curved boundaries.
If you're building large-scale FLIP setups, this small constraint can make a massive difference in visual quality.
Happy simming.