weightRemapper Node

Unlike in a skinCluster-based system if you use a translateUVN deformer you'll need to define which maps are left/right pairs for each other. To do this we just need:

  1. An inputWeightList attribute

  2. An outputWeightList attribute (these attributes need to have useArrayDataBuilder set to true so that they update properly)

  3. A the left and right vertex pairing for each shape

  4. An internal system for pairing the input and output shape plugs.

Those first two requirements can be done with no problem, but the third requirement is where it's easy to misstep.

Precalculating or Recalculating?

One solution is to have the node take a mesh as an input, and then internally generate the left/right pairing. This is a pretty user friendly way of doing things since any less-technical artists that want to build a component using the new node don't need to fuss around with creating the vertex-pairing themselves. The issue with using that solution is that the vertex pairing needs to be recalculated every time you open the scene, and each unique weightRemapper node has to calculate (and store) the pairing independently. C++ is fast though, so maybe the difference in speed is worth the ease-of-use.

To test the speed I created a single mesh with 5,000 verts which had two clusters connected by a weightRemapper node. The weightRemapper node had two modes controlled with a boolean attribute, one mode used a precomputed intArray pairing to flip the weight map, and the other mode computed that same intArray internally using an undeformed duplicate of the mesh as an input.

The precomputed method added .006 seconds to the file load time.

The method that computed the intArray from the mesh added .32 seconds to the file load time.

Now a more dense mesh would make that difference even greater, based on my testing a 10,000 vert mesh would add 1.256 seconds to the file load time. If we have 50 weightRemapper nodes (which is a reasonable ballpark estimate) we'll be adding more than a minute to the scene load time. That's already too much in my opinion, especially when you consider that 10,000 verts is nowhere near the limit, you could have multiple meshes, and you could have more than 50 weightRemapper nodes.

So what originally seemed like the best solution is actually going to cost us time. So instead we should precompute the pairing for each mesh we intend to deform and pass it into the weightRemapper node.

Generating the Vertex Pairing

Because the vertex pairing will only be computed once and the code we use to generate it is not part of the compiled C++ node, we don't need to be too concerned with the optimization of the code. Below is a very simple example.

import maya.api.OpenMaya as om2
import maya.api.OpenMayaAnim as omAnim2
import time 

s_time = time.time()

sel = om2.MSelectionList()
shape_mObj = sel.getDependNode(0)

mit = om2.MItGeometry(shape_mObj)
mit_second = om2.MItGeometry(shape_mObj)
pairing = om2.MIntArray()

while not mit.isDone () :
    src_index = mit.index()
    src_pos = mit.position()
    closest_index = -1
    closest_distance = 100
    while not mit_second.isDone () :
        dest_pos = mit_second.position()
        dest_pos[0] *= -1
        distance = dest_pos.distanceTo(src_pos)
        if distance < closest_distance:
            closest_distance = distance
            closest_index = mit_second.index()
            if closest_distance < .001:

print time.time() - s_time

What you'll notice about the above code is that the intArray it produces is not:

left_id, right_id, left_id, right_id

Instead the first element is the id of the vertex that is paired with vtx[0], and the secon element is the id of the vertex that is paired with vtx[1]. This makes the intArray half as long but just as good. This does of course mean that one source_id cannot be paired to multiple destination_ids, we have to have a 1 to 1 pairing but for our purposes this doesn't matter.

Using the Pairing:

So we've got our inputWeightList and our vertex pairing inside of our node now, how are we going to create the outputWeightList? This is another place where it's easy to misstep. We could do something like this:

for i in range(intArray.length()):
    get the input weight at element "i"
    make that the new value of outputWeights[intArray[i]]

Which would work but why are we setting every single element on the outputWeights? Seems a bit wasteful. We only need to remap the non-default weights from the inputWeights. Something kind of like this:

//for each component
for (unsigned i = 0; i < inputWeights_handle.elementCount(); i++, inputWeights_handle.next())
	//get the input weights index and value
	unsigned comp_index = inputWeights_handle.elementIndex();
	if (comp_index > remapArray_val_mIntArray.length() - 1)
	//status = inputWeights_handle.jumpToElement(remapArray_val_mIntArray[comp_index]);
	float inputWeight = 1;
	if (status == MS::kSuccess)
		inputWeight = inputWeights_handle.inputValue().asFloat();
	MDataHandle weightElement = outputBuilder.addElement(remapArray_val_mIntArray[comp_index]);

Notice how we are using in inputWeights_handle.next() to only loop through the non-default weights that exist, instead of using jumpToElement(i) which would get us every the weight value for every component. If your deformerSet is 100 times smaller than the overall mesh then the size of the array you output will be reduce by 100 times.

What About Symmetrizing WeightMaps?:

There's a reason I called this post weightRemapper and not weightMirror. Not only can we use the node to flip the weightMap but by providing a different pre-computed intArray that pairs the vertices in such a way that the weightRemapper makes the weightMap symmetrical. But wait, how can each weightRemapepr know if it should take the precomputed symmArray or flipArray. The best solution I could come up with was to add a "remapAttr" string attribute to the weightRemapper node which gives the name of the attribute on the shape node that contains the desired precomputed intArray. This makes the system very versatile.

We can either use a script or a callback that is triggered when new shapes are added to the deformers whose wieghts are connected to the weightList to not only connect the elements of the per-shape weightList attribute but also to connect the intArray from each shape. The callback thing I may discuss in another post.

Could we do even more?

Is there other functionality that might make sense to include in this node? One thing that comes to mind is an interactive copyWeights sort of behavior. So if you had a separate mesh for the brow you could provide the index/(index and barycentric weight) data needed to do a closestPointOnSurface copy. That seems interesting but is probably outside of the scope of this simple weightRemapper, but perhaps a future project?

Featured Posts
Recent Posts
Search By Tags
Follow Us
  • Facebook Basic Square
  • Twitter Basic Square
  • Google+ Basic Square