Thursday, July 7, 2016

Scripting A Simple Pose Reader

I recently learned this setup and just wanted to share a scripted version I had been working on.  For anyone who has never used a pose reader setup, it's a fantastic way to make your actions that are driven by specific poses to be more stable.  When I first learned rigging in school, I was taught to use set driven keyframes to drive an object based on a certain pose, so I included that setup in this blog as a comparison to the simple pose reader setup.  As a side note, the Pirate rig that is in my latest demo reel actually utilized this "simple pose reader" quite a bit - as I was learning the technique I started scripting it.  I used it to drive things like special deformation joints, corrective blendshapes, auto hips and shoulders, as well as accessories on the pirate's belt would move as the user interacts with the leg controls.  It was quite fun to work with such a simple and effective setup.

Anyway, here is a quick gif video of an example comparing two setups, the green is an action driven by a pose through an SDK - and the red is an action driven by a pose through the PSR. 


The SDK setup is a simple, the joint's rotateZ attribute from 0-90 will drive the translateY attribute of the sphere from 0-1.  Very limited without adding more animations.

Here is the simple graph setup for the PSR to drive the movement of the sphere.  To summarize, you are utilizing the PSR's controlled rotation values and remapping those values to different values that are sent over to the sphere (in this instance a single rotation of the joint drives the translate y of the sphere).



And here is the script I was working with...


  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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# Maya imports
import pymel.core as pm
import maya.OpenMaya as om


