PMX Import now works

This commit is contained in:
Yusarina
2025-04-10 23:40:51 +01:00
parent 3414ad8917
commit 69cc03098f
42 changed files with 12920 additions and 824 deletions
+40
View File
@@ -8,6 +8,7 @@ from bpy_extras.io_utils import ImportHelper
from typing import Optional, Callable, Dict, List, Union, Set
from ..common import clear_default_objects
from ..translations import t
from ..mmd.core.pmx.importer import PMXImporter
# Configure logging
logging.basicConfig(level=logging.INFO)
@@ -94,6 +95,12 @@ import_types: Dict[str, ImportMethod] = {
files=files, directory=directory, filepath=filepath,
automatic_bone_orientation=False, use_prepost_rot=False, use_anim=False
),
"pmx": lambda directory, files, filepath: import_multi_files(
directory=directory,
files=files,
filepath=filepath,
method=lambda directory, filepath: import_pmx_file(filepath)
),
"smd": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
"dmx": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
"gltf": lambda directory, files, filepath: bpy.ops.import_scene.gltf(files=files, filepath=filepath),
@@ -193,3 +200,36 @@ class AvatarToolKit_OT_Import(Operator, ImportHelper):
self.report({'INFO'}, t('Quick_Access.import_success'))
return {'FINISHED'}
def import_pmx_file(filepath: str) -> None:
"""
Import a PMX file using the MMD Tools PMXImporter
Args:
filepath: Path to the PMX file
"""
# Default import settings
import_settings = {
"filepath": filepath,
"scale": 0.08,
"types": {"MESH", "ARMATURE", "MORPHS", "DISPLAY"},
"clean_model": True,
"remove_doubles": False,
"fix_IK_links": True,
"ik_loop_factor": 3,
"use_mipmap": True,
"sph_blend_factor": 1.0,
"spa_blend_factor": 1.0,
"rename_LR_bones": False,
"use_underscore": False,
"apply_bone_fixed_axis": False,
}
# Create and execute the importer
importer = PMXImporter()
try:
importer.execute(**import_settings)
logger.info(f"Successfully imported PMX file: {filepath}")
except Exception as e:
logger.error(f"Failed to import PMX file: {str(e)}", exc_info=True)
raise
View File
+66
View File
@@ -0,0 +1,66 @@
# -*- 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
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import os
import tomllib
# This is a temporary workaround i be changing how MMD Tools works later when it comes to getting version number.
try:
current_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(os.path.dirname(current_dir))
manifest_path = os.path.join(root_dir, 'blender_manifest.toml')
if os.path.exists(manifest_path):
with open(manifest_path, 'rb') as f:
manifest = tomllib.load(f)
AVATAR_TOOLKIT_VERSION = manifest.get('version', '0.2.1')
else:
AVATAR_TOOLKIT_VERSION = '0.2.1'
except Exception:
AVATAR_TOOLKIT_VERSION = '0.2.1'
+114 -126
View File
@@ -1,27 +1,28 @@
# -*- coding: utf-8 -*-
# Copyright 2013 MMD Tools authors
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
# All credit goes to the original authors.
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import contextlib
from typing import Generator, List, Optional, TypeVar, Dict, Any, Set, Tuple, Type
from typing import Generator, List, Optional, TypeVar
import bpy
from bpy.types import Object, Material, Context
from mathutils import Vector, Matrix
from ...logging_setup import logger
from ...addon_preferences import get_preference, save_preference
class Props: # For API changes of only name changed properties
show_in_front = "show_in_front"
display_type = "display_type"
display_size = "display_size"
empty_display_type = "empty_display_type"
empty_display_size = "empty_display_size"
class __EditMode:
"""Context manager for edit mode operations"""
def __init__(self, obj: Object):
def __init__(self, obj):
if not isinstance(obj, bpy.types.Object):
raise ValueError("Expected a Blender Object")
raise ValueError
self.__prevMode = obj.mode
self.__obj = obj
self.__obj_select = obj.select_get()
@@ -40,18 +41,17 @@ class __EditMode:
class __SelectObjects:
"""Context manager for object selection operations"""
def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None):
def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None):
if not isinstance(active_object, bpy.types.Object):
raise ValueError("Expected a Blender Object")
raise ValueError
try:
bpy.ops.object.mode_set(mode="OBJECT")
except Exception:
pass
context = FnContext.ensure_context()
contenxt = FnContext.ensure_context()
for i in context.selected_objects:
for i in contenxt.selected_objects:
i.select_set(False)
self.__active_object = active_object
@@ -60,10 +60,10 @@ class __SelectObjects:
self.__hides: List[bool] = []
for i in self.__selected_objects:
self.__hides.append(i.hide_get())
FnContext.select_object(context, i)
FnContext.set_active_object(context, active_object)
FnContext.select_object(contenxt, i)
FnContext.set_active_object(contenxt, active_object)
def __enter__(self) -> Object:
def __enter__(self) -> bpy.types.Object:
return self.__active_object
def __exit__(self, type, value, traceback):
@@ -71,14 +71,12 @@ class __SelectObjects:
i.hide_set(j)
def setParent(obj: Object, parent: Object) -> None:
"""Set parent relationship between objects"""
def setParent(obj, parent):
with select_object(parent, objects=[parent, obj]):
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
"""Set parent relationship to a specific bone"""
def setParentToBone(obj, parent, bone_name):
with select_object(parent, objects=[parent, obj]):
bpy.ops.object.mode_set(mode="POSE")
parent.data.bones.active = parent.data.bones[bone_name]
@@ -86,7 +84,7 @@ def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
bpy.ops.object.mode_set(mode="OBJECT")
def edit_object(obj: Object):
def edit_object(obj):
"""Set the object interaction mode to 'EDIT'
It is recommended to use 'edit_object' with 'with' statement like the following code.
@@ -97,7 +95,7 @@ def edit_object(obj: Object):
return __EditMode(obj)
def select_object(obj: Object, objects: Optional[List[Object]] = None):
def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None):
"""Select objects.
It is recommended to use 'select_object' with 'with' statement like the following code.
@@ -106,22 +104,20 @@ def select_object(obj: Object, objects: Optional[List[Object]] = None):
with select_object(obj):
some functions...
"""
# TODO: Reimplement with bpy.context.temp_override (If it ain't broke, don't fix it.)
return __SelectObjects(obj, objects)
def duplicateObject(obj: Object, total_len: int) -> List[Object]:
"""Duplicate an object multiple times"""
def duplicateObject(obj, total_len):
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
def createObject(name: str = "Object", object_data: Optional[Any] = None, target_scene: Optional[Any] = None) -> Object:
"""Create a new object and link it to the scene"""
def createObject(name="Object", object_data=None, target_scene=None):
context = FnContext.ensure_context(target_scene)
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data))
def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object:
"""Create a sphere mesh object"""
def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None):
import bmesh
if target_object is None:
@@ -142,8 +138,7 @@ def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, targe
return target_object
def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object:
"""Create a box mesh object"""
def makeBox(size=(1, 1, 1), target_object=None):
import bmesh
from mathutils import Matrix
@@ -164,9 +159,9 @@ def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optiona
return target_object
def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object:
"""Create a capsule mesh object"""
def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None):
import math
import bmesh
if target_object is None:
@@ -179,6 +174,7 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig
top = (0, 0, height / 2 + radius)
verts.new(top)
# f = lambda i: radius*i/ring_count
f = lambda i: radius * math.sin(0.5 * math.pi * i / ring_count)
for i in range(ring_count, 0, -1):
z = f(i - 1)
@@ -228,12 +224,10 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig
class TransformConstraintOp:
"""Helper class for transform constraints"""
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
@staticmethod
def create(constraints, name: str, map_type: str):
"""Create a transform constraint"""
def create(constraints, name, map_type):
c = constraints.get(name, None)
if c and c.type != "TRANSFORM":
constraints.remove(c)
@@ -251,8 +245,7 @@ class TransformConstraintOp:
return c
@classmethod
def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]:
"""Get min/max attribute names for a constraint type"""
def min_max_attributes(cls, map_type, name_id=""):
key = (map_type, name_id)
ret = cls.__MIN_MAX_MAP.get(key, None)
if ret is None:
@@ -262,8 +255,7 @@ class TransformConstraintOp:
return ret
@classmethod
def update_min_max(cls, constraint, value: float, influence: Optional[float] = 1):
"""Update min/max values for a constraint"""
def update_min_max(cls, constraint, value, influence=1):
c = constraint
if not c or c.type != "TRANSFORM":
return
@@ -283,19 +275,18 @@ class TransformConstraintOp:
class FnObject:
"""Function collection for object operations"""
def __init__(self):
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def mesh_remove_shape_key(mesh_object: Object, shape_key: bpy.types.ShapeKey) -> None:
"""Remove a shape key from a mesh object, cleaning up drivers"""
def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey):
assert isinstance(mesh_object.data, bpy.types.Mesh)
key: bpy.types.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
for fc_curve in mesh_object.animation_data.drivers:
if not fc_curve.data_path.startswith(shape_key.path_from_id()):
continue
@@ -311,52 +302,42 @@ class FnObject:
mesh_object.active_shape_key_index = min(last_index, len(key_blocks) - 1)
T = TypeVar("T")
ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = TypeVar("ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE")
class FnContext:
"""Function collection for context operations"""
def __init__(self):
raise NotImplementedError("This class is not expected to be instantiated.")
@staticmethod
def ensure_context(context: Optional[Context] = None) -> Context:
"""Get a valid context, using bpy.context if none provided"""
def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
return context or bpy.context
@staticmethod
def get_active_object(context: Context) -> Optional[Object]:
"""Get the active object from context safely"""
if context is None or not hasattr(context, 'active_object'):
return None
def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
return context.active_object
@staticmethod
def set_active_object(context: Context, obj: Object) -> Object:
"""Set the active object in context"""
def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
context.view_layer.objects.active = obj
return obj
@staticmethod
def set_active_and_select_single_object(context: Context, obj: Object) -> Object:
"""Set an object as active and the only selected object"""
def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
@staticmethod
def get_scene_objects(context: Context) -> List[Object]:
"""Get all objects in the scene safely"""
if context is None or not hasattr(context, 'scene') or not hasattr(context.scene, 'objects'):
return []
def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects:
return context.scene.objects
@staticmethod
def ensure_selectable(context: Context, obj: Object) -> Object:
"""Make sure an object is selectable by unhiding it and its collections"""
def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.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:
for lc in layer_collection.children:
if __layer_check(lc):
@@ -379,47 +360,47 @@ class FnContext:
return obj
@staticmethod
def select_object(context: Context, obj: Object) -> Object:
"""Select an object in the context"""
def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
FnContext.ensure_selectable(context, obj).select_set(True)
return obj
@staticmethod
def select_objects(context: Context, *objects: Object) -> List[Object]:
"""Select multiple objects in the context"""
def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]:
return [FnContext.select_object(context, obj) for obj in objects]
@staticmethod
def select_single_object(context: Context, obj: Object) -> Object:
"""Select only the specified object, deselecting all others"""
def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.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: Context, obj: Object) -> Object:
"""Link an object to the active collection"""
def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
context.collection.objects.link(obj)
return obj
@staticmethod
def new_and_link_object(context: Context, name: str, object_data: Optional[Any]) -> Object:
"""Create a new object and link it to the active collection"""
def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object:
return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
@staticmethod
def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[Object]:
def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]:
"""
Duplicate an object to reach the target count.
Duplicate object.
This function duplicates the given object and returns a list of duplicated objects.
Args:
context: The context in which the duplication is performed
object_to_duplicate: The object to be duplicated
target_count: The desired count of duplicated objects
context (bpy.types.Context): The context in which the duplication is performed.
object_to_duplicate (bpy.types.Object): The object to be duplicated.
target_count (int): The desired count of duplicated objects.
Returns:
A list of duplicated objects
List[bpy.types.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.
"""
for o in context.selected_objects:
o.select_set(False)
@@ -443,16 +424,16 @@ class FnContext:
return result_objects
@staticmethod
def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[bpy.types.LayerCollection]:
def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]:
"""
Find the layer collection containing the target object.
Finds the layer collection that contains the given target_object in the user's collections.
Args:
context: The Blender context
target_object: The target object to find the layer collection for
context (bpy.types.Context): The Blender context.
target_object (bpy.types.Object): The target object to find the layer collection for.
Returns:
The layer collection containing the target object, or None if not found
Optional[bpy.types.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
@@ -460,6 +441,7 @@ class FnContext:
if layer_collection.name == name:
return layer_collection
child_layer_collection: bpy.types.LayerCollection
for child_layer_collection in layer_collection.children:
found = find_layer_collection_by_name(child_layer_collection, name)
if found is not None:
@@ -467,6 +449,7 @@ class FnContext:
return None
user_collection: bpy.types.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:
@@ -476,16 +459,27 @@ class FnContext:
@staticmethod
@contextlib.contextmanager
def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]:
def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]:
"""
Temporarily override the active layer collection to the one containing the target object.
Context manager to temporarily override the active_layer_collection that contains the target object.
This context manager allows you to temporarily change the active_layer_collection in the given context to the one that contains the target object.
It ensures that the original active_layer_collection is restored after the context is exited.
Args:
context: The context to modify
target_object: The object whose collection should become active
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.
Yields:
The modified context
bpy.types.Context: The modified context with the active_layer_collection overridden.
Example:
with FnContext.temp_override_active_layer_collection(context, target_object):
# Perform operations with the modified context
bpy.ops.object.select_all(action='DESELECT')
target_object.select_set(True)
bpy.ops.object.delete()
"""
original_layer_collection = context.view_layer.active_layer_collection
target_layer_collection = FnContext.find_user_layer_collection_by_object(context, target_object)
@@ -498,36 +492,30 @@ class FnContext:
context.view_layer.active_layer_collection = original_layer_collection
@staticmethod
@contextlib.contextmanager
def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]:
addon: bpy.types.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:
return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value)
@staticmethod
def temp_override_objects(
context: Context,
active_object: Optional[Object] = None,
selected_objects: Optional[List[Object]] = None,
**keywords
) -> Generator[Context, None, None]:
"""Create a temporary context override for object operations using Blender 4.4+ temp_override."""
override_dict = {}
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]:
if active_object is not None:
override_dict["active_object"] = active_object
override_dict["object"] = active_object
keywords["active_object"] = active_object
keywords["object"] = active_object
if selected_objects is not None:
override_dict["selected_objects"] = selected_objects
override_dict["selected_editable_objects"] = selected_objects
override_dict.update(keywords)
with context.temp_override(**override_dict) as override_context:
yield override_context
keywords["selected_objects"] = selected_objects
keywords["selected_editable_objects"] = selected_objects
@staticmethod
def get_preference(key: str, default: T = None) -> T:
"""
Get a preference value using Avatar Toolkit's preference system."""
return get_preference(key, default)
@staticmethod
def save_preference(key: str, value: Any) -> None:
"""Save a preference value using Avatar Toolkit's preference system."""
save_preference(key, value)
return context.temp_override(window=window, area=area, region=region, **keywords)
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
+141 -164
View File
@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright 2013 MMD Tools authors
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
# All credit goes to the original authors.
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math
from typing import TYPE_CHECKING, Iterable, Optional, Set
@@ -12,19 +11,13 @@ from typing import TYPE_CHECKING, Iterable, Optional, Set
import bpy
from mathutils import Vector
from ..logging_setup import logger
from .. import common
from ..common import ProgressTracker
from .. import bpyutils
from ..bpyutils import TransformConstraintOp
from ..utils import ItemOp
# Constants for bone collections
BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools"
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection"
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection"
BONE_COLLECTION_NAME_SHADOW = "mmd_shadow"
BONE_COLLECTION_NAME_DUMMY = "mmd_dummy"
SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NAME_DUMMY]
if TYPE_CHECKING:
from ..properties.root import MMDRoot, MMDDisplayItemFrame
from ..properties.pose_bone import MMDBone
def remove_constraint(constraints, name):
@@ -42,6 +35,15 @@ def remove_edit_bones(edit_bones, bone_names):
edit_bones.remove(b)
BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools"
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection"
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection"
BONE_COLLECTION_NAME_SHADOW = "mmd_shadow"
BONE_COLLECTION_NAME_DUMMY = "mmd_dummy"
SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NAME_DUMMY]
class FnBone:
AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首")
AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指")
@@ -77,6 +79,23 @@ class FnBone:
bones = armature_object.pose.bones
return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone)
@staticmethod
def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True):
for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone: MMDBone = b.mmd_bone
mmd_bone.enabled_fixed_axis = enable
lock_rotation = b.lock_rotation[:]
if enable:
axes = b.bone.matrix_local.to_3x3().transposed()
if lock_rotation.count(False) == 1:
mmd_bone.fixed_axis = axes[lock_rotation.index(False)].xzy
else:
mmd_bone.fixed_axis = axes[1].xzy # Y-axis
elif all(b.lock_location) and lock_rotation.count(True) > 1 and lock_rotation == (b.lock_ik_x, b.lock_ik_y, b.lock_ik_z):
# unlock transform locks if fixed axis was applied
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = (False, False, False)
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
@@ -217,81 +236,59 @@ class FnBone:
display_item_frames.remove(i)
mmd_root.active_display_item_frame = 0
@staticmethod
def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True):
for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone = b.mmd_bone
mmd_bone.enabled_fixed_axis = enable
lock_rotation = b.lock_rotation[:]
if enable:
axes = b.bone.matrix_local.to_3x3().transposed()
if lock_rotation.count(False) == 1:
mmd_bone.fixed_axis = axes[lock_rotation.index(False)].xzy
else:
mmd_bone.fixed_axis = axes[1].xzy # Y-axis
elif all(b.lock_location) and lock_rotation.count(True) > 1 and lock_rotation == (b.lock_ik_x, b.lock_ik_y, b.lock_ik_z):
# unlock transform locks if fixed axis was applied
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = (False, False, False)
b.lock_location = b.lock_scale = (False, False, False)
@staticmethod
def apply_bone_fixed_axis(armature_object: bpy.types.Object):
with ProgressTracker(bpy.context, 100, "Applying Bone Fixed Axis") as progress:
bone_map = {}
for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
continue
mmd_bone = b.mmd_bone
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)
progress.step("Processing bones")
bone_map = {}
for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
continue
mmd_bone: MMDBone = b.mmd_bone
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
force_align = True
with common.edit_object(armature_object) as data:
bone: bpy.types.EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
continue
fixed_axis, is_tip, parent_tip = bone_map[bone.name]
if fixed_axis.length:
axes = [bone.x_axis, bone.y_axis, bone.z_axis]
direction = fixed_axis.normalized().xzy
idx, val = max([(i, direction.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
idx_1, idx_2 = (idx + 1) % 3, (idx + 2) % 3
axes[idx] = -direction if val < 0 else direction
axes[idx_2] = axes[idx].cross(axes[idx_1])
axes[idx_1] = axes[idx_2].cross(axes[idx])
if parent_tip and bone.use_connect:
bone.use_connect = False
bone.head = bone.parent.head
if force_align:
tail = bone.head + axes[1].normalized() * bone.length
if is_tip or (tail - bone.tail).length > 1e-4:
for c in bone.children:
if c.use_connect:
c.use_connect = False
if is_tip:
c.head = bone.head
bone.tail = tail
bone.align_roll(axes[2])
bone_map[bone.name] = tuple(i != idx for i in range(3))
else:
bone_map[bone.name] = (True, True, True)
bone.select = True
progress.step("Applying locks")
for bone_name, locks in bone_map.items():
b = armature_object.pose.bones[bone_name]
b.lock_location = (True, True, True)
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
force_align = True
with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
continue
fixed_axis, is_tip, parent_tip = bone_map[bone.name]
if fixed_axis.length:
axes = [bone.x_axis, bone.y_axis, bone.z_axis]
direction = fixed_axis.normalized().xzy
idx, val = max([(i, direction.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
idx_1, idx_2 = (idx + 1) % 3, (idx + 2) % 3
axes[idx] = -direction if val < 0 else direction
axes[idx_2] = axes[idx].cross(axes[idx_1])
axes[idx_1] = axes[idx_2].cross(axes[idx])
if parent_tip and bone.use_connect:
bone.use_connect = False
bone.head = bone.parent.head
if force_align:
tail = bone.head + axes[1].normalized() * bone.length
if is_tip or (tail - bone.tail).length > 1e-4:
for c in bone.children:
if c.use_connect:
c.use_connect = False
if is_tip:
c.head = bone.head
bone.tail = tail
bone.align_roll(axes[2])
bone_map[bone.name] = tuple(i != idx for i in range(3))
else:
bone_map[bone.name] = (True, True, True)
bone.select = True
for bone_name, locks in bone_map.items():
b = armature_object.pose.bones[bone_name]
b.lock_location = (True, True, True)
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):
for b in FnBone.__get_selected_pose_bones(armature_object):
mmd_bone = b.mmd_bone
mmd_bone: MMDBone = b.mmd_bone
mmd_bone.enabled_local_axes = enable
if enable:
axes = b.bone.matrix_local.to_3x3().transposed()
@@ -300,25 +297,22 @@ class FnBone:
@staticmethod
def apply_bone_local_axes(armature_object: bpy.types.Object):
with ProgressTracker(bpy.context, 100, "Applying Bone Local Axes") as progress:
bone_map = {}
for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
bone_map = {}
for b in armature_object.pose.bones:
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
continue
mmd_bone: MMDBone = b.mmd_bone
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
with bpyutils.edit_object(armature_object) as data:
bone: bpy.types.EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
continue
mmd_bone = b.mmd_bone
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
progress.step("Processing bones")
with common.edit_object(armature_object) as data:
bone: bpy.types.EditBone
for bone in data.edit_bones:
if bone.name not in bone_map:
bone.select = False
continue
local_axis_x, local_axis_z = bone_map[bone.name]
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
bone.select = True
local_axis_x, local_axis_z = bone_map[bone.name]
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
bone.select = True
@staticmethod
def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z):
@@ -336,21 +330,17 @@ class FnBone:
@staticmethod
def apply_auto_bone_roll(armature):
with ProgressTracker(bpy.context, 100, "Applying Auto Bone Roll") as progress:
bone_names = []
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)
progress.step("Processing bones")
with common.edit_object(armature) as data:
bone: bpy.types.EditBone
for bone in data.edit_bones:
if bone.name not in bone_names:
continue
FnBone.update_auto_bone_roll(bone)
bone.select = True
bone_names = []
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
for bone in data.edit_bones:
if bone.name not in bone_names:
continue
FnBone.update_auto_bone_roll(bone)
bone.select = True
@staticmethod
def update_auto_bone_roll(edit_bone):
@@ -385,8 +375,6 @@ class FnBone:
@staticmethod
def clean_additional_transformation(armature_object: bpy.types.Object):
logger.info(f"Cleaning additional transformations for {armature_object.name}")
# clean constraints
p_bone: bpy.types.PoseBone
for p_bone in armature_object.pose.bones:
@@ -396,7 +384,6 @@ class FnBone:
remove_constraint(constraints, "mmd_additional_location")
if remove_constraint(constraints, "mmd_additional_parent"):
p_bone.bone.use_inherit_rotation = True
# clean shadow bones
shadow_bone_types = {
"DUMMY",
@@ -410,48 +397,41 @@ class FnBone:
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
if len(shadow_bone_names) > 0:
with common.edit_object(armature_object) as data:
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):
with ProgressTracker(bpy.context, 100, "Applying Additional Transformations") as progress:
def __is_dirty_bone(b):
if b.is_mmd_shadow_bone:
return False
mmd_bone = b.mmd_bone
if mmd_bone.has_additional_rotation or mmd_bone.has_additional_location:
return True
return mmd_bone.is_additional_transform_dirty
def __is_dirty_bone(b):
if b.is_mmd_shadow_bone:
return False
mmd_bone = b.mmd_bone
if mmd_bone.has_additional_rotation or mmd_bone.has_additional_location:
return True
return mmd_bone.is_additional_transform_dirty
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
progress.step("Setting up constraints")
# setup constraints
shadow_bone_pool = []
for p_bone in dirty_bones:
sb = FnBone.__setup_constraints(p_bone)
if sb:
shadow_bone_pool.append(sb)
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
progress.step("Setting up shadow bones")
# setup shadow bones
with common.edit_object(armature_object) as data:
edit_bones = data.edit_bones
for sb in shadow_bone_pool:
sb.update_edit_bones(edit_bones)
# setup constraints
shadow_bone_pool = []
for p_bone in dirty_bones:
sb = FnBone.__setup_constraints(p_bone)
if sb:
shadow_bone_pool.append(sb)
pose_bones = armature_object.pose.bones
# setup shadow bones
with bpyutils.edit_object(armature_object) as data:
edit_bones = data.edit_bones
for sb in shadow_bone_pool:
sb.update_pose_bones(pose_bones)
sb.update_edit_bones(edit_bones)
progress.step("Finalizing")
# finish
for p_bone in dirty_bones:
p_bone.mmd_bone.is_additional_transform_dirty = False
pose_bones = armature_object.pose.bones
for sb in shadow_bone_pool:
sb.update_pose_bones(pose_bones)
# finish
for p_bone in dirty_bones:
p_bone.mmd_bone.is_additional_transform_dirty = False
@staticmethod
def __setup_constraints(p_bone):
@@ -459,7 +439,7 @@ class FnBone:
mmd_bone = p_bone.mmd_bone
influence = mmd_bone.additional_transform_influence
target_bone = mmd_bone.additional_transform_bone
mute_rotation = not mmd_bone.has_additional_rotation
mute_rotation = not mmd_bone.has_additional_rotation # or p_bone.is_in_ik_chain
mute_location = not mmd_bone.has_additional_location
constraints = p_bone.constraints
@@ -501,15 +481,12 @@ class MigrationFnBone:
@staticmethod
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object):
with ProgressTracker(bpy.context, 100, "Fixing MMD IK Limit Override") as progress:
pose_bone: bpy.types.PoseBone
for pose_bone in armature_object.pose.bones:
constraint: bpy.types.Constraint
for constraint in pose_bone.constraints:
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
constraint.owner_space = "LOCAL"
progress.step("Fixed IK limit overrides")
pose_bone: bpy.types.PoseBone
for pose_bone in armature_object.pose.bones:
constraint: bpy.types.Constraint
for constraint in pose_bone.constraints:
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
constraint.owner_space = "LOCAL"
class _AT_ShadowBoneRemove:
+257
View File
@@ -0,0 +1,257 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math
from typing import Optional
import bpy
from ..bpyutils import FnContext, Props
class FnCamera:
@staticmethod
def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]:
if obj is None:
return None
if FnCamera.is_mmd_camera_root(obj):
return obj
if obj.parent is not None and FnCamera.is_mmd_camera_root(obj.parent):
return obj.parent
return None
@staticmethod
def is_mmd_camera(obj: bpy.types.Object) -> bool:
return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None
@staticmethod
def is_mmd_camera_root(obj: bpy.types.Object) -> bool:
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):
d = id_data.driver_add(data_path, index).driver
d.type = "SCRIPTED"
if "$empty_distance" in expression:
v = d.variables.new()
v.name = "empty_distance"
v.type = "TRANSFORMS"
v.targets[0].id = camera_object
v.targets[0].transform_type = "LOC_Y"
v.targets[0].transform_space = "LOCAL_SPACE"
expression = expression.replace("$empty_distance", v.name)
if "$is_perspective" in expression:
v = d.variables.new()
v.name = "is_perspective"
v.type = "SINGLE_PROP"
v.targets[0].id_type = "OBJECT"
v.targets[0].id = camera_object.parent
v.targets[0].data_path = "mmd_camera.is_perspective"
expression = expression.replace("$is_perspective", v.name)
if "$angle" in expression:
v = d.variables.new()
v.name = "angle"
v.type = "SINGLE_PROP"
v.targets[0].id_type = "OBJECT"
v.targets[0].id = camera_object.parent
v.targets[0].data_path = "mmd_camera.angle"
expression = expression.replace("$angle", v.name)
if "$sensor_height" in expression:
v = d.variables.new()
v.name = "sensor_height"
v.type = "SINGLE_PROP"
v.targets[0].id_type = "CAMERA"
v.targets[0].id = camera_object.data
v.targets[0].data_path = "sensor_height"
expression = expression.replace("$sensor_height", v.name)
d.expression = expression
__add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45")
__add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1)
__add_driver(camera_object.data, "type", "not $is_perspective")
__add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2")
@staticmethod
def remove_drivers(camera_object: bpy.types.Object):
camera_object.data.driver_remove("ortho_scale")
camera_object.driver_remove("rotation_euler")
camera_object.data.driver_remove("ortho_scale")
camera_object.data.driver_remove("lens")
class MigrationFnCamera:
@staticmethod
def update_mmd_camera():
for camera_object in bpy.data.objects:
if camera_object.type != "CAMERA":
continue
root_object = FnCamera.find_root(camera_object)
if root_object is None:
# It's not a MMD Camera
continue
FnCamera.remove_drivers(camera_object)
FnCamera.add_drivers(camera_object)
class MMDCamera:
def __init__(self, obj):
root_object = FnCamera.find_root(obj)
if root_object is None:
raise ValueError("%s is not MMDCamera" % str(obj))
self.__emptyObj = getattr(root_object, "original", obj)
@staticmethod
def isMMDCamera(obj: bpy.types.Object) -> bool:
return FnCamera.find_root(obj) is not None
@staticmethod
def addDrivers(cameraObj: bpy.types.Object):
FnCamera.add_drivers(cameraObj)
@staticmethod
def removeDrivers(cameraObj: bpy.types.Object):
if cameraObj.type != "CAMERA":
return
FnCamera.remove_drivers(cameraObj)
@staticmethod
def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0):
if FnCamera.is_mmd_camera(cameraObj):
return MMDCamera(cameraObj)
empty = bpy.data.objects.new(name="MMD_Camera", object_data=None)
FnContext.link_object(FnContext.ensure_context(), empty)
cameraObj.parent = empty
cameraObj.data.sensor_fit = "VERTICAL"
cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV
cameraObj.data.ortho_scale = 25 * scale
cameraObj.data.clip_end = 500 * scale
setattr(cameraObj.data, Props.display_size, 5 * scale)
cameraObj.location = (0, -45 * scale, 0)
cameraObj.rotation_mode = "XYZ"
cameraObj.rotation_euler = (math.radians(90), 0, 0)
cameraObj.lock_location = (True, False, True)
cameraObj.lock_rotation = (True, True, True)
cameraObj.lock_scale = (True, True, True)
cameraObj.data.dof.focus_object = empty
FnCamera.add_drivers(cameraObj)
empty.location = (0, 0, 10 * scale)
empty.rotation_mode = "YXZ"
setattr(empty, Props.empty_display_size, 5 * scale)
empty.lock_scale = (True, True, True)
empty.mmd_type = "CAMERA"
empty.mmd_camera.angle = math.radians(30)
empty.mmd_camera.persp = True
return MMDCamera(empty)
@staticmethod
def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1):
scene = bpy.context.scene
mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera"))
FnContext.link_object(FnContext.ensure_context(), mmd_cam)
MMDCamera.convertToMMDCamera(mmd_cam, scale=scale)
mmd_cam_root = mmd_cam.parent
_camera_override_func = None
if cameraObj is None:
if scene.camera is None:
scene.camera = mmd_cam
return MMDCamera(mmd_cam_root)
_camera_override_func = lambda: scene.camera
_target_override_func = None
if cameraTarget is None:
_target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj
action_name = mmd_cam_root.name
parent_action = bpy.data.actions.new(name=action_name)
distance_action = bpy.data.actions.new(name=action_name + "_dis")
FnCamera.remove_drivers(mmd_cam)
from math import atan
from mathutils import Matrix, Vector
render = scene.render
factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x)
matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1]))
neg_z_vector = Vector((0, 0, -1))
frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end + 1, scene.frame_current
frame_count = frame_end - frame_start
frames = range(frame_start, frame_end)
fcurves = []
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves:
c.keyframe_points.add(frame_count)
for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)):
scene.frame_set(f)
if _camera_override_func:
cameraObj = _camera_override_func()
if _target_override_func:
cameraTarget = _target_override_func(cameraObj)
cam_matrix_world = cameraObj.matrix_world
cam_target_loc = cameraTarget.matrix_world.translation
cam_rotation = (cam_matrix_world @ matrix_rotation).to_euler(mmd_cam_root.rotation_mode)
cam_vec = cam_matrix_world.to_3x3() @ neg_z_vector
if cameraObj.data.type == "ORTHO":
cam_dis = -(9 / 5) * cameraObj.data.ortho_scale
if cameraObj.data.sensor_fit != "VERTICAL":
if cameraObj.data.sensor_fit == "HORIZONTAL":
cam_dis *= factor
else:
cam_dis *= min(1, factor)
else:
target_vec = cam_target_loc - cam_matrix_world.translation
cam_dis = -max(target_vec.length * cam_vec.dot(target_vec.normalized()), min_distance)
cam_target_loc = cam_matrix_world.translation - cam_vec * cam_dis
tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2
if cameraObj.data.sensor_fit != "VERTICAL":
ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height
if cameraObj.data.sensor_fit == "HORIZONTAL":
tan_val *= factor * ratio
else: # cameraObj.data.sensor_fit == 'AUTO'
tan_val *= min(ratio, factor * ratio)
x.co, y.co, z.co = ((f, i) for i in cam_target_loc)
rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation)
dis.co = (f, cam_dis)
fov.co = (f, 2 * atan(tan_val))
persp.co = (f, cameraObj.data.type != "ORTHO")
persp.interpolation = "CONSTANT"
for kp in (x, y, z, rx, ry, rz, fov, dis):
kp.interpolation = "LINEAR"
FnCamera.add_drivers(mmd_cam)
mmd_cam_root.animation_data_create().action = parent_action
mmd_cam.animation_data_create().action = distance_action
scene.frame_set(frame_current)
return MMDCamera(mmd_cam_root)
def object(self):
return self.__emptyObj
def camera(self):
for i in self.__emptyObj.children:
if i.type == "CAMERA":
return i
raise KeyError
+14
View File
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
class MaterialNotFoundError(KeyError):
"""Exception raised when a material is not found in the scene"""
def __init__(self, *args: object) -> None:
"""Constructor for MaterialNotFoundError"""
super().__init__(*args)
+69
View File
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
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
+130 -109
View File
@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright 2013 MMD Tools authors
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
# All credit goes to the original authors.
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
import os
@@ -13,27 +12,40 @@ from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
import bpy
from mathutils import Vector
from ..logging_setup import logger
from ..bpyutils import FnContext
from .exceptions import MaterialNotFoundError
from .shader import _NodeGroupUtils
if TYPE_CHECKING:
from ..properties.material import MMDMaterial
# Constants for sphere modes
# TODO: use enum instead of constants
SPHERE_MODE_OFF = 0
SPHERE_MODE_MULT = 1
SPHERE_MODE_ADD = 2
SPHERE_MODE_SUBTEX = 3
class _DummyTexture:
def __init__(self, image):
self.type = "IMAGE"
self.image = image
self.use_mipmap = True
class _DummyTextureSlot:
def __init__(self, image):
self.diffuse_color_factor = 1
self.uv_layer = ""
self.texture = _DummyTexture(image)
class FnMaterial:
__NODES_ARE_READONLY: bool = False
def __init__(self, material: bpy.types.Material):
self.__material = material
self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY
logger.debug(f"Initializing FnMaterial for {material.name}")
@staticmethod
def set_nodes_are_readonly(nodes_are_readonly: bool):
@@ -115,7 +127,7 @@ class FnMaterial:
@property
def material_id(self):
mmd_mat = self.__material.mmd_material
mmd_mat: MMDMaterial = self.__material.mmd_material
if mmd_mat.material_id < 0:
max_id = -1
for mat in bpy.data.materials:
@@ -129,9 +141,11 @@ class FnMaterial:
def __same_image_file(self, image, filepath):
if image and image.source == "FILE":
img_filepath = bpy.path.abspath(image.filepath)
# pylint: disable=assignment-from-no-return
img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user()
if img_filepath == filepath:
return True
# pylint: disable=bare-except
try:
return os.path.samefile(img_filepath, filepath)
except:
@@ -141,34 +155,28 @@ class FnMaterial:
def _load_image(self, filepath):
img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None)
if img is None:
# pylint: disable=bare-except
try:
img = bpy.data.images.load(filepath)
logger.debug(f"Loaded image from {filepath}")
except:
logger.warning(f"Cannot create a texture for {filepath}. No such file.")
logging.warning("Cannot create a texture for %s. No such file.", filepath)
img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
img.source = "FILE"
img.filepath = filepath
# For Blender 4.4+
if img.depth == 32 and img.file_format != "BMP":
img.alpha_mode = "CHANNEL_PACKED"
else:
use_alpha = img.depth == 32 and img.file_format != "BMP"
if hasattr(img, "use_alpha"):
img.use_alpha = use_alpha
elif not use_alpha:
img.alpha_mode = "NONE"
return img
def update_toon_texture(self):
if self._nodes_are_readonly:
return
mmd_mat = self.__material.mmd_material
mmd_mat: MMDMaterial = self.__material.mmd_material
if mmd_mat.is_shared_toon_texture:
# Get shared toon folder from preferences
context = bpy.context
addon_prefs = context.preferences.addons.get("avatar_toolkit", None)
if addon_prefs:
shared_toon_folder = addon_prefs.preferences.shared_toon_folder
else:
shared_toon_folder = ""
toon_path = os.path.join(shared_toon_folder, f"toon{mmd_mat.shared_toon_texture + 1:02d}.bmp")
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))
self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path))
elif mmd_mat.toon_texture != "":
self.create_toon_texture(mmd_mat.toon_texture)
@@ -192,15 +200,13 @@ class FnMaterial:
if self._nodes_are_readonly:
return
mat = self.__material
mmd_mat = 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)),)
# For Blender 4.4+
if hasattr(mat, "line_color"): # freestyle line color
mat.line_color = line_color
mat_edge = bpy.data.materials.get("mmd_edge." + mat.name, None)
mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None)
if mat_edge:
mat_edge.mmd_material.edge_color = line_color
@@ -216,11 +222,11 @@ class FnMaterial:
pass
def get_texture(self):
return self.__get_texture_node("mmd_base_tex")
return self.__get_texture_node("mmd_base_tex", use_dummy=True)
def create_texture(self, filepath):
texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1))
return texture
return _DummyTextureSlot(texture.image)
def remove_texture(self):
if self._nodes_are_readonly:
@@ -228,7 +234,7 @@ class FnMaterial:
self.__remove_texture_node("mmd_base_tex")
def get_sphere_texture(self):
return self.__get_texture_node("mmd_sphere_tex")
return self.__get_texture_node("mmd_sphere_tex", use_dummy=True)
def use_sphere_texture(self, use_sphere, obj=None):
if self._nodes_are_readonly:
@@ -241,7 +247,7 @@ class FnMaterial:
def create_sphere_texture(self, filepath, obj=None):
texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2))
self.update_sphere_texture_type(obj)
return texture
return _DummyTextureSlot(texture.image)
def update_sphere_texture_type(self, obj=None):
if self._nodes_are_readonly:
@@ -258,8 +264,10 @@ class FnMaterial:
texture = self.__get_texture_node("mmd_sphere_tex")
if texture and (not texture.inputs["Vector"].is_linked or texture.inputs["Vector"].links[0].from_node.name == "mmd_tex_uv"):
# For Blender 4.4+
texture.image.colorspace_settings.name = "Linear Rec.709" if is_sph_add else "sRGB"
if hasattr(texture, "color_space"):
texture.color_space = "NONE" if is_sph_add else "COLOR"
elif hasattr(texture.image, "colorspace_settings"):
texture.image.colorspace_settings.name = "Linear Rec.709" if is_sph_add else "sRGB"
mat = self.material
nodes, links = mat.node_tree.nodes, mat.node_tree.links
@@ -269,7 +277,7 @@ class FnMaterial:
next(uv_layers, None) # skip base UV
subtex_uv = getattr(next(uv_layers, None), "name", "")
if subtex_uv != "UV1":
logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex')
logging.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv)
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
else:
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
@@ -280,7 +288,7 @@ class FnMaterial:
self.__remove_texture_node("mmd_sphere_tex")
def get_toon_texture(self):
return self.__get_texture_node("mmd_toon_tex")
return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
def use_toon_texture(self, use_toon):
if self._nodes_are_readonly:
@@ -289,18 +297,18 @@ class FnMaterial:
def create_toon_texture(self, filepath):
texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5))
return texture
return _DummyTextureSlot(texture.image)
def remove_toon_texture(self):
if self._nodes_are_readonly:
return
self.__remove_texture_node("mmd_toon_tex")
def __get_texture_node(self, node_name):
def __get_texture_node(self, node_name, use_dummy=False):
mat = self.material
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
if isinstance(texture, bpy.types.ShaderNodeTexImage):
return texture
return _DummyTexture(texture.image) if use_dummy else texture
return None
def __remove_texture_node(self, node_name):
@@ -318,6 +326,7 @@ class FnMaterial:
self.__update_shader_nodes()
nodes = self.material.node_tree.nodes
texture = nodes.new("ShaderNodeTexImage")
# pylint: disable=assignment-from-no-return
texture.label = bpy.path.display_name(node_name)
texture.name = node_name
texture.location = nodes["mmd_shader"].location + Vector((pos[0] * 210, pos[1] * 220))
@@ -330,7 +339,6 @@ class FnMaterial:
return
mat = self.material
mmd_mat = mat.mmd_material
# For Blender 4.4+
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
@@ -339,7 +347,6 @@ class FnMaterial:
return
mat = self.material
mmd_mat = mat.mmd_material
# For Blender 4.4+
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
@@ -348,14 +355,17 @@ class FnMaterial:
return
mat = self.material
mmd_mat = mat.mmd_material
# For Blender 4.4+
mat.blend_method = "HASHED"
# Update alpha in diffuse_color
if len(mat.diffuse_color) > 3:
if hasattr(mat, "blend_method"):
mat.blend_method = "HASHED" # 'BLEND'
# mat.show_transparent_back = False
elif hasattr(mat, "transparency_method"):
mat.use_transparency = True
mat.transparency_method = "Z_TRANSPARENCY"
mat.game_settings.alpha_blend = "ALPHA"
if hasattr(mat, "alpha"):
mat.alpha = mmd_mat.alpha
elif len(mat.diffuse_color) > 3:
mat.diffuse_color[3] = mmd_mat.alpha
self.__update_shader_input("Alpha", mmd_mat.alpha)
self.update_self_shadow_map()
@@ -372,11 +382,11 @@ class FnMaterial:
return
mat = self.material
mmd_mat = mat.mmd_material
# For Blender 4.4+
mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37)
mat.metallic = pow(1 - mat.roughness, 2.7)
if hasattr(mat, "metallic"):
mat.metallic = pow(1 - mat.roughness, 2.7)
if hasattr(mat, "specular_hardness"):
mat.specular_hardness = mmd_mat.shininess
self.__update_shader_input("Reflect", mmd_mat.shininess)
def update_is_double_sided(self):
@@ -384,10 +394,10 @@ class FnMaterial:
return
mat = self.material
mmd_mat = mat.mmd_material
# For Blender 4.4+
mat.use_backface_culling = not mmd_mat.is_double_sided
if hasattr(mat, "game_settings"):
mat.game_settings.use_backface_culling = not mmd_mat.is_double_sided
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)
def update_self_shadow_map(self):
@@ -396,9 +406,8 @@ class FnMaterial:
mat = self.material
mmd_mat = mat.mmd_material
cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
# For Blender 4.4+
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
if hasattr(mat, "shadow_method"):
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
def update_self_shadow(self):
if self._nodes_are_readonly:
@@ -424,8 +433,16 @@ class FnMaterial:
return child
return None
# For Blender 4.4+
preferred_output_node_target = "EEVEE"
if hasattr(context, "engine"):
active_render_engine = context.engine
else:
# use ALL anyway
active_render_engine = "ALL"
preferred_output_node_target = {
"CYCLES": "CYCLES",
"BLENDER_EEVEE_NEXT": "EEVEE",
}.get(active_render_engine, "ALL")
tex_node = None
for target in [preferred_output_node_target, "ALL"]:
@@ -453,21 +470,25 @@ class FnMaterial:
# ambient should be half the diffuse
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
# For Blender 4.4+
shadow_method = getattr(m, "shadow_method", None)
if mmd_material.diffuse_color is None:
mmd_material.diffuse_color = m.diffuse_color[:3]
# For Blender 4.4+
if len(m.diffuse_color) > 3:
if hasattr(m, "alpha"):
mmd_material.alpha = m.alpha
elif len(m.diffuse_color) > 3:
mmd_material.alpha = m.diffuse_color[3]
mmd_material.specular_color = m.specular_color
# For Blender 4.4+
mmd_material.shininess = pow(1 / max(m.roughness, 0.099), 1 / 0.37)
mmd_material.is_double_sided = not m.use_backface_culling
if hasattr(m, "specular_hardness"):
mmd_material.shininess = m.specular_hardness
else:
mmd_material.shininess = pow(1 / max(m.roughness, 0.099), 1 / 0.37)
if hasattr(m, "game_settings"):
mmd_material.is_double_sided = not m.game_settings.use_backface_culling
elif hasattr(m, "use_backface_culling"):
mmd_material.is_double_sided = not m.use_backface_culling
if shadow_method:
mmd_material.enabled_self_shadow_map = (shadow_method != "NONE") and mmd_material.alpha > 1e-3
@@ -504,13 +525,13 @@ class FnMaterial:
node_shader = nodes.get("mmd_shader", None)
if node_shader is None:
node_shader = nodes.new("ShaderNodeGroup")
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 = 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,)
@@ -522,7 +543,7 @@ class FnMaterial:
node_uv = nodes.get("mmd_tex_uv", None)
if node_uv is None:
node_uv = nodes.new("ShaderNodeGroup")
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))
node_uv.node_tree = self.__get_shader_uv()
@@ -530,7 +551,7 @@ class FnMaterial:
if not (node_shader.outputs["Shader"].is_linked or node_shader.outputs["Color"].is_linked or node_shader.outputs["Alpha"].is_linked):
node_output = next((n for n in nodes if isinstance(n, bpy.types.ShaderNodeOutputMaterial) and n.is_active_output), None)
if node_output is None:
node_output = nodes.new("ShaderNodeOutputMaterial")
node_output: bpy.types.ShaderNodeOutputMaterial = nodes.new("ShaderNodeOutputMaterial")
node_output.is_active_output = True
node_output.location = node_shader.location + Vector((400, 0))
links.new(node_shader.outputs["Shader"], node_output.inputs["Surface"])
@@ -548,26 +569,26 @@ class FnMaterial:
def __get_shader_uv(self):
group_name = "MMDTexUV"
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
ng = _NodeGroupUtils(shader)
############################################################################
_node_output = ng.new_node("NodeGroupOutput", (6, 0))
_node_output: bpy.types.NodeGroupOutput = ng.new_node("NodeGroupOutput", (6, 0))
tex_coord = ng.new_node("ShaderNodeTexCoord", (0, 0))
tex_coord: bpy.types.ShaderNodeTexCoord = ng.new_node("ShaderNodeTexCoord", (0, 0))
tex_coord1 = ng.new_node("ShaderNodeUVMap", (4, -2))
tex_coord1: bpy.types.ShaderNodeUVMap = ng.new_node("ShaderNodeUVMap", (4, -2))
tex_coord1.uv_map = "UV1"
vec_trans = ng.new_node("ShaderNodeVectorTransform", (1, -1))
vec_trans: bpy.types.ShaderNodeVectorTransform = ng.new_node("ShaderNodeVectorTransform", (1, -1))
vec_trans.vector_type = "NORMAL"
vec_trans.convert_from = "OBJECT"
vec_trans.convert_to = "CAMERA"
node_vector = ng.new_node("ShaderNodeMapping", (2, -1))
node_vector: bpy.types.ShaderNodeMapping = ng.new_node("ShaderNodeMapping", (2, -1))
node_vector.vector_type = "POINT"
node_vector.inputs["Location"].default_value = (0.5, 0.5, 0.0)
node_vector.inputs["Scale"].default_value = (0.5, 0.5, 1.0)
@@ -585,43 +606,43 @@ class FnMaterial:
def __get_shader(self):
group_name = "MMDShaderDev"
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
ng = _NodeGroupUtils(shader)
############################################################################
node_input = ng.new_node("NodeGroupInput", (-5, -1))
_node_output = ng.new_node("NodeGroupOutput", (11, 1))
node_input: bpy.types.NodeGroupInput = ng.new_node("NodeGroupInput", (-5, -1))
_node_output: bpy.types.NodeGroupOutput = ng.new_node("NodeGroupOutput", (11, 1))
node_diffuse = ng.new_mix_node("ADD", (-3, 4), fac=0.6)
node_diffuse: bpy.types.ShaderNodeMath = ng.new_mix_node("ADD", (-3, 4), fac=0.6)
node_diffuse.use_clamp = True
node_tex = ng.new_mix_node("MULTIPLY", (-2, 3.5))
node_toon = ng.new_mix_node("MULTIPLY", (-1, 3))
node_sph = ng.new_mix_node("MULTIPLY", (0, 2.5))
node_spa = ng.new_mix_node("ADD", (0, 1.5))
node_sphere = ng.new_mix_node("MIX", (1, 1))
node_tex: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (-2, 3.5))
node_toon: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (-1, 3))
node_sph: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (0, 2.5))
node_spa: bpy.types.ShaderNodeMath = ng.new_mix_node("ADD", (0, 1.5))
node_sphere: bpy.types.ShaderNodeMath = ng.new_mix_node("MIX", (1, 1))
node_geo = ng.new_node("ShaderNodeNewGeometry", (6, 3.5))
node_invert = ng.new_math_node("LESS_THAN", (7, 3))
node_cull = ng.new_math_node("MAXIMUM", (8, 2.5))
node_alpha = ng.new_math_node("MINIMUM", (9, 2))
node_geo: bpy.types.ShaderNodeNewGeometry = ng.new_node("ShaderNodeNewGeometry", (6, 3.5))
node_invert: bpy.types.ShaderNodeMath = ng.new_math_node("LESS_THAN", (7, 3))
node_cull: bpy.types.ShaderNodeMath = ng.new_math_node("MAXIMUM", (8, 2.5))
node_alpha: bpy.types.ShaderNodeMath = ng.new_math_node("MINIMUM", (9, 2))
node_alpha.use_clamp = True
node_alpha_tex = ng.new_math_node("MULTIPLY", (-1, -2))
node_alpha_toon = ng.new_math_node("MULTIPLY", (0, -2.5))
node_alpha_sph = ng.new_math_node("MULTIPLY", (1, -3))
node_alpha_tex: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (-1, -2))
node_alpha_toon: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (0, -2.5))
node_alpha_sph: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (1, -3))
node_reflect = ng.new_math_node("DIVIDE", (7, -1.5), value1=1)
node_reflect: bpy.types.ShaderNodeMath = ng.new_math_node("DIVIDE", (7, -1.5), value1=1)
node_reflect.use_clamp = True
shader_diffuse = ng.new_node("ShaderNodeBsdfDiffuse", (8, 0))
shader_glossy = ng.new_node("ShaderNodeBsdfAnisotropic", (8, -1))
shader_base_mix = ng.new_node("ShaderNodeMixShader", (9, 0))
shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = ng.new_node("ShaderNodeBsdfDiffuse", (8, 0))
shader_glossy: bpy.types.ShaderNodeBsdfAnisotropic = ng.new_node("ShaderNodeBsdfAnisotropic", (8, -1))
shader_base_mix: bpy.types.ShaderNodeMixShader = ng.new_node("ShaderNodeMixShader", (9, 0))
shader_base_mix.inputs["Fac"].default_value = 0.02
shader_trans = ng.new_node("ShaderNodeBsdfTransparent", (9, 1))
shader_alpha_mix = ng.new_node("ShaderNodeMixShader", (10, 1))
shader_trans: bpy.types.ShaderNodeBsdfTransparent = ng.new_node("ShaderNodeBsdfTransparent", (9, 1))
shader_alpha_mix: bpy.types.ShaderNodeMixShader = ng.new_node("ShaderNodeMixShader", (10, 1))
links = ng.links
links.new(node_reflect.outputs["Value"], shader_glossy.inputs["Roughness"])
@@ -679,7 +700,7 @@ class FnMaterial:
class MigrationFnMaterial:
@staticmethod
def update_mmd_shader():
mmd_shader_node_tree = bpy.data.node_groups.get("MMDShaderDev")
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
if mmd_shader_node_tree is None:
return
@@ -687,11 +708,11 @@ class MigrationFnMaterial:
if "Color" in ng.node_output.inputs:
return
shader_diffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0]
node_sphere = shader_diffuse.inputs["Color"].links[0].from_node
node_output = ng.node_output
shader_alpha_mix = node_output.inputs["Shader"].links[0].from_node
node_alpha = shader_alpha_mix.inputs["Fac"].links[0].from_node
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
shader_alpha_mix: bpy.types.ShaderNodeMixShader = node_output.inputs["Shader"].links[0].from_node
node_alpha: bpy.types.ShaderNodeMath = shader_alpha_mix.inputs["Fac"].links[0].from_node
ng.new_output_socket("Color", node_sphere.outputs["Color"])
ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
File diff suppressed because it is too large Load Diff
+798
View File
@@ -0,0 +1,798 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
import re
from typing import TYPE_CHECKING, Tuple, cast
import bpy
from .. import bpyutils, utils
from ..bpyutils import FnContext, FnObject, TransformConstraintOp
if TYPE_CHECKING:
from .model import Model
class FnMorph:
def __init__(self, morph, model: "Model"):
self.__morph = morph
self.__rig = model
@classmethod
def storeShapeKeyOrder(cls, obj, shape_key_names):
if len(shape_key_names) < 1:
return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
if obj.data.shape_keys is None:
bpy.ops.object.shape_key_add()
def __move_to_bottom(key_blocks, name):
obj.active_shape_key_index = key_blocks.find(name)
bpy.ops.object.shape_key_move(type="BOTTOM")
key_blocks = obj.data.shape_keys.key_blocks
for name in shape_key_names:
if name not in key_blocks:
obj.shape_key_add(name=name, from_mix=False)
elif len(key_blocks) > 1:
__move_to_bottom(key_blocks, name)
@classmethod
def fixShapeKeyOrder(cls, obj, shape_key_names):
if len(shape_key_names) < 1:
return
assert FnContext.get_active_object(FnContext.ensure_context()) == obj
key_blocks = getattr(obj.data.shape_keys, "key_blocks", None)
if key_blocks is None:
return
for name in shape_key_names:
idx = key_blocks.find(name)
if idx < 0:
continue
obj.active_shape_key_index = idx
bpy.ops.object.shape_key_move(type="BOTTOM")
@staticmethod
def get_morph_slider(rig):
return _MorphSlider(rig)
@staticmethod
def category_guess(morph):
name_lower = morph.name.lower()
if "mouth" in name_lower:
morph.category = "MOUTH"
elif "eye" in name_lower:
if "brow" in name_lower:
morph.category = "EYEBROW"
else:
morph.category = "EYE"
@classmethod
def load_morphs(cls, rig):
mmd_root = rig.rootObject().mmd_root
vertex_morphs = mmd_root.vertex_morphs
uv_morphs = mmd_root.uv_morphs
for obj in rig.meshes():
for kb in getattr(obj.data.shape_keys, "key_blocks", ())[1:]:
if not kb.name.startswith("mmd_") and kb.name not in vertex_morphs:
item = vertex_morphs.add()
item.name = kb.name
item.name_e = kb.name
cls.category_guess(item)
for g, name, x in FnMorph.get_uv_morph_vertex_groups(obj):
if name not in uv_morphs:
item = uv_morphs.add()
item.name = item.name_e = name
item.data_type = "VERTEX_GROUP"
cls.category_guess(item)
@staticmethod
def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str):
assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys
if shape_keys is None:
return
key_blocks = shape_keys.key_blocks
if key_blocks and shape_key_name in key_blocks:
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):
assert isinstance(mesh_object.data, bpy.types.Mesh)
shape_keys = mesh_object.data.shape_keys
if shape_keys is None:
return
key_blocks = shape_keys.key_blocks
if src_name not in key_blocks:
return
if dest_name in key_blocks:
FnObject.mesh_remove_shape_key(mesh_object, key_blocks[dest_name])
mesh_object.active_shape_key_index = key_blocks.find(src_name)
mesh_object.show_only_shape_key, last = True, mesh_object.show_only_shape_key
mesh_object.shape_key_add(name=dest_name, from_mix=True)
mesh_object.show_only_shape_key = last
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"):
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):
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name):
obj.vertex_groups.remove(vg)
for vg_name in tuple(i[0].name for i in FnMorph.get_uv_morph_vertex_groups(obj, src_name)):
obj.vertex_groups.active = obj.vertex_groups[vg_name]
with bpy.context.temp_override(object=obj, window=bpy.context.window, region=bpy.context.region):
bpy.ops.object.vertex_group_copy()
obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name)
@staticmethod
def overwrite_bone_morphs_from_action_pose(armature_object):
armature = armature_object.id_data
# Use animation_data and action instead of action_pose
if armature.animation_data is None or armature.animation_data.action is None:
logging.warning('[WARNING] armature "%s" has no animation data or action', armature_object.name)
return
action = armature.animation_data.action
pose_markers = action.pose_markers
if not pose_markers:
return
root = armature_object.parent
mmd_root = root.mmd_root
bone_morphs = mmd_root.bone_morphs
utils.selectAObject(armature_object)
original_mode = bpy.context.object.mode
bpy.ops.object.mode_set(mode="POSE")
try:
for index, pose_marker in enumerate(pose_markers):
bone_morph = next(iter([m for m in bone_morphs if m.name == pose_marker.name]), None)
if bone_morph is None:
bone_morph = bone_morphs.add()
bone_morph.name = pose_marker.name
bpy.ops.pose.select_all(action="SELECT")
bpy.ops.pose.transforms_clear()
frame = pose_marker.frame
bpy.context.scene.frame_set(int(frame))
mmd_root.active_morph = bone_morphs.find(bone_morph.name)
bpy.ops.mmd_tools.apply_bone_morph()
bpy.ops.pose.transforms_clear()
finally:
bpy.ops.object.mode_set(mode=original_mode)
utils.selectAObject(root)
@staticmethod
def clean_uv_morph_vertex_groups(obj):
# remove empty vertex groups of uv morphs
vg_indices = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)}
vertex_groups = obj.vertex_groups
for v in obj.data.vertices:
for x in v.groups:
if x.group in vg_indices and x.weight > 0:
vg_indices.remove(x.group)
for i in sorted(vg_indices, reverse=True):
vg = vertex_groups[i]
m = obj.modifiers.get("mmd_bind%s" % hash(vg.name), None)
if m:
obj.modifiers.remove(m)
vertex_groups.remove(vg)
@staticmethod
def get_uv_morph_offset_map(obj, morph):
offset_map = {} # offset_map[vertex_index] = offset_xyzw
if morph.data_type == "VERTEX_GROUP":
scale = morph.vertex_group_scale
axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)}
for v in obj.data.vertices:
i = v.index
for x in v.groups:
if x.group in axis_map and x.weight > 0:
axis, weight = axis_map[x.group], x.weight
d = offset_map.setdefault(i, [0, 0, 0, 0])
d["XYZW".index(axis[1])] += -weight * scale if axis[0] == "-" else weight * scale
else:
for val in morph.data:
i = val.index
if i in offset_map:
offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset)]
else:
offset_map[i] = val.offset
return offset_map
@staticmethod
def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"):
vertex_groups = obj.vertex_groups
morph_name = getattr(morph, "name", None)
if offset_axes:
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph_name, offset_axes):
vertex_groups.remove(vg)
if not morph_name or not offsets:
return
axis_indices = tuple("XYZW".index(x) for x in offset_axes) or tuple(range(4))
offset_map = FnMorph.get_uv_morph_offset_map(obj, morph) if offset_axes else {}
for data in offsets:
idx, offset = data.index, data.offset
for i in axis_indices:
offset_map.setdefault(idx, [0, 0, 0, 0])[i] += round(offset[i], 5)
max_value = max(max(abs(x) for x in v) for v in offset_map.values() or ([0],))
scale = morph.vertex_group_scale = max(abs(morph.vertex_group_scale), max_value)
for idx, offset in offset_map.items():
for val, axis in zip(offset, "XYZW"):
if abs(val) > 1e-4:
vg_name = "UV_{0}{1}{2}".format(morph_name, "-" if val < 0 else "+", axis)
vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name)
vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE")
def update_mat_related_mesh(self, new_mesh=None):
for offset in self.__morph.data:
# Use the new_mesh if provided
meshObj = new_mesh
if new_mesh is None:
# Try to find the mesh by material name
meshObj = self.__rig.findMesh(offset.material)
if meshObj is None:
# Given this point we need to loop through all the meshes
for mesh in self.__rig.meshes():
if mesh.data.materials.find(offset.material) >= 0:
meshObj = mesh
break
# Finally update the reference
if meshObj is not None:
offset.related_mesh = meshObj.data.name
@staticmethod
def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object):
"""Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]"""
mmd_root = mmd_root_object.mmd_root
def morph_data_equals(l, r) -> bool:
return (
l.related_mesh_data == r.related_mesh_data
and l.offset_type == r.offset_type
and l.material == r.material
and all(a == b for a, b in zip(l.diffuse_color, r.diffuse_color))
and all(a == b for a, b in zip(l.specular_color, r.specular_color))
and l.shininess == r.shininess
and all(a == b for a, b in zip(l.ambient_color, r.ambient_color))
and all(a == b for a, b in zip(l.edge_color, r.edge_color))
and l.edge_weight == r.edge_weight
and all(a == b for a, b in zip(l.texture_factor, r.texture_factor))
and all(a == b for a, b in zip(l.sphere_texture_factor, r.sphere_texture_factor))
and all(a == b for a, b in zip(l.toon_texture_factor, r.toon_texture_factor))
)
def morph_equals(l, r) -> 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[]
for material_morph in mmd_root.material_morphs:
save_materil_morph_datas = []
remove_material_morph_data_indices = []
for index, material_morph_data in enumerate(material_morph.data):
if any(morph_data_equals(material_morph_data, saved_material_morph_data) for saved_material_morph_data in save_materil_morph_datas):
remove_material_morph_data_indices.append(index)
continue
save_materil_morph_datas.append(material_morph_data)
for index in reversed(remove_material_morph_data_indices):
material_morph.data.remove(index)
# Mark duplicated mmd_root.material_morphs[]
save_material_morphs = []
remove_material_morph_names = []
for material_morph in sorted(mmd_root.material_morphs, key=lambda m: m.name):
if any(morph_equals(material_morph, saved_material_morph) for saved_material_morph in save_material_morphs):
remove_material_morph_names.append(material_morph.name)
continue
save_material_morphs.append(material_morph)
# Remove marked mmd_root.material_morphs[]
for material_morph_name in remove_material_morph_names:
mmd_root.material_morphs.remove(mmd_root.material_morphs.find(material_morph_name))
class _MorphSlider:
def __init__(self, model: "Model"):
self.__rig = model
def placeholder(self, create=False, binded=False):
rig = self.__rig
root = rig.rootObject()
obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None)
if create and obj is None:
obj = bpy.data.objects.new(name=".placeholder", object_data=bpy.data.meshes.new(".placeholder"))
obj.mmd_type = "PLACEHOLDER"
obj.parent = root
FnContext.link_object(FnContext.ensure_context(), obj)
if obj and obj.data.shape_keys is None:
key = obj.shape_key_add(name="--- morph sliders ---")
key.mute = True
obj.active_shape_key_index = 0
if binded and obj and obj.data.shape_keys.key_blocks[0].mute:
return None
return obj
@property
def dummy_armature(self):
obj = self.placeholder()
return self.__dummy_armature(obj) if obj else None
def __dummy_armature(self, obj, create=False):
arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None)
if create and arm is None:
arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature"))
arm.mmd_type = "PLACEHOLDER"
arm.parent = obj
FnContext.link_object(FnContext.ensure_context(), arm)
from .bone import FnBone
FnBone.setup_special_bone_collections(arm)
return arm
def get(self, morph_name):
obj = self.placeholder()
if obj is None:
return None
key_blocks = obj.data.shape_keys.key_blocks
if key_blocks[0].mute:
return None
return key_blocks.get(morph_name, None)
def create(self):
self.__rig.loadMorphs()
obj = self.placeholder(create=True)
self.__load(obj, self.__rig.rootObject().mmd_root)
return obj
def __load(self, obj, mmd_root):
attr_list = ("group", "vertex", "bone", "uv", "material")
morph_sliders = obj.data.shape_keys.key_blocks
for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())):
name = m.name
# if name[-1] == '\\': # fix driver's bug???
# m.name = name = name + ' '
if name and name not in morph_sliders:
obj.shape_key_add(name=name, from_mix=False)
@staticmethod
def __driver_variables(id_data, path, index=-1):
d = id_data.driver_add(path, index)
variables = d.driver.variables
for x in variables:
variables.remove(x)
return d.driver, variables
@staticmethod
def __add_single_prop(variables, id_obj, data_path, prefix):
var = variables.new()
var.name = f"{prefix}{len(variables)}"
var.type = "SINGLE_PROP"
target = var.targets[0]
target.id_type = "OBJECT"
target.id = id_obj
target.data_path = data_path
return var
@staticmethod
def __shape_key_driver_check(key_block, resolve_path=False):
if resolve_path:
try:
key_block.id_data.path_resolve(key_block.path_from_id())
except ValueError:
return False
if not key_block.id_data.animation_data:
return True
d = key_block.id_data.animation_data.drivers.find(key_block.path_from_id("value"))
if isinstance(d, int): # for Blender 2.76 or older
data_path = key_block.path_from_id("value")
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):
from math import ceil, floor
names_in_use = names_in_use or {}
rig = self.__rig
morph_sliders = self.placeholder()
morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {}
for mesh_object in rig.meshes():
for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[bpy.types.ShapeKey], ())):
if kb.name in names_in_use:
continue
if kb.name.startswith("mmd_bind"):
kb.driver_remove("value")
ms = morph_sliders[kb.relative_key.name]
kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, floor(ms.value)), max(ms.slider_max, ceil(ms.value))
kb.relative_key.value = ms.value
kb.relative_key.mute = False
FnObject.mesh_remove_shape_key(mesh_object, kb)
elif kb.name in morph_sliders and self.__shape_key_driver_check(kb):
ms = morph_sliders[kb.name]
kb.driver_remove("value")
kb.slider_min, kb.slider_max = min(ms.slider_min, floor(kb.value)), max(ms.slider_max, ceil(kb.value))
for m in mesh_object.modifiers: # uv morph
if m.name.startswith("mmd_bind") and m.name not in names_in_use:
mesh_object.modifiers.remove(m)
from .shader import _MaterialMorph
for m in rig.materials():
if m and m.node_tree:
for n in sorted((x for x in m.node_tree.nodes if x.name.startswith("mmd_bind")), key=lambda x: -x.location[0]):
_MaterialMorph.reset_morph_links(n)
m.node_tree.nodes.remove(n)
attributes = set(TransformConstraintOp.min_max_attributes("LOCATION", "to"))
attributes |= set(TransformConstraintOp.min_max_attributes("ROTATION", "to"))
for b in rig.armature().pose.bones:
for c in b.constraints:
if c.name.startswith("mmd_bind") and c.name[:-4] not in names_in_use:
for attr in attributes:
c.driver_remove(attr)
b.constraints.remove(c)
def unbind(self):
mmd_root = self.__rig.rootObject().mmd_root
# after unbind, the weird lag problem will disappear.
mmd_root.morph_panel_show_settings = True
for m in mmd_root.bone_morphs:
for d in m.data:
d.name = ""
for m in mmd_root.material_morphs:
for d in m.data:
d.name = ""
obj = self.placeholder()
if obj:
obj.data.shape_keys.key_blocks[0].mute = True
arm = self.__dummy_armature(obj)
if arm:
for b in arm.pose.bones:
if b.name.startswith("mmd_bind"):
b.driver_remove("location")
b.driver_remove("rotation_quaternion")
self.__cleanup()
def bind(self):
rig = self.__rig
root = rig.rootObject()
armObj = rig.armature()
mmd_root = root.mmd_root
# hide detail to avoid weird lag problem
mmd_root.morph_panel_show_settings = False
obj = self.create()
arm = self.__dummy_armature(obj, create=True)
morph_sliders = obj.data.shape_keys.key_blocks
# data gathering
group_map = {}
shape_key_map = {}
uv_morph_map = {}
for mesh_object in rig.meshes():
mesh_object.show_only_shape_key = False
key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ())
for kb in key_blocks:
kb_name = kb.name
if kb_name not in morph_sliders:
continue
if self.__shape_key_driver_check(kb, resolve_path=True):
name_bind, kb_bind = kb_name, kb
else:
name_bind = "mmd_bind%s" % hash(morph_sliders[kb_name])
if name_bind not in key_blocks:
mesh_object.shape_key_add(name=name_bind, from_mix=False)
kb_bind = key_blocks[name_bind]
kb_bind.relative_key = kb
kb_bind.slider_min = -10
kb_bind.slider_max = 10
data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"')
groups = []
shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups))
group_map.setdefault(("vertex_morphs", kb_name), []).append(groups)
uv_layers = [l.name for l in mesh_object.data.uv_layers if not l.name.startswith("_")]
uv_layers += [""] * (5 - len(uv_layers))
for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh_object):
morph = mmd_root.uv_morphs.get(morph_name, None)
if morph is None or morph.data_type != "VERTEX_GROUP":
continue
uv_layer = "_" + uv_layers[morph.uv_index] if axis[1] in "ZW" else uv_layers[morph.uv_index]
if uv_layer not in mesh_object.data.uv_layers:
continue
name_bind = "mmd_bind%s" % hash(vg.name)
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
mod.axis_u, mod.axis_v = ("Y", "X") if axis[1] in "YW" else ("X", "Y")
mod.uv_layer = uv_layer
name_bind = "mmd_bind%s" % hash(morph_name)
mod.object_from = mod.object_to = arm
if axis[0] == "-":
mod.bone_from, mod.bone_to = "mmd_bind_ctrl_base", name_bind
else:
mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base"
bone_offset_map = {}
with bpyutils.edit_object(arm) as data:
from .bone import FnBone
edit_bones = data.edit_bones
def __get_bone(name, parent):
b = edit_bones.get(name, None) or edit_bones.new(name=name)
b.head = (0, 0, 0)
b.tail = (0, 0, 1)
b.use_deform = False
b.parent = parent
return b
for m in mmd_root.bone_morphs:
morph_name = m.name.replace('"', '\\"')
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
for d in m.data:
if not d.bone:
d.name = ""
continue
d.name = name_bind = f"mmd_bind{hash(d)}"
b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None))
groups = []
bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups)
group_map.setdefault(("bone_morphs", m.name), []).append(groups)
ctrl_base = FnBone.set_edit_bone_to_dummy(__get_bone("mmd_bind_ctrl_base", None))
for m in mmd_root.uv_morphs:
morph_name = m.name.replace('"', '\\"')
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
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 = []
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.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 = {}
for m in mmd_root.material_morphs:
morph_name = m.name.replace('"', '\\"')
data_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
groups = []
group_map.setdefault(("material_morphs", m.name), []).append(groups)
material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups)
for d in m.data:
d.name = name_bind = f"mmd_bind{hash(d)}"
# add '#' before material name to avoid conflict with group_dict
table = material_offset_map.setdefault("#" + d.material, ([], []))
table[1 if d.offset_type == "ADD" else 0].append((m.name, d, name_bind))
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)
morph_name = m.name.replace('"', '\\"')
morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value'
for d in m.data:
data_name = d.name.replace('"', '\\"')
factor_path = f'mmd_root.group_morphs["{morph_name}"].data["{data_name}"].factor'
for groups in group_map.get((d.morph_type, d.name), ()):
groups.append((m.name, morph_path, factor_path))
self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys())
def __config_groups(variables, expression, groups):
for g_name, morph_path, factor_path in groups:
var = self.__add_single_prop(variables, obj, morph_path, "g")
fvar = self.__add_single_prop(variables, root, factor_path, "w")
expression = f"{expression}+{var.name}*{fvar.name}"
return expression
# vertex morphs
for kb_bind, morph_data_path, groups in (i for l in shape_key_map.values() for i in l):
driver, variables = self.__driver_variables(kb_bind, "value")
var = self.__add_single_prop(variables, obj, morph_data_path, "v")
if kb_bind.name.startswith("mmd_bind"):
driver.expression = f"-({__config_groups(variables, var.name, groups)})"
kb_bind.relative_key.mute = True
else:
driver.expression = __config_groups(variables, var.name, groups)
kb_bind.mute = False
# bone morphs
def __config_bone_morph(constraints, map_type, attributes, val, val_str):
c_name = f"mmd_bind{hash(data)}.{map_type[:3]}"
c = TransformConstraintOp.create(constraints, c_name, map_type)
TransformConstraintOp.update_min_max(c, val, None)
c.show_expanded = False
c.target = arm
c.subtarget = bname
for attr in attributes:
driver, variables = self.__driver_variables(armObj, c.path_from_id(attr))
var = self.__add_single_prop(variables, obj, morph_data_path, "b")
expression = __config_groups(variables, var.name, groups)
sign = "-" if attr.startswith("to_min") else ""
driver.expression = f"{sign}{val_str}*({expression})"
from math import pi
attributes_rot = TransformConstraintOp.min_max_attributes("ROTATION", "to")
attributes_loc = TransformConstraintOp.min_max_attributes("LOCATION", "to")
for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values():
b = arm.pose.bones[bname]
b.location = data.location
b.rotation_quaternion = data.rotation.__class__(*data.rotation.to_axis_angle()) # Fix for consistency
b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND"
pb = armObj.pose.bones[data.bone]
__config_bone_morph(pb.constraints, "ROTATION", attributes_rot, pi, "pi")
__config_bone_morph(pb.constraints, "LOCATION", attributes_loc, 100, "100")
# uv morphs
# HACK: workaround for Blender 2.80+, data_path can't be properly detected (Save & Reopen file also works)
root.parent, root.parent, root.matrix_parent_inverse = arm, root.parent, root.matrix_parent_inverse.copy()
b = arm.pose.bones["mmd_bind_ctrl_base"]
b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND"
for bname, data_path, scale_path, groups in (i for l in uv_morph_map.values() for i in l):
b = arm.pose.bones[bname]
b.is_mmd_shadow_bone = True
b.mmd_shadow_bone_type = "BIND"
driver, variables = self.__driver_variables(b, "location", index=0)
var = self.__add_single_prop(variables, obj, data_path, "u")
fvar = self.__add_single_prop(variables, root, scale_path, "s")
driver.expression = f"({__config_groups(variables, var.name, groups)})*{fvar.name}"
# material morphs
from .shader import _MaterialMorph
group_dict = material_offset_map.get("group_dict", {})
def __config_material_morph(mat, morph_list):
nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list))
for (morph_name, data, name_bind), node in zip(morph_list, nodes):
node.label, node.name = morph_name, name_bind
data_path, groups = group_dict[morph_name]
driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id("default_value"))
var = self.__add_single_prop(variables, obj, data_path, "m")
driver.expression = "%s" % __config_groups(variables, var.name, groups)
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.")
mul_list, add_list = [], []
else:
mat_name = "#" + mat.name
mul_list, add_list = material_offset_map.get(mat_name, ([], []))
morph_list = tuple(mul_all + mul_list + add_all + add_list)
__config_material_morph(mat, morph_list)
mat_edge = bpy.data.materials.get("mmd_edge." + mat.name, None)
if mat_edge:
__config_material_morph(mat_edge, morph_list)
morph_sliders[0].mute = False
class MigrationFnMorph:
@staticmethod
def update_mmd_morph():
from .material import FnMaterial
for root in bpy.data.objects:
if root.mmd_type != "ROOT":
continue
for mat_morph in root.mmd_root.material_morphs:
for morph_data in mat_morph.data:
if morph_data.material_data is not None:
# SUPPORT_UNTIL: 5 LTS
# The material_id is also no longer used, but for compatibility with older version mmd_tools, keep it.
if "material_id" not in morph_data.material_data.mmd_material or "material_id" not in morph_data or morph_data.material_data.mmd_material["material_id"] == morph_data["material_id"]:
# In the new version, the related_mesh property is no longer used.
# Explicitly remove this property to avoid misuse.
if "related_mesh" in morph_data:
del morph_data["related_mesh"]
continue
else:
# Compat case. The new version mmd_tools saved. And old version mmd_tools edit. Then new version mmd_tools load again.
# Go update path.
pass
morph_data.material_data = None
if "material_id" in morph_data:
mat_id = morph_data["material_id"]
if mat_id != -1:
fnMat = FnMaterial.from_material_id(mat_id)
if fnMat:
morph_data.material_data = fnMat.material
else:
morph_data["material_id"] = -1
morph_data.related_mesh_data = None
if "related_mesh" in morph_data:
related_mesh = morph_data["related_mesh"]
del morph_data["related_mesh"]
if related_mesh != "" and related_mesh in bpy.data.meshes:
morph_data.related_mesh_data = bpy.data.meshes[related_mesh]
@staticmethod
def ensure_material_id_not_conflict():
mat_ids_set = set()
# The reference library properties cannot be modified and bypassed in advance.
need_update_mat = []
for mat in bpy.data.materials:
if mat.mmd_material.material_id < 0:
continue
if mat.library is not None:
mat_ids_set.add(mat.mmd_material.material_id)
else:
need_update_mat.append(mat)
for mat in need_update_mat:
if mat.mmd_material.material_id in mat_ids_set:
mat.mmd_material.material_id = max(mat_ids_set) + 1
mat_ids_set.add(mat.mmd_material.material_id)
@staticmethod
def compatible_with_old_version_mmd_tools():
MigrationFnMorph.ensure_material_id_not_conflict()
for root in bpy.data.objects:
if root.mmd_type != "ROOT":
continue
for mat_morph in root.mmd_root.material_morphs:
for morph_data in mat_morph.data:
morph_data["related_mesh"] = morph_data.related_mesh
if morph_data.material_data is None:
morph_data.material_id = -1
else:
morph_data.material_id = morph_data.material_data.mmd_material.material_id
File diff suppressed because it is too large Load Diff
@@ -1,27 +1,21 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
# All credit goes to the original authors.
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
import collections
import logging
import os
import time
import typing
from typing import TYPE_CHECKING, List, Optional, Dict, Set, Tuple, Any, Union
from typing import TYPE_CHECKING, List, Optional
import bpy
from mathutils import Matrix, Vector
from bpy.types import Context, Object
from ...logging_setup import logger
from ...common import ProgressTracker
from ...translations import t
from ...mmd.core import bpyutils, utils
from ...mmd.core.bpyutils import FnContext
from ... import bpyutils, utils
from ...bpyutils import FnContext
from .. import pmx
from ..bone import FnBone
from ..material import FnMaterial
@@ -32,20 +26,17 @@ from ..vmd.importer import BoneConverter
from ...operators.misc import MoveObject
if TYPE_CHECKING:
from ...mmd.properties.pose_bone import MMDBone
from ...mmd.properties.root import MMDRoot
from ...properties.pose_bone import MMDBone
from ...properties.root import MMDRoot
class PMXImporter:
"""PMX model importer for Avatar Toolkit"""
CATEGORIES = {
0: "SYSTEM",
1: "EYEBROW",
2: "EYE",
3: "MOUTH",
}
MORPH_TYPES = {
0: "group_morphs",
1: "vertex_morphs",
@@ -83,17 +74,15 @@ class PMXImporter:
self.__materialFaceCountTable = None
@staticmethod
def __safe_name(name: str, max_length: int = 59) -> str:
"""Create a safe name that won't exceed Blender's name length limits"""
def __safe_name(name, max_length=59):
return str(bytes(name, "utf8")[:max_length], "utf8", errors="replace")
@staticmethod
def flipUV_V(uv: Tuple[float, float]) -> Tuple[float, float]:
"""Flip the V coordinate of UV mapping"""
def flipUV_V(uv):
u, v = uv
return u, 1.0 - v
def __createObjects(self) -> None:
def __createObjects(self):
"""Create main objects and link them to scene."""
pmxModel = self.__model
obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54)
@@ -112,15 +101,13 @@ class PMXImporter:
txt.from_string(pmxModel.comment_e.replace("\r", ""))
mmd_root.comment_e_text = txt.name
def __createMeshObject(self) -> None:
"""Create a mesh object for the model"""
def __createMeshObject(self):
model_name = self.__root.name
self.__meshObj = bpy.data.objects.new(name=model_name + "_mesh", object_data=bpy.data.meshes.new(name=model_name))
self.__meshObj.parent = self.__armObj
FnContext.link_object(self.__targetContext, self.__meshObj)
def __createBasisShapeKey(self) -> None:
"""Create a basis shape key if it doesn't exist"""
def __createBasisShapeKey(self):
if self.__meshObj.data.shape_keys:
assert len(self.__meshObj.data.vertices) > 0
assert len(self.__meshObj.data.shape_keys.key_blocks) > 1
@@ -128,13 +115,11 @@ class PMXImporter:
FnContext.set_active_object(self.__targetContext, self.__meshObj)
bpy.ops.object.shape_key_add()
def __importVertexGroup(self) -> None:
"""Import vertex groups from bones"""
def __importVertexGroup(self):
vgroups = self.__meshObj.vertex_groups
self.__vertexGroupTable = [vgroups.new(name=i.name) for i in self.__model.bones] or [vgroups.new(name="NO BONES")]
def __importVertices(self) -> None:
"""Import vertices with weights and other properties"""
def __importVertices(self):
self.__importVertexGroup()
pmxModel = self.__model
@@ -180,13 +165,12 @@ class PMXImporter:
for bone, weight in zip(pv_bones, pv_weights):
vertex_group_table[bone].add(index=idx, weight=weight, type="ADD")
else:
raise Exception("Unknown bone weight type.")
raise Exception("unkown bone weight type.")
vg_edge_scale.lock_weight = True
vg_vertex_order.lock_weight = True
def __storeVerticesSDEF(self) -> None:
"""Store SDEF vertex data for smooth deformation"""
def __storeVerticesSDEF(self):
if len(self.__sdefVertices) < 1:
return
@@ -199,28 +183,33 @@ class PMXImporter:
sdefC.data[i].co = Vector(w.c).xzy * self.__scale
sdefR0.data[i].co = Vector(w.r0).xzy * self.__scale
sdefR1.data[i].co = Vector(w.r1).xzy * self.__scale
logger.info("Stored %d SDEF vertices", len(self.__sdefVertices))
logging.info("Stored %d SDEF vertices", len(self.__sdefVertices))
def __importTextures(self) -> None:
"""Import textures from the PMX model"""
def __importTextures(self):
pmxModel = self.__model
self.__textureTable = []
for i in pmxModel.textures:
self.__textureTable.append(bpy.path.resolve_ncase(path=i.path))
def __createEditBones(self, obj: Object, pmx_bones: List[Any]) -> Tuple[List[str], List[str]]:
"""Create EditBones from pmx file data.
def __createEditBones(self, obj, pmx_bones):
"""create EditBones from pmx file data.
@return the list of bone names which can be accessed by the bone index of pmx data.
"""
editBoneTable = []
nameTable = []
specialTipBones = []
dependency_cycle_ik_bones = []
# for i, p_bone in enumerate(pmx_bones):
# if p_bone.isIK:
# if p_bone.target != -1:
# t = pmx_bones[p_bone.target]
# if p_bone.parent == t.parent:
# dependency_cycle_ik_bones.append(i)
from math import isfinite
def _VectorXZY(v: List[float]) -> Vector:
def _VectorXZY(v):
return Vector(v).xzy if all(isfinite(n) for n in v) else Vector((0, 0, 0))
with bpyutils.edit_object(obj) as data:
@@ -250,7 +239,7 @@ class PMXImporter:
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
if m_bone.isIK and m_bone.target != -1:
logger.debug("Checking IK links of %s", b_bone.name)
logging.debug(" - checking IK links of %s", b_bone.name)
b_target = editBoneTable[m_bone.target]
for i in range(len(m_bone.ik_links)):
b_bone_link = editBoneTable[m_bone.ik_links[i].target]
@@ -258,11 +247,11 @@ class PMXImporter:
b_bone_tail = b_target if i == 0 else editBoneTable[m_bone.ik_links[i - 1].target]
loc = b_bone_tail.head - b_bone_link.head
if loc.length < 0.001:
logger.warning("Unsolved IK link %s", b_bone_link.name)
logging.warning(" ** unsolved IK link %s **", b_bone_link.name)
elif b_bone_tail.parent != b_bone_link:
logger.warning("Skipped IK link %s", b_bone_link.name)
logging.warning(" ** skipped IK link %s **", b_bone_link.name)
elif (b_bone_link.tail - b_bone_tail.head).length > 1e-4:
logger.debug("Fix IK link %s", b_bone_link.name)
logging.debug(" * fix IK link %s", b_bone_link.name)
b_bone_link.tail = b_bone_link.head + loc
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
@@ -277,7 +266,7 @@ class PMXImporter:
else:
b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale
if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]:
logger.debug("Special tip bone %s, display %s", b_bone.name, str(m_bone.displayConnection))
logging.debug(" * special tip bone %s, display %s", b_bone.name, str(m_bone.displayConnection))
specialTipBones.append(b_bone.name)
for b_bone, m_bone in zip(editBoneTable, pmx_bones):
@@ -297,21 +286,19 @@ class PMXImporter:
continue
if not m_bone.isMovable:
continue
logger.warning("Connected: %s (%d)-> %s", b_bone.name, len(b_bone.children), t.name)
logging.warning(" * connected: %s (%d)-> %s", b_bone.name, len(b_bone.children), t.name)
t.use_connect = True
return nameTable, specialTipBones
def __sortPoseBonesByBoneIndex(self, pose_bones: List[bpy.types.PoseBone], bone_names: List[str]) -> List[bpy.types.PoseBone]:
"""Sort pose bones by their bone index in the PMX file"""
def __sortPoseBonesByBoneIndex(self, pose_bones: List[bpy.types.PoseBone], bone_names):
r: List[bpy.types.PoseBone] = []
for i in bone_names:
r.append(pose_bones[i])
return r
@staticmethod
def convertIKLimitAngles(min_angle: List[float], max_angle: List[float], bone_matrix: Matrix, invert: bool = False) -> Tuple[Vector, Vector]:
"""Convert IK limit angles to Blender's coordinate system"""
def convertIKLimitAngles(min_angle, max_angle, bone_matrix, invert=False):
mat = bone_matrix.to_3x3() * -1
mat[1], mat[2] = mat[2].copy(), mat[1].copy()
mat.transpose()
@@ -338,13 +325,25 @@ class PMXImporter:
new_min_angle[i], new_max_angle[i] = new_max_angle[i], new_min_angle[i]
return new_min_angle, new_max_angle
def __applyIk(self, index: int, pmx_bone: Any, pose_bones: List[bpy.types.PoseBone]) -> None:
"""Create an IK bone constraint
def __applyIk(self, index, pmx_bone, pose_bones):
"""create a IK bone constraint
If the IK bone and the target bone is separated, a dummy IK target bone is created as a child of the IK bone.
@param index the bone index
@param pmx_bone pmx.Bone
@param pose_bones the list of PoseBones sorted by the bone index
"""
# for tracking mmd ik target, simple explaination:
# + Root
# | + link1
# | + link0 (ik_constraint_bone) <- ik constraint, chain_count=2
# | + IK target (ik_target) <- constraint 'mmd_ik_target_override', subtarget=link0
# + IK bone (ik_bone)
#
# it is possible that the link0 is the IK target,
# so ik constraint will be on link1, chain_count=1
# the IK target isn't affected by IK bone
ik_bone = pose_bones[index]
ik_target = pose_bones[pmx_bone.target]
ik_constraint_bone = ik_target.parent
@@ -355,17 +354,16 @@ class PMXImporter:
if len(pmx_bone.ik_links) > 1:
ik_constraint_bone_real = pose_bones[pmx_bone.ik_links[1].target]
del pmx_bone.ik_links[0]
logger.warning("Fix IK settings of IK bone (%s)", ik_bone.name)
logging.warning(" * fix IK settings of IK bone (%s)", ik_bone.name)
is_valid_ik = ik_constraint_bone == ik_constraint_bone_real
if not is_valid_ik:
ik_constraint_bone = ik_constraint_bone_real
logger.warning("IK bone (%s) warning: IK target (%s) is not a child of IK link 0 (%s)",
ik_bone.name, ik_target.name, ik_constraint_bone.name)
logging.warning(" * IK bone (%s) warning: IK target (%s) is not a child of IK link 0 (%s)", ik_bone.name, ik_target.name, ik_constraint_bone.name)
elif any(pose_bones[i.target].parent != pose_bones[j.target] for i, j in zip(pmx_bone.ik_links, pmx_bone.ik_links[1:])):
logger.warning("Invalid IK bone (%s): IK chain does not follow parent-child relationship", ik_bone.name)
logging.warning(" * Invalid IK bone (%s): IK chain does not follow parent-child relationship", ik_bone.name)
return
if ik_constraint_bone is None or len(pmx_bone.ik_links) < 1:
logger.warning("Invalid IK bone (%s)", ik_bone.name)
logging.warning(" * Invalid IK bone (%s)", ik_bone.name)
return
c = ik_target.constraints.new(type="DAMPED_TRACK")
@@ -421,8 +419,7 @@ class PMXImporter:
c.use_limit_y = bone.ik_max_y != c.max_y or bone.ik_min_y != c.min_y
c.use_limit_z = bone.ik_max_z != c.max_z or bone.ik_min_z != c.min_z
def __importBones(self) -> None:
"""Import bones from the PMX model"""
def __importBones(self):
pmxModel = self.__model
boneNameTable, specialTipBones = self.__createEditBones(self.__armObj, pmxModel.bones)
@@ -476,8 +473,7 @@ class PMXImporter:
b_bone.lock_location = [True, True, True]
b_bone.lock_scale = [True, True, True]
def __importRigids(self) -> None:
"""Import rigid bodies from the PMX model"""
def __importRigids(self):
start_time = time.time()
self.__rigidTable = {}
context = FnContext.ensure_context()
@@ -509,10 +505,9 @@ class PMXImporter:
MoveObject.set_index(obj, i)
self.__rigidTable[i] = obj
logger.debug("Finished importing rigid bodies in %.2f seconds", time.time() - start_time)
logging.debug("Finished importing rigid bodies in %f seconds.", time.time() - start_time)
def __importJoints(self) -> None:
"""Import joints from the PMX model"""
def __importJoints(self):
start_time = time.time()
context = FnContext.ensure_context()
joint_pool = FnRigidBody.new_joint_objects(context, FnModel.ensure_joint_group_object(context, self.__rig.rootObject()), len(self.__model.joints), FnModel.get_empty_display_size(self.__rig.rootObject()))
@@ -538,10 +533,9 @@ class PMXImporter:
obj.hide_set(True)
MoveObject.set_index(obj, i)
logger.debug("Finished importing joints in %.2f seconds", time.time() - start_time)
logging.debug("Finished importing joints in %f seconds.", time.time() - start_time)
def __importMaterials(self) -> None:
"""Import materials from the PMX model"""
def __importMaterials(self):
self.__importTextures()
pmxModel = self.__model
@@ -594,8 +588,7 @@ class PMXImporter:
texture_slot.uv_layer = "UV1" # for SubTexture
mmd_mat.sphere_texture_type = str(i.sphere_texture_mode)
def __importFaces(self) -> None:
"""Import faces/polygons from the PMX model"""
def __importFaces(self):
pmxModel = self.__model
mesh = self.__meshObj.data
vertex_map = self.__vertex_map
@@ -624,42 +617,38 @@ class PMXImporter:
bf.image = self.__imageTable.get(mi, None)
if pmxModel.header and pmxModel.header.additional_uvs:
logger.info("Importing %d additional uvs", pmxModel.header.additional_uvs)
logging.info("Importing %d additional uvs", pmxModel.header.additional_uvs)
zw_data_map = collections.OrderedDict()
split_uvzw = lambda uvi: (self.flipUV_V(uvi[:2]), uvi[2:])
for i in range(pmxModel.header.additional_uvs):
add_uv = uv_layers[uv_textures.new(name="UV" + str(i + 1)).name]
logger.info(" - %s...(uv channels)", add_uv.name)
logging.info(" - %s...(uv channels)", add_uv.name)
uv_table = {vi: split_uvzw(v.additional_uvs[i]) for vi, v in enumerate(pmxModel.vertices)}
add_uv.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i][0]))
if not any(any(s[1]) for s in uv_table.values()):
logger.info("\t- zw are all zeros: %s", add_uv.name)
logging.info("\t- zw are all zeros: %s", add_uv.name)
else:
zw_data_map["_" + add_uv.name] = {k: self.flipUV_V(v[1]) for k, v in uv_table.items()}
for name, zw_table in zw_data_map.items():
logger.info(" - %s...(zw channels of %s)", name, name[1:])
logging.info(" - %s...(zw channels of %s)", name, name[1:])
add_zw = uv_textures.new(name=name)
if add_zw is None:
logger.warning("\t* Lost zw channels")
logging.warning("\t* Lost zw channels")
continue
add_zw = uv_layers[add_zw.name]
add_zw.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in zw_table[i]))
self.__fixOverlappingFaceMaterials(mesh.materials, mesh.vertices, loop_indices, material_indices)
def __fixOverlappingFaceMaterials(self, materials: List[bpy.types.Material],
vertices: List[bpy.types.MeshVertex],
loop_indices: List[int],
material_indices: List[int]) -> None:
"""Fix overlapping face materials to prevent z-fighting"""
# FIXME: This is not the best way to setup blend_method, might just work for some common cases.
def __fixOverlappingFaceMaterials(self, materials, vertices, loop_indices, material_indices):
# FIXME: This is not the best way to setup blend_method, might just work for some common cases. And FnMaterial.update_alpha() is still using 'HASHED'.
# For EEVEE, basically users should know which blend_method is best for each material of their models.
# For Cycles, users have to offset or delete those z-fighting faces to fix it manually.
check = {}
mi_skip = -1
_vi_cache = {}
def _rounded_co_vi(vi: int) -> Tuple[float, float, float]:
def _rounded_co_vi(vi):
if vi not in _vi_cache:
vco = vertices[vi].co
_vi_cache[vi] = (round(vco[0], 6), round(vco[1], 6), round(vco[2], 6))
@@ -674,13 +663,12 @@ class PMXImporter:
if verts not in check:
check[verts] = mi
elif check[verts] < mi:
logger.debug("Fix blend method of material: %s", materials[mi].name)
logging.debug(" >> fix blend method of material: %s", materials[mi].name)
materials[mi].blend_method = "BLEND"
materials[mi].show_transparent_back = False
mi_skip = mi
def __importVertexMorphs(self) -> None:
"""Import vertex morphs from the PMX model"""
def __importVertexMorphs(self):
mmd_root = self.__root.mmd_root
categories = self.CATEGORIES
self.__createBasisShapeKey()
@@ -694,8 +682,7 @@ class PMXImporter:
shapeKeyPoint = shapeKey.data[md.index]
shapeKeyPoint.co += Vector(md.offset).xzy * self.__scale
def __importMaterialMorphs(self) -> None:
"""Import material morphs from the PMX model"""
def __importMaterialMorphs(self):
mmd_root = self.__root.mmd_root
categories = self.CATEGORIES
for morph in (x for x in self.__model.morphs if isinstance(x, pmx.MaterialMorph)):
@@ -719,8 +706,7 @@ class PMXImporter:
data.sphere_texture_factor = morph_data.sphere_texture_factor
data.toon_texture_factor = morph_data.toon_texture_factor
def __importBoneMorphs(self) -> None:
"""Import bone morphs from the PMX model"""
def __importBoneMorphs(self):
mmd_root = self.__root.mmd_root
categories = self.CATEGORIES
for morph in (x for x in self.__model.morphs if isinstance(x, pmx.BoneMorph)):
@@ -738,8 +724,7 @@ class PMXImporter:
data.location = converter.convert_location(morph_data.location_offset)
data.rotation = converter.convert_rotation(morph_data.rotation_offset)
def __importUVMorphs(self) -> None:
"""Import UV morphs from the PMX model"""
def __importUVMorphs(self):
mmd_root = self.__root.mmd_root
categories = self.CATEGORIES
__OffsetData = collections.namedtuple("OffsetData", "index, offset")
@@ -755,8 +740,7 @@ class PMXImporter:
FnMorph.store_uv_morph_data(self.__meshObj, uv_morph, offsets, "")
uv_morph.data_type = "VERTEX_GROUP"
def __importGroupMorphs(self) -> None:
"""Import group morphs from the PMX model"""
def __importGroupMorphs(self):
mmd_root = self.__root.mmd_root
categories = self.CATEGORIES
morph_types = self.MORPH_TYPES
@@ -775,8 +759,7 @@ class PMXImporter:
data.morph_type = morph_types[m.type_index()]
data.factor = morph_data.factor
def __importDisplayFrames(self) -> None:
"""Import display frames from the PMX model"""
def __importDisplayFrames(self):
pmxModel = self.__model
root = self.__root
morph_types = self.MORPH_TYPES
@@ -801,18 +784,17 @@ class PMXImporter:
FnBone.sync_bone_collections_from_display_item_frames(self.__armObj)
def __addArmatureModifier(self, meshObj: Object, armObj: Object) -> None:
"""Add armature modifier to mesh object"""
def __addArmatureModifier(self, meshObj, armObj):
# TODO: move to model.py
armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE")
armModifier.object = armObj
armModifier.use_vertex_groups = True
armModifier.name = "mmd_bone_order_override"
armModifier.show_render = armModifier.show_viewport = len(meshObj.data.vertices) > 0
def __assignCustomNormals(self) -> None:
"""Assign custom normals to the mesh"""
def __assignCustomNormals(self):
mesh: bpy.types.Mesh = self.__meshObj.data
logger.info("Setting custom normals...")
logging.info("Setting custom normals...")
if self.__vertex_map:
verts, faces = self.__model.vertices, self.__model.faces
custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f]
@@ -820,29 +802,26 @@ class PMXImporter:
else:
custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices]
mesh.normals_split_custom_set_from_vertices(custom_normals)
logger.info("Custom normals applied successfully")
logging.info(" - Done!!")
def __renameLRBones(self, use_underscore: bool) -> None:
"""Rename left/right bones with proper naming convention"""
def __renameLRBones(self, use_underscore):
pose_bones = self.__armObj.pose.bones
for i in pose_bones:
self.__rig.renameBone(i.name, utils.convertNameToLR(i.name, use_underscore))
# self.__meshObj.vertex_groups[i.mmd_bone.name_j].name = i.name
def __translateBoneNames(self) -> None:
"""Translate bone names using the provided translator"""
def __translateBoneNames(self):
pose_bones = self.__armObj.pose.bones
for i in pose_bones:
self.__rig.renameBone(i.name, self.__translator.translate(i.name))
def __fixRepeatedMorphName(self) -> None:
"""Fix repeated morph names to ensure uniqueness"""
def __fixRepeatedMorphName(self):
used_names = set()
for m in self.__model.morphs:
m.name = utils.unique_name(m.name or "Morph", used_names)
used_names.add(m.name)
def execute(self, context: Context, **args) -> None:
"""Execute the PMX import process with the given arguments"""
def execute(self, **args):
if "pmx" in args:
self.__model = args["pmx"]
else:
@@ -860,95 +839,78 @@ class PMXImporter:
self.__apply_bone_fixed_axis = args.get("apply_bone_fixed_axis", False)
self.__translator = args.get("translator", None)
logger.info("****************************************")
logger.info("Starting PMX import process")
logger.info("----------------------------------------")
logging.info("****************************************")
logging.info(" mmd_tools.import_pmx module")
logging.info("----------------------------------------")
logging.info(" Start to load model data form a pmx file")
logging.info(" by the mmd_tools.pmx modlue.")
logging.info("")
start_time = time.time()
with ProgressTracker(context, 100, "Importing PMX Model") as progress:
self.__createObjects()
progress.step("Created base objects")
self.__createObjects()
if "MESH" in types:
if clean_model:
_PMXCleaner.clean(self.__model, "MORPHS" not in types)
if remove_doubles:
self.__vertex_map = _PMXCleaner.remove_doubles(self.__model, "MORPHS" not in types)
progress.step("Preparing mesh data")
if "MESH" in types:
if clean_model:
_PMXCleaner.clean(self.__model, "MORPHS" not in types)
if remove_doubles:
self.__vertex_map = _PMXCleaner.remove_doubles(self.__model, "MORPHS" not in types)
self.__createMeshObject()
self.__importVertices()
self.__importMaterials()
self.__importFaces()
self.__meshObj.data.update()
self.__assignCustomNormals()
self.__storeVerticesSDEF()
if "ARMATURE" in types:
# for tracking bone order
if "MESH" not in types:
self.__createMeshObject()
progress.step("Importing vertices")
self.__importVertices()
progress.step("Importing materials")
self.__importMaterials()
progress.step("Importing faces")
self.__importFaces()
self.__meshObj.data.update()
progress.step("Assigning custom normals")
self.__assignCustomNormals()
progress.step("Processing SDEF vertices")
self.__storeVerticesSDEF()
self.__importVertexGroup()
self.__importBones()
if args.get("rename_LR_bones", False):
use_underscore = args.get("use_underscore", False)
self.__renameLRBones(use_underscore)
if self.__translator:
self.__translateBoneNames()
if self.__apply_bone_fixed_axis:
FnBone.apply_bone_fixed_axis(self.__armObj)
FnBone.apply_additional_transformation(self.__armObj)
if "ARMATURE" in types:
progress.step("Preparing armature")
# for tracking bone order
if "MESH" not in types:
self.__createMeshObject()
self.__importVertexGroup()
progress.step("Importing bones")
self.__importBones()
if args.get("rename_LR_bones", False):
use_underscore = args.get("use_underscore", False)
self.__renameLRBones(use_underscore)
if self.__translator:
self.__translateBoneNames()
if self.__apply_bone_fixed_axis:
FnBone.apply_bone_fixed_axis(self.__armObj)
FnBone.apply_additional_transformation(self.__armObj)
if "PHYSICS" in types:
self.__importRigids()
self.__importJoints()
if "PHYSICS" in types:
progress.step("Importing rigid bodies")
self.__importRigids()
progress.step("Importing joints")
self.__importJoints()
if "DISPLAY" in types:
self.__importDisplayFrames()
else:
self.__rig.initialDisplayFrames()
if "DISPLAY" in types:
progress.step("Importing display frames")
self.__importDisplayFrames()
else:
self.__rig.initialDisplayFrames()
if "MORPHS" in types:
self.__importGroupMorphs()
self.__importVertexMorphs()
self.__importBoneMorphs()
self.__importMaterialMorphs()
self.__importUVMorphs()
if "MORPHS" in types:
progress.step("Importing group morphs")
self.__importGroupMorphs()
progress.step("Importing vertex morphs")
self.__importVertexMorphs()
progress.step("Importing bone morphs")
self.__importBoneMorphs()
progress.step("Importing material morphs")
self.__importMaterialMorphs()
progress.step("Importing UV morphs")
self.__importUVMorphs()
if self.__meshObj:
self.__addArmatureModifier(self.__meshObj, self.__armObj)
if self.__meshObj:
progress.step("Adding armature modifier")
self.__addArmatureModifier(self.__meshObj, self.__armObj)
FnModel.change_mmd_ik_loop_factor(self.__root, args.get("ik_loop_factor", 1))
# bpy.context.scene.gravity[2] = -9.81 * 10 * self.__scale
utils.selectAObject(self.__root)
FnModel.change_mmd_ik_loop_factor(self.__root, args.get("ik_loop_factor", 1))
utils.selectAObject(self.__root)
logger.info("Finished importing the model in %.2f seconds", time.time() - start_time)
logger.info("----------------------------------------")
logging.info(" Finished importing the model in %f seconds.", time.time() - start_time)
logging.info("----------------------------------------")
logging.info(" mmd_tools.import_pmx module")
logging.info("****************************************")
class _PMXCleaner:
"""Helper class for cleaning PMX data during import"""
@classmethod
def clean(cls, pmx_model: Any, mesh_only: bool) -> None:
"""Clean PMX data by removing unused vertices and faces"""
logger.info("Cleaning PMX data...")
def clean(cls, pmx_model, mesh_only):
logging.info("Cleaning PMX data...")
pmx_faces = pmx_model.faces
pmx_vertices = pmx_model.vertices
@@ -958,7 +920,7 @@ class _PMXCleaner:
index_map = {v: v for f in pmx_faces for v in f}
is_index_clean = len(index_map) == len(pmx_vertices)
if is_index_clean:
logger.info("Vertices are clean, no cleaning needed")
logging.info(" (vertices is clean)")
else:
new_vertex_count = 0
for v in sorted(index_map):
@@ -966,7 +928,7 @@ class _PMXCleaner:
pmx_vertices[new_vertex_count] = pmx_vertices[v]
index_map[v] = new_vertex_count
new_vertex_count += 1
logger.warning("Removed %d unused vertices", len(pmx_vertices) - new_vertex_count)
logging.warning(" - removed %d vertices", len(pmx_vertices) - new_vertex_count)
del pmx_vertices[new_vertex_count:]
# update vertex indices of faces
@@ -974,7 +936,7 @@ class _PMXCleaner:
f[:] = [index_map[v] for v in f]
if mesh_only:
logger.info("Mesh-only cleaning completed")
logging.info(" - Done (mesh only)!!")
return
if not is_index_clean:
@@ -984,12 +946,11 @@ class _PMXCleaner:
return x.index is not None
cls.__clean_pmx_morphs(pmx_model.morphs, __update_index)
logger.info("PMX cleaning completed")
logging.info(" - Done!!")
@classmethod
def remove_doubles(cls, pmx_model: Any, mesh_only: bool) -> Optional[Dict[int, Tuple[int, int]]]:
"""Remove duplicate vertices from the PMX model"""
logger.info("Removing duplicate vertices...")
def remove_doubles(cls, pmx_model, mesh_only):
logging.info("Removing doubles...")
pmx_vertices = pmx_model.vertices
vertex_map = [None] * len(pmx_vertices)
@@ -1013,17 +974,18 @@ class _PMXCleaner:
counts = len(vertex_map) - len(keys)
keys.clear()
if counts:
logger.warning("%d duplicate vertices will be removed", counts)
logging.warning(" - %d vertices will be removed", counts)
else:
logger.info("No duplicate vertices found")
logging.info(" - Done (no changes)!!")
return None
# clean face
# face_key_func = lambda f: frozenset(vertex_map[x][0] for x in f)
face_key_func = lambda f: frozenset({vertex_map[x][0]: tuple(pmx_vertices[x].uv) for x in f}.items())
cls.__clean_pmx_faces(pmx_model.faces, pmx_model.materials, face_key_func)
if mesh_only:
logger.info("Mesh-only duplicate removal completed")
logging.info(" - Done (mesh only)!!")
else:
# clean vertex/uv morphs
def __update_index(x):
@@ -1032,12 +994,11 @@ class _PMXCleaner:
return x.index is not None
cls.__clean_pmx_morphs(pmx_model.morphs, __update_index)
logger.info("Duplicate removal completed")
logging.info(" - Done!!")
return vertex_map
@staticmethod
def __clean_pmx_faces(pmx_faces: List[List[int]], pmx_materials: List[Any], face_key_func: Callable) -> None:
"""Clean PMX faces by removing duplicates and updating material vertex counts"""
def __clean_pmx_faces(pmx_faces, pmx_materials, face_key_func):
new_face_count = 0
face_iter = iter(pmx_faces)
for mat in pmx_materials:
@@ -1057,14 +1018,13 @@ class _PMXCleaner:
mat.vertex_count = new_vertex_count
face_iter = None
if new_face_count == len(pmx_faces):
logger.info("Faces are clean, no cleaning needed")
logging.info(" (faces is clean)")
else:
logger.warning("Removed %d duplicate faces", len(pmx_faces) - new_face_count)
logging.warning(" - removed %d faces", len(pmx_faces) - new_face_count)
del pmx_faces[new_face_count:]
@staticmethod
def __clean_pmx_morphs(pmx_morphs: List[Any], index_update_func: Callable) -> None:
"""Clean PMX morphs by updating vertex indices and removing invalid offsets"""
def __clean_pmx_morphs(pmx_morphs, index_update_func):
for m in pmx_morphs:
if not isinstance(m, pmx.VertexMorph) and not isinstance(m, pmx.UVMorph):
continue
@@ -1072,4 +1032,4 @@ class _PMXCleaner:
m.offsets = [x for x in m.offsets if index_update_func(x)]
counts = old_len - len(m.offsets)
if counts:
logger.warning('Removed %d (of %d) offsets from morph "%s"', counts, old_len, m.name)
logging.warning(' - removed %d (of %d) offsets of "%s"', counts, old_len, m.name)
+290
View File
@@ -0,0 +1,290 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import List, Optional
import bpy
from mathutils import Euler, Vector
from ..bpyutils import FnContext, Props
SHAPE_SPHERE = 0
SHAPE_BOX = 1
SHAPE_CAPSULE = 2
MODE_STATIC = 0
MODE_DYNAMIC = 1
MODE_DYNAMIC_BONE = 2
def shapeType(collision_shape):
return ("SPHERE", "BOX", "CAPSULE").index(collision_shape)
def collisionShape(shape_type):
return ("SPHERE", "BOX", "CAPSULE")[shape_type]
def setRigidBodyWorldEnabled(enable):
if bpy.ops.rigidbody.world_add.poll():
bpy.ops.rigidbody.world_add()
rigidbody_world = bpy.context.scene.rigidbody_world
enabled = rigidbody_world.enabled
rigidbody_world.enabled = enable
return enabled
class RigidBodyMaterial:
COLORS = [
0x7FDDD4,
0xF0E68C,
0xEE82EE,
0xFFE4E1,
0x8FEEEE,
0xADFF2F,
0xFA8072,
0x9370DB,
0x40E0D0,
0x96514D,
0x5A964E,
0xE6BFAB,
0xD3381C,
0x165E83,
0x701682,
0x828216,
]
@classmethod
def getMaterial(cls, number):
number = int(number)
material_name = "mmd_tools_rigid_%d" % (number)
if material_name not in bpy.data.materials:
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)]
mat.specular_intensity = 0
if len(mat.diffuse_color) > 3:
mat.diffuse_color[3] = 0.5
mat.blend_method = "BLEND"
if hasattr(mat, "shadow_method"):
mat.shadow_method = "NONE"
mat.use_backface_culling = True
mat.show_transparent_back = False
mat.use_nodes = True
nodes, links = mat.node_tree.nodes, mat.node_tree.links
nodes.clear()
node_color = nodes.new("ShaderNodeBackground")
node_color.inputs["Color"].default_value = mat.diffuse_color
node_output = nodes.new("ShaderNodeOutputMaterial")
links.new(node_color.outputs[0], node_output.inputs["Surface"])
else:
mat = bpy.data.materials[material_name]
return mat
class FnRigidBody:
@staticmethod
def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]:
if count < 1:
return []
obj = FnRigidBody.new_rigid_body_object(context, parent_object)
if count == 1:
return [obj]
return FnContext.duplicate_object(context, obj, count)
@staticmethod
def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object:
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"
obj.rotation_mode = "YXZ"
setattr(obj, Props.display_type, "SOLID")
obj.show_transparent = True
obj.hide_render = True
obj.display.show_shadows = False
with context.temp_override(object=obj):
bpy.ops.rigidbody.object_add(type="ACTIVE")
return obj
@staticmethod
def setup_rigid_body_object(
obj: bpy.types.Object,
shape_type: str,
location: Vector,
rotation: Euler,
size: Vector,
dynamics_type: str,
collision_group_number: Optional[int] = None,
collision_group_mask: Optional[List[bool]] = None,
name: Optional[str] = None,
name_e: Optional[str] = None,
bone: Optional[str] = None,
friction: Optional[float] = None,
mass: Optional[float] = None,
angular_damping: Optional[float] = None,
linear_damping: Optional[float] = None,
bounce: Optional[float] = None,
) -> bpy.types.Object:
obj.location = location
obj.rotation_euler = rotation
obj.mmd_rigid.shape = collisionShape(shape_type)
obj.mmd_rigid.size = size
obj.mmd_rigid.type = str(dynamics_type) if dynamics_type in range(3) else "1"
if collision_group_number is not None:
obj.mmd_rigid.collision_group_number = collision_group_number
if collision_group_mask is not None:
obj.mmd_rigid.collision_group_mask = collision_group_mask
if name is not None:
obj.name = name
obj.mmd_rigid.name_j = name
obj.data.name = name
if name_e is not None:
obj.mmd_rigid.name_e = name_e
if bone is not None:
obj.mmd_rigid.bone = bone
else:
obj.mmd_rigid.bone = ""
rb = obj.rigid_body
if friction is not None:
rb.friction = friction
if mass is not None:
rb.mass = mass
if angular_damping is not None:
rb.angular_damping = angular_damping
if linear_damping is not None:
rb.linear_damping = linear_damping
if bounce is not None:
rb.restitution = bounce
return obj
@staticmethod
def get_rigid_body_size(obj: bpy.types.Object):
assert obj.mmd_type == "RIGID_BODY"
x0, y0, z0 = obj.bound_box[0]
x1, y1, z1 = obj.bound_box[6]
assert x1 >= x0 and y1 >= y0 and z1 >= z0
shape = obj.mmd_rigid.shape
if shape == "SPHERE":
radius = (z1 - z0) / 2
return (radius, 0.0, 0.0)
elif shape == "BOX":
x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2
return (x, y, z)
elif shape == "CAPSULE":
diameter = x1 - x0
radius = diameter / 2
height = abs((z1 - z0) - diameter)
return (radius, height, 0.0)
else:
raise ValueError(f"Invalid shape type: {shape}")
@staticmethod
def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object:
obj = FnContext.new_and_link_object(context, name="Joint", object_data=None)
obj.parent = parent_object
obj.mmd_type = "JOINT"
obj.rotation_mode = "YXZ"
setattr(obj, Props.empty_display_type, "ARROWS")
setattr(obj, Props.empty_display_size, 0.1 * empty_display_size)
obj.hide_render = True
with context.temp_override():
context.view_layer.objects.active = obj
bpy.ops.rigidbody.constraint_add(type="GENERIC_SPRING")
rigid_body_constraint = obj.rigid_body_constraint
rigid_body_constraint.disable_collisions = False
rigid_body_constraint.use_limit_ang_x = True
rigid_body_constraint.use_limit_ang_y = True
rigid_body_constraint.use_limit_ang_z = True
rigid_body_constraint.use_limit_lin_x = True
rigid_body_constraint.use_limit_lin_y = True
rigid_body_constraint.use_limit_lin_z = True
rigid_body_constraint.use_spring_x = True
rigid_body_constraint.use_spring_y = True
rigid_body_constraint.use_spring_z = True
rigid_body_constraint.use_spring_ang_x = True
rigid_body_constraint.use_spring_ang_y = True
rigid_body_constraint.use_spring_ang_z = True
return obj
@staticmethod
def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]:
if count < 1:
return []
obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size)
if count == 1:
return [obj]
return FnContext.duplicate_object(context, obj, count)
@staticmethod
def setup_joint_object(
obj: bpy.types.Object,
location: Vector,
rotation: Euler,
rigid_a: bpy.types.Object,
rigid_b: bpy.types.Object,
maximum_location: Vector,
minimum_location: Vector,
maximum_rotation: Euler,
minimum_rotation: Euler,
spring_angular: Vector,
spring_linear: Vector,
name: str,
name_e: Optional[str] = None,
) -> bpy.types.Object:
obj.name = f"J.{name}"
obj.location = location
obj.rotation_euler = rotation
rigid_body_constraint = obj.rigid_body_constraint
rigid_body_constraint.object1 = rigid_a
rigid_body_constraint.object2 = rigid_b
rigid_body_constraint.limit_lin_x_upper = maximum_location.x
rigid_body_constraint.limit_lin_y_upper = maximum_location.y
rigid_body_constraint.limit_lin_z_upper = maximum_location.z
rigid_body_constraint.limit_lin_x_lower = minimum_location.x
rigid_body_constraint.limit_lin_y_lower = minimum_location.y
rigid_body_constraint.limit_lin_z_lower = minimum_location.z
rigid_body_constraint.limit_ang_x_upper = maximum_rotation.x
rigid_body_constraint.limit_ang_y_upper = maximum_rotation.y
rigid_body_constraint.limit_ang_z_upper = maximum_rotation.z
rigid_body_constraint.limit_ang_x_lower = minimum_rotation.x
rigid_body_constraint.limit_ang_y_lower = minimum_rotation.y
rigid_body_constraint.limit_ang_z_lower = minimum_rotation.z
obj.mmd_joint.name_j = name
if name_e is not None:
obj.mmd_joint.name_e = name_e
obj.mmd_joint.spring_linear = spring_linear
obj.mmd_joint.spring_angular = spring_angular
return obj
+334
View File
@@ -0,0 +1,334 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
import time
import bpy
from mathutils import Matrix, Vector
from ..bpyutils import FnObject
def _hash(v):
if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)):
return hash(type(v).__name__ + v.name)
elif isinstance(v, bpy.types.Pose):
return hash(type(v).__name__ + v.id_data.name)
else:
raise NotImplementedError("hash")
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"
def __init__(self):
raise NotImplementedError("not allowed")
@classmethod
def __init_cache(cls, obj, shapekey):
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:
cls.g_verts[key] = cls.__find_vertices(obj)
cls.g_bone_check[key] = {}
cls.__g_armature_check[key] = key_armature
cls.g_shapekey_data[key] = None
return True
return False
@classmethod
def __check_bone_update(cls, obj, bone0, bone1):
check = cls.g_bone_check[_hash(obj)]
key = (_hash(bone0), _hash(bone1))
if key not in check or (bone0.matrix, bone1.matrix) != check[key]:
check[key] = (bone0.matrix.copy(), bone1.matrix.copy())
return True
return False
@classmethod
def mute_sdef_set(cls, obj, mute):
key_blocks = getattr(obj.data.shape_keys, "key_blocks", ())
if cls.SHAPEKEY_NAME in key_blocks:
shapekey = key_blocks[cls.SHAPEKEY_NAME]
shapekey.mute = mute
if cls.has_sdef_data(obj):
cls.__init_cache(obj, shapekey)
cls.__sdef_muted(obj, shapekey)
@classmethod
def __sdef_muted(cls, obj, shapekey):
mute = shapekey.mute
if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"):
mod = obj.modifiers.get("mmd_bone_order_override")
if mod and mod.type == "ARMATURE":
if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != "EDIT":
mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3])
obj.vertex_groups.new(name=cls.MASK_NAME).add(mask, 1, "REPLACE")
mod.vertex_group = "" if mute else cls.MASK_NAME
mod.invert_vertex_group = True
shapekey.vertex_group = cls.MASK_NAME
cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute
return mute
@staticmethod
def has_sdef_data(obj):
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)
return kb and "mmd_sdef_c" in kb and "mmd_sdef_r0" in kb and "mmd_sdef_r1" in kb
return False
@classmethod
def __find_vertices(cls, obj):
if not cls.has_sdef_data(obj):
return {}
vertices = {}
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}
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
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
if len(bgs) >= 2:
bgs.sort(key=lambda x: x.group)
# preprocessing
w0, w1 = bgs[0].weight, bgs[1].weight
# w0 + w1 == 1
w0 = w0 / (w0 + w1)
w1 = 1 - w0
c, r0, r1 = sdef_c[i].co, sdef_r0[i].co, sdef_r1[i].co
rw = r0 * w0 + r1 * w1
r0 = c + r0 - rw
r1 = c + r1 - rw
key = (bgs[0].group, bgs[1].group)
if key not in vertices:
# TODO basically we can not cache any bone reference
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)
return vertices
@classmethod
def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale):
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):
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
# cls.driver_function(shapekey.id_data.original.key_blocks[shapekey.name], obj_name, bulk_update, use_skip, use_scale) # update original data
data_path = shapekey.path_from_id("value")
obj = next(i for i in shapekey.id_data.animation_data.drivers if i.data_path == data_path).driver.variables["obj"].targets[0].id
cls.__init_cache(obj, shapekey)
if cls.__sdef_muted(obj, shapekey):
return 0.0
pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones
if not bulk_update:
shapekey_data = shapekey.data
if use_scale:
# with scale
key_blocks = tuple(k for k in shapekey.id_data.key_blocks[1:] if not k.mute and k.value and k.name != cls.SHAPEKEY_NAME)
for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values():
bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name]
# if use_skip and not cls.__check_bone_update(obj, bone0, bone1):
# continue
mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted()
mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted()
rot0 = mat0.to_euler("YXZ").to_quaternion()
rot1 = mat1.to_euler("YXZ").to_quaternion()
if rot1.dot(rot0) < 0:
rot1 = -rot1
s0, s1 = mat0.to_scale(), mat1.to_scale()
for vid, w0, w1, pos_c, cr0, cr1 in sdef_data:
s = s0 * w0 + s1 * w1
mat_rot = (rot0 * w0 + rot1 * w1).normalized().to_matrix() @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])])
delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = ''
shapekey_data[vid].co = (mat_rot @ (pos_c + delta)) - delta + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1
else:
# default
for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values():
bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name]
if use_skip and not cls.__check_bone_update(obj, bone0, bone1):
continue
mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted()
mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted()
# workaround some weird result of matrix.to_quaternion() using to_euler(), but still minor issues
rot0 = mat0.to_euler("YXZ").to_quaternion()
rot1 = mat1.to_euler("YXZ").to_quaternion()
if rot1.dot(rot0) < 0:
rot1 = -rot1
for vid, w0, w1, pos_c, cr0, cr1 in sdef_data:
mat_rot = (rot0 * w0 + rot1 * w1).normalized().to_matrix()
shapekey_data[vid].co = (mat_rot @ pos_c) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1
else: # bulk update
shapekey_data = cls.g_shapekey_data[_hash(obj)]
if shapekey_data is None:
import numpy as np
shapekey_data = np.zeros(len(shapekey.data) * 3, dtype=np.float32)
shapekey.data.foreach_get("co", shapekey_data)
shapekey_data = cls.g_shapekey_data[_hash(obj)] = shapekey_data.reshape(len(shapekey.data), 3)
if use_scale:
# scale & bulk update
key_blocks = tuple(k for k in shapekey.id_data.key_blocks[1:] if not k.mute and k.value and k.name != cls.SHAPEKEY_NAME)
for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values():
bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name]
# if use_skip and not cls.__check_bone_update(obj, bone0, bone1):
# continue
mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted()
mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted()
rot0 = mat0.to_euler("YXZ").to_quaternion()
rot1 = mat1.to_euler("YXZ").to_quaternion()
if rot1.dot(rot0) < 0:
rot1 = -rot1
s0, s1 = mat0.to_scale(), mat1.to_scale()
def scale(mat_rot, w0, w1):
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):
delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = ''
return (mat_rot @ (pos_c + delta)) - delta
shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data]
else:
# bulk update
for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values():
bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name]
if use_skip and not cls.__check_bone_update(obj, bone0, bone1):
continue
mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted()
mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted()
rot0 = mat0.to_euler("YXZ").to_quaternion()
rot1 = mat1.to_euler("YXZ").to_quaternion()
if rot1.dot(rot0) < 0:
rot1 = -rot1
shapekey_data[vids] = [((rot0 * w0 + rot1 * w1).normalized().to_matrix() @ pos_c) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data]
shapekey.data.foreach_set("co", shapekey_data.reshape(3 * len(shapekey.data)))
return 1.0 # shapkey value
@classmethod
def register_driver_function(cls):
if "mmd_sdef_driver" not in bpy.app.driver_namespace:
bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function
if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace:
bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap
BENCH_LOOP = 10
@classmethod
def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip):
# warmed up
cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale)
cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale)
# benchmark
t = time.time()
for i in range(cls.BENCH_LOOP):
cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale)
default_time = time.time() - t
t = time.time()
for i in range(cls.BENCH_LOOP):
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)
return result
@classmethod
def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False):
# Unbind first
cls.unbind(obj)
if not cls.has_sdef_data(obj):
return False
# Create the shapekey for the driver
shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False)
cls.__init_cache(obj, shapekey)
cls.__sdef_muted(obj, shapekey)
cls.register_driver_function()
if bulk_update is None:
bulk_update = cls.__get_benchmark_result(obj, shapekey, use_scale, use_skip)
# Add the driver to the shapekey
f = obj.data.shape_keys.driver_add('key_blocks["' + cls.SHAPEKEY_NAME + '"].value', -1)
if hasattr(f.driver, "show_debug_info"):
f.driver.show_debug_info = False
f.driver.type = "SCRIPTED"
ov = f.driver.variables.new()
ov.name = "obj"
ov.type = "SINGLE_PROP"
ov.targets[0].id = obj
ov.targets[0].data_path = "name"
if not bulk_update and use_skip: # FIXME: force disable use_skip=True for bulk_update=False on 2.8
use_skip = False
mod = obj.modifiers.get("mmd_bone_order_override")
variables = f.driver.variables
for name in set(data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)): # add required bones for dependency graph
var = variables.new()
var.type = "TRANSFORMS"
var.targets[0].id = mod.object
var.targets[0].bone_target = name
f.driver.use_self = True
param = (bulk_update, use_skip, use_scale)
f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param)
return True
@classmethod
def unbind(cls, obj):
if obj.data.shape_keys:
if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks:
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:
mod.vertex_group = ""
mod.invert_vertex_group = False
break
if cls.MASK_NAME in obj.vertex_groups:
obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME])
cls.clear_cache(obj)
@classmethod
def clear_cache(cls, obj=None, unused_only=False):
if unused_only:
valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj)
for key in cls.g_verts.keys() - valid_keys:
del cls.g_verts[key]
for key in cls.g_shapekey_data.keys() - cls.g_verts.keys():
del cls.g_shapekey_data[key]
for key in cls.g_bone_check.keys() - cls.g_verts.keys():
del cls.g_bone_check[key]
elif obj:
key = _hash(obj)
if key in cls.g_verts:
del cls.g_verts[key]
if key in cls.g_shapekey_data:
del cls.g_shapekey_data[key]
if key in cls.g_bone_check:
del cls.g_bone_check[key]
else:
cls.g_verts = {}
cls.g_bone_check = {}
cls.g_shapekey_data = {}
+346
View File
@@ -0,0 +1,346 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, Tuple, cast
import bpy
class _NodeTreeUtils:
def __init__(self, shader: bpy.types.ShaderNodeTree):
self.shader = shader
self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore
self.links = shader.links
def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]:
return next((n for n in self.nodes if n.bl_idname == node_type), None)
def new_node(self, idname: str, pos: Tuple[int, int]) -> bpy.types.ShaderNode:
node: bpy.types.ShaderNode = self.nodes.new(idname)
node.location = (pos[0] * 210, pos[1] * 220)
return node
def new_math_node(self, operation, pos, value1=None, value2=None):
node = self.new_node("ShaderNodeMath", pos)
node.operation = operation
if value1 is not None:
node.inputs[0].default_value = value1
if value2 is not None:
node.inputs[1].default_value = value2
return node
def new_vector_math_node(self, operation, pos, vector1=None, vector2=None):
node = self.new_node("ShaderNodeVectorMath", pos)
node.operation = operation
if vector1 is not None:
node.inputs[0].default_value = vector1
if vector2 is not None:
node.inputs[1].default_value = vector2
return node
def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None):
node = self.new_node("ShaderNodeMixRGB", pos)
node.blend_type = blend_type
if fac is not None:
node.inputs["Fac"].default_value = fac
if color1 is not None:
node.inputs["Color1"].default_value = color1
if color2 is not None:
node.inputs["Color2"].default_value = color2
return node
SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"}
SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"}
class _NodeGroupUtils(_NodeTreeUtils):
def __init__(self, shader: bpy.types.ShaderNodeTree):
super().__init__(shader)
self.__node_input: Optional[bpy.types.NodeGroupInput] = None
self.__node_output: Optional[bpy.types.NodeGroupOutput] = None
@property
def node_input(self) -> bpy.types.NodeGroupInput:
if not self.__node_input:
self.__node_input = cast(bpy.types.NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0)))
return self.__node_input
@property
def node_output(self) -> bpy.types.NodeGroupOutput:
if not self.__node_output:
self.__node_output = cast(bpy.types.NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0)))
return self.__node_output
def hide_nodes(self, hide_sockets=True):
skip_nodes = {self.__node_input, self.__node_output}
for n in (x for x in self.nodes if x not in skip_nodes):
n.hide = True
if not hide_sockets:
continue
for s in n.inputs:
s.hide = not s.is_linked
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):
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):
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):
if io_name not in io_sockets:
idname = socket_type or socket.bl_idname
interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname))
if idname in SOCKET_SUBTYPE_MAPPING:
interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "")
if not min_max:
if idname.endswith("Factor") or io_name.endswith("Alpha"):
interface_socket.min_value, interface_socket.max_value = 0, 1
elif idname.endswith("Float") or idname.endswith("Vector"):
interface_socket.min_value, interface_socket.max_value = -10, 10
if socket is not None:
self.links.new(io_sockets[io_name], socket)
if default_val is not None:
interface_socket.default_value = default_val
if min_max is not None:
interface_socket.min_value, interface_socket.max_value = min_max
class _MaterialMorph:
@classmethod
def update_morph_inputs(cls, material, morph):
if material and material.node_tree and morph.name in material.node_tree.nodes:
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):
node, nodes = None, []
for m in morphs:
node = cls.__morph_node_add(material, m, node)
nodes.append(node)
if node:
node = cls.__morph_node_add(material, None, node) or node
for n in reversed(nodes):
n.location += node.location
if n.node_tree.name != node.node_tree.name:
n.location.x -= 100
if node.name.startswith("mmd_"):
n.location.y += 1500
node = n
return nodes
@classmethod
def reset_morph_links(cls, node):
cls.__update_morph_links(node, reset=True)
@classmethod
def __update_morph_links(cls, node, reset=False):
nodes, links = node.id_data.nodes, node.id_data.links
if reset:
if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links):
return
def __init_link(socket_morph, socket_shader):
if socket_shader and socket_morph.is_linked:
links.new(socket_morph.links[0].from_socket, socket_shader)
else:
def __init_link(socket_morph, socket_shader):
if socket_shader:
if socket_shader.is_linked:
links.new(socket_shader.links[0].from_socket, socket_morph)
if socket_morph.type == "VALUE":
socket_morph.default_value = socket_shader.default_value
else:
socket_morph.default_value[:3] = socket_shader.default_value[:3]
shader = nodes.get("mmd_shader", None)
if shader:
__init_link(node.inputs["Ambient1"], shader.inputs.get("Ambient Color"))
__init_link(node.inputs["Diffuse1"], shader.inputs.get("Diffuse Color"))
__init_link(node.inputs["Specular1"], shader.inputs.get("Specular Color"))
__init_link(node.inputs["Reflect1"], shader.inputs.get("Reflect"))
__init_link(node.inputs["Alpha1"], shader.inputs.get("Alpha"))
__init_link(node.inputs["Base1 RGB"], shader.inputs.get("Base Tex"))
__init_link(node.inputs["Toon1 RGB"], shader.inputs.get("Toon Tex")) # FIXME toon only affect shadow color
__init_link(node.inputs["Sphere1 RGB"], shader.inputs.get("Sphere Tex"))
elif "mmd_edge_preview" in nodes:
shader = nodes["mmd_edge_preview"]
__init_link(node.inputs["Edge1 RGB"], shader.inputs["Color"])
__init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"])
@classmethod
def __update_node_inputs(cls, node, morph):
node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3]
node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3]
node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3]
node.inputs["Reflect2"].default_value = morph.shininess
node.inputs["Alpha2"].default_value = morph.diffuse_color[3]
node.inputs["Edge2 RGB"].default_value[:3] = morph.edge_color[:3]
node.inputs["Edge2 A"].default_value = morph.edge_color[3]
node.inputs["Base2 RGB"].default_value[:3] = morph.texture_factor[:3]
node.inputs["Base2 A"].default_value = morph.texture_factor[3]
node.inputs["Toon2 RGB"].default_value[:3] = morph.toon_texture_factor[:3]
node.inputs["Toon2 A"].default_value = morph.toon_texture_factor[3]
node.inputs["Sphere2 RGB"].default_value[:3] = morph.sphere_texture_factor[:3]
node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3]
@classmethod
def __morph_node_add(cls, material, morph, prev_node):
nodes, links = material.node_tree.nodes, material.node_tree.links
shader = nodes.get("mmd_shader", None)
if morph:
node = nodes.new("ShaderNodeGroup")
node.parent = getattr(shader, "parent", None)
node.location = (-250, 0)
node.node_tree = cls.__get_shader("Add" if morph.offset_type == "ADD" else "Mul")
cls.__update_node_inputs(node, morph)
if prev_node:
for id_name in ("Ambient", "Diffuse", "Specular", "Reflect", "Alpha"):
links.new(prev_node.outputs[id_name], node.inputs[id_name + "1"])
for id_name in ("Edge", "Base", "Toon", "Sphere"):
links.new(prev_node.outputs[id_name + " RGB"], node.inputs[id_name + "1 RGB"])
links.new(prev_node.outputs[id_name + " A"], node.inputs[id_name + "1 A"])
else: # initial first node
if node.node_tree.name.endswith("Add"):
node.inputs["Base1 A"].default_value = 1
node.inputs["Toon1 A"].default_value = 1
node.inputs["Sphere1 A"].default_value = 1
cls.__update_morph_links(node)
return node
# connect last node to shader
if shader:
def __soft_link(socket_out, socket_in):
if socket_out and socket_in:
links.new(socket_out, socket_in)
__soft_link(prev_node.outputs["Ambient"], shader.inputs.get("Ambient Color"))
__soft_link(prev_node.outputs["Diffuse"], shader.inputs.get("Diffuse Color"))
__soft_link(prev_node.outputs["Specular"], shader.inputs.get("Specular Color"))
__soft_link(prev_node.outputs["Reflect"], shader.inputs.get("Reflect"))
__soft_link(prev_node.outputs["Alpha"], shader.inputs.get("Alpha"))
__soft_link(prev_node.outputs["Base Tex"], shader.inputs.get("Base Tex"))
__soft_link(prev_node.outputs["Toon Tex"], shader.inputs.get("Toon Tex"))
if int(material.mmd_material.sphere_texture_type) != 2: # shader.inputs['Sphere Mul/Add'].default_value < 0.5
__soft_link(prev_node.outputs["Sphere Tex"], shader.inputs.get("Sphere Tex"))
else:
__soft_link(prev_node.outputs["Sphere Tex Add"], shader.inputs.get("Sphere Tex"))
elif "mmd_edge_preview" in nodes:
shader = nodes["mmd_edge_preview"]
links.new(prev_node.outputs["Edge RGB"], shader.inputs["Color"])
links.new(prev_node.outputs["Edge A"], shader.inputs["Alpha"])
return shader
@classmethod
def __get_shader(cls, morph_type):
group_name = "MMDMorph" + morph_type
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes):
return shader
ng = _NodeGroupUtils(shader)
links = ng.links
use_mul = morph_type == "Mul"
############################################################################
node_input = ng.new_node("NodeGroupInput", (-3, 0))
ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat")
ng.new_node("NodeGroupOutput", (3, 0))
def __blend_color_add(id_name, pos, tag=""):
# MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac))
# MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2
# https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400
node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos[0] + 1, pos[1]))
links.new(node_input.outputs["Fac"], node_mix.inputs["Fac"])
ng.new_input_socket("%s1" % id_name + tag, node_mix.inputs["Color1"])
ng.new_input_socket("%s2" % id_name + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector")
ng.new_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):
# Tex Color = tex_rgb * tex_a + (1 - tex_a)
# : tex_rgb = TexRGB * ColorMul + ColorAdd
# : tex_a = TexA * ValueMul + ValueAdd
if id_name != "Sphere":
node_mix = ng.new_mix_node("MULTIPLY", pos, color1=(1, 1, 1, 1))
links.new(node_tex_a_output, node_mix.inputs[0])
links.new(node_tex_rgb.outputs["Color"], node_mix.inputs[2])
ng.new_output_socket(id_name + " Tex", node_mix.outputs[0])
else:
node_inv = ng.new_math_node("SUBTRACT", (pos[0], pos[1] - 0.25), value1=1.0)
node_scale = ng.new_vector_math_node("SCALE", (pos[0], pos[1]))
node_add = ng.new_vector_math_node("ADD", (pos[0] + 1, pos[1]))
links.new(node_tex_a_output, node_inv.inputs[1])
links.new(node_tex_rgb.outputs["Color"], node_scale.inputs[0])
links.new(node_tex_a_output, node_scale.inputs["Scale"])
links.new(node_scale.outputs[0], node_add.inputs[0])
links.new(node_inv.outputs[0], node_add.inputs[1])
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=""):
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)
pos_x = -2
__blend_color_add("Ambient", (pos_x, +0.5))
__blend_color_add("Diffuse", (pos_x, +0.0))
__blend_color_add("Specular", (pos_x, -0.5))
combine_reflect1_alpha1_edge1 = ng.new_node("ShaderNodeCombineRGB", (-2, -1.5))
combine_reflect2_alpha2_edge2 = ng.new_node("ShaderNodeCombineRGB", (-2, -1.75))
separate_reflect_alpha_edge = ng.new_node("ShaderNodeSeparateRGB", (pos_x + 2, -1.5))
__add_sockets("Reflect", combine_reflect1_alpha1_edge1.inputs[0], combine_reflect2_alpha2_edge2.inputs[0], separate_reflect_alpha_edge.outputs[0])
__add_sockets("Alpha", combine_reflect1_alpha1_edge1.inputs[1], combine_reflect2_alpha2_edge2.inputs[1], separate_reflect_alpha_edge.outputs[1])
__blend_color_add("Edge", (pos_x, -1.0), " RGB")
__add_sockets("Edge", combine_reflect1_alpha1_edge1.inputs[2], combine_reflect2_alpha2_edge2.inputs[2], separate_reflect_alpha_edge.outputs[2], tag=" A")
node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos_x + 1, -1.5))
links.new(node_input.outputs["Fac"], node_mix.inputs[0])
links.new(combine_reflect1_alpha1_edge1.outputs[0], node_mix.inputs[1])
links.new(combine_reflect2_alpha2_edge2.outputs[0], node_mix.inputs[2])
links.new(node_mix.outputs[0], separate_reflect_alpha_edge.inputs[0])
combine_base1a_toon1a_sphere1a = ng.new_node("ShaderNodeCombineRGB", (-2, -2.0))
combine_base2a_toon2a_sphere2a = ng.new_node("ShaderNodeCombineRGB", (-2, -2.25))
separate_basea_toona_spherea = ng.new_node("ShaderNodeSeparateRGB", (pos_x + 2, -2.0))
node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos_x + 1, -2.0))
links.new(node_input.outputs["Fac"], node_mix.inputs[0])
links.new(combine_base1a_toon1a_sphere1a.outputs[0], node_mix.inputs[1])
links.new(combine_base2a_toon2a_sphere2a.outputs[0], node_mix.inputs[2])
links.new(node_mix.outputs[0], separate_basea_toona_spherea.inputs[0])
base_rgb = __blend_color_add("Base", (pos_x, -2.5), " RGB")
__add_sockets("Base", combine_base1a_toon1a_sphere1a.inputs[0], combine_base2a_toon2a_sphere2a.inputs[0], separate_basea_toona_spherea.outputs[0], tag=" A")
__blend_tex_color("Base", (pos_x + 3, -2.5), base_rgb, separate_basea_toona_spherea.outputs[0])
toon_rgb = __blend_color_add("Toon", (pos_x, -3.0), " RGB")
__add_sockets("Toon", combine_base1a_toon1a_sphere1a.inputs[1], combine_base2a_toon2a_sphere2a.inputs[1], separate_basea_toona_spherea.outputs[1], tag=" A")
__blend_tex_color("Toon", (pos_x + 3, -3.0), toon_rgb, separate_basea_toona_spherea.outputs[1])
sphere_rgb = __blend_color_add("Sphere", (pos_x, -3.5), " RGB")
__add_sockets("Sphere", combine_base1a_toon1a_sphere1a.inputs[2], combine_base2a_toon2a_sphere2a.inputs[2], separate_basea_toona_spherea.outputs[2], tag=" A")
__blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2])
ng.hide_nodes()
return ng.shader
+738
View File
@@ -0,0 +1,738 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import itertools
import re
from abc import ABC, abstractmethod
from enum import Enum
from typing import TYPE_CHECKING, Callable, Dict, Optional, Set, Tuple
import bpy
from ..translations import DictionaryEnum
from ..utils import convertLRToName, convertNameToLR
from .model import FnModel, Model
if TYPE_CHECKING:
from ..properties.morph import _MorphBase
from ..properties.root import MMDRoot
from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex
class MMDTranslationElementType(Enum):
BONE = "Bones"
MORPH = "Morphs"
MATERIAL = "Materials"
DISPLAY = "Display"
PHYSICS = "Physics"
INFO = "Information"
class MMDDataHandlerABC(ABC):
@classmethod
@property
@abstractmethod
def type_name(cls) -> str:
pass
@classmethod
@abstractmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
pass
@classmethod
@abstractmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
pass
@classmethod
@abstractmethod
def update_index(cls, mmd_translation_element: "MMDTranslationElement"):
pass
@classmethod
@abstractmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
pass
@classmethod
@abstractmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
pass
@classmethod
@abstractmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
"""Returns (name, name_j, name_e)"""
@classmethod
def is_restorable(cls, mmd_translation_element: "MMDTranslationElement") -> bool:
return (mmd_translation_element.name, mmd_translation_element.name_j, mmd_translation_element.name_e) != cls.get_names(mmd_translation_element)
@classmethod
def check_data_visible(cls, filter_selected: bool, filter_visible: bool, select: bool, hide: bool) -> bool:
return filter_selected and not select or filter_visible and hide
@classmethod
def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str, original_value: str, index: int):
row = layout.row(align=True)
row.prop(mmd_translation_element, prop_name, text="")
if getattr(mmd_translation_element, prop_name) == original_value:
row.label(text="", icon="BLANK1")
return
op = row.operator("mmd_tools.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH")
op.index = index
op.prop_name = prop_name
op.restore_value = original_value
@classmethod
def prop_disabled(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str):
row = layout.row(align=True)
row.enabled = False
row.prop(mmd_translation_element, prop_name, text="")
row.label(text="", icon="BLANK1")
class MMDBoneHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.BONE.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
row = layout.row(align=True)
row.label(text="", icon="BONE_DATA")
prop_row = row.row()
cls.prop_restorable(prop_row, mmd_translation_element, "name", pose_bone.name, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", pose_bone.mmd_bone.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", pose_bone.mmd_bone.name_e, index)
row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.bone.select else "RESTRICT_SELECT_ON")
row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if pose_bone.bone.hide else "HIDE_OFF")
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
pose_bone: bpy.types.PoseBone
for index, pose_bone in enumerate(armature_object.pose.bones):
if not any(c.is_visible for c in pose_bone.bone.collections):
continue
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.BONE.name
mmd_translation_element.object = armature_object
mmd_translation_element.data_path = f"pose.bones[{index}]"
mmd_translation_element.name = pose_bone.name
mmd_translation_element.name_j = pose_bone.mmd_bone.name_j
mmd_translation_element.name_e = pose_bone.mmd_bone.name_e
@classmethod
def update_index(cls, mmd_translation_element: "MMDTranslationElement"):
bpy.context.view_layer.objects.active = mmd_translation_element.object
mmd_translation_element.object.id_data.data.bones.active = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path).bone
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.BONE.name:
continue
pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if cls.check_data_visible(filter_selected, filter_visible, pose_bone.bone.select, pose_bone.bone.hide):
continue
if check_blank_name(mmd_translation_element.name_j, mmd_translation_element.name_e):
continue
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if name is not None:
pose_bone.name = name
if name_j is not None:
pose_bone.mmd_bone.name_j = name_j
if name_e is not None:
pose_bone.mmd_bone.name_e = name_e
@classmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
return (pose_bone.name, pose_bone.mmd_bone.name_j, pose_bone.mmd_bone.name_e)
class MMDMorphHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.MORPH.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
row = layout.row(align=True)
row.label(text="", icon="SHAPEKEY_DATA")
prop_row = row.row()
cls.prop_disabled(prop_row, mmd_translation_element, "name")
cls.prop_restorable(prop_row, mmd_translation_element, "name", morph.name, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", morph.name_e, index)
row.label(text="", icon="BLANK1")
row.label(text="", icon="BLANK1")
MORPH_DATA_PATH_EXTRACT = re.compile(r"mmd_root\.(?P<morphs_name>[^\[]*)\[(?P<index>\d*)\]")
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
root_object: bpy.types.Object = mmd_translation.id_data
mmd_root: "MMDRoot" = root_object.mmd_root
for morphs_name, morphs in {
"material_morphs": mmd_root.material_morphs,
"uv_morphs": mmd_root.uv_morphs,
"bone_morphs": mmd_root.bone_morphs,
"vertex_morphs": mmd_root.vertex_morphs,
"group_morphs": mmd_root.group_morphs,
}.items():
morph: "_MorphBase"
for index, morph in enumerate(morphs):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.MORPH.name
mmd_translation_element.object = root_object
mmd_translation_element.data_path = f"mmd_root.{morphs_name}[{index}]"
mmd_translation_element.name = morph.name
# mmd_translation_element.name_j = None
mmd_translation_element.name_e = morph.name_e
@classmethod
def update_index(cls, mmd_translation_element: "MMDTranslationElement"):
match = cls.MORPH_DATA_PATH_EXTRACT.match(mmd_translation_element.data_path)
if not match:
return
mmd_translation_element.object.mmd_root.active_morph_type = match["morphs_name"]
mmd_translation_element.object.mmd_root.active_morph = int(match["index"])
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.MORPH.name:
continue
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if check_blank_name(morph.name, morph.name_e):
continue
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if name is not None:
morph.name = name
if name_e is not None:
morph.name_e = name_e
@classmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
return (morph.name, "", morph.name_e)
class MMDMaterialHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.MATERIAL.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
mesh_object: bpy.types.Object = mmd_translation_element.object
material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
row = layout.row(align=True)
row.label(text="", icon="MATERIAL_DATA")
prop_row = row.row()
cls.prop_restorable(prop_row, mmd_translation_element, "name", material.name, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", material.mmd_material.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", material.mmd_material.name_e, index)
row.prop(mesh_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mesh_object.select_get() else "RESTRICT_SELECT_ON")
row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mesh_object.hide_get() else "HIDE_OFF")
MATERIAL_DATA_PATH_EXTRACT = re.compile(r"data\.materials\[(?P<index>\d*)\]")
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
checked_materials: Set[bpy.types.Material] = set()
mesh_object: bpy.types.Object
for mesh_object in FnModel.iterate_mesh_objects(mmd_translation.id_data):
material: bpy.types.Material
for index, material in enumerate(mesh_object.data.materials):
if material in checked_materials:
continue
checked_materials.add(material)
if not hasattr(material, "mmd_material"):
continue
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.MATERIAL.name
mmd_translation_element.object = mesh_object
mmd_translation_element.data_path = f"data.materials[{index}]"
mmd_translation_element.name = material.name
mmd_translation_element.name_j = material.mmd_material.name_j
mmd_translation_element.name_e = material.mmd_material.name_e
@classmethod
def update_index(cls, mmd_translation_element: "MMDTranslationElement"):
id_data: bpy.types.Object = mmd_translation_element.object
bpy.context.view_layer.objects.active = id_data
match = cls.MATERIAL_DATA_PATH_EXTRACT.match(mmd_translation_element.data_path)
if not match:
return
id_data.active_material_index = int(match["index"])
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.MATERIAL.name:
continue
mesh_object: bpy.types.Object = mmd_translation_element.object
if cls.check_data_visible(filter_selected, filter_visible, mesh_object.select_get(), mesh_object.hide_get()):
continue
material: bpy.types.Material = mesh_object.path_resolve(mmd_translation_element.data_path)
if check_blank_name(material.mmd_material.name_j, material.mmd_material.name_e):
continue
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if name is not None:
material.name = name
if name_j is not None:
material.mmd_material.name_j = name_j
if name_e is not None:
material.mmd_material.name_e = name_e
@classmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
return (material.name, material.mmd_material.name_j, material.mmd_material.name_e)
class MMDDisplayHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.DISPLAY.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
row = layout.row(align=True)
row.label(text="", icon="GROUP_BONE")
prop_row = row.row()
cls.prop_restorable(prop_row, mmd_translation_element, "name", bone_collection.name, index)
cls.prop_disabled(prop_row, mmd_translation_element, "name")
cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
row.prop(mmd_translation_element.object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mmd_translation_element.object.select_get() else "RESTRICT_SELECT_ON")
row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mmd_translation_element.object.hide_get() else "HIDE_OFF")
DISPLAY_DATA_PATH_EXTRACT = re.compile(r"data\.collections\[(?P<index>\d*)\]")
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data)
bone_collection: bpy.types.BoneCollection
for index, bone_collection in enumerate(armature_object.data.collections):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.DISPLAY.name
mmd_translation_element.object = armature_object
mmd_translation_element.data_path = f"data.collections[{index}]"
mmd_translation_element.name = bone_collection.name
# mmd_translation_element.name_j = None
# mmd_translation_element.name_e = None
@classmethod
def update_index(cls, mmd_translation_element: "MMDTranslationElement"):
id_data: bpy.types.Object = mmd_translation_element.object
bpy.context.view_layer.objects.active = id_data
match = cls.DISPLAY_DATA_PATH_EXTRACT.match(mmd_translation_element.data_path)
if not match:
return
id_data.data.collections.active_index = int(match["index"])
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.DISPLAY.name:
continue
obj: bpy.types.Object = mmd_translation_element.object
if cls.check_data_visible(filter_selected, filter_visible, obj.select_get(), obj.hide_get()):
continue
bone_collection: bpy.types.BoneCollection = obj.path_resolve(mmd_translation_element.data_path)
if check_blank_name(bone_collection.name, ""):
continue
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
if name is not None:
bone_collection.name = name
@classmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path)
return (bone_collection.name, "", "")
class MMDPhysicsHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.PHYSICS.name
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
obj: bpy.types.Object = mmd_translation_element.object
if FnModel.is_rigid_body_object(obj):
icon = "MESH_ICOSPHERE"
mmd_object = obj.mmd_rigid
elif FnModel.is_joint_object(obj):
icon = "CONSTRAINT"
mmd_object = obj.mmd_joint
row = layout.row(align=True)
row.label(text="", icon=icon)
prop_row = row.row()
cls.prop_restorable(prop_row, mmd_translation_element, "name", obj.name, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_j", mmd_object.name_j, index)
cls.prop_restorable(prop_row, mmd_translation_element, "name_e", mmd_object.name_e, index)
row.prop(obj, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if obj.select_get() else "RESTRICT_SELECT_ON")
row.prop(obj, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if obj.hide_get() else "HIDE_OFF")
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
root_object: bpy.types.Object = mmd_translation.id_data
model = Model(root_object)
obj: bpy.types.Object
for obj in model.rigidBodies():
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
mmd_translation_element.object = obj
mmd_translation_element.data_path = "mmd_rigid"
mmd_translation_element.name = obj.name
mmd_translation_element.name_j = obj.mmd_rigid.name_j
mmd_translation_element.name_e = obj.mmd_rigid.name_e
obj: bpy.types.Object
for obj in model.joints():
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name
mmd_translation_element.object = obj
mmd_translation_element.data_path = "mmd_joint"
mmd_translation_element.name = obj.name
mmd_translation_element.name_j = obj.mmd_joint.name_j
mmd_translation_element.name_e = obj.mmd_joint.name_e
@classmethod
def update_index(cls, mmd_translation_element: "MMDTranslationElement"):
bpy.context.view_layer.objects.active = mmd_translation_element.object
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.PHYSICS.name:
continue
obj: bpy.types.Object = mmd_translation_element.object
if cls.check_data_visible(filter_selected, filter_visible, obj.select_get(), obj.hide_get()):
continue
if FnModel.is_rigid_body_object(obj):
mmd_object = obj.mmd_rigid
elif FnModel.is_joint_object(obj):
mmd_object = obj.mmd_joint
if check_blank_name(mmd_object.name_j, mmd_object.name_e):
continue
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
obj: bpy.types.Object = mmd_translation_element.object
if FnModel.is_rigid_body_object(obj):
mmd_object = obj.mmd_rigid
elif FnModel.is_joint_object(obj):
mmd_object = obj.mmd_joint
if name is not None:
obj.name = name
if name_j is not None:
mmd_object.name_j = name_j
if name_e is not None:
mmd_object.name_e = name_e
@classmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
obj: bpy.types.Object = mmd_translation_element.object
if FnModel.is_rigid_body_object(obj):
mmd_object = obj.mmd_rigid
elif FnModel.is_joint_object(obj):
mmd_object = obj.mmd_joint
return (obj.name, mmd_object.name_j, mmd_object.name_e)
class MMDInfoHandler(MMDDataHandlerABC):
@classmethod
@property
def type_name(cls) -> str:
return MMDTranslationElementType.INFO.name
TYPE_TO_ICONS = {
"EMPTY": "EMPTY_DATA",
"ARMATURE": "ARMATURE_DATA",
"MESH": "MESH_DATA",
}
@classmethod
def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int):
info_object: bpy.types.Object = mmd_translation_element.object
row = layout.row(align=True)
row.label(text="", icon=MMDInfoHandler.TYPE_TO_ICONS.get(info_object.type, "OBJECT_DATA"))
prop_row = row.row()
cls.prop_restorable(prop_row, mmd_translation_element, "name", info_object.name, index)
cls.prop_disabled(prop_row, mmd_translation_element, "name")
cls.prop_disabled(prop_row, mmd_translation_element, "name_e")
row.prop(info_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if info_object.select_get() else "RESTRICT_SELECT_ON")
row.prop(info_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if info_object.hide_get() else "HIDE_OFF")
@classmethod
def collect_data(cls, mmd_translation: "MMDTranslation"):
root_object: bpy.types.Object = mmd_translation.id_data
info_objects = [root_object]
armature_object = FnModel.find_armature_object(root_object)
if armature_object is not None:
info_objects.append(armature_object)
for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)):
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add()
mmd_translation_element.type = MMDTranslationElementType.INFO.name
mmd_translation_element.object = info_object
mmd_translation_element.data_path = ""
mmd_translation_element.name = info_object.name
# mmd_translation_element.name_j = None
# mmd_translation_element.name_e = None
@classmethod
def update_index(cls, mmd_translation_element: "MMDTranslationElement"):
bpy.context.view_layer.objects.active = mmd_translation_element.object
@classmethod
def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]):
mmd_translation_element: "MMDTranslationElement"
for index, mmd_translation_element in enumerate(mmd_translation.translation_elements):
if mmd_translation_element.type != MMDTranslationElementType.INFO.name:
continue
info_object: bpy.types.Object = mmd_translation_element.object
if cls.check_data_visible(filter_selected, filter_visible, info_object.select_get(), info_object.hide_get()):
continue
if check_blank_name(info_object.name, ""):
continue
if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element):
continue
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add()
mmd_translation_element_index.value = index
@classmethod
def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]):
info_object: bpy.types.Object = mmd_translation_element.object
if name is not None:
info_object.name = name
@classmethod
def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]:
info_object: bpy.types.Object = mmd_translation_element.object
return (info_object.name, "", "")
MMD_DATA_HANDLERS: Set[MMDDataHandlerABC] = {
MMDBoneHandler,
MMDMorphHandler,
MMDMaterialHandler,
MMDDisplayHandler,
MMDPhysicsHandler,
MMDInfoHandler,
}
MMD_DATA_TYPE_TO_HANDLERS: Dict[str, MMDDataHandlerABC] = {h.type_name: h for h in MMD_DATA_HANDLERS}
class FnTranslations:
@staticmethod
def apply_translations(root_object: bpy.types.Object):
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
mmd_translation_element_index: "MMDTranslationElementIndex"
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
name, name_j, name_e = handler.get_names(mmd_translation_element)
handler.set_names(
mmd_translation_element,
mmd_translation_element.name if mmd_translation_element.name != name else None,
mmd_translation_element.name_j if mmd_translation_element.name_j != name_j else None,
mmd_translation_element.name_e if mmd_translation_element.name_e != name_e else None,
)
@staticmethod
def execute_translation_batch(root_object: bpy.types.Object) -> Tuple[Dict[str, str], Optional[bpy.types.Text]]:
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
batch_operation_script = mmd_translation.batch_operation_script
if not batch_operation_script:
return ({}, None)
translator = DictionaryEnum.get_translator(mmd_translation.dictionary)
def translate(name: str) -> str:
if translator:
return translator.translate(name, name)
return name
batch_operation_script_ast = compile(mmd_translation.batch_operation_script, "<string>", "eval")
batch_operation_target: str = mmd_translation.batch_operation_target
mmd_translation_element_index: "MMDTranslationElementIndex"
for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices:
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type]
name = mmd_translation_element.name
name_j = mmd_translation_element.name_j
name_e = mmd_translation_element.name_e
org_name, org_name_j, org_name_e = handler.get_names(mmd_translation_element)
# pylint: disable=eval-used
result_name = str(
eval(
batch_operation_script_ast,
{"__builtins__": {}},
{
"to_english": translate,
"to_mmd_lr": convertLRToName,
"to_blender_lr": convertNameToLR,
"name": name,
"name_j": name_j if name_j != "" else name,
"name_e": name_e if name_e != "" else name,
"org_name": org_name,
"org_name_j": org_name_j,
"org_name_e": org_name_e,
},
)
)
if batch_operation_target == "BLENDER":
mmd_translation_element.name = result_name
elif batch_operation_target == "JAPANESE":
mmd_translation_element.name_j = result_name
elif batch_operation_target == "ENGLISH":
mmd_translation_element.name_e = result_name
return (translator.fails, translator.save_fails())
@staticmethod
def update_index(mmd_translation: "MMDTranslation"):
if mmd_translation.filtered_translation_element_indices_active_index < 0:
return
mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index]
mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value]
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].update_index(mmd_translation_element)
@staticmethod
def collect_data(mmd_translation: "MMDTranslation"):
mmd_translation.translation_elements.clear()
for handler in MMD_DATA_HANDLERS:
handler.collect_data(mmd_translation)
@staticmethod
def update_query(mmd_translation: "MMDTranslation"):
mmd_translation.filtered_translation_element_indices.clear()
mmd_translation.filtered_translation_element_indices_active_index = -1
filter_japanese_blank: bool = mmd_translation.filter_japanese_blank
filter_english_blank: bool = mmd_translation.filter_english_blank
filter_selected: bool = mmd_translation.filter_selected
filter_visible: bool = mmd_translation.filter_visible
def check_blank_name(name_j: str, name_e: str) -> bool:
return filter_japanese_blank and name_j or filter_english_blank and name_e
for handler in MMD_DATA_HANDLERS:
if handler.type_name in mmd_translation.filter_types:
handler.update_query(mmd_translation, filter_selected, filter_visible, check_blank_name)
@staticmethod
def clear_data(mmd_translation: "MMDTranslation"):
mmd_translation.translation_elements.clear()
mmd_translation.filtered_translation_element_indices.clear()
mmd_translation.filtered_translation_element_indices_active_index = -1
mmd_translation.filter_restorable = False
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
+673
View File
@@ -0,0 +1,673 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
import math
import os
from typing import Union
import bpy
from mathutils import Quaternion, Vector
from ... import utils
from .. import vmd
from ..camera import MMDCamera
from ..lamp import MMDLamp
class _MirrorMapper:
def __init__(self, data_map=None):
from ...operators.view import FlipPose
self.__data_map = data_map
self.__flip_name = FlipPose.flip_name
def get(self, name, default=None):
return self.__data_map.get(self.__flip_name(name), None) or self.__data_map.get(name, default)
@staticmethod
def get_location(location):
return (-location[0], location[1], location[2])
@staticmethod
def get_rotation(rotation_xyzw):
return (rotation_xyzw[0], -rotation_xyzw[1], -rotation_xyzw[2], rotation_xyzw[3])
@staticmethod
def get_rotation3(rotation_xyz):
return (rotation_xyz[0], -rotation_xyz[1], -rotation_xyz[2])
class RenamedBoneMapper:
def __init__(self, armObj=None, rename_LR_bones=True, use_underscore=False, translator=None):
self.__pose_bones = armObj.pose.bones if armObj else None
self.__rename_LR_bones = rename_LR_bones
self.__use_underscore = use_underscore
self.__translator = translator
def init(self, armObj):
self.__pose_bones = armObj.pose.bones
return self
def get(self, bone_name, default=None):
bl_bone_name = bone_name
if self.__rename_LR_bones:
bl_bone_name = utils.convertNameToLR(bl_bone_name, self.__use_underscore)
if self.__translator:
bl_bone_name = self.__translator.translate(bl_bone_name)
return self.__pose_bones.get(bl_bone_name, default)
class _InterpolationHelper:
def __init__(self, mat):
self.__indices = indices = [0, 1, 2]
l = sorted((-abs(mat[i][j]), i, j) for i in range(3) for j in range(3))
_, i, j = l[0]
if i != j:
indices[i], indices[j] = indices[j], indices[i]
_, i, j = next(k for k in l if k[1] != i and k[2] != j)
if indices[i] != j:
idx = indices.index(j)
indices[i], indices[idx] = indices[idx], indices[i]
def convert(self, interpolation_xyz):
return (interpolation_xyz[i] for i in self.__indices)
class BoneConverter:
def __init__(self, pose_bone, scale, invert=False):
mat = pose_bone.bone.matrix_local.to_3x3()
mat[1], mat[2] = mat[2].copy(), mat[1].copy()
self.__mat = mat.transposed()
self.__scale = scale
if invert:
self.__mat.invert()
self.convert_interpolation = _InterpolationHelper(self.__mat).convert
def convert_location(self, location):
return (self.__mat @ Vector(location)) * self.__scale
def convert_rotation(self, rotation_xyzw):
rot = Quaternion()
rot.x, rot.y, rot.z, rot.w = rotation_xyzw
return Quaternion((self.__mat @ rot.axis) * -1, rot.angle).normalized()
class BoneConverterPoseMode:
def __init__(self, pose_bone, scale, invert=False):
mat = pose_bone.matrix.to_3x3()
mat[1], mat[2] = mat[2].copy(), mat[1].copy()
self.__mat = mat.transposed()
self.__scale = scale
self.__mat_rot = pose_bone.matrix_basis.to_3x3()
self.__mat_loc = self.__mat_rot @ self.__mat
self.__offset = pose_bone.location.copy()
self.convert_location = self._convert_location
self.convert_rotation = self._convert_rotation
if invert:
self.__mat.invert()
self.__mat_rot.invert()
self.__mat_loc.invert()
self.convert_location = self._convert_location_inverted
self.convert_rotation = self._convert_rotation_inverted
self.convert_interpolation = _InterpolationHelper(self.__mat_loc).convert
def _convert_location(self, location):
return self.__offset + (self.__mat_loc @ Vector(location)) * self.__scale
def _convert_rotation(self, rotation_xyzw):
rot = Quaternion()
rot.x, rot.y, rot.z, rot.w = rotation_xyzw
rot = Quaternion((self.__mat @ rot.axis) * -1, rot.angle)
return (self.__mat_rot @ rot.to_matrix()).to_quaternion()
def _convert_location_inverted(self, location):
return (self.__mat_loc @ (Vector(location) - self.__offset)) * self.__scale
def _convert_rotation_inverted(self, rotation_xyzw):
rot = Quaternion()
rot.x, rot.y, rot.z, rot.w = rotation_xyzw
rot = (self.__mat_rot @ rot.to_matrix()).to_quaternion()
return Quaternion((self.__mat @ rot.axis) * -1, rot.angle).normalized()
class _FnBezier:
@classmethod
def from_fcurve(cls, kp0, kp1):
p0, p1, p2, p3 = kp0.co, kp0.handle_right, kp1.handle_left, kp1.co
if p1.x > p3.x:
t = (p3.x - p0.x) / (p1.x - p0.x)
p1 = (1 - t) * p0 + p1 * t
if p0.x > p2.x:
t = (p3.x - p0.x) / (p3.x - p2.x)
p2 = (1 - t) * p3 + p2 * t
return cls(p0, p1, p2, p3)
def __init__(self, p0, p1, p2, p3): # assuming VMD's bezier or F-Curve's bezier
# assert(p0.x <= p1.x <= p3.x and p0.x <= p2.x <= p3.x)
self._p0, self._p1, self._p2, self._p3 = p0, p1, p2, p3
@property
def points(self):
return self._p0, self._p1, self._p2, self._p3
def split(self, t):
p0, p1, p2, p3 = self._p0, self._p1, self._p2, self._p3
p01t = (1 - t) * p0 + t * p1
p12t = (1 - t) * p1 + t * p2
p23t = (1 - t) * p2 + t * p3
p012t = (1 - t) * p01t + t * p12t
p123t = (1 - t) * p12t + t * p23t
pt = (1 - t) * p012t + t * p123t
return _FnBezier(p0, p01t, p012t, pt), _FnBezier(pt, p123t, p23t, p3), pt
def evaluate(self, t):
p0, p1, p2, p3 = self._p0, self._p1, self._p2, self._p3
p01t = (1 - t) * p0 + t * p1
p12t = (1 - t) * p1 + t * p2
p23t = (1 - t) * p2 + t * p3
p012t = (1 - t) * p01t + t * p12t
p123t = (1 - t) * p12t + t * p23t
return (1 - t) * p012t + t * p123t
def split_by_x(self, x):
return self.split(self.axis_to_t(x))
def evaluate_by_x(self, x):
return self.evaluate(self.axis_to_t(x))
def axis_to_t(self, val, axis=0):
p0, p1, p2, p3 = self._p0[axis], self._p1[axis], self._p2[axis], self._p3[axis]
a = p3 - p0 + 3 * (p1 - p2)
b = 3 * (p0 - 2 * p1 + p2)
c = 3 * (p1 - p0)
d = p0 - val
return next(self.__find_roots(a, b, c, d))
def find_critical(self):
p0, p1, p2, p3 = self._p0.y, self._p1.y, self._p2.y, self._p3.y
p_min, p_max = (p0, p3) if p0 < p3 else (p3, p0)
if p1 > p_max or p1 < p_min or p2 > p_max or p2 < p_min:
a = 3 * (p3 - p0 + 3 * (p1 - p2))
b = 6 * (p0 - 2 * p1 + p2)
c = 3 * (p1 - p0)
yield from self.__find_roots(0, a, b, c)
@staticmethod
def __find_roots(a, b, c, d): # a*t*t*t + b*t*t + c*t + d = 0
# TODO fix precision errors (ex: t=0 and t=1) and improve performance
if a == 0:
if b == 0:
t = -d / c
if 0 <= t <= 1:
yield t
else:
D = c * c - 4 * b * d
if D < 0:
return
D = D**0.5
b2 = 2 * b
t = (-c + D) / b2
if 0 <= t <= 1:
yield t
t = (-c - D) / b2
if 0 <= t <= 1:
yield t
return
def _sqrt3(v):
return -((-v) ** (1 / 3)) if v < 0 else v ** (1 / 3)
A = b * c / (6 * a * a) - b * b * b / (27 * a * a * a) - d / (2 * a)
B = c / (3 * a) - b * b / (9 * a * a)
b_3a = -b / (3 * a)
D = A * A + B * B * B
if D > 0:
D = D**0.5
t = b_3a + _sqrt3(A + D) + _sqrt3(A - D)
if 0 <= t <= 1:
yield t
elif D == 0:
t = b_3a + _sqrt3(A) * 2
if 0 <= t <= 1:
yield t
t = b_3a - _sqrt3(A)
if 0 <= t <= 1:
yield t
else:
R = A / (-B * B * B) ** 0.5
t = b_3a + 2 * (-B) ** 0.5 * math.cos(math.acos(R) / 3)
if 0 <= t <= 1:
yield t
t = b_3a + 2 * (-B) ** 0.5 * math.cos((math.acos(R) + 2 * math.pi) / 3)
if 0 <= t <= 1:
yield t
t = b_3a + 2 * (-B) ** 0.5 * math.cos((math.acos(R) - 2 * math.pi) / 3)
if 0 <= t <= 1:
yield t
class HasAnimationData:
animation_data: bpy.types.AnimData
class VMDImporter:
def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False):
self.__vmdFile = vmd.File()
self.__vmdFile.load(filepath=filepath)
logging.debug(str(self.__vmdFile.header))
self.__scale = scale
self.__convert_mmd_camera = convert_mmd_camera
self.__convert_mmd_lamp = convert_mmd_lamp
self.__bone_mapper = bone_mapper
self.__bone_util_cls = BoneConverterPoseMode if use_pose_mode else BoneConverter
self.__frame_margin = frame_margin + 1
self.__mirror = use_mirror
self.__use_NLA = use_NLA
@staticmethod
def __minRotationDiff(prev_q, curr_q):
t1 = (prev_q.w - curr_q.w) ** 2 + (prev_q.x - curr_q.x) ** 2 + (prev_q.y - curr_q.y) ** 2 + (prev_q.z - curr_q.z) ** 2
t2 = (prev_q.w + curr_q.w) ** 2 + (prev_q.x + curr_q.x) ** 2 + (prev_q.y + curr_q.y) ** 2 + (prev_q.z + curr_q.z) ** 2
# t1 = prev_q.rotation_difference(curr_q).angle
# t2 = prev_q.rotation_difference(-curr_q).angle
return -curr_q if t2 < t1 else curr_q
@staticmethod
def __setInterpolation(bezier, kp0, kp1):
if bezier[0] == bezier[1] and bezier[2] == bezier[3]:
kp0.interpolation = "LINEAR"
else:
kp0.interpolation = "BEZIER"
kp0.handle_right_type = "FREE"
kp1.handle_left_type = "FREE"
d = (kp1.co - kp0.co) / 127.0
kp0.handle_right = kp0.co + Vector((d.x * bezier[0], d.y * bezier[1]))
kp1.handle_left = kp0.co + Vector((d.x * bezier[2], d.y * bezier[3]))
@staticmethod
def __fixFcurveHandles(fcurve):
kp0 = fcurve.keyframe_points[0]
kp0.handle_left_type = "FREE"
kp0.handle_left = kp0.co + Vector((-1, 0))
kp = fcurve.keyframe_points[-1]
kp.handle_right_type = "FREE"
kp.handle_right = kp.co + Vector((1, 0))
@staticmethod
def __keyframe_insert_inner(fcurves: bpy.types.ActionFCurves, path: str, index: int, frame: float, value: float):
fcurve = fcurves.find(path, index=index)
if fcurve is None:
fcurve = fcurves.new(path, index=index)
fcurve.keyframe_points.insert(frame, value, options={"FAST"})
@staticmethod
def __keyframe_insert(fcurves: bpy.types.ActionFCurves, path: str, frame: float, value: Union[int, float, Vector]):
if isinstance(value, (int, float)):
VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value)
elif isinstance(value, Vector):
VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value[0])
VMDImporter.__keyframe_insert_inner(fcurves, path, 1, frame, value[1])
VMDImporter.__keyframe_insert_inner(fcurves, path, 2, frame, value[2])
else:
raise TypeError("Unsupported type: {0}".format(type(value)))
def __getBoneConverter(self, bone):
converter = self.__bone_util_cls(bone, self.__scale)
mode = bone.rotation_mode
compatible_quaternion = self.__minRotationDiff
class _ConverterWrap:
convert_location = converter.convert_location
convert_interpolation = converter.convert_interpolation
if mode == "QUATERNION":
convert_rotation = converter.convert_rotation
compatible_rotation = compatible_quaternion
elif mode == "AXIS_ANGLE":
@staticmethod
def convert_rotation(rot):
(x, y, z), angle = converter.convert_rotation(rot).to_axis_angle()
return (angle, x, y, z)
@staticmethod
def compatible_rotation(prev, curr):
angle, x, y, z = curr
if prev[1] * x + prev[2] * y + prev[3] * z < 0:
angle, x, y, z = -angle, -x, -y, -z
angle_diff = prev[0] - angle
if abs(angle_diff) > math.pi:
pi_2 = math.pi * 2
bias = -0.5 if angle_diff < 0 else 0.5
angle += int(bias + angle_diff / pi_2) * pi_2
return (angle, x, y, z)
else:
convert_rotation = lambda rot: converter.convert_rotation(rot).to_euler(mode)
compatible_rotation = lambda prev, curr: curr.make_compatible(prev) or curr
return _ConverterWrap
def __assign_action(self, target: Union[bpy.types.ID, HasAnimationData], action: bpy.types.Action):
if target.animation_data is None:
target.animation_data_create()
if not self.__use_NLA:
target.animation_data.action = action
else:
frame_current = bpy.context.scene.frame_current
target_track: bpy.types.NlaTrack = target.animation_data.nla_tracks.new()
target_track.name = action.name
target_strip = target_track.strips.new(action.name, frame_current, action)
target_strip.blend_type = "COMBINE"
def __assignToArmature(self, armObj, action_name=None):
boneAnim = self.__vmdFile.boneAnimation
logging.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name)
if len(boneAnim) < 1:
return
action_name = action_name or armObj.name
action = bpy.data.actions.new(name=action_name)
extra_frame = 1 if self.__frame_margin > 1 else 0
pose_bones = armObj.pose.bones
if self.__bone_mapper:
pose_bones = self.__bone_mapper(armObj)
_loc = _rot = lambda i: i
if self.__mirror:
pose_bones = _MirrorMapper(pose_bones)
_loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation
class _Dummy:
pass
dummy_keyframe_points = iter(lambda: _Dummy, None)
prop_rot_map = {"QUATERNION": "rotation_quaternion", "AXIS_ANGLE": "rotation_axis_angle"}
bone_name_table = {}
for name, keyFrames in boneAnim.items():
num_frame = len(keyFrames)
if num_frame < 1:
continue
bone = pose_bones.get(name, None)
if bone is None:
logging.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames))
continue
logging.info("(bone) frames:%5d name: %s", len(keyFrames), name)
assert bone_name_table.get(bone.name, name) == name
bone_name_table[bone.name] = name
fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3)
data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler")
bone_rotation = getattr(bone, data_path_rot)
default_values = list(bone.location) + list(bone_rotation)
data_path = 'pose.bones["%s"].location' % bone.name
for axis_i in range(3):
fcurves[axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name)
data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot)
for axis_i in range(len(bone_rotation)):
fcurves[3 + axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name)
for i in range(len(default_values)):
c = fcurves[i]
c.keyframe_points.add(extra_frame + num_frame)
kp_iter = iter(c.keyframe_points)
if extra_frame:
kp = next(kp_iter)
kp.co = (1, default_values[i])
kp.interpolation = "LINEAR"
fcurves[i] = kp_iter
converter = self.__getBoneConverter(bone)
prev_rot = bone_rotation if extra_frame else None
prev_kps, indices = None, tuple(converter.convert_interpolation((0, 16, 32))) + (48,) * len(bone_rotation)
keyFrames.sort(key=lambda x: x.frame_number)
for k, x, y, z, r0, r1, r2, r3 in zip(keyFrames, *fcurves):
frame = k.frame_number + self.__frame_margin
loc = converter.convert_location(_loc(k.location))
curr_rot = converter.convert_rotation(_rot(k.rotation))
if prev_rot is not None:
curr_rot = converter.compatible_rotation(prev_rot, curr_rot)
# FIXME the rotation interpolation has slightly different result
# Blender: rot(x) = prev_rot*(1 - bezier(t)) + curr_rot*bezier(t)
# MMD: rot(x) = prev_rot.slerp(curr_rot, factor=bezier(t))
prev_rot = curr_rot
x.co = (frame, loc[0])
y.co = (frame, loc[1])
z.co = (frame, loc[2])
r0.co = (frame, curr_rot[0])
r1.co = (frame, curr_rot[1])
r2.co = (frame, curr_rot[2])
r3.co = (frame, curr_rot[-1])
curr_kps = (x, y, z, r0, r1, r2, r3)
if prev_kps is not None:
interp = k.interp
for idx, prev_kp, kp in zip(indices, prev_kps, curr_kps):
self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp)
prev_kps = curr_kps
for c in action.fcurves:
self.__fixFcurveHandles(c)
# property animation
propertyAnim = self.__vmdFile.propertyAnimation
if len(propertyAnim) > 0:
logging.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name)
for keyFrame in propertyAnim:
logging.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states)
frame = keyFrame.frame_number + self.__frame_margin
for ikName, enable in keyFrame.ik_states:
bone = pose_bones.get(ikName, None)
if not bone:
continue
self.__keyframe_insert(action.fcurves, f'pose.bones["{bone.name}"].mmd_ik_toggle', frame, enable)
self.__assign_action(armObj, action)
# Ensure IK toggle state is set based on the first frame of VMD animation
if len(propertyAnim) > 0:
# Collect IK states from the first frame
first_frame_ik_states = {}
first_frame = float('inf')
for keyFrame in propertyAnim:
frame_num = keyFrame.frame_number
if frame_num < first_frame:
first_frame = frame_num
for ikName, enable in keyFrame.ik_states:
first_frame_ik_states[ikName] = enable
elif frame_num == first_frame:
for ikName, enable in keyFrame.ik_states:
if ikName not in first_frame_ik_states:
first_frame_ik_states[ikName] = enable
# Set the mmd_ik_toggle property for each bone based on the collected first frame IK states
for ikName, enable in first_frame_ik_states.items():
bone = pose_bones.get(ikName, None)
if bone and bone.mmd_ik_toggle != enable:
bone.mmd_ik_toggle = enable # This will trigger the _pose_bone_update_mmd_ik_toggle method
def __assignToMesh(self, meshObj, action_name=None):
shapeKeyAnim = self.__vmdFile.shapeKeyAnimation
logging.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name)
if len(shapeKeyAnim) < 1:
return
action_name = action_name or meshObj.name
action = bpy.data.actions.new(name=action_name)
mirror_map = _MirrorMapper(meshObj.data.shape_keys.key_blocks) if self.__mirror else {}
shapeKeyDict = {k: mirror_map.get(k, v) for k, v in meshObj.data.shape_keys.key_blocks.items()}
from math import ceil, floor
for name, keyFrames in shapeKeyAnim.items():
if name not in shapeKeyDict:
logging.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames))
continue
logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name)
shapeKey = shapeKeyDict[name]
fcurve = action.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name)
fcurve.keyframe_points.add(len(keyFrames))
keyFrames.sort(key=lambda x: x.frame_number)
for k, v in zip(keyFrames, fcurve.keyframe_points):
v.co = (k.frame_number + self.__frame_margin, k.weight)
v.interpolation = "LINEAR"
weights = tuple(i.weight for i in keyFrames)
shapeKey.slider_min = min(shapeKey.slider_min, floor(min(weights)))
shapeKey.slider_max = max(shapeKey.slider_max, ceil(max(weights)))
self.__assign_action(meshObj.data.shape_keys, action)
def __assignToRoot(self, rootObj, action_name=None):
propertyAnim = self.__vmdFile.propertyAnimation
logging.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name)
if len(propertyAnim) < 1:
return
action_name = action_name or rootObj.name
action = bpy.data.actions.new(name=action_name)
logging.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim])
for keyFrame in propertyAnim:
self.__keyframe_insert(action.fcurves, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible))
self.__assign_action(rootObj, action)
@staticmethod
def detectCameraChange(fcurve, threshold=10.0):
frames = list(fcurve.keyframe_points)
frameCount = len(frames)
frames.sort(key=lambda x: x.co[0])
for i, f in enumerate(frames):
if i + 1 < frameCount:
n = frames[i + 1]
if n.co[0] - f.co[0] <= 1.0 and abs(f.co[1] - n.co[1]) > threshold:
f.interpolation = "CONSTANT"
def __assignToCamera(self, cameraObj, action_name=None):
mmdCameraInstance = MMDCamera.convertToMMDCamera(cameraObj, self.__scale)
mmdCamera = mmdCameraInstance.object()
cameraObj = mmdCameraInstance.camera()
cameraAnim = self.__vmdFile.cameraAnimation
logging.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name)
if len(cameraAnim) < 1:
return
action_name = action_name or mmdCamera.name
parent_action = bpy.data.actions.new(name=action_name)
distance_action = bpy.data.actions.new(name=action_name + "_dis")
_loc = _rot = lambda i: i
if self.__mirror:
_loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation3
fcurves = []
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z
for i in range(3):
fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov
fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp
fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis
for c in fcurves:
c.keyframe_points.add(len(cameraAnim))
prev_kps, indices = None, (0, 8, 4, 12, 12, 12, 16, 20) # x, z, y, rx, ry, rz, dis, fov
cameraAnim.sort(key=lambda x: x.frame_number)
for k, x, y, z, rx, ry, rz, fov, persp, dis in zip(cameraAnim, *(c.keyframe_points for c in fcurves)):
frame = k.frame_number + self.__frame_margin
x.co, z.co, y.co = ((frame, val * self.__scale) for val in _loc(k.location))
rx.co, rz.co, ry.co = ((frame, val) for val in _rot(k.rotation))
fov.co = (frame, math.radians(k.angle))
dis.co = (frame, k.distance * self.__scale)
persp.co = (frame, k.persp)
persp.interpolation = "CONSTANT"
curr_kps = (x, y, z, rx, ry, rz, dis, fov)
if prev_kps is not None:
interp = k.interp
for idx, prev_kp, kp in zip(indices, prev_kps, curr_kps):
self.__setInterpolation(interp[idx : idx + 4 : 2] + interp[idx + 1 : idx + 4 : 2], prev_kp, kp)
prev_kps = curr_kps
for fcurve in fcurves:
self.__fixFcurveHandles(fcurve)
if fcurve.data_path == "rotation_euler":
self.detectCameraChange(fcurve)
self.__assign_action(mmdCamera, parent_action)
self.__assign_action(cameraObj, distance_action)
@staticmethod
def detectLampChange(fcurve, threshold=0.1):
frames = list(fcurve.keyframe_points)
frameCount = len(frames)
frames.sort(key=lambda x: x.co[0])
for i, f in enumerate(frames):
f.interpolation = "LINEAR"
if i + 1 < frameCount:
n = frames[i + 1]
if n.co[0] - f.co[0] <= 1.0 and abs(f.co[1] - n.co[1]) > threshold:
f.interpolation = "CONSTANT"
def __assignToLamp(self, lampObj, action_name=None):
mmdLampInstance = MMDLamp.convertToMMDLamp(lampObj, self.__scale)
mmdLamp = mmdLampInstance.object()
lampObj = mmdLampInstance.lamp()
lampAnim = self.__vmdFile.lampAnimation
logging.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name)
if len(lampAnim) < 1:
return
action_name = action_name or mmdLamp.name
color_action = bpy.data.actions.new(name=action_name + "_color")
location_action = bpy.data.actions.new(name=action_name + "_loc")
_loc = _MirrorMapper.get_location if self.__mirror else lambda i: i
for keyFrame in lampAnim:
frame = keyFrame.frame_number + self.__frame_margin
self.__keyframe_insert(color_action.fcurves, "color", frame, Vector(keyFrame.color))
self.__keyframe_insert(location_action.fcurves, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1)
for fcurve in location_action.fcurves:
self.detectLampChange(fcurve)
self.__assign_action(lampObj.data, color_action)
self.__assign_action(lampObj, location_action)
def assign(self, obj, action_name=None):
if obj is None:
return
if action_name is None:
action_name = os.path.splitext(os.path.basename(self.__vmdFile.filepath))[0]
if MMDCamera.isMMDCamera(obj):
self.__assignToCamera(obj, action_name + "_camera")
elif MMDLamp.isMMDLamp(obj):
self.__assignToLamp(obj, action_name + "_lamp")
elif getattr(obj.data, "shape_keys", None):
self.__assignToMesh(obj, action_name + "_facial")
elif obj.type == "ARMATURE":
self.__assignToArmature(obj, action_name + "_bone")
elif obj.type == "CAMERA" and self.__convert_mmd_camera:
self.__assignToCamera(obj, action_name + "_camera")
elif obj.type == "LAMP" and self.__convert_mmd_lamp:
self.__assignToLamp(obj, action_name + "_lamp")
elif obj.mmd_type == "ROOT":
self.__assignToRoot(obj, action_name + "_display")
else:
pass
+243
View File
@@ -0,0 +1,243 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
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)
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
+406
View File
@@ -0,0 +1,406 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from bpy.props import BoolProperty, StringProperty
from bpy.types import Operator
from .. import cycles_converter
from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.shader import _NodeGroupUtils
class ConvertMaterialsForCycles(Operator):
bl_idname = "mmd_tools.convert_materials_for_cycles"
bl_label = "Convert Materials For Cycles"
bl_description = "Convert materials of selected objects for Cycles."
bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.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(
name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=False,
options={"SKIP_SAVE"},
)
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == "MESH"), None)
def draw(self, context):
layout = self.layout
layout.prop(self, "use_principled")
layout.prop(self, "clean_nodes")
def execute(self, context):
try:
context.scene.render.engine = "CYCLES"
except:
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
return {"CANCELLED"}
for obj in (x for x in context.selected_objects if x.type == "MESH"):
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
return {"FINISHED"}
class ConvertMaterials(Operator):
bl_idname = "mmd_tools.convert_materials"
bl_label = "Convert Materials"
bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.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(
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(
name="Subsurface",
default=0.001,
soft_min=0.000,
soft_max=1.000,
precision=3,
options={"SKIP_SAVE"},
)
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == "MESH"), None)
def execute(self, context):
for obj in context.selected_objects:
if obj.type != "MESH":
continue
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
return {"FINISHED"}
class ConvertBSDFMaterials(Operator):
bl_idname = 'mmd_tools.convert_bsdf_materials'
bl_label = 'Convert Blender Materials'
bl_description = 'Convert materials of selected objects.'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return next((x for x in context.selected_objects if x.type == 'MESH'), None)
def execute(self, context):
for obj in context.selected_objects:
if obj.type != 'MESH':
continue
cycles_converter.convertToMMDShader(obj)
return {'FINISHED'}
class _OpenTextureBase:
"""Create a texture for mmd model material."""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
filepath: StringProperty(
name="File Path",
description="Filepath used for importing the file",
maxlen=1024,
subtype="FILE_PATH",
)
use_filter_image: BoolProperty(
default=True,
options={"HIDDEN"},
)
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class OpenTexture(Operator, _OpenTextureBase):
bl_idname = "mmd_tools.material_open_texture"
bl_label = "Open Texture"
bl_description = "Create main texture of active material"
def execute(self, context):
mat = context.active_object.active_material
fnMat = FnMaterial(mat)
fnMat.create_texture(self.filepath)
return {"FINISHED"}
class RemoveTexture(Operator):
"""Create a texture for mmd model material."""
bl_idname = "mmd_tools.material_remove_texture"
bl_label = "Remove Texture"
bl_description = "Remove main texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
mat = context.active_object.active_material
fnMat = FnMaterial(mat)
fnMat.remove_texture()
return {"FINISHED"}
class OpenSphereTextureSlot(Operator, _OpenTextureBase):
"""Create a texture for mmd model material."""
bl_idname = "mmd_tools.material_open_sphere_texture"
bl_label = "Open Sphere Texture"
bl_description = "Create sphere texture of active material"
def execute(self, context):
mat = context.active_object.active_material
fnMat = FnMaterial(mat)
fnMat.create_sphere_texture(self.filepath, context.active_object)
return {"FINISHED"}
class RemoveSphereTexture(Operator):
"""Create a texture for mmd model material."""
bl_idname = "mmd_tools.material_remove_sphere_texture"
bl_label = "Remove Sphere Texture"
bl_description = "Remove sphere texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
mat = context.active_object.active_material
fnMat = FnMaterial(mat)
fnMat.remove_sphere_texture()
return {"FINISHED"}
class MoveMaterialUp(Operator):
bl_idname = "mmd_tools.move_material_up"
bl_label = "Move Material Up"
bl_description = "Moves selected material one slot up"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
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
def execute(self, context):
obj = context.active_object
current_idx = obj.active_material_index
prev_index = current_idx - 1
try:
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
except MaterialNotFoundError:
self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"}
obj.active_material_index = prev_index
return {"FINISHED"}
class MoveMaterialDown(Operator):
bl_idname = "mmd_tools.move_material_down"
bl_label = "Move Material Down"
bl_description = "Moves the selected material one slot down"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
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
def execute(self, context):
obj = context.active_object
current_idx = obj.active_material_index
next_index = current_idx + 1
try:
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
except MaterialNotFoundError:
self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"}
obj.active_material_index = next_index
return {"FINISHED"}
class EdgePreviewSetup(Operator):
bl_idname = "mmd_tools.edge_preview_setup"
bl_label = "Edge Preview Setup"
bl_description = 'Preview toon edge settings of active model using "Solidify" modifier'
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
action: bpy.props.EnumProperty(
name="Action",
description="Select action",
items=[
("CREATE", "Create", "Create toon edge", 0),
("CLEAN", "Clean", "Clear toon edge", 1),
],
default="CREATE",
)
def execute(self, context):
from ..core.model import FnModel
root = FnModel.find_root_object(context.active_object)
if root is None:
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
if self.action == "CLEAN":
for obj in FnModel.iterate_mesh_objects(root):
self.__clean_toon_edge(obj)
else:
from ..bpyutils import Props
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))
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
return {"FINISHED"}
def __clean_toon_edge(self, obj):
if "mmd_edge_preview" in obj.modifiers:
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
if "mmd_edge_preview" in obj.vertex_groups:
obj.vertex_groups.remove(obj.vertex_groups["mmd_edge_preview"])
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
def __create_toon_edge(self, obj, scale=1.0):
self.__clean_toon_edge(obj)
materials = obj.data.materials
material_offset = len(materials)
for m in tuple(materials):
if m and m.mmd_material.enabled_toon_edge:
mat_edge = self.__get_edge_material("mmd_edge." + m.name, m.mmd_material.edge_color, materials)
materials.append(mat_edge)
elif material_offset > 1:
mat_edge = self.__get_edge_material("mmd_edge.disabled", (0, 0, 0, 0), materials)
materials.append(mat_edge)
if len(materials) > material_offset:
mod = obj.modifiers.get("mmd_edge_preview", None)
if mod is None:
mod = obj.modifiers.new("mmd_edge_preview", "SOLIDIFY")
mod.material_offset = material_offset
mod.thickness_vertex_group = 1e-3 # avoid overlapped faces
mod.use_flip_normals = True
mod.use_rim = False
mod.offset = 1
self.__create_edge_preview_group(obj)
mod.thickness = scale
mod.vertex_group = "mmd_edge_preview"
return len(materials) - material_offset
def __create_edge_preview_group(self, obj):
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 = {}
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}
vg_edge_preview = obj.vertex_groups.new(name="mmd_edge_preview")
for i, mi in {v: f.material_index for f in reversed(obj.data.polygons) for v in f.vertices}.items():
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):
if mat_name in materials:
return materials[mat_name]
mat = bpy.data.materials.get(mat_name, None)
if mat is None:
mat = bpy.data.materials.new(mat_name)
mmd_mat = mat.mmd_material
# note: edge affects ground shadow
mmd_mat.is_double_sided = mmd_mat.enabled_drop_shadow = False
mmd_mat.enabled_self_shadow_map = mmd_mat.enabled_self_shadow = False
# mmd_mat.enabled_self_shadow_map = True # for blender 2.78+ BI viewport only
mmd_mat.diffuse_color = mmd_mat.specular_color = (0, 0, 0)
mmd_mat.ambient_color = edge_color[:3]
mmd_mat.alpha = edge_color[3]
mmd_mat.edge_color = edge_color
self.__make_shader(mat)
return mat
def __make_shader(self, m):
m.use_nodes = True
nodes, links = m.node_tree.nodes, m.node_tree.links
node_shader = nodes.get("mmd_edge_preview", None)
if node_shader is None or not any(s.is_linked for s in node_shader.outputs):
XPOS, YPOS = 210, 110
nodes.clear()
node_shader = nodes.new("ShaderNodeGroup")
node_shader.name = "mmd_edge_preview"
node_shader.location = (0, 0)
node_shader.width = 200
node_shader.node_tree = self.__get_edge_preview_shader()
node_out = nodes.new("ShaderNodeOutputMaterial")
node_out.location = (XPOS * 2, YPOS * 0)
links.new(node_shader.outputs["Shader"], node_out.inputs["Surface"])
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):
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):
return shader
ng = _NodeGroupUtils(shader)
node_input = ng.new_node("NodeGroupInput", (-5, 0))
node_output = ng.new_node("NodeGroupOutput", (3, 0))
############################################################################
node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
node_color.mute = True
ng.new_input_socket("Color", node_color.inputs["Color1"])
############################################################################
node_ray = ng.new_node("ShaderNodeLightPath", (-3, 1.5))
node_geo = ng.new_node("ShaderNodeNewGeometry", (-3, 0))
node_max = ng.new_math_node("MAXIMUM", (-2, 1.5))
node_max.mute = True
node_gt = ng.new_math_node("GREATER_THAN", (-1, 1))
node_alpha = ng.new_math_node("MULTIPLY", (0, 1))
node_trans = ng.new_node("ShaderNodeBsdfTransparent", (0, 0))
node_rgb = ng.new_node("ShaderNodeBackground", (0, -0.5))
node_mix = ng.new_node("ShaderNodeMixShader", (1, 0.5))
links = ng.links
links.new(node_ray.outputs["Is Camera Ray"], node_max.inputs[0])
links.new(node_ray.outputs["Is Glossy Ray"], node_max.inputs[1])
links.new(node_max.outputs["Value"], node_gt.inputs[0])
links.new(node_geo.outputs["Backfacing"], node_gt.inputs[1])
links.new(node_gt.outputs["Value"], node_alpha.inputs[0])
links.new(node_alpha.outputs["Value"], node_mix.inputs["Fac"])
links.new(node_trans.outputs["BSDF"], node_mix.inputs[1])
links.new(node_rgb.outputs[0], node_mix.inputs[2])
links.new(node_color.outputs["Color"], node_rgb.inputs["Color"])
ng.new_input_socket("Alpha", node_alpha.inputs[1])
ng.new_output_socket("Shader", node_mix.outputs["Shader"])
return shader
+310
View File
@@ -0,0 +1,310 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re
import bpy
from .. import utils
from ..bpyutils import FnContext, FnObject
from ..core.bone import FnBone
from ..core.model import FnModel, Model
from ..core.morph import FnMorph
class SelectObject(bpy.types.Operator):
bl_idname = "mmd_tools.object_select"
bl_label = "Select Object"
bl_description = "Select the object"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
name: bpy.props.StringProperty(
name="Name",
description="The object name",
default="",
options={"HIDDEN", "SKIP_SAVE"},
)
def execute(self, context):
utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"}
class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
bl_idname = "mmd_tools.object_move"
bl_label = "Move Object"
bl_description = "Move active object up/down in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
@classmethod
def set_index(cls, obj, index):
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):
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):
for i, x in enumerate(objects):
cls.set_index(x, i)
@classmethod
def poll(cls, context):
return context.active_object
def execute(self, context):
obj = context.active_object
objects = self.__get_objects(obj)
if obj not in objects:
self.report({"ERROR"}, 'Can not move object "%s"' % obj.name)
return {"CANCELLED"}
objects.sort(key=lambda x: x.name)
self.move(objects, objects.index(obj), self.type)
self.normalize_indices(objects)
return {"FINISHED"}
def __get_objects(self, obj):
class __MovableList(list):
def move(self, index_old, index_new):
item = self[index_old]
self.remove(item)
self.insert(index_new, item)
objects = []
root = FnModel.find_root_object(obj)
if root:
rig = Model(root)
if obj.mmd_type == "NONE" and obj.type == "MESH":
objects = rig.meshes()
elif obj.mmd_type == "RIGID_BODY":
objects = rig.rigidBodies()
elif obj.mmd_type == "JOINT":
objects = rig.joints()
return __MovableList(objects)
class CleanShapeKeys(bpy.types.Operator):
bl_idname = "mmd_tools.clean_shape_keys"
bl_label = "Clean Shape Keys"
bl_description = "Remove unused shape keys of selected mesh objects"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod
def __can_remove(key_block):
if key_block.relative_key == key_block:
return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data):
if v0.co != v1.co:
return False
return True
def __shape_key_clean(self, obj, key_blocks):
for kb in key_blocks:
if self.__can_remove(kb):
FnObject.mesh_remove_shape_key(obj, kb)
if len(key_blocks) == 1:
FnObject.mesh_remove_shape_key(obj, key_blocks[0])
def execute(self, context):
obj: bpy.types.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
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
return {"FINISHED"}
class SeparateByMaterials(bpy.types.Operator):
bl_idname = "mmd_tools.separate_by_materials"
bl_label = "Separate By Materials"
bl_options = {"REGISTER", "UNDO"}
clean_shape_keys: bpy.props.BoolProperty(
name="Clean Shape Keys",
description="Remove unused shape keys of separated objects",
default=True,
)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "MESH"
def __separate_by_materials(self, obj):
utils.separateByMaterials(obj)
if self.clean_shape_keys:
bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
self.__separate_by_materials(obj)
else:
bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view()
# Store the current material names
rig = Model(root)
mat_names = [getattr(mat, "name", None) for mat in rig.materials()]
self.__separate_by_materials(obj)
for mesh in rig.meshes():
FnMorph.clean_uv_morph_vertex_groups(mesh)
if len(mesh.data.materials) > 0:
mat = mesh.data.materials[0]
idx = mat_names.index(getattr(mat, "name", None))
MoveObject.set_index(mesh, idx)
for morph in root.mmd_root.material_morphs:
FnMorph(morph, rig).update_mat_related_mesh()
utils.clearUnusedMeshes()
return {"FINISHED"}
class JoinMeshes(bpy.types.Operator):
bl_idname = "mmd_tools.join_meshes"
bl_label = "Join Meshes"
bl_description = "Join the Model meshes into a single one"
bl_options = {"REGISTER", "UNDO"}
sort_shape_keys: bpy.props.BoolProperty(
name="Sort Shape Keys",
description="Sort shape keys in the order of vertex morph",
default=True,
)
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view()
# Find all the meshes in mmd_root
rig = Model(root)
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
if not meshes_list:
self.report({"ERROR"}, "The model does not have any meshes")
return {"CANCELLED"}
active_mesh = meshes_list[0]
FnContext.select_objects(context, *meshes_list)
FnContext.set_active_object(context, active_mesh)
# Store the current order of the materials
for m in meshes_list[1:]:
for mat in m.data.materials:
if mat not in active_mesh.data.materials[:]:
active_mesh.data.materials.append(mat)
# Join selected meshes
bpy.ops.object.join()
if self.sort_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:
FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
utils.clearUnusedMeshes()
return {"FINISHED"}
class AttachMeshesToMMD(bpy.types.Operator):
bl_idname = "mmd_tools.attach_meshes"
bl_label = "Attach Meshes to Model"
bl_description = "Finds existing meshes and attaches them to the selected MMD model"
bl_options = {"REGISTER", "UNDO"}
add_armature_modifier: bpy.props.BoolProperty(default=True)
def execute(self, context: bpy.types.Context):
root = FnModel.find_root_object(context.active_object)
if root is None:
self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"}
armObj = FnModel.find_armature_object(root)
if armObj is None:
self.report({"ERROR"}, "Model Armature not found")
return {"CANCELLED"}
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
return {"FINISHED"}
class ChangeMMDIKLoopFactor(bpy.types.Operator):
bl_idname = "mmd_tools.change_mmd_ik_loop_factor"
bl_label = "Change MMD IK Loop Factor"
bl_description = "Multiplier for all bones' IK iterations in Blender"
bl_options = {"REGISTER", "UNDO"}
mmd_ik_loop_factor: bpy.props.IntProperty(
name="MMD IK Loop Factor",
description="Scaling factor of MMD IK loop",
min=1,
soft_max=10,
max=100,
)
@classmethod
def poll(cls, context):
return FnModel.find_root_object(context.active_object) is not None
def invoke(self, context, event):
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):
root_object = FnModel.find_root_object(context.active_object)
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
return {"FINISHED"}
class RecalculateBoneRoll(bpy.types.Operator):
bl_idname = "mmd_tools.recalculate_bone_roll"
bl_label = "Recalculate bone roll"
bl_description = "Recalculate bone roll for arm related bones"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "ARMATURE"
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
def draw(self, context):
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):
arm = context.active_object
FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"}
+486
View File
@@ -0,0 +1,486 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from ..bpyutils import FnContext
from ..core.bone import FnBone, MigrationFnBone
from ..core.model import FnModel, Model
class MorphSliderSetup(bpy.types.Operator):
bl_idname = "mmd_tools.morph_slider_setup"
bl_label = "Morph Slider Setup"
bl_description = "Translate MMD morphs of selected object into format usable by Blender"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
type: bpy.props.EnumProperty(
name="Type",
description="Select type",
items=[
("CREATE", "Create", "Create placeholder object for morph sliders", "SHAPEKEY_DATA", 0),
("BIND", "Bind", "Bind morph sliders", "DRIVER", 1),
("UNBIND", "Unbind", "Unbind morph sliders", "X", 2),
],
default="CREATE",
)
def execute(self, context: bpy.types.Context):
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object)
if self.type == "BIND":
rig.morph_slider.bind()
elif self.type == "UNBIND":
rig.morph_slider.unbind()
else:
rig.morph_slider.create()
FnContext.set_active_object(context, active_object)
return {"FINISHED"}
class CleanRiggingObjects(bpy.types.Operator):
bl_idname = "mmd_tools.clean_rig"
bl_label = "Clean Rig"
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):
root_object = FnModel.find_root_object(context.active_object)
assert root_object is not None
rig = Model(root_object)
rig.clean()
FnContext.set_active_object(context, root_object)
return {"FINISHED"}
class BuildRig(bpy.types.Operator):
bl_idname = "mmd_tools.build_rig"
bl_label = "Build Rig"
bl_description = "Translate physics of selected object into format usable by Blender"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
non_collision_distance_scale: bpy.props.FloatProperty(
name="Non-Collision Distance Scale",
description="The distance scale for creating extra non-collision constraints while building physics",
min=0,
soft_max=10,
default=1.5,
)
collision_margin: bpy.props.FloatProperty(
name="Collision Margin",
description="The collision margin between rigid bodies. If 0, the default value for each shape is adopted.",
unit="LENGTH",
min=0,
soft_max=10,
default=1e-06,
)
def execute(self, context):
root_object = FnModel.find_root_object(context.active_object)
with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object)
rig.build(self.non_collision_distance_scale, self.collision_margin)
FnContext.set_active_object(context, root_object)
return {"FINISHED"}
class CleanAdditionalTransformConstraints(bpy.types.Operator):
bl_idname = "mmd_tools.clean_additional_transform"
bl_label = "Clean Additional Transform"
bl_description = "Delete shadow bones of selected object and revert bones to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
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))
FnContext.set_active_object(context, active_object)
return {"FINISHED"}
class ApplyAdditionalTransformConstraints(bpy.types.Operator):
bl_idname = "mmd_tools.apply_additional_transform"
bl_label = "Apply Additional Transform"
bl_description = "Translate appended bones of selected object for Blender"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
armature_object = FnModel.find_armature_object(root_object)
assert armature_object is not None
MigrationFnBone.fix_mmd_ik_limit_override(armature_object)
FnBone.apply_additional_transformation(armature_object)
FnContext.set_active_object(context, active_object)
return {"FINISHED"}
class SetupBoneFixedAxes(bpy.types.Operator):
bl_idname = "mmd_tools.bone_fixed_axis_setup"
bl_label = "Setup Bone Fixed Axis"
bl_description = "Setup fixed axis of selected bones"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
type: bpy.props.EnumProperty(
name="Type",
description="Select type",
items=[
("DISABLE", "Disable", "Disable MMD fixed axis of selected bones", 0),
("LOAD", "Load", "Load/Enable MMD fixed axis of selected bones from their Y-axis or the only rotatable axis", 1),
("APPLY", "Apply", "Align bone axes to MMD fixed axis of each bone", 2),
],
default="LOAD",
)
def execute(self, context):
armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object")
return {"CANCELLED"}
if self.type == "APPLY":
FnBone.apply_bone_fixed_axis(armature_object)
FnBone.apply_additional_transformation(armature_object)
else:
FnBone.load_bone_fixed_axis(armature_object, enable=(self.type == "LOAD"))
return {"FINISHED"}
class SetupBoneLocalAxes(bpy.types.Operator):
bl_idname = "mmd_tools.bone_local_axes_setup"
bl_label = "Setup Bone Local Axes"
bl_description = "Setup local axes of each bone"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
type: bpy.props.EnumProperty(
name="Type",
description="Select type",
items=[
("DISABLE", "Disable", "Disable MMD local axes of selected bones", 0),
("LOAD", "Load", "Load/Enable MMD local axes of selected bones from their bone axes", 1),
("APPLY", "Apply", "Align bone axes to MMD local axes of each bone", 2),
],
default="LOAD",
)
def execute(self, context):
armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object")
return {"CANCELLED"}
if self.type == "APPLY":
FnBone.apply_bone_local_axes(armature_object)
FnBone.apply_additional_transformation(armature_object)
else:
FnBone.load_bone_local_axes(armature_object, enable=(self.type == "LOAD"))
return {"FINISHED"}
class AddMissingVertexGroupsFromBones(bpy.types.Operator):
bl_idname = "mmd_tools.add_missing_vertex_groups_from_bones"
bl_label = "Add Missing Vertex Groups from Bones"
bl_description = "Add the missing vertex groups to the selected mesh"
bl_options = {"REGISTER", "UNDO"}
search_in_all_meshes: bpy.props.BoolProperty(
name="Search in all meshes",
description="Search for vertex groups in all meshes",
default=False,
)
@classmethod
def poll(cls, context: bpy.types.Context):
return FnModel.find_root_object(context.active_object) is not None
def execute(self, context: bpy.types.Context):
active_object: bpy.types.Object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object)
if bone_order_mesh_object is None:
return {"CANCELLED"}
FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes)
return {"FINISHED"}
class CreateMMDModelRoot(bpy.types.Operator):
bl_idname = "mmd_tools.create_mmd_model_root_object"
bl_label = "Create a MMD Model Root Object"
bl_description = "Create a MMD model root object with a basic armature"
bl_options = {"REGISTER", "UNDO"}
name_j: bpy.props.StringProperty(
name="Name",
description="The name of the MMD model",
default="New MMD Model",
)
name_e: bpy.props.StringProperty(
name="Name(Eng)",
description="The english name of the MMD model",
default="New MMD Model",
)
scale: bpy.props.FloatProperty(
name="Scale",
description="Scale",
default=0.08,
)
def execute(self, context):
rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True)
rig.initialDisplayFrames()
return {"FINISHED"}
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
class ConvertToMMDModel(bpy.types.Operator):
bl_idname = "mmd_tools.convert_to_mmd_model"
bl_label = "Convert to a MMD Model"
bl_description = "Convert active armature with its meshes to a MMD model (experimental)"
bl_options = {"REGISTER", "UNDO"}
ambient_color_source: bpy.props.EnumProperty(
name="Ambient Color Source",
description="Select ambient color source",
items=[
("DIFFUSE", "Diffuse", "Diffuse color", 0),
("MIRROR", "Mirror", 'Mirror color (if property "mirror_color" is available)', 1),
],
default="DIFFUSE",
)
edge_threshold: bpy.props.FloatProperty(
name="Edge Threshold",
description="MMD toon edge will not be enabled if freestyle line color alpha less than this value",
min=0,
max=1.001,
precision=3,
step=0.1,
default=0.1,
)
edge_alpha_min: bpy.props.FloatProperty(
name="Minimum Edge Alpha",
description="Minimum alpha of MMD toon edge color",
min=0,
max=1,
precision=3,
step=0.1,
default=0.5,
)
scale: bpy.props.FloatProperty(
name="Scale",
description="Scaling factor for converting the model",
default=0.08,
)
convert_material_nodes: bpy.props.BoolProperty(
name="Convert Material Nodes",
default=True,
)
middle_joint_bones_lock: bpy.props.BoolProperty(
name="Middle Joint Bones Lock",
description="Lock specific bones for backward compatibility.",
default=False,
)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type == "ARMATURE" and obj.mode != "EDIT"
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
def execute(self, context):
# TODO convert some basic MMD properties
armature_object = context.active_object
scale = self.scale
model_name = "New MMD Model"
root_object = FnModel.find_root_object(armature_object)
if root_object is None or root_object != armature_object.parent:
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):
if mesh.parent is None:
return False
return mesh.parent == armature_object or __is_child_of_armature(mesh.parent)
def __is_using_armature(mesh):
for m in mesh.modifiers:
if m.type == "ARMATURE" and m.object == armature_object:
return True
return False
def __get_root(mesh):
if mesh.parent is None:
return mesh
return __get_root(mesh.parent)
for x in objects:
if __is_using_armature(x) and not __is_child_of_armature(x):
x_root = __get_root(x)
m = x_root.matrix_world
x_root.parent_type = "OBJECT"
x_root.parent = armature_object
x_root.matrix_world = m
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model):
root_object = mmd_model.rootObject()
armature_object = mmd_model.armature()
mesh_objects = tuple(mmd_model.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}
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)
from ..core.material import FnMaterial
FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes)
try:
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
if self.ambient_color_source == "MIRROR" and hasattr(m, "mirror_color"):
mmd_material.ambient_color = m.mirror_color
else:
mmd_material.ambient_color = [0.5 * c for c in mmd_material.diffuse_color]
if hasattr(m, "line_color"): # freestyle line color
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)]
finally:
FnMaterial.set_nodes_are_readonly(False)
from .display_item import DisplayItemQuickSetup
FnBone.sync_display_item_frames_from_bone_collections(armature_object)
mmd_model.initialDisplayFrames(reset=False) # ensure default frames
DisplayItemQuickSetup.load_facial_items(root_object.mmd_root)
root_object.mmd_root.active_display_item_frame = 0
class ResetObjectVisibility(bpy.types.Operator):
bl_idname = "mmd_tools.reset_object_visibility"
bl_label = "Reset Object Visivility"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context: bpy.types.Context):
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):
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
mmd_root_object.hide_set(False)
rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object)
if rigid_group_object:
rigid_group_object.hide_set(True)
joint_group_object = FnModel.find_joint_group_object(mmd_root_object)
if joint_group_object:
joint_group_object.hide_set(True)
temporary_group_object = FnModel.find_temporary_group_object(mmd_root_object)
if temporary_group_object:
temporary_group_object.hide_set(True)
mmd_root.show_meshes = True
mmd_root.show_armature = True
mmd_root.show_temporary_objects = False
mmd_root.show_rigid_bodies = False
mmd_root.show_names_of_rigid_bodies = False
mmd_root.show_joints = False
mmd_root.show_names_of_joints = False
return {"FINISHED"}
class AssembleAll(bpy.types.Operator):
bl_idname = "mmd_tools.assemble_all"
bl_label = "Assemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
rig = Model(root_object)
MigrationFnBone.fix_mmd_ik_limit_override(rig.armature())
FnBone.apply_additional_transformation(rig.armature())
rig.build()
rig.morph_slider.bind()
with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_bind()
root_object.mmd_root.use_property_driver = True
FnContext.set_active_object(context, active_object)
return {"FINISHED"}
class DisassembleAll(bpy.types.Operator):
bl_idname = "mmd_tools.disassemble_all"
bl_label = "Disassemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
active_object = context.active_object
root_object = FnModel.find_root_object(active_object)
assert root_object is not None
with FnContext.temp_override_active_layer_collection(context, root_object) as context:
root_object.mmd_root.use_property_driver = False
with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_unbind()
rig = Model(root_object)
rig.morph_slider.unbind()
rig.clean()
FnBone.clean_additional_transformation(rig.armature())
FnContext.set_active_object(context, active_object)
return {"FINISHED"}
+313
View File
@@ -0,0 +1,313 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import itertools
from operator import itemgetter
from typing import Dict, List, Optional, Set
import bmesh
import bpy
from ..bpyutils import FnContext
from ..core.model import FnModel, Model
class MessageException(Exception):
"""Class for error with message."""
class ModelJoinByBonesOperator(bpy.types.Operator):
bl_idname = "mmd_tools.model_join_by_bones"
bl_label = "Model Join by Bones"
bl_options = {"REGISTER", "UNDO"}
join_type: bpy.props.EnumProperty(
name="Join Type",
items=[
("CONNECTED", "Connected", ""),
("OFFSET", "Keep Offset", ""),
],
default="OFFSET",
)
@classmethod
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
if context.mode != "POSE":
return False
if active_object is None:
return False
if active_object.type != "ARMATURE":
return False
if len(list(filter(lambda o: o.type == "ARMATURE", context.selected_objects))) < 2:
return False
return len(context.selected_pose_bones) > 0
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context):
try:
self.join(context)
except MessageException as ex:
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def join(self, context: bpy.types.Context):
bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_object)
child_root_objects = {FnModel.find_root_object(o) for o in context.selected_objects}
child_root_objects.remove(parent_root_object)
if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("No MMD Models selected")
with FnContext.temp_override_active_layer_collection(context, parent_root_object):
FnModel.join_models(parent_root_object, child_root_objects)
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.armature.parent_set(type="OFFSET")
# 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)
child_edit_bones.remove(parent_edit_bone)
child_edit_bone: bpy.types.EditBone
for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True
bpy.ops.object.mode_set(mode="POSE")
class ModelSeparateByBonesOperator(bpy.types.Operator):
bl_idname = "mmd_tools.model_separate_by_bones"
bl_label = "Model Separate by Bones"
bl_options = {"REGISTER", "UNDO"}
separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True)
include_descendant_bones: bpy.props.BoolProperty(name="Include Descendant Bones", default=True)
weight_threshold: bpy.props.FloatProperty(name="Weight Threshold", default=0.001, min=0.0, max=1.0, precision=4, subtype="FACTOR")
boundary_joint_owner: bpy.props.EnumProperty(
name="Boundary Joint Owner",
items=[
("SOURCE", "Source Model", ""),
("DESTINATION", "Destination Model", ""),
],
default="DESTINATION",
)
@classmethod
def poll(cls, context: bpy.types.Context):
active_object: Optional[bpy.types.Object] = context.active_object
if context.mode != "POSE":
return False
if active_object is None:
return False
if active_object.type != "ARMATURE":
return False
if FnModel.find_root_object(active_object) is None:
return False
return len(context.selected_pose_bones) > 0
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context):
try:
self.separate(context)
except MessageException as ex:
self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"}
return {"FINISHED"}
def separate(self, context: bpy.types.Context):
weight_threshold: float = self.weight_threshold
mmd_scale = 0.08
target_armature_object: bpy.types.Object = context.active_object
bpy.ops.object.mode_set(mode="EDIT")
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
if self.include_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}
mmd_root_object: bpy.types.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(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
# separate armature bones
separate_armature_object: Optional[bpy.types.Object]
if self.separate_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)
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}
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints
separate_joints: Set[bpy.types.Object] = {
joint_object
for joint_object in mmd_model.joints()
if boundary_joint_owner_condition(
[
joint_object.rigid_body_constraint.object1 in separate_rigid_bodies,
joint_object.rigid_body_constraint.object2 in separate_rigid_bodies,
]
)
}
separate_mesh_objects: Set[bpy.types.Object]
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object]
if len(mmd_model_mesh_objects) == 0:
separate_mesh_objects = set()
model2separate_mesh_objects = dict()
else:
# select meshes
obj: bpy.types.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
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]
bpy.ops.object.mode_set(mode="OBJECT")
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
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()
if self.separate_armature:
with context.temp_override(
active_object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
):
bpy.ops.object.join()
# add mesh
with context.temp_override(
object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object
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:
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
armature_modifier.object = separate_model_armature_object
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)
with context.temp_override(
object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# move separate objects to new collection
mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object)
assert mmd_layer_collection is not None
separate_layer_collection = FnContext.find_user_layer_collection_by_object(context, separate_root_object)
assert separate_layer_collection is not None
if mmd_layer_collection.name != 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)
FnModel.copy_mmd_root(
separate_root_object,
mmd_root_object,
overwrite=True,
replace_name2values={
# replace related_mesh property values
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()}
},
)
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()
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
target_bmesh.from_mesh(mesh, face_normals=False)
target_bmesh.select_mode |= {"VERT"}
deform_layer = target_bmesh.verts.layers.deform.verify()
selected_vertex_count = 0
vert: bmesh.types.BMVert
for vert in target_bmesh.verts:
vert.select_set(False)
# Find the largest weight vertex group
weights = [(group_index, weight) for group_index, weight in vert[deform_layer].items() if vertex_groups[group_index].name in deform_bones]
weights.sort(key=lambda i: vertex_groups[i[0]].name in separate_bones, reverse=True)
weights.sort(key=itemgetter(1), reverse=True)
group_index, weight = next(iter(weights), (0, -1))
if weight < weight_threshold:
continue
if vertex_groups[group_index].name not in separate_bones:
continue
selected_vertex_count += 1
vert.select_set(True)
if selected_vertex_count > 0:
mesh2selected_vertex_count[mesh_object] = selected_vertex_count
target_bmesh.select_flush_mode()
target_bmesh.to_mesh(mesh)
target_bmesh.clear()
return mesh2selected_vertex_count
+776
View File
@@ -0,0 +1,776 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, cast
import bpy
from mathutils import Quaternion, Vector
from ..core.model import FnModel
from .. import bpyutils, utils
from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial
from ..core.morph import FnMorph
from ..utils import ItemMoveOp, ItemOp
# Util functions
def divide_vector_components(vec1, vec2):
if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components")
result = []
for v1, v2 in zip(vec1, vec2):
if v2 == 0:
if v1 == 0:
v2 = 1 # If we have a 0/0 case we change the divisor to 1
else:
raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero")
result.append(v1 / v2)
return result
def multiply_vector_components(vec1, vec2):
if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components")
result = []
for v1, v2 in zip(vec1, vec2):
result.append(v1 * v2)
return result
def special_division(n1, n2):
"""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:
n2 = 1
else:
raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero")
return n1 / n2
class AddMorph(bpy.types.Operator):
bl_idname = "mmd_tools.morph_add"
bl_label = "Add Morph"
bl_description = "Add a morph item to active morph list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
morph_type = mmd_root.active_morph_type
morphs = getattr(mmd_root, morph_type)
morph, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph)
morph.name = "New Morph"
if morph_type.startswith("uv"):
morph.data_type = "VERTEX_GROUP"
return {"FINISHED"}
class RemoveMorph(bpy.types.Operator):
bl_idname = "mmd_tools.morph_remove"
bl_label = "Remove Morph"
bl_description = "Remove morph item(s) from the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
all: bpy.props.BoolProperty(
name="All",
description="Delete all morph items",
default=False,
options={"SKIP_SAVE"},
)
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
morph_type = mmd_root.active_morph_type
if morph_type.startswith("material"):
bpy.ops.mmd_tools.clear_temp_materials()
elif morph_type.startswith("uv"):
bpy.ops.mmd_tools.clear_uv_morph_view()
morphs = getattr(mmd_root, morph_type)
if self.all:
morphs.clear()
mmd_root.active_morph = 0
else:
morphs.remove(mmd_root.active_morph)
mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
return {"FINISHED"}
class MoveMorph(bpy.types.Operator, ItemMoveOp):
bl_idname = "mmd_tools.morph_move"
bl_label = "Move Morph"
bl_description = "Move active morph item up/down in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
mmd_root.active_morph = self.move(
getattr(mmd_root, mmd_root.active_morph_type),
mmd_root.active_morph,
self.type,
)
return {"FINISHED"}
class CopyMorph(bpy.types.Operator):
bl_idname = "mmd_tools.morph_copy"
bl_label = "Copy Morph"
bl_description = "Make a copy of active morph in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
mmd_root = root.mmd_root
morph_type = mmd_root.active_morph_type
morphs = getattr(mmd_root, morph_type)
morph = ItemOp.get_by_index(morphs, mmd_root.active_morph)
if morph is None:
return {"CANCELLED"}
name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer())
if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root):
FnMorph.copy_shape_key(obj, name_orig, name_tmp)
elif morph_type.startswith("uv"):
if morph.data_type == "VERTEX_GROUP":
for obj in FnModel.iterate_mesh_objects(root):
FnMorph.copy_uv_morph_vertex_groups(obj, name_orig, name_tmp)
morph_new, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph)
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
return {"FINISHED"}
class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
bl_idname = "mmd_tools.morph_overwrite_from_active_action_pose"
bl_label = "Overwrite Bone Morphs from active Action Pose"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
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):
root = FnModel.find_root_object(context.active_object)
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
return {"FINISHED"}
class AddMorphOffset(bpy.types.Operator):
bl_idname = "mmd_tools.morph_offset_add"
bl_label = "Add Morph Offset"
bl_description = "Add a morph offset item to the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
morph_type = mmd_root.active_morph_type
morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph)
if morph is None:
return {"CANCELLED"}
item, morph.active_data = ItemOp.add_after(morph.data, morph.active_data)
if morph_type.startswith("material"):
if obj.type == "MESH" and obj.mmd_type == "NONE":
item.related_mesh = obj.data.name
active_material = obj.active_material
if active_material and "_temp" not in active_material.name:
item.material = active_material.name
elif morph_type.startswith("bone"):
pose_bone = context.active_pose_bone
if pose_bone:
item.bone = pose_bone.name
item.location = pose_bone.location
item.rotation = pose_bone.rotation_quaternion
return {"FINISHED"}
class RemoveMorphOffset(bpy.types.Operator):
bl_idname = "mmd_tools.morph_offset_remove"
bl_label = "Remove Morph Offset"
bl_description = "Remove morph offset item(s) from the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
all: bpy.props.BoolProperty(
name="All",
description="Delete all morph offset items",
default=False,
options={"SKIP_SAVE"},
)
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
mmd_root = root.mmd_root
morph_type = mmd_root.active_morph_type
morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph)
if morph is None:
return {"CANCELLED"}
if morph_type.startswith("material"):
bpy.ops.mmd_tools.clear_temp_materials()
if self.all:
if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root):
FnMorph.remove_shape_key(obj, 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)
return {"FINISHED"}
morph.data.clear()
morph.active_data = 0
else:
morph.data.remove(morph.active_data)
morph.active_data = max(0, morph.active_data - 1)
return {"FINISHED"}
class InitMaterialOffset(bpy.types.Operator):
bl_idname = "mmd_tools.material_morph_offset_init"
bl_label = "Init Material Offset"
bl_description = "Set all offset values to target value"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
target_value: bpy.props.FloatProperty(
name="Target Value",
description="Target value",
default=0,
)
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
morph = mmd_root.material_morphs[mmd_root.active_morph]
mat_data = morph.data[morph.active_data]
val = self.target_value
mat_data.diffuse_color = mat_data.edge_color = (val,) * 4
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
return {"FINISHED"}
class ApplyMaterialOffset(bpy.types.Operator):
bl_idname = "mmd_tools.apply_material_morph_offset"
bl_label = "Apply Material Offset"
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
morph = mmd_root.material_morphs[mmd_root.active_morph]
mat_data = morph.data[morph.active_data]
if not mat_data.related_mesh:
self.report({"ERROR"}, "You need to choose a Related Mesh first")
return {"CANCELLED"}
meshObj = FnModel.find_mesh_object_by_name(morph.id_data, mat_data.related_mesh)
if meshObj is None:
self.report({"ERROR"}, "The model mesh can't be found")
return {"CANCELLED"}
try:
work_mat_name = mat_data.material + "_temp"
work_mat, base_mat = FnMaterial.swap_materials(meshObj, work_mat_name, mat_data.material)
except MaterialNotFoundError:
self.report({"ERROR"}, "Material not found")
return {"CANCELLED"}
base_mmd_mat = base_mat.mmd_material
work_mmd_mat = work_mat.mmd_material
if mat_data.offset_type == "MULT":
try:
diffuse_offset = divide_vector_components(work_mmd_mat.diffuse_color, base_mmd_mat.diffuse_color) + [special_division(work_mmd_mat.alpha, base_mmd_mat.alpha)]
specular_offset = divide_vector_components(work_mmd_mat.specular_color, base_mmd_mat.specular_color)
edge_offset = divide_vector_components(work_mmd_mat.edge_color, base_mmd_mat.edge_color)
mat_data.diffuse_color = diffuse_offset
mat_data.specular_color = specular_offset
mat_data.shininess = special_division(work_mmd_mat.shininess, base_mmd_mat.shininess)
mat_data.ambient_color = divide_vector_components(work_mmd_mat.ambient_color, base_mmd_mat.ambient_color)
mat_data.edge_color = edge_offset
mat_data.edge_weight = special_division(work_mmd_mat.edge_weight, base_mmd_mat.edge_weight)
except ZeroDivisionError:
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
except ValueError:
self.report({"ERROR"}, "An unexpected error happened")
# We should stop on our tracks and re-raise the exception
raise
if mat_data.offset_type == "ADD":
diffuse_offset = list(work_mmd_mat.diffuse_color - base_mmd_mat.diffuse_color) + [work_mmd_mat.alpha - base_mmd_mat.alpha]
specular_offset = list(work_mmd_mat.specular_color - base_mmd_mat.specular_color)
edge_offset = Vector(work_mmd_mat.edge_color) - Vector(base_mmd_mat.edge_color)
mat_data.diffuse_color = diffuse_offset
mat_data.specular_color = specular_offset
mat_data.shininess = work_mmd_mat.shininess - base_mmd_mat.shininess
mat_data.ambient_color = work_mmd_mat.ambient_color - base_mmd_mat.ambient_color
mat_data.edge_color = list(edge_offset)
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)
return {"FINISHED"}
class CreateWorkMaterial(bpy.types.Operator):
bl_idname = "mmd_tools.create_work_material"
bl_label = "Create Work Material"
bl_description = "Creates a temporary material to edit this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
morph = mmd_root.material_morphs[mmd_root.active_morph]
mat_data = morph.data[morph.active_data]
if not mat_data.related_mesh:
self.report({"ERROR"}, "You need to choose a Related Mesh first")
return {"CANCELLED"}
meshObj = FnModel.find_mesh_object_by_name(morph.id_data, mat_data.related_mesh)
if meshObj is None:
self.report({"ERROR"}, "The model mesh can't be found")
return {"CANCELLED"}
base_mat = meshObj.data.materials.get(mat_data.material, None)
if base_mat is None:
self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material)
return {"CANCELLED"}
work_mat_name = base_mat.name + "_temp"
if work_mat_name in bpy.data.materials:
self.report({"ERROR"}, 'Temporary material "%s" is in use' % work_mat_name)
return {"CANCELLED"}
work_mat = base_mat.copy()
work_mat.name = work_mat_name
meshObj.data.materials.append(work_mat)
FnMaterial.swap_materials(meshObj, base_mat.name, work_mat.name)
base_mmd_mat = base_mat.mmd_material
work_mmd_mat = work_mat.mmd_material
work_mmd_mat.material_id = -1
# Apply the offsets
if mat_data.offset_type == "MULT":
diffuse_offset = multiply_vector_components(base_mmd_mat.diffuse_color, mat_data.diffuse_color[0:3])
specular_offset = multiply_vector_components(base_mmd_mat.specular_color, mat_data.specular_color)
edge_offset = multiply_vector_components(base_mmd_mat.edge_color, mat_data.edge_color)
ambient_offset = multiply_vector_components(base_mmd_mat.ambient_color, mat_data.ambient_color)
work_mmd_mat.diffuse_color = diffuse_offset
work_mmd_mat.alpha *= mat_data.diffuse_color[3]
work_mmd_mat.specular_color = specular_offset
work_mmd_mat.shininess *= mat_data.shininess
work_mmd_mat.ambient_color = ambient_offset
work_mmd_mat.edge_color = edge_offset
work_mmd_mat.edge_weight *= mat_data.edge_weight
elif mat_data.offset_type == "ADD":
diffuse_offset = Vector(base_mmd_mat.diffuse_color) + Vector(mat_data.diffuse_color[0:3])
specular_offset = Vector(base_mmd_mat.specular_color) + Vector(mat_data.specular_color)
edge_offset = Vector(base_mmd_mat.edge_color) + Vector(mat_data.edge_color)
ambient_offset = Vector(base_mmd_mat.ambient_color) + Vector(mat_data.ambient_color)
work_mmd_mat.diffuse_color = list(diffuse_offset)
work_mmd_mat.alpha += mat_data.diffuse_color[3]
work_mmd_mat.specular_color = list(specular_offset)
work_mmd_mat.shininess += mat_data.shininess
work_mmd_mat.ambient_color = list(ambient_offset)
work_mmd_mat.edge_color = list(edge_offset)
work_mmd_mat.edge_weight += mat_data.edge_weight
return {"FINISHED"}
class ClearTempMaterials(bpy.types.Operator):
bl_idname = "mmd_tools.clear_temp_materials"
bl_label = "Clear Temp Materials"
bl_description = "Clears all the temporary materials"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
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):
if m and "_temp" in m.name:
base_mat_name = m.name.split("_temp")[0]
try:
FnMaterial.swap_materials(meshObj, m.name, base_mat_name)
return True
except MaterialNotFoundError:
self.report({"WARNING"}, "Base material for %s was not found" % m.name)
return False
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
return {"FINISHED"}
class ViewBoneMorph(bpy.types.Operator):
bl_idname = "mmd_tools.view_bone_morph"
bl_label = "View Bone Morph"
bl_description = "View the result of active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
mmd_root = root.mmd_root
armature = FnModel.find_armature_object(root)
utils.selectSingleBone(context, armature, None, True)
morph = mmd_root.bone_morphs[mmd_root.active_morph]
for morph_data in morph.data:
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
if p_bone:
p_bone.bone.select = True
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
return {"FINISHED"}
class ClearBoneMorphView(bpy.types.Operator):
bl_idname = "mmd_tools.clear_bone_morph_view"
bl_label = "Clear Bone Morph View"
bl_description = "Reset transforms of all bones to their default values"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
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()
return {"FINISHED"}
class ApplyBoneMorph(bpy.types.Operator):
bl_idname = "mmd_tools.apply_bone_morph"
bl_label = "Apply Bone Morph"
bl_description = "Apply current pose to active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
armature = FnModel.find_armature_object(root)
mmd_root = root.mmd_root
morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph.data.clear()
morph.active_data = 0
for p_bone in armature.pose.bones:
if p_bone.location.length > 0 or p_bone.matrix_basis.decompose()[1].angle > 0:
item = morph.data.add()
item.bone = p_bone.name
item.location = p_bone.location
item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
p_bone.bone.select = True
else:
p_bone.bone.select = False
return {"FINISHED"}
class SelectRelatedBone(bpy.types.Operator):
bl_idname = "mmd_tools.select_bone_morph_offset_bone"
bl_label = "Select Related Bone"
bl_description = "Select the bone assigned to this offset in the armature"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
mmd_root = root.mmd_root
armature = FnModel.find_armature_object(root)
morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph_data = morph.data[morph.active_data]
utils.selectSingleBone(context, armature, morph_data.bone)
return {"FINISHED"}
class EditBoneOffset(bpy.types.Operator):
bl_idname = "mmd_tools.edit_bone_morph_offset"
bl_label = "Edit Related Bone"
bl_description = "Applies the location and rotation of this offset to the bone"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
mmd_root = root.mmd_root
armature = FnModel.find_armature_object(root)
morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph_data = morph.data[morph.active_data]
p_bone = armature.pose.bones[morph_data.bone]
mtx = Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix().to_4x4()
mtx.translation = morph_data.location
p_bone.matrix_basis = mtx
utils.selectSingleBone(context, armature, p_bone.name)
return {"FINISHED"}
class ApplyBoneOffset(bpy.types.Operator):
bl_idname = "mmd_tools.apply_bone_morph_offset"
bl_label = "Apply Bone Morph Offset"
bl_description = "Stores the current bone location and rotation into this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
mmd_root = root.mmd_root
armature = FnModel.find_armature_object(root)
assert armature is not None
morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph_data = morph.data[morph.active_data]
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()
return {"FINISHED"}
class ViewUVMorph(bpy.types.Operator):
bl_idname = "mmd_tools.view_uv_morph"
bl_label = "View UV Morph"
bl_description = "View the result of active UV morph on current mesh object"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
mmd_root = root.mmd_root
meshes = tuple(FnModel.iterate_mesh_objects(root))
if len(meshes) == 1:
obj = meshes[0]
elif obj not in meshes:
self.report({"ERROR"}, "Please select a mesh object")
return {"CANCELLED"}
meshObj = obj
bpy.ops.mmd_tools.clear_uv_morph_view()
selected = meshObj.select_get()
with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data)
morph = mmd_root.uv_morphs[mmd_root.active_morph]
uv_textures = mesh.uv_layers
base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")]
if morph.uv_index >= len(base_uv_layers):
self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index)
return {"CANCELLED"}
uv_layer_name = base_uv_layers[morph.uv_index].name
if morph.uv_index == 0 or uv_textures.active.name not in {uv_layer_name, "_" + uv_layer_name}:
uv_textures.active = uv_textures[uv_layer_name]
uv_layer_name = uv_textures.active.name
uv_tex = uv_textures.new(name="__uv.%s" % uv_layer_name)
if uv_tex is None:
self.report({"ERROR"}, "Failed to create a temporary uv layer")
return {"CANCELLED"}
offsets = FnMorph.get_uv_morph_offset_map(meshObj, morph).items()
offsets = {k: getattr(Vector(v), "zw" if uv_layer_name.startswith("_") else "xy") for k, v in offsets}
if len(offsets) > 0:
base_uv_data = mesh.uv_layers.active.data
temp_uv_data = mesh.uv_layers[uv_tex.name].data
for i, l in enumerate(mesh.loops):
select = temp_uv_data[i].select = l.vertex_index in offsets
if select:
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index]
uv_textures.active = uv_tex
uv_tex.active_render = True
meshObj.hide_set(False)
meshObj.select_set(selected)
return {"FINISHED"}
class ClearUVMorphView(bpy.types.Operator):
bl_idname = "mmd_tools.clear_uv_morph_view"
bl_label = "Clear UV Morph View"
bl_description = "Clear all temporary data of UV morphs"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
assert root is not None
for m in FnModel.iterate_mesh_objects(root):
mesh = m.data
uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers)
for t in uv_textures:
if t.name.startswith("__uv."):
uv_textures.remove(t)
if len(uv_textures) > 0:
uv_textures[0].active_render = True
uv_textures.active_index = 0
animation_data = mesh.animation_data
if animation_data:
nla_tracks = animation_data.nla_tracks
for t in nla_tracks:
if t.name.startswith("__uv."):
nla_tracks.remove(t)
if animation_data.action and animation_data.action.name.startswith("__uv."):
animation_data.action = None
if animation_data.action is None and len(nla_tracks) == 0:
mesh.animation_data_clear()
for act in bpy.data.actions:
if act.name.startswith("__uv.") and act.users < 1:
bpy.data.actions.remove(act)
return {"FINISHED"}
class EditUVMorph(bpy.types.Operator):
bl_idname = "mmd_tools.edit_uv_morph"
bl_label = "Edit UV Morph"
bl_description = "Edit UV morph on a temporary UV layer (use UV Editor to edit the result)"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
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):
obj = context.active_object
meshObj = obj
selected = meshObj.select_get()
with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data)
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_mode(type="VERT", action="ENABLE")
bpy.ops.mesh.reveal() # unhide all vertices
bpy.ops.mesh.select_all(action="DESELECT")
bpy.ops.object.mode_set(mode="OBJECT")
vertices = mesh.vertices
for l, d in zip(mesh.loops, mesh.uv_layers.active.data):
if d.select:
vertices[l.vertex_index].select = True
polygons = mesh.polygons
polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active)
bpy.ops.object.mode_set(mode="EDIT")
meshObj.select_set(selected)
return {"FINISHED"}
class ApplyUVMorph(bpy.types.Operator):
bl_idname = "mmd_tools.apply_uv_morph"
bl_label = "Apply UV Morph"
bl_description = "Calculate the UV offsets of selected vertices and apply to active UV morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod
def poll(cls, context):
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):
obj = context.active_object
root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root
meshObj = obj
selected = meshObj.select_get()
with bpyutils.select_object(meshObj):
mesh = cast(bpy.types.Mesh, meshObj.data)
morph = mmd_root.uv_morphs[mmd_root.active_morph]
base_uv_name = mesh.uv_layers.active.name[5:]
if base_uv_name not in mesh.uv_layers:
self.report({"ERROR"}, ' * UV map "%s" not found' % base_uv_name)
return {"CANCELLED"}
base_uv_data = mesh.uv_layers[base_uv_name].data
temp_uv_data = mesh.uv_layers.active.data
axis_type = "ZW" if base_uv_name.startswith("_") else "XY"
from collections import namedtuple
__OffsetData = namedtuple("OffsetData", "index, offset")
offsets = {}
vertices = mesh.vertices
for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data):
if vertices[l.vertex_index].select and l.vertex_index not in offsets:
dx, dy = i1.uv - i0.uv
if abs(dx) > 0.0001 or abs(dy) > 0.0001:
offsets[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy))
FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type)
morph.data_type = "VERTEX_GROUP"
meshObj.select_set(selected)
return {"FINISHED"}
class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
bl_idname = "mmd_tools.clean_duplicated_material_morphs"
bl_label = "Clean Duplicated Material Morphs"
bl_description = "Clean duplicated material morphs"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return FnModel.find_root_object(context.active_object) is not None
def execute(self, context: bpy.types.Context):
mmd_root_object = FnModel.find_root_object(context.active_object)
FnMorph.clean_duplicated_material_morphs(mmd_root_object)
return {"FINISHED"}
+579
View File
@@ -0,0 +1,579 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math
from typing import Dict, Optional, Tuple, cast
import bpy
from mathutils import Euler, Vector
from .. import utils
from ..bpyutils import FnContext, Props
from ..core import rigid_body
from ..core.model import FnModel, Model
from ..core.rigid_body import FnRigidBody
class SelectRigidBody(bpy.types.Operator):
bl_idname = "mmd_tools.rigid_body_select"
bl_label = "Select Rigid Body"
bl_description = "Select similar rigidbody objects which have the same property values with active rigidbody object"
bl_options = {"REGISTER", "UNDO"}
properties: bpy.props.EnumProperty(
name="Properties",
description="Select the properties to be compared",
options={"ENUM_FLAG"},
items=[
("collision_group_number", "Collision Group", "Collision group", 1),
("collision_group_mask", "Collision Group Mask", "Collision group mask", 2),
("type", "Rigid Type", "Rigid type", 4),
("shape", "Shape", "Collision shape", 8),
("bone", "Bone", "Target bone", 16),
],
default=set(),
)
hide_others: bpy.props.BoolProperty(
name="Hide Others",
description="Hide the rigidbody object which does not have the same property values with active rigidbody object",
default=False,
)
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
@classmethod
def poll(cls, context):
return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
if root is None:
self.report({"ERROR"}, "The model root can't be found")
return {"CANCELLED"}
selection = set(FnModel.iterate_rigid_body_objects(root))
for prop_name in self.properties:
prop_value = getattr(obj.mmd_rigid, prop_name)
if prop_name == "collision_group_mask":
prop_value = tuple(prop_value)
for i in selection.copy():
if tuple(i.mmd_rigid.collision_group_mask) != prop_value:
selection.remove(i)
if self.hide_others:
i.select_set(False)
i.hide_set(True)
else:
for i in selection.copy():
if getattr(i.mmd_rigid, prop_name) != prop_value:
selection.remove(i)
if self.hide_others:
i.select_set(False)
i.hide_set(True)
for i in selection:
i.hide_set(False)
i.select_set(True)
return {"FINISHED"}
class AddRigidBody(bpy.types.Operator):
bl_idname = "mmd_tools.rigid_body_add"
bl_label = "Add Rigid Body"
bl_description = "Add Rigid Bodies to selected bones"
bl_options = {"REGISTER", "UNDO", "PRESET", "INTERNAL"}
name_j: bpy.props.StringProperty(
name="Name",
description="The name of rigid body ($name_j means use the japanese name of target bone)",
default="$name_j",
)
name_e: bpy.props.StringProperty(
name="Name(Eng)",
description="The english name of rigid body ($name_e means use the english name of target bone)",
default="$name_e",
)
collision_group_number: bpy.props.IntProperty(
name="Collision Group",
description="The collision group of the object",
min=0,
max=15,
)
collision_group_mask: bpy.props.BoolVectorProperty(
name="Collision Group Mask",
description="The groups the object can not collide with",
size=16,
subtype="LAYER",
)
rigid_type: bpy.props.EnumProperty(
name="Rigid Type",
description="Select rigid type",
items=[
(str(rigid_body.MODE_STATIC), "Bone", "Rigid body's orientation completely determined by attached bone", 1),
(str(rigid_body.MODE_DYNAMIC), "Physics", "Attached bone's orientation completely determined by rigid body", 2),
(str(rigid_body.MODE_DYNAMIC_BONE), "Physics + Bone", "Bone determined by combination of parent and attached rigid body", 3),
],
)
rigid_shape: bpy.props.EnumProperty(
name="Shape",
description="Select the collision shape",
items=[
("SPHERE", "Sphere", "", 1),
("BOX", "Box", "", 2),
("CAPSULE", "Capsule", "", 3),
],
)
size: bpy.props.FloatVectorProperty(
name="Size",
description="Size of the object, the values will multiply the length of target bone",
subtype="XYZ",
size=3,
min=0,
default=[0.6, 0.6, 0.6],
)
mass: bpy.props.FloatProperty(
name="Mass",
description="How much the object 'weights' irrespective of gravity",
min=0.001,
default=1,
)
friction: bpy.props.FloatProperty(
name="Friction",
description="Resistance of object to movement",
min=0,
soft_max=1,
default=0.5,
)
bounce: bpy.props.FloatProperty(
name="Restitution",
description="Tendency of object to bounce after colliding with another (0 = stays still, 1 = perfectly elastic)",
min=0,
soft_max=1,
)
linear_damping: bpy.props.FloatProperty(
name="Linear Damping",
description="Amount of linear velocity that is lost over time",
min=0,
max=1,
default=0.04,
)
angular_damping: bpy.props.FloatProperty(
name="Angular Damping",
description="Amount of angular velocity that is lost over time",
min=0,
max=1,
default=0.1,
)
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None):
name_j: str = self.name_j
name_e: str = self.name_e
size = self.size.copy()
loc = Vector((0.0, 0.0, 0.0))
rot = Euler((0.0, 0.0, 0.0))
bone_name: Optional[str] = None
if pose_bone is None:
size *= getattr(root_object, Props.empty_display_size)
else:
bone_name = pose_bone.name
mmd_bone = pose_bone.mmd_bone
name_j = name_j.replace("$name_j", mmd_bone.name_j or bone_name)
name_e = name_e.replace("$name_e", mmd_bone.name_e or bone_name)
target_bone = pose_bone.bone
loc = (target_bone.head_local + target_bone.tail_local) / 2
rot = target_bone.matrix_local.to_euler("YXZ")
rot.rotate_axis("X", math.pi / 2)
size *= target_bone.length
if 1:
pass # bypass resizing
elif self.rigid_shape == "SPHERE":
size.x *= 0.8
elif self.rigid_shape == "BOX":
size.x /= 3
size.y /= 3
size.z *= 0.8
elif self.rigid_shape == "CAPSULE":
size.x /= 3
return FnRigidBody.setup_rigid_body_object(
obj=FnRigidBody.new_rigid_body_object(context, FnModel.ensure_rigid_group_object(context, root_object)),
shape_type=rigid_body.shapeType(self.rigid_shape),
location=loc,
rotation=rot,
size=size,
dynamics_type=int(self.rigid_type),
name=name_j,
name_e=name_e,
collision_group_number=self.collision_group_number,
collision_group_mask=self.collision_group_mask,
mass=self.mass,
friction=self.friction,
bounce=self.bounce,
linear_damping=self.linear_damping,
angular_damping=self.angular_damping,
bone=bone_name,
)
@classmethod
def poll(cls, context):
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return False
armature_object = FnModel.find_armature_object(root_object)
if armature_object is None:
return False
return True
def execute(self, context):
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))
if active_object != armature_object:
FnContext.select_single_object(context, root_object).select_set(False)
elif armature_object.mode != "POSE":
bpy.ops.object.mode_set(mode="POSE")
selected_pose_bones = []
if context.selected_pose_bones:
selected_pose_bones = context.selected_pose_bones
armature_object.select_set(False)
if len(selected_pose_bones) > 0:
for pose_bone in selected_pose_bones:
rigid = self.__add_rigid_body(context, root_object, pose_bone)
rigid.select_set(True)
else:
rigid = self.__add_rigid_body(context, root_object)
rigid.select_set(True)
return {"FINISHED"}
def invoke(self, context, event):
no_bone = True
if context.selected_bones and len(context.selected_bones) > 0:
no_bone = False
elif context.selected_pose_bones and len(context.selected_pose_bones) > 0:
no_bone = False
if no_bone:
self.name_j = "Rigid"
self.name_e = "Rigid_e"
else:
if self.name_j == "Rigid":
self.name_j = "$name_j"
if self.name_e == "Rigid_e":
self.name_e = "$name_e"
vm = context.window_manager
return vm.invoke_props_dialog(self)
class RemoveRigidBody(bpy.types.Operator):
bl_idname = "mmd_tools.rigid_body_remove"
bl_label = "Remove Rigid Body"
bl_description = "Deletes the currently selected Rigid Body"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True)
if root:
utils.selectAObject(root)
return {"FINISHED"}
class RigidBodyBake(bpy.types.Operator):
bl_idname = "mmd_tools.ptcache_rigid_body_bake"
bl_label = "Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context):
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
return {"FINISHED"}
class RigidBodyDeleteBake(bpy.types.Operator):
bl_idname = "mmd_tools.ptcache_rigid_body_delete_bake"
bl_label = "Delete Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context):
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
return {"FINISHED"}
class AddJoint(bpy.types.Operator):
bl_idname = "mmd_tools.joint_add"
bl_label = "Add Joint"
bl_description = "Add Joint(s) to selected rigidbody objects"
bl_options = {"REGISTER", "UNDO", "PRESET", "INTERNAL"}
use_bone_rotation: bpy.props.BoolProperty(
name="Use Bone Rotation",
description="Match joint orientation to bone orientation if enabled",
default=True,
)
limit_linear_lower: bpy.props.FloatVectorProperty(
name="Limit Linear Lower",
description="Lower limit of translation",
subtype="XYZ",
size=3,
)
limit_linear_upper: bpy.props.FloatVectorProperty(
name="Limit Linear Upper",
description="Upper limit of translation",
subtype="XYZ",
size=3,
)
limit_angular_lower: bpy.props.FloatVectorProperty(
name="Limit Angular Lower",
description="Lower limit of rotation",
subtype="EULER",
size=3,
min=-math.pi * 2,
max=math.pi * 2,
default=[-math.pi / 4] * 3,
)
limit_angular_upper: bpy.props.FloatVectorProperty(
name="Limit Angular Upper",
description="Upper limit of rotation",
subtype="EULER",
size=3,
min=-math.pi * 2,
max=math.pi * 2,
default=[math.pi / 4] * 3,
)
spring_linear: bpy.props.FloatVectorProperty(
name="Spring(Linear)",
description="Spring constant of movement",
subtype="XYZ",
size=3,
min=0,
)
spring_angular: bpy.props.FloatVectorProperty(
name="Spring(Angular)",
description="Spring constant of rotation",
subtype="XYZ",
size=3,
min=0,
)
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
obj_seq = tuple(bone_map.keys())
for rigid_a, bone_a in bone_map.items():
for rigid_b, bone_b in bone_map.items():
if bone_a and bone_b and bone_b.parent == bone_a:
obj_seq = ()
yield (rigid_a, rigid_b)
if len(obj_seq) == 2:
if obj_seq[1].mmd_rigid.type == str(rigid_body.MODE_STATIC):
yield (obj_seq[1], obj_seq[0])
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):
loc: Optional[Vector] = None
rot = Euler((0.0, 0.0, 0.0))
rigid_a, rigid_b = rigid_pair
bone_a = bone_map[rigid_a]
bone_b = bone_map[rigid_b]
if bone_a and bone_b:
if bone_a.parent == bone_b:
rigid_b, rigid_a = rigid_a, rigid_b
bone_b, bone_a = bone_a, bone_b
if bone_b.parent == bone_a:
loc = bone_b.head_local
if self.use_bone_rotation:
rot = bone_b.matrix_local.to_euler("YXZ")
rot.rotate_axis("X", math.pi / 2)
if loc is None:
loc = (rigid_a.location + rigid_b.location) / 2
name_j = rigid_b.mmd_rigid.name_j or rigid_b.name
name_e = rigid_b.mmd_rigid.name_e or rigid_b.name
return FnRigidBody.setup_joint_object(
obj=FnRigidBody.new_joint_object(context, FnModel.ensure_joint_group_object(context, root_object), FnModel.get_empty_display_size(root_object)),
name=name_j,
name_e=name_e,
location=loc,
rotation=rot,
rigid_a=rigid_a,
rigid_b=rigid_b,
maximum_location=self.limit_linear_upper,
minimum_location=self.limit_linear_lower,
maximum_rotation=self.limit_angular_upper,
minimum_rotation=self.limit_angular_lower,
spring_linear=self.spring_linear,
spring_angular=self.spring_angular,
)
@classmethod
def poll(cls, context):
root_object = FnModel.find_root_object(context.active_object)
if root_object is None:
return False
armature_object = FnModel.find_armature_object(root_object)
if armature_object is None:
return False
return True
def execute(self, context):
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))
bones = cast(bpy.types.Armature, armature_object.data).bones
bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()}
if len(bone_map) < 2:
self.report({"ERROR"}, "Please select two or more mmd rigid objects")
return {"CANCELLED"}
FnContext.select_single_object(context, root_object).select_set(False)
if context.scene.rigidbody_world is None:
bpy.ops.rigidbody.world_add()
for pair in self.__enumerate_rigid_pair(bone_map):
joint = self.__add_joint(context, root_object, pair, bone_map)
joint.select_set(True)
return {"FINISHED"}
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
class RemoveJoint(bpy.types.Operator):
bl_idname = "mmd_tools.joint_remove"
bl_label = "Remove Joint"
bl_description = "Deletes the currently selected Joint"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return FnModel.is_joint_object(context.active_object)
def execute(self, context):
obj = context.active_object
root = FnModel.find_root_object(obj)
utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True)
if root:
utils.selectAObject(root)
return {"FINISHED"}
class UpdateRigidBodyWorld(bpy.types.Operator):
bl_idname = "mmd_tools.rigid_body_world_update"
bl_label = "Update Rigid Body World"
bl_description = "Update rigid body world and references of rigid body constraint according to current scene objects (experimental)"
bl_options = {"REGISTER", "UNDO"}
@staticmethod
def __get_rigid_body_world_objects():
rigid_body.setRigidBodyWorldEnabled(True)
rbw = bpy.context.scene.rigidbody_world
if not rbw.collection:
rbw.collection = bpy.data.collections.new("RigidBodyWorld")
rbw.collection.use_fake_user = True
if not rbw.constraints:
rbw.constraints = bpy.data.collections.new("RigidBodyConstraints")
rbw.constraints.use_fake_user = True
bpy.context.scene.rigidbody_world.substeps_per_frame = 6
bpy.context.scene.rigidbody_world.solver_iterations = 10
return rbw.collection.objects, rbw.constraints.objects
def execute(self, context):
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):
if obj in scene_objs:
if obj not in group.values():
group.link(obj)
return True
elif obj in group.values():
group.unlink(obj)
return False
def _references(obj):
yield obj
if getattr(obj, "proxy", None):
yield from _references(obj.proxy)
if getattr(obj, "override_library", None):
yield from _references(obj.override_library.reference)
need_rebuild_physics = scene.rigidbody_world is None or scene.rigidbody_world.collection is None or scene.rigidbody_world.constraints is None
rb_objs, rbc_objs = self.__get_rigid_body_world_objects()
objects = bpy.data.objects
table = {}
# Perhaps due to a bug in Blender,
# when bpy.ops.rigidbody.world_remove(),
# Object.rigid_body are removed,
# but Object.rigid_body_constraint are retained.
# Therefore, it must be checked with Object.mmd_type.
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
if not _update_group(i, rb_objs):
continue
rb_map = table.setdefault(FnModel.find_root_object(i), {})
if i in rb_map: # means rb_map[i] will replace i
rb_objs.unlink(i)
continue
for r in _references(i):
rb_map[r] = i
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
# mass, friction, restitution, linear_dumping, angular_dumping
for i in (x for x in objects if x.rigid_body_constraint):
if not _update_group(i, rbc_objs):
continue
rbc, root_object = i.rigid_body_constraint, FnModel.find_root_object(i)
rb_map = table.get(root_object, {})
rbc.object1 = rb_map.get(rbc.object1, rbc.object1)
rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
if need_rebuild_physics:
for root_object in scene.objects:
if root_object.mmd_type != "ROOT":
continue
if not root_object.mmd_root.is_built:
continue
with FnContext.temp_override_active_layer_collection(context, root_object):
Model(root_object).build()
# After rebuild. First play. Will be crash!
# But saved it before. Reload after crash. The play can be work.
return {"FINISHED"}
+110
View File
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Set
import bpy
from bpy.types import Operator
from ..core.model import FnModel
from ..core.sdef import FnSDEF
def _get_target_objects(context):
root_objects: Set[bpy.types.Object] = set()
selected_objects: Set[bpy.types.Object] = set()
for i in context.selected_objects:
if i.type == "MESH":
selected_objects.add(i)
continue
root_object = FnModel.find_root_object(i)
if root_object is None:
continue
if root_object in root_objects:
continue
root_objects.add(root_object)
selected_objects |= set(FnModel.iterate_mesh_objects(root_object))
return selected_objects, root_objects
class ResetSDEFCache(Operator):
bl_idname = "mmd_tools.sdef_cache_reset"
bl_label = "Reset MMD SDEF cache"
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context):
target_meshes, _ = _get_target_objects(context)
for i in target_meshes:
FnSDEF.clear_cache(i)
FnSDEF.clear_cache(unused_only=True)
return {"FINISHED"}
class BindSDEF(Operator):
bl_idname = "mmd_tools.sdef_bind"
bl_label = "Bind SDEF Driver"
bl_description = "Bind MMD SDEF data of selected objects"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
mode: bpy.props.EnumProperty(
name="Mode",
description="Select mode",
items=[
("2", "Bulk", "Speed up with numpy (may be slower in some cases)", 2),
("1", "Normal", "Normal mode", 1),
("0", "- Auto -", "Select best mode by benchmark result", 0),
],
default="0",
)
use_skip: bpy.props.BoolProperty(
name="Skip",
description="Skip when the bones are not moving",
default=True,
)
use_scale: bpy.props.BoolProperty(
name="Scale",
description="Support bone scaling (slow)",
default=False,
)
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
# TODO: Utility Functionalize
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context)
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)
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
return {"FINISHED"}
class UnbindSDEF(Operator):
bl_idname = "mmd_tools.sdef_unbind"
bl_label = "Unbind SDEF Driver"
bl_description = "Unbind MMD SDEF data of selected objects"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
# TODO: Utility Functionalize
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context)
for i in target_meshes:
FnSDEF.unbind(i)
for r in root_objects:
r.mmd_root.use_sdef = False
return {"FINISHED"}
+336
View File
@@ -0,0 +1,336 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import TYPE_CHECKING, cast
import bpy
from ..core.model import FnModel, Model
from ..core.translations import MMD_DATA_TYPE_TO_HANDLERS, FnTranslations
from ..translations import DictionaryEnum
if TYPE_CHECKING:
from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex
class TranslateMMDModel(bpy.types.Operator):
bl_idname = "mmd_tools.translate_mmd_model"
bl_label = "Translate a MMD Model"
bl_description = "Translate Japanese names of a MMD model"
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
dictionary: bpy.props.EnumProperty(
name="Dictionary",
items=DictionaryEnum.get_dictionary_items,
description="Translate names from Japanese to English using selected dictionary",
)
types: bpy.props.EnumProperty(
name="Types",
description="Select which parts will be translated",
options={"ENUM_FLAG"},
items=[
("BONE", "Bones", "Bones", 1),
("MORPH", "Morphs", "Morphs", 2),
("MATERIAL", "Materials", "Materials", 4),
("DISPLAY", "Display", "Display frames", 8),
("PHYSICS", "Physics", "Rigidbodies and joints", 16),
("INFO", "Information", "Model name and comments", 32),
],
default={
"BONE",
"MORPH",
"MATERIAL",
"DISPLAY",
"PHYSICS",
},
)
modes: bpy.props.EnumProperty(
name="Modes",
description="Select translation mode",
options={"ENUM_FLAG"},
items=[
("MMD", "MMD Names", "Fill MMD English names", 1),
("BLENDER", "Blender Names", "Translate blender names (experimental)", 2),
],
default={"MMD"},
)
use_morph_prefix: bpy.props.BoolProperty(
name="Use Morph Prefix",
description="Add/remove prefix to English name of morph",
default=False,
)
overwrite: bpy.props.BoolProperty(
name="Overwrite",
description="Overwrite a translated English name",
default=False,
)
allow_fails: bpy.props.BoolProperty(
name="Allow Fails",
description="Allow incompletely translated names",
default=False,
)
@classmethod
def poll(cls, context):
obj = context.active_object
return obj in context.selected_objects and FnModel.find_root_object(obj)
def invoke(self, context, event):
vm = context.window_manager
return vm.invoke_props_dialog(self)
def execute(self, context):
try:
self.__translator = DictionaryEnum.get_translator(self.dictionary)
except Exception as e:
self.report({"ERROR"}, "Failed to load dictionary: %s" % e)
return {"CANCELLED"}
obj = context.active_object
root = FnModel.find_root_object(obj)
rig = Model(root)
if "MMD" in self.modes:
for i in self.types:
getattr(self, "translate_%s" % i.lower())(rig)
if "BLENDER" in self.modes:
self.translate_blender_names(rig)
translator = self.__translator
txt = translator.save_fails()
if translator.fails:
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(translator.fails), txt.name))
return {"FINISHED"}
def translate(self, name_j, name_e):
if not self.overwrite and name_e and self.__translator.is_translated(name_e):
return name_e
if self.allow_fails:
name_e = None
return self.__translator.translate(name_j, name_e)
def translate_blender_names(self, rig: Model):
if "BONE" in self.types:
for b in rig.armature().pose.bones:
rig.renameBone(b.name, self.translate(b.name, b.name))
if "MORPH" in self.types:
for i in (x for x in rig.meshes() if x.data.shape_keys):
for kb in i.data.shape_keys.key_blocks:
kb.name = self.translate(kb.name, kb.name)
if "MATERIAL" in self.types:
for m in (x for x in rig.materials() if x):
m.name = self.translate(m.name, m.name)
if "DISPLAY" in self.types:
g: bpy.types.BoneCollection
for g in cast(bpy.types.Armature, rig.armature().data).collections:
g.name = self.translate(g.name, g.name)
if "PHYSICS" in self.types:
for i in rig.rigidBodies():
i.name = self.translate(i.name, i.name)
for i in rig.joints():
i.name = self.translate(i.name, i.name)
if "INFO" in self.types:
objects = [rig.rootObject(), rig.armature()]
objects.extend(rig.meshes())
for i in objects:
i.name = self.translate(i.name, i.name)
def translate_info(self, rig):
mmd_root = rig.rootObject().mmd_root
mmd_root.name_e = self.translate(mmd_root.name, mmd_root.name_e)
comment_text = bpy.data.texts.get(mmd_root.comment_text, None)
comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None)
if comment_text and comment_e_text:
comment_e = self.translate(comment_text.as_string(), comment_e_text.as_string())
comment_e_text.from_string(comment_e)
def translate_bone(self, rig):
bones = rig.armature().pose.bones
for b in bones:
if b.is_mmd_shadow_bone:
continue
b.mmd_bone.name_e = self.translate(b.mmd_bone.name_j, b.mmd_bone.name_e)
def translate_morph(self, rig):
mmd_root = rig.rootObject().mmd_root
attr_list = ("group", "vertex", "bone", "uv", "material")
prefix_list = ("G_", "", "B_", "UV_", "M_")
for attr, prefix in zip(attr_list, prefix_list):
for m in getattr(mmd_root, attr + "_morphs", []):
m.name_e = self.translate(m.name, m.name_e)
if not prefix:
continue
if self.use_morph_prefix:
if not m.name_e.startswith(prefix):
m.name_e = prefix + m.name_e
elif m.name_e.startswith(prefix):
m.name_e = m.name_e[len(prefix) :]
def translate_material(self, rig):
for m in rig.materials():
if m is None:
continue
m.mmd_material.name_e = self.translate(m.mmd_material.name_j, m.mmd_material.name_e)
def translate_display(self, rig):
mmd_root = rig.rootObject().mmd_root
for f in mmd_root.display_item_frames:
f.name_e = self.translate(f.name, f.name_e)
def translate_physics(self, rig):
for i in rig.rigidBodies():
i.mmd_rigid.name_e = self.translate(i.mmd_rigid.name_j, i.mmd_rigid.name_e)
for i in rig.joints():
i.mmd_joint.name_e = self.translate(i.mmd_joint.name_j, i.mmd_joint.name_e)
DEFAULT_SHOW_ROW_COUNT = 20
class MMD_TOOLS_UL_MMDTranslationElementIndex(bpy.types.UIList):
def draw_item(self, context, layout: bpy.types.UILayout, data, mmd_translation_element_index: "MMDTranslationElementIndex", icon, active_data, active_propname, index: int):
mmd_translation_element: "MMDTranslationElement" = data.translation_elements[mmd_translation_element_index.value]
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(layout, mmd_translation_element, index)
class RestoreMMDDataReferenceOperator(bpy.types.Operator):
bl_idname = "mmd_tools.restore_mmd_translation_element_name"
bl_label = "Restore this Name"
bl_options = {"INTERNAL"}
index: bpy.props.IntProperty()
prop_name: bpy.props.StringProperty()
restore_value: bpy.props.StringProperty()
def execute(self, context: bpy.types.Context):
root_object = FnModel.find_root_object(context.object)
mmd_translation_element_index = root_object.mmd_root.translation.filtered_translation_element_indices[self.index].value
mmd_translation_element = root_object.mmd_root.translation.translation_elements[mmd_translation_element_index]
setattr(mmd_translation_element, self.prop_name, self.restore_value)
return {"FINISHED"}
class GlobalTranslationPopup(bpy.types.Operator):
bl_idname = "mmd_tools.global_translation_popup"
bl_label = "Global Translation Popup"
bl_options = {"INTERNAL", "UNDO"}
@classmethod
def poll(cls, context):
return FnModel.find_root_object(context.object) is not None
def draw(self, _context):
layout = self.layout
mmd_translation = self._mmd_translation
col = layout.column(align=True)
col.label(text="Filter", icon="FILTER")
row = col.row()
row.prop(mmd_translation, "filter_types")
group = row.row(align=True, heading="is Blank:")
group.alignment = "RIGHT"
group.prop(mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese")
group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English")
group = row.row(align=True)
group.prop(mmd_translation, "filter_restorable", toggle=True, icon="FILE_REFRESH", icon_only=True)
group.prop(mmd_translation, "filter_selected", toggle=True, icon="RESTRICT_SELECT_OFF", icon_only=True)
group.prop(mmd_translation, "filter_visible", toggle=True, icon="HIDE_OFF", icon_only=True)
col = layout.column(align=True)
box = col.box().column(align=True)
row = box.row(align=True)
row.label(text="Select the target column for Batch Operations:", icon="TRACKER")
row = box.row(align=True)
row.label(text="", icon="BLANK1")
row.prop(mmd_translation, "batch_operation_target", expand=True)
row.label(text="", icon="RESTRICT_SELECT_OFF")
row.label(text="", icon="HIDE_OFF")
if len(mmd_translation.filtered_translation_element_indices) > DEFAULT_SHOW_ROW_COUNT:
row.label(text="", icon="BLANK1")
col.template_list(
"MMD_TOOLS_UL_MMDTranslationElementIndex",
"",
mmd_translation,
"filtered_translation_element_indices",
mmd_translation,
"filtered_translation_element_indices_active_index",
rows=DEFAULT_SHOW_ROW_COUNT,
)
box = layout.box().column(align=True)
box.label(text="Batch Operation:", icon="MODIFIER")
box.prop(mmd_translation, "batch_operation_script", text="", icon="SCRIPT")
box.separator()
row = box.row()
row.prop(mmd_translation, "batch_operation_script_preset", text="Preset", icon="CON_TRANSFORM_CACHE")
row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute")
box.separator()
translation_box = box.box().column(align=True)
translation_box.label(text="Dictionaries:", icon="HELP")
row = translation_box.row()
row.prop(mmd_translation, "dictionary", text="to_english")
# row.operator(ExecuteTranslationScriptOperator.bl_idname, text='Write to .csv')
translation_box.separator()
row = translation_box.row()
row.prop(mmd_translation, "dictionary", text="replace")
def invoke(self, context: bpy.types.Context, _event):
root_object = FnModel.find_root_object(context.object)
if root_object is None:
return {"CANCELLED"}
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
self._mmd_translation = mmd_translation
FnTranslations.clear_data(mmd_translation)
FnTranslations.collect_data(mmd_translation)
FnTranslations.update_query(mmd_translation)
return context.window_manager.invoke_props_dialog(self, width=800)
def execute(self, context):
root_object = FnModel.find_root_object(context.object)
if root_object is None:
return {"CANCELLED"}
FnTranslations.apply_translations(root_object)
FnTranslations.clear_data(root_object.mmd_root.translation)
return {"FINISHED"}
class ExecuteTranslationBatchOperator(bpy.types.Operator):
bl_idname = "mmd_tools.execute_translation_batch"
bl_label = "Execute Translation Batch"
bl_options = {"INTERNAL"}
def execute(self, context: bpy.types.Context):
root = FnModel.find_root_object(context.object)
if root is None:
return {"CANCELLED"}
fails, text = FnTranslations.execute_translation_batch(root)
if fails:
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(fails), text.name))
return {"FINISHED"}
+150
View File
@@ -0,0 +1,150 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re
from bpy.types import Operator
from mathutils import Matrix
class _SetShadingBase:
bl_options = {"REGISTER", "UNDO"}
@staticmethod
def _get_view3d_spaces(context):
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):
try:
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
except TypeError:
pass
@staticmethod
def _reset_material_shading(context, use_shadeless=False):
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:
continue
s.material.use_nodes = False
s.material.use_shadeless = use_shadeless
def execute(self, context):
context.scene.render.engine = "BLENDER_EEVEE_NEXT"
shading_mode = getattr(self, "_shading_mode", None)
for space in self._get_view3d_spaces(context):
shading = space.shading
shading.type = "SOLID"
shading.light = "FLAT" if shading_mode == "SHADELESS" else "STUDIO"
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
shading.show_object_outline = False
shading.show_backface_culling = False
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"
_shading_mode = "GLSL"
class SetShadelessGLSLShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.set_shadeless_glsl_shading"
bl_label = "Shadeless GLSL View"
bl_description = "Use only toon shading"
_shading_mode = "SHADELESS"
class ResetShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.reset_shading"
bl_label = "Reset View"
bl_description = "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"}
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
__LR_REGEX = [
{"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},
{"re": re.compile(r"^(L|R)([\.\- _])(.+)$", re.IGNORECASE), "lr": 0},
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
]
__LR_MAP = {
"RIGHT": "LEFT",
"Right": "Left",
"right": "left",
"LEFT": "RIGHT",
"Left": "Right",
"left": "right",
"L": "R",
"l": "r",
"R": "L",
"r": "l",
"": "",
"": "",
}
@classmethod
def flip_name(cls, name):
for regex in cls.__LR_REGEX:
match = regex["re"].match(name)
if match:
groups = match.groups()
lr = groups[regex["lr"]]
if lr in cls.__LR_MAP:
flip_lr = cls.__LR_MAP[lr]
name = ""
for i, s in enumerate(groups):
if i == regex["lr"]:
name += flip_lr
elif s:
name += s
return name
return ""
@staticmethod
def __cmul(vec1, vec2):
return type(vec1)([x * y for x, y in zip(vec1, vec2)])
@staticmethod
def __matrix_compose(loc, rot, scale):
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
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()
loc = cls.__cmul(mi @ loc, (-1, 1, 1))
rot = cls.__cmul(Quaternion(mi @ rot.axis, rot.angle).normalized(), (1, 1, -1, -1))
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
def execute(self, context):
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))
return {"FINISHED"}
+34
View File
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
def patch_library_overridable(property: "bpy.props._PropertyDeferred") -> "bpy.props._PropertyDeferred":
"""Apply recursively for each mmd_tools property class annotations.
Args:
property: The property to be patched.
Returns:
The patched property.
"""
property.keywords.setdefault("override", set()).add("LIBRARY_OVERRIDABLE")
if property.function.__name__ not in {"PointerProperty", "CollectionProperty"}:
return property
property_type = property.keywords["type"]
# The __annotations__ cannot be inherited. Manually search for base classes.
for inherited_type in (property_type, *property_type.__bases__):
if not inherited_type.__module__.startswith("mmd_tools.properties"):
continue
for annotation in inherited_type.__annotations__.values():
if not isinstance(annotation, bpy.props._PropertyDeferred):
continue
patch_library_overridable(annotation)
return property
+287
View File
@@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from .. import utils
from ..core import material
from ..core.material import FnMaterial
from ..core.model import FnModel
from . import patch_library_overridable
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_ambient_color()
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_diffuse_color()
def _mmd_material_update_alpha(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_alpha()
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_specular_color()
def _mmd_material_update_shininess(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_shininess()
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_is_double_sided()
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context):
FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object)
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_toon_texture()
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_drop_shadow()
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_self_shadow_map()
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_self_shadow()
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_enabled_toon_edge()
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_edge_color()
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context):
FnMaterial(prop.id_data).update_edge_weight()
def _mmd_material_get_name_j(prop: "MMDMaterial"):
return prop.get("name_j", "")
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str):
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:
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials})
else:
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)})
prop["name_j"] = prop_value
# ===========================================
# Property classes
# ===========================================
class MMDMaterial(bpy.types.PropertyGroup):
"""マテリアル"""
name_j: bpy.props.StringProperty(
name="Name",
description="Japanese Name",
default="",
set=_mmd_material_set_name_j,
get=_mmd_material_get_name_j,
)
name_e: bpy.props.StringProperty(
name="Name(Eng)",
description="English Name",
default="",
)
material_id: bpy.props.IntProperty(
name="Material ID",
description="Unique ID for the reference of material morph",
default=-1,
min=-1,
)
ambient_color: bpy.props.FloatVectorProperty(
name="Ambient Color",
description="Ambient color",
subtype="COLOR",
size=3,
min=0,
max=1,
precision=3,
step=0.1,
default=[0.4, 0.4, 0.4],
update=_mmd_material_update_ambient_color,
)
diffuse_color: bpy.props.FloatVectorProperty(
name="Diffuse Color",
description="Diffuse color",
subtype="COLOR",
size=3,
min=0,
max=1,
precision=3,
step=0.1,
default=[0.8, 0.8, 0.8],
update=_mmd_material_update_diffuse_color,
)
alpha: bpy.props.FloatProperty(
name="Alpha",
description="Alpha transparency",
min=0,
max=1,
precision=3,
step=0.1,
default=1.0,
update=_mmd_material_update_alpha,
)
specular_color: bpy.props.FloatVectorProperty(
name="Specular Color",
description="Specular color",
subtype="COLOR",
size=3,
min=0,
max=1,
precision=3,
step=0.1,
default=[0.625, 0.625, 0.625],
update=_mmd_material_update_specular_color,
)
shininess: bpy.props.FloatProperty(
name="Reflect",
description="Sharpness of reflected highlights",
min=0,
soft_max=512,
step=100.0,
default=50.0,
update=_mmd_material_update_shininess,
)
is_double_sided: bpy.props.BoolProperty(
name="Double Sided",
description="Both sides of mesh should be rendered",
default=False,
update=_mmd_material_update_is_double_sided,
)
enabled_drop_shadow: bpy.props.BoolProperty(
name="Ground Shadow",
description="Display ground shadow",
default=True,
update=_mmd_material_update_enabled_drop_shadow,
)
enabled_self_shadow_map: bpy.props.BoolProperty(
name="Self Shadow Map",
description="Object can become shadowed by other objects",
default=True,
update=_mmd_material_update_enabled_self_shadow_map,
)
enabled_self_shadow: bpy.props.BoolProperty(
name="Self Shadow",
description="Object can cast shadows",
default=True,
update=_mmd_material_update_enabled_self_shadow,
)
enabled_toon_edge: bpy.props.BoolProperty(
name="Toon Edge",
description="Use toon edge",
default=False,
update=_mmd_material_update_enabled_toon_edge,
)
edge_color: bpy.props.FloatVectorProperty(
name="Edge Color",
description="Toon edge color",
subtype="COLOR",
size=4,
min=0,
max=1,
precision=3,
step=0.1,
default=[0, 0, 0, 1],
update=_mmd_material_update_edge_color,
)
edge_weight: bpy.props.FloatProperty(
name="Edge Weight",
description="Toon edge size",
min=0,
max=100,
soft_max=2,
step=1.0,
default=1.0,
update=_mmd_material_update_edge_weight,
)
sphere_texture_type: bpy.props.EnumProperty(
name="Sphere Map Type",
description="Choose sphere texture blend type",
items=[
(str(material.SPHERE_MODE_OFF), "Off", "", 1),
(str(material.SPHERE_MODE_MULT), "Multiply", "", 2),
(str(material.SPHERE_MODE_ADD), "Add", "", 3),
(str(material.SPHERE_MODE_SUBTEX), "SubTexture", "", 4),
],
update=_mmd_material_update_sphere_texture_type,
)
is_shared_toon_texture: bpy.props.BoolProperty(
name="Use Shared Toon Texture",
description="Use shared toon texture or custom toon texture",
default=False,
update=_mmd_material_update_toon_texture,
)
toon_texture: bpy.props.StringProperty(
name="Toon Texture",
subtype="FILE_PATH",
description="The file path of custom toon texture",
default="",
update=_mmd_material_update_toon_texture,
)
shared_toon_texture: bpy.props.IntProperty(
name="Shared Toon Texture",
description="Shared toon texture id (toon01.bmp ~ toon10.bmp)",
default=0,
min=0,
max=9,
update=_mmd_material_update_toon_texture,
)
comment: bpy.props.StringProperty(
name="Comment",
description="Comment",
)
def is_id_unique(self):
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():
bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial))
@staticmethod
def unregister():
del bpy.types.Material.mmd_material
+488
View File
@@ -0,0 +1,488 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy
from .. import utils
from ..core.bone import FnBone
from ..core.material import FnMaterial
from ..core.model import FnModel, Model
from ..core.morph import FnMorph
def _morph_base_get_name(prop: "_MorphBase") -> str:
return prop.get("name", "")
def _morph_base_set_name(prop: "_MorphBase", value: str):
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}
value = utils.unique_name(value, used_names)
if prop_name is not None:
if morph_type == "vertex_morphs":
kb_list = {}
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)
if prop_name in kb_list:
value = utils.unique_name(value, used_names | kb_list.keys())
for kb in kb_list[prop_name]:
kb.name = value
elif morph_type == "uv_morphs":
vg_list = {}
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)
if prop_name in vg_list:
value = utils.unique_name(value, used_names | vg_list.keys())
for vg in vg_list[prop_name]:
vg.name = vg.name.replace(prop_name, value)
if 1: # morph_type != 'group_morphs':
for m in mmd_root.group_morphs:
for d in m.data:
if d.name == prop_name and d.morph_type == morph_type:
d.name = value
frame_facial = mmd_root.display_item_frames.get("表情")
for item in getattr(frame_facial, "data", []):
if item.name == prop_name and item.morph_type == morph_type:
item.name = value
break
obj = Model(prop.id_data).morph_slider.placeholder()
if obj and value not in obj.data.shape_keys.key_blocks:
kb = obj.data.shape_keys.key_blocks.get(prop_name, None)
if kb:
kb.name = value
prop["name"] = value
class _MorphBase:
name: bpy.props.StringProperty(
name="Name",
description="Japanese Name",
set=_morph_base_set_name,
get=_morph_base_get_name,
)
name_e: bpy.props.StringProperty(
name="Name(Eng)",
description="English Name",
default="",
)
category: bpy.props.EnumProperty(
name="Category",
description="Select category",
items=[
("SYSTEM", "Hidden", "", 0),
("EYEBROW", "Eye Brow", "", 1),
("EYE", "Eye", "", 2),
("MOUTH", "Mouth", "", 3),
("OTHER", "Other", "", 4),
],
default="OTHER",
)
def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
bone_id = prop.get("bone_id", -1)
if bone_id < 0:
return ""
root_object = prop.id_data
armature_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)
if pose_bone is None:
return ""
return pose_bone.name
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str):
root = prop.id_data
arm = 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.
if arm is None:
return
if value not in arm.pose.bones.keys():
prop["bone_id"] = -1
return
pose_bone = arm.pose.bones[value]
prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context):
if not prop.name.startswith("mmd_bind"):
return
arm = FnModel(prop.id_data).morph_slider.dummy_armature
if arm:
bone = arm.pose.bones.get(prop.name, None)
if bone:
bone.location = prop.location
bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency
class BoneMorphData(bpy.types.PropertyGroup):
""" """
bone: bpy.props.StringProperty(
name="Bone",
description="Target bone",
set=_bone_morph_data_set_bone,
get=_bone_morph_data_get_bone,
)
bone_id: bpy.props.IntProperty(
name="Bone ID",
)
location: bpy.props.FloatVectorProperty(
name="Location",
description="Location",
subtype="TRANSLATION",
size=3,
default=[0, 0, 0],
update=_bone_morph_data_update_location_or_rotation,
)
rotation: bpy.props.FloatVectorProperty(
name="Rotation",
description="Rotation in quaternions",
subtype="QUATERNION",
size=4,
default=[1, 0, 0, 0],
update=_bone_morph_data_update_location_or_rotation,
)
class BoneMorph(_MorphBase, bpy.types.PropertyGroup):
"""Bone Morph"""
data: bpy.props.CollectionProperty(
name="Morph Data",
type=BoneMorphData,
)
active_data: bpy.props.IntProperty(
name="Active Bone Data",
min=0,
default=0,
)
def _material_morph_data_get_material(prop: "MaterialMorphData"):
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):
if value not in bpy.data.materials:
prop["material_data"] = None
prop["material_id"] = -1
else:
mat = bpy.data.materials[value]
fnMat = FnMaterial(mat)
prop["material_data"] = mat
prop["material_id"] = fnMat.material_id
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str):
mesh = FnModel.find_mesh_object_by_name(prop.id_data, value)
if mesh is not None:
prop["related_mesh_data"] = mesh.data
else:
prop["related_mesh_data"] = None
def _material_morph_data_get_related_mesh(prop):
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):
if not prop.name.startswith("mmd_bind"):
return
from ..core.shader import _MaterialMorph
mat = prop["material_data"]
if mat is not None:
_MaterialMorph.update_morph_inputs(mat, prop)
else:
for mat in FnModel(prop.id_data).materials():
_MaterialMorph.update_morph_inputs(mat, prop)
class MaterialMorphData(bpy.types.PropertyGroup):
""" """
related_mesh: bpy.props.StringProperty(
name="Related Mesh",
description="Stores a reference to the mesh where this morph data belongs to",
set=_material_morph_data_set_related_mesh,
get=_material_morph_data_get_related_mesh,
)
related_mesh_data: bpy.props.PointerProperty(
name="Related Mesh Data",
type=bpy.types.Mesh,
)
offset_type: bpy.props.EnumProperty(name="Offset Type", description="Select offset type", items=[("MULT", "Multiply", "", 0), ("ADD", "Add", "", 1)], default="ADD")
material: bpy.props.StringProperty(
name="Material",
description="Target material",
get=_material_morph_data_get_material,
set=_material_morph_data_set_material,
)
material_id: bpy.props.IntProperty(
name="Material ID",
default=-1,
)
material_data: bpy.props.PointerProperty(
name="Material Data",
type=bpy.types.Material,
)
diffuse_color: bpy.props.FloatVectorProperty(
name="Diffuse Color",
description="Diffuse color",
subtype="COLOR",
size=4,
soft_min=0,
soft_max=1,
precision=3,
step=0.1,
default=[0, 0, 0, 1],
update=_material_morph_data_update_modifiable_values,
)
specular_color: bpy.props.FloatVectorProperty(
name="Specular Color",
description="Specular color",
subtype="COLOR",
size=3,
soft_min=0,
soft_max=1,
precision=3,
step=0.1,
default=[0, 0, 0],
update=_material_morph_data_update_modifiable_values,
)
shininess: bpy.props.FloatProperty(
name="Reflect",
description="Reflect",
soft_min=0,
soft_max=500,
step=100.0,
default=0.0,
update=_material_morph_data_update_modifiable_values,
)
ambient_color: bpy.props.FloatVectorProperty(
name="Ambient Color",
description="Ambient color",
subtype="COLOR",
size=3,
soft_min=0,
soft_max=1,
precision=3,
step=0.1,
default=[0, 0, 0],
update=_material_morph_data_update_modifiable_values,
)
edge_color: bpy.props.FloatVectorProperty(
name="Edge Color",
description="Edge color",
subtype="COLOR",
size=4,
soft_min=0,
soft_max=1,
precision=3,
step=0.1,
default=[0, 0, 0, 1],
update=_material_morph_data_update_modifiable_values,
)
edge_weight: bpy.props.FloatProperty(
name="Edge Weight",
description="Edge weight",
soft_min=0,
soft_max=2,
step=0.1,
default=0,
update=_material_morph_data_update_modifiable_values,
)
texture_factor: bpy.props.FloatVectorProperty(
name="Texture factor",
description="Texture factor",
subtype="COLOR",
size=4,
soft_min=0,
soft_max=1,
precision=3,
step=0.1,
default=[0, 0, 0, 1],
update=_material_morph_data_update_modifiable_values,
)
sphere_texture_factor: bpy.props.FloatVectorProperty(
name="Sphere Texture factor",
description="Sphere texture factor",
subtype="COLOR",
size=4,
soft_min=0,
soft_max=1,
precision=3,
step=0.1,
default=[0, 0, 0, 1],
update=_material_morph_data_update_modifiable_values,
)
toon_texture_factor: bpy.props.FloatVectorProperty(
name="Toon Texture factor",
description="Toon texture factor",
subtype="COLOR",
size=4,
soft_min=0,
soft_max=1,
precision=3,
step=0.1,
default=[0, 0, 0, 1],
update=_material_morph_data_update_modifiable_values,
)
class MaterialMorph(_MorphBase, bpy.types.PropertyGroup):
"""Material Morph"""
data: bpy.props.CollectionProperty(
name="Morph Data",
type=MaterialMorphData,
)
active_data: bpy.props.IntProperty(
name="Active Material Data",
min=0,
default=0,
)
class UVMorphOffset(bpy.types.PropertyGroup):
"""UV Morph Offset"""
index: bpy.props.IntProperty(
name="Vertex Index",
description="Vertex index",
min=0,
default=0,
)
offset: bpy.props.FloatVectorProperty(
name="UV Offset",
description="UV offset",
size=4,
# min=-1,
# max=1,
# precision=3,
step=0.1,
default=[0, 0, 0, 0],
)
class UVMorph(_MorphBase, bpy.types.PropertyGroup):
"""UV Morph"""
uv_index: bpy.props.IntProperty(
name="UV Index",
description="UV index (UV, UV1 ~ UV4)",
min=0,
max=4,
default=0,
)
data_type: bpy.props.EnumProperty(
name="Data Type",
description="Select data type",
items=[
("DATA", "Data", "Store offset data in root object (deprecated)", 0),
("VERTEX_GROUP", "Vertex Group", "Store offset data in vertex groups", 1),
],
default="DATA",
)
data: bpy.props.CollectionProperty(
name="Morph Data",
type=UVMorphOffset,
)
active_data: bpy.props.IntProperty(
name="Active UV Data",
min=0,
default=0,
)
vertex_group_scale: bpy.props.FloatProperty(
name="Vertex Group Scale",
description='The value scale of "Vertex Group" data type',
precision=3,
step=0.1,
default=1,
)
class GroupMorphOffset(bpy.types.PropertyGroup):
"""Group Morph Offset"""
morph_type: bpy.props.EnumProperty(
name="Morph Type",
description="Select morph type",
items=[
("material_morphs", "Material", "Material Morphs", 0),
("uv_morphs", "UV", "UV Morphs", 1),
("bone_morphs", "Bone", "Bone Morphs", 2),
("vertex_morphs", "Vertex", "Vertex Morphs", 3),
("group_morphs", "Group", "Group Morphs", 4),
],
default="vertex_morphs",
)
factor: bpy.props.FloatProperty(name="Factor", description="Factor", soft_min=0, soft_max=1, precision=3, step=0.1, default=0)
class GroupMorph(_MorphBase, bpy.types.PropertyGroup):
"""Group Morph"""
data: bpy.props.CollectionProperty(
name="Morph Data",
type=GroupMorphOffset,
)
active_data: bpy.props.IntProperty(
name="Active Group Data",
min=0,
default=0,
)
class VertexMorph(_MorphBase, bpy.types.PropertyGroup):
"""Vertex Morph"""
+87 -113
View File
@@ -1,41 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
# All credit goes to the original authors.
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import cast
import bpy
from bpy.types import PropertyGroup, Context, PoseBone
from bpy.props import (
StringProperty,
IntProperty,
BoolProperty,
FloatProperty,
FloatVectorProperty
)
from ..logging_setup import logger
from ..bone import FnBone
from ..core.bone import FnBone
from . import patch_library_overridable
def _mmd_bone_update_additional_transform(prop, context: Context):
"""Update handler for additional transform properties"""
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context):
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():
FnBone.apply_additional_transformation(prop.id_data)
def _mmd_bone_update_additional_transform_influence(prop, context: Context):
"""Update handler for additional transform influence"""
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context):
pose_bone = context.active_pose_bone
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer():
FnBone.update_additional_transform_influence(pose_bone)
else:
prop["is_additional_transform_dirty"] = True
def _mmd_bone_get_additional_transform_bone(prop):
"""Getter for additional transform bone property"""
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
arm = prop.id_data
bone_id = prop.get("additional_transform_bone_id", -1)
if bone_id < 0:
@@ -45,8 +37,8 @@ def _mmd_bone_get_additional_transform_bone(prop):
return ""
return pose_bone.name
def _mmd_bone_set_additional_transform_bone(prop, value: str):
"""Setter for additional transform bone property"""
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str):
arm = prop.id_data
prop["is_additional_transform_dirty"] = True
if value not in arm.pose.bones.keys():
@@ -55,85 +47,70 @@ def _mmd_bone_set_additional_transform_bone(prop, value: str):
pose_bone = arm.pose.bones[value]
prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context):
"""Update handler for IK toggle property"""
v = prop.mmd_ik_toggle
armature_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:
logger.debug('Updating IK constraint %s on bone %s', c.name, b.name)
c.influence = v
b_chain = b if c.use_tail else b.parent
for chain_bone in ([b_chain] + b_chain.parent_recursive)[:c.chain_count]:
limit_c = next((c for c in chain_bone.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None)
if limit_c:
limit_c.influence = v
class MMDBone(PropertyGroup):
"""Property group for MMD bone properties"""
name_j: StringProperty(
class MMDBone(bpy.types.PropertyGroup):
name_j: bpy.props.StringProperty(
name="Name",
description="Japanese Name",
default="",
)
name_e: StringProperty(
name_e: bpy.props.StringProperty(
name="Name(Eng)",
description="English Name",
default="",
)
bone_id: IntProperty(
bone_id: bpy.props.IntProperty(
name="Bone ID",
description="Unique ID for the reference of bone morph and rotate+/move+",
default=-1,
min=-1,
)
transform_order: IntProperty(
transform_order: bpy.props.IntProperty(
name="Transform Order",
description="Deformation tier",
min=0,
max=100,
soft_max=7,
)
is_controllable: BoolProperty(
is_controllable: bpy.props.BoolProperty(
name="Controllable",
description="Is controllable",
default=True,
)
transform_after_dynamics: BoolProperty(
transform_after_dynamics: bpy.props.BoolProperty(
name="After Dynamics",
description="After physics",
default=False,
)
enabled_fixed_axis: BoolProperty(
enabled_fixed_axis: bpy.props.BoolProperty(
name="Fixed Axis",
description="Use fixed axis",
default=False,
)
fixed_axis: FloatVectorProperty(
fixed_axis: bpy.props.FloatVectorProperty(
name="Fixed Axis",
description="Fixed axis",
subtype="XYZ",
size=3,
precision=3,
step=0.1,
step=0.1, # 0.1 / 100
default=[0, 0, 0],
)
enabled_local_axes: BoolProperty(
enabled_local_axes: bpy.props.BoolProperty(
name="Local Axes",
description="Use local axes",
default=False,
)
local_axis_x: FloatVectorProperty(
local_axis_x: bpy.props.FloatVectorProperty(
name="Local X-Axis",
description="Local x-axis",
subtype="XYZ",
@@ -142,8 +119,8 @@ class MMDBone(PropertyGroup):
step=0.1,
default=[1, 0, 0],
)
local_axis_z: FloatVectorProperty(
local_axis_z: bpy.props.FloatVectorProperty(
name="Local Z-Axis",
description="Local z-axis",
subtype="XYZ",
@@ -152,14 +129,14 @@ class MMDBone(PropertyGroup):
step=0.1,
default=[0, 0, 1],
)
is_tip: BoolProperty(
is_tip: bpy.props.BoolProperty(
name="Tip Bone",
description="Is zero length bone",
default=False,
)
ik_rotation_constraint: FloatProperty(
ik_rotation_constraint: bpy.props.FloatProperty(
name="IK Rotation Constraint",
description="The unit angle of IK",
subtype="ANGLE",
@@ -167,36 +144,36 @@ class MMDBone(PropertyGroup):
soft_max=4,
default=1,
)
has_additional_rotation: BoolProperty(
has_additional_rotation: bpy.props.BoolProperty(
name="Additional Rotation",
description="Additional rotation",
default=False,
update=_mmd_bone_update_additional_transform,
)
has_additional_location: BoolProperty(
has_additional_location: bpy.props.BoolProperty(
name="Additional Location",
description="Additional location",
default=False,
update=_mmd_bone_update_additional_transform,
)
additional_transform_bone: StringProperty(
additional_transform_bone: bpy.props.StringProperty(
name="Additional Transform Bone",
description="Additional transform bone",
set=_mmd_bone_set_additional_transform_bone,
get=_mmd_bone_get_additional_transform_bone,
update=_mmd_bone_update_additional_transform,
)
additional_transform_bone_id: IntProperty(
additional_transform_bone_id: bpy.props.IntProperty(
name="Additional Transform Bone ID",
default=-1,
update=_mmd_bone_update_additional_transform,
)
additional_transform_influence: FloatProperty(
additional_transform_influence: bpy.props.FloatProperty(
name="Additional Transform Influence",
description="Additional transform influence",
default=1,
@@ -204,47 +181,44 @@ class MMDBone(PropertyGroup):
soft_max=1,
update=_mmd_bone_update_additional_transform_influence,
)
is_additional_transform_dirty: BoolProperty(
name="",
default=True
)
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True)
def is_id_unique(self):
"""Check if the bone ID is unique"""
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():
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"))
bpy.types.PoseBone.mmd_ik_toggle = patch_library_overridable(
bpy.props.BoolProperty(
name="MMD IK Toggle",
description="MMD IK toggle is used to import/export animation of IK on-off",
update=_pose_bone_update_mmd_ik_toggle,
default=True,
)
)
def register():
"""Register MMD bone properties"""
logger.info("Registering MMD bone properties")
bpy.utils.register_class(MMDBone)
# Add properties to PoseBone
bpy.types.PoseBone.mmd_bone = bpy.props.PointerProperty(type=MMDBone)
bpy.types.PoseBone.is_mmd_shadow_bone = bpy.props.BoolProperty(
name="is_mmd_shadow_bone",
default=False
)
bpy.types.PoseBone.mmd_shadow_bone_type = bpy.props.StringProperty(
name="mmd_shadow_bone_type"
)
bpy.types.PoseBone.mmd_ik_toggle = bpy.props.BoolProperty(
name="MMD IK Toggle",
description="MMD IK toggle is used to import/export animation of IK on-off",
update=_pose_bone_update_mmd_ik_toggle,
default=True,
)
@staticmethod
def unregister():
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 unregister():
"""Unregister MMD bone properties"""
logger.info("Unregistering MMD bone properties")
# Remove properties from PoseBone
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
bpy.utils.unregister_class(MMDBone)
def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context):
v = prop.mmd_ik_toggle
armature_object = cast(bpy.types.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)
c.influence = v
b = b if c.use_tail else b.parent
for b in ([b] + b.parent_recursive)[: c.chain_count]:
c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None)
if c:
c.influence = v
+295
View File
@@ -0,0 +1,295 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
"""Properties for rigid bodies and joints"""
import bpy
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
def _updateCollisionGroup(prop, _context):
obj = prop.id_data
materials = 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
rb = obj.rigid_body
if rb:
rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC
def _updateShape(prop, _context):
obj = prop.id_data
if len(obj.data.vertices) > 0:
size = prop.size
prop.size = size # update mesh
rb = obj.rigid_body
if rb:
rb.collision_shape = prop.shape
def _get_bone(prop):
obj = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation:
arm = relation.target
bone_name = relation.subtarget
if arm is not None and bone_name in arm.data.bones:
return bone_name
return prop.get("bone", "")
def _set_bone(prop, value):
bone_name = value
obj = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation is None:
relation = obj.constraints.new("CHILD_OF")
relation.name = "mmd_tools_rigid_parent"
relation.mute = True
arm = relation.target
if arm is None:
root = FnModel.find_root_object(obj)
if root:
arm = relation.target = FnModel.find_armature_object(root)
if arm is not None and bone_name in arm.data.bones:
relation.subtarget = bone_name
else:
relation.subtarget = bone_name = ""
prop["bone"] = bone_name
def _get_size(prop):
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
assert obj.mode == "OBJECT" # not support other mode yet
shape = prop.shape
mesh = obj.data
rb = obj.rigid_body
if len(mesh.vertices) == 0 or rb is None or rb.collision_shape != shape:
if shape == "SPHERE":
bpyutils.makeSphere(
radius=value[0],
target_object=obj,
)
elif shape == "BOX":
bpyutils.makeBox(
size=value,
target_object=obj,
)
elif shape == "CAPSULE":
bpyutils.makeCapsule(
radius=value[0],
height=value[1],
target_object=obj,
)
mesh.update()
if rb:
rb.collision_shape = shape
else:
if shape == "SPHERE":
radius = max(value[0], 1e-3)
for v in mesh.vertices:
vec = v.co.normalized()
v.co = vec * radius
elif shape == "BOX":
x = max(value[0], 1e-3)
y = max(value[1], 1e-3)
z = max(value[2], 1e-3)
for v in mesh.vertices:
x0, y0, z0 = v.co
x0 = -x if x0 < 0 else x
y0 = -y if y0 < 0 else y
z0 = -z if z0 < 0 else z
v.co = [x0, y0, z0]
elif shape == "CAPSULE":
r0, h0, xx = FnRigidBody.get_rigid_body_size(prop.id_data)
h0 *= 0.5
radius = max(value[0], 1e-3)
height = max(value[1], 1e-3) * 0.5
scale = radius / max(r0, 1e-3)
for v in mesh.vertices:
x0, y0, z0 = v.co
x0 *= scale
y0 *= scale
if z0 < 0:
z0 = (z0 + h0) * scale - height
else:
z0 = (z0 - h0) * scale + height
v.co = [x0, y0, z0]
mesh.update()
def _get_rigid_name(prop):
return prop.get("name", "")
def _set_rigid_name(prop, value):
prop["name"] = value
class MMDRigidBody(bpy.types.PropertyGroup):
name_j: bpy.props.StringProperty(
name="Name",
description="Japanese Name",
default="",
get=_get_rigid_name,
set=_set_rigid_name,
)
name_e: bpy.props.StringProperty(
name="Name(Eng)",
description="English Name",
default="",
)
collision_group_number: bpy.props.IntProperty(
name="Collision Group",
description="The collision group of the object",
min=0,
max=15,
default=1,
update=_updateCollisionGroup,
)
collision_group_mask: bpy.props.BoolVectorProperty(
name="Collision Group Mask",
description="The groups the object can not collide with",
size=16,
subtype="LAYER",
)
type: bpy.props.EnumProperty(
name="Rigid Type",
description="Select rigid type",
items=[
(str(rigid_body.MODE_STATIC), "Bone", "Rigid body's orientation completely determined by attached bone", 1),
(str(rigid_body.MODE_DYNAMIC), "Physics", "Attached bone's orientation completely determined by rigid body", 2),
(str(rigid_body.MODE_DYNAMIC_BONE), "Physics + Bone", "Bone determined by combination of parent and attached rigid body", 3),
],
update=_updateType,
)
shape: bpy.props.EnumProperty(
name="Shape",
description="Select the collision shape",
items=[
("SPHERE", "Sphere", "", 1),
("BOX", "Box", "", 2),
("CAPSULE", "Capsule", "", 3),
],
update=_updateShape,
)
bone: bpy.props.StringProperty(
name="Bone",
description="Target bone",
default="",
get=_get_bone,
set=_set_bone,
)
size: bpy.props.FloatVectorProperty(
name="Size",
description="Size of the object",
subtype="XYZ",
size=3,
min=0,
step=0.1,
get=_get_size,
set=_set_size,
)
@staticmethod
def register():
bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody))
@staticmethod
def unregister():
del bpy.types.Object.mmd_rigid
def _updateSpringLinear(prop, context):
obj = prop.id_data
rbc = obj.rigid_body_constraint
if rbc:
rbc.spring_stiffness_x = prop.spring_linear[0]
rbc.spring_stiffness_y = prop.spring_linear[1]
rbc.spring_stiffness_z = prop.spring_linear[2]
def _updateSpringAngular(prop, context):
obj = 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]
rbc.spring_stiffness_ang_y = prop.spring_angular[1]
rbc.spring_stiffness_ang_z = prop.spring_angular[2]
class MMDJoint(bpy.types.PropertyGroup):
name_j: bpy.props.StringProperty(
name="Name",
description="Japanese Name",
default="",
)
name_e: bpy.props.StringProperty(
name="Name(Eng)",
description="English Name",
default="",
)
spring_linear: bpy.props.FloatVectorProperty(
name="Spring(Linear)",
description="Spring constant of movement",
subtype="XYZ",
size=3,
min=0,
step=0.1,
update=_updateSpringLinear,
)
spring_angular: bpy.props.FloatVectorProperty(
name="Spring(Angular)",
description="Spring constant of rotation",
subtype="XYZ",
size=3,
min=0,
step=0.1,
update=_updateSpringAngular,
)
@staticmethod
def register():
bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint))
@staticmethod
def unregister():
del bpy.types.Object.mmd_joint
+9 -14
View File
@@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
# All credit goes to the original authors.
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
"""Properties for MMD model root object"""
@@ -500,26 +499,22 @@ class MMDRoot(bpy.types.PropertyGroup):
@staticmethod
def __get_select(prop: bpy.types.Object) -> bool:
# TODO: Object.select is deprecated since v4.0.0, use Object.select_get() method instead
# utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead")
utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead")
return prop.select_get()
@staticmethod
def __set_select(prop: bpy.types.Object, value: bool) -> None:
# TODO: Object.select is deprecated since v4.0.0, use Object.select_set() method instead
# utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead")
utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead")
prop.select_set(value)
@staticmethod
def __get_hide(prop: bpy.types.Object) -> bool:
# TODO: Object.hide is deprecated since v4.0.0, use Object.hide_get() method instead
# utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead")
utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead")
return prop.hide_get()
@staticmethod
def __set_hide(prop: bpy.types.Object, value: bool) -> None:
# TODO: Object.hide is deprecated since v4.0.0, use Object.hide_set() method instead
# utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead")
utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead")
prop.hide_set(value)
if prop.hide_viewport != value:
prop.hide_viewport = value
@@ -579,4 +574,4 @@ class MMDRoot(bpy.types.PropertyGroup):
del bpy.types.Object.hide
del bpy.types.Object.select
del bpy.types.Object.mmd_root
del bpy.types.Object.mmd_type
del bpy.types.Object.mmd_type
+127
View File
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Dict, List, Optional, Tuple
import bpy
from ..core.translations import FnTranslations, MMDTranslationElementType
from ..translations import DictionaryEnum
MMD_TRANSLATION_ELEMENT_TYPE_ENUM_ITEMS = [
(MMDTranslationElementType.BONE.name, MMDTranslationElementType.BONE.value, "Bones", 1),
(MMDTranslationElementType.MORPH.name, MMDTranslationElementType.MORPH.value, "Morphs", 2),
(MMDTranslationElementType.MATERIAL.name, MMDTranslationElementType.MATERIAL.value, "Materials", 4),
(MMDTranslationElementType.DISPLAY.name, MMDTranslationElementType.DISPLAY.value, "Display frames", 8),
(MMDTranslationElementType.PHYSICS.name, MMDTranslationElementType.PHYSICS.value, "Rigidbodies and joints", 16),
(MMDTranslationElementType.INFO.name, MMDTranslationElementType.INFO.value, "Model name and comments", 32),
]
class MMDTranslationElement(bpy.types.PropertyGroup):
type: bpy.props.EnumProperty(items=MMD_TRANSLATION_ELEMENT_TYPE_ENUM_ITEMS)
object: bpy.props.PointerProperty(type=bpy.types.Object)
data_path: bpy.props.StringProperty()
name: bpy.props.StringProperty()
name_j: bpy.props.StringProperty()
name_e: bpy.props.StringProperty()
class MMDTranslationElementIndex(bpy.types.PropertyGroup):
value: bpy.props.IntProperty()
BATCH_OPERATION_SCRIPT_PRESETS: Dict[str, Tuple[Optional[str], str, str, int]] = {
"NOTHING": ("", "", "", 1),
"CLEAR": (None, "Clear", '""', 10),
"TO_ENGLISH": ("BLENDER", "Translate to English", "to_english(name)", 2),
"TO_MMD_LR": ("JAPANESE", "Blender L/R to MMD L/R", "to_mmd_lr(name)", 3),
"TO_BLENDER_LR": ("BLENDER", "MMD L/R to Blender L/R", "to_blender_lr(name_j)", 4),
"RESTORE_BLENDER": ("BLENDER", "Restore Blender Names", "org_name", 5),
"RESTORE_JAPANESE": ("JAPANESE", "Restore Japanese MMD Names", "org_name_j", 6),
"RESTORE_ENGLISH": ("ENGLISH", "Restore English MMD Names", "org_name_e", 7),
"ENGLISH_IF_EMPTY_JAPANESE": (None, "Copy English MMD Names, if empty copy Japanese MMD Name", "name_e if name_e else name_j", 8),
"JAPANESE_IF_EMPTY_ENGLISH": (None, "Copy Japanese MMD Names, if empty copy English MMD Name", "name_j if name_j else name_e", 9),
}
BATCH_OPERATION_SCRIPT_PRESET_ITEMS: List[Tuple[str, str, str, int]] = [(k, t[1], t[2], t[3]) for k, t in BATCH_OPERATION_SCRIPT_PRESETS.items()]
class MMDTranslation(bpy.types.PropertyGroup):
@staticmethod
def _update_index(mmd_translation: "MMDTranslation", _context):
FnTranslations.update_index(mmd_translation)
@staticmethod
def _collect_data(mmd_translation: "MMDTranslation", _context):
FnTranslations.collect_data(mmd_translation)
@staticmethod
def _update_query(mmd_translation: "MMDTranslation", _context):
FnTranslations.update_query(mmd_translation)
@staticmethod
def _update_batch_operation_script_preset(mmd_translation: "MMDTranslation", _context):
if mmd_translation.batch_operation_script_preset == "NOTHING":
return
id2scripts: Dict[str, str] = {i[0]: i[2] for i in BATCH_OPERATION_SCRIPT_PRESET_ITEMS}
batch_operation_script = id2scripts.get(mmd_translation.batch_operation_script_preset)
if batch_operation_script is None:
return
mmd_translation.batch_operation_script = batch_operation_script
batch_operation_target = BATCH_OPERATION_SCRIPT_PRESETS[mmd_translation.batch_operation_script_preset][0]
if batch_operation_target:
mmd_translation.batch_operation_target = batch_operation_target
translation_elements: bpy.props.CollectionProperty(type=MMDTranslationElement)
filtered_translation_element_indices_active_index: bpy.props.IntProperty(update=_update_index.__func__)
filtered_translation_element_indices: bpy.props.CollectionProperty(type=MMDTranslationElementIndex)
filter_japanese_blank: bpy.props.BoolProperty(name="Japanese Blank", default=False, update=_update_query.__func__)
filter_english_blank: bpy.props.BoolProperty(name="English Blank", default=False, update=_update_query.__func__)
filter_restorable: bpy.props.BoolProperty(name="Restorable", default=False, update=_update_query.__func__)
filter_selected: bpy.props.BoolProperty(name="Selected", default=False, update=_update_query.__func__)
filter_visible: bpy.props.BoolProperty(name="Visible", default=False, update=_update_query.__func__)
filter_types: bpy.props.EnumProperty(
items=MMD_TRANSLATION_ELEMENT_TYPE_ENUM_ITEMS,
default={
"BONE",
"MORPH",
"MATERIAL",
"DISPLAY",
"PHYSICS",
},
options={"ENUM_FLAG"},
update=_update_query.__func__,
)
dictionary: bpy.props.EnumProperty(
items=DictionaryEnum.get_dictionary_items,
name="Dictionary",
)
batch_operation_target: bpy.props.EnumProperty(
items=[
("BLENDER", "Blender Name (name)", "", 1),
("JAPANESE", "Japanese MMD Name (name_j)", "", 2),
("ENGLISH", "English MMD Name (name_e)", "", 3),
],
name="Operation Target",
default="JAPANESE",
)
batch_operation_script_preset: bpy.props.EnumProperty(
items=BATCH_OPERATION_SCRIPT_PRESET_ITEMS,
name="Operation Script Preset",
default="NOTHING",
update=_update_batch_operation_script_preset.__func__,
)
batch_operation_script: bpy.props.StringProperty()
+461
View File
@@ -0,0 +1,461 @@
# -*- coding: utf-8 -*-
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import csv
import logging
import time
import bpy
from .bpyutils import FnContext
jp_half_to_full_tuples = (
("ヴ", ""),
("ガ", ""),
("ギ", ""),
("グ", ""),
("ゲ", ""),
("ゴ", ""),
("ザ", ""),
("ジ", ""),
("ズ", ""),
("ゼ", ""),
("ゾ", ""),
("ダ", ""),
("ヂ", ""),
("ヅ", ""),
("デ", ""),
("ド", ""),
("バ", ""),
("パ", ""),
("ビ", ""),
("ピ", ""),
("ブ", ""),
("プ", ""),
("ベ", ""),
("ペ", ""),
("ボ", ""),
("ポ", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("ソ", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
("", ""),
)
jp_to_en_tuples = [
("全ての親", "ParentNode"),
("操作中心", "ControlNode"),
("センター", "Center"),
("センター", "Center"),
("グループ", "Group"),
("グルーブ", "Groove"),
("キャンセル", "Cancel"),
("上半身", "UpperBody"),
("下半身", "LowerBody"),
("手首", "Wrist"),
("足首", "Ankle"),
("", "Neck"),
("", "Head"),
("", "Face"),
("下顎", "Chin"),
("下あご", "Chin"),
("あご", "Jaw"),
("", "Jaw"),
("両目", "Eyes"),
("", "Eye"),
("", "Eyebrow"),
("", "Tongue"),
("", "Tears"),
("泣き", "Cry"),
("", "Teeth"),
("照れ", "Blush"),
("青ざめ", "Pale"),
("ガーン", "Gloom"),
("", "Sweat"),
("", "Anger"),
("感情", "Emotion"),
("", "Marks"),
("暗い", "Dark"),
("", "Waist"),
("", "Hair"),
("三つ編み", "Braid"),
("", "Breast"),
("", "Boob"),
("おっぱい", "Tits"),
("", "Muscle"),
("", "Belly"),
("鎖骨", "Clavicle"),
("", "Shoulder"),
("", "Arm"),
("うで", "Arm"),
("ひじ", "Elbow"),
("", "Elbow"),
("", "Hand"),
("親指", "Thumb"),
("人指", "IndexFinger"),
("人差指", "IndexFinger"),
("中指", "MiddleFinger"),
("薬指", "RingFinger"),
("小指", "LittleFinger"),
("", "Leg"),
("ひざ", "Knee"),
("つま", "Toe"),
("", "Sleeve"),
("新規", "New"),
("ボーン", "Bone"),
("", "Twist"),
("回転", "Rotation"),
("", "Axis"),
("ネクタイ", "Necktie"),
("ネクタイ", "Necktie"),
("ヘッドセット", "Headset"),
("飾り", "Accessory"),
("リボン", "Ribbon"),
("", "Collar"),
("", "String"),
("コード", "Cord"),
("イヤリング", "Earring"),
("メガネ", "Eyeglasses"),
("眼鏡", "Glasses"),
("帽子", "Hat"),
("スカート", "Skirt"),
("スカート", "Skirt"),
("パンツ", "Pantsu"),
("シャツ", "Shirt"),
("フリル", "Frill"),
("マフラー", "Muffler"),
("マフラー", "Muffler"),
("", "Clothes"),
("ブーツ", "Boots"),
("ねこみみ", "CatEars"),
("ジップ", "Zip"),
("ジップ", "Zip"),
("ダミー", "Dummy"),
("ダミー", "Dummy"),
("", "Category"),
("あほ毛", "Antenna"),
("アホ毛", "Antenna"),
("モミアゲ", "Sideburn"),
("もみあげ", "Sideburn"),
("ツインテ", "Twintail"),
("おさげ", "Pigtail"),
("ひらひら", "Flutter"),
("調整", "Adjustment"),
("補助", "Aux"),
("", "Right"),
("", "Left"),
("", "Front"),
("後ろ", "Behind"),
("", "Back"),
("", "Side"),
("", "Middle"),
("", "Upper"),
("", "Lower"),
("", "Parent"),
("", "Tip"),
("パーツ", "Part"),
("", "Light"),
("", "Return"),
("", "Wing"),
("", "Base"), # ideally 'Root' but to avoid confusion
("", "Strand"),
("", "Tail"),
("", "Butt"),
# full-width unicode forms I think: https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms
("", "0"),
("", "1"),
("", "2"),
("", "3"),
("", "4"),
("", "5"),
("", "6"),
("", "7"),
("", "8"),
("", "9"),
("", "a"),
("", "b"),
("", "c"),
("", "d"),
("", "e"),
("", "f"),
("", "g"),
("", "h"),
("", "i"),
("", "j"),
("", "k"),
("", "l"),
("", "m"),
("", "n"),
("", "o"),
("", "p"),
("", "q"),
("", "r"),
("", "s"),
("", "t"),
("", "u"),
("", "v"),
("", "w"),
("", "x"),
("", "y"),
("", "z"),
("", "A"),
("", "B"),
("", "C"),
("", "D"),
("", "E"),
("", "F"),
("", "G"),
("", "H"),
("", "I"),
("", "J"),
("", "K"),
("", "L"),
("", "M"),
("", "N"),
("", "O"),
("", "P"),
("", "Q"),
("", "R"),
("", "S"),
("", "T"),
("", "U"),
("", "V"),
("", "W"),
("", "X"),
("", "Y"),
("", "Z"),
("", "+"),
("", "-"),
("_", "_"),
("", "/"),
(".", "_"), # probably should be combined with the global 'use underscore' option
]
def translateFromJp(name):
for tuple in jp_to_en_tuples:
if tuple[0] in name:
name = name.replace(tuple[0], tuple[1])
return name
def getTranslator(csvfile="", keep_order=False):
translator = MMDTranslator()
if isinstance(csvfile, bpy.types.Text):
translator.load_from_stream(csvfile)
elif isinstance(csvfile, dict):
translator.csv_tuples.extend(csvfile.items())
elif csvfile in bpy.data.texts.keys():
translator.load_from_stream(bpy.data.texts[csvfile])
else:
translator.load(csvfile)
if not keep_order:
translator.sort()
translator.update()
return translator
class MMDTranslator:
def __init__(self):
self.__csv_tuples = []
self.__fails = {}
@staticmethod
def default_csv_filepath():
return __file__[:-3] + ".csv"
@staticmethod
def get_csv_text(text_name=None):
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:
csv_text = bpy.data.texts.new(text_name)
return csv_text
@staticmethod
def replace_from_tuples(name, tuples):
for pair in tuples:
if pair[0] in name:
name = name.replace(pair[0], pair[1])
return name
@property
def csv_tuples(self):
return self.__csv_tuples
@property
def fails(self):
return self.__fails
def sort(self):
self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row))
def update(self):
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)
def half_to_full(self, name):
return self.replace_from_tuples(name, jp_half_to_full_tuples)
def is_translated(self, name):
try:
name.encode("ascii", errors="strict")
except UnicodeEncodeError:
return False
return True
def translate(self, name, default=None, from_full_width=True):
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):
self.__fails[name] = name_new
return default
return name_new
def save_fails(self, text_name=None):
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))
return txt
def load_from_stream(self, csvfile=None):
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))
def save_to_stream(self, csvfile=None):
csvfile = csvfile or self.get_csv_text()
lineterminator = "\r\n"
if isinstance(csvfile, bpy.types.Text):
csvfile.clear()
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))
def load(self, filepath=None):
filepath = filepath or self.default_csv_filepath()
logging.info("Loading csv file:\t%s", filepath)
with open(filepath, "rt", encoding="utf-8", newline="") as csvfile:
self.load_from_stream(csvfile)
def save(self, filepath=None):
filepath = filepath or self.default_csv_filepath()
logging.info("Saving csv file:\t%s", filepath)
with open(filepath, "wt", encoding="utf-8", newline="") as csvfile:
self.save_to_stream(csvfile)
class DictionaryEnum:
__items_ttl = 0.0
__items_cache = None
@staticmethod
def get_dictionary_items(prop, context):
if DictionaryEnum.__items_ttl > time.time():
return DictionaryEnum.__items_cache
DictionaryEnum.__items_ttl = time.time() + 5
DictionaryEnum.__items_cache = items = []
if "import" in prop.bl_rna.identifier:
items.append(("DISABLED", "Disabled", "", 0))
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)))
import os
folder = FnContext.get_addon_preferences_attribute(context, "dictionary_folder", "")
if os.path.isdir(folder):
for filename in sorted(x for x in os.listdir(folder) if x.lower().endswith(".csv")):
filepath = os.path.join(folder, filename)
if os.path.isfile(filepath):
items.append((filepath, filename, filepath, "FILE", len(items)))
if "dictionary" in prop:
prop["dictionary"] = min(prop["dictionary"], len(items) - 1)
return items
@staticmethod
def get_translator(dictionary):
if dictionary == "DISABLED":
return None
if dictionary == "INTERNAL":
return getTranslator(dict(jp_to_en_tuples))
return getTranslator(dictionary)
+133 -95
View File
@@ -1,85 +1,75 @@
# -*- coding: utf-8 -*-
# Copyright 2013 MMD Tools authors
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
# All credit goes to the original authors.
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
# Copyright 2014 MMD Tools authors
# This file was originally part of the MMD Tools add-on for Blender
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import logging
import os
import re
from typing import Callable, Optional, Set, List, Dict, Any
from typing import Callable, Optional, Set
import bpy
from bpy.types import Object, Context, Bone, PoseBone
from ...logging_setup import logger
from .bpyutils import FnContext
def selectAObject(obj: Object) -> None:
"""Select a single object and make it active"""
## 指定したオブジェクトのみを選択状態かつアクティブにする
def selectAObject(obj):
try:
bpy.ops.object.mode_set(mode="OBJECT")
except Exception:
logger.debug(f"Failed to set object mode for {obj.name}")
pass
bpy.ops.object.select_all(action="DESELECT")
FnContext.select_object(FnContext.ensure_context(), obj)
FnContext.set_active_object(FnContext.ensure_context(), obj)
def enterEditMode(obj: Object) -> None:
"""Enter edit mode for the specified object"""
## 現在のモードを指定したオブジェクトのEdit Modeに変更する
def enterEditMode(obj):
selectAObject(obj)
if obj.mode != "EDIT":
bpy.ops.object.mode_set(mode="EDIT")
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
"""Set an object's parent to a specific bone"""
def setParentToBone(obj, parent, bone_name):
selectAObject(obj)
FnContext.set_active_object(FnContext.ensure_context(), parent)
bpy.ops.object.mode_set(mode="POSE")
parent.data.bones.active = parent.data.bones[bone_name]
bpy.ops.object.parent_set(type="BONE", keep_transform=False)
bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False)
bpy.ops.object.mode_set(mode="OBJECT")
def selectSingleBone(context: Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None:
"""Select a single bone in an armature"""
def selectSingleBone(context, armature, bone_name, reset_pose=False):
try:
bpy.ops.object.mode_set(mode="OBJECT")
except Exception:
logger.debug(f"Failed to set object mode for bone selection: {bone_name}")
except:
pass
for i in context.selected_objects:
i.select_set(False)
FnContext.set_active_object(context, armature)
bpy.ops.object.mode_set(mode="POSE")
if reset_pose:
for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity()
armature_bones = armature.data.bones
for bone in armature_bones:
bone.select = bone.name == bone_name
bone.select_head = bone.select_tail = bone.select
if bone.select:
armature_bones.active = bone
bone.hide = False
armature_bones: bpy.types.ArmatureBones = armature.data.bones
i: bpy.types.Bone
for i in armature_bones:
i.select = i.name == bone_name
i.select_head = i.select_tail = i.select
if i.select:
armature_bones.active = i
i.hide = False
# Regular expressions for name conversion
__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$")
__CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
def convertNameToLR(name: str, use_underscore: bool = False) -> str:
"""Convert Japanese left/right naming to Blender's L/R convention"""
## 日本語で左右を命名されている名前をblender方式のL(R)に変更する
def convertNameToLR(name, use_underscore=False):
m = __CONVERT_NAME_TO_L_REGEXP.match(name)
delimiter = "_" if use_underscore else "."
if m:
@@ -94,8 +84,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: str) -> str:
"""Convert Blender's L/R convention to Japanese left/right naming"""
def convertLRToName(name):
match = __CONVERT_L_TO_NAME_REGEXP.search(name)
if match:
return f"{name[0:match.start()]}{match['after']}{name[match.end():]}"
@@ -107,8 +96,8 @@ def convertLRToName(name: str) -> str:
return name
def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None:
"""Merge weights from source vertex group to destination vertex group"""
## src_vertex_groupのWeightをdest_vertex_groupにaddする
def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name):
mesh = meshObj.data
src_vertex_group = meshObj.vertex_groups[src_vertex_group_name]
dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name]
@@ -122,38 +111,30 @@ def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_gr
pass
def separateByMaterials(meshObj: Object) -> None:
"""Separate a mesh object by materials"""
def separateByMaterials(meshObj: bpy.types.Object):
if len(meshObj.data.materials) < 2:
selectAObject(meshObj)
return
matrix_parent_inverse = meshObj.matrix_parent_inverse.copy()
prev_parent = meshObj.parent
dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
bpy.context.collection.objects.link(dummy_parent)
meshObj.parent = dummy_parent
meshObj.active_shape_key_index = 0
try:
enterEditMode(meshObj)
bpy.ops.mesh.select_all(action="SELECT")
bpy.ops.mesh.separate(type="MATERIAL")
finally:
bpy.ops.object.mode_set(mode="OBJECT")
for i in dummy_parent.children:
materials = i.data.materials
i.name = getattr(materials[0], "name", "None") if len(materials) else "None"
i.parent = prev_parent
i.matrix_parent_inverse = matrix_parent_inverse
bpy.data.objects.remove(dummy_parent)
def clearUnusedMeshes() -> None:
"""Remove unused mesh data blocks"""
def clearUnusedMeshes():
meshes_to_delete = []
for mesh in bpy.data.meshes:
if mesh.users == 0:
@@ -163,44 +144,72 @@ def clearUnusedMeshes() -> None:
bpy.data.meshes.remove(mesh)
def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]:
"""Create a mapping from bone names to pose bones"""
return {(i.mmd_bone.name_j or i.name): i for i in armObj.pose.bones}
## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を
# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成
def makePmxBoneMap(armObj):
# 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}
__REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$")
def unique_name(name: str, used_names: Set[str]) -> str:
"""Create a unique name that doesn't exist in the used_names set
"""Helper function for storing unique names.
This function is a limited and simplified version of bpy_extras.io_utils.unique_name.
Args:
name (str): The name to make unique
used_names (Set[str]): A set of names that are already used
name (str): The name to make unique.
used_names (Set[str]): A set of names that are already used.
Returns:
str: The unique name, formatted as "{name}.{number:03d}"
str: The unique name, formatted as "{name}.{number:03d}".
"""
if name not in used_names:
return name
count = 1
new_name = orig_name = __REMOVE_PREFIX_DIGITS_REGEXP.sub("", name)
while new_name in used_names:
new_name = f"{orig_name}.{count:03d}"
count += 1
return new_name
def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
"""Safely get a relative path, handling different drive issues on Windows
def int2base(x, base, width=0):
"""
Method to convert an int to a base
Source: http://stackoverflow.com/questions/2267362
"""
import string
digs = string.digits + string.ascii_uppercase
assert 2 <= base <= len(digs)
digits, negtive = "", False
if x <= 0:
if x == 0:
return "0" * max(1, width)
x, negtive, width = -x, True, width - 1
while x:
digits = digs[x % base] + digits
x //= base
digits = "0" * (width - len(digits)) + digits
if negtive:
digits = "-" + digits
return digits
def saferelpath(path, start, strategy="inside"):
"""
On Windows relpath will raise a ValueError
when trying to calculate the relative path to a
different drive.
This method will behave different depending on the strategy
choosen to handle the different drive issue.
Strategies:
- inside: returns the basename of the path
- outside: prepends '..' to the basename if on different drive
- absolute: returns the absolute path
- inside: this will just return the basename of the path given
- outside: this will prepend '..' to the basename
- absolute: this will return the absolute path instead of a relative.
See http://bugs.python.org/issue7195
"""
if strategy == "inside":
return os.path.basename(path)
@@ -216,20 +225,15 @@ def saferelpath(path: str, start: str, strategy: str = "inside") -> str:
return os.path.relpath(path, start)
class ItemOp:
"""Operations for managing collections of items"""
@staticmethod
def get_by_index(items: List[Any], index: int) -> Optional[Any]:
"""Get an item by index with bounds checking"""
def get_by_index(items, index):
if 0 <= index < len(items):
return items[index]
return None
@staticmethod
def resize(items: bpy.types.bpy_prop_collection, length: int) -> None:
"""Resize a collection to the specified length"""
def resize(items: bpy.types.bpy_prop_collection, length: int):
count = length - len(items)
if count > 0:
for i in range(count):
@@ -239,8 +243,7 @@ class ItemOp:
items.remove(length)
@staticmethod
def add_after(items: bpy.types.bpy_prop_collection, index: int) -> tuple:
"""Add a new item after the specified index"""
def add_after(items, index):
index_end = len(items)
index = max(0, min(index_end, index + 1))
items.add()
@@ -249,28 +252,24 @@ class ItemOp:
class ItemMoveOp:
"""Operations for moving items in collections"""
type: bpy.props.EnumProperty(
name="Type",
description="Move type",
items=[
("UP", "Up", "", 0),
("DOWN", "Down", "", 1),
("TOP", "Top", "", 2),
("BOTTOM", "Bottom", "", 3),
],
default="UP",
)
@staticmethod
def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str,
index_min: int = 0, index_max: Optional[int] = None) -> int:
"""Move an item in a collection
Args:
items: The collection to modify
index: Current index of the item
move_type: Type of move ('UP', 'DOWN', 'TOP', 'BOTTOM')
index_min: Minimum allowed index
index_max: Maximum allowed index
Returns:
int: The new index after moving
"""
def move(items, index, move_type, index_min=0, index_max=None):
if index_max is None:
index_max = len(items) - 1
else:
index_max = min(index_max, len(items) - 1)
index_min = min(index_min, index_max)
if index < index_min:
@@ -292,5 +291,44 @@ class ItemMoveOp:
if index_new != index:
items.move(index, index_new)
return index_new
def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None):
"""Decorator to mark a function as deprecated.
Args:
deprecated_in (Optional[str]): Version in which the function was deprecated.
details (Optional[str]): Additional details about the deprecation.
Returns:
Callable: The decorated function.
"""
def _function_wrapper(function: Callable):
def _inner_wrapper(*args, **kwargs):
warn_deprecation(function.__name__, deprecated_in, details)
return function(*args, **kwargs)
return _inner_wrapper
return _function_wrapper
def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None:
"""Reports a deprecation warning.
Args:
function_name (str): Name of the deprecated function.
deprecated_in (Optional[str]): Version in which the function was deprecated.
details (Optional[str]): Additional details about the deprecation.
"""
logging.warning(
"%s is deprecated%s%s",
function_name,
f" since {deprecated_in}" if deprecated_in else "",
f": {details}" if details else "",
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)
+240
View File
@@ -0,0 +1,240 @@
# -*- 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)