3414ad8917
- 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.
1076 lines
49 KiB
Python
1076 lines
49 KiB
Python
# -*- 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)
|