Initial MMD Importer Commit
- This is the initial commit I spent several hours trying to get it up two Avatar Toolkit standard, it does not work yet because there are files missing but I been doing this since 6am and it is 4pm almost, i need food. - I have also removed as much legacy code as i could, MMD Tools contains so much of it even though there have a 4.2+ only version there have not removed any of the legacy code for pre 4.2.... this is going to take a while. God I hope this works fine once I am done.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,587 @@
|
|||||||
|
# -*- 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 math
|
||||||
|
from typing import TYPE_CHECKING, Iterable, Optional, Set
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
from ..logging_setup import logger
|
||||||
|
from .. import common
|
||||||
|
from ..common import ProgressTracker
|
||||||
|
from ..bpyutils import TransformConstraintOp
|
||||||
|
|
||||||
|
# Constants for bone collections
|
||||||
|
BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools"
|
||||||
|
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection"
|
||||||
|
BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection"
|
||||||
|
BONE_COLLECTION_NAME_SHADOW = "mmd_shadow"
|
||||||
|
BONE_COLLECTION_NAME_DUMMY = "mmd_dummy"
|
||||||
|
|
||||||
|
SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NAME_DUMMY]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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 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 load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True):
|
||||||
|
for b in FnBone.__get_selected_pose_bones(armature_object):
|
||||||
|
mmd_bone = b.mmd_bone
|
||||||
|
mmd_bone.enabled_fixed_axis = enable
|
||||||
|
lock_rotation = b.lock_rotation[:]
|
||||||
|
if enable:
|
||||||
|
axes = b.bone.matrix_local.to_3x3().transposed()
|
||||||
|
if lock_rotation.count(False) == 1:
|
||||||
|
mmd_bone.fixed_axis = axes[lock_rotation.index(False)].xzy
|
||||||
|
else:
|
||||||
|
mmd_bone.fixed_axis = axes[1].xzy # Y-axis
|
||||||
|
elif all(b.lock_location) and lock_rotation.count(True) > 1 and lock_rotation == (b.lock_ik_x, b.lock_ik_y, b.lock_ik_z):
|
||||||
|
# unlock transform locks if fixed axis was applied
|
||||||
|
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = (False, False, False)
|
||||||
|
b.lock_location = b.lock_scale = (False, False, False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def apply_bone_fixed_axis(armature_object: bpy.types.Object):
|
||||||
|
with ProgressTracker(bpy.context, 100, "Applying Bone Fixed Axis") as progress:
|
||||||
|
bone_map = {}
|
||||||
|
for b in armature_object.pose.bones:
|
||||||
|
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis:
|
||||||
|
continue
|
||||||
|
mmd_bone = b.mmd_bone
|
||||||
|
parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip
|
||||||
|
bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip)
|
||||||
|
|
||||||
|
progress.step("Processing bones")
|
||||||
|
|
||||||
|
force_align = True
|
||||||
|
with common.edit_object(armature_object) as data:
|
||||||
|
bone: bpy.types.EditBone
|
||||||
|
for bone in data.edit_bones:
|
||||||
|
if bone.name not in bone_map:
|
||||||
|
bone.select = False
|
||||||
|
continue
|
||||||
|
fixed_axis, is_tip, parent_tip = bone_map[bone.name]
|
||||||
|
if fixed_axis.length:
|
||||||
|
axes = [bone.x_axis, bone.y_axis, bone.z_axis]
|
||||||
|
direction = fixed_axis.normalized().xzy
|
||||||
|
idx, val = max([(i, direction.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1]))
|
||||||
|
idx_1, idx_2 = (idx + 1) % 3, (idx + 2) % 3
|
||||||
|
axes[idx] = -direction if val < 0 else direction
|
||||||
|
axes[idx_2] = axes[idx].cross(axes[idx_1])
|
||||||
|
axes[idx_1] = axes[idx_2].cross(axes[idx])
|
||||||
|
if parent_tip and bone.use_connect:
|
||||||
|
bone.use_connect = False
|
||||||
|
bone.head = bone.parent.head
|
||||||
|
if force_align:
|
||||||
|
tail = bone.head + axes[1].normalized() * bone.length
|
||||||
|
if is_tip or (tail - bone.tail).length > 1e-4:
|
||||||
|
for c in bone.children:
|
||||||
|
if c.use_connect:
|
||||||
|
c.use_connect = False
|
||||||
|
if is_tip:
|
||||||
|
c.head = bone.head
|
||||||
|
bone.tail = tail
|
||||||
|
bone.align_roll(axes[2])
|
||||||
|
bone_map[bone.name] = tuple(i != idx for i in range(3))
|
||||||
|
else:
|
||||||
|
bone_map[bone.name] = (True, True, True)
|
||||||
|
bone.select = True
|
||||||
|
|
||||||
|
progress.step("Applying locks")
|
||||||
|
|
||||||
|
for bone_name, locks in bone_map.items():
|
||||||
|
b = armature_object.pose.bones[bone_name]
|
||||||
|
b.lock_location = (True, True, True)
|
||||||
|
b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_bone_local_axes(armature_object: bpy.types.Object, enable=True):
|
||||||
|
for b in FnBone.__get_selected_pose_bones(armature_object):
|
||||||
|
mmd_bone = b.mmd_bone
|
||||||
|
mmd_bone.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):
|
||||||
|
with ProgressTracker(bpy.context, 100, "Applying Bone Local Axes") as progress:
|
||||||
|
bone_map = {}
|
||||||
|
for b in armature_object.pose.bones:
|
||||||
|
if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes:
|
||||||
|
continue
|
||||||
|
mmd_bone = b.mmd_bone
|
||||||
|
bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z)
|
||||||
|
|
||||||
|
progress.step("Processing bones")
|
||||||
|
|
||||||
|
with common.edit_object(armature_object) as data:
|
||||||
|
bone: bpy.types.EditBone
|
||||||
|
for bone in data.edit_bones:
|
||||||
|
if bone.name not in bone_map:
|
||||||
|
bone.select = False
|
||||||
|
continue
|
||||||
|
local_axis_x, local_axis_z = bone_map[bone.name]
|
||||||
|
FnBone.update_bone_roll(bone, local_axis_x, local_axis_z)
|
||||||
|
bone.select = True
|
||||||
|
|
||||||
|
@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):
|
||||||
|
with ProgressTracker(bpy.context, 100, "Applying Auto Bone Roll") as progress:
|
||||||
|
bone_names = []
|
||||||
|
for b in armature.pose.bones:
|
||||||
|
if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j):
|
||||||
|
bone_names.append(b.name)
|
||||||
|
|
||||||
|
progress.step("Processing bones")
|
||||||
|
|
||||||
|
with common.edit_object(armature) as data:
|
||||||
|
bone: bpy.types.EditBone
|
||||||
|
for bone in data.edit_bones:
|
||||||
|
if bone.name not in bone_names:
|
||||||
|
continue
|
||||||
|
FnBone.update_auto_bone_roll(bone)
|
||||||
|
bone.select = True
|
||||||
|
|
||||||
|
@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):
|
||||||
|
logger.info(f"Cleaning additional transformations for {armature_object.name}")
|
||||||
|
|
||||||
|
# clean constraints
|
||||||
|
p_bone: bpy.types.PoseBone
|
||||||
|
for p_bone in armature_object.pose.bones:
|
||||||
|
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 common.edit_object(armature_object) as data:
|
||||||
|
remove_edit_bones(data.edit_bones, shadow_bone_names)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def apply_additional_transformation(armature_object: bpy.types.Object):
|
||||||
|
with ProgressTracker(bpy.context, 100, "Applying Additional Transformations") as progress:
|
||||||
|
def __is_dirty_bone(b):
|
||||||
|
if b.is_mmd_shadow_bone:
|
||||||
|
return False
|
||||||
|
mmd_bone = b.mmd_bone
|
||||||
|
if mmd_bone.has_additional_rotation or mmd_bone.has_additional_location:
|
||||||
|
return True
|
||||||
|
return mmd_bone.is_additional_transform_dirty
|
||||||
|
|
||||||
|
dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)]
|
||||||
|
|
||||||
|
progress.step("Setting up constraints")
|
||||||
|
|
||||||
|
# setup constraints
|
||||||
|
shadow_bone_pool = []
|
||||||
|
for p_bone in dirty_bones:
|
||||||
|
sb = FnBone.__setup_constraints(p_bone)
|
||||||
|
if sb:
|
||||||
|
shadow_bone_pool.append(sb)
|
||||||
|
|
||||||
|
progress.step("Setting up shadow bones")
|
||||||
|
|
||||||
|
# setup shadow bones
|
||||||
|
with common.edit_object(armature_object) as data:
|
||||||
|
edit_bones = data.edit_bones
|
||||||
|
for sb in shadow_bone_pool:
|
||||||
|
sb.update_edit_bones(edit_bones)
|
||||||
|
|
||||||
|
pose_bones = armature_object.pose.bones
|
||||||
|
for sb in shadow_bone_pool:
|
||||||
|
sb.update_pose_bones(pose_bones)
|
||||||
|
|
||||||
|
progress.step("Finalizing")
|
||||||
|
|
||||||
|
# finish
|
||||||
|
for p_bone in dirty_bones:
|
||||||
|
p_bone.mmd_bone.is_additional_transform_dirty = False
|
||||||
|
|
||||||
|
@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
|
||||||
|
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):
|
||||||
|
with ProgressTracker(bpy.context, 100, "Fixing MMD IK Limit Override") as progress:
|
||||||
|
pose_bone: bpy.types.PoseBone
|
||||||
|
for pose_bone in armature_object.pose.bones:
|
||||||
|
constraint: bpy.types.Constraint
|
||||||
|
for constraint in pose_bone.constraints:
|
||||||
|
if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name:
|
||||||
|
constraint.owner_space = "LOCAL"
|
||||||
|
|
||||||
|
progress.step("Fixed IK limit overrides")
|
||||||
|
|
||||||
|
|
||||||
|
class _AT_ShadowBoneRemove:
|
||||||
|
def __init__(self, bone_name):
|
||||||
|
self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name)
|
||||||
|
|
||||||
|
def update_edit_bones(self, edit_bones):
|
||||||
|
remove_edit_bones(edit_bones, self.__shadow_bone_names)
|
||||||
|
|
||||||
|
def update_pose_bones(self, pose_bones):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class _AT_ShadowBoneCreate:
|
||||||
|
def __init__(self, bone_name, target_bone_name):
|
||||||
|
self.__dummy_bone_name = "_dummy_" + bone_name
|
||||||
|
self.__shadow_bone_name = "_shadow_" + bone_name
|
||||||
|
self.__bone_name = bone_name
|
||||||
|
self.__target_bone_name = target_bone_name
|
||||||
|
self.__constraint_pool = []
|
||||||
|
|
||||||
|
def __is_well_aligned(self, bone0, bone1):
|
||||||
|
return bone0.x_axis.dot(bone1.x_axis) > 0.99 and bone0.y_axis.dot(bone1.y_axis) > 0.99
|
||||||
|
|
||||||
|
def __update_constraints(self, use_shadow=True):
|
||||||
|
subtarget = self.__shadow_bone_name if use_shadow else self.__target_bone_name
|
||||||
|
for c in self.__constraint_pool:
|
||||||
|
c.subtarget = subtarget
|
||||||
|
|
||||||
|
def add_constraint(self, constraint):
|
||||||
|
self.__constraint_pool.append(constraint)
|
||||||
|
|
||||||
|
def update_edit_bones(self, edit_bones):
|
||||||
|
bone = edit_bones[self.__bone_name]
|
||||||
|
target_bone = edit_bones[self.__target_bone_name]
|
||||||
|
if bone != target_bone and self.__is_well_aligned(bone, target_bone):
|
||||||
|
_AT_ShadowBoneRemove(self.__bone_name).update_edit_bones(edit_bones)
|
||||||
|
return
|
||||||
|
|
||||||
|
dummy_bone_name = self.__dummy_bone_name
|
||||||
|
dummy = edit_bones.get(dummy_bone_name, None) or FnBone.set_edit_bone_to_dummy(edit_bones.new(name=dummy_bone_name))
|
||||||
|
dummy.parent = target_bone
|
||||||
|
dummy.head = target_bone.head
|
||||||
|
dummy.tail = dummy.head + bone.tail - bone.head
|
||||||
|
dummy.roll = bone.roll
|
||||||
|
|
||||||
|
shadow_bone_name = self.__shadow_bone_name
|
||||||
|
shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name))
|
||||||
|
shadow.parent = target_bone.parent
|
||||||
|
shadow.head = dummy.head
|
||||||
|
shadow.tail = dummy.tail
|
||||||
|
shadow.roll = bone.roll
|
||||||
|
|
||||||
|
def update_pose_bones(self, pose_bones):
|
||||||
|
if self.__shadow_bone_name not in pose_bones:
|
||||||
|
self.__update_constraints(use_shadow=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
dummy_p_bone = pose_bones[self.__dummy_bone_name]
|
||||||
|
dummy_p_bone.is_mmd_shadow_bone = True
|
||||||
|
dummy_p_bone.mmd_shadow_bone_type = "DUMMY"
|
||||||
|
|
||||||
|
shadow_p_bone = pose_bones[self.__shadow_bone_name]
|
||||||
|
shadow_p_bone.is_mmd_shadow_bone = True
|
||||||
|
shadow_p_bone.mmd_shadow_bone_type = "SHADOW"
|
||||||
|
|
||||||
|
if "mmd_tools_at_dummy" not in shadow_p_bone.constraints:
|
||||||
|
c = shadow_p_bone.constraints.new("COPY_TRANSFORMS")
|
||||||
|
c.name = "mmd_tools_at_dummy"
|
||||||
|
c.target = dummy_p_bone.id_data
|
||||||
|
c.subtarget = dummy_p_bone.name
|
||||||
|
c.target_space = "POSE"
|
||||||
|
c.owner_space = "POSE"
|
||||||
|
|
||||||
|
self.__update_constraints()
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
# -*- 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,296 @@
|
|||||||
|
# -*- 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,697 @@
|
|||||||
|
# -*- 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
|
||||||
|
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from mathutils import Vector
|
||||||
|
|
||||||
|
from ..logging_setup import logger
|
||||||
|
from .exceptions import MaterialNotFoundError
|
||||||
|
from .shader import _NodeGroupUtils
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..properties.material import MMDMaterial
|
||||||
|
|
||||||
|
# Constants for sphere modes
|
||||||
|
SPHERE_MODE_OFF = 0
|
||||||
|
SPHERE_MODE_MULT = 1
|
||||||
|
SPHERE_MODE_ADD = 2
|
||||||
|
SPHERE_MODE_SUBTEX = 3
|
||||||
|
|
||||||
|
|
||||||
|
class FnMaterial:
|
||||||
|
__NODES_ARE_READONLY: bool = False
|
||||||
|
|
||||||
|
def __init__(self, material: bpy.types.Material):
|
||||||
|
self.__material = material
|
||||||
|
self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY
|
||||||
|
logger.debug(f"Initializing FnMaterial for {material.name}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_nodes_are_readonly(nodes_are_readonly: bool):
|
||||||
|
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 = 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":
|
||||||
|
img_filepath = bpy.path.abspath(image.filepath)
|
||||||
|
if img_filepath == filepath:
|
||||||
|
return True
|
||||||
|
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:
|
||||||
|
try:
|
||||||
|
img = bpy.data.images.load(filepath)
|
||||||
|
logger.debug(f"Loaded image from {filepath}")
|
||||||
|
except:
|
||||||
|
logger.warning(f"Cannot create a texture for {filepath}. No such file.")
|
||||||
|
img = bpy.data.images.new(os.path.basename(filepath), 1, 1)
|
||||||
|
img.source = "FILE"
|
||||||
|
img.filepath = filepath
|
||||||
|
# For Blender 4.4+
|
||||||
|
if img.depth == 32 and img.file_format != "BMP":
|
||||||
|
img.alpha_mode = "CHANNEL_PACKED"
|
||||||
|
else:
|
||||||
|
img.alpha_mode = "NONE"
|
||||||
|
return img
|
||||||
|
|
||||||
|
def update_toon_texture(self):
|
||||||
|
if self._nodes_are_readonly:
|
||||||
|
return
|
||||||
|
mmd_mat = self.__material.mmd_material
|
||||||
|
if mmd_mat.is_shared_toon_texture:
|
||||||
|
# Get shared toon folder from preferences
|
||||||
|
context = bpy.context
|
||||||
|
addon_prefs = context.preferences.addons.get("avatar_toolkit", None)
|
||||||
|
if addon_prefs:
|
||||||
|
shared_toon_folder = addon_prefs.preferences.shared_toon_folder
|
||||||
|
else:
|
||||||
|
shared_toon_folder = ""
|
||||||
|
toon_path = os.path.join(shared_toon_folder, f"toon{mmd_mat.shared_toon_texture + 1:02d}.bmp")
|
||||||
|
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 = mat.mmd_material
|
||||||
|
color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3]
|
||||||
|
line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),)
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
if hasattr(mat, "line_color"): # freestyle line color
|
||||||
|
mat.line_color = line_color
|
||||||
|
|
||||||
|
mat_edge = bpy.data.materials.get("mmd_edge." + mat.name, None)
|
||||||
|
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")
|
||||||
|
|
||||||
|
def create_texture(self, filepath):
|
||||||
|
texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1))
|
||||||
|
return texture
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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 texture
|
||||||
|
|
||||||
|
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"):
|
||||||
|
# For Blender 4.4+
|
||||||
|
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":
|
||||||
|
logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex')
|
||||||
|
links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"])
|
||||||
|
else:
|
||||||
|
links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"])
|
||||||
|
|
||||||
|
def remove_sphere_texture(self):
|
||||||
|
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")
|
||||||
|
|
||||||
|
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 texture
|
||||||
|
|
||||||
|
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):
|
||||||
|
mat = self.material
|
||||||
|
texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None)
|
||||||
|
if isinstance(texture, bpy.types.ShaderNodeTexImage):
|
||||||
|
return 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")
|
||||||
|
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
|
||||||
|
# For Blender 4.4+
|
||||||
|
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
||||||
|
self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,))
|
||||||
|
|
||||||
|
def update_diffuse_color(self):
|
||||||
|
if self._nodes_are_readonly:
|
||||||
|
return
|
||||||
|
mat = self.material
|
||||||
|
mmd_mat = mat.mmd_material
|
||||||
|
# For Blender 4.4+
|
||||||
|
mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat)
|
||||||
|
self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,))
|
||||||
|
|
||||||
|
def update_alpha(self):
|
||||||
|
if self._nodes_are_readonly:
|
||||||
|
return
|
||||||
|
mat = self.material
|
||||||
|
mmd_mat = mat.mmd_material
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
mat.blend_method = "HASHED"
|
||||||
|
|
||||||
|
# Update alpha in diffuse_color
|
||||||
|
if len(mat.diffuse_color) > 3:
|
||||||
|
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
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37)
|
||||||
|
mat.metallic = pow(1 - mat.roughness, 2.7)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
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
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
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
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
preferred_output_node_target = "EEVEE"
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
shadow_method = getattr(m, "shadow_method", None)
|
||||||
|
|
||||||
|
if mmd_material.diffuse_color is None:
|
||||||
|
mmd_material.diffuse_color = m.diffuse_color[:3]
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
if len(m.diffuse_color) > 3:
|
||||||
|
mmd_material.alpha = m.diffuse_color[3]
|
||||||
|
|
||||||
|
mmd_material.specular_color = m.specular_color
|
||||||
|
|
||||||
|
# For Blender 4.4+
|
||||||
|
mmd_material.shininess = pow(1 / max(m.roughness, 0.099), 1 / 0.37)
|
||||||
|
mmd_material.is_double_sided = not m.use_backface_culling
|
||||||
|
|
||||||
|
if 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 = nodes.new("ShaderNodeGroup")
|
||||||
|
node_shader.name = "mmd_shader"
|
||||||
|
node_shader.location = (0, 1500)
|
||||||
|
node_shader.width = 200
|
||||||
|
node_shader.node_tree = self.__get_shader()
|
||||||
|
|
||||||
|
mmd_mat = mat.mmd_material
|
||||||
|
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 = 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 = 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.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||||
|
if len(shader.nodes):
|
||||||
|
return shader
|
||||||
|
|
||||||
|
ng = _NodeGroupUtils(shader)
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
_node_output = ng.new_node("NodeGroupOutput", (6, 0))
|
||||||
|
|
||||||
|
tex_coord = ng.new_node("ShaderNodeTexCoord", (0, 0))
|
||||||
|
|
||||||
|
tex_coord1 = ng.new_node("ShaderNodeUVMap", (4, -2))
|
||||||
|
tex_coord1.uv_map = "UV1"
|
||||||
|
|
||||||
|
vec_trans = ng.new_node("ShaderNodeVectorTransform", (1, -1))
|
||||||
|
vec_trans.vector_type = "NORMAL"
|
||||||
|
vec_trans.convert_from = "OBJECT"
|
||||||
|
vec_trans.convert_to = "CAMERA"
|
||||||
|
|
||||||
|
node_vector = ng.new_node("ShaderNodeMapping", (2, -1))
|
||||||
|
node_vector.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.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||||
|
if len(shader.nodes):
|
||||||
|
return shader
|
||||||
|
|
||||||
|
ng = _NodeGroupUtils(shader)
|
||||||
|
|
||||||
|
############################################################################
|
||||||
|
node_input = ng.new_node("NodeGroupInput", (-5, -1))
|
||||||
|
_node_output = ng.new_node("NodeGroupOutput", (11, 1))
|
||||||
|
|
||||||
|
node_diffuse = ng.new_mix_node("ADD", (-3, 4), fac=0.6)
|
||||||
|
node_diffuse.use_clamp = True
|
||||||
|
|
||||||
|
node_tex = ng.new_mix_node("MULTIPLY", (-2, 3.5))
|
||||||
|
node_toon = ng.new_mix_node("MULTIPLY", (-1, 3))
|
||||||
|
node_sph = ng.new_mix_node("MULTIPLY", (0, 2.5))
|
||||||
|
node_spa = ng.new_mix_node("ADD", (0, 1.5))
|
||||||
|
node_sphere = ng.new_mix_node("MIX", (1, 1))
|
||||||
|
|
||||||
|
node_geo = ng.new_node("ShaderNodeNewGeometry", (6, 3.5))
|
||||||
|
node_invert = ng.new_math_node("LESS_THAN", (7, 3))
|
||||||
|
node_cull = ng.new_math_node("MAXIMUM", (8, 2.5))
|
||||||
|
node_alpha = ng.new_math_node("MINIMUM", (9, 2))
|
||||||
|
node_alpha.use_clamp = True
|
||||||
|
node_alpha_tex = ng.new_math_node("MULTIPLY", (-1, -2))
|
||||||
|
node_alpha_toon = ng.new_math_node("MULTIPLY", (0, -2.5))
|
||||||
|
node_alpha_sph = ng.new_math_node("MULTIPLY", (1, -3))
|
||||||
|
|
||||||
|
node_reflect = ng.new_math_node("DIVIDE", (7, -1.5), value1=1)
|
||||||
|
node_reflect.use_clamp = True
|
||||||
|
|
||||||
|
shader_diffuse = ng.new_node("ShaderNodeBsdfDiffuse", (8, 0))
|
||||||
|
shader_glossy = ng.new_node("ShaderNodeBsdfAnisotropic", (8, -1))
|
||||||
|
shader_base_mix = ng.new_node("ShaderNodeMixShader", (9, 0))
|
||||||
|
shader_base_mix.inputs["Fac"].default_value = 0.02
|
||||||
|
shader_trans = ng.new_node("ShaderNodeBsdfTransparent", (9, 1))
|
||||||
|
shader_alpha_mix = ng.new_node("ShaderNodeMixShader", (10, 1))
|
||||||
|
|
||||||
|
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 = 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 = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0]
|
||||||
|
node_sphere = shader_diffuse.inputs["Color"].links[0].from_node
|
||||||
|
node_output = ng.node_output
|
||||||
|
shader_alpha_mix = node_output.inputs["Shader"].links[0].from_node
|
||||||
|
node_alpha = shader_alpha_mix.inputs["Fac"].links[0].from_node
|
||||||
|
|
||||||
|
ng.new_output_socket("Color", node_sphere.outputs["Color"])
|
||||||
|
ng.new_output_socket("Alpha", node_alpha.outputs["Value"])
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 MMD Tools authors
|
||||||
|
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
|
||||||
|
# All credit goes to the original authors.
|
||||||
|
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
|
||||||
|
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
|
||||||
|
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
from bpy.types import PropertyGroup, Context, PoseBone
|
||||||
|
from bpy.props import (
|
||||||
|
StringProperty,
|
||||||
|
IntProperty,
|
||||||
|
BoolProperty,
|
||||||
|
FloatProperty,
|
||||||
|
FloatVectorProperty
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..logging_setup import logger
|
||||||
|
from ..bone import FnBone
|
||||||
|
|
||||||
|
def _mmd_bone_update_additional_transform(prop, context: Context):
|
||||||
|
"""Update handler for additional transform properties"""
|
||||||
|
prop["is_additional_transform_dirty"] = True
|
||||||
|
p_bone = context.active_pose_bone
|
||||||
|
if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer():
|
||||||
|
FnBone.apply_additional_transformation(prop.id_data)
|
||||||
|
|
||||||
|
def _mmd_bone_update_additional_transform_influence(prop, context: Context):
|
||||||
|
"""Update handler for additional transform influence"""
|
||||||
|
pose_bone = context.active_pose_bone
|
||||||
|
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer():
|
||||||
|
FnBone.update_additional_transform_influence(pose_bone)
|
||||||
|
else:
|
||||||
|
prop["is_additional_transform_dirty"] = True
|
||||||
|
|
||||||
|
def _mmd_bone_get_additional_transform_bone(prop):
|
||||||
|
"""Getter for additional transform bone property"""
|
||||||
|
arm = prop.id_data
|
||||||
|
bone_id = prop.get("additional_transform_bone_id", -1)
|
||||||
|
if bone_id < 0:
|
||||||
|
return ""
|
||||||
|
pose_bone = FnBone.find_pose_bone_by_bone_id(arm, bone_id)
|
||||||
|
if pose_bone is None:
|
||||||
|
return ""
|
||||||
|
return pose_bone.name
|
||||||
|
|
||||||
|
def _mmd_bone_set_additional_transform_bone(prop, value: str):
|
||||||
|
"""Setter for additional transform bone property"""
|
||||||
|
arm = prop.id_data
|
||||||
|
prop["is_additional_transform_dirty"] = True
|
||||||
|
if value not in arm.pose.bones.keys():
|
||||||
|
prop["additional_transform_bone_id"] = -1
|
||||||
|
return
|
||||||
|
pose_bone = arm.pose.bones[value]
|
||||||
|
prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
|
||||||
|
|
||||||
|
def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context):
|
||||||
|
"""Update handler for IK toggle property"""
|
||||||
|
v = prop.mmd_ik_toggle
|
||||||
|
armature_object = prop.id_data
|
||||||
|
for b in armature_object.pose.bones:
|
||||||
|
for c in b.constraints:
|
||||||
|
if c.type == "IK" and c.subtarget == prop.name:
|
||||||
|
logger.debug('Updating IK constraint %s on bone %s', c.name, b.name)
|
||||||
|
c.influence = v
|
||||||
|
b_chain = b if c.use_tail else b.parent
|
||||||
|
for chain_bone in ([b_chain] + b_chain.parent_recursive)[:c.chain_count]:
|
||||||
|
limit_c = next((c for c in chain_bone.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None)
|
||||||
|
if limit_c:
|
||||||
|
limit_c.influence = v
|
||||||
|
|
||||||
|
class MMDBone(PropertyGroup):
|
||||||
|
"""Property group for MMD bone properties"""
|
||||||
|
name_j: StringProperty(
|
||||||
|
name="Name",
|
||||||
|
description="Japanese Name",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
name_e: StringProperty(
|
||||||
|
name="Name(Eng)",
|
||||||
|
description="English Name",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
bone_id: IntProperty(
|
||||||
|
name="Bone ID",
|
||||||
|
description="Unique ID for the reference of bone morph and rotate+/move+",
|
||||||
|
default=-1,
|
||||||
|
min=-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
transform_order: IntProperty(
|
||||||
|
name="Transform Order",
|
||||||
|
description="Deformation tier",
|
||||||
|
min=0,
|
||||||
|
max=100,
|
||||||
|
soft_max=7,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_controllable: BoolProperty(
|
||||||
|
name="Controllable",
|
||||||
|
description="Is controllable",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
transform_after_dynamics: BoolProperty(
|
||||||
|
name="After Dynamics",
|
||||||
|
description="After physics",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
enabled_fixed_axis: BoolProperty(
|
||||||
|
name="Fixed Axis",
|
||||||
|
description="Use fixed axis",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
fixed_axis: FloatVectorProperty(
|
||||||
|
name="Fixed Axis",
|
||||||
|
description="Fixed axis",
|
||||||
|
subtype="XYZ",
|
||||||
|
size=3,
|
||||||
|
precision=3,
|
||||||
|
step=0.1,
|
||||||
|
default=[0, 0, 0],
|
||||||
|
)
|
||||||
|
|
||||||
|
enabled_local_axes: BoolProperty(
|
||||||
|
name="Local Axes",
|
||||||
|
description="Use local axes",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
local_axis_x: FloatVectorProperty(
|
||||||
|
name="Local X-Axis",
|
||||||
|
description="Local x-axis",
|
||||||
|
subtype="XYZ",
|
||||||
|
size=3,
|
||||||
|
precision=3,
|
||||||
|
step=0.1,
|
||||||
|
default=[1, 0, 0],
|
||||||
|
)
|
||||||
|
|
||||||
|
local_axis_z: FloatVectorProperty(
|
||||||
|
name="Local Z-Axis",
|
||||||
|
description="Local z-axis",
|
||||||
|
subtype="XYZ",
|
||||||
|
size=3,
|
||||||
|
precision=3,
|
||||||
|
step=0.1,
|
||||||
|
default=[0, 0, 1],
|
||||||
|
)
|
||||||
|
|
||||||
|
is_tip: BoolProperty(
|
||||||
|
name="Tip Bone",
|
||||||
|
description="Is zero length bone",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
ik_rotation_constraint: FloatProperty(
|
||||||
|
name="IK Rotation Constraint",
|
||||||
|
description="The unit angle of IK",
|
||||||
|
subtype="ANGLE",
|
||||||
|
soft_min=0,
|
||||||
|
soft_max=4,
|
||||||
|
default=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
has_additional_rotation: BoolProperty(
|
||||||
|
name="Additional Rotation",
|
||||||
|
description="Additional rotation",
|
||||||
|
default=False,
|
||||||
|
update=_mmd_bone_update_additional_transform,
|
||||||
|
)
|
||||||
|
|
||||||
|
has_additional_location: BoolProperty(
|
||||||
|
name="Additional Location",
|
||||||
|
description="Additional location",
|
||||||
|
default=False,
|
||||||
|
update=_mmd_bone_update_additional_transform,
|
||||||
|
)
|
||||||
|
|
||||||
|
additional_transform_bone: StringProperty(
|
||||||
|
name="Additional Transform Bone",
|
||||||
|
description="Additional transform bone",
|
||||||
|
set=_mmd_bone_set_additional_transform_bone,
|
||||||
|
get=_mmd_bone_get_additional_transform_bone,
|
||||||
|
update=_mmd_bone_update_additional_transform,
|
||||||
|
)
|
||||||
|
|
||||||
|
additional_transform_bone_id: IntProperty(
|
||||||
|
name="Additional Transform Bone ID",
|
||||||
|
default=-1,
|
||||||
|
update=_mmd_bone_update_additional_transform,
|
||||||
|
)
|
||||||
|
|
||||||
|
additional_transform_influence: FloatProperty(
|
||||||
|
name="Additional Transform Influence",
|
||||||
|
description="Additional transform influence",
|
||||||
|
default=1,
|
||||||
|
soft_min=-1,
|
||||||
|
soft_max=1,
|
||||||
|
update=_mmd_bone_update_additional_transform_influence,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_additional_transform_dirty: BoolProperty(
|
||||||
|
name="",
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_id_unique(self):
|
||||||
|
"""Check if the bone ID is unique"""
|
||||||
|
return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
"""Register MMD bone properties"""
|
||||||
|
logger.info("Registering MMD bone properties")
|
||||||
|
bpy.utils.register_class(MMDBone)
|
||||||
|
|
||||||
|
# Add properties to PoseBone
|
||||||
|
bpy.types.PoseBone.mmd_bone = bpy.props.PointerProperty(type=MMDBone)
|
||||||
|
bpy.types.PoseBone.is_mmd_shadow_bone = bpy.props.BoolProperty(
|
||||||
|
name="is_mmd_shadow_bone",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
bpy.types.PoseBone.mmd_shadow_bone_type = bpy.props.StringProperty(
|
||||||
|
name="mmd_shadow_bone_type"
|
||||||
|
)
|
||||||
|
bpy.types.PoseBone.mmd_ik_toggle = bpy.props.BoolProperty(
|
||||||
|
name="MMD IK Toggle",
|
||||||
|
description="MMD IK toggle is used to import/export animation of IK on-off",
|
||||||
|
update=_pose_bone_update_mmd_ik_toggle,
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
"""Unregister MMD bone properties"""
|
||||||
|
logger.info("Unregistering MMD bone properties")
|
||||||
|
|
||||||
|
# Remove properties from PoseBone
|
||||||
|
del bpy.types.PoseBone.mmd_ik_toggle
|
||||||
|
del bpy.types.PoseBone.mmd_shadow_bone_type
|
||||||
|
del bpy.types.PoseBone.is_mmd_shadow_bone
|
||||||
|
del bpy.types.PoseBone.mmd_bone
|
||||||
|
|
||||||
|
bpy.utils.unregister_class(MMDBone)
|
||||||
@@ -0,0 +1,582 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 MMD Tools authors
|
||||||
|
# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit.
|
||||||
|
# All credit goes to the original authors.
|
||||||
|
# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed.
|
||||||
|
# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under.
|
||||||
|
# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/
|
||||||
|
|
||||||
|
"""Properties for MMD model root object"""
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from .. import utils
|
||||||
|
from ..bpyutils import FnContext
|
||||||
|
from ..core.material import FnMaterial
|
||||||
|
from ..core.model import FnModel
|
||||||
|
from ..core.sdef import FnSDEF
|
||||||
|
from . import patch_library_overridable
|
||||||
|
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
|
||||||
|
from .translations import MMDTranslation
|
||||||
|
|
||||||
|
|
||||||
|
def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
|
||||||
|
d = constraint.driver_add(path, index)
|
||||||
|
variables = d.driver.variables
|
||||||
|
for x in variables:
|
||||||
|
variables.remove(x)
|
||||||
|
return d.driver, variables
|
||||||
|
|
||||||
|
|
||||||
|
def __add_single_prop(variables, id_obj, data_path, prefix):
|
||||||
|
var = variables.new()
|
||||||
|
var.name = prefix + str(len(variables))
|
||||||
|
var.type = "SINGLE_PROP"
|
||||||
|
target = var.targets[0]
|
||||||
|
target.id_type = "OBJECT"
|
||||||
|
target.id = id_obj
|
||||||
|
target.data_path = data_path
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleUsePropertyDriver(self: "MMDRoot", _context):
|
||||||
|
root_object: bpy.types.Object = self.id_data
|
||||||
|
armature_object = FnModel.find_armature_object(root_object)
|
||||||
|
|
||||||
|
if armature_object is None:
|
||||||
|
ik_map = {}
|
||||||
|
else:
|
||||||
|
bones = armature_object.pose.bones
|
||||||
|
ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}
|
||||||
|
|
||||||
|
if self.use_property_driver:
|
||||||
|
for ik, (b, c) in ik_map.items():
|
||||||
|
driver, variables = __driver_variables(c, "influence")
|
||||||
|
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
|
||||||
|
b = b if c.use_tail else b.parent
|
||||||
|
for b in ([b] + b.parent_recursive)[: c.chain_count]:
|
||||||
|
c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None)
|
||||||
|
if c:
|
||||||
|
driver, variables = __driver_variables(c, "influence")
|
||||||
|
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
|
||||||
|
for i in FnModel.iterate_mesh_objects(root_object):
|
||||||
|
for prop_hide in ("hide_viewport", "hide_render"):
|
||||||
|
driver, variables = __driver_variables(i, prop_hide)
|
||||||
|
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
|
||||||
|
else:
|
||||||
|
for ik, (b, c) in ik_map.items():
|
||||||
|
c.driver_remove("influence")
|
||||||
|
b = b if c.use_tail else b.parent
|
||||||
|
for b in ([b] + b.parent_recursive)[: c.chain_count]:
|
||||||
|
c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None)
|
||||||
|
if c:
|
||||||
|
c.driver_remove("influence")
|
||||||
|
for i in FnModel.iterate_mesh_objects(root_object):
|
||||||
|
for prop_hide in ("hide_viewport", "hide_render"):
|
||||||
|
i.driver_remove(prop_hide)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Callback functions
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleUseToonTexture(self: "MMDRoot", _context):
|
||||||
|
use_toon = self.use_toon_texture
|
||||||
|
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||||
|
for m in i.data.materials:
|
||||||
|
if m:
|
||||||
|
FnMaterial(m).use_toon_texture(use_toon)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleUseSphereTexture(self: "MMDRoot", _context):
|
||||||
|
use_sphere = self.use_sphere_texture
|
||||||
|
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||||
|
for m in i.data.materials:
|
||||||
|
if m:
|
||||||
|
FnMaterial(m).use_sphere_texture(use_sphere, i)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleUseSDEF(self: "MMDRoot", _context):
|
||||||
|
mute_sdef = not self.use_sdef
|
||||||
|
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||||
|
FnSDEF.mute_sdef_set(i, mute_sdef)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
|
||||||
|
root = self.id_data
|
||||||
|
hide = not self.show_meshes
|
||||||
|
for i in FnModel.iterate_mesh_objects(self.id_data):
|
||||||
|
i.hide_set(hide)
|
||||||
|
i.hide_render = hide
|
||||||
|
if hide and context.active_object is None:
|
||||||
|
FnContext.set_active_object(context, root)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context):
|
||||||
|
root = self.id_data
|
||||||
|
hide = not self.show_rigid_bodies
|
||||||
|
for i in FnModel.iterate_rigid_body_objects(root):
|
||||||
|
i.hide_set(hide)
|
||||||
|
if hide and context.active_object is None:
|
||||||
|
FnContext.set_active_object(context, root)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleVisibilityOfJoints(self: "MMDRoot", context):
|
||||||
|
root_object = self.id_data
|
||||||
|
hide = not self.show_joints
|
||||||
|
for i in FnModel.iterate_joint_objects(root_object):
|
||||||
|
i.hide_set(hide)
|
||||||
|
if hide and context.active_object is None:
|
||||||
|
FnContext.set_active_object(context, root_object)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context):
|
||||||
|
root_object: bpy.types.Object = self.id_data
|
||||||
|
hide = not self.show_temporary_objects
|
||||||
|
with FnContext.temp_override_active_layer_collection(context, root_object):
|
||||||
|
for i in FnModel.iterate_temporary_objects(root_object):
|
||||||
|
i.hide_set(hide)
|
||||||
|
if hide and context.active_object is None:
|
||||||
|
FnContext.set_active_object(context, root_object)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context):
|
||||||
|
root = self.id_data
|
||||||
|
show_names = root.mmd_root.show_names_of_rigid_bodies
|
||||||
|
for i in FnModel.iterate_rigid_body_objects(root):
|
||||||
|
i.show_name = show_names
|
||||||
|
|
||||||
|
|
||||||
|
def _toggleShowNamesOfJoints(self: "MMDRoot", _context):
|
||||||
|
root = self.id_data
|
||||||
|
show_names = root.mmd_root.show_names_of_joints
|
||||||
|
for i in FnModel.iterate_joint_objects(root):
|
||||||
|
i.show_name = show_names
|
||||||
|
|
||||||
|
|
||||||
|
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool):
|
||||||
|
root = prop.id_data
|
||||||
|
arm = FnModel.find_armature_object(root)
|
||||||
|
if arm is None:
|
||||||
|
return
|
||||||
|
if not v and bpy.context.active_object == arm:
|
||||||
|
FnContext.set_active_object(bpy.context, root)
|
||||||
|
arm.hide_set(not v)
|
||||||
|
|
||||||
|
|
||||||
|
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"):
|
||||||
|
if prop.id_data.mmd_type != "ROOT":
|
||||||
|
return False
|
||||||
|
arm = FnModel.find_armature_object(prop.id_data)
|
||||||
|
return arm and not arm.hide_get()
|
||||||
|
|
||||||
|
|
||||||
|
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int):
|
||||||
|
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||||
|
if FnModel.is_rigid_body_object(obj):
|
||||||
|
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||||
|
prop["active_rigidbody_object_index"] = v
|
||||||
|
|
||||||
|
|
||||||
|
def _getActiveRigidbodyObject(prop: "MMDRoot"):
|
||||||
|
context = bpy.context
|
||||||
|
active_obj = FnContext.get_active_object(context)
|
||||||
|
if FnModel.is_rigid_body_object(active_obj):
|
||||||
|
prop["active_rigidbody_object_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
|
||||||
|
return prop.get("active_rigidbody_object_index", 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _setActiveJointObject(prop: "MMDRoot", v: int):
|
||||||
|
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||||
|
if FnModel.is_joint_object(obj):
|
||||||
|
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||||
|
prop["active_joint_object_index"] = v
|
||||||
|
|
||||||
|
|
||||||
|
def _getActiveJointObject(prop: "MMDRoot"):
|
||||||
|
context = bpy.context
|
||||||
|
active_obj = FnContext.get_active_object(context)
|
||||||
|
if FnModel.is_joint_object(active_obj):
|
||||||
|
prop["active_joint_object_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
|
||||||
|
return prop.get("active_joint_object_index", 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _setActiveMorph(prop: "MMDRoot", v: bool):
|
||||||
|
if "active_morph_indices" not in prop:
|
||||||
|
prop["active_morph_indices"] = [0] * 5
|
||||||
|
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v
|
||||||
|
|
||||||
|
|
||||||
|
def _getActiveMorph(prop: "MMDRoot"):
|
||||||
|
if "active_morph_indices" in prop:
|
||||||
|
return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _setActiveMeshObject(prop: "MMDRoot", v: int):
|
||||||
|
obj = FnContext.get_scene_objects(bpy.context)[v]
|
||||||
|
if FnModel.is_mesh_object(obj):
|
||||||
|
FnContext.set_active_and_select_single_object(bpy.context, obj)
|
||||||
|
prop["active_mesh_index"] = v
|
||||||
|
|
||||||
|
|
||||||
|
def _getActiveMeshObject(prop: "MMDRoot"):
|
||||||
|
context = bpy.context
|
||||||
|
active_obj = FnContext.get_active_object(context)
|
||||||
|
if FnModel.is_mesh_object(active_obj):
|
||||||
|
prop["active_mesh_index"] = FnContext.get_scene_objects(context).find(active_obj.name)
|
||||||
|
return prop.get("active_mesh_index", -1)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# Property classes
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
|
||||||
|
class MMDDisplayItem(bpy.types.PropertyGroup):
|
||||||
|
"""PMX 表示項目(表示枠内の1項目)"""
|
||||||
|
|
||||||
|
type: bpy.props.EnumProperty(
|
||||||
|
name="Type",
|
||||||
|
description="Select item type",
|
||||||
|
items=[
|
||||||
|
("BONE", "Bone", "", 1),
|
||||||
|
("MORPH", "Morph", "", 2),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
morph_type: bpy.props.EnumProperty(
|
||||||
|
name="Morph Type",
|
||||||
|
description="Select morph type",
|
||||||
|
items=[
|
||||||
|
("material_morphs", "Material", "Material Morphs", 0),
|
||||||
|
("uv_morphs", "UV", "UV Morphs", 1),
|
||||||
|
("bone_morphs", "Bone", "Bone Morphs", 2),
|
||||||
|
("vertex_morphs", "Vertex", "Vertex Morphs", 3),
|
||||||
|
("group_morphs", "Group", "Group Morphs", 4),
|
||||||
|
],
|
||||||
|
default="vertex_morphs",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MMDDisplayItemFrame(bpy.types.PropertyGroup):
|
||||||
|
"""PMX 表示枠
|
||||||
|
|
||||||
|
PMXファイル内では表示枠がリストで格納されています。
|
||||||
|
"""
|
||||||
|
|
||||||
|
name_e: bpy.props.StringProperty(
|
||||||
|
name="Name(Eng)",
|
||||||
|
description="English Name",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 特殊枠フラグ
|
||||||
|
# 特殊枠はファイル仕様上の固定枠(削除、リネーム不可)
|
||||||
|
is_special: bpy.props.BoolProperty(
|
||||||
|
name="Special",
|
||||||
|
description="Is special",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 表示項目のリスト
|
||||||
|
data: bpy.props.CollectionProperty(
|
||||||
|
name="Display Items",
|
||||||
|
type=MMDDisplayItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 現在アクティブな項目のインデックス
|
||||||
|
active_item: bpy.props.IntProperty(
|
||||||
|
name="Active Display Item",
|
||||||
|
min=0,
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MMDRoot(bpy.types.PropertyGroup):
|
||||||
|
"""MMDモデルデータ
|
||||||
|
|
||||||
|
モデルルート用に作成されたEmtpyオブジェクトで使用します
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: bpy.props.StringProperty(
|
||||||
|
name="Name",
|
||||||
|
description="The name of the MMD model",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
name_e: bpy.props.StringProperty(
|
||||||
|
name="Name (English)",
|
||||||
|
description="The english name of the MMD model",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
comment_text: bpy.props.StringProperty(
|
||||||
|
name="Comment",
|
||||||
|
description="The text datablock of the comment",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
comment_e_text: bpy.props.StringProperty(
|
||||||
|
name="Comment (English)",
|
||||||
|
description="The text datablock of the english comment",
|
||||||
|
default="",
|
||||||
|
)
|
||||||
|
|
||||||
|
ik_loop_factor: bpy.props.IntProperty(
|
||||||
|
name="MMD IK Loop Factor",
|
||||||
|
description="Scaling factor of MMD IK loop",
|
||||||
|
min=1,
|
||||||
|
soft_max=10,
|
||||||
|
max=100,
|
||||||
|
default=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: Replace to driver for NLA
|
||||||
|
show_meshes: bpy.props.BoolProperty(
|
||||||
|
name="Show Meshes",
|
||||||
|
description="Show all meshes of the MMD model",
|
||||||
|
# get=_show_meshes_get,
|
||||||
|
# set=_show_meshes_set,
|
||||||
|
update=_toggleVisibilityOfMeshes,
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
show_rigid_bodies: bpy.props.BoolProperty(
|
||||||
|
name="Show Rigid Bodies",
|
||||||
|
description="Show all rigid bodies of the MMD model",
|
||||||
|
update=_toggleVisibilityOfRigidBodies,
|
||||||
|
)
|
||||||
|
|
||||||
|
show_joints: bpy.props.BoolProperty(
|
||||||
|
name="Show Joints",
|
||||||
|
description="Show all joints of the MMD model",
|
||||||
|
update=_toggleVisibilityOfJoints,
|
||||||
|
)
|
||||||
|
|
||||||
|
show_temporary_objects: bpy.props.BoolProperty(
|
||||||
|
name="Show Temps",
|
||||||
|
description="Show all temporary objects of the MMD model",
|
||||||
|
update=_toggleVisibilityOfTemporaryObjects,
|
||||||
|
)
|
||||||
|
|
||||||
|
show_armature: bpy.props.BoolProperty(
|
||||||
|
name="Show Armature",
|
||||||
|
description="Show the armature object of the MMD model",
|
||||||
|
get=_getVisibilityOfMMDRigArmature,
|
||||||
|
set=_setVisibilityOfMMDRigArmature,
|
||||||
|
)
|
||||||
|
|
||||||
|
show_names_of_rigid_bodies: bpy.props.BoolProperty(
|
||||||
|
name="Show Rigid Body Names",
|
||||||
|
description="Show rigid body names",
|
||||||
|
update=_toggleShowNamesOfRigidBodies,
|
||||||
|
)
|
||||||
|
|
||||||
|
show_names_of_joints: bpy.props.BoolProperty(
|
||||||
|
name="Show Joint Names",
|
||||||
|
description="Show joint names",
|
||||||
|
update=_toggleShowNamesOfJoints,
|
||||||
|
)
|
||||||
|
|
||||||
|
use_toon_texture: bpy.props.BoolProperty(
|
||||||
|
name="Use Toon Texture",
|
||||||
|
description="Use toon texture",
|
||||||
|
update=_toggleUseToonTexture,
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
use_sphere_texture: bpy.props.BoolProperty(
|
||||||
|
name="Use Sphere Texture",
|
||||||
|
description="Use sphere texture",
|
||||||
|
update=_toggleUseSphereTexture,
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
use_sdef: bpy.props.BoolProperty(
|
||||||
|
name="Use SDEF",
|
||||||
|
description="Use SDEF",
|
||||||
|
update=_toggleUseSDEF,
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
use_property_driver: bpy.props.BoolProperty(
|
||||||
|
name="Use Property Driver",
|
||||||
|
description="Setup drivers for MMD property animation (Visibility and IK toggles)",
|
||||||
|
update=_toggleUsePropertyDriver,
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
is_built: bpy.props.BoolProperty(
|
||||||
|
name="Is Built",
|
||||||
|
)
|
||||||
|
|
||||||
|
active_rigidbody_index: bpy.props.IntProperty(
|
||||||
|
name="Active Rigidbody Index",
|
||||||
|
min=0,
|
||||||
|
get=_getActiveRigidbodyObject,
|
||||||
|
set=_setActiveRigidbodyObject,
|
||||||
|
)
|
||||||
|
|
||||||
|
active_joint_index: bpy.props.IntProperty(
|
||||||
|
name="Active Joint Index",
|
||||||
|
min=0,
|
||||||
|
get=_getActiveJointObject,
|
||||||
|
set=_setActiveJointObject,
|
||||||
|
)
|
||||||
|
|
||||||
|
# *************************
|
||||||
|
# Display Items
|
||||||
|
# *************************
|
||||||
|
display_item_frames: bpy.props.CollectionProperty(
|
||||||
|
name="Display Frames",
|
||||||
|
type=MMDDisplayItemFrame,
|
||||||
|
)
|
||||||
|
|
||||||
|
active_display_item_frame: bpy.props.IntProperty(
|
||||||
|
name="Active Display Item Frame",
|
||||||
|
min=0,
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# *************************
|
||||||
|
# Morph
|
||||||
|
# *************************
|
||||||
|
material_morphs: bpy.props.CollectionProperty(
|
||||||
|
name="Material Morphs",
|
||||||
|
type=MaterialMorph,
|
||||||
|
)
|
||||||
|
uv_morphs: bpy.props.CollectionProperty(
|
||||||
|
name="UV Morphs",
|
||||||
|
type=UVMorph,
|
||||||
|
)
|
||||||
|
bone_morphs: bpy.props.CollectionProperty(
|
||||||
|
name="Bone Morphs",
|
||||||
|
type=BoneMorph,
|
||||||
|
)
|
||||||
|
vertex_morphs: bpy.props.CollectionProperty(name="Vertex Morphs", type=VertexMorph)
|
||||||
|
group_morphs: bpy.props.CollectionProperty(
|
||||||
|
name="Group Morphs",
|
||||||
|
type=GroupMorph,
|
||||||
|
)
|
||||||
|
active_morph_type: bpy.props.EnumProperty(
|
||||||
|
name="Active Morph Type",
|
||||||
|
description="Select current morph type",
|
||||||
|
items=[
|
||||||
|
("material_morphs", "Material", "Material Morphs", 0),
|
||||||
|
("uv_morphs", "UV", "UV Morphs", 1),
|
||||||
|
("bone_morphs", "Bone", "Bone Morphs", 2),
|
||||||
|
("vertex_morphs", "Vertex", "Vertex Morphs", 3),
|
||||||
|
("group_morphs", "Group", "Group Morphs", 4),
|
||||||
|
],
|
||||||
|
default="vertex_morphs",
|
||||||
|
)
|
||||||
|
active_morph: bpy.props.IntProperty(
|
||||||
|
name="Active Morph",
|
||||||
|
min=0,
|
||||||
|
set=_setActiveMorph,
|
||||||
|
get=_getActiveMorph,
|
||||||
|
)
|
||||||
|
morph_panel_show_settings: bpy.props.BoolProperty(
|
||||||
|
name="Morph Panel Show Settings",
|
||||||
|
description="Show Morph Settings",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
active_mesh_index: bpy.props.IntProperty(
|
||||||
|
name="Active Mesh",
|
||||||
|
min=0,
|
||||||
|
set=_setActiveMeshObject,
|
||||||
|
get=_getActiveMeshObject,
|
||||||
|
)
|
||||||
|
|
||||||
|
# *************************
|
||||||
|
# Translation
|
||||||
|
# *************************
|
||||||
|
translation: bpy.props.PointerProperty(
|
||||||
|
name="Translation",
|
||||||
|
type=MMDTranslation,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_select(prop: bpy.types.Object) -> bool:
|
||||||
|
# TODO: Object.select is deprecated since v4.0.0, use Object.select_get() method instead
|
||||||
|
# utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead")
|
||||||
|
return prop.select_get()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __set_select(prop: bpy.types.Object, value: bool) -> None:
|
||||||
|
# TODO: Object.select is deprecated since v4.0.0, use Object.select_set() method instead
|
||||||
|
# utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead")
|
||||||
|
prop.select_set(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __get_hide(prop: bpy.types.Object) -> bool:
|
||||||
|
# TODO: Object.hide is deprecated since v4.0.0, use Object.hide_get() method instead
|
||||||
|
# utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead")
|
||||||
|
return prop.hide_get()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def __set_hide(prop: bpy.types.Object, value: bool) -> None:
|
||||||
|
# TODO: Object.hide is deprecated since v4.0.0, use Object.hide_set() method instead
|
||||||
|
# utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead")
|
||||||
|
prop.hide_set(value)
|
||||||
|
if prop.hide_viewport != value:
|
||||||
|
prop.hide_viewport = value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def register():
|
||||||
|
bpy.types.Object.mmd_type = patch_library_overridable(
|
||||||
|
bpy.props.EnumProperty(
|
||||||
|
name="Type",
|
||||||
|
description="Internal MMD type of this object (DO NOT CHANGE IT DIRECTLY)",
|
||||||
|
default="NONE",
|
||||||
|
items=[
|
||||||
|
("NONE", "None", "", 1),
|
||||||
|
("ROOT", "Root", "", 2),
|
||||||
|
("RIGID_GRP_OBJ", "Rigid Body Grp Empty", "", 3),
|
||||||
|
("JOINT_GRP_OBJ", "Joint Grp Empty", "", 4),
|
||||||
|
("TEMPORARY_GRP_OBJ", "Temporary Grp Empty", "", 5),
|
||||||
|
("PLACEHOLDER", "Place Holder", "", 6),
|
||||||
|
("CAMERA", "Camera", "", 21),
|
||||||
|
("JOINT", "Joint", "", 22),
|
||||||
|
("RIGID_BODY", "Rigid body", "", 23),
|
||||||
|
("LIGHT", "Light", "", 24),
|
||||||
|
("TRACK_TARGET", "Track Target", "", 51),
|
||||||
|
("NON_COLLISION_CONSTRAINT", "Non Collision Constraint", "", 52),
|
||||||
|
("SPRING_CONSTRAINT", "Spring Constraint", "", 53),
|
||||||
|
("SPRING_GOAL", "Spring Goal", "", 54),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bpy.types.Object.mmd_root = patch_library_overridable(bpy.props.PointerProperty(type=MMDRoot))
|
||||||
|
|
||||||
|
bpy.types.Object.select = patch_library_overridable(
|
||||||
|
bpy.props.BoolProperty(
|
||||||
|
get=MMDRoot.__get_select,
|
||||||
|
set=MMDRoot.__set_select,
|
||||||
|
options={
|
||||||
|
"SKIP_SAVE",
|
||||||
|
"ANIMATABLE",
|
||||||
|
"LIBRARY_EDITABLE",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bpy.types.Object.hide = patch_library_overridable(
|
||||||
|
bpy.props.BoolProperty(
|
||||||
|
get=MMDRoot.__get_hide,
|
||||||
|
set=MMDRoot.__set_hide,
|
||||||
|
options={
|
||||||
|
"SKIP_SAVE",
|
||||||
|
"ANIMATABLE",
|
||||||
|
"LIBRARY_EDITABLE",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def unregister():
|
||||||
|
del bpy.types.Object.hide
|
||||||
|
del bpy.types.Object.select
|
||||||
|
del bpy.types.Object.mmd_root
|
||||||
|
del bpy.types.Object.mmd_type
|
||||||
Reference in New Issue
Block a user