Skip to content

Commit

Permalink
Added 2 functions to export all skins to a folder and to import all s…
Browse files Browse the repository at this point in the history
…kins that are contained in a folder
  • Loading branch information
jasonlabbe committed Apr 14, 2024
1 parent 2c6b4db commit 7a40bcf
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 71 deletions.
171 changes: 109 additions & 62 deletions scripts/weights_editor_tool/classes/skinned_obj.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys
import os
import random
import glob

if sys.version_info < (3, 0):
import cPickle
Expand Down Expand Up @@ -643,33 +644,64 @@ 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.
Args:
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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
89 changes: 80 additions & 9 deletions scripts/weights_editor_tool/weights_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@

class WeightsEditor(QtWidgets.QMainWindow):

version = "2.3.1"
version = "2.3.2"
instance = None
cb_selection_changed = None
shortcuts = []
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<br><br>"
"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<br><br>"
Expand All @@ -521,11 +538,16 @@ def _create_gui(self):
"<b>This may be long for dense meshes!</b>",
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.<br><br>"
"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)

Expand All @@ -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)
Expand Down Expand Up @@ -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 = {}
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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())
Expand Down

0 comments on commit 7a40bcf

Please sign in to comment.