Files
blender-portable-repo/scripts/addons/cc_blender_tools-main/bones.py
T
2026-03-17 14:30:01 -06:00

1490 lines
55 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 mathutils
from math import pi, atan
from . import drivers, utils, vars
from rna_prop_ui import rna_idprop_ui_create
def cmp_rl_bone_names(name, bone_name):
"""Reduce supplied bone names to their base form without prefixes and compare."""
if bone_name.startswith("RL_"):
bone_name = bone_name[3:]
elif bone_name.startswith("CC_Base_"):
bone_name = bone_name[8:]
if name.startswith("RL_"):
name = name[3:]
elif name.startswith("CC_Base_"):
name = name[8:]
return name == bone_name
def get_rl_edit_bone(rig, name) -> bpy.types.EditBone:
if name:
if name in rig.data.edit_bones:
return rig.data.edit_bones[name]
# remove "CC_Base_" from start of bone name and try again...
if name.startswith("CC_Base_"):
name = name[8:]
if name in rig.data.edit_bones:
return rig.data.edit_bones[name]
if name.startswith("RL_"):
name = name[3:]
if name in rig.data.edit_bones:
return rig.data.edit_bones[name]
return None
def get_rl_bone(rig, name):
if name:
if name in rig.data.bones:
return rig.data.bones[name]
# remove "CC_Base_" from start of bone name and try again...
if name.startswith("CC_Base_"):
name = name[8:]
if name in rig.data.bones:
return rig.data.bones[name]
if name.startswith("RL_"):
name = name[3:]
if name in rig.data.bones:
return rig.data.bones[name]
return None
def get_rl_pose_bone(rig, name) -> bpy.types.PoseBone:
if name:
if name in rig.pose.bones:
return rig.pose.bones[name]
# remove "CC_Base_" from start of bone name and try again...
if name.startswith("CC_Base_"):
name = name[8:]
if name in rig.pose.bones:
return rig.pose.bones[name]
if name.startswith("RL_"):
name = name[3:]
if name in rig.pose.bones:
return rig.pose.bones[name]
return None
def get_edit_bone(rig, name) -> bpy.types.EditBone:
if name:
if type(name) is list:
for n in name:
if n in rig.data.edit_bones:
return rig.data.edit_bones[n]
else:
if name in rig.data.edit_bones:
return rig.data.edit_bones[name]
return None
def get_bone(rig, name) -> bpy.types.Bone:
if name:
if type(name) is list:
for n in name:
if n in rig.data.bones:
return rig.data.bones[n]
else:
if name in rig.data.bones:
return rig.data.bones[name]
return None
def get_pose_bone(rig, name) -> bpy.types.PoseBone:
if name:
if type(name) is list:
for n in name:
if n in rig.pose.bones:
return rig.pose.bones[n]
else:
if name in rig.pose.bones:
return rig.pose.bones[name]
return None
def find_target_pose_bone(rig, rl_bone_name, bone_mapping = None) -> bpy.types.PoseBone:
target_bone_name = find_target_bone_name(rig, rl_bone_name, bone_mapping)
if target_bone_name in rig.pose.bones:
return rig.pose.bones[target_bone_name]
return None
def is_target_bone_name(bone_name, target_name):
if not bone_name or not target_name:
return False
if target_name == bone_name:
return True
if cmp_rl_bone_names(target_name, bone_name):
return True
target_name = rl_export_bone_name(target_name)
if target_name == bone_name:
return True
if cmp_rl_bone_names(target_name, bone_name):
return True
def rl_export_bone_name(bone_name):
bone_name = bone_name.replace(' ', '_')
bone_name = bone_name.replace('(', '_')
bone_name = bone_name.replace(')', '_')
bone_name = bone_name.replace('&', '_')
return bone_name
def find_target_bone_name(rig, rl_bone_name, bone_mapping=None):
if not rig or not rl_bone_name:
return None
target_bone_name = None
if bone_mapping:
target_bone_name = get_rigify_meta_bone(rig, bone_mapping, rl_bone_name)
else:
target_bone_name = rl_bone_name
if target_bone_name in rig.pose.bones:
return target_bone_name
for pose_bone in rig.pose.bones:
if cmp_rl_bone_names(target_bone_name, pose_bone.name):
return pose_bone.name
target_bone_name = rl_export_bone_name(target_bone_name)
for pose_bone in rig.pose.bones:
if cmp_rl_bone_names(target_bone_name, pose_bone.name):
return pose_bone.name
return None
def find_pivot_bone(rig, bone_name):
if bone_name in rig.data.bones:
bone: bpy.types.Bone = rig.data.bones[bone_name]
for child in bone.children:
if child.name.startswith("CC_Base_Pivot"):
return child
return None
def get_rigify_meta_bone(rigify_rig, bone_mapping, cc3_bone_name):
if cc3_bone_name == "RL_BoneRoot" or cc3_bone_name == "CC_Base_BoneRoot":
return "root"
for bone_map in bone_mapping:
if bone_map[1] == cc3_bone_name:
# try to find the parent in the ORG bones
org_bone_name = f"ORG-{bone_map[0]}"
if org_bone_name in rigify_rig.data.bones:
return org_bone_name
# then try the DEF bones
def_bone_name = f"DEF-{bone_map[0]}"
if def_bone_name in rigify_rig.data.bones:
return def_bone_name
return None
def get_rigify_meta_bones(rigify_rig, bone_mapping, cc3_bone_name):
meta_bone_names = []
if cc3_bone_name == "RL_BoneRoot" or cc3_bone_name == "CC_Base_BoneRoot":
return ["root"]
for bone_map in bone_mapping:
if bone_map[1] == cc3_bone_name:
# try to find the parent in the ORG bones
org_bone_name = f"ORG-{bone_map[0]}"
if org_bone_name in rigify_rig.data.bones:
meta_bone_names.append(org_bone_name)
# then try the DEF bones
def_bone_name = f"DEF-{bone_map[0]}"
if def_bone_name in rigify_rig.data.bones:
meta_bone_names.append(def_bone_name)
return meta_bone_names
def get_align_vector(axis):
if axis == "X":
return mathutils.Vector((1,0,0))
if axis == "Y":
return mathutils.Vector((0,1,0))
if axis == "Z":
return mathutils.Vector((0,0,1))
if axis == "-X":
return mathutils.Vector((-1,0,0))
if axis == "-Y":
return mathutils.Vector((0,-1,0))
if axis == "-Z":
return mathutils.Vector((0,0,-1))
return None
def align_edit_bone_roll(edit_bone : bpy.types.EditBone, axis):
align_vector = get_align_vector(axis)
if align_vector:
edit_bone.align_roll(align_vector)
def rename_bone(rig, from_name, to_name):
if utils.edit_mode_to(rig):
bone = get_edit_bone(rig, from_name)
if bone and to_name not in rig.data.edit_bones:
bone.name = to_name
else:
utils.log_error(f"Bone {from_name} cannot be renamed as {to_name} already exists in rig!")
def copy_edit_bone(rig, src_name, dst_name, parent_name, scale):
if utils.edit_mode_to(rig):
src_bone = get_edit_bone(rig, src_name)
if src_bone and dst_name not in rig.data.edit_bones:
dst_bone = rig.data.edit_bones.new(dst_name)
dst_bone.head = src_bone.head
dst_bone.tail = src_bone.head + (src_bone.tail - src_bone.head) * scale
dst_bone.roll = src_bone.roll
if parent_name != "":
if parent_name in rig.data.edit_bones:
dst_bone.parent = rig.data.edit_bones[parent_name]
else:
utils.log_error(f"Unable to find parent bone {parent_name} in rig!")
return dst_bone
else:
if src_name not in rig.data.edit_bones:
utils.log_error(f"Unable to find source bone {src_name} in rig!")
if dst_name in rig.data.edit_bones:
utils.log_error(f"Destination bone {dst_name} already exists in rig!")
else:
utils.log_error(f"Unable to edit rig!")
return None
def new_edit_bone(rig, bone_name, parent_name, allow_existing = True):
if utils.edit_mode_to(rig):
can_add = allow_existing or bone_name not in rig.data.edit_bones
if can_add:
bone = rig.data.edit_bones.new(bone_name)
bone.head = mathutils.Vector((0,0,0))
bone.tail = bone.head + mathutils.Vector((0,0,0.05))
bone.roll = 0
if parent_name != "":
if parent_name in rig.data.edit_bones:
bone.parent = rig.data.edit_bones[parent_name]
else:
utils.log_error(f"Unable to find parent bone {parent_name} in rig!")
return bone
else:
utils.log_error(f"Destination bone {bone_name} already exists in rig!")
else:
utils.log_error(f"Unable to edit rig!")
return None
def reparent_edit_bone(rig, bone_name, parent_name):
if utils.edit_mode_to(rig):
if bone_name in rig.data.bones:
bone = rig.data.edit_bones[bone_name]
if bone:
if parent_name != "":
parent_bone = get_edit_bone(rig, parent_name)
if parent_bone:
bone.parent = parent_bone
return bone
else:
utils.log_error(f"Could not find parent bone: {parent_name} in Rig!")
else:
utils.log_error(f"Could not find target bone: {bone_name} in Rig!")
else:
utils.log_error(f"Unable to edit rig!")
return None
def copy_rl_edit_bone(cc3_rig, dst_rig, cc3_name, dst_name, dst_parent_name, scale):
if utils.edit_mode_to(cc3_rig):
src_bone = get_rl_edit_bone(cc3_rig, cc3_name)
if src_bone:
# cc3 rig is usually scaled by 0.01, so calculate the world positions.
head_pos = cc3_rig.matrix_world @ src_bone.head
tail_pos = cc3_rig.matrix_world @ src_bone.tail
roll = src_bone.roll
if utils.edit_mode_to(dst_rig):
# meta and rigify rigs are at 1.0 scale so all bones are in world space (at the origin)
dst_bone = dst_rig.data.edit_bones.new(dst_name)
dst_bone.head = head_pos
dst_bone.tail = head_pos + (tail_pos - head_pos) * scale
dst_bone.roll = roll
if dst_parent_name != "":
parent_bone = get_edit_bone(dst_rig, dst_parent_name)
if parent_bone:
dst_bone.parent = parent_bone
else:
utils.log_error(f"Could not find parent bone: {dst_parent_name} in target Rig!")
return dst_bone
else:
utils.log_error(f"Unable to edit target rig!")
else:
utils.log_error(f"Could not find bone: {cc3_name} in CC3 Rig!")
else:
utils.log_error(f"Unable to edit CC3 rig!")
return None
def copy_rig_bind_pose(rig_from, rig_to):
rig_def = {}
utils.set_only_active_object(rig_from)
if utils.edit_mode_to(rig_from):
for edit_bone in rig_from.data.edit_bones:
rig_def[edit_bone.name] = {
"head": edit_bone.head.copy(),
"tail": edit_bone.tail.copy(),
"roll": edit_bone.roll,
}
utils.set_only_active_object(rig_to)
if utils.edit_mode_to(rig_to):
for edit_bone in rig_to.data.edit_bones:
if edit_bone.name in rig_def:
bone_def = rig_def[edit_bone.name]
edit_bone.head = bone_def["head"].copy()
edit_bone.tail = bone_def["tail"].copy()
edit_bone.roll = bone_def["roll"]
def get_bone_children(bone, bone_list = None, include_root = False):
is_root = False
if bone_list is None:
is_root = True
bone_list = []
if include_root or not is_root:
bone_list.append(bone)
for child in bone.children:
get_bone_children(child, bone_list, include_root)
return bone_list
def get_edit_bone_subtree_defs(rig, bone : bpy.types.EditBone, tree = None):
if tree is None:
tree = []
# bone must have a parent for it to be a sub-tree
if utils.edit_mode_to(rig) and bone.parent:
bone_data = [bone.name,
rig.matrix_world @ bone.head,
rig.matrix_world @ bone.tail,
bone.head_radius,
bone.tail_radius,
bone.roll,
bone.parent.name]
tree.append(bone_data)
for child_bone in bone.children:
get_edit_bone_subtree_defs(rig, child_bone, tree)
return tree
def copy_rl_edit_bone_subtree(cc3_rig, dst_rig, cc3_name, dst_name, dst_parent_name, dst_prefix, collection, layer, vertex_group_map):
src_bone_defs = None
# copy the cc3 bone sub-tree to the destination rig
if utils.edit_mode_to(cc3_rig):
cc3_bone = get_edit_bone(cc3_rig, cc3_name)
src_bone_defs = get_edit_bone_subtree_defs(cc3_rig, cc3_bone)
if utils.edit_mode_to(dst_rig):
for bone_def in src_bone_defs:
src_name = bone_def[0]
if src_name == cc3_name:
name = dst_name
parent_name = dst_parent_name
else:
name = f"{dst_prefix}{bone_def[0]}"
src_parent_name = bone_def[6]
parent_name = vertex_group_map[src_parent_name]
head = bone_def[1]
tail = bone_def[2]
head_radius = bone_def[3]
tail_radius = bone_def[4]
roll = bone_def[5]
bone : bpy.types.EditBone = dst_rig.data.edit_bones.new(name)
bone.head = head
bone.tail = tail
bone.head_radius = head_radius
bone.tail_radius = tail_radius
bone.roll = roll
# store the name of the newly created bone (in case Blender has changed it)
vertex_group_map[src_name] = bone.name
bone_def.append(bone.name)
# set the edit bone layers
set_bone_collection(dst_rig, bone, collection, None, layer)
# set the bone parent
parent_bone = get_edit_bone(dst_rig, parent_name)
if parent_bone:
bone.parent = parent_bone
# set pose bone layers
if utils.object_mode():
for bone_def in src_bone_defs:
name = bone_def[7]
pose_bone = dst_rig.data.bones[name]
set_bone_collection(dst_rig, pose_bone, collection, None, layer)
return src_bone_defs
def add_copy_transforms_constraint(from_rig, to_rig, from_bone, to_bone, influence = 1.0, space="WORLD"):
try:
if utils.object_mode():
to_pose_bone : bpy.types.PoseBone = to_rig.pose.bones[to_bone]
c : bpy.types.CopyTransformsConstraint = to_pose_bone.constraints.new(type="COPY_TRANSFORMS")
c.target = from_rig
c.subtarget = from_bone
c.head_tail = 0
c.mix_mode = "REPLACE"
c.target_space = space
c.owner_space = space
c.influence = influence
return c
except Exception as e:
utils.log_error(f"Unable to add copy transforms constraint: {to_bone} {from_bone}", e)
return None
def add_copy_rotation_constraint(from_rig, to_rig, from_bone, to_bone, influence = 1.0, space="WORLD"):
try:
if utils.object_mode():
to_pose_bone : bpy.types.PoseBone = to_rig.pose.bones[to_bone]
c : bpy.types.CopyRotationConstraint = to_pose_bone.constraints.new(type="COPY_ROTATION")
c.target = from_rig
c.subtarget = from_bone
c.use_x = True
c.use_y = True
c.use_z = True
c.invert_x = False
c.invert_y = False
c.invert_z = False
c.mix_mode = "REPLACE"
c.target_space = space
if space == "LOCAL_OWNER_ORIENT":
space = "LOCAL"
c.owner_space = space
c.influence = influence
return c
except Exception as e:
utils.log_error(f"Unable to add copy rotation constraint: {to_bone} {from_bone}", e)
return None
def add_copy_scale_constraint(from_rig, to_rig, from_bone, to_bone, influence = 1.0, space="WORLD"):
try:
if utils.object_mode():
to_pose_bone : bpy.types.PoseBone = to_rig.pose.bones[to_bone]
c : bpy.types.CopyScaleConstraint = to_pose_bone.constraints.new(type="COPY_SCALE")
c.target = from_rig
c.subtarget = from_bone
c.use_x = True
c.use_y = True
c.use_z = True
c.target_space = space
if space == "LOCAL_OWNER_ORIENT":
space = "LOCAL"
c.owner_space = space
c.influence = influence
return c
except Exception as e:
utils.log_error(f"Unable to add copy scale constraint: {to_bone} {from_bone}", e)
return None
def add_copy_location_constraint(from_rig, to_rig, from_bone, to_bone, influence = 1.0, space="WORLD", axes=None):
try:
if utils.object_mode():
to_pose_bone : bpy.types.PoseBone = to_rig.pose.bones[to_bone]
c : bpy.types.CopyLocationConstraint = to_pose_bone.constraints.new(type="COPY_LOCATION")
c.target = from_rig
c.subtarget = from_bone
c.use_x = True
c.use_y = True
c.use_z = True
c.invert_x = False
c.invert_y = False
c.invert_z = False
c.target_space = space
if space == "LOCAL_OWNER_ORIENT":
space = "LOCAL"
c.owner_space = space
c.influence = influence
if axes:
c.use_x = "X" in axes
c.use_y = "Y" in axes
c.use_z = "Z" in axes
c.invert_x = "-X" in axes
c.invert_y = "-Y" in axes
c.invert_z = "-Z" in axes
return c
except Exception as e:
utils.log_error(f"Unable to add copy location constraint: {to_bone} {from_bone}", e)
return None
def add_stretch_to_constraint(from_rig, to_rig, from_bone, to_bone, influence = 1.0, head_tail = 0.0, space="WORLD"):
try:
if utils.object_mode():
to_pose_bone : bpy.types.PoseBone = to_rig.pose.bones[to_bone]
c : bpy.types.StretchToConstraint = to_pose_bone.constraints.new(type="STRETCH_TO")
c.target = from_rig
c.subtarget = from_bone
c.head_tail = head_tail
c.target_space = space
if space == "LOCAL_OWNER_ORIENT":
space = "LOCAL"
c.owner_space = space
c.influence = influence
return c
except Exception as e:
utils.log_error(f"Unable to add copy stretch to constraint: {to_bone} {from_bone}", e)
return None
def add_damped_track_constraint(rig, bone_name, target_name, influence):
try:
if utils.object_mode():
pose_bone : bpy.types.PoseBone = rig.pose.bones[bone_name]
c : bpy.types.DampedTrackConstraint = pose_bone.constraints.new(type="DAMPED_TRACK")
c.target = rig
c.subtarget = target_name
c.head_tail = 0
c.track_axis = "TRACK_Y"
c.influence = influence
return c
except Exception as e:
utils.log_error(f"Unable to add damped track constraint: {bone_name} {target_name}", e)
return None
def add_limit_distance_constraint(from_rig, to_rig, from_bone, to_bone, distance, influence = 1.0, space="WORLD"):
try:
if utils.object_mode():
to_pose_bone : bpy.types.PoseBone = to_rig.pose.bones[to_bone]
c : bpy.types.LimitDistanceConstraint = to_pose_bone.constraints.new(type="LIMIT_DISTANCE")
c.target = from_rig
c.subtarget = from_bone
c.distance = distance
c.limit_mode = "LIMITDIST_ONSURFACE"
c.target_space = space
c.owner_space = space
c.influence = influence
return c
except Exception as e:
utils.log_error(f"Unable to add limit distance constraint: {to_bone} {from_bone}", e)
return None
def add_child_of_constraint(parent_rig, child_rig, parent_bone, child_bone, influence = 1.0, space="WORLD"):
try:
if utils.object_mode():
to_pose_bone : bpy.types.PoseBone = child_rig.pose.bones[child_bone]
c : bpy.types.ChildOfConstraint = to_pose_bone.constraints.new(type="CHILD_OF")
c.target = parent_rig
c.subtarget = parent_bone
c.target_space = space
c.owner_space = space
c.influence = influence
return c
except Exception as e:
utils.log_error(f"Unable to add child of constraint: {child_bone} {parent_bone}", e)
return None
def add_inverse_kinematic_constraint(from_rig, to_rig, from_bone, to_bone, influence = 1.0, space="WORLD",
use_tail = True, use_stretch = True, use_rotation = True, use_location = True,
weight = 1.0, orient_weight = 0.0, chain_count = 1):
try:
if utils.object_mode():
to_pose_bone : bpy.types.PoseBone = to_rig.pose.bones[to_bone]
c : bpy.types.KinematicConstraint = to_pose_bone.constraints.new(type="IK")
c.target = from_rig
c.subtarget = from_bone
c.use_tail = use_tail
c.use_stretch = use_stretch
c.use_rotation = use_rotation
c.use_location = use_location
c.weight = weight
c.chain_count = chain_count
c.orient_weight = orient_weight
c.target_space = space
c.owner_space = space
c.influence = influence
return c
except Exception as e:
utils.log_error(f"Unable to add inverse kinematic constraint: {to_bone} {from_bone}", e)
return None
def set_pose_bone_lock(pose_bone : bpy.types.PoseBone,
lock_ik = [0, 0, 0],
lock_location = [0, 0, 0],
lock_rotation = [0, 0, 0, 0],
lock_scale = [0, 0, 0],):
for i, lock in enumerate(lock_location):
pose_bone.lock_location[i] = lock > 0
for i, lock in enumerate(lock_rotation):
if i == 3:
pose_bone.lock_rotation_w = lock > 0
else:
pose_bone.lock_rotation[i] = lock > 0
pose_bone.lock_ik_x = lock_ik[0] > 0
pose_bone.lock_ik_y = lock_ik[1] > 0
pose_bone.lock_ik_z = lock_ik[2] > 0
for i, lock in enumerate(lock_scale):
pose_bone.lock_scale[i] = lock > 0
def set_edit_bone_flags(edit_bone, flags, deform):
edit_bone.use_connect = True if "C" in flags else False
edit_bone.use_local_location = True if "L" in flags else False
edit_bone.use_inherit_rotation = True if "R" in flags else False
edit_bone.use_deform = deform
def store_armature_settings(rig, include_pose=False):
if not rig: return None
collections = {}
layers = []
if utils.B400():
for collection in rig.data.collections:
collections[collection.name] = collection.is_visible
else:
for i in range(0, 32):
layers.append(rig.data.layers[i])
visibility = { "layers": layers,
"collections": collections,
"show_in_front": rig.show_in_front,
"display_type": rig.display_type,
"pose_position": rig.data.pose_position,
"action": utils.safe_get_action(rig),
"location": rig.location }
if include_pose:
pose_data = {}
pose_bone: bpy.types.PoseBone
for pose_bone in rig.pose.bones:
pose_data[pose_bone.name] = [pose_bone.location, pose_bone.rotation_axis_angle, pose_bone.rotation_euler,
pose_bone.rotation_quaternion, pose_bone.scale, pose_bone.rotation_mode]
visibility["pose"] = pose_data
return visibility
def restore_armature_settings(rig, visibility, include_pose=False):
if not rig: return
if utils.B400():
collections = visibility["collections"]
for collection in collections:
rig.data.collections[collection].is_visible = collections[collection]
else:
layers = visibility["layers"]
for i in range(0, 32):
rig.data.layers[i] = layers[i]
rig.show_in_front = visibility["show_in_front"]
rig.display_type = visibility["display_type"]
rig.data.pose_position = visibility["pose_position"]
utils.safe_set_action(rig, visibility["action"])
rig.location = visibility["location"]
if include_pose:
pose_data = visibility["pose"]
for bone_name in pose_data:
rig.pose.bones[bone_name].rotation_mode = pose_data[bone_name][5]
rig.pose.bones[bone_name].location = pose_data[bone_name][0]
rig.pose.bones[bone_name].rotation_axis_angle = pose_data[bone_name][1]
rig.pose.bones[bone_name].rotation_euler = pose_data[bone_name][2]
rig.pose.bones[bone_name].rotation_quaternion = pose_data[bone_name][3]
rig.pose.bones[bone_name].scale = pose_data[bone_name][4]
def set_rig_bind_pose(rig):
rig.data.pose_position = "POSE"
utils.safe_set_action(rig, None)
clear_pose(rig)
def copy_position(rig, bone, copy_bones, offset):
if utils.edit_mode_to(rig):
if bone in rig.data.edit_bones:
edit_bone = rig.data.edit_bones[bone]
head_position = mathutils.Vector((0,0,0))
tail_position = mathutils.Vector((0,0,0))
num = 0
for copy_name in copy_bones:
if copy_name in rig.data.edit_bones:
copy_bone = rig.data.edit_bones[copy_name]
dir = (copy_bone.tail - copy_bone.head).normalized()
head_position += copy_bone.head + dir * offset
tail_position += copy_bone.tail + dir * offset
num += 1
head_position /= num
tail_position /= num
edit_bone.head = head_position
edit_bone.tail = tail_position
return edit_bone
else:
utils.log_error(f"Cannot find bone {bone} in rig!")
return None
def is_bone_in_collections(rig, bone: bpy.types.Bone, collections=None, groups=None, layers=None):
if utils.B400():
if collections:
for collection in collections:
if collection in rig.data.collections:
if bone.name in rig.data.collections[collection].bones:
return True
else:
if groups:
if bone.name in rig.pose.bones:
pose_bone: bpy.types.PoseBone = rig.pose.bones[bone.name]
if pose_bone.bone_group and pose_bone.bone_group.name in groups:
return True
if layers:
for layer in layers:
if not bone.layers[layer]:
return False
return True
return False
def set_bone_collection(rig, bone, collection=None, group=None, layer=None, color=None):
"""Sets the bone collection (Any) (Blender 4.0+),
or group (PoseBone only) or layer (Bone or EditBone) (< Blender 4.0)"""
if utils.B400():
if collection:
if not collection in rig.data.collections:
rig.data.collections.new(collection)
bone_collection = rig.data.collections[collection]
bone_collection.assign(bone)
if color is not None:
set_bone_color(bone, color)
else:
if group:
if group not in rig.pose.bone_groups:
rig.pose.bone_groups.new(name=group)
group = rig.pose.bone_groups[group]
if type(bone) is not bpy.types.PoseBone and bone.name in rig.pose.bones:
pose_bone = rig.pose.bones[bone.name]
pose_bone.bone_group = group
elif type(bone) is bpy.types.PoseBone:
bone.bone_group = group
if layer:
if type(bone) is bpy.types.PoseBone:
bone = bone.bone
bone.layers[layer] = True
for i, l in enumerate(bone.layers):
bone.layers[i] = i == layer
CUSTOM_COLORS = {
"Active": (0.7686275243759155, 1.0, 1.0),
"Select": (0.5960784554481506, 0.8980392813682556, 1.0),
"IK": (0.8000000715255737, 0.0, 0.0),
"FK": (0.3764706254005432, 0.7803922295570374, 0.20784315466880798),
"SPECIAL": (0.9803922176361084, 0.9019608497619629, 0.2392157018184662),
"SIM": (0.98, 0.24, 0.9),
"TWEAK": (0.2196078598499298, 0.49803924560546875, 0.7843137979507446),
"TWEAK_DISABLED": (0.270588, 0.396078, 0.521569),
"ROOT": (0.6901960968971252, 0.46666669845581055, 0.6784313917160034),
"DETAIL": (0.9843137860298157, 0.5372549295425415, 0.33725491166114807),
"DEFAULT": (0.3764706254005432, 0.7803922295570374, 0.20784315466880798),
"SKIN": (0.647059, 0.780392, 0.588235),
"PIVOT": (0.9803922176361084, 0.9019608497619629, 0.2392157018184662),
"MESH": (0.9803922176361084, 0.9019608497619629, 0.2392157018184662),
}
def set_bone_color(bone, color_code):
if utils.B400():
bone.color.palette = "CUSTOM"
bone.color.custom.normal = CUSTOM_COLORS[color_code]
bone.color.custom.active = CUSTOM_COLORS["Active"]
bone.color.custom.select = CUSTOM_COLORS["Select"]
def set_bone_collection_visibility(rig, collection, layer, visible, only=False):
if utils.B400():
if only:
for coll in rig.data.collections:
coll.is_visible = False
if collection in rig.data.collections:
rig.data.collections[collection].is_visible = visible
else:
rig.data.layers[layer] = visible
if only:
for i in range(0, 32):
if i != layer:
rig.data.layers[i] = not visible
def make_bones_visible(arm, protected=False, collections=None, layers=None):
bone : bpy.types.Bone
pose_bone : bpy.types.PoseBone
for bone in arm.data.bones:
pose_bone = get_pose_bone(arm, bone.name)
# make all active bone layers visible so they can be unhidden and selectable
if utils.B400():
for collection in arm.data.collections:
if collections:
collection.is_visible = collection.name in collections
else:
collection.is_visible = True
#if protected:
# collection.is_editable = True
else:
for i, l in enumerate(bone.layers):
if l:
if layers:
arm.data.layers[i] = i in layers
else:
arm.data.layers[i] = True
if protected:
arm.data.layers_protected[i] = False
# show and select bone
bone.hide = False
bone.hide_select = False
def is_bone_collection_visible(arm, collection=None, layer=None):
if utils.B400():
if collection in arm.data.collections:
return arm.data.collections[collection].is_visible
return False
else:
return arm.data.layers[layer]
def add_bone_collection(rig, collection):
if collection not in rig.data.collections:
rig.data.collections.new(collection)
return rig.data.collections[collection]
def assign_rl_base_collections(rig):
if utils.B400():
if "Deform" not in rig.data.collections:
deform = add_bone_collection(rig, "Deform")
none = add_bone_collection(rig, "Non-Deform")
twist = add_bone_collection(rig, "Twist")
share = add_bone_collection(rig, "Share")
none_deform_bones = [
"CC_Base_R_Upperarm",
"CC_Base_L_Upperarm",
"CC_Base_R_Forearm",
"CC_Base_L_Forearm",
"CC_Base_R_Thigh",
"CC_Base_L_Thigh",
"CC_Base_R_Calf",
"CC_Base_L_Calf",
"CC_Base_FacialBone",
"CC_Base_BoneRoot",
"RL_BoneRoot",
]
for bone in rig.data.bones:
if "Twist" in bone.name:
twist.assign(bone)
elif "ShareBone" in bone.name:
share.assign(bone)
if bone.name in none_deform_bones:
none.assign(bone)
else:
deform.assign(bone)
def get_distance_between(rig, bone_a_name, bone_b_name):
if utils.edit_mode_to(rig):
if bone_a_name in rig.data.edit_bones and bone_b_name in rig.data.edit_bones:
bone_a = rig.data.edit_bones[bone_a_name]
bone_b = rig.data.edit_bones[bone_b_name]
delta : mathutils.Vector = bone_b.head - bone_a.head
return delta.length
else:
utils.log_error(f"Could not find all bones: {bone_a_name} and {bone_b_name} in Rig!")
else:
utils.log_error(f"Unable to edit rig!")
return 0
def generate_eye_widget(rig, bone_name, bones, distance, scale):
wgt : bpy.types.Object = None
if utils.object_mode():
if len(bones) == 1:
bpy.ops.mesh.primitive_circle_add(vertices=16, radius=1, rotation=[0,0,11.25])
bpy.ops.object.transform_apply(rotation=True)
wgt = utils.get_active_object()
else:
bpy.ops.mesh.primitive_circle_add(vertices=16, radius=1.35, rotation=[0,0,11.25])
bpy.ops.object.transform_apply(rotation=True)
wgt = utils.get_active_object()
mesh : bpy.types.Mesh = wgt.data
vert: bpy.types.MeshVertex
for vert in mesh.vertices:
if vert.co.x < -0.01:
vert.co.x -= 0.5 * distance / scale
elif vert.co.x > 0.01:
vert.co.x += 0.5 * distance / scale
if wgt:
collection : bpy.types.Collection
for collection in bpy.data.collections:
if collection.name.startswith("WGTS_rig"):
collection.objects.link(wgt)
elif wgt.name in collection.objects:
collection.objects.unlink(wgt)
if bone_name in rig.pose.bones:
pose_bone : bpy.types.PoseBone
pose_bone = rig.pose.bones[bone_name]
pose_bone.custom_shape = wgt
wgt.name = "WGT-rig_" + bone_name
return wgt
def make_widget_collection(collection_name) -> bpy.types.Collection:
wgt_collection: bpy.types.Collection = None
for collection in bpy.data.collections:
if collection.name.startswith(collection_name):
wgt_collection = collection
if not wgt_collection:
wgt_collection = bpy.data.collections.new(collection_name)
bpy.context.scene.collection.children.link(wgt_collection)
wgt_collection.hide_render = True
layer_collections = utils.get_view_layer_collections(search=collection_name)
for collection in layer_collections:
collection.exclude = True
collection.hide_viewport = True
return wgt_collection
def add_widget_to_collection(widget, collection_name=None, collection_suffix=None, remove_other=True):
if collection_name:
widget_collection = make_widget_collection(collection_name)
if widget.name not in widget_collection.objects:
widget_collection.objects.link(widget)
for collection in bpy.data.collections:
if remove_other and collection != widget_collection and widget.name in collection.objects:
collection.objects.unlink(widget)
if collection_suffix:
for collection in bpy.data.collections:
if collection.name.startswith(collection_suffix):
collection.objects.link(widget)
elif remove_other and widget.name in collection.objects:
collection.objects.unlink(widget)
def make_sphere_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size,
rotation=[0,0,0], location=[0,0,0])
wgt1 = utils.get_active_object()
bpy.ops.object.transform_apply(rotation=True)
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size,
rotation=[1.570796,0,0], location=[0,0,0])
wgt2 = utils.get_active_object()
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size,
rotation=[0,1.570796,0], location=[0,0,0])
wgt3 = utils.get_active_object()
bpy.ops.object.transform_apply(rotation=True)
utils.try_select_objects([wgt1, wgt2, wgt3], True)
utils.set_active_object(wgt1)
bpy.ops.object.join()
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def make_circle_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size,
rotation=[0,0,0], location=[0,0,0])
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def make_root_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size,
rotation=[0,0,0], location=[0,0,0])
wgt1 = utils.get_active_object()
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size * 0.95,
rotation=[0,0,0], location=[0,0,0])
wgt2 = utils.get_active_object()
mesh = bpy.data.meshes.new(widget_name)
mesh.from_pydata([(-size, 0, 0), (size, 0, 0), (0, -size, 0), (0, size, 0)],
[(0, 1), (2,3)],
[])
mesh.update()
wgt3 = bpy.data.objects.new(widget_name, mesh)
wgt3.location = [0,0,0]
bpy.context.collection.objects.link(wgt3)
utils.try_select_objects([wgt1, wgt2, wgt3], True)
utils.set_active_object(wgt1)
bpy.ops.object.join()
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def make_axes_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
mesh = bpy.data.meshes.new(widget_name)
mesh.from_pydata([(-size, 0, 0), (size, 0, 0), (0, -size, 0), (0, size, 0), (0, 0, -size), (0, 0, size)],
[(0, 1), (2,3), (4,5)],
[])
mesh.update()
wgt = bpy.data.objects.new(widget_name, mesh)
wgt.location = [0,0,0]
bpy.context.collection.objects.link(wgt)
wgt.name = widget_name
return wgt
def make_spindle_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size,
rotation=[1.570796,0,0], location=[0,size,0])
bpy.ops.object.transform_apply(rotation=True)
wgt1 = utils.get_active_object()
mesh = bpy.data.meshes.new(widget_name)
mesh.from_pydata([(0, 0, 0), (0, 1, 0)],
[(0, 1)],
[])
mesh.update()
wgt2 = bpy.data.objects.new(widget_name, mesh)
wgt2.location = [0,0,0]
bpy.context.collection.objects.link(wgt2)
utils.try_select_objects([wgt1, wgt2], True)
utils.set_active_object(wgt1)
bpy.ops.object.join()
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def make_cone_spindle_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size*0.25,
rotation=[1.570796,0,0], location=[0,size*0.5,0])
bpy.ops.object.transform_apply(rotation=True)
wgt1 = utils.get_active_object()
wgt2 = bpy.ops.mesh.primitive_cone_add(vertices=4, radius1=size*0.125, radius2=0, depth=1,
rotation=[-1.570796,0,0], location=[0,size*0.5,0])
bpy.ops.object.transform_apply(rotation=True)
wgt2 = utils.get_active_object()
utils.try_select_objects([wgt1, wgt2], True)
utils.set_active_object(wgt1)
bpy.ops.object.join()
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def make_spike_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
wgt1 = bpy.ops.mesh.primitive_cone_add(vertices=4, radius1=size*0.125, radius2=0, depth=size*0.8,
rotation=[-1.570796,0,0], location=[0,size*0.6,0])
bpy.ops.object.transform_apply(rotation=True)
wgt1 = utils.get_active_object()
wgt2 = bpy.ops.mesh.primitive_cone_add(vertices=4, radius1=size*0.125, radius2=0, depth=size*0.2,
end_fill_type="NOTHING",
rotation=[ 1.570796,0,0], location=[0,size*0.1,0])
bpy.ops.object.transform_apply(rotation=True)
wgt2 = utils.get_active_object()
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size*0.25,
rotation=[1.570796,0,0], location=[0,size*0.5,0])
bpy.ops.object.transform_apply(rotation=True)
wgt3 = utils.get_active_object()
utils.try_select_objects([wgt1, wgt2, wgt3], True)
utils.set_active_object(wgt1)
bpy.ops.object.join()
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def make_limb_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size*0.25,
rotation=[1.570796,0,0], location=[0,size*0.5,0])
bpy.ops.object.transform_apply(rotation=True)
wgt1 = utils.get_active_object()
mesh = bpy.data.meshes.new(widget_name)
mesh.from_pydata([(0, 0, 0), (0, 1, 0)],
[(0, 1)],
[])
mesh.update()
wgt2 = bpy.data.objects.new(widget_name, mesh)
wgt2.location = [0,0,0]
bpy.context.collection.objects.link(wgt2)
utils.try_select_objects([wgt1, wgt2], True)
utils.set_active_object(wgt1)
bpy.ops.object.join()
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def make_cone_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
bpy.ops.mesh.primitive_circle_add(vertices=32, radius=size,
rotation=[1.570796,0,0], location=[0,0,0])
bpy.ops.object.transform_apply(rotation=True)
wgt1 = utils.get_active_object()
mesh = bpy.data.meshes.new(widget_name)
mesh.from_pydata([(0, size*2, 0), (0, 0, size), (size, 0, 0), (-size, 0, 0), (0, 0, -size)],
[(0, 1), (0, 2), (0, 3), (0, 4)],
[])
mesh.update()
wgt2 = bpy.data.objects.new(widget_name, mesh)
wgt2.location = [0,0,0]
bpy.context.collection.objects.link(wgt2)
utils.try_select_objects([wgt1, wgt2], True)
utils.set_active_object(wgt1)
bpy.ops.object.join()
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def make_dbl_circle_widget(widget_name, size):
if widget_name in bpy.data.objects:
wgt = bpy.data.objects[widget_name]
else:
bpy.ops.mesh.primitive_circle_add(vertices=64, radius=size,
rotation=[1.570796,0,0], location=[0,0,0])
bpy.ops.object.transform_apply(rotation=True)
wgt1 = utils.get_active_object()
bpy.ops.mesh.primitive_circle_add(vertices=64, radius=size * 1.025,
rotation=[1.570796,0,0], location=[0,0,0])
bpy.ops.object.transform_apply(rotation=True)
wgt2 = utils.get_active_object()
utils.try_select_objects([wgt1, wgt2], True)
utils.set_active_object(wgt1)
bpy.ops.object.join()
wgt = utils.get_active_object()
wgt.name = widget_name
return wgt
def generate_spring_widget(rig, name, type, size):
wgt : bpy.types.Object = None
wgt_name = "WGT-rig_" + name
if wgt_name in bpy.data.objects:
return bpy.data.objects[wgt_name]
if utils.object_mode():
if type == "FK":
wgt = make_spindle_widget(wgt_name, size)
if type == "IK":
wgt = make_cone_widget(wgt_name, size)
if type == "GRP":
wgt = make_dbl_circle_widget(wgt_name, size)
if type == "TWK":
wgt = make_sphere_widget(wgt_name, size)
if wgt:
add_widget_to_collection(wgt, collection_suffix="WGTS_rig")
return wgt
def add_pose_bone_custom_property(rig, pose_bone_name, prop_name, prop_value):
if utils.object_mode():
if pose_bone_name in rig.pose.bones:
pose_bone = rig.pose.bones[pose_bone_name]
rna_idprop_ui_create(pose_bone, prop_name, default=prop_value, overridable=True, min=0, max=1)
def add_constraint_scripted_influence_driver(rig, pose_bone_name, data_path, variable_name, constraint = None, constraint_type = "", expression = ""):
if utils.object_mode():
if pose_bone_name in rig.pose.bones:
pose_bone = rig.pose.bones[pose_bone_name]
cons = []
if constraint:
cons.append(constraint)
elif constraint_type:
for con in pose_bone.constraints:
if con.type == constraint_type:
cons.append(con)
for con in cons:
if expression:
driver = drivers.make_driver(con, "influence", "SCRIPTED", expression)
else:
driver = drivers.make_driver(con, "influence", "SUM")
if driver:
var = drivers.make_driver_var(driver, "SINGLE_PROP", variable_name, rig, target_type = "OBJECT", data_path = data_path)
def get_data_path_pose_bone_property(pose_bone_name, variable_name):
data_path = f"pose.bones[\"{pose_bone_name}\"][\"{variable_name}\"]"
return data_path
def get_data_rigify_limb_property(limb_id, variable_name):
"""
limb_id = "LEFT_LEFT", "RIGHT_LEFT", "LEFT_ARM", "RIGHT_ARM", "TORSO", "JAW", "EYES"\n
variable_name = "IK_Stretch", "IK_FK", "neck_follow", "head_follow", "mouth_lock", "eyes_follow"
"""
if limb_id == "LEFT_LEG":
return get_data_path_pose_bone_property("thigh_parent.L", variable_name)
elif limb_id == "RIGHT_LEFT":
return get_data_path_pose_bone_property("thigh_parent.R", variable_name)
elif limb_id == "LEFT_ARM":
return get_data_path_pose_bone_property("upper_arm_parent.L", variable_name)
elif limb_id == "RIGHT_ARM":
return get_data_path_pose_bone_property("upper_arm_parent.R", variable_name)
elif limb_id == "TORSO":
return get_data_path_pose_bone_property("torso", variable_name)
elif limb_id == "JAW":
return get_data_path_pose_bone_property("jaw_master", variable_name)
elif limb_id == "EYES":
return get_data_path_pose_bone_property("eyes", variable_name)
return ""
def add_bone_prop_driver(rig, pose_bone_name, bone_data_path, bone_data_index, props, prop_name, variable_name):
if utils.object_mode():
pose_bone : bpy.types.PoseBone
if pose_bone_name in rig.pose.bones:
pose_bone = rig.pose.bones[pose_bone_name]
fcurve : bpy.types.FCurve
fcurve = pose_bone.driver_add(bone_data_path, bone_data_index)
driver : bpy.types.Driver = fcurve.driver
driver.type = "SUM"
var : bpy.types.DriverVariable = driver.variables.new()
var.name = variable_name
var.type = "SINGLE_PROP"
var.targets[0].id_type = "SCENE"
var.targets[0].id = props.id_data
var.targets[0].data_path = props.path_from_id(prop_name)
def clear_constraints(rig, pose_bone_name):
if pose_bone_name:
if utils.object_mode():
if pose_bone_name in rig.pose.bones:
pose_bone = rig.pose.bones[pose_bone_name]
constraints = []
for con in pose_bone.constraints:
constraints.append(con)
for con in constraints:
pose_bone.constraints.remove(con)
def find_constraint(pose_bone: bpy.types.PoseBone, of_type, with_subtarget=None) -> bpy.types.Constraint:
if pose_bone:
con: bpy.types.Constraint
for con in pose_bone.constraints:
if con.type == of_type:
if with_subtarget and hasattr(con, "subtarget"):
if con.subtarget != with_subtarget:
continue
return con
return None
def clear_drivers(rig):
# rig object drivers (pose bone drivers)
drivers = rig.animation_data.drivers
if drivers:
fcurves = []
for fc in drivers:
fcurves.append(fc)
for fc in fcurves:
drivers.remove(fc)
# rig armature drivers (bone drivers)
drivers = rig.data.animation_data.drivers
if drivers:
fcurves = []
for fc in drivers:
fcurves.append(fc)
for fc in fcurves:
drivers.remove(fc)
def select_all_bones(arm, select = True, clear_active = True):
mode = utils.get_mode()
data = arm.data.bones
if mode == "EDIT":
data = arm.data.edit_bones
if data:
for bone in data:
bone.select_head = select
bone.select_tail = select
bone.select = select
if clear_active:
data.active = None
return True
else:
return False
def set_active_bone(arm, bone_name, deselect_all = True):
if deselect_all:
select_all_bones(arm, select=False, clear_active=True)
mode = utils.get_mode()
data = arm.data.bones
if mode == "EDIT":
data = arm.data.edit_bones
if bone_name in data:
bone = data[bone_name]
bone.select_head = True
bone.select_tail = True
bone.select = True
data.active = bone
return True
else:
return False
def get_bone_name_from_data_path(data_path : str):
if data_path.startswith("pose.bones[\""):
start = data_path.find('"', 0) + 1
end = data_path.find('"', start)
return data_path[start:end]
return None
def get_roll(bone):
mat = bone.matrix_local.to_3x3()
quat = mat.to_quaternion()
if abs(quat.w) < 1e-4:
roll = pi
else:
roll = 2*atan(quat.y/quat.w)
return roll
def clear_pose(arm):
"""Clears the pose, makes all bones visible and clears the bone selections."""
# select all bones in pose mode
arm.data.pose_position = "POSE"
utils.pose_mode_to(arm)
bone : bpy.types.Bone
make_bones_visible(arm)
for bone in arm.data.bones:
# show and select bone
bone.hide = False
bone.hide_select = False
bone.select = True
bone.select_head = True
bone.select_tail = True
# unlock the bones
pose_bone : bpy.types.PoseBone
for pose_bone in arm.pose.bones:
pose_bone.lock_location = [False, False, False]
pose_bone.lock_rotation = [False, False, False]
pose_bone.lock_rotation_w = False
pose_bone.lock_scale = [False, False, False]
# clear pose
bpy.ops.pose.transforms_clear()
# clear bone selections
for bone in arm.data.bones:
bone.select = False
bone.select_head = False
bone.select_tail = False
utils.object_mode_to(arm)
def reset_root_bone(arm):
if utils.edit_mode_to(arm):
root_bone = arm.data.edit_bones[0]
if "root" in root_bone.name.lower():
head = root_bone.head
length = root_bone.length
tail = head + mathutils.Vector((0,-1,0)) * length
root_bone.tail = tail
root_bone.align_roll(mathutils.Vector((0,0,1)))
utils.object_mode()
def bone_mapping_contains_bone(bone_mapping, bone_name):
for bone_mapping in bone_mapping:
if cmp_rl_bone_names(bone_mapping[1], bone_name):
return True
return False
def get_accessory_root_bone(bone_mapping, bone):
root = None
if not bone_mapping_contains_bone(bone_mapping, bone.name):
while bone.parent:
if not bone_mapping_contains_bone(bone_mapping, bone.parent.name):
root = bone.parent
bone = bone.parent
return root
def bone_parent_in_list(bone_list, bone):
if bone:
while bone.parent:
if bone.parent.name in bone_list:
return True
bone = bone.parent
return False
def find_accessory_bones(bone_mapping, cc3_rig):
accessory_bones = []
for bone in cc3_rig.data.bones:
bone_name = bone.name
if not bone_mapping_contains_bone(bone_mapping, bone_name):
if bone_name not in accessory_bones and not bone_parent_in_list(accessory_bones, bone):
utils.log_info(f"Accessory Bone: {bone_name}")
accessory_bones.append(bone_name)
return accessory_bones