├── .gitignore ├── README.md ├── __init__.py ├── const.py ├── gui.py └── methods.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skonverter v 0.5 2 | Simple skin converter written for Autodesk Maya. Mostly written for my own learning/experimentation and 3 | getting back into Maya after working primarily in MotionBuilder for a year or so. 4 | 5 | [Read more on my Dev Blog here!]( http://evancox.net/skonverter-what/ ) If you're interested in contributing, please feel free to let me know or just start tinkering on your own and lets merge! :) 6 | 7 | ## The meat - Get it running 8 | * Download/move/install the Skonverter package into your Maya python path. 9 | * Call the following lines of code for a quick example, or check out the example file provided in the package. 10 | ```python 11 | import skonverter 12 | skonverter.run() # Loads with GUI 13 | ``` 14 | ```python 15 | # Template for running the tool headless 16 | import skonverter 17 | data, message = skonverter.run_skin_calculation( transform, root_bone, tolerance = -1, file_path ) # Returns data dictionary 18 | #################### 19 | ## Your code here ## 20 | #################### 21 | result, message = skonverter.run_skin_application( transform, data, file_path ) 22 | 23 | # Note: file_path kwargs above are optional and only necessary if not passing in a data obj. 24 | ``` 25 | 26 | ## Known Limitations 27 | * Does not automatically create a skinCluster 28 | * Does not allow for existing constraints on the bind skeleton. Assumes all bones are free to move and have no dependencies aside from their children 29 | 30 | ## The Goods 31 | * Pretty fast for a purely python converter 32 | * Requires no existing range of motion animation as it does all the necessary transform manipulation itself. 33 | * Built from the command line up, so it's ready to be implemented into your automagic pipelines. No UI hackery needed. 34 | 35 | ## Development - Things to do 36 | * The user interface could be more intuitive, but as it stands, it works and is usable. 37 | * Division in the weight calc and normalization methods lead to some rounding issues. Maya doesn't seem to mind, however I would really like to remove them. 38 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | File holds initialization logic for the simple skin converter. 3 | 4 | * Author : Evan Cox coxevan90@gmail.com 5 | ''' 6 | # Skonverter module imports 7 | import gui 8 | import methods 9 | import const 10 | 11 | # Maya lib imports 12 | import maya.cmds 13 | 14 | # Python std lib imports 15 | import os 16 | 17 | if const.DEBUG: 18 | reload( gui ) 19 | reload( methods ) 20 | 21 | 22 | def run( ): 23 | """ 24 | Runs Skonverter UI. 25 | """ 26 | skonverter_window = gui.Skonverter_Interface( ) 27 | skonverter_window.show( ) 28 | 29 | 30 | def run_skin_calculation( transform, root_bone, tolerance = -1, file_path = '' ): 31 | """ 32 | Main method call for the skin converter to calculate skin weights from the transform's shape node. 33 | 34 | Returns [ data, message ] 35 | Data : skin calculation data 36 | Message : message about the results. If data == False, message holds more info. 37 | """ 38 | data, message = methods.determine_weighting( transform, root_bone, tolerance = tolerance ) 39 | 40 | # Data is valid, so if a file path was provided too, save the data out. 41 | if file_path and data: 42 | methods.save_json( file_path, data ) 43 | 44 | return data, message 45 | 46 | 47 | def run_skin_application( transform = None, data = None, file_path = '' ): 48 | """ 49 | Main method call for the skin converter to apply skin weights to the transform. 50 | 51 | Returns [ result, message ] 52 | Result : Whether or not the application was successful 53 | Message : message about the results. If data == False, message holds more info. 54 | """ 55 | result, data = methods.determine_data_to_source( data, file_path ) 56 | if not result: 57 | maya.cmds.warning( data ) 58 | return result, data 59 | 60 | result, message = methods.apply_weighting( transform, data = data ) 61 | if not result: 62 | maya.cmds.warning( message ) 63 | 64 | return result, message 65 | 66 | 67 | if __name__ not in '__main__': 68 | print "*** Loaded Skonverter ***" -------------------------------------------------------------------------------- /const.py: -------------------------------------------------------------------------------- 1 | """ 2 | File contains constant values for the Skonverter tool. 3 | """ 4 | 5 | VERSION = 0.5 6 | 7 | BONE_DELTA = 2 # The amount of distance each bone should be moved to determine weighting. 8 | FAILURE_THRESHOLD = 0 # How many bones can fail before user is prompted with warning 9 | 10 | # Slows down the processes a good bit, but displays some useful information in regards to actual weighting without 11 | # having to save out the files and parse. Lots of printing. Probably needs to be handled better/spit out easier to read info, but for 12 | # the purposes of the tool, it works as it is now. 13 | DEBUG = False 14 | 15 | FILE_PREFERENCE = True # We prefer to use the file passed in if both data and filepath are passed in during initialzation of the script. 16 | 17 | # Should the weighting be normalized? Really, this shouldn't be ever changed, but if the situation arises, this is here. 18 | NORMALIZE = True 19 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | ''' 2 | File contains methods related to gui creation for the simple skin converter. 3 | 4 | Not intended for use outside of the run method. 5 | 6 | * Author : Evan Cox coxevan90@gmail.com 7 | ''' 8 | 9 | # Skonverter module imports 10 | import methods 11 | import const 12 | 13 | # Maya std lib imports 14 | import pymel.core 15 | 16 | # UI Lib imports 17 | import PySide.QtGui 18 | 19 | # Python std lib imports 20 | import os 21 | 22 | 23 | 24 | class Skonverter_Interface( ): 25 | def __init__( self ): 26 | # UI Elements needed for update 27 | self.transform_field = None 28 | self.root_field = None 29 | 30 | # Kwargs for methods 31 | self.transform = None 32 | self.root_bone = None 33 | 34 | self.tolerance = -1 35 | 36 | # To be filled later. 37 | self.data = None 38 | 39 | def show( self ): 40 | """ 41 | Method handles the logic for the generation of the UI 42 | """ 43 | window_name = 'Skonverter' 44 | if pymel.core.window( window_name, exists=True ): 45 | pymel.core.deleteUI( window_name ) 46 | if const.DEBUG: 47 | print 'Refreshing UI' 48 | 49 | # Make the window. 50 | main_window = pymel.core.window( window_name, wh = ( 200, 200 ), title = 'Skonverter v{0}'.format( const.VERSION ))#, sizeable = False ) 51 | main_layout = pymel.core.columnLayout('main_layout', p = main_window ) 52 | 53 | button_layout = main_layout 54 | 55 | ## Make the transform/root_bone selection interface 56 | # ------ Transform obj ------ # 57 | transform_layout = pymel.core.horizontalLayout( 'transform_layout', p = main_layout ) 58 | transform_label = pymel.core.text( 'Transform: ', p = transform_layout ) 59 | self.transform_field = pymel.core.textField( 'transform_field', text = '...', p = transform_layout ) 60 | self.transform_field.changeCommand( self.on_transform_field_change ) 61 | 62 | transform_button_command = pymel.core.Callback( self.fill_field, self.transform_field ) 63 | transform_button = pymel.core.button( 'transform_button', label = '<<<', p = transform_layout, c = transform_button_command ) 64 | 65 | # ----- Root Bone ------ # 66 | root_layout = pymel.core.horizontalLayout( 'root_layout', p = main_layout ) 67 | root_label = pymel.core.text( 'Root Bone: ', p = root_layout ) 68 | self.root_field = pymel.core.textField( 'root_field', text = '...', p = root_layout ) 69 | self.root_field.changeCommand( self.on_root_field_change ) 70 | 71 | root_button_command = pymel.core.Callback( self.fill_field, self.root_field ) 72 | root_button = pymel.core.button( 'root_button', label = '<<<', p = root_layout, c = root_button_command ) 73 | 74 | # ------ Tolerance Field -------# 75 | tolerance_layout = pymel.core.horizontalLayout( 'tolerance_layout', p = main_layout ) 76 | tolerance_label = pymel.core.text( 'Tolerance: ', p = tolerance_layout ) 77 | self.tolerance_field = pymel.core.textField( 'tolerance_field', text = '-1', p = tolerance_layout ) 78 | self.tolerance_field.changeCommand( self.on_tolerance_field_change ) 79 | 80 | # ------- File path field -------# 81 | file_path_layout = pymel.core.horizontalLayout( 'file_path_layout', p = main_layout ) 82 | file_path_label = pymel.core.text( 'Data Path: ', p = file_path_layout ) 83 | self.file_path_field = pymel.core.textField( 'tolerance_field', text = 'None', p = file_path_layout ) 84 | self.file_path_field.changeCommand( self.on_file_path_field_change ) 85 | 86 | path_button_command = self.get_file_path 87 | path_button = pymel.core.button( 'file_path_button', label = '<<<', p = file_path_layout, c = path_button_command ) 88 | 89 | # Redistribute the layouts 90 | horizontal_layouts = [ transform_layout, root_layout, tolerance_layout, file_path_layout ] 91 | for layout in horizontal_layouts: 92 | layout.redistribute( ) 93 | 94 | # Make the Calculate button 95 | calculate_button_command = pymel.core.Callback( self.run_calculate_weighting_command ) 96 | calculate_button = pymel.core.button('calc_button', l='Calculate Weighting', p = button_layout, c = calculate_button_command ) 97 | 98 | # Make the Apply button 99 | apply_button_command = pymel.core.Callback( self.run_apply_weighting_command ) 100 | apply_button = pymel.core.button('apply_button', l='Apply Weighting', p = button_layout, c = apply_button_command ) 101 | 102 | main_window.show( ) 103 | 104 | ###################### 105 | ## Callback Methods ## 106 | ###################### 107 | 108 | def run_apply_weighting_command( self ): 109 | data = methods.load_json( self.file_path_field.getText( ) ) 110 | if not data: 111 | self.warning('Data is invalid') 112 | return False 113 | 114 | methods.apply_weighting( self.transform, data = data ) 115 | 116 | def run_calculate_weighting_command( self ): 117 | target_file_path, message = PySide.QtGui.QFileDialog( ).getSaveFileName( None, 'Save Location' ) 118 | data, message = methods.determine_weighting( self.transform, root_bone = self.root_bone, tolerance = self.tolerance ) 119 | if not data: 120 | self.warning( message ) 121 | self.file_path_field.setText( 'Data invalid' ) 122 | return False 123 | 124 | methods.save_json( target_file_path, data ) 125 | if os.path.exists( target_file_path ): 126 | self.file_path = target_file_path 127 | self.file_path_field.setText( target_file_path ) 128 | 129 | return True 130 | 131 | def fill_field( self, field ): 132 | # get the selection 133 | selection = pymel.core.selected( ) 134 | if not selection: 135 | self.warning( 'Nothing selected. Please select an object and re-click the <<< button.' ) 136 | return 137 | 138 | field.setText( str( selection[ 0 ].name( ) ) ) 139 | 140 | field_obj_path = field.getFullPathName( ).lower( ) 141 | # TODO: Hard coded value set here. Fix this in another update, but for now, this is all we need to be getting/setting anyway. 142 | if 'transform' in field_obj_path: 143 | self.transform = selection[ 0 ] 144 | elif 'root' in field_obj_path: 145 | self.root_bone = selection[ 0 ] 146 | else: 147 | self.warning('Was expecting transform or root. Obj path : {0}'.format( field_obj_path ) ) 148 | return True 149 | 150 | def on_transform_field_change( self, *args ): 151 | try: 152 | self.transform = pymel.core.PyNode( self.transform_field.getText( ) ) 153 | except pymel.core.MayaNodeError as excep: 154 | self.warning( excep ) 155 | return True 156 | 157 | def on_root_field_change( self, *args ): 158 | try: 159 | self.root_bone = pymel.core.PyNode( self.root_field.getText( ) ) 160 | except pymel.core.MayaNodeError as excep: 161 | self.warning( excep ) 162 | return True 163 | 164 | def on_tolerance_field_change( self, *args ): 165 | self.tolerance = float( self.tolerance_field.getText( ) ) 166 | return True 167 | 168 | def on_file_path_field_change( self, *args ): 169 | self.file_path = self.file_path_field.getText( ) 170 | return True 171 | 172 | def get_file_path( self, *args ): 173 | file_path, message = PySide.QtGui.QFileDialog( ).getOpenFileName( None, 'Open file' ) 174 | 175 | # IF the path does not exist, we reset the UI 176 | if not os.path.exists( file_path ): 177 | self.warning( 'filepath not valid' ) 178 | self.file_path_field.setText( 'Invalid file path' ) 179 | self.data = None 180 | return None 181 | 182 | self.file_path = file_path 183 | self.file_path_field.setText( self.file_path ) 184 | 185 | return self.file_path 186 | 187 | 188 | ##################### 189 | ## Utility Methods ## 190 | ##################### 191 | 192 | @staticmethod 193 | def warning( message ): 194 | if len( message ) > 160: 195 | print message 196 | pymel.core.warning( 'Skonverter UI | Check console for more information' ) 197 | return True 198 | 199 | pymel.core.warning( 'Skonverter UI | {0}'.format( message ) ) 200 | return True -------------------------------------------------------------------------------- /methods.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the core methods of the skin converter. 3 | 4 | Author : Evan Cox, coxevan90@gmail.com 5 | """ 6 | 7 | # Maya sdk imports 8 | import maya.cmds 9 | import pymel.core 10 | import maya.api.OpenMaya as om2 11 | 12 | # Python std lib imports 13 | import pprint 14 | import json 15 | import os 16 | 17 | # Skonverter module imports 18 | import const 19 | 20 | 21 | BONE_DELTA = const.BONE_DELTA # The amount of distance each bone should be moved to determine weighting. 22 | FAILURE_THRESHOLD = const.FAILURE_THRESHOLD # How many bones can fail before user is prompted with warning 23 | DEBUG = const.DEBUG 24 | NORMALIZE = const.NORMALIZE 25 | 26 | 27 | ################## 28 | ## Main methods ## 29 | ################## 30 | 31 | 32 | def determine_weighting( transform, root_bone = None, tolerance = -1 ): 33 | """ 34 | Main method which holds logic for determining the weighting for a given transform. 35 | """ 36 | if not transform or not root_bone: 37 | return False, 'One or both objects were not found. Transform : {0} | Root_bone : {1}'.format( transform, root_bone ) 38 | 39 | try: 40 | # If it's not a pymel node, we'll make it one for the early stages of data collection about objects, not verts. 41 | if not isinstance( transform, pymel.core.nodetypes.DagNode ): 42 | transform = pymel.core.PyNode( transform ) 43 | if not isinstance( root_bone, pymel.core.nodetypes.DagNode ): 44 | root_bone = pymel.core.PyNode( root_bone ) 45 | except pymel.core.MayaNodeError as exception: 46 | return False, '{0}'.format( exception ) 47 | 48 | rest_vert_positions = query_vertex_positions( transform ) 49 | ordered_bone_list = get_ordered_bone_list( root_bone, [ root_bone ] ) 50 | bone_vert_association = { } 51 | 52 | # Create a list of tuples with each bone and its corresponding locator. We then make an expression locking them. Animation/constraints cannot be on the skeleton. 53 | #bone_and_locators = create_locators( ordered_bone_list ) 54 | #create_expression( bone_and_locators ) 55 | 56 | weight_data = { } # Final data to be sent out, Dictionary to associate verts with bones and weights for application to the skin cluster. 57 | 58 | for index, bone in enumerate( ordered_bone_list ): 59 | bone_name = bone.name( ) 60 | 61 | # Do our movement and calculation 62 | print 'Skin Weight Calculation : Processing {0}/{1} | {2}'.format( index + 1, len( ordered_bone_list ), bone_name ) 63 | 64 | child_bones = [ ] 65 | # Move the children to counter the movement of the parent. 66 | _children = bone.getChildren( ) 67 | for _child in _children: 68 | _child_bone_name = _child.name( ) 69 | # Get the starting translation for the child, counter translate the children 70 | _child_starting_translation = maya.cmds.xform( _child_bone_name, query = True, translation = True, a = True, ws = True ) 71 | _child_new_translation = add_vector3s( _child_starting_translation, [ 0, -BONE_DELTA, 0 ] ) 72 | maya.cmds.xform( _child_bone_name, translation = _child_new_translation, a = True, ws = True ) 73 | 74 | # Store the childs bone name and the starting position 75 | child_bones.append( ( _child_bone_name, _child_starting_translation ) ) 76 | 77 | # Get the starting translation so we can reset it back after we move it a bit 78 | starting_translation = maya.cmds.xform( bone_name, query = True, translation = True, a = True, worldSpace = True ) 79 | new_translation = add_vector3s( starting_translation, [ 0, BONE_DELTA, 0 ] ) 80 | maya.cmds.xform( bone_name, translation = new_translation, a = True, worldSpace = True ) 81 | 82 | # Get the new vert positions and calculate the weights for the specific bone 83 | new_vert_positions = query_vertex_positions( transform ) 84 | new_vert_weights = calculate_vertex_weights( new_vert_positions, rest_vert_positions ) 85 | 86 | # Reset the bone 87 | maya.cmds.xform( bone_name, translation = starting_translation, a = True, worldSpace = True ) 88 | 89 | # Save the weighting to the association dictionary 90 | bone_vert_association[ bone_name ] = new_vert_weights 91 | 92 | # Reset the children to their starting positions 93 | if child_bones: 94 | for _child, vector in child_bones: 95 | maya.cmds.xform( _child, translation = vector, a = True, ws = True ) 96 | 97 | # Get the vert id and it's associated weight for this bone 98 | for vert_id, weight in bone_vert_association[ bone_name ].iteritems(): 99 | # If the weight is below the tolerance, we skip it 100 | if weight <= tolerance: 101 | continue 102 | 103 | vert_id_string = str( vert_id ) 104 | if vert_id_string not in weight_data.keys( ): 105 | weight_data[ vert_id_string ] = [ ] 106 | weight_data[ vert_id_string ].append( ( bone_name, weight ) ) 107 | 108 | print 'Skin Weight calculation complete | Organizing data into skinPercent readable format' 109 | bone_name_list = [ bone.name( ) for bone in ordered_bone_list ] # Make this list so we don't have to keep pymel objs around for the weight application. We only need the names 110 | 111 | if NORMALIZE: 112 | messages = [ ] 113 | print 'Skin Weight organization complete | Normalizing weight data' 114 | for vert_id_string in weight_data.keys( ): 115 | weight_list = weight_data[ vert_id_string ] 116 | normalized_weight_list, message = normalize_vertex_weighting( weight_list ) 117 | 118 | weight_data[ vert_id_string ] = normalized_weight_list 119 | messages.append( message ) 120 | 121 | # If we failed at all during the normalization step, we want to know. 122 | if False in messages: 123 | failures = [ result for result in messages if not result ] 124 | print 'Even after normalizing: {0} still are not 1.0'.format( len( failures ) ) 125 | 126 | data = consolidate_data( weight_data, bone_name_list ) 127 | 128 | print 'Skin Calculation : Complete' 129 | # Clean up the scene and collect some last minute info. 130 | return data, 'Success' 131 | 132 | 133 | def apply_weighting( transform, skincluster = None, data = None ): 134 | """ 135 | Main method which holds logic for applying weighting data 136 | """ 137 | # If it's not a pymel node, we'll make it one for the early stages of data collection about objects, not verts. 138 | try: 139 | if not isinstance( transform, pymel.core.nodetypes.DagNode ): 140 | transform = pymel.core.PyNode( transform ) 141 | except pymel.core.MayaNodeError as exception : 142 | return False, '{0}'.format( exception ) 143 | 144 | if not skincluster: 145 | print 'Skin Weight Application : Getting the skin cluster' 146 | # Get the objects history and then grab the skincluster from it 147 | history = maya.cmds.listHistory( transform.name( ) ) 148 | skinclusters = maya.cmds.ls( history, type = 'skinCluster' ) 149 | 150 | # If we did not get a skincluster, we need to get out 151 | if not skinclusters: 152 | return False, 'No skin cluster found, apply one to the mesh passed in' 153 | skincluster = skinclusters[ 0 ] 154 | py_cluster = pymel.core.PyNode( skincluster ) 155 | 156 | # Unpack the data from the file 157 | weight_data = data[ 'weight' ] 158 | ordered_bone_list = data[ 'order' ] 159 | 160 | bone_failure = [ ] # List to catch our failures :( 161 | print 'Skin Weight Application : Applying vertex weighting' 162 | 163 | # Logging variable declaration/initialization 164 | notify_total_weights = False 165 | vertices_evaluated = [ ] 166 | total_weights = [ ] 167 | 168 | # Remove any normalizing that maya will try to do and remove all the weighting on the current skincluster. 169 | py_cluster.setNormalizeWeights( 0 ) 170 | remove_all_weighting( skincluster, transform, ordered_bone_list, weight_data.keys( ) ) 171 | 172 | # For each vert, we'll get the list of ( bone_name, weight ) and apply it to the vertex 173 | for vert_id in weight_data.keys( ): 174 | weight_list = weight_data[ vert_id ] 175 | 176 | if NORMALIZE: 177 | weight_list, message = normalize_vertex_weighting( weight_list ) 178 | if not message: 179 | notify_total_weights = True 180 | 181 | if DEBUG: 182 | total_weight = calculate_total_vertex_weight( weight_list ) 183 | total_weights.append( calculate_total_vertex_weight( weight_list ) ) 184 | 185 | # We apply the weighting to the skin cluster 186 | try: 187 | maya.cmds.skinPercent( skincluster, transform + '.vtx[{0}]'.format( vert_id ), tv = weight_data[ vert_id ] ) 188 | vertices_evaluated.append( vert_id ) 189 | 190 | # If we failed, we catch it and save it to the failure list to notify the user later 191 | except RuntimeError as excep: 192 | failure_string = 'Bone Failure: {0}'.format( excep ) 193 | if failure_string not in bone_failure: 194 | bone_failure.append( failure_string ) 195 | 196 | # Only notify the user of failure if within this level 197 | if len( bone_failure ) > FAILURE_THRESHOLD: 198 | maya.cmds.warning( 'Failed: See console for more info' ) 199 | for failure_string in bone_failure: 200 | print failure_string 201 | 202 | if DEBUG: 203 | non_one_count = 0 204 | for weight in total_weights: 205 | if weight != 1.0: 206 | non_one_count += 1 207 | print 'Verts not exactly normalized to 1.0 :', non_one_count 208 | 209 | if notify_total_weights: 210 | pprint.pprint( total_weights ) 211 | print 'Skin Weight Application : Complete' 212 | 213 | return True, 'Success' 214 | 215 | 216 | ##################### 217 | ## Utility methods ## 218 | ##################### 219 | 220 | def get_ordered_bone_list( bone, bone_list = None ): 221 | ''' 222 | Recursive search for children of the and places them in the . 223 | ''' 224 | # We didn't get a bone_list passed in, so we'll just make one to return later 225 | if bone_list == None: 226 | bone_list = [ ] 227 | 228 | for child in bone.getChildren( ): 229 | # If the child we got isn't a Joint, we're going to pass on it. 230 | if not isinstance( child, pymel.core.nodetypes.Joint ): 231 | continue 232 | 233 | # Add the child to the list and run the method again 234 | bone_list.append( child ) 235 | get_ordered_bone_list( child, bone_list ) 236 | 237 | return bone_list 238 | 239 | 240 | def query_vertex_positions( transform ): 241 | """ 242 | Queries all vertex positions from a given transform 243 | 244 | Using Maya Python API 2.0 245 | """ 246 | 247 | # Create a selectionList and add our transform to it 248 | sel_list = om2.MSelectionList() 249 | sel_list.add( transform.name() ) 250 | 251 | # Get the dag path of the first item in the selection list 252 | sel_obj = sel_list.getDagPath( 0 ) 253 | 254 | # create a Mesh functionset from our dag object 255 | mfn_object = om2.MFnMesh( sel_obj ) 256 | 257 | return mfn_object.getPoints() 258 | 259 | 260 | def calculate_vertex_distance( vector1, vector2 ): 261 | """ 262 | Calculate the absolute distance between two vectors. 263 | """ 264 | #x = ( round_float( vector1[ 0 ] ) - round_float( vector2[ 0 ] ) ) ** 2 265 | #y = ( round_float( vector1[ 1 ] ) - round_float( vector2[ 1 ] ) ) ** 2 266 | #z = ( round_float( vector1[ 2 ] ) - round_float( vector2[ 2 ] ) ) ** 2 267 | 268 | difference_x = vector1[ 0 ] - vector2[ 0 ] 269 | difference_y = vector1[ 1 ] - vector2[ 1 ] 270 | difference_z = vector1[ 2 ] - vector2[ 2 ] 271 | 272 | difference_x_sqr = difference_x ** 2 273 | difference_y_sqr = difference_y ** 2 274 | difference_z_sqr = difference_z ** 2 275 | 276 | sum_of_deltas = difference_x_sqr + difference_y_sqr + difference_z_sqr 277 | 278 | distance = sum_of_deltas ** 0.5 279 | 280 | return distance 281 | 282 | 283 | def round_float( number ): 284 | """ 285 | Rounds a float by converting it to a string, which can limit the amount of digits past the decimal. Then converts it back into a float. 286 | """ 287 | rounded = float( '{0:.3f}'.format( number ) ) 288 | 289 | return rounded 290 | 291 | 292 | def calculate_vertex_weights( new_positions, rest_positions ): 293 | """ 294 | Here we do some really simple math to determine the weights we want to associate with this bone. The weights being spit out here 295 | will not be perfect as it's the amount of movement th1at this bone causes in the verts ( between 0 and 1 ). 296 | 297 | Why won't it be perfect? The hierarchy says the root bone will move all other bones below, etc. That means, even if the root bone has no weighting to it 298 | from many of the vertices, it'll stay say all verts will be 1.0 because the root bone moves each bone. We don't worry about this though, as the application 299 | process goes from the root bone to the tips, meaning that weight is applied to the root bone at first, and then slowly chipped away by other bones, similar 300 | to the way some people choose to paint their weights in a normal situation. 301 | """ 302 | # We make a dictionary to store weights in 303 | list_of_weights = {} 304 | 305 | # Get list of indexes to get distance on 306 | index_list = [(i) for i, j in enumerate(zip(new_positions, rest_positions)) if j[0] != j[1]] 307 | 308 | for index in index_list: 309 | # Calculate the vertex distance and divide by the bone's movement (Default is 1 unit) 310 | weight = calculate_vertex_distance( new_positions[index], rest_positions[index] ) / BONE_DELTA 311 | 312 | # Round the weighting. 313 | weight = round_float( weight ) 314 | 315 | # TODO: Probably need more checks here to ensure that the weighting is valid 316 | list_of_weights[index] = weight 317 | 318 | return list_of_weights 319 | 320 | 321 | def add_vector3s( vector1, vector2 ): 322 | """ 323 | Adds two vector 3's together. 324 | """ 325 | x1, y1, z1 = vector1[ : ] 326 | x2, y2, z2 = vector2[ : ] 327 | 328 | x = x1 + x2 329 | y = y1 + y2 330 | z = z1 + z2 331 | 332 | return [ x, y, z ] 333 | 334 | 335 | def remove_all_weighting( skincluster, transform, bone_list, vert_list ): 336 | """ 337 | Removes all weighting from the given skin cluster. This method assumes the skincluster has it's normalization mode set to None 338 | """ 339 | # Make a weight list with each bone being set to 0.0 340 | zero_value_list = [ ( bone_name, 0.0 ) for bone_name in bone_list ] 341 | 342 | # For each vert in the vert_list passed in, we reset all the weighting to zero. This skin cluster should already be set to not normalize, so the weighting will be completely gone. 343 | for vert_id in vert_list: 344 | maya.cmds.skinPercent( skincluster, transform + '.vtx[{0}]'.format( vert_id ), tv = zero_value_list ) 345 | return True 346 | 347 | 348 | def normalize_vertex_weighting( weight_list ): 349 | """ 350 | Normalizes vertex weights if they are above or below 1.0 by dividing each individual weight value by the sum of all the weights associated with that vertex. 351 | 352 | weight_list is a list of tuples ( bone_name, weight ). 353 | """ 354 | total = calculate_total_vertex_weight( weight_list ) 355 | normalized_weight_list = [ ] 356 | for bone_name, weight in weight_list: 357 | # For each bone, divide its corresponding weight value by the total 358 | normalized_weight = weight / total 359 | 360 | # Make a new tuple and add it to the normalized_weight_list 361 | normalized_weight_tuple = ( bone_name, normalized_weight ) 362 | normalized_weight_list.append( normalized_weight_tuple ) 363 | 364 | # If we're in debug mode, we want to recalculate the total weight for these normalized weights. Lets check again to be sure they're fully normalized. 365 | if DEBUG: 366 | total = calculate_total_vertex_weight( normalized_weight_list ) 367 | if total != 1.0: 368 | print '{0}'.format( total ) 369 | return normalized_weight_list, False 370 | 371 | return normalized_weight_list, True 372 | 373 | 374 | def calculate_total_vertex_weight( weight_list ): 375 | """ 376 | Given a weightlist, this method will add up the weights. 377 | """ 378 | weight = 0 379 | for bone, bone_weight in weight_list: 380 | if bone_weight != 0: 381 | weight += bone_weight 382 | return weight 383 | 384 | ############################ 385 | ## JSON Writing & Loading ## 386 | ############################ 387 | 388 | def save_json( file_path, content ): 389 | """ 390 | Saves the content to the file_path in the json format 391 | """ 392 | with open( file_path, 'w' ) as data_file: 393 | json_data = json.dumps( content ) 394 | data_file.write( json_data ) 395 | return True 396 | 397 | 398 | def load_json( file_path ): 399 | """ 400 | Checks file path, loads json file, returns data 401 | """ 402 | if not os.path.exists( file_path ): 403 | return None 404 | 405 | with open( file_path, 'r' ) as data_file: 406 | data_from_file = data_file.readlines( )[0] 407 | json_data = json.loads( data_from_file ) 408 | 409 | return json_data 410 | 411 | ########################### 412 | ## Data handling methods ## 413 | ########################### 414 | 415 | def consolidate_data( weight_list, bone_list ): 416 | """ 417 | Creates the final data dictionary out of the two lists. 418 | 419 | Data format is pretty simple right now. Hopefully continues to be in the future. 420 | """ 421 | return { 'weight': weight_list, 'order': bone_list } 422 | 423 | def verify_data( data ): 424 | """ 425 | Checks the validity of the data. 426 | 427 | Very few checks right now as the data is in a very rigid state 428 | """ 429 | print 'Skonverter | Verifying data' 430 | 431 | if not isinstance( data, dict ): 432 | return False, 'Data must be a dictionary' 433 | 434 | if not 'order' in data.keys( ) or not 'weight' in data.keys( ): 435 | return False, 'Data either does not contain key "order" or key "weight"' 436 | 437 | # Check the order 438 | for bone_string in data['order']: 439 | if not isinstance( bone_string, basestring ): 440 | return False, 'Bone list must be strings' 441 | 442 | return True, 'Data is valid' 443 | 444 | def determine_data_to_source( data, file_path ): 445 | """ 446 | Handles the loading and verification of data. Determines whether or not to get the data from the disk or to use the data passed in. 447 | """ 448 | # Check to see if the file path and/or data is even valid. 449 | file_path_validity = os.path.exists( file_path ) 450 | data_validity, message = verify_data( data ) 451 | 452 | # If not neither 453 | if not file_path_validity and not data_validity: 454 | return False, 'No valid data or file passed in' 455 | 456 | # If not file 457 | if not file_path_validity: 458 | # The path is not valid. Verify the data and return 459 | return data_validity, data 460 | 461 | # If not data 462 | if not data_validity: 463 | # We load the data from the file 464 | result, data = load_data_from_file( file_path ) 465 | 466 | # If the file path is valid and the data is valid, check the const to determine preference. 467 | if data_validity and file_path_validity: 468 | if const.FILE_PREFERENCE: 469 | # We go with the file 470 | return load_data_from_file( file_path ) 471 | 472 | else: 473 | # We go with the data we were passed in. 474 | return data_validity, data 475 | 476 | 477 | def load_data_from_file( file_path ): 478 | # Load the data 479 | file_data = load_json( file_path ) 480 | 481 | # Verify it 482 | result, message = verify_data( file_data ) 483 | if result: 484 | return result, file_data 485 | else: 486 | return False, message --------------------------------------------------------------------------------