Holy shit this was a pain
- Truly fixes PMX Import lol, i messed up completely - Updated MMD Tools to use Cats One
This commit is contained in:
+402
-112
@@ -1,30 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
# Copyright 2015 MMD Tools authors
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
|
||||
from collections import namedtuple
|
||||
from typing import Optional, cast
|
||||
|
||||
import bpy
|
||||
from mathutils import Quaternion, Vector
|
||||
|
||||
from ..core.model import FnModel
|
||||
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
|
||||
from ....logging_setup import logger
|
||||
|
||||
|
||||
# Util functions
|
||||
def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
|
||||
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):
|
||||
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
|
||||
@@ -34,17 +30,17 @@ def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float
|
||||
return result
|
||||
|
||||
|
||||
def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
|
||||
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):
|
||||
for v1, v2 in zip(vec1, vec2, strict=False):
|
||||
result.append(v1 * v2)
|
||||
return result
|
||||
|
||||
|
||||
def special_division(n1: float, n2: float) -> float:
|
||||
"""This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
|
||||
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
|
||||
@@ -59,7 +55,7 @@ class AddMorph(bpy.types.Operator):
|
||||
bl_description = "Add a morph item to active morph list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -69,7 +65,6 @@ class AddMorph(bpy.types.Operator):
|
||||
morph.name = "New Morph"
|
||||
if morph_type.startswith("uv"):
|
||||
morph.data_type = "VERTEX_GROUP"
|
||||
logger.debug(f"Added new morph of type {morph_type}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -86,7 +81,7 @@ class RemoveMorph(bpy.types.Operator):
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -101,21 +96,19 @@ class RemoveMorph(bpy.types.Operator):
|
||||
if self.all:
|
||||
morphs.clear()
|
||||
mmd_root.active_morph = 0
|
||||
logger.debug(f"Removed all morphs of type {morph_type}")
|
||||
else:
|
||||
morphs.remove(mmd_root.active_morph)
|
||||
mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
|
||||
logger.debug(f"Removed morph at index {mmd_root.active_morph} of type {morph_type}")
|
||||
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"
|
||||
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: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -124,7 +117,6 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
||||
mmd_root.active_morph,
|
||||
self.type,
|
||||
)
|
||||
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -134,7 +126,7 @@ class CopyMorph(bpy.types.Operator):
|
||||
bl_description = "Make a copy of active morph in the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -146,7 +138,7 @@ class CopyMorph(bpy.types.Operator):
|
||||
if morph is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer())
|
||||
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):
|
||||
@@ -161,7 +153,6 @@ class CopyMorph(bpy.types.Operator):
|
||||
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
|
||||
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -171,17 +162,14 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
return False
|
||||
return root is not None and root.mmd_root.active_morph_type == "bone_morphs"
|
||||
|
||||
return root.mmd_root.active_morph_type == "bone_morphs"
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
|
||||
logger.info("Overwrote bone morphs from active action pose")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -191,7 +179,7 @@ class AddMorphOffset(bpy.types.Operator):
|
||||
bl_description = "Add a morph offset item to the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -216,7 +204,6 @@ class AddMorphOffset(bpy.types.Operator):
|
||||
item.location = pose_bone.location
|
||||
item.rotation = pose_bone.rotation_quaternion
|
||||
|
||||
logger.debug(f"Added morph offset to {morph_type}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -233,7 +220,7 @@ class RemoveMorphOffset(bpy.types.Operator):
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -250,21 +237,17 @@ class RemoveMorphOffset(bpy.types.Operator):
|
||||
if morph_type.startswith("vertex"):
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.remove_shape_key(obj, morph.name)
|
||||
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
|
||||
return {"FINISHED"}
|
||||
elif morph_type.startswith("uv"):
|
||||
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)
|
||||
logger.debug(f"Removed all UV morph offsets for {morph.name}")
|
||||
return {"FINISHED"}
|
||||
morph.data.clear()
|
||||
morph.active_data = 0
|
||||
logger.debug(f"Cleared all morph offsets for {morph.name}")
|
||||
else:
|
||||
morph.data.remove(morph.active_data)
|
||||
morph.active_data = max(0, morph.active_data - 1)
|
||||
logger.debug(f"Removed morph offset at index {morph.active_data}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -280,7 +263,7 @@ class InitMaterialOffset(bpy.types.Operator):
|
||||
default=0,
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -292,7 +275,6 @@ class InitMaterialOffset(bpy.types.Operator):
|
||||
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
|
||||
logger.debug(f"Initialized material offset with value {val}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -302,7 +284,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -340,7 +322,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
|
||||
except ZeroDivisionError:
|
||||
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
|
||||
logger.warning("Zero division detected, switching to ADD offset type")
|
||||
except ValueError:
|
||||
self.report({"ERROR"}, "An unexpected error happened")
|
||||
# We should stop on our tracks and re-raise the exception
|
||||
@@ -358,7 +339,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
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)
|
||||
logger.info(f"Applied material offset for {mat_data.material}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -368,7 +348,7 @@ class CreateWorkMaterial(bpy.types.Operator):
|
||||
bl_description = "Creates a temporary material to edit this offset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -385,12 +365,12 @@ class CreateWorkMaterial(bpy.types.Operator):
|
||||
|
||||
base_mat = meshObj.data.materials.get(mat_data.material, None)
|
||||
if base_mat is None:
|
||||
self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material)
|
||||
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"}, 'Temporary material "%s" is in use' % work_mat_name)
|
||||
self.report({"ERROR"}, f'Temporary material "{work_mat_name}" is in use')
|
||||
return {"CANCELLED"}
|
||||
|
||||
work_mat = base_mat.copy()
|
||||
@@ -427,7 +407,6 @@ class CreateWorkMaterial(bpy.types.Operator):
|
||||
work_mmd_mat.edge_color = list(edge_offset)
|
||||
work_mmd_mat.edge_weight += mat_data.edge_weight
|
||||
|
||||
logger.info(f"Created work material {work_mat_name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -437,24 +416,23 @@ class ClearTempMaterials(bpy.types.Operator):
|
||||
bl_description = "Clears all the temporary materials"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
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: Optional[bpy.types.Material]) -> bool:
|
||||
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"}, "Base material for %s was not found" % m.name)
|
||||
self.report({"WARNING"}, f"Base material for {m.name} was not found")
|
||||
return False
|
||||
|
||||
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
|
||||
logger.info("Cleared all temporary materials")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -464,7 +442,7 @@ class ViewBoneMorph(bpy.types.Operator):
|
||||
bl_description = "View the result of active bone morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -475,12 +453,10 @@ class ViewBoneMorph(bpy.types.Operator):
|
||||
for morph_data in morph.data:
|
||||
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
|
||||
if p_bone:
|
||||
# Blender 5.0: use pose bone select property directly
|
||||
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
|
||||
logger.info(f"Viewing bone morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -490,14 +466,13 @@ class ClearBoneMorphView(bpy.types.Operator):
|
||||
bl_description = "Reset transforms of all bones to their default values"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
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()
|
||||
logger.info("Cleared bone morph view")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -507,7 +482,7 @@ class ApplyBoneMorph(bpy.types.Operator):
|
||||
bl_description = "Apply current pose to active bone morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -522,11 +497,9 @@ class ApplyBoneMorph(bpy.types.Operator):
|
||||
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()
|
||||
# Blender 5.0: use pose bone select property directly
|
||||
p_bone.select = True
|
||||
else:
|
||||
p_bone.select = False
|
||||
logger.info(f"Applied current pose to bone morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -536,7 +509,7 @@ class SelectRelatedBone(bpy.types.Operator):
|
||||
bl_description = "Select the bone assigned to this offset in the armature"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -545,7 +518,6 @@ class SelectRelatedBone(bpy.types.Operator):
|
||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||
morph_data = morph.data[morph.active_data]
|
||||
utils.selectSingleBone(context, armature, morph_data.bone)
|
||||
logger.debug(f"Selected bone: {morph_data.bone}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -555,7 +527,7 @@ class EditBoneOffset(bpy.types.Operator):
|
||||
bl_description = "Applies the location and rotation of this offset to the bone"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -568,7 +540,6 @@ class EditBoneOffset(bpy.types.Operator):
|
||||
mtx.translation = morph_data.location
|
||||
p_bone.matrix_basis = mtx
|
||||
utils.selectSingleBone(context, armature, p_bone.name)
|
||||
logger.debug(f"Edited bone offset for {p_bone.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -578,7 +549,7 @@ class ApplyBoneOffset(bpy.types.Operator):
|
||||
bl_description = "Stores the current bone location and rotation into this offset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -590,7 +561,6 @@ class ApplyBoneOffset(bpy.types.Operator):
|
||||
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()
|
||||
logger.debug(f"Applied bone offset for {p_bone.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -600,7 +570,7 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
bl_description = "View the result of active UV morph on current mesh object"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -618,11 +588,11 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
|
||||
selected = meshObj.select_get()
|
||||
with bpyutils.select_object(meshObj):
|
||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
||||
mesh = cast("bpy.types.Mesh", meshObj.data)
|
||||
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
||||
uv_textures = mesh.uv_layers
|
||||
|
||||
base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")]
|
||||
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"}
|
||||
@@ -632,7 +602,7 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
uv_textures.active = uv_textures[uv_layer_name]
|
||||
|
||||
uv_layer_name = uv_textures.active.name
|
||||
uv_tex = uv_textures.new(name="__uv.%s" % uv_layer_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"}
|
||||
@@ -642,17 +612,15 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
if len(offsets) > 0:
|
||||
base_uv_data = mesh.uv_layers.active.data
|
||||
temp_uv_data = mesh.uv_layers[uv_tex.name].data
|
||||
for i, l in enumerate(mesh.loops):
|
||||
# Blender 5.0+: UV selection is now stored in face-corner attributes
|
||||
# Skipping UV selection assignment as it's not critical for morph preview
|
||||
select = l.vertex_index in offsets
|
||||
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[l.vertex_index]
|
||||
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)
|
||||
logger.info(f"Viewing UV morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -662,24 +630,24 @@ class ClearUVMorphView(bpy.types.Operator):
|
||||
bl_description = "Clear all temporary data of UV morphs"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
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_layers = mesh.uv_layers
|
||||
for t in list(uv_layers): # Create a copy to iterate safely
|
||||
uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers)
|
||||
for t in reversed(uv_textures):
|
||||
if t.name.startswith("__uv."):
|
||||
uv_layers.remove(t)
|
||||
if len(uv_layers) > 0:
|
||||
# Only set active_index
|
||||
uv_layers.active_index = 0
|
||||
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 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."):
|
||||
@@ -687,10 +655,9 @@ class ClearUVMorphView(bpy.types.Operator):
|
||||
if animation_data.action is None and len(nla_tracks) == 0:
|
||||
mesh.animation_data_clear()
|
||||
|
||||
for act in bpy.data.actions:
|
||||
for act in reversed(bpy.data.actions):
|
||||
if act.name.startswith("__uv.") and act.users < 1:
|
||||
bpy.data.actions.remove(act)
|
||||
logger.info("Cleared UV morph view")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -701,20 +668,20 @@ class EditUVMorph(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
if obj.type != "MESH":
|
||||
if obj is None or obj.type != "MESH":
|
||||
return False
|
||||
active_uv_layer = obj.data.uv_layers.active
|
||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
||||
return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
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)
|
||||
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
|
||||
@@ -722,16 +689,15 @@ class EditUVMorph(bpy.types.Operator):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
vertices = mesh.vertices
|
||||
for l, d in zip(mesh.loops, mesh.uv_layers.active.data):
|
||||
for loop, d in zip(mesh.loops, mesh.uv_layers.active.data, strict=False):
|
||||
if d.select:
|
||||
vertices[l.vertex_index].select = True
|
||||
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)
|
||||
logger.info("Editing UV morph")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -742,14 +708,14 @@ class ApplyUVMorph(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
if obj.type != "MESH":
|
||||
if obj is None or obj.type != "MESH":
|
||||
return False
|
||||
active_uv_layer = obj.data.uv_layers.active
|
||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
||||
return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -757,34 +723,31 @@ class ApplyUVMorph(bpy.types.Operator):
|
||||
|
||||
selected = meshObj.select_get()
|
||||
with bpyutils.select_object(meshObj):
|
||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
||||
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"}, ' * UV map "%s" not found' % base_uv_name)
|
||||
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"
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
__OffsetData = namedtuple("OffsetData", "index, offset")
|
||||
offsets = {}
|
||||
vertices = mesh.vertices
|
||||
for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data):
|
||||
if vertices[l.vertex_index].select and l.vertex_index not in offsets:
|
||||
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[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy))
|
||||
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)
|
||||
logger.info(f"Applied UV morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -795,12 +758,339 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
def poll(cls, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
return root is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
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)
|
||||
logger.info("Cleaned duplicated material morphs")
|
||||
|
||||
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"}
|
||||
|
||||
Reference in New Issue
Block a user