From fbbffe9c497ea541cd46194e0c2e5609e85f9740 Mon Sep 17 00:00:00 2001 From: Amir Shehata Date: Sat, 13 Jun 2020 20:56:11 -0700 Subject: [PATCH 1/2] MB-Lab: Add a simplified export expression feature This feature makes use of Shape keys. The work flow is: 1. Create a character 2. Create a basis shape key followed by as many shape keys you desire 3. Once you're done click on the "Export Expression Shape Keys" 3a. Optionally you can select the "Apply to all Phenotype". This will apply the shape keys to all the expression files for all the same phenotype. Signed-off-by: Amir Shehata --- __init__.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++- file_ops.py | 2 +- morphengine.py | 12 ++++- 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 4eb5ed4f..00c12e4c 100644 --- a/__init__.py +++ b/__init__.py @@ -26,7 +26,7 @@ import logging -import time +import time, ntpath import datetime import json import os @@ -896,6 +896,9 @@ def mbcrea_enum_expressions_items_update(self, context): bpy.types.Scene.mblab_facs_rig = bpy.props.BoolProperty( name="Import FACS Rig") +bpy.types.Scene.mblab_copy_to_all_phenotype = bpy.props.BoolProperty( + name="Apply to all Phenotype") + #Hair color Drop Down List bpy.types.Scene.mblab_hair_color = bpy.props.EnumProperty( items=hair_style_list, @@ -2989,6 +2992,8 @@ def draw(self, context): box_tools = self.layout.box() box_tools.label(text="TOOLS CATEGORIES", icon="RNA") + box_tools.operator('mbcrea.button_export_shape_keys', icon='EXPORT') + box_tools.prop(scn, "mblab_copy_to_all_phenotype") if gui_active_panel_first != "adaptation_tools": box_tools.operator('mbcrea.button_adaptation_tools_on', icon=icon_expand) else: @@ -4285,6 +4290,115 @@ def execute(self, context): print(copie) return {'FINISHED'} +class ButtonExportShapeKeys(bpy.types.Operator): + bl_label = 'Export Expression Shape Keys' + bl_idname = 'mbcrea.button_export_shape_keys' + bl_description = 'PRE-FINALIZATION Tool: Export character shape keys to character morphs.\n\ +Morphs will be available next time you create same character type' + bl_context = 'objectmode' + bl_options = {'REGISTER', 'INTERNAL'} + + def execute(self, context): + # iterate through all shape keys on the mesh data and generate + # The delta between given shape key and the basis shape key. + # If no basis shape key operation will fail. + # If any of the shape keys override existing shape keys, operation + # will fail. This operation is meant to amend to current Lab keys + # and not to override them. + mesh = algorithms.get_active_body() + mname = mesh.name + if not mesh: + self.report({'ERROR'}, "No active mesh. Please select an MB-Lab Character") + # do a first pass over existing + basis_found = False + key_blocks = None + try: + key_blocks = bpy.data.objects[mname].data.shape_keys.key_blocks + except: + self.ShowMessageBox("Mesh has no shape keys", "Error", 'ERROR') + return {'FINISHED'} + + expression_keys = self.getExpressionKeys() + for key in bpy.data.objects[mname].data.shape_keys.key_blocks: + if key.name == 'Basis': + if basis_found: + self.ShowMessageBox("More than one instance of the Basis Shape Key. Invalid", + "Error", 'ERROR') + return {'FINISHED'} + basis_found = True + continue + if key.name in expression_keys: + self.ShowMessageBox("Shape key %s already exists. Aborting operation" % key.name, "Error", 'ERROR') + return {'FINISHED'} + if not basis_found: + self.ShowMessageBox("Basis shape key not found. Invalid setup", "Error", 'ERROR') + return {'FINISHED'} + + # keys are good lets calculate each one relative to the basis + basis_vertices = [] + new_expressions = {} + for key in bpy.data.objects[mname].data.shape_keys.key_blocks: + if key.name == 'Basis': + basis_vertices = self.getVertexList(key.data) + continue + key_vertices = self.getVertexList(key.data) + indexed_vertices = morphcreator.substract_with_index(basis_vertices, key_vertices) + if len(indexed_vertices) < 1: + continue + # append and write to the expressions file + new_expressions[key.name] = indexed_vertices + self.writeShapeKeyData(new_expressions) + return {'FINISHED'} + + def writeData(self, path, new_data): + data = file_ops.load_json_data(path) + data = dict(data, **new_data) + file_ops.save_json_data(path, data) + + def writeShapeKeyData(self, new_data): + phenotype_path = mblab_humanoid.morph_engine.get_expressions_file() + id1 = ntpath.basename(phenotype_path).split('_')[0] + id2 = ntpath.basename(phenotype_path).split('_')[1] + if bpy.context.scene.mblab_copy_to_all_phenotype: + # we have to check all files + for path in mblab_humanoid.morph_engine.get_all_expressions_files(): + fname = ntpath.basename(path) + cur_id1 = fname.split('_')[0] + cur_id2 = fname.split('_')[1] + # Apply if this is the same phenotype + if cur_id1 == id1 and (cur_id2 == id2 or not 'an' in cur_id2): + self.writeData(path, new_data) + else: + path = mblab_humanoid.morph_engine.get_expressions_file() + self.writeData(path, new_data) + + def getExpressionKeys(self): + keys = [] + if bpy.context.scene.mblab_copy_to_all_phenotype: + # we have to check all files + for path in mblab_humanoid.morph_engine.get_all_expressions_files(): + data = file_ops.load_json_data(path) + keys += list(data.keys()) + else: + path = mblab_humanoid.morph_engine.get_expressions_file() + data = file_ops.load_json_data(path) + keys += list(data.keys()) + return keys + + def getVertexList(self, key_data): + kl_v = key_data.values() + result = [] + for l in kl_v: + result.append(l.co) + return result + + def ShowMessageBox(self, message = "", title = "Error !", icon = 'ERROR'): + + def draw(self, context): + self.layout.label(text=message) + bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) + + class ButtonAdaptationToolsON(bpy.types.Operator): bl_label = 'Model edition' bl_idname = 'mbcrea.button_adaptation_tools_on' @@ -4754,6 +4868,7 @@ def execute(self, context): OBJECT_OT_remove_color_preset, OBJECT_OT_undo_remove_color, ButtonForTest, + ButtonExportShapeKeys, ButtonAdaptationToolsON, ButtonAdaptationToolsOFF, ButtonCompatToolsON, @@ -4823,4 +4938,4 @@ def unregister(): if __name__ == "__main__": register() -# \ No newline at end of file +# diff --git a/file_ops.py b/file_ops.py index f3d698ea..a8e28da5 100644 --- a/file_ops.py +++ b/file_ops.py @@ -130,7 +130,7 @@ def exists_database(lib_path): def save_json_data(json_path, char_data): try: with open(json_path, "w") as j_file: - json.dump(char_data, j_file) + json.dump(char_data, j_file, indent=2) j_file.close() except IOError: if simple_path(json_path) != "": diff --git a/morphengine.py b/morphengine.py index 4cf033d3..0817d2cd 100644 --- a/morphengine.py +++ b/morphengine.py @@ -36,7 +36,7 @@ from . import proxyengine from . import utils #End Teto -import time, json +import time, json, glob import operator logger = logging.getLogger(__name__) @@ -174,6 +174,14 @@ def init_final_form(self): def __repr__(self): return "MorphEngine {0} with {1} morphings".format(self.obj_name, len(self.morph_data)) + def get_expressions_file(self): + return os.path.join(file_ops.get_data_path(), "expressions_morphs", + self.expressions_filename) + + def get_all_expressions_files(self): + file_filter = os.path.join(file_ops.get_data_path(), "expressions_morphs", "*.json") + return glob.glob(file_filter) + def get_object(self): if self.obj_name in bpy.data.objects: return bpy.data.objects[self.obj_name] @@ -419,4 +427,4 @@ def calculate_morph(self, morph_name, val, add_vertices_to_update=True): self.morph_values[morph_name] = val else: logger.debug("Morph data {0} not found".format(morph_name)) - \ No newline at end of file + From 5ef676c2d6a435aa03a2712515eb21527b6f6be8 Mon Sep 17 00:00:00 2001 From: Amir Shehata Date: Sun, 14 Jun 2020 11:33:15 -0700 Subject: [PATCH 2/2] MB-Lab: Import expression shape keys update Addressed two comment: 1. removed indent=2 for json.dump 2. renamed the tool to Import instead of Export I also added an option to override existing shapekeys. This is important since it allows the user to override their own shapekeys. It can be expanded later to expose a list of all the expression shape keys, which the user can modify. However, I still don't think it's a good idea to override the default shape keys, as they are pretty good already. Signed-off-by: Amir Shehata --- __init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/__init__.py b/__init__.py index 00c12e4c..a811ebdb 100644 --- a/__init__.py +++ b/__init__.py @@ -898,6 +898,8 @@ def mbcrea_enum_expressions_items_update(self, context): bpy.types.Scene.mblab_copy_to_all_phenotype = bpy.props.BoolProperty( name="Apply to all Phenotype") +bpy.types.Scene.mblab_override_expressions = bpy.props.BoolProperty( + name="Override Existing Expressions") #Hair color Drop Down List bpy.types.Scene.mblab_hair_color = bpy.props.EnumProperty( @@ -2992,8 +2994,9 @@ def draw(self, context): box_tools = self.layout.box() box_tools.label(text="TOOLS CATEGORIES", icon="RNA") - box_tools.operator('mbcrea.button_export_shape_keys', icon='EXPORT') + box_tools.operator('mbcrea.button_import_shape_keys', icon='EXPORT') box_tools.prop(scn, "mblab_copy_to_all_phenotype") + box_tools.prop(scn, "mblab_override_expressions") if gui_active_panel_first != "adaptation_tools": box_tools.operator('mbcrea.button_adaptation_tools_on', icon=icon_expand) else: @@ -4291,9 +4294,9 @@ def execute(self, context): return {'FINISHED'} class ButtonExportShapeKeys(bpy.types.Operator): - bl_label = 'Export Expression Shape Keys' - bl_idname = 'mbcrea.button_export_shape_keys' - bl_description = 'PRE-FINALIZATION Tool: Export character shape keys to character morphs.\n\ + bl_label = 'Import Expression Shape Keys' + bl_idname = 'mbcrea.button_import_shape_keys' + bl_description = 'PRE-FINALIZATION Tool: Import character shape keys to character morphs.\n\ Morphs will be available next time you create same character type' bl_context = 'objectmode' bl_options = {'REGISTER', 'INTERNAL'} @@ -4327,7 +4330,7 @@ def execute(self, context): return {'FINISHED'} basis_found = True continue - if key.name in expression_keys: + if key.name in expression_keys and not mblab_override_expressions: self.ShowMessageBox("Shape key %s already exists. Aborting operation" % key.name, "Error", 'ERROR') return {'FINISHED'} if not basis_found: