# Copyright 2015 MMD Tools authors # This file is part of MMD Tools. from collections import namedtuple from typing import Optional, cast import bpy from mathutils import Quaternion, Vector from .. import bpyutils, utils from ..core.exceptions import MaterialNotFoundError from ..core.material import FnMaterial from ..core.model import FnModel from ..core.morph import FnMorph from ..utils import ItemMoveOp, ItemOp # Util functions def divide_vector_components(vec1, vec2): if len(vec1) != len(vec2): raise ValueError("Vectors should have the same number of components") result = [] for v1, v2 in zip(vec1, vec2, strict=False): if v2 == 0: if v1 == 0: v2 = 1 # If we have a 0/0 case we change the divisor to 1 else: raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero") result.append(v1 / v2) return result def multiply_vector_components(vec1, vec2): if len(vec1) != len(vec2): raise ValueError("Vectors should have the same number of components") result = [] for v1, v2 in zip(vec1, vec2, strict=False): result.append(v1 * v2) return result def special_division(n1, n2): """Return 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised""" if n2 == 0: if n1 == 0: n2 = 1 else: raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero") return n1 / n2 class AddMorph(bpy.types.Operator): bl_idname = "mmd_tools.morph_add" bl_label = "Add Morph" bl_description = "Add a morph item to active morph list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type morphs = getattr(mmd_root, morph_type) morph, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph) morph.name = "New Morph" if morph_type.startswith("uv"): morph.data_type = "VERTEX_GROUP" return {"FINISHED"} class RemoveMorph(bpy.types.Operator): bl_idname = "mmd_tools.morph_remove" bl_label = "Remove Morph" bl_description = "Remove morph item(s) from the list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} all: bpy.props.BoolProperty( name="All", description="Delete all morph items", default=False, options={"SKIP_SAVE"}, ) def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type if morph_type.startswith("material"): bpy.ops.mmd_tools.clear_temp_materials() elif morph_type.startswith("uv"): bpy.ops.mmd_tools.clear_uv_morph_view() morphs = getattr(mmd_root, morph_type) if self.all: morphs.clear() mmd_root.active_morph = 0 else: morphs.remove(mmd_root.active_morph) mmd_root.active_morph = max(0, mmd_root.active_morph - 1) return {"FINISHED"} class MoveMorph(bpy.types.Operator, ItemMoveOp): bl_idname = "mmd_tools.morph_move" bl_label = "Move Morph" bl_description = "Move active morph item up/down in the list. This will not affect the morph order in exported PMX files (use Display Panel order instead)." bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root mmd_root.active_morph = self.move( getattr(mmd_root, mmd_root.active_morph_type), mmd_root.active_morph, self.type, ) return {"FINISHED"} class CopyMorph(bpy.types.Operator): bl_idname = "mmd_tools.morph_copy" bl_label = "Copy Morph" bl_description = "Make a copy of active morph in the list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type morphs = getattr(mmd_root, morph_type) morph = ItemOp.get_by_index(morphs, mmd_root.active_morph) if morph is None: return {"CANCELLED"} name_orig, name_tmp = morph.name, f"_tmp{str(morph.as_pointer())}" if morph_type.startswith("vertex"): for obj in FnModel.iterate_mesh_objects(root): FnMorph.copy_shape_key(obj, name_orig, name_tmp) elif morph_type.startswith("uv"): if morph.data_type == "VERTEX_GROUP": for obj in FnModel.iterate_mesh_objects(root): FnMorph.copy_uv_morph_vertex_groups(obj, name_orig, name_tmp) morph_new, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph) for k, v in morph.items(): morph_new[k] = v if k != "name" else name_tmp morph_new.name = name_orig + "_copy" # trigger name check return {"FINISHED"} class OverwriteBoneMorphsFromActionPose(bpy.types.Operator): bl_idname = "mmd_tools.morph_overwrite_from_active_action_pose" bl_label = "Overwrite Bone Morphs from active Action Pose" bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): root = FnModel.find_root_object(context.active_object) return root is not None and root.mmd_root.active_morph_type == "bone_morphs" def execute(self, context): root = FnModel.find_root_object(context.active_object) FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root)) return {"FINISHED"} class AddMorphOffset(bpy.types.Operator): bl_idname = "mmd_tools.morph_offset_add" bl_label = "Add Morph Offset" bl_description = "Add a morph offset item to the list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) if morph is None: return {"CANCELLED"} item, morph.active_data = ItemOp.add_after(morph.data, morph.active_data) if morph_type.startswith("material"): if obj.type == "MESH" and obj.mmd_type == "NONE": item.related_mesh = obj.data.name active_material = obj.active_material if active_material and "_temp" not in active_material.name: item.material = active_material.name elif morph_type.startswith("bone"): pose_bone = context.active_pose_bone if pose_bone: item.bone = pose_bone.name item.location = pose_bone.location item.rotation = pose_bone.rotation_quaternion return {"FINISHED"} class RemoveMorphOffset(bpy.types.Operator): bl_idname = "mmd_tools.morph_offset_remove" bl_label = "Remove Morph Offset" bl_description = "Remove morph offset item(s) from the list" bl_options = {"REGISTER", "UNDO", "INTERNAL"} all: bpy.props.BoolProperty( name="All", description="Delete all morph offset items", default=False, options={"SKIP_SAVE"}, ) def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) if morph is None: return {"CANCELLED"} if morph_type.startswith("material"): bpy.ops.mmd_tools.clear_temp_materials() if self.all: if morph_type.startswith("vertex"): for obj in FnModel.iterate_mesh_objects(root): FnMorph.remove_shape_key(obj, morph.name) return {"FINISHED"} if morph_type.startswith("uv"): if morph.data_type == "VERTEX_GROUP": for obj in FnModel.iterate_mesh_objects(root): FnMorph.store_uv_morph_data(obj, morph) return {"FINISHED"} morph.data.clear() morph.active_data = 0 else: morph.data.remove(morph.active_data) morph.active_data = max(0, morph.active_data - 1) return {"FINISHED"} class InitMaterialOffset(bpy.types.Operator): bl_idname = "mmd_tools.material_morph_offset_init" bl_label = "Init Material Offset" bl_description = "Set all offset values to target value" bl_options = {"REGISTER", "UNDO", "INTERNAL"} target_value: bpy.props.FloatProperty( name="Target Value", description="Target value", default=0, ) def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph = mmd_root.material_morphs[mmd_root.active_morph] mat_data = morph.data[morph.active_data] val = self.target_value mat_data.diffuse_color = mat_data.edge_color = (val,) * 4 mat_data.specular_color = mat_data.ambient_color = (val,) * 3 mat_data.shininess = mat_data.edge_weight = val mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4 return {"FINISHED"} class ApplyMaterialOffset(bpy.types.Operator): bl_idname = "mmd_tools.apply_material_morph_offset" bl_label = "Apply Material Offset" bl_description = "Calculates the offsets and apply them, then the temporary material is removed" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph = mmd_root.material_morphs[mmd_root.active_morph] mat_data = morph.data[morph.active_data] if not mat_data.related_mesh: self.report({"ERROR"}, "You need to choose a Related Mesh first") return {"CANCELLED"} meshObj = FnModel.find_mesh_object_by_name(morph.id_data, mat_data.related_mesh) if meshObj is None: self.report({"ERROR"}, "The model mesh can't be found") return {"CANCELLED"} try: work_mat_name = mat_data.material + "_temp" work_mat, base_mat = FnMaterial.swap_materials(meshObj, work_mat_name, mat_data.material) except MaterialNotFoundError: self.report({"ERROR"}, "Material not found") return {"CANCELLED"} base_mmd_mat = base_mat.mmd_material work_mmd_mat = work_mat.mmd_material if mat_data.offset_type == "MULT": try: diffuse_offset = divide_vector_components(work_mmd_mat.diffuse_color, base_mmd_mat.diffuse_color) + [special_division(work_mmd_mat.alpha, base_mmd_mat.alpha)] specular_offset = divide_vector_components(work_mmd_mat.specular_color, base_mmd_mat.specular_color) edge_offset = divide_vector_components(work_mmd_mat.edge_color, base_mmd_mat.edge_color) mat_data.diffuse_color = diffuse_offset mat_data.specular_color = specular_offset mat_data.shininess = special_division(work_mmd_mat.shininess, base_mmd_mat.shininess) mat_data.ambient_color = divide_vector_components(work_mmd_mat.ambient_color, base_mmd_mat.ambient_color) mat_data.edge_color = edge_offset mat_data.edge_weight = special_division(work_mmd_mat.edge_weight, base_mmd_mat.edge_weight) except ZeroDivisionError: mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD except ValueError: self.report({"ERROR"}, "An unexpected error happened") # We should stop on our tracks and re-raise the exception raise if mat_data.offset_type == "ADD": diffuse_offset = list(work_mmd_mat.diffuse_color - base_mmd_mat.diffuse_color) + [work_mmd_mat.alpha - base_mmd_mat.alpha] specular_offset = list(work_mmd_mat.specular_color - base_mmd_mat.specular_color) edge_offset = Vector(work_mmd_mat.edge_color) - Vector(base_mmd_mat.edge_color) mat_data.diffuse_color = diffuse_offset mat_data.specular_color = specular_offset mat_data.shininess = work_mmd_mat.shininess - base_mmd_mat.shininess mat_data.ambient_color = work_mmd_mat.ambient_color - base_mmd_mat.ambient_color mat_data.edge_color = list(edge_offset) mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat) return {"FINISHED"} class CreateWorkMaterial(bpy.types.Operator): bl_idname = "mmd_tools.create_work_material" bl_label = "Create Work Material" bl_description = "Creates a temporary material to edit this offset" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph = mmd_root.material_morphs[mmd_root.active_morph] mat_data = morph.data[morph.active_data] if not mat_data.related_mesh: self.report({"ERROR"}, "You need to choose a Related Mesh first") return {"CANCELLED"} meshObj = FnModel.find_mesh_object_by_name(morph.id_data, mat_data.related_mesh) if meshObj is None: self.report({"ERROR"}, "The model mesh can't be found") return {"CANCELLED"} base_mat = meshObj.data.materials.get(mat_data.material, None) if base_mat is None: self.report({"ERROR"}, f'Material "{mat_data.material}" not found') return {"CANCELLED"} work_mat_name = base_mat.name + "_temp" if work_mat_name in bpy.data.materials: self.report({"ERROR"}, f'Temporary material "{work_mat_name}" is in use') return {"CANCELLED"} work_mat = base_mat.copy() work_mat.name = work_mat_name meshObj.data.materials.append(work_mat) FnMaterial.swap_materials(meshObj, base_mat.name, work_mat.name) base_mmd_mat = base_mat.mmd_material work_mmd_mat = work_mat.mmd_material work_mmd_mat.material_id = -1 # Apply the offsets if mat_data.offset_type == "MULT": diffuse_offset = multiply_vector_components(base_mmd_mat.diffuse_color, mat_data.diffuse_color[0:3]) specular_offset = multiply_vector_components(base_mmd_mat.specular_color, mat_data.specular_color) edge_offset = multiply_vector_components(base_mmd_mat.edge_color, mat_data.edge_color) ambient_offset = multiply_vector_components(base_mmd_mat.ambient_color, mat_data.ambient_color) work_mmd_mat.diffuse_color = diffuse_offset work_mmd_mat.alpha *= mat_data.diffuse_color[3] work_mmd_mat.specular_color = specular_offset work_mmd_mat.shininess *= mat_data.shininess work_mmd_mat.ambient_color = ambient_offset work_mmd_mat.edge_color = edge_offset work_mmd_mat.edge_weight *= mat_data.edge_weight elif mat_data.offset_type == "ADD": diffuse_offset = Vector(base_mmd_mat.diffuse_color) + Vector(mat_data.diffuse_color[0:3]) specular_offset = Vector(base_mmd_mat.specular_color) + Vector(mat_data.specular_color) edge_offset = Vector(base_mmd_mat.edge_color) + Vector(mat_data.edge_color) ambient_offset = Vector(base_mmd_mat.ambient_color) + Vector(mat_data.ambient_color) work_mmd_mat.diffuse_color = list(diffuse_offset) work_mmd_mat.alpha += mat_data.diffuse_color[3] work_mmd_mat.specular_color = list(specular_offset) work_mmd_mat.shininess += mat_data.shininess work_mmd_mat.ambient_color = list(ambient_offset) work_mmd_mat.edge_color = list(edge_offset) work_mmd_mat.edge_weight += mat_data.edge_weight return {"FINISHED"} class ClearTempMaterials(bpy.types.Operator): bl_idname = "mmd_tools.clear_temp_materials" bl_label = "Clear Temp Materials" bl_description = "Clears all the temporary materials" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None for meshObj in FnModel.iterate_mesh_objects(root): def __pre_remove(m, meshObj=meshObj): if m and "_temp" in m.name: base_mat_name = m.name.split("_temp")[0] try: FnMaterial.swap_materials(meshObj, m.name, base_mat_name) return True except MaterialNotFoundError: self.report({"WARNING"}, f"Base material for {m.name} was not found") return False FnMaterial.clean_materials(meshObj, can_remove=__pre_remove) return {"FINISHED"} class ViewBoneMorph(bpy.types.Operator): bl_idname = "mmd_tools.view_bone_morph" bl_label = "View Bone Morph" bl_description = "View the result of active bone morph" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None mmd_root = root.mmd_root armature = FnModel.find_armature_object(root) utils.selectSingleBone(context, armature, None, True) morph = mmd_root.bone_morphs[mmd_root.active_morph] for morph_data in morph.data: p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None) if p_bone: p_bone.select = True mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4() mtx.translation = p_bone.location + morph_data.location p_bone.matrix_basis = mtx return {"FINISHED"} class ClearBoneMorphView(bpy.types.Operator): bl_idname = "mmd_tools.clear_bone_morph_view" bl_label = "Clear Bone Morph View" bl_description = "Reset transforms of all bones to their default values" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None armature = FnModel.find_armature_object(root) for p_bone in armature.pose.bones: p_bone.matrix_basis.identity() return {"FINISHED"} class ApplyBoneMorph(bpy.types.Operator): bl_idname = "mmd_tools.apply_bone_morph" bl_label = "Apply Bone Morph" bl_description = "Apply current pose to active bone morph" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None armature = FnModel.find_armature_object(root) mmd_root = root.mmd_root morph = mmd_root.bone_morphs[mmd_root.active_morph] morph.data.clear() morph.active_data = 0 for p_bone in armature.pose.bones: if p_bone.location.length > 0 or p_bone.matrix_basis.decompose()[1].angle > 0: item = morph.data.add() item.bone = p_bone.name item.location = p_bone.location item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() p_bone.select = True else: p_bone.select = False return {"FINISHED"} class SelectRelatedBone(bpy.types.Operator): bl_idname = "mmd_tools.select_bone_morph_offset_bone" bl_label = "Select Related Bone" bl_description = "Select the bone assigned to this offset in the armature" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None mmd_root = root.mmd_root armature = FnModel.find_armature_object(root) morph = mmd_root.bone_morphs[mmd_root.active_morph] morph_data = morph.data[morph.active_data] utils.selectSingleBone(context, armature, morph_data.bone) return {"FINISHED"} class EditBoneOffset(bpy.types.Operator): bl_idname = "mmd_tools.edit_bone_morph_offset" bl_label = "Edit Related Bone" bl_description = "Applies the location and rotation of this offset to the bone" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None mmd_root = root.mmd_root armature = FnModel.find_armature_object(root) morph = mmd_root.bone_morphs[mmd_root.active_morph] morph_data = morph.data[morph.active_data] p_bone = armature.pose.bones[morph_data.bone] mtx = Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix().to_4x4() mtx.translation = morph_data.location p_bone.matrix_basis = mtx utils.selectSingleBone(context, armature, p_bone.name) return {"FINISHED"} class ApplyBoneOffset(bpy.types.Operator): bl_idname = "mmd_tools.apply_bone_morph_offset" bl_label = "Apply Bone Morph Offset" bl_description = "Stores the current bone location and rotation into this offset" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None mmd_root = root.mmd_root armature = FnModel.find_armature_object(root) assert armature is not None morph = mmd_root.bone_morphs[mmd_root.active_morph] morph_data = morph.data[morph.active_data] p_bone = armature.pose.bones[morph_data.bone] morph_data.location = p_bone.location morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() return {"FINISHED"} class ViewUVMorph(bpy.types.Operator): bl_idname = "mmd_tools.view_uv_morph" bl_label = "View UV Morph" bl_description = "View the result of active UV morph on current mesh object" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None mmd_root = root.mmd_root meshes = tuple(FnModel.iterate_mesh_objects(root)) if len(meshes) == 1: obj = meshes[0] elif obj not in meshes: self.report({"ERROR"}, "Please select a mesh object") return {"CANCELLED"} meshObj = obj bpy.ops.mmd_tools.clear_uv_morph_view() selected = meshObj.select_get() with bpyutils.select_object(meshObj): mesh = cast("bpy.types.Mesh", meshObj.data) morph = mmd_root.uv_morphs[mmd_root.active_morph] uv_textures = mesh.uv_layers base_uv_layers = [layer for layer in mesh.uv_layers if not layer.name.startswith("_")] if morph.uv_index >= len(base_uv_layers): self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index) return {"CANCELLED"} uv_layer_name = base_uv_layers[morph.uv_index].name if morph.uv_index == 0 or uv_textures.active.name not in {uv_layer_name, "_" + uv_layer_name}: uv_textures.active = uv_textures[uv_layer_name] uv_layer_name = uv_textures.active.name uv_tex = uv_textures.new(name=f"__uv.{uv_layer_name}") if uv_tex is None: self.report({"ERROR"}, "Failed to create a temporary uv layer") return {"CANCELLED"} offsets = FnMorph.get_uv_morph_offset_map(meshObj, morph).items() offsets = {k: getattr(Vector(v), "zw" if uv_layer_name.startswith("_") else "xy") for k, v in offsets} if len(offsets) > 0: base_uv_data = mesh.uv_layers.active.data temp_uv_data = mesh.uv_layers[uv_tex.name].data for i, loop in enumerate(mesh.loops): select = temp_uv_data[i].select = loop.vertex_index in offsets if select: temp_uv_data[i].uv = base_uv_data[i].uv + offsets[loop.vertex_index] uv_textures.active = uv_tex uv_tex.active_render = True meshObj.hide_set(False) meshObj.select_set(selected) return {"FINISHED"} class ClearUVMorphView(bpy.types.Operator): bl_idname = "mmd_tools.clear_uv_morph_view" bl_label = "Clear UV Morph View" bl_description = "Clear all temporary data of UV morphs" bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) assert root is not None for m in FnModel.iterate_mesh_objects(root): mesh = m.data uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers) for t in reversed(uv_textures): if t.name.startswith("__uv."): uv_textures.remove(t) if len(uv_textures) > 0: uv_textures[0].active_render = True uv_textures.active_index = 0 animation_data = mesh.animation_data if animation_data: nla_tracks = animation_data.nla_tracks for t in reversed(nla_tracks): if t.name.startswith("__uv."): nla_tracks.remove(t) if animation_data.action and animation_data.action.name.startswith("__uv."): animation_data.action = None if animation_data.action is None and len(nla_tracks) == 0: mesh.animation_data_clear() for act in reversed(bpy.data.actions): if act.name.startswith("__uv.") and act.users < 1: bpy.data.actions.remove(act) return {"FINISHED"} class EditUVMorph(bpy.types.Operator): bl_idname = "mmd_tools.edit_uv_morph" bl_label = "Edit UV Morph" bl_description = "Edit UV morph on a temporary UV layer (use UV Editor to edit the result)" bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): obj = context.active_object if obj is None or obj.type != "MESH": return False active_uv_layer = obj.data.uv_layers.active return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.") def execute(self, context): obj = context.active_object meshObj = obj selected = meshObj.select_get() with bpyutils.select_object(meshObj): mesh = cast("bpy.types.Mesh", meshObj.data) bpy.ops.object.mode_set(mode="EDIT") bpy.ops.mesh.select_mode(type="VERT", action="ENABLE") bpy.ops.mesh.reveal() # unhide all vertices bpy.ops.mesh.select_all(action="DESELECT") bpy.ops.object.mode_set(mode="OBJECT") vertices = mesh.vertices for loop, d in zip(mesh.loops, mesh.uv_layers.active.data, strict=False): if d.select: vertices[loop.vertex_index].select = True polygons = mesh.polygons polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active) bpy.ops.object.mode_set(mode="EDIT") meshObj.select_set(selected) return {"FINISHED"} class ApplyUVMorph(bpy.types.Operator): bl_idname = "mmd_tools.apply_uv_morph" bl_label = "Apply UV Morph" bl_description = "Calculate the UV offsets of selected vertices and apply to active UV morph" bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): obj = context.active_object if obj is None or obj.type != "MESH": return False active_uv_layer = obj.data.uv_layers.active return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.") def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root meshObj = obj selected = meshObj.select_get() with bpyutils.select_object(meshObj): mesh = cast("bpy.types.Mesh", meshObj.data) morph = mmd_root.uv_morphs[mmd_root.active_morph] base_uv_name = mesh.uv_layers.active.name[5:] if base_uv_name not in mesh.uv_layers: self.report({"ERROR"}, f' * UV map "{base_uv_name}" not found') return {"CANCELLED"} base_uv_data = mesh.uv_layers[base_uv_name].data temp_uv_data = mesh.uv_layers.active.data axis_type = "ZW" if base_uv_name.startswith("_") else "XY" __OffsetData = namedtuple("OffsetData", "index, offset") offsets = {} vertices = mesh.vertices for loop, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data, strict=False): if vertices[loop.vertex_index].select and loop.vertex_index not in offsets: dx, dy = i1.uv - i0.uv if abs(dx) > 0.0001 or abs(dy) > 0.0001: offsets[loop.vertex_index] = __OffsetData(loop.vertex_index, (dx, dy, dx, dy)) FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type) morph.data_type = "VERTEX_GROUP" meshObj.select_set(selected) return {"FINISHED"} class CleanDuplicatedMaterialMorphs(bpy.types.Operator): bl_idname = "mmd_tools.clean_duplicated_material_morphs" bl_label = "Clean Duplicated Material Morphs" bl_description = "Clean duplicated material morphs" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): root = FnModel.find_root_object(context.active_object) return root is not None def execute(self, context: bpy.types.Context): mmd_root_object = FnModel.find_root_object(context.active_object) FnMorph.clean_duplicated_material_morphs(mmd_root_object) return {"FINISHED"} class ConvertBoneMorphToVertexMorph(bpy.types.Operator): bl_idname = "mmd_tools.convert_bone_morph_to_vertex_morph" bl_label = "Convert To Vertex Morph" bl_description = "Convert a bone morph into a single vertex morph by applying the bone transformations.\nIf a corresponding vertex morph already exists, it will be updated." bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): root = FnModel.find_root_object(context.active_object) if root is None: return False mmd_root = root.mmd_root if mmd_root.active_morph_type != "bone_morphs": return False morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph) return morph is not None and len(morph.data) > 0 def execute(self, context): obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root # Get the active bone morph bone_morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph) if bone_morph is None: self.report({"ERROR"}, "No active bone morph") return {"CANCELLED"} original_name = bone_morph.name target_name = original_name # Add 'B' suffix if necessary if not original_name.endswith("B"): bone_morph.name = original_name + "B" target_name = original_name else: # If already has B suffix, use name without B target_name = original_name[:-1] try: # Step 1: import from ..core.model import Model rig = Model(root) # Ensure morph slider is bound bpy.ops.mmd_tools.morph_slider_setup(type="BIND") # Re-obtain placeholder object placeholder_obj = rig.morph_slider.placeholder() if placeholder_obj is None or placeholder_obj.data.shape_keys is None: self.report({"ERROR"}, "Failed to create morph slider system") return {"CANCELLED"} shape_keys = placeholder_obj.data.shape_keys key_blocks = shape_keys.key_blocks # Step 2: Check if target bone morph exists current_morph_name = bone_morph.name if current_morph_name not in key_blocks: self.report({"ERROR"}, f"Bone morph '{current_morph_name}' not found in morph sliders") return {"CANCELLED"} # Step 3: Save all current morph values original_values = {} for key_block in key_blocks: if key_block.name != "--- morph sliders ---": original_values[key_block.name] = key_block.value # Step 4: Set all morphs to 0 for key_block in key_blocks: if key_block.name != "--- morph sliders ---": key_block.value = 0 # Step 5: Set target bone morph to 1.0 key_blocks[current_morph_name].value = 1.0 # Step 6: Use Armature Modifier's "Apply as Shape Key" functionality created_shape_keys = [] for mesh_obj in FnModel.iterate_mesh_objects(root): # Switch to this mesh object context.view_layer.objects.active = mesh_obj # Ensure mesh object has shape keys if mesh_obj.data.shape_keys is None: mesh_obj.shape_key_add(name="Basis", from_mix=False) # Delete existing shape key with same name if target_name in mesh_obj.data.shape_keys.key_blocks: idx = mesh_obj.data.shape_keys.key_blocks.find(target_name) if idx >= 0: mesh_obj.active_shape_key_index = idx bpy.ops.object.shape_key_remove() # Find armature modifier armature_modifier = None for modifier in mesh_obj.modifiers: if modifier.type == "ARMATURE": armature_modifier = modifier break if armature_modifier is None: self.report({"WARNING"}, f"No armature modifier found on mesh '{mesh_obj.name}'") continue # Use Apply as Shape Key functionality, keeping the modifier bpy.ops.object.modifier_apply_as_shapekey(modifier=armature_modifier.name, keep_modifier=True) # Rename the newly created shape key to target name shape_key_blocks = mesh_obj.data.shape_keys.key_blocks new_shape_key = shape_key_blocks[-1] # Latest created shape key new_shape_key.name = target_name new_shape_key.value = 0.0 # Set to 0 to avoid double effect created_shape_keys.append((mesh_obj.name, target_name)) self.report({"INFO"}, f"Created shape key '{target_name}' on mesh '{mesh_obj.name}'") # Step 7: Restore all original morph values for key_name, original_value in original_values.items(): if key_name in key_blocks: key_blocks[key_name].value = original_value # Step 8: Create or update vertex morph entry vertex_morph_exists = False for i, morph in enumerate(mmd_root.vertex_morphs): if morph.name == target_name: vertex_morph_exists = True mmd_root.active_morph_type = "vertex_morphs" mmd_root.active_morph = i break if not vertex_morph_exists: mmd_root.active_morph_type = "vertex_morphs" morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph) morph.name = target_name # Step 9: Add to facial expression display frame facial_frame = None for frame in mmd_root.display_item_frames: if frame.name == "表情": facial_frame = frame break if facial_frame: morph_exists_in_frame = False for item in facial_frame.data: if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs": morph_exists_in_frame = True break if not morph_exists_in_frame: new_item = facial_frame.data.add() new_item.type = "MORPH" new_item.morph_type = "vertex_morphs" new_item.name = target_name facial_frame.active_item = len(facial_frame.data) - 1 for i, frame in enumerate(mmd_root.display_item_frames): if frame.name == "表情": mmd_root.active_display_item_frame = i break # UNBIND bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND") # Success message shape_key_info = ", ".join([f"{mesh}:{key}" for mesh, key in created_shape_keys]) self.report({"INFO"}, f"Successfully converted bone morph '{original_name}' to vertex morph '{target_name}'. Created shape keys: {shape_key_info}") except Exception as e: self.report({"ERROR"}, f"Error during conversion: {str(e)}") return {"CANCELLED"} return {"FINISHED"} class ConvertGroupMorphToVertexMorph(bpy.types.Operator): bl_idname = "mmd_tools.convert_group_morph_to_vertex_morph" bl_label = "Convert To Vertex Morph" bl_description = "Convert a group morph into a single vertex morph by merging only the vertex morphs within the group.\nIf a corresponding vertex morph already exists, it will be updated." bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): root = FnModel.find_root_object(context.active_object) if root is None: return False mmd_root = root.mmd_root if mmd_root.active_morph_type != "group_morphs": return False morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph) return morph is not None and len(morph.data) > 0 def execute(self, context): bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND") obj = context.active_object root = FnModel.find_root_object(obj) mmd_root = root.mmd_root # Get the active group morph group_morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph) if group_morph is None: self.report({"ERROR"}, "No active group morph") return {"CANCELLED"} # Check if the group morph contains any vertex morphs to convert has_vertex_morphs = False for offset in group_morph.data: if offset.morph_type == "vertex_morphs": has_vertex_morphs = True break if not has_vertex_morphs: self.report({"ERROR"}, "The group morph does not contain any vertex morphs to convert") return {"CANCELLED"} original_name = group_morph.name target_name = original_name # Add 'G' suffix if necessary if not original_name.endswith("G"): group_morph.name = original_name + "G" target_name = original_name else: # If already has G suffix, use name without G target_name = original_name[:-1] # First, reset all shape keys to zero for obj in FnModel.iterate_mesh_objects(root): if obj.data.shape_keys: for kb in obj.data.shape_keys.key_blocks: kb.value = 0 # Apply only the vertex morphs from the group morph for offset in group_morph.data: if offset.morph_type == "vertex_morphs": # Find the vertex morph by name vertex_morph = getattr(root.mmd_root, offset.morph_type).get(offset.name) if vertex_morph: # Apply this morph at the specified factor for obj in FnModel.iterate_mesh_objects(root): if obj.data.shape_keys: kb = obj.data.shape_keys.key_blocks.get(offset.name) if kb: kb.value = offset.factor # Now add a new shape key from mix for each mesh for obj in FnModel.iterate_mesh_objects(root): if obj.data.shape_keys: # Make this the active object context.view_layer.objects.active = obj # Remove existing shape key if it exists if target_name in obj.data.shape_keys.key_blocks: idx = obj.data.shape_keys.key_blocks.find(target_name) if idx >= 0: obj.active_shape_key_index = idx bpy.ops.object.shape_key_remove() # Add shape key from mix bpy.ops.object.shape_key_add(from_mix=True) # Rename the newly created shape key new_key = obj.data.shape_keys.key_blocks[-1] new_key.name = target_name # Check if a vertex morph with the target name already exists vertex_morph_exists = False for i, morph in enumerate(mmd_root.vertex_morphs): if morph.name == target_name: vertex_morph_exists = True mmd_root.active_morph_type = "vertex_morphs" mmd_root.active_morph = i break # If not, create a new vertex morph if not vertex_morph_exists: # Switch to vertex morphs panel mmd_root.active_morph_type = "vertex_morphs" # Add new vertex morph morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph) morph.name = target_name # Add the new vertex morph to the facial display frame facial_frame = None for frame in mmd_root.display_item_frames: if frame.name == "表情": # This is the facial display frame facial_frame = frame break if facial_frame: # Check if this morph is already in the facial frame morph_exists_in_frame = False for item in facial_frame.data: if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs": morph_exists_in_frame = True break # If not, add it if not morph_exists_in_frame: new_item = facial_frame.data.add() new_item.type = "MORPH" new_item.morph_type = "vertex_morphs" new_item.name = target_name # Make this the active item in the facial frame facial_frame.active_item = len(facial_frame.data) - 1 # Set the facial frame as active for i, frame in enumerate(mmd_root.display_item_frames): if frame.name == "表情": mmd_root.active_display_item_frame = i break # Reset all shape keys for obj in FnModel.iterate_mesh_objects(root): if obj.data.shape_keys: for kb in obj.data.shape_keys.key_blocks: kb.value = 0 self.report({"INFO"}, f"Successfully converted vertex morphs in group to vertex morph '{target_name}' and added to facial display frame") return {"FINISHED"}