Files
blender-portable-repo/scripts/addons/cc_blender_tools-2_3_3/rigidbody.py
T
2026-03-17 15:16:34 -06:00

1179 lines
46 KiB
Python

# Copyright (C) 2021 Victor Soupday
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_blender_tools>
#
# CC/iC Blender Tools is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CC/iC Blender Tools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with CC/iC Blender Tools. If not, see <https://www.gnu.org/licenses/>.
import bpy
import math
import bmesh
from mathutils import Vector, Matrix, Quaternion
from math import radians
from . import jsonutils, drivers, bones, utils, vars
# these must be floats
BASE_COLLISION_RADIUS = 0.015
MARGIN = BASE_COLLISION_RADIUS * 2.0 / 3.0
MASS = 5.0 #0.5
STIFFNESS = 1.0
DAMPENING = 0.5
LIMIT = 1.0
ANGLE_RANGE = 120.0
LINEAR_LIMIT = 0.001
CURVE = 1.0
INFLUENCE = 1.0
UPSCALE = 5.0
COLLIDER_PREFIX = "COLLIDER"
COLLIDER_COLLECTION_NAME = "Rigid Body Colliders"
def init_rigidbody_world():
if not bpy.context.scene.rigidbody_world or not bpy.context.scene.rigidbody_world.enabled:
try:
bpy.ops.rigidbody.world_add()
except: ...
if "RigidBodyWorld" not in bpy.data.collections:
bpy.data.collections.new("RigidBodyWorld")
if "RigidBodyConstraints" not in bpy.data.collections:
bpy.data.collections.new("RigidBodyConstraints")
if bpy.context.scene.rigidbody_world.collection is None:
collection_world = bpy.data.collections["RigidBodyWorld"]
bpy.context.scene.rigidbody_world.collection = collection_world
if bpy.context.scene.rigidbody_world.constraints is None:
collection_constraints = bpy.data.collections["RigidBodyConstraints"]
bpy.context.scene.rigidbody_world.constraints = collection_constraints
bpy.context.scene.rigidbody_world.time_scale = 1.0
if bpy.context.scene.rigidbody_world.substeps_per_frame < 10:
bpy.context.scene.rigidbody_world.substeps_per_frame = 10
if bpy.context.scene.rigidbody_world.solver_iterations < 100:
bpy.context.scene.rigidbody_world.solver_iterations = 100
def add_body_node(co, name,
enabled = True,
parent_object = None,
location_target = None,
location_sub_target = None,
kinematic = False,
passive = False,
mass_driver = True,
dampening_driver = True, dampening_fac = 1.0,
radius_driver = True):
bpy.ops.mesh.primitive_ico_sphere_add(subdivisions = 1, radius = UPSCALE * BASE_COLLISION_RADIUS,
enter_editmode = False,
align = 'WORLD', location = co)
body_node = utils.get_active_object()
body_node.hide_render = True
body_node.scale = (1.0/UPSCALE, 1.0/UPSCALE, 1.0/UPSCALE)
body_node.name = utils.unique_name(name)
# add rigid body
bpy.ops.rigidbody.object_add()
body_node.rigid_body.collision_shape = 'SPHERE'
body_node.rigid_body.type = "PASSIVE" if passive else "ACTIVE"
body_node.rigid_body.enabled = enabled
body_node.rigid_body.mass = MASS
body_node.rigid_body.kinematic = kinematic
body_node.rigid_body.use_margin = True
body_node.rigid_body.collision_margin = MARGIN
body_node.rigid_body.linear_damping = 0.9
body_node.rigid_body.angular_damping = 0.9
body_node.rigid_body.friction = 0
body_node.rigid_body.restitution = 0
#body_node.rigid_body.collision_margin = margin
if parent_object:
body_node.location = co
body_node.parent = parent_object
body_node.matrix_parent_inverse = parent_object.matrix_world.inverted()
if location_target:
c : bpy.types.CopyTransformsConstraint = body_node.constraints.new(type="COPY_TRANSFORMS")
c.target = location_target
c.subtarget = location_sub_target
c.head_tail = 0
c.mix_mode = "REPLACE"
c.influence = 1.0
if mass_driver:
mass_expr = f"mass"
driver = drivers.make_driver(body_node.rigid_body, "mass", "SCRIPTED", mass_expr)
drivers.make_driver_var(driver, "SINGLE_PROP", "mass", parent_object,
data_path = f"[\"rigid_body_mass\"]")
if radius_driver:
# sphere colliders use embedded margins (i.e. the margin shrinks the radius)
#radius_expr = f"(radius / {UPSCALE * BASE_COLLISION_SIZE})"
#for i in range(0, 3):
# driver = drivers.make_driver(body_node, "scale", "SCRIPTED", radius_expr, index=i)
# drivers.make_driver_var(driver, "SINGLE_PROP", "radius", parent_object,
# data_path = f"[\"rigid_body_radius\"]")
margin_expr = f"margin"
driver = drivers.make_driver(body_node.rigid_body, "collision_margin", "SCRIPTED",
margin_expr)
drivers.make_driver_var(driver, "SINGLE_PROP", "margin", parent_object,
data_path = f"[\"rigid_body_margin\"]")
if dampening_driver:
# L + (1 - L)t
expr_limit = "(1.0 - (1.0 / pow(10.0, limit)))"
expr_fac = f"pow({dampening_fac}, curve)"
dampening_expr = f"({expr_limit} * (1.0 - {expr_fac})) + (1.0 * {expr_fac})"
driver = drivers.make_driver(body_node.rigid_body, "linear_damping", "SCRIPTED",
dampening_expr)
drivers.make_driver_var(driver, "SINGLE_PROP", "limit", parent_object,
data_path = f"[\"rigid_body_limit\"]")
drivers.make_driver_var(driver, "SINGLE_PROP", "curve", parent_object,
data_path = f"[\"rigid_body_curve\"]")
driver = drivers.make_driver(body_node.rigid_body, "angular_damping", "SCRIPTED",
dampening_expr)
drivers.make_driver_var(driver, "SINGLE_PROP", "limit", parent_object,
data_path = f"[\"rigid_body_limit\"]")
drivers.make_driver_var(driver, "SINGLE_PROP", "curve", parent_object,
data_path = f"[\"rigid_body_curve\"]")
return body_node
def connect_spring(arm, prefix, bone_name, head_body, tail_body,
parent_object = None,
use_linear_limit = True,
use_angular_limit = True, angular_limit_fac = 1.0,
use_linear_spring = False,
use_angular_spring = True,
dampening_driver = True,
stiffness_driver = True,
influence_driver = True,
angular_limit_driver = True):
# add an empty
bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', radius = BASE_COLLISION_RADIUS * 1.5, location=head_body.location)
constraint_object = utils.get_active_object()
constraint_object.hide_render = True
if parent_object:
constraint_object.location = head_body.location
constraint_object.parent = head_body
constraint_object.matrix_parent_inverse = head_body.matrix_world.inverted()
constraint_object.name = utils.unique_name(f"{prefix}_{bone_name}_Spring")
# add rigid body constraint
bpy.ops.rigidbody.constraint_add()
# configure constraint
rbc = constraint_object.rigid_body_constraint
rbc.type = 'GENERIC_SPRING'
rbc.enabled = True
rbc.disable_collisions = True
rbc.object1 = head_body
rbc.object2 = tail_body
if use_linear_limit:
rbc.use_limit_lin_x = True
rbc.use_limit_lin_y = True
rbc.use_limit_lin_z = True
rbc.limit_lin_x_lower = -LINEAR_LIMIT
rbc.limit_lin_y_lower = -LINEAR_LIMIT
rbc.limit_lin_z_lower = -LINEAR_LIMIT
rbc.limit_lin_x_upper = LINEAR_LIMIT
rbc.limit_lin_y_upper = LINEAR_LIMIT
rbc.limit_lin_z_upper = LINEAR_LIMIT
else:
rbc.use_limit_lin_x = False
rbc.use_limit_lin_y = False
rbc.use_limit_lin_z = False
if use_angular_limit:
rbc.use_limit_ang_x = True
rbc.use_limit_ang_y = True
rbc.use_limit_ang_z = True
rbc.limit_ang_x_lower = -ANGLE_RANGE * 0.008726645
rbc.limit_ang_y_lower = -ANGLE_RANGE * 0.008726645
rbc.limit_ang_z_lower = -ANGLE_RANGE * 0.008726645
rbc.limit_ang_x_upper = ANGLE_RANGE * 0.008726645
rbc.limit_ang_y_upper = ANGLE_RANGE * 0.008726645
rbc.limit_ang_z_upper = ANGLE_RANGE * 0.008726645
else:
rbc.use_limit_ang_x = False
rbc.use_limit_ang_y = False
rbc.use_limit_ang_z = False
if use_angular_spring:
rbc.use_spring_ang_x = True
rbc.use_spring_ang_y = True
rbc.use_spring_ang_z = True
rbc.spring_damping_ang_x = DAMPENING
rbc.spring_damping_ang_y = DAMPENING
rbc.spring_damping_ang_z = DAMPENING
rbc.spring_stiffness_ang_x = STIFFNESS
rbc.spring_stiffness_ang_y = STIFFNESS
rbc.spring_stiffness_ang_z = STIFFNESS
else:
rbc.use_spring_ang_x = False
rbc.use_spring_ang_y = False
rbc.use_spring_ang_z = False
if use_linear_spring:
rbc.use_spring_x = True
rbc.use_spring_y = True
rbc.use_spring_z = True
rbc.spring_damping_x = DAMPENING
rbc.spring_damping_y = DAMPENING
rbc.spring_damping_z = DAMPENING
rbc.spring_stiffness_x = STIFFNESS
rbc.spring_stiffness_y = STIFFNESS
rbc.spring_stiffness_z = STIFFNESS
else:
rbc.use_spring_x = False
rbc.use_spring_y = False
rbc.use_spring_z = False
#rbc.spring_type = 'SPRING1'
# add pose bone constraint to stretch to tail_body
pose_bone : bpy.types.PoseBone = arm.pose.bones[bone_name]
c : bpy.types.StretchToConstraint = pose_bone.constraints.new(type="STRETCH_TO")
c.name = utils.unique_name("Spring_StretchTo")
c.target = tail_body
c.influence = INFLUENCE
c.rest_length = (parent_object.matrix_world.inverted() @ tail_body.location -
parent_object.matrix_world.inverted() @ head_body.location).length
if dampening_driver:
if use_linear_spring:
dampening_props = ["spring_damping_x", "spring_damping_y", "spring_damping_z"]
for prop in dampening_props:
driver = drivers.make_driver(rbc, prop, "SUM")
drivers.make_driver_var(driver, "SINGLE_PROP", "dampening", parent_object,
data_path = f"[\"rigid_body_dampening\"]")
if use_angular_spring:
dampening_props = ["spring_damping_ang_x", "spring_damping_ang_y", "spring_damping_ang_z"]
for prop in dampening_props:
driver = drivers.make_driver(rbc, prop, "SUM")
drivers.make_driver_var(driver, "SINGLE_PROP", "dampening", parent_object,
data_path = f"[\"rigid_body_dampening\"]")
if stiffness_driver:
if use_linear_spring:
stiffness_props = ["spring_stiffness_x", "spring_stiffness_y", "spring_stiffness_z"]
for prop in stiffness_props:
driver = drivers.make_driver(rbc, prop, "SUM")
drivers.make_driver_var(driver, "SINGLE_PROP", "stiffnes", parent_object,
data_path = f"[\"rigid_body_stiffness\"]")
if use_angular_spring:
stiffness_props = ["spring_stiffness_ang_x", "spring_stiffness_ang_y", "spring_stiffness_ang_z"]
for prop in stiffness_props:
driver = drivers.make_driver(rbc, prop, "SUM")
drivers.make_driver_var(driver, "SINGLE_PROP", "stiffnes", parent_object,
data_path = f"[\"rigid_body_stiffness\"]")
if angular_limit_driver and use_angular_limit:
ang_limit_props = ["limit_ang_x_lower", "limit_ang_y_lower", "limit_ang_z_lower",
"limit_ang_x_upper", "limit_ang_y_upper", "limit_ang_z_upper"]
for prop in ang_limit_props:
if "lower" in prop:
expr = f"-limit * 0.008726645 * pow({angular_limit_fac}, curve)"
else:
expr = f"limit * 0.008726645 * pow({angular_limit_fac}, curve)"
driver = drivers.make_driver(rbc, prop, "SCRIPTED", expr)
drivers.make_driver_var(driver, "SINGLE_PROP", "limit", parent_object,
data_path = f"[\"rigid_body_angle_limit\"]")
drivers.make_driver_var(driver, "SINGLE_PROP", "curve", parent_object,
data_path = f"[\"rigid_body_curve\"]")
if influence_driver:
driver = drivers.make_driver(c, "influence", "SUM")
drivers.make_driver_var(driver, "SINGLE_PROP", "influence", parent_object,
data_path = f"[\"rigid_body_influence\"]")
return
def connect_fixed(arm, bone_name, head_body, tail_body, parent_object, copy_location = True, size = 0.075):
# add an empty
bpy.ops.object.empty_add(type='CIRCLE', align='WORLD', location=head_body.location, radius = size)
constraint_object = utils.get_active_object()
constraint_object.hide_render = True
if parent_object:
constraint_object.parent = head_body
constraint_object.location = Vector((0,0,0))
# add rigid body constraint
bpy.ops.rigidbody.constraint_add()
# configure constraint
rbc = constraint_object.rigid_body_constraint
rbc.type = 'FIXED'
rbc.object1 = head_body
rbc.object2 = tail_body
rbc.enabled = True
rbc.disable_collisions = True
return
def build_bone_map(arm, edit_bone : bpy.types.EditBone, bone_map : dict = None, length = 0, rigified = False):
if bone_map is None:
bone_map = {}
if length > 0 and rigified:
# for rigified spring rigs, we only want to use the SIM bones (and the spring rig root)
if not edit_bone.name.startswith("SIM-"):
return False
index = len(bone_map)
head = arm.matrix_world @ edit_bone.head
tail = arm.matrix_world @ edit_bone.tail
length += (head - tail).length
mapping = { "index": index,
"head": head,
"tail": tail,
"length": length,
"total": 0,
"fac": 0,
"parent": None,
"children": None,
"head_body": None,
"tail_body": None}
if edit_bone.parent and edit_bone.parent.name in bone_map:
mapping["parent"] = edit_bone.parent.name
bone_map[edit_bone.name] = mapping
children = []
for child_bone in edit_bone.children:
if build_bone_map(arm, child_bone, bone_map, length, rigified):
children.append(child_bone.name)
mapping["children"] = children
# if end of chain, calculate the length factors
if not children:
total = length
up = edit_bone
while up.name in bone_map:
if total > bone_map[up.name]["total"]:
bone_map[up.name]["total"] = total
bone_map[up.name]["fac"] = bone_map[up.name]["length"] / total
if up.parent:
up = up.parent
else:
break
return True
def remove_existing_rigid_body_system(arm, rig_prefix, spring_rig_bone_name):
if not arm:
return None
pose_position = arm.data.pose_position
arm.data.pose_position = "REST"
rigid_body_system_name = get_rigid_body_system_name(arm, rig_prefix)
settings = None
DRIVER_PROPS = [
"mass", "collision_margin", "linear_damping", "angular_damping", "influence",
"spring_damping_ang_x", "spring_damping_ang_y", "spring_damping_ang_z",
"spring_damping_x", "spring_damping_y", "spring_damping_z",
"spring_stiffness_ang_x", "spring_stiffness_ang_y", "spring_stiffness_ang_z",
"spring_stiffness_x", "spring_stiffness_y", "spring_stiffness_z",
"limit_ang_x_lower", "limit_ang_y_lower", "limit_ang_z_lower",
"limit_ang_x_upper", "limit_ang_y_upper", "limit_ang_z_upper",
]
to_delete = []
for obj in bpy.data.objects:
if vars.NODE_PREFIX in obj.name and rigid_body_system_name in obj.name:
utils.log_info(f"Found Rigid Body System: {obj.name}")
utils.log_info(f" Removing drivers...")
for child in obj.children:
if child.rigid_body:
for prop in DRIVER_PROPS:
child.rigid_body.driver_remove(prop)
if child.rigid_body_constraint:
for prop in DRIVER_PROPS:
child.rigid_body_constraint.driver_remove(prop)
to_delete.append(obj)
if not settings:
settings = {
"rigid_body_influence": obj["rigid_body_influence"],
"rigid_body_limit": obj["rigid_body_limit"],
"rigid_body_curve": obj["rigid_body_curve"],
"rigid_body_mass": obj["rigid_body_mass"],
"rigid_body_dampening": obj["rigid_body_dampening"],
"rigid_body_stiffness": obj["rigid_body_stiffness"],
"rigid_body_margin": obj["rigid_body_margin"],
"rigid_body_angle_limit": obj["rigid_body_angle_limit"],
}
utils.log_indent()
remove_constraints = []
utils.log_info(f"Removing bone constraints and drivers...")
for bone in arm.pose.bones:
c : bpy.types.StretchToConstraint
for c in bone.constraints:
if c.type == "STRETCH_TO":
if vars.NODE_PREFIX in c.name and "Spring_StretchTo" in c.name:
#utils.log_info(f"Removing Bone Constraint: {c.name} from {bone.name}")
c.driver_remove("influence")
remove_constraints.append([bone, c])
for bone, c in remove_constraints:
bone.constraints.remove(c)
for obj in to_delete:
if utils.object_exists(obj):
utils.log_info(f"Removing Rigid Body System: {obj.name}")
utils.delete_object_tree(obj)
utils.log_recess()
set_rigify_simulation_influence(arm, spring_rig_bone_name, 0.0, 1.0)
arm.data.pose_position = pose_position
return settings
def add_rigid_body_system(arm, parent_bone_name, rig_prefix, settings = None):
rigid_body_system_name = get_rigid_body_system_name(arm, rig_prefix)
bpy.ops.object.empty_add(type='SINGLE_ARROW', align='WORLD', location=(0,0,0))
rigid_body_system = utils.get_active_object()
rigid_body_system.hide_render = True
rigid_body_system.parent = arm
rigid_body_system.parent_type = "BONE"
rigid_body_system.parent_bone = parent_bone_name
rigid_body_system.location = Vector((0,0,0))
rigid_body_system.name = utils.unique_name(rigid_body_system_name)
if settings:
# these aren't declared global so it shouldn't overwrite them permamently...
influence = settings["rigid_body_influence"]
limit = settings["rigid_body_limit"]
curve = settings["rigid_body_curve"]
mass = settings["rigid_body_mass"]
dampening = settings["rigid_body_dampening"]
stiffness = settings["rigid_body_stiffness"]
margin = settings["rigid_body_margin"]
angle_limit = settings["rigid_body_angle_limit"]
else:
influence = INFLUENCE
limit = LIMIT
curve = CURVE
mass = MASS
dampening = DAMPENING
stiffness = STIFFNESS
margin = MARGIN
angle_limit = ANGLE_RANGE
drivers.add_custom_float_property(rigid_body_system, "rigid_body_influence", influence, 0.0, 1.0,
description = "How much of the simulation is copied into the pose bones")
drivers.add_custom_float_property(rigid_body_system, "rigid_body_limit", limit, 0.0, 4.0,
description = "How much to restrain the overall movement of the rigid body simulation")
drivers.add_custom_float_property(rigid_body_system, "rigid_body_curve", curve, 1.0/8.0, 8.0, 1.0/8.0, 2.0,
description = "The dampening curve factor along the length of the spring bone chains. Less curve gives more movement near the roots")
drivers.add_custom_float_property(rigid_body_system, "rigid_body_mass", mass, 0.0, 100.0, 0.1, 10.0,
description = "Mass of the rigid body particles representing the bones. More mass, more inertia")
drivers.add_custom_float_property(rigid_body_system, "rigid_body_dampening", dampening, 0.0, 10000.0, 0.0, 10.0,
description = "Spring dampening, how quickly the hair movement slows down.")
drivers.add_custom_float_property(rigid_body_system, "rigid_body_stiffness", stiffness, 0.0, 10000.0, 0.0, 100.0,
description = "Spring stiffness, how resistant to movement.")
drivers.add_custom_float_property(rigid_body_system, "rigid_body_margin", margin, 0.0, 1.0, 0.001, BASE_COLLISION_RADIUS,
description = "Collision margin. How far into the surface to be considered a collision.")
drivers.add_custom_float_property(rigid_body_system, "rigid_body_angle_limit", angle_limit, 0.0, 360.0, 0.0, 120.0,
description = "Angular limit of movement")
return rigid_body_system
def is_rigid_body(chr_cache, obj):
if chr_cache and obj:
obj, proxy, is_proxy = chr_cache.get_related_physics_objects(obj)
if proxy:
obj = proxy
return obj and obj.rigid_body is not None
def get_rigid_body(chr_cache, obj):
if chr_cache and obj:
obj, proxy, is_proxy = chr_cache.get_related_physics_objects(obj)
if proxy:
obj = proxy
return obj.rigid_body
return None
def enable_rigid_body_collision_mesh(chr_cache, obj):
arm = None
if chr_cache:
arm = chr_cache.get_armature()
if arm:
pose_position = arm.data.pose_position
arm.data.pose_position = "REST"
obj, proxy, is_proxy = chr_cache.get_related_physics_objects(obj)
if proxy:
if obj.rigid_body:
# if there is a collision body proxy but
# there is a rigid body mod on the real body, remove it:
utils.set_active_object(obj)
hidden = False
if not obj.visible_get():
hidden = True
utils.unhide(obj)
bpy.ops.rigidbody.object_remove()
if hidden:
utils.hide(obj)
obj = proxy
if obj.rigid_body is None:
utils.set_active_object(obj)
hidden = False
if not obj.visible_get():
hidden = True
utils.unhide(obj)
bpy.ops.rigidbody.object_add()
if hidden:
utils.hide(obj)
obj.rigid_body.collision_shape = 'MESH'
obj.rigid_body.type = "PASSIVE"
obj.rigid_body.enabled = True
obj.rigid_body.mass = 1.0
obj.rigid_body.kinematic = True
obj.rigid_body.use_margin = True
obj.rigid_body.mesh_source = 'DEFORM'
obj.rigid_body.use_deform = True
obj.rigid_body.friction = 0
obj.rigid_body.restitution = 0
obj.rigid_body.collision_margin = MARGIN
obj.rigid_body.linear_damping = 0
obj.rigid_body.angular_damping = 0
if arm:
arm.data.pose_position = pose_position
def disable_rigid_body_collision_mesh(chr_cache, obj):
arm = None
if chr_cache:
arm = chr_cache.get_armature()
if arm:
pose_position = arm.data.pose_position
arm.data.pose_position = "REST"
obj, proxy, is_proxy = chr_cache.get_related_physics_objects(obj)
if proxy:
if obj.rigid_body:
utils.set_active_object(obj)
hidden = False
if not obj.visible_get():
hidden = True
utils.unhide(obj)
bpy.ops.rigidbody.object_remove()
if hidden:
utils.hide(obj)
obj = proxy
if obj.rigid_body is not None:
utils.set_active_object(obj)
hidden = False
if not obj.visible_get():
hidden = True
utils.unhide(obj)
bpy.ops.rigidbody.object_remove()
if hidden:
utils.hide(obj)
if arm:
arm.data.pose_position = pose_position
def get_rigid_body_system_name(arm, rig_prefix):
return f"{arm.name}_{rig_prefix}_RigidBody"
def get_spring_rigid_body_system(arm, rig_prefix):
if arm:
rigid_body_system_name = get_rigid_body_system_name(arm, rig_prefix)
for obj in bpy.data.objects:
if vars.NODE_PREFIX in obj.name and rigid_body_system_name in obj.name:
return obj
return None
def build_spring_rigid_body_system(chr_cache, spring_rig_prefix, spring_rig_bone_name, settings = None):
props = vars.props()
arm = chr_cache.get_armature()
if not arm or spring_rig_bone_name not in arm.data.bones:
return False
pose_position = arm.data.pose_position
arm.data.pose_position = "REST"
spring_rig_bone = arm.pose.bones[spring_rig_bone_name]
rigified = "rigified" in spring_rig_bone and spring_rig_bone["rigified"]
# generate a map of the spring rig bones
if not utils.edit_mode_to(arm):
return
root_bone = arm.data.edit_bones[spring_rig_bone_name]
# fix old spring rig bone name
if spring_rig_bone_name.startswith("RL_"):
root_bone.name = "RLS_" + spring_rig_bone_name[3:]
spring_rig_bone_name = root_bone.name
utils.log_info(f"Updating spring rig name to {spring_rig_bone_name}")
bone_map = {}
build_bone_map(arm, root_bone, rigified = rigified, bone_map = bone_map)
utils.object_mode_to(arm)
# remove any existing rig and store it's settings
rigid_body_system = get_spring_rigid_body_system(arm, spring_rig_prefix)
if rigid_body_system:
if not settings:
settings = remove_existing_rigid_body_system(arm, spring_rig_prefix, spring_rig_bone_name)
else:
remove_existing_rigid_body_system(arm, spring_rig_prefix, spring_rig_bone_name)
# create a new spring rig
utils.log_info(f"Building Rigid Body System from: {spring_rig_bone_name}")
rigid_body_system = add_rigid_body_system(arm, spring_rig_bone_name, spring_rig_prefix, settings)
# add the root node for the spring rig
root_body = add_body_node(bone_map[spring_rig_bone_name]["head"],
parent_object=rigid_body_system,
name = f"{spring_rig_prefix}_{spring_rig_bone_name}",
enabled = False, kinematic = True, passive = True,
#location_target = arm, location_sub_target = spring_rig_bone_name,
dampening_driver = False, mass_driver = False, radius_driver = False)
# from the bone map, generate the rigid body nodes and their spring constraints
for bone_name in bone_map:
if bone_name == spring_rig_bone_name:
continue
mapping = bone_map[bone_name]
parent_name = mapping["parent"]
# anything connected to the rig bone is fixed in place, these are the roots of the bone chains
if parent_name == spring_rig_bone_name:
head_body = add_body_node(mapping["head"], name = f"{spring_rig_prefix}_{bone_name}_Head",
parent_object = rigid_body_system)
mapping["head_body"] = head_body
connect_fixed(arm, bone_name, root_body, head_body, parent_object = rigid_body_system)
# child bones of a bone chain, connect the tail_body of the parent to the tail_body for this bone
else:
parent_mapping = bone_map[parent_name]
head_body = parent_mapping["tail_body"]
if not head_body:
dampening_fac = 1.0 - parent_mapping["fac"]
head_body = add_body_node(parent_mapping["tail"], name = f"{spring_rig_prefix}_{parent_name}_Tail",
parent_object = rigid_body_system, dampening_fac = dampening_fac)
parent_mapping["tail_body"] = head_body
# add the tail node rigid body
fac = mapping["fac"]
dampening_fac = 1.0 - fac
tail_body = add_body_node(mapping["tail"], name = f"{spring_rig_prefix}_{bone_name}_Tail",
parent_object = rigid_body_system, dampening_fac = dampening_fac)
mapping["head_body"] = head_body
mapping["tail_body"] = tail_body
# connect the head and the tail together with a generic spring constraint
connect_spring(arm, spring_rig_prefix, bone_name, head_body, tail_body,
parent_object = rigid_body_system,
use_angular_spring=True,
use_linear_spring=False,
use_angular_limit=True, angular_limit_fac = fac,
use_linear_limit=True,
)
set_rigify_simulation_influence(arm, spring_rig_bone_name, 1.0, 1.0)
init_rigidbody_world()
collections = utils.get_object_scene_collections(arm)
system_objects = utils.get_object_tree(rigid_body_system)
for obj in system_objects:
utils.move_object_to_scene_collections(obj, collections)
utils.hide(obj)
arm.data.pose_position = pose_position
def set_rigify_simulation_influence(arm, spring_rig_bone_name, sim_value, ik_fk_value):
# activate the simulation constraint influence
if arm and spring_rig_bone_name in arm.pose.bones:
spring_rig_bone = arm.pose.bones[spring_rig_bone_name]
child_bones = bones.get_bone_children(spring_rig_bone, include_root=False)
for child_bone in child_bones:
if "SIM" in child_bone:
child_bone["SIM"] = sim_value
if "IK_FK" in child_bone:
child_bone["IK_FK"] = ik_fk_value
def add_simulation_bone_collection(arm):
bones.add_bone_collection(arm, "Simulation", "Simulation", "THEME02")
def reset_cache(context):
if bpy.context.scene.use_preview_range:
start = bpy.context.scene.frame_preview_start
end = bpy.context.scene.frame_preview_end
else:
start = bpy.context.scene.frame_start
end = bpy.context.scene.frame_end
rigidbody_world = bpy.context.scene.rigidbody_world
if rigidbody_world:
cache = rigidbody_world.point_cache
# free the bake
if cache.is_baked:
utils.log_info("Freeing baked point cache...")
utils.safe_free_bake(bpy.context.scene.rigidbody_world.point_cache)
# invalidate the cache
utils.log_info("Invalidating point cache...")
steps = 10
interations = rigidbody_world.solver_iterations
if cache:
cache.frame_start = 1
cache.frame_end = 1
try:
steps = rigidbody_world.steps_per_second
rigidbody_world.steps_per_second = 1
except:
pass
try:
steps = rigidbody_world.substeps_per_frame
rigidbody_world.substeps_per_frame = 1
except:
pass
rigidbody_world.solver_iterations = 1
# reset the cache
utils.log_info("Setting rigid body world bake cache frame range to [" + str(start) + " - " + str(end) + "]")
if cache:
cache.frame_start = start
cache.frame_end = end
try:
rigidbody_world.steps_per_second = steps
except:
pass
try:
rigidbody_world.substeps_per_frame = steps
except:
pass
rigidbody_world.solver_iterations = interations
def create_capsule_collider(name, parent, location, rotation, scale, radius, length, axis):
bm = bmesh.new()
try:
bmesh.ops.create_uvsphere(bm, u_segments=8, v_segments=9, radius=radius)
except:
bmesh.ops.create_uvsphere(bm, u_segments=8, v_segments=9, diameter=radius)
bm.verts.ensure_lookup_table()
i = 2
for vert in bm.verts:
if vert.co[i] < 0:
vert.co[i] -= length * 0.5
elif vert.co[i] > 0:
vert.co[i] += length * 0.5
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
mesh.update()
bm.free()
capsule = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(capsule)
capsule.display_type = 'WIRE'
capsule.location = parent.matrix_world @ location
r = Quaternion()
r.identity()
if axis == "X":
mat_rot_y = Matrix.Rotation(radians(90), 4, 'Y')
mat_rot_z = Matrix.Rotation(radians(90), 4, 'Z')
r.rotate(mat_rot_z)
r.rotate(mat_rot_y)
elif axis == "Y":
mat_rot_x = Matrix.Rotation(radians(90), 4, 'X')
mat_rot_z = Matrix.Rotation(radians(90), 4, 'Z')
r.rotate(mat_rot_z)
r.rotate(mat_rot_x)
r.rotate(rotation)
utils.set_transform_rotation(capsule, rotate_quat(parent.matrix_world, r))
capsule.scale = parent.scale * scale
return capsule
def create_sphere_collider(name, parent, location, rotation, scale, radius):
bm = bmesh.new()
try:
bmesh.ops.create_uvsphere(bm, u_segments=8, v_segments=9, radius=radius)
except:
bmesh.ops.create_uvsphere(bm, u_segments=8, v_segments=9, diameter=radius)
bm.verts.ensure_lookup_table()
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
mesh.update()
bm.free()
sphere = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(sphere)
sphere.display_type = 'WIRE'
sphere.location = parent.matrix_world @ location
utils.set_transform_rotation(sphere, rotate_quat(parent.matrix_world, rotation))
sphere.scale = parent.scale * scale
return sphere
def create_box_collider(name, parent, location, rotation, scale, extents, axis):
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=1.0)
bm.verts.ensure_lookup_table()
for vert in bm.verts:
for i, extent in enumerate(extents):
if vert.co[i] < 0:
vert.co[i] = -extent
elif vert.co[i] > 0:
vert.co[i] = extent
mesh = bpy.data.meshes.new(name)
bm.to_mesh(mesh)
mesh.update()
bm.free()
box = bpy.data.objects.new(name, mesh)
bpy.context.scene.collection.objects.link(box)
box.display_type = 'WIRE'
box.location = parent.matrix_world @ location
utils.set_transform_rotation(box, rotate_quat(parent.matrix_world, rotation))
box.scale = parent.scale * scale
return box
def rotate_quat(M: Matrix, Q: Quaternion):
return (M @ Q.to_matrix().to_4x4()).to_quaternion()
def fix_quat(q):
return [q[3], q[0], q[1], q[2]]
def unfix_quat(q):
return [q[1], q[2], q[3], q[0]]
def build_rigid_body_colliders(chr_cache, json_data, first_import = False, bone_mapping = None):
physics_json = None
if json_data:
chr_json = jsonutils.get_character_json(json_data, chr_cache.get_character_id())
physics_json = jsonutils.get_physics_json(chr_json)
if not chr_cache or not json_data or not physics_json:
utils.log_error("Invalid character data for collider setup!")
return False
if "Collision Shapes" not in physics_json:
utils.log_error("No collision shapes in json data!")
return False
arm = chr_cache.get_armature()
if not arm:
utils.log_error("No armature in character!")
return False
RV = utils.store_render_visibility_state(arm)
utils.unhide(arm)
collection = utils.create_collection(COLLIDER_COLLECTION_NAME)
collection.hide_render = True
use_bind_data = False
if not first_import and "Has BindPose Data" in physics_json:
use_bind_data = True
utils.object_mode_to(arm)
arm_settings = bones.store_armature_settings(arm)
bones.make_bones_visible(arm)
old_action = utils.safe_get_action(arm)
old_pose = arm.data.pose_position
if use_bind_data:
bones.set_rig_bind_pose(arm)
else:
# reset the existing action back to the first frame
# (the colliders in the json data are posed to the first frame)
arm.data.pose_position = "POSE"
bpy.ops.screen.animation_cancel(restore_frame=False)
bpy.ops.screen.frame_jump(end = False)
# build and attach the colliders
collider_cache = []
utils.set_active_object(arm, True)
collider_json = physics_json["Collision Shapes"]
for bone_name in collider_json:
for shape_name in collider_json[bone_name]:
target_bone_name = bone_name
if bone_mapping:
target_bone_name = bones.get_rigify_meta_bone(arm, bone_mapping, bone_name)
if target_bone_name not in arm.data.bones:
continue
name = f"{COLLIDER_PREFIX}_{bone_name}_{shape_name}"
shape_data = collider_json[bone_name][shape_name]
active = shape_data["Bone Active"]
shape = shape_data["Bound Type"]
axis = shape_data["Bound Axis"]
margin = shape_data["Margin"] * 0.01
friction = shape_data["Friction"]
elasticity = shape_data["Elasticity"] / 10.0
translate = Vector(shape_data["WorldTranslate"])
rotate = Quaternion(fix_quat(shape_data["WorldRotationQ"]))
scale = Vector(shape_data["WorldScale"])
if use_bind_data:
translate = Vector(shape_data["BindPose WorldTranslate"])
rotate = Quaternion(fix_quat(shape_data["BindPose WorldRotationQ"]))
scale = Vector(shape_data["BindPose WorldScale"])
axis = shape_data["BindPose Bound Axis"]
obj : bpy.types.Object = None
if shape == "Box":
extent = Vector(shape_data["Extent"]) / 2.0
obj = create_box_collider(name, arm, translate, rotate, scale, extent, axis)
elif shape == "Capsule":
radius = shape_data["Radius"]
length = shape_data["Capsule Length"]
obj = create_capsule_collider(name, arm, translate, rotate, scale, radius, length, axis)
elif shape == "Sphere":
radius = shape_data["Radius"]
obj = create_sphere_collider(name, arm, translate, rotate, scale, radius)
if not obj:
continue
# using operators to parent because matrix_parent_inverse doesn't work correctly
if bones.set_active_bone(arm, target_bone_name, deselect_all=True):
utils.set_active_object(arm, True)
utils.unhide(obj)
obj.select_set(True)
bpy.ops.object.parent_set(type='BONE', keep_transform=True)
else:
utils.log_error(f"Unable to parent rigid body collider {obj.name} to armature!")
utils.delete_mesh_object(obj)
continue
if active:
utils.set_active_object(obj)
# enable cloth collision
# NOTE: Disabled, cloth collisions with the primitive colliders is bad...
if False:
collision_mod = obj.modifiers.new(utils.unique_name("Collision"), type="COLLISION")
collision_mod.settings.thickness_outer = margin
collision_mod.settings.thickness_inner = margin
collision_mod.settings.cloth_friction = friction
collision_mod.settings.damping = 0.0
# enable rigid body collision
bpy.ops.rigidbody.object_add()
if shape == "Capsule":
obj.rigid_body.collision_shape = 'CAPSULE'
elif shape == "Box":
obj.rigid_body.collision_shape = 'BOX'
obj.rigid_body.type = "PASSIVE"
obj.rigid_body.kinematic = True
obj.rigid_body.use_margin = True
obj.rigid_body.friction = friction
obj.rigid_body.restitution = elasticity
obj.rigid_body.collision_margin = margin
obj.rigid_body.linear_damping = 0
obj.rigid_body.angular_damping = 0
utils.move_object_to_scene_collections(obj, [collection])
utils.hide(obj)
obj.hide_render = True
cache = {"bone_name": bone_name, "shape_name": shape_name, "object": obj }
collider_cache.append(cache)
# save the bind pose collider transforms to the json data so they can be
# reconstructed later without needing the original pose:
if not use_bind_data:
# set to bind pose
bones.set_rig_bind_pose(arm)
# write the bind translation, rotation quaternion, scale and axis of the colliders to the json data
for cache in collider_cache:
bone_name = cache["bone_name"]
shape_name = cache["shape_name"]
obj = cache["object"]
shape_data = collider_json[bone_name][shape_name]
shape_data["BindPose WorldTranslate"] = list(obj.matrix_world.translation * 100.0)
shape_data["BindPose WorldRotationQ"] = unfix_quat(list(obj.matrix_world.to_quaternion()))
shape_data["BindPose WorldScale"] = list(obj.scale)
# bind posed collider data is always in Z axis
shape_data["BindPose Bound Axis"] = "Z"
physics_json["Has BindPose Data"] = True
# write back the updated json data
chr_cache.write_json_data(json_data)
# restore the original action
utils.safe_set_action(arm, old_action)
arm.data.pose_position = old_pose
bones.restore_armature_settings(arm, arm_settings)
utils.restore_render_visibility_state(RV)
return True
def get_rigid_body_colliders(arm):
colliders = []
if arm:
for obj in arm.children:
if is_rigid_body_collider(obj):
colliders.append(obj)
return colliders
def has_rigid_body_colliders(arm):
obj : bpy.types.Object
if arm:
for obj in arm.children:
if is_rigid_body_collider(obj):
return True
return False
def is_rigid_body_collider(obj):
return utils.object_exists_is_mesh(obj) and obj.name.startswith(COLLIDER_PREFIX)
def get_rigidbody_collider_collection():
collection = None
if COLLIDER_COLLECTION_NAME in bpy.data.collections:
collection = bpy.data.collections[COLLIDER_COLLECTION_NAME]
return collection
def remove_rigid_body_colliders(arm):
collection = get_rigidbody_collider_collection()
colliders = get_rigid_body_colliders(arm)
for collider in colliders:
utils.delete_mesh_object(collider)
if collection and len(collection.objects) == 0:
bpy.data.collections.remove(collection)
def colliders_visible(arm, colliders = None):
if not colliders:
colliders = get_rigid_body_colliders(arm)
for collider in colliders:
if not collider.visible_get():
return False
return True
def hide_colliders(arm):
if arm:
colliders = get_rigid_body_colliders(arm)
hide_state = colliders_visible(arm, colliders)
if hide_state:
toggle_show_colliders(arm)
def toggle_show_colliders(arm):
colliders = get_rigid_body_colliders(arm)
hide_state = colliders_visible(arm, colliders)
layer_collections = utils.get_view_layer_collections(search = COLLIDER_COLLECTION_NAME)
for collection in layer_collections:
collection.exclude = False
collection.hide_viewport = False
for collider in colliders:
utils.hide(collider, hide_state)
def convert_colliders_to_rigify(chr_cache, cc3_rig, rigify_rig, bone_mapping):
obj : bpy.types.Object
if cc3_rig and rigify_rig:
utils.object_mode_to(rigify_rig)
cc3_arm_settings = bones.store_armature_settings(cc3_rig)
rigify_arm_settings = bones.store_armature_settings(rigify_rig)
cc3_rig.location = (0,0,0)
rigify_rig.location = (0,0,0)
bones.set_rig_bind_pose(cc3_rig)
bones.set_rig_bind_pose(rigify_rig)
bones.make_bones_visible(rigify_rig)
# make sure the colliders can be make visible and selectable
layer_collections = utils.get_view_layer_collections(search = COLLIDER_COLLECTION_NAME)
for collection in layer_collections:
collection.exclude = False
collection.hide_viewport = False
colliders = get_rigid_body_colliders(cc3_rig)
for obj in colliders:
bone_name = obj.parent_bone
rigify_bone_name = bones.get_rigify_meta_bone(rigify_rig, bone_mapping, bone_name)
if rigify_bone_name:
# using operators to parent because matrix_parent_inverse doesn't work correctly
utils.set_active_object(rigify_rig, True)
if bones.set_active_bone(rigify_rig, rigify_bone_name, deselect_all=True):
utils.unhide(obj)
obj.select_set(True)
bpy.ops.object.parent_set(type='BONE', keep_transform=True)
else:
utils.log_error(f"Enable to parent collider object {obj.name} to rigify rig!")
utils.delete_mesh_object(obj)
else:
utils.log_error(f"Unable to map {bone_name} to rigify bone!")
utils.delete_mesh_object(obj)
# hide the colliders
for obj in colliders:
if utils.object_exists(obj):
utils.hide(obj)
for collection in layer_collections:
collection.hide_viewport = True
bones.restore_armature_settings(cc3_rig, cc3_arm_settings)
bones.restore_armature_settings(rigify_rig, rigify_arm_settings)