Skip to content

Cult of Rig Posts

Pilot Season – Day 16 – Automatically loading callbacks on scene load

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.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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)

Pilot Season – Day 15 – Events and Application loops

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.

Enjoy:

Pilot Season – Day 14 – Working dynamic callback

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:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
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)

Pilot Season – Day 13 – Scripting Version of fk ik Switch

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:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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")

Pilot Season – Day 12 – Intro to OpenMaya2

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

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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) &gt;= 2:
    if mobTuple[0] is not None:
        srtWMtx = om2.MTransformationMatrix(wMtxFromMob(mobTuple[0]))
        srtWMtx.rotateBy(getMRotFromNodeOutput(mobTuple[1]), om2.MSpace.kWorld)