Upfate Bone and Camrea

This commit is contained in:
Yusarina
2025-04-17 00:02:18 +01:00
parent d1af3fffed
commit bf92ca905b
2 changed files with 363 additions and 206 deletions
+168 -76
View File
@@ -6,21 +6,24 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import TYPE_CHECKING, Iterable, Optional, Set from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Dict, Tuple, Any, Union, cast
import bpy import bpy
from mathutils import Vector from mathutils import Vector
from bpy.types import Object, EditBone, PoseBone, Constraint, Armature, BoneCollection
from .. import bpyutils from .. import bpyutils
from ..bpyutils import TransformConstraintOp from ..bpyutils import TransformConstraintOp
from ..utils import ItemOp from ..utils import ItemOp
from ....logging_setup import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from ..properties.root import MMDRoot, MMDDisplayItemFrame from ..properties.root import MMDRoot, MMDDisplayItemFrame
from ..properties.pose_bone import MMDBone from ..properties.pose_bone import MMDBone
def remove_constraint(constraints, name): def remove_constraint(constraints: bpy.types.ConstraintSequence, name: str) -> bool:
"""Remove a constraint by name if it exists"""
c = constraints.get(name, None) c = constraints.get(name, None)
if c: if c:
constraints.remove(c) constraints.remove(c)
@@ -28,7 +31,8 @@ def remove_constraint(constraints, name):
return False return False
def remove_edit_bones(edit_bones, bone_names): def remove_edit_bones(edit_bones: bpy.types.ArmatureEditBones, bone_names: List[str]) -> None:
"""Remove edit bones by name"""
for name in bone_names: for name in bone_names:
b = edit_bones.get(name, None) b = edit_bones.get(name, None)
if b: if b:
@@ -45,33 +49,39 @@ SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NA
class FnBone: class FnBone:
AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首") AUTO_LOCAL_AXIS_ARMS: Tuple[str, ...] = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指") AUTO_LOCAL_AXIS_FINGERS: Tuple[str, ...] = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー") AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: Tuple[str, ...] = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
def __init__(self): def __init__(self) -> None:
raise NotImplementedError("This class cannot be instantiated.") raise NotImplementedError("This class cannot be instantiated.")
@staticmethod @staticmethod
def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]: def find_pose_bone_by_bone_id(armature_object: Object, bone_id: int) -> Optional[PoseBone]:
"""Find a pose bone by its bone ID"""
for bone in armature_object.pose.bones: for bone in armature_object.pose.bones:
if bone.mmd_bone.bone_id != bone_id: if bone.mmd_bone.bone_id != bone_id:
continue continue
return bone return bone
logger.debug(f"Bone with ID {bone_id} not found in armature {armature_object.name}")
return None return None
@staticmethod @staticmethod
def __new_bone_id(armature_object: bpy.types.Object) -> int: def __new_bone_id(armature_object: Object) -> int:
"""Generate a new unique bone ID"""
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1 return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
@staticmethod @staticmethod
def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int: def get_or_assign_bone_id(pose_bone: PoseBone) -> int:
"""Get the bone ID or assign a new one if not set"""
if pose_bone.mmd_bone.bone_id < 0: if pose_bone.mmd_bone.bone_id < 0:
pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data) pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data)
logger.debug(f"Assigned new bone ID {pose_bone.mmd_bone.bone_id} to bone {pose_bone.name}")
return pose_bone.mmd_bone.bone_id return pose_bone.mmd_bone.bone_id
@staticmethod @staticmethod
def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]: def __get_selected_pose_bones(armature_object: Object) -> Iterable[PoseBone]:
"""Get selected pose bones from the armature"""
if armature_object.mode == "EDIT": if armature_object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
bpy.ops.object.mode_set(mode="EDIT") # back to edit mode bpy.ops.object.mode_set(mode="EDIT") # back to edit mode
@@ -80,9 +90,11 @@ class FnBone:
return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone) return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone)
@staticmethod @staticmethod
def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True): def load_bone_fixed_axis(armature_object: Object, enable: bool = True) -> None:
"""Load fixed axis settings for selected bones"""
logger.debug(f"Loading bone fixed axis (enable={enable}) for {armature_object.name}")
for b in FnBone.__get_selected_pose_bones(armature_object): for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone: MMDBone = b.mmd_bone mmd_bone = b.mmd_bone
mmd_bone.enabled_fixed_axis = enable mmd_bone.enabled_fixed_axis = enable
lock_rotation = b.lock_rotation[:] lock_rotation = b.lock_rotation[:]
if enable: if enable:
@@ -97,53 +109,66 @@ class FnBone:
b.lock_location = b.lock_scale = (False, False, False) b.lock_location = b.lock_scale = (False, False, False)
@staticmethod @staticmethod
def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object: def setup_special_bone_collections(armature_object: Object) -> Object:
armature: bpy.types.Armature = armature_object.data """Set up special bone collections for MMD"""
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections bone_collections = armature.collections
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES: for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
if bone_collection_name in bone_collections: if bone_collection_name in bone_collections:
continue continue
bone_collection = bone_collections.new(bone_collection_name) bone_collection = bone_collections.new(bone_collection_name)
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False) FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
logger.debug(f"Created special bone collection: {bone_collection_name}")
return armature_object return armature_object
@staticmethod @staticmethod
def __is_mmd_tools_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: def __is_mmd_tools_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is an MMD Tools collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
@staticmethod @staticmethod
def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: def __is_special_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is a special MMD collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
@staticmethod @staticmethod
def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool): def __set_bone_collection_to_special(bone_collection: BoneCollection, is_visible: bool) -> None:
"""Mark a bone collection as special"""
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
bone_collection.is_visible = is_visible bone_collection.is_visible = is_visible
@staticmethod @staticmethod
def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: def __is_normal_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is a normal MMD collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
@staticmethod @staticmethod
def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection): def __set_bone_collection_to_normal(bone_collection: BoneCollection) -> None:
"""Mark a bone collection as normal"""
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
@staticmethod @staticmethod
def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone: def __set_edit_bone_to_special(edit_bone: EditBone, bone_collection_name: str) -> EditBone:
"""Set an edit bone to a special collection"""
edit_bone.id_data.collections[bone_collection_name].assign(edit_bone) edit_bone.id_data.collections[bone_collection_name].assign(edit_bone)
edit_bone.use_deform = False edit_bone.use_deform = False
return edit_bone return edit_bone
@staticmethod @staticmethod
def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: def set_edit_bone_to_dummy(edit_bone: EditBone) -> EditBone:
"""Set an edit bone as a dummy bone"""
logger.debug(f"Setting bone {edit_bone.name} as dummy bone")
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY) return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
@staticmethod @staticmethod
def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: def set_edit_bone_to_shadow(edit_bone: EditBone) -> EditBone:
"""Set an edit bone as a shadow bone"""
logger.debug(f"Setting bone {edit_bone.name} as shadow bone")
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW) return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
@staticmethod @staticmethod
def __unassign_mmd_tools_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: def __unassign_mmd_tools_bone_collections(edit_bone: EditBone) -> EditBone:
"""Unassign an edit bone from all MMD Tools collections"""
for bone_collection in edit_bone.collections: for bone_collection in edit_bone.collections:
if not FnBone.__is_mmd_tools_bone_collection(bone_collection): if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
continue continue
@@ -151,18 +176,24 @@ class FnBone:
return edit_bone return edit_bone
@staticmethod @staticmethod
def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object): def sync_bone_collections_from_display_item_frames(armature_object: Object) -> None:
armature: bpy.types.Armature = armature_object.data """Synchronize bone collections from display item frames"""
logger.info(f"Syncing bone collections from display item frames for {armature_object.name}")
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections bone_collections = armature.collections
from .model import FnModel from .model import FnModel
root_object: bpy.types.Object = FnModel.find_root_object(armature_object) root_object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root if not root_object:
logger.error(f"No root object found for armature {armature_object.name}")
return
mmd_root = root_object.mmd_root
bones = armature.bones bones = armature.bones
used_groups = set() used_groups: Set[str] = set()
unassigned_bone_names = {b.name for b in bones} unassigned_bone_names: Set[str] = {b.name for b in bones}
for frame in mmd_root.display_item_frames: for frame in mmd_root.display_item_frames:
for item in frame.data: for item in frame.data:
@@ -174,6 +205,7 @@ class FnBone:
if bone_collection is None: if bone_collection is None:
bone_collection = bone_collections.new(name=group_name) bone_collection = bone_collections.new(name=group_name)
FnBone.__set_bone_collection_to_normal(bone_collection) FnBone.__set_bone_collection_to_normal(bone_collection)
logger.debug(f"Created new bone collection: {group_name}")
bone_collection.assign(bones[item.name]) bone_collection.assign(bones[item.name])
for name in unassigned_bone_names: for name in unassigned_bone_names:
@@ -192,32 +224,40 @@ class FnBone:
continue continue
if not FnBone.__is_normal_bone_collection(bone_collection): if not FnBone.__is_normal_bone_collection(bone_collection):
continue continue
logger.debug(f"Removing unused bone collection: {bone_collection.name}")
bone_collections.remove(bone_collection) bone_collections.remove(bone_collection)
@staticmethod @staticmethod
def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object): def sync_display_item_frames_from_bone_collections(armature_object: Object) -> None:
armature: bpy.types.Armature = armature_object.data """Synchronize display item frames from bone collections"""
bone_collections: bpy.types.BoneCollections = armature.collections logger.info(f"Syncing display item frames from bone collections for {armature_object.name}")
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections
from .model import FnModel from .model import FnModel
root_object: bpy.types.Object = FnModel.find_root_object(armature_object) root_object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root if not root_object:
logger.error(f"No root object found for armature {armature_object.name}")
return
mmd_root = root_object.mmd_root
display_item_frames = mmd_root.display_item_frames display_item_frames = mmd_root.display_item_frames
used_frame_index: Set[int] = set() used_frame_index: Set[int] = set()
bone_collection: bpy.types.BoneCollection bone_collection: BoneCollection
for bone_collection in bone_collections: for bone_collection in bone_collections:
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection): if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
continue continue
bone_collection_name = bone_collection.name bone_collection_name = bone_collection.name
display_item_frame: Optional[MMDDisplayItemFrame] = display_item_frames.get(bone_collection_name) display_item_frame = display_item_frames.get(bone_collection_name)
if display_item_frame is None: if display_item_frame is None:
display_item_frame = display_item_frames.add() display_item_frame = display_item_frames.add()
display_item_frame.name = bone_collection_name display_item_frame.name = bone_collection_name
display_item_frame.name_e = bone_collection_name display_item_frame.name_e = bone_collection_name
logger.debug(f"Created new display item frame: {bone_collection_name}")
used_frame_index.add(display_item_frames.find(bone_collection_name)) used_frame_index.add(display_item_frames.find(bone_collection_name))
ItemOp.resize(display_item_frame.data, len(bone_collection.bones)) ItemOp.resize(display_item_frame.data, len(bone_collection.bones))
@@ -232,23 +272,27 @@ class FnBone:
if display_item_frame.is_special: if display_item_frame.is_special:
if display_item_frame.name != "表情": if display_item_frame.name != "表情":
display_item_frame.data.clear() display_item_frame.data.clear()
logger.debug(f"Cleared special display item frame: {display_item_frame.name}")
else: else:
logger.debug(f"Removing unused display item frame: {display_item_frames[i].name}")
display_item_frames.remove(i) display_item_frames.remove(i)
mmd_root.active_display_item_frame = 0 mmd_root.active_display_item_frame = 0
@staticmethod @staticmethod
def apply_bone_fixed_axis(armature_object: bpy.types.Object): def apply_bone_fixed_axis(armature_object: Object) -> None:
bone_map = {} """Apply fixed axis to bones"""
logger.info(f"Applying bone fixed axis for {armature_object.name}")
bone_map: Dict[str, Tuple[Vector, bool, bool]] = {}
for b in armature_object.pose.bones: for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis: if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
continue continue
mmd_bone: MMDBone = b.mmd_bone mmd_bone = b.mmd_bone
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip) bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
force_align = True force_align = True
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone bone: EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_map: if bone.name not in bone_map:
bone.select = False bone.select = False
@@ -279,6 +323,7 @@ class FnBone:
else: else:
bone_map[bone.name] = (True, True, True) bone_map[bone.name] = (True, True, True)
bone.select = True bone.select = True
logger.debug(f"Applied fixed axis to bone: {bone.name}")
for bone_name, locks in bone_map.items(): for bone_name, locks in bone_map.items():
b = armature_object.pose.bones[bone_name] b = armature_object.pose.bones[bone_name]
@@ -286,9 +331,11 @@ class FnBone:
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
@staticmethod @staticmethod
def load_bone_local_axes(armature_object: bpy.types.Object, enable=True): def load_bone_local_axes(armature_object: Object, enable: bool = True) -> None:
"""Load local axes for selected bones"""
logger.debug(f"Loading bone local axes (enable={enable}) for {armature_object.name}")
for b in FnBone.__get_selected_pose_bones(armature_object): for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone: MMDBone = b.mmd_bone mmd_bone = b.mmd_bone
mmd_bone.enabled_local_axes = enable mmd_bone.enabled_local_axes = enable
if enable: if enable:
axes = b.bone.matrix_local.to_3x3().transposed() axes = b.bone.matrix_local.to_3x3().transposed()
@@ -296,16 +343,18 @@ class FnBone:
mmd_bone.local_axis_z = axes[2].xzy mmd_bone.local_axis_z = axes[2].xzy
@staticmethod @staticmethod
def apply_bone_local_axes(armature_object: bpy.types.Object): def apply_bone_local_axes(armature_object: Object) -> None:
bone_map = {} """Apply local axes to bones"""
logger.info(f"Applying bone local axes for {armature_object.name}")
bone_map: Dict[str, Tuple[Vector, Vector]] = {}
for b in armature_object.pose.bones: for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes: if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
continue continue
mmd_bone: MMDBone = b.mmd_bone mmd_bone = b.mmd_bone
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z) bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone bone: EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_map: if bone.name not in bone_map:
bone.select = False bone.select = False
@@ -313,15 +362,18 @@ class FnBone:
local_axis_x, local_axis_z = bone_map[bone.name] local_axis_x, local_axis_z = bone_map[bone.name]
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z) FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
bone.select = True bone.select = True
logger.debug(f"Applied local axes to bone: {bone.name}")
@staticmethod @staticmethod
def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z): def update_bone_roll(edit_bone: EditBone, mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> None:
"""Update bone roll based on local axes"""
axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z) axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z)
idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1])) idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3]) edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
@staticmethod @staticmethod
def get_axes(mmd_local_axis_x, mmd_local_axis_z): def get_axes(mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> Tuple[Vector, Vector, Vector]:
"""Get axes from local axis vectors"""
x_axis = Vector(mmd_local_axis_x).normalized().xzy x_axis = Vector(mmd_local_axis_x).normalized().xzy
z_axis = Vector(mmd_local_axis_z).normalized().xzy z_axis = Vector(mmd_local_axis_z).normalized().xzy
y_axis = z_axis.cross(x_axis).normalized() y_axis = z_axis.cross(x_axis).normalized()
@@ -329,21 +381,25 @@ class FnBone:
return (x_axis, y_axis, z_axis) return (x_axis, y_axis, z_axis)
@staticmethod @staticmethod
def apply_auto_bone_roll(armature): def apply_auto_bone_roll(armature: Object) -> None:
bone_names = [] """Apply automatic bone roll to appropriate bones"""
logger.info(f"Applying auto bone roll for {armature.name}")
bone_names: List[str] = []
for b in armature.pose.bones: for b in armature.pose.bones:
if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j): if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j):
bone_names.append(b.name) bone_names.append(b.name)
with bpyutils.edit_object(armature) as data: with bpyutils.edit_object(armature) as data:
bone: bpy.types.EditBone bone: EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_names: if bone.name not in bone_names:
continue continue
FnBone.update_auto_bone_roll(bone) FnBone.update_auto_bone_roll(bone)
bone.select = True bone.select = True
logger.debug(f"Applied auto bone roll to bone: {bone.name}")
@staticmethod @staticmethod
def update_auto_bone_roll(edit_bone): def update_auto_bone_roll(edit_bone: EditBone) -> None:
"""Update bone roll automatically"""
# make a triangle face (p1,p2,p3) # make a triangle face (p1,p2,p3)
p1 = edit_bone.head.copy() p1 = edit_bone.head.copy()
p2 = edit_bone.tail.copy() p2 = edit_bone.tail.copy()
@@ -364,7 +420,8 @@ class FnBone:
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy) FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
@staticmethod @staticmethod
def has_auto_local_axis(name_j): def has_auto_local_axis(name_j: str) -> bool:
"""Check if a bone should have automatic local axis"""
if name_j: if name_j:
if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
return True return True
@@ -374,9 +431,11 @@ class FnBone:
return False return False
@staticmethod @staticmethod
def clean_additional_transformation(armature_object: bpy.types.Object): def clean_additional_transformation(armature_object: Object) -> None:
"""Clean additional transformation constraints and bones"""
logger.info(f"Cleaning additional transformations for {armature_object.name}")
# clean constraints # clean constraints
p_bone: bpy.types.PoseBone p_bone: PoseBone
for p_bone in armature_object.pose.bones: for p_bone in armature_object.pose.bones:
p_bone.mmd_bone.is_additional_transform_dirty = True p_bone.mmd_bone.is_additional_transform_dirty = True
constraints = p_bone.constraints constraints = p_bone.constraints
@@ -392,17 +451,21 @@ class FnBone:
"ADDITIONAL_TRANSFORM_INVERT", "ADDITIONAL_TRANSFORM_INVERT",
} }
def __is_at_shadow_bone(b): def __is_at_shadow_bone(b: PoseBone) -> bool:
return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)] shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
if len(shadow_bone_names) > 0: if len(shadow_bone_names) > 0:
logger.debug(f"Removing {len(shadow_bone_names)} shadow bones")
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
remove_edit_bones(data.edit_bones, shadow_bone_names) remove_edit_bones(data.edit_bones, shadow_bone_names)
@staticmethod @staticmethod
def apply_additional_transformation(armature_object: bpy.types.Object): def apply_additional_transformation(armature_object: Object) -> None:
def __is_dirty_bone(b): """Apply additional transformation to bones"""
logger.info(f"Applying additional transformations for {armature_object.name}")
def __is_dirty_bone(b: PoseBone) -> bool:
if b.is_mmd_shadow_bone: if b.is_mmd_shadow_bone:
return False return False
mmd_bone = b.mmd_bone mmd_bone = b.mmd_bone
@@ -411,9 +474,10 @@ class FnBone:
return mmd_bone.is_additional_transform_dirty return mmd_bone.is_additional_transform_dirty
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)] dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
logger.debug(f"Found {len(dirty_bones)} dirty bones to process")
# setup constraints # setup constraints
shadow_bone_pool = [] shadow_bone_pool: List[Union[_AT_ShadowBoneRemove, _AT_ShadowBoneCreate]] = []
for p_bone in dirty_bones: for p_bone in dirty_bones:
sb = FnBone.__setup_constraints(p_bone) sb = FnBone.__setup_constraints(p_bone)
if sb: if sb:
@@ -434,7 +498,8 @@ class FnBone:
p_bone.mmd_bone.is_additional_transform_dirty = False p_bone.mmd_bone.is_additional_transform_dirty = False
@staticmethod @staticmethod
def __setup_constraints(p_bone): def __setup_constraints(p_bone: PoseBone) -> Optional[Union['_AT_ShadowBoneRemove', '_AT_ShadowBoneCreate']]:
"""Set up constraints for additional transformation"""
bone_name = p_bone.name bone_name = p_bone.name
mmd_bone = p_bone.mmd_bone mmd_bone = p_bone.mmd_bone
influence = mmd_bone.additional_transform_influence influence = mmd_bone.additional_transform_influence
@@ -447,12 +512,14 @@ class FnBone:
rot = remove_constraint(constraints, "mmd_additional_rotation") rot = remove_constraint(constraints, "mmd_additional_rotation")
loc = remove_constraint(constraints, "mmd_additional_location") loc = remove_constraint(constraints, "mmd_additional_location")
if rot or loc: if rot or loc:
logger.debug(f"Removing additional transform constraints for bone: {bone_name}")
return _AT_ShadowBoneRemove(bone_name) return _AT_ShadowBoneRemove(bone_name)
return None return None
logger.debug(f"Setting up additional transform for bone: {bone_name} targeting {target_bone}")
shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone) shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone)
def __config(name, mute, map_type, value): def __config(name: str, mute: bool, map_type: str, value: float) -> None:
if mute: if mute:
remove_constraint(constraints, name) remove_constraint(constraints, name)
return return
@@ -467,62 +534,81 @@ class FnBone:
return shadow_bone return shadow_bone
@staticmethod @staticmethod
def update_additional_transform_influence(pose_bone: bpy.types.PoseBone): def update_additional_transform_influence(pose_bone: PoseBone) -> None:
"""Update the influence of additional transform constraints"""
influence = pose_bone.mmd_bone.additional_transform_influence influence = pose_bone.mmd_bone.additional_transform_influence
constraints = pose_bone.constraints constraints = pose_bone.constraints
c = constraints.get("mmd_additional_rotation", None) c = constraints.get("mmd_additional_rotation", None)
TransformConstraintOp.update_min_max(c, math.pi, influence) TransformConstraintOp.update_min_max(c, math.pi, influence)
c = constraints.get("mmd_additional_location", None) c = constraints.get("mmd_additional_location", None)
TransformConstraintOp.update_min_max(c, 100, influence) TransformConstraintOp.update_min_max(c, 100, influence)
logger.debug(f"Updated additional transform influence for bone: {pose_bone.name} to {influence}")
class MigrationFnBone: class MigrationFnBone:
"""Migration Functions for old MMD models broken by bugs or issues""" """Migration Functions for old MMD models broken by bugs or issues"""
@staticmethod @staticmethod
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object): def fix_mmd_ik_limit_override(armature_object: Object) -> None:
pose_bone: bpy.types.PoseBone """Fix IK limit override constraints in old MMD models"""
logger.info(f"Fixing MMD IK limit overrides for {armature_object.name}")
pose_bone: PoseBone
for pose_bone in armature_object.pose.bones: for pose_bone in armature_object.pose.bones:
constraint: bpy.types.Constraint constraint: Constraint
for constraint in pose_bone.constraints: for constraint in pose_bone.constraints:
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name: if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
constraint.owner_space = "LOCAL" constraint.owner_space = "LOCAL"
logger.debug(f"Fixed IK limit override for bone: {pose_bone.name}")
class _AT_ShadowBoneRemove: class _AT_ShadowBoneRemove:
def __init__(self, bone_name): """Handler for removing shadow bones"""
def __init__(self, bone_name: str) -> None:
"""Initialize with bone name"""
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name) self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
def update_edit_bones(self, edit_bones): def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
"""Update edit bones by removing shadow bones"""
remove_edit_bones(edit_bones, self.__shadow_bone_names) remove_edit_bones(edit_bones, self.__shadow_bone_names)
logger.debug(f"Removed shadow bones: {self.__shadow_bone_names}")
def update_pose_bones(self, pose_bones): def update_pose_bones(self, pose_bones: bpy.types.ArmaturePoseBones) -> None:
"""Update pose bones (no-op for removal)"""
pass pass
class _AT_ShadowBoneCreate: class _AT_ShadowBoneCreate:
def __init__(self, bone_name, target_bone_name): """Handler for creating shadow bones"""
def __init__(self, bone_name: str, target_bone_name: str) -> None:
"""Initialize with bone names"""
self.__dummy_bone_name = "_dummy_" + bone_name self.__dummy_bone_name = "_dummy_" + bone_name
self.__shadow_bone_name = "_shadow_" + bone_name self.__shadow_bone_name = "_shadow_" + bone_name
self.__bone_name = bone_name self.__bone_name = bone_name
self.__target_bone_name = target_bone_name self.__target_bone_name = target_bone_name
self.__constraint_pool = [] self.__constraint_pool: List[Constraint] = []
def __is_well_aligned(self, bone0, bone1): def __is_well_aligned(self, bone0: EditBone, bone1: EditBone) -> bool:
"""Check if two bones are well aligned"""
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99 return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
def __update_constraints(self, use_shadow=True): def __update_constraints(self, use_shadow: bool = True) -> None:
"""Update constraints to use shadow or target bone"""
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
for c in self.__constraint_pool: for c in self.__constraint_pool:
c.subtarget = subtarget c.subtarget = subtarget
def add_constraint(self, constraint): def add_constraint(self, constraint: Constraint) -> None:
"""Add a constraint to the pool"""
self.__constraint_pool.append(constraint) self.__constraint_pool.append(constraint)
def update_edit_bones(self, edit_bones): def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
"""Update edit bones by creating shadow bones"""
bone = edit_bones[self.__bone_name] bone = edit_bones[self.__bone_name]
target_bone = edit_bones[self.__target_bone_name] target_bone = edit_bones[self.__target_bone_name]
if bone != target_bone and self.__is_well_aligned(bone, target_bone): if bone != target_bone and self.__is_well_aligned(bone, target_bone):
logger.debug(f"Bones are well aligned, removing shadow bones for {self.__bone_name}")
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones) _AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
return return
@@ -532,6 +618,7 @@ class _AT_ShadowBoneCreate:
dummy.head = target_bone.head dummy.head = target_bone.head
dummy.tail = dummy.head + bone.tail - bone.head dummy.tail = dummy.head + bone.tail - bone.head
dummy.roll = bone.roll dummy.roll = bone.roll
logger.debug(f"Created/updated dummy bone: {dummy_bone_name}")
shadow_bone_name = self.__shadow_bone_name shadow_bone_name = self.__shadow_bone_name
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name)) shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
@@ -539,9 +626,12 @@ class _AT_ShadowBoneCreate:
shadow.head = dummy.head shadow.head = dummy.head
shadow.tail = dummy.tail shadow.tail = dummy.tail
shadow.roll = bone.roll shadow.roll = bone.roll
logger.debug(f"Created/updated shadow bone: {shadow_bone_name}")
def update_pose_bones(self, pose_bones): def update_pose_bones(self, pose_bones: bpy.types.ArmaturePoseBones) -> None:
"""Update pose bones by setting up shadow bone properties"""
if self.__shadow_bone_name not in pose_bones: if self.__shadow_bone_name not in pose_bones:
logger.debug(f"Shadow bone {self.__shadow_bone_name} not found, using target bone directly")
self.__update_constraints(use_shadow=False) self.__update_constraints(use_shadow=False)
return return
@@ -560,5 +650,7 @@ class _AT_ShadowBoneCreate:
c.subtarget = dummy_p_bone.name c.subtarget = dummy_p_bone.name
c.target_space = "POSE" c.target_space = "POSE"
c.owner_space = "POSE" c.owner_space = "POSE"
logger.debug(f"Created copy transforms constraint for shadow bone: {self.__shadow_bone_name}")
self.__update_constraints() self.__update_constraints()
logger.debug(f"Updated constraints for shadow bone: {self.__shadow_bone_name}")
+88 -23
View File
@@ -6,16 +6,19 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import Optional from typing import Optional, List, Tuple, Callable, Any, Union
import bpy import bpy
from bpy.types import Object, ID, Camera, Context
from mathutils import Vector, Matrix, Euler
from ..bpyutils import FnContext, Props from ..bpyutils import FnContext, Props
from core.logging_setup import logger
class FnCamera: class FnCamera:
@staticmethod @staticmethod
def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]: def find_root(obj: Optional[Object]) -> Optional[Object]:
"""Find the root object of an MMD camera setup."""
if obj is None: if obj is None:
return None return None
if FnCamera.is_mmd_camera_root(obj): if FnCamera.is_mmd_camera_root(obj):
@@ -25,16 +28,22 @@ class FnCamera:
return None return None
@staticmethod @staticmethod
def is_mmd_camera(obj: bpy.types.Object) -> bool: def is_mmd_camera(obj: Object) -> bool:
"""Check if an object is an MMD camera."""
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
@staticmethod @staticmethod
def is_mmd_camera_root(obj: bpy.types.Object) -> bool: def is_mmd_camera_root(obj: Object) -> bool:
"""Check if an object is an MMD camera root."""
return obj.type == "EMPTY" and obj.mmd_type == "CAMERA" return obj.type == "EMPTY" and obj.mmd_type == "CAMERA"
@staticmethod @staticmethod
def add_drivers(camera_object: bpy.types.Object): def add_drivers(camera_object: Object) -> None:
def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1): """Add drivers to the camera object for MMD camera functionality."""
logger.debug(f"Adding drivers to camera: {camera_object.name}")
def __add_driver(id_data: ID, data_path: str, expression: str, index: int = -1) -> None:
"""Add a driver to the specified ID data."""
d = id_data.driver_add(data_path, index).driver d = id_data.driver_add(data_path, index).driver
d.type = "SCRIPTED" d.type = "SCRIPTED"
if "$empty_distance" in expression: if "$empty_distance" in expression:
@@ -72,22 +81,36 @@ class FnCamera:
d.expression = expression d.expression = expression
try:
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45") __add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
__add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1) __add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1)
__add_driver(camera_object.data, "type", "not $is_perspective") __add_driver(camera_object.data, "type", "not $is_perspective")
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2") __add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
logger.debug(f"Successfully added drivers to camera: {camera_object.name}")
except Exception as e:
logger.error(f"Failed to add drivers to camera {camera_object.name}: {str(e)}")
@staticmethod @staticmethod
def remove_drivers(camera_object: bpy.types.Object): def remove_drivers(camera_object: Object) -> None:
"""Remove drivers from the camera object."""
logger.debug(f"Removing drivers from camera: {camera_object.name}")
try:
camera_object.data.driver_remove("ortho_scale") camera_object.data.driver_remove("ortho_scale")
camera_object.driver_remove("rotation_euler") camera_object.driver_remove("rotation_euler")
camera_object.data.driver_remove("ortho_scale") camera_object.data.driver_remove("ortho_scale")
camera_object.data.driver_remove("lens") camera_object.data.driver_remove("lens")
logger.debug(f"Successfully removed drivers from camera: {camera_object.name}")
except Exception as e:
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {str(e)}")
class MigrationFnCamera: class MigrationFnCamera:
@staticmethod @staticmethod
def update_mmd_camera(): def update_mmd_camera() -> None:
"""Update all MMD cameras in the scene."""
logger.info("Updating all MMD cameras in the scene")
updated_count = 0
for camera_object in bpy.data.objects: for camera_object in bpy.data.objects:
if camera_object.type != "CAMERA": if camera_object.type != "CAMERA":
continue continue
@@ -97,39 +120,57 @@ class MigrationFnCamera:
# It's not a MMD Camera # It's not a MMD Camera
continue continue
try:
FnCamera.remove_drivers(camera_object) FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object) FnCamera.add_drivers(camera_object)
updated_count += 1
except Exception as e:
logger.error(f"Failed to update MMD camera {camera_object.name}: {str(e)}")
logger.info(f"Updated {updated_count} MMD cameras")
class MMDCamera: class MMDCamera:
def __init__(self, obj): def __init__(self, obj: Object):
"""Initialize an MMD camera."""
root_object = FnCamera.find_root(obj) root_object = FnCamera.find_root(obj)
if root_object is None: if root_object is None:
raise ValueError("%s is not MMDCamera" % str(obj)) logger.error(f"Object {obj.name} is not an MMD camera")
raise ValueError(f"{obj.name} is not an MMD camera")
self.__emptyObj = getattr(root_object, "original", obj) self.__emptyObj = getattr(root_object, "original", obj)
logger.debug(f"Initialized MMD camera with root: {self.__emptyObj.name}")
@staticmethod @staticmethod
def isMMDCamera(obj: bpy.types.Object) -> bool: def isMMDCamera(obj: Object) -> bool:
"""Check if an object is an MMD camera."""
return FnCamera.find_root(obj) is not None return FnCamera.find_root(obj) is not None
@staticmethod @staticmethod
def addDrivers(cameraObj: bpy.types.Object): def addDrivers(cameraObj: Object) -> None:
"""Add drivers to the camera object."""
FnCamera.add_drivers(cameraObj) FnCamera.add_drivers(cameraObj)
@staticmethod @staticmethod
def removeDrivers(cameraObj: bpy.types.Object): def removeDrivers(cameraObj: Object) -> None:
"""Remove drivers from the camera object. """
if cameraObj.type != "CAMERA": if cameraObj.type != "CAMERA":
return return
FnCamera.remove_drivers(cameraObj) FnCamera.remove_drivers(cameraObj)
@staticmethod @staticmethod
def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0): def convertToMMDCamera(cameraObj: Object, scale: float = 1.0) -> 'MMDCamera':
"""Convert a camera to an MMD camera."""
logger.info(f"Converting camera {cameraObj.name} to MMD camera with scale {scale}")
if FnCamera.is_mmd_camera(cameraObj): if FnCamera.is_mmd_camera(cameraObj):
logger.debug(f"Camera {cameraObj.name} is already an MMD camera")
return MMDCamera(cameraObj) return MMDCamera(cameraObj)
try:
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None) empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty) context = FnContext.ensure_context()
FnContext.link_object(context, empty)
cameraObj.parent = empty cameraObj.parent = empty
cameraObj.data.sensor_fit = "VERTICAL" cameraObj.data.sensor_fit = "VERTICAL"
@@ -153,24 +194,39 @@ class MMDCamera:
empty.mmd_type = "CAMERA" empty.mmd_type = "CAMERA"
empty.mmd_camera.angle = math.radians(30) empty.mmd_camera.angle = math.radians(30)
empty.mmd_camera.persp = True empty.mmd_camera.persp = True
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
return MMDCamera(empty) return MMDCamera(empty)
except Exception as e:
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {str(e)}")
raise
@staticmethod @staticmethod
def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1): def newMMDCameraAnimation(
cameraObj: Optional[Object],
cameraTarget: Optional[Object] = None,
scale: float = 1.0,
min_distance: float = 0.1
) -> 'MMDCamera':
"""Create a new MMD camera animation."""
logger.info(f"Creating new MMD camera animation with scale {scale}")
try:
scene = bpy.context.scene scene = bpy.context.scene
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera")) mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
FnContext.link_object(FnContext.ensure_context(), mmd_cam) FnContext.link_object(FnContext.ensure_context(), mmd_cam)
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale) MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
mmd_cam_root = mmd_cam.parent mmd_cam_root = mmd_cam.parent
_camera_override_func = None _camera_override_func: Optional[Callable[[], Object]] = None
if cameraObj is None: if cameraObj is None:
if scene.camera is None: if scene.camera is None:
scene.camera = mmd_cam scene.camera = mmd_cam
logger.debug("Set scene camera to new MMD camera")
return MMDCamera(mmd_cam_root) return MMDCamera(mmd_cam_root)
_camera_override_func = lambda: scene.camera _camera_override_func = lambda: scene.camera
_target_override_func = None _target_override_func: Optional[Callable[[Object], Object]] = None
if cameraTarget is None: if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj _target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
@@ -180,7 +236,6 @@ class MMDCamera:
FnCamera.remove_drivers(mmd_cam) FnCamera.remove_drivers(mmd_cam)
from math import atan from math import atan
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
render = scene.render render = scene.render
@@ -202,6 +257,7 @@ class MMDCamera:
for c in fcurves: for c in fcurves:
c.keyframe_points.add(frame_count) c.keyframe_points.add(frame_count)
logger.debug(f"Processing {frame_count} frames for camera animation")
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)): for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)):
scene.frame_set(f) scene.frame_set(f)
if _camera_override_func: if _camera_override_func:
@@ -245,13 +301,22 @@ class MMDCamera:
mmd_cam_root.animation_data_create().action = parent_action mmd_cam_root.animation_data_create().action = parent_action
mmd_cam.animation_data_create().action = distance_action mmd_cam.animation_data_create().action = distance_action
scene.frame_set(frame_current) scene.frame_set(frame_current)
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
return MMDCamera(mmd_cam_root) return MMDCamera(mmd_cam_root)
def object(self): except Exception as e:
logger.error(f"Failed to create MMD camera animation: {str(e)}")
raise
def object(self) -> Object:
"""Get the root object of the MMD camera."""
return self.__emptyObj return self.__emptyObj
def camera(self): def camera(self) -> Object:
"""Get the camera object of the MMD camera."""
for i in self.__emptyObj.children: for i in self.__emptyObj.children:
if i.type == "CAMERA": if i.type == "CAMERA":
return i return i
raise KeyError logger.error(f"No camera found for MMD camera root {self.__emptyObj.name}")
raise KeyError(f"No camera found for MMD camera root {self.__emptyObj.name}")