f0bda259d3
- Replace mesh.uv_textures with mesh.uv_layers - Remove deprecated UV selection properties - Add compatibility helpers for UV vertex selection - Fix morph operators UV handling
807 lines
34 KiB
Python
807 lines
34 KiB
Python
# -*- 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.
|
|
|
|
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
|
|
|
|
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.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]:
|
|
if len(vec1) != len(vec2):
|
|
raise ValueError("Vectors should have the same number of components")
|
|
result = []
|
|
for v1, v2 in zip(vec1, vec2):
|
|
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: List[float], vec2: List[float]) -> List[float]:
|
|
if len(vec1) != len(vec2):
|
|
raise ValueError("Vectors should have the same number of components")
|
|
result = []
|
|
for v1, v2 in zip(vec1, vec2):
|
|
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"""
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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"
|
|
logger.debug(f"Added new morph of type {morph_type}")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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
|
|
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_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
def execute(self, context: bpy.types.Context) -> Set[str]:
|
|
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,
|
|
)
|
|
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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, "_tmp%s" % 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
|
|
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
|
|
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: bpy.types.Context) -> bool:
|
|
root = FnModel.find_root_object(context.active_object)
|
|
if root is None:
|
|
return False
|
|
|
|
return root.mmd_root.active_morph_type == "bone_morphs"
|
|
|
|
def execute(self, context: bpy.types.Context) -> Set[str]:
|
|
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"}
|
|
|
|
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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
|
|
|
|
logger.debug(f"Added morph offset to {morph_type}")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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)
|
|
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
|
|
return {"FINISHED"}
|
|
elif 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"}
|
|
|
|
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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
|
|
logger.debug(f"Initialized material offset with value {val}")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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
|
|
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
|
|
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)
|
|
logger.info(f"Applied material offset for {mat_data.material}")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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"}, 'Material "%s" not found' % mat_data.material)
|
|
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)
|
|
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
|
|
|
|
logger.info(f"Created work material {work_mat_name}")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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:
|
|
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)
|
|
return False
|
|
|
|
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
|
|
logger.info("Cleared all temporary materials")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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:
|
|
# 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"}
|
|
|
|
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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"}
|
|
|
|
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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()
|
|
# 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"}
|
|
|
|
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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)
|
|
logger.debug(f"Selected bone: {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: bpy.types.Context) -> Set[str]:
|
|
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)
|
|
logger.debug(f"Edited bone offset for {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: bpy.types.Context) -> Set[str]:
|
|
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()
|
|
logger.debug(f"Applied bone offset for {p_bone.name}")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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 = [l for l in mesh.uv_layers if not l.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="__uv.%s" % 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, 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
|
|
if select:
|
|
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index]
|
|
|
|
uv_textures.active = uv_tex
|
|
meshObj.hide_set(False)
|
|
meshObj.select_set(selected)
|
|
logger.info(f"Viewing UV morph: {morph.name}")
|
|
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: bpy.types.Context) -> Set[str]:
|
|
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
|
|
if t.name.startswith("__uv."):
|
|
uv_layers.remove(t)
|
|
if len(uv_layers) > 0:
|
|
# Only set active_index
|
|
uv_layers.active_index = 0
|
|
|
|
animation_data = mesh.animation_data
|
|
if animation_data:
|
|
nla_tracks = animation_data.nla_tracks
|
|
for t in 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 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"}
|
|
|
|
|
|
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: bpy.types.Context) -> bool:
|
|
obj = context.active_object
|
|
if obj.type != "MESH":
|
|
return False
|
|
active_uv_layer = obj.data.uv_layers.active
|
|
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
|
|
|
def execute(self, context: bpy.types.Context) -> Set[str]:
|
|
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 l, d in zip(mesh.loops, mesh.uv_layers.active.data):
|
|
if d.select:
|
|
vertices[l.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"}
|
|
|
|
|
|
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: bpy.types.Context) -> bool:
|
|
obj = context.active_object
|
|
if obj.type != "MESH":
|
|
return False
|
|
active_uv_layer = obj.data.uv_layers.active
|
|
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
|
|
|
def execute(self, context: bpy.types.Context) -> Set[str]:
|
|
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"}, ' * UV map "%s" not found' % base_uv_name)
|
|
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:
|
|
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))
|
|
|
|
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"}
|
|
|
|
|
|
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: bpy.types.Context) -> bool:
|
|
return FnModel.find_root_object(context.active_object) is not None
|
|
|
|
def execute(self, context: bpy.types.Context) -> Set[str]:
|
|
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"}
|