I suspect that mixing two or more disparate fluids is one of the first challenges Houdini newbies, like myself, set themselves. I blogged about an approach recently in Stripy Viscous Fluid Impacts in Houdini - I created a single point cloud to act as the fluid's initial location, with different colors depending on the position of each point.

The slight problem with this approach is that because the fluid has a single surface, Houdini doesn't render any interior detail showing how different fluids interact. To simulate the effect in the video above, where drops of a blue liquid fall into a body of a red liquid, my new approach creates two separate fluid surfaces and that missing interior detail is visible.

As well as looking better, this technique allows for totally different materials on each fluid. You could, for example, animate pouring mercury or oil into water.

Creating the liquid geometry source

The first step is to create the point cloud that is used to define the initial location of the fluid - the body of the liquid and the three drops. This is a geometry node with a tube for the liquid body and three spheres for the drops:



There are some important extra steps:


  • The two wrangle nodes set the density: my drips are more dense than the main liquid body, so I explicitly set the density of each in VEX with @density = 4; for the drips and 1 for the body.
  • The two group nodes assigns groups to the different liquids, DRIP and BODY, these will be used later when creating the surfaces.
  • The Point Separation in both Scatter nodes is set to 0.2 which will match the Particle Separation in the FLIP Object.
With the geometry in place, I used the FLIP Fluid from Object shelf tool to create a fluid from the geometry.

Building the glass

The drinking glass was created from two tubes with slightly different radii subtracted from each other with a Cookie node. A third, shorter tube acts as the base:




Containing the fluid

After building the glass, I went ahead and converted it to a rigid body hoping it would contain the fluid. However, no amount of fiddling would make its concave shape work nicely. 

The solution was to create a rigid body from another tube (with end caps) and check "invert sign" in the static object node inside the DOP Network. 

Creating Multiple Fluid Surfaces

Now for the magic. The FLIP Fluid from Object shelf too creates a network to render the fluid surface. Using a Blast node (added just before the Fluid Compress), I delete all the points from one group (i.e. check Delete Non Selected and set the Group Type to points), then copy the entire network and change the group name in the Blast node to the other group. 

These two fluid surface networks can be assigned their own materials. In my project, I simply copied the basic liquid and changed the colors so that I have red and blue liquids.

The Object Merge node in the fluid interior network allows for multiple objects and I simply add the copied render node to that.

Gotchas

All well and good. However, since I slowed time down quite a lot, my fluid (especially the drips) was quite jittery. My first attempt to resolve this was to add smoothing to the Particle Fluid Surface. This went some way to helping, but fluid blobs would magically appear and vanish depending on smoothing.

My solution is to increase the fluid substeps, up the Velocity Smoothing in FLIP Solver to 1, and check Limit Refinement in the Particle Fluid Nodes.

@cgcris_com has also suggested turning off Rebuild SDF in the Particle Fluid Surface node. The video above hasn't implemented this suggestion, but I'll try that soon.
0

