Files
Avatar-Toolkit/core/mmd/operators/morph.py
T
Yusarina a929f68ad4 Holy shit this was a pain
- Truly fixes PMX Import lol, i messed up completely
- Updated MMD Tools to use Cats One
2025-11-19 06:35:06 +00:00

1097 lines
44 KiB
Python

# 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"}