def __init__(self, settings, util, export_location): self._logger = Logger(self.__class__.__name__) self._settings = settings self._util = util self._animSequences = AnimSequences(self._settings) self._exportLayers = ExportLayers(self._settings) self._export_location = export_location
class FbxExportUI: def __init__(self, settings, util, export_location): self._logger = Logger(self.__class__.__name__) self._settings = settings self._util = util self._animSequences = AnimSequences(self._settings) self._exportLayers = ExportLayers(self._settings) self._export_location = export_location def create_panel(self, panel_width): cmds.frameLayout(self._util.to_ui_name('frm_fbx_export'), label='FBX Export', borderStyle='etchedOut', collapsable=True, collapse=True, enable=False, width=panel_width) self._init_anim_sequences_ui() self._init_export_layers_ui() cmds.setParent(upLevel=True) # ANIM SEQUENCE ================================================== [ public ] def init_anim_sequences(self): anim_sequence_names = None if cmds.objExists('anim_sequences'): anim_sequence_names = cmds.listRelatives('anim_sequences') self._animSequences.clear() if anim_sequence_names is not None: # Load items from settings for anim_sequence_name in anim_sequence_names: name_value = self._settings.load('name', '', anim_sequence_name, 'anim_sequences') start_value = self._settings.load('start', '', anim_sequence_name, 'anim_sequences') end_value = self._settings.load('end', '', anim_sequence_name, 'anim_sequences') active_value = self._settings.load('active', True, anim_sequence_name, 'anim_sequences') self._animSequences.add(name_value, start_value, end_value, active_value) if self._animSequences.get_size() == 0: # Add default item self._animSequences.add('InitialPose', '1', '2', True) self._animSequences.add('AnimSeq01', 'start', 'end', True) # Update UI self._update_anim_sequences_ui() def get_anim_sequences(self): return self._animSequences def add_anim_sequence(self, *args): selected_objs = cmds.ls(selection=True) self._util.disable_undo() try: start = str(int(cmds.playbackOptions(query=True, min=True))) end = str(int(cmds.playbackOptions(query=True, max=True))) name = 'AnimSeq' active = True anim_sequence = self._animSequences.add(name, start, end, active) name = anim_sequence.get_name() + str(anim_sequence.get_index() + 1).zfill(2) self.update_anim_sequence(anim_sequence.get_index(), '', 'name', name) cmds.evalDeferred(self._update_anim_sequences_ui) finally: self._util.select_obj(selected_objs) self._util.enable_undo() def update_anim_sequence(self, idx, widget_name, attribute, value, *args): self._util.disable_undo() try: if attribute == 'name': value = self._util.to_proper_name(value) self._animSequences.update(idx, name = value) if cmds.textField(widget_name, query=True, exists=True): cmds.textField(widget_name, edit=True, text=value, annotation=value) elif attribute == 'start': value = self._to_range_num(value) self._animSequences.update(idx, start = value) if cmds.textField(widget_name, query=True, exists=True): cmds.textField(widget_name, edit=True, text=value, annotation=value) elif attribute == 'end': value = self._to_range_num(value) self._animSequences.update(idx, end = value) if cmds.textField(widget_name, query=True, exists=True): cmds.textField(widget_name, edit=True, text=value, annotation=value) elif attribute == 'active': self._animSequences.update(idx, active = value) finally: self._util.enable_undo() def delete_anim_sequence(self, idx, *args): selected_objs = cmds.ls(selection=True) self._util.disable_undo() try: self._animSequences.remove(idx) cmds.evalDeferred(self._update_anim_sequences_ui) finally: self._util.select_obj(selected_objs) self._util.enable_undo() def move_anim_sequence_up(self, idx, *args): self._util.disable_undo() try: moved = self._animSequences.move_up(idx) if moved: cmds.evalDeferred(self._update_anim_sequences_ui) finally: self._util.enable_undo() def move_anim_sequence_down(self, idx, *args): self._util.disable_undo() try: moved = self._animSequences.move_down(idx) if moved: cmds.evalDeferred(self._update_anim_sequences_ui) finally: self._util.enable_undo() # EXPORT LAYER =================================================== [ public ] def init_export_layers(self): export_layer_names = None if cmds.objExists('export_layers'): export_layer_names = cmds.listRelatives('export_layers') self._exportLayers.clear() if export_layer_names is not None: # Load items from settings for export_layer_name in export_layer_names: name_value = self._settings.load('name', '', export_layer_name, 'export_layers') objects_value = self._settings.load('objects', '', export_layer_name, 'export_layers', as_list = True) active_value = self._settings.load('active', True, export_layer_name, 'export_layers') self._exportLayers.add(name_value, objects_value, active_value) # Add export objects from the scene fbx_export_layer = self._util.to_node_name('FBX_Export') character_name = self._settings.get_ref_name() objects = [] if character_name == '': # Set default character name character_name = 'Character' # Check if it hasn't been added before is_new_export_layer = True for exportLayer in self._exportLayers.get_list(): if character_name == exportLayer.get_name(): is_new_export_layer = False break # Check that FBX export layer exists is_fbx_export_layer_exists = cmds.objExists(fbx_export_layer) if is_new_export_layer and is_fbx_export_layer_exists: # Get export objects from the scene export_objects = cmds.editDisplayLayerMembers(fbx_export_layer, query=True) if export_objects is not None and len(export_objects) > 0: objects = export_objects self._exportLayers.add(character_name, objects, True) # Update UI self._update_export_layers_ui() def get_export_layers(self): return self._exportLayers def add_export_layer(self, idx): selected_objs = cmds.ls(selection=True) self._util.disable_undo() try: name = 'NewLayer' active = True export_layer = self._exportLayers.add(name, [], active) name = export_layer.get_name() + str(export_layer.get_index()).zfill(2) self.update_export_layer(export_layer.get_index(), '', 'name', name) cmds.evalDeferred(self._update_export_layers_ui) finally: self._util.select_obj(selected_objs) self._util.enable_undo() def update_export_layer(self, idx, widget_name, attribute, value, *args): self._util.disable_undo() try: if attribute == 'name': value = self._util.to_proper_name(value) self._exportLayers.update(idx, name = value) if cmds.textField(widget_name, query=True, exists=True): cmds.textField(widget_name, edit=True, text=value, annotation=value) elif attribute == 'active': self._exportLayers.update(idx, active = value) finally: self._util.enable_undo() def delete_export_layer(self, idx, *args): selected_objs = cmds.ls(selection=True) self._util.disable_undo() try: self._exportLayers.remove(idx) cmds.evalDeferred(self._update_export_layers_ui) finally: self._util.select_obj(selected_objs) self._util.enable_undo() def move_export_layer_up(self, idx, *args): self._util.disable_undo() try: moved = self._exportLayers.move_up(idx) if moved: cmds.evalDeferred(self._update_export_layers_ui) finally: self._util.enable_undo() def move_export_layer_down(self, idx, *args): self._util.disable_undo() try: moved = self._exportLayers.move_down(idx) if moved: cmds.evalDeferred(self._update_export_layers_ui) finally: self._util.enable_undo() def add_selected_objects_to_layer(self, idx, widget_name, *args): self._util.disable_undo() try: selected_objects = cmds.ls(selection=True) added = self._exportLayers.get(idx).add_objects(selected_objects) if added: objects = self._exportLayers.get(idx).get_objects() if objects is None: objects = [] self._exportLayers.update(idx, objects = objects) cmds.text(widget_name, edit=True, label=str(self._exportLayers.get(idx).get_size())) finally: self._util.enable_undo() def remove_selected_objects_from_layer(self, idx, widget_name, *args): self._util.disable_undo() try: selected_objects = cmds.ls(selection=True) removed = self._exportLayers.get(idx).remove_objects(selected_objects) if removed: objects = self._exportLayers.get(idx).get_objects() if objects is None: objects = [] self._exportLayers.update(idx, objects = objects) cmds.text(widget_name, edit=True, label=str(self._exportLayers.get(idx).get_size())) finally: self._util.enable_undo() def empty_objects_from_layer(self, idx, widget_name, *args): self._util.disable_undo() try: emptied = self._exportLayers.get(idx).clear_objects() if emptied: objects = self._exportLayers.get(idx).get_objects() if objects is None: objects = [] self._exportLayers.update(idx, objects = objects) cmds.text(widget_name, edit=True, label=str(self._exportLayers.get(idx).get_size())) finally: self._util.enable_undo() def select_objects_from_layer(self, idx, *args): export_objects = self._exportLayers.get(idx).get_objects() self._util.select_obj(export_objects) def fbx_export(self, idx, *args): dialog_title = '[Rig-a-thon] FBX Export' last_export_name = self._util.load_from_cache('last_export_name', '') export_name = self._util.dialog_box_ok_cancel(dialog_title, 'Enter export name:', last_export_name) if export_name is None or len(export_name.strip()) == 0: # Cancel export return self._util.save_to_cache('last_export_name', export_name) base_export_path = os.path.join(self._export_location, self._util.to_proper_name(export_name)) if os.path.exists(base_export_path): # Export path already exists allow_overwrite = self._util.dialog_box_yes_no(dialog_title, 'Export name already exists. Do you want to overwrite existing files? ') if not allow_overwrite: return # Get selected objects selected_objs = cmds.ls(selection=True) # Get initial time slider frame range min_frame = cmds.playbackOptions(query=True, min=True) max_frame = cmds.playbackOptions(query=True, max=True) try: # Get anim options fps = cmds.currentUnit(query=True, time=True) rotationInterp = 1 for option in cmds.optionVar(list=True): if option == 'rotationInterpolationDefault': rotationInterp = cmds.optionVar(query=option) self._logger.info('# FBX Export started [Time Unit: $fps, RotationInterp: $rotationInterp]' \ .replace('$fps', str(fps)) \ .replace('$rotationInterp', str(rotationInterp)) ) exported_fbx_paths = [] # Iterate thru animSequences and exportLayers anim_sequences = self._animSequences.get_list() export_layers = self._exportLayers.get_list() for export_layer in export_layers: export_all = idx == -1 export_selected = idx == export_layer.get_index() export_layer_active = export_layer.is_active() if export_all: if not export_layer_active: # Skip inactive layers continue elif not export_selected: continue export_objects = export_layer.get_objects() if len(export_objects) == 0: # No objects to export self._logger.warn('# Skipping export layer \'$layer\' - no objects to export.'.replace('$layer', export_layer.get_name())) continue missing_obj = False for export_object in export_objects: if not cmds.objExists(export_object): # Missing export object missing_obj = True break if missing_obj: self._logger.warn('# Skipping export layer \'$layer\' - missing object \'$object\'.' \ .replace('$layer', export_layer.get_name()) \ .replace('$object', export_object) \ ) continue for anim_sequence in anim_sequences: if anim_sequence.is_active(): export_path = self.export_anim(base_export_path, export_layer, anim_sequence, min_frame, max_frame) if export_path is not None: exported_fbx_paths.append(export_path) if len(exported_fbx_paths) > 0: self._logger.info('\t* FBX Export complete.') self._logger.info('# Exported FBX files:') for export_fbx_path in exported_fbx_paths: self._logger.info('\t- ' + export_fbx_path) cmds.confirmDialog(title=dialog_title, message='Export complete.\n\nSee Script Editor for details. ', button='OK') else: self._logger.info('\t* FBX Export complete. No objects were exported.') cmds.confirmDialog(title=dialog_title, message='Export failed.\n\nSee Script Editor for details. ', button='OK') self._logger.command_message('Export complete. See Script Editor for details.') finally: # Restore time slider frame range cmds.playbackOptions(min=min_frame, animationStartTime=min_frame) cmds.playbackOptions(max=max_frame, animationEndTime=max_frame) # Restore selections if len(selected_objs) > 0: cmds.select(selected_objs) else: cmds.select(clear=True) def export_anim(self, base_export_path, export_layer, anim_sequence, min_frame, max_frame): # Set new time slider frame range new_start_frame = anim_sequence.get_start() new_end_frame = anim_sequence.get_end() if new_start_frame == 'start': new_start_frame = str(int(min_frame)) if new_end_frame == 'end': new_end_frame = str(int(max_frame)) if int(new_start_frame) >= int(new_end_frame): self._logger.warn('# Skipping FBX Export on animation sequence \'$anim_sequence\' - End frame should be greater than Start frame.' \ .replace('$anim_sequence', anim_sequence.get_name()) \ ) return None cmds.playbackOptions(min=new_start_frame, animationStartTime=new_start_frame) cmds.playbackOptions(max=new_end_frame, animationEndTime=new_end_frame) # Format export path export_filename = '$layer_name_$anim_sequence_$start_$end.fbx' \ .replace('$layer_name', export_layer.get_name()) \ .replace('$anim_sequence', anim_sequence.get_name()) \ .replace('$start', str(new_start_frame)) \ .replace('$end', str(new_end_frame)) export_path = os.path.join(base_export_path, export_filename) self._logger.info('# Export path: ' + export_path) # Retrieve joint chains joint_chains = [] for export_object in export_layer.get_objects(): if cmds.nodeType(export_object) == 'joint': joint_chains.append(export_object) if len(joint_chains) == 0: self._logger.warn('# Skipping FBX Export on export layer \'$export_layer\' - No joints found.' \ .replace('$export_layer', export_layer.get_name()) \ ) return None # Clear keys for joint_chain in joint_chains: cmds.cutKey(joint_chain, hierarchy='both') # Bake animation self._logger.info('\t- Baking animation...') cmds.select(joint_chains, hierarchy=True) cmds.bakeResults(simulation=True, time=(new_start_frame, new_end_frame)) # Run an euler filter self._logger.info('\t- Running Euler filter...') cmds.select(joint_chains, hierarchy=True) cmds.filterCurve() # Set FBX properties mel.eval('FBXExportConstraints -v 1;') mel.eval('FBXExportCacheFile -v 0;') if not os.path.isdir(base_export_path): # Create directory os.makedirs(base_export_path) # Export as FBX self._logger.info('\t- Exporting...') cmds.select(export_layer.get_objects()) cmds.file(export_path, force=True, options='v=0;', type='FBX export', preserveReferences=True, exportSelected=True) self._logger.info('\t* DONE!') # Clear keys for joint_chain in joint_chains: cmds.cutKey(joint_chain, hierarchy='both') return export_path # ANIM SEQUENCE ================================================= [ private ] def _init_anim_sequences_ui(self): cmds.columnLayout(self._util.to_ui_name('col_anim_sequences'), adjustableColumn=True) cmds.setParent(upLevel=True) def _update_anim_sequences_ui(self): col1_width = 10 col2_width = 34 col3_width = 4 col4_width = 34 col6_width = 10 col7_width = 12 col8_width = 10 # Delete existing UI elements child_elements = cmds.columnLayout(self._util.to_ui_name('col_anim_sequences'), query=True, childArray=True) if child_elements is not None: cmds.deleteUI(child_elements) # Create new UI elements cmds.frameLayout(parent=self._util.to_ui_name('col_anim_sequences'), label=' Animation Sequences', borderStyle='out', font='obliqueLabelFont') cmds.setParent(upLevel=True) cmds.rowLayout(parent=self._util.to_ui_name('col_anim_sequences'), numberOfColumns=7, adjustableColumn=5) cmds.separator(style='none', width=col1_width) cmds.text(' Start', font='boldLabelFont', width=col2_width, align='left') cmds.separator(style='none', width=col3_width) cmds.text(' End', font='boldLabelFont', width=col4_width, align='left') cmds.text(' Animation', font='boldLabelFont', align='left') cmds.text('', font='boldLabelFont', align='left') cmds.text(' Sort', font='boldLabelFont', align='left') cmds.setParent(upLevel=True) for index in range(0, self._animSequences.get_size()): cmds.rowLayout(parent=self._util.to_ui_name('col_anim_sequences'), numberOfColumns=8, adjustableColumn=5) # Close button cmds.iconTextButton(label='x', style='textOnly', font='boldLabelFont', command=partial(self.delete_anim_sequence, index), width=col1_width) txtField_start_name = self._util.to_ui_name('txtField_anim_sequence_start_name' + str(index)) txtField_end_name = self._util.to_ui_name('txtField_anim_sequence_end_name' + str(index)) txtField_name_name = self._util.to_ui_name('txtField_anim_sequence_name_name' + str(index)) chk_active_name = self._util.to_ui_name('chk_anim_sequence_active_name' + str(index)) start_value = self._animSequences.get(index).get_start() end_value = self._animSequences.get(index).get_end() name_value = self._animSequences.get(index).get_name() active_value = self._animSequences.get(index).is_active() cmds.textField(txtField_start_name, text=start_value, annotation=start_value, changeCommand=partial(self.update_anim_sequence, index, txtField_start_name, 'start'), width=col2_width) cmds.text('-', width=col3_width) cmds.textField(txtField_end_name, text=end_value, annotation=end_value, changeCommand=partial(self.update_anim_sequence, index, txtField_end_name, 'end'), width=col4_width) cmds.textField(txtField_name_name, text=name_value, annotation=name_value, changeCommand=partial(self.update_anim_sequence, index, txtField_name_name, 'name')) cmds.checkBox(chk_active_name, value=active_value, label='', changeCommand=partial(self.update_anim_sequence, index, chk_active_name, 'active')) cmds.iconTextButton(label='^', style='textOnly', font='boldLabelFont', command=partial(self.move_anim_sequence_up, index), width=col7_width) cmds.iconTextButton(label='v', style='textOnly', font='boldLabelFont', command=partial(self.move_anim_sequence_down, index), width=col8_width) cmds.setParent(upLevel=True) cmds.rowLayout(parent=self._util.to_ui_name('col_anim_sequences'), numberOfColumns=2) cmds.separator(style='none', width=col1_width) cmds.button(label=' New ', command=partial(self.add_anim_sequence)) cmds.setParent(upLevel=True) # EXPORT LAYER ================================================== [ private ] def _init_export_layers_ui(self): cmds.columnLayout(self._util.to_ui_name('col_export_layers'), adjustableColumn=True) cmds.setParent(upLevel=True) def _update_export_layers_ui(self): col1_width = 10 col2_width = 12 col3_width = 18 col6_width = 12 col7_width = 10 col8_width = 10 # Delete existing UI elements child_elements = cmds.columnLayout(self._util.to_ui_name('col_export_layers'), query=True, childArray=True) if child_elements is not None: for element in child_elements: cmds.deleteUI(element) # Create new UI elements cmds.columnLayout(self._util.to_ui_name('col_export_layers'), edit=True) cmds.frameLayout(parent=self._util.to_ui_name('col_export_layers'), label=' Export Layers', borderStyle='out', font='obliqueLabelFont') cmds.setParent(upLevel=True) cmds.rowLayout(parent=self._util.to_ui_name('col_export_layers'), numberOfColumns=7, adjustableColumn=4) cmds.separator(style='none', width=col1_width) cmds.separator(style='none', width=col2_width) cmds.separator(style='none', width=col3_width) cmds.text(label='Layer', font='boldLabelFont', align='left') cmds.text(label='', font='boldLabelFont', align='left') cmds.text(label='', font='boldLabelFont', align='left') cmds.text(' Sort', font='boldLabelFont', align='left') cmds.setParent(upLevel=True) for index in range(0, self._exportLayers.get_size()): cmds.rowLayout(parent=self._util.to_ui_name('col_export_layers'), numberOfColumns=8, adjustableColumn=4) # Close button cmds.iconTextButton(label='x', style='textOnly', font='boldLabelFont', command=partial(self.delete_export_layer, index), width=col1_width) txt_object_count_name = self._util.to_ui_name('txt_export_layer_object_count' + str(index)) txtField_layer_name = self._util.to_ui_name('txtField_export_layer_layer_name' + str(index)) chk_active_name = self._util.to_ui_name('chk_export_layer_active_name' + str(index)) layer_value = self._exportLayers.get(index).get_name() active_value = self._exportLayers.get(index).is_active() cmds.text(txt_object_count_name, label=str(self._exportLayers.get(index).get_size()), annotation='Object count', width=col2_width) layer_btn = cmds.button(label='L', annotation='Layer Menu...', width=col3_width) cmds.textField(txtField_layer_name, text=layer_value, annotation=layer_value, changeCommand=partial(self.update_export_layer, index, txtField_layer_name, 'name')) cmds.button(label='Export', command=partial(self.fbx_export, index)) cmds.checkBox(chk_active_name, value=active_value, label='', changeCommand=partial(self.update_export_layer, index, chk_active_name, 'active')) cmds.iconTextButton(label='^', style='textOnly', font='boldLabelFont', command=partial(self.move_export_layer_up, index), width=col7_width) cmds.iconTextButton(label='v', style='textOnly', font='boldLabelFont', command=partial(self.move_export_layer_down, index), width=col8_width) cmds.setParent(upLevel=True) # Context menu cmds.popupMenu(parent=layer_btn, button=1) cmds.menuItem(label='Add Selected Objects', command=partial(self.add_selected_objects_to_layer, index, txt_object_count_name)) cmds.menuItem(label='Remove Selected Objects', command=partial(self.remove_selected_objects_from_layer, index, txt_object_count_name)) cmds.menuItem(divider=True) cmds.menuItem(label='Empty the Layer', command=partial(self.empty_objects_from_layer, index, txt_object_count_name)) cmds.menuItem(divider=True) cmds.menuItem(label='Select Objects', command=partial(self.select_objects_from_layer, index)) cmds.rowLayout(parent=self._util.to_ui_name('col_export_layers'), numberOfColumns=6, adjustableColumn=3) cmds.separator(style='none', width=col1_width) cmds.button(label=' New ', command=partial(self.add_export_layer)) cmds.separator(style='none') cmds.button(label=' Export All ', command=partial(self.fbx_export, -1)) cmds.separator(style='none', width=col6_width) cmds.separator(style='none', width=col7_width) cmds.setParent(upLevel=True) def _to_range_num(self, num): # Allow 'start' and 'end' as numeric values num = num.strip() if num == 'start' or num == 'end': return num numeric_only = '' for c in num: numeric_only += c if c.isnumeric() or c == '.' else '' num = numeric_only try: return str(int(float(num))) except: return '0'