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:

  1. Move the particle inward slightly
  2. Shrink its pscale radius 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:

  1. Move the particle inward
  2. 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:

ParameterSuggested ValueDescription
padding0.001safety margin from surface
move_ratio0.2portion handled by movement
max_move0.02limits movement to prevent overshoot
shrink_ratio1.0remaining correction handled by shrinking
min_pscale0.0005prevents bubbles disappearing
iters2improves stability

Typical behavior:

SettingResult
More movementbubbles slide along surface
More shrinkbubbles maintain stable distribution
Balancedbest 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.