From 3c2a63f29a84c549543d7363b134e2fbfc058790 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 26 Sep 2024 00:11:52 +0100 Subject: [PATCH] Improvements and fixes to some whoopsies - Fixed double import - Improved Cleanup mesh, includes removal of empty shapekeys and unused vertex groups. - Optimise Armature now includes fix broken names function - Optimise Weights now includes a limit for bone weights - Material conversion now handles transparency - automatically rename bones to a standard naming convention such as _Left _Right Still no where near complete, but I getting there. --- functions/mmd_functions.py | 108 +++++++++++++++++++++++++++++-------- 1 file changed, 87 insertions(+), 21 deletions(-) diff --git a/functions/mmd_functions.py b/functions/mmd_functions.py index bdfcbd7..38a2d6f 100644 --- a/functions/mmd_functions.py +++ b/functions/mmd_functions.py @@ -1,6 +1,7 @@ import bpy import numpy as np -from bpy.types import Operator, Context, Material, ShaderNodeTexImage, ShaderNodeGroup +import re +from bpy.types import Operator, Context, Material, ShaderNodeTexImage, ShaderNodeGroup, Object from ..core.register import register_wrap from ..functions.translations import t from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress @@ -26,25 +27,40 @@ class AvatarToolKit_OT_CleanupMesh(Operator): update_progress(self, context, t("MMDOptions.removing_unused_vertex_groups")) for obj in get_all_meshes(context): - vgroups = obj.vertex_groups - for vgroup in vgroups: - if not any(vgroup.index in [g.group for g in v.groups] for v in obj.data.vertices): - vgroups.remove(vgroup) + self.remove_unused_vertex_groups(obj) update_progress(self, context, t("MMDOptions.removing_unused_vertices")) for obj in get_all_meshes(context): - bpy.ops.object.select_all(action='DESELECT') - obj.select_set(True) - context.view_layer.objects.active = obj - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.mesh.delete_loose() - bpy.ops.object.mode_set(mode='OBJECT') + self.remove_unused_vertices(obj) + + update_progress(self, context, t("MMDOptions.removing_empty_shape_keys")) + for obj in get_all_meshes(context): + self.remove_empty_shape_keys(obj) - update_progress(self, context, t("MMDOptions.cleanup_complete")) finish_progress(context) return {'FINISHED'} + def remove_unused_vertex_groups(self, obj): + vgroups = obj.vertex_groups + for vgroup in vgroups: + if not any(vgroup.index in [g.group for g in v.groups] for v in obj.data.vertices): + vgroups.remove(vgroup) + + def remove_unused_vertices(self, obj): + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.mesh.delete_loose() + bpy.ops.object.mode_set(mode='OBJECT') + + def remove_empty_shape_keys(self, obj): + if obj.data.shape_keys: + for key in obj.data.shape_keys.key_blocks: + if key.name != 'Basis' and all(abs(key.data[i].co[j] - obj.data.shape_keys.reference_key.data[i].co[j]) < 0.0001 for i in range(len(key.data)) for j in range(3)): + obj.shape_key_remove(key) + @register_wrap class AvatarToolKit_OT_OptimizeWeights(Operator): bl_idname = "avatar_toolkit.optimize_weights" @@ -52,13 +68,21 @@ class AvatarToolKit_OT_OptimizeWeights(Operator): bl_description = t("MMDOptions.optimize_weights.desc") bl_options = {'REGISTER', 'UNDO'} + max_weights: bpy.props.IntProperty( + name=t("MMDOptions.max_weights.label"), + description=t("MMDOptions.max_weights.desc"), + default=4, + min=1, + max=8 + ) + def execute(self, context: Context) -> set[str]: armature = get_selected_armature(context) if not armature: self.report({'ERROR'}, t("MMDOptions.no_armature_selected")) return {'CANCELLED'} - init_progress(context, 3) + init_progress(context, 4) update_progress(self, context, t("MMDOptions.merging_weights")) for obj in get_all_meshes(context): @@ -69,10 +93,21 @@ class AvatarToolKit_OT_OptimizeWeights(Operator): update_progress(self, context, t("MMDOptions.removing_zero_weight_bones")) bpy.ops.avatar_toolkit.remove_zero_weight_bones('EXEC_DEFAULT') + update_progress(self, context, t("MMDOptions.limiting_vertex_weights")) + for obj in get_all_meshes(context): + self.limit_vertex_weights(obj) + update_progress(self, context, t("MMDOptions.weight_optimization_complete")) finish_progress(context) return {'FINISHED'} + def limit_vertex_weights(self, obj): + for v in obj.data.vertices: + if len(v.groups) > self.max_weights: + sorted_groups = sorted(v.groups, key=lambda g: g.weight, reverse=True) + for g in sorted_groups[self.max_weights:]: + obj.vertex_groups[g.group].remove([v.index]) + @register_wrap class AvatarToolKit_OT_OptimizeArmature(Operator): bl_idname = "avatar_toolkit.optimize_armature" @@ -86,7 +121,7 @@ class AvatarToolKit_OT_OptimizeArmature(Operator): self.report({'ERROR'}, t("MMDOptions.no_armature_selected")) return {'CANCELLED'} - init_progress(context, 7) + init_progress(context, 9) update_progress(self, context, t("MMDOptions.fixing_bone_rolls")) bpy.ops.object.mode_set(mode='EDIT') @@ -111,6 +146,12 @@ class AvatarToolKit_OT_OptimizeArmature(Operator): update_progress(self, context, t("MMDOptions.reordering_bones")) self.reorder_bones(context, armature) + update_progress(self, context, t("MMDOptions.fixing_armature_names")) + self.fix_armature_names(armature) + + update_progress(self, context, t("MMDOptions.renaming_bones")) + self.rename_bones(armature) + update_progress(self, context, t("MMDOptions.armature_optimization_complete")) finish_progress(context) return {'FINISHED'} @@ -126,12 +167,29 @@ class AvatarToolKit_OT_OptimizeArmature(Operator): for root_bone in sorted(root_bones, key=lambda b: b.name): sort_bones(root_bone) -import bpy -import numpy as np -from bpy.types import Operator, Context, Material, ShaderNodeTexImage, ShaderNodeGroup, Object -from ..core.register import register_wrap -from ..functions.translations import t -from ..core.common import get_all_meshes, init_progress, update_progress, finish_progress + def fix_armature_names(self, armature): + for bone in armature.data.bones: + fixed_name = self.get_fixed_bone_name(bone.name) + if fixed_name != bone.name: + bone.name = fixed_name + + def get_fixed_bone_name(self, name): + name = name.replace(' ', '_') + name = re.sub(r'[^\w]', '', name) + return name + + def rename_bones(self, armature): + for bone in armature.data.bones: + new_name = self.get_standardized_bone_name(bone.name) + if new_name != bone.name: + bone.name = new_name + + def get_standardized_bone_name(self, name): + if 'left' in name.lower(): + return f"Left_{name}" + elif 'right' in name.lower(): + return f"Right_{name}" + return name def bake_mmd_colors(node_base_tex: ShaderNodeTexImage, node_mmd_shader: ShaderNodeGroup): ambient_color_input = node_mmd_shader.inputs.get("Ambient Color") @@ -202,6 +260,7 @@ def add_principled_shader(material: Material, bake_mmd=True): if node_base_tex and node_base_tex.image: links.new(node_base_tex.outputs["Color"], principled_shader.inputs["Base Color"]) + links.new(node_base_tex.outputs["Alpha"], principled_shader.inputs["Alpha"]) elif principled_base_color is not None: principled_shader.inputs["Base Color"].default_value = principled_base_color @@ -211,6 +270,12 @@ def add_principled_shader(material: Material, bake_mmd=True): principled_shader.inputs["Clearcoat Roughness"].default_value = 0 principled_shader.inputs["IOR"].default_value = 1.45 + # Handle transparency + if material.blend_method != 'OPAQUE': + principled_shader.inputs["Alpha"].default_value = material.alpha_threshold + material.blend_method = 'CLIP' + material.shadow_method = 'CLIP' + def fix_mmd_shader(material: Material): mmd_shader_node = material.node_tree.nodes.get("mmd_shader") if mmd_shader_node: @@ -301,3 +366,4 @@ class AvatarToolKit_OT_ConvertMaterials(Operator): for link in input.links: if link.from_node not in used_nodes: self.traverse_node_tree(link.from_node, used_nodes) +