c31d25dd01
You can choose between errors, warning, info or full debug, errors will always log to ensure we don't have silent failures with debug on or off.
314 lines
13 KiB
Python
314 lines
13 KiB
Python
# -*- 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
|