Procedural Skinning in Houdini

A quick rundown on procedurally skinning / capturing geometry for rigid body animation in Houdini

Note that this is not using the new and revised SOP based skinning found in Houdini 18.5, which I would recommend you look into over the techniques shown here.

This might be pretty niche but I find procedural skinning very useful especially for rigid body rigging as I can maintain a non destructive workflow all the way to a rigged asset and skin using just groups and attributes. It's probably not as useful within VFX since you can simply drive rigid animation with transform hierarchies, but in games you'll want to end up with a skinned mesh.

The goal

The plan is to skin the keys of a piano. Not the most exciting asset, but it's also pretty dull to skin x amount of keys every time the mesh changes so I'd say it's a good use case. Although in most of my project I'm explicit about what geo gets skinned to which bones, a piano is a good opportunity to also do this assignment procedurally. These are the steps:

  1. Create bones
  2. Generate assignments
  3. Assign capture data

1 - Bones

In this instance I just create the bones manually, and I feel in most cases this is what you'll do even if you skin procedurally. However, there are cases where you will easily be able to define bone creation using the same parameters that you use to generate the geometry, in which case a Python helper node on the side for this would work.

I'll also create a new Geometry network specifically for my skinned geo, but leave it empty for now. My setup now looks like this, with a whole bunch of bone nodes attached to a root bone.

2 - Generate assignment data

We need to generate the data that tells each point which bone they should skin to. In most cases you would write these assignments somewhat explicitly through some dictionary or some other method of mapping point numbers to bone numbers. We can do better in this case though. I'll start by jumping into the new skinned geometry network and object merging in my piano.

Now we'll want to store an integer point attribute that refers to which bone any point will be skinned to. I already have a group for just the tangents, so I'll start by splitting out the tangents from the rest of the piano. Now a connectivity node would work for giving each tangent an ID and so a bone index, but the connectivity node seems to give random ordering so how will that work? The "class" attribute you get from connectivity actually increments based on point ordering, so what we can do in our case is add a sort node that sorts by whatever axis we want our connectivity to increment along. Very nifty!

At this point we just need to add a bone ID for the rest of the piano. I'll just create a integer point attribute named "class" and give it a default value of 72, which happens to be one more than the amount of keys I have, which will result in targeting the root bone instead of any of the key bones. ( And I know there's no such thing as a 71 key piano, I don't want to hear about it )

3 - Assign capture / skinning data

We are now in for some actual skinning. There's two nodes that do most of the work when it comes to skinning. The Bone Capture node and the Bone Deform node. Put them down in that order. The bone deform will work out of the box but in the capture node we'll have to set "Capture Frame" to before our current time, so 0 will usually do. We also need to specify the "Extra Regions" parameter to point to all the bones we are using. The order of the items in the resulting list matters, and the rightmost item is considered item 0, so in my case root would be the 72nd item after key72 because I named the keys from 1:

../../root ../../key72 ../../key71 ../../key70 ../../key69 ../../key68 etc...

Now if you check the point attributes that the capture node adds you'll notice they look different than most Houdini attributes, and you can't really interact with them the same way as you can other attributes. Luckily Houdini provides a pair of nodes called Capture Attribute Unpack and Capture Attribute Pack which you'll have to put down in that order. Now we can add a point wrangle in the middle and adjust our skinning as we need.

We need to set two arrays. One integer array of bone indexes and one float array of their corresponding weights. In our case they will both have just one item, and the weight will always be 1.0. This is my Vex code:

f[]@boneCapture_data = { 1.0 };

int bone_ids[];
append(bone_ids, i@class);
i[]@boneCapture_index = bone_ids;

Why is _data declared in one line and _index by declaring an array variable? The first one is a literal array and is constructed at compile time, and so cannot include variables. This is why we make a dynamic array for the other attribute.

You'll now be able to set the display flag on the bone deform node and it'll deform based on the changes to the bones! If you want to realign or otherwise change the positions of the bones just display an earlier node and when you're ready click "Force Recapture" in the capture node. You can also export it out of Houdini with for example an FBX ROP and it'll retain its skinning.

Definitely how pianos work.

Till next time!