# -*- 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: Optional[Dict[str, Dict[Any, Any]]] = None) -> 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: if hasattr(obj, 'mmd_type') and obj.mmd_type == "ROOT": return obj obj = obj.parent return None @staticmethod def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: """Find the armature object of the model. Args: root_object (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. """ for o in root_object.children: if o.type == "ARMATURE": return o return None @staticmethod def find_rigid_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: 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: 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: bpy.types.Object) -> Optional[bpy.types.Object]: 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: 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: bpy.types.Object) -> Optional[bpy.types.Object]: 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: 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: bpy.types.Object) -> Optional[bpy.types.Object]: 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_bone_order_override" in o.modifiers: return o return None @staticmethod def find_mesh_object_by_name(root_object: bpy.types.Object, name: str) -> Optional[bpy.types.Object]: 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: bpy.types.Object) -> Iterator[bpy.types.Object]: 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: bpy.types.Object) -> Iterator[bpy.types.Object]: return FnModel.__iterate_child_mesh_objects(FnModel.find_armature_object(root_object)) @staticmethod def iterate_rigid_body_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: 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: bpy.types.Object) -> Iterator[bpy.types.Object]: return FnModel.iterate_filtered_child_objects(FnModel.is_joint_object, FnModel.find_joint_group_object(root_object)) @staticmethod def iterate_temporary_objects(root_object: bpy.types.Object, rigid_track_only: bool = False) -> Iterator[bpy.types.Object]: 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: bpy.types.Object) -> Iterator[bpy.types.Material]: 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: bpy.types.Object) -> Iterator[bpy.types.Material]: 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 join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]) -> None: logger.info(f"Joining models to parent root: {parent_root_object.name}") parent_armature_object = FnModel.find_armature_object(parent_root_object) with bpy.context.temp_override( active_object=parent_armature_object, selected_editable_objects=[parent_armature_object], ): bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs: List[Any], pose_bones: List[bpy.types.PoseBone]) -> None: """This function will also update the references of bone morphs and rotate+/move+.""" bone_id = bone.mmd_bone.bone_id # Change Bone ID bone.mmd_bone.bone_id = new_bone_id # Update Relative Bone Morph # Update the reference of bone morph # 更新骨骼表情 for bone_morph in bone_morphs: for data in bone_morph.data: if data.bone_id != bone_id: continue data.bone_id = new_bone_id # Update Relative Additional Transform # Update the reference of rotate+/move+ # 更新付与親 for pose_bone in pose_bones: if pose_bone.is_mmd_shadow_bone: continue mmd_bone = pose_bone.mmd_bone if mmd_bone.additional_transform_bone_id != bone_id: continue mmd_bone.additional_transform_bone_id = new_bone_id max_bone_id = max( ( b.mmd_bone.bone_id for o in itertools.chain( child_root_objects, [parent_root_object], ) for b in FnModel.find_armature_object(o).pose.bones if not b.is_mmd_shadow_bone ), default=-1, ) child_root_object: bpy.types.Object for child_root_object in child_root_objects: logger.info(f"Processing child root: {child_root_object.name}") child_armature_object = FnModel.find_armature_object(child_root_object) child_pose_bones = child_armature_object.pose.bones child_bone_morphs = child_root_object.mmd_root.bone_morphs for pose_bone in child_pose_bones: if pose_bone.is_mmd_shadow_bone: continue if pose_bone.mmd_bone.bone_id != -1: max_bone_id += 1 _change_bone_id(pose_bone, max_bone_id, child_bone_morphs, child_pose_bones) child_armature_matrix = child_armature_object.matrix_parent_inverse.copy() with bpy.context.temp_override( active_object=child_armature_object, selected_editable_objects=[child_armature_object], ): bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # Disconnect mesh dependencies because transform_apply fails when mesh data are multiple used. related_meshes: Dict['MaterialMorphData', bpy.types.Mesh] = {} 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 try: # replace mesh armature modifier.object mesh: bpy.types.Object for mesh in FnModel.__iterate_child_mesh_objects(child_armature_object): with bpy.context.temp_override( active_object=mesh, selected_editable_objects=[mesh], ): bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) finally: # Restore mesh dependencies for material_morph in child_root_object.mmd_root.material_morphs: for material_morph_data in material_morph.data: material_morph_data.related_mesh_data = related_meshes.get(material_morph_data, None) # join armatures with bpy.context.temp_override( active_object=parent_armature_object, selected_editable_objects=[parent_armature_object, child_armature_object], ): bpy.ops.object.join() for mesh in FnModel.__iterate_child_mesh_objects(parent_armature_object): armature_modifier: bpy.types.ArmatureModifier = mesh.modifiers["mmd_bone_order_override"] if "mmd_bone_order_override" in mesh.modifiers else mesh.modifiers.new("mmd_bone_order_override", "ARMATURE") if armature_modifier.object is None: armature_modifier.object = parent_armature_object mesh.matrix_parent_inverse = child_armature_matrix child_rigid_group_object = FnModel.find_rigid_group_object(child_root_object) if child_rigid_group_object is not None: parent_rigid_group_object = FnModel.find_rigid_group_object(parent_root_object) with bpy.context.temp_override( object=parent_rigid_group_object, selected_editable_objects=[parent_rigid_group_object, *FnModel.iterate_rigid_body_objects(child_root_object)], ): bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.data.objects.remove(child_rigid_group_object) child_joint_group_object = FnModel.find_joint_group_object(child_root_object) if child_joint_group_object is not None: parent_joint_group_object = FnModel.find_joint_group_object(parent_root_object) with bpy.context.temp_override( object=parent_joint_group_object, selected_editable_objects=[parent_joint_group_object, *FnModel.iterate_joint_objects(child_root_object)], ): bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.data.objects.remove(child_joint_group_object) child_temporary_group_object = FnModel.find_temporary_group_object(child_root_object) if child_temporary_group_object is not None: parent_temporary_group_object = FnModel.find_temporary_group_object(parent_root_object) with bpy.context.temp_override( object=parent_temporary_group_object, selected_editable_objects=[parent_temporary_group_object, *FnModel.iterate_temporary_objects(child_root_object)], ): bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) for obj in list(FnModel.iterate_child_objects(child_temporary_group_object)): bpy.data.objects.remove(obj) bpy.data.objects.remove(child_temporary_group_object) FnModel.copy_mmd_root(parent_root_object, child_root_object, overwrite=False) # Remove unused objects from child models if len(child_root_object.children) == 0: bpy.data.objects.remove(child_root_object) logger.info("Model joining completed successfully") @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_bone_order_override" return modifier @staticmethod def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool) -> None: logger.info(f"Attaching mesh objects to {parent_root_object.name}") armature_object = FnModel.find_armature_object(parent_root_object) if armature_object is None: error_msg = f"Armature object not found in {parent_root_object.name}" logger.error(error_msg) raise ValueError(error_msg) 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): logger.debug(f"Skipping non-mesh object: {mesh_object.name}") continue if FnModel.find_root_object(mesh_object) is not None: logger.debug(f"Skipping mesh with existing root: {mesh_object.name}") 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 logger.debug(f"Attached mesh: {mesh_object.name}") if add_armature_modifier: FnModel._add_armature_modifier(mesh_object, armature_object) logger.debug(f"Added armature modifier to: {mesh_object.name}") @staticmethod def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool) -> None: logger.info(f"Adding missing vertex groups from bones to {mesh_object.name}") armature_object = FnModel.find_armature_object(root_object) if armature_object is None: error_msg = f"Armature object not found in {root_object.name}" logger.error(error_msg) raise ValueError(error_msg) vertex_group_names: Set[str] = set() search_meshes = FnModel.iterate_mesh_objects(root_object) if search_in_all_meshes else [mesh_object] for search_mesh in search_meshes: vertex_group_names.update(search_mesh.vertex_groups.keys()) added_count = 0 pose_bone: bpy.types.PoseBone for pose_bone in armature_object.pose.bones: pose_bone_name = pose_bone.name if pose_bone_name in vertex_group_names: continue if pose_bone_name.startswith("_"): continue mesh_object.vertex_groups.new(name=pose_bone_name) added_count += 1 logger.debug(f"Added {added_count} missing vertex groups to {mesh_object.name}") @staticmethod def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int) -> None: logger.info(f"Changing IK loop factor to {new_ik_loop_factor}") mmd_root = root_object.mmd_root old_ik_loop_factor = mmd_root.ik_loop_factor if new_ik_loop_factor == old_ik_loop_factor: logger.debug("IK loop factor already set to the requested value") return armature_object = FnModel.find_armature_object(root_object) updated_count = 0 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.debug(f"Update {constraint.name} of {pose_bone.name}: {constraint.iterations} -> {iterations}") constraint.iterations = iterations updated_count += 1 mmd_root.ik_loop_factor = new_ik_loop_factor logger.info(f"Updated {updated_count} IK constraints") @staticmethod def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None: 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]]) -> None: 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]]) -> None: 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: error_msg = f"Unsupported destination: {destination}" logger.error(error_msg) raise ValueError(error_msg) @staticmethod def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True) -> None: logger.info(f"Initializing display item frames for {root_object.name}") 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) logger.debug(f"Display item frames initialized with {len(frames)} frames") @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) -> None: logger.info("Updating MMD IK loop factor for all armatures") updated_count = 0 for armature_object in bpy.data.objects: if armature_object.type != "ARMATURE": continue if "mmd_ik_loop_factor" not in armature_object: continue root_object = FnModel.find_root_object(armature_object) if root_object: root_object.mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1) del armature_object["mmd_ik_loop_factor"] updated_count += 1 logger.info(f"Updated IK loop factor for {updated_count} armatures") @staticmethod def update_avatar_toolkit_version() -> None: logger.info("Updating Avatar Toolkit version for all MMD root objects") updated_count = 0 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"] = "0.2.1" updated_count += 1 logger.info(f"Updated Avatar Toolkit version for {updated_count} root objects") class Model: def __init__(self, root_obj: bpy.types.Object) -> None: 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 logger.debug(f"Model initialized with root object: {self.__root.name}") @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) -> 'Model': if obj_name is None: obj_name = name context = FnContext.ensure_context() logger.info(f"Creating new MMD model: {name}") 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: logger.debug(f"Using existing armature: {armature_object.name}") 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: logger.debug("Creating new armature") 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: logger.debug("Adding root bone") bone_name = "全ての親" bone_name_english = "Root" # Create the root bone with bpyutils.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) logger.info(f"Model created successfully: {name}") return Model(root) @staticmethod def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]: return FnModel.find_root_object(obj) def initialDisplayFrames(self, reset: bool = True) -> None: FnModel.initalize_display_item_frames(self.__root, reset=reset) @property def morph_slider(self) -> Any: return FnMorph.get_morph_slider(self) def loadMorphs(self) -> None: logger.info(f"Loading morphs for model: {self.__root.name}") FnMorph.load_morphs(self) def create_ik_constraint(self, bone: bpy.types.PoseBone, ik_target: bpy.types.PoseBone) -> bpy.types.KinematicConstraint: """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. """ logger.debug(f"Creating IK constraint on {bone.name} targeting {ik_target.name}") 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: logger.debug(f"Creating rigid group object for {self.__root.name}") 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: logger.debug(f"Creating joint group object for {self.__root.name}") 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: logger.debug(f"Creating temporary group object for {self.__root.name}") 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) -> None: 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: str) -> Optional[bpy.types.Object]: """ Helper method to find a mesh by name """ if mesh_name == "": return None for mesh in self.meshes(): if mesh.name == mesh_name or mesh.data.name == mesh_name: return mesh return None def findMeshByIndex(self, index: int) -> Optional[bpy.types.Object]: """ Helper method to 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: """ Helper method to 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 == mesh_name or mesh.data.name == mesh_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: bool = False) -> Iterator[bpy.types.Object]: return FnModel.iterate_temporary_objects(self.__root, rigid_track_only) def materials(self) -> Iterator[bpy.types.Material]: """ Helper method to list all materials in all meshes """ materials: Dict[bpy.types.Material, int] = {} # 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: str, new_bone_name: str) -> None: if old_bone_name == new_bone_name: return logger.info(f"Renaming bone: {old_bone_name} -> {new_bone_name}") armature = self.armature() bone = armature.pose.bones[old_bone_name] bone.name = new_bone_name new_bone_name = bone.name # Get the actual name (might be adjusted by Blender) 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: float = 1.5, collision_margin: float = 1e-06) -> None: logger.info(f"Building physics rig for {self.__root.name}") rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False) if self.__root.mmd_root.is_built: logger.info("Model is already built, cleaning first") 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) -> None: logger.info(f"Cleaning physics rig for {self.__root.name}") 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) logger.debug(f"Removed rigid track constraint from {i.name}") 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) -> None: logger.debug("Removing temporary objects") with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()): bpy.ops.object.delete() def __restoreTransforms(self, obj: bpy.types.Object) -> None: for attr in ("location", "rotation_euler"): attr_name = "__backup_%s__" % attr val = obj.get(attr_name, None) if val is not None: setattr(obj, attr, val) del obj[attr_name] logger.debug(f"Restored {attr} for {obj.name}") def __backupTransforms(self, obj: bpy.types.Object) -> None: for attr in ("location", "rotation_euler"): attr_name = "__backup_%s__" % attr if attr_name in obj: # should not happen in normal build/clean cycle continue obj[attr_name] = getattr(obj, attr, None) logger.debug(f"Backed up {attr} for {obj.name}") def __preBuild(self) -> None: logger.debug("Pre-build preparation") self.__fake_parent_map: Dict[bpy.types.Object, List[bpy.types.Object]] = {} self.__rigid_body_matrix_map: Dict[bpy.types.Object, Any] = {} self.__empty_parent_map: Dict[bpy.types.Object, bpy.types.Object] = {} no_parents: List[bpy.types.Object] = [] 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: List[bpy.types.Object] = [] 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) -> None: logger.debug("Post-build finalization") 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 logger.debug(f"Parented empty {empty.name} to rigid object {rigid_obj.name}") 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 logger.debug(f"Enabled rigid track constraint for {p_bone.name}") def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float) -> None: assert rigid_obj.mmd_type == "RIGID_BODY" rb = rigid_obj.rigid_body if rb is None: logger.warning(f"No rigid body for {rigid_obj.name}") 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 logger.debug(f"Updated rigid body {rigid_obj.name} with type {rigid_type}") def __getRigidRange(self, obj: bpy.types.Object) -> float: return (Vector(obj.bound_box[0]) - Vector(obj.bound_box[6])).length def __createNonCollisionConstraint(self, nonCollisionJointTable: List[Tuple[bpy.types.Object, bpy.types.Object]]) -> None: 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 = bpyutils.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 = bpyutils.duplicateObject(ncc_obj, total_len) logger.debug(" created %d ncc.", len(ncc_objs)) for ncc_obj, pair in zip(ncc_objs, nonCollisionJointTable): 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: float, collision_margin: float) -> List[bpy.types.Object]: logger.debug("--------------------------------") logger.debug(" Build riggings of rigid bodies") logger.debug("--------------------------------") rigid_objects = list(self.rigidBodies()) rigid_object_groups: List[List[bpy.types.Object]] = [[] for i in range(16)] for i in rigid_objects: rigid_object_groups[i.mmd_rigid.collision_group_number].append(i) jointMap: Dict[frozenset, bpy.types.Object] = {} 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: List[Tuple[bpy.types.Object, bpy.types.Object]] = [] non_collision_pairs: Set[frozenset] = 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) -> None: logger.info("Building joints") 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) logger.debug(f"Built joint: {i.name}") def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]) -> None: armature_object = self.armature() armature: bpy.types.Armature with bpyutils.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) -> None: logger.info("Disconnecting physics bones") def editor(edit_bone: bpy.types.EditBone) -> None: rna_prop_ui.rna_idprop_ui_create(edit_bone, "mmd_bone_use_connect", default=edit_bone.use_connect) edit_bone.use_connect = False logger.debug(f"Disconnected bone: {edit_bone.name}") self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)}) def connectPhysicsBones(self) -> None: logger.info("Connecting physics bones") def editor(edit_bone: bpy.types.EditBone) -> None: 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) logger.debug(f"Connected bone: {edit_bone.name}") del edit_bone["mmd_bone_use_connect"] self.__editPhysicsBones(editor, {str(MODE_STATIC), str(MODE_DYNAMIC), str(MODE_DYNAMIC_BONE)})