Add a comment


  1. It's been a fairly busy few months at my "proper" job, so my recreational Houdini tinkering has taken a bit of a back seat. However, when I saw my Swarm Chemistry hero, Hiroki Sayama tweeting a link to How a life-like system emerges from a simple particle motion law, I thought I'd dust off Houdini to see if I could implement this model in VEX.

    The paper discusses a simple particle system, named Primordial Particle Systems (PPS), that leads to life-like structures through morphogenesis. Each particle in the system is defined by its position and heading and, with each step in the simulation, alters its heading based on the PPS rule and moves forward at a defined speed. The heading is updated based on the number of neighbors to the particle's left and right. 

    The project set up is super simple: 



    Inside a geometry node, I create a grid, and randomly scatter 19,000 points across it. An attribute wrangle node assigns a random value to @angle:
    @angle = $PI * 2 * rand(@ptnum); 
    The real magic happens inside another attribute wrangle inside the solver.

    In a nutshell, my VEX code iterates over each point's neighbors and sums the neighbor count to its left and right. To figure out the chirality, I use some simple trigonometry to rotate the vector defined by the current particle and the neighbor by the current particle's angle, then calculate the angle of the rotated vector. 
    while(pciterate(pointCloud)) {

        vector otherPosition;
        pcimport(pointCloud, "P", otherPosition);

        vector2 offsetPosition = set(otherPosition.x - @P.x, otherPosition.z - @P.z);
        float xx = offsetPosition.x * cos(-@angle) - offsetPosition.y * sin(-@angle);
        float yy = offsetPosition.x * sin(-@angle) + offsetPosition.y * cos(-@angle);
        
        float otherAngle = atan2(yy, xx); 

        if (otherAngle >= 0) {
            L++;
        } 
        else {
            R++;
        }   
    }
    After iterating over the nearby particles, I update the angle based on the PPS rule:
    float N = float(L + R);
    @angle += alpha + beta * N * sign(R - L);
    ...and, finally, I can update the particle's position based on its angle and speed:
    vector velocity = set(cos(@angle) * @speed, 0.0, sin(@angle) * @speed);  
    @P += velocity ;
    Not quite finally, because to make things pretty, I update the color using the number of neighbors to control hue:
    @Cd = hsvtorgb(N / maxParticles, 1.0, 1.0); 
    Easy!

    Solitons Emerging from Tweaked Model



    I couldn't help tinkering with the published PPS math by making the speed a function of the number of local neighbors:
    @speed = 1.5 * (N / maxParticles);
    In the video above, alpha is 182° and beta is -13°.

    References

    Schmickl, T. et al. How a life-like system emerges from a simple particle motion law. Sci. Rep. 6, 37969; doi: 10.1038/srep37969 (2016).


    5

    View comments


  2. This blog post discusses a technique for rendering SideFX Houdini FLIP fluids as sparse fields of wormlike particles (hence my slightly over-the-top Sparse Vermiform moniker) with their color based on the fluid system's gas pressure field.

    The project begins with an oblate spheroid that is converted to a FLIP Fluid using the FLIP Fluid from Object shelf tool. The fluid object sits within a box that's converted to a static body with its volume inverted to act as a container.

    To introduce a wave-like motion to the fluid, I decided to animate gravity by adding an expression to the z channel of the default gravity node: sin($F * 2) * 3.333.

    Now for the magic (that probably everybody knew apart from me!): delving into FLIP fluid object's FLIP Configure Object, you can find a series of vector and scalar field nodes for different fields such as velocity, mass density, divergence and pressure. Passing the gas pressure field to each particle is easy as adding a Gas Field to Particle node to the DOP network and plugging it into the FLIP Solver:


    I set both the attribute and source field to pressure - now, each particle now has an @pressure attribute that corresponds to the local gas pressure in the FLIP fluid. Although I'm using this attribute to set the particle color, it could be used for many other effects, such as making viscosity or temperature a function of pressure.

    Moving onto rendering, I trashed the automatically created nodes for creating a fluid surface and created my own geometry node containing:


    The sparsity is introduced with a Delete node that culls points based on their @ptnum attribute using "delete by expression": (@ptnum % 75) != 0.

    Now that each point has a @pressure, the Attribute Wrangle node uses that to generate an RGB color from a hue based on pressure and populate the @Cd attribute:

        float hue = smooth(0, 1, abs(sin(@pressure * 0.1)));
        @Cd = hsvtorgb(hue, 1, 1);

    The Trail node with a "connect as polygons" result type plugged into a Wireframe node yields the little worms. 

    Voila! 



    3

    View comments


  3. This post continues from my recent blog entry, Particle Advection by Reaction Diffusion in SideFX Houdini. In this project, I've done away with the VEX in the DOP network, and replaced it with a VDB Analysis node to create a vector field that represents the gradient in the reaction diffusion volume. This allows me to use a POP Advect by Volumes node in the DOP network rather than hand coding by own force wrangle.

    The network that creates the advecting force is:



    The middle network box, Reaction Diffusion Field, is pretty much identical to the Gray Scott reaction diffusion solver from my previous article. However, this time the v chemical species is used to define the density of another VDB in the Reaction Diffusion Geometry network box, on the right. After converting that VDB to polygons, I get a nice geometry representation of my reaction diffusion system:



    This solver isn't super fast, so rather than recalculating it every time I run the simulation, it's saved to a file using a File Cache node. 

    The Gradient Field network box creates a vector field from the cached geometry. Here, the geometry is converted to a fog VDB and that, in turn, is converted to a vector field using a VDB Analysis gradient operator. I played around by adding intermediate nodes to resample and convert the VDB topology to SDF to get an effective field. 

    I used the POP shelf tool to create a particle system and simply added a POP Advect by Volumes to advect the particles. 

    Easy ðŸ™‚ 



    0

    Add a comment


  4. After watching this excellent tutorial that discusses advecting particles by magnetic fields to create an animation of the sun, I was inspired to use the same technique to advect particles by fields that are a function of reaction diffusion systems

    The source for my reaction diffusion vector field is a geometry node. This field this geometry node outputs defines the concentrations of the component chemical-species that will later be used to advect the particles:



    The VDB node in this network defines the name of the field, I've used rdField, and the type: a Vector Float. The Volume Wrangle defines the initial state: I want a central cube where the  field's z is high surrounded by an area where the x is high:
    if (@P.x > -0.1 && @P.x < 0.1 &&
        @P.y > -0.1 && @P.y < 0.1 &&
        @P.z > -0.1 && @P.z < 0.1) {
     
            v@foo.x *= 0.1;
            v@foo.z += rand(@P * 11) * 5;
    }
    else {
            v@foo.z *= 0;
            v@foo.x += rand(@P * 11) * 5;
    }
    Inside the solver, I iterate over the volume, applying the VEX to create the reaction diffusion simulation. I've done this many times in two dimensions and adding a third dimension is easy for VDBs in Houdini:
    float delta = volumevoxeldiameter(0, "rdField") / sqrt(3);  
    vector laplacian = set(0, 0, 0);
    laplacian += volumesamplev(0, "
    rdField", v@P + set( delta,  0,  0));
    laplacian += volumesamplev(0, "
    rdField", v@P + set( -delta, 0,  0));
    laplacian += volumesamplev(0, "
    rdField", v@P + set( 0,  delta,  0));
    laplacian += volumesamplev(0, "
    rdField", v@P + set( 0, -delta,  0));
    laplacian += volumesamplev(0, "
    rdField", v@P + set( 0,  0,  delta));
    laplacian += volumesamplev(0, "
    rdField", v@P + set( 0,  0, -delta));
    laplacian -= (@
    rdField * 6.0);  
    float dU = 0.189;
    float dV = 0.03;
    float k = 0.082;
    float f = 0.0425;
    float timeStep = 0.75; 
     
    float u = v@foo.x;
    float v = v@foo.z;
     
    float reactionRate = u * v * v;
    float deltaU = (dU * laplacian.x) - reactionRate + f * (1 - u);
    float deltaV = (dV * laplacian.z) + reactionRate - (f + k) * v;
     
    float newU = clamp(u + deltaU * timeStep, 0, 1);
    float newV = clamp(v + deltaV * timeStep, 0, 1);
     
    v@rdField = set(newU, newU, newV); 
    The Laplacian is generated by sampling the neighboring voxels at each side of the current voxel. The VEX functions volumevoxeldiameter and volumesamplev make this pretty simple. Dividing the former by the square root of 3 gives the voxel side length.

    To assist during development, I added a volume slice. Sure enough, after a handful of iterations, my little box in the centre of the volume has now "evolved" based on the Gray-Scott model:



    The next step is to use the shelf tools to create a particle system and a DOP Network:



    Houdini does have a POP Advect by Volumes node, but that expects volumes where the vector fields define velocity. My vector field defines values to which I want particles advected towards. So, the advecting happens inside a POP Wrangle node with the Input 2 set to the output of the geometry node discussed above. The VEX in this node looks at the surrounding voxels and sets the force based on the difference of opposite neighboring voxels:
    float mult = 10; 
    float delta = volumevoxeldiameter(1, "rdField") / sqrt(3); 

    v@force.x += volumesamplev(1, "rdField", v@P + set( delta,  0,  0)).b * mult;
    v@force.x -= volumesamplev(1, "rdField", v@P + set( -delta, 0,  0)).b * mult;

    v@force.y += volumesamplev(1, "rdField", v@P + set( 0,  delta,  0)).b * mult;
    v@force.y -= volumesamplev(1, "rdField", v@P + set( 0, -delta,  0)).b * mult;

    v@force.z += volumesamplev(1, "rdField", v@P + set( 0,  0,  delta)).b * mult;
    v@force.z -= volumesamplev(1, "rdField", v@P + set( 0,  0, -delta)).b * mult;
    A few additional nodes keep the particles within the bounding box and add a speed limit and some drag.

    The second clip takes things a little further. First of all, the Gray-Scott parameters are slightly tweaked to yield a solitons rather than a coral effect:
    float dU = 0.2097;
    float dV = 0.080;
    float k = 0.062;
    float f = 0.025;
    float timeStep = 0.5; 
    I then extended the POP Wrangle in the DOP Network to add an additional force: not only are the particles advected by the relative nearby chemical strengths in the volume, they are also advected towards other nearby particles. The Attributes from Volume node copies the rdField into the particle's Cd and the VEX uses that:
    float mult = 10;
    float delta = volumevoxeldiameter(1, "
    rdField") / sqrt(3);
    v@force.x += volumesamplev(1, "
    rdField", v@P + set( delta,  0,  0)).b * mult;
    v@force.x -= volumesamplev(1, "
    rdField", v@P + set( -delta, 0,  0)).b * mult;
    v@force.y += volumesamplev(1, "
    rdField", v@P + set( 0,  delta,  0)).b * mult;
    v@force.y -= volumesamplev(1, "
    rdField", v@P + set( 0, -delta,  0)).b * mult;
    v@force.z += volumesamplev(1, "
    rdField", v@P + set( 0,  0,  delta)).b * mult;
    v@force.z -= volumesamplev(1, "
    rdField", v@P + set( 0,  0, -delta)).b * mult; 
    int point;
    int count = 48;
    int neighbours[] = nearpoints(0, v@P, 2, count); 
     
    foreach(point; neighbours) {
        vector otherColor = point(0, "Cd", point);
        vector otherPosition = point(0, "P", point);
        float dist = distance(@P, otherPosition);  
        vector direction = normalize(otherPosition - @P);
     
        if (dist < 0.1) {
            dist = 0.5;
            direction = -direction;
        }
     
        v@force += 0.04 * direction * (1 / pow(dist,2)) * otherColor.b;
    The particles are rendered as little strands by giving them trails that feed into a Wireframe node:

    The final clip in the video above is based on the Belousov-Zhabotinsky Reaction, so the solve VEX is rather different:
    float alpha = 1.3;
    float beta = 1.55; //1.55;
    float gamma = 1.65;
    float delta = volumevoxeldiameter(0, "rdField") / sqrt(3);
    vector accumulator = set(0, 0, 0); 
     
    // sides...
    accumulator += volumesamplev(0, "rdField", v@P + set( delta,  0,  0));
    accumulator += volumesamplev(0, "rdField", v@P + set( -delta, 0,  0));
    accumulator += volumesamplev(0, "rdField", v@P + set( 0,  delta,  0));
    accumulator += volumesamplev(0, "rdField", v@P + set( 0, -delta,  0));
    accumulator += volumesamplev(0, "rdField", v@P + set( 0,  0,  delta));
    accumulator += volumesamplev(0, "rdField", v@P + set( 0,  0, -delta));
    // top corners...
    accumulator += volumesamplev(0, "rdField", v@P + set(  delta, -delta,  delta));
    accumulator += volumesamplev(0, "rdField", v@P + set( -delta, -delta,  delta));
    accumulator += volumesamplev(0, "rdField", v@P + set(  delta, -delta, -delta));
    accumulator += volumesamplev(0, "rdField", v@P + set( -delta, -delta, -delta));
    // bottom corners...
    accumulator += volumesamplev(0, "rdField", v@P + set(  delta, delta,  delta));
    accumulator += volumesamplev(0, "rdField", v@P + set( -delta, delta,  delta));
    accumulator += volumesamplev(0, "rdField", v@P + set(  delta, delta, -delta));
    accumulator += volumesamplev(0, "rdField", v@P + set( -delta, delta, -delta));
    accumulator += @rdField;
    vector average = ((@rdField * 2) + (accumulator / 15)) / 3;
     
    float a = average.x + average.x * (alpha * gamma * average.y) - average.z;
    float b = average.y + average.y * ((beta * average.z) - (alpha * average.x));
    float c = average.z + average.z * ((gamma * average.x) - (beta * average.y)); 
     
    a = clamp(a, 0, 1);
    b = clamp(b, 0, 1);
    c = clamp(c, 0, 1);
     
    v@rdField = set(a, b, c);
    v@rdField.r += (rand(@P * @Time) * 0.05) - 0.025;
    v@rdField.g += (rand(13 * @P * @Time) * 0.05) - 0.025;
    v@rdField.b += (rand(17 * @P * @Time) * 0.05) - 0.025; 
    In this example, the positions of the trails are smoothed out to get rid of any sharp angles:











    1

    View comments


  5. I played with animating mitosis in Houdini last year (see Simulating Mitosis in Houdini), but the math wasn't quite right, so I thought I'd revisit my VEX to see if it could be improved. After some tinkering, the video above shows my latest (hopefully improved) results.

    The basic project remains the same: 




    • A point generate geometry node creates a single source point
    • An attribute wrangle creates three new attributes for the point used for the mitosis animation:
      • float @age
      • float @numChildren
      • vector @velocity
    • A jitter node which is useful if the generate node is used to create more than one point
    • A solver node to iterate over the geometry and is where the magic happens
    The VEX inside the solver is where the point separation and generation happens:


    int pointCloud = pcopen(0, "P", @P, 3 , 50);
    vector localCentre = pcfilter(pointCloud, "P");
     
    vector direction = normalize(v@P - localCentre); 
    float r = distance(v@P, localCentre);
    if(r < 1) {
        r = 2 - (r * 2);
    }
     
    v@velocity += (direction / (r * r)) * 0.1; 
    if (@age > 1 && @numChildren < 3) {
        vector randOffset;

        randOffset.x = -0.01 + (noise(localCentre.x + @Time + @age) * 0.02);
        randOffset.y = -0.01 + (noise(localCentre.y + @Time + @age) * 0.02);
        randOffset.z = -0.01 + (noise(localCentre.z + @Time + @age) * 0.02);

        @age = 0;
        @numChildren += 1;
     
        int newPoint = addpoint(geoself(), @P + randOffset);
    }
    else {
        @age += abs((random($FF * @ptnum)) * 0.05);
    }

    @P += @velocity;
    v@velocity *= 0.5;

    As in my previous version, I find the average position of each point's neighbors. However, this time, I create a direction vector that is normalized (i.e. always has a length of 1) and a separate float, r, which is the distance of each point to the local average position. This approach allows me to fake the distance for points close to the local center (so they don't fly off into infinity) and get a bit Newtonian with the math used to calculate the velocity (i.e. by dividing by r²).

    The mitosing point cloud is used to generate two VDB based polygon surfaces, the larger of which is smoothed to give the nice metaball effect as the points move apart:


    Finally, I gave the outer surface a thin film refracting material, added some depth-of-field, and, voilà, little mitosing blobs! 





    4

    View comments


  6. This post describes a simple way to create a system comprising of a regularly surfaced fluid and a faux grain system. The video above contains three clips using the same basic technique: creating a single point source for the FLIP SOP initial data but using groups to render some as a fluid and some as individual tiny spheres - the grains. 

    The first clip shows a granular sphere dropping into a fluid tank. The initial fluid tank geometry network looks like this:



    I added a blast node to the fluid surface node created by the fluid tank shelf tool to remove all the particles from the DRIP group. Then created another node to render the DRIP group particles as faux grains:



    Not bad for a first attempt - the granular sphere splashes into the fluid tank and its grains are advected by the fluid. 

    The second clip flips the effect round: a fluid sphere falls into a torus built from hundreds-and-thousands (or nonpareils for those across the pond). The fundaments are the same, but I use an attribute wrangle in the source geometry node to give the sugary donut a high viscosity to retain its shape.

    To get the grains to dissolve (maybe disintegrate is a better word), I diffuse the viscosity. This is done with a POP wrangle node attached to the FLIP solver with the following VEX:


    int pointCloud = pcopen(geoself(), 'P', @P, 0.3, 48); 
    float newViscosity = pcfilter(pointCloud, 'viscosity'); 
    if (newViscosity < @viscosity) {
        @viscosity =  newViscosity;
    }
    The VEX averages the viscosity of 48 nearby particles and, if less than the current particle's viscosity, updates the current viscosity with that average. Only updating the viscosity with a smaller value ensure the fluid doesn't congeal.

    The third and final clip is another experiment dropping a granular body into a liquid. Here, I wanted the granular geometry to not only disintegrate but its component grains to separate.  As part of my debugging process, I was updating the point color based on viscosity - since it seemed like a nice effect, I kept that in too.

    The final VEX for my POP wrangle is:
    if (inpointgroup(geoself(), "PILL", @ptnum)) {

        // Diffuse viscosity...
       
        int pointCloud = pcopen(geoself(), 'P', @P, 0.4, 36);
       
        float newViscosity = pcfilter(pointCloud, 'viscosity');
       
        if (newViscosity < @viscosity) {
            @viscosity =  newViscosity;
        }
       
        @Cd = set((@viscosity / 3.0), 
                  1 - (@viscosity / 3.0), 
                  1 - (@viscosity / 3.0));
       
        // Separate low viscosity particles...
       
        float viscosityLimit = 2.0;
       
        if (@viscosity < viscosityLimit) {
           
            float maxdist = 0.1;
            int maxpts = 36;
            float seperationStrength = (viscosityLimit - @viscosity) * -50;
       
            int neighbours[] = nearpoints(geoself(), v@P, maxdist, maxpts);
            int point;
           
            foreach(point; neighbours) {
                if (inpointgroup(geoself(), "PILL", point)) {
                    @force += (point(geoself(), "P", point) - @P) * seperationStrength;
                }
            }
        }
       
    }
    As I'd been tinkering with this technique for a while, I decided to speed things up in the third clip by not rendering the fluid - but actually, I think the final result is quite nice just with the spheres.

    A few caveats: the prevent mysteriously appearing grains, the birth threshold under particle motion / reseeding in my FLIP solver is zero. Also, the POP wrangle nodes attached to the FLIP solver have their first input set to myself


    1

    View comments


  7. Fibonacci spheres are created from a point set that follows a spiral path to form a sphere (you can see an example, with code at OpenProcessing).

    For the video above, I used the approach described in the Entagma Strange Attractors tutorial, but changed the VEX in my solver to:
    if (inpointgroup(geoself(), "active", @ptnum)) { 
        float samples = 500; 
        float offset = 2.0 / samples; 
        float increment = $PI * (3.0 - sqrt(2.0)); 
        float i = @ptnum; 
        float y = ((i * offset) - 1) + (offset / 5); 
        float r = sqrt(1 - pow(y,2)); 
        float phi = (i % samples) * increment; 
        float x = cos(phi) * r * 1.5; 
        float z = sin(phi) * r * 1.5; 
         
        vector p = set(x, y, z);  
        int newPoint = addpoint(geoself(), p);  
        if (newPoint <= samples) { 
            setpointgroup(geoself(), "active", newPoint, 1); 
        } 
        setpointgroup(geoself(), "active", @ptnum, 0);  
        int previous = @ptnum - 4; // second clip is simply `@ptnum`  
        if(previous > 0) { 
            int polyline = addprim(geoself(), "polyline"); 
            addvertex(geoself(), polyline, previous); 
            addvertex(geoself(), polyline, newPoint);  
        }     
    } 
    This code (the core algorithm is borrowed from this StackOverflow answer) yields a complete sphere over 500 frames.

    To jazz things up a little, the parametric geometry lives inside a spherical volume with a geometry area light in its centre - this gives a nice fog that receives shadows.

    The first clip was raytraced, which gives lovely results but takes an age. The second clip is micropolygon rendered, which is much faster but not half as pretty.
    0

    Add a comment


  8. This video contains five clips using SideFX Houdini's Grain Solver with an attached POP Wrangle that uses VEX to generate custom forces. Here's a quick rundown of the VEX I used for each clip (please forgive the use of a variable named oomph).

    Clip One "Twin Peaks"

    Here, I compare each grain's current angle to the scene's origin to the current time. If the grain's angle is within a certain distance of the current time, I calculate a vertical force based on the Gaussian function of the distance of the particle from the origin The sigma of the Gaussian function is based on the sine of the time to give a rotating and pulsating force.
    float e = 2.7182818284590452353602874713527;  
    float tau = $PI * 2; float width = 0.15; 
    vector force = v@force;  
    float particleAngle = atan2(v@P.x, v@P.z); 
    float timeAngle = ($PI + @Time); 

    float angleDist = abs(particleAngle - timeAngle) % tau;  

    float oomph = 30;        
    float angleDist2 = abs(particleAngle - timeAngle - $PI) % tau; 
    if (angleDist < width || angleDist2 < width) {
        float dist =  clamp(length(@P) - 1.0, -1.0, 1.0);     
        float variance = pow(0.125 + abs(sin(@Time * 2.25)) * 0.35, 2.0);     
        float gaus = (1.0 / sqrt(tau * variance)) * pow(e, -pow(dist, 2) / (2 * variance));        
        oomph *= gaus;         
        @force = set(force.x, force.y + oomph, force.z); 
    }

    Clip Two: "Three Peaks"

    This VEX is not dissimilar to the previous one, but rather than calculating the difference between each grain's angle and the current time, the force is based on the sine of the angle added to the current time. 

    The final render is a top down view with a little depth-of-field added.
    float e = 2.7182818284590452353602874713527;  
    float tau = $PI * 2;  
    vector force = v@force;  
    float particleAngle = (atan2(v@P.x, v@P.z) * 3) + (@Time * 10); 
    float oomph = 20 * max(0, sin(particleAngle));  
    float dist =  clamp(length(@P) - 1.0, -1.0, 1.0);  
    float variance = pow(0.125 + abs(sin(@Time * 1.5)) * 0.35, 2.0);  
    float gaus = (1.0 / sqrt(tau * variance)) * pow(e, -pow(dist, 2) / (2 * variance));  
    oomph *= gaus;  
    @force = set(force.x, force.y + oomph, force.z);

    Clip Three: "Rotating Ridges"

    The upwards force is a function of each grain's angle to the scene's origin multiplied by the sine of its distance to the origin:


    float tau = $PI * 2; 
    float width = 0.3;  
    vector force = v@force;     
    float particleAngle = atan2(v@P.x, v@P.z); 
    float timeAngle = ($PI + @Time);   
    float dist = abs(particleAngle - timeAngle) % tau;   
    if (dist < width) { 
        float oomph = 25;     
        oomph *= abs(sin((@Time * -2) + length(@P * 6)));
        @force = set(force.x, force.y + oomph, force.z); 
    }

    Clip Four: "Ripples 1"

    The upwards force is based on the difference between each grain's distance from the scene's origin and the current time multiplied by some curlnoise. The force is also tempered by each grain's vertical position so that it decreases with height.
    vector force = v@force;    
    float oomph = 0;     
    int dist = int(length(@P) * 20); 
    int time = int(@Frame % 40);      
    if (dist == time) {    
        oomph = 90; 
    }     
    oomph *= (1 - clamp(v@P.y, 0.0, 1.0));     
    vector4 noiseSource = set(v@P.x * 0.5, v@P.y * 0.5, 
    v@P.z * 0.5, @Time * 5);  
    vector noise = curlnoise(noiseSource);  
    oomph *= 1.0 + (0.25 * noise.x);      
    @force = set(force.x, force.y + oomph, force.z);

    Clip Five: "Ripples 1"

    The upwards force is based on the sine of the distance of each grain from the scene's origin added to the current time.


    float phase = length(@P)  * 5.0; 
    phase = max(0.0, sin(phase + (@Time * -3.0)));    
    float amplitude = 15 * (1 - clamp(v@P.y, 0.0, 1.0)) * (2.5 - clamp(length(@P), 0.0, 2.5));  
    vector force = v@force;       
    @force = set(force.x, force.y + (amplitude * phase), force.z);


    0

    Add a comment


  9. The Rayleigh-Taylor instability is the instability between two fluids of different densities. It can appear as "fingers" of a denser liquid dropping into a less dense liquid or as a mushroom cloud in an explosion.

    The phenomenon "comes for free" in SideFX Houdini FLIP Fluids. By simply creating a box, converting that to a FLIP fluid object and using groups to assign a lower density to the particles in the top half, it's easy to create an effect like this:



    However, I wondered what would happen if rather than using the default linear gravity, I created concentric spheres of fluids of different densities and made the fluids subject to a radial gravity force (see Houdini FLIP Fluid and Radial Gravity).

    Set up was pretty simple: for the fluid source, I created two spheres (an inner and outer). A Cookie node subtracts the inner from the outer so that I have a proper core and shell. Both of those geometries are converted to points and assigned to groups. Attribute Wrangle nodes define the densities - in my case the inner points have a density of 1 and the outer points have a density of 1.25.


    I added a Blast node to the automatically generated fluid surface geometry node (so that only the inner fluid is rendered) and my radial gravity code in a POP Wrangle in the DOP Network. I tweaked the VEX a little so that it would consider density:
    float mass = 100;
    vector origin = {0, 0, 0};
    float dist = max(distance(v@P, origin), 0.001);
    float gravityStrength = (mass + @density) / pow(dist, 2.0);
    vector gravity = (origin - v@P) * gravityStrength;
    @force += gravity;
    The final result, shown in the video above, is a fluid expanding into an invisible surrounding liquid of a greater density. Quite a nice effect with very little work! 



    1

    View comments


  10. Following on from my recent blog post, Mixing Fluids in Houdini, I wanted to simulate a toroidal eddy effect where the incoming drip takes the form of a torus and the fluid flows around the circumference of its minor radius. 

    My first thought was to use a POP Axis Force, but that rotates particles around the circumference of the major radius. So, I took another approach: create lots of curves placed around a circle and use those as the geometry source for a POP Curve Force.

    To create the geometry, I simply created two circles and copied the smaller one to the points of the larger one:



    The PolyFrame nodes ensures the smaller circles radially align by setting the normal and tangent names:



    Making the smaller circle an arc rather than a full circle and setting both circle's type to open arc, the resulting geometry looks like a fruit bowl:




    I used the same technique I discussed in Mixing Fluids in Houdini to split the fluid into two groups. Over in the DOP Network, the POP Curve Force was set to use the bowl shape of curves and assigned to the drip group. This means that only the drip is affected by the curves and the body of the fluid remains static.

    Here's the final render, along with some other fluid mixing experiments:






    0

    Add a comment

About Me
About Me
Labels
Labels
Blog Archive
Loading