Poking Maya's transform nodes and the worldMatrix

Posted 23 November, 2012 Anthony Tan (staring at yet another render. But still loving it for some reason..)

« previous | next »

tags: maya

Ever been poking around in the channel editor and wondered what the heck worldMatrix did? Me too. Well, not really, not till i needed to rig up a sliding snake of cubes...

Prerequisites

Not a huge preload on this, but ideally, you should know:

  1. what a matrix is and what a vector is;
  2. that in 3D space, while we see a position as (x,y,z) you need to be using the homogenous form of (x,y,z,1). Yes, that looks like voodoo, but even just me mentioning this will make the matrices appear less insane;
  3. basic matrix multiplication;
  4. what an identity matrix looks like, and why it does 'nothing'
  5. and you should also be aware that :
  6. Maya uses row vectors and post multiplication. This is Very Important to recall otherwise you'll get all sorts of crazy insane confusion. If you're coming from blender, I believe it does premults and column vectors.
  7. Okay, time to dive in, oh, and yes, there's heavy use of Ye Olde Script Editor today..

    Step one: Script editor

    Hike open the script editor, and import PyMEL libraries since I'll be using them as shorthand...

    import pymel.core as pm
    

    ...create a simple cube in an empty scene...

    the_cube = pm.polyCube(name = "Harold") # Yes. Harold.
    

    ...define a pretty print function for matricies (note, there's no error checking here, this is just a lazy print function. Since we're also dealing with floats, also just doing a simple pad to 3 d.p.)...

    def pp(matrix, title =None, footer = "\n"):
        """
        pretty prints a matrix with an optional title and end of matrix separator 
        """
        if not (title == None):
            print(title)
        for row in matrix:
            print("["),
            for cell in row:
                print("{0:.3f},".format(cell)),
            print("]")
        if not (footer == None):
            print(footer),
    

    and now lets just see what we have... each transform node has three matrices of interest - worldMatrix, worldInverseMatrix, xformMatrix. We're going to be doing this a heap, so might as well do it cleanly:

    def queryMatrices(transform):
          queryAttrs = ["worldMatrix", "worldInverseMatrix","xformMatrix"]
          for attrs in queryAttrs:
                pp(transform.attr(attrs).get(),title=attrs)
    

    Okay. You should get three pretty much identical looking 4x4 matrices, with a diagonal of ones. This is the identity matrix so if you apply (note: when I say 'apply' I mean post-multiply) any of these matrices to a point in space you'll get no change.

    Righto, take Harold and move him seven units on the X axis (you can do this in the channel box if you want)

    the_cube[0].translateBy([7,0,0])
    queryMatrices(the_cube[0])
    

    okay, see that bottom row now has a 7 in it? Cool, we know stuff is now working. I'm going to stick to transforms only but if you want to know more, check out the maya docs under "technical documentation/nodes/transform" for the gory details of Maya's matrix setup. It's good to know, but a bit beyond scope here.

    At this point, the cube is parented to world space, so there should be no surprises here, so lets start adding in corruptions to the world space... assign Harold to a group of his own and check out the parent's transforms

    the_cube_parent = pm.group(the_cube, name = "Richard")
    queryMatrices(the_cube_parent)
    

    looky - it's another set of identity matrices.. which is completely logical given that the parent itself is sitting in world space and has no transforms. Lets move it now by say, -7 on the X axis, query it.

    the_cube_parent.translateBy([-7,0,0])
    queryMatrices(the_cube_parent)
    

    and now lets check out Harold again..

    queryMatrices(the_cube[0])
    

    Woah. Neat (I'm easily amused). See how the world and world inverse matrices are now back to the identity? Yep, ol Harold from a worldspace point of view, hasn't moved... but hang about, his xformMatrix still has his original translation in it. Thus illustrating the major point - worldMatrix and worldInverseMatrix stores information that runs all the way up the parent chain, while xformMatrix stores localspace information. If you were to do the following:

    the_cube_grandparent = pm.group(the_cube_parent, name = "Bob")
    the_cube_grandparent.translateBy([0,9,0])
    

    and check the matrices, you'd see how the information flows down. Each level explcitly keeps a hold of its localspace transform (that's xformMatrix) and multiplies the parent's worldMatrix by this to get its own worldMatrix. Matrices rock when dealing with nested transforms, they can be multiplied together (and inverses applied if you want) to infinty and back. Yay vector maths!

    Oh, yes, see that handy dandy (use with caution) option to tun off inheriting transforms? If you toggle this on Harold, and recheck his matrices, you'll see that the world matrix gets set to the identity matrix.

    And just to round up to make sure we're all clear on this, let's just set this up as follows. First, delete Bob/Harold/Richard, and then recreate them. Move Harold by 7 on X, Richard by 2 on Y, and Bob 5 on Z.

    pm.delete(the_cube, the_cube_parent, the_cube_grandparent)
    the_cube = pm.polyCube(name = "Harold")
    the_cube[0].translateBy([7,0,0])
    the_cube_parent = pm.group(the_cube, name = "Richard")
    the_cube_parent.translateBy([0,2,0])
    the_cube_grandparent = pm.group(the_cube_parent, name = "Bob")
    the_cube_grandparent.translateBy([0,0,5])
    

    Harold should be at the location in space (7,2,5) which can be verified with fancy tools, or simply by counting squares in the ortho view. His local transform, or xformMatrix should only reflect his transform of (7,0,0), but his worldspace xform should hold the entire chain up the parent.

    Step 3: ...profit?

    Okay, fine, fun, sure, this is all fun and dandy from a mathemagical point of view, but you're asking 'so what?' and 'how do i use this without rolling my own node?'. Short answer, there's not a heap you can do in trivial example land, but it's kind of an important tool to have in your arsenal when working out how to rig something up.

    But since we're talking about examples.. there's a plugin that rolls with Maya that's sort of undocumented - it's called the decomposeMatrix node. It should be loaded by default, but if the following gives you an unknown node, check your plugins for decomposematrix.mll/so

    decompose_node = pm.shadingNode("decomposeMatrix", asUtility = True)
    

    You can then either manually type in values or you can connect up a matrix. E.g.:

    the_cube[0].attr("worldInverseMatrix").connect(decompose_node.attr("inputMatrix"))
    

    caveat: the display in the AE does NOT update the inputmatrix in a live sense if you do this, you need to constantly tab off/and on to get it to update so tearing the tab off doesn't help much. don't worry though, the outputs are always live. Those readouts though, should give you all you need to transform any children of Harold back to worldspace if you wanted without needing to establish a direct parent-child relationship (a bit like applying a parent constraint)

    How about we make Sue always stuck back to worldspace by linking in all that data from the decompose node? If you do that, then Sue should pop back to the origin, and never be able to move - since we've applied the matrix required to always send it back to the origin. Not exactly useful, but you get the idea..

    Alternatively you could always transform anything from worldspace into Harold-space by applying the matrix directly.. how? Ah, right, yes, it's a little bit of a pain, you have to apply a pointMatrixMult node which will just do the matrix mult for you and output your new position in space, which is the actual reason I went down this path, you can place things that are in worldspace coordinates, that live down inside a parent hierarchy into the correct *local* space.

    Specifically, the pointOnCurve node will give you a worldspace coordinate of an input curve at a parameter position. Handy dandy for control (fie to those people who don't think NURBS rock) but problematic if you have a nested hierarchy. The solution in this case was to apply the inverse world matrix transform to the output position reported, and then feed that result into an object (a cube in this case) that lives as a child of the parent curve - so you can move the parent curve and have everything glue, basically.

    and the cube snake thing?

 
previous
Maya gotchas - nParticles getting velocity from like, nowhere
next
A primitive HFR test, and cubes. Of course.
tags: maya
  • IMHO, the correct plural of matrix is matrices. But you could say matrixes and be understood. But let's not.
  • if you're wondering, the references to the_cube[0] are because pm.polyCube returns a list with both the transform and the shape node in it
  • PyMEL was not necessary for this, but I think that the syntax makes more sense and reads 'better' for what it's worth