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:
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+97
-107
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user