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.
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
from mathutils import Vector
from bpy.types import Object, EditBone, PoseBone, Constraint, Armature, BoneCollection
from .. import bpyutils
from ..bpyutils import TransformConstraintOp
from ..utils import ItemOp
from ....logging_setup import logger
if TYPE_CHECKING:
from ..properties.root import MMDRoot, MMDDisplayItemFrame
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)
if c:
constraints.remove(c)
@@ -28,7 +31,8 @@ def remove_constraint(constraints, name):
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:
b = edit_bones.get(name, None)
if b:
@@ -45,33 +49,39 @@ SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NA
class FnBone:
AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
AUTO_LOCAL_AXIS_ARMS: Tuple[str, ...] = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS: Tuple[str, ...] = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: Tuple[str, ...] = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
def __init__(self):
def __init__(self) -> None:
raise NotImplementedError("This class cannot be instantiated.")
@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:
if bone.mmd_bone.bone_id != bone_id:
continue
return bone
logger.debug(f"Bone with ID {bone_id} not found in armature {armature_object.name}")
return None
@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
@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:
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
@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":
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
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)
@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):
mmd_bone: MMDBone = b.mmd_bone
mmd_bone = b.mmd_bone
mmd_bone.enabled_fixed_axis = enable
lock_rotation = b.lock_rotation[:]
if enable:
@@ -97,53 +109,66 @@ class FnBone:
b.lock_location = b.lock_scale = (False, False, False)
@staticmethod
def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object:
armature: bpy.types.Armature = armature_object.data
def setup_special_bone_collections(armature_object: Object) -> Object:
"""Set up special bone collections for MMD"""
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
if bone_collection_name in bone_collections:
continue
bone_collection = bone_collections.new(bone_collection_name)
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
logger.debug(f"Created special bone collection: {bone_collection_name}")
return armature_object
@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
@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)
@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.is_visible = is_visible
@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)
@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
@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.use_deform = False
return edit_bone
@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)
@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)
@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:
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
continue
@@ -151,18 +176,24 @@ class FnBone:
return edit_bone
@staticmethod
def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object):
armature: bpy.types.Armature = armature_object.data
def sync_bone_collections_from_display_item_frames(armature_object: Object) -> None:
"""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
from .model import FnModel
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root
root_object = FnModel.find_root_object(armature_object)
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
used_groups = set()
unassigned_bone_names = {b.name for b in bones}
used_groups: Set[str] = set()
unassigned_bone_names: Set[str] = {b.name for b in bones}
for frame in mmd_root.display_item_frames:
for item in frame.data:
@@ -174,6 +205,7 @@ class FnBone:
if bone_collection is None:
bone_collection = bone_collections.new(name=group_name)
FnBone.__set_bone_collection_to_normal(bone_collection)
logger.debug(f"Created new bone collection: {group_name}")
bone_collection.assign(bones[item.name])
for name in unassigned_bone_names:
@@ -192,32 +224,40 @@ class FnBone:
continue
if not FnBone.__is_normal_bone_collection(bone_collection):
continue
logger.debug(f"Removing unused bone collection: {bone_collection.name}")
bone_collections.remove(bone_collection)
@staticmethod
def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object):
armature: bpy.types.Armature = armature_object.data
bone_collections: bpy.types.BoneCollections = armature.collections
def sync_display_item_frames_from_bone_collections(armature_object: Object) -> None:
"""Synchronize display item frames from bone 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
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root
root_object = FnModel.find_root_object(armature_object)
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
used_frame_index: Set[int] = set()
bone_collection: bpy.types.BoneCollection
bone_collection: BoneCollection
for bone_collection in bone_collections:
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
continue
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:
display_item_frame = display_item_frames.add()
display_item_frame.name = 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))
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.name != "表情":
display_item_frame.data.clear()
logger.debug(f"Cleared special display item frame: {display_item_frame.name}")
else:
logger.debug(f"Removing unused display item frame: {display_item_frames[i].name}")
display_item_frames.remove(i)
mmd_root.active_display_item_frame = 0
@staticmethod
def apply_bone_fixed_axis(armature_object: bpy.types.Object):
bone_map = {}
def apply_bone_fixed_axis(armature_object: Object) -> None:
"""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:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
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
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
force_align = True
with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone
bone: EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
@@ -279,6 +323,7 @@ class FnBone:
else:
bone_map[bone.name] = (True, True, True)
bone.select = True
logger.debug(f"Applied fixed axis to bone: {bone.name}")
for bone_name, locks in bone_map.items():
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
@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):
mmd_bone: MMDBone = b.mmd_bone
mmd_bone = b.mmd_bone
mmd_bone.enabled_local_axes = enable
if enable:
axes = b.bone.matrix_local.to_3x3().transposed()
@@ -296,16 +343,18 @@ class FnBone:
mmd_bone.local_axis_z = axes[2].xzy
@staticmethod
def apply_bone_local_axes(armature_object: bpy.types.Object):
bone_map = {}
def apply_bone_local_axes(armature_object: Object) -> None:
"""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:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
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)
with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone
bone: EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
@@ -313,15 +362,18 @@ class FnBone:
local_axis_x, local_axis_z = bone_map[bone.name]
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
bone.select = True
logger.debug(f"Applied local axes to bone: {bone.name}")
@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)
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])
@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
z_axis = Vector(mmd_local_axis_z).normalized().xzy
y_axis = z_axis.cross(x_axis).normalized()
@@ -329,21 +381,25 @@ class FnBone:
return (x_axis, y_axis, z_axis)
@staticmethod
def apply_auto_bone_roll(armature):
bone_names = []
def apply_auto_bone_roll(armature: Object) -> None:
"""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:
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)
with bpyutils.edit_object(armature) as data:
bone: bpy.types.EditBone
bone: EditBone
for bone in data.edit_bones:
if bone.name not in bone_names:
continue
FnBone.update_auto_bone_roll(bone)
bone.select = True
logger.debug(f"Applied auto bone roll to bone: {bone.name}")
@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)
p1 = edit_bone.head.copy()
p2 = edit_bone.tail.copy()
@@ -364,7 +420,8 @@ class FnBone:
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
@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 in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
return True
@@ -374,9 +431,11 @@ class FnBone:
return False
@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
p_bone: bpy.types.PoseBone
p_bone: PoseBone
for p_bone in armature_object.pose.bones:
p_bone.mmd_bone.is_additional_transform_dirty = True
constraints = p_bone.constraints
@@ -392,17 +451,21 @@ class FnBone:
"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
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
if len(shadow_bone_names) > 0:
logger.debug(f"Removing {len(shadow_bone_names)} shadow bones")
with bpyutils.edit_object(armature_object) as data:
remove_edit_bones(data.edit_bones, shadow_bone_names)
@staticmethod
def apply_additional_transformation(armature_object: bpy.types.Object):
def __is_dirty_bone(b):
def apply_additional_transformation(armature_object: Object) -> None:
"""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:
return False
mmd_bone = b.mmd_bone
@@ -411,9 +474,10 @@ class FnBone:
return mmd_bone.is_additional_transform_dirty
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
shadow_bone_pool = []
shadow_bone_pool: List[Union[_AT_ShadowBoneRemove, _AT_ShadowBoneCreate]] = []
for p_bone in dirty_bones:
sb = FnBone.__setup_constraints(p_bone)
if sb:
@@ -434,7 +498,8 @@ class FnBone:
p_bone.mmd_bone.is_additional_transform_dirty = False
@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
mmd_bone = p_bone.mmd_bone
influence = mmd_bone.additional_transform_influence
@@ -447,12 +512,14 @@ class FnBone:
rot = remove_constraint(constraints, "mmd_additional_rotation")
loc = remove_constraint(constraints, "mmd_additional_location")
if rot or loc:
logger.debug(f"Removing additional transform constraints for bone: {bone_name}")
return _AT_ShadowBoneRemove(bone_name)
return None
logger.debug(f"Setting up additional transform for bone: {bone_name} targeting {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:
remove_constraint(constraints, name)
return
@@ -467,62 +534,81 @@ class FnBone:
return shadow_bone
@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
constraints = pose_bone.constraints
c = constraints.get("mmd_additional_rotation", None)
TransformConstraintOp.update_min_max(c, math.pi, influence)
c = constraints.get("mmd_additional_location", None)
TransformConstraintOp.update_min_max(c, 100, influence)
logger.debug(f"Updated additional transform influence for bone: {pose_bone.name} to {influence}")
class MigrationFnBone:
"""Migration Functions for old MMD models broken by bugs or issues"""
@staticmethod
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object):
pose_bone: bpy.types.PoseBone
def fix_mmd_ik_limit_override(armature_object: Object) -> None:
"""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:
constraint: bpy.types.Constraint
constraint: Constraint
for constraint in pose_bone.constraints:
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
constraint.owner_space = "LOCAL"
logger.debug(f"Fixed IK limit override for bone: {pose_bone.name}")
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)
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)
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
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.__shadow_bone_name = "_shadow_" + bone_name
self.__bone_name = 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
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
for c in self.__constraint_pool:
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)
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]
target_bone = edit_bones[self.__target_bone_name]
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)
return
@@ -532,6 +618,7 @@ class _AT_ShadowBoneCreate:
dummy.head = target_bone.head
dummy.tail = dummy.head + bone.tail - bone.head
dummy.roll = bone.roll
logger.debug(f"Created/updated dummy bone: {dummy_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))
@@ -539,9 +626,12 @@ class _AT_ShadowBoneCreate:
shadow.head = dummy.head
shadow.tail = dummy.tail
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:
logger.debug(f"Shadow bone {self.__shadow_bone_name} not found, using target bone directly")
self.__update_constraints(use_shadow=False)
return
@@ -560,5 +650,7 @@ class _AT_ShadowBoneCreate:
c.subtarget = dummy_p_bone.name
c.target_space = "POSE"
c.owner_space = "POSE"
logger.debug(f"Created copy transforms constraint for shadow bone: {self.__shadow_bone_name}")
self.__update_constraints()
logger.debug(f"Updated constraints for shadow bone: {self.__shadow_bone_name}")
+195 -130
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.
import math
from typing import Optional
from typing import Optional, List, Tuple, Callable, Any, Union
import bpy
from bpy.types import Object, ID, Camera, Context
from mathutils import Vector, Matrix, Euler
from ..bpyutils import FnContext, Props
from core.logging_setup import logger
class FnCamera:
@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:
return None
if FnCamera.is_mmd_camera_root(obj):
@@ -25,16 +28,22 @@ class FnCamera:
return None
@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
@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"
@staticmethod
def add_drivers(camera_object: bpy.types.Object):
def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1):
def add_drivers(camera_object: Object) -> None:
"""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.type = "SCRIPTED"
if "$empty_distance" in expression:
@@ -72,22 +81,36 @@ class FnCamera:
d.expression = expression
__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.data, "type", "not $is_perspective")
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
try:
__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.data, "type", "not $is_perspective")
__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
def remove_drivers(camera_object: bpy.types.Object):
camera_object.data.driver_remove("ortho_scale")
camera_object.driver_remove("rotation_euler")
camera_object.data.driver_remove("ortho_scale")
camera_object.data.driver_remove("lens")
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.driver_remove("rotation_euler")
camera_object.data.driver_remove("ortho_scale")
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:
@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:
if camera_object.type != "CAMERA":
continue
@@ -97,161 +120,203 @@ class MigrationFnCamera:
# It's not a MMD Camera
continue
FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object)
try:
FnCamera.remove_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:
def __init__(self, obj):
def __init__(self, obj: Object):
"""Initialize an MMD camera."""
root_object = FnCamera.find_root(obj)
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)
logger.debug(f"Initialized MMD camera with root: {self.__emptyObj.name}")
@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
@staticmethod
def addDrivers(cameraObj: bpy.types.Object):
def addDrivers(cameraObj: Object) -> None:
"""Add drivers to the camera object."""
FnCamera.add_drivers(cameraObj)
@staticmethod
def removeDrivers(cameraObj: bpy.types.Object):
def removeDrivers(cameraObj: Object) -> None:
"""Remove drivers from the camera object. """
if cameraObj.type != "CAMERA":
return
FnCamera.remove_drivers(cameraObj)
@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):
logger.debug(f"Camera {cameraObj.name} is already an MMD camera")
return MMDCamera(cameraObj)
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty)
try:
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
context = FnContext.ensure_context()
FnContext.link_object(context, empty)
cameraObj.parent = empty
cameraObj.data.sensor_fit = "VERTICAL"
cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV
cameraObj.data.ortho_scale = 25 * scale
cameraObj.data.clip_end = 500 * scale
setattr(cameraObj.data, Props.display_size, 5 * scale)
cameraObj.location = (0, -45 * scale, 0)
cameraObj.rotation_mode = "XYZ"
cameraObj.rotation_euler = (math.radians(90), 0, 0)
cameraObj.lock_location = (True, False, True)
cameraObj.lock_rotation = (True, True, True)
cameraObj.lock_scale = (True, True, True)
cameraObj.data.dof.focus_object = empty
FnCamera.add_drivers(cameraObj)
cameraObj.parent = empty
cameraObj.data.sensor_fit = "VERTICAL"
cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV
cameraObj.data.ortho_scale = 25 * scale
cameraObj.data.clip_end = 500 * scale
setattr(cameraObj.data, Props.display_size, 5 * scale)
cameraObj.location = (0, -45 * scale, 0)
cameraObj.rotation_mode = "XYZ"
cameraObj.rotation_euler = (math.radians(90), 0, 0)
cameraObj.lock_location = (True, False, True)
cameraObj.lock_rotation = (True, True, True)
cameraObj.lock_scale = (True, True, True)
cameraObj.data.dof.focus_object = empty
FnCamera.add_drivers(cameraObj)
empty.location = (0, 0, 10 * scale)
empty.rotation_mode = "YXZ"
setattr(empty, Props.empty_display_size, 5 * scale)
empty.lock_scale = (True, True, True)
empty.mmd_type = "CAMERA"
empty.mmd_camera.angle = math.radians(30)
empty.mmd_camera.persp = True
return MMDCamera(empty)
empty.location = (0, 0, 10 * scale)
empty.rotation_mode = "YXZ"
setattr(empty, Props.empty_display_size, 5 * scale)
empty.lock_scale = (True, True, True)
empty.mmd_type = "CAMERA"
empty.mmd_camera.angle = math.radians(30)
empty.mmd_camera.persp = True
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
return MMDCamera(empty)
except Exception as e:
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {str(e)}")
raise
@staticmethod
def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1):
scene = bpy.context.scene
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
mmd_cam_root = mmd_cam.parent
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
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
mmd_cam_root = mmd_cam.parent
_camera_override_func = None
if cameraObj is None:
if scene.camera is None:
scene.camera = mmd_cam
return MMDCamera(mmd_cam_root)
_camera_override_func = lambda: scene.camera
_camera_override_func: Optional[Callable[[], Object]] = None
if cameraObj is None:
if scene.camera is None:
scene.camera = mmd_cam
logger.debug("Set scene camera to new MMD camera")
return MMDCamera(mmd_cam_root)
_camera_override_func = lambda: scene.camera
_target_override_func = None
if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
_target_override_func: Optional[Callable[[Object], Object]] = None
if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
action_name = mmd_cam_root.name
parent_action = bpy.data.actions.new(name=action_name)
distance_action = bpy.data.actions.new(name=action_name + "_dis")
FnCamera.remove_drivers(mmd_cam)
action_name = mmd_cam_root.name
parent_action = bpy.data.actions.new(name=action_name)
distance_action = bpy.data.actions.new(name=action_name + "_dis")
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
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
neg_z_vector = Vector((0, 0, -1))
frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end + 1, scene.frame_current
frame_count = frame_end - frame_start
frames = range(frame_start, frame_end)
render = scene.render
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
neg_z_vector = Vector((0, 0, -1))
frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end + 1, scene.frame_current
frame_count = frame_end - frame_start
frames = range(frame_start, frame_end)
fcurves = []
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves:
c.keyframe_points.add(frame_count)
fcurves = []
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves:
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)):
scene.frame_set(f)
if _camera_override_func:
cameraObj = _camera_override_func()
if _target_override_func:
cameraTarget = _target_override_func(cameraObj)
cam_matrix_world = cameraObj.matrix_world
cam_target_loc = cameraTarget.matrix_world.translation
cam_rotation = (cam_matrix_world @ matrix_rotation).to_euler(mmd_cam_root.rotation_mode)
cam_vec = cam_matrix_world.to_3x3() @ neg_z_vector
if cameraObj.data.type == "ORTHO":
cam_dis = -(9 / 5) * cameraObj.data.ortho_scale
if cameraObj.data.sensor_fit != "VERTICAL":
if cameraObj.data.sensor_fit == "HORIZONTAL":
cam_dis *= factor
else:
cam_dis *= min(1, factor)
else:
target_vec = cam_target_loc - cam_matrix_world.translation
cam_dis = -max(target_vec.length * cam_vec.dot(target_vec.normalized()), min_distance)
cam_target_loc = cam_matrix_world.translation - cam_vec * cam_dis
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)
if _camera_override_func:
cameraObj = _camera_override_func()
if _target_override_func:
cameraTarget = _target_override_func(cameraObj)
cam_matrix_world = cameraObj.matrix_world
cam_target_loc = cameraTarget.matrix_world.translation
cam_rotation = (cam_matrix_world @ matrix_rotation).to_euler(mmd_cam_root.rotation_mode)
cam_vec = cam_matrix_world.to_3x3() @ neg_z_vector
if cameraObj.data.type == "ORTHO":
cam_dis = -(9 / 5) * cameraObj.data.ortho_scale
tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2
if cameraObj.data.sensor_fit != "VERTICAL":
ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height
if cameraObj.data.sensor_fit == "HORIZONTAL":
cam_dis *= factor
else:
cam_dis *= min(1, factor)
else:
target_vec = cam_target_loc - cam_matrix_world.translation
cam_dis = -max(target_vec.length * cam_vec.dot(target_vec.normalized()), min_distance)
cam_target_loc = cam_matrix_world.translation - cam_vec * cam_dis
tan_val *= factor * ratio
else: # cameraObj.data.sensor_fit == 'AUTO'
tan_val *= min(ratio, factor * ratio)
tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2
if cameraObj.data.sensor_fit != "VERTICAL":
ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height
if cameraObj.data.sensor_fit == "HORIZONTAL":
tan_val *= factor * ratio
else: # cameraObj.data.sensor_fit == 'AUTO'
tan_val *= min(ratio, factor * ratio)
x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
dis.co = (f, cam_dis)
fov.co = (f, 2 * atan(tan_val))
persp.co = (f, cameraObj.data.type != "ORTHO")
persp.interpolation = "CONSTANT"
for kp in (x, y, z, rx, ry, rz, fov, dis):
kp.interpolation = "LINEAR"
x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
dis.co = (f, cam_dis)
fov.co = (f, 2 * atan(tan_val))
persp.co = (f, cameraObj.data.type != "ORTHO")
persp.interpolation = "CONSTANT"
for kp in (x, y, z, rx, ry, rz, fov, dis):
kp.interpolation = "LINEAR"
FnCamera.add_drivers(mmd_cam)
mmd_cam_root.animation_data_create().action = parent_action
mmd_cam.animation_data_create().action = distance_action
scene.frame_set(frame_current)
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
return MMDCamera(mmd_cam_root)
except Exception as e:
logger.error(f"Failed to create MMD camera animation: {str(e)}")
raise
FnCamera.add_drivers(mmd_cam)
mmd_cam_root.animation_data_create().action = parent_action
mmd_cam.animation_data_create().action = distance_action
scene.frame_set(frame_current)
return MMDCamera(mmd_cam_root)
def object(self):
def object(self) -> Object:
"""Get the root object of the MMD camera."""
return self.__emptyObj
def camera(self):
def camera(self) -> Object:
"""Get the camera object of the MMD camera."""
for i in self.__emptyObj.children:
if i.type == "CAMERA":
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}")