Merge pull request #159 from Yusarina/mmd-tools-improvements

Mmd tools improvements
This commit is contained in:
Onan Chew
2025-04-22 23:11:09 -04:00
committed by GitHub
36 changed files with 2263 additions and 1487 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
schema_version = "1.0.0"
id = "avatar_toolkit"
version = "0.2.1"
version = "0.3.0"
name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo"
+27 -2
View File
@@ -25,12 +25,18 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
non_standard_messages: List[str] = []
scale_messages: List[str] = []
# Check if this is a PMX model
is_pmx_model = False
if armature and hasattr(armature, 'mmd_type') or (hasattr(armature, 'parent') and armature.parent and hasattr(armature.parent, 'mmd_type')):
is_pmx_model = True
logger.debug("Detected PMX model, using specialized validation")
if validation_mode == 'NONE':
logger.debug("Validation mode is NONE, skipping validation")
if detailed_messages:
return True, [], False, [], [], []
return True, [t("Validation.mode.none")], False, [], [], []
else:
return True, [], False
return True, [t("Validation.mode.none")], False
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
logger.warning("Basic armature check failed")
@@ -125,6 +131,21 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
non_standard_messages.append(t("Armature.validation.standardize_note.line2"))
non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
# Special handling for PMX models
if is_pmx_model:
logger.info("PMX model detected, applying specialized validation")
# For PMX models, we'll be more lenient with validation
# and provide specific guidance for these models
if not messages:
messages = [t("Armature.validation.pmx_model_detected")]
# Add PMX-specific messages
if validation_mode == 'STRICT':
messages.append(t("Armature.validation.pmx_model_strict"))
messages.append(t("Armature.validation.pmx_model_standardize"))
else:
messages.append(t("Armature.validation.pmx_model_basic"))
# Combine messages in correct order
messages.extend(non_standard_messages)
@@ -149,6 +170,10 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
else:
return True, messages, True
# Ensure messages has at least one element
if not messages:
messages = [t("Armature.validation.unknown_format")]
logger.info(f"Armature validation complete. Valid: {is_valid}")
if detailed_messages:
return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages
-66
View File
@@ -1,66 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file is part of MMD Tools.
import bpy
from ..bpyutils import FnContext, Props
class MMDLamp:
def __init__(self, obj):
if MMDLamp.isLamp(obj):
obj = obj.parent
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
self.__emptyObj = obj
else:
raise ValueError("%s is not MMDLamp" % str(obj))
@staticmethod
def isLamp(obj):
return obj and obj.type in {"LIGHT", "LAMP"}
@staticmethod
def isMMDLamp(obj):
if MMDLamp.isLamp(obj):
obj = obj.parent
return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
@staticmethod
def convertToMMDLamp(lampObj, scale=1.0):
if MMDLamp.isMMDLamp(lampObj):
return MMDLamp(lampObj)
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)
setattr(empty, Props.empty_display_size, 0.4)
empty.scale = [10 * scale] * 3
empty.mmd_type = "LIGHT"
empty.location = (0, 0, 11 * scale)
lampObj.parent = empty
lampObj.data.color = (0.602, 0.602, 0.602)
lampObj.location = (0.5, -0.5, 1.0)
lampObj.rotation_mode = "XYZ"
lampObj.rotation_euler = (0, 0, 0)
lampObj.lock_rotation = (True, True, True)
constraint = lampObj.constraints.new(type="TRACK_TO")
constraint.name = "mmd_lamp_track"
constraint.target = empty
constraint.track_axis = "TRACK_NEGATIVE_Z"
constraint.up_axis = "UP_Y"
return MMDLamp(empty)
def object(self):
return self.__emptyObj
def lamp(self):
for i in self.__emptyObj.children:
if MMDLamp.isLamp(i):
return i
raise KeyError
+82 -67
View File
@@ -6,9 +6,13 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import contextlib
from typing import Generator, List, Optional, TypeVar
from typing import Generator, List, Optional, TypeVar, Any, Set, Tuple, Dict, Union
import bpy
from bpy.types import Object, Context, ID, Key, ShapeKey, FCurve, LayerCollection, Collection
from bpy.types import AddonPreferences, Addon, WindowManager, Area, Region, Window
from ..logging_setup import logger
class Props: # For API changes of only name changed properties
@@ -20,7 +24,7 @@ class Props: # For API changes of only name changed properties
class __EditMode:
def __init__(self, obj):
def __init__(self, obj: Object):
if not isinstance(obj, bpy.types.Object):
raise ValueError
self.__prevMode = obj.mode
@@ -30,10 +34,10 @@ class __EditMode:
if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
def __enter__(self):
def __enter__(self) -> Any:
return self.__obj.data
def __exit__(self, type, value, traceback):
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
if self.__prevMode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update edited data
bpy.ops.object.mode_set(mode=self.__prevMode)
@@ -41,17 +45,18 @@ class __EditMode:
class __SelectObjects:
def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None):
def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None):
if not isinstance(active_object, bpy.types.Object):
raise ValueError
try:
bpy.ops.object.mode_set(mode="OBJECT")
except Exception:
logger.debug("Failed to set object mode")
pass
contenxt = FnContext.ensure_context()
context = FnContext.ensure_context()
for i in contenxt.selected_objects:
for i in context.selected_objects:
i.select_set(False)
self.__active_object = active_object
@@ -60,23 +65,23 @@ class __SelectObjects:
self.__hides: List[bool] = []
for i in self.__selected_objects:
self.__hides.append(i.hide_get())
FnContext.select_object(contenxt, i)
FnContext.set_active_object(contenxt, active_object)
FnContext.select_object(context, i)
FnContext.set_active_object(context, active_object)
def __enter__(self) -> bpy.types.Object:
def __enter__(self) -> Object:
return self.__active_object
def __exit__(self, type, value, traceback):
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
for i, j in zip(self.__selected_objects, self.__hides):
i.hide_set(j)
def setParent(obj, parent):
def setParent(obj: Object, parent: Object) -> None:
with select_object(parent, objects=[parent, obj]):
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
def setParentToBone(obj, parent, bone_name):
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
with select_object(parent, objects=[parent, obj]):
bpy.ops.object.mode_set(mode="POSE")
parent.data.bones.active = parent.data.bones[bone_name]
@@ -84,7 +89,7 @@ def setParentToBone(obj, parent, bone_name):
bpy.ops.object.mode_set(mode="OBJECT")
def edit_object(obj):
def edit_object(obj: Object) -> __EditMode:
"""Set the object interaction mode to 'EDIT'
It is recommended to use 'edit_object' with 'with' statement like the following code.
@@ -95,7 +100,7 @@ def edit_object(obj):
return __EditMode(obj)
def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None):
def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __SelectObjects:
"""Select objects.
It is recommended to use 'select_object' with 'with' statement like the following code.
@@ -108,20 +113,23 @@ def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object
return __SelectObjects(obj, objects)
def duplicateObject(obj, total_len):
def duplicateObject(obj: Object, total_len: int) -> List[Object]:
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
def createObject(name="Object", object_data=None, target_scene=None):
def createObject(name: str = "Object", object_data: Optional[ID] = None, target_scene: Optional[bpy.types.Scene] = None) -> Object:
context = FnContext.ensure_context(target_scene)
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data))
def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object:
import bmesh
if target_object is None:
target_object = createObject(name="Sphere")
logger.debug(f"Created new sphere object: {target_object.name}")
else:
logger.debug(f"Using existing object for sphere: {target_object.name}")
mesh = target_object.data
bm = bmesh.new()
@@ -138,12 +146,15 @@ def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
return target_object
def makeBox(size=(1, 1, 1), target_object=None):
def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object:
import bmesh
from mathutils import Matrix
if target_object is None:
target_object = createObject(name="Box")
logger.debug(f"Created new box object: {target_object.name}")
else:
logger.debug(f"Using existing object for box: {target_object.name}")
mesh = target_object.data
bm = bmesh.new()
@@ -159,13 +170,16 @@ def makeBox(size=(1, 1, 1), target_object=None):
return target_object
def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None):
def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object:
import math
import bmesh
if target_object is None:
target_object = createObject(name="Capsule")
logger.debug(f"Created new capsule object: {target_object.name}")
else:
logger.debug(f"Using existing object for capsule: {target_object.name}")
height = max(height, 1e-3)
mesh = target_object.data
@@ -224,10 +238,10 @@ def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=N
class TransformConstraintOp:
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
__MIN_MAX_MAP: Dict[Union[str, Tuple[str, str]], Union[str, Tuple[str, ...]]] = {"ROTATION": "_rot", "SCALE": "_scale"}
@staticmethod
def create(constraints, name, map_type):
def create(constraints: bpy.types.ObjectConstraints, name: str, map_type: str) -> bpy.types.TransformConstraint:
c = constraints.get(name, None)
if c and c.type != "TRANSFORM":
constraints.remove(c)
@@ -245,7 +259,7 @@ class TransformConstraintOp:
return c
@classmethod
def min_max_attributes(cls, map_type, name_id=""):
def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]:
key = (map_type, name_id)
ret = cls.__MIN_MAX_MAP.get(key, None)
if ret is None:
@@ -255,7 +269,7 @@ class TransformConstraintOp:
return ret
@classmethod
def update_min_max(cls, constraint, value, influence=1):
def update_min_max(cls, constraint: bpy.types.TransformConstraint, value: float, influence: Optional[float] = 1) -> None:
c = constraint
if not c or c.type != "TRANSFORM":
return
@@ -279,14 +293,14 @@ class FnObject:
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey):
def mesh_remove_shape_key(mesh_object: Object, shape_key: ShapeKey) -> None:
assert isinstance(mesh_object.data, bpy.types.Mesh)
key: bpy.types.Key = shape_key.id_data
key: Key = shape_key.id_data
assert key == mesh_object.data.shape_keys
if mesh_object.animation_data is not None:
fc_curve: bpy.types.FCurve
fc_curve: FCurve
for fc_curve in mesh_object.animation_data.drivers:
if not fc_curve.data_path.startswith(shape_key.path_from_id()):
continue
@@ -310,35 +324,35 @@ class FnContext:
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
def ensure_context(context: Optional[Context] = None) -> Context:
return context or bpy.context
@staticmethod
def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
def get_active_object(context: Context) -> Optional[Object]:
return context.active_object
@staticmethod
def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def set_active_object(context: Context, obj: Object) -> Object:
context.view_layer.objects.active = obj
return obj
@staticmethod
def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def set_active_and_select_single_object(context: Context, obj: Object) -> Object:
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
@staticmethod
def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects:
def get_scene_objects(context: Context) -> bpy.types.SceneObjects:
return context.scene.objects
@staticmethod
def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def ensure_selectable(context: Context, obj: Object) -> Object:
obj.hide_viewport = False
obj.hide_select = False
obj.hide_set(False)
if obj not in context.selectable_objects:
def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool:
def __layer_check(layer_collection: LayerCollection) -> bool:
for lc in layer_collection.children:
if __layer_check(lc):
lc.hide_viewport = False
@@ -360,44 +374,44 @@ class FnContext:
return obj
@staticmethod
def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def select_object(context: Context, obj: Object) -> Object:
FnContext.ensure_selectable(context, obj).select_set(True)
return obj
@staticmethod
def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]:
def select_objects(context: Context, *objects: Object) -> List[Object]:
return [FnContext.select_object(context, obj) for obj in objects]
@staticmethod
def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def select_single_object(context: Context, obj: Object) -> Object:
for i in context.selected_objects:
if i != obj:
i.select_set(False)
return FnContext.select_object(context, obj)
@staticmethod
def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
def link_object(context: Context, obj: Object) -> Object:
context.collection.objects.link(obj)
return obj
@staticmethod
def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object:
def new_and_link_object(context: Context, name: str, object_data: Optional[ID]) -> Object:
return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
@staticmethod
def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]:
def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[Object]:
"""
Duplicate object.
This function duplicates the given object and returns a list of duplicated objects.
Args:
context (bpy.types.Context): The context in which the duplication is performed.
object_to_duplicate (bpy.types.Object): The object to be duplicated.
context (Context): The context in which the duplication is performed.
object_to_duplicate (Object): The object to be duplicated.
target_count (int): The desired count of duplicated objects.
Returns:
List[bpy.types.Object]: A list of duplicated objects.
List[Object]: A list of duplicated objects.
Raises:
AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated.
@@ -421,27 +435,28 @@ class FnContext:
last_selected_objects[i].select_set(True)
last_selected_objects = context.selected_objects
assert len(result_objects) == target_count
logger.debug(f"Duplicated object {object_to_duplicate.name} to create {target_count} objects")
return result_objects
@staticmethod
def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]:
def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[LayerCollection]:
"""
Finds the layer collection that contains the given target_object in the user's collections.
Args:
context (bpy.types.Context): The Blender context.
target_object (bpy.types.Object): The target object to find the layer collection for.
context (Context): The Blender context.
target_object (Object): The target object to find the layer collection for.
Returns:
Optional[bpy.types.LayerCollection]: The layer collection that contains the target_object, or None if not found.
Optional[LayerCollection]: The layer collection that contains the target_object, or None if not found.
"""
scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection
scene_layer_collection: LayerCollection = context.view_layer.layer_collection
def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]:
def find_layer_collection_by_name(layer_collection: LayerCollection, name: str) -> Optional[LayerCollection]:
if layer_collection.name == name:
return layer_collection
child_layer_collection: bpy.types.LayerCollection
child_layer_collection: LayerCollection
for child_layer_collection in layer_collection.children:
found = find_layer_collection_by_name(child_layer_collection, name)
if found is not None:
@@ -449,7 +464,7 @@ class FnContext:
return None
user_collection: bpy.types.Collection
user_collection: Collection
for user_collection in target_object.users_collection:
found = find_layer_collection_by_name(scene_layer_collection, user_collection.name)
if found is not None:
@@ -459,7 +474,7 @@ class FnContext:
@staticmethod
@contextlib.contextmanager
def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]:
def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]:
"""
Context manager to temporarily override the active_layer_collection that contains the target object.
@@ -467,11 +482,11 @@ class FnContext:
It ensures that the original active_layer_collection is restored after the context is exited.
Args:
context (bpy.types.Context): The context in which the active_layer_collection will be overridden.
target_object (bpy.types.Object): The target object whose layer collection will be set as the active_layer_collection.
context (Context): The context in which the active_layer_collection will be overridden.
target_object (Object): The target object whose layer collection will be set as the active_layer_collection.
Yields:
bpy.types.Context: The modified context with the active_layer_collection overridden.
Context: The modified context with the active_layer_collection overridden.
Example:
with FnContext.temp_override_active_layer_collection(context, target_object):
@@ -492,24 +507,24 @@ class FnContext:
context.view_layer.active_layer_collection = original_layer_collection
@staticmethod
def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]:
addon: bpy.types.Addon = context.preferences.addons.get(__package__, None)
def __get_addon_preferences(context: Context) -> Optional[AddonPreferences]:
addon: Addon = context.preferences.addons.get(__package__, None)
return addon.preferences if addon else None
@staticmethod
def get_addon_preferences_attribute(context: bpy.types.Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
def get_addon_preferences_attribute(context: Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value)
@staticmethod
def temp_override_objects(
context: bpy.types.Context,
window: Optional[bpy.types.Window] = None,
area: Optional[bpy.types.Area] = None,
region: Optional[bpy.types.Region] = None,
active_object: Optional[bpy.types.Object] = None,
selected_objects: Optional[List[bpy.types.Object]] = None,
**keywords,
) -> Generator[bpy.types.Context, None, None]:
context: Context,
window: Optional[Window] = None,
area: Optional[Area] = None,
region: Optional[Region] = None,
active_object: Optional[Object] = None,
selected_objects: Optional[List[Object]] = None,
**keywords: Any,
) -> Generator[Context, None, None]:
if active_object is not None:
keywords["active_object"] = active_object
keywords["object"] = active_object
+169 -78
View File
@@ -6,29 +6,32 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math
from typing import TYPE_CHECKING, Iterable, Optional, Set
from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Dict, Tuple, Any, Union, cast
import bpy
from mathutils import Vector
from bpy.types import Object, EditBone, PoseBone, Constraint, Armature, BoneCollection
from .. import bpyutils
from ..bpyutils import TransformConstraintOp
from ..utils import ItemOp
from ....core.logging_setup import logger
if TYPE_CHECKING:
from ..properties.root import MMDRoot, MMDDisplayItemFrame
from ..properties.pose_bone import MMDBone
def remove_constraint(constraints, name):
def remove_constraint(constraints: Any, name: str) -> bool:
"""Remove a constraint by name if it exists"""
c = constraints.get(name, None)
if c:
constraints.remove(c)
return True
return False
def remove_edit_bones(edit_bones, bone_names):
def remove_edit_bones(edit_bones: bpy.types.ArmatureEditBones, bone_names: List[str]) -> None:
"""Remove edit bones by name"""
for name in bone_names:
b = edit_bones.get(name, None)
if b:
@@ -45,33 +48,39 @@ SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NA
class FnBone:
AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
AUTO_LOCAL_AXIS_ARMS: Tuple[str, ...] = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS: Tuple[str, ...] = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: Tuple[str, ...] = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
def __init__(self):
def __init__(self) -> None:
raise NotImplementedError("This class cannot be instantiated.")
@staticmethod
def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]:
def find_pose_bone_by_bone_id(armature_object: Object, bone_id: int) -> Optional[PoseBone]:
"""Find a pose bone by its bone ID"""
for bone in armature_object.pose.bones:
if bone.mmd_bone.bone_id != bone_id:
continue
return bone
logger.debug(f"Bone with ID {bone_id} not found in armature {armature_object.name}")
return None
@staticmethod
def __new_bone_id(armature_object: bpy.types.Object) -> int:
def __new_bone_id(armature_object: Object) -> int:
"""Generate a new unique bone ID"""
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
@staticmethod
def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int:
def get_or_assign_bone_id(pose_bone: PoseBone) -> int:
"""Get the bone ID or assign a new one if not set"""
if pose_bone.mmd_bone.bone_id < 0:
pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data)
logger.debug(f"Assigned new bone ID {pose_bone.mmd_bone.bone_id} to bone {pose_bone.name}")
return pose_bone.mmd_bone.bone_id
@staticmethod
def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]:
def __get_selected_pose_bones(armature_object: Object) -> Iterable[PoseBone]:
"""Get selected pose bones from the armature"""
if armature_object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
bpy.ops.object.mode_set(mode="EDIT") # back to edit mode
@@ -80,9 +89,11 @@ class FnBone:
return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone)
@staticmethod
def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True):
def load_bone_fixed_axis(armature_object: Object, enable: bool = True) -> None:
"""Load fixed axis settings for selected bones"""
logger.debug(f"Loading bone fixed axis (enable={enable}) for {armature_object.name}")
for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone: MMDBone = b.mmd_bone
mmd_bone = b.mmd_bone
mmd_bone.enabled_fixed_axis = enable
lock_rotation = b.lock_rotation[:]
if enable:
@@ -97,53 +108,66 @@ class FnBone:
b.lock_location = b.lock_scale = (False, False, False)
@staticmethod
def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object:
armature: bpy.types.Armature = armature_object.data
def setup_special_bone_collections(armature_object: Object) -> Object:
"""Set up special bone collections for MMD"""
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
if bone_collection_name in bone_collections:
continue
bone_collection = bone_collections.new(bone_collection_name)
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
logger.debug(f"Created special bone collection: {bone_collection_name}")
return armature_object
@staticmethod
def __is_mmd_tools_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
def __is_mmd_tools_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is an MMD Tools collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
@staticmethod
def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
def __is_special_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is a special MMD collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
@staticmethod
def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool):
def __set_bone_collection_to_special(bone_collection: BoneCollection, is_visible: bool) -> None:
"""Mark a bone collection as special"""
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
bone_collection.is_visible = is_visible
@staticmethod
def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
def __is_normal_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is a normal MMD collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
@staticmethod
def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection):
def __set_bone_collection_to_normal(bone_collection: BoneCollection) -> None:
"""Mark a bone collection as normal"""
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
@staticmethod
def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone:
def __set_edit_bone_to_special(edit_bone: EditBone, bone_collection_name: str) -> EditBone:
"""Set an edit bone to a special collection"""
edit_bone.id_data.collections[bone_collection_name].assign(edit_bone)
edit_bone.use_deform = False
return edit_bone
@staticmethod
def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
def set_edit_bone_to_dummy(edit_bone: EditBone) -> EditBone:
"""Set an edit bone as a dummy bone"""
logger.debug(f"Setting bone {edit_bone.name} as dummy bone")
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
@staticmethod
def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
def set_edit_bone_to_shadow(edit_bone: EditBone) -> EditBone:
"""Set an edit bone as a shadow bone"""
logger.debug(f"Setting bone {edit_bone.name} as shadow bone")
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
@staticmethod
def __unassign_mmd_tools_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
def __unassign_mmd_tools_bone_collections(edit_bone: EditBone) -> EditBone:
"""Unassign an edit bone from all MMD Tools collections"""
for bone_collection in edit_bone.collections:
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
continue
@@ -151,18 +175,24 @@ class FnBone:
return edit_bone
@staticmethod
def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object):
armature: bpy.types.Armature = armature_object.data
def sync_bone_collections_from_display_item_frames(armature_object: Object) -> None:
"""Synchronize bone collections from display item frames"""
logger.info(f"Syncing bone collections from display item frames for {armature_object.name}")
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections
from .model import FnModel
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root
root_object = FnModel.find_root_object(armature_object)
if not root_object:
logger.error(f"No root object found for armature {armature_object.name}")
return
mmd_root = root_object.mmd_root
bones = armature.bones
used_groups = set()
unassigned_bone_names = {b.name for b in bones}
used_groups: Set[str] = set()
unassigned_bone_names: Set[str] = {b.name for b in bones}
for frame in mmd_root.display_item_frames:
for item in frame.data:
@@ -174,6 +204,7 @@ class FnBone:
if bone_collection is None:
bone_collection = bone_collections.new(name=group_name)
FnBone.__set_bone_collection_to_normal(bone_collection)
logger.debug(f"Created new bone collection: {group_name}")
bone_collection.assign(bones[item.name])
for name in unassigned_bone_names:
@@ -192,32 +223,40 @@ class FnBone:
continue
if not FnBone.__is_normal_bone_collection(bone_collection):
continue
logger.debug(f"Removing unused bone collection: {bone_collection.name}")
bone_collections.remove(bone_collection)
@staticmethod
def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object):
armature: bpy.types.Armature = armature_object.data
bone_collections: bpy.types.BoneCollections = armature.collections
def sync_display_item_frames_from_bone_collections(armature_object: Object) -> None:
"""Synchronize display item frames from bone collections"""
logger.info(f"Syncing display item frames from bone collections for {armature_object.name}")
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections
from .model import FnModel
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root
root_object = FnModel.find_root_object(armature_object)
if not root_object:
logger.error(f"No root object found for armature {armature_object.name}")
return
mmd_root = root_object.mmd_root
display_item_frames = mmd_root.display_item_frames
used_frame_index: Set[int] = set()
bone_collection: bpy.types.BoneCollection
bone_collection: BoneCollection
for bone_collection in bone_collections:
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
continue
bone_collection_name = bone_collection.name
display_item_frame: Optional[MMDDisplayItemFrame] = display_item_frames.get(bone_collection_name)
display_item_frame = display_item_frames.get(bone_collection_name)
if display_item_frame is None:
display_item_frame = display_item_frames.add()
display_item_frame.name = bone_collection_name
display_item_frame.name_e = bone_collection_name
logger.debug(f"Created new display item frame: {bone_collection_name}")
used_frame_index.add(display_item_frames.find(bone_collection_name))
ItemOp.resize(display_item_frame.data, len(bone_collection.bones))
@@ -232,23 +271,27 @@ class FnBone:
if display_item_frame.is_special:
if display_item_frame.name != "表情":
display_item_frame.data.clear()
logger.debug(f"Cleared special display item frame: {display_item_frame.name}")
else:
logger.debug(f"Removing unused display item frame: {display_item_frames[i].name}")
display_item_frames.remove(i)
mmd_root.active_display_item_frame = 0
@staticmethod
def apply_bone_fixed_axis(armature_object: bpy.types.Object):
bone_map = {}
def apply_bone_fixed_axis(armature_object: Object) -> None:
"""Apply fixed axis to bones"""
logger.info(f"Applying bone fixed axis for {armature_object.name}")
bone_map: Dict[str, Tuple[Vector, bool, bool]] = {}
for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
continue
mmd_bone: MMDBone = b.mmd_bone
mmd_bone = b.mmd_bone
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
force_align = True
with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone
bone: EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
@@ -279,6 +322,7 @@ class FnBone:
else:
bone_map[bone.name] = (True, True, True)
bone.select = True
logger.debug(f"Applied fixed axis to bone: {bone.name}")
for bone_name, locks in bone_map.items():
b = armature_object.pose.bones[bone_name]
@@ -286,9 +330,11 @@ class FnBone:
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
@staticmethod
def load_bone_local_axes(armature_object: bpy.types.Object, enable=True):
def load_bone_local_axes(armature_object: Object, enable: bool = True) -> None:
"""Load local axes for selected bones"""
logger.debug(f"Loading bone local axes (enable={enable}) for {armature_object.name}")
for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone: MMDBone = b.mmd_bone
mmd_bone = b.mmd_bone
mmd_bone.enabled_local_axes = enable
if enable:
axes = b.bone.matrix_local.to_3x3().transposed()
@@ -296,16 +342,18 @@ class FnBone:
mmd_bone.local_axis_z = axes[2].xzy
@staticmethod
def apply_bone_local_axes(armature_object: bpy.types.Object):
bone_map = {}
def apply_bone_local_axes(armature_object: Object) -> None:
"""Apply local axes to bones"""
logger.info(f"Applying bone local axes for {armature_object.name}")
bone_map: Dict[str, Tuple[Vector, Vector]] = {}
for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
continue
mmd_bone: MMDBone = b.mmd_bone
mmd_bone = b.mmd_bone
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone
bone: EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
@@ -313,15 +361,18 @@ class FnBone:
local_axis_x, local_axis_z = bone_map[bone.name]
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
bone.select = True
logger.debug(f"Applied local axes to bone: {bone.name}")
@staticmethod
def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z):
def update_bone_roll(edit_bone: EditBone, mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> None:
"""Update bone roll based on local axes"""
axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z)
idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
@staticmethod
def get_axes(mmd_local_axis_x, mmd_local_axis_z):
def get_axes(mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> Tuple[Vector, Vector, Vector]:
"""Get axes from local axis vectors"""
x_axis = Vector(mmd_local_axis_x).normalized().xzy
z_axis = Vector(mmd_local_axis_z).normalized().xzy
y_axis = z_axis.cross(x_axis).normalized()
@@ -329,21 +380,25 @@ class FnBone:
return (x_axis, y_axis, z_axis)
@staticmethod
def apply_auto_bone_roll(armature):
bone_names = []
def apply_auto_bone_roll(armature: Object) -> None:
"""Apply automatic bone roll to appropriate bones"""
logger.info(f"Applying auto bone roll for {armature.name}")
bone_names: List[str] = []
for b in armature.pose.bones:
if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j):
bone_names.append(b.name)
with bpyutils.edit_object(armature) as data:
bone: bpy.types.EditBone
bone: EditBone
for bone in data.edit_bones:
if bone.name not in bone_names:
continue
FnBone.update_auto_bone_roll(bone)
bone.select = True
logger.debug(f"Applied auto bone roll to bone: {bone.name}")
@staticmethod
def update_auto_bone_roll(edit_bone):
def update_auto_bone_roll(edit_bone: EditBone) -> None:
"""Update bone roll automatically"""
# make a triangle face (p1,p2,p3)
p1 = edit_bone.head.copy()
p2 = edit_bone.tail.copy()
@@ -364,7 +419,8 @@ class FnBone:
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
@staticmethod
def has_auto_local_axis(name_j):
def has_auto_local_axis(name_j: str) -> bool:
"""Check if a bone should have automatic local axis"""
if name_j:
if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
return True
@@ -374,9 +430,11 @@ class FnBone:
return False
@staticmethod
def clean_additional_transformation(armature_object: bpy.types.Object):
def clean_additional_transformation(armature_object: Object) -> None:
"""Clean additional transformation constraints and bones"""
logger.info(f"Cleaning additional transformations for {armature_object.name}")
# clean constraints
p_bone: bpy.types.PoseBone
p_bone: PoseBone
for p_bone in armature_object.pose.bones:
p_bone.mmd_bone.is_additional_transform_dirty = True
constraints = p_bone.constraints
@@ -392,17 +450,21 @@ class FnBone:
"ADDITIONAL_TRANSFORM_INVERT",
}
def __is_at_shadow_bone(b):
def __is_at_shadow_bone(b: PoseBone) -> bool:
return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
if len(shadow_bone_names) > 0:
logger.debug(f"Removing {len(shadow_bone_names)} shadow bones")
with bpyutils.edit_object(armature_object) as data:
remove_edit_bones(data.edit_bones, shadow_bone_names)
@staticmethod
def apply_additional_transformation(armature_object: bpy.types.Object):
def __is_dirty_bone(b):
def apply_additional_transformation(armature_object: Object) -> None:
"""Apply additional transformation to bones"""
logger.info(f"Applying additional transformations for {armature_object.name}")
def __is_dirty_bone(b: PoseBone) -> bool:
if b.is_mmd_shadow_bone:
return False
mmd_bone = b.mmd_bone
@@ -411,9 +473,10 @@ class FnBone:
return mmd_bone.is_additional_transform_dirty
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
logger.debug(f"Found {len(dirty_bones)} dirty bones to process")
# setup constraints
shadow_bone_pool = []
shadow_bone_pool: List[Union[_AT_ShadowBoneRemove, _AT_ShadowBoneCreate]] = []
for p_bone in dirty_bones:
sb = FnBone.__setup_constraints(p_bone)
if sb:
@@ -434,7 +497,8 @@ class FnBone:
p_bone.mmd_bone.is_additional_transform_dirty = False
@staticmethod
def __setup_constraints(p_bone):
def __setup_constraints(p_bone: PoseBone) -> Optional[Union['_AT_ShadowBoneRemove', '_AT_ShadowBoneCreate']]:
"""Set up constraints for additional transformation"""
bone_name = p_bone.name
mmd_bone = p_bone.mmd_bone
influence = mmd_bone.additional_transform_influence
@@ -447,12 +511,14 @@ class FnBone:
rot = remove_constraint(constraints, "mmd_additional_rotation")
loc = remove_constraint(constraints, "mmd_additional_location")
if rot or loc:
logger.debug(f"Removing additional transform constraints for bone: {bone_name}")
return _AT_ShadowBoneRemove(bone_name)
return None
logger.debug(f"Setting up additional transform for bone: {bone_name} targeting {target_bone}")
shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone)
def __config(name, mute, map_type, value):
def __config(name: str, mute: bool, map_type: str, value: float) -> None:
if mute:
remove_constraint(constraints, name)
return
@@ -467,62 +533,81 @@ class FnBone:
return shadow_bone
@staticmethod
def update_additional_transform_influence(pose_bone: bpy.types.PoseBone):
def update_additional_transform_influence(pose_bone: PoseBone) -> None:
"""Update the influence of additional transform constraints"""
influence = pose_bone.mmd_bone.additional_transform_influence
constraints = pose_bone.constraints
c = constraints.get("mmd_additional_rotation", None)
TransformConstraintOp.update_min_max(c, math.pi, influence)
c = constraints.get("mmd_additional_location", None)
TransformConstraintOp.update_min_max(c, 100, influence)
logger.debug(f"Updated additional transform influence for bone: {pose_bone.name} to {influence}")
class MigrationFnBone:
"""Migration Functions for old MMD models broken by bugs or issues"""
@staticmethod
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object):
pose_bone: bpy.types.PoseBone
def fix_mmd_ik_limit_override(armature_object: Object) -> None:
"""Fix IK limit override constraints in old MMD models"""
logger.info(f"Fixing MMD IK limit overrides for {armature_object.name}")
pose_bone: PoseBone
for pose_bone in armature_object.pose.bones:
constraint: bpy.types.Constraint
constraint: Constraint
for constraint in pose_bone.constraints:
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
constraint.owner_space = "LOCAL"
logger.debug(f"Fixed IK limit override for bone: {pose_bone.name}")
class _AT_ShadowBoneRemove:
def __init__(self, bone_name):
"""Handler for removing shadow bones"""
def __init__(self, bone_name: str) -> None:
"""Initialize with bone name"""
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
def update_edit_bones(self, edit_bones):
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
"""Update edit bones by removing shadow bones"""
remove_edit_bones(edit_bones, self.__shadow_bone_names)
logger.debug(f"Removed shadow bones: {self.__shadow_bone_names}")
def update_pose_bones(self, pose_bones):
def update_pose_bones(self, pose_bones: Any) -> None:
"""Update pose bones (no-op for removal)"""
pass
class _AT_ShadowBoneCreate:
def __init__(self, bone_name, target_bone_name):
"""Handler for creating shadow bones"""
def __init__(self, bone_name: str, target_bone_name: str) -> None:
"""Initialize with bone names"""
self.__dummy_bone_name = "_dummy_" + bone_name
self.__shadow_bone_name = "_shadow_" + bone_name
self.__bone_name = bone_name
self.__target_bone_name = target_bone_name
self.__constraint_pool = []
self.__constraint_pool: List[Constraint] = []
def __is_well_aligned(self, bone0, bone1):
def __is_well_aligned(self, bone0: EditBone, bone1: EditBone) -> bool:
"""Check if two bones are well aligned"""
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
def __update_constraints(self, use_shadow=True):
def __update_constraints(self, use_shadow: bool = True) -> None:
"""Update constraints to use shadow or target bone"""
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
for c in self.__constraint_pool:
c.subtarget = subtarget
def add_constraint(self, constraint):
def add_constraint(self, constraint: Constraint) -> None:
"""Add a constraint to the pool"""
self.__constraint_pool.append(constraint)
def update_edit_bones(self, edit_bones):
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
"""Update edit bones by creating shadow bones"""
bone = edit_bones[self.__bone_name]
target_bone = edit_bones[self.__target_bone_name]
if bone != target_bone and self.__is_well_aligned(bone, target_bone):
logger.debug(f"Bones are well aligned, removing shadow bones for {self.__bone_name}")
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
return
@@ -532,6 +617,7 @@ class _AT_ShadowBoneCreate:
dummy.head = target_bone.head
dummy.tail = dummy.head + bone.tail - bone.head
dummy.roll = bone.roll
logger.debug(f"Created/updated dummy bone: {dummy_bone_name}")
shadow_bone_name = self.__shadow_bone_name
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
@@ -539,12 +625,15 @@ class _AT_ShadowBoneCreate:
shadow.head = dummy.head
shadow.tail = dummy.tail
shadow.roll = bone.roll
logger.debug(f"Created/updated shadow bone: {shadow_bone_name}")
def update_pose_bones(self, pose_bones):
def update_pose_bones(self, pose_bones: Any) -> None:
"""Update pose bones by setting up shadow bone properties"""
if self.__shadow_bone_name not in pose_bones:
logger.debug(f"Shadow bone {self.__shadow_bone_name} not found, using target bone directly")
self.__update_constraints(use_shadow=False)
return
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"
@@ -560,5 +649,7 @@ class _AT_ShadowBoneCreate:
c.subtarget = dummy_p_bone.name
c.target_space = "POSE"
c.owner_space = "POSE"
logger.debug(f"Created copy transforms constraint for shadow bone: {self.__shadow_bone_name}")
self.__update_constraints()
logger.debug(f"Updated constraints for shadow bone: {self.__shadow_bone_name}")
+195 -130
View File
@@ -6,16 +6,19 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math
from typing import Optional
from typing import Optional, List, Tuple, Callable, Any, Union
import bpy
from bpy.types import Object, ID, Camera, Context
from mathutils import Vector, Matrix, Euler
from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
class FnCamera:
@staticmethod
def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
def find_root(obj: Optional[Object]) -> Optional[Object]:
"""Find the root object of an MMD camera setup."""
if obj is None:
return None
if FnCamera.is_mmd_camera_root(obj):
@@ -25,16 +28,22 @@ class FnCamera:
return None
@staticmethod
def is_mmd_camera(obj: bpy.types.Object) -> bool:
def is_mmd_camera(obj: Object) -> bool:
"""Check if an object is an MMD camera."""
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
@staticmethod
def is_mmd_camera_root(obj: bpy.types.Object) -> bool:
def is_mmd_camera_root(obj: Object) -> bool:
"""Check if an object is an MMD camera root."""
return obj.type == "EMPTY" and obj.mmd_type == "CAMERA"
@staticmethod
def add_drivers(camera_object: bpy.types.Object):
def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1):
def add_drivers(camera_object: Object) -> None:
"""Add drivers to the camera object for MMD camera functionality."""
logger.debug(f"Adding drivers to camera: {camera_object.name}")
def __add_driver(id_data: ID, data_path: str, expression: str, index: int = -1) -> None:
"""Add a driver to the specified ID data."""
d = id_data.driver_add(data_path, index).driver
d.type = "SCRIPTED"
if "$empty_distance" in expression:
@@ -72,22 +81,36 @@ class FnCamera:
d.expression = expression
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
__add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1)
__add_driver(camera_object.data, "type", "not $is_perspective")
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
try:
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
__add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1)
__add_driver(camera_object.data, "type", "not $is_perspective")
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
logger.debug(f"Successfully added drivers to camera: {camera_object.name}")
except Exception as e:
logger.error(f"Failed to add drivers to camera {camera_object.name}: {str(e)}")
@staticmethod
def remove_drivers(camera_object: bpy.types.Object):
camera_object.data.driver_remove("ortho_scale")
camera_object.driver_remove("rotation_euler")
camera_object.data.driver_remove("ortho_scale")
camera_object.data.driver_remove("lens")
def remove_drivers(camera_object: Object) -> None:
"""Remove drivers from the camera object."""
logger.debug(f"Removing drivers from camera: {camera_object.name}")
try:
camera_object.data.driver_remove("ortho_scale")
camera_object.driver_remove("rotation_euler")
camera_object.data.driver_remove("ortho_scale")
camera_object.data.driver_remove("lens")
logger.debug(f"Successfully removed drivers from camera: {camera_object.name}")
except Exception as e:
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {str(e)}")
class MigrationFnCamera:
@staticmethod
def update_mmd_camera():
def update_mmd_camera() -> None:
"""Update all MMD cameras in the scene."""
logger.info("Updating all MMD cameras in the scene")
updated_count = 0
for camera_object in bpy.data.objects:
if camera_object.type != "CAMERA":
continue
@@ -97,161 +120,203 @@ class MigrationFnCamera:
# It's not a MMD Camera
continue
FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object)
try:
FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object)
updated_count += 1
except Exception as e:
logger.error(f"Failed to update MMD camera {camera_object.name}: {str(e)}")
logger.info(f"Updated {updated_count} MMD cameras")
class MMDCamera:
def __init__(self, obj):
def __init__(self, obj: Object):
"""Initialize an MMD camera."""
root_object = FnCamera.find_root(obj)
if root_object is None:
raise ValueError("%s is not MMDCamera" % str(obj))
logger.error(f"Object {obj.name} is not an MMD camera")
raise ValueError(f"{obj.name} is not an MMD camera")
self.__emptyObj = getattr(root_object, "original", obj)
logger.debug(f"Initialized MMD camera with root: {self.__emptyObj.name}")
@staticmethod
def isMMDCamera(obj: bpy.types.Object) -> bool:
def isMMDCamera(obj: Object) -> bool:
"""Check if an object is an MMD camera."""
return FnCamera.find_root(obj) is not None
@staticmethod
def addDrivers(cameraObj: bpy.types.Object):
def addDrivers(cameraObj: Object) -> None:
"""Add drivers to the camera object."""
FnCamera.add_drivers(cameraObj)
@staticmethod
def removeDrivers(cameraObj: bpy.types.Object):
def removeDrivers(cameraObj: Object) -> None:
"""Remove drivers from the camera object. """
if cameraObj.type != "CAMERA":
return
FnCamera.remove_drivers(cameraObj)
@staticmethod
def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0):
def convertToMMDCamera(cameraObj: Object, scale: float = 1.0) -> 'MMDCamera':
"""Convert a camera to an MMD camera."""
logger.info(f"Converting camera {cameraObj.name} to MMD camera with scale {scale}")
if FnCamera.is_mmd_camera(cameraObj):
logger.debug(f"Camera {cameraObj.name} is already an MMD camera")
return MMDCamera(cameraObj)
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty)
try:
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
context = FnContext.ensure_context()
FnContext.link_object(context, empty)
cameraObj.parent = empty
cameraObj.data.sensor_fit = "VERTICAL"
cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV
cameraObj.data.ortho_scale = 25 * scale
cameraObj.data.clip_end = 500 * scale
setattr(cameraObj.data, Props.display_size, 5 * scale)
cameraObj.location = (0, -45 * scale, 0)
cameraObj.rotation_mode = "XYZ"
cameraObj.rotation_euler = (math.radians(90), 0, 0)
cameraObj.lock_location = (True, False, True)
cameraObj.lock_rotation = (True, True, True)
cameraObj.lock_scale = (True, True, True)
cameraObj.data.dof.focus_object = empty
FnCamera.add_drivers(cameraObj)
cameraObj.parent = empty
cameraObj.data.sensor_fit = "VERTICAL"
cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV
cameraObj.data.ortho_scale = 25 * scale
cameraObj.data.clip_end = 500 * scale
setattr(cameraObj.data, Props.display_size, 5 * scale)
cameraObj.location = (0, -45 * scale, 0)
cameraObj.rotation_mode = "XYZ"
cameraObj.rotation_euler = (math.radians(90), 0, 0)
cameraObj.lock_location = (True, False, True)
cameraObj.lock_rotation = (True, True, True)
cameraObj.lock_scale = (True, True, True)
cameraObj.data.dof.focus_object = empty
FnCamera.add_drivers(cameraObj)
empty.location = (0, 0, 10 * scale)
empty.rotation_mode = "YXZ"
setattr(empty, Props.empty_display_size, 5 * scale)
empty.lock_scale = (True, True, True)
empty.mmd_type = "CAMERA"
empty.mmd_camera.angle = math.radians(30)
empty.mmd_camera.persp = True
return MMDCamera(empty)
empty.location = (0, 0, 10 * scale)
empty.rotation_mode = "YXZ"
setattr(empty, Props.empty_display_size, 5 * scale)
empty.lock_scale = (True, True, True)
empty.mmd_type = "CAMERA"
empty.mmd_camera.angle = math.radians(30)
empty.mmd_camera.persp = True
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
return MMDCamera(empty)
except Exception as e:
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {str(e)}")
raise
@staticmethod
def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1):
scene = bpy.context.scene
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
mmd_cam_root = mmd_cam.parent
def newMMDCameraAnimation(
cameraObj: Optional[Object],
cameraTarget: Optional[Object] = None,
scale: float = 1.0,
min_distance: float = 0.1
) -> 'MMDCamera':
"""Create a new MMD camera animation."""
logger.info(f"Creating new MMD camera animation with scale {scale}")
try:
scene = bpy.context.scene
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
mmd_cam_root = mmd_cam.parent
_camera_override_func = None
if cameraObj is None:
if scene.camera is None:
scene.camera = mmd_cam
return MMDCamera(mmd_cam_root)
_camera_override_func = lambda: scene.camera
_camera_override_func: Optional[Callable[[], Object]] = None
if cameraObj is None:
if scene.camera is None:
scene.camera = mmd_cam
logger.debug("Set scene camera to new MMD camera")
return MMDCamera(mmd_cam_root)
_camera_override_func = lambda: scene.camera
_target_override_func = None
if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
_target_override_func: Optional[Callable[[Object], Object]] = None
if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
action_name = mmd_cam_root.name
parent_action = bpy.data.actions.new(name=action_name)
distance_action = bpy.data.actions.new(name=action_name + "_dis")
FnCamera.remove_drivers(mmd_cam)
action_name = mmd_cam_root.name
parent_action = bpy.data.actions.new(name=action_name)
distance_action = bpy.data.actions.new(name=action_name + "_dis")
FnCamera.remove_drivers(mmd_cam)
from math import atan
from math import atan
from mathutils import Matrix, Vector
from mathutils import Matrix, Vector
render = scene.render
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
neg_z_vector = Vector((0, 0, -1))
frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end + 1, scene.frame_current
frame_count = frame_end - frame_start
frames = range(frame_start, frame_end)
render = scene.render
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
neg_z_vector = Vector((0, 0, -1))
frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end + 1, scene.frame_current
frame_count = frame_end - frame_start
frames = range(frame_start, frame_end)
fcurves = []
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves:
c.keyframe_points.add(frame_count)
fcurves = []
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves:
c.keyframe_points.add(frame_count)
logger.debug(f"Processing {frame_count} frames for camera animation")
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)):
scene.frame_set(f)
if _camera_override_func:
cameraObj = _camera_override_func()
if _target_override_func:
cameraTarget = _target_override_func(cameraObj)
cam_matrix_world = cameraObj.matrix_world
cam_target_loc = cameraTarget.matrix_world.translation
cam_rotation = (cam_matrix_world @ matrix_rotation).to_euler(mmd_cam_root.rotation_mode)
cam_vec = cam_matrix_world.to_3x3() @ neg_z_vector
if cameraObj.data.type == "ORTHO":
cam_dis = -(9 / 5) * cameraObj.data.ortho_scale
if cameraObj.data.sensor_fit != "VERTICAL":
if cameraObj.data.sensor_fit == "HORIZONTAL":
cam_dis *= factor
else:
cam_dis *= min(1, factor)
else:
target_vec = cam_target_loc - cam_matrix_world.translation
cam_dis = -max(target_vec.length * cam_vec.dot(target_vec.normalized()), min_distance)
cam_target_loc = cam_matrix_world.translation - cam_vec * cam_dis
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)):
scene.frame_set(f)
if _camera_override_func:
cameraObj = _camera_override_func()
if _target_override_func:
cameraTarget = _target_override_func(cameraObj)
cam_matrix_world = cameraObj.matrix_world
cam_target_loc = cameraTarget.matrix_world.translation
cam_rotation = (cam_matrix_world @ matrix_rotation).to_euler(mmd_cam_root.rotation_mode)
cam_vec = cam_matrix_world.to_3x3() @ neg_z_vector
if cameraObj.data.type == "ORTHO":
cam_dis = -(9 / 5) * cameraObj.data.ortho_scale
tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2
if cameraObj.data.sensor_fit != "VERTICAL":
ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height
if cameraObj.data.sensor_fit == "HORIZONTAL":
cam_dis *= factor
else:
cam_dis *= min(1, factor)
else:
target_vec = cam_target_loc - cam_matrix_world.translation
cam_dis = -max(target_vec.length * cam_vec.dot(target_vec.normalized()), min_distance)
cam_target_loc = cam_matrix_world.translation - cam_vec * cam_dis
tan_val *= factor * ratio
else: # cameraObj.data.sensor_fit == 'AUTO'
tan_val *= min(ratio, factor * ratio)
tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2
if cameraObj.data.sensor_fit != "VERTICAL":
ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height
if cameraObj.data.sensor_fit == "HORIZONTAL":
tan_val *= factor * ratio
else: # cameraObj.data.sensor_fit == 'AUTO'
tan_val *= min(ratio, factor * ratio)
x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
dis.co = (f, cam_dis)
fov.co = (f, 2 * atan(tan_val))
persp.co = (f, cameraObj.data.type != "ORTHO")
persp.interpolation = "CONSTANT"
for kp in (x, y, z, rx, ry, rz, fov, dis):
kp.interpolation = "LINEAR"
x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
dis.co = (f, cam_dis)
fov.co = (f, 2 * atan(tan_val))
persp.co = (f, cameraObj.data.type != "ORTHO")
persp.interpolation = "CONSTANT"
for kp in (x, y, z, rx, ry, rz, fov, dis):
kp.interpolation = "LINEAR"
FnCamera.add_drivers(mmd_cam)
mmd_cam_root.animation_data_create().action = parent_action
mmd_cam.animation_data_create().action = distance_action
scene.frame_set(frame_current)
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
return MMDCamera(mmd_cam_root)
except Exception as e:
logger.error(f"Failed to create MMD camera animation: {str(e)}")
raise
FnCamera.add_drivers(mmd_cam)
mmd_cam_root.animation_data_create().action = parent_action
mmd_cam.animation_data_create().action = distance_action
scene.frame_set(frame_current)
return MMDCamera(mmd_cam_root)
def object(self):
def object(self) -> Object:
"""Get the root object of the MMD camera."""
return self.__emptyObj
def camera(self):
def camera(self) -> Object:
"""Get the camera object of the MMD camera."""
for i in self.__emptyObj.children:
if i.type == "CAMERA":
return i
raise KeyError
logger.error(f"No camera found for MMD camera root {self.__emptyObj.name}")
raise KeyError(f"No camera found for MMD camera root {self.__emptyObj.name}")
+30 -13
View File
@@ -6,36 +6,48 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
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):
def __init__(self, obj: Object) -> None:
if MMDLamp.isLamp(obj):
obj = obj.parent
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
self.__emptyObj = obj
self.__emptyObj: Object = obj
else:
raise ValueError("%s is not MMDLamp" % str(obj))
error_msg = f"{str(obj)} is not MMDLamp"
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod
def isLamp(obj):
return obj and obj.type in {"LIGHT", "LAMP"}
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"}
@staticmethod
def isMMDLamp(obj):
def isMMDLamp(obj: Optional[Object]) -> bool:
"""Check if the object is an MMD lamp"""
if MMDLamp.isLamp(obj):
obj = obj.parent
return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
return obj is not None and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
@staticmethod
def convertToMMDLamp(lampObj, scale=1.0):
def convertToMMDLamp(lampObj: Object, scale: float = 1.0) -> 'MMDLamp':
"""Convert a regular lamp to an MMD lamp"""
if MMDLamp.isMMDLamp(lampObj):
logger.debug(f"Object {lampObj.name} is already an MMD lamp")
return MMDLamp(lampObj)
empty = bpy.data.objects.new(name="MMD_Light", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty)
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.rotation_mode = "XYZ"
empty.lock_rotation = (True, True, True)
@@ -57,13 +69,18 @@ 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):
def object(self) -> Object:
"""Get the empty object that represents this MMD lamp"""
return self.__emptyObj
def lamp(self):
def lamp(self) -> Object:
"""Get the actual lamp/light object"""
for i in self.__emptyObj.children:
if MMDLamp.isLamp(i):
return i
raise KeyError
error_msg = f"No lamp found in MMD lamp {self.__emptyObj.name}"
logger.error(error_msg)
raise KeyError(error_msg)
+124 -64
View File
@@ -7,7 +7,7 @@
import logging
import os
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast, Dict, List, Any, Union, Set
import bpy
from mathutils import Vector
@@ -15,6 +15,7 @@ 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
@@ -27,48 +28,53 @@ SPHERE_MODE_SUBTEX = 3
class _DummyTexture:
def __init__(self, image):
self.type = "IMAGE"
self.image = image
self.use_mipmap = True
def __init__(self, image: bpy.types.Image):
self.type: str = "IMAGE"
self.image: bpy.types.Image = image
self.use_mipmap: bool = True
class _DummyTextureSlot:
def __init__(self, image):
self.diffuse_color_factor = 1
self.uv_layer = ""
self.texture = _DummyTexture(image)
def __init__(self, image: bpy.types.Image):
self.diffuse_color_factor: float = 1
self.uv_layer: str = ""
self.texture: _DummyTexture = _DummyTexture(image)
class FnMaterial:
__NODES_ARE_READONLY: bool = False
def __init__(self, material: bpy.types.Material):
self.__material = material
self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY
self.__material: bpy.types.Material = material
self._nodes_are_readonly: bool = FnMaterial.__NODES_ARE_READONLY
@staticmethod
def set_nodes_are_readonly(nodes_are_readonly: bool):
def set_nodes_are_readonly(nodes_are_readonly: bool) -> None:
FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
@classmethod
def from_material_id(cls, material_id: str):
def from_material_id(cls, material_id: str) -> Optional['FnMaterial']:
for material in bpy.data.materials:
if material.mmd_material.material_id == material_id:
return cls(material)
return None
@staticmethod
def clean_materials(obj, can_remove: Callable[[bpy.types.Material], bool]):
def clean_materials(obj: bpy.types.Object, can_remove: Callable[[bpy.types.Material], bool]) -> None:
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: str | int, mat2_ref: str | int, reverse=False, swap_slots=False) -> Tuple[bpy.types.Material, bpy.types.Material]:
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]:
"""
This method will assign the polygons of mat1 to mat2.
If reverse is True it will also swap the polygons assigned to mat2 to mat1.
@@ -98,8 +104,12 @@ class FnMaterial:
except (KeyError, IndexError) as exc:
# Wrap exceptions within our custom ones
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:
@@ -113,33 +123,37 @@ class FnMaterial:
return mat1, mat2
@staticmethod
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]):
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}")
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):
mmd_mat: MMDMaterial = self.__material.mmd_material
def material_id(self) -> int:
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):
def material(self) -> bpy.types.Material:
return self.__material
def __same_image_file(self, image, filepath):
def __same_image_file(self, image: Optional[bpy.types.Image], filepath: str) -> bool:
if image and image.source == "FILE":
# pylint: disable=assignment-from-no-return
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
@@ -152,14 +166,15 @@ class FnMaterial:
pass
return False
def _load_image(self, filepath):
def _load_image(self, filepath: str) -> bpy.types.Image:
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:
logging.warning("Cannot create a texture for %s. No such file.", filepath)
logger.warning(f"Cannot create a texture for {filepath}. No such file.")
img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
img.source = "FILE"
img.filepath = filepath
@@ -170,43 +185,46 @@ class FnMaterial:
img.alpha_mode = "NONE"
return img
def update_toon_texture(self):
def update_toon_texture(self) -> None:
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))
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):
def _mix_diffuse_and_ambient(self, mmd_mat: 'MMDMaterial') -> List[float]:
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):
def update_drop_shadow(self) -> None:
pass
def update_enabled_toon_edge(self):
def update_enabled_toon_edge(self) -> None:
if self._nodes_are_readonly:
return
self.update_edge_color()
def update_edge_color(self):
def update_edge_color(self) -> None:
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: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None)
mat_edge: Optional[bpy.types.Material] = bpy.data.materials.get("mmd_edge." + mat.name, None)
if mat_edge:
mat_edge.mmd_material.edge_color = line_color
@@ -217,39 +235,46 @@ 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):
def update_edge_weight(self) -> None:
pass
def get_texture(self):
def get_texture(self) -> Optional[_DummyTexture]:
return self.__get_texture_node("mmd_base_tex", use_dummy=True)
def create_texture(self, filepath):
def create_texture(self, filepath: str) -> _DummyTextureSlot:
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):
def remove_texture(self) -> None:
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):
def get_sphere_texture(self) -> Optional[_DummyTexture]:
return self.__get_texture_node("mmd_sphere_tex", use_dummy=True)
def use_sphere_texture(self, use_sphere, obj=None):
def use_sphere_texture(self, use_sphere: bool, obj: Optional[bpy.types.Object] = None) -> 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, obj=None):
def create_sphere_texture(self, filepath: str, obj: Optional[bpy.types.Object] = None) -> _DummyTextureSlot:
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=None):
def update_sphere_texture_type(self, obj: Optional[bpy.types.Object] = None) -> None:
if self._nodes_are_readonly:
return
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
@@ -277,48 +302,54 @@ class FnMaterial:
next(uv_layers, None) # skip base UV
subtex_uv = getattr(next(uv_layers, None), "name", "")
if subtex_uv != "UV1":
logging.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv)
logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex')
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):
def remove_sphere_texture(self) -> None:
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):
def get_toon_texture(self) -> Optional[_DummyTexture]:
return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
def use_toon_texture(self, use_toon):
def use_toon_texture(self, use_toon: bool) -> None:
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):
def create_toon_texture(self, filepath: str) -> _DummyTextureSlot:
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):
def remove_toon_texture(self) -> None:
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, use_dummy=False):
def __get_texture_node(self, node_name: str, use_dummy: bool = False) -> Optional[Union[bpy.types.ShaderNodeTexImage, _DummyTexture]]:
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):
def __remove_texture_node(self, node_name: str) -> None:
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, filepath, pos):
def __create_texture_node(self, node_name: str, filepath: str, pos: Tuple[float, float]) -> bpy.types.ShaderNodeTexImage:
texture = self.__get_texture_node(node_name)
if texture is None:
from mathutils import Vector
@@ -334,23 +365,25 @@ class FnMaterial:
self.__update_shader_nodes()
return texture
def update_ambient_color(self):
def update_ambient_color(self) -> None:
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):
def update_diffuse_color(self) -> None:
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):
def update_alpha(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
@@ -368,16 +401,18 @@ 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):
def update_specular_color(self) -> None:
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):
def update_shininess(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
@@ -388,8 +423,9 @@ class FnMaterial:
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):
def update_is_double_sided(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
@@ -399,8 +435,9 @@ 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):
def update_self_shadow_map(self) -> None:
if self._nodes_are_readonly:
return
mat = self.material
@@ -408,21 +445,24 @@ 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):
def update_self_shadow(self) -> None:
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, context=bpy.context):
def convert_to_mmd_material(material: bpy.types.Material, context: bpy.types.Context = bpy.context) -> None:
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):
def search_tex_image_node(node: bpy.types.ShaderNode) -> Optional[bpy.types.ShaderNodeTexImage]:
if node.type == "TEX_IMAGE":
return node
for node_input in node.inputs:
@@ -459,6 +499,7 @@ 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
@@ -466,6 +507,7 @@ class FnMaterial:
if bsdf_node:
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]
@@ -498,9 +540,10 @@ class FnMaterial:
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_')]
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, val):
def __update_shader_input(self, name: str, val: Any) -> None:
mat = self.material
if mat.name.startswith("mmd_"): # skip mmd_edge.*
return
@@ -512,26 +555,29 @@ 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):
def __update_shader_nodes(self) -> None:
mat = self.material
if mat.node_tree is None:
logger.debug(f"Creating node tree for {mat.name}")
mat.use_nodes = True
mat.node_tree.nodes.clear()
nodes, links = mat.node_tree.nodes, mat.node_tree.links
class _Dummy:
default_value, is_linked = None, True
default_value: Any = None
is_linked: bool = 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.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,)
@@ -543,6 +589,7 @@ 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))
@@ -567,12 +614,13 @@ 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):
def __get_shader_uv(self) -> bpy.types.ShaderNodeTree:
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)
############################################################################
@@ -604,12 +652,13 @@ class FnMaterial:
return shader
def __get_shader(self):
def __get_shader(self) -> bpy.types.ShaderNodeTree:
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)
############################################################################
@@ -699,15 +748,18 @@ class FnMaterial:
class MigrationFnMaterial:
@staticmethod
def update_mmd_shader():
def update_mmd_shader() -> None:
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
@@ -716,3 +768,11 @@ 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))
+161 -89
View File
@@ -6,9 +6,8 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import itertools
import logging
import time
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast, List, Tuple
import bpy
import idprop
@@ -20,15 +19,17 @@ from ..bpyutils import FnContext, Props
from . import rigid_body
from .morph import FnMorph
from .rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC
from ....core.logging_setup import logger
if TYPE_CHECKING:
from ..properties.morph import MaterialMorphData
from ..properties.rigid_body import MMDRigidBody
from bpy.types import Context, Object, PropertyGroup, Material, Mesh, Armature, EditBone, PoseBone, KinematicConstraint
class FnModel:
@staticmethod
def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Dict[str, Dict[Any, Any]] = None):
def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Optional[Dict[str, Dict[Any, Any]]] = None) -> None:
FnModel.__copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values or {})
@staticmethod
@@ -40,9 +41,11 @@ class FnModel:
Optional[bpy.types.Object]: The root object of the model. If the object is not a part of a model, None is returned.
Generally, the root object is a object with type == "EMPTY" and mmd_type == "ROOT".
"""
while obj is not None and obj.mmd_type != "ROOT":
while obj is not None:
if hasattr(obj, 'mmd_type') and obj.mmd_type == "ROOT":
return obj
obj = obj.parent
return obj
return None
@staticmethod
def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]:
@@ -213,7 +216,8 @@ class FnModel:
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE"
@staticmethod
def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]):
def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]) -> None:
logger.info(f"Joining models to parent root: {parent_root_object.name}")
parent_armature_object = FnModel.find_armature_object(parent_root_object)
with bpy.context.temp_override(
active_object=parent_armature_object,
@@ -221,7 +225,7 @@ class FnModel:
):
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pose_bones):
def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs: List[Any], pose_bones: List[bpy.types.PoseBone]) -> None:
"""This function will also update the references of bone morphs and rotate+/move+."""
bone_id = bone.mmd_bone.bone_id
@@ -259,6 +263,7 @@ class FnModel:
child_root_object: bpy.types.Object
for child_root_object in child_root_objects:
logger.info(f"Processing child root: {child_root_object.name}")
child_armature_object = FnModel.find_armature_object(child_root_object)
child_pose_bones = child_armature_object.pose.bones
child_bone_morphs = child_root_object.mmd_root.bone_morphs
@@ -279,7 +284,7 @@ class FnModel:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Disconnect mesh dependencies because transform_apply fails when mesh data are multiple used.
related_meshes: Dict[MaterialMorphData, bpy.types.Mesh] = {}
related_meshes: Dict['MaterialMorphData', bpy.types.Mesh] = {}
for material_morph in child_root_object.mmd_root.material_morphs:
for material_morph_data in material_morph.data:
if material_morph_data.related_mesh_data is not None:
@@ -352,6 +357,8 @@ class FnModel:
# Remove unused objects from child models
if len(child_root_object.children) == 0:
bpy.data.objects.remove(child_root_object)
logger.info("Model joining completed successfully")
@staticmethod
def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier:
@@ -369,10 +376,13 @@ class FnModel:
return modifier
@staticmethod
def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool):
def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool) -> None:
logger.info(f"Attaching mesh objects to {parent_root_object.name}")
armature_object = FnModel.find_armature_object(parent_root_object)
if armature_object is None:
raise ValueError(f"Armature object not found in {parent_root_object}")
error_msg = f"Armature object not found in {parent_root_object.name}"
logger.error(error_msg)
raise ValueError(error_msg)
def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object:
if obj.parent is None:
@@ -381,9 +391,11 @@ class FnModel:
for mesh_object in mesh_objects:
if not FnModel.is_mesh_object(mesh_object):
logger.debug(f"Skipping non-mesh object: {mesh_object.name}")
continue
if FnModel.find_root_object(mesh_object) is not None:
logger.debug(f"Skipping mesh with existing root: {mesh_object.name}")
continue
mesh_root_object = __get_root_object(mesh_object)
@@ -391,15 +403,20 @@ class FnModel:
mesh_root_object.parent_type = "OBJECT"
mesh_root_object.parent = armature_object
mesh_root_object.matrix_world = original_matrix_world
logger.debug(f"Attached mesh: {mesh_object.name}")
if add_armature_modifier:
FnModel._add_armature_modifier(mesh_object, armature_object)
logger.debug(f"Added armature modifier to: {mesh_object.name}")
@staticmethod
def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool):
def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool) -> None:
logger.info(f"Adding missing vertex groups from bones to {mesh_object.name}")
armature_object = FnModel.find_armature_object(root_object)
if armature_object is None:
raise ValueError(f"Armature object not found in {root_object}")
error_msg = f"Armature object not found in {root_object.name}"
logger.error(error_msg)
raise ValueError(error_msg)
vertex_group_names: Set[str] = set()
@@ -408,6 +425,7 @@ class FnModel:
for search_mesh in search_meshes:
vertex_group_names.update(search_mesh.vertex_groups.keys())
added_count = 0
pose_bone: bpy.types.PoseBone
for pose_bone in armature_object.pose.bones:
pose_bone_name = pose_bone.name
@@ -419,28 +437,34 @@ class FnModel:
continue
mesh_object.vertex_groups.new(name=pose_bone_name)
added_count += 1
logger.debug(f"Added {added_count} missing vertex groups to {mesh_object.name}")
@staticmethod
def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int):
def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int) -> None:
logger.info(f"Changing IK loop factor to {new_ik_loop_factor}")
mmd_root = root_object.mmd_root
old_ik_loop_factor = mmd_root.ik_loop_factor
if new_ik_loop_factor == old_ik_loop_factor:
logger.debug("IK loop factor already set to the requested value")
return
armature_object = FnModel.find_armature_object(root_object)
updated_count = 0
for pose_bone in armature_object.pose.bones:
for constraint in (cast(bpy.types.KinematicConstraint, c) for c in pose_bone.constraints if c.type == "IK"):
iterations = int(constraint.iterations * new_ik_loop_factor / old_ik_loop_factor)
logging.info("Update %s of %s: %d -> %d", constraint.name, pose_bone.name, constraint.iterations, iterations)
logger.debug(f"Update {constraint.name} of {pose_bone.name}: {constraint.iterations} -> {iterations}")
constraint.iterations = iterations
updated_count += 1
mmd_root.ik_loop_factor = new_ik_loop_factor
return
logger.info(f"Updated {updated_count} IK constraints")
@staticmethod
def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
destination_rna_properties = destination.bl_rna.properties
for name in source.keys():
is_attr = hasattr(source, name)
@@ -466,7 +490,7 @@ class FnModel:
destination[name] = value
@staticmethod
def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
if overwrite:
destination.clear()
@@ -499,16 +523,19 @@ class FnModel:
FnModel.__copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values)
@staticmethod
def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
if isinstance(destination, bpy.types.PropertyGroup):
FnModel.__copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
elif isinstance(destination, bpy.types.bpy_prop_collection):
FnModel.__copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
else:
raise ValueError(f"Unsupported destination: {destination}")
error_msg = f"Unsupported destination: {destination}"
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod
def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True):
def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True) -> None:
logger.info(f"Initializing display item frames for {root_object.name}")
frames = root_object.mmd_root.display_item_frames
if reset and len(frames) > 0:
root_object.mmd_root.active_display_item_frame = 0
@@ -531,6 +558,8 @@ class FnModel:
if not reset:
frames.move(frames.find("Root"), 0)
frames.move(frames.find("表情"), 1)
logger.debug(f"Display item frames initialized with {len(frames)} frames")
@staticmethod
def get_empty_display_size(root_object: bpy.types.Object) -> float:
@@ -541,19 +570,28 @@ class MigrationFnModel:
"""Migration Functions for old MMD models broken by bugs or issues"""
@classmethod
def update_mmd_ik_loop_factor(cls):
def update_mmd_ik_loop_factor(cls) -> None:
logger.info("Updating MMD IK loop factor for all armatures")
updated_count = 0
for armature_object in bpy.data.objects:
if armature_object.type != "ARMATURE":
continue
if "mmd_ik_loop_factor" not in armature_object:
return
continue
FnModel.find_root_object(armature_object).mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1)
del armature_object["mmd_ik_loop_factor"]
root_object = FnModel.find_root_object(armature_object)
if root_object:
root_object.mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1)
del armature_object["mmd_ik_loop_factor"]
updated_count += 1
logger.info(f"Updated IK loop factor for {updated_count} armatures")
@staticmethod
def update_avatar_toolkit_version():
def update_avatar_toolkit_version() -> None:
logger.info("Updating Avatar Toolkit version for all MMD root objects")
updated_count = 0
for root_object in bpy.data.objects:
if root_object.type != "EMPTY":
continue
@@ -565,10 +603,13 @@ class MigrationFnModel:
continue
root_object["avatar_toolkit_version"] = "0.2.1"
updated_count += 1
logger.info(f"Updated Avatar Toolkit version for {updated_count} root objects")
class Model:
def __init__(self, root_obj):
def __init__(self, root_obj: bpy.types.Object) -> None:
if root_obj is None:
raise ValueError("must be MMD ROOT type object")
if root_obj.mmd_type != "ROOT":
@@ -578,13 +619,15 @@ class Model:
self.__rigid_grp: Optional[bpy.types.Object] = None
self.__joint_grp: Optional[bpy.types.Object] = None
self.__temporary_grp: Optional[bpy.types.Object] = None
logger.debug(f"Model initialized with root object: {self.__root.name}")
@staticmethod
def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False):
def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False) -> 'Model':
if obj_name is None:
obj_name = name
context = FnContext.ensure_context()
logger.info(f"Creating new MMD model: {name}")
root: bpy.types.Object = bpy.data.objects.new(name=obj_name, object_data=None)
root.mmd_type = "ROOT"
@@ -595,6 +638,7 @@ class Model:
FnContext.link_object(context, root)
if armature_object:
logger.debug(f"Using existing armature: {armature_object.name}")
m = armature_object.matrix_world
armature_object.parent_type = "OBJECT"
armature_object.parent = root
@@ -602,6 +646,7 @@ class Model:
root.matrix_world = m
armature_object.matrix_local.identity()
else:
logger.debug("Creating new armature")
armature_object = bpy.data.objects.new(name=obj_name + "_arm", object_data=bpy.data.armatures.new(name=obj_name))
armature_object.parent = root
FnContext.link_object(context, armature_object)
@@ -614,6 +659,7 @@ class Model:
FnBone.setup_special_bone_collections(armature_object)
if add_root_bone:
logger.debug("Adding root bone")
bone_name = "全ての親"
bone_name_english = "Root"
@@ -637,34 +683,37 @@ class Model:
bone_collection.assign(data_bone)
FnContext.set_active_and_select_single_object(context, root)
logger.info(f"Model created successfully: {name}")
return Model(root)
@staticmethod
def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
return FnModel.find_root_object(obj)
def initialDisplayFrames(self, reset=True):
def initialDisplayFrames(self, reset: bool = True) -> None:
FnModel.initalize_display_item_frames(self.__root, reset=reset)
@property
def morph_slider(self):
def morph_slider(self) -> Any:
return FnMorph.get_morph_slider(self)
def loadMorphs(self):
def loadMorphs(self) -> None:
logger.info(f"Loading morphs for model: {self.__root.name}")
FnMorph.load_morphs(self)
def create_ik_constraint(self, bone, ik_target):
def create_ik_constraint(self, bone: bpy.types.PoseBone, ik_target: bpy.types.PoseBone) -> bpy.types.KinematicConstraint:
"""create IK constraint
Args:
bone: A pose bone to add a IK constraint
id_target: A pose bone for IK target
ik_target: A pose bone for IK target
Returns:
The bpy.types.KinematicConstraint object created. It is set target
and subtarget options.
"""
logger.debug(f"Creating IK constraint on {bone.name} targeting {ik_target.name}")
ik_target_name = ik_target.name
ik_const = bone.constraints.new("IK")
ik_const.target = self.__arm
@@ -693,6 +742,7 @@ class Model:
if self.__rigid_grp is None:
self.__rigid_grp = FnModel.find_rigid_group_object(self.__root)
if self.__rigid_grp is None:
logger.debug(f"Creating rigid group object for {self.__root.name}")
rigids = bpy.data.objects.new(name="rigidbodies", object_data=None)
FnContext.link_object(FnContext.ensure_context(), rigids)
rigids.mmd_type = "RIGID_GRP_OBJ"
@@ -710,6 +760,7 @@ class Model:
if self.__joint_grp is None:
self.__joint_grp = FnModel.find_joint_group_object(self.__root)
if self.__joint_grp is None:
logger.debug(f"Creating joint group object for {self.__root.name}")
joints = bpy.data.objects.new(name="joints", object_data=None)
FnContext.link_object(FnContext.ensure_context(), joints)
joints.mmd_type = "JOINT_GRP_OBJ"
@@ -727,6 +778,7 @@ class Model:
if self.__temporary_grp is None:
self.__temporary_grp = FnModel.find_temporary_group_object(self.__root)
if self.__temporary_grp is None:
logger.debug(f"Creating temporary group object for {self.__root.name}")
temporarys = bpy.data.objects.new(name="temporary", object_data=None)
FnContext.link_object(FnContext.ensure_context(), temporarys)
temporarys.mmd_type = "TEMPORARY_GRP_OBJ"
@@ -740,7 +792,7 @@ class Model:
def meshes(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_mesh_objects(self.__root)
def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True):
def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True) -> None:
FnModel.attach_mesh_objects(self.rootObject(), meshes, add_armature_modifier)
def firstMesh(self) -> Optional[bpy.types.Object]:
@@ -748,7 +800,7 @@ class Model:
return i
return None
def findMesh(self, mesh_name) -> Optional[bpy.types.Object]:
def findMesh(self, mesh_name: str) -> Optional[bpy.types.Object]:
"""
Helper method to find a mesh by name
"""
@@ -787,25 +839,26 @@ class Model:
def joints(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_joint_objects(self.__root)
def temporaryObjects(self, rigid_track_only=False) -> Iterator[bpy.types.Object]:
def temporaryObjects(self, rigid_track_only: bool = False) -> Iterator[bpy.types.Object]:
return FnModel.iterate_temporary_objects(self.__root, rigid_track_only)
def materials(self) -> Iterator[bpy.types.Material]:
"""
Helper method to list all materials in all meshes
"""
materials = {} # Use dict instead of set to guarantee preserve order
materials: Dict[bpy.types.Material, int] = {} # Use dict instead of set to guarantee preserve order
for mesh in self.meshes():
materials.update((slot.material, 0) for slot in mesh.material_slots if slot.material is not None)
return iter(materials.keys())
def renameBone(self, old_bone_name, new_bone_name):
def renameBone(self, old_bone_name: str, new_bone_name: str) -> None:
if old_bone_name == new_bone_name:
return
logger.info(f"Renaming bone: {old_bone_name} -> {new_bone_name}")
armature = self.armature()
bone = armature.pose.bones[old_bone_name]
bone.name = new_bone_name
new_bone_name = bone.name
new_bone_name = bone.name # Get the actual name (might be adjusted by Blender)
mmd_root = self.rootObject().mmd_root
for frame in mmd_root.display_item_frames:
@@ -816,28 +869,31 @@ class Model:
if old_bone_name in mesh.vertex_groups:
mesh.vertex_groups[old_bone_name].name = new_bone_name
def build(self, non_collision_distance_scale=1.5, collision_margin=1e-06):
def build(self, non_collision_distance_scale: float = 1.5, collision_margin: float = 1e-06) -> None:
logger.info(f"Building physics rig for {self.__root.name}")
rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
if self.__root.mmd_root.is_built:
logger.info("Model is already built, cleaning first")
self.clean()
self.__root.mmd_root.is_built = True
logging.info("****************************************")
logging.info(" Build rig")
logging.info("****************************************")
logger.info("****************************************")
logger.info(" Build rig")
logger.info("****************************************")
start_time = time.time()
self.__preBuild()
self.disconnectPhysicsBones()
self.buildRigids(non_collision_distance_scale, collision_margin)
self.buildJoints()
self.__postBuild()
logging.info(" Finished building in %f seconds.", time.time() - start_time)
logger.info(" Finished building in %f seconds.", time.time() - start_time)
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled)
def clean(self):
def clean(self) -> None:
logger.info(f"Cleaning physics rig for {self.__root.name}")
rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
logging.info("****************************************")
logging.info(" Clean rig")
logging.info("****************************************")
logger.info("****************************************")
logger.info(" Clean rig")
logger.info("****************************************")
start_time = time.time()
pose_bones = []
@@ -848,13 +904,14 @@ class Model:
if "mmd_tools_rigid_track" in i.constraints:
const = i.constraints["mmd_tools_rigid_track"]
i.constraints.remove(const)
logger.debug(f"Removed rigid track constraint from {i.name}")
rigid_track_counts = 0
for i in self.rigidBodies():
rigid_type = int(i.mmd_rigid.type)
if "mmd_tools_rigid_parent" not in i.constraints:
rigid_track_counts += 1
logging.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name)
logger.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name)
i.mmd_rigid.bone = i.mmd_rigid.bone
relation = i.constraints["mmd_tools_rigid_parent"]
relation.mute = True
@@ -884,35 +941,39 @@ class Model:
mmd_root = self.rootObject().mmd_root
if mmd_root.show_temporary_objects:
mmd_root.show_temporary_objects = False
logging.info(" Finished cleaning in %f seconds.", time.time() - start_time)
logger.info(" Finished cleaning in %f seconds.", time.time() - start_time)
mmd_root.is_built = False
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled)
def __removeTemporaryObjects(self):
def __removeTemporaryObjects(self) -> None:
logger.debug("Removing temporary objects")
with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()):
bpy.ops.object.delete()
def __restoreTransforms(self, obj):
def __restoreTransforms(self, obj: bpy.types.Object) -> None:
for attr in ("location", "rotation_euler"):
attr_name = "__backup_%s__" % attr
val = obj.get(attr_name, None)
if val is not None:
setattr(obj, attr, val)
del obj[attr_name]
logger.debug(f"Restored {attr} for {obj.name}")
def __backupTransforms(self, obj):
def __backupTransforms(self, obj: bpy.types.Object) -> None:
for attr in ("location", "rotation_euler"):
attr_name = "__backup_%s__" % attr
if attr_name in obj: # should not happen in normal build/clean cycle
continue
obj[attr_name] = getattr(obj, attr, None)
logger.debug(f"Backed up {attr} for {obj.name}")
def __preBuild(self):
self.__fake_parent_map = {}
self.__rigid_body_matrix_map = {}
self.__empty_parent_map = {}
def __preBuild(self) -> None:
logger.debug("Pre-build preparation")
self.__fake_parent_map: Dict[bpy.types.Object, List[bpy.types.Object]] = {}
self.__rigid_body_matrix_map: Dict[bpy.types.Object, Any] = {}
self.__empty_parent_map: Dict[bpy.types.Object, bpy.types.Object] = {}
no_parents = []
no_parents: List[bpy.types.Object] = []
for i in self.rigidBodies():
self.__backupTransforms(i)
# mute relation
@@ -932,7 +993,7 @@ class Model:
# update changes of armature constraints
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
parented = []
parented: List[bpy.types.Object] = []
for i in self.joints():
self.__backupTransforms(i)
rbc = i.rigid_body_constraint
@@ -950,7 +1011,8 @@ class Model:
# assert(len(no_parents) == len(parented))
def __postBuild(self):
def __postBuild(self) -> None:
logger.debug("Post-build finalization")
self.__fake_parent_map = None
self.__rigid_body_matrix_map = None
@@ -962,6 +1024,7 @@ class Model:
matrix_world = empty.matrix_world
empty.parent = rigid_obj
empty.matrix_world = matrix_world
logger.debug(f"Parented empty {empty.name} to rigid object {rigid_obj.name}")
self.__empty_parent_map = None
arm = self.armature()
@@ -970,11 +1033,13 @@ class Model:
c = p_bone.constraints.get("mmd_tools_rigid_track", None)
if c:
c.mute = False
logger.debug(f"Enabled rigid track constraint for {p_bone.name}")
def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float):
def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float) -> None:
assert rigid_obj.mmd_type == "RIGID_BODY"
rb = rigid_obj.rigid_body
if rb is None:
logger.warning(f"No rigid body for {rigid_obj.name}")
return
rigid = rigid_obj.mmd_rigid
@@ -1018,7 +1083,7 @@ class Model:
fake_children = self.__fake_parent_map.get(rigid_obj, None)
if fake_children:
for fake_child in fake_children:
logging.debug(" - fake_child: %s", fake_child.name)
logger.debug(" - fake_child: %s", fake_child.name)
t, r, s = (m @ fake_child.matrix_local).decompose()
fake_child.location = t
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode)
@@ -1032,7 +1097,7 @@ class Model:
fake_children = self.__fake_parent_map.get(rigid_obj, None)
if fake_children:
for fake_child in fake_children:
logging.debug(" - fake_child: %s", fake_child.name)
logger.debug(" - fake_child: %s", fake_child.name)
t, r, s = (m @ fake_child.matrix_local).decompose()
fake_child.location = t
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode)
@@ -1062,7 +1127,7 @@ class Model:
ori_rigid_obj = self.__empty_parent_map[empty]
ori_rb = ori_rigid_obj.rigid_body
if ori_rb and rb.mass > ori_rb.mass:
logging.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name)
logger.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name)
# re-parenting
rigid_obj.mmd_rigid.bone = bone_name
rigid_obj.constraints.remove(relation)
@@ -1070,21 +1135,22 @@ class Model:
# revert change
ori_rigid_obj.mmd_rigid.bone = bone_name
else:
logging.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name)
logger.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name)
rb.collision_shape = rigid.shape
logger.debug(f"Updated rigid body {rigid_obj.name} with type {rigid_type}")
def __getRigidRange(self, obj):
def __getRigidRange(self, obj: bpy.types.Object) -> float:
return (Vector(obj.bound_box[0]) - Vector(obj.bound_box[6])).length
def __createNonCollisionConstraint(self, nonCollisionJointTable):
def __createNonCollisionConstraint(self, nonCollisionJointTable: List[Tuple[bpy.types.Object, bpy.types.Object]]) -> None:
total_len = len(nonCollisionJointTable)
if total_len < 1:
return
start_time = time.time()
logging.debug("-" * 60)
logging.debug(" creating ncc, counts: %d", total_len)
logger.debug("-" * 60)
logger.debug(" creating ncc, counts: %d", total_len)
ncc_obj = bpyutils.createObject(name="ncc", object_data=None)
ncc_obj.location = [0, 0, 0]
@@ -1099,26 +1165,26 @@ class Model:
rb.disable_collisions = True
ncc_objs = bpyutils.duplicateObject(ncc_obj, total_len)
logging.debug(" created %d ncc.", len(ncc_objs))
logger.debug(" created %d ncc.", len(ncc_objs))
for ncc_obj, pair in zip(ncc_objs, nonCollisionJointTable):
rbc = ncc_obj.rigid_body_constraint
rbc.object1, rbc.object2 = pair
ncc_obj.hide_set(True)
ncc_obj.hide_select = True
logging.debug(" finish in %f seconds.", time.time() - start_time)
logging.debug("-" * 60)
logger.debug(" finish in %f seconds.", time.time() - start_time)
logger.debug("-" * 60)
def buildRigids(self, non_collision_distance_scale, collision_margin):
logging.debug("--------------------------------")
logging.debug(" Build riggings of rigid bodies")
logging.debug("--------------------------------")
def buildRigids(self, non_collision_distance_scale: float, collision_margin: float) -> List[bpy.types.Object]:
logger.debug("--------------------------------")
logger.debug(" Build riggings of rigid bodies")
logger.debug("--------------------------------")
rigid_objects = list(self.rigidBodies())
rigid_object_groups = [[] for i in range(16)]
rigid_object_groups: List[List[bpy.types.Object]] = [[] for i in range(16)]
for i in rigid_objects:
rigid_object_groups[i.mmd_rigid.collision_group_number].append(i)
jointMap = {}
jointMap: Dict[frozenset, bpy.types.Object] = {}
for joint in self.joints():
rbc = joint.rigid_body_constraint
if rbc is None:
@@ -1126,10 +1192,10 @@ class Model:
rbc.disable_collisions = False
jointMap[frozenset((rbc.object1, rbc.object2))] = joint
logging.info("Creating non collision constraints")
logger.info("Creating non collision constraints")
# create non collision constraints
nonCollisionJointTable = []
non_collision_pairs = set()
nonCollisionJointTable: List[Tuple[bpy.types.Object, bpy.types.Object]] = []
non_collision_pairs: Set[frozenset] = set()
rigid_object_cnt = len(rigid_objects)
for obj_a in rigid_objects:
for n, ignore in enumerate(obj_a.mmd_rigid.collision_group_mask):
@@ -1150,12 +1216,13 @@ class Model:
nonCollisionJointTable.append((obj_a, obj_b))
non_collision_pairs.add(pair)
for cnt, i in enumerate(rigid_objects):
logging.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name)
logger.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name)
self.updateRigid(i, collision_margin)
self.__createNonCollisionConstraint(nonCollisionJointTable)
return rigid_objects
def buildJoints(self):
def buildJoints(self) -> None:
logger.info("Building joints")
for i in self.joints():
rbc = i.rigid_body_constraint
if rbc is None:
@@ -1168,8 +1235,9 @@ class Model:
t, r, s = (m @ i.matrix_local).decompose()
i.location = t
i.rotation_euler = r.to_euler(i.rotation_mode)
logger.debug(f"Built joint: {i.name}")
def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]):
def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]) -> None:
armature_object = self.armature()
armature: bpy.types.Armature
@@ -1177,7 +1245,7 @@ class Model:
edit_bones = armature.edit_bones
rigid_body_object: bpy.types.Object
for rigid_body_object in self.rigidBodies():
mmd_rigid: MMDRigidBody = rigid_body_object.mmd_rigid
mmd_rigid: 'MMDRigidBody' = rigid_body_object.mmd_rigid
if mmd_rigid.type not in target_modes:
continue
@@ -1188,21 +1256,25 @@ class Model:
editor(edit_bone)
def disconnectPhysicsBones(self):
def editor(edit_bone: bpy.types.EditBone):
def disconnectPhysicsBones(self) -> None:
logger.info("Disconnecting physics bones")
def editor(edit_bone: bpy.types.EditBone) -> None:
rna_prop_ui.rna_idprop_ui_create(edit_bone, "mmd_bone_use_connect", default=edit_bone.use_connect)
edit_bone.use_connect = False
logger.debug(f"Disconnected bone: {edit_bone.name}")
self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)})
def connectPhysicsBones(self):
def editor(edit_bone: bpy.types.EditBone):
def connectPhysicsBones(self) -> None:
logger.info("Connecting physics bones")
def editor(edit_bone: bpy.types.EditBone) -> None:
mmd_bone_use_connect_str: Optional[str] = edit_bone.get("mmd_bone_use_connect")
if mmd_bone_use_connect_str is None:
return
if not edit_bone.use_connect: # wasn't it overwritten?
edit_bone.use_connect = bool(mmd_bone_use_connect_str)
logger.debug(f"Connected bone: {edit_bone.name}")
del edit_bone["mmd_bone_use_connect"]
self.__editPhysicsBones(editor, {str(MODE_STATIC), str(MODE_DYNAMIC), str(MODE_DYNAMIC_BONE)})
+61 -59
View File
@@ -5,33 +5,35 @@
# 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
import re
from typing import TYPE_CHECKING, Tuple, cast
from typing import TYPE_CHECKING, Tuple, cast, List, Dict, Optional, Set, Any, Union, Iterator
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, model: "Model"):
def __init__(self, morph: Any, model: "Model"):
self.__morph = morph
self.__rig = model
@classmethod
def storeShapeKeyOrder(cls, obj, shape_key_names):
def storeShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
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, name):
def __move_to_bottom(key_blocks: bpy.types.bpy_prop_collection, name: str) -> None:
obj.active_shape_key_index = key_blocks.find(name)
bpy.ops.object.shape_key_move(type="BOTTOM")
@@ -43,7 +45,7 @@ class FnMorph:
__move_to_bottom(key_blocks, name)
@classmethod
def fixShapeKeyOrder(cls, obj, shape_key_names):
def fixShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
if len(shape_key_names) < 1:
return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
@@ -58,11 +60,11 @@ class FnMorph:
bpy.ops.object.shape_key_move(type="BOTTOM")
@staticmethod
def get_morph_slider(rig):
def get_morph_slider(rig: "Model") -> "_MorphSlider":
return _MorphSlider(rig)
@staticmethod
def category_guess(morph):
def category_guess(morph: Any) -> None:
name_lower = morph.name.lower()
if "mouth" in name_lower:
morph.category = "MOUTH"
@@ -73,7 +75,7 @@ class FnMorph:
morph.category = "EYE"
@classmethod
def load_morphs(cls, rig):
def load_morphs(cls, rig: "Model") -> None:
mmd_root = rig.rootObject().mmd_root
vertex_morphs = mmd_root.vertex_morphs
uv_morphs = mmd_root.uv_morphs
@@ -92,7 +94,7 @@ class FnMorph:
cls.category_guess(item)
@staticmethod
def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str):
def remove_shape_key(mesh_object: Object, shape_key_name: str) -> None:
assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys
@@ -104,7 +106,7 @@ class FnMorph:
FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name])
@staticmethod
def copy_shape_key(mesh_object: bpy.types.Object, src_name: str, dest_name: str):
def copy_shape_key(mesh_object: Object, src_name: str, dest_name: str) -> None:
assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys
@@ -126,13 +128,13 @@ class FnMorph:
mesh_object.active_shape_key_index = key_blocks.find(dest_name)
@staticmethod
def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes="XYZW"):
def get_uv_morph_vertex_groups(obj: Object, morph_name: Optional[str] = None, offset_axes: str = "XYZW") -> Iterator[Tuple[bpy.types.VertexGroup, str, str]]:
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, src_name, dest_name):
def copy_uv_morph_vertex_groups(obj: Object, src_name: str, dest_name: str) -> None:
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
obj.vertex_groups.remove(vg)
@@ -143,12 +145,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):
def overwrite_bone_morphs_from_action_pose(armature_object: Object) -> None:
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:
logging.warning('[WARNING] armature "%s" has no animation data or action', armature_object.name)
logger.warning('Armature "%s" has no animation data or action', armature_object.name)
return
action = armature.animation_data.action
@@ -187,9 +189,9 @@ class FnMorph:
utils.selectAObject(root)
@staticmethod
def clean_uv_morph_vertex_groups(obj):
def clean_uv_morph_vertex_groups(obj: Object) -> None:
# remove empty vertex groups of uv morphs
vg_indices = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
vg_indices: Set[int] = {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:
@@ -203,8 +205,8 @@ class FnMorph:
vertex_groups.remove(vg)
@staticmethod
def get_uv_morph_offset_map(obj, morph):
offset_map = {} # offset_map[vertex_index] = offset_xyzw
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
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)}
@@ -225,7 +227,7 @@ class FnMorph:
return offset_map
@staticmethod
def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"):
def store_uv_morph_data(obj: Object, morph: Any, offsets: Optional[List[Any]] = None, offset_axes: str = "XYZW") -> None:
vertex_groups = obj.vertex_groups
morph_name = getattr(morph, "name", None)
if offset_axes:
@@ -250,7 +252,7 @@ class FnMorph:
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=None):
def update_mat_related_mesh(self, new_mesh: Optional[Object] = None) -> None:
for offset in self.__morph.data:
# Use the new_mesh if provided
meshObj = new_mesh
@@ -270,11 +272,11 @@ class FnMorph:
offset.related_mesh = meshObj.data.name
@staticmethod
def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object):
def clean_duplicated_material_morphs(mmd_root_object: Object) -> None:
"""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, r) -> bool:
def morph_data_equals(l: Any, r: Any) -> bool:
return (
l.related_mesh_data == r.related_mesh_data
and l.offset_type == r.offset_type
@@ -290,7 +292,7 @@ class FnMorph:
and all(a == b for a, b in zip(l.toon_texture_factor, r.toon_texture_factor))
)
def morph_equals(l, r) -> bool:
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))
# Remove duplicated mmd_root.material_morphs.data[]
@@ -325,7 +327,7 @@ class _MorphSlider:
def __init__(self, model: "Model"):
self.__rig = model
def placeholder(self, create=False, binded=False):
def placeholder(self, create: bool = False, binded: bool = False) -> Optional[Object]:
rig = self.__rig
root = rig.rootObject()
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None)
@@ -343,11 +345,11 @@ class _MorphSlider:
return obj
@property
def dummy_armature(self):
def dummy_armature(self) -> Optional[Object]:
obj = self.placeholder()
return self.__dummy_armature(obj) if obj else None
def __dummy_armature(self, obj, create=False):
def __dummy_armature(self, obj: Object, create: bool = False) -> Optional[Object]:
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"))
@@ -360,7 +362,7 @@ class _MorphSlider:
FnBone.setup_special_bone_collections(arm)
return arm
def get(self, morph_name):
def get(self, morph_name: str) -> Optional[ShapeKey]:
obj = self.placeholder()
if obj is None:
return None
@@ -369,13 +371,13 @@ class _MorphSlider:
return None
return key_blocks.get(morph_name, None)
def create(self):
def create(self) -> Object:
self.__rig.loadMorphs()
obj = self.placeholder(create=True)
self.__load(obj, self.__rig.rootObject().mmd_root)
return obj
def __load(self, obj, mmd_root):
def __load(self, obj: Object, mmd_root: Any) -> None:
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", ())):
@@ -386,7 +388,7 @@ class _MorphSlider:
obj.shape_key_add(name=name, from_mix=False)
@staticmethod
def __driver_variables(id_data, path, index=-1):
def __driver_variables(id_data: Any, path: str, index: int = -1) -> Tuple[Any, Any]:
d = id_data.driver_add(path, index)
variables = d.driver.variables
for x in variables:
@@ -394,7 +396,7 @@ class _MorphSlider:
return d.driver, variables
@staticmethod
def __add_single_prop(variables, id_obj, data_path, prefix):
def __add_single_prop(variables: Any, id_obj: Object, data_path: str, prefix: str) -> Any:
var = variables.new()
var.name = f"{prefix}{len(variables)}"
var.type = "SINGLE_PROP"
@@ -405,7 +407,7 @@ class _MorphSlider:
return var
@staticmethod
def __shape_key_driver_check(key_block, resolve_path=False):
def __shape_key_driver_check(key_block: ShapeKey, resolve_path: bool = False) -> bool:
if resolve_path:
try:
key_block.id_data.path_resolve(key_block.path_from_id())
@@ -419,7 +421,7 @@ 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=None):
def __cleanup(self, names_in_use: Optional[Dict[str, Any]] = None) -> None:
from math import ceil, floor
names_in_use = names_in_use or {}
@@ -427,7 +429,7 @@ class _MorphSlider:
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[bpy.types.ShapeKey], ())):
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[ShapeKey], ())):
if kb.name in names_in_use:
continue
@@ -465,7 +467,7 @@ class _MorphSlider:
c.driver_remove(attr)
b.constraints.remove(c)
def unbind(self):
def unbind(self) -> None:
mmd_root = self.__rig.rootObject().mmd_root
# after unbind, the weird lag problem will disappear.
@@ -488,7 +490,7 @@ class _MorphSlider:
b.driver_remove("rotation_quaternion")
self.__cleanup()
def bind(self):
def bind(self) -> None:
rig = self.__rig
root = rig.rootObject()
armObj = rig.armature()
@@ -502,10 +504,10 @@ class _MorphSlider:
morph_sliders = obj.data.shape_keys.key_blocks
# data gathering
group_map = {}
group_map: Dict[Tuple[str, str], List[List[Any]]] = {}
shape_key_map = {}
uv_morph_map = {}
shape_key_map: Dict[str, List[Tuple[ShapeKey, str, List[Any]]]] = {}
uv_morph_map: Dict[str, List[Tuple[str, str, str, List[Any]]]] = {}
for mesh_object in rig.meshes():
mesh_object.show_only_shape_key = False
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
@@ -526,7 +528,7 @@ class _MorphSlider:
kb_bind.slider_max = 10
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"')
groups = []
groups: List[Any] = []
shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
@@ -542,7 +544,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
@@ -555,13 +557,13 @@ class _MorphSlider:
else:
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base"
bone_offset_map = {}
bone_offset_map: Dict[str, Tuple[str, Any, str, str, List[Any]]] = {}
with bpyutils.edit_object(arm) as data:
from .bone import FnBone
edit_bones = data.edit_bones
def __get_bone(name, parent):
def __get_bone(name: str, parent: Optional[bpy.types.EditBone]) -> bpy.types.EditBone:
b = edit_bones.get(name, None) or edit_bones.new(name=name)
b.head = (0, 0, 0)
b.tail = (0, 0, 1)
@@ -578,7 +580,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 = []
groups: List[Any] = []
bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
group_map.setdefault(("bone_morphs", m.name), []).append(groups)
@@ -589,21 +591,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 = []
groups: List[Any] = []
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 = bone_offset_map.keys() | uv_morph_map.keys()
used_bone_names: Set[str] = set(bone_offset_map.keys()) | set(uv_morph_map.keys())
used_bone_names.add(ctrl_base.name)
for b in edit_bones: # cleanup
if b.name.startswith("mmd_bind") and b.name not in used_bone_names:
edit_bones.remove(b)
material_offset_map = {}
material_offset_map: Dict[str, Any] = {}
for m in mmd_root.material_morphs:
morph_name = m.name.replace('"', '\\"')
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
groups = []
groups: List[Any] = []
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:
@@ -614,7 +616,7 @@ class _MorphSlider:
for m in mmd_root.group_morphs:
if len(m.data) != len(set(m.data.keys())):
logging.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:
@@ -625,7 +627,7 @@ class _MorphSlider:
self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys())
def __config_groups(variables, expression, groups):
def __config_groups(variables: Any, expression: str, groups: List[Any]) -> str:
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")
@@ -644,7 +646,7 @@ class _MorphSlider:
kb_bind.mute = False
# bone morphs
def __config_bone_morph(constraints, map_type, attributes, val, val_str):
def __config_bone_morph(constraints: bpy.types.ArmatureConstraints, map_type: str, attributes: Set[str], val: float, val_str: str) -> None:
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)
@@ -692,7 +694,7 @@ class _MorphSlider:
group_dict = material_offset_map.get("group_dict", {})
def __config_material_morph(mat, morph_list):
def __config_material_morph(mat: Material, morph_list: List[Tuple[str, Any, str]]) -> None:
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):
node.label, node.name = morph_name, name_bind
@@ -704,7 +706,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 == "":
logging.warning("Oh no. The material name should never empty.")
logger.warning("Oh no. The material name should never be empty.")
mul_list, add_list = [], []
else:
mat_name = "#" + mat.name
@@ -720,7 +722,7 @@ class _MorphSlider:
class MigrationFnMorph:
@staticmethod
def update_mmd_morph():
def update_mmd_morph() -> None:
from .material import FnMaterial
for root in bpy.data.objects:
@@ -762,11 +764,11 @@ class MigrationFnMorph:
morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
@staticmethod
def ensure_material_id_not_conflict():
mat_ids_set = set()
def ensure_material_id_not_conflict() -> None:
mat_ids_set: Set[int] = set()
# The reference library properties cannot be modified and bypassed in advance.
need_update_mat = []
need_update_mat: List[Material] = []
for mat in bpy.data.materials:
if mat.mmd_material.material_id < 0:
continue
@@ -781,7 +783,7 @@ class MigrationFnMorph:
mat_ids_set.add(mat.mmd_material.material_id)
@staticmethod
def compatible_with_old_version_mmd_tools():
def compatible_with_old_version_mmd_tools() -> None:
MigrationFnMorph.ensure_material_id_not_conflict()
for root in bpy.data.objects:
File diff suppressed because it is too large Load Diff
+35 -12
View File
@@ -5,12 +5,13 @@
# 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.
from typing import List, Optional
from typing import List, Optional, Tuple, Union, Dict, Any, Set, cast
import bpy
from mathutils import Euler, Vector
from mathutils import Euler, Vector, Matrix
from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
SHAPE_SPHERE = 0
SHAPE_BOX = 1
@@ -21,25 +22,30 @@ MODE_DYNAMIC = 1
MODE_DYNAMIC_BONE = 2
def shapeType(collision_shape):
def shapeType(collision_shape: str) -> int:
"""Convert collision shape name to type index"""
return ("SPHERE", "BOX", "CAPSULE").index(collision_shape)
def collisionShape(shape_type):
def collisionShape(shape_type: int) -> str:
"""Convert shape type index to collision shape name"""
return ("SPHERE", "BOX", "CAPSULE")[shape_type]
def setRigidBodyWorldEnabled(enable):
def setRigidBodyWorldEnabled(enable: bool) -> bool:
"""Enable or disable the rigid body world and return previous state"""
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 = [
COLORS: List[int] = [
0x7FDDD4,
0xF0E68C,
0xEE82EE,
@@ -59,10 +65,12 @@ class RigidBodyMaterial:
]
@classmethod
def getMaterial(cls, number):
def getMaterial(cls, number: int) -> bpy.types.Material:
"""Get or create a material for rigid bodies with the specified number"""
number = int(number)
material_name = "mmd_tools_rigid_%d" % (number)
material_name = f"mmd_tools_rigid_{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)]
@@ -89,9 +97,11 @@ 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:
@@ -101,6 +111,8 @@ 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"
@@ -118,11 +130,11 @@ class FnRigidBody:
@staticmethod
def setup_rigid_body_object(
obj: bpy.types.Object,
shape_type: str,
shape_type: int,
location: Vector,
rotation: Euler,
size: Vector,
dynamics_type: str,
dynamics_type: int,
collision_group_number: Optional[int] = None,
collision_group_mask: Optional[List[bool]] = None,
name: Optional[str] = None,
@@ -134,6 +146,8 @@ 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
@@ -175,7 +189,8 @@ class FnRigidBody:
return obj
@staticmethod
def get_rigid_body_size(obj: bpy.types.Object):
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"""
assert obj.mmd_type == "RIGID_BODY"
x0, y0, z0 = obj.bound_box[0]
@@ -195,10 +210,14 @@ class FnRigidBody:
height = abs((z1 - z0) - diameter)
return (radius, height, 0.0)
else:
raise ValueError(f"Invalid shape type: {shape}")
error_msg = f"Invalid shape type: {shape}"
logger.error(error_msg)
raise ValueError(error_msg)
@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"
@@ -230,9 +249,11 @@ 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:
@@ -256,6 +277,8 @@ 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 -29
View File
@@ -7,14 +7,19 @@
import logging
import time
from typing import Dict, List, Tuple, Set, Optional, Any, Union, cast, TypeVar, Callable
import bpy
from mathutils import Matrix, Vector
import numpy as np
from mathutils import Matrix, Vector, Quaternion, Euler
from bpy.types import Object, PoseBone, Pose, ShapeKey, Modifier, VertexGroup
from ..bpyutils import FnObject
from ....core.logging_setup import logger
T = TypeVar('T')
def _hash(v):
def _hash(v: Union[Object, PoseBone, Pose]) -> int:
if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
return hash(type(v).__name__ + v.name)
elif isinstance(v, bpy.types.Pose):
@@ -24,23 +29,24 @@ def _hash(v):
class FnSDEF:
g_verts = {} # global cache
g_shapekey_data = {}
g_bone_check = {}
__g_armature_check = {}
SHAPEKEY_NAME = "mmd_sdef_skinning"
MASK_NAME = "mmd_sdef_mask"
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"
def __init__(self):
def __init__(self) -> None:
raise NotImplementedError("not allowed")
@classmethod
def __init_cache(cls, obj, shapekey):
def __init_cache(cls, obj: Object, shapekey: ShapeKey) -> bool:
key = _hash(obj)
obj = getattr(obj, "original", obj)
mod = obj.modifiers.get("mmd_bone_order_override")
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
@@ -49,7 +55,7 @@ class FnSDEF:
return False
@classmethod
def __check_bone_update(cls, obj, bone0, bone1):
def __check_bone_update(cls, obj: Object, bone0: PoseBone, bone1: PoseBone) -> bool:
check = cls.g_bone_check[_hash(obj)]
key = (_hash(bone0), _hash(bone1))
if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
@@ -58,17 +64,18 @@ class FnSDEF:
return False
@classmethod
def mute_sdef_set(cls, obj, mute):
def mute_sdef_set(cls, obj: Object, mute: bool) -> None:
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, shapekey):
def __sdef_muted(cls, obj: Object, shapekey: ShapeKey) -> bool:
mute = shapekey.mute
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
mod = obj.modifiers.get("mmd_bone_order_override")
@@ -80,10 +87,11 @@ 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):
def has_sdef_data(obj: Object) -> bool:
mod = obj.modifiers.get("mmd_bone_order_override")
if mod and mod.type == "ARMATURE" and mod.object:
kb = getattr(obj.data.shape_keys, "key_blocks", None)
@@ -91,18 +99,21 @@ class FnSDEF:
return False
@classmethod
def __find_vertices(cls, obj):
def __find_vertices(cls, obj: Object) -> Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]:
if not cls.has_sdef_data(obj):
return {}
vertices = {}
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 = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in 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}
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
@@ -125,16 +136,19 @@ 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, bulk_update, use_skip, use_scale):
def driver_function_wrap(cls, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
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, obj_name, bulk_update, use_skip, use_scale):
def driver_function(cls, shapekey: ShapeKey, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
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
@@ -206,11 +220,11 @@ class FnSDEF:
rot1 = -rot1
s0, s1 = mat0.to_scale(), mat1.to_scale()
def scale(mat_rot, w0, w1):
def scale(mat_rot: Matrix, w0: float, w1: float) -> Matrix:
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, pos_c, vid):
def offset(mat_rot: Matrix, pos_c: Vector, vid: int) -> Vector:
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
@@ -233,16 +247,19 @@ class FnSDEF:
return 1.0 # shapkey value
@classmethod
def register_driver_function(cls):
def register_driver_function(cls) -> None:
"""Register driver functions in Blender's driver namespace."""
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 = 10
BENCH_LOOP: int = 10
@classmethod
def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip):
def __get_benchmark_result(cls, obj: Object, shapkey: ShapeKey, use_scale: bool, use_skip: bool) -> bool:
# 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)
@@ -256,14 +273,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
logging.info("FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s", default_time, bulk_time, result)
logger.info(f"SDEF benchmark for {obj.name}: default {default_time:.4f}s vs bulk_update {bulk_time:.4f}s => bulk_update={result}")
return result
@classmethod
def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False):
def bind(cls, obj: Object, bulk_update: Optional[bool] = None, use_skip: bool = True, use_scale: bool = False) -> bool:
# Unbind first
cls.unbind(obj)
if not cls.has_sdef_data(obj):
logger.debug(f"Object {obj.name} does not have SDEF data")
return False
# Create the shapekey for the driver
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
@@ -294,32 +312,38 @@ class FnSDEF:
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}")
return True
@classmethod
def unbind(cls, obj):
def unbind(cls, obj: Object) -> None:
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=None, unused_only=False):
def clear_cache(cls, obj: Optional[Object] = None, unused_only: bool = False) -> None:
if unused_only:
valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj)
for key in cls.g_verts.keys() - valid_keys:
removed_keys = cls.g_verts.keys() - valid_keys
for key in removed_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:
@@ -328,7 +352,9 @@ 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 = {}
+56 -36
View File
@@ -5,25 +5,33 @@
# 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.
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: bpy.types.ShaderNodeTree):
def __init__(self, shader: ShaderNodeTree):
self.shader = shader
self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore
self.nodes: bpy.types.bpy_prop_collection[ShaderNode] = shader.nodes # type: ignore
self.links = shader.links
def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]:
def _find_node(self, node_type: str) -> Optional[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]) -> bpy.types.ShaderNode:
node: bpy.types.ShaderNode = self.nodes.new(idname)
def new_node(self, idname: str, pos: Tuple[int, int]) -> ShaderNode:
node: ShaderNode = self.nodes.new(idname)
node.location = (pos[0] * 210, pos[1] * 220)
return node
def new_math_node(self, operation, pos, value1=None, value2=None):
def new_math_node(self, operation: str, pos: Tuple[int, int], value1: Optional[float] = None, value2: Optional[float] = None) -> ShaderNode:
node = self.new_node("ShaderNodeMath", pos)
node.operation = operation
if value1 is not None:
@@ -32,7 +40,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = value2
return node
def new_vector_math_node(self, operation, pos, vector1=None, vector2=None):
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:
node = self.new_node("ShaderNodeVectorMath", pos)
node.operation = operation
if vector1 is not None:
@@ -41,7 +49,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = vector2
return node
def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None):
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:
node = self.new_node("ShaderNodeMixRGB", pos)
node.blend_type = blend_type
if fac is not None:
@@ -53,30 +61,30 @@ class _NodeTreeUtils:
return node
SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"}
SOCKET_TYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "NodeSocketFloat"}
SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"}
SOCKET_SUBTYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "FACTOR"}
class _NodeGroupUtils(_NodeTreeUtils):
def __init__(self, shader: bpy.types.ShaderNodeTree):
def __init__(self, shader: ShaderNodeTree):
super().__init__(shader)
self.__node_input: Optional[bpy.types.NodeGroupInput] = None
self.__node_output: Optional[bpy.types.NodeGroupOutput] = None
self.__node_input: Optional[NodeGroupInput] = None
self.__node_output: Optional[NodeGroupOutput] = None
@property
def node_input(self) -> bpy.types.NodeGroupInput:
def node_input(self) -> NodeGroupInput:
if not self.__node_input:
self.__node_input = cast(bpy.types.NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
self.__node_input = cast(NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
return self.__node_input
@property
def node_output(self) -> bpy.types.NodeGroupOutput:
def node_output(self) -> NodeGroupOutput:
if not self.__node_output:
self.__node_output = cast(bpy.types.NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
self.__node_output = cast(NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
return self.__node_output
def hide_nodes(self, hide_sockets=True):
def hide_nodes(self, hide_sockets: bool = True) -> None:
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
@@ -87,15 +95,15 @@ class _NodeGroupUtils(_NodeTreeUtils):
for s in n.outputs:
s.hide = not s.is_linked
def new_input_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
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:
self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type)
def new_output_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
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:
self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type)
def __new_io(self, in_out, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None):
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:
if io_name not in io_sockets:
idname = socket_type or socket.bl_idname
idname = socket_type or (socket.bl_idname if socket else "NodeSocketFloat")
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, "")
@@ -114,14 +122,18 @@ class _NodeGroupUtils(_NodeTreeUtils):
class _MaterialMorph:
@classmethod
def update_morph_inputs(cls, material, morph):
def update_morph_inputs(cls, material: Optional[Material], morph: Any) -> None:
"""Update material morph inputs based on morph data"""
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, morphs):
def setup_morph_nodes(cls, material: Material, morphs: List[Any]) -> List[ShaderNode]:
"""Set up morph nodes for a material"""
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)
@@ -137,23 +149,25 @@ class _MaterialMorph:
return nodes
@classmethod
def reset_morph_links(cls, node):
def reset_morph_links(cls, node: ShaderNode) -> None:
"""Reset morph links for a node"""
logger.debug(f"Resetting morph links for {node.name}")
cls.__update_morph_links(node, reset=True)
@classmethod
def __update_morph_links(cls, node, reset=False):
def __update_morph_links(cls, node: ShaderNode, reset: bool = False) -> None:
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):
return
def __init_link(socket_morph, socket_shader):
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
if socket_shader and socket_morph.is_linked:
links.new(socket_morph.links[0].from_socket, socket_shader)
else:
def __init_link(socket_morph, socket_shader):
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
if socket_shader:
if socket_shader.is_linked:
links.new(socket_shader.links[0].from_socket, socket_morph)
@@ -178,7 +192,8 @@ class _MaterialMorph:
__init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
@classmethod
def __update_node_inputs(cls, node, morph):
def __update_node_inputs(cls, node: ShaderNode, morph: Any) -> None:
"""Update node inputs based on morph data"""
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]
@@ -196,7 +211,8 @@ class _MaterialMorph:
node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
@classmethod
def __morph_node_add(cls, material, morph, prev_node):
def __morph_node_add(cls, material: Material, morph: Optional[Any], prev_node: Optional[ShaderNode]) -> Optional[ShaderNode]:
"""Add a morph node to a material"""
nodes, links = material.node_tree.nodes, material.node_tree.links
shader = nodes.get("mmd_shader", None)
@@ -221,8 +237,9 @@ 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, socket_in):
def __soft_link(socket_out: Optional[bpy.types.NodeSocket], socket_in: Optional[bpy.types.NodeSocket]) -> None:
if socket_out and socket_in:
links.new(socket_out, socket_in)
@@ -244,12 +261,14 @@ class _MaterialMorph:
return shader
@classmethod
def __get_shader(cls, morph_type):
def __get_shader(cls, morph_type: str) -> ShaderNodeTree:
"""Get or create a shader node group for the specified 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
@@ -260,7 +279,7 @@ class _MaterialMorph:
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
ng.new_node("NodeGroupOutput", (3, 0))
def __blend_color_add(id_name, pos, tag=""):
def __blend_color_add(id_name: str, pos: Tuple[int, int], tag: str = "") -> ShaderNode:
# 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
@@ -271,7 +290,7 @@ class _MaterialMorph:
ng.new_output_socket(id_name + tag, node_mix.outputs["Color"])
return node_mix
def __blend_tex_color(id_name, pos, node_tex_rgb, node_tex_a_output):
def __blend_tex_color(id_name: str, pos: Tuple[int, int], node_tex_rgb: ShaderNode, node_tex_a_output: bpy.types.NodeSocket) -> None:
# Tex Color = tex_rgb * tex_a + (1 - tex_a)
# : tex_rgb = TexRGB * ColorMul + ColorAdd
# : tex_a = TexA * ValueMul + ValueAdd
@@ -294,7 +313,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, input1, input2, output, tag=""):
def __add_sockets(id_name: str, input1: bpy.types.NodeSocket, input2: bpy.types.NodeSocket, output: bpy.types.NodeSocket, tag: str = "") -> None:
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)
@@ -343,4 +362,5 @@ 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
+46 -29
View File
@@ -5,39 +5,44 @@
# 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.
from typing import Iterable, Optional
from typing import Iterable, Optional, Any, List, Tuple, Union
import bpy
from bpy.types import Material, NodeTree, Node, NodeSocket, ShaderNodeGroup, ShaderNodeOutputMaterial, NodeLink
from ..logging_setup import logger
from .core.shader import _NodeGroupUtils
from .core.material import FnMaterial
def __switchToCyclesRenderEngine():
def __switchToCyclesRenderEngine() -> None:
if bpy.context.scene.render.engine != "CYCLES":
logger.debug("Switching render engine to Cycles")
bpy.context.scene.render.engine = "CYCLES"
def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader):
def __exposeNodeTreeInput(in_socket: NodeSocket, name: str, default_value: Any, node_input: Node, shader: NodeTree) -> None:
_NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value)
def __exposeNodeTreeOutput(out_socket, name, node_output, shader):
def __exposeNodeTreeOutput(out_socket: NodeSocket, name: str, node_output: Node, shader: NodeTree) -> None:
_NodeGroupUtils(shader).new_output_socket(name, out_socket)
def __getMaterialOutput(nodes, bl_idname):
def __getMaterialOutput(nodes: bpy.types.Nodes, bl_idname: str) -> Node:
o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname)
o.is_active_output = True
return o
def create_MMDAlphaShader():
def create_MMDAlphaShader() -> NodeTree:
__switchToCyclesRenderEngine()
if "MMDAlphaShader" in bpy.data.node_groups:
logger.debug("Using existing MMDAlphaShader node group")
return bpy.data.node_groups["MMDAlphaShader"]
logger.info("Creating new MMDAlphaShader node group")
shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree")
node_input = shader.nodes.new("NodeGroupInput")
@@ -59,26 +64,28 @@ def create_MMDAlphaShader():
return shader
def create_MMDBasicShader():
def create_MMDBasicShader() -> NodeTree:
__switchToCyclesRenderEngine()
if "MMDBasicShader" in bpy.data.node_groups:
logger.debug("Using existing MMDBasicShader node group")
return bpy.data.node_groups["MMDBasicShader"]
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
logger.info("Creating new MMDBasicShader node group")
shader: NodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput")
node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput")
node_input: Node = shader.nodes.new("NodeGroupInput")
node_output: Node = shader.nodes.new("NodeGroupOutput")
node_output.location.x += 250
node_input.location.x -= 500
dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse")
dif: Node = shader.nodes.new("ShaderNodeBsdfDiffuse")
dif.location.x -= 250
dif.location.y += 150
glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic")
glo: Node = shader.nodes.new("ShaderNodeBsdfAnisotropic")
glo.location.x -= 250
glo.location.y -= 150
mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader")
mix: Node = shader.nodes.new("ShaderNodeMixShader")
shader.links.new(mix.inputs[1], dif.outputs["BSDF"])
shader.links.new(mix.inputs[2], glo.outputs["BSDF"])
@@ -91,7 +98,7 @@ def create_MMDBasicShader():
return shader
def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]:
def __enum_linked_nodes(node: Node) -> Iterable[Node]:
yield node
if node.parent:
yield node.parent
@@ -99,7 +106,8 @@ def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]:
yield from __enum_linked_nodes(n)
def __cleanNodeTree(material: bpy.types.Material):
def __cleanNodeTree(material: Material) -> None:
logger.debug(f"Cleaning node tree for material: {material.name}")
nodes = material.node_tree.nodes
node_names = set(n.name for n in nodes)
for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}):
@@ -109,40 +117,46 @@ def __cleanNodeTree(material: bpy.types.Material):
nodes.remove(nodes[name])
def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
def convertToCyclesShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None:
logger.info(f"Converting {obj.name} to Cycles shader (use_principled={use_principled}, clean_nodes={clean_nodes})")
__switchToCyclesRenderEngine()
convertToBlenderShader(obj, use_principled, clean_nodes, subsurface)
def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
def convertToBlenderShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None:
for i in obj.material_slots:
if not i.material:
continue
if not i.material.use_nodes:
logger.debug(f"Enabling nodes for material: {i.material.name}")
i.material.use_nodes = True
__convertToMMDBasicShader(i.material)
if use_principled:
logger.debug(f"Converting material to Principled BSDF: {i.material.name}")
__convertToPrincipledBsdf(i.material, subsurface)
if clean_nodes:
__cleanNodeTree(i.material)
def convertToMMDShader(obj):
def convertToMMDShader(obj: bpy.types.Object) -> None:
"""BSDF -> MMDShaderDev conversion."""
logger.info(f"Converting {obj.name} to MMD shader")
for i in obj.material_slots:
if not i.material:
continue
if not i.material.use_nodes:
logger.debug(f"Enabling nodes for material: {i.material.name}")
i.material.use_nodes = True
FnMaterial.convert_to_mmd_material(i.material)
def __convertToMMDBasicShader(material: bpy.types.Material):
def __convertToMMDBasicShader(material: Material) -> None:
logger.debug(f"Converting material to MMD Basic Shader: {material.name}")
# TODO: test me
mmd_basic_shader_grp = create_MMDBasicShader()
mmd_alpha_shader_grp = create_MMDAlphaShader()
if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)):
if not any(filter(lambda x: isinstance(x, ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)):
# Add nodes for Cycles Render
shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
shader.node_tree = mmd_basic_shader_grp
shader.inputs[0].default_value[:3] = material.diffuse_color[:3]
shader.inputs[1].default_value[:3] = material.specular_color[:3]
@@ -157,7 +171,8 @@ def __convertToMMDBasicShader(material: bpy.types.Material):
alpha_value = material.diffuse_color[3]
if alpha_value < 1.0:
alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
logger.debug(f"Material has alpha: {material.name}, alpha={alpha_value}")
alpha_shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
alpha_shader.location.x = shader.location.x + 250
alpha_shader.location.y = shader.location.y - 150
alpha_shader.node_tree = mmd_alpha_shader_grp
@@ -165,21 +180,22 @@ def __convertToMMDBasicShader(material: bpy.types.Material):
material.node_tree.links.new(alpha_shader.inputs[0], outplug)
outplug = alpha_shader.outputs[0]
material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial")
material_output: ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial")
material.node_tree.links.new(material_output.inputs["Surface"], outplug)
material_output.location.x = shader.location.x + 500
material_output.location.y = shader.location.y - 150
def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float):
def __convertToPrincipledBsdf(material: Material, subsurface: float) -> None:
logger.debug(f"Converting material to Principled BSDF: {material.name}")
node_names = set()
for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)):
for s in (n for n in material.node_tree.nodes if isinstance(n, ShaderNodeGroup)):
if s.node_tree.name == "MMDBasicShader":
l: bpy.types.NodeLink
l: NodeLink
for l in s.outputs[0].links:
to_node = l.to_node
# assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader
if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader":
if isinstance(to_node, ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader":
__switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node)
node_names.add(to_node.name)
else:
@@ -194,8 +210,9 @@ def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float):
nodes.remove(nodes[name])
def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None):
shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, subsurface: float, node_alpha: Optional[ShaderNodeGroup] = None) -> None:
logger.debug(f"Switching to Principled BSDF: {node_basic.name}")
shader: Node = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
shader.parent = node_basic.parent
shader.location.x = node_basic.location.x
shader.location.y = node_basic.location.y
+82 -38
View File
@@ -6,13 +6,16 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator
from bpy.props import BoolProperty, StringProperty, FloatProperty
from bpy.types import Operator, Context, Object, Material
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
from .. import cycles_converter
from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.shader import _NodeGroupUtils
from ....core.logging_setup import logger
class ConvertMaterialsForCycles(Operator):
@@ -21,14 +24,14 @@ class ConvertMaterialsForCycles(Operator):
bl_description = "Convert materials of selected objects for Cycles."
bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.BoolProperty(
use_principled: BoolProperty(
name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=False,
options={"SKIP_SAVE"},
)
clean_nodes: bpy.props.BoolProperty(
clean_nodes: BoolProperty(
name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=False,
@@ -36,22 +39,27 @@ class ConvertMaterialsForCycles(Operator):
)
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == "MESH"), None)
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
def draw(self, context):
def draw(self, context: Context) -> None:
layout = self.layout
layout.prop(self, "use_principled")
layout.prop(self, "clean_nodes")
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
try:
context.scene.render.engine = "CYCLES"
except:
except Exception as e:
logger.error(f"Failed to change to Cycles render engine: {str(e)}")
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
return {"CANCELLED"}
logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}")
for obj in (x for x in context.selected_objects if x.type == "MESH"):
logger.debug(f"Converting materials for object: {obj.name}")
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
return {"FINISHED"}
@@ -61,21 +69,21 @@ class ConvertMaterials(Operator):
bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.BoolProperty(
use_principled: BoolProperty(
name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=True,
options={"SKIP_SAVE"},
)
clean_nodes: bpy.props.BoolProperty(
clean_nodes: BoolProperty(
name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=True,
options={"SKIP_SAVE"},
)
subsurface: bpy.props.FloatProperty(
subsurface: FloatProperty(
name="Subsurface",
default=0.001,
soft_min=0.000,
@@ -85,13 +93,15 @@ class ConvertMaterials(Operator):
)
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == "MESH"), None)
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}")
for obj in context.selected_objects:
if obj.type != "MESH":
continue
logger.debug(f"Converting materials for object: {obj.name}")
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
return {"FINISHED"}
@@ -102,20 +112,22 @@ class ConvertBSDFMaterials(Operator):
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == 'MESH'), None)
def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
logger.info("Converting BSDF materials to MMD shader")
for obj in context.selected_objects:
if obj.type != 'MESH':
continue
logger.debug(f"Converting BSDF materials for object: {obj.name}")
cycles_converter.convertToMMDShader(obj)
return {'FINISHED'}
class _OpenTextureBase:
"""Create a texture for mmd model material."""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
filepath: StringProperty(
name="File Path",
@@ -129,7 +141,7 @@ class _OpenTextureBase:
options={"HIDDEN"},
)
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
@@ -139,8 +151,13 @@ class OpenTexture(Operator, _OpenTextureBase):
bl_label = "Open Texture"
bl_description = "Create main texture of active material"
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Creating texture for material: {mat.name} from {self.filepath}")
fnMat = FnMaterial(mat)
fnMat.create_texture(self.filepath)
return {"FINISHED"}
@@ -154,8 +171,13 @@ class RemoveTexture(Operator):
bl_description = "Remove main texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Removing texture from material: {mat.name}")
fnMat = FnMaterial(mat)
fnMat.remove_texture()
return {"FINISHED"}
@@ -168,8 +190,13 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
bl_label = "Open Sphere Texture"
bl_description = "Create sphere texture of active material"
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}")
fnMat = FnMaterial(mat)
fnMat.create_sphere_texture(self.filepath, context.active_object)
return {"FINISHED"}
@@ -183,8 +210,13 @@ class RemoveSphereTexture(Operator):
bl_description = "Remove sphere texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Removing sphere texture from material: {mat.name}")
fnMat = FnMaterial(mat)
fnMat.remove_sphere_texture()
return {"FINISHED"}
@@ -197,18 +229,21 @@ class MoveMaterialUp(Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
return valid_mesh and obj.active_material_index > 0
return bool(valid_mesh and obj.active_material_index > 0)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
current_idx = obj.active_material_index
prev_index = current_idx - 1
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
try:
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"}
obj.active_material_index = prev_index
@@ -223,18 +258,21 @@ class MoveMaterialDown(Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
return valid_mesh and obj.active_material_index < len(obj.material_slots) - 1
return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
current_idx = obj.active_material_index
next_index = current_idx + 1
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
try:
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"}
obj.active_material_index = next_index
@@ -257,26 +295,31 @@ class EdgePreviewSetup(Operator):
default="CREATE",
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
from ..core.model import FnModel
root = FnModel.find_root_object(context.active_object)
if root is None:
logger.error("No MMD model root found")
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
if self.action == "CLEAN":
logger.info(f"Cleaning toon edge for model: {root.name}")
for obj in FnModel.iterate_mesh_objects(root):
self.__clean_toon_edge(obj)
else:
from ..bpyutils import Props
logger.info(f"Creating toon edge for model: {root.name}")
scale = 0.2 * getattr(root, Props.empty_display_size)
counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
logger.info(f"Created {counts} toon edge(s)")
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
return {"FINISHED"}
def __clean_toon_edge(self, obj):
def __clean_toon_edge(self, obj: Object) -> None:
logger.debug(f"Cleaning toon edge for object: {obj.name}")
if "mmd_edge_preview" in obj.modifiers:
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
@@ -285,7 +328,8 @@ class EdgePreviewSetup(Operator):
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
def __create_toon_edge(self, obj, scale=1.0):
def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int:
logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}")
self.__clean_toon_edge(obj)
materials = obj.data.materials
material_offset = len(materials)
@@ -310,10 +354,10 @@ class EdgePreviewSetup(Operator):
mod.vertex_group = "mmd_edge_preview"
return len(materials) - material_offset
def __create_edge_preview_group(self, obj):
def __create_edge_preview_group(self, obj: Object) -> None:
vertices, materials = obj.data.vertices, obj.data.materials
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
scale_map = {}
scale_map: Dict[int, float] = {}
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
if vg_scale_index >= 0:
scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
@@ -322,7 +366,7 @@ class EdgePreviewSetup(Operator):
weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")
def __get_edge_material(self, mat_name, edge_color, materials):
def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
if mat_name in materials:
return materials[mat_name]
mat = bpy.data.materials.get(mat_name, None)
@@ -340,7 +384,7 @@ class EdgePreviewSetup(Operator):
self.__make_shader(mat)
return mat
def __make_shader(self, m):
def __make_shader(self, m: Material) -> None:
m.use_nodes = True
nodes, links = m.node_tree.nodes, m.node_tree.links
@@ -361,7 +405,7 @@ class EdgePreviewSetup(Operator):
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
def __get_edge_preview_shader(self):
def __get_edge_preview_shader(self) -> bpy.types.NodeTree:
group_name = "MMDEdgePreview"
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
+56 -27
View File
@@ -6,14 +6,17 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
import bpy
from bpy.types import Context, Object, Operator, ShapeKey
from .. import utils
from ..bpyutils import FnContext, FnObject
from ..core.bone import FnBone
from ..core.model import FnModel, Model
from ..core.morph import FnMorph
from ....core.logging_setup import logger
class SelectObject(bpy.types.Operator):
@@ -29,7 +32,8 @@ class SelectObject(bpy.types.Operator):
options={"HIDDEN", "SKIP_SAVE"},
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
logger.debug(f"Selecting object: {self.name}")
utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"}
@@ -43,41 +47,43 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
@classmethod
def set_index(cls, obj, index):
def set_index(cls, obj: Object, index: int) -> None:
m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
@classmethod
def get_name(cls, obj, prefix=None):
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name
return name[len(prefix) :] if prefix and name.startswith(prefix) else name
@classmethod
def normalize_indices(cls, objects):
def normalize_indices(cls, objects: List[Object]) -> None:
for i, x in enumerate(objects):
cls.set_index(x, i)
@classmethod
def poll(cls, context):
return context.active_object
def poll(cls, context: Context) -> bool:
return context.active_object is not None
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
objects = self.__get_objects(obj)
if obj not in objects:
self.report({"ERROR"}, 'Can not move object "%s"' % obj.name)
logger.error(f'Cannot move object "{obj.name}"')
self.report({"ERROR"}, f'Can not move object "{obj.name}"')
return {"CANCELLED"}
objects.sort(key=lambda x: x.name)
logger.debug(f"Moving object {obj.name} {self.type}")
self.move(objects, objects.index(obj), self.type)
self.normalize_indices(objects)
return {"FINISHED"}
def __get_objects(self, obj):
def __get_objects(self, obj: Object) -> Any:
class __MovableList(list):
def move(self, index_old, index_new):
def move(self, index_old: int, index_new: int) -> None:
item = self[index_old]
self.remove(item)
self.insert(index_new, item)
@@ -102,11 +108,11 @@ class CleanShapeKeys(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod
def __can_remove(key_block):
def __can_remove(key_block: ShapeKey) -> bool:
if key_block.relative_key == key_block:
return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
@@ -114,20 +120,24 @@ class CleanShapeKeys(bpy.types.Operator):
return False
return True
def __shape_key_clean(self, obj, key_blocks):
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
for kb in key_blocks:
if self.__can_remove(kb):
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, kb)
if len(key_blocks) == 1:
logger.debug(f"Removing single shape key: {key_blocks[0].name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, key_blocks[0])
def execute(self, context):
obj: bpy.types.Object
def execute(self, context: Context) -> Set[str]:
logger.info("Cleaning shape keys for selected objects")
obj: Object
for obj in context.selected_objects:
if obj.type != "MESH" or obj.data.shape_keys is None:
continue
if not obj.data.shape_keys.use_relative:
continue # not be considered yet
logger.debug(f"Processing shape keys for {obj.name}")
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
return {"FINISHED"}
@@ -144,21 +154,25 @@ class SeparateByMaterials(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
return obj and obj.type == "MESH"
def __separate_by_materials(self, obj):
def __separate_by_materials(self, obj: Object) -> None:
logger.info(f"Separating {obj.name} by materials")
utils.separateByMaterials(obj)
if self.clean_shape_keys:
logger.debug("Cleaning shape keys after separation")
bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
logger.debug("No root object found, separating single object")
self.__separate_by_materials(obj)
else:
logger.debug(f"Root object found: {root.name}, preparing for separation")
bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -171,9 +185,11 @@ class SeparateByMaterials(bpy.types.Operator):
if len(mesh.data.materials) > 0:
mat = mesh.data.materials[0]
idx = mat_names.index(getattr(mat, "name", None))
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
MoveObject.set_index(mesh, idx)
for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh()
utils.clearUnusedMeshes()
return {"FINISHED"}
@@ -191,13 +207,15 @@ class JoinMeshes(bpy.types.Operator):
default=True,
)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
logger.info(f"Joining meshes for model: {root.name}")
bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -205,9 +223,11 @@ class JoinMeshes(bpy.types.Operator):
rig = Model(root)
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
if not meshes_list:
logger.error("No meshes found in the model")
self.report({"ERROR"}, "The model does not have any meshes")
return {"CANCELLED"}
active_mesh = meshes_list[0]
logger.debug(f"Found {len(meshes_list)} meshes, using {active_mesh.name} as active")
FnContext.select_objects(context, *meshes_list)
FnContext.set_active_object(context, active_mesh)
@@ -216,15 +236,19 @@ class JoinMeshes(bpy.types.Operator):
for m in meshes_list[1:]:
for mat in m.data.materials:
if mat not in active_mesh.data.materials[:]:
logger.debug(f"Adding material {mat.name} to active mesh")
active_mesh.data.materials.append(mat)
# Join selected meshes
logger.debug("Joining meshes")
bpy.ops.object.join()
if self.sort_shape_keys:
logger.debug("Sorting shape keys")
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
active_mesh.active_shape_key_index = 0
for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
utils.clearUnusedMeshes()
return {"FINISHED"}
@@ -238,17 +262,20 @@ class AttachMeshesToMMD(bpy.types.Operator):
add_armature_modifier: bpy.props.BoolProperty(default=True)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
root = FnModel.find_root_object(context.active_object)
if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
armObj = FnModel.find_armature_object(root)
if armObj is None:
logger.error("Model armature not found")
self.report({"ERROR"}, "Model Armature not found")
return {"CANCELLED"}
logger.info(f"Attaching meshes to model: {root.name}")
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
return {"FINISHED"}
@@ -268,17 +295,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object)
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
vm = context.window_manager
return vm.invoke_props_dialog(self)
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object)
logger.info(f"Changing IK loop factor to {self.mmd_ik_loop_factor} for model: {root_object.name}")
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
return {"FINISHED"}
@@ -290,21 +318,22 @@ class RecalculateBoneRoll(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
obj = context.active_object
return obj and obj.type == "ARMATURE"
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
def draw(self, context):
def draw(self, context: Context) -> None:
layout = self.layout
c = layout.column()
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
c.label(text="Click [OK] to run the operation.")
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
arm = context.active_object
logger.info(f"Recalculating bone roll for armature: {arm.name}")
FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"}
+61 -24
View File
@@ -6,10 +6,12 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from typing import Optional, Set, Dict, Any, List, Tuple, Union
from ..bpyutils import FnContext
from ..core.bone import FnBone, MigrationFnBone
from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MorphSliderSetup(bpy.types.Operator):
@@ -29,18 +31,22 @@ class MorphSliderSetup(bpy.types.Operator):
default="CREATE",
)
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.debug(f"Executing MorphSliderSetup with type: {self.type}")
with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object)
if self.type == "BIND":
logger.info(f"Binding morph sliders for {root_object.name}")
rig.morph_slider.bind()
elif self.type == "UNBIND":
logger.info(f"Unbinding morph sliders for {root_object.name}")
rig.morph_slider.unbind()
else:
logger.info(f"Creating morph sliders for {root_object.name}")
rig.morph_slider.create()
FnContext.set_active_object(context, active_object)
@@ -53,10 +59,11 @@ class CleanRiggingObjects(bpy.types.Operator):
bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object)
assert root_object is not None
logger.info(f"Cleaning rig for {root_object.name}")
rig = Model(root_object)
rig.clean()
FnContext.set_active_object(context, root_object)
@@ -86,9 +93,10 @@ class BuildRig(bpy.types.Operator):
default=1e-06,
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object)
logger.info(f"Building rig for {root_object.name} with non_collision_distance_scale={self.non_collision_distance_scale}, collision_margin={self.collision_margin}")
with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object)
rig.build(self.non_collision_distance_scale, self.collision_margin)
@@ -103,11 +111,14 @@ class CleanAdditionalTransformConstraints(bpy.types.Operator):
bl_description = "Delete shadow bones of selected object and revert bones to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
FnBone.clean_additional_transformation(FnModel.find_armature_object(root_object))
logger.info(f"Cleaning additional transform constraints for {root_object.name}")
armature_object = FnModel.find_armature_object(root_object)
FnBone.clean_additional_transformation(armature_object)
FnContext.set_active_object(context, active_object)
return {"FINISHED"}
@@ -118,11 +129,12 @@ class ApplyAdditionalTransformConstraints(bpy.types.Operator):
bl_description = "Translate appended bones of selected object for Blender"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.info(f"Applying additional transform constraints for {root_object.name}")
armature_object = FnModel.find_armature_object(root_object)
assert armature_object is not None
@@ -149,12 +161,14 @@ class SetupBoneFixedAxes(bpy.types.Operator):
default="LOAD",
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object")
logger.error("Setup Bone Fixed Axis failed: Active object is not an armature object")
return {"CANCELLED"}
logger.info(f"Setting up bone fixed axes with type: {self.type}")
if self.type == "APPLY":
FnBone.apply_bone_fixed_axis(armature_object)
FnBone.apply_additional_transformation(armature_object)
@@ -180,12 +194,14 @@ class SetupBoneLocalAxes(bpy.types.Operator):
default="LOAD",
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object")
logger.error("Setup Bone Local Axes failed: Active object is not an armature object")
return {"CANCELLED"}
logger.info(f"Setting up bone local axes with type: {self.type}")
if self.type == "APPLY":
FnBone.apply_bone_local_axes(armature_object)
FnBone.apply_additional_transformation(armature_object)
@@ -207,16 +223,18 @@ class AddMissingVertexGroupsFromBones(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object: bpy.types.Object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.info(f"Adding missing vertex groups from bones for {root_object.name}, search_in_all_meshes={self.search_in_all_meshes}")
bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object)
if bone_order_mesh_object is None:
logger.error("Failed to find bone order mesh object")
return {"CANCELLED"}
FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes)
@@ -246,12 +264,13 @@ class CreateMMDModelRoot(bpy.types.Operator):
default=0.08,
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info(f"Creating MMD model root object with name_j={self.name_j}, name_e={self.name_e}, scale={self.scale}")
rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True)
rig.initialDisplayFrames()
return {"FINISHED"}
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
@@ -305,15 +324,16 @@ class ConvertToMMDModel(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object
return obj and obj.type == "ARMATURE" and obj.mode != "EDIT"
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info(f"Converting to MMD model with scale={self.scale}, convert_material_nodes={self.convert_material_nodes}")
# TODO convert some basic MMD properties
armature_object = context.active_object
scale = self.scale
@@ -321,29 +341,31 @@ class ConvertToMMDModel(bpy.types.Operator):
root_object = FnModel.find_root_object(armature_object)
if root_object is None or root_object != armature_object.parent:
logger.debug("Creating new MMD model")
Model.create(model_name, model_name, scale, armature_object=armature_object)
self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context))
self.__configure_rig(context, Model(armature_object.parent))
return {"FINISHED"}
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects):
def __is_child_of_armature(mesh):
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects) -> None:
def __is_child_of_armature(mesh: bpy.types.Object) -> bool:
if mesh.parent is None:
return False
return mesh.parent == armature_object or __is_child_of_armature(mesh.parent)
def __is_using_armature(mesh):
def __is_using_armature(mesh: bpy.types.Object) -> bool:
for m in mesh.modifiers:
if m.type == "ARMATURE" and m.object == armature_object:
return True
return False
def __get_root(mesh):
def __get_root(mesh: bpy.types.Object) -> bpy.types.Object:
if mesh.parent is None:
return mesh
return __get_root(mesh.parent)
attached_count = 0
for x in objects:
if __is_using_armature(x) and not __is_child_of_armature(x):
x_root = __get_root(x)
@@ -351,27 +373,35 @@ class ConvertToMMDModel(bpy.types.Operator):
x_root.parent_type = "OBJECT"
x_root.parent = armature_object
x_root.matrix_world = m
attached_count += 1
logger.debug(f"Attached {attached_count} meshes to armature")
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model):
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model) -> None:
root_object = mmd_model.rootObject()
armature_object = mmd_model.armature()
mesh_objects = tuple(mmd_model.meshes())
logger.info(f"Configuring rig for {root_object.name} with {len(mesh_objects)} meshes")
mmd_model.loadMorphs()
if self.middle_joint_bones_lock:
vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups}
locked_bones = 0
for pose_bone in armature_object.pose.bones:
if not pose_bone.parent:
continue
if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups:
continue
pose_bone.lock_location = (True, True, True)
locked_bones += 1
logger.debug(f"Locked {locked_bones} middle joint bones")
from ..core.material import FnMaterial
FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes)
try:
converted_materials = 0
for m in (x for mesh in mesh_objects for x in mesh.data.materials if x):
FnMaterial.convert_to_mmd_material(m, context)
mmd_material = m.mmd_material
@@ -384,6 +414,8 @@ class ConvertToMMDModel(bpy.types.Operator):
line_color = list(m.line_color)
mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold
mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)]
converted_materials += 1
logger.debug(f"Converted {converted_materials} materials")
finally:
FnMaterial.set_nodes_are_readonly(False)
from .display_item import DisplayItemQuickSetup
@@ -400,16 +432,17 @@ class ResetObjectVisibility(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context: bpy.types.Context):
def poll(cls, context: bpy.types.Context) -> bool:
active_object: bpy.types.Object = context.active_object
return FnModel.find_root_object(active_object) is not None
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object: bpy.types.Object = context.active_object
mmd_root_object = FnModel.find_root_object(active_object)
assert mmd_root_object is not None
mmd_root = mmd_root_object.mmd_root
logger.info(f"Resetting object visibility for {mmd_root_object.name}")
mmd_root_object.hide_set(False)
rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object)
@@ -440,11 +473,12 @@ class AssembleAll(bpy.types.Operator):
bl_label = "Assemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.info(f"Assembling all components for {root_object.name}")
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
rig = Model(root_object)
MigrationFnBone.fix_mmd_ik_limit_override(rig.armature())
@@ -452,6 +486,7 @@ class AssembleAll(bpy.types.Operator):
rig.build()
rig.morph_slider.bind()
logger.debug("Binding SDEF weights")
with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_bind()
root_object.mmd_root.use_property_driver = True
@@ -466,13 +501,15 @@ class DisassembleAll(bpy.types.Operator):
bl_label = "Disassemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
logger.info(f"Disassembling all components for {root_object.name}")
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
root_object.mmd_root.use_property_driver = False
logger.debug("Unbinding SDEF weights")
with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_unbind()
+72 -31
View File
@@ -7,13 +7,17 @@
import itertools
from operator import itemgetter
from typing import Dict, List, Optional, Set
from typing import Dict, List, Optional, Set, Tuple, Any
import bmesh
import bpy
import numpy as np
import numpy.typing as npt
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
from ..bpyutils import FnContext
from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MessageException(Exception):
@@ -35,8 +39,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
if context.mode != "POSE":
return False
@@ -52,19 +56,22 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting model join by bones operation")
self.join(context)
logger.info("Model join by bones completed successfully")
except MessageException as ex:
logger.error(f"Model join by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def join(self, context: bpy.types.Context):
def join(self, context: Context) -> None:
bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_object)
@@ -74,6 +81,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("No MMD Models selected")
logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}")
with FnContext.temp_override_active_layer_collection(context, parent_root_object):
FnModel.join_models(parent_root_object, child_root_objects)
@@ -82,11 +90,12 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
# Connect child bones
if self.join_type == "CONNECTED":
parent_edit_bone: bpy.types.EditBone = context.active_bone
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
parent_edit_bone: EditBone = context.active_bone
child_edit_bones: Set[EditBone] = set(context.selected_bones)
child_edit_bones.remove(parent_edit_bone)
child_edit_bone: bpy.types.EditBone
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
child_edit_bone: EditBone
for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True
@@ -111,8 +120,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
)
@classmethod
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
def poll(cls, context: Context) -> bool:
active_object: Optional[Object] = context.active_object
if context.mode != "POSE":
return False
@@ -128,56 +137,70 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0
def invoke(self, context, event):
def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context):
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting model separate by bones operation")
self.separate(context)
logger.info("Model separate by bones completed successfully")
except MessageException as ex:
logger.error(f"Model separate by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def separate(self, context: bpy.types.Context):
def separate(self, context: Context) -> None:
weight_threshold: float = self.weight_threshold
mmd_scale = 0.08
target_armature_object: bpy.types.Object = context.active_object
target_armature_object: Object = context.active_object
logger.debug(f"Target armature: {target_armature_object.name}")
bpy.ops.object.mode_set(mode="EDIT")
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
root_bones: Set[EditBone] = set(context.selected_bones)
logger.debug(f"Selected root bones: {len(root_bones)}")
if self.include_descendant_bones:
logger.debug("Including descendant bones")
for edit_bone in root_bones:
with context.temp_override(active_bone=edit_bone):
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
logger.debug(f"Total bones to separate: {len(separate_bones)}")
mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object)
mmd_root_object: Object = FnModel.find_root_object(context.active_object)
mmd_model = Model(mmd_root_object)
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
mmd_model_mesh_objects = list(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
mmd_model_mesh_objects = list(mesh_selection_result.keys())
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
# separate armature bones
separate_armature_object: Optional[bpy.types.Object]
separate_armature_object: Optional[Object]
if self.separate_armature:
logger.debug("Separating armature")
target_armature_object.select_set(True)
bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
if separate_armature_object:
logger.debug(f"Created separate armature: {separate_armature_object.name}")
bpy.ops.object.mode_set(mode="OBJECT")
# collect separate rigid bodies
separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate")
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints
separate_joints: Set[bpy.types.Object] = {
separate_joints: Set[Object] = {
joint_object
for joint_object in mmd_model.joints()
if boundary_joint_owner_condition(
@@ -187,35 +210,43 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
]
)
}
logger.debug(f"Found {len(separate_joints)} joints to separate")
separate_mesh_objects: Set[bpy.types.Object]
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object]
separate_mesh_objects: Set[Object]
model2separate_mesh_objects: Dict[Object, Object]
if len(mmd_model_mesh_objects) == 0:
logger.debug("No mesh objects to separate")
separate_mesh_objects = set()
model2separate_mesh_objects = dict()
else:
# select meshes
obj: bpy.types.Object
logger.debug("Selecting meshes for separation")
obj: Object
for obj in context.view_layer.objects:
obj.select_set(obj in mmd_model_mesh_objects)
context.view_layer.objects.active = mmd_model_mesh_objects[0]
# separate mesh by selected vertices
logger.debug("Separating meshes by selected vertices")
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="SELECTED")
separate_mesh_objects: List[bpy.types.Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
bpy.ops.object.mode_set(mode="OBJECT")
logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects")
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
logger.debug(f"Creating new model with scale {mmd_scale}")
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False)
separate_model.initialDisplayFrames()
separate_root_object = separate_model.rootObject()
separate_root_object.matrix_world = mmd_root_object.matrix_world
separate_model_armature_object = separate_model.armature()
logger.debug(f"Created separate model with root: {separate_root_object.name}")
if self.separate_armature:
logger.debug("Joining separate armature to new model")
with context.temp_override(
active_object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
@@ -223,6 +254,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
bpy.ops.object.join()
# add mesh
logger.debug("Parenting separate mesh objects to new model")
with context.temp_override(
object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
@@ -230,19 +262,23 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object
logger.debug("Updating armature modifiers on separate meshes")
for separate_mesh in separate_mesh_objects:
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
if armature_modifier is None:
logger.debug(f"Creating new armature modifier for {separate_mesh.name}")
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
armature_modifier.object = separate_model_armature_object
logger.debug("Parenting rigid bodies to new model")
with context.temp_override(
object=separate_model.rigidGroupObject(),
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
logger.debug("Parenting joints to new model")
with context.temp_override(
object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
@@ -257,10 +293,12 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
assert separate_layer_collection is not None
if mmd_layer_collection.name != separate_layer_collection.name:
logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}")
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
separate_layer_collection.collection.objects.link(separate_object)
mmd_layer_collection.collection.objects.unlink(separate_object)
logger.debug("Copying MMD root properties")
FnModel.copy_mmd_root(
separate_root_object,
mmd_root_object,
@@ -271,13 +309,15 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
},
)
def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]:
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = dict()
def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]:
"""Select vertices weighted to the bones to be separated"""
logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}")
mesh2selected_vertex_count: Dict[Object, int] = dict()
target_bmesh: bmesh.types.BMesh = bmesh.new()
for mesh_object in mmd_model_mesh_objects:
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
mesh: bpy.types.Mesh = mesh_object.data
mesh: Mesh = mesh_object.data
target_bmesh.from_mesh(mesh, face_normals=False)
target_bmesh.select_mode |= {"VERT"}
deform_layer = target_bmesh.verts.layers.deform.verify()
@@ -304,6 +344,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
vert.select_set(True)
if selected_vertex_count > 0:
logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}")
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
target_bmesh.select_flush_mode()
target_bmesh.to_mesh(mesh)
+59 -32
View File
@@ -5,7 +5,7 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, cast
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
import bpy
from mathutils import Quaternion, Vector
@@ -16,10 +16,11 @@ from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.morph import FnMorph
from ..utils import ItemMoveOp, ItemOp
from ....logging_setup import logger
# Util functions
def divide_vector_components(vec1, vec2):
def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components")
result = []
@@ -33,7 +34,7 @@ def divide_vector_components(vec1, vec2):
return result
def multiply_vector_components(vec1, vec2):
def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components")
result = []
@@ -42,7 +43,7 @@ def multiply_vector_components(vec1, vec2):
return result
def special_division(n1, n2):
def special_division(n1: float, n2: float) -> float:
"""This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
if n2 == 0:
if n1 == 0:
@@ -58,7 +59,7 @@ class AddMorph(bpy.types.Operator):
bl_description = "Add a morph item to active morph list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -68,6 +69,7 @@ class AddMorph(bpy.types.Operator):
morph.name = "New Morph"
if morph_type.startswith("uv"):
morph.data_type = "VERTEX_GROUP"
logger.debug(f"Added new morph of type {morph_type}")
return {"FINISHED"}
@@ -84,7 +86,7 @@ class RemoveMorph(bpy.types.Operator):
options={"SKIP_SAVE"},
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -99,9 +101,11 @@ class RemoveMorph(bpy.types.Operator):
if self.all:
morphs.clear()
mmd_root.active_morph = 0
logger.debug(f"Removed all morphs of type {morph_type}")
else:
morphs.remove(mmd_root.active_morph)
mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
logger.debug(f"Removed morph at index {mmd_root.active_morph} of type {morph_type}")
return {"FINISHED"}
@@ -111,7 +115,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
bl_description = "Move active morph item up/down in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -120,6 +124,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
mmd_root.active_morph,
self.type,
)
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
return {"FINISHED"}
@@ -129,7 +134,7 @@ class CopyMorph(bpy.types.Operator):
bl_description = "Make a copy of active morph in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -156,6 +161,7 @@ class CopyMorph(bpy.types.Operator):
for k, v in morph.items():
morph_new[k] = v if k != "name" else name_tmp
morph_new.name = name_orig + "_copy" # trigger name check
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
return {"FINISHED"}
@@ -165,17 +171,17 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
root = FnModel.find_root_object(context.active_object)
if root is None:
return False
return root.mmd_root.active_morph_type == "bone_morphs"
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
root = FnModel.find_root_object(context.active_object)
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
logger.info("Overwrote bone morphs from active action pose")
return {"FINISHED"}
@@ -185,7 +191,7 @@ class AddMorphOffset(bpy.types.Operator):
bl_description = "Add a morph offset item to the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -210,6 +216,7 @@ class AddMorphOffset(bpy.types.Operator):
item.location = pose_bone.location
item.rotation = pose_bone.rotation_quaternion
logger.debug(f"Added morph offset to {morph_type}")
return {"FINISHED"}
@@ -226,7 +233,7 @@ class RemoveMorphOffset(bpy.types.Operator):
options={"SKIP_SAVE"},
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -243,17 +250,21 @@ class RemoveMorphOffset(bpy.types.Operator):
if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root):
FnMorph.remove_shape_key(obj, morph.name)
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
return {"FINISHED"}
elif morph_type.startswith("uv"):
if morph.data_type == "VERTEX_GROUP":
for obj in FnModel.iterate_mesh_objects(root):
FnMorph.store_uv_morph_data(obj, morph)
logger.debug(f"Removed all UV morph offsets for {morph.name}")
return {"FINISHED"}
morph.data.clear()
morph.active_data = 0
logger.debug(f"Cleared all morph offsets for {morph.name}")
else:
morph.data.remove(morph.active_data)
morph.active_data = max(0, morph.active_data - 1)
logger.debug(f"Removed morph offset at index {morph.active_data}")
return {"FINISHED"}
@@ -269,7 +280,7 @@ class InitMaterialOffset(bpy.types.Operator):
default=0,
)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -281,6 +292,7 @@ class InitMaterialOffset(bpy.types.Operator):
mat_data.specular_color = mat_data.ambient_color = (val,) * 3
mat_data.shininess = mat_data.edge_weight = val
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4
logger.debug(f"Initialized material offset with value {val}")
return {"FINISHED"}
@@ -290,7 +302,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -328,6 +340,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
except ZeroDivisionError:
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
logger.warning("Zero division detected, switching to ADD offset type")
except ValueError:
self.report({"ERROR"}, "An unexpected error happened")
# We should stop on our tracks and re-raise the exception
@@ -345,6 +358,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight
FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
logger.info(f"Applied material offset for {mat_data.material}")
return {"FINISHED"}
@@ -354,7 +368,7 @@ class CreateWorkMaterial(bpy.types.Operator):
bl_description = "Creates a temporary material to edit this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -413,6 +427,7 @@ class CreateWorkMaterial(bpy.types.Operator):
work_mmd_mat.edge_color = list(edge_offset)
work_mmd_mat.edge_weight += mat_data.edge_weight
logger.info(f"Created work material {work_mat_name}")
return {"FINISHED"}
@@ -422,13 +437,13 @@ class ClearTempMaterials(bpy.types.Operator):
bl_description = "Clears all the temporary materials"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
for meshObj in FnModel.iterate_mesh_objects(root):
def __pre_remove(m):
def __pre_remove(m: Optional[bpy.types.Material]) -> bool:
if m and "_temp" in m.name:
base_mat_name = m.name.split("_temp")[0]
try:
@@ -439,6 +454,7 @@ class ClearTempMaterials(bpy.types.Operator):
return False
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
logger.info("Cleared all temporary materials")
return {"FINISHED"}
@@ -448,7 +464,7 @@ class ViewBoneMorph(bpy.types.Operator):
bl_description = "View the result of active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -463,6 +479,7 @@ class ViewBoneMorph(bpy.types.Operator):
mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4()
mtx.translation = p_bone.location + morph_data.location
p_bone.matrix_basis = mtx
logger.info(f"Viewing bone morph: {morph.name}")
return {"FINISHED"}
@@ -472,13 +489,14 @@ class ClearBoneMorphView(bpy.types.Operator):
bl_description = "Reset transforms of all bones to their default values"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
armature = FnModel.find_armature_object(root)
for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity()
logger.info("Cleared bone morph view")
return {"FINISHED"}
@@ -488,7 +506,7 @@ class ApplyBoneMorph(bpy.types.Operator):
bl_description = "Apply current pose to active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -506,6 +524,7 @@ class ApplyBoneMorph(bpy.types.Operator):
p_bone.bone.select = True
else:
p_bone.bone.select = False
logger.info(f"Applied current pose to bone morph: {morph.name}")
return {"FINISHED"}
@@ -515,7 +534,7 @@ class SelectRelatedBone(bpy.types.Operator):
bl_description = "Select the bone assigned to this offset in the armature"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -524,6 +543,7 @@ class SelectRelatedBone(bpy.types.Operator):
morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph_data = morph.data[morph.active_data]
utils.selectSingleBone(context, armature, morph_data.bone)
logger.debug(f"Selected bone: {morph_data.bone}")
return {"FINISHED"}
@@ -533,7 +553,7 @@ class EditBoneOffset(bpy.types.Operator):
bl_description = "Applies the location and rotation of this offset to the bone"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -546,6 +566,7 @@ class EditBoneOffset(bpy.types.Operator):
mtx.translation = morph_data.location
p_bone.matrix_basis = mtx
utils.selectSingleBone(context, armature, p_bone.name)
logger.debug(f"Edited bone offset for {p_bone.name}")
return {"FINISHED"}
@@ -555,7 +576,7 @@ class ApplyBoneOffset(bpy.types.Operator):
bl_description = "Stores the current bone location and rotation into this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -567,6 +588,7 @@ class ApplyBoneOffset(bpy.types.Operator):
p_bone = armature.pose.bones[morph_data.bone]
morph_data.location = p_bone.location
morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
logger.debug(f"Applied bone offset for {p_bone.name}")
return {"FINISHED"}
@@ -576,7 +598,7 @@ class ViewUVMorph(bpy.types.Operator):
bl_description = "View the result of active UV morph on current mesh object"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -627,6 +649,7 @@ class ViewUVMorph(bpy.types.Operator):
uv_tex.active_render = True
meshObj.hide_set(False)
meshObj.select_set(selected)
logger.info(f"Viewing UV morph: {morph.name}")
return {"FINISHED"}
@@ -636,7 +659,7 @@ class ClearUVMorphView(bpy.types.Operator):
bl_description = "Clear all temporary data of UV morphs"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
@@ -664,6 +687,7 @@ class ClearUVMorphView(bpy.types.Operator):
for act in bpy.data.actions:
if act.name.startswith("__uv.") and act.users < 1:
bpy.data.actions.remove(act)
logger.info("Cleared UV morph view")
return {"FINISHED"}
@@ -674,14 +698,14 @@ class EditUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object
if obj.type != "MESH":
return False
active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
meshObj = obj
@@ -704,6 +728,7 @@ class EditUVMorph(bpy.types.Operator):
bpy.ops.object.mode_set(mode="EDIT")
meshObj.select_set(selected)
logger.info("Editing UV morph")
return {"FINISHED"}
@@ -714,14 +739,14 @@ class ApplyUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object
if obj.type != "MESH":
return False
active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
@@ -756,6 +781,7 @@ class ApplyUVMorph(bpy.types.Operator):
morph.data_type = "VERTEX_GROUP"
meshObj.select_set(selected)
logger.info(f"Applied UV morph: {morph.name}")
return {"FINISHED"}
@@ -766,11 +792,12 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
mmd_root_object = FnModel.find_root_object(context.active_object)
FnMorph.clean_duplicated_material_morphs(mmd_root_object)
logger.info("Cleaned duplicated material morphs")
return {"FINISHED"}
+38 -24
View File
@@ -6,7 +6,7 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math
from typing import Dict, Optional, Tuple, cast
from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator
import bpy
from mathutils import Euler, Vector
@@ -16,6 +16,7 @@ from ..bpyutils import FnContext, Props
from ..core import rigid_body
from ..core.model import FnModel, Model
from ..core.rigid_body import FnRigidBody
from ...logging_setup import logger
class SelectRigidBody(bpy.types.Operator):
@@ -43,15 +44,15 @@ class SelectRigidBody(bpy.types.Operator):
default=False,
)
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
@@ -173,7 +174,7 @@ class AddRigidBody(bpy.types.Operator):
default=0.1,
)
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None):
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object:
name_j: str = self.name_j
name_e: str = self.name_e
size = self.size.copy()
@@ -226,7 +227,7 @@ class AddRigidBody(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return False
@@ -237,7 +238,7 @@ class AddRigidBody(bpy.types.Operator):
return True
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
@@ -254,15 +255,17 @@ class AddRigidBody(bpy.types.Operator):
armature_object.select_set(False)
if len(selected_pose_bones) > 0:
logger.info(f"Adding rigid bodies to {len(selected_pose_bones)} selected bones")
for pose_bone in selected_pose_bones:
rigid = self.__add_rigid_body(context, root_object, pose_bone)
rigid.select_set(True)
else:
logger.info("Adding a single rigid body without bone attachment")
rigid = self.__add_rigid_body(context, root_object)
rigid.select_set(True)
return {"FINISHED"}
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
no_bone = True
if context.selected_bones and len(context.selected_bones) > 0:
no_bone = False
@@ -288,12 +291,13 @@ class RemoveRigidBody(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
logger.info(f"Removing rigid body: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True)
if root:
@@ -306,7 +310,8 @@ class RigidBodyBake(bpy.types.Operator):
bl_label = "Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info("Baking rigid body simulation")
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
@@ -318,7 +323,8 @@ class RigidBodyDeleteBake(bpy.types.Operator):
bl_label = "Delete Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context):
def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info("Deleting rigid body simulation bake")
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
@@ -381,7 +387,7 @@ class AddJoint(bpy.types.Operator):
min=0,
)
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> Generator[Tuple[bpy.types.Object, bpy.types.Object], None, None]:
obj_seq = tuple(bone_map.keys())
for rigid_a, bone_a in bone_map.items():
for rigid_b, bone_b in bone_map.items():
@@ -394,7 +400,7 @@ class AddJoint(bpy.types.Operator):
else:
yield obj_seq
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map):
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> bpy.types.Object:
loc: Optional[Vector] = None
rot = Euler((0.0, 0.0, 0.0))
rigid_a, rigid_b = rigid_pair
@@ -432,7 +438,7 @@ class AddJoint(bpy.types.Operator):
)
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return False
@@ -443,7 +449,7 @@ class AddJoint(bpy.types.Operator):
return True
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
@@ -456,15 +462,19 @@ class AddJoint(bpy.types.Operator):
FnContext.select_single_object(context, root_object).select_set(False)
if context.scene.rigidbody_world is None:
logger.info("Creating rigid body world")
bpy.ops.rigidbody.world_add()
joint_count = 0
for pair in self.__enumerate_rigid_pair(bone_map):
joint = self.__add_joint(context, root_object, pair, bone_map)
joint.select_set(True)
joint_count += 1
logger.info(f"Added {joint_count} joints between rigid bodies")
return {"FINISHED"}
def invoke(self, context, event):
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
@@ -476,12 +486,13 @@ class RemoveJoint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_joint_object(context.active_object)
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object
root = FnModel.find_root_object(obj)
logger.info(f"Removing joint: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True)
if root:
@@ -496,7 +507,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
@staticmethod
def __get_rigid_body_world_objects():
def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]:
rigid_body.setRigidBodyWorldEnabled(True)
rbw = bpy.context.scene.rigidbody_world
if not rbw.collection:
@@ -511,12 +522,12 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
return rbw.collection.objects, rbw.constraints.objects
def execute(self, context):
def execute(self, context: bpy.types.Context) -> Set[str]:
scene = context.scene
scene_objs = set(scene.objects)
scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects)
def _update_group(obj, group):
def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool:
if obj in scene_objs:
if obj not in group.values():
group.link(obj)
@@ -525,7 +536,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
group.unlink(obj)
return False
def _references(obj):
def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]:
yield obj
if getattr(obj, "proxy", None):
yield from _references(obj.proxy)
@@ -542,6 +553,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# Object.rigid_body are removed,
# but Object.rigid_body_constraint are retained.
# Therefore, it must be checked with Object.mmd_type.
logger.info("Updating rigid body world objects")
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
if not _update_group(i, rb_objs):
continue
@@ -556,6 +568,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
# mass, friction, restitution, linear_dumping, angular_dumping
logger.info("Updating rigid body constraints")
for i in (x for x in objects if x.rigid_body_constraint):
if not _update_group(i, rbc_objs):
continue
@@ -566,6 +579,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
if need_rebuild_physics:
logger.info("Rebuilding physics for models")
for root_object in scene.objects:
if root_object.mmd_type != "ROOT":
continue
+17 -11
View File
@@ -5,18 +5,19 @@
# 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.
from typing import Set
from typing import Set, Tuple
import bpy
from bpy.types import Operator
from bpy.types import Operator, Context, Object
from ..core.model import FnModel
from ..core.sdef import FnSDEF
from ....core.logging_setup import logger
def _get_target_objects(context):
root_objects: Set[bpy.types.Object] = set()
selected_objects: Set[bpy.types.Object] = set()
def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]:
root_objects: Set[Object] = set()
selected_objects: Set[Object] = set()
for i in context.selected_objects:
if i.type == "MESH":
selected_objects.add(i)
@@ -40,11 +41,13 @@ class ResetSDEFCache(Operator):
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
target_meshes, _ = _get_target_objects(context)
logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects")
for i in target_meshes:
FnSDEF.clear_cache(i)
FnSDEF.clear_cache(unused_only=True)
logger.debug("SDEF cache reset completed")
return {"FINISHED"}
@@ -75,19 +78,20 @@ class BindSDEF(Operator):
default=False,
)
def invoke(self, context, event):
def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
vm = context.window_manager
return vm.invoke_props_dialog(self)
# TODO: Utility Functionalize
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Binding SDEF for {len(target_meshes)} objects with mode={self.mode}, skip={self.use_skip}, scale={self.use_scale}")
for r in root_objects:
r.mmd_root.use_sdef = True
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
count = sum(FnSDEF.bind(i, *param) for i in target_meshes)
logger.info(f"Successfully bound SDEF for {count} of {len(target_meshes)} meshes")
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
return {"FINISHED"}
@@ -98,13 +102,15 @@ class UnbindSDEF(Operator):
bl_description = "Unbind MMD SDEF data of selected objects"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
# TODO: Utility Functionalize
def execute(self, context):
def execute(self, context: Context) -> Set[str]:
target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Unbinding SDEF for {len(target_meshes)} objects")
for i in target_meshes:
FnSDEF.unbind(i)
for r in root_objects:
r.mmd_root.use_sdef = False
logger.debug("SDEF unbinding completed")
return {"FINISHED"}
+42 -34
View File
@@ -6,29 +6,32 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
from bpy.types import Operator
from mathutils import Matrix
from bpy.types import Operator, Context
from mathutils import Matrix, Vector, Quaternion
from ...logging_setup import logger
class _SetShadingBase:
bl_options = {"REGISTER", "UNDO"}
bl_options: Set[str] = {"REGISTER", "UNDO"}
@staticmethod
def _get_view3d_spaces(context):
def _get_view3d_spaces(context: Context) -> Iterator[Any]:
if getattr(context.area, "type", None) == "VIEW_3D":
return (context.area.spaces[0],)
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
@staticmethod
def _reset_color_management(context, use_display_device=True):
def _reset_color_management(context: Context, use_display_device: bool = True) -> None:
try:
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
except TypeError:
pass
@staticmethod
def _reset_material_shading(context, use_shadeless=False):
def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None:
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
for s in i.material_slots:
if s.material is None:
@@ -36,10 +39,11 @@ class _SetShadingBase:
s.material.use_nodes = False
s.material.use_shadeless = use_shadeless
def execute(self, context):
def execute(self, context: Context) -> Dict[str, str]:
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT")
shading_mode = getattr(self, "_shading_mode", None)
shading_mode: Optional[str] = getattr(self, "_shading_mode", None)
for space in self._get_view3d_spaces(context):
shading = space.shading
shading.type = "SOLID"
@@ -47,39 +51,40 @@ class _SetShadingBase:
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
shading.show_object_outline = False
shading.show_backface_culling = False
logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}")
return {"FINISHED"}
class SetGLSLShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.set_glsl_shading"
bl_label = "GLSL View"
bl_description = "Use GLSL shading with additional lighting"
bl_idname: str = "mmd_tools.set_glsl_shading"
bl_label: str = "GLSL View"
bl_description: str = "Use GLSL shading with additional lighting"
_shading_mode = "GLSL"
_shading_mode: str = "GLSL"
class SetShadelessGLSLShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.set_shadeless_glsl_shading"
bl_label = "Shadeless GLSL View"
bl_description = "Use only toon shading"
bl_idname: str = "mmd_tools.set_shadeless_glsl_shading"
bl_label: str = "Shadeless GLSL View"
bl_description: str = "Use only toon shading"
_shading_mode = "SHADELESS"
_shading_mode: str = "SHADELESS"
class ResetShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.reset_shading"
bl_label = "Reset View"
bl_description = "Reset to default Blender shading"
bl_idname: str = "mmd_tools.reset_shading"
bl_label: str = "Reset View"
bl_description: str = "Reset to default Blender shading"
class FlipPose(Operator):
bl_idname = "mmd_tools.flip_pose"
bl_label = "Flip Pose"
bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
bl_options = {"REGISTER", "UNDO"}
bl_idname: str = "mmd_tools.flip_pose"
bl_label: str = "Flip Pose"
bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
bl_options: Set[str] = {"REGISTER", "UNDO"}
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
__LR_REGEX = [
__LR_REGEX: List[Dict[str, Any]] = [
{"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
@@ -87,7 +92,7 @@ class FlipPose(Operator):
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
]
__LR_MAP = {
__LR_MAP: Dict[str, str] = {
"RIGHT": "LEFT",
"Right": "Left",
"right": "left",
@@ -103,7 +108,7 @@ class FlipPose(Operator):
}
@classmethod
def flip_name(cls, name):
def flip_name(cls, name: str) -> str:
for regex in cls.__LR_REGEX:
match = regex["re"].match(name)
if match:
@@ -121,17 +126,15 @@ class FlipPose(Operator):
return ""
@staticmethod
def __cmul(vec1, vec2):
def __cmul(vec1: Union[Vector, Quaternion], vec2: Tuple[float, float, float, float]) -> Union[Vector, Quaternion]:
return type(vec1)([x * y for x, y in zip(vec1, vec2)])
@staticmethod
def __matrix_compose(loc, rot, scale):
def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix:
return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)])
@classmethod
def __flip_pose(cls, matrix_basis, bone_src, bone_dest):
from mathutils import Quaternion
def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None:
m = bone_dest.bone.matrix_local.to_3x3().transposed()
mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
loc, rot, scale = matrix_basis.decompose()
@@ -140,11 +143,16 @@ class FlipPose(Operator):
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
@classmethod
def poll(cls, context):
def poll(cls, context: Context) -> bool:
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
def execute(self, context):
def execute(self, context: Context) -> Dict[str, str]:
logger.info("Executing flip pose operation")
pose_bones = context.active_object.pose.bones
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b))
flip_name = self.flip_name(b.name)
target_bone = pose_bones.get(flip_name, b)
logger.debug(f"Flipping pose from {b.name} to {target_bone.name}")
self.__flip_pose(mat, b, target_bone)
logger.info("Flip pose operation completed")
return {"FINISHED"}
+25 -19
View File
@@ -6,81 +6,85 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from typing import Optional, Set, Dict, Any, List, Tuple, Union, Type
from .. import utils
from ..core import material
from ..core.material import FnMaterial
from ..core.model import FnModel
from . import patch_library_overridable
from ....core.logging_setup import logger
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context):
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_ambient_color()
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context):
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_diffuse_color()
def _mmd_material_update_alpha(prop: "MMDMaterial", _context):
def _mmd_material_update_alpha(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_alpha()
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context):
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_specular_color()
def _mmd_material_update_shininess(prop: "MMDMaterial", _context):
def _mmd_material_update_shininess(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_shininess()
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context):
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_is_double_sided()
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context):
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object)
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context):
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_toon_texture()
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context):
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_drop_shadow()
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context):
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_self_shadow_map()
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context):
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_self_shadow()
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context):
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_enabled_toon_edge()
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context):
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_edge_color()
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context):
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_edge_weight()
def _mmd_material_get_name_j(prop: "MMDMaterial"):
def _mmd_material_get_name_j(prop: "MMDMaterial") -> str:
return prop.get("name_j", "")
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str):
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str) -> None:
prop_value = value
if prop_value and prop_value != prop.get("name_j"):
root = FnModel.find_root_object(bpy.context.active_object)
if root is None:
logger.debug(f"No root object found, using unique name for material: {value}")
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials})
else:
logger.debug(f"Root object found, using unique name for material within model: {value}")
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)})
prop["name_j"] = prop_value
@@ -275,13 +279,15 @@ class MMDMaterial(bpy.types.PropertyGroup):
description="Comment",
)
def is_id_unique(self):
def is_id_unique(self) -> bool:
return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None)
@staticmethod
def register():
def register() -> None:
logger.debug("Registering MMD material properties")
bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial))
@staticmethod
def unregister():
def unregister() -> None:
logger.debug("Unregistering MMD material properties")
del bpy.types.Material.mmd_material
+28 -22
View File
@@ -6,33 +6,33 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from typing import Optional, List, Dict, Any, Set, Tuple, Union, TypeVar, Type
from bpy.types import PropertyGroup, Object, ShapeKey
from .. import utils
from ..core.bone import FnBone
from ..core.material import FnMaterial
from ..core.model import FnModel, Model
from ..core.morph import FnMorph
from ....core.logging_setup import logger
def _morph_base_get_name(prop: "_MorphBase") -> str:
return prop.get("name", "")
def _morph_base_set_name(prop: "_MorphBase", value: str):
def _morph_base_set_name(prop: "_MorphBase", value: str) -> None:
mmd_root = prop.id_data.mmd_root
# morph_type = mmd_root.active_morph_type
morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower()
# assert(prop.bl_rna.identifier.endswith('Morph'))
# logging.debug('_set_name: %s %s %s', prop, value, morph_type)
prop_name = prop.get("name", None)
if prop_name == value:
return
used_names = {x.name for x in getattr(mmd_root, morph_type) if x != prop}
used_names: Set[str] = {x.name for x in getattr(mmd_root, morph_type) if x != prop}
value = utils.unique_name(value, used_names)
if prop_name is not None:
if morph_type == "vertex_morphs":
kb_list = {}
kb_list: Dict[str, List[ShapeKey]] = {}
for mesh in FnModel.iterate_mesh_objects(prop.id_data):
for kb in getattr(mesh.data.shape_keys, "key_blocks", ()):
kb_list.setdefault(kb.name, []).append(kb)
@@ -43,7 +43,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str):
kb.name = value
elif morph_type == "uv_morphs":
vg_list = {}
vg_list: Dict[str, List[Any]] = {}
for mesh in FnModel.iterate_mesh_objects(prop.id_data):
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh):
vg_list.setdefault(n, []).append(vg)
@@ -72,6 +72,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str):
kb.name = value
prop["name"] = value
logger.debug(f"Renamed morph from '{prop_name}' to '{value}'")
class _MorphBase:
@@ -101,11 +102,11 @@ class _MorphBase:
def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
bone_id = prop.get("bone_id", -1)
bone_id: int = prop.get("bone_id", -1)
if bone_id < 0:
return ""
root_object = prop.id_data
armature_object = FnModel.find_armature_object(root_object)
root_object: Object = prop.id_data
armature_object: Optional[Object] = FnModel.find_armature_object(root_object)
if armature_object is None:
return ""
pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id)
@@ -114,9 +115,9 @@ def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
return pose_bone.name
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str):
root = prop.id_data
arm = FnModel.find_armature_object(root)
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None:
root: Object = prop.id_data
arm: Optional[Object] = FnModel.find_armature_object(root)
# Load the library_override file. This function is triggered when loading, but the arm obj cannot be found.
# The arm obj is exist, but the relative relationship has not yet been established.
@@ -128,9 +129,10 @@ def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str):
return
pose_bone = arm.pose.bones[value]
prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
logger.debug(f"Set bone morph data bone to '{value}' with ID {prop['bone_id']}")
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context):
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context: bpy.types.Context) -> None:
if not prop.name.startswith("mmd_bind"):
return
arm = FnModel(prop.id_data).morph_slider.dummy_armature
@@ -139,6 +141,7 @@ def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context
if bone:
bone.location = prop.location
bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency
logger.debug(f"Updated bone morph data location/rotation for '{prop.name}'")
class BoneMorphData(bpy.types.PropertyGroup):
@@ -188,40 +191,44 @@ class BoneMorph(_MorphBase, bpy.types.PropertyGroup):
)
def _material_morph_data_get_material(prop: "MaterialMorphData"):
def _material_morph_data_get_material(prop: "MaterialMorphData") -> str:
mat_p = prop.get("material_data", None)
if mat_p is not None:
return mat_p.name
return ""
def _material_morph_data_set_material(prop: "MaterialMorphData", value: str):
def _material_morph_data_set_material(prop: "MaterialMorphData", value: str) -> None:
if value not in bpy.data.materials:
prop["material_data"] = None
prop["material_id"] = -1
logger.debug(f"Material '{value}' not found, setting material_data to None")
else:
mat = bpy.data.materials[value]
fnMat = FnMaterial(mat)
prop["material_data"] = mat
prop["material_id"] = fnMat.material_id
logger.debug(f"Set material morph data material to '{value}' with ID {fnMat.material_id}")
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str):
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str) -> None:
mesh = FnModel.find_mesh_object_by_name(prop.id_data, value)
if mesh is not None:
prop["related_mesh_data"] = mesh.data
logger.debug(f"Set material morph data related mesh to '{value}'")
else:
prop["related_mesh_data"] = None
logger.debug(f"Mesh '{value}' not found, setting related_mesh_data to None")
def _material_morph_data_get_related_mesh(prop):
def _material_morph_data_get_related_mesh(prop: "MaterialMorphData") -> str:
mesh_p = prop.get("related_mesh_data", None)
if mesh_p is not None:
return mesh_p.name
return ""
def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context):
def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context: bpy.types.Context) -> None:
if not prop.name.startswith("mmd_bind"):
return
from ..core.shader import _MaterialMorph
@@ -229,9 +236,11 @@ def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _co
mat = prop["material_data"]
if mat is not None:
_MaterialMorph.update_morph_inputs(mat, prop)
logger.debug(f"Updated material morph modifiable values for '{prop.name}'")
else:
for mat in FnModel(prop.id_data).materials():
_MaterialMorph.update_morph_inputs(mat, prop)
logger.debug(f"Updated material morph modifiable values for all materials")
class MaterialMorphData(bpy.types.PropertyGroup):
@@ -407,9 +416,6 @@ class UVMorphOffset(bpy.types.PropertyGroup):
name="UV Offset",
description="UV offset",
size=4,
# min=-1,
# max=1,
# precision=3,
step=0.1,
default=[0, 0, 0, 0],
)
+18 -12
View File
@@ -5,29 +5,33 @@
# 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.
from typing import cast
from typing import cast, Optional, Any, Union
import bpy
from bpy.types import Context, PropertyGroup, PoseBone, Object, Armature
from ..core.bone import FnBone
from . import patch_library_overridable
from ....core.logging_setup import logger
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context):
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: Context) -> None:
prop["is_additional_transform_dirty"] = True
p_bone = context.active_pose_bone
if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer():
logger.debug(f"Applying additional transformation for {p_bone.name}")
FnBone.apply_additional_transformation(prop.id_data)
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context):
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: Context) -> None:
pose_bone = context.active_pose_bone
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer():
logger.debug(f"Updating additional transform influence for {pose_bone.name}")
FnBone.update_additional_transform_influence(pose_bone)
else:
prop["is_additional_transform_dirty"] = True
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone") -> str:
arm = prop.id_data
bone_id = prop.get("additional_transform_bone_id", -1)
if bone_id < 0:
@@ -38,7 +42,7 @@ def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
return pose_bone.name
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str):
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str) -> None:
arm = prop.id_data
prop["is_additional_transform_dirty"] = True
if value not in arm.pose.bones.keys():
@@ -48,7 +52,7 @@ def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str):
prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
class MMDBone(bpy.types.PropertyGroup):
class MMDBone(PropertyGroup):
name_j: bpy.props.StringProperty(
name="Name",
description="Japanese Name",
@@ -184,11 +188,12 @@ class MMDBone(bpy.types.PropertyGroup):
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True)
def is_id_unique(self):
def is_id_unique(self) -> bool:
return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None)
@staticmethod
def register():
def register() -> None:
logger.debug("Registering MMDBone properties")
bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone))
bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False))
bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type"))
@@ -202,20 +207,21 @@ class MMDBone(bpy.types.PropertyGroup):
)
@staticmethod
def unregister():
def unregister() -> None:
logger.debug("Unregistering MMDBone properties")
del bpy.types.PoseBone.mmd_ik_toggle
del bpy.types.PoseBone.mmd_shadow_bone_type
del bpy.types.PoseBone.is_mmd_shadow_bone
del bpy.types.PoseBone.mmd_bone
def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context):
def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context: Any) -> None:
v = prop.mmd_ik_toggle
armature_object = cast(bpy.types.Object, prop.id_data)
armature_object = cast(Object, prop.id_data)
for b in armature_object.pose.bones:
for c in b.constraints:
if c.type == "IK" and c.subtarget == prop.name:
# logging.debug(' %s %s', b.name, c.name)
logger.debug(f"Updating IK toggle for {b.name} {c.name}")
c.influence = v
b = b if c.use_tail else b.parent
for b in ([b] + b.parent_recursive)[: c.chain_count]:
+36 -28
View File
@@ -8,32 +8,35 @@
"""Properties for rigid bodies and joints"""
import bpy
from typing import Optional, Any, Set, List, Dict, Tuple, Union
from bpy.types import Context, Object, PropertyGroup, Material
from .. import bpyutils
from ..core import rigid_body
from ..core.rigid_body import RigidBodyMaterial, FnRigidBody
from ..core.model import FnModel
from . import patch_library_overridable
from ....core.logging_setup import logger
def _updateCollisionGroup(prop, _context):
obj = prop.id_data
materials = obj.data.materials
def _updateCollisionGroup(prop: PropertyGroup, _context: Context) -> None:
obj: Object = prop.id_data
materials: List[Material] = obj.data.materials
if len(materials) == 0:
materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number))
else:
obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number)
def _updateType(prop, _context):
obj = prop.id_data
def _updateType(prop: PropertyGroup, _context: Context) -> None:
obj: Object = prop.id_data
rb = obj.rigid_body
if rb:
rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC
def _updateShape(prop, _context):
obj = prop.id_data
def _updateShape(prop: PropertyGroup, _context: Context) -> None:
obj: Object = prop.id_data
if len(obj.data.vertices) > 0:
size = prop.size
@@ -44,8 +47,8 @@ def _updateShape(prop, _context):
rb.collision_shape = prop.shape
def _get_bone(prop):
obj = prop.id_data
def _get_bone(prop: PropertyGroup) -> str:
obj: Object = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation:
arm = relation.target
@@ -55,9 +58,9 @@ def _get_bone(prop):
return prop.get("bone", "")
def _set_bone(prop, value):
bone_name = value
obj = prop.id_data
def _set_bone(prop: PropertyGroup, value: str) -> None:
bone_name: str = value
obj: Object = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation is None:
relation = obj.constraints.new("CHILD_OF")
@@ -78,16 +81,16 @@ def _set_bone(prop, value):
prop["bone"] = bone_name
def _get_size(prop):
def _get_size(prop: PropertyGroup) -> Tuple[float, float, float]:
if prop.id_data.mmd_type != "RIGID_BODY":
return (0, 0, 0)
return FnRigidBody.get_rigid_body_size(prop.id_data)
def _set_size(prop, value):
obj = prop.id_data
def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None:
obj: Object = prop.id_data
assert obj.mode == "OBJECT" # not support other mode yet
shape = prop.shape
shape: str = prop.shape
mesh = obj.data
rb = obj.rigid_body
@@ -146,15 +149,15 @@ def _set_size(prop, value):
mesh.update()
def _get_rigid_name(prop):
def _get_rigid_name(prop: PropertyGroup) -> str:
return prop.get("name", "")
def _set_rigid_name(prop, value):
def _set_rigid_name(prop: PropertyGroup, value: str) -> None:
prop["name"] = value
class MMDRigidBody(bpy.types.PropertyGroup):
class MMDRigidBody(PropertyGroup):
name_j: bpy.props.StringProperty(
name="Name",
description="Japanese Name",
@@ -227,16 +230,18 @@ class MMDRigidBody(bpy.types.PropertyGroup):
)
@staticmethod
def register():
def register() -> None:
logger.debug("Registering MMDRigidBody property")
bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody))
@staticmethod
def unregister():
def unregister() -> None:
logger.debug("Unregistering MMDRigidBody property")
del bpy.types.Object.mmd_rigid
def _updateSpringLinear(prop, context):
obj = prop.id_data
def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None:
obj: Object = prop.id_data
rbc = obj.rigid_body_constraint
if rbc:
rbc.spring_stiffness_x = prop.spring_linear[0]
@@ -244,8 +249,8 @@ def _updateSpringLinear(prop, context):
rbc.spring_stiffness_z = prop.spring_linear[2]
def _updateSpringAngular(prop, context):
obj = prop.id_data
def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None:
obj: Object = prop.id_data
rbc = obj.rigid_body_constraint
if rbc and hasattr(rbc, "use_spring_ang_x"):
rbc.spring_stiffness_ang_x = prop.spring_angular[0]
@@ -253,7 +258,7 @@ def _updateSpringAngular(prop, context):
rbc.spring_stiffness_ang_z = prop.spring_angular[2]
class MMDJoint(bpy.types.PropertyGroup):
class MMDJoint(PropertyGroup):
name_j: bpy.props.StringProperty(
name="Name",
description="Japanese Name",
@@ -287,9 +292,12 @@ class MMDJoint(bpy.types.PropertyGroup):
)
@staticmethod
def register():
def register() -> None:
logger.debug("Registering MMDJoint property")
bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint))
@staticmethod
def unregister():
def unregister() -> None:
logger.debug("Unregistering MMDJoint property")
del bpy.types.Object.mmd_joint
+41 -25
View File
@@ -8,6 +8,7 @@
"""Properties for MMD model root object"""
import bpy
from typing import Optional, List, Dict, Any, Set, Tuple, Union, Type, TypeVar, cast
from .. import utils
from ..bpyutils import FnContext
@@ -17,9 +18,10 @@ from ..core.sdef import FnSDEF
from . import patch_library_overridable
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
from .translations import MMDTranslation
from ....core.logging_setup import logger
def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
def __driver_variables(constraint: bpy.types.Constraint, path: str, index: int = -1) -> Tuple[bpy.types.Driver, Any]:
d = constraint.driver_add(path, index)
variables = d.driver.variables
for x in variables:
@@ -27,7 +29,7 @@ def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
return d.driver, variables
def __add_single_prop(variables, id_obj, data_path, prefix):
def __add_single_prop(variables: Any, id_obj: bpy.types.Object, data_path: str, prefix: str) -> Any:
var = variables.new()
var.name = prefix + str(len(variables))
var.type = "SINGLE_PROP"
@@ -38,17 +40,18 @@ def __add_single_prop(variables, id_obj, data_path, prefix):
return var
def _toggleUsePropertyDriver(self: "MMDRoot", _context):
def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> None:
root_object: bpy.types.Object = self.id_data
armature_object = FnModel.find_armature_object(root_object)
if armature_object is None:
ik_map = {}
ik_map: Dict[Any, Tuple[Any, Any]] = {}
else:
bones = armature_object.pose.bones
ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}
if self.use_property_driver:
logger.debug("Enabling property drivers for %s", root_object.name)
for ik, (b, c) in ik_map.items():
driver, variables = __driver_variables(c, "influence")
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
@@ -63,6 +66,7 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context):
driver, variables = __driver_variables(i, prop_hide)
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
else:
logger.debug("Disabling property drivers for %s", root_object.name)
for ik, (b, c) in ik_map.items():
c.driver_remove("influence")
b = b if c.use_tail else b.parent
@@ -80,31 +84,35 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context):
# ===========================================
def _toggleUseToonTexture(self: "MMDRoot", _context):
def _toggleUseToonTexture(self: "MMDRoot", _context: bpy.types.Context) -> None:
use_toon = self.use_toon_texture
logger.debug("Toggling toon texture to %s for %s", use_toon, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data):
for m in i.data.materials:
if m:
FnMaterial(m).use_toon_texture(use_toon)
def _toggleUseSphereTexture(self: "MMDRoot", _context):
def _toggleUseSphereTexture(self: "MMDRoot", _context: bpy.types.Context) -> None:
use_sphere = self.use_sphere_texture
logger.debug("Toggling sphere texture to %s for %s", use_sphere, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data):
for m in i.data.materials:
if m:
FnMaterial(m).use_sphere_texture(use_sphere, i)
def _toggleUseSDEF(self: "MMDRoot", _context):
def _toggleUseSDEF(self: "MMDRoot", _context: bpy.types.Context) -> None:
mute_sdef = not self.use_sdef
logger.debug("Toggling SDEF to %s for %s", not mute_sdef, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data):
FnSDEF.mute_sdef_set(i, mute_sdef)
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context) -> None:
root = self.id_data
hide = not self.show_meshes
logger.debug("Toggling mesh visibility to %s for %s", not hide, root.name)
for i in FnModel.iterate_mesh_objects(self.id_data):
i.hide_set(hide)
i.hide_render = hide
@@ -112,27 +120,30 @@ def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
FnContext.set_active_object(context, root)
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context):
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context) -> None:
root = self.id_data
hide = not self.show_rigid_bodies
logger.debug("Toggling rigid body visibility to %s for %s", not hide, root.name)
for i in FnModel.iterate_rigid_body_objects(root):
i.hide_set(hide)
if hide and context.active_object is None:
FnContext.set_active_object(context, root)
def _toggleVisibilityOfJoints(self: "MMDRoot", context):
def _toggleVisibilityOfJoints(self: "MMDRoot", context: bpy.types.Context) -> None:
root_object = self.id_data
hide = not self.show_joints
logger.debug("Toggling joint visibility to %s for %s", not hide, root_object.name)
for i in FnModel.iterate_joint_objects(root_object):
i.hide_set(hide)
if hide and context.active_object is None:
FnContext.set_active_object(context, root_object)
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context):
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context) -> None:
root_object: bpy.types.Object = self.id_data
hide = not self.show_temporary_objects
logger.debug("Toggling temporary object visibility to %s for %s", not hide, root_object.name)
with FnContext.temp_override_active_layer_collection(context, root_object):
for i in FnModel.iterate_temporary_objects(root_object):
i.hide_set(hide)
@@ -140,45 +151,48 @@ def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Cont
FnContext.set_active_object(context, root_object)
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context):
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context: bpy.types.Context) -> None:
root = self.id_data
show_names = root.mmd_root.show_names_of_rigid_bodies
logger.debug("Toggling rigid body names to %s for %s", show_names, root.name)
for i in FnModel.iterate_rigid_body_objects(root):
i.show_name = show_names
def _toggleShowNamesOfJoints(self: "MMDRoot", _context):
def _toggleShowNamesOfJoints(self: "MMDRoot", _context: bpy.types.Context) -> None:
root = self.id_data
show_names = root.mmd_root.show_names_of_joints
logger.debug("Toggling joint names to %s for %s", show_names, root.name)
for i in FnModel.iterate_joint_objects(root):
i.show_name = show_names
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool):
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool) -> None:
root = prop.id_data
arm = FnModel.find_armature_object(root)
if arm is None:
return
if not v and bpy.context.active_object == arm:
FnContext.set_active_object(bpy.context, root)
logger.debug("Setting armature visibility to %s for %s", v, root.name)
arm.hide_set(not v)
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"):
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot") -> bool:
if prop.id_data.mmd_type != "ROOT":
return False
arm = FnModel.find_armature_object(prop.id_data)
return arm and not arm.hide_get()
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int):
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int) -> None:
obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_rigid_body_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_rigidbody_object_index"] = v
def _getActiveRigidbodyObject(prop: "MMDRoot"):
def _getActiveRigidbodyObject(prop: "MMDRoot") -> int:
context = bpy.context
active_obj = FnContext.get_active_object(context)
if FnModel.is_rigid_body_object(active_obj):
@@ -186,14 +200,14 @@ def _getActiveRigidbodyObject(prop: "MMDRoot"):
return prop.get("active_rigidbody_object_index", 0)
def _setActiveJointObject(prop: "MMDRoot", v: int):
def _setActiveJointObject(prop: "MMDRoot", v: int) -> None:
obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_joint_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_joint_object_index"] = v
def _getActiveJointObject(prop: "MMDRoot"):
def _getActiveJointObject(prop: "MMDRoot") -> int:
context = bpy.context
active_obj = FnContext.get_active_object(context)
if FnModel.is_joint_object(active_obj):
@@ -201,26 +215,26 @@ def _getActiveJointObject(prop: "MMDRoot"):
return prop.get("active_joint_object_index", 0)
def _setActiveMorph(prop: "MMDRoot", v: bool):
def _setActiveMorph(prop: "MMDRoot", v: bool) -> None:
if "active_morph_indices" not in prop:
prop["active_morph_indices"] = [0] * 5
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v
def _getActiveMorph(prop: "MMDRoot"):
def _getActiveMorph(prop: "MMDRoot") -> int:
if "active_morph_indices" in prop:
return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
return 0
def _setActiveMeshObject(prop: "MMDRoot", v: int):
def _setActiveMeshObject(prop: "MMDRoot", v: int) -> None:
obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_mesh_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_mesh_index"] = v
def _getActiveMeshObject(prop: "MMDRoot"):
def _getActiveMeshObject(prop: "MMDRoot") -> int:
context = bpy.context
active_obj = FnContext.get_active_object(context)
if FnModel.is_mesh_object(active_obj):
@@ -520,7 +534,8 @@ class MMDRoot(bpy.types.PropertyGroup):
prop.hide_viewport = value
@staticmethod
def register():
def register() -> None:
logger.debug("Registering MMDRoot property group")
bpy.types.Object.mmd_type = patch_library_overridable(
bpy.props.EnumProperty(
name="Type",
@@ -570,7 +585,8 @@ class MMDRoot(bpy.types.PropertyGroup):
)
@staticmethod
def unregister():
def unregister() -> None:
logger.debug("Unregistering MMDRoot property group")
del bpy.types.Object.hide
del bpy.types.Object.select
del bpy.types.Object.mmd_root
+92 -37
View File
@@ -6,14 +6,20 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import csv
import logging
import time
from typing import List, Tuple, Dict, Optional, Any, Generator, Union, TextIO, Iterator, Set
import bpy
from bpy.types import Text, Context
from .bpyutils import FnContext
from ..logging_setup import logger
jp_half_to_full_tuples = (
# Type definitions for translation tuples
TranslationTuple = Tuple[str, str]
TranslationList = List[TranslationTuple]
jp_half_to_full_tuples: TranslationList = (
("ヴ", ""),
("ガ", ""),
("ギ", ""),
@@ -103,7 +109,7 @@ jp_half_to_full_tuples = (
("", ""),
)
jp_to_en_tuples = [
jp_to_en_tuples: TranslationList = [
("全ての親", "ParentNode"),
("操作中心", "ControlNode"),
("センター", "Center"),
@@ -293,22 +299,30 @@ jp_to_en_tuples = [
]
def translateFromJp(name):
def translateFromJp(name: str) -> str:
"""Translate a Japanese name to English using the translation tuples."""
logger.debug(f"Translating from Japanese: {name}")
for tuple in jp_to_en_tuples:
if tuple[0] in name:
name = name.replace(tuple[0], tuple[1])
logger.debug(f"Translation result: {name}")
return name
def getTranslator(csvfile="", keep_order=False):
def getTranslator(csvfile: Union[str, Dict[str, str], Text] = "", keep_order: bool = False) -> 'MMDTranslator':
"""Get a translator instance with the specified CSV file."""
translator = MMDTranslator()
if isinstance(csvfile, bpy.types.Text):
logger.debug(f"Loading translator from Text object: {csvfile.name}")
translator.load_from_stream(csvfile)
elif isinstance(csvfile, dict):
logger.debug(f"Loading translator from dictionary with {len(csvfile)} entries")
translator.csv_tuples.extend(csvfile.items())
elif csvfile in bpy.data.texts.keys():
logger.debug(f"Loading translator from text data: {csvfile}")
translator.load_from_stream(bpy.data.texts[csvfile])
else:
logger.debug(f"Loading translator from file: {csvfile}")
translator.load(csvfile)
if not keep_order:
@@ -318,16 +332,20 @@ def getTranslator(csvfile="", keep_order=False):
class MMDTranslator:
def __init__(self):
self.__csv_tuples = []
self.__fails = {}
"""Handles translation of Japanese text to English for MMD models."""
def __init__(self) -> None:
self.__csv_tuples: List[Tuple[str, str]] = []
self.__fails: Dict[str, str] = {}
@staticmethod
def default_csv_filepath():
def default_csv_filepath() -> str:
"""Get the default CSV filepath for translations."""
return __file__[:-3] + ".csv"
@staticmethod
def get_csv_text(text_name=None):
def get_csv_text(text_name: Optional[str] = None) -> Text:
"""Get or create a Text object for CSV data."""
text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath())
csv_text = bpy.data.texts.get(text_name, None)
if csv_text is None:
@@ -335,69 +353,88 @@ class MMDTranslator:
return csv_text
@staticmethod
def replace_from_tuples(name, tuples):
def replace_from_tuples(name: str, tuples: List[Tuple[str, str]]) -> str:
"""Replace parts of a string based on translation tuples."""
for pair in tuples:
if pair[0] in name:
name = name.replace(pair[0], pair[1])
return name
@property
def csv_tuples(self):
def csv_tuples(self) -> List[Tuple[str, str]]:
"""Get the CSV tuples."""
return self.__csv_tuples
@property
def fails(self):
def fails(self) -> Dict[str, str]:
"""Get the failed translations."""
return self.__fails
def sort(self):
def sort(self) -> None:
"""Sort the CSV tuples by length (longest first) and then alphabetically."""
logger.debug("Sorting translation tuples")
self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row))
def update(self):
def update(self) -> None:
"""Update the CSV tuples, removing duplicates."""
from collections import OrderedDict
count_old = len(self.__csv_tuples)
tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0])
self.__csv_tuples.clear()
self.__csv_tuples.extend(tuples_dict.values())
logging.info(" - removed items:\t%d\t(of %d)", count_old - len(self.__csv_tuples), count_old)
logger.info("Translation update - removed items: %d (of %d)", count_old - len(self.__csv_tuples), count_old)
def half_to_full(self, name):
def half_to_full(self, name: str) -> str:
"""Convert half-width Japanese characters to full-width."""
return self.replace_from_tuples(name, jp_half_to_full_tuples)
def is_translated(self, name):
def is_translated(self, name: str) -> bool:
"""Check if a string is already translated (contains only ASCII characters)."""
try:
name.encode("ascii", errors="strict")
except UnicodeEncodeError:
return False
return True
def translate(self, name, default=None, from_full_width=True):
def translate(self, name: str, default: Optional[str] = None, from_full_width: bool = True) -> str:
"""Translate a string from Japanese to English."""
logger.debug(f"Translating: {name}")
if from_full_width:
name = self.half_to_full(name)
name_new = self.replace_from_tuples(name, self.__csv_tuples)
if default is not None and not self.is_translated(name_new):
logger.warning(f"Translation failed for: {name}")
self.__fails[name] = name_new
return default
return name_new
def save_fails(self, text_name=None):
def save_fails(self, text_name: Optional[str] = None) -> Text:
"""Save failed translations to a Text object."""
text_name = text_name or (__name__ + ".fails")
txt = self.get_csv_text(text_name)
fmt = '"%s","%s"'
items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row))
txt.from_string("\n".join(fmt % (k, v) for k, v in items))
logger.info(f"Saved {len(items)} failed translations to {text_name}")
return txt
def load_from_stream(self, csvfile=None):
def load_from_stream(self, csvfile: Union[Text, Iterator[str]] = None) -> None:
"""Load translations from a stream."""
csvfile = csvfile or self.get_csv_text()
if isinstance(csvfile, bpy.types.Text):
csvfile = (l.body + "\n" for l in csvfile.lines)
spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True)
csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2]
self.__csv_tuples = csv_tuples
logging.info(" - load items:\t%d", len(self.__csv_tuples))
logger.info("Loaded %d translation items", len(self.__csv_tuples))
def save_to_stream(self, csvfile=None):
def save_to_stream(self, csvfile: Union[Text, TextIO] = None) -> None:
"""Save translations to a stream.
Args:
csvfile: The CSV file or stream to save to
"""
csvfile = csvfile or self.get_csv_text()
lineterminator = "\r\n"
if isinstance(csvfile, bpy.types.Text):
@@ -405,27 +442,38 @@ class MMDTranslator:
lineterminator = "\n"
spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL)
spamwriter.writerows(self.__csv_tuples)
logging.info(" - save items:\t%d", len(self.__csv_tuples))
logger.info("Saved %d translation items", len(self.__csv_tuples))
def load(self, filepath=None):
def load(self, filepath: Optional[str] = None) -> None:
"""Load translations from a file."""
filepath = filepath or self.default_csv_filepath()
logging.info("Loading csv file:\t%s", filepath)
with open(filepath, "rt", encoding="utf-8", newline="") as csvfile:
self.load_from_stream(csvfile)
logger.info("Loading CSV file: %s", filepath)
try:
with open(filepath, "rt", encoding="utf-8", newline="") as csvfile:
self.load_from_stream(csvfile)
except Exception as e:
logger.error(f"Failed to load CSV file: {e}")
def save(self, filepath=None):
def save(self, filepath: Optional[str] = None) -> None:
"""Save translations to a file."""
filepath = filepath or self.default_csv_filepath()
logging.info("Saving csv file:\t%s", filepath)
with open(filepath, "wt", encoding="utf-8", newline="") as csvfile:
self.save_to_stream(csvfile)
logger.info("Saving CSV file: %s", filepath)
try:
with open(filepath, "wt", encoding="utf-8", newline="") as csvfile:
self.save_to_stream(csvfile)
except Exception as e:
logger.error(f"Failed to save CSV file: {e}")
class DictionaryEnum:
__items_ttl = 0.0
__items_cache = None
"""Handles dictionary enumeration for UI."""
__items_ttl: float = 0.0
__items_cache: Optional[List[Tuple[str, str, str, int]]] = None
@staticmethod
def get_dictionary_items(prop, context):
def get_dictionary_items(prop: Any, context: Context) -> List[Tuple[str, str, str, Union[int, str], int]]:
"""Get dictionary items for UI enumeration."""
if DictionaryEnum.__items_ttl > time.time():
return DictionaryEnum.__items_cache
@@ -437,7 +485,7 @@ class DictionaryEnum:
items.append(("INTERNAL", "Internal Dictionary", "The dictionary defined in " + __name__, len(items)))
for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")):
items.append((txt_name, txt_name, "bpy.data.texts['%s']" % txt_name, "TEXT", len(items)))
items.append((txt_name, txt_name, f"bpy.data.texts['{txt_name}']", "TEXT", len(items)))
import os
@@ -450,12 +498,19 @@ class DictionaryEnum:
if "dictionary" in prop:
prop["dictionary"] = min(prop["dictionary"], len(items) - 1)
logger.debug(f"Found {len(items)} dictionary items")
return items
@staticmethod
def get_translator(dictionary):
def get_translator(dictionary: str) -> Optional[MMDTranslator]:
"""Get a translator for the specified dictionary."""
if dictionary == "DISABLED":
logger.debug("Translation disabled")
return None
if dictionary == "INTERNAL":
logger.debug("Using internal dictionary")
return getTranslator(dict(jp_to_en_tuples))
logger.debug(f"Using dictionary: {dictionary}")
return getTranslator(dictionary)
+25 -27
View File
@@ -5,18 +5,19 @@
# 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
import os
import re
from typing import Callable, Optional, Set
from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any
import bpy
from bpy.types import Object, Bone, PoseBone, Mesh, VertexGroup
from ..logging_setup import logger
from .bpyutils import FnContext
## 指定したオブジェクトのみを選択状態かつアクティブにする
def selectAObject(obj):
def selectAObject(obj: Object) -> None:
try:
bpy.ops.object.mode_set(mode="OBJECT")
except Exception:
@@ -27,13 +28,13 @@ def selectAObject(obj):
## 現在のモードを指定したオブジェクトのEdit Modeに変更する
def enterEditMode(obj):
def enterEditMode(obj: Object) -> None:
selectAObject(obj)
if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
def setParentToBone(obj, parent, bone_name):
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
selectAObject(obj)
FnContext.set_active_object(FnContext.ensure_context(), parent)
bpy.ops.object.mode_set(mode="POSE")
@@ -42,7 +43,7 @@ def setParentToBone(obj, parent, bone_name):
bpy.ops.object.mode_set(mode="OBJECT")
def selectSingleBone(context, armature, bone_name, reset_pose=False):
def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None:
try:
bpy.ops.object.mode_set(mode="OBJECT")
except:
@@ -55,7 +56,7 @@ def selectSingleBone(context, armature, bone_name, reset_pose=False):
for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity()
armature_bones: bpy.types.ArmatureBones = armature.data.bones
i: bpy.types.Bone
i: Bone
for i in armature_bones:
i.select = i.name == bone_name
i.select_head = i.select_tail = i.select
@@ -69,7 +70,7 @@ __CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
## 日本語で左右を命名されている名前をblender方式のL(R)に変更する
def convertNameToLR(name, use_underscore=False):
def convertNameToLR(name: str, use_underscore: bool = False) -> str:
m = __CONVERT_NAME_TO_L_REGEXP.match(name)
delimiter = "_" if use_underscore else "."
if m:
@@ -84,7 +85,7 @@ __CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[lL])(?P<aft
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
def convertLRToName(name):
def convertLRToName(name: str) -> str:
match = __CONVERT_L_TO_NAME_REGEXP.search(name)
if match:
return f"{name[0:match.start()]}{match['after']}{name[match.end():]}"
@@ -97,7 +98,7 @@ def convertLRToName(name):
## src_vertex_groupのWeightをdest_vertex_groupにaddする
def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None:
mesh = meshObj.data
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]
@@ -111,7 +112,7 @@ def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
pass
def separateByMaterials(meshObj: bpy.types.Object):
def separateByMaterials(meshObj: Object) -> None:
if len(meshObj.data.materials) < 2:
selectAObject(meshObj)
return
@@ -134,7 +135,7 @@ def separateByMaterials(meshObj: bpy.types.Object):
bpy.data.objects.remove(dummy_parent)
def clearUnusedMeshes():
def clearUnusedMeshes() -> None:
meshes_to_delete = []
for mesh in bpy.data.meshes:
if mesh.users == 0:
@@ -146,7 +147,7 @@ def clearUnusedMeshes():
## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
def makePmxBoneMap(armObj):
def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]:
# Maintain backward compatibility with mmd_tools v0.4.x or older.
return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones}
@@ -175,7 +176,7 @@ def unique_name(name: str, used_names: Set[str]) -> str:
return new_name
def int2base(x, base, width=0):
def int2base(x: int, base: int, width: int = 0) -> str:
"""
Method to convert an int to a base
Source: http://stackoverflow.com/questions/2267362
@@ -198,7 +199,7 @@ def int2base(x, base, width=0):
return digits
def saferelpath(path, start, strategy="inside"):
def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
"""
On Windows relpath will raise a ValueError
when trying to calculate the relative path to a
@@ -227,13 +228,13 @@ def saferelpath(path, start, strategy="inside"):
class ItemOp:
@staticmethod
def get_by_index(items, index):
def get_by_index(items: bpy.types.bpy_prop_collection, index: int) -> Optional[Any]:
if 0 <= index < len(items):
return items[index]
return None
@staticmethod
def resize(items: bpy.types.bpy_prop_collection, length: int):
def resize(items: bpy.types.bpy_prop_collection, length: int) -> None:
count = length - len(items)
if count > 0:
for i in range(count):
@@ -243,7 +244,7 @@ class ItemOp:
items.remove(length)
@staticmethod
def add_after(items, index):
def add_after(items: bpy.types.bpy_prop_collection, index: int) -> Tuple[Any, int]:
index_end = len(items)
index = max(0, min(index_end, index + 1))
items.add()
@@ -265,7 +266,8 @@ class ItemMoveOp:
)
@staticmethod
def move(items, index, move_type, index_min=0, index_max=None):
def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str,
index_min: int = 0, index_max: Optional[int] = None) -> int:
if index_max is None:
index_max = len(items) - 1
else:
@@ -294,7 +296,7 @@ class ItemMoveOp:
return index_new
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None):
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None) -> Callable:
"""Decorator to mark a function as deprecated.
Args:
deprecated_in (Optional[str]): Version in which the function was deprecated.
@@ -303,8 +305,8 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non
Callable: The decorated function.
"""
def _function_wrapper(function: Callable):
def _inner_wrapper(*args, **kwargs):
def _function_wrapper(function: Callable) -> Callable:
def _inner_wrapper(*args: Any, **kwargs: Any) -> Any:
warn_deprecation(function.__name__, deprecated_in, details)
return function(*args, **kwargs)
@@ -320,7 +322,7 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de
deprecated_in (Optional[str]): Version in which the function was deprecated.
details (Optional[str]): Additional details about the deprecation.
"""
logging.warning(
logger.warning(
"%s is deprecated%s%s",
function_name,
f" since {deprecated_in}" if deprecated_in else "",
@@ -328,7 +330,3 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de
stack_info=True,
stacklevel=4,
)
# import warnings # pylint: disable=import-outside-toplevel
# warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2)
+1 -1
View File
@@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
# Define which version series this installation can update to
# For example: ["0.1"] means only look for 0.1.x updates
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates
ALLOWED_VERSION_SERIES = ["0.2"]
ALLOWED_VERSION_SERIES = ["0.3"]
is_checking_for_update: bool = False
update_needed: bool = False
-240
View File
@@ -1,240 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2012 MMD Tools authors
# This file is part of MMD Tools.
from typing import Iterable, Optional
import bpy
from .core.shader import _NodeGroupUtils
from .core.material import FnMaterial
def __switchToCyclesRenderEngine():
if bpy.context.scene.render.engine != "CYCLES":
bpy.context.scene.render.engine = "CYCLES"
def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader):
_NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value)
def __exposeNodeTreeOutput(out_socket, name, node_output, shader):
_NodeGroupUtils(shader).new_output_socket(name, out_socket)
def __getMaterialOutput(nodes, bl_idname):
o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname)
o.is_active_output = True
return o
def create_MMDAlphaShader():
__switchToCyclesRenderEngine()
if "MMDAlphaShader" in bpy.data.node_groups:
return bpy.data.node_groups["MMDAlphaShader"]
shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree")
node_input = shader.nodes.new("NodeGroupInput")
node_output = shader.nodes.new("NodeGroupOutput")
node_output.location.x += 250
node_input.location.x -= 500
trans = shader.nodes.new("ShaderNodeBsdfTransparent")
trans.location.x -= 250
trans.location.y += 150
mix = shader.nodes.new("ShaderNodeMixShader")
shader.links.new(mix.inputs[1], trans.outputs["BSDF"])
__exposeNodeTreeInput(mix.inputs[2], "Shader", None, node_input, shader)
__exposeNodeTreeInput(mix.inputs["Fac"], "Alpha", 1.0, node_input, shader)
__exposeNodeTreeOutput(mix.outputs["Shader"], "Shader", node_output, shader)
return shader
def create_MMDBasicShader():
__switchToCyclesRenderEngine()
if "MMDBasicShader" in bpy.data.node_groups:
return bpy.data.node_groups["MMDBasicShader"]
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput")
node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput")
node_output.location.x += 250
node_input.location.x -= 500
dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse")
dif.location.x -= 250
dif.location.y += 150
glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic")
glo.location.x -= 250
glo.location.y -= 150
mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader")
shader.links.new(mix.inputs[1], dif.outputs["BSDF"])
shader.links.new(mix.inputs[2], glo.outputs["BSDF"])
__exposeNodeTreeInput(dif.inputs["Color"], "diffuse", [1.0, 1.0, 1.0, 1.0], node_input, shader)
__exposeNodeTreeInput(glo.inputs["Color"], "glossy", [1.0, 1.0, 1.0, 1.0], node_input, shader)
__exposeNodeTreeInput(glo.inputs["Roughness"], "glossy_rough", 0.0, node_input, shader)
__exposeNodeTreeInput(mix.inputs["Fac"], "reflection", 0.02, node_input, shader)
__exposeNodeTreeOutput(mix.outputs["Shader"], "shader", node_output, shader)
return shader
def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]:
yield node
if node.parent:
yield node.parent
for n in set(l.from_node for i in node.inputs for l in i.links):
yield from __enum_linked_nodes(n)
def __cleanNodeTree(material: bpy.types.Material):
nodes = material.node_tree.nodes
node_names = set(n.name for n in nodes)
for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}):
if any(i.is_linked for i in o.inputs):
node_names -= set(linked.name for linked in __enum_linked_nodes(o))
for name in node_names:
nodes.remove(nodes[name])
def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
__switchToCyclesRenderEngine()
convertToBlenderShader(obj, use_principled, clean_nodes, subsurface)
def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
for i in obj.material_slots:
if not i.material:
continue
if not i.material.use_nodes:
i.material.use_nodes = True
__convertToMMDBasicShader(i.material)
if use_principled:
__convertToPrincipledBsdf(i.material, subsurface)
if clean_nodes:
__cleanNodeTree(i.material)
def convertToMMDShader(obj):
"""BSDF -> MMDShaderDev conversion."""
for i in obj.material_slots:
if not i.material:
continue
if not i.material.use_nodes:
i.material.use_nodes = True
FnMaterial.convert_to_mmd_material(i.material)
def __convertToMMDBasicShader(material: bpy.types.Material):
# TODO: test me
mmd_basic_shader_grp = create_MMDBasicShader()
mmd_alpha_shader_grp = create_MMDAlphaShader()
if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)):
# Add nodes for Cycles Render
shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
shader.node_tree = mmd_basic_shader_grp
shader.inputs[0].default_value[:3] = material.diffuse_color[:3]
shader.inputs[1].default_value[:3] = material.specular_color[:3]
shader.inputs["glossy_rough"].default_value = 1.0 / getattr(material, "specular_hardness", 50)
outplug = shader.outputs[0]
location = shader.location.copy()
location.x -= 1000
alpha_value = 1.0
if len(material.diffuse_color) > 3:
alpha_value = material.diffuse_color[3]
if alpha_value < 1.0:
alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
alpha_shader.location.x = shader.location.x + 250
alpha_shader.location.y = shader.location.y - 150
alpha_shader.node_tree = mmd_alpha_shader_grp
alpha_shader.inputs[1].default_value = alpha_value
material.node_tree.links.new(alpha_shader.inputs[0], outplug)
outplug = alpha_shader.outputs[0]
material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial")
material.node_tree.links.new(material_output.inputs["Surface"], outplug)
material_output.location.x = shader.location.x + 500
material_output.location.y = shader.location.y - 150
def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float):
node_names = set()
for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)):
if s.node_tree.name == "MMDBasicShader":
l: bpy.types.NodeLink
for l in s.outputs[0].links:
to_node = l.to_node
# assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader
if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader":
__switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node)
node_names.add(to_node.name)
else:
__switchToPrincipledBsdf(material.node_tree, s, subsurface)
node_names.add(s.name)
elif s.node_tree.name == "MMDShaderDev":
__switchToPrincipledBsdf(material.node_tree, s, subsurface)
node_names.add(s.name)
# remove MMD shader nodes
nodes = material.node_tree.nodes
for name in node_names:
nodes.remove(nodes[name])
def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None):
shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
shader.parent = node_basic.parent
shader.location.x = node_basic.location.x
shader.location.y = node_basic.location.y
alpha_socket_name = "Alpha"
if node_basic.node_tree.name == "MMDShaderDev":
node_alpha, alpha_socket_name = node_basic, "Base Alpha"
if "Base Tex" in node_basic.inputs and node_basic.inputs["Base Tex"].is_linked:
node_tree.links.new(node_basic.inputs["Base Tex"].links[0].from_socket, shader.inputs["Base Color"])
elif "Diffuse Color" in node_basic.inputs:
shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["Diffuse Color"].default_value[:3]
elif "diffuse" in node_basic.inputs:
shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["diffuse"].default_value[:3]
if node_basic.inputs["diffuse"].is_linked:
node_tree.links.new(node_basic.inputs["diffuse"].links[0].from_socket, shader.inputs["Base Color"])
shader.inputs["IOR"].default_value = 1.0
shader.inputs["Subsurface Weight"].default_value = subsurface
output_links = node_basic.outputs[0].links
if node_alpha:
output_links = node_alpha.outputs[0].links
shader.parent = node_alpha.parent or shader.parent
shader.location.x = node_alpha.location.x
if alpha_socket_name in node_alpha.inputs:
if "Alpha" in shader.inputs:
shader.inputs["Alpha"].default_value = node_alpha.inputs[alpha_socket_name].default_value
if node_alpha.inputs[alpha_socket_name].is_linked:
node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"])
else:
shader.inputs["Transmission"].default_value = 1 - node_alpha.inputs[alpha_socket_name].default_value
if node_alpha.inputs[alpha_socket_name].is_linked:
node_invert = node_tree.nodes.new("ShaderNodeMath")
node_invert.parent = shader.parent
node_invert.location.x = node_alpha.location.x - 250
node_invert.location.y = node_alpha.location.y - 300
node_invert.operation = "SUBTRACT"
node_invert.use_clamp = True
node_invert.inputs[0].default_value = 1
node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1])
node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"])
for l in output_links:
node_tree.links.new(shader.outputs[0], l.to_socket)
+8 -1
View File
@@ -1,7 +1,7 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.1)",
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.0)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.",
@@ -63,6 +63,13 @@
"PoseMode.basis": "Basis",
"Armature.validation.no_armature": "No armature selected",
"Armature.validation.pmx_model_detected": "PMX model detected. Japanese bone names may not match standard naming conventions.",
"Armature.validation.pmx_model_strict": "Consider using the 'Standardize Armature' option to convert Japanese bone names to standard names.",
"Armature.validation.pmx_model_standardize": "This will make the model compatible with standard avatar systems.",
"Armature.validation.pmx_model_basic": "PMX models use Japanese bone names which may not match standard naming conventions.",
"Armature.validation.unknown_format": "Unknown armature format detected.",
"Validation.mode.none": "Validation is disabled in settings.",
"Validation.no_messages": "No validation messages available.",
"Armature.validation.not_armature": "Selected object is not an armature",
"Armature.validation.no_bones": "Armature has no bones",
"Armature.validation.basic_check_failed": "Basic armature validation failed",
+8 -1
View File
@@ -1,7 +1,7 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AvatarToolkit.label": "アバターツールキット (アルファ 0.2.1)",
"AvatarToolkit.label": "アバターツールキット (アルファ 0.3.0)",
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AvatarToolkit.desc3": "GitHubで報告してください。",
@@ -63,6 +63,13 @@
"PoseMode.basis": "基本形",
"Armature.validation.no_armature": "アーマチュアが選択されていません",
"Armature.validation.pmx_model_detected": "PMXモデルが検出されました。日本語の骨名が標準の命名規則と一致しない場合があります。",
"Armature.validation.pmx_model_strict": "「アーマチュアの標準化」オプションを使用して、日本語の骨名を標準名に変換することを検討してください。",
"Armature.validation.pmx_model_standardize": "これにより、モデルが標準的なアバターシステムと互換性を持つようになります。",
"Armature.validation.pmx_model_basic": "PMXモデルは日本語の骨名を使用しており、標準の命名規則と一致しない場合があります。",
"Armature.validation.unknown_format": "不明なアーマチュア形式が検出されました。",
"Validation.mode.none": "検証は設定で無効になっています。",
"Validation.no_messages": "検証メッセージはありません。",
"Armature.validation.not_armature": "選択されたオブジェクトはアーマチュアではありません",
"Armature.validation.no_bones": "アーマチュアにボーンがありません",
"Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました",
+8 -1
View File
@@ -1,7 +1,7 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.2.1)",
"AvatarToolkit.label": "아바타 툴킷 (알파 0.3.0)",
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
@@ -63,6 +63,13 @@
"PoseMode.basis": "기본",
"Armature.validation.no_armature": "선택된 아마추어 없음",
"Armature.validation.pmx_model_detected": "PMX 모델이 감지되었습니다. 일본어 본 이름이 표준 명명 규칙과 일치하지 않을 수 있습니다.",
"Armature.validation.pmx_model_strict": "'아마추어 표준화' 옵션을 사용하여 일본어 본 이름을 표준 이름으로 변환하는 것을 고려하세요.",
"Armature.validation.pmx_model_standardize": "이렇게 하면 모델이 표준 아바타 시스템과 호환됩니다.",
"Armature.validation.pmx_model_basic": "PMX 모델은 일본어 본 이름을 사용하며 표준 명명 규칙과 일치하지 않을 수 있습니다.",
"Armature.validation.unknown_format": "알 수 없는 아마추어 형식이 감지되었습니다.",
"Validation.mode.none": "유효성 검사가 설정에서 비활성화되었습니다.",
"Validation.no_messages": "사용 가능한 유효성 검사 메시지가 없습니다.",
"Armature.validation.not_armature": "선택된 객체가 아마추어가 아님",
"Armature.validation.no_bones": "아마추어에 본이 없음",
"Armature.validation.basic_check_failed": "기본 아마추어 검증 실패",
+57 -11
View File
@@ -89,16 +89,33 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
if active_armature:
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True)
# Check if this is a PMX model
is_pmx_model = False
if hasattr(active_armature, 'mmd_type') or (hasattr(active_armature, 'parent') and active_armature.parent and hasattr(active_armature.parent, 'mmd_type')):
is_pmx_model = True
info_box = col.box()
# If it's a PMX model, display a prominent notice
if is_pmx_model:
pmx_box = info_box.box()
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
validation_mode = context.scene.avatar_toolkit.validation_mode
if validation_mode == 'STRICT':
pmx_box.label(text=t("Armature.validation.pmx_model_strict"))
pmx_box.label(text=t("Armature.validation.pmx_model_standardize"))
else:
pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
if not is_valid:
# Display non-standard bones and hierarchy issues
if len(messages) > 1:
if messages and len(messages) > 0:
# Found Bones section
validation_box = info_box.box()
row = validation_box.row()
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
if props.show_found_bones:
if props.show_found_bones and len(messages) > 0:
for line in messages[0].split('\n'):
validation_box.label(text=line)
@@ -127,15 +144,31 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
if props.show_non_standard:
if non_standard_messages:
if non_standard_messages and len(non_standard_messages) > 0:
for message in non_standard_messages:
for line in message.split('\n'):
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=line)
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_non_standard_issues"))
# For PMX models, if no non-standard messages but it's a PMX model,
# we should still indicate there might be non-standard bones
if is_pmx_model:
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_basic"))
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_strict"))
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_standardize"))
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_non_standard_issues"))
# Hierarchy Issues section
validation_box = info_box.box()
@@ -190,9 +223,14 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
row.label(text=msg.name)
else:
# If no specific issues, show acceptable message
info_box.label(text=messages[0], icon='INFO')
info_box.label(text=messages[1])
info_box.label(text=messages[2])
if messages and len(messages) > 0:
info_box.label(text=messages[0], icon='INFO')
if len(messages) > 1:
info_box.label(text=messages[1])
if len(messages) > 2:
info_box.label(text=messages[2])
else:
info_box.label(text=t("Validation.no_messages"), icon='INFO')
elif is_valid and not is_acceptable:
row = info_box.row()
split = row.split(factor=0.6)
@@ -204,9 +242,16 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
elif is_valid and is_acceptable:
# Show acceptable standard message
info_box.label(text=messages[0], icon='INFO')
info_box.label(text=messages[1])
info_box.label(text=messages[2])
if messages and len(messages) > 0:
info_box.label(text=messages[0], icon='INFO')
# Only try to access additional messages if they exist
if len(messages) > 1:
info_box.label(text=messages[1])
if len(messages) > 2:
info_box.label(text=messages[2])
else:
info_box.label(text=t("Validation.no_messages"), icon='INFO')
# Add standardize button
standardize_box = info_box.box()
@@ -252,3 +297,4 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
button_row.scale_y = 1.5
button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT')
button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT')