# Copyright (C) 2021 Victor Soupday # This file is part of CC/iC 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 . 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)