a929f68ad4
- Truly fixes PMX Import lol, i messed up completely - Updated MMD Tools to use Cats One
319 lines
11 KiB
Python
319 lines
11 KiB
Python
# Copyright 2014 MMD Tools authors
|
|
# This file is part of MMD Tools.
|
|
|
|
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<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
|
|
|
|
@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 = f"{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 is not None
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
objects = self.__get_objects(obj)
|
|
if obj not in objects:
|
|
self.report({"ERROR"}, f'Can not move object "{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, strict=False):
|
|
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 = "Sep by Mat(High Risk)"
|
|
bl_description = "Separate by Materials (High Risk)\nSeparate the mesh into multiple objects based on materials.\nHIGH RISK & BUGGY: This operation is not reversible and may cause various issues. It splits adjacent geometry by material, and merging later will not reconnect shared edges.\nKnown issues include potential mesh corruption, UV mapping problems, and other unpredictable behaviors. Use with extreme caution and backup your work first."
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
clean_shape_keys: bpy.props.BoolProperty(
|
|
name="Clean Shape Keys",
|
|
description="Remove unused shape keys of separated objects",
|
|
default=True,
|
|
)
|
|
|
|
keep_normals: bpy.props.BoolProperty(
|
|
name="Keep Normals",
|
|
default=True,
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
obj = context.active_object
|
|
return obj is not None and obj.type == "MESH"
|
|
|
|
def __separate_by_materials(self, obj):
|
|
utils.separateByMaterials(obj, self.keep_normals)
|
|
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)
|
|
|
|
# Sep by Mat crashes Blender if used after morph assembly
|
|
rig = Model(root)
|
|
rig.morph_slider.unbind()
|
|
|
|
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):
|
|
root = FnModel.find_root_object(context.active_object)
|
|
return root 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 is not None 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"}
|