Merge pull request #159 from Yusarina/mmd-tools-improvements
Mmd tools improvements
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "avatar_toolkit"
|
||||
version = "0.2.1"
|
||||
version = "0.3.0"
|
||||
name = "Avatar Toolkit"
|
||||
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
||||
maintainer = "Team NekoNeo"
|
||||
|
||||
@@ -25,12 +25,18 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
|
||||
non_standard_messages: List[str] = []
|
||||
scale_messages: List[str] = []
|
||||
|
||||
# Check if this is a PMX model
|
||||
is_pmx_model = False
|
||||
if armature and hasattr(armature, 'mmd_type') or (hasattr(armature, 'parent') and armature.parent and hasattr(armature.parent, 'mmd_type')):
|
||||
is_pmx_model = True
|
||||
logger.debug("Detected PMX model, using specialized validation")
|
||||
|
||||
if validation_mode == 'NONE':
|
||||
logger.debug("Validation mode is NONE, skipping validation")
|
||||
if detailed_messages:
|
||||
return True, [], False, [], [], []
|
||||
return True, [t("Validation.mode.none")], False, [], [], []
|
||||
else:
|
||||
return True, [], False
|
||||
return True, [t("Validation.mode.none")], False
|
||||
|
||||
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
|
||||
logger.warning("Basic armature check failed")
|
||||
@@ -125,6 +131,21 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
|
||||
non_standard_messages.append(t("Armature.validation.standardize_note.line2"))
|
||||
non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
|
||||
|
||||
# Special handling for PMX models
|
||||
if is_pmx_model:
|
||||
logger.info("PMX model detected, applying specialized validation")
|
||||
# For PMX models, we'll be more lenient with validation
|
||||
# and provide specific guidance for these models
|
||||
if not messages:
|
||||
messages = [t("Armature.validation.pmx_model_detected")]
|
||||
|
||||
# Add PMX-specific messages
|
||||
if validation_mode == 'STRICT':
|
||||
messages.append(t("Armature.validation.pmx_model_strict"))
|
||||
messages.append(t("Armature.validation.pmx_model_standardize"))
|
||||
else:
|
||||
messages.append(t("Armature.validation.pmx_model_basic"))
|
||||
|
||||
# Combine messages in correct order
|
||||
messages.extend(non_standard_messages)
|
||||
|
||||
@@ -149,6 +170,10 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
|
||||
else:
|
||||
return True, messages, True
|
||||
|
||||
# Ensure messages has at least one element
|
||||
if not messages:
|
||||
messages = [t("Armature.validation.unknown_format")]
|
||||
|
||||
logger.info(f"Armature validation complete. Valid: {is_valid}")
|
||||
if detailed_messages:
|
||||
return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages
|
||||
|
||||
@@ -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
@@ -6,9 +6,13 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import contextlib
|
||||
from typing import Generator, List, Optional, TypeVar
|
||||
from typing import Generator, List, Optional, TypeVar, Any, Set, Tuple, Dict, Union
|
||||
|
||||
import bpy
|
||||
from bpy.types import Object, Context, ID, Key, ShapeKey, FCurve, LayerCollection, Collection
|
||||
from bpy.types import AddonPreferences, Addon, WindowManager, Area, Region, Window
|
||||
|
||||
from ..logging_setup import logger
|
||||
|
||||
|
||||
class Props: # For API changes of only name changed properties
|
||||
@@ -20,7 +24,7 @@ class Props: # For API changes of only name changed properties
|
||||
|
||||
|
||||
class __EditMode:
|
||||
def __init__(self, obj):
|
||||
def __init__(self, obj: Object):
|
||||
if not isinstance(obj, bpy.types.Object):
|
||||
raise ValueError
|
||||
self.__prevMode = obj.mode
|
||||
@@ -30,10 +34,10 @@ class __EditMode:
|
||||
if obj.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> Any:
|
||||
return self.__obj.data
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
|
||||
if self.__prevMode == "EDIT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT") # update edited data
|
||||
bpy.ops.object.mode_set(mode=self.__prevMode)
|
||||
@@ -41,17 +45,18 @@ class __EditMode:
|
||||
|
||||
|
||||
class __SelectObjects:
|
||||
def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None):
|
||||
def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None):
|
||||
if not isinstance(active_object, bpy.types.Object):
|
||||
raise ValueError
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
logger.debug("Failed to set object mode")
|
||||
pass
|
||||
|
||||
contenxt = FnContext.ensure_context()
|
||||
context = FnContext.ensure_context()
|
||||
|
||||
for i in contenxt.selected_objects:
|
||||
for i in context.selected_objects:
|
||||
i.select_set(False)
|
||||
|
||||
self.__active_object = active_object
|
||||
@@ -60,23 +65,23 @@ class __SelectObjects:
|
||||
self.__hides: List[bool] = []
|
||||
for i in self.__selected_objects:
|
||||
self.__hides.append(i.hide_get())
|
||||
FnContext.select_object(contenxt, i)
|
||||
FnContext.set_active_object(contenxt, active_object)
|
||||
FnContext.select_object(context, i)
|
||||
FnContext.set_active_object(context, active_object)
|
||||
|
||||
def __enter__(self) -> bpy.types.Object:
|
||||
def __enter__(self) -> Object:
|
||||
return self.__active_object
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
|
||||
for i, j in zip(self.__selected_objects, self.__hides):
|
||||
i.hide_set(j)
|
||||
|
||||
|
||||
def setParent(obj, parent):
|
||||
def setParent(obj: Object, parent: Object) -> None:
|
||||
with select_object(parent, objects=[parent, obj]):
|
||||
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
|
||||
|
||||
|
||||
def setParentToBone(obj, parent, bone_name):
|
||||
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
||||
with select_object(parent, objects=[parent, obj]):
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
parent.data.bones.active = parent.data.bones[bone_name]
|
||||
@@ -84,7 +89,7 @@ def setParentToBone(obj, parent, bone_name):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
|
||||
def edit_object(obj):
|
||||
def edit_object(obj: Object) -> __EditMode:
|
||||
"""Set the object interaction mode to 'EDIT'
|
||||
|
||||
It is recommended to use 'edit_object' with 'with' statement like the following code.
|
||||
@@ -95,7 +100,7 @@ def edit_object(obj):
|
||||
return __EditMode(obj)
|
||||
|
||||
|
||||
def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None):
|
||||
def select_object(obj: Object, objects: Optional[List[Object]] = None) -> __SelectObjects:
|
||||
"""Select objects.
|
||||
|
||||
It is recommended to use 'select_object' with 'with' statement like the following code.
|
||||
@@ -108,20 +113,23 @@ def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object
|
||||
return __SelectObjects(obj, objects)
|
||||
|
||||
|
||||
def duplicateObject(obj, total_len):
|
||||
def duplicateObject(obj: Object, total_len: int) -> List[Object]:
|
||||
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
|
||||
|
||||
|
||||
def createObject(name="Object", object_data=None, target_scene=None):
|
||||
def createObject(name: str = "Object", object_data: Optional[ID] = None, target_scene: Optional[bpy.types.Scene] = None) -> Object:
|
||||
context = FnContext.ensure_context(target_scene)
|
||||
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data))
|
||||
|
||||
|
||||
def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
|
||||
def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object:
|
||||
import bmesh
|
||||
|
||||
if target_object is None:
|
||||
target_object = createObject(name="Sphere")
|
||||
logger.debug(f"Created new sphere object: {target_object.name}")
|
||||
else:
|
||||
logger.debug(f"Using existing object for sphere: {target_object.name}")
|
||||
|
||||
mesh = target_object.data
|
||||
bm = bmesh.new()
|
||||
@@ -138,12 +146,15 @@ def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
|
||||
return target_object
|
||||
|
||||
|
||||
def makeBox(size=(1, 1, 1), target_object=None):
|
||||
def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object:
|
||||
import bmesh
|
||||
from mathutils import Matrix
|
||||
|
||||
if target_object is None:
|
||||
target_object = createObject(name="Box")
|
||||
logger.debug(f"Created new box object: {target_object.name}")
|
||||
else:
|
||||
logger.debug(f"Using existing object for box: {target_object.name}")
|
||||
|
||||
mesh = target_object.data
|
||||
bm = bmesh.new()
|
||||
@@ -159,13 +170,16 @@ def makeBox(size=(1, 1, 1), target_object=None):
|
||||
return target_object
|
||||
|
||||
|
||||
def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None):
|
||||
def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object:
|
||||
import math
|
||||
|
||||
import bmesh
|
||||
|
||||
if target_object is None:
|
||||
target_object = createObject(name="Capsule")
|
||||
logger.debug(f"Created new capsule object: {target_object.name}")
|
||||
else:
|
||||
logger.debug(f"Using existing object for capsule: {target_object.name}")
|
||||
|
||||
height = max(height, 1e-3)
|
||||
|
||||
mesh = target_object.data
|
||||
@@ -224,10 +238,10 @@ def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=N
|
||||
|
||||
|
||||
class TransformConstraintOp:
|
||||
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
|
||||
__MIN_MAX_MAP: Dict[Union[str, Tuple[str, str]], Union[str, Tuple[str, ...]]] = {"ROTATION": "_rot", "SCALE": "_scale"}
|
||||
|
||||
@staticmethod
|
||||
def create(constraints, name, map_type):
|
||||
def create(constraints: bpy.types.ObjectConstraints, name: str, map_type: str) -> bpy.types.TransformConstraint:
|
||||
c = constraints.get(name, None)
|
||||
if c and c.type != "TRANSFORM":
|
||||
constraints.remove(c)
|
||||
@@ -245,7 +259,7 @@ class TransformConstraintOp:
|
||||
return c
|
||||
|
||||
@classmethod
|
||||
def min_max_attributes(cls, map_type, name_id=""):
|
||||
def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]:
|
||||
key = (map_type, name_id)
|
||||
ret = cls.__MIN_MAX_MAP.get(key, None)
|
||||
if ret is None:
|
||||
@@ -255,7 +269,7 @@ class TransformConstraintOp:
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def update_min_max(cls, constraint, value, influence=1):
|
||||
def update_min_max(cls, constraint: bpy.types.TransformConstraint, value: float, influence: Optional[float] = 1) -> None:
|
||||
c = constraint
|
||||
if not c or c.type != "TRANSFORM":
|
||||
return
|
||||
@@ -279,14 +293,14 @@ class FnObject:
|
||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey):
|
||||
def mesh_remove_shape_key(mesh_object: Object, shape_key: ShapeKey) -> None:
|
||||
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
||||
|
||||
key: bpy.types.Key = shape_key.id_data
|
||||
key: Key = shape_key.id_data
|
||||
assert key == mesh_object.data.shape_keys
|
||||
|
||||
if mesh_object.animation_data is not None:
|
||||
fc_curve: bpy.types.FCurve
|
||||
fc_curve: FCurve
|
||||
for fc_curve in mesh_object.animation_data.drivers:
|
||||
if not fc_curve.data_path.startswith(shape_key.path_from_id()):
|
||||
continue
|
||||
@@ -310,35 +324,35 @@ class FnContext:
|
||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
|
||||
def ensure_context(context: Optional[Context] = None) -> Context:
|
||||
return context or bpy.context
|
||||
|
||||
@staticmethod
|
||||
def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
|
||||
def get_active_object(context: Context) -> Optional[Object]:
|
||||
return context.active_object
|
||||
|
||||
@staticmethod
|
||||
def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
def set_active_object(context: Context, obj: Object) -> Object:
|
||||
context.view_layer.objects.active = obj
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
def set_active_and_select_single_object(context: Context, obj: Object) -> Object:
|
||||
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
|
||||
|
||||
@staticmethod
|
||||
def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects:
|
||||
def get_scene_objects(context: Context) -> bpy.types.SceneObjects:
|
||||
return context.scene.objects
|
||||
|
||||
@staticmethod
|
||||
def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
def ensure_selectable(context: Context, obj: Object) -> Object:
|
||||
obj.hide_viewport = False
|
||||
obj.hide_select = False
|
||||
obj.hide_set(False)
|
||||
|
||||
if obj not in context.selectable_objects:
|
||||
|
||||
def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool:
|
||||
def __layer_check(layer_collection: LayerCollection) -> bool:
|
||||
for lc in layer_collection.children:
|
||||
if __layer_check(lc):
|
||||
lc.hide_viewport = False
|
||||
@@ -360,44 +374,44 @@ class FnContext:
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
def select_object(context: Context, obj: Object) -> Object:
|
||||
FnContext.ensure_selectable(context, obj).select_set(True)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]:
|
||||
def select_objects(context: Context, *objects: Object) -> List[Object]:
|
||||
return [FnContext.select_object(context, obj) for obj in objects]
|
||||
|
||||
@staticmethod
|
||||
def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
def select_single_object(context: Context, obj: Object) -> Object:
|
||||
for i in context.selected_objects:
|
||||
if i != obj:
|
||||
i.select_set(False)
|
||||
return FnContext.select_object(context, obj)
|
||||
|
||||
@staticmethod
|
||||
def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
def link_object(context: Context, obj: Object) -> Object:
|
||||
context.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object:
|
||||
def new_and_link_object(context: Context, name: str, object_data: Optional[ID]) -> Object:
|
||||
return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
|
||||
|
||||
@staticmethod
|
||||
def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]:
|
||||
def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[Object]:
|
||||
"""
|
||||
Duplicate object.
|
||||
|
||||
This function duplicates the given object and returns a list of duplicated objects.
|
||||
|
||||
Args:
|
||||
context (bpy.types.Context): The context in which the duplication is performed.
|
||||
object_to_duplicate (bpy.types.Object): The object to be duplicated.
|
||||
context (Context): The context in which the duplication is performed.
|
||||
object_to_duplicate (Object): The object to be duplicated.
|
||||
target_count (int): The desired count of duplicated objects.
|
||||
|
||||
Returns:
|
||||
List[bpy.types.Object]: A list of duplicated objects.
|
||||
List[Object]: A list of duplicated objects.
|
||||
|
||||
Raises:
|
||||
AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated.
|
||||
@@ -421,27 +435,28 @@ class FnContext:
|
||||
last_selected_objects[i].select_set(True)
|
||||
last_selected_objects = context.selected_objects
|
||||
assert len(result_objects) == target_count
|
||||
logger.debug(f"Duplicated object {object_to_duplicate.name} to create {target_count} objects")
|
||||
return result_objects
|
||||
|
||||
@staticmethod
|
||||
def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]:
|
||||
def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[LayerCollection]:
|
||||
"""
|
||||
Finds the layer collection that contains the given target_object in the user's collections.
|
||||
|
||||
Args:
|
||||
context (bpy.types.Context): The Blender context.
|
||||
target_object (bpy.types.Object): The target object to find the layer collection for.
|
||||
context (Context): The Blender context.
|
||||
target_object (Object): The target object to find the layer collection for.
|
||||
|
||||
Returns:
|
||||
Optional[bpy.types.LayerCollection]: The layer collection that contains the target_object, or None if not found.
|
||||
Optional[LayerCollection]: The layer collection that contains the target_object, or None if not found.
|
||||
"""
|
||||
scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection
|
||||
scene_layer_collection: LayerCollection = context.view_layer.layer_collection
|
||||
|
||||
def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]:
|
||||
def find_layer_collection_by_name(layer_collection: LayerCollection, name: str) -> Optional[LayerCollection]:
|
||||
if layer_collection.name == name:
|
||||
return layer_collection
|
||||
|
||||
child_layer_collection: bpy.types.LayerCollection
|
||||
child_layer_collection: LayerCollection
|
||||
for child_layer_collection in layer_collection.children:
|
||||
found = find_layer_collection_by_name(child_layer_collection, name)
|
||||
if found is not None:
|
||||
@@ -449,7 +464,7 @@ class FnContext:
|
||||
|
||||
return None
|
||||
|
||||
user_collection: bpy.types.Collection
|
||||
user_collection: Collection
|
||||
for user_collection in target_object.users_collection:
|
||||
found = find_layer_collection_by_name(scene_layer_collection, user_collection.name)
|
||||
if found is not None:
|
||||
@@ -459,7 +474,7 @@ class FnContext:
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]:
|
||||
def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]:
|
||||
"""
|
||||
Context manager to temporarily override the active_layer_collection that contains the target object.
|
||||
|
||||
@@ -467,11 +482,11 @@ class FnContext:
|
||||
It ensures that the original active_layer_collection is restored after the context is exited.
|
||||
|
||||
Args:
|
||||
context (bpy.types.Context): The context in which the active_layer_collection will be overridden.
|
||||
target_object (bpy.types.Object): The target object whose layer collection will be set as the active_layer_collection.
|
||||
context (Context): The context in which the active_layer_collection will be overridden.
|
||||
target_object (Object): The target object whose layer collection will be set as the active_layer_collection.
|
||||
|
||||
Yields:
|
||||
bpy.types.Context: The modified context with the active_layer_collection overridden.
|
||||
Context: The modified context with the active_layer_collection overridden.
|
||||
|
||||
Example:
|
||||
with FnContext.temp_override_active_layer_collection(context, target_object):
|
||||
@@ -492,24 +507,24 @@ class FnContext:
|
||||
context.view_layer.active_layer_collection = original_layer_collection
|
||||
|
||||
@staticmethod
|
||||
def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]:
|
||||
addon: bpy.types.Addon = context.preferences.addons.get(__package__, None)
|
||||
def __get_addon_preferences(context: Context) -> Optional[AddonPreferences]:
|
||||
addon: Addon = context.preferences.addons.get(__package__, None)
|
||||
return addon.preferences if addon else None
|
||||
|
||||
@staticmethod
|
||||
def get_addon_preferences_attribute(context: bpy.types.Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
|
||||
def get_addon_preferences_attribute(context: Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE:
|
||||
return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value)
|
||||
|
||||
@staticmethod
|
||||
def temp_override_objects(
|
||||
context: bpy.types.Context,
|
||||
window: Optional[bpy.types.Window] = None,
|
||||
area: Optional[bpy.types.Area] = None,
|
||||
region: Optional[bpy.types.Region] = None,
|
||||
active_object: Optional[bpy.types.Object] = None,
|
||||
selected_objects: Optional[List[bpy.types.Object]] = None,
|
||||
**keywords,
|
||||
) -> Generator[bpy.types.Context, None, None]:
|
||||
context: Context,
|
||||
window: Optional[Window] = None,
|
||||
area: Optional[Area] = None,
|
||||
region: Optional[Region] = None,
|
||||
active_object: Optional[Object] = None,
|
||||
selected_objects: Optional[List[Object]] = None,
|
||||
**keywords: Any,
|
||||
) -> Generator[Context, None, None]:
|
||||
if active_object is not None:
|
||||
keywords["active_object"] = active_object
|
||||
keywords["object"] = active_object
|
||||
|
||||
+168
-77
@@ -6,29 +6,32 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, Set
|
||||
from typing import TYPE_CHECKING, Iterable, Optional, Set, List, Dict, Tuple, Any, Union, cast
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
from bpy.types import Object, EditBone, PoseBone, Constraint, Armature, BoneCollection
|
||||
|
||||
from .. import bpyutils
|
||||
from ..bpyutils import TransformConstraintOp
|
||||
from ..utils import ItemOp
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..properties.root import MMDRoot, MMDDisplayItemFrame
|
||||
from ..properties.pose_bone import MMDBone
|
||||
|
||||
|
||||
def remove_constraint(constraints, name):
|
||||
def remove_constraint(constraints: Any, name: str) -> bool:
|
||||
"""Remove a constraint by name if it exists"""
|
||||
c = constraints.get(name, None)
|
||||
if c:
|
||||
constraints.remove(c)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def remove_edit_bones(edit_bones, bone_names):
|
||||
def remove_edit_bones(edit_bones: bpy.types.ArmatureEditBones, bone_names: List[str]) -> None:
|
||||
"""Remove edit bones by name"""
|
||||
for name in bone_names:
|
||||
b = edit_bones.get(name, None)
|
||||
if b:
|
||||
@@ -45,33 +48,39 @@ SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NA
|
||||
|
||||
|
||||
class FnBone:
|
||||
AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
|
||||
AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指")
|
||||
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
|
||||
AUTO_LOCAL_AXIS_ARMS: Tuple[str, ...] = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
|
||||
AUTO_LOCAL_AXIS_FINGERS: Tuple[str, ...] = ("親指", "人指", "中指", "薬指", "小指")
|
||||
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: Tuple[str, ...] = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
raise NotImplementedError("This class cannot be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]:
|
||||
def find_pose_bone_by_bone_id(armature_object: Object, bone_id: int) -> Optional[PoseBone]:
|
||||
"""Find a pose bone by its bone ID"""
|
||||
for bone in armature_object.pose.bones:
|
||||
if bone.mmd_bone.bone_id != bone_id:
|
||||
continue
|
||||
return bone
|
||||
logger.debug(f"Bone with ID {bone_id} not found in armature {armature_object.name}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def __new_bone_id(armature_object: bpy.types.Object) -> int:
|
||||
def __new_bone_id(armature_object: Object) -> int:
|
||||
"""Generate a new unique bone ID"""
|
||||
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
|
||||
|
||||
@staticmethod
|
||||
def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int:
|
||||
def get_or_assign_bone_id(pose_bone: PoseBone) -> int:
|
||||
"""Get the bone ID or assign a new one if not set"""
|
||||
if pose_bone.mmd_bone.bone_id < 0:
|
||||
pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data)
|
||||
logger.debug(f"Assigned new bone ID {pose_bone.mmd_bone.bone_id} to bone {pose_bone.name}")
|
||||
return pose_bone.mmd_bone.bone_id
|
||||
|
||||
@staticmethod
|
||||
def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]:
|
||||
def __get_selected_pose_bones(armature_object: Object) -> Iterable[PoseBone]:
|
||||
"""Get selected pose bones from the armature"""
|
||||
if armature_object.mode == "EDIT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
|
||||
bpy.ops.object.mode_set(mode="EDIT") # back to edit mode
|
||||
@@ -80,9 +89,11 @@ class FnBone:
|
||||
return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone)
|
||||
|
||||
@staticmethod
|
||||
def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True):
|
||||
def load_bone_fixed_axis(armature_object: Object, enable: bool = True) -> None:
|
||||
"""Load fixed axis settings for selected bones"""
|
||||
logger.debug(f"Loading bone fixed axis (enable={enable}) for {armature_object.name}")
|
||||
for b in FnBone.__get_selected_pose_bones(armature_object):
|
||||
mmd_bone: MMDBone = b.mmd_bone
|
||||
mmd_bone = b.mmd_bone
|
||||
mmd_bone.enabled_fixed_axis = enable
|
||||
lock_rotation = b.lock_rotation[:]
|
||||
if enable:
|
||||
@@ -97,53 +108,66 @@ class FnBone:
|
||||
b.lock_location = b.lock_scale = (False, False, False)
|
||||
|
||||
@staticmethod
|
||||
def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object:
|
||||
armature: bpy.types.Armature = armature_object.data
|
||||
def setup_special_bone_collections(armature_object: Object) -> Object:
|
||||
"""Set up special bone collections for MMD"""
|
||||
armature = cast(Armature, armature_object.data)
|
||||
bone_collections = armature.collections
|
||||
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
|
||||
if bone_collection_name in bone_collections:
|
||||
continue
|
||||
bone_collection = bone_collections.new(bone_collection_name)
|
||||
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
|
||||
logger.debug(f"Created special bone collection: {bone_collection_name}")
|
||||
return armature_object
|
||||
|
||||
@staticmethod
|
||||
def __is_mmd_tools_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||
def __is_mmd_tools_bone_collection(bone_collection: BoneCollection) -> bool:
|
||||
"""Check if a bone collection is an MMD Tools collection"""
|
||||
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
|
||||
|
||||
@staticmethod
|
||||
def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||
def __is_special_bone_collection(bone_collection: BoneCollection) -> bool:
|
||||
"""Check if a bone collection is a special MMD collection"""
|
||||
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
|
||||
|
||||
@staticmethod
|
||||
def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool):
|
||||
def __set_bone_collection_to_special(bone_collection: BoneCollection, is_visible: bool) -> None:
|
||||
"""Mark a bone collection as special"""
|
||||
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
|
||||
bone_collection.is_visible = is_visible
|
||||
|
||||
@staticmethod
|
||||
def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||
def __is_normal_bone_collection(bone_collection: BoneCollection) -> bool:
|
||||
"""Check if a bone collection is a normal MMD collection"""
|
||||
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
|
||||
|
||||
@staticmethod
|
||||
def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection):
|
||||
def __set_bone_collection_to_normal(bone_collection: BoneCollection) -> None:
|
||||
"""Mark a bone collection as normal"""
|
||||
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
|
||||
|
||||
@staticmethod
|
||||
def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone:
|
||||
def __set_edit_bone_to_special(edit_bone: EditBone, bone_collection_name: str) -> EditBone:
|
||||
"""Set an edit bone to a special collection"""
|
||||
edit_bone.id_data.collections[bone_collection_name].assign(edit_bone)
|
||||
edit_bone.use_deform = False
|
||||
return edit_bone
|
||||
|
||||
@staticmethod
|
||||
def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||
def set_edit_bone_to_dummy(edit_bone: EditBone) -> EditBone:
|
||||
"""Set an edit bone as a dummy bone"""
|
||||
logger.debug(f"Setting bone {edit_bone.name} as dummy bone")
|
||||
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
|
||||
|
||||
@staticmethod
|
||||
def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||
def set_edit_bone_to_shadow(edit_bone: EditBone) -> EditBone:
|
||||
"""Set an edit bone as a shadow bone"""
|
||||
logger.debug(f"Setting bone {edit_bone.name} as shadow bone")
|
||||
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
|
||||
|
||||
@staticmethod
|
||||
def __unassign_mmd_tools_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||
def __unassign_mmd_tools_bone_collections(edit_bone: EditBone) -> EditBone:
|
||||
"""Unassign an edit bone from all MMD Tools collections"""
|
||||
for bone_collection in edit_bone.collections:
|
||||
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
|
||||
continue
|
||||
@@ -151,18 +175,24 @@ class FnBone:
|
||||
return edit_bone
|
||||
|
||||
@staticmethod
|
||||
def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object):
|
||||
armature: bpy.types.Armature = armature_object.data
|
||||
def sync_bone_collections_from_display_item_frames(armature_object: Object) -> None:
|
||||
"""Synchronize bone collections from display item frames"""
|
||||
logger.info(f"Syncing bone collections from display item frames for {armature_object.name}")
|
||||
armature = cast(Armature, armature_object.data)
|
||||
bone_collections = armature.collections
|
||||
|
||||
from .model import FnModel
|
||||
|
||||
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
|
||||
mmd_root: MMDRoot = root_object.mmd_root
|
||||
root_object = FnModel.find_root_object(armature_object)
|
||||
if not root_object:
|
||||
logger.error(f"No root object found for armature {armature_object.name}")
|
||||
return
|
||||
|
||||
mmd_root = root_object.mmd_root
|
||||
|
||||
bones = armature.bones
|
||||
used_groups = set()
|
||||
unassigned_bone_names = {b.name for b in bones}
|
||||
used_groups: Set[str] = set()
|
||||
unassigned_bone_names: Set[str] = {b.name for b in bones}
|
||||
|
||||
for frame in mmd_root.display_item_frames:
|
||||
for item in frame.data:
|
||||
@@ -174,6 +204,7 @@ class FnBone:
|
||||
if bone_collection is None:
|
||||
bone_collection = bone_collections.new(name=group_name)
|
||||
FnBone.__set_bone_collection_to_normal(bone_collection)
|
||||
logger.debug(f"Created new bone collection: {group_name}")
|
||||
bone_collection.assign(bones[item.name])
|
||||
|
||||
for name in unassigned_bone_names:
|
||||
@@ -192,32 +223,40 @@ class FnBone:
|
||||
continue
|
||||
if not FnBone.__is_normal_bone_collection(bone_collection):
|
||||
continue
|
||||
logger.debug(f"Removing unused bone collection: {bone_collection.name}")
|
||||
bone_collections.remove(bone_collection)
|
||||
|
||||
@staticmethod
|
||||
def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object):
|
||||
armature: bpy.types.Armature = armature_object.data
|
||||
bone_collections: bpy.types.BoneCollections = armature.collections
|
||||
def sync_display_item_frames_from_bone_collections(armature_object: Object) -> None:
|
||||
"""Synchronize display item frames from bone collections"""
|
||||
logger.info(f"Syncing display item frames from bone collections for {armature_object.name}")
|
||||
armature = cast(Armature, armature_object.data)
|
||||
bone_collections = armature.collections
|
||||
|
||||
from .model import FnModel
|
||||
|
||||
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
|
||||
mmd_root: MMDRoot = root_object.mmd_root
|
||||
root_object = FnModel.find_root_object(armature_object)
|
||||
if not root_object:
|
||||
logger.error(f"No root object found for armature {armature_object.name}")
|
||||
return
|
||||
|
||||
mmd_root = root_object.mmd_root
|
||||
display_item_frames = mmd_root.display_item_frames
|
||||
|
||||
used_frame_index: Set[int] = set()
|
||||
|
||||
bone_collection: bpy.types.BoneCollection
|
||||
bone_collection: BoneCollection
|
||||
for bone_collection in bone_collections:
|
||||
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
|
||||
continue
|
||||
|
||||
bone_collection_name = bone_collection.name
|
||||
display_item_frame: Optional[MMDDisplayItemFrame] = display_item_frames.get(bone_collection_name)
|
||||
display_item_frame = display_item_frames.get(bone_collection_name)
|
||||
if display_item_frame is None:
|
||||
display_item_frame = display_item_frames.add()
|
||||
display_item_frame.name = bone_collection_name
|
||||
display_item_frame.name_e = bone_collection_name
|
||||
logger.debug(f"Created new display item frame: {bone_collection_name}")
|
||||
used_frame_index.add(display_item_frames.find(bone_collection_name))
|
||||
|
||||
ItemOp.resize(display_item_frame.data, len(bone_collection.bones))
|
||||
@@ -232,23 +271,27 @@ class FnBone:
|
||||
if display_item_frame.is_special:
|
||||
if display_item_frame.name != "表情":
|
||||
display_item_frame.data.clear()
|
||||
logger.debug(f"Cleared special display item frame: {display_item_frame.name}")
|
||||
else:
|
||||
logger.debug(f"Removing unused display item frame: {display_item_frames[i].name}")
|
||||
display_item_frames.remove(i)
|
||||
mmd_root.active_display_item_frame = 0
|
||||
|
||||
@staticmethod
|
||||
def apply_bone_fixed_axis(armature_object: bpy.types.Object):
|
||||
bone_map = {}
|
||||
def apply_bone_fixed_axis(armature_object: Object) -> None:
|
||||
"""Apply fixed axis to bones"""
|
||||
logger.info(f"Applying bone fixed axis for {armature_object.name}")
|
||||
bone_map: Dict[str, Tuple[Vector, bool, bool]] = {}
|
||||
for b in armature_object.pose.bones:
|
||||
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
|
||||
continue
|
||||
mmd_bone: MMDBone = b.mmd_bone
|
||||
mmd_bone = b.mmd_bone
|
||||
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
|
||||
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
|
||||
|
||||
force_align = True
|
||||
with bpyutils.edit_object(armature_object) as data:
|
||||
bone: bpy.types.EditBone
|
||||
bone: EditBone
|
||||
for bone in data.edit_bones:
|
||||
if bone.name not in bone_map:
|
||||
bone.select = False
|
||||
@@ -279,6 +322,7 @@ class FnBone:
|
||||
else:
|
||||
bone_map[bone.name] = (True, True, True)
|
||||
bone.select = True
|
||||
logger.debug(f"Applied fixed axis to bone: {bone.name}")
|
||||
|
||||
for bone_name, locks in bone_map.items():
|
||||
b = armature_object.pose.bones[bone_name]
|
||||
@@ -286,9 +330,11 @@ class FnBone:
|
||||
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
|
||||
|
||||
@staticmethod
|
||||
def load_bone_local_axes(armature_object: bpy.types.Object, enable=True):
|
||||
def load_bone_local_axes(armature_object: Object, enable: bool = True) -> None:
|
||||
"""Load local axes for selected bones"""
|
||||
logger.debug(f"Loading bone local axes (enable={enable}) for {armature_object.name}")
|
||||
for b in FnBone.__get_selected_pose_bones(armature_object):
|
||||
mmd_bone: MMDBone = b.mmd_bone
|
||||
mmd_bone = b.mmd_bone
|
||||
mmd_bone.enabled_local_axes = enable
|
||||
if enable:
|
||||
axes = b.bone.matrix_local.to_3x3().transposed()
|
||||
@@ -296,16 +342,18 @@ class FnBone:
|
||||
mmd_bone.local_axis_z = axes[2].xzy
|
||||
|
||||
@staticmethod
|
||||
def apply_bone_local_axes(armature_object: bpy.types.Object):
|
||||
bone_map = {}
|
||||
def apply_bone_local_axes(armature_object: Object) -> None:
|
||||
"""Apply local axes to bones"""
|
||||
logger.info(f"Applying bone local axes for {armature_object.name}")
|
||||
bone_map: Dict[str, Tuple[Vector, Vector]] = {}
|
||||
for b in armature_object.pose.bones:
|
||||
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
|
||||
continue
|
||||
mmd_bone: MMDBone = b.mmd_bone
|
||||
mmd_bone = b.mmd_bone
|
||||
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
|
||||
|
||||
with bpyutils.edit_object(armature_object) as data:
|
||||
bone: bpy.types.EditBone
|
||||
bone: EditBone
|
||||
for bone in data.edit_bones:
|
||||
if bone.name not in bone_map:
|
||||
bone.select = False
|
||||
@@ -313,15 +361,18 @@ class FnBone:
|
||||
local_axis_x, local_axis_z = bone_map[bone.name]
|
||||
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
|
||||
bone.select = True
|
||||
logger.debug(f"Applied local axes to bone: {bone.name}")
|
||||
|
||||
@staticmethod
|
||||
def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z):
|
||||
def update_bone_roll(edit_bone: EditBone, mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> None:
|
||||
"""Update bone roll based on local axes"""
|
||||
axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z)
|
||||
idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
|
||||
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
|
||||
|
||||
@staticmethod
|
||||
def get_axes(mmd_local_axis_x, mmd_local_axis_z):
|
||||
def get_axes(mmd_local_axis_x: Vector, mmd_local_axis_z: Vector) -> Tuple[Vector, Vector, Vector]:
|
||||
"""Get axes from local axis vectors"""
|
||||
x_axis = Vector(mmd_local_axis_x).normalized().xzy
|
||||
z_axis = Vector(mmd_local_axis_z).normalized().xzy
|
||||
y_axis = z_axis.cross(x_axis).normalized()
|
||||
@@ -329,21 +380,25 @@ class FnBone:
|
||||
return (x_axis, y_axis, z_axis)
|
||||
|
||||
@staticmethod
|
||||
def apply_auto_bone_roll(armature):
|
||||
bone_names = []
|
||||
def apply_auto_bone_roll(armature: Object) -> None:
|
||||
"""Apply automatic bone roll to appropriate bones"""
|
||||
logger.info(f"Applying auto bone roll for {armature.name}")
|
||||
bone_names: List[str] = []
|
||||
for b in armature.pose.bones:
|
||||
if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j):
|
||||
bone_names.append(b.name)
|
||||
with bpyutils.edit_object(armature) as data:
|
||||
bone: bpy.types.EditBone
|
||||
bone: EditBone
|
||||
for bone in data.edit_bones:
|
||||
if bone.name not in bone_names:
|
||||
continue
|
||||
FnBone.update_auto_bone_roll(bone)
|
||||
bone.select = True
|
||||
logger.debug(f"Applied auto bone roll to bone: {bone.name}")
|
||||
|
||||
@staticmethod
|
||||
def update_auto_bone_roll(edit_bone):
|
||||
def update_auto_bone_roll(edit_bone: EditBone) -> None:
|
||||
"""Update bone roll automatically"""
|
||||
# make a triangle face (p1,p2,p3)
|
||||
p1 = edit_bone.head.copy()
|
||||
p2 = edit_bone.tail.copy()
|
||||
@@ -364,7 +419,8 @@ class FnBone:
|
||||
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
|
||||
|
||||
@staticmethod
|
||||
def has_auto_local_axis(name_j):
|
||||
def has_auto_local_axis(name_j: str) -> bool:
|
||||
"""Check if a bone should have automatic local axis"""
|
||||
if name_j:
|
||||
if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
|
||||
return True
|
||||
@@ -374,9 +430,11 @@ class FnBone:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def clean_additional_transformation(armature_object: bpy.types.Object):
|
||||
def clean_additional_transformation(armature_object: Object) -> None:
|
||||
"""Clean additional transformation constraints and bones"""
|
||||
logger.info(f"Cleaning additional transformations for {armature_object.name}")
|
||||
# clean constraints
|
||||
p_bone: bpy.types.PoseBone
|
||||
p_bone: PoseBone
|
||||
for p_bone in armature_object.pose.bones:
|
||||
p_bone.mmd_bone.is_additional_transform_dirty = True
|
||||
constraints = p_bone.constraints
|
||||
@@ -392,17 +450,21 @@ class FnBone:
|
||||
"ADDITIONAL_TRANSFORM_INVERT",
|
||||
}
|
||||
|
||||
def __is_at_shadow_bone(b):
|
||||
def __is_at_shadow_bone(b: PoseBone) -> bool:
|
||||
return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types
|
||||
|
||||
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
|
||||
if len(shadow_bone_names) > 0:
|
||||
logger.debug(f"Removing {len(shadow_bone_names)} shadow bones")
|
||||
with bpyutils.edit_object(armature_object) as data:
|
||||
remove_edit_bones(data.edit_bones, shadow_bone_names)
|
||||
|
||||
@staticmethod
|
||||
def apply_additional_transformation(armature_object: bpy.types.Object):
|
||||
def __is_dirty_bone(b):
|
||||
def apply_additional_transformation(armature_object: Object) -> None:
|
||||
"""Apply additional transformation to bones"""
|
||||
logger.info(f"Applying additional transformations for {armature_object.name}")
|
||||
|
||||
def __is_dirty_bone(b: PoseBone) -> bool:
|
||||
if b.is_mmd_shadow_bone:
|
||||
return False
|
||||
mmd_bone = b.mmd_bone
|
||||
@@ -411,9 +473,10 @@ class FnBone:
|
||||
return mmd_bone.is_additional_transform_dirty
|
||||
|
||||
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
|
||||
logger.debug(f"Found {len(dirty_bones)} dirty bones to process")
|
||||
|
||||
# setup constraints
|
||||
shadow_bone_pool = []
|
||||
shadow_bone_pool: List[Union[_AT_ShadowBoneRemove, _AT_ShadowBoneCreate]] = []
|
||||
for p_bone in dirty_bones:
|
||||
sb = FnBone.__setup_constraints(p_bone)
|
||||
if sb:
|
||||
@@ -434,7 +497,8 @@ class FnBone:
|
||||
p_bone.mmd_bone.is_additional_transform_dirty = False
|
||||
|
||||
@staticmethod
|
||||
def __setup_constraints(p_bone):
|
||||
def __setup_constraints(p_bone: PoseBone) -> Optional[Union['_AT_ShadowBoneRemove', '_AT_ShadowBoneCreate']]:
|
||||
"""Set up constraints for additional transformation"""
|
||||
bone_name = p_bone.name
|
||||
mmd_bone = p_bone.mmd_bone
|
||||
influence = mmd_bone.additional_transform_influence
|
||||
@@ -447,12 +511,14 @@ class FnBone:
|
||||
rot = remove_constraint(constraints, "mmd_additional_rotation")
|
||||
loc = remove_constraint(constraints, "mmd_additional_location")
|
||||
if rot or loc:
|
||||
logger.debug(f"Removing additional transform constraints for bone: {bone_name}")
|
||||
return _AT_ShadowBoneRemove(bone_name)
|
||||
return None
|
||||
|
||||
logger.debug(f"Setting up additional transform for bone: {bone_name} targeting {target_bone}")
|
||||
shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone)
|
||||
|
||||
def __config(name, mute, map_type, value):
|
||||
def __config(name: str, mute: bool, map_type: str, value: float) -> None:
|
||||
if mute:
|
||||
remove_constraint(constraints, name)
|
||||
return
|
||||
@@ -467,62 +533,81 @@ class FnBone:
|
||||
return shadow_bone
|
||||
|
||||
@staticmethod
|
||||
def update_additional_transform_influence(pose_bone: bpy.types.PoseBone):
|
||||
def update_additional_transform_influence(pose_bone: PoseBone) -> None:
|
||||
"""Update the influence of additional transform constraints"""
|
||||
influence = pose_bone.mmd_bone.additional_transform_influence
|
||||
constraints = pose_bone.constraints
|
||||
c = constraints.get("mmd_additional_rotation", None)
|
||||
TransformConstraintOp.update_min_max(c, math.pi, influence)
|
||||
c = constraints.get("mmd_additional_location", None)
|
||||
TransformConstraintOp.update_min_max(c, 100, influence)
|
||||
logger.debug(f"Updated additional transform influence for bone: {pose_bone.name} to {influence}")
|
||||
|
||||
|
||||
class MigrationFnBone:
|
||||
"""Migration Functions for old MMD models broken by bugs or issues"""
|
||||
|
||||
@staticmethod
|
||||
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object):
|
||||
pose_bone: bpy.types.PoseBone
|
||||
def fix_mmd_ik_limit_override(armature_object: Object) -> None:
|
||||
"""Fix IK limit override constraints in old MMD models"""
|
||||
logger.info(f"Fixing MMD IK limit overrides for {armature_object.name}")
|
||||
pose_bone: PoseBone
|
||||
for pose_bone in armature_object.pose.bones:
|
||||
constraint: bpy.types.Constraint
|
||||
constraint: Constraint
|
||||
for constraint in pose_bone.constraints:
|
||||
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
|
||||
constraint.owner_space = "LOCAL"
|
||||
logger.debug(f"Fixed IK limit override for bone: {pose_bone.name}")
|
||||
|
||||
|
||||
class _AT_ShadowBoneRemove:
|
||||
def __init__(self, bone_name):
|
||||
"""Handler for removing shadow bones"""
|
||||
|
||||
def __init__(self, bone_name: str) -> None:
|
||||
"""Initialize with bone name"""
|
||||
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
|
||||
|
||||
def update_edit_bones(self, edit_bones):
|
||||
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
|
||||
"""Update edit bones by removing shadow bones"""
|
||||
remove_edit_bones(edit_bones, self.__shadow_bone_names)
|
||||
logger.debug(f"Removed shadow bones: {self.__shadow_bone_names}")
|
||||
|
||||
def update_pose_bones(self, pose_bones):
|
||||
def update_pose_bones(self, pose_bones: Any) -> None:
|
||||
"""Update pose bones (no-op for removal)"""
|
||||
pass
|
||||
|
||||
|
||||
class _AT_ShadowBoneCreate:
|
||||
def __init__(self, bone_name, target_bone_name):
|
||||
"""Handler for creating shadow bones"""
|
||||
|
||||
def __init__(self, bone_name: str, target_bone_name: str) -> None:
|
||||
"""Initialize with bone names"""
|
||||
self.__dummy_bone_name = "_dummy_" + bone_name
|
||||
self.__shadow_bone_name = "_shadow_" + bone_name
|
||||
self.__bone_name = bone_name
|
||||
self.__target_bone_name = target_bone_name
|
||||
self.__constraint_pool = []
|
||||
self.__constraint_pool: List[Constraint] = []
|
||||
|
||||
def __is_well_aligned(self, bone0, bone1):
|
||||
def __is_well_aligned(self, bone0: EditBone, bone1: EditBone) -> bool:
|
||||
"""Check if two bones are well aligned"""
|
||||
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
|
||||
|
||||
def __update_constraints(self, use_shadow=True):
|
||||
def __update_constraints(self, use_shadow: bool = True) -> None:
|
||||
"""Update constraints to use shadow or target bone"""
|
||||
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
|
||||
for c in self.__constraint_pool:
|
||||
c.subtarget = subtarget
|
||||
|
||||
def add_constraint(self, constraint):
|
||||
def add_constraint(self, constraint: Constraint) -> None:
|
||||
"""Add a constraint to the pool"""
|
||||
self.__constraint_pool.append(constraint)
|
||||
|
||||
def update_edit_bones(self, edit_bones):
|
||||
def update_edit_bones(self, edit_bones: bpy.types.ArmatureEditBones) -> None:
|
||||
"""Update edit bones by creating shadow bones"""
|
||||
bone = edit_bones[self.__bone_name]
|
||||
target_bone = edit_bones[self.__target_bone_name]
|
||||
if bone != target_bone and self.__is_well_aligned(bone, target_bone):
|
||||
logger.debug(f"Bones are well aligned, removing shadow bones for {self.__bone_name}")
|
||||
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
|
||||
return
|
||||
|
||||
@@ -532,6 +617,7 @@ class _AT_ShadowBoneCreate:
|
||||
dummy.head = target_bone.head
|
||||
dummy.tail = dummy.head + bone.tail - bone.head
|
||||
dummy.roll = bone.roll
|
||||
logger.debug(f"Created/updated dummy bone: {dummy_bone_name}")
|
||||
|
||||
shadow_bone_name = self.__shadow_bone_name
|
||||
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
|
||||
@@ -539,9 +625,12 @@ class _AT_ShadowBoneCreate:
|
||||
shadow.head = dummy.head
|
||||
shadow.tail = dummy.tail
|
||||
shadow.roll = bone.roll
|
||||
logger.debug(f"Created/updated shadow bone: {shadow_bone_name}")
|
||||
|
||||
def update_pose_bones(self, pose_bones):
|
||||
def update_pose_bones(self, pose_bones: Any) -> None:
|
||||
"""Update pose bones by setting up shadow bone properties"""
|
||||
if self.__shadow_bone_name not in pose_bones:
|
||||
logger.debug(f"Shadow bone {self.__shadow_bone_name} not found, using target bone directly")
|
||||
self.__update_constraints(use_shadow=False)
|
||||
return
|
||||
|
||||
@@ -560,5 +649,7 @@ class _AT_ShadowBoneCreate:
|
||||
c.subtarget = dummy_p_bone.name
|
||||
c.target_space = "POSE"
|
||||
c.owner_space = "POSE"
|
||||
logger.debug(f"Created copy transforms constraint for shadow bone: {self.__shadow_bone_name}")
|
||||
|
||||
self.__update_constraints()
|
||||
logger.debug(f"Updated constraints for shadow bone: {self.__shadow_bone_name}")
|
||||
|
||||
+88
-23
@@ -6,16 +6,19 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import math
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Tuple, Callable, Any, Union
|
||||
|
||||
import bpy
|
||||
from bpy.types import Object, ID, Camera, Context
|
||||
from mathutils import Vector, Matrix, Euler
|
||||
|
||||
from ..bpyutils import FnContext, Props
|
||||
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
class FnCamera:
|
||||
@staticmethod
|
||||
def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
|
||||
def find_root(obj: Optional[Object]) -> Optional[Object]:
|
||||
"""Find the root object of an MMD camera setup."""
|
||||
if obj is None:
|
||||
return None
|
||||
if FnCamera.is_mmd_camera_root(obj):
|
||||
@@ -25,16 +28,22 @@ class FnCamera:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def is_mmd_camera(obj: bpy.types.Object) -> bool:
|
||||
def is_mmd_camera(obj: Object) -> bool:
|
||||
"""Check if an object is an MMD camera."""
|
||||
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
|
||||
|
||||
@staticmethod
|
||||
def is_mmd_camera_root(obj: bpy.types.Object) -> bool:
|
||||
def is_mmd_camera_root(obj: Object) -> bool:
|
||||
"""Check if an object is an MMD camera root."""
|
||||
return obj.type == "EMPTY" and obj.mmd_type == "CAMERA"
|
||||
|
||||
@staticmethod
|
||||
def add_drivers(camera_object: bpy.types.Object):
|
||||
def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1):
|
||||
def add_drivers(camera_object: Object) -> None:
|
||||
"""Add drivers to the camera object for MMD camera functionality."""
|
||||
logger.debug(f"Adding drivers to camera: {camera_object.name}")
|
||||
|
||||
def __add_driver(id_data: ID, data_path: str, expression: str, index: int = -1) -> None:
|
||||
"""Add a driver to the specified ID data."""
|
||||
d = id_data.driver_add(data_path, index).driver
|
||||
d.type = "SCRIPTED"
|
||||
if "$empty_distance" in expression:
|
||||
@@ -72,22 +81,36 @@ class FnCamera:
|
||||
|
||||
d.expression = expression
|
||||
|
||||
try:
|
||||
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
|
||||
__add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1)
|
||||
__add_driver(camera_object.data, "type", "not $is_perspective")
|
||||
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
|
||||
logger.debug(f"Successfully added drivers to camera: {camera_object.name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add drivers to camera {camera_object.name}: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def remove_drivers(camera_object: bpy.types.Object):
|
||||
def remove_drivers(camera_object: Object) -> None:
|
||||
"""Remove drivers from the camera object."""
|
||||
logger.debug(f"Removing drivers from camera: {camera_object.name}")
|
||||
try:
|
||||
camera_object.data.driver_remove("ortho_scale")
|
||||
camera_object.driver_remove("rotation_euler")
|
||||
camera_object.data.driver_remove("ortho_scale")
|
||||
camera_object.data.driver_remove("lens")
|
||||
logger.debug(f"Successfully removed drivers from camera: {camera_object.name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove drivers from camera {camera_object.name}: {str(e)}")
|
||||
|
||||
|
||||
class MigrationFnCamera:
|
||||
@staticmethod
|
||||
def update_mmd_camera():
|
||||
def update_mmd_camera() -> None:
|
||||
"""Update all MMD cameras in the scene."""
|
||||
logger.info("Updating all MMD cameras in the scene")
|
||||
updated_count = 0
|
||||
|
||||
for camera_object in bpy.data.objects:
|
||||
if camera_object.type != "CAMERA":
|
||||
continue
|
||||
@@ -97,39 +120,57 @@ class MigrationFnCamera:
|
||||
# It's not a MMD Camera
|
||||
continue
|
||||
|
||||
try:
|
||||
FnCamera.remove_drivers(camera_object)
|
||||
FnCamera.add_drivers(camera_object)
|
||||
updated_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update MMD camera {camera_object.name}: {str(e)}")
|
||||
|
||||
logger.info(f"Updated {updated_count} MMD cameras")
|
||||
|
||||
|
||||
class MMDCamera:
|
||||
def __init__(self, obj):
|
||||
def __init__(self, obj: Object):
|
||||
"""Initialize an MMD camera."""
|
||||
root_object = FnCamera.find_root(obj)
|
||||
if root_object is None:
|
||||
raise ValueError("%s is not MMDCamera" % str(obj))
|
||||
logger.error(f"Object {obj.name} is not an MMD camera")
|
||||
raise ValueError(f"{obj.name} is not an MMD camera")
|
||||
|
||||
self.__emptyObj = getattr(root_object, "original", obj)
|
||||
logger.debug(f"Initialized MMD camera with root: {self.__emptyObj.name}")
|
||||
|
||||
@staticmethod
|
||||
def isMMDCamera(obj: bpy.types.Object) -> bool:
|
||||
def isMMDCamera(obj: Object) -> bool:
|
||||
"""Check if an object is an MMD camera."""
|
||||
return FnCamera.find_root(obj) is not None
|
||||
|
||||
@staticmethod
|
||||
def addDrivers(cameraObj: bpy.types.Object):
|
||||
def addDrivers(cameraObj: Object) -> None:
|
||||
"""Add drivers to the camera object."""
|
||||
FnCamera.add_drivers(cameraObj)
|
||||
|
||||
@staticmethod
|
||||
def removeDrivers(cameraObj: bpy.types.Object):
|
||||
def removeDrivers(cameraObj: Object) -> None:
|
||||
"""Remove drivers from the camera object. """
|
||||
if cameraObj.type != "CAMERA":
|
||||
return
|
||||
FnCamera.remove_drivers(cameraObj)
|
||||
|
||||
@staticmethod
|
||||
def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0):
|
||||
def convertToMMDCamera(cameraObj: Object, scale: float = 1.0) -> 'MMDCamera':
|
||||
"""Convert a camera to an MMD camera."""
|
||||
logger.info(f"Converting camera {cameraObj.name} to MMD camera with scale {scale}")
|
||||
|
||||
if FnCamera.is_mmd_camera(cameraObj):
|
||||
logger.debug(f"Camera {cameraObj.name} is already an MMD camera")
|
||||
return MMDCamera(cameraObj)
|
||||
|
||||
try:
|
||||
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.data.sensor_fit = "VERTICAL"
|
||||
@@ -153,24 +194,39 @@ class MMDCamera:
|
||||
empty.mmd_type = "CAMERA"
|
||||
empty.mmd_camera.angle = math.radians(30)
|
||||
empty.mmd_camera.persp = True
|
||||
|
||||
logger.info(f"Successfully converted {cameraObj.name} to MMD camera")
|
||||
return MMDCamera(empty)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to convert camera {cameraObj.name} to MMD camera: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1):
|
||||
def newMMDCameraAnimation(
|
||||
cameraObj: Optional[Object],
|
||||
cameraTarget: Optional[Object] = None,
|
||||
scale: float = 1.0,
|
||||
min_distance: float = 0.1
|
||||
) -> 'MMDCamera':
|
||||
"""Create a new MMD camera animation."""
|
||||
logger.info(f"Creating new MMD camera animation with scale {scale}")
|
||||
|
||||
try:
|
||||
scene = bpy.context.scene
|
||||
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
|
||||
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
|
||||
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
|
||||
mmd_cam_root = mmd_cam.parent
|
||||
|
||||
_camera_override_func = None
|
||||
_camera_override_func: Optional[Callable[[], Object]] = None
|
||||
if cameraObj is None:
|
||||
if scene.camera is None:
|
||||
scene.camera = mmd_cam
|
||||
logger.debug("Set scene camera to new MMD camera")
|
||||
return MMDCamera(mmd_cam_root)
|
||||
_camera_override_func = lambda: scene.camera
|
||||
|
||||
_target_override_func = None
|
||||
_target_override_func: Optional[Callable[[Object], Object]] = None
|
||||
if cameraTarget is None:
|
||||
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
|
||||
|
||||
@@ -180,7 +236,6 @@ class MMDCamera:
|
||||
FnCamera.remove_drivers(mmd_cam)
|
||||
|
||||
from math import atan
|
||||
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
render = scene.render
|
||||
@@ -202,6 +257,7 @@ class MMDCamera:
|
||||
for c in fcurves:
|
||||
c.keyframe_points.add(frame_count)
|
||||
|
||||
logger.debug(f"Processing {frame_count} frames for camera animation")
|
||||
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)):
|
||||
scene.frame_set(f)
|
||||
if _camera_override_func:
|
||||
@@ -245,13 +301,22 @@ class MMDCamera:
|
||||
mmd_cam_root.animation_data_create().action = parent_action
|
||||
mmd_cam.animation_data_create().action = distance_action
|
||||
scene.frame_set(frame_current)
|
||||
|
||||
logger.info(f"Successfully created MMD camera animation with {frame_count} frames")
|
||||
return MMDCamera(mmd_cam_root)
|
||||
|
||||
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
|
||||
|
||||
def camera(self):
|
||||
def camera(self) -> Object:
|
||||
"""Get the camera object of the MMD camera."""
|
||||
for i in self.__emptyObj.children:
|
||||
if i.type == "CAMERA":
|
||||
return i
|
||||
raise KeyError
|
||||
logger.error(f"No camera found for MMD camera root {self.__emptyObj.name}")
|
||||
raise KeyError(f"No camera found for MMD camera root {self.__emptyObj.name}")
|
||||
|
||||
+30
-13
@@ -6,36 +6,48 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import bpy
|
||||
from typing import Optional, Union, Any, List, Tuple
|
||||
from bpy.types import Object, Context
|
||||
|
||||
from ..bpyutils import FnContext, Props
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
class MMDLamp:
|
||||
def __init__(self, obj):
|
||||
def __init__(self, obj: Object) -> None:
|
||||
if MMDLamp.isLamp(obj):
|
||||
obj = obj.parent
|
||||
if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT":
|
||||
self.__emptyObj = obj
|
||||
self.__emptyObj: Object = obj
|
||||
else:
|
||||
raise ValueError("%s is not MMDLamp" % str(obj))
|
||||
error_msg = f"{str(obj)} is not MMDLamp"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
@staticmethod
|
||||
def isLamp(obj):
|
||||
return obj and obj.type in {"LIGHT", "LAMP"}
|
||||
def isLamp(obj: Optional[Object]) -> bool:
|
||||
"""Check if the object is a lamp/light object"""
|
||||
return obj is not None and obj.type in {"LIGHT", "LAMP"}
|
||||
|
||||
@staticmethod
|
||||
def isMMDLamp(obj):
|
||||
def isMMDLamp(obj: Optional[Object]) -> bool:
|
||||
"""Check if the object is an MMD lamp"""
|
||||
if MMDLamp.isLamp(obj):
|
||||
obj = obj.parent
|
||||
return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
|
||||
return obj is not None and obj.type == "EMPTY" and obj.mmd_type == "LIGHT"
|
||||
|
||||
@staticmethod
|
||||
def convertToMMDLamp(lampObj, scale=1.0):
|
||||
def convertToMMDLamp(lampObj: Object, scale: float = 1.0) -> 'MMDLamp':
|
||||
"""Convert a regular lamp to an MMD lamp"""
|
||||
if MMDLamp.isMMDLamp(lampObj):
|
||||
logger.debug(f"Object {lampObj.name} is already an MMD lamp")
|
||||
return MMDLamp(lampObj)
|
||||
|
||||
empty = bpy.data.objects.new(name="MMD_Light", object_data=None)
|
||||
FnContext.link_object(FnContext.ensure_context(), empty)
|
||||
logger.info(f"Converting {lampObj.name} to MMD lamp with scale {scale}")
|
||||
|
||||
empty: Object = bpy.data.objects.new(name="MMD_Light", object_data=None)
|
||||
context = FnContext.ensure_context()
|
||||
FnContext.link_object(context, empty)
|
||||
|
||||
empty.rotation_mode = "XYZ"
|
||||
empty.lock_rotation = (True, True, True)
|
||||
@@ -57,13 +69,18 @@ class MMDLamp:
|
||||
constraint.track_axis = "TRACK_NEGATIVE_Z"
|
||||
constraint.up_axis = "UP_Y"
|
||||
|
||||
logger.debug(f"Successfully created MMD lamp from {lampObj.name}")
|
||||
return MMDLamp(empty)
|
||||
|
||||
def object(self):
|
||||
def object(self) -> Object:
|
||||
"""Get the empty object that represents this MMD lamp"""
|
||||
return self.__emptyObj
|
||||
|
||||
def lamp(self):
|
||||
def lamp(self) -> Object:
|
||||
"""Get the actual lamp/light object"""
|
||||
for i in self.__emptyObj.children:
|
||||
if MMDLamp.isLamp(i):
|
||||
return i
|
||||
raise KeyError
|
||||
error_msg = f"No lamp found in MMD lamp {self.__emptyObj.name}"
|
||||
logger.error(error_msg)
|
||||
raise KeyError(error_msg)
|
||||
|
||||
+124
-64
@@ -7,7 +7,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast, Dict, List, Any, Union, Set
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
@@ -15,6 +15,7 @@ from mathutils import Vector
|
||||
from ..bpyutils import FnContext
|
||||
from .exceptions import MaterialNotFoundError
|
||||
from .shader import _NodeGroupUtils
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..properties.material import MMDMaterial
|
||||
@@ -27,48 +28,53 @@ SPHERE_MODE_SUBTEX = 3
|
||||
|
||||
|
||||
class _DummyTexture:
|
||||
def __init__(self, image):
|
||||
self.type = "IMAGE"
|
||||
self.image = image
|
||||
self.use_mipmap = True
|
||||
def __init__(self, image: bpy.types.Image):
|
||||
self.type: str = "IMAGE"
|
||||
self.image: bpy.types.Image = image
|
||||
self.use_mipmap: bool = True
|
||||
|
||||
|
||||
class _DummyTextureSlot:
|
||||
def __init__(self, image):
|
||||
self.diffuse_color_factor = 1
|
||||
self.uv_layer = ""
|
||||
self.texture = _DummyTexture(image)
|
||||
def __init__(self, image: bpy.types.Image):
|
||||
self.diffuse_color_factor: float = 1
|
||||
self.uv_layer: str = ""
|
||||
self.texture: _DummyTexture = _DummyTexture(image)
|
||||
|
||||
|
||||
class FnMaterial:
|
||||
__NODES_ARE_READONLY: bool = False
|
||||
|
||||
def __init__(self, material: bpy.types.Material):
|
||||
self.__material = material
|
||||
self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY
|
||||
self.__material: bpy.types.Material = material
|
||||
self._nodes_are_readonly: bool = FnMaterial.__NODES_ARE_READONLY
|
||||
|
||||
@staticmethod
|
||||
def set_nodes_are_readonly(nodes_are_readonly: bool):
|
||||
def set_nodes_are_readonly(nodes_are_readonly: bool) -> None:
|
||||
FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
|
||||
|
||||
@classmethod
|
||||
def from_material_id(cls, material_id: str):
|
||||
def from_material_id(cls, material_id: str) -> Optional['FnMaterial']:
|
||||
for material in bpy.data.materials:
|
||||
if material.mmd_material.material_id == material_id:
|
||||
return cls(material)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def clean_materials(obj, can_remove: Callable[[bpy.types.Material], bool]):
|
||||
def clean_materials(obj: bpy.types.Object, can_remove: Callable[[bpy.types.Material], bool]) -> None:
|
||||
materials = obj.data.materials
|
||||
materials_pop = materials.pop
|
||||
removed_count = 0
|
||||
for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True):
|
||||
m = materials_pop(index=i)
|
||||
removed_count += 1
|
||||
if m.users < 1:
|
||||
bpy.data.materials.remove(m)
|
||||
|
||||
if removed_count > 0:
|
||||
logger.debug(f"Removed {removed_count} materials from {obj.name}")
|
||||
|
||||
@staticmethod
|
||||
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: str | int, mat2_ref: str | int, reverse=False, swap_slots=False) -> Tuple[bpy.types.Material, bpy.types.Material]:
|
||||
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: Union[str, int], mat2_ref: Union[str, int], reverse: bool = False, swap_slots: bool = False) -> Tuple[bpy.types.Material, bpy.types.Material]:
|
||||
"""
|
||||
This method will assign the polygons of mat1 to mat2.
|
||||
If reverse is True it will also swap the polygons assigned to mat2 to mat1.
|
||||
@@ -98,8 +104,12 @@ class FnMaterial:
|
||||
except (KeyError, IndexError) as exc:
|
||||
# Wrap exceptions within our custom ones
|
||||
raise MaterialNotFoundError() from exc
|
||||
|
||||
mat1_idx = mesh.materials.find(mat1.name)
|
||||
mat2_idx = mesh.materials.find(mat2.name)
|
||||
|
||||
logger.debug(f"Swapping materials: {mat1.name} (idx:{mat1_idx}) <-> {mat2.name} (idx:{mat2_idx}) in {mesh_object.name}")
|
||||
|
||||
# Swap polygons
|
||||
for poly in mesh.polygons:
|
||||
if poly.material_index == mat1_idx:
|
||||
@@ -113,33 +123,37 @@ class FnMaterial:
|
||||
return mat1, mat2
|
||||
|
||||
@staticmethod
|
||||
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]):
|
||||
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]) -> None:
|
||||
"""
|
||||
This method will fix the material order. Which is lost after joining meshes.
|
||||
"""
|
||||
materials = cast(bpy.types.Mesh, meshObj.data).materials
|
||||
logger.debug(f"Fixing material order for {meshObj.name}")
|
||||
|
||||
for new_idx, mat in enumerate(material_names):
|
||||
# Get the material that is currently on this index
|
||||
other_mat = materials[new_idx]
|
||||
if other_mat.name == mat:
|
||||
continue # This is already in place
|
||||
logger.debug(f"Moving material {mat} to index {new_idx}")
|
||||
FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True)
|
||||
|
||||
@property
|
||||
def material_id(self):
|
||||
mmd_mat: MMDMaterial = self.__material.mmd_material
|
||||
def material_id(self) -> int:
|
||||
mmd_mat: 'MMDMaterial' = self.__material.mmd_material
|
||||
if mmd_mat.material_id < 0:
|
||||
max_id = -1
|
||||
for mat in bpy.data.materials:
|
||||
max_id = max(max_id, mat.mmd_material.material_id)
|
||||
mmd_mat.material_id = max_id + 1
|
||||
logger.debug(f"Assigned new material ID {mmd_mat.material_id} to {self.__material.name}")
|
||||
return mmd_mat.material_id
|
||||
|
||||
@property
|
||||
def material(self):
|
||||
def material(self) -> bpy.types.Material:
|
||||
return self.__material
|
||||
|
||||
def __same_image_file(self, image, filepath):
|
||||
def __same_image_file(self, image: Optional[bpy.types.Image], filepath: str) -> bool:
|
||||
if image and image.source == "FILE":
|
||||
# pylint: disable=assignment-from-no-return
|
||||
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
|
||||
@@ -152,14 +166,15 @@ class FnMaterial:
|
||||
pass
|
||||
return False
|
||||
|
||||
def _load_image(self, filepath):
|
||||
def _load_image(self, filepath: str) -> bpy.types.Image:
|
||||
img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None)
|
||||
if img is None:
|
||||
# pylint: disable=bare-except
|
||||
try:
|
||||
logger.debug(f"Loading image: {filepath}")
|
||||
img = bpy.data.images.load(filepath)
|
||||
except:
|
||||
logging.warning("Cannot create a texture for %s. No such file.", filepath)
|
||||
logger.warning(f"Cannot create a texture for {filepath}. No such file.")
|
||||
img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
|
||||
img.source = "FILE"
|
||||
img.filepath = filepath
|
||||
@@ -170,43 +185,46 @@ class FnMaterial:
|
||||
img.alpha_mode = "NONE"
|
||||
return img
|
||||
|
||||
def update_toon_texture(self):
|
||||
def update_toon_texture(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mmd_mat: MMDMaterial = self.__material.mmd_material
|
||||
mmd_mat: 'MMDMaterial' = self.__material.mmd_material
|
||||
if mmd_mat.is_shared_toon_texture:
|
||||
shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "")
|
||||
toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1))
|
||||
logger.debug(f"Using shared toon texture: {toon_path}")
|
||||
self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path))
|
||||
elif mmd_mat.toon_texture != "":
|
||||
logger.debug(f"Using custom toon texture: {mmd_mat.toon_texture}")
|
||||
self.create_toon_texture(mmd_mat.toon_texture)
|
||||
else:
|
||||
logger.debug(f"Removing toon texture from {self.__material.name}")
|
||||
self.remove_toon_texture()
|
||||
|
||||
def _mix_diffuse_and_ambient(self, mmd_mat):
|
||||
def _mix_diffuse_and_ambient(self, mmd_mat: 'MMDMaterial') -> List[float]:
|
||||
r, g, b = mmd_mat.diffuse_color
|
||||
ar, ag, ab = mmd_mat.ambient_color
|
||||
return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)]
|
||||
|
||||
def update_drop_shadow(self):
|
||||
def update_drop_shadow(self) -> None:
|
||||
pass
|
||||
|
||||
def update_enabled_toon_edge(self):
|
||||
def update_enabled_toon_edge(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
self.update_edge_color()
|
||||
|
||||
def update_edge_color(self):
|
||||
def update_edge_color(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.__material
|
||||
mmd_mat: MMDMaterial = mat.mmd_material
|
||||
mmd_mat: 'MMDMaterial' = mat.mmd_material
|
||||
color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
|
||||
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
|
||||
if hasattr(mat, "line_color"): # freestyle line color
|
||||
mat.line_color = line_color
|
||||
|
||||
mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None)
|
||||
mat_edge: Optional[bpy.types.Material] = bpy.data.materials.get("mmd_edge." + mat.name, None)
|
||||
if mat_edge:
|
||||
mat_edge.mmd_material.edge_color = line_color
|
||||
|
||||
@@ -218,38 +236,45 @@ class FnMaterial:
|
||||
if node_shader and "Alpha" in node_shader.inputs:
|
||||
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
|
||||
|
||||
def get_texture(self):
|
||||
def get_texture(self) -> Optional[_DummyTexture]:
|
||||
return self.__get_texture_node("mmd_base_tex", use_dummy=True)
|
||||
|
||||
def create_texture(self, filepath):
|
||||
def create_texture(self, filepath: str) -> _DummyTextureSlot:
|
||||
texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1))
|
||||
logger.debug(f"Created base texture for {self.__material.name}: {filepath}")
|
||||
return _DummyTextureSlot(texture.image)
|
||||
|
||||
def remove_texture(self):
|
||||
def remove_texture(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
logger.debug(f"Removing base texture from {self.__material.name}")
|
||||
self.__remove_texture_node("mmd_base_tex")
|
||||
|
||||
def get_sphere_texture(self):
|
||||
def get_sphere_texture(self) -> Optional[_DummyTexture]:
|
||||
return self.__get_texture_node("mmd_sphere_tex", use_dummy=True)
|
||||
|
||||
def use_sphere_texture(self, use_sphere, obj=None):
|
||||
def use_sphere_texture(self, use_sphere: bool, obj: Optional[bpy.types.Object] = None) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
if use_sphere:
|
||||
logger.debug(f"Enabling sphere texture for {self.__material.name}")
|
||||
self.update_sphere_texture_type(obj)
|
||||
else:
|
||||
logger.debug(f"Disabling sphere texture for {self.__material.name}")
|
||||
self.__update_shader_input("Sphere Tex Fac", 0)
|
||||
|
||||
def create_sphere_texture(self, filepath, obj=None):
|
||||
def create_sphere_texture(self, filepath: str, obj: Optional[bpy.types.Object] = None) -> _DummyTextureSlot:
|
||||
texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2))
|
||||
logger.debug(f"Created sphere texture for {self.__material.name}: {filepath}")
|
||||
self.update_sphere_texture_type(obj)
|
||||
return _DummyTextureSlot(texture.image)
|
||||
|
||||
def update_sphere_texture_type(self, obj=None):
|
||||
def update_sphere_texture_type(self, obj: Optional[bpy.types.Object] = None) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
|
||||
@@ -277,48 +302,54 @@ class FnMaterial:
|
||||
next(uv_layers, None) # skip base UV
|
||||
subtex_uv = getattr(next(uv_layers, None), "name", "")
|
||||
if subtex_uv != "UV1":
|
||||
logging.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv)
|
||||
logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex')
|
||||
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
|
||||
else:
|
||||
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
|
||||
|
||||
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:
|
||||
return
|
||||
logger.debug(f"Removing sphere texture from {self.__material.name}")
|
||||
self.__remove_texture_node("mmd_sphere_tex")
|
||||
|
||||
def get_toon_texture(self):
|
||||
def get_toon_texture(self) -> Optional[_DummyTexture]:
|
||||
return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
|
||||
|
||||
def use_toon_texture(self, use_toon):
|
||||
def use_toon_texture(self, use_toon: bool) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
logger.debug(f"{'Enabling' if use_toon else 'Disabling'} toon texture for {self.__material.name}")
|
||||
self.__update_shader_input("Toon Tex Fac", use_toon)
|
||||
|
||||
def create_toon_texture(self, filepath):
|
||||
def create_toon_texture(self, filepath: str) -> _DummyTextureSlot:
|
||||
texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5))
|
||||
logger.debug(f"Created toon texture for {self.__material.name}: {filepath}")
|
||||
return _DummyTextureSlot(texture.image)
|
||||
|
||||
def remove_toon_texture(self):
|
||||
def remove_toon_texture(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
logger.debug(f"Removing toon texture from {self.__material.name}")
|
||||
self.__remove_texture_node("mmd_toon_tex")
|
||||
|
||||
def __get_texture_node(self, node_name, use_dummy=False):
|
||||
def __get_texture_node(self, node_name: str, use_dummy: bool = False) -> Optional[Union[bpy.types.ShaderNodeTexImage, _DummyTexture]]:
|
||||
mat = self.material
|
||||
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
||||
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
||||
return _DummyTexture(texture.image) if use_dummy else texture
|
||||
return None
|
||||
|
||||
def __remove_texture_node(self, node_name):
|
||||
def __remove_texture_node(self, node_name: str) -> None:
|
||||
mat = self.material
|
||||
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
||||
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
||||
mat.node_tree.nodes.remove(texture)
|
||||
mat.update_tag()
|
||||
|
||||
def __create_texture_node(self, node_name, filepath, pos):
|
||||
def __create_texture_node(self, node_name: str, filepath: str, pos: Tuple[float, float]) -> bpy.types.ShaderNodeTexImage:
|
||||
texture = self.__get_texture_node(node_name)
|
||||
if texture is None:
|
||||
from mathutils import Vector
|
||||
@@ -334,23 +365,25 @@ class FnMaterial:
|
||||
self.__update_shader_nodes()
|
||||
return texture
|
||||
|
||||
def update_ambient_color(self):
|
||||
def update_ambient_color(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
||||
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
|
||||
logger.debug(f"Updated ambient color for {mat.name}")
|
||||
|
||||
def update_diffuse_color(self):
|
||||
def update_diffuse_color(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
||||
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
|
||||
logger.debug(f"Updated diffuse color for {mat.name}")
|
||||
|
||||
def update_alpha(self):
|
||||
def update_alpha(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
@@ -368,16 +401,18 @@ class FnMaterial:
|
||||
mat.diffuse_color[3] = mmd_mat.alpha
|
||||
self.__update_shader_input("Alpha", mmd_mat.alpha)
|
||||
self.update_self_shadow_map()
|
||||
logger.debug(f"Updated alpha for {mat.name}: {mmd_mat.alpha}")
|
||||
|
||||
def update_specular_color(self):
|
||||
def update_specular_color(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
mat.specular_color = mmd_mat.specular_color
|
||||
self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,))
|
||||
logger.debug(f"Updated specular color for {mat.name}")
|
||||
|
||||
def update_shininess(self):
|
||||
def update_shininess(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
@@ -388,8 +423,9 @@ class FnMaterial:
|
||||
if hasattr(mat, "specular_hardness"):
|
||||
mat.specular_hardness = mmd_mat.shininess
|
||||
self.__update_shader_input("Reflect", mmd_mat.shininess)
|
||||
logger.debug(f"Updated shininess for {mat.name}: {mmd_mat.shininess}")
|
||||
|
||||
def update_is_double_sided(self):
|
||||
def update_is_double_sided(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
@@ -399,8 +435,9 @@ class FnMaterial:
|
||||
elif hasattr(mat, "use_backface_culling"):
|
||||
mat.use_backface_culling = not mmd_mat.is_double_sided
|
||||
self.__update_shader_input("Double Sided", mmd_mat.is_double_sided)
|
||||
logger.debug(f"Updated double-sided setting for {mat.name}: {mmd_mat.is_double_sided}")
|
||||
|
||||
def update_self_shadow_map(self):
|
||||
def update_self_shadow_map(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
@@ -408,21 +445,24 @@ class FnMaterial:
|
||||
cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
|
||||
if hasattr(mat, "shadow_method"):
|
||||
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
|
||||
logger.debug(f"Updated self shadow map for {mat.name}: {cast_shadows}")
|
||||
|
||||
def update_self_shadow(self):
|
||||
def update_self_shadow(self) -> None:
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow)
|
||||
logger.debug(f"Updated self shadow for {mat.name}: {mmd_mat.enabled_self_shadow}")
|
||||
|
||||
@staticmethod
|
||||
def convert_to_mmd_material(material, context=bpy.context):
|
||||
def convert_to_mmd_material(material: bpy.types.Material, context: bpy.types.Context = bpy.context) -> None:
|
||||
m, mmd_material = material, material.mmd_material
|
||||
logger.debug(f"Converting material to MMD material: {material.name}")
|
||||
|
||||
if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None:
|
||||
|
||||
def search_tex_image_node(node: bpy.types.ShaderNode):
|
||||
def search_tex_image_node(node: bpy.types.ShaderNode) -> Optional[bpy.types.ShaderNodeTexImage]:
|
||||
if node.type == "TEX_IMAGE":
|
||||
return node
|
||||
for node_input in node.inputs:
|
||||
@@ -459,6 +499,7 @@ class FnMaterial:
|
||||
if tex_node is None:
|
||||
tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None)
|
||||
if tex_node:
|
||||
logger.debug(f"Found texture node for {material.name}: {tex_node.name}")
|
||||
tex_node.name = "mmd_base_tex"
|
||||
else:
|
||||
# Take the Base Color from BSDF if there's no texture
|
||||
@@ -466,6 +507,7 @@ class FnMaterial:
|
||||
if bsdf_node:
|
||||
base_color_input = bsdf_node.inputs.get('Base Color') or bsdf_node.inputs.get('Color')
|
||||
if base_color_input:
|
||||
logger.debug(f"Using BSDF base color for {material.name}")
|
||||
mmd_material.diffuse_color = base_color_input.default_value[:3]
|
||||
# ambient should be half the diffuse
|
||||
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
|
||||
@@ -498,9 +540,10 @@ class FnMaterial:
|
||||
if m.use_nodes:
|
||||
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED' or n.type.startswith('BSDF_')]
|
||||
for n in nodes_to_remove:
|
||||
logger.debug(f"Removing BSDF node from {material.name}: {n.name}")
|
||||
m.node_tree.nodes.remove(n)
|
||||
|
||||
def __update_shader_input(self, name, val):
|
||||
def __update_shader_input(self, name: str, val: Any) -> None:
|
||||
mat = self.material
|
||||
if mat.name.startswith("mmd_"): # skip mmd_edge.*
|
||||
return
|
||||
@@ -512,26 +555,29 @@ class FnMaterial:
|
||||
val = min(max(val, interface_socket.min_value), interface_socket.max_value)
|
||||
shader.inputs[name].default_value = val
|
||||
|
||||
def __update_shader_nodes(self):
|
||||
def __update_shader_nodes(self) -> None:
|
||||
mat = self.material
|
||||
if mat.node_tree is None:
|
||||
logger.debug(f"Creating node tree for {mat.name}")
|
||||
mat.use_nodes = True
|
||||
mat.node_tree.nodes.clear()
|
||||
|
||||
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
||||
|
||||
class _Dummy:
|
||||
default_value, is_linked = None, True
|
||||
default_value: Any = None
|
||||
is_linked: bool = True
|
||||
|
||||
node_shader = nodes.get("mmd_shader", None)
|
||||
if node_shader is None:
|
||||
logger.debug(f"Creating MMD shader node for {mat.name}")
|
||||
node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
|
||||
node_shader.name = "mmd_shader"
|
||||
node_shader.location = (0, 1500)
|
||||
node_shader.width = 200
|
||||
node_shader.node_tree = self.__get_shader()
|
||||
|
||||
mmd_mat: MMDMaterial = mat.mmd_material
|
||||
mmd_mat: 'MMDMaterial' = mat.mmd_material
|
||||
node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,)
|
||||
node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,)
|
||||
node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,)
|
||||
@@ -543,6 +589,7 @@ class FnMaterial:
|
||||
|
||||
node_uv = nodes.get("mmd_tex_uv", None)
|
||||
if node_uv is None:
|
||||
logger.debug(f"Creating MMD UV node for {mat.name}")
|
||||
node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
|
||||
node_uv.name = "mmd_tex_uv"
|
||||
node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220))
|
||||
@@ -567,12 +614,13 @@ class FnMaterial:
|
||||
if not texture.inputs["Vector"].is_linked:
|
||||
links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"])
|
||||
|
||||
def __get_shader_uv(self):
|
||||
def __get_shader_uv(self) -> bpy.types.ShaderNodeTree:
|
||||
group_name = "MMDTexUV"
|
||||
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||
if len(shader.nodes):
|
||||
return shader
|
||||
|
||||
logger.debug(f"Creating MMD UV shader node group")
|
||||
ng = _NodeGroupUtils(shader)
|
||||
|
||||
############################################################################
|
||||
@@ -604,12 +652,13 @@ class FnMaterial:
|
||||
|
||||
return shader
|
||||
|
||||
def __get_shader(self):
|
||||
def __get_shader(self) -> bpy.types.ShaderNodeTree:
|
||||
group_name = "MMDShaderDev"
|
||||
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||
if len(shader.nodes):
|
||||
return shader
|
||||
|
||||
logger.debug(f"Creating MMD shader node group")
|
||||
ng = _NodeGroupUtils(shader)
|
||||
|
||||
############################################################################
|
||||
@@ -699,15 +748,18 @@ class FnMaterial:
|
||||
|
||||
class MigrationFnMaterial:
|
||||
@staticmethod
|
||||
def update_mmd_shader():
|
||||
def update_mmd_shader() -> None:
|
||||
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
|
||||
if mmd_shader_node_tree is None:
|
||||
logger.debug("No MMD shader node tree found, skipping update")
|
||||
return
|
||||
|
||||
ng = _NodeGroupUtils(mmd_shader_node_tree)
|
||||
if "Color" in ng.node_output.inputs:
|
||||
logger.debug("MMD shader already has Color output, skipping update")
|
||||
return
|
||||
|
||||
logger.info("Updating MMD shader node tree")
|
||||
shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0]
|
||||
node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node
|
||||
node_output: bpy.types.NodeGroupOutput = ng.node_output
|
||||
@@ -716,3 +768,11 @@ class MigrationFnMaterial:
|
||||
|
||||
ng.new_output_socket("Color", node_sphere.outputs["Color"])
|
||||
ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
|
||||
logger.info("MMD shader node tree updated successfully")
|
||||
|
||||
# Add Self Shadow input if it doesn't exist
|
||||
if "Self Shadow" not in ng.node_input.outputs:
|
||||
logger.info("Adding Self Shadow input to MMD shader")
|
||||
# Find shader_base_mix node to connect Self Shadow
|
||||
shader_base_mix = shader_alpha_mix.inputs[2].links[0].from_node
|
||||
ng.new_input_socket("Self Shadow", shader_base_mix.inputs["Fac"], 0, min_max=(0, 1))
|
||||
|
||||
+160
-88
@@ -6,9 +6,8 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast, List, Tuple
|
||||
|
||||
import bpy
|
||||
import idprop
|
||||
@@ -20,15 +19,17 @@ from ..bpyutils import FnContext, Props
|
||||
from . import rigid_body
|
||||
from .morph import FnMorph
|
||||
from .rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..properties.morph import MaterialMorphData
|
||||
from ..properties.rigid_body import MMDRigidBody
|
||||
from bpy.types import Context, Object, PropertyGroup, Material, Mesh, Armature, EditBone, PoseBone, KinematicConstraint
|
||||
|
||||
|
||||
class FnModel:
|
||||
@staticmethod
|
||||
def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Dict[str, Dict[Any, Any]] = None):
|
||||
def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Optional[Dict[str, Dict[Any, Any]]] = None) -> None:
|
||||
FnModel.__copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values or {})
|
||||
|
||||
@staticmethod
|
||||
@@ -40,9 +41,11 @@ class FnModel:
|
||||
Optional[bpy.types.Object]: The root object of the model. If the object is not a part of a model, None is returned.
|
||||
Generally, the root object is a object with type == "EMPTY" and mmd_type == "ROOT".
|
||||
"""
|
||||
while obj is not None and obj.mmd_type != "ROOT":
|
||||
obj = obj.parent
|
||||
while obj is not None:
|
||||
if hasattr(obj, 'mmd_type') and obj.mmd_type == "ROOT":
|
||||
return obj
|
||||
obj = obj.parent
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]:
|
||||
@@ -213,7 +216,8 @@ class FnModel:
|
||||
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
|
||||
@staticmethod
|
||||
def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]):
|
||||
def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]) -> None:
|
||||
logger.info(f"Joining models to parent root: {parent_root_object.name}")
|
||||
parent_armature_object = FnModel.find_armature_object(parent_root_object)
|
||||
with bpy.context.temp_override(
|
||||
active_object=parent_armature_object,
|
||||
@@ -221,7 +225,7 @@ class FnModel:
|
||||
):
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
|
||||
def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pose_bones):
|
||||
def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs: List[Any], pose_bones: List[bpy.types.PoseBone]) -> None:
|
||||
"""This function will also update the references of bone morphs and rotate+/move+."""
|
||||
bone_id = bone.mmd_bone.bone_id
|
||||
|
||||
@@ -259,6 +263,7 @@ class FnModel:
|
||||
|
||||
child_root_object: bpy.types.Object
|
||||
for child_root_object in child_root_objects:
|
||||
logger.info(f"Processing child root: {child_root_object.name}")
|
||||
child_armature_object = FnModel.find_armature_object(child_root_object)
|
||||
child_pose_bones = child_armature_object.pose.bones
|
||||
child_bone_morphs = child_root_object.mmd_root.bone_morphs
|
||||
@@ -279,7 +284,7 @@ class FnModel:
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
|
||||
# Disconnect mesh dependencies because transform_apply fails when mesh data are multiple used.
|
||||
related_meshes: Dict[MaterialMorphData, bpy.types.Mesh] = {}
|
||||
related_meshes: Dict['MaterialMorphData', bpy.types.Mesh] = {}
|
||||
for material_morph in child_root_object.mmd_root.material_morphs:
|
||||
for material_morph_data in material_morph.data:
|
||||
if material_morph_data.related_mesh_data is not None:
|
||||
@@ -353,6 +358,8 @@ class FnModel:
|
||||
if len(child_root_object.children) == 0:
|
||||
bpy.data.objects.remove(child_root_object)
|
||||
|
||||
logger.info("Model joining completed successfully")
|
||||
|
||||
@staticmethod
|
||||
def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier:
|
||||
for m in mesh_object.modifiers:
|
||||
@@ -369,10 +376,13 @@ class FnModel:
|
||||
return modifier
|
||||
|
||||
@staticmethod
|
||||
def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool):
|
||||
def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool) -> None:
|
||||
logger.info(f"Attaching mesh objects to {parent_root_object.name}")
|
||||
armature_object = FnModel.find_armature_object(parent_root_object)
|
||||
if armature_object is None:
|
||||
raise ValueError(f"Armature object not found in {parent_root_object}")
|
||||
error_msg = f"Armature object not found in {parent_root_object.name}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object:
|
||||
if obj.parent is None:
|
||||
@@ -381,9 +391,11 @@ class FnModel:
|
||||
|
||||
for mesh_object in mesh_objects:
|
||||
if not FnModel.is_mesh_object(mesh_object):
|
||||
logger.debug(f"Skipping non-mesh object: {mesh_object.name}")
|
||||
continue
|
||||
|
||||
if FnModel.find_root_object(mesh_object) is not None:
|
||||
logger.debug(f"Skipping mesh with existing root: {mesh_object.name}")
|
||||
continue
|
||||
|
||||
mesh_root_object = __get_root_object(mesh_object)
|
||||
@@ -391,15 +403,20 @@ class FnModel:
|
||||
mesh_root_object.parent_type = "OBJECT"
|
||||
mesh_root_object.parent = armature_object
|
||||
mesh_root_object.matrix_world = original_matrix_world
|
||||
logger.debug(f"Attached mesh: {mesh_object.name}")
|
||||
|
||||
if add_armature_modifier:
|
||||
FnModel._add_armature_modifier(mesh_object, armature_object)
|
||||
logger.debug(f"Added armature modifier to: {mesh_object.name}")
|
||||
|
||||
@staticmethod
|
||||
def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool):
|
||||
def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool) -> None:
|
||||
logger.info(f"Adding missing vertex groups from bones to {mesh_object.name}")
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
if armature_object is None:
|
||||
raise ValueError(f"Armature object not found in {root_object}")
|
||||
error_msg = f"Armature object not found in {root_object.name}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
vertex_group_names: Set[str] = set()
|
||||
|
||||
@@ -408,6 +425,7 @@ class FnModel:
|
||||
for search_mesh in search_meshes:
|
||||
vertex_group_names.update(search_mesh.vertex_groups.keys())
|
||||
|
||||
added_count = 0
|
||||
pose_bone: bpy.types.PoseBone
|
||||
for pose_bone in armature_object.pose.bones:
|
||||
pose_bone_name = pose_bone.name
|
||||
@@ -419,28 +437,34 @@ class FnModel:
|
||||
continue
|
||||
|
||||
mesh_object.vertex_groups.new(name=pose_bone_name)
|
||||
added_count += 1
|
||||
|
||||
logger.debug(f"Added {added_count} missing vertex groups to {mesh_object.name}")
|
||||
|
||||
@staticmethod
|
||||
def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int):
|
||||
def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int) -> None:
|
||||
logger.info(f"Changing IK loop factor to {new_ik_loop_factor}")
|
||||
mmd_root = root_object.mmd_root
|
||||
old_ik_loop_factor = mmd_root.ik_loop_factor
|
||||
|
||||
if new_ik_loop_factor == old_ik_loop_factor:
|
||||
logger.debug("IK loop factor already set to the requested value")
|
||||
return
|
||||
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
updated_count = 0
|
||||
for pose_bone in armature_object.pose.bones:
|
||||
for constraint in (cast(bpy.types.KinematicConstraint, c) for c in pose_bone.constraints if c.type == "IK"):
|
||||
iterations = int(constraint.iterations * new_ik_loop_factor / old_ik_loop_factor)
|
||||
logging.info("Update %s of %s: %d -> %d", constraint.name, pose_bone.name, constraint.iterations, iterations)
|
||||
logger.debug(f"Update {constraint.name} of {pose_bone.name}: {constraint.iterations} -> {iterations}")
|
||||
constraint.iterations = iterations
|
||||
updated_count += 1
|
||||
|
||||
mmd_root.ik_loop_factor = new_ik_loop_factor
|
||||
|
||||
return
|
||||
logger.info(f"Updated {updated_count} IK constraints")
|
||||
|
||||
@staticmethod
|
||||
def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
|
||||
def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
|
||||
destination_rna_properties = destination.bl_rna.properties
|
||||
for name in source.keys():
|
||||
is_attr = hasattr(source, name)
|
||||
@@ -466,7 +490,7 @@ class FnModel:
|
||||
destination[name] = value
|
||||
|
||||
@staticmethod
|
||||
def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
|
||||
def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
|
||||
if overwrite:
|
||||
destination.clear()
|
||||
|
||||
@@ -499,16 +523,19 @@ class FnModel:
|
||||
FnModel.__copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values)
|
||||
|
||||
@staticmethod
|
||||
def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]):
|
||||
def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]) -> None:
|
||||
if isinstance(destination, bpy.types.PropertyGroup):
|
||||
FnModel.__copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
|
||||
elif isinstance(destination, bpy.types.bpy_prop_collection):
|
||||
FnModel.__copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values)
|
||||
else:
|
||||
raise ValueError(f"Unsupported destination: {destination}")
|
||||
error_msg = f"Unsupported destination: {destination}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
@staticmethod
|
||||
def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True):
|
||||
def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True) -> None:
|
||||
logger.info(f"Initializing display item frames for {root_object.name}")
|
||||
frames = root_object.mmd_root.display_item_frames
|
||||
if reset and len(frames) > 0:
|
||||
root_object.mmd_root.active_display_item_frame = 0
|
||||
@@ -532,6 +559,8 @@ class FnModel:
|
||||
frames.move(frames.find("Root"), 0)
|
||||
frames.move(frames.find("表情"), 1)
|
||||
|
||||
logger.debug(f"Display item frames initialized with {len(frames)} frames")
|
||||
|
||||
@staticmethod
|
||||
def get_empty_display_size(root_object: bpy.types.Object) -> float:
|
||||
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"""
|
||||
|
||||
@classmethod
|
||||
def update_mmd_ik_loop_factor(cls):
|
||||
def update_mmd_ik_loop_factor(cls) -> None:
|
||||
logger.info("Updating MMD IK loop factor for all armatures")
|
||||
updated_count = 0
|
||||
for armature_object in bpy.data.objects:
|
||||
if armature_object.type != "ARMATURE":
|
||||
continue
|
||||
|
||||
if "mmd_ik_loop_factor" not in armature_object:
|
||||
return
|
||||
continue
|
||||
|
||||
FnModel.find_root_object(armature_object).mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1)
|
||||
root_object = FnModel.find_root_object(armature_object)
|
||||
if root_object:
|
||||
root_object.mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1)
|
||||
del armature_object["mmd_ik_loop_factor"]
|
||||
updated_count += 1
|
||||
|
||||
logger.info(f"Updated IK loop factor for {updated_count} armatures")
|
||||
|
||||
@staticmethod
|
||||
def update_avatar_toolkit_version():
|
||||
def update_avatar_toolkit_version() -> None:
|
||||
logger.info("Updating Avatar Toolkit version for all MMD root objects")
|
||||
updated_count = 0
|
||||
for root_object in bpy.data.objects:
|
||||
if root_object.type != "EMPTY":
|
||||
continue
|
||||
@@ -565,10 +603,13 @@ class MigrationFnModel:
|
||||
continue
|
||||
|
||||
root_object["avatar_toolkit_version"] = "0.2.1"
|
||||
updated_count += 1
|
||||
|
||||
logger.info(f"Updated Avatar Toolkit version for {updated_count} root objects")
|
||||
|
||||
|
||||
class Model:
|
||||
def __init__(self, root_obj):
|
||||
def __init__(self, root_obj: bpy.types.Object) -> None:
|
||||
if root_obj is None:
|
||||
raise ValueError("must be MMD ROOT type object")
|
||||
if root_obj.mmd_type != "ROOT":
|
||||
@@ -578,13 +619,15 @@ class Model:
|
||||
self.__rigid_grp: Optional[bpy.types.Object] = None
|
||||
self.__joint_grp: Optional[bpy.types.Object] = None
|
||||
self.__temporary_grp: Optional[bpy.types.Object] = None
|
||||
logger.debug(f"Model initialized with root object: {self.__root.name}")
|
||||
|
||||
@staticmethod
|
||||
def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False):
|
||||
def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False) -> 'Model':
|
||||
if obj_name is None:
|
||||
obj_name = name
|
||||
|
||||
context = FnContext.ensure_context()
|
||||
logger.info(f"Creating new MMD model: {name}")
|
||||
|
||||
root: bpy.types.Object = bpy.data.objects.new(name=obj_name, object_data=None)
|
||||
root.mmd_type = "ROOT"
|
||||
@@ -595,6 +638,7 @@ class Model:
|
||||
FnContext.link_object(context, root)
|
||||
|
||||
if armature_object:
|
||||
logger.debug(f"Using existing armature: {armature_object.name}")
|
||||
m = armature_object.matrix_world
|
||||
armature_object.parent_type = "OBJECT"
|
||||
armature_object.parent = root
|
||||
@@ -602,6 +646,7 @@ class Model:
|
||||
root.matrix_world = m
|
||||
armature_object.matrix_local.identity()
|
||||
else:
|
||||
logger.debug("Creating new armature")
|
||||
armature_object = bpy.data.objects.new(name=obj_name + "_arm", object_data=bpy.data.armatures.new(name=obj_name))
|
||||
armature_object.parent = root
|
||||
FnContext.link_object(context, armature_object)
|
||||
@@ -614,6 +659,7 @@ class Model:
|
||||
FnBone.setup_special_bone_collections(armature_object)
|
||||
|
||||
if add_root_bone:
|
||||
logger.debug("Adding root bone")
|
||||
bone_name = "全ての親"
|
||||
bone_name_english = "Root"
|
||||
|
||||
@@ -637,34 +683,37 @@ class Model:
|
||||
bone_collection.assign(data_bone)
|
||||
|
||||
FnContext.set_active_and_select_single_object(context, root)
|
||||
logger.info(f"Model created successfully: {name}")
|
||||
return Model(root)
|
||||
|
||||
@staticmethod
|
||||
def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
|
||||
return FnModel.find_root_object(obj)
|
||||
|
||||
def initialDisplayFrames(self, reset=True):
|
||||
def initialDisplayFrames(self, reset: bool = True) -> None:
|
||||
FnModel.initalize_display_item_frames(self.__root, reset=reset)
|
||||
|
||||
@property
|
||||
def morph_slider(self):
|
||||
def morph_slider(self) -> Any:
|
||||
return FnMorph.get_morph_slider(self)
|
||||
|
||||
def loadMorphs(self):
|
||||
def loadMorphs(self) -> None:
|
||||
logger.info(f"Loading morphs for model: {self.__root.name}")
|
||||
FnMorph.load_morphs(self)
|
||||
|
||||
def create_ik_constraint(self, bone, ik_target):
|
||||
def create_ik_constraint(self, bone: bpy.types.PoseBone, ik_target: bpy.types.PoseBone) -> bpy.types.KinematicConstraint:
|
||||
"""create IK constraint
|
||||
|
||||
Args:
|
||||
bone: A pose bone to add a IK constraint
|
||||
id_target: A pose bone for IK target
|
||||
ik_target: A pose bone for IK target
|
||||
|
||||
Returns:
|
||||
The bpy.types.KinematicConstraint object created. It is set target
|
||||
and subtarget options.
|
||||
|
||||
"""
|
||||
logger.debug(f"Creating IK constraint on {bone.name} targeting {ik_target.name}")
|
||||
ik_target_name = ik_target.name
|
||||
ik_const = bone.constraints.new("IK")
|
||||
ik_const.target = self.__arm
|
||||
@@ -693,6 +742,7 @@ class Model:
|
||||
if self.__rigid_grp is None:
|
||||
self.__rigid_grp = FnModel.find_rigid_group_object(self.__root)
|
||||
if self.__rigid_grp is None:
|
||||
logger.debug(f"Creating rigid group object for {self.__root.name}")
|
||||
rigids = bpy.data.objects.new(name="rigidbodies", object_data=None)
|
||||
FnContext.link_object(FnContext.ensure_context(), rigids)
|
||||
rigids.mmd_type = "RIGID_GRP_OBJ"
|
||||
@@ -710,6 +760,7 @@ class Model:
|
||||
if self.__joint_grp is None:
|
||||
self.__joint_grp = FnModel.find_joint_group_object(self.__root)
|
||||
if self.__joint_grp is None:
|
||||
logger.debug(f"Creating joint group object for {self.__root.name}")
|
||||
joints = bpy.data.objects.new(name="joints", object_data=None)
|
||||
FnContext.link_object(FnContext.ensure_context(), joints)
|
||||
joints.mmd_type = "JOINT_GRP_OBJ"
|
||||
@@ -727,6 +778,7 @@ class Model:
|
||||
if self.__temporary_grp is None:
|
||||
self.__temporary_grp = FnModel.find_temporary_group_object(self.__root)
|
||||
if self.__temporary_grp is None:
|
||||
logger.debug(f"Creating temporary group object for {self.__root.name}")
|
||||
temporarys = bpy.data.objects.new(name="temporary", object_data=None)
|
||||
FnContext.link_object(FnContext.ensure_context(), temporarys)
|
||||
temporarys.mmd_type = "TEMPORARY_GRP_OBJ"
|
||||
@@ -740,7 +792,7 @@ class Model:
|
||||
def meshes(self) -> Iterator[bpy.types.Object]:
|
||||
return FnModel.iterate_mesh_objects(self.__root)
|
||||
|
||||
def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True):
|
||||
def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True) -> None:
|
||||
FnModel.attach_mesh_objects(self.rootObject(), meshes, add_armature_modifier)
|
||||
|
||||
def firstMesh(self) -> Optional[bpy.types.Object]:
|
||||
@@ -748,7 +800,7 @@ class Model:
|
||||
return i
|
||||
return None
|
||||
|
||||
def findMesh(self, mesh_name) -> Optional[bpy.types.Object]:
|
||||
def findMesh(self, mesh_name: str) -> Optional[bpy.types.Object]:
|
||||
"""
|
||||
Helper method to find a mesh by name
|
||||
"""
|
||||
@@ -787,25 +839,26 @@ class Model:
|
||||
def joints(self) -> Iterator[bpy.types.Object]:
|
||||
return FnModel.iterate_joint_objects(self.__root)
|
||||
|
||||
def temporaryObjects(self, rigid_track_only=False) -> Iterator[bpy.types.Object]:
|
||||
def temporaryObjects(self, rigid_track_only: bool = False) -> Iterator[bpy.types.Object]:
|
||||
return FnModel.iterate_temporary_objects(self.__root, rigid_track_only)
|
||||
|
||||
def materials(self) -> Iterator[bpy.types.Material]:
|
||||
"""
|
||||
Helper method to list all materials in all meshes
|
||||
"""
|
||||
materials = {} # Use dict instead of set to guarantee preserve order
|
||||
materials: Dict[bpy.types.Material, int] = {} # Use dict instead of set to guarantee preserve order
|
||||
for mesh in self.meshes():
|
||||
materials.update((slot.material, 0) for slot in mesh.material_slots if slot.material is not None)
|
||||
return iter(materials.keys())
|
||||
|
||||
def renameBone(self, old_bone_name, new_bone_name):
|
||||
def renameBone(self, old_bone_name: str, new_bone_name: str) -> None:
|
||||
if old_bone_name == new_bone_name:
|
||||
return
|
||||
logger.info(f"Renaming bone: {old_bone_name} -> {new_bone_name}")
|
||||
armature = self.armature()
|
||||
bone = armature.pose.bones[old_bone_name]
|
||||
bone.name = new_bone_name
|
||||
new_bone_name = bone.name
|
||||
new_bone_name = bone.name # Get the actual name (might be adjusted by Blender)
|
||||
|
||||
mmd_root = self.rootObject().mmd_root
|
||||
for frame in mmd_root.display_item_frames:
|
||||
@@ -816,28 +869,31 @@ class Model:
|
||||
if old_bone_name in mesh.vertex_groups:
|
||||
mesh.vertex_groups[old_bone_name].name = new_bone_name
|
||||
|
||||
def build(self, non_collision_distance_scale=1.5, collision_margin=1e-06):
|
||||
def build(self, non_collision_distance_scale: float = 1.5, collision_margin: float = 1e-06) -> None:
|
||||
logger.info(f"Building physics rig for {self.__root.name}")
|
||||
rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
|
||||
if self.__root.mmd_root.is_built:
|
||||
logger.info("Model is already built, cleaning first")
|
||||
self.clean()
|
||||
self.__root.mmd_root.is_built = True
|
||||
logging.info("****************************************")
|
||||
logging.info(" Build rig")
|
||||
logging.info("****************************************")
|
||||
logger.info("****************************************")
|
||||
logger.info(" Build rig")
|
||||
logger.info("****************************************")
|
||||
start_time = time.time()
|
||||
self.__preBuild()
|
||||
self.disconnectPhysicsBones()
|
||||
self.buildRigids(non_collision_distance_scale, collision_margin)
|
||||
self.buildJoints()
|
||||
self.__postBuild()
|
||||
logging.info(" Finished building in %f seconds.", time.time() - start_time)
|
||||
logger.info(" Finished building in %f seconds.", time.time() - start_time)
|
||||
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled)
|
||||
|
||||
def clean(self):
|
||||
def clean(self) -> None:
|
||||
logger.info(f"Cleaning physics rig for {self.__root.name}")
|
||||
rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False)
|
||||
logging.info("****************************************")
|
||||
logging.info(" Clean rig")
|
||||
logging.info("****************************************")
|
||||
logger.info("****************************************")
|
||||
logger.info(" Clean rig")
|
||||
logger.info("****************************************")
|
||||
start_time = time.time()
|
||||
|
||||
pose_bones = []
|
||||
@@ -848,13 +904,14 @@ class Model:
|
||||
if "mmd_tools_rigid_track" in i.constraints:
|
||||
const = i.constraints["mmd_tools_rigid_track"]
|
||||
i.constraints.remove(const)
|
||||
logger.debug(f"Removed rigid track constraint from {i.name}")
|
||||
|
||||
rigid_track_counts = 0
|
||||
for i in self.rigidBodies():
|
||||
rigid_type = int(i.mmd_rigid.type)
|
||||
if "mmd_tools_rigid_parent" not in i.constraints:
|
||||
rigid_track_counts += 1
|
||||
logging.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name)
|
||||
logger.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name)
|
||||
i.mmd_rigid.bone = i.mmd_rigid.bone
|
||||
relation = i.constraints["mmd_tools_rigid_parent"]
|
||||
relation.mute = True
|
||||
@@ -884,35 +941,39 @@ class Model:
|
||||
mmd_root = self.rootObject().mmd_root
|
||||
if mmd_root.show_temporary_objects:
|
||||
mmd_root.show_temporary_objects = False
|
||||
logging.info(" Finished cleaning in %f seconds.", time.time() - start_time)
|
||||
logger.info(" Finished cleaning in %f seconds.", time.time() - start_time)
|
||||
mmd_root.is_built = False
|
||||
rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled)
|
||||
|
||||
def __removeTemporaryObjects(self):
|
||||
def __removeTemporaryObjects(self) -> None:
|
||||
logger.debug("Removing temporary objects")
|
||||
with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()):
|
||||
bpy.ops.object.delete()
|
||||
|
||||
def __restoreTransforms(self, obj):
|
||||
def __restoreTransforms(self, obj: bpy.types.Object) -> None:
|
||||
for attr in ("location", "rotation_euler"):
|
||||
attr_name = "__backup_%s__" % attr
|
||||
val = obj.get(attr_name, None)
|
||||
if val is not None:
|
||||
setattr(obj, attr, val)
|
||||
del obj[attr_name]
|
||||
logger.debug(f"Restored {attr} for {obj.name}")
|
||||
|
||||
def __backupTransforms(self, obj):
|
||||
def __backupTransforms(self, obj: bpy.types.Object) -> None:
|
||||
for attr in ("location", "rotation_euler"):
|
||||
attr_name = "__backup_%s__" % attr
|
||||
if attr_name in obj: # should not happen in normal build/clean cycle
|
||||
continue
|
||||
obj[attr_name] = getattr(obj, attr, None)
|
||||
logger.debug(f"Backed up {attr} for {obj.name}")
|
||||
|
||||
def __preBuild(self):
|
||||
self.__fake_parent_map = {}
|
||||
self.__rigid_body_matrix_map = {}
|
||||
self.__empty_parent_map = {}
|
||||
def __preBuild(self) -> None:
|
||||
logger.debug("Pre-build preparation")
|
||||
self.__fake_parent_map: Dict[bpy.types.Object, List[bpy.types.Object]] = {}
|
||||
self.__rigid_body_matrix_map: Dict[bpy.types.Object, Any] = {}
|
||||
self.__empty_parent_map: Dict[bpy.types.Object, bpy.types.Object] = {}
|
||||
|
||||
no_parents = []
|
||||
no_parents: List[bpy.types.Object] = []
|
||||
for i in self.rigidBodies():
|
||||
self.__backupTransforms(i)
|
||||
# mute relation
|
||||
@@ -932,7 +993,7 @@ class Model:
|
||||
# update changes of armature constraints
|
||||
bpy.context.scene.frame_set(bpy.context.scene.frame_current)
|
||||
|
||||
parented = []
|
||||
parented: List[bpy.types.Object] = []
|
||||
for i in self.joints():
|
||||
self.__backupTransforms(i)
|
||||
rbc = i.rigid_body_constraint
|
||||
@@ -950,7 +1011,8 @@ class Model:
|
||||
|
||||
# assert(len(no_parents) == len(parented))
|
||||
|
||||
def __postBuild(self):
|
||||
def __postBuild(self) -> None:
|
||||
logger.debug("Post-build finalization")
|
||||
self.__fake_parent_map = None
|
||||
self.__rigid_body_matrix_map = None
|
||||
|
||||
@@ -962,6 +1024,7 @@ class Model:
|
||||
matrix_world = empty.matrix_world
|
||||
empty.parent = rigid_obj
|
||||
empty.matrix_world = matrix_world
|
||||
logger.debug(f"Parented empty {empty.name} to rigid object {rigid_obj.name}")
|
||||
self.__empty_parent_map = None
|
||||
|
||||
arm = self.armature()
|
||||
@@ -970,11 +1033,13 @@ class Model:
|
||||
c = p_bone.constraints.get("mmd_tools_rigid_track", None)
|
||||
if c:
|
||||
c.mute = False
|
||||
logger.debug(f"Enabled rigid track constraint for {p_bone.name}")
|
||||
|
||||
def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float):
|
||||
def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float) -> None:
|
||||
assert rigid_obj.mmd_type == "RIGID_BODY"
|
||||
rb = rigid_obj.rigid_body
|
||||
if rb is None:
|
||||
logger.warning(f"No rigid body for {rigid_obj.name}")
|
||||
return
|
||||
|
||||
rigid = rigid_obj.mmd_rigid
|
||||
@@ -1018,7 +1083,7 @@ class Model:
|
||||
fake_children = self.__fake_parent_map.get(rigid_obj, None)
|
||||
if fake_children:
|
||||
for fake_child in fake_children:
|
||||
logging.debug(" - fake_child: %s", fake_child.name)
|
||||
logger.debug(" - fake_child: %s", fake_child.name)
|
||||
t, r, s = (m @ fake_child.matrix_local).decompose()
|
||||
fake_child.location = t
|
||||
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode)
|
||||
@@ -1032,7 +1097,7 @@ class Model:
|
||||
fake_children = self.__fake_parent_map.get(rigid_obj, None)
|
||||
if fake_children:
|
||||
for fake_child in fake_children:
|
||||
logging.debug(" - fake_child: %s", fake_child.name)
|
||||
logger.debug(" - fake_child: %s", fake_child.name)
|
||||
t, r, s = (m @ fake_child.matrix_local).decompose()
|
||||
fake_child.location = t
|
||||
fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode)
|
||||
@@ -1062,7 +1127,7 @@ class Model:
|
||||
ori_rigid_obj = self.__empty_parent_map[empty]
|
||||
ori_rb = ori_rigid_obj.rigid_body
|
||||
if ori_rb and rb.mass > ori_rb.mass:
|
||||
logging.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name)
|
||||
logger.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name)
|
||||
# re-parenting
|
||||
rigid_obj.mmd_rigid.bone = bone_name
|
||||
rigid_obj.constraints.remove(relation)
|
||||
@@ -1070,21 +1135,22 @@ class Model:
|
||||
# revert change
|
||||
ori_rigid_obj.mmd_rigid.bone = bone_name
|
||||
else:
|
||||
logging.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name)
|
||||
logger.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name)
|
||||
|
||||
rb.collision_shape = rigid.shape
|
||||
logger.debug(f"Updated rigid body {rigid_obj.name} with type {rigid_type}")
|
||||
|
||||
def __getRigidRange(self, obj):
|
||||
def __getRigidRange(self, obj: bpy.types.Object) -> float:
|
||||
return (Vector(obj.bound_box[0]) - Vector(obj.bound_box[6])).length
|
||||
|
||||
def __createNonCollisionConstraint(self, nonCollisionJointTable):
|
||||
def __createNonCollisionConstraint(self, nonCollisionJointTable: List[Tuple[bpy.types.Object, bpy.types.Object]]) -> None:
|
||||
total_len = len(nonCollisionJointTable)
|
||||
if total_len < 1:
|
||||
return
|
||||
|
||||
start_time = time.time()
|
||||
logging.debug("-" * 60)
|
||||
logging.debug(" creating ncc, counts: %d", total_len)
|
||||
logger.debug("-" * 60)
|
||||
logger.debug(" creating ncc, counts: %d", total_len)
|
||||
|
||||
ncc_obj = bpyutils.createObject(name="ncc", object_data=None)
|
||||
ncc_obj.location = [0, 0, 0]
|
||||
@@ -1099,26 +1165,26 @@ class Model:
|
||||
rb.disable_collisions = True
|
||||
|
||||
ncc_objs = bpyutils.duplicateObject(ncc_obj, total_len)
|
||||
logging.debug(" created %d ncc.", len(ncc_objs))
|
||||
logger.debug(" created %d ncc.", len(ncc_objs))
|
||||
|
||||
for ncc_obj, pair in zip(ncc_objs, nonCollisionJointTable):
|
||||
rbc = ncc_obj.rigid_body_constraint
|
||||
rbc.object1, rbc.object2 = pair
|
||||
ncc_obj.hide_set(True)
|
||||
ncc_obj.hide_select = True
|
||||
logging.debug(" finish in %f seconds.", time.time() - start_time)
|
||||
logging.debug("-" * 60)
|
||||
logger.debug(" finish in %f seconds.", time.time() - start_time)
|
||||
logger.debug("-" * 60)
|
||||
|
||||
def buildRigids(self, non_collision_distance_scale, collision_margin):
|
||||
logging.debug("--------------------------------")
|
||||
logging.debug(" Build riggings of rigid bodies")
|
||||
logging.debug("--------------------------------")
|
||||
def buildRigids(self, non_collision_distance_scale: float, collision_margin: float) -> List[bpy.types.Object]:
|
||||
logger.debug("--------------------------------")
|
||||
logger.debug(" Build riggings of rigid bodies")
|
||||
logger.debug("--------------------------------")
|
||||
rigid_objects = list(self.rigidBodies())
|
||||
rigid_object_groups = [[] for i in range(16)]
|
||||
rigid_object_groups: List[List[bpy.types.Object]] = [[] for i in range(16)]
|
||||
for i in rigid_objects:
|
||||
rigid_object_groups[i.mmd_rigid.collision_group_number].append(i)
|
||||
|
||||
jointMap = {}
|
||||
jointMap: Dict[frozenset, bpy.types.Object] = {}
|
||||
for joint in self.joints():
|
||||
rbc = joint.rigid_body_constraint
|
||||
if rbc is None:
|
||||
@@ -1126,10 +1192,10 @@ class Model:
|
||||
rbc.disable_collisions = False
|
||||
jointMap[frozenset((rbc.object1, rbc.object2))] = joint
|
||||
|
||||
logging.info("Creating non collision constraints")
|
||||
logger.info("Creating non collision constraints")
|
||||
# create non collision constraints
|
||||
nonCollisionJointTable = []
|
||||
non_collision_pairs = set()
|
||||
nonCollisionJointTable: List[Tuple[bpy.types.Object, bpy.types.Object]] = []
|
||||
non_collision_pairs: Set[frozenset] = set()
|
||||
rigid_object_cnt = len(rigid_objects)
|
||||
for obj_a in rigid_objects:
|
||||
for n, ignore in enumerate(obj_a.mmd_rigid.collision_group_mask):
|
||||
@@ -1150,12 +1216,13 @@ class Model:
|
||||
nonCollisionJointTable.append((obj_a, obj_b))
|
||||
non_collision_pairs.add(pair)
|
||||
for cnt, i in enumerate(rigid_objects):
|
||||
logging.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name)
|
||||
logger.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name)
|
||||
self.updateRigid(i, collision_margin)
|
||||
self.__createNonCollisionConstraint(nonCollisionJointTable)
|
||||
return rigid_objects
|
||||
|
||||
def buildJoints(self):
|
||||
def buildJoints(self) -> None:
|
||||
logger.info("Building joints")
|
||||
for i in self.joints():
|
||||
rbc = i.rigid_body_constraint
|
||||
if rbc is None:
|
||||
@@ -1168,8 +1235,9 @@ class Model:
|
||||
t, r, s = (m @ i.matrix_local).decompose()
|
||||
i.location = t
|
||||
i.rotation_euler = r.to_euler(i.rotation_mode)
|
||||
logger.debug(f"Built joint: {i.name}")
|
||||
|
||||
def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]):
|
||||
def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]) -> None:
|
||||
armature_object = self.armature()
|
||||
|
||||
armature: bpy.types.Armature
|
||||
@@ -1177,7 +1245,7 @@ class Model:
|
||||
edit_bones = armature.edit_bones
|
||||
rigid_body_object: bpy.types.Object
|
||||
for rigid_body_object in self.rigidBodies():
|
||||
mmd_rigid: MMDRigidBody = rigid_body_object.mmd_rigid
|
||||
mmd_rigid: 'MMDRigidBody' = rigid_body_object.mmd_rigid
|
||||
if mmd_rigid.type not in target_modes:
|
||||
continue
|
||||
|
||||
@@ -1188,21 +1256,25 @@ class Model:
|
||||
|
||||
editor(edit_bone)
|
||||
|
||||
def disconnectPhysicsBones(self):
|
||||
def editor(edit_bone: bpy.types.EditBone):
|
||||
def disconnectPhysicsBones(self) -> None:
|
||||
logger.info("Disconnecting physics bones")
|
||||
def editor(edit_bone: bpy.types.EditBone) -> None:
|
||||
rna_prop_ui.rna_idprop_ui_create(edit_bone, "mmd_bone_use_connect", default=edit_bone.use_connect)
|
||||
edit_bone.use_connect = False
|
||||
logger.debug(f"Disconnected bone: {edit_bone.name}")
|
||||
|
||||
self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)})
|
||||
|
||||
def connectPhysicsBones(self):
|
||||
def editor(edit_bone: bpy.types.EditBone):
|
||||
def connectPhysicsBones(self) -> None:
|
||||
logger.info("Connecting physics bones")
|
||||
def editor(edit_bone: bpy.types.EditBone) -> None:
|
||||
mmd_bone_use_connect_str: Optional[str] = edit_bone.get("mmd_bone_use_connect")
|
||||
if mmd_bone_use_connect_str is None:
|
||||
return
|
||||
|
||||
if not edit_bone.use_connect: # wasn't it overwritten?
|
||||
edit_bone.use_connect = bool(mmd_bone_use_connect_str)
|
||||
logger.debug(f"Connected bone: {edit_bone.name}")
|
||||
del edit_bone["mmd_bone_use_connect"]
|
||||
|
||||
self.__editPhysicsBones(editor, {str(MODE_STATIC), str(MODE_DYNAMIC), str(MODE_DYNAMIC_BONE)})
|
||||
|
||||
+61
-59
@@ -5,33 +5,35 @@
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Tuple, cast
|
||||
from typing import TYPE_CHECKING, Tuple, cast, List, Dict, Optional, Set, Any, Union, Iterator
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
from bpy.types import Object, ShapeKey, Material, Mesh, Armature, PoseBone, Constraint
|
||||
|
||||
from .. import bpyutils, utils
|
||||
from ..bpyutils import FnContext, FnObject, TransformConstraintOp
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .model import Model
|
||||
|
||||
|
||||
class FnMorph:
|
||||
def __init__(self, morph, model: "Model"):
|
||||
def __init__(self, morph: Any, model: "Model"):
|
||||
self.__morph = morph
|
||||
self.__rig = model
|
||||
|
||||
@classmethod
|
||||
def storeShapeKeyOrder(cls, obj, shape_key_names):
|
||||
def storeShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
|
||||
if len(shape_key_names) < 1:
|
||||
return
|
||||
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
|
||||
if obj.data.shape_keys is None:
|
||||
bpy.ops.object.shape_key_add()
|
||||
|
||||
def __move_to_bottom(key_blocks, name):
|
||||
def __move_to_bottom(key_blocks: bpy.types.bpy_prop_collection, name: str) -> None:
|
||||
obj.active_shape_key_index = key_blocks.find(name)
|
||||
bpy.ops.object.shape_key_move(type="BOTTOM")
|
||||
|
||||
@@ -43,7 +45,7 @@ class FnMorph:
|
||||
__move_to_bottom(key_blocks, name)
|
||||
|
||||
@classmethod
|
||||
def fixShapeKeyOrder(cls, obj, shape_key_names):
|
||||
def fixShapeKeyOrder(cls, obj: Object, shape_key_names: List[str]) -> None:
|
||||
if len(shape_key_names) < 1:
|
||||
return
|
||||
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
|
||||
@@ -58,11 +60,11 @@ class FnMorph:
|
||||
bpy.ops.object.shape_key_move(type="BOTTOM")
|
||||
|
||||
@staticmethod
|
||||
def get_morph_slider(rig):
|
||||
def get_morph_slider(rig: "Model") -> "_MorphSlider":
|
||||
return _MorphSlider(rig)
|
||||
|
||||
@staticmethod
|
||||
def category_guess(morph):
|
||||
def category_guess(morph: Any) -> None:
|
||||
name_lower = morph.name.lower()
|
||||
if "mouth" in name_lower:
|
||||
morph.category = "MOUTH"
|
||||
@@ -73,7 +75,7 @@ class FnMorph:
|
||||
morph.category = "EYE"
|
||||
|
||||
@classmethod
|
||||
def load_morphs(cls, rig):
|
||||
def load_morphs(cls, rig: "Model") -> None:
|
||||
mmd_root = rig.rootObject().mmd_root
|
||||
vertex_morphs = mmd_root.vertex_morphs
|
||||
uv_morphs = mmd_root.uv_morphs
|
||||
@@ -92,7 +94,7 @@ class FnMorph:
|
||||
cls.category_guess(item)
|
||||
|
||||
@staticmethod
|
||||
def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str):
|
||||
def remove_shape_key(mesh_object: Object, shape_key_name: str) -> None:
|
||||
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
||||
|
||||
shape_keys = mesh_object.data.shape_keys
|
||||
@@ -104,7 +106,7 @@ class FnMorph:
|
||||
FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name])
|
||||
|
||||
@staticmethod
|
||||
def copy_shape_key(mesh_object: bpy.types.Object, src_name: str, dest_name: str):
|
||||
def copy_shape_key(mesh_object: Object, src_name: str, dest_name: str) -> None:
|
||||
assert isinstance(mesh_object.data, bpy.types.Mesh)
|
||||
|
||||
shape_keys = mesh_object.data.shape_keys
|
||||
@@ -126,13 +128,13 @@ class FnMorph:
|
||||
mesh_object.active_shape_key_index = key_blocks.find(dest_name)
|
||||
|
||||
@staticmethod
|
||||
def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes="XYZW"):
|
||||
def get_uv_morph_vertex_groups(obj: Object, morph_name: Optional[str] = None, offset_axes: str = "XYZW") -> Iterator[Tuple[bpy.types.VertexGroup, str, str]]:
|
||||
pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW")
|
||||
# yield (vertex_group, morph_name, axis),...
|
||||
return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name))
|
||||
|
||||
@staticmethod
|
||||
def copy_uv_morph_vertex_groups(obj, src_name, dest_name):
|
||||
def copy_uv_morph_vertex_groups(obj: Object, src_name: str, dest_name: str) -> None:
|
||||
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
|
||||
obj.vertex_groups.remove(vg)
|
||||
|
||||
@@ -143,12 +145,12 @@ class FnMorph:
|
||||
obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name)
|
||||
|
||||
@staticmethod
|
||||
def overwrite_bone_morphs_from_action_pose(armature_object):
|
||||
def overwrite_bone_morphs_from_action_pose(armature_object: Object) -> None:
|
||||
armature = armature_object.id_data
|
||||
|
||||
# Use animation_data and action instead of action_pose
|
||||
if armature.animation_data is None or armature.animation_data.action is None:
|
||||
logging.warning('[WARNING] armature "%s" has no animation data or action', armature_object.name)
|
||||
logger.warning('Armature "%s" has no animation data or action', armature_object.name)
|
||||
return
|
||||
|
||||
action = armature.animation_data.action
|
||||
@@ -187,9 +189,9 @@ class FnMorph:
|
||||
utils.selectAObject(root)
|
||||
|
||||
@staticmethod
|
||||
def clean_uv_morph_vertex_groups(obj):
|
||||
def clean_uv_morph_vertex_groups(obj: Object) -> None:
|
||||
# remove empty vertex groups of uv morphs
|
||||
vg_indices = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
|
||||
vg_indices: Set[int] = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
|
||||
vertex_groups = obj.vertex_groups
|
||||
for v in obj.data.vertices:
|
||||
for x in v.groups:
|
||||
@@ -203,8 +205,8 @@ class FnMorph:
|
||||
vertex_groups.remove(vg)
|
||||
|
||||
@staticmethod
|
||||
def get_uv_morph_offset_map(obj, morph):
|
||||
offset_map = {} # offset_map[vertex_index] = offset_xyzw
|
||||
def get_uv_morph_offset_map(obj: Object, morph: Any) -> Dict[int, List[float]]:
|
||||
offset_map: Dict[int, List[float]] = {} # offset_map[vertex_index] = offset_xyzw
|
||||
if morph.data_type == "VERTEX_GROUP":
|
||||
scale = morph.vertex_group_scale
|
||||
axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)}
|
||||
@@ -225,7 +227,7 @@ class FnMorph:
|
||||
return offset_map
|
||||
|
||||
@staticmethod
|
||||
def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"):
|
||||
def store_uv_morph_data(obj: Object, morph: Any, offsets: Optional[List[Any]] = None, offset_axes: str = "XYZW") -> None:
|
||||
vertex_groups = obj.vertex_groups
|
||||
morph_name = getattr(morph, "name", None)
|
||||
if offset_axes:
|
||||
@@ -250,7 +252,7 @@ class FnMorph:
|
||||
vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name)
|
||||
vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE")
|
||||
|
||||
def update_mat_related_mesh(self, new_mesh=None):
|
||||
def update_mat_related_mesh(self, new_mesh: Optional[Object] = None) -> None:
|
||||
for offset in self.__morph.data:
|
||||
# Use the new_mesh if provided
|
||||
meshObj = new_mesh
|
||||
@@ -270,11 +272,11 @@ class FnMorph:
|
||||
offset.related_mesh = meshObj.data.name
|
||||
|
||||
@staticmethod
|
||||
def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object):
|
||||
def clean_duplicated_material_morphs(mmd_root_object: Object) -> None:
|
||||
"""Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]"""
|
||||
mmd_root = mmd_root_object.mmd_root
|
||||
|
||||
def morph_data_equals(l, r) -> bool:
|
||||
def morph_data_equals(l: Any, r: Any) -> bool:
|
||||
return (
|
||||
l.related_mesh_data == r.related_mesh_data
|
||||
and l.offset_type == r.offset_type
|
||||
@@ -290,7 +292,7 @@ class FnMorph:
|
||||
and all(a == b for a, b in zip(l.toon_texture_factor, r.toon_texture_factor))
|
||||
)
|
||||
|
||||
def morph_equals(l, r) -> bool:
|
||||
def morph_equals(l: Any, r: Any) -> bool:
|
||||
return len(l.data) == len(r.data) and all(morph_data_equals(a, b) for a, b in zip(l.data, r.data))
|
||||
|
||||
# Remove duplicated mmd_root.material_morphs.data[]
|
||||
@@ -325,7 +327,7 @@ class _MorphSlider:
|
||||
def __init__(self, model: "Model"):
|
||||
self.__rig = model
|
||||
|
||||
def placeholder(self, create=False, binded=False):
|
||||
def placeholder(self, create: bool = False, binded: bool = False) -> Optional[Object]:
|
||||
rig = self.__rig
|
||||
root = rig.rootObject()
|
||||
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None)
|
||||
@@ -343,11 +345,11 @@ class _MorphSlider:
|
||||
return obj
|
||||
|
||||
@property
|
||||
def dummy_armature(self):
|
||||
def dummy_armature(self) -> Optional[Object]:
|
||||
obj = self.placeholder()
|
||||
return self.__dummy_armature(obj) if obj else None
|
||||
|
||||
def __dummy_armature(self, obj, create=False):
|
||||
def __dummy_armature(self, obj: Object, create: bool = False) -> Optional[Object]:
|
||||
arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None)
|
||||
if create and arm is None:
|
||||
arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature"))
|
||||
@@ -360,7 +362,7 @@ class _MorphSlider:
|
||||
FnBone.setup_special_bone_collections(arm)
|
||||
return arm
|
||||
|
||||
def get(self, morph_name):
|
||||
def get(self, morph_name: str) -> Optional[ShapeKey]:
|
||||
obj = self.placeholder()
|
||||
if obj is None:
|
||||
return None
|
||||
@@ -369,13 +371,13 @@ class _MorphSlider:
|
||||
return None
|
||||
return key_blocks.get(morph_name, None)
|
||||
|
||||
def create(self):
|
||||
def create(self) -> Object:
|
||||
self.__rig.loadMorphs()
|
||||
obj = self.placeholder(create=True)
|
||||
self.__load(obj, self.__rig.rootObject().mmd_root)
|
||||
return obj
|
||||
|
||||
def __load(self, obj, mmd_root):
|
||||
def __load(self, obj: Object, mmd_root: Any) -> None:
|
||||
attr_list = ("group", "vertex", "bone", "uv", "material")
|
||||
morph_sliders = obj.data.shape_keys.key_blocks
|
||||
for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())):
|
||||
@@ -386,7 +388,7 @@ class _MorphSlider:
|
||||
obj.shape_key_add(name=name, from_mix=False)
|
||||
|
||||
@staticmethod
|
||||
def __driver_variables(id_data, path, index=-1):
|
||||
def __driver_variables(id_data: Any, path: str, index: int = -1) -> Tuple[Any, Any]:
|
||||
d = id_data.driver_add(path, index)
|
||||
variables = d.driver.variables
|
||||
for x in variables:
|
||||
@@ -394,7 +396,7 @@ class _MorphSlider:
|
||||
return d.driver, variables
|
||||
|
||||
@staticmethod
|
||||
def __add_single_prop(variables, id_obj, data_path, prefix):
|
||||
def __add_single_prop(variables: Any, id_obj: Object, data_path: str, prefix: str) -> Any:
|
||||
var = variables.new()
|
||||
var.name = f"{prefix}{len(variables)}"
|
||||
var.type = "SINGLE_PROP"
|
||||
@@ -405,7 +407,7 @@ class _MorphSlider:
|
||||
return var
|
||||
|
||||
@staticmethod
|
||||
def __shape_key_driver_check(key_block, resolve_path=False):
|
||||
def __shape_key_driver_check(key_block: ShapeKey, resolve_path: bool = False) -> bool:
|
||||
if resolve_path:
|
||||
try:
|
||||
key_block.id_data.path_resolve(key_block.path_from_id())
|
||||
@@ -419,7 +421,7 @@ class _MorphSlider:
|
||||
d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None)
|
||||
return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables)))
|
||||
|
||||
def __cleanup(self, names_in_use=None):
|
||||
def __cleanup(self, names_in_use: Optional[Dict[str, Any]] = None) -> None:
|
||||
from math import ceil, floor
|
||||
|
||||
names_in_use = names_in_use or {}
|
||||
@@ -427,7 +429,7 @@ class _MorphSlider:
|
||||
morph_sliders = self.placeholder()
|
||||
morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {}
|
||||
for mesh_object in rig.meshes():
|
||||
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[bpy.types.ShapeKey], ())):
|
||||
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[ShapeKey], ())):
|
||||
if kb.name in names_in_use:
|
||||
continue
|
||||
|
||||
@@ -465,7 +467,7 @@ class _MorphSlider:
|
||||
c.driver_remove(attr)
|
||||
b.constraints.remove(c)
|
||||
|
||||
def unbind(self):
|
||||
def unbind(self) -> None:
|
||||
mmd_root = self.__rig.rootObject().mmd_root
|
||||
|
||||
# after unbind, the weird lag problem will disappear.
|
||||
@@ -488,7 +490,7 @@ class _MorphSlider:
|
||||
b.driver_remove("rotation_quaternion")
|
||||
self.__cleanup()
|
||||
|
||||
def bind(self):
|
||||
def bind(self) -> None:
|
||||
rig = self.__rig
|
||||
root = rig.rootObject()
|
||||
armObj = rig.armature()
|
||||
@@ -502,10 +504,10 @@ class _MorphSlider:
|
||||
morph_sliders = obj.data.shape_keys.key_blocks
|
||||
|
||||
# data gathering
|
||||
group_map = {}
|
||||
group_map: Dict[Tuple[str, str], List[List[Any]]] = {}
|
||||
|
||||
shape_key_map = {}
|
||||
uv_morph_map = {}
|
||||
shape_key_map: Dict[str, List[Tuple[ShapeKey, str, List[Any]]]] = {}
|
||||
uv_morph_map: Dict[str, List[Tuple[str, str, str, List[Any]]]] = {}
|
||||
for mesh_object in rig.meshes():
|
||||
mesh_object.show_only_shape_key = False
|
||||
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
|
||||
@@ -526,7 +528,7 @@ class _MorphSlider:
|
||||
kb_bind.slider_max = 10
|
||||
|
||||
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"')
|
||||
groups = []
|
||||
groups: List[Any] = []
|
||||
shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
|
||||
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
|
||||
|
||||
@@ -542,7 +544,7 @@ class _MorphSlider:
|
||||
continue
|
||||
|
||||
name_bind = "mmd_bind%s" % hash(vg.name)
|
||||
uv_morph_map.setdefault(name_bind, ())
|
||||
uv_morph_map.setdefault(name_bind, [])
|
||||
mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP")
|
||||
mod.show_expanded = False
|
||||
mod.vertex_group = vg.name
|
||||
@@ -555,13 +557,13 @@ class _MorphSlider:
|
||||
else:
|
||||
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base"
|
||||
|
||||
bone_offset_map = {}
|
||||
bone_offset_map: Dict[str, Tuple[str, Any, str, str, List[Any]]] = {}
|
||||
with bpyutils.edit_object(arm) as data:
|
||||
from .bone import FnBone
|
||||
|
||||
edit_bones = data.edit_bones
|
||||
|
||||
def __get_bone(name, parent):
|
||||
def __get_bone(name: str, parent: Optional[bpy.types.EditBone]) -> bpy.types.EditBone:
|
||||
b = edit_bones.get(name, None) or edit_bones.new(name=name)
|
||||
b.head = (0, 0, 0)
|
||||
b.tail = (0, 0, 1)
|
||||
@@ -578,7 +580,7 @@ class _MorphSlider:
|
||||
continue
|
||||
d.name = name_bind = f"mmd_bind{hash(d)}"
|
||||
b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None))
|
||||
groups = []
|
||||
groups: List[Any] = []
|
||||
bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
|
||||
group_map.setdefault(("bone_morphs", m.name), []).append(groups)
|
||||
|
||||
@@ -589,21 +591,21 @@ class _MorphSlider:
|
||||
scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale'
|
||||
name_bind = f"mmd_bind{hash(m.name)}"
|
||||
b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base))
|
||||
groups = []
|
||||
groups: List[Any] = []
|
||||
uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups))
|
||||
group_map.setdefault(("uv_morphs", m.name), []).append(groups)
|
||||
|
||||
used_bone_names = bone_offset_map.keys() | uv_morph_map.keys()
|
||||
used_bone_names: Set[str] = set(bone_offset_map.keys()) | set(uv_morph_map.keys())
|
||||
used_bone_names.add(ctrl_base.name)
|
||||
for b in edit_bones: # cleanup
|
||||
if b.name.startswith("mmd_bind") and b.name not in used_bone_names:
|
||||
edit_bones.remove(b)
|
||||
|
||||
material_offset_map = {}
|
||||
material_offset_map: Dict[str, Any] = {}
|
||||
for m in mmd_root.material_morphs:
|
||||
morph_name = m.name.replace('"', '\\"')
|
||||
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
|
||||
groups = []
|
||||
groups: List[Any] = []
|
||||
group_map.setdefault(("material_morphs", m.name), []).append(groups)
|
||||
material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups)
|
||||
for d in m.data:
|
||||
@@ -614,7 +616,7 @@ class _MorphSlider:
|
||||
|
||||
for m in mmd_root.group_morphs:
|
||||
if len(m.data) != len(set(m.data.keys())):
|
||||
logging.warning(' * Found duplicated morph data in Group Morph "%s"', m.name)
|
||||
logger.warning('Found duplicated morph data in Group Morph "%s"', m.name)
|
||||
morph_name = m.name.replace('"', '\\"')
|
||||
morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
|
||||
for d in m.data:
|
||||
@@ -625,7 +627,7 @@ class _MorphSlider:
|
||||
|
||||
self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys())
|
||||
|
||||
def __config_groups(variables, expression, groups):
|
||||
def __config_groups(variables: Any, expression: str, groups: List[Any]) -> str:
|
||||
for g_name, morph_path, factor_path in groups:
|
||||
var = self.__add_single_prop(variables, obj, morph_path, "g")
|
||||
fvar = self.__add_single_prop(variables, root, factor_path, "w")
|
||||
@@ -644,7 +646,7 @@ class _MorphSlider:
|
||||
kb_bind.mute = False
|
||||
|
||||
# bone morphs
|
||||
def __config_bone_morph(constraints, map_type, attributes, val, val_str):
|
||||
def __config_bone_morph(constraints: bpy.types.ArmatureConstraints, map_type: str, attributes: Set[str], val: float, val_str: str) -> None:
|
||||
c_name = f"mmd_bind{hash(data)}.{map_type[:3]}"
|
||||
c = TransformConstraintOp.create(constraints, c_name, map_type)
|
||||
TransformConstraintOp.update_min_max(c, val, None)
|
||||
@@ -692,7 +694,7 @@ class _MorphSlider:
|
||||
|
||||
group_dict = material_offset_map.get("group_dict", {})
|
||||
|
||||
def __config_material_morph(mat, morph_list):
|
||||
def __config_material_morph(mat: Material, morph_list: List[Tuple[str, Any, str]]) -> None:
|
||||
nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list))
|
||||
for (morph_name, data, name_bind), node in zip(morph_list, nodes):
|
||||
node.label, node.name = morph_name, name_bind
|
||||
@@ -704,7 +706,7 @@ class _MorphSlider:
|
||||
for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")):
|
||||
mul_all, add_all = material_offset_map.get("#", ([], []))
|
||||
if mat.name == "":
|
||||
logging.warning("Oh no. The material name should never empty.")
|
||||
logger.warning("Oh no. The material name should never be empty.")
|
||||
mul_list, add_list = [], []
|
||||
else:
|
||||
mat_name = "#" + mat.name
|
||||
@@ -720,7 +722,7 @@ class _MorphSlider:
|
||||
|
||||
class MigrationFnMorph:
|
||||
@staticmethod
|
||||
def update_mmd_morph():
|
||||
def update_mmd_morph() -> None:
|
||||
from .material import FnMaterial
|
||||
|
||||
for root in bpy.data.objects:
|
||||
@@ -762,11 +764,11 @@ class MigrationFnMorph:
|
||||
morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
|
||||
|
||||
@staticmethod
|
||||
def ensure_material_id_not_conflict():
|
||||
mat_ids_set = set()
|
||||
def ensure_material_id_not_conflict() -> None:
|
||||
mat_ids_set: Set[int] = set()
|
||||
|
||||
# The reference library properties cannot be modified and bypassed in advance.
|
||||
need_update_mat = []
|
||||
need_update_mat: List[Material] = []
|
||||
for mat in bpy.data.materials:
|
||||
if mat.mmd_material.material_id < 0:
|
||||
continue
|
||||
@@ -781,7 +783,7 @@ class MigrationFnMorph:
|
||||
mat_ids_set.add(mat.mmd_material.material_id)
|
||||
|
||||
@staticmethod
|
||||
def compatible_with_old_version_mmd_tools():
|
||||
def compatible_with_old_version_mmd_tools() -> None:
|
||||
MigrationFnMorph.ensure_material_id_not_conflict()
|
||||
|
||||
for root in bpy.data.objects:
|
||||
|
||||
+447
-166
File diff suppressed because it is too large
Load Diff
+35
-12
@@ -5,12 +5,13 @@
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Tuple, Union, Dict, Any, Set, cast
|
||||
|
||||
import bpy
|
||||
from mathutils import Euler, Vector
|
||||
from mathutils import Euler, Vector, Matrix
|
||||
|
||||
from ..bpyutils import FnContext, Props
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
SHAPE_SPHERE = 0
|
||||
SHAPE_BOX = 1
|
||||
@@ -21,25 +22,30 @@ MODE_DYNAMIC = 1
|
||||
MODE_DYNAMIC_BONE = 2
|
||||
|
||||
|
||||
def shapeType(collision_shape):
|
||||
def shapeType(collision_shape: str) -> int:
|
||||
"""Convert collision shape name to type index"""
|
||||
return ("SPHERE", "BOX", "CAPSULE").index(collision_shape)
|
||||
|
||||
|
||||
def collisionShape(shape_type):
|
||||
def collisionShape(shape_type: int) -> str:
|
||||
"""Convert shape type index to collision shape name"""
|
||||
return ("SPHERE", "BOX", "CAPSULE")[shape_type]
|
||||
|
||||
|
||||
def setRigidBodyWorldEnabled(enable):
|
||||
def setRigidBodyWorldEnabled(enable: bool) -> bool:
|
||||
"""Enable or disable the rigid body world and return previous state"""
|
||||
if bpy.ops.rigidbody.world_add.poll():
|
||||
logger.debug("Creating rigid body world")
|
||||
bpy.ops.rigidbody.world_add()
|
||||
rigidbody_world = bpy.context.scene.rigidbody_world
|
||||
enabled = rigidbody_world.enabled
|
||||
rigidbody_world.enabled = enable
|
||||
logger.debug(f"Rigid body world enabled: {enable} (was: {enabled})")
|
||||
return enabled
|
||||
|
||||
|
||||
class RigidBodyMaterial:
|
||||
COLORS = [
|
||||
COLORS: List[int] = [
|
||||
0x7FDDD4,
|
||||
0xF0E68C,
|
||||
0xEE82EE,
|
||||
@@ -59,10 +65,12 @@ class RigidBodyMaterial:
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def getMaterial(cls, number):
|
||||
def getMaterial(cls, number: int) -> bpy.types.Material:
|
||||
"""Get or create a material for rigid bodies with the specified number"""
|
||||
number = int(number)
|
||||
material_name = "mmd_tools_rigid_%d" % (number)
|
||||
material_name = f"mmd_tools_rigid_{number}"
|
||||
if material_name not in bpy.data.materials:
|
||||
logger.debug(f"Creating rigid body material: {material_name}")
|
||||
mat = bpy.data.materials.new(material_name)
|
||||
color = cls.COLORS[number]
|
||||
mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)]
|
||||
@@ -89,9 +97,11 @@ class RigidBodyMaterial:
|
||||
class FnRigidBody:
|
||||
@staticmethod
|
||||
def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]:
|
||||
"""Create multiple rigid body objects parented to the specified object"""
|
||||
if count < 1:
|
||||
return []
|
||||
|
||||
logger.debug(f"Creating {count} rigid body objects parented to {parent_object.name}")
|
||||
obj = FnRigidBody.new_rigid_body_object(context, parent_object)
|
||||
|
||||
if count == 1:
|
||||
@@ -101,6 +111,8 @@ class FnRigidBody:
|
||||
|
||||
@staticmethod
|
||||
def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object:
|
||||
"""Create a new rigid body object parented to the specified object"""
|
||||
logger.debug(f"Creating new rigid body object parented to {parent_object.name}")
|
||||
obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody"))
|
||||
obj.parent = parent_object
|
||||
obj.mmd_type = "RIGID_BODY"
|
||||
@@ -118,11 +130,11 @@ class FnRigidBody:
|
||||
@staticmethod
|
||||
def setup_rigid_body_object(
|
||||
obj: bpy.types.Object,
|
||||
shape_type: str,
|
||||
shape_type: int,
|
||||
location: Vector,
|
||||
rotation: Euler,
|
||||
size: Vector,
|
||||
dynamics_type: str,
|
||||
dynamics_type: int,
|
||||
collision_group_number: Optional[int] = None,
|
||||
collision_group_mask: Optional[List[bool]] = None,
|
||||
name: Optional[str] = None,
|
||||
@@ -134,6 +146,8 @@ class FnRigidBody:
|
||||
linear_damping: Optional[float] = None,
|
||||
bounce: Optional[float] = None,
|
||||
) -> bpy.types.Object:
|
||||
"""Set up a rigid body object with the specified parameters"""
|
||||
logger.debug(f"Setting up rigid body object: {obj.name}")
|
||||
obj.location = location
|
||||
obj.rotation_euler = rotation
|
||||
|
||||
@@ -175,7 +189,8 @@ class FnRigidBody:
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def get_rigid_body_size(obj: bpy.types.Object):
|
||||
def get_rigid_body_size(obj: bpy.types.Object) -> Tuple[float, float, float]:
|
||||
"""Get the size of a rigid body object based on its shape type"""
|
||||
assert obj.mmd_type == "RIGID_BODY"
|
||||
|
||||
x0, y0, z0 = obj.bound_box[0]
|
||||
@@ -195,10 +210,14 @@ class FnRigidBody:
|
||||
height = abs((z1 - z0) - diameter)
|
||||
return (radius, height, 0.0)
|
||||
else:
|
||||
raise ValueError(f"Invalid shape type: {shape}")
|
||||
error_msg = f"Invalid shape type: {shape}"
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
@staticmethod
|
||||
def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object:
|
||||
"""Create a new joint object parented to the specified object"""
|
||||
logger.debug(f"Creating new joint object parented to {parent_object.name}")
|
||||
obj = FnContext.new_and_link_object(context, name="Joint", object_data=None)
|
||||
obj.parent = parent_object
|
||||
obj.mmd_type = "JOINT"
|
||||
@@ -230,9 +249,11 @@ class FnRigidBody:
|
||||
|
||||
@staticmethod
|
||||
def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]:
|
||||
"""Create multiple joint objects parented to the specified object"""
|
||||
if count < 1:
|
||||
return []
|
||||
|
||||
logger.debug(f"Creating {count} joint objects parented to {parent_object.name}")
|
||||
obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size)
|
||||
|
||||
if count == 1:
|
||||
@@ -256,6 +277,8 @@ class FnRigidBody:
|
||||
name: str,
|
||||
name_e: Optional[str] = None,
|
||||
) -> bpy.types.Object:
|
||||
"""Set up a joint object with the specified parameters"""
|
||||
logger.debug(f"Setting up joint object: {obj.name} with name {name}")
|
||||
obj.name = f"J.{name}"
|
||||
|
||||
obj.location = location
|
||||
|
||||
+55
-29
@@ -7,14 +7,19 @@
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, List, Tuple, Set, Optional, Any, Union, cast, TypeVar, Callable
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
import numpy as np
|
||||
from mathutils import Matrix, Vector, Quaternion, Euler
|
||||
from bpy.types import Object, PoseBone, Pose, ShapeKey, Modifier, VertexGroup
|
||||
|
||||
from ..bpyutils import FnObject
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def _hash(v):
|
||||
def _hash(v: Union[Object, PoseBone, Pose]) -> int:
|
||||
if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
|
||||
return hash(type(v).__name__ + v.name)
|
||||
elif isinstance(v, bpy.types.Pose):
|
||||
@@ -24,23 +29,24 @@ def _hash(v):
|
||||
|
||||
|
||||
class FnSDEF:
|
||||
g_verts = {} # global cache
|
||||
g_shapekey_data = {}
|
||||
g_bone_check = {}
|
||||
__g_armature_check = {}
|
||||
SHAPEKEY_NAME = "mmd_sdef_skinning"
|
||||
MASK_NAME = "mmd_sdef_mask"
|
||||
g_verts: Dict[int, Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]] = {} # global cache
|
||||
g_shapekey_data: Dict[int, Optional[np.ndarray]] = {}
|
||||
g_bone_check: Dict[int, Dict[Union[Tuple[int, int], str], Union[Tuple[Matrix, Matrix], bool]]] = {}
|
||||
__g_armature_check: Dict[int, Optional[int]] = {}
|
||||
SHAPEKEY_NAME: str = "mmd_sdef_skinning"
|
||||
MASK_NAME: str = "mmd_sdef_mask"
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
raise NotImplementedError("not allowed")
|
||||
|
||||
@classmethod
|
||||
def __init_cache(cls, obj, shapekey):
|
||||
def __init_cache(cls, obj: Object, shapekey: ShapeKey) -> bool:
|
||||
key = _hash(obj)
|
||||
obj = getattr(obj, "original", obj)
|
||||
mod = obj.modifiers.get("mmd_bone_order_override")
|
||||
key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None
|
||||
if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature:
|
||||
logger.debug(f"Initializing SDEF cache for {obj.name}")
|
||||
cls.g_verts[key] = cls.__find_vertices(obj)
|
||||
cls.g_bone_check[key] = {}
|
||||
cls.__g_armature_check[key] = key_armature
|
||||
@@ -49,7 +55,7 @@ class FnSDEF:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def __check_bone_update(cls, obj, bone0, bone1):
|
||||
def __check_bone_update(cls, obj: Object, bone0: PoseBone, bone1: PoseBone) -> bool:
|
||||
check = cls.g_bone_check[_hash(obj)]
|
||||
key = (_hash(bone0), _hash(bone1))
|
||||
if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
|
||||
@@ -58,17 +64,18 @@ class FnSDEF:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def mute_sdef_set(cls, obj, mute):
|
||||
def mute_sdef_set(cls, obj: Object, mute: bool) -> None:
|
||||
key_blocks = getattr(obj.data.shape_keys, "key_blocks", ())
|
||||
if cls.SHAPEKEY_NAME in key_blocks:
|
||||
shapekey = key_blocks[cls.SHAPEKEY_NAME]
|
||||
shapekey.mute = mute
|
||||
if cls.has_sdef_data(obj):
|
||||
logger.debug(f"Setting SDEF mute state to {mute} for {obj.name}")
|
||||
cls.__init_cache(obj, shapekey)
|
||||
cls.__sdef_muted(obj, shapekey)
|
||||
|
||||
@classmethod
|
||||
def __sdef_muted(cls, obj, shapekey):
|
||||
def __sdef_muted(cls, obj: Object, shapekey: ShapeKey) -> bool:
|
||||
mute = shapekey.mute
|
||||
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
|
||||
mod = obj.modifiers.get("mmd_bone_order_override")
|
||||
@@ -80,10 +87,11 @@ class FnSDEF:
|
||||
mod.invert_vertex_group = True
|
||||
shapekey.vertex_group = cls.MASK_NAME
|
||||
cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute
|
||||
logger.debug(f"SDEF mute state updated to {mute} for {obj.name}")
|
||||
return mute
|
||||
|
||||
@staticmethod
|
||||
def has_sdef_data(obj):
|
||||
def has_sdef_data(obj: Object) -> bool:
|
||||
mod = obj.modifiers.get("mmd_bone_order_override")
|
||||
if mod and mod.type == "ARMATURE" and mod.object:
|
||||
kb = getattr(obj.data.shape_keys, "key_blocks", None)
|
||||
@@ -91,18 +99,21 @@ class FnSDEF:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def __find_vertices(cls, obj):
|
||||
def __find_vertices(cls, obj: Object) -> Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]]:
|
||||
if not cls.has_sdef_data(obj):
|
||||
return {}
|
||||
|
||||
vertices = {}
|
||||
vertices: Dict[Tuple[int, int], Tuple[PoseBone, PoseBone, List[Tuple[int, float, float, Vector, Vector, Vector]], List[int]]] = {}
|
||||
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones
|
||||
bone_map = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
|
||||
bone_map: Dict[int, PoseBone] = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones}
|
||||
sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data
|
||||
sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data
|
||||
sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data
|
||||
vd = obj.data.vertices
|
||||
|
||||
logger.debug(f"Finding SDEF vertices for {obj.name}")
|
||||
vertex_count = 0
|
||||
|
||||
for i in range(len(sdef_c)):
|
||||
if vd[i].co != sdef_c[i].co:
|
||||
bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups
|
||||
@@ -125,16 +136,19 @@ class FnSDEF:
|
||||
vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], [])
|
||||
vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2))
|
||||
vertices[key][3].append(i)
|
||||
vertex_count += 1
|
||||
|
||||
logger.debug(f"Found {vertex_count} SDEF vertices in {obj.name}")
|
||||
return vertices
|
||||
|
||||
@classmethod
|
||||
def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale):
|
||||
def driver_function_wrap(cls, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
|
||||
obj = bpy.data.objects[obj_name]
|
||||
shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]
|
||||
return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale)
|
||||
|
||||
@classmethod
|
||||
def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale):
|
||||
def driver_function(cls, shapekey: ShapeKey, obj_name: str, bulk_update: bool, use_skip: bool, use_scale: bool) -> float:
|
||||
obj = bpy.data.objects[obj_name]
|
||||
if getattr(shapekey.id_data, "is_evaluated", False):
|
||||
# For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver
|
||||
@@ -206,11 +220,11 @@ class FnSDEF:
|
||||
rot1 = -rot1
|
||||
s0, s1 = mat0.to_scale(), mat1.to_scale()
|
||||
|
||||
def scale(mat_rot, w0, w1):
|
||||
def scale(mat_rot: Matrix, w0: float, w1: float) -> Matrix:
|
||||
s = s0 * w0 + s1 * w1
|
||||
return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])])
|
||||
|
||||
def offset(mat_rot, pos_c, vid):
|
||||
def offset(mat_rot: Matrix, pos_c: Vector, vid: int) -> Vector:
|
||||
delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = ''
|
||||
return (mat_rot @ (pos_c + delta)) - delta
|
||||
|
||||
@@ -233,16 +247,19 @@ class FnSDEF:
|
||||
return 1.0 # shapkey value
|
||||
|
||||
@classmethod
|
||||
def register_driver_function(cls):
|
||||
def register_driver_function(cls) -> None:
|
||||
"""Register driver functions in Blender's driver namespace."""
|
||||
if "mmd_sdef_driver" not in bpy.app.driver_namespace:
|
||||
logger.debug("Registering SDEF driver function")
|
||||
bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function
|
||||
if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace:
|
||||
logger.debug("Registering SDEF driver wrapper function")
|
||||
bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap
|
||||
|
||||
BENCH_LOOP = 10
|
||||
BENCH_LOOP: int = 10
|
||||
|
||||
@classmethod
|
||||
def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip):
|
||||
def __get_benchmark_result(cls, obj: Object, shapkey: ShapeKey, use_scale: bool, use_skip: bool) -> bool:
|
||||
# warmed up
|
||||
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
|
||||
cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale)
|
||||
@@ -256,14 +273,15 @@ class FnSDEF:
|
||||
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
|
||||
bulk_time = time.time() - t
|
||||
result = default_time > bulk_time
|
||||
logging.info("FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s", default_time, bulk_time, result)
|
||||
logger.info(f"SDEF benchmark for {obj.name}: default {default_time:.4f}s vs bulk_update {bulk_time:.4f}s => bulk_update={result}")
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False):
|
||||
def bind(cls, obj: Object, bulk_update: Optional[bool] = None, use_skip: bool = True, use_scale: bool = False) -> bool:
|
||||
# Unbind first
|
||||
cls.unbind(obj)
|
||||
if not cls.has_sdef_data(obj):
|
||||
logger.debug(f"Object {obj.name} does not have SDEF data")
|
||||
return False
|
||||
# Create the shapekey for the driver
|
||||
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
|
||||
@@ -294,32 +312,38 @@ class FnSDEF:
|
||||
f.driver.use_self = True
|
||||
param = (bulk_update, use_skip, use_scale)
|
||||
f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param)
|
||||
logger.info(f"Successfully bound SDEF to {obj.name} with bulk_update={bulk_update}, use_skip={use_skip}, use_scale={use_scale}")
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def unbind(cls, obj):
|
||||
def unbind(cls, obj: Object) -> None:
|
||||
if obj.data.shape_keys:
|
||||
if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks:
|
||||
logger.debug(f"Removing SDEF shape key from {obj.name}")
|
||||
FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME])
|
||||
for mod in obj.modifiers:
|
||||
if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME:
|
||||
logger.debug(f"Clearing SDEF vertex group from modifier in {obj.name}")
|
||||
mod.vertex_group = ""
|
||||
mod.invert_vertex_group = False
|
||||
break
|
||||
if cls.MASK_NAME in obj.vertex_groups:
|
||||
logger.debug(f"Removing SDEF vertex group from {obj.name}")
|
||||
obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME])
|
||||
cls.clear_cache(obj)
|
||||
|
||||
@classmethod
|
||||
def clear_cache(cls, obj=None, unused_only=False):
|
||||
def clear_cache(cls, obj: Optional[Object] = None, unused_only: bool = False) -> None:
|
||||
if unused_only:
|
||||
valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj)
|
||||
for key in cls.g_verts.keys() - valid_keys:
|
||||
removed_keys = cls.g_verts.keys() - valid_keys
|
||||
for key in removed_keys:
|
||||
del cls.g_verts[key]
|
||||
for key in cls.g_shapekey_data.keys() - cls.g_verts.keys():
|
||||
del cls.g_shapekey_data[key]
|
||||
for key in cls.g_bone_check.keys() - cls.g_verts.keys():
|
||||
del cls.g_bone_check[key]
|
||||
logger.debug(f"Cleared {len(removed_keys)} unused SDEF cache entries")
|
||||
elif obj:
|
||||
key = _hash(obj)
|
||||
if key in cls.g_verts:
|
||||
@@ -328,7 +352,9 @@ class FnSDEF:
|
||||
del cls.g_shapekey_data[key]
|
||||
if key in cls.g_bone_check:
|
||||
del cls.g_bone_check[key]
|
||||
logger.debug(f"Cleared SDEF cache for {obj.name}")
|
||||
else:
|
||||
logger.debug("Cleared all SDEF cache")
|
||||
cls.g_verts = {}
|
||||
cls.g_bone_check = {}
|
||||
cls.g_shapekey_data = {}
|
||||
|
||||
+56
-36
@@ -5,25 +5,33 @@
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import Optional, Tuple, cast
|
||||
from typing import Optional, Tuple, cast, List, Dict, Any, Union
|
||||
import bpy
|
||||
from bpy.types import (
|
||||
ShaderNodeTree,
|
||||
ShaderNode,
|
||||
NodeGroupInput,
|
||||
NodeGroupOutput,
|
||||
Material
|
||||
)
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
class _NodeTreeUtils:
|
||||
def __init__(self, shader: bpy.types.ShaderNodeTree):
|
||||
def __init__(self, shader: ShaderNodeTree):
|
||||
self.shader = shader
|
||||
self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore
|
||||
self.nodes: bpy.types.bpy_prop_collection[ShaderNode] = shader.nodes # type: ignore
|
||||
self.links = shader.links
|
||||
|
||||
def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]:
|
||||
def _find_node(self, node_type: str) -> Optional[ShaderNode]:
|
||||
return next((n for n in self.nodes if n.bl_idname == node_type), None)
|
||||
|
||||
def new_node(self, idname: str, pos: Tuple[int, int]) -> bpy.types.ShaderNode:
|
||||
node: bpy.types.ShaderNode = self.nodes.new(idname)
|
||||
def new_node(self, idname: str, pos: Tuple[int, int]) -> ShaderNode:
|
||||
node: ShaderNode = self.nodes.new(idname)
|
||||
node.location = (pos[0] * 210, pos[1] * 220)
|
||||
return node
|
||||
|
||||
def new_math_node(self, operation, pos, value1=None, value2=None):
|
||||
def new_math_node(self, operation: str, pos: Tuple[int, int], value1: Optional[float] = None, value2: Optional[float] = None) -> ShaderNode:
|
||||
node = self.new_node("ShaderNodeMath", pos)
|
||||
node.operation = operation
|
||||
if value1 is not None:
|
||||
@@ -32,7 +40,7 @@ class _NodeTreeUtils:
|
||||
node.inputs[1].default_value = value2
|
||||
return node
|
||||
|
||||
def new_vector_math_node(self, operation, pos, vector1=None, vector2=None):
|
||||
def new_vector_math_node(self, operation: str, pos: Tuple[int, int], vector1: Optional[Tuple[float, float, float, float]] = None, vector2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode:
|
||||
node = self.new_node("ShaderNodeVectorMath", pos)
|
||||
node.operation = operation
|
||||
if vector1 is not None:
|
||||
@@ -41,7 +49,7 @@ class _NodeTreeUtils:
|
||||
node.inputs[1].default_value = vector2
|
||||
return node
|
||||
|
||||
def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None):
|
||||
def new_mix_node(self, blend_type: str, pos: Tuple[int, int], fac: Optional[float] = None, color1: Optional[Tuple[float, float, float, float]] = None, color2: Optional[Tuple[float, float, float, float]] = None) -> ShaderNode:
|
||||
node = self.new_node("ShaderNodeMixRGB", pos)
|
||||
node.blend_type = blend_type
|
||||
if fac is not None:
|
||||
@@ -53,30 +61,30 @@ class _NodeTreeUtils:
|
||||
return node
|
||||
|
||||
|
||||
SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"}
|
||||
SOCKET_TYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "NodeSocketFloat"}
|
||||
|
||||
SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"}
|
||||
SOCKET_SUBTYPE_MAPPING: Dict[str, str] = {"NodeSocketFloatFactor": "FACTOR"}
|
||||
|
||||
|
||||
class _NodeGroupUtils(_NodeTreeUtils):
|
||||
def __init__(self, shader: bpy.types.ShaderNodeTree):
|
||||
def __init__(self, shader: ShaderNodeTree):
|
||||
super().__init__(shader)
|
||||
self.__node_input: Optional[bpy.types.NodeGroupInput] = None
|
||||
self.__node_output: Optional[bpy.types.NodeGroupOutput] = None
|
||||
self.__node_input: Optional[NodeGroupInput] = None
|
||||
self.__node_output: Optional[NodeGroupOutput] = None
|
||||
|
||||
@property
|
||||
def node_input(self) -> bpy.types.NodeGroupInput:
|
||||
def node_input(self) -> NodeGroupInput:
|
||||
if not self.__node_input:
|
||||
self.__node_input = cast(bpy.types.NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
|
||||
self.__node_input = cast(NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
|
||||
return self.__node_input
|
||||
|
||||
@property
|
||||
def node_output(self) -> bpy.types.NodeGroupOutput:
|
||||
def node_output(self) -> NodeGroupOutput:
|
||||
if not self.__node_output:
|
||||
self.__node_output = cast(bpy.types.NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
|
||||
self.__node_output = cast(NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
|
||||
return self.__node_output
|
||||
|
||||
def hide_nodes(self, hide_sockets=True):
|
||||
def hide_nodes(self, hide_sockets: bool = True) -> None:
|
||||
skip_nodes = {self.__node_input, self.__node_output}
|
||||
for n in (x for x in self.nodes if x not in skip_nodes):
|
||||
n.hide = True
|
||||
@@ -87,15 +95,15 @@ class _NodeGroupUtils(_NodeTreeUtils):
|
||||
for s in n.outputs:
|
||||
s.hide = not s.is_linked
|
||||
|
||||
def new_input_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
|
||||
def new_input_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
|
||||
self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type)
|
||||
|
||||
def new_output_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None):
|
||||
def new_output_socket(self, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
|
||||
self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type)
|
||||
|
||||
def __new_io(self, in_out, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None):
|
||||
def __new_io(self, in_out: str, io_sockets: bpy.types.bpy_prop_collection, io_name: str, socket: Optional[bpy.types.NodeSocket], default_val: Optional[Union[float, Tuple[float, float, float, float]]] = None, min_max: Optional[Tuple[float, float]] = None, socket_type: Optional[str] = None) -> None:
|
||||
if io_name not in io_sockets:
|
||||
idname = socket_type or socket.bl_idname
|
||||
idname = socket_type or (socket.bl_idname if socket else "NodeSocketFloat")
|
||||
interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname))
|
||||
if idname in SOCKET_SUBTYPE_MAPPING:
|
||||
interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "")
|
||||
@@ -114,14 +122,18 @@ class _NodeGroupUtils(_NodeTreeUtils):
|
||||
|
||||
class _MaterialMorph:
|
||||
@classmethod
|
||||
def update_morph_inputs(cls, material, morph):
|
||||
def update_morph_inputs(cls, material: Optional[Material], morph: Any) -> None:
|
||||
"""Update material morph inputs based on morph data"""
|
||||
if material and material.node_tree and morph.name in material.node_tree.nodes:
|
||||
logger.debug(f"Updating morph inputs for {morph.name} in {material.name}")
|
||||
cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph)
|
||||
cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph)
|
||||
|
||||
@classmethod
|
||||
def setup_morph_nodes(cls, material, morphs):
|
||||
def setup_morph_nodes(cls, material: Material, morphs: List[Any]) -> List[ShaderNode]:
|
||||
"""Set up morph nodes for a material"""
|
||||
node, nodes = None, []
|
||||
logger.debug(f"Setting up {len(morphs)} morph nodes for {material.name}")
|
||||
for m in morphs:
|
||||
node = cls.__morph_node_add(material, m, node)
|
||||
nodes.append(node)
|
||||
@@ -137,23 +149,25 @@ class _MaterialMorph:
|
||||
return nodes
|
||||
|
||||
@classmethod
|
||||
def reset_morph_links(cls, node):
|
||||
def reset_morph_links(cls, node: ShaderNode) -> None:
|
||||
"""Reset morph links for a node"""
|
||||
logger.debug(f"Resetting morph links for {node.name}")
|
||||
cls.__update_morph_links(node, reset=True)
|
||||
|
||||
@classmethod
|
||||
def __update_morph_links(cls, node, reset=False):
|
||||
def __update_morph_links(cls, node: ShaderNode, reset: bool = False) -> None:
|
||||
nodes, links = node.id_data.nodes, node.id_data.links
|
||||
if reset:
|
||||
if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links):
|
||||
return
|
||||
|
||||
def __init_link(socket_morph, socket_shader):
|
||||
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
|
||||
if socket_shader and socket_morph.is_linked:
|
||||
links.new(socket_morph.links[0].from_socket, socket_shader)
|
||||
|
||||
else:
|
||||
|
||||
def __init_link(socket_morph, socket_shader):
|
||||
def __init_link(socket_morph: bpy.types.NodeSocket, socket_shader: Optional[bpy.types.NodeSocket]) -> None:
|
||||
if socket_shader:
|
||||
if socket_shader.is_linked:
|
||||
links.new(socket_shader.links[0].from_socket, socket_morph)
|
||||
@@ -178,7 +192,8 @@ class _MaterialMorph:
|
||||
__init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
|
||||
|
||||
@classmethod
|
||||
def __update_node_inputs(cls, node, morph):
|
||||
def __update_node_inputs(cls, node: ShaderNode, morph: Any) -> None:
|
||||
"""Update node inputs based on morph data"""
|
||||
node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3]
|
||||
node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3]
|
||||
node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3]
|
||||
@@ -196,7 +211,8 @@ class _MaterialMorph:
|
||||
node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
|
||||
|
||||
@classmethod
|
||||
def __morph_node_add(cls, material, morph, prev_node):
|
||||
def __morph_node_add(cls, material: Material, morph: Optional[Any], prev_node: Optional[ShaderNode]) -> Optional[ShaderNode]:
|
||||
"""Add a morph node to a material"""
|
||||
nodes, links = material.node_tree.nodes, material.node_tree.links
|
||||
|
||||
shader = nodes.get("mmd_shader", None)
|
||||
@@ -221,8 +237,9 @@ class _MaterialMorph:
|
||||
return node
|
||||
# connect last node to shader
|
||||
if shader:
|
||||
logger.debug(f"Connecting last node to shader for {material.name}")
|
||||
|
||||
def __soft_link(socket_out, socket_in):
|
||||
def __soft_link(socket_out: Optional[bpy.types.NodeSocket], socket_in: Optional[bpy.types.NodeSocket]) -> None:
|
||||
if socket_out and socket_in:
|
||||
links.new(socket_out, socket_in)
|
||||
|
||||
@@ -244,12 +261,14 @@ class _MaterialMorph:
|
||||
return shader
|
||||
|
||||
@classmethod
|
||||
def __get_shader(cls, morph_type):
|
||||
def __get_shader(cls, morph_type: str) -> ShaderNodeTree:
|
||||
"""Get or create a shader node group for the specified morph type"""
|
||||
group_name = "MMDMorph" + morph_type
|
||||
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||
if len(shader.nodes):
|
||||
return shader
|
||||
|
||||
logger.info(f"Creating new shader node group: {group_name}")
|
||||
ng = _NodeGroupUtils(shader)
|
||||
links = ng.links
|
||||
|
||||
@@ -260,7 +279,7 @@ class _MaterialMorph:
|
||||
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
|
||||
ng.new_node("NodeGroupOutput", (3, 0))
|
||||
|
||||
def __blend_color_add(id_name, pos, tag=""):
|
||||
def __blend_color_add(id_name: str, pos: Tuple[int, int], tag: str = "") -> ShaderNode:
|
||||
# MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac))
|
||||
# MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2
|
||||
# https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400
|
||||
@@ -271,7 +290,7 @@ class _MaterialMorph:
|
||||
ng.new_output_socket(id_name + tag, node_mix.outputs["Color"])
|
||||
return node_mix
|
||||
|
||||
def __blend_tex_color(id_name, pos, node_tex_rgb, node_tex_a_output):
|
||||
def __blend_tex_color(id_name: str, pos: Tuple[int, int], node_tex_rgb: ShaderNode, node_tex_a_output: bpy.types.NodeSocket) -> None:
|
||||
# Tex Color = tex_rgb * tex_a + (1 - tex_a)
|
||||
# : tex_rgb = TexRGB * ColorMul + ColorAdd
|
||||
# : tex_a = TexA * ValueMul + ValueAdd
|
||||
@@ -294,7 +313,7 @@ class _MaterialMorph:
|
||||
ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor")
|
||||
ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor")
|
||||
|
||||
def __add_sockets(id_name, input1, input2, output, tag=""):
|
||||
def __add_sockets(id_name: str, input1: bpy.types.NodeSocket, input2: bpy.types.NodeSocket, output: bpy.types.NodeSocket, tag: str = "") -> None:
|
||||
ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul)
|
||||
ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul)
|
||||
ng.new_output_socket(f"{id_name}{tag}", output)
|
||||
@@ -343,4 +362,5 @@ class _MaterialMorph:
|
||||
__blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2])
|
||||
|
||||
ng.hide_nodes()
|
||||
logger.debug(f"Shader node group {group_name} created successfully")
|
||||
return ng.shader
|
||||
|
||||
@@ -5,39 +5,44 @@
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import Iterable, Optional
|
||||
from typing import Iterable, Optional, Any, List, Tuple, Union
|
||||
|
||||
import bpy
|
||||
from bpy.types import Material, NodeTree, Node, NodeSocket, ShaderNodeGroup, ShaderNodeOutputMaterial, NodeLink
|
||||
|
||||
from ..logging_setup import logger
|
||||
from .core.shader import _NodeGroupUtils
|
||||
from .core.material import FnMaterial
|
||||
|
||||
|
||||
def __switchToCyclesRenderEngine():
|
||||
def __switchToCyclesRenderEngine() -> None:
|
||||
if bpy.context.scene.render.engine != "CYCLES":
|
||||
logger.debug("Switching render engine to Cycles")
|
||||
bpy.context.scene.render.engine = "CYCLES"
|
||||
|
||||
|
||||
def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader):
|
||||
def __exposeNodeTreeInput(in_socket: NodeSocket, name: str, default_value: Any, node_input: Node, shader: NodeTree) -> None:
|
||||
_NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value)
|
||||
|
||||
|
||||
def __exposeNodeTreeOutput(out_socket, name, node_output, shader):
|
||||
def __exposeNodeTreeOutput(out_socket: NodeSocket, name: str, node_output: Node, shader: NodeTree) -> None:
|
||||
_NodeGroupUtils(shader).new_output_socket(name, out_socket)
|
||||
|
||||
|
||||
def __getMaterialOutput(nodes, bl_idname):
|
||||
def __getMaterialOutput(nodes: bpy.types.Nodes, bl_idname: str) -> Node:
|
||||
o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname)
|
||||
o.is_active_output = True
|
||||
return o
|
||||
|
||||
|
||||
def create_MMDAlphaShader():
|
||||
def create_MMDAlphaShader() -> NodeTree:
|
||||
__switchToCyclesRenderEngine()
|
||||
|
||||
if "MMDAlphaShader" in bpy.data.node_groups:
|
||||
logger.debug("Using existing MMDAlphaShader node group")
|
||||
return bpy.data.node_groups["MMDAlphaShader"]
|
||||
|
||||
logger.info("Creating new MMDAlphaShader node group")
|
||||
shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree")
|
||||
|
||||
node_input = shader.nodes.new("NodeGroupInput")
|
||||
@@ -59,26 +64,28 @@ def create_MMDAlphaShader():
|
||||
return shader
|
||||
|
||||
|
||||
def create_MMDBasicShader():
|
||||
def create_MMDBasicShader() -> NodeTree:
|
||||
__switchToCyclesRenderEngine()
|
||||
|
||||
if "MMDBasicShader" in bpy.data.node_groups:
|
||||
logger.debug("Using existing MMDBasicShader node group")
|
||||
return bpy.data.node_groups["MMDBasicShader"]
|
||||
|
||||
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
|
||||
logger.info("Creating new MMDBasicShader node group")
|
||||
shader: NodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree")
|
||||
|
||||
node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput")
|
||||
node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput")
|
||||
node_input: Node = shader.nodes.new("NodeGroupInput")
|
||||
node_output: Node = shader.nodes.new("NodeGroupOutput")
|
||||
node_output.location.x += 250
|
||||
node_input.location.x -= 500
|
||||
|
||||
dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse")
|
||||
dif: Node = shader.nodes.new("ShaderNodeBsdfDiffuse")
|
||||
dif.location.x -= 250
|
||||
dif.location.y += 150
|
||||
glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic")
|
||||
glo: Node = shader.nodes.new("ShaderNodeBsdfAnisotropic")
|
||||
glo.location.x -= 250
|
||||
glo.location.y -= 150
|
||||
mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader")
|
||||
mix: Node = shader.nodes.new("ShaderNodeMixShader")
|
||||
shader.links.new(mix.inputs[1], dif.outputs["BSDF"])
|
||||
shader.links.new(mix.inputs[2], glo.outputs["BSDF"])
|
||||
|
||||
@@ -91,7 +98,7 @@ def create_MMDBasicShader():
|
||||
return shader
|
||||
|
||||
|
||||
def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]:
|
||||
def __enum_linked_nodes(node: Node) -> Iterable[Node]:
|
||||
yield node
|
||||
if node.parent:
|
||||
yield node.parent
|
||||
@@ -99,7 +106,8 @@ def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]:
|
||||
yield from __enum_linked_nodes(n)
|
||||
|
||||
|
||||
def __cleanNodeTree(material: bpy.types.Material):
|
||||
def __cleanNodeTree(material: Material) -> None:
|
||||
logger.debug(f"Cleaning node tree for material: {material.name}")
|
||||
nodes = material.node_tree.nodes
|
||||
node_names = set(n.name for n in nodes)
|
||||
for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}):
|
||||
@@ -109,40 +117,46 @@ def __cleanNodeTree(material: bpy.types.Material):
|
||||
nodes.remove(nodes[name])
|
||||
|
||||
|
||||
def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
|
||||
def convertToCyclesShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None:
|
||||
logger.info(f"Converting {obj.name} to Cycles shader (use_principled={use_principled}, clean_nodes={clean_nodes})")
|
||||
__switchToCyclesRenderEngine()
|
||||
convertToBlenderShader(obj, use_principled, clean_nodes, subsurface)
|
||||
|
||||
|
||||
def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001):
|
||||
def convertToBlenderShader(obj: bpy.types.Object, use_principled: bool = False, clean_nodes: bool = False, subsurface: float = 0.001) -> None:
|
||||
for i in obj.material_slots:
|
||||
if not i.material:
|
||||
continue
|
||||
if not i.material.use_nodes:
|
||||
logger.debug(f"Enabling nodes for material: {i.material.name}")
|
||||
i.material.use_nodes = True
|
||||
__convertToMMDBasicShader(i.material)
|
||||
if use_principled:
|
||||
logger.debug(f"Converting material to Principled BSDF: {i.material.name}")
|
||||
__convertToPrincipledBsdf(i.material, subsurface)
|
||||
if clean_nodes:
|
||||
__cleanNodeTree(i.material)
|
||||
|
||||
def convertToMMDShader(obj):
|
||||
def convertToMMDShader(obj: bpy.types.Object) -> None:
|
||||
"""BSDF -> MMDShaderDev conversion."""
|
||||
logger.info(f"Converting {obj.name} to MMD shader")
|
||||
for i in obj.material_slots:
|
||||
if not i.material:
|
||||
continue
|
||||
if not i.material.use_nodes:
|
||||
logger.debug(f"Enabling nodes for material: {i.material.name}")
|
||||
i.material.use_nodes = True
|
||||
FnMaterial.convert_to_mmd_material(i.material)
|
||||
|
||||
def __convertToMMDBasicShader(material: bpy.types.Material):
|
||||
def __convertToMMDBasicShader(material: Material) -> None:
|
||||
logger.debug(f"Converting material to MMD Basic Shader: {material.name}")
|
||||
# TODO: test me
|
||||
mmd_basic_shader_grp = create_MMDBasicShader()
|
||||
mmd_alpha_shader_grp = create_MMDAlphaShader()
|
||||
|
||||
if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)):
|
||||
if not any(filter(lambda x: isinstance(x, ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)):
|
||||
# Add nodes for Cycles Render
|
||||
shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
|
||||
shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
|
||||
shader.node_tree = mmd_basic_shader_grp
|
||||
shader.inputs[0].default_value[:3] = material.diffuse_color[:3]
|
||||
shader.inputs[1].default_value[:3] = material.specular_color[:3]
|
||||
@@ -157,7 +171,8 @@ def __convertToMMDBasicShader(material: bpy.types.Material):
|
||||
alpha_value = material.diffuse_color[3]
|
||||
|
||||
if alpha_value < 1.0:
|
||||
alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
|
||||
logger.debug(f"Material has alpha: {material.name}, alpha={alpha_value}")
|
||||
alpha_shader: ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup")
|
||||
alpha_shader.location.x = shader.location.x + 250
|
||||
alpha_shader.location.y = shader.location.y - 150
|
||||
alpha_shader.node_tree = mmd_alpha_shader_grp
|
||||
@@ -165,21 +180,22 @@ def __convertToMMDBasicShader(material: bpy.types.Material):
|
||||
material.node_tree.links.new(alpha_shader.inputs[0], outplug)
|
||||
outplug = alpha_shader.outputs[0]
|
||||
|
||||
material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial")
|
||||
material_output: ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial")
|
||||
material.node_tree.links.new(material_output.inputs["Surface"], outplug)
|
||||
material_output.location.x = shader.location.x + 500
|
||||
material_output.location.y = shader.location.y - 150
|
||||
|
||||
|
||||
def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float):
|
||||
def __convertToPrincipledBsdf(material: Material, subsurface: float) -> None:
|
||||
logger.debug(f"Converting material to Principled BSDF: {material.name}")
|
||||
node_names = set()
|
||||
for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)):
|
||||
for s in (n for n in material.node_tree.nodes if isinstance(n, ShaderNodeGroup)):
|
||||
if s.node_tree.name == "MMDBasicShader":
|
||||
l: bpy.types.NodeLink
|
||||
l: NodeLink
|
||||
for l in s.outputs[0].links:
|
||||
to_node = l.to_node
|
||||
# assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader
|
||||
if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader":
|
||||
if isinstance(to_node, ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader":
|
||||
__switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node)
|
||||
node_names.add(to_node.name)
|
||||
else:
|
||||
@@ -194,8 +210,9 @@ def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float):
|
||||
nodes.remove(nodes[name])
|
||||
|
||||
|
||||
def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None):
|
||||
shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
|
||||
def __switchToPrincipledBsdf(node_tree: NodeTree, node_basic: ShaderNodeGroup, subsurface: float, node_alpha: Optional[ShaderNodeGroup] = None) -> None:
|
||||
logger.debug(f"Switching to Principled BSDF: {node_basic.name}")
|
||||
shader: Node = node_tree.nodes.new("ShaderNodeBsdfPrincipled")
|
||||
shader.parent = node_basic.parent
|
||||
shader.location.x = node_basic.location.x
|
||||
shader.location.y = node_basic.location.y
|
||||
|
||||
@@ -6,13 +6,16 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
from bpy.types import Operator
|
||||
from bpy.props import BoolProperty, StringProperty, FloatProperty
|
||||
from bpy.types import Operator, Context, Object, Material
|
||||
|
||||
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
|
||||
|
||||
from .. import cycles_converter
|
||||
from ..core.exceptions import MaterialNotFoundError
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.shader import _NodeGroupUtils
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
class ConvertMaterialsForCycles(Operator):
|
||||
@@ -21,14 +24,14 @@ class ConvertMaterialsForCycles(Operator):
|
||||
bl_description = "Convert materials of selected objects for Cycles."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_principled: bpy.props.BoolProperty(
|
||||
use_principled: BoolProperty(
|
||||
name="Convert to Principled BSDF",
|
||||
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
clean_nodes: bpy.props.BoolProperty(
|
||||
clean_nodes: BoolProperty(
|
||||
name="Clean Nodes",
|
||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||
default=False,
|
||||
@@ -36,22 +39,27 @@ class ConvertMaterialsForCycles(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None)
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
layout.prop(self, "use_principled")
|
||||
layout.prop(self, "clean_nodes")
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
context.scene.render.engine = "CYCLES"
|
||||
except:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to change to Cycles render engine: {str(e)}")
|
||||
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}")
|
||||
for obj in (x for x in context.selected_objects if x.type == "MESH"):
|
||||
logger.debug(f"Converting materials for object: {obj.name}")
|
||||
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -61,21 +69,21 @@ class ConvertMaterials(Operator):
|
||||
bl_description = "Convert materials of selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_principled: bpy.props.BoolProperty(
|
||||
use_principled: BoolProperty(
|
||||
name="Convert to Principled BSDF",
|
||||
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
clean_nodes: bpy.props.BoolProperty(
|
||||
clean_nodes: BoolProperty(
|
||||
name="Clean Nodes",
|
||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
subsurface: bpy.props.FloatProperty(
|
||||
subsurface: FloatProperty(
|
||||
name="Subsurface",
|
||||
default=0.001,
|
||||
soft_min=0.000,
|
||||
@@ -85,13 +93,15 @@ class ConvertMaterials(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None)
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}")
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
logger.debug(f"Converting materials for object: {obj.name}")
|
||||
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -102,20 +112,22 @@ class ConvertBSDFMaterials(Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return next((x for x in context.selected_objects if x.type == 'MESH'), None)
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info("Converting BSDF materials to MMD shader")
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
logger.debug(f"Converting BSDF materials for object: {obj.name}")
|
||||
cycles_converter.convertToMMDShader(obj)
|
||||
return {'FINISHED'}
|
||||
|
||||
class _OpenTextureBase:
|
||||
"""Create a texture for mmd model material."""
|
||||
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
@@ -129,7 +141,7 @@ class _OpenTextureBase:
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
@@ -139,8 +151,13 @@ class OpenTexture(Operator, _OpenTextureBase):
|
||||
bl_label = "Open Texture"
|
||||
bl_description = "Create main texture of active material"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
mat = context.active_object.active_material
|
||||
if not mat:
|
||||
logger.error("No active material found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Creating texture for material: {mat.name} from {self.filepath}")
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.create_texture(self.filepath)
|
||||
return {"FINISHED"}
|
||||
@@ -154,8 +171,13 @@ class RemoveTexture(Operator):
|
||||
bl_description = "Remove main texture of active material"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
mat = context.active_object.active_material
|
||||
if not mat:
|
||||
logger.error("No active material found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Removing texture from material: {mat.name}")
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.remove_texture()
|
||||
return {"FINISHED"}
|
||||
@@ -168,8 +190,13 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
|
||||
bl_label = "Open Sphere Texture"
|
||||
bl_description = "Create sphere texture of active material"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
mat = context.active_object.active_material
|
||||
if not mat:
|
||||
logger.error("No active material found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}")
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.create_sphere_texture(self.filepath, context.active_object)
|
||||
return {"FINISHED"}
|
||||
@@ -183,8 +210,13 @@ class RemoveSphereTexture(Operator):
|
||||
bl_description = "Remove sphere texture of active material"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
mat = context.active_object.active_material
|
||||
if not mat:
|
||||
logger.error("No active material found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Removing sphere texture from material: {mat.name}")
|
||||
fnMat = FnMaterial(mat)
|
||||
fnMat.remove_sphere_texture()
|
||||
return {"FINISHED"}
|
||||
@@ -197,18 +229,21 @@ class MoveMaterialUp(Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
obj = context.active_object
|
||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
return valid_mesh and obj.active_material_index > 0
|
||||
return bool(valid_mesh and obj.active_material_index > 0)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
current_idx = obj.active_material_index
|
||||
prev_index = current_idx - 1
|
||||
|
||||
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
|
||||
try:
|
||||
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
|
||||
except MaterialNotFoundError:
|
||||
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
|
||||
self.report({"ERROR"}, "Materials not found")
|
||||
return {"CANCELLED"}
|
||||
obj.active_material_index = prev_index
|
||||
@@ -223,18 +258,21 @@ class MoveMaterialDown(Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
obj = context.active_object
|
||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
return valid_mesh and obj.active_material_index < len(obj.material_slots) - 1
|
||||
return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
current_idx = obj.active_material_index
|
||||
next_index = current_idx + 1
|
||||
|
||||
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
|
||||
try:
|
||||
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
|
||||
except MaterialNotFoundError:
|
||||
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
|
||||
self.report({"ERROR"}, "Materials not found")
|
||||
return {"CANCELLED"}
|
||||
obj.active_material_index = next_index
|
||||
@@ -257,26 +295,31 @@ class EdgePreviewSetup(Operator):
|
||||
default="CREATE",
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
from ..core.model import FnModel
|
||||
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
logger.error("No MMD model root found")
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.action == "CLEAN":
|
||||
logger.info(f"Cleaning toon edge for model: {root.name}")
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
self.__clean_toon_edge(obj)
|
||||
else:
|
||||
from ..bpyutils import Props
|
||||
|
||||
logger.info(f"Creating toon edge for model: {root.name}")
|
||||
scale = 0.2 * getattr(root, Props.empty_display_size)
|
||||
counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
|
||||
logger.info(f"Created {counts} toon edge(s)")
|
||||
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
|
||||
return {"FINISHED"}
|
||||
|
||||
def __clean_toon_edge(self, obj):
|
||||
def __clean_toon_edge(self, obj: Object) -> None:
|
||||
logger.debug(f"Cleaning toon edge for object: {obj.name}")
|
||||
if "mmd_edge_preview" in obj.modifiers:
|
||||
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
|
||||
|
||||
@@ -285,7 +328,8 @@ class EdgePreviewSetup(Operator):
|
||||
|
||||
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
|
||||
|
||||
def __create_toon_edge(self, obj, scale=1.0):
|
||||
def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int:
|
||||
logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}")
|
||||
self.__clean_toon_edge(obj)
|
||||
materials = obj.data.materials
|
||||
material_offset = len(materials)
|
||||
@@ -310,10 +354,10 @@ class EdgePreviewSetup(Operator):
|
||||
mod.vertex_group = "mmd_edge_preview"
|
||||
return len(materials) - material_offset
|
||||
|
||||
def __create_edge_preview_group(self, obj):
|
||||
def __create_edge_preview_group(self, obj: Object) -> None:
|
||||
vertices, materials = obj.data.vertices, obj.data.materials
|
||||
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
|
||||
scale_map = {}
|
||||
scale_map: Dict[int, float] = {}
|
||||
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
|
||||
if vg_scale_index >= 0:
|
||||
scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
|
||||
@@ -322,7 +366,7 @@ class EdgePreviewSetup(Operator):
|
||||
weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
|
||||
vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")
|
||||
|
||||
def __get_edge_material(self, mat_name, edge_color, materials):
|
||||
def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
|
||||
if mat_name in materials:
|
||||
return materials[mat_name]
|
||||
mat = bpy.data.materials.get(mat_name, None)
|
||||
@@ -340,7 +384,7 @@ class EdgePreviewSetup(Operator):
|
||||
self.__make_shader(mat)
|
||||
return mat
|
||||
|
||||
def __make_shader(self, m):
|
||||
def __make_shader(self, m: Material) -> None:
|
||||
m.use_nodes = True
|
||||
nodes, links = m.node_tree.nodes, m.node_tree.links
|
||||
|
||||
@@ -361,7 +405,7 @@ class EdgePreviewSetup(Operator):
|
||||
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
|
||||
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
|
||||
|
||||
def __get_edge_preview_shader(self):
|
||||
def __get_edge_preview_shader(self) -> bpy.types.NodeTree:
|
||||
group_name = "MMDEdgePreview"
|
||||
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||
if len(shader.nodes):
|
||||
|
||||
+56
-27
@@ -6,14 +6,17 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import re
|
||||
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Object, Operator, ShapeKey
|
||||
|
||||
from .. import utils
|
||||
from ..bpyutils import FnContext, FnObject
|
||||
from ..core.bone import FnBone
|
||||
from ..core.model import FnModel, Model
|
||||
from ..core.morph import FnMorph
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
class SelectObject(bpy.types.Operator):
|
||||
@@ -29,7 +32,8 @@ class SelectObject(bpy.types.Operator):
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.debug(f"Selecting object: {self.name}")
|
||||
utils.selectAObject(context.scene.objects[self.name])
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -43,41 +47,43 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
|
||||
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
|
||||
|
||||
@classmethod
|
||||
def set_index(cls, obj, index):
|
||||
def set_index(cls, obj: Object, index: int) -> None:
|
||||
m = cls.__PREFIX_REGEXP.match(obj.name)
|
||||
name = m.group("name") if m else obj.name
|
||||
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
|
||||
|
||||
@classmethod
|
||||
def get_name(cls, obj, prefix=None):
|
||||
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
|
||||
m = cls.__PREFIX_REGEXP.match(obj.name)
|
||||
name = m.group("name") if m else obj.name
|
||||
return name[len(prefix) :] if prefix and name.startswith(prefix) else name
|
||||
|
||||
@classmethod
|
||||
def normalize_indices(cls, objects):
|
||||
def normalize_indices(cls, objects: List[Object]) -> None:
|
||||
for i, x in enumerate(objects):
|
||||
cls.set_index(x, i)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return context.active_object is not None
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
objects = self.__get_objects(obj)
|
||||
if obj not in objects:
|
||||
self.report({"ERROR"}, 'Can not move object "%s"' % obj.name)
|
||||
logger.error(f'Cannot move object "{obj.name}"')
|
||||
self.report({"ERROR"}, f'Can not move object "{obj.name}"')
|
||||
return {"CANCELLED"}
|
||||
|
||||
objects.sort(key=lambda x: x.name)
|
||||
logger.debug(f"Moving object {obj.name} {self.type}")
|
||||
self.move(objects, objects.index(obj), self.type)
|
||||
self.normalize_indices(objects)
|
||||
return {"FINISHED"}
|
||||
|
||||
def __get_objects(self, obj):
|
||||
def __get_objects(self, obj: Object) -> Any:
|
||||
class __MovableList(list):
|
||||
def move(self, index_old, index_new):
|
||||
def move(self, index_old: int, index_new: int) -> None:
|
||||
item = self[index_old]
|
||||
self.remove(item)
|
||||
self.insert(index_new, item)
|
||||
@@ -102,11 +108,11 @@ class CleanShapeKeys(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return any(o.type == "MESH" for o in context.selected_objects)
|
||||
|
||||
@staticmethod
|
||||
def __can_remove(key_block):
|
||||
def __can_remove(key_block: ShapeKey) -> bool:
|
||||
if key_block.relative_key == key_block:
|
||||
return False # Basis
|
||||
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
|
||||
@@ -114,20 +120,24 @@ class CleanShapeKeys(bpy.types.Operator):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __shape_key_clean(self, obj, key_blocks):
|
||||
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
|
||||
for kb in key_blocks:
|
||||
if self.__can_remove(kb):
|
||||
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
|
||||
FnObject.mesh_remove_shape_key(obj, kb)
|
||||
if len(key_blocks) == 1:
|
||||
logger.debug(f"Removing single shape key: {key_blocks[0].name} from {obj.name}")
|
||||
FnObject.mesh_remove_shape_key(obj, key_blocks[0])
|
||||
|
||||
def execute(self, context):
|
||||
obj: bpy.types.Object
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info("Cleaning shape keys for selected objects")
|
||||
obj: Object
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH" or obj.data.shape_keys is None:
|
||||
continue
|
||||
if not obj.data.shape_keys.use_relative:
|
||||
continue # not be considered yet
|
||||
logger.debug(f"Processing shape keys for {obj.name}")
|
||||
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -144,21 +154,25 @@ class SeparateByMaterials(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
|
||||
def __separate_by_materials(self, obj):
|
||||
def __separate_by_materials(self, obj: Object) -> None:
|
||||
logger.info(f"Separating {obj.name} by materials")
|
||||
utils.separateByMaterials(obj)
|
||||
if self.clean_shape_keys:
|
||||
logger.debug("Cleaning shape keys after separation")
|
||||
bpy.ops.mmd_tools.clean_shape_keys()
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root is None:
|
||||
logger.debug("No root object found, separating single object")
|
||||
self.__separate_by_materials(obj)
|
||||
else:
|
||||
logger.debug(f"Root object found: {root.name}, preparing for separation")
|
||||
bpy.ops.mmd_tools.clear_temp_materials()
|
||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||
|
||||
@@ -171,9 +185,11 @@ class SeparateByMaterials(bpy.types.Operator):
|
||||
if len(mesh.data.materials) > 0:
|
||||
mat = mesh.data.materials[0]
|
||||
idx = mat_names.index(getattr(mat, "name", None))
|
||||
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
|
||||
MoveObject.set_index(mesh, idx)
|
||||
|
||||
for morph in root.mmd_root.material_morphs:
|
||||
logger.debug(f"Updating material morph: {morph.name}")
|
||||
FnMorph(morph, rig).update_mat_related_mesh()
|
||||
utils.clearUnusedMeshes()
|
||||
return {"FINISHED"}
|
||||
@@ -191,13 +207,15 @@ class JoinMeshes(bpy.types.Operator):
|
||||
default=True,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root is None:
|
||||
logger.error("No MMD model found")
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Joining meshes for model: {root.name}")
|
||||
bpy.ops.mmd_tools.clear_temp_materials()
|
||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||
|
||||
@@ -205,9 +223,11 @@ class JoinMeshes(bpy.types.Operator):
|
||||
rig = Model(root)
|
||||
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
|
||||
if not meshes_list:
|
||||
logger.error("No meshes found in the model")
|
||||
self.report({"ERROR"}, "The model does not have any meshes")
|
||||
return {"CANCELLED"}
|
||||
active_mesh = meshes_list[0]
|
||||
logger.debug(f"Found {len(meshes_list)} meshes, using {active_mesh.name} as active")
|
||||
|
||||
FnContext.select_objects(context, *meshes_list)
|
||||
FnContext.set_active_object(context, active_mesh)
|
||||
@@ -216,15 +236,19 @@ class JoinMeshes(bpy.types.Operator):
|
||||
for m in meshes_list[1:]:
|
||||
for mat in m.data.materials:
|
||||
if mat not in active_mesh.data.materials[:]:
|
||||
logger.debug(f"Adding material {mat.name} to active mesh")
|
||||
active_mesh.data.materials.append(mat)
|
||||
|
||||
# Join selected meshes
|
||||
logger.debug("Joining meshes")
|
||||
bpy.ops.object.join()
|
||||
|
||||
if self.sort_shape_keys:
|
||||
logger.debug("Sorting shape keys")
|
||||
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
|
||||
active_mesh.active_shape_key_index = 0
|
||||
for morph in root.mmd_root.material_morphs:
|
||||
logger.debug(f"Updating material morph: {morph.name}")
|
||||
FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
|
||||
utils.clearUnusedMeshes()
|
||||
return {"FINISHED"}
|
||||
@@ -238,17 +262,20 @@ class AttachMeshesToMMD(bpy.types.Operator):
|
||||
|
||||
add_armature_modifier: bpy.props.BoolProperty(default=True)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
logger.error("No MMD model found")
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
armObj = FnModel.find_armature_object(root)
|
||||
if armObj is None:
|
||||
logger.error("Model armature not found")
|
||||
self.report({"ERROR"}, "Model Armature not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Attaching meshes to model: {root.name}")
|
||||
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -268,17 +295,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
logger.info(f"Changing IK loop factor to {self.mmd_ik_loop_factor} for model: {root_object.name}")
|
||||
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -290,21 +318,22 @@ class RecalculateBoneRoll(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "ARMATURE"
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context):
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
c = layout.column()
|
||||
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
|
||||
c.label(text="Click [OK] to run the operation.")
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
arm = context.active_object
|
||||
logger.info(f"Recalculating bone roll for armature: {arm.name}")
|
||||
FnBone.apply_auto_bone_roll(arm)
|
||||
return {"FINISHED"}
|
||||
|
||||
+61
-24
@@ -6,10 +6,12 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import bpy
|
||||
from typing import Optional, Set, Dict, Any, List, Tuple, Union
|
||||
|
||||
from ..bpyutils import FnContext
|
||||
from ..core.bone import FnBone, MigrationFnBone
|
||||
from ..core.model import FnModel, Model
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
class MorphSliderSetup(bpy.types.Operator):
|
||||
@@ -29,18 +31,22 @@ class MorphSliderSetup(bpy.types.Operator):
|
||||
default="CREATE",
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
logger.debug(f"Executing MorphSliderSetup with type: {self.type}")
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||
rig = Model(root_object)
|
||||
if self.type == "BIND":
|
||||
logger.info(f"Binding morph sliders for {root_object.name}")
|
||||
rig.morph_slider.bind()
|
||||
elif self.type == "UNBIND":
|
||||
logger.info(f"Unbinding morph sliders for {root_object.name}")
|
||||
rig.morph_slider.unbind()
|
||||
else:
|
||||
logger.info(f"Creating morph sliders for {root_object.name}")
|
||||
rig.morph_slider.create()
|
||||
FnContext.set_active_object(context, active_object)
|
||||
|
||||
@@ -53,10 +59,11 @@ class CleanRiggingObjects(bpy.types.Operator):
|
||||
bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
assert root_object is not None
|
||||
|
||||
logger.info(f"Cleaning rig for {root_object.name}")
|
||||
rig = Model(root_object)
|
||||
rig.clean()
|
||||
FnContext.set_active_object(context, root_object)
|
||||
@@ -86,9 +93,10 @@ class BuildRig(bpy.types.Operator):
|
||||
default=1e-06,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
|
||||
logger.info(f"Building rig for {root_object.name} with non_collision_distance_scale={self.non_collision_distance_scale}, collision_margin={self.collision_margin}")
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||
rig = Model(root_object)
|
||||
rig.build(self.non_collision_distance_scale, self.collision_margin)
|
||||
@@ -103,11 +111,14 @@ class CleanAdditionalTransformConstraints(bpy.types.Operator):
|
||||
bl_description = "Delete shadow bones of selected object and revert bones to default MMD state"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
FnBone.clean_additional_transformation(FnModel.find_armature_object(root_object))
|
||||
|
||||
logger.info(f"Cleaning additional transform constraints for {root_object.name}")
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
FnBone.clean_additional_transformation(armature_object)
|
||||
FnContext.set_active_object(context, active_object)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -118,11 +129,12 @@ class ApplyAdditionalTransformConstraints(bpy.types.Operator):
|
||||
bl_description = "Translate appended bones of selected object for Blender"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
logger.info(f"Applying additional transform constraints for {root_object.name}")
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
assert armature_object is not None
|
||||
|
||||
@@ -149,12 +161,14 @@ class SetupBoneFixedAxes(bpy.types.Operator):
|
||||
default="LOAD",
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
armature_object = context.active_object
|
||||
if not armature_object or armature_object.type != "ARMATURE":
|
||||
self.report({"ERROR"}, "Active object is not an armature object")
|
||||
logger.error("Setup Bone Fixed Axis failed: Active object is not an armature object")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Setting up bone fixed axes with type: {self.type}")
|
||||
if self.type == "APPLY":
|
||||
FnBone.apply_bone_fixed_axis(armature_object)
|
||||
FnBone.apply_additional_transformation(armature_object)
|
||||
@@ -180,12 +194,14 @@ class SetupBoneLocalAxes(bpy.types.Operator):
|
||||
default="LOAD",
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
armature_object = context.active_object
|
||||
if not armature_object or armature_object.type != "ARMATURE":
|
||||
self.report({"ERROR"}, "Active object is not an armature object")
|
||||
logger.error("Setup Bone Local Axes failed: Active object is not an armature object")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Setting up bone local axes with type: {self.type}")
|
||||
if self.type == "APPLY":
|
||||
FnBone.apply_bone_local_axes(armature_object)
|
||||
FnBone.apply_additional_transformation(armature_object)
|
||||
@@ -207,16 +223,18 @@ class AddMissingVertexGroupsFromBones(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object: bpy.types.Object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
logger.info(f"Adding missing vertex groups from bones for {root_object.name}, search_in_all_meshes={self.search_in_all_meshes}")
|
||||
bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object)
|
||||
if bone_order_mesh_object is None:
|
||||
logger.error("Failed to find bone order mesh object")
|
||||
return {"CANCELLED"}
|
||||
|
||||
FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes)
|
||||
@@ -246,12 +264,13 @@ class CreateMMDModelRoot(bpy.types.Operator):
|
||||
default=0.08,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
logger.info(f"Creating MMD model root object with name_j={self.name_j}, name_e={self.name_e}, scale={self.scale}")
|
||||
rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True)
|
||||
rig.initialDisplayFrames()
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
@@ -305,15 +324,16 @@ class ConvertToMMDModel(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "ARMATURE" and obj.mode != "EDIT"
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
logger.info(f"Converting to MMD model with scale={self.scale}, convert_material_nodes={self.convert_material_nodes}")
|
||||
# TODO convert some basic MMD properties
|
||||
armature_object = context.active_object
|
||||
scale = self.scale
|
||||
@@ -321,29 +341,31 @@ class ConvertToMMDModel(bpy.types.Operator):
|
||||
|
||||
root_object = FnModel.find_root_object(armature_object)
|
||||
if root_object is None or root_object != armature_object.parent:
|
||||
logger.debug("Creating new MMD model")
|
||||
Model.create(model_name, model_name, scale, armature_object=armature_object)
|
||||
|
||||
self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context))
|
||||
self.__configure_rig(context, Model(armature_object.parent))
|
||||
return {"FINISHED"}
|
||||
|
||||
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects):
|
||||
def __is_child_of_armature(mesh):
|
||||
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects) -> None:
|
||||
def __is_child_of_armature(mesh: bpy.types.Object) -> bool:
|
||||
if mesh.parent is None:
|
||||
return False
|
||||
return mesh.parent == armature_object or __is_child_of_armature(mesh.parent)
|
||||
|
||||
def __is_using_armature(mesh):
|
||||
def __is_using_armature(mesh: bpy.types.Object) -> bool:
|
||||
for m in mesh.modifiers:
|
||||
if m.type == "ARMATURE" and m.object == armature_object:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __get_root(mesh):
|
||||
def __get_root(mesh: bpy.types.Object) -> bpy.types.Object:
|
||||
if mesh.parent is None:
|
||||
return mesh
|
||||
return __get_root(mesh.parent)
|
||||
|
||||
attached_count = 0
|
||||
for x in objects:
|
||||
if __is_using_armature(x) and not __is_child_of_armature(x):
|
||||
x_root = __get_root(x)
|
||||
@@ -351,27 +373,35 @@ class ConvertToMMDModel(bpy.types.Operator):
|
||||
x_root.parent_type = "OBJECT"
|
||||
x_root.parent = armature_object
|
||||
x_root.matrix_world = m
|
||||
attached_count += 1
|
||||
|
||||
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()
|
||||
armature_object = mmd_model.armature()
|
||||
mesh_objects = tuple(mmd_model.meshes())
|
||||
|
||||
logger.info(f"Configuring rig for {root_object.name} with {len(mesh_objects)} meshes")
|
||||
mmd_model.loadMorphs()
|
||||
|
||||
if self.middle_joint_bones_lock:
|
||||
vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups}
|
||||
locked_bones = 0
|
||||
for pose_bone in armature_object.pose.bones:
|
||||
if not pose_bone.parent:
|
||||
continue
|
||||
if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups:
|
||||
continue
|
||||
pose_bone.lock_location = (True, True, True)
|
||||
locked_bones += 1
|
||||
logger.debug(f"Locked {locked_bones} middle joint bones")
|
||||
|
||||
from ..core.material import FnMaterial
|
||||
|
||||
FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes)
|
||||
try:
|
||||
converted_materials = 0
|
||||
for m in (x for mesh in mesh_objects for x in mesh.data.materials if x):
|
||||
FnMaterial.convert_to_mmd_material(m, context)
|
||||
mmd_material = m.mmd_material
|
||||
@@ -384,6 +414,8 @@ class ConvertToMMDModel(bpy.types.Operator):
|
||||
line_color = list(m.line_color)
|
||||
mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold
|
||||
mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)]
|
||||
converted_materials += 1
|
||||
logger.debug(f"Converted {converted_materials} materials")
|
||||
finally:
|
||||
FnMaterial.set_nodes_are_readonly(False)
|
||||
from .display_item import DisplayItemQuickSetup
|
||||
@@ -400,16 +432,17 @@ class ResetObjectVisibility(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
active_object: bpy.types.Object = context.active_object
|
||||
return FnModel.find_root_object(active_object) is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object: bpy.types.Object = context.active_object
|
||||
mmd_root_object = FnModel.find_root_object(active_object)
|
||||
assert mmd_root_object is not None
|
||||
mmd_root = mmd_root_object.mmd_root
|
||||
|
||||
logger.info(f"Resetting object visibility for {mmd_root_object.name}")
|
||||
mmd_root_object.hide_set(False)
|
||||
|
||||
rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object)
|
||||
@@ -440,11 +473,12 @@ class AssembleAll(bpy.types.Operator):
|
||||
bl_label = "Assemble All"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
logger.info(f"Assembling all components for {root_object.name}")
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
|
||||
rig = Model(root_object)
|
||||
MigrationFnBone.fix_mmd_ik_limit_override(rig.armature())
|
||||
@@ -452,6 +486,7 @@ class AssembleAll(bpy.types.Operator):
|
||||
rig.build()
|
||||
rig.morph_slider.bind()
|
||||
|
||||
logger.debug("Binding SDEF weights")
|
||||
with context.temp_override(selected_objects=[active_object]):
|
||||
bpy.ops.mmd_tools.sdef_bind()
|
||||
root_object.mmd_root.use_property_driver = True
|
||||
@@ -466,13 +501,15 @@ class DisassembleAll(bpy.types.Operator):
|
||||
bl_label = "Disassemble All"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object = context.active_object
|
||||
root_object = FnModel.find_root_object(active_object)
|
||||
assert root_object is not None
|
||||
|
||||
logger.info(f"Disassembling all components for {root_object.name}")
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
|
||||
root_object.mmd_root.use_property_driver = False
|
||||
logger.debug("Unbinding SDEF weights")
|
||||
with context.temp_override(selected_objects=[active_object]):
|
||||
bpy.ops.mmd_tools.sdef_unbind()
|
||||
|
||||
|
||||
@@ -7,13 +7,17 @@
|
||||
|
||||
import itertools
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List, Optional, Set
|
||||
from typing import Dict, List, Optional, Set, Tuple, Any
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
|
||||
|
||||
from ..bpyutils import FnContext
|
||||
from ..core.model import FnModel, Model
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
class MessageException(Exception):
|
||||
@@ -35,8 +39,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
active_object: Optional[bpy.types.Object] = context.active_object
|
||||
def poll(cls, context: Context) -> bool:
|
||||
active_object: Optional[Object] = context.active_object
|
||||
|
||||
if context.mode != "POSE":
|
||||
return False
|
||||
@@ -52,19 +56,22 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
|
||||
return len(context.selected_pose_bones) > 0
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
logger.info("Starting model join by bones operation")
|
||||
self.join(context)
|
||||
logger.info("Model join by bones completed successfully")
|
||||
except MessageException as ex:
|
||||
logger.error(f"Model join by bones failed: {str(ex)}")
|
||||
self.report(type={"ERROR"}, message=str(ex))
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def join(self, context: bpy.types.Context):
|
||||
def join(self, context: Context) -> None:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
parent_root_object = FnModel.find_root_object(context.active_object)
|
||||
@@ -74,6 +81,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
if parent_root_object is None or len(child_root_objects) == 0:
|
||||
raise MessageException("No MMD Models selected")
|
||||
|
||||
logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}")
|
||||
with FnContext.temp_override_active_layer_collection(context, parent_root_object):
|
||||
FnModel.join_models(parent_root_object, child_root_objects)
|
||||
|
||||
@@ -82,11 +90,12 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
|
||||
# Connect child bones
|
||||
if self.join_type == "CONNECTED":
|
||||
parent_edit_bone: bpy.types.EditBone = context.active_bone
|
||||
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
|
||||
parent_edit_bone: EditBone = context.active_bone
|
||||
child_edit_bones: Set[EditBone] = set(context.selected_bones)
|
||||
child_edit_bones.remove(parent_edit_bone)
|
||||
|
||||
child_edit_bone: bpy.types.EditBone
|
||||
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
|
||||
child_edit_bone: EditBone
|
||||
for child_edit_bone in child_edit_bones:
|
||||
child_edit_bone.use_connect = True
|
||||
|
||||
@@ -111,8 +120,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
active_object: Optional[bpy.types.Object] = context.active_object
|
||||
def poll(cls, context: Context) -> bool:
|
||||
active_object: Optional[Object] = context.active_object
|
||||
|
||||
if context.mode != "POSE":
|
||||
return False
|
||||
@@ -128,56 +137,70 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
|
||||
return len(context.selected_pose_bones) > 0
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
logger.info("Starting model separate by bones operation")
|
||||
self.separate(context)
|
||||
logger.info("Model separate by bones completed successfully")
|
||||
except MessageException as ex:
|
||||
logger.error(f"Model separate by bones failed: {str(ex)}")
|
||||
self.report(type={"ERROR"}, message=str(ex))
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def separate(self, context: bpy.types.Context):
|
||||
def separate(self, context: Context) -> None:
|
||||
weight_threshold: float = self.weight_threshold
|
||||
mmd_scale = 0.08
|
||||
|
||||
target_armature_object: bpy.types.Object = context.active_object
|
||||
target_armature_object: Object = context.active_object
|
||||
logger.debug(f"Target armature: {target_armature_object.name}")
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
|
||||
root_bones: Set[EditBone] = set(context.selected_bones)
|
||||
logger.debug(f"Selected root bones: {len(root_bones)}")
|
||||
|
||||
if self.include_descendant_bones:
|
||||
logger.debug("Including descendant bones")
|
||||
for edit_bone in root_bones:
|
||||
with context.temp_override(active_bone=edit_bone):
|
||||
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
|
||||
|
||||
separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones}
|
||||
deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
|
||||
separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones}
|
||||
deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
|
||||
logger.debug(f"Total bones to separate: {len(separate_bones)}")
|
||||
|
||||
mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object)
|
||||
mmd_root_object: Object = FnModel.find_root_object(context.active_object)
|
||||
mmd_model = Model(mmd_root_object)
|
||||
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
|
||||
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
|
||||
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
|
||||
|
||||
mmd_model_mesh_objects = list(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
|
||||
mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
|
||||
mmd_model_mesh_objects = list(mesh_selection_result.keys())
|
||||
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
|
||||
|
||||
# separate armature bones
|
||||
separate_armature_object: Optional[bpy.types.Object]
|
||||
separate_armature_object: Optional[Object]
|
||||
if self.separate_armature:
|
||||
logger.debug("Separating armature")
|
||||
target_armature_object.select_set(True)
|
||||
bpy.ops.armature.separate()
|
||||
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
|
||||
if separate_armature_object:
|
||||
logger.debug(f"Created separate armature: {separate_armature_object.name}")
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# collect separate rigid bodies
|
||||
separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
|
||||
separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
|
||||
logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate")
|
||||
|
||||
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
|
||||
|
||||
# collect separate joints
|
||||
separate_joints: Set[bpy.types.Object] = {
|
||||
separate_joints: Set[Object] = {
|
||||
joint_object
|
||||
for joint_object in mmd_model.joints()
|
||||
if boundary_joint_owner_condition(
|
||||
@@ -187,35 +210,43 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
]
|
||||
)
|
||||
}
|
||||
logger.debug(f"Found {len(separate_joints)} joints to separate")
|
||||
|
||||
separate_mesh_objects: Set[bpy.types.Object]
|
||||
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object]
|
||||
separate_mesh_objects: Set[Object]
|
||||
model2separate_mesh_objects: Dict[Object, Object]
|
||||
if len(mmd_model_mesh_objects) == 0:
|
||||
logger.debug("No mesh objects to separate")
|
||||
separate_mesh_objects = set()
|
||||
model2separate_mesh_objects = dict()
|
||||
else:
|
||||
# select meshes
|
||||
obj: bpy.types.Object
|
||||
logger.debug("Selecting meshes for separation")
|
||||
obj: Object
|
||||
for obj in context.view_layer.objects:
|
||||
obj.select_set(obj in mmd_model_mesh_objects)
|
||||
context.view_layer.objects.active = mmd_model_mesh_objects[0]
|
||||
|
||||
# separate mesh by selected vertices
|
||||
logger.debug("Separating meshes by selected vertices")
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.mesh.separate(type="SELECTED")
|
||||
separate_mesh_objects: List[bpy.types.Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
|
||||
separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects")
|
||||
|
||||
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
|
||||
|
||||
logger.debug(f"Creating new model with scale {mmd_scale}")
|
||||
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False)
|
||||
|
||||
separate_model.initialDisplayFrames()
|
||||
separate_root_object = separate_model.rootObject()
|
||||
separate_root_object.matrix_world = mmd_root_object.matrix_world
|
||||
separate_model_armature_object = separate_model.armature()
|
||||
logger.debug(f"Created separate model with root: {separate_root_object.name}")
|
||||
|
||||
if self.separate_armature:
|
||||
logger.debug("Joining separate armature to new model")
|
||||
with context.temp_override(
|
||||
active_object=separate_model_armature_object,
|
||||
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
|
||||
@@ -223,6 +254,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
bpy.ops.object.join()
|
||||
|
||||
# add mesh
|
||||
logger.debug("Parenting separate mesh objects to new model")
|
||||
with context.temp_override(
|
||||
object=separate_model_armature_object,
|
||||
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
|
||||
@@ -230,19 +262,23 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
|
||||
# replace mesh armature modifier.object
|
||||
logger.debug("Updating armature modifiers on separate meshes")
|
||||
for separate_mesh in separate_mesh_objects:
|
||||
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
|
||||
if armature_modifier is None:
|
||||
logger.debug(f"Creating new armature modifier for {separate_mesh.name}")
|
||||
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
|
||||
|
||||
armature_modifier.object = separate_model_armature_object
|
||||
|
||||
logger.debug("Parenting rigid bodies to new model")
|
||||
with context.temp_override(
|
||||
object=separate_model.rigidGroupObject(),
|
||||
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
|
||||
):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
|
||||
logger.debug("Parenting joints to new model")
|
||||
with context.temp_override(
|
||||
object=separate_model.jointGroupObject(),
|
||||
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
|
||||
@@ -257,10 +293,12 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
assert separate_layer_collection is not None
|
||||
|
||||
if mmd_layer_collection.name != separate_layer_collection.name:
|
||||
logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}")
|
||||
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
|
||||
separate_layer_collection.collection.objects.link(separate_object)
|
||||
mmd_layer_collection.collection.objects.unlink(separate_object)
|
||||
|
||||
logger.debug("Copying MMD root properties")
|
||||
FnModel.copy_mmd_root(
|
||||
separate_root_object,
|
||||
mmd_root_object,
|
||||
@@ -271,13 +309,15 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
},
|
||||
)
|
||||
|
||||
def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]:
|
||||
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = dict()
|
||||
def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]:
|
||||
"""Select vertices weighted to the bones to be separated"""
|
||||
logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}")
|
||||
mesh2selected_vertex_count: Dict[Object, int] = dict()
|
||||
target_bmesh: bmesh.types.BMesh = bmesh.new()
|
||||
for mesh_object in mmd_model_mesh_objects:
|
||||
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
|
||||
|
||||
mesh: bpy.types.Mesh = mesh_object.data
|
||||
mesh: Mesh = mesh_object.data
|
||||
target_bmesh.from_mesh(mesh, face_normals=False)
|
||||
target_bmesh.select_mode |= {"VERT"}
|
||||
deform_layer = target_bmesh.verts.layers.deform.verify()
|
||||
@@ -304,6 +344,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
vert.select_set(True)
|
||||
|
||||
if selected_vertex_count > 0:
|
||||
logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}")
|
||||
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
|
||||
target_bmesh.select_flush_mode()
|
||||
target_bmesh.to_mesh(mesh)
|
||||
|
||||
+59
-32
@@ -5,7 +5,7 @@
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import Optional, cast
|
||||
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
|
||||
|
||||
import bpy
|
||||
from mathutils import Quaternion, Vector
|
||||
@@ -16,10 +16,11 @@ from ..core.exceptions import MaterialNotFoundError
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.morph import FnMorph
|
||||
from ..utils import ItemMoveOp, ItemOp
|
||||
from ....logging_setup import logger
|
||||
|
||||
|
||||
# Util functions
|
||||
def divide_vector_components(vec1, vec2):
|
||||
def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
|
||||
if len(vec1) != len(vec2):
|
||||
raise ValueError("Vectors should have the same number of components")
|
||||
result = []
|
||||
@@ -33,7 +34,7 @@ def divide_vector_components(vec1, vec2):
|
||||
return result
|
||||
|
||||
|
||||
def multiply_vector_components(vec1, vec2):
|
||||
def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
|
||||
if len(vec1) != len(vec2):
|
||||
raise ValueError("Vectors should have the same number of components")
|
||||
result = []
|
||||
@@ -42,7 +43,7 @@ def multiply_vector_components(vec1, vec2):
|
||||
return result
|
||||
|
||||
|
||||
def special_division(n1, n2):
|
||||
def special_division(n1: float, n2: float) -> float:
|
||||
"""This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
|
||||
if n2 == 0:
|
||||
if n1 == 0:
|
||||
@@ -58,7 +59,7 @@ class AddMorph(bpy.types.Operator):
|
||||
bl_description = "Add a morph item to active morph list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -68,6 +69,7 @@ class AddMorph(bpy.types.Operator):
|
||||
morph.name = "New Morph"
|
||||
if morph_type.startswith("uv"):
|
||||
morph.data_type = "VERTEX_GROUP"
|
||||
logger.debug(f"Added new morph of type {morph_type}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -84,7 +86,7 @@ class RemoveMorph(bpy.types.Operator):
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -99,9 +101,11 @@ class RemoveMorph(bpy.types.Operator):
|
||||
if self.all:
|
||||
morphs.clear()
|
||||
mmd_root.active_morph = 0
|
||||
logger.debug(f"Removed all morphs of type {morph_type}")
|
||||
else:
|
||||
morphs.remove(mmd_root.active_morph)
|
||||
mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
|
||||
logger.debug(f"Removed morph at index {mmd_root.active_morph} of type {morph_type}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -111,7 +115,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
||||
bl_description = "Move active morph item up/down in the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -120,6 +124,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
||||
mmd_root.active_morph,
|
||||
self.type,
|
||||
)
|
||||
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -129,7 +134,7 @@ class CopyMorph(bpy.types.Operator):
|
||||
bl_description = "Make a copy of active morph in the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -156,6 +161,7 @@ class CopyMorph(bpy.types.Operator):
|
||||
for k, v in morph.items():
|
||||
morph_new[k] = v if k != "name" else name_tmp
|
||||
morph_new.name = name_orig + "_copy" # trigger name check
|
||||
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -165,17 +171,17 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
return False
|
||||
|
||||
return root.mmd_root.active_morph_type == "bone_morphs"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
|
||||
|
||||
logger.info("Overwrote bone morphs from active action pose")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -185,7 +191,7 @@ class AddMorphOffset(bpy.types.Operator):
|
||||
bl_description = "Add a morph offset item to the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -210,6 +216,7 @@ class AddMorphOffset(bpy.types.Operator):
|
||||
item.location = pose_bone.location
|
||||
item.rotation = pose_bone.rotation_quaternion
|
||||
|
||||
logger.debug(f"Added morph offset to {morph_type}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -226,7 +233,7 @@ class RemoveMorphOffset(bpy.types.Operator):
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -243,17 +250,21 @@ class RemoveMorphOffset(bpy.types.Operator):
|
||||
if morph_type.startswith("vertex"):
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.remove_shape_key(obj, morph.name)
|
||||
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
|
||||
return {"FINISHED"}
|
||||
elif morph_type.startswith("uv"):
|
||||
if morph.data_type == "VERTEX_GROUP":
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.store_uv_morph_data(obj, morph)
|
||||
logger.debug(f"Removed all UV morph offsets for {morph.name}")
|
||||
return {"FINISHED"}
|
||||
morph.data.clear()
|
||||
morph.active_data = 0
|
||||
logger.debug(f"Cleared all morph offsets for {morph.name}")
|
||||
else:
|
||||
morph.data.remove(morph.active_data)
|
||||
morph.active_data = max(0, morph.active_data - 1)
|
||||
logger.debug(f"Removed morph offset at index {morph.active_data}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -269,7 +280,7 @@ class InitMaterialOffset(bpy.types.Operator):
|
||||
default=0,
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -281,6 +292,7 @@ class InitMaterialOffset(bpy.types.Operator):
|
||||
mat_data.specular_color = mat_data.ambient_color = (val,) * 3
|
||||
mat_data.shininess = mat_data.edge_weight = val
|
||||
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4
|
||||
logger.debug(f"Initialized material offset with value {val}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -290,7 +302,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -328,6 +340,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
|
||||
except ZeroDivisionError:
|
||||
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
|
||||
logger.warning("Zero division detected, switching to ADD offset type")
|
||||
except ValueError:
|
||||
self.report({"ERROR"}, "An unexpected error happened")
|
||||
# We should stop on our tracks and re-raise the exception
|
||||
@@ -345,6 +358,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight
|
||||
|
||||
FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
|
||||
logger.info(f"Applied material offset for {mat_data.material}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -354,7 +368,7 @@ class CreateWorkMaterial(bpy.types.Operator):
|
||||
bl_description = "Creates a temporary material to edit this offset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -413,6 +427,7 @@ class CreateWorkMaterial(bpy.types.Operator):
|
||||
work_mmd_mat.edge_color = list(edge_offset)
|
||||
work_mmd_mat.edge_weight += mat_data.edge_weight
|
||||
|
||||
logger.info(f"Created work material {work_mat_name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -422,13 +437,13 @@ class ClearTempMaterials(bpy.types.Operator):
|
||||
bl_description = "Clears all the temporary materials"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
for meshObj in FnModel.iterate_mesh_objects(root):
|
||||
|
||||
def __pre_remove(m):
|
||||
def __pre_remove(m: Optional[bpy.types.Material]) -> bool:
|
||||
if m and "_temp" in m.name:
|
||||
base_mat_name = m.name.split("_temp")[0]
|
||||
try:
|
||||
@@ -439,6 +454,7 @@ class ClearTempMaterials(bpy.types.Operator):
|
||||
return False
|
||||
|
||||
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
|
||||
logger.info("Cleared all temporary materials")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -448,7 +464,7 @@ class ViewBoneMorph(bpy.types.Operator):
|
||||
bl_description = "View the result of active bone morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -463,6 +479,7 @@ class ViewBoneMorph(bpy.types.Operator):
|
||||
mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4()
|
||||
mtx.translation = p_bone.location + morph_data.location
|
||||
p_bone.matrix_basis = mtx
|
||||
logger.info(f"Viewing bone morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -472,13 +489,14 @@ class ClearBoneMorphView(bpy.types.Operator):
|
||||
bl_description = "Reset transforms of all bones to their default values"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
armature = FnModel.find_armature_object(root)
|
||||
for p_bone in armature.pose.bones:
|
||||
p_bone.matrix_basis.identity()
|
||||
logger.info("Cleared bone morph view")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -488,7 +506,7 @@ class ApplyBoneMorph(bpy.types.Operator):
|
||||
bl_description = "Apply current pose to active bone morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -506,6 +524,7 @@ class ApplyBoneMorph(bpy.types.Operator):
|
||||
p_bone.bone.select = True
|
||||
else:
|
||||
p_bone.bone.select = False
|
||||
logger.info(f"Applied current pose to bone morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -515,7 +534,7 @@ class SelectRelatedBone(bpy.types.Operator):
|
||||
bl_description = "Select the bone assigned to this offset in the armature"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -524,6 +543,7 @@ class SelectRelatedBone(bpy.types.Operator):
|
||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||
morph_data = morph.data[morph.active_data]
|
||||
utils.selectSingleBone(context, armature, morph_data.bone)
|
||||
logger.debug(f"Selected bone: {morph_data.bone}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -533,7 +553,7 @@ class EditBoneOffset(bpy.types.Operator):
|
||||
bl_description = "Applies the location and rotation of this offset to the bone"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -546,6 +566,7 @@ class EditBoneOffset(bpy.types.Operator):
|
||||
mtx.translation = morph_data.location
|
||||
p_bone.matrix_basis = mtx
|
||||
utils.selectSingleBone(context, armature, p_bone.name)
|
||||
logger.debug(f"Edited bone offset for {p_bone.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -555,7 +576,7 @@ class ApplyBoneOffset(bpy.types.Operator):
|
||||
bl_description = "Stores the current bone location and rotation into this offset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -567,6 +588,7 @@ class ApplyBoneOffset(bpy.types.Operator):
|
||||
p_bone = armature.pose.bones[morph_data.bone]
|
||||
morph_data.location = p_bone.location
|
||||
morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
|
||||
logger.debug(f"Applied bone offset for {p_bone.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -576,7 +598,7 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
bl_description = "View the result of active UV morph on current mesh object"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -627,6 +649,7 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
uv_tex.active_render = True
|
||||
meshObj.hide_set(False)
|
||||
meshObj.select_set(selected)
|
||||
logger.info(f"Viewing UV morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -636,7 +659,7 @@ class ClearUVMorphView(bpy.types.Operator):
|
||||
bl_description = "Clear all temporary data of UV morphs"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -664,6 +687,7 @@ class ClearUVMorphView(bpy.types.Operator):
|
||||
for act in bpy.data.actions:
|
||||
if act.name.startswith("__uv.") and act.users < 1:
|
||||
bpy.data.actions.remove(act)
|
||||
logger.info("Cleared UV morph view")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -674,14 +698,14 @@ class EditUVMorph(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
obj = context.active_object
|
||||
if obj.type != "MESH":
|
||||
return False
|
||||
active_uv_layer = obj.data.uv_layers.active
|
||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
meshObj = obj
|
||||
|
||||
@@ -704,6 +728,7 @@ class EditUVMorph(bpy.types.Operator):
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
meshObj.select_set(selected)
|
||||
logger.info("Editing UV morph")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -714,14 +739,14 @@ class ApplyUVMorph(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
obj = context.active_object
|
||||
if obj.type != "MESH":
|
||||
return False
|
||||
active_uv_layer = obj.data.uv_layers.active
|
||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -756,6 +781,7 @@ class ApplyUVMorph(bpy.types.Operator):
|
||||
morph.data_type = "VERTEX_GROUP"
|
||||
|
||||
meshObj.select_set(selected)
|
||||
logger.info(f"Applied UV morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -766,11 +792,12 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
mmd_root_object = FnModel.find_root_object(context.active_object)
|
||||
FnMorph.clean_duplicated_material_morphs(mmd_root_object)
|
||||
logger.info("Cleaned duplicated material morphs")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import math
|
||||
from typing import Dict, Optional, Tuple, cast
|
||||
from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator
|
||||
|
||||
import bpy
|
||||
from mathutils import Euler, Vector
|
||||
@@ -16,6 +16,7 @@ from ..bpyutils import FnContext, Props
|
||||
from ..core import rigid_body
|
||||
from ..core.model import FnModel, Model
|
||||
from ..core.rigid_body import FnRigidBody
|
||||
from ...logging_setup import logger
|
||||
|
||||
|
||||
class SelectRigidBody(bpy.types.Operator):
|
||||
@@ -43,15 +44,15 @@ class SelectRigidBody(bpy.types.Operator):
|
||||
default=False,
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return FnModel.is_rigid_body_object(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root is None:
|
||||
@@ -173,7 +174,7 @@ class AddRigidBody(bpy.types.Operator):
|
||||
default=0.1,
|
||||
)
|
||||
|
||||
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None):
|
||||
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object:
|
||||
name_j: str = self.name_j
|
||||
name_e: str = self.name_e
|
||||
size = self.size.copy()
|
||||
@@ -226,7 +227,7 @@ class AddRigidBody(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
return False
|
||||
@@ -237,7 +238,7 @@ class AddRigidBody(bpy.types.Operator):
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object = context.active_object
|
||||
|
||||
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
|
||||
@@ -254,15 +255,17 @@ class AddRigidBody(bpy.types.Operator):
|
||||
|
||||
armature_object.select_set(False)
|
||||
if len(selected_pose_bones) > 0:
|
||||
logger.info(f"Adding rigid bodies to {len(selected_pose_bones)} selected bones")
|
||||
for pose_bone in selected_pose_bones:
|
||||
rigid = self.__add_rigid_body(context, root_object, pose_bone)
|
||||
rigid.select_set(True)
|
||||
else:
|
||||
logger.info("Adding a single rigid body without bone attachment")
|
||||
rigid = self.__add_rigid_body(context, root_object)
|
||||
rigid.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
||||
no_bone = True
|
||||
if context.selected_bones and len(context.selected_bones) > 0:
|
||||
no_bone = False
|
||||
@@ -288,12 +291,13 @@ class RemoveRigidBody(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return FnModel.is_rigid_body_object(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
logger.info(f"Removing rigid body: {obj.name}")
|
||||
utils.selectAObject(obj) # ensure this is the only one object select
|
||||
bpy.ops.object.delete(use_global=True)
|
||||
if root:
|
||||
@@ -306,7 +310,8 @@ class RigidBodyBake(bpy.types.Operator):
|
||||
bl_label = "Bake"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
logger.info("Baking rigid body simulation")
|
||||
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
||||
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
|
||||
|
||||
@@ -318,7 +323,8 @@ class RigidBodyDeleteBake(bpy.types.Operator):
|
||||
bl_label = "Delete Bake"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
logger.info("Deleting rigid body simulation bake")
|
||||
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
||||
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
|
||||
|
||||
@@ -381,7 +387,7 @@ class AddJoint(bpy.types.Operator):
|
||||
min=0,
|
||||
)
|
||||
|
||||
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
|
||||
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> Generator[Tuple[bpy.types.Object, bpy.types.Object], None, None]:
|
||||
obj_seq = tuple(bone_map.keys())
|
||||
for rigid_a, bone_a in bone_map.items():
|
||||
for rigid_b, bone_b in bone_map.items():
|
||||
@@ -394,7 +400,7 @@ class AddJoint(bpy.types.Operator):
|
||||
else:
|
||||
yield obj_seq
|
||||
|
||||
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map):
|
||||
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> bpy.types.Object:
|
||||
loc: Optional[Vector] = None
|
||||
rot = Euler((0.0, 0.0, 0.0))
|
||||
rigid_a, rigid_b = rigid_pair
|
||||
@@ -432,7 +438,7 @@ class AddJoint(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
return False
|
||||
@@ -443,7 +449,7 @@ class AddJoint(bpy.types.Operator):
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
active_object = context.active_object
|
||||
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
|
||||
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
|
||||
@@ -456,15 +462,19 @@ class AddJoint(bpy.types.Operator):
|
||||
|
||||
FnContext.select_single_object(context, root_object).select_set(False)
|
||||
if context.scene.rigidbody_world is None:
|
||||
logger.info("Creating rigid body world")
|
||||
bpy.ops.rigidbody.world_add()
|
||||
|
||||
joint_count = 0
|
||||
for pair in self.__enumerate_rigid_pair(bone_map):
|
||||
joint = self.__add_joint(context, root_object, pair, bone_map)
|
||||
joint.select_set(True)
|
||||
joint_count += 1
|
||||
|
||||
logger.info(f"Added {joint_count} joints between rigid bodies")
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
@@ -476,12 +486,13 @@ class RemoveJoint(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return FnModel.is_joint_object(context.active_object)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
logger.info(f"Removing joint: {obj.name}")
|
||||
utils.selectAObject(obj) # ensure this is the only one object select
|
||||
bpy.ops.object.delete(use_global=True)
|
||||
if root:
|
||||
@@ -496,7 +507,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@staticmethod
|
||||
def __get_rigid_body_world_objects():
|
||||
def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]:
|
||||
rigid_body.setRigidBodyWorldEnabled(True)
|
||||
rbw = bpy.context.scene.rigidbody_world
|
||||
if not rbw.collection:
|
||||
@@ -511,12 +522,12 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
|
||||
return rbw.collection.objects, rbw.constraints.objects
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
scene = context.scene
|
||||
scene_objs = set(scene.objects)
|
||||
scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects)
|
||||
|
||||
def _update_group(obj, group):
|
||||
def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool:
|
||||
if obj in scene_objs:
|
||||
if obj not in group.values():
|
||||
group.link(obj)
|
||||
@@ -525,7 +536,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
group.unlink(obj)
|
||||
return False
|
||||
|
||||
def _references(obj):
|
||||
def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]:
|
||||
yield obj
|
||||
if getattr(obj, "proxy", None):
|
||||
yield from _references(obj.proxy)
|
||||
@@ -542,6 +553,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
# Object.rigid_body are removed,
|
||||
# but Object.rigid_body_constraint are retained.
|
||||
# Therefore, it must be checked with Object.mmd_type.
|
||||
logger.info("Updating rigid body world objects")
|
||||
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
|
||||
if not _update_group(i, rb_objs):
|
||||
continue
|
||||
@@ -556,6 +568,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
|
||||
# mass, friction, restitution, linear_dumping, angular_dumping
|
||||
|
||||
logger.info("Updating rigid body constraints")
|
||||
for i in (x for x in objects if x.rigid_body_constraint):
|
||||
if not _update_group(i, rbc_objs):
|
||||
continue
|
||||
@@ -566,6 +579,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
|
||||
|
||||
if need_rebuild_physics:
|
||||
logger.info("Rebuilding physics for models")
|
||||
for root_object in scene.objects:
|
||||
if root_object.mmd_type != "ROOT":
|
||||
continue
|
||||
|
||||
+17
-11
@@ -5,18 +5,19 @@
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import Set
|
||||
from typing import Set, Tuple
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from bpy.types import Operator, Context, Object
|
||||
|
||||
from ..core.model import FnModel
|
||||
from ..core.sdef import FnSDEF
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
def _get_target_objects(context):
|
||||
root_objects: Set[bpy.types.Object] = set()
|
||||
selected_objects: Set[bpy.types.Object] = set()
|
||||
def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]:
|
||||
root_objects: Set[Object] = set()
|
||||
selected_objects: Set[Object] = set()
|
||||
for i in context.selected_objects:
|
||||
if i.type == "MESH":
|
||||
selected_objects.add(i)
|
||||
@@ -40,11 +41,13 @@ class ResetSDEFCache(Operator):
|
||||
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
target_meshes, _ = _get_target_objects(context)
|
||||
logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects")
|
||||
for i in target_meshes:
|
||||
FnSDEF.clear_cache(i)
|
||||
FnSDEF.clear_cache(unused_only=True)
|
||||
logger.debug("SDEF cache reset completed")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -75,19 +78,20 @@ class BindSDEF(Operator):
|
||||
default=False,
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
# TODO: Utility Functionalize
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
target_meshes, root_objects = _get_target_objects(context)
|
||||
logger.info(f"Binding SDEF for {len(target_meshes)} objects with mode={self.mode}, skip={self.use_skip}, scale={self.use_scale}")
|
||||
|
||||
for r in root_objects:
|
||||
r.mmd_root.use_sdef = True
|
||||
|
||||
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
|
||||
count = sum(FnSDEF.bind(i, *param) for i in target_meshes)
|
||||
logger.info(f"Successfully bound SDEF for {count} of {len(target_meshes)} meshes")
|
||||
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -98,13 +102,15 @@ class UnbindSDEF(Operator):
|
||||
bl_description = "Unbind MMD SDEF data of selected objects"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
# TODO: Utility Functionalize
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
target_meshes, root_objects = _get_target_objects(context)
|
||||
logger.info(f"Unbinding SDEF for {len(target_meshes)} objects")
|
||||
|
||||
for i in target_meshes:
|
||||
FnSDEF.unbind(i)
|
||||
|
||||
for r in root_objects:
|
||||
r.mmd_root.use_sdef = False
|
||||
|
||||
logger.debug("SDEF unbinding completed")
|
||||
return {"FINISHED"}
|
||||
|
||||
+42
-34
@@ -6,29 +6,32 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import re
|
||||
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
|
||||
|
||||
from bpy.types import Operator
|
||||
from mathutils import Matrix
|
||||
from bpy.types import Operator, Context
|
||||
from mathutils import Matrix, Vector, Quaternion
|
||||
|
||||
from ...logging_setup import logger
|
||||
|
||||
|
||||
class _SetShadingBase:
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_options: Set[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
@staticmethod
|
||||
def _get_view3d_spaces(context):
|
||||
def _get_view3d_spaces(context: Context) -> Iterator[Any]:
|
||||
if getattr(context.area, "type", None) == "VIEW_3D":
|
||||
return (context.area.spaces[0],)
|
||||
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
|
||||
|
||||
@staticmethod
|
||||
def _reset_color_management(context, use_display_device=True):
|
||||
def _reset_color_management(context: Context, use_display_device: bool = True) -> None:
|
||||
try:
|
||||
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _reset_material_shading(context, use_shadeless=False):
|
||||
def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None:
|
||||
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
|
||||
for s in i.material_slots:
|
||||
if s.material is None:
|
||||
@@ -36,10 +39,11 @@ class _SetShadingBase:
|
||||
s.material.use_nodes = False
|
||||
s.material.use_shadeless = use_shadeless
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Dict[str, str]:
|
||||
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
|
||||
logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT")
|
||||
|
||||
shading_mode = getattr(self, "_shading_mode", None)
|
||||
shading_mode: Optional[str] = getattr(self, "_shading_mode", None)
|
||||
for space in self._get_view3d_spaces(context):
|
||||
shading = space.shading
|
||||
shading.type = "SOLID"
|
||||
@@ -47,39 +51,40 @@ class _SetShadingBase:
|
||||
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
|
||||
shading.show_object_outline = False
|
||||
shading.show_backface_culling = False
|
||||
logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetGLSLShading(Operator, _SetShadingBase):
|
||||
bl_idname = "mmd_tools.set_glsl_shading"
|
||||
bl_label = "GLSL View"
|
||||
bl_description = "Use GLSL shading with additional lighting"
|
||||
bl_idname: str = "mmd_tools.set_glsl_shading"
|
||||
bl_label: str = "GLSL View"
|
||||
bl_description: str = "Use GLSL shading with additional lighting"
|
||||
|
||||
_shading_mode = "GLSL"
|
||||
_shading_mode: str = "GLSL"
|
||||
|
||||
|
||||
class SetShadelessGLSLShading(Operator, _SetShadingBase):
|
||||
bl_idname = "mmd_tools.set_shadeless_glsl_shading"
|
||||
bl_label = "Shadeless GLSL View"
|
||||
bl_description = "Use only toon shading"
|
||||
bl_idname: str = "mmd_tools.set_shadeless_glsl_shading"
|
||||
bl_label: str = "Shadeless GLSL View"
|
||||
bl_description: str = "Use only toon shading"
|
||||
|
||||
_shading_mode = "SHADELESS"
|
||||
_shading_mode: str = "SHADELESS"
|
||||
|
||||
|
||||
class ResetShading(Operator, _SetShadingBase):
|
||||
bl_idname = "mmd_tools.reset_shading"
|
||||
bl_label = "Reset View"
|
||||
bl_description = "Reset to default Blender shading"
|
||||
bl_idname: str = "mmd_tools.reset_shading"
|
||||
bl_label: str = "Reset View"
|
||||
bl_description: str = "Reset to default Blender shading"
|
||||
|
||||
|
||||
class FlipPose(Operator):
|
||||
bl_idname = "mmd_tools.flip_pose"
|
||||
bl_label = "Flip Pose"
|
||||
bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
bl_idname: str = "mmd_tools.flip_pose"
|
||||
bl_label: str = "Flip Pose"
|
||||
bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
|
||||
bl_options: Set[str] = {"REGISTER", "UNDO"}
|
||||
|
||||
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
|
||||
__LR_REGEX = [
|
||||
__LR_REGEX: List[Dict[str, Any]] = [
|
||||
{"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
|
||||
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
|
||||
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
|
||||
@@ -87,7 +92,7 @@ class FlipPose(Operator):
|
||||
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
|
||||
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
|
||||
]
|
||||
__LR_MAP = {
|
||||
__LR_MAP: Dict[str, str] = {
|
||||
"RIGHT": "LEFT",
|
||||
"Right": "Left",
|
||||
"right": "left",
|
||||
@@ -103,7 +108,7 @@ class FlipPose(Operator):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def flip_name(cls, name):
|
||||
def flip_name(cls, name: str) -> str:
|
||||
for regex in cls.__LR_REGEX:
|
||||
match = regex["re"].match(name)
|
||||
if match:
|
||||
@@ -121,17 +126,15 @@ class FlipPose(Operator):
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def __cmul(vec1, vec2):
|
||||
def __cmul(vec1: Union[Vector, Quaternion], vec2: Tuple[float, float, float, float]) -> Union[Vector, Quaternion]:
|
||||
return type(vec1)([x * y for x, y in zip(vec1, vec2)])
|
||||
|
||||
@staticmethod
|
||||
def __matrix_compose(loc, rot, scale):
|
||||
def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix:
|
||||
return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)])
|
||||
|
||||
@classmethod
|
||||
def __flip_pose(cls, matrix_basis, bone_src, bone_dest):
|
||||
from mathutils import Quaternion
|
||||
|
||||
def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None:
|
||||
m = bone_dest.bone.matrix_local.to_3x3().transposed()
|
||||
mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
|
||||
loc, rot, scale = matrix_basis.decompose()
|
||||
@@ -140,11 +143,16 @@ class FlipPose(Operator):
|
||||
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: Context) -> Dict[str, str]:
|
||||
logger.info("Executing flip pose operation")
|
||||
pose_bones = context.active_object.pose.bones
|
||||
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
|
||||
self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b))
|
||||
flip_name = self.flip_name(b.name)
|
||||
target_bone = pose_bones.get(flip_name, b)
|
||||
logger.debug(f"Flipping pose from {b.name} to {target_bone.name}")
|
||||
self.__flip_pose(mat, b, target_bone)
|
||||
logger.info("Flip pose operation completed")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -6,81 +6,85 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import bpy
|
||||
from typing import Optional, Set, Dict, Any, List, Tuple, Union, Type
|
||||
|
||||
from .. import utils
|
||||
from ..core import material
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.model import FnModel
|
||||
from . import patch_library_overridable
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_ambient_color()
|
||||
|
||||
|
||||
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_diffuse_color()
|
||||
|
||||
|
||||
def _mmd_material_update_alpha(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_alpha(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_alpha()
|
||||
|
||||
|
||||
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_specular_color()
|
||||
|
||||
|
||||
def _mmd_material_update_shininess(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_shininess(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_shininess()
|
||||
|
||||
|
||||
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_is_double_sided()
|
||||
|
||||
|
||||
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context):
|
||||
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object)
|
||||
|
||||
|
||||
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_toon_texture()
|
||||
|
||||
|
||||
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_drop_shadow()
|
||||
|
||||
|
||||
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_self_shadow_map()
|
||||
|
||||
|
||||
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_self_shadow()
|
||||
|
||||
|
||||
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_enabled_toon_edge()
|
||||
|
||||
|
||||
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_edge_color()
|
||||
|
||||
|
||||
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context):
|
||||
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
|
||||
FnMaterial(prop.id_data).update_edge_weight()
|
||||
|
||||
|
||||
def _mmd_material_get_name_j(prop: "MMDMaterial"):
|
||||
def _mmd_material_get_name_j(prop: "MMDMaterial") -> str:
|
||||
return prop.get("name_j", "")
|
||||
|
||||
|
||||
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str):
|
||||
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str) -> None:
|
||||
prop_value = value
|
||||
if prop_value and prop_value != prop.get("name_j"):
|
||||
root = FnModel.find_root_object(bpy.context.active_object)
|
||||
if root is None:
|
||||
logger.debug(f"No root object found, using unique name for material: {value}")
|
||||
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials})
|
||||
else:
|
||||
logger.debug(f"Root object found, using unique name for material within model: {value}")
|
||||
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)})
|
||||
|
||||
prop["name_j"] = prop_value
|
||||
@@ -275,13 +279,15 @@ class MMDMaterial(bpy.types.PropertyGroup):
|
||||
description="Comment",
|
||||
)
|
||||
|
||||
def is_id_unique(self):
|
||||
def is_id_unique(self) -> bool:
|
||||
return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None)
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
def register() -> None:
|
||||
logger.debug("Registering MMD material properties")
|
||||
bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial))
|
||||
|
||||
@staticmethod
|
||||
def unregister():
|
||||
def unregister() -> None:
|
||||
logger.debug("Unregistering MMD material properties")
|
||||
del bpy.types.Material.mmd_material
|
||||
|
||||
@@ -6,33 +6,33 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import bpy
|
||||
from typing import Optional, List, Dict, Any, Set, Tuple, Union, TypeVar, Type
|
||||
from bpy.types import PropertyGroup, Object, ShapeKey
|
||||
|
||||
from .. import utils
|
||||
from ..core.bone import FnBone
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.model import FnModel, Model
|
||||
from ..core.morph import FnMorph
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
def _morph_base_get_name(prop: "_MorphBase") -> str:
|
||||
return prop.get("name", "")
|
||||
|
||||
|
||||
def _morph_base_set_name(prop: "_MorphBase", value: str):
|
||||
def _morph_base_set_name(prop: "_MorphBase", value: str) -> None:
|
||||
mmd_root = prop.id_data.mmd_root
|
||||
# morph_type = mmd_root.active_morph_type
|
||||
morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower()
|
||||
# assert(prop.bl_rna.identifier.endswith('Morph'))
|
||||
# logging.debug('_set_name: %s %s %s', prop, value, morph_type)
|
||||
prop_name = prop.get("name", None)
|
||||
if prop_name == value:
|
||||
return
|
||||
|
||||
used_names = {x.name for x in getattr(mmd_root, morph_type) if x != prop}
|
||||
used_names: Set[str] = {x.name for x in getattr(mmd_root, morph_type) if x != prop}
|
||||
value = utils.unique_name(value, used_names)
|
||||
if prop_name is not None:
|
||||
if morph_type == "vertex_morphs":
|
||||
kb_list = {}
|
||||
kb_list: Dict[str, List[ShapeKey]] = {}
|
||||
for mesh in FnModel.iterate_mesh_objects(prop.id_data):
|
||||
for kb in getattr(mesh.data.shape_keys, "key_blocks", ()):
|
||||
kb_list.setdefault(kb.name, []).append(kb)
|
||||
@@ -43,7 +43,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str):
|
||||
kb.name = value
|
||||
|
||||
elif morph_type == "uv_morphs":
|
||||
vg_list = {}
|
||||
vg_list: Dict[str, List[Any]] = {}
|
||||
for mesh in FnModel.iterate_mesh_objects(prop.id_data):
|
||||
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh):
|
||||
vg_list.setdefault(n, []).append(vg)
|
||||
@@ -72,6 +72,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str):
|
||||
kb.name = value
|
||||
|
||||
prop["name"] = value
|
||||
logger.debug(f"Renamed morph from '{prop_name}' to '{value}'")
|
||||
|
||||
|
||||
class _MorphBase:
|
||||
@@ -101,11 +102,11 @@ class _MorphBase:
|
||||
|
||||
|
||||
def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
|
||||
bone_id = prop.get("bone_id", -1)
|
||||
bone_id: int = prop.get("bone_id", -1)
|
||||
if bone_id < 0:
|
||||
return ""
|
||||
root_object = prop.id_data
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
root_object: Object = prop.id_data
|
||||
armature_object: Optional[Object] = FnModel.find_armature_object(root_object)
|
||||
if armature_object is None:
|
||||
return ""
|
||||
pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id)
|
||||
@@ -114,9 +115,9 @@ def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
|
||||
return pose_bone.name
|
||||
|
||||
|
||||
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str):
|
||||
root = prop.id_data
|
||||
arm = FnModel.find_armature_object(root)
|
||||
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None:
|
||||
root: Object = prop.id_data
|
||||
arm: Optional[Object] = FnModel.find_armature_object(root)
|
||||
|
||||
# Load the library_override file. This function is triggered when loading, but the arm obj cannot be found.
|
||||
# The arm obj is exist, but the relative relationship has not yet been established.
|
||||
@@ -128,9 +129,10 @@ def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str):
|
||||
return
|
||||
pose_bone = arm.pose.bones[value]
|
||||
prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
|
||||
logger.debug(f"Set bone morph data bone to '{value}' with ID {prop['bone_id']}")
|
||||
|
||||
|
||||
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context):
|
||||
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context: bpy.types.Context) -> None:
|
||||
if not prop.name.startswith("mmd_bind"):
|
||||
return
|
||||
arm = FnModel(prop.id_data).morph_slider.dummy_armature
|
||||
@@ -139,6 +141,7 @@ def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context
|
||||
if bone:
|
||||
bone.location = prop.location
|
||||
bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency
|
||||
logger.debug(f"Updated bone morph data location/rotation for '{prop.name}'")
|
||||
|
||||
|
||||
class BoneMorphData(bpy.types.PropertyGroup):
|
||||
@@ -188,40 +191,44 @@ class BoneMorph(_MorphBase, bpy.types.PropertyGroup):
|
||||
)
|
||||
|
||||
|
||||
def _material_morph_data_get_material(prop: "MaterialMorphData"):
|
||||
def _material_morph_data_get_material(prop: "MaterialMorphData") -> str:
|
||||
mat_p = prop.get("material_data", None)
|
||||
if mat_p is not None:
|
||||
return mat_p.name
|
||||
return ""
|
||||
|
||||
|
||||
def _material_morph_data_set_material(prop: "MaterialMorphData", value: str):
|
||||
def _material_morph_data_set_material(prop: "MaterialMorphData", value: str) -> None:
|
||||
if value not in bpy.data.materials:
|
||||
prop["material_data"] = None
|
||||
prop["material_id"] = -1
|
||||
logger.debug(f"Material '{value}' not found, setting material_data to None")
|
||||
else:
|
||||
mat = bpy.data.materials[value]
|
||||
fnMat = FnMaterial(mat)
|
||||
prop["material_data"] = mat
|
||||
prop["material_id"] = fnMat.material_id
|
||||
logger.debug(f"Set material morph data material to '{value}' with ID {fnMat.material_id}")
|
||||
|
||||
|
||||
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str):
|
||||
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str) -> None:
|
||||
mesh = FnModel.find_mesh_object_by_name(prop.id_data, value)
|
||||
if mesh is not None:
|
||||
prop["related_mesh_data"] = mesh.data
|
||||
logger.debug(f"Set material morph data related mesh to '{value}'")
|
||||
else:
|
||||
prop["related_mesh_data"] = None
|
||||
logger.debug(f"Mesh '{value}' not found, setting related_mesh_data to None")
|
||||
|
||||
|
||||
def _material_morph_data_get_related_mesh(prop):
|
||||
def _material_morph_data_get_related_mesh(prop: "MaterialMorphData") -> str:
|
||||
mesh_p = prop.get("related_mesh_data", None)
|
||||
if mesh_p is not None:
|
||||
return mesh_p.name
|
||||
return ""
|
||||
|
||||
|
||||
def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context):
|
||||
def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context: bpy.types.Context) -> None:
|
||||
if not prop.name.startswith("mmd_bind"):
|
||||
return
|
||||
from ..core.shader import _MaterialMorph
|
||||
@@ -229,9 +236,11 @@ def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _co
|
||||
mat = prop["material_data"]
|
||||
if mat is not None:
|
||||
_MaterialMorph.update_morph_inputs(mat, prop)
|
||||
logger.debug(f"Updated material morph modifiable values for '{prop.name}'")
|
||||
else:
|
||||
for mat in FnModel(prop.id_data).materials():
|
||||
_MaterialMorph.update_morph_inputs(mat, prop)
|
||||
logger.debug(f"Updated material morph modifiable values for all materials")
|
||||
|
||||
|
||||
class MaterialMorphData(bpy.types.PropertyGroup):
|
||||
@@ -407,9 +416,6 @@ class UVMorphOffset(bpy.types.PropertyGroup):
|
||||
name="UV Offset",
|
||||
description="UV offset",
|
||||
size=4,
|
||||
# min=-1,
|
||||
# max=1,
|
||||
# precision=3,
|
||||
step=0.1,
|
||||
default=[0, 0, 0, 0],
|
||||
)
|
||||
|
||||
@@ -5,29 +5,33 @@
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
from typing import cast
|
||||
from typing import cast, Optional, Any, Union
|
||||
import bpy
|
||||
from bpy.types import Context, PropertyGroup, PoseBone, Object, Armature
|
||||
|
||||
from ..core.bone import FnBone
|
||||
from . import patch_library_overridable
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context):
|
||||
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: Context) -> None:
|
||||
prop["is_additional_transform_dirty"] = True
|
||||
p_bone = context.active_pose_bone
|
||||
if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer():
|
||||
logger.debug(f"Applying additional transformation for {p_bone.name}")
|
||||
FnBone.apply_additional_transformation(prop.id_data)
|
||||
|
||||
|
||||
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context):
|
||||
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: Context) -> None:
|
||||
pose_bone = context.active_pose_bone
|
||||
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer():
|
||||
logger.debug(f"Updating additional transform influence for {pose_bone.name}")
|
||||
FnBone.update_additional_transform_influence(pose_bone)
|
||||
else:
|
||||
prop["is_additional_transform_dirty"] = True
|
||||
|
||||
|
||||
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
|
||||
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone") -> str:
|
||||
arm = prop.id_data
|
||||
bone_id = prop.get("additional_transform_bone_id", -1)
|
||||
if bone_id < 0:
|
||||
@@ -38,7 +42,7 @@ def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
|
||||
return pose_bone.name
|
||||
|
||||
|
||||
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str):
|
||||
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str) -> None:
|
||||
arm = prop.id_data
|
||||
prop["is_additional_transform_dirty"] = True
|
||||
if value not in arm.pose.bones.keys():
|
||||
@@ -48,7 +52,7 @@ def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str):
|
||||
prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
|
||||
|
||||
|
||||
class MMDBone(bpy.types.PropertyGroup):
|
||||
class MMDBone(PropertyGroup):
|
||||
name_j: bpy.props.StringProperty(
|
||||
name="Name",
|
||||
description="Japanese Name",
|
||||
@@ -184,11 +188,12 @@ class MMDBone(bpy.types.PropertyGroup):
|
||||
|
||||
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True)
|
||||
|
||||
def is_id_unique(self):
|
||||
def is_id_unique(self) -> bool:
|
||||
return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None)
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
def register() -> None:
|
||||
logger.debug("Registering MMDBone properties")
|
||||
bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone))
|
||||
bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False))
|
||||
bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type"))
|
||||
@@ -202,20 +207,21 @@ class MMDBone(bpy.types.PropertyGroup):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def unregister():
|
||||
def unregister() -> None:
|
||||
logger.debug("Unregistering MMDBone properties")
|
||||
del bpy.types.PoseBone.mmd_ik_toggle
|
||||
del bpy.types.PoseBone.mmd_shadow_bone_type
|
||||
del bpy.types.PoseBone.is_mmd_shadow_bone
|
||||
del bpy.types.PoseBone.mmd_bone
|
||||
|
||||
|
||||
def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context):
|
||||
def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context: Any) -> None:
|
||||
v = prop.mmd_ik_toggle
|
||||
armature_object = cast(bpy.types.Object, prop.id_data)
|
||||
armature_object = cast(Object, prop.id_data)
|
||||
for b in armature_object.pose.bones:
|
||||
for c in b.constraints:
|
||||
if c.type == "IK" and c.subtarget == prop.name:
|
||||
# logging.debug(' %s %s', b.name, c.name)
|
||||
logger.debug(f"Updating IK toggle for {b.name} {c.name}")
|
||||
c.influence = v
|
||||
b = b if c.use_tail else b.parent
|
||||
for b in ([b] + b.parent_recursive)[: c.chain_count]:
|
||||
|
||||
@@ -8,32 +8,35 @@
|
||||
"""Properties for rigid bodies and joints"""
|
||||
|
||||
import bpy
|
||||
from typing import Optional, Any, Set, List, Dict, Tuple, Union
|
||||
from bpy.types import Context, Object, PropertyGroup, Material
|
||||
|
||||
from .. import bpyutils
|
||||
from ..core import rigid_body
|
||||
from ..core.rigid_body import RigidBodyMaterial, FnRigidBody
|
||||
from ..core.model import FnModel
|
||||
from . import patch_library_overridable
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
def _updateCollisionGroup(prop, _context):
|
||||
obj = prop.id_data
|
||||
materials = obj.data.materials
|
||||
def _updateCollisionGroup(prop: PropertyGroup, _context: Context) -> None:
|
||||
obj: Object = prop.id_data
|
||||
materials: List[Material] = obj.data.materials
|
||||
if len(materials) == 0:
|
||||
materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number))
|
||||
else:
|
||||
obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number)
|
||||
|
||||
|
||||
def _updateType(prop, _context):
|
||||
obj = prop.id_data
|
||||
def _updateType(prop: PropertyGroup, _context: Context) -> None:
|
||||
obj: Object = prop.id_data
|
||||
rb = obj.rigid_body
|
||||
if rb:
|
||||
rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC
|
||||
|
||||
|
||||
def _updateShape(prop, _context):
|
||||
obj = prop.id_data
|
||||
def _updateShape(prop: PropertyGroup, _context: Context) -> None:
|
||||
obj: Object = prop.id_data
|
||||
|
||||
if len(obj.data.vertices) > 0:
|
||||
size = prop.size
|
||||
@@ -44,8 +47,8 @@ def _updateShape(prop, _context):
|
||||
rb.collision_shape = prop.shape
|
||||
|
||||
|
||||
def _get_bone(prop):
|
||||
obj = prop.id_data
|
||||
def _get_bone(prop: PropertyGroup) -> str:
|
||||
obj: Object = prop.id_data
|
||||
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
|
||||
if relation:
|
||||
arm = relation.target
|
||||
@@ -55,9 +58,9 @@ def _get_bone(prop):
|
||||
return prop.get("bone", "")
|
||||
|
||||
|
||||
def _set_bone(prop, value):
|
||||
bone_name = value
|
||||
obj = prop.id_data
|
||||
def _set_bone(prop: PropertyGroup, value: str) -> None:
|
||||
bone_name: str = value
|
||||
obj: Object = prop.id_data
|
||||
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
|
||||
if relation is None:
|
||||
relation = obj.constraints.new("CHILD_OF")
|
||||
@@ -78,16 +81,16 @@ def _set_bone(prop, value):
|
||||
prop["bone"] = bone_name
|
||||
|
||||
|
||||
def _get_size(prop):
|
||||
def _get_size(prop: PropertyGroup) -> Tuple[float, float, float]:
|
||||
if prop.id_data.mmd_type != "RIGID_BODY":
|
||||
return (0, 0, 0)
|
||||
return FnRigidBody.get_rigid_body_size(prop.id_data)
|
||||
|
||||
|
||||
def _set_size(prop, value):
|
||||
obj = prop.id_data
|
||||
def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None:
|
||||
obj: Object = prop.id_data
|
||||
assert obj.mode == "OBJECT" # not support other mode yet
|
||||
shape = prop.shape
|
||||
shape: str = prop.shape
|
||||
|
||||
mesh = obj.data
|
||||
rb = obj.rigid_body
|
||||
@@ -146,15 +149,15 @@ def _set_size(prop, value):
|
||||
mesh.update()
|
||||
|
||||
|
||||
def _get_rigid_name(prop):
|
||||
def _get_rigid_name(prop: PropertyGroup) -> str:
|
||||
return prop.get("name", "")
|
||||
|
||||
|
||||
def _set_rigid_name(prop, value):
|
||||
def _set_rigid_name(prop: PropertyGroup, value: str) -> None:
|
||||
prop["name"] = value
|
||||
|
||||
|
||||
class MMDRigidBody(bpy.types.PropertyGroup):
|
||||
class MMDRigidBody(PropertyGroup):
|
||||
name_j: bpy.props.StringProperty(
|
||||
name="Name",
|
||||
description="Japanese Name",
|
||||
@@ -227,16 +230,18 @@ class MMDRigidBody(bpy.types.PropertyGroup):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
def register() -> None:
|
||||
logger.debug("Registering MMDRigidBody property")
|
||||
bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody))
|
||||
|
||||
@staticmethod
|
||||
def unregister():
|
||||
def unregister() -> None:
|
||||
logger.debug("Unregistering MMDRigidBody property")
|
||||
del bpy.types.Object.mmd_rigid
|
||||
|
||||
|
||||
def _updateSpringLinear(prop, context):
|
||||
obj = prop.id_data
|
||||
def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None:
|
||||
obj: Object = prop.id_data
|
||||
rbc = obj.rigid_body_constraint
|
||||
if rbc:
|
||||
rbc.spring_stiffness_x = prop.spring_linear[0]
|
||||
@@ -244,8 +249,8 @@ def _updateSpringLinear(prop, context):
|
||||
rbc.spring_stiffness_z = prop.spring_linear[2]
|
||||
|
||||
|
||||
def _updateSpringAngular(prop, context):
|
||||
obj = prop.id_data
|
||||
def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None:
|
||||
obj: Object = prop.id_data
|
||||
rbc = obj.rigid_body_constraint
|
||||
if rbc and hasattr(rbc, "use_spring_ang_x"):
|
||||
rbc.spring_stiffness_ang_x = prop.spring_angular[0]
|
||||
@@ -253,7 +258,7 @@ def _updateSpringAngular(prop, context):
|
||||
rbc.spring_stiffness_ang_z = prop.spring_angular[2]
|
||||
|
||||
|
||||
class MMDJoint(bpy.types.PropertyGroup):
|
||||
class MMDJoint(PropertyGroup):
|
||||
name_j: bpy.props.StringProperty(
|
||||
name="Name",
|
||||
description="Japanese Name",
|
||||
@@ -287,9 +292,12 @@ class MMDJoint(bpy.types.PropertyGroup):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
def register() -> None:
|
||||
logger.debug("Registering MMDJoint property")
|
||||
bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint))
|
||||
|
||||
@staticmethod
|
||||
def unregister():
|
||||
def unregister() -> None:
|
||||
logger.debug("Unregistering MMDJoint property")
|
||||
del bpy.types.Object.mmd_joint
|
||||
|
||||
|
||||
+41
-25
@@ -8,6 +8,7 @@
|
||||
"""Properties for MMD model root object"""
|
||||
|
||||
import bpy
|
||||
from typing import Optional, List, Dict, Any, Set, Tuple, Union, Type, TypeVar, cast
|
||||
|
||||
from .. import utils
|
||||
from ..bpyutils import FnContext
|
||||
@@ -17,9 +18,10 @@ from ..core.sdef import FnSDEF
|
||||
from . import patch_library_overridable
|
||||
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
|
||||
from .translations import MMDTranslation
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
|
||||
def __driver_variables(constraint: bpy.types.Constraint, path: str, index: int = -1) -> Tuple[bpy.types.Driver, Any]:
|
||||
d = constraint.driver_add(path, index)
|
||||
variables = d.driver.variables
|
||||
for x in variables:
|
||||
@@ -27,7 +29,7 @@ def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
|
||||
return d.driver, variables
|
||||
|
||||
|
||||
def __add_single_prop(variables, id_obj, data_path, prefix):
|
||||
def __add_single_prop(variables: Any, id_obj: bpy.types.Object, data_path: str, prefix: str) -> Any:
|
||||
var = variables.new()
|
||||
var.name = prefix + str(len(variables))
|
||||
var.type = "SINGLE_PROP"
|
||||
@@ -38,17 +40,18 @@ def __add_single_prop(variables, id_obj, data_path, prefix):
|
||||
return var
|
||||
|
||||
|
||||
def _toggleUsePropertyDriver(self: "MMDRoot", _context):
|
||||
def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
||||
root_object: bpy.types.Object = self.id_data
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
|
||||
if armature_object is None:
|
||||
ik_map = {}
|
||||
ik_map: Dict[Any, Tuple[Any, Any]] = {}
|
||||
else:
|
||||
bones = armature_object.pose.bones
|
||||
ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}
|
||||
|
||||
if self.use_property_driver:
|
||||
logger.debug("Enabling property drivers for %s", root_object.name)
|
||||
for ik, (b, c) in ik_map.items():
|
||||
driver, variables = __driver_variables(c, "influence")
|
||||
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
|
||||
@@ -63,6 +66,7 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context):
|
||||
driver, variables = __driver_variables(i, prop_hide)
|
||||
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
|
||||
else:
|
||||
logger.debug("Disabling property drivers for %s", root_object.name)
|
||||
for ik, (b, c) in ik_map.items():
|
||||
c.driver_remove("influence")
|
||||
b = b if c.use_tail else b.parent
|
||||
@@ -80,31 +84,35 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context):
|
||||
# ===========================================
|
||||
|
||||
|
||||
def _toggleUseToonTexture(self: "MMDRoot", _context):
|
||||
def _toggleUseToonTexture(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
||||
use_toon = self.use_toon_texture
|
||||
logger.debug("Toggling toon texture to %s for %s", use_toon, self.id_data.name)
|
||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||
for m in i.data.materials:
|
||||
if m:
|
||||
FnMaterial(m).use_toon_texture(use_toon)
|
||||
|
||||
|
||||
def _toggleUseSphereTexture(self: "MMDRoot", _context):
|
||||
def _toggleUseSphereTexture(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
||||
use_sphere = self.use_sphere_texture
|
||||
logger.debug("Toggling sphere texture to %s for %s", use_sphere, self.id_data.name)
|
||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||
for m in i.data.materials:
|
||||
if m:
|
||||
FnMaterial(m).use_sphere_texture(use_sphere, i)
|
||||
|
||||
|
||||
def _toggleUseSDEF(self: "MMDRoot", _context):
|
||||
def _toggleUseSDEF(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
||||
mute_sdef = not self.use_sdef
|
||||
logger.debug("Toggling SDEF to %s for %s", not mute_sdef, self.id_data.name)
|
||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||
FnSDEF.mute_sdef_set(i, mute_sdef)
|
||||
|
||||
|
||||
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
|
||||
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context) -> None:
|
||||
root = self.id_data
|
||||
hide = not self.show_meshes
|
||||
logger.debug("Toggling mesh visibility to %s for %s", not hide, root.name)
|
||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||
i.hide_set(hide)
|
||||
i.hide_render = hide
|
||||
@@ -112,27 +120,30 @@ def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
|
||||
FnContext.set_active_object(context, root)
|
||||
|
||||
|
||||
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context):
|
||||
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context) -> None:
|
||||
root = self.id_data
|
||||
hide = not self.show_rigid_bodies
|
||||
logger.debug("Toggling rigid body visibility to %s for %s", not hide, root.name)
|
||||
for i in FnModel.iterate_rigid_body_objects(root):
|
||||
i.hide_set(hide)
|
||||
if hide and context.active_object is None:
|
||||
FnContext.set_active_object(context, root)
|
||||
|
||||
|
||||
def _toggleVisibilityOfJoints(self: "MMDRoot", context):
|
||||
def _toggleVisibilityOfJoints(self: "MMDRoot", context: bpy.types.Context) -> None:
|
||||
root_object = self.id_data
|
||||
hide = not self.show_joints
|
||||
logger.debug("Toggling joint visibility to %s for %s", not hide, root_object.name)
|
||||
for i in FnModel.iterate_joint_objects(root_object):
|
||||
i.hide_set(hide)
|
||||
if hide and context.active_object is None:
|
||||
FnContext.set_active_object(context, root_object)
|
||||
|
||||
|
||||
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context):
|
||||
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context) -> None:
|
||||
root_object: bpy.types.Object = self.id_data
|
||||
hide = not self.show_temporary_objects
|
||||
logger.debug("Toggling temporary object visibility to %s for %s", not hide, root_object.name)
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||
for i in FnModel.iterate_temporary_objects(root_object):
|
||||
i.hide_set(hide)
|
||||
@@ -140,45 +151,48 @@ def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Cont
|
||||
FnContext.set_active_object(context, root_object)
|
||||
|
||||
|
||||
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context):
|
||||
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
||||
root = self.id_data
|
||||
show_names = root.mmd_root.show_names_of_rigid_bodies
|
||||
logger.debug("Toggling rigid body names to %s for %s", show_names, root.name)
|
||||
for i in FnModel.iterate_rigid_body_objects(root):
|
||||
i.show_name = show_names
|
||||
|
||||
|
||||
def _toggleShowNamesOfJoints(self: "MMDRoot", _context):
|
||||
def _toggleShowNamesOfJoints(self: "MMDRoot", _context: bpy.types.Context) -> None:
|
||||
root = self.id_data
|
||||
show_names = root.mmd_root.show_names_of_joints
|
||||
logger.debug("Toggling joint names to %s for %s", show_names, root.name)
|
||||
for i in FnModel.iterate_joint_objects(root):
|
||||
i.show_name = show_names
|
||||
|
||||
|
||||
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool):
|
||||
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool) -> None:
|
||||
root = prop.id_data
|
||||
arm = FnModel.find_armature_object(root)
|
||||
if arm is None:
|
||||
return
|
||||
if not v and bpy.context.active_object == arm:
|
||||
FnContext.set_active_object(bpy.context, root)
|
||||
logger.debug("Setting armature visibility to %s for %s", v, root.name)
|
||||
arm.hide_set(not v)
|
||||
|
||||
|
||||
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"):
|
||||
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot") -> bool:
|
||||
if prop.id_data.mmd_type != "ROOT":
|
||||
return False
|
||||
arm = FnModel.find_armature_object(prop.id_data)
|
||||
return arm and not arm.hide_get()
|
||||
|
||||
|
||||
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int):
|
||||
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int) -> None:
|
||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||
if FnModel.is_rigid_body_object(obj):
|
||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||
prop["active_rigidbody_object_index"] = v
|
||||
|
||||
|
||||
def _getActiveRigidbodyObject(prop: "MMDRoot"):
|
||||
def _getActiveRigidbodyObject(prop: "MMDRoot") -> int:
|
||||
context = bpy.context
|
||||
active_obj = FnContext.get_active_object(context)
|
||||
if FnModel.is_rigid_body_object(active_obj):
|
||||
@@ -186,14 +200,14 @@ def _getActiveRigidbodyObject(prop: "MMDRoot"):
|
||||
return prop.get("active_rigidbody_object_index", 0)
|
||||
|
||||
|
||||
def _setActiveJointObject(prop: "MMDRoot", v: int):
|
||||
def _setActiveJointObject(prop: "MMDRoot", v: int) -> None:
|
||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||
if FnModel.is_joint_object(obj):
|
||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||
prop["active_joint_object_index"] = v
|
||||
|
||||
|
||||
def _getActiveJointObject(prop: "MMDRoot"):
|
||||
def _getActiveJointObject(prop: "MMDRoot") -> int:
|
||||
context = bpy.context
|
||||
active_obj = FnContext.get_active_object(context)
|
||||
if FnModel.is_joint_object(active_obj):
|
||||
@@ -201,26 +215,26 @@ def _getActiveJointObject(prop: "MMDRoot"):
|
||||
return prop.get("active_joint_object_index", 0)
|
||||
|
||||
|
||||
def _setActiveMorph(prop: "MMDRoot", v: bool):
|
||||
def _setActiveMorph(prop: "MMDRoot", v: bool) -> None:
|
||||
if "active_morph_indices" not in prop:
|
||||
prop["active_morph_indices"] = [0] * 5
|
||||
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v
|
||||
|
||||
|
||||
def _getActiveMorph(prop: "MMDRoot"):
|
||||
def _getActiveMorph(prop: "MMDRoot") -> int:
|
||||
if "active_morph_indices" in prop:
|
||||
return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
|
||||
return 0
|
||||
|
||||
|
||||
def _setActiveMeshObject(prop: "MMDRoot", v: int):
|
||||
def _setActiveMeshObject(prop: "MMDRoot", v: int) -> None:
|
||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||
if FnModel.is_mesh_object(obj):
|
||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||
prop["active_mesh_index"] = v
|
||||
|
||||
|
||||
def _getActiveMeshObject(prop: "MMDRoot"):
|
||||
def _getActiveMeshObject(prop: "MMDRoot") -> int:
|
||||
context = bpy.context
|
||||
active_obj = FnContext.get_active_object(context)
|
||||
if FnModel.is_mesh_object(active_obj):
|
||||
@@ -520,7 +534,8 @@ class MMDRoot(bpy.types.PropertyGroup):
|
||||
prop.hide_viewport = value
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
def register() -> None:
|
||||
logger.debug("Registering MMDRoot property group")
|
||||
bpy.types.Object.mmd_type = patch_library_overridable(
|
||||
bpy.props.EnumProperty(
|
||||
name="Type",
|
||||
@@ -570,7 +585,8 @@ class MMDRoot(bpy.types.PropertyGroup):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def unregister():
|
||||
def unregister() -> None:
|
||||
logger.debug("Unregistering MMDRoot property group")
|
||||
del bpy.types.Object.hide
|
||||
del bpy.types.Object.select
|
||||
del bpy.types.Object.mmd_root
|
||||
|
||||
+88
-33
@@ -6,14 +6,20 @@
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import csv
|
||||
import logging
|
||||
import time
|
||||
from typing import List, Tuple, Dict, Optional, Any, Generator, Union, TextIO, Iterator, Set
|
||||
|
||||
import bpy
|
||||
from bpy.types import Text, Context
|
||||
|
||||
from .bpyutils import FnContext
|
||||
from ..logging_setup import logger
|
||||
|
||||
jp_half_to_full_tuples = (
|
||||
# Type definitions for translation tuples
|
||||
TranslationTuple = Tuple[str, str]
|
||||
TranslationList = List[TranslationTuple]
|
||||
|
||||
jp_half_to_full_tuples: TranslationList = (
|
||||
("ヴ", "ヴ"),
|
||||
("ガ", "ガ"),
|
||||
("ギ", "ギ"),
|
||||
@@ -103,7 +109,7 @@ jp_half_to_full_tuples = (
|
||||
("ン", "ン"),
|
||||
)
|
||||
|
||||
jp_to_en_tuples = [
|
||||
jp_to_en_tuples: TranslationList = [
|
||||
("全ての親", "ParentNode"),
|
||||
("操作中心", "ControlNode"),
|
||||
("センター", "Center"),
|
||||
@@ -293,22 +299,30 @@ jp_to_en_tuples = [
|
||||
]
|
||||
|
||||
|
||||
def translateFromJp(name):
|
||||
def translateFromJp(name: str) -> str:
|
||||
"""Translate a Japanese name to English using the translation tuples."""
|
||||
logger.debug(f"Translating from Japanese: {name}")
|
||||
for tuple in jp_to_en_tuples:
|
||||
if tuple[0] in name:
|
||||
name = name.replace(tuple[0], tuple[1])
|
||||
logger.debug(f"Translation result: {name}")
|
||||
return name
|
||||
|
||||
|
||||
def getTranslator(csvfile="", keep_order=False):
|
||||
def getTranslator(csvfile: Union[str, Dict[str, str], Text] = "", keep_order: bool = False) -> 'MMDTranslator':
|
||||
"""Get a translator instance with the specified CSV file."""
|
||||
translator = MMDTranslator()
|
||||
if isinstance(csvfile, bpy.types.Text):
|
||||
logger.debug(f"Loading translator from Text object: {csvfile.name}")
|
||||
translator.load_from_stream(csvfile)
|
||||
elif isinstance(csvfile, dict):
|
||||
logger.debug(f"Loading translator from dictionary with {len(csvfile)} entries")
|
||||
translator.csv_tuples.extend(csvfile.items())
|
||||
elif csvfile in bpy.data.texts.keys():
|
||||
logger.debug(f"Loading translator from text data: {csvfile}")
|
||||
translator.load_from_stream(bpy.data.texts[csvfile])
|
||||
else:
|
||||
logger.debug(f"Loading translator from file: {csvfile}")
|
||||
translator.load(csvfile)
|
||||
|
||||
if not keep_order:
|
||||
@@ -318,16 +332,20 @@ def getTranslator(csvfile="", keep_order=False):
|
||||
|
||||
|
||||
class MMDTranslator:
|
||||
def __init__(self):
|
||||
self.__csv_tuples = []
|
||||
self.__fails = {}
|
||||
"""Handles translation of Japanese text to English for MMD models."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.__csv_tuples: List[Tuple[str, str]] = []
|
||||
self.__fails: Dict[str, str] = {}
|
||||
|
||||
@staticmethod
|
||||
def default_csv_filepath():
|
||||
def default_csv_filepath() -> str:
|
||||
"""Get the default CSV filepath for translations."""
|
||||
return __file__[:-3] + ".csv"
|
||||
|
||||
@staticmethod
|
||||
def get_csv_text(text_name=None):
|
||||
def get_csv_text(text_name: Optional[str] = None) -> Text:
|
||||
"""Get or create a Text object for CSV data."""
|
||||
text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath())
|
||||
csv_text = bpy.data.texts.get(text_name, None)
|
||||
if csv_text is None:
|
||||
@@ -335,69 +353,88 @@ class MMDTranslator:
|
||||
return csv_text
|
||||
|
||||
@staticmethod
|
||||
def replace_from_tuples(name, tuples):
|
||||
def replace_from_tuples(name: str, tuples: List[Tuple[str, str]]) -> str:
|
||||
"""Replace parts of a string based on translation tuples."""
|
||||
for pair in tuples:
|
||||
if pair[0] in name:
|
||||
name = name.replace(pair[0], pair[1])
|
||||
return name
|
||||
|
||||
@property
|
||||
def csv_tuples(self):
|
||||
def csv_tuples(self) -> List[Tuple[str, str]]:
|
||||
"""Get the CSV tuples."""
|
||||
return self.__csv_tuples
|
||||
|
||||
@property
|
||||
def fails(self):
|
||||
def fails(self) -> Dict[str, str]:
|
||||
"""Get the failed translations."""
|
||||
return self.__fails
|
||||
|
||||
def sort(self):
|
||||
def sort(self) -> None:
|
||||
"""Sort the CSV tuples by length (longest first) and then alphabetically."""
|
||||
logger.debug("Sorting translation tuples")
|
||||
self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row))
|
||||
|
||||
def update(self):
|
||||
def update(self) -> None:
|
||||
"""Update the CSV tuples, removing duplicates."""
|
||||
from collections import OrderedDict
|
||||
|
||||
count_old = len(self.__csv_tuples)
|
||||
tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0])
|
||||
self.__csv_tuples.clear()
|
||||
self.__csv_tuples.extend(tuples_dict.values())
|
||||
logging.info(" - removed items:\t%d\t(of %d)", count_old - len(self.__csv_tuples), count_old)
|
||||
logger.info("Translation update - removed items: %d (of %d)", count_old - len(self.__csv_tuples), count_old)
|
||||
|
||||
def half_to_full(self, name):
|
||||
def half_to_full(self, name: str) -> str:
|
||||
"""Convert half-width Japanese characters to full-width."""
|
||||
return self.replace_from_tuples(name, jp_half_to_full_tuples)
|
||||
|
||||
def is_translated(self, name):
|
||||
def is_translated(self, name: str) -> bool:
|
||||
"""Check if a string is already translated (contains only ASCII characters)."""
|
||||
try:
|
||||
name.encode("ascii", errors="strict")
|
||||
except UnicodeEncodeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def translate(self, name, default=None, from_full_width=True):
|
||||
def translate(self, name: str, default: Optional[str] = None, from_full_width: bool = True) -> str:
|
||||
"""Translate a string from Japanese to English."""
|
||||
logger.debug(f"Translating: {name}")
|
||||
if from_full_width:
|
||||
name = self.half_to_full(name)
|
||||
name_new = self.replace_from_tuples(name, self.__csv_tuples)
|
||||
if default is not None and not self.is_translated(name_new):
|
||||
logger.warning(f"Translation failed for: {name}")
|
||||
self.__fails[name] = name_new
|
||||
return default
|
||||
return name_new
|
||||
|
||||
def save_fails(self, text_name=None):
|
||||
def save_fails(self, text_name: Optional[str] = None) -> Text:
|
||||
"""Save failed translations to a Text object."""
|
||||
text_name = text_name or (__name__ + ".fails")
|
||||
txt = self.get_csv_text(text_name)
|
||||
fmt = '"%s","%s"'
|
||||
items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row))
|
||||
txt.from_string("\n".join(fmt % (k, v) for k, v in items))
|
||||
logger.info(f"Saved {len(items)} failed translations to {text_name}")
|
||||
return txt
|
||||
|
||||
def load_from_stream(self, csvfile=None):
|
||||
def load_from_stream(self, csvfile: Union[Text, Iterator[str]] = None) -> None:
|
||||
"""Load translations from a stream."""
|
||||
csvfile = csvfile or self.get_csv_text()
|
||||
if isinstance(csvfile, bpy.types.Text):
|
||||
csvfile = (l.body + "\n" for l in csvfile.lines)
|
||||
spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True)
|
||||
csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2]
|
||||
self.__csv_tuples = csv_tuples
|
||||
logging.info(" - load items:\t%d", len(self.__csv_tuples))
|
||||
logger.info("Loaded %d translation items", len(self.__csv_tuples))
|
||||
|
||||
def save_to_stream(self, csvfile=None):
|
||||
def save_to_stream(self, csvfile: Union[Text, TextIO] = None) -> None:
|
||||
"""Save translations to a stream.
|
||||
|
||||
Args:
|
||||
csvfile: The CSV file or stream to save to
|
||||
"""
|
||||
csvfile = csvfile or self.get_csv_text()
|
||||
lineterminator = "\r\n"
|
||||
if isinstance(csvfile, bpy.types.Text):
|
||||
@@ -405,27 +442,38 @@ class MMDTranslator:
|
||||
lineterminator = "\n"
|
||||
spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL)
|
||||
spamwriter.writerows(self.__csv_tuples)
|
||||
logging.info(" - save items:\t%d", len(self.__csv_tuples))
|
||||
logger.info("Saved %d translation items", len(self.__csv_tuples))
|
||||
|
||||
def load(self, filepath=None):
|
||||
def load(self, filepath: Optional[str] = None) -> None:
|
||||
"""Load translations from a file."""
|
||||
filepath = filepath or self.default_csv_filepath()
|
||||
logging.info("Loading csv file:\t%s", filepath)
|
||||
logger.info("Loading CSV file: %s", filepath)
|
||||
try:
|
||||
with open(filepath, "rt", encoding="utf-8", newline="") as csvfile:
|
||||
self.load_from_stream(csvfile)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load CSV file: {e}")
|
||||
|
||||
def save(self, filepath=None):
|
||||
def save(self, filepath: Optional[str] = None) -> None:
|
||||
"""Save translations to a file."""
|
||||
filepath = filepath or self.default_csv_filepath()
|
||||
logging.info("Saving csv file:\t%s", filepath)
|
||||
logger.info("Saving CSV file: %s", filepath)
|
||||
try:
|
||||
with open(filepath, "wt", encoding="utf-8", newline="") as csvfile:
|
||||
self.save_to_stream(csvfile)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save CSV file: {e}")
|
||||
|
||||
|
||||
class DictionaryEnum:
|
||||
__items_ttl = 0.0
|
||||
__items_cache = None
|
||||
"""Handles dictionary enumeration for UI."""
|
||||
|
||||
__items_ttl: float = 0.0
|
||||
__items_cache: Optional[List[Tuple[str, str, str, int]]] = None
|
||||
|
||||
@staticmethod
|
||||
def get_dictionary_items(prop, context):
|
||||
def get_dictionary_items(prop: Any, context: Context) -> List[Tuple[str, str, str, Union[int, str], int]]:
|
||||
"""Get dictionary items for UI enumeration."""
|
||||
if DictionaryEnum.__items_ttl > time.time():
|
||||
return DictionaryEnum.__items_cache
|
||||
|
||||
@@ -437,7 +485,7 @@ class DictionaryEnum:
|
||||
items.append(("INTERNAL", "Internal Dictionary", "The dictionary defined in " + __name__, len(items)))
|
||||
|
||||
for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")):
|
||||
items.append((txt_name, txt_name, "bpy.data.texts['%s']" % txt_name, "TEXT", len(items)))
|
||||
items.append((txt_name, txt_name, f"bpy.data.texts['{txt_name}']", "TEXT", len(items)))
|
||||
|
||||
import os
|
||||
|
||||
@@ -450,12 +498,19 @@ class DictionaryEnum:
|
||||
|
||||
if "dictionary" in prop:
|
||||
prop["dictionary"] = min(prop["dictionary"], len(items) - 1)
|
||||
|
||||
logger.debug(f"Found {len(items)} dictionary items")
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def get_translator(dictionary):
|
||||
def get_translator(dictionary: str) -> Optional[MMDTranslator]:
|
||||
"""Get a translator for the specified dictionary."""
|
||||
if dictionary == "DISABLED":
|
||||
logger.debug("Translation disabled")
|
||||
return None
|
||||
if dictionary == "INTERNAL":
|
||||
logger.debug("Using internal dictionary")
|
||||
return getTranslator(dict(jp_to_en_tuples))
|
||||
|
||||
logger.debug(f"Using dictionary: {dictionary}")
|
||||
return getTranslator(dictionary)
|
||||
|
||||
+25
-27
@@ -5,18 +5,19 @@
|
||||
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
|
||||
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Callable, Optional, Set
|
||||
from typing import Callable, Dict, List, Optional, Set, Tuple, Union, Any
|
||||
|
||||
import bpy
|
||||
from bpy.types import Object, Bone, PoseBone, Mesh, VertexGroup
|
||||
|
||||
from ..logging_setup import logger
|
||||
from .bpyutils import FnContext
|
||||
|
||||
|
||||
## 指定したオブジェクトのみを選択状態かつアクティブにする
|
||||
def selectAObject(obj):
|
||||
def selectAObject(obj: Object) -> None:
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
@@ -27,13 +28,13 @@ def selectAObject(obj):
|
||||
|
||||
|
||||
## 現在のモードを指定したオブジェクトのEdit Modeに変更する
|
||||
def enterEditMode(obj):
|
||||
def enterEditMode(obj: Object) -> None:
|
||||
selectAObject(obj)
|
||||
if obj.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
|
||||
def setParentToBone(obj, parent, bone_name):
|
||||
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
||||
selectAObject(obj)
|
||||
FnContext.set_active_object(FnContext.ensure_context(), parent)
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
@@ -42,7 +43,7 @@ def setParentToBone(obj, parent, bone_name):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
|
||||
def selectSingleBone(context, armature, bone_name, reset_pose=False):
|
||||
def selectSingleBone(context: bpy.types.Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None:
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except:
|
||||
@@ -55,7 +56,7 @@ def selectSingleBone(context, armature, bone_name, reset_pose=False):
|
||||
for p_bone in armature.pose.bones:
|
||||
p_bone.matrix_basis.identity()
|
||||
armature_bones: bpy.types.ArmatureBones = armature.data.bones
|
||||
i: bpy.types.Bone
|
||||
i: Bone
|
||||
for i in armature_bones:
|
||||
i.select = i.name == bone_name
|
||||
i.select_head = i.select_tail = i.select
|
||||
@@ -69,7 +70,7 @@ __CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
|
||||
|
||||
|
||||
## 日本語で左右を命名されている名前をblender方式のL(R)に変更する
|
||||
def convertNameToLR(name, use_underscore=False):
|
||||
def convertNameToLR(name: str, use_underscore: bool = False) -> str:
|
||||
m = __CONVERT_NAME_TO_L_REGEXP.match(name)
|
||||
delimiter = "_" if use_underscore else "."
|
||||
if m:
|
||||
@@ -84,7 +85,7 @@ __CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[lL])(?P<aft
|
||||
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
|
||||
|
||||
|
||||
def convertLRToName(name):
|
||||
def convertLRToName(name: str) -> str:
|
||||
match = __CONVERT_L_TO_NAME_REGEXP.search(name)
|
||||
if match:
|
||||
return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}"
|
||||
@@ -97,7 +98,7 @@ def convertLRToName(name):
|
||||
|
||||
|
||||
## src_vertex_groupのWeightをdest_vertex_groupにaddする
|
||||
def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
|
||||
def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None:
|
||||
mesh = meshObj.data
|
||||
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
|
||||
dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]
|
||||
@@ -111,7 +112,7 @@ def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
|
||||
pass
|
||||
|
||||
|
||||
def separateByMaterials(meshObj: bpy.types.Object):
|
||||
def separateByMaterials(meshObj: Object) -> None:
|
||||
if len(meshObj.data.materials) < 2:
|
||||
selectAObject(meshObj)
|
||||
return
|
||||
@@ -134,7 +135,7 @@ def separateByMaterials(meshObj: bpy.types.Object):
|
||||
bpy.data.objects.remove(dummy_parent)
|
||||
|
||||
|
||||
def clearUnusedMeshes():
|
||||
def clearUnusedMeshes() -> None:
|
||||
meshes_to_delete = []
|
||||
for mesh in bpy.data.meshes:
|
||||
if mesh.users == 0:
|
||||
@@ -146,7 +147,7 @@ def clearUnusedMeshes():
|
||||
|
||||
## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
|
||||
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
|
||||
def makePmxBoneMap(armObj):
|
||||
def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]:
|
||||
# Maintain backward compatibility with mmd_tools v0.4.x or older.
|
||||
return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones}
|
||||
|
||||
@@ -175,7 +176,7 @@ def unique_name(name: str, used_names: Set[str]) -> str:
|
||||
return new_name
|
||||
|
||||
|
||||
def int2base(x, base, width=0):
|
||||
def int2base(x: int, base: int, width: int = 0) -> str:
|
||||
"""
|
||||
Method to convert an int to a base
|
||||
Source: http://stackoverflow.com/questions/2267362
|
||||
@@ -198,7 +199,7 @@ def int2base(x, base, width=0):
|
||||
return digits
|
||||
|
||||
|
||||
def saferelpath(path, start, strategy="inside"):
|
||||
def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
|
||||
"""
|
||||
On Windows relpath will raise a ValueError
|
||||
when trying to calculate the relative path to a
|
||||
@@ -227,13 +228,13 @@ def saferelpath(path, start, strategy="inside"):
|
||||
|
||||
class ItemOp:
|
||||
@staticmethod
|
||||
def get_by_index(items, index):
|
||||
def get_by_index(items: bpy.types.bpy_prop_collection, index: int) -> Optional[Any]:
|
||||
if 0 <= index < len(items):
|
||||
return items[index]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def resize(items: bpy.types.bpy_prop_collection, length: int):
|
||||
def resize(items: bpy.types.bpy_prop_collection, length: int) -> None:
|
||||
count = length - len(items)
|
||||
if count > 0:
|
||||
for i in range(count):
|
||||
@@ -243,7 +244,7 @@ class ItemOp:
|
||||
items.remove(length)
|
||||
|
||||
@staticmethod
|
||||
def add_after(items, index):
|
||||
def add_after(items: bpy.types.bpy_prop_collection, index: int) -> Tuple[Any, int]:
|
||||
index_end = len(items)
|
||||
index = max(0, min(index_end, index + 1))
|
||||
items.add()
|
||||
@@ -265,7 +266,8 @@ class ItemMoveOp:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def move(items, index, move_type, index_min=0, index_max=None):
|
||||
def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str,
|
||||
index_min: int = 0, index_max: Optional[int] = None) -> int:
|
||||
if index_max is None:
|
||||
index_max = len(items) - 1
|
||||
else:
|
||||
@@ -294,7 +296,7 @@ class ItemMoveOp:
|
||||
return index_new
|
||||
|
||||
|
||||
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None):
|
||||
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None) -> Callable:
|
||||
"""Decorator to mark a function as deprecated.
|
||||
Args:
|
||||
deprecated_in (Optional[str]): Version in which the function was deprecated.
|
||||
@@ -303,8 +305,8 @@ def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = Non
|
||||
Callable: The decorated function.
|
||||
"""
|
||||
|
||||
def _function_wrapper(function: Callable):
|
||||
def _inner_wrapper(*args, **kwargs):
|
||||
def _function_wrapper(function: Callable) -> Callable:
|
||||
def _inner_wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
warn_deprecation(function.__name__, deprecated_in, details)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
@@ -320,7 +322,7 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de
|
||||
deprecated_in (Optional[str]): Version in which the function was deprecated.
|
||||
details (Optional[str]): Additional details about the deprecation.
|
||||
"""
|
||||
logging.warning(
|
||||
logger.warning(
|
||||
"%s is deprecated%s%s",
|
||||
function_name,
|
||||
f" since {deprecated_in}" if deprecated_in else "",
|
||||
@@ -328,7 +330,3 @@ def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, de
|
||||
stack_info=True,
|
||||
stacklevel=4,
|
||||
)
|
||||
|
||||
# import warnings # pylint: disable=import-outside-toplevel
|
||||
|
||||
# warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2)
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
|
||||
# Define which version series this installation can update to
|
||||
# For example: ["0.1"] means only look for 0.1.x updates
|
||||
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates
|
||||
ALLOWED_VERSION_SERIES = ["0.2"]
|
||||
ALLOWED_VERSION_SERIES = ["0.3"]
|
||||
|
||||
is_checking_for_update: bool = False
|
||||
update_needed: bool = False
|
||||
|
||||
@@ -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)
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"authors": ["Avatar Toolkit Team"],
|
||||
"messages": {
|
||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.1)",
|
||||
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.0)",
|
||||
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
|
||||
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
|
||||
"AvatarToolkit.desc3": "please report it on our Github.",
|
||||
@@ -63,6 +63,13 @@
|
||||
"PoseMode.basis": "Basis",
|
||||
|
||||
"Armature.validation.no_armature": "No armature selected",
|
||||
"Armature.validation.pmx_model_detected": "PMX model detected. Japanese bone names may not match standard naming conventions.",
|
||||
"Armature.validation.pmx_model_strict": "Consider using the 'Standardize Armature' option to convert Japanese bone names to standard names.",
|
||||
"Armature.validation.pmx_model_standardize": "This will make the model compatible with standard avatar systems.",
|
||||
"Armature.validation.pmx_model_basic": "PMX models use Japanese bone names which may not match standard naming conventions.",
|
||||
"Armature.validation.unknown_format": "Unknown armature format detected.",
|
||||
"Validation.mode.none": "Validation is disabled in settings.",
|
||||
"Validation.no_messages": "No validation messages available.",
|
||||
"Armature.validation.not_armature": "Selected object is not an armature",
|
||||
"Armature.validation.no_bones": "Armature has no bones",
|
||||
"Armature.validation.basic_check_failed": "Basic armature validation failed",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"authors": ["Avatar Toolkit Team"],
|
||||
"messages": {
|
||||
"AvatarToolkit.label": "アバターツールキット (アルファ 0.2.1)",
|
||||
"AvatarToolkit.label": "アバターツールキット (アルファ 0.3.0)",
|
||||
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
||||
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
||||
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
||||
@@ -63,6 +63,13 @@
|
||||
"PoseMode.basis": "基本形",
|
||||
|
||||
"Armature.validation.no_armature": "アーマチュアが選択されていません",
|
||||
"Armature.validation.pmx_model_detected": "PMXモデルが検出されました。日本語の骨名が標準の命名規則と一致しない場合があります。",
|
||||
"Armature.validation.pmx_model_strict": "「アーマチュアの標準化」オプションを使用して、日本語の骨名を標準名に変換することを検討してください。",
|
||||
"Armature.validation.pmx_model_standardize": "これにより、モデルが標準的なアバターシステムと互換性を持つようになります。",
|
||||
"Armature.validation.pmx_model_basic": "PMXモデルは日本語の骨名を使用しており、標準の命名規則と一致しない場合があります。",
|
||||
"Armature.validation.unknown_format": "不明なアーマチュア形式が検出されました。",
|
||||
"Validation.mode.none": "検証は設定で無効になっています。",
|
||||
"Validation.no_messages": "検証メッセージはありません。",
|
||||
"Armature.validation.not_armature": "選択されたオブジェクトはアーマチュアではありません",
|
||||
"Armature.validation.no_bones": "アーマチュアにボーンがありません",
|
||||
"Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"authors": ["Avatar Toolkit Team"],
|
||||
"messages": {
|
||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.2.1)",
|
||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.3.0)",
|
||||
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
||||
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
||||
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
||||
@@ -63,6 +63,13 @@
|
||||
"PoseMode.basis": "기본",
|
||||
|
||||
"Armature.validation.no_armature": "선택된 아마추어 없음",
|
||||
"Armature.validation.pmx_model_detected": "PMX 모델이 감지되었습니다. 일본어 본 이름이 표준 명명 규칙과 일치하지 않을 수 있습니다.",
|
||||
"Armature.validation.pmx_model_strict": "'아마추어 표준화' 옵션을 사용하여 일본어 본 이름을 표준 이름으로 변환하는 것을 고려하세요.",
|
||||
"Armature.validation.pmx_model_standardize": "이렇게 하면 모델이 표준 아바타 시스템과 호환됩니다.",
|
||||
"Armature.validation.pmx_model_basic": "PMX 모델은 일본어 본 이름을 사용하며 표준 명명 규칙과 일치하지 않을 수 있습니다.",
|
||||
"Armature.validation.unknown_format": "알 수 없는 아마추어 형식이 감지되었습니다.",
|
||||
"Validation.mode.none": "유효성 검사가 설정에서 비활성화되었습니다.",
|
||||
"Validation.no_messages": "사용 가능한 유효성 검사 메시지가 없습니다.",
|
||||
"Armature.validation.not_armature": "선택된 객체가 아마추어가 아님",
|
||||
"Armature.validation.no_bones": "아마추어에 본이 없음",
|
||||
"Armature.validation.basic_check_failed": "기본 아마추어 검증 실패",
|
||||
|
||||
@@ -89,16 +89,33 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||
if active_armature:
|
||||
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True)
|
||||
|
||||
# Check if this is a PMX model
|
||||
is_pmx_model = False
|
||||
if hasattr(active_armature, 'mmd_type') or (hasattr(active_armature, 'parent') and active_armature.parent and hasattr(active_armature.parent, 'mmd_type')):
|
||||
is_pmx_model = True
|
||||
|
||||
info_box = col.box()
|
||||
|
||||
# If it's a PMX model, display a prominent notice
|
||||
if is_pmx_model:
|
||||
pmx_box = info_box.box()
|
||||
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
|
||||
|
||||
validation_mode = context.scene.avatar_toolkit.validation_mode
|
||||
if validation_mode == 'STRICT':
|
||||
pmx_box.label(text=t("Armature.validation.pmx_model_strict"))
|
||||
pmx_box.label(text=t("Armature.validation.pmx_model_standardize"))
|
||||
else:
|
||||
pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
|
||||
|
||||
if not is_valid:
|
||||
# Display non-standard bones and hierarchy issues
|
||||
if len(messages) > 1:
|
||||
if messages and len(messages) > 0:
|
||||
# Found Bones section
|
||||
validation_box = info_box.box()
|
||||
row = validation_box.row()
|
||||
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
|
||||
if props.show_found_bones:
|
||||
if props.show_found_bones and len(messages) > 0:
|
||||
for line in messages[0].split('\n'):
|
||||
validation_box.label(text=line)
|
||||
|
||||
@@ -127,12 +144,28 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
|
||||
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
|
||||
if props.show_non_standard:
|
||||
if non_standard_messages:
|
||||
if non_standard_messages and len(non_standard_messages) > 0:
|
||||
for message in non_standard_messages:
|
||||
for line in message.split('\n'):
|
||||
sub_row = validation_box.row()
|
||||
sub_row.alert = True
|
||||
sub_row.label(text=line)
|
||||
else:
|
||||
# For PMX models, if no non-standard messages but it's a PMX model,
|
||||
# we should still indicate there might be non-standard bones
|
||||
if is_pmx_model:
|
||||
sub_row = validation_box.row()
|
||||
sub_row.alert = True
|
||||
sub_row.label(text=t("Armature.validation.pmx_model_basic"))
|
||||
|
||||
sub_row = validation_box.row()
|
||||
sub_row.alert = True
|
||||
sub_row.label(text=t("Armature.validation.pmx_model_strict"))
|
||||
|
||||
sub_row = validation_box.row()
|
||||
sub_row.alert = True
|
||||
sub_row.label(text=t("Armature.validation.pmx_model_standardize"))
|
||||
|
||||
else:
|
||||
sub_row = validation_box.row()
|
||||
sub_row.label(text=t("Validation.no_non_standard_issues"))
|
||||
@@ -190,9 +223,14 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||
row.label(text=msg.name)
|
||||
else:
|
||||
# If no specific issues, show acceptable message
|
||||
if messages and len(messages) > 0:
|
||||
info_box.label(text=messages[0], icon='INFO')
|
||||
if len(messages) > 1:
|
||||
info_box.label(text=messages[1])
|
||||
if len(messages) > 2:
|
||||
info_box.label(text=messages[2])
|
||||
else:
|
||||
info_box.label(text=t("Validation.no_messages"), icon='INFO')
|
||||
elif is_valid and not is_acceptable:
|
||||
row = info_box.row()
|
||||
split = row.split(factor=0.6)
|
||||
@@ -204,9 +242,16 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
|
||||
elif is_valid and is_acceptable:
|
||||
# Show acceptable standard message
|
||||
if messages and len(messages) > 0:
|
||||
info_box.label(text=messages[0], icon='INFO')
|
||||
|
||||
# Only try to access additional messages if they exist
|
||||
if len(messages) > 1:
|
||||
info_box.label(text=messages[1])
|
||||
if len(messages) > 2:
|
||||
info_box.label(text=messages[2])
|
||||
else:
|
||||
info_box.label(text=t("Validation.no_messages"), icon='INFO')
|
||||
|
||||
# Add standardize button
|
||||
standardize_box = info_box.box()
|
||||
@@ -252,3 +297,4 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||
button_row.scale_y = 1.5
|
||||
button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT')
|
||||
button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user