PMX Import now works
This commit is contained in:
@@ -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()
|
||||
@@ -1,533 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2013 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
|
||||
# All credit goes to the original authors.
|
||||
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
|
||||
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
|
||||
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
|
||||
|
||||
import contextlib
|
||||
from typing import Generator, List, Optional, TypeVar, Dict, Any, Set, Tuple, Type
|
||||
|
||||
import bpy
|
||||
from bpy.types import Object, Material, Context
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from ...logging_setup import logger
|
||||
from ...addon_preferences import get_preference, save_preference
|
||||
|
||||
|
||||
class __EditMode:
|
||||
"""Context manager for edit mode operations"""
|
||||
def __init__(self, obj: Object):
|
||||
if not isinstance(obj, bpy.types.Object):
|
||||
raise ValueError("Expected a Blender Object")
|
||||
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:
|
||||
"""Context manager for object selection operations"""
|
||||
def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None):
|
||||
if not isinstance(active_object, bpy.types.Object):
|
||||
raise ValueError("Expected a Blender Object")
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
context = FnContext.ensure_context()
|
||||
|
||||
for i in context.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(context, i)
|
||||
FnContext.set_active_object(context, active_object)
|
||||
|
||||
def __enter__(self) -> 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: Object, parent: Object) -> None:
|
||||
"""Set parent relationship between objects"""
|
||||
with select_object(parent, objects=[parent, obj]):
|
||||
bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False)
|
||||
|
||||
|
||||
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
||||
"""Set parent relationship to a specific bone"""
|
||||
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: Object):
|
||||
"""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: Object, objects: Optional[List[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...
|
||||
"""
|
||||
return __SelectObjects(obj, objects)
|
||||
|
||||
|
||||
def duplicateObject(obj: Object, total_len: int) -> List[Object]:
|
||||
"""Duplicate an object multiple times"""
|
||||
return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len)
|
||||
|
||||
|
||||
def createObject(name: str = "Object", object_data: Optional[Any] = None, target_scene: Optional[Any] = None) -> Object:
|
||||
"""Create a new object and link it to the scene"""
|
||||
context = FnContext.ensure_context(target_scene)
|
||||
return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data))
|
||||
|
||||
|
||||
def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object:
|
||||
"""Create a sphere mesh object"""
|
||||
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: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object:
|
||||
"""Create a box mesh object"""
|
||||
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: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object:
|
||||
"""Create a capsule mesh object"""
|
||||
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 * 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:
|
||||
"""Helper class for transform constraints"""
|
||||
__MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"}
|
||||
|
||||
@staticmethod
|
||||
def create(constraints, name: str, map_type: str):
|
||||
"""Create a transform constraint"""
|
||||
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: str, name_id: str = "") -> Tuple[str, ...]:
|
||||
"""Get min/max attribute names for a constraint type"""
|
||||
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: float, influence: Optional[float] = 1):
|
||||
"""Update min/max values for a constraint"""
|
||||
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:
|
||||
"""Function collection for object operations"""
|
||||
def __init__(self):
|
||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def mesh_remove_shape_key(mesh_object: Object, shape_key: bpy.types.ShapeKey) -> None:
|
||||
"""Remove a shape key from a mesh object, cleaning up drivers"""
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class FnContext:
|
||||
"""Function collection for context operations"""
|
||||
def __init__(self):
|
||||
raise NotImplementedError("This class is not expected to be instantiated.")
|
||||
|
||||
@staticmethod
|
||||
def ensure_context(context: Optional[Context] = None) -> Context:
|
||||
"""Get a valid context, using bpy.context if none provided"""
|
||||
return context or bpy.context
|
||||
|
||||
@staticmethod
|
||||
def get_active_object(context: Context) -> Optional[Object]:
|
||||
"""Get the active object from context safely"""
|
||||
if context is None or not hasattr(context, 'active_object'):
|
||||
return None
|
||||
return context.active_object
|
||||
|
||||
@staticmethod
|
||||
def set_active_object(context: Context, obj: Object) -> Object:
|
||||
"""Set the active object in context"""
|
||||
context.view_layer.objects.active = obj
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def set_active_and_select_single_object(context: Context, obj: Object) -> Object:
|
||||
"""Set an object as active and the only selected object"""
|
||||
return FnContext.set_active_object(context, FnContext.select_single_object(context, obj))
|
||||
|
||||
@staticmethod
|
||||
def get_scene_objects(context: Context) -> List[Object]:
|
||||
"""Get all objects in the scene safely"""
|
||||
if context is None or not hasattr(context, 'scene') or not hasattr(context.scene, 'objects'):
|
||||
return []
|
||||
return context.scene.objects
|
||||
|
||||
@staticmethod
|
||||
def ensure_selectable(context: Context, obj: Object) -> Object:
|
||||
"""Make sure an object is selectable by unhiding it and its collections"""
|
||||
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: Context, obj: Object) -> Object:
|
||||
"""Select an object in the context"""
|
||||
FnContext.ensure_selectable(context, obj).select_set(True)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def select_objects(context: Context, *objects: Object) -> List[Object]:
|
||||
"""Select multiple objects in the context"""
|
||||
return [FnContext.select_object(context, obj) for obj in objects]
|
||||
|
||||
@staticmethod
|
||||
def select_single_object(context: Context, obj: Object) -> Object:
|
||||
"""Select only the specified object, deselecting all others"""
|
||||
for i in context.selected_objects:
|
||||
if i != obj:
|
||||
i.select_set(False)
|
||||
return FnContext.select_object(context, obj)
|
||||
|
||||
@staticmethod
|
||||
def link_object(context: Context, obj: Object) -> Object:
|
||||
"""Link an object to the active collection"""
|
||||
context.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def new_and_link_object(context: Context, name: str, object_data: Optional[Any]) -> Object:
|
||||
"""Create a new object and link it to the active collection"""
|
||||
return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data))
|
||||
|
||||
@staticmethod
|
||||
def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[Object]:
|
||||
"""
|
||||
Duplicate an object to reach the target count.
|
||||
|
||||
Args:
|
||||
context: The context in which the duplication is performed
|
||||
object_to_duplicate: The object to be duplicated
|
||||
target_count: The desired count of duplicated objects
|
||||
|
||||
Returns:
|
||||
A list of duplicated objects
|
||||
"""
|
||||
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: Context, target_object: Object) -> Optional[bpy.types.LayerCollection]:
|
||||
"""
|
||||
Find the layer collection containing the target object.
|
||||
|
||||
Args:
|
||||
context: The Blender context
|
||||
target_object: The target object to find the layer collection for
|
||||
|
||||
Returns:
|
||||
The layer collection containing 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
|
||||
|
||||
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
|
||||
|
||||
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: Context, target_object: Object) -> Generator[Context, None, None]:
|
||||
"""
|
||||
Temporarily override the active layer collection to the one containing the target object.
|
||||
|
||||
Args:
|
||||
context: The context to modify
|
||||
target_object: The object whose collection should become active
|
||||
|
||||
Yields:
|
||||
The modified context
|
||||
"""
|
||||
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
|
||||
@contextlib.contextmanager
|
||||
def temp_override_objects(
|
||||
context: Context,
|
||||
active_object: Optional[Object] = None,
|
||||
selected_objects: Optional[List[Object]] = None,
|
||||
**keywords
|
||||
) -> Generator[Context, None, None]:
|
||||
"""Create a temporary context override for object operations using Blender 4.4+ temp_override."""
|
||||
override_dict = {}
|
||||
|
||||
if active_object is not None:
|
||||
override_dict["active_object"] = active_object
|
||||
override_dict["object"] = active_object
|
||||
|
||||
if selected_objects is not None:
|
||||
override_dict["selected_objects"] = selected_objects
|
||||
override_dict["selected_editable_objects"] = selected_objects
|
||||
|
||||
override_dict.update(keywords)
|
||||
|
||||
with context.temp_override(**override_dict) as override_context:
|
||||
yield override_context
|
||||
|
||||
@staticmethod
|
||||
def get_preference(key: str, default: T = None) -> T:
|
||||
"""
|
||||
Get a preference value using Avatar Toolkit's preference system."""
|
||||
return get_preference(key, default)
|
||||
|
||||
@staticmethod
|
||||
def save_preference(key: str, value: Any) -> None:
|
||||
"""Save a preference value using Avatar Toolkit's preference system."""
|
||||
save_preference(key, value)
|
||||
@@ -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
|
||||
@@ -1,296 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2013 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
|
||||
# All credit goes to the original authors.
|
||||
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
|
||||
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
|
||||
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Callable, Optional, Set, List, Dict, Any
|
||||
|
||||
import bpy
|
||||
from bpy.types import Object, Context, Bone, PoseBone
|
||||
|
||||
from ...logging_setup import logger
|
||||
from .bpyutils import FnContext
|
||||
|
||||
|
||||
def selectAObject(obj: Object) -> None:
|
||||
"""Select a single object and make it active"""
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
logger.debug(f"Failed to set object mode for {obj.name}")
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
FnContext.select_object(FnContext.ensure_context(), obj)
|
||||
FnContext.set_active_object(FnContext.ensure_context(), obj)
|
||||
|
||||
|
||||
def enterEditMode(obj: Object) -> None:
|
||||
"""Enter edit mode for the specified object"""
|
||||
selectAObject(obj)
|
||||
if obj.mode != "EDIT":
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
|
||||
def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None:
|
||||
"""Set an object's parent to a specific bone"""
|
||||
selectAObject(obj)
|
||||
FnContext.set_active_object(FnContext.ensure_context(), parent)
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
parent.data.bones.active = parent.data.bones[bone_name]
|
||||
bpy.ops.object.parent_set(type="BONE", keep_transform=False)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
|
||||
def selectSingleBone(context: Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None:
|
||||
"""Select a single bone in an armature"""
|
||||
try:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
except Exception:
|
||||
logger.debug(f"Failed to set object mode for bone selection: {bone_name}")
|
||||
|
||||
for i in context.selected_objects:
|
||||
i.select_set(False)
|
||||
|
||||
FnContext.set_active_object(context, armature)
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
|
||||
if reset_pose:
|
||||
for p_bone in armature.pose.bones:
|
||||
p_bone.matrix_basis.identity()
|
||||
|
||||
armature_bones = armature.data.bones
|
||||
for bone in armature_bones:
|
||||
bone.select = bone.name == bone_name
|
||||
bone.select_head = bone.select_tail = bone.select
|
||||
if bone.select:
|
||||
armature_bones.active = bone
|
||||
bone.hide = False
|
||||
|
||||
|
||||
# Regular expressions for name conversion
|
||||
__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$")
|
||||
__CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$")
|
||||
|
||||
|
||||
def convertNameToLR(name: str, use_underscore: bool = False) -> str:
|
||||
"""Convert Japanese left/right naming to Blender's L/R convention"""
|
||||
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: str) -> str:
|
||||
"""Convert Blender's L/R convention to Japanese left/right naming"""
|
||||
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
|
||||
|
||||
|
||||
def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None:
|
||||
"""Merge weights from source vertex group to destination vertex group"""
|
||||
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: Object) -> None:
|
||||
"""Separate a mesh object by materials"""
|
||||
if len(meshObj.data.materials) < 2:
|
||||
selectAObject(meshObj)
|
||||
return
|
||||
|
||||
matrix_parent_inverse = meshObj.matrix_parent_inverse.copy()
|
||||
prev_parent = meshObj.parent
|
||||
dummy_parent = bpy.data.objects.new(name="tmp", object_data=None)
|
||||
bpy.context.collection.objects.link(dummy_parent)
|
||||
|
||||
meshObj.parent = dummy_parent
|
||||
meshObj.active_shape_key_index = 0
|
||||
|
||||
try:
|
||||
enterEditMode(meshObj)
|
||||
bpy.ops.mesh.select_all(action="SELECT")
|
||||
bpy.ops.mesh.separate(type="MATERIAL")
|
||||
finally:
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
for i in dummy_parent.children:
|
||||
materials = i.data.materials
|
||||
i.name = getattr(materials[0], "name", "None") if len(materials) else "None"
|
||||
i.parent = prev_parent
|
||||
i.matrix_parent_inverse = matrix_parent_inverse
|
||||
|
||||
bpy.data.objects.remove(dummy_parent)
|
||||
|
||||
|
||||
def clearUnusedMeshes() -> None:
|
||||
"""Remove unused mesh data blocks"""
|
||||
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)
|
||||
|
||||
|
||||
def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]:
|
||||
"""Create a mapping from bone names to pose bones"""
|
||||
return {(i.mmd_bone.name_j or i.name): i for i in armObj.pose.bones}
|
||||
|
||||
|
||||
__REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$")
|
||||
|
||||
|
||||
def unique_name(name: str, used_names: Set[str]) -> str:
|
||||
"""Create a unique name that doesn't exist in the used_names set
|
||||
|
||||
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 saferelpath(path: str, start: str, strategy: str = "inside") -> str:
|
||||
"""Safely get a relative path, handling different drive issues on Windows
|
||||
|
||||
Strategies:
|
||||
- inside: returns the basename of the path
|
||||
- outside: prepends '..' to the basename if on different drive
|
||||
- absolute: returns the absolute path
|
||||
"""
|
||||
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:
|
||||
"""Operations for managing collections of items"""
|
||||
|
||||
@staticmethod
|
||||
def get_by_index(items: List[Any], index: int) -> Optional[Any]:
|
||||
"""Get an item by index with bounds checking"""
|
||||
if 0 <= index < len(items):
|
||||
return items[index]
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def resize(items: bpy.types.bpy_prop_collection, length: int) -> None:
|
||||
"""Resize a collection to the specified length"""
|
||||
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: bpy.types.bpy_prop_collection, index: int) -> tuple:
|
||||
"""Add a new item after the specified 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:
|
||||
"""Operations for moving items in collections"""
|
||||
|
||||
@staticmethod
|
||||
def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str,
|
||||
index_min: int = 0, index_max: Optional[int] = None) -> int:
|
||||
"""Move an item in a collection
|
||||
|
||||
Args:
|
||||
items: The collection to modify
|
||||
index: Current index of the item
|
||||
move_type: Type of move ('UP', 'DOWN', 'TOP', 'BOTTOM')
|
||||
index_min: Minimum allowed index
|
||||
index_max: Maximum allowed index
|
||||
|
||||
Returns:
|
||||
int: The new index after moving
|
||||
"""
|
||||
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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user