From 3414ad89178bd6c2667c484795087b8a594f8325 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 3 Apr 2025 15:39:03 +0100 Subject: [PATCH 1/6] 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. --- core/importers/pmx/__ini__.py | 0 core/importers/pmx/importer.py | 1075 ++++++++++++++++++++++++++++++ core/mmd/__init__.py | 0 core/mmd/bone.py | 587 ++++++++++++++++ core/mmd/core/bpyutils.py | 533 +++++++++++++++ core/mmd/core/utils.py | 296 ++++++++ core/mmd/material.py | 697 +++++++++++++++++++ core/mmd/properties/__init__.py | 0 core/mmd/properties/pose_bone.py | 250 +++++++ core/mmd/properties/root.py | 582 ++++++++++++++++ 10 files changed, 4020 insertions(+) create mode 100644 core/importers/pmx/__ini__.py create mode 100644 core/importers/pmx/importer.py create mode 100644 core/mmd/__init__.py create mode 100644 core/mmd/bone.py create mode 100644 core/mmd/core/bpyutils.py create mode 100644 core/mmd/core/utils.py create mode 100644 core/mmd/material.py create mode 100644 core/mmd/properties/__init__.py create mode 100644 core/mmd/properties/pose_bone.py create mode 100644 core/mmd/properties/root.py diff --git a/core/importers/pmx/__ini__.py b/core/importers/pmx/__ini__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/importers/pmx/importer.py b/core/importers/pmx/importer.py new file mode 100644 index 0000000..3f0a1f2 --- /dev/null +++ b/core/importers/pmx/importer.py @@ -0,0 +1,1075 @@ +# -*- 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 +import collections +import os +import time +import typing +from typing import TYPE_CHECKING, List, Optional, Dict, Set, Tuple, Any, Union +from mathutils import Matrix, Vector + +from bpy.types import Context, Object + +from ...logging_setup import logger +from ...common import ProgressTracker +from ...translations import t + +from ...mmd.core import bpyutils, utils +from ...mmd.core.bpyutils import FnContext +from .. import pmx +from ..bone import FnBone +from ..material import FnMaterial +from ..model import FnModel, Model +from ..morph import FnMorph +from ..rigid_body import FnRigidBody +from ..vmd.importer import BoneConverter +from ...operators.misc import MoveObject + +if TYPE_CHECKING: + from ...mmd.properties.pose_bone import MMDBone + from ...mmd.properties.root import MMDRoot + + +class PMXImporter: + """PMX model importer for Avatar Toolkit""" + + CATEGORIES = { + 0: "SYSTEM", + 1: "EYEBROW", + 2: "EYE", + 3: "MOUTH", + } + + MORPH_TYPES = { + 0: "group_morphs", + 1: "vertex_morphs", + 2: "bone_morphs", + 3: "uv_morphs", + 4: "uv_morphs", + 5: "uv_morphs", + 6: "uv_morphs", + 7: "uv_morphs", + 8: "material_morphs", + } + + def __init__(self): + self.__model = None + self.__targetContext = FnContext.ensure_context() + + self.__scale = None + + self.__root: Optional[bpy.types.Object] = None + self.__armObj: Optional[bpy.types.Object] = None + self.__meshObj: Optional[bpy.types.Object] = None + + self.__vertexGroupTable = None + self.__textureTable = None + self.__rigidTable = None + + self.__boneTable = [] + self.__materialTable = [] + self.__imageTable = {} + + self.__sdefVertices = {} # pmx vertices + self.__blender_ik_links = set() + self.__vertex_map = None + + self.__materialFaceCountTable = None + + @staticmethod + def __safe_name(name: str, max_length: int = 59) -> str: + """Create a safe name that won't exceed Blender's name length limits""" + return str(bytes(name, "utf8")[:max_length], "utf8", errors="replace") + + @staticmethod + def flipUV_V(uv: Tuple[float, float]) -> Tuple[float, float]: + """Flip the V coordinate of UV mapping""" + u, v = uv + return u, 1.0 - v + + def __createObjects(self) -> None: + """Create main objects and link them to scene.""" + pmxModel = self.__model + obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54) + self.__rig = Model.create(pmxModel.name, pmxModel.name_e, self.__scale, obj_name) + root = self.__rig.rootObject() + mmd_root: MMDRoot = root.mmd_root + self.__root = root + self.__armObj = self.__rig.armature() + + root["import_folder"] = os.path.dirname(pmxModel.filepath) + + txt = bpy.data.texts.new(obj_name) + txt.from_string(pmxModel.comment.replace("\r", "")) + mmd_root.comment_text = txt.name + txt = bpy.data.texts.new(obj_name + "_e") + txt.from_string(pmxModel.comment_e.replace("\r", "")) + mmd_root.comment_e_text = txt.name + + def __createMeshObject(self) -> None: + """Create a mesh object for the model""" + model_name = self.__root.name + self.__meshObj = bpy.data.objects.new(name=model_name + "_mesh", object_data=bpy.data.meshes.new(name=model_name)) + self.__meshObj.parent = self.__armObj + FnContext.link_object(self.__targetContext, self.__meshObj) + + def __createBasisShapeKey(self) -> None: + """Create a basis shape key if it doesn't exist""" + if self.__meshObj.data.shape_keys: + assert len(self.__meshObj.data.vertices) > 0 + assert len(self.__meshObj.data.shape_keys.key_blocks) > 1 + return + FnContext.set_active_object(self.__targetContext, self.__meshObj) + bpy.ops.object.shape_key_add() + + def __importVertexGroup(self) -> None: + """Import vertex groups from bones""" + vgroups = self.__meshObj.vertex_groups + self.__vertexGroupTable = [vgroups.new(name=i.name) for i in self.__model.bones] or [vgroups.new(name="NO BONES")] + + def __importVertices(self) -> None: + """Import vertices with weights and other properties""" + self.__importVertexGroup() + + pmxModel = self.__model + pmx_vertices = pmxModel.vertices + vertex_count = len(pmx_vertices) + vertex_map = self.__vertex_map + if vertex_map: + indices = collections.OrderedDict(vertex_map).keys() + pmx_vertices = tuple(pmxModel.vertices[x] for x in indices) + vertex_count = len(indices) + if vertex_count < 1: + return + + mesh: bpy.types.Mesh = self.__meshObj.data + mesh.vertices.add(count=vertex_count) + mesh.vertices.foreach_set("co", tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * self.__scale))) + + vertex_group_table = self.__vertexGroupTable + vg_edge_scale = self.__meshObj.vertex_groups.new(name="mmd_edge_scale") + vg_vertex_order = self.__meshObj.vertex_groups.new(name="mmd_vertex_order") + for i, pv in enumerate(pmx_vertices): + pv_bones, pv_weights, idx = pv.weight.bones, pv.weight.weights, (i,) + + vg_edge_scale.add(index=idx, weight=pv.edge_scale, type="REPLACE") + vg_vertex_order.add(index=idx, weight=i / vertex_count, type="REPLACE") + + if isinstance(pv_weights, pmx.BoneWeightSDEF): + if pv_bones[0] > pv_bones[1]: + pv_bones.reverse() + pv_weights.weight = 1.0 - pv_weights.weight + pv_weights.r0, pv_weights.r1 = pv_weights.r1, pv_weights.r0 + vertex_group_table[pv_bones[0]].add(index=idx, weight=pv_weights.weight, type="ADD") + vertex_group_table[pv_bones[1]].add(index=idx, weight=1.0 - pv_weights.weight, type="ADD") + self.__sdefVertices[i] = pv + elif len(pv_bones) == 1: + bone_index = pv_bones[0] + if bone_index >= 0: + vertex_group_table[bone_index].add(index=idx, weight=1.0, type="ADD") + elif len(pv_bones) == 2: + vertex_group_table[pv_bones[0]].add(index=idx, weight=pv_weights[0], type="ADD") + vertex_group_table[pv_bones[1]].add(index=idx, weight=1.0 - pv_weights[0], type="ADD") + elif len(pv_bones) == 4: + for bone, weight in zip(pv_bones, pv_weights): + vertex_group_table[bone].add(index=idx, weight=weight, type="ADD") + else: + raise Exception("Unknown bone weight type.") + + vg_edge_scale.lock_weight = True + vg_vertex_order.lock_weight = True + + def __storeVerticesSDEF(self) -> None: + """Store SDEF vertex data for smooth deformation""" + if len(self.__sdefVertices) < 1: + return + + self.__createBasisShapeKey() + sdefC = self.__meshObj.shape_key_add(name="mmd_sdef_c") + sdefR0 = self.__meshObj.shape_key_add(name="mmd_sdef_r0") + sdefR1 = self.__meshObj.shape_key_add(name="mmd_sdef_r1") + for i, pv in self.__sdefVertices.items(): + w = pv.weight.weights + sdefC.data[i].co = Vector(w.c).xzy * self.__scale + sdefR0.data[i].co = Vector(w.r0).xzy * self.__scale + sdefR1.data[i].co = Vector(w.r1).xzy * self.__scale + logger.info("Stored %d SDEF vertices", len(self.__sdefVertices)) + + def __importTextures(self) -> None: + """Import textures from the PMX model""" + pmxModel = self.__model + + self.__textureTable = [] + for i in pmxModel.textures: + self.__textureTable.append(bpy.path.resolve_ncase(path=i.path)) + + def __createEditBones(self, obj: Object, pmx_bones: List[Any]) -> Tuple[List[str], List[str]]: + """Create EditBones from pmx file data. + @return the list of bone names which can be accessed by the bone index of pmx data. + """ + editBoneTable = [] + nameTable = [] + specialTipBones = [] + dependency_cycle_ik_bones = [] + + from math import isfinite + + def _VectorXZY(v: List[float]) -> Vector: + return Vector(v).xzy if all(isfinite(n) for n in v) else Vector((0, 0, 0)) + + with bpyutils.edit_object(obj) as data: + for i in pmx_bones: + bone = data.edit_bones.new(name=i.name) + loc = _VectorXZY(i.location) * self.__scale + bone.head = loc + editBoneTable.append(bone) + nameTable.append(bone.name) + + for i, (b_bone, m_bone) in enumerate(zip(editBoneTable, pmx_bones)): + if m_bone.parent != -1: + if i not in dependency_cycle_ik_bones: + b_bone.parent = editBoneTable[m_bone.parent] + else: + b_bone.parent = editBoneTable[m_bone.parent].parent + + for b_bone, m_bone in zip(editBoneTable, pmx_bones): + if isinstance(m_bone.displayConnection, int): + if m_bone.displayConnection != -1: + b_bone.tail = editBoneTable[m_bone.displayConnection].head + else: + b_bone.tail = b_bone.head + else: + loc = _VectorXZY(m_bone.displayConnection) * self.__scale + b_bone.tail = b_bone.head + loc + + for b_bone, m_bone in zip(editBoneTable, pmx_bones): + if m_bone.isIK and m_bone.target != -1: + logger.debug("Checking IK links of %s", b_bone.name) + b_target = editBoneTable[m_bone.target] + for i in range(len(m_bone.ik_links)): + b_bone_link = editBoneTable[m_bone.ik_links[i].target] + if self.__fix_IK_links or b_bone_link.length < 0.001: + b_bone_tail = b_target if i == 0 else editBoneTable[m_bone.ik_links[i - 1].target] + loc = b_bone_tail.head - b_bone_link.head + if loc.length < 0.001: + logger.warning("Unsolved IK link %s", b_bone_link.name) + elif b_bone_tail.parent != b_bone_link: + logger.warning("Skipped IK link %s", b_bone_link.name) + elif (b_bone_link.tail - b_bone_tail.head).length > 1e-4: + logger.debug("Fix IK link %s", b_bone_link.name) + b_bone_link.tail = b_bone_link.head + loc + + for b_bone, m_bone in zip(editBoneTable, pmx_bones): + # Set the length of too short bones to 1 because Blender delete them. + if b_bone.length < 0.001: + if not self.__apply_bone_fixed_axis and m_bone.axis is not None: + fixed_axis = Vector(m_bone.axis) + if fixed_axis.length: + b_bone.tail = b_bone.head + fixed_axis.xzy.normalized() * self.__scale + else: + b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale + else: + b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale + if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]: + logger.debug("Special tip bone %s, display %s", b_bone.name, str(m_bone.displayConnection)) + specialTipBones.append(b_bone.name) + + for b_bone, m_bone in zip(editBoneTable, pmx_bones): + if m_bone.localCoordinate is not None: + FnBone.update_bone_roll(b_bone, m_bone.localCoordinate.x_axis, m_bone.localCoordinate.z_axis) + elif FnBone.has_auto_local_axis(m_bone.name): + FnBone.update_auto_bone_roll(b_bone) + + for b_bone, m_bone in zip(editBoneTable, pmx_bones): + if isinstance(m_bone.displayConnection, int) and m_bone.displayConnection >= 0: + t = editBoneTable[m_bone.displayConnection] + if t.parent is None or t.parent != b_bone: + continue + if pmx_bones[m_bone.displayConnection].isMovable: + continue + if (b_bone.tail - t.head).length > 1e-4: + continue + if not m_bone.isMovable: + continue + logger.warning("Connected: %s (%d)-> %s", b_bone.name, len(b_bone.children), t.name) + t.use_connect = True + + return nameTable, specialTipBones + + def __sortPoseBonesByBoneIndex(self, pose_bones: List[bpy.types.PoseBone], bone_names: List[str]) -> List[bpy.types.PoseBone]: + """Sort pose bones by their bone index in the PMX file""" + r: List[bpy.types.PoseBone] = [] + for i in bone_names: + r.append(pose_bones[i]) + return r + + @staticmethod + def convertIKLimitAngles(min_angle: List[float], max_angle: List[float], bone_matrix: Matrix, invert: bool = False) -> Tuple[Vector, Vector]: + """Convert IK limit angles to Blender's coordinate system""" + mat = bone_matrix.to_3x3() * -1 + mat[1], mat[2] = mat[2].copy(), mat[1].copy() + mat.transpose() + if invert: + mat.invert() + + # align matrix to global axes + m = Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + i_set, j_set = [0, 1, 2], [0, 1, 2] + for _ in range(3): + ii, jj = i_set[0], j_set[0] + for i in i_set: + for j in j_set: + if abs(mat[i][j]) > abs(mat[ii][jj]): + ii, jj = i, j + i_set.remove(ii) + j_set.remove(jj) + m[ii][jj] = -1 if mat[ii][jj] < 0 else 1 + + new_min_angle = m @ Vector(min_angle) + new_max_angle = m @ Vector(max_angle) + for i in range(3): + if new_min_angle[i] > new_max_angle[i]: + new_min_angle[i], new_max_angle[i] = new_max_angle[i], new_min_angle[i] + return new_min_angle, new_max_angle + + def __applyIk(self, index: int, pmx_bone: Any, pose_bones: List[bpy.types.PoseBone]) -> None: + """Create an IK bone constraint + If the IK bone and the target bone is separated, a dummy IK target bone is created as a child of the IK bone. + @param index the bone index + @param pmx_bone pmx.Bone + @param pose_bones the list of PoseBones sorted by the bone index + """ + ik_bone = pose_bones[index] + ik_target = pose_bones[pmx_bone.target] + ik_constraint_bone = ik_target.parent + is_valid_ik = False + if len(pmx_bone.ik_links) > 0: + ik_constraint_bone_real = pose_bones[pmx_bone.ik_links[0].target] + if ik_constraint_bone_real == ik_target: + if len(pmx_bone.ik_links) > 1: + ik_constraint_bone_real = pose_bones[pmx_bone.ik_links[1].target] + del pmx_bone.ik_links[0] + logger.warning("Fix IK settings of IK bone (%s)", ik_bone.name) + is_valid_ik = ik_constraint_bone == ik_constraint_bone_real + if not is_valid_ik: + ik_constraint_bone = ik_constraint_bone_real + logger.warning("IK bone (%s) warning: IK target (%s) is not a child of IK link 0 (%s)", + ik_bone.name, ik_target.name, ik_constraint_bone.name) + elif any(pose_bones[i.target].parent != pose_bones[j.target] for i, j in zip(pmx_bone.ik_links, pmx_bone.ik_links[1:])): + logger.warning("Invalid IK bone (%s): IK chain does not follow parent-child relationship", ik_bone.name) + return + if ik_constraint_bone is None or len(pmx_bone.ik_links) < 1: + logger.warning("Invalid IK bone (%s)", ik_bone.name) + return + + c = ik_target.constraints.new(type="DAMPED_TRACK") + c.name = "mmd_ik_target_override" + c.mute = True + c.influence = 0 + c.target = self.__armObj + c.subtarget = ik_constraint_bone.name + if not is_valid_ik or next((c for c in ik_constraint_bone.constraints if c.type == "IK" and c.is_valid), None): + c.name = "mmd_ik_target_custom" + c.subtarget = ik_bone.name # point to IK control bone + ik_bone.mmd_bone.ik_rotation_constraint = pmx_bone.rotationConstraint + use_custom_ik = True + else: + ik_constraint_bone.mmd_bone.ik_rotation_constraint = pmx_bone.rotationConstraint + use_custom_ik = False + + ikConst = self.__rig.create_ik_constraint(ik_constraint_bone, ik_bone) + ikConst.iterations = pmx_bone.loopCount + ikConst.chain_count = len(pmx_bone.ik_links) + if not is_valid_ik: + ikConst.pole_target = self.__armObj # make it an incomplete/invalid setting + for idx, i in enumerate(pmx_bone.ik_links): + if use_custom_ik or i.target in self.__blender_ik_links: + c = ik_bone.constraints.new(type="LIMIT_ROTATION") + c.mute = True + c.influence = 0 + c.name = "mmd_ik_limit_custom%d" % idx + use_limits = c.use_limit_x = c.use_limit_y = c.use_limit_z = i.maximumAngle is not None + if use_limits: + minimum, maximum = self.convertIKLimitAngles(i.minimumAngle, i.maximumAngle, pose_bones[i.target].bone.matrix_local) + c.max_x, c.max_y, c.max_z = maximum + c.min_x, c.min_y, c.min_z = minimum + continue + self.__blender_ik_links.add(i.target) + if i.maximumAngle is not None: + bone = pose_bones[i.target] + minimum, maximum = self.convertIKLimitAngles(i.minimumAngle, i.maximumAngle, bone.bone.matrix_local) + + bone.use_ik_limit_x = True + bone.use_ik_limit_y = True + bone.use_ik_limit_z = True + bone.ik_max_x, bone.ik_max_y, bone.ik_max_z = maximum + bone.ik_min_x, bone.ik_min_y, bone.ik_min_z = minimum + + c = bone.constraints.new(type="LIMIT_ROTATION") + c.mute = not is_valid_ik + c.name = "mmd_ik_limit_override" + c.owner_space = "LOCAL" + c.max_x, c.max_y, c.max_z = maximum + c.min_x, c.min_y, c.min_z = minimum + c.use_limit_x = bone.ik_max_x != c.max_x or bone.ik_min_x != c.min_x + c.use_limit_y = bone.ik_max_y != c.max_y or bone.ik_min_y != c.min_y + c.use_limit_z = bone.ik_max_z != c.max_z or bone.ik_min_z != c.min_z + + def __importBones(self) -> None: + """Import bones from the PMX model""" + pmxModel = self.__model + + boneNameTable, specialTipBones = self.__createEditBones(self.__armObj, pmxModel.bones) + pose_bones = self.__sortPoseBonesByBoneIndex(self.__armObj.pose.bones, boneNameTable) + self.__boneTable = pose_bones + for i, pmx_bone in sorted(enumerate(pmxModel.bones), key=lambda x: x[1].transform_order): + b_bone = pose_bones[i] + mmd_bone: MMDBone = b_bone.mmd_bone + mmd_bone.name_j = b_bone.name # pmx_bone.name + mmd_bone.name_e = pmx_bone.name_e + mmd_bone.is_controllable = pmx_bone.isControllable + mmd_bone.transform_order = pmx_bone.transform_order + mmd_bone.transform_after_dynamics = pmx_bone.transAfterPhis + + if pmx_bone.displayConnection == -1 or pmx_bone.displayConnection == (0.0, 0.0, 0.0): + mmd_bone.is_tip = True + elif b_bone.name in specialTipBones: + mmd_bone.is_tip = True + + b_bone.bone.hide = not pmx_bone.visible # or mmd_bone.is_tip + + if not pmx_bone.isRotatable: + b_bone.lock_rotation = [True, True, True] + + if not pmx_bone.isMovable: + b_bone.lock_location = [True, True, True] + + if pmx_bone.isIK: + if 0 <= pmx_bone.target < len(pose_bones): + self.__applyIk(i, pmx_bone, pose_bones) + + if pmx_bone.hasAdditionalRotate or pmx_bone.hasAdditionalLocation: + bone_index, influ = pmx_bone.additionalTransform + mmd_bone.has_additional_rotation = pmx_bone.hasAdditionalRotate + mmd_bone.has_additional_location = pmx_bone.hasAdditionalLocation + mmd_bone.additional_transform_influence = influ + if 0 <= bone_index < len(pose_bones): + mmd_bone.additional_transform_bone = pose_bones[bone_index].name + + if pmx_bone.localCoordinate is not None: + mmd_bone.enabled_local_axes = True + mmd_bone.local_axis_x = pmx_bone.localCoordinate.x_axis + mmd_bone.local_axis_z = pmx_bone.localCoordinate.z_axis + + if pmx_bone.axis is not None: + mmd_bone.enabled_fixed_axis = True + mmd_bone.fixed_axis = pmx_bone.axis + + if not self.__apply_bone_fixed_axis and mmd_bone.is_tip: + b_bone.lock_rotation = [True, False, True] + b_bone.lock_location = [True, True, True] + b_bone.lock_scale = [True, True, True] + + def __importRigids(self) -> None: + """Import rigid bodies from the PMX model""" + start_time = time.time() + self.__rigidTable = {} + context = FnContext.ensure_context() + rigid_pool = FnRigidBody.new_rigid_body_objects(context, FnModel.ensure_rigid_group_object(context, self.__rig.rootObject()), len(self.__model.rigids)) + for i, (rigid, rigid_obj) in enumerate(zip(self.__model.rigids, rigid_pool)): + loc = Vector(rigid.location).xzy * self.__scale + rot = Vector(rigid.rotation).xzy * -1 + size = Vector(rigid.size).xzy if rigid.type == pmx.Rigid.TYPE_BOX else Vector(rigid.size) + + obj = FnRigidBody.setup_rigid_body_object( + obj=rigid_obj, + shape_type=rigid.type, + location=loc, + rotation=rot, + size=size * self.__scale, + dynamics_type=rigid.mode, + name=rigid.name, + name_e=rigid.name_e, + collision_group_number=rigid.collision_group_number, + collision_group_mask=[rigid.collision_group_mask & (1 << i) == 0 for i in range(16)], + mass=rigid.mass, + friction=rigid.friction, + angular_damping=rigid.rotation_attenuation, + linear_damping=rigid.velocity_attenuation, + bounce=rigid.bounce, + bone=None if rigid.bone == -1 or rigid.bone is None else self.__boneTable[rigid.bone].name, + ) + obj.hide_set(True) + MoveObject.set_index(obj, i) + self.__rigidTable[i] = obj + + logger.debug("Finished importing rigid bodies in %.2f seconds", time.time() - start_time) + + def __importJoints(self) -> None: + """Import joints from the PMX model""" + start_time = time.time() + context = FnContext.ensure_context() + joint_pool = FnRigidBody.new_joint_objects(context, FnModel.ensure_joint_group_object(context, self.__rig.rootObject()), len(self.__model.joints), FnModel.get_empty_display_size(self.__rig.rootObject())) + for i, (joint, joint_obj) in enumerate(zip(self.__model.joints, joint_pool)): + loc = Vector(joint.location).xzy * self.__scale + rot = Vector(joint.rotation).xzy * -1 + + obj = FnRigidBody.setup_joint_object( + obj=joint_obj, + name=joint.name, + name_e=joint.name_e, + location=loc, + rotation=rot, + rigid_a=self.__rigidTable.get(joint.src_rigid, None), + rigid_b=self.__rigidTable.get(joint.dest_rigid, None), + maximum_location=Vector(joint.maximum_location).xzy * self.__scale, + minimum_location=Vector(joint.minimum_location).xzy * self.__scale, + maximum_rotation=Vector(joint.minimum_rotation).xzy * -1, + minimum_rotation=Vector(joint.maximum_rotation).xzy * -1, + spring_linear=Vector(joint.spring_constant).xzy, + spring_angular=Vector(joint.spring_rotation_constant).xzy, + ) + obj.hide_set(True) + MoveObject.set_index(obj, i) + + logger.debug("Finished importing joints in %.2f seconds", time.time() - start_time) + + def __importMaterials(self) -> None: + """Import materials from the PMX model""" + self.__importTextures() + + pmxModel = self.__model + + self.__materialFaceCountTable = [] + for i in pmxModel.materials: + mat = bpy.data.materials.new(name=self.__safe_name(i.name, max_length=50)) + self.__materialTable.append(mat) + mmd_mat = mat.mmd_material + mmd_mat.name_j = i.name + mmd_mat.name_e = i.name_e + mmd_mat.ambient_color = i.ambient + mmd_mat.diffuse_color = i.diffuse[0:3] + mmd_mat.alpha = i.diffuse[3] + mmd_mat.specular_color = i.specular + mmd_mat.shininess = i.shininess + mmd_mat.is_double_sided = i.is_double_sided + mmd_mat.enabled_drop_shadow = i.enabled_drop_shadow + mmd_mat.enabled_self_shadow_map = i.enabled_self_shadow_map + mmd_mat.enabled_self_shadow = i.enabled_self_shadow + mmd_mat.enabled_toon_edge = i.enabled_toon_edge + mmd_mat.edge_color = i.edge_color + mmd_mat.edge_weight = i.edge_size + mmd_mat.comment = i.comment + + self.__materialFaceCountTable.append(int(i.vertex_count / 3)) + self.__meshObj.data.materials.append(mat) + fnMat = FnMaterial(mat) + if i.texture != -1: + texture_slot = fnMat.create_texture(self.__textureTable[i.texture]) + texture_slot.texture.use_mipmap = self.__use_mipmap + self.__imageTable[len(self.__materialTable) - 1] = texture_slot.texture.image + + if i.is_shared_toon_texture: + mmd_mat.is_shared_toon_texture = True + mmd_mat.shared_toon_texture = i.toon_texture + else: + mmd_mat.is_shared_toon_texture = False + if i.toon_texture >= 0: + mmd_mat.toon_texture = self.__textureTable[i.toon_texture] + + if i.sphere_texture_mode == 2: + amount = self.__spa_blend_factor + else: + amount = self.__sph_blend_factor + if i.sphere_texture != -1 and amount != 0.0: + texture_slot = fnMat.create_sphere_texture(self.__textureTable[i.sphere_texture]) + texture_slot.diffuse_color_factor = amount + if i.sphere_texture_mode == 3 and getattr(pmxModel.header, "additional_uvs", 0): + texture_slot.uv_layer = "UV1" # for SubTexture + mmd_mat.sphere_texture_type = str(i.sphere_texture_mode) + + def __importFaces(self) -> None: + """Import faces/polygons from the PMX model""" + pmxModel = self.__model + mesh = self.__meshObj.data + vertex_map = self.__vertex_map + + loop_indices_orig = tuple(i for f in pmxModel.faces for i in f) + loop_indices = tuple(vertex_map[i][1] for i in loop_indices_orig) if vertex_map else loop_indices_orig + material_indices = tuple(i for i, c in enumerate(self.__materialFaceCountTable) for x in range(c)) + + mesh.loops.add(len(pmxModel.faces) * 3) + mesh.loops.foreach_set("vertex_index", loop_indices) + + mesh.polygons.add(len(pmxModel.faces)) + mesh.polygons.foreach_set("loop_start", tuple(range(0, len(mesh.loops), 3))) + mesh.polygons.foreach_set("loop_total", (3,) * len(pmxModel.faces)) + mesh.polygons.foreach_set("use_smooth", (True,) * len(pmxModel.faces)) + mesh.polygons.foreach_set("material_index", material_indices) + + uv_textures, uv_layers = getattr(mesh, "uv_textures", mesh.uv_layers), mesh.uv_layers + uv_tex = uv_textures.new() + uv_layer = uv_layers[uv_tex.name] + uv_table = {vi: self.flipUV_V(v.uv) for vi, v in enumerate(pmxModel.vertices)} + uv_layer.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i])) + + if hasattr(mesh, "uv_textures"): + for bf, mi in zip(uv_tex.data, material_indices): + bf.image = self.__imageTable.get(mi, None) + + if pmxModel.header and pmxModel.header.additional_uvs: + logger.info("Importing %d additional uvs", pmxModel.header.additional_uvs) + zw_data_map = collections.OrderedDict() + split_uvzw = lambda uvi: (self.flipUV_V(uvi[:2]), uvi[2:]) + for i in range(pmxModel.header.additional_uvs): + add_uv = uv_layers[uv_textures.new(name="UV" + str(i + 1)).name] + logger.info(" - %s...(uv channels)", add_uv.name) + uv_table = {vi: split_uvzw(v.additional_uvs[i]) for vi, v in enumerate(pmxModel.vertices)} + add_uv.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i][0])) + if not any(any(s[1]) for s in uv_table.values()): + logger.info("\t- zw are all zeros: %s", add_uv.name) + else: + zw_data_map["_" + add_uv.name] = {k: self.flipUV_V(v[1]) for k, v in uv_table.items()} + for name, zw_table in zw_data_map.items(): + logger.info(" - %s...(zw channels of %s)", name, name[1:]) + add_zw = uv_textures.new(name=name) + if add_zw is None: + logger.warning("\t* Lost zw channels") + continue + add_zw = uv_layers[add_zw.name] + add_zw.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in zw_table[i])) + + self.__fixOverlappingFaceMaterials(mesh.materials, mesh.vertices, loop_indices, material_indices) + + def __fixOverlappingFaceMaterials(self, materials: List[bpy.types.Material], + vertices: List[bpy.types.MeshVertex], + loop_indices: List[int], + material_indices: List[int]) -> None: + """Fix overlapping face materials to prevent z-fighting""" + # FIXME: This is not the best way to setup blend_method, might just work for some common cases. + # For EEVEE, basically users should know which blend_method is best for each material of their models. + # For Cycles, users have to offset or delete those z-fighting faces to fix it manually. + check = {} + mi_skip = -1 + _vi_cache = {} + + def _rounded_co_vi(vi: int) -> Tuple[float, float, float]: + if vi not in _vi_cache: + vco = vertices[vi].co + _vi_cache[vi] = (round(vco[0], 6), round(vco[1], 6), round(vco[2], 6)) + return _vi_cache[vi] + + assert len(loop_indices) == len(material_indices) * 3 + for i, mi in enumerate(material_indices): + if mi <= mi_skip: + continue + si = 3 * i + verts = tuple(sorted((_rounded_co_vi(loop_indices[si]), _rounded_co_vi(loop_indices[si + 1]), _rounded_co_vi(loop_indices[si + 2])))) + if verts not in check: + check[verts] = mi + elif check[verts] < mi: + logger.debug("Fix blend method of material: %s", materials[mi].name) + materials[mi].blend_method = "BLEND" + materials[mi].show_transparent_back = False + mi_skip = mi + + def __importVertexMorphs(self) -> None: + """Import vertex morphs from the PMX model""" + mmd_root = self.__root.mmd_root + categories = self.CATEGORIES + self.__createBasisShapeKey() + for morph in (x for x in self.__model.morphs if isinstance(x, pmx.VertexMorph)): + shapeKey = self.__meshObj.shape_key_add(name=morph.name) + vtx_morph = mmd_root.vertex_morphs.add() + vtx_morph.name = morph.name + vtx_morph.name_e = morph.name_e + vtx_morph.category = categories.get(morph.category, "OTHER") + for md in morph.offsets: + shapeKeyPoint = shapeKey.data[md.index] + shapeKeyPoint.co += Vector(md.offset).xzy * self.__scale + + def __importMaterialMorphs(self) -> None: + """Import material morphs from the PMX model""" + mmd_root = self.__root.mmd_root + categories = self.CATEGORIES + for morph in (x for x in self.__model.morphs if isinstance(x, pmx.MaterialMorph)): + mat_morph = mmd_root.material_morphs.add() + mat_morph.name = morph.name + mat_morph.name_e = morph.name_e + mat_morph.category = categories.get(morph.category, "OTHER") + for morph_data in morph.offsets: + data = mat_morph.data.add() + data.related_mesh = self.__meshObj.data.name + if 0 <= morph_data.index < len(self.__materialTable): + data.material = self.__materialTable[morph_data.index].name + data.offset_type = ["MULT", "ADD"][morph_data.offset_type] + data.diffuse_color = morph_data.diffuse_offset + data.specular_color = morph_data.specular_offset + data.shininess = morph_data.shininess_offset + data.ambient_color = morph_data.ambient_offset + data.edge_color = morph_data.edge_color_offset + data.edge_weight = morph_data.edge_size_offset + data.texture_factor = morph_data.texture_factor + data.sphere_texture_factor = morph_data.sphere_texture_factor + data.toon_texture_factor = morph_data.toon_texture_factor + + def __importBoneMorphs(self) -> None: + """Import bone morphs from the PMX model""" + mmd_root = self.__root.mmd_root + categories = self.CATEGORIES + for morph in (x for x in self.__model.morphs if isinstance(x, pmx.BoneMorph)): + bone_morph = mmd_root.bone_morphs.add() + bone_morph.name = morph.name + bone_morph.name_e = morph.name_e + bone_morph.category = categories.get(morph.category, "OTHER") + for morph_data in morph.offsets: + if not (0 <= morph_data.index < len(self.__boneTable)): + continue + data = bone_morph.data.add() + bl_bone = self.__boneTable[morph_data.index] + data.bone = bl_bone.name + converter = BoneConverter(bl_bone, self.__scale) + data.location = converter.convert_location(morph_data.location_offset) + data.rotation = converter.convert_rotation(morph_data.rotation_offset) + + def __importUVMorphs(self) -> None: + """Import UV morphs from the PMX model""" + mmd_root = self.__root.mmd_root + categories = self.CATEGORIES + __OffsetData = collections.namedtuple("OffsetData", "index, offset") + __convert_offset = lambda x: (x[0], -x[1], x[2], -x[3]) + for morph in (x for x in self.__model.morphs if isinstance(x, pmx.UVMorph)): + uv_morph = mmd_root.uv_morphs.add() + uv_morph.name = morph.name + uv_morph.name_e = morph.name_e + uv_morph.category = categories.get(morph.category, "OTHER") + uv_morph.uv_index = morph.uv_index + + offsets = (__OffsetData(d.index, __convert_offset(d.offset)) for d in morph.offsets) + FnMorph.store_uv_morph_data(self.__meshObj, uv_morph, offsets, "") + uv_morph.data_type = "VERTEX_GROUP" + + def __importGroupMorphs(self) -> None: + """Import group morphs from the PMX model""" + mmd_root = self.__root.mmd_root + categories = self.CATEGORIES + morph_types = self.MORPH_TYPES + pmx_morphs = self.__model.morphs + for morph in (x for x in pmx_morphs if isinstance(x, pmx.GroupMorph)): + group_morph = mmd_root.group_morphs.add() + group_morph.name = morph.name + group_morph.name_e = morph.name_e + group_morph.category = categories.get(morph.category, "OTHER") + for morph_data in morph.offsets: + if not (0 <= morph_data.morph < len(pmx_morphs)): + continue + data = group_morph.data.add() + m = pmx_morphs[morph_data.morph] + data.name = m.name + data.morph_type = morph_types[m.type_index()] + data.factor = morph_data.factor + + def __importDisplayFrames(self) -> None: + """Import display frames from the PMX model""" + pmxModel = self.__model + root = self.__root + morph_types = self.MORPH_TYPES + + for i in pmxModel.display: + frame = root.mmd_root.display_item_frames.add() + frame.name = i.name + frame.name_e = i.name_e + frame.is_special = i.isSpecial + for disp_type, index in i.data: + item = frame.data.add() + if disp_type == 0: + item.type = "BONE" + item.name = self.__boneTable[index].name + elif disp_type == 1: + item.type = "MORPH" + morph = pmxModel.morphs[index] + item.name = morph.name + item.morph_type = morph_types[morph.type_index()] + else: + raise Exception("Unknown display item type.") + + FnBone.sync_bone_collections_from_display_item_frames(self.__armObj) + + def __addArmatureModifier(self, meshObj: Object, armObj: Object) -> None: + """Add armature modifier to mesh object""" + armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE") + armModifier.object = armObj + armModifier.use_vertex_groups = True + armModifier.name = "mmd_bone_order_override" + armModifier.show_render = armModifier.show_viewport = len(meshObj.data.vertices) > 0 + + def __assignCustomNormals(self) -> None: + """Assign custom normals to the mesh""" + mesh: bpy.types.Mesh = self.__meshObj.data + logger.info("Setting custom normals...") + if self.__vertex_map: + verts, faces = self.__model.vertices, self.__model.faces + custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f] + mesh.normals_split_custom_set(custom_normals) + else: + custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices] + mesh.normals_split_custom_set_from_vertices(custom_normals) + logger.info("Custom normals applied successfully") + + def __renameLRBones(self, use_underscore: bool) -> None: + """Rename left/right bones with proper naming convention""" + pose_bones = self.__armObj.pose.bones + for i in pose_bones: + self.__rig.renameBone(i.name, utils.convertNameToLR(i.name, use_underscore)) + + def __translateBoneNames(self) -> None: + """Translate bone names using the provided translator""" + pose_bones = self.__armObj.pose.bones + for i in pose_bones: + self.__rig.renameBone(i.name, self.__translator.translate(i.name)) + + def __fixRepeatedMorphName(self) -> None: + """Fix repeated morph names to ensure uniqueness""" + used_names = set() + for m in self.__model.morphs: + m.name = utils.unique_name(m.name or "Morph", used_names) + used_names.add(m.name) + + def execute(self, context: Context, **args) -> None: + """Execute the PMX import process with the given arguments""" + if "pmx" in args: + self.__model = args["pmx"] + else: + self.__model = pmx.load(args["filepath"]) + self.__fixRepeatedMorphName() + + types = args.get("types", set()) + clean_model = args.get("clean_model", False) + remove_doubles = args.get("remove_doubles", False) + self.__scale = args.get("scale", 1.0) + self.__use_mipmap = args.get("use_mipmap", True) + self.__sph_blend_factor = args.get("sph_blend_factor", 1.0) + self.__spa_blend_factor = args.get("spa_blend_factor", 1.0) + self.__fix_IK_links = args.get("fix_IK_links", False) + self.__apply_bone_fixed_axis = args.get("apply_bone_fixed_axis", False) + self.__translator = args.get("translator", None) + + logger.info("****************************************") + logger.info("Starting PMX import process") + logger.info("----------------------------------------") + + start_time = time.time() + + with ProgressTracker(context, 100, "Importing PMX Model") as progress: + self.__createObjects() + progress.step("Created base objects") + + if "MESH" in types: + if clean_model: + _PMXCleaner.clean(self.__model, "MORPHS" not in types) + if remove_doubles: + self.__vertex_map = _PMXCleaner.remove_doubles(self.__model, "MORPHS" not in types) + + progress.step("Preparing mesh data") + self.__createMeshObject() + progress.step("Importing vertices") + self.__importVertices() + progress.step("Importing materials") + self.__importMaterials() + progress.step("Importing faces") + self.__importFaces() + self.__meshObj.data.update() + progress.step("Assigning custom normals") + self.__assignCustomNormals() + progress.step("Processing SDEF vertices") + self.__storeVerticesSDEF() + + if "ARMATURE" in types: + progress.step("Preparing armature") + # for tracking bone order + if "MESH" not in types: + self.__createMeshObject() + self.__importVertexGroup() + progress.step("Importing bones") + self.__importBones() + if args.get("rename_LR_bones", False): + use_underscore = args.get("use_underscore", False) + self.__renameLRBones(use_underscore) + if self.__translator: + self.__translateBoneNames() + if self.__apply_bone_fixed_axis: + FnBone.apply_bone_fixed_axis(self.__armObj) + FnBone.apply_additional_transformation(self.__armObj) + + if "PHYSICS" in types: + progress.step("Importing rigid bodies") + self.__importRigids() + progress.step("Importing joints") + self.__importJoints() + + if "DISPLAY" in types: + progress.step("Importing display frames") + self.__importDisplayFrames() + else: + self.__rig.initialDisplayFrames() + + if "MORPHS" in types: + progress.step("Importing group morphs") + self.__importGroupMorphs() + progress.step("Importing vertex morphs") + self.__importVertexMorphs() + progress.step("Importing bone morphs") + self.__importBoneMorphs() + progress.step("Importing material morphs") + self.__importMaterialMorphs() + progress.step("Importing UV morphs") + self.__importUVMorphs() + + if self.__meshObj: + progress.step("Adding armature modifier") + self.__addArmatureModifier(self.__meshObj, self.__armObj) + + FnModel.change_mmd_ik_loop_factor(self.__root, args.get("ik_loop_factor", 1)) + utils.selectAObject(self.__root) + + logger.info("Finished importing the model in %.2f seconds", time.time() - start_time) + logger.info("----------------------------------------") + + +class _PMXCleaner: + """Helper class for cleaning PMX data during import""" + + @classmethod + def clean(cls, pmx_model: Any, mesh_only: bool) -> None: + """Clean PMX data by removing unused vertices and faces""" + logger.info("Cleaning PMX data...") + pmx_faces = pmx_model.faces + pmx_vertices = pmx_model.vertices + + # clean face/vertex + cls.__clean_pmx_faces(pmx_faces, pmx_model.materials, lambda f: frozenset(f)) + + index_map = {v: v for f in pmx_faces for v in f} + is_index_clean = len(index_map) == len(pmx_vertices) + if is_index_clean: + logger.info("Vertices are clean, no cleaning needed") + else: + new_vertex_count = 0 + for v in sorted(index_map): + if v != new_vertex_count: + pmx_vertices[new_vertex_count] = pmx_vertices[v] + index_map[v] = new_vertex_count + new_vertex_count += 1 + logger.warning("Removed %d unused vertices", len(pmx_vertices) - new_vertex_count) + del pmx_vertices[new_vertex_count:] + + # update vertex indices of faces + for f in pmx_faces: + f[:] = [index_map[v] for v in f] + + if mesh_only: + logger.info("Mesh-only cleaning completed") + return + + if not is_index_clean: + # clean vertex/uv morphs + def __update_index(x): + x.index = index_map.get(x.index, None) + return x.index is not None + + cls.__clean_pmx_morphs(pmx_model.morphs, __update_index) + logger.info("PMX cleaning completed") + + @classmethod + def remove_doubles(cls, pmx_model: Any, mesh_only: bool) -> Optional[Dict[int, Tuple[int, int]]]: + """Remove duplicate vertices from the PMX model""" + logger.info("Removing duplicate vertices...") + pmx_vertices = pmx_model.vertices + + vertex_map = [None] * len(pmx_vertices) + # gather vertex data + for i, v in enumerate(pmx_vertices): + vertex_map[i] = [tuple(v.co)] + if not mesh_only: + for i, m in enumerate(pmx_model.morphs): + if not isinstance(m, pmx.VertexMorph) and not isinstance(m, pmx.UVMorph): + continue + for x in m.offsets: + vertex_map[x.index].append((i,) + tuple(x.offset)) + # generate vertex merging table + keys = {} + for i, v in enumerate(vertex_map): + k = tuple(v) + if k in keys: + vertex_map[i] = keys[k] # merge pmx_vertices[i] to pmx_vertices[keys[k][0]] + else: + vertex_map[i] = keys[k] = (i, len(keys)) # (pmx index, blender index) + counts = len(vertex_map) - len(keys) + keys.clear() + if counts: + logger.warning("%d duplicate vertices will be removed", counts) + else: + logger.info("No duplicate vertices found") + return None + + # clean face + face_key_func = lambda f: frozenset({vertex_map[x][0]: tuple(pmx_vertices[x].uv) for x in f}.items()) + cls.__clean_pmx_faces(pmx_model.faces, pmx_model.materials, face_key_func) + + if mesh_only: + logger.info("Mesh-only duplicate removal completed") + else: + # clean vertex/uv morphs + def __update_index(x): + indices = vertex_map[x.index] + x.index = indices[1] if x.index == indices[0] else None + return x.index is not None + + cls.__clean_pmx_morphs(pmx_model.morphs, __update_index) + logger.info("Duplicate removal completed") + return vertex_map + + @staticmethod + def __clean_pmx_faces(pmx_faces: List[List[int]], pmx_materials: List[Any], face_key_func: Callable) -> None: + """Clean PMX faces by removing duplicates and updating material vertex counts""" + new_face_count = 0 + face_iter = iter(pmx_faces) + for mat in pmx_materials: + used_faces = set() + new_vertex_count = 0 + for i in range(int(mat.vertex_count / 3)): + f = next(face_iter) + + f_key = face_key_func(f) + if len(f_key) != 3 or f_key in used_faces: + continue + used_faces.add(f_key) + + pmx_faces[new_face_count] = list(f) + new_face_count += 1 + new_vertex_count += 3 + mat.vertex_count = new_vertex_count + face_iter = None + if new_face_count == len(pmx_faces): + logger.info("Faces are clean, no cleaning needed") + else: + logger.warning("Removed %d duplicate faces", len(pmx_faces) - new_face_count) + del pmx_faces[new_face_count:] + + @staticmethod + def __clean_pmx_morphs(pmx_morphs: List[Any], index_update_func: Callable) -> None: + """Clean PMX morphs by updating vertex indices and removing invalid offsets""" + for m in pmx_morphs: + if not isinstance(m, pmx.VertexMorph) and not isinstance(m, pmx.UVMorph): + continue + old_len = len(m.offsets) + m.offsets = [x for x in m.offsets if index_update_func(x)] + counts = old_len - len(m.offsets) + if counts: + logger.warning('Removed %d (of %d) offsets from morph "%s"', counts, old_len, m.name) diff --git a/core/mmd/__init__.py b/core/mmd/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/mmd/bone.py b/core/mmd/bone.py new file mode 100644 index 0000000..7ac61f2 --- /dev/null +++ b/core/mmd/bone.py @@ -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() diff --git a/core/mmd/core/bpyutils.py b/core/mmd/core/bpyutils.py new file mode 100644 index 0000000..2800d3c --- /dev/null +++ b/core/mmd/core/bpyutils.py @@ -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) \ No newline at end of file diff --git a/core/mmd/core/utils.py b/core/mmd/core/utils.py new file mode 100644 index 0000000..4a6f5df --- /dev/null +++ b/core/mmd/core/utils.py @@ -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(?P[._])[lL])(?P($|(?P=separator)))") +__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P(?P[._])[rR])(?P($|(?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 diff --git a/core/mmd/material.py b/core/mmd/material.py new file mode 100644 index 0000000..576e212 --- /dev/null +++ b/core/mmd/material.py @@ -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"]) diff --git a/core/mmd/properties/__init__.py b/core/mmd/properties/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/mmd/properties/pose_bone.py b/core/mmd/properties/pose_bone.py new file mode 100644 index 0000000..7795325 --- /dev/null +++ b/core/mmd/properties/pose_bone.py @@ -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) \ No newline at end of file diff --git a/core/mmd/properties/root.py b/core/mmd/properties/root.py new file mode 100644 index 0000000..6423a1e --- /dev/null +++ b/core/mmd/properties/root.py @@ -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 \ No newline at end of file From 036e260dd68bf7c1f20ad2d5c33201f4599b9385 Mon Sep 17 00:00:00 2001 From: 989onan Date: Thu, 3 Apr 2025 19:12:55 -0400 Subject: [PATCH 2/6] Vastly improve Merge Doubles - removed advanced merge doubles, it just does advanced by default - same behavior as advanced was before, but now completes the task in under a second. Thanks to the power of BMesh! - Labels now reflect this change --- functions/optimization/remove_doubles.py | 280 ++++++----------------- resources/translations/en_US.json | 6 +- ui/optimization_panel.py | 3 +- 3 files changed, 72 insertions(+), 217 deletions(-) diff --git a/functions/optimization/remove_doubles.py b/functions/optimization/remove_doubles.py index e5c20e5..f41e3ef 100644 --- a/functions/optimization/remove_doubles.py +++ b/functions/optimization/remove_doubles.py @@ -1,3 +1,4 @@ +import traceback import bpy import numpy as np from typing import List, TypedDict, Any, Literal, TypeAlias, cast @@ -9,6 +10,8 @@ from ...core.common import ( get_all_meshes, ) from ...core.armature_validation import validate_armature +import bmesh +import mathutils # Constants MERGE_ITERATION_COUNT = 20 @@ -19,83 +22,38 @@ ModalReturnType: TypeAlias = Literal['RUNNING_MODAL', 'FINISHED', 'CANCELLED'] class MeshEntry(TypedDict): mesh: Object - shapekeys: list[str] - vertices: int - cur_vertex_pass: int + shapekeys: list[bpy.types.Object] -def create_duplicate_for_merge(context: Context, mesh: Object, shapekey_name: str) -> Object: +def create_duplicate_for_merge(context: Context, mesh: Object, shapekey_name: str = "") -> Object: """Creates a duplicate mesh object for merge testing""" - context.view_layer.objects.active = mesh + + if(shapekey_name != ""): + mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(shapekey_name) + bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') mesh.select_set(True) + context.view_layer.objects.active = mesh bpy.ops.object.duplicate() - bpy.ops.object.shape_key_move(type='TOP') + if(shapekey_name != ""): + bpy.ops.object.shape_key_move(type='TOP') + bpy.ops.object.shape_key_remove(all=True,apply_mix=False) duplicate = context.view_layer.objects.active - duplicate.name = f"{shapekey_name}_object_is_{mesh.name}" + if(shapekey_name != ""): + duplicate.name = f"{shapekey_name}_object_is_{mesh.name}" + + else: + duplicate.name = f"object_is_{mesh.name}" return duplicate -def process_vertex_merging(mesh_data: bpy.types.Mesh, vertices_original: dict[int, Any], current_vertex: int) -> list[int]: - """Process vertex merging and return merged vertex indices""" - merged_vertices = [] - i, j = 0, 0 - - while i < len(vertices_original): - if j + 1 > len(mesh_data.vertices): - merged_vertices.append(i) - j = j - 1 - elif mesh_data.vertices[j].co.xyz != vertices_original[i]: - merged_vertices.append(i) - j = j - 1 - elif vertices_original[i] == vertices_original[current_vertex]: - merged_vertices.append(i) - i, j = i + 1, j + 1 - - return merged_vertices - -def vertex_moves(mesh_data: bpy.types.Mesh, vertex: int) -> bool: - - for shapekey in mesh_data.shape_keys.key_blocks: - data: bpy.types.ShapeKey = shapekey - - if data.points[vertex].co.xyz != mesh_data.vertices[vertex].co.xyz: - return True - - return False - -def merge_vertex_at_index(mesh_data: bpy.types.Mesh, index: int, distance: float): - - select_target_vertex = [False]*len(mesh_data.vertices) - select_target_vertex[index] = True - - bpy.ops.object.mode_set(mode='OBJECT') - mesh_data.vertices.foreach_set("select",select_target_vertex) - bpy.ops.object.mode_set(mode='EDIT') - for _ in range(0,20): #for some reason, if using merge to unselected on a vertex, the vertex will only merge to 1 other vertex. so we gotta spam it to fix it. - bpy.ops.mesh.remove_doubles(threshold=distance, use_unselected=True, use_sharp_edge_from_normals=False) +def select_obj(context: Context, obj: Object, target_mode='OBJECT'): bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode=target_mode) -class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator): - bl_idname = "avatar_toolkit.remove_doubles_advanced" - bl_label = t("Optimization.remove_doubles_advanced") - bl_description = t("Optimization.remove_doubles_advanced_desc") - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context: Context) -> bool: - """Check if the operator can be executed""" - armature = get_active_armature(context) - if not armature: - return False - valid, _, _ = validate_armature(armature) - return valid - - def execute(self, context: Context) -> set[str]: - """Execute the advanced remove doubles operator""" - context.scene.avatar_toolkit.remove_doubles_advanced = True - bpy.ops.avatar_toolkit.remove_doubles('INVOKE_DEFAULT') - return {'RUNNING_MODAL'} class AvatarToolkit_OT_RemoveDoubles(Operator): bl_idname = "avatar_toolkit.remove_doubles" @@ -104,7 +62,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator): bl_options = {'REGISTER', 'UNDO'} objects_to_do: list[MeshEntry] = [] - + merge_distance: bpy.props.FloatProperty(name=t("Optimization.merge_distance"), description=t("Optimization.merge_distance_desc"), default=.001) @classmethod def poll(cls, context: Context) -> bool: """Check if the operator can be executed""" @@ -117,27 +75,27 @@ class AvatarToolkit_OT_RemoveDoubles(Operator): def draw(self, context: Context) -> None: """Draw the operator's UI""" layout = self.layout - layout.prop(context.scene.avatar_toolkit, "remove_doubles_merge_distance") - layout.label(text=t("Optimization.remove_doubles_warning")) - layout.label(text=t("Optimization.remove_doubles_wait")) + layout.prop(self, "merge_distance") def invoke(self, context: Context, event: Event) -> set[str]: """Initialize the operator""" logger.info("Starting modal execution of merge doubles safely") return context.window_manager.invoke_props_dialog(self) - def setup_mesh_entry(self, mesh: Object) -> MeshEntry: + def setup_mesh_entry(self, context: Context, mesh: Object) -> MeshEntry: """Set up mesh entry data structure""" + #create shapekey objects to merge doubles on. + shapes: list[bpy.types.Object] = [] + if(mesh.data.shape_keys): + for shape in mesh.data.shape_keys.key_blocks: + shapes.append(create_duplicate_for_merge(context,mesh,shape.name)) + else: + shapes.append(create_duplicate_for_merge(context,mesh)) mesh_entry: MeshEntry = { "mesh": mesh, - "shapekeys": [], - "vertices": len(mesh.data.vertices), - "cur_vertex_pass": 0 + "shapekeys": shapes } - - if mesh.data.shape_keys: - mesh_entry["shapekeys"] = [shape.name for shape in mesh.data.shape_keys.key_blocks] - + return mesh_entry def execute(self, context: Context) -> set[str]: @@ -157,7 +115,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator): for mesh in objects: if mesh.data.name not in [obj["mesh"].data.name for obj in self.objects_to_do]: logger.debug(f"Setting up data for object {mesh.name}") - mesh_entry = self.setup_mesh_entry(mesh) + mesh_entry = self.setup_mesh_entry(context, mesh) self.objects_to_do.append(mesh_entry) context.window_manager.modal_handler_add(self) @@ -167,148 +125,50 @@ class AvatarToolkit_OT_RemoveDoubles(Operator): logger.error(f"Error in execute: {str(e)}") return {'CANCELLED'} - def modify_mesh(self, context: Context, mesh: MeshEntry) -> None: - """Basic mesh modification for simple cases""" - try: - mesh["mesh"].select_set(True) - context.view_layer.objects.active = mesh["mesh"] - mesh_data = mesh["mesh"].data - - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.mode_set(mode='OBJECT') - - # Select vertices with different positions in shape keys - for index, point in enumerate(mesh["mesh"].active_shape_key.points): - if point.co.xyz != mesh_data.shape_keys.key_blocks[0].points[index].co.xyz: - mesh_data.vertices[index].select = True - logger.debug(f"Shapekey has moved vertex at index {index}") - - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.mode_set(mode='OBJECT') - mesh["mesh"].select_set(False) - - except Exception as e: - logger.error(f"Error in modify_mesh: {str(e)}") - - def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> int: - """Advanced mesh modification with shape key handling""" - try: - final_merged_vertex_group = [] - initialized_final = False - merge_distance = context.scene.avatar_toolkit.remove_doubles_merge_distance - - for shapekey_name in mesh_entry["shapekeys"]: - duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name) - vertices_original = {i: v.co.xyz for i, v in enumerate(duplicate.data.vertices)} - - - merge_vertex_at_index(duplicate.data, mesh_entry["cur_vertex_pass"], merge_distance) #merge the vertex at our pass to find vertices that would merge to our vertex at this shapekey. - - # Process merging - merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"]) # find what vertices actually merged. - - if not initialized_final: - final_merged_vertex_group = merged_vertices.copy() - initialized_final = True - else: - final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices] # remove vertices that merged from the list if they didn't merge during this shapkey. - bpy.ops.object.delete() - - # Apply final merging - if final_merged_vertex_group: - self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance) # merge all vertices that merged on every shapekey no matter the shapekey during the loop. - - return len(final_merged_vertex_group) - - except Exception as e: - logger.error(f"Error in modify_mesh_advanced: {str(e)}") - return 1 - - def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None: - """Apply final vertex merging operations""" - mesh = mesh_entry["mesh"] - context.view_layer.objects.active = mesh - mesh.select_set(True) - - bpy.ops.object.mode_set(mode='OBJECT') - select_target_group = [False] * len(mesh.data.vertices) - for vertex_index in vertex_group: - select_target_group[vertex_index] = True - - mesh.data.vertices.foreach_set("select", select_target_group) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False) - bpy.ops.object.mode_set(mode='OBJECT') - - def process_simple_mesh(self, context: Context, mesh: MeshEntry, merge_distance: float) -> None: - """Process mesh without shapekeys using simple merge operation""" - logger.debug(f"Processing mesh without shapekeys: {mesh['mesh'].name}") - mesh["mesh"].select_set(True) - context.view_layer.objects.active = mesh["mesh"] - bpy.ops.object.mode_set(mode='EDIT') - mesh["mesh"].data.vertices.foreach_set("select", [False] * len(mesh["mesh"].data.vertices)) - - bpy.ops.mesh.select_all(action="INVERT") - bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False) - bpy.ops.object.mode_set(mode='OBJECT') - mesh["mesh"].select_set(False) - - def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None: - """Complete the mesh processing by performing final merge operations""" - logger.debug("Finishing mesh processing") - mesh["mesh"].select_set(True) - context.view_layer.objects.active = mesh["mesh"] - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action="INVERT") - bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False) - - bpy.ops.object.mode_set(mode='OBJECT') - mesh["mesh"].select_set(False) - def modal(self, context: Context, event: Event) -> set[ModalReturnType]: """Modal operator execution""" try: - if not self.objects_to_do: + if not self.objects_to_do or len(self.objects_to_do) <= 0: self.report({'INFO'}, t("Optimization.remove_doubles_completed")) logger.info("Finishing modal execution of merge doubles safely") return {'FINISHED'} - - mesh = self.objects_to_do[0] - mesh_data = mesh["mesh"].data - advanced = context.scene.avatar_toolkit.remove_doubles_advanced - merge_distance = context.scene.avatar_toolkit.remove_doubles_merge_distance - - if len(mesh['shapekeys']) > 0 and not advanced: - shapekeyname = mesh['shapekeys'].pop(0) - mesh["mesh"].active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekeyname) - logger.debug(f"Processing shapekey {shapekeyname}") - self.modify_mesh(context, mesh) + + mesh: MeshEntry = self.objects_to_do.pop(0) + merge_distance: float = self.merge_distance + + + #find which vertices merge on all shapekeys using bmesh, a fast way of doing it - @989onan + final_merged_vertex_group = [i for i in range(0,len(mesh['mesh'].data.vertices))] + for shape in mesh["shapekeys"]: + select_obj(context, shape, target_mode='EDIT') + bmesh_mesh: bmesh.types.BMesh = bmesh.from_edit_mesh(shape.data) + selected_verts: list[bmesh.types.BMVert] = [vert for vert in bmesh_mesh.verts if vert.select == True] + i: int = 0 + merged_vertices: set[int] = set() + mergers: dict[bmesh.types.BMVert, bmesh.types.BMVert] + for name,mergers in bmesh.ops.find_doubles(bmesh_mesh,verts=selected_verts,dist=merge_distance).items(): + for source_vert,target_vert in mergers.items(): + merged_vertices.add(source_vert.index) + merged_vertices.add(target_vert.index) + + final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices] - elif not mesh_data.shape_keys: - self.process_simple_mesh(context, mesh, merge_distance) - self.objects_to_do.pop(0) - - elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced: #advanced merging vertex by vertex - if(mesh["cur_vertex_pass"] < 0): #make sure it doesn't go below 0 and explode when advancing backwards from a previous step - mesh["cur_vertex_pass"] = 0 - - if vertex_moves(mesh["mesh"].data, mesh["cur_vertex_pass"]): # do not do advanced merging for vertices that don't move - mesh["cur_vertex_pass"] -= self.modify_mesh_advanced(context, mesh)-2 #advance forward or backwards based on how many vertices actually got merged, changing the list size. - #if above returns 1 (no vertices other than this one being merged to ourselves), advance by 1. else don't advance or go backwards. Makes sure all vertices get merged in the end. - else: - mesh["cur_vertex_pass"] += 1 + select_obj(context, mesh['mesh'], target_mode='EDIT') + data_mesh: bpy.types.Mesh = mesh['mesh'].data + bmesh_mesh: bmesh.types.BMesh = bmesh.from_edit_mesh(data_mesh) + mergable_on_all_shapes: list[bmesh.types.BMVert] = [vert for vert in bmesh_mesh.verts if vert.index in final_merged_vertex_group] + + mappings: dict[bmesh.types.BMVert,bmesh.types.BMVert] = bmesh.ops.find_doubles(bmesh_mesh,verts=mergable_on_all_shapes,dist=merge_distance)["targetmap"] - elif (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced and len(mesh['shapekeys']) > 0: #after advanced merging has gone past all the moving vertices, now we need to merge non moving vertices. - shapekeyname = mesh['shapekeys'].pop(0) - mesh["mesh"].active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekeyname) - logger.debug(f"Processing shapekey {shapekeyname}") - self.modify_mesh(context, mesh) - else: - self.finish_mesh_processing(context, mesh, advanced, merge_distance) - self.objects_to_do.pop(0) + bmesh.ops.weld_verts(bmesh_mesh,targetmap=mappings) + bmesh.update_edit_mesh(data_mesh, destructive=True) + + + for shape in mesh["shapekeys"]: + bpy.data.objects.remove(shape) return {'RUNNING_MODAL'} except Exception as e: - logger.error(f"Error in modal: {str(e)}") + logger.error(f"Error in modal: {traceback.format_exception(e)}") return {'CANCELLED'} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 2c642d6..cb7a600 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -128,9 +128,7 @@ "Optimization.combine_materials": "Combine Materials", "Optimization.combine_materials_desc": "Combine similar materials to reduce draw calls", "Optimization.remove_doubles": "Remove Doubles", - "Optimization.remove_doubles_desc": "Remove duplicate vertices", - "Optimization.remove_doubles_advanced": "Advanced", - "Optimization.remove_doubles_advanced_desc": "Remove duplicate vertices with advanced options", + "Optimization.remove_doubles_desc": "Remove duplicate vertices safely, keeping shapekeys preserved.", "Optimization.join_all_meshes": "Join All", "Optimization.join_all_meshes_desc": "Join all meshes in the scene", "Optimization.join_selected_meshes": "Join Selected", @@ -158,8 +156,6 @@ "Optimization.error.join_selected": "Failed to join selected meshes: {error}", "Optimization.merge_distance": "Merge Distance", "Optimization.merge_distance_desc": "Distance within which vertices will be merged", - "Optimization.remove_doubles_warning": "This process may take a long time", - "Optimization.remove_doubles_wait": "Blender may seem unresponsive during this operation", "Optimization.error.remove_doubles": "Failed to remove doubles: {error}", "Optimization.no_armature": "No armature selected", "Optimization.processing_mesh": "Processing mesh: {name}", diff --git a/ui/optimization_panel.py b/ui/optimization_panel.py index 04eb8dc..cfa1559 100644 --- a/ui/optimization_panel.py +++ b/ui/optimization_panel.py @@ -4,7 +4,7 @@ from bpy.types import Panel, Context, UILayout, Operator from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.translations import t from ..functions.optimization.materials_tools import AvatarToolkit_OT_CombineMaterials -from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles,AvatarToolkit_OT_RemoveDoublesAdvanced +from ..functions.optimization.remove_doubles import AvatarToolkit_OT_RemoveDoubles from ..functions.optimization.mesh_tools import AvatarToolkit_OT_JoinAllMeshes, AvatarToolkit_OT_JoinSelectedMeshes class AvatarToolKit_PT_OptimizationPanel(Panel): @@ -40,7 +40,6 @@ class AvatarToolKit_PT_OptimizationPanel(Panel): # Remove Doubles Row row: UILayout = col.row(align=True) row.operator(AvatarToolkit_OT_RemoveDoubles.bl_idname, icon='MESH_DATA') - row.operator(AvatarToolkit_OT_RemoveDoublesAdvanced.bl_idname, icon='PREFERENCES') # Join Meshes Box join_box: UILayout = layout.box() From 046ebfa72d45e44468d5fd92156699abb0c864b2 Mon Sep 17 00:00:00 2001 From: 989onan Date: Thu, 3 Apr 2025 19:44:56 -0400 Subject: [PATCH 3/6] bugfix fix pairs not merging if they would merge on one shapekey but not another --- functions/optimization/remove_doubles.py | 37 +++++++++++++++++------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/functions/optimization/remove_doubles.py b/functions/optimization/remove_doubles.py index f41e3ef..b4f33aa 100644 --- a/functions/optimization/remove_doubles.py +++ b/functions/optimization/remove_doubles.py @@ -138,32 +138,49 @@ class AvatarToolkit_OT_RemoveDoubles(Operator): #find which vertices merge on all shapekeys using bmesh, a fast way of doing it - @989onan - final_merged_vertex_group = [i for i in range(0,len(mesh['mesh'].data.vertices))] + #final_merged_vertex_group = [i for i in range(0,len(mesh['mesh'].data.vertices))] + final_merged_vertex_group: dict[set[int],list[int]] = [] for shape in mesh["shapekeys"]: select_obj(context, shape, target_mode='EDIT') bmesh_mesh: bmesh.types.BMesh = bmesh.from_edit_mesh(shape.data) selected_verts: list[bmesh.types.BMVert] = [vert for vert in bmesh_mesh.verts if vert.select == True] i: int = 0 - merged_vertices: set[int] = set() + merged_vertices: dict[set[int],list[int]] = {} #make a list of sets which act as pairs. the pairs being sets means it doesn't matter if element 0 is at index 1, it is still considered the same pair mergers: dict[bmesh.types.BMVert, bmesh.types.BMVert] for name,mergers in bmesh.ops.find_doubles(bmesh_mesh,verts=selected_verts,dist=merge_distance).items(): for source_vert,target_vert in mergers.items(): - merged_vertices.add(source_vert.index) - merged_vertices.add(target_vert.index) + pair: set[int] = set() + pair.add(source_vert.index) + pair.add(target_vert.index) + frozen_pair = frozenset(pair) + merged_vertices[frozen_pair] = [source_vert.index,target_vert.index] #put the pairs we have found into a list. - final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices] - + if(final_merged_vertex_group == []): #populate list if it is empty + final_merged_vertex_group = merged_vertices + new_dict: dict[set[int],list[int]] = {} + + #update our final list, keeping pairs that exist on all shapekeys and not just one. + for key,value in final_merged_vertex_group.items(): + if key in merged_vertices.keys(): + new_dict[key] = value + final_merged_vertex_group = new_dict + + #create an edit mesh and ensure it's vertex table select_obj(context, mesh['mesh'], target_mode='EDIT') data_mesh: bpy.types.Mesh = mesh['mesh'].data + mappings: dict[bmesh.types.BMVert,bmesh.types.BMVert] = {} bmesh_mesh: bmesh.types.BMesh = bmesh.from_edit_mesh(data_mesh) - mergable_on_all_shapes: list[bmesh.types.BMVert] = [vert for vert in bmesh_mesh.verts if vert.index in final_merged_vertex_group] - - mappings: dict[bmesh.types.BMVert,bmesh.types.BMVert] = bmesh.ops.find_doubles(bmesh_mesh,verts=mergable_on_all_shapes,dist=merge_distance)["targetmap"] + bmesh_mesh.verts.ensure_lookup_table() + #turn our pairs into a dictionary, which allows for merging vertices based on the shared pairs. + for key,value in final_merged_vertex_group.items(): + mappings[bmesh_mesh.verts[value[0]]] = bmesh_mesh.verts[value[1]] + + #weld the verts and update the source mesh bmesh.ops.weld_verts(bmesh_mesh,targetmap=mappings) bmesh.update_edit_mesh(data_mesh, destructive=True) - + #delete the shapekey reading meshes. for shape in mesh["shapekeys"]: bpy.data.objects.remove(shape) From 88e88b94a325d0b9ef5aa916c1a670a93c1e99cc Mon Sep 17 00:00:00 2001 From: 989onan Date: Thu, 3 Apr 2025 20:14:17 -0400 Subject: [PATCH 4/6] hotfix --- functions/optimization/remove_doubles.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/functions/optimization/remove_doubles.py b/functions/optimization/remove_doubles.py index b4f33aa..4240d93 100644 --- a/functions/optimization/remove_doubles.py +++ b/functions/optimization/remove_doubles.py @@ -26,23 +26,21 @@ class MeshEntry(TypedDict): def create_duplicate_for_merge(context: Context, mesh: Object, shapekey_name: str = "") -> Object: """Creates a duplicate mesh object for merge testing""" - - if(shapekey_name != ""): - mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(shapekey_name) - + bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') mesh.select_set(True) context.view_layer.objects.active = mesh bpy.ops.object.duplicate() - if(shapekey_name != ""): - bpy.ops.object.shape_key_move(type='TOP') - bpy.ops.object.shape_key_remove(all=True,apply_mix=False) - duplicate = context.view_layer.objects.active - if(shapekey_name != ""): - duplicate.name = f"{shapekey_name}_object_is_{mesh.name}" + if(shapekey_name != ""): + for shape in duplicate.data.shape_keys.key_blocks: + shape.value = 0 + duplicate.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(shapekey_name) + duplicate.active_shape_key.value = 1 + bpy.ops.object.shape_key_remove(all=True,apply_mix=True) + duplicate.name = f"{shapekey_name}_object_is_{mesh.name}" else: duplicate.name = f"object_is_{mesh.name}" return duplicate @@ -187,5 +185,6 @@ class AvatarToolkit_OT_RemoveDoubles(Operator): return {'RUNNING_MODAL'} except Exception as e: + print(traceback.format_exception(e)) logger.error(f"Error in modal: {traceback.format_exception(e)}") return {'CANCELLED'} From 6bafc7d7ac4edd4010752c9513c719e79a175181 Mon Sep 17 00:00:00 2001 From: 989onan Date: Sat, 5 Apr 2025 17:54:39 -0400 Subject: [PATCH 5/6] add explode model - Add method that allows for exploding the model into pieces for kit bashing or painting in substance painter. --- functions/tools/general_mesh_tools.py | 95 ++++++++++++++++++++++++++- resources/translations/en_US.json | 6 ++ ui/tools_panel.py | 3 +- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/functions/tools/general_mesh_tools.py b/functions/tools/general_mesh_tools.py index 0ac6d3c..5695f15 100644 --- a/functions/tools/general_mesh_tools.py +++ b/functions/tools/general_mesh_tools.py @@ -1,7 +1,7 @@ import bpy import numpy as np from bpy.types import Operator, Context -from typing import Set +from typing import Set, Literal from ...core.translations import t from ...core.logging_setup import logger from ...core.common import get_active_armature, get_all_meshes @@ -99,3 +99,96 @@ class AvatarToolkit_OT_SelectShortestSeamPath(Operator): return {'FINISHED'} +class AvatarToolkit_OT_ExplodeMesh(Operator): + """Explodes the mesh for use with painting programs, or painting inside blender.""" + bl_idname = "avatar_toolkit.explode_mesh" + bl_label = t("Tools.explode_mesh") + bl_description = t("Tools.explode_mesh_desc") + bl_options = {'REGISTER', 'UNDO'} + distance: bpy.props.FloatProperty(default=2.0,name=t("Tools.explode_mesh.distance"),description=t("Tools.explode_mesh.distance_desc")) + split_on_seams: bpy.props.BoolProperty(default=True,name=t("Tools.explode_mesh.split_on_seams"),description=t("Tools.explode_mesh.split_on_seams_desc")) + + def draw(self, context: Context) -> None: + """Draw the operator's UI""" + layout = self.layout + layout.prop(self, "distance") + + def invoke(self, context: Context, event: bpy.types.Event) -> set[str]: + """Initialize the operator""" + return context.window_manager.invoke_props_dialog(self) + + @classmethod + def poll(cls, context: Context) -> bool: + + return context.view_layer.objects.active.type == "MESH" and len(context.view_layer.objects.selected) == 1 + + + + def execute(self, context: Context) -> Set[str]: + + mesh_obj: bpy.types.Object = context.view_layer.objects.active.type + mesh: bpy.types.Mesh = context.view_layer.objects.active.data + if(self.split_on_seams): + + #set to correct mode + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type='EDGE') + + #mark seams by islands + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.uv.select_all(action="SELECT") + bpy.ops.uv.seams_from_islands(mark_seams=True,mark_sharp=False) + + #clear selection + bpy.ops.mesh.select_all(action="DESELECT") + bpy.ops.object.mode_set(mode='OBJECT') + bm = bmesh.new() # create an empty BMesh + bm.from_mesh(mesh) # fill it in from active mesh + + #select seam edges + for idx,edge in enumerate(bm.edges): + edge.select = edge.seam + bm.to_mesh(mesh) + bm.free() + + #split edges. + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.edge_split() + + #separate by loose. + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_mode(type='FACE') + + bpy.ops.mesh.select_all(action="SELECT") + + bpy.ops.mesh.separate(type='LOOSE') + + + distance: float = self.distance + + + #set origins to geometry + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.origin_set(type="ORIGIN_GEOMETRY",center="BOUNDS") + + #store original settings + origin_only_orig: bool = context.scene.tool_settings.use_transform_data_origin + pos_only_orig: bool = context.scene.tool_settings.use_transform_pivot_point_align + parents_only_orig: bool = context.scene.tool_settings.use_transform_skip_children + original_pivot: Literal['BOUNDING_BOX_CENTER', 'CURSOR', 'INDIVIDUAL_ORIGINS', 'MEDIAN_POINT', 'ACTIVE_ELEMENT'] = context.scene.tool_settings.transform_pivot_point + + #set scene settings correctly. + context.scene.tool_settings.use_transform_data_origin = False + context.scene.tool_settings.use_transform_pivot_point_align = True + context.scene.tool_settings.use_transform_skip_children = False + context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT' + + #spread out separated objects + bpy.ops.transform.resize(value=(self.distance, self.distance, self.distance), orient_type='GLOBAL') + + #restore settings. + context.scene.tool_settings.use_transform_data_origin = origin_only_orig + context.scene.tool_settings.use_transform_pivot_point_align = pos_only_orig + context.scene.tool_settings.use_transform_skip_children = parents_only_orig + context.scene.tool_settings.transform_pivot_point = original_pivot + return {'FINISHED'} \ No newline at end of file diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index cb7a600..e29ef01 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -215,6 +215,12 @@ "Tools.clean_weights_threshold_desc": "Minimum weight value to consider a bone as weighted", "Tools.find_shortest_seam_path": "Find Shortest Seam Path", "Tools.find_shortest_seam_path_desc": "Find shortest path of seams between two selected vertices connected to seams.", + "Tools.explode_mesh":"Explode Mesh for Painting", + "Tools.explode_mesh_desc": "Explodes the mesh for use with painting programs, or painting inside blender.", + "Tools.explode_mesh.distance": "Distance", + "Tools.explode_mesh.distance_desc": "Scale factor for distance between exploded items on model.", + "Tools.explode_mesh.split_on_seams_desc":"Split model on UV seams to separate islands from each other.", + "Tools.explode_mesh.split_on_seams":"Split on Seams", "Tools.apply_modifier_on_shapekey_obj":"Apply Modifier on Shapekey Object", "Tools.apply_modifier_on_shapekey_obj_desc":"Applies a modifier on an object regardless of it having shapekeys.", "Tools.merge_title": "Merge Tools", diff --git a/ui/tools_panel.py b/ui/tools_panel.py index b8aa933..fd4f25c 100644 --- a/ui/tools_panel.py +++ b/ui/tools_panel.py @@ -18,7 +18,7 @@ from ..functions.tools.bone_tools import ( from ..functions.tools.standardize_armature import AvatarToolkit_OT_StandardizeArmature from ..functions.tools.merge_tools import AvatarToolkit_OT_MergeToActive, AvatarToolkit_OT_MergeToParent, AvatarToolkit_OT_ConnectBones from ..functions.tools.rigify_converter import AvatarToolkit_OT_ConvertRigifyToUnity -from ..functions.tools.general_mesh_tools import AvatarToolkit_OT_SelectShortestSeamPath +from ..functions.tools.general_mesh_tools import AvatarToolkit_OT_SelectShortestSeamPath, AvatarToolkit_OT_ExplodeMesh from ..functions.custom_tools.force_apply_modifier import AvatarToolkit_OT_ApplyModifierForShapkeyObj class AvatarToolKit_PT_ToolsPanel(Panel): @@ -68,6 +68,7 @@ class AvatarToolKit_PT_ToolsPanel(Panel): col.separator(factor=0.5) col.operator(AvatarToolkit_OT_SelectShortestSeamPath.bl_idname,text=t("Tools.find_shortest_seam_path"),icon="MESH_DATA") col.operator(AvatarToolkit_OT_ApplyModifierForShapkeyObj.bl_idname,text=t("Tools.apply_modifier_on_shapekey_obj"),icon="SHAPEKEY_DATA") + col.operator(AvatarToolkit_OT_ExplodeMesh.bl_idname,text=t("Tools.explode_mesh"),icon="MOD_EXPLODE") # Standardization Tools From 69cc03098f29e02d29edaacad722426d41c67496 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 10 Apr 2025 23:40:51 +0100 Subject: [PATCH 6/6] PMX Import now works --- core/importers/importer.py | 40 + core/importers/pmx/__ini__.py | 0 core/lamp.py | 66 + core/mmd/__init__.py | 26 + core/mmd/{core => }/bpyutils.py | 240 ++- core/mmd/core/__init__.py | 6 + core/mmd/{ => core}/bone.py | 305 ++-- core/mmd/core/camera.py | 257 +++ core/mmd/core/exceptions.py | 14 + core/mmd/core/lamp.py | 69 + core/mmd/{ => core}/material.py | 239 +-- core/mmd/core/model.py | 1208 +++++++++++++ core/mmd/core/morph.py | 798 +++++++++ core/mmd/core/pmx/__init__.py | 1625 ++++++++++++++++++ core/{importers => mmd/core}/pmx/importer.py | 366 ++-- core/mmd/core/rigid_body.py | 290 ++++ core/mmd/core/sdef.py | 334 ++++ core/mmd/core/shader.py | 346 ++++ core/mmd/core/translations.py | 738 ++++++++ core/mmd/core/vmd/__init__.py | 6 + core/mmd/core/vmd/importer.py | 673 ++++++++ core/mmd/cycles_converter.py | 243 +++ core/mmd/operators/__init__.py | 6 + core/mmd/operators/material.py | 406 +++++ core/mmd/operators/misc.py | 310 ++++ core/mmd/operators/model.py | 486 ++++++ core/mmd/operators/model_edit.py | 313 ++++ core/mmd/operators/morph.py | 776 +++++++++ core/mmd/operators/rigid_body.py | 579 +++++++ core/mmd/operators/sdef.py | 110 ++ core/mmd/operators/translations.py | 336 ++++ core/mmd/operators/view.py | 150 ++ core/mmd/properties/__init__.py | 34 + core/mmd/properties/material.py | 287 ++++ core/mmd/properties/morph.py | 488 ++++++ core/mmd/properties/pose_bone.py | 200 +-- core/mmd/properties/rigid_body.py | 295 ++++ core/mmd/properties/root.py | 23 +- core/mmd/properties/translations.py | 127 ++ core/mmd/translations.py | 461 +++++ core/mmd/{core => }/utils.py | 228 ++- cycles_converter.py | 240 +++ 42 files changed, 12920 insertions(+), 824 deletions(-) delete mode 100644 core/importers/pmx/__ini__.py create mode 100644 core/lamp.py rename core/mmd/{core => }/bpyutils.py (64%) create mode 100644 core/mmd/core/__init__.py rename core/mmd/{ => core}/bone.py (72%) create mode 100644 core/mmd/core/camera.py create mode 100644 core/mmd/core/exceptions.py create mode 100644 core/mmd/core/lamp.py rename core/mmd/{ => core}/material.py (75%) create mode 100644 core/mmd/core/model.py create mode 100644 core/mmd/core/morph.py create mode 100644 core/mmd/core/pmx/__init__.py rename core/{importers => mmd/core}/pmx/importer.py (77%) create mode 100644 core/mmd/core/rigid_body.py create mode 100644 core/mmd/core/sdef.py create mode 100644 core/mmd/core/shader.py create mode 100644 core/mmd/core/translations.py create mode 100644 core/mmd/core/vmd/__init__.py create mode 100644 core/mmd/core/vmd/importer.py create mode 100644 core/mmd/cycles_converter.py create mode 100644 core/mmd/operators/__init__.py create mode 100644 core/mmd/operators/material.py create mode 100644 core/mmd/operators/misc.py create mode 100644 core/mmd/operators/model.py create mode 100644 core/mmd/operators/model_edit.py create mode 100644 core/mmd/operators/morph.py create mode 100644 core/mmd/operators/rigid_body.py create mode 100644 core/mmd/operators/sdef.py create mode 100644 core/mmd/operators/translations.py create mode 100644 core/mmd/operators/view.py create mode 100644 core/mmd/properties/material.py create mode 100644 core/mmd/properties/morph.py create mode 100644 core/mmd/properties/rigid_body.py create mode 100644 core/mmd/properties/translations.py create mode 100644 core/mmd/translations.py rename core/mmd/{core => }/utils.py (52%) create mode 100644 cycles_converter.py diff --git a/core/importers/importer.py b/core/importers/importer.py index feb8e93..237fc92 100644 --- a/core/importers/importer.py +++ b/core/importers/importer.py @@ -8,6 +8,7 @@ from bpy_extras.io_utils import ImportHelper from typing import Optional, Callable, Dict, List, Union, Set from ..common import clear_default_objects from ..translations import t +from ..mmd.core.pmx.importer import PMXImporter # Configure logging logging.basicConfig(level=logging.INFO) @@ -94,6 +95,12 @@ import_types: Dict[str, ImportMethod] = { files=files, directory=directory, filepath=filepath, automatic_bone_orientation=False, use_prepost_rot=False, use_anim=False ), + "pmx": lambda directory, files, filepath: import_multi_files( + directory=directory, + files=files, + filepath=filepath, + method=lambda directory, filepath: import_pmx_file(filepath) + ), "smd": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"), "dmx": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"), "gltf": lambda directory, files, filepath: bpy.ops.import_scene.gltf(files=files, filepath=filepath), @@ -193,3 +200,36 @@ class AvatarToolKit_OT_Import(Operator, ImportHelper): self.report({'INFO'}, t('Quick_Access.import_success')) return {'FINISHED'} +def import_pmx_file(filepath: str) -> None: + """ + Import a PMX file using the MMD Tools PMXImporter + + Args: + filepath: Path to the PMX file + """ + + # Default import settings + import_settings = { + "filepath": filepath, + "scale": 0.08, + "types": {"MESH", "ARMATURE", "MORPHS", "DISPLAY"}, + "clean_model": True, + "remove_doubles": False, + "fix_IK_links": True, + "ik_loop_factor": 3, + "use_mipmap": True, + "sph_blend_factor": 1.0, + "spa_blend_factor": 1.0, + "rename_LR_bones": False, + "use_underscore": False, + "apply_bone_fixed_axis": False, + } + + # Create and execute the importer + importer = PMXImporter() + try: + importer.execute(**import_settings) + logger.info(f"Successfully imported PMX file: {filepath}") + except Exception as e: + logger.error(f"Failed to import PMX file: {str(e)}", exc_info=True) + raise diff --git a/core/importers/pmx/__ini__.py b/core/importers/pmx/__ini__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/lamp.py b/core/lamp.py new file mode 100644 index 0000000..10593d3 --- /dev/null +++ b/core/lamp.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from ..bpyutils import FnContext, Props + + +class MMDLamp: + def __init__(self, obj): + if MMDLamp.isLamp(obj): + obj = obj.parent + if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT": + self.__emptyObj = obj + else: + raise ValueError("%s is not MMDLamp" % str(obj)) + + @staticmethod + def isLamp(obj): + return obj and obj.type in {"LIGHT", "LAMP"} + + @staticmethod + def isMMDLamp(obj): + if MMDLamp.isLamp(obj): + obj = obj.parent + return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT" + + @staticmethod + def convertToMMDLamp(lampObj, scale=1.0): + if MMDLamp.isMMDLamp(lampObj): + return MMDLamp(lampObj) + + empty = bpy.data.objects.new(name="MMD_Light", object_data=None) + FnContext.link_object(FnContext.ensure_context(), empty) + + empty.rotation_mode = "XYZ" + empty.lock_rotation = (True, True, True) + setattr(empty, Props.empty_display_size, 0.4) + empty.scale = [10 * scale] * 3 + empty.mmd_type = "LIGHT" + empty.location = (0, 0, 11 * scale) + + lampObj.parent = empty + lampObj.data.color = (0.602, 0.602, 0.602) + lampObj.location = (0.5, -0.5, 1.0) + lampObj.rotation_mode = "XYZ" + lampObj.rotation_euler = (0, 0, 0) + lampObj.lock_rotation = (True, True, True) + + constraint = lampObj.constraints.new(type="TRACK_TO") + constraint.name = "mmd_lamp_track" + constraint.target = empty + constraint.track_axis = "TRACK_NEGATIVE_Z" + constraint.up_axis = "UP_Y" + + return MMDLamp(empty) + + def object(self): + return self.__emptyObj + + def lamp(self): + for i in self.__emptyObj.children: + if MMDLamp.isLamp(i): + return i + raise KeyError diff --git a/core/mmd/__init__.py b/core/mmd/__init__.py index e69de29..af8a62d 100644 --- a/core/mmd/__init__.py +++ b/core/mmd/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import os +import tomllib + +# This is a temporary workaround i be changing how MMD Tools works later when it comes to getting version number. + +try: + + current_dir = os.path.dirname(os.path.abspath(__file__)) + root_dir = os.path.dirname(os.path.dirname(current_dir)) + manifest_path = os.path.join(root_dir, 'blender_manifest.toml') + + if os.path.exists(manifest_path): + with open(manifest_path, 'rb') as f: + manifest = tomllib.load(f) + AVATAR_TOOLKIT_VERSION = manifest.get('version', '0.2.1') + else: + AVATAR_TOOLKIT_VERSION = '0.2.1' +except Exception: + AVATAR_TOOLKIT_VERSION = '0.2.1' \ No newline at end of file diff --git a/core/mmd/core/bpyutils.py b/core/mmd/bpyutils.py similarity index 64% rename from core/mmd/core/bpyutils.py rename to core/mmd/bpyutils.py index 2800d3c..c5c9d76 100644 --- a/core/mmd/core/bpyutils.py +++ b/core/mmd/bpyutils.py @@ -1,27 +1,28 @@ # -*- coding: utf-8 -*- -# Copyright 2013 MMD Tools authors -# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit. -# All credit goes to the original authors. -# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed. -# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under. -# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/ +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import contextlib -from typing import Generator, List, Optional, TypeVar, Dict, Any, Set, Tuple, Type +from typing import Generator, List, Optional, TypeVar import bpy -from bpy.types import Object, Material, Context -from mathutils import Vector, Matrix -from ...logging_setup import logger -from ...addon_preferences import get_preference, save_preference + +class Props: # For API changes of only name changed properties + show_in_front = "show_in_front" + display_type = "display_type" + display_size = "display_size" + empty_display_type = "empty_display_type" + empty_display_size = "empty_display_size" class __EditMode: - """Context manager for edit mode operations""" - def __init__(self, obj: Object): + def __init__(self, obj): if not isinstance(obj, bpy.types.Object): - raise ValueError("Expected a Blender Object") + raise ValueError self.__prevMode = obj.mode self.__obj = obj self.__obj_select = obj.select_get() @@ -40,18 +41,17 @@ class __EditMode: class __SelectObjects: - """Context manager for object selection operations""" - def __init__(self, active_object: Object, selected_objects: Optional[List[Object]] = None): + def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None): if not isinstance(active_object, bpy.types.Object): - raise ValueError("Expected a Blender Object") + raise ValueError try: bpy.ops.object.mode_set(mode="OBJECT") except Exception: pass - context = FnContext.ensure_context() + contenxt = FnContext.ensure_context() - for i in context.selected_objects: + for i in contenxt.selected_objects: i.select_set(False) self.__active_object = active_object @@ -60,10 +60,10 @@ class __SelectObjects: self.__hides: List[bool] = [] for i in self.__selected_objects: self.__hides.append(i.hide_get()) - FnContext.select_object(context, i) - FnContext.set_active_object(context, active_object) + FnContext.select_object(contenxt, i) + FnContext.set_active_object(contenxt, active_object) - def __enter__(self) -> Object: + def __enter__(self) -> bpy.types.Object: return self.__active_object def __exit__(self, type, value, traceback): @@ -71,14 +71,12 @@ class __SelectObjects: i.hide_set(j) -def setParent(obj: Object, parent: Object) -> None: - """Set parent relationship between objects""" +def setParent(obj, parent): with select_object(parent, objects=[parent, obj]): bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False) -def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None: - """Set parent relationship to a specific bone""" +def setParentToBone(obj, parent, bone_name): with select_object(parent, objects=[parent, obj]): bpy.ops.object.mode_set(mode="POSE") parent.data.bones.active = parent.data.bones[bone_name] @@ -86,7 +84,7 @@ def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None: bpy.ops.object.mode_set(mode="OBJECT") -def edit_object(obj: Object): +def edit_object(obj): """Set the object interaction mode to 'EDIT' It is recommended to use 'edit_object' with 'with' statement like the following code. @@ -97,7 +95,7 @@ def edit_object(obj: Object): return __EditMode(obj) -def select_object(obj: Object, objects: Optional[List[Object]] = None): +def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None): """Select objects. It is recommended to use 'select_object' with 'with' statement like the following code. @@ -106,22 +104,20 @@ def select_object(obj: Object, objects: Optional[List[Object]] = None): with select_object(obj): some functions... """ + # TODO: Reimplement with bpy.context.temp_override (If it ain't broke, don't fix it.) return __SelectObjects(obj, objects) -def duplicateObject(obj: Object, total_len: int) -> List[Object]: - """Duplicate an object multiple times""" +def duplicateObject(obj, total_len): return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len) -def createObject(name: str = "Object", object_data: Optional[Any] = None, target_scene: Optional[Any] = None) -> Object: - """Create a new object and link it to the scene""" +def createObject(name="Object", object_data=None, target_scene=None): context = FnContext.ensure_context(target_scene) return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data)) -def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, target_object: Optional[Object] = None) -> Object: - """Create a sphere mesh object""" +def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None): import bmesh if target_object is None: @@ -142,8 +138,7 @@ def makeSphere(segment: int = 8, ring_count: int = 5, radius: float = 1.0, targe return target_object -def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optional[Object] = None) -> Object: - """Create a box mesh object""" +def makeBox(size=(1, 1, 1), target_object=None): import bmesh from mathutils import Matrix @@ -164,9 +159,9 @@ def makeBox(size: Tuple[float, float, float] = (1, 1, 1), target_object: Optiona return target_object -def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, height: float = 1.0, target_object: Optional[Object] = None) -> Object: - """Create a capsule mesh object""" +def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None): import math + import bmesh if target_object is None: @@ -179,6 +174,7 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig top = (0, 0, height / 2 + radius) verts.new(top) + # f = lambda i: radius*i/ring_count f = lambda i: radius * math.sin(0.5 * math.pi * i / ring_count) for i in range(ring_count, 0, -1): z = f(i - 1) @@ -228,12 +224,10 @@ def makeCapsule(segment: int = 8, ring_count: int = 2, radius: float = 1.0, heig class TransformConstraintOp: - """Helper class for transform constraints""" __MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"} @staticmethod - def create(constraints, name: str, map_type: str): - """Create a transform constraint""" + def create(constraints, name, map_type): c = constraints.get(name, None) if c and c.type != "TRANSFORM": constraints.remove(c) @@ -251,8 +245,7 @@ class TransformConstraintOp: return c @classmethod - def min_max_attributes(cls, map_type: str, name_id: str = "") -> Tuple[str, ...]: - """Get min/max attribute names for a constraint type""" + def min_max_attributes(cls, map_type, name_id=""): key = (map_type, name_id) ret = cls.__MIN_MAX_MAP.get(key, None) if ret is None: @@ -262,8 +255,7 @@ class TransformConstraintOp: return ret @classmethod - def update_min_max(cls, constraint, value: float, influence: Optional[float] = 1): - """Update min/max values for a constraint""" + def update_min_max(cls, constraint, value, influence=1): c = constraint if not c or c.type != "TRANSFORM": return @@ -283,19 +275,18 @@ class TransformConstraintOp: class FnObject: - """Function collection for object operations""" def __init__(self): raise NotImplementedError("This class is not expected to be instantiated.") @staticmethod - def mesh_remove_shape_key(mesh_object: Object, shape_key: bpy.types.ShapeKey) -> None: - """Remove a shape key from a mesh object, cleaning up drivers""" + def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey): assert isinstance(mesh_object.data, bpy.types.Mesh) key: bpy.types.Key = shape_key.id_data assert key == mesh_object.data.shape_keys if mesh_object.animation_data is not None: + fc_curve: bpy.types.FCurve for fc_curve in mesh_object.animation_data.drivers: if not fc_curve.data_path.startswith(shape_key.path_from_id()): continue @@ -311,52 +302,42 @@ class FnObject: mesh_object.active_shape_key_index = min(last_index, len(key_blocks) - 1) -T = TypeVar("T") +ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = TypeVar("ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE") class FnContext: - """Function collection for context operations""" def __init__(self): raise NotImplementedError("This class is not expected to be instantiated.") @staticmethod - def ensure_context(context: Optional[Context] = None) -> Context: - """Get a valid context, using bpy.context if none provided""" + def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context: return context or bpy.context @staticmethod - def get_active_object(context: Context) -> Optional[Object]: - """Get the active object from context safely""" - if context is None or not hasattr(context, 'active_object'): - return None + def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]: return context.active_object @staticmethod - def set_active_object(context: Context, obj: Object) -> Object: - """Set the active object in context""" + def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: context.view_layer.objects.active = obj return obj @staticmethod - def set_active_and_select_single_object(context: Context, obj: Object) -> Object: - """Set an object as active and the only selected object""" + def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: return FnContext.set_active_object(context, FnContext.select_single_object(context, obj)) @staticmethod - def get_scene_objects(context: Context) -> List[Object]: - """Get all objects in the scene safely""" - if context is None or not hasattr(context, 'scene') or not hasattr(context.scene, 'objects'): - return [] + def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects: return context.scene.objects @staticmethod - def ensure_selectable(context: Context, obj: Object) -> Object: - """Make sure an object is selectable by unhiding it and its collections""" + def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: obj.hide_viewport = False obj.hide_select = False obj.hide_set(False) if obj not in context.selectable_objects: + def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool: for lc in layer_collection.children: if __layer_check(lc): @@ -379,47 +360,47 @@ class FnContext: return obj @staticmethod - def select_object(context: Context, obj: Object) -> Object: - """Select an object in the context""" + def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: FnContext.ensure_selectable(context, obj).select_set(True) return obj @staticmethod - def select_objects(context: Context, *objects: Object) -> List[Object]: - """Select multiple objects in the context""" + def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]: return [FnContext.select_object(context, obj) for obj in objects] @staticmethod - def select_single_object(context: Context, obj: Object) -> Object: - """Select only the specified object, deselecting all others""" + def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: for i in context.selected_objects: if i != obj: i.select_set(False) return FnContext.select_object(context, obj) @staticmethod - def link_object(context: Context, obj: Object) -> Object: - """Link an object to the active collection""" + def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: context.collection.objects.link(obj) return obj @staticmethod - def new_and_link_object(context: Context, name: str, object_data: Optional[Any]) -> Object: - """Create a new object and link it to the active collection""" + def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object: return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data)) @staticmethod - def duplicate_object(context: Context, object_to_duplicate: Object, target_count: int) -> List[Object]: + def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]: """ - Duplicate an object to reach the target count. - + Duplicate object. + + This function duplicates the given object and returns a list of duplicated objects. + Args: - context: The context in which the duplication is performed - object_to_duplicate: The object to be duplicated - target_count: The desired count of duplicated objects - + context (bpy.types.Context): The context in which the duplication is performed. + object_to_duplicate (bpy.types.Object): The object to be duplicated. + target_count (int): The desired count of duplicated objects. + Returns: - A list of duplicated objects + List[bpy.types.Object]: A list of duplicated objects. + + Raises: + AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated. """ for o in context.selected_objects: o.select_set(False) @@ -443,16 +424,16 @@ class FnContext: return result_objects @staticmethod - def find_user_layer_collection_by_object(context: Context, target_object: Object) -> Optional[bpy.types.LayerCollection]: + def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]: """ - Find the layer collection containing the target object. - + Finds the layer collection that contains the given target_object in the user's collections. + Args: - context: The Blender context - target_object: The target object to find the layer collection for - + context (bpy.types.Context): The Blender context. + target_object (bpy.types.Object): The target object to find the layer collection for. + Returns: - The layer collection containing the target object, or None if not found + Optional[bpy.types.LayerCollection]: The layer collection that contains the target_object, or None if not found. """ scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection @@ -460,6 +441,7 @@ class FnContext: if layer_collection.name == name: return layer_collection + child_layer_collection: bpy.types.LayerCollection for child_layer_collection in layer_collection.children: found = find_layer_collection_by_name(child_layer_collection, name) if found is not None: @@ -467,6 +449,7 @@ class FnContext: return None + user_collection: bpy.types.Collection for user_collection in target_object.users_collection: found = find_layer_collection_by_name(scene_layer_collection, user_collection.name) if found is not None: @@ -476,16 +459,27 @@ class FnContext: @staticmethod @contextlib.contextmanager - def temp_override_active_layer_collection(context: Context, target_object: Object) -> Generator[Context, None, None]: + def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]: """ - Temporarily override the active layer collection to the one containing the target object. - + Context manager to temporarily override the active_layer_collection that contains the target object. + + This context manager allows you to temporarily change the active_layer_collection in the given context to the one that contains the target object. + It ensures that the original active_layer_collection is restored after the context is exited. + Args: - context: The context to modify - target_object: The object whose collection should become active - + context (bpy.types.Context): The context in which the active_layer_collection will be overridden. + target_object (bpy.types.Object): The target object whose layer collection will be set as the active_layer_collection. + Yields: - The modified context + bpy.types.Context: The modified context with the active_layer_collection overridden. + + Example: + with FnContext.temp_override_active_layer_collection(context, target_object): + # Perform operations with the modified context + bpy.ops.object.select_all(action='DESELECT') + target_object.select_set(True) + bpy.ops.object.delete() + """ original_layer_collection = context.view_layer.active_layer_collection target_layer_collection = FnContext.find_user_layer_collection_by_object(context, target_object) @@ -498,36 +492,30 @@ class FnContext: context.view_layer.active_layer_collection = original_layer_collection @staticmethod - @contextlib.contextmanager + def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]: + addon: bpy.types.Addon = context.preferences.addons.get(__package__, None) + return addon.preferences if addon else None + + @staticmethod + def get_addon_preferences_attribute(context: bpy.types.Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE: + return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value) + + @staticmethod def temp_override_objects( - context: 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 = {} - + context: bpy.types.Context, + window: Optional[bpy.types.Window] = None, + area: Optional[bpy.types.Area] = None, + region: Optional[bpy.types.Region] = None, + active_object: Optional[bpy.types.Object] = None, + selected_objects: Optional[List[bpy.types.Object]] = None, + **keywords, + ) -> Generator[bpy.types.Context, None, None]: if active_object is not None: - override_dict["active_object"] = active_object - override_dict["object"] = active_object + keywords["active_object"] = active_object + keywords["object"] = active_object if selected_objects is not None: - override_dict["selected_objects"] = selected_objects - override_dict["selected_editable_objects"] = selected_objects - - override_dict.update(keywords) - - with context.temp_override(**override_dict) as override_context: - yield override_context + keywords["selected_objects"] = selected_objects + keywords["selected_editable_objects"] = selected_objects - @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) \ No newline at end of file + return context.temp_override(window=window, area=area, region=region, **keywords) diff --git a/core/mmd/core/__init__.py b/core/mmd/core/__init__.py new file mode 100644 index 0000000..f3342f2 --- /dev/null +++ b/core/mmd/core/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. \ No newline at end of file diff --git a/core/mmd/bone.py b/core/mmd/core/bone.py similarity index 72% rename from core/mmd/bone.py rename to core/mmd/core/bone.py index 7ac61f2..73fa58c 100644 --- a/core/mmd/bone.py +++ b/core/mmd/core/bone.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright 2013 MMD Tools authors -# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit. -# All credit goes to the original authors. -# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed. -# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under. -# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/ +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import math from typing import TYPE_CHECKING, Iterable, Optional, Set @@ -12,19 +11,13 @@ from typing import TYPE_CHECKING, Iterable, Optional, Set import bpy from mathutils import Vector -from ..logging_setup import logger -from .. import common -from ..common import ProgressTracker +from .. import bpyutils from ..bpyutils import TransformConstraintOp +from ..utils import ItemOp -# Constants for bone collections -BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools" -BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection" -BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection" -BONE_COLLECTION_NAME_SHADOW = "mmd_shadow" -BONE_COLLECTION_NAME_DUMMY = "mmd_dummy" - -SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NAME_DUMMY] +if TYPE_CHECKING: + from ..properties.root import MMDRoot, MMDDisplayItemFrame + from ..properties.pose_bone import MMDBone def remove_constraint(constraints, name): @@ -42,6 +35,15 @@ def remove_edit_bones(edit_bones, bone_names): edit_bones.remove(b) +BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools" +BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection" +BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection" +BONE_COLLECTION_NAME_SHADOW = "mmd_shadow" +BONE_COLLECTION_NAME_DUMMY = "mmd_dummy" + +SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NAME_DUMMY] + + class FnBone: AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首") AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指") @@ -77,6 +79,23 @@ class FnBone: bones = armature_object.pose.bones return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone) + @staticmethod + def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True): + for b in FnBone.__get_selected_pose_bones(armature_object): + mmd_bone: MMDBone = b.mmd_bone + mmd_bone.enabled_fixed_axis = enable + lock_rotation = b.lock_rotation[:] + if enable: + axes = b.bone.matrix_local.to_3x3().transposed() + if lock_rotation.count(False) == 1: + mmd_bone.fixed_axis = axes[lock_rotation.index(False)].xzy + else: + mmd_bone.fixed_axis = axes[1].xzy # Y-axis + elif all(b.lock_location) and lock_rotation.count(True) > 1 and lock_rotation == (b.lock_ik_x, b.lock_ik_y, b.lock_ik_z): + # unlock transform locks if fixed axis was applied + b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = (False, False, False) + b.lock_location = b.lock_scale = (False, False, False) + @staticmethod def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object: armature: bpy.types.Armature = armature_object.data @@ -217,81 +236,59 @@ class FnBone: display_item_frames.remove(i) mmd_root.active_display_item_frame = 0 - @staticmethod - def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True): - for b in FnBone.__get_selected_pose_bones(armature_object): - mmd_bone = b.mmd_bone - mmd_bone.enabled_fixed_axis = enable - lock_rotation = b.lock_rotation[:] - if enable: - axes = b.bone.matrix_local.to_3x3().transposed() - if lock_rotation.count(False) == 1: - mmd_bone.fixed_axis = axes[lock_rotation.index(False)].xzy - else: - mmd_bone.fixed_axis = axes[1].xzy # Y-axis - elif all(b.lock_location) and lock_rotation.count(True) > 1 and lock_rotation == (b.lock_ik_x, b.lock_ik_y, b.lock_ik_z): - # unlock transform locks if fixed axis was applied - b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = (False, False, False) - b.lock_location = b.lock_scale = (False, False, False) - @staticmethod def apply_bone_fixed_axis(armature_object: bpy.types.Object): - with ProgressTracker(bpy.context, 100, "Applying Bone Fixed Axis") as progress: - bone_map = {} - for b in armature_object.pose.bones: - if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis: - continue - mmd_bone = b.mmd_bone - 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") + bone_map = {} + for b in armature_object.pose.bones: + if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis: + continue + mmd_bone: MMDBone = b.mmd_bone + parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip + bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip) - force_align = True - with 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 + force_align = True + with bpyutils.edit_object(armature_object) as data: + bone: bpy.types.EditBone + for bone in data.edit_bones: + if bone.name not in bone_map: + bone.select = False + continue + fixed_axis, is_tip, parent_tip = bone_map[bone.name] + if fixed_axis.length: + axes = [bone.x_axis, bone.y_axis, bone.z_axis] + direction = fixed_axis.normalized().xzy + idx, val = max([(i, direction.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1])) + idx_1, idx_2 = (idx + 1) % 3, (idx + 2) % 3 + axes[idx] = -direction if val < 0 else direction + axes[idx_2] = axes[idx].cross(axes[idx_1]) + axes[idx_1] = axes[idx_2].cross(axes[idx]) + if parent_tip and bone.use_connect: + bone.use_connect = False + bone.head = bone.parent.head + if force_align: + tail = bone.head + axes[1].normalized() * bone.length + if is_tip or (tail - bone.tail).length > 1e-4: + for c in bone.children: + if c.use_connect: + c.use_connect = False + if is_tip: + c.head = bone.head + bone.tail = tail + bone.align_roll(axes[2]) + bone_map[bone.name] = tuple(i != idx for i in range(3)) + else: + bone_map[bone.name] = (True, True, True) + bone.select = True + + for bone_name, locks in bone_map.items(): + b = armature_object.pose.bones[bone_name] + b.lock_location = (True, True, True) + b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks @staticmethod def load_bone_local_axes(armature_object: bpy.types.Object, enable=True): for b in FnBone.__get_selected_pose_bones(armature_object): - mmd_bone = b.mmd_bone + mmd_bone: MMDBone = b.mmd_bone mmd_bone.enabled_local_axes = enable if enable: axes = b.bone.matrix_local.to_3x3().transposed() @@ -300,25 +297,22 @@ class FnBone: @staticmethod def apply_bone_local_axes(armature_object: bpy.types.Object): - with ProgressTracker(bpy.context, 100, "Applying Bone Local Axes") as progress: - bone_map = {} - for b in armature_object.pose.bones: - if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes: + bone_map = {} + for b in armature_object.pose.bones: + if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes: + continue + mmd_bone: MMDBone = b.mmd_bone + bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z) + + with bpyutils.edit_object(armature_object) as data: + bone: bpy.types.EditBone + for bone in data.edit_bones: + if bone.name not in bone_map: + bone.select = False continue - 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 + 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): @@ -336,21 +330,17 @@ class FnBone: @staticmethod def apply_auto_bone_roll(armature): - with ProgressTracker(bpy.context, 100, "Applying Auto Bone Roll") as progress: - bone_names = [] - for b in armature.pose.bones: - if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j): - bone_names.append(b.name) - - progress.step("Processing bones") - - with common.edit_object(armature) as data: - 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 + bone_names = [] + for b in armature.pose.bones: + if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j): + bone_names.append(b.name) + with bpyutils.edit_object(armature) as data: + bone: bpy.types.EditBone + for bone in data.edit_bones: + if bone.name not in bone_names: + continue + FnBone.update_auto_bone_roll(bone) + bone.select = True @staticmethod def update_auto_bone_roll(edit_bone): @@ -385,8 +375,6 @@ class FnBone: @staticmethod def clean_additional_transformation(armature_object: bpy.types.Object): - logger.info(f"Cleaning additional transformations for {armature_object.name}") - # clean constraints p_bone: bpy.types.PoseBone for p_bone in armature_object.pose.bones: @@ -396,7 +384,6 @@ class FnBone: remove_constraint(constraints, "mmd_additional_location") if remove_constraint(constraints, "mmd_additional_parent"): p_bone.bone.use_inherit_rotation = True - # clean shadow bones shadow_bone_types = { "DUMMY", @@ -410,48 +397,41 @@ class FnBone: shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)] if len(shadow_bone_names) > 0: - with common.edit_object(armature_object) as data: + with bpyutils.edit_object(armature_object) as data: remove_edit_bones(data.edit_bones, shadow_bone_names) @staticmethod def apply_additional_transformation(armature_object: bpy.types.Object): - with ProgressTracker(bpy.context, 100, "Applying Additional Transformations") as progress: - def __is_dirty_bone(b): - if b.is_mmd_shadow_bone: - return False - 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 + 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) + dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)] - 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) + # setup constraints + shadow_bone_pool = [] + for p_bone in dirty_bones: + sb = FnBone.__setup_constraints(p_bone) + if sb: + shadow_bone_pool.append(sb) - pose_bones = armature_object.pose.bones + # setup shadow bones + with bpyutils.edit_object(armature_object) as data: + edit_bones = data.edit_bones for sb in shadow_bone_pool: - sb.update_pose_bones(pose_bones) + sb.update_edit_bones(edit_bones) - progress.step("Finalizing") - - # finish - for p_bone in dirty_bones: - p_bone.mmd_bone.is_additional_transform_dirty = False + pose_bones = armature_object.pose.bones + for sb in shadow_bone_pool: + sb.update_pose_bones(pose_bones) + + # finish + for p_bone in dirty_bones: + p_bone.mmd_bone.is_additional_transform_dirty = False @staticmethod def __setup_constraints(p_bone): @@ -459,7 +439,7 @@ class FnBone: mmd_bone = p_bone.mmd_bone influence = mmd_bone.additional_transform_influence target_bone = mmd_bone.additional_transform_bone - mute_rotation = not mmd_bone.has_additional_rotation + mute_rotation = not mmd_bone.has_additional_rotation # or p_bone.is_in_ik_chain mute_location = not mmd_bone.has_additional_location constraints = p_bone.constraints @@ -501,15 +481,12 @@ class MigrationFnBone: @staticmethod def fix_mmd_ik_limit_override(armature_object: bpy.types.Object): - with ProgressTracker(bpy.context, 100, "Fixing MMD IK Limit Override") as progress: - pose_bone: bpy.types.PoseBone - for pose_bone in armature_object.pose.bones: - constraint: bpy.types.Constraint - 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") + pose_bone: bpy.types.PoseBone + for pose_bone in armature_object.pose.bones: + constraint: bpy.types.Constraint + for constraint in pose_bone.constraints: + if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name: + constraint.owner_space = "LOCAL" class _AT_ShadowBoneRemove: diff --git a/core/mmd/core/camera.py b/core/mmd/core/camera.py new file mode 100644 index 0000000..9c5b2bd --- /dev/null +++ b/core/mmd/core/camera.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import math +from typing import Optional + +import bpy + +from ..bpyutils import FnContext, Props + + +class FnCamera: + @staticmethod + def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]: + if obj is None: + return None + if FnCamera.is_mmd_camera_root(obj): + return obj + if obj.parent is not None and FnCamera.is_mmd_camera_root(obj.parent): + return obj.parent + return None + + @staticmethod + def is_mmd_camera(obj: bpy.types.Object) -> bool: + return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None + + @staticmethod + def is_mmd_camera_root(obj: bpy.types.Object) -> bool: + return obj.type == "EMPTY" and obj.mmd_type == "CAMERA" + + @staticmethod + def add_drivers(camera_object: bpy.types.Object): + def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1): + d = id_data.driver_add(data_path, index).driver + d.type = "SCRIPTED" + if "$empty_distance" in expression: + v = d.variables.new() + v.name = "empty_distance" + v.type = "TRANSFORMS" + v.targets[0].id = camera_object + v.targets[0].transform_type = "LOC_Y" + v.targets[0].transform_space = "LOCAL_SPACE" + expression = expression.replace("$empty_distance", v.name) + if "$is_perspective" in expression: + v = d.variables.new() + v.name = "is_perspective" + v.type = "SINGLE_PROP" + v.targets[0].id_type = "OBJECT" + v.targets[0].id = camera_object.parent + v.targets[0].data_path = "mmd_camera.is_perspective" + expression = expression.replace("$is_perspective", v.name) + if "$angle" in expression: + v = d.variables.new() + v.name = "angle" + v.type = "SINGLE_PROP" + v.targets[0].id_type = "OBJECT" + v.targets[0].id = camera_object.parent + v.targets[0].data_path = "mmd_camera.angle" + expression = expression.replace("$angle", v.name) + if "$sensor_height" in expression: + v = d.variables.new() + v.name = "sensor_height" + v.type = "SINGLE_PROP" + v.targets[0].id_type = "CAMERA" + v.targets[0].id = camera_object.data + v.targets[0].data_path = "sensor_height" + expression = expression.replace("$sensor_height", v.name) + + d.expression = expression + + __add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45") + __add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1) + __add_driver(camera_object.data, "type", "not $is_perspective") + __add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2") + + @staticmethod + def remove_drivers(camera_object: bpy.types.Object): + camera_object.data.driver_remove("ortho_scale") + camera_object.driver_remove("rotation_euler") + camera_object.data.driver_remove("ortho_scale") + camera_object.data.driver_remove("lens") + + +class MigrationFnCamera: + @staticmethod + def update_mmd_camera(): + for camera_object in bpy.data.objects: + if camera_object.type != "CAMERA": + continue + + root_object = FnCamera.find_root(camera_object) + if root_object is None: + # It's not a MMD Camera + continue + + FnCamera.remove_drivers(camera_object) + FnCamera.add_drivers(camera_object) + + +class MMDCamera: + def __init__(self, obj): + root_object = FnCamera.find_root(obj) + if root_object is None: + raise ValueError("%s is not MMDCamera" % str(obj)) + + self.__emptyObj = getattr(root_object, "original", obj) + + @staticmethod + def isMMDCamera(obj: bpy.types.Object) -> bool: + return FnCamera.find_root(obj) is not None + + @staticmethod + def addDrivers(cameraObj: bpy.types.Object): + FnCamera.add_drivers(cameraObj) + + @staticmethod + def removeDrivers(cameraObj: bpy.types.Object): + if cameraObj.type != "CAMERA": + return + FnCamera.remove_drivers(cameraObj) + + @staticmethod + def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0): + if FnCamera.is_mmd_camera(cameraObj): + return MMDCamera(cameraObj) + + empty = bpy.data.objects.new(name="MMD_Camera", object_data=None) + FnContext.link_object(FnContext.ensure_context(), empty) + + cameraObj.parent = empty + cameraObj.data.sensor_fit = "VERTICAL" + cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV + cameraObj.data.ortho_scale = 25 * scale + cameraObj.data.clip_end = 500 * scale + setattr(cameraObj.data, Props.display_size, 5 * scale) + cameraObj.location = (0, -45 * scale, 0) + cameraObj.rotation_mode = "XYZ" + cameraObj.rotation_euler = (math.radians(90), 0, 0) + cameraObj.lock_location = (True, False, True) + cameraObj.lock_rotation = (True, True, True) + cameraObj.lock_scale = (True, True, True) + cameraObj.data.dof.focus_object = empty + FnCamera.add_drivers(cameraObj) + + empty.location = (0, 0, 10 * scale) + empty.rotation_mode = "YXZ" + setattr(empty, Props.empty_display_size, 5 * scale) + empty.lock_scale = (True, True, True) + empty.mmd_type = "CAMERA" + empty.mmd_camera.angle = math.radians(30) + empty.mmd_camera.persp = True + return MMDCamera(empty) + + @staticmethod + def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1): + scene = bpy.context.scene + mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera")) + FnContext.link_object(FnContext.ensure_context(), mmd_cam) + MMDCamera.convertToMMDCamera(mmd_cam, scale=scale) + mmd_cam_root = mmd_cam.parent + + _camera_override_func = None + if cameraObj is None: + if scene.camera is None: + scene.camera = mmd_cam + return MMDCamera(mmd_cam_root) + _camera_override_func = lambda: scene.camera + + _target_override_func = None + if cameraTarget is None: + _target_override_func = lambda camObj: camObj.data.dof.focus_object or camObj + + action_name = mmd_cam_root.name + parent_action = bpy.data.actions.new(name=action_name) + distance_action = bpy.data.actions.new(name=action_name + "_dis") + FnCamera.remove_drivers(mmd_cam) + + from math import atan + + from mathutils import Matrix, Vector + + render = scene.render + factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x) + matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1])) + neg_z_vector = Vector((0, 0, -1)) + frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end + 1, scene.frame_current + frame_count = frame_end - frame_start + frames = range(frame_start, frame_end) + + fcurves = [] + for i in range(3): + fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z + for i in range(3): + fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz + fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov + fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp + fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis + for c in fcurves: + c.keyframe_points.add(frame_count) + + for f, x, y, z, rx, ry, rz, fov, persp, dis in zip(frames, *(c.keyframe_points for c in fcurves)): + scene.frame_set(f) + if _camera_override_func: + cameraObj = _camera_override_func() + if _target_override_func: + cameraTarget = _target_override_func(cameraObj) + cam_matrix_world = cameraObj.matrix_world + cam_target_loc = cameraTarget.matrix_world.translation + cam_rotation = (cam_matrix_world @ matrix_rotation).to_euler(mmd_cam_root.rotation_mode) + cam_vec = cam_matrix_world.to_3x3() @ neg_z_vector + if cameraObj.data.type == "ORTHO": + cam_dis = -(9 / 5) * cameraObj.data.ortho_scale + if cameraObj.data.sensor_fit != "VERTICAL": + if cameraObj.data.sensor_fit == "HORIZONTAL": + cam_dis *= factor + else: + cam_dis *= min(1, factor) + else: + target_vec = cam_target_loc - cam_matrix_world.translation + cam_dis = -max(target_vec.length * cam_vec.dot(target_vec.normalized()), min_distance) + cam_target_loc = cam_matrix_world.translation - cam_vec * cam_dis + + tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2 + if cameraObj.data.sensor_fit != "VERTICAL": + ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height + if cameraObj.data.sensor_fit == "HORIZONTAL": + tan_val *= factor * ratio + else: # cameraObj.data.sensor_fit == 'AUTO' + tan_val *= min(ratio, factor * ratio) + + x.co, y.co, z.co = ((f, i) for i in cam_target_loc) + rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation) + dis.co = (f, cam_dis) + fov.co = (f, 2 * atan(tan_val)) + persp.co = (f, cameraObj.data.type != "ORTHO") + persp.interpolation = "CONSTANT" + for kp in (x, y, z, rx, ry, rz, fov, dis): + kp.interpolation = "LINEAR" + + FnCamera.add_drivers(mmd_cam) + mmd_cam_root.animation_data_create().action = parent_action + mmd_cam.animation_data_create().action = distance_action + scene.frame_set(frame_current) + return MMDCamera(mmd_cam_root) + + def object(self): + return self.__emptyObj + + def camera(self): + for i in self.__emptyObj.children: + if i.type == "CAMERA": + return i + raise KeyError diff --git a/core/mmd/core/exceptions.py b/core/mmd/core/exceptions.py new file mode 100644 index 0000000..c89366a --- /dev/null +++ b/core/mmd/core/exceptions.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + + +class MaterialNotFoundError(KeyError): + """Exception raised when a material is not found in the scene""" + + def __init__(self, *args: object) -> None: + """Constructor for MaterialNotFoundError""" + super().__init__(*args) diff --git a/core/mmd/core/lamp.py b/core/mmd/core/lamp.py new file mode 100644 index 0000000..549a83b --- /dev/null +++ b/core/mmd/core/lamp.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import bpy + +from ..bpyutils import FnContext, Props + + +class MMDLamp: + def __init__(self, obj): + if MMDLamp.isLamp(obj): + obj = obj.parent + if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT": + self.__emptyObj = obj + else: + raise ValueError("%s is not MMDLamp" % str(obj)) + + @staticmethod + def isLamp(obj): + return obj and obj.type in {"LIGHT", "LAMP"} + + @staticmethod + def isMMDLamp(obj): + if MMDLamp.isLamp(obj): + obj = obj.parent + return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT" + + @staticmethod + def convertToMMDLamp(lampObj, scale=1.0): + if MMDLamp.isMMDLamp(lampObj): + return MMDLamp(lampObj) + + empty = bpy.data.objects.new(name="MMD_Light", object_data=None) + FnContext.link_object(FnContext.ensure_context(), empty) + + empty.rotation_mode = "XYZ" + empty.lock_rotation = (True, True, True) + setattr(empty, Props.empty_display_size, 0.4) + empty.scale = [10 * scale] * 3 + empty.mmd_type = "LIGHT" + empty.location = (0, 0, 11 * scale) + + lampObj.parent = empty + lampObj.data.color = (0.602, 0.602, 0.602) + lampObj.location = (0.5, -0.5, 1.0) + lampObj.rotation_mode = "XYZ" + lampObj.rotation_euler = (0, 0, 0) + lampObj.lock_rotation = (True, True, True) + + constraint = lampObj.constraints.new(type="TRACK_TO") + constraint.name = "mmd_lamp_track" + constraint.target = empty + constraint.track_axis = "TRACK_NEGATIVE_Z" + constraint.up_axis = "UP_Y" + + return MMDLamp(empty) + + def object(self): + return self.__emptyObj + + def lamp(self): + for i in self.__emptyObj.children: + if MMDLamp.isLamp(i): + return i + raise KeyError diff --git a/core/mmd/material.py b/core/mmd/core/material.py similarity index 75% rename from core/mmd/material.py rename to core/mmd/core/material.py index 576e212..68fba09 100644 --- a/core/mmd/material.py +++ b/core/mmd/core/material.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright 2013 MMD Tools authors -# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit. -# All credit goes to the original authors. -# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed. -# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under. -# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/ +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import logging import os @@ -13,27 +12,40 @@ from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast import bpy from mathutils import Vector -from ..logging_setup import logger +from ..bpyutils import FnContext from .exceptions import MaterialNotFoundError from .shader import _NodeGroupUtils if TYPE_CHECKING: from ..properties.material import MMDMaterial -# Constants for sphere modes +# TODO: use enum instead of constants SPHERE_MODE_OFF = 0 SPHERE_MODE_MULT = 1 SPHERE_MODE_ADD = 2 SPHERE_MODE_SUBTEX = 3 +class _DummyTexture: + def __init__(self, image): + self.type = "IMAGE" + self.image = image + self.use_mipmap = True + + +class _DummyTextureSlot: + def __init__(self, image): + self.diffuse_color_factor = 1 + self.uv_layer = "" + self.texture = _DummyTexture(image) + + class FnMaterial: __NODES_ARE_READONLY: bool = False def __init__(self, material: bpy.types.Material): self.__material = material self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY - logger.debug(f"Initializing FnMaterial for {material.name}") @staticmethod def set_nodes_are_readonly(nodes_are_readonly: bool): @@ -115,7 +127,7 @@ class FnMaterial: @property def material_id(self): - mmd_mat = self.__material.mmd_material + mmd_mat: MMDMaterial = self.__material.mmd_material if mmd_mat.material_id < 0: max_id = -1 for mat in bpy.data.materials: @@ -129,9 +141,11 @@ class FnMaterial: def __same_image_file(self, image, filepath): if image and image.source == "FILE": - img_filepath = bpy.path.abspath(image.filepath) + # pylint: disable=assignment-from-no-return + img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user() if img_filepath == filepath: return True + # pylint: disable=bare-except try: return os.path.samefile(img_filepath, filepath) except: @@ -141,34 +155,28 @@ class FnMaterial: def _load_image(self, filepath): img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None) if img is None: + # pylint: disable=bare-except try: img = bpy.data.images.load(filepath) - logger.debug(f"Loaded image from {filepath}") except: - logger.warning(f"Cannot create a texture for {filepath}. No such file.") + logging.warning("Cannot create a texture for %s. No such file.", filepath) img = bpy.data.images.new(os.path.basename(filepath), 1, 1) img.source = "FILE" img.filepath = filepath - # For Blender 4.4+ - if img.depth == 32 and img.file_format != "BMP": - img.alpha_mode = "CHANNEL_PACKED" - else: + use_alpha = img.depth == 32 and img.file_format != "BMP" + if hasattr(img, "use_alpha"): + img.use_alpha = use_alpha + elif not use_alpha: img.alpha_mode = "NONE" return img def update_toon_texture(self): if self._nodes_are_readonly: return - mmd_mat = self.__material.mmd_material + mmd_mat: MMDMaterial = self.__material.mmd_material if mmd_mat.is_shared_toon_texture: - # Get shared toon folder from preferences - context = bpy.context - addon_prefs = context.preferences.addons.get("avatar_toolkit", None) - if addon_prefs: - shared_toon_folder = addon_prefs.preferences.shared_toon_folder - else: - shared_toon_folder = "" - toon_path = os.path.join(shared_toon_folder, f"toon{mmd_mat.shared_toon_texture + 1:02d}.bmp") + shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "") + toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1)) self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path)) elif mmd_mat.toon_texture != "": self.create_toon_texture(mmd_mat.toon_texture) @@ -192,15 +200,13 @@ class FnMaterial: if self._nodes_are_readonly: return mat = self.__material - mmd_mat = mat.mmd_material + mmd_mat: MMDMaterial = mat.mmd_material color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3] line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),) - - # For Blender 4.4+ if hasattr(mat, "line_color"): # freestyle line color mat.line_color = line_color - mat_edge = bpy.data.materials.get("mmd_edge." + mat.name, None) + mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None) if mat_edge: mat_edge.mmd_material.edge_color = line_color @@ -216,11 +222,11 @@ class FnMaterial: pass def get_texture(self): - return self.__get_texture_node("mmd_base_tex") + return self.__get_texture_node("mmd_base_tex", use_dummy=True) def create_texture(self, filepath): texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1)) - return texture + return _DummyTextureSlot(texture.image) def remove_texture(self): if self._nodes_are_readonly: @@ -228,7 +234,7 @@ class FnMaterial: self.__remove_texture_node("mmd_base_tex") def get_sphere_texture(self): - return self.__get_texture_node("mmd_sphere_tex") + return self.__get_texture_node("mmd_sphere_tex", use_dummy=True) def use_sphere_texture(self, use_sphere, obj=None): if self._nodes_are_readonly: @@ -241,7 +247,7 @@ class FnMaterial: def create_sphere_texture(self, filepath, obj=None): texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2)) self.update_sphere_texture_type(obj) - return texture + return _DummyTextureSlot(texture.image) def update_sphere_texture_type(self, obj=None): if self._nodes_are_readonly: @@ -258,8 +264,10 @@ class FnMaterial: texture = self.__get_texture_node("mmd_sphere_tex") if texture and (not texture.inputs["Vector"].is_linked or texture.inputs["Vector"].links[0].from_node.name == "mmd_tex_uv"): - # For Blender 4.4+ - texture.image.colorspace_settings.name = "Linear Rec.709" if is_sph_add else "sRGB" + if hasattr(texture, "color_space"): + texture.color_space = "NONE" if is_sph_add else "COLOR" + elif hasattr(texture.image, "colorspace_settings"): + texture.image.colorspace_settings.name = "Linear Rec.709" if is_sph_add else "sRGB" mat = self.material nodes, links = mat.node_tree.nodes, mat.node_tree.links @@ -269,7 +277,7 @@ class FnMaterial: next(uv_layers, None) # skip base UV subtex_uv = getattr(next(uv_layers, None), "name", "") if subtex_uv != "UV1": - logger.info(f'Material({mat.name}): object "{obj.name}" use UV "{subtex_uv}" for SubTex') + logging.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv) links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"]) else: links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"]) @@ -280,7 +288,7 @@ class FnMaterial: self.__remove_texture_node("mmd_sphere_tex") def get_toon_texture(self): - return self.__get_texture_node("mmd_toon_tex") + return self.__get_texture_node("mmd_toon_tex", use_dummy=True) def use_toon_texture(self, use_toon): if self._nodes_are_readonly: @@ -289,18 +297,18 @@ class FnMaterial: def create_toon_texture(self, filepath): texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5)) - return texture + return _DummyTextureSlot(texture.image) def remove_toon_texture(self): if self._nodes_are_readonly: return self.__remove_texture_node("mmd_toon_tex") - def __get_texture_node(self, node_name): + def __get_texture_node(self, node_name, use_dummy=False): mat = self.material texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None) if isinstance(texture, bpy.types.ShaderNodeTexImage): - return texture + return _DummyTexture(texture.image) if use_dummy else texture return None def __remove_texture_node(self, node_name): @@ -318,6 +326,7 @@ class FnMaterial: self.__update_shader_nodes() nodes = self.material.node_tree.nodes texture = nodes.new("ShaderNodeTexImage") + # pylint: disable=assignment-from-no-return texture.label = bpy.path.display_name(node_name) texture.name = node_name texture.location = nodes["mmd_shader"].location + Vector((pos[0] * 210, pos[1] * 220)) @@ -330,7 +339,6 @@ class FnMaterial: return mat = self.material mmd_mat = mat.mmd_material - # For Blender 4.4+ mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat) self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,)) @@ -339,7 +347,6 @@ class FnMaterial: return mat = self.material mmd_mat = mat.mmd_material - # For Blender 4.4+ mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat) self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,)) @@ -348,14 +355,17 @@ class FnMaterial: return mat = self.material mmd_mat = mat.mmd_material - - # For Blender 4.4+ - mat.blend_method = "HASHED" - - # Update alpha in diffuse_color - if len(mat.diffuse_color) > 3: + if hasattr(mat, "blend_method"): + mat.blend_method = "HASHED" # 'BLEND' + # mat.show_transparent_back = False + elif hasattr(mat, "transparency_method"): + mat.use_transparency = True + mat.transparency_method = "Z_TRANSPARENCY" + mat.game_settings.alpha_blend = "ALPHA" + if hasattr(mat, "alpha"): + mat.alpha = mmd_mat.alpha + elif len(mat.diffuse_color) > 3: mat.diffuse_color[3] = mmd_mat.alpha - self.__update_shader_input("Alpha", mmd_mat.alpha) self.update_self_shadow_map() @@ -372,11 +382,11 @@ class FnMaterial: return mat = self.material mmd_mat = mat.mmd_material - - # For Blender 4.4+ mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37) - mat.metallic = pow(1 - mat.roughness, 2.7) - + if hasattr(mat, "metallic"): + mat.metallic = pow(1 - mat.roughness, 2.7) + if hasattr(mat, "specular_hardness"): + mat.specular_hardness = mmd_mat.shininess self.__update_shader_input("Reflect", mmd_mat.shininess) def update_is_double_sided(self): @@ -384,10 +394,10 @@ class FnMaterial: return mat = self.material mmd_mat = mat.mmd_material - - # For Blender 4.4+ - mat.use_backface_culling = not mmd_mat.is_double_sided - + if hasattr(mat, "game_settings"): + mat.game_settings.use_backface_culling = not mmd_mat.is_double_sided + elif hasattr(mat, "use_backface_culling"): + mat.use_backface_culling = not mmd_mat.is_double_sided self.__update_shader_input("Double Sided", mmd_mat.is_double_sided) def update_self_shadow_map(self): @@ -396,9 +406,8 @@ class FnMaterial: mat = self.material mmd_mat = mat.mmd_material cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False - - # For Blender 4.4+ - mat.shadow_method = "HASHED" if cast_shadows else "NONE" + if hasattr(mat, "shadow_method"): + mat.shadow_method = "HASHED" if cast_shadows else "NONE" def update_self_shadow(self): if self._nodes_are_readonly: @@ -424,8 +433,16 @@ class FnMaterial: return child return None - # For Blender 4.4+ - preferred_output_node_target = "EEVEE" + if hasattr(context, "engine"): + active_render_engine = context.engine + else: + # use ALL anyway + active_render_engine = "ALL" + + preferred_output_node_target = { + "CYCLES": "CYCLES", + "BLENDER_EEVEE_NEXT": "EEVEE", + }.get(active_render_engine, "ALL") tex_node = None for target in [preferred_output_node_target, "ALL"]: @@ -453,21 +470,25 @@ class FnMaterial: # ambient should be half the diffuse mmd_material.ambient_color = [x * 0.5 for x in mmd_material.diffuse_color] - # For Blender 4.4+ shadow_method = getattr(m, "shadow_method", None) if mmd_material.diffuse_color is None: mmd_material.diffuse_color = m.diffuse_color[:3] - - # For Blender 4.4+ - if len(m.diffuse_color) > 3: + if hasattr(m, "alpha"): + mmd_material.alpha = m.alpha + elif len(m.diffuse_color) > 3: mmd_material.alpha = m.diffuse_color[3] mmd_material.specular_color = m.specular_color - - # For Blender 4.4+ - mmd_material.shininess = pow(1 / max(m.roughness, 0.099), 1 / 0.37) - mmd_material.is_double_sided = not m.use_backface_culling + if hasattr(m, "specular_hardness"): + mmd_material.shininess = m.specular_hardness + else: + mmd_material.shininess = pow(1 / max(m.roughness, 0.099), 1 / 0.37) + + if hasattr(m, "game_settings"): + mmd_material.is_double_sided = not m.game_settings.use_backface_culling + elif hasattr(m, "use_backface_culling"): + mmd_material.is_double_sided = not m.use_backface_culling if shadow_method: mmd_material.enabled_self_shadow_map = (shadow_method != "NONE") and mmd_material.alpha > 1e-3 @@ -504,13 +525,13 @@ class FnMaterial: node_shader = nodes.get("mmd_shader", None) if node_shader is None: - node_shader = nodes.new("ShaderNodeGroup") + node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup") node_shader.name = "mmd_shader" node_shader.location = (0, 1500) node_shader.width = 200 node_shader.node_tree = self.__get_shader() - mmd_mat = mat.mmd_material + mmd_mat: MMDMaterial = mat.mmd_material node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,) node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,) node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,) @@ -522,7 +543,7 @@ class FnMaterial: node_uv = nodes.get("mmd_tex_uv", None) if node_uv is None: - node_uv = nodes.new("ShaderNodeGroup") + node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup") node_uv.name = "mmd_tex_uv" node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220)) node_uv.node_tree = self.__get_shader_uv() @@ -530,7 +551,7 @@ class FnMaterial: if not (node_shader.outputs["Shader"].is_linked or node_shader.outputs["Color"].is_linked or node_shader.outputs["Alpha"].is_linked): node_output = next((n for n in nodes if isinstance(n, bpy.types.ShaderNodeOutputMaterial) and n.is_active_output), None) if node_output is None: - node_output = nodes.new("ShaderNodeOutputMaterial") + node_output: bpy.types.ShaderNodeOutputMaterial = nodes.new("ShaderNodeOutputMaterial") node_output.is_active_output = True node_output.location = node_shader.location + Vector((400, 0)) links.new(node_shader.outputs["Shader"], node_output.inputs["Surface"]) @@ -548,26 +569,26 @@ class FnMaterial: def __get_shader_uv(self): group_name = "MMDTexUV" - shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") + shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") if len(shader.nodes): return shader ng = _NodeGroupUtils(shader) ############################################################################ - _node_output = ng.new_node("NodeGroupOutput", (6, 0)) + _node_output: bpy.types.NodeGroupOutput = ng.new_node("NodeGroupOutput", (6, 0)) - tex_coord = ng.new_node("ShaderNodeTexCoord", (0, 0)) + tex_coord: bpy.types.ShaderNodeTexCoord = ng.new_node("ShaderNodeTexCoord", (0, 0)) - tex_coord1 = ng.new_node("ShaderNodeUVMap", (4, -2)) + tex_coord1: bpy.types.ShaderNodeUVMap = ng.new_node("ShaderNodeUVMap", (4, -2)) tex_coord1.uv_map = "UV1" - vec_trans = ng.new_node("ShaderNodeVectorTransform", (1, -1)) + vec_trans: bpy.types.ShaderNodeVectorTransform = ng.new_node("ShaderNodeVectorTransform", (1, -1)) vec_trans.vector_type = "NORMAL" vec_trans.convert_from = "OBJECT" vec_trans.convert_to = "CAMERA" - node_vector = ng.new_node("ShaderNodeMapping", (2, -1)) + node_vector: bpy.types.ShaderNodeMapping = ng.new_node("ShaderNodeMapping", (2, -1)) node_vector.vector_type = "POINT" node_vector.inputs["Location"].default_value = (0.5, 0.5, 0.0) node_vector.inputs["Scale"].default_value = (0.5, 0.5, 1.0) @@ -585,43 +606,43 @@ class FnMaterial: def __get_shader(self): group_name = "MMDShaderDev" - shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") + shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") if len(shader.nodes): return shader ng = _NodeGroupUtils(shader) ############################################################################ - node_input = ng.new_node("NodeGroupInput", (-5, -1)) - _node_output = ng.new_node("NodeGroupOutput", (11, 1)) + node_input: bpy.types.NodeGroupInput = ng.new_node("NodeGroupInput", (-5, -1)) + _node_output: bpy.types.NodeGroupOutput = ng.new_node("NodeGroupOutput", (11, 1)) - node_diffuse = ng.new_mix_node("ADD", (-3, 4), fac=0.6) + node_diffuse: bpy.types.ShaderNodeMath = ng.new_mix_node("ADD", (-3, 4), fac=0.6) node_diffuse.use_clamp = True - node_tex = ng.new_mix_node("MULTIPLY", (-2, 3.5)) - node_toon = ng.new_mix_node("MULTIPLY", (-1, 3)) - node_sph = ng.new_mix_node("MULTIPLY", (0, 2.5)) - node_spa = ng.new_mix_node("ADD", (0, 1.5)) - node_sphere = ng.new_mix_node("MIX", (1, 1)) + node_tex: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (-2, 3.5)) + node_toon: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (-1, 3)) + node_sph: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (0, 2.5)) + node_spa: bpy.types.ShaderNodeMath = ng.new_mix_node("ADD", (0, 1.5)) + node_sphere: bpy.types.ShaderNodeMath = ng.new_mix_node("MIX", (1, 1)) - node_geo = ng.new_node("ShaderNodeNewGeometry", (6, 3.5)) - node_invert = ng.new_math_node("LESS_THAN", (7, 3)) - node_cull = ng.new_math_node("MAXIMUM", (8, 2.5)) - node_alpha = ng.new_math_node("MINIMUM", (9, 2)) + node_geo: bpy.types.ShaderNodeNewGeometry = ng.new_node("ShaderNodeNewGeometry", (6, 3.5)) + node_invert: bpy.types.ShaderNodeMath = ng.new_math_node("LESS_THAN", (7, 3)) + node_cull: bpy.types.ShaderNodeMath = ng.new_math_node("MAXIMUM", (8, 2.5)) + node_alpha: bpy.types.ShaderNodeMath = ng.new_math_node("MINIMUM", (9, 2)) node_alpha.use_clamp = True - node_alpha_tex = ng.new_math_node("MULTIPLY", (-1, -2)) - node_alpha_toon = ng.new_math_node("MULTIPLY", (0, -2.5)) - node_alpha_sph = ng.new_math_node("MULTIPLY", (1, -3)) + node_alpha_tex: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (-1, -2)) + node_alpha_toon: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (0, -2.5)) + node_alpha_sph: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (1, -3)) - node_reflect = ng.new_math_node("DIVIDE", (7, -1.5), value1=1) + node_reflect: bpy.types.ShaderNodeMath = ng.new_math_node("DIVIDE", (7, -1.5), value1=1) node_reflect.use_clamp = True - shader_diffuse = ng.new_node("ShaderNodeBsdfDiffuse", (8, 0)) - shader_glossy = ng.new_node("ShaderNodeBsdfAnisotropic", (8, -1)) - shader_base_mix = ng.new_node("ShaderNodeMixShader", (9, 0)) + shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = ng.new_node("ShaderNodeBsdfDiffuse", (8, 0)) + shader_glossy: bpy.types.ShaderNodeBsdfAnisotropic = ng.new_node("ShaderNodeBsdfAnisotropic", (8, -1)) + shader_base_mix: bpy.types.ShaderNodeMixShader = ng.new_node("ShaderNodeMixShader", (9, 0)) shader_base_mix.inputs["Fac"].default_value = 0.02 - shader_trans = ng.new_node("ShaderNodeBsdfTransparent", (9, 1)) - shader_alpha_mix = ng.new_node("ShaderNodeMixShader", (10, 1)) + shader_trans: bpy.types.ShaderNodeBsdfTransparent = ng.new_node("ShaderNodeBsdfTransparent", (9, 1)) + shader_alpha_mix: bpy.types.ShaderNodeMixShader = ng.new_node("ShaderNodeMixShader", (10, 1)) links = ng.links links.new(node_reflect.outputs["Value"], shader_glossy.inputs["Roughness"]) @@ -679,7 +700,7 @@ class FnMaterial: class MigrationFnMaterial: @staticmethod def update_mmd_shader(): - mmd_shader_node_tree = bpy.data.node_groups.get("MMDShaderDev") + mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev") if mmd_shader_node_tree is None: return @@ -687,11 +708,11 @@ class MigrationFnMaterial: if "Color" in ng.node_output.inputs: return - shader_diffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0] - node_sphere = shader_diffuse.inputs["Color"].links[0].from_node - node_output = ng.node_output - shader_alpha_mix = node_output.inputs["Shader"].links[0].from_node - node_alpha = shader_alpha_mix.inputs["Fac"].links[0].from_node + shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0] + node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node + node_output: bpy.types.NodeGroupOutput = ng.node_output + shader_alpha_mix: bpy.types.ShaderNodeMixShader = node_output.inputs["Shader"].links[0].from_node + node_alpha: bpy.types.ShaderNodeMath = shader_alpha_mix.inputs["Fac"].links[0].from_node ng.new_output_socket("Color", node_sphere.outputs["Color"]) ng.new_output_socket("Alpha", node_alpha.outputs["Value"]) diff --git a/core/mmd/core/model.py b/core/mmd/core/model.py new file mode 100644 index 0000000..103d52f --- /dev/null +++ b/core/mmd/core/model.py @@ -0,0 +1,1208 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import itertools +import logging +import time +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast + +import bpy +import idprop +import rna_prop_ui +from mathutils import Vector + +from .. import AVATAR_TOOLKIT_VERSION, bpyutils +from ..bpyutils import FnContext, Props +from . import rigid_body +from .morph import FnMorph +from .rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC + +if TYPE_CHECKING: + from ..properties.morph import MaterialMorphData + from ..properties.rigid_body import MMDRigidBody + + +class FnModel: + @staticmethod + def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Dict[str, Dict[Any, Any]] = None): + FnModel.__copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values or {}) + + @staticmethod + def find_root_object(obj: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]: + """Find the root object of the model. + Args: + obj (bpy.types.Object): The object to start searching from. + Returns: + Optional[bpy.types.Object]: The root object of the model. If the object is not a part of a model, None is returned. + Generally, the root object is a object with type == "EMPTY" and mmd_type == "ROOT". + """ + while obj is not None and obj.mmd_type != "ROOT": + obj = obj.parent + return obj + + @staticmethod + def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + """Find the armature object of the model. + Args: + root_object (bpy.types.Object): The root object of the model. + Returns: + Optional[bpy.types.Object]: The armature object of the model. If the model does not have an armature, None is returned. + """ + for o in root_object.children: + if o.type == "ARMATURE": + return o + return None + + @staticmethod + def find_rigid_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + for o in root_object.children: + if o.type == "EMPTY" and o.mmd_type == "RIGID_GRP_OBJ": + return o + return None + + @staticmethod + def __new_group_object(context: bpy.types.Context, name: str, mmd_type: str, parent: bpy.types.Object) -> bpy.types.Object: + group_object = FnContext.new_and_link_object(context, name=name, object_data=None) + group_object.mmd_type = mmd_type + group_object.parent = parent + group_object.hide_set(True) + group_object.hide_select = True + group_object.lock_rotation = group_object.lock_location = group_object.lock_scale = [True, True, True] + return group_object + + @staticmethod + def ensure_rigid_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + rigid_group_object = FnModel.find_rigid_group_object(root_object) + if rigid_group_object is not None: + return rigid_group_object + return FnModel.__new_group_object(context, name="rigidbodies", mmd_type="RIGID_GRP_OBJ", parent=root_object) + + @staticmethod + def find_joint_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + for o in root_object.children: + if o.type == "EMPTY" and o.mmd_type == "JOINT_GRP_OBJ": + return o + return None + + @staticmethod + def ensure_joint_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + joint_group_object = FnModel.find_joint_group_object(root_object) + if joint_group_object is not None: + return joint_group_object + return FnModel.__new_group_object(context, name="joints", mmd_type="JOINT_GRP_OBJ", parent=root_object) + + @staticmethod + def find_temporary_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + for o in root_object.children: + if o.type == "EMPTY" and o.mmd_type == "TEMPORARY_GRP_OBJ": + return o + return None + + @staticmethod + def ensure_temporary_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + temporary_group_object = FnModel.find_temporary_group_object(root_object) + if temporary_group_object is not None: + return temporary_group_object + return FnModel.__new_group_object(context, name="temporary", mmd_type="TEMPORARY_GRP_OBJ", parent=root_object) + + @staticmethod + def find_bone_order_mesh_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return None + + for o in armature_object.children: + if o.type == "MESH" and "mmd_bone_order_override" in o.modifiers: + return o + return None + + @staticmethod + def find_mesh_object_by_name(root_object: bpy.types.Object, name: str) -> Optional[bpy.types.Object]: + if not name: + return None + + for o in FnModel.iterate_mesh_objects(root_object): + if o.name == name or (hasattr(o.data, 'name') and o.data.name == name): + return o + return None + + @staticmethod + def iterate_child_objects(obj: bpy.types.Object) -> Iterator[bpy.types.Object]: + for child in obj.children: + yield child + yield from FnModel.iterate_child_objects(child) + + @staticmethod + def iterate_filtered_child_objects(condition_function: Callable[[bpy.types.Object], bool], obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]: + if obj is None: + return iter(()) + return FnModel.__iterate_filtered_child_objects_internal(condition_function, obj) + + @staticmethod + def __iterate_filtered_child_objects_internal(condition_function: Callable[[bpy.types.Object], bool], obj: bpy.types.Object) -> Iterator[bpy.types.Object]: + for child in obj.children: + if condition_function(child): + yield child + yield from FnModel.__iterate_filtered_child_objects_internal(condition_function, child) + + @staticmethod + def __iterate_child_mesh_objects(obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]: + return FnModel.iterate_filtered_child_objects(FnModel.is_mesh_object, obj) + + @staticmethod + def iterate_mesh_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: + return FnModel.__iterate_child_mesh_objects(FnModel.find_armature_object(root_object)) + + @staticmethod + def iterate_rigid_body_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: + if root_object.mmd_root.is_built: + return itertools.chain( + FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_armature_object(root_object)), + FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_rigid_group_object(root_object)), + ) + return FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_rigid_group_object(root_object)) + + @staticmethod + def iterate_joint_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: + return FnModel.iterate_filtered_child_objects(FnModel.is_joint_object, FnModel.find_joint_group_object(root_object)) + + @staticmethod + def iterate_temporary_objects(root_object: bpy.types.Object, rigid_track_only: bool = False) -> Iterator[bpy.types.Object]: + rigid_body_objects = FnModel.iterate_filtered_child_objects(FnModel.is_temporary_object, FnModel.find_rigid_group_object(root_object)) + + if rigid_track_only: + return rigid_body_objects + + temporary_group_object = FnModel.find_temporary_group_object(root_object) + if temporary_group_object is None: + return rigid_body_objects + return itertools.chain(rigid_body_objects, FnModel.__iterate_filtered_child_objects_internal(FnModel.is_temporary_object, temporary_group_object)) + + @staticmethod + def iterate_materials(root_object: bpy.types.Object) -> Iterator[bpy.types.Material]: + return (material for mesh_object in FnModel.iterate_mesh_objects(root_object) for material in cast(bpy.types.Mesh, mesh_object.data).materials if material is not None) + + @staticmethod + def iterate_unique_materials(root_object: bpy.types.Object) -> Iterator[bpy.types.Material]: + materials: Dict[bpy.types.Material, None] = {} # use dict because set does not guarantee the order + materials.update((material, None) for material in FnModel.iterate_materials(root_object)) + return iter(materials.keys()) + + @staticmethod + def is_root_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.mmd_type == "ROOT" + + @staticmethod + def is_rigid_body_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.mmd_type == "RIGID_BODY" + + @staticmethod + def is_joint_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.mmd_type == "JOINT" + + @staticmethod + def is_temporary_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.mmd_type in {"TRACK_TARGET", "NON_COLLISION_CONSTRAINT", "SPRING_CONSTRAINT", "SPRING_GOAL"} + + @staticmethod + def is_mesh_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" + + @staticmethod + def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]): + parent_armature_object = FnModel.find_armature_object(parent_root_object) + with bpy.context.temp_override( + active_object=parent_armature_object, + selected_editable_objects=[parent_armature_object], + ): + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pose_bones): + """This function will also update the references of bone morphs and rotate+/move+.""" + bone_id = bone.mmd_bone.bone_id + + # Change Bone ID + bone.mmd_bone.bone_id = new_bone_id + + # Update Relative Bone Morph # Update the reference of bone morph # 更新骨骼表情 + for bone_morph in bone_morphs: + for data in bone_morph.data: + if data.bone_id != bone_id: + continue + data.bone_id = new_bone_id + + # Update Relative Additional Transform # Update the reference of rotate+/move+ # 更新付与親 + for pose_bone in pose_bones: + if pose_bone.is_mmd_shadow_bone: + continue + mmd_bone = pose_bone.mmd_bone + if mmd_bone.additional_transform_bone_id != bone_id: + continue + mmd_bone.additional_transform_bone_id = new_bone_id + + max_bone_id = max( + ( + b.mmd_bone.bone_id + for o in itertools.chain( + child_root_objects, + [parent_root_object], + ) + for b in FnModel.find_armature_object(o).pose.bones + if not b.is_mmd_shadow_bone + ), + default=-1, + ) + + child_root_object: bpy.types.Object + for child_root_object in child_root_objects: + child_armature_object = FnModel.find_armature_object(child_root_object) + child_pose_bones = child_armature_object.pose.bones + child_bone_morphs = child_root_object.mmd_root.bone_morphs + + for pose_bone in child_pose_bones: + if pose_bone.is_mmd_shadow_bone: + continue + if pose_bone.mmd_bone.bone_id != -1: + max_bone_id += 1 + _change_bone_id(pose_bone, max_bone_id, child_bone_morphs, child_pose_bones) + + child_armature_matrix = child_armature_object.matrix_parent_inverse.copy() + + with bpy.context.temp_override( + active_object=child_armature_object, + selected_editable_objects=[child_armature_object], + ): + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + + # Disconnect mesh dependencies because transform_apply fails when mesh data are multiple used. + related_meshes: Dict[MaterialMorphData, bpy.types.Mesh] = {} + for material_morph in child_root_object.mmd_root.material_morphs: + for material_morph_data in material_morph.data: + if material_morph_data.related_mesh_data is not None: + related_meshes[material_morph_data] = material_morph_data.related_mesh_data + material_morph_data.related_mesh_data = None + try: + # replace mesh armature modifier.object + mesh: bpy.types.Object + for mesh in FnModel.__iterate_child_mesh_objects(child_armature_object): + with bpy.context.temp_override( + active_object=mesh, + selected_editable_objects=[mesh], + ): + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + finally: + # Restore mesh dependencies + for material_morph in child_root_object.mmd_root.material_morphs: + for material_morph_data in material_morph.data: + material_morph_data.related_mesh_data = related_meshes.get(material_morph_data, None) + + # join armatures + with bpy.context.temp_override( + active_object=parent_armature_object, + selected_editable_objects=[parent_armature_object, child_armature_object], + ): + bpy.ops.object.join() + + for mesh in FnModel.__iterate_child_mesh_objects(parent_armature_object): + armature_modifier: bpy.types.ArmatureModifier = mesh.modifiers["mmd_bone_order_override"] if "mmd_bone_order_override" in mesh.modifiers else mesh.modifiers.new("mmd_bone_order_override", "ARMATURE") + if armature_modifier.object is None: + armature_modifier.object = parent_armature_object + mesh.matrix_parent_inverse = child_armature_matrix + + child_rigid_group_object = FnModel.find_rigid_group_object(child_root_object) + if child_rigid_group_object is not None: + parent_rigid_group_object = FnModel.find_rigid_group_object(parent_root_object) + + with bpy.context.temp_override( + object=parent_rigid_group_object, + selected_editable_objects=[parent_rigid_group_object, *FnModel.iterate_rigid_body_objects(child_root_object)], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + bpy.data.objects.remove(child_rigid_group_object) + + child_joint_group_object = FnModel.find_joint_group_object(child_root_object) + if child_joint_group_object is not None: + parent_joint_group_object = FnModel.find_joint_group_object(parent_root_object) + with bpy.context.temp_override( + object=parent_joint_group_object, + selected_editable_objects=[parent_joint_group_object, *FnModel.iterate_joint_objects(child_root_object)], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + bpy.data.objects.remove(child_joint_group_object) + + child_temporary_group_object = FnModel.find_temporary_group_object(child_root_object) + if child_temporary_group_object is not None: + parent_temporary_group_object = FnModel.find_temporary_group_object(parent_root_object) + with bpy.context.temp_override( + object=parent_temporary_group_object, + selected_editable_objects=[parent_temporary_group_object, *FnModel.iterate_temporary_objects(child_root_object)], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + + for obj in list(FnModel.iterate_child_objects(child_temporary_group_object)): + bpy.data.objects.remove(obj) + bpy.data.objects.remove(child_temporary_group_object) + + FnModel.copy_mmd_root(parent_root_object, child_root_object, overwrite=False) + + # Remove unused objects from child models + if len(child_root_object.children) == 0: + bpy.data.objects.remove(child_root_object) + + @staticmethod + def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier: + for m in mesh_object.modifiers: + if m.type != "ARMATURE": + continue + # already has armature modifier. + return cast(bpy.types.ArmatureModifier, m) + + modifier = cast(bpy.types.ArmatureModifier, mesh_object.modifiers.new(name="Armature", type="ARMATURE")) + modifier.object = armature_object + modifier.use_vertex_groups = True + modifier.name = "mmd_bone_order_override" + + return modifier + + @staticmethod + def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool): + armature_object = FnModel.find_armature_object(parent_root_object) + if armature_object is None: + raise ValueError(f"Armature object not found in {parent_root_object}") + + def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object: + if obj.parent is None: + return obj + return __get_root_object(obj.parent) + + for mesh_object in mesh_objects: + if not FnModel.is_mesh_object(mesh_object): + continue + + if FnModel.find_root_object(mesh_object) is not None: + continue + + mesh_root_object = __get_root_object(mesh_object) + original_matrix_world = mesh_root_object.matrix_world + mesh_root_object.parent_type = "OBJECT" + mesh_root_object.parent = armature_object + mesh_root_object.matrix_world = original_matrix_world + + if add_armature_modifier: + FnModel._add_armature_modifier(mesh_object, armature_object) + + @staticmethod + def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool): + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + raise ValueError(f"Armature object not found in {root_object}") + + vertex_group_names: Set[str] = set() + + search_meshes = FnModel.iterate_mesh_objects(root_object) if search_in_all_meshes else [mesh_object] + + for search_mesh in search_meshes: + vertex_group_names.update(search_mesh.vertex_groups.keys()) + + pose_bone: bpy.types.PoseBone + for pose_bone in armature_object.pose.bones: + pose_bone_name = pose_bone.name + + if pose_bone_name in vertex_group_names: + continue + + if pose_bone_name.startswith("_"): + continue + + mesh_object.vertex_groups.new(name=pose_bone_name) + + @staticmethod + def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: int): + mmd_root = root_object.mmd_root + old_ik_loop_factor = mmd_root.ik_loop_factor + + if new_ik_loop_factor == old_ik_loop_factor: + return + + armature_object = FnModel.find_armature_object(root_object) + for pose_bone in armature_object.pose.bones: + for constraint in (cast(bpy.types.KinematicConstraint, c) for c in pose_bone.constraints if c.type == "IK"): + iterations = int(constraint.iterations * new_ik_loop_factor / old_ik_loop_factor) + logging.info("Update %s of %s: %d -> %d", constraint.name, pose_bone.name, constraint.iterations, iterations) + constraint.iterations = iterations + + mmd_root.ik_loop_factor = new_ik_loop_factor + + return + + @staticmethod + def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]): + destination_rna_properties = destination.bl_rna.properties + for name in source.keys(): + is_attr = hasattr(source, name) + value = getattr(source, name) if is_attr else source[name] + if isinstance(value, bpy.types.PropertyGroup): + FnModel.__copy_property_group(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) + elif isinstance(value, bpy.types.bpy_prop_collection): + FnModel.__copy_collection_property(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) + elif isinstance(value, idprop.types.IDPropertyArray): + pass + # _copy_collection_property(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) + else: + value2values = replace_name2values.get(name) + if value2values is not None: + replace_value = value2values.get(value) + if replace_value is not None: + value = replace_value + + if overwrite or destination_rna_properties[name].default == getattr(destination, name) if is_attr else destination[name]: + if is_attr: + setattr(destination, name, value) + else: + destination[name] = value + + @staticmethod + def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]): + if overwrite: + destination.clear() + + len_source = len(source) + if len_source == 0: + return + + source_names: Set[str] = set(source.keys()) + if len(source_names) == len_source and source[0].name != "": + # names work + destination_names: Set[str] = set(destination.keys()) + + missing_names = source_names - destination_names + + destination_index = 0 + for name, value in source.items(): + if name in missing_names: + new_element = destination.add() + new_element["name"] = name + + FnModel.__copy_property(destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) + destination.move(destination.find(name), destination_index) + destination_index += 1 + else: + # names not work + while len_source > len(destination): + destination.add() + + for index, name in enumerate(source.keys()): + FnModel.__copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values) + + @staticmethod + def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]): + if isinstance(destination, bpy.types.PropertyGroup): + FnModel.__copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values) + elif isinstance(destination, bpy.types.bpy_prop_collection): + FnModel.__copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values) + else: + raise ValueError(f"Unsupported destination: {destination}") + + @staticmethod + def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True): + frames = root_object.mmd_root.display_item_frames + if reset and len(frames) > 0: + root_object.mmd_root.active_display_item_frame = 0 + frames.clear() + + frame_names = {"Root": "Root", "表情": "Facial"} + + for frame_name, frame_name_e in frame_names.items(): + frame = frames.get(frame_name, None) or frames.add() + frame.name = frame_name + frame.name_e = frame_name_e + frame.is_special = True + + arm = FnModel.find_armature_object(root_object) + if arm is not None and len(arm.data.bones) > 0 and len(frames[0].data) < 1: + item = frames[0].data.add() + item.type = "BONE" + item.name = arm.data.bones[0].name + + if not reset: + frames.move(frames.find("Root"), 0) + frames.move(frames.find("表情"), 1) + + @staticmethod + def get_empty_display_size(root_object: bpy.types.Object) -> float: + return getattr(root_object, Props.empty_display_size) + + +class MigrationFnModel: + """Migration Functions for old MMD models broken by bugs or issues""" + + @classmethod + def update_mmd_ik_loop_factor(cls): + for armature_object in bpy.data.objects: + if armature_object.type != "ARMATURE": + continue + + if "mmd_ik_loop_factor" not in armature_object: + return + + FnModel.find_root_object(armature_object).mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1) + del armature_object["mmd_ik_loop_factor"] + + @staticmethod + def update_avatar_toolkit_version(): + for root_object in bpy.data.objects: + if root_object.type != "EMPTY": + continue + + if not FnModel.is_root_object(root_object): + continue + + if "avatar_toolkit_version" in root_object: + continue + + root_object["avatar_toolkit_version"] = "0.2.1" + + +class Model: + def __init__(self, root_obj): + if root_obj is None: + raise ValueError("must be MMD ROOT type object") + if root_obj.mmd_type != "ROOT": + raise ValueError("must be MMD ROOT type object") + self.__root: bpy.types.Object = getattr(root_obj, "original", root_obj) + self.__arm: Optional[bpy.types.Object] = None + self.__rigid_grp: Optional[bpy.types.Object] = None + self.__joint_grp: Optional[bpy.types.Object] = None + self.__temporary_grp: Optional[bpy.types.Object] = None + + @staticmethod + def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False): + if obj_name is None: + obj_name = name + + context = FnContext.ensure_context() + + root: bpy.types.Object = bpy.data.objects.new(name=obj_name, object_data=None) + root.mmd_type = "ROOT" + root.mmd_root.name = name + root.mmd_root.name_e = name_e + root["avatar_toolkit_version"] = AVATAR_TOOLKIT_VERSION + setattr(root, Props.empty_display_size, scale / 0.2) + FnContext.link_object(context, root) + + if armature_object: + m = armature_object.matrix_world + armature_object.parent_type = "OBJECT" + armature_object.parent = root + # armature_object.matrix_world = m + root.matrix_world = m + armature_object.matrix_local.identity() + else: + armature_object = bpy.data.objects.new(name=obj_name + "_arm", object_data=bpy.data.armatures.new(name=obj_name)) + armature_object.parent = root + FnContext.link_object(context, armature_object) + armature_object.lock_rotation = armature_object.lock_location = armature_object.lock_scale = [True, True, True] + setattr(armature_object, Props.show_in_front, True) + setattr(armature_object, Props.display_type, "WIRE") + + from .bone import FnBone + + FnBone.setup_special_bone_collections(armature_object) + + if add_root_bone: + bone_name = "全ての親" + bone_name_english = "Root" + + # Create the root bone + with bpyutils.edit_object(armature_object) as data: + bone = data.edit_bones.new(name=bone_name) + bone.head = (0.0, 0.0, 0.0) + bone.tail = (0.0, 0.0, getattr(root, Props.empty_display_size)) + + # Set MMD bone properties + pose_bone = armature_object.pose.bones[bone_name] + pose_bone.mmd_bone.name_j = bone_name + pose_bone.mmd_bone.name_e = bone_name_english + + # Create a bone collection named "Root" + bone_collection_name = bone_name_english + bone_collection = armature_object.data.collections.new(name=bone_collection_name) + + # Assign the new bone to the bone collection + data_bone = armature_object.data.bones[bone_name] + bone_collection.assign(data_bone) + + FnContext.set_active_and_select_single_object(context, root) + return Model(root) + + @staticmethod + def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]: + return FnModel.find_root_object(obj) + + def initialDisplayFrames(self, reset=True): + FnModel.initalize_display_item_frames(self.__root, reset=reset) + + @property + def morph_slider(self): + return FnMorph.get_morph_slider(self) + + def loadMorphs(self): + FnMorph.load_morphs(self) + + def create_ik_constraint(self, bone, ik_target): + """create IK constraint + + Args: + bone: A pose bone to add a IK constraint + id_target: A pose bone for IK target + + Returns: + The bpy.types.KinematicConstraint object created. It is set target + and subtarget options. + + """ + ik_target_name = ik_target.name + ik_const = bone.constraints.new("IK") + ik_const.target = self.__arm + ik_const.subtarget = ik_target_name + return ik_const + + def allObjects(self, obj: Optional[bpy.types.Object] = None) -> Iterator[bpy.types.Object]: + if obj is None: + obj: bpy.types.Object = self.__root + yield obj + yield from FnModel.iterate_child_objects(obj) + + def rootObject(self) -> bpy.types.Object: + return self.__root + + def armature(self) -> bpy.types.Object: + if self.__arm is None: + self.__arm = FnModel.find_armature_object(self.__root) + assert self.__arm is not None + return self.__arm + + def hasRigidGroupObject(self) -> bool: + return FnModel.find_rigid_group_object(self.__root) is not None + + def rigidGroupObject(self) -> bpy.types.Object: + if self.__rigid_grp is None: + self.__rigid_grp = FnModel.find_rigid_group_object(self.__root) + if self.__rigid_grp is None: + rigids = bpy.data.objects.new(name="rigidbodies", object_data=None) + FnContext.link_object(FnContext.ensure_context(), rigids) + rigids.mmd_type = "RIGID_GRP_OBJ" + rigids.parent = self.__root + rigids.hide_set(True) + rigids.hide_select = True + rigids.lock_rotation = rigids.lock_location = rigids.lock_scale = [True, True, True] + self.__rigid_grp = rigids + return self.__rigid_grp + + def hasJointGroupObject(self) -> bool: + return FnModel.find_joint_group_object(self.__root) is not None + + def jointGroupObject(self) -> bpy.types.Object: + if self.__joint_grp is None: + self.__joint_grp = FnModel.find_joint_group_object(self.__root) + if self.__joint_grp is None: + joints = bpy.data.objects.new(name="joints", object_data=None) + FnContext.link_object(FnContext.ensure_context(), joints) + joints.mmd_type = "JOINT_GRP_OBJ" + joints.parent = self.__root + joints.hide_set(True) + joints.hide_select = True + joints.lock_rotation = joints.lock_location = joints.lock_scale = [True, True, True] + self.__joint_grp = joints + return self.__joint_grp + + def hasTemporaryGroupObject(self) -> bool: + return FnModel.find_temporary_group_object(self.__root) is not None + + def temporaryGroupObject(self) -> bpy.types.Object: + if self.__temporary_grp is None: + self.__temporary_grp = FnModel.find_temporary_group_object(self.__root) + if self.__temporary_grp is None: + temporarys = bpy.data.objects.new(name="temporary", object_data=None) + FnContext.link_object(FnContext.ensure_context(), temporarys) + temporarys.mmd_type = "TEMPORARY_GRP_OBJ" + temporarys.parent = self.__root + temporarys.hide_set(True) + temporarys.hide_select = True + temporarys.lock_rotation = temporarys.lock_location = temporarys.lock_scale = [True, True, True] + self.__temporary_grp = temporarys + return self.__temporary_grp + + def meshes(self) -> Iterator[bpy.types.Object]: + return FnModel.iterate_mesh_objects(self.__root) + + def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True): + FnModel.attach_mesh_objects(self.rootObject(), meshes, add_armature_modifier) + + def firstMesh(self) -> Optional[bpy.types.Object]: + for i in self.meshes(): + return i + return None + + def findMesh(self, mesh_name) -> Optional[bpy.types.Object]: + """ + Helper method to find a mesh by name + """ + if mesh_name == "": + return None + for mesh in self.meshes(): + if mesh.name == mesh_name or mesh.data.name == mesh_name: + return mesh + return None + + def findMeshByIndex(self, index: int) -> Optional[bpy.types.Object]: + """ + Helper method to find the mesh by index + """ + if index < 0: + return None + for i, mesh in enumerate(self.meshes()): + if i == index: + return mesh + return None + + def getMeshIndex(self, mesh_name: str) -> int: + """ + Helper method to get the index of a mesh. Returns -1 if not found + """ + if mesh_name == "": + return -1 + for i, mesh in enumerate(self.meshes()): + if mesh.name == mesh_name or mesh.data.name == mesh_name: + return i + return -1 + + def rigidBodies(self) -> Iterator[bpy.types.Object]: + return FnModel.iterate_rigid_body_objects(self.__root) + + def joints(self) -> Iterator[bpy.types.Object]: + return FnModel.iterate_joint_objects(self.__root) + + def temporaryObjects(self, rigid_track_only=False) -> Iterator[bpy.types.Object]: + return FnModel.iterate_temporary_objects(self.__root, rigid_track_only) + + def materials(self) -> Iterator[bpy.types.Material]: + """ + Helper method to list all materials in all meshes + """ + materials = {} # Use dict instead of set to guarantee preserve order + for mesh in self.meshes(): + materials.update((slot.material, 0) for slot in mesh.material_slots if slot.material is not None) + return iter(materials.keys()) + + def renameBone(self, old_bone_name, new_bone_name): + if old_bone_name == new_bone_name: + return + armature = self.armature() + bone = armature.pose.bones[old_bone_name] + bone.name = new_bone_name + new_bone_name = bone.name + + mmd_root = self.rootObject().mmd_root + for frame in mmd_root.display_item_frames: + for item in frame.data: + if item.type == "BONE" and item.name == old_bone_name: + item.name = new_bone_name + for mesh in self.meshes(): + if old_bone_name in mesh.vertex_groups: + mesh.vertex_groups[old_bone_name].name = new_bone_name + + def build(self, non_collision_distance_scale=1.5, collision_margin=1e-06): + rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False) + if self.__root.mmd_root.is_built: + self.clean() + self.__root.mmd_root.is_built = True + logging.info("****************************************") + logging.info(" Build rig") + logging.info("****************************************") + start_time = time.time() + self.__preBuild() + self.disconnectPhysicsBones() + self.buildRigids(non_collision_distance_scale, collision_margin) + self.buildJoints() + self.__postBuild() + logging.info(" Finished building in %f seconds.", time.time() - start_time) + rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled) + + def clean(self): + rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False) + logging.info("****************************************") + logging.info(" Clean rig") + logging.info("****************************************") + start_time = time.time() + + pose_bones = [] + arm = self.armature() + if arm is not None: + pose_bones = arm.pose.bones + for i in pose_bones: + if "mmd_tools_rigid_track" in i.constraints: + const = i.constraints["mmd_tools_rigid_track"] + i.constraints.remove(const) + + rigid_track_counts = 0 + for i in self.rigidBodies(): + rigid_type = int(i.mmd_rigid.type) + if "mmd_tools_rigid_parent" not in i.constraints: + rigid_track_counts += 1 + logging.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name) + i.mmd_rigid.bone = i.mmd_rigid.bone + relation = i.constraints["mmd_tools_rigid_parent"] + relation.mute = True + if rigid_type == rigid_body.MODE_STATIC: + i.parent_type = "OBJECT" + i.parent = self.rigidGroupObject() + elif rigid_type in [rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE]: + arm = relation.target + bone_name = relation.subtarget + if arm is not None and bone_name != "": + for c in arm.pose.bones[bone_name].constraints: + if c.type == "IK": + c.mute = False + self.__restoreTransforms(i) + + for i in self.joints(): + self.__restoreTransforms(i) + + self.__removeTemporaryObjects() + self.connectPhysicsBones() + + arm = self.armature() + if arm is not None: # update armature + arm.update_tag() + bpy.context.scene.frame_set(bpy.context.scene.frame_current) + + mmd_root = self.rootObject().mmd_root + if mmd_root.show_temporary_objects: + mmd_root.show_temporary_objects = False + logging.info(" Finished cleaning in %f seconds.", time.time() - start_time) + mmd_root.is_built = False + rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled) + + def __removeTemporaryObjects(self): + with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()): + bpy.ops.object.delete() + + def __restoreTransforms(self, obj): + for attr in ("location", "rotation_euler"): + attr_name = "__backup_%s__" % attr + val = obj.get(attr_name, None) + if val is not None: + setattr(obj, attr, val) + del obj[attr_name] + + def __backupTransforms(self, obj): + for attr in ("location", "rotation_euler"): + attr_name = "__backup_%s__" % attr + if attr_name in obj: # should not happen in normal build/clean cycle + continue + obj[attr_name] = getattr(obj, attr, None) + + def __preBuild(self): + self.__fake_parent_map = {} + self.__rigid_body_matrix_map = {} + self.__empty_parent_map = {} + + no_parents = [] + for i in self.rigidBodies(): + self.__backupTransforms(i) + # mute relation + relation = i.constraints["mmd_tools_rigid_parent"] + relation.mute = True + # mute IK + if int(i.mmd_rigid.type) in [rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE]: + arm = relation.target + bone_name = relation.subtarget + if arm is not None and bone_name != "": + for c in arm.pose.bones[bone_name].constraints: + if c.type == "IK": + c.mute = True + c.influence = c.influence # trigger update + else: + no_parents.append(i) + # update changes of armature constraints + bpy.context.scene.frame_set(bpy.context.scene.frame_current) + + parented = [] + for i in self.joints(): + self.__backupTransforms(i) + rbc = i.rigid_body_constraint + if rbc is None: + continue + obj1, obj2 = rbc.object1, rbc.object2 + if obj2 in no_parents: + if obj1 not in no_parents and obj2 not in parented: + self.__fake_parent_map.setdefault(obj1, []).append(obj2) + parented.append(obj2) + elif obj1 in no_parents: + if obj1 not in parented: + self.__fake_parent_map.setdefault(obj2, []).append(obj1) + parented.append(obj1) + + # assert(len(no_parents) == len(parented)) + + def __postBuild(self): + self.__fake_parent_map = None + self.__rigid_body_matrix_map = None + + # update changes + bpy.context.scene.frame_set(bpy.context.scene.frame_current) + + # parenting empty to rigid object at once for speeding up + for empty, rigid_obj in self.__empty_parent_map.items(): + matrix_world = empty.matrix_world + empty.parent = rigid_obj + empty.matrix_world = matrix_world + self.__empty_parent_map = None + + arm = self.armature() + if arm: + for p_bone in arm.pose.bones: + c = p_bone.constraints.get("mmd_tools_rigid_track", None) + if c: + c.mute = False + + def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float): + assert rigid_obj.mmd_type == "RIGID_BODY" + rb = rigid_obj.rigid_body + if rb is None: + return + + rigid = rigid_obj.mmd_rigid + rigid_type = int(rigid.type) + relation = rigid_obj.constraints["mmd_tools_rigid_parent"] + + if relation.target is None: + relation.target = self.armature() + + arm = relation.target + if relation.subtarget not in arm.pose.bones: + bone_name = "" + else: + bone_name = relation.subtarget + + if rigid_type == rigid_body.MODE_STATIC: + rb.kinematic = True + else: + rb.kinematic = False + + if collision_margin == 0.0: + rb.use_margin = False + else: + rb.use_margin = True + rb.collision_margin = collision_margin + + if arm is not None and bone_name != "": + target_bone = arm.pose.bones[bone_name] + + if rigid_type == rigid_body.MODE_STATIC: + m = target_bone.matrix @ target_bone.bone.matrix_local.inverted() + self.__rigid_body_matrix_map[rigid_obj] = m + orig_scale = rigid_obj.scale.copy() + to_matrix_world = rigid_obj.matrix_world @ rigid_obj.matrix_local.inverted() + matrix_world = to_matrix_world @ (m @ rigid_obj.matrix_local) + rigid_obj.parent = arm + rigid_obj.parent_type = "BONE" + rigid_obj.parent_bone = bone_name + rigid_obj.matrix_world = matrix_world + rigid_obj.scale = orig_scale + fake_children = self.__fake_parent_map.get(rigid_obj, None) + if fake_children: + for fake_child in fake_children: + logging.debug(" - fake_child: %s", fake_child.name) + t, r, s = (m @ fake_child.matrix_local).decompose() + fake_child.location = t + fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode) + + elif rigid_type in [rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE]: + m = target_bone.matrix @ target_bone.bone.matrix_local.inverted() + self.__rigid_body_matrix_map[rigid_obj] = m + t, r, s = (m @ rigid_obj.matrix_local).decompose() + rigid_obj.location = t + rigid_obj.rotation_euler = r.to_euler(rigid_obj.rotation_mode) + fake_children = self.__fake_parent_map.get(rigid_obj, None) + if fake_children: + for fake_child in fake_children: + logging.debug(" - fake_child: %s", fake_child.name) + t, r, s = (m @ fake_child.matrix_local).decompose() + fake_child.location = t + fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode) + + if "mmd_tools_rigid_track" not in target_bone.constraints: + empty = bpy.data.objects.new(name="mmd_bonetrack", object_data=None) + FnContext.link_object(FnContext.ensure_context(), empty) + empty.matrix_world = target_bone.matrix + setattr(empty, Props.empty_display_type, "ARROWS") + setattr(empty, Props.empty_display_size, 0.1 * getattr(self.__root, Props.empty_display_size)) + empty.mmd_type = "TRACK_TARGET" + empty.hide_set(True) + empty.parent = self.temporaryGroupObject() + + rigid_obj.mmd_rigid.bone = bone_name + rigid_obj.constraints.remove(relation) + + self.__empty_parent_map[empty] = rigid_obj + + const_type = ("COPY_TRANSFORMS", "COPY_ROTATION")[rigid_type - 1] + const = target_bone.constraints.new(const_type) + const.mute = True + const.name = "mmd_tools_rigid_track" + const.target = empty + else: + empty = target_bone.constraints["mmd_tools_rigid_track"].target + ori_rigid_obj = self.__empty_parent_map[empty] + ori_rb = ori_rigid_obj.rigid_body + if ori_rb and rb.mass > ori_rb.mass: + logging.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name) + # re-parenting + rigid_obj.mmd_rigid.bone = bone_name + rigid_obj.constraints.remove(relation) + self.__empty_parent_map[empty] = rigid_obj + # revert change + ori_rigid_obj.mmd_rigid.bone = bone_name + else: + logging.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name) + + rb.collision_shape = rigid.shape + + def __getRigidRange(self, obj): + return (Vector(obj.bound_box[0]) - Vector(obj.bound_box[6])).length + + def __createNonCollisionConstraint(self, nonCollisionJointTable): + total_len = len(nonCollisionJointTable) + if total_len < 1: + return + + start_time = time.time() + logging.debug("-" * 60) + logging.debug(" creating ncc, counts: %d", total_len) + + ncc_obj = bpyutils.createObject(name="ncc", object_data=None) + ncc_obj.location = [0, 0, 0] + setattr(ncc_obj, Props.empty_display_type, "ARROWS") + setattr(ncc_obj, Props.empty_display_size, 0.5 * getattr(self.__root, Props.empty_display_size)) + ncc_obj.mmd_type = "NON_COLLISION_CONSTRAINT" + ncc_obj.hide_render = True + ncc_obj.parent = self.temporaryGroupObject() + + bpy.ops.rigidbody.constraint_add(type="GENERIC") + rb = ncc_obj.rigid_body_constraint + rb.disable_collisions = True + + ncc_objs = bpyutils.duplicateObject(ncc_obj, total_len) + logging.debug(" created %d ncc.", len(ncc_objs)) + + for ncc_obj, pair in zip(ncc_objs, nonCollisionJointTable): + rbc = ncc_obj.rigid_body_constraint + rbc.object1, rbc.object2 = pair + ncc_obj.hide_set(True) + ncc_obj.hide_select = True + logging.debug(" finish in %f seconds.", time.time() - start_time) + logging.debug("-" * 60) + + def buildRigids(self, non_collision_distance_scale, collision_margin): + logging.debug("--------------------------------") + logging.debug(" Build riggings of rigid bodies") + logging.debug("--------------------------------") + rigid_objects = list(self.rigidBodies()) + rigid_object_groups = [[] for i in range(16)] + for i in rigid_objects: + rigid_object_groups[i.mmd_rigid.collision_group_number].append(i) + + jointMap = {} + for joint in self.joints(): + rbc = joint.rigid_body_constraint + if rbc is None: + continue + rbc.disable_collisions = False + jointMap[frozenset((rbc.object1, rbc.object2))] = joint + + logging.info("Creating non collision constraints") + # create non collision constraints + nonCollisionJointTable = [] + non_collision_pairs = set() + rigid_object_cnt = len(rigid_objects) + for obj_a in rigid_objects: + for n, ignore in enumerate(obj_a.mmd_rigid.collision_group_mask): + if not ignore: + continue + for obj_b in rigid_object_groups[n]: + if obj_a == obj_b: + continue + pair = frozenset((obj_a, obj_b)) + if pair in non_collision_pairs: + continue + if pair in jointMap: + joint = jointMap[pair] + joint.rigid_body_constraint.disable_collisions = True + else: + distance = (obj_a.location - obj_b.location).length + if distance < non_collision_distance_scale * (self.__getRigidRange(obj_a) + self.__getRigidRange(obj_b)) * 0.5: + nonCollisionJointTable.append((obj_a, obj_b)) + non_collision_pairs.add(pair) + for cnt, i in enumerate(rigid_objects): + logging.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name) + self.updateRigid(i, collision_margin) + self.__createNonCollisionConstraint(nonCollisionJointTable) + return rigid_objects + + def buildJoints(self): + for i in self.joints(): + rbc = i.rigid_body_constraint + if rbc is None: + continue + m = self.__rigid_body_matrix_map.get(rbc.object1, None) + if m is None: + m = self.__rigid_body_matrix_map.get(rbc.object2, None) + if m is None: + continue + t, r, s = (m @ i.matrix_local).decompose() + i.location = t + i.rotation_euler = r.to_euler(i.rotation_mode) + + def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]): + armature_object = self.armature() + + armature: bpy.types.Armature + with bpyutils.edit_object(armature_object) as armature: + edit_bones = armature.edit_bones + rigid_body_object: bpy.types.Object + for rigid_body_object in self.rigidBodies(): + mmd_rigid: MMDRigidBody = rigid_body_object.mmd_rigid + if mmd_rigid.type not in target_modes: + continue + + bone_name: str = mmd_rigid.bone + edit_bone = edit_bones.get(bone_name) + if edit_bone is None: + continue + + editor(edit_bone) + + def disconnectPhysicsBones(self): + def editor(edit_bone: bpy.types.EditBone): + rna_prop_ui.rna_idprop_ui_create(edit_bone, "mmd_bone_use_connect", default=edit_bone.use_connect) + edit_bone.use_connect = False + + self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)}) + + def connectPhysicsBones(self): + def editor(edit_bone: bpy.types.EditBone): + mmd_bone_use_connect_str: Optional[str] = edit_bone.get("mmd_bone_use_connect") + if mmd_bone_use_connect_str is None: + return + + if not edit_bone.use_connect: # wasn't it overwritten? + edit_bone.use_connect = bool(mmd_bone_use_connect_str) + del edit_bone["mmd_bone_use_connect"] + + self.__editPhysicsBones(editor, {str(MODE_STATIC), str(MODE_DYNAMIC), str(MODE_DYNAMIC_BONE)}) diff --git a/core/mmd/core/morph.py b/core/mmd/core/morph.py new file mode 100644 index 0000000..aaa707e --- /dev/null +++ b/core/mmd/core/morph.py @@ -0,0 +1,798 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import logging +import re +from typing import TYPE_CHECKING, Tuple, cast + +import bpy + +from .. import bpyutils, utils +from ..bpyutils import FnContext, FnObject, TransformConstraintOp + +if TYPE_CHECKING: + from .model import Model + + +class FnMorph: + def __init__(self, morph, model: "Model"): + self.__morph = morph + self.__rig = model + + @classmethod + def storeShapeKeyOrder(cls, obj, shape_key_names): + if len(shape_key_names) < 1: + return + assert FnContext.get_active_object(FnContext.ensure_context()) == obj + if obj.data.shape_keys is None: + bpy.ops.object.shape_key_add() + + def __move_to_bottom(key_blocks, name): + obj.active_shape_key_index = key_blocks.find(name) + bpy.ops.object.shape_key_move(type="BOTTOM") + + key_blocks = obj.data.shape_keys.key_blocks + for name in shape_key_names: + if name not in key_blocks: + obj.shape_key_add(name=name, from_mix=False) + elif len(key_blocks) > 1: + __move_to_bottom(key_blocks, name) + + @classmethod + def fixShapeKeyOrder(cls, obj, shape_key_names): + if len(shape_key_names) < 1: + return + assert FnContext.get_active_object(FnContext.ensure_context()) == obj + key_blocks = getattr(obj.data.shape_keys, "key_blocks", None) + if key_blocks is None: + return + for name in shape_key_names: + idx = key_blocks.find(name) + if idx < 0: + continue + obj.active_shape_key_index = idx + bpy.ops.object.shape_key_move(type="BOTTOM") + + @staticmethod + def get_morph_slider(rig): + return _MorphSlider(rig) + + @staticmethod + def category_guess(morph): + name_lower = morph.name.lower() + if "mouth" in name_lower: + morph.category = "MOUTH" + elif "eye" in name_lower: + if "brow" in name_lower: + morph.category = "EYEBROW" + else: + morph.category = "EYE" + + @classmethod + def load_morphs(cls, rig): + mmd_root = rig.rootObject().mmd_root + vertex_morphs = mmd_root.vertex_morphs + uv_morphs = mmd_root.uv_morphs + for obj in rig.meshes(): + for kb in getattr(obj.data.shape_keys, "key_blocks", ())[1:]: + if not kb.name.startswith("mmd_") and kb.name not in vertex_morphs: + item = vertex_morphs.add() + item.name = kb.name + item.name_e = kb.name + cls.category_guess(item) + for g, name, x in FnMorph.get_uv_morph_vertex_groups(obj): + if name not in uv_morphs: + item = uv_morphs.add() + item.name = item.name_e = name + item.data_type = "VERTEX_GROUP" + cls.category_guess(item) + + @staticmethod + def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str): + assert isinstance(mesh_object.data, bpy.types.Mesh) + + shape_keys = mesh_object.data.shape_keys + if shape_keys is None: + return + + key_blocks = shape_keys.key_blocks + if key_blocks and shape_key_name in key_blocks: + FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name]) + + @staticmethod + def copy_shape_key(mesh_object: bpy.types.Object, src_name: str, dest_name: str): + assert isinstance(mesh_object.data, bpy.types.Mesh) + + shape_keys = mesh_object.data.shape_keys + if shape_keys is None: + return + + key_blocks = shape_keys.key_blocks + + if src_name not in key_blocks: + return + + if dest_name in key_blocks: + FnObject.mesh_remove_shape_key(mesh_object, key_blocks[dest_name]) + + mesh_object.active_shape_key_index = key_blocks.find(src_name) + mesh_object.show_only_shape_key, last = True, mesh_object.show_only_shape_key + mesh_object.shape_key_add(name=dest_name, from_mix=True) + mesh_object.show_only_shape_key = last + mesh_object.active_shape_key_index = key_blocks.find(dest_name) + + @staticmethod + def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes="XYZW"): + pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW") + # yield (vertex_group, morph_name, axis),... + return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name)) + + @staticmethod + def copy_uv_morph_vertex_groups(obj, src_name, dest_name): + for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, dest_name): + obj.vertex_groups.remove(vg) + + for vg_name in tuple(i[0].name for i in FnMorph.get_uv_morph_vertex_groups(obj, src_name)): + obj.vertex_groups.active = obj.vertex_groups[vg_name] + with bpy.context.temp_override(object=obj, window=bpy.context.window, region=bpy.context.region): + bpy.ops.object.vertex_group_copy() + obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name) + + @staticmethod + def overwrite_bone_morphs_from_action_pose(armature_object): + armature = armature_object.id_data + + # Use animation_data and action instead of action_pose + if armature.animation_data is None or armature.animation_data.action is None: + logging.warning('[WARNING] armature "%s" has no animation data or action', armature_object.name) + return + + action = armature.animation_data.action + pose_markers = action.pose_markers + + if not pose_markers: + return + + root = armature_object.parent + mmd_root = root.mmd_root + bone_morphs = mmd_root.bone_morphs + + utils.selectAObject(armature_object) + original_mode = bpy.context.object.mode + bpy.ops.object.mode_set(mode="POSE") + try: + for index, pose_marker in enumerate(pose_markers): + bone_morph = next(iter([m for m in bone_morphs if m.name == pose_marker.name]), None) + if bone_morph is None: + bone_morph = bone_morphs.add() + bone_morph.name = pose_marker.name + + bpy.ops.pose.select_all(action="SELECT") + bpy.ops.pose.transforms_clear() + + frame = pose_marker.frame + bpy.context.scene.frame_set(int(frame)) + + mmd_root.active_morph = bone_morphs.find(bone_morph.name) + bpy.ops.mmd_tools.apply_bone_morph() + + bpy.ops.pose.transforms_clear() + + finally: + bpy.ops.object.mode_set(mode=original_mode) + utils.selectAObject(root) + + @staticmethod + def clean_uv_morph_vertex_groups(obj): + # remove empty vertex groups of uv morphs + vg_indices = {g.index for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj)} + vertex_groups = obj.vertex_groups + for v in obj.data.vertices: + for x in v.groups: + if x.group in vg_indices and x.weight > 0: + vg_indices.remove(x.group) + for i in sorted(vg_indices, reverse=True): + vg = vertex_groups[i] + m = obj.modifiers.get("mmd_bind%s" % hash(vg.name), None) + if m: + obj.modifiers.remove(m) + vertex_groups.remove(vg) + + @staticmethod + def get_uv_morph_offset_map(obj, morph): + offset_map = {} # offset_map[vertex_index] = offset_xyzw + if morph.data_type == "VERTEX_GROUP": + scale = morph.vertex_group_scale + axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)} + for v in obj.data.vertices: + i = v.index + for x in v.groups: + if x.group in axis_map and x.weight > 0: + axis, weight = axis_map[x.group], x.weight + d = offset_map.setdefault(i, [0, 0, 0, 0]) + d["XYZW".index(axis[1])] += -weight * scale if axis[0] == "-" else weight * scale + else: + for val in morph.data: + i = val.index + if i in offset_map: + offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset)] + else: + offset_map[i] = val.offset + return offset_map + + @staticmethod + def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"): + vertex_groups = obj.vertex_groups + morph_name = getattr(morph, "name", None) + if offset_axes: + for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph_name, offset_axes): + vertex_groups.remove(vg) + if not morph_name or not offsets: + return + + axis_indices = tuple("XYZW".index(x) for x in offset_axes) or tuple(range(4)) + offset_map = FnMorph.get_uv_morph_offset_map(obj, morph) if offset_axes else {} + for data in offsets: + idx, offset = data.index, data.offset + for i in axis_indices: + offset_map.setdefault(idx, [0, 0, 0, 0])[i] += round(offset[i], 5) + + max_value = max(max(abs(x) for x in v) for v in offset_map.values() or ([0],)) + scale = morph.vertex_group_scale = max(abs(morph.vertex_group_scale), max_value) + for idx, offset in offset_map.items(): + for val, axis in zip(offset, "XYZW"): + if abs(val) > 1e-4: + vg_name = "UV_{0}{1}{2}".format(morph_name, "-" if val < 0 else "+", axis) + vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name) + vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE") + + def update_mat_related_mesh(self, new_mesh=None): + for offset in self.__morph.data: + # Use the new_mesh if provided + meshObj = new_mesh + if new_mesh is None: + # Try to find the mesh by material name + meshObj = self.__rig.findMesh(offset.material) + + if meshObj is None: + # Given this point we need to loop through all the meshes + for mesh in self.__rig.meshes(): + if mesh.data.materials.find(offset.material) >= 0: + meshObj = mesh + break + + # Finally update the reference + if meshObj is not None: + offset.related_mesh = meshObj.data.name + + @staticmethod + def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object): + """Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]""" + mmd_root = mmd_root_object.mmd_root + + def morph_data_equals(l, r) -> bool: + return ( + l.related_mesh_data == r.related_mesh_data + and l.offset_type == r.offset_type + and l.material == r.material + and all(a == b for a, b in zip(l.diffuse_color, r.diffuse_color)) + and all(a == b for a, b in zip(l.specular_color, r.specular_color)) + and l.shininess == r.shininess + and all(a == b for a, b in zip(l.ambient_color, r.ambient_color)) + and all(a == b for a, b in zip(l.edge_color, r.edge_color)) + and l.edge_weight == r.edge_weight + and all(a == b for a, b in zip(l.texture_factor, r.texture_factor)) + and all(a == b for a, b in zip(l.sphere_texture_factor, r.sphere_texture_factor)) + and all(a == b for a, b in zip(l.toon_texture_factor, r.toon_texture_factor)) + ) + + def morph_equals(l, r) -> bool: + return len(l.data) == len(r.data) and all(morph_data_equals(a, b) for a, b in zip(l.data, r.data)) + + # Remove duplicated mmd_root.material_morphs.data[] + for material_morph in mmd_root.material_morphs: + save_materil_morph_datas = [] + remove_material_morph_data_indices = [] + for index, material_morph_data in enumerate(material_morph.data): + if any(morph_data_equals(material_morph_data, saved_material_morph_data) for saved_material_morph_data in save_materil_morph_datas): + remove_material_morph_data_indices.append(index) + continue + save_materil_morph_datas.append(material_morph_data) + + for index in reversed(remove_material_morph_data_indices): + material_morph.data.remove(index) + + # Mark duplicated mmd_root.material_morphs[] + save_material_morphs = [] + remove_material_morph_names = [] + for material_morph in sorted(mmd_root.material_morphs, key=lambda m: m.name): + if any(morph_equals(material_morph, saved_material_morph) for saved_material_morph in save_material_morphs): + remove_material_morph_names.append(material_morph.name) + continue + + save_material_morphs.append(material_morph) + + # Remove marked mmd_root.material_morphs[] + for material_morph_name in remove_material_morph_names: + mmd_root.material_morphs.remove(mmd_root.material_morphs.find(material_morph_name)) + + +class _MorphSlider: + def __init__(self, model: "Model"): + self.__rig = model + + def placeholder(self, create=False, binded=False): + rig = self.__rig + root = rig.rootObject() + obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None) + if create and obj is None: + obj = bpy.data.objects.new(name=".placeholder", object_data=bpy.data.meshes.new(".placeholder")) + obj.mmd_type = "PLACEHOLDER" + obj.parent = root + FnContext.link_object(FnContext.ensure_context(), obj) + if obj and obj.data.shape_keys is None: + key = obj.shape_key_add(name="--- morph sliders ---") + key.mute = True + obj.active_shape_key_index = 0 + if binded and obj and obj.data.shape_keys.key_blocks[0].mute: + return None + return obj + + @property + def dummy_armature(self): + obj = self.placeholder() + return self.__dummy_armature(obj) if obj else None + + def __dummy_armature(self, obj, create=False): + arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None) + if create and arm is None: + arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature")) + arm.mmd_type = "PLACEHOLDER" + arm.parent = obj + FnContext.link_object(FnContext.ensure_context(), arm) + + from .bone import FnBone + + FnBone.setup_special_bone_collections(arm) + return arm + + def get(self, morph_name): + obj = self.placeholder() + if obj is None: + return None + key_blocks = obj.data.shape_keys.key_blocks + if key_blocks[0].mute: + return None + return key_blocks.get(morph_name, None) + + def create(self): + self.__rig.loadMorphs() + obj = self.placeholder(create=True) + self.__load(obj, self.__rig.rootObject().mmd_root) + return obj + + def __load(self, obj, mmd_root): + attr_list = ("group", "vertex", "bone", "uv", "material") + morph_sliders = obj.data.shape_keys.key_blocks + for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())): + name = m.name + # if name[-1] == '\\': # fix driver's bug??? + # m.name = name = name + ' ' + if name and name not in morph_sliders: + obj.shape_key_add(name=name, from_mix=False) + + @staticmethod + def __driver_variables(id_data, path, index=-1): + d = id_data.driver_add(path, index) + variables = d.driver.variables + for x in variables: + variables.remove(x) + return d.driver, variables + + @staticmethod + def __add_single_prop(variables, id_obj, data_path, prefix): + var = variables.new() + var.name = f"{prefix}{len(variables)}" + var.type = "SINGLE_PROP" + target = var.targets[0] + target.id_type = "OBJECT" + target.id = id_obj + target.data_path = data_path + return var + + @staticmethod + def __shape_key_driver_check(key_block, resolve_path=False): + if resolve_path: + try: + key_block.id_data.path_resolve(key_block.path_from_id()) + except ValueError: + return False + if not key_block.id_data.animation_data: + return True + d = key_block.id_data.animation_data.drivers.find(key_block.path_from_id("value")) + if isinstance(d, int): # for Blender 2.76 or older + data_path = key_block.path_from_id("value") + d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None) + return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables))) + + def __cleanup(self, names_in_use=None): + from math import ceil, floor + + names_in_use = names_in_use or {} + rig = self.__rig + morph_sliders = self.placeholder() + morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {} + for mesh_object in rig.meshes(): + for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[bpy.types.ShapeKey], ())): + if kb.name in names_in_use: + continue + + if kb.name.startswith("mmd_bind"): + kb.driver_remove("value") + ms = morph_sliders[kb.relative_key.name] + kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, floor(ms.value)), max(ms.slider_max, ceil(ms.value)) + kb.relative_key.value = ms.value + kb.relative_key.mute = False + FnObject.mesh_remove_shape_key(mesh_object, kb) + + elif kb.name in morph_sliders and self.__shape_key_driver_check(kb): + ms = morph_sliders[kb.name] + kb.driver_remove("value") + kb.slider_min, kb.slider_max = min(ms.slider_min, floor(kb.value)), max(ms.slider_max, ceil(kb.value)) + + for m in mesh_object.modifiers: # uv morph + if m.name.startswith("mmd_bind") and m.name not in names_in_use: + mesh_object.modifiers.remove(m) + + from .shader import _MaterialMorph + + for m in rig.materials(): + if m and m.node_tree: + for n in sorted((x for x in m.node_tree.nodes if x.name.startswith("mmd_bind")), key=lambda x: -x.location[0]): + _MaterialMorph.reset_morph_links(n) + m.node_tree.nodes.remove(n) + + attributes = set(TransformConstraintOp.min_max_attributes("LOCATION", "to")) + attributes |= set(TransformConstraintOp.min_max_attributes("ROTATION", "to")) + for b in rig.armature().pose.bones: + for c in b.constraints: + if c.name.startswith("mmd_bind") and c.name[:-4] not in names_in_use: + for attr in attributes: + c.driver_remove(attr) + b.constraints.remove(c) + + def unbind(self): + mmd_root = self.__rig.rootObject().mmd_root + + # after unbind, the weird lag problem will disappear. + mmd_root.morph_panel_show_settings = True + + for m in mmd_root.bone_morphs: + for d in m.data: + d.name = "" + for m in mmd_root.material_morphs: + for d in m.data: + d.name = "" + obj = self.placeholder() + if obj: + obj.data.shape_keys.key_blocks[0].mute = True + arm = self.__dummy_armature(obj) + if arm: + for b in arm.pose.bones: + if b.name.startswith("mmd_bind"): + b.driver_remove("location") + b.driver_remove("rotation_quaternion") + self.__cleanup() + + def bind(self): + rig = self.__rig + root = rig.rootObject() + armObj = rig.armature() + mmd_root = root.mmd_root + + # hide detail to avoid weird lag problem + mmd_root.morph_panel_show_settings = False + + obj = self.create() + arm = self.__dummy_armature(obj, create=True) + morph_sliders = obj.data.shape_keys.key_blocks + + # data gathering + group_map = {} + + shape_key_map = {} + uv_morph_map = {} + for mesh_object in rig.meshes(): + mesh_object.show_only_shape_key = False + key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ()) + for kb in key_blocks: + kb_name = kb.name + if kb_name not in morph_sliders: + continue + + if self.__shape_key_driver_check(kb, resolve_path=True): + name_bind, kb_bind = kb_name, kb + else: + name_bind = "mmd_bind%s" % hash(morph_sliders[kb_name]) + if name_bind not in key_blocks: + mesh_object.shape_key_add(name=name_bind, from_mix=False) + kb_bind = key_blocks[name_bind] + kb_bind.relative_key = kb + kb_bind.slider_min = -10 + kb_bind.slider_max = 10 + + data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"') + groups = [] + shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups)) + group_map.setdefault(("vertex_morphs", kb_name), []).append(groups) + + uv_layers = [l.name for l in mesh_object.data.uv_layers if not l.name.startswith("_")] + uv_layers += [""] * (5 - len(uv_layers)) + for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh_object): + morph = mmd_root.uv_morphs.get(morph_name, None) + if morph is None or morph.data_type != "VERTEX_GROUP": + continue + + uv_layer = "_" + uv_layers[morph.uv_index] if axis[1] in "ZW" else uv_layers[morph.uv_index] + if uv_layer not in mesh_object.data.uv_layers: + continue + + name_bind = "mmd_bind%s" % hash(vg.name) + uv_morph_map.setdefault(name_bind, ()) + mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP") + mod.show_expanded = False + mod.vertex_group = vg.name + mod.axis_u, mod.axis_v = ("Y", "X") if axis[1] in "YW" else ("X", "Y") + mod.uv_layer = uv_layer + name_bind = "mmd_bind%s" % hash(morph_name) + mod.object_from = mod.object_to = arm + if axis[0] == "-": + mod.bone_from, mod.bone_to = "mmd_bind_ctrl_base", name_bind + else: + mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base" + + bone_offset_map = {} + with bpyutils.edit_object(arm) as data: + from .bone import FnBone + + edit_bones = data.edit_bones + + def __get_bone(name, parent): + b = edit_bones.get(name, None) or edit_bones.new(name=name) + b.head = (0, 0, 0) + b.tail = (0, 0, 1) + b.use_deform = False + b.parent = parent + return b + + for m in mmd_root.bone_morphs: + morph_name = m.name.replace('"', '\\"') + data_path = f'data.shape_keys.key_blocks["{morph_name}"].value' + for d in m.data: + if not d.bone: + d.name = "" + continue + d.name = name_bind = f"mmd_bind{hash(d)}" + b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None)) + groups = [] + bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups) + group_map.setdefault(("bone_morphs", m.name), []).append(groups) + + ctrl_base = FnBone.set_edit_bone_to_dummy(__get_bone("mmd_bind_ctrl_base", None)) + for m in mmd_root.uv_morphs: + morph_name = m.name.replace('"', '\\"') + data_path = f'data.shape_keys.key_blocks["{morph_name}"].value' + scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale' + name_bind = f"mmd_bind{hash(m.name)}" + b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base)) + groups = [] + uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups)) + group_map.setdefault(("uv_morphs", m.name), []).append(groups) + + used_bone_names = bone_offset_map.keys() | uv_morph_map.keys() + used_bone_names.add(ctrl_base.name) + for b in edit_bones: # cleanup + if b.name.startswith("mmd_bind") and b.name not in used_bone_names: + edit_bones.remove(b) + + material_offset_map = {} + for m in mmd_root.material_morphs: + morph_name = m.name.replace('"', '\\"') + data_path = f'data.shape_keys.key_blocks["{morph_name}"].value' + groups = [] + group_map.setdefault(("material_morphs", m.name), []).append(groups) + material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups) + for d in m.data: + d.name = name_bind = f"mmd_bind{hash(d)}" + # add '#' before material name to avoid conflict with group_dict + table = material_offset_map.setdefault("#" + d.material, ([], [])) + table[1 if d.offset_type == "ADD" else 0].append((m.name, d, name_bind)) + + for m in mmd_root.group_morphs: + if len(m.data) != len(set(m.data.keys())): + logging.warning(' * Found duplicated morph data in Group Morph "%s"', m.name) + morph_name = m.name.replace('"', '\\"') + morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value' + for d in m.data: + data_name = d.name.replace('"', '\\"') + factor_path = f'mmd_root.group_morphs["{morph_name}"].data["{data_name}"].factor' + for groups in group_map.get((d.morph_type, d.name), ()): + groups.append((m.name, morph_path, factor_path)) + + self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys()) + + def __config_groups(variables, expression, groups): + for g_name, morph_path, factor_path in groups: + var = self.__add_single_prop(variables, obj, morph_path, "g") + fvar = self.__add_single_prop(variables, root, factor_path, "w") + expression = f"{expression}+{var.name}*{fvar.name}" + return expression + + # vertex morphs + for kb_bind, morph_data_path, groups in (i for l in shape_key_map.values() for i in l): + driver, variables = self.__driver_variables(kb_bind, "value") + var = self.__add_single_prop(variables, obj, morph_data_path, "v") + if kb_bind.name.startswith("mmd_bind"): + driver.expression = f"-({__config_groups(variables, var.name, groups)})" + kb_bind.relative_key.mute = True + else: + driver.expression = __config_groups(variables, var.name, groups) + kb_bind.mute = False + + # bone morphs + def __config_bone_morph(constraints, map_type, attributes, val, val_str): + c_name = f"mmd_bind{hash(data)}.{map_type[:3]}" + c = TransformConstraintOp.create(constraints, c_name, map_type) + TransformConstraintOp.update_min_max(c, val, None) + c.show_expanded = False + c.target = arm + c.subtarget = bname + for attr in attributes: + driver, variables = self.__driver_variables(armObj, c.path_from_id(attr)) + var = self.__add_single_prop(variables, obj, morph_data_path, "b") + expression = __config_groups(variables, var.name, groups) + sign = "-" if attr.startswith("to_min") else "" + driver.expression = f"{sign}{val_str}*({expression})" + + from math import pi + + attributes_rot = TransformConstraintOp.min_max_attributes("ROTATION", "to") + attributes_loc = TransformConstraintOp.min_max_attributes("LOCATION", "to") + for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values(): + b = arm.pose.bones[bname] + b.location = data.location + b.rotation_quaternion = data.rotation.__class__(*data.rotation.to_axis_angle()) # Fix for consistency + b.is_mmd_shadow_bone = True + b.mmd_shadow_bone_type = "BIND" + pb = armObj.pose.bones[data.bone] + __config_bone_morph(pb.constraints, "ROTATION", attributes_rot, pi, "pi") + __config_bone_morph(pb.constraints, "LOCATION", attributes_loc, 100, "100") + + # uv morphs + # HACK: workaround for Blender 2.80+, data_path can't be properly detected (Save & Reopen file also works) + root.parent, root.parent, root.matrix_parent_inverse = arm, root.parent, root.matrix_parent_inverse.copy() + b = arm.pose.bones["mmd_bind_ctrl_base"] + b.is_mmd_shadow_bone = True + b.mmd_shadow_bone_type = "BIND" + for bname, data_path, scale_path, groups in (i for l in uv_morph_map.values() for i in l): + b = arm.pose.bones[bname] + b.is_mmd_shadow_bone = True + b.mmd_shadow_bone_type = "BIND" + driver, variables = self.__driver_variables(b, "location", index=0) + var = self.__add_single_prop(variables, obj, data_path, "u") + fvar = self.__add_single_prop(variables, root, scale_path, "s") + driver.expression = f"({__config_groups(variables, var.name, groups)})*{fvar.name}" + + # material morphs + from .shader import _MaterialMorph + + group_dict = material_offset_map.get("group_dict", {}) + + def __config_material_morph(mat, morph_list): + nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list)) + for (morph_name, data, name_bind), node in zip(morph_list, nodes): + node.label, node.name = morph_name, name_bind + data_path, groups = group_dict[morph_name] + driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id("default_value")) + var = self.__add_single_prop(variables, obj, data_path, "m") + driver.expression = "%s" % __config_groups(variables, var.name, groups) + + for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")): + mul_all, add_all = material_offset_map.get("#", ([], [])) + if mat.name == "": + logging.warning("Oh no. The material name should never empty.") + mul_list, add_list = [], [] + else: + mat_name = "#" + mat.name + mul_list, add_list = material_offset_map.get(mat_name, ([], [])) + morph_list = tuple(mul_all + mul_list + add_all + add_list) + __config_material_morph(mat, morph_list) + mat_edge = bpy.data.materials.get("mmd_edge." + mat.name, None) + if mat_edge: + __config_material_morph(mat_edge, morph_list) + + morph_sliders[0].mute = False + + +class MigrationFnMorph: + @staticmethod + def update_mmd_morph(): + from .material import FnMaterial + + for root in bpy.data.objects: + if root.mmd_type != "ROOT": + continue + + for mat_morph in root.mmd_root.material_morphs: + for morph_data in mat_morph.data: + if morph_data.material_data is not None: + # SUPPORT_UNTIL: 5 LTS + # The material_id is also no longer used, but for compatibility with older version mmd_tools, keep it. + if "material_id" not in morph_data.material_data.mmd_material or "material_id" not in morph_data or morph_data.material_data.mmd_material["material_id"] == morph_data["material_id"]: + # In the new version, the related_mesh property is no longer used. + # Explicitly remove this property to avoid misuse. + if "related_mesh" in morph_data: + del morph_data["related_mesh"] + continue + + else: + # Compat case. The new version mmd_tools saved. And old version mmd_tools edit. Then new version mmd_tools load again. + # Go update path. + pass + + morph_data.material_data = None + if "material_id" in morph_data: + mat_id = morph_data["material_id"] + if mat_id != -1: + fnMat = FnMaterial.from_material_id(mat_id) + if fnMat: + morph_data.material_data = fnMat.material + else: + morph_data["material_id"] = -1 + + morph_data.related_mesh_data = None + if "related_mesh" in morph_data: + related_mesh = morph_data["related_mesh"] + del morph_data["related_mesh"] + if related_mesh != "" and related_mesh in bpy.data.meshes: + morph_data.related_mesh_data = bpy.data.meshes[related_mesh] + + @staticmethod + def ensure_material_id_not_conflict(): + mat_ids_set = set() + + # The reference library properties cannot be modified and bypassed in advance. + need_update_mat = [] + for mat in bpy.data.materials: + if mat.mmd_material.material_id < 0: + continue + if mat.library is not None: + mat_ids_set.add(mat.mmd_material.material_id) + else: + need_update_mat.append(mat) + + for mat in need_update_mat: + if mat.mmd_material.material_id in mat_ids_set: + mat.mmd_material.material_id = max(mat_ids_set) + 1 + mat_ids_set.add(mat.mmd_material.material_id) + + @staticmethod + def compatible_with_old_version_mmd_tools(): + MigrationFnMorph.ensure_material_id_not_conflict() + + for root in bpy.data.objects: + if root.mmd_type != "ROOT": + continue + + for mat_morph in root.mmd_root.material_morphs: + for morph_data in mat_morph.data: + morph_data["related_mesh"] = morph_data.related_mesh + + if morph_data.material_data is None: + morph_data.material_id = -1 + else: + morph_data.material_id = morph_data.material_data.mmd_material.material_id diff --git a/core/mmd/core/pmx/__init__.py b/core/mmd/core/pmx/__init__.py new file mode 100644 index 0000000..7de70bd --- /dev/null +++ b/core/mmd/core/pmx/__init__.py @@ -0,0 +1,1625 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import logging +import os +import struct + + +class InvalidFileError(Exception): + pass +class UnsupportedVersionError(Exception): + pass + +class FileStream: + def __init__(self, path, file_obj, pmx_header): + self.__path = path + self.__file_obj = file_obj + self.__header = pmx_header + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def path(self): + return self.__path + + def header(self): + if self.__header is None: + raise Exception + return self.__header + + def setHeader(self, pmx_header): + self.__header = pmx_header + + def close(self): + if self.__file_obj is not None: + logging.debug('close the file("%s")', self.__path) + self.__file_obj.close() + self.__file_obj = None + +class FileReadStream(FileStream): + def __init__(self, path, pmx_header=None): + self.__fin = open(path, 'rb') + FileStream.__init__(self, path, self.__fin, pmx_header) + + def __readIndex(self, size, typedict): + index = None + if size in typedict : + index, = struct.unpack(typedict[size], self.__fin.read(size)) + else: + raise ValueError('invalid data size %s'%str(size)) + return index + + def __readSignedIndex(self, size): + return self.__readIndex(size, { 1 :"'%self.charset + +class Coordinate: + """ """ + def __init__(self, xAxis, zAxis): + self.x_axis = xAxis + self.z_axis = zAxis + +class Header: + PMX_SIGN = b'PMX ' + VERSION = 2.0 + def __init__(self, model=None): + self.sign = self.PMX_SIGN + self.version = 0 + + self.encoding = Encoding('utf-16-le') + self.additional_uvs = 0 + + self.vertex_index_size = 1 + self.texture_index_size = 1 + self.material_index_size = 1 + self.bone_index_size = 1 + self.morph_index_size = 1 + self.rigid_index_size = 1 + + if model is not None: + self.updateIndexSizes(model) + + def updateIndexSizes(self, model): + self.vertex_index_size = self.__getIndexSize(len(model.vertices), False) + self.texture_index_size = self.__getIndexSize(len(model.textures), True) + self.material_index_size = self.__getIndexSize(len(model.materials), True) + self.bone_index_size = self.__getIndexSize(len(model.bones), True) + self.morph_index_size = self.__getIndexSize(len(model.morphs), True) + self.rigid_index_size = self.__getIndexSize(len(model.rigids), True) + + @staticmethod + def __getIndexSize(num, signed): + s = 1 + if signed: + s = 2 + if (1<<8)/s > num: + return 1 + elif (1<<16)/s > num: + return 2 + else: + return 4 + + def load(self, fs): + logging.info('loading pmx header information...') + self.sign = fs.readBytes(4) + logging.debug('File signature is %s', self.sign) + if self.sign[:3] != self.PMX_SIGN[:3]: + logging.info('File signature is invalid') + logging.error('This file is unsupported format, or corrupt file.') + raise InvalidFileError('File signature is invalid.') + self.version = fs.readFloat() + logging.info('pmx format version: %f', self.version) + if self.version != self.VERSION: + logging.error('PMX version %.1f is unsupported', self.version) + raise UnsupportedVersionError('unsupported PMX version: %.1f'%self.version) + if fs.readByte() != 8 or self.sign[3] != self.PMX_SIGN[3]: + logging.warning(' * This file might be corrupted.') + self.encoding = Encoding(fs.readByte()) + self.additional_uvs = fs.readByte() + self.vertex_index_size = fs.readByte() + self.texture_index_size = fs.readByte() + self.material_index_size = fs.readByte() + self.bone_index_size = fs.readByte() + self.morph_index_size = fs.readByte() + self.rigid_index_size = fs.readByte() + + logging.info('----------------------------') + logging.info('pmx header information') + logging.info('----------------------------') + logging.info('pmx version: %.1f', self.version) + logging.info('encoding: %s', str(self.encoding)) + logging.info('number of uvs: %d', self.additional_uvs) + logging.info('vertex index size: %d byte(s)', self.vertex_index_size) + logging.info('texture index: %d byte(s)', self.texture_index_size) + logging.info('material index: %d byte(s)', self.material_index_size) + logging.info('bone index: %d byte(s)', self.bone_index_size) + logging.info('morph index: %d byte(s)', self.morph_index_size) + logging.info('rigid index: %d byte(s)', self.rigid_index_size) + logging.info('----------------------------') + + def save(self, fs): + fs.writeBytes(self.PMX_SIGN) + fs.writeFloat(self.VERSION) + fs.writeByte(8) + fs.writeByte(self.encoding.index) + fs.writeByte(self.additional_uvs) + fs.writeByte(self.vertex_index_size) + fs.writeByte(self.texture_index_size) + fs.writeByte(self.material_index_size) + fs.writeByte(self.bone_index_size) + fs.writeByte(self.morph_index_size) + fs.writeByte(self.rigid_index_size) + + def __repr__(self): + return '
'%( + str(self.encoding), + self.additional_uvs, + self.vertex_index_size, + self.texture_index_size, + self.material_index_size, + self.bone_index_size, + self.morph_index_size, + self.rigid_index_size, + ) + +class Model: + def __init__(self): + self.filepath = '' + self.header = None + + self.name = '' + self.name_e = '' + self.comment = '' + self.comment_e = '' + + self.vertices = [] + self.faces = [] + self.textures = [] + self.materials = [] + self.bones = [] + self.morphs = [] + + self.display = [] + dsp_root = Display() + dsp_root.isSpecial = True + dsp_root.name = 'Root' + dsp_root.name_e = 'Root' + self.display.append(dsp_root) + dsp_face = Display() + dsp_face.isSpecial = True + dsp_face.name = '表情' + dsp_face.name_e = 'Facial' + self.display.append(dsp_face) + + self.rigids = [] + self.joints = [] + + def load(self, fs): + self.filepath = fs.path() + self.header = fs.header() + + self.name = fs.readStr() + self.name_e = fs.readStr() + + self.comment = fs.readStr() + self.comment_e = fs.readStr() + + logging.info('Model name: %s', self.name) + logging.info('Model name(english): %s', self.name_e) + logging.info('Comment:%s', self.comment) + logging.info('Comment(english):%s', self.comment_e) + + logging.info('') + logging.info('------------------------------') + logging.info('Load Vertices') + logging.info('------------------------------') + num_vertices = fs.readInt() + self.vertices = [] + for i in range(num_vertices): + v = Vertex() + v.load(fs) + self.vertices.append(v) + logging.info('----- Loaded %d vertices', len(self.vertices)) + + logging.info('') + logging.info('------------------------------') + logging.info(' Load Faces') + logging.info('------------------------------') + num_faces = fs.readInt() + self.faces = [] + for i in range(int(num_faces/3)): + f1 = fs.readVertexIndex() + f2 = fs.readVertexIndex() + f3 = fs.readVertexIndex() + self.faces.append((f3, f2, f1)) + logging.info(' Load %d faces', len(self.faces)) + + logging.info('') + logging.info('------------------------------') + logging.info(' Load Textures') + logging.info('------------------------------') + num_textures = fs.readInt() + self.textures = [] + for i in range(num_textures): + t = Texture() + t.load(fs) + self.textures.append(t) + logging.info('Texture %d: %s', i, t.path) + logging.info(' ----- Loaded %d textures', len(self.textures)) + + logging.info('') + logging.info('------------------------------') + logging.info(' Load Materials') + logging.info('------------------------------') + num_materials = fs.readInt() + self.materials = [] + for i in range(num_materials): + m = Material() + m.load(fs, num_textures) + self.materials.append(m) + + logging.info('Material %d: %s', i, m.name) + logging.debug(' Name(english): %s', m.name_e) + logging.debug(' Comment: %s', m.comment) + logging.debug(' Vertex Count: %d', m.vertex_count) + logging.debug(' Diffuse: (%.2f, %.2f, %.2f, %.2f)', *m.diffuse) + logging.debug(' Specular: (%.2f, %.2f, %.2f)', *m.specular) + logging.debug(' Shininess: %f', m.shininess) + logging.debug(' Ambient: (%.2f, %.2f, %.2f)', *m.ambient) + logging.debug(' Double Sided: %s', str(m.is_double_sided)) + logging.debug(' Drop Shadow: %s', str(m.enabled_drop_shadow)) + logging.debug(' Self Shadow: %s', str(m.enabled_self_shadow)) + logging.debug(' Self Shadow Map: %s', str(m.enabled_self_shadow_map)) + logging.debug(' Edge: %s', str(m.enabled_toon_edge)) + logging.debug(' Edge Color: (%.2f, %.2f, %.2f, %.2f)', *m.edge_color) + logging.debug(' Edge Size: %.2f', m.edge_size) + if m.texture != -1: + logging.debug(' Texture Index: %d', m.texture) + else: + logging.debug(' Texture: None') + if m.sphere_texture != -1: + logging.debug(' Sphere Texture Index: %d', m.sphere_texture) + logging.debug(' Sphere Texture Mode: %d', m.sphere_texture_mode) + else: + logging.debug(' Sphere Texture: None') + logging.debug('') + + logging.info('----- Loaded %d materials.', len(self.materials)) + + logging.info('') + logging.info('------------------------------') + logging.info(' Load Bones') + logging.info('------------------------------') + num_bones = fs.readInt() + self.bones = [] + for i in range(num_bones): + b = Bone() + b.load(fs) + self.bones.append(b) + + logging.info('Bone %d: %s', i, b.name) + logging.debug(' Name(english): %s', b.name_e) + logging.debug(' Location: (%f, %f, %f)', *b.location) + logging.debug(' displayConnection: %s', str(b.displayConnection)) + logging.debug(' Parent: %s', str(b.parent)) + logging.debug(' Transform Order: %s', str(b.transform_order)) + logging.debug(' Rotatable: %s', str(b.isRotatable)) + logging.debug(' Movable: %s', str(b.isMovable)) + logging.debug(' Visible: %s', str(b.visible)) + logging.debug(' Controllable: %s', str(b.isControllable)) + logging.debug(' Additional Location: %s', str(b.hasAdditionalLocation)) + logging.debug(' Additional Rotation: %s', str(b.hasAdditionalRotate)) + if b.additionalTransform is not None: + logging.debug(' Additional Transform: Bone:%d, influence: %f', *b.additionalTransform) + logging.debug(' IK: %s', str(b.isIK)) + if b.isIK: + logging.debug(' Unit Angle: %f', b.rotationConstraint) + logging.debug(' Target: %d', b.target) + for j, link in enumerate(b.ik_links): + logging.debug(' IK Link %d: %d, %s - %s', j, link.target, str(link.minimumAngle), str(link.maximumAngle)) + logging.debug('') + logging.info('----- Loaded %d bones.', len(self.bones)) + + logging.info('') + logging.info('------------------------------') + logging.info(' Load Morphs') + logging.info('------------------------------') + num_morph = fs.readInt() + self.morphs = [] + display_categories = {0: 'System', 1: 'Eyebrow', 2: 'Eye', 3: 'Mouth', 4: 'Other'} + for i in range(num_morph): + m = Morph.create(fs) + self.morphs.append(m) + + logging.info('%s %d: %s', m.__class__.__name__, i, m.name) + logging.debug(' Name(english): %s', m.name_e) + logging.debug(' Category: %s (%d)', display_categories.get(m.category, '#Invalid'), m.category) + logging.debug('') + logging.info('----- Loaded %d morphs.', len(self.morphs)) + + logging.info('') + logging.info('------------------------------') + logging.info(' Load Display Items') + logging.info('------------------------------') + num_disp = fs.readInt() + self.display = [] + for i in range(num_disp): + d = Display() + d.load(fs) + self.display.append(d) + + logging.info('Display Item %d: %s', i, d.name) + logging.debug(' Name(english): %s', d.name_e) + logging.debug('') + logging.info('----- Loaded %d display items.', len(self.display)) + + logging.info('') + logging.info('------------------------------') + logging.info(' Load Rigid Bodies') + logging.info('------------------------------') + num_rigid = fs.readInt() + self.rigids = [] + rigid_types = {0: 'Sphere', 1: 'Box', 2: 'Capsule'} + rigid_modes = {0: 'Static', 1: 'Dynamic', 2: 'Dynamic(track to bone)'} + for i in range(num_rigid): + r = Rigid() + r.load(fs) + self.rigids.append(r) + logging.info('Rigid Body %d: %s', i, r.name) + logging.debug(' Name(english): %s', r.name_e) + logging.debug(' Type: %s', rigid_types[r.type]) + logging.debug(' Mode: %s (%d)', rigid_modes.get(r.mode, '#Invalid'), r.mode) + logging.debug(' Related bone: %s', r.bone) + logging.debug(' Collision group: %d', r.collision_group_number) + logging.debug(' Collision group mask: 0x%x', r.collision_group_mask) + logging.debug(' Size: (%f, %f, %f)', *r.size) + logging.debug(' Location: (%f, %f, %f)', *r.location) + logging.debug(' Rotation: (%f, %f, %f)', *r.rotation) + logging.debug(' Mass: %f', r.mass) + logging.debug(' Bounce: %f', r.bounce) + logging.debug(' Friction: %f', r.friction) + logging.debug('') + + logging.info('----- Loaded %d rigid bodies.', len(self.rigids)) + + logging.info('') + logging.info('------------------------------') + logging.info(' Load Joints') + logging.info('------------------------------') + num_joints = fs.readInt() + self.joints = [] + for i in range(num_joints): + j = Joint() + j.load(fs) + self.joints.append(j) + + logging.info('Joint %d: %s', i, j.name) + logging.debug(' Name(english): %s', j.name_e) + logging.debug(' Rigid A: %s', j.src_rigid) + logging.debug(' Rigid B: %s', j.dest_rigid) + logging.debug(' Location: (%f, %f, %f)', *j.location) + logging.debug(' Rotation: (%f, %f, %f)', *j.rotation) + logging.debug(' Location Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_location + j.maximum_location)) + logging.debug(' Rotation Limit: (%f, %f, %f) - (%f, %f, %f)', *(j.minimum_rotation + j.maximum_rotation)) + logging.debug(' Spring: (%f, %f, %f)', *j.spring_constant) + logging.debug(' Spring(rotation): (%f, %f, %f)', *j.spring_rotation_constant) + logging.debug('') + + logging.info('----- Loaded %d joints.', len(self.joints)) + + def save(self, fs): + fs.writeStr(self.name) + fs.writeStr(self.name_e) + + fs.writeStr(self.comment) + fs.writeStr(self.comment_e) + + logging.info('''exportings pmx model data... +name: %s +name(english): %s +comment: +%s +comment(english): +%s +''', self.name, self.name_e, self.comment, self.comment_e) + + logging.info('exporting vertices... %d', len(self.vertices)) + fs.writeInt(len(self.vertices)) + for i in self.vertices: + i.save(fs) + logging.info('finished exporting vertices.') + + logging.info('exporting faces... %d', len(self.faces)) + fs.writeInt(len(self.faces)*3) + for f3, f2, f1 in self.faces: + fs.writeVertexIndex(f1) + fs.writeVertexIndex(f2) + fs.writeVertexIndex(f3) + logging.info('finished exporting faces.') + + logging.info('exporting textures... %d', len(self.textures)) + fs.writeInt(len(self.textures)) + for i in self.textures: + i.save(fs) + logging.info('finished exporting textures.') + + logging.info('exporting materials... %d', len(self.materials)) + fs.writeInt(len(self.materials)) + for i in self.materials: + i.save(fs) + logging.info('finished exporting materials.') + + logging.info('exporting bones... %d', len(self.bones)) + fs.writeInt(len(self.bones)) + for i in self.bones: + i.save(fs) + logging.info('finished exporting bones.') + + logging.info('exporting morphs... %d', len(self.morphs)) + fs.writeInt(len(self.morphs)) + for i in self.morphs: + i.save(fs) + logging.info('finished exporting morphs.') + + logging.info('exporting display items... %d', len(self.display)) + fs.writeInt(len(self.display)) + for i in self.display: + i.save(fs) + logging.info('finished exporting display items.') + + logging.info('exporting rigid bodies... %d', len(self.rigids)) + fs.writeInt(len(self.rigids)) + for i in self.rigids: + i.save(fs) + logging.info('finished exporting rigid bodies.') + + logging.info('exporting joints... %d', len(self.joints)) + fs.writeInt(len(self.joints)) + for i in self.joints: + i.save(fs) + logging.info('finished exporting joints.') + logging.info('finished exporting the model.') + + + def __repr__(self): + return ''%( + self.name, + self.name_e, + self.comment, + self.comment_e, + str(self.textures), + ) + +class Vertex: + def __init__(self): + self.co = [0.0, 0.0, 0.0] + self.normal = [0.0, 0.0, 0.0] + self.uv = [0.0, 0.0] + self.additional_uvs = [] + self.weight = None + self.edge_scale = 1 + + def __repr__(self): + return ''%( + str(self.co), + str(self.normal), + str(self.uv), + str(self.additional_uvs), + str(self.weight), + str(self.edge_scale), + ) + + def load(self, fs): + self.co = fs.readVector(3) + self.normal = fs.readVector(3) + self.uv = fs.readVector(2) + self.additional_uvs = [] + for i in range(fs.header().additional_uvs): + self.additional_uvs.append(fs.readVector(4)) + self.weight = BoneWeight() + self.weight.load(fs) + self.edge_scale = fs.readFloat() + + def save(self, fs): + fs.writeVector(self.co) + fs.writeVector(self.normal) + fs.writeVector(self.uv) + for i in self.additional_uvs: + fs.writeVector(i) + for i in range(fs.header().additional_uvs-len(self.additional_uvs)): + fs.writeVector((0,0,0,0)) + self.weight.save(fs) + fs.writeFloat(self.edge_scale) + +class BoneWeightSDEF: + def __init__(self, weight=0, c=None, r0=None, r1=None): + self.weight = weight + self.c = c + self.r0 = r0 + self.r1 = r1 + +class BoneWeight: + BDEF1 = 0 + BDEF2 = 1 + BDEF4 = 2 + SDEF = 3 + + TYPES = [ + (BDEF1, 'BDEF1'), + (BDEF2, 'BDEF2'), + (BDEF4, 'BDEF4'), + (SDEF, 'SDEF'), + ] + + def __init__(self): + self.bones = [] + self.weights = [] + self.type = self.BDEF1 + + def convertIdToName(self, type_id): + t = list(filter(lambda x: x[0]==type_id, self.TYPES)) + if len(t) > 0: + return t[0][1] + else: + return None + + def convertNameToId(self, type_name): + t = list(filter(lambda x: x[1]==type_name, self.TYPES)) + if len(t) > 0: + return t[0][0] + else: + return None + + def load(self, fs): + self.type = fs.readByte() + self.bones = [] + self.weights = [] + + if self.type == self.BDEF1: + self.bones.append(fs.readBoneIndex()) + elif self.type == self.BDEF2: + self.bones.append(fs.readBoneIndex()) + self.bones.append(fs.readBoneIndex()) + self.weights.append(fs.readFloat()) + elif self.type == self.BDEF4: + self.bones.append(fs.readBoneIndex()) + self.bones.append(fs.readBoneIndex()) + self.bones.append(fs.readBoneIndex()) + self.bones.append(fs.readBoneIndex()) + self.weights = fs.readVector(4) + elif self.type == self.SDEF: + self.bones.append(fs.readBoneIndex()) + self.bones.append(fs.readBoneIndex()) + self.weights = BoneWeightSDEF() + self.weights.weight = fs.readFloat() + self.weights.c = fs.readVector(3) + self.weights.r0 = fs.readVector(3) + self.weights.r1 = fs.readVector(3) + else: + raise ValueError('invalid weight type %s'%str(self.type)) + + def save(self, fs): + fs.writeByte(self.type) + if self.type == self.BDEF1: + fs.writeBoneIndex(self.bones[0]) + elif self.type == self.BDEF2: + for i in range(2): + fs.writeBoneIndex(self.bones[i]) + fs.writeFloat(self.weights[0]) + elif self.type == self.BDEF4: + for i in range(4): + fs.writeBoneIndex(self.bones[i]) + for i in range(4): + fs.writeFloat(self.weights[i]) + elif self.type == self.SDEF: + for i in range(2): + fs.writeBoneIndex(self.bones[i]) + if not isinstance(self.weights, BoneWeightSDEF): + raise ValueError + fs.writeFloat(self.weights.weight) + fs.writeVector(self.weights.c) + fs.writeVector(self.weights.r0) + fs.writeVector(self.weights.r1) + else: + raise ValueError('invalid weight type %s'%str(self.type)) + + +class Texture: + def __init__(self): + self.path = '' + + def __repr__(self): + return ''%str(self.path) + + def load(self, fs): + self.path = fs.readStr() + self.path = self.path.replace('\\', os.path.sep) + if not os.path.isabs(self.path): + self.path = os.path.normpath(os.path.join(os.path.dirname(fs.path()), self.path)) + + def save(self, fs): + try: + relPath = os.path.relpath(self.path, os.path.dirname(fs.path())) + except ValueError: + relPath = self.path + relPath = relPath.replace(os.path.sep, '\\') # always save using windows path conventions + logging.info('writing to pmx file the relative texture path: %s', relPath) + fs.writeStr(relPath) + +class SharedTexture(Texture): + def __init__(self): + self.number = 0 + self.prefix = '' + +class Material: + SPHERE_MODE_OFF = 0 + SPHERE_MODE_MULT = 1 + SPHERE_MODE_ADD = 2 + SPHERE_MODE_SUBTEX = 3 + + def __init__(self): + self.name = '' + self.name_e = '' + + self.diffuse = [] + self.specular = [] + self.shininess = 0 + self.ambient = [] + + self.is_double_sided = True + self.enabled_drop_shadow = True + self.enabled_self_shadow_map = True + self.enabled_self_shadow = True + self.enabled_toon_edge = False + + self.edge_color = [] + self.edge_size = 1 + + self.texture = -1 + self.sphere_texture = -1 + self.sphere_texture_mode = 0 + self.is_shared_toon_texture = True + self.toon_texture = 0 + + self.comment = '' + self.vertex_count = 0 + + def __repr__(self): + return ''%( + self.name, + self.name_e, + str(self.diffuse), + str(self.specular), + str(self.shininess), + str(self.ambient), + str(self.is_double_sided), + str(self.enabled_drop_shadow), + str(self.enabled_self_shadow_map), + str(self.enabled_self_shadow), + str(self.enabled_toon_edge), + str(self.edge_color), + str(self.edge_size), + str(self.texture), + str(self.sphere_texture), + str(self.toon_texture), + str(self.comment),) + + def load(self, fs, num_textures): + def __tex_index(index): + return index if 0 <= index < num_textures else -1 + + self.name = fs.readStr() + self.name_e = fs.readStr() + + self.diffuse = fs.readVector(4) + self.specular = fs.readVector(3) + self.shininess = fs.readFloat() + self.ambient = fs.readVector(3) + + flags = fs.readByte() + self.is_double_sided = bool(flags & 1) + self.enabled_drop_shadow = bool(flags & 2) + self.enabled_self_shadow_map = bool(flags & 4) + self.enabled_self_shadow = bool(flags & 8) + self.enabled_toon_edge = bool(flags & 16) + + self.edge_color = fs.readVector(4) + self.edge_size = fs.readFloat() + + self.texture = __tex_index(fs.readTextureIndex()) + self.sphere_texture = __tex_index(fs.readTextureIndex()) + self.sphere_texture_mode = fs.readSignedByte() + + self.is_shared_toon_texture = fs.readSignedByte() + self.is_shared_toon_texture = (self.is_shared_toon_texture == 1) + if self.is_shared_toon_texture: + self.toon_texture = fs.readSignedByte() + else: + self.toon_texture = __tex_index(fs.readTextureIndex()) + + self.comment = fs.readStr() + self.vertex_count = fs.readInt() + + def save(self, fs): + fs.writeStr(self.name) + fs.writeStr(self.name_e) + + fs.writeVector(self.diffuse) + fs.writeVector(self.specular) + fs.writeFloat(self.shininess) + fs.writeVector(self.ambient) + + flags = 0 + flags |= int(self.is_double_sided) + flags |= int(self.enabled_drop_shadow) << 1 + flags |= int(self.enabled_self_shadow_map) << 2 + flags |= int(self.enabled_self_shadow) << 3 + flags |= int(self.enabled_toon_edge) << 4 + fs.writeByte(flags) + + fs.writeVector(self.edge_color) + fs.writeFloat(self.edge_size) + + fs.writeTextureIndex(self.texture) + fs.writeTextureIndex(self.sphere_texture) + fs.writeSignedByte(self.sphere_texture_mode) + + if self.is_shared_toon_texture: + fs.writeSignedByte(1) + fs.writeSignedByte(self.toon_texture) + else: + fs.writeSignedByte(0) + fs.writeTextureIndex(self.toon_texture) + + fs.writeStr(self.comment) + fs.writeInt(self.vertex_count) + + +class Bone: + def __init__(self): + self.name = '' + self.name_e = '' + + self.location = [] + self.parent = None + self.transform_order = 0 + + # 接続先表示方法 + # 座標オフセット(float3)または、boneIndex(int) + self.displayConnection = -1 + + self.isRotatable = True + self.isMovable = True + self.visible = True + self.isControllable = True + + self.isIK = False + + # 回転付与 + self.hasAdditionalRotate = False + + # 移動付与 + self.hasAdditionalLocation = False + + # 回転付与および移動付与の付与量 + self.additionalTransform = None + + # 軸固定 + # 軸ベクトルfloat3 + self.axis = None + + # ローカル軸 + self.localCoordinate = None + + self.transAfterPhis = False + + # 外部親変形 + self.externalTransKey = None + + # 以下IKボーンのみ有効な変数 + self.target = None + self.loopCount = 8 + # IKループ計三時の1回あたりの制限角度(ラジアン) + self.rotationConstraint = 0.03 + + # IKLinkオブジェクトの配列 + self.ik_links = [] + + def __repr__(self): + return ''%( + self.name, + self.name_e,) + + def load(self, fs): + self.name = fs.readStr() + self.name_e = fs.readStr() + + self.location = fs.readVector(3) + self.parent = fs.readBoneIndex() + self.transform_order = fs.readInt() + + flags = fs.readShort() + if flags & 0x0001: + self.displayConnection = fs.readBoneIndex() + else: + self.displayConnection = fs.readVector(3) + + self.isRotatable = ((flags & 0x0002) != 0) + self.isMovable = ((flags & 0x0004) != 0) + self.visible = ((flags & 0x0008) != 0) + self.isControllable = ((flags & 0x0010) != 0) + + self.isIK = ((flags & 0x0020) != 0) + + self.hasAdditionalRotate = ((flags & 0x0100) != 0) + self.hasAdditionalLocation = ((flags & 0x0200) != 0) + if self.hasAdditionalRotate or self.hasAdditionalLocation: + t = fs.readBoneIndex() + v = fs.readFloat() + self.additionalTransform = (t, v) + else: + self.additionalTransform = None + + + if flags & 0x0400: + self.axis = fs.readVector(3) + else: + self.axis = None + + if flags & 0x0800: + xaxis = fs.readVector(3) + zaxis = fs.readVector(3) + self.localCoordinate = Coordinate(xaxis, zaxis) + else: + self.localCoordinate = None + + self.transAfterPhis = ((flags & 0x1000) != 0) + + if flags & 0x2000: + self.externalTransKey = fs.readInt() + else: + self.externalTransKey = None + + if self.isIK: + self.target = fs.readBoneIndex() + self.loopCount = fs.readInt() + self.rotationConstraint = fs.readFloat() + + iklink_num = fs.readInt() + self.ik_links = [] + for i in range(iklink_num): + link = IKLink() + link.load(fs) + self.ik_links.append(link) + + def save(self, fs): + fs.writeStr(self.name) + fs.writeStr(self.name_e) + + fs.writeVector(self.location) + fs.writeBoneIndex(-1 if self.parent is None else self.parent) + fs.writeInt(self.transform_order) + + flags = 0 + flags |= int(isinstance(self.displayConnection, int)) + flags |= int(self.isRotatable) << 1 + flags |= int(self.isMovable) << 2 + flags |= int(self.visible) << 3 + flags |= int(self.isControllable) << 4 + flags |= int(self.isIK) << 5 + + flags |= int(self.hasAdditionalRotate) << 8 + flags |= int(self.hasAdditionalLocation) << 9 + flags |= int(self.axis is not None) << 10 + flags |= int(self.localCoordinate is not None) << 11 + + flags |= int(self.transAfterPhis) << 12 + flags |= int(self.externalTransKey is not None) << 13 + + fs.writeShort(flags) + + if flags & 0x0001: + fs.writeBoneIndex(self.displayConnection) + else: + fs.writeVector(self.displayConnection) + + if self.hasAdditionalRotate or self.hasAdditionalLocation: + fs.writeBoneIndex(self.additionalTransform[0]) + fs.writeFloat(self.additionalTransform[1]) + + if flags & 0x0400: + fs.writeVector(self.axis) + + if flags & 0x0800: + fs.writeVector(self.localCoordinate.x_axis) + fs.writeVector(self.localCoordinate.z_axis) + + if flags & 0x2000: + fs.writeInt(self.externalTransKey) + + if self.isIK: + fs.writeBoneIndex(self.target) + fs.writeInt(self.loopCount) + fs.writeFloat(self.rotationConstraint) + + fs.writeInt(len(self.ik_links)) + for i in self.ik_links: + i.save(fs) + + +class IKLink: + def __init__(self): + self.target = None + self.maximumAngle = None + self.minimumAngle = None + + def __repr__(self): + return ''%(str(self.target)) + + def load(self, fs): + self.target = fs.readBoneIndex() + flag = fs.readByte() + if flag == 1: + self.minimumAngle = fs.readVector(3) + self.maximumAngle = fs.readVector(3) + else: + self.minimumAngle = None + self.maximumAngle = None + + def save(self, fs): + fs.writeBoneIndex(self.target) + if isinstance(self.minimumAngle, (tuple, list)) and isinstance(self.maximumAngle, (tuple, list)): + fs.writeByte(1) + fs.writeVector(self.minimumAngle) + fs.writeVector(self.maximumAngle) + else: + fs.writeByte(0) + +class Morph: + CATEGORY_SYSTEM = 0 + CATEGORY_EYEBROW = 1 + CATEGORY_EYE = 2 + CATEGORY_MOUTH = 3 + CATEGORY_OHTER = 4 + + def __init__(self, name, name_e, category, **kwargs): + self.offsets = [] + self.name = name + self.name_e = name_e + self.category = category + + def __repr__(self): + return ''%(self.name, self.name_e) + + def type_index(self): + raise NotImplementedError + + @staticmethod + def create(fs): + _CLASSES = { + 0: GroupMorph, + 1: VertexMorph, + 2: BoneMorph, + 3: UVMorph, + 4: UVMorph, + 5: UVMorph, + 6: UVMorph, + 7: UVMorph, + 8: MaterialMorph, + } + + name = fs.readStr() + name_e = fs.readStr() + logging.debug('morph: %s', name) + category = fs.readSignedByte() + typeIndex = fs.readSignedByte() + ret = _CLASSES[typeIndex](name, name_e, category, type_index = typeIndex) + ret.load(fs) + return ret + + def load(self, fs): + """ Implement for loading morph data. + """ + raise NotImplementedError + + def save(self, fs): + fs.writeStr(self.name) + fs.writeStr(self.name_e) + fs.writeSignedByte(self.category) + fs.writeSignedByte(self.type_index()) + fs.writeInt(len(self.offsets)) + for i in self.offsets: + i.save(fs) + +class VertexMorph(Morph): + def __init__(self, *args, **kwargs): + Morph.__init__(self, *args, **kwargs) + + def type_index(self): + return 1 + + def load(self, fs): + num = fs.readInt() + for i in range(num): + t = VertexMorphOffset() + t.load(fs) + self.offsets.append(t) + +class VertexMorphOffset: + def __init__(self): + self.index = 0 + self.offset = [] + + def load(self, fs): + self.index = fs.readVertexIndex() + self.offset = fs.readVector(3) + + def save(self, fs): + fs.writeVertexIndex(self.index) + fs.writeVector(self.offset) + +class UVMorph(Morph): + def __init__(self, *args, **kwargs): + self.uv_index = kwargs.get('type_index', 3) - 3 + Morph.__init__(self, *args, **kwargs) + + def type_index(self): + return self.uv_index + 3 + + def load(self, fs): + self.offsets = [] + num = fs.readInt() + for i in range(num): + t = UVMorphOffset() + t.load(fs) + self.offsets.append(t) + +class UVMorphOffset: + def __init__(self): + self.index = 0 + self.offset = [] + + def load(self, fs): + self.index = fs.readVertexIndex() + self.offset = fs.readVector(4) + + def save(self, fs): + fs.writeVertexIndex(self.index) + fs.writeVector(self.offset) + +class BoneMorph(Morph): + def __init__(self, *args, **kwargs): + Morph.__init__(self, *args, **kwargs) + + def type_index(self): + return 2 + + def load(self, fs): + self.offsets = [] + num = fs.readInt() + for i in range(num): + t = BoneMorphOffset() + t.load(fs) + self.offsets.append(t) + +class BoneMorphOffset: + def __init__(self): + self.index = None + self.location_offset = [] + self.rotation_offset = [] + + def load(self, fs): + self.index = fs.readBoneIndex() + self.location_offset = fs.readVector(3) + self.rotation_offset = fs.readVector(4) + if not any(self.rotation_offset): + self.rotation_offset = (0, 0, 0, 1) + + def save(self, fs): + fs.writeBoneIndex(self.index) + fs.writeVector(self.location_offset) + fs.writeVector(self.rotation_offset) + +class MaterialMorph(Morph): + def __init__(self, *args, **kwargs): + Morph.__init__(self, *args, **kwargs) + + def type_index(self): + return 8 + + def load(self, fs): + self.offsets = [] + num = fs.readInt() + for i in range(num): + t = MaterialMorphOffset() + t.load(fs) + self.offsets.append(t) + +class MaterialMorphOffset: + TYPE_MULT = 0 + TYPE_ADD = 1 + + def __init__(self): + self.index = 0 + self.offset_type = 0 + self.diffuse_offset = [] + self.specular_offset = [] + self.shininess_offset = 0 + self.ambient_offset = [] + self.edge_color_offset = [] + self.edge_size_offset = [] + self.texture_factor = [] + self.sphere_texture_factor = [] + self.toon_texture_factor = [] + + def load(self, fs): + self.index = fs.readMaterialIndex() + self.offset_type = fs.readSignedByte() + self.diffuse_offset = fs.readVector(4) + self.specular_offset = fs.readVector(3) + self.shininess_offset = fs.readFloat() + self.ambient_offset = fs.readVector(3) + self.edge_color_offset = fs.readVector(4) + self.edge_size_offset = fs.readFloat() + self.texture_factor = fs.readVector(4) + self.sphere_texture_factor = fs.readVector(4) + self.toon_texture_factor = fs.readVector(4) + + def save(self, fs): + fs.writeMaterialIndex(self.index) + fs.writeSignedByte(self.offset_type) + fs.writeVector(self.diffuse_offset) + fs.writeVector(self.specular_offset) + fs.writeFloat(self.shininess_offset) + fs.writeVector(self.ambient_offset) + fs.writeVector(self.edge_color_offset) + fs.writeFloat(self.edge_size_offset) + fs.writeVector(self.texture_factor) + fs.writeVector(self.sphere_texture_factor) + fs.writeVector(self.toon_texture_factor) + +class GroupMorph(Morph): + def __init__(self, *args, **kwargs): + Morph.__init__(self, *args, **kwargs) + + def type_index(self): + return 0 + + def load(self, fs): + self.offsets = [] + num = fs.readInt() + for i in range(num): + t = GroupMorphOffset() + t.load(fs) + self.offsets.append(t) + +class GroupMorphOffset: + def __init__(self): + self.morph = None + self.factor = 0.0 + + def load(self, fs): + self.morph = fs.readMorphIndex() + self.factor = fs.readFloat() + + def save(self, fs): + fs.writeMorphIndex(self.morph) + fs.writeFloat(self.factor) + + +class Display: + def __init__(self): + self.name = '' + self.name_e = '' + + self.isSpecial = False + + self.data = [] + + def __repr__(self): + return ''%( + self.name, + self.name_e, + ) + + def load(self, fs): + self.name = fs.readStr() + self.name_e = fs.readStr() + + self.isSpecial = (fs.readByte() == 1) + num = fs.readInt() + self.data = [] + for i in range(num): + disp_type = fs.readByte() + index = None + if disp_type == 0: + index = fs.readBoneIndex() + elif disp_type == 1: + index = fs.readMorphIndex() + else: + raise Exception('invalid value.') + self.data.append((disp_type, index)) + logging.debug('the number of display elements: %d', len(self.data)) + + def save(self, fs): + fs.writeStr(self.name) + fs.writeStr(self.name_e) + + fs.writeByte(int(self.isSpecial)) + fs.writeInt(len(self.data)) + + for disp_type, index in self.data: + fs.writeByte(disp_type) + if disp_type == 0: + fs.writeBoneIndex(index) + elif disp_type == 1: + fs.writeMorphIndex(index) + else: + raise Exception('invalid value.') + +class Rigid: + TYPE_SPHERE = 0 + TYPE_BOX = 1 + TYPE_CAPSULE = 2 + + MODE_STATIC = 0 + MODE_DYNAMIC = 1 + MODE_DYNAMIC_BONE = 2 + def __init__(self): + self.name = '' + self.name_e = '' + + self.bone = None + self.collision_group_number = 0 + self.collision_group_mask = 0 + + self.type = 0 + self.size = [] + + self.location = [] + self.rotation = [] + + self.mass = 1 + self.velocity_attenuation = [] + self.rotation_attenuation = [] + self.bounce = [] + self.friction = [] + + self.mode = 0 + + def __repr__(self): + return ''%( + self.name, + self.name_e, + ) + + def load(self, fs): + self.name = fs.readStr() + self.name_e = fs.readStr() + + boneIndex = fs.readBoneIndex() + if boneIndex != -1: + self.bone = boneIndex + else: + self.bone = None + + self.collision_group_number = fs.readSignedByte() + self.collision_group_mask = fs.readUnsignedShort() + + self.type = fs.readSignedByte() + self.size = fs.readVector(3) + + self.location = fs.readVector(3) + self.rotation = fs.readVector(3) + + self.mass = fs.readFloat() + self.velocity_attenuation = fs.readFloat() + self.rotation_attenuation = fs.readFloat() + self.bounce = fs.readFloat() + self.friction = fs.readFloat() + + self.mode = fs.readSignedByte() + + def save(self, fs): + fs.writeStr(self.name) + fs.writeStr(self.name_e) + + if self.bone is None: + fs.writeBoneIndex(-1) + else: + fs.writeBoneIndex(self.bone) + + fs.writeSignedByte(self.collision_group_number) + fs.writeUnsignedShort(self.collision_group_mask) + + fs.writeSignedByte(self.type) + fs.writeVector(self.size) + + fs.writeVector(self.location) + fs.writeVector(self.rotation) + + fs.writeFloat(self.mass) + fs.writeFloat(self.velocity_attenuation) + fs.writeFloat(self.rotation_attenuation) + fs.writeFloat(self.bounce) + fs.writeFloat(self.friction) + + fs.writeSignedByte(self.mode) + +class Joint: + MODE_SPRING6DOF = 0 + def __init__(self): + self.name = '' + self.name_e = '' + + self.mode = 0 + + self.src_rigid = None + self.dest_rigid = None + + self.location = [] + self.rotation = [] + + self.maximum_location = [] + self.minimum_location = [] + self.maximum_rotation = [] + self.minimum_rotation = [] + + self.spring_constant = [] + self.spring_rotation_constant = [] + + def load(self, fs): + try: self._load(fs) + except struct.error: # possibly contains truncated data + if self.src_rigid is None or self.dest_rigid is None: raise + self.location = self.location or (0, 0, 0) + self.rotation = self.rotation or (0, 0, 0) + self.maximum_location = self.maximum_location or (0, 0, 0) + self.minimum_location = self.minimum_location or (0, 0, 0) + self.maximum_rotation = self.maximum_rotation or (0, 0, 0) + self.minimum_rotation = self.minimum_rotation or (0, 0, 0) + self.spring_constant = self.spring_constant or (0, 0, 0) + self.spring_rotation_constant = self.spring_rotation_constant or (0, 0, 0) + + def _load(self, fs): + self.name = fs.readStr() + self.name_e = fs.readStr() + + self.mode = fs.readSignedByte() + + self.src_rigid = fs.readRigidIndex() + self.dest_rigid = fs.readRigidIndex() + if self.src_rigid == -1: + self.src_rigid = None + if self.dest_rigid == -1: + self.dest_rigid = None + + self.location = fs.readVector(3) + self.rotation = fs.readVector(3) + + self.minimum_location = fs.readVector(3) + self.maximum_location = fs.readVector(3) + self.minimum_rotation = fs.readVector(3) + self.maximum_rotation = fs.readVector(3) + + self.spring_constant = fs.readVector(3) + self.spring_rotation_constant = fs.readVector(3) + + def save(self, fs): + fs.writeStr(self.name) + fs.writeStr(self.name_e) + + fs.writeSignedByte(self.mode) + + if self.src_rigid is not None: + fs.writeRigidIndex(self.src_rigid) + else: + fs.writeRigidIndex(-1) + if self.dest_rigid is not None: + fs.writeRigidIndex(self.dest_rigid) + else: + fs.writeRigidIndex(-1) + + fs.writeVector(self.location) + fs.writeVector(self.rotation) + + fs.writeVector(self.minimum_location) + fs.writeVector(self.maximum_location) + fs.writeVector(self.minimum_rotation) + fs.writeVector(self.maximum_rotation) + + fs.writeVector(self.spring_constant) + fs.writeVector(self.spring_rotation_constant) + + + +def load(path): + with FileReadStream(path) as fs: + logging.info('****************************************') + logging.info(' mmd_tools.pmx module') + logging.info('----------------------------------------') + logging.info(' Start to load model data form a pmx file') + logging.info(' by the mmd_tools.pmx modlue.') + logging.info('') + header = Header() + header.load(fs) + fs.setHeader(header) + model = Model() + try: + model.load(fs) + except struct.error as e: + logging.error(' * Corrupted file: %s', e) + #raise + logging.info(' Finished loading.') + logging.info('----------------------------------------') + logging.info(' mmd_tools.pmx module') + logging.info('****************************************') + return model + +def save(path, model, add_uv_count=0): + with FileWriteStream(path) as fs: + header = Header(model) + header.additional_uvs = max(0, min(4, add_uv_count)) # UV1~UV4 + header.save(fs) + fs.setHeader(header) + model.save(fs) diff --git a/core/importers/pmx/importer.py b/core/mmd/core/pmx/importer.py similarity index 77% rename from core/importers/pmx/importer.py rename to core/mmd/core/pmx/importer.py index 3f0a1f2..d1916a8 100644 --- a/core/importers/pmx/importer.py +++ b/core/mmd/core/pmx/importer.py @@ -1,27 +1,21 @@ # -*- coding: utf-8 -*- # Copyright 2014 MMD Tools authors -# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit. -# All credit goes to the original authors. -# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed. -# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under. -# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/ +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. -import bpy import collections +import logging import os import time -import typing -from typing import TYPE_CHECKING, List, Optional, Dict, Set, Tuple, Any, Union +from typing import TYPE_CHECKING, List, Optional + +import bpy from mathutils import Matrix, Vector -from bpy.types import Context, Object - -from ...logging_setup import logger -from ...common import ProgressTracker -from ...translations import t - -from ...mmd.core import bpyutils, utils -from ...mmd.core.bpyutils import FnContext +from ... import bpyutils, utils +from ...bpyutils import FnContext from .. import pmx from ..bone import FnBone from ..material import FnMaterial @@ -32,20 +26,17 @@ from ..vmd.importer import BoneConverter from ...operators.misc import MoveObject if TYPE_CHECKING: - from ...mmd.properties.pose_bone import MMDBone - from ...mmd.properties.root import MMDRoot + from ...properties.pose_bone import MMDBone + from ...properties.root import MMDRoot class PMXImporter: - """PMX model importer for Avatar Toolkit""" - CATEGORIES = { 0: "SYSTEM", 1: "EYEBROW", 2: "EYE", 3: "MOUTH", } - MORPH_TYPES = { 0: "group_morphs", 1: "vertex_morphs", @@ -83,17 +74,15 @@ class PMXImporter: self.__materialFaceCountTable = None @staticmethod - def __safe_name(name: str, max_length: int = 59) -> str: - """Create a safe name that won't exceed Blender's name length limits""" + def __safe_name(name, max_length=59): return str(bytes(name, "utf8")[:max_length], "utf8", errors="replace") @staticmethod - def flipUV_V(uv: Tuple[float, float]) -> Tuple[float, float]: - """Flip the V coordinate of UV mapping""" + def flipUV_V(uv): u, v = uv return u, 1.0 - v - def __createObjects(self) -> None: + def __createObjects(self): """Create main objects and link them to scene.""" pmxModel = self.__model obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54) @@ -112,15 +101,13 @@ class PMXImporter: txt.from_string(pmxModel.comment_e.replace("\r", "")) mmd_root.comment_e_text = txt.name - def __createMeshObject(self) -> None: - """Create a mesh object for the model""" + def __createMeshObject(self): model_name = self.__root.name self.__meshObj = bpy.data.objects.new(name=model_name + "_mesh", object_data=bpy.data.meshes.new(name=model_name)) self.__meshObj.parent = self.__armObj FnContext.link_object(self.__targetContext, self.__meshObj) - def __createBasisShapeKey(self) -> None: - """Create a basis shape key if it doesn't exist""" + def __createBasisShapeKey(self): if self.__meshObj.data.shape_keys: assert len(self.__meshObj.data.vertices) > 0 assert len(self.__meshObj.data.shape_keys.key_blocks) > 1 @@ -128,13 +115,11 @@ class PMXImporter: FnContext.set_active_object(self.__targetContext, self.__meshObj) bpy.ops.object.shape_key_add() - def __importVertexGroup(self) -> None: - """Import vertex groups from bones""" + def __importVertexGroup(self): vgroups = self.__meshObj.vertex_groups self.__vertexGroupTable = [vgroups.new(name=i.name) for i in self.__model.bones] or [vgroups.new(name="NO BONES")] - def __importVertices(self) -> None: - """Import vertices with weights and other properties""" + def __importVertices(self): self.__importVertexGroup() pmxModel = self.__model @@ -180,13 +165,12 @@ class PMXImporter: for bone, weight in zip(pv_bones, pv_weights): vertex_group_table[bone].add(index=idx, weight=weight, type="ADD") else: - raise Exception("Unknown bone weight type.") + raise Exception("unkown bone weight type.") vg_edge_scale.lock_weight = True vg_vertex_order.lock_weight = True - def __storeVerticesSDEF(self) -> None: - """Store SDEF vertex data for smooth deformation""" + def __storeVerticesSDEF(self): if len(self.__sdefVertices) < 1: return @@ -199,28 +183,33 @@ class PMXImporter: sdefC.data[i].co = Vector(w.c).xzy * self.__scale sdefR0.data[i].co = Vector(w.r0).xzy * self.__scale sdefR1.data[i].co = Vector(w.r1).xzy * self.__scale - logger.info("Stored %d SDEF vertices", len(self.__sdefVertices)) + logging.info("Stored %d SDEF vertices", len(self.__sdefVertices)) - def __importTextures(self) -> None: - """Import textures from the PMX model""" + def __importTextures(self): pmxModel = self.__model self.__textureTable = [] for i in pmxModel.textures: self.__textureTable.append(bpy.path.resolve_ncase(path=i.path)) - def __createEditBones(self, obj: Object, pmx_bones: List[Any]) -> Tuple[List[str], List[str]]: - """Create EditBones from pmx file data. + def __createEditBones(self, obj, pmx_bones): + """create EditBones from pmx file data. @return the list of bone names which can be accessed by the bone index of pmx data. """ editBoneTable = [] nameTable = [] specialTipBones = [] dependency_cycle_ik_bones = [] + # for i, p_bone in enumerate(pmx_bones): + # if p_bone.isIK: + # if p_bone.target != -1: + # t = pmx_bones[p_bone.target] + # if p_bone.parent == t.parent: + # dependency_cycle_ik_bones.append(i) from math import isfinite - def _VectorXZY(v: List[float]) -> Vector: + def _VectorXZY(v): return Vector(v).xzy if all(isfinite(n) for n in v) else Vector((0, 0, 0)) with bpyutils.edit_object(obj) as data: @@ -250,7 +239,7 @@ class PMXImporter: for b_bone, m_bone in zip(editBoneTable, pmx_bones): if m_bone.isIK and m_bone.target != -1: - logger.debug("Checking IK links of %s", b_bone.name) + logging.debug(" - checking IK links of %s", b_bone.name) b_target = editBoneTable[m_bone.target] for i in range(len(m_bone.ik_links)): b_bone_link = editBoneTable[m_bone.ik_links[i].target] @@ -258,11 +247,11 @@ class PMXImporter: b_bone_tail = b_target if i == 0 else editBoneTable[m_bone.ik_links[i - 1].target] loc = b_bone_tail.head - b_bone_link.head if loc.length < 0.001: - logger.warning("Unsolved IK link %s", b_bone_link.name) + logging.warning(" ** unsolved IK link %s **", b_bone_link.name) elif b_bone_tail.parent != b_bone_link: - logger.warning("Skipped IK link %s", b_bone_link.name) + logging.warning(" ** skipped IK link %s **", b_bone_link.name) elif (b_bone_link.tail - b_bone_tail.head).length > 1e-4: - logger.debug("Fix IK link %s", b_bone_link.name) + logging.debug(" * fix IK link %s", b_bone_link.name) b_bone_link.tail = b_bone_link.head + loc for b_bone, m_bone in zip(editBoneTable, pmx_bones): @@ -277,7 +266,7 @@ class PMXImporter: else: b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]: - logger.debug("Special tip bone %s, display %s", b_bone.name, str(m_bone.displayConnection)) + logging.debug(" * special tip bone %s, display %s", b_bone.name, str(m_bone.displayConnection)) specialTipBones.append(b_bone.name) for b_bone, m_bone in zip(editBoneTable, pmx_bones): @@ -297,21 +286,19 @@ class PMXImporter: continue if not m_bone.isMovable: continue - logger.warning("Connected: %s (%d)-> %s", b_bone.name, len(b_bone.children), t.name) + logging.warning(" * connected: %s (%d)-> %s", b_bone.name, len(b_bone.children), t.name) t.use_connect = True return nameTable, specialTipBones - def __sortPoseBonesByBoneIndex(self, pose_bones: List[bpy.types.PoseBone], bone_names: List[str]) -> List[bpy.types.PoseBone]: - """Sort pose bones by their bone index in the PMX file""" + def __sortPoseBonesByBoneIndex(self, pose_bones: List[bpy.types.PoseBone], bone_names): r: List[bpy.types.PoseBone] = [] for i in bone_names: r.append(pose_bones[i]) return r @staticmethod - def convertIKLimitAngles(min_angle: List[float], max_angle: List[float], bone_matrix: Matrix, invert: bool = False) -> Tuple[Vector, Vector]: - """Convert IK limit angles to Blender's coordinate system""" + def convertIKLimitAngles(min_angle, max_angle, bone_matrix, invert=False): mat = bone_matrix.to_3x3() * -1 mat[1], mat[2] = mat[2].copy(), mat[1].copy() mat.transpose() @@ -338,13 +325,25 @@ class PMXImporter: new_min_angle[i], new_max_angle[i] = new_max_angle[i], new_min_angle[i] return new_min_angle, new_max_angle - def __applyIk(self, index: int, pmx_bone: Any, pose_bones: List[bpy.types.PoseBone]) -> None: - """Create an IK bone constraint + def __applyIk(self, index, pmx_bone, pose_bones): + """create a IK bone constraint If the IK bone and the target bone is separated, a dummy IK target bone is created as a child of the IK bone. @param index the bone index @param pmx_bone pmx.Bone @param pose_bones the list of PoseBones sorted by the bone index """ + + # for tracking mmd ik target, simple explaination: + # + Root + # | + link1 + # | + link0 (ik_constraint_bone) <- ik constraint, chain_count=2 + # | + IK target (ik_target) <- constraint 'mmd_ik_target_override', subtarget=link0 + # + IK bone (ik_bone) + # + # it is possible that the link0 is the IK target, + # so ik constraint will be on link1, chain_count=1 + # the IK target isn't affected by IK bone + ik_bone = pose_bones[index] ik_target = pose_bones[pmx_bone.target] ik_constraint_bone = ik_target.parent @@ -355,17 +354,16 @@ class PMXImporter: if len(pmx_bone.ik_links) > 1: ik_constraint_bone_real = pose_bones[pmx_bone.ik_links[1].target] del pmx_bone.ik_links[0] - logger.warning("Fix IK settings of IK bone (%s)", ik_bone.name) + logging.warning(" * fix IK settings of IK bone (%s)", ik_bone.name) is_valid_ik = ik_constraint_bone == ik_constraint_bone_real if not is_valid_ik: ik_constraint_bone = ik_constraint_bone_real - logger.warning("IK bone (%s) warning: IK target (%s) is not a child of IK link 0 (%s)", - ik_bone.name, ik_target.name, ik_constraint_bone.name) + logging.warning(" * IK bone (%s) warning: IK target (%s) is not a child of IK link 0 (%s)", ik_bone.name, ik_target.name, ik_constraint_bone.name) elif any(pose_bones[i.target].parent != pose_bones[j.target] for i, j in zip(pmx_bone.ik_links, pmx_bone.ik_links[1:])): - logger.warning("Invalid IK bone (%s): IK chain does not follow parent-child relationship", ik_bone.name) + logging.warning(" * Invalid IK bone (%s): IK chain does not follow parent-child relationship", ik_bone.name) return if ik_constraint_bone is None or len(pmx_bone.ik_links) < 1: - logger.warning("Invalid IK bone (%s)", ik_bone.name) + logging.warning(" * Invalid IK bone (%s)", ik_bone.name) return c = ik_target.constraints.new(type="DAMPED_TRACK") @@ -421,8 +419,7 @@ class PMXImporter: c.use_limit_y = bone.ik_max_y != c.max_y or bone.ik_min_y != c.min_y c.use_limit_z = bone.ik_max_z != c.max_z or bone.ik_min_z != c.min_z - def __importBones(self) -> None: - """Import bones from the PMX model""" + def __importBones(self): pmxModel = self.__model boneNameTable, specialTipBones = self.__createEditBones(self.__armObj, pmxModel.bones) @@ -476,8 +473,7 @@ class PMXImporter: b_bone.lock_location = [True, True, True] b_bone.lock_scale = [True, True, True] - def __importRigids(self) -> None: - """Import rigid bodies from the PMX model""" + def __importRigids(self): start_time = time.time() self.__rigidTable = {} context = FnContext.ensure_context() @@ -509,10 +505,9 @@ class PMXImporter: MoveObject.set_index(obj, i) self.__rigidTable[i] = obj - logger.debug("Finished importing rigid bodies in %.2f seconds", time.time() - start_time) + logging.debug("Finished importing rigid bodies in %f seconds.", time.time() - start_time) - def __importJoints(self) -> None: - """Import joints from the PMX model""" + def __importJoints(self): start_time = time.time() context = FnContext.ensure_context() joint_pool = FnRigidBody.new_joint_objects(context, FnModel.ensure_joint_group_object(context, self.__rig.rootObject()), len(self.__model.joints), FnModel.get_empty_display_size(self.__rig.rootObject())) @@ -538,10 +533,9 @@ class PMXImporter: obj.hide_set(True) MoveObject.set_index(obj, i) - logger.debug("Finished importing joints in %.2f seconds", time.time() - start_time) + logging.debug("Finished importing joints in %f seconds.", time.time() - start_time) - def __importMaterials(self) -> None: - """Import materials from the PMX model""" + def __importMaterials(self): self.__importTextures() pmxModel = self.__model @@ -594,8 +588,7 @@ class PMXImporter: texture_slot.uv_layer = "UV1" # for SubTexture mmd_mat.sphere_texture_type = str(i.sphere_texture_mode) - def __importFaces(self) -> None: - """Import faces/polygons from the PMX model""" + def __importFaces(self): pmxModel = self.__model mesh = self.__meshObj.data vertex_map = self.__vertex_map @@ -624,42 +617,38 @@ class PMXImporter: bf.image = self.__imageTable.get(mi, None) if pmxModel.header and pmxModel.header.additional_uvs: - logger.info("Importing %d additional uvs", pmxModel.header.additional_uvs) + logging.info("Importing %d additional uvs", pmxModel.header.additional_uvs) zw_data_map = collections.OrderedDict() split_uvzw = lambda uvi: (self.flipUV_V(uvi[:2]), uvi[2:]) for i in range(pmxModel.header.additional_uvs): add_uv = uv_layers[uv_textures.new(name="UV" + str(i + 1)).name] - logger.info(" - %s...(uv channels)", add_uv.name) + logging.info(" - %s...(uv channels)", add_uv.name) uv_table = {vi: split_uvzw(v.additional_uvs[i]) for vi, v in enumerate(pmxModel.vertices)} add_uv.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in uv_table[i][0])) if not any(any(s[1]) for s in uv_table.values()): - logger.info("\t- zw are all zeros: %s", add_uv.name) + logging.info("\t- zw are all zeros: %s", add_uv.name) else: zw_data_map["_" + add_uv.name] = {k: self.flipUV_V(v[1]) for k, v in uv_table.items()} for name, zw_table in zw_data_map.items(): - logger.info(" - %s...(zw channels of %s)", name, name[1:]) + logging.info(" - %s...(zw channels of %s)", name, name[1:]) add_zw = uv_textures.new(name=name) if add_zw is None: - logger.warning("\t* Lost zw channels") + logging.warning("\t* Lost zw channels") continue add_zw = uv_layers[add_zw.name] add_zw.data.foreach_set("uv", tuple(v for i in loop_indices_orig for v in zw_table[i])) self.__fixOverlappingFaceMaterials(mesh.materials, mesh.vertices, loop_indices, material_indices) - def __fixOverlappingFaceMaterials(self, materials: List[bpy.types.Material], - vertices: List[bpy.types.MeshVertex], - loop_indices: List[int], - material_indices: List[int]) -> None: - """Fix overlapping face materials to prevent z-fighting""" - # FIXME: This is not the best way to setup blend_method, might just work for some common cases. + def __fixOverlappingFaceMaterials(self, materials, vertices, loop_indices, material_indices): + # FIXME: This is not the best way to setup blend_method, might just work for some common cases. And FnMaterial.update_alpha() is still using 'HASHED'. # For EEVEE, basically users should know which blend_method is best for each material of their models. # For Cycles, users have to offset or delete those z-fighting faces to fix it manually. check = {} mi_skip = -1 _vi_cache = {} - def _rounded_co_vi(vi: int) -> Tuple[float, float, float]: + def _rounded_co_vi(vi): if vi not in _vi_cache: vco = vertices[vi].co _vi_cache[vi] = (round(vco[0], 6), round(vco[1], 6), round(vco[2], 6)) @@ -674,13 +663,12 @@ class PMXImporter: if verts not in check: check[verts] = mi elif check[verts] < mi: - logger.debug("Fix blend method of material: %s", materials[mi].name) + logging.debug(" >> fix blend method of material: %s", materials[mi].name) materials[mi].blend_method = "BLEND" materials[mi].show_transparent_back = False mi_skip = mi - def __importVertexMorphs(self) -> None: - """Import vertex morphs from the PMX model""" + def __importVertexMorphs(self): mmd_root = self.__root.mmd_root categories = self.CATEGORIES self.__createBasisShapeKey() @@ -694,8 +682,7 @@ class PMXImporter: shapeKeyPoint = shapeKey.data[md.index] shapeKeyPoint.co += Vector(md.offset).xzy * self.__scale - def __importMaterialMorphs(self) -> None: - """Import material morphs from the PMX model""" + def __importMaterialMorphs(self): mmd_root = self.__root.mmd_root categories = self.CATEGORIES for morph in (x for x in self.__model.morphs if isinstance(x, pmx.MaterialMorph)): @@ -719,8 +706,7 @@ class PMXImporter: data.sphere_texture_factor = morph_data.sphere_texture_factor data.toon_texture_factor = morph_data.toon_texture_factor - def __importBoneMorphs(self) -> None: - """Import bone morphs from the PMX model""" + def __importBoneMorphs(self): mmd_root = self.__root.mmd_root categories = self.CATEGORIES for morph in (x for x in self.__model.morphs if isinstance(x, pmx.BoneMorph)): @@ -738,8 +724,7 @@ class PMXImporter: data.location = converter.convert_location(morph_data.location_offset) data.rotation = converter.convert_rotation(morph_data.rotation_offset) - def __importUVMorphs(self) -> None: - """Import UV morphs from the PMX model""" + def __importUVMorphs(self): mmd_root = self.__root.mmd_root categories = self.CATEGORIES __OffsetData = collections.namedtuple("OffsetData", "index, offset") @@ -755,8 +740,7 @@ class PMXImporter: FnMorph.store_uv_morph_data(self.__meshObj, uv_morph, offsets, "") uv_morph.data_type = "VERTEX_GROUP" - def __importGroupMorphs(self) -> None: - """Import group morphs from the PMX model""" + def __importGroupMorphs(self): mmd_root = self.__root.mmd_root categories = self.CATEGORIES morph_types = self.MORPH_TYPES @@ -775,8 +759,7 @@ class PMXImporter: data.morph_type = morph_types[m.type_index()] data.factor = morph_data.factor - def __importDisplayFrames(self) -> None: - """Import display frames from the PMX model""" + def __importDisplayFrames(self): pmxModel = self.__model root = self.__root morph_types = self.MORPH_TYPES @@ -801,18 +784,17 @@ class PMXImporter: FnBone.sync_bone_collections_from_display_item_frames(self.__armObj) - def __addArmatureModifier(self, meshObj: Object, armObj: Object) -> None: - """Add armature modifier to mesh object""" + def __addArmatureModifier(self, meshObj, armObj): + # TODO: move to model.py armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE") armModifier.object = armObj armModifier.use_vertex_groups = True armModifier.name = "mmd_bone_order_override" armModifier.show_render = armModifier.show_viewport = len(meshObj.data.vertices) > 0 - def __assignCustomNormals(self) -> None: - """Assign custom normals to the mesh""" + def __assignCustomNormals(self): mesh: bpy.types.Mesh = self.__meshObj.data - logger.info("Setting custom normals...") + logging.info("Setting custom normals...") if self.__vertex_map: verts, faces = self.__model.vertices, self.__model.faces custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f] @@ -820,29 +802,26 @@ class PMXImporter: else: custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices] mesh.normals_split_custom_set_from_vertices(custom_normals) - logger.info("Custom normals applied successfully") + logging.info(" - Done!!") - def __renameLRBones(self, use_underscore: bool) -> None: - """Rename left/right bones with proper naming convention""" + def __renameLRBones(self, use_underscore): pose_bones = self.__armObj.pose.bones for i in pose_bones: self.__rig.renameBone(i.name, utils.convertNameToLR(i.name, use_underscore)) + # self.__meshObj.vertex_groups[i.mmd_bone.name_j].name = i.name - def __translateBoneNames(self) -> None: - """Translate bone names using the provided translator""" + def __translateBoneNames(self): pose_bones = self.__armObj.pose.bones for i in pose_bones: self.__rig.renameBone(i.name, self.__translator.translate(i.name)) - def __fixRepeatedMorphName(self) -> None: - """Fix repeated morph names to ensure uniqueness""" + def __fixRepeatedMorphName(self): used_names = set() for m in self.__model.morphs: m.name = utils.unique_name(m.name or "Morph", used_names) used_names.add(m.name) - def execute(self, context: Context, **args) -> None: - """Execute the PMX import process with the given arguments""" + def execute(self, **args): if "pmx" in args: self.__model = args["pmx"] else: @@ -860,95 +839,78 @@ class PMXImporter: self.__apply_bone_fixed_axis = args.get("apply_bone_fixed_axis", False) self.__translator = args.get("translator", None) - logger.info("****************************************") - logger.info("Starting PMX import process") - logger.info("----------------------------------------") + logging.info("****************************************") + logging.info(" mmd_tools.import_pmx module") + logging.info("----------------------------------------") + logging.info(" Start to load model data form a pmx file") + logging.info(" by the mmd_tools.pmx modlue.") + logging.info("") start_time = time.time() - with ProgressTracker(context, 100, "Importing PMX Model") as progress: - self.__createObjects() - progress.step("Created base objects") + self.__createObjects() - if "MESH" in types: - if clean_model: - _PMXCleaner.clean(self.__model, "MORPHS" not in types) - if remove_doubles: - self.__vertex_map = _PMXCleaner.remove_doubles(self.__model, "MORPHS" not in types) - - progress.step("Preparing mesh data") + if "MESH" in types: + if clean_model: + _PMXCleaner.clean(self.__model, "MORPHS" not in types) + if remove_doubles: + self.__vertex_map = _PMXCleaner.remove_doubles(self.__model, "MORPHS" not in types) + self.__createMeshObject() + self.__importVertices() + self.__importMaterials() + self.__importFaces() + self.__meshObj.data.update() + self.__assignCustomNormals() + self.__storeVerticesSDEF() + + if "ARMATURE" in types: + # for tracking bone order + if "MESH" not in types: self.__createMeshObject() - progress.step("Importing vertices") - self.__importVertices() - progress.step("Importing materials") - self.__importMaterials() - progress.step("Importing faces") - self.__importFaces() - self.__meshObj.data.update() - progress.step("Assigning custom normals") - self.__assignCustomNormals() - progress.step("Processing SDEF vertices") - self.__storeVerticesSDEF() + self.__importVertexGroup() + self.__importBones() + if args.get("rename_LR_bones", False): + use_underscore = args.get("use_underscore", False) + self.__renameLRBones(use_underscore) + if self.__translator: + self.__translateBoneNames() + if self.__apply_bone_fixed_axis: + FnBone.apply_bone_fixed_axis(self.__armObj) + FnBone.apply_additional_transformation(self.__armObj) - if "ARMATURE" in types: - progress.step("Preparing armature") - # for tracking bone order - if "MESH" not in types: - self.__createMeshObject() - self.__importVertexGroup() - progress.step("Importing bones") - self.__importBones() - if args.get("rename_LR_bones", False): - use_underscore = args.get("use_underscore", False) - self.__renameLRBones(use_underscore) - if self.__translator: - self.__translateBoneNames() - if self.__apply_bone_fixed_axis: - FnBone.apply_bone_fixed_axis(self.__armObj) - FnBone.apply_additional_transformation(self.__armObj) + if "PHYSICS" in types: + self.__importRigids() + self.__importJoints() - if "PHYSICS" in types: - progress.step("Importing rigid bodies") - self.__importRigids() - progress.step("Importing joints") - self.__importJoints() + if "DISPLAY" in types: + self.__importDisplayFrames() + else: + self.__rig.initialDisplayFrames() - if "DISPLAY" in types: - progress.step("Importing display frames") - self.__importDisplayFrames() - else: - self.__rig.initialDisplayFrames() + if "MORPHS" in types: + self.__importGroupMorphs() + self.__importVertexMorphs() + self.__importBoneMorphs() + self.__importMaterialMorphs() + self.__importUVMorphs() - if "MORPHS" in types: - progress.step("Importing group morphs") - self.__importGroupMorphs() - progress.step("Importing vertex morphs") - self.__importVertexMorphs() - progress.step("Importing bone morphs") - self.__importBoneMorphs() - progress.step("Importing material morphs") - self.__importMaterialMorphs() - progress.step("Importing UV morphs") - self.__importUVMorphs() + if self.__meshObj: + self.__addArmatureModifier(self.__meshObj, self.__armObj) - if self.__meshObj: - progress.step("Adding armature modifier") - self.__addArmatureModifier(self.__meshObj, self.__armObj) + FnModel.change_mmd_ik_loop_factor(self.__root, args.get("ik_loop_factor", 1)) + # bpy.context.scene.gravity[2] = -9.81 * 10 * self.__scale + utils.selectAObject(self.__root) - FnModel.change_mmd_ik_loop_factor(self.__root, args.get("ik_loop_factor", 1)) - utils.selectAObject(self.__root) - - logger.info("Finished importing the model in %.2f seconds", time.time() - start_time) - logger.info("----------------------------------------") + logging.info(" Finished importing the model in %f seconds.", time.time() - start_time) + logging.info("----------------------------------------") + logging.info(" mmd_tools.import_pmx module") + logging.info("****************************************") class _PMXCleaner: - """Helper class for cleaning PMX data during import""" - @classmethod - def clean(cls, pmx_model: Any, mesh_only: bool) -> None: - """Clean PMX data by removing unused vertices and faces""" - logger.info("Cleaning PMX data...") + def clean(cls, pmx_model, mesh_only): + logging.info("Cleaning PMX data...") pmx_faces = pmx_model.faces pmx_vertices = pmx_model.vertices @@ -958,7 +920,7 @@ class _PMXCleaner: index_map = {v: v for f in pmx_faces for v in f} is_index_clean = len(index_map) == len(pmx_vertices) if is_index_clean: - logger.info("Vertices are clean, no cleaning needed") + logging.info(" (vertices is clean)") else: new_vertex_count = 0 for v in sorted(index_map): @@ -966,7 +928,7 @@ class _PMXCleaner: pmx_vertices[new_vertex_count] = pmx_vertices[v] index_map[v] = new_vertex_count new_vertex_count += 1 - logger.warning("Removed %d unused vertices", len(pmx_vertices) - new_vertex_count) + logging.warning(" - removed %d vertices", len(pmx_vertices) - new_vertex_count) del pmx_vertices[new_vertex_count:] # update vertex indices of faces @@ -974,7 +936,7 @@ class _PMXCleaner: f[:] = [index_map[v] for v in f] if mesh_only: - logger.info("Mesh-only cleaning completed") + logging.info(" - Done (mesh only)!!") return if not is_index_clean: @@ -984,12 +946,11 @@ class _PMXCleaner: return x.index is not None cls.__clean_pmx_morphs(pmx_model.morphs, __update_index) - logger.info("PMX cleaning completed") + logging.info(" - Done!!") @classmethod - def remove_doubles(cls, pmx_model: Any, mesh_only: bool) -> Optional[Dict[int, Tuple[int, int]]]: - """Remove duplicate vertices from the PMX model""" - logger.info("Removing duplicate vertices...") + def remove_doubles(cls, pmx_model, mesh_only): + logging.info("Removing doubles...") pmx_vertices = pmx_model.vertices vertex_map = [None] * len(pmx_vertices) @@ -1013,17 +974,18 @@ class _PMXCleaner: counts = len(vertex_map) - len(keys) keys.clear() if counts: - logger.warning("%d duplicate vertices will be removed", counts) + logging.warning(" - %d vertices will be removed", counts) else: - logger.info("No duplicate vertices found") + logging.info(" - Done (no changes)!!") return None # clean face + # face_key_func = lambda f: frozenset(vertex_map[x][0] for x in f) face_key_func = lambda f: frozenset({vertex_map[x][0]: tuple(pmx_vertices[x].uv) for x in f}.items()) cls.__clean_pmx_faces(pmx_model.faces, pmx_model.materials, face_key_func) if mesh_only: - logger.info("Mesh-only duplicate removal completed") + logging.info(" - Done (mesh only)!!") else: # clean vertex/uv morphs def __update_index(x): @@ -1032,12 +994,11 @@ class _PMXCleaner: return x.index is not None cls.__clean_pmx_morphs(pmx_model.morphs, __update_index) - logger.info("Duplicate removal completed") + logging.info(" - Done!!") return vertex_map @staticmethod - def __clean_pmx_faces(pmx_faces: List[List[int]], pmx_materials: List[Any], face_key_func: Callable) -> None: - """Clean PMX faces by removing duplicates and updating material vertex counts""" + def __clean_pmx_faces(pmx_faces, pmx_materials, face_key_func): new_face_count = 0 face_iter = iter(pmx_faces) for mat in pmx_materials: @@ -1057,14 +1018,13 @@ class _PMXCleaner: mat.vertex_count = new_vertex_count face_iter = None if new_face_count == len(pmx_faces): - logger.info("Faces are clean, no cleaning needed") + logging.info(" (faces is clean)") else: - logger.warning("Removed %d duplicate faces", len(pmx_faces) - new_face_count) + logging.warning(" - removed %d faces", len(pmx_faces) - new_face_count) del pmx_faces[new_face_count:] @staticmethod - def __clean_pmx_morphs(pmx_morphs: List[Any], index_update_func: Callable) -> None: - """Clean PMX morphs by updating vertex indices and removing invalid offsets""" + def __clean_pmx_morphs(pmx_morphs, index_update_func): for m in pmx_morphs: if not isinstance(m, pmx.VertexMorph) and not isinstance(m, pmx.UVMorph): continue @@ -1072,4 +1032,4 @@ class _PMXCleaner: m.offsets = [x for x in m.offsets if index_update_func(x)] counts = old_len - len(m.offsets) if counts: - logger.warning('Removed %d (of %d) offsets from morph "%s"', counts, old_len, m.name) + logging.warning(' - removed %d (of %d) offsets of "%s"', counts, old_len, m.name) diff --git a/core/mmd/core/rigid_body.py b/core/mmd/core/rigid_body.py new file mode 100644 index 0000000..ec3aeb8 --- /dev/null +++ b/core/mmd/core/rigid_body.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +from typing import List, Optional + +import bpy +from mathutils import Euler, Vector + +from ..bpyutils import FnContext, Props + +SHAPE_SPHERE = 0 +SHAPE_BOX = 1 +SHAPE_CAPSULE = 2 + +MODE_STATIC = 0 +MODE_DYNAMIC = 1 +MODE_DYNAMIC_BONE = 2 + + +def shapeType(collision_shape): + return ("SPHERE", "BOX", "CAPSULE").index(collision_shape) + + +def collisionShape(shape_type): + return ("SPHERE", "BOX", "CAPSULE")[shape_type] + + +def setRigidBodyWorldEnabled(enable): + if bpy.ops.rigidbody.world_add.poll(): + bpy.ops.rigidbody.world_add() + rigidbody_world = bpy.context.scene.rigidbody_world + enabled = rigidbody_world.enabled + rigidbody_world.enabled = enable + return enabled + + +class RigidBodyMaterial: + COLORS = [ + 0x7FDDD4, + 0xF0E68C, + 0xEE82EE, + 0xFFE4E1, + 0x8FEEEE, + 0xADFF2F, + 0xFA8072, + 0x9370DB, + 0x40E0D0, + 0x96514D, + 0x5A964E, + 0xE6BFAB, + 0xD3381C, + 0x165E83, + 0x701682, + 0x828216, + ] + + @classmethod + def getMaterial(cls, number): + number = int(number) + material_name = "mmd_tools_rigid_%d" % (number) + if material_name not in bpy.data.materials: + mat = bpy.data.materials.new(material_name) + color = cls.COLORS[number] + mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)] + mat.specular_intensity = 0 + if len(mat.diffuse_color) > 3: + mat.diffuse_color[3] = 0.5 + mat.blend_method = "BLEND" + if hasattr(mat, "shadow_method"): + mat.shadow_method = "NONE" + mat.use_backface_culling = True + mat.show_transparent_back = False + mat.use_nodes = True + nodes, links = mat.node_tree.nodes, mat.node_tree.links + nodes.clear() + node_color = nodes.new("ShaderNodeBackground") + node_color.inputs["Color"].default_value = mat.diffuse_color + node_output = nodes.new("ShaderNodeOutputMaterial") + links.new(node_color.outputs[0], node_output.inputs["Surface"]) + else: + mat = bpy.data.materials[material_name] + return mat + + +class FnRigidBody: + @staticmethod + def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]: + if count < 1: + return [] + + obj = FnRigidBody.new_rigid_body_object(context, parent_object) + + if count == 1: + return [obj] + + return FnContext.duplicate_object(context, obj, count) + + @staticmethod + def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object: + obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody")) + obj.parent = parent_object + obj.mmd_type = "RIGID_BODY" + obj.rotation_mode = "YXZ" + setattr(obj, Props.display_type, "SOLID") + obj.show_transparent = True + obj.hide_render = True + obj.display.show_shadows = False + + with context.temp_override(object=obj): + bpy.ops.rigidbody.object_add(type="ACTIVE") + + return obj + + @staticmethod + def setup_rigid_body_object( + obj: bpy.types.Object, + shape_type: str, + location: Vector, + rotation: Euler, + size: Vector, + dynamics_type: str, + collision_group_number: Optional[int] = None, + collision_group_mask: Optional[List[bool]] = None, + name: Optional[str] = None, + name_e: Optional[str] = None, + bone: Optional[str] = None, + friction: Optional[float] = None, + mass: Optional[float] = None, + angular_damping: Optional[float] = None, + linear_damping: Optional[float] = None, + bounce: Optional[float] = None, + ) -> bpy.types.Object: + obj.location = location + obj.rotation_euler = rotation + + obj.mmd_rigid.shape = collisionShape(shape_type) + obj.mmd_rigid.size = size + obj.mmd_rigid.type = str(dynamics_type) if dynamics_type in range(3) else "1" + + if collision_group_number is not None: + obj.mmd_rigid.collision_group_number = collision_group_number + + if collision_group_mask is not None: + obj.mmd_rigid.collision_group_mask = collision_group_mask + + if name is not None: + obj.name = name + obj.mmd_rigid.name_j = name + obj.data.name = name + + if name_e is not None: + obj.mmd_rigid.name_e = name_e + + if bone is not None: + obj.mmd_rigid.bone = bone + else: + obj.mmd_rigid.bone = "" + + rb = obj.rigid_body + if friction is not None: + rb.friction = friction + if mass is not None: + rb.mass = mass + if angular_damping is not None: + rb.angular_damping = angular_damping + if linear_damping is not None: + rb.linear_damping = linear_damping + if bounce is not None: + rb.restitution = bounce + + return obj + + @staticmethod + def get_rigid_body_size(obj: bpy.types.Object): + assert obj.mmd_type == "RIGID_BODY" + + x0, y0, z0 = obj.bound_box[0] + x1, y1, z1 = obj.bound_box[6] + assert x1 >= x0 and y1 >= y0 and z1 >= z0 + + shape = obj.mmd_rigid.shape + if shape == "SPHERE": + radius = (z1 - z0) / 2 + return (radius, 0.0, 0.0) + elif shape == "BOX": + x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2 + return (x, y, z) + elif shape == "CAPSULE": + diameter = x1 - x0 + radius = diameter / 2 + height = abs((z1 - z0) - diameter) + return (radius, height, 0.0) + else: + raise ValueError(f"Invalid shape type: {shape}") + + @staticmethod + def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object: + obj = FnContext.new_and_link_object(context, name="Joint", object_data=None) + obj.parent = parent_object + obj.mmd_type = "JOINT" + obj.rotation_mode = "YXZ" + setattr(obj, Props.empty_display_type, "ARROWS") + setattr(obj, Props.empty_display_size, 0.1 * empty_display_size) + obj.hide_render = True + + with context.temp_override(): + context.view_layer.objects.active = obj + bpy.ops.rigidbody.constraint_add(type="GENERIC_SPRING") + + rigid_body_constraint = obj.rigid_body_constraint + rigid_body_constraint.disable_collisions = False + rigid_body_constraint.use_limit_ang_x = True + rigid_body_constraint.use_limit_ang_y = True + rigid_body_constraint.use_limit_ang_z = True + rigid_body_constraint.use_limit_lin_x = True + rigid_body_constraint.use_limit_lin_y = True + rigid_body_constraint.use_limit_lin_z = True + rigid_body_constraint.use_spring_x = True + rigid_body_constraint.use_spring_y = True + rigid_body_constraint.use_spring_z = True + rigid_body_constraint.use_spring_ang_x = True + rigid_body_constraint.use_spring_ang_y = True + rigid_body_constraint.use_spring_ang_z = True + + return obj + + @staticmethod + def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]: + if count < 1: + return [] + + obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size) + + if count == 1: + return [obj] + + return FnContext.duplicate_object(context, obj, count) + + @staticmethod + def setup_joint_object( + obj: bpy.types.Object, + location: Vector, + rotation: Euler, + rigid_a: bpy.types.Object, + rigid_b: bpy.types.Object, + maximum_location: Vector, + minimum_location: Vector, + maximum_rotation: Euler, + minimum_rotation: Euler, + spring_angular: Vector, + spring_linear: Vector, + name: str, + name_e: Optional[str] = None, + ) -> bpy.types.Object: + obj.name = f"J.{name}" + + obj.location = location + obj.rotation_euler = rotation + + rigid_body_constraint = obj.rigid_body_constraint + rigid_body_constraint.object1 = rigid_a + rigid_body_constraint.object2 = rigid_b + rigid_body_constraint.limit_lin_x_upper = maximum_location.x + rigid_body_constraint.limit_lin_y_upper = maximum_location.y + rigid_body_constraint.limit_lin_z_upper = maximum_location.z + + rigid_body_constraint.limit_lin_x_lower = minimum_location.x + rigid_body_constraint.limit_lin_y_lower = minimum_location.y + rigid_body_constraint.limit_lin_z_lower = minimum_location.z + + rigid_body_constraint.limit_ang_x_upper = maximum_rotation.x + rigid_body_constraint.limit_ang_y_upper = maximum_rotation.y + rigid_body_constraint.limit_ang_z_upper = maximum_rotation.z + + rigid_body_constraint.limit_ang_x_lower = minimum_rotation.x + rigid_body_constraint.limit_ang_y_lower = minimum_rotation.y + rigid_body_constraint.limit_ang_z_lower = minimum_rotation.z + + obj.mmd_joint.name_j = name + if name_e is not None: + obj.mmd_joint.name_e = name_e + + obj.mmd_joint.spring_linear = spring_linear + obj.mmd_joint.spring_angular = spring_angular + + return obj diff --git a/core/mmd/core/sdef.py b/core/mmd/core/sdef.py new file mode 100644 index 0000000..4e4f768 --- /dev/null +++ b/core/mmd/core/sdef.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import logging +import time + +import bpy +from mathutils import Matrix, Vector + +from ..bpyutils import FnObject + + +def _hash(v): + if isinstance(v, (bpy.types.Object, bpy.types.PoseBone)): + return hash(type(v).__name__ + v.name) + elif isinstance(v, bpy.types.Pose): + return hash(type(v).__name__ + v.id_data.name) + else: + raise NotImplementedError("hash") + + +class FnSDEF: + g_verts = {} # global cache + g_shapekey_data = {} + g_bone_check = {} + __g_armature_check = {} + SHAPEKEY_NAME = "mmd_sdef_skinning" + MASK_NAME = "mmd_sdef_mask" + + def __init__(self): + raise NotImplementedError("not allowed") + + @classmethod + def __init_cache(cls, obj, shapekey): + key = _hash(obj) + obj = getattr(obj, "original", obj) + mod = obj.modifiers.get("mmd_bone_order_override") + key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None + if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature: + cls.g_verts[key] = cls.__find_vertices(obj) + cls.g_bone_check[key] = {} + cls.__g_armature_check[key] = key_armature + cls.g_shapekey_data[key] = None + return True + return False + + @classmethod + def __check_bone_update(cls, obj, bone0, bone1): + check = cls.g_bone_check[_hash(obj)] + key = (_hash(bone0), _hash(bone1)) + if key not in check or (bone0.matrix, bone1.matrix) != check[key]: + check[key] = (bone0.matrix.copy(), bone1.matrix.copy()) + return True + return False + + @classmethod + def mute_sdef_set(cls, obj, mute): + key_blocks = getattr(obj.data.shape_keys, "key_blocks", ()) + if cls.SHAPEKEY_NAME in key_blocks: + shapekey = key_blocks[cls.SHAPEKEY_NAME] + shapekey.mute = mute + if cls.has_sdef_data(obj): + cls.__init_cache(obj, shapekey) + cls.__sdef_muted(obj, shapekey) + + @classmethod + def __sdef_muted(cls, obj, shapekey): + mute = shapekey.mute + if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"): + mod = obj.modifiers.get("mmd_bone_order_override") + if mod and mod.type == "ARMATURE": + if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != "EDIT": + mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3]) + obj.vertex_groups.new(name=cls.MASK_NAME).add(mask, 1, "REPLACE") + mod.vertex_group = "" if mute else cls.MASK_NAME + mod.invert_vertex_group = True + shapekey.vertex_group = cls.MASK_NAME + cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute + return mute + + @staticmethod + def has_sdef_data(obj): + mod = obj.modifiers.get("mmd_bone_order_override") + if mod and mod.type == "ARMATURE" and mod.object: + kb = getattr(obj.data.shape_keys, "key_blocks", None) + return kb and "mmd_sdef_c" in kb and "mmd_sdef_r0" in kb and "mmd_sdef_r1" in kb + return False + + @classmethod + def __find_vertices(cls, obj): + if not cls.has_sdef_data(obj): + return {} + + vertices = {} + pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones + bone_map = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones} + sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data + sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data + sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data + vd = obj.data.vertices + + for i in range(len(sdef_c)): + if vd[i].co != sdef_c[i].co: + bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups + if len(bgs) >= 2: + bgs.sort(key=lambda x: x.group) + # preprocessing + w0, w1 = bgs[0].weight, bgs[1].weight + # w0 + w1 == 1 + w0 = w0 / (w0 + w1) + w1 = 1 - w0 + + c, r0, r1 = sdef_c[i].co, sdef_r0[i].co, sdef_r1[i].co + rw = r0 * w0 + r1 * w1 + r0 = c + r0 - rw + r1 = c + r1 - rw + + key = (bgs[0].group, bgs[1].group) + if key not in vertices: + # TODO basically we can not cache any bone reference + vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], []) + vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2)) + vertices[key][3].append(i) + return vertices + + @classmethod + def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale): + obj = bpy.data.objects[obj_name] + shapekey = obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME] + return cls.driver_function(shapekey, obj_name, bulk_update, use_skip, use_scale) + + @classmethod + def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): + obj = bpy.data.objects[obj_name] + if getattr(shapekey.id_data, "is_evaluated", False): + # For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver + # cls.driver_function(shapekey.id_data.original.key_blocks[shapekey.name], obj_name, bulk_update, use_skip, use_scale) # update original data + data_path = shapekey.path_from_id("value") + obj = next(i for i in shapekey.id_data.animation_data.drivers if i.data_path == data_path).driver.variables["obj"].targets[0].id + cls.__init_cache(obj, shapekey) + if cls.__sdef_muted(obj, shapekey): + return 0.0 + + pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones + if not bulk_update: + shapekey_data = shapekey.data + if use_scale: + # with scale + key_blocks = tuple(k for k in shapekey.id_data.key_blocks[1:] if not k.mute and k.value and k.name != cls.SHAPEKEY_NAME) + for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): + bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] + # if use_skip and not cls.__check_bone_update(obj, bone0, bone1): + # continue + mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted() + mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted() + rot0 = mat0.to_euler("YXZ").to_quaternion() + rot1 = mat1.to_euler("YXZ").to_quaternion() + if rot1.dot(rot0) < 0: + rot1 = -rot1 + s0, s1 = mat0.to_scale(), mat1.to_scale() + for vid, w0, w1, pos_c, cr0, cr1 in sdef_data: + s = s0 * w0 + s1 * w1 + mat_rot = (rot0 * w0 + rot1 * w1).normalized().to_matrix() @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])]) + delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = '' + shapekey_data[vid].co = (mat_rot @ (pos_c + delta)) - delta + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 + else: + # default + for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): + bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] + if use_skip and not cls.__check_bone_update(obj, bone0, bone1): + continue + mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted() + mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted() + # workaround some weird result of matrix.to_quaternion() using to_euler(), but still minor issues + rot0 = mat0.to_euler("YXZ").to_quaternion() + rot1 = mat1.to_euler("YXZ").to_quaternion() + if rot1.dot(rot0) < 0: + rot1 = -rot1 + for vid, w0, w1, pos_c, cr0, cr1 in sdef_data: + mat_rot = (rot0 * w0 + rot1 * w1).normalized().to_matrix() + shapekey_data[vid].co = (mat_rot @ pos_c) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 + else: # bulk update + shapekey_data = cls.g_shapekey_data[_hash(obj)] + if shapekey_data is None: + import numpy as np + + shapekey_data = np.zeros(len(shapekey.data) * 3, dtype=np.float32) + shapekey.data.foreach_get("co", shapekey_data) + shapekey_data = cls.g_shapekey_data[_hash(obj)] = shapekey_data.reshape(len(shapekey.data), 3) + if use_scale: + # scale & bulk update + key_blocks = tuple(k for k in shapekey.id_data.key_blocks[1:] if not k.mute and k.value and k.name != cls.SHAPEKEY_NAME) + for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): + bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] + # if use_skip and not cls.__check_bone_update(obj, bone0, bone1): + # continue + mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted() + mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted() + rot0 = mat0.to_euler("YXZ").to_quaternion() + rot1 = mat1.to_euler("YXZ").to_quaternion() + if rot1.dot(rot0) < 0: + rot1 = -rot1 + s0, s1 = mat0.to_scale(), mat1.to_scale() + + def scale(mat_rot, w0, w1): + s = s0 * w0 + s1 * w1 + return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])]) + + def offset(mat_rot, pos_c, vid): + delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = '' + return (mat_rot @ (pos_c + delta)) - delta + + shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] + else: + # bulk update + for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): + bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] + if use_skip and not cls.__check_bone_update(obj, bone0, bone1): + continue + mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted() + mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted() + rot0 = mat0.to_euler("YXZ").to_quaternion() + rot1 = mat1.to_euler("YXZ").to_quaternion() + if rot1.dot(rot0) < 0: + rot1 = -rot1 + shapekey_data[vids] = [((rot0 * w0 + rot1 * w1).normalized().to_matrix() @ pos_c) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] + shapekey.data.foreach_set("co", shapekey_data.reshape(3 * len(shapekey.data))) + + return 1.0 # shapkey value + + @classmethod + def register_driver_function(cls): + if "mmd_sdef_driver" not in bpy.app.driver_namespace: + bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function + if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace: + bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap + + BENCH_LOOP = 10 + + @classmethod + def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip): + # warmed up + cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale) + cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale) + # benchmark + t = time.time() + for i in range(cls.BENCH_LOOP): + cls.driver_function(shapkey, obj.name, bulk_update=False, use_skip=False, use_scale=use_scale) + default_time = time.time() - t + t = time.time() + for i in range(cls.BENCH_LOOP): + cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale) + bulk_time = time.time() - t + result = default_time > bulk_time + logging.info("FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s", default_time, bulk_time, result) + return result + + @classmethod + def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False): + # Unbind first + cls.unbind(obj) + if not cls.has_sdef_data(obj): + return False + # Create the shapekey for the driver + shapekey = obj.shape_key_add(name=cls.SHAPEKEY_NAME, from_mix=False) + cls.__init_cache(obj, shapekey) + cls.__sdef_muted(obj, shapekey) + cls.register_driver_function() + if bulk_update is None: + bulk_update = cls.__get_benchmark_result(obj, shapekey, use_scale, use_skip) + # Add the driver to the shapekey + f = obj.data.shape_keys.driver_add('key_blocks["' + cls.SHAPEKEY_NAME + '"].value', -1) + if hasattr(f.driver, "show_debug_info"): + f.driver.show_debug_info = False + f.driver.type = "SCRIPTED" + ov = f.driver.variables.new() + ov.name = "obj" + ov.type = "SINGLE_PROP" + ov.targets[0].id = obj + ov.targets[0].data_path = "name" + if not bulk_update and use_skip: # FIXME: force disable use_skip=True for bulk_update=False on 2.8 + use_skip = False + mod = obj.modifiers.get("mmd_bone_order_override") + variables = f.driver.variables + for name in set(data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)): # add required bones for dependency graph + var = variables.new() + var.type = "TRANSFORMS" + var.targets[0].id = mod.object + var.targets[0].bone_target = name + f.driver.use_self = True + param = (bulk_update, use_skip, use_scale) + f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param) + return True + + @classmethod + def unbind(cls, obj): + if obj.data.shape_keys: + if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks: + FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]) + for mod in obj.modifiers: + if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME: + mod.vertex_group = "" + mod.invert_vertex_group = False + break + if cls.MASK_NAME in obj.vertex_groups: + obj.vertex_groups.remove(obj.vertex_groups[cls.MASK_NAME]) + cls.clear_cache(obj) + + @classmethod + def clear_cache(cls, obj=None, unused_only=False): + if unused_only: + valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj) + for key in cls.g_verts.keys() - valid_keys: + del cls.g_verts[key] + for key in cls.g_shapekey_data.keys() - cls.g_verts.keys(): + del cls.g_shapekey_data[key] + for key in cls.g_bone_check.keys() - cls.g_verts.keys(): + del cls.g_bone_check[key] + elif obj: + key = _hash(obj) + if key in cls.g_verts: + del cls.g_verts[key] + if key in cls.g_shapekey_data: + del cls.g_shapekey_data[key] + if key in cls.g_bone_check: + del cls.g_bone_check[key] + else: + cls.g_verts = {} + cls.g_bone_check = {} + cls.g_shapekey_data = {} diff --git a/core/mmd/core/shader.py b/core/mmd/core/shader.py new file mode 100644 index 0000000..9d32742 --- /dev/null +++ b/core/mmd/core/shader.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +from typing import Optional, Tuple, cast +import bpy + + +class _NodeTreeUtils: + def __init__(self, shader: bpy.types.ShaderNodeTree): + self.shader = shader + self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore + self.links = shader.links + + def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]: + return next((n for n in self.nodes if n.bl_idname == node_type), None) + + def new_node(self, idname: str, pos: Tuple[int, int]) -> bpy.types.ShaderNode: + node: bpy.types.ShaderNode = self.nodes.new(idname) + node.location = (pos[0] * 210, pos[1] * 220) + return node + + def new_math_node(self, operation, pos, value1=None, value2=None): + node = self.new_node("ShaderNodeMath", pos) + node.operation = operation + if value1 is not None: + node.inputs[0].default_value = value1 + if value2 is not None: + node.inputs[1].default_value = value2 + return node + + def new_vector_math_node(self, operation, pos, vector1=None, vector2=None): + node = self.new_node("ShaderNodeVectorMath", pos) + node.operation = operation + if vector1 is not None: + node.inputs[0].default_value = vector1 + if vector2 is not None: + node.inputs[1].default_value = vector2 + return node + + def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None): + node = self.new_node("ShaderNodeMixRGB", pos) + node.blend_type = blend_type + if fac is not None: + node.inputs["Fac"].default_value = fac + if color1 is not None: + node.inputs["Color1"].default_value = color1 + if color2 is not None: + node.inputs["Color2"].default_value = color2 + return node + + +SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"} + +SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"} + + +class _NodeGroupUtils(_NodeTreeUtils): + def __init__(self, shader: bpy.types.ShaderNodeTree): + super().__init__(shader) + self.__node_input: Optional[bpy.types.NodeGroupInput] = None + self.__node_output: Optional[bpy.types.NodeGroupOutput] = None + + @property + def node_input(self) -> bpy.types.NodeGroupInput: + if not self.__node_input: + self.__node_input = cast(bpy.types.NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0))) + return self.__node_input + + @property + def node_output(self) -> bpy.types.NodeGroupOutput: + if not self.__node_output: + self.__node_output = cast(bpy.types.NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0))) + return self.__node_output + + def hide_nodes(self, hide_sockets=True): + skip_nodes = {self.__node_input, self.__node_output} + for n in (x for x in self.nodes if x not in skip_nodes): + n.hide = True + if not hide_sockets: + continue + for s in n.inputs: + s.hide = not s.is_linked + for s in n.outputs: + s.hide = not s.is_linked + + def new_input_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None): + self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type) + + def new_output_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None): + self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type) + + def __new_io(self, in_out, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None): + if io_name not in io_sockets: + idname = socket_type or socket.bl_idname + interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname)) + if idname in SOCKET_SUBTYPE_MAPPING: + interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "") + if not min_max: + if idname.endswith("Factor") or io_name.endswith("Alpha"): + interface_socket.min_value, interface_socket.max_value = 0, 1 + elif idname.endswith("Float") or idname.endswith("Vector"): + interface_socket.min_value, interface_socket.max_value = -10, 10 + if socket is not None: + self.links.new(io_sockets[io_name], socket) + if default_val is not None: + interface_socket.default_value = default_val + if min_max is not None: + interface_socket.min_value, interface_socket.max_value = min_max + + +class _MaterialMorph: + @classmethod + def update_morph_inputs(cls, material, morph): + if material and material.node_tree and morph.name in material.node_tree.nodes: + cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph) + cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph) + + @classmethod + def setup_morph_nodes(cls, material, morphs): + node, nodes = None, [] + for m in morphs: + node = cls.__morph_node_add(material, m, node) + nodes.append(node) + if node: + node = cls.__morph_node_add(material, None, node) or node + for n in reversed(nodes): + n.location += node.location + if n.node_tree.name != node.node_tree.name: + n.location.x -= 100 + if node.name.startswith("mmd_"): + n.location.y += 1500 + node = n + return nodes + + @classmethod + def reset_morph_links(cls, node): + cls.__update_morph_links(node, reset=True) + + @classmethod + def __update_morph_links(cls, node, reset=False): + nodes, links = node.id_data.nodes, node.id_data.links + if reset: + if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links): + return + + def __init_link(socket_morph, socket_shader): + if socket_shader and socket_morph.is_linked: + links.new(socket_morph.links[0].from_socket, socket_shader) + + else: + + def __init_link(socket_morph, socket_shader): + if socket_shader: + if socket_shader.is_linked: + links.new(socket_shader.links[0].from_socket, socket_morph) + if socket_morph.type == "VALUE": + socket_morph.default_value = socket_shader.default_value + else: + socket_morph.default_value[:3] = socket_shader.default_value[:3] + + shader = nodes.get("mmd_shader", None) + if shader: + __init_link(node.inputs["Ambient1"], shader.inputs.get("Ambient Color")) + __init_link(node.inputs["Diffuse1"], shader.inputs.get("Diffuse Color")) + __init_link(node.inputs["Specular1"], shader.inputs.get("Specular Color")) + __init_link(node.inputs["Reflect1"], shader.inputs.get("Reflect")) + __init_link(node.inputs["Alpha1"], shader.inputs.get("Alpha")) + __init_link(node.inputs["Base1 RGB"], shader.inputs.get("Base Tex")) + __init_link(node.inputs["Toon1 RGB"], shader.inputs.get("Toon Tex")) # FIXME toon only affect shadow color + __init_link(node.inputs["Sphere1 RGB"], shader.inputs.get("Sphere Tex")) + elif "mmd_edge_preview" in nodes: + shader = nodes["mmd_edge_preview"] + __init_link(node.inputs["Edge1 RGB"], shader.inputs["Color"]) + __init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"]) + + @classmethod + def __update_node_inputs(cls, node, morph): + node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3] + node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3] + node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3] + node.inputs["Reflect2"].default_value = morph.shininess + node.inputs["Alpha2"].default_value = morph.diffuse_color[3] + + node.inputs["Edge2 RGB"].default_value[:3] = morph.edge_color[:3] + node.inputs["Edge2 A"].default_value = morph.edge_color[3] + + node.inputs["Base2 RGB"].default_value[:3] = morph.texture_factor[:3] + node.inputs["Base2 A"].default_value = morph.texture_factor[3] + node.inputs["Toon2 RGB"].default_value[:3] = morph.toon_texture_factor[:3] + node.inputs["Toon2 A"].default_value = morph.toon_texture_factor[3] + node.inputs["Sphere2 RGB"].default_value[:3] = morph.sphere_texture_factor[:3] + node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3] + + @classmethod + def __morph_node_add(cls, material, morph, prev_node): + nodes, links = material.node_tree.nodes, material.node_tree.links + + shader = nodes.get("mmd_shader", None) + if morph: + node = nodes.new("ShaderNodeGroup") + node.parent = getattr(shader, "parent", None) + node.location = (-250, 0) + node.node_tree = cls.__get_shader("Add" if morph.offset_type == "ADD" else "Mul") + cls.__update_node_inputs(node, morph) + if prev_node: + for id_name in ("Ambient", "Diffuse", "Specular", "Reflect", "Alpha"): + links.new(prev_node.outputs[id_name], node.inputs[id_name + "1"]) + for id_name in ("Edge", "Base", "Toon", "Sphere"): + links.new(prev_node.outputs[id_name + " RGB"], node.inputs[id_name + "1 RGB"]) + links.new(prev_node.outputs[id_name + " A"], node.inputs[id_name + "1 A"]) + else: # initial first node + if node.node_tree.name.endswith("Add"): + node.inputs["Base1 A"].default_value = 1 + node.inputs["Toon1 A"].default_value = 1 + node.inputs["Sphere1 A"].default_value = 1 + cls.__update_morph_links(node) + return node + # connect last node to shader + if shader: + + def __soft_link(socket_out, socket_in): + if socket_out and socket_in: + links.new(socket_out, socket_in) + + __soft_link(prev_node.outputs["Ambient"], shader.inputs.get("Ambient Color")) + __soft_link(prev_node.outputs["Diffuse"], shader.inputs.get("Diffuse Color")) + __soft_link(prev_node.outputs["Specular"], shader.inputs.get("Specular Color")) + __soft_link(prev_node.outputs["Reflect"], shader.inputs.get("Reflect")) + __soft_link(prev_node.outputs["Alpha"], shader.inputs.get("Alpha")) + __soft_link(prev_node.outputs["Base Tex"], shader.inputs.get("Base Tex")) + __soft_link(prev_node.outputs["Toon Tex"], shader.inputs.get("Toon Tex")) + if int(material.mmd_material.sphere_texture_type) != 2: # shader.inputs['Sphere Mul/Add'].default_value < 0.5 + __soft_link(prev_node.outputs["Sphere Tex"], shader.inputs.get("Sphere Tex")) + else: + __soft_link(prev_node.outputs["Sphere Tex Add"], shader.inputs.get("Sphere Tex")) + elif "mmd_edge_preview" in nodes: + shader = nodes["mmd_edge_preview"] + links.new(prev_node.outputs["Edge RGB"], shader.inputs["Color"]) + links.new(prev_node.outputs["Edge A"], shader.inputs["Alpha"]) + return shader + + @classmethod + def __get_shader(cls, morph_type): + group_name = "MMDMorph" + morph_type + shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") + if len(shader.nodes): + return shader + + ng = _NodeGroupUtils(shader) + links = ng.links + + use_mul = morph_type == "Mul" + + ############################################################################ + node_input = ng.new_node("NodeGroupInput", (-3, 0)) + ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat") + ng.new_node("NodeGroupOutput", (3, 0)) + + def __blend_color_add(id_name, pos, tag=""): + # MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac)) + # MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2 + # https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400 + node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos[0] + 1, pos[1])) + links.new(node_input.outputs["Fac"], node_mix.inputs["Fac"]) + ng.new_input_socket("%s1" % id_name + tag, node_mix.inputs["Color1"]) + ng.new_input_socket("%s2" % id_name + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector") + ng.new_output_socket(id_name + tag, node_mix.outputs["Color"]) + return node_mix + + def __blend_tex_color(id_name, pos, node_tex_rgb, node_tex_a_output): + # Tex Color = tex_rgb * tex_a + (1 - tex_a) + # : tex_rgb = TexRGB * ColorMul + ColorAdd + # : tex_a = TexA * ValueMul + ValueAdd + if id_name != "Sphere": + node_mix = ng.new_mix_node("MULTIPLY", pos, color1=(1, 1, 1, 1)) + links.new(node_tex_a_output, node_mix.inputs[0]) + links.new(node_tex_rgb.outputs["Color"], node_mix.inputs[2]) + ng.new_output_socket(id_name + " Tex", node_mix.outputs[0]) + else: + node_inv = ng.new_math_node("SUBTRACT", (pos[0], pos[1] - 0.25), value1=1.0) + node_scale = ng.new_vector_math_node("SCALE", (pos[0], pos[1])) + node_add = ng.new_vector_math_node("ADD", (pos[0] + 1, pos[1])) + + links.new(node_tex_a_output, node_inv.inputs[1]) + links.new(node_tex_rgb.outputs["Color"], node_scale.inputs[0]) + links.new(node_tex_a_output, node_scale.inputs["Scale"]) + links.new(node_scale.outputs[0], node_add.inputs[0]) + links.new(node_inv.outputs[0], node_add.inputs[1]) + + ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor") + ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor") + + def __add_sockets(id_name, input1, input2, output, tag=""): + ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul) + ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul) + ng.new_output_socket(f"{id_name}{tag}", output) + + pos_x = -2 + __blend_color_add("Ambient", (pos_x, +0.5)) + __blend_color_add("Diffuse", (pos_x, +0.0)) + __blend_color_add("Specular", (pos_x, -0.5)) + + combine_reflect1_alpha1_edge1 = ng.new_node("ShaderNodeCombineRGB", (-2, -1.5)) + combine_reflect2_alpha2_edge2 = ng.new_node("ShaderNodeCombineRGB", (-2, -1.75)) + separate_reflect_alpha_edge = ng.new_node("ShaderNodeSeparateRGB", (pos_x + 2, -1.5)) + + __add_sockets("Reflect", combine_reflect1_alpha1_edge1.inputs[0], combine_reflect2_alpha2_edge2.inputs[0], separate_reflect_alpha_edge.outputs[0]) + __add_sockets("Alpha", combine_reflect1_alpha1_edge1.inputs[1], combine_reflect2_alpha2_edge2.inputs[1], separate_reflect_alpha_edge.outputs[1]) + + __blend_color_add("Edge", (pos_x, -1.0), " RGB") + __add_sockets("Edge", combine_reflect1_alpha1_edge1.inputs[2], combine_reflect2_alpha2_edge2.inputs[2], separate_reflect_alpha_edge.outputs[2], tag=" A") + + node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos_x + 1, -1.5)) + links.new(node_input.outputs["Fac"], node_mix.inputs[0]) + links.new(combine_reflect1_alpha1_edge1.outputs[0], node_mix.inputs[1]) + links.new(combine_reflect2_alpha2_edge2.outputs[0], node_mix.inputs[2]) + links.new(node_mix.outputs[0], separate_reflect_alpha_edge.inputs[0]) + + combine_base1a_toon1a_sphere1a = ng.new_node("ShaderNodeCombineRGB", (-2, -2.0)) + combine_base2a_toon2a_sphere2a = ng.new_node("ShaderNodeCombineRGB", (-2, -2.25)) + separate_basea_toona_spherea = ng.new_node("ShaderNodeSeparateRGB", (pos_x + 2, -2.0)) + + node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos_x + 1, -2.0)) + links.new(node_input.outputs["Fac"], node_mix.inputs[0]) + links.new(combine_base1a_toon1a_sphere1a.outputs[0], node_mix.inputs[1]) + links.new(combine_base2a_toon2a_sphere2a.outputs[0], node_mix.inputs[2]) + links.new(node_mix.outputs[0], separate_basea_toona_spherea.inputs[0]) + + base_rgb = __blend_color_add("Base", (pos_x, -2.5), " RGB") + __add_sockets("Base", combine_base1a_toon1a_sphere1a.inputs[0], combine_base2a_toon2a_sphere2a.inputs[0], separate_basea_toona_spherea.outputs[0], tag=" A") + __blend_tex_color("Base", (pos_x + 3, -2.5), base_rgb, separate_basea_toona_spherea.outputs[0]) + + toon_rgb = __blend_color_add("Toon", (pos_x, -3.0), " RGB") + __add_sockets("Toon", combine_base1a_toon1a_sphere1a.inputs[1], combine_base2a_toon2a_sphere2a.inputs[1], separate_basea_toona_spherea.outputs[1], tag=" A") + __blend_tex_color("Toon", (pos_x + 3, -3.0), toon_rgb, separate_basea_toona_spherea.outputs[1]) + + sphere_rgb = __blend_color_add("Sphere", (pos_x, -3.5), " RGB") + __add_sockets("Sphere", combine_base1a_toon1a_sphere1a.inputs[2], combine_base2a_toon2a_sphere2a.inputs[2], separate_basea_toona_spherea.outputs[2], tag=" A") + __blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2]) + + ng.hide_nodes() + return ng.shader diff --git a/core/mmd/core/translations.py b/core/mmd/core/translations.py new file mode 100644 index 0000000..6574ba0 --- /dev/null +++ b/core/mmd/core/translations.py @@ -0,0 +1,738 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import itertools +import re +from abc import ABC, abstractmethod +from enum import Enum +from typing import TYPE_CHECKING, Callable, Dict, Optional, Set, Tuple + +import bpy + +from ..translations import DictionaryEnum +from ..utils import convertLRToName, convertNameToLR +from .model import FnModel, Model + +if TYPE_CHECKING: + from ..properties.morph import _MorphBase + from ..properties.root import MMDRoot + from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex + + +class MMDTranslationElementType(Enum): + BONE = "Bones" + MORPH = "Morphs" + MATERIAL = "Materials" + DISPLAY = "Display" + PHYSICS = "Physics" + INFO = "Information" + + +class MMDDataHandlerABC(ABC): + @classmethod + @property + @abstractmethod + def type_name(cls) -> str: + pass + + @classmethod + @abstractmethod + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + pass + + @classmethod + @abstractmethod + def collect_data(cls, mmd_translation: "MMDTranslation"): + pass + + @classmethod + @abstractmethod + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): + pass + + @classmethod + @abstractmethod + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + pass + + @classmethod + @abstractmethod + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + pass + + @classmethod + @abstractmethod + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + """Returns (name, name_j, name_e)""" + + @classmethod + def is_restorable(cls, mmd_translation_element: "MMDTranslationElement") -> bool: + return (mmd_translation_element.name, mmd_translation_element.name_j, mmd_translation_element.name_e) != cls.get_names(mmd_translation_element) + + @classmethod + def check_data_visible(cls, filter_selected: bool, filter_visible: bool, select: bool, hide: bool) -> bool: + return filter_selected and not select or filter_visible and hide + + @classmethod + def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str, original_value: str, index: int): + row = layout.row(align=True) + row.prop(mmd_translation_element, prop_name, text="") + + if getattr(mmd_translation_element, prop_name) == original_value: + row.label(text="", icon="BLANK1") + return + + op = row.operator("mmd_tools.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH") + op.index = index + op.prop_name = prop_name + op.restore_value = original_value + + @classmethod + def prop_disabled(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str): + row = layout.row(align=True) + row.enabled = False + row.prop(mmd_translation_element, prop_name, text="") + row.label(text="", icon="BLANK1") + + +class MMDBoneHandler(MMDDataHandlerABC): + @classmethod + @property + def type_name(cls) -> str: + return MMDTranslationElementType.BONE.name + + @classmethod + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + row = layout.row(align=True) + row.label(text="", icon="BONE_DATA") + prop_row = row.row() + cls.prop_restorable(prop_row, mmd_translation_element, "name", pose_bone.name, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_j", pose_bone.mmd_bone.name_j, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_e", pose_bone.mmd_bone.name_e, index) + row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.bone.select else "RESTRICT_SELECT_ON") + row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if pose_bone.bone.hide else "HIDE_OFF") + + @classmethod + def collect_data(cls, mmd_translation: "MMDTranslation"): + armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data) + pose_bone: bpy.types.PoseBone + for index, pose_bone in enumerate(armature_object.pose.bones): + if not any(c.is_visible for c in pose_bone.bone.collections): + continue + + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() + mmd_translation_element.type = MMDTranslationElementType.BONE.name + mmd_translation_element.object = armature_object + mmd_translation_element.data_path = f"pose.bones[{index}]" + mmd_translation_element.name = pose_bone.name + mmd_translation_element.name_j = pose_bone.mmd_bone.name_j + mmd_translation_element.name_e = pose_bone.mmd_bone.name_e + + @classmethod + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): + bpy.context.view_layer.objects.active = mmd_translation_element.object + mmd_translation_element.object.id_data.data.bones.active = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path).bone + + @classmethod + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" + for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): + if mmd_translation_element.type != MMDTranslationElementType.BONE.name: + continue + + pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + + if cls.check_data_visible(filter_selected, filter_visible, pose_bone.bone.select, pose_bone.bone.hide): + continue + + if check_blank_name(mmd_translation_element.name_j, mmd_translation_element.name_e): + continue + + if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): + continue + + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index.value = index + + @classmethod + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + if name is not None: + pose_bone.name = name + if name_j is not None: + pose_bone.mmd_bone.name_j = name_j + if name_e is not None: + pose_bone.mmd_bone.name_e = name_e + + @classmethod + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + return (pose_bone.name, pose_bone.mmd_bone.name_j, pose_bone.mmd_bone.name_e) + + +class MMDMorphHandler(MMDDataHandlerABC): + @classmethod + @property + def type_name(cls) -> str: + return MMDTranslationElementType.MORPH.name + + @classmethod + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + row = layout.row(align=True) + row.label(text="", icon="SHAPEKEY_DATA") + prop_row = row.row() + cls.prop_disabled(prop_row, mmd_translation_element, "name") + cls.prop_restorable(prop_row, mmd_translation_element, "name", morph.name, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_e", morph.name_e, index) + row.label(text="", icon="BLANK1") + row.label(text="", icon="BLANK1") + + MORPH_DATA_PATH_EXTRACT = re.compile(r"mmd_root\.(?P[^\[]*)\[(?P\d*)\]") + + @classmethod + def collect_data(cls, mmd_translation: "MMDTranslation"): + root_object: bpy.types.Object = mmd_translation.id_data + mmd_root: "MMDRoot" = root_object.mmd_root + + for morphs_name, morphs in { + "material_morphs": mmd_root.material_morphs, + "uv_morphs": mmd_root.uv_morphs, + "bone_morphs": mmd_root.bone_morphs, + "vertex_morphs": mmd_root.vertex_morphs, + "group_morphs": mmd_root.group_morphs, + }.items(): + morph: "_MorphBase" + for index, morph in enumerate(morphs): + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() + mmd_translation_element.type = MMDTranslationElementType.MORPH.name + mmd_translation_element.object = root_object + mmd_translation_element.data_path = f"mmd_root.{morphs_name}[{index}]" + mmd_translation_element.name = morph.name + # mmd_translation_element.name_j = None + mmd_translation_element.name_e = morph.name_e + + @classmethod + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): + match = cls.MORPH_DATA_PATH_EXTRACT.match(mmd_translation_element.data_path) + if not match: + return + + mmd_translation_element.object.mmd_root.active_morph_type = match["morphs_name"] + mmd_translation_element.object.mmd_root.active_morph = int(match["index"]) + + @classmethod + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" + for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): + if mmd_translation_element.type != MMDTranslationElementType.MORPH.name: + continue + + morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + if check_blank_name(morph.name, morph.name_e): + continue + + if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): + continue + + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index.value = index + + @classmethod + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + if name is not None: + morph.name = name + if name_e is not None: + morph.name_e = name_e + + @classmethod + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + return (morph.name, "", morph.name_e) + + +class MMDMaterialHandler(MMDDataHandlerABC): + @classmethod + @property + def type_name(cls) -> str: + return MMDTranslationElementType.MATERIAL.name + + @classmethod + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + mesh_object: bpy.types.Object = mmd_translation_element.object + material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + row = layout.row(align=True) + row.label(text="", icon="MATERIAL_DATA") + prop_row = row.row() + cls.prop_restorable(prop_row, mmd_translation_element, "name", material.name, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_j", material.mmd_material.name_j, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_e", material.mmd_material.name_e, index) + row.prop(mesh_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mesh_object.select_get() else "RESTRICT_SELECT_ON") + row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mesh_object.hide_get() else "HIDE_OFF") + + MATERIAL_DATA_PATH_EXTRACT = re.compile(r"data\.materials\[(?P\d*)\]") + + @classmethod + def collect_data(cls, mmd_translation: "MMDTranslation"): + checked_materials: Set[bpy.types.Material] = set() + mesh_object: bpy.types.Object + for mesh_object in FnModel.iterate_mesh_objects(mmd_translation.id_data): + material: bpy.types.Material + for index, material in enumerate(mesh_object.data.materials): + if material in checked_materials: + continue + + checked_materials.add(material) + + if not hasattr(material, "mmd_material"): + continue + + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() + mmd_translation_element.type = MMDTranslationElementType.MATERIAL.name + mmd_translation_element.object = mesh_object + mmd_translation_element.data_path = f"data.materials[{index}]" + mmd_translation_element.name = material.name + mmd_translation_element.name_j = material.mmd_material.name_j + mmd_translation_element.name_e = material.mmd_material.name_e + + @classmethod + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): + id_data: bpy.types.Object = mmd_translation_element.object + bpy.context.view_layer.objects.active = id_data + + match = cls.MATERIAL_DATA_PATH_EXTRACT.match(mmd_translation_element.data_path) + if not match: + return + + id_data.active_material_index = int(match["index"]) + + @classmethod + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" + for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): + if mmd_translation_element.type != MMDTranslationElementType.MATERIAL.name: + continue + + mesh_object: bpy.types.Object = mmd_translation_element.object + if cls.check_data_visible(filter_selected, filter_visible, mesh_object.select_get(), mesh_object.hide_get()): + continue + + material: bpy.types.Material = mesh_object.path_resolve(mmd_translation_element.data_path) + if check_blank_name(material.mmd_material.name_j, material.mmd_material.name_e): + continue + + if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): + continue + + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index.value = index + + @classmethod + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + if name is not None: + material.name = name + if name_j is not None: + material.mmd_material.name_j = name_j + if name_e is not None: + material.mmd_material.name_e = name_e + + @classmethod + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + return (material.name, material.mmd_material.name_j, material.mmd_material.name_e) + + +class MMDDisplayHandler(MMDDataHandlerABC): + @classmethod + @property + def type_name(cls) -> str: + return MMDTranslationElementType.DISPLAY.name + + @classmethod + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + row = layout.row(align=True) + row.label(text="", icon="GROUP_BONE") + + prop_row = row.row() + cls.prop_restorable(prop_row, mmd_translation_element, "name", bone_collection.name, index) + cls.prop_disabled(prop_row, mmd_translation_element, "name") + cls.prop_disabled(prop_row, mmd_translation_element, "name_e") + row.prop(mmd_translation_element.object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mmd_translation_element.object.select_get() else "RESTRICT_SELECT_ON") + row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mmd_translation_element.object.hide_get() else "HIDE_OFF") + + DISPLAY_DATA_PATH_EXTRACT = re.compile(r"data\.collections\[(?P\d*)\]") + + @classmethod + def collect_data(cls, mmd_translation: "MMDTranslation"): + armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data) + bone_collection: bpy.types.BoneCollection + for index, bone_collection in enumerate(armature_object.data.collections): + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() + mmd_translation_element.type = MMDTranslationElementType.DISPLAY.name + mmd_translation_element.object = armature_object + mmd_translation_element.data_path = f"data.collections[{index}]" + mmd_translation_element.name = bone_collection.name + # mmd_translation_element.name_j = None + # mmd_translation_element.name_e = None + + @classmethod + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): + id_data: bpy.types.Object = mmd_translation_element.object + bpy.context.view_layer.objects.active = id_data + + match = cls.DISPLAY_DATA_PATH_EXTRACT.match(mmd_translation_element.data_path) + if not match: + return + + id_data.data.collections.active_index = int(match["index"]) + + @classmethod + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" + for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): + if mmd_translation_element.type != MMDTranslationElementType.DISPLAY.name: + continue + + obj: bpy.types.Object = mmd_translation_element.object + if cls.check_data_visible(filter_selected, filter_visible, obj.select_get(), obj.hide_get()): + continue + + bone_collection: bpy.types.BoneCollection = obj.path_resolve(mmd_translation_element.data_path) + if check_blank_name(bone_collection.name, ""): + continue + + if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): + continue + + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index.value = index + + @classmethod + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + if name is not None: + bone_collection.name = name + + @classmethod + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + return (bone_collection.name, "", "") + + +class MMDPhysicsHandler(MMDDataHandlerABC): + @classmethod + @property + def type_name(cls) -> str: + return MMDTranslationElementType.PHYSICS.name + + @classmethod + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + obj: bpy.types.Object = mmd_translation_element.object + + if FnModel.is_rigid_body_object(obj): + icon = "MESH_ICOSPHERE" + mmd_object = obj.mmd_rigid + elif FnModel.is_joint_object(obj): + icon = "CONSTRAINT" + mmd_object = obj.mmd_joint + + row = layout.row(align=True) + row.label(text="", icon=icon) + prop_row = row.row() + cls.prop_restorable(prop_row, mmd_translation_element, "name", obj.name, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_j", mmd_object.name_j, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_e", mmd_object.name_e, index) + row.prop(obj, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if obj.select_get() else "RESTRICT_SELECT_ON") + row.prop(obj, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if obj.hide_get() else "HIDE_OFF") + + @classmethod + def collect_data(cls, mmd_translation: "MMDTranslation"): + root_object: bpy.types.Object = mmd_translation.id_data + model = Model(root_object) + + obj: bpy.types.Object + for obj in model.rigidBodies(): + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() + mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name + mmd_translation_element.object = obj + mmd_translation_element.data_path = "mmd_rigid" + mmd_translation_element.name = obj.name + mmd_translation_element.name_j = obj.mmd_rigid.name_j + mmd_translation_element.name_e = obj.mmd_rigid.name_e + + obj: bpy.types.Object + for obj in model.joints(): + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() + mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name + mmd_translation_element.object = obj + mmd_translation_element.data_path = "mmd_joint" + mmd_translation_element.name = obj.name + mmd_translation_element.name_j = obj.mmd_joint.name_j + mmd_translation_element.name_e = obj.mmd_joint.name_e + + @classmethod + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): + bpy.context.view_layer.objects.active = mmd_translation_element.object + + @classmethod + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" + for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): + if mmd_translation_element.type != MMDTranslationElementType.PHYSICS.name: + continue + + obj: bpy.types.Object = mmd_translation_element.object + if cls.check_data_visible(filter_selected, filter_visible, obj.select_get(), obj.hide_get()): + continue + + if FnModel.is_rigid_body_object(obj): + mmd_object = obj.mmd_rigid + elif FnModel.is_joint_object(obj): + mmd_object = obj.mmd_joint + + if check_blank_name(mmd_object.name_j, mmd_object.name_e): + continue + + if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): + continue + + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index.value = index + + @classmethod + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + obj: bpy.types.Object = mmd_translation_element.object + + if FnModel.is_rigid_body_object(obj): + mmd_object = obj.mmd_rigid + elif FnModel.is_joint_object(obj): + mmd_object = obj.mmd_joint + + if name is not None: + obj.name = name + if name_j is not None: + mmd_object.name_j = name_j + if name_e is not None: + mmd_object.name_e = name_e + + @classmethod + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + obj: bpy.types.Object = mmd_translation_element.object + + if FnModel.is_rigid_body_object(obj): + mmd_object = obj.mmd_rigid + elif FnModel.is_joint_object(obj): + mmd_object = obj.mmd_joint + + return (obj.name, mmd_object.name_j, mmd_object.name_e) + + +class MMDInfoHandler(MMDDataHandlerABC): + @classmethod + @property + def type_name(cls) -> str: + return MMDTranslationElementType.INFO.name + + TYPE_TO_ICONS = { + "EMPTY": "EMPTY_DATA", + "ARMATURE": "ARMATURE_DATA", + "MESH": "MESH_DATA", + } + + @classmethod + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + info_object: bpy.types.Object = mmd_translation_element.object + row = layout.row(align=True) + row.label(text="", icon=MMDInfoHandler.TYPE_TO_ICONS.get(info_object.type, "OBJECT_DATA")) + prop_row = row.row() + cls.prop_restorable(prop_row, mmd_translation_element, "name", info_object.name, index) + cls.prop_disabled(prop_row, mmd_translation_element, "name") + cls.prop_disabled(prop_row, mmd_translation_element, "name_e") + row.prop(info_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if info_object.select_get() else "RESTRICT_SELECT_ON") + row.prop(info_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if info_object.hide_get() else "HIDE_OFF") + + @classmethod + def collect_data(cls, mmd_translation: "MMDTranslation"): + root_object: bpy.types.Object = mmd_translation.id_data + info_objects = [root_object] + armature_object = FnModel.find_armature_object(root_object) + if armature_object is not None: + info_objects.append(armature_object) + + for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)): + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() + mmd_translation_element.type = MMDTranslationElementType.INFO.name + mmd_translation_element.object = info_object + mmd_translation_element.data_path = "" + mmd_translation_element.name = info_object.name + # mmd_translation_element.name_j = None + # mmd_translation_element.name_e = None + + @classmethod + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): + bpy.context.view_layer.objects.active = mmd_translation_element.object + + @classmethod + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" + for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): + if mmd_translation_element.type != MMDTranslationElementType.INFO.name: + continue + + info_object: bpy.types.Object = mmd_translation_element.object + if cls.check_data_visible(filter_selected, filter_visible, info_object.select_get(), info_object.hide_get()): + continue + + if check_blank_name(info_object.name, ""): + continue + + if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): + continue + + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index.value = index + + @classmethod + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + info_object: bpy.types.Object = mmd_translation_element.object + if name is not None: + info_object.name = name + + @classmethod + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + info_object: bpy.types.Object = mmd_translation_element.object + return (info_object.name, "", "") + + +MMD_DATA_HANDLERS: Set[MMDDataHandlerABC] = { + MMDBoneHandler, + MMDMorphHandler, + MMDMaterialHandler, + MMDDisplayHandler, + MMDPhysicsHandler, + MMDInfoHandler, +} + +MMD_DATA_TYPE_TO_HANDLERS: Dict[str, MMDDataHandlerABC] = {h.type_name: h for h in MMD_DATA_HANDLERS} + + +class FnTranslations: + @staticmethod + def apply_translations(root_object: bpy.types.Object): + mmd_translation: "MMDTranslation" = root_object.mmd_root.translation + mmd_translation_element_index: "MMDTranslationElementIndex" + for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices: + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] + handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type] + name, name_j, name_e = handler.get_names(mmd_translation_element) + handler.set_names( + mmd_translation_element, + mmd_translation_element.name if mmd_translation_element.name != name else None, + mmd_translation_element.name_j if mmd_translation_element.name_j != name_j else None, + mmd_translation_element.name_e if mmd_translation_element.name_e != name_e else None, + ) + + @staticmethod + def execute_translation_batch(root_object: bpy.types.Object) -> Tuple[Dict[str, str], Optional[bpy.types.Text]]: + mmd_translation: "MMDTranslation" = root_object.mmd_root.translation + batch_operation_script = mmd_translation.batch_operation_script + if not batch_operation_script: + return ({}, None) + + translator = DictionaryEnum.get_translator(mmd_translation.dictionary) + + def translate(name: str) -> str: + if translator: + return translator.translate(name, name) + return name + + batch_operation_script_ast = compile(mmd_translation.batch_operation_script, "", "eval") + batch_operation_target: str = mmd_translation.batch_operation_target + + mmd_translation_element_index: "MMDTranslationElementIndex" + for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices: + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] + + handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type] + + name = mmd_translation_element.name + name_j = mmd_translation_element.name_j + name_e = mmd_translation_element.name_e + org_name, org_name_j, org_name_e = handler.get_names(mmd_translation_element) + + # pylint: disable=eval-used + result_name = str( + eval( + batch_operation_script_ast, + {"__builtins__": {}}, + { + "to_english": translate, + "to_mmd_lr": convertLRToName, + "to_blender_lr": convertNameToLR, + "name": name, + "name_j": name_j if name_j != "" else name, + "name_e": name_e if name_e != "" else name, + "org_name": org_name, + "org_name_j": org_name_j, + "org_name_e": org_name_e, + }, + ) + ) + + if batch_operation_target == "BLENDER": + mmd_translation_element.name = result_name + elif batch_operation_target == "JAPANESE": + mmd_translation_element.name_j = result_name + elif batch_operation_target == "ENGLISH": + mmd_translation_element.name_e = result_name + + return (translator.fails, translator.save_fails()) + + @staticmethod + def update_index(mmd_translation: "MMDTranslation"): + if mmd_translation.filtered_translation_element_indices_active_index < 0: + return + + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index] + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] + + MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].update_index(mmd_translation_element) + + @staticmethod + def collect_data(mmd_translation: "MMDTranslation"): + mmd_translation.translation_elements.clear() + for handler in MMD_DATA_HANDLERS: + handler.collect_data(mmd_translation) + + @staticmethod + def update_query(mmd_translation: "MMDTranslation"): + mmd_translation.filtered_translation_element_indices.clear() + mmd_translation.filtered_translation_element_indices_active_index = -1 + + filter_japanese_blank: bool = mmd_translation.filter_japanese_blank + filter_english_blank: bool = mmd_translation.filter_english_blank + + filter_selected: bool = mmd_translation.filter_selected + filter_visible: bool = mmd_translation.filter_visible + + def check_blank_name(name_j: str, name_e: str) -> bool: + return filter_japanese_blank and name_j or filter_english_blank and name_e + + for handler in MMD_DATA_HANDLERS: + if handler.type_name in mmd_translation.filter_types: + handler.update_query(mmd_translation, filter_selected, filter_visible, check_blank_name) + + @staticmethod + def clear_data(mmd_translation: "MMDTranslation"): + mmd_translation.translation_elements.clear() + mmd_translation.filtered_translation_element_indices.clear() + mmd_translation.filtered_translation_element_indices_active_index = -1 + mmd_translation.filter_restorable = False diff --git a/core/mmd/core/vmd/__init__.py b/core/mmd/core/vmd/__init__.py new file mode 100644 index 0000000..f3342f2 --- /dev/null +++ b/core/mmd/core/vmd/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. \ No newline at end of file diff --git a/core/mmd/core/vmd/importer.py b/core/mmd/core/vmd/importer.py new file mode 100644 index 0000000..07eb925 --- /dev/null +++ b/core/mmd/core/vmd/importer.py @@ -0,0 +1,673 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import logging +import math +import os +from typing import Union + +import bpy +from mathutils import Quaternion, Vector + +from ... import utils +from .. import vmd +from ..camera import MMDCamera +from ..lamp import MMDLamp + + +class _MirrorMapper: + def __init__(self, data_map=None): + from ...operators.view import FlipPose + + self.__data_map = data_map + self.__flip_name = FlipPose.flip_name + + def get(self, name, default=None): + return self.__data_map.get(self.__flip_name(name), None) or self.__data_map.get(name, default) + + @staticmethod + def get_location(location): + return (-location[0], location[1], location[2]) + + @staticmethod + def get_rotation(rotation_xyzw): + return (rotation_xyzw[0], -rotation_xyzw[1], -rotation_xyzw[2], rotation_xyzw[3]) + + @staticmethod + def get_rotation3(rotation_xyz): + return (rotation_xyz[0], -rotation_xyz[1], -rotation_xyz[2]) + + +class RenamedBoneMapper: + def __init__(self, armObj=None, rename_LR_bones=True, use_underscore=False, translator=None): + self.__pose_bones = armObj.pose.bones if armObj else None + self.__rename_LR_bones = rename_LR_bones + self.__use_underscore = use_underscore + self.__translator = translator + + def init(self, armObj): + self.__pose_bones = armObj.pose.bones + return self + + def get(self, bone_name, default=None): + bl_bone_name = bone_name + if self.__rename_LR_bones: + bl_bone_name = utils.convertNameToLR(bl_bone_name, self.__use_underscore) + if self.__translator: + bl_bone_name = self.__translator.translate(bl_bone_name) + return self.__pose_bones.get(bl_bone_name, default) + + +class _InterpolationHelper: + def __init__(self, mat): + self.__indices = indices = [0, 1, 2] + l = sorted((-abs(mat[i][j]), i, j) for i in range(3) for j in range(3)) + _, i, j = l[0] + if i != j: + indices[i], indices[j] = indices[j], indices[i] + _, i, j = next(k for k in l if k[1] != i and k[2] != j) + if indices[i] != j: + idx = indices.index(j) + indices[i], indices[idx] = indices[idx], indices[i] + + def convert(self, interpolation_xyz): + return (interpolation_xyz[i] for i in self.__indices) + + +class BoneConverter: + def __init__(self, pose_bone, scale, invert=False): + mat = pose_bone.bone.matrix_local.to_3x3() + mat[1], mat[2] = mat[2].copy(), mat[1].copy() + self.__mat = mat.transposed() + self.__scale = scale + if invert: + self.__mat.invert() + self.convert_interpolation = _InterpolationHelper(self.__mat).convert + + def convert_location(self, location): + return (self.__mat @ Vector(location)) * self.__scale + + def convert_rotation(self, rotation_xyzw): + rot = Quaternion() + rot.x, rot.y, rot.z, rot.w = rotation_xyzw + return Quaternion((self.__mat @ rot.axis) * -1, rot.angle).normalized() + + +class BoneConverterPoseMode: + def __init__(self, pose_bone, scale, invert=False): + mat = pose_bone.matrix.to_3x3() + mat[1], mat[2] = mat[2].copy(), mat[1].copy() + self.__mat = mat.transposed() + self.__scale = scale + self.__mat_rot = pose_bone.matrix_basis.to_3x3() + self.__mat_loc = self.__mat_rot @ self.__mat + self.__offset = pose_bone.location.copy() + self.convert_location = self._convert_location + self.convert_rotation = self._convert_rotation + if invert: + self.__mat.invert() + self.__mat_rot.invert() + self.__mat_loc.invert() + self.convert_location = self._convert_location_inverted + self.convert_rotation = self._convert_rotation_inverted + self.convert_interpolation = _InterpolationHelper(self.__mat_loc).convert + + def _convert_location(self, location): + return self.__offset + (self.__mat_loc @ Vector(location)) * self.__scale + + def _convert_rotation(self, rotation_xyzw): + rot = Quaternion() + rot.x, rot.y, rot.z, rot.w = rotation_xyzw + rot = Quaternion((self.__mat @ rot.axis) * -1, rot.angle) + return (self.__mat_rot @ rot.to_matrix()).to_quaternion() + + def _convert_location_inverted(self, location): + return (self.__mat_loc @ (Vector(location) - self.__offset)) * self.__scale + + def _convert_rotation_inverted(self, rotation_xyzw): + rot = Quaternion() + rot.x, rot.y, rot.z, rot.w = rotation_xyzw + rot = (self.__mat_rot @ rot.to_matrix()).to_quaternion() + return Quaternion((self.__mat @ rot.axis) * -1, rot.angle).normalized() + + +class _FnBezier: + @classmethod + def from_fcurve(cls, kp0, kp1): + p0, p1, p2, p3 = kp0.co, kp0.handle_right, kp1.handle_left, kp1.co + if p1.x > p3.x: + t = (p3.x - p0.x) / (p1.x - p0.x) + p1 = (1 - t) * p0 + p1 * t + if p0.x > p2.x: + t = (p3.x - p0.x) / (p3.x - p2.x) + p2 = (1 - t) * p3 + p2 * t + return cls(p0, p1, p2, p3) + + def __init__(self, p0, p1, p2, p3): # assuming VMD's bezier or F-Curve's bezier + # assert(p0.x <= p1.x <= p3.x and p0.x <= p2.x <= p3.x) + self._p0, self._p1, self._p2, self._p3 = p0, p1, p2, p3 + + @property + def points(self): + return self._p0, self._p1, self._p2, self._p3 + + def split(self, t): + p0, p1, p2, p3 = self._p0, self._p1, self._p2, self._p3 + p01t = (1 - t) * p0 + t * p1 + p12t = (1 - t) * p1 + t * p2 + p23t = (1 - t) * p2 + t * p3 + p012t = (1 - t) * p01t + t * p12t + p123t = (1 - t) * p12t + t * p23t + pt = (1 - t) * p012t + t * p123t + return _FnBezier(p0, p01t, p012t, pt), _FnBezier(pt, p123t, p23t, p3), pt + + def evaluate(self, t): + p0, p1, p2, p3 = self._p0, self._p1, self._p2, self._p3 + p01t = (1 - t) * p0 + t * p1 + p12t = (1 - t) * p1 + t * p2 + p23t = (1 - t) * p2 + t * p3 + p012t = (1 - t) * p01t + t * p12t + p123t = (1 - t) * p12t + t * p23t + return (1 - t) * p012t + t * p123t + + def split_by_x(self, x): + return self.split(self.axis_to_t(x)) + + def evaluate_by_x(self, x): + return self.evaluate(self.axis_to_t(x)) + + def axis_to_t(self, val, axis=0): + p0, p1, p2, p3 = self._p0[axis], self._p1[axis], self._p2[axis], self._p3[axis] + a = p3 - p0 + 3 * (p1 - p2) + b = 3 * (p0 - 2 * p1 + p2) + c = 3 * (p1 - p0) + d = p0 - val + return next(self.__find_roots(a, b, c, d)) + + def find_critical(self): + p0, p1, p2, p3 = self._p0.y, self._p1.y, self._p2.y, self._p3.y + p_min, p_max = (p0, p3) if p0 < p3 else (p3, p0) + if p1 > p_max or p1 < p_min or p2 > p_max or p2 < p_min: + a = 3 * (p3 - p0 + 3 * (p1 - p2)) + b = 6 * (p0 - 2 * p1 + p2) + c = 3 * (p1 - p0) + yield from self.__find_roots(0, a, b, c) + + @staticmethod + def __find_roots(a, b, c, d): # a*t*t*t + b*t*t + c*t + d = 0 + # TODO fix precision errors (ex: t=0 and t=1) and improve performance + if a == 0: + if b == 0: + t = -d / c + if 0 <= t <= 1: + yield t + else: + D = c * c - 4 * b * d + if D < 0: + return + D = D**0.5 + b2 = 2 * b + t = (-c + D) / b2 + if 0 <= t <= 1: + yield t + t = (-c - D) / b2 + if 0 <= t <= 1: + yield t + return + + def _sqrt3(v): + return -((-v) ** (1 / 3)) if v < 0 else v ** (1 / 3) + + A = b * c / (6 * a * a) - b * b * b / (27 * a * a * a) - d / (2 * a) + B = c / (3 * a) - b * b / (9 * a * a) + b_3a = -b / (3 * a) + D = A * A + B * B * B + + if D > 0: + D = D**0.5 + t = b_3a + _sqrt3(A + D) + _sqrt3(A - D) + if 0 <= t <= 1: + yield t + elif D == 0: + t = b_3a + _sqrt3(A) * 2 + if 0 <= t <= 1: + yield t + t = b_3a - _sqrt3(A) + if 0 <= t <= 1: + yield t + else: + R = A / (-B * B * B) ** 0.5 + t = b_3a + 2 * (-B) ** 0.5 * math.cos(math.acos(R) / 3) + if 0 <= t <= 1: + yield t + t = b_3a + 2 * (-B) ** 0.5 * math.cos((math.acos(R) + 2 * math.pi) / 3) + if 0 <= t <= 1: + yield t + t = b_3a + 2 * (-B) ** 0.5 * math.cos((math.acos(R) - 2 * math.pi) / 3) + if 0 <= t <= 1: + yield t + + +class HasAnimationData: + animation_data: bpy.types.AnimData + + +class VMDImporter: + def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False): + self.__vmdFile = vmd.File() + self.__vmdFile.load(filepath=filepath) + logging.debug(str(self.__vmdFile.header)) + self.__scale = scale + self.__convert_mmd_camera = convert_mmd_camera + self.__convert_mmd_lamp = convert_mmd_lamp + self.__bone_mapper = bone_mapper + self.__bone_util_cls = BoneConverterPoseMode if use_pose_mode else BoneConverter + self.__frame_margin = frame_margin + 1 + self.__mirror = use_mirror + self.__use_NLA = use_NLA + + @staticmethod + def __minRotationDiff(prev_q, curr_q): + t1 = (prev_q.w - curr_q.w) ** 2 + (prev_q.x - curr_q.x) ** 2 + (prev_q.y - curr_q.y) ** 2 + (prev_q.z - curr_q.z) ** 2 + t2 = (prev_q.w + curr_q.w) ** 2 + (prev_q.x + curr_q.x) ** 2 + (prev_q.y + curr_q.y) ** 2 + (prev_q.z + curr_q.z) ** 2 + # t1 = prev_q.rotation_difference(curr_q).angle + # t2 = prev_q.rotation_difference(-curr_q).angle + return -curr_q if t2 < t1 else curr_q + + @staticmethod + def __setInterpolation(bezier, kp0, kp1): + if bezier[0] == bezier[1] and bezier[2] == bezier[3]: + kp0.interpolation = "LINEAR" + else: + kp0.interpolation = "BEZIER" + kp0.handle_right_type = "FREE" + kp1.handle_left_type = "FREE" + d = (kp1.co - kp0.co) / 127.0 + kp0.handle_right = kp0.co + Vector((d.x * bezier[0], d.y * bezier[1])) + kp1.handle_left = kp0.co + Vector((d.x * bezier[2], d.y * bezier[3])) + + @staticmethod + def __fixFcurveHandles(fcurve): + kp0 = fcurve.keyframe_points[0] + kp0.handle_left_type = "FREE" + kp0.handle_left = kp0.co + Vector((-1, 0)) + kp = fcurve.keyframe_points[-1] + kp.handle_right_type = "FREE" + kp.handle_right = kp.co + Vector((1, 0)) + + @staticmethod + def __keyframe_insert_inner(fcurves: bpy.types.ActionFCurves, path: str, index: int, frame: float, value: float): + fcurve = fcurves.find(path, index=index) + if fcurve is None: + fcurve = fcurves.new(path, index=index) + fcurve.keyframe_points.insert(frame, value, options={"FAST"}) + + @staticmethod + def __keyframe_insert(fcurves: bpy.types.ActionFCurves, path: str, frame: float, value: Union[int, float, Vector]): + if isinstance(value, (int, float)): + VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value) + + elif isinstance(value, Vector): + VMDImporter.__keyframe_insert_inner(fcurves, path, 0, frame, value[0]) + VMDImporter.__keyframe_insert_inner(fcurves, path, 1, frame, value[1]) + VMDImporter.__keyframe_insert_inner(fcurves, path, 2, frame, value[2]) + + else: + raise TypeError("Unsupported type: {0}".format(type(value))) + + def __getBoneConverter(self, bone): + converter = self.__bone_util_cls(bone, self.__scale) + mode = bone.rotation_mode + compatible_quaternion = self.__minRotationDiff + + class _ConverterWrap: + convert_location = converter.convert_location + convert_interpolation = converter.convert_interpolation + if mode == "QUATERNION": + convert_rotation = converter.convert_rotation + compatible_rotation = compatible_quaternion + elif mode == "AXIS_ANGLE": + + @staticmethod + def convert_rotation(rot): + (x, y, z), angle = converter.convert_rotation(rot).to_axis_angle() + return (angle, x, y, z) + + @staticmethod + def compatible_rotation(prev, curr): + angle, x, y, z = curr + if prev[1] * x + prev[2] * y + prev[3] * z < 0: + angle, x, y, z = -angle, -x, -y, -z + angle_diff = prev[0] - angle + if abs(angle_diff) > math.pi: + pi_2 = math.pi * 2 + bias = -0.5 if angle_diff < 0 else 0.5 + angle += int(bias + angle_diff / pi_2) * pi_2 + return (angle, x, y, z) + + else: + convert_rotation = lambda rot: converter.convert_rotation(rot).to_euler(mode) + compatible_rotation = lambda prev, curr: curr.make_compatible(prev) or curr + + return _ConverterWrap + + def __assign_action(self, target: Union[bpy.types.ID, HasAnimationData], action: bpy.types.Action): + if target.animation_data is None: + target.animation_data_create() + + if not self.__use_NLA: + target.animation_data.action = action + else: + frame_current = bpy.context.scene.frame_current + target_track: bpy.types.NlaTrack = target.animation_data.nla_tracks.new() + target_track.name = action.name + target_strip = target_track.strips.new(action.name, frame_current, action) + target_strip.blend_type = "COMBINE" + + def __assignToArmature(self, armObj, action_name=None): + boneAnim = self.__vmdFile.boneAnimation + logging.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name) + if len(boneAnim) < 1: + return + + action_name = action_name or armObj.name + action = bpy.data.actions.new(name=action_name) + + extra_frame = 1 if self.__frame_margin > 1 else 0 + + pose_bones = armObj.pose.bones + if self.__bone_mapper: + pose_bones = self.__bone_mapper(armObj) + + _loc = _rot = lambda i: i + if self.__mirror: + pose_bones = _MirrorMapper(pose_bones) + _loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation + + class _Dummy: + pass + + dummy_keyframe_points = iter(lambda: _Dummy, None) + prop_rot_map = {"QUATERNION": "rotation_quaternion", "AXIS_ANGLE": "rotation_axis_angle"} + + bone_name_table = {} + for name, keyFrames in boneAnim.items(): + num_frame = len(keyFrames) + if num_frame < 1: + continue + bone = pose_bones.get(name, None) + if bone is None: + logging.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames)) + continue + logging.info("(bone) frames:%5d name: %s", len(keyFrames), name) + assert bone_name_table.get(bone.name, name) == name + bone_name_table[bone.name] = name + + fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3) + data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler") + bone_rotation = getattr(bone, data_path_rot) + default_values = list(bone.location) + list(bone_rotation) + data_path = 'pose.bones["%s"].location' % bone.name + for axis_i in range(3): + fcurves[axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) + data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot) + for axis_i in range(len(bone_rotation)): + fcurves[3 + axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) + + for i in range(len(default_values)): + c = fcurves[i] + c.keyframe_points.add(extra_frame + num_frame) + kp_iter = iter(c.keyframe_points) + if extra_frame: + kp = next(kp_iter) + kp.co = (1, default_values[i]) + kp.interpolation = "LINEAR" + fcurves[i] = kp_iter + + converter = self.__getBoneConverter(bone) + prev_rot = bone_rotation if extra_frame else None + prev_kps, indices = None, tuple(converter.convert_interpolation((0, 16, 32))) + (48,) * len(bone_rotation) + keyFrames.sort(key=lambda x: x.frame_number) + for k, x, y, z, r0, r1, r2, r3 in zip(keyFrames, *fcurves): + frame = k.frame_number + self.__frame_margin + loc = converter.convert_location(_loc(k.location)) + curr_rot = converter.convert_rotation(_rot(k.rotation)) + if prev_rot is not None: + curr_rot = converter.compatible_rotation(prev_rot, curr_rot) + # FIXME the rotation interpolation has slightly different result + # Blender: rot(x) = prev_rot*(1 - bezier(t)) + curr_rot*bezier(t) + # MMD: rot(x) = prev_rot.slerp(curr_rot, factor=bezier(t)) + prev_rot = curr_rot + + x.co = (frame, loc[0]) + y.co = (frame, loc[1]) + z.co = (frame, loc[2]) + r0.co = (frame, curr_rot[0]) + r1.co = (frame, curr_rot[1]) + r2.co = (frame, curr_rot[2]) + r3.co = (frame, curr_rot[-1]) + + curr_kps = (x, y, z, r0, r1, r2, r3) + if prev_kps is not None: + interp = k.interp + for idx, prev_kp, kp in zip(indices, prev_kps, curr_kps): + self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp) + prev_kps = curr_kps + + for c in action.fcurves: + self.__fixFcurveHandles(c) + + # property animation + propertyAnim = self.__vmdFile.propertyAnimation + if len(propertyAnim) > 0: + logging.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name) + for keyFrame in propertyAnim: + logging.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states) + frame = keyFrame.frame_number + self.__frame_margin + for ikName, enable in keyFrame.ik_states: + bone = pose_bones.get(ikName, None) + if not bone: + continue + + self.__keyframe_insert(action.fcurves, f'pose.bones["{bone.name}"].mmd_ik_toggle', frame, enable) + + self.__assign_action(armObj, action) + + # Ensure IK toggle state is set based on the first frame of VMD animation + if len(propertyAnim) > 0: + # Collect IK states from the first frame + first_frame_ik_states = {} + first_frame = float('inf') + for keyFrame in propertyAnim: + frame_num = keyFrame.frame_number + if frame_num < first_frame: + first_frame = frame_num + for ikName, enable in keyFrame.ik_states: + first_frame_ik_states[ikName] = enable + elif frame_num == first_frame: + for ikName, enable in keyFrame.ik_states: + if ikName not in first_frame_ik_states: + first_frame_ik_states[ikName] = enable + # Set the mmd_ik_toggle property for each bone based on the collected first frame IK states + for ikName, enable in first_frame_ik_states.items(): + bone = pose_bones.get(ikName, None) + if bone and bone.mmd_ik_toggle != enable: + bone.mmd_ik_toggle = enable # This will trigger the _pose_bone_update_mmd_ik_toggle method + + def __assignToMesh(self, meshObj, action_name=None): + shapeKeyAnim = self.__vmdFile.shapeKeyAnimation + logging.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name) + if len(shapeKeyAnim) < 1: + return + + action_name = action_name or meshObj.name + action = bpy.data.actions.new(name=action_name) + + mirror_map = _MirrorMapper(meshObj.data.shape_keys.key_blocks) if self.__mirror else {} + shapeKeyDict = {k: mirror_map.get(k, v) for k, v in meshObj.data.shape_keys.key_blocks.items()} + + from math import ceil, floor + + for name, keyFrames in shapeKeyAnim.items(): + if name not in shapeKeyDict: + logging.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames)) + continue + logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name) + shapeKey = shapeKeyDict[name] + fcurve = action.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name) + fcurve.keyframe_points.add(len(keyFrames)) + keyFrames.sort(key=lambda x: x.frame_number) + for k, v in zip(keyFrames, fcurve.keyframe_points): + v.co = (k.frame_number + self.__frame_margin, k.weight) + v.interpolation = "LINEAR" + weights = tuple(i.weight for i in keyFrames) + shapeKey.slider_min = min(shapeKey.slider_min, floor(min(weights))) + shapeKey.slider_max = max(shapeKey.slider_max, ceil(max(weights))) + + self.__assign_action(meshObj.data.shape_keys, action) + + def __assignToRoot(self, rootObj, action_name=None): + propertyAnim = self.__vmdFile.propertyAnimation + logging.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name) + if len(propertyAnim) < 1: + return + + action_name = action_name or rootObj.name + action = bpy.data.actions.new(name=action_name) + + logging.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim]) + for keyFrame in propertyAnim: + self.__keyframe_insert(action.fcurves, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible)) + + self.__assign_action(rootObj, action) + + @staticmethod + def detectCameraChange(fcurve, threshold=10.0): + frames = list(fcurve.keyframe_points) + frameCount = len(frames) + frames.sort(key=lambda x: x.co[0]) + for i, f in enumerate(frames): + if i + 1 < frameCount: + n = frames[i + 1] + if n.co[0] - f.co[0] <= 1.0 and abs(f.co[1] - n.co[1]) > threshold: + f.interpolation = "CONSTANT" + + def __assignToCamera(self, cameraObj, action_name=None): + mmdCameraInstance = MMDCamera.convertToMMDCamera(cameraObj, self.__scale) + mmdCamera = mmdCameraInstance.object() + cameraObj = mmdCameraInstance.camera() + + cameraAnim = self.__vmdFile.cameraAnimation + logging.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name) + if len(cameraAnim) < 1: + return + + action_name = action_name or mmdCamera.name + parent_action = bpy.data.actions.new(name=action_name) + distance_action = bpy.data.actions.new(name=action_name + "_dis") + + _loc = _rot = lambda i: i + if self.__mirror: + _loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation3 + + fcurves = [] + for i in range(3): + fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z + for i in range(3): + fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz + fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov + fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp + fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis + for c in fcurves: + c.keyframe_points.add(len(cameraAnim)) + + prev_kps, indices = None, (0, 8, 4, 12, 12, 12, 16, 20) # x, z, y, rx, ry, rz, dis, fov + cameraAnim.sort(key=lambda x: x.frame_number) + for k, x, y, z, rx, ry, rz, fov, persp, dis in zip(cameraAnim, *(c.keyframe_points for c in fcurves)): + frame = k.frame_number + self.__frame_margin + x.co, z.co, y.co = ((frame, val * self.__scale) for val in _loc(k.location)) + rx.co, rz.co, ry.co = ((frame, val) for val in _rot(k.rotation)) + fov.co = (frame, math.radians(k.angle)) + dis.co = (frame, k.distance * self.__scale) + persp.co = (frame, k.persp) + + persp.interpolation = "CONSTANT" + curr_kps = (x, y, z, rx, ry, rz, dis, fov) + if prev_kps is not None: + interp = k.interp + for idx, prev_kp, kp in zip(indices, prev_kps, curr_kps): + self.__setInterpolation(interp[idx : idx + 4 : 2] + interp[idx + 1 : idx + 4 : 2], prev_kp, kp) + prev_kps = curr_kps + + for fcurve in fcurves: + self.__fixFcurveHandles(fcurve) + if fcurve.data_path == "rotation_euler": + self.detectCameraChange(fcurve) + + self.__assign_action(mmdCamera, parent_action) + self.__assign_action(cameraObj, distance_action) + + @staticmethod + def detectLampChange(fcurve, threshold=0.1): + frames = list(fcurve.keyframe_points) + frameCount = len(frames) + frames.sort(key=lambda x: x.co[0]) + for i, f in enumerate(frames): + f.interpolation = "LINEAR" + if i + 1 < frameCount: + n = frames[i + 1] + if n.co[0] - f.co[0] <= 1.0 and abs(f.co[1] - n.co[1]) > threshold: + f.interpolation = "CONSTANT" + + def __assignToLamp(self, lampObj, action_name=None): + mmdLampInstance = MMDLamp.convertToMMDLamp(lampObj, self.__scale) + mmdLamp = mmdLampInstance.object() + lampObj = mmdLampInstance.lamp() + + lampAnim = self.__vmdFile.lampAnimation + logging.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name) + if len(lampAnim) < 1: + return + + action_name = action_name or mmdLamp.name + color_action = bpy.data.actions.new(name=action_name + "_color") + location_action = bpy.data.actions.new(name=action_name + "_loc") + + _loc = _MirrorMapper.get_location if self.__mirror else lambda i: i + for keyFrame in lampAnim: + frame = keyFrame.frame_number + self.__frame_margin + self.__keyframe_insert(color_action.fcurves, "color", frame, Vector(keyFrame.color)) + self.__keyframe_insert(location_action.fcurves, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1) + + for fcurve in location_action.fcurves: + self.detectLampChange(fcurve) + + self.__assign_action(lampObj.data, color_action) + self.__assign_action(lampObj, location_action) + + def assign(self, obj, action_name=None): + if obj is None: + return + if action_name is None: + action_name = os.path.splitext(os.path.basename(self.__vmdFile.filepath))[0] + + if MMDCamera.isMMDCamera(obj): + self.__assignToCamera(obj, action_name + "_camera") + elif MMDLamp.isMMDLamp(obj): + self.__assignToLamp(obj, action_name + "_lamp") + elif getattr(obj.data, "shape_keys", None): + self.__assignToMesh(obj, action_name + "_facial") + elif obj.type == "ARMATURE": + self.__assignToArmature(obj, action_name + "_bone") + elif obj.type == "CAMERA" and self.__convert_mmd_camera: + self.__assignToCamera(obj, action_name + "_camera") + elif obj.type == "LAMP" and self.__convert_mmd_lamp: + self.__assignToLamp(obj, action_name + "_lamp") + elif obj.mmd_type == "ROOT": + self.__assignToRoot(obj, action_name + "_display") + else: + pass diff --git a/core/mmd/cycles_converter.py b/core/mmd/cycles_converter.py new file mode 100644 index 0000000..2a8e531 --- /dev/null +++ b/core/mmd/cycles_converter.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +from typing import Iterable, Optional + +import bpy + +from .core.shader import _NodeGroupUtils +from .core.material import FnMaterial + + +def __switchToCyclesRenderEngine(): + if bpy.context.scene.render.engine != "CYCLES": + bpy.context.scene.render.engine = "CYCLES" + + +def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader): + _NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value) + + +def __exposeNodeTreeOutput(out_socket, name, node_output, shader): + _NodeGroupUtils(shader).new_output_socket(name, out_socket) + + +def __getMaterialOutput(nodes, bl_idname): + o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname) + o.is_active_output = True + return o + + +def create_MMDAlphaShader(): + __switchToCyclesRenderEngine() + + if "MMDAlphaShader" in bpy.data.node_groups: + return bpy.data.node_groups["MMDAlphaShader"] + + shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree") + + node_input = shader.nodes.new("NodeGroupInput") + node_output = shader.nodes.new("NodeGroupOutput") + node_output.location.x += 250 + node_input.location.x -= 500 + + trans = shader.nodes.new("ShaderNodeBsdfTransparent") + trans.location.x -= 250 + trans.location.y += 150 + mix = shader.nodes.new("ShaderNodeMixShader") + + shader.links.new(mix.inputs[1], trans.outputs["BSDF"]) + + __exposeNodeTreeInput(mix.inputs[2], "Shader", None, node_input, shader) + __exposeNodeTreeInput(mix.inputs["Fac"], "Alpha", 1.0, node_input, shader) + __exposeNodeTreeOutput(mix.outputs["Shader"], "Shader", node_output, shader) + + return shader + + +def create_MMDBasicShader(): + __switchToCyclesRenderEngine() + + if "MMDBasicShader" in bpy.data.node_groups: + return bpy.data.node_groups["MMDBasicShader"] + + shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree") + + node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput") + node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput") + node_output.location.x += 250 + node_input.location.x -= 500 + + dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse") + dif.location.x -= 250 + dif.location.y += 150 + glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic") + glo.location.x -= 250 + glo.location.y -= 150 + mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader") + shader.links.new(mix.inputs[1], dif.outputs["BSDF"]) + shader.links.new(mix.inputs[2], glo.outputs["BSDF"]) + + __exposeNodeTreeInput(dif.inputs["Color"], "diffuse", [1.0, 1.0, 1.0, 1.0], node_input, shader) + __exposeNodeTreeInput(glo.inputs["Color"], "glossy", [1.0, 1.0, 1.0, 1.0], node_input, shader) + __exposeNodeTreeInput(glo.inputs["Roughness"], "glossy_rough", 0.0, node_input, shader) + __exposeNodeTreeInput(mix.inputs["Fac"], "reflection", 0.02, node_input, shader) + __exposeNodeTreeOutput(mix.outputs["Shader"], "shader", node_output, shader) + + return shader + + +def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]: + yield node + if node.parent: + yield node.parent + for n in set(l.from_node for i in node.inputs for l in i.links): + yield from __enum_linked_nodes(n) + + +def __cleanNodeTree(material: bpy.types.Material): + nodes = material.node_tree.nodes + node_names = set(n.name for n in nodes) + for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}): + if any(i.is_linked for i in o.inputs): + node_names -= set(linked.name for linked in __enum_linked_nodes(o)) + for name in node_names: + nodes.remove(nodes[name]) + + +def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): + __switchToCyclesRenderEngine() + convertToBlenderShader(obj, use_principled, clean_nodes, subsurface) + + +def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): + for i in obj.material_slots: + if not i.material: + continue + if not i.material.use_nodes: + i.material.use_nodes = True + __convertToMMDBasicShader(i.material) + if use_principled: + __convertToPrincipledBsdf(i.material, subsurface) + if clean_nodes: + __cleanNodeTree(i.material) + +def convertToMMDShader(obj): + """BSDF -> MMDShaderDev conversion.""" + for i in obj.material_slots: + if not i.material: + continue + if not i.material.use_nodes: + i.material.use_nodes = True + FnMaterial.convert_to_mmd_material(i.material) + +def __convertToMMDBasicShader(material: bpy.types.Material): + # TODO: test me + mmd_basic_shader_grp = create_MMDBasicShader() + mmd_alpha_shader_grp = create_MMDAlphaShader() + + if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)): + # Add nodes for Cycles Render + shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") + shader.node_tree = mmd_basic_shader_grp + shader.inputs[0].default_value[:3] = material.diffuse_color[:3] + shader.inputs[1].default_value[:3] = material.specular_color[:3] + shader.inputs["glossy_rough"].default_value = 1.0 / getattr(material, "specular_hardness", 50) + outplug = shader.outputs[0] + + location = shader.location.copy() + location.x -= 1000 + + alpha_value = 1.0 + if len(material.diffuse_color) > 3: + alpha_value = material.diffuse_color[3] + + if alpha_value < 1.0: + alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") + alpha_shader.location.x = shader.location.x + 250 + alpha_shader.location.y = shader.location.y - 150 + alpha_shader.node_tree = mmd_alpha_shader_grp + alpha_shader.inputs[1].default_value = alpha_value + material.node_tree.links.new(alpha_shader.inputs[0], outplug) + outplug = alpha_shader.outputs[0] + + material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial") + material.node_tree.links.new(material_output.inputs["Surface"], outplug) + material_output.location.x = shader.location.x + 500 + material_output.location.y = shader.location.y - 150 + + +def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float): + node_names = set() + for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)): + if s.node_tree.name == "MMDBasicShader": + l: bpy.types.NodeLink + for l in s.outputs[0].links: + to_node = l.to_node + # assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader + if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader": + __switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node) + node_names.add(to_node.name) + else: + __switchToPrincipledBsdf(material.node_tree, s, subsurface) + node_names.add(s.name) + elif s.node_tree.name == "MMDShaderDev": + __switchToPrincipledBsdf(material.node_tree, s, subsurface) + node_names.add(s.name) + # remove MMD shader nodes + nodes = material.node_tree.nodes + for name in node_names: + nodes.remove(nodes[name]) + + +def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None): + shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled") + shader.parent = node_basic.parent + shader.location.x = node_basic.location.x + shader.location.y = node_basic.location.y + + alpha_socket_name = "Alpha" + if node_basic.node_tree.name == "MMDShaderDev": + node_alpha, alpha_socket_name = node_basic, "Base Alpha" + if "Base Tex" in node_basic.inputs and node_basic.inputs["Base Tex"].is_linked: + node_tree.links.new(node_basic.inputs["Base Tex"].links[0].from_socket, shader.inputs["Base Color"]) + elif "Diffuse Color" in node_basic.inputs: + shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["Diffuse Color"].default_value[:3] + elif "diffuse" in node_basic.inputs: + shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["diffuse"].default_value[:3] + if node_basic.inputs["diffuse"].is_linked: + node_tree.links.new(node_basic.inputs["diffuse"].links[0].from_socket, shader.inputs["Base Color"]) + + shader.inputs["IOR"].default_value = 1.0 + shader.inputs["Subsurface Weight"].default_value = subsurface + + output_links = node_basic.outputs[0].links + if node_alpha: + output_links = node_alpha.outputs[0].links + shader.parent = node_alpha.parent or shader.parent + shader.location.x = node_alpha.location.x + + if alpha_socket_name in node_alpha.inputs: + if "Alpha" in shader.inputs: + shader.inputs["Alpha"].default_value = node_alpha.inputs[alpha_socket_name].default_value + if node_alpha.inputs[alpha_socket_name].is_linked: + node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"]) + else: + shader.inputs["Transmission"].default_value = 1 - node_alpha.inputs[alpha_socket_name].default_value + if node_alpha.inputs[alpha_socket_name].is_linked: + node_invert = node_tree.nodes.new("ShaderNodeMath") + node_invert.parent = shader.parent + node_invert.location.x = node_alpha.location.x - 250 + node_invert.location.y = node_alpha.location.y - 300 + node_invert.operation = "SUBTRACT" + node_invert.use_clamp = True + node_invert.inputs[0].default_value = 1 + node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1]) + node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"]) + + for l in output_links: + node_tree.links.new(shader.outputs[0], l.to_socket) diff --git a/core/mmd/operators/__init__.py b/core/mmd/operators/__init__.py new file mode 100644 index 0000000..f3342f2 --- /dev/null +++ b/core/mmd/operators/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. \ No newline at end of file diff --git a/core/mmd/operators/material.py b/core/mmd/operators/material.py new file mode 100644 index 0000000..23f2d49 --- /dev/null +++ b/core/mmd/operators/material.py @@ -0,0 +1,406 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import bpy +from bpy.props import BoolProperty, StringProperty +from bpy.types import Operator + +from .. import cycles_converter +from ..core.exceptions import MaterialNotFoundError +from ..core.material import FnMaterial +from ..core.shader import _NodeGroupUtils + + +class ConvertMaterialsForCycles(Operator): + bl_idname = "mmd_tools.convert_materials_for_cycles" + bl_label = "Convert Materials For Cycles" + bl_description = "Convert materials of selected objects for Cycles." + bl_options = {"REGISTER", "UNDO"} + + use_principled: bpy.props.BoolProperty( + name="Convert to Principled BSDF", + description="Convert MMD shader nodes to Principled BSDF as well if enabled", + default=False, + options={"SKIP_SAVE"}, + ) + + clean_nodes: bpy.props.BoolProperty( + name="Clean Nodes", + description="Remove redundant nodes as well if enabled. Disable it to keep node data.", + default=False, + options={"SKIP_SAVE"}, + ) + + @classmethod + def poll(cls, context): + return next((x for x in context.selected_objects if x.type == "MESH"), None) + + def draw(self, context): + layout = self.layout + layout.prop(self, "use_principled") + layout.prop(self, "clean_nodes") + + def execute(self, context): + try: + context.scene.render.engine = "CYCLES" + except: + self.report({"ERROR"}, " * Failed to change to Cycles render engine.") + return {"CANCELLED"} + for obj in (x for x in context.selected_objects if x.type == "MESH"): + cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes) + return {"FINISHED"} + + +class ConvertMaterials(Operator): + bl_idname = "mmd_tools.convert_materials" + bl_label = "Convert Materials" + bl_description = "Convert materials of selected objects." + bl_options = {"REGISTER", "UNDO"} + + use_principled: bpy.props.BoolProperty( + name="Convert to Principled BSDF", + description="Convert MMD shader nodes to Principled BSDF as well if enabled", + default=True, + options={"SKIP_SAVE"}, + ) + + clean_nodes: bpy.props.BoolProperty( + name="Clean Nodes", + description="Remove redundant nodes as well if enabled. Disable it to keep node data.", + default=True, + options={"SKIP_SAVE"}, + ) + + subsurface: bpy.props.FloatProperty( + name="Subsurface", + default=0.001, + soft_min=0.000, + soft_max=1.000, + precision=3, + options={"SKIP_SAVE"}, + ) + + @classmethod + def poll(cls, context): + return next((x for x in context.selected_objects if x.type == "MESH"), None) + + def execute(self, context): + for obj in context.selected_objects: + if obj.type != "MESH": + continue + cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface) + return {"FINISHED"} + +class ConvertBSDFMaterials(Operator): + bl_idname = 'mmd_tools.convert_bsdf_materials' + bl_label = 'Convert Blender Materials' + bl_description = 'Convert materials of selected objects.' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return next((x for x in context.selected_objects if x.type == 'MESH'), None) + + def execute(self, context): + for obj in context.selected_objects: + if obj.type != 'MESH': + continue + cycles_converter.convertToMMDShader(obj) + return {'FINISHED'} + +class _OpenTextureBase: + """Create a texture for mmd model material.""" + + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + filepath: StringProperty( + name="File Path", + description="Filepath used for importing the file", + maxlen=1024, + subtype="FILE_PATH", + ) + + use_filter_image: BoolProperty( + default=True, + options={"HIDDEN"}, + ) + + def invoke(self, context, event): + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + +class OpenTexture(Operator, _OpenTextureBase): + bl_idname = "mmd_tools.material_open_texture" + bl_label = "Open Texture" + bl_description = "Create main texture of active material" + + def execute(self, context): + mat = context.active_object.active_material + fnMat = FnMaterial(mat) + fnMat.create_texture(self.filepath) + return {"FINISHED"} + + +class RemoveTexture(Operator): + """Create a texture for mmd model material.""" + + bl_idname = "mmd_tools.material_remove_texture" + bl_label = "Remove Texture" + bl_description = "Remove main texture of active material" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + mat = context.active_object.active_material + fnMat = FnMaterial(mat) + fnMat.remove_texture() + return {"FINISHED"} + + +class OpenSphereTextureSlot(Operator, _OpenTextureBase): + """Create a texture for mmd model material.""" + + bl_idname = "mmd_tools.material_open_sphere_texture" + bl_label = "Open Sphere Texture" + bl_description = "Create sphere texture of active material" + + def execute(self, context): + mat = context.active_object.active_material + fnMat = FnMaterial(mat) + fnMat.create_sphere_texture(self.filepath, context.active_object) + return {"FINISHED"} + + +class RemoveSphereTexture(Operator): + """Create a texture for mmd model material.""" + + bl_idname = "mmd_tools.material_remove_sphere_texture" + bl_label = "Remove Sphere Texture" + bl_description = "Remove sphere texture of active material" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + mat = context.active_object.active_material + fnMat = FnMaterial(mat) + fnMat.remove_sphere_texture() + return {"FINISHED"} + + +class MoveMaterialUp(Operator): + bl_idname = "mmd_tools.move_material_up" + bl_label = "Move Material Up" + bl_description = "Moves selected material one slot up" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + obj = context.active_object + valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" + return valid_mesh and obj.active_material_index > 0 + + def execute(self, context): + obj = context.active_object + current_idx = obj.active_material_index + prev_index = current_idx - 1 + try: + FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True) + except MaterialNotFoundError: + self.report({"ERROR"}, "Materials not found") + return {"CANCELLED"} + obj.active_material_index = prev_index + + return {"FINISHED"} + + +class MoveMaterialDown(Operator): + bl_idname = "mmd_tools.move_material_down" + bl_label = "Move Material Down" + bl_description = "Moves the selected material one slot down" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + obj = context.active_object + valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" + return valid_mesh and obj.active_material_index < len(obj.material_slots) - 1 + + def execute(self, context): + obj = context.active_object + current_idx = obj.active_material_index + next_index = current_idx + 1 + try: + FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True) + except MaterialNotFoundError: + self.report({"ERROR"}, "Materials not found") + return {"CANCELLED"} + obj.active_material_index = next_index + return {"FINISHED"} + + +class EdgePreviewSetup(Operator): + bl_idname = "mmd_tools.edge_preview_setup" + bl_label = "Edge Preview Setup" + bl_description = 'Preview toon edge settings of active model using "Solidify" modifier' + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + action: bpy.props.EnumProperty( + name="Action", + description="Select action", + items=[ + ("CREATE", "Create", "Create toon edge", 0), + ("CLEAN", "Clean", "Clear toon edge", 1), + ], + default="CREATE", + ) + + def execute(self, context): + from ..core.model import FnModel + + root = FnModel.find_root_object(context.active_object) + if root is None: + self.report({"ERROR"}, "Select a MMD model") + return {"CANCELLED"} + + if self.action == "CLEAN": + for obj in FnModel.iterate_mesh_objects(root): + self.__clean_toon_edge(obj) + else: + from ..bpyutils import Props + + scale = 0.2 * getattr(root, Props.empty_display_size) + counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root)) + self.report({"INFO"}, "Created %d toon edge(s)" % counts) + return {"FINISHED"} + + def __clean_toon_edge(self, obj): + if "mmd_edge_preview" in obj.modifiers: + obj.modifiers.remove(obj.modifiers["mmd_edge_preview"]) + + if "mmd_edge_preview" in obj.vertex_groups: + obj.vertex_groups.remove(obj.vertex_groups["mmd_edge_preview"]) + + FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge.")) + + def __create_toon_edge(self, obj, scale=1.0): + self.__clean_toon_edge(obj) + materials = obj.data.materials + material_offset = len(materials) + for m in tuple(materials): + if m and m.mmd_material.enabled_toon_edge: + mat_edge = self.__get_edge_material("mmd_edge." + m.name, m.mmd_material.edge_color, materials) + materials.append(mat_edge) + elif material_offset > 1: + mat_edge = self.__get_edge_material("mmd_edge.disabled", (0, 0, 0, 0), materials) + materials.append(mat_edge) + if len(materials) > material_offset: + mod = obj.modifiers.get("mmd_edge_preview", None) + if mod is None: + mod = obj.modifiers.new("mmd_edge_preview", "SOLIDIFY") + mod.material_offset = material_offset + mod.thickness_vertex_group = 1e-3 # avoid overlapped faces + mod.use_flip_normals = True + mod.use_rim = False + mod.offset = 1 + self.__create_edge_preview_group(obj) + mod.thickness = scale + mod.vertex_group = "mmd_edge_preview" + return len(materials) - material_offset + + def __create_edge_preview_group(self, obj): + vertices, materials = obj.data.vertices, obj.data.materials + weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m} + scale_map = {} + vg_scale_index = obj.vertex_groups.find("mmd_edge_scale") + if vg_scale_index >= 0: + scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index} + vg_edge_preview = obj.vertex_groups.new(name="mmd_edge_preview") + for i, mi in {v: f.material_index for f in reversed(obj.data.polygons) for v in f.vertices}.items(): + weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02 + vg_edge_preview.add(index=[i], weight=weight, type="REPLACE") + + def __get_edge_material(self, mat_name, edge_color, materials): + if mat_name in materials: + return materials[mat_name] + mat = bpy.data.materials.get(mat_name, None) + if mat is None: + mat = bpy.data.materials.new(mat_name) + mmd_mat = mat.mmd_material + # note: edge affects ground shadow + mmd_mat.is_double_sided = mmd_mat.enabled_drop_shadow = False + mmd_mat.enabled_self_shadow_map = mmd_mat.enabled_self_shadow = False + # mmd_mat.enabled_self_shadow_map = True # for blender 2.78+ BI viewport only + mmd_mat.diffuse_color = mmd_mat.specular_color = (0, 0, 0) + mmd_mat.ambient_color = edge_color[:3] + mmd_mat.alpha = edge_color[3] + mmd_mat.edge_color = edge_color + self.__make_shader(mat) + return mat + + def __make_shader(self, m): + m.use_nodes = True + nodes, links = m.node_tree.nodes, m.node_tree.links + + node_shader = nodes.get("mmd_edge_preview", None) + if node_shader is None or not any(s.is_linked for s in node_shader.outputs): + XPOS, YPOS = 210, 110 + nodes.clear() + node_shader = nodes.new("ShaderNodeGroup") + node_shader.name = "mmd_edge_preview" + node_shader.location = (0, 0) + node_shader.width = 200 + node_shader.node_tree = self.__get_edge_preview_shader() + + node_out = nodes.new("ShaderNodeOutputMaterial") + node_out.location = (XPOS * 2, YPOS * 0) + links.new(node_shader.outputs["Shader"], node_out.inputs["Surface"]) + + node_shader.inputs["Color"].default_value = m.mmd_material.edge_color + node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3] + + def __get_edge_preview_shader(self): + group_name = "MMDEdgePreview" + shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") + if len(shader.nodes): + return shader + + ng = _NodeGroupUtils(shader) + + node_input = ng.new_node("NodeGroupInput", (-5, 0)) + node_output = ng.new_node("NodeGroupOutput", (3, 0)) + + ############################################################################ + node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5)) + node_color.mute = True + + ng.new_input_socket("Color", node_color.inputs["Color1"]) + + ############################################################################ + node_ray = ng.new_node("ShaderNodeLightPath", (-3, 1.5)) + node_geo = ng.new_node("ShaderNodeNewGeometry", (-3, 0)) + node_max = ng.new_math_node("MAXIMUM", (-2, 1.5)) + node_max.mute = True + node_gt = ng.new_math_node("GREATER_THAN", (-1, 1)) + node_alpha = ng.new_math_node("MULTIPLY", (0, 1)) + node_trans = ng.new_node("ShaderNodeBsdfTransparent", (0, 0)) + node_rgb = ng.new_node("ShaderNodeBackground", (0, -0.5)) + node_mix = ng.new_node("ShaderNodeMixShader", (1, 0.5)) + + links = ng.links + links.new(node_ray.outputs["Is Camera Ray"], node_max.inputs[0]) + links.new(node_ray.outputs["Is Glossy Ray"], node_max.inputs[1]) + links.new(node_max.outputs["Value"], node_gt.inputs[0]) + links.new(node_geo.outputs["Backfacing"], node_gt.inputs[1]) + links.new(node_gt.outputs["Value"], node_alpha.inputs[0]) + links.new(node_alpha.outputs["Value"], node_mix.inputs["Fac"]) + links.new(node_trans.outputs["BSDF"], node_mix.inputs[1]) + links.new(node_rgb.outputs[0], node_mix.inputs[2]) + links.new(node_color.outputs["Color"], node_rgb.inputs["Color"]) + + ng.new_input_socket("Alpha", node_alpha.inputs[1]) + ng.new_output_socket("Shader", node_mix.outputs["Shader"]) + + return shader diff --git a/core/mmd/operators/misc.py b/core/mmd/operators/misc.py new file mode 100644 index 0000000..c59815e --- /dev/null +++ b/core/mmd/operators/misc.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import re + +import bpy + +from .. import utils +from ..bpyutils import FnContext, FnObject +from ..core.bone import FnBone +from ..core.model import FnModel, Model +from ..core.morph import FnMorph + + +class SelectObject(bpy.types.Operator): + bl_idname = "mmd_tools.object_select" + bl_label = "Select Object" + bl_description = "Select the object" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + name: bpy.props.StringProperty( + name="Name", + description="The object name", + default="", + options={"HIDDEN", "SKIP_SAVE"}, + ) + + def execute(self, context): + utils.selectAObject(context.scene.objects[self.name]) + return {"FINISHED"} + + +class MoveObject(bpy.types.Operator, utils.ItemMoveOp): + bl_idname = "mmd_tools.object_move" + bl_label = "Move Object" + bl_description = "Move active object up/down in the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + __PREFIX_REGEXP = re.compile(r"(?P[0-9A-Z]{3}_)(?P.*)") + + @classmethod + def set_index(cls, obj, index): + m = cls.__PREFIX_REGEXP.match(obj.name) + name = m.group("name") if m else obj.name + obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name) + + @classmethod + def get_name(cls, obj, prefix=None): + m = cls.__PREFIX_REGEXP.match(obj.name) + name = m.group("name") if m else obj.name + return name[len(prefix) :] if prefix and name.startswith(prefix) else name + + @classmethod + def normalize_indices(cls, objects): + for i, x in enumerate(objects): + cls.set_index(x, i) + + @classmethod + def poll(cls, context): + return context.active_object + + def execute(self, context): + obj = context.active_object + objects = self.__get_objects(obj) + if obj not in objects: + self.report({"ERROR"}, 'Can not move object "%s"' % obj.name) + return {"CANCELLED"} + + objects.sort(key=lambda x: x.name) + self.move(objects, objects.index(obj), self.type) + self.normalize_indices(objects) + return {"FINISHED"} + + def __get_objects(self, obj): + class __MovableList(list): + def move(self, index_old, index_new): + item = self[index_old] + self.remove(item) + self.insert(index_new, item) + + objects = [] + root = FnModel.find_root_object(obj) + if root: + rig = Model(root) + if obj.mmd_type == "NONE" and obj.type == "MESH": + objects = rig.meshes() + elif obj.mmd_type == "RIGID_BODY": + objects = rig.rigidBodies() + elif obj.mmd_type == "JOINT": + objects = rig.joints() + return __MovableList(objects) + + +class CleanShapeKeys(bpy.types.Operator): + bl_idname = "mmd_tools.clean_shape_keys" + bl_label = "Clean Shape Keys" + bl_description = "Remove unused shape keys of selected mesh objects" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return any(o.type == "MESH" for o in context.selected_objects) + + @staticmethod + def __can_remove(key_block): + if key_block.relative_key == key_block: + return False # Basis + for v0, v1 in zip(key_block.relative_key.data, key_block.data): + if v0.co != v1.co: + return False + return True + + def __shape_key_clean(self, obj, key_blocks): + for kb in key_blocks: + if self.__can_remove(kb): + FnObject.mesh_remove_shape_key(obj, kb) + if len(key_blocks) == 1: + FnObject.mesh_remove_shape_key(obj, key_blocks[0]) + + def execute(self, context): + obj: bpy.types.Object + for obj in context.selected_objects: + if obj.type != "MESH" or obj.data.shape_keys is None: + continue + if not obj.data.shape_keys.use_relative: + continue # not be considered yet + self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks) + return {"FINISHED"} + + +class SeparateByMaterials(bpy.types.Operator): + bl_idname = "mmd_tools.separate_by_materials" + bl_label = "Separate By Materials" + bl_options = {"REGISTER", "UNDO"} + + clean_shape_keys: bpy.props.BoolProperty( + name="Clean Shape Keys", + description="Remove unused shape keys of separated objects", + default=True, + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return obj and obj.type == "MESH" + + def __separate_by_materials(self, obj): + utils.separateByMaterials(obj) + if self.clean_shape_keys: + bpy.ops.mmd_tools.clean_shape_keys() + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + if root is None: + self.__separate_by_materials(obj) + else: + bpy.ops.mmd_tools.clear_temp_materials() + bpy.ops.mmd_tools.clear_uv_morph_view() + + # Store the current material names + rig = Model(root) + mat_names = [getattr(mat, "name", None) for mat in rig.materials()] + self.__separate_by_materials(obj) + for mesh in rig.meshes(): + FnMorph.clean_uv_morph_vertex_groups(mesh) + if len(mesh.data.materials) > 0: + mat = mesh.data.materials[0] + idx = mat_names.index(getattr(mat, "name", None)) + MoveObject.set_index(mesh, idx) + + for morph in root.mmd_root.material_morphs: + FnMorph(morph, rig).update_mat_related_mesh() + utils.clearUnusedMeshes() + return {"FINISHED"} + + +class JoinMeshes(bpy.types.Operator): + bl_idname = "mmd_tools.join_meshes" + bl_label = "Join Meshes" + bl_description = "Join the Model meshes into a single one" + bl_options = {"REGISTER", "UNDO"} + + sort_shape_keys: bpy.props.BoolProperty( + name="Sort Shape Keys", + description="Sort shape keys in the order of vertex morph", + default=True, + ) + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + if root is None: + self.report({"ERROR"}, "Select a MMD model") + return {"CANCELLED"} + + bpy.ops.mmd_tools.clear_temp_materials() + bpy.ops.mmd_tools.clear_uv_morph_view() + + # Find all the meshes in mmd_root + rig = Model(root) + meshes_list = sorted(rig.meshes(), key=lambda x: x.name) + if not meshes_list: + self.report({"ERROR"}, "The model does not have any meshes") + return {"CANCELLED"} + active_mesh = meshes_list[0] + + FnContext.select_objects(context, *meshes_list) + FnContext.set_active_object(context, active_mesh) + + # Store the current order of the materials + for m in meshes_list[1:]: + for mat in m.data.materials: + if mat not in active_mesh.data.materials[:]: + active_mesh.data.materials.append(mat) + + # Join selected meshes + bpy.ops.object.join() + + if self.sort_shape_keys: + FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys()) + active_mesh.active_shape_key_index = 0 + for morph in root.mmd_root.material_morphs: + FnMorph(morph, rig).update_mat_related_mesh(active_mesh) + utils.clearUnusedMeshes() + return {"FINISHED"} + + +class AttachMeshesToMMD(bpy.types.Operator): + bl_idname = "mmd_tools.attach_meshes" + bl_label = "Attach Meshes to Model" + bl_description = "Finds existing meshes and attaches them to the selected MMD model" + bl_options = {"REGISTER", "UNDO"} + + add_armature_modifier: bpy.props.BoolProperty(default=True) + + def execute(self, context: bpy.types.Context): + root = FnModel.find_root_object(context.active_object) + if root is None: + self.report({"ERROR"}, "Select a MMD model") + return {"CANCELLED"} + + armObj = FnModel.find_armature_object(root) + if armObj is None: + self.report({"ERROR"}, "Model Armature not found") + return {"CANCELLED"} + + FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier) + return {"FINISHED"} + + +class ChangeMMDIKLoopFactor(bpy.types.Operator): + bl_idname = "mmd_tools.change_mmd_ik_loop_factor" + bl_label = "Change MMD IK Loop Factor" + bl_description = "Multiplier for all bones' IK iterations in Blender" + bl_options = {"REGISTER", "UNDO"} + + mmd_ik_loop_factor: bpy.props.IntProperty( + name="MMD IK Loop Factor", + description="Scaling factor of MMD IK loop", + min=1, + soft_max=10, + max=100, + ) + + @classmethod + def poll(cls, context): + return FnModel.find_root_object(context.active_object) is not None + + def invoke(self, context, event): + root_object = FnModel.find_root_object(context.active_object) + self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor + vm = context.window_manager + return vm.invoke_props_dialog(self) + + def execute(self, context): + root_object = FnModel.find_root_object(context.active_object) + FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor) + return {"FINISHED"} + + +class RecalculateBoneRoll(bpy.types.Operator): + bl_idname = "mmd_tools.recalculate_bone_roll" + bl_label = "Recalculate bone roll" + bl_description = "Recalculate bone roll for arm related bones" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + obj = context.active_object + return obj and obj.type == "ARMATURE" + + def invoke(self, context, event): + vm = context.window_manager + return vm.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + c = layout.column() + c.label(text="This operation will break existing f-curve/action.", icon="QUESTION") + c.label(text="Click [OK] to run the operation.") + + def execute(self, context): + arm = context.active_object + FnBone.apply_auto_bone_roll(arm) + return {"FINISHED"} diff --git a/core/mmd/operators/model.py b/core/mmd/operators/model.py new file mode 100644 index 0000000..16fe3ba --- /dev/null +++ b/core/mmd/operators/model.py @@ -0,0 +1,486 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import bpy + +from ..bpyutils import FnContext +from ..core.bone import FnBone, MigrationFnBone +from ..core.model import FnModel, Model + + +class MorphSliderSetup(bpy.types.Operator): + bl_idname = "mmd_tools.morph_slider_setup" + bl_label = "Morph Slider Setup" + bl_description = "Translate MMD morphs of selected object into format usable by Blender" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + type: bpy.props.EnumProperty( + name="Type", + description="Select type", + items=[ + ("CREATE", "Create", "Create placeholder object for morph sliders", "SHAPEKEY_DATA", 0), + ("BIND", "Bind", "Bind morph sliders", "DRIVER", 1), + ("UNBIND", "Unbind", "Unbind morph sliders", "X", 2), + ], + default="CREATE", + ) + + def execute(self, context: bpy.types.Context): + active_object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + + with FnContext.temp_override_active_layer_collection(context, root_object): + rig = Model(root_object) + if self.type == "BIND": + rig.morph_slider.bind() + elif self.type == "UNBIND": + rig.morph_slider.unbind() + else: + rig.morph_slider.create() + FnContext.set_active_object(context, active_object) + + return {"FINISHED"} + + +class CleanRiggingObjects(bpy.types.Operator): + bl_idname = "mmd_tools.clean_rig" + bl_label = "Clean Rig" + bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + root_object = FnModel.find_root_object(context.active_object) + assert root_object is not None + + rig = Model(root_object) + rig.clean() + FnContext.set_active_object(context, root_object) + return {"FINISHED"} + + +class BuildRig(bpy.types.Operator): + bl_idname = "mmd_tools.build_rig" + bl_label = "Build Rig" + bl_description = "Translate physics of selected object into format usable by Blender" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + non_collision_distance_scale: bpy.props.FloatProperty( + name="Non-Collision Distance Scale", + description="The distance scale for creating extra non-collision constraints while building physics", + min=0, + soft_max=10, + default=1.5, + ) + + collision_margin: bpy.props.FloatProperty( + name="Collision Margin", + description="The collision margin between rigid bodies. If 0, the default value for each shape is adopted.", + unit="LENGTH", + min=0, + soft_max=10, + default=1e-06, + ) + + def execute(self, context): + root_object = FnModel.find_root_object(context.active_object) + + with FnContext.temp_override_active_layer_collection(context, root_object): + rig = Model(root_object) + rig.build(self.non_collision_distance_scale, self.collision_margin) + FnContext.set_active_object(context, root_object) + + return {"FINISHED"} + + +class CleanAdditionalTransformConstraints(bpy.types.Operator): + bl_idname = "mmd_tools.clean_additional_transform" + bl_label = "Clean Additional Transform" + bl_description = "Delete shadow bones of selected object and revert bones to default MMD state" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + active_object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + FnBone.clean_additional_transformation(FnModel.find_armature_object(root_object)) + FnContext.set_active_object(context, active_object) + return {"FINISHED"} + + +class ApplyAdditionalTransformConstraints(bpy.types.Operator): + bl_idname = "mmd_tools.apply_additional_transform" + bl_label = "Apply Additional Transform" + bl_description = "Translate appended bones of selected object for Blender" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + active_object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + + armature_object = FnModel.find_armature_object(root_object) + assert armature_object is not None + + MigrationFnBone.fix_mmd_ik_limit_override(armature_object) + FnBone.apply_additional_transformation(armature_object) + FnContext.set_active_object(context, active_object) + return {"FINISHED"} + + +class SetupBoneFixedAxes(bpy.types.Operator): + bl_idname = "mmd_tools.bone_fixed_axis_setup" + bl_label = "Setup Bone Fixed Axis" + bl_description = "Setup fixed axis of selected bones" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + type: bpy.props.EnumProperty( + name="Type", + description="Select type", + items=[ + ("DISABLE", "Disable", "Disable MMD fixed axis of selected bones", 0), + ("LOAD", "Load", "Load/Enable MMD fixed axis of selected bones from their Y-axis or the only rotatable axis", 1), + ("APPLY", "Apply", "Align bone axes to MMD fixed axis of each bone", 2), + ], + default="LOAD", + ) + + def execute(self, context): + armature_object = context.active_object + if not armature_object or armature_object.type != "ARMATURE": + self.report({"ERROR"}, "Active object is not an armature object") + return {"CANCELLED"} + + if self.type == "APPLY": + FnBone.apply_bone_fixed_axis(armature_object) + FnBone.apply_additional_transformation(armature_object) + else: + FnBone.load_bone_fixed_axis(armature_object, enable=(self.type == "LOAD")) + return {"FINISHED"} + + +class SetupBoneLocalAxes(bpy.types.Operator): + bl_idname = "mmd_tools.bone_local_axes_setup" + bl_label = "Setup Bone Local Axes" + bl_description = "Setup local axes of each bone" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + type: bpy.props.EnumProperty( + name="Type", + description="Select type", + items=[ + ("DISABLE", "Disable", "Disable MMD local axes of selected bones", 0), + ("LOAD", "Load", "Load/Enable MMD local axes of selected bones from their bone axes", 1), + ("APPLY", "Apply", "Align bone axes to MMD local axes of each bone", 2), + ], + default="LOAD", + ) + + def execute(self, context): + armature_object = context.active_object + if not armature_object or armature_object.type != "ARMATURE": + self.report({"ERROR"}, "Active object is not an armature object") + return {"CANCELLED"} + + if self.type == "APPLY": + FnBone.apply_bone_local_axes(armature_object) + FnBone.apply_additional_transformation(armature_object) + else: + FnBone.load_bone_local_axes(armature_object, enable=(self.type == "LOAD")) + return {"FINISHED"} + + +class AddMissingVertexGroupsFromBones(bpy.types.Operator): + bl_idname = "mmd_tools.add_missing_vertex_groups_from_bones" + bl_label = "Add Missing Vertex Groups from Bones" + bl_description = "Add the missing vertex groups to the selected mesh" + bl_options = {"REGISTER", "UNDO"} + + search_in_all_meshes: bpy.props.BoolProperty( + name="Search in all meshes", + description="Search for vertex groups in all meshes", + default=False, + ) + + @classmethod + def poll(cls, context: bpy.types.Context): + return FnModel.find_root_object(context.active_object) is not None + + def execute(self, context: bpy.types.Context): + active_object: bpy.types.Object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + + bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object) + if bone_order_mesh_object is None: + return {"CANCELLED"} + + FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes) + + return {"FINISHED"} + + +class CreateMMDModelRoot(bpy.types.Operator): + bl_idname = "mmd_tools.create_mmd_model_root_object" + bl_label = "Create a MMD Model Root Object" + bl_description = "Create a MMD model root object with a basic armature" + bl_options = {"REGISTER", "UNDO"} + + name_j: bpy.props.StringProperty( + name="Name", + description="The name of the MMD model", + default="New MMD Model", + ) + name_e: bpy.props.StringProperty( + name="Name(Eng)", + description="The english name of the MMD model", + default="New MMD Model", + ) + scale: bpy.props.FloatProperty( + name="Scale", + description="Scale", + default=0.08, + ) + + def execute(self, context): + rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True) + rig.initialDisplayFrames() + return {"FINISHED"} + + def invoke(self, context, event): + vm = context.window_manager + return vm.invoke_props_dialog(self) + + +class ConvertToMMDModel(bpy.types.Operator): + bl_idname = "mmd_tools.convert_to_mmd_model" + bl_label = "Convert to a MMD Model" + bl_description = "Convert active armature with its meshes to a MMD model (experimental)" + bl_options = {"REGISTER", "UNDO"} + + ambient_color_source: bpy.props.EnumProperty( + name="Ambient Color Source", + description="Select ambient color source", + items=[ + ("DIFFUSE", "Diffuse", "Diffuse color", 0), + ("MIRROR", "Mirror", 'Mirror color (if property "mirror_color" is available)', 1), + ], + default="DIFFUSE", + ) + edge_threshold: bpy.props.FloatProperty( + name="Edge Threshold", + description="MMD toon edge will not be enabled if freestyle line color alpha less than this value", + min=0, + max=1.001, + precision=3, + step=0.1, + default=0.1, + ) + edge_alpha_min: bpy.props.FloatProperty( + name="Minimum Edge Alpha", + description="Minimum alpha of MMD toon edge color", + min=0, + max=1, + precision=3, + step=0.1, + default=0.5, + ) + scale: bpy.props.FloatProperty( + name="Scale", + description="Scaling factor for converting the model", + default=0.08, + ) + convert_material_nodes: bpy.props.BoolProperty( + name="Convert Material Nodes", + default=True, + ) + middle_joint_bones_lock: bpy.props.BoolProperty( + name="Middle Joint Bones Lock", + description="Lock specific bones for backward compatibility.", + default=False, + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return obj and obj.type == "ARMATURE" and obj.mode != "EDIT" + + def invoke(self, context, event): + vm = context.window_manager + return vm.invoke_props_dialog(self) + + def execute(self, context): + # TODO convert some basic MMD properties + armature_object = context.active_object + scale = self.scale + model_name = "New MMD Model" + + root_object = FnModel.find_root_object(armature_object) + if root_object is None or root_object != armature_object.parent: + Model.create(model_name, model_name, scale, armature_object=armature_object) + + self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context)) + self.__configure_rig(context, Model(armature_object.parent)) + return {"FINISHED"} + + def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects): + def __is_child_of_armature(mesh): + if mesh.parent is None: + return False + return mesh.parent == armature_object or __is_child_of_armature(mesh.parent) + + def __is_using_armature(mesh): + for m in mesh.modifiers: + if m.type == "ARMATURE" and m.object == armature_object: + return True + return False + + def __get_root(mesh): + if mesh.parent is None: + return mesh + return __get_root(mesh.parent) + + for x in objects: + if __is_using_armature(x) and not __is_child_of_armature(x): + x_root = __get_root(x) + m = x_root.matrix_world + x_root.parent_type = "OBJECT" + x_root.parent = armature_object + x_root.matrix_world = m + + def __configure_rig(self, context: bpy.types.Context, mmd_model: Model): + root_object = mmd_model.rootObject() + armature_object = mmd_model.armature() + mesh_objects = tuple(mmd_model.meshes()) + + mmd_model.loadMorphs() + + if self.middle_joint_bones_lock: + vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups} + for pose_bone in armature_object.pose.bones: + if not pose_bone.parent: + continue + if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups: + continue + pose_bone.lock_location = (True, True, True) + + from ..core.material import FnMaterial + + FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes) + try: + for m in (x for mesh in mesh_objects for x in mesh.data.materials if x): + FnMaterial.convert_to_mmd_material(m, context) + mmd_material = m.mmd_material + if self.ambient_color_source == "MIRROR" and hasattr(m, "mirror_color"): + mmd_material.ambient_color = m.mirror_color + else: + mmd_material.ambient_color = [0.5 * c for c in mmd_material.diffuse_color] + + if hasattr(m, "line_color"): # freestyle line color + line_color = list(m.line_color) + mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold + mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)] + finally: + FnMaterial.set_nodes_are_readonly(False) + from .display_item import DisplayItemQuickSetup + + FnBone.sync_display_item_frames_from_bone_collections(armature_object) + mmd_model.initialDisplayFrames(reset=False) # ensure default frames + DisplayItemQuickSetup.load_facial_items(root_object.mmd_root) + root_object.mmd_root.active_display_item_frame = 0 + + +class ResetObjectVisibility(bpy.types.Operator): + bl_idname = "mmd_tools.reset_object_visibility" + bl_label = "Reset Object Visivility" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + @classmethod + def poll(cls, context: bpy.types.Context): + active_object: bpy.types.Object = context.active_object + return FnModel.find_root_object(active_object) is not None + + def execute(self, context: bpy.types.Context): + active_object: bpy.types.Object = context.active_object + mmd_root_object = FnModel.find_root_object(active_object) + assert mmd_root_object is not None + mmd_root = mmd_root_object.mmd_root + + mmd_root_object.hide_set(False) + + rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object) + if rigid_group_object: + rigid_group_object.hide_set(True) + + joint_group_object = FnModel.find_joint_group_object(mmd_root_object) + if joint_group_object: + joint_group_object.hide_set(True) + + temporary_group_object = FnModel.find_temporary_group_object(mmd_root_object) + if temporary_group_object: + temporary_group_object.hide_set(True) + + mmd_root.show_meshes = True + mmd_root.show_armature = True + mmd_root.show_temporary_objects = False + mmd_root.show_rigid_bodies = False + mmd_root.show_names_of_rigid_bodies = False + mmd_root.show_joints = False + mmd_root.show_names_of_joints = False + + return {"FINISHED"} + + +class AssembleAll(bpy.types.Operator): + bl_idname = "mmd_tools.assemble_all" + bl_label = "Assemble All" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + active_object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + + with FnContext.temp_override_active_layer_collection(context, root_object) as context: + rig = Model(root_object) + MigrationFnBone.fix_mmd_ik_limit_override(rig.armature()) + FnBone.apply_additional_transformation(rig.armature()) + rig.build() + rig.morph_slider.bind() + + with context.temp_override(selected_objects=[active_object]): + bpy.ops.mmd_tools.sdef_bind() + root_object.mmd_root.use_property_driver = True + + FnContext.set_active_object(context, active_object) + + return {"FINISHED"} + + +class DisassembleAll(bpy.types.Operator): + bl_idname = "mmd_tools.disassemble_all" + bl_label = "Disassemble All" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + active_object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + + with FnContext.temp_override_active_layer_collection(context, root_object) as context: + root_object.mmd_root.use_property_driver = False + with context.temp_override(selected_objects=[active_object]): + bpy.ops.mmd_tools.sdef_unbind() + + rig = Model(root_object) + rig.morph_slider.unbind() + rig.clean() + FnBone.clean_additional_transformation(rig.armature()) + + FnContext.set_active_object(context, active_object) + + return {"FINISHED"} diff --git a/core/mmd/operators/model_edit.py b/core/mmd/operators/model_edit.py new file mode 100644 index 0000000..ca21046 --- /dev/null +++ b/core/mmd/operators/model_edit.py @@ -0,0 +1,313 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import itertools +from operator import itemgetter +from typing import Dict, List, Optional, Set + +import bmesh +import bpy + +from ..bpyutils import FnContext +from ..core.model import FnModel, Model + + +class MessageException(Exception): + """Class for error with message.""" + + +class ModelJoinByBonesOperator(bpy.types.Operator): + bl_idname = "mmd_tools.model_join_by_bones" + bl_label = "Model Join by Bones" + bl_options = {"REGISTER", "UNDO"} + + join_type: bpy.props.EnumProperty( + name="Join Type", + items=[ + ("CONNECTED", "Connected", ""), + ("OFFSET", "Keep Offset", ""), + ], + default="OFFSET", + ) + + @classmethod + def poll(cls, context: bpy.types.Context): + active_object: Optional[bpy.types.Object] = context.active_object + + if context.mode != "POSE": + return False + + if active_object is None: + return False + + if active_object.type != "ARMATURE": + return False + + if len(list(filter(lambda o: o.type == "ARMATURE", context.selected_objects))) < 2: + return False + + return len(context.selected_pose_bones) > 0 + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context: bpy.types.Context): + try: + self.join(context) + except MessageException as ex: + self.report(type={"ERROR"}, message=str(ex)) + return {"CANCELLED"} + + return {"FINISHED"} + + def join(self, context: bpy.types.Context): + bpy.ops.object.mode_set(mode="OBJECT") + + parent_root_object = FnModel.find_root_object(context.active_object) + child_root_objects = {FnModel.find_root_object(o) for o in context.selected_objects} + child_root_objects.remove(parent_root_object) + + if parent_root_object is None or len(child_root_objects) == 0: + raise MessageException("No MMD Models selected") + + with FnContext.temp_override_active_layer_collection(context, parent_root_object): + FnModel.join_models(parent_root_object, child_root_objects) + + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.armature.parent_set(type="OFFSET") + + # Connect child bones + if self.join_type == "CONNECTED": + parent_edit_bone: bpy.types.EditBone = context.active_bone + child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones) + child_edit_bones.remove(parent_edit_bone) + + child_edit_bone: bpy.types.EditBone + for child_edit_bone in child_edit_bones: + child_edit_bone.use_connect = True + + bpy.ops.object.mode_set(mode="POSE") + + +class ModelSeparateByBonesOperator(bpy.types.Operator): + bl_idname = "mmd_tools.model_separate_by_bones" + bl_label = "Model Separate by Bones" + bl_options = {"REGISTER", "UNDO"} + + separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True) + include_descendant_bones: bpy.props.BoolProperty(name="Include Descendant Bones", default=True) + weight_threshold: bpy.props.FloatProperty(name="Weight Threshold", default=0.001, min=0.0, max=1.0, precision=4, subtype="FACTOR") + boundary_joint_owner: bpy.props.EnumProperty( + name="Boundary Joint Owner", + items=[ + ("SOURCE", "Source Model", ""), + ("DESTINATION", "Destination Model", ""), + ], + default="DESTINATION", + ) + + @classmethod + def poll(cls, context: bpy.types.Context): + active_object: Optional[bpy.types.Object] = context.active_object + + if context.mode != "POSE": + return False + + if active_object is None: + return False + + if active_object.type != "ARMATURE": + return False + + if FnModel.find_root_object(active_object) is None: + return False + + return len(context.selected_pose_bones) > 0 + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context: bpy.types.Context): + try: + self.separate(context) + except MessageException as ex: + self.report(type={"ERROR"}, message=str(ex)) + return {"CANCELLED"} + + return {"FINISHED"} + + def separate(self, context: bpy.types.Context): + weight_threshold: float = self.weight_threshold + mmd_scale = 0.08 + + target_armature_object: bpy.types.Object = context.active_object + + bpy.ops.object.mode_set(mode="EDIT") + root_bones: Set[bpy.types.EditBone] = set(context.selected_bones) + + if self.include_descendant_bones: + for edit_bone in root_bones: + with context.temp_override(active_bone=edit_bone): + bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1) + + separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones} + deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform} + + mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object) + mmd_model = Model(mmd_root_object) + mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes()) + + mmd_model_mesh_objects = list(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys()) + + # separate armature bones + separate_armature_object: Optional[bpy.types.Object] + if self.separate_armature: + target_armature_object.select_set(True) + bpy.ops.armature.separate() + separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None) + bpy.ops.object.mode_set(mode="OBJECT") + + # collect separate rigid bodies + separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones} + + boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all + + # collect separate joints + separate_joints: Set[bpy.types.Object] = { + joint_object + for joint_object in mmd_model.joints() + if boundary_joint_owner_condition( + [ + joint_object.rigid_body_constraint.object1 in separate_rigid_bodies, + joint_object.rigid_body_constraint.object2 in separate_rigid_bodies, + ] + ) + } + + separate_mesh_objects: Set[bpy.types.Object] + model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object] + if len(mmd_model_mesh_objects) == 0: + separate_mesh_objects = set() + model2separate_mesh_objects = dict() + else: + # select meshes + obj: bpy.types.Object + for obj in context.view_layer.objects: + obj.select_set(obj in mmd_model_mesh_objects) + context.view_layer.objects.active = mmd_model_mesh_objects[0] + + # separate mesh by selected vertices + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.separate(type="SELECTED") + separate_mesh_objects: List[bpy.types.Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects] + bpy.ops.object.mode_set(mode="OBJECT") + + model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects)) + + separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False) + + separate_model.initialDisplayFrames() + separate_root_object = separate_model.rootObject() + separate_root_object.matrix_world = mmd_root_object.matrix_world + separate_model_armature_object = separate_model.armature() + + if self.separate_armature: + with context.temp_override( + active_object=separate_model_armature_object, + selected_editable_objects=[separate_model_armature_object, separate_armature_object], + ): + bpy.ops.object.join() + + # add mesh + with context.temp_override( + object=separate_model_armature_object, + selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + + # replace mesh armature modifier.object + for separate_mesh in separate_mesh_objects: + armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None) + if armature_modifier is None: + armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE") + + armature_modifier.object = separate_model_armature_object + + with context.temp_override( + object=separate_model.rigidGroupObject(), + selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + + with context.temp_override( + object=separate_model.jointGroupObject(), + selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + + # move separate objects to new collection + mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object) + assert mmd_layer_collection is not None + + separate_layer_collection = FnContext.find_user_layer_collection_by_object(context, separate_root_object) + assert separate_layer_collection is not None + + if mmd_layer_collection.name != separate_layer_collection.name: + for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints): + separate_layer_collection.collection.objects.link(separate_object) + mmd_layer_collection.collection.objects.unlink(separate_object) + + FnModel.copy_mmd_root( + separate_root_object, + mmd_root_object, + overwrite=True, + replace_name2values={ + # replace related_mesh property values + "related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()} + }, + ) + + def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]: + mesh2selected_vertex_count: Dict[bpy.types.Object, int] = dict() + target_bmesh: bmesh.types.BMesh = bmesh.new() + for mesh_object in mmd_model_mesh_objects: + vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups + + mesh: bpy.types.Mesh = mesh_object.data + target_bmesh.from_mesh(mesh, face_normals=False) + target_bmesh.select_mode |= {"VERT"} + deform_layer = target_bmesh.verts.layers.deform.verify() + + selected_vertex_count = 0 + vert: bmesh.types.BMVert + for vert in target_bmesh.verts: + vert.select_set(False) + + # Find the largest weight vertex group + weights = [(group_index, weight) for group_index, weight in vert[deform_layer].items() if vertex_groups[group_index].name in deform_bones] + + weights.sort(key=lambda i: vertex_groups[i[0]].name in separate_bones, reverse=True) + weights.sort(key=itemgetter(1), reverse=True) + group_index, weight = next(iter(weights), (0, -1)) + + if weight < weight_threshold: + continue + + if vertex_groups[group_index].name not in separate_bones: + continue + + selected_vertex_count += 1 + vert.select_set(True) + + if selected_vertex_count > 0: + mesh2selected_vertex_count[mesh_object] = selected_vertex_count + target_bmesh.select_flush_mode() + target_bmesh.to_mesh(mesh) + + target_bmesh.clear() + + return mesh2selected_vertex_count diff --git a/core/mmd/operators/morph.py b/core/mmd/operators/morph.py new file mode 100644 index 0000000..1b34420 --- /dev/null +++ b/core/mmd/operators/morph.py @@ -0,0 +1,776 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +from typing import Optional, cast + +import bpy +from mathutils import Quaternion, Vector + +from ..core.model import FnModel +from .. import bpyutils, utils +from ..core.exceptions import MaterialNotFoundError +from ..core.material import FnMaterial +from ..core.morph import FnMorph +from ..utils import ItemMoveOp, ItemOp + + +# Util functions +def divide_vector_components(vec1, vec2): + if len(vec1) != len(vec2): + raise ValueError("Vectors should have the same number of components") + result = [] + for v1, v2 in zip(vec1, vec2): + if v2 == 0: + if v1 == 0: + v2 = 1 # If we have a 0/0 case we change the divisor to 1 + else: + raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero") + result.append(v1 / v2) + return result + + +def multiply_vector_components(vec1, vec2): + if len(vec1) != len(vec2): + raise ValueError("Vectors should have the same number of components") + result = [] + for v1, v2 in zip(vec1, vec2): + result.append(v1 * v2) + return result + + +def special_division(n1, n2): + """This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised""" + if n2 == 0: + if n1 == 0: + n2 = 1 + else: + raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero") + return n1 / n2 + + +class AddMorph(bpy.types.Operator): + bl_idname = "mmd_tools.morph_add" + bl_label = "Add Morph" + bl_description = "Add a morph item to active morph list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + mmd_root = root.mmd_root + morph_type = mmd_root.active_morph_type + morphs = getattr(mmd_root, morph_type) + morph, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph) + morph.name = "New Morph" + if morph_type.startswith("uv"): + morph.data_type = "VERTEX_GROUP" + return {"FINISHED"} + + +class RemoveMorph(bpy.types.Operator): + bl_idname = "mmd_tools.morph_remove" + bl_label = "Remove Morph" + bl_description = "Remove morph item(s) from the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + all: bpy.props.BoolProperty( + name="All", + description="Delete all morph items", + default=False, + options={"SKIP_SAVE"}, + ) + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + mmd_root = root.mmd_root + + morph_type = mmd_root.active_morph_type + if morph_type.startswith("material"): + bpy.ops.mmd_tools.clear_temp_materials() + elif morph_type.startswith("uv"): + bpy.ops.mmd_tools.clear_uv_morph_view() + + morphs = getattr(mmd_root, morph_type) + if self.all: + morphs.clear() + mmd_root.active_morph = 0 + else: + morphs.remove(mmd_root.active_morph) + mmd_root.active_morph = max(0, mmd_root.active_morph - 1) + return {"FINISHED"} + + +class MoveMorph(bpy.types.Operator, ItemMoveOp): + bl_idname = "mmd_tools.morph_move" + bl_label = "Move Morph" + bl_description = "Move active morph item up/down in the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + mmd_root = root.mmd_root + mmd_root.active_morph = self.move( + getattr(mmd_root, mmd_root.active_morph_type), + mmd_root.active_morph, + self.type, + ) + return {"FINISHED"} + + +class CopyMorph(bpy.types.Operator): + bl_idname = "mmd_tools.morph_copy" + bl_label = "Copy Morph" + bl_description = "Make a copy of active morph in the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + mmd_root = root.mmd_root + + morph_type = mmd_root.active_morph_type + morphs = getattr(mmd_root, morph_type) + morph = ItemOp.get_by_index(morphs, mmd_root.active_morph) + if morph is None: + return {"CANCELLED"} + + name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer()) + + if morph_type.startswith("vertex"): + for obj in FnModel.iterate_mesh_objects(root): + FnMorph.copy_shape_key(obj, name_orig, name_tmp) + + elif morph_type.startswith("uv"): + if morph.data_type == "VERTEX_GROUP": + for obj in FnModel.iterate_mesh_objects(root): + FnMorph.copy_uv_morph_vertex_groups(obj, name_orig, name_tmp) + + morph_new, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph) + for k, v in morph.items(): + morph_new[k] = v if k != "name" else name_tmp + morph_new.name = name_orig + "_copy" # trigger name check + return {"FINISHED"} + + +class OverwriteBoneMorphsFromActionPose(bpy.types.Operator): + bl_idname = "mmd_tools.morph_overwrite_from_active_action_pose" + bl_label = "Overwrite Bone Morphs from active Action Pose" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + @classmethod + def poll(cls, context): + root = FnModel.find_root_object(context.active_object) + if root is None: + return False + + return root.mmd_root.active_morph_type == "bone_morphs" + + def execute(self, context): + root = FnModel.find_root_object(context.active_object) + FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root)) + + return {"FINISHED"} + + +class AddMorphOffset(bpy.types.Operator): + bl_idname = "mmd_tools.morph_offset_add" + bl_label = "Add Morph Offset" + bl_description = "Add a morph offset item to the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + mmd_root = root.mmd_root + morph_type = mmd_root.active_morph_type + morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) + if morph is None: + return {"CANCELLED"} + + item, morph.active_data = ItemOp.add_after(morph.data, morph.active_data) + + if morph_type.startswith("material"): + if obj.type == "MESH" and obj.mmd_type == "NONE": + item.related_mesh = obj.data.name + active_material = obj.active_material + if active_material and "_temp" not in active_material.name: + item.material = active_material.name + + elif morph_type.startswith("bone"): + pose_bone = context.active_pose_bone + if pose_bone: + item.bone = pose_bone.name + item.location = pose_bone.location + item.rotation = pose_bone.rotation_quaternion + + return {"FINISHED"} + + +class RemoveMorphOffset(bpy.types.Operator): + bl_idname = "mmd_tools.morph_offset_remove" + bl_label = "Remove Morph Offset" + bl_description = "Remove morph offset item(s) from the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + all: bpy.props.BoolProperty( + name="All", + description="Delete all morph offset items", + default=False, + options={"SKIP_SAVE"}, + ) + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + mmd_root = root.mmd_root + morph_type = mmd_root.active_morph_type + morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) + if morph is None: + return {"CANCELLED"} + + if morph_type.startswith("material"): + bpy.ops.mmd_tools.clear_temp_materials() + + if self.all: + if morph_type.startswith("vertex"): + for obj in FnModel.iterate_mesh_objects(root): + FnMorph.remove_shape_key(obj, morph.name) + return {"FINISHED"} + elif morph_type.startswith("uv"): + if morph.data_type == "VERTEX_GROUP": + for obj in FnModel.iterate_mesh_objects(root): + FnMorph.store_uv_morph_data(obj, morph) + return {"FINISHED"} + morph.data.clear() + morph.active_data = 0 + else: + morph.data.remove(morph.active_data) + morph.active_data = max(0, morph.active_data - 1) + return {"FINISHED"} + + +class InitMaterialOffset(bpy.types.Operator): + bl_idname = "mmd_tools.material_morph_offset_init" + bl_label = "Init Material Offset" + bl_description = "Set all offset values to target value" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + target_value: bpy.props.FloatProperty( + name="Target Value", + description="Target value", + default=0, + ) + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + mmd_root = root.mmd_root + morph = mmd_root.material_morphs[mmd_root.active_morph] + mat_data = morph.data[morph.active_data] + + val = self.target_value + mat_data.diffuse_color = mat_data.edge_color = (val,) * 4 + mat_data.specular_color = mat_data.ambient_color = (val,) * 3 + mat_data.shininess = mat_data.edge_weight = val + mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4 + return {"FINISHED"} + + +class ApplyMaterialOffset(bpy.types.Operator): + bl_idname = "mmd_tools.apply_material_morph_offset" + bl_label = "Apply Material Offset" + bl_description = "Calculates the offsets and apply them, then the temporary material is removed" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + mmd_root = root.mmd_root + morph = mmd_root.material_morphs[mmd_root.active_morph] + mat_data = morph.data[morph.active_data] + + if not mat_data.related_mesh: + self.report({"ERROR"}, "You need to choose a Related Mesh first") + return {"CANCELLED"} + meshObj = FnModel.find_mesh_object_by_name(morph.id_data, mat_data.related_mesh) + if meshObj is None: + self.report({"ERROR"}, "The model mesh can't be found") + return {"CANCELLED"} + try: + work_mat_name = mat_data.material + "_temp" + work_mat, base_mat = FnMaterial.swap_materials(meshObj, work_mat_name, mat_data.material) + except MaterialNotFoundError: + self.report({"ERROR"}, "Material not found") + return {"CANCELLED"} + + base_mmd_mat = base_mat.mmd_material + work_mmd_mat = work_mat.mmd_material + + if mat_data.offset_type == "MULT": + try: + diffuse_offset = divide_vector_components(work_mmd_mat.diffuse_color, base_mmd_mat.diffuse_color) + [special_division(work_mmd_mat.alpha, base_mmd_mat.alpha)] + specular_offset = divide_vector_components(work_mmd_mat.specular_color, base_mmd_mat.specular_color) + edge_offset = divide_vector_components(work_mmd_mat.edge_color, base_mmd_mat.edge_color) + mat_data.diffuse_color = diffuse_offset + mat_data.specular_color = specular_offset + mat_data.shininess = special_division(work_mmd_mat.shininess, base_mmd_mat.shininess) + mat_data.ambient_color = divide_vector_components(work_mmd_mat.ambient_color, base_mmd_mat.ambient_color) + mat_data.edge_color = edge_offset + mat_data.edge_weight = special_division(work_mmd_mat.edge_weight, base_mmd_mat.edge_weight) + + except ZeroDivisionError: + mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD + except ValueError: + self.report({"ERROR"}, "An unexpected error happened") + # We should stop on our tracks and re-raise the exception + raise + + if mat_data.offset_type == "ADD": + diffuse_offset = list(work_mmd_mat.diffuse_color - base_mmd_mat.diffuse_color) + [work_mmd_mat.alpha - base_mmd_mat.alpha] + specular_offset = list(work_mmd_mat.specular_color - base_mmd_mat.specular_color) + edge_offset = Vector(work_mmd_mat.edge_color) - Vector(base_mmd_mat.edge_color) + mat_data.diffuse_color = diffuse_offset + mat_data.specular_color = specular_offset + mat_data.shininess = work_mmd_mat.shininess - base_mmd_mat.shininess + mat_data.ambient_color = work_mmd_mat.ambient_color - base_mmd_mat.ambient_color + mat_data.edge_color = list(edge_offset) + mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight + + FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat) + return {"FINISHED"} + + +class CreateWorkMaterial(bpy.types.Operator): + bl_idname = "mmd_tools.create_work_material" + bl_label = "Create Work Material" + bl_description = "Creates a temporary material to edit this offset" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + mmd_root = root.mmd_root + morph = mmd_root.material_morphs[mmd_root.active_morph] + mat_data = morph.data[morph.active_data] + + if not mat_data.related_mesh: + self.report({"ERROR"}, "You need to choose a Related Mesh first") + return {"CANCELLED"} + meshObj = FnModel.find_mesh_object_by_name(morph.id_data, mat_data.related_mesh) + if meshObj is None: + self.report({"ERROR"}, "The model mesh can't be found") + return {"CANCELLED"} + + base_mat = meshObj.data.materials.get(mat_data.material, None) + if base_mat is None: + self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material) + return {"CANCELLED"} + + work_mat_name = base_mat.name + "_temp" + if work_mat_name in bpy.data.materials: + self.report({"ERROR"}, 'Temporary material "%s" is in use' % work_mat_name) + return {"CANCELLED"} + + work_mat = base_mat.copy() + work_mat.name = work_mat_name + meshObj.data.materials.append(work_mat) + FnMaterial.swap_materials(meshObj, base_mat.name, work_mat.name) + base_mmd_mat = base_mat.mmd_material + work_mmd_mat = work_mat.mmd_material + work_mmd_mat.material_id = -1 + + # Apply the offsets + if mat_data.offset_type == "MULT": + diffuse_offset = multiply_vector_components(base_mmd_mat.diffuse_color, mat_data.diffuse_color[0:3]) + specular_offset = multiply_vector_components(base_mmd_mat.specular_color, mat_data.specular_color) + edge_offset = multiply_vector_components(base_mmd_mat.edge_color, mat_data.edge_color) + ambient_offset = multiply_vector_components(base_mmd_mat.ambient_color, mat_data.ambient_color) + work_mmd_mat.diffuse_color = diffuse_offset + work_mmd_mat.alpha *= mat_data.diffuse_color[3] + work_mmd_mat.specular_color = specular_offset + work_mmd_mat.shininess *= mat_data.shininess + work_mmd_mat.ambient_color = ambient_offset + work_mmd_mat.edge_color = edge_offset + work_mmd_mat.edge_weight *= mat_data.edge_weight + elif mat_data.offset_type == "ADD": + diffuse_offset = Vector(base_mmd_mat.diffuse_color) + Vector(mat_data.diffuse_color[0:3]) + specular_offset = Vector(base_mmd_mat.specular_color) + Vector(mat_data.specular_color) + edge_offset = Vector(base_mmd_mat.edge_color) + Vector(mat_data.edge_color) + ambient_offset = Vector(base_mmd_mat.ambient_color) + Vector(mat_data.ambient_color) + work_mmd_mat.diffuse_color = list(diffuse_offset) + work_mmd_mat.alpha += mat_data.diffuse_color[3] + work_mmd_mat.specular_color = list(specular_offset) + work_mmd_mat.shininess += mat_data.shininess + work_mmd_mat.ambient_color = list(ambient_offset) + work_mmd_mat.edge_color = list(edge_offset) + work_mmd_mat.edge_weight += mat_data.edge_weight + + return {"FINISHED"} + + +class ClearTempMaterials(bpy.types.Operator): + bl_idname = "mmd_tools.clear_temp_materials" + bl_label = "Clear Temp Materials" + bl_description = "Clears all the temporary materials" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + for meshObj in FnModel.iterate_mesh_objects(root): + + def __pre_remove(m): + if m and "_temp" in m.name: + base_mat_name = m.name.split("_temp")[0] + try: + FnMaterial.swap_materials(meshObj, m.name, base_mat_name) + return True + except MaterialNotFoundError: + self.report({"WARNING"}, "Base material for %s was not found" % m.name) + return False + + FnMaterial.clean_materials(meshObj, can_remove=__pre_remove) + return {"FINISHED"} + + +class ViewBoneMorph(bpy.types.Operator): + bl_idname = "mmd_tools.view_bone_morph" + bl_label = "View Bone Morph" + bl_description = "View the result of active bone morph" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + mmd_root = root.mmd_root + armature = FnModel.find_armature_object(root) + utils.selectSingleBone(context, armature, None, True) + morph = mmd_root.bone_morphs[mmd_root.active_morph] + for morph_data in morph.data: + p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None) + if p_bone: + p_bone.bone.select = True + mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4() + mtx.translation = p_bone.location + morph_data.location + p_bone.matrix_basis = mtx + return {"FINISHED"} + + +class ClearBoneMorphView(bpy.types.Operator): + bl_idname = "mmd_tools.clear_bone_morph_view" + bl_label = "Clear Bone Morph View" + bl_description = "Reset transforms of all bones to their default values" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + armature = FnModel.find_armature_object(root) + for p_bone in armature.pose.bones: + p_bone.matrix_basis.identity() + return {"FINISHED"} + + +class ApplyBoneMorph(bpy.types.Operator): + bl_idname = "mmd_tools.apply_bone_morph" + bl_label = "Apply Bone Morph" + bl_description = "Apply current pose to active bone morph" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + armature = FnModel.find_armature_object(root) + mmd_root = root.mmd_root + morph = mmd_root.bone_morphs[mmd_root.active_morph] + morph.data.clear() + morph.active_data = 0 + for p_bone in armature.pose.bones: + if p_bone.location.length > 0 or p_bone.matrix_basis.decompose()[1].angle > 0: + item = morph.data.add() + item.bone = p_bone.name + item.location = p_bone.location + item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() + p_bone.bone.select = True + else: + p_bone.bone.select = False + return {"FINISHED"} + + +class SelectRelatedBone(bpy.types.Operator): + bl_idname = "mmd_tools.select_bone_morph_offset_bone" + bl_label = "Select Related Bone" + bl_description = "Select the bone assigned to this offset in the armature" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + mmd_root = root.mmd_root + armature = FnModel.find_armature_object(root) + morph = mmd_root.bone_morphs[mmd_root.active_morph] + morph_data = morph.data[morph.active_data] + utils.selectSingleBone(context, armature, morph_data.bone) + return {"FINISHED"} + + +class EditBoneOffset(bpy.types.Operator): + bl_idname = "mmd_tools.edit_bone_morph_offset" + bl_label = "Edit Related Bone" + bl_description = "Applies the location and rotation of this offset to the bone" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + mmd_root = root.mmd_root + armature = FnModel.find_armature_object(root) + morph = mmd_root.bone_morphs[mmd_root.active_morph] + morph_data = morph.data[morph.active_data] + p_bone = armature.pose.bones[morph_data.bone] + mtx = Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix().to_4x4() + mtx.translation = morph_data.location + p_bone.matrix_basis = mtx + utils.selectSingleBone(context, armature, p_bone.name) + return {"FINISHED"} + + +class ApplyBoneOffset(bpy.types.Operator): + bl_idname = "mmd_tools.apply_bone_morph_offset" + bl_label = "Apply Bone Morph Offset" + bl_description = "Stores the current bone location and rotation into this offset" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + mmd_root = root.mmd_root + armature = FnModel.find_armature_object(root) + assert armature is not None + morph = mmd_root.bone_morphs[mmd_root.active_morph] + morph_data = morph.data[morph.active_data] + p_bone = armature.pose.bones[morph_data.bone] + morph_data.location = p_bone.location + morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() + return {"FINISHED"} + + +class ViewUVMorph(bpy.types.Operator): + bl_idname = "mmd_tools.view_uv_morph" + bl_label = "View UV Morph" + bl_description = "View the result of active UV morph on current mesh object" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + mmd_root = root.mmd_root + + meshes = tuple(FnModel.iterate_mesh_objects(root)) + if len(meshes) == 1: + obj = meshes[0] + elif obj not in meshes: + self.report({"ERROR"}, "Please select a mesh object") + return {"CANCELLED"} + meshObj = obj + + bpy.ops.mmd_tools.clear_uv_morph_view() + + selected = meshObj.select_get() + with bpyutils.select_object(meshObj): + mesh = cast(bpy.types.Mesh, meshObj.data) + morph = mmd_root.uv_morphs[mmd_root.active_morph] + uv_textures = mesh.uv_layers + + base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")] + if morph.uv_index >= len(base_uv_layers): + self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index) + return {"CANCELLED"} + + uv_layer_name = base_uv_layers[morph.uv_index].name + if morph.uv_index == 0 or uv_textures.active.name not in {uv_layer_name, "_" + uv_layer_name}: + uv_textures.active = uv_textures[uv_layer_name] + + uv_layer_name = uv_textures.active.name + uv_tex = uv_textures.new(name="__uv.%s" % uv_layer_name) + if uv_tex is None: + self.report({"ERROR"}, "Failed to create a temporary uv layer") + return {"CANCELLED"} + + offsets = FnMorph.get_uv_morph_offset_map(meshObj, morph).items() + offsets = {k: getattr(Vector(v), "zw" if uv_layer_name.startswith("_") else "xy") for k, v in offsets} + if len(offsets) > 0: + base_uv_data = mesh.uv_layers.active.data + temp_uv_data = mesh.uv_layers[uv_tex.name].data + for i, l in enumerate(mesh.loops): + select = temp_uv_data[i].select = l.vertex_index in offsets + if select: + temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index] + + uv_textures.active = uv_tex + uv_tex.active_render = True + meshObj.hide_set(False) + meshObj.select_set(selected) + return {"FINISHED"} + + +class ClearUVMorphView(bpy.types.Operator): + bl_idname = "mmd_tools.clear_uv_morph_view" + bl_label = "Clear UV Morph View" + bl_description = "Clear all temporary data of UV morphs" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + assert root is not None + for m in FnModel.iterate_mesh_objects(root): + mesh = m.data + uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers) + for t in uv_textures: + if t.name.startswith("__uv."): + uv_textures.remove(t) + if len(uv_textures) > 0: + uv_textures[0].active_render = True + uv_textures.active_index = 0 + + animation_data = mesh.animation_data + if animation_data: + nla_tracks = animation_data.nla_tracks + for t in nla_tracks: + if t.name.startswith("__uv."): + nla_tracks.remove(t) + if animation_data.action and animation_data.action.name.startswith("__uv."): + animation_data.action = None + if animation_data.action is None and len(nla_tracks) == 0: + mesh.animation_data_clear() + + for act in bpy.data.actions: + if act.name.startswith("__uv.") and act.users < 1: + bpy.data.actions.remove(act) + return {"FINISHED"} + + +class EditUVMorph(bpy.types.Operator): + bl_idname = "mmd_tools.edit_uv_morph" + bl_label = "Edit UV Morph" + bl_description = "Edit UV morph on a temporary UV layer (use UV Editor to edit the result)" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + @classmethod + def poll(cls, context): + obj = context.active_object + if obj.type != "MESH": + return False + active_uv_layer = obj.data.uv_layers.active + return active_uv_layer and active_uv_layer.name.startswith("__uv.") + + def execute(self, context): + obj = context.active_object + meshObj = obj + + selected = meshObj.select_get() + with bpyutils.select_object(meshObj): + mesh = cast(bpy.types.Mesh, meshObj.data) + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.select_mode(type="VERT", action="ENABLE") + bpy.ops.mesh.reveal() # unhide all vertices + bpy.ops.mesh.select_all(action="DESELECT") + bpy.ops.object.mode_set(mode="OBJECT") + + vertices = mesh.vertices + for l, d in zip(mesh.loops, mesh.uv_layers.active.data): + if d.select: + vertices[l.vertex_index].select = True + + polygons = mesh.polygons + polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active) + + bpy.ops.object.mode_set(mode="EDIT") + meshObj.select_set(selected) + return {"FINISHED"} + + +class ApplyUVMorph(bpy.types.Operator): + bl_idname = "mmd_tools.apply_uv_morph" + bl_label = "Apply UV Morph" + bl_description = "Calculate the UV offsets of selected vertices and apply to active UV morph" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + @classmethod + def poll(cls, context): + obj = context.active_object + if obj.type != "MESH": + return False + active_uv_layer = obj.data.uv_layers.active + return active_uv_layer and active_uv_layer.name.startswith("__uv.") + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + mmd_root = root.mmd_root + meshObj = obj + + selected = meshObj.select_get() + with bpyutils.select_object(meshObj): + mesh = cast(bpy.types.Mesh, meshObj.data) + morph = mmd_root.uv_morphs[mmd_root.active_morph] + + base_uv_name = mesh.uv_layers.active.name[5:] + if base_uv_name not in mesh.uv_layers: + self.report({"ERROR"}, ' * UV map "%s" not found' % base_uv_name) + return {"CANCELLED"} + + base_uv_data = mesh.uv_layers[base_uv_name].data + temp_uv_data = mesh.uv_layers.active.data + axis_type = "ZW" if base_uv_name.startswith("_") else "XY" + + from collections import namedtuple + + __OffsetData = namedtuple("OffsetData", "index, offset") + offsets = {} + vertices = mesh.vertices + for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data): + if vertices[l.vertex_index].select and l.vertex_index not in offsets: + dx, dy = i1.uv - i0.uv + if abs(dx) > 0.0001 or abs(dy) > 0.0001: + offsets[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy)) + + FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type) + morph.data_type = "VERTEX_GROUP" + + meshObj.select_set(selected) + return {"FINISHED"} + + +class CleanDuplicatedMaterialMorphs(bpy.types.Operator): + bl_idname = "mmd_tools.clean_duplicated_material_morphs" + bl_label = "Clean Duplicated Material Morphs" + bl_description = "Clean duplicated material morphs" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return FnModel.find_root_object(context.active_object) is not None + + def execute(self, context: bpy.types.Context): + mmd_root_object = FnModel.find_root_object(context.active_object) + FnMorph.clean_duplicated_material_morphs(mmd_root_object) + + return {"FINISHED"} diff --git a/core/mmd/operators/rigid_body.py b/core/mmd/operators/rigid_body.py new file mode 100644 index 0000000..22e3515 --- /dev/null +++ b/core/mmd/operators/rigid_body.py @@ -0,0 +1,579 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import math +from typing import Dict, Optional, Tuple, cast + +import bpy +from mathutils import Euler, Vector + +from .. import utils +from ..bpyutils import FnContext, Props +from ..core import rigid_body +from ..core.model import FnModel, Model +from ..core.rigid_body import FnRigidBody + + +class SelectRigidBody(bpy.types.Operator): + bl_idname = "mmd_tools.rigid_body_select" + bl_label = "Select Rigid Body" + bl_description = "Select similar rigidbody objects which have the same property values with active rigidbody object" + bl_options = {"REGISTER", "UNDO"} + + properties: bpy.props.EnumProperty( + name="Properties", + description="Select the properties to be compared", + options={"ENUM_FLAG"}, + items=[ + ("collision_group_number", "Collision Group", "Collision group", 1), + ("collision_group_mask", "Collision Group Mask", "Collision group mask", 2), + ("type", "Rigid Type", "Rigid type", 4), + ("shape", "Shape", "Collision shape", 8), + ("bone", "Bone", "Target bone", 16), + ], + default=set(), + ) + hide_others: bpy.props.BoolProperty( + name="Hide Others", + description="Hide the rigidbody object which does not have the same property values with active rigidbody object", + default=False, + ) + + def invoke(self, context, event): + vm = context.window_manager + return vm.invoke_props_dialog(self) + + @classmethod + def poll(cls, context): + return FnModel.is_rigid_body_object(context.active_object) + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + if root is None: + self.report({"ERROR"}, "The model root can't be found") + return {"CANCELLED"} + + selection = set(FnModel.iterate_rigid_body_objects(root)) + + for prop_name in self.properties: + prop_value = getattr(obj.mmd_rigid, prop_name) + if prop_name == "collision_group_mask": + prop_value = tuple(prop_value) + for i in selection.copy(): + if tuple(i.mmd_rigid.collision_group_mask) != prop_value: + selection.remove(i) + if self.hide_others: + i.select_set(False) + i.hide_set(True) + else: + for i in selection.copy(): + if getattr(i.mmd_rigid, prop_name) != prop_value: + selection.remove(i) + if self.hide_others: + i.select_set(False) + i.hide_set(True) + + for i in selection: + i.hide_set(False) + i.select_set(True) + + return {"FINISHED"} + + +class AddRigidBody(bpy.types.Operator): + bl_idname = "mmd_tools.rigid_body_add" + bl_label = "Add Rigid Body" + bl_description = "Add Rigid Bodies to selected bones" + bl_options = {"REGISTER", "UNDO", "PRESET", "INTERNAL"} + + name_j: bpy.props.StringProperty( + name="Name", + description="The name of rigid body ($name_j means use the japanese name of target bone)", + default="$name_j", + ) + name_e: bpy.props.StringProperty( + name="Name(Eng)", + description="The english name of rigid body ($name_e means use the english name of target bone)", + default="$name_e", + ) + collision_group_number: bpy.props.IntProperty( + name="Collision Group", + description="The collision group of the object", + min=0, + max=15, + ) + collision_group_mask: bpy.props.BoolVectorProperty( + name="Collision Group Mask", + description="The groups the object can not collide with", + size=16, + subtype="LAYER", + ) + rigid_type: bpy.props.EnumProperty( + name="Rigid Type", + description="Select rigid type", + items=[ + (str(rigid_body.MODE_STATIC), "Bone", "Rigid body's orientation completely determined by attached bone", 1), + (str(rigid_body.MODE_DYNAMIC), "Physics", "Attached bone's orientation completely determined by rigid body", 2), + (str(rigid_body.MODE_DYNAMIC_BONE), "Physics + Bone", "Bone determined by combination of parent and attached rigid body", 3), + ], + ) + rigid_shape: bpy.props.EnumProperty( + name="Shape", + description="Select the collision shape", + items=[ + ("SPHERE", "Sphere", "", 1), + ("BOX", "Box", "", 2), + ("CAPSULE", "Capsule", "", 3), + ], + ) + size: bpy.props.FloatVectorProperty( + name="Size", + description="Size of the object, the values will multiply the length of target bone", + subtype="XYZ", + size=3, + min=0, + default=[0.6, 0.6, 0.6], + ) + mass: bpy.props.FloatProperty( + name="Mass", + description="How much the object 'weights' irrespective of gravity", + min=0.001, + default=1, + ) + friction: bpy.props.FloatProperty( + name="Friction", + description="Resistance of object to movement", + min=0, + soft_max=1, + default=0.5, + ) + bounce: bpy.props.FloatProperty( + name="Restitution", + description="Tendency of object to bounce after colliding with another (0 = stays still, 1 = perfectly elastic)", + min=0, + soft_max=1, + ) + linear_damping: bpy.props.FloatProperty( + name="Linear Damping", + description="Amount of linear velocity that is lost over time", + min=0, + max=1, + default=0.04, + ) + angular_damping: bpy.props.FloatProperty( + name="Angular Damping", + description="Amount of angular velocity that is lost over time", + min=0, + max=1, + default=0.1, + ) + + def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None): + name_j: str = self.name_j + name_e: str = self.name_e + size = self.size.copy() + loc = Vector((0.0, 0.0, 0.0)) + rot = Euler((0.0, 0.0, 0.0)) + bone_name: Optional[str] = None + + if pose_bone is None: + size *= getattr(root_object, Props.empty_display_size) + else: + bone_name = pose_bone.name + mmd_bone = pose_bone.mmd_bone + name_j = name_j.replace("$name_j", mmd_bone.name_j or bone_name) + name_e = name_e.replace("$name_e", mmd_bone.name_e or bone_name) + + target_bone = pose_bone.bone + loc = (target_bone.head_local + target_bone.tail_local) / 2 + rot = target_bone.matrix_local.to_euler("YXZ") + rot.rotate_axis("X", math.pi / 2) + + size *= target_bone.length + if 1: + pass # bypass resizing + elif self.rigid_shape == "SPHERE": + size.x *= 0.8 + elif self.rigid_shape == "BOX": + size.x /= 3 + size.y /= 3 + size.z *= 0.8 + elif self.rigid_shape == "CAPSULE": + size.x /= 3 + + return FnRigidBody.setup_rigid_body_object( + obj=FnRigidBody.new_rigid_body_object(context, FnModel.ensure_rigid_group_object(context, root_object)), + shape_type=rigid_body.shapeType(self.rigid_shape), + location=loc, + rotation=rot, + size=size, + dynamics_type=int(self.rigid_type), + name=name_j, + name_e=name_e, + collision_group_number=self.collision_group_number, + collision_group_mask=self.collision_group_mask, + mass=self.mass, + friction=self.friction, + bounce=self.bounce, + linear_damping=self.linear_damping, + angular_damping=self.angular_damping, + bone=bone_name, + ) + + @classmethod + def poll(cls, context): + root_object = FnModel.find_root_object(context.active_object) + if root_object is None: + return False + + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return False + + return True + + def execute(self, context): + active_object = context.active_object + + root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object)) + armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object)) + + if active_object != armature_object: + FnContext.select_single_object(context, root_object).select_set(False) + elif armature_object.mode != "POSE": + bpy.ops.object.mode_set(mode="POSE") + + selected_pose_bones = [] + if context.selected_pose_bones: + selected_pose_bones = context.selected_pose_bones + + armature_object.select_set(False) + if len(selected_pose_bones) > 0: + for pose_bone in selected_pose_bones: + rigid = self.__add_rigid_body(context, root_object, pose_bone) + rigid.select_set(True) + else: + rigid = self.__add_rigid_body(context, root_object) + rigid.select_set(True) + return {"FINISHED"} + + def invoke(self, context, event): + no_bone = True + if context.selected_bones and len(context.selected_bones) > 0: + no_bone = False + elif context.selected_pose_bones and len(context.selected_pose_bones) > 0: + no_bone = False + + if no_bone: + self.name_j = "Rigid" + self.name_e = "Rigid_e" + else: + if self.name_j == "Rigid": + self.name_j = "$name_j" + if self.name_e == "Rigid_e": + self.name_e = "$name_e" + vm = context.window_manager + return vm.invoke_props_dialog(self) + + +class RemoveRigidBody(bpy.types.Operator): + bl_idname = "mmd_tools.rigid_body_remove" + bl_label = "Remove Rigid Body" + bl_description = "Deletes the currently selected Rigid Body" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return FnModel.is_rigid_body_object(context.active_object) + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + utils.selectAObject(obj) # ensure this is the only one object select + bpy.ops.object.delete(use_global=True) + if root: + utils.selectAObject(root) + return {"FINISHED"} + + +class RigidBodyBake(bpy.types.Operator): + bl_idname = "mmd_tools.ptcache_rigid_body_bake" + bl_label = "Bake" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context: bpy.types.Context): + with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): + bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True) + + return {"FINISHED"} + + +class RigidBodyDeleteBake(bpy.types.Operator): + bl_idname = "mmd_tools.ptcache_rigid_body_delete_bake" + bl_label = "Delete Bake" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context: bpy.types.Context): + with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): + bpy.ops.ptcache.free_bake("INVOKE_DEFAULT") + + return {"FINISHED"} + + +class AddJoint(bpy.types.Operator): + bl_idname = "mmd_tools.joint_add" + bl_label = "Add Joint" + bl_description = "Add Joint(s) to selected rigidbody objects" + bl_options = {"REGISTER", "UNDO", "PRESET", "INTERNAL"} + + use_bone_rotation: bpy.props.BoolProperty( + name="Use Bone Rotation", + description="Match joint orientation to bone orientation if enabled", + default=True, + ) + limit_linear_lower: bpy.props.FloatVectorProperty( + name="Limit Linear Lower", + description="Lower limit of translation", + subtype="XYZ", + size=3, + ) + limit_linear_upper: bpy.props.FloatVectorProperty( + name="Limit Linear Upper", + description="Upper limit of translation", + subtype="XYZ", + size=3, + ) + limit_angular_lower: bpy.props.FloatVectorProperty( + name="Limit Angular Lower", + description="Lower limit of rotation", + subtype="EULER", + size=3, + min=-math.pi * 2, + max=math.pi * 2, + default=[-math.pi / 4] * 3, + ) + limit_angular_upper: bpy.props.FloatVectorProperty( + name="Limit Angular Upper", + description="Upper limit of rotation", + subtype="EULER", + size=3, + min=-math.pi * 2, + max=math.pi * 2, + default=[math.pi / 4] * 3, + ) + spring_linear: bpy.props.FloatVectorProperty( + name="Spring(Linear)", + description="Spring constant of movement", + subtype="XYZ", + size=3, + min=0, + ) + spring_angular: bpy.props.FloatVectorProperty( + name="Spring(Angular)", + description="Spring constant of rotation", + subtype="XYZ", + size=3, + min=0, + ) + + def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]): + obj_seq = tuple(bone_map.keys()) + for rigid_a, bone_a in bone_map.items(): + for rigid_b, bone_b in bone_map.items(): + if bone_a and bone_b and bone_b.parent == bone_a: + obj_seq = () + yield (rigid_a, rigid_b) + if len(obj_seq) == 2: + if obj_seq[1].mmd_rigid.type == str(rigid_body.MODE_STATIC): + yield (obj_seq[1], obj_seq[0]) + else: + yield obj_seq + + def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map): + loc: Optional[Vector] = None + rot = Euler((0.0, 0.0, 0.0)) + rigid_a, rigid_b = rigid_pair + bone_a = bone_map[rigid_a] + bone_b = bone_map[rigid_b] + if bone_a and bone_b: + if bone_a.parent == bone_b: + rigid_b, rigid_a = rigid_a, rigid_b + bone_b, bone_a = bone_a, bone_b + if bone_b.parent == bone_a: + loc = bone_b.head_local + if self.use_bone_rotation: + rot = bone_b.matrix_local.to_euler("YXZ") + rot.rotate_axis("X", math.pi / 2) + if loc is None: + loc = (rigid_a.location + rigid_b.location) / 2 + + name_j = rigid_b.mmd_rigid.name_j or rigid_b.name + name_e = rigid_b.mmd_rigid.name_e or rigid_b.name + + return FnRigidBody.setup_joint_object( + obj=FnRigidBody.new_joint_object(context, FnModel.ensure_joint_group_object(context, root_object), FnModel.get_empty_display_size(root_object)), + name=name_j, + name_e=name_e, + location=loc, + rotation=rot, + rigid_a=rigid_a, + rigid_b=rigid_b, + maximum_location=self.limit_linear_upper, + minimum_location=self.limit_linear_lower, + maximum_rotation=self.limit_angular_upper, + minimum_rotation=self.limit_angular_lower, + spring_linear=self.spring_linear, + spring_angular=self.spring_angular, + ) + + @classmethod + def poll(cls, context): + root_object = FnModel.find_root_object(context.active_object) + if root_object is None: + return False + + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return False + + return True + + def execute(self, context): + active_object = context.active_object + root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object)) + armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object)) + bones = cast(bpy.types.Armature, armature_object.data).bones + bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()} + + if len(bone_map) < 2: + self.report({"ERROR"}, "Please select two or more mmd rigid objects") + return {"CANCELLED"} + + FnContext.select_single_object(context, root_object).select_set(False) + if context.scene.rigidbody_world is None: + bpy.ops.rigidbody.world_add() + + for pair in self.__enumerate_rigid_pair(bone_map): + joint = self.__add_joint(context, root_object, pair, bone_map) + joint.select_set(True) + + return {"FINISHED"} + + def invoke(self, context, event): + vm = context.window_manager + return vm.invoke_props_dialog(self) + + +class RemoveJoint(bpy.types.Operator): + bl_idname = "mmd_tools.joint_remove" + bl_label = "Remove Joint" + bl_description = "Deletes the currently selected Joint" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return FnModel.is_joint_object(context.active_object) + + def execute(self, context): + obj = context.active_object + root = FnModel.find_root_object(obj) + utils.selectAObject(obj) # ensure this is the only one object select + bpy.ops.object.delete(use_global=True) + if root: + utils.selectAObject(root) + return {"FINISHED"} + + +class UpdateRigidBodyWorld(bpy.types.Operator): + bl_idname = "mmd_tools.rigid_body_world_update" + bl_label = "Update Rigid Body World" + bl_description = "Update rigid body world and references of rigid body constraint according to current scene objects (experimental)" + bl_options = {"REGISTER", "UNDO"} + + @staticmethod + def __get_rigid_body_world_objects(): + rigid_body.setRigidBodyWorldEnabled(True) + rbw = bpy.context.scene.rigidbody_world + if not rbw.collection: + rbw.collection = bpy.data.collections.new("RigidBodyWorld") + rbw.collection.use_fake_user = True + if not rbw.constraints: + rbw.constraints = bpy.data.collections.new("RigidBodyConstraints") + rbw.constraints.use_fake_user = True + + bpy.context.scene.rigidbody_world.substeps_per_frame = 6 + bpy.context.scene.rigidbody_world.solver_iterations = 10 + + return rbw.collection.objects, rbw.constraints.objects + + def execute(self, context): + scene = context.scene + scene_objs = set(scene.objects) + scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects) + + def _update_group(obj, group): + if obj in scene_objs: + if obj not in group.values(): + group.link(obj) + return True + elif obj in group.values(): + group.unlink(obj) + return False + + def _references(obj): + yield obj + if getattr(obj, "proxy", None): + yield from _references(obj.proxy) + if getattr(obj, "override_library", None): + yield from _references(obj.override_library.reference) + + need_rebuild_physics = scene.rigidbody_world is None or scene.rigidbody_world.collection is None or scene.rigidbody_world.constraints is None + rb_objs, rbc_objs = self.__get_rigid_body_world_objects() + objects = bpy.data.objects + table = {} + + # Perhaps due to a bug in Blender, + # when bpy.ops.rigidbody.world_remove(), + # Object.rigid_body are removed, + # but Object.rigid_body_constraint are retained. + # Therefore, it must be checked with Object.mmd_type. + for i in (x for x in objects if x.mmd_type == "RIGID_BODY"): + if not _update_group(i, rb_objs): + continue + + rb_map = table.setdefault(FnModel.find_root_object(i), {}) + if i in rb_map: # means rb_map[i] will replace i + rb_objs.unlink(i) + continue + for r in _references(i): + rb_map[r] = i + + # TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters. + # mass, friction, restitution, linear_dumping, angular_dumping + + for i in (x for x in objects if x.rigid_body_constraint): + if not _update_group(i, rbc_objs): + continue + + rbc, root_object = i.rigid_body_constraint, FnModel.find_root_object(i) + rb_map = table.get(root_object, {}) + rbc.object1 = rb_map.get(rbc.object1, rbc.object1) + rbc.object2 = rb_map.get(rbc.object2, rbc.object2) + + if need_rebuild_physics: + for root_object in scene.objects: + if root_object.mmd_type != "ROOT": + continue + if not root_object.mmd_root.is_built: + continue + with FnContext.temp_override_active_layer_collection(context, root_object): + Model(root_object).build() + # After rebuild. First play. Will be crash! + # But saved it before. Reload after crash. The play can be work. + + return {"FINISHED"} diff --git a/core/mmd/operators/sdef.py b/core/mmd/operators/sdef.py new file mode 100644 index 0000000..e38badd --- /dev/null +++ b/core/mmd/operators/sdef.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +from typing import Set + +import bpy +from bpy.types import Operator + +from ..core.model import FnModel +from ..core.sdef import FnSDEF + + +def _get_target_objects(context): + root_objects: Set[bpy.types.Object] = set() + selected_objects: Set[bpy.types.Object] = set() + for i in context.selected_objects: + if i.type == "MESH": + selected_objects.add(i) + continue + + root_object = FnModel.find_root_object(i) + if root_object is None: + continue + if root_object in root_objects: + continue + + root_objects.add(root_object) + + selected_objects |= set(FnModel.iterate_mesh_objects(root_object)) + return selected_objects, root_objects + + +class ResetSDEFCache(Operator): + bl_idname = "mmd_tools.sdef_cache_reset" + bl_label = "Reset MMD SDEF cache" + bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + def execute(self, context): + target_meshes, _ = _get_target_objects(context) + for i in target_meshes: + FnSDEF.clear_cache(i) + FnSDEF.clear_cache(unused_only=True) + return {"FINISHED"} + + +class BindSDEF(Operator): + bl_idname = "mmd_tools.sdef_bind" + bl_label = "Bind SDEF Driver" + bl_description = "Bind MMD SDEF data of selected objects" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + mode: bpy.props.EnumProperty( + name="Mode", + description="Select mode", + items=[ + ("2", "Bulk", "Speed up with numpy (may be slower in some cases)", 2), + ("1", "Normal", "Normal mode", 1), + ("0", "- Auto -", "Select best mode by benchmark result", 0), + ], + default="0", + ) + use_skip: bpy.props.BoolProperty( + name="Skip", + description="Skip when the bones are not moving", + default=True, + ) + use_scale: bpy.props.BoolProperty( + name="Scale", + description="Support bone scaling (slow)", + default=False, + ) + + def invoke(self, context, event): + vm = context.window_manager + return vm.invoke_props_dialog(self) + + # TODO: Utility Functionalize + def execute(self, context): + target_meshes, root_objects = _get_target_objects(context) + + for r in root_objects: + r.mmd_root.use_sdef = True + + param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale) + count = sum(FnSDEF.bind(i, *param) for i in target_meshes) + self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)") + return {"FINISHED"} + + +class UnbindSDEF(Operator): + bl_idname = "mmd_tools.sdef_unbind" + bl_label = "Unbind SDEF Driver" + bl_description = "Unbind MMD SDEF data of selected objects" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + # TODO: Utility Functionalize + def execute(self, context): + target_meshes, root_objects = _get_target_objects(context) + for i in target_meshes: + FnSDEF.unbind(i) + + for r in root_objects: + r.mmd_root.use_sdef = False + + return {"FINISHED"} diff --git a/core/mmd/operators/translations.py b/core/mmd/operators/translations.py new file mode 100644 index 0000000..371427c --- /dev/null +++ b/core/mmd/operators/translations.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +from typing import TYPE_CHECKING, cast + +import bpy + +from ..core.model import FnModel, Model +from ..core.translations import MMD_DATA_TYPE_TO_HANDLERS, FnTranslations +from ..translations import DictionaryEnum + +if TYPE_CHECKING: + from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex + + +class TranslateMMDModel(bpy.types.Operator): + bl_idname = "mmd_tools.translate_mmd_model" + bl_label = "Translate a MMD Model" + bl_description = "Translate Japanese names of a MMD model" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + + dictionary: bpy.props.EnumProperty( + name="Dictionary", + items=DictionaryEnum.get_dictionary_items, + description="Translate names from Japanese to English using selected dictionary", + ) + types: bpy.props.EnumProperty( + name="Types", + description="Select which parts will be translated", + options={"ENUM_FLAG"}, + items=[ + ("BONE", "Bones", "Bones", 1), + ("MORPH", "Morphs", "Morphs", 2), + ("MATERIAL", "Materials", "Materials", 4), + ("DISPLAY", "Display", "Display frames", 8), + ("PHYSICS", "Physics", "Rigidbodies and joints", 16), + ("INFO", "Information", "Model name and comments", 32), + ], + default={ + "BONE", + "MORPH", + "MATERIAL", + "DISPLAY", + "PHYSICS", + }, + ) + modes: bpy.props.EnumProperty( + name="Modes", + description="Select translation mode", + options={"ENUM_FLAG"}, + items=[ + ("MMD", "MMD Names", "Fill MMD English names", 1), + ("BLENDER", "Blender Names", "Translate blender names (experimental)", 2), + ], + default={"MMD"}, + ) + use_morph_prefix: bpy.props.BoolProperty( + name="Use Morph Prefix", + description="Add/remove prefix to English name of morph", + default=False, + ) + overwrite: bpy.props.BoolProperty( + name="Overwrite", + description="Overwrite a translated English name", + default=False, + ) + allow_fails: bpy.props.BoolProperty( + name="Allow Fails", + description="Allow incompletely translated names", + default=False, + ) + + @classmethod + def poll(cls, context): + obj = context.active_object + return obj in context.selected_objects and FnModel.find_root_object(obj) + + def invoke(self, context, event): + vm = context.window_manager + return vm.invoke_props_dialog(self) + + def execute(self, context): + try: + self.__translator = DictionaryEnum.get_translator(self.dictionary) + except Exception as e: + self.report({"ERROR"}, "Failed to load dictionary: %s" % e) + return {"CANCELLED"} + + obj = context.active_object + root = FnModel.find_root_object(obj) + rig = Model(root) + + if "MMD" in self.modes: + for i in self.types: + getattr(self, "translate_%s" % i.lower())(rig) + + if "BLENDER" in self.modes: + self.translate_blender_names(rig) + + translator = self.__translator + txt = translator.save_fails() + if translator.fails: + self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(translator.fails), txt.name)) + return {"FINISHED"} + + def translate(self, name_j, name_e): + if not self.overwrite and name_e and self.__translator.is_translated(name_e): + return name_e + if self.allow_fails: + name_e = None + return self.__translator.translate(name_j, name_e) + + def translate_blender_names(self, rig: Model): + if "BONE" in self.types: + for b in rig.armature().pose.bones: + rig.renameBone(b.name, self.translate(b.name, b.name)) + + if "MORPH" in self.types: + for i in (x for x in rig.meshes() if x.data.shape_keys): + for kb in i.data.shape_keys.key_blocks: + kb.name = self.translate(kb.name, kb.name) + + if "MATERIAL" in self.types: + for m in (x for x in rig.materials() if x): + m.name = self.translate(m.name, m.name) + + if "DISPLAY" in self.types: + g: bpy.types.BoneCollection + for g in cast(bpy.types.Armature, rig.armature().data).collections: + g.name = self.translate(g.name, g.name) + + if "PHYSICS" in self.types: + for i in rig.rigidBodies(): + i.name = self.translate(i.name, i.name) + + for i in rig.joints(): + i.name = self.translate(i.name, i.name) + + if "INFO" in self.types: + objects = [rig.rootObject(), rig.armature()] + objects.extend(rig.meshes()) + for i in objects: + i.name = self.translate(i.name, i.name) + + def translate_info(self, rig): + mmd_root = rig.rootObject().mmd_root + mmd_root.name_e = self.translate(mmd_root.name, mmd_root.name_e) + + comment_text = bpy.data.texts.get(mmd_root.comment_text, None) + comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None) + if comment_text and comment_e_text: + comment_e = self.translate(comment_text.as_string(), comment_e_text.as_string()) + comment_e_text.from_string(comment_e) + + def translate_bone(self, rig): + bones = rig.armature().pose.bones + for b in bones: + if b.is_mmd_shadow_bone: + continue + b.mmd_bone.name_e = self.translate(b.mmd_bone.name_j, b.mmd_bone.name_e) + + def translate_morph(self, rig): + mmd_root = rig.rootObject().mmd_root + attr_list = ("group", "vertex", "bone", "uv", "material") + prefix_list = ("G_", "", "B_", "UV_", "M_") + for attr, prefix in zip(attr_list, prefix_list): + for m in getattr(mmd_root, attr + "_morphs", []): + m.name_e = self.translate(m.name, m.name_e) + if not prefix: + continue + if self.use_morph_prefix: + if not m.name_e.startswith(prefix): + m.name_e = prefix + m.name_e + elif m.name_e.startswith(prefix): + m.name_e = m.name_e[len(prefix) :] + + def translate_material(self, rig): + for m in rig.materials(): + if m is None: + continue + m.mmd_material.name_e = self.translate(m.mmd_material.name_j, m.mmd_material.name_e) + + def translate_display(self, rig): + mmd_root = rig.rootObject().mmd_root + for f in mmd_root.display_item_frames: + f.name_e = self.translate(f.name, f.name_e) + + def translate_physics(self, rig): + for i in rig.rigidBodies(): + i.mmd_rigid.name_e = self.translate(i.mmd_rigid.name_j, i.mmd_rigid.name_e) + + for i in rig.joints(): + i.mmd_joint.name_e = self.translate(i.mmd_joint.name_j, i.mmd_joint.name_e) + + +DEFAULT_SHOW_ROW_COUNT = 20 + + +class MMD_TOOLS_UL_MMDTranslationElementIndex(bpy.types.UIList): + def draw_item(self, context, layout: bpy.types.UILayout, data, mmd_translation_element_index: "MMDTranslationElementIndex", icon, active_data, active_propname, index: int): + mmd_translation_element: "MMDTranslationElement" = data.translation_elements[mmd_translation_element_index.value] + MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(layout, mmd_translation_element, index) + + +class RestoreMMDDataReferenceOperator(bpy.types.Operator): + bl_idname = "mmd_tools.restore_mmd_translation_element_name" + bl_label = "Restore this Name" + bl_options = {"INTERNAL"} + + index: bpy.props.IntProperty() + prop_name: bpy.props.StringProperty() + restore_value: bpy.props.StringProperty() + + def execute(self, context: bpy.types.Context): + root_object = FnModel.find_root_object(context.object) + mmd_translation_element_index = root_object.mmd_root.translation.filtered_translation_element_indices[self.index].value + mmd_translation_element = root_object.mmd_root.translation.translation_elements[mmd_translation_element_index] + setattr(mmd_translation_element, self.prop_name, self.restore_value) + + return {"FINISHED"} + + +class GlobalTranslationPopup(bpy.types.Operator): + bl_idname = "mmd_tools.global_translation_popup" + bl_label = "Global Translation Popup" + bl_options = {"INTERNAL", "UNDO"} + + @classmethod + def poll(cls, context): + return FnModel.find_root_object(context.object) is not None + + def draw(self, _context): + layout = self.layout + mmd_translation = self._mmd_translation + + col = layout.column(align=True) + col.label(text="Filter", icon="FILTER") + row = col.row() + row.prop(mmd_translation, "filter_types") + + group = row.row(align=True, heading="is Blank:") + group.alignment = "RIGHT" + group.prop(mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese") + group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English") + + group = row.row(align=True) + group.prop(mmd_translation, "filter_restorable", toggle=True, icon="FILE_REFRESH", icon_only=True) + group.prop(mmd_translation, "filter_selected", toggle=True, icon="RESTRICT_SELECT_OFF", icon_only=True) + group.prop(mmd_translation, "filter_visible", toggle=True, icon="HIDE_OFF", icon_only=True) + + col = layout.column(align=True) + box = col.box().column(align=True) + row = box.row(align=True) + row.label(text="Select the target column for Batch Operations:", icon="TRACKER") + row = box.row(align=True) + row.label(text="", icon="BLANK1") + row.prop(mmd_translation, "batch_operation_target", expand=True) + row.label(text="", icon="RESTRICT_SELECT_OFF") + row.label(text="", icon="HIDE_OFF") + + if len(mmd_translation.filtered_translation_element_indices) > DEFAULT_SHOW_ROW_COUNT: + row.label(text="", icon="BLANK1") + + col.template_list( + "MMD_TOOLS_UL_MMDTranslationElementIndex", + "", + mmd_translation, + "filtered_translation_element_indices", + mmd_translation, + "filtered_translation_element_indices_active_index", + rows=DEFAULT_SHOW_ROW_COUNT, + ) + + box = layout.box().column(align=True) + box.label(text="Batch Operation:", icon="MODIFIER") + box.prop(mmd_translation, "batch_operation_script", text="", icon="SCRIPT") + + box.separator() + row = box.row() + row.prop(mmd_translation, "batch_operation_script_preset", text="Preset", icon="CON_TRANSFORM_CACHE") + row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute") + + box.separator() + translation_box = box.box().column(align=True) + translation_box.label(text="Dictionaries:", icon="HELP") + row = translation_box.row() + row.prop(mmd_translation, "dictionary", text="to_english") + # row.operator(ExecuteTranslationScriptOperator.bl_idname, text='Write to .csv') + + translation_box.separator() + row = translation_box.row() + row.prop(mmd_translation, "dictionary", text="replace") + + def invoke(self, context: bpy.types.Context, _event): + root_object = FnModel.find_root_object(context.object) + if root_object is None: + return {"CANCELLED"} + + mmd_translation: "MMDTranslation" = root_object.mmd_root.translation + self._mmd_translation = mmd_translation + FnTranslations.clear_data(mmd_translation) + FnTranslations.collect_data(mmd_translation) + FnTranslations.update_query(mmd_translation) + + return context.window_manager.invoke_props_dialog(self, width=800) + + def execute(self, context): + root_object = FnModel.find_root_object(context.object) + if root_object is None: + return {"CANCELLED"} + + FnTranslations.apply_translations(root_object) + FnTranslations.clear_data(root_object.mmd_root.translation) + + return {"FINISHED"} + + +class ExecuteTranslationBatchOperator(bpy.types.Operator): + bl_idname = "mmd_tools.execute_translation_batch" + bl_label = "Execute Translation Batch" + bl_options = {"INTERNAL"} + + def execute(self, context: bpy.types.Context): + root = FnModel.find_root_object(context.object) + if root is None: + return {"CANCELLED"} + + fails, text = FnTranslations.execute_translation_batch(root) + if fails: + self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(fails), text.name)) + + return {"FINISHED"} diff --git a/core/mmd/operators/view.py b/core/mmd/operators/view.py new file mode 100644 index 0000000..0072312 --- /dev/null +++ b/core/mmd/operators/view.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import re + +from bpy.types import Operator +from mathutils import Matrix + + +class _SetShadingBase: + bl_options = {"REGISTER", "UNDO"} + + @staticmethod + def _get_view3d_spaces(context): + if getattr(context.area, "type", None) == "VIEW_3D": + return (context.area.spaces[0],) + return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D") + + @staticmethod + def _reset_color_management(context, use_display_device=True): + try: + context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device] + except TypeError: + pass + + @staticmethod + def _reset_material_shading(context, use_shadeless=False): + for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"): + for s in i.material_slots: + if s.material is None: + continue + s.material.use_nodes = False + s.material.use_shadeless = use_shadeless + + def execute(self, context): + context.scene.render.engine = "BLENDER_EEVEE_NEXT" + + shading_mode = getattr(self, "_shading_mode", None) + for space in self._get_view3d_spaces(context): + shading = space.shading + shading.type = "SOLID" + shading.light = "FLAT" if shading_mode == "SHADELESS" else "STUDIO" + shading.color_type = "TEXTURE" if shading_mode else "MATERIAL" + shading.show_object_outline = False + shading.show_backface_culling = False + return {"FINISHED"} + + +class SetGLSLShading(Operator, _SetShadingBase): + bl_idname = "mmd_tools.set_glsl_shading" + bl_label = "GLSL View" + bl_description = "Use GLSL shading with additional lighting" + + _shading_mode = "GLSL" + + +class SetShadelessGLSLShading(Operator, _SetShadingBase): + bl_idname = "mmd_tools.set_shadeless_glsl_shading" + bl_label = "Shadeless GLSL View" + bl_description = "Use only toon shading" + + _shading_mode = "SHADELESS" + + +class ResetShading(Operator, _SetShadingBase): + bl_idname = "mmd_tools.reset_shading" + bl_label = "Reset View" + bl_description = "Reset to default Blender shading" + + +class FlipPose(Operator): + bl_idname = "mmd_tools.flip_pose" + bl_label = "Flip Pose" + bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis." + bl_options = {"REGISTER", "UNDO"} + + # https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html + __LR_REGEX = [ + {"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1}, + {"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2}, + {"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0}, + {"re": re.compile(r"^(L|R)([\.\- _])(.+)$", re.IGNORECASE), "lr": 0}, + {"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1}, + {"re": re.compile(r"^(左|右)(.+)$"), "lr": 0}, + ] + __LR_MAP = { + "RIGHT": "LEFT", + "Right": "Left", + "right": "left", + "LEFT": "RIGHT", + "Left": "Right", + "left": "right", + "L": "R", + "l": "r", + "R": "L", + "r": "l", + "左": "右", + "右": "左", + } + + @classmethod + def flip_name(cls, name): + for regex in cls.__LR_REGEX: + match = regex["re"].match(name) + if match: + groups = match.groups() + lr = groups[regex["lr"]] + if lr in cls.__LR_MAP: + flip_lr = cls.__LR_MAP[lr] + name = "" + for i, s in enumerate(groups): + if i == regex["lr"]: + name += flip_lr + elif s: + name += s + return name + return "" + + @staticmethod + def __cmul(vec1, vec2): + return type(vec1)([x * y for x, y in zip(vec1, vec2)]) + + @staticmethod + def __matrix_compose(loc, rot, scale): + return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)]) + + @classmethod + def __flip_pose(cls, matrix_basis, bone_src, bone_dest): + from mathutils import Quaternion + + m = bone_dest.bone.matrix_local.to_3x3().transposed() + mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted() + loc, rot, scale = matrix_basis.decompose() + loc = cls.__cmul(mi @ loc, (-1, 1, 1)) + rot = cls.__cmul(Quaternion(mi @ rot.axis, rot.angle).normalized(), (1, 1, -1, -1)) + bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale) + + @classmethod + def poll(cls, context): + return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE" + + def execute(self, context): + pose_bones = context.active_object.pose.bones + for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]: + self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b)) + return {"FINISHED"} diff --git a/core/mmd/properties/__init__.py b/core/mmd/properties/__init__.py index e69de29..9f5926d 100644 --- a/core/mmd/properties/__init__.py +++ b/core/mmd/properties/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import bpy + + +def patch_library_overridable(property: "bpy.props._PropertyDeferred") -> "bpy.props._PropertyDeferred": + """Apply recursively for each mmd_tools property class annotations. + Args: + property: The property to be patched. + + Returns: + The patched property. + """ + property.keywords.setdefault("override", set()).add("LIBRARY_OVERRIDABLE") + + if property.function.__name__ not in {"PointerProperty", "CollectionProperty"}: + return property + + property_type = property.keywords["type"] + # The __annotations__ cannot be inherited. Manually search for base classes. + for inherited_type in (property_type, *property_type.__bases__): + if not inherited_type.__module__.startswith("mmd_tools.properties"): + continue + for annotation in inherited_type.__annotations__.values(): + if not isinstance(annotation, bpy.props._PropertyDeferred): + continue + patch_library_overridable(annotation) + + return property diff --git a/core/mmd/properties/material.py b/core/mmd/properties/material.py new file mode 100644 index 0000000..d3df3a3 --- /dev/null +++ b/core/mmd/properties/material.py @@ -0,0 +1,287 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import bpy + +from .. import utils +from ..core import material +from ..core.material import FnMaterial +from ..core.model import FnModel +from . import patch_library_overridable + + +def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_ambient_color() + + +def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_diffuse_color() + + +def _mmd_material_update_alpha(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_alpha() + + +def _mmd_material_update_specular_color(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_specular_color() + + +def _mmd_material_update_shininess(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_shininess() + + +def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_is_double_sided() + + +def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context): + FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object) + + +def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_toon_texture() + + +def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_drop_shadow() + + +def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_self_shadow_map() + + +def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_self_shadow() + + +def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_enabled_toon_edge() + + +def _mmd_material_update_edge_color(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_edge_color() + + +def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context): + FnMaterial(prop.id_data).update_edge_weight() + + +def _mmd_material_get_name_j(prop: "MMDMaterial"): + return prop.get("name_j", "") + + +def _mmd_material_set_name_j(prop: "MMDMaterial", value: str): + prop_value = value + if prop_value and prop_value != prop.get("name_j"): + root = FnModel.find_root_object(bpy.context.active_object) + if root is None: + prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials}) + else: + prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)}) + + prop["name_j"] = prop_value + + +# =========================================== +# Property classes +# =========================================== + + +class MMDMaterial(bpy.types.PropertyGroup): + """マテリアル""" + + name_j: bpy.props.StringProperty( + name="Name", + description="Japanese Name", + default="", + set=_mmd_material_set_name_j, + get=_mmd_material_get_name_j, + ) + + name_e: bpy.props.StringProperty( + name="Name(Eng)", + description="English Name", + default="", + ) + + material_id: bpy.props.IntProperty( + name="Material ID", + description="Unique ID for the reference of material morph", + default=-1, + min=-1, + ) + + ambient_color: bpy.props.FloatVectorProperty( + name="Ambient Color", + description="Ambient color", + subtype="COLOR", + size=3, + min=0, + max=1, + precision=3, + step=0.1, + default=[0.4, 0.4, 0.4], + update=_mmd_material_update_ambient_color, + ) + + diffuse_color: bpy.props.FloatVectorProperty( + name="Diffuse Color", + description="Diffuse color", + subtype="COLOR", + size=3, + min=0, + max=1, + precision=3, + step=0.1, + default=[0.8, 0.8, 0.8], + update=_mmd_material_update_diffuse_color, + ) + + alpha: bpy.props.FloatProperty( + name="Alpha", + description="Alpha transparency", + min=0, + max=1, + precision=3, + step=0.1, + default=1.0, + update=_mmd_material_update_alpha, + ) + + specular_color: bpy.props.FloatVectorProperty( + name="Specular Color", + description="Specular color", + subtype="COLOR", + size=3, + min=0, + max=1, + precision=3, + step=0.1, + default=[0.625, 0.625, 0.625], + update=_mmd_material_update_specular_color, + ) + + shininess: bpy.props.FloatProperty( + name="Reflect", + description="Sharpness of reflected highlights", + min=0, + soft_max=512, + step=100.0, + default=50.0, + update=_mmd_material_update_shininess, + ) + + is_double_sided: bpy.props.BoolProperty( + name="Double Sided", + description="Both sides of mesh should be rendered", + default=False, + update=_mmd_material_update_is_double_sided, + ) + + enabled_drop_shadow: bpy.props.BoolProperty( + name="Ground Shadow", + description="Display ground shadow", + default=True, + update=_mmd_material_update_enabled_drop_shadow, + ) + + enabled_self_shadow_map: bpy.props.BoolProperty( + name="Self Shadow Map", + description="Object can become shadowed by other objects", + default=True, + update=_mmd_material_update_enabled_self_shadow_map, + ) + + enabled_self_shadow: bpy.props.BoolProperty( + name="Self Shadow", + description="Object can cast shadows", + default=True, + update=_mmd_material_update_enabled_self_shadow, + ) + + enabled_toon_edge: bpy.props.BoolProperty( + name="Toon Edge", + description="Use toon edge", + default=False, + update=_mmd_material_update_enabled_toon_edge, + ) + + edge_color: bpy.props.FloatVectorProperty( + name="Edge Color", + description="Toon edge color", + subtype="COLOR", + size=4, + min=0, + max=1, + precision=3, + step=0.1, + default=[0, 0, 0, 1], + update=_mmd_material_update_edge_color, + ) + + edge_weight: bpy.props.FloatProperty( + name="Edge Weight", + description="Toon edge size", + min=0, + max=100, + soft_max=2, + step=1.0, + default=1.0, + update=_mmd_material_update_edge_weight, + ) + + sphere_texture_type: bpy.props.EnumProperty( + name="Sphere Map Type", + description="Choose sphere texture blend type", + items=[ + (str(material.SPHERE_MODE_OFF), "Off", "", 1), + (str(material.SPHERE_MODE_MULT), "Multiply", "", 2), + (str(material.SPHERE_MODE_ADD), "Add", "", 3), + (str(material.SPHERE_MODE_SUBTEX), "SubTexture", "", 4), + ], + update=_mmd_material_update_sphere_texture_type, + ) + + is_shared_toon_texture: bpy.props.BoolProperty( + name="Use Shared Toon Texture", + description="Use shared toon texture or custom toon texture", + default=False, + update=_mmd_material_update_toon_texture, + ) + + toon_texture: bpy.props.StringProperty( + name="Toon Texture", + subtype="FILE_PATH", + description="The file path of custom toon texture", + default="", + update=_mmd_material_update_toon_texture, + ) + + shared_toon_texture: bpy.props.IntProperty( + name="Shared Toon Texture", + description="Shared toon texture id (toon01.bmp ~ toon10.bmp)", + default=0, + min=0, + max=9, + update=_mmd_material_update_toon_texture, + ) + + comment: bpy.props.StringProperty( + name="Comment", + description="Comment", + ) + + def is_id_unique(self): + return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None) + + @staticmethod + def register(): + bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial)) + + @staticmethod + def unregister(): + del bpy.types.Material.mmd_material diff --git a/core/mmd/properties/morph.py b/core/mmd/properties/morph.py new file mode 100644 index 0000000..ba94350 --- /dev/null +++ b/core/mmd/properties/morph.py @@ -0,0 +1,488 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import bpy + +from .. import utils +from ..core.bone import FnBone +from ..core.material import FnMaterial +from ..core.model import FnModel, Model +from ..core.morph import FnMorph + + +def _morph_base_get_name(prop: "_MorphBase") -> str: + return prop.get("name", "") + + +def _morph_base_set_name(prop: "_MorphBase", value: str): + mmd_root = prop.id_data.mmd_root + # morph_type = mmd_root.active_morph_type + morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower() + # assert(prop.bl_rna.identifier.endswith('Morph')) + # logging.debug('_set_name: %s %s %s', prop, value, morph_type) + prop_name = prop.get("name", None) + if prop_name == value: + return + + used_names = {x.name for x in getattr(mmd_root, morph_type) if x != prop} + value = utils.unique_name(value, used_names) + if prop_name is not None: + if morph_type == "vertex_morphs": + kb_list = {} + for mesh in FnModel.iterate_mesh_objects(prop.id_data): + for kb in getattr(mesh.data.shape_keys, "key_blocks", ()): + kb_list.setdefault(kb.name, []).append(kb) + + if prop_name in kb_list: + value = utils.unique_name(value, used_names | kb_list.keys()) + for kb in kb_list[prop_name]: + kb.name = value + + elif morph_type == "uv_morphs": + vg_list = {} + for mesh in FnModel.iterate_mesh_objects(prop.id_data): + for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh): + vg_list.setdefault(n, []).append(vg) + + if prop_name in vg_list: + value = utils.unique_name(value, used_names | vg_list.keys()) + for vg in vg_list[prop_name]: + vg.name = vg.name.replace(prop_name, value) + + if 1: # morph_type != 'group_morphs': + for m in mmd_root.group_morphs: + for d in m.data: + if d.name == prop_name and d.morph_type == morph_type: + d.name = value + + frame_facial = mmd_root.display_item_frames.get("表情") + for item in getattr(frame_facial, "data", []): + if item.name == prop_name and item.morph_type == morph_type: + item.name = value + break + + obj = Model(prop.id_data).morph_slider.placeholder() + if obj and value not in obj.data.shape_keys.key_blocks: + kb = obj.data.shape_keys.key_blocks.get(prop_name, None) + if kb: + kb.name = value + + prop["name"] = value + + +class _MorphBase: + name: bpy.props.StringProperty( + name="Name", + description="Japanese Name", + set=_morph_base_set_name, + get=_morph_base_get_name, + ) + name_e: bpy.props.StringProperty( + name="Name(Eng)", + description="English Name", + default="", + ) + category: bpy.props.EnumProperty( + name="Category", + description="Select category", + items=[ + ("SYSTEM", "Hidden", "", 0), + ("EYEBROW", "Eye Brow", "", 1), + ("EYE", "Eye", "", 2), + ("MOUTH", "Mouth", "", 3), + ("OTHER", "Other", "", 4), + ], + default="OTHER", + ) + + +def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: + bone_id = prop.get("bone_id", -1) + if bone_id < 0: + return "" + root_object = prop.id_data + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return "" + pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id) + if pose_bone is None: + return "" + return pose_bone.name + + +def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str): + root = prop.id_data + arm = FnModel.find_armature_object(root) + + # Load the library_override file. This function is triggered when loading, but the arm obj cannot be found. + # The arm obj is exist, but the relative relationship has not yet been established. + if arm is None: + return + + if value not in arm.pose.bones.keys(): + prop["bone_id"] = -1 + return + pose_bone = arm.pose.bones[value] + prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) + + +def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context): + if not prop.name.startswith("mmd_bind"): + return + arm = FnModel(prop.id_data).morph_slider.dummy_armature + if arm: + bone = arm.pose.bones.get(prop.name, None) + if bone: + bone.location = prop.location + bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency + + +class BoneMorphData(bpy.types.PropertyGroup): + """ """ + + bone: bpy.props.StringProperty( + name="Bone", + description="Target bone", + set=_bone_morph_data_set_bone, + get=_bone_morph_data_get_bone, + ) + + bone_id: bpy.props.IntProperty( + name="Bone ID", + ) + + location: bpy.props.FloatVectorProperty( + name="Location", + description="Location", + subtype="TRANSLATION", + size=3, + default=[0, 0, 0], + update=_bone_morph_data_update_location_or_rotation, + ) + + rotation: bpy.props.FloatVectorProperty( + name="Rotation", + description="Rotation in quaternions", + subtype="QUATERNION", + size=4, + default=[1, 0, 0, 0], + update=_bone_morph_data_update_location_or_rotation, + ) + + +class BoneMorph(_MorphBase, bpy.types.PropertyGroup): + """Bone Morph""" + + data: bpy.props.CollectionProperty( + name="Morph Data", + type=BoneMorphData, + ) + active_data: bpy.props.IntProperty( + name="Active Bone Data", + min=0, + default=0, + ) + + +def _material_morph_data_get_material(prop: "MaterialMorphData"): + mat_p = prop.get("material_data", None) + if mat_p is not None: + return mat_p.name + return "" + + +def _material_morph_data_set_material(prop: "MaterialMorphData", value: str): + if value not in bpy.data.materials: + prop["material_data"] = None + prop["material_id"] = -1 + else: + mat = bpy.data.materials[value] + fnMat = FnMaterial(mat) + prop["material_data"] = mat + prop["material_id"] = fnMat.material_id + + +def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str): + mesh = FnModel.find_mesh_object_by_name(prop.id_data, value) + if mesh is not None: + prop["related_mesh_data"] = mesh.data + else: + prop["related_mesh_data"] = None + + +def _material_morph_data_get_related_mesh(prop): + mesh_p = prop.get("related_mesh_data", None) + if mesh_p is not None: + return mesh_p.name + return "" + + +def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context): + if not prop.name.startswith("mmd_bind"): + return + from ..core.shader import _MaterialMorph + + mat = prop["material_data"] + if mat is not None: + _MaterialMorph.update_morph_inputs(mat, prop) + else: + for mat in FnModel(prop.id_data).materials(): + _MaterialMorph.update_morph_inputs(mat, prop) + + +class MaterialMorphData(bpy.types.PropertyGroup): + """ """ + + related_mesh: bpy.props.StringProperty( + name="Related Mesh", + description="Stores a reference to the mesh where this morph data belongs to", + set=_material_morph_data_set_related_mesh, + get=_material_morph_data_get_related_mesh, + ) + + related_mesh_data: bpy.props.PointerProperty( + name="Related Mesh Data", + type=bpy.types.Mesh, + ) + + offset_type: bpy.props.EnumProperty(name="Offset Type", description="Select offset type", items=[("MULT", "Multiply", "", 0), ("ADD", "Add", "", 1)], default="ADD") + + material: bpy.props.StringProperty( + name="Material", + description="Target material", + get=_material_morph_data_get_material, + set=_material_morph_data_set_material, + ) + + material_id: bpy.props.IntProperty( + name="Material ID", + default=-1, + ) + + material_data: bpy.props.PointerProperty( + name="Material Data", + type=bpy.types.Material, + ) + + diffuse_color: bpy.props.FloatVectorProperty( + name="Diffuse Color", + description="Diffuse color", + subtype="COLOR", + size=4, + soft_min=0, + soft_max=1, + precision=3, + step=0.1, + default=[0, 0, 0, 1], + update=_material_morph_data_update_modifiable_values, + ) + + specular_color: bpy.props.FloatVectorProperty( + name="Specular Color", + description="Specular color", + subtype="COLOR", + size=3, + soft_min=0, + soft_max=1, + precision=3, + step=0.1, + default=[0, 0, 0], + update=_material_morph_data_update_modifiable_values, + ) + + shininess: bpy.props.FloatProperty( + name="Reflect", + description="Reflect", + soft_min=0, + soft_max=500, + step=100.0, + default=0.0, + update=_material_morph_data_update_modifiable_values, + ) + + ambient_color: bpy.props.FloatVectorProperty( + name="Ambient Color", + description="Ambient color", + subtype="COLOR", + size=3, + soft_min=0, + soft_max=1, + precision=3, + step=0.1, + default=[0, 0, 0], + update=_material_morph_data_update_modifiable_values, + ) + + edge_color: bpy.props.FloatVectorProperty( + name="Edge Color", + description="Edge color", + subtype="COLOR", + size=4, + soft_min=0, + soft_max=1, + precision=3, + step=0.1, + default=[0, 0, 0, 1], + update=_material_morph_data_update_modifiable_values, + ) + + edge_weight: bpy.props.FloatProperty( + name="Edge Weight", + description="Edge weight", + soft_min=0, + soft_max=2, + step=0.1, + default=0, + update=_material_morph_data_update_modifiable_values, + ) + + texture_factor: bpy.props.FloatVectorProperty( + name="Texture factor", + description="Texture factor", + subtype="COLOR", + size=4, + soft_min=0, + soft_max=1, + precision=3, + step=0.1, + default=[0, 0, 0, 1], + update=_material_morph_data_update_modifiable_values, + ) + + sphere_texture_factor: bpy.props.FloatVectorProperty( + name="Sphere Texture factor", + description="Sphere texture factor", + subtype="COLOR", + size=4, + soft_min=0, + soft_max=1, + precision=3, + step=0.1, + default=[0, 0, 0, 1], + update=_material_morph_data_update_modifiable_values, + ) + + toon_texture_factor: bpy.props.FloatVectorProperty( + name="Toon Texture factor", + description="Toon texture factor", + subtype="COLOR", + size=4, + soft_min=0, + soft_max=1, + precision=3, + step=0.1, + default=[0, 0, 0, 1], + update=_material_morph_data_update_modifiable_values, + ) + + +class MaterialMorph(_MorphBase, bpy.types.PropertyGroup): + """Material Morph""" + + data: bpy.props.CollectionProperty( + name="Morph Data", + type=MaterialMorphData, + ) + active_data: bpy.props.IntProperty( + name="Active Material Data", + min=0, + default=0, + ) + + +class UVMorphOffset(bpy.types.PropertyGroup): + """UV Morph Offset""" + + index: bpy.props.IntProperty( + name="Vertex Index", + description="Vertex index", + min=0, + default=0, + ) + offset: bpy.props.FloatVectorProperty( + name="UV Offset", + description="UV offset", + size=4, + # min=-1, + # max=1, + # precision=3, + step=0.1, + default=[0, 0, 0, 0], + ) + + +class UVMorph(_MorphBase, bpy.types.PropertyGroup): + """UV Morph""" + + uv_index: bpy.props.IntProperty( + name="UV Index", + description="UV index (UV, UV1 ~ UV4)", + min=0, + max=4, + default=0, + ) + data_type: bpy.props.EnumProperty( + name="Data Type", + description="Select data type", + items=[ + ("DATA", "Data", "Store offset data in root object (deprecated)", 0), + ("VERTEX_GROUP", "Vertex Group", "Store offset data in vertex groups", 1), + ], + default="DATA", + ) + data: bpy.props.CollectionProperty( + name="Morph Data", + type=UVMorphOffset, + ) + active_data: bpy.props.IntProperty( + name="Active UV Data", + min=0, + default=0, + ) + vertex_group_scale: bpy.props.FloatProperty( + name="Vertex Group Scale", + description='The value scale of "Vertex Group" data type', + precision=3, + step=0.1, + default=1, + ) + + +class GroupMorphOffset(bpy.types.PropertyGroup): + """Group Morph Offset""" + + morph_type: bpy.props.EnumProperty( + name="Morph Type", + description="Select morph type", + items=[ + ("material_morphs", "Material", "Material Morphs", 0), + ("uv_morphs", "UV", "UV Morphs", 1), + ("bone_morphs", "Bone", "Bone Morphs", 2), + ("vertex_morphs", "Vertex", "Vertex Morphs", 3), + ("group_morphs", "Group", "Group Morphs", 4), + ], + default="vertex_morphs", + ) + factor: bpy.props.FloatProperty(name="Factor", description="Factor", soft_min=0, soft_max=1, precision=3, step=0.1, default=0) + + +class GroupMorph(_MorphBase, bpy.types.PropertyGroup): + """Group Morph""" + + data: bpy.props.CollectionProperty( + name="Morph Data", + type=GroupMorphOffset, + ) + active_data: bpy.props.IntProperty( + name="Active Group Data", + min=0, + default=0, + ) + + +class VertexMorph(_MorphBase, bpy.types.PropertyGroup): + """Vertex Morph""" diff --git a/core/mmd/properties/pose_bone.py b/core/mmd/properties/pose_bone.py index 7795325..3584c42 100644 --- a/core/mmd/properties/pose_bone.py +++ b/core/mmd/properties/pose_bone.py @@ -1,41 +1,33 @@ # -*- coding: utf-8 -*- # Copyright 2014 MMD Tools authors -# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit. -# All credit goes to the original authors. -# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed. -# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under. -# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/ +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. +from typing import cast import bpy -from bpy.types import PropertyGroup, Context, PoseBone -from bpy.props import ( - StringProperty, - IntProperty, - BoolProperty, - FloatProperty, - FloatVectorProperty -) -from ..logging_setup import logger -from ..bone import FnBone +from ..core.bone import FnBone +from . import patch_library_overridable -def _mmd_bone_update_additional_transform(prop, context: Context): - """Update handler for additional transform properties""" + +def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context): prop["is_additional_transform_dirty"] = True p_bone = context.active_pose_bone if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer(): FnBone.apply_additional_transformation(prop.id_data) -def _mmd_bone_update_additional_transform_influence(prop, context: Context): - """Update handler for additional transform influence""" + +def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context): pose_bone = context.active_pose_bone if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer(): FnBone.update_additional_transform_influence(pose_bone) else: prop["is_additional_transform_dirty"] = True -def _mmd_bone_get_additional_transform_bone(prop): - """Getter for additional transform bone property""" + +def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"): arm = prop.id_data bone_id = prop.get("additional_transform_bone_id", -1) if bone_id < 0: @@ -45,8 +37,8 @@ def _mmd_bone_get_additional_transform_bone(prop): return "" return pose_bone.name -def _mmd_bone_set_additional_transform_bone(prop, value: str): - """Setter for additional transform bone property""" + +def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str): arm = prop.id_data prop["is_additional_transform_dirty"] = True if value not in arm.pose.bones.keys(): @@ -55,85 +47,70 @@ def _mmd_bone_set_additional_transform_bone(prop, value: str): pose_bone = arm.pose.bones[value] prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) -def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context): - """Update handler for IK toggle property""" - v = prop.mmd_ik_toggle - armature_object = prop.id_data - for b in armature_object.pose.bones: - for c in b.constraints: - if c.type == "IK" and c.subtarget == prop.name: - logger.debug('Updating IK constraint %s on bone %s', c.name, b.name) - c.influence = v - b_chain = b if c.use_tail else b.parent - for chain_bone in ([b_chain] + b_chain.parent_recursive)[:c.chain_count]: - limit_c = next((c for c in chain_bone.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None) - if limit_c: - limit_c.influence = v -class MMDBone(PropertyGroup): - """Property group for MMD bone properties""" - name_j: StringProperty( +class MMDBone(bpy.types.PropertyGroup): + name_j: bpy.props.StringProperty( name="Name", description="Japanese Name", default="", ) - - name_e: StringProperty( + + name_e: bpy.props.StringProperty( name="Name(Eng)", description="English Name", default="", ) - - bone_id: IntProperty( + + bone_id: bpy.props.IntProperty( name="Bone ID", description="Unique ID for the reference of bone morph and rotate+/move+", default=-1, min=-1, ) - - transform_order: IntProperty( + + transform_order: bpy.props.IntProperty( name="Transform Order", description="Deformation tier", min=0, max=100, soft_max=7, ) - - is_controllable: BoolProperty( + + is_controllable: bpy.props.BoolProperty( name="Controllable", description="Is controllable", default=True, ) - - transform_after_dynamics: BoolProperty( + + transform_after_dynamics: bpy.props.BoolProperty( name="After Dynamics", description="After physics", default=False, ) - - enabled_fixed_axis: BoolProperty( + + enabled_fixed_axis: bpy.props.BoolProperty( name="Fixed Axis", description="Use fixed axis", default=False, ) - - fixed_axis: FloatVectorProperty( + + fixed_axis: bpy.props.FloatVectorProperty( name="Fixed Axis", description="Fixed axis", subtype="XYZ", size=3, precision=3, - step=0.1, + step=0.1, # 0.1 / 100 default=[0, 0, 0], ) - - enabled_local_axes: BoolProperty( + + enabled_local_axes: bpy.props.BoolProperty( name="Local Axes", description="Use local axes", default=False, ) - - local_axis_x: FloatVectorProperty( + + local_axis_x: bpy.props.FloatVectorProperty( name="Local X-Axis", description="Local x-axis", subtype="XYZ", @@ -142,8 +119,8 @@ class MMDBone(PropertyGroup): step=0.1, default=[1, 0, 0], ) - - local_axis_z: FloatVectorProperty( + + local_axis_z: bpy.props.FloatVectorProperty( name="Local Z-Axis", description="Local z-axis", subtype="XYZ", @@ -152,14 +129,14 @@ class MMDBone(PropertyGroup): step=0.1, default=[0, 0, 1], ) - - is_tip: BoolProperty( + + is_tip: bpy.props.BoolProperty( name="Tip Bone", description="Is zero length bone", default=False, ) - - ik_rotation_constraint: FloatProperty( + + ik_rotation_constraint: bpy.props.FloatProperty( name="IK Rotation Constraint", description="The unit angle of IK", subtype="ANGLE", @@ -167,36 +144,36 @@ class MMDBone(PropertyGroup): soft_max=4, default=1, ) - - has_additional_rotation: BoolProperty( + + has_additional_rotation: bpy.props.BoolProperty( name="Additional Rotation", description="Additional rotation", default=False, update=_mmd_bone_update_additional_transform, ) - - has_additional_location: BoolProperty( + + has_additional_location: bpy.props.BoolProperty( name="Additional Location", description="Additional location", default=False, update=_mmd_bone_update_additional_transform, ) - - additional_transform_bone: StringProperty( + + additional_transform_bone: bpy.props.StringProperty( name="Additional Transform Bone", description="Additional transform bone", set=_mmd_bone_set_additional_transform_bone, get=_mmd_bone_get_additional_transform_bone, update=_mmd_bone_update_additional_transform, ) - - additional_transform_bone_id: IntProperty( + + additional_transform_bone_id: bpy.props.IntProperty( name="Additional Transform Bone ID", default=-1, update=_mmd_bone_update_additional_transform, ) - - additional_transform_influence: FloatProperty( + + additional_transform_influence: bpy.props.FloatProperty( name="Additional Transform Influence", description="Additional transform influence", default=1, @@ -204,47 +181,44 @@ class MMDBone(PropertyGroup): soft_max=1, update=_mmd_bone_update_additional_transform_influence, ) - - is_additional_transform_dirty: BoolProperty( - name="", - default=True - ) - + + is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True) + def is_id_unique(self): - """Check if the bone ID is unique""" return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None) + @staticmethod + def register(): + bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone)) + bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False)) + bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type")) + bpy.types.PoseBone.mmd_ik_toggle = patch_library_overridable( + bpy.props.BoolProperty( + name="MMD IK Toggle", + description="MMD IK toggle is used to import/export animation of IK on-off", + update=_pose_bone_update_mmd_ik_toggle, + default=True, + ) + ) -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, - ) + @staticmethod + def unregister(): + del bpy.types.PoseBone.mmd_ik_toggle + del bpy.types.PoseBone.mmd_shadow_bone_type + del bpy.types.PoseBone.is_mmd_shadow_bone + del bpy.types.PoseBone.mmd_bone -def 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) \ No newline at end of file +def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context): + v = prop.mmd_ik_toggle + armature_object = cast(bpy.types.Object, prop.id_data) + for b in armature_object.pose.bones: + for c in b.constraints: + if c.type == "IK" and c.subtarget == prop.name: + # logging.debug(' %s %s', b.name, c.name) + c.influence = v + b = b if c.use_tail else b.parent + for b in ([b] + b.parent_recursive)[: c.chain_count]: + c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None) + if c: + c.influence = v diff --git a/core/mmd/properties/rigid_body.py b/core/mmd/properties/rigid_body.py new file mode 100644 index 0000000..3941657 --- /dev/null +++ b/core/mmd/properties/rigid_body.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +"""Properties for rigid bodies and joints""" + +import bpy + +from .. import bpyutils +from ..core import rigid_body +from ..core.rigid_body import RigidBodyMaterial, FnRigidBody +from ..core.model import FnModel +from . import patch_library_overridable + + +def _updateCollisionGroup(prop, _context): + obj = prop.id_data + materials = obj.data.materials + if len(materials) == 0: + materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number)) + else: + obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number) + + +def _updateType(prop, _context): + obj = prop.id_data + rb = obj.rigid_body + if rb: + rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC + + +def _updateShape(prop, _context): + obj = prop.id_data + + if len(obj.data.vertices) > 0: + size = prop.size + prop.size = size # update mesh + + rb = obj.rigid_body + if rb: + rb.collision_shape = prop.shape + + +def _get_bone(prop): + obj = prop.id_data + relation = obj.constraints.get("mmd_tools_rigid_parent", None) + if relation: + arm = relation.target + bone_name = relation.subtarget + if arm is not None and bone_name in arm.data.bones: + return bone_name + return prop.get("bone", "") + + +def _set_bone(prop, value): + bone_name = value + obj = prop.id_data + relation = obj.constraints.get("mmd_tools_rigid_parent", None) + if relation is None: + relation = obj.constraints.new("CHILD_OF") + relation.name = "mmd_tools_rigid_parent" + relation.mute = True + + arm = relation.target + if arm is None: + root = FnModel.find_root_object(obj) + if root: + arm = relation.target = FnModel.find_armature_object(root) + + if arm is not None and bone_name in arm.data.bones: + relation.subtarget = bone_name + else: + relation.subtarget = bone_name = "" + + prop["bone"] = bone_name + + +def _get_size(prop): + if prop.id_data.mmd_type != "RIGID_BODY": + return (0, 0, 0) + return FnRigidBody.get_rigid_body_size(prop.id_data) + + +def _set_size(prop, value): + obj = prop.id_data + assert obj.mode == "OBJECT" # not support other mode yet + shape = prop.shape + + mesh = obj.data + rb = obj.rigid_body + + if len(mesh.vertices) == 0 or rb is None or rb.collision_shape != shape: + if shape == "SPHERE": + bpyutils.makeSphere( + radius=value[0], + target_object=obj, + ) + elif shape == "BOX": + bpyutils.makeBox( + size=value, + target_object=obj, + ) + elif shape == "CAPSULE": + bpyutils.makeCapsule( + radius=value[0], + height=value[1], + target_object=obj, + ) + mesh.update() + if rb: + rb.collision_shape = shape + else: + if shape == "SPHERE": + radius = max(value[0], 1e-3) + for v in mesh.vertices: + vec = v.co.normalized() + v.co = vec * radius + elif shape == "BOX": + x = max(value[0], 1e-3) + y = max(value[1], 1e-3) + z = max(value[2], 1e-3) + for v in mesh.vertices: + x0, y0, z0 = v.co + x0 = -x if x0 < 0 else x + y0 = -y if y0 < 0 else y + z0 = -z if z0 < 0 else z + v.co = [x0, y0, z0] + elif shape == "CAPSULE": + r0, h0, xx = FnRigidBody.get_rigid_body_size(prop.id_data) + h0 *= 0.5 + radius = max(value[0], 1e-3) + height = max(value[1], 1e-3) * 0.5 + scale = radius / max(r0, 1e-3) + for v in mesh.vertices: + x0, y0, z0 = v.co + x0 *= scale + y0 *= scale + if z0 < 0: + z0 = (z0 + h0) * scale - height + else: + z0 = (z0 - h0) * scale + height + v.co = [x0, y0, z0] + mesh.update() + + +def _get_rigid_name(prop): + return prop.get("name", "") + + +def _set_rigid_name(prop, value): + prop["name"] = value + + +class MMDRigidBody(bpy.types.PropertyGroup): + name_j: bpy.props.StringProperty( + name="Name", + description="Japanese Name", + default="", + get=_get_rigid_name, + set=_set_rigid_name, + ) + + name_e: bpy.props.StringProperty( + name="Name(Eng)", + description="English Name", + default="", + ) + + collision_group_number: bpy.props.IntProperty( + name="Collision Group", + description="The collision group of the object", + min=0, + max=15, + default=1, + update=_updateCollisionGroup, + ) + + collision_group_mask: bpy.props.BoolVectorProperty( + name="Collision Group Mask", + description="The groups the object can not collide with", + size=16, + subtype="LAYER", + ) + + type: bpy.props.EnumProperty( + name="Rigid Type", + description="Select rigid type", + items=[ + (str(rigid_body.MODE_STATIC), "Bone", "Rigid body's orientation completely determined by attached bone", 1), + (str(rigid_body.MODE_DYNAMIC), "Physics", "Attached bone's orientation completely determined by rigid body", 2), + (str(rigid_body.MODE_DYNAMIC_BONE), "Physics + Bone", "Bone determined by combination of parent and attached rigid body", 3), + ], + update=_updateType, + ) + + shape: bpy.props.EnumProperty( + name="Shape", + description="Select the collision shape", + items=[ + ("SPHERE", "Sphere", "", 1), + ("BOX", "Box", "", 2), + ("CAPSULE", "Capsule", "", 3), + ], + update=_updateShape, + ) + + bone: bpy.props.StringProperty( + name="Bone", + description="Target bone", + default="", + get=_get_bone, + set=_set_bone, + ) + + size: bpy.props.FloatVectorProperty( + name="Size", + description="Size of the object", + subtype="XYZ", + size=3, + min=0, + step=0.1, + get=_get_size, + set=_set_size, + ) + + @staticmethod + def register(): + bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody)) + + @staticmethod + def unregister(): + del bpy.types.Object.mmd_rigid + + +def _updateSpringLinear(prop, context): + obj = prop.id_data + rbc = obj.rigid_body_constraint + if rbc: + rbc.spring_stiffness_x = prop.spring_linear[0] + rbc.spring_stiffness_y = prop.spring_linear[1] + rbc.spring_stiffness_z = prop.spring_linear[2] + + +def _updateSpringAngular(prop, context): + obj = prop.id_data + rbc = obj.rigid_body_constraint + if rbc and hasattr(rbc, "use_spring_ang_x"): + rbc.spring_stiffness_ang_x = prop.spring_angular[0] + rbc.spring_stiffness_ang_y = prop.spring_angular[1] + rbc.spring_stiffness_ang_z = prop.spring_angular[2] + + +class MMDJoint(bpy.types.PropertyGroup): + name_j: bpy.props.StringProperty( + name="Name", + description="Japanese Name", + default="", + ) + + name_e: bpy.props.StringProperty( + name="Name(Eng)", + description="English Name", + default="", + ) + + spring_linear: bpy.props.FloatVectorProperty( + name="Spring(Linear)", + description="Spring constant of movement", + subtype="XYZ", + size=3, + min=0, + step=0.1, + update=_updateSpringLinear, + ) + + spring_angular: bpy.props.FloatVectorProperty( + name="Spring(Angular)", + description="Spring constant of rotation", + subtype="XYZ", + size=3, + min=0, + step=0.1, + update=_updateSpringAngular, + ) + + @staticmethod + def register(): + bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint)) + + @staticmethod + def unregister(): + del bpy.types.Object.mmd_joint diff --git a/core/mmd/properties/root.py b/core/mmd/properties/root.py index 6423a1e..8188ed1 100644 --- a/core/mmd/properties/root.py +++ b/core/mmd/properties/root.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- # Copyright 2014 MMD Tools authors -# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit. -# All credit goes to the original authors. -# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed. -# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under. -# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/ +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. """Properties for MMD model root object""" @@ -500,26 +499,22 @@ class MMDRoot(bpy.types.PropertyGroup): @staticmethod def __get_select(prop: bpy.types.Object) -> bool: - # TODO: Object.select is deprecated since v4.0.0, use Object.select_get() method instead - # utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead") + utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead") return prop.select_get() @staticmethod def __set_select(prop: bpy.types.Object, value: bool) -> None: - # TODO: Object.select is deprecated since v4.0.0, use Object.select_set() method instead - # utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead") + utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead") prop.select_set(value) @staticmethod def __get_hide(prop: bpy.types.Object) -> bool: - # TODO: Object.hide is deprecated since v4.0.0, use Object.hide_get() method instead - # utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead") + utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead") return prop.hide_get() @staticmethod def __set_hide(prop: bpy.types.Object, value: bool) -> None: - # TODO: Object.hide is deprecated since v4.0.0, use Object.hide_set() method instead - # utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead") + utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead") prop.hide_set(value) if prop.hide_viewport != value: prop.hide_viewport = value @@ -579,4 +574,4 @@ class MMDRoot(bpy.types.PropertyGroup): del bpy.types.Object.hide del bpy.types.Object.select del bpy.types.Object.mmd_root - del bpy.types.Object.mmd_type \ No newline at end of file + del bpy.types.Object.mmd_type diff --git a/core/mmd/properties/translations.py b/core/mmd/properties/translations.py new file mode 100644 index 0000000..a70a9fc --- /dev/null +++ b/core/mmd/properties/translations.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +from typing import Dict, List, Optional, Tuple + +import bpy + +from ..core.translations import FnTranslations, MMDTranslationElementType +from ..translations import DictionaryEnum + +MMD_TRANSLATION_ELEMENT_TYPE_ENUM_ITEMS = [ + (MMDTranslationElementType.BONE.name, MMDTranslationElementType.BONE.value, "Bones", 1), + (MMDTranslationElementType.MORPH.name, MMDTranslationElementType.MORPH.value, "Morphs", 2), + (MMDTranslationElementType.MATERIAL.name, MMDTranslationElementType.MATERIAL.value, "Materials", 4), + (MMDTranslationElementType.DISPLAY.name, MMDTranslationElementType.DISPLAY.value, "Display frames", 8), + (MMDTranslationElementType.PHYSICS.name, MMDTranslationElementType.PHYSICS.value, "Rigidbodies and joints", 16), + (MMDTranslationElementType.INFO.name, MMDTranslationElementType.INFO.value, "Model name and comments", 32), +] + + +class MMDTranslationElement(bpy.types.PropertyGroup): + type: bpy.props.EnumProperty(items=MMD_TRANSLATION_ELEMENT_TYPE_ENUM_ITEMS) + object: bpy.props.PointerProperty(type=bpy.types.Object) + data_path: bpy.props.StringProperty() + name: bpy.props.StringProperty() + name_j: bpy.props.StringProperty() + name_e: bpy.props.StringProperty() + + +class MMDTranslationElementIndex(bpy.types.PropertyGroup): + value: bpy.props.IntProperty() + + +BATCH_OPERATION_SCRIPT_PRESETS: Dict[str, Tuple[Optional[str], str, str, int]] = { + "NOTHING": ("", "", "", 1), + "CLEAR": (None, "Clear", '""', 10), + "TO_ENGLISH": ("BLENDER", "Translate to English", "to_english(name)", 2), + "TO_MMD_LR": ("JAPANESE", "Blender L/R to MMD L/R", "to_mmd_lr(name)", 3), + "TO_BLENDER_LR": ("BLENDER", "MMD L/R to Blender L/R", "to_blender_lr(name_j)", 4), + "RESTORE_BLENDER": ("BLENDER", "Restore Blender Names", "org_name", 5), + "RESTORE_JAPANESE": ("JAPANESE", "Restore Japanese MMD Names", "org_name_j", 6), + "RESTORE_ENGLISH": ("ENGLISH", "Restore English MMD Names", "org_name_e", 7), + "ENGLISH_IF_EMPTY_JAPANESE": (None, "Copy English MMD Names, if empty copy Japanese MMD Name", "name_e if name_e else name_j", 8), + "JAPANESE_IF_EMPTY_ENGLISH": (None, "Copy Japanese MMD Names, if empty copy English MMD Name", "name_j if name_j else name_e", 9), +} + +BATCH_OPERATION_SCRIPT_PRESET_ITEMS: List[Tuple[str, str, str, int]] = [(k, t[1], t[2], t[3]) for k, t in BATCH_OPERATION_SCRIPT_PRESETS.items()] + + +class MMDTranslation(bpy.types.PropertyGroup): + @staticmethod + def _update_index(mmd_translation: "MMDTranslation", _context): + FnTranslations.update_index(mmd_translation) + + @staticmethod + def _collect_data(mmd_translation: "MMDTranslation", _context): + FnTranslations.collect_data(mmd_translation) + + @staticmethod + def _update_query(mmd_translation: "MMDTranslation", _context): + FnTranslations.update_query(mmd_translation) + + @staticmethod + def _update_batch_operation_script_preset(mmd_translation: "MMDTranslation", _context): + if mmd_translation.batch_operation_script_preset == "NOTHING": + return + + id2scripts: Dict[str, str] = {i[0]: i[2] for i in BATCH_OPERATION_SCRIPT_PRESET_ITEMS} + + batch_operation_script = id2scripts.get(mmd_translation.batch_operation_script_preset) + if batch_operation_script is None: + return + + mmd_translation.batch_operation_script = batch_operation_script + batch_operation_target = BATCH_OPERATION_SCRIPT_PRESETS[mmd_translation.batch_operation_script_preset][0] + if batch_operation_target: + mmd_translation.batch_operation_target = batch_operation_target + + translation_elements: bpy.props.CollectionProperty(type=MMDTranslationElement) + filtered_translation_element_indices_active_index: bpy.props.IntProperty(update=_update_index.__func__) + filtered_translation_element_indices: bpy.props.CollectionProperty(type=MMDTranslationElementIndex) + + filter_japanese_blank: bpy.props.BoolProperty(name="Japanese Blank", default=False, update=_update_query.__func__) + filter_english_blank: bpy.props.BoolProperty(name="English Blank", default=False, update=_update_query.__func__) + filter_restorable: bpy.props.BoolProperty(name="Restorable", default=False, update=_update_query.__func__) + filter_selected: bpy.props.BoolProperty(name="Selected", default=False, update=_update_query.__func__) + filter_visible: bpy.props.BoolProperty(name="Visible", default=False, update=_update_query.__func__) + filter_types: bpy.props.EnumProperty( + items=MMD_TRANSLATION_ELEMENT_TYPE_ENUM_ITEMS, + default={ + "BONE", + "MORPH", + "MATERIAL", + "DISPLAY", + "PHYSICS", + }, + options={"ENUM_FLAG"}, + update=_update_query.__func__, + ) + + dictionary: bpy.props.EnumProperty( + items=DictionaryEnum.get_dictionary_items, + name="Dictionary", + ) + + batch_operation_target: bpy.props.EnumProperty( + items=[ + ("BLENDER", "Blender Name (name)", "", 1), + ("JAPANESE", "Japanese MMD Name (name_j)", "", 2), + ("ENGLISH", "English MMD Name (name_e)", "", 3), + ], + name="Operation Target", + default="JAPANESE", + ) + + batch_operation_script_preset: bpy.props.EnumProperty( + items=BATCH_OPERATION_SCRIPT_PRESET_ITEMS, + name="Operation Script Preset", + default="NOTHING", + update=_update_batch_operation_script_preset.__func__, + ) + + batch_operation_script: bpy.props.StringProperty() diff --git a/core/mmd/translations.py b/core/mmd/translations.py new file mode 100644 index 0000000..b7f5e3c --- /dev/null +++ b/core/mmd/translations.py @@ -0,0 +1,461 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. + +import csv +import logging +import time + +import bpy + +from .bpyutils import FnContext + +jp_half_to_full_tuples = ( + ("ヴ", "ヴ"), + ("ガ", "ガ"), + ("ギ", "ギ"), + ("グ", "グ"), + ("ゲ", "ゲ"), + ("ゴ", "ゴ"), + ("ザ", "ザ"), + ("ジ", "ジ"), + ("ズ", "ズ"), + ("ゼ", "ゼ"), + ("ゾ", "ゾ"), + ("ダ", "ダ"), + ("ヂ", "ヂ"), + ("ヅ", "ヅ"), + ("デ", "デ"), + ("ド", "ド"), + ("バ", "バ"), + ("パ", "パ"), + ("ビ", "ビ"), + ("ピ", "ピ"), + ("ブ", "ブ"), + ("プ", "プ"), + ("ベ", "ベ"), + ("ペ", "ペ"), + ("ボ", "ボ"), + ("ポ", "ポ"), + ("。", "。"), + ("「", "「"), + ("」", "」"), + ("、", "、"), + ("・", "・"), + ("ヲ", "ヲ"), + ("ァ", "ァ"), + ("ィ", "ィ"), + ("ゥ", "ゥ"), + ("ェ", "ェ"), + ("ォ", "ォ"), + ("ャ", "ャ"), + ("ュ", "ュ"), + ("ョ", "ョ"), + ("ッ", "ッ"), + ("ー", "ー"), + ("ア", "ア"), + ("イ", "イ"), + ("ウ", "ウ"), + ("エ", "エ"), + ("オ", "オ"), + ("カ", "カ"), + ("キ", "キ"), + ("ク", "ク"), + ("ケ", "ケ"), + ("コ", "コ"), + ("サ", "サ"), + ("シ", "シ"), + ("ス", "ス"), + ("セ", "セ"), + ("ソ", "ソ"), + ("タ", "タ"), + ("チ", "チ"), + ("ツ", "ツ"), + ("テ", "テ"), + ("ト", "ト"), + ("ナ", "ナ"), + ("ニ", "ニ"), + ("ヌ", "ヌ"), + ("ネ", "ネ"), + ("ノ", "ノ"), + ("ハ", "ハ"), + ("ヒ", "ヒ"), + ("フ", "フ"), + ("ヘ", "ヘ"), + ("ホ", "ホ"), + ("マ", "マ"), + ("ミ", "ミ"), + ("ム", "ム"), + ("メ", "メ"), + ("モ", "モ"), + ("ヤ", "ヤ"), + ("ユ", "ユ"), + ("ヨ", "ヨ"), + ("ラ", "ラ"), + ("リ", "リ"), + ("ル", "ル"), + ("レ", "レ"), + ("ロ", "ロ"), + ("ワ", "ワ"), + ("ン", "ン"), +) + +jp_to_en_tuples = [ + ("全ての親", "ParentNode"), + ("操作中心", "ControlNode"), + ("センター", "Center"), + ("センター", "Center"), + ("グループ", "Group"), + ("グルーブ", "Groove"), + ("キャンセル", "Cancel"), + ("上半身", "UpperBody"), + ("下半身", "LowerBody"), + ("手首", "Wrist"), + ("足首", "Ankle"), + ("首", "Neck"), + ("頭", "Head"), + ("顔", "Face"), + ("下顎", "Chin"), + ("下あご", "Chin"), + ("あご", "Jaw"), + ("顎", "Jaw"), + ("両目", "Eyes"), + ("目", "Eye"), + ("眉", "Eyebrow"), + ("舌", "Tongue"), + ("涙", "Tears"), + ("泣き", "Cry"), + ("歯", "Teeth"), + ("照れ", "Blush"), + ("青ざめ", "Pale"), + ("ガーン", "Gloom"), + ("汗", "Sweat"), + ("怒", "Anger"), + ("感情", "Emotion"), + ("符", "Marks"), + ("暗い", "Dark"), + ("腰", "Waist"), + ("髪", "Hair"), + ("三つ編み", "Braid"), + ("胸", "Breast"), + ("乳", "Boob"), + ("おっぱい", "Tits"), + ("筋", "Muscle"), + ("腹", "Belly"), + ("鎖骨", "Clavicle"), + ("肩", "Shoulder"), + ("腕", "Arm"), + ("うで", "Arm"), + ("ひじ", "Elbow"), + ("肘", "Elbow"), + ("手", "Hand"), + ("親指", "Thumb"), + ("人指", "IndexFinger"), + ("人差指", "IndexFinger"), + ("中指", "MiddleFinger"), + ("薬指", "RingFinger"), + ("小指", "LittleFinger"), + ("足", "Leg"), + ("ひざ", "Knee"), + ("つま", "Toe"), + ("袖", "Sleeve"), + ("新規", "New"), + ("ボーン", "Bone"), + ("捩", "Twist"), + ("回転", "Rotation"), + ("軸", "Axis"), + ("ネクタイ", "Necktie"), + ("ネクタイ", "Necktie"), + ("ヘッドセット", "Headset"), + ("飾り", "Accessory"), + ("リボン", "Ribbon"), + ("襟", "Collar"), + ("紐", "String"), + ("コード", "Cord"), + ("イヤリング", "Earring"), + ("メガネ", "Eyeglasses"), + ("眼鏡", "Glasses"), + ("帽子", "Hat"), + ("スカート", "Skirt"), + ("スカート", "Skirt"), + ("パンツ", "Pantsu"), + ("シャツ", "Shirt"), + ("フリル", "Frill"), + ("マフラー", "Muffler"), + ("マフラー", "Muffler"), + ("服", "Clothes"), + ("ブーツ", "Boots"), + ("ねこみみ", "CatEars"), + ("ジップ", "Zip"), + ("ジップ", "Zip"), + ("ダミー", "Dummy"), + ("ダミー", "Dummy"), + ("基", "Category"), + ("あほ毛", "Antenna"), + ("アホ毛", "Antenna"), + ("モミアゲ", "Sideburn"), + ("もみあげ", "Sideburn"), + ("ツインテ", "Twintail"), + ("おさげ", "Pigtail"), + ("ひらひら", "Flutter"), + ("調整", "Adjustment"), + ("補助", "Aux"), + ("右", "Right"), + ("左", "Left"), + ("前", "Front"), + ("後ろ", "Behind"), + ("後", "Back"), + ("横", "Side"), + ("中", "Middle"), + ("上", "Upper"), + ("下", "Lower"), + ("親", "Parent"), + ("先", "Tip"), + ("パーツ", "Part"), + ("光", "Light"), + ("戻", "Return"), + ("羽", "Wing"), + ("根", "Base"), # ideally 'Root' but to avoid confusion + ("毛", "Strand"), + ("尾", "Tail"), + ("尻", "Butt"), + # full-width unicode forms I think: https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms + ("0", "0"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ("6", "6"), + ("7", "7"), + ("8", "8"), + ("9", "9"), + ("a", "a"), + ("b", "b"), + ("c", "c"), + ("d", "d"), + ("e", "e"), + ("f", "f"), + ("g", "g"), + ("h", "h"), + ("i", "i"), + ("j", "j"), + ("k", "k"), + ("l", "l"), + ("m", "m"), + ("n", "n"), + ("o", "o"), + ("p", "p"), + ("q", "q"), + ("r", "r"), + ("s", "s"), + ("t", "t"), + ("u", "u"), + ("v", "v"), + ("w", "w"), + ("x", "x"), + ("y", "y"), + ("z", "z"), + ("A", "A"), + ("B", "B"), + ("C", "C"), + ("D", "D"), + ("E", "E"), + ("F", "F"), + ("G", "G"), + ("H", "H"), + ("I", "I"), + ("J", "J"), + ("K", "K"), + ("L", "L"), + ("M", "M"), + ("N", "N"), + ("O", "O"), + ("P", "P"), + ("Q", "Q"), + ("R", "R"), + ("S", "S"), + ("T", "T"), + ("U", "U"), + ("V", "V"), + ("W", "W"), + ("X", "X"), + ("Y", "Y"), + ("Z", "Z"), + ("+", "+"), + ("-", "-"), + ("_", "_"), + ("/", "/"), + (".", "_"), # probably should be combined with the global 'use underscore' option +] + + +def translateFromJp(name): + for tuple in jp_to_en_tuples: + if tuple[0] in name: + name = name.replace(tuple[0], tuple[1]) + return name + + +def getTranslator(csvfile="", keep_order=False): + translator = MMDTranslator() + if isinstance(csvfile, bpy.types.Text): + translator.load_from_stream(csvfile) + elif isinstance(csvfile, dict): + translator.csv_tuples.extend(csvfile.items()) + elif csvfile in bpy.data.texts.keys(): + translator.load_from_stream(bpy.data.texts[csvfile]) + else: + translator.load(csvfile) + + if not keep_order: + translator.sort() + translator.update() + return translator + + +class MMDTranslator: + def __init__(self): + self.__csv_tuples = [] + self.__fails = {} + + @staticmethod + def default_csv_filepath(): + return __file__[:-3] + ".csv" + + @staticmethod + def get_csv_text(text_name=None): + text_name = text_name or bpy.path.basename(MMDTranslator.default_csv_filepath()) + csv_text = bpy.data.texts.get(text_name, None) + if csv_text is None: + csv_text = bpy.data.texts.new(text_name) + return csv_text + + @staticmethod + def replace_from_tuples(name, tuples): + for pair in tuples: + if pair[0] in name: + name = name.replace(pair[0], pair[1]) + return name + + @property + def csv_tuples(self): + return self.__csv_tuples + + @property + def fails(self): + return self.__fails + + def sort(self): + self.__csv_tuples.sort(key=lambda row: (-len(row[0]), row)) + + def update(self): + from collections import OrderedDict + + count_old = len(self.__csv_tuples) + tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0]) + self.__csv_tuples.clear() + self.__csv_tuples.extend(tuples_dict.values()) + logging.info(" - removed items:\t%d\t(of %d)", count_old - len(self.__csv_tuples), count_old) + + def half_to_full(self, name): + return self.replace_from_tuples(name, jp_half_to_full_tuples) + + def is_translated(self, name): + try: + name.encode("ascii", errors="strict") + except UnicodeEncodeError: + return False + return True + + def translate(self, name, default=None, from_full_width=True): + if from_full_width: + name = self.half_to_full(name) + name_new = self.replace_from_tuples(name, self.__csv_tuples) + if default is not None and not self.is_translated(name_new): + self.__fails[name] = name_new + return default + return name_new + + def save_fails(self, text_name=None): + text_name = text_name or (__name__ + ".fails") + txt = self.get_csv_text(text_name) + fmt = '"%s","%s"' + items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row)) + txt.from_string("\n".join(fmt % (k, v) for k, v in items)) + return txt + + def load_from_stream(self, csvfile=None): + csvfile = csvfile or self.get_csv_text() + if isinstance(csvfile, bpy.types.Text): + csvfile = (l.body + "\n" for l in csvfile.lines) + spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True) + csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2] + self.__csv_tuples = csv_tuples + logging.info(" - load items:\t%d", len(self.__csv_tuples)) + + def save_to_stream(self, csvfile=None): + csvfile = csvfile or self.get_csv_text() + lineterminator = "\r\n" + if isinstance(csvfile, bpy.types.Text): + csvfile.clear() + lineterminator = "\n" + spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL) + spamwriter.writerows(self.__csv_tuples) + logging.info(" - save items:\t%d", len(self.__csv_tuples)) + + def load(self, filepath=None): + filepath = filepath or self.default_csv_filepath() + logging.info("Loading csv file:\t%s", filepath) + with open(filepath, "rt", encoding="utf-8", newline="") as csvfile: + self.load_from_stream(csvfile) + + def save(self, filepath=None): + filepath = filepath or self.default_csv_filepath() + logging.info("Saving csv file:\t%s", filepath) + with open(filepath, "wt", encoding="utf-8", newline="") as csvfile: + self.save_to_stream(csvfile) + + +class DictionaryEnum: + __items_ttl = 0.0 + __items_cache = None + + @staticmethod + def get_dictionary_items(prop, context): + if DictionaryEnum.__items_ttl > time.time(): + return DictionaryEnum.__items_cache + + DictionaryEnum.__items_ttl = time.time() + 5 + DictionaryEnum.__items_cache = items = [] + if "import" in prop.bl_rna.identifier: + items.append(("DISABLED", "Disabled", "", 0)) + + items.append(("INTERNAL", "Internal Dictionary", "The dictionary defined in " + __name__, len(items))) + + for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")): + items.append((txt_name, txt_name, "bpy.data.texts['%s']" % txt_name, "TEXT", len(items))) + + import os + + folder = FnContext.get_addon_preferences_attribute(context, "dictionary_folder", "") + if os.path.isdir(folder): + for filename in sorted(x for x in os.listdir(folder) if x.lower().endswith(".csv")): + filepath = os.path.join(folder, filename) + if os.path.isfile(filepath): + items.append((filepath, filename, filepath, "FILE", len(items))) + + if "dictionary" in prop: + prop["dictionary"] = min(prop["dictionary"], len(items) - 1) + return items + + @staticmethod + def get_translator(dictionary): + if dictionary == "DISABLED": + return None + if dictionary == "INTERNAL": + return getTranslator(dict(jp_to_en_tuples)) + return getTranslator(dictionary) diff --git a/core/mmd/core/utils.py b/core/mmd/utils.py similarity index 52% rename from core/mmd/core/utils.py rename to core/mmd/utils.py index 4a6f5df..c4006ac 100644 --- a/core/mmd/core/utils.py +++ b/core/mmd/utils.py @@ -1,85 +1,75 @@ # -*- coding: utf-8 -*- -# Copyright 2013 MMD Tools authors -# This file was originally part of the MMD Tools project, However Neoneko has added it to Avatar Toolkit. -# All credit goes to the original authors. -# Please note that some code was modified to fit the needs of Avatar Toolkit and some code may of been removed. -# MMD Tools is licensed under the terms of the GPL-3.0 license which Avatar Toolkit is also licensed under. -# You can find MMD Tools at: https://github.com/MMD-Blender/blender_mmd_tools/ +# Copyright 2014 MMD Tools authors +# This file was originally part of the MMD Tools add-on for Blender +# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools +# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. +# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. import logging import os import re -from typing import Callable, Optional, Set, List, Dict, Any +from typing import Callable, Optional, Set import bpy -from bpy.types import Object, Context, Bone, PoseBone -from ...logging_setup import logger from .bpyutils import FnContext -def selectAObject(obj: Object) -> None: - """Select a single object and make it active""" +## 指定したオブジェクトのみを選択状態かつアクティブにする +def selectAObject(obj): try: bpy.ops.object.mode_set(mode="OBJECT") except Exception: - logger.debug(f"Failed to set object mode for {obj.name}") - + pass bpy.ops.object.select_all(action="DESELECT") FnContext.select_object(FnContext.ensure_context(), obj) FnContext.set_active_object(FnContext.ensure_context(), obj) -def enterEditMode(obj: Object) -> None: - """Enter edit mode for the specified object""" +## 現在のモードを指定したオブジェクトのEdit Modeに変更する +def enterEditMode(obj): selectAObject(obj) if obj.mode != "EDIT": bpy.ops.object.mode_set(mode="EDIT") -def setParentToBone(obj: Object, parent: Object, bone_name: str) -> None: - """Set an object's parent to a specific bone""" +def setParentToBone(obj, parent, bone_name): selectAObject(obj) FnContext.set_active_object(FnContext.ensure_context(), parent) bpy.ops.object.mode_set(mode="POSE") parent.data.bones.active = parent.data.bones[bone_name] - bpy.ops.object.parent_set(type="BONE", keep_transform=False) + bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False) bpy.ops.object.mode_set(mode="OBJECT") -def selectSingleBone(context: Context, armature: Object, bone_name: str, reset_pose: bool = False) -> None: - """Select a single bone in an armature""" +def selectSingleBone(context, armature, bone_name, reset_pose=False): try: bpy.ops.object.mode_set(mode="OBJECT") - except Exception: - logger.debug(f"Failed to set object mode for bone selection: {bone_name}") - + except: + pass for i in context.selected_objects: i.select_set(False) - FnContext.set_active_object(context, armature) bpy.ops.object.mode_set(mode="POSE") - if reset_pose: for p_bone in armature.pose.bones: p_bone.matrix_basis.identity() - - armature_bones = armature.data.bones - for bone in armature_bones: - bone.select = bone.name == bone_name - bone.select_head = bone.select_tail = bone.select - if bone.select: - armature_bones.active = bone - bone.hide = False + armature_bones: bpy.types.ArmatureBones = armature.data.bones + i: bpy.types.Bone + for i in armature_bones: + i.select = i.name == bone_name + i.select_head = i.select_tail = i.select + if i.select: + armature_bones.active = i + i.hide = False -# Regular expressions for name conversion __CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$") __CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$") -def convertNameToLR(name: str, use_underscore: bool = False) -> str: - """Convert Japanese left/right naming to Blender's L/R convention""" +## 日本語で左右を命名されている名前をblender方式のL(R)に変更する +def convertNameToLR(name, use_underscore=False): m = __CONVERT_NAME_TO_L_REGEXP.match(name) delimiter = "_" if use_underscore else "." if m: @@ -94,8 +84,7 @@ __CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P(?P[._])[lL])(?P(?P[._])[rR])(?P($|(?P=separator)))") -def convertLRToName(name: str) -> str: - """Convert Blender's L/R convention to Japanese left/right naming""" +def convertLRToName(name): match = __CONVERT_L_TO_NAME_REGEXP.search(name) if match: return f"左{name[0:match.start()]}{match['after']}{name[match.end():]}" @@ -107,8 +96,8 @@ def convertLRToName(name: str) -> str: return name -def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_group_name: str) -> None: - """Merge weights from source vertex group to destination vertex group""" +## src_vertex_groupのWeightをdest_vertex_groupにaddする +def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name): mesh = meshObj.data src_vertex_group = meshObj.vertex_groups[src_vertex_group_name] dest_vertex_group = meshObj.vertex_groups[dest_vertex_group_name] @@ -122,38 +111,30 @@ def mergeVertexGroup(meshObj: Object, src_vertex_group_name: str, dest_vertex_gr pass -def separateByMaterials(meshObj: Object) -> None: - """Separate a mesh object by materials""" +def separateByMaterials(meshObj: bpy.types.Object): if len(meshObj.data.materials) < 2: selectAObject(meshObj) return - matrix_parent_inverse = meshObj.matrix_parent_inverse.copy() prev_parent = meshObj.parent dummy_parent = bpy.data.objects.new(name="tmp", object_data=None) - bpy.context.collection.objects.link(dummy_parent) - meshObj.parent = dummy_parent meshObj.active_shape_key_index = 0 - try: enterEditMode(meshObj) bpy.ops.mesh.select_all(action="SELECT") bpy.ops.mesh.separate(type="MATERIAL") finally: bpy.ops.object.mode_set(mode="OBJECT") - for i in dummy_parent.children: materials = i.data.materials i.name = getattr(materials[0], "name", "None") if len(materials) else "None" i.parent = prev_parent i.matrix_parent_inverse = matrix_parent_inverse - bpy.data.objects.remove(dummy_parent) -def clearUnusedMeshes() -> None: - """Remove unused mesh data blocks""" +def clearUnusedMeshes(): meshes_to_delete = [] for mesh in bpy.data.meshes: if mesh.users == 0: @@ -163,44 +144,72 @@ def clearUnusedMeshes() -> None: bpy.data.meshes.remove(mesh) -def makePmxBoneMap(armObj: Object) -> Dict[str, PoseBone]: - """Create a mapping from bone names to pose bones""" - return {(i.mmd_bone.name_j or i.name): i for i in armObj.pose.bones} +## Boneのカスタムプロパティにname_jが存在する場合、name_jの値を +# それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成 +def makePmxBoneMap(armObj): + # Maintain backward compatibility with mmd_tools v0.4.x or older. + return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones} __REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$") def unique_name(name: str, used_names: Set[str]) -> str: - """Create a unique name that doesn't exist in the used_names set - + """Helper function for storing unique names. + This function is a limited and simplified version of bpy_extras.io_utils.unique_name. + Args: - name (str): The name to make unique - used_names (Set[str]): A set of names that are already used - + name (str): The name to make unique. + used_names (Set[str]): A set of names that are already used. + Returns: - str: The unique name, formatted as "{name}.{number:03d}" + str: The unique name, formatted as "{name}.{number:03d}". """ if name not in used_names: return name - count = 1 new_name = orig_name = __REMOVE_PREFIX_DIGITS_REGEXP.sub("", name) - while new_name in used_names: new_name = f"{orig_name}.{count:03d}" count += 1 - return new_name -def saferelpath(path: str, start: str, strategy: str = "inside") -> str: - """Safely get a relative path, handling different drive issues on Windows - +def int2base(x, base, width=0): + """ + Method to convert an int to a base + Source: http://stackoverflow.com/questions/2267362 + """ + import string + + digs = string.digits + string.ascii_uppercase + assert 2 <= base <= len(digs) + digits, negtive = "", False + if x <= 0: + if x == 0: + return "0" * max(1, width) + x, negtive, width = -x, True, width - 1 + while x: + digits = digs[x % base] + digits + x //= base + digits = "0" * (width - len(digits)) + digits + if negtive: + digits = "-" + digits + return digits + + +def saferelpath(path, start, strategy="inside"): + """ + On Windows relpath will raise a ValueError + when trying to calculate the relative path to a + different drive. + This method will behave different depending on the strategy + choosen to handle the different drive issue. Strategies: - - inside: returns the basename of the path - - outside: prepends '..' to the basename if on different drive - - absolute: returns the absolute path + - inside: this will just return the basename of the path given + - outside: this will prepend '..' to the basename + - absolute: this will return the absolute path instead of a relative. + See http://bugs.python.org/issue7195 """ if strategy == "inside": return os.path.basename(path) @@ -216,20 +225,15 @@ def saferelpath(path: str, start: str, strategy: str = "inside") -> str: return os.path.relpath(path, start) - class ItemOp: - """Operations for managing collections of items""" - @staticmethod - def get_by_index(items: List[Any], index: int) -> Optional[Any]: - """Get an item by index with bounds checking""" + def get_by_index(items, index): if 0 <= index < len(items): return items[index] return None @staticmethod - def resize(items: bpy.types.bpy_prop_collection, length: int) -> None: - """Resize a collection to the specified length""" + def resize(items: bpy.types.bpy_prop_collection, length: int): count = length - len(items) if count > 0: for i in range(count): @@ -239,8 +243,7 @@ class ItemOp: items.remove(length) @staticmethod - def add_after(items: bpy.types.bpy_prop_collection, index: int) -> tuple: - """Add a new item after the specified index""" + def add_after(items, index): index_end = len(items) index = max(0, min(index_end, index + 1)) items.add() @@ -249,28 +252,24 @@ class ItemOp: class ItemMoveOp: - """Operations for moving items in collections""" - + type: bpy.props.EnumProperty( + name="Type", + description="Move type", + items=[ + ("UP", "Up", "", 0), + ("DOWN", "Down", "", 1), + ("TOP", "Top", "", 2), + ("BOTTOM", "Bottom", "", 3), + ], + default="UP", + ) + @staticmethod - def move(items: bpy.types.bpy_prop_collection, index: int, move_type: str, - index_min: int = 0, index_max: Optional[int] = None) -> int: - """Move an item in a collection - - Args: - items: The collection to modify - index: Current index of the item - move_type: Type of move ('UP', 'DOWN', 'TOP', 'BOTTOM') - index_min: Minimum allowed index - index_max: Maximum allowed index - - Returns: - int: The new index after moving - """ + def move(items, index, move_type, index_min=0, index_max=None): if index_max is None: index_max = len(items) - 1 else: index_max = min(index_max, len(items) - 1) - index_min = min(index_min, index_max) if index < index_min: @@ -292,5 +291,44 @@ class ItemMoveOp: if index_new != index: items.move(index, index_new) - return index_new + + +def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None): + """Decorator to mark a function as deprecated. + Args: + deprecated_in (Optional[str]): Version in which the function was deprecated. + details (Optional[str]): Additional details about the deprecation. + Returns: + Callable: The decorated function. + """ + + def _function_wrapper(function: Callable): + def _inner_wrapper(*args, **kwargs): + warn_deprecation(function.__name__, deprecated_in, details) + return function(*args, **kwargs) + + return _inner_wrapper + + return _function_wrapper + + +def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None: + """Reports a deprecation warning. + Args: + function_name (str): Name of the deprecated function. + deprecated_in (Optional[str]): Version in which the function was deprecated. + details (Optional[str]): Additional details about the deprecation. + """ + logging.warning( + "%s is deprecated%s%s", + function_name, + f" since {deprecated_in}" if deprecated_in else "", + f": {details}" if details else "", + stack_info=True, + stacklevel=4, + ) + + # import warnings # pylint: disable=import-outside-toplevel + + # warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2) diff --git a/cycles_converter.py b/cycles_converter.py new file mode 100644 index 0000000..f0d391a --- /dev/null +++ b/cycles_converter.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +# Copyright 2012 MMD Tools authors +# This file is part of MMD Tools. + +from typing import Iterable, Optional + +import bpy + +from .core.shader import _NodeGroupUtils +from .core.material import FnMaterial + + +def __switchToCyclesRenderEngine(): + if bpy.context.scene.render.engine != "CYCLES": + bpy.context.scene.render.engine = "CYCLES" + + +def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader): + _NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value) + + +def __exposeNodeTreeOutput(out_socket, name, node_output, shader): + _NodeGroupUtils(shader).new_output_socket(name, out_socket) + + +def __getMaterialOutput(nodes, bl_idname): + o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname) + o.is_active_output = True + return o + + +def create_MMDAlphaShader(): + __switchToCyclesRenderEngine() + + if "MMDAlphaShader" in bpy.data.node_groups: + return bpy.data.node_groups["MMDAlphaShader"] + + shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree") + + node_input = shader.nodes.new("NodeGroupInput") + node_output = shader.nodes.new("NodeGroupOutput") + node_output.location.x += 250 + node_input.location.x -= 500 + + trans = shader.nodes.new("ShaderNodeBsdfTransparent") + trans.location.x -= 250 + trans.location.y += 150 + mix = shader.nodes.new("ShaderNodeMixShader") + + shader.links.new(mix.inputs[1], trans.outputs["BSDF"]) + + __exposeNodeTreeInput(mix.inputs[2], "Shader", None, node_input, shader) + __exposeNodeTreeInput(mix.inputs["Fac"], "Alpha", 1.0, node_input, shader) + __exposeNodeTreeOutput(mix.outputs["Shader"], "Shader", node_output, shader) + + return shader + + +def create_MMDBasicShader(): + __switchToCyclesRenderEngine() + + if "MMDBasicShader" in bpy.data.node_groups: + return bpy.data.node_groups["MMDBasicShader"] + + shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree") + + node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput") + node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput") + node_output.location.x += 250 + node_input.location.x -= 500 + + dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse") + dif.location.x -= 250 + dif.location.y += 150 + glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic") + glo.location.x -= 250 + glo.location.y -= 150 + mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader") + shader.links.new(mix.inputs[1], dif.outputs["BSDF"]) + shader.links.new(mix.inputs[2], glo.outputs["BSDF"]) + + __exposeNodeTreeInput(dif.inputs["Color"], "diffuse", [1.0, 1.0, 1.0, 1.0], node_input, shader) + __exposeNodeTreeInput(glo.inputs["Color"], "glossy", [1.0, 1.0, 1.0, 1.0], node_input, shader) + __exposeNodeTreeInput(glo.inputs["Roughness"], "glossy_rough", 0.0, node_input, shader) + __exposeNodeTreeInput(mix.inputs["Fac"], "reflection", 0.02, node_input, shader) + __exposeNodeTreeOutput(mix.outputs["Shader"], "shader", node_output, shader) + + return shader + + +def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]: + yield node + if node.parent: + yield node.parent + for n in set(l.from_node for i in node.inputs for l in i.links): + yield from __enum_linked_nodes(n) + + +def __cleanNodeTree(material: bpy.types.Material): + nodes = material.node_tree.nodes + node_names = set(n.name for n in nodes) + for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}): + if any(i.is_linked for i in o.inputs): + node_names -= set(linked.name for linked in __enum_linked_nodes(o)) + for name in node_names: + nodes.remove(nodes[name]) + + +def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): + __switchToCyclesRenderEngine() + convertToBlenderShader(obj, use_principled, clean_nodes, subsurface) + + +def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): + for i in obj.material_slots: + if not i.material: + continue + if not i.material.use_nodes: + i.material.use_nodes = True + __convertToMMDBasicShader(i.material) + if use_principled: + __convertToPrincipledBsdf(i.material, subsurface) + if clean_nodes: + __cleanNodeTree(i.material) + +def convertToMMDShader(obj): + """BSDF -> MMDShaderDev conversion.""" + for i in obj.material_slots: + if not i.material: + continue + if not i.material.use_nodes: + i.material.use_nodes = True + FnMaterial.convert_to_mmd_material(i.material) + +def __convertToMMDBasicShader(material: bpy.types.Material): + # TODO: test me + mmd_basic_shader_grp = create_MMDBasicShader() + mmd_alpha_shader_grp = create_MMDAlphaShader() + + if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)): + # Add nodes for Cycles Render + shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") + shader.node_tree = mmd_basic_shader_grp + shader.inputs[0].default_value[:3] = material.diffuse_color[:3] + shader.inputs[1].default_value[:3] = material.specular_color[:3] + shader.inputs["glossy_rough"].default_value = 1.0 / getattr(material, "specular_hardness", 50) + outplug = shader.outputs[0] + + location = shader.location.copy() + location.x -= 1000 + + alpha_value = 1.0 + if len(material.diffuse_color) > 3: + alpha_value = material.diffuse_color[3] + + if alpha_value < 1.0: + alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") + alpha_shader.location.x = shader.location.x + 250 + alpha_shader.location.y = shader.location.y - 150 + alpha_shader.node_tree = mmd_alpha_shader_grp + alpha_shader.inputs[1].default_value = alpha_value + material.node_tree.links.new(alpha_shader.inputs[0], outplug) + outplug = alpha_shader.outputs[0] + + material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial") + material.node_tree.links.new(material_output.inputs["Surface"], outplug) + material_output.location.x = shader.location.x + 500 + material_output.location.y = shader.location.y - 150 + + +def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float): + node_names = set() + for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)): + if s.node_tree.name == "MMDBasicShader": + l: bpy.types.NodeLink + for l in s.outputs[0].links: + to_node = l.to_node + # assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader + if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader": + __switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node) + node_names.add(to_node.name) + else: + __switchToPrincipledBsdf(material.node_tree, s, subsurface) + node_names.add(s.name) + elif s.node_tree.name == "MMDShaderDev": + __switchToPrincipledBsdf(material.node_tree, s, subsurface) + node_names.add(s.name) + # remove MMD shader nodes + nodes = material.node_tree.nodes + for name in node_names: + nodes.remove(nodes[name]) + + +def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None): + shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled") + shader.parent = node_basic.parent + shader.location.x = node_basic.location.x + shader.location.y = node_basic.location.y + + alpha_socket_name = "Alpha" + if node_basic.node_tree.name == "MMDShaderDev": + node_alpha, alpha_socket_name = node_basic, "Base Alpha" + if "Base Tex" in node_basic.inputs and node_basic.inputs["Base Tex"].is_linked: + node_tree.links.new(node_basic.inputs["Base Tex"].links[0].from_socket, shader.inputs["Base Color"]) + elif "Diffuse Color" in node_basic.inputs: + shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["Diffuse Color"].default_value[:3] + elif "diffuse" in node_basic.inputs: + shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["diffuse"].default_value[:3] + if node_basic.inputs["diffuse"].is_linked: + node_tree.links.new(node_basic.inputs["diffuse"].links[0].from_socket, shader.inputs["Base Color"]) + + shader.inputs["IOR"].default_value = 1.0 + shader.inputs["Subsurface Weight"].default_value = subsurface + + output_links = node_basic.outputs[0].links + if node_alpha: + output_links = node_alpha.outputs[0].links + shader.parent = node_alpha.parent or shader.parent + shader.location.x = node_alpha.location.x + + if alpha_socket_name in node_alpha.inputs: + if "Alpha" in shader.inputs: + shader.inputs["Alpha"].default_value = node_alpha.inputs[alpha_socket_name].default_value + if node_alpha.inputs[alpha_socket_name].is_linked: + node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"]) + else: + shader.inputs["Transmission"].default_value = 1 - node_alpha.inputs[alpha_socket_name].default_value + if node_alpha.inputs[alpha_socket_name].is_linked: + node_invert = node_tree.nodes.new("ShaderNodeMath") + node_invert.parent = shader.parent + node_invert.location.x = node_alpha.location.x - 250 + node_invert.location.y = node_alpha.location.y - 300 + node_invert.operation = "SUBTRACT" + node_invert.use_clamp = True + node_invert.inputs[0].default_value = 1 + node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1]) + node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"]) + + for l in output_links: + node_tree.links.new(shader.outputs[0], l.to_socket)