Update Logging
You can choose between errors, warning, info or full debug, errors will always log to ensure we don't have silent failures with debug on or off.
This commit is contained in:
@@ -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'
|
||||
@@ -0,0 +1,521 @@
|
||||
# -*- 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 contextlib
|
||||
from typing import Generator, List, Optional, TypeVar
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
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:
|
||||
def __init__(self, obj):
|
||||
if not isinstance(obj, bpy.types.Object):
|
||||
raise ValueError
|
||||
self.__prevMode = obj.mode
|
||||
self.__obj = obj
|
||||
self.__obj_select = obj.select_get()
|
||||
with select_object(obj):
|
||||
if obj.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
def __enter__(self):
|
||||
return self.__obj.data
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self.__prevMode == "EDIT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT") # update edited data
|
||||
bpy.ops.object.mode_set(mode=self.__prevMode)
|
||||
self.__obj.select_set(self.__obj_select)
|
||||
|
||||
|
||||
class __SelectObjects:
|
||||
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
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
contenxt = FnContext.ensure_context()
|
||||
|
||||
for i in contenxt.selected_objects:
|
||||
i.select_set(False)
|
||||
|
||||
self.__active_object = active_object
|
||||
self.__selected_objects = tuple(set(selected_objects) | set([active_object])) if selected_objects else (active_object,)
|
||||
|
||||
self.__hides: List[bool] = []
|
||||
for i in self.__selected_objects:
|
||||
self.__hides.append(i.hide_get())
|
||||
FnContext.select_object(contenxt, i)
|
||||
FnContext.set_active_object(contenxt, active_object)
|
||||
|
||||
def __enter__(self) -> bpy.types.Object:
|
||||
return self.__active_object
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
for i, j in zip(self.__selected_objects, self.__hides):
|
||||
i.hide_set(j)
|
||||
|
||||
|
||||
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, 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]
|
||||
bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False)
|
||||
bpy.ops.object.mode_set(mode="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.
|
||||
|
||||
with edit_object:
|
||||
some functions...
|
||||
"""
|
||||
return __EditMode(obj)
|
||||
|
||||
|
||||
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.
|
||||
This function can select "hidden" objects safely.
|
||||
|
||||
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, total_len):
|
||||
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
|
||||
|
||||
|
||||
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=8, ring_count=5, radius=1.0, target_object=None):
|
||||
import bmesh
|
||||
|
||||
if target_object is None:
|
||||
target_object = createObject(name="Sphere")
|
||||
|
||||
mesh = target_object.data
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_uvsphere(
|
||||
bm,
|
||||
u_segments=segment,
|
||||
v_segments=ring_count,
|
||||
radius=radius,
|
||||
)
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
return target_object
|
||||
|
||||
|
||||
def makeBox(size=(1, 1, 1), target_object=None):
|
||||
import bmesh
|
||||
from mathutils import Matrix
|
||||
|
||||
if target_object is None:
|
||||
target_object = createObject(name="Box")
|
||||
|
||||
mesh = target_object.data
|
||||
bm = bmesh.new()
|
||||
bmesh.ops.create_cube(
|
||||
bm,
|
||||
size=2,
|
||||
matrix=Matrix([[size[0], 0, 0, 0], [0, size[1], 0, 0], [0, 0, size[2], 0], [0, 0, 0, 1]]),
|
||||
)
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
return target_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:
|
||||
target_object = createObject(name="Capsule")
|
||||
height = max(height, 1e-3)
|
||||
|
||||
mesh = target_object.data
|
||||
bm = bmesh.new()
|
||||
verts = bm.verts
|
||||
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)
|
||||
t = math.sqrt(radius**2 - z**2)
|
||||
for j in range(segment):
|
||||
theta = 2 * math.pi / segment * j
|
||||
x = t * math.sin(-theta)
|
||||
y = t * math.cos(-theta)
|
||||
verts.new((x, y, z + height / 2))
|
||||
|
||||
for i in range(ring_count):
|
||||
z = -f(i)
|
||||
t = math.sqrt(radius**2 - z**2)
|
||||
for j in range(segment):
|
||||
theta = 2 * math.pi / segment * j
|
||||
x = t * math.sin(-theta)
|
||||
y = t * math.cos(-theta)
|
||||
verts.new((x, y, z - height / 2))
|
||||
|
||||
bottom = (0, 0, -(height / 2 + radius))
|
||||
verts.new(bottom)
|
||||
if hasattr(verts, "ensure_lookup_table"):
|
||||
verts.ensure_lookup_table()
|
||||
|
||||
faces = bm.faces
|
||||
for i in range(1, segment):
|
||||
faces.new([verts[x] for x in (0, i, i + 1)])
|
||||
faces.new([verts[x] for x in (0, segment, 1)])
|
||||
offset = segment + 1
|
||||
for i in range(ring_count * 2 - 1):
|
||||
for j in range(segment - 1):
|
||||
t = offset + j
|
||||
faces.new([verts[x] for x in (t - segment, t, t + 1, t - segment + 1)])
|
||||
faces.new([verts[x] for x in (offset - 1, offset + segment - 1, offset, offset - segment)])
|
||||
offset += segment
|
||||
for i in range(segment - 1):
|
||||
t = offset + i
|
||||
faces.new([verts[x] for x in (t - segment, offset, t - segment + 1)])
|
||||
faces.new([verts[x] for x in (offset - 1, offset, offset - segment)])
|
||||
|
||||
for f in bm.faces:
|
||||
f.smooth = True
|
||||
bm.normal_update()
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
return target_object
|
||||
|
||||
|
||||
class TransformConstraintOp:
|
||||
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
|
||||
|
||||
@staticmethod
|
||||
def create(constraints, name, map_type):
|
||||
c = constraints.get(name, None)
|
||||
if c and c.type != "TRANSFORM":
|
||||
constraints.remove(c)
|
||||
c = None
|
||||
if c is None:
|
||||
c = constraints.new("TRANSFORM")
|
||||
c.name = name
|
||||
c.use_motion_extrapolate = True
|
||||
c.target_space = c.owner_space = "LOCAL"
|
||||
c.map_from = c.map_to = map_type
|
||||
c.map_to_x_from = "X"
|
||||
c.map_to_y_from = "Y"
|
||||
c.map_to_z_from = "Z"
|
||||
c.influence = 1
|
||||
return c
|
||||
|
||||
@classmethod
|
||||
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:
|
||||
defaults = (i + j + k for i in ("from_", "to_") for j in ("min_", "max_") for k in "xyz")
|
||||
extension = cls.__MIN_MAX_MAP.get(map_type, "")
|
||||
ret = cls.__MIN_MAX_MAP[key] = tuple(n + extension for n in defaults if name_id in n)
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def update_min_max(cls, constraint, value, influence=1):
|
||||
c = constraint
|
||||
if not c or c.type != "TRANSFORM":
|
||||
return
|
||||
|
||||
for attr in cls.min_max_attributes(c.map_from, "from_min"):
|
||||
setattr(c, attr, -value)
|
||||
for attr in cls.min_max_attributes(c.map_from, "from_max"):
|
||||
setattr(c, attr, value)
|
||||
|
||||
if influence is None:
|
||||
return
|
||||
|
||||
for attr in cls.min_max_attributes(c.map_to, "to_min"):
|
||||
setattr(c, attr, -value * influence)
|
||||
for attr in cls.min_max_attributes(c.map_to, "to_max"):
|
||||
setattr(c, attr, value * influence)
|
||||
|
||||
|
||||
class FnObject:
|
||||
def __init__(self):
|
||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey):
|
||||
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
|
||||
mesh_object.driver_remove(fc_curve.data_path)
|
||||
|
||||
key_blocks = key.key_blocks
|
||||
|
||||
last_index = mesh_object.active_shape_key_index or 0
|
||||
if last_index >= key_blocks.find(shape_key.name):
|
||||
last_index = max(0, last_index - 1)
|
||||
|
||||
mesh_object.shape_key_remove(shape_key)
|
||||
mesh_object.active_shape_key_index = min(last_index, len(key_blocks) - 1)
|
||||
|
||||
|
||||
ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = TypeVar("ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE")
|
||||
|
||||
|
||||
class FnContext:
|
||||
def __init__(self):
|
||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context:
|
||||
return context or bpy.context
|
||||
|
||||
@staticmethod
|
||||
def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]:
|
||||
return context.active_object
|
||||
|
||||
@staticmethod
|
||||
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: 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: bpy.types.Context) -> bpy.types.SceneObjects:
|
||||
return context.scene.objects
|
||||
|
||||
@staticmethod
|
||||
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):
|
||||
lc.hide_viewport = False
|
||||
lc.collection.hide_viewport = False
|
||||
lc.collection.hide_select = False
|
||||
return True
|
||||
if obj in layer_collection.collection.objects.values():
|
||||
if layer_collection.exclude:
|
||||
layer_collection.exclude = False
|
||||
return True
|
||||
return False
|
||||
|
||||
selected_objects = set(context.selected_objects)
|
||||
__layer_check(context.view_layer.layer_collection)
|
||||
if len(context.selected_objects) != len(selected_objects):
|
||||
for i in context.selected_objects:
|
||||
if i not in selected_objects:
|
||||
i.select_set(False)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
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: 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: 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: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object:
|
||||
context.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object:
|
||||
return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
|
||||
|
||||
@staticmethod
|
||||
def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]:
|
||||
"""
|
||||
Duplicate object.
|
||||
|
||||
This function duplicates the given object and returns a list of duplicated objects.
|
||||
|
||||
Args:
|
||||
context (bpy.types.Context): The context in which the duplication is performed.
|
||||
object_to_duplicate (bpy.types.Object): The object to be duplicated.
|
||||
target_count (int): The desired count of duplicated objects.
|
||||
|
||||
Returns:
|
||||
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)
|
||||
object_to_duplicate.select_set(True)
|
||||
assert len(context.selected_objects) == 1
|
||||
assert context.selected_objects[0] == object_to_duplicate
|
||||
last_selected_objects = result_objects = [object_to_duplicate]
|
||||
while len(result_objects) < target_count:
|
||||
bpy.ops.object.duplicate()
|
||||
result_objects.extend(context.selected_objects)
|
||||
remain = target_count - len(result_objects) - len(context.selected_objects)
|
||||
if remain < 0:
|
||||
last_selected_objects = context.selected_objects
|
||||
for i in range(-remain):
|
||||
last_selected_objects[i].select_set(False)
|
||||
else:
|
||||
for i in range(min(remain, len(last_selected_objects))):
|
||||
last_selected_objects[i].select_set(True)
|
||||
last_selected_objects = context.selected_objects
|
||||
assert len(result_objects) == target_count
|
||||
return result_objects
|
||||
|
||||
@staticmethod
|
||||
def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]:
|
||||
"""
|
||||
Finds the layer collection that contains the given target_object in the user's collections.
|
||||
|
||||
Args:
|
||||
context (bpy.types.Context): The Blender context.
|
||||
target_object (bpy.types.Object): The target object to find the layer collection for.
|
||||
|
||||
Returns:
|
||||
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
|
||||
|
||||
def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]:
|
||||
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:
|
||||
return found
|
||||
|
||||
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:
|
||||
return found
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@contextlib.contextmanager
|
||||
def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]:
|
||||
"""
|
||||
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 (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:
|
||||
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)
|
||||
if target_layer_collection is not None:
|
||||
context.view_layer.active_layer_collection = target_layer_collection
|
||||
try:
|
||||
yield context
|
||||
finally:
|
||||
if context.view_layer.active_layer_collection.name != original_layer_collection.name:
|
||||
context.view_layer.active_layer_collection = original_layer_collection
|
||||
|
||||
@staticmethod
|
||||
def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]:
|
||||
addon: bpy.types.Addon = context.preferences.addons.get(__package__, None)
|
||||
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:
|
||||
keywords["active_object"] = active_object
|
||||
keywords["object"] = active_object
|
||||
|
||||
if selected_objects is not None:
|
||||
keywords["selected_objects"] = selected_objects
|
||||
keywords["selected_editable_objects"] = selected_objects
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,564 @@
|
||||
# -*- 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 TYPE_CHECKING, Iterable, Optional, Set
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
from .. import bpyutils
|
||||
from ..bpyutils import TransformConstraintOp
|
||||
from ..utils import ItemOp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..properties.root import MMDRoot, MMDDisplayItemFrame
|
||||
from ..properties.pose_bone import MMDBone
|
||||
|
||||
|
||||
def remove_constraint(constraints, name):
|
||||
c = constraints.get(name, None)
|
||||
if c:
|
||||
constraints.remove(c)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def remove_edit_bones(edit_bones, bone_names):
|
||||
for name in bone_names:
|
||||
b = edit_bones.get(name, None)
|
||||
if b:
|
||||
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 = ("親指", "人指", "中指", "薬指", "小指")
|
||||
AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー")
|
||||
|
||||
def __init__(self):
|
||||
raise NotImplementedError("This class cannot be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]:
|
||||
for bone in armature_object.pose.bones:
|
||||
if bone.mmd_bone.bone_id != bone_id:
|
||||
continue
|
||||
return bone
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def __new_bone_id(armature_object: bpy.types.Object) -> int:
|
||||
return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1
|
||||
|
||||
@staticmethod
|
||||
def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int:
|
||||
if pose_bone.mmd_bone.bone_id < 0:
|
||||
pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data)
|
||||
return pose_bone.mmd_bone.bone_id
|
||||
|
||||
@staticmethod
|
||||
def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]:
|
||||
if armature_object.mode == "EDIT":
|
||||
bpy.ops.object.mode_set(mode="OBJECT") # update selected bones
|
||||
bpy.ops.object.mode_set(mode="EDIT") # back to edit mode
|
||||
context_selected_bones = bpy.context.selected_pose_bones or bpy.context.selected_bones or []
|
||||
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
|
||||
bone_collections = armature.collections
|
||||
for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES:
|
||||
if bone_collection_name in bone_collections:
|
||||
continue
|
||||
bone_collection = bone_collections.new(bone_collection_name)
|
||||
FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False)
|
||||
return armature_object
|
||||
|
||||
@staticmethod
|
||||
def __is_mmd_tools_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||
return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection
|
||||
|
||||
@staticmethod
|
||||
def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
|
||||
|
||||
@staticmethod
|
||||
def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool):
|
||||
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL
|
||||
bone_collection.is_visible = is_visible
|
||||
|
||||
@staticmethod
|
||||
def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool:
|
||||
return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME)
|
||||
|
||||
@staticmethod
|
||||
def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection):
|
||||
bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL
|
||||
|
||||
@staticmethod
|
||||
def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone:
|
||||
edit_bone.id_data.collections[bone_collection_name].assign(edit_bone)
|
||||
edit_bone.use_deform = False
|
||||
return edit_bone
|
||||
|
||||
@staticmethod
|
||||
def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY)
|
||||
|
||||
@staticmethod
|
||||
def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||
return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW)
|
||||
|
||||
@staticmethod
|
||||
def __unassign_mmd_tools_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone:
|
||||
for bone_collection in edit_bone.collections:
|
||||
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
|
||||
continue
|
||||
bone_collection.unassign(edit_bone)
|
||||
return edit_bone
|
||||
|
||||
@staticmethod
|
||||
def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object):
|
||||
armature: bpy.types.Armature = armature_object.data
|
||||
bone_collections = armature.collections
|
||||
|
||||
from .model import FnModel
|
||||
|
||||
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
|
||||
mmd_root: MMDRoot = root_object.mmd_root
|
||||
|
||||
bones = armature.bones
|
||||
used_groups = set()
|
||||
unassigned_bone_names = {b.name for b in bones}
|
||||
|
||||
for frame in mmd_root.display_item_frames:
|
||||
for item in frame.data:
|
||||
if item.type == "BONE" and item.name in unassigned_bone_names:
|
||||
unassigned_bone_names.remove(item.name)
|
||||
group_name = frame.name
|
||||
used_groups.add(group_name)
|
||||
bone_collection = bone_collections.get(group_name)
|
||||
if bone_collection is None:
|
||||
bone_collection = bone_collections.new(name=group_name)
|
||||
FnBone.__set_bone_collection_to_normal(bone_collection)
|
||||
bone_collection.assign(bones[item.name])
|
||||
|
||||
for name in unassigned_bone_names:
|
||||
for bc in bones[name].collections:
|
||||
if not FnBone.__is_mmd_tools_bone_collection(bc):
|
||||
continue
|
||||
if not FnBone.__is_normal_bone_collection(bc):
|
||||
continue
|
||||
bc.unassign(bones[name])
|
||||
|
||||
# remove unused bone groups
|
||||
for bone_collection in bone_collections.values():
|
||||
if bone_collection.name in used_groups:
|
||||
continue
|
||||
if not FnBone.__is_mmd_tools_bone_collection(bone_collection):
|
||||
continue
|
||||
if not FnBone.__is_normal_bone_collection(bone_collection):
|
||||
continue
|
||||
bone_collections.remove(bone_collection)
|
||||
|
||||
@staticmethod
|
||||
def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object):
|
||||
armature: bpy.types.Armature = armature_object.data
|
||||
bone_collections: bpy.types.BoneCollections = armature.collections
|
||||
|
||||
from .model import FnModel
|
||||
|
||||
root_object: bpy.types.Object = FnModel.find_root_object(armature_object)
|
||||
mmd_root: MMDRoot = root_object.mmd_root
|
||||
display_item_frames = mmd_root.display_item_frames
|
||||
|
||||
used_frame_index: Set[int] = set()
|
||||
|
||||
bone_collection: bpy.types.BoneCollection
|
||||
for bone_collection in bone_collections:
|
||||
if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection):
|
||||
continue
|
||||
|
||||
bone_collection_name = bone_collection.name
|
||||
display_item_frame: Optional[MMDDisplayItemFrame] = display_item_frames.get(bone_collection_name)
|
||||
if display_item_frame is None:
|
||||
display_item_frame = display_item_frames.add()
|
||||
display_item_frame.name = bone_collection_name
|
||||
display_item_frame.name_e = bone_collection_name
|
||||
used_frame_index.add(display_item_frames.find(bone_collection_name))
|
||||
|
||||
ItemOp.resize(display_item_frame.data, len(bone_collection.bones))
|
||||
for display_item, bone in zip(display_item_frame.data, bone_collection.bones):
|
||||
display_item.type = "BONE"
|
||||
display_item.name = bone.name
|
||||
|
||||
for i in reversed(range(len(display_item_frames))):
|
||||
if i in used_frame_index:
|
||||
continue
|
||||
display_item_frame = display_item_frames[i]
|
||||
if display_item_frame.is_special:
|
||||
if display_item_frame.name != "表情":
|
||||
display_item_frame.data.clear()
|
||||
else:
|
||||
display_item_frames.remove(i)
|
||||
mmd_root.active_display_item_frame = 0
|
||||
|
||||
@staticmethod
|
||||
def apply_bone_fixed_axis(armature_object: bpy.types.Object):
|
||||
bone_map = {}
|
||||
for b in armature_object.pose.bones:
|
||||
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
|
||||
continue
|
||||
mmd_bone: MMDBone = b.mmd_bone
|
||||
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
|
||||
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
|
||||
|
||||
force_align = True
|
||||
with bpyutils.edit_object(armature_object) as data:
|
||||
bone: bpy.types.EditBone
|
||||
for bone in data.edit_bones:
|
||||
if bone.name not in bone_map:
|
||||
bone.select = False
|
||||
continue
|
||||
fixed_axis, is_tip, parent_tip = bone_map[bone.name]
|
||||
if fixed_axis.length:
|
||||
axes = [bone.x_axis, bone.y_axis, bone.z_axis]
|
||||
direction = fixed_axis.normalized().xzy
|
||||
idx, val = max([(i, direction.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
|
||||
idx_1, idx_2 = (idx + 1) % 3, (idx + 2) % 3
|
||||
axes[idx] = -direction if val < 0 else direction
|
||||
axes[idx_2] = axes[idx].cross(axes[idx_1])
|
||||
axes[idx_1] = axes[idx_2].cross(axes[idx])
|
||||
if parent_tip and bone.use_connect:
|
||||
bone.use_connect = False
|
||||
bone.head = bone.parent.head
|
||||
if force_align:
|
||||
tail = bone.head + axes[1].normalized() * bone.length
|
||||
if is_tip or (tail - bone.tail).length > 1e-4:
|
||||
for c in bone.children:
|
||||
if c.use_connect:
|
||||
c.use_connect = False
|
||||
if is_tip:
|
||||
c.head = bone.head
|
||||
bone.tail = tail
|
||||
bone.align_roll(axes[2])
|
||||
bone_map[bone.name] = tuple(i != idx for i in range(3))
|
||||
else:
|
||||
bone_map[bone.name] = (True, True, True)
|
||||
bone.select = True
|
||||
|
||||
for bone_name, locks in bone_map.items():
|
||||
b = armature_object.pose.bones[bone_name]
|
||||
b.lock_location = (True, True, True)
|
||||
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
|
||||
|
||||
@staticmethod
|
||||
def load_bone_local_axes(armature_object: bpy.types.Object, enable=True):
|
||||
for b in FnBone.__get_selected_pose_bones(armature_object):
|
||||
mmd_bone: MMDBone = b.mmd_bone
|
||||
mmd_bone.enabled_local_axes = enable
|
||||
if enable:
|
||||
axes = b.bone.matrix_local.to_3x3().transposed()
|
||||
mmd_bone.local_axis_x = axes[0].xzy
|
||||
mmd_bone.local_axis_z = axes[2].xzy
|
||||
|
||||
@staticmethod
|
||||
def apply_bone_local_axes(armature_object: bpy.types.Object):
|
||||
bone_map = {}
|
||||
for b in armature_object.pose.bones:
|
||||
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
|
||||
continue
|
||||
mmd_bone: MMDBone = b.mmd_bone
|
||||
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
|
||||
|
||||
with bpyutils.edit_object(armature_object) as data:
|
||||
bone: bpy.types.EditBone
|
||||
for bone in data.edit_bones:
|
||||
if bone.name not in bone_map:
|
||||
bone.select = False
|
||||
continue
|
||||
local_axis_x, local_axis_z = bone_map[bone.name]
|
||||
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
|
||||
bone.select = True
|
||||
|
||||
@staticmethod
|
||||
def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z):
|
||||
axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z)
|
||||
idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
|
||||
edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3])
|
||||
|
||||
@staticmethod
|
||||
def get_axes(mmd_local_axis_x, mmd_local_axis_z):
|
||||
x_axis = Vector(mmd_local_axis_x).normalized().xzy
|
||||
z_axis = Vector(mmd_local_axis_z).normalized().xzy
|
||||
y_axis = z_axis.cross(x_axis).normalized()
|
||||
z_axis = x_axis.cross(y_axis).normalized() # correction
|
||||
return (x_axis, y_axis, z_axis)
|
||||
|
||||
@staticmethod
|
||||
def apply_auto_bone_roll(armature):
|
||||
bone_names = []
|
||||
for b in armature.pose.bones:
|
||||
if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j):
|
||||
bone_names.append(b.name)
|
||||
with bpyutils.edit_object(armature) as data:
|
||||
bone: bpy.types.EditBone
|
||||
for bone in data.edit_bones:
|
||||
if bone.name not in bone_names:
|
||||
continue
|
||||
FnBone.update_auto_bone_roll(bone)
|
||||
bone.select = True
|
||||
|
||||
@staticmethod
|
||||
def update_auto_bone_roll(edit_bone):
|
||||
# make a triangle face (p1,p2,p3)
|
||||
p1 = edit_bone.head.copy()
|
||||
p2 = edit_bone.tail.copy()
|
||||
p3 = p2.copy()
|
||||
# translate p3 in xz plane
|
||||
# the normal vector of the face tracks -Y direction
|
||||
xz = Vector((p2.x - p1.x, p2.z - p1.z))
|
||||
xz.normalize()
|
||||
theta = math.atan2(xz.y, xz.x)
|
||||
norm = edit_bone.vector.length
|
||||
p3.z += norm * math.cos(theta)
|
||||
p3.x -= norm * math.sin(theta)
|
||||
# calculate the normal vector of the face
|
||||
y = (p2 - p1).normalized()
|
||||
z_tmp = (p3 - p1).normalized()
|
||||
x = y.cross(z_tmp) # normal vector
|
||||
# z = x.cross(y)
|
||||
FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy)
|
||||
|
||||
@staticmethod
|
||||
def has_auto_local_axis(name_j):
|
||||
if name_j:
|
||||
if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS:
|
||||
return True
|
||||
for finger_name in FnBone.AUTO_LOCAL_AXIS_FINGERS:
|
||||
if finger_name in name_j:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def clean_additional_transformation(armature_object: bpy.types.Object):
|
||||
# clean constraints
|
||||
p_bone: bpy.types.PoseBone
|
||||
for p_bone in armature_object.pose.bones:
|
||||
p_bone.mmd_bone.is_additional_transform_dirty = True
|
||||
constraints = p_bone.constraints
|
||||
remove_constraint(constraints, "mmd_additional_rotation")
|
||||
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",
|
||||
"SHADOW",
|
||||
"ADDITIONAL_TRANSFORM",
|
||||
"ADDITIONAL_TRANSFORM_INVERT",
|
||||
}
|
||||
|
||||
def __is_at_shadow_bone(b):
|
||||
return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types
|
||||
|
||||
shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)]
|
||||
if len(shadow_bone_names) > 0:
|
||||
with bpyutils.edit_object(armature_object) as data:
|
||||
remove_edit_bones(data.edit_bones, shadow_bone_names)
|
||||
|
||||
@staticmethod
|
||||
def apply_additional_transformation(armature_object: bpy.types.Object):
|
||||
def __is_dirty_bone(b):
|
||||
if b.is_mmd_shadow_bone:
|
||||
return False
|
||||
mmd_bone = b.mmd_bone
|
||||
if mmd_bone.has_additional_rotation or mmd_bone.has_additional_location:
|
||||
return True
|
||||
return mmd_bone.is_additional_transform_dirty
|
||||
|
||||
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
|
||||
|
||||
# setup constraints
|
||||
shadow_bone_pool = []
|
||||
for p_bone in dirty_bones:
|
||||
sb = FnBone.__setup_constraints(p_bone)
|
||||
if sb:
|
||||
shadow_bone_pool.append(sb)
|
||||
|
||||
# setup shadow bones
|
||||
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)
|
||||
|
||||
pose_bones = armature_object.pose.bones
|
||||
for sb in shadow_bone_pool:
|
||||
sb.update_pose_bones(pose_bones)
|
||||
|
||||
# finish
|
||||
for p_bone in dirty_bones:
|
||||
p_bone.mmd_bone.is_additional_transform_dirty = False
|
||||
|
||||
@staticmethod
|
||||
def __setup_constraints(p_bone):
|
||||
bone_name = p_bone.name
|
||||
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 # or p_bone.is_in_ik_chain
|
||||
mute_location = not mmd_bone.has_additional_location
|
||||
|
||||
constraints = p_bone.constraints
|
||||
if not target_bone or (mute_rotation and mute_location) or influence == 0:
|
||||
rot = remove_constraint(constraints, "mmd_additional_rotation")
|
||||
loc = remove_constraint(constraints, "mmd_additional_location")
|
||||
if rot or loc:
|
||||
return _AT_ShadowBoneRemove(bone_name)
|
||||
return None
|
||||
|
||||
shadow_bone = _AT_ShadowBoneCreate(bone_name, target_bone)
|
||||
|
||||
def __config(name, mute, map_type, value):
|
||||
if mute:
|
||||
remove_constraint(constraints, name)
|
||||
return
|
||||
c = TransformConstraintOp.create(constraints, name, map_type)
|
||||
c.target = p_bone.id_data
|
||||
shadow_bone.add_constraint(c)
|
||||
TransformConstraintOp.update_min_max(c, value, influence)
|
||||
|
||||
__config("mmd_additional_rotation", mute_rotation, "ROTATION", math.pi)
|
||||
__config("mmd_additional_location", mute_location, "LOCATION", 100)
|
||||
|
||||
return shadow_bone
|
||||
|
||||
@staticmethod
|
||||
def update_additional_transform_influence(pose_bone: bpy.types.PoseBone):
|
||||
influence = pose_bone.mmd_bone.additional_transform_influence
|
||||
constraints = pose_bone.constraints
|
||||
c = constraints.get("mmd_additional_rotation", None)
|
||||
TransformConstraintOp.update_min_max(c, math.pi, influence)
|
||||
c = constraints.get("mmd_additional_location", None)
|
||||
TransformConstraintOp.update_min_max(c, 100, influence)
|
||||
|
||||
|
||||
class MigrationFnBone:
|
||||
"""Migration Functions for old MMD models broken by bugs or issues"""
|
||||
|
||||
@staticmethod
|
||||
def fix_mmd_ik_limit_override(armature_object: bpy.types.Object):
|
||||
pose_bone: bpy.types.PoseBone
|
||||
for pose_bone in armature_object.pose.bones:
|
||||
constraint: bpy.types.Constraint
|
||||
for constraint in pose_bone.constraints:
|
||||
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
|
||||
constraint.owner_space = "LOCAL"
|
||||
|
||||
|
||||
class _AT_ShadowBoneRemove:
|
||||
def __init__(self, bone_name):
|
||||
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
|
||||
|
||||
def update_edit_bones(self, edit_bones):
|
||||
remove_edit_bones(edit_bones, self.__shadow_bone_names)
|
||||
|
||||
def update_pose_bones(self, pose_bones):
|
||||
pass
|
||||
|
||||
|
||||
class _AT_ShadowBoneCreate:
|
||||
def __init__(self, bone_name, target_bone_name):
|
||||
self.__dummy_bone_name = "_dummy_" + bone_name
|
||||
self.__shadow_bone_name = "_shadow_" + bone_name
|
||||
self.__bone_name = bone_name
|
||||
self.__target_bone_name = target_bone_name
|
||||
self.__constraint_pool = []
|
||||
|
||||
def __is_well_aligned(self, bone0, bone1):
|
||||
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
|
||||
|
||||
def __update_constraints(self, use_shadow=True):
|
||||
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
|
||||
for c in self.__constraint_pool:
|
||||
c.subtarget = subtarget
|
||||
|
||||
def add_constraint(self, constraint):
|
||||
self.__constraint_pool.append(constraint)
|
||||
|
||||
def update_edit_bones(self, edit_bones):
|
||||
bone = edit_bones[self.__bone_name]
|
||||
target_bone = edit_bones[self.__target_bone_name]
|
||||
if bone != target_bone and self.__is_well_aligned(bone, target_bone):
|
||||
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
|
||||
return
|
||||
|
||||
dummy_bone_name = self.__dummy_bone_name
|
||||
dummy = edit_bones.get(dummy_bone_name, None) or FnBone.set_edit_bone_to_dummy(edit_bones.new(name=dummy_bone_name))
|
||||
dummy.parent = target_bone
|
||||
dummy.head = target_bone.head
|
||||
dummy.tail = dummy.head + bone.tail - bone.head
|
||||
dummy.roll = bone.roll
|
||||
|
||||
shadow_bone_name = self.__shadow_bone_name
|
||||
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
|
||||
shadow.parent = target_bone.parent
|
||||
shadow.head = dummy.head
|
||||
shadow.tail = dummy.tail
|
||||
shadow.roll = bone.roll
|
||||
|
||||
def update_pose_bones(self, pose_bones):
|
||||
if self.__shadow_bone_name not in pose_bones:
|
||||
self.__update_constraints(use_shadow=False)
|
||||
return
|
||||
|
||||
dummy_p_bone = pose_bones[self.__dummy_bone_name]
|
||||
dummy_p_bone.is_mmd_shadow_bone = True
|
||||
dummy_p_bone.mmd_shadow_bone_type = "DUMMY"
|
||||
|
||||
shadow_p_bone = pose_bones[self.__shadow_bone_name]
|
||||
shadow_p_bone.is_mmd_shadow_bone = True
|
||||
shadow_p_bone.mmd_shadow_bone_type = "SHADOW"
|
||||
|
||||
if "mmd_tools_at_dummy" not in shadow_p_bone.constraints:
|
||||
c = shadow_p_bone.constraints.new("COPY_TRANSFORMS")
|
||||
c.name = "mmd_tools_at_dummy"
|
||||
c.target = dummy_p_bone.id_data
|
||||
c.subtarget = dummy_p_bone.name
|
||||
c.target_space = "POSE"
|
||||
c.owner_space = "POSE"
|
||||
|
||||
self.__update_constraints()
|
||||
@@ -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
|
||||
@@ -0,0 +1,718 @@
|
||||
# -*- 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 os
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
|
||||
|
||||
import bpy
|
||||
from mathutils import Vector
|
||||
|
||||
from ..bpyutils import FnContext
|
||||
from .exceptions import MaterialNotFoundError
|
||||
from .shader import _NodeGroupUtils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..properties.material import MMDMaterial
|
||||
|
||||
# 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
|
||||
|
||||
@staticmethod
|
||||
def set_nodes_are_readonly(nodes_are_readonly: bool):
|
||||
FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly
|
||||
|
||||
@classmethod
|
||||
def from_material_id(cls, material_id: str):
|
||||
for material in bpy.data.materials:
|
||||
if material.mmd_material.material_id == material_id:
|
||||
return cls(material)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def clean_materials(obj, can_remove: Callable[[bpy.types.Material], bool]):
|
||||
materials = obj.data.materials
|
||||
materials_pop = materials.pop
|
||||
for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True):
|
||||
m = materials_pop(index=i)
|
||||
if m.users < 1:
|
||||
bpy.data.materials.remove(m)
|
||||
|
||||
@staticmethod
|
||||
def swap_materials(mesh_object: bpy.types.Object, mat1_ref: str | int, mat2_ref: str | int, reverse=False, swap_slots=False) -> Tuple[bpy.types.Material, bpy.types.Material]:
|
||||
"""
|
||||
This method will assign the polygons of mat1 to mat2.
|
||||
If reverse is True it will also swap the polygons assigned to mat2 to mat1.
|
||||
The reference to materials can be indexes or names
|
||||
Finally it will also swap the material slots if the option is given.
|
||||
|
||||
Args:
|
||||
mesh_object (bpy.types.Object): The mesh object
|
||||
mat1_ref (str | int): The reference to the first material
|
||||
mat2_ref (str | int): The reference to the second material
|
||||
reverse (bool, optional): If true it will also swap the polygons assigned to mat2 to mat1. Defaults to False.
|
||||
swap_slots (bool, optional): If true it will also swap the material slots. Defaults to False.
|
||||
|
||||
Retruns:
|
||||
Tuple[bpy.types.Material, bpy.types.Material]: The swapped materials
|
||||
|
||||
Raises:
|
||||
MaterialNotFoundError: If one of the materials is not found
|
||||
"""
|
||||
mesh = cast(bpy.types.Mesh, mesh_object.data)
|
||||
try:
|
||||
# Try to find the materials
|
||||
mat1 = mesh.materials[mat1_ref]
|
||||
mat2 = mesh.materials[mat2_ref]
|
||||
if None in (mat1, mat2):
|
||||
raise MaterialNotFoundError()
|
||||
except (KeyError, IndexError) as exc:
|
||||
# Wrap exceptions within our custom ones
|
||||
raise MaterialNotFoundError() from exc
|
||||
mat1_idx = mesh.materials.find(mat1.name)
|
||||
mat2_idx = mesh.materials.find(mat2.name)
|
||||
# Swap polygons
|
||||
for poly in mesh.polygons:
|
||||
if poly.material_index == mat1_idx:
|
||||
poly.material_index = mat2_idx
|
||||
elif reverse and poly.material_index == mat2_idx:
|
||||
poly.material_index = mat1_idx
|
||||
# Swap slots if specified
|
||||
if swap_slots:
|
||||
mesh_object.material_slots[mat1_idx].material = mat2
|
||||
mesh_object.material_slots[mat2_idx].material = mat1
|
||||
return mat1, mat2
|
||||
|
||||
@staticmethod
|
||||
def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]):
|
||||
"""
|
||||
This method will fix the material order. Which is lost after joining meshes.
|
||||
"""
|
||||
materials = cast(bpy.types.Mesh, meshObj.data).materials
|
||||
for new_idx, mat in enumerate(material_names):
|
||||
# Get the material that is currently on this index
|
||||
other_mat = materials[new_idx]
|
||||
if other_mat.name == mat:
|
||||
continue # This is already in place
|
||||
FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True)
|
||||
|
||||
@property
|
||||
def material_id(self):
|
||||
mmd_mat: MMDMaterial = self.__material.mmd_material
|
||||
if mmd_mat.material_id < 0:
|
||||
max_id = -1
|
||||
for mat in bpy.data.materials:
|
||||
max_id = max(max_id, mat.mmd_material.material_id)
|
||||
mmd_mat.material_id = max_id + 1
|
||||
return mmd_mat.material_id
|
||||
|
||||
@property
|
||||
def material(self):
|
||||
return self.__material
|
||||
|
||||
def __same_image_file(self, image, filepath):
|
||||
if image and image.source == "FILE":
|
||||
# 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:
|
||||
pass
|
||||
return False
|
||||
|
||||
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)
|
||||
except:
|
||||
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
|
||||
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: MMDMaterial = self.__material.mmd_material
|
||||
if mmd_mat.is_shared_toon_texture:
|
||||
shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "")
|
||||
toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1))
|
||||
self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path))
|
||||
elif mmd_mat.toon_texture != "":
|
||||
self.create_toon_texture(mmd_mat.toon_texture)
|
||||
else:
|
||||
self.remove_toon_texture()
|
||||
|
||||
def _mix_diffuse_and_ambient(self, mmd_mat):
|
||||
r, g, b = mmd_mat.diffuse_color
|
||||
ar, ag, ab = mmd_mat.ambient_color
|
||||
return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)]
|
||||
|
||||
def update_drop_shadow(self):
|
||||
pass
|
||||
|
||||
def update_enabled_toon_edge(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
self.update_edge_color()
|
||||
|
||||
def update_edge_color(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.__material
|
||||
mmd_mat: MMDMaterial = mat.mmd_material
|
||||
color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
|
||||
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
|
||||
if hasattr(mat, "line_color"): # freestyle line color
|
||||
mat.line_color = line_color
|
||||
|
||||
mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None)
|
||||
if mat_edge:
|
||||
mat_edge.mmd_material.edge_color = line_color
|
||||
|
||||
if mat.name.startswith("mmd_edge.") and mat.node_tree:
|
||||
mmd_mat.ambient_color, mmd_mat.alpha = color, alpha
|
||||
node_shader = mat.node_tree.nodes.get("mmd_edge_preview", None)
|
||||
if node_shader and "Color" in node_shader.inputs:
|
||||
node_shader.inputs["Color"].default_value = mmd_mat.edge_color
|
||||
if node_shader and "Alpha" in node_shader.inputs:
|
||||
node_shader.inputs["Alpha"].default_value = alpha
|
||||
|
||||
def update_edge_weight(self):
|
||||
pass
|
||||
|
||||
def get_texture(self):
|
||||
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 _DummyTextureSlot(texture.image)
|
||||
|
||||
def remove_texture(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
self.__remove_texture_node("mmd_base_tex")
|
||||
|
||||
def get_sphere_texture(self):
|
||||
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:
|
||||
return
|
||||
if use_sphere:
|
||||
self.update_sphere_texture_type(obj)
|
||||
else:
|
||||
self.__update_shader_input("Sphere Tex Fac", 0)
|
||||
|
||||
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 _DummyTextureSlot(texture.image)
|
||||
|
||||
def update_sphere_texture_type(self, obj=None):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
sphere_texture_type = int(self.material.mmd_material.sphere_texture_type)
|
||||
is_sph_add = sphere_texture_type == 2
|
||||
|
||||
if sphere_texture_type not in (1, 2, 3):
|
||||
self.__update_shader_input("Sphere Tex Fac", 0)
|
||||
else:
|
||||
self.__update_shader_input("Sphere Tex Fac", 1)
|
||||
self.__update_shader_input("Sphere Mul/Add", is_sph_add)
|
||||
self.__update_shader_input("Sphere Tex", (0, 0, 0, 1) if is_sph_add else (1, 1, 1, 1))
|
||||
|
||||
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"):
|
||||
if hasattr(texture, "color_space"):
|
||||
texture.color_space = "NONE" if is_sph_add else "COLOR"
|
||||
elif hasattr(texture.image, "colorspace_settings"):
|
||||
texture.image.colorspace_settings.name = "Linear Rec.709" if is_sph_add else "sRGB"
|
||||
|
||||
mat = self.material
|
||||
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
||||
if sphere_texture_type == 3:
|
||||
if obj and obj.type == "MESH" and mat in tuple(obj.data.materials):
|
||||
uv_layers = (l for l in obj.data.uv_layers if not l.name.startswith("_"))
|
||||
next(uv_layers, None) # skip base UV
|
||||
subtex_uv = getattr(next(uv_layers, None), "name", "")
|
||||
if subtex_uv != "UV1":
|
||||
logging.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv)
|
||||
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
|
||||
else:
|
||||
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
|
||||
|
||||
def remove_sphere_texture(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
self.__remove_texture_node("mmd_sphere_tex")
|
||||
|
||||
def get_toon_texture(self):
|
||||
return self.__get_texture_node("mmd_toon_tex", use_dummy=True)
|
||||
|
||||
def use_toon_texture(self, use_toon):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
self.__update_shader_input("Toon Tex Fac", use_toon)
|
||||
|
||||
def create_toon_texture(self, filepath):
|
||||
texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5))
|
||||
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, use_dummy=False):
|
||||
mat = self.material
|
||||
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
||||
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
||||
return _DummyTexture(texture.image) if use_dummy else texture
|
||||
return None
|
||||
|
||||
def __remove_texture_node(self, node_name):
|
||||
mat = self.material
|
||||
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
||||
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
||||
mat.node_tree.nodes.remove(texture)
|
||||
mat.update_tag()
|
||||
|
||||
def __create_texture_node(self, node_name, filepath, pos):
|
||||
texture = self.__get_texture_node(node_name)
|
||||
if texture is None:
|
||||
from mathutils import Vector
|
||||
|
||||
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))
|
||||
texture.image = self._load_image(filepath)
|
||||
self.__update_shader_nodes()
|
||||
return texture
|
||||
|
||||
def update_ambient_color(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
||||
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
|
||||
|
||||
def update_diffuse_color(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
||||
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
|
||||
|
||||
def update_alpha(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
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()
|
||||
|
||||
def update_specular_color(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
mat.specular_color = mmd_mat.specular_color
|
||||
self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,))
|
||||
|
||||
def update_shininess(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
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):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
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):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False
|
||||
if hasattr(mat, "shadow_method"):
|
||||
mat.shadow_method = "HASHED" if cast_shadows else "NONE"
|
||||
|
||||
def update_self_shadow(self):
|
||||
if self._nodes_are_readonly:
|
||||
return
|
||||
mat = self.material
|
||||
mmd_mat = mat.mmd_material
|
||||
self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow)
|
||||
|
||||
@staticmethod
|
||||
def convert_to_mmd_material(material, context=bpy.context):
|
||||
m, mmd_material = material, material.mmd_material
|
||||
|
||||
if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None:
|
||||
|
||||
def search_tex_image_node(node: bpy.types.ShaderNode):
|
||||
if node.type == "TEX_IMAGE":
|
||||
return node
|
||||
for node_input in node.inputs:
|
||||
if not node_input.is_linked:
|
||||
continue
|
||||
child = search_tex_image_node(node_input.links[0].from_node)
|
||||
if child is not None:
|
||||
return child
|
||||
return None
|
||||
|
||||
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"]:
|
||||
output_node = m.node_tree.get_output_node(target)
|
||||
if output_node is None:
|
||||
continue
|
||||
|
||||
if not output_node.inputs[0].is_linked:
|
||||
continue
|
||||
|
||||
tex_node = search_tex_image_node(output_node.inputs[0].links[0].from_node)
|
||||
break
|
||||
|
||||
if tex_node is None:
|
||||
tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None)
|
||||
if tex_node:
|
||||
tex_node.name = "mmd_base_tex"
|
||||
else:
|
||||
# Take the Base Color from BSDF if there's no texture
|
||||
bsdf_node = next((n for n in m.node_tree.nodes if n.type.startswith('BSDF_')), None)
|
||||
if bsdf_node:
|
||||
base_color_input = bsdf_node.inputs.get('Base Color') or bsdf_node.inputs.get('Color')
|
||||
if base_color_input:
|
||||
mmd_material.diffuse_color = base_color_input.default_value[:3]
|
||||
# ambient should be half the diffuse
|
||||
mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color]
|
||||
|
||||
shadow_method = getattr(m, "shadow_method", None)
|
||||
|
||||
if mmd_material.diffuse_color is None:
|
||||
mmd_material.diffuse_color = 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
|
||||
if hasattr(m, "specular_hardness"):
|
||||
mmd_material.shininess = m.specular_hardness
|
||||
else:
|
||||
mmd_material.shininess = pow(1 / max(m.roughness, 0.099), 1 / 0.37)
|
||||
|
||||
if hasattr(m, "game_settings"):
|
||||
mmd_material.is_double_sided = not m.game_settings.use_backface_culling
|
||||
elif hasattr(m, "use_backface_culling"):
|
||||
mmd_material.is_double_sided = not m.use_backface_culling
|
||||
|
||||
if shadow_method:
|
||||
mmd_material.enabled_self_shadow_map = (shadow_method != "NONE") and mmd_material.alpha > 1e-3
|
||||
mmd_material.enabled_self_shadow = shadow_method != "NONE"
|
||||
|
||||
# delete bsdf node if it's there
|
||||
if m.use_nodes:
|
||||
nodes_to_remove = [n for n in m.node_tree.nodes if n.type == 'BSDF_PRINCIPLED' or n.type.startswith('BSDF_')]
|
||||
for n in nodes_to_remove:
|
||||
m.node_tree.nodes.remove(n)
|
||||
|
||||
def __update_shader_input(self, name, val):
|
||||
mat = self.material
|
||||
if mat.name.startswith("mmd_"): # skip mmd_edge.*
|
||||
return
|
||||
self.__update_shader_nodes()
|
||||
shader = mat.node_tree.nodes.get("mmd_shader", None)
|
||||
if shader and name in shader.inputs:
|
||||
interface_socket = shader.node_tree.interface.items_tree[name]
|
||||
if hasattr(interface_socket, "min_value"):
|
||||
val = min(max(val, interface_socket.min_value), interface_socket.max_value)
|
||||
shader.inputs[name].default_value = val
|
||||
|
||||
def __update_shader_nodes(self):
|
||||
mat = self.material
|
||||
if mat.node_tree is None:
|
||||
mat.use_nodes = True
|
||||
mat.node_tree.nodes.clear()
|
||||
|
||||
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
||||
|
||||
class _Dummy:
|
||||
default_value, is_linked = None, True
|
||||
|
||||
node_shader = nodes.get("mmd_shader", None)
|
||||
if node_shader is None:
|
||||
node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup")
|
||||
node_shader.name = "mmd_shader"
|
||||
node_shader.location = (0, 1500)
|
||||
node_shader.width = 200
|
||||
node_shader.node_tree = self.__get_shader()
|
||||
|
||||
mmd_mat: MMDMaterial = mat.mmd_material
|
||||
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,)
|
||||
node_shader.inputs.get("Reflect", _Dummy).default_value = mmd_mat.shininess
|
||||
node_shader.inputs.get("Alpha", _Dummy).default_value = mmd_mat.alpha
|
||||
node_shader.inputs.get("Double Sided", _Dummy).default_value = mmd_mat.is_double_sided
|
||||
node_shader.inputs.get("Self Shadow", _Dummy).default_value = mmd_mat.enabled_self_shadow
|
||||
self.update_sphere_texture_type()
|
||||
|
||||
node_uv = nodes.get("mmd_tex_uv", None)
|
||||
if node_uv is None:
|
||||
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()
|
||||
|
||||
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: 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"])
|
||||
|
||||
for name_id in ("Base", "Toon", "Sphere"):
|
||||
texture = self.__get_texture_node("mmd_%s_tex" % name_id.lower())
|
||||
if texture:
|
||||
name_tex_in, name_alpha_in, name_uv_out = (name_id + x for x in (" Tex", " Alpha", " UV"))
|
||||
if not node_shader.inputs.get(name_tex_in, _Dummy).is_linked:
|
||||
links.new(texture.outputs["Color"], node_shader.inputs[name_tex_in])
|
||||
if not node_shader.inputs.get(name_alpha_in, _Dummy).is_linked:
|
||||
links.new(texture.outputs["Alpha"], node_shader.inputs[name_alpha_in])
|
||||
if not texture.inputs["Vector"].is_linked:
|
||||
links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"])
|
||||
|
||||
def __get_shader_uv(self):
|
||||
group_name = "MMDTexUV"
|
||||
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||
if len(shader.nodes):
|
||||
return shader
|
||||
|
||||
ng = _NodeGroupUtils(shader)
|
||||
|
||||
############################################################################
|
||||
_node_output: bpy.types.NodeGroupOutput = ng.new_node("NodeGroupOutput", (6, 0))
|
||||
|
||||
tex_coord: bpy.types.ShaderNodeTexCoord = ng.new_node("ShaderNodeTexCoord", (0, 0))
|
||||
|
||||
tex_coord1: bpy.types.ShaderNodeUVMap = ng.new_node("ShaderNodeUVMap", (4, -2))
|
||||
tex_coord1.uv_map = "UV1"
|
||||
|
||||
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: 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)
|
||||
|
||||
links = ng.links
|
||||
links.new(tex_coord.outputs["Normal"], vec_trans.inputs["Vector"])
|
||||
links.new(vec_trans.outputs["Vector"], node_vector.inputs["Vector"])
|
||||
|
||||
ng.new_output_socket("Base UV", tex_coord.outputs["UV"])
|
||||
ng.new_output_socket("Toon UV", node_vector.outputs["Vector"])
|
||||
ng.new_output_socket("Sphere UV", node_vector.outputs["Vector"])
|
||||
ng.new_output_socket("SubTex UV", tex_coord1.outputs["UV"])
|
||||
|
||||
return shader
|
||||
|
||||
def __get_shader(self):
|
||||
group_name = "MMDShaderDev"
|
||||
shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||
if len(shader.nodes):
|
||||
return shader
|
||||
|
||||
ng = _NodeGroupUtils(shader)
|
||||
|
||||
############################################################################
|
||||
node_input: bpy.types.NodeGroupInput = ng.new_node("NodeGroupInput", (-5, -1))
|
||||
_node_output: bpy.types.NodeGroupOutput = ng.new_node("NodeGroupOutput", (11, 1))
|
||||
|
||||
node_diffuse: bpy.types.ShaderNodeMath = ng.new_mix_node("ADD", (-3, 4), fac=0.6)
|
||||
node_diffuse.use_clamp = True
|
||||
|
||||
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: 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: 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: bpy.types.ShaderNodeMath = ng.new_math_node("DIVIDE", (7, -1.5), value1=1)
|
||||
node_reflect.use_clamp = True
|
||||
|
||||
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: 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"])
|
||||
links.new(shader_diffuse.outputs["BSDF"], shader_base_mix.inputs[1])
|
||||
links.new(shader_glossy.outputs["BSDF"], shader_base_mix.inputs[2])
|
||||
|
||||
links.new(node_diffuse.outputs["Color"], node_tex.inputs["Color1"])
|
||||
links.new(node_tex.outputs["Color"], node_toon.inputs["Color1"])
|
||||
links.new(node_toon.outputs["Color"], node_sph.inputs["Color1"])
|
||||
links.new(node_toon.outputs["Color"], node_spa.inputs["Color1"])
|
||||
links.new(node_sph.outputs["Color"], node_sphere.inputs["Color1"])
|
||||
links.new(node_spa.outputs["Color"], node_sphere.inputs["Color2"])
|
||||
links.new(node_sphere.outputs["Color"], shader_diffuse.inputs["Color"])
|
||||
|
||||
links.new(node_geo.outputs["Backfacing"], node_invert.inputs[0])
|
||||
links.new(node_invert.outputs["Value"], node_cull.inputs[0])
|
||||
links.new(node_cull.outputs["Value"], node_alpha.inputs[0])
|
||||
links.new(node_alpha_tex.outputs["Value"], node_alpha_toon.inputs[0])
|
||||
links.new(node_alpha_toon.outputs["Value"], node_alpha_sph.inputs[0])
|
||||
links.new(node_alpha_sph.outputs["Value"], node_alpha.inputs[1])
|
||||
|
||||
links.new(node_alpha.outputs["Value"], shader_alpha_mix.inputs["Fac"])
|
||||
links.new(shader_trans.outputs["BSDF"], shader_alpha_mix.inputs[1])
|
||||
links.new(shader_base_mix.outputs["Shader"], shader_alpha_mix.inputs[2])
|
||||
|
||||
############################################################################
|
||||
ng.new_input_socket("Ambient Color", node_diffuse.inputs["Color1"], (0.4, 0.4, 0.4, 1))
|
||||
ng.new_input_socket("Diffuse Color", node_diffuse.inputs["Color2"], (0.8, 0.8, 0.8, 1))
|
||||
# ↓ specular should be disabled by default
|
||||
ng.new_input_socket("Specular Color", shader_glossy.inputs["Color"], (0.0, 0.0, 0.0, 1))
|
||||
ng.new_input_socket("Reflect", node_reflect.inputs[1], 50, min_max=(1, 512))
|
||||
ng.new_input_socket("Base Tex Fac", node_tex.inputs["Fac"], 1)
|
||||
ng.new_input_socket("Base Tex", node_tex.inputs["Color2"], (1, 1, 1, 1))
|
||||
ng.new_input_socket("Toon Tex Fac", node_toon.inputs["Fac"], 1)
|
||||
ng.new_input_socket("Toon Tex", node_toon.inputs["Color2"], (1, 1, 1, 1))
|
||||
ng.new_input_socket("Sphere Tex Fac", node_sph.inputs["Fac"], 1)
|
||||
ng.new_input_socket("Sphere Tex", node_sph.inputs["Color2"], (1, 1, 1, 1))
|
||||
ng.new_input_socket("Sphere Mul/Add", node_sphere.inputs["Fac"], 0)
|
||||
ng.new_input_socket("Double Sided", node_cull.inputs[1], 0, min_max=(0, 1))
|
||||
ng.new_input_socket("Alpha", node_alpha_tex.inputs[0], 1, min_max=(0, 1))
|
||||
ng.new_input_socket("Base Alpha", node_alpha_tex.inputs[1], 1, min_max=(0, 1))
|
||||
ng.new_input_socket("Toon Alpha", node_alpha_toon.inputs[1], 1, min_max=(0, 1))
|
||||
ng.new_input_socket("Sphere Alpha", node_alpha_sph.inputs[1], 1, min_max=(0, 1))
|
||||
|
||||
links.new(node_input.outputs["Sphere Tex Fac"], node_spa.inputs["Fac"])
|
||||
links.new(node_input.outputs["Sphere Tex"], node_spa.inputs["Color2"])
|
||||
|
||||
ng.new_output_socket("Shader", shader_alpha_mix.outputs["Shader"])
|
||||
ng.new_output_socket("Color", node_sphere.outputs["Color"])
|
||||
ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
|
||||
|
||||
return shader
|
||||
|
||||
|
||||
class MigrationFnMaterial:
|
||||
@staticmethod
|
||||
def update_mmd_shader():
|
||||
mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev")
|
||||
if mmd_shader_node_tree is None:
|
||||
return
|
||||
|
||||
ng = _NodeGroupUtils(mmd_shader_node_tree)
|
||||
if "Color" in ng.node_output.inputs:
|
||||
return
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -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"""
|
||||
@@ -0,0 +1,224 @@
|
||||
# -*- 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 cast
|
||||
import bpy
|
||||
|
||||
from ..core.bone import FnBone
|
||||
from . import patch_library_overridable
|
||||
|
||||
|
||||
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: "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: "MMDBone"):
|
||||
arm = prop.id_data
|
||||
bone_id = prop.get("additional_transform_bone_id", -1)
|
||||
if bone_id < 0:
|
||||
return ""
|
||||
pose_bone = FnBone.find_pose_bone_by_bone_id(arm, bone_id)
|
||||
if pose_bone is None:
|
||||
return ""
|
||||
return pose_bone.name
|
||||
|
||||
|
||||
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():
|
||||
prop["additional_transform_bone_id"] = -1
|
||||
return
|
||||
pose_bone = arm.pose.bones[value]
|
||||
prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
|
||||
|
||||
|
||||
class MMDBone(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="",
|
||||
)
|
||||
|
||||
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: bpy.props.IntProperty(
|
||||
name="Transform Order",
|
||||
description="Deformation tier",
|
||||
min=0,
|
||||
max=100,
|
||||
soft_max=7,
|
||||
)
|
||||
|
||||
is_controllable: bpy.props.BoolProperty(
|
||||
name="Controllable",
|
||||
description="Is controllable",
|
||||
default=True,
|
||||
)
|
||||
|
||||
transform_after_dynamics: bpy.props.BoolProperty(
|
||||
name="After Dynamics",
|
||||
description="After physics",
|
||||
default=False,
|
||||
)
|
||||
|
||||
enabled_fixed_axis: bpy.props.BoolProperty(
|
||||
name="Fixed Axis",
|
||||
description="Use fixed axis",
|
||||
default=False,
|
||||
)
|
||||
|
||||
fixed_axis: bpy.props.FloatVectorProperty(
|
||||
name="Fixed Axis",
|
||||
description="Fixed axis",
|
||||
subtype="XYZ",
|
||||
size=3,
|
||||
precision=3,
|
||||
step=0.1, # 0.1 / 100
|
||||
default=[0, 0, 0],
|
||||
)
|
||||
|
||||
enabled_local_axes: bpy.props.BoolProperty(
|
||||
name="Local Axes",
|
||||
description="Use local axes",
|
||||
default=False,
|
||||
)
|
||||
|
||||
local_axis_x: bpy.props.FloatVectorProperty(
|
||||
name="Local X-Axis",
|
||||
description="Local x-axis",
|
||||
subtype="XYZ",
|
||||
size=3,
|
||||
precision=3,
|
||||
step=0.1,
|
||||
default=[1, 0, 0],
|
||||
)
|
||||
|
||||
local_axis_z: bpy.props.FloatVectorProperty(
|
||||
name="Local Z-Axis",
|
||||
description="Local z-axis",
|
||||
subtype="XYZ",
|
||||
size=3,
|
||||
precision=3,
|
||||
step=0.1,
|
||||
default=[0, 0, 1],
|
||||
)
|
||||
|
||||
is_tip: bpy.props.BoolProperty(
|
||||
name="Tip Bone",
|
||||
description="Is zero length bone",
|
||||
default=False,
|
||||
)
|
||||
|
||||
ik_rotation_constraint: bpy.props.FloatProperty(
|
||||
name="IK Rotation Constraint",
|
||||
description="The unit angle of IK",
|
||||
subtype="ANGLE",
|
||||
soft_min=0,
|
||||
soft_max=4,
|
||||
default=1,
|
||||
)
|
||||
|
||||
has_additional_rotation: bpy.props.BoolProperty(
|
||||
name="Additional Rotation",
|
||||
description="Additional rotation",
|
||||
default=False,
|
||||
update=_mmd_bone_update_additional_transform,
|
||||
)
|
||||
|
||||
has_additional_location: bpy.props.BoolProperty(
|
||||
name="Additional Location",
|
||||
description="Additional location",
|
||||
default=False,
|
||||
update=_mmd_bone_update_additional_transform,
|
||||
)
|
||||
|
||||
additional_transform_bone: bpy.props.StringProperty(
|
||||
name="Additional Transform Bone",
|
||||
description="Additional transform bone",
|
||||
set=_mmd_bone_set_additional_transform_bone,
|
||||
get=_mmd_bone_get_additional_transform_bone,
|
||||
update=_mmd_bone_update_additional_transform,
|
||||
)
|
||||
|
||||
additional_transform_bone_id: bpy.props.IntProperty(
|
||||
name="Additional Transform Bone ID",
|
||||
default=-1,
|
||||
update=_mmd_bone_update_additional_transform,
|
||||
)
|
||||
|
||||
additional_transform_influence: bpy.props.FloatProperty(
|
||||
name="Additional Transform Influence",
|
||||
description="Additional transform influence",
|
||||
default=1,
|
||||
soft_min=-1,
|
||||
soft_max=1,
|
||||
update=_mmd_bone_update_additional_transform_influence,
|
||||
)
|
||||
|
||||
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True)
|
||||
|
||||
def is_id_unique(self):
|
||||
return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None)
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone))
|
||||
bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False))
|
||||
bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type"))
|
||||
bpy.types.PoseBone.mmd_ik_toggle = patch_library_overridable(
|
||||
bpy.props.BoolProperty(
|
||||
name="MMD IK Toggle",
|
||||
description="MMD IK toggle is used to import/export animation of IK on-off",
|
||||
update=_pose_bone_update_mmd_ik_toggle,
|
||||
default=True,
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def unregister():
|
||||
del bpy.types.PoseBone.mmd_ik_toggle
|
||||
del bpy.types.PoseBone.mmd_shadow_bone_type
|
||||
del bpy.types.PoseBone.is_mmd_shadow_bone
|
||||
del bpy.types.PoseBone.mmd_bone
|
||||
|
||||
|
||||
def _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
|
||||
@@ -0,0 +1,577 @@
|
||||
# -*- 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 MMD model root object"""
|
||||
|
||||
import bpy
|
||||
|
||||
from .. import utils
|
||||
from ..bpyutils import FnContext
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.model import FnModel
|
||||
from ..core.sdef import FnSDEF
|
||||
from . import patch_library_overridable
|
||||
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
|
||||
from .translations import MMDTranslation
|
||||
|
||||
|
||||
def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
|
||||
d = constraint.driver_add(path, index)
|
||||
variables = d.driver.variables
|
||||
for x in variables:
|
||||
variables.remove(x)
|
||||
return d.driver, variables
|
||||
|
||||
|
||||
def __add_single_prop(variables, id_obj, data_path, prefix):
|
||||
var = variables.new()
|
||||
var.name = prefix + str(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
|
||||
|
||||
|
||||
def _toggleUsePropertyDriver(self: "MMDRoot", _context):
|
||||
root_object: bpy.types.Object = self.id_data
|
||||
armature_object = FnModel.find_armature_object(root_object)
|
||||
|
||||
if armature_object is None:
|
||||
ik_map = {}
|
||||
else:
|
||||
bones = armature_object.pose.bones
|
||||
ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}
|
||||
|
||||
if self.use_property_driver:
|
||||
for ik, (b, c) in ik_map.items():
|
||||
driver, variables = __driver_variables(c, "influence")
|
||||
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
|
||||
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:
|
||||
driver, variables = __driver_variables(c, "influence")
|
||||
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
|
||||
for i in FnModel.iterate_mesh_objects(root_object):
|
||||
for prop_hide in ("hide_viewport", "hide_render"):
|
||||
driver, variables = __driver_variables(i, prop_hide)
|
||||
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
|
||||
else:
|
||||
for ik, (b, c) in ik_map.items():
|
||||
c.driver_remove("influence")
|
||||
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.driver_remove("influence")
|
||||
for i in FnModel.iterate_mesh_objects(root_object):
|
||||
for prop_hide in ("hide_viewport", "hide_render"):
|
||||
i.driver_remove(prop_hide)
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Callback functions
|
||||
# ===========================================
|
||||
|
||||
|
||||
def _toggleUseToonTexture(self: "MMDRoot", _context):
|
||||
use_toon = self.use_toon_texture
|
||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||
for m in i.data.materials:
|
||||
if m:
|
||||
FnMaterial(m).use_toon_texture(use_toon)
|
||||
|
||||
|
||||
def _toggleUseSphereTexture(self: "MMDRoot", _context):
|
||||
use_sphere = self.use_sphere_texture
|
||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||
for m in i.data.materials:
|
||||
if m:
|
||||
FnMaterial(m).use_sphere_texture(use_sphere, i)
|
||||
|
||||
|
||||
def _toggleUseSDEF(self: "MMDRoot", _context):
|
||||
mute_sdef = not self.use_sdef
|
||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||
FnSDEF.mute_sdef_set(i, mute_sdef)
|
||||
|
||||
|
||||
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
|
||||
root = self.id_data
|
||||
hide = not self.show_meshes
|
||||
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||
i.hide_set(hide)
|
||||
i.hide_render = hide
|
||||
if hide and context.active_object is None:
|
||||
FnContext.set_active_object(context, root)
|
||||
|
||||
|
||||
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context):
|
||||
root = self.id_data
|
||||
hide = not self.show_rigid_bodies
|
||||
for i in FnModel.iterate_rigid_body_objects(root):
|
||||
i.hide_set(hide)
|
||||
if hide and context.active_object is None:
|
||||
FnContext.set_active_object(context, root)
|
||||
|
||||
|
||||
def _toggleVisibilityOfJoints(self: "MMDRoot", context):
|
||||
root_object = self.id_data
|
||||
hide = not self.show_joints
|
||||
for i in FnModel.iterate_joint_objects(root_object):
|
||||
i.hide_set(hide)
|
||||
if hide and context.active_object is None:
|
||||
FnContext.set_active_object(context, root_object)
|
||||
|
||||
|
||||
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context):
|
||||
root_object: bpy.types.Object = self.id_data
|
||||
hide = not self.show_temporary_objects
|
||||
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||
for i in FnModel.iterate_temporary_objects(root_object):
|
||||
i.hide_set(hide)
|
||||
if hide and context.active_object is None:
|
||||
FnContext.set_active_object(context, root_object)
|
||||
|
||||
|
||||
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context):
|
||||
root = self.id_data
|
||||
show_names = root.mmd_root.show_names_of_rigid_bodies
|
||||
for i in FnModel.iterate_rigid_body_objects(root):
|
||||
i.show_name = show_names
|
||||
|
||||
|
||||
def _toggleShowNamesOfJoints(self: "MMDRoot", _context):
|
||||
root = self.id_data
|
||||
show_names = root.mmd_root.show_names_of_joints
|
||||
for i in FnModel.iterate_joint_objects(root):
|
||||
i.show_name = show_names
|
||||
|
||||
|
||||
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool):
|
||||
root = prop.id_data
|
||||
arm = FnModel.find_armature_object(root)
|
||||
if arm is None:
|
||||
return
|
||||
if not v and bpy.context.active_object == arm:
|
||||
FnContext.set_active_object(bpy.context, root)
|
||||
arm.hide_set(not v)
|
||||
|
||||
|
||||
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"):
|
||||
if prop.id_data.mmd_type != "ROOT":
|
||||
return False
|
||||
arm = FnModel.find_armature_object(prop.id_data)
|
||||
return arm and not arm.hide_get()
|
||||
|
||||
|
||||
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int):
|
||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||
if FnModel.is_rigid_body_object(obj):
|
||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||
prop["active_rigidbody_object_index"] = v
|
||||
|
||||
|
||||
def _getActiveRigidbodyObject(prop: "MMDRoot"):
|
||||
context = bpy.context
|
||||
active_obj = FnContext.get_active_object(context)
|
||||
if FnModel.is_rigid_body_object(active_obj):
|
||||
prop["active_rigidbody_object_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
|
||||
return prop.get("active_rigidbody_object_index", 0)
|
||||
|
||||
|
||||
def _setActiveJointObject(prop: "MMDRoot", v: int):
|
||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||
if FnModel.is_joint_object(obj):
|
||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||
prop["active_joint_object_index"] = v
|
||||
|
||||
|
||||
def _getActiveJointObject(prop: "MMDRoot"):
|
||||
context = bpy.context
|
||||
active_obj = FnContext.get_active_object(context)
|
||||
if FnModel.is_joint_object(active_obj):
|
||||
prop["active_joint_object_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
|
||||
return prop.get("active_joint_object_index", 0)
|
||||
|
||||
|
||||
def _setActiveMorph(prop: "MMDRoot", v: bool):
|
||||
if "active_morph_indices" not in prop:
|
||||
prop["active_morph_indices"] = [0] * 5
|
||||
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v
|
||||
|
||||
|
||||
def _getActiveMorph(prop: "MMDRoot"):
|
||||
if "active_morph_indices" in prop:
|
||||
return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
|
||||
return 0
|
||||
|
||||
|
||||
def _setActiveMeshObject(prop: "MMDRoot", v: int):
|
||||
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||
if FnModel.is_mesh_object(obj):
|
||||
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||
prop["active_mesh_index"] = v
|
||||
|
||||
|
||||
def _getActiveMeshObject(prop: "MMDRoot"):
|
||||
context = bpy.context
|
||||
active_obj = FnContext.get_active_object(context)
|
||||
if FnModel.is_mesh_object(active_obj):
|
||||
prop["active_mesh_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
|
||||
return prop.get("active_mesh_index", -1)
|
||||
|
||||
|
||||
# ===========================================
|
||||
# Property classes
|
||||
# ===========================================
|
||||
|
||||
|
||||
class MMDDisplayItem(bpy.types.PropertyGroup):
|
||||
"""PMX 表示項目(表示枠内の1項目)"""
|
||||
|
||||
type: bpy.props.EnumProperty(
|
||||
name="Type",
|
||||
description="Select item type",
|
||||
items=[
|
||||
("BONE", "Bone", "", 1),
|
||||
("MORPH", "Morph", "", 2),
|
||||
],
|
||||
)
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
class MMDDisplayItemFrame(bpy.types.PropertyGroup):
|
||||
"""PMX 表示枠
|
||||
|
||||
PMXファイル内では表示枠がリストで格納されています。
|
||||
"""
|
||||
|
||||
name_e: bpy.props.StringProperty(
|
||||
name="Name(Eng)",
|
||||
description="English Name",
|
||||
default="",
|
||||
)
|
||||
|
||||
# 特殊枠フラグ
|
||||
# 特殊枠はファイル仕様上の固定枠(削除、リネーム不可)
|
||||
is_special: bpy.props.BoolProperty(
|
||||
name="Special",
|
||||
description="Is special",
|
||||
default=False,
|
||||
)
|
||||
|
||||
# 表示項目のリスト
|
||||
data: bpy.props.CollectionProperty(
|
||||
name="Display Items",
|
||||
type=MMDDisplayItem,
|
||||
)
|
||||
|
||||
# 現在アクティブな項目のインデックス
|
||||
active_item: bpy.props.IntProperty(
|
||||
name="Active Display Item",
|
||||
min=0,
|
||||
default=0,
|
||||
)
|
||||
|
||||
|
||||
class MMDRoot(bpy.types.PropertyGroup):
|
||||
"""MMDモデルデータ
|
||||
|
||||
モデルルート用に作成されたEmtpyオブジェクトで使用します
|
||||
"""
|
||||
|
||||
name: bpy.props.StringProperty(
|
||||
name="Name",
|
||||
description="The name of the MMD model",
|
||||
default="",
|
||||
)
|
||||
|
||||
name_e: bpy.props.StringProperty(
|
||||
name="Name (English)",
|
||||
description="The english name of the MMD model",
|
||||
default="",
|
||||
)
|
||||
|
||||
comment_text: bpy.props.StringProperty(
|
||||
name="Comment",
|
||||
description="The text datablock of the comment",
|
||||
default="",
|
||||
)
|
||||
|
||||
comment_e_text: bpy.props.StringProperty(
|
||||
name="Comment (English)",
|
||||
description="The text datablock of the english comment",
|
||||
default="",
|
||||
)
|
||||
|
||||
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,
|
||||
default=1,
|
||||
)
|
||||
|
||||
# TODO: Replace to driver for NLA
|
||||
show_meshes: bpy.props.BoolProperty(
|
||||
name="Show Meshes",
|
||||
description="Show all meshes of the MMD model",
|
||||
# get=_show_meshes_get,
|
||||
# set=_show_meshes_set,
|
||||
update=_toggleVisibilityOfMeshes,
|
||||
default=True,
|
||||
)
|
||||
|
||||
show_rigid_bodies: bpy.props.BoolProperty(
|
||||
name="Show Rigid Bodies",
|
||||
description="Show all rigid bodies of the MMD model",
|
||||
update=_toggleVisibilityOfRigidBodies,
|
||||
)
|
||||
|
||||
show_joints: bpy.props.BoolProperty(
|
||||
name="Show Joints",
|
||||
description="Show all joints of the MMD model",
|
||||
update=_toggleVisibilityOfJoints,
|
||||
)
|
||||
|
||||
show_temporary_objects: bpy.props.BoolProperty(
|
||||
name="Show Temps",
|
||||
description="Show all temporary objects of the MMD model",
|
||||
update=_toggleVisibilityOfTemporaryObjects,
|
||||
)
|
||||
|
||||
show_armature: bpy.props.BoolProperty(
|
||||
name="Show Armature",
|
||||
description="Show the armature object of the MMD model",
|
||||
get=_getVisibilityOfMMDRigArmature,
|
||||
set=_setVisibilityOfMMDRigArmature,
|
||||
)
|
||||
|
||||
show_names_of_rigid_bodies: bpy.props.BoolProperty(
|
||||
name="Show Rigid Body Names",
|
||||
description="Show rigid body names",
|
||||
update=_toggleShowNamesOfRigidBodies,
|
||||
)
|
||||
|
||||
show_names_of_joints: bpy.props.BoolProperty(
|
||||
name="Show Joint Names",
|
||||
description="Show joint names",
|
||||
update=_toggleShowNamesOfJoints,
|
||||
)
|
||||
|
||||
use_toon_texture: bpy.props.BoolProperty(
|
||||
name="Use Toon Texture",
|
||||
description="Use toon texture",
|
||||
update=_toggleUseToonTexture,
|
||||
default=True,
|
||||
)
|
||||
|
||||
use_sphere_texture: bpy.props.BoolProperty(
|
||||
name="Use Sphere Texture",
|
||||
description="Use sphere texture",
|
||||
update=_toggleUseSphereTexture,
|
||||
default=True,
|
||||
)
|
||||
|
||||
use_sdef: bpy.props.BoolProperty(
|
||||
name="Use SDEF",
|
||||
description="Use SDEF",
|
||||
update=_toggleUseSDEF,
|
||||
default=True,
|
||||
)
|
||||
|
||||
use_property_driver: bpy.props.BoolProperty(
|
||||
name="Use Property Driver",
|
||||
description="Setup drivers for MMD property animation (Visibility and IK toggles)",
|
||||
update=_toggleUsePropertyDriver,
|
||||
default=False,
|
||||
)
|
||||
|
||||
is_built: bpy.props.BoolProperty(
|
||||
name="Is Built",
|
||||
)
|
||||
|
||||
active_rigidbody_index: bpy.props.IntProperty(
|
||||
name="Active Rigidbody Index",
|
||||
min=0,
|
||||
get=_getActiveRigidbodyObject,
|
||||
set=_setActiveRigidbodyObject,
|
||||
)
|
||||
|
||||
active_joint_index: bpy.props.IntProperty(
|
||||
name="Active Joint Index",
|
||||
min=0,
|
||||
get=_getActiveJointObject,
|
||||
set=_setActiveJointObject,
|
||||
)
|
||||
|
||||
# *************************
|
||||
# Display Items
|
||||
# *************************
|
||||
display_item_frames: bpy.props.CollectionProperty(
|
||||
name="Display Frames",
|
||||
type=MMDDisplayItemFrame,
|
||||
)
|
||||
|
||||
active_display_item_frame: bpy.props.IntProperty(
|
||||
name="Active Display Item Frame",
|
||||
min=0,
|
||||
default=0,
|
||||
)
|
||||
|
||||
# *************************
|
||||
# Morph
|
||||
# *************************
|
||||
material_morphs: bpy.props.CollectionProperty(
|
||||
name="Material Morphs",
|
||||
type=MaterialMorph,
|
||||
)
|
||||
uv_morphs: bpy.props.CollectionProperty(
|
||||
name="UV Morphs",
|
||||
type=UVMorph,
|
||||
)
|
||||
bone_morphs: bpy.props.CollectionProperty(
|
||||
name="Bone Morphs",
|
||||
type=BoneMorph,
|
||||
)
|
||||
vertex_morphs: bpy.props.CollectionProperty(name="Vertex Morphs", type=VertexMorph)
|
||||
group_morphs: bpy.props.CollectionProperty(
|
||||
name="Group Morphs",
|
||||
type=GroupMorph,
|
||||
)
|
||||
active_morph_type: bpy.props.EnumProperty(
|
||||
name="Active Morph Type",
|
||||
description="Select current 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",
|
||||
)
|
||||
active_morph: bpy.props.IntProperty(
|
||||
name="Active Morph",
|
||||
min=0,
|
||||
set=_setActiveMorph,
|
||||
get=_getActiveMorph,
|
||||
)
|
||||
morph_panel_show_settings: bpy.props.BoolProperty(
|
||||
name="Morph Panel Show Settings",
|
||||
description="Show Morph Settings",
|
||||
default=True,
|
||||
)
|
||||
active_mesh_index: bpy.props.IntProperty(
|
||||
name="Active Mesh",
|
||||
min=0,
|
||||
set=_setActiveMeshObject,
|
||||
get=_getActiveMeshObject,
|
||||
)
|
||||
|
||||
# *************************
|
||||
# Translation
|
||||
# *************************
|
||||
translation: bpy.props.PointerProperty(
|
||||
name="Translation",
|
||||
type=MMDTranslation,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __get_select(prop: bpy.types.Object) -> bool:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def register():
|
||||
bpy.types.Object.mmd_type = patch_library_overridable(
|
||||
bpy.props.EnumProperty(
|
||||
name="Type",
|
||||
description="Internal MMD type of this object (DO NOT CHANGE IT DIRECTLY)",
|
||||
default="NONE",
|
||||
items=[
|
||||
("NONE", "None", "", 1),
|
||||
("ROOT", "Root", "", 2),
|
||||
("RIGID_GRP_OBJ", "Rigid Body Grp Empty", "", 3),
|
||||
("JOINT_GRP_OBJ", "Joint Grp Empty", "", 4),
|
||||
("TEMPORARY_GRP_OBJ", "Temporary Grp Empty", "", 5),
|
||||
("PLACEHOLDER", "Place Holder", "", 6),
|
||||
("CAMERA", "Camera", "", 21),
|
||||
("JOINT", "Joint", "", 22),
|
||||
("RIGID_BODY", "Rigid body", "", 23),
|
||||
("LIGHT", "Light", "", 24),
|
||||
("TRACK_TARGET", "Track Target", "", 51),
|
||||
("NON_COLLISION_CONSTRAINT", "Non Collision Constraint", "", 52),
|
||||
("SPRING_CONSTRAINT", "Spring Constraint", "", 53),
|
||||
("SPRING_GOAL", "Spring Goal", "", 54),
|
||||
],
|
||||
)
|
||||
)
|
||||
bpy.types.Object.mmd_root = patch_library_overridable(bpy.props.PointerProperty(type=MMDRoot))
|
||||
|
||||
bpy.types.Object.select = patch_library_overridable(
|
||||
bpy.props.BoolProperty(
|
||||
get=MMDRoot.__get_select,
|
||||
set=MMDRoot.__set_select,
|
||||
options={
|
||||
"SKIP_SAVE",
|
||||
"ANIMATABLE",
|
||||
"LIBRARY_EDITABLE",
|
||||
},
|
||||
)
|
||||
)
|
||||
bpy.types.Object.hide = patch_library_overridable(
|
||||
bpy.props.BoolProperty(
|
||||
get=MMDRoot.__get_hide,
|
||||
set=MMDRoot.__set_hide,
|
||||
options={
|
||||
"SKIP_SAVE",
|
||||
"ANIMATABLE",
|
||||
"LIBRARY_EDITABLE",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def unregister():
|
||||
del bpy.types.Object.hide
|
||||
del bpy.types.Object.select
|
||||
del bpy.types.Object.mmd_root
|
||||
del bpy.types.Object.mmd_type
|
||||
@@ -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)
|
||||
@@ -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 os
|
||||
import re
|
||||
from typing import Callable, Optional, Set
|
||||
|
||||
import bpy
|
||||
|
||||
from .bpyutils import FnContext
|
||||
|
||||
|
||||
## 指定したオブジェクトのみを選択状態かつアクティブにする
|
||||
def selectAObject(obj):
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
pass
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
FnContext.select_object(FnContext.ensure_context(), obj)
|
||||
FnContext.set_active_object(FnContext.ensure_context(), obj)
|
||||
|
||||
|
||||
## 現在のモードを指定したオブジェクトのEdit Modeに変更する
|
||||
def enterEditMode(obj):
|
||||
selectAObject(obj)
|
||||
if obj.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
|
||||
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", xmirror=False, keep_transform=False)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
|
||||
def selectSingleBone(context, armature, bone_name, reset_pose=False):
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
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: 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
|
||||
|
||||
|
||||
__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$")
|
||||
__CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
|
||||
|
||||
|
||||
## 日本語で左右を命名されている名前をblender方式のL(R)に変更する
|
||||
def convertNameToLR(name, use_underscore=False):
|
||||
m = __CONVERT_NAME_TO_L_REGEXP.match(name)
|
||||
delimiter = "_" if use_underscore else "."
|
||||
if m:
|
||||
name = m.group(1) + m.group(2) + delimiter + "L"
|
||||
m = __CONVERT_NAME_TO_R_REGEXP.match(name)
|
||||
if m:
|
||||
name = m.group(1) + m.group(2) + delimiter + "R"
|
||||
return name
|
||||
|
||||
|
||||
__CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[lL])(?P<after>($|(?P=separator)))")
|
||||
__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P<lr>(?P<separator>[._])[rR])(?P<after>($|(?P=separator)))")
|
||||
|
||||
|
||||
def convertLRToName(name):
|
||||
match = __CONVERT_L_TO_NAME_REGEXP.search(name)
|
||||
if match:
|
||||
return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}"
|
||||
|
||||
match = __CONVERT_R_TO_NAME_REGEXP.search(name)
|
||||
if match:
|
||||
return f"右{name[0:match.start()]}{match['after']}{name[match.end():]}"
|
||||
|
||||
return name
|
||||
|
||||
|
||||
## 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]
|
||||
|
||||
vtxIndex = src_vertex_group.index
|
||||
for v in mesh.vertices:
|
||||
try:
|
||||
gi = [i.group for i in v.groups].index(vtxIndex)
|
||||
dest_vertex_group.add([v.index], v.groups[gi].weight, "ADD")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
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)
|
||||
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():
|
||||
meshes_to_delete = []
|
||||
for mesh in bpy.data.meshes:
|
||||
if mesh.users == 0:
|
||||
meshes_to_delete.append(mesh)
|
||||
|
||||
for mesh in meshes_to_delete:
|
||||
bpy.data.meshes.remove(mesh)
|
||||
|
||||
|
||||
## 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:
|
||||
"""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.
|
||||
|
||||
Returns:
|
||||
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 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: 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)
|
||||
|
||||
if strategy == "absolute":
|
||||
return os.path.abspath(path)
|
||||
|
||||
if strategy == "outside" and os.name == "nt":
|
||||
d1, _ = os.path.splitdrive(path)
|
||||
d2, _ = os.path.splitdrive(start)
|
||||
if d1 != d2:
|
||||
return ".." + os.sep + os.path.basename(path)
|
||||
|
||||
return os.path.relpath(path, start)
|
||||
|
||||
class ItemOp:
|
||||
@staticmethod
|
||||
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):
|
||||
count = length - len(items)
|
||||
if count > 0:
|
||||
for i in range(count):
|
||||
items.add()
|
||||
elif count < 0:
|
||||
for i in range(-count):
|
||||
items.remove(length)
|
||||
|
||||
@staticmethod
|
||||
def add_after(items, index):
|
||||
index_end = len(items)
|
||||
index = max(0, min(index_end, index + 1))
|
||||
items.add()
|
||||
items.move(index_end, index)
|
||||
return items[index], index
|
||||
|
||||
|
||||
class ItemMoveOp:
|
||||
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, 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:
|
||||
items.move(index, index_min)
|
||||
return index_min
|
||||
elif index > index_max:
|
||||
items.move(index, index_max)
|
||||
return index_max
|
||||
|
||||
index_new = index
|
||||
if move_type == "UP":
|
||||
index_new = max(index_min, index - 1)
|
||||
elif move_type == "DOWN":
|
||||
index_new = min(index + 1, index_max)
|
||||
elif move_type == "TOP":
|
||||
index_new = index_min
|
||||
elif move_type == "BOTTOM":
|
||||
index_new = index_max
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user