def simple_pose_reader( root_joint ):
    """
    Function to create a simple pose reader with "bend", "twist", "side" attributes 
    on the selected joint to use to drive other systems
    
    Args:
        root_joint (pynode) : a pynode object representing the main joint used for the pose reading
        
    Returns:
        None
        
    """
    
    def first_or_default( sequence, default=None ):
        """
        Function to return the first item in a list or a default value
        
        Args:
            sequence (list) : a list of items to parse
            default (object) : a default value to return back if nothing was parsed from sequence
            
        Returns:
            The first object within the sequence or a default value
            
        """
        
        for item in sequence:
            return item
            
        return default

        
    def get_bone_draw_axis( joint, default=om.MVector(0,1,0) ):
        """
        Function to return the first item in a list or a default value
        
        Args:
            joint (pynode) : the pynode that represents the joint to determine the draw_axis of
            default (object) : a default value to return back if the draw_axis was not determined
            
        Returns:
            A MVector type that represents the normalized direction of the joint draw axis
            
        """
        
        try:
            child = first_or_default( joint.getChildren( type='joint' ) )
            
        except:
            raise ValueError( joint + " does not have any children" )
        
        # Get the local position of the child joint ( localspace is offset from the parent space )
        pos = [ value for value in child.getTranslation( localSpace=True ) ]
        
        # Check which axis is greater than the others, this will determine the draw_axis vector
        # X Axis
        if abs( pos[0] ) > abs( pos[1] ) and abs( pos[0] ) > abs( pos[2] ):
        
            if pos[0] > 0.0:
                return om.MVector( 1, 0, 0 )
                
            return om.MVector( -1, 0, 0 )
            
        # Y Axis
        elif abs( pos[1] ) > abs( pos[0] ) and abs( pos[1] ) > abs( pos[2] ):
        
            if pos[1] > 0.0:
                return om.MVector( 0, 1, 0 )
                
            return om.MVector( 0, -1, 0 )
            
        # Z Axis
        elif abs( pos[2] ) > abs( pos[0] ) and abs( pos[2] ) > abs( pos[1] ):
        
            if pos[2] > 0.0:
                return om.MVector( 0, 0, 1 )
                
            return om.MVector( 0, 0, -1 )
            
        return default

        
    def snap_to_transform( snap_transform, snap_to_transform ):
        """
        Function to snap a transform to another transform based on position and orientation
        
        Args:
            snap_transform (pynode) : the object to snap
            snap_to_transform (pynode) : the object to snap to
            
        Returns:
            None
            
        """
        
        snap_transform.setTranslation( snap_to_transform.getTranslation( worldSpace=True ), 
                                       worldSpace=True )
                                                      
        snap_transform.setRotation( snap_to_transform.getRotation( worldSpace=True ), 
                                    worldSpace=True )

    # ensure pynode
    root_joint = pm.PyNode( root_joint )
    
    setup_dict = { 'root_joint' : root_joint,
                   'child_joint' : first_or_default( root_joint.getChildren( type='joint' ) ),
                   'parent' : root_joint.getParent() }

    # create pose reader attributes on root_joint
    for attr in [ 'Bend', 'Twist', 'Side' ]:
        setup_dict['root_joint'].addAttr( 'psr_' + attr.lower(), 
                                          attributeType='float', 
                                          niceName='PSR ' + attr, 
                                          keyable=True )
        attr_name_list = [ setup_dict['root_joint'].name(), '.psr_', attr.lower() ]
        setup_dict[ 'attr_psr_' + attr.lower() ] = pm.PyNode( ''.join( attr_name_list ) )
    
    # create organizing groups
    setup_dict[ 'psr_main_grp' ] = pm.group( name=setup_dict['root_joint'].name() + '_psrMain_GRP', 
                                             empty=True )
    setup_dict[ 'psr_target_grp' ] = pm.group( name=setup_dict['root_joint'].name() + '_psrTarget_GRP',
                                               empty=True )
    setup_dict[ 'psr_twist_grp' ] = pm.group( name=setup_dict['root_joint'].name() + '_psrTwist_GRP',
                                              empty=True )

    # create locators
    for loc in [ 'psrMain', 'psrMainTarget', 'psrMainUp', 'psrTwist', 'psrTwistTarget', 'psrTwistUp' ]:
        loc_name_list = [ setup_dict['root_joint'].name(), '_', loc, '_LOC' ]
        setup_dict[ loc ] = pm.spaceLocator( name= ''.join( loc_name_list ) )
        setup_dict[ loc ].setParent( setup_dict['psr_main_grp'] )

    # target locators parent under the target group, which is driven by the root_joint
    # the main grp is parented under the root_joint parent to maintain aiming without
    # taking in extra transforms from the root_joint or it's children
    [ setup_dict[ item ].setParent( setup_dict[ 'psr_target_grp' ] ) for item in [ 'psrMainTarget', 'psrTwistTarget' ] ]
    [ setup_dict[ item ].setParent( setup_dict[ 'psr_main_grp' ] ) for item in [ 'psr_target_grp', 'psr_twist_grp' ] ]
    [ setup_dict[ item ].setParent( setup_dict[ 'psr_twist_grp' ] ) for item in [ 'psrTwist', 'psrTwistUp' ] ]
    
    if setup_dict[ 'parent' ]:
        setup_dict[ 'psr_main_grp' ].setParent( setup_dict[ 'parent' ] )
    
    # align main group to the selected root joint
    snap_to_transform( setup_dict[ 'psr_main_grp' ], setup_dict[ 'root_joint' ] )

    draw_axis = get_bone_draw_axis( setup_dict['root_joint'] )
    child_trans = setup_dict[ 'child_joint' ].getAttr( 't' )
    child_offset = om.MVector( child_trans[0], child_trans[1], child_trans[2] )

    # X draw axis
    if draw_axis == om.MVector( 1, 0, 0 ) or draw_axis == om.MVector( -1, 0, 0 ): 
        main_up_offset = om.MVector( child_offset.x, child_offset.x, 0 )
        main_up_vector = om.MVector( 0, -1, 0 )
        twist_target_offset = om.MVector( 0, child_offset.x, 0 )
        setup_dict[ 'twist_driver_rot' ] = [ '.rotateY', '.rotateX', '.rotateZ' ]
    
    # Y draw axis
    elif draw_axis == om.MVector( 0, 1, 0 ) or draw_axis == om.MVector( 0, -1, 0 ): 
        main_up_offset = om.MVector( 0, child_offset.y, child_offset.y )
        main_up_vector = om.MVector( 0, 0, -1 )
        twist_target_offset = om.MVector( 0, child_offset.y, 0 )
        setup_dict[ 'twist_driver_rot' ] = [ '.rotateZ', '.rotateY', '.rotateX' ]
        
    # Z draw axis
    else: 
        main_up_offset = om.MVector( child_offset.z, 0, child_offset.z )
        main_up_vector = om.MVector( -1, 0, 0 )
        twist_target_offset = om.MVector( 0, 0, child_offset.z )
        setup_dict[ 'twist_driver_rot' ] = [ '.rotateX', '.rotateY', '.rotateY' ]
        
    setup_dict[ 'psrMainTarget' ].setTranslation( child_offset * 0.5, 
                                                  localSpace=True, 
                                                  relative=True )
    for loc in ['psrMain', 'psrTwistUp']:                                                  
        setup_dict[ loc ].setTranslation( child_offset * -1.0, 
                                          localSpace=True, 
                                          relative=True )

    setup_dict[ 'psrTwistTarget' ].setTranslation( twist_target_offset * -1.0, 
                                                   localSpace=True, 
                                                   relative=True )
                                              
    setup_dict[ 'psrMainUp' ].setTranslation( main_up_offset * -1.0, 
                                              localSpace=True, 
                                              relative=True )
                                              
    setup_dict[ 'psrMainAC' ] = pm.aimConstraint( setup_dict[ 'psrMainTarget' ], 
                                                  setup_dict[ 'psrMain' ], 
                                                  maintainOffset=True, 
                                                  aimVector=[ draw_axis.x, draw_axis.y, draw_axis.z ],
                                                  upVector=[ main_up_vector.x, main_up_vector.y, main_up_vector.z ],
                                                  worldUpType='objectrotation',
                                                  worldUpObject=setup_dict[ 'psrMainUp' ].name(),
                                                  weight=1.0 )
                                                  
    setup_dict[ 'psrTwistAC' ] = pm.aimConstraint( setup_dict[ 'psrTwistTarget' ], 
                                                   setup_dict[ 'psrTwist' ], 
                                                   maintainOffset=True, 
                                                   aimVector=[ main_up_vector.x, main_up_vector.y, main_up_vector.z ],
                                                   upVector=[ draw_axis.x * -1, draw_axis.y * -1, draw_axis.z * -1 ],
                                                   worldUpType='objectrotation',
                                                   worldUpObject=setup_dict[ 'psrTwistUp' ].name(),
                                                   weight=1.0 )
                                                          
    setup_dict[ 'psr_target_grp' ].setParent( setup_dict[ 'root_joint' ] )

    # the bend and side rotations of main psr locator drives the twist psr grp
    # this allows the child twist locator (under the twist psr grp) to maintain 
    # an accurate twist rotation only
    pm.connectAttr( setup_dict['psrMain'] + setup_dict['twist_driver_rot'][0], 
                    setup_dict[ 'psr_twist_grp'] + setup_dict['twist_driver_rot'][0], 
                    force=True )
    pm.connectAttr( setup_dict['psrMain'] + setup_dict['twist_driver_rot'][2], 
                    setup_dict[ 'psr_twist_grp'] + setup_dict['twist_driver_rot'][2],
                    force=True )
                            
    # connect final calculations to the custom attributes on the root_joint
    # these can be used for various setups that are driven from the pose reader
    pm.connectAttr( setup_dict['psrMain'] + setup_dict['twist_driver_rot'][0], 
                    setup_dict[ 'attr_psr_bend' ],
                    force=True )
    pm.connectAttr( setup_dict['psrMain'] + setup_dict['twist_driver_rot'][2], 
                    setup_dict[ 'attr_psr_side' ],
                    force=True )
    pm.connectAttr( setup_dict['psrTwist'] + setup_dict['twist_driver_rot'][1], 
                    setup_dict[ 'attr_psr_twist' ],
                    force=True )


