Files
Avatar-Toolkit/core/mmd/core/model.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

1574 lines
71 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
import time
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast, List, Tuple
import bpy
import idprop
import rna_prop_ui
from mathutils import Vector
from .. import AVATAR_TOOLKIT_VERSION, bpyutils
from ..bpyutils import FnContext, Props
from . import rigid_body
from .morph import FnMorph
from .rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC
from ....core.logging_setup import logger
if TYPE_CHECKING:
from ..properties.morph import MaterialMorphData
from ..properties.rigid_body import MMDRigidBody
from bpy.types import Context, Object, PropertyGroup, Material, Mesh, Armature, EditBone, PoseBone, KinematicConstraint
class FnModel:
@staticmethod
def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Dict[str, Dict[Any, Any]] = None):
FnModel.__copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values or {})
@staticmethod
def find_root_object(obj: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]:
"""Find the root object of the model.
Args:
obj (bpy.types.Object): The object to start searching from.
Returns:
Optional[bpy.types.Object]: The root object of the model. If the object is not a part of a model, None is returned.
Generally, the root object is a object with type == "EMPTY" and mmd_type == "ROOT".
"""
while obj is not None and obj.mmd_type != "ROOT":
obj = obj.parent
return obj
@staticmethod
def find_armature_object(root_object: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]:
"""Find the armature object of the model.
Args:
root_object (Optional[bpy.types.Object]): The root object of the model.
Returns:
Optional[bpy.types.Object]: The armature object of the model. If the model does not have an armature, None is returned.
"""
if root_object is None:
return None
for o in root_object.children:
if o.type == "ARMATURE":
return o
return None
@staticmethod
def find_rigid_group_object(root_object: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]:
if root_object is None:
return None
for o in root_object.children:
if o.type == "EMPTY" and o.mmd_type == "RIGID_GRP_OBJ":
return o
return None
@staticmethod
def __new_group_object(context: bpy.types.Context, name: str, mmd_type: str, parent: bpy.types.Object) -> bpy.types.Object:
group_object = FnContext.new_and_link_object(context, name=name, object_data=None)
group_object.mmd_type = mmd_type
group_object.parent = parent
group_object.hide_set(True)
group_object.hide_select = True
group_object.lock_rotation = group_object.lock_location = group_object.lock_scale = [True, True, True]
return group_object
@staticmethod
def ensure_rigid_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object:
if root_object is None:
raise ValueError("root_object cannot be None")
rigid_group_object = FnModel.find_rigid_group_object(root_object)
if rigid_group_object is not None:
return rigid_group_object
return FnModel.__new_group_object(context, name="rigidbodies", mmd_type="RIGID_GRP_OBJ", parent=root_object)
@staticmethod
def find_joint_group_object(root_object: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]:
if root_object is None:
return None
for o in root_object.children:
if o.type == "EMPTY" and o.mmd_type == "JOINT_GRP_OBJ":
return o
return None
@staticmethod
def ensure_joint_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object:
if root_object is None:
raise ValueError("root_object cannot be None")
joint_group_object = FnModel.find_joint_group_object(root_object)
if joint_group_object is not None:
return joint_group_object
return FnModel.__new_group_object(context, name="joints", mmd_type="JOINT_GRP_OBJ", parent=root_object)
@staticmethod
def find_temporary_group_object(root_object: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]:
if root_object is None:
return None
for o in root_object.children:
if o.type == "EMPTY" and o.mmd_type == "TEMPORARY_GRP_OBJ":
return o
return None
@staticmethod
def ensure_temporary_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object:
if root_object is None:
raise ValueError("root_object cannot be None")
temporary_group_object = FnModel.find_temporary_group_object(root_object)
if temporary_group_object is not None:
return temporary_group_object
return FnModel.__new_group_object(context, name="temporary", mmd_type="TEMPORARY_GRP_OBJ", parent=root_object)
@staticmethod
def find_bone_order_mesh_object(root_object: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]:
if root_object is None:
return None
armature_object = FnModel.find_armature_object(root_object)
if armature_object is None:
return None
for o in armature_object.children:
if o.type == "MESH" and "mmd_armature" in o.modifiers:
return o
return None
@staticmethod
def find_mesh_object_by_name(root_object: Optional[bpy.types.Object], name: str) -> Optional[bpy.types.Object]:
if root_object is None:
return None
if not name:
return None
for o in FnModel.iterate_mesh_objects(root_object):
if o.name == name or (hasattr(o.data, "name") and o.data.name == name):
return o
return None
@staticmethod
def iterate_child_objects(obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]:
if obj is None:
return iter(())
for child in obj.children:
yield child
yield from FnModel.iterate_child_objects(child)
@staticmethod
def iterate_filtered_child_objects(condition_function: Callable[[bpy.types.Object], bool], obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]:
if obj is None:
return iter(())
return FnModel.__iterate_filtered_child_objects_internal(condition_function, obj)
@staticmethod
def __iterate_filtered_child_objects_internal(condition_function: Callable[[bpy.types.Object], bool], obj: bpy.types.Object) -> Iterator[bpy.types.Object]:
for child in obj.children:
if condition_function(child):
yield child
yield from FnModel.__iterate_filtered_child_objects_internal(condition_function, child)
@staticmethod
def __iterate_child_mesh_objects(obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]:
return FnModel.iterate_filtered_child_objects(FnModel.is_mesh_object, obj)
@staticmethod
def iterate_mesh_objects(root_object: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]:
if root_object is None:
return iter(())
return FnModel.__iterate_child_mesh_objects(FnModel.find_armature_object(root_object))
@staticmethod
def iterate_rigid_body_objects(root_object: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]:
if root_object is None:
return iter(())
if root_object.mmd_root.is_built:
return itertools.chain(
FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_armature_object(root_object)),
FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_rigid_group_object(root_object)),
)
return FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_rigid_group_object(root_object))
@staticmethod
def iterate_joint_objects(root_object: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]:
if root_object is None:
return iter(())
return FnModel.iterate_filtered_child_objects(FnModel.is_joint_object, FnModel.find_joint_group_object(root_object))
@staticmethod
def iterate_temporary_objects(root_object: Optional[bpy.types.Object], rigid_track_only: bool = False) -> Iterator[bpy.types.Object]:
if root_object is None:
return iter(())
rigid_body_objects = FnModel.iterate_filtered_child_objects(FnModel.is_temporary_object, FnModel.find_rigid_group_object(root_object))
if rigid_track_only:
return rigid_body_objects
temporary_group_object = FnModel.find_temporary_group_object(root_object)
if temporary_group_object is None:
return rigid_body_objects
return itertools.chain(rigid_body_objects, FnModel.__iterate_filtered_child_objects_internal(FnModel.is_temporary_object, temporary_group_object))
@staticmethod
def iterate_materials(root_object: Optional[bpy.types.Object]) -> Iterator[bpy.types.Material]:
if root_object is None:
return iter(())
return (material for mesh_object in FnModel.iterate_mesh_objects(root_object) for material in cast("bpy.types.Mesh", mesh_object.data).materials if material is not None)
@staticmethod
def iterate_unique_materials(root_object: Optional[bpy.types.Object]) -> Iterator[bpy.types.Material]:
if root_object is None:
return iter(())
materials: Dict[bpy.types.Material, None] = {} # use dict because set does not guarantee the order
materials.update((material, None) for material in FnModel.iterate_materials(root_object))
return iter(materials.keys())
@staticmethod
def is_root_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]:
return obj is not None and obj.mmd_type == "ROOT"
@staticmethod
def is_rigid_body_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]:
return obj is not None and obj.mmd_type == "RIGID_BODY"
@staticmethod
def is_joint_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]:
return obj is not None and obj.mmd_type == "JOINT"
@staticmethod
def is_temporary_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]:
return obj is not None and obj.mmd_type in {"TRACK_TARGET", "NON_COLLISION_CONSTRAINT", "SPRING_CONSTRAINT", "SPRING_GOAL"}
@staticmethod
def is_mesh_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]:
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE"
@staticmethod
def get_max_bone_id(pose_bones):
"""Find maximum bone ID from pose bones, return -1 if no valid IDs found"""
max_bone_id = -1
for bone in pose_bones:
if not hasattr(bone, "is_mmd_shadow_bone") or not bone.is_mmd_shadow_bone:
max_bone_id = max(max_bone_id, bone.mmd_bone.bone_id)
return max_bone_id
@staticmethod
def unsafe_change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pose_bones):
"""
Change bone ID and updates all references without validating if new_bone_id is already in use.
If new_bone_id is already in use, it may cause conflicts and corrupt existing bone references.
"""
# Store the original bone_id and change it
bone_id = bone.mmd_bone.bone_id
bone.mmd_bone.bone_id = new_bone_id
# Update all bone_id references in bone morphs
for bone_morph in bone_morphs:
for data in bone_morph.data:
if data.bone_id == bone_id:
data.bone_id = new_bone_id
# Update all additional_transform_bone_id references in pose bones
for pose_bone in pose_bones:
if not hasattr(pose_bone, "is_mmd_shadow_bone") or not pose_bone.is_mmd_shadow_bone:
mmd_bone = pose_bone.mmd_bone
if mmd_bone.additional_transform_bone_id == bone_id:
mmd_bone.additional_transform_bone_id = new_bone_id
# Update all display_connection_bone_id references in pose bones
for pose_bone in pose_bones:
if not hasattr(pose_bone, "is_mmd_shadow_bone") or not pose_bone.is_mmd_shadow_bone:
mmd_bone = pose_bone.mmd_bone
if mmd_bone.display_connection_bone_id == bone_id:
mmd_bone.display_connection_bone_id = new_bone_id
@staticmethod
def safe_change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pose_bones):
"""
Change bone ID and updates all references safely by detecting and resolving conflicts automatically.
If new_bone_id is already in use, shifts all conflicting bone IDs sequentially until a gap is found.
"""
# Validate new_bone_id is non-negative
if new_bone_id < 0:
logger.warning(f"Attempted to set negative bone_id ({new_bone_id}) for bone '{bone.name}'. Using 0 instead.")
new_bone_id = 0
# Check if new_bone_id is already in use
bones_using_id = [pb for pb in pose_bones if pb.mmd_bone.bone_id == new_bone_id]
if bones_using_id:
# Find all bones that need to be shifted (those with consecutive IDs starting from new_bone_id)
bones_to_shift = []
current_id = new_bone_id
# Sort all pose bones by bone ID
sorted_bones = sorted([pb for pb in pose_bones if pb.mmd_bone.bone_id >= new_bone_id],
key=lambda pb: pb.mmd_bone.bone_id)
# Add bones to shift until we find a gap
for pb in sorted_bones:
if pb.mmd_bone.bone_id == current_id:
bones_to_shift.append(pb)
current_id += 1
else:
# Found a gap, stop adding bones
break
# Sort by bone ID in descending order to avoid conflicts during shifting
bones_to_shift.sort(key=lambda pb: pb.mmd_bone.bone_id, reverse=True)
# Shift bone IDs upward
for shift_bone in bones_to_shift:
FnModel.unsafe_change_bone_id(shift_bone, shift_bone.mmd_bone.bone_id + 1, bone_morphs, pose_bones)
# Now change our target bone's ID
FnModel.unsafe_change_bone_id(bone, new_bone_id, bone_morphs, pose_bones)
@staticmethod
def swap_bone_ids(bone_a, bone_b, bone_morphs, pose_bones):
"""Safely swap bone IDs between two bones and update all references"""
# Store original IDs
id_a = bone_a.mmd_bone.bone_id
id_b = bone_b.mmd_bone.bone_id
# Check for invalid bone IDs
if id_a < 0:
logger.warning(f"Cannot swap bone '{bone_a.name}' with invalid bone_id ({id_a})")
return
if id_b < 0:
logger.warning(f"Cannot swap bone '{bone_b.name}' with invalid bone_id ({id_b})")
return
# If both bones have the same ID, no swap needed
if id_a == id_b:
return
# Use temporary ID for three-step swap
temp_id = FnModel.get_max_bone_id(pose_bones) + 1
FnModel.unsafe_change_bone_id(bone_a, temp_id, bone_morphs, pose_bones)
FnModel.unsafe_change_bone_id(bone_b, id_a, bone_morphs, pose_bones)
FnModel.unsafe_change_bone_id(bone_a, id_b, bone_morphs, pose_bones)
@staticmethod
def shift_bone_id(old_bone_id: int, new_bone_id: int, bone_morphs, pose_bones):
"""
Shifts a bone to a specified ID position within a fixed bone ID order structure.
Maintains the gap structure of bone IDs unchanged, only changes which bone corresponds to which ID.
Other bones shift positions to accommodate the change while preserving relative order.
"""
if old_bone_id < 0:
logger.warning(f"Cannot shift bone with invalid old_bone_id ({old_bone_id})")
return
if new_bone_id < 0:
logger.warning(f"Cannot shift bone to invalid new_bone_id ({new_bone_id})")
return
if old_bone_id == new_bone_id:
return
valid_bones = [pb for pb in pose_bones if not (hasattr(pb, "is_mmd_shadow_bone") and pb.is_mmd_shadow_bone) and pb.mmd_bone.bone_id >= 0]
valid_bones.sort(key=lambda pb: pb.mmd_bone.bone_id)
# Extract current bone IDs (this order structure must remain unchanged)
fixed_bone_ids = [pb.mmd_bone.bone_id for pb in valid_bones]
# Find the bone to move and target position
old_pos = None
new_pos = None
moving_bone = None
for i, bone in enumerate(valid_bones):
if bone.mmd_bone.bone_id == old_bone_id:
old_pos = i
moving_bone = bone
if bone.mmd_bone.bone_id == new_bone_id:
new_pos = i
# If old_bone_id doesn't exist, return directly
if old_pos is None or moving_bone is None:
logger.warning(f"Could not find bone with ID {old_bone_id}")
return
# If new_bone_id doesn't exist, use safe_change_bone_id instead
if new_pos is None:
FnModel.safe_change_bone_id(moving_bone, new_bone_id, bone_morphs, pose_bones)
return
# 1. Determine the changes and build the translation map
id_translation_map = {}
bone_to_new_id_map = {}
if old_pos < new_pos: # Move down (ID increases)
# Bone at old_pos moves to new_pos's ID.
# Bones from old_pos+1 to new_pos shift up to fill the gap.
id_translation_map[old_bone_id] = fixed_bone_ids[new_pos]
bone_to_new_id_map[moving_bone.name] = fixed_bone_ids[new_pos]
for i in range(old_pos, new_pos):
bone_to_shift = valid_bones[i + 1]
target_id = fixed_bone_ids[i]
id_translation_map[bone_to_shift.mmd_bone.bone_id] = target_id
bone_to_new_id_map[bone_to_shift.name] = target_id
else: # Move up (ID decreases)
# Bone at old_pos moves to new_pos's ID.
# Bones from new_pos to old_pos-1 shift down.
id_translation_map[old_bone_id] = fixed_bone_ids[new_pos]
bone_to_new_id_map[moving_bone.name] = fixed_bone_ids[new_pos]
for i in range(new_pos + 1, old_pos + 1):
bone_to_shift = valid_bones[i - 1]
target_id = fixed_bone_ids[i]
id_translation_map[bone_to_shift.mmd_bone.bone_id] = target_id
bone_to_new_id_map[bone_to_shift.name] = target_id
# 2. Assign the new IDs to the affected bones
for bone_name, new_id in bone_to_new_id_map.items():
pose_bones[bone_name].mmd_bone.bone_id = new_id
# 3. Batch update all references (morphs and other bones)
if not id_translation_map:
return
for bone_morph in bone_morphs:
for data in bone_morph.data:
if data.bone_id in id_translation_map:
data.bone_id = id_translation_map[data.bone_id]
for pose_bone in pose_bones:
if not (hasattr(pose_bone, "is_mmd_shadow_bone") and pose_bone.is_mmd_shadow_bone):
mmd_bone = pose_bone.mmd_bone
if mmd_bone.additional_transform_bone_id in id_translation_map:
mmd_bone.additional_transform_bone_id = id_translation_map[mmd_bone.additional_transform_bone_id]
if mmd_bone.display_connection_bone_id in id_translation_map:
mmd_bone.display_connection_bone_id = id_translation_map[mmd_bone.display_connection_bone_id]
@staticmethod
def realign_bone_ids(bone_id_offset: int, bone_morphs, pose_bones):
"""Realigns all bone IDs sequentially without gaps and sorts bones in MMD-compatible hierarchy order."""
# Build bone_id to pose_bone index for fast lookup
bone_id_to_pose_bone = {}
valid_bones = []
for pose_bone in pose_bones:
if not (hasattr(pose_bone, "is_mmd_shadow_bone") and pose_bone.is_mmd_shadow_bone):
valid_bones.append(pose_bone)
bone_id = pose_bone.mmd_bone.bone_id
if bone_id >= 0:
bone_id_to_pose_bone[bone_id] = pose_bone
def get_sort_key(bone):
"""Generate sorting key that only moves bones violating parent-child rules and additional transform rules"""
transform_order = getattr(bone.mmd_bone, "transform_order", 0)
current_id = bone.mmd_bone.bone_id if bone.mmd_bone.bone_id >= 0 else float("inf")
additional_transform_bone_id = getattr(bone.mmd_bone, "additional_transform_bone_id", -1)
# Check if this bone violates parent-child order rules
violation_found = False
max_ancestor_id = -1
parent = bone.parent
while parent:
if hasattr(parent, "is_mmd_shadow_bone") and parent.is_mmd_shadow_bone:
parent = parent.parent
continue
parent_transform_order = getattr(parent.mmd_bone, "transform_order", 0)
parent_id = parent.mmd_bone.bone_id
# The rule that can be solved by sorting:
# if parent.transform_order == child.transform_order,
# then parent.bone_id must be < child.bone_id
if parent_transform_order == transform_order and parent_id >= 0 and current_id >= 0 and parent_id >= current_id:
violation_found = True
max_ancestor_id = max(max_ancestor_id, parent_id)
parent = parent.parent
# Check additional transform constraint
# additional_transform_bone_id must be smaller than current bone_id when transform_order is the same
if additional_transform_bone_id >= 0 and current_id >= 0 and additional_transform_bone_id >= current_id:
additional_transform_bone = bone_id_to_pose_bone.get(additional_transform_bone_id)
if additional_transform_bone:
additional_transform_order = getattr(additional_transform_bone.mmd_bone, "transform_order", 0)
# Only apply constraint when transform_order is the same
if additional_transform_order == transform_order:
violation_found = True
max_ancestor_id = max(max_ancestor_id, additional_transform_bone_id)
if violation_found:
# Move this bone after its ancestors and additional transform dependencies
return (max_ancestor_id + 0.1, current_id, bone.name)
# Keep original position - use current bone_id for sorting
return (current_id, current_id, bone.name)
# Sort - only bones violating rules will be moved
valid_bones.sort(key=get_sort_key)
# Create a translation map from old bone_id to new bone_id
id_translation_map = {}
bone_to_new_id_map = {}
for i, bone in enumerate(valid_bones):
new_id = bone_id_offset + i
old_id = bone.mmd_bone.bone_id
if old_id != new_id:
if old_id >= 0:
id_translation_map[old_id] = new_id
bone_to_new_id_map[bone.name] = new_id
# Assign the new IDs to the bones themselves
for bone in valid_bones:
new_id = bone_to_new_id_map[bone.name]
if bone.mmd_bone.bone_id != new_id:
bone.mmd_bone.bone_id = new_id
# Batch update all references (morphs and other bones) using the translation map
if not id_translation_map: # No changes needed
return
for bone_morph in bone_morphs:
for data in bone_morph.data:
if data.bone_id in id_translation_map:
data.bone_id = id_translation_map[data.bone_id]
for pose_bone in pose_bones:
if not (hasattr(pose_bone, "is_mmd_shadow_bone") and pose_bone.is_mmd_shadow_bone):
mmd_bone = pose_bone.mmd_bone
if mmd_bone.additional_transform_bone_id in id_translation_map:
mmd_bone.additional_transform_bone_id = id_translation_map[mmd_bone.additional_transform_bone_id]
if mmd_bone.display_connection_bone_id in id_translation_map:
mmd_bone.display_connection_bone_id = id_translation_map[mmd_bone.display_connection_bone_id]
@staticmethod
def clean_invalid_bone_id_references(mmd_root_object) -> int:
"""
Scan all bones and bone morphs to clean up invalid bone ID references.
This function performs two main tasks:
1. For bone properties that reference another bone by ID (e.g.,
additional_transform_bone_id, display_connection_bone_id), it
resets the ID to -1 if the target bone no longer exists.
2. For Bone Morphs, it removes individual morph data entries
that reference a bone that no longer exists.
Args:
mmd_root_object: The MMD root object (should have mmd_root property).
Returns:
int: The total number of invalid references that were cleaned or removed.
"""
if not mmd_root_object or not hasattr(mmd_root_object, "mmd_root"):
logger.warning("Invalid mmd_root_object provided")
return 0
# Find armature
armature = FnModel.find_armature_object(mmd_root_object)
if not armature:
logger.warning(f"Armature not found for MMD model '{mmd_root_object.name}'")
return 0
pose_bones = armature.pose.bones
bone_morphs = mmd_root_object.mmd_root.bone_morphs
if not pose_bones:
return 0
cleaned_count = 0
valid_bone_ids = {b.mmd_bone.bone_id for b in pose_bones if hasattr(b, "mmd_bone") and b.mmd_bone.bone_id >= 0 and not getattr(b, "is_mmd_shadow_bone", False)}
# Step 2: Clean up ID references on the bones themselves.
for bone in pose_bones:
if not hasattr(bone, "mmd_bone"):
continue
mmd_bone = bone.mmd_bone
# --- Clean up Additional Transform ---
ref_bone_id = mmd_bone.additional_transform_bone_id
if ref_bone_id >= 0 and ref_bone_id not in valid_bone_ids:
mmd_bone.additional_transform_bone_id = -1
mmd_bone.is_additional_transform_dirty = True
cleaned_count += 1
logger.info(f"Cleaned invalid additional transform reference on bone '{bone.name}' (bone_id: {ref_bone_id} does not exist)")
# --- Clean up Display Connection ---
ref_bone_id = mmd_bone.display_connection_bone_id
if ref_bone_id >= 0 and ref_bone_id not in valid_bone_ids:
mmd_bone.display_connection_bone_id = -1
cleaned_count += 1
logger.info(f"Cleaned invalid display connection reference on bone '{bone.name}' (bone_id: {ref_bone_id} does not exist)")
# Step 3: Clean up invalid references within Bone Morphs.
if bone_morphs:
for morph in bone_morphs:
morph_data = morph.data
for i in range(len(morph_data) - 1, -1, -1):
item = morph_data[i]
ref_bone_id = item.bone_id
if ref_bone_id >= 0 and ref_bone_id not in valid_bone_ids:
item.bone_id = -1
cleaned_count += 1
logger.info(f"Cleaned invalid bone reference on morph '{morph.name}' (bone_id: {ref_bone_id} does not exist)")
return cleaned_count
@staticmethod
def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]):
if not parent_root_object or not child_root_objects:
return
context = FnContext.ensure_context()
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
parent_armature_object = FnModel.find_armature_object(parent_root_object)
# Get the maximum bone ID of parent model's armature to avoid ID conflicts during merging
max_bone_id = FnModel.get_max_bone_id(parent_armature_object.pose.bones)
# Store original transform matrix for parent root object
original_matrix_world = parent_root_object.matrix_world.copy()
parent_root_object.matrix_world = Matrix.Identity(4)
# Apply child transform
for child_root_object in child_root_objects:
child_root_object.matrix_world = original_matrix_world.inverted() @ child_root_object.matrix_world
FnContext.set_active_and_select_single_object(context, child_root_object)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Reset object visibility
FnContext.set_active_and_select_single_object(context, parent_root_object)
bpy.ops.mmd_tools.reset_object_visibility()
for child_root_object in child_root_objects:
FnContext.set_active_and_select_single_object(context, child_root_object)
bpy.ops.mmd_tools.reset_object_visibility()
# Store material morph references for all child models
related_meshes = {}
# Process each child model
for child_root_object in child_root_objects:
if child_root_object is None:
continue
child_armature_object = FnModel.find_armature_object(child_root_object)
if child_armature_object is None:
continue
bpy.ops.object.mode_set(mode="OBJECT")
# Update bone IDs
child_pose_bones = child_armature_object.pose.bones
child_bone_morphs = child_root_object.mmd_root.bone_morphs
# Reassign bone IDs to avoid conflicts
FnModel.realign_bone_ids(max_bone_id + 1, child_bone_morphs, child_pose_bones)
max_bone_id = FnModel.get_max_bone_id(child_pose_bones)
# Save material morph references for this child model
for material_morph in child_root_object.mmd_root.material_morphs:
for material_morph_data in material_morph.data:
if material_morph_data.related_mesh_data is not None:
related_meshes[material_morph_data] = material_morph_data.related_mesh_data
material_morph_data.related_mesh_data = None
# Move mesh objects to parent armature using parent_set
mesh_objects = list(FnModel.__iterate_child_mesh_objects(child_armature_object))
if mesh_objects:
with select_object(parent_armature_object, objects=[parent_armature_object] + mesh_objects):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
for mesh in mesh_objects:
FnContext.set_active_and_select_single_object(context, mesh)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
for mesh in mesh_objects:
armature_modifier = next((mod for mod in mesh.modifiers if mod.type == "ARMATURE"), None)
if armature_modifier is None:
armature_modifier = mesh.modifiers.new("mmd_armature", "ARMATURE")
armature_modifier.object = parent_armature_object
# Handle rigid bodies
child_rigid_group_object = FnModel.find_rigid_group_object(child_root_object)
if child_rigid_group_object:
parent_rigid_group_object = FnModel.ensure_rigid_group_object(context, parent_root_object)
rigid_objects = list(FnModel.iterate_rigid_body_objects(child_root_object))
if rigid_objects:
with select_object(parent_rigid_group_object, objects=[parent_rigid_group_object] + rigid_objects):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
bpy.data.objects.remove(child_rigid_group_object)
# Handle joints
child_joint_group_object = FnModel.find_joint_group_object(child_root_object)
if child_joint_group_object:
parent_joint_group_object = FnModel.ensure_joint_group_object(context, parent_root_object)
joint_objects = list(FnModel.iterate_joint_objects(child_root_object))
if joint_objects:
with select_object(parent_joint_group_object, objects=[parent_joint_group_object] + joint_objects):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
bpy.data.objects.remove(child_joint_group_object)
# Handle temporary objects
child_temporary_group_object = FnModel.find_temporary_group_object(child_root_object)
if child_temporary_group_object:
parent_temporary_group_object = FnModel.ensure_temporary_group_object(context, parent_root_object)
temp_objects = list(FnModel.iterate_temporary_objects(child_root_object))
if temp_objects:
with select_object(parent_temporary_group_object, objects=[parent_temporary_group_object] + temp_objects):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
bpy.data.objects.remove(child_temporary_group_object)
# Copy MMD root properties
FnModel.copy_mmd_root(parent_root_object, child_root_object, overwrite=False)
# Clean additional transform before join
bpy.ops.object.mode_set(mode="OBJECT")
FnContext.set_active_and_select_single_object(context, parent_root_object)
bpy.ops.mmd_tools.clean_additional_transform()
for child_root_object in child_root_objects:
FnContext.set_active_and_select_single_object(context, child_root_object)
bpy.ops.mmd_tools.clean_additional_transform()
# Join all child armatures to parent armature
bpy.ops.object.mode_set(mode="OBJECT")
child_armature_objects = [FnModel.find_armature_object(child_root) for child_root in child_root_objects if FnModel.find_armature_object(child_root) is not None]
armature_data_to_remove = [child_arm.data for child_arm in child_armature_objects if child_arm.data]
if child_armature_objects:
with select_object(parent_armature_object, objects=[parent_armature_object] + child_armature_objects):
bpy.ops.object.join()
for armature_data in armature_data_to_remove:
if armature_data and armature_data.users == 0:
bpy.data.armatures.remove(armature_data)
# Apply additional transform after join
FnContext.set_active_object(context, parent_root_object)
bpy.ops.mmd_tools.clean_additional_transform()
bpy.ops.mmd_tools.apply_additional_transform()
# Remove empty child root objects
for child_root_object in child_root_objects:
assert len(child_root_object.children) == 0
bpy.data.objects.remove(child_root_object)
# Restore material morph references for all child models
for material_morph_data, mesh_data in related_meshes.items():
material_morph_data.related_mesh_data = mesh_data
# Restore original transform matrix for parent root object
parent_root_object.matrix_world = original_matrix_world
@staticmethod
def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier:
for m in mesh_object.modifiers:
if m.type != "ARMATURE":
continue
# already has armature modifier.
return cast("bpy.types.ArmatureModifier", m)
modifier = cast("bpy.types.ArmatureModifier", mesh_object.modifiers.new(name="Armature", type="ARMATURE"))
modifier.object = armature_object
modifier.use_vertex_groups = True
modifier.name = "mmd_armature"
return modifier
@staticmethod
def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool):
armature_object = FnModel.find_armature_object(parent_root_object)
if armature_object is None:
raise ValueError(f"Armature object not found in {parent_root_object}")
def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object:
if obj.parent is None:
return obj
return __get_root_object(obj.parent)
for mesh_object in mesh_objects:
if not FnModel.is_mesh_object(mesh_object):
continue
mesh_root_object = __get_root_object(mesh_object)
original_matrix_world = mesh_root_object.matrix_world
mesh_root_object.parent_type = "OBJECT"
mesh_root_object.parent = armature_object
mesh_root_object.matrix_world = original_matrix_world
if add_armature_modifier:
FnModel._add_armature_modifier(mesh_object, armature_object)
@staticmethod
def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int):
mmd_root = root_object.mmd_root
old_ik_loop_factor = mmd_root.ik_loop_factor
if new_ik_loop_factor == old_ik_loop_factor:
return
armature_object = FnModel.find_armature_object(root_object)
for pose_bone in armature_object.pose.bones:
for constraint in (cast("bpy.types.KinematicConstraint", c) for c in pose_bone.constraints if c.type == "IK"):
iterations = int(constraint.iterations * new_ik_loop_factor / old_ik_loop_factor)
logger.info("Update %s of %s: %d -> %d", constraint.name, pose_bone.name, constraint.iterations, iterations)
constraint.iterations = iterations
mmd_root.ik_loop_factor = new_ik_loop_factor
return
@staticmethod
def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
destination_rna_properties = destination.bl_rna.properties
for name in source.keys():
is_attr = hasattr(source, name)
value = getattr(source, name) if is_attr else source[name]
if isinstance(value, bpy.types.PropertyGroup):
FnModel.__copy_property_group(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values)
elif isinstance(value, bpy.types.bpy_prop_collection):
FnModel.__copy_collection_property(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values)
elif isinstance(value, idprop.types.IDPropertyArray):
pass
# _copy_collection_property(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values)
else:
value2values = replace_name2values.get(name)
if value2values is not None:
replace_value = value2values.get(value)
if replace_value is not None:
value = replace_value
if overwrite or destination_rna_properties[name].default == getattr(destination, name) if is_attr else destination[name]:
if is_attr:
setattr(destination, name, value)
else:
destination[name] = value
@staticmethod
def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
if overwrite:
destination.clear()
len_source = len(source)
if len_source == 0:
return
source_names: Set[str] = set(source.keys())
if len(source_names) == len_source and source[0].name != "":
# names work
destination_names: Set[str] = set(destination.keys())
missing_names = source_names - destination_names
destination_index = 0
for name, value in source.items():
if name in missing_names:
new_element = destination.add()
new_element["name"] = name
FnModel.__copy_property(destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values)
destination.move(destination.find(name), destination_index)
destination_index += 1
else:
# names not work
while len_source > len(destination):
destination.add()
for index, name in enumerate(source.keys()):
FnModel.__copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values)
@staticmethod
def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
if isinstance(destination, bpy.types.PropertyGroup):
FnModel.__copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
elif isinstance(destination, bpy.types.bpy_prop_collection):
FnModel.__copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
else:
raise ValueError(f"Unsupported destination: {destination}")
@staticmethod
def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True):
frames = root_object.mmd_root.display_item_frames
if reset and len(frames) > 0:
root_object.mmd_root.active_display_item_frame = 0
frames.clear()
frame_names = {"Root": "Root", "表情": "Facial"}
for frame_name, frame_name_e in frame_names.items():
frame = frames.get(frame_name, None) or frames.add()
frame.name = frame_name
frame.name_e = frame_name_e
frame.is_special = True
arm = FnModel.find_armature_object(root_object)
if arm is not None and len(arm.data.bones) > 0 and len(frames[0].data) < 1:
item = frames[0].data.add()
item.type = "BONE"
item.name = arm.data.bones[0].name
if not reset:
frames.move(frames.find("Root"), 0)
frames.move(frames.find("表情"), 1)
@staticmethod
def get_empty_display_size(root_object: bpy.types.Object) -> float:
return getattr(root_object, Props.empty_display_size)
class MigrationFnModel:
"""Migration Functions for old MMD models broken by bugs or issues"""
@classmethod
def update_mmd_ik_loop_factor(cls):
for armature_object in bpy.data.objects:
if armature_object.type != "ARMATURE":
continue
if "mmd_ik_loop_factor" not in armature_object:
return
FnModel.find_root_object(armature_object).mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1)
del armature_object["mmd_ik_loop_factor"]
@staticmethod
def update_AVATAR_TOOLKIT_VERSION():
for root_object in bpy.data.objects:
if root_object.type != "EMPTY":
continue
if not FnModel.is_root_object(root_object):
continue
if "AVATAR_TOOLKIT_VERSION" in root_object:
continue
root_object["AVATAR_TOOLKIT_VERSION"] = "2.8.0"
class Model:
def __init__(self, root_obj):
if root_obj is None:
raise ValueError("must be MMD ROOT type object")
if root_obj.mmd_type != "ROOT":
raise ValueError("must be MMD ROOT type object")
self.__root: bpy.types.Object = getattr(root_obj, "original", root_obj)
self.__arm: Optional[bpy.types.Object] = None
self.__rigid_grp: Optional[bpy.types.Object] = None
self.__joint_grp: Optional[bpy.types.Object] = None
self.__temporary_grp: Optional[bpy.types.Object] = None
@staticmethod
def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False):
if obj_name is None:
obj_name = name
context = FnContext.ensure_context()
root: bpy.types.Object = bpy.data.objects.new(name=obj_name, object_data=None)
root.mmd_type = "ROOT"
root.mmd_root.name = name
root.mmd_root.name_e = name_e
root["AVATAR_TOOLKIT_VERSION"] = AVATAR_TOOLKIT_VERSION
setattr(root, Props.empty_display_size, scale / 0.2)
FnContext.link_object(context, root)
if armature_object:
m = armature_object.matrix_world
armature_object.parent_type = "OBJECT"
armature_object.parent = root
# armature_object.matrix_world = m
root.matrix_world = m
armature_object.matrix_local.identity()
else:
armature_object = bpy.data.objects.new(name=obj_name + "_arm", object_data=bpy.data.armatures.new(name=obj_name))
armature_object.parent = root
FnContext.link_object(context, armature_object)
armature_object.lock_rotation = armature_object.lock_location = armature_object.lock_scale = [True, True, True]
setattr(armature_object, Props.show_in_front, True)
setattr(armature_object, Props.display_type, "WIRE")
from .bone import FnBone
FnBone.setup_special_bone_collections(armature_object)
if add_root_bone:
bone_name = "全ての親"
bone_name_english = "Root"
# Create the root bone
with edit_object(armature_object) as data:
bone = data.edit_bones.new(name=bone_name)
bone.head = (0.0, 0.0, 0.0)
bone.tail = (0.0, 0.0, getattr(root, Props.empty_display_size))
# Set MMD bone properties
pose_bone = armature_object.pose.bones[bone_name]
pose_bone.mmd_bone.name_j = bone_name
pose_bone.mmd_bone.name_e = bone_name_english
# Create a bone collection named "Root"
bone_collection_name = bone_name_english
bone_collection = armature_object.data.collections.new(name=bone_collection_name)
# Assign the new bone to the bone collection
data_bone = armature_object.data.bones[bone_name]
bone_collection.assign(data_bone)
FnContext.set_active_and_select_single_object(context, root)
return Model(root)
@staticmethod
def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
return FnModel.find_root_object(obj)
def initialDisplayFrames(self, reset=True):
FnModel.initalize_display_item_frames(self.__root, reset=reset)
@property
def morph_slider(self):
return FnMorph.get_morph_slider(self)
def loadMorphs(self):
FnMorph.load_morphs(self)
def create_ik_constraint(self, bone, ik_target):
"""Create IK constraint
Args:
bone: A pose bone to add a IK constraint
ik_target: A pose bone for IK target
Returns:
The bpy.types.KinematicConstraint object created. It is set target
and subtarget options.
"""
ik_target_name = ik_target.name
ik_const = bone.constraints.new("IK")
ik_const.target = self.__arm
ik_const.subtarget = ik_target_name
return ik_const
def allObjects(self, obj: Optional[bpy.types.Object] = None) -> Iterator[bpy.types.Object]:
if obj is None:
obj: bpy.types.Object = self.__root
yield obj
yield from FnModel.iterate_child_objects(obj)
def rootObject(self) -> bpy.types.Object:
return self.__root
def armature(self) -> bpy.types.Object:
if self.__arm is None:
self.__arm = FnModel.find_armature_object(self.__root)
assert self.__arm is not None
return self.__arm
def hasRigidGroupObject(self) -> bool:
return FnModel.find_rigid_group_object(self.__root) is not None
def rigidGroupObject(self) -> bpy.types.Object:
if self.__rigid_grp is None:
self.__rigid_grp = FnModel.find_rigid_group_object(self.__root)
if self.__rigid_grp is None:
rigids = bpy.data.objects.new(name="rigidbodies", object_data=None)
FnContext.link_object(FnContext.ensure_context(), rigids)
rigids.mmd_type = "RIGID_GRP_OBJ"
rigids.parent = self.__root
rigids.hide_set(True)
rigids.hide_select = True
rigids.lock_rotation = rigids.lock_location = rigids.lock_scale = [True, True, True]
self.__rigid_grp = rigids
return self.__rigid_grp
def hasJointGroupObject(self) -> bool:
return FnModel.find_joint_group_object(self.__root) is not None
def jointGroupObject(self) -> bpy.types.Object:
if self.__joint_grp is None:
self.__joint_grp = FnModel.find_joint_group_object(self.__root)
if self.__joint_grp is None:
joints = bpy.data.objects.new(name="joints", object_data=None)
FnContext.link_object(FnContext.ensure_context(), joints)
joints.mmd_type = "JOINT_GRP_OBJ"
joints.parent = self.__root
joints.hide_set(True)
joints.hide_select = True
joints.lock_rotation = joints.lock_location = joints.lock_scale = [True, True, True]
self.__joint_grp = joints
return self.__joint_grp
def hasTemporaryGroupObject(self) -> bool:
return FnModel.find_temporary_group_object(self.__root) is not None
def temporaryGroupObject(self) -> bpy.types.Object:
if self.__temporary_grp is None:
self.__temporary_grp = FnModel.find_temporary_group_object(self.__root)
if self.__temporary_grp is None:
temporarys = bpy.data.objects.new(name="temporary", object_data=None)
FnContext.link_object(FnContext.ensure_context(), temporarys)
temporarys.mmd_type = "TEMPORARY_GRP_OBJ"
temporarys.parent = self.__root
temporarys.hide_set(True)
temporarys.hide_select = True
temporarys.lock_rotation = temporarys.lock_location = temporarys.lock_scale = [True, True, True]
self.__temporary_grp = temporarys
return self.__temporary_grp
def meshes(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_mesh_objects(self.__root)
def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True):
FnModel.attach_mesh_objects(self.rootObject(), meshes, add_armature_modifier)
def firstMesh(self) -> Optional[bpy.types.Object]:
for i in self.meshes():
return i
return None
def findMesh(self, mesh_name) -> Optional[bpy.types.Object]:
"""Find the mesh by name"""
if mesh_name == "":
return None
for mesh in self.meshes():
if mesh_name in {mesh.name, mesh.data.name}:
return mesh
return None
def findMeshByIndex(self, index: int) -> Optional[bpy.types.Object]:
"""Find the mesh by index"""
if index < 0:
return None
for i, mesh in enumerate(self.meshes()):
if i == index:
return mesh
return None
def getMeshIndex(self, mesh_name: str) -> int:
"""Get the index of a mesh. Returns -1 if not found"""
if mesh_name == "":
return -1
for i, mesh in enumerate(self.meshes()):
if mesh_name in {mesh.name, mesh.data.name}:
return i
return -1
def rigidBodies(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_rigid_body_objects(self.__root)
def joints(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_joint_objects(self.__root)
def temporaryObjects(self, rigid_track_only=False) -> Iterator[bpy.types.Object]:
return FnModel.iterate_temporary_objects(self.__root, rigid_track_only)
def materials(self) -> Iterator[bpy.types.Material]:
"""List all materials in all meshes"""
materials = {} # Use dict instead of set to guarantee preserve order
for mesh in self.meshes():
materials.update((slot.material, 0) for slot in mesh.material_slots if slot.material is not None)
return iter(materials.keys())
def renameBone(self, old_bone_name, new_bone_name):
if old_bone_name == new_bone_name:
return
armature = self.armature()
bone = armature.pose.bones[old_bone_name]
bone.name = new_bone_name
new_bone_name = bone.name
mmd_root = self.rootObject().mmd_root
for frame in mmd_root.display_item_frames:
for item in frame.data:
if item.type == "BONE" and item.name == old_bone_name:
item.name = new_bone_name
for mesh in self.meshes():
if old_bone_name in mesh.vertex_groups:
mesh.vertex_groups[old_bone_name].name = new_bone_name
def build(self, non_collision_distance_scale=1.5, collision_margin=1e-06):
rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
if self.__root.mmd_root.is_built:
self.clean()
self.__root.mmd_root.is_built = True
logger.info("****************************************")
logger.info(" Build rig")
logger.info("****************************************")
start_time = time.time()
self.__preBuild()
self.disconnectPhysicsBones()
self.buildRigids(non_collision_distance_scale, collision_margin)
self.buildJoints()
self.__postBuild()
logger.info(" Finished building in %f seconds.", time.time() - start_time)
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled)
def clean(self):
rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
logger.info("****************************************")
logger.info(" Clean rig")
logger.info("****************************************")
start_time = time.time()
pose_bones = []
arm = self.armature()
if arm is not None:
pose_bones = arm.pose.bones
for i in pose_bones:
if "mmd_tools_rigid_track" in i.constraints:
const = i.constraints["mmd_tools_rigid_track"]
i.constraints.remove(const)
rigid_track_counts = 0
for i in self.rigidBodies():
rigid_type = int(i.mmd_rigid.type)
if "mmd_tools_rigid_parent" not in i.constraints:
rigid_track_counts += 1
logger.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name)
i.mmd_rigid.bone = i.mmd_rigid.bone
relation = i.constraints["mmd_tools_rigid_parent"]
relation.mute = True
if rigid_type == rigid_body.MODE_STATIC:
i.parent_type = "OBJECT"
i.parent = self.rigidGroupObject()
elif rigid_type in {rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE}:
arm = relation.target
bone_name = relation.subtarget
if arm is not None and bone_name != "":
for c in arm.pose.bones[bone_name].constraints:
if c.type == "IK":
c.mute = False
self.__restoreTransforms(i)
for i in self.joints():
self.__restoreTransforms(i)
self.__removeTemporaryObjects()
self.connectPhysicsBones()
arm = self.armature()
if arm is not None: # update armature
arm.update_tag()
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
mmd_root = self.rootObject().mmd_root
if mmd_root.show_temporary_objects:
mmd_root.show_temporary_objects = False
logger.info(" Finished cleaning in %f seconds.", time.time() - start_time)
mmd_root.is_built = False
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled)
def __removeTemporaryObjects(self):
with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()):
bpy.ops.object.delete()
def __restoreTransforms(self, obj):
for attr in ("location", "rotation_euler"):
attr_name = f"__backup_{attr}__"
val = obj.get(attr_name, None)
if val is not None:
setattr(obj, attr, val)
# Use property_unset instead of del for Blender 5.0 compatibility
obj.property_unset(attr_name)
def __backupTransforms(self, obj):
for attr in ("location", "rotation_euler"):
attr_name = f"__backup_{attr}__"
if attr_name in obj: # should not happen in normal build/clean cycle
continue
obj[attr_name] = getattr(obj, attr, None)
def __preBuild(self):
self.__fake_parent_map = {}
self.__rigid_body_matrix_map = {}
self.__empty_parent_map = {}
no_parents = []
for i in self.rigidBodies():
self.__backupTransforms(i)
# mute relation
relation = i.constraints["mmd_tools_rigid_parent"]
relation.mute = True
# mute IK
if int(i.mmd_rigid.type) in {rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE}:
arm = relation.target
bone_name = relation.subtarget
if arm is not None and bone_name != "":
for c in arm.pose.bones[bone_name].constraints:
if c.type == "IK":
c.mute = True
c.influence = c.influence # trigger update
else:
no_parents.append(i)
# update changes of armature constraints
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
parented = []
for i in self.joints():
self.__backupTransforms(i)
rbc = i.rigid_body_constraint
if rbc is None:
continue
obj1, obj2 = rbc.object1, rbc.object2
if obj2 in no_parents:
if obj1 not in no_parents and obj2 not in parented:
self.__fake_parent_map.setdefault(obj1, []).append(obj2)
parented.append(obj2)
elif obj1 in no_parents:
if obj1 not in parented:
self.__fake_parent_map.setdefault(obj2, []).append(obj1)
parented.append(obj1)
# assert(len(no_parents) == len(parented))
def __postBuild(self):
self.__fake_parent_map = None
self.__rigid_body_matrix_map = None
# update changes
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
# parenting empty to rigid object at once for speeding up
for empty, rigid_obj in self.__empty_parent_map.items():
matrix_world = empty.matrix_world
empty.parent = rigid_obj
empty.matrix_world = matrix_world
self.__empty_parent_map = None
arm = self.armature()
if arm:
for p_bone in arm.pose.bones:
c = p_bone.constraints.get("mmd_tools_rigid_track", None)
if c:
c.mute = False
def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float):
assert rigid_obj.mmd_type == "RIGID_BODY"
rb = rigid_obj.rigid_body
if rb is None:
return
rigid = rigid_obj.mmd_rigid
rigid_type = int(rigid.type)
relation = rigid_obj.constraints["mmd_tools_rigid_parent"]
if relation.target is None:
relation.target = self.armature()
arm = relation.target
if relation.subtarget not in arm.pose.bones:
bone_name = ""
else:
bone_name = relation.subtarget
if rigid_type == rigid_body.MODE_STATIC:
rb.kinematic = True
else:
rb.kinematic = False
if collision_margin == 0.0:
rb.use_margin = False
else:
rb.use_margin = True
rb.collision_margin = collision_margin
if arm is not None and bone_name != "":
target_bone = arm.pose.bones[bone_name]
if rigid_type == rigid_body.MODE_STATIC:
m = target_bone.matrix @ target_bone.bone.matrix_local.inverted()
self.__rigid_body_matrix_map[rigid_obj] = m
orig_scale = rigid_obj.scale.copy()
to_matrix_world = rigid_obj.matrix_world @ rigid_obj.matrix_local.inverted()
matrix_world = to_matrix_world @ (m @ rigid_obj.matrix_local)
rigid_obj.parent = arm
rigid_obj.parent_type = "BONE"
rigid_obj.parent_bone = bone_name
rigid_obj.matrix_world = matrix_world
rigid_obj.scale = orig_scale
fake_children = self.__fake_parent_map.get(rigid_obj, None)
if fake_children:
for fake_child in fake_children:
logger.debug(" - fake_child: %s", fake_child.name)
t, r, s = (m @ fake_child.matrix_local).decompose()
fake_child.location = t
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode)
elif rigid_type in {rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE}:
m = target_bone.matrix @ target_bone.bone.matrix_local.inverted()
self.__rigid_body_matrix_map[rigid_obj] = m
t, r, s = (m @ rigid_obj.matrix_local).decompose()
rigid_obj.location = t
rigid_obj.rotation_euler = r.to_euler(rigid_obj.rotation_mode)
fake_children = self.__fake_parent_map.get(rigid_obj, None)
if fake_children:
for fake_child in fake_children:
logger.debug(" - fake_child: %s", fake_child.name)
t, r, s = (m @ fake_child.matrix_local).decompose()
fake_child.location = t
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode)
if "mmd_tools_rigid_track" not in target_bone.constraints:
empty = bpy.data.objects.new(name="mmd_bonetrack", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty)
empty.matrix_world = target_bone.matrix
setattr(empty, Props.empty_display_type, "ARROWS")
setattr(empty, Props.empty_display_size, 0.1 * getattr(self.__root, Props.empty_display_size))
empty.mmd_type = "TRACK_TARGET"
empty.hide_set(True)
empty.parent = self.temporaryGroupObject()
rigid_obj.mmd_rigid.bone = bone_name
rigid_obj.constraints.remove(relation)
self.__empty_parent_map[empty] = rigid_obj
const_type = ("COPY_TRANSFORMS", "COPY_ROTATION")[rigid_type - 1]
const = target_bone.constraints.new(const_type)
const.mute = True
const.name = "mmd_tools_rigid_track"
const.target = empty
else:
empty = target_bone.constraints["mmd_tools_rigid_track"].target
ori_rigid_obj = self.__empty_parent_map[empty]
ori_rb = ori_rigid_obj.rigid_body
if ori_rb and rb.mass > ori_rb.mass:
logger.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name)
# re-parenting
rigid_obj.mmd_rigid.bone = bone_name
rigid_obj.constraints.remove(relation)
self.__empty_parent_map[empty] = rigid_obj
# revert change
ori_rigid_obj.mmd_rigid.bone = bone_name
else:
logger.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name)
rb.collision_shape = rigid.shape
@staticmethod
def __getRigidRange(obj):
return (Vector(obj.bound_box[0]) - Vector(obj.bound_box[6])).length
def __createNonCollisionConstraint(self, nonCollisionJointTable):
total_len = len(nonCollisionJointTable)
if total_len < 1:
return
start_time = time.time()
logger.debug("-" * 60)
logger.debug(" creating ncc, counts: %d", total_len)
ncc_obj = createObject(name="ncc", object_data=None)
ncc_obj.location = [0, 0, 0]
setattr(ncc_obj, Props.empty_display_type, "ARROWS")
setattr(ncc_obj, Props.empty_display_size, 0.5 * getattr(self.__root, Props.empty_display_size))
ncc_obj.mmd_type = "NON_COLLISION_CONSTRAINT"
ncc_obj.hide_render = True
ncc_obj.parent = self.temporaryGroupObject()
bpy.ops.rigidbody.constraint_add(type="GENERIC")
rb = ncc_obj.rigid_body_constraint
rb.disable_collisions = True
ncc_objs = duplicateObject(ncc_obj, total_len)
logger.debug(" created %d ncc.", len(ncc_objs))
for ncc_obj, pair in zip(ncc_objs, nonCollisionJointTable, strict=False):
rbc = ncc_obj.rigid_body_constraint
rbc.object1, rbc.object2 = pair
ncc_obj.hide_set(True)
ncc_obj.hide_select = True
logger.debug(" finish in %f seconds.", time.time() - start_time)
logger.debug("-" * 60)
def buildRigids(self, non_collision_distance_scale, collision_margin):
logger.debug("--------------------------------")
logger.debug(" Build riggings of rigid bodies")
logger.debug("--------------------------------")
rigid_objects = list(self.rigidBodies())
rigid_object_groups = [[] for i in range(16)]
for i in rigid_objects:
rigid_object_groups[i.mmd_rigid.collision_group_number].append(i)
jointMap = {}
for joint in self.joints():
rbc = joint.rigid_body_constraint
if rbc is None:
continue
rbc.disable_collisions = False
jointMap[frozenset((rbc.object1, rbc.object2))] = joint
logger.info("Creating non collision constraints")
# create non collision constraints
nonCollisionJointTable = []
non_collision_pairs = set()
rigid_object_cnt = len(rigid_objects)
for obj_a in rigid_objects:
for n, ignore in enumerate(obj_a.mmd_rigid.collision_group_mask):
if not ignore:
continue
for obj_b in rigid_object_groups[n]:
if obj_a == obj_b:
continue
pair = frozenset((obj_a, obj_b))
if pair in non_collision_pairs:
continue
if pair in jointMap:
joint = jointMap[pair]
joint.rigid_body_constraint.disable_collisions = True
else:
distance = (obj_a.location - obj_b.location).length
if distance < non_collision_distance_scale * (self.__getRigidRange(obj_a) + self.__getRigidRange(obj_b)) * 0.5:
nonCollisionJointTable.append((obj_a, obj_b))
non_collision_pairs.add(pair)
for cnt, i in enumerate(rigid_objects):
logger.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name)
self.updateRigid(i, collision_margin)
self.__createNonCollisionConstraint(nonCollisionJointTable)
return rigid_objects
def buildJoints(self):
for i in self.joints():
rbc = i.rigid_body_constraint
if rbc is None:
continue
m = self.__rigid_body_matrix_map.get(rbc.object1, None)
if m is None:
m = self.__rigid_body_matrix_map.get(rbc.object2, None)
if m is None:
continue
t, r, s = (m @ i.matrix_local).decompose()
i.location = t
i.rotation_euler = r.to_euler(i.rotation_mode)
def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]):
armature_object = self.armature()
armature: bpy.types.Armature
with edit_object(armature_object) as armature:
edit_bones = armature.edit_bones
rigid_body_object: bpy.types.Object
for rigid_body_object in self.rigidBodies():
mmd_rigid: MMDRigidBody = rigid_body_object.mmd_rigid
if mmd_rigid.type not in target_modes:
continue
bone_name: str = mmd_rigid.bone
edit_bone = edit_bones.get(bone_name)
if edit_bone is None:
continue
editor(edit_bone)
def disconnectPhysicsBones(self):
def editor(edit_bone: bpy.types.EditBone):
rna_prop_ui.rna_idprop_ui_create(edit_bone, "mmd_bone_use_connect", default=edit_bone.use_connect)
edit_bone.use_connect = False
self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)})
def connectPhysicsBones(self):
def editor(edit_bone: bpy.types.EditBone):
mmd_bone_use_connect_str: Optional[str] = edit_bone.get("mmd_bone_use_connect")
if mmd_bone_use_connect_str is None:
return
if not edit_bone.use_connect: # wasn't it overwritten?
edit_bone.use_connect = bool(mmd_bone_use_connect_str)
del edit_bone["mmd_bone_use_connect"]
self.__editPhysicsBones(editor, {str(MODE_STATIC), str(MODE_DYNAMIC), str(MODE_DYNAMIC_BONE)})