Files
Yusarina cfe760e8df Updated Operations and Properties
- Updated Operations and Properties with tpying and logging.

I have not updated translation files, this is because i want to gut MMD Tools system and replace it with our own, however I want to make MMD Tools more simple and ajust it to our needs only. This is going to take a while and my aim for this is Alpha 4, also the MMD Translation system hurt my head....

- Fixes a couple of bugs as well, with quick access and the PMX importer.
2025-04-23 00:43:38 +01:00

355 lines
15 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.
import itertools
from operator import itemgetter
from typing import Dict, List, Optional, Set, Tuple, Any
import bmesh
import bpy
import numpy as np
import numpy.typing as npt
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
from ..bpyutils import FnContext
from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MessageException(Exception):
"""Class for error with message."""
class ModelJoinByBonesOperator(bpy.types.Operator):
bl_idname = "mmd_tools.model_join_by_bones"
bl_label = "Model Join by Bones"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.EnumProperty(
name="Join Type",
items=[
("CONNECTED", "Connected", ""),
("OFFSET", "Keep Offset", ""),
],
default="OFFSET",
)
@classmethod
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
if context.mode != "POSE":
return False
if active_object is None:
return False
if active_object.type != "ARMATURE":
return False
if len(list(filter(lambda o: o.type == "ARMATURE", context.selected_objects))) < 2:
return False
return len(context.selected_pose_bones) > 0
def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting model join by bones operation")
self.join(context)
logger.info("Model join by bones completed successfully")
except MessageException as ex:
logger.error(f"Model join by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def join(self, context: Context) -> None:
bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_object)
child_root_objects = {FnModel.find_root_object(o) for o in context.selected_objects}
child_root_objects.remove(parent_root_object)
if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("No MMD Models selected")
logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}")
with FnContext.temp_override_active_layer_collection(context, parent_root_object):
FnModel.join_models(parent_root_object, child_root_objects)
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.armature.parent_set(type="OFFSET")
# Connect child bones
if self.join_type == "CONNECTED":
parent_edit_bone: EditBone = context.active_bone
child_edit_bones: Set[EditBone] = set(context.selected_bones)
child_edit_bones.remove(parent_edit_bone)
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
child_edit_bone: EditBone
for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True
bpy.ops.object.mode_set(mode="POSE")
class ModelSeparateByBonesOperator(bpy.types.Operator):
bl_idname = "mmd_tools.model_separate_by_bones"
bl_label = "Model Separate by Bones"
bl_options = {"REGISTER", "UNDO"}
separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True)
include_descendant_bones: bpy.props.BoolProperty(name="Include Descendant Bones", default=True)
weight_threshold: bpy.props.FloatProperty(name="Weight Threshold", default=0.001, min=0.0, max=1.0, precision=4, subtype="FACTOR")
boundary_joint_owner: bpy.props.EnumProperty(
name="Boundary Joint Owner",
items=[
("SOURCE", "Source Model", ""),
("DESTINATION", "Destination Model", ""),
],
default="DESTINATION",
)
@classmethod
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
if context.mode != "POSE":
return False
if active_object is None:
return False
if active_object.type != "ARMATURE":
return False
if FnModel.find_root_object(active_object) is None:
return False
return len(context.selected_pose_bones) > 0
def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting model separate by bones operation")
self.separate(context)
logger.info("Model separate by bones completed successfully")
except MessageException as ex:
logger.error(f"Model separate by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def separate(self, context: Context) -> None:
weight_threshold: float = self.weight_threshold
mmd_scale = 0.08
target_armature_object: Object = context.active_object
logger.debug(f"Target armature: {target_armature_object.name}")
bpy.ops.object.mode_set(mode="EDIT")
root_bones: Set[EditBone] = set(context.selected_bones)
logger.debug(f"Selected root bones: {len(root_bones)}")
if self.include_descendant_bones:
logger.debug("Including descendant bones")
for edit_bone in root_bones:
with context.temp_override(active_bone=edit_bone):
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
logger.debug(f"Total bones to separate: {len(separate_bones)}")
mmd_root_object: Object = FnModel.find_root_object(context.active_object)
mmd_model = Model(mmd_root_object)
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
mmd_model_mesh_objects = list(mesh_selection_result.keys())
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
# separate armature bones
separate_armature_object: Optional[Object]
if self.separate_armature:
logger.debug("Separating armature")
target_armature_object.select_set(True)
bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
if separate_armature_object:
logger.debug(f"Created separate armature: {separate_armature_object.name}")
bpy.ops.object.mode_set(mode="OBJECT")
# collect separate rigid bodies
separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate")
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints
separate_joints: Set[Object] = {
joint_object
for joint_object in mmd_model.joints()
if boundary_joint_owner_condition(
[
joint_object.rigid_body_constraint.object1 in separate_rigid_bodies,
joint_object.rigid_body_constraint.object2 in separate_rigid_bodies,
]
)
}
logger.debug(f"Found {len(separate_joints)} joints to separate")
separate_mesh_objects: Set[Object]
model2separate_mesh_objects: Dict[Object, Object]
if len(mmd_model_mesh_objects) == 0:
logger.debug("No mesh objects to separate")
separate_mesh_objects = set()
model2separate_mesh_objects = dict()
else:
# select meshes
logger.debug("Selecting meshes for separation")
obj: Object
for obj in context.view_layer.objects:
obj.select_set(obj in mmd_model_mesh_objects)
context.view_layer.objects.active = mmd_model_mesh_objects[0]
# separate mesh by selected vertices
logger.debug("Separating meshes by selected vertices")
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="SELECTED")
separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
bpy.ops.object.mode_set(mode="OBJECT")
logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects")
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
logger.debug(f"Creating new model with scale {mmd_scale}")
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False)
separate_model.initialDisplayFrames()
separate_root_object = separate_model.rootObject()
separate_root_object.matrix_world = mmd_root_object.matrix_world
separate_model_armature_object = separate_model.armature()
logger.debug(f"Created separate model with root: {separate_root_object.name}")
if self.separate_armature:
logger.debug("Joining separate armature to new model")
with context.temp_override(
active_object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
):
bpy.ops.object.join()
# add mesh
logger.debug("Parenting separate mesh objects to new model")
with context.temp_override(
object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object
logger.debug("Updating armature modifiers on separate meshes")
for separate_mesh in separate_mesh_objects:
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
if armature_modifier is None:
logger.debug(f"Creating new armature modifier for {separate_mesh.name}")
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
armature_modifier.object = separate_model_armature_object
logger.debug("Parenting rigid bodies to new model")
with context.temp_override(
object=separate_model.rigidGroupObject(),
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
logger.debug("Parenting joints to new model")
with context.temp_override(
object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# move separate objects to new collection
mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object)
assert mmd_layer_collection is not None
separate_layer_collection = FnContext.find_user_layer_collection_by_object(context, separate_root_object)
assert separate_layer_collection is not None
if mmd_layer_collection.name != separate_layer_collection.name:
logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}")
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
separate_layer_collection.collection.objects.link(separate_object)
mmd_layer_collection.collection.objects.unlink(separate_object)
logger.debug("Copying MMD root properties")
FnModel.copy_mmd_root(
separate_root_object,
mmd_root_object,
overwrite=True,
replace_name2values={
# replace related_mesh property values
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()}
},
)
def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]:
"""Select vertices weighted to the bones to be separated"""
logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}")
mesh2selected_vertex_count: Dict[Object, int] = dict()
target_bmesh: bmesh.types.BMesh = bmesh.new()
for mesh_object in mmd_model_mesh_objects:
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
mesh: Mesh = mesh_object.data
target_bmesh.from_mesh(mesh, face_normals=False)
target_bmesh.select_mode |= {"VERT"}
deform_layer = target_bmesh.verts.layers.deform.verify()
selected_vertex_count = 0
vert: bmesh.types.BMVert
for vert in target_bmesh.verts:
vert.select_set(False)
# Find the largest weight vertex group
weights = [(group_index, weight) for group_index, weight in vert[deform_layer].items() if vertex_groups[group_index].name in deform_bones]
weights.sort(key=lambda i: vertex_groups[i[0]].name in separate_bones, reverse=True)
weights.sort(key=itemgetter(1), reverse=True)
group_index, weight = next(iter(weights), (0, -1))
if weight < weight_threshold:
continue
if vertex_groups[group_index].name not in separate_bones:
continue
selected_vertex_count += 1
vert.select_set(True)
if selected_vertex_count > 0:
logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}")
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
target_bmesh.select_flush_mode()
target_bmesh.to_mesh(mesh)
target_bmesh.clear()
return mesh2selected_vertex_count