selection = pm.ls( selection=True )

if selection:
    simple_pose_reader( selection[0] )

Sunday, July 3, 2016

Maya Picture in Picture Tool (PiP) Development Part 2

I haven't had too much time to devote to getting PiP fully complete in a while - I am to a point now where I would like to put up a beta download and see what kind of feedback I get for improvements.

pip_tool.zip


Just unzip the pip_tool folder to your Maya python path (example, your documents/maya/scripts/ folder).  The code to launch the tool, from a Python tab in the Maya script Editor...


1
2
import pip_tool.pip as PiP
PiP.jbPiP_UI()


This can be dragged to a shelf for later use.


I recently posted about my exploration with unit tests, I wanted to also post some of my early work with unit testing for PiP.  I feel like most folks who are early on in their learning path with unit tests would benefit from seeing examples of what kinds of things to test for.  This isn't my most current test library for PiP but it should get the point across...



  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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"""
Jason Breneman

unittest_pip.py

This file contains the unit test library for the Picture in Picture tool.  Some unit tests
do not pass specifically due to launching Maya through a standalone session.

"""

# maya libraries
import maya.standalone
import maya.cmds as cmds

try:
    maya.standalone.initialize()
    
except:
    pass

# python libraries
import unittest
import os
import uuid
import shutil
import logging

