« Node Frenzy! | Home

Walkthrough: Jitter Deformer Node

By riggerman | November 19, 2009

Hey all! As promised I will be explaining in my own not-so-technical words how I got a my own python scripted deformer node working in Maya! This is a reeeeeeeeeally beginners’ level post and shouldn’t be read by API expert.

Also, bear in mind this is not the jitter node from the devkit examples. I started with the

yTwistNode file and worked my way to a deformer that moves all vertex of a mesh to random directions (controlled by a multiplier value).

Here’s a little screengrab of meshes being deformed with my plug-in. Thanks to my co-worker Paulo Nogueira for the idea of using toon shading with this node to achieve this nice hand-drawn trembling effect!

Jitter Deformer in action! from Riggerman on Vimeo.

Ok, so I’ll explain what I see it’s important in general first and then later concentrate on the actual deformation algorithm.

IMPORTANT: I will rename a lot of variable names so pay a lot of attention, if you forget to replace a variable name you can get a lot of errors later.

  • Maya Plug-in bureaucracy
  • Before dirtying our hands with the fun part, let’s see some boring parts that should be right or else nothing else works.
    Right after the commented copyright lines in the start of the file you’ll see a lot of module imports. Something we will be using and is not included is the random module. Since we won’t use the math module, let’s switch them. Your imports should look like this:

    import random, sys

    import maya.OpenMaya as OpenMaya
    import maya.OpenMayaMPx as OpenMayaMPx

    Pay attention now, because right under them we already have two really important lines:

    kPluginNodeTypeName = “spyTwistNode”
    yTwistNodeId = OpenMaya.MTypeId(0×8702)

    These lines define important variables for your plug-in. The first var, kPluginNodeTypeName is the name of your node (speaking plain english, the string that identifies your node when using the createNode command in Maya).
    The second var is an ID number for your plug-in, this number is unique to every plug-in ever made and your should ask Autodesk for a unique ID so if you write something for others to download and use, its ID won’t conflict and your Maya won’t crash or explode. Me? I just used some random address number because I was just studying and won’t be distributing these.

    Let’s use the Search/Replace function (every text editor has it) to replace “yTwistNode” for “jitter“.

    These vars are used right at the end of our code, in the initializePlugin and uninitializePlugin functions.
    These two self-explaining functions are the ones called whenever you load and unload a plugin in Maya (through a loadPlugin/unloadPlugin command or through the Plugin Manager Menu).
    Scroll down to the end of the file to find them. You’ll see the register/unregister commands are enclosed in try/except statements, that’s for giving a nice fail message in case something goes wrong with your ID or something else.

    # initialize the script plug-in
    def initializePlugin(mobject):

      mplugin = OpenMayaMPx.MFnPlugin(mobject)
      try:
        mplugin.registerNode( kPluginNodeTypeName, jitterId, nodeCreator, nodeInitializer, OpenMayaMPx.MPxNode.kDeformerNode )
      except:
        sys.stderr.write( “Failed to register node: %s\n” % kPluginNodeTypeName )

    # uninitialize the script plug-in
    def uninitializePlugin(mobject):

      mplugin = OpenMayaMPx.MFnPlugin(mobject)
      try:
        mplugin.deregisterNode( jitterId )
      except:
        sys.stderr.write( “Failed to unregister node: %s\n” % kPluginNodeTypeName )

    NOTE: Do not change these functions names or else Maya won’t load your plug-ins.

  • The Node initialization function
  • Ok, now that the gruesome part is over, we can focus in actually working on our node! The first step to this is to set its attributes - you know, the data it will work with! Check out this part of the code:

    def nodeInitializer():

      # angle
      nAttr = OpenMaya.MFnNumericAttribute()
      jitter.angle = nAttr.create( “angle”, “fa”, OpenMaya.MFnNumericData.kDouble, 0.0 )
      #nAttr.setDefault(0.0)
      nAttr.setKeyable(True)

    This code starts by instancing the MFnNumericAttribute object so we can access its functions. In this case we are creating a numeric attribute, but it could be a MFnTypedAttribute to work with strings or even a MFnGenericAttribute to work with mesh data. Check out the respective API help pages to see what kind of functionalities these guys have and how to use them.
    The following line creates a attribute called “angle” on the node, let’s rename it to “seed“, it will be a attribute that controls the ‘randomness’ of our deformation. Usually when using python pseudo-random function, this seed is constantly changing so we never get the same values, but we’ll force it to be always a number we define, thus returning to us the same results over and over - I did it this way so the node is predictable, renderfarm friendly.
    Also, don’t forget to change its unit type to kFloat, I don’t want to use double.

    Alright! For the rest of this line we define the full attribute name, the short attribute name, its unit type and a default value; check the MFnNumbericAttribute create function API page for more info. Let’s set it like this:

    jitter.seed = nAttr.create( “seed”, “sd”, OpenMaya.MFnNumericData.kFloat, 0.0 )

    Before moving on, we gotta create another attribute. Up to now we have a seed attribute that controls the randomness of the deformation, now we need something to control its amplitude. Copy and paste all this section right underneath the seed attribute and apply the necessary changes, like so:

    # multiplier
    nAttr = OpenMaya.MFnNumericAttribute()
    jitter.multiplier = nAttr.create( “multiplier”, “mu”, OpenMaya.MFnNumericData.kFloat, 0.0 )
    nAttr.setKeyable(1)

    This multiplier attribute will be the number we multiply with the random function to increase our deformation’s power.

    Ok! Now we have our attributes defined, let’s add them to our node. The code that does this is right underneath our attributes:

    # add attribute
    try:

      jitter.addAttribute( jitter.angle )
      outputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
      jitter.attributeAffects( jitter.angle, outputGeom )

    except:

      sys.stderr.write( “Failed to create attributes of %s node\n”, kPluginNodeTypeName )

    We only replaced the node’s name but the angle attribute is still there. Change all occurrences to seed.
    Now on to explain what is going on: you can see the code is enclosed in a try/except statement, that will log a pretty fail message when things go wrong trying to attach your attributes on your node.
    The first line inside the try statement adds your attribute to your node, nothing special here.
    The second line and third line tells Maya that our attribute affects the geometry - that means that whenever we change our seed value, the output geometry must be recalculated. Let’s abstract a little: if we had a node that accepted a float value and outputted half of this value, we would say that everytime we change our input value, the half-output value had to be recalculated, right? So one attribute affects another. In our case, we are dealing with a node that deforms meshes, so everytime we edit the seed attribute value, the deformation will change, will be affected. That’s what these last lines are for, if you omit these, when your seed value changes the geometry outputted from your node won’t change at all.

    Ok, moving on! Duplicate the first and last lines changing seed to our second attribute, multiplier. You gotta end with something along these lines:

    # add attributes
    try:

      jitter.addAttribute( jitter.seed )
      jitter.addAttribute( jitter.multiplier )
      outputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
      jitter.attributeAffects( jitter.seed, outputGeom )
      jitter.attributeAffects( jitter.multiplier, outputGeom )

    except:

      OpenMaya.MGlobal.displayError( “Failed to create attributes of %s node\n” + kPluginNodeTypeName )
  • Meet Mr. Jitter Node
  • Alright, with most of our plug-in covered, all that is left is for us to define HOW the deformation is going to happen. We got all the material and room to do the job, now we just have to tell Maya how to use out input data.

    The first line define some important stuff. If you changed all angle occurrences, this line should be instancing a MObject as a variable called guess what… seed. Duplicate it for the multiplier attribute, like so:

    # class variables
    seed = OpenMaya.MObject()
    multiplier = OpenMaya.MObject()

    Leave the initialization method as it is, we don’t need to change anything here.

    And finally, the cool part, the deform method! As you can see, this guy has a lot of convenient handles so we can access data from our node. We mainly use the dataBlock here that gives us access to our attributes and the geomIter iterator to loop through all points in our geometry, enabling to make changes to them!

    The deformation algorithm starts by getting our seed value as a double, but remember we changed the unit type of our attribute to float back there? Do the changes correctly here, don’t forget to duplicate it to also get our multiplier value and you should get:

    # get the seed from the datablock
    dataHandle = dataBlock.inputValue( jitter.seed )
    seedValue = dataHandle.asFloat()

    # get the multiplier from the datablock
    dataHandle = dataBlock.inputValue( jitter.multiplier )
    multiplierValue = dataHandle.asFloat()

    Now we have these two nice variables to work with however we want, seedValue and multiplierValue.

    Oh wait, what do I see? We can get the node’s envelope value too! This way we can turn the deformation on and off in our calculations! You shouldn’t change theses lines:

    # get the envelope
    envelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope
    envelopeHandle = dataBlock.inputValue( envelope )
    envelopeValue = envelopeHandle.asFloat()

    Now that we have everything we need, let’s rock! We’ll use the geomIter iterator to loop through all points in our mesh and do all kinds of silly things with them:

    while geomIter.isDone() == False:

      point = geomIter.position()
      ff = angleValue * point.y * envelopeValue
      if ff != 0.0:
        cct= math.cos(ff)
        cst= math.sin(ff)
        tt= point.x*cct-point.z*cst
        point.z= point.x*cst + point.z*cct
        point.x=tt
      geomIter.setPosition( point )
      geomIter.next()

    Looks scary doesn’t it? Don’t worry, most of the weird formulas are the twisting calculations and we don’t need to understand these (at least not for this node!)
    What we need to know is: the first line is a loop that checks if there’s still points left to be looped through. The geometry iterator starts from the first point and goes to the very last one of your mesh and while it is at it, it can recover valuable information for your like the point’s position! The iterator goes on to the next point using the geomIter.next() method, at the very end of the loop. Let’s delete what we won’t use and rearrange so we can think clearly:

    while geomIter.isDone() == False:

      point = geomIter.position()
        #change point variable here…
      geomIter.setPosition( point )
      geomIter.next()

    At this point you have a working node! You can experiment any calculations here that the node should work just fine. I’ll explain how to jitter the points using the random module.

    Remember I said we would set the random’s seed value so we can control it’s (pesudo)randomness? To understand that, fire up Maya’s script editor, go to a python tab and write:

    import random
    print random.random()

    Re-run this piece many times to get different values everytime. Now, try to run this code:

    import random
    random.seed(0)
    print random.random()

    …and now you get always the same value, unless you change the seed value. Got it? Random module by default uses your machine’s time as seed but you can override that by setting the seed manually. That’s how we’re getting the same values over and over for our deformer node.
    So the first line of code we should write does that with a twist, I’ll explain shortly:

    random.seed( seedValue + geomIter.index() )

    So if every point had the same seed, they would have the same value, right? I change this by adding the own point index value to the seed, making every point different! Now, what happens is that we still need something to shake these guys, not only scramble them a little and thats it. I did it by also adding the multiplierValue to it:

    random.seed( seedValue + geomIter.index() + multiplierValue )

    I could add another attribute to control the offset, but since I wanted a simple example I used this already create attribute (this way you scramble the points by modifying the multiplier attribute).
    Now that the seed is guaranteed to create different values for each point, let’s mix in our node’s attributes to control the value. Remember, multiplierValue will control how far the point is displaced from it’s starting position and envelopeValue will control how much of this effect should be applied to the points. We do the random value like this, yes, three times so they’re different:

    randomValueX = random.random()*(multiplierValue*envelopeValue) - multiplierValue/2
    randomValueY = random.random()*(multiplierValue*envelopeValue) - multiplierValue/2
    randomValueZ = random.random()*(multiplierValue*envelopeValue) - multiplierValue/2

    To add our randomized values to the point’s XYZ position we gotta transform our units into a MVector object. We do this like so:

    randomVector = OpenMaya.MVector(randomValueX, randomValueY, randomValueZ)

    Now we can sum our point position with the scrambled values:

    geomIter.setPosition(point+randomVector)

    Now you can save and load your plug-in in Maya using the Plug-in Manager! Test your node by creating a sphere and typing:

    import maya.cmds as cmds
    cmds.deformer(type=”jitter”)

    Well, this is the end of this walkthrough and I hope it helped you as much as it helped me to better understand it by writing. If you find anything wrong or have a suggestion, please email me or comment!

    EDIT: (24/nov/2009)
    Ryan Trowbridge has pointed out a faster way of computing the deformations:
    Most (if not all) of the devkit examples set the points positions one by one while iterating the mesh points, but this method is slower and since our deformation isn’t neighbor dependent or anything like that, we can set the points position all at once using MItGeometry.allPositions ().
    In his own words: “The problem with this is that when the node goes to deform, it is iterating through x amount of vertices and each time it iterates it updates the scene using setPosition() So a 10,000 vertice mesh is updating the scene 10,000 times (…) The reason this one is faster is it get all the vertices and sets all the vertices at once“.

    Well, to do this we need to create a MPointArray object to store the information we need. This line goes before the iteration loop:

    allPoints = OpenMaya.MPointArray()

    Now, inside the loop we can replace the setPosition line for this one:

    allPoints.append(point+normalVec)

    This will insert each point value in the point array object, so we can pass on this big list all at once to the final command, that goes right outside the loop:

    geomIter.setAllPositions(allPoints)

    And that’s all! Now your deformer will run much faster! Thanks Ryan!

    Topics: Maya Python |

    Email to a friend »

    Use this form to send your friend this post.






    Comments

    You must be logged in to post a comment.