Saturday, January 23, 2016

Alembic Exporter Tool Source Code


  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
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
"""
BYU Alembic Exporter (Ram's Horn Version)

This script is intended to run from a shelf tool in Maya. When it is 
  run it provides a dialog box for the user to select the rigged 
  object they wish to export and then exports a separate Alembic file 
  for each of the rigged objects the user selects, preserving any 
  references made by internal parent contraints.

This script assumes that all rigs have had a custom BYU Alembic 
  Export attribute added to them at the transform node that is at the 
  root of all the geometry in the rig and all rigs have been 
  referenced into the scene through the Reference Editor. It also 
  assumes that the scene exists in the internal structure of the BYU
  pipeline. 

Example: A scene contains Andre and his jackhammer as rigged objects.
  The jackhammer's animation is constrained to Andre's wrist through
  a parent constraint. 
  
  When the shelf tool is clicked, the script displays a dialog box 
  with two items available to select: Andre and The Jackhammer. When 
  both are selected and the export button is clicked, the script 
  searches the selected rigs for their BYU Alembic Export Flag.
  
  The script then searches for all the parent constraints in the rig.
  If the namespace of the parent constraint doesn't match the 
  namespace of its target, the script adds the target to the list of
  objects to be exported in the same Alembic file. In this case, the
  parent constraint on the jackhammer is identified because Andre's
  wrist is not in the same namespace as the jackhammer.

  Based on the current shot number, the script finds the appropriate 
  location to save the Alembic file in the pipeline. The filename is 
  generated based on the name of the rig being exported All other 
  Alembic Export options are set based on global preferences in the 
  BYU pipeline. In this case, two Alembic files are generated with
  two separate MEL commands: Andre (Containing only Andre) and the
  Jackhammer (Containing the Jackhammer and Andre).    
"""

from PyQt4.QtCore import *
from PyQt4.QtGui import *

import maya.cmds as cmds
import maya.OpenMayaUI as omu
from pymel.core import *
import utilities as amu #asset manager utilities
import os
import sip

WINDOW_WIDTH = 330
WINDOW_HEIGHT = 300

def maya_main_window():
    ptr = omu.MQtUtil.mainWindow()
    return sip.wrapinstance(long(ptr), QObject)     

