Day 15 of the stream, An introduction to Events and Application loops
We got blackboard heavy today, but understanding the basics of how interactive software works is important.
We spend a good hour going over how software simulates interactivity, how it tracks change, and at what points in those cycles you can insert your own operations.
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)
Day 13 of the stream, we keep keeping on with scripting until we obtain the right numbers and get ourselves ready to implement the dynamic, callback driven version
We conclude the scripting tangent required to deploy an actual dynamic FK to IK switch.
With OpenMaya2 introduced in the Day 12 stream, the design of the callback done and some attributes at hand, we finally get onto writing the actual code necessary for the callback to retrieve and transform the value until we obtain the right numbers.
As in previous scripting posts and videos the transcription of the code to the point we left it at the end of the episode is available at the end of this post.
Next stream all we’ll have to will be converting it to an actual callback and possibly ensuring it’s re-installed at scene open, but for now enjoy the scripting part taken from snippets to useful numbers:
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)
_MAYA_MATRIX_ATTRIBUTE_NAME ='worldMatrix'def wMtxFromMob(node_mob):
"""
finds the world matrix attribute and returns its value in matrix form
:param node_mob: [MObject] the node to get the world matrix from
:return: [MMatrix] the matrix value of the world transform on the argument node
"""ifnot node_mob.hasFn(om2.MFn.kDagNode):
returnNone
mfn_dag = om2.MFnDagNode(node_mob)
wMtxPlug = mfn_dag.findPlug(_MAYA_MATRIX_ATTRIBUTE_NAME,False)
elPlug = wMtxPlug.elementByLogicalIndex(0)
node_mob_attr = elPlug.asMObject()
mfn_mtxData = om2.MFnMatrixData(node_mob_attr)return mfn_mtxData.matrix()def mtxFromPlugSource(plug):
"""
takes a plug and retrieves the plug's source plug
then will try to initialise matrix data from it and return it
if valid
:param plug: [MPlug] a plug connected to a matrix type source
:return: [MMatrix | None]
"""if plug.isDestination:
mtxPlug = plug.source()
node_mob_attr = mtxPlug.asMObject()if node_mob_attr.hasFn(om2.MFn.kMatrixAttribute):
mfn_mtxData = om2.MFnMatrixData(node_mob_attr)return mfn_mtxData.matrix()returnNonedef mPointFromPlugSource(plug):
"""
similar to mtxFromPlugSource but will retrieve a translate compound source
:param plug: [MPlug] a plug connected to a translate compound triplet source
:return: [MPoing | None]
"""if plug.isDestination:
sourcePlug = plug.source()ifnot sourcePlug.isCompoundornot sourcePlug.numChildren()==3:
returnNone
mp = om2.MPoint()
returnPoint =[False,False,False]for i inxrange(sourcePlug.numChildren()):
realName = sourcePlug.child(i).partialName(includeNodeName =False, useAlias =False)if realName =='tx':
mp.x= sourcePlug.child(i).asFloat()
returnPoint[0]=Trueelif realName =='ty':
mp.y= sourcePlug.child(i).asFloat()
returnPoint[1]=Trueelif realName =='tz':
mp.z= sourcePlug.child(i).asFloat()
returnPoint[2]=Trueifall(returnPoint):
return mp
returnNone
_MAYA_OUTPUT_ATTRIBUTE_NAME ='output'def getMRotFromNodeOutput(node_mob, rotOrder = om2.MEulerRotation.kXYZ):
"""
finds the angular output of the argument node and returns it
as a Euler rotation where that angle is the X element
:param node_mob: [MObject] the node to get the output port from
:param rotOrder: [int] the factory constant for the desired rotation order
the returned Euler rotation should be set to
:return: [MEulerRotation] the Euler rotation composition where the
angular output on the argument node is the X value
"""
mfn_dep = om2.MFnDependencyNode(node_mob)
angle = om2.MAngle(0.0)if node_mob.hasFn(om2.MFn.kAnimBlend)and mfn_dep.hasAttribute(_MAYA_OUTPUT_ATTRIBUTE_NAME):
plug = mfn_dep.findPlug(_MAYA_OUTPUT_ATTRIBUTE_NAME,False)
angle = plug.asMAngle()
rot = om2.MEulerRotation(angle.asRadians(),0.0,0.0, rotOrder)return rot
# A dictionary of all attributes we're interested in by name, ready# to accept values for each.
attribs ={'blendedRotation': None,'fk_bfr_mtx': None,'ik_bfr_mtx': None,'ikPedalOffset': None,}
mobTuple =tuple(iterSelection())if mobTuple:
mfn_dep = om2.MFnDependencyNode(mobTuple[0])
plug = om2.MPlug()if mfn_dep.hasAttribute("FKIK_switch"):
plug = mfn_dep.findPlug("FKIK_switch",False)if plug.isNull:
print("invalid selection, no FKIK_switch attribute found on first selected item")else:
for eachPName in attribs.iterkeys():
plug = mfn_dep.findPlug(eachPName,False)
attribs[eachPName]= plug
isValid =Truefor p in attribs.itervalues():
if p isNone:
isValid =Falsebreakif isValid:
blendedRot = fk_bfr_mtx = ik_bfr_mtx = ikPedalOffset =None
blendedRot = attribs.get("blendedRotation").source().asMAngle().asRadians()
fk_bfr_mtx = mtxFromPlugSource(attribs.get("fk_bfr_mtx"))
ik_bfr_mtx = mtxFromPlugSource(attribs.get("ik_bfr_mtx"))
ikPedalOffset = mPointFromPlugSource(attribs.get("ikPedalOffset"))
projectedLen = ikPedalOffset.y
z =math.sin(blendedRot) * projectedLen
y =(1.0-math.cos(blendedRot)) * -projectedLen
else:
print("invalid selection count, nothing found selected from iterator")
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)
_MAYA_MATRIX_ATTRIBUTE_NAME = 'worldMatrix'
def wMtxFromMob(node_mob):
"""
finds the world matrix attribute and returns its value in matrix form
:param node_mob: [MObject] the node to get the world matrix from
:return: [MMatrix] the matrix value of the world transform on the argument node
"""
if not node_mob.hasFn(om2.MFn.kDagNode):
return None
mfn_dag = om2.MFnDagNode(node_mob)
wMtxPlug = mfn_dag.findPlug(_MAYA_MATRIX_ATTRIBUTE_NAME, False)
elPlug = wMtxPlug.elementByLogicalIndex(0)
node_mob_attr = elPlug.asMObject()
mfn_mtxData = om2.MFnMatrixData(node_mob_attr)
return mfn_mtxData.matrix()
def mtxFromPlugSource(plug):
"""
takes a plug and retrieves the plug's source plug
then will try to initialise matrix data from it and return it
if valid
:param plug: [MPlug] a plug connected to a matrix type source
:return: [MMatrix | None]
"""
if plug.isDestination:
mtxPlug = plug.source()
node_mob_attr = mtxPlug.asMObject()
if node_mob_attr.hasFn(om2.MFn.kMatrixAttribute):
mfn_mtxData = om2.MFnMatrixData(node_mob_attr)
return mfn_mtxData.matrix()
return None
def mPointFromPlugSource(plug):
"""
similar to mtxFromPlugSource but will retrieve a translate compound source
:param plug: [MPlug] a plug connected to a translate compound triplet source
:return: [MPoing | None]
"""
if plug.isDestination:
sourcePlug = plug.source()
if not sourcePlug.isCompound or not sourcePlug.numChildren() == 3:
return None
mp = om2.MPoint()
returnPoint = [False,False,False]
for i in xrange(sourcePlug.numChildren()):
realName = sourcePlug.child(i).partialName(includeNodeName = False, useAlias = False)
if realName == 'tx':
mp.x = sourcePlug.child(i).asFloat()
returnPoint[0] = True
elif realName == 'ty':
mp.y = sourcePlug.child(i).asFloat()
returnPoint[1] = True
elif realName == 'tz':
mp.z = sourcePlug.child(i).asFloat()
returnPoint[2] = True
if all(returnPoint):
return mp
return None
_MAYA_OUTPUT_ATTRIBUTE_NAME = 'output'
def getMRotFromNodeOutput(node_mob, rotOrder = om2.MEulerRotation.kXYZ):
"""
finds the angular output of the argument node and returns it
as a Euler rotation where that angle is the X element
:param node_mob: [MObject] the node to get the output port from
:param rotOrder: [int] the factory constant for the desired rotation order
the returned Euler rotation should be set to
:return: [MEulerRotation] the Euler rotation composition where the
angular output on the argument node is the X value
"""
mfn_dep = om2.MFnDependencyNode(node_mob)
angle = om2.MAngle(0.0)
if node_mob.hasFn(om2.MFn.kAnimBlend) and mfn_dep.hasAttribute(_MAYA_OUTPUT_ATTRIBUTE_NAME):
plug = mfn_dep.findPlug(_MAYA_OUTPUT_ATTRIBUTE_NAME, False)
angle = plug.asMAngle()
rot = om2.MEulerRotation(angle.asRadians(), 0.0, 0.0, rotOrder)
return rot
# A dictionary of all attributes we're interested in by name, ready
# to accept values for each.
attribs = {
'blendedRotation': None,
'fk_bfr_mtx': None,
'ik_bfr_mtx': None,
'ikPedalOffset': None,
}
mobTuple = tuple(iterSelection())
if mobTuple:
mfn_dep = om2.MFnDependencyNode(mobTuple[0])
plug = om2.MPlug()
if mfn_dep.hasAttribute("FKIK_switch"):
plug = mfn_dep.findPlug("FKIK_switch", False)
if plug.isNull:
print("invalid selection, no FKIK_switch attribute found on first selected item")
else:
for eachPName in attribs.iterkeys():
plug = mfn_dep.findPlug(eachPName, False)
attribs[eachPName] = plug
isValid = True
for p in attribs.itervalues():
if p is None:
isValid = False
break
if isValid:
blendedRot = fk_bfr_mtx = ik_bfr_mtx = ikPedalOffset = None
blendedRot = attribs.get("blendedRotation").source().asMAngle().asRadians()
fk_bfr_mtx = mtxFromPlugSource(attribs.get("fk_bfr_mtx"))
ik_bfr_mtx = mtxFromPlugSource(attribs.get("ik_bfr_mtx"))
ikPedalOffset = mPointFromPlugSource(attribs.get("ikPedalOffset"))
projectedLen = ikPedalOffset.y
z = math.sin(blendedRot) * projectedLen
y = (1.0-math.cos(blendedRot)) * -projectedLen
else:
print("invalid selection count, nothing found selected from iterator")
Day 12 of the stream, originally intended to be focused on developing an FK IK switch Callback turned into a necessary introduction to dealing with transform attributes in OpenMaya2
While not necessarily what I was expecting to stream about I don’t regret going off this tangent.
Day 11 stream about callbacks is a prerequisite watch to understand this one, unless you only want to hear about OM2 quirks and how-to, in which case this stands on its own.
We start by designing the callback we want to implement starting from its boundaries, then do our shopping list, and finally we introduce all kind of OpenMaya2 related notions and quirks to make sure transforms and attributes come in correctly.
We don’t have a callback yet by the end, but we now have all the Maya API bits covered to develop one next stream.
Similarly to the previous episode (second half of day 11) it seems useful to commit a cleaned up transcription of the final script from the stream to make it available here. You will find it at the end of the page
from maya.apiimport OpenMaya as om2
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 inxrange(sel.length()):
yield sel.getDependNode(i)
_MAYA_MATRIX_ATTRIBUTE_NAME ='worldMatrix'def wMtxFromMob(node_mob):
"""
finds the world matrix attribute and returns its value in matrix form
:param node_mob: [MObject] the node to get the world matrix from
:return: [MMatrix] the matrix value of the world transform on the argument node
"""ifnot node_mob.hasFn(om2.MFn.kDagNode):
returnNone
mfn_dag = om2.MFnDagNode(node_mob)
wMtxPlug = mfn_dag.findPlug(_MAYA_MATRIX_ATTRIBUTE_NAME,False)
elPlug = wMtxPlug.elementByLogicalIndex(0)
node_mob_attr = elPlug.asMObject()
mfn_mtxData = om2.MFnMatrixData(node_mob_attr)return mfn_mtxData.matrix()
_MAYA_OUTPUT_ATTRIBUTE_NAME ='output'def getMRotFromNodeOutput(node_mob, rotOrder = om2.MEulerRotation.kXYZ):
"""
finds the angular output of the argument node and returns it
as a Euler rotation where that angle is the X element
:param node_mob: [MObject] the node to get the output port from
:param rotOrder: [int] the factory constant for the desired rotation order
the returned Euler rotation should be set to
:return: [MEulerRotation] the Euler rotation composition where the
angular output on the argument node is the X value
"""
mfn_dep = om2.MFnDependencyNode(node_mob)
angle = om2.MAngle(0.0)if node_mob.hasFn(om2.MFn.kAnimBlend)and mfn_dep.hasAttribute(_MAYA_OUTPUT_ATTRIBUTE_NAME):
plug = mfn_dep.findPlug(_MAYA_OUTPUT_ATTRIBUTE_NAME,False)
angle = plug.asMAngle()
rot = om2.MEulerRotation(angle.asRadians(),0.0,0.0, rotOrder)return rot
mobTuple =tuple(iterSelection())iflen(mobTuple) >=2:
if mobTuple[0]isnotNone:
srtWMtx = om2.MTransformationMatrix(wMtxFromMob(mobTuple[0]))
srtWMtx.rotateBy(getMRotFromNodeOutput(mobTuple[1]), om2.MSpace.kWorld)
from maya.api import OpenMaya as om2
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)
_MAYA_MATRIX_ATTRIBUTE_NAME = 'worldMatrix'
def wMtxFromMob(node_mob):
"""
finds the world matrix attribute and returns its value in matrix form
:param node_mob: [MObject] the node to get the world matrix from
:return: [MMatrix] the matrix value of the world transform on the argument node
"""
if not node_mob.hasFn(om2.MFn.kDagNode):
return None
mfn_dag = om2.MFnDagNode(node_mob)
wMtxPlug = mfn_dag.findPlug(_MAYA_MATRIX_ATTRIBUTE_NAME, False)
elPlug = wMtxPlug.elementByLogicalIndex(0)
node_mob_attr = elPlug.asMObject()
mfn_mtxData = om2.MFnMatrixData(node_mob_attr)
return mfn_mtxData.matrix()
_MAYA_OUTPUT_ATTRIBUTE_NAME = 'output'
def getMRotFromNodeOutput(node_mob, rotOrder = om2.MEulerRotation.kXYZ):
"""
finds the angular output of the argument node and returns it
as a Euler rotation where that angle is the X element
:param node_mob: [MObject] the node to get the output port from
:param rotOrder: [int] the factory constant for the desired rotation order
the returned Euler rotation should be set to
:return: [MEulerRotation] the Euler rotation composition where the
angular output on the argument node is the X value
"""
mfn_dep = om2.MFnDependencyNode(node_mob)
angle = om2.MAngle(0.0)
if node_mob.hasFn(om2.MFn.kAnimBlend) and mfn_dep.hasAttribute(_MAYA_OUTPUT_ATTRIBUTE_NAME):
plug = mfn_dep.findPlug(_MAYA_OUTPUT_ATTRIBUTE_NAME, False)
angle = plug.asMAngle()
rot = om2.MEulerRotation(angle.asRadians(), 0.0, 0.0, rotOrder)
return rot
mobTuple = tuple(iterSelection())
if len(mobTuple) >= 2:
if mobTuple[0] is not None:
srtWMtx = om2.MTransformationMatrix(wMtxFromMob(mobTuple[0]))
srtWMtx.rotateBy(getMRotFromNodeOutput(mobTuple[1]), om2.MSpace.kWorld)
Day 11 of the stream, where we introduce guides to the components, a space switch to the pedals, and most offer a demo of Maya Callbacks and how they can be introduced and managed.
This was a very long stream, and it’s sort of divided in two parts. Unintentionally, but luckily, it’s split right down the middle of its duration.
Part 1 lasts until the end of the first hour and it’s about adding live guides to components:
We show how to re-purpose some items we already had sitting around to add a guide mode to the two components we had finalized before, as well as introduce a parent switch/blend to the pedals.
Part 2, from the one hour mark on, is about Maya Callbacks:
Maya Callbacks are a native facility of Maya to introduce cross-talk between nodes driven by events that isn’t dependent on the graph, nor affects it.
It’s extremely useful to establish behavior in a graph that would otherwise be cyclic if it was left to live evaluation all the time:
E.G. Object A drives object B, but object B also drives in a similar fashion object A
Script notes:
Towards the end of the stream someone asked if I could post the example code, which seems like a very good idea.
At the end of this post you will find a formatted and cleaned up version of the episode’s script that you can run on a selection of N objects, and will install a callback that matches the translation of any node immediately downstream of the message plug of the node with the callback. If this sounds convoluted, don’t worry, the stream should make it a lot clearer.
Usage wise all you need to do to make it work and demonstrate how it can affect objects reciprocally is to have a couple nodes selected when you run it that are also connected to each other cyclically by their message plug. Again, if it sounds hard to understand, don’t worry, by the end of the video that should also be pretty obvious.
The script posted here isn’t meant to be a commando typing exercise, nor to crash your Maya session, so the callback is expanded with a circuit breaker to compare the effect of the loop, which wasn’t present in the one we worked on during the stream. That part represents what I was talking about when I mentioned you can use callbacks to manage benign cycles by exiting early on a successful comparison.
Last but not least: It also turns out I was probably incorrect in answering about deploying callbacks. They still don’t seem to leave any record in the scene, and therefore need to be managed on scene open by “the pipeline”.
It seems to me that showing a simple callback manager and tracker might be in the future of this season if the programming component shown today for the first time is to people’s liking 🙂
On to the videos, and script will follow at the end of the page:
from maya.apiimport OpenMaya as om2
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 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)for eachCB in cbs:
om2.MMessage.removeCallback(eachCB)len(cbs)def translationPlugsFromAnyPlug(plug):
"""
:param plug: [MPlug] plug on a node to retrieve translation related plugs from
:return: [tuple(MPlug)] tuple of compound translate plug,
and three axes translate plugs
"""
node = plug.node()ifnot node.hasFn(om2.MFn.kTransform): # this should exclude nodes without translate plugsreturn
mfn_dep = om2.MFnDependencyNode(node)
pNames =('translate','tx','ty','tz')returntuple([mfn_dep.findPlug(eachName,False)for eachName in pNames])def msgConnectedPlugs(plug):
"""
:param plug: [MPlug] plug on a node owning message plug
we wish to retrieve all destination plugs from
:return: [tuple(MPlug)] all plugs on other nodes receiving a message connection
coming from the one owning the argument plug
"""
mfn_dep = om2.MFnDependencyNode(plug.node())
msgPlug = mfn_dep.findPlug('message',False)returntuple([om2.MPlug(otherP)for otherP in msgPlug.destinations()])def almostEqual(a, b, rel_tol=1e-09, abs_tol=0.0):
"""
Lifted from pre 3.5 isclose() implementation,
floating point error tolerant comparison
:param a: [float] first number in comparison
:param b: [float] second number in comparison
:param rel_tol: [float] relative tolerance in comparison
:param abs_tol: [float] absolute tolerance in case of relative tolerance issues
:return: [bool] args are equal or not
"""returnabs(a-b) <=max(rel_tol * max(abs(a),abs(b)), abs_tol)def cb(msg, plug1, plug2, payload):
if msg !=2056: #check most common case first and return unless it'sreturn# an attribute edit type of callback
srcTranslationPlugs = translationPlugsFromAnyPlug(plug1)ifnotlen(srcTranslationPlugs):
return# trim out the first plug, the translate compound, and only work on the triplet xyz
values =[p.asFloat()for p in srcTranslationPlugs[1:4]]for eachDestPlug in msgConnectedPlugs(plug1): # all receiving plugs
destTranslationPlugs = translationPlugsFromAnyPlug(eachDestPlug)[1:4]for i, p inenumerate(destTranslationPlugs):
if almostEqual(p.asFloat(), values[i]):
continue
p.setFloat(values[i])for eachMob in iterSelection():
removeCallbacksFromNode(eachMob)
om2.MNodeMessage.addAttributeChangedCallback(eachMob, cb)
from maya.api import OpenMaya as om2
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)
for eachCB in cbs:
om2.MMessage.removeCallback(eachCB)
len(cbs)
def translationPlugsFromAnyPlug(plug):
"""
:param plug: [MPlug] plug on a node to retrieve translation related plugs from
:return: [tuple(MPlug)] tuple of compound translate plug,
and three axes translate plugs
"""
node = plug.node()
if not node.hasFn(om2.MFn.kTransform): # this should exclude nodes without translate plugs
return
mfn_dep = om2.MFnDependencyNode(node)
pNames = ('translate', 'tx', 'ty', 'tz')
return tuple([mfn_dep.findPlug(eachName, False) for eachName in pNames])
def msgConnectedPlugs(plug):
"""
:param plug: [MPlug] plug on a node owning message plug
we wish to retrieve all destination plugs from
:return: [tuple(MPlug)] all plugs on other nodes receiving a message connection
coming from the one owning the argument plug
"""
mfn_dep = om2.MFnDependencyNode(plug.node())
msgPlug = mfn_dep.findPlug('message', False)
return tuple([om2.MPlug(otherP) for otherP in msgPlug.destinations()])
def almostEqual(a, b, rel_tol=1e-09, abs_tol=0.0):
"""
Lifted from pre 3.5 isclose() implementation,
floating point error tolerant comparison
:param a: [float] first number in comparison
:param b: [float] second number in comparison
:param rel_tol: [float] relative tolerance in comparison
:param abs_tol: [float] absolute tolerance in case of relative tolerance issues
:return: [bool] args are equal or not
"""
return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
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
srcTranslationPlugs = translationPlugsFromAnyPlug(plug1)
if not len(srcTranslationPlugs):
return
# trim out the first plug, the translate compound, and only work on the triplet xyz
values = [p.asFloat() for p in srcTranslationPlugs[1:4]]
for eachDestPlug in msgConnectedPlugs(plug1): # all receiving plugs
destTranslationPlugs = translationPlugsFromAnyPlug(eachDestPlug)[1:4]
for i, p in enumerate(destTranslationPlugs):
if almostEqual(p.asFloat(), values[i]):
continue
p.setFloat(values[i])
for eachMob in iterSelection():
removeCallbacksFromNode(eachMob)
om2.MNodeMessage.addAttributeChangedCallback(eachMob, cb)