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" schema_version = "1.0.0"
id = "avatar_toolkit" id = "avatar_toolkit"
version = "0.2.1" version = "0.3.0"
name = "Avatar Toolkit" name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo" 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] = [] non_standard_messages: List[str] = []
scale_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': if validation_mode == 'NONE':
logger.debug("Validation mode is NONE, skipping validation") logger.debug("Validation mode is NONE, skipping validation")
if detailed_messages: if detailed_messages:
return True, [], False, [], [], [] return True, [t("Validation.mode.none")], False, [], [], []
else: else:
return True, [], False return True, [t("Validation.mode.none")], False
if not armature or armature.type != 'ARMATURE' or not armature.data.bones: if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
logger.warning("Basic armature check failed") 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.line2"))
non_standard_messages.append(t("Armature.validation.standardize_note.line3")) 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 # Combine messages in correct order
messages.extend(non_standard_messages) messages.extend(non_standard_messages)
@@ -149,6 +170,10 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
else: else:
return True, messages, True 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}") logger.info(f"Armature validation complete. Valid: {is_valid}")
if detailed_messages: if detailed_messages:
return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import contextlib import contextlib
from typing import Generator, List, Optional, TypeVar from typing import Generator, List, Optional, TypeVar, Any, Set, Tuple, Dict, Union
import bpy 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 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: class __EditMode:
def __init__(self, obj): def __init__(self, obj: Object):
if not isinstance(obj, bpy.types.Object): if not isinstance(obj, bpy.types.Object):
raise ValueError raise ValueError
self.__prevMode = obj.mode self.__prevMode = obj.mode
@@ -30,10 +34,10 @@ class __EditMode:
if obj.mode != "EDIT": if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
def __enter__(self): def __enter__(self) -> Any:
return self.__obj.data return self.__obj.data
def __exit__(self, type, value, traceback): def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
if self.__prevMode == "EDIT": if self.__prevMode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update edited data bpy.ops.object.mode_set(mode="OBJECT") # update edited data
bpy.ops.object.mode_set(mode=self.__prevMode) bpy.ops.object.mode_set(mode=self.__prevMode)
@@ -41,17 +45,18 @@ class __EditMode:
class __SelectObjects: 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): if not isinstance(active_object, bpy.types.Object):
raise ValueError raise ValueError
try: try:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
except Exception: except Exception:
logger.debug("Failed to set object mode")
pass 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) i.select_set(False)
self.__active_object = active_object self.__active_object = active_object
@@ -60,23 +65,23 @@ class __SelectObjects:
self.__hides: List[bool] = [] self.__hides: List[bool] = []
for i in self.__selected_objects: for i in self.__selected_objects:
self.__hides.append(i.hide_get()) self.__hides.append(i.hide_get())
FnContext.select_object(contenxt, i) FnContext.select_object(context, i)
FnContext.set_active_object(contenxt, active_object) FnContext.set_active_object(context, active_object)
def __enter__(self) -> bpy.types.Object: def __enter__(self) -> Object:
return self.__active_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): for i, j in zip(self.__selected_objects, self.__hides):
i.hide_set(j) i.hide_set(j)
def setParent(obj, parent): def setParent(obj: Object, parent: Object) -> None:
with select_object(parent, objects=[parent, obj]): with select_object(parent, objects=[parent, obj]):
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False) 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]): with select_object(parent, objects=[parent, obj]):
bpy.ops.object.mode_set(mode="POSE") bpy.ops.object.mode_set(mode="POSE")
parent.data.bones.active = parent.data.bones[bone_name] 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") bpy.ops.object.mode_set(mode="OBJECT")
def edit_object(obj): def edit_object(obj: Object) -> __EditMode:
"""Set the object interaction mode to 'EDIT' """Set the object interaction mode to 'EDIT'
It is recommended to use 'edit_object' with 'with' statement like the following code. 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) 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. """Select objects.
It is recommended to use 'select_object' with 'with' statement like the following code. 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) 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) 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) context = FnContext.ensure_context(target_scene)
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data)) 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 import bmesh
if target_object is None: if target_object is None:
target_object = createObject(name="Sphere") 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 mesh = target_object.data
bm = bmesh.new() bm = bmesh.new()
@@ -138,12 +146,15 @@ def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
return target_object 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 import bmesh
from mathutils import Matrix from mathutils import Matrix
if target_object is None: if target_object is None:
target_object = createObject(name="Box") 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 mesh = target_object.data
bm = bmesh.new() bm = bmesh.new()
@@ -159,13 +170,16 @@ def makeBox(size=(1, 1, 1), target_object=None):
return target_object 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 math
import bmesh import bmesh
if target_object is None: if target_object is None:
target_object = createObject(name="Capsule") 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) height = max(height, 1e-3)
mesh = target_object.data 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: 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 @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) c = constraints.get(name, None)
if c and c.type != "TRANSFORM": if c and c.type != "TRANSFORM":
constraints.remove(c) constraints.remove(c)
@@ -245,7 +259,7 @@ class TransformConstraintOp:
return c return c
@classmethod @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) key = (map_type, name_id)
ret = cls.__MIN_MAX_MAP.get(key, None) ret = cls.__MIN_MAX_MAP.get(key, None)
if ret is None: if ret is None:
@@ -255,7 +269,7 @@ class TransformConstraintOp:
return ret return ret
@classmethod @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 c = constraint
if not c or c.type != "TRANSFORM": if not c or c.type != "TRANSFORM":
return return
@@ -279,14 +293,14 @@ class FnObject:
raise NotImplementedError("This class is not expected to be instantiated.") raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod @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) 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 assert key == mesh_object.data.shape_keys
if mesh_object.animation_data is not None: 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: for fc_curve in mesh_object.animation_data.drivers:
if not fc_curve.data_path.startswith(shape_key.path_from_id()): if not fc_curve.data_path.startswith(shape_key.path_from_id()):
continue continue
@@ -310,35 +324,35 @@ class FnContext:
raise NotImplementedError("This class is not expected to be instantiated.") raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod @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 return context or bpy.context
@staticmethod @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 return context.active_object
@staticmethod @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 context.view_layer.objects.active = obj
return obj return obj
@staticmethod @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)) return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
@staticmethod @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 return context.scene.objects
@staticmethod @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_viewport = False
obj.hide_select = False obj.hide_select = False
obj.hide_set(False) obj.hide_set(False)
if obj not in context.selectable_objects: 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: for lc in layer_collection.children:
if __layer_check(lc): if __layer_check(lc):
lc.hide_viewport = False lc.hide_viewport = False
@@ -360,44 +374,44 @@ class FnContext:
return obj return obj
@staticmethod @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) FnContext.ensure_selectable(context, obj).select_set(True)
return obj return obj
@staticmethod @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] return [FnContext.select_object(context, obj) for obj in objects]
@staticmethod @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: for i in context.selected_objects:
if i != obj: if i != obj:
i.select_set(False) i.select_set(False)
return FnContext.select_object(context, obj) return FnContext.select_object(context, obj)
@staticmethod @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) context.collection.objects.link(obj)
return obj return obj
@staticmethod @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)) return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
@staticmethod @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. Duplicate object.
This function duplicates the given object and returns a list of duplicated objects. This function duplicates the given object and returns a list of duplicated objects.
Args: Args:
context (bpy.types.Context): The context in which the duplication is performed. context (Context): The context in which the duplication is performed.
object_to_duplicate (bpy.types.Object): The object to be duplicated. object_to_duplicate (Object): The object to be duplicated.
target_count (int): The desired count of duplicated objects. target_count (int): The desired count of duplicated objects.
Returns: Returns:
List[bpy.types.Object]: A list of duplicated objects. List[Object]: A list of duplicated objects.
Raises: 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. 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[i].select_set(True)
last_selected_objects = context.selected_objects last_selected_objects = context.selected_objects
assert len(result_objects) == target_count assert len(result_objects) == target_count
logger.debug(f"Duplicated object {object_to_duplicate.name} to create {target_count} objects")
return result_objects return result_objects
@staticmethod @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. Finds the layer collection that contains the given target_object in the user's collections.
Args: Args:
context (bpy.types.Context): The Blender context. context (Context): The Blender context.
target_object (bpy.types.Object): The target object to find the layer collection for. target_object (Object): The target object to find the layer collection for.
Returns: 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: if layer_collection.name == name:
return layer_collection return layer_collection
child_layer_collection: bpy.types.LayerCollection child_layer_collection: LayerCollection
for child_layer_collection in layer_collection.children: for child_layer_collection in layer_collection.children:
found = find_layer_collection_by_name(child_layer_collection, name) found = find_layer_collection_by_name(child_layer_collection, name)
if found is not None: if found is not None:
@@ -449,7 +464,7 @@ class FnContext:
return None return None
user_collection: bpy.types.Collection user_collection: Collection
for user_collection in target_object.users_collection: for user_collection in target_object.users_collection:
found = find_layer_collection_by_name(scene_layer_collection, user_collection.name) found = find_layer_collection_by_name(scene_layer_collection, user_collection.name)
if found is not None: if found is not None:
@@ -459,7 +474,7 @@ class FnContext:
@staticmethod @staticmethod
@contextlib.contextmanager @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. 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. It ensures that the original active_layer_collection is restored after the context is exited.
Args: Args:
context (bpy.types.Context): The context in which the active_layer_collection will be overridden. context (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. target_object (Object): The target object whose layer collection will be set as the active_layer_collection.
Yields: Yields:
bpy.types.Context: The modified context with the active_layer_collection overridden. Context: The modified context with the active_layer_collection overridden.
Example: Example:
with FnContext.temp_override_active_layer_collection(context, target_object): 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 context.view_layer.active_layer_collection = original_layer_collection
@staticmethod @staticmethod
def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]: def __get_addon_preferences(context: Context) -> Optional[AddonPreferences]:
addon: bpy.types.Addon = context.preferences.addons.get(__package__, None) addon: Addon = context.preferences.addons.get(__package__, None)
return addon.preferences if addon else None return addon.preferences if addon else None
@staticmethod @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) return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value)
@staticmethod @staticmethod
def temp_override_objects( def temp_override_objects(
context: bpy.types.Context, context: Context,
window: Optional[bpy.types.Window] = None, window: Optional[Window] = None,
area: Optional[bpy.types.Area] = None, area: Optional[Area] = None,
region: Optional[bpy.types.Region] = None, region: Optional[Region] = None,
active_object: Optional[bpy.types.Object] = None, active_object: Optional[Object] = None,
selected_objects: Optional[List[bpy.types.Object]] = None, selected_objects: Optional[List[Object]] = None,
**keywords, **keywords: Any,
) -> Generator[bpy.types.Context, None, None]: ) -> Generator[Context, None, None]:
if active_object is not None: if active_object is not None:
keywords["active_object"] = active_object keywords["active_object"] = active_object
keywords["object"] = active_object keywords["object"] = active_object
+168 -77
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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import TYPE_CHECKING, Iterable, Optional, Set from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Dict, Tuple, Any, Union, cast
import bpy import bpy
from mathutils import Vector from mathutils import Vector
from bpy.types import Object, EditBone, PoseBone, Constraint, Armature, BoneCollection
from .. import bpyutils from .. import bpyutils
from ..bpyutils import TransformConstraintOp from ..bpyutils import TransformConstraintOp
from ..utils import ItemOp from ..utils import ItemOp
from ....core.logging_setup import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from ..properties.root import MMDRoot, MMDDisplayItemFrame from ..properties.root import MMDRoot, MMDDisplayItemFrame
from ..properties.pose_bone import MMDBone from ..properties.pose_bone import MMDBone
def remove_constraint(constraints, name): def remove_constraint(constraints: Any, name: str) -> bool:
"""Remove a constraint by name if it exists"""
c = constraints.get(name, None) c = constraints.get(name, None)
if c: if c:
constraints.remove(c) constraints.remove(c)
return True return True
return False return False
def remove_edit_bones(edit_bones: bpy.types.ArmatureEditBones, bone_names: List[str]) -> None:
def remove_edit_bones(edit_bones, bone_names): """Remove edit bones by name"""
for name in bone_names: for name in bone_names:
b = edit_bones.get(name, None) b = edit_bones.get(name, None)
if b: if b:
@@ -45,33 +48,39 @@ SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NA
class FnBone: class FnBone:
AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首") AUTO_LOCAL_AXIS_ARMS: Tuple[str, ...] = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指") AUTO_LOCAL_AXIS_FINGERS: Tuple[str, ...] = ("親指", "人指", "中指", "薬指", "小指")
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー") AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: Tuple[str, ...] = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
def __init__(self): def __init__(self) -> None:
raise NotImplementedError("This class cannot be instantiated.") raise NotImplementedError("This class cannot be instantiated.")
@staticmethod @staticmethod
def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]: def find_pose_bone_by_bone_id(armature_object: Object, bone_id: int) -> Optional[PoseBone]:
"""Find a pose bone by its bone ID"""
for bone in armature_object.pose.bones: for bone in armature_object.pose.bones:
if bone.mmd_bone.bone_id != bone_id: if bone.mmd_bone.bone_id != bone_id:
continue continue
return bone return bone
logger.debug(f"Bone with ID {bone_id} not found in armature {armature_object.name}")
return None return None
@staticmethod @staticmethod
def __new_bone_id(armature_object: bpy.types.Object) -> int: def __new_bone_id(armature_object: Object) -> int:
"""Generate a new unique bone ID"""
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1 return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
@staticmethod @staticmethod
def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int: def get_or_assign_bone_id(pose_bone: PoseBone) -> int:
"""Get the bone ID or assign a new one if not set"""
if pose_bone.mmd_bone.bone_id < 0: if pose_bone.mmd_bone.bone_id < 0:
pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data) pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data)
logger.debug(f"Assigned new bone ID {pose_bone.mmd_bone.bone_id} to bone {pose_bone.name}")
return pose_bone.mmd_bone.bone_id return pose_bone.mmd_bone.bone_id
@staticmethod @staticmethod
def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]: def __get_selected_pose_bones(armature_object: Object) -> Iterable[PoseBone]:
"""Get selected pose bones from the armature"""
if armature_object.mode == "EDIT": if armature_object.mode == "EDIT":
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
bpy.ops.object.mode_set(mode="EDIT") # back to edit mode bpy.ops.object.mode_set(mode="EDIT") # back to edit mode
@@ -80,9 +89,11 @@ class FnBone:
return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone) return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone)
@staticmethod @staticmethod
def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True): def load_bone_fixed_axis(armature_object: Object, enable: bool = True) -> None:
"""Load fixed axis settings for selected bones"""
logger.debug(f"Loading bone fixed axis (enable={enable}) for {armature_object.name}")
for b in FnBone.__get_selected_pose_bones(armature_object): for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone: MMDBone = b.mmd_bone mmd_bone = b.mmd_bone
mmd_bone.enabled_fixed_axis = enable mmd_bone.enabled_fixed_axis = enable
lock_rotation = b.lock_rotation[:] lock_rotation = b.lock_rotation[:]
if enable: if enable:
@@ -97,53 +108,66 @@ class FnBone:
b.lock_location = b.lock_scale = (False, False, False) b.lock_location = b.lock_scale = (False, False, False)
@staticmethod @staticmethod
def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object: def setup_special_bone_collections(armature_object: Object) -> Object:
armature: bpy.types.Armature = armature_object.data """Set up special bone collections for MMD"""
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections bone_collections = armature.collections
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES: for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
if bone_collection_name in bone_collections: if bone_collection_name in bone_collections:
continue continue
bone_collection = bone_collections.new(bone_collection_name) bone_collection = bone_collections.new(bone_collection_name)
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False) FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
logger.debug(f"Created special bone collection: {bone_collection_name}")
return armature_object return armature_object
@staticmethod @staticmethod
def __is_mmd_tools_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: def __is_mmd_tools_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is an MMD Tools collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
@staticmethod @staticmethod
def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: def __is_special_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is a special MMD collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
@staticmethod @staticmethod
def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool): def __set_bone_collection_to_special(bone_collection: BoneCollection, is_visible: bool) -> None:
"""Mark a bone collection as special"""
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
bone_collection.is_visible = is_visible bone_collection.is_visible = is_visible
@staticmethod @staticmethod
def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: def __is_normal_bone_collection(bone_collection: BoneCollection) -> bool:
"""Check if a bone collection is a normal MMD collection"""
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
@staticmethod @staticmethod
def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection): def __set_bone_collection_to_normal(bone_collection: BoneCollection) -> None:
"""Mark a bone collection as normal"""
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
@staticmethod @staticmethod
def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone: def __set_edit_bone_to_special(edit_bone: EditBone, bone_collection_name: str) -> EditBone:
"""Set an edit bone to a special collection"""
edit_bone.id_data.collections[bone_collection_name].assign(edit_bone) edit_bone.id_data.collections[bone_collection_name].assign(edit_bone)
edit_bone.use_deform = False edit_bone.use_deform = False
return edit_bone return edit_bone
@staticmethod @staticmethod
def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: def set_edit_bone_to_dummy(edit_bone: EditBone) -> EditBone:
"""Set an edit bone as a dummy bone"""
logger.debug(f"Setting bone {edit_bone.name} as dummy bone")
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY) return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
@staticmethod @staticmethod
def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: def set_edit_bone_to_shadow(edit_bone: EditBone) -> EditBone:
"""Set an edit bone as a shadow bone"""
logger.debug(f"Setting bone {edit_bone.name} as shadow bone")
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW) return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
@staticmethod @staticmethod
def __unassign_mmd_tools_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: def __unassign_mmd_tools_bone_collections(edit_bone: EditBone) -> EditBone:
"""Unassign an edit bone from all MMD Tools collections"""
for bone_collection in edit_bone.collections: for bone_collection in edit_bone.collections:
if not FnBone.__is_mmd_tools_bone_collection(bone_collection): if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
continue continue
@@ -151,18 +175,24 @@ class FnBone:
return edit_bone return edit_bone
@staticmethod @staticmethod
def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object): def sync_bone_collections_from_display_item_frames(armature_object: Object) -> None:
armature: bpy.types.Armature = armature_object.data """Synchronize bone collections from display item frames"""
logger.info(f"Syncing bone collections from display item frames for {armature_object.name}")
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections bone_collections = armature.collections
from .model import FnModel from .model import FnModel
root_object: bpy.types.Object = FnModel.find_root_object(armature_object) root_object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root if not root_object:
logger.error(f"No root object found for armature {armature_object.name}")
return
mmd_root = root_object.mmd_root
bones = armature.bones bones = armature.bones
used_groups = set() used_groups: Set[str] = set()
unassigned_bone_names = {b.name for b in bones} unassigned_bone_names: Set[str] = {b.name for b in bones}
for frame in mmd_root.display_item_frames: for frame in mmd_root.display_item_frames:
for item in frame.data: for item in frame.data:
@@ -174,6 +204,7 @@ class FnBone:
if bone_collection is None: if bone_collection is None:
bone_collection = bone_collections.new(name=group_name) bone_collection = bone_collections.new(name=group_name)
FnBone.__set_bone_collection_to_normal(bone_collection) FnBone.__set_bone_collection_to_normal(bone_collection)
logger.debug(f"Created new bone collection: {group_name}")
bone_collection.assign(bones[item.name]) bone_collection.assign(bones[item.name])
for name in unassigned_bone_names: for name in unassigned_bone_names:
@@ -192,32 +223,40 @@ class FnBone:
continue continue
if not FnBone.__is_normal_bone_collection(bone_collection): if not FnBone.__is_normal_bone_collection(bone_collection):
continue continue
logger.debug(f"Removing unused bone collection: {bone_collection.name}")
bone_collections.remove(bone_collection) bone_collections.remove(bone_collection)
@staticmethod @staticmethod
def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object): def sync_display_item_frames_from_bone_collections(armature_object: Object) -> None:
armature: bpy.types.Armature = armature_object.data """Synchronize display item frames from bone collections"""
bone_collections: bpy.types.BoneCollections = armature.collections logger.info(f"Syncing display item frames from bone collections for {armature_object.name}")
armature = cast(Armature, armature_object.data)
bone_collections = armature.collections
from .model import FnModel from .model import FnModel
root_object: bpy.types.Object = FnModel.find_root_object(armature_object) root_object = FnModel.find_root_object(armature_object)
mmd_root: MMDRoot = root_object.mmd_root if not root_object:
logger.error(f"No root object found for armature {armature_object.name}")
return
mmd_root = root_object.mmd_root
display_item_frames = mmd_root.display_item_frames display_item_frames = mmd_root.display_item_frames
used_frame_index: Set[int] = set() used_frame_index: Set[int] = set()
bone_collection: bpy.types.BoneCollection bone_collection: BoneCollection
for bone_collection in bone_collections: for bone_collection in bone_collections:
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection): if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
continue continue
bone_collection_name = bone_collection.name bone_collection_name = bone_collection.name
display_item_frame: Optional[MMDDisplayItemFrame] = display_item_frames.get(bone_collection_name) display_item_frame = display_item_frames.get(bone_collection_name)
if display_item_frame is None: if display_item_frame is None:
display_item_frame = display_item_frames.add() display_item_frame = display_item_frames.add()
display_item_frame.name = bone_collection_name display_item_frame.name = bone_collection_name
display_item_frame.name_e = bone_collection_name display_item_frame.name_e = bone_collection_name
logger.debug(f"Created new display item frame: {bone_collection_name}")
used_frame_index.add(display_item_frames.find(bone_collection_name)) used_frame_index.add(display_item_frames.find(bone_collection_name))
ItemOp.resize(display_item_frame.data, len(bone_collection.bones)) ItemOp.resize(display_item_frame.data, len(bone_collection.bones))
@@ -232,23 +271,27 @@ class FnBone:
if display_item_frame.is_special: if display_item_frame.is_special:
if display_item_frame.name != "表情": if display_item_frame.name != "表情":
display_item_frame.data.clear() display_item_frame.data.clear()
logger.debug(f"Cleared special display item frame: {display_item_frame.name}")
else: else:
logger.debug(f"Removing unused display item frame: {display_item_frames[i].name}")
display_item_frames.remove(i) display_item_frames.remove(i)
mmd_root.active_display_item_frame = 0 mmd_root.active_display_item_frame = 0
@staticmethod @staticmethod
def apply_bone_fixed_axis(armature_object: bpy.types.Object): def apply_bone_fixed_axis(armature_object: Object) -> None:
bone_map = {} """Apply fixed axis to bones"""
logger.info(f"Applying bone fixed axis for {armature_object.name}")
bone_map: Dict[str, Tuple[Vector, bool, bool]] = {}
for b in armature_object.pose.bones: for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis: if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
continue continue
mmd_bone: MMDBone = b.mmd_bone mmd_bone = b.mmd_bone
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip) bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
force_align = True force_align = True
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone bone: EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_map: if bone.name not in bone_map:
bone.select = False bone.select = False
@@ -279,6 +322,7 @@ class FnBone:
else: else:
bone_map[bone.name] = (True, True, True) bone_map[bone.name] = (True, True, True)
bone.select = True bone.select = True
logger.debug(f"Applied fixed axis to bone: {bone.name}")
for bone_name, locks in bone_map.items(): for bone_name, locks in bone_map.items():
b = armature_object.pose.bones[bone_name] b = armature_object.pose.bones[bone_name]
@@ -286,9 +330,11 @@ class FnBone:
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
@staticmethod @staticmethod
def load_bone_local_axes(armature_object: bpy.types.Object, enable=True): def load_bone_local_axes(armature_object: Object, enable: bool = True) -> None:
"""Load local axes for selected bones"""
logger.debug(f"Loading bone local axes (enable={enable}) for {armature_object.name}")
for b in FnBone.__get_selected_pose_bones(armature_object): for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone: MMDBone = b.mmd_bone mmd_bone = b.mmd_bone
mmd_bone.enabled_local_axes = enable mmd_bone.enabled_local_axes = enable
if enable: if enable:
axes = b.bone.matrix_local.to_3x3().transposed() axes = b.bone.matrix_local.to_3x3().transposed()
@@ -296,16 +342,18 @@ class FnBone:
mmd_bone.local_axis_z = axes[2].xzy mmd_bone.local_axis_z = axes[2].xzy
@staticmethod @staticmethod
def apply_bone_local_axes(armature_object: bpy.types.Object): def apply_bone_local_axes(armature_object: Object) -> None:
bone_map = {} """Apply local axes to bones"""
logger.info(f"Applying bone local axes for {armature_object.name}")
bone_map: Dict[str, Tuple[Vector, Vector]] = {}
for b in armature_object.pose.bones: for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes: if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
continue continue
mmd_bone: MMDBone = b.mmd_bone mmd_bone = b.mmd_bone
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z) bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone bone: EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_map: if bone.name not in bone_map:
bone.select = False bone.select = False
@@ -313,15 +361,18 @@ class FnBone:
local_axis_x, local_axis_z = bone_map[bone.name] local_axis_x, local_axis_z = bone_map[bone.name]
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z) FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
bone.select = True bone.select = True
logger.debug(f"Applied local axes to bone: {bone.name}")
@staticmethod @staticmethod
def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z): def update_bone_roll(edit_bone: EditBone, mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> None:
"""Update bone roll based on local axes"""
axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z) axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z)
idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1])) idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3]) edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
@staticmethod @staticmethod
def get_axes(mmd_local_axis_x, mmd_local_axis_z): def get_axes(mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> Tuple[Vector, Vector, Vector]:
"""Get axes from local axis vectors"""
x_axis = Vector(mmd_local_axis_x).normalized().xzy x_axis = Vector(mmd_local_axis_x).normalized().xzy
z_axis = Vector(mmd_local_axis_z).normalized().xzy z_axis = Vector(mmd_local_axis_z).normalized().xzy
y_axis = z_axis.cross(x_axis).normalized() y_axis = z_axis.cross(x_axis).normalized()
@@ -329,21 +380,25 @@ class FnBone:
return (x_axis, y_axis, z_axis) return (x_axis, y_axis, z_axis)
@staticmethod @staticmethod
def apply_auto_bone_roll(armature): def apply_auto_bone_roll(armature: Object) -> None:
bone_names = [] """Apply automatic bone roll to appropriate bones"""
logger.info(f"Applying auto bone roll for {armature.name}")
bone_names: List[str] = []
for b in armature.pose.bones: for b in armature.pose.bones:
if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j): if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j):
bone_names.append(b.name) bone_names.append(b.name)
with bpyutils.edit_object(armature) as data: with bpyutils.edit_object(armature) as data:
bone: bpy.types.EditBone bone: EditBone
for bone in data.edit_bones: for bone in data.edit_bones:
if bone.name not in bone_names: if bone.name not in bone_names:
continue continue
FnBone.update_auto_bone_roll(bone) FnBone.update_auto_bone_roll(bone)
bone.select = True bone.select = True
logger.debug(f"Applied auto bone roll to bone: {bone.name}")
@staticmethod @staticmethod
def update_auto_bone_roll(edit_bone): def update_auto_bone_roll(edit_bone: EditBone) -> None:
"""Update bone roll automatically"""
# make a triangle face (p1,p2,p3) # make a triangle face (p1,p2,p3)
p1 = edit_bone.head.copy() p1 = edit_bone.head.copy()
p2 = edit_bone.tail.copy() p2 = edit_bone.tail.copy()
@@ -364,7 +419,8 @@ class FnBone:
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy) FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
@staticmethod @staticmethod
def has_auto_local_axis(name_j): def has_auto_local_axis(name_j: str) -> bool:
"""Check if a bone should have automatic local axis"""
if name_j: if name_j:
if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
return True return True
@@ -374,9 +430,11 @@ class FnBone:
return False return False
@staticmethod @staticmethod
def clean_additional_transformation(armature_object: bpy.types.Object): def clean_additional_transformation(armature_object: Object) -> None:
"""Clean additional transformation constraints and bones"""
logger.info(f"Cleaning additional transformations for {armature_object.name}")
# clean constraints # clean constraints
p_bone: bpy.types.PoseBone p_bone: PoseBone
for p_bone in armature_object.pose.bones: for p_bone in armature_object.pose.bones:
p_bone.mmd_bone.is_additional_transform_dirty = True p_bone.mmd_bone.is_additional_transform_dirty = True
constraints = p_bone.constraints constraints = p_bone.constraints
@@ -392,17 +450,21 @@ class FnBone:
"ADDITIONAL_TRANSFORM_INVERT", "ADDITIONAL_TRANSFORM_INVERT",
} }
def __is_at_shadow_bone(b): def __is_at_shadow_bone(b: PoseBone) -> bool:
return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)] shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
if len(shadow_bone_names) > 0: if len(shadow_bone_names) > 0:
logger.debug(f"Removing {len(shadow_bone_names)} shadow bones")
with bpyutils.edit_object(armature_object) as data: with bpyutils.edit_object(armature_object) as data:
remove_edit_bones(data.edit_bones, shadow_bone_names) remove_edit_bones(data.edit_bones, shadow_bone_names)
@staticmethod @staticmethod
def apply_additional_transformation(armature_object: bpy.types.Object): def apply_additional_transformation(armature_object: Object) -> None:
def __is_dirty_bone(b): """Apply additional transformation to bones"""
logger.info(f"Applying additional transformations for {armature_object.name}")
def __is_dirty_bone(b: PoseBone) -> bool:
if b.is_mmd_shadow_bone: if b.is_mmd_shadow_bone:
return False return False
mmd_bone = b.mmd_bone mmd_bone = b.mmd_bone
@@ -411,9 +473,10 @@ class FnBone:
return mmd_bone.is_additional_transform_dirty return mmd_bone.is_additional_transform_dirty
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)] dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
logger.debug(f"Found {len(dirty_bones)} dirty bones to process")
# setup constraints # setup constraints
shadow_bone_pool = [] shadow_bone_pool: List[Union[_AT_ShadowBoneRemove, _AT_ShadowBoneCreate]] = []
for p_bone in dirty_bones: for p_bone in dirty_bones:
sb = FnBone.__setup_constraints(p_bone) sb = FnBone.__setup_constraints(p_bone)
if sb: if sb:
@@ -434,7 +497,8 @@ class FnBone:
p_bone.mmd_bone.is_additional_transform_dirty = False p_bone.mmd_bone.is_additional_transform_dirty = False
@staticmethod @staticmethod
def __setup_constraints(p_bone): def __setup_constraints(p_bone: PoseBone) -> Optional[Union['_AT_ShadowBoneRemove', '_AT_ShadowBoneCreate']]:
"""Set up constraints for additional transformation"""
bone_name = p_bone.name bone_name = p_bone.name
mmd_bone = p_bone.mmd_bone mmd_bone = p_bone.mmd_bone
influence = mmd_bone.additional_transform_influence influence = mmd_bone.additional_transform_influence
@@ -447,12 +511,14 @@ class FnBone:
rot = remove_constraint(constraints, "mmd_additional_rotation") rot = remove_constraint(constraints, "mmd_additional_rotation")
loc = remove_constraint(constraints, "mmd_additional_location") loc = remove_constraint(constraints, "mmd_additional_location")
if rot or loc: if rot or loc:
logger.debug(f"Removing additional transform constraints for bone: {bone_name}")
return _AT_ShadowBoneRemove(bone_name) return _AT_ShadowBoneRemove(bone_name)
return None return None
logger.debug(f"Setting up additional transform for bone: {bone_name} targeting {target_bone}")
shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone) shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone)
def __config(name, mute, map_type, value): def __config(name: str, mute: bool, map_type: str, value: float) -> None:
if mute: if mute:
remove_constraint(constraints, name) remove_constraint(constraints, name)
return return
@@ -467,62 +533,81 @@ class FnBone:
return shadow_bone return shadow_bone
@staticmethod @staticmethod
def update_additional_transform_influence(pose_bone: bpy.types.PoseBone): def update_additional_transform_influence(pose_bone: PoseBone) -> None:
"""Update the influence of additional transform constraints"""
influence = pose_bone.mmd_bone.additional_transform_influence influence = pose_bone.mmd_bone.additional_transform_influence
constraints = pose_bone.constraints constraints = pose_bone.constraints
c = constraints.get("mmd_additional_rotation", None) c = constraints.get("mmd_additional_rotation", None)
TransformConstraintOp.update_min_max(c, math.pi, influence) TransformConstraintOp.update_min_max(c, math.pi, influence)
c = constraints.get("mmd_additional_location", None) c = constraints.get("mmd_additional_location", None)
TransformConstraintOp.update_min_max(c, 100, influence) TransformConstraintOp.update_min_max(c, 100, influence)
logger.debug(f"Updated additional transform influence for bone: {pose_bone.name} to {influence}")
class MigrationFnBone: class MigrationFnBone:
"""Migration Functions for old MMD models broken by bugs or issues""" """Migration Functions for old MMD models broken by bugs or issues"""
@staticmethod @staticmethod
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object): def fix_mmd_ik_limit_override(armature_object: Object) -> None:
pose_bone: bpy.types.PoseBone """Fix IK limit override constraints in old MMD models"""
logger.info(f"Fixing MMD IK limit overrides for {armature_object.name}")
pose_bone: PoseBone
for pose_bone in armature_object.pose.bones: for pose_bone in armature_object.pose.bones:
constraint: bpy.types.Constraint constraint: Constraint
for constraint in pose_bone.constraints: for constraint in pose_bone.constraints:
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name: if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
constraint.owner_space = "LOCAL" constraint.owner_space = "LOCAL"
logger.debug(f"Fixed IK limit override for bone: {pose_bone.name}")
class _AT_ShadowBoneRemove: class _AT_ShadowBoneRemove:
def __init__(self, bone_name): """Handler for removing shadow bones"""
def __init__(self, bone_name: str) -> None:
"""Initialize with bone name"""
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name) self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
def update_edit_bones(self, edit_bones): def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
"""Update edit bones by removing shadow bones"""
remove_edit_bones(edit_bones, self.__shadow_bone_names) remove_edit_bones(edit_bones, self.__shadow_bone_names)
logger.debug(f"Removed shadow bones: {self.__shadow_bone_names}")
def update_pose_bones(self, pose_bones): def update_pose_bones(self, pose_bones: Any) -> None:
"""Update pose bones (no-op for removal)"""
pass pass
class _AT_ShadowBoneCreate: class _AT_ShadowBoneCreate:
def __init__(self, bone_name, target_bone_name): """Handler for creating shadow bones"""
def __init__(self, bone_name: str, target_bone_name: str) -> None:
"""Initialize with bone names"""
self.__dummy_bone_name = "_dummy_" + bone_name self.__dummy_bone_name = "_dummy_" + bone_name
self.__shadow_bone_name = "_shadow_" + bone_name self.__shadow_bone_name = "_shadow_" + bone_name
self.__bone_name = bone_name self.__bone_name = bone_name
self.__target_bone_name = target_bone_name self.__target_bone_name = target_bone_name
self.__constraint_pool = [] self.__constraint_pool: List[Constraint] = []
def __is_well_aligned(self, bone0, bone1): def __is_well_aligned(self, bone0: EditBone, bone1: EditBone) -> bool:
"""Check if two bones are well aligned"""
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99 return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
def __update_constraints(self, use_shadow=True): def __update_constraints(self, use_shadow: bool = True) -> None:
"""Update constraints to use shadow or target bone"""
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
for c in self.__constraint_pool: for c in self.__constraint_pool:
c.subtarget = subtarget c.subtarget = subtarget
def add_constraint(self, constraint): def add_constraint(self, constraint: Constraint) -> None:
"""Add a constraint to the pool"""
self.__constraint_pool.append(constraint) self.__constraint_pool.append(constraint)
def update_edit_bones(self, edit_bones): def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
"""Update edit bones by creating shadow bones"""
bone = edit_bones[self.__bone_name] bone = edit_bones[self.__bone_name]
target_bone = edit_bones[self.__target_bone_name] target_bone = edit_bones[self.__target_bone_name]
if bone != target_bone and self.__is_well_aligned(bone, target_bone): if bone != target_bone and self.__is_well_aligned(bone, target_bone):
logger.debug(f"Bones are well aligned, removing shadow bones for {self.__bone_name}")
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones) _AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
return return
@@ -532,6 +617,7 @@ class _AT_ShadowBoneCreate:
dummy.head = target_bone.head dummy.head = target_bone.head
dummy.tail = dummy.head + bone.tail - bone.head dummy.tail = dummy.head + bone.tail - bone.head
dummy.roll = bone.roll dummy.roll = bone.roll
logger.debug(f"Created/updated dummy bone: {dummy_bone_name}")
shadow_bone_name = self.__shadow_bone_name shadow_bone_name = self.__shadow_bone_name
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name)) shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
@@ -539,9 +625,12 @@ class _AT_ShadowBoneCreate:
shadow.head = dummy.head shadow.head = dummy.head
shadow.tail = dummy.tail shadow.tail = dummy.tail
shadow.roll = bone.roll shadow.roll = bone.roll
logger.debug(f"Created/updated shadow bone: {shadow_bone_name}")
def update_pose_bones(self, pose_bones): def update_pose_bones(self, pose_bones: Any) -> None:
"""Update pose bones by setting up shadow bone properties"""
if self.__shadow_bone_name not in pose_bones: if self.__shadow_bone_name not in pose_bones:
logger.debug(f"Shadow bone {self.__shadow_bone_name} not found, using target bone directly")
self.__update_constraints(use_shadow=False) self.__update_constraints(use_shadow=False)
return return
@@ -560,5 +649,7 @@ class _AT_ShadowBoneCreate:
c.subtarget = dummy_p_bone.name c.subtarget = dummy_p_bone.name
c.target_space = "POSE" c.target_space = "POSE"
c.owner_space = "POSE" c.owner_space = "POSE"
logger.debug(f"Created copy transforms constraint for shadow bone: {self.__shadow_bone_name}")
self.__update_constraints() self.__update_constraints()
logger.debug(f"Updated constraints for shadow bone: {self.__shadow_bone_name}")
+88 -23
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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import Optional from typing import Optional, List, Tuple, Callable, Any, Union
import bpy import bpy
from bpy.types import Object, ID, Camera, Context
from mathutils import Vector, Matrix, Euler
from ..bpyutils import FnContext, Props from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
class FnCamera: class FnCamera:
@staticmethod @staticmethod
def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]: def find_root(obj: Optional[Object]) -> Optional[Object]:
"""Find the root object of an MMD camera setup."""
if obj is None: if obj is None:
return None return None
if FnCamera.is_mmd_camera_root(obj): if FnCamera.is_mmd_camera_root(obj):
@@ -25,16 +28,22 @@ class FnCamera:
return None return None
@staticmethod @staticmethod
def is_mmd_camera(obj: bpy.types.Object) -> bool: def is_mmd_camera(obj: Object) -> bool:
"""Check if an object is an MMD camera."""
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
@staticmethod @staticmethod
def is_mmd_camera_root(obj: bpy.types.Object) -> bool: def is_mmd_camera_root(obj: Object) -> bool:
"""Check if an object is an MMD camera root."""
return obj.type == "EMPTY" and obj.mmd_type == "CAMERA" return obj.type == "EMPTY" and obj.mmd_type == "CAMERA"
@staticmethod @staticmethod
def add_drivers(camera_object: bpy.types.Object): def add_drivers(camera_object: Object) -> None:
def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1): """Add drivers to the camera object for MMD camera functionality."""
logger.debug(f"Adding drivers to camera: {camera_object.name}")
def __add_driver(id_data: ID, data_path: str, expression: str, index: int = -1) -> None:
"""Add a driver to the specified ID data."""
d = id_data.driver_add(data_path, index).driver d = id_data.driver_add(data_path, index).driver
d.type = "SCRIPTED" d.type = "SCRIPTED"
if "$empty_distance" in expression: if "$empty_distance" in expression:
@@ -72,22 +81,36 @@ class FnCamera:
d.expression = expression d.expression = expression
try:
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45") __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, "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, "type", "not $is_perspective")
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2") __add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
logger.debug(f"Successfully added drivers to camera: {camera_object.name}")
except Exception as e:
logger.error(f"Failed to add drivers to camera {camera_object.name}: {str(e)}")
@staticmethod @staticmethod
def remove_drivers(camera_object: bpy.types.Object): def remove_drivers(camera_object: Object) -> None:
"""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.data.driver_remove("ortho_scale")
camera_object.driver_remove("rotation_euler") camera_object.driver_remove("rotation_euler")
camera_object.data.driver_remove("ortho_scale") camera_object.data.driver_remove("ortho_scale")
camera_object.data.driver_remove("lens") camera_object.data.driver_remove("lens")
logger.debug(f"Successfully removed drivers from camera: {camera_object.name}")
except Exception as e:
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {str(e)}")
class MigrationFnCamera: class MigrationFnCamera:
@staticmethod @staticmethod
def update_mmd_camera(): def update_mmd_camera() -> None:
"""Update all MMD cameras in the scene."""
logger.info("Updating all MMD cameras in the scene")
updated_count = 0
for camera_object in bpy.data.objects: for camera_object in bpy.data.objects:
if camera_object.type != "CAMERA": if camera_object.type != "CAMERA":
continue continue
@@ -97,39 +120,57 @@ class MigrationFnCamera:
# It's not a MMD Camera # It's not a MMD Camera
continue continue
try:
FnCamera.remove_drivers(camera_object) FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object) FnCamera.add_drivers(camera_object)
updated_count += 1
except Exception as e:
logger.error(f"Failed to update MMD camera {camera_object.name}: {str(e)}")
logger.info(f"Updated {updated_count} MMD cameras")
class MMDCamera: class MMDCamera:
def __init__(self, obj): def __init__(self, obj: Object):
"""Initialize an MMD camera."""
root_object = FnCamera.find_root(obj) root_object = FnCamera.find_root(obj)
if root_object is None: if root_object is None:
raise ValueError("%s is not MMDCamera" % str(obj)) logger.error(f"Object {obj.name} is not an MMD camera")
raise ValueError(f"{obj.name} is not an MMD camera")
self.__emptyObj = getattr(root_object, "original", obj) self.__emptyObj = getattr(root_object, "original", obj)
logger.debug(f"Initialized MMD camera with root: {self.__emptyObj.name}")
@staticmethod @staticmethod
def isMMDCamera(obj: bpy.types.Object) -> bool: def isMMDCamera(obj: Object) -> bool:
"""Check if an object is an MMD camera."""
return FnCamera.find_root(obj) is not None return FnCamera.find_root(obj) is not None
@staticmethod @staticmethod
def addDrivers(cameraObj: bpy.types.Object): def addDrivers(cameraObj: Object) -> None:
"""Add drivers to the camera object."""
FnCamera.add_drivers(cameraObj) FnCamera.add_drivers(cameraObj)
@staticmethod @staticmethod
def removeDrivers(cameraObj: bpy.types.Object): def removeDrivers(cameraObj: Object) -> None:
"""Remove drivers from the camera object. """
if cameraObj.type != "CAMERA": if cameraObj.type != "CAMERA":
return return
FnCamera.remove_drivers(cameraObj) FnCamera.remove_drivers(cameraObj)
@staticmethod @staticmethod
def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0): def convertToMMDCamera(cameraObj: Object, scale: float = 1.0) -> 'MMDCamera':
"""Convert a camera to an MMD camera."""
logger.info(f"Converting camera {cameraObj.name} to MMD camera with scale {scale}")
if FnCamera.is_mmd_camera(cameraObj): if FnCamera.is_mmd_camera(cameraObj):
logger.debug(f"Camera {cameraObj.name} is already an MMD camera")
return MMDCamera(cameraObj) return MMDCamera(cameraObj)
try:
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None) empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty) context = FnContext.ensure_context()
FnContext.link_object(context, empty)
cameraObj.parent = empty cameraObj.parent = empty
cameraObj.data.sensor_fit = "VERTICAL" cameraObj.data.sensor_fit = "VERTICAL"
@@ -153,24 +194,39 @@ class MMDCamera:
empty.mmd_type = "CAMERA" empty.mmd_type = "CAMERA"
empty.mmd_camera.angle = math.radians(30) empty.mmd_camera.angle = math.radians(30)
empty.mmd_camera.persp = True empty.mmd_camera.persp = True
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
return MMDCamera(empty) return MMDCamera(empty)
except Exception as e:
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {str(e)}")
raise
@staticmethod @staticmethod
def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1): def newMMDCameraAnimation(
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 scene = bpy.context.scene
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera")) mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
FnContext.link_object(FnContext.ensure_context(), mmd_cam) FnContext.link_object(FnContext.ensure_context(), mmd_cam)
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale) MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
mmd_cam_root = mmd_cam.parent mmd_cam_root = mmd_cam.parent
_camera_override_func = None _camera_override_func: Optional[Callable[[], Object]] = None
if cameraObj is None: if cameraObj is None:
if scene.camera is None: if scene.camera is None:
scene.camera = mmd_cam scene.camera = mmd_cam
logger.debug("Set scene camera to new MMD camera")
return MMDCamera(mmd_cam_root) return MMDCamera(mmd_cam_root)
_camera_override_func = lambda: scene.camera _camera_override_func = lambda: scene.camera
_target_override_func = None _target_override_func: Optional[Callable[[Object], Object]] = None
if cameraTarget is None: if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj _target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
@@ -180,7 +236,6 @@ class MMDCamera:
FnCamera.remove_drivers(mmd_cam) FnCamera.remove_drivers(mmd_cam)
from math import atan from math import atan
from mathutils import Matrix, Vector from mathutils import Matrix, Vector
render = scene.render render = scene.render
@@ -202,6 +257,7 @@ class MMDCamera:
for c in fcurves: for c in fcurves:
c.keyframe_points.add(frame_count) 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)): 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) scene.frame_set(f)
if _camera_override_func: if _camera_override_func:
@@ -245,13 +301,22 @@ class MMDCamera:
mmd_cam_root.animation_data_create().action = parent_action mmd_cam_root.animation_data_create().action = parent_action
mmd_cam.animation_data_create().action = distance_action mmd_cam.animation_data_create().action = distance_action
scene.frame_set(frame_current) scene.frame_set(frame_current)
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
return MMDCamera(mmd_cam_root) return MMDCamera(mmd_cam_root)
def object(self): except Exception as e:
logger.error(f"Failed to create MMD camera animation: {str(e)}")
raise
def object(self) -> Object:
"""Get the root object of the MMD camera."""
return self.__emptyObj return self.__emptyObj
def camera(self): def camera(self) -> Object:
"""Get the camera object of the MMD camera."""
for i in self.__emptyObj.children: for i in self.__emptyObj.children:
if i.type == "CAMERA": if i.type == "CAMERA":
return i return i
raise KeyError logger.error(f"No camera found for MMD camera root {self.__emptyObj.name}")
raise KeyError(f"No camera found for MMD camera root {self.__emptyObj.name}")
+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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, Union, Any, List, Tuple
from bpy.types import Object, Context
from ..bpyutils import FnContext, Props from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
class MMDLamp: class MMDLamp:
def __init__(self, obj): def __init__(self, obj: Object) -> None:
if MMDLamp.isLamp(obj): if MMDLamp.isLamp(obj):
obj = obj.parent obj = obj.parent
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT": if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
self.__emptyObj = obj self.__emptyObj: Object = obj
else: 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 @staticmethod
def isLamp(obj): def isLamp(obj: Optional[Object]) -> bool:
return obj and obj.type in {"LIGHT", "LAMP"} """Check if the object is a lamp/light object"""
return obj is not None and obj.type in {"LIGHT", "LAMP"}
@staticmethod @staticmethod
def isMMDLamp(obj): def isMMDLamp(obj: Optional[Object]) -> bool:
"""Check if the object is an MMD lamp"""
if MMDLamp.isLamp(obj): if MMDLamp.isLamp(obj):
obj = obj.parent 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 @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): if MMDLamp.isMMDLamp(lampObj):
logger.debug(f"Object {lampObj.name} is already an MMD lamp")
return MMDLamp(lampObj) return MMDLamp(lampObj)
empty = bpy.data.objects.new(name="MMD_Light", object_data=None) logger.info(f"Converting {lampObj.name} to MMD lamp with scale {scale}")
FnContext.link_object(FnContext.ensure_context(), empty)
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.rotation_mode = "XYZ"
empty.lock_rotation = (True, True, True) empty.lock_rotation = (True, True, True)
@@ -57,13 +69,18 @@ class MMDLamp:
constraint.track_axis = "TRACK_NEGATIVE_Z" constraint.track_axis = "TRACK_NEGATIVE_Z"
constraint.up_axis = "UP_Y" constraint.up_axis = "UP_Y"
logger.debug(f"Successfully created MMD lamp from {lampObj.name}")
return MMDLamp(empty) return MMDLamp(empty)
def object(self): def object(self) -> Object:
"""Get the empty object that represents this MMD lamp"""
return self.__emptyObj return self.__emptyObj
def lamp(self): def lamp(self) -> Object:
"""Get the actual lamp/light object"""
for i in self.__emptyObj.children: for i in self.__emptyObj.children:
if MMDLamp.isLamp(i): if MMDLamp.isLamp(i):
return 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 logging
import os 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 import bpy
from mathutils import Vector from mathutils import Vector
@@ -15,6 +15,7 @@ from mathutils import Vector
from ..bpyutils import FnContext from ..bpyutils import FnContext
from .exceptions import MaterialNotFoundError from .exceptions import MaterialNotFoundError
from .shader import _NodeGroupUtils from .shader import _NodeGroupUtils
from ....core.logging_setup import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from ..properties.material import MMDMaterial from ..properties.material import MMDMaterial
@@ -27,48 +28,53 @@ SPHERE_MODE_SUBTEX = 3
class _DummyTexture: class _DummyTexture:
def __init__(self, image): def __init__(self, image: bpy.types.Image):
self.type = "IMAGE" self.type: str = "IMAGE"
self.image = image self.image: bpy.types.Image = image
self.use_mipmap = True self.use_mipmap: bool = True
class _DummyTextureSlot: class _DummyTextureSlot:
def __init__(self, image): def __init__(self, image: bpy.types.Image):
self.diffuse_color_factor = 1 self.diffuse_color_factor: float = 1
self.uv_layer = "" self.uv_layer: str = ""
self.texture = _DummyTexture(image) self.texture: _DummyTexture = _DummyTexture(image)
class FnMaterial: class FnMaterial:
__NODES_ARE_READONLY: bool = False __NODES_ARE_READONLY: bool = False
def __init__(self, material: bpy.types.Material): def __init__(self, material: bpy.types.Material):
self.__material = material self.__material: bpy.types.Material = material
self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY self._nodes_are_readonly: bool = FnMaterial.__NODES_ARE_READONLY
@staticmethod @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 FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
@classmethod @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: for material in bpy.data.materials:
if material.mmd_material.material_id == material_id: if material.mmd_material.material_id == material_id:
return cls(material) return cls(material)
return None return None
@staticmethod @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 = obj.data.materials
materials_pop = materials.pop materials_pop = materials.pop
removed_count = 0
for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True): for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True):
m = materials_pop(index=i) m = materials_pop(index=i)
removed_count += 1
if m.users < 1: if m.users < 1:
bpy.data.materials.remove(m) bpy.data.materials.remove(m)
if removed_count > 0:
logger.debug(f"Removed {removed_count} materials from {obj.name}")
@staticmethod @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. 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. 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: except (KeyError, IndexError) as exc:
# Wrap exceptions within our custom ones # Wrap exceptions within our custom ones
raise MaterialNotFoundError() from exc raise MaterialNotFoundError() from exc
mat1_idx = mesh.materials.find(mat1.name) mat1_idx = mesh.materials.find(mat1.name)
mat2_idx = mesh.materials.find(mat2.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 # Swap polygons
for poly in mesh.polygons: for poly in mesh.polygons:
if poly.material_index == mat1_idx: if poly.material_index == mat1_idx:
@@ -113,33 +123,37 @@ class FnMaterial:
return mat1, mat2 return mat1, mat2
@staticmethod @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. This method will fix the material order. Which is lost after joining meshes.
""" """
materials = cast(bpy.types.Mesh, meshObj.data).materials 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): for new_idx, mat in enumerate(material_names):
# Get the material that is currently on this index # Get the material that is currently on this index
other_mat = materials[new_idx] other_mat = materials[new_idx]
if other_mat.name == mat: if other_mat.name == mat:
continue # This is already in place 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) FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True)
@property @property
def material_id(self): def material_id(self) -> int:
mmd_mat: MMDMaterial = self.__material.mmd_material mmd_mat: 'MMDMaterial' = self.__material.mmd_material
if mmd_mat.material_id < 0: if mmd_mat.material_id < 0:
max_id = -1 max_id = -1
for mat in bpy.data.materials: for mat in bpy.data.materials:
max_id = max(max_id, mat.mmd_material.material_id) max_id = max(max_id, mat.mmd_material.material_id)
mmd_mat.material_id = max_id + 1 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 return mmd_mat.material_id
@property @property
def material(self): def material(self) -> bpy.types.Material:
return self.__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": if image and image.source == "FILE":
# pylint: disable=assignment-from-no-return # pylint: disable=assignment-from-no-return
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user() img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
@@ -152,14 +166,15 @@ class FnMaterial:
pass pass
return False 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) img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None)
if img is None: if img is None:
# pylint: disable=bare-except # pylint: disable=bare-except
try: try:
logger.debug(f"Loading image: {filepath}")
img = bpy.data.images.load(filepath) img = bpy.data.images.load(filepath)
except: 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 = bpy.data.images.new(os.path.basename(filepath), 1, 1)
img.source = "FILE" img.source = "FILE"
img.filepath = filepath img.filepath = filepath
@@ -170,43 +185,46 @@ class FnMaterial:
img.alpha_mode = "NONE" img.alpha_mode = "NONE"
return img return img
def update_toon_texture(self): def update_toon_texture(self) -> None:
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mmd_mat: MMDMaterial = self.__material.mmd_material mmd_mat: 'MMDMaterial' = self.__material.mmd_material
if mmd_mat.is_shared_toon_texture: if mmd_mat.is_shared_toon_texture:
shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "") 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)) toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1))
logger.debug(f"Using shared toon texture: {toon_path}")
self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path)) self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path))
elif mmd_mat.toon_texture != "": elif mmd_mat.toon_texture != "":
logger.debug(f"Using custom toon texture: {mmd_mat.toon_texture}")
self.create_toon_texture(mmd_mat.toon_texture) self.create_toon_texture(mmd_mat.toon_texture)
else: else:
logger.debug(f"Removing toon texture from {self.__material.name}")
self.remove_toon_texture() 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 r, g, b = mmd_mat.diffuse_color
ar, ag, ab = mmd_mat.ambient_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)] 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 pass
def update_enabled_toon_edge(self): def update_enabled_toon_edge(self) -> None:
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
self.update_edge_color() self.update_edge_color()
def update_edge_color(self): def update_edge_color(self) -> None:
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.__material 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] color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),) line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
if hasattr(mat, "line_color"): # freestyle line color if hasattr(mat, "line_color"): # freestyle line color
mat.line_color = 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: if mat_edge:
mat_edge.mmd_material.edge_color = line_color mat_edge.mmd_material.edge_color = line_color
@@ -218,38 +236,45 @@ class FnMaterial:
if node_shader and "Alpha" in node_shader.inputs: if node_shader and "Alpha" in node_shader.inputs:
node_shader.inputs["Alpha"].default_value = alpha node_shader.inputs["Alpha"].default_value = alpha
def update_edge_weight(self): logger.debug(f"Updated edge color for {mat.name}")
def update_edge_weight(self) -> None:
pass pass
def get_texture(self): def get_texture(self) -> Optional[_DummyTexture]:
return self.__get_texture_node("mmd_base_tex", use_dummy=True) 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)) 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) return _DummyTextureSlot(texture.image)
def remove_texture(self): def remove_texture(self) -> None:
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
logger.debug(f"Removing base texture from {self.__material.name}")
self.__remove_texture_node("mmd_base_tex") 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) 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: if self._nodes_are_readonly:
return return
if use_sphere: if use_sphere:
logger.debug(f"Enabling sphere texture for {self.__material.name}")
self.update_sphere_texture_type(obj) self.update_sphere_texture_type(obj)
else: else:
logger.debug(f"Disabling sphere texture for {self.__material.name}")
self.__update_shader_input("Sphere Tex Fac", 0) 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)) 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) self.update_sphere_texture_type(obj)
return _DummyTextureSlot(texture.image) 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: if self._nodes_are_readonly:
return return
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type) sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
@@ -277,48 +302,54 @@ class FnMaterial:
next(uv_layers, None) # skip base UV next(uv_layers, None) # skip base UV
subtex_uv = getattr(next(uv_layers, None), "name", "") subtex_uv = getattr(next(uv_layers, None), "name", "")
if subtex_uv != "UV1": 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"]) links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
else: else:
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"]) links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
def remove_sphere_texture(self): logger.debug(f"Updated sphere texture type for {self.material.name}: {sphere_texture_type}")
def remove_sphere_texture(self) -> None:
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
logger.debug(f"Removing sphere texture from {self.__material.name}")
self.__remove_texture_node("mmd_sphere_tex") 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) 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: if self._nodes_are_readonly:
return 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) 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)) 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) return _DummyTextureSlot(texture.image)
def remove_toon_texture(self): def remove_toon_texture(self) -> None:
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
logger.debug(f"Removing toon texture from {self.__material.name}")
self.__remove_texture_node("mmd_toon_tex") 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 mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None) texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage): if isinstance(texture, bpy.types.ShaderNodeTexImage):
return _DummyTexture(texture.image) if use_dummy else texture return _DummyTexture(texture.image) if use_dummy else texture
return None return None
def __remove_texture_node(self, node_name): def __remove_texture_node(self, node_name: str) -> None:
mat = self.material mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None) texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage): if isinstance(texture, bpy.types.ShaderNodeTexImage):
mat.node_tree.nodes.remove(texture) mat.node_tree.nodes.remove(texture)
mat.update_tag() 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) texture = self.__get_texture_node(node_name)
if texture is None: if texture is None:
from mathutils import Vector from mathutils import Vector
@@ -334,23 +365,25 @@ class FnMaterial:
self.__update_shader_nodes() self.__update_shader_nodes()
return texture return texture
def update_ambient_color(self): def update_ambient_color(self) -> None:
if self._nodes_are_readonly: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat) mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,)) 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: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat) mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,)) 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: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
@@ -368,16 +401,18 @@ class FnMaterial:
mat.diffuse_color[3] = mmd_mat.alpha mat.diffuse_color[3] = mmd_mat.alpha
self.__update_shader_input("Alpha", mmd_mat.alpha) self.__update_shader_input("Alpha", mmd_mat.alpha)
self.update_self_shadow_map() 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: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
mat.specular_color = mmd_mat.specular_color mat.specular_color = mmd_mat.specular_color
self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,)) 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: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
@@ -388,8 +423,9 @@ class FnMaterial:
if hasattr(mat, "specular_hardness"): if hasattr(mat, "specular_hardness"):
mat.specular_hardness = mmd_mat.shininess mat.specular_hardness = mmd_mat.shininess
self.__update_shader_input("Reflect", 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: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
@@ -399,8 +435,9 @@ class FnMaterial:
elif hasattr(mat, "use_backface_culling"): elif hasattr(mat, "use_backface_culling"):
mat.use_backface_culling = not mmd_mat.is_double_sided mat.use_backface_culling = not mmd_mat.is_double_sided
self.__update_shader_input("Double Sided", 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: if self._nodes_are_readonly:
return return
mat = self.material 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 cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
if hasattr(mat, "shadow_method"): if hasattr(mat, "shadow_method"):
mat.shadow_method = "HASHED" if cast_shadows else "NONE" 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: if self._nodes_are_readonly:
return return
mat = self.material mat = self.material
mmd_mat = mat.mmd_material mmd_mat = mat.mmd_material
self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow) 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 @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 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: 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": if node.type == "TEX_IMAGE":
return node return node
for node_input in node.inputs: for node_input in node.inputs:
@@ -459,6 +499,7 @@ class FnMaterial:
if tex_node is None: if tex_node is None:
tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None) tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None)
if tex_node: if tex_node:
logger.debug(f"Found texture node for {material.name}: {tex_node.name}")
tex_node.name = "mmd_base_tex" tex_node.name = "mmd_base_tex"
else: else:
# Take the Base Color from BSDF if there's no texture # Take the Base Color from BSDF if there's no texture
@@ -466,6 +507,7 @@ class FnMaterial:
if bsdf_node: if bsdf_node:
base_color_input = bsdf_node.inputs.get('Base Color') or bsdf_node.inputs.get('Color') base_color_input = bsdf_node.inputs.get('Base Color') or bsdf_node.inputs.get('Color')
if base_color_input: if base_color_input:
logger.debug(f"Using BSDF base color for {material.name}")
mmd_material.diffuse_color = base_color_input.default_value[:3] mmd_material.diffuse_color = base_color_input.default_value[:3]
# ambient should be half the diffuse # ambient should be half the diffuse
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color] mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
@@ -498,9 +540,10 @@ class FnMaterial:
if m.use_nodes: if m.use_nodes:
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED' or n.type.startswith('BSDF_')] nodes_to_remove = [n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED' or n.type.startswith('BSDF_')]
for n in nodes_to_remove: for n in nodes_to_remove:
logger.debug(f"Removing BSDF node from {material.name}: {n.name}")
m.node_tree.nodes.remove(n) 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 mat = self.material
if mat.name.startswith("mmd_"): # skip mmd_edge.* if mat.name.startswith("mmd_"): # skip mmd_edge.*
return return
@@ -512,26 +555,29 @@ class FnMaterial:
val = min(max(val, interface_socket.min_value), interface_socket.max_value) val = min(max(val, interface_socket.min_value), interface_socket.max_value)
shader.inputs[name].default_value = val shader.inputs[name].default_value = val
def __update_shader_nodes(self): def __update_shader_nodes(self) -> None:
mat = self.material mat = self.material
if mat.node_tree is None: if mat.node_tree is None:
logger.debug(f"Creating node tree for {mat.name}")
mat.use_nodes = True mat.use_nodes = True
mat.node_tree.nodes.clear() mat.node_tree.nodes.clear()
nodes, links = mat.node_tree.nodes, mat.node_tree.links nodes, links = mat.node_tree.nodes, mat.node_tree.links
class _Dummy: class _Dummy:
default_value, is_linked = None, True default_value: Any = None
is_linked: bool = True
node_shader = nodes.get("mmd_shader", None) node_shader = nodes.get("mmd_shader", None)
if node_shader is 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: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
node_shader.name = "mmd_shader" node_shader.name = "mmd_shader"
node_shader.location = (0, 1500) node_shader.location = (0, 1500)
node_shader.width = 200 node_shader.width = 200
node_shader.node_tree = self.__get_shader() 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("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("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,)
node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_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) node_uv = nodes.get("mmd_tex_uv", None)
if node_uv is 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: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
node_uv.name = "mmd_tex_uv" node_uv.name = "mmd_tex_uv"
node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220)) 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: if not texture.inputs["Vector"].is_linked:
links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"]) 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" 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") 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): if len(shader.nodes):
return shader return shader
logger.debug(f"Creating MMD UV shader node group")
ng = _NodeGroupUtils(shader) ng = _NodeGroupUtils(shader)
############################################################################ ############################################################################
@@ -604,12 +652,13 @@ class FnMaterial:
return shader return shader
def __get_shader(self): def __get_shader(self) -> bpy.types.ShaderNodeTree:
group_name = "MMDShaderDev" 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") 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): if len(shader.nodes):
return shader return shader
logger.debug(f"Creating MMD shader node group")
ng = _NodeGroupUtils(shader) ng = _NodeGroupUtils(shader)
############################################################################ ############################################################################
@@ -699,15 +748,18 @@ class FnMaterial:
class MigrationFnMaterial: class MigrationFnMaterial:
@staticmethod @staticmethod
def update_mmd_shader(): def update_mmd_shader() -> None:
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev") mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
if mmd_shader_node_tree is None: if mmd_shader_node_tree is None:
logger.debug("No MMD shader node tree found, skipping update")
return return
ng = _NodeGroupUtils(mmd_shader_node_tree) ng = _NodeGroupUtils(mmd_shader_node_tree)
if "Color" in ng.node_output.inputs: if "Color" in ng.node_output.inputs:
logger.debug("MMD shader already has Color output, skipping update")
return 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] 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_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node
node_output: bpy.types.NodeGroupOutput = ng.node_output 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("Color", node_sphere.outputs["Color"])
ng.new_output_socket("Alpha", node_alpha.outputs["Value"]) 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))
+160 -88
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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import itertools import itertools
import logging
import time 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 bpy
import idprop import idprop
@@ -20,15 +19,17 @@ from ..bpyutils import FnContext, Props
from . import rigid_body from . import rigid_body
from .morph import FnMorph from .morph import FnMorph
from .rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC from .rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC
from ....core.logging_setup import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from ..properties.morph import MaterialMorphData from ..properties.morph import MaterialMorphData
from ..properties.rigid_body import MMDRigidBody from ..properties.rigid_body import MMDRigidBody
from bpy.types import Context, Object, PropertyGroup, Material, Mesh, Armature, EditBone, PoseBone, KinematicConstraint
class FnModel: class FnModel:
@staticmethod @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 {}) FnModel.__copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values or {})
@staticmethod @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. 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". 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:
obj = obj.parent if hasattr(obj, 'mmd_type') and obj.mmd_type == "ROOT":
return obj return obj
obj = obj.parent
return None
@staticmethod @staticmethod
def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: 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" return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE"
@staticmethod @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) parent_armature_object = FnModel.find_armature_object(parent_root_object)
with bpy.context.temp_override( with bpy.context.temp_override(
active_object=parent_armature_object, active_object=parent_armature_object,
@@ -221,7 +225,7 @@ class FnModel:
): ):
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) 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+.""" """This function will also update the references of bone morphs and rotate+/move+."""
bone_id = bone.mmd_bone.bone_id bone_id = bone.mmd_bone.bone_id
@@ -259,6 +263,7 @@ class FnModel:
child_root_object: bpy.types.Object child_root_object: bpy.types.Object
for child_root_object in child_root_objects: 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_armature_object = FnModel.find_armature_object(child_root_object)
child_pose_bones = child_armature_object.pose.bones child_pose_bones = child_armature_object.pose.bones
child_bone_morphs = child_root_object.mmd_root.bone_morphs 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) bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
# Disconnect mesh dependencies because transform_apply fails when mesh data are multiple used. # 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 in child_root_object.mmd_root.material_morphs:
for material_morph_data in material_morph.data: for material_morph_data in material_morph.data:
if material_morph_data.related_mesh_data is not None: if material_morph_data.related_mesh_data is not None:
@@ -353,6 +358,8 @@ class FnModel:
if len(child_root_object.children) == 0: if len(child_root_object.children) == 0:
bpy.data.objects.remove(child_root_object) bpy.data.objects.remove(child_root_object)
logger.info("Model joining completed successfully")
@staticmethod @staticmethod
def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier: def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier:
for m in mesh_object.modifiers: for m in mesh_object.modifiers:
@@ -369,10 +376,13 @@ class FnModel:
return modifier return modifier
@staticmethod @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) armature_object = FnModel.find_armature_object(parent_root_object)
if armature_object is None: 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: def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object:
if obj.parent is None: if obj.parent is None:
@@ -381,9 +391,11 @@ class FnModel:
for mesh_object in mesh_objects: for mesh_object in mesh_objects:
if not FnModel.is_mesh_object(mesh_object): if not FnModel.is_mesh_object(mesh_object):
logger.debug(f"Skipping non-mesh object: {mesh_object.name}")
continue continue
if FnModel.find_root_object(mesh_object) is not None: if FnModel.find_root_object(mesh_object) is not None:
logger.debug(f"Skipping mesh with existing root: {mesh_object.name}")
continue continue
mesh_root_object = __get_root_object(mesh_object) mesh_root_object = __get_root_object(mesh_object)
@@ -391,15 +403,20 @@ class FnModel:
mesh_root_object.parent_type = "OBJECT" mesh_root_object.parent_type = "OBJECT"
mesh_root_object.parent = armature_object mesh_root_object.parent = armature_object
mesh_root_object.matrix_world = original_matrix_world mesh_root_object.matrix_world = original_matrix_world
logger.debug(f"Attached mesh: {mesh_object.name}")
if add_armature_modifier: if add_armature_modifier:
FnModel._add_armature_modifier(mesh_object, armature_object) FnModel._add_armature_modifier(mesh_object, armature_object)
logger.debug(f"Added armature modifier to: {mesh_object.name}")
@staticmethod @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) armature_object = FnModel.find_armature_object(root_object)
if armature_object is None: 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() vertex_group_names: Set[str] = set()
@@ -408,6 +425,7 @@ class FnModel:
for search_mesh in search_meshes: for search_mesh in search_meshes:
vertex_group_names.update(search_mesh.vertex_groups.keys()) vertex_group_names.update(search_mesh.vertex_groups.keys())
added_count = 0
pose_bone: bpy.types.PoseBone pose_bone: bpy.types.PoseBone
for pose_bone in armature_object.pose.bones: for pose_bone in armature_object.pose.bones:
pose_bone_name = pose_bone.name pose_bone_name = pose_bone.name
@@ -419,28 +437,34 @@ class FnModel:
continue continue
mesh_object.vertex_groups.new(name=pose_bone_name) 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 @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 mmd_root = root_object.mmd_root
old_ik_loop_factor = mmd_root.ik_loop_factor old_ik_loop_factor = mmd_root.ik_loop_factor
if new_ik_loop_factor == old_ik_loop_factor: if new_ik_loop_factor == old_ik_loop_factor:
logger.debug("IK loop factor already set to the requested value")
return return
armature_object = FnModel.find_armature_object(root_object) armature_object = FnModel.find_armature_object(root_object)
updated_count = 0
for pose_bone in armature_object.pose.bones: 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"): 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) 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 constraint.iterations = iterations
updated_count += 1
mmd_root.ik_loop_factor = new_ik_loop_factor mmd_root.ik_loop_factor = new_ik_loop_factor
logger.info(f"Updated {updated_count} IK constraints")
return
@staticmethod @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 destination_rna_properties = destination.bl_rna.properties
for name in source.keys(): for name in source.keys():
is_attr = hasattr(source, name) is_attr = hasattr(source, name)
@@ -466,7 +490,7 @@ class FnModel:
destination[name] = value destination[name] = value
@staticmethod @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: if overwrite:
destination.clear() destination.clear()
@@ -499,16 +523,19 @@ class FnModel:
FnModel.__copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values) FnModel.__copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values)
@staticmethod @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): if isinstance(destination, bpy.types.PropertyGroup):
FnModel.__copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values) FnModel.__copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
elif isinstance(destination, bpy.types.bpy_prop_collection): elif isinstance(destination, bpy.types.bpy_prop_collection):
FnModel.__copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values) FnModel.__copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
else: else:
raise ValueError(f"Unsupported destination: {destination}") error_msg = f"Unsupported destination: {destination}"
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod @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 frames = root_object.mmd_root.display_item_frames
if reset and len(frames) > 0: if reset and len(frames) > 0:
root_object.mmd_root.active_display_item_frame = 0 root_object.mmd_root.active_display_item_frame = 0
@@ -532,6 +559,8 @@ class FnModel:
frames.move(frames.find("Root"), 0) frames.move(frames.find("Root"), 0)
frames.move(frames.find("表情"), 1) frames.move(frames.find("表情"), 1)
logger.debug(f"Display item frames initialized with {len(frames)} frames")
@staticmethod @staticmethod
def get_empty_display_size(root_object: bpy.types.Object) -> float: def get_empty_display_size(root_object: bpy.types.Object) -> float:
return getattr(root_object, Props.empty_display_size) return getattr(root_object, Props.empty_display_size)
@@ -541,19 +570,28 @@ class MigrationFnModel:
"""Migration Functions for old MMD models broken by bugs or issues""" """Migration Functions for old MMD models broken by bugs or issues"""
@classmethod @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: for armature_object in bpy.data.objects:
if armature_object.type != "ARMATURE": if armature_object.type != "ARMATURE":
continue continue
if "mmd_ik_loop_factor" not in armature_object: 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) 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"] del armature_object["mmd_ik_loop_factor"]
updated_count += 1
logger.info(f"Updated IK loop factor for {updated_count} armatures")
@staticmethod @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: for root_object in bpy.data.objects:
if root_object.type != "EMPTY": if root_object.type != "EMPTY":
continue continue
@@ -565,10 +603,13 @@ class MigrationFnModel:
continue continue
root_object["avatar_toolkit_version"] = "0.2.1" 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: class Model:
def __init__(self, root_obj): def __init__(self, root_obj: bpy.types.Object) -> None:
if root_obj is None: if root_obj is None:
raise ValueError("must be MMD ROOT type object") raise ValueError("must be MMD ROOT type object")
if root_obj.mmd_type != "ROOT": if root_obj.mmd_type != "ROOT":
@@ -578,13 +619,15 @@ class Model:
self.__rigid_grp: Optional[bpy.types.Object] = None self.__rigid_grp: Optional[bpy.types.Object] = None
self.__joint_grp: Optional[bpy.types.Object] = None self.__joint_grp: Optional[bpy.types.Object] = None
self.__temporary_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 @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: if obj_name is None:
obj_name = name obj_name = name
context = FnContext.ensure_context() 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: bpy.types.Object = bpy.data.objects.new(name=obj_name, object_data=None)
root.mmd_type = "ROOT" root.mmd_type = "ROOT"
@@ -595,6 +638,7 @@ class Model:
FnContext.link_object(context, root) FnContext.link_object(context, root)
if armature_object: if armature_object:
logger.debug(f"Using existing armature: {armature_object.name}")
m = armature_object.matrix_world m = armature_object.matrix_world
armature_object.parent_type = "OBJECT" armature_object.parent_type = "OBJECT"
armature_object.parent = root armature_object.parent = root
@@ -602,6 +646,7 @@ class Model:
root.matrix_world = m root.matrix_world = m
armature_object.matrix_local.identity() armature_object.matrix_local.identity()
else: 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 = bpy.data.objects.new(name=obj_name + "_arm", object_data=bpy.data.armatures.new(name=obj_name))
armature_object.parent = root armature_object.parent = root
FnContext.link_object(context, armature_object) FnContext.link_object(context, armature_object)
@@ -614,6 +659,7 @@ class Model:
FnBone.setup_special_bone_collections(armature_object) FnBone.setup_special_bone_collections(armature_object)
if add_root_bone: if add_root_bone:
logger.debug("Adding root bone")
bone_name = "全ての親" bone_name = "全ての親"
bone_name_english = "Root" bone_name_english = "Root"
@@ -637,34 +683,37 @@ class Model:
bone_collection.assign(data_bone) bone_collection.assign(data_bone)
FnContext.set_active_and_select_single_object(context, root) FnContext.set_active_and_select_single_object(context, root)
logger.info(f"Model created successfully: {name}")
return Model(root) return Model(root)
@staticmethod @staticmethod
def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]: def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
return FnModel.find_root_object(obj) 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) FnModel.initalize_display_item_frames(self.__root, reset=reset)
@property @property
def morph_slider(self): def morph_slider(self) -> Any:
return FnMorph.get_morph_slider(self) 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) 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 """create IK constraint
Args: Args:
bone: A pose bone to add a IK constraint 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: Returns:
The bpy.types.KinematicConstraint object created. It is set target The bpy.types.KinematicConstraint object created. It is set target
and subtarget options. and subtarget options.
""" """
logger.debug(f"Creating IK constraint on {bone.name} targeting {ik_target.name}")
ik_target_name = ik_target.name ik_target_name = ik_target.name
ik_const = bone.constraints.new("IK") ik_const = bone.constraints.new("IK")
ik_const.target = self.__arm ik_const.target = self.__arm
@@ -693,6 +742,7 @@ class Model:
if self.__rigid_grp is None: if self.__rigid_grp is None:
self.__rigid_grp = FnModel.find_rigid_group_object(self.__root) self.__rigid_grp = FnModel.find_rigid_group_object(self.__root)
if self.__rigid_grp is None: 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) rigids = bpy.data.objects.new(name="rigidbodies", object_data=None)
FnContext.link_object(FnContext.ensure_context(), rigids) FnContext.link_object(FnContext.ensure_context(), rigids)
rigids.mmd_type = "RIGID_GRP_OBJ" rigids.mmd_type = "RIGID_GRP_OBJ"
@@ -710,6 +760,7 @@ class Model:
if self.__joint_grp is None: if self.__joint_grp is None:
self.__joint_grp = FnModel.find_joint_group_object(self.__root) self.__joint_grp = FnModel.find_joint_group_object(self.__root)
if self.__joint_grp is None: 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) joints = bpy.data.objects.new(name="joints", object_data=None)
FnContext.link_object(FnContext.ensure_context(), joints) FnContext.link_object(FnContext.ensure_context(), joints)
joints.mmd_type = "JOINT_GRP_OBJ" joints.mmd_type = "JOINT_GRP_OBJ"
@@ -727,6 +778,7 @@ class Model:
if self.__temporary_grp is None: if self.__temporary_grp is None:
self.__temporary_grp = FnModel.find_temporary_group_object(self.__root) self.__temporary_grp = FnModel.find_temporary_group_object(self.__root)
if self.__temporary_grp is None: 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) temporarys = bpy.data.objects.new(name="temporary", object_data=None)
FnContext.link_object(FnContext.ensure_context(), temporarys) FnContext.link_object(FnContext.ensure_context(), temporarys)
temporarys.mmd_type = "TEMPORARY_GRP_OBJ" temporarys.mmd_type = "TEMPORARY_GRP_OBJ"
@@ -740,7 +792,7 @@ class Model:
def meshes(self) -> Iterator[bpy.types.Object]: def meshes(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_mesh_objects(self.__root) 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) FnModel.attach_mesh_objects(self.rootObject(), meshes, add_armature_modifier)
def firstMesh(self) -> Optional[bpy.types.Object]: def firstMesh(self) -> Optional[bpy.types.Object]:
@@ -748,7 +800,7 @@ class Model:
return i return i
return None 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 Helper method to find a mesh by name
""" """
@@ -787,25 +839,26 @@ class Model:
def joints(self) -> Iterator[bpy.types.Object]: def joints(self) -> Iterator[bpy.types.Object]:
return FnModel.iterate_joint_objects(self.__root) 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) return FnModel.iterate_temporary_objects(self.__root, rigid_track_only)
def materials(self) -> Iterator[bpy.types.Material]: def materials(self) -> Iterator[bpy.types.Material]:
""" """
Helper method to list all materials in all meshes 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(): for mesh in self.meshes():
materials.update((slot.material, 0) for slot in mesh.material_slots if slot.material is not None) materials.update((slot.material, 0) for slot in mesh.material_slots if slot.material is not None)
return iter(materials.keys()) 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: if old_bone_name == new_bone_name:
return return
logger.info(f"Renaming bone: {old_bone_name} -> {new_bone_name}")
armature = self.armature() armature = self.armature()
bone = armature.pose.bones[old_bone_name] bone = armature.pose.bones[old_bone_name]
bone.name = new_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 mmd_root = self.rootObject().mmd_root
for frame in mmd_root.display_item_frames: for frame in mmd_root.display_item_frames:
@@ -816,28 +869,31 @@ class Model:
if old_bone_name in mesh.vertex_groups: if old_bone_name in mesh.vertex_groups:
mesh.vertex_groups[old_bone_name].name = new_bone_name 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) rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
if self.__root.mmd_root.is_built: if self.__root.mmd_root.is_built:
logger.info("Model is already built, cleaning first")
self.clean() self.clean()
self.__root.mmd_root.is_built = True self.__root.mmd_root.is_built = True
logging.info("****************************************") logger.info("****************************************")
logging.info(" Build rig") logger.info(" Build rig")
logging.info("****************************************") logger.info("****************************************")
start_time = time.time() start_time = time.time()
self.__preBuild() self.__preBuild()
self.disconnectPhysicsBones() self.disconnectPhysicsBones()
self.buildRigids(non_collision_distance_scale, collision_margin) self.buildRigids(non_collision_distance_scale, collision_margin)
self.buildJoints() self.buildJoints()
self.__postBuild() 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) 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) rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
logging.info("****************************************") logger.info("****************************************")
logging.info(" Clean rig") logger.info(" Clean rig")
logging.info("****************************************") logger.info("****************************************")
start_time = time.time() start_time = time.time()
pose_bones = [] pose_bones = []
@@ -848,13 +904,14 @@ class Model:
if "mmd_tools_rigid_track" in i.constraints: if "mmd_tools_rigid_track" in i.constraints:
const = i.constraints["mmd_tools_rigid_track"] const = i.constraints["mmd_tools_rigid_track"]
i.constraints.remove(const) i.constraints.remove(const)
logger.debug(f"Removed rigid track constraint from {i.name}")
rigid_track_counts = 0 rigid_track_counts = 0
for i in self.rigidBodies(): for i in self.rigidBodies():
rigid_type = int(i.mmd_rigid.type) rigid_type = int(i.mmd_rigid.type)
if "mmd_tools_rigid_parent" not in i.constraints: if "mmd_tools_rigid_parent" not in i.constraints:
rigid_track_counts += 1 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 i.mmd_rigid.bone = i.mmd_rigid.bone
relation = i.constraints["mmd_tools_rigid_parent"] relation = i.constraints["mmd_tools_rigid_parent"]
relation.mute = True relation.mute = True
@@ -884,35 +941,39 @@ class Model:
mmd_root = self.rootObject().mmd_root mmd_root = self.rootObject().mmd_root
if mmd_root.show_temporary_objects: if mmd_root.show_temporary_objects:
mmd_root.show_temporary_objects = False 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 mmd_root.is_built = False
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled) 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()): with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()):
bpy.ops.object.delete() bpy.ops.object.delete()
def __restoreTransforms(self, obj): def __restoreTransforms(self, obj: bpy.types.Object) -> None:
for attr in ("location", "rotation_euler"): for attr in ("location", "rotation_euler"):
attr_name = "__backup_%s__" % attr attr_name = "__backup_%s__" % attr
val = obj.get(attr_name, None) val = obj.get(attr_name, None)
if val is not None: if val is not None:
setattr(obj, attr, val) setattr(obj, attr, val)
del obj[attr_name] 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"): for attr in ("location", "rotation_euler"):
attr_name = "__backup_%s__" % attr attr_name = "__backup_%s__" % attr
if attr_name in obj: # should not happen in normal build/clean cycle if attr_name in obj: # should not happen in normal build/clean cycle
continue continue
obj[attr_name] = getattr(obj, attr, None) obj[attr_name] = getattr(obj, attr, None)
logger.debug(f"Backed up {attr} for {obj.name}")
def __preBuild(self): def __preBuild(self) -> None:
self.__fake_parent_map = {} logger.debug("Pre-build preparation")
self.__rigid_body_matrix_map = {} self.__fake_parent_map: Dict[bpy.types.Object, List[bpy.types.Object]] = {}
self.__empty_parent_map = {} 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(): for i in self.rigidBodies():
self.__backupTransforms(i) self.__backupTransforms(i)
# mute relation # mute relation
@@ -932,7 +993,7 @@ class Model:
# update changes of armature constraints # update changes of armature constraints
bpy.context.scene.frame_set(bpy.context.scene.frame_current) bpy.context.scene.frame_set(bpy.context.scene.frame_current)
parented = [] parented: List[bpy.types.Object] = []
for i in self.joints(): for i in self.joints():
self.__backupTransforms(i) self.__backupTransforms(i)
rbc = i.rigid_body_constraint rbc = i.rigid_body_constraint
@@ -950,7 +1011,8 @@ class Model:
# assert(len(no_parents) == len(parented)) # assert(len(no_parents) == len(parented))
def __postBuild(self): def __postBuild(self) -> None:
logger.debug("Post-build finalization")
self.__fake_parent_map = None self.__fake_parent_map = None
self.__rigid_body_matrix_map = None self.__rigid_body_matrix_map = None
@@ -962,6 +1024,7 @@ class Model:
matrix_world = empty.matrix_world matrix_world = empty.matrix_world
empty.parent = rigid_obj empty.parent = rigid_obj
empty.matrix_world = matrix_world empty.matrix_world = matrix_world
logger.debug(f"Parented empty {empty.name} to rigid object {rigid_obj.name}")
self.__empty_parent_map = None self.__empty_parent_map = None
arm = self.armature() arm = self.armature()
@@ -970,11 +1033,13 @@ class Model:
c = p_bone.constraints.get("mmd_tools_rigid_track", None) c = p_bone.constraints.get("mmd_tools_rigid_track", None)
if c: if c:
c.mute = False 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" assert rigid_obj.mmd_type == "RIGID_BODY"
rb = rigid_obj.rigid_body rb = rigid_obj.rigid_body
if rb is None: if rb is None:
logger.warning(f"No rigid body for {rigid_obj.name}")
return return
rigid = rigid_obj.mmd_rigid rigid = rigid_obj.mmd_rigid
@@ -1018,7 +1083,7 @@ class Model:
fake_children = self.__fake_parent_map.get(rigid_obj, None) fake_children = self.__fake_parent_map.get(rigid_obj, None)
if fake_children: if fake_children:
for fake_child in 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() t, r, s = (m @ fake_child.matrix_local).decompose()
fake_child.location = t fake_child.location = t
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode) 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) fake_children = self.__fake_parent_map.get(rigid_obj, None)
if fake_children: if fake_children:
for fake_child in 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() t, r, s = (m @ fake_child.matrix_local).decompose()
fake_child.location = t fake_child.location = t
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode) 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_rigid_obj = self.__empty_parent_map[empty]
ori_rb = ori_rigid_obj.rigid_body ori_rb = ori_rigid_obj.rigid_body
if ori_rb and rb.mass > ori_rb.mass: 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 # re-parenting
rigid_obj.mmd_rigid.bone = bone_name rigid_obj.mmd_rigid.bone = bone_name
rigid_obj.constraints.remove(relation) rigid_obj.constraints.remove(relation)
@@ -1070,21 +1135,22 @@ class Model:
# revert change # revert change
ori_rigid_obj.mmd_rigid.bone = bone_name ori_rigid_obj.mmd_rigid.bone = bone_name
else: 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 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 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) total_len = len(nonCollisionJointTable)
if total_len < 1: if total_len < 1:
return return
start_time = time.time() start_time = time.time()
logging.debug("-" * 60) logger.debug("-" * 60)
logging.debug(" creating ncc, counts: %d", total_len) logger.debug(" creating ncc, counts: %d", total_len)
ncc_obj = bpyutils.createObject(name="ncc", object_data=None) ncc_obj = bpyutils.createObject(name="ncc", object_data=None)
ncc_obj.location = [0, 0, 0] ncc_obj.location = [0, 0, 0]
@@ -1099,26 +1165,26 @@ class Model:
rb.disable_collisions = True rb.disable_collisions = True
ncc_objs = bpyutils.duplicateObject(ncc_obj, total_len) 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): for ncc_obj, pair in zip(ncc_objs, nonCollisionJointTable):
rbc = ncc_obj.rigid_body_constraint rbc = ncc_obj.rigid_body_constraint
rbc.object1, rbc.object2 = pair rbc.object1, rbc.object2 = pair
ncc_obj.hide_set(True) ncc_obj.hide_set(True)
ncc_obj.hide_select = True ncc_obj.hide_select = True
logging.debug(" finish in %f seconds.", time.time() - start_time) logger.debug(" finish in %f seconds.", time.time() - start_time)
logging.debug("-" * 60) logger.debug("-" * 60)
def buildRigids(self, non_collision_distance_scale, collision_margin): def buildRigids(self, non_collision_distance_scale: float, collision_margin: float) -> List[bpy.types.Object]:
logging.debug("--------------------------------") logger.debug("--------------------------------")
logging.debug(" Build riggings of rigid bodies") logger.debug(" Build riggings of rigid bodies")
logging.debug("--------------------------------") logger.debug("--------------------------------")
rigid_objects = list(self.rigidBodies()) 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: for i in rigid_objects:
rigid_object_groups[i.mmd_rigid.collision_group_number].append(i) rigid_object_groups[i.mmd_rigid.collision_group_number].append(i)
jointMap = {} jointMap: Dict[frozenset, bpy.types.Object] = {}
for joint in self.joints(): for joint in self.joints():
rbc = joint.rigid_body_constraint rbc = joint.rigid_body_constraint
if rbc is None: if rbc is None:
@@ -1126,10 +1192,10 @@ class Model:
rbc.disable_collisions = False rbc.disable_collisions = False
jointMap[frozenset((rbc.object1, rbc.object2))] = joint jointMap[frozenset((rbc.object1, rbc.object2))] = joint
logging.info("Creating non collision constraints") logger.info("Creating non collision constraints")
# create non collision constraints # create non collision constraints
nonCollisionJointTable = [] nonCollisionJointTable: List[Tuple[bpy.types.Object, bpy.types.Object]] = []
non_collision_pairs = set() non_collision_pairs: Set[frozenset] = set()
rigid_object_cnt = len(rigid_objects) rigid_object_cnt = len(rigid_objects)
for obj_a in rigid_objects: for obj_a in rigid_objects:
for n, ignore in enumerate(obj_a.mmd_rigid.collision_group_mask): for n, ignore in enumerate(obj_a.mmd_rigid.collision_group_mask):
@@ -1150,12 +1216,13 @@ class Model:
nonCollisionJointTable.append((obj_a, obj_b)) nonCollisionJointTable.append((obj_a, obj_b))
non_collision_pairs.add(pair) non_collision_pairs.add(pair)
for cnt, i in enumerate(rigid_objects): 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.updateRigid(i, collision_margin)
self.__createNonCollisionConstraint(nonCollisionJointTable) self.__createNonCollisionConstraint(nonCollisionJointTable)
return rigid_objects return rigid_objects
def buildJoints(self): def buildJoints(self) -> None:
logger.info("Building joints")
for i in self.joints(): for i in self.joints():
rbc = i.rigid_body_constraint rbc = i.rigid_body_constraint
if rbc is None: if rbc is None:
@@ -1168,8 +1235,9 @@ class Model:
t, r, s = (m @ i.matrix_local).decompose() t, r, s = (m @ i.matrix_local).decompose()
i.location = t i.location = t
i.rotation_euler = r.to_euler(i.rotation_mode) 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_object = self.armature()
armature: bpy.types.Armature armature: bpy.types.Armature
@@ -1177,7 +1245,7 @@ class Model:
edit_bones = armature.edit_bones edit_bones = armature.edit_bones
rigid_body_object: bpy.types.Object rigid_body_object: bpy.types.Object
for rigid_body_object in self.rigidBodies(): 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: if mmd_rigid.type not in target_modes:
continue continue
@@ -1188,21 +1256,25 @@ class Model:
editor(edit_bone) editor(edit_bone)
def disconnectPhysicsBones(self): def disconnectPhysicsBones(self) -> None:
def editor(edit_bone: bpy.types.EditBone): 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) rna_prop_ui.rna_idprop_ui_create(edit_bone, "mmd_bone_use_connect", default=edit_bone.use_connect)
edit_bone.use_connect = False edit_bone.use_connect = False
logger.debug(f"Disconnected bone: {edit_bone.name}")
self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)}) self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)})
def connectPhysicsBones(self): def connectPhysicsBones(self) -> None:
def editor(edit_bone: bpy.types.EditBone): 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") mmd_bone_use_connect_str: Optional[str] = edit_bone.get("mmd_bone_use_connect")
if mmd_bone_use_connect_str is None: if mmd_bone_use_connect_str is None:
return return
if not edit_bone.use_connect: # wasn't it overwritten? if not edit_bone.use_connect: # wasn't it overwritten?
edit_bone.use_connect = bool(mmd_bone_use_connect_str) 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"] del edit_bone["mmd_bone_use_connect"]
self.__editPhysicsBones(editor, {str(MODE_STATIC), str(MODE_DYNAMIC), str(MODE_DYNAMIC_BONE)}) 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. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
import re 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 bpy
import numpy as np
from bpy.types import Object, ShapeKey, Material, Mesh, Armature, PoseBone, Constraint
from .. import bpyutils, utils from .. import bpyutils, utils
from ..bpyutils import FnContext, FnObject, TransformConstraintOp from ..bpyutils import FnContext, FnObject, TransformConstraintOp
from ....core.logging_setup import logger
if TYPE_CHECKING: if TYPE_CHECKING:
from .model import Model from .model import Model
class FnMorph: class FnMorph:
def __init__(self, morph, model: "Model"): def __init__(self, morph: Any, model: "Model"):
self.__morph = morph self.__morph = morph
self.__rig = model self.__rig = model
@classmethod @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: if len(shape_key_names) < 1:
return return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj assert FnContext.get_active_object(FnContext.ensure_context()) == obj
if obj.data.shape_keys is None: if obj.data.shape_keys is None:
bpy.ops.object.shape_key_add() 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) obj.active_shape_key_index = key_blocks.find(name)
bpy.ops.object.shape_key_move(type="BOTTOM") bpy.ops.object.shape_key_move(type="BOTTOM")
@@ -43,7 +45,7 @@ class FnMorph:
__move_to_bottom(key_blocks, name) __move_to_bottom(key_blocks, name)
@classmethod @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: if len(shape_key_names) < 1:
return return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj assert FnContext.get_active_object(FnContext.ensure_context()) == obj
@@ -58,11 +60,11 @@ class FnMorph:
bpy.ops.object.shape_key_move(type="BOTTOM") bpy.ops.object.shape_key_move(type="BOTTOM")
@staticmethod @staticmethod
def get_morph_slider(rig): def get_morph_slider(rig: "Model") -> "_MorphSlider":
return _MorphSlider(rig) return _MorphSlider(rig)
@staticmethod @staticmethod
def category_guess(morph): def category_guess(morph: Any) -> None:
name_lower = morph.name.lower() name_lower = morph.name.lower()
if "mouth" in name_lower: if "mouth" in name_lower:
morph.category = "MOUTH" morph.category = "MOUTH"
@@ -73,7 +75,7 @@ class FnMorph:
morph.category = "EYE" morph.category = "EYE"
@classmethod @classmethod
def load_morphs(cls, rig): def load_morphs(cls, rig: "Model") -> None:
mmd_root = rig.rootObject().mmd_root mmd_root = rig.rootObject().mmd_root
vertex_morphs = mmd_root.vertex_morphs vertex_morphs = mmd_root.vertex_morphs
uv_morphs = mmd_root.uv_morphs uv_morphs = mmd_root.uv_morphs
@@ -92,7 +94,7 @@ class FnMorph:
cls.category_guess(item) cls.category_guess(item)
@staticmethod @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) assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys 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]) FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name])
@staticmethod @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) assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys shape_keys = mesh_object.data.shape_keys
@@ -126,13 +128,13 @@ class FnMorph:
mesh_object.active_shape_key_index = key_blocks.find(dest_name) mesh_object.active_shape_key_index = key_blocks.find(dest_name)
@staticmethod @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") pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW")
# yield (vertex_group, morph_name, axis),... # 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)) return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name))
@staticmethod @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): for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
obj.vertex_groups.remove(vg) obj.vertex_groups.remove(vg)
@@ -143,12 +145,12 @@ class FnMorph:
obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name) obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name)
@staticmethod @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 armature = armature_object.id_data
# Use animation_data and action instead of action_pose # Use animation_data and action instead of action_pose
if armature.animation_data is None or armature.animation_data.action is None: 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 return
action = armature.animation_data.action action = armature.animation_data.action
@@ -187,9 +189,9 @@ class FnMorph:
utils.selectAObject(root) utils.selectAObject(root)
@staticmethod @staticmethod
def clean_uv_morph_vertex_groups(obj): def clean_uv_morph_vertex_groups(obj: Object) -> None:
# remove empty vertex groups of uv morphs # 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 vertex_groups = obj.vertex_groups
for v in obj.data.vertices: for v in obj.data.vertices:
for x in v.groups: for x in v.groups:
@@ -203,8 +205,8 @@ class FnMorph:
vertex_groups.remove(vg) vertex_groups.remove(vg)
@staticmethod @staticmethod
def get_uv_morph_offset_map(obj, morph): def get_uv_morph_offset_map(obj: Object, morph: Any) -> Dict[int, List[float]]:
offset_map = {} # offset_map[vertex_index] = offset_xyzw offset_map: Dict[int, List[float]] = {} # offset_map[vertex_index] = offset_xyzw
if morph.data_type == "VERTEX_GROUP": if morph.data_type == "VERTEX_GROUP":
scale = morph.vertex_group_scale scale = morph.vertex_group_scale
axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)} 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 return offset_map
@staticmethod @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 vertex_groups = obj.vertex_groups
morph_name = getattr(morph, "name", None) morph_name = getattr(morph, "name", None)
if offset_axes: if offset_axes:
@@ -250,7 +252,7 @@ class FnMorph:
vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name) vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name)
vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE") 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: for offset in self.__morph.data:
# Use the new_mesh if provided # Use the new_mesh if provided
meshObj = new_mesh meshObj = new_mesh
@@ -270,11 +272,11 @@ class FnMorph:
offset.related_mesh = meshObj.data.name offset.related_mesh = meshObj.data.name
@staticmethod @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[]""" """Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]"""
mmd_root = mmd_root_object.mmd_root mmd_root = mmd_root_object.mmd_root
def morph_data_equals(l, r) -> bool: def morph_data_equals(l: Any, r: Any) -> bool:
return ( return (
l.related_mesh_data == r.related_mesh_data l.related_mesh_data == r.related_mesh_data
and l.offset_type == r.offset_type 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)) 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)) 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[] # Remove duplicated mmd_root.material_morphs.data[]
@@ -325,7 +327,7 @@ class _MorphSlider:
def __init__(self, model: "Model"): def __init__(self, model: "Model"):
self.__rig = 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 rig = self.__rig
root = rig.rootObject() root = rig.rootObject()
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None) 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 return obj
@property @property
def dummy_armature(self): def dummy_armature(self) -> Optional[Object]:
obj = self.placeholder() obj = self.placeholder()
return self.__dummy_armature(obj) if obj else None 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) arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None)
if create and arm is None: if create and arm is None:
arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature")) 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) FnBone.setup_special_bone_collections(arm)
return arm return arm
def get(self, morph_name): def get(self, morph_name: str) -> Optional[ShapeKey]:
obj = self.placeholder() obj = self.placeholder()
if obj is None: if obj is None:
return None return None
@@ -369,13 +371,13 @@ class _MorphSlider:
return None return None
return key_blocks.get(morph_name, None) return key_blocks.get(morph_name, None)
def create(self): def create(self) -> Object:
self.__rig.loadMorphs() self.__rig.loadMorphs()
obj = self.placeholder(create=True) obj = self.placeholder(create=True)
self.__load(obj, self.__rig.rootObject().mmd_root) self.__load(obj, self.__rig.rootObject().mmd_root)
return obj return obj
def __load(self, obj, mmd_root): def __load(self, obj: Object, mmd_root: Any) -> None:
attr_list = ("group", "vertex", "bone", "uv", "material") attr_list = ("group", "vertex", "bone", "uv", "material")
morph_sliders = obj.data.shape_keys.key_blocks 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", ())): 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) obj.shape_key_add(name=name, from_mix=False)
@staticmethod @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) d = id_data.driver_add(path, index)
variables = d.driver.variables variables = d.driver.variables
for x in variables: for x in variables:
@@ -394,7 +396,7 @@ class _MorphSlider:
return d.driver, variables return d.driver, variables
@staticmethod @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 = variables.new()
var.name = f"{prefix}{len(variables)}" var.name = f"{prefix}{len(variables)}"
var.type = "SINGLE_PROP" var.type = "SINGLE_PROP"
@@ -405,7 +407,7 @@ class _MorphSlider:
return var return var
@staticmethod @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: if resolve_path:
try: try:
key_block.id_data.path_resolve(key_block.path_from_id()) 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) 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))) 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 from math import ceil, floor
names_in_use = names_in_use or {} names_in_use = names_in_use or {}
@@ -427,7 +429,7 @@ class _MorphSlider:
morph_sliders = self.placeholder() morph_sliders = self.placeholder()
morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {} morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {}
for mesh_object in rig.meshes(): 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: if kb.name in names_in_use:
continue continue
@@ -465,7 +467,7 @@ class _MorphSlider:
c.driver_remove(attr) c.driver_remove(attr)
b.constraints.remove(c) b.constraints.remove(c)
def unbind(self): def unbind(self) -> None:
mmd_root = self.__rig.rootObject().mmd_root mmd_root = self.__rig.rootObject().mmd_root
# after unbind, the weird lag problem will disappear. # after unbind, the weird lag problem will disappear.
@@ -488,7 +490,7 @@ class _MorphSlider:
b.driver_remove("rotation_quaternion") b.driver_remove("rotation_quaternion")
self.__cleanup() self.__cleanup()
def bind(self): def bind(self) -> None:
rig = self.__rig rig = self.__rig
root = rig.rootObject() root = rig.rootObject()
armObj = rig.armature() armObj = rig.armature()
@@ -502,10 +504,10 @@ class _MorphSlider:
morph_sliders = obj.data.shape_keys.key_blocks morph_sliders = obj.data.shape_keys.key_blocks
# data gathering # data gathering
group_map = {} group_map: Dict[Tuple[str, str], List[List[Any]]] = {}
shape_key_map = {} shape_key_map: Dict[str, List[Tuple[ShapeKey, str, List[Any]]]] = {}
uv_morph_map = {} uv_morph_map: Dict[str, List[Tuple[str, str, str, List[Any]]]] = {}
for mesh_object in rig.meshes(): for mesh_object in rig.meshes():
mesh_object.show_only_shape_key = False mesh_object.show_only_shape_key = False
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ()) key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
@@ -526,7 +528,7 @@ class _MorphSlider:
kb_bind.slider_max = 10 kb_bind.slider_max = 10
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"') 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)) shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups) group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
@@ -542,7 +544,7 @@ class _MorphSlider:
continue continue
name_bind = "mmd_bind%s" % hash(vg.name) 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 = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP")
mod.show_expanded = False mod.show_expanded = False
mod.vertex_group = vg.name mod.vertex_group = vg.name
@@ -555,13 +557,13 @@ class _MorphSlider:
else: else:
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base" 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: with bpyutils.edit_object(arm) as data:
from .bone import FnBone from .bone import FnBone
edit_bones = data.edit_bones 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 = edit_bones.get(name, None) or edit_bones.new(name=name)
b.head = (0, 0, 0) b.head = (0, 0, 0)
b.tail = (0, 0, 1) b.tail = (0, 0, 1)
@@ -578,7 +580,7 @@ class _MorphSlider:
continue continue
d.name = name_bind = f"mmd_bind{hash(d)}" d.name = name_bind = f"mmd_bind{hash(d)}"
b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None)) 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) bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
group_map.setdefault(("bone_morphs", m.name), []).append(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' scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale'
name_bind = f"mmd_bind{hash(m.name)}" name_bind = f"mmd_bind{hash(m.name)}"
b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base)) 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)) uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups))
group_map.setdefault(("uv_morphs", m.name), []).append(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) used_bone_names.add(ctrl_base.name)
for b in edit_bones: # cleanup for b in edit_bones: # cleanup
if b.name.startswith("mmd_bind") and b.name not in used_bone_names: if b.name.startswith("mmd_bind") and b.name not in used_bone_names:
edit_bones.remove(b) edit_bones.remove(b)
material_offset_map = {} material_offset_map: Dict[str, Any] = {}
for m in mmd_root.material_morphs: for m in mmd_root.material_morphs:
morph_name = m.name.replace('"', '\\"') morph_name = m.name.replace('"', '\\"')
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value' data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
groups = [] groups: List[Any] = []
group_map.setdefault(("material_morphs", m.name), []).append(groups) group_map.setdefault(("material_morphs", m.name), []).append(groups)
material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups) material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups)
for d in m.data: for d in m.data:
@@ -614,7 +616,7 @@ class _MorphSlider:
for m in mmd_root.group_morphs: for m in mmd_root.group_morphs:
if len(m.data) != len(set(m.data.keys())): 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_name = m.name.replace('"', '\\"')
morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value' morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
for d in m.data: 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()) 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: for g_name, morph_path, factor_path in groups:
var = self.__add_single_prop(variables, obj, morph_path, "g") var = self.__add_single_prop(variables, obj, morph_path, "g")
fvar = self.__add_single_prop(variables, root, factor_path, "w") fvar = self.__add_single_prop(variables, root, factor_path, "w")
@@ -644,7 +646,7 @@ class _MorphSlider:
kb_bind.mute = False kb_bind.mute = False
# bone morphs # 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_name = f"mmd_bind{hash(data)}.{map_type[:3]}"
c = TransformConstraintOp.create(constraints, c_name, map_type) c = TransformConstraintOp.create(constraints, c_name, map_type)
TransformConstraintOp.update_min_max(c, val, None) TransformConstraintOp.update_min_max(c, val, None)
@@ -692,7 +694,7 @@ class _MorphSlider:
group_dict = material_offset_map.get("group_dict", {}) 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)) nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list))
for (morph_name, data, name_bind), node in zip(morph_list, nodes): for (morph_name, data, name_bind), node in zip(morph_list, nodes):
node.label, node.name = morph_name, name_bind 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_")): 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("#", ([], [])) mul_all, add_all = material_offset_map.get("#", ([], []))
if mat.name == "": 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 = [], [] mul_list, add_list = [], []
else: else:
mat_name = "#" + mat.name mat_name = "#" + mat.name
@@ -720,7 +722,7 @@ class _MorphSlider:
class MigrationFnMorph: class MigrationFnMorph:
@staticmethod @staticmethod
def update_mmd_morph(): def update_mmd_morph() -> None:
from .material import FnMaterial from .material import FnMaterial
for root in bpy.data.objects: for root in bpy.data.objects:
@@ -762,11 +764,11 @@ class MigrationFnMorph:
morph_data.related_mesh_data = bpy.data.meshes[related_mesh] morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
@staticmethod @staticmethod
def ensure_material_id_not_conflict(): def ensure_material_id_not_conflict() -> None:
mat_ids_set = set() mat_ids_set: Set[int] = set()
# The reference library properties cannot be modified and bypassed in advance. # 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: for mat in bpy.data.materials:
if mat.mmd_material.material_id < 0: if mat.mmd_material.material_id < 0:
continue continue
@@ -781,7 +783,7 @@ class MigrationFnMorph:
mat_ids_set.add(mat.mmd_material.material_id) mat_ids_set.add(mat.mmd_material.material_id)
@staticmethod @staticmethod
def compatible_with_old_version_mmd_tools(): def compatible_with_old_version_mmd_tools() -> None:
MigrationFnMorph.ensure_material_id_not_conflict() MigrationFnMorph.ensure_material_id_not_conflict()
for root in bpy.data.objects: 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. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import List, Optional from typing import List, Optional, Tuple, Union, Dict, Any, Set, cast
import bpy import bpy
from mathutils import Euler, Vector from mathutils import Euler, Vector, Matrix
from ..bpyutils import FnContext, Props from ..bpyutils import FnContext, Props
from ....core.logging_setup import logger
SHAPE_SPHERE = 0 SHAPE_SPHERE = 0
SHAPE_BOX = 1 SHAPE_BOX = 1
@@ -21,25 +22,30 @@ MODE_DYNAMIC = 1
MODE_DYNAMIC_BONE = 2 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) 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] 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(): if bpy.ops.rigidbody.world_add.poll():
logger.debug("Creating rigid body world")
bpy.ops.rigidbody.world_add() bpy.ops.rigidbody.world_add()
rigidbody_world = bpy.context.scene.rigidbody_world rigidbody_world = bpy.context.scene.rigidbody_world
enabled = rigidbody_world.enabled enabled = rigidbody_world.enabled
rigidbody_world.enabled = enable rigidbody_world.enabled = enable
logger.debug(f"Rigid body world enabled: {enable} (was: {enabled})")
return enabled return enabled
class RigidBodyMaterial: class RigidBodyMaterial:
COLORS = [ COLORS: List[int] = [
0x7FDDD4, 0x7FDDD4,
0xF0E68C, 0xF0E68C,
0xEE82EE, 0xEE82EE,
@@ -59,10 +65,12 @@ class RigidBodyMaterial:
] ]
@classmethod @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) 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: if material_name not in bpy.data.materials:
logger.debug(f"Creating rigid body material: {material_name}")
mat = bpy.data.materials.new(material_name) mat = bpy.data.materials.new(material_name)
color = cls.COLORS[number] color = cls.COLORS[number]
mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)] 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: class FnRigidBody:
@staticmethod @staticmethod
def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]: 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: if count < 1:
return [] return []
logger.debug(f"Creating {count} rigid body objects parented to {parent_object.name}")
obj = FnRigidBody.new_rigid_body_object(context, parent_object) obj = FnRigidBody.new_rigid_body_object(context, parent_object)
if count == 1: if count == 1:
@@ -101,6 +111,8 @@ class FnRigidBody:
@staticmethod @staticmethod
def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object: 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 = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody"))
obj.parent = parent_object obj.parent = parent_object
obj.mmd_type = "RIGID_BODY" obj.mmd_type = "RIGID_BODY"
@@ -118,11 +130,11 @@ class FnRigidBody:
@staticmethod @staticmethod
def setup_rigid_body_object( def setup_rigid_body_object(
obj: bpy.types.Object, obj: bpy.types.Object,
shape_type: str, shape_type: int,
location: Vector, location: Vector,
rotation: Euler, rotation: Euler,
size: Vector, size: Vector,
dynamics_type: str, dynamics_type: int,
collision_group_number: Optional[int] = None, collision_group_number: Optional[int] = None,
collision_group_mask: Optional[List[bool]] = None, collision_group_mask: Optional[List[bool]] = None,
name: Optional[str] = None, name: Optional[str] = None,
@@ -134,6 +146,8 @@ class FnRigidBody:
linear_damping: Optional[float] = None, linear_damping: Optional[float] = None,
bounce: Optional[float] = None, bounce: Optional[float] = None,
) -> bpy.types.Object: ) -> 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.location = location
obj.rotation_euler = rotation obj.rotation_euler = rotation
@@ -175,7 +189,8 @@ class FnRigidBody:
return obj return obj
@staticmethod @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" assert obj.mmd_type == "RIGID_BODY"
x0, y0, z0 = obj.bound_box[0] x0, y0, z0 = obj.bound_box[0]
@@ -195,10 +210,14 @@ class FnRigidBody:
height = abs((z1 - z0) - diameter) height = abs((z1 - z0) - diameter)
return (radius, height, 0.0) return (radius, height, 0.0)
else: else:
raise ValueError(f"Invalid shape type: {shape}") error_msg = f"Invalid shape type: {shape}"
logger.error(error_msg)
raise ValueError(error_msg)
@staticmethod @staticmethod
def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object: 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 = FnContext.new_and_link_object(context, name="Joint", object_data=None)
obj.parent = parent_object obj.parent = parent_object
obj.mmd_type = "JOINT" obj.mmd_type = "JOINT"
@@ -230,9 +249,11 @@ class FnRigidBody:
@staticmethod @staticmethod
def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]: 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: if count < 1:
return [] return []
logger.debug(f"Creating {count} joint objects parented to {parent_object.name}")
obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size) obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size)
if count == 1: if count == 1:
@@ -256,6 +277,8 @@ class FnRigidBody:
name: str, name: str,
name_e: Optional[str] = None, name_e: Optional[str] = None,
) -> bpy.types.Object: ) -> 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.name = f"J.{name}"
obj.location = location obj.location = location
+55 -29
View File
@@ -7,14 +7,19 @@
import logging import logging
import time import time
from typing import Dict, List, Tuple, Set, Optional, Any, Union, cast, TypeVar, Callable
import bpy 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 ..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)): if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
return hash(type(v).__name__ + v.name) return hash(type(v).__name__ + v.name)
elif isinstance(v, bpy.types.Pose): elif isinstance(v, bpy.types.Pose):
@@ -24,23 +29,24 @@ def _hash(v):
class FnSDEF: class FnSDEF:
g_verts = {} # global cache 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 = {} g_shapekey_data: Dict[int, Optional[np.ndarray]] = {}
g_bone_check = {} g_bone_check: Dict[int, Dict[Union[Tuple[int, int], str], Union[Tuple[Matrix, Matrix], bool]]] = {}
__g_armature_check = {} __g_armature_check: Dict[int, Optional[int]] = {}
SHAPEKEY_NAME = "mmd_sdef_skinning" SHAPEKEY_NAME: str = "mmd_sdef_skinning"
MASK_NAME = "mmd_sdef_mask" MASK_NAME: str = "mmd_sdef_mask"
def __init__(self): def __init__(self) -> None:
raise NotImplementedError("not allowed") raise NotImplementedError("not allowed")
@classmethod @classmethod
def __init_cache(cls, obj, shapekey): def __init_cache(cls, obj: Object, shapekey: ShapeKey) -> bool:
key = _hash(obj) key = _hash(obj)
obj = getattr(obj, "original", obj) obj = getattr(obj, "original", obj)
mod = obj.modifiers.get("mmd_bone_order_override") 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 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: 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_verts[key] = cls.__find_vertices(obj)
cls.g_bone_check[key] = {} cls.g_bone_check[key] = {}
cls.__g_armature_check[key] = key_armature cls.__g_armature_check[key] = key_armature
@@ -49,7 +55,7 @@ class FnSDEF:
return False return False
@classmethod @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)] check = cls.g_bone_check[_hash(obj)]
key = (_hash(bone0), _hash(bone1)) key = (_hash(bone0), _hash(bone1))
if key not in check or (bone0.matrix, bone1.matrix) != check[key]: if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
@@ -58,17 +64,18 @@ class FnSDEF:
return False return False
@classmethod @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", ()) key_blocks = getattr(obj.data.shape_keys, "key_blocks", ())
if cls.SHAPEKEY_NAME in key_blocks: if cls.SHAPEKEY_NAME in key_blocks:
shapekey = key_blocks[cls.SHAPEKEY_NAME] shapekey = key_blocks[cls.SHAPEKEY_NAME]
shapekey.mute = mute shapekey.mute = mute
if cls.has_sdef_data(obj): if cls.has_sdef_data(obj):
logger.debug(f"Setting SDEF mute state to {mute} for {obj.name}")
cls.__init_cache(obj, shapekey) cls.__init_cache(obj, shapekey)
cls.__sdef_muted(obj, shapekey) cls.__sdef_muted(obj, shapekey)
@classmethod @classmethod
def __sdef_muted(cls, obj, shapekey): def __sdef_muted(cls, obj: Object, shapekey: ShapeKey) -> bool:
mute = shapekey.mute mute = shapekey.mute
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"): if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
mod = obj.modifiers.get("mmd_bone_order_override") mod = obj.modifiers.get("mmd_bone_order_override")
@@ -80,10 +87,11 @@ class FnSDEF:
mod.invert_vertex_group = True mod.invert_vertex_group = True
shapekey.vertex_group = cls.MASK_NAME shapekey.vertex_group = cls.MASK_NAME
cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute
logger.debug(f"SDEF mute state updated to {mute} for {obj.name}")
return mute return mute
@staticmethod @staticmethod
def has_sdef_data(obj): def has_sdef_data(obj: Object) -> bool:
mod = obj.modifiers.get("mmd_bone_order_override") mod = obj.modifiers.get("mmd_bone_order_override")
if mod and mod.type == "ARMATURE" and mod.object: if mod and mod.type == "ARMATURE" and mod.object:
kb = getattr(obj.data.shape_keys, "key_blocks", None) kb = getattr(obj.data.shape_keys, "key_blocks", None)
@@ -91,18 +99,21 @@ class FnSDEF:
return False return False
@classmethod @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): if not cls.has_sdef_data(obj):
return {} 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 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_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data
sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].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 sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data
vd = obj.data.vertices vd = obj.data.vertices
logger.debug(f"Finding SDEF vertices for {obj.name}")
vertex_count = 0
for i in range(len(sdef_c)): for i in range(len(sdef_c)):
if vd[i].co != sdef_c[i].co: 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 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] = (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][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2))
vertices[key][3].append(i) vertices[key][3].append(i)
vertex_count += 1
logger.debug(f"Found {vertex_count} SDEF vertices in {obj.name}")
return vertices return vertices
@classmethod @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] obj = bpy.data.objects[obj_name]
shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME] shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]
return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale) return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale)
@classmethod @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] obj = bpy.data.objects[obj_name]
if getattr(shapekey.id_data, "is_evaluated", False): 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 # 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 rot1 = -rot1
s0, s1 = mat0.to_scale(), mat1.to_scale() 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 s = s0 * w0 + s1 * w1
return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])]) 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 = '' 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 return (mat_rot @ (pos_c + delta)) - delta
@@ -233,16 +247,19 @@ class FnSDEF:
return 1.0 # shapkey value return 1.0 # shapkey value
@classmethod @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: 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 bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function
if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace: 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 bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap
BENCH_LOOP = 10 BENCH_LOOP: int = 10
@classmethod @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 # 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=True, use_skip=False, use_scale=use_scale)
cls.driver_function(shapkey, obj.name, bulk_update=False, 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) cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
bulk_time = time.time() - t bulk_time = time.time() - t
result = default_time > bulk_time 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 return result
@classmethod @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 # Unbind first
cls.unbind(obj) cls.unbind(obj)
if not cls.has_sdef_data(obj): if not cls.has_sdef_data(obj):
logger.debug(f"Object {obj.name} does not have SDEF data")
return False return False
# Create the shapekey for the driver # Create the shapekey for the driver
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False) shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
@@ -294,32 +312,38 @@ class FnSDEF:
f.driver.use_self = True f.driver.use_self = True
param = (bulk_update, use_skip, use_scale) param = (bulk_update, use_skip, use_scale)
f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param) 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 return True
@classmethod @classmethod
def unbind(cls, obj): def unbind(cls, obj: Object) -> None:
if obj.data.shape_keys: if obj.data.shape_keys:
if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks: 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]) FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME])
for mod in obj.modifiers: for mod in obj.modifiers:
if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME: 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.vertex_group = ""
mod.invert_vertex_group = False mod.invert_vertex_group = False
break break
if cls.MASK_NAME in obj.vertex_groups: 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]) obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME])
cls.clear_cache(obj) cls.clear_cache(obj)
@classmethod @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: if unused_only:
valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj) 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] del cls.g_verts[key]
for key in cls.g_shapekey_data.keys() - cls.g_verts.keys(): for key in cls.g_shapekey_data.keys() - cls.g_verts.keys():
del cls.g_shapekey_data[key] del cls.g_shapekey_data[key]
for key in cls.g_bone_check.keys() - cls.g_verts.keys(): for key in cls.g_bone_check.keys() - cls.g_verts.keys():
del cls.g_bone_check[key] del cls.g_bone_check[key]
logger.debug(f"Cleared {len(removed_keys)} unused SDEF cache entries")
elif obj: elif obj:
key = _hash(obj) key = _hash(obj)
if key in cls.g_verts: if key in cls.g_verts:
@@ -328,7 +352,9 @@ class FnSDEF:
del cls.g_shapekey_data[key] del cls.g_shapekey_data[key]
if key in cls.g_bone_check: if key in cls.g_bone_check:
del cls.g_bone_check[key] del cls.g_bone_check[key]
logger.debug(f"Cleared SDEF cache for {obj.name}")
else: else:
logger.debug("Cleared all SDEF cache")
cls.g_verts = {} cls.g_verts = {}
cls.g_bone_check = {} cls.g_bone_check = {}
cls.g_shapekey_data = {} 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. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, Tuple, cast from typing import Optional, Tuple, cast, List, Dict, Any, Union
import bpy import bpy
from bpy.types import (
ShaderNodeTree,
ShaderNode,
NodeGroupInput,
NodeGroupOutput,
Material
)
from ....core.logging_setup import logger
class _NodeTreeUtils: class _NodeTreeUtils:
def __init__(self, shader: bpy.types.ShaderNodeTree): def __init__(self, shader: ShaderNodeTree):
self.shader = shader 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 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) 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: def new_node(self, idname: str, pos: Tuple[int, int]) -> ShaderNode:
node: bpy.types.ShaderNode = self.nodes.new(idname) node: ShaderNode = self.nodes.new(idname)
node.location = (pos[0] * 210, pos[1] * 220) node.location = (pos[0] * 210, pos[1] * 220)
return node 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 = self.new_node("ShaderNodeMath", pos)
node.operation = operation node.operation = operation
if value1 is not None: if value1 is not None:
@@ -32,7 +40,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = value2 node.inputs[1].default_value = value2
return node 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 = self.new_node("ShaderNodeVectorMath", pos)
node.operation = operation node.operation = operation
if vector1 is not None: if vector1 is not None:
@@ -41,7 +49,7 @@ class _NodeTreeUtils:
node.inputs[1].default_value = vector2 node.inputs[1].default_value = vector2
return node 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 = self.new_node("ShaderNodeMixRGB", pos)
node.blend_type = blend_type node.blend_type = blend_type
if fac is not None: if fac is not None:
@@ -53,30 +61,30 @@ class _NodeTreeUtils:
return node 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): class _NodeGroupUtils(_NodeTreeUtils):
def __init__(self, shader: bpy.types.ShaderNodeTree): def __init__(self, shader: ShaderNodeTree):
super().__init__(shader) super().__init__(shader)
self.__node_input: Optional[bpy.types.NodeGroupInput] = None self.__node_input: Optional[NodeGroupInput] = None
self.__node_output: Optional[bpy.types.NodeGroupOutput] = None self.__node_output: Optional[NodeGroupOutput] = None
@property @property
def node_input(self) -> bpy.types.NodeGroupInput: def node_input(self) -> NodeGroupInput:
if not self.__node_input: 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 return self.__node_input
@property @property
def node_output(self) -> bpy.types.NodeGroupOutput: def node_output(self) -> NodeGroupOutput:
if not self.__node_output: 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 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} skip_nodes = {self.__node_input, self.__node_output}
for n in (x for x in self.nodes if x not in skip_nodes): for n in (x for x in self.nodes if x not in skip_nodes):
n.hide = True n.hide = True
@@ -87,15 +95,15 @@ class _NodeGroupUtils(_NodeTreeUtils):
for s in n.outputs: for s in n.outputs:
s.hide = not s.is_linked 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) 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) 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: 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)) 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: if idname in SOCKET_SUBTYPE_MAPPING:
interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "") interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "")
@@ -114,14 +122,18 @@ class _NodeGroupUtils(_NodeTreeUtils):
class _MaterialMorph: class _MaterialMorph:
@classmethod @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: 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_node_inputs(material.node_tree.nodes[morph.name], morph)
cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph) cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph)
@classmethod @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, [] node, nodes = None, []
logger.debug(f"Setting up {len(morphs)} morph nodes for {material.name}")
for m in morphs: for m in morphs:
node = cls.__morph_node_add(material, m, node) node = cls.__morph_node_add(material, m, node)
nodes.append(node) nodes.append(node)
@@ -137,23 +149,25 @@ class _MaterialMorph:
return nodes return nodes
@classmethod @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) cls.__update_morph_links(node, reset=True)
@classmethod @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 nodes, links = node.id_data.nodes, node.id_data.links
if reset: if reset:
if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links): if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links):
return 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: if socket_shader and socket_morph.is_linked:
links.new(socket_morph.links[0].from_socket, socket_shader) links.new(socket_morph.links[0].from_socket, socket_shader)
else: 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:
if socket_shader.is_linked: if socket_shader.is_linked:
links.new(socket_shader.links[0].from_socket, socket_morph) 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"]) __init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
@classmethod @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["Ambient2"].default_value[:3] = morph.ambient_color[:3]
node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3] node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3]
node.inputs["Specular2"].default_value[:3] = morph.specular_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] node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
@classmethod @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 nodes, links = material.node_tree.nodes, material.node_tree.links
shader = nodes.get("mmd_shader", None) shader = nodes.get("mmd_shader", None)
@@ -221,8 +237,9 @@ class _MaterialMorph:
return node return node
# connect last node to shader # connect last node to shader
if 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: if socket_out and socket_in:
links.new(socket_out, socket_in) links.new(socket_out, socket_in)
@@ -244,12 +261,14 @@ class _MaterialMorph:
return shader return shader
@classmethod @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 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") shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes): if len(shader.nodes):
return shader return shader
logger.info(f"Creating new shader node group: {group_name}")
ng = _NodeGroupUtils(shader) ng = _NodeGroupUtils(shader)
links = ng.links links = ng.links
@@ -260,7 +279,7 @@ class _MaterialMorph:
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat") ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
ng.new_node("NodeGroupOutput", (3, 0)) 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_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac))
# MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2 # MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2
# https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400 # 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"]) ng.new_output_socket(id_name + tag, node_mix.outputs["Color"])
return node_mix 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 Color = tex_rgb * tex_a + (1 - tex_a)
# : tex_rgb = TexRGB * ColorMul + ColorAdd # : tex_rgb = TexRGB * ColorMul + ColorAdd
# : tex_a = TexA * ValueMul + ValueAdd # : 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", node_add.outputs[0], socket_type="NodeSocketColor")
ng.new_output_socket(id_name + " Tex Add", node_scale.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}1{tag}", input1, use_mul)
ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul) ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul)
ng.new_output_socket(f"{id_name}{tag}", output) 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]) __blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2])
ng.hide_nodes() ng.hide_nodes()
logger.debug(f"Shader node group {group_name} created successfully")
return ng.shader 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. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Iterable, Optional from typing import Iterable, Optional, Any, List, Tuple, Union
import bpy 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.shader import _NodeGroupUtils
from .core.material import FnMaterial from .core.material import FnMaterial
def __switchToCyclesRenderEngine(): def __switchToCyclesRenderEngine() -> None:
if bpy.context.scene.render.engine != "CYCLES": if bpy.context.scene.render.engine != "CYCLES":
logger.debug("Switching render engine to Cycles")
bpy.context.scene.render.engine = "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) _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) _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 = 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 o.is_active_output = True
return o return o
def create_MMDAlphaShader(): def create_MMDAlphaShader() -> NodeTree:
__switchToCyclesRenderEngine() __switchToCyclesRenderEngine()
if "MMDAlphaShader" in bpy.data.node_groups: if "MMDAlphaShader" in bpy.data.node_groups:
logger.debug("Using existing MMDAlphaShader node group")
return bpy.data.node_groups["MMDAlphaShader"] return bpy.data.node_groups["MMDAlphaShader"]
logger.info("Creating new MMDAlphaShader node group")
shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree") shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree")
node_input = shader.nodes.new("NodeGroupInput") node_input = shader.nodes.new("NodeGroupInput")
@@ -59,26 +64,28 @@ def create_MMDAlphaShader():
return shader return shader
def create_MMDBasicShader(): def create_MMDBasicShader() -> NodeTree:
__switchToCyclesRenderEngine() __switchToCyclesRenderEngine()
if "MMDBasicShader" in bpy.data.node_groups: if "MMDBasicShader" in bpy.data.node_groups:
logger.debug("Using existing MMDBasicShader node group")
return bpy.data.node_groups["MMDBasicShader"] 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_input: Node = shader.nodes.new("NodeGroupInput")
node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput") node_output: Node = shader.nodes.new("NodeGroupOutput")
node_output.location.x += 250 node_output.location.x += 250
node_input.location.x -= 500 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.x -= 250
dif.location.y += 150 dif.location.y += 150
glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic") glo: Node = shader.nodes.new("ShaderNodeBsdfAnisotropic")
glo.location.x -= 250 glo.location.x -= 250
glo.location.y -= 150 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[1], dif.outputs["BSDF"])
shader.links.new(mix.inputs[2], glo.outputs["BSDF"]) shader.links.new(mix.inputs[2], glo.outputs["BSDF"])
@@ -91,7 +98,7 @@ def create_MMDBasicShader():
return shader return shader
def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]: def __enum_linked_nodes(node: Node) -> Iterable[Node]:
yield node yield node
if node.parent: if node.parent:
yield 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) 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 nodes = material.node_tree.nodes
node_names = set(n.name for n in 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"}): 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]) 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() __switchToCyclesRenderEngine()
convertToBlenderShader(obj, use_principled, clean_nodes, subsurface) 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: for i in obj.material_slots:
if not i.material: if not i.material:
continue continue
if not i.material.use_nodes: if not i.material.use_nodes:
logger.debug(f"Enabling nodes for material: {i.material.name}")
i.material.use_nodes = True i.material.use_nodes = True
__convertToMMDBasicShader(i.material) __convertToMMDBasicShader(i.material)
if use_principled: if use_principled:
logger.debug(f"Converting material to Principled BSDF: {i.material.name}")
__convertToPrincipledBsdf(i.material, subsurface) __convertToPrincipledBsdf(i.material, subsurface)
if clean_nodes: if clean_nodes:
__cleanNodeTree(i.material) __cleanNodeTree(i.material)
def convertToMMDShader(obj): def convertToMMDShader(obj: bpy.types.Object) -> None:
"""BSDF -> MMDShaderDev conversion.""" """BSDF -> MMDShaderDev conversion."""
logger.info(f"Converting {obj.name} to MMD shader")
for i in obj.material_slots: for i in obj.material_slots:
if not i.material: if not i.material:
continue continue
if not i.material.use_nodes: if not i.material.use_nodes:
logger.debug(f"Enabling nodes for material: {i.material.name}")
i.material.use_nodes = True i.material.use_nodes = True
FnMaterial.convert_to_mmd_material(i.material) 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 # TODO: test me
mmd_basic_shader_grp = create_MMDBasicShader() mmd_basic_shader_grp = create_MMDBasicShader()
mmd_alpha_shader_grp = create_MMDAlphaShader() 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 # 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.node_tree = mmd_basic_shader_grp
shader.inputs[0].default_value[:3] = material.diffuse_color[:3] shader.inputs[0].default_value[:3] = material.diffuse_color[:3]
shader.inputs[1].default_value[:3] = material.specular_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] alpha_value = material.diffuse_color[3]
if alpha_value < 1.0: 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.x = shader.location.x + 250
alpha_shader.location.y = shader.location.y - 150 alpha_shader.location.y = shader.location.y - 150
alpha_shader.node_tree = mmd_alpha_shader_grp 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) material.node_tree.links.new(alpha_shader.inputs[0], outplug)
outplug = alpha_shader.outputs[0] 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.node_tree.links.new(material_output.inputs["Surface"], outplug)
material_output.location.x = shader.location.x + 500 material_output.location.x = shader.location.x + 500
material_output.location.y = shader.location.y - 150 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() 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": if s.node_tree.name == "MMDBasicShader":
l: bpy.types.NodeLink l: NodeLink
for l in s.outputs[0].links: for l in s.outputs[0].links:
to_node = l.to_node to_node = l.to_node
# assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader # 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) __switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node)
node_names.add(to_node.name) node_names.add(to_node.name)
else: else:
@@ -194,8 +210,9 @@ def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float):
nodes.remove(nodes[name]) 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): def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, subsurface: float, node_alpha: Optional[ShaderNodeGroup] = None) -> None:
shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled") logger.debug(f"Switching to Principled BSDF: {node_basic.name}")
shader: Node = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
shader.parent = node_basic.parent shader.parent = node_basic.parent
shader.location.x = node_basic.location.x shader.location.x = node_basic.location.x
shader.location.y = node_basic.location.y 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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from bpy.props import BoolProperty, StringProperty from bpy.props import BoolProperty, StringProperty, FloatProperty
from bpy.types import Operator from bpy.types import Operator, Context, Object, Material
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
from .. import cycles_converter from .. import cycles_converter
from ..core.exceptions import MaterialNotFoundError from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.shader import _NodeGroupUtils from ..core.shader import _NodeGroupUtils
from ....core.logging_setup import logger
class ConvertMaterialsForCycles(Operator): class ConvertMaterialsForCycles(Operator):
@@ -21,14 +24,14 @@ class ConvertMaterialsForCycles(Operator):
bl_description = "Convert materials of selected objects for Cycles." bl_description = "Convert materials of selected objects for Cycles."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.BoolProperty( use_principled: BoolProperty(
name="Convert to Principled BSDF", name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled", description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=False, default=False,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
clean_nodes: bpy.props.BoolProperty( clean_nodes: BoolProperty(
name="Clean Nodes", name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.", description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=False, default=False,
@@ -36,22 +39,27 @@ class ConvertMaterialsForCycles(Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) 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 = self.layout
layout.prop(self, "use_principled") layout.prop(self, "use_principled")
layout.prop(self, "clean_nodes") layout.prop(self, "clean_nodes")
def execute(self, context): def execute(self, context: Context) -> Set[str]:
try: try:
context.scene.render.engine = "CYCLES" 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.") self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
return {"CANCELLED"} 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"): 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) cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
return {"FINISHED"} return {"FINISHED"}
@@ -61,21 +69,21 @@ class ConvertMaterials(Operator):
bl_description = "Convert materials of selected objects." bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.BoolProperty( use_principled: BoolProperty(
name="Convert to Principled BSDF", name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled", description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=True, default=True,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
clean_nodes: bpy.props.BoolProperty( clean_nodes: BoolProperty(
name="Clean Nodes", name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.", description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=True, default=True,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
subsurface: bpy.props.FloatProperty( subsurface: FloatProperty(
name="Subsurface", name="Subsurface",
default=0.001, default=0.001,
soft_min=0.000, soft_min=0.000,
@@ -85,13 +93,15 @@ class ConvertMaterials(Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) 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: for obj in context.selected_objects:
if obj.type != "MESH": if obj.type != "MESH":
continue 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) cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
return {"FINISHED"} return {"FINISHED"}
@@ -102,20 +112,22 @@ class ConvertBSDFMaterials(Operator):
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == 'MESH'), None) 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: for obj in context.selected_objects:
if obj.type != 'MESH': if obj.type != 'MESH':
continue continue
logger.debug(f"Converting BSDF materials for object: {obj.name}")
cycles_converter.convertToMMDShader(obj) cycles_converter.convertToMMDShader(obj)
return {'FINISHED'} return {'FINISHED'}
class _OpenTextureBase: class _OpenTextureBase:
"""Create a texture for mmd model material.""" """Create a texture for mmd model material."""
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
filepath: StringProperty( filepath: StringProperty(
name="File Path", name="File Path",
@@ -129,7 +141,7 @@ class _OpenTextureBase:
options={"HIDDEN"}, options={"HIDDEN"},
) )
def invoke(self, context, event): def invoke(self, context: Context, event: Any) -> Set[str]:
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
@@ -139,8 +151,13 @@ class OpenTexture(Operator, _OpenTextureBase):
bl_label = "Open Texture" bl_label = "Open Texture"
bl_description = "Create main texture of active material" 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 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 = FnMaterial(mat)
fnMat.create_texture(self.filepath) fnMat.create_texture(self.filepath)
return {"FINISHED"} return {"FINISHED"}
@@ -154,8 +171,13 @@ class RemoveTexture(Operator):
bl_description = "Remove main texture of active material" bl_description = "Remove main texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material 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 = FnMaterial(mat)
fnMat.remove_texture() fnMat.remove_texture()
return {"FINISHED"} return {"FINISHED"}
@@ -168,8 +190,13 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
bl_label = "Open Sphere Texture" bl_label = "Open Sphere Texture"
bl_description = "Create sphere texture of active material" 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 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 = FnMaterial(mat)
fnMat.create_sphere_texture(self.filepath, context.active_object) fnMat.create_sphere_texture(self.filepath, context.active_object)
return {"FINISHED"} return {"FINISHED"}
@@ -183,8 +210,13 @@ class RemoveSphereTexture(Operator):
bl_description = "Remove sphere texture of active material" bl_description = "Remove sphere texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material 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 = FnMaterial(mat)
fnMat.remove_sphere_texture() fnMat.remove_sphere_texture()
return {"FINISHED"} return {"FINISHED"}
@@ -197,18 +229,21 @@ class MoveMaterialUp(Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
obj = context.active_object obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" 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 obj = context.active_object
current_idx = obj.active_material_index current_idx = obj.active_material_index
prev_index = current_idx - 1 prev_index = current_idx - 1
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
try: try:
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True) FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
except MaterialNotFoundError: except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
self.report({"ERROR"}, "Materials not found") self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"} return {"CANCELLED"}
obj.active_material_index = prev_index obj.active_material_index = prev_index
@@ -223,18 +258,21 @@ class MoveMaterialDown(Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
obj = context.active_object obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" 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 obj = context.active_object
current_idx = obj.active_material_index current_idx = obj.active_material_index
next_index = current_idx + 1 next_index = current_idx + 1
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
try: try:
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True) FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
except MaterialNotFoundError: except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
self.report({"ERROR"}, "Materials not found") self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"} return {"CANCELLED"}
obj.active_material_index = next_index obj.active_material_index = next_index
@@ -257,26 +295,31 @@ class EdgePreviewSetup(Operator):
default="CREATE", default="CREATE",
) )
def execute(self, context): def execute(self, context: Context) -> Set[str]:
from ..core.model import FnModel from ..core.model import FnModel
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
logger.error("No MMD model root found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
if self.action == "CLEAN": if self.action == "CLEAN":
logger.info(f"Cleaning toon edge for model: {root.name}")
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
self.__clean_toon_edge(obj) self.__clean_toon_edge(obj)
else: else:
from ..bpyutils import Props from ..bpyutils import Props
logger.info(f"Creating toon edge for model: {root.name}")
scale = 0.2 * getattr(root, Props.empty_display_size) 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)) 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) self.report({"INFO"}, "Created %d toon edge(s)" % counts)
return {"FINISHED"} 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: if "mmd_edge_preview" in obj.modifiers:
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"]) 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.")) 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) self.__clean_toon_edge(obj)
materials = obj.data.materials materials = obj.data.materials
material_offset = len(materials) material_offset = len(materials)
@@ -310,10 +354,10 @@ class EdgePreviewSetup(Operator):
mod.vertex_group = "mmd_edge_preview" mod.vertex_group = "mmd_edge_preview"
return len(materials) - material_offset 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 vertices, materials = obj.data.vertices, obj.data.materials
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m} 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") vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
if vg_scale_index >= 0: 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} 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 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") 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: if mat_name in materials:
return materials[mat_name] return materials[mat_name]
mat = bpy.data.materials.get(mat_name, None) mat = bpy.data.materials.get(mat_name, None)
@@ -340,7 +384,7 @@ class EdgePreviewSetup(Operator):
self.__make_shader(mat) self.__make_shader(mat)
return mat return mat
def __make_shader(self, m): def __make_shader(self, m: Material) -> None:
m.use_nodes = True m.use_nodes = True
nodes, links = m.node_tree.nodes, m.node_tree.links 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["Color"].default_value = m.mmd_material.edge_color
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3] 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" group_name = "MMDEdgePreview"
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes): 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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re import re
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
import bpy import bpy
from bpy.types import Context, Object, Operator, ShapeKey
from .. import utils from .. import utils
from ..bpyutils import FnContext, FnObject from ..bpyutils import FnContext, FnObject
from ..core.bone import FnBone from ..core.bone import FnBone
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ....core.logging_setup import logger
class SelectObject(bpy.types.Operator): class SelectObject(bpy.types.Operator):
@@ -29,7 +32,8 @@ class SelectObject(bpy.types.Operator):
options={"HIDDEN", "SKIP_SAVE"}, 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]) utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"} 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>.*)") __PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
@classmethod @classmethod
def set_index(cls, obj, index): def set_index(cls, obj: Object, index: int) -> None:
m = cls.__PREFIX_REGEXP.match(obj.name) m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name name = m.group("name") if m else obj.name
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name) obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
@classmethod @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) m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name name = m.group("name") if m else obj.name
return name[len(prefix) :] if prefix and name.startswith(prefix) else name return name[len(prefix) :] if prefix and name.startswith(prefix) else name
@classmethod @classmethod
def normalize_indices(cls, objects): def normalize_indices(cls, objects: List[Object]) -> None:
for i, x in enumerate(objects): for i, x in enumerate(objects):
cls.set_index(x, i) cls.set_index(x, i)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return context.active_object return context.active_object is not None
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj = context.active_object obj = context.active_object
objects = self.__get_objects(obj) objects = self.__get_objects(obj)
if obj not in objects: 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"} return {"CANCELLED"}
objects.sort(key=lambda x: x.name) 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.move(objects, objects.index(obj), self.type)
self.normalize_indices(objects) self.normalize_indices(objects)
return {"FINISHED"} return {"FINISHED"}
def __get_objects(self, obj): def __get_objects(self, obj: Object) -> Any:
class __MovableList(list): class __MovableList(list):
def move(self, index_old, index_new): def move(self, index_old: int, index_new: int) -> None:
item = self[index_old] item = self[index_old]
self.remove(item) self.remove(item)
self.insert(index_new, item) self.insert(index_new, item)
@@ -102,11 +108,11 @@ class CleanShapeKeys(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return any(o.type == "MESH" for o in context.selected_objects) return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod @staticmethod
def __can_remove(key_block): def __can_remove(key_block: ShapeKey) -> bool:
if key_block.relative_key == key_block: if key_block.relative_key == key_block:
return False # Basis return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data): 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 False
return True 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: for kb in key_blocks:
if self.__can_remove(kb): if self.__can_remove(kb):
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, kb) FnObject.mesh_remove_shape_key(obj, kb)
if len(key_blocks) == 1: 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]) FnObject.mesh_remove_shape_key(obj, key_blocks[0])
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj: bpy.types.Object logger.info("Cleaning shape keys for selected objects")
obj: Object
for obj in context.selected_objects: for obj in context.selected_objects:
if obj.type != "MESH" or obj.data.shape_keys is None: if obj.type != "MESH" or obj.data.shape_keys is None:
continue continue
if not obj.data.shape_keys.use_relative: if not obj.data.shape_keys.use_relative:
continue # not be considered yet 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) self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
return {"FINISHED"} return {"FINISHED"}
@@ -144,21 +154,25 @@ class SeparateByMaterials(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
obj = context.active_object obj = context.active_object
return obj and obj.type == "MESH" 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) utils.separateByMaterials(obj)
if self.clean_shape_keys: if self.clean_shape_keys:
logger.debug("Cleaning shape keys after separation")
bpy.ops.mmd_tools.clean_shape_keys() bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
if root is None: if root is None:
logger.debug("No root object found, separating single object")
self.__separate_by_materials(obj) self.__separate_by_materials(obj)
else: else:
logger.debug(f"Root object found: {root.name}, preparing for separation")
bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view() bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -171,9 +185,11 @@ class SeparateByMaterials(bpy.types.Operator):
if len(mesh.data.materials) > 0: if len(mesh.data.materials) > 0:
mat = mesh.data.materials[0] mat = mesh.data.materials[0]
idx = mat_names.index(getattr(mat, "name", None)) idx = mat_names.index(getattr(mat, "name", None))
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
MoveObject.set_index(mesh, idx) MoveObject.set_index(mesh, idx)
for morph in root.mmd_root.material_morphs: for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh() FnMorph(morph, rig).update_mat_related_mesh()
utils.clearUnusedMeshes() utils.clearUnusedMeshes()
return {"FINISHED"} return {"FINISHED"}
@@ -191,13 +207,15 @@ class JoinMeshes(bpy.types.Operator):
default=True, default=True,
) )
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
if root is None: if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Joining meshes for model: {root.name}")
bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view() bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -205,9 +223,11 @@ class JoinMeshes(bpy.types.Operator):
rig = Model(root) rig = Model(root)
meshes_list = sorted(rig.meshes(), key=lambda x: x.name) meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
if not meshes_list: if not meshes_list:
logger.error("No meshes found in the model")
self.report({"ERROR"}, "The model does not have any meshes") self.report({"ERROR"}, "The model does not have any meshes")
return {"CANCELLED"} return {"CANCELLED"}
active_mesh = meshes_list[0] 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.select_objects(context, *meshes_list)
FnContext.set_active_object(context, active_mesh) FnContext.set_active_object(context, active_mesh)
@@ -216,15 +236,19 @@ class JoinMeshes(bpy.types.Operator):
for m in meshes_list[1:]: for m in meshes_list[1:]:
for mat in m.data.materials: for mat in m.data.materials:
if mat not in active_mesh.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) active_mesh.data.materials.append(mat)
# Join selected meshes # Join selected meshes
logger.debug("Joining meshes")
bpy.ops.object.join() bpy.ops.object.join()
if self.sort_shape_keys: if self.sort_shape_keys:
logger.debug("Sorting shape keys")
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys()) FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
active_mesh.active_shape_key_index = 0 active_mesh.active_shape_key_index = 0
for morph in root.mmd_root.material_morphs: 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) FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
utils.clearUnusedMeshes() utils.clearUnusedMeshes()
return {"FINISHED"} return {"FINISHED"}
@@ -238,17 +262,20 @@ class AttachMeshesToMMD(bpy.types.Operator):
add_armature_modifier: bpy.props.BoolProperty(default=True) 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) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
armObj = FnModel.find_armature_object(root) armObj = FnModel.find_armature_object(root)
if armObj is None: if armObj is None:
logger.error("Model armature not found")
self.report({"ERROR"}, "Model Armature not found") self.report({"ERROR"}, "Model Armature not found")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Attaching meshes to model: {root.name}")
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier) FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
return {"FINISHED"} return {"FINISHED"}
@@ -268,17 +295,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None 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) root_object = FnModel.find_root_object(context.active_object)
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) 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) 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) FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
return {"FINISHED"} return {"FINISHED"}
@@ -290,21 +318,22 @@ class RecalculateBoneRoll(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
obj = context.active_object obj = context.active_object
return obj and obj.type == "ARMATURE" return obj and obj.type == "ARMATURE"
def invoke(self, context, event): def invoke(self, context: Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
def draw(self, context): def draw(self, context: Context) -> None:
layout = self.layout layout = self.layout
c = layout.column() c = layout.column()
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION") c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
c.label(text="Click [OK] to run the operation.") c.label(text="Click [OK] to run the operation.")
def execute(self, context): def execute(self, context: Context) -> Set[str]:
arm = context.active_object arm = context.active_object
logger.info(f"Recalculating bone roll for armature: {arm.name}")
FnBone.apply_auto_bone_roll(arm) FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"} 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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, Set, Dict, Any, List, Tuple, Union
from ..bpyutils import FnContext from ..bpyutils import FnContext
from ..core.bone import FnBone, MigrationFnBone from ..core.bone import FnBone, MigrationFnBone
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MorphSliderSetup(bpy.types.Operator): class MorphSliderSetup(bpy.types.Operator):
@@ -29,18 +31,22 @@ class MorphSliderSetup(bpy.types.Operator):
default="CREATE", default="CREATE",
) )
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None 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): with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object) rig = Model(root_object)
if self.type == "BIND": if self.type == "BIND":
logger.info(f"Binding morph sliders for {root_object.name}")
rig.morph_slider.bind() rig.morph_slider.bind()
elif self.type == "UNBIND": elif self.type == "UNBIND":
logger.info(f"Unbinding morph sliders for {root_object.name}")
rig.morph_slider.unbind() rig.morph_slider.unbind()
else: else:
logger.info(f"Creating morph sliders for {root_object.name}")
rig.morph_slider.create() rig.morph_slider.create()
FnContext.set_active_object(context, active_object) 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_description = "Delete temporary physics objects of selected object and revert physics to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} 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) root_object = FnModel.find_root_object(context.active_object)
assert root_object is not None assert root_object is not None
logger.info(f"Cleaning rig for {root_object.name}")
rig = Model(root_object) rig = Model(root_object)
rig.clean() rig.clean()
FnContext.set_active_object(context, root_object) FnContext.set_active_object(context, root_object)
@@ -86,9 +93,10 @@ class BuildRig(bpy.types.Operator):
default=1e-06, 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) 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): with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object) rig = Model(root_object)
rig.build(self.non_collision_distance_scale, self.collision_margin) 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_description = "Delete shadow bones of selected object and revert bones to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None 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) FnContext.set_active_object(context, active_object)
return {"FINISHED"} return {"FINISHED"}
@@ -118,11 +129,12 @@ class ApplyAdditionalTransformConstraints(bpy.types.Operator):
bl_description = "Translate appended bones of selected object for Blender" bl_description = "Translate appended bones of selected object for Blender"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None 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) armature_object = FnModel.find_armature_object(root_object)
assert armature_object is not None assert armature_object is not None
@@ -149,12 +161,14 @@ class SetupBoneFixedAxes(bpy.types.Operator):
default="LOAD", default="LOAD",
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
armature_object = context.active_object armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE": if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object") 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"} return {"CANCELLED"}
logger.info(f"Setting up bone fixed axes with type: {self.type}")
if self.type == "APPLY": if self.type == "APPLY":
FnBone.apply_bone_fixed_axis(armature_object) FnBone.apply_bone_fixed_axis(armature_object)
FnBone.apply_additional_transformation(armature_object) FnBone.apply_additional_transformation(armature_object)
@@ -180,12 +194,14 @@ class SetupBoneLocalAxes(bpy.types.Operator):
default="LOAD", default="LOAD",
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
armature_object = context.active_object armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE": if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object") 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"} return {"CANCELLED"}
logger.info(f"Setting up bone local axes with type: {self.type}")
if self.type == "APPLY": if self.type == "APPLY":
FnBone.apply_bone_local_axes(armature_object) FnBone.apply_bone_local_axes(armature_object)
FnBone.apply_additional_transformation(armature_object) FnBone.apply_additional_transformation(armature_object)
@@ -207,16 +223,18 @@ class AddMissingVertexGroupsFromBones(bpy.types.Operator):
) )
@classmethod @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 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 active_object: bpy.types.Object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None 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) bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object)
if bone_order_mesh_object is None: if bone_order_mesh_object is None:
logger.error("Failed to find bone order mesh object")
return {"CANCELLED"} return {"CANCELLED"}
FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes) 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, 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 = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True)
rig.initialDisplayFrames() rig.initialDisplayFrames()
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
@@ -305,15 +324,16 @@ class ConvertToMMDModel(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object obj = context.active_object
return obj and obj.type == "ARMATURE" and obj.mode != "EDIT" 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 vm = context.window_manager
return vm.invoke_props_dialog(self) 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 # TODO convert some basic MMD properties
armature_object = context.active_object armature_object = context.active_object
scale = self.scale scale = self.scale
@@ -321,29 +341,31 @@ class ConvertToMMDModel(bpy.types.Operator):
root_object = FnModel.find_root_object(armature_object) root_object = FnModel.find_root_object(armature_object)
if root_object is None or root_object != armature_object.parent: 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) Model.create(model_name, model_name, scale, armature_object=armature_object)
self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context)) self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context))
self.__configure_rig(context, Model(armature_object.parent)) self.__configure_rig(context, Model(armature_object.parent))
return {"FINISHED"} return {"FINISHED"}
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects): def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects) -> None:
def __is_child_of_armature(mesh): def __is_child_of_armature(mesh: bpy.types.Object) -> bool:
if mesh.parent is None: if mesh.parent is None:
return False return False
return mesh.parent == armature_object or __is_child_of_armature(mesh.parent) 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: for m in mesh.modifiers:
if m.type == "ARMATURE" and m.object == armature_object: if m.type == "ARMATURE" and m.object == armature_object:
return True return True
return False return False
def __get_root(mesh): def __get_root(mesh: bpy.types.Object) -> bpy.types.Object:
if mesh.parent is None: if mesh.parent is None:
return mesh return mesh
return __get_root(mesh.parent) return __get_root(mesh.parent)
attached_count = 0
for x in objects: for x in objects:
if __is_using_armature(x) and not __is_child_of_armature(x): if __is_using_armature(x) and not __is_child_of_armature(x):
x_root = __get_root(x) x_root = __get_root(x)
@@ -351,27 +373,35 @@ class ConvertToMMDModel(bpy.types.Operator):
x_root.parent_type = "OBJECT" x_root.parent_type = "OBJECT"
x_root.parent = armature_object x_root.parent = armature_object
x_root.matrix_world = m x_root.matrix_world = m
attached_count += 1
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model): logger.debug(f"Attached {attached_count} meshes to armature")
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model) -> None:
root_object = mmd_model.rootObject() root_object = mmd_model.rootObject()
armature_object = mmd_model.armature() armature_object = mmd_model.armature()
mesh_objects = tuple(mmd_model.meshes()) mesh_objects = tuple(mmd_model.meshes())
logger.info(f"Configuring rig for {root_object.name} with {len(mesh_objects)} meshes")
mmd_model.loadMorphs() mmd_model.loadMorphs()
if self.middle_joint_bones_lock: if self.middle_joint_bones_lock:
vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups} 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: for pose_bone in armature_object.pose.bones:
if not pose_bone.parent: if not pose_bone.parent:
continue continue
if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups: if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups:
continue continue
pose_bone.lock_location = (True, True, True) pose_bone.lock_location = (True, True, True)
locked_bones += 1
logger.debug(f"Locked {locked_bones} middle joint bones")
from ..core.material import FnMaterial from ..core.material import FnMaterial
FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes) FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes)
try: try:
converted_materials = 0
for m in (x for mesh in mesh_objects for x in mesh.data.materials if x): for m in (x for mesh in mesh_objects for x in mesh.data.materials if x):
FnMaterial.convert_to_mmd_material(m, context) FnMaterial.convert_to_mmd_material(m, context)
mmd_material = m.mmd_material mmd_material = m.mmd_material
@@ -384,6 +414,8 @@ class ConvertToMMDModel(bpy.types.Operator):
line_color = list(m.line_color) line_color = list(m.line_color)
mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold 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)] 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: finally:
FnMaterial.set_nodes_are_readonly(False) FnMaterial.set_nodes_are_readonly(False)
from .display_item import DisplayItemQuickSetup from .display_item import DisplayItemQuickSetup
@@ -400,16 +432,17 @@ class ResetObjectVisibility(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context: bpy.types.Context): def poll(cls, context: bpy.types.Context) -> bool:
active_object: bpy.types.Object = context.active_object active_object: bpy.types.Object = context.active_object
return FnModel.find_root_object(active_object) is not None 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 active_object: bpy.types.Object = context.active_object
mmd_root_object = FnModel.find_root_object(active_object) mmd_root_object = FnModel.find_root_object(active_object)
assert mmd_root_object is not None assert mmd_root_object is not None
mmd_root = mmd_root_object.mmd_root mmd_root = mmd_root_object.mmd_root
logger.info(f"Resetting object visibility for {mmd_root_object.name}")
mmd_root_object.hide_set(False) mmd_root_object.hide_set(False)
rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object) 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_label = "Assemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None 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: with FnContext.temp_override_active_layer_collection(context, root_object) as context:
rig = Model(root_object) rig = Model(root_object)
MigrationFnBone.fix_mmd_ik_limit_override(rig.armature()) MigrationFnBone.fix_mmd_ik_limit_override(rig.armature())
@@ -452,6 +486,7 @@ class AssembleAll(bpy.types.Operator):
rig.build() rig.build()
rig.morph_slider.bind() rig.morph_slider.bind()
logger.debug("Binding SDEF weights")
with context.temp_override(selected_objects=[active_object]): with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_bind() bpy.ops.mmd_tools.sdef_bind()
root_object.mmd_root.use_property_driver = True root_object.mmd_root.use_property_driver = True
@@ -466,13 +501,15 @@ class DisassembleAll(bpy.types.Operator):
bl_label = "Disassemble All" bl_label = "Disassemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None 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: with FnContext.temp_override_active_layer_collection(context, root_object) as context:
root_object.mmd_root.use_property_driver = False root_object.mmd_root.use_property_driver = False
logger.debug("Unbinding SDEF weights")
with context.temp_override(selected_objects=[active_object]): with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_unbind() bpy.ops.mmd_tools.sdef_unbind()
+72 -31
View File
@@ -7,13 +7,17 @@
import itertools import itertools
from operator import itemgetter from operator import itemgetter
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set, Tuple, Any
import bmesh import bmesh
import bpy 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 ..bpyutils import FnContext
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MessageException(Exception): class MessageException(Exception):
@@ -35,8 +39,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: bpy.types.Context): def poll(cls, context: Context) -> bool:
active_object: Optional[bpy.types.Object] = context.active_object active_object: Optional[Object] = context.active_object
if context.mode != "POSE": if context.mode != "POSE":
return False return False
@@ -52,19 +56,22 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0 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) return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context): def execute(self, context: Context) -> Set[str]:
try: try:
logger.info("Starting model join by bones operation")
self.join(context) self.join(context)
logger.info("Model join by bones completed successfully")
except MessageException as ex: except MessageException as ex:
logger.error(f"Model join by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex)) self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"} return {"CANCELLED"}
return {"FINISHED"} return {"FINISHED"}
def join(self, context: bpy.types.Context): def join(self, context: Context) -> None:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_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: if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("No MMD Models selected") 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): with FnContext.temp_override_active_layer_collection(context, parent_root_object):
FnModel.join_models(parent_root_object, child_root_objects) FnModel.join_models(parent_root_object, child_root_objects)
@@ -82,11 +90,12 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
# Connect child bones # Connect child bones
if self.join_type == "CONNECTED": if self.join_type == "CONNECTED":
parent_edit_bone: bpy.types.EditBone = context.active_bone parent_edit_bone: EditBone = context.active_bone
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones) child_edit_bones: Set[EditBone] = set(context.selected_bones)
child_edit_bones.remove(parent_edit_bone) 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: for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True child_edit_bone.use_connect = True
@@ -111,8 +120,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: bpy.types.Context): def poll(cls, context: Context) -> bool:
active_object: Optional[bpy.types.Object] = context.active_object active_object: Optional[Object] = context.active_object
if context.mode != "POSE": if context.mode != "POSE":
return False return False
@@ -128,56 +137,70 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0 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) return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context): def execute(self, context: Context) -> Set[str]:
try: try:
logger.info("Starting model separate by bones operation")
self.separate(context) self.separate(context)
logger.info("Model separate by bones completed successfully")
except MessageException as ex: except MessageException as ex:
logger.error(f"Model separate by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex)) self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"} return {"CANCELLED"}
return {"FINISHED"} return {"FINISHED"}
def separate(self, context: bpy.types.Context): def separate(self, context: Context) -> None:
weight_threshold: float = self.weight_threshold weight_threshold: float = self.weight_threshold
mmd_scale = 0.08 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") 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: if self.include_descendant_bones:
logger.debug("Including descendant bones")
for edit_bone in root_bones: for edit_bone in root_bones:
with context.temp_override(active_bone=edit_bone): with context.temp_override(active_bone=edit_bone):
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1) 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} separate_bones: Dict[str, 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} 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 = 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 bones
separate_armature_object: Optional[bpy.types.Object] separate_armature_object: Optional[Object]
if self.separate_armature: if self.separate_armature:
logger.debug("Separating armature")
target_armature_object.select_set(True) target_armature_object.select_set(True)
bpy.ops.armature.separate() bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None) 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") bpy.ops.object.mode_set(mode="OBJECT")
# collect separate rigid bodies # 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 boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints # collect separate joints
separate_joints: Set[bpy.types.Object] = { separate_joints: Set[Object] = {
joint_object joint_object
for joint_object in mmd_model.joints() for joint_object in mmd_model.joints()
if boundary_joint_owner_condition( 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] separate_mesh_objects: Set[Object]
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object] model2separate_mesh_objects: Dict[Object, Object]
if len(mmd_model_mesh_objects) == 0: if len(mmd_model_mesh_objects) == 0:
logger.debug("No mesh objects to separate")
separate_mesh_objects = set() separate_mesh_objects = set()
model2separate_mesh_objects = dict() model2separate_mesh_objects = dict()
else: else:
# select meshes # select meshes
obj: bpy.types.Object logger.debug("Selecting meshes for separation")
obj: Object
for obj in context.view_layer.objects: for obj in context.view_layer.objects:
obj.select_set(obj in mmd_model_mesh_objects) obj.select_set(obj in mmd_model_mesh_objects)
context.view_layer.objects.active = mmd_model_mesh_objects[0] context.view_layer.objects.active = mmd_model_mesh_objects[0]
# separate mesh by selected vertices # separate mesh by selected vertices
logger.debug("Separating meshes by selected vertices")
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="SELECTED") 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") 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)) 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: 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_model.initialDisplayFrames()
separate_root_object = separate_model.rootObject() separate_root_object = separate_model.rootObject()
separate_root_object.matrix_world = mmd_root_object.matrix_world separate_root_object.matrix_world = mmd_root_object.matrix_world
separate_model_armature_object = separate_model.armature() separate_model_armature_object = separate_model.armature()
logger.debug(f"Created separate model with root: {separate_root_object.name}")
if self.separate_armature: if self.separate_armature:
logger.debug("Joining separate armature to new model")
with context.temp_override( with context.temp_override(
active_object=separate_model_armature_object, active_object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, separate_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() bpy.ops.object.join()
# add mesh # add mesh
logger.debug("Parenting separate mesh objects to new model")
with context.temp_override( with context.temp_override(
object=separate_model_armature_object, object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects], 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) bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object # replace mesh armature modifier.object
logger.debug("Updating armature modifiers on separate meshes")
for separate_mesh in separate_mesh_objects: 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) 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: 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: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
armature_modifier.object = separate_model_armature_object armature_modifier.object = separate_model_armature_object
logger.debug("Parenting rigid bodies to new model")
with context.temp_override( with context.temp_override(
object=separate_model.rigidGroupObject(), object=separate_model.rigidGroupObject(),
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies], selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
): ):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
logger.debug("Parenting joints to new model")
with context.temp_override( with context.temp_override(
object=separate_model.jointGroupObject(), object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints], selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
@@ -257,10 +293,12 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
assert separate_layer_collection is not None assert separate_layer_collection is not None
if mmd_layer_collection.name != separate_layer_collection.name: 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): for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
separate_layer_collection.collection.objects.link(separate_object) separate_layer_collection.collection.objects.link(separate_object)
mmd_layer_collection.collection.objects.unlink(separate_object) mmd_layer_collection.collection.objects.unlink(separate_object)
logger.debug("Copying MMD root properties")
FnModel.copy_mmd_root( FnModel.copy_mmd_root(
separate_root_object, separate_root_object,
mmd_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]: 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]:
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = dict() """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() target_bmesh: bmesh.types.BMesh = bmesh.new()
for mesh_object in mmd_model_mesh_objects: for mesh_object in mmd_model_mesh_objects:
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups 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.from_mesh(mesh, face_normals=False)
target_bmesh.select_mode |= {"VERT"} target_bmesh.select_mode |= {"VERT"}
deform_layer = target_bmesh.verts.layers.deform.verify() deform_layer = target_bmesh.verts.layers.deform.verify()
@@ -304,6 +344,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
vert.select_set(True) vert.select_set(True)
if selected_vertex_count > 0: 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 mesh2selected_vertex_count[mesh_object] = selected_vertex_count
target_bmesh.select_flush_mode() target_bmesh.select_flush_mode()
target_bmesh.to_mesh(mesh) 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. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, cast from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
import bpy import bpy
from mathutils import Quaternion, Vector from mathutils import Quaternion, Vector
@@ -16,10 +16,11 @@ from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ..utils import ItemMoveOp, ItemOp from ..utils import ItemMoveOp, ItemOp
from ....logging_setup import logger
# Util functions # 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): if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components") raise ValueError("Vectors should have the same number of components")
result = [] result = []
@@ -33,7 +34,7 @@ def divide_vector_components(vec1, vec2):
return result 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): if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components") raise ValueError("Vectors should have the same number of components")
result = [] result = []
@@ -42,7 +43,7 @@ def multiply_vector_components(vec1, vec2):
return result 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""" """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 n2 == 0:
if n1 == 0: if n1 == 0:
@@ -58,7 +59,7 @@ class AddMorph(bpy.types.Operator):
bl_description = "Add a morph item to active morph list" bl_description = "Add a morph item to active morph list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -68,6 +69,7 @@ class AddMorph(bpy.types.Operator):
morph.name = "New Morph" morph.name = "New Morph"
if morph_type.startswith("uv"): if morph_type.startswith("uv"):
morph.data_type = "VERTEX_GROUP" morph.data_type = "VERTEX_GROUP"
logger.debug(f"Added new morph of type {morph_type}")
return {"FINISHED"} return {"FINISHED"}
@@ -84,7 +86,7 @@ class RemoveMorph(bpy.types.Operator):
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -99,9 +101,11 @@ class RemoveMorph(bpy.types.Operator):
if self.all: if self.all:
morphs.clear() morphs.clear()
mmd_root.active_morph = 0 mmd_root.active_morph = 0
logger.debug(f"Removed all morphs of type {morph_type}")
else: else:
morphs.remove(mmd_root.active_morph) morphs.remove(mmd_root.active_morph)
mmd_root.active_morph = max(0, mmd_root.active_morph - 1) 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"} return {"FINISHED"}
@@ -111,7 +115,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
bl_description = "Move active morph item up/down in the list" bl_description = "Move active morph item up/down in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -120,6 +124,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
mmd_root.active_morph, mmd_root.active_morph,
self.type, self.type,
) )
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
return {"FINISHED"} return {"FINISHED"}
@@ -129,7 +134,7 @@ class CopyMorph(bpy.types.Operator):
bl_description = "Make a copy of active morph in the list" bl_description = "Make a copy of active morph in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -156,6 +161,7 @@ class CopyMorph(bpy.types.Operator):
for k, v in morph.items(): for k, v in morph.items():
morph_new[k] = v if k != "name" else name_tmp morph_new[k] = v if k != "name" else name_tmp
morph_new.name = name_orig + "_copy" # trigger name check morph_new.name = name_orig + "_copy" # trigger name check
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -165,17 +171,17 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
return False return False
return root.mmd_root.active_morph_type == "bone_morphs" 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) root = FnModel.find_root_object(context.active_object)
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root)) FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
logger.info("Overwrote bone morphs from active action pose")
return {"FINISHED"} return {"FINISHED"}
@@ -185,7 +191,7 @@ class AddMorphOffset(bpy.types.Operator):
bl_description = "Add a morph offset item to the list" bl_description = "Add a morph offset item to the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -210,6 +216,7 @@ class AddMorphOffset(bpy.types.Operator):
item.location = pose_bone.location item.location = pose_bone.location
item.rotation = pose_bone.rotation_quaternion item.rotation = pose_bone.rotation_quaternion
logger.debug(f"Added morph offset to {morph_type}")
return {"FINISHED"} return {"FINISHED"}
@@ -226,7 +233,7 @@ class RemoveMorphOffset(bpy.types.Operator):
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -243,17 +250,21 @@ class RemoveMorphOffset(bpy.types.Operator):
if morph_type.startswith("vertex"): if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
FnMorph.remove_shape_key(obj, morph.name) FnMorph.remove_shape_key(obj, morph.name)
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
return {"FINISHED"} return {"FINISHED"}
elif morph_type.startswith("uv"): elif morph_type.startswith("uv"):
if morph.data_type == "VERTEX_GROUP": if morph.data_type == "VERTEX_GROUP":
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
FnMorph.store_uv_morph_data(obj, morph) FnMorph.store_uv_morph_data(obj, morph)
logger.debug(f"Removed all UV morph offsets for {morph.name}")
return {"FINISHED"} return {"FINISHED"}
morph.data.clear() morph.data.clear()
morph.active_data = 0 morph.active_data = 0
logger.debug(f"Cleared all morph offsets for {morph.name}")
else: else:
morph.data.remove(morph.active_data) morph.data.remove(morph.active_data)
morph.active_data = max(0, morph.active_data - 1) morph.active_data = max(0, morph.active_data - 1)
logger.debug(f"Removed morph offset at index {morph.active_data}")
return {"FINISHED"} return {"FINISHED"}
@@ -269,7 +280,7 @@ class InitMaterialOffset(bpy.types.Operator):
default=0, default=0,
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root 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.specular_color = mat_data.ambient_color = (val,) * 3
mat_data.shininess = mat_data.edge_weight = val mat_data.shininess = mat_data.edge_weight = val
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4 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"} 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_description = "Calculates the offsets and apply them, then the temporary material is removed"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -328,6 +340,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
except ZeroDivisionError: except ZeroDivisionError:
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD 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: except ValueError:
self.report({"ERROR"}, "An unexpected error happened") self.report({"ERROR"}, "An unexpected error happened")
# We should stop on our tracks and re-raise the exception # 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 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) FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
logger.info(f"Applied material offset for {mat_data.material}")
return {"FINISHED"} return {"FINISHED"}
@@ -354,7 +368,7 @@ class CreateWorkMaterial(bpy.types.Operator):
bl_description = "Creates a temporary material to edit this offset" bl_description = "Creates a temporary material to edit this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root 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_color = list(edge_offset)
work_mmd_mat.edge_weight += mat_data.edge_weight work_mmd_mat.edge_weight += mat_data.edge_weight
logger.info(f"Created work material {work_mat_name}")
return {"FINISHED"} return {"FINISHED"}
@@ -422,13 +437,13 @@ class ClearTempMaterials(bpy.types.Operator):
bl_description = "Clears all the temporary materials" bl_description = "Clears all the temporary materials"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
for meshObj in FnModel.iterate_mesh_objects(root): 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: if m and "_temp" in m.name:
base_mat_name = m.name.split("_temp")[0] base_mat_name = m.name.split("_temp")[0]
try: try:
@@ -439,6 +454,7 @@ class ClearTempMaterials(bpy.types.Operator):
return False return False
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove) FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
logger.info("Cleared all temporary materials")
return {"FINISHED"} return {"FINISHED"}
@@ -448,7 +464,7 @@ class ViewBoneMorph(bpy.types.Operator):
bl_description = "View the result of active bone morph" bl_description = "View the result of active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None 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 = (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 mtx.translation = p_bone.location + morph_data.location
p_bone.matrix_basis = mtx p_bone.matrix_basis = mtx
logger.info(f"Viewing bone morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -472,13 +489,14 @@ class ClearBoneMorphView(bpy.types.Operator):
bl_description = "Reset transforms of all bones to their default values" bl_description = "Reset transforms of all bones to their default values"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
armature = FnModel.find_armature_object(root) armature = FnModel.find_armature_object(root)
for p_bone in armature.pose.bones: for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity() p_bone.matrix_basis.identity()
logger.info("Cleared bone morph view")
return {"FINISHED"} return {"FINISHED"}
@@ -488,7 +506,7 @@ class ApplyBoneMorph(bpy.types.Operator):
bl_description = "Apply current pose to active bone morph" bl_description = "Apply current pose to active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -506,6 +524,7 @@ class ApplyBoneMorph(bpy.types.Operator):
p_bone.bone.select = True p_bone.bone.select = True
else: else:
p_bone.bone.select = False p_bone.bone.select = False
logger.info(f"Applied current pose to bone morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -515,7 +534,7 @@ class SelectRelatedBone(bpy.types.Operator):
bl_description = "Select the bone assigned to this offset in the armature" bl_description = "Select the bone assigned to this offset in the armature"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -524,6 +543,7 @@ class SelectRelatedBone(bpy.types.Operator):
morph = mmd_root.bone_morphs[mmd_root.active_morph] morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph_data = morph.data[morph.active_data] morph_data = morph.data[morph.active_data]
utils.selectSingleBone(context, armature, morph_data.bone) utils.selectSingleBone(context, armature, morph_data.bone)
logger.debug(f"Selected bone: {morph_data.bone}")
return {"FINISHED"} 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_description = "Applies the location and rotation of this offset to the bone"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -546,6 +566,7 @@ class EditBoneOffset(bpy.types.Operator):
mtx.translation = morph_data.location mtx.translation = morph_data.location
p_bone.matrix_basis = mtx p_bone.matrix_basis = mtx
utils.selectSingleBone(context, armature, p_bone.name) utils.selectSingleBone(context, armature, p_bone.name)
logger.debug(f"Edited bone offset for {p_bone.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -555,7 +576,7 @@ class ApplyBoneOffset(bpy.types.Operator):
bl_description = "Stores the current bone location and rotation into this offset" bl_description = "Stores the current bone location and rotation into this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -567,6 +588,7 @@ class ApplyBoneOffset(bpy.types.Operator):
p_bone = armature.pose.bones[morph_data.bone] p_bone = armature.pose.bones[morph_data.bone]
morph_data.location = p_bone.location 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() 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"} 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_description = "View the result of active UV morph on current mesh object"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -627,6 +649,7 @@ class ViewUVMorph(bpy.types.Operator):
uv_tex.active_render = True uv_tex.active_render = True
meshObj.hide_set(False) meshObj.hide_set(False)
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info(f"Viewing UV morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -636,7 +659,7 @@ class ClearUVMorphView(bpy.types.Operator):
bl_description = "Clear all temporary data of UV morphs" bl_description = "Clear all temporary data of UV morphs"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -664,6 +687,7 @@ class ClearUVMorphView(bpy.types.Operator):
for act in bpy.data.actions: for act in bpy.data.actions:
if act.name.startswith("__uv.") and act.users < 1: if act.name.startswith("__uv.") and act.users < 1:
bpy.data.actions.remove(act) bpy.data.actions.remove(act)
logger.info("Cleared UV morph view")
return {"FINISHED"} return {"FINISHED"}
@@ -674,14 +698,14 @@ class EditUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object obj = context.active_object
if obj.type != "MESH": if obj.type != "MESH":
return False return False
active_uv_layer = obj.data.uv_layers.active active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.") 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 obj = context.active_object
meshObj = obj meshObj = obj
@@ -704,6 +728,7 @@ class EditUVMorph(bpy.types.Operator):
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info("Editing UV morph")
return {"FINISHED"} return {"FINISHED"}
@@ -714,14 +739,14 @@ class ApplyUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object obj = context.active_object
if obj.type != "MESH": if obj.type != "MESH":
return False return False
active_uv_layer = obj.data.uv_layers.active active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.") 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 obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -756,6 +781,7 @@ class ApplyUVMorph(bpy.types.Operator):
morph.data_type = "VERTEX_GROUP" morph.data_type = "VERTEX_GROUP"
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info(f"Applied UV morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -766,11 +792,12 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None 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) mmd_root_object = FnModel.find_root_object(context.active_object)
FnMorph.clean_duplicated_material_morphs(mmd_root_object) FnMorph.clean_duplicated_material_morphs(mmd_root_object)
logger.info("Cleaned duplicated material morphs")
return {"FINISHED"} return {"FINISHED"}
+37 -23
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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import Dict, Optional, Tuple, cast from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator
import bpy import bpy
from mathutils import Euler, Vector from mathutils import Euler, Vector
@@ -16,6 +16,7 @@ from ..bpyutils import FnContext, Props
from ..core import rigid_body from ..core import rigid_body
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.rigid_body import FnRigidBody from ..core.rigid_body import FnRigidBody
from ...logging_setup import logger
class SelectRigidBody(bpy.types.Operator): class SelectRigidBody(bpy.types.Operator):
@@ -43,15 +44,15 @@ class SelectRigidBody(bpy.types.Operator):
default=False, default=False,
) )
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_rigid_body_object(context.active_object) 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 obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
if root is None: if root is None:
@@ -173,7 +174,7 @@ class AddRigidBody(bpy.types.Operator):
default=0.1, 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_j: str = self.name_j
name_e: str = self.name_e name_e: str = self.name_e
size = self.size.copy() size = self.size.copy()
@@ -226,7 +227,7 @@ class AddRigidBody(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
if root_object is None: if root_object is None:
return False return False
@@ -237,7 +238,7 @@ class AddRigidBody(bpy.types.Operator):
return True return True
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(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) armature_object.select_set(False)
if len(selected_pose_bones) > 0: 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: for pose_bone in selected_pose_bones:
rigid = self.__add_rigid_body(context, root_object, pose_bone) rigid = self.__add_rigid_body(context, root_object, pose_bone)
rigid.select_set(True) rigid.select_set(True)
else: else:
logger.info("Adding a single rigid body without bone attachment")
rigid = self.__add_rigid_body(context, root_object) rigid = self.__add_rigid_body(context, root_object)
rigid.select_set(True) rigid.select_set(True)
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
no_bone = True no_bone = True
if context.selected_bones and len(context.selected_bones) > 0: if context.selected_bones and len(context.selected_bones) > 0:
no_bone = False no_bone = False
@@ -288,12 +291,13 @@ class RemoveRigidBody(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_rigid_body_object(context.active_object) 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 obj = context.active_object
root = FnModel.find_root_object(obj) 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 utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True) bpy.ops.object.delete(use_global=True)
if root: if root:
@@ -306,7 +310,8 @@ class RigidBodyBake(bpy.types.Operator):
bl_label = "Bake" bl_label = "Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} 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): with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True) bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
@@ -318,7 +323,8 @@ class RigidBodyDeleteBake(bpy.types.Operator):
bl_label = "Delete Bake" bl_label = "Delete Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} 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): with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT") bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
@@ -381,7 +387,7 @@ class AddJoint(bpy.types.Operator):
min=0, 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()) obj_seq = tuple(bone_map.keys())
for rigid_a, bone_a in bone_map.items(): for rigid_a, bone_a in bone_map.items():
for rigid_b, bone_b in bone_map.items(): for rigid_b, bone_b in bone_map.items():
@@ -394,7 +400,7 @@ class AddJoint(bpy.types.Operator):
else: else:
yield obj_seq 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 loc: Optional[Vector] = None
rot = Euler((0.0, 0.0, 0.0)) rot = Euler((0.0, 0.0, 0.0))
rigid_a, rigid_b = rigid_pair rigid_a, rigid_b = rigid_pair
@@ -432,7 +438,7 @@ class AddJoint(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
if root_object is None: if root_object is None:
return False return False
@@ -443,7 +449,7 @@ class AddJoint(bpy.types.Operator):
return True return True
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(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)) 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) FnContext.select_single_object(context, root_object).select_set(False)
if context.scene.rigidbody_world is None: if context.scene.rigidbody_world is None:
logger.info("Creating rigid body world")
bpy.ops.rigidbody.world_add() bpy.ops.rigidbody.world_add()
joint_count = 0
for pair in self.__enumerate_rigid_pair(bone_map): for pair in self.__enumerate_rigid_pair(bone_map):
joint = self.__add_joint(context, root_object, pair, bone_map) joint = self.__add_joint(context, root_object, pair, bone_map)
joint.select_set(True) joint.select_set(True)
joint_count += 1
logger.info(f"Added {joint_count} joints between rigid bodies")
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
@@ -476,12 +486,13 @@ class RemoveJoint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_joint_object(context.active_object) 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 obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
logger.info(f"Removing joint: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True) bpy.ops.object.delete(use_global=True)
if root: if root:
@@ -496,7 +507,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@staticmethod @staticmethod
def __get_rigid_body_world_objects(): def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]:
rigid_body.setRigidBodyWorldEnabled(True) rigid_body.setRigidBodyWorldEnabled(True)
rbw = bpy.context.scene.rigidbody_world rbw = bpy.context.scene.rigidbody_world
if not rbw.collection: if not rbw.collection:
@@ -511,12 +522,12 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
return rbw.collection.objects, rbw.constraints.objects return rbw.collection.objects, rbw.constraints.objects
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
scene = context.scene scene = context.scene
scene_objs = set(scene.objects) 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) 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 in scene_objs:
if obj not in group.values(): if obj not in group.values():
group.link(obj) group.link(obj)
@@ -525,7 +536,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
group.unlink(obj) group.unlink(obj)
return False return False
def _references(obj): def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]:
yield obj yield obj
if getattr(obj, "proxy", None): if getattr(obj, "proxy", None):
yield from _references(obj.proxy) yield from _references(obj.proxy)
@@ -542,6 +553,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# Object.rigid_body are removed, # Object.rigid_body are removed,
# but Object.rigid_body_constraint are retained. # but Object.rigid_body_constraint are retained.
# Therefore, it must be checked with Object.mmd_type. # 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"): for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
if not _update_group(i, rb_objs): if not _update_group(i, rb_objs):
continue continue
@@ -556,6 +568,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters. # TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
# mass, friction, restitution, linear_dumping, angular_dumping # 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): for i in (x for x in objects if x.rigid_body_constraint):
if not _update_group(i, rbc_objs): if not _update_group(i, rbc_objs):
continue continue
@@ -566,6 +579,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
rbc.object2 = rb_map.get(rbc.object2, rbc.object2) rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
if need_rebuild_physics: if need_rebuild_physics:
logger.info("Rebuilding physics for models")
for root_object in scene.objects: for root_object in scene.objects:
if root_object.mmd_type != "ROOT": if root_object.mmd_type != "ROOT":
continue 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. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Set from typing import Set, Tuple
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator, Context, Object
from ..core.model import FnModel from ..core.model import FnModel
from ..core.sdef import FnSDEF from ..core.sdef import FnSDEF
from ....core.logging_setup import logger
def _get_target_objects(context): def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]:
root_objects: Set[bpy.types.Object] = set() root_objects: Set[Object] = set()
selected_objects: Set[bpy.types.Object] = set() selected_objects: Set[Object] = set()
for i in context.selected_objects: for i in context.selected_objects:
if i.type == "MESH": if i.type == "MESH":
selected_objects.add(i) 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_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: Context) -> Set[str]:
target_meshes, _ = _get_target_objects(context) target_meshes, _ = _get_target_objects(context)
logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects")
for i in target_meshes: for i in target_meshes:
FnSDEF.clear_cache(i) FnSDEF.clear_cache(i)
FnSDEF.clear_cache(unused_only=True) FnSDEF.clear_cache(unused_only=True)
logger.debug("SDEF cache reset completed")
return {"FINISHED"} return {"FINISHED"}
@@ -75,19 +78,20 @@ class BindSDEF(Operator):
default=False, default=False,
) )
def invoke(self, context, event): def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
# TODO: Utility Functionalize def execute(self, context: Context) -> Set[str]:
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context) 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: for r in root_objects:
r.mmd_root.use_sdef = True r.mmd_root.use_sdef = True
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale) param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
count = sum(FnSDEF.bind(i, *param) for i in target_meshes) 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)") self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
return {"FINISHED"} return {"FINISHED"}
@@ -98,13 +102,15 @@ class UnbindSDEF(Operator):
bl_description = "Unbind MMD SDEF data of selected objects" bl_description = "Unbind MMD SDEF data of selected objects"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
# TODO: Utility Functionalize def execute(self, context: Context) -> Set[str]:
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context) target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Unbinding SDEF for {len(target_meshes)} objects")
for i in target_meshes: for i in target_meshes:
FnSDEF.unbind(i) FnSDEF.unbind(i)
for r in root_objects: for r in root_objects:
r.mmd_root.use_sdef = False r.mmd_root.use_sdef = False
logger.debug("SDEF unbinding completed")
return {"FINISHED"} 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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re import re
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
from bpy.types import Operator from bpy.types import Operator, Context
from mathutils import Matrix from mathutils import Matrix, Vector, Quaternion
from ...logging_setup import logger
class _SetShadingBase: class _SetShadingBase:
bl_options = {"REGISTER", "UNDO"} bl_options: Set[str] = {"REGISTER", "UNDO"}
@staticmethod @staticmethod
def _get_view3d_spaces(context): def _get_view3d_spaces(context: Context) -> Iterator[Any]:
if getattr(context.area, "type", None) == "VIEW_3D": if getattr(context.area, "type", None) == "VIEW_3D":
return (context.area.spaces[0],) return (context.area.spaces[0],)
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D") return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
@staticmethod @staticmethod
def _reset_color_management(context, use_display_device=True): def _reset_color_management(context: Context, use_display_device: bool = True) -> None:
try: try:
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device] context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
except TypeError: except TypeError:
pass pass
@staticmethod @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 i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
for s in i.material_slots: for s in i.material_slots:
if s.material is None: if s.material is None:
@@ -36,10 +39,11 @@ class _SetShadingBase:
s.material.use_nodes = False s.material.use_nodes = False
s.material.use_shadeless = use_shadeless 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" 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): for space in self._get_view3d_spaces(context):
shading = space.shading shading = space.shading
shading.type = "SOLID" shading.type = "SOLID"
@@ -47,39 +51,40 @@ class _SetShadingBase:
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL" shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
shading.show_object_outline = False shading.show_object_outline = False
shading.show_backface_culling = False shading.show_backface_culling = False
logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}")
return {"FINISHED"} return {"FINISHED"}
class SetGLSLShading(Operator, _SetShadingBase): class SetGLSLShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.set_glsl_shading" bl_idname: str = "mmd_tools.set_glsl_shading"
bl_label = "GLSL View" bl_label: str = "GLSL View"
bl_description = "Use GLSL shading with additional lighting" bl_description: str = "Use GLSL shading with additional lighting"
_shading_mode = "GLSL" _shading_mode: str = "GLSL"
class SetShadelessGLSLShading(Operator, _SetShadingBase): class SetShadelessGLSLShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.set_shadeless_glsl_shading" bl_idname: str = "mmd_tools.set_shadeless_glsl_shading"
bl_label = "Shadeless GLSL View" bl_label: str = "Shadeless GLSL View"
bl_description = "Use only toon shading" bl_description: str = "Use only toon shading"
_shading_mode = "SHADELESS" _shading_mode: str = "SHADELESS"
class ResetShading(Operator, _SetShadingBase): class ResetShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.reset_shading" bl_idname: str = "mmd_tools.reset_shading"
bl_label = "Reset View" bl_label: str = "Reset View"
bl_description = "Reset to default Blender shading" bl_description: str = "Reset to default Blender shading"
class FlipPose(Operator): class FlipPose(Operator):
bl_idname = "mmd_tools.flip_pose" bl_idname: str = "mmd_tools.flip_pose"
bl_label = "Flip Pose" bl_label: str = "Flip Pose"
bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis." bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
bl_options = {"REGISTER", "UNDO"} bl_options: Set[str] = {"REGISTER", "UNDO"}
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html # 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"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2}, {"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0}, {"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"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0}, {"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
] ]
__LR_MAP = { __LR_MAP: Dict[str, str] = {
"RIGHT": "LEFT", "RIGHT": "LEFT",
"Right": "Left", "Right": "Left",
"right": "left", "right": "left",
@@ -103,7 +108,7 @@ class FlipPose(Operator):
} }
@classmethod @classmethod
def flip_name(cls, name): def flip_name(cls, name: str) -> str:
for regex in cls.__LR_REGEX: for regex in cls.__LR_REGEX:
match = regex["re"].match(name) match = regex["re"].match(name)
if match: if match:
@@ -121,17 +126,15 @@ class FlipPose(Operator):
return "" return ""
@staticmethod @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)]) return type(vec1)([x * y for x, y in zip(vec1, vec2)])
@staticmethod @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)]) 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 @classmethod
def __flip_pose(cls, matrix_basis, bone_src, bone_dest): def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None:
from mathutils import Quaternion
m = bone_dest.bone.matrix_local.to_3x3().transposed() 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() mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
loc, rot, scale = matrix_basis.decompose() 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) bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
@classmethod @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" 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 pose_bones = context.active_object.pose.bones
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_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"} 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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, Set, Dict, Any, List, Tuple, Union, Type
from .. import utils from .. import utils
from ..core import material from ..core import material
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.model import FnModel from ..core.model import FnModel
from . import patch_library_overridable 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() 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() 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() 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() 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() 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() 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) 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() 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() 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() 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() 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() 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() 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() 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", "") 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 prop_value = value
if prop_value and prop_value != prop.get("name_j"): if prop_value and prop_value != prop.get("name_j"):
root = FnModel.find_root_object(bpy.context.active_object) root = FnModel.find_root_object(bpy.context.active_object)
if root is None: 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}) prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials})
else: 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_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)})
prop["name_j"] = prop_value prop["name_j"] = prop_value
@@ -275,13 +279,15 @@ class MMDMaterial(bpy.types.PropertyGroup):
description="Comment", 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) 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 @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)) bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial))
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMD material properties")
del bpy.types.Material.mmd_material 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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy 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 .. import utils
from ..core.bone import FnBone from ..core.bone import FnBone
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ....core.logging_setup import logger
def _morph_base_get_name(prop: "_MorphBase") -> str: def _morph_base_get_name(prop: "_MorphBase") -> str:
return prop.get("name", "") 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 mmd_root = prop.id_data.mmd_root
# morph_type = mmd_root.active_morph_type
morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower() 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) prop_name = prop.get("name", None)
if prop_name == value: if prop_name == value:
return 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) value = utils.unique_name(value, used_names)
if prop_name is not None: if prop_name is not None:
if morph_type == "vertex_morphs": if morph_type == "vertex_morphs":
kb_list = {} kb_list: Dict[str, List[ShapeKey]] = {}
for mesh in FnModel.iterate_mesh_objects(prop.id_data): for mesh in FnModel.iterate_mesh_objects(prop.id_data):
for kb in getattr(mesh.data.shape_keys, "key_blocks", ()): for kb in getattr(mesh.data.shape_keys, "key_blocks", ()):
kb_list.setdefault(kb.name, []).append(kb) kb_list.setdefault(kb.name, []).append(kb)
@@ -43,7 +43,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str):
kb.name = value kb.name = value
elif morph_type == "uv_morphs": elif morph_type == "uv_morphs":
vg_list = {} vg_list: Dict[str, List[Any]] = {}
for mesh in FnModel.iterate_mesh_objects(prop.id_data): for mesh in FnModel.iterate_mesh_objects(prop.id_data):
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh): for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh):
vg_list.setdefault(n, []).append(vg) vg_list.setdefault(n, []).append(vg)
@@ -72,6 +72,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str):
kb.name = value kb.name = value
prop["name"] = value prop["name"] = value
logger.debug(f"Renamed morph from '{prop_name}' to '{value}'")
class _MorphBase: class _MorphBase:
@@ -101,11 +102,11 @@ class _MorphBase:
def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: 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: if bone_id < 0:
return "" return ""
root_object = prop.id_data root_object: Object = prop.id_data
armature_object = FnModel.find_armature_object(root_object) armature_object: Optional[Object] = FnModel.find_armature_object(root_object)
if armature_object is None: if armature_object is None:
return "" return ""
pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id) 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 return pose_bone.name
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str): def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None:
root = prop.id_data root: Object = prop.id_data
arm = FnModel.find_armature_object(root) 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. # 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. # 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 return
pose_bone = arm.pose.bones[value] pose_bone = arm.pose.bones[value]
prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) 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"): if not prop.name.startswith("mmd_bind"):
return return
arm = FnModel(prop.id_data).morph_slider.dummy_armature 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: if bone:
bone.location = prop.location bone.location = prop.location
bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency 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): 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) mat_p = prop.get("material_data", None)
if mat_p is not None: if mat_p is not None:
return mat_p.name return mat_p.name
return "" 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: if value not in bpy.data.materials:
prop["material_data"] = None prop["material_data"] = None
prop["material_id"] = -1 prop["material_id"] = -1
logger.debug(f"Material '{value}' not found, setting material_data to None")
else: else:
mat = bpy.data.materials[value] mat = bpy.data.materials[value]
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
prop["material_data"] = mat prop["material_data"] = mat
prop["material_id"] = fnMat.material_id 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) mesh = FnModel.find_mesh_object_by_name(prop.id_data, value)
if mesh is not None: if mesh is not None:
prop["related_mesh_data"] = mesh.data prop["related_mesh_data"] = mesh.data
logger.debug(f"Set material morph data related mesh to '{value}'")
else: else:
prop["related_mesh_data"] = None 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) mesh_p = prop.get("related_mesh_data", None)
if mesh_p is not None: if mesh_p is not None:
return mesh_p.name return mesh_p.name
return "" 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"): if not prop.name.startswith("mmd_bind"):
return return
from ..core.shader import _MaterialMorph from ..core.shader import _MaterialMorph
@@ -229,9 +236,11 @@ def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _co
mat = prop["material_data"] mat = prop["material_data"]
if mat is not None: if mat is not None:
_MaterialMorph.update_morph_inputs(mat, prop) _MaterialMorph.update_morph_inputs(mat, prop)
logger.debug(f"Updated material morph modifiable values for '{prop.name}'")
else: else:
for mat in FnModel(prop.id_data).materials(): for mat in FnModel(prop.id_data).materials():
_MaterialMorph.update_morph_inputs(mat, prop) _MaterialMorph.update_morph_inputs(mat, prop)
logger.debug(f"Updated material morph modifiable values for all materials")
class MaterialMorphData(bpy.types.PropertyGroup): class MaterialMorphData(bpy.types.PropertyGroup):
@@ -407,9 +416,6 @@ class UVMorphOffset(bpy.types.PropertyGroup):
name="UV Offset", name="UV Offset",
description="UV offset", description="UV offset",
size=4, size=4,
# min=-1,
# max=1,
# precision=3,
step=0.1, step=0.1,
default=[0, 0, 0, 0], 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. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import cast from typing import cast, Optional, Any, Union
import bpy import bpy
from bpy.types import Context, PropertyGroup, PoseBone, Object, Armature
from ..core.bone import FnBone from ..core.bone import FnBone
from . import patch_library_overridable 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 prop["is_additional_transform_dirty"] = True
p_bone = context.active_pose_bone p_bone = context.active_pose_bone
if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer(): 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) 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 pose_bone = context.active_pose_bone
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer(): 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) FnBone.update_additional_transform_influence(pose_bone)
else: else:
prop["is_additional_transform_dirty"] = True 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 arm = prop.id_data
bone_id = prop.get("additional_transform_bone_id", -1) bone_id = prop.get("additional_transform_bone_id", -1)
if bone_id < 0: if bone_id < 0:
@@ -38,7 +42,7 @@ def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
return pose_bone.name 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 arm = prop.id_data
prop["is_additional_transform_dirty"] = True prop["is_additional_transform_dirty"] = True
if value not in arm.pose.bones.keys(): 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) 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_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -184,11 +188,12 @@ class MMDBone(bpy.types.PropertyGroup):
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True) 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) 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 @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.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.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")) 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 @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMDBone properties")
del bpy.types.PoseBone.mmd_ik_toggle del bpy.types.PoseBone.mmd_ik_toggle
del bpy.types.PoseBone.mmd_shadow_bone_type del bpy.types.PoseBone.mmd_shadow_bone_type
del bpy.types.PoseBone.is_mmd_shadow_bone del bpy.types.PoseBone.is_mmd_shadow_bone
del bpy.types.PoseBone.mmd_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 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 b in armature_object.pose.bones:
for c in b.constraints: for c in b.constraints:
if c.type == "IK" and c.subtarget == prop.name: 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 c.influence = v
b = b if c.use_tail else b.parent b = b if c.use_tail else b.parent
for b in ([b] + b.parent_recursive)[: c.chain_count]: for b in ([b] + b.parent_recursive)[: c.chain_count]:
+36 -28
View File
@@ -8,32 +8,35 @@
"""Properties for rigid bodies and joints""" """Properties for rigid bodies and joints"""
import bpy import bpy
from typing import Optional, Any, Set, List, Dict, Tuple, Union
from bpy.types import Context, Object, PropertyGroup, Material
from .. import bpyutils from .. import bpyutils
from ..core import rigid_body from ..core import rigid_body
from ..core.rigid_body import RigidBodyMaterial, FnRigidBody from ..core.rigid_body import RigidBodyMaterial, FnRigidBody
from ..core.model import FnModel from ..core.model import FnModel
from . import patch_library_overridable from . import patch_library_overridable
from ....core.logging_setup import logger
def _updateCollisionGroup(prop, _context): def _updateCollisionGroup(prop: PropertyGroup, _context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
materials = obj.data.materials materials: List[Material] = obj.data.materials
if len(materials) == 0: if len(materials) == 0:
materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number)) materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number))
else: else:
obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number) obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number)
def _updateType(prop, _context): def _updateType(prop: PropertyGroup, _context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
rb = obj.rigid_body rb = obj.rigid_body
if rb: if rb:
rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC
def _updateShape(prop, _context): def _updateShape(prop: PropertyGroup, _context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
if len(obj.data.vertices) > 0: if len(obj.data.vertices) > 0:
size = prop.size size = prop.size
@@ -44,8 +47,8 @@ def _updateShape(prop, _context):
rb.collision_shape = prop.shape rb.collision_shape = prop.shape
def _get_bone(prop): def _get_bone(prop: PropertyGroup) -> str:
obj = prop.id_data obj: Object = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None) relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation: if relation:
arm = relation.target arm = relation.target
@@ -55,9 +58,9 @@ def _get_bone(prop):
return prop.get("bone", "") return prop.get("bone", "")
def _set_bone(prop, value): def _set_bone(prop: PropertyGroup, value: str) -> None:
bone_name = value bone_name: str = value
obj = prop.id_data obj: Object = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None) relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation is None: if relation is None:
relation = obj.constraints.new("CHILD_OF") relation = obj.constraints.new("CHILD_OF")
@@ -78,16 +81,16 @@ def _set_bone(prop, value):
prop["bone"] = bone_name 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": if prop.id_data.mmd_type != "RIGID_BODY":
return (0, 0, 0) return (0, 0, 0)
return FnRigidBody.get_rigid_body_size(prop.id_data) return FnRigidBody.get_rigid_body_size(prop.id_data)
def _set_size(prop, value): def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None:
obj = prop.id_data obj: Object = prop.id_data
assert obj.mode == "OBJECT" # not support other mode yet assert obj.mode == "OBJECT" # not support other mode yet
shape = prop.shape shape: str = prop.shape
mesh = obj.data mesh = obj.data
rb = obj.rigid_body rb = obj.rigid_body
@@ -146,15 +149,15 @@ def _set_size(prop, value):
mesh.update() mesh.update()
def _get_rigid_name(prop): def _get_rigid_name(prop: PropertyGroup) -> str:
return prop.get("name", "") return prop.get("name", "")
def _set_rigid_name(prop, value): def _set_rigid_name(prop: PropertyGroup, value: str) -> None:
prop["name"] = value prop["name"] = value
class MMDRigidBody(bpy.types.PropertyGroup): class MMDRigidBody(PropertyGroup):
name_j: bpy.props.StringProperty( name_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -227,16 +230,18 @@ class MMDRigidBody(bpy.types.PropertyGroup):
) )
@staticmethod @staticmethod
def register(): def register() -> None:
logger.debug("Registering MMDRigidBody property")
bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody)) bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody))
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMDRigidBody property")
del bpy.types.Object.mmd_rigid del bpy.types.Object.mmd_rigid
def _updateSpringLinear(prop, context): def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
rbc = obj.rigid_body_constraint rbc = obj.rigid_body_constraint
if rbc: if rbc:
rbc.spring_stiffness_x = prop.spring_linear[0] rbc.spring_stiffness_x = prop.spring_linear[0]
@@ -244,8 +249,8 @@ def _updateSpringLinear(prop, context):
rbc.spring_stiffness_z = prop.spring_linear[2] rbc.spring_stiffness_z = prop.spring_linear[2]
def _updateSpringAngular(prop, context): def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
rbc = obj.rigid_body_constraint rbc = obj.rigid_body_constraint
if rbc and hasattr(rbc, "use_spring_ang_x"): if rbc and hasattr(rbc, "use_spring_ang_x"):
rbc.spring_stiffness_ang_x = prop.spring_angular[0] 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] rbc.spring_stiffness_ang_z = prop.spring_angular[2]
class MMDJoint(bpy.types.PropertyGroup): class MMDJoint(PropertyGroup):
name_j: bpy.props.StringProperty( name_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -287,9 +292,12 @@ class MMDJoint(bpy.types.PropertyGroup):
) )
@staticmethod @staticmethod
def register(): def register() -> None:
logger.debug("Registering MMDJoint property")
bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint)) bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint))
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMDJoint property")
del bpy.types.Object.mmd_joint del bpy.types.Object.mmd_joint
+41 -25
View File
@@ -8,6 +8,7 @@
"""Properties for MMD model root object""" """Properties for MMD model root object"""
import bpy import bpy
from typing import Optional, List, Dict, Any, Set, Tuple, Union, Type, TypeVar, cast
from .. import utils from .. import utils
from ..bpyutils import FnContext from ..bpyutils import FnContext
@@ -17,9 +18,10 @@ from ..core.sdef import FnSDEF
from . import patch_library_overridable from . import patch_library_overridable
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
from .translations import MMDTranslation 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) d = constraint.driver_add(path, index)
variables = d.driver.variables variables = d.driver.variables
for x in variables: for x in variables:
@@ -27,7 +29,7 @@ def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
return d.driver, variables 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 = variables.new()
var.name = prefix + str(len(variables)) var.name = prefix + str(len(variables))
var.type = "SINGLE_PROP" var.type = "SINGLE_PROP"
@@ -38,17 +40,18 @@ def __add_single_prop(variables, id_obj, data_path, prefix):
return var return var
def _toggleUsePropertyDriver(self: "MMDRoot", _context): def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> None:
root_object: bpy.types.Object = self.id_data root_object: bpy.types.Object = self.id_data
armature_object = FnModel.find_armature_object(root_object) armature_object = FnModel.find_armature_object(root_object)
if armature_object is None: if armature_object is None:
ik_map = {} ik_map: Dict[Any, Tuple[Any, Any]] = {}
else: else:
bones = armature_object.pose.bones 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} 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: if self.use_property_driver:
logger.debug("Enabling property drivers for %s", root_object.name)
for ik, (b, c) in ik_map.items(): for ik, (b, c) in ik_map.items():
driver, variables = __driver_variables(c, "influence") 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 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, variables = __driver_variables(i, prop_hide)
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
else: else:
logger.debug("Disabling property drivers for %s", root_object.name)
for ik, (b, c) in ik_map.items(): for ik, (b, c) in ik_map.items():
c.driver_remove("influence") c.driver_remove("influence")
b = b if c.use_tail else b.parent 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 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 i in FnModel.iterate_mesh_objects(self.id_data):
for m in i.data.materials: for m in i.data.materials:
if m: if m:
FnMaterial(m).use_toon_texture(use_toon) 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 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 i in FnModel.iterate_mesh_objects(self.id_data):
for m in i.data.materials: for m in i.data.materials:
if m: if m:
FnMaterial(m).use_sphere_texture(use_sphere, i) 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 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): for i in FnModel.iterate_mesh_objects(self.id_data):
FnSDEF.mute_sdef_set(i, mute_sdef) 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 root = self.id_data
hide = not self.show_meshes 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): for i in FnModel.iterate_mesh_objects(self.id_data):
i.hide_set(hide) i.hide_set(hide)
i.hide_render = hide i.hide_render = hide
@@ -112,27 +120,30 @@ def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
FnContext.set_active_object(context, root) 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 root = self.id_data
hide = not self.show_rigid_bodies 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): for i in FnModel.iterate_rigid_body_objects(root):
i.hide_set(hide) i.hide_set(hide)
if hide and context.active_object is None: if hide and context.active_object is None:
FnContext.set_active_object(context, root) 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 root_object = self.id_data
hide = not self.show_joints 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): for i in FnModel.iterate_joint_objects(root_object):
i.hide_set(hide) i.hide_set(hide)
if hide and context.active_object is None: if hide and context.active_object is None:
FnContext.set_active_object(context, root_object) 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 root_object: bpy.types.Object = self.id_data
hide = not self.show_temporary_objects 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): with FnContext.temp_override_active_layer_collection(context, root_object):
for i in FnModel.iterate_temporary_objects(root_object): for i in FnModel.iterate_temporary_objects(root_object):
i.hide_set(hide) i.hide_set(hide)
@@ -140,45 +151,48 @@ def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Cont
FnContext.set_active_object(context, root_object) 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 root = self.id_data
show_names = root.mmd_root.show_names_of_rigid_bodies 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): for i in FnModel.iterate_rigid_body_objects(root):
i.show_name = show_names i.show_name = show_names
def _toggleShowNamesOfJoints(self: "MMDRoot", _context): def _toggleShowNamesOfJoints(self: "MMDRoot", _context: bpy.types.Context) -> None:
root = self.id_data root = self.id_data
show_names = root.mmd_root.show_names_of_joints 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): for i in FnModel.iterate_joint_objects(root):
i.show_name = show_names i.show_name = show_names
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool): def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool) -> None:
root = prop.id_data root = prop.id_data
arm = FnModel.find_armature_object(root) arm = FnModel.find_armature_object(root)
if arm is None: if arm is None:
return return
if not v and bpy.context.active_object == arm: if not v and bpy.context.active_object == arm:
FnContext.set_active_object(bpy.context, root) FnContext.set_active_object(bpy.context, root)
logger.debug("Setting armature visibility to %s for %s", v, root.name)
arm.hide_set(not v) arm.hide_set(not v)
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"): def _getVisibilityOfMMDRigArmature(prop: "MMDRoot") -> bool:
if prop.id_data.mmd_type != "ROOT": if prop.id_data.mmd_type != "ROOT":
return False return False
arm = FnModel.find_armature_object(prop.id_data) arm = FnModel.find_armature_object(prop.id_data)
return arm and not arm.hide_get() 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] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_rigid_body_object(obj): if FnModel.is_rigid_body_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_rigidbody_object_index"] = v prop["active_rigidbody_object_index"] = v
def _getActiveRigidbodyObject(prop: "MMDRoot"): def _getActiveRigidbodyObject(prop: "MMDRoot") -> int:
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_rigid_body_object(active_obj): if FnModel.is_rigid_body_object(active_obj):
@@ -186,14 +200,14 @@ def _getActiveRigidbodyObject(prop: "MMDRoot"):
return prop.get("active_rigidbody_object_index", 0) 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] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_joint_object(obj): if FnModel.is_joint_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_joint_object_index"] = v prop["active_joint_object_index"] = v
def _getActiveJointObject(prop: "MMDRoot"): def _getActiveJointObject(prop: "MMDRoot") -> int:
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_joint_object(active_obj): if FnModel.is_joint_object(active_obj):
@@ -201,26 +215,26 @@ def _getActiveJointObject(prop: "MMDRoot"):
return prop.get("active_joint_object_index", 0) 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: if "active_morph_indices" not in prop:
prop["active_morph_indices"] = [0] * 5 prop["active_morph_indices"] = [0] * 5
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v 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: if "active_morph_indices" in prop:
return prop["active_morph_indices"][prop.get("active_morph_type", 3)] return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
return 0 return 0
def _setActiveMeshObject(prop: "MMDRoot", v: int): def _setActiveMeshObject(prop: "MMDRoot", v: int) -> None:
obj = FnContext.get_scene_objects(bpy.context)[v] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_mesh_object(obj): if FnModel.is_mesh_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_mesh_index"] = v prop["active_mesh_index"] = v
def _getActiveMeshObject(prop: "MMDRoot"): def _getActiveMeshObject(prop: "MMDRoot") -> int:
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_mesh_object(active_obj): if FnModel.is_mesh_object(active_obj):
@@ -520,7 +534,8 @@ class MMDRoot(bpy.types.PropertyGroup):
prop.hide_viewport = value prop.hide_viewport = value
@staticmethod @staticmethod
def register(): def register() -> None:
logger.debug("Registering MMDRoot property group")
bpy.types.Object.mmd_type = patch_library_overridable( bpy.types.Object.mmd_type = patch_library_overridable(
bpy.props.EnumProperty( bpy.props.EnumProperty(
name="Type", name="Type",
@@ -570,7 +585,8 @@ class MMDRoot(bpy.types.PropertyGroup):
) )
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMDRoot property group")
del bpy.types.Object.hide del bpy.types.Object.hide
del bpy.types.Object.select del bpy.types.Object.select
del bpy.types.Object.mmd_root del bpy.types.Object.mmd_root
+88 -33
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. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import csv import csv
import logging
import time import time
from typing import List, Tuple, Dict, Optional, Any, Generator, Union, TextIO, Iterator, Set
import bpy import bpy
from bpy.types import Text, Context
from .bpyutils import FnContext 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"), ("全ての親", "ParentNode"),
("操作中心", "ControlNode"), ("操作中心", "ControlNode"),
("センター", "Center"), ("センター", "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: for tuple in jp_to_en_tuples:
if tuple[0] in name: if tuple[0] in name:
name = name.replace(tuple[0], tuple[1]) name = name.replace(tuple[0], tuple[1])
logger.debug(f"Translation result: {name}")
return 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() translator = MMDTranslator()
if isinstance(csvfile, bpy.types.Text): if isinstance(csvfile, bpy.types.Text):
logger.debug(f"Loading translator from Text object: {csvfile.name}")
translator.load_from_stream(csvfile) translator.load_from_stream(csvfile)
elif isinstance(csvfile, dict): elif isinstance(csvfile, dict):
logger.debug(f"Loading translator from dictionary with {len(csvfile)} entries")
translator.csv_tuples.extend(csvfile.items()) translator.csv_tuples.extend(csvfile.items())
elif csvfile in bpy.data.texts.keys(): elif csvfile in bpy.data.texts.keys():
logger.debug(f"Loading translator from text data: {csvfile}")
translator.load_from_stream(bpy.data.texts[csvfile]) translator.load_from_stream(bpy.data.texts[csvfile])
else: else:
logger.debug(f"Loading translator from file: {csvfile}")
translator.load(csvfile) translator.load(csvfile)
if not keep_order: if not keep_order:
@@ -318,16 +332,20 @@ def getTranslator(csvfile="", keep_order=False):
class MMDTranslator: class MMDTranslator:
def __init__(self): """Handles translation of Japanese text to English for MMD models."""
self.__csv_tuples = []
self.__fails = {} def __init__(self) -> None:
self.__csv_tuples: List[Tuple[str, str]] = []
self.__fails: Dict[str, str] = {}
@staticmethod @staticmethod
def default_csv_filepath(): def default_csv_filepath() -> str:
"""Get the default CSV filepath for translations."""
return __file__[:-3] + ".csv" return __file__[:-3] + ".csv"
@staticmethod @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()) text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath())
csv_text = bpy.data.texts.get(text_name, None) csv_text = bpy.data.texts.get(text_name, None)
if csv_text is None: if csv_text is None:
@@ -335,69 +353,88 @@ class MMDTranslator:
return csv_text return csv_text
@staticmethod @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: for pair in tuples:
if pair[0] in name: if pair[0] in name:
name = name.replace(pair[0], pair[1]) name = name.replace(pair[0], pair[1])
return name return name
@property @property
def csv_tuples(self): def csv_tuples(self) -> List[Tuple[str, str]]:
"""Get the CSV tuples."""
return self.__csv_tuples return self.__csv_tuples
@property @property
def fails(self): def fails(self) -> Dict[str, str]:
"""Get the failed translations."""
return self.__fails 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)) 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 from collections import OrderedDict
count_old = len(self.__csv_tuples) 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]) 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.clear()
self.__csv_tuples.extend(tuples_dict.values()) 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) 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: try:
name.encode("ascii", errors="strict") name.encode("ascii", errors="strict")
except UnicodeEncodeError: except UnicodeEncodeError:
return False return False
return True 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: if from_full_width:
name = self.half_to_full(name) name = self.half_to_full(name)
name_new = self.replace_from_tuples(name, self.__csv_tuples) name_new = self.replace_from_tuples(name, self.__csv_tuples)
if default is not None and not self.is_translated(name_new): if default is not None and not self.is_translated(name_new):
logger.warning(f"Translation failed for: {name}")
self.__fails[name] = name_new self.__fails[name] = name_new
return default return default
return name_new 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") text_name = text_name or (__name__ + ".fails")
txt = self.get_csv_text(text_name) txt = self.get_csv_text(text_name)
fmt = '"%s","%s"' fmt = '"%s","%s"'
items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row)) 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)) 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 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() csvfile = csvfile or self.get_csv_text()
if isinstance(csvfile, bpy.types.Text): if isinstance(csvfile, bpy.types.Text):
csvfile = (l.body + "\n" for l in csvfile.lines) csvfile = (l.body + "\n" for l in csvfile.lines)
spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True) spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True)
csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2] csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2]
self.__csv_tuples = csv_tuples 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() csvfile = csvfile or self.get_csv_text()
lineterminator = "\r\n" lineterminator = "\r\n"
if isinstance(csvfile, bpy.types.Text): if isinstance(csvfile, bpy.types.Text):
@@ -405,27 +442,38 @@ class MMDTranslator:
lineterminator = "\n" lineterminator = "\n"
spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL) spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL)
spamwriter.writerows(self.__csv_tuples) 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() filepath = filepath or self.default_csv_filepath()
logging.info("Loading csv file:\t%s", filepath) logger.info("Loading CSV file: %s", filepath)
try:
with open(filepath, "rt", encoding="utf-8", newline="") as csvfile: with open(filepath, "rt", encoding="utf-8", newline="") as csvfile:
self.load_from_stream(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() filepath = filepath or self.default_csv_filepath()
logging.info("Saving csv file:\t%s", filepath) logger.info("Saving CSV file: %s", filepath)
try:
with open(filepath, "wt", encoding="utf-8", newline="") as csvfile: with open(filepath, "wt", encoding="utf-8", newline="") as csvfile:
self.save_to_stream(csvfile) self.save_to_stream(csvfile)
except Exception as e:
logger.error(f"Failed to save CSV file: {e}")
class DictionaryEnum: class DictionaryEnum:
__items_ttl = 0.0 """Handles dictionary enumeration for UI."""
__items_cache = None
__items_ttl: float = 0.0
__items_cache: Optional[List[Tuple[str, str, str, int]]] = None
@staticmethod @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(): if DictionaryEnum.__items_ttl > time.time():
return DictionaryEnum.__items_cache return DictionaryEnum.__items_cache
@@ -437,7 +485,7 @@ class DictionaryEnum:
items.append(("INTERNAL", "Internal Dictionary", "The dictionary defined in " + __name__, len(items))) 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")): 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 import os
@@ -450,12 +498,19 @@ class DictionaryEnum:
if "dictionary" in prop: if "dictionary" in prop:
prop["dictionary"] = min(prop["dictionary"], len(items) - 1) prop["dictionary"] = min(prop["dictionary"], len(items) - 1)
logger.debug(f"Found {len(items)} dictionary items")
return items return items
@staticmethod @staticmethod
def get_translator(dictionary): def get_translator(dictionary: str) -> Optional[MMDTranslator]:
"""Get a translator for the specified dictionary."""
if dictionary == "DISABLED": if dictionary == "DISABLED":
logger.debug("Translation disabled")
return None return None
if dictionary == "INTERNAL": if dictionary == "INTERNAL":
logger.debug("Using internal dictionary")
return getTranslator(dict(jp_to_en_tuples)) return getTranslator(dict(jp_to_en_tuples))
logger.debug(f"Using dictionary: {dictionary}")
return getTranslator(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. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
import os import os
import re import re
from typing import Callable, Optional, Set from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any
import bpy import bpy
from bpy.types import Object, Bone, PoseBone, Mesh, VertexGroup
from ..logging_setup import logger
from .bpyutils import FnContext from .bpyutils import FnContext
## 指定したオブジェクトのみを選択状態かつアクティブにする ## 指定したオブジェクトのみを選択状態かつアクティブにする
def selectAObject(obj): def selectAObject(obj: Object) -> None:
try: try:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
except Exception: except Exception:
@@ -27,13 +28,13 @@ def selectAObject(obj):
## 現在のモードを指定したオブジェクトのEdit Modeに変更する ## 現在のモードを指定したオブジェクトのEdit Modeに変更する
def enterEditMode(obj): def enterEditMode(obj: Object) -> None:
selectAObject(obj) selectAObject(obj)
if obj.mode != "EDIT": if obj.mode != "EDIT":
bpy.ops.object.mode_set(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) selectAObject(obj)
FnContext.set_active_object(FnContext.ensure_context(), parent) FnContext.set_active_object(FnContext.ensure_context(), parent)
bpy.ops.object.mode_set(mode="POSE") bpy.ops.object.mode_set(mode="POSE")
@@ -42,7 +43,7 @@ def setParentToBone(obj, parent, bone_name):
bpy.ops.object.mode_set(mode="OBJECT") 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: try:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
except: except:
@@ -55,7 +56,7 @@ def selectSingleBone(context, armature, bone_name, reset_pose=False):
for p_bone in armature.pose.bones: for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity() p_bone.matrix_basis.identity()
armature_bones: bpy.types.ArmatureBones = armature.data.bones armature_bones: bpy.types.ArmatureBones = armature.data.bones
i: bpy.types.Bone i: Bone
for i in armature_bones: for i in armature_bones:
i.select = i.name == bone_name i.select = i.name == bone_name
i.select_head = i.select_tail = i.select i.select_head = i.select_tail = i.select
@@ -69,7 +70,7 @@ __CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
## 日本語で左右を命名されている名前をblender方式のL(R)に変更する ## 日本語で左右を命名されている名前を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) m = __CONVERT_NAME_TO_L_REGEXP.match(name)
delimiter = "_" if use_underscore else "." delimiter = "_" if use_underscore else "."
if m: 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)))") __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) match = __CONVERT_L_TO_NAME_REGEXP.search(name)
if match: if match:
return f"{name[0:match.start()]}{match['after']}{name[match.end():]}" 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する ## 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 mesh = meshObj.data
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name] src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
dest_vertex_group = meshObj.vertex_groups[dest_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 pass
def separateByMaterials(meshObj: bpy.types.Object): def separateByMaterials(meshObj: Object) -> None:
if len(meshObj.data.materials) < 2: if len(meshObj.data.materials) < 2:
selectAObject(meshObj) selectAObject(meshObj)
return return
@@ -134,7 +135,7 @@ def separateByMaterials(meshObj: bpy.types.Object):
bpy.data.objects.remove(dummy_parent) bpy.data.objects.remove(dummy_parent)
def clearUnusedMeshes(): def clearUnusedMeshes() -> None:
meshes_to_delete = [] meshes_to_delete = []
for mesh in bpy.data.meshes: for mesh in bpy.data.meshes:
if mesh.users == 0: if mesh.users == 0:
@@ -146,7 +147,7 @@ def clearUnusedMeshes():
## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を ## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成 # それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
def makePmxBoneMap(armObj): def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]:
# Maintain backward compatibility with mmd_tools v0.4.x or older. # 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} 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 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 Method to convert an int to a base
Source: http://stackoverflow.com/questions/2267362 Source: http://stackoverflow.com/questions/2267362
@@ -198,7 +199,7 @@ def int2base(x, base, width=0):
return digits 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 On Windows relpath will raise a ValueError
when trying to calculate the relative path to a when trying to calculate the relative path to a
@@ -227,13 +228,13 @@ def saferelpath(path, start, strategy="inside"):
class ItemOp: class ItemOp:
@staticmethod @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): if 0 <= index < len(items):
return items[index] return items[index]
return None return None
@staticmethod @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) count = length - len(items)
if count > 0: if count > 0:
for i in range(count): for i in range(count):
@@ -243,7 +244,7 @@ class ItemOp:
items.remove(length) items.remove(length)
@staticmethod @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_end = len(items)
index = max(0, min(index_end, index + 1)) index = max(0, min(index_end, index + 1))
items.add() items.add()
@@ -265,7 +266,8 @@ class ItemMoveOp:
) )
@staticmethod @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: if index_max is None:
index_max = len(items) - 1 index_max = len(items) - 1
else: else:
@@ -294,7 +296,7 @@ class ItemMoveOp:
return index_new 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. """Decorator to mark a function as deprecated.
Args: Args:
deprecated_in (Optional[str]): Version in which the function was deprecated. 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. Callable: The decorated function.
""" """
def _function_wrapper(function: Callable): def _function_wrapper(function: Callable) -> Callable:
def _inner_wrapper(*args, **kwargs): def _inner_wrapper(*args: Any, **kwargs: Any) -> Any:
warn_deprecation(function.__name__, deprecated_in, details) warn_deprecation(function.__name__, deprecated_in, details)
return function(*args, **kwargs) 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. deprecated_in (Optional[str]): Version in which the function was deprecated.
details (Optional[str]): Additional details about the deprecation. details (Optional[str]): Additional details about the deprecation.
""" """
logging.warning( logger.warning(
"%s is deprecated%s%s", "%s is deprecated%s%s",
function_name, function_name,
f" since {deprecated_in}" if deprecated_in else "", 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, stack_info=True,
stacklevel=4, 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 # Define which version series this installation can update to
# For example: ["0.1"] means only look for 0.1.x updates # 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 # ["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 is_checking_for_update: bool = False
update_needed: 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"], "authors": ["Avatar Toolkit Team"],
"messages": { "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.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.", "AvatarToolkit.desc3": "please report it on our Github.",
@@ -63,6 +63,13 @@
"PoseMode.basis": "Basis", "PoseMode.basis": "Basis",
"Armature.validation.no_armature": "No armature selected", "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.not_armature": "Selected object is not an armature",
"Armature.validation.no_bones": "Armature has no bones", "Armature.validation.no_bones": "Armature has no bones",
"Armature.validation.basic_check_failed": "Basic armature validation failed", "Armature.validation.basic_check_failed": "Basic armature validation failed",
+8 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "アバターツールキット (アルファ 0.2.1)", "AvatarToolkit.label": "アバターツールキット (アルファ 0.3.0)",
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AvatarToolkit.desc3": "GitHubで報告してください。", "AvatarToolkit.desc3": "GitHubで報告してください。",
@@ -63,6 +63,13 @@
"PoseMode.basis": "基本形", "PoseMode.basis": "基本形",
"Armature.validation.no_armature": "アーマチュアが選択されていません", "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.not_armature": "選択されたオブジェクトはアーマチュアではありません",
"Armature.validation.no_bones": "アーマチュアにボーンがありません", "Armature.validation.no_bones": "アーマチュアにボーンがありません",
"Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました", "Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました",
+8 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.2.1)", "AvatarToolkit.label": "아바타 툴킷 (알파 0.3.0)",
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
"AvatarToolkit.desc3": "Github에 보고해 주세요.", "AvatarToolkit.desc3": "Github에 보고해 주세요.",
@@ -63,6 +63,13 @@
"PoseMode.basis": "기본", "PoseMode.basis": "기본",
"Armature.validation.no_armature": "선택된 아마추어 없음", "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.not_armature": "선택된 객체가 아마추어가 아님",
"Armature.validation.no_bones": "아마추어에 본이 없음", "Armature.validation.no_bones": "아마추어에 본이 없음",
"Armature.validation.basic_check_failed": "기본 아마추어 검증 실패", "Armature.validation.basic_check_failed": "기본 아마추어 검증 실패",
+49 -3
View File
@@ -89,16 +89,33 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
if active_armature: if active_armature:
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True) 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() 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: if not is_valid:
# Display non-standard bones and hierarchy issues # Display non-standard bones and hierarchy issues
if len(messages) > 1: if messages and len(messages) > 0:
# Found Bones section # Found Bones section
validation_box = info_box.box() validation_box = info_box.box()
row = validation_box.row() 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) 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'): for line in messages[0].split('\n'):
validation_box.label(text=line) validation_box.label(text=line)
@@ -127,12 +144,28 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"), 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) icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
if props.show_non_standard: 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 message in non_standard_messages:
for line in message.split('\n'): for line in message.split('\n'):
sub_row = validation_box.row() sub_row = validation_box.row()
sub_row.alert = True sub_row.alert = True
sub_row.label(text=line) sub_row.label(text=line)
else:
# 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: else:
sub_row = validation_box.row() sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_non_standard_issues")) sub_row.label(text=t("Validation.no_non_standard_issues"))
@@ -190,9 +223,14 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
row.label(text=msg.name) row.label(text=msg.name)
else: else:
# If no specific issues, show acceptable message # If no specific issues, show acceptable message
if messages and len(messages) > 0:
info_box.label(text=messages[0], icon='INFO') info_box.label(text=messages[0], icon='INFO')
if len(messages) > 1:
info_box.label(text=messages[1]) info_box.label(text=messages[1])
if len(messages) > 2:
info_box.label(text=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: elif is_valid and not is_acceptable:
row = info_box.row() row = info_box.row()
split = row.split(factor=0.6) 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') info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
elif is_valid and is_acceptable: elif is_valid and is_acceptable:
# Show acceptable standard message # Show acceptable standard message
if messages and len(messages) > 0:
info_box.label(text=messages[0], icon='INFO') 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]) info_box.label(text=messages[1])
if len(messages) > 2:
info_box.label(text=messages[2]) info_box.label(text=messages[2])
else:
info_box.label(text=t("Validation.no_messages"), icon='INFO')
# Add standardize button # Add standardize button
standardize_box = info_box.box() standardize_box = info_box.box()
@@ -252,3 +297,4 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
button_row.scale_y = 1.5 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_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT')
button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT') button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT')