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:
@@ -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