class AlembicExportDialog(QDialog):
    def __init__(self, parent=maya_main_window()):
        QDialog.__init__(self, parent)
        self.save_file()
        self.setWindowTitle('Select Objects for Export')
        self.setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT)
        self.create_layout()
        self.create_connections()
        self.create_export_list()
    
    #Create the Layout for the QT Dialog Box
    def create_layout(self):
        self.selection_list = self.create_selection_list()

        button_layout = self.create_button_layout()
        
        main_layout = QVBoxLayout()
        main_layout.setSpacing(2)
        main_layout.setMargin(2)
        main_layout.addWidget(self.selection_list)
        main_layout.addLayout(button_layout)
        
        self.setLayout(main_layout)

    #Set up the selection list to allow multiple selections
    def create_selection_list(self):
        selection_list = QListWidget()
        selection_list.setSelectionMode(QAbstractItemView.ExtendedSelection)
        selection_list.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
        return selection_list

    #Add the Export and Cancel Buttons to the bottom and format them
    def create_button_layout(self):
        button_layout = QHBoxLayout()
        button_layout.setSpacing(2)
        button_layout.addStretch()

        self.export_button = QPushButton('Export Alembic')
        self.cancel_button = QPushButton('Cancel')
    
        button_layout.addWidget(self.export_button)
        button_layout.addWidget(self.cancel_button)

        return button_layout
    
    #Attach actions to the buttons
    def create_connections(self):
        self.connect(self.export_button, SIGNAL('clicked()'), self.export_alembic)
        self.connect(self.cancel_button, SIGNAL('clicked()'), self.close_dialog)
    
    #Populate selection list with list of loaded references
    def create_export_list(self):
        self.selection_list.clear()
        
        loaded_references = self.get_loaded_references()
        
        for ref in loaded_references:
            item = QListWidgetItem(ref) 
            item.setText(ref)
            self.selection_list.addItem(item)
        
        self.selection_list.sortItems(0)
    
    #Some scenes have rigs that reference invalid files. Skip over any invalid references
    def get_loaded_references(self):
        references = cmds.ls(references=True)
        loaded=[]

        print "Loaded References: "
        for ref in references:

            print "Checking status of " + ref
            try:
                if cmds.referenceQuery(ref, isLoaded=True):
                    loaded.append(ref)
            except:
                print "Warning: " + ref + " was not associated with a reference file"

        return loaded
    
    
    ########################################################################
    # SLOTS
    ########################################################################

    #Export a seperate Alembic file for each rig and save them to the appropriate shot directory
    def export_alembic(self):
        self.save_file()
        
        #Extract list of selected References
        selected_references = []
        selected_list_items = self.selection_list.selectedItems()
        for item in selected_list_items:
            selected_references.append(item.text())
        
        #Confirm the selection and export to Alembic file
        if self.show_confirm_alembic_dialog(selected_references) == 'Yes':
            loadPlugin("AbcExport")
            for ref in selected_references:
                abc_filepath = self.build_alembic_filepath(ref)
                print "Export to " + abc_filepath
                command = self.build_alembic_command(ref, abc_filepath)
                print command
                Mel.eval(command)
        
        self.close_dialog()
    

    def build_alembic_filepath(self, ref):
        #Get Shot Directory
        current_filepath = cmds.file(q=True, sceneName=True)
        checkout_filepath = os.path.join(amu.getUserCheckoutDir(), os.path.basename(os.path.dirname(current_filepath)))
        dest_filepath = amu.getCheckinDest(checkout_filepath)
        
        #Get Rig Name
        ref_filepath = cmds.referenceQuery(unicode(ref), filename=True)
        rig_name = os.path.basename(ref_filepath).split('.')[0]
        
        return os.path.join(os.path.dirname(dest_filepath), 'animation_cache', 'abc', rig_name +'.abc')

    def build_alembic_command(self, ref, abc_filepath):

        #Look for Alembic tag
        tagged = self.get_tagged_node(ref)

        if tagged == "":
            return ""

        #Look for Parent Contraints that depend on Animation Data outside of the current rig
        dependency_list = self.get_constraint_dependancies(ref)

        #Concatenate list of objects to be exported
        obj_string = ""
        obj_string = " ".join([obj_string, "-root %s"%(tagged.name())])
        for dep in dependency_list:
            dep_ref = ls(dep)
            tagged = self.get_tagged_node(dep_ref[0])
            obj_string = " ".join([obj_string, "-root %s"%(tagged.name())])

        #Define start/end frames
        start_frame = cmds.playbackOptions(q=1, animationStartTime=True) - 5
        end_frame = cmds.playbackOptions(q=1, animationEndTime=True) + 5

        #Build Command
        command = 'AbcExport -j "%s -frameRange %s %s -step 0.25 -writeVisibility -nn -uv -file %s"'%(obj_string, str(start_frame), str(end_frame), abc_filepath)
        
        return command

    #Search for custom Alembic Export Tag
    def get_tagged_node(self, ref):

        #Perform a Depth First search of the tree structure within the rig
        ref_nodes = cmds.referenceQuery(unicode(ref), nodes=True)
        root_node = ls(ref_nodes[0]) #First node listed is always the root of our rigs
        if root_node[0].hasAttr("BYU_Alembic_Export_Flag"):
            tagged_node = root_node[0]
        else:
            tagged_node = self.get_tagged_children(root_node[0])

        #The Alembic Tag is required. If it's not found, report the error.
        if tagged_node == "":
            self.show_no_tag_dialog(unicode(ref))
            return ""

        print tagged_node
        return tagged_node

    #Recursively search children for Alembic Export Flag
    def get_tagged_children(self, node):
        for child in node.listRelatives(c=True):
            if child.hasAttr("BYU_Alembic_Export_Flag"):
                return child
            else:
                tagged_child = self.get_tagged_children(child)
                if tagged_child != "":
                    return tagged_child
        return ""

    #Search parent contraints for dependencies outside of the current rig
    def get_constraint_dependancies(self, ref):
        ref_nodes = cmds.referenceQuery(unicode(ref), nodes=True)
        root_node = ls(ref_nodes[0]) #First node listed is always the root of our rigs
        dependency_list = self.get_dependant_children(root_node[0])

        return dependency_list

    #List all the parent contraints in the file and check the namespace of their targets
    def get_dependant_children(self, node):
        dep_list = []
        for constraint in node.listRelatives(ad=True, type="parentConstraint"):
            parent = constraint.listRelatives(p=True)
            constraintNS = parent[0].split(':')[0]
            target_list = cmds.parentConstraint(unicode(constraint), q=True, tl=True)
            targetNS = target_list[0].split(':')[0]
            if constraintNS != targetNS and targetNS not in dep_list:
                dep_list.append(targetNS + 'RN')

        print dep_list
        return dep_list

    def save_file(self):
        if not cmds.file(q=True, sceneName=True) == '':
            cmds.file(save=True, force=True) #save file
    
    def show_confirm_alembic_dialog(self, references):
        return cmds.confirmDialog( title         = 'Export Alembic'
                                 , message       = 'Export Alembic for:\n' + str(references)
                                 , button        = ['Yes', 'No']
                                 , defaultButton = 'Yes'
                                 , cancelButton  = 'No'
                                 , dismissString = 'No')

    def show_no_tag_dialog(self, ref):
        return cmds.confirmDialog( title         = 'No Alembic Tag Found'
                                 , message       = 'Unable to locate Alembic Export tag for ' + ref + '.'
                                 , button        = ['OK']
                                 , defaultButton = 'OK'
                                 , cancelButton  = 'OK'
                                 , dismissString = 'OK')
    
    def close_dialog(self):
        self.close()

def go():
    dialog = AlembicExportDialog()
    dialog.show()
    
if __name__ == '__main__':
    go()