Upfate Bone and Camrea
This commit is contained in:
+168
-76
@@ -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}")
|
||||||
|
|||||||
+195
-130
@@ -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
|
||||||
|
|
||||||
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
|
try:
|
||||||
__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, "ortho_scale", "25*abs($empty_distance)/45")
|
||||||
__add_driver(camera_object.data, "type", "not $is_perspective")
|
__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, "lens", "$sensor_height/tan($angle/2)/2")
|
__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
|
@staticmethod
|
||||||
def remove_drivers(camera_object: bpy.types.Object):
|
def remove_drivers(camera_object: Object) -> None:
|
||||||
camera_object.data.driver_remove("ortho_scale")
|
"""Remove drivers from the camera object."""
|
||||||
camera_object.driver_remove("rotation_euler")
|
logger.debug(f"Removing drivers from camera: {camera_object.name}")
|
||||||
camera_object.data.driver_remove("ortho_scale")
|
try:
|
||||||
camera_object.data.driver_remove("lens")
|
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:
|
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,161 +120,203 @@ class MigrationFnCamera:
|
|||||||
# It's not a MMD Camera
|
# It's not a MMD Camera
|
||||||
continue
|
continue
|
||||||
|
|
||||||
FnCamera.remove_drivers(camera_object)
|
try:
|
||||||
FnCamera.add_drivers(camera_object)
|
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:
|
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)
|
||||||
|
|
||||||
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
|
try:
|
||||||
FnContext.link_object(FnContext.ensure_context(), empty)
|
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
|
||||||
|
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"
|
||||||
cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV
|
cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV
|
||||||
cameraObj.data.ortho_scale = 25 * scale
|
cameraObj.data.ortho_scale = 25 * scale
|
||||||
cameraObj.data.clip_end = 500 * scale
|
cameraObj.data.clip_end = 500 * scale
|
||||||
setattr(cameraObj.data, Props.display_size, 5 * scale)
|
setattr(cameraObj.data, Props.display_size, 5 * scale)
|
||||||
cameraObj.location = (0, -45 * scale, 0)
|
cameraObj.location = (0, -45 * scale, 0)
|
||||||
cameraObj.rotation_mode = "XYZ"
|
cameraObj.rotation_mode = "XYZ"
|
||||||
cameraObj.rotation_euler = (math.radians(90), 0, 0)
|
cameraObj.rotation_euler = (math.radians(90), 0, 0)
|
||||||
cameraObj.lock_location = (True, False, True)
|
cameraObj.lock_location = (True, False, True)
|
||||||
cameraObj.lock_rotation = (True, True, True)
|
cameraObj.lock_rotation = (True, True, True)
|
||||||
cameraObj.lock_scale = (True, True, True)
|
cameraObj.lock_scale = (True, True, True)
|
||||||
cameraObj.data.dof.focus_object = empty
|
cameraObj.data.dof.focus_object = empty
|
||||||
FnCamera.add_drivers(cameraObj)
|
FnCamera.add_drivers(cameraObj)
|
||||||
|
|
||||||
empty.location = (0, 0, 10 * scale)
|
empty.location = (0, 0, 10 * scale)
|
||||||
empty.rotation_mode = "YXZ"
|
empty.rotation_mode = "YXZ"
|
||||||
setattr(empty, Props.empty_display_size, 5 * scale)
|
setattr(empty, Props.empty_display_size, 5 * scale)
|
||||||
empty.lock_scale = (True, True, True)
|
empty.lock_scale = (True, True, True)
|
||||||
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
|
||||||
return MMDCamera(empty)
|
|
||||||
|
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
|
@staticmethod
|
||||||
def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1):
|
def newMMDCameraAnimation(
|
||||||
scene = bpy.context.scene
|
cameraObj: Optional[Object],
|
||||||
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
|
cameraTarget: Optional[Object] = None,
|
||||||
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
|
scale: float = 1.0,
|
||||||
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
|
min_distance: float = 0.1
|
||||||
mmd_cam_root = mmd_cam.parent
|
) -> '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
|
_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
|
||||||
return MMDCamera(mmd_cam_root)
|
logger.debug("Set scene camera to new MMD camera")
|
||||||
_camera_override_func = lambda: scene.camera
|
return MMDCamera(mmd_cam_root)
|
||||||
|
_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
|
||||||
|
|
||||||
action_name = mmd_cam_root.name
|
action_name = mmd_cam_root.name
|
||||||
parent_action = bpy.data.actions.new(name=action_name)
|
parent_action = bpy.data.actions.new(name=action_name)
|
||||||
distance_action = bpy.data.actions.new(name=action_name + "_dis")
|
distance_action = bpy.data.actions.new(name=action_name + "_dis")
|
||||||
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
|
||||||
|
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
|
fcurves = []
|
||||||
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
|
for i in range(3):
|
||||||
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
|
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
|
||||||
neg_z_vector = Vector((0, 0, -1))
|
for i in range(3):
|
||||||
frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end + 1, scene.frame_current
|
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
|
||||||
frame_count = frame_end - frame_start
|
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
|
||||||
frames = range(frame_start, frame_end)
|
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 = []
|
logger.debug(f"Processing {frame_count} frames for camera animation")
|
||||||
for i in range(3):
|
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)):
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
|
scene.frame_set(f)
|
||||||
for i in range(3):
|
if _camera_override_func:
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
|
cameraObj = _camera_override_func()
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
|
if _target_override_func:
|
||||||
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
|
cameraTarget = _target_override_func(cameraObj)
|
||||||
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
|
cam_matrix_world = cameraObj.matrix_world
|
||||||
for c in fcurves:
|
cam_target_loc = cameraTarget.matrix_world.translation
|
||||||
c.keyframe_points.add(frame_count)
|
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)):
|
tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2
|
||||||
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 != "VERTICAL":
|
||||||
|
ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height
|
||||||
if cameraObj.data.sensor_fit == "HORIZONTAL":
|
if cameraObj.data.sensor_fit == "HORIZONTAL":
|
||||||
cam_dis *= factor
|
tan_val *= factor * ratio
|
||||||
else:
|
else: # cameraObj.data.sensor_fit == 'AUTO'
|
||||||
cam_dis *= min(1, factor)
|
tan_val *= min(ratio, factor * ratio)
|
||||||
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 = cameraObj.data.sensor_height / cameraObj.data.lens / 2
|
x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
|
||||||
if cameraObj.data.sensor_fit != "VERTICAL":
|
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
|
||||||
ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height
|
dis.co = (f, cam_dis)
|
||||||
if cameraObj.data.sensor_fit == "HORIZONTAL":
|
fov.co = (f, 2 * atan(tan_val))
|
||||||
tan_val *= factor * ratio
|
persp.co = (f, cameraObj.data.type != "ORTHO")
|
||||||
else: # cameraObj.data.sensor_fit == 'AUTO'
|
persp.interpolation = "CONSTANT"
|
||||||
tan_val *= min(ratio, factor * ratio)
|
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)
|
FnCamera.add_drivers(mmd_cam)
|
||||||
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
|
mmd_cam_root.animation_data_create().action = parent_action
|
||||||
dis.co = (f, cam_dis)
|
mmd_cam.animation_data_create().action = distance_action
|
||||||
fov.co = (f, 2 * atan(tan_val))
|
scene.frame_set(frame_current)
|
||||||
persp.co = (f, cameraObj.data.type != "ORTHO")
|
|
||||||
persp.interpolation = "CONSTANT"
|
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
|
||||||
for kp in (x, y, z, rx, ry, rz, fov, dis):
|
return MMDCamera(mmd_cam_root)
|
||||||
kp.interpolation = "LINEAR"
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to create MMD camera animation: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
FnCamera.add_drivers(mmd_cam)
|
def object(self) -> Object:
|
||||||
mmd_cam_root.animation_data_create().action = parent_action
|
"""Get the root object of the MMD camera."""
|
||||||
mmd_cam.animation_data_create().action = distance_action
|
|
||||||
scene.frame_set(frame_current)
|
|
||||||
return MMDCamera(mmd_cam_root)
|
|
||||||
|
|
||||||
def object(self):
|
|
||||||
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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user