logging.basicConfig( level=logging.INFO )
logger = logging.getLogger( __name__ )
logger.info( "PiP Unit Test starting...\n" )

MAYA_VERSION = cmds.about( version=True )
MAYA_APP_DIR = os.environ["MAYA_APP_DIR"]
ROOT_DEV_DIR = sys.path[0].replace( "\\", "/" ).replace( "/pip_tool/tests", "" )
ROOT_TOOL_PATH = MAYA_APP_DIR + "/scripts"

logger.info( "'Maya Version' : " + MAYA_VERSION  + "\n" )
logger.info( "'MAYA_APP_DIR' : " + MAYA_APP_DIR + "\n" )


class TestLibrary( unittest.TestCase ):
    """
    Test Library
    
    unit test class for Picture in Picture tool
    
    """

    
    def setUp( self ):
        """
        Method to provide any standard setup instructions for a test case function
        
        Args:
            self (object) : reference to the TestLibrary class instance
            
        Returns:
            None
            
        """
        
        pass
        
        
    def tearDown( self ):
        """
        Method to provide any standard tear down instructions for a test case function
        
        Args:
            self (object) : reference to the TestLibrary class instance
            
        Returns:
            None
            
        """
        
        pass
    
    
    def test_files_installed_check( self ):
        """
        Test method for copying files from the development environment to the 
        MAYA_APP_DIR path
        
        Args:
            self (object) : reference to the TestLibrary class instance
            
        Returns:
            None
            
        """
        
        logger.info( "*** TestCase *** Remove existing install, and apply a fresh install\n" )
        
        if os.path.exists( ROOT_TOOL_PATH + "/pip_tool" ):
            shutil.rmtree( ROOT_TOOL_PATH + "/pip_tool" )

        shutil.copytree( ROOT_DEV_DIR + "/pip_tool", ROOT_TOOL_PATH + "/pip_tool" )
        
        
    def test_import_check( self ):
        """
        Test method to check for a successful module import
        
        Args:
            self (object) : reference to the TestLibrary class instance
            
        Returns:
            None
            
        """
        logger.info( "*** TestCase *** Import pip_tool module\n" )
        
        import_success = False
        error_message = "TestCase Failure, PiP module did not import correctly.\n"
        
        try:
            import pip_tool.pip as PiP
            import_success = True
            
        except:
            pass
            
        self.assertTrue( import_success, error_message )

        
        
    def test_pip_instance( self ):
        """
        Test method to check a successful instantiation of PiP
        
        Args:
            self (object) : reference to the TestLibrary class instance
            
        Returns:
            None
            
        """
        
        logger.info( "*** TestCase *** Load a PiP instance\n" )
        
        import pip_tool.pip as PiP
        reload( PiP )
        loadout = PiP.jbPiP_UI()
        
        loadout_exists = cmds.modelEditor( loadout.name_instance + "__ME", 
                                           query=True, 
                                           exists=True )
        error_message = "TestCase Failure, PiP loadout does not exist.  Maya UI required for successful TestCase\n"
        self.assertTrue( loadout_exists, error_message )
        
        
    def test_pip_callback_newscene( self ):
        """
        Test method to check if the new scene callback gets properly deleted when
        a new scene event occurs
        
        Args:
            self (object) : reference to the TestLibrary class instance
            
        Returns:
            None
            
        """
        
        logger.info( "*** TestCase *** Delete PiP on New Scene Callback\n" )
        
        import pip_tool.pip as PiP
        reload( PiP )
        loadout = PiP.jbPiP_UI()
                
        cmds.file( new=True, force=True )
        
        self.assertEquals( loadout.newscene_callbackid, None )


# if starting from a command prompt, load unit test class instance
if __name__ == "__main__":
    unittest.main()

From these examples, you can see I started with testing the results of early development stages such as moving files from my development path to the Maya script path, or just a simple import check, UI loading checks, etc.  Kind of a neat thing to note here is my unit tests are being ran from the windows command line to launch a standalone Maya session (no UI).  The problem with this is PiP is a UI based tool, so as I kept developing more tests I realized I could run some tests in command line but to run some specific tests I would need a normal Maya session.

The current beta release seems to have a few graphical glitches on certain computer setups - that and a few other "nice to haves" are the known issues that I am wanting to polish before I consider it complete. Anyway, that's all for now!