From 7a40bcfca5f84e557ac367d953a4e10bcc567b62 Mon Sep 17 00:00:00 2001 From: Jason Labbe Date: Sun, 14 Apr 2024 15:18:10 +0800 Subject: [PATCH] Added 2 functions to export all skins to a folder and to import all skins that are contained in a folder --- .../classes/skinned_obj.py | 171 +++++++++++------- scripts/weights_editor_tool/weights_editor.py | 89 ++++++++- 2 files changed, 189 insertions(+), 71 deletions(-) diff --git a/scripts/weights_editor_tool/classes/skinned_obj.py b/scripts/weights_editor_tool/classes/skinned_obj.py index 40baf02..9e9145f 100644 --- a/scripts/weights_editor_tool/classes/skinned_obj.py +++ b/scripts/weights_editor_tool/classes/skinned_obj.py @@ -1,6 +1,7 @@ import sys import os import random +import glob if sys.version_info < (3, 0): import cPickle @@ -643,7 +644,53 @@ def apply_current_skin_weights(self, vert_indexes, normalize=False, display_prog if normalize: cmds.skinCluster(self.skin_cluster, e=True, forceNormalizeWeights=True) - def import_skin(self, file_path=None, world_space=False, create_missing_infs=True, prompt=True): + def serialize(self): + if not self.has_valid_skin(): + raise RuntimeError("Unable to detect a skinCluster on '{}'.".format(self.name)) + + skin_data = self.skin_data.copy() + mesh_points = self._get_world_points() + + with status_progress_bar.StatusProgressBar("Saving vert positions", len(mesh_points)) as pbar: + for vert_index, pnt in enumerate(mesh_points): + skin_data[vert_index]["world_pos"] = [pnt.x, pnt.y, pnt.z] + + if pbar.was_cancelled(): + raise RuntimeError("User cancelled") + + pbar.next() + + influence_data = {} + influence_ids = self.get_influence_ids() + + with status_progress_bar.StatusProgressBar("Saving influence positions", len(influence_ids)) as pbar: + for inf_id, inf in influence_ids.items(): + influence_data[inf_id] = { + "name": inf, + "world_matrix": cmds.xform(inf, q=True, ws=True, m=True) + } + + if pbar.was_cancelled(): + raise RuntimeError("User cancelled") + + pbar.next() + + return { + "version": constants.EXPORT_VERSION, + "object": self.name, + "verts": skin_data.data, + "influences": influence_data, + "skin_cluster": { + "name": self.skin_cluster, + "vert_count": cmds.polyEvaluate(self.name, vertex=True), + "influence_count": len(influence_ids), + "max_influences": cmds.getAttr("{}.maxInfluences".format(self.skin_cluster)), + "skinning_method": cmds.getAttr("{}.skinningMethod".format(self.skin_cluster)), + "dqs_support_non_rigid": cmds.getAttr("{}.dqsSupportNonRigid".format(self.skin_cluster)) + } + } + + def import_skin(self, file_path=None, world_space=False, create_missing_infs=True): """ Imports skin weights from a file. @@ -651,25 +698,10 @@ def import_skin(self, file_path=None, world_space=False, create_missing_infs=Tru file_path(string): An absolute path to save weights to. world_space(bool): False=loads by point order, True=loads by world positions create_missing_infs(bool): Create any missing influences so the skin can still import. - prompt(bool): Asks for user confirmation if enabled. """ if not self.is_valid(): raise RuntimeError("Need to pick an object first.") - if prompt: - msg_box = QtWidgets.QMessageBox( - QtWidgets.QMessageBox.Warning, - "Undos will be lost", - "The tool's undo stack will reset and be lost.\n" - "Would you like to continue?") - - msg_box.addButton(QtWidgets.QMessageBox.Cancel) - msg_box.addButton(QtWidgets.QMessageBox.Ok) - msg_box.setDefaultButton(QtWidgets.QMessageBox.Cancel) - - if msg_box.exec_() == QtWidgets.QMessageBox.Cancel: - return False - if file_path is None: file_path = self._launch_file_picker(1, "Import skin") if not file_path: @@ -775,52 +807,6 @@ def import_skin(self, file_path=None, world_space=False, create_missing_infs=Tru return True - def serialize(self): - if not self.has_valid_skin(): - raise RuntimeError("Unable to detect a skinCluster on '{}'.".format(self.name)) - - skin_data = self.skin_data.copy() - mesh_points = self._get_world_points() - - with status_progress_bar.StatusProgressBar("Saving vert positions", len(mesh_points)) as pbar: - for vert_index, pnt in enumerate(mesh_points): - skin_data[vert_index]["world_pos"] = [pnt.x, pnt.y, pnt.z] - - if pbar.was_cancelled(): - raise RuntimeError("User cancelled") - - pbar.next() - - influence_data = {} - influence_ids = self.get_influence_ids() - - with status_progress_bar.StatusProgressBar("Saving influence positions", len(influence_ids)) as pbar: - for inf_id, inf in influence_ids.items(): - influence_data[inf_id] = { - "name": inf, - "world_matrix": cmds.xform(inf, q=True, ws=True, m=True) - } - - if pbar.was_cancelled(): - raise RuntimeError("User cancelled") - - pbar.next() - - return { - "version": constants.EXPORT_VERSION, - "object": self.name, - "verts": skin_data.data, - "influences": influence_data, - "skin_cluster": { - "name": self.skin_cluster, - "vert_count": cmds.polyEvaluate(self.name, vertex=True), - "influence_count": len(influence_ids), - "max_influences": cmds.getAttr("{}.maxInfluences".format(self.skin_cluster)), - "skinning_method": cmds.getAttr("{}.skinningMethod".format(self.skin_cluster)), - "dqs_support_non_rigid": cmds.getAttr("{}.dqsSupportNonRigid".format(self.skin_cluster)) - } - } - def export_skin(self, file_path=None): """ Exports skin weights to a file. @@ -849,3 +835,64 @@ def export_skin(self, file_path=None): f.write(cPickle.dumps(skin_data)) return file_path + + @classmethod + def export_all_skins(cls, delete_skin_cluster, export_folder=None): + """ + Fetches all skinClusters in the scene and exports them all to a specified folder. + + Args: + delete_skin_cluster(bool): If enabled, deletes the skinCluster after it's exported. + export_folder(str): An absolute path to an existing folder to export the skins to. If None, a file picker will launch. + """ + if export_folder is None: + export_folder = cls._launch_file_picker(3, "The folder to export all skins to") + if not export_folder: + return + + skin_clusters = cmds.ls(type="skinCluster") + if not skin_clusters: + OpenMaya.MGlobal.displayWarning("There are no skinClusters in the scene to export.") + return + + for skin_cluster in skin_clusters: + meshes = cmds.ls(cmds.listHistory(skin_cluster) or [], type="mesh") + if not meshes: + continue + + transform = cmds.listRelatives(meshes[0], parent=True)[0] + export_path = f"{export_folder}/{transform}.skin" + skinned_obj = cls.create(transform) + skinned_obj.export_skin(export_path) + if delete_skin_cluster: + cmds.delete(transform, ch=True) + + @classmethod + def import_all_skins(cls, world_space, create_missing_infs, import_folder=None): + """ + Fetches all skin files from the supplied folder and tries to import them all into the scene. + It tries to load by name using the skin's file name. + + Args: + world_space(bool): False=loads by point order, True=loads by world positions + create_missing_infs(bool): Create any missing influences so the skin can still import. + import_folder(string): An absolute path to a folder that contains skin files. + """ + if import_folder is None: + import_folder = cls._launch_file_picker(3, "Pick a folder with skin files to import them") + if not import_folder: + return + + skin_files = glob.glob(f"{import_folder}/*.skin") + if not skin_files: + OpenMaya.MGlobal.displayWarning("The folder contains no skin files to import with.") + return + + for skin_path in skin_files: + transform = os.path.basename(skin_path).split(".")[0] + if not cmds.objExists(transform): + OpenMaya.MGlobal.displayWarning("Unable to find the object to import weights onto: `{0}`".format(transform)) + continue + + skinned_obj = SkinnedObj.create(transform) + skinned_obj.import_skin(file_path=skin_path, world_space=world_space, create_missing_infs=create_missing_infs) diff --git a/scripts/weights_editor_tool/weights_editor.py b/scripts/weights_editor_tool/weights_editor.py index 1799176..c1ca87c 100644 --- a/scripts/weights_editor_tool/weights_editor.py +++ b/scripts/weights_editor_tool/weights_editor.py @@ -49,7 +49,7 @@ class WeightsEditor(QtWidgets.QMainWindow): - version = "2.3.1" + version = "2.3.2" instance = None cb_selection_changed = None shortcuts = [] @@ -299,6 +299,11 @@ def _create_gui(self): self._hide_long_names_action.toggled.connect(self._hide_long_names_on_triggered) self._options_menu.addAction(self._hide_long_names_action) + self._delete_skin_on_export_all_action = QtWidgets.QAction("Delete skinClusters on `Export all`", self) + self._delete_skin_on_export_all_action.setCheckable(True) + self._delete_skin_on_export_all_action.setChecked(True) + self._options_menu.addAction(self._delete_skin_on_export_all_action) + self._visibility_separator = QtWidgets.QAction("Visibility settings", self) self._visibility_separator.setSeparator(True) self._options_menu.addAction(self._visibility_separator) @@ -509,6 +514,18 @@ def _create_gui(self): tool_tip="Export selected object's skin weights to a file", click_event=self._export_weights_on_clicked) + self._export_all_weights_button = self._create_button( + "Export all weights", "interface/export_weights.png", + tool_tip="Export skin weights from all skin clusters in the scene to a folder

" + "Go to `Tool settings` to toggle if skinClusters should be deleted after they export.", + click_event=self._export_all_weights_on_clicked) + + self._export_layout = utils.wrap_layout( + [self._export_weights_button, + self._export_all_weights_button, + "stretch"], + QtCore.Qt.Horizontal) + self._import_weights_button = self._create_button( "Import weights", "interface/import_weights.png", tool_tip="Import skin weights onto the selected object

" @@ -521,11 +538,16 @@ def _create_gui(self): "This may be long for dense meshes!", click_event=partial(self._import_weights_on_clicked, True)) - self._export_import_layout = utils.wrap_layout( - [self._export_weights_button, - 15, - self._import_weights_button, + self._import_all_weights_button = self._create_button( + "Import all weights", "interface/import_weights.png", + tool_tip="Pick a folder with skin files and try to import them all.

" + "It will search the mesh by name using the skin's file name.", + click_event=self._import_all_weights_on_clicked) + + self._import_layout = utils.wrap_layout( + [self._import_weights_button, self._import_weights_world_button, + self._import_all_weights_button, "stretch"], QtCore.Qt.Horizontal) @@ -534,7 +556,8 @@ def _create_gui(self): self._smooth_layout, self._mirror_layout, self._copy_vert_layout, - self._export_import_layout], + self._export_layout, + self._import_layout], QtCore.Qt.Vertical, margins=[0, 0, 0, 0], spacing=5) @@ -944,10 +967,12 @@ def _save_state(self): "show_set_button.isChecked": self._show_set_button.isChecked(), "show_inf_button.isChecked": self._show_inf_button.isChecked(), "hide_long_names_action.isChecked": self._hide_long_names_action.isChecked(), + "delete_skin_on_export_all_action.isChecked": self._delete_skin_on_export_all_action.isChecked(), "weights_table.max_display_count": self._weights_table.table_model.max_display_count, "add_presets_values": self._add_preset_values, "scale_presets_values": self._scale_preset_values, - "set_presets_values": self._set_preset_values + "set_presets_values": self._set_preset_values, + "skinned_obj.last_browsing_path": SkinnedObj.last_browsing_path } hotkeys_data = {} @@ -1031,7 +1056,8 @@ def _restore_state(self): "show_scale_button.isChecked": self._show_scale_button, "show_set_button.isChecked": self._show_set_button, "show_inf_button.isChecked": self._show_inf_button, - "hide_long_names_action.isChecked": self._hide_long_names_action + "hide_long_names_action.isChecked": self._hide_long_names_action, + "delete_skin_on_export_all_action.isChecked": self._delete_skin_on_export_all_action } for key, checkbox in checkboxes.items(): @@ -1060,6 +1086,10 @@ def _restore_state(self): if "set_presets_values" in data: self._set_preset_values = data["set_presets_values"] self._append_set_presets_buttons(self._set_preset_values) + + path = data.get("skinned_obj.last_browsing_path") + if path and os.path.exists(path): + SkinnedObj.last_browsing_path = path def _update_obj(self, obj): """ @@ -1714,10 +1744,51 @@ def _export_weights_on_clicked(self): print(traceback.format_exc()) OpenMaya.MGlobal.displayError(str(err)) + def _export_all_weights_on_clicked(self): + try: + delete_skin_clusters = self._delete_skin_on_export_all_action.isChecked() + SkinnedObj.export_all_skins(delete_skin_clusters) + except Exception as err: + print(traceback.format_exc()) + OpenMaya.MGlobal.displayError(str(err)) + def _import_weights_on_clicked(self, use_world_positions): try: + msg_box = QtWidgets.QMessageBox( + QtWidgets.QMessageBox.Warning, + "Undos will be lost", + "The tool's undo stack will reset and be lost.\n" + "Would you like to continue?") + + msg_box.addButton(QtWidgets.QMessageBox.Cancel) + msg_box.addButton(QtWidgets.QMessageBox.Ok) + msg_box.setDefaultButton(QtWidgets.QMessageBox.Cancel) + if msg_box.exec_() == QtWidgets.QMessageBox.Cancel: + return False + status = self.obj.import_skin(world_space=use_world_positions) - if status: + if status and self.obj.is_valid(): + self._update_obj(self.obj.name) + except Exception as err: + print(traceback.format_exc()) + OpenMaya.MGlobal.displayError(str(err)) + + def _import_all_weights_on_clicked(self): + try: + msg_box = QtWidgets.QMessageBox( + QtWidgets.QMessageBox.Warning, + "Undos will be lost", + "The tool's undo stack will reset and be lost.\n" + "Would you like to continue?") + + msg_box.addButton(QtWidgets.QMessageBox.Cancel) + msg_box.addButton(QtWidgets.QMessageBox.Ok) + msg_box.setDefaultButton(QtWidgets.QMessageBox.Cancel) + if msg_box.exec_() == QtWidgets.QMessageBox.Cancel: + return False + + SkinnedObj.import_all_skins(False, True) + if self.obj.is_valid(): self._update_obj(self.obj.name) except Exception as err: print(traceback.format_exc())