580 lines
21 KiB
Python
580 lines
21 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 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"}
|