Creating custom groom noise functions using VEX

If you've tried to create realistic and detailed groom assets in Houdini, you might have noticed that achieving good-looking frizz or noise using the native tools is tricky. Sometimes you need that extra bit of customization for creative purposes or you need to replicate a specific noise pattern from reference. I'll walk you through a setup that allows you to generate noise functions for groom assets. Creating noise functions is simple, but you must consider some things when it comes to groom. First, you want to ensure you don't move the roots. Moving the curves' roots will break the groom. Second, you want to ensure the hair length stays the same. Keeping the length the same is not a technical requirement. It just needs to make sense visually. If your noise setup meets these two conditions, it works.

Download Sample Scene

Let's focus on keeping the roots in place first. The following is a handy trick for any geometry manipulation. First, we'll store the original position as a rest attribute so we can always return to it. Having a rest attribute means we can blend any modified shape with the original one, as long as we don’t mess with the point order. Storing the rest attribute is very straightforward. It's just a copy of the current position.

//point wrangle
v@rest = v@P;

After storing the rest attribute, you can modify the point positions of the geometry. We can bring back the original shape using a mixing script.

//point wrangle
float blend = chf("blend");
v@P = v@P * blend + v@rest * (1-blend);

This script will mix between the rest position stored in the rest attribute and the current point position by a constant value. You can use any normalized point attribute such as a noise or painted attribute as the mixing value. We've got curves, and we want to blend this effect by the length of the curves using the curveu attribute. Just make sure there is a curveu attribute. If there's not, you can create one using the resample node. Let's rewrite the script like this:

//point wrangle
float blend = chramp("blend_ramp",f@curveu);
v@P = v@P * blend + v@rest * (1-blend);

Adjusting the ramp allows us to blend the effect along each hair and keep the roots in place. We might still accidentally move the roots, so forcing them into the rest position is better. Let’s add this to the script.

//point wrangle
float blend = chramp("blend_ramp",f@curveu);
v@P = v@P * blend + v@rest * (1-blend);

int vtx_prim_index = vertexprimindex(0,@ptnum);
if ( vtx_prim_index == 0 ){
    v@P = v@rest;
}

Now that we’ve taken care of sticking the roots to the original positions and blending along the length of the curves, we can create any noise function between these two nodes. Let's explore this quickly. A simple way to add noise to curves is to use the attribute noise sop. Set the attribute to "P" and range values to "zero centered." In the "noise pattern" section, check the "use VEXpression" box. In the box, write the following:

float rand = rand(@primnum);
pos += fit01(rand,0,100);
elementsize += fit01(rand,-0.1,0.1);
offset += fit01(rand,0,100);

Now you can adjust all the parameters to get the desired effect. The attribute noise sop, randomized using the VEXpression, is an excellent way to get detailed and non-repetitive noise. Playing around with this, you’ll understand what I meant by keeping the hair length the same as the original. Noise effect like this stretches the curves and results in uneven segment length. Ideally, the resulting curves should be the same length as the original, with an even segment length. We can resample the curves after the noise to fix the uneven segment lengths, but the curves are still longer than the original. To fix this, we must measure the hair length before and after the noise to know how much to scale down the curves. Hair length can be measured using the measure node and setting the measurement type to "perimeter." But I've also made a script: Measure Hair Length (VEX).

To know how much the noise stretches the curves, plug the measure sop or the measuring script before and after the noise and divide the original length by the post-noise length. Now, we could use this scale ratio to scale the whole hair down, but there are better solutions than scaling the hair since scaling the hair can cause unwanted intersections. Another approach would be to delete the points on the curves that exceed the original length, but this would modify the point order in a way that can break attributes. We want to slide the curves’ points down along the primitive by the scaling ratio, keeping the point order the same. I'm writing a post about resampling curves using VEX, and I will explore sampling positions on curves using the primuv() function in more detail. But for now, I'll just leave this script here.

//primitive wrangle
float length_A = f@length_A;
float length_B = f@length_B;

float scale = length_A / length_B;

int pts[] = primpoints(0,@primnum);

int prim;
vector uv;

for ( int i=0; i<len(pts); i++ ){
    
    vector pos = point(0,"P",pts[i]);
    xyzdist(0,pos,prim,uv);
    
    uv.x *= scale;
    
    vector new_pos = primuv(0,"P",prim,uv);
    setpointattrib(0,"P",pts[i],new_pos);
}

Now we've taken care of the roots, blending the noise along the length and keeping the same size as the original. We've opened up the possibility of building any noise function. Everything goes, whether made using the attribute noise sop or point vop/wrangle. Just as a final note, if you're interested in building noise functions using VEX, you can get started using this script as a base:

//point wrangle
vector noise = noise(v@P*chf("scale"));

noise = fit01(noise,-1,1);
noise *= chf("amplitude");

v@P += noise;
Previous
Previous

Resampling curves using VEX part 1: Constant segment count

Next
Next

Simple Clumping Using VEX