We made some progress on our component instrumentation and we can now reliably retrieve interesting objects and their relationship to our components starting from a simple selection.
Don’t be afraid if you’re not interested. Or maybe not up to scratch on your scripting to follow the details.
This will NOT be necessary to follow the stream once we’ll resume non programming work.
I do, however, recommend that you try and practice, even if you’re unfamiliar with Python or OpenMaya, and need to use other means to obtain similar results.
Even if you’re completely new to programming and boil the experience down to copying and pasting then running the stuff I will be sharing. There is great mileage to be had in exploring interacting with the Maya client through means other than the UI.
Since we’ll be moving predominantly on the scripting front for a few streams I won’t be posting every bit of text, but all of it will be promptly made available on my Public Repository on Github as linked here.
We start by diving into some exploratory work to learn how to deal with containers through OpenMaya, and how high level concepts such as assets can be mapped to simple graph operations.
Our long term goal is to make the guiding and de-guiding process much, much easier than if we had to manage it manually.
We provide a quick primer on Maya attributes and how to form compound attributes, which will be very important for our guide clean up process, and then show how to do an FKoverIK set up which we’ll use to simplify the guide.
A very simple script is used during the stream. As usual it’s provided in this post, and I will eventually add it to the GitHub repository.
All new episode now free of low frequency hum in the audio!
Enjoy,
Day 16 of the stream, this is where we wrap callbacks for good and show how to automatically deploy one on the rig at scene load time through a Script Node
Closing the callback saga we finally wrap it all together, make the callback work with the UI when an attribute is slid and not just changed, and finally make sure it auto deploys on the rig whenever the scene is opened through the use of a Script Node.
As usual with scripting heavy sessions at the end of this post you will find the transcriptions of the final result, in this case the script to add to the Script Node that will deploy the callback on scene load events.
from maya.apiimport OpenMaya as om2
from maya import cmds
importmathdef removeCallbacksFromNode(node_mob):
"""
:param node_mob: [MObject] the node to remove all node callbacks from
:return: [int] number of callbacks removed
"""
cbs = om2.MMessage.nodeCallbacks(node_mob)
cbCount =len(cbs)for eachCB in cbs:
om2.MMessage.removeCallback(eachCB)return cbCount
def cb(msg, plug1, plug2, payload):
fkik_attrName ='FKIK_switch'if msg !=2056: # check most common case first and return unless it'sreturn# an attribute edit type of callbackifnot plug1.partialName(includeNodeName=False, useAlias=False)== fkik_attrName:
# We ensure if the attribute being changed is uninteresting we do nothingreturn
isFK = plug1.asBool()==False# Switched To FK
isIK =not isFK # Switched to IK
settingsAttrs ={# all interesting attribute names in keys, respective plugs in values'fkRotation': None,'ikRotation': None,'fk_ctrl_rotx': None,'ik_ctrl_translate': None,'ikPedalOffset': None,'dirtyTracker': None,}
mfn_dep = om2.MFnDependencyNode(plug1.node())# We populate the dictionary of interesting attributes with their plugsfor eachPName in settingsAttrs.iterkeys():
plug = mfn_dep.findPlug(eachPName,False)
settingsAttrs[eachPName]= plug
for p in settingsAttrs.itervalues():
# We will exit early and do nothing if a plug couldn't be initialised, the object# is malformed, or we installed the callback on an object that is only# conformant by accident and can't operate as we expect it to.if p isNone:
return
dirtyTrackerPlug = settingsAttrs.get('dirtyTracker')
isDirty = dirtyTrackerPlug.asBool()!= plug1.asBool()if isDirty:
dirtyTrackerPlug.setBool(plug1.asBool())else:
return
angle =None# empty initif isFK:
# Simplest case, if we switched to FK we copy the roation from IK# to the FK control's X rotation value
angle = -settingsAttrs.get("ikRotation").source().asDouble()
fkSourcePlug = settingsAttrs.get("fk_ctrl_rotx").source()
fkSourcePlug.setDouble(angle)elif isIK:
# If instead we switched to IK we need to# derive the translation of the IK control that produces the result# of an equivalent rotation to the one coming from the FK control
angle = settingsAttrs.get("fkRotation").source().asDouble()
projectedLen = settingsAttrs.get("ikPedalOffset").source().asDouble()
y =(math.cos(angle) * projectedLen) - projectedLen
z =math.sin(angle) * projectedLen
ikSourcePlug = settingsAttrs.get("ik_ctrl_translate").source()for i inxrange(ikSourcePlug.numChildren()):
realName = ikSourcePlug.child(i).partialName(includeNodeName=False, useAlias=False)if realName =='ty':
ikSourcePlug.child(i).setDouble(y)elif realName =='tz':
ikSourcePlug.child(i).setDouble(z)# full scene DAG path to the object we're looking for, our settings panel
settingsStrPath ="unicycle|pedals_M_cmpnt|control|pedals_M_settings_ctrl"# check for its existence
found = cmds.objExists(settingsStrPath)if found: # act only if found# the following is A way to get an MObject from a name in OM2
sel = om2.MSelectionList();
sel.add(settingsStrPath)
settingsMob = sel.getDependNode(0)# first we need to ensure the node isn't dirty by default# by aligning the value of the switch the rig was save with# into the dirty tracker
mfn_dep = om2.MFnDependencyNode(settingsMob)
fkikPlug = mfn_dep.findPlug('FKIK_switch',False)
dirtyTracker = mfn_dep.findPlug('dirtyTracker',False)
dirtyTracker.setBool(fkikPlug.asBool())# as this is on scene open only the following callback removal# shouldn't be necessary since callbacks don't persist on scene save,# but why not?
removeCallbacksFromNode(settingsMob)# and we finally add the callback implementation to the settings node
om2.MNodeMessage.addAttributeChangedCallback(settingsMob, cb)
from maya.api import OpenMaya as om2
from maya import cmds
import math
def removeCallbacksFromNode(node_mob):
"""
:param node_mob: [MObject] the node to remove all node callbacks from
:return: [int] number of callbacks removed
"""
cbs = om2.MMessage.nodeCallbacks(node_mob)
cbCount = len(cbs)
for eachCB in cbs:
om2.MMessage.removeCallback(eachCB)
return cbCount
def cb(msg, plug1, plug2, payload):
fkik_attrName = 'FKIK_switch'
if msg != 2056: # check most common case first and return unless it's
return # an attribute edit type of callback
if not plug1.partialName(includeNodeName=False, useAlias=False) == fkik_attrName:
# We ensure if the attribute being changed is uninteresting we do nothing
return
isFK = plug1.asBool() == False # Switched To FK
isIK = not isFK # Switched to IK
settingsAttrs = { # all interesting attribute names in keys, respective plugs in values
'fkRotation': None,
'ikRotation': None,
'fk_ctrl_rotx': None,
'ik_ctrl_translate': None,
'ikPedalOffset': None,
'dirtyTracker': None,
}
mfn_dep = om2.MFnDependencyNode(plug1.node())
# We populate the dictionary of interesting attributes with their plugs
for eachPName in settingsAttrs.iterkeys():
plug = mfn_dep.findPlug(eachPName, False)
settingsAttrs[eachPName] = plug
for p in settingsAttrs.itervalues():
# We will exit early and do nothing if a plug couldn't be initialised, the object
# is malformed, or we installed the callback on an object that is only
# conformant by accident and can't operate as we expect it to.
if p is None:
return
dirtyTrackerPlug = settingsAttrs.get('dirtyTracker')
isDirty = dirtyTrackerPlug.asBool() != plug1.asBool()
if isDirty:
dirtyTrackerPlug.setBool(plug1.asBool())
else:
return
angle = None # empty init
if isFK:
# Simplest case, if we switched to FK we copy the roation from IK
# to the FK control's X rotation value
angle = -settingsAttrs.get("ikRotation").source().asDouble()
fkSourcePlug = settingsAttrs.get("fk_ctrl_rotx").source()
fkSourcePlug.setDouble(angle)
elif isIK:
# If instead we switched to IK we need to
# derive the translation of the IK control that produces the result
# of an equivalent rotation to the one coming from the FK control
angle = settingsAttrs.get("fkRotation").source().asDouble()
projectedLen = settingsAttrs.get("ikPedalOffset").source().asDouble()
y = (math.cos(angle) * projectedLen) - projectedLen
z = math.sin(angle) * projectedLen
ikSourcePlug = settingsAttrs.get("ik_ctrl_translate").source()
for i in xrange(ikSourcePlug.numChildren()):
realName = ikSourcePlug.child(i).partialName(includeNodeName=False, useAlias=False)
if realName == 'ty':
ikSourcePlug.child(i).setDouble(y)
elif realName == 'tz':
ikSourcePlug.child(i).setDouble(z)
# full scene DAG path to the object we're looking for, our settings panel
settingsStrPath = "unicycle|pedals_M_cmpnt|control|pedals_M_settings_ctrl"
# check for its existence
found = cmds.objExists(settingsStrPath)
if found: # act only if found
# the following is A way to get an MObject from a name in OM2
sel = om2.MSelectionList();
sel.add(settingsStrPath)
settingsMob = sel.getDependNode(0)
# first we need to ensure the node isn't dirty by default
# by aligning the value of the switch the rig was save with
# into the dirty tracker
mfn_dep = om2.MFnDependencyNode(settingsMob)
fkikPlug = mfn_dep.findPlug('FKIK_switch', False)
dirtyTracker = mfn_dep.findPlug('dirtyTracker', False)
dirtyTracker.setBool(fkikPlug.asBool())
# as this is on scene open only the following callback removal
# shouldn't be necessary since callbacks don't persist on scene save,
# but why not?
removeCallbacksFromNode(settingsMob)
# and we finally add the callback implementation to the settings node
om2.MNodeMessage.addAttributeChangedCallback(settingsMob, cb)
Day 14 of the stream, we finally install a fully working callback for the FK <-> IK switch on the pedals.
Finishing the Maya Callbacks and OpenMaya stretch of the stream we finally have a fully working FK <-> IK switch for the pedals.
We only have the virtual slider case to fix, which is more a matter of finding a good stepped signal to work off of than anything else, and then maybe ensuring callbacks are installed every time the scene is opened.
Exercise for home, if you’re inclined:
Look into making the callback work with virtual sliders.
Hint 1: not unlike the reciprocal behaviour added as a demo to the end of the Day 11 Stream it’s about comparing the result we get, with something that changes reliably and not depending on UI interaction
Hint 2: the angles we get from the separate IK and FK feed could be compared against the angle coming from the blend, which is graph dependent and not UI dependent, and comparison should provide the clean state switch we’re after.
As with all previous script heavy days you will find the commented transcription at the end of the page.
Enjoy:
fkik_attrName ='FKIK_switch'from maya.apiimport _OpenMaya_py2 as om2
from maya import cmds
importmathdef iterSelection():
"""
generator style iterator over current Maya active selection
:return: [MObject) an MObject for each item in the selection
"""
sel = om2.MGlobal.getActiveSelectionList()for i inxrange(sel.length()):
yield sel.getDependNode(i)def removeCallbacksFromNode(node_mob):
"""
:param node_mob: [MObject] the node to remove all node callbacks from
:return: [int] number of callbacks removed
"""
cbs = om2.MMessage.nodeCallbacks(node_mob)
cbCount =len(cbs)for eachCB in cbs:
om2.MMessage.removeCallback(eachCB)return cbCount
def removeCallbacksFromSel():
"""
Will remove all callbacks from each node in the current selection
:return: [(int, int)] total number of objects that had callbacks removed,
and total count of all callbacks removed across them
"""
cbCount =0
mobCount =0for eachMob in iterSelection():
mobCount +=1
cbCount += removeCallbacksFromNode(eachMob)return mobCount, cbCount
def cb(msg, plug1, plug2, payload):
if msg !=2056: #check most common case first and return unless it'sreturn# an attribute edit type of callbackifnot plug1.partialName(includeNodeName=False, useAlias=False)== fkik_attrName:
# We ensure if the attribute being changed is uninteresting we do nothingreturn
isFK = plug1.asBool()==False# Switched To FK
isIK =not isFK # Switched to IK
settingsAttrs ={# all interesting attribute names in keys, respective plugs in values'fkRotation': None,'ikRotation': None,'fk_ctrl_rotx': None,'ik_ctrl_translate': None,'ikPedalOffset': None,}
mfn_dep = om2.MFnDependencyNode(plug1.node())# We populate the dictionary of interesting attributes with their plugsfor eachPName in settingsAttrs.iterkeys():
plug = mfn_dep.findPlug(eachPName,False)
settingsAttrs[eachPName]= plug
for p in settingsAttrs.itervalues():
# We will exit early and do nothing if a plug couldn't be initialised, the object# is malformed, or we installed the callback on an object that is only# conformant by accident and can't operate as we expect it to.if p isNone:
return
angle =None# empty initif isFK:
# Simplest case, if we switched to FK we copy the roation from IK# to the FK control's X rotation value
angle = -settingsAttrs.get("ikRotation").source().asDouble()
fkSourcePlug = settingsAttrs.get("fk_ctrl_rotx").source()
fkSourcePlug.setDouble(angle)elif isIK:
# If instead we switched to IK we need to# derive the translation of the IK control that produces the result# of an equivalent rotation to the one coming from the FK control
angle = settingsAttrs.get("fkRotation").source().asDouble()
projectedLen = settingsAttrs.get("ikPedalOffset").source().asDouble()
y =(math.cos(angle) * projectedLen ) - projectedLen
z =math.sin(angle) * projectedLen
ikSourcePlug = settingsAttrs.get("ik_ctrl_translate").source()for i inxrange(ikSourcePlug.numChildren()):
realName = ikSourcePlug.child(i).partialName(includeNodeName =False, useAlias =False)if realName =='ty':
ikSourcePlug.child(i).setDouble(y)elif realName =='tz':
ikSourcePlug.child(i).setDouble(z)
removeCallbacksFromSel()for eachMob in iterSelection():
om2.MNodeMessage.addAttributeChangedCallback(eachMob, cb)
fkik_attrName = 'FKIK_switch'
from maya.api import _OpenMaya_py2 as om2
from maya import cmds
import math
def iterSelection():
"""
generator style iterator over current Maya active selection
:return: [MObject) an MObject for each item in the selection
"""
sel = om2.MGlobal.getActiveSelectionList()
for i in xrange(sel.length()):
yield sel.getDependNode(i)
def removeCallbacksFromNode(node_mob):
"""
:param node_mob: [MObject] the node to remove all node callbacks from
:return: [int] number of callbacks removed
"""
cbs = om2.MMessage.nodeCallbacks(node_mob)
cbCount = len(cbs)
for eachCB in cbs:
om2.MMessage.removeCallback(eachCB)
return cbCount
def removeCallbacksFromSel():
"""
Will remove all callbacks from each node in the current selection
:return: [(int, int)] total number of objects that had callbacks removed,
and total count of all callbacks removed across them
"""
cbCount = 0
mobCount = 0
for eachMob in iterSelection():
mobCount += 1
cbCount += removeCallbacksFromNode(eachMob)
return mobCount, cbCount
def cb(msg, plug1, plug2, payload):
if msg != 2056: #check most common case first and return unless it's
return # an attribute edit type of callback
if not plug1.partialName(includeNodeName=False, useAlias=False) == fkik_attrName:
# We ensure if the attribute being changed is uninteresting we do nothing
return
isFK = plug1.asBool() == False # Switched To FK
isIK = not isFK # Switched to IK
settingsAttrs = { # all interesting attribute names in keys, respective plugs in values
'fkRotation': None,
'ikRotation': None,
'fk_ctrl_rotx': None,
'ik_ctrl_translate': None,
'ikPedalOffset': None,
}
mfn_dep = om2.MFnDependencyNode(plug1.node())
# We populate the dictionary of interesting attributes with their plugs
for eachPName in settingsAttrs.iterkeys():
plug = mfn_dep.findPlug(eachPName, False)
settingsAttrs[eachPName] = plug
for p in settingsAttrs.itervalues():
# We will exit early and do nothing if a plug couldn't be initialised, the object
# is malformed, or we installed the callback on an object that is only
# conformant by accident and can't operate as we expect it to.
if p is None:
return
angle = None # empty init
if isFK:
# Simplest case, if we switched to FK we copy the roation from IK
# to the FK control's X rotation value
angle = -settingsAttrs.get("ikRotation").source().asDouble()
fkSourcePlug = settingsAttrs.get("fk_ctrl_rotx").source()
fkSourcePlug.setDouble(angle)
elif isIK:
# If instead we switched to IK we need to
# derive the translation of the IK control that produces the result
# of an equivalent rotation to the one coming from the FK control
angle = settingsAttrs.get("fkRotation").source().asDouble()
projectedLen = settingsAttrs.get("ikPedalOffset").source().asDouble()
y = ( math.cos(angle) * projectedLen ) - projectedLen
z = math.sin(angle) * projectedLen
ikSourcePlug = settingsAttrs.get("ik_ctrl_translate").source()
for i in xrange(ikSourcePlug.numChildren()):
realName = ikSourcePlug.child(i).partialName(includeNodeName = False, useAlias = False)
if realName == 'ty':
ikSourcePlug.child(i).setDouble(y)
elif realName == 'tz':
ikSourcePlug.child(i).setDouble(z)
removeCallbacksFromSel()
for eachMob in iterSelection():
om2.MNodeMessage.addAttributeChangedCallback(eachMob, cb)