PMX Import now works
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -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 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 = {}
|
||||
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: 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
|
||||
keywords["selected_objects"] = selected_objects
|
||||
keywords["selected_editable_objects"] = selected_objects
|
||||
|
||||
override_dict.update(keywords)
|
||||
|
||||
with context.temp_override(**override_dict) as override_context:
|
||||
yield override_context
|
||||
|
||||
@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)
|
||||
@@ -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.
|
||||
@@ -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,38 +236,18 @@ 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
|
||||
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)
|
||||
|
||||
progress.step("Processing bones")
|
||||
|
||||
force_align = True
|
||||
with common.edit_object(armature_object) as data:
|
||||
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:
|
||||
@@ -281,8 +280,6 @@ class FnBone:
|
||||
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)
|
||||
@@ -291,7 +288,7 @@ class FnBone:
|
||||
@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,17 +297,14 @@ 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:
|
||||
continue
|
||||
mmd_bone = b.mmd_bone
|
||||
mmd_bone: MMDBone = b.mmd_bone
|
||||
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
|
||||
|
||||
progress.step("Processing bones")
|
||||
|
||||
with common.edit_object(armature_object) as data:
|
||||
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:
|
||||
@@ -336,15 +330,11 @@ 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:
|
||||
with bpyutils.edit_object(armature) as data:
|
||||
bone: bpy.types.EditBone
|
||||
for bone in data.edit_bones:
|
||||
if bone.name not in bone_names:
|
||||
@@ -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,12 +397,11 @@ 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
|
||||
@@ -426,8 +412,6 @@ class FnBone:
|
||||
|
||||
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:
|
||||
@@ -435,10 +419,8 @@ class FnBone:
|
||||
if sb:
|
||||
shadow_bone_pool.append(sb)
|
||||
|
||||
progress.step("Setting up shadow bones")
|
||||
|
||||
# setup shadow bones
|
||||
with common.edit_object(armature_object) as data:
|
||||
with bpyutils.edit_object(armature_object) as data:
|
||||
edit_bones = data.edit_bones
|
||||
for sb in shadow_bone_pool:
|
||||
sb.update_edit_bones(edit_bones)
|
||||
@@ -447,8 +429,6 @@ class FnBone:
|
||||
for sb in shadow_bone_pool:
|
||||
sb.update_pose_bones(pose_bones)
|
||||
|
||||
progress.step("Finalizing")
|
||||
|
||||
# finish
|
||||
for p_bone in dirty_bones:
|
||||
p_bone.mmd_bone.is_additional_transform_dirty = False
|
||||
@@ -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,7 +481,6 @@ 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
|
||||
@@ -509,8 +488,6 @@ class MigrationFnBone:
|
||||
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
|
||||
constraint.owner_space = "LOCAL"
|
||||
|
||||
progress.step("Fixed IK limit overrides")
|
||||
|
||||
|
||||
class _AT_ShadowBoneRemove:
|
||||
def __init__(self, bone_name):
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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,7 +264,9 @@ 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+
|
||||
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
|
||||
@@ -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)
|
||||
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+
|
||||
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,8 +406,7 @@ 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+
|
||||
if hasattr(mat, "shadow_method"):
|
||||
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
|
||||
|
||||
def update_self_shadow(self):
|
||||
@@ -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,20 +470,24 @@ 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+
|
||||
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:
|
||||
@@ -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
@@ -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,43 +839,35 @@ 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")
|
||||
|
||||
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")
|
||||
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()
|
||||
|
||||
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)
|
||||
@@ -908,47 +879,38 @@ class PMXImporter:
|
||||
FnBone.apply_additional_transformation(self.__armObj)
|
||||
|
||||
if "PHYSICS" in types:
|
||||
progress.step("Importing rigid bodies")
|
||||
self.__importRigids()
|
||||
progress.step("Importing joints")
|
||||
self.__importJoints()
|
||||
|
||||
if "DISPLAY" in types:
|
||||
progress.step("Importing display frames")
|
||||
self.__importDisplayFrames()
|
||||
else:
|
||||
self.__rig.initialDisplayFrames()
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
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)
|
||||
@@ -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
|
||||
@@ -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 = {}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"""
|
||||
@@ -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,43 +47,28 @@ 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,
|
||||
@@ -99,41 +76,41 @@ class MMDBone(PropertyGroup):
|
||||
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",
|
||||
@@ -143,7 +120,7 @@ class MMDBone(PropertyGroup):
|
||||
default=[1, 0, 0],
|
||||
)
|
||||
|
||||
local_axis_z: FloatVectorProperty(
|
||||
local_axis_z: bpy.props.FloatVectorProperty(
|
||||
name="Local Z-Axis",
|
||||
description="Local z-axis",
|
||||
subtype="XYZ",
|
||||
@@ -153,13 +130,13 @@ class MMDBone(PropertyGroup):
|
||||
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",
|
||||
@@ -168,21 +145,21 @@ class MMDBone(PropertyGroup):
|
||||
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,
|
||||
@@ -190,13 +167,13 @@ class MMDBone(PropertyGroup):
|
||||
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,
|
||||
@@ -205,46 +182,43 @@ class MMDBone(PropertyGroup):
|
||||
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():
|
||||
"""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(
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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", "0"),
|
||||
("1", "1"),
|
||||
("2", "2"),
|
||||
("3", "3"),
|
||||
("4", "4"),
|
||||
("5", "5"),
|
||||
("6", "6"),
|
||||
("7", "7"),
|
||||
("8", "8"),
|
||||
("9", "9"),
|
||||
("a", "a"),
|
||||
("b", "b"),
|
||||
("c", "c"),
|
||||
("d", "d"),
|
||||
("e", "e"),
|
||||
("f", "f"),
|
||||
("g", "g"),
|
||||
("h", "h"),
|
||||
("i", "i"),
|
||||
("j", "j"),
|
||||
("k", "k"),
|
||||
("l", "l"),
|
||||
("m", "m"),
|
||||
("n", "n"),
|
||||
("o", "o"),
|
||||
("p", "p"),
|
||||
("q", "q"),
|
||||
("r", "r"),
|
||||
("s", "s"),
|
||||
("t", "t"),
|
||||
("u", "u"),
|
||||
("v", "v"),
|
||||
("w", "w"),
|
||||
("x", "x"),
|
||||
("y", "y"),
|
||||
("z", "z"),
|
||||
("A", "A"),
|
||||
("B", "B"),
|
||||
("C", "C"),
|
||||
("D", "D"),
|
||||
("E", "E"),
|
||||
("F", "F"),
|
||||
("G", "G"),
|
||||
("H", "H"),
|
||||
("I", "I"),
|
||||
("J", "J"),
|
||||
("K", "K"),
|
||||
("L", "L"),
|
||||
("M", "M"),
|
||||
("N", "N"),
|
||||
("O", "O"),
|
||||
("P", "P"),
|
||||
("Q", "Q"),
|
||||
("R", "R"),
|
||||
("S", "S"),
|
||||
("T", "T"),
|
||||
("U", "U"),
|
||||
("V", "V"),
|
||||
("W", "W"),
|
||||
("X", "X"),
|
||||
("Y", "Y"),
|
||||
("Z", "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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user