Holy shit this was a pain

- Truly fixes PMX Import lol, i messed up completely
- Updated MMD Tools to use Cats One
This commit is contained in:
Yusarina
2025-11-19 06:35:06 +00:00
parent f0bda259d3
commit a929f68ad4
38 changed files with 4479 additions and 2709 deletions
+1 -1
View File
@@ -3,4 +3,4 @@
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
+97 -187
View File
@@ -1,44 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2015 MMD Tools authors
# This file is part of MMD Tools.
import math
from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Dict, Tuple, Any, Union, cast
from typing import TYPE_CHECKING, Iterable, Optional, Set
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 ....core.logging_setup import logger
if TYPE_CHECKING:
from ..properties.root import MMDRoot, MMDDisplayItemFrame
from ..properties.pose_bone import MMDBone
from ..properties.root import MMDDisplayItemFrame, MMDRoot
def remove_constraint(constraints: Any, name: str) -> bool:
"""Remove a constraint by name if it exists"""
def remove_constraint(constraints, name):
c = constraints.get(name, None)
if c:
constraints.remove(c)
return True
return False
def remove_edit_bones(edit_bones: bpy.types.ArmatureEditBones, bone_names: List[str]) -> None:
"""Remove edit bones by name"""
def remove_edit_bones(edit_bones, bone_names):
for name in bone_names:
b = edit_bones.get(name, None)
if b:
edit_bones.remove(b)
BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools"
BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools_local"
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection"
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection"
BONE_COLLECTION_NAME_SHADOW = "mmd_shadow"
@@ -48,52 +41,44 @@ SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NA
class FnBone:
AUTO_LOCAL_AXIS_ARMS: Tuple[str, ...] = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS: Tuple[str, ...] = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: Tuple[str, ...] = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
def __init__(self) -> None:
def __init__(self):
raise NotImplementedError("This class cannot be instantiated.")
@staticmethod
def find_pose_bone_by_bone_id(armature_object: Object, bone_id: int) -> Optional[PoseBone]:
"""Find a pose bone by its bone ID"""
def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]:
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: Object) -> int:
"""Generate a new unique bone ID"""
def __new_bone_id(armature_object: bpy.types.Object) -> int:
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
@staticmethod
def get_or_assign_bone_id(pose_bone: PoseBone) -> int:
"""Get the bone ID or assign a new one if not set"""
def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int:
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: Object) -> Iterable[PoseBone]:
"""Get selected pose bones from the armature"""
def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]:
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
context_selected_bones = bpy.context.selected_pose_bones or bpy.context.selected_bones or []
bones = armature_object.pose.bones
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: 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}")
def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True):
for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone = b.mmd_bone
mmd_bone: MMDBone = b.mmd_bone
mmd_bone.enabled_fixed_axis = enable
lock_rotation = b.lock_rotation[:]
if enable:
@@ -108,91 +93,72 @@ class FnBone:
b.lock_location = b.lock_scale = (False, False, False)
@staticmethod
def setup_special_bone_collections(armature_object: Object) -> Object:
"""Set up special bone collections for MMD"""
armature = cast(Armature, armature_object.data)
def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object:
armature: bpy.types.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: BoneCollection) -> bool:
"""Check if a bone collection is an MMD Tools collection"""
def __is_mmd_tools_local_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
@staticmethod
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)
def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
return bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) == BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
@staticmethod
def __set_bone_collection_to_special(bone_collection: BoneCollection, is_visible: bool) -> None:
"""Mark a bone collection as special"""
def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool):
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: 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)
def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
return bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) == BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
@staticmethod
def __set_bone_collection_to_normal(bone_collection: BoneCollection) -> None:
"""Mark a bone collection as normal"""
def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection):
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
@staticmethod
def __set_edit_bone_to_special(edit_bone: EditBone, bone_collection_name: str) -> EditBone:
"""Set an edit bone to a special collection"""
def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone:
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: EditBone) -> EditBone:
"""Set an edit bone as a dummy bone"""
logger.debug(f"Setting bone {edit_bone.name} as dummy bone")
def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
@staticmethod
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")
def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
@staticmethod
def __unassign_mmd_tools_bone_collections(edit_bone: EditBone) -> EditBone:
"""Unassign an edit bone from all MMD Tools collections"""
def __unassign_mmd_tools_local_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
for bone_collection in edit_bone.collections:
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
if not FnBone.__is_mmd_tools_local_bone_collection(bone_collection):
continue
bone_collection.unassign(edit_bone)
return edit_bone
@staticmethod
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)
def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object):
armature: bpy.types.Armature = armature_object.data
bone_collections = armature.collections
from .model import FnModel
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
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root
bones = armature.bones
used_groups: Set[str] = set()
unassigned_bone_names: Set[str] = {b.name for b in bones}
used_groups = set()
unassigned_bone_names = {b.name for b in bones}
for frame in mmd_root.display_item_frames:
for item in frame.data:
@@ -204,12 +170,11 @@ 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:
for bc in bones[name].collections:
if not FnBone.__is_mmd_tools_bone_collection(bc):
if not FnBone.__is_mmd_tools_local_bone_collection(bc):
continue
if not FnBone.__is_normal_bone_collection(bc):
continue
@@ -219,48 +184,40 @@ class FnBone:
for bone_collection in bone_collections.values():
if bone_collection.name in used_groups:
continue
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
if not FnBone.__is_mmd_tools_local_bone_collection(bone_collection):
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: 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
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
from .model import FnModel
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
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root
display_item_frames = mmd_root.display_item_frames
used_frame_index: Set[int] = set()
bone_collection: BoneCollection
bone_collection: bpy.types.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 = display_item_frames.get(bone_collection_name)
display_item_frame: Optional[MMDDisplayItemFrame] = 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))
for display_item, bone in zip(display_item_frame.data, bone_collection.bones):
for display_item, bone in zip(display_item_frame.data, bone_collection.bones, strict=False):
display_item.type = "BONE"
display_item.name = bone.name
@@ -271,27 +228,23 @@ 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: 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]] = {}
def apply_bone_fixed_axis(armature_object: bpy.types.Object):
bone_map = {}
for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
continue
mmd_bone = b.mmd_bone
mmd_bone: MMDBone = 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: EditBone
bone: bpy.types.EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
@@ -322,7 +275,6 @@ 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]
@@ -330,11 +282,9 @@ 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: Object, enable: bool = True) -> None:
"""Load local axes for selected bones"""
logger.debug(f"Loading bone local axes (enable={enable}) for {armature_object.name}")
def load_bone_local_axes(armature_object: bpy.types.Object, enable=True):
for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone = b.mmd_bone
mmd_bone: MMDBone = b.mmd_bone
mmd_bone.enabled_local_axes = enable
if enable:
axes = b.bone.matrix_local.to_3x3().transposed()
@@ -342,18 +292,16 @@ class FnBone:
mmd_bone.local_axis_z = axes[2].xzy
@staticmethod
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]] = {}
def apply_bone_local_axes(armature_object: bpy.types.Object):
bone_map = {}
for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
continue
mmd_bone = b.mmd_bone
mmd_bone: MMDBone = 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: EditBone
bone: bpy.types.EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
@@ -361,18 +309,15 @@ 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: EditBone, mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> None:
"""Update bone roll based on local axes"""
def update_bone_roll(edit_bone: bpy.types.EditBone, 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]))
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
@staticmethod
def get_axes(mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> Tuple[Vector, Vector, Vector]:
"""Get axes from local axis vectors"""
def get_axes(mmd_local_axis_x, mmd_local_axis_z):
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()
@@ -380,25 +325,18 @@ class FnBone:
return (x_axis, y_axis, z_axis)
@staticmethod
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)
def apply_auto_bone_roll(armature):
bone_names = [b.name 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)]
with bpyutils.edit_object(armature) as data:
bone: EditBone
bone: bpy.types.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: EditBone) -> None:
"""Update bone roll automatically"""
def update_auto_bone_roll(edit_bone):
# make a triangle face (p1,p2,p3)
p1 = edit_bone.head.copy()
p2 = edit_bone.tail.copy()
@@ -419,8 +357,7 @@ class FnBone:
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
@staticmethod
def has_auto_local_axis(name_j: str) -> bool:
"""Check if a bone should have automatic local axis"""
def has_auto_local_axis(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:
return True
@@ -430,11 +367,12 @@ class FnBone:
return False
@staticmethod
def clean_additional_transformation(armature_object: Object) -> None:
"""Clean additional transformation constraints and bones"""
logger.info(f"Cleaning additional transformations for {armature_object.name}")
def clean_additional_transformation(armature_object: bpy.types.Object):
if armature_object.type != "ARMATURE" or armature_object.pose is None:
return
# clean constraints
p_bone: PoseBone
p_bone: bpy.types.PoseBone
for p_bone in armature_object.pose.bones:
p_bone.mmd_bone.is_additional_transform_dirty = True
constraints = p_bone.constraints
@@ -450,21 +388,17 @@ class FnBone:
"ADDITIONAL_TRANSFORM_INVERT",
}
def __is_at_shadow_bone(b: PoseBone) -> bool:
def __is_at_shadow_bone(b):
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: Object) -> None:
"""Apply additional transformation to bones"""
logger.info(f"Applying additional transformations for {armature_object.name}")
def __is_dirty_bone(b: PoseBone) -> bool:
def apply_additional_transformation(armature_object: bpy.types.Object):
def __is_dirty_bone(b):
if b.is_mmd_shadow_bone:
return False
mmd_bone = b.mmd_bone
@@ -473,10 +407,9 @@ 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: List[Union[_AT_ShadowBoneRemove, _AT_ShadowBoneCreate]] = []
shadow_bone_pool = []
for p_bone in dirty_bones:
sb = FnBone.__setup_constraints(p_bone)
if sb:
@@ -497,8 +430,7 @@ class FnBone:
p_bone.mmd_bone.is_additional_transform_dirty = False
@staticmethod
def __setup_constraints(p_bone: PoseBone) -> Optional[Union['_AT_ShadowBoneRemove', '_AT_ShadowBoneCreate']]:
"""Set up constraints for additional transformation"""
def __setup_constraints(p_bone):
bone_name = p_bone.name
mmd_bone = p_bone.mmd_bone
influence = mmd_bone.additional_transform_influence
@@ -511,18 +443,21 @@ 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: str, mute: bool, map_type: str, value: float) -> None:
def __config(name, mute, map_type, value):
if mute:
remove_constraint(constraints, name)
return
c = TransformConstraintOp.create(constraints, name, map_type)
# FIXME: Some bones require specific rotation modes to match MMD behavior.
# Currently using hardcoded bone names as a temporary solution.
# See https://github.com/MMD-Blender/blender_mmd_tools_local/issues/242
if bone_name in {"左肩C", "右肩C", "肩C.L", "肩C.R", "肩C_L", "肩C_R"}:
c.from_rotation_mode = "ZYX" # Best matches MMD behavior for shoulder bones
c.target = p_bone.id_data
shadow_bone.add_constraint(c)
TransformConstraintOp.update_min_max(c, value, influence)
@@ -533,81 +468,62 @@ class FnBone:
return shadow_bone
@staticmethod
def update_additional_transform_influence(pose_bone: PoseBone) -> None:
"""Update the influence of additional transform constraints"""
def update_additional_transform_influence(pose_bone: bpy.types.PoseBone):
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: 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
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object):
pose_bone: bpy.types.PoseBone
for pose_bone in armature_object.pose.bones:
constraint: Constraint
constraint: bpy.types.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:
"""Handler for removing shadow bones"""
def __init__(self, bone_name: str) -> None:
"""Initialize with bone name"""
def __init__(self, bone_name):
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
"""Update edit bones by removing shadow bones"""
def update_edit_bones(self, edit_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: Any) -> None:
"""Update pose bones (no-op for removal)"""
def update_pose_bones(self, pose_bones):
pass
class _AT_ShadowBoneCreate:
"""Handler for creating shadow bones"""
def __init__(self, bone_name: str, target_bone_name: str) -> None:
"""Initialize with bone names"""
def __init__(self, bone_name, target_bone_name):
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: List[Constraint] = []
self.__constraint_pool = []
def __is_well_aligned(self, bone0: EditBone, bone1: EditBone) -> bool:
"""Check if two bones are well aligned"""
def __is_well_aligned(self, bone0, bone1):
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: bool = True) -> None:
"""Update constraints to use shadow or target bone"""
def __update_constraints(self, use_shadow=True):
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: Constraint) -> None:
"""Add a constraint to the pool"""
def add_constraint(self, constraint):
self.__constraint_pool.append(constraint)
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
"""Update edit bones by creating shadow bones"""
def update_edit_bones(self, edit_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
@@ -617,7 +533,6 @@ 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))
@@ -625,15 +540,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: Any) -> None:
"""Update pose bones by setting up shadow bone properties"""
def update_pose_bones(self, 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)
return
dummy_p_bone = pose_bones[self.__dummy_bone_name]
dummy_p_bone.is_mmd_shadow_bone = True
dummy_p_bone.mmd_shadow_bone_type = "DUMMY"
@@ -649,7 +561,5 @@ 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}")
+135 -224
View File
@@ -1,26 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# This file is part of MMD Tools.
import math
from typing import Optional, List, Tuple, Callable, Any, Union
from typing import Optional
import bpy
from bpy.types import Object, ID, Camera, Context
from bpy_extras import anim_utils
from mathutils import Vector, Matrix, Euler
import traceback
from mathutils import Matrix, Vector
from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
class FnCamera:
@staticmethod
def find_root(obj: Optional[Object]) -> Optional[Object]:
"""Find the root object of an MMD camera setup."""
def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
if obj is None:
return None
if FnCamera.is_mmd_camera_root(obj):
@@ -30,22 +22,16 @@ class FnCamera:
return None
@staticmethod
def is_mmd_camera(obj: Object) -> bool:
"""Check if an object is an MMD camera."""
def is_mmd_camera(obj: bpy.types.Object) -> bool:
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
@staticmethod
def is_mmd_camera_root(obj: Object) -> bool:
"""Check if an object is an MMD camera root."""
def is_mmd_camera_root(obj: bpy.types.Object) -> bool:
return obj.type == "EMPTY" and obj.mmd_type == "CAMERA"
@staticmethod
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."""
def add_drivers(camera_object: bpy.types.Object):
def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1):
d = id_data.driver_add(data_path, index).driver
d.type = "SCRIPTED"
if "$empty_distance" in expression:
@@ -73,46 +59,31 @@ class FnCamera:
v.targets[0].data_path = "mmd_camera.angle"
expression = expression.replace("$angle", v.name)
if "$sensor_height" in expression:
v = d.variables.new()
v.name = "sensor_height"
v.type = "SINGLE_PROP"
v.targets[0].id_type = "CAMERA"
v.targets[0].id = camera_object.data
v.targets[0].data_path = "sensor_height"
expression = expression.replace("$sensor_height", v.name)
# Use fixed sensor_height instead of dynamic reference.
# When controlled by MMD angle, sensor_height shouldn't change.
# This avoids unnecessary dependency cycles.
# Reference: https://github.com/MMD-Blender/blender_mmd_tools_local/issues/227
current_sensor_height = camera_object.data.sensor_height
expression = expression.replace("$sensor_height", str(current_sensor_height))
d.expression = expression
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:
logger.error(f"Failed to add drivers to camera {camera_object.name}: {traceback.format_exc()}")
__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")
@staticmethod
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:
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {traceback.format_exc()}")
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("type")
camera_object.data.driver_remove("lens")
class MigrationFnCamera:
@staticmethod
def update_mmd_camera() -> None:
"""Update all MMD cameras in the scene."""
logger.info("Updating all MMD cameras in the scene")
updated_count = 0
def update_mmd_camera():
for camera_object in bpy.data.objects:
if camera_object.type != "CAMERA":
continue
@@ -122,216 +93,156 @@ class MigrationFnCamera:
# It's not a MMD Camera
continue
try:
FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object)
updated_count += 1
except Exception:
logger.error(f"Failed to update MMD camera {camera_object.name}: {traceback.format_exc()}")
logger.info(f"Updated {updated_count} MMD cameras")
FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object)
class MMDCamera:
def __init__(self, obj: Object):
"""Initialize an MMD camera."""
def __init__(self, obj):
root_object = FnCamera.find_root(obj)
if root_object is None:
logger.error(f"Object {obj.name} is not an MMD camera")
raise ValueError(f"{obj.name} is not an MMD camera")
raise ValueError(f"{str(obj)} is not MMDCamera")
self.__emptyObj = getattr(root_object, "original", obj)
logger.debug(f"Initialized MMD camera with root: {self.__emptyObj.name}")
@staticmethod
def isMMDCamera(obj: Object) -> bool:
"""Check if an object is an MMD camera."""
def isMMDCamera(obj: bpy.types.Object) -> bool:
return FnCamera.find_root(obj) is not None
@staticmethod
def addDrivers(cameraObj: Object) -> None:
"""Add drivers to the camera object."""
def addDrivers(cameraObj: bpy.types.Object):
FnCamera.add_drivers(cameraObj)
@staticmethod
def removeDrivers(cameraObj: Object) -> None:
"""Remove drivers from the camera object. """
def removeDrivers(cameraObj: bpy.types.Object):
if cameraObj.type != "CAMERA":
return
FnCamera.remove_drivers(cameraObj)
@staticmethod
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}")
def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0):
if FnCamera.is_mmd_camera(cameraObj):
logger.debug(f"Camera {cameraObj.name} is already an MMD camera")
return MMDCamera(cameraObj)
try:
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
context = FnContext.ensure_context()
FnContext.link_object(context, empty)
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
FnContext.link_object(FnContext.ensure_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
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
return MMDCamera(empty)
except Exception:
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {traceback.format_exc()}")
raise
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)
@staticmethod
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
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
_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
_camera_override_func = None
if cameraObj is None:
if scene.camera is None:
scene.camera = mmd_cam
return MMDCamera(mmd_cam_root)
def _camera_override_func():
return scene.camera
_target_override_func: Optional[Callable[[Object], Object]] = None
if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
_target_override_func = None
if cameraTarget is None:
def _target_override_func(camObj):
return 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 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 = [parent_action.fcurves.new(data_path="location", index=i) for i in range(3)] # x, y, z
fcurves.extend(parent_action.fcurves.new(data_path="rotation_euler", index=i) for i in range(3)) # 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)
# Get channelbags for camera actions using Blender 5.0 API
if not parent_action.slots:
parent_slot = parent_action.slots.new(for_id=mmd_cam_root)
else:
parent_slot = parent_action.slots[0]
parent_channelbag = anim_utils.action_ensure_channelbag_for_slot(parent_action, parent_slot)
if not distance_action.slots:
distance_slot = distance_action.slots.new(for_id=mmd_cam)
else:
distance_slot = distance_action.slots[0]
distance_channelbag = anim_utils.action_ensure_channelbag_for_slot(distance_action, distance_slot)
fcurves = []
for i in range(3):
fcurves.append(parent_channelbag.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3):
fcurves.append(parent_channelbag.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_channelbag.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_channelbag.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
tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves), strict=False):
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":
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)
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
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"
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)
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:
logger.error(f"Failed to create MMD camera animation: {traceback.format_exc()}")
raise
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 * math.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"
def object(self) -> Object:
"""Get the root object of the MMD camera."""
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):
return self.__emptyObj
def camera(self) -> Object:
"""Get the camera object of the MMD camera."""
def camera(self):
for i in self.__emptyObj.children:
if i.type == "CAMERA":
return i
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}")
raise KeyError
+5 -7
View File
@@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2016 MMD Tools authors
# This file is part of MMD Tools.
# Module for custom exceptions
class MaterialNotFoundError(KeyError):
"""Exception raised when a material is not found in the scene"""
def __init__(self, *args: object) -> None:
"""Constructor for MaterialNotFoundError"""
"""Initialize MaterialNotFoundError"""
super().__init__(*args)
+14 -35
View File
@@ -1,53 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# This file is part of MMD Tools.
import bpy
from typing import Optional, Union, Any, List, Tuple
from bpy.types import Object, Context
from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
class MMDLamp:
def __init__(self, obj: Object) -> None:
def __init__(self, obj):
if MMDLamp.isLamp(obj):
obj = obj.parent
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
self.__emptyObj: Object = obj
self.__emptyObj = obj
else:
error_msg = f"{str(obj)} is not MMDLamp"
logger.error(error_msg)
raise ValueError(error_msg)
raise ValueError(f"{str(obj)} is not MMDLamp")
@staticmethod
def isLamp(obj: Optional[Object]) -> bool:
"""Check if the object is a lamp/light object"""
return obj is not None and obj.type in {"LIGHT", "LAMP"}
def isLamp(obj):
return obj and obj.type in {"LIGHT", "LAMP"}
@staticmethod
def isMMDLamp(obj: Optional[Object]) -> bool:
"""Check if the object is an MMD lamp"""
def isMMDLamp(obj):
if MMDLamp.isLamp(obj):
obj = obj.parent
return obj is not None and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
@staticmethod
def convertToMMDLamp(lampObj: Object, scale: float = 1.0) -> 'MMDLamp':
"""Convert a regular lamp to an MMD lamp"""
def convertToMMDLamp(lampObj, scale=1.0):
if MMDLamp.isMMDLamp(lampObj):
logger.debug(f"Object {lampObj.name} is already an MMD lamp")
return MMDLamp(lampObj)
logger.info(f"Converting {lampObj.name} to MMD lamp with scale {scale}")
empty: Object = bpy.data.objects.new(name="MMD_Light", object_data=None)
context = FnContext.ensure_context()
FnContext.link_object(context, empty)
empty = bpy.data.objects.new(name="MMD_Light", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty)
empty.rotation_mode = "XYZ"
empty.lock_rotation = (True, True, True)
@@ -69,18 +53,13 @@ class MMDLamp:
constraint.track_axis = "TRACK_NEGATIVE_Z"
constraint.up_axis = "UP_Y"
logger.debug(f"Successfully created MMD lamp from {lampObj.name}")
return MMDLamp(empty)
def object(self) -> Object:
"""Get the empty object that represents this MMD lamp"""
def object(self):
return self.__emptyObj
def lamp(self) -> Object:
"""Get the actual lamp/light object"""
def lamp(self):
for i in self.__emptyObj.children:
if MMDLamp.isLamp(i):
return i
error_msg = f"No lamp found in MMD lamp {self.__emptyObj.name}"
logger.error(error_msg)
raise KeyError(error_msg)
raise KeyError
+89 -159
View File
@@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# This file is part of MMD Tools.
import logging
from ....core.logging_setup import logger
import os
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast, Dict, List, Any, Union, Set
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
import bpy
from mathutils import Vector
@@ -15,7 +12,6 @@ from mathutils import Vector
from ..bpyutils import FnContext
from .exceptions import MaterialNotFoundError
from .shader import _NodeGroupUtils
from ....core.logging_setup import logger
if TYPE_CHECKING:
from ..properties.material import MMDMaterial
@@ -28,55 +24,51 @@ SPHERE_MODE_SUBTEX = 3
class _DummyTexture:
def __init__(self, image: bpy.types.Image):
self.type: str = "IMAGE"
self.image: bpy.types.Image = image
self.use_mipmap: bool = True
def __init__(self, image):
self.type = "IMAGE"
self.image = image
self.use_mipmap = True
class _DummyTextureSlot:
def __init__(self, image: bpy.types.Image):
self.diffuse_color_factor: float = 1
self.uv_layer: str = ""
self.texture: _DummyTexture = _DummyTexture(image)
def __init__(self, image):
self.diffuse_color_factor = 1
self.uv_layer = ""
self.texture = _DummyTexture(image)
class FnMaterial:
__NODES_ARE_READONLY: bool = False
def __init__(self, material: bpy.types.Material):
self.__material: bpy.types.Material = material
self._nodes_are_readonly: bool = FnMaterial.__NODES_ARE_READONLY
self.__material = material
self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY
@staticmethod
def set_nodes_are_readonly(nodes_are_readonly: bool) -> None:
def set_nodes_are_readonly(nodes_are_readonly: bool):
FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
@classmethod
def from_material_id(cls, material_id: str) -> Optional['FnMaterial']:
def from_material_id(cls, material_id: int):
for material in bpy.data.materials:
if material.mmd_material.material_id == material_id:
return cls(material)
return None
@staticmethod
def clean_materials(obj: bpy.types.Object, can_remove: Callable[[bpy.types.Material], bool]) -> None:
def clean_materials(obj, can_remove: Callable[[bpy.types.Material], bool]):
materials = obj.data.materials
materials_pop = materials.pop
removed_count = 0
for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True):
m = materials_pop(index=i)
removed_count += 1
if m.users < 1:
bpy.data.materials.remove(m)
if removed_count > 0:
logger.debug(f"Removed {removed_count} materials from {obj.name}")
@staticmethod
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: Union[str, int], mat2_ref: Union[str, int], reverse: bool = False, swap_slots: bool = False) -> Tuple[bpy.types.Material, bpy.types.Material]:
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: str | int, mat2_ref: str | int, reverse=False, swap_slots=False) -> Tuple[bpy.types.Material, bpy.types.Material]:
"""
This method will assign the polygons of mat1 to mat2.
Assign the polygons of mat1 to mat2.
If reverse is True it will also swap the polygons assigned to mat2 to mat1.
The reference to materials can be indexes or names
Finally it will also swap the material slots if the option is given.
@@ -94,22 +86,18 @@ class FnMaterial:
Raises:
MaterialNotFoundError: If one of the materials is not found
"""
mesh = cast(bpy.types.Mesh, mesh_object.data)
mesh = cast("bpy.types.Mesh", mesh_object.data)
try:
# Try to find the materials
mat1 = mesh.materials[mat1_ref]
mat2 = mesh.materials[mat2_ref]
if None in (mat1, mat2):
raise MaterialNotFoundError()
if None in {mat1, mat2}:
raise MaterialNotFoundError
except (KeyError, IndexError) as exc:
# Wrap exceptions within our custom ones
raise MaterialNotFoundError() from exc
raise MaterialNotFoundError from exc
mat1_idx = mesh.materials.find(mat1.name)
mat2_idx = mesh.materials.find(mat2.name)
logger.debug(f"Swapping materials: {mat1.name} (idx:{mat1_idx}) <-> {mat2.name} (idx:{mat2_idx}) in {mesh_object.name}")
# Swap polygons
for poly in mesh.polygons:
if poly.material_index == mat1_idx:
@@ -123,37 +111,31 @@ class FnMaterial:
return mat1, mat2
@staticmethod
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]) -> None:
"""
This method will fix the material order. Which is lost after joining meshes.
"""
materials = cast(bpy.types.Mesh, meshObj.data).materials
logger.debug(f"Fixing material order for {meshObj.name}")
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]):
"""Fix the material order which is lost after joining meshes."""
materials = cast("bpy.types.Mesh", meshObj.data).materials
for new_idx, mat in enumerate(material_names):
# Get the material that is currently on this index
other_mat = materials[new_idx]
if other_mat.name == mat:
continue # This is already in place
logger.debug(f"Moving material {mat} to index {new_idx}")
FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True)
@property
def material_id(self) -> int:
mmd_mat: 'MMDMaterial' = self.__material.mmd_material
def material_id(self):
mmd_mat: MMDMaterial = self.__material.mmd_material
if mmd_mat.material_id < 0:
max_id = -1
for mat in bpy.data.materials:
max_id = max(max_id, mat.mmd_material.material_id)
mmd_mat.material_id = max_id + 1
logger.debug(f"Assigned new material ID {mmd_mat.material_id} to {self.__material.name}")
return mmd_mat.material_id
@property
def material(self) -> bpy.types.Material:
def material(self):
return self.__material
def __same_image_file(self, image: Optional[bpy.types.Image], filepath: str) -> bool:
def __same_image_file(self, image, filepath):
if image and image.source == "FILE":
# pylint: disable=assignment-from-no-return
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
@@ -162,19 +144,18 @@ class FnMaterial:
# pylint: disable=bare-except
try:
return os.path.samefile(img_filepath, filepath)
except:
pass
except Exception as e:
logger.warning(f"Failed to compare files '{img_filepath}' and '{filepath}': {e}")
return False
def _load_image(self, filepath: str) -> bpy.types.Image:
def _load_image(self, filepath):
img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None)
if img is None:
# pylint: disable=bare-except
try:
logger.debug(f"Loading image: {filepath}")
img = bpy.data.images.load(filepath)
except:
logger.warning(f"Cannot create a texture for {filepath}. No such file.")
except Exception:
logger.warning("Cannot create a texture for %s. No such file.", filepath)
img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
img.source = "FILE"
img.filepath = filepath
@@ -185,46 +166,43 @@ class FnMaterial:
img.alpha_mode = "NONE"
return img
def update_toon_texture(self) -> None:
def update_toon_texture(self):
if self._nodes_are_readonly:
return
mmd_mat: 'MMDMaterial' = self.__material.mmd_material
mmd_mat: MMDMaterial = self.__material.mmd_material
if mmd_mat.is_shared_toon_texture:
shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "")
toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1))
logger.debug(f"Using shared toon texture: {toon_path}")
self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path))
self.create_toon_texture(str(Path(toon_path).resolve()))
elif mmd_mat.toon_texture != "":
logger.debug(f"Using custom toon texture: {mmd_mat.toon_texture}")
self.create_toon_texture(mmd_mat.toon_texture)
else:
logger.debug(f"Removing toon texture from {self.__material.name}")
self.remove_toon_texture()
def _mix_diffuse_and_ambient(self, mmd_mat: 'MMDMaterial') -> List[float]:
def _mix_diffuse_and_ambient(self, mmd_mat):
r, g, b = mmd_mat.diffuse_color
ar, ag, ab = mmd_mat.ambient_color
return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)]
def update_drop_shadow(self) -> None:
def update_drop_shadow(self):
pass
def update_enabled_toon_edge(self) -> None:
def update_enabled_toon_edge(self):
if self._nodes_are_readonly:
return
self.update_edge_color()
def update_edge_color(self) -> None:
def update_edge_color(self):
if self._nodes_are_readonly:
return
mat = self.__material
mmd_mat: 'MMDMaterial' = mat.mmd_material
mmd_mat: MMDMaterial = mat.mmd_material
color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
if hasattr(mat, "line_color"): # freestyle line color
mat.line_color = line_color
mat_edge: Optional[bpy.types.Material] = bpy.data.materials.get("mmd_edge." + mat.name, None)
mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None)
if mat_edge:
mat_edge.mmd_material.edge_color = line_color
@@ -235,52 +213,45 @@ class FnMaterial:
node_shader.inputs["Color"].default_value = mmd_mat.edge_color
if node_shader and "Alpha" in node_shader.inputs:
node_shader.inputs["Alpha"].default_value = alpha
logger.debug(f"Updated edge color for {mat.name}")
def update_edge_weight(self) -> None:
def update_edge_weight(self):
pass
def get_texture(self) -> Optional[_DummyTexture]:
def get_texture(self):
return self.__get_texture_node("mmd_base_tex", use_dummy=True)
def create_texture(self, filepath: str) -> _DummyTextureSlot:
def create_texture(self, filepath):
texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1))
logger.debug(f"Created base texture for {self.__material.name}: {filepath}")
return _DummyTextureSlot(texture.image)
def remove_texture(self) -> None:
def remove_texture(self):
if self._nodes_are_readonly:
return
logger.debug(f"Removing base texture from {self.__material.name}")
self.__remove_texture_node("mmd_base_tex")
def get_sphere_texture(self) -> Optional[_DummyTexture]:
def get_sphere_texture(self):
return self.__get_texture_node("mmd_sphere_tex", use_dummy=True)
def use_sphere_texture(self, use_sphere: bool, obj: Optional[bpy.types.Object] = None) -> None:
def use_sphere_texture(self, use_sphere, obj=None):
if self._nodes_are_readonly:
return
if use_sphere:
logger.debug(f"Enabling sphere texture for {self.__material.name}")
self.update_sphere_texture_type(obj)
else:
logger.debug(f"Disabling sphere texture for {self.__material.name}")
self.__update_shader_input("Sphere Tex Fac", 0)
def create_sphere_texture(self, filepath: str, obj: Optional[bpy.types.Object] = None) -> _DummyTextureSlot:
def create_sphere_texture(self, filepath, obj=None):
texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2))
logger.debug(f"Created sphere texture for {self.__material.name}: {filepath}")
self.update_sphere_texture_type(obj)
return _DummyTextureSlot(texture.image)
def update_sphere_texture_type(self, obj: Optional[bpy.types.Object] = None) -> None:
def update_sphere_texture_type(self, obj=None):
if self._nodes_are_readonly:
return
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
is_sph_add = sphere_texture_type == 2
if sphere_texture_type not in (1, 2, 3):
if sphere_texture_type not in {1, 2, 3}:
self.__update_shader_input("Sphere Tex Fac", 0)
else:
self.__update_shader_input("Sphere Tex Fac", 1)
@@ -298,62 +269,54 @@ class FnMaterial:
nodes, links = mat.node_tree.nodes, mat.node_tree.links
if sphere_texture_type == 3:
if obj and obj.type == "MESH" and mat in tuple(obj.data.materials):
uv_layers = (l for l in obj.data.uv_layers if not l.name.startswith("_"))
uv_layers = (layer for layer in obj.data.uv_layers if not layer.name.startswith("_"))
next(uv_layers, None) # skip base UV
subtex_uv = getattr(next(uv_layers, None), "name", "")
if subtex_uv != "UV1":
logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex')
logger.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv)
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
else:
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
logger.debug(f"Updated sphere texture type for {self.material.name}: {sphere_texture_type}")
def remove_sphere_texture(self) -> None:
def remove_sphere_texture(self):
if self._nodes_are_readonly:
return
logger.debug(f"Removing sphere texture from {self.__material.name}")
self.__remove_texture_node("mmd_sphere_tex")
def get_toon_texture(self) -> Optional[_DummyTexture]:
def get_toon_texture(self):
return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
def use_toon_texture(self, use_toon: bool) -> None:
def use_toon_texture(self, use_toon):
if self._nodes_are_readonly:
return
logger.debug(f"{'Enabling' if use_toon else 'Disabling'} toon texture for {self.__material.name}")
self.__update_shader_input("Toon Tex Fac", use_toon)
def create_toon_texture(self, filepath: str) -> _DummyTextureSlot:
def create_toon_texture(self, filepath):
texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5))
logger.debug(f"Created toon texture for {self.__material.name}: {filepath}")
return _DummyTextureSlot(texture.image)
def remove_toon_texture(self) -> None:
def remove_toon_texture(self):
if self._nodes_are_readonly:
return
logger.debug(f"Removing toon texture from {self.__material.name}")
self.__remove_texture_node("mmd_toon_tex")
def __get_texture_node(self, node_name: str, use_dummy: bool = False) -> Optional[Union[bpy.types.ShaderNodeTexImage, _DummyTexture]]:
def __get_texture_node(self, node_name, use_dummy=False):
mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage):
return _DummyTexture(texture.image) if use_dummy else texture
return None
def __remove_texture_node(self, node_name: str) -> None:
def __remove_texture_node(self, node_name):
mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage):
mat.node_tree.nodes.remove(texture)
mat.update_tag()
def __create_texture_node(self, node_name: str, filepath: str, pos: Tuple[float, float]) -> bpy.types.ShaderNodeTexImage:
def __create_texture_node(self, node_name, filepath, pos):
texture = self.__get_texture_node(node_name)
if texture is None:
from mathutils import Vector
self.__update_shader_nodes()
nodes = self.material.node_tree.nodes
texture = nodes.new("ShaderNodeTexImage")
@@ -365,25 +328,23 @@ class FnMaterial:
self.__update_shader_nodes()
return texture
def update_ambient_color(self) -> None:
def update_ambient_color(self):
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
logger.debug(f"Updated ambient color for {mat.name}")
def update_diffuse_color(self) -> None:
def update_diffuse_color(self):
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
logger.debug(f"Updated diffuse color for {mat.name}")
def update_alpha(self) -> None:
def update_alpha(self):
if self._nodes_are_readonly:
return
mat = self.material
@@ -401,31 +362,28 @@ class FnMaterial:
mat.diffuse_color[3] = mmd_mat.alpha
self.__update_shader_input("Alpha", mmd_mat.alpha)
self.update_self_shadow_map()
logger.debug(f"Updated alpha for {mat.name}: {mmd_mat.alpha}")
def update_specular_color(self) -> None:
def update_specular_color(self):
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
mat.specular_color = mmd_mat.specular_color
self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,))
logger.debug(f"Updated specular color for {mat.name}")
def update_shininess(self) -> None:
def update_shininess(self):
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37)
if hasattr(mat, "metallic"):
mat.metallic = pow(1 - mat.roughness, 2.7)
mat.metallic = 0.0
if hasattr(mat, "specular_hardness"):
mat.specular_hardness = mmd_mat.shininess
self.__update_shader_input("Reflect", mmd_mat.shininess)
logger.debug(f"Updated shininess for {mat.name}: {mmd_mat.shininess}")
def update_is_double_sided(self) -> None:
def update_is_double_sided(self):
if self._nodes_are_readonly:
return
mat = self.material
@@ -435,9 +393,8 @@ class FnMaterial:
elif hasattr(mat, "use_backface_culling"):
mat.use_backface_culling = not mmd_mat.is_double_sided
self.__update_shader_input("Double Sided", mmd_mat.is_double_sided)
logger.debug(f"Updated double-sided setting for {mat.name}: {mmd_mat.is_double_sided}")
def update_self_shadow_map(self) -> None:
def update_self_shadow_map(self):
if self._nodes_are_readonly:
return
mat = self.material
@@ -445,24 +402,21 @@ class FnMaterial:
cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
if hasattr(mat, "shadow_method"):
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
logger.debug(f"Updated self shadow map for {mat.name}: {cast_shadows}")
def update_self_shadow(self) -> None:
def update_self_shadow(self):
if self._nodes_are_readonly:
return
mat = self.material
mmd_mat = mat.mmd_material
self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow)
logger.debug(f"Updated self shadow for {mat.name}: {mmd_mat.enabled_self_shadow}")
@staticmethod
def convert_to_mmd_material(material: bpy.types.Material, context: bpy.types.Context = bpy.context) -> None:
def convert_to_mmd_material(material, context=bpy.context):
m, mmd_material = material, material.mmd_material
logger.debug(f"Converting material to MMD material: {material.name}")
if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None:
def search_tex_image_node(node: bpy.types.ShaderNode) -> Optional[bpy.types.ShaderNodeTexImage]:
def search_tex_image_node(node: bpy.types.ShaderNode):
if node.type == "TEX_IMAGE":
return node
for node_input in node.inputs:
@@ -482,6 +436,7 @@ class FnMaterial:
preferred_output_node_target = {
"CYCLES": "CYCLES",
"BLENDER_EEVEE": "EEVEE",
"BLENDER_EEVEE_NEXT": "EEVEE", # Keep for backwards compatibility with 4.x
}.get(active_render_engine, "ALL")
tex_node = None
@@ -499,15 +454,13 @@ class FnMaterial:
if tex_node is None:
tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None)
if tex_node:
logger.debug(f"Found texture node for {material.name}: {tex_node.name}")
tex_node.name = "mmd_base_tex"
else:
# Take the Base Color from BSDF if there's no texture
bsdf_node = next((n for n in m.node_tree.nodes if n.type.startswith('BSDF_')), None)
bsdf_node = next((n for n in m.node_tree.nodes if n.type.startswith("BSDF_")), None)
if bsdf_node:
base_color_input = bsdf_node.inputs.get('Base Color') or bsdf_node.inputs.get('Color')
base_color_input = bsdf_node.inputs.get("Base Color") or bsdf_node.inputs.get("Color")
if base_color_input:
logger.debug(f"Using BSDF base color for {material.name}")
mmd_material.diffuse_color = base_color_input.default_value[:3]
# ambient should be half the diffuse
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
@@ -538,12 +491,11 @@ class FnMaterial:
# delete bsdf node if it's there
if m.use_nodes:
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED' or n.type.startswith('BSDF_')]
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == "BSDF_PRINCIPLED" or n.type.startswith("BSDF_")]
for n in nodes_to_remove:
logger.debug(f"Removing BSDF node from {material.name}: {n.name}")
m.node_tree.nodes.remove(n)
def __update_shader_input(self, name: str, val: Any) -> None:
def __update_shader_input(self, name, val):
mat = self.material
if mat.name.startswith("mmd_"): # skip mmd_edge.*
return
@@ -555,34 +507,26 @@ class FnMaterial:
val = min(max(val, interface_socket.min_value), interface_socket.max_value)
shader.inputs[name].default_value = val
def __update_shader_nodes(self) -> None:
def __update_shader_nodes(self):
mat = self.material
if mat.node_tree is None:
logger.debug(f"Creating node tree for {mat.name}")
# Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
# Creating a new material automatically creates a node tree
if mat.node_tree is None:
# Fallback: node tree should exist, but if not, log warning
logger.warning(f"Node tree is None for material {mat.name} - this should not happen")
return
mat.use_nodes = True
mat.node_tree.nodes.clear()
nodes, links = mat.node_tree.nodes, mat.node_tree.links
class _Dummy:
default_value: Any = None
is_linked: bool = True
default_value, is_linked = None, True
node_shader = nodes.get("mmd_shader", None)
if node_shader is None:
logger.debug(f"Creating MMD shader node for {mat.name}")
node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
node_shader.name = "mmd_shader"
node_shader.location = (0, 1500)
node_shader.location = (0, 300)
node_shader.width = 200
node_shader.node_tree = self.__get_shader()
mmd_mat: 'MMDMaterial' = mat.mmd_material
mmd_mat: MMDMaterial = mat.mmd_material
node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,)
node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,)
node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,)
@@ -594,7 +538,6 @@ class FnMaterial:
node_uv = nodes.get("mmd_tex_uv", None)
if node_uv is None:
logger.debug(f"Creating MMD UV node for {mat.name}")
node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
node_uv.name = "mmd_tex_uv"
node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220))
@@ -609,7 +552,7 @@ class FnMaterial:
links.new(node_shader.outputs["Shader"], node_output.inputs["Surface"])
for name_id in ("Base", "Toon", "Sphere"):
texture = self.__get_texture_node("mmd_%s_tex" % name_id.lower())
texture = self.__get_texture_node(f"mmd_{name_id.lower()}_tex")
if texture:
name_tex_in, name_alpha_in, name_uv_out = (name_id + x for x in (" Tex", " Alpha", " UV"))
if not node_shader.inputs.get(name_tex_in, _Dummy).is_linked:
@@ -619,13 +562,12 @@ class FnMaterial:
if not texture.inputs["Vector"].is_linked:
links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"])
def __get_shader_uv(self) -> bpy.types.ShaderNodeTree:
def __get_shader_uv(self):
group_name = "MMDTexUV"
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
logger.debug(f"Creating MMD UV shader node group")
ng = _NodeGroupUtils(shader)
############################################################################
@@ -657,13 +599,12 @@ class FnMaterial:
return shader
def __get_shader(self) -> bpy.types.ShaderNodeTree:
def __get_shader(self):
group_name = "MMDShaderDev"
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
logger.debug(f"Creating MMD shader node group")
ng = _NodeGroupUtils(shader)
############################################################################
@@ -753,18 +694,15 @@ class FnMaterial:
class MigrationFnMaterial:
@staticmethod
def update_mmd_shader() -> None:
def update_mmd_shader():
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
if mmd_shader_node_tree is None:
logger.debug("No MMD shader node tree found, skipping update")
return
ng = _NodeGroupUtils(mmd_shader_node_tree)
if "Color" in ng.node_output.inputs:
logger.debug("MMD shader already has Color output, skipping update")
return
logger.info("Updating MMD shader node tree")
shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0]
node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node
node_output: bpy.types.NodeGroupOutput = ng.node_output
@@ -773,11 +711,3 @@ class MigrationFnMaterial:
ng.new_output_socket("Color", node_sphere.outputs["Color"])
ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
logger.info("MMD shader node tree updated successfully")
# Add Self Shadow input if it doesn't exist
if "Self Shadow" not in ng.node_input.outputs:
logger.info("Adding Self Shadow input to MMD shader")
# Find shader_base_mix node to connect Self Shadow
shader_base_mix = shader_alpha_mix.inputs[2].links[0].from_node
ng.new_input_socket("Self Shadow", shader_base_mix.inputs["Fac"], 0, min_max=(0, 1))
+610 -317
View File
File diff suppressed because it is too large Load Diff
+97 -107
View File
@@ -1,39 +1,34 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2016 MMD Tools authors
# This file is part of MMD Tools.
from ....core.logging_setup import logger
import math
import re
from typing import TYPE_CHECKING, Tuple, cast, List, Dict, Optional, Set, Any, Union, Iterator
from typing import TYPE_CHECKING, Tuple, cast
import bpy
import numpy as np
from bpy.types import Object, ShapeKey, Material, Mesh, Armature, PoseBone, Constraint
from .. import bpyutils, utils
from ..bpyutils import FnContext, FnObject, TransformConstraintOp
from ....core.logging_setup import logger
if TYPE_CHECKING:
from .model import Model
class FnMorph:
def __init__(self, morph: Any, model: "Model"):
def __init__(self, morph, model: "Model"):
self.__morph = morph
self.__rig = model
@classmethod
def storeShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
def storeShapeKeyOrder(cls, obj, shape_key_names):
if len(shape_key_names) < 1:
return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
if obj.data.shape_keys is None:
bpy.ops.object.shape_key_add()
def __move_to_bottom(key_blocks: bpy.types.bpy_prop_collection, name: str) -> None:
def __move_to_bottom(key_blocks, name):
obj.active_shape_key_index = key_blocks.find(name)
bpy.ops.object.shape_key_move(type="BOTTOM")
@@ -45,7 +40,7 @@ class FnMorph:
__move_to_bottom(key_blocks, name)
@classmethod
def fixShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
def fixShapeKeyOrder(cls, obj, shape_key_names):
if len(shape_key_names) < 1:
return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
@@ -60,11 +55,11 @@ class FnMorph:
bpy.ops.object.shape_key_move(type="BOTTOM")
@staticmethod
def get_morph_slider(rig: "Model") -> "_MorphSlider":
def get_morph_slider(rig):
return _MorphSlider(rig)
@staticmethod
def category_guess(morph: Any) -> None:
def category_guess(morph):
name_lower = morph.name.lower()
if "mouth" in name_lower:
morph.category = "MOUTH"
@@ -75,7 +70,7 @@ class FnMorph:
morph.category = "EYE"
@classmethod
def load_morphs(cls, rig: "Model") -> None:
def load_morphs(cls, rig):
mmd_root = rig.rootObject().mmd_root
vertex_morphs = mmd_root.vertex_morphs
uv_morphs = mmd_root.uv_morphs
@@ -94,7 +89,7 @@ class FnMorph:
cls.category_guess(item)
@staticmethod
def remove_shape_key(mesh_object: Object, shape_key_name: str) -> None:
def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str):
assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys
@@ -106,7 +101,7 @@ class FnMorph:
FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name])
@staticmethod
def copy_shape_key(mesh_object: Object, src_name: str, dest_name: str) -> None:
def copy_shape_key(mesh_object: bpy.types.Object, src_name: str, dest_name: str):
assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys
@@ -128,13 +123,13 @@ class FnMorph:
mesh_object.active_shape_key_index = key_blocks.find(dest_name)
@staticmethod
def get_uv_morph_vertex_groups(obj: Object, morph_name: Optional[str] = None, offset_axes: str = "XYZW") -> Iterator[Tuple[bpy.types.VertexGroup, str, str]]:
def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes="XYZW"):
pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW")
# yield (vertex_group, morph_name, axis),...
return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name))
@staticmethod
def copy_uv_morph_vertex_groups(obj: Object, src_name: str, dest_name: str) -> None:
def copy_uv_morph_vertex_groups(obj, src_name, dest_name):
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
obj.vertex_groups.remove(vg)
@@ -145,12 +140,12 @@ class FnMorph:
obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name)
@staticmethod
def overwrite_bone_morphs_from_action_pose(armature_object: Object) -> None:
def overwrite_bone_morphs_from_action_pose(armature_object):
armature = armature_object.id_data
# Use animation_data and action instead of action_pose
if armature.animation_data is None or armature.animation_data.action is None:
logger.warning('Armature "%s" has no animation data or action', armature_object.name)
logger.warning('[WARNING] armature "%s" has no animation data or action', armature_object.name)
return
action = armature.animation_data.action
@@ -164,7 +159,7 @@ class FnMorph:
bone_morphs = mmd_root.bone_morphs
utils.selectAObject(armature_object)
original_mode = bpy.context.object.mode
original_mode = bpy.context.active_object.mode
bpy.ops.object.mode_set(mode="POSE")
try:
for index, pose_marker in enumerate(pose_markers):
@@ -175,7 +170,7 @@ class FnMorph:
bpy.ops.pose.select_all(action="SELECT")
bpy.ops.pose.transforms_clear()
frame = pose_marker.frame
bpy.context.scene.frame_set(int(frame))
@@ -189,9 +184,9 @@ class FnMorph:
utils.selectAObject(root)
@staticmethod
def clean_uv_morph_vertex_groups(obj: Object) -> None:
def clean_uv_morph_vertex_groups(obj):
# remove empty vertex groups of uv morphs
vg_indices: Set[int] = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
vg_indices = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
vertex_groups = obj.vertex_groups
for v in obj.data.vertices:
for x in v.groups:
@@ -205,8 +200,8 @@ class FnMorph:
vertex_groups.remove(vg)
@staticmethod
def get_uv_morph_offset_map(obj: Object, morph: Any) -> Dict[int, List[float]]:
offset_map: Dict[int, List[float]] = {} # offset_map[vertex_index] = offset_xyzw
def get_uv_morph_offset_map(obj, morph):
offset_map = {} # offset_map[vertex_index] = offset_xyzw
if morph.data_type == "VERTEX_GROUP":
scale = morph.vertex_group_scale
axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)}
@@ -221,13 +216,13 @@ class FnMorph:
for val in morph.data:
i = val.index
if i in offset_map:
offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset)]
offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset, strict=False)]
else:
offset_map[i] = val.offset
return offset_map
@staticmethod
def store_uv_morph_data(obj: Object, morph: Any, offsets: Optional[List[Any]] = None, offset_axes: str = "XYZW") -> None:
def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"):
vertex_groups = obj.vertex_groups
morph_name = getattr(morph, "name", None)
if offset_axes:
@@ -246,13 +241,13 @@ class FnMorph:
max_value = max(max(abs(x) for x in v) for v in offset_map.values() or ([0],))
scale = morph.vertex_group_scale = max(abs(morph.vertex_group_scale), max_value)
for idx, offset in offset_map.items():
for val, axis in zip(offset, "XYZW"):
for val, axis in zip(offset, "XYZW", strict=False):
if abs(val) > 1e-4:
vg_name = "UV_{0}{1}{2}".format(morph_name, "-" if val < 0 else "+", axis)
vg_name = f"UV_{morph_name}{'-' if val < 0 else '+'}{axis}"
vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name)
vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE")
def update_mat_related_mesh(self, new_mesh: Optional[Object] = None) -> None:
def update_mat_related_mesh(self, new_mesh=None):
for offset in self.__morph.data:
# Use the new_mesh if provided
meshObj = new_mesh
@@ -272,28 +267,28 @@ class FnMorph:
offset.related_mesh = meshObj.data.name
@staticmethod
def clean_duplicated_material_morphs(mmd_root_object: Object) -> None:
def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object):
"""Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]"""
mmd_root = mmd_root_object.mmd_root
def morph_data_equals(l: Any, r: Any) -> bool:
def morph_data_equals(left, right) -> bool:
return (
l.related_mesh_data == r.related_mesh_data
and l.offset_type == r.offset_type
and l.material == r.material
and all(a == b for a, b in zip(l.diffuse_color, r.diffuse_color))
and all(a == b for a, b in zip(l.specular_color, r.specular_color))
and l.shininess == r.shininess
and all(a == b for a, b in zip(l.ambient_color, r.ambient_color))
and all(a == b for a, b in zip(l.edge_color, r.edge_color))
and l.edge_weight == r.edge_weight
and all(a == b for a, b in zip(l.texture_factor, r.texture_factor))
and all(a == b for a, b in zip(l.sphere_texture_factor, r.sphere_texture_factor))
and all(a == b for a, b in zip(l.toon_texture_factor, r.toon_texture_factor))
left.related_mesh_data == right.related_mesh_data
and left.offset_type == right.offset_type
and left.material == right.material
and all(a == b for a, b in zip(left.diffuse_color, right.diffuse_color, strict=False))
and all(a == b for a, b in zip(left.specular_color, right.specular_color, strict=False))
and left.shininess == right.shininess
and all(a == b for a, b in zip(left.ambient_color, right.ambient_color, strict=False))
and all(a == b for a, b in zip(left.edge_color, right.edge_color, strict=False))
and left.edge_weight == right.edge_weight
and all(a == b for a, b in zip(left.texture_factor, right.texture_factor, strict=False))
and all(a == b for a, b in zip(left.sphere_texture_factor, right.sphere_texture_factor, strict=False))
and all(a == b for a, b in zip(left.toon_texture_factor, right.toon_texture_factor, strict=False))
)
def morph_equals(l: Any, r: Any) -> bool:
return len(l.data) == len(r.data) and all(morph_data_equals(a, b) for a, b in zip(l.data, r.data))
def morph_equals(left, right) -> bool:
return len(left.data) == len(right.data) and all(morph_data_equals(a, b) for a, b in zip(left.data, right.data, strict=False))
# Remove duplicated mmd_root.material_morphs.data[]
for material_morph in mmd_root.material_morphs:
@@ -327,7 +322,7 @@ class _MorphSlider:
def __init__(self, model: "Model"):
self.__rig = model
def placeholder(self, create: bool = False, binded: bool = False) -> Optional[Object]:
def placeholder(self, create=False, binded=False):
rig = self.__rig
root = rig.rootObject()
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None)
@@ -345,11 +340,11 @@ class _MorphSlider:
return obj
@property
def dummy_armature(self) -> Optional[Object]:
def dummy_armature(self):
obj = self.placeholder()
return self.__dummy_armature(obj) if obj else None
def __dummy_armature(self, obj: Object, create: bool = False) -> Optional[Object]:
def __dummy_armature(self, obj, create=False):
arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None)
if create and arm is None:
arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature"))
@@ -362,7 +357,7 @@ class _MorphSlider:
FnBone.setup_special_bone_collections(arm)
return arm
def get(self, morph_name: str) -> Optional[ShapeKey]:
def get(self, morph_name):
obj = self.placeholder()
if obj is None:
return None
@@ -371,13 +366,13 @@ class _MorphSlider:
return None
return key_blocks.get(morph_name, None)
def create(self) -> Object:
def create(self):
self.__rig.loadMorphs()
obj = self.placeholder(create=True)
self.__load(obj, self.__rig.rootObject().mmd_root)
return obj
def __load(self, obj: Object, mmd_root: Any) -> None:
def __load(self, obj, mmd_root):
attr_list = ("group", "vertex", "bone", "uv", "material")
morph_sliders = obj.data.shape_keys.key_blocks
for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())):
@@ -388,15 +383,15 @@ class _MorphSlider:
obj.shape_key_add(name=name, from_mix=False)
@staticmethod
def __driver_variables(id_data: Any, path: str, index: int = -1) -> Tuple[Any, Any]:
def __driver_variables(id_data, path, index=-1):
d = id_data.driver_add(path, index)
variables = d.driver.variables
for x in variables:
for x in reversed(variables):
variables.remove(x)
return d.driver, variables
@staticmethod
def __add_single_prop(variables: Any, id_obj: Object, data_path: str, prefix: str) -> Any:
def __add_single_prop(variables, id_obj, data_path, prefix):
var = variables.new()
var.name = f"{prefix}{len(variables)}"
var.type = "SINGLE_PROP"
@@ -407,7 +402,7 @@ class _MorphSlider:
return var
@staticmethod
def __shape_key_driver_check(key_block: ShapeKey, resolve_path: bool = False) -> bool:
def __shape_key_driver_check(key_block, resolve_path=False):
if resolve_path:
try:
key_block.id_data.path_resolve(key_block.path_from_id())
@@ -421,22 +416,20 @@ class _MorphSlider:
d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None)
return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables)))
def __cleanup(self, names_in_use: Optional[Dict[str, Any]] = None) -> None:
from math import ceil, floor
def __cleanup(self, names_in_use=None):
names_in_use = names_in_use or {}
rig = self.__rig
morph_sliders = self.placeholder()
morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {}
for mesh_object in rig.meshes():
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[ShapeKey], ())):
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast("Tuple[bpy.types.ShapeKey]", ())):
if kb.name in names_in_use:
continue
if kb.name.startswith("mmd_bind"):
kb.driver_remove("value")
ms = morph_sliders[kb.relative_key.name]
kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, floor(ms.value)), max(ms.slider_max, ceil(ms.value))
kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, math.floor(ms.value)), max(ms.slider_max, math.ceil(ms.value))
kb.relative_key.value = ms.value
kb.relative_key.mute = False
FnObject.mesh_remove_shape_key(mesh_object, kb)
@@ -444,9 +437,9 @@ class _MorphSlider:
elif kb.name in morph_sliders and self.__shape_key_driver_check(kb):
ms = morph_sliders[kb.name]
kb.driver_remove("value")
kb.slider_min, kb.slider_max = min(ms.slider_min, floor(kb.value)), max(ms.slider_max, ceil(kb.value))
kb.slider_min, kb.slider_max = min(ms.slider_min, math.floor(kb.value)), max(ms.slider_max, math.ceil(kb.value))
for m in mesh_object.modifiers: # uv morph
for m in reversed(mesh_object.modifiers): # uv morph
if m.name.startswith("mmd_bind") and m.name not in names_in_use:
mesh_object.modifiers.remove(m)
@@ -461,13 +454,13 @@ class _MorphSlider:
attributes = set(TransformConstraintOp.min_max_attributes("LOCATION", "to"))
attributes |= set(TransformConstraintOp.min_max_attributes("ROTATION", "to"))
for b in rig.armature().pose.bones:
for c in b.constraints:
for c in reversed(b.constraints):
if c.name.startswith("mmd_bind") and c.name[:-4] not in names_in_use:
for attr in attributes:
c.driver_remove(attr)
b.constraints.remove(c)
def unbind(self) -> None:
def unbind(self):
mmd_root = self.__rig.rootObject().mmd_root
# after unbind, the weird lag problem will disappear.
@@ -490,7 +483,7 @@ class _MorphSlider:
b.driver_remove("rotation_quaternion")
self.__cleanup()
def bind(self) -> None:
def bind(self):
rig = self.__rig
root = rig.rootObject()
armObj = rig.armature()
@@ -504,10 +497,10 @@ class _MorphSlider:
morph_sliders = obj.data.shape_keys.key_blocks
# data gathering
group_map: Dict[Tuple[str, str], List[List[Any]]] = {}
group_map = {}
shape_key_map: Dict[str, List[Tuple[ShapeKey, str, List[Any]]]] = {}
uv_morph_map: Dict[str, List[Tuple[str, str, str, List[Any]]]] = {}
shape_key_map = {}
uv_morph_map = {}
for mesh_object in rig.meshes():
mesh_object.show_only_shape_key = False
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
@@ -528,11 +521,11 @@ class _MorphSlider:
kb_bind.slider_max = 10
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"')
groups: List[Any] = []
groups = []
shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
uv_layers = [l.name for l in mesh_object.data.uv_layers if not l.name.startswith("_")]
uv_layers = [layer.name for layer in mesh_object.data.uv_layers if not layer.name.startswith("_")]
uv_layers += [""] * (5 - len(uv_layers))
for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh_object):
morph = mmd_root.uv_morphs.get(morph_name, None)
@@ -544,7 +537,7 @@ class _MorphSlider:
continue
name_bind = "mmd_bind%s" % hash(vg.name)
uv_morph_map.setdefault(name_bind, [])
uv_morph_map.setdefault(name_bind, ())
mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP")
mod.show_expanded = False
mod.vertex_group = vg.name
@@ -557,13 +550,13 @@ class _MorphSlider:
else:
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base"
bone_offset_map: Dict[str, Tuple[str, Any, str, str, List[Any]]] = {}
bone_offset_map = {}
with bpyutils.edit_object(arm) as data:
from .bone import FnBone
edit_bones = data.edit_bones
def __get_bone(name: str, parent: Optional[bpy.types.EditBone]) -> bpy.types.EditBone:
def __get_bone(name, parent):
b = edit_bones.get(name, None) or edit_bones.new(name=name)
b.head = (0, 0, 0)
b.tail = (0, 0, 1)
@@ -580,7 +573,7 @@ class _MorphSlider:
continue
d.name = name_bind = f"mmd_bind{hash(d)}"
b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None))
groups: List[Any] = []
groups = []
bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
group_map.setdefault(("bone_morphs", m.name), []).append(groups)
@@ -591,21 +584,21 @@ class _MorphSlider:
scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale'
name_bind = f"mmd_bind{hash(m.name)}"
b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base))
groups: List[Any] = []
groups = []
uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups))
group_map.setdefault(("uv_morphs", m.name), []).append(groups)
used_bone_names: Set[str] = set(bone_offset_map.keys()) | set(uv_morph_map.keys())
used_bone_names = bone_offset_map.keys() | uv_morph_map.keys()
used_bone_names.add(ctrl_base.name)
for b in edit_bones: # cleanup
for b in reversed(edit_bones): # cleanup
if b.name.startswith("mmd_bind") and b.name not in used_bone_names:
edit_bones.remove(b)
material_offset_map: Dict[str, Any] = {}
material_offset_map = {}
for m in mmd_root.material_morphs:
morph_name = m.name.replace('"', '\\"')
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
groups: List[Any] = []
groups = []
group_map.setdefault(("material_morphs", m.name), []).append(groups)
material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups)
for d in m.data:
@@ -616,7 +609,7 @@ class _MorphSlider:
for m in mmd_root.group_morphs:
if len(m.data) != len(set(m.data.keys())):
logger.warning('Found duplicated morph data in Group Morph "%s"', m.name)
logger.warning(' * Found duplicated morph data in Group Morph "%s"', m.name)
morph_name = m.name.replace('"', '\\"')
morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
for d in m.data:
@@ -627,7 +620,7 @@ class _MorphSlider:
self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys())
def __config_groups(variables: Any, expression: str, groups: List[Any]) -> str:
def __config_groups(variables, expression, groups):
for g_name, morph_path, factor_path in groups:
var = self.__add_single_prop(variables, obj, morph_path, "g")
fvar = self.__add_single_prop(variables, root, factor_path, "w")
@@ -635,7 +628,7 @@ class _MorphSlider:
return expression
# vertex morphs
for kb_bind, morph_data_path, groups in (i for l in shape_key_map.values() for i in l):
for kb_bind, morph_data_path, groups in (i for value_list in shape_key_map.values() for i in value_list):
driver, variables = self.__driver_variables(kb_bind, "value")
var = self.__add_single_prop(variables, obj, morph_data_path, "v")
if kb_bind.name.startswith("mmd_bind"):
@@ -646,7 +639,7 @@ class _MorphSlider:
kb_bind.mute = False
# bone morphs
def __config_bone_morph(constraints: bpy.types.ArmatureConstraints, map_type: str, attributes: Set[str], val: float, val_str: str) -> None:
def __config_bone_morph(constraints, map_type, attributes, val, val_str):
c_name = f"mmd_bind{hash(data)}.{map_type[:3]}"
c = TransformConstraintOp.create(constraints, c_name, map_type)
TransformConstraintOp.update_min_max(c, val, None)
@@ -660,8 +653,6 @@ class _MorphSlider:
sign = "-" if attr.startswith("to_min") else ""
driver.expression = f"{sign}{val_str}*({expression})"
from math import pi
attributes_rot = TransformConstraintOp.min_max_attributes("ROTATION", "to")
attributes_loc = TransformConstraintOp.min_max_attributes("LOCATION", "to")
for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values():
@@ -671,7 +662,7 @@ class _MorphSlider:
b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND"
pb = armObj.pose.bones[data.bone]
__config_bone_morph(pb.constraints, "ROTATION", attributes_rot, pi, "pi")
__config_bone_morph(pb.constraints, "ROTATION", attributes_rot, math.pi, "pi")
__config_bone_morph(pb.constraints, "LOCATION", attributes_loc, 100, "100")
# uv morphs
@@ -680,7 +671,7 @@ class _MorphSlider:
b = arm.pose.bones["mmd_bind_ctrl_base"]
b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND"
for bname, data_path, scale_path, groups in (i for l in uv_morph_map.values() for i in l):
for bname, data_path, scale_path, groups in (i for value_list in uv_morph_map.values() for i in value_list):
b = arm.pose.bones[bname]
b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND"
@@ -694,9 +685,9 @@ class _MorphSlider:
group_dict = material_offset_map.get("group_dict", {})
def __config_material_morph(mat: Material, morph_list: List[Tuple[str, Any, str]]) -> None:
def __config_material_morph(mat, morph_list):
nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list))
for (morph_name, data, name_bind), node in zip(morph_list, nodes):
for (morph_name, data, name_bind), node in zip(morph_list, nodes, strict=False):
node.label, node.name = morph_name, name_bind
data_path, groups = group_dict[morph_name]
driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id("default_value"))
@@ -706,7 +697,7 @@ class _MorphSlider:
for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")):
mul_all, add_all = material_offset_map.get("#", ([], []))
if mat.name == "":
logger.warning("Oh no. The material name should never be empty.")
logger.warning("Oh no. The material name should never empty.")
mul_list, add_list = [], []
else:
mat_name = "#" + mat.name
@@ -722,7 +713,7 @@ class _MorphSlider:
class MigrationFnMorph:
@staticmethod
def update_mmd_morph() -> None:
def update_mmd_morph():
from .material import FnMaterial
for root in bpy.data.objects:
@@ -733,7 +724,7 @@ class MigrationFnMorph:
for morph_data in mat_morph.data:
if morph_data.material_data is not None:
# SUPPORT_UNTIL: 5 LTS
# The material_id is also no longer used, but for compatibility with older version mmd_tools, keep it.
# The material_id is also no longer used, but for compatibility with older version mmd_tools_local, keep it.
if "material_id" not in morph_data.material_data.mmd_material or "material_id" not in morph_data or morph_data.material_data.mmd_material["material_id"] == morph_data["material_id"]:
# In the new version, the related_mesh property is no longer used.
# Explicitly remove this property to avoid misuse.
@@ -741,15 +732,14 @@ class MigrationFnMorph:
del morph_data["related_mesh"]
continue
else:
# Compat case. The new version mmd_tools saved. And old version mmd_tools edit. Then new version mmd_tools load again.
# Go update path.
pass
# Compat case. The new version mmd_tools_local saved. And old version mmd_tools_local edit. Then new version mmd_tools_local load again.
# Go update path.
pass
morph_data.material_data = None
if "material_id" in morph_data:
mat_id = morph_data["material_id"]
if mat_id != -1:
if mat_id >= 0:
fnMat = FnMaterial.from_material_id(mat_id)
if fnMat:
morph_data.material_data = fnMat.material
@@ -764,11 +754,11 @@ class MigrationFnMorph:
morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
@staticmethod
def ensure_material_id_not_conflict() -> None:
mat_ids_set: Set[int] = set()
def ensure_material_id_not_conflict():
mat_ids_set = set()
# The reference library properties cannot be modified and bypassed in advance.
need_update_mat: List[Material] = []
need_update_mat = []
for mat in bpy.data.materials:
if mat.mmd_material.material_id < 0:
continue
@@ -783,7 +773,7 @@ class MigrationFnMorph:
mat_ids_set.add(mat.mmd_material.material_id)
@staticmethod
def compatible_with_old_version_mmd_tools() -> None:
def compatible_with_old_version_mmd_tools_local():
MigrationFnMorph.ensure_material_id_not_conflict()
for root in bpy.data.objects:
+177 -177
View File
@@ -5,7 +5,7 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
from .....core.logging_setup import logger
import os
import struct
@@ -40,7 +40,7 @@ class FileStream:
def close(self):
if self.__file_obj is not None:
logging.debug('close the file("%s")', self.__path)
logger.debug('close the file("%s")', self.__path)
self.__file_obj.close()
self.__file_obj = None
@@ -260,20 +260,20 @@ class Header:
return 4
def load(self, fs):
logging.info('loading pmx header information...')
logger.info('loading pmx header information...')
self.sign = fs.readBytes(4)
logging.debug('File signature is %s', self.sign)
logger.debug('File signature is %s', self.sign)
if self.sign[:3] != self.PMX_SIGN[:3]:
logging.info('File signature is invalid')
logging.error('This file is unsupported format, or corrupt file.')
logger.info('File signature is invalid')
logger.error('This file is unsupported format, or corrupt file.')
raise InvalidFileError('File signature is invalid.')
self.version = fs.readFloat()
logging.info('pmx format version: %f', self.version)
logger.info('pmx format version: %f', self.version)
if self.version != self.VERSION:
logging.error('PMX version %.1f is unsupported', self.version)
logger.error('PMX version %.1f is unsupported', self.version)
raise UnsupportedVersionError('unsupported PMX version: %.1f'%self.version)
if fs.readByte() != 8 or self.sign[3] != self.PMX_SIGN[3]:
logging.warning(' * This file might be corrupted.')
logger.warning(' * This file might be corrupted.')
self.encoding = Encoding(fs.readByte())
self.additional_uvs = fs.readByte()
self.vertex_index_size = fs.readByte()
@@ -283,19 +283,19 @@ class Header:
self.morph_index_size = fs.readByte()
self.rigid_index_size = fs.readByte()
logging.info('----------------------------')
logging.info('pmx header information')
logging.info('----------------------------')
logging.info('pmx version: %.1f', self.version)
logging.info('encoding: %s', str(self.encoding))
logging.info('number of uvs: %d', self.additional_uvs)
logging.info('vertex index size: %d byte(s)', self.vertex_index_size)
logging.info('texture index: %d byte(s)', self.texture_index_size)
logging.info('material index: %d byte(s)', self.material_index_size)
logging.info('bone index: %d byte(s)', self.bone_index_size)
logging.info('morph index: %d byte(s)', self.morph_index_size)
logging.info('rigid index: %d byte(s)', self.rigid_index_size)
logging.info('----------------------------')
logger.info('----------------------------')
logger.info('pmx header information')
logger.info('----------------------------')
logger.info('pmx version: %.1f', self.version)
logger.info('encoding: %s', str(self.encoding))
logger.info('number of uvs: %d', self.additional_uvs)
logger.info('vertex index size: %d byte(s)', self.vertex_index_size)
logger.info('texture index: %d byte(s)', self.texture_index_size)
logger.info('material index: %d byte(s)', self.material_index_size)
logger.info('bone index: %d byte(s)', self.bone_index_size)
logger.info('morph index: %d byte(s)', self.morph_index_size)
logger.info('rigid index: %d byte(s)', self.rigid_index_size)
logger.info('----------------------------')
def save(self, fs):
fs.writeBytes(self.PMX_SIGN)
@@ -364,27 +364,27 @@ class Model:
self.comment = fs.readStr()
self.comment_e = fs.readStr()
logging.info('Model name: %s', self.name)
logging.info('Model name(english): %s', self.name_e)
logging.info('Comment:%s', self.comment)
logging.info('Comment(english):%s', self.comment_e)
logger.info('Model name: %s', self.name)
logger.info('Model name(english): %s', self.name_e)
logger.info('Comment:%s', self.comment)
logger.info('Comment(english):%s', self.comment_e)
logging.info('')
logging.info('------------------------------')
logging.info('Load Vertices')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info('Load Vertices')
logger.info('------------------------------')
num_vertices = fs.readInt()
self.vertices = []
for i in range(num_vertices):
v = Vertex()
v.load(fs)
self.vertices.append(v)
logging.info('----- Loaded %d vertices', len(self.vertices))
logger.info('----- Loaded %d vertices', len(self.vertices))
logging.info('')
logging.info('------------------------------')
logging.info(' Load Faces')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info(' Load Faces')
logger.info('------------------------------')
num_faces = fs.readInt()
self.faces = []
for i in range(int(num_faces/3)):
@@ -392,25 +392,25 @@ class Model:
f2 = fs.readVertexIndex()
f3 = fs.readVertexIndex()
self.faces.append((f3, f2, f1))
logging.info(' Load %d faces', len(self.faces))
logger.info(' Load %d faces', len(self.faces))
logging.info('')
logging.info('------------------------------')
logging.info(' Load Textures')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info(' Load Textures')
logger.info('------------------------------')
num_textures = fs.readInt()
self.textures = []
for i in range(num_textures):
t = Texture()
t.load(fs)
self.textures.append(t)
logging.info('Texture %d: %s', i, t.path)
logging.info(' ----- Loaded %d textures', len(self.textures))
logger.info('Texture %d: %s', i, t.path)
logger.info(' ----- Loaded %d textures', len(self.textures))
logging.info('')
logging.info('------------------------------')
logging.info(' Load Materials')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info(' Load Materials')
logger.info('------------------------------')
num_materials = fs.readInt()
self.materials = []
for i in range(num_materials):
@@ -418,38 +418,38 @@ class Model:
m.load(fs, num_textures)
self.materials.append(m)
logging.info('Material %d: %s', i, m.name)
logging.debug(' Name(english): %s', m.name_e)
logging.debug(' Comment: %s', m.comment)
logging.debug(' Vertex Count: %d', m.vertex_count)
logging.debug(' Diffuse: (%.2f, %.2f, %.2f, %.2f)', *m.diffuse)
logging.debug(' Specular: (%.2f, %.2f, %.2f)', *m.specular)
logging.debug(' Shininess: %f', m.shininess)
logging.debug(' Ambient: (%.2f, %.2f, %.2f)', *m.ambient)
logging.debug(' Double Sided: %s', str(m.is_double_sided))
logging.debug(' Drop Shadow: %s', str(m.enabled_drop_shadow))
logging.debug(' Self Shadow: %s', str(m.enabled_self_shadow))
logging.debug(' Self Shadow Map: %s', str(m.enabled_self_shadow_map))
logging.debug(' Edge: %s', str(m.enabled_toon_edge))
logging.debug(' Edge Color: (%.2f, %.2f, %.2f, %.2f)', *m.edge_color)
logging.debug(' Edge Size: %.2f', m.edge_size)
logger.info('Material %d: %s', i, m.name)
logger.debug(' Name(english): %s', m.name_e)
logger.debug(' Comment: %s', m.comment)
logger.debug(' Vertex Count: %d', m.vertex_count)
logger.debug(' Diffuse: (%.2f, %.2f, %.2f, %.2f)', *m.diffuse)
logger.debug(' Specular: (%.2f, %.2f, %.2f)', *m.specular)
logger.debug(' Shininess: %f', m.shininess)
logger.debug(' Ambient: (%.2f, %.2f, %.2f)', *m.ambient)
logger.debug(' Double Sided: %s', str(m.is_double_sided))
logger.debug(' Drop Shadow: %s', str(m.enabled_drop_shadow))
logger.debug(' Self Shadow: %s', str(m.enabled_self_shadow))
logger.debug(' Self Shadow Map: %s', str(m.enabled_self_shadow_map))
logger.debug(' Edge: %s', str(m.enabled_toon_edge))
logger.debug(' Edge Color: (%.2f, %.2f, %.2f, %.2f)', *m.edge_color)
logger.debug(' Edge Size: %.2f', m.edge_size)
if m.texture != -1:
logging.debug(' Texture Index: %d', m.texture)
logger.debug(' Texture Index: %d', m.texture)
else:
logging.debug(' Texture: None')
logger.debug(' Texture: None')
if m.sphere_texture != -1:
logging.debug(' Sphere Texture Index: %d', m.sphere_texture)
logging.debug(' Sphere Texture Mode: %d', m.sphere_texture_mode)
logger.debug(' Sphere Texture Index: %d', m.sphere_texture)
logger.debug(' Sphere Texture Mode: %d', m.sphere_texture_mode)
else:
logging.debug(' Sphere Texture: None')
logging.debug('')
logger.debug(' Sphere Texture: None')
logger.debug('')
logging.info('----- Loaded %d materials.', len(self.materials))
logger.info('----- Loaded %d materials.', len(self.materials))
logging.info('')
logging.info('------------------------------')
logging.info(' Load Bones')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info(' Load Bones')
logger.info('------------------------------')
num_bones = fs.readInt()
self.bones = []
for i in range(num_bones):
@@ -457,33 +457,33 @@ class Model:
b.load(fs)
self.bones.append(b)
logging.info('Bone %d: %s', i, b.name)
logging.debug(' Name(english): %s', b.name_e)
logging.debug(' Location: (%f, %f, %f)', *b.location)
logging.debug(' displayConnection: %s', str(b.displayConnection))
logging.debug(' Parent: %s', str(b.parent))
logging.debug(' Transform Order: %s', str(b.transform_order))
logging.debug(' Rotatable: %s', str(b.isRotatable))
logging.debug(' Movable: %s', str(b.isMovable))
logging.debug(' Visible: %s', str(b.visible))
logging.debug(' Controllable: %s', str(b.isControllable))
logging.debug(' Additional Location: %s', str(b.hasAdditionalLocation))
logging.debug(' Additional Rotation: %s', str(b.hasAdditionalRotate))
logger.info('Bone %d: %s', i, b.name)
logger.debug(' Name(english): %s', b.name_e)
logger.debug(' Location: (%f, %f, %f)', *b.location)
logger.debug(' displayConnection: %s', str(b.displayConnection))
logger.debug(' Parent: %s', str(b.parent))
logger.debug(' Transform Order: %s', str(b.transform_order))
logger.debug(' Rotatable: %s', str(b.isRotatable))
logger.debug(' Movable: %s', str(b.isMovable))
logger.debug(' Visible: %s', str(b.visible))
logger.debug(' Controllable: %s', str(b.isControllable))
logger.debug(' Additional Location: %s', str(b.hasAdditionalLocation))
logger.debug(' Additional Rotation: %s', str(b.hasAdditionalRotate))
if b.additionalTransform is not None:
logging.debug(' Additional Transform: Bone:%d, influence: %f', *b.additionalTransform)
logging.debug(' IK: %s', str(b.isIK))
logger.debug(' Additional Transform: Bone:%d, influence: %f', *b.additionalTransform)
logger.debug(' IK: %s', str(b.isIK))
if b.isIK:
logging.debug(' Unit Angle: %f', b.rotationConstraint)
logging.debug(' Target: %d', b.target)
logger.debug(' Unit Angle: %f', b.rotationConstraint)
logger.debug(' Target: %d', b.target)
for j, link in enumerate(b.ik_links):
logging.debug(' IK Link %d: %d, %s - %s', j, link.target, str(link.minimumAngle), str(link.maximumAngle))
logging.debug('')
logging.info('----- Loaded %d bones.', len(self.bones))
logger.debug(' IK Link %d: %d, %s - %s', j, link.target, str(link.minimumAngle), str(link.maximumAngle))
logger.debug('')
logger.info('----- Loaded %d bones.', len(self.bones))
logging.info('')
logging.info('------------------------------')
logging.info(' Load Morphs')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info(' Load Morphs')
logger.info('------------------------------')
num_morph = fs.readInt()
self.morphs = []
display_categories = {0: 'System', 1: 'Eyebrow', 2: 'Eye', 3: 'Mouth', 4: 'Other'}
@@ -491,16 +491,16 @@ class Model:
m = Morph.create(fs)
self.morphs.append(m)
logging.info('%s %d: %s', m.__class__.__name__, i, m.name)
logging.debug(' Name(english): %s', m.name_e)
logging.debug(' Category: %s (%d)', display_categories.get(m.category, '#Invalid'), m.category)
logging.debug('')
logging.info('----- Loaded %d morphs.', len(self.morphs))
logger.info('%s %d: %s', m.__class__.__name__, i, m.name)
logger.debug(' Name(english): %s', m.name_e)
logger.debug(' Category: %s (%d)', display_categories.get(m.category, '#Invalid'), m.category)
logger.debug('')
logger.info('----- Loaded %d morphs.', len(self.morphs))
logging.info('')
logging.info('------------------------------')
logging.info(' Load Display Items')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info(' Load Display Items')
logger.info('------------------------------')
num_disp = fs.readInt()
self.display = []
for i in range(num_disp):
@@ -508,15 +508,15 @@ class Model:
d.load(fs)
self.display.append(d)
logging.info('Display Item %d: %s', i, d.name)
logging.debug(' Name(english): %s', d.name_e)
logging.debug('')
logging.info('----- Loaded %d display items.', len(self.display))
logger.info('Display Item %d: %s', i, d.name)
logger.debug(' Name(english): %s', d.name_e)
logger.debug('')
logger.info('----- Loaded %d display items.', len(self.display))
logging.info('')
logging.info('------------------------------')
logging.info(' Load Rigid Bodies')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info(' Load Rigid Bodies')
logger.info('------------------------------')
num_rigid = fs.readInt()
self.rigids = []
rigid_types = {0: 'Sphere', 1: 'Box', 2: 'Capsule'}
@@ -525,27 +525,27 @@ class Model:
r = Rigid()
r.load(fs)
self.rigids.append(r)
logging.info('Rigid Body %d: %s', i, r.name)
logging.debug(' Name(english): %s', r.name_e)
logging.debug(' Type: %s', rigid_types[r.type])
logging.debug(' Mode: %s (%d)', rigid_modes.get(r.mode, '#Invalid'), r.mode)
logging.debug(' Related bone: %s', r.bone)
logging.debug(' Collision group: %d', r.collision_group_number)
logging.debug(' Collision group mask: 0x%x', r.collision_group_mask)
logging.debug(' Size: (%f, %f, %f)', *r.size)
logging.debug(' Location: (%f, %f, %f)', *r.location)
logging.debug(' Rotation: (%f, %f, %f)', *r.rotation)
logging.debug(' Mass: %f', r.mass)
logging.debug(' Bounce: %f', r.bounce)
logging.debug(' Friction: %f', r.friction)
logging.debug('')
logger.info('Rigid Body %d: %s', i, r.name)
logger.debug(' Name(english): %s', r.name_e)
logger.debug(' Type: %s', rigid_types[r.type])
logger.debug(' Mode: %s (%d)', rigid_modes.get(r.mode, '#Invalid'), r.mode)
logger.debug(' Related bone: %s', r.bone)
logger.debug(' Collision group: %d', r.collision_group_number)
logger.debug(' Collision group mask: 0x%x', r.collision_group_mask)
logger.debug(' Size: (%f, %f, %f)', *r.size)
logger.debug(' Location: (%f, %f, %f)', *r.location)
logger.debug(' Rotation: (%f, %f, %f)', *r.rotation)
logger.debug(' Mass: %f', r.mass)
logger.debug(' Bounce: %f', r.bounce)
logger.debug(' Friction: %f', r.friction)
logger.debug('')
logging.info('----- Loaded %d rigid bodies.', len(self.rigids))
logger.info('----- Loaded %d rigid bodies.', len(self.rigids))
logging.info('')
logging.info('------------------------------')
logging.info(' Load Joints')
logging.info('------------------------------')
logger.info('')
logger.info('------------------------------')
logger.info(' Load Joints')
logger.info('------------------------------')
num_joints = fs.readInt()
self.joints = []
for i in range(num_joints):
@@ -553,19 +553,19 @@ class Model:
j.load(fs)
self.joints.append(j)
logging.info('Joint %d: %s', i, j.name)
logging.debug(' Name(english): %s', j.name_e)
logging.debug(' Rigid A: %s', j.src_rigid)
logging.debug(' Rigid B: %s', j.dest_rigid)
logging.debug(' Location: (%f, %f, %f)', *j.location)
logging.debug(' Rotation: (%f, %f, %f)', *j.rotation)
logging.debug(' Location Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_location + j.maximum_location))
logging.debug(' Rotation Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_rotation + j.maximum_rotation))
logging.debug(' Spring: (%f, %f, %f)', *j.spring_constant)
logging.debug(' Spring(rotation): (%f, %f, %f)', *j.spring_rotation_constant)
logging.debug('')
logger.info('Joint %d: %s', i, j.name)
logger.debug(' Name(english): %s', j.name_e)
logger.debug(' Rigid A: %s', j.src_rigid)
logger.debug(' Rigid B: %s', j.dest_rigid)
logger.debug(' Location: (%f, %f, %f)', *j.location)
logger.debug(' Rotation: (%f, %f, %f)', *j.rotation)
logger.debug(' Location Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_location + j.maximum_location))
logger.debug(' Rotation Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_rotation + j.maximum_rotation))
logger.debug(' Spring: (%f, %f, %f)', *j.spring_constant)
logger.debug(' Spring(rotation): (%f, %f, %f)', *j.spring_rotation_constant)
logger.debug('')
logging.info('----- Loaded %d joints.', len(self.joints))
logger.info('----- Loaded %d joints.', len(self.joints))
def save(self, fs):
fs.writeStr(self.name)
@@ -574,7 +574,7 @@ class Model:
fs.writeStr(self.comment)
fs.writeStr(self.comment_e)
logging.info('''exportings pmx model data...
logger.info('''exportings pmx model data...
name: %s
name(english): %s
comment:
@@ -583,62 +583,62 @@ comment(english):
%s
''', self.name, self.name_e, self.comment, self.comment_e)
logging.info('exporting vertices... %d', len(self.vertices))
logger.info('exporting vertices... %d', len(self.vertices))
fs.writeInt(len(self.vertices))
for i in self.vertices:
i.save(fs)
logging.info('finished exporting vertices.')
logger.info('finished exporting vertices.')
logging.info('exporting faces... %d', len(self.faces))
logger.info('exporting faces... %d', len(self.faces))
fs.writeInt(len(self.faces)*3)
for f3, f2, f1 in self.faces:
fs.writeVertexIndex(f1)
fs.writeVertexIndex(f2)
fs.writeVertexIndex(f3)
logging.info('finished exporting faces.')
logger.info('finished exporting faces.')
logging.info('exporting textures... %d', len(self.textures))
logger.info('exporting textures... %d', len(self.textures))
fs.writeInt(len(self.textures))
for i in self.textures:
i.save(fs)
logging.info('finished exporting textures.')
logger.info('finished exporting textures.')
logging.info('exporting materials... %d', len(self.materials))
logger.info('exporting materials... %d', len(self.materials))
fs.writeInt(len(self.materials))
for i in self.materials:
i.save(fs)
logging.info('finished exporting materials.')
logger.info('finished exporting materials.')
logging.info('exporting bones... %d', len(self.bones))
logger.info('exporting bones... %d', len(self.bones))
fs.writeInt(len(self.bones))
for i in self.bones:
i.save(fs)
logging.info('finished exporting bones.')
logger.info('finished exporting bones.')
logging.info('exporting morphs... %d', len(self.morphs))
logger.info('exporting morphs... %d', len(self.morphs))
fs.writeInt(len(self.morphs))
for i in self.morphs:
i.save(fs)
logging.info('finished exporting morphs.')
logger.info('finished exporting morphs.')
logging.info('exporting display items... %d', len(self.display))
logger.info('exporting display items... %d', len(self.display))
fs.writeInt(len(self.display))
for i in self.display:
i.save(fs)
logging.info('finished exporting display items.')
logger.info('finished exporting display items.')
logging.info('exporting rigid bodies... %d', len(self.rigids))
logger.info('exporting rigid bodies... %d', len(self.rigids))
fs.writeInt(len(self.rigids))
for i in self.rigids:
i.save(fs)
logging.info('finished exporting rigid bodies.')
logger.info('finished exporting rigid bodies.')
logging.info('exporting joints... %d', len(self.joints))
logger.info('exporting joints... %d', len(self.joints))
fs.writeInt(len(self.joints))
for i in self.joints:
i.save(fs)
logging.info('finished exporting joints.')
logging.info('finished exporting the model.')
logger.info('finished exporting joints.')
logger.info('finished exporting the model.')
def __repr__(self):
@@ -803,7 +803,7 @@ class Texture:
except ValueError:
relPath = self.path
relPath = relPath.replace(os.path.sep, '\\') # always save using windows path conventions
logging.info('writing to pmx file the relative texture path: %s', relPath)
logger.info('writing to pmx file the relative texture path: %s', relPath)
fs.writeStr(relPath)
class SharedTexture(Texture):
@@ -1170,7 +1170,7 @@ class Morph:
name = fs.readStr()
name_e = fs.readStr()
logging.debug('morph: %s', name)
logger.debug('morph: %s', name)
category = fs.readSignedByte()
typeIndex = fs.readSignedByte()
ret = _CLASSES[typeIndex](name, name_e, category, type_index = typeIndex)
@@ -1399,7 +1399,7 @@ class Display:
else:
raise Exception('invalid value.')
self.data.append((disp_type, index))
logging.debug('the number of display elements: %d', len(self.data))
logger.debug('the number of display elements: %d', len(self.data))
def save(self, fs):
fs.writeStr(self.name)
@@ -1595,12 +1595,12 @@ class Joint:
def load(path):
with FileReadStream(path) as fs:
logging.info('****************************************')
logging.info(' mmd_tools.pmx module')
logging.info('----------------------------------------')
logging.info(' Start to load model data form a pmx file')
logging.info(' by the mmd_tools.pmx modlue.')
logging.info('')
logger.info('****************************************')
logger.info(' mmd_tools.pmx module')
logger.info('----------------------------------------')
logger.info(' Start to load model data form a pmx file')
logger.info(' by the mmd_tools.pmx modlue.')
logger.info('')
header = Header()
header.load(fs)
fs.setHeader(header)
@@ -1608,12 +1608,12 @@ def load(path):
try:
model.load(fs)
except struct.error as e:
logging.error(' * Corrupted file: %s', e)
logger.error(' * Corrupted file: %s', e)
#raise
logging.info(' Finished loading.')
logging.info('----------------------------------------')
logging.info(' mmd_tools.pmx module')
logging.info('****************************************')
logger.info(' Finished loading.')
logger.info('----------------------------------------')
logger.info(' mmd_tools.pmx module')
logger.info('****************************************')
return model
def save(path, model, add_uv_count=0):
+75 -34
View File
@@ -6,6 +6,7 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import collections
import math
import os
import time
from typing import TYPE_CHECKING, List, Optional, Dict, Tuple, Set, Callable, Any, Union, FrozenSet, Iterator
@@ -103,7 +104,7 @@ class PMXImporter:
obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54)
logger.info(f"Creating objects for model: {obj_name}")
self.__rig = Model.create(pmxModel.name, pmxModel.name_e, self.__scale or 1.0, obj_name)
self.__rig = Model.create(pmxModel.name, pmxModel.name_e, self.__scale, obj_name)
root = self.__rig.rootObject()
mmd_root: 'MMDRoot' = root.mmd_root
self.__root = root
@@ -192,7 +193,7 @@ class PMXImporter:
mesh: Mesh = self.__meshObj.data
mesh.vertices.add(count=vertex_count)
mesh.vertices.foreach_set("co", tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * (self.__scale or 1.0))))
mesh.vertices.foreach_set("co", tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * self.__scale)))
vertex_group_table = self.__vertexGroupTable
if not vertex_group_table:
@@ -249,9 +250,9 @@ class PMXImporter:
for i, pv in self.__sdefVertices.items():
w = pv.weight.weights
sdefC.data[i].co = Vector(w.c).xzy * (self.__scale or 1.0)
sdefR0.data[i].co = Vector(w.r0).xzy * (self.__scale or 1.0)
sdefR1.data[i].co = Vector(w.r1).xzy * (self.__scale or 1.0)
sdefC.data[i].co = Vector(w.c).xzy * self.__scale
sdefR0.data[i].co = Vector(w.r0).xzy * self.__scale
sdefR1.data[i].co = Vector(w.r1).xzy * self.__scale
logger.debug(f"Stored {len(self.__sdefVertices)} SDEF vertices in shape keys")
@@ -290,13 +291,13 @@ class PMXImporter:
# Create bones
for i in pmx_bones:
bone = data.edit_bones.new(name=i.name)
loc = _VectorXZY(i.location) * (self.__scale or 1.0)
loc = _VectorXZY(i.location) * self.__scale
bone.head = loc
editBoneTable.append(bone)
nameTable.append(bone.name)
# Set parent relationships
for i, (b_bone, m_bone) in enumerate(zip(editBoneTable, pmx_bones)):
for i, (b_bone, m_bone) in enumerate(zip(editBoneTable, pmx_bones, strict=False)):
if m_bone.parent != -1:
if i not in dependency_cycle_ik_bones:
b_bone.parent = editBoneTable[m_bone.parent]
@@ -304,18 +305,18 @@ class PMXImporter:
b_bone.parent = editBoneTable[m_bone.parent].parent
# Set tail positions
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
if isinstance(m_bone.displayConnection, int):
if m_bone.displayConnection != -1:
b_bone.tail = editBoneTable[m_bone.displayConnection].head
else:
b_bone.tail = b_bone.head
else:
loc = _VectorXZY(m_bone.displayConnection) * (self.__scale or 1.0)
loc = _VectorXZY(m_bone.displayConnection) * self.__scale
b_bone.tail = b_bone.head + loc
# Check and fix IK links
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
if m_bone.isIK and m_bone.target != -1:
logger.debug(f"Checking IK links of {b_bone.name}")
b_target = editBoneTable[m_bone.target]
@@ -333,30 +334,30 @@ class PMXImporter:
b_bone_link.tail = b_bone_link.head + loc
# Fix too short bones
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
# Set the length of too short bones to 1 because Blender delete them.
if b_bone.length < 0.001:
if not self.__apply_bone_fixed_axis and m_bone.axis is not None:
fixed_axis = Vector(m_bone.axis)
if fixed_axis.length:
b_bone.tail = b_bone.head + fixed_axis.xzy.normalized() * (self.__scale or 1.0)
b_bone.tail = b_bone.head + fixed_axis.xzy.normalized() * self.__scale
else:
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * (self.__scale or 1.0)
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale
else:
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * (self.__scale or 1.0)
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale
if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]:
logger.debug(f"Special tip bone {b_bone.name}, display {str(m_bone.displayConnection)}")
specialTipBones.append(b_bone.name)
# Update bone roll
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
if m_bone.localCoordinate is not None:
FnBone.update_bone_roll(b_bone, m_bone.localCoordinate.x_axis, m_bone.localCoordinate.z_axis)
elif FnBone.has_auto_local_axis(m_bone.name):
FnBone.update_auto_bone_roll(b_bone)
# Set bone connections
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
for b_bone, m_bone in zip(editBoneTable, pmx_bones, strict=False):
if isinstance(m_bone.displayConnection, int) and m_bone.displayConnection >= 0:
t = editBoneTable[m_bone.displayConnection]
if t.parent is None or t.parent != b_bone:
@@ -590,7 +591,7 @@ class PMXImporter:
)
for i, (rigid, rigid_obj) in enumerate(zip(self.__model.rigids, rigid_pool)):
loc = Vector(rigid.location).xzy * (self.__scale or 1.0)
loc = Vector(rigid.location).xzy * self.__scale
rot = Vector(rigid.rotation).xzy * -1
size = Vector(rigid.size).xzy if rigid.type == pmx.Rigid.TYPE_BOX else Vector(rigid.size)
@@ -599,7 +600,7 @@ class PMXImporter:
shape_type=rigid.type,
location=loc,
rotation=rot,
size=size * (self.__scale or 1.0),
size=size * self.__scale,
dynamics_type=rigid.mode,
name=rigid.name,
name_e=rigid.name_e,
@@ -637,7 +638,7 @@ class PMXImporter:
)
for i, (joint, joint_obj) in enumerate(zip(self.__model.joints, joint_pool)):
loc = Vector(joint.location).xzy * (self.__scale or 1.0)
loc = Vector(joint.location).xzy * self.__scale
rot = Vector(joint.rotation).xzy * -1
obj = FnRigidBody.setup_joint_object(
@@ -648,8 +649,8 @@ class PMXImporter:
rotation=rot,
rigid_a=self.__rigidTable.get(joint.src_rigid, None),
rigid_b=self.__rigidTable.get(joint.dest_rigid, None),
maximum_location=Vector(joint.maximum_location).xzy * (self.__scale or 1.0),
minimum_location=Vector(joint.minimum_location).xzy * (self.__scale or 1.0),
maximum_location=Vector(joint.maximum_location).xzy * self.__scale,
minimum_location=Vector(joint.minimum_location).xzy * self.__scale,
maximum_rotation=Vector(joint.minimum_rotation).xzy * -1,
minimum_rotation=Vector(joint.maximum_rotation).xzy * -1,
spring_linear=Vector(joint.spring_constant).xzy,
@@ -746,18 +747,22 @@ class PMXImporter:
mesh.polygons.foreach_set("use_smooth", (True,) * len(pmxModel.faces))
mesh.polygons.foreach_set("material_index", material_indices)
uv_layers = mesh.uv_layers
uv_layer = uv_layers.new()
uv_textures, uv_layers = getattr(mesh, "uv_textures", mesh.uv_layers), mesh.uv_layers
uv_tex = uv_textures.new()
uv_layer = uv_layers[uv_tex.name]
uv_table = {vi: self.flipUV_V(v.uv) for vi, v in enumerate(pmxModel.vertices)}
uv_layer.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i]))
if hasattr(mesh, "uv_textures"):
for bf, mi in zip(uv_tex.data, material_indices, strict=False):
bf.image = self.__imageTable.get(mi, None)
if pmxModel.header and pmxModel.header.additional_uvs:
logger.info(f"Importing {pmxModel.header.additional_uvs} additional UVs")
zw_data_map = collections.OrderedDict()
split_uvzw = lambda uvi: (self.flipUV_V(uvi[:2]), uvi[2:])
for i in range(pmxModel.header.additional_uvs):
add_uv = uv_layers.new(name="UV" + str(i + 1))
add_uv = uv_layers[uv_textures.new(name="UV" + str(i + 1)).name]
logger.info(f" - {add_uv.name}...(uv channels)")
uv_table = {vi: split_uvzw(v.additional_uvs[i]) for vi, v in enumerate(pmxModel.vertices)}
add_uv.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i][0]))
@@ -767,10 +772,11 @@ class PMXImporter:
zw_data_map["_" + add_uv.name] = {k: self.flipUV_V(v[1]) for k, v in uv_table.items()}
for name, zw_table in zw_data_map.items():
logger.info(f" - {name}...(zw channels of {name[1:]})")
add_zw = uv_layers.new(name=name)
add_zw = uv_textures.new(name=name)
if add_zw is None:
logger.warning("\t* Lost zw channels")
continue
add_zw = uv_layers[add_zw.name]
add_zw.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in zw_table[i]))
self.__fixOverlappingFaceMaterials(mesh.materials, mesh.vertices, loop_indices, material_indices)
@@ -825,14 +831,18 @@ class PMXImporter:
logger.debug(f"Found {len(vertex_morphs)} vertex morphs")
for morph in vertex_morphs:
shapeKey = self.__meshObj.shape_key_add(name=morph.name)
shapeKey = self.__meshObj.shape_key_add(name=morph.name, from_mix=False)
shapeKey.value = 0.0 # Set shape key value to 0 (inactive) on import
vtx_morph = mmd_root.vertex_morphs.add()
vtx_morph.name = morph.name
vtx_morph.name_e = morph.name_e
vtx_morph.category = categories.get(morph.category, "OTHER")
for md in morph.offsets:
shapeKeyPoint = shapeKey.data[md.index]
shapeKeyPoint.co += Vector(md.offset).xzy * (self.__scale or 1.0)
if md.index < len(shapeKey.data):
shapeKeyPoint = shapeKey.data[md.index]
shapeKeyPoint.co += Vector(md.offset).xzy * self.__scale
else:
logger.warning(f"Morph {morph.name} has out-of-range vertex index: {md.index}")
logger.debug(f"Imported vertex morph: {morph.name} with {len(morph.offsets)} offsets")
def __importMaterialMorphs(self) -> None:
@@ -893,7 +903,7 @@ class PMXImporter:
data = bone_morph.data.add()
bl_bone = self.__boneTable[morph_data.index]
data.bone = bl_bone.name
converter = BoneConverter(bl_bone, self.__scale or 1.0)
converter = BoneConverter(bl_bone, self.__scale)
data.location = converter.convert_location(morph_data.location_offset)
data.rotation = converter.convert_rotation(morph_data.rotation_offset)
valid_offsets += 1
@@ -996,12 +1006,19 @@ class PMXImporter:
armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE")
armModifier.object = armObj
armModifier.use_vertex_groups = True
armModifier.name = "mmd_bone_order_override"
armModifier.show_render = armModifier.show_viewport = len(meshObj.data.vertices) > 0
armModifier.name = "mmd_armature"
logger.debug("Armature modifier added")
def __assignCustomNormals(self) -> None:
"""Assign custom normals to the mesh"""
# NOTE: This uses the older Blender API instead of the newer mesh.attributes approach
# because it requires "INT16_2D" format for proper functionality.
# Manual calculation of normals in INT16_2D format is overly complex.
# The newer implementation was removed in commit [ad47b9a] due to these issues.
# The current implementation uses normals_split_custom_set() with 179-degree sharp edge
# marking as a workaround. While not ideal, this remains the most practical solution
# for preserving custom normals in most cases.
if not self.__meshObj or not self.__model:
logger.error("Mesh object or model not created")
return
@@ -1009,17 +1026,41 @@ class PMXImporter:
mesh: Mesh = self.__meshObj.data
logger.info("Setting custom normals...")
# CRITICAL: Mark sharp edges (based on angle) BEFORE setting custom normals
# For mesh.normals_split_custom_set() to work as expected, two conditions must be met:
# 1. The normal vectors must be non-zero (mentioned in Blender documentation)
# 2. Some edges must be marked as sharp (NOT mentioned in Blender documentation)
# An angle of 179 degrees is confirmed to be sufficient to preserve all custom normals.
# 180 degrees does not work because it misses some sharp edges required for normals_split_custom_set to work 100% correctly.
current_mode = bpy.context.active_object.mode if bpy.context.active_object else 'OBJECT'
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action="DESELECT")
bpy.context.view_layer.objects.active = self.__meshObj
# Mark sharp edges
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(179))
bpy.ops.mesh.mark_sharp()
bpy.ops.object.mode_set(mode="OBJECT")
# Logging
total_edges = len(mesh.edges)
sharp_edges = sum(1 for edge in mesh.edges if edge.use_edge_sharp)
percentage = (sharp_edges / total_edges) * 100 if total_edges > 0 else 0
logger.info(f" - Marked {sharp_edges}/{total_edges} ({percentage:.2f}%) sharp edges with angle: 179 degrees")
if self.__vertex_map:
verts, faces = self.__model.vertices, self.__model.faces
custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f]
mesh.normals_split_custom_set(custom_normals)
logger.debug(f"Set {len(custom_normals)} custom normals using face data")
else:
custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices]
mesh.normals_split_custom_set_from_vertices(custom_normals)
logger.debug(f"Set {len(custom_normals)} custom normals from vertices")
logger.info("Custom normals set successfully")
bpy.ops.object.mode_set(mode=current_mode)
logger.info(" - Done!!")
# Continue without custom normals - mesh will use auto-calculated normals
def __renameLRBones(self, use_underscore: bool) -> None:
"""Rename bones with left/right naming convention"""
+20 -45
View File
@@ -1,17 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# This file is part of MMD Tools.
from typing import List, Optional, Tuple, Union, Dict, Any, Set, cast
from ....core.logging_setup import logger
from typing import List, Optional
import bpy
from mathutils import Euler, Vector, Matrix
from mathutils import Euler, Vector
from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
SHAPE_SPHERE = 0
SHAPE_BOX = 1
@@ -22,30 +18,25 @@ MODE_DYNAMIC = 1
MODE_DYNAMIC_BONE = 2
def shapeType(collision_shape: str) -> int:
"""Convert collision shape name to type index"""
def shapeType(collision_shape):
return ("SPHERE", "BOX", "CAPSULE").index(collision_shape)
def collisionShape(shape_type: int) -> str:
"""Convert shape type index to collision shape name"""
def collisionShape(shape_type):
return ("SPHERE", "BOX", "CAPSULE")[shape_type]
def setRigidBodyWorldEnabled(enable: bool) -> bool:
"""Enable or disable the rigid body world and return previous state"""
def setRigidBodyWorldEnabled(enable):
if bpy.ops.rigidbody.world_add.poll():
logger.debug("Creating rigid body world")
bpy.ops.rigidbody.world_add()
rigidbody_world = bpy.context.scene.rigidbody_world
enabled = rigidbody_world.enabled
rigidbody_world.enabled = enable
logger.debug(f"Rigid body world enabled: {enable} (was: {enabled})")
return enabled
class RigidBodyMaterial:
COLORS: List[int] = [
COLORS = [
0x7FDDD4,
0xF0E68C,
0xEE82EE,
@@ -65,12 +56,10 @@ class RigidBodyMaterial:
]
@classmethod
def getMaterial(cls, number: int) -> bpy.types.Material:
"""Get or create a material for rigid bodies with the specified number"""
def getMaterial(cls, number):
number = int(number)
material_name = f"mmd_tools_rigid_{number}"
material_name = "mmd_tools_rigid_%d" % (number)
if material_name not in bpy.data.materials:
logger.debug(f"Creating rigid body material: {material_name}")
mat = bpy.data.materials.new(material_name)
color = cls.COLORS[number]
mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)]
@@ -82,7 +71,7 @@ class RigidBodyMaterial:
mat.shadow_method = "NONE"
mat.use_backface_culling = True
mat.show_transparent_back = False
# Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
mat.use_nodes = True
nodes, links = mat.node_tree.nodes, mat.node_tree.links
nodes.clear()
node_color = nodes.new("ShaderNodeBackground")
@@ -97,11 +86,9 @@ class RigidBodyMaterial:
class FnRigidBody:
@staticmethod
def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]:
"""Create multiple rigid body objects parented to the specified object"""
if count < 1:
return []
logger.debug(f"Creating {count} rigid body objects parented to {parent_object.name}")
obj = FnRigidBody.new_rigid_body_object(context, parent_object)
if count == 1:
@@ -111,8 +98,6 @@ class FnRigidBody:
@staticmethod
def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object:
"""Create a new rigid body object parented to the specified object"""
logger.debug(f"Creating new rigid body object parented to {parent_object.name}")
obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody"))
obj.parent = parent_object
obj.mmd_type = "RIGID_BODY"
@@ -130,11 +115,11 @@ class FnRigidBody:
@staticmethod
def setup_rigid_body_object(
obj: bpy.types.Object,
shape_type: int,
shape_type: str,
location: Vector,
rotation: Euler,
size: Vector,
dynamics_type: int,
dynamics_type: str,
collision_group_number: Optional[int] = None,
collision_group_mask: Optional[List[bool]] = None,
name: Optional[str] = None,
@@ -146,8 +131,6 @@ class FnRigidBody:
linear_damping: Optional[float] = None,
bounce: Optional[float] = None,
) -> bpy.types.Object:
"""Set up a rigid body object with the specified parameters"""
logger.debug(f"Setting up rigid body object: {obj.name}")
obj.location = location
obj.rotation_euler = rotation
@@ -189,35 +172,31 @@ class FnRigidBody:
return obj
@staticmethod
def get_rigid_body_size(obj: bpy.types.Object) -> Tuple[float, float, float]:
"""Get the size of a rigid body object based on its shape type"""
def get_rigid_body_size(obj: bpy.types.Object):
assert obj.mmd_type == "RIGID_BODY"
x0, y0, z0 = obj.bound_box[0]
x1, y1, z1 = obj.bound_box[6]
assert x1 >= x0 and y1 >= y0 and z1 >= z0
if not (x1 >= x0 and y1 >= y0 and z1 >= z0):
logger.warning(f"Rigid body '{obj.name}' has invalid bounding box coordinates, using default size")
return (1.0, 1.0, 1.0)
shape = obj.mmd_rigid.shape
if shape == "SPHERE":
radius = (z1 - z0) / 2
return (radius, 0.0, 0.0)
elif shape == "BOX":
if shape == "BOX":
x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2
return (x, y, z)
elif shape == "CAPSULE":
if shape == "CAPSULE":
diameter = x1 - x0
radius = diameter / 2
height = abs((z1 - z0) - diameter)
return (radius, height, 0.0)
else:
error_msg = f"Invalid shape type: {shape}"
logger.error(error_msg)
raise ValueError(error_msg)
raise ValueError(f"Invalid shape type: {shape}")
@staticmethod
def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object:
"""Create a new joint object parented to the specified object"""
logger.debug(f"Creating new joint object parented to {parent_object.name}")
obj = FnContext.new_and_link_object(context, name="Joint", object_data=None)
obj.parent = parent_object
obj.mmd_type = "JOINT"
@@ -249,11 +228,9 @@ class FnRigidBody:
@staticmethod
def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]:
"""Create multiple joint objects parented to the specified object"""
if count < 1:
return []
logger.debug(f"Creating {count} joint objects parented to {parent_object.name}")
obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size)
if count == 1:
@@ -277,8 +254,6 @@ class FnRigidBody:
name: str,
name_e: Optional[str] = None,
) -> bpy.types.Object:
"""Set up a joint object with the specified parameters"""
logger.debug(f"Setting up joint object: {obj.name} with name {name}")
obj.name = f"J.{name}"
obj.location = location
+55 -80
View File
@@ -1,52 +1,42 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2018 MMD Tools authors
# This file is part of MMD Tools.
import logging
from ....core.logging_setup import logger
import time
from typing import Dict, List, Tuple, Set, Optional, Any, Union, cast, TypeVar, Callable
import bpy
import numpy as np
from mathutils import Matrix, Vector, Quaternion, Euler
from bpy.types import Object, PoseBone, Pose, ShapeKey, Modifier, VertexGroup
from mathutils import Matrix, Vector
from ..bpyutils import FnObject
from ....core.logging_setup import logger
T = TypeVar('T')
def _hash(v: Union[Object, PoseBone, Pose]) -> int:
def _hash(v):
if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
return hash(type(v).__name__ + v.name)
elif isinstance(v, bpy.types.Pose):
if isinstance(v, bpy.types.Pose):
return hash(type(v).__name__ + v.id_data.name)
else:
raise NotImplementedError("hash")
raise NotImplementedError("hash")
class FnSDEF:
g_verts: Dict[int, Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]] = {} # global cache
g_shapekey_data: Dict[int, Optional[np.ndarray]] = {}
g_bone_check: Dict[int, Dict[Union[Tuple[int, int], str], Union[Tuple[Matrix, Matrix], bool]]] = {}
__g_armature_check: Dict[int, Optional[int]] = {}
SHAPEKEY_NAME: str = "mmd_sdef_skinning"
MASK_NAME: str = "mmd_sdef_mask"
g_verts = {} # global cache
g_shapekey_data = {}
g_bone_check = {}
__g_armature_check = {}
SHAPEKEY_NAME = "mmd_sdef_skinning"
MASK_NAME = "mmd_sdef_mask"
def __init__(self) -> None:
def __init__(self):
raise NotImplementedError("not allowed")
@classmethod
def __init_cache(cls, obj: Object, shapekey: ShapeKey) -> bool:
def __init_cache(cls, obj, shapekey):
key = _hash(obj)
obj = getattr(obj, "original", obj)
mod = obj.modifiers.get("mmd_bone_order_override")
mod = obj.modifiers.get("mmd_armature")
key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None
if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature:
logger.debug(f"Initializing SDEF cache for {obj.name}")
cls.g_verts[key] = cls.__find_vertices(obj)
cls.g_bone_check[key] = {}
cls.__g_armature_check[key] = key_armature
@@ -55,7 +45,7 @@ class FnSDEF:
return False
@classmethod
def __check_bone_update(cls, obj: Object, bone0: PoseBone, bone1: PoseBone) -> bool:
def __check_bone_update(cls, obj, bone0, bone1):
check = cls.g_bone_check[_hash(obj)]
key = (_hash(bone0), _hash(bone1))
if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
@@ -64,21 +54,20 @@ class FnSDEF:
return False
@classmethod
def mute_sdef_set(cls, obj: Object, mute: bool) -> None:
def mute_sdef_set(cls, obj, mute):
key_blocks = getattr(obj.data.shape_keys, "key_blocks", ())
if cls.SHAPEKEY_NAME in key_blocks:
shapekey = key_blocks[cls.SHAPEKEY_NAME]
shapekey.mute = mute
if cls.has_sdef_data(obj):
logger.debug(f"Setting SDEF mute state to {mute} for {obj.name}")
cls.__init_cache(obj, shapekey)
cls.__sdef_muted(obj, shapekey)
@classmethod
def __sdef_muted(cls, obj: Object, shapekey: ShapeKey) -> bool:
def __sdef_muted(cls, obj, shapekey):
mute = shapekey.mute
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
mod = obj.modifiers.get("mmd_bone_order_override")
mod = obj.modifiers.get("mmd_armature")
if mod and mod.type == "ARMATURE":
if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != "EDIT":
mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3])
@@ -87,33 +76,32 @@ class FnSDEF:
mod.invert_vertex_group = True
shapekey.vertex_group = cls.MASK_NAME
cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute
logger.debug(f"SDEF mute state updated to {mute} for {obj.name}")
return mute
@staticmethod
def has_sdef_data(obj: Object) -> bool:
mod = obj.modifiers.get("mmd_bone_order_override")
def has_sdef_data(obj):
if obj is None or not hasattr(obj, "modifiers") or not hasattr(obj, "data") or obj.data is None:
return False
mod = obj.modifiers.get("mmd_armature")
if mod and mod.type == "ARMATURE" and mod.object:
kb = getattr(obj.data.shape_keys, "key_blocks", None)
return kb and "mmd_sdef_c" in kb and "mmd_sdef_r0" in kb and "mmd_sdef_r1" in kb
return False
@classmethod
def __find_vertices(cls, obj: Object) -> Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]:
def __find_vertices(cls, obj):
if not cls.has_sdef_data(obj):
logger.debug(f"SDEF vertex search skipped for '{obj.name}': No SDEF data found")
return {}
vertices: Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]] = {}
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones
bone_map: Dict[int, PoseBone] = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
vertices = {}
pose_bones = obj.modifiers.get("mmd_armature").object.pose.bones
bone_map = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data
sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data
sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data
vd = obj.data.vertices
logger.debug(f"Finding SDEF vertices for {obj.name}")
vertex_count = 0
for i in range(len(sdef_c)):
if vd[i].co != sdef_c[i].co:
bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups
@@ -122,7 +110,7 @@ class FnSDEF:
# preprocessing
w0, w1 = bgs[0].weight, bgs[1].weight
# w0 + w1 == 1
w0 = w0 / (w0 + w1)
w0 /= (w0 + w1)
w1 = 1 - w0
c, r0, r1 = sdef_c[i].co, sdef_r0[i].co, sdef_r1[i].co
@@ -136,19 +124,22 @@ class FnSDEF:
vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], [])
vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2))
vertices[key][3].append(i)
vertex_count += 1
logger.debug(f"Found {vertex_count} SDEF vertices in {obj.name}")
return vertices
@classmethod
def driver_function_wrap(cls, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale):
if obj_name not in bpy.data.objects:
logger.warning(f"SDEF driver wrap: Object '{obj_name}' not found")
return 0.0
obj = bpy.data.objects[obj_name]
shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]
return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale)
@classmethod
def driver_function(cls, shapekey: ShapeKey, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale):
if obj_name not in bpy.data.objects:
logger.warning(f"SDEF driver: Object '{obj_name}' not found, driver will be inactive")
return 0.0
obj = bpy.data.objects[obj_name]
if getattr(shapekey.id_data, "is_evaluated", False):
# For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver
@@ -159,7 +150,7 @@ class FnSDEF:
if cls.__sdef_muted(obj, shapekey):
return 0.0
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones
pose_bones = obj.modifiers.get("mmd_armature").object.pose.bones
if not bulk_update:
shapekey_data = shapekey.data
if use_scale:
@@ -200,8 +191,6 @@ class FnSDEF:
else: # bulk update
shapekey_data = cls.g_shapekey_data[_hash(obj)]
if shapekey_data is None:
import numpy as np
shapekey_data = np.zeros(len(shapekey.data) * 3, dtype=np.float32)
shapekey.data.foreach_get("co", shapekey_data)
shapekey_data = cls.g_shapekey_data[_hash(obj)] = shapekey_data.reshape(len(shapekey.data), 3)
@@ -220,15 +209,15 @@ class FnSDEF:
rot1 = -rot1
s0, s1 = mat0.to_scale(), mat1.to_scale()
def scale(mat_rot: Matrix, w0: float, w1: float) -> Matrix:
def scale(mat_rot, w0, w1, s0, s1):
s = s0 * w0 + s1 * w1
return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])])
def offset(mat_rot: Matrix, pos_c: Vector, vid: int) -> Vector:
def offset(mat_rot, pos_c, vid):
delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = ''
return (mat_rot @ (pos_c + delta)) - delta
shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data]
shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1, s0, s1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data]
else:
# bulk update
for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values():
@@ -247,19 +236,16 @@ class FnSDEF:
return 1.0 # shapkey value
@classmethod
def register_driver_function(cls) -> None:
"""Register driver functions in Blender's driver namespace."""
def register_driver_function(cls):
if "mmd_sdef_driver" not in bpy.app.driver_namespace:
logger.debug("Registering SDEF driver function")
bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function
if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace:
logger.debug("Registering SDEF driver wrapper function")
bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap
BENCH_LOOP: int = 10
BENCH_LOOP = 10
@classmethod
def __get_benchmark_result(cls, obj: Object, shapkey: ShapeKey, use_scale: bool, use_skip: bool) -> bool:
def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip):
# warmed up
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale)
@@ -273,15 +259,15 @@ class FnSDEF:
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
bulk_time = time.time() - t
result = default_time > bulk_time
logger.info(f"SDEF benchmark for {obj.name}: default {default_time:.4f}s vs bulk_update {bulk_time:.4f}s => bulk_update={result}")
logger.info("FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s", default_time, bulk_time, result)
return result
@classmethod
def bind(cls, obj: Object, bulk_update: Optional[bool] = None, use_skip: bool = True, use_scale: bool = False) -> bool:
def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False):
# Unbind first
cls.unbind(obj)
if not cls.has_sdef_data(obj):
logger.debug(f"Object {obj.name} does not have SDEF data")
logger.debug(f"SDEF bind skipped for '{obj.name}': No SDEF data found")
return False
# Create the shapekey for the driver
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
@@ -300,50 +286,41 @@ class FnSDEF:
ov.type = "SINGLE_PROP"
ov.targets[0].id = obj
ov.targets[0].data_path = "name"
if not bulk_update and use_skip: # FIXME: force disable use_skip=True for bulk_update=False on 2.8
use_skip = False
mod = obj.modifiers.get("mmd_bone_order_override")
mod = obj.modifiers.get("mmd_armature")
variables = f.driver.variables
for name in set(data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)): # add required bones for dependency graph
for name in {data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)}: # add required bones for dependency graph
var = variables.new()
var.type = "TRANSFORMS"
var.targets[0].id = mod.object
var.targets[0].bone_target = name
f.driver.use_self = True
param = (bulk_update, use_skip, use_scale)
f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param)
logger.info(f"Successfully bound SDEF to {obj.name} with bulk_update={bulk_update}, use_skip={use_skip}, use_scale={use_scale}")
f.driver.expression = f"mmd_sdef_driver(self, obj, bulk_update={bulk_update}, use_skip={use_skip}, use_scale={use_scale})"
return True
@classmethod
def unbind(cls, obj: Object) -> None:
def unbind(cls, obj):
if obj.data.shape_keys:
if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks:
logger.debug(f"Removing SDEF shape key from {obj.name}")
FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME])
for mod in obj.modifiers:
if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME:
logger.debug(f"Clearing SDEF vertex group from modifier in {obj.name}")
mod.vertex_group = ""
mod.invert_vertex_group = False
break
if cls.MASK_NAME in obj.vertex_groups:
logger.debug(f"Removing SDEF vertex group from {obj.name}")
obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME])
cls.clear_cache(obj)
@classmethod
def clear_cache(cls, obj: Optional[Object] = None, unused_only: bool = False) -> None:
def clear_cache(cls, obj=None, unused_only=False):
if unused_only:
valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj)
removed_keys = cls.g_verts.keys() - valid_keys
for key in removed_keys:
valid_keys = {_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj}
for key in cls.g_verts.keys() - valid_keys:
del cls.g_verts[key]
for key in cls.g_shapekey_data.keys() - cls.g_verts.keys():
del cls.g_shapekey_data[key]
for key in cls.g_bone_check.keys() - cls.g_verts.keys():
del cls.g_bone_check[key]
logger.debug(f"Cleared {len(removed_keys)} unused SDEF cache entries")
elif obj:
key = _hash(obj)
if key in cls.g_verts:
@@ -352,9 +329,7 @@ class FnSDEF:
del cls.g_shapekey_data[key]
if key in cls.g_bone_check:
del cls.g_bone_check[key]
logger.debug(f"Cleared SDEF cache for {obj.name}")
else:
logger.debug("Cleared all SDEF cache")
cls.g_verts = {}
cls.g_bone_check = {}
cls.g_shapekey_data = {}
+43 -66
View File
@@ -1,37 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2019 MMD Tools authors
# This file is part of MMD Tools.
from typing import Optional, Tuple, cast
from typing import Optional, Tuple, cast, List, Dict, Any, Union
import bpy
from bpy.types import (
ShaderNodeTree,
ShaderNode,
NodeGroupInput,
NodeGroupOutput,
Material
)
from ....core.logging_setup import logger
class _NodeTreeUtils:
def __init__(self, shader: ShaderNodeTree):
def __init__(self, shader: bpy.types.ShaderNodeTree):
self.shader = shader
self.nodes: bpy.types.bpy_prop_collection[ShaderNode] = shader.nodes # type: ignore
self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore[assignment]
self.links = shader.links
def _find_node(self, node_type: str) -> Optional[ShaderNode]:
def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]:
return next((n for n in self.nodes if n.bl_idname == node_type), None)
def new_node(self, idname: str, pos: Tuple[int, int]) -> ShaderNode:
node: ShaderNode = self.nodes.new(idname)
def new_node(self, idname: str, pos: Tuple[int, int]) -> bpy.types.ShaderNode:
node: bpy.types.ShaderNode = self.nodes.new(idname)
node.location = (pos[0] * 210, pos[1] * 220)
return node
def new_math_node(self, operation: str, pos: Tuple[int, int], value1: Optional[float] = None, value2: Optional[float] = None) -> ShaderNode:
def new_math_node(self, operation, pos, value1=None, value2=None):
node = self.new_node("ShaderNodeMath", pos)
node.operation = operation
if value1 is not None:
@@ -40,7 +29,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = value2
return node
def new_vector_math_node(self, operation: str, pos: Tuple[int, int], vector1: Optional[Tuple[float, float, float, float]] = None, vector2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode:
def new_vector_math_node(self, operation, pos, vector1=None, vector2=None):
node = self.new_node("ShaderNodeVectorMath", pos)
node.operation = operation
if vector1 is not None:
@@ -49,7 +38,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = vector2
return node
def new_mix_node(self, blend_type: str, pos: Tuple[int, int], fac: Optional[float] = None, color1: Optional[Tuple[float, float, float, float]] = None, color2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode:
def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None):
node = self.new_node("ShaderNodeMixRGB", pos)
node.blend_type = blend_type
if fac is not None:
@@ -61,30 +50,30 @@ class _NodeTreeUtils:
return node
SOCKET_TYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "NodeSocketFloat"}
SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"}
SOCKET_SUBTYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "FACTOR"}
SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"}
class _NodeGroupUtils(_NodeTreeUtils):
def __init__(self, shader: ShaderNodeTree):
def __init__(self, shader: bpy.types.ShaderNodeTree):
super().__init__(shader)
self.__node_input: Optional[NodeGroupInput] = None
self.__node_output: Optional[NodeGroupOutput] = None
self.__node_input: Optional[bpy.types.NodeGroupInput] = None
self.__node_output: Optional[bpy.types.NodeGroupOutput] = None
@property
def node_input(self) -> NodeGroupInput:
def node_input(self) -> bpy.types.NodeGroupInput:
if not self.__node_input:
self.__node_input = cast(NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
self.__node_input = cast("bpy.types.NodeGroupInput", self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
return self.__node_input
@property
def node_output(self) -> NodeGroupOutput:
def node_output(self) -> bpy.types.NodeGroupOutput:
if not self.__node_output:
self.__node_output = cast(NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
self.__node_output = cast("bpy.types.NodeGroupOutput", self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
return self.__node_output
def hide_nodes(self, hide_sockets: bool = True) -> None:
def hide_nodes(self, hide_sockets=True):
skip_nodes = {self.__node_input, self.__node_output}
for n in (x for x in self.nodes if x not in skip_nodes):
n.hide = True
@@ -95,22 +84,22 @@ class _NodeGroupUtils(_NodeTreeUtils):
for s in n.outputs:
s.hide = not s.is_linked
def new_input_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
def new_input_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type)
def new_output_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
def new_output_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type)
def __new_io(self, in_out: str, io_sockets: bpy.types.bpy_prop_collection, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
def __new_io(self, in_out, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None):
if io_name not in io_sockets:
idname = socket_type or (socket.bl_idname if socket else "NodeSocketFloat")
idname = socket_type or socket.bl_idname
interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname))
if idname in SOCKET_SUBTYPE_MAPPING:
interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "")
if not min_max:
if idname.endswith("Factor") or io_name.endswith("Alpha"):
interface_socket.min_value, interface_socket.max_value = 0, 1
elif idname.endswith("Float") or idname.endswith("Vector"):
elif idname.endswith(("Float", "Vector")):
interface_socket.min_value, interface_socket.max_value = -10, 10
if socket is not None:
self.links.new(io_sockets[io_name], socket)
@@ -122,18 +111,14 @@ class _NodeGroupUtils(_NodeTreeUtils):
class _MaterialMorph:
@classmethod
def update_morph_inputs(cls, material: Optional[Material], morph: Any) -> None:
"""Update material morph inputs based on morph data"""
def update_morph_inputs(cls, material, morph):
if material and material.node_tree and morph.name in material.node_tree.nodes:
logger.debug(f"Updating morph inputs for {morph.name} in {material.name}")
cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph)
cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph)
@classmethod
def setup_morph_nodes(cls, material: Material, morphs: List[Any]) -> List[ShaderNode]:
"""Set up morph nodes for a material"""
def setup_morph_nodes(cls, material, morphs):
node, nodes = None, []
logger.debug(f"Setting up {len(morphs)} morph nodes for {material.name}")
for m in morphs:
node = cls.__morph_node_add(material, m, node)
nodes.append(node)
@@ -149,25 +134,23 @@ class _MaterialMorph:
return nodes
@classmethod
def reset_morph_links(cls, node: ShaderNode) -> None:
"""Reset morph links for a node"""
logger.debug(f"Resetting morph links for {node.name}")
def reset_morph_links(cls, node):
cls.__update_morph_links(node, reset=True)
@classmethod
def __update_morph_links(cls, node: ShaderNode, reset: bool = False) -> None:
def __update_morph_links(cls, node, reset=False):
nodes, links = node.id_data.nodes, node.id_data.links
if reset:
if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links):
if any(link.from_node.name.startswith("mmd_bind") for i in node.inputs for link in i.links):
return
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
def __init_link(socket_morph, socket_shader):
if socket_shader and socket_morph.is_linked:
links.new(socket_morph.links[0].from_socket, socket_shader)
else:
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
def __init_link(socket_morph, socket_shader):
if socket_shader:
if socket_shader.is_linked:
links.new(socket_shader.links[0].from_socket, socket_morph)
@@ -192,8 +175,7 @@ class _MaterialMorph:
__init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
@classmethod
def __update_node_inputs(cls, node: ShaderNode, morph: Any) -> None:
"""Update node inputs based on morph data"""
def __update_node_inputs(cls, node, morph):
node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3]
node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3]
node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3]
@@ -211,8 +193,7 @@ class _MaterialMorph:
node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
@classmethod
def __morph_node_add(cls, material: Material, morph: Optional[Any], prev_node: Optional[ShaderNode]) -> Optional[ShaderNode]:
"""Add a morph node to a material"""
def __morph_node_add(cls, material, morph, prev_node):
nodes, links = material.node_tree.nodes, material.node_tree.links
shader = nodes.get("mmd_shader", None)
@@ -237,9 +218,8 @@ class _MaterialMorph:
return node
# connect last node to shader
if shader:
logger.debug(f"Connecting last node to shader for {material.name}")
def __soft_link(socket_out: Optional[bpy.types.NodeSocket], socket_in: Optional[bpy.types.NodeSocket]) -> None:
def __soft_link(socket_out, socket_in):
if socket_out and socket_in:
links.new(socket_out, socket_in)
@@ -261,14 +241,12 @@ class _MaterialMorph:
return shader
@classmethod
def __get_shader(cls, morph_type: str) -> ShaderNodeTree:
"""Get or create a shader node group for the specified morph type"""
def __get_shader(cls, morph_type):
group_name = "MMDMorph" + morph_type
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
logger.info(f"Creating new shader node group: {group_name}")
ng = _NodeGroupUtils(shader)
links = ng.links
@@ -279,18 +257,18 @@ class _MaterialMorph:
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
ng.new_node("NodeGroupOutput", (3, 0))
def __blend_color_add(id_name: str, pos: Tuple[int, int], tag: str = "") -> ShaderNode:
def __blend_color_add(id_name, pos, tag=""):
# MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac))
# MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2
# https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400
node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos[0] + 1, pos[1]))
links.new(node_input.outputs["Fac"], node_mix.inputs["Fac"])
ng.new_input_socket("%s1" % id_name + tag, node_mix.inputs["Color1"])
ng.new_input_socket("%s2" % id_name + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector")
ng.new_input_socket(f"{id_name}1" + tag, node_mix.inputs["Color1"])
ng.new_input_socket(f"{id_name}2" + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector")
ng.new_output_socket(id_name + tag, node_mix.outputs["Color"])
return node_mix
def __blend_tex_color(id_name: str, pos: Tuple[int, int], node_tex_rgb: ShaderNode, node_tex_a_output: bpy.types.NodeSocket) -> None:
def __blend_tex_color(id_name, pos, node_tex_rgb, node_tex_a_output):
# Tex Color = tex_rgb * tex_a + (1 - tex_a)
# : tex_rgb = TexRGB * ColorMul + ColorAdd
# : tex_a = TexA * ValueMul + ValueAdd
@@ -313,7 +291,7 @@ class _MaterialMorph:
ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor")
ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor")
def __add_sockets(id_name: str, input1: bpy.types.NodeSocket, input2: bpy.types.NodeSocket, output: bpy.types.NodeSocket, tag: str = "") -> None:
def __add_sockets(id_name, input1, input2, output, tag=""):
ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul)
ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul)
ng.new_output_socket(f"{id_name}{tag}", output)
@@ -362,5 +340,4 @@ class _MaterialMorph:
__blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2])
ng.hide_nodes()
logger.debug(f"Shader node group {group_name} created successfully")
return ng.shader
+56 -81
View File
@@ -1,9 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# Copyright 2021 MMD Tools authors
# This file is part of MMD Tools.
import itertools
import re
@@ -33,11 +29,7 @@ class MMDTranslationElementType(Enum):
class MMDDataHandlerABC(ABC):
@classmethod
@property
@abstractmethod
def type_name(cls) -> str:
pass
type_name: str
@classmethod
@abstractmethod
@@ -67,7 +59,8 @@ class MMDDataHandlerABC(ABC):
@classmethod
@abstractmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
"""Returns (name, name_j, name_e)"""
"""Return (name, name_j, name_e)"""
pass
@classmethod
def is_restorable(cls, mmd_translation_element: "MMDTranslationElement") -> bool:
@@ -75,7 +68,7 @@ class MMDDataHandlerABC(ABC):
@classmethod
def check_data_visible(cls, filter_selected: bool, filter_visible: bool, select: bool, hide: bool) -> bool:
return filter_selected and not select or filter_visible and hide
return (filter_selected and not select) or (filter_visible and hide)
@classmethod
def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str, original_value: str, index: int):
@@ -86,7 +79,7 @@ class MMDDataHandlerABC(ABC):
row.label(text="", icon="BLANK1")
return
op = row.operator("mmd_tools.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH")
op = row.operator("mmd_tools_local.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH")
op.index = index
op.prop_name = prop_name
op.restore_value = original_value
@@ -100,10 +93,7 @@ class MMDDataHandlerABC(ABC):
class MMDBoneHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.BONE.name
type_name = MMDTranslationElementType.BONE.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
@@ -114,18 +104,18 @@ class MMDBoneHandler(MMDDataHandlerABC):
cls.prop_restorable(prop_row, mmd_translation_element, "name", pose_bone.name, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", pose_bone.mmd_bone.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", pose_bone.mmd_bone.name_e, index)
row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.bone.select else "RESTRICT_SELECT_ON")
row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if pose_bone.bone.hide else "HIDE_OFF")
row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.select else "RESTRICT_SELECT_ON")
row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True)
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
pose_bone: bpy.types.PoseBone
for index, pose_bone in enumerate(armature_object.pose.bones):
if not any(c.is_visible for c in pose_bone.bone.collections):
if pose_bone.bone.hide or (pose_bone.bone.collections and not any(c.is_visible for c in pose_bone.bone.collections)):
continue
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.BONE.name
mmd_translation_element.object = armature_object
mmd_translation_element.data_path = f"pose.bones[{index}]"
@@ -140,14 +130,14 @@ class MMDBoneHandler(MMDDataHandlerABC):
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.BONE.name:
continue
pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if cls.check_data_visible(filter_selected, filter_visible, pose_bone.bone.select, pose_bone.bone.hide):
if cls.check_data_visible(filter_selected, filter_visible, pose_bone.select, pose_bone.bone.hide):
continue
if check_blank_name(mmd_translation_element.name_j, mmd_translation_element.name_e):
@@ -156,7 +146,7 @@ class MMDBoneHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
@@ -176,14 +166,11 @@ class MMDBoneHandler(MMDDataHandlerABC):
class MMDMorphHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.MORPH.name
type_name = MMDTranslationElementType.MORPH.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
row = layout.row(align=True)
row.label(text="", icon="SHAPEKEY_DATA")
prop_row = row.row()
@@ -198,7 +185,7 @@ class MMDMorphHandler(MMDDataHandlerABC):
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
root_object: bpy.types.Object = mmd_translation.id_data
mmd_root: "MMDRoot" = root_object.mmd_root
mmd_root: MMDRoot = root_object.mmd_root
for morphs_name, morphs in {
"material_morphs": mmd_root.material_morphs,
@@ -207,9 +194,9 @@ class MMDMorphHandler(MMDDataHandlerABC):
"vertex_morphs": mmd_root.vertex_morphs,
"group_morphs": mmd_root.group_morphs,
}.items():
morph: "_MorphBase"
morph: _MorphBase
for index, morph in enumerate(morphs):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.MORPH.name
mmd_translation_element.object = root_object
mmd_translation_element.data_path = f"mmd_root.{morphs_name}[{index}]"
@@ -228,24 +215,24 @@ class MMDMorphHandler(MMDDataHandlerABC):
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.MORPH.name:
continue
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if check_blank_name(morph.name, morph.name_e):
continue
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if name is not None:
morph.name = name
if name_e is not None:
@@ -253,15 +240,12 @@ class MMDMorphHandler(MMDDataHandlerABC):
@classmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
morph: _MorphBase = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
return (morph.name, "", morph.name_e)
class MMDMaterialHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.MATERIAL.name
type_name = MMDTranslationElementType.MATERIAL.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
@@ -274,7 +258,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", material.mmd_material.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", material.mmd_material.name_e, index)
row.prop(mesh_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mesh_object.select_get() else "RESTRICT_SELECT_ON")
row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mesh_object.hide_get() else "HIDE_OFF")
row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True)
MATERIAL_DATA_PATH_EXTRACT = re.compile(r"data\.materials\[(?P<index>\d*)\]")
@@ -293,7 +277,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
if not hasattr(material, "mmd_material"):
continue
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.MATERIAL.name
mmd_translation_element.object = mesh_object
mmd_translation_element.data_path = f"data.materials[{index}]"
@@ -314,7 +298,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.MATERIAL.name:
continue
@@ -330,7 +314,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
@@ -350,10 +334,7 @@ class MMDMaterialHandler(MMDDataHandlerABC):
class MMDDisplayHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.DISPLAY.name
type_name = MMDTranslationElementType.DISPLAY.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
@@ -366,7 +347,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
cls.prop_disabled(prop_row, mmd_translation_element, "name")
cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
row.prop(mmd_translation_element.object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mmd_translation_element.object.select_get() else "RESTRICT_SELECT_ON")
row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mmd_translation_element.object.hide_get() else "HIDE_OFF")
row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True)
DISPLAY_DATA_PATH_EXTRACT = re.compile(r"data\.collections\[(?P<index>\d*)\]")
@@ -375,7 +356,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
bone_collection: bpy.types.BoneCollection
for index, bone_collection in enumerate(armature_object.data.collections):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.DISPLAY.name
mmd_translation_element.object = armature_object
mmd_translation_element.data_path = f"data.collections[{index}]"
@@ -396,7 +377,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.DISPLAY.name:
continue
@@ -412,7 +393,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
@@ -428,10 +409,7 @@ class MMDDisplayHandler(MMDDataHandlerABC):
class MMDPhysicsHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.PHYSICS.name
type_name = MMDTranslationElementType.PHYSICS.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
@@ -451,7 +429,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", mmd_object.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", mmd_object.name_e, index)
row.prop(obj, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if obj.select_get() else "RESTRICT_SELECT_ON")
row.prop(obj, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if obj.hide_get() else "HIDE_OFF")
row.prop(obj, "hide", text="", emboss=False, icon_only=True)
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
@@ -460,7 +438,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
obj: bpy.types.Object
for obj in model.rigidBodies():
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
mmd_translation_element.object = obj
mmd_translation_element.data_path = "mmd_rigid"
@@ -470,7 +448,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
obj: bpy.types.Object
for obj in model.joints():
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
mmd_translation_element.object = obj
mmd_translation_element.data_path = "mmd_joint"
@@ -484,7 +462,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.PHYSICS.name:
continue
@@ -504,7 +482,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
@@ -536,10 +514,7 @@ class MMDPhysicsHandler(MMDDataHandlerABC):
class MMDInfoHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.INFO.name
type_name = MMDTranslationElementType.INFO.name
TYPE_TO_ICONS = {
"EMPTY": "EMPTY_DATA",
@@ -557,7 +532,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
cls.prop_disabled(prop_row, mmd_translation_element, "name")
cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
row.prop(info_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if info_object.select_get() else "RESTRICT_SELECT_ON")
row.prop(info_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if info_object.hide_get() else "HIDE_OFF")
row.prop(info_object, "hide", text="", emboss=False, icon_only=True)
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
@@ -568,7 +543,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
info_objects.append(armature_object)
for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.INFO.name
mmd_translation_element.object = info_object
mmd_translation_element.data_path = ""
@@ -582,7 +557,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
mmd_translation_element: MMDTranslationElement
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.INFO.name:
continue
@@ -597,7 +572,7 @@ class MMDInfoHandler(MMDDataHandlerABC):
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
@@ -627,10 +602,10 @@ MMD_DATA_TYPE_TO_HANDLERS: Dict[str, MMDDataHandlerABC] = {h.type_name: h for h
class FnTranslations:
@staticmethod
def apply_translations(root_object: bpy.types.Object):
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
mmd_translation_element_index: "MMDTranslationElementIndex"
mmd_translation: MMDTranslation = root_object.mmd_root.translation
mmd_translation_element_index: MMDTranslationElementIndex
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
name, name_j, name_e = handler.get_names(mmd_translation_element)
handler.set_names(
@@ -642,7 +617,7 @@ class FnTranslations:
@staticmethod
def execute_translation_batch(root_object: bpy.types.Object) -> Tuple[Dict[str, str], Optional[bpy.types.Text]]:
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
mmd_translation: MMDTranslation = root_object.mmd_root.translation
batch_operation_script = mmd_translation.batch_operation_script
if not batch_operation_script:
return ({}, None)
@@ -657,9 +632,9 @@ class FnTranslations:
batch_operation_script_ast = compile(mmd_translation.batch_operation_script, "<string>", "eval")
batch_operation_target: str = mmd_translation.batch_operation_target
mmd_translation_element_index: "MMDTranslationElementIndex"
mmd_translation_element_index: MMDTranslationElementIndex
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
@@ -684,7 +659,7 @@ class FnTranslations:
"org_name_j": org_name_j,
"org_name_e": org_name_e,
},
)
),
)
if batch_operation_target == "BLENDER":
@@ -701,8 +676,8 @@ class FnTranslations:
if mmd_translation.filtered_translation_element_indices_active_index < 0:
return
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index]
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
mmd_translation_element_index: MMDTranslationElementIndex = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index]
mmd_translation_element: MMDTranslationElement = mmd_translation.translation_elements[mmd_translation_element_index.value]
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].update_index(mmd_translation_element)
@@ -724,7 +699,7 @@ class FnTranslations:
filter_visible: bool = mmd_translation.filter_visible
def check_blank_name(name_j: str, name_e: str) -> bool:
return filter_japanese_blank and name_j or filter_english_blank and name_e
return (filter_japanese_blank and name_j) or (filter_english_blank and name_e)
for handler in MMD_DATA_HANDLERS:
if handler.type_name in mmd_translation.filter_types:
+1 -1
View File
@@ -3,4 +3,4 @@
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
+14 -14
View File
@@ -5,7 +5,7 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
from .....core.logging_setup import logger
import math
import os
from typing import Union
@@ -261,7 +261,7 @@ class VMDImporter:
def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False):
self.__vmdFile = vmd.File()
self.__vmdFile.load(filepath=filepath)
logging.debug(str(self.__vmdFile.header))
logger.debug(str(self.__vmdFile.header))
self.__scale = scale
self.__convert_mmd_camera = convert_mmd_camera
self.__convert_mmd_lamp = convert_mmd_lamp
@@ -381,7 +381,7 @@ class VMDImporter:
def __assignToArmature(self, armObj, action_name=None):
boneAnim = self.__vmdFile.boneAnimation
logging.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name)
logger.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name)
if len(boneAnim) < 1:
return
@@ -412,9 +412,9 @@ class VMDImporter:
continue
bone = pose_bones.get(name, None)
if bone is None:
logging.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames))
logger.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames))
continue
logging.info("(bone) frames:%5d name: %s", len(keyFrames), name)
logger.info("(bone) frames:%5d name: %s", len(keyFrames), name)
assert bone_name_table.get(bone.name, name) == name
bone_name_table[bone.name] = name
@@ -480,9 +480,9 @@ class VMDImporter:
# property animation
propertyAnim = self.__vmdFile.propertyAnimation
if len(propertyAnim) > 0:
logging.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name)
logger.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name)
for keyFrame in propertyAnim:
logging.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states)
logger.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states)
frame = keyFrame.frame_number + self.__frame_margin
for ikName, enable in keyFrame.ik_states:
bone = pose_bones.get(ikName, None)
@@ -516,7 +516,7 @@ class VMDImporter:
def __assignToMesh(self, meshObj, action_name=None):
shapeKeyAnim = self.__vmdFile.shapeKeyAnimation
logging.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name)
logger.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name)
if len(shapeKeyAnim) < 1:
return
@@ -530,9 +530,9 @@ class VMDImporter:
for name, keyFrames in shapeKeyAnim.items():
if name not in shapeKeyDict:
logging.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames))
logger.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames))
continue
logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name)
logger.info("(mesh) frames:%5d name: %s", len(keyFrames), name)
shapeKey = shapeKeyDict[name]
channelbag = self.__get_channelbag(action, meshObj.data.shape_keys)
fcurve = channelbag.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name)
@@ -549,14 +549,14 @@ class VMDImporter:
def __assignToRoot(self, rootObj, action_name=None):
propertyAnim = self.__vmdFile.propertyAnimation
logging.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name)
logger.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name)
if len(propertyAnim) < 1:
return
action_name = action_name or rootObj.name
action = bpy.data.actions.new(name=action_name)
logging.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim])
logger.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim])
for keyFrame in propertyAnim:
self.__keyframe_insert(action, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible), rootObj)
@@ -579,7 +579,7 @@ class VMDImporter:
cameraObj = mmdCameraInstance.camera()
cameraAnim = self.__vmdFile.cameraAnimation
logging.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name)
logger.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name)
if len(cameraAnim) < 1:
return
@@ -650,7 +650,7 @@ class VMDImporter:
lampObj = mmdLampInstance.lamp()
lampAnim = self.__vmdFile.lampAnimation
logging.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name)
logger.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name)
if len(lampAnim) < 1:
return