Could this script be improved/more elegant?

I made a script which takes all the transform values in an object’s local Matrix and transfers them into the OffsetParentMatrix (OPM). This is basically to replace buffer groups and make the outliner have less DAG nodes and look a bit cleaner. (I think it only works if there has been no freezing of transforms)

2 questions:

  1. Is there a better way to write the first few lines? I had to write that to make the input work for both lists and single objects. The single object (a string obj) gets converted into a list with a single element. I have that in a few other scripts, but I guess there’s a better way to go about handling inputs.
  2. Can someone point me to a good resource to learn about how to script matrix math without using maya’s matrix nodes? I just seems silly to create nodes to calculate a matrix just to then delete them after the getAttr.

Also if you see anything else that could be better or conforming to standards more, let me know :slight_smile: I’m still fairly new to coding and want to make sure I don’t get into nasty habits or write my code in a way that noone understands it O_O

def zero_transforms(objects, t=True, r=True, s=True):
    """ transfers matrix to offset parent matrix -> objects end up with zeroed channels
    for joints it keeps jointOrients intact"""
    if type(objects) is str: # in case only one object is selected to make the loop work with a list
        targets = [objects]
    else:
        targets = objects
        
    for i in targets:
        if cmds.nodeType(i) == 'joint':
            # get .matrix from a temp object without rot, all JNTs have 0 rot after orientation
            parent = cmds.listRelatives(i, p=True)[0]
            cmds.group(n='temp_GRP', p=parent, em=True) # matching parent's orientation
            cmds.matchTransform('temp_GRP', i, pos=True)
            offset = cmds.getAttr('temp_GRP.matrix')
            cmds.setAttr(i+'.offsetParentMatrix', offset, typ='matrix')
            cmds.xform(i, t=[0,0,0])
            cmds.delete('temp_GRP')
        else:
            # temp multMatrix to recalculate matrix
            tempMM = cmds.shadingNode('multMatrix', n='temp_MM', au=True)
            parent = cmds.listRelatives(i, p=True)[0]
            cmds.connectAttr(i+'.worldMatrix[0]', tempMM+'.matrixIn[0]')
            cmds.connectAttr(parent+'.worldInverseMatrix[0]', tempMM+'.matrixIn[1]')
            # temp pickMatrix with options based on keyword flags
            tempPM = cmds.createNode('pickMatrix', n='temp_PM')
            cmds.connectAttr(tempMM+'.matrixSum', tempPM+'.inputMatrix')
            cmds.setAttr(tempPM+'.useTranslate', t)
            cmds.setAttr(tempPM+'.useRotate', r)
            cmds.setAttr(tempPM+'.useScale', s)
            # get offsetParent Matrix
            opm = cmds.getAttr(tempPM+'.outputMatrix')
            # delete temp nodes
            cmds.delete(tempMM, tempPM)
            # Set the offset parent matrix to the transform attributes
            cmds.setAttr(f"{i}.offsetParentMatrix", opm, type="matrix")
            # Zero out the transform attributes
            if t == True:
                cmds.xform(i, translation = [0, 0, 0])
            if r == True:
                cmds.xform(i, rotation = [0, 0, 0])
            if s == True:
                cmds.xform(i, scale = [1, 1, 1])

Since you’re new to coding, I’m not gonna write too much for you, but I will share ideas. This may take some back-and-forth, and that’s completely fine. Writing some code to test then throw away is part of the process.

But First: PLEASE PLEASE PLEASE use the full flag name in code you’re planning to reuse. It’s a big pet-peeve of mine. Anybody who is going to read your code will thank you (and that includes your future self)

For those first 3 lines, I would probably do this. Its personal preference whether you do it in one line like this, or keep it as blocks. But isinstance is definitely what you’re looking for
targets = [objects] if isinstance(objects, str) else objects

cmds.group returns the name of the group you just made. And that should include any required full-path disambiguation, or extra numbers at the end. So instead of hard coding “temp_GRP” as the name of the object in all those commands, you can do it once in the first place it’s used and re-use it later.

And you’re right. There’s probably a much easier way to do that matrix stuff which doesn’t require all those nodes… hmmmmm
I am completely guessing here, but I think you can combine what you’re doing in the joints and transforms sections: Make the temp object as a sibling of your target. Then match only the channels you care about using matchTransforms. Then just use cmds.xform to query the local matrix of your temp object, and set the OPM of your target. Then clear out whatever you need.

And I would definitely wrap the whole thing in an undo chunk.

That should be a good first pass, I think.

woah @tfox_TD thanks, those are some great tips! I didn’t know writing conditions is possible on one line. Elegant :slight_smile:

  1. Your pet-peeve hehe. Do you make any exceptions?
# this
cmds.joint("joint", edit=True, orientJoint="yxz", 
    secondaryAxisOrient="xup", children=True, zeroScaleOrient=True)
# instead of 
cmds.joint("joint", e=True, oj="yxz", sao="xup", ch=True, zso=True)
# ?

Is there no concern about using up a lot of space or breaking up the function call over multiple lines? Or is it just that the benefit of understanding the keywords is greater than the increase in script length?


  1. When you say “wrap it in an undo chunk”, does that mean something like this? (found that in an old post from 2012, maybe there’s a more up to date way to do that?)
cmds.undoInfo(openChunk=True)
try:
    #CODE CODE CODE
finally:
    cmds.undoInfo(closeChunk=True)

or something else? Also since I’m using my function in other modules would you do this undo-wrapping for every single function in a larger network of scripts?

thanks for the help!

I FULLY recognize that I’m a bit overzealous when it comes to that :blush: , but I do think that cmds.ls(sl=True) is OK. As well as q and e flags, because those are universal.
As for line continuation, I use an auto-formatter called Black. It just does whatever it wants, and I don’t think about it.

Hmmm … if you’re going to be reusing it a bunch, I’d say go ahead and make a context. It’s not much code, and it makes the chunks cleaner. I’d suggest looking at the built in contextlib. Specifically:
from contextlib import contextmanager

However, if you’re not going to re-use it, the try/finally is a perfect solution for one-offs.

I too have been working on a similar script recently. I’m guessing you’re probably not too worried about speed, but if you’re going for more elegant you could try using the Maya API MMatrix and/or MTransformationMatrix classes instead of hooking up and calculating a bunch of temporary transform nodes.

This is my simple code as of right now. It does not handle joint orients, yet

import maya.cmds as cmds
import maya.api.OpenMaya as om2

obj = cmds.ls(selection=True)[0]
local_mtx = om2.MMatrix(cmds.getAttr('{}.matrix'.format(obj)))
offset_parent_mtx = om2.MMatrix(cmds.getAttr('{}.offsetParentMatrix'.format(obj)))
dag_mtx = local_mtx * offset_parent_mtx

cmds.setAttr('{}.offsetParentMatrix'.format(obj), dag_mtx, type='matrix')
cmds.setAttr('{}.translate'.format(obj), 0,0,0)
cmds.setAttr('{}.rotate'.format(obj), 0,0,0)

And if you have Maya 2024 it’s even simpler

obj = cmds.ls(selection=True)[0]
dag_mtx = om2.MMatrix(cmds.getAttr('{}.dagLocalMatrix'.format(obj)))

cmds.setAttr('{}.offsetParentMatrix'.format(obj), dag_mtx, type='matrix')
cmds.setAttr('{}.translate'.format(obj), 0,0,0)
cmds.setAttr('{}.rotate'.format(obj), 0,0,0)

@tfox_TD I looked into the contextmanager and it looks and sounds super interesting but I don’t quite get it ^^ I made a contextmanager in my master script which imports and uses a bunch of utility scripts. When I undo that master script it takes the same amount of time as without it, so I’m not sure what the benefit is… Or do I need to put the contextmanager on every single method in scripts which the master script imports?

@WeatherMan oooh that’s very elegant. I didn’t think it was so simple to do matrix math with scripting. Very nice. Yea joint orients are a bit misterious, so for joints I’m still doing the temp_node for now.

Here is the updated and simplified script (removed the option to isolate translation, rotation or scale since I will probably always do all channels)

def zero_transforms(nodes):
    """ 
    transfers local matrix to offset parent matrix 
    leaving nodes with zeroed channels
    (maintains jointOrients for joints)
    """
    objects = [nodes] if isinstance(nodes, str) else nodes
    for obj in objects:
        if cmds.nodeType(obj) == "joint":
            # get .matrix from a temp node without rotation
            # since JNTs have 0 rot after orientation
            parent = cmds.listRelatives(obj, parent=True)[0]
            temp_grp = cmds.group(n="temp_GRP", p=parent, em=True)
            cmds.matchTransform(temp_grp, obj, position=True)
            offset = cmds.getAttr(f"{temp_grp}.matrix")
            cmds.setAttr(f"{obj}.offsetParentMatrix", offset, typ="matrix")
            cmds.setAttr(f"{obj}.translate", 0,0,0)
            cmds.delete(temp_grp)
        else:
            # direct matrix muliplication with OpenMaya
            parent = cmds.listRelatives(obj, parent=True)[0]
            invparent_mtx = om2.MMatrix(
                cmds.getAttr(f"{parent}.worldInverseMatrix[0]")
            )
            objworld_mtx = om2.MMatrix(
                cmds.getAttr(f"{obj}.worldMatrix[0]")
            )
            offsetparent_mtx = objworld_mtx * invparent_mtx
            cmds.setAttr(
                f"{obj}.offsetParentMatrix", offsetparent_mtx, type="matrix"
            )
            cmds.setAttr(f"{obj}.translate", 0,0,0)
            cmds.setAttr(f"{obj}.rotate", 0,0,0)

Thanks for the advice :slight_smile:

The contextmanager is purely for convenience of doing something both before and after a chunk of code. If you have to open/close or setup/teardown things, contextmanagers let you do that in a reusable package.

from contextlib import contextmanager

@contextmanager
def undoChunk()
    cmds.undoInfo(openChunk=True)
    try:
        yield
    finally:
        cmds.undoInfo(closeChunk=True)

def otherCode():
    with undoChunk():
        # CODE CODE CODE



And now that I think about it more, it may not be something you need. Maya will apparently undo a script as one big chunk, so I doubt my advice matters. I’m working across programs that don’t do that, so it got stuck in my brain. Sorry.
It’s still a useful tool to have, though.

2 Likes

ah gotcha. Thanks for explaining anyway. I can see how that would be veeery helpful if maya didn’t do it by itself for scripts. Cheers