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

5798 lines
242 KiB
Python

# ***** BEGIN GPL LICENSE BLOCK *****
#
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****
import bpy
#from . import Rigger_Toolbox
from mathutils import Matrix
import math
import mathutils
import numpy as np
import random
from . import Tools
import inspect
# import time
def draw_wgt(scale, bone, shape = 'sphere'):
if shape == 'BONE':
return None
if shape == 'ORIGINAL':
return bone.custom_shape
shape_obj_name = 'WGTB_' + bone.name
shape_name = 'WGTB_' + shape.lower() + '_' + bone.name
#check if the object and shape already exists
if shape_obj_name in bpy.data.objects:
shape_obj = bpy.data.objects[shape_obj_name]
#check if the shape exists
if shape_obj.data.name == shape_name:
return shape_obj
elif shape_name in bpy.data.meshes:
mesh = bpy.data.meshes[shape_name]
shape_obj.data = mesh
return shape_obj
else:
mesh = bpy.data.meshes.new(shape_name)
shape_obj.data = mesh
else:
if shape_name in bpy.data.meshes:
bpy.data.meshes.remove(bpy.data.meshes[shape_name])
mesh = bpy.data.meshes.new(shape_name)
shape_obj = bpy.data.objects.new(shape_obj_name, mesh)
#coordinates of the different widget shape
arrows = {'vertices': [(0.0, 0.0, -1.1981923897508295e-08), (0.0, 0.0, 0.40204665064811707), (0.0, 0.40204665064811707, -1.1981923897508295e-08), (0.40204665064811707, 0.0, -1.1981923897508295e-08), (-0.008419156074523926, 0.008419156074523926, 0.39362743496894836), (-0.008419156074523926, 0.008419156074523926, 0.4104657769203186), (-0.008419185876846313, -0.008419156074523926, 0.39362743496894836), (-0.008419156074523926, -0.008419156074523926, 0.4104657769203186), (0.008419156074523926, 0.008419156074523926, 0.39362743496894836), (0.008419156074523926, 0.008419156074523926, 0.4104657769203186), (0.008419156074523926, -0.008419156074523926, 0.39362743496894836), (0.008419156074523926, -0.008419156074523926, 0.4104657769203186), (0.39362743496894836, 0.008419156074523926, -0.008419156074523926), (0.39362743496894836, 0.008419156074523926, 0.008419156074523926), (0.39362743496894836, -0.008419156074523926, -0.008419156074523926), (0.39362743496894836, -0.008419156074523926, 0.008419156074523926), (0.4104657769203186, 0.008419156074523926, -0.008419156074523926), (0.4104657769203186, 0.008419156074523926, 0.008419156074523926), (0.4104657769203186, -0.008419156074523926, -0.008419156074523926), (0.4104657769203186, -0.008419156074523926, 0.008419156074523926), (-0.008419156074523926, 0.4104657769203186, -0.008419167250394821), (-0.008419156074523926, 0.4104657769203186, 0.008419143967330456), (-0.008419156074523926, 0.39362743496894836, -0.008419167250394821), (-0.008419156074523926, 0.39362743496894836, 0.008419143967330456), (0.008419156074523926, 0.4104657769203186, -0.008419167250394821), (0.008419156074523926, 0.4104657769203186, 0.008419143967330456), (0.008419156074523926, 0.39362743496894836, -0.008419167250394821), (0.008419156074523926, 0.39362743496894836, 0.008419143967330456)], 'edges': [[2, 0], [0, 1], [0, 3], [6, 4], [4, 5], [5, 7], [7, 6], [10, 6], [7, 11], [11, 10], [8, 10], [11, 9], [9, 8], [4, 8], [9, 5], [14, 12], [12, 13], [13, 15], [15, 14], [18, 14], [15, 19], [19, 18], [16, 18], [19, 17], [17, 16], [12, 16], [17, 13], [22, 20], [20, 21], [21, 23], [23, 22], [26, 22], [23, 27], [27, 26], [24, 26], [27, 25], [25, 24], [20, 24], [25, 21]], 'faces': []}
plain_axes = {'vertices': [(0.0, 0.0, 0.40140801668167114), (0.0, 0.0, -0.40140801668167114), (0.40140801668167114, 0.0, 0.0), (-0.40140801668167114, 0.0, 0.0), (0.0, -0.40140801668167114, 0.0), (0.0, 0.40140801668167114, 0.0), (0.0, 0.0, 0.0)], 'edges': [[6, 5], [4, 6], [6, 1], [6, 3], [0, 6], [2, 6]], 'faces': []}
single_arrow = {'vertices': [(0.0, 1.3104262563956581e-08, 0.2997905910015106), (0.0, 0.0, 0.0), (0.011538410559296608, 0.013322020880877972, 0.29990124702453613), (0.011538410559296608, -0.013321994803845882, 0.29990124702453613), (0.0, 1.7448547495746425e-08, 0.3991762399673462), (-0.011538410559296608, 0.013322020880877972, 0.29990124702453613), (-0.011538410559296608, -0.013321994803845882, 0.29990124702453613)], 'edges': [[0, 1], [2, 4], [2, 3], [5, 2], [3, 6], [5, 6], [6, 4], [4, 5], [4, 3]], 'faces': []}
circle = {'vertices': [(1.3969838619232178e-09, -1.7606295088512525e-08, -0.4027850925922394), (-0.1541391760110855, -1.6266094249317575e-08, -0.37212488055229187), (-0.2848120629787445, -1.2449531183733598e-08, -0.2848120629787445), (-0.37212488055229187, -6.737637558984488e-09, -0.1541391760110855), (-0.4027850925922394, 5.551115123125783e-17, 0.0), (-0.37212488055229187, 6.737637558984488e-09, 0.1541391760110855), (-0.2848120629787445, 1.2449531183733598e-08, 0.2848120629787445), (-0.1541391760110855, 1.6266094249317575e-08, 0.37212488055229187), (1.3969838619232178e-09, 1.7606295088512525e-08, 0.4027850925922394), (0.1541391760110855, 1.6266094249317575e-08, 0.37212488055229187), (0.2848120629787445, 1.2449531183733598e-08, 0.2848120629787445), (0.37212488055229187, 6.737637558984488e-09, 0.1541391760110855), (0.4027850925922394, 5.551115123125783e-17, 0.0), (0.37212488055229187, -6.737637558984488e-09, -0.1541391760110855), (0.2848120629787445, -1.2449531183733598e-08, -0.2848120629787445), (0.1541391760110855, -1.6266094249317575e-08, -0.37212488055229187)], 'edges': [[1, 0], [2, 1], [3, 2], [4, 3], [5, 4], [6, 5], [7, 6], [8, 7], [9, 8], [10, 9], [11, 10], [12, 11], [13, 12], [14, 13], [15, 14], [0, 15]], 'faces': []}
cube = {'vertices': [(-0.4013901352882385, -0.4013901352882385, -0.4013901352882385), (-0.4013901352882385, -0.4013901352882385, 0.4013901352882385), (-0.4013901352882385, 0.4013901352882385, 0.4013901352882385), (-0.4013901352882385, 0.4013901352882385, -0.4013901352882385), (0.4013901352882385, -0.4013901352882385, -0.4013901352882385), (0.4013901352882385, -0.4013901352882385, 0.4013901352882385), (0.4013901352882385, 0.4013901352882385, 0.4013901352882385), (0.4013901352882385, 0.4013901352882385, -0.4013901352882385)], 'edges': [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]], 'faces': []}
sphere = {'vertices': [(0.0, 0.40137654542922974, 0.0), (-0.10388384014368057, 0.3876999318599701, 0.0), (-0.20068827271461487, 0.34760206937789917, 0.0), (-0.2838158905506134, 0.2838158905506134, 0.0), (-0.34760230779647827, 0.20068813860416412, 0.0), (-0.3876999318599701, 0.10388384014368057, 0.0), (-0.40137654542922974, 3.030309159157696e-08, 0.0), (-0.3876999318599701, -0.10388379544019699, 0.0), (-0.34760230779647827, -0.20068813860416412, 0.0), (-0.2838161289691925, -0.2838158905506134, 0.0), (-0.20068827271461487, -0.34760206937789917, 0.0), (-0.10388395935297012, -0.3876999318599701, 0.0), (-1.5630175198566576e-07, -0.40137654542922974, 0.0), (0.10388367623090744, -0.3876999318599701, 0.0), (0.20068803429603577, -0.34760230779647827, 0.0), (0.2838158905506134, -0.2838161289691925, 0.0), (0.34760206937789917, -0.20068837702274323, 0.0), (0.387699693441391, -0.10388403385877609, 0.0), (0.40137654542922974, -1.8660485068267008e-07, 0.0), (0.3876999318599701, 0.10388367623090744, 0.0), (0.34760230779647827, 0.20068803429603577, 0.0), (0.2838161289691925, 0.2838158905506134, 0.0), (0.20068851113319397, 0.34760206937789917, 0.0), (0.10388415306806564, 0.387699693441391, 0.0), (0.0, 2.990487502074757e-08, 0.40137654542922974), (-0.10388384014368057, 2.990487502074757e-08, 0.3876999318599701), (-0.20068827271461487, 2.990487502074757e-08, 0.34760206937789917), (-0.2838158905506134, 2.990487502074757e-08, 0.2838158905506134), (-0.34760230779647827, 1.4952437510373784e-08, 0.20068813860416412), (-0.3876999318599701, 7.476218755186892e-09, 0.10388384014368057), (-0.40137654542922974, 3.564938905328222e-15, 3.030309159157696e-08), (-0.3876999318599701, -7.476218755186892e-09, -0.10388379544019699), (-0.34760230779647827, -1.4952437510373784e-08, -0.20068813860416412), (-0.2838161289691925, -2.990487502074757e-08, -0.2838158905506134), (-0.20068827271461487, -2.990487502074757e-08, -0.34760206937789917), (-0.10388395935297012, -2.990487502074757e-08, -0.3876999318599701), (-1.5630175198566576e-07, -2.990487502074757e-08, -0.40137654542922974), (0.10388367623090744, -2.990487502074757e-08, -0.3876999318599701), (0.20068803429603577, -2.990487502074757e-08, -0.34760230779647827), (0.2838158905506134, -2.990487502074757e-08, -0.2838161289691925), (0.34760206937789917, -1.4952437510373784e-08, -0.20068837702274323), (0.387699693441391, -7.476218755186892e-09, -0.10388403385877609), (0.40137654542922974, -1.425975562131289e-14, -1.8660485068267008e-07), (0.3876999318599701, 7.476218755186892e-09, 0.10388367623090744), (0.34760230779647827, 1.4952437510373784e-08, 0.20068803429603577), (0.2838161289691925, 2.990487502074757e-08, 0.2838158905506134), (0.20068851113319397, 2.990487502074757e-08, 0.34760206937789917), (0.10388415306806564, 2.990487502074757e-08, 0.387699693441391), (-2.990487502074757e-08, 1.782469452664111e-15, 0.40137654542922974), (-3.738109199957762e-08, -0.10388384014368057, 0.3876999318599701), (-4.485731253112135e-08, -0.20068827271461487, 0.34760206937789917), (-5.980975004149514e-08, -0.2838158905506134, 0.2838158905506134), (-2.990487502074757e-08, -0.34760230779647827, 0.20068813860416412), (-2.990487502074757e-08, -0.3876999318599701, 0.10388384014368057), (-2.990487502074757e-08, -0.40137654542922974, 3.030309159157696e-08), (-2.990487502074757e-08, -0.3876999318599701, -0.10388379544019699), (0.0, -0.34760230779647827, -0.20068813860416412), (0.0, -0.2838161289691925, -0.2838158905506134), (1.4952437510373784e-08, -0.20068827271461487, -0.34760206937789917), (2.2428656265560676e-08, -0.10388395935297012, -0.3876999318599701), (2.990486080989285e-08, -1.5630175198566576e-07, -0.40137654542922974), (3.738109199957762e-08, 0.10388367623090744, -0.3876999318599701), (4.485731253112135e-08, 0.20068803429603577, -0.34760230779647827), (5.980975004149514e-08, 0.2838158905506134, -0.2838161289691925), (2.990487502074757e-08, 0.34760206937789917, -0.20068837702274323), (2.990487502074757e-08, 0.387699693441391, -0.10388403385877609), (2.990487502074757e-08, 0.40137654542922974, -1.8660485068267008e-07), (2.990487502074757e-08, 0.3876999318599701, 0.10388367623090744), (0.0, 0.34760230779647827, 0.20068803429603577), (0.0, 0.2838161289691925, 0.2838158905506134), (-1.4952437510373784e-08, 0.20068851113319397, 0.34760206937789917), (-2.2428656265560676e-08, 0.10388415306806564, 0.387699693441391), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 0.0)], 'edges': [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 11], [11, 12], [12, 13], [13, 14], [14, 15], [15, 16], [16, 17], [17, 18], [18, 19], [19, 20], [20, 21], [21, 22], [22, 23], [0, 23], [24, 25], [25, 26], [26, 27], [27, 28], [28, 29], [29, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, 37], [37, 38], [38, 39], [39, 40], [40, 41], [41, 42], [42, 43], [43, 44], [44, 45], [45, 46], [46, 47], [24, 47], [48, 49], [49, 50], [50, 51], [51, 52], [52, 53], [53, 54], [54, 55], [55, 56], [56, 57], [57, 58], [58, 59], [59, 60], [60, 61], [61, 62], [62, 63], [63, 64], [64, 65], [65, 66], [66, 67], [67, 68], [68, 69], [69, 70], [70, 71], [48, 71]], 'faces': []}
cone = {'vertices': [(-3.0009825735533013e-09, 0.402785062789917, 1.7763568394002505e-15), (0.2848120331764221, 0.2848120331764221, 0.0), (0.402785062789917, 0.0, 0.0), (0.2848120331764221, -0.2848120331764221, 0.0), (-3.0009825735533013e-09, -0.402785062789917, -1.7763568394002505e-15), (-0.2848120331764221, -0.2848120331764221, 0.0), (-0.402785062789917, 0.0, 0.0), (-0.2848120331764221, 0.2848120331764221, 0.0), (-3.0009825735533013e-09, -3.552713678800501e-15, 0.805570125579834)], 'edges': [[0, 1], [0, 8], [8, 1], [1, 2], [8, 2], [2, 3], [8, 3], [3, 4], [8, 4], [4, 5], [8, 5], [5, 6], [8, 6], [6, 7], [8, 7], [7, 0]], 'faces': []}
shapes = {'arrows':arrows, 'plain_axes':plain_axes,'single_arrow':single_arrow, 'cube':cube, 'sphere' : sphere , 'circle' : circle, 'cone' : cone}
shape = shape.lower()
mesh.from_pydata(np.array(shapes[shape]['vertices'])*scale , shapes[shape]['edges'], shapes[shape]['faces'])
shape_obj['WGT_TempCtrl'] = True
return shape_obj
def original_shape_properties(ctrl, posebone, size):
'''copy all the properties from the original bone shape'''
for prop in dir(posebone):
if ctrl.btc.setup == 'POLE' and prop == 'custom_shape_transform':
continue
#iterate only over custom shape properties
if 'custom_shape' not in prop:
continue
if not hasattr(ctrl, prop):
continue
value = getattr(posebone, prop)
if 'scale' in prop:
value = tuple(value * size)
setattr(ctrl, prop, value)
def get_ctrls_from_selection(scene, ids, controllers, id_type = 'org_id'):
'''choose which bones are relevant based on the enum selection properties'''
#if setup_ids is in globals then it is running the operator and should be using selected
select_filter = 'SELECTED' if 'setup_ids' in globals() else scene.btc.target
ctrls = []
#get the relevant bones
for rig in controllers:
if rig.type == 'ARMATURE':
for ctrl in rig.pose.bones:
#Filter out Helper bones
if ctrl.btc.org in {'MCH', 'ORG'}:
continue
id = getattr(ctrl.btc, id_type)
#filter the bones using btc.target
if select_filter == 'SELECTED':
if id not in ids:
continue
else:
if not id:
continue
ctrls.append(ctrl)
elif rig.type == 'EMPTY':
id = getattr(rig.animtoolbox, 'setup_id')
# filter the bones using btc.target
if select_filter == 'SELECTED':
if id not in ids:
continue
else:
if not id:
continue
ctrls.append(rig)
return ctrls
def tempctrl_shapesize(self, context):
'''multiple the size of the controller shapes'''
scene = context.scene
org_ids = get_bone_ids(context, id = 'org_id')
setup_ids = get_bone_ids(context, id = 'setup_id')
controllers, controlled_objs = get_controllers_controlled(context)
ctrls = get_ctrls_from_selection(scene, setup_ids, controllers, id_type = 'setup_id')
ctrl_org_bones = map_ctrl_org_bones(org_ids, controllers, controlled_objs, id_type = 'org_id')
if any([ctrl.type == 'EMPTY' for ctrl in controllers]):
empty_org_bones = map_ctrl_org_bones(setup_ids, controllers, controlled_objs, id_type = 'setup_id')
for ctrl in ctrls:
if ctrl.id_data.type == 'ARMATURE':
scale = mathutils.Vector([self.shape_size]*3)
if ctrl in ctrl_org_bones:
org_bone = ctrl_org_bones[ctrl]
if ctrl.custom_shape == org_bone.custom_shape:
#Multiple the scale with the scale from the original bone
scale = mathutils.Vector(np.array(org_bone.custom_shape_scale_xyz) * tuple([self.shape_size]*3))
#child bones that have the same bone length get half of the size
if ctrl.btc.child and round(ctrl.id_data.data.bones[ctrl.name].length, 2) == round(ctrl.id_data.data.bones[ctrl.parent.name].length, 2):
scale *= 0.5
ctrl.custom_shape_scale_xyz = scale
elif ctrl.id_data.type == 'EMPTY':
org_bone = empty_org_bones[ctrl]
ctrl.empty_display_size = self.shape_size * org_bone.length * 0.5
#if it's a child then make it smaller
if ctrl.animtoolbox.child : ctrl.empty_display_size *= 0.5
def tempctrl_shape_type(self, context):
'''change the display type of the ctrl bone'''
scene = context.scene
setup_ids = get_bone_ids(context, id = 'setup_id')
org_ids = get_bone_ids(context, id = 'org_id')
controllers, controlled_objs = get_controllers_controlled(context)
ctrls = get_ctrls_from_selection(scene, setup_ids, controllers, id_type = 'setup_id')#
if not ctrls:
return
#map bone and ctrls
ctrl_org_bones = map_ctrl_org_bones(setup_ids, controllers, controlled_objs, id_type = 'setup_id')
if not len(ctrl_org_bones.values()):
return
length_avg = sum([ctrl.id_data.data.bones[ctrl.name].length for ctrl in ctrl_org_bones.values()]) / len(ctrl_org_bones.values())
for ctrl in ctrls:
if ctrl.id_data.type == 'EMPTY':
ctrl.empty_display_type = self.shape_type
continue
if self.shape_type == 'ORIGINAL':
if ctrl not in ctrl_org_bones:
continue
org_bone = ctrl_org_bones[ctrl]
original_shape_properties(ctrl, org_bone , self.shape_size)
if ctrl.btc.setup == 'POLE':
continue
if org_bone.custom_shape_transform is not None and ctrl.custom_shape_transform is None:
#check if the transform bone shape already exists
transform_bonename = org_bone.custom_shape_transform.name
if transform_bonename in ctrl.id_data.pose.bones:
ctrl.custom_shape_transform = ctrl.id_data.pose.bones[transform_bonename]
continue
#add an extra transform bone, if it doesnt exist
rig = ctrl.id_data
bpy.ops.object.mode_set(mode = 'EDIT')
boneshape = add_ctrl_bone(rig, org_bone.custom_shape_transform, rig.data.edit_bones[ctrl.name].parent, '')
bone_name = boneshape.name
add_bone_to_collection(rig, boneshape)
bpy.ops.object.mode_set(mode = 'POSE')
posebone = rig.pose.bones[bone_name]
constraint_add(posebone, org_bone.custom_shape_transform, 'COPY_TRANSFORMS')
ctrl.custom_shape_transform = posebone
else:
# org_obj = ctrl.id_data.animtoolbox.controlled
length = (length_avg / ctrl.length)*0.4 + 0.4
# ctrl.custom_shape = draw_wgt(length, ctrl_org_bones[ctrl], shape = self.shape_type)
ctrl.custom_shape = draw_wgt(length, ctrl, shape = self.shape_type)
# ctrl.use_custom_shape_bone_size = False
ctrl.custom_shape_scale_xyz = tuple([self.shape_size]*3)
ctrl.custom_shape_transform = None
#If it's a child then assign the parent shape
for ctrl in ctrls:
if ctrl.id_data.type == 'EMPTY':
continue
if not ctrl.parent and not ctrl.btc.child:
continue
if ctrl.parent in ctrls and ctrl.btc.org_id == ctrl.parent.btc.org_id:
ctrl.custom_shape = ctrl.parent.custom_shape
ctrl.custom_shape_transform = ctrl.parent.custom_shape_transform
#Get the scale from the parent, if they are the same length then get half of it
scale = ctrl.parent.custom_shape_scale_xyz.copy()
rig = ctrl.id_data
if round(rig.data.bones[ctrl.name].length, 2) == round(rig.data.bones[ctrl.parent.name].length, 2):
scale *= 0.5
ctrl.custom_shape_scale_xyz = scale
def map_ctrl_org_bones(ids, controllers, controlled_objs, id_type = 'setup_id'):
'''returns a dictionary of the ctrls and original bones'''
ctrl_org_bones = dict()
ids_org_bone = dict()
#get the original bones and their ids in a dictionary
for org_rig in controlled_objs:
ids_org_bone = {getattr(bone.btc, id_type) : bone for bone in org_rig.pose.bones if getattr(bone.btc, id_type) in ids}
for ctrl_rig in controllers:
if ctrl_rig.type == 'ARMATURE':
for ctrl_bone in ctrl_rig.pose.bones:
id = getattr(ctrl_bone.btc, id_type)
if id not in ids:
continue
if id not in ids_org_bone:
continue
ctrl_org_bones.update({ctrl_bone : ids_org_bone[id]})
elif ctrl_rig.type == 'EMPTY':
id = getattr(ctrl_rig.animtoolbox, 'setup_id')
if id not in ids:
continue
if id not in ids_org_bone:
continue
ctrl_org_bones.update({ctrl_rig : ids_org_bone[id]})
return ctrl_org_bones
def get_bone_color_sets(context, self):
# Get the current theme
theme = bpy.context.preferences.themes[0]
# Get all bone color sets from the theme
color_sets = []
for i, color_set in enumerate(theme.bone_color_sets, start=1):
if color_set.normal: # Check if the color set is not empty
i_str = str(i).zfill(2)
color_icon_name = "COLORSET_{}_VEC".format(i_str)
color_set_name = "THEME"+i_str
text = i_str + ' - Theme Color Set'
color_sets.append((color_set_name, text, text, color_icon_name, i))
return color_sets
def update_bone_color(context, self):
context = bpy.context
scene = context.scene
color_set = scene.btc.color_set
org_ids = get_bone_ids(context, id = 'org_id')
setup_ids = get_bone_ids(context, id = 'setup_id')
controllers, controlled_objs = get_controllers_controlled(context)
ctrls = get_ctrls_from_selection(scene, setup_ids, controllers, id_type = 'setup_id')
# ctrl_org_bones = map_ctrl_org_bones(setup_ids, controllers, controlled_objs, id_type = 'setup_id')
for ctrl in ctrls:
if ctrl.id_data.type == 'EMPTY':
continue
ctrl.color.palette = color_set
def add_twist_to_track(source, target, constraints):
if target.id_data.type == 'ARMATURE':
#Adding an extra copy rotation for the twist axis
for rot_bone in target.id_data.pose.bones:
#Find the org bone to use for local rotation for the twist
if rot_bone.btc.org == 'ORG' and rot_bone.btc.setup_id == target.btc.setup_id:
break
constraint_add(rot_bone, target, 'COPY_ROTATION')
rot_con = constraint_add(source, rot_bone, 'COPY_ROTATION')
rot_con.target_space = 'LOCAL'
rot_con.owner_space = 'LOCAL'
else:
rot_con = constraint_add(source, target, 'COPY_ROTATION')
constraints.append(rot_con)
return rot_con
def constraints_filter(self, source, target, con_type = 'COPY_TRANSFORMS'):
'''add constraints but with attribute filter'''
constraints = []
attr_dict = {'x' : 0, 'y' : 1, 'z' : 2}
#update all the properties and constraint for the twist before adding track constraints
if con_type == 'DAMPED_TRACK':
# twist = False if con_type == 'COPY_TRANSFORMS' else self.twist
self.filter_location = (False, False, False)
self.filter_scale = (False, False, False)
if self.twist:
rot_con = add_twist_to_track(source, target, constraints)
#Adding Track constraint without any filters
all_attributes = list(self.filter_location) + list(self.filter_rotation) + list(self.filter_scale)
if not any(all_attributes) : # or con_type == 'TRACK_TO'
con = constraint_add(source, target, con_type)
constraints.append(con)
return constraints
con = None
#Adding Constraints with filtered attributes
if con_type == 'COPY_TRANSFORMS':
filter_con_type = {'filter_rotation': 'COPY_ROTATION', 'filter_scale' : 'COPY_SCALE', 'filter_location' : 'COPY_LOCATION'}
for filter, con_type in filter_con_type.items():
#check if anything is turned on for this attribute
filter_transform = getattr(self, filter)
if all(filter_transform):
continue
con = constraint_add(source, target, con_type)
for attr, i in attr_dict.items():
setattr(con, 'use_' + attr, not filter_transform[i])
constraints.append(con)
else:
#In Case Track to is using Filters
if any(list(self.filter_rotation)):
for attr, i in attr_dict.items():
#Exclude axes that are not part of the twist
if self.twist and attr != self.axis[1].lower():
setattr(rot_con,'use_' + attr, False)
if self.filter_rotation[i]:
continue
#if it's the same axis of the direction axis then skip it
if attr.upper() in self.axis:
continue
con = constraint_add(source, target, 'LOCKED_TRACK')
con.lock_axis = ('LOCK_' + attr).upper()
constraints.append(con)
return constraints
def constraint_add(source, target, con_type = 'COPY_TRANSFORMS'):
'''Add a constraint with the source and target, available both to bones and empties'''
constraint = source.constraints.new(con_type)
if target:
if hasattr(target, 'bone'):
constraint.target = target.id_data
constraint.subtarget = target.name
else:
constraint.target = target
#Add name suffix if it's not on a control rig object
# if 'bate' not in constraint.id_data.keys():
constraint.name = con_type.capitalize() + '_to_Ctrl'
return constraint
def constraint_ctrl_to_bone(ctrl, bone, bake_loc, con_type, constraints = []):
constraint = constraint_add(ctrl, bone, con_type)
if bake_loc == 'TAIL':
constraint.head_tail = 1
constraints.append(constraint)
return constraints
def constraint_org_to_ctrl(self, ctrls, selected):
if not self.con_back:
return
# con_type = 'DAMPED_TRACK' if self.bake_loc == 'TRACK' else 'COPY_TRANSFORMS'
if self.bake_loc == 'TRACK':
# con_type = 'TRACK_TO' if self.twist and 'Y' in self.axis else 'DAMPED_TRACK'
con_type = 'DAMPED_TRACK'
else:
con_type = 'COPY_TRANSFORMS'
for ctrl, bone in zip(ctrls, selected):
if ctrl.btc.setup == 'ROOT': #dont need to add constrain for the root bone
continue
if self.child and child_names: #switch to the child controling the original bone
child_name = child_names[ctrls.index(ctrl)]
child_ctrl = rig.pose.bones[child_name]
ctrl = child_ctrl
# add constraints but with attribute filter
constraints = constraints_filter(self, bone, ctrl, con_type)
if not constraints:
continue
for constraint in constraints:
if constraint.type in {'DAMPED_TRACK', 'TRACK_TO', 'LOCKED_TRACK'}:
direction = 'NEGATIVE_' if '-' == self.axis[0] else ''
constraint.track_axis = 'TRACK_' + direction + self.axis[1]
add_enabled_driver(bone.id_data, ctrl.id_data, constraint)
bone.bone.select = False
def create_scene_collection(obj):
#create a collection
if 'Ctrl_Helpers' not in bpy.data.collections:
collection = bpy.data.collections.new('Ctrl_Helpers')
bpy.context.scene.collection.children.link(collection)
else:
collection = bpy.data.collections['Ctrl_Helpers']
if obj not in collection.objects.values():
collection.objects.link(obj)
return collection
def register_ctrl(controller, controlled):
ctrl_items = bpy.context.scene.btc.ctrl_items
#check if it's already registered
controllers = [item.controller for item in ctrl_items]
if controller in controllers:
return
#register the controller in the scene
ctrl_item = ctrl_items.add()
ctrl_item.controller = controller
ctrl_item.controlled = controlled
def find_empty_ctrl(context, posebone):
#Get all the empty ctrls
empty_ctrls = [ctrl.controller for ctrl in context.scene.btc.ctrl_items if ctrl.controller.type == 'EMPTY']
for empty in empty_ctrls:
if posebone.btc.setup_id == empty.animtoolbox.setup_id:
return empty
def add_empty(context, source=None, nameextension = 'Ctrl_'):
scene = context.scene
btc = scene.btc
if source:
empty = bpy.data.objects.new(nameextension + source.name, None)
else:
#If it's not assigned from bone or object then create a root at cursor
empty = bpy.data.objects.new('Root', None)
source = scene.cursor
empty.empty_display_size = btc.shape_size * 0.5
if hasattr(source, 'length'):
empty.empty_display_size *= source.length
empty.empty_display_type = btc.shape_type
#bpy.context.btc.ctrl_items.controller = empty
#for a bone or cursor assign matrix, for empty matrix world
if hasattr(source, 'matrix'):
empty.matrix_world = source.matrix.to_4x4()
elif hasattr(source, 'matrix_world'):
empty.matrix_world = source.matrix_world.to_4x4()
temp_col = create_scene_collection(empty)
return empty
def add_control_empties(self, context, bones):
#Add an empty to the scene
empties = []
root_empty = None
setup = 'EMPTY' if self.bake_loc == 'COPY' else 'TRACK_TO_EMPTY'
setup_ids = set()
for bone in bones:
empty = add_empty(context, bone)
#add setup id
setup_id = add_id(empty, setup)
setup_ids.add(setup_id)
bone.btc.setup_id = empty.animtoolbox.setup_id = setup_id
bone.btc.setup = setup
#define controller and controlled
empty.animtoolbox.controlled = bone.id_data
register_ctrl(empty, bone.id_data)
if root_empty:
empty.parent = root_empty
empties.append(empty)
return empties, root_empty, setup_ids
def ctrls_orientation(self, ctrls, child_names):
if not self.orientation:
return
for i, ctrl in enumerate(ctrls):
ctrl_edit = rig.data.edit_bones[ctrl.name]
loc, rot, scale = ctrl_edit.matrix.decompose()
new_rot = mathutils.Euler((0, 0, 0), 'XYZ').to_matrix()
ctrl_edit.matrix = Matrix.LocRotScale(loc, new_rot, scale)
if not self.child:
continue
child_edit = rig.data.edit_bones[child_names[i]]
loc, rot, scale = child_edit.matrix.decompose()
child_edit.matrix = Matrix.LocRotScale(loc, new_rot, scale)
def get_bone_matrix(bone):
'''check for type of matrix, bone or object'''
if hasattr(bone, 'matrix_local'):
matrix = bone.matrix_local
#Relevant for edit bones
elif hasattr(bone, 'matrix'):
matrix = bone.matrix
#Relevant for object empties
elif hasattr(bone, 'matrix_world'):
matrix = bone.matrix_world
# if type(bone.id_data) == bpy.types.Object:
# matrix = bone.id_data.matrix_world @ matrix
return matrix
def add_ctrl_bone(rig, bone, parent_bone, nameextension = 'Ctrl_', bake_loc = 'HEAD', length_factor = 1, orient_world = False):
ctrl_bone = rig.data.edit_bones.new(nameextension + bone.name)
ctrl_bone.parent = parent_bone
if bone is None:
#sctrl_bone.matrix_world = Matrix()
ctrl_bone.matrix = Matrix()
else:
#get the source bone
bone_rig = bone.id_data
#check that the bone id data is object and not armature
if type(bone_rig) == bpy.types.Object:
bone_rig = bone_rig.data
if bone.name in bone_rig.edit_bones:
bone = bone_rig.edit_bones[bone.name]
#in case the bone still doesn't exist in the edit bones then get it from data.bones,
# usually when the object is linked
elif bone.name in bone_rig.bones:
bone = bone_rig.bones[bone.name]
matrix = get_bone_matrix(bone)
loc, rot, scale = matrix.decompose()
# Create a rotation matrix to srotate 90 degrees around the X-axis (bone pointing upwards)
if orient_world:
# rot = mathutils.Euler((90 * (3.14159 / 180), 0, 0), 'XYZ').to_matrix()
rot = mathutils.Euler((0, 0, 0), 'XYZ').to_matrix()
#decompose and compose a new matrix at the tail location
if bake_loc == 'TAIL':
loc = bone.tail_local if hasattr(bone, 'tail_local') else bone.tail
matrix = Matrix.LocRotScale(loc, rot, scale)
ctrl_bone.length = bone.length * length_factor
ctrl_bone.matrix = matrix
return ctrl_bone
def add_ctrl_rig(bones):
'''Adding the control rig bones'''
org_obj = bones[0].id_data
rigname = 'Controller_' + org_obj.name
#check if the rig object exists, if not add a new one
if rigname not in bpy.data.objects:
armature = bpy.data.armatures.new('Controller_' + org_obj.data.name)
rig = bpy.data.objects.new(rigname, object_data = armature)
else:
rig = bpy.data.objects[rigname]
col = create_scene_collection(rig)
#stamp the rig with the addon module stamp
#rig['bate'] = True
rig.animtoolbox.controlled = org_obj
org_obj.animtoolbox.controller = rig
bpy.context.view_layer.objects.active = rig
rig.show_in_front = bpy.context.preferences.addons[__package__].preferences.in_front
#register the controller and what is being controlled in the scene btc properties
register_ctrl(rig, org_obj)
return rig, col
def move_root_editbone(root_bone):
scene = bpy.context.scene
btc = scene.btc
#if There's no object applied as a reference for the root then use the cursor
if not btc.root_object:
root_bone.matrix = Matrix.Identity(4)
root_bone.matrix.translation = scene.cursor.matrix.translation
root_bone.length = 1
return
#get the matrix from the original bone that is used as a root
if btc.root_object.type == 'ARMATURE' and btc.root_bone in btc.root_object.data.bones:
root_bone.head = btc.root_object.data.bones[btc.root_bone].head
root_bone.tail = btc.root_object.data.bones[btc.root_bone].tail
root_bone.matrix = btc.root_object.data.bones[btc.root_bone].matrix_local
root_bone.length = btc.root_object.data.bones[btc.root_bone].length
elif btc.root_object:
root_bone.length = 1
root_bone.matrix = btc.root_object.matrix_world
def add_root_bone(rig):
scene = bpy.context.scene
btc = scene.btc
if not btc.root:
return None
#return only the root bone, otherwise it will return empty
root_bone = None
root_org_bone = None
found_root = False
#check if the root bone is already in the rig, and if yes then return
for posebone in rig.pose.bones:
if posebone.btc.setup != 'ROOT':
continue
#if nothing is used as a ref for the root then use the existing root
if not btc.root_object:
found_root = True
root_bone = rig.data.edit_bones[posebone.name]
break
if btc.root_bone == '':
#if the current root is in the same matrix of the ref object then use it
if btc.root_object.matrix_world != posebone.matrix:
continue
else:
org_id = posebone.btc.org_id
if org_id != btc.root_object.pose.bones[btc.root_bone].btc.org_id:
continue
found_root = True
root_bone = rig.data.edit_bones[posebone.name]
root_org_bone = posebone
#add a new root bone
if not root_bone:
root_bone = rig.data.edit_bones.new(btc.root_bone)
root_bone.name = 'Temp_Root'
root_bone.select = True
if not found_root:
move_root_editbone(root_bone)
if not btc.root_bone:
root_bone.select = False
add_bone_to_collection(rig, root_bone, col_name = 'Root', visible = True)
return root_bone
def add_control_bones(self, bones, rig, root_ctrl, child = False, orient_world = False, prefix = 'Ctrl_'):
'''Adding all the control bones, ctrl childs and override transform bones during edit mode'''
for bone in bones:
bone.id_data.data.bones[bone.name].select = False
#deselect the original rig in case it's linked
if rig.mode != 'EDIT':
bpy.ops.object.mode_set(mode = 'EDIT')
ctrls = []
child_ctrls = []
mch_bones = []
#root_bone = add_root_bone(rig)
for bone in bones:
ctrl_bone = add_ctrl_bone(rig, bone, root_ctrl, prefix, 'HEAD', orient_world = orient_world)
ctrls.append(ctrl_bone)
# add_bone_to_collection(rig, ctrl_bone, col_name = 'Org', visible = True)
#add bone in case it's using transform from another bone
if self.shape_type == 'ORIGINAL' and bone.custom_shape_transform is not None:
bone_shape_transform = add_ctrl_bone(rig, bone.custom_shape_transform, root_ctrl, '', self.bake_loc)
add_bone_to_collection(rig, bone_shape_transform)
#add child controls
if child:
child_ctrl = add_ctrl_bone(rig, bone, ctrl_bone, 'Child_' + prefix, 'HEAD', 0.5, orient_world = orient_world)
child_ctrls.append(child_ctrl)
add_bone_to_collection(rig, child_ctrl, col_name = 'Child Ctrls', visible = True)
ctrl_bone.select = True
return ctrls, child_ctrls
def add_ctrl_group(rig):
#Add the bone group if doesn't exist
bone_groups = rig.pose.bone_groups
if 'Ctrl Bones' not in bone_groups:
ctrl_group = bone_groups.new(name = 'Ctrl Bones')
ctrl_group.color_set = bpy.context.scene.btc.color_set
# ctrl_group.color_set = 'THEME09'
else:
ctrl_group = bone_groups['Ctrl Bones']
return ctrl_group
def add_bone_setup_id(bone, child = False, setup = 'NONE', org ='CTRL', setup_id = 0, org_id = 0):
#add the properties to the bone
# if setup_id and bone.btc.setup_id and setup_id != bone.btc.setup_id:
# return
bone.btc.setup_id = setup_id
bone.btc.org_id = org_id
bone.btc.setup = setup
bone.btc.org = org
bone.btc.child = child
def add_id(rig, setup, org = False):
'''check if the id already exists, add and return it'''
#btc = context.scene.btc
id = random.randint(10**8, (10**9)-1)
while str(id) in rig.animtoolbox.ctrl_setups:
id = random.randint(10**8, (10**9)-1)
if not org:
id_setup = rig.animtoolbox.ctrl_setups.add()
id_setup.name = str(id)
id_setup.setup = setup
return id
class BtcSelections(bpy.types.Operator):
"""Select all the bones that are constrained to the empty"""
bl_idname = "anim.btc_selections"
bl_label = "Selections"
bl_options = {'REGISTER', 'UNDO'}
# @classmethod
# def poll(cls, context):
# return context.object.type == 'ARMATURE'
def execute(self, context):
obj = context.object
scene = context.scene
objs = [(item.controller, item.controlled) for item in scene.btc.ctrl_items]
if len(objs) < 1:
return {'CANCELLED'}
scene_ctrl_objs, scene_constrained_objs = zip(*objs)
scene_constrained_objs = set(scene_constrained_objs)
ctrl_objs = set()
ctrl_bones = set()
constrained_rigs = set()
for obj in context.selected_objects:
if scene.btc.selection in {'RELATIVE_CTRLS', 'CONTROLLERS'}:
if obj in scene_constrained_objs and obj.type == 'ARMATURE':
#First collect the selected constrained bones and deselect them
for bone in obj.data.bones:
if not bone.select and scene.btc.selection == 'RELATIVE_CTRLS':
continue
for con in obj.pose.bones[bone.name].constraints:
if not '_to_Ctrl' in con.name:
continue
ctrl_objs.update({con.target})
ctrl_bones.update({con.subtarget})
bone.select = False
obj.select_set(False)
#if current selection is a controller then assign it as well
elif obj in scene_ctrl_objs:
ctrl_objs.update({obj})
if scene.btc.selection == 'RELATIVE_CTRLS' and obj.type == 'ARMATURE':
for bone in obj.data.bones:
if bone.select:
ctrl_bones.update({bone.name})
elif scene.btc.selection in {'RELATIVE_CONSTRAINED', 'CONSTRAINED'}:
if obj in scene_ctrl_objs:
#First collect the control empties or bones and deselect them
ctrl_objs.update({obj})
if obj.type == 'ARMATURE':
for bone in obj.data.bones:
if bone.select:
ctrl_bones.update({bone.name})
bone.select = False
obj.select_set(False)
constrained_rigs.update({obj.animtoolbox.controlled})
#if current selection is a controlled rig then assign it as well
elif obj in scene_constrained_objs:
constrained_rigs.update({obj})
if scene.btc.selection in {'RELATIVE_CTRLS', 'CONTROLLERS'}:
#select the relative objects and bones
for ctrl_obj in ctrl_objs:
if ctrl_obj is None:
continue
ctrl_obj.select_set(True)
context.view_layer.objects.active = ctrl_obj
if ctrl_obj.type != 'ARMATURE':
continue
#select the bones if it's a control rig
if context.mode != 'POSE':
bpy.ops.object.mode_set(mode = 'POSE')
for ctrl_bone in ctrl_obj.data.bones:
if scene.btc.selection == 'RELATIVE_CTRLS':
ctrl_bone.select = True if ctrl_bone.name in ctrl_bones else False
elif scene.btc.selection == 'CONTROLLERS':
ctrl_bone.select = True
#Reset active bone in selection order to use context.selected_pose_bone[-1].bone
if context.selected_pose_bones[-1].name in ctrl_obj.pose.bones:
ctrl_obj.data.bones.active = context.selected_pose_bones[-1].bone
elif scene.btc.selection in {'RELATIVE_CONSTRAINED', 'CONSTRAINED'}:
for constrained_rig in constrained_rigs:
#iterate over the constrained bones
constrained_rig.select_set(True)
context.view_layer.objects.active = constrained_rig
if constrained_rig.mode != 'POSE':
bpy.ops.object.mode_set(mode = 'POSE')
for bone in constrained_rig.data.bones:
for con in constrained_rig.pose.bones[bone.name].constraints:
if not '_to_Ctrl' in con.name:
continue
if con.target in ctrl_objs and scene.btc.selection == 'RELATIVE_CONSTRAINED':
#bone.select = True if not con.subtarget or con.subtarget in ctrl_bones else False
if not con.subtarget or con.subtarget in ctrl_bones:
bone.select = True
elif scene.btc.selection == 'CONSTRAINED':
bone.select = True
constrained_rig.data.bones.active = bone
else:
bone.select = False
if bone.select:
constrained_rig.data.bones.active = bone
if hasattr(Tools, 'bone_selection'):
#Reset selection order in order to use context.selected_pose_bone[-1].bone
Tools.bone_selection = list(context.selected_pose_bones)
Tools.obj_selection = list(context.selected_objects)
# Tools.selection_order(self, context)
return {'FINISHED'}
def copy_modifiers(modifier):
#Copy the attributes of the modifier
mod_attr = {}
for key in dir(modifier): #add all the attributes into a dictionary
value = getattr(modifier, key)
mod_attr.update({key: value})
#mod_list.append(attr)
return mod_attr
def paste_modifiers(fcu, mod_attr):
#for mod in mod_list:
if mod_attr['type'] == 'CYCLES' and len(fcu.modifiers): #can add cycle modifier only as the first modifier
return
new_mod = fcu.modifiers.new(mod_attr['type'])
# if new_mod is None:
# return
for attr, value in mod_attr.items():
if type(value) is float or type(value) is int or type(value) is bool:
if not new_mod.is_property_readonly(attr):
setattr(new_mod, attr, value)
def update_range_type(self, context):
#get the default for custom frame range from the current scene frame range
if self.bake_range_type != 'CUSTOM':
return
if self.bake_range[1]:
return
self.bake_range = (context.scene.frame_start, context.scene.frame_end)
def update_bake_range(self, context):
#Make sure the start frame is not bigger or equal to the end frame
if self.bake_range_type != 'CUSTOM':
return
if self.bake_range[0] >= self.bake_range[1]:
self['bake_range'][1] = self.bake_range[0] + 1
def get_framerange(fcu = None):
#get the start and end frames
scene = bpy.context.scene
bake_range_type = scene.btc.bake_range_type
#check that there is something in the action otherwise use the scene frame range
frame_end = fcu.id_data.frame_range[1] if fcu else None
if bake_range_type == 'KEYFRAMES' and fcu and frame_end:
if len(fcu.keyframe_points) < 2:
action = fcu.id_data
range = action.frame_range[1] - action.frame_range[0]
if range > 5:
frame_start = round(action.frame_range[0])
frame_end = round(action.frame_range[1]+1)
else:
#In case the action is very short, because maybe it's just adjustment layer then get the scene frame range
frame_start = scene.frame_start
frame_end = scene.frame_end+1
else:
#if it is not smartbake or if it's only 2 frames
frame_start = round(fcu.range()[0])
frame_end = round(fcu.range()[1]+1)
elif bake_range_type == 'CUSTOM':
frame_start = scene.btc.bake_range[0]
frame_end = scene.btc.bake_range[1]+1
else:# bake_range_type == 'SCENE' or if fcu is none
frame_start = scene.frame_start
frame_end = scene.frame_end+1
return frame_start, frame_end
class smartfcu:
def __init__(self, fcu, posebone, sb = True, ik = False):
self.posebone = posebone
self.org_obj = posebone.id_data
self.fcu = fcu
if fcu:
self.data_path = fcu.data_path
self.index = fcu.array_index
self.group = fcu.group.name if fcu.group else posebone.name
self.extrapolation = fcu.extrapolation
else:
self.extrapolation = 'CONSTANT'
self.modifiers = []
self.interpolations = dict()
self.handle_left_types = dict()
self.handle_right_types = dict()
self.frames = []
self.all_frames = []
if ik:
return
#get the start and end frames
scene = bpy.context.scene
self.bake_range_type = scene.btc.bake_range_type
self.frame_start, self.frame_end = get_framerange(fcu)
self.update_frames(fcu, sb)
def update_frames(self, fcu, sb):
'''update the fcurve frames from all the actions '''
frame_start, frame_end = get_framerange(fcu)
#In case of multiple actions(layers) it will get the min and max of their frame ranges
if hasattr(self, 'frame_start'):
self.frame_start = min(self.frame_start, frame_start)
self.frame_end = max(self.frame_end, frame_end)
else:
self.frame_start = frame_start
self.frame_end = frame_end
#Get all the frames either smartbake or not
if sb and self.bake_range_type == 'KEYFRAMES' and fcu: #and (len(fcu.keyframe_points) > 2 or len(self.frames) > 2)
self.frames += [round(keyframe.co[0], 2) for keyframe in fcu.keyframe_points]
#Filter out the frames that are out of range
self.frames += list(filter(lambda frame: self.frame_start <= frame <= self.frame_end, self.frames))
#bake on every frame if there is no smartbake or not enough keyframes
else:
self.frames += [frame for frame in range( self.frame_start, self.frame_end)]
self.frames = sorted(set(self.frames))
def add_interpolations(self, ctrl_fcu = None):
keyframe_points = [keyframe for keyframe in self.fcu.keyframe_points]
#if there are keyframes from the ctrl fcurve then add them to the interpolations
if ctrl_fcu:
keyframe_points += [keyframe for keyframe in ctrl_fcu.keyframe_points]
if len(keyframe_points):
#updating and not just getting a new dict in case of baking both from origin and from the ctrls
self.interpolations.update({round(keyframe.co[0], 2) : keyframe.interpolation for keyframe in keyframe_points})
self.handle_left_types.update({round(keyframe.co[0], 2) : keyframe.handle_left_type for keyframe in keyframe_points})
self.handle_right_types.update({round(keyframe.co[0], 2) : keyframe.handle_right_type for keyframe in keyframe_points})
else:
self.interpolations = {frame : 'BEZIER' for frame in self.frames}
self.handle_left_types = self.handle_right_types = {frame : 'FREE' for frame in self.frames}
def add_inbetweens(self):
self.inbetweens = []
for i, frame in enumerate(self.frames[:-1]):
if (self.frames[i+1] - frame) <= 1:
continue
self.inbetweens.append(round(frame + (self.frames[i+1] - frame)*1/3, 2))
self.inbetweens.append(round(frame + (self.frames[i+1] - frame)*2/3, 2))
self.inbetweens.sort()
self.all_frames += sorted(self.frames + self.inbetweens)
def add_no_inbetweens(self):
self.all_frames = self.frames
self.inbetweens = []
#give only linear interpolation to all the keyframes
self.interpolations = {frame : 'LINEAR' for frame in self.frames}
def copy(self, smartfcurve, copy_fcurves = True):
'''Copying the smartfcurve attribute from another one'''
self.posebone = smartfcurve.posebone
if hasattr(smartfcurve, 'ctrlbone'):
self.ctrlbone = smartfcurve.ctrlbone
if copy_fcurves:
self.fcu = smartfcurve.fcu
self.data_path = smartfcurve.data_path
self.index = smartfcurve.index
if hasattr(smartfcurve, 'group'):
self.group = smartfcurve.group
self.frames = smartfcurve.frames
self.all_frames = smartfcurve.all_frames
self.modifiers = smartfcurve.modifiers
self.frame_start = smartfcurve.frame_start
self.frame_end = smartfcurve.frame_end
if hasattr(smartfcurve, 'inbetweens'):
self.inbetweens = smartfcurve.inbetweens
if hasattr(smartfcurve, 'interpolations'):
self.interpolations = smartfcurve.interpolations
if hasattr(smartfcurve, 'handle_left_types'):
self.handle_left_types = smartfcurve.handle_left_types
if hasattr(smartfcurve, 'handle_right_types'):
self.handle_right_types = smartfcurve.handle_right_types
def update_fcurves(self, fcu, data_path):
'''update the fcurves, for example after adding a new one'''
self.fcu = fcu
self.group = fcu.group.name
self.data_path = data_path
def get_bone_fcurves(posebone):
'''get all the fcurves of a bone'''
transform_fcus = dict()
actions = set()
#get the keyframes
bone_path = posebone.path_from_id()
obj = posebone.id_data
transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"]
#Get all the actions from all the layers of the bone
get_bone_layers_actions(posebone, actions)
if obj.animation_data.action:
actions.add(obj.animation_data.action)
if not actions:
return transform_fcus
for action in actions:
fcurves = Tools.get_fcurves_channelbag(obj, action)
for transform in transformations:
if transform == 'rotation_quaternion' or transform == 'rotation_axis_angle':
array_len = 4
else:
array_len = 3
#check if it's a vector or just one string
for i in range(array_len):
fcu = fcurves.find(data_path = bone_path + '.' + transform, index = i)
if fcu is None:
continue
if transform not in transform_fcus:
transform_fcus.update({transform : [fcu]})
else:
transform_fcus[transform].append(fcu)
return transform_fcus
def add_bone_fcurves(posebone, transform_fcus):
'''add new fcurves to a bone'''
#get the keyframes
bone_path = posebone.path_from_id()
obj = posebone.id_data
if posebone.rotation_mode == 'QUATERNION':
rotation = 'rotation_quaternion'
elif posebone.rotation_mode == 'AXIS_ANGLE':
rotation = 'rotation_axis_angle'
else:
rotation = 'rotation_euler'
transformations = ['location', rotation]
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
for transform in transformations:
if transform in transform_fcus:
continue
if transform == 'rotation_quaternion' or transform == 'rotation_axis_angle':
array_len = 4
else:
array_len = 3
for i in range(array_len):
fcu = fcurves.new(data_path = bone_path + '.' + transform, index = i)
Tools.add_group_to_fcurve(obj, fcu, posebone.name)
# bone_fcurves.append(fcu)
if transform not in transform_fcus:
transform_fcus.update({transform : [fcu]})
else:
transform_fcus[transform].append(fcu)
return transform_fcus
def get_transforms_array(posebone, track_filter = False):
'''return a dictionary with all the transforms and array of the bone depending on its rotation mode'''
setup = posebone.btc.setup if type(posebone) == bpy.types.PoseBone else None
transforms = {"location" : 3, "scale" : 3} if setup not in {'TRACK_TO', 'TRACK_TO_EMPTY'} or not track_filter else dict()
if len(posebone.rotation_mode) == 3:
transforms.update({'rotation_euler' : 3})
else:
transforms.update({'rotation_' + posebone.rotation_mode.lower() : 4})
return transforms
def smartbake_to_ctrls(context, self, posebones, ctrls, ctrl_rig = None, distance = False):
#Iterating over posebones
#Getting all the fcurves from the posebones from all the layer actions
#Iterating over the posebone fcurves
#Adding missing fcurves
#Iterating over the ctrlbones
#Converting posebone datapath to ctrlbone datapath
#Creating smartfcurve from the fcurve
#Merging smartcurves
#Merging to smartframes
scene = context.scene
frame_current = scene.frame_current
smartfcus = dict()
smartframes = set()
# merged_smartfcus = dict()
#baked_ctrls = []
#Store all the fcurves data and the frame count
for posebone in posebones:
#Boolean to check if an fcurve was found
org_rig = posebone.id_data
# id = posebone.btc.setup_id
path = posebone.path_from_id()
if not ctrl_rig:
ctrl_rig = org_rig.animtoolbox.controller
if ctrl_rig:
if not hasattr(ctrl_rig, 'animation_data'):
continue
if ctrl_rig.animation_data is None:
continue
if ctrl_rig.animation_data.action is None:
continue
transforms = get_transforms_array(posebone)
actions = set()
get_bone_layers_actions(posebone, actions)
if posebone.id_data.animation_data.action:
actions.add(posebone.id_data.animation_data.action)
#getting all the fcurves from all the actions
fcus = (fcu for action in actions for fcu in Tools.get_fcurves_channelbag(org_rig, action))
#find the ctrl bone
for ctrl_bone in ctrls: #ctrl_rig.pose.bones
if type(ctrl_bone) == bpy.types.PoseBone:
if ctrl_bone.btc.org_id == posebone.btc.org_id:
break
else:
#in case it's an empty then check the setup id of the object
if ctrl_bone.animtoolbox.setup_id == posebone.btc.setup_id:
break
ctrl_rig = ctrl_bone.id_data
#######Get smartfcurves from the current fcurves#########
for fcu in fcus:
if path not in fcu.data_path:
continue
#check that it's a really a transform channel
if not any(fcu.data_path.endswith(transform) for transform in transforms.keys()):
continue
#if it's using euler and has too many array coming from quaternion, then skip
#fixes a bug with the last index in case there is both euler and quaternion and we bake from euler to quaternion
if 'rotation' in fcu.data_path:
if len(ctrl_bone.rotation_mode) == 3 and fcu.array_index > 2:
continue
smartfcurve = get_smartfcurve(fcu, smartfcus, posebone, fcu.data_path, index = None, sb = self.smartbake)
add_missing_ctrl_smartfcurves(self, ctrl_bone, posebone, smartfcus)
#####Assign all the properties to the smartfcurves and change from posebone to ctrl bone######
for smartfcurve in smartfcus.values():
if posebone != smartfcurve.posebone:
continue
if type(ctrl_bone) == bpy.types.PoseBone:
#change smartfcurve attributes from original to ctrl bone
ctrl_path = ctrl_bone.path_from_id()
new_data_path = smartfcurve.data_path.replace(path, ctrl_path)
else:
#in case it's an empty, keep only the transform
new_data_path = smartfcurve.data_path.replace(path + '.', '')
new_data_path = replace_rot_mode(new_data_path, ctrl_bone)
smartfcurve.ctrlbone = ctrl_bone
smartfcurve.data_path = new_data_path
smartfcurve.posebone = posebone
smartfcurve.group = ctrl_bone.name
#using distance for relative controls
if distance:
smartfcurve.distance = Matrix.Identity(4)
if self.bake_loc == 'TRACK':
ctrl_bone_matrix = ctrl_bone.matrix if type(ctrl_bone) == bpy.types.PoseBone else ctrl_bone.matrix_world
smartfcurve.distance = (self.org_obj.matrix_world @ posebone.matrix).inverted() @ ctrl_bone_matrix #@ ctrl_bone.id_data.matrix_world
#getting the action or channelbag for adding new fcurves
fcurves_container = Tools.get_fcurves_container(ctrl_rig, ctrl_rig.animation_data.action)
fcu_new = smartbake_add_fcurve(fcurves_container, new_data_path, smartfcurve.index, smartfcurve.group)
smartfcurve.update_fcurves(fcu_new, new_data_path)
if Tools.filter_properties(self, fcu_new):
fcu_new.select = False
fcu_new.hide = True
smartframes = get_smartframes(scene, smartfcus)
smartframes = sorted(smartframes)
smartbake_write_keyframes(scene, smartframes, smartfcus, bake_ctrls = True)
add_interpolations(smartfcus)
scene.frame_set(frame_current)
def add_missing_ctrl_smartfcurves(self, ctrl_bone, posebone, smartfcus):
'''Add Transform smartfcurves that are missing from the Temp Ctrls Used in smartbake_to_ctrls'''
if posebone.btc.setup == 'ROOT':
return
#Get the transform of the controller in case it's a different rotation order
ctrl_transforms = get_transforms_array(ctrl_bone)
path = posebone.path_from_id()
#####Find the MISSING transforms and get them also smartfcurves###
for transform, arrays in ctrl_transforms.items():
for array in range(arrays):
data_path = path + '.' + transform
smartfcurve = smartfcus.get((data_path, array))
if smartfcurve:
continue
if 'rotation' in transform:
if len(ctrl_bone.rotation_mode) == 3 and array > 2:
continue
smartfcurve = smartfcu(None, posebone, self.smartbake)
smartfcurve.data_path, smartfcurve.index = data_path, array
find_smartfcus_to_copy(smartfcurve, smartfcus)
smartfcus.update({(data_path, array) : smartfcurve})
def find_smartfcus_to_copy(smartfcurve, smartfcus):
'''Find an fcurve to copy keyframes from. Either from a different array or transform'''
data_path = smartfcurve.data_path
#Get the keyframes from another array in the same transform
for i in range(4):
if smartfcurve.index == i:
continue
smartfcu_copy = smartfcus.get((data_path, i))
if not smartfcu_copy:
continue
smartfcurve.copy(smartfcu_copy, copy_fcurves = False)
if smartfcurve.frames:
return
#Get keyframes from another transform channel
transforms = get_transforms_array(smartfcurve.posebone)
path = smartfcurve.data_path.split('"].')[0]
org_transform = smartfcurve.data_path.split('"].')[1]
for transform, array_index in transforms.items():
if transform == org_transform:
continue
data_path_copy = path + '"].' + transform
for i in range(array_index):
smartfcu_copy = smartfcus.get((data_path_copy, i))
if not smartfcu_copy:
continue
smartfcurve.copy(smartfcu_copy, copy_fcurves = False)
if smartfcurve.frames:
return
def get_bone_layers_actions(bone, actions):
'''get the actions from all layers'''
if not bpy.context.scene.btc.bake_layers:
return actions
obj = bone.id_data
if not obj.animation_data:
return actions
if not obj.animation_data.use_nla:
return actions
for track in obj.animation_data.nla_tracks:
if track.mute:
continue
for strip in track.strips:
if strip.mute:
continue
if strip.action in actions:
continue
actions.add(strip.action)
return actions
def smartbake_to_ik(context, posebones, chain, sb):
scene = context.scene
frame_current = scene.frame_current
ctrls = chain.ctrls_to_bake
smartfcus = dict()
smartframes = set()
bone_path_ids = [bone.path_from_id() for bone in posebones if bone.btc.setup_id]
actions = set()
#Using this dict to get actions per object/slot
obj_actions = dict()
for bone in chain.org_bones:
# if scene.btc.bake_layers:
get_bone_layers_actions(bone, actions)
obj = bone.id_data
if obj.animation_data.action:
actions.add(obj.animation_data.action)
obj_actions = ({obj : actions})
#Store all the fcurves data and the frame count
#It is going to use the smartframes from the overall frames
frames_start = []
frames_end = []
for obj, action in obj_actions.items():
for action in actions:
if action is None:
continue
fcurves = Tools.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
#check if the fcurve is in the original bones
path = fcu.data_path.split('"].')[0] + ('"]')
if path not in bone_path_ids:
continue
frames = np.zeros(len(fcu.keyframe_points)*2)
fcu.keyframe_points.foreach_get('co', frames)
frames = frames[::2]
smartframes.update(frames)
frame_start, frame_end = get_framerange(fcu)
frames_start.append(frame_start)
frames_end.append(frame_end)
if scene.btc.bake_range_type != 'KEYFRAMES':
smartframes.update([frame_start, frame_end])
smartframes = sorted([round(frame, 2) for frame in smartframes
if min(frames_start) <= frame < max(frames_end)])
if not sb and smartframes:
smartframes = [frame for frame in range(int(min(smartframes)), int(max(smartframes))+1)]
#create the fcurves and add them to smartfcurves
for ctrl in ctrls:
ctrl_action = ctrl.id_data.animation_data.action
path = ctrl.path_from_id()
if ctrl.rotation_mode == 'QUATERNION':
rot = 'rotation_quaternion'
elif ctrl.rotation_mode == 'AXIS_ANGLE':
rot = 'rotation_axis_angle'
else:
rot = 'rotation_euler'
for transform in ['location', rot, 'scale']:
#add the array length, depends on the transform
array_len = 4 if transform in ['rotation_quaternion', 'rotation_axis_angle'] else 3
for array_index in range(array_len):
data_path = path + '.' + transform
fcurves = Tools.get_fcurves_channelbag(ctrl.id_data, ctrl_action)
try:
fcu = fcurves.new(data_path = data_path, index = array_index)
Tools.add_group_to_fcurve(ctrl.id_data, fcu, ctrl.name)
except RuntimeError:
#in case it was already existing because of previous setup, find it and clean it
fcu = fcurves.find(data_path, index = array_index)
if fcu is None:
continue
fcu.keyframe_points.clear()
smartfcurve = smartfcu(fcu, ctrl, sb, ik = True)
smartfcurve.frames = smartframes
smartfcurve.ctrlbone = ctrl
smartfcurve.add_inbetweens()
smartfcurve.add_interpolations()
smartfcus.update({(fcu.data_path, fcu.array_index) : smartfcurve})
# smartframes = sorted([round(frame, 2) for frame in smartframes])
smartbake_write_keyframes(scene, smartframes, smartfcus, chain, bake_ctrls = True)
add_interpolations(smartfcus)
scene.frame_set(frame_current)
def deselect_all_fcurves(fcurves):
#hide and unselect all the fcurve channels
array_true = [True] * len(fcurves)
array_false = [False] * len(fcurves)
fcurves.foreach_set('hide', array_true)
fcurves.foreach_set('select', array_false)
def get_enabled_keyframes(ctrl_rig, action_frames_remove):
'''Get all the frames where Temp Ctrls are turned off'''
action = ctrl_rig.animation_data.action
if action in action_frames_remove:
return action_frames_remove[action]
fcurves = Tools.get_fcurves_channelbag(ctrl_rig, action)
enable_fcu = fcurves.find('animtoolbox.ctrls_enabled')
if not enable_fcu:
return None
action_frame_range = action.frame_range
frames_remove = []
keyframes = np.zeros(len(enable_fcu.keyframe_points)*2)
enable_fcu.keyframe_points.foreach_get('co', keyframes)
frames = keyframes[::2]
values = keyframes[1::2]
if values[0] == 0:
frames_remove += list(range(round(action_frame_range[0]), round(frames[0])))
for i in range(1, len(frames)):
if values[i-1] == 0 and values[i] == 1:
frames_remove += list(range(round(frames[i-1]), round(frames[i])))
if values[-1] == 0:
frames_remove += list(range(round(frames[-1]), round(action_frame_range[1])))
action_frames_remove.update({action : frames_remove})
return frames_remove
def replace_rot_mode(origin_data_path, posebone):
#use bone rotation mode
transform = origin_data_path.split('"].')[-1]
if 'rotation' in transform:
rot_type = 'euler' if posebone.rotation_mode not in {'QUATERNION', 'AXIS_ANGLE'} else posebone.rotation_mode.lower()
origin_data_path = origin_data_path.replace(transform, 'rotation_' + rot_type)
return origin_data_path
def filter_transform(transform, array_index):
#Skipping transforms that are excluded in the filter
atb = bpy.context.scene.animtoolbox
if transform == 'rotation_euler':
# it's a 4 array by default so move 1 array forward for rotation euler
array_index +=1
if 'rotation' in transform:
transform = transform[:8]
filter_transform = getattr(atb, 'filter_' + transform)
return True if filter_transform[array_index] else False
def add_missing_fcurves(action, posebone, smartfcus):
#Get a dictionary with the transformations and arrays
transforms = get_transforms_array(posebone, track_filter = True)
obj = posebone.id_data
fcu_container = Tools.get_fcurves_container(obj, action)
if not fcu_container:
return
for transform, index_len in transforms.items():
data_path = posebone.path_from_id() + '.' + transform
for array_index in range(index_len):
if filter_transform(transform, array_index):
continue
smartfcurve = smartfcus.get((data_path, array_index))
if smartfcurve:
continue
fcu = fcu_container.fcurves.new(data_path, index = array_index)
group = fcu_container.groups.new(posebone.name) if posebone.name not in fcu_container.groups else fcu_container.groups[posebone.name]
fcu.group = group
smartfcurve = get_smartfcurve(fcu, smartfcus, posebone, fcu.data_path, fcu.array_index, True)
# if not smartfcurve.frames:
# #Need to check if this is running twice
# find_smartfcus_to_copy(smartfcurve, smartfcus)
smartfcus.update({(data_path, array_index) : smartfcurve})
def smartbake_add_fcurve(fcu_container , data_path, array_index, groupname):
'''Add a new fcurve to an action if it's still not existing'''
fcu = fcu_container.fcurves.find(data_path, index = array_index)
if not fcu:
fcu = fcu_container.fcurves.new(data_path, index = array_index)
#get the group
group = fcu_container.groups.new(groupname) if groupname not in fcu_container.groups else fcu_container.groups[groupname]
fcu.group = group
return fcu
def get_smartfcurve(fcu, smartfcus, posebone, data_path, index = None, sb = True):
array_index = fcu.array_index if fcu else index
#check if it has any transformation
if data_path.split('.')[-1] not in ['location', 'rotation_euler', 'rotation_quaternion', 'scale']:
return None
#if the smartfcurve exist then update it, else create one
if (data_path, array_index) in smartfcus:
smartfcurve = smartfcus[(data_path, array_index)]
#merge frames with previous frames
smartfcurve.update_frames(fcu, sb)
else:
smartfcurve = smartfcu(fcu, posebone, sb)
smartfcus.update({(data_path, array_index) : smartfcurve})
smartfcurve.data_path = data_path
smartfcurve.index = array_index
#add the modifiers
if fcu:
for mod in fcu.modifiers:
smartfcurve.modifiers.append(mod)
return smartfcurve
def get_smartframes(scene, smartfcus):
'''add inbetweens to the smartfcurves and add to the overall smartframes using this after all the layers have been iterated'''
# #add in betweens and interpolations
smartbake = scene.btc.smartbake
smartframes = set()
def get_frames(smartfcurve):
if scene.btc.bake_range_type == 'KEYFRAMES':
#if there are still no frames applied then take it from another related smartfcurve
if not smartfcurve.frames:
find_smartfcus_to_copy(smartfcurve, smartfcus)
#check after merging all the layers if thereäs still more then two keyframes
if len(smartfcurve.frames) > 2 and smartbake:
smartfcurve.add_interpolations()
smartfcurve.add_inbetweens()
return smartfcurve
else:
#if there is still not enough keyframes then just use the whole frame range
smartfcurve.frames = [frame for frame in range(smartfcurve.frame_start, smartfcurve.frame_end)]
smartfcurve.add_no_inbetweens()
return smartfcurve
for smartfcurve in smartfcus.values():
get_frames(smartfcurve)
#Add to the overall smartframes
smartframes = set(smartfcurve.all_frames).union(smartframes)
return smartframes
def remove_unnecessey_keyframes(smartfcurve):
'''remove unnecessey keyframes that were created on extra index when using keyframe insert, usually on quaternions'''
keyframes = smartfcurve.fcu.keyframe_points
if len(keyframes) == len(smartfcurve.all_frames):
return
i = 0
btc = bpy.context.scene.btc
frame_start, frame_end = btc.bake_range
while i < len(keyframes):
key = keyframes[i]
#if it's custom frame range then skip frames that are not included
# if btc.bake_range_type == 'CUSTOM':
if not frame_start < key.co[0] < frame_end and btc.bake_range_type == 'CUSTOM':
i += 1
continue
if round(key.co[0], 2) not in smartfcurve.all_frames:
keyframes.remove(key)
else:
i += 1
def get_ctrl_matrix(bone, fcu, smartfcurve, chain, bones_matrices):
'''Set the matrix of a bone when writing keyframes to the bake.'''
org_obj_mat = smartfcurve.org_obj.matrix_world
if type(bone) == bpy.types.PoseBone:
# if fcu.data_path in data_paths:
# return True
if bone in bones_matrices:
return True
if bone.btc.setup == 'POLE' and chain:
if not 'location' in fcu.data_path:
return False
# bone.matrix.translation = find_pole_vector(chain.base_bone, chain.ik_tip, bone).translation
matrix = find_pole_vector(chain.base_bone, chain.ik_tip, bone)
bones_matrices.update({bone : matrix.copy()})
return True
if hasattr(smartfcurve, 'distance'):
#If it is using track constraint then get the distance, This is currently being used always with worldspace controls
matrix = org_obj_mat @ smartfcurve.posebone.matrix @ smartfcurve.distance #
else:
matrix = get_bone_matrix(bone)
matrix = Tools.reverse_childof_constraint(bone, matrix)
#Add the bone and matrix only to bones that are constrained
bones_matrices.update({bone : matrix.copy()})
#In case of empties
if type(bone) != bpy.types.PoseBone:
bone.matrix_world = matrix
return True
def euler_filter_context_override():
context = bpy.context
window = context.window
screen = context.screen
#Storing the first area in the screen
old_area = screen.areas[0].type
area = screen.areas[0]
area.type = 'GRAPH_EDITOR'
region = area.regions[1]
with context.temp_override(window=window, area=area, region=region):
bpy.ops.graph.euler_filter()
#restoring the old area
screen.areas[0].type = old_area
def get_constrained_bones(smartfcus):
constrained = set()
# conbones_matrix = dict()
for smartfcurve in smartfcus.values():
bone = smartfcurve.posebone
for con in bone.constraints:
if con.type == 'CHILD_OF':
continue
if not hasattr(con, 'target'):
continue
if not con.target:
continue
if con.target == bone.id_data.animtoolbox.controller:
continue
#in case of empties that are not assigned as a controller
if con.target.animtoolbox.controlled == bone.id_data:
continue
constrained.add(bone)
return constrained
def get_con_influence(constrained):
#This is done separatly so that we don't run view_layer.update() everytime for each bone
if not constrained:
return None
con_influence = dict()
#Turn off influence from tempctrls to check the other constraints offset
for bone in constrained:
for con in bone.constraints:
if not hasattr(con, 'target'):
continue
if con.target is None:
continue
if con.target.animtoolbox.controlled == bone.id_data:
con_influence.update({con : con.influence})
con.influence = 0
return con_influence
def set_ctrl_matrix(bones, ctrls):
'''Assigning matrix, in case the original armature has no action'''
constrained = set()
bones_matrices = dict()
for bone, ctrl in zip(bones, ctrls):
if bone.btc.setup =='ROOT':
continue
matrix_copied = Tools.reverse_childof_constraint(ctrl, bone.matrix.copy(), constrained)
bones_matrices.update({ctrl : matrix_copied})
bones_matrices = Tools.reorder_bones_matrices(bones_matrices, constrained)
Tools.paste_bones_matrices(bones_matrices, constrained)
def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_ctrls = False, posebones = set()):
#Store the matrix of a bone in case it needs an extra iteration because of constraints
#Need to update the object as well in case of using empties
constrained = get_constrained_bones(smartfcus)
def get_parents_matrices(smartfcurve, bones_matrices):
#get matrix also for parent bones
if not hasattr(bone, 'parent_recursive'):
return
parent_posebones = set(posebones).intersection(set(bone.parent_recursive))
if not parent_posebones:
return
for p_bone in parent_posebones:
if p_bone in bones_matrices.keys():
continue
get_ctrl_matrix(p_bone, smartfcurve.fcu, smartfcurve, chain, bones_matrices)
def set_parents_matrices(pasted_bones, bones_matrices):
#get matrix also for parent bones
if not hasattr(bone, 'parent_recursive'):
return
#Get all the parent bones that are also included in this calculation. If they don't have a keyframe,
#setting the matrix only on the child bone can cause wrong the wrong matrix
parent_posebones = (bone for bone in reversed(bone.parent_recursive) if bone in posebones and bone not in pasted_bones)
#Pasting the matrix first to all the parent bones
if parent_posebones:
for p_bone in parent_posebones:
Tools.paste_bone_matrix(p_bone, bones_matrices[p_bone], constrained, x_filter = False)
pasted_bones.add(p_bone)
bone_fcus = dict()
wm = bpy.context.window_manager
wm.progress_begin(0, len(smartframes))
for frame in smartframes:
scene.frame_set(frame = int(frame), subframe = round(frame % 1, 2))
#assign the matrix only once for each bone
bones_matrices = dict()
for smartfcurve in smartfcus.values():
if frame not in smartfcurve.all_frames:
continue
fcu = smartfcurve.fcu
#Write the keys either to a ctrl bone or to the original pose bones
bone = smartfcurve.ctrlbone if bake_ctrls else smartfcurve.posebone
obj = bone.id_data
#add the keyframes to all the fcurves in the end of the iteration
if get_ctrl_matrix(bone, fcu, smartfcurve, chain, bones_matrices):
#get matrix also for parent bones
get_parents_matrices(smartfcurve, bones_matrices)
if bone in bone_fcus:
bone_fcus[bone].add(fcu)
else:
bone_fcus.update({bone : {fcu}})
#re-order first with constrained bones
bones_matrices = Tools.reorder_bones_matrices(bones_matrices, constrained)
#Using this set to avoid pasting the matrix multiple times to the same bone
pasted_bones = set()
#Store the influence and set them to 0 before calculating the matrix without the constraint (to get the offset)
con_influence = get_con_influence(constrained)
#Paste the matrix to the bones, and add keyframes to them
for bone, matrix in bones_matrices.items():
if bone not in bone_fcus:
continue
obj = bone.id_data
set_parents_matrices(pasted_bones, bones_matrices)
#Paste the matrix to the bone itself before entering the keyframe
if bone not in pasted_bones:
if type(bone) != bpy.types.PoseBone:
bone.matrix_world = matrix
else:
Tools.paste_bone_matrix(bone, matrix, constrained, x_filter = False)
# bone.matrix = matrix
pasted_bones.add(bone)
#Add keyframes to all the fcurves of the bone
for fcu in bone_fcus[bone]:
groupname = fcu.group.name if fcu.group else ""
obj.keyframe_insert(fcu.data_path, index = fcu.array_index , frame = frame, group = groupname) #
#Turn the influence of the constraints back on
if con_influence:
for con, influence in con_influence.items():
con.influence = influence
wm.progress_update(smartframes.index(frame))
#####add interpolations#####
for smartfcurve in smartfcus.values():
remove_unnecessey_keyframes(smartfcurve)
#Copy Paste the modifiers from the ctrl rig
if smartfcurve.modifiers:
for mod in smartfcurve.modifiers:
mod_attr = copy_modifiers(mod)
paste_modifiers(smartfcurve.fcu, mod_attr)
#apply the saved extrapolation
smartfcurve.fcu.extrapolation = smartfcurve.extrapolation
# keyframes = smartfcurve.fcu.keyframe_points
if not smartfcurve.interpolations:
continue
for key in smartfcurve.fcu.keyframe_points:
#skip inbetween keyframes
frame = round(key.co[0], 2)
if frame in smartfcurve.inbetweens:
continue
if frame not in smartfcurve.interpolations:
continue
key.interpolation = smartfcurve.interpolations[frame]
if key.interpolation != 'BEZIER':
continue
key.handle_left_type = smartfcurve.handle_left_types[frame]
key.handle_right_type = smartfcurve.handle_right_types[frame]
wm.progress_end()
def add_interpolations(smartfcus):
#turn inbetween keyframes values to handles
for smartfcurve in smartfcus.values():
fcu = smartfcurve.fcu
keys = fcu.keyframe_points
i = 0
while i < len(keys)-3:
frame = round(keys[i].co[0], 2)
if frame not in smartfcurve.interpolations:
i +=1
continue
#skip inbetweens
if frame in smartfcurve.inbetweens:
i +=1
continue
if smartfcurve.interpolations[frame] != 'BEZIER':
i +=1
continue
#keyframes
P0 = keys[i].co[1]
P3 = keys[i+3].co[1]
#inbetweens
P1 = keys[i+1].co[1]
P2 = keys[i+2].co[1]
P1_frame = keys[i+1].co[0]
P2_frame = keys[i+2].co[0]
cp1 = (1/6)*( -5*P0 + 18*P1 - 9*P2 + 2*P3)
cp2 = (1/6)*( 2*P0 - 9*P1 +18*P2 - 5*P3)
keys[i].handle_right_type = 'FREE'
keys[i+3].handle_left_type = 'FREE'
keys[i].handle_right = [P1_frame, cp1]
keys[i+3].handle_left = [P2_frame, cp2]
i += 3
# removing the inbetween keyframes
i = 0
while i < len(keys):
key = keys[i]
if round(key.co[0], 2) in smartfcurve.inbetweens:
keys.remove(key)
else:
i += 1
# fcu.update()
def count_fcurve_ranges(obj, posebones):
#get all the paths of the bones
bone_path_ids = [bone.path_from_id() for bone in posebones if bone.btc.setup_id]
frame_range = set()
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
for fcu in fcurves:
path = fcu.data_path.split('"].')[0] + ('"]')
if path not in bone_path_ids:
continue
frame_range.add(round(fcu.range()[0], 2))
frame_range.add(round(fcu.range()[1], 2))
return sorted(frame_range)
# class ConvertRotatinMode(bpy.types.Operator):
# """Bake all the bones with constraints to the empties"""
# bl_idname = "anim.convert_rotation_mode"
# bl_label = "Convert Rotation Mode"
# bl_options = {'REGISTER', 'UNDO'}
# # @classmethod
# # def poll(cls, context):
# # return context.object.type == 'ARMATURE'
# def execute(self, context):
# #obj = context.object
# scene = context.scene
# selected_bones = context.selected_pose_bones
# return {'FINISHED'}
def check_for_actions(obj):
'''checking if any action exist in the object, either active action or in the nla'''
if not obj:
return False
if obj.animation_data.action:
return True
#if there is no action assigned and no use of nla then quit
if not obj.animation_data.use_nla:
return False
for track in obj.animation_data.nla_tracks:
for strip in track.strips:
if strip.action:
return True
return False
class BakeTempCtrls(bpy.types.Operator):
"""Bake all the bones with constraints to the Temp Ctrls"""
bl_idname = "anim.bake_temp_ctrls"
bl_label = "Bake Temp Ctrls"
bl_options = {'REGISTER', 'UNDO'}
# @classmethod
# def poll(cls, context):
# return context.object.type == 'ARMATURE'
def execute(self, context):
# sec = time.time()
self.report({'INFO'}, 'Baked Temp Ctrls')
scene = context.scene
ids = get_bone_ids(context)
controllers, controlled_objs = get_controllers_controlled(context)
#in case the controllers are empties
for ctrl in controllers:
if not ctrl.animtoolbox.setup_id:
continue
ids.add(ctrl.animtoolbox.setup_id)
# controllers_remove_child(ctrl, controllers) <-- ?
if not ids:
return {'CANCELLED'}
temp_col = find_temp_ctrl_collection(scene, controllers)
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
included_collections = exclude_collections(temp_col, context, controlled_objs)
#Select only the bones with the constraints
for obj in controlled_objs:
if obj.type != 'ARMATURE':
continue
#Relevant only for temp ctrls and not empties
if obj.animtoolbox.controller:
if not check_for_actions(obj.animtoolbox.controller):
continue
obj.select_set(True)
context.view_layer.objects.active = obj
obj_vis, obj_hide = unhide_org(obj)
if obj.mode != 'POSE':
bpy.ops.object.mode_set(mode = 'POSE')
baked_bones = []
#get all the bones that should be baked via id
for posebone in obj.pose.bones:
#Disabling root from bake
if posebone.btc.setup == 'ROOT':
continue
#check if selected if it has any of selected ids
if scene.btc.target == 'SELECTED' and posebone.btc.setup_id in ids:
baked_bones.append(posebone)
#if not only selected check if it has any setup_id
elif scene.btc.target != 'SELECTED' and posebone.btc.setup_id:
baked_bones.append(posebone)
#ctrl_rig = obj.animtoolbox.controller
if not obj.animation_data:
obj.animation_data_create()
if not obj.animation_data.action:
action = bpy.data.actions.new(obj.name)
obj.animation_data.action = action
obj.animation_data.action_slot = Tools.add_action_slot(obj, action)
else:
action = obj.animation_data.action
fcurves = Tools.get_fcurves_channelbag(obj, action)
deselect_all_fcurves(fcurves)
smartbake(context, action, baked_bones, ids)
if obj_vis: obj.hide_set(True)
if obj_hide: obj.hide_viewport = True
include_collections(included_collections, hidden_objs, temp_col, controlled_objs)
if not scene.btc.clean_constraints:
return {'FINISHED'}
remove_constraints(context, ids, controllers, controlled_objs)
if not scene.btc.clean_ctrls:
return {'FINISHED'}
remove_tempctrls(context, self, controllers, controlled_objs, ids)
# print('\nseconds\n', time.time() - sec)
return {'FINISHED'}
def smartbake(context, org_action, posebones, ids):
'''Smart Bake back to the original controls'''
scene = context.scene
frame_current = scene.frame_current
smartfcus = dict()
smartframes = set()
action_frames_remove = dict()
#Use this to check if an fcurve exist also in the original action (and not just on a layer)
org_action_fcus = set()
transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"]
#Store all the fcurves data and the frame count
for posebone in posebones:
#Boolean to check if an fcurve was found
setup_id = posebone.btc.setup_id
#if the bone is not part of the setup ids then skip
if setup_id not in ids:
continue
# find_fcu = False
path = posebone.path_from_id()
if 'EMPTY' in posebone.btc.setup:
ctrl_rig = find_empty_ctrl(context, posebone)
else:
ctrl_rig = posebone.id_data.animtoolbox.controller
actions = {org_action}
get_bone_layers_actions(posebone, actions)
#Get smartfcurves from the Original Bones
for action in actions:
fcurves = Tools.get_fcurves_channelbag(posebone.id_data, action)
for fcu in fcurves:
if path not in fcu.data_path:
continue
#apply the bake only to the transformation channels
if fcu.data_path.split('"].')[-1] not in transformations:
continue
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
if posebone.btc.setup in {'TRACK_TO', 'TRACK_TO_EMPTY'} and 'rotation' not in fcu.data_path:
continue
if scene.btc.from_origin:
smartfcurve = get_smartfcurve(fcu, smartfcus, posebone, fcu.data_path, fcu.array_index, scene.btc.smartbake)
if smartfcurve is None:
continue
#if it's not the original action then create a new fcurve in org_action where it's baked to and assign it to smartfcurve
#Usefull especially when working inside a new empty layer
if action != org_action and (fcu.data_path, fcu.array_index) not in org_action_fcus:
fcurves_container = Tools.get_fcurves_container(posebone.id_data, org_action)
if fcurves_container is None:
fcurves_container = Tools.add_channelbag(posebone.id_data, org_action)
# check if the fcurve exists in the original action, and if not then create one
org_fcu = smartbake_add_fcurve(fcurves_container, fcu.data_path, fcu.array_index, fcu.group.name)
smartfcurve.fcu = org_fcu
#Add frames from other smartfcurves, need to check if I need to move this after the first iteration
org_action_fcus.add((org_fcu.data_path, org_fcu.array_index))
#clean the fcurve in case there are issues with the fcurve like double keyframes
# if action == org_action:
# fcurves_remove_keys(scene.btc, fcu)
fcu.select = True
fcu.hide = False
if ctrl_rig is None:
continue
frames_remove = get_enabled_keyframes(ctrl_rig, action_frames_remove)
#Getting the Fcurve container, either action or channelbag
fcurves_container = Tools.get_fcurves_container(posebone.id_data, org_action)
if not fcurves_container:
fcurves_container = Tools.add_channelbag(posebone.id_data, org_action)
if scene.btc.from_ctrl:
if not ctrl_rig.animation_data.action:
new_action = bpy.data.actions.new(name = 'Temp_Ctrls_' + action.name)
ctrl_rig.animation_data.action = new_action
Tools.add_slot_to_animdata(ctrl_rig.animation_data)
ctrl_actions = {ctrl_rig.animation_data.action}
if ctrl_rig.type == 'ARMATURE':
for ctrl_bone in ctrl_rig.pose.bones:
if ctrl_bone.btc.setup_id != setup_id:
continue
ctrl_path = ctrl_bone.path_from_id()
get_bone_layers_actions(ctrl_bone, ctrl_actions)
else:
#for empties get the layers from the empty ctrl
get_bone_layers_actions(ctrl_rig, ctrl_actions)
ctrl_path = ''
for ctrl_action in ctrl_actions:
#iterate over ctrl rig fcurves
fcurves = Tools.get_fcurves_channelbag(ctrl_rig, ctrl_action)
for ctrl_fcu in fcurves:
#find the fcurve
if ctrl_path not in ctrl_fcu.data_path:
continue
if 'TRACK_TO' in posebone.btc.setup and not 'rotation' in ctrl_fcu.data_path:
continue
#bake only transformation channels
if ctrl_fcu.data_path.split('"].')[-1] not in transformations:
continue
#change the path to the original bone path
#Get the original path either using bones or empties
origin_data_path = ctrl_fcu.data_path.replace(ctrl_path, path) if ctrl_path else path + '.' + ctrl_fcu.data_path
origin_data_path = replace_rot_mode(origin_data_path, posebone)
#if it's using euler and has too many array coming from quaternion, then skip
if ctrl_fcu.array_index > 2 and origin_data_path.split('"].')[-1] == 'rotation_euler':
continue
#fcurve was not found, get the keyframes from the ctrl bone
#update the smartfcurve frames from the controlers
smartfcurve = smartfcus.get((origin_data_path, ctrl_fcu.array_index))
if smartfcurve:
smartfcurve.update_frames(ctrl_fcu, scene.btc.smartbake)
else:
#Add the fcurve if it's in the ctrl but not in the original aaction
fcu = smartbake_add_fcurve(fcurves_container, origin_data_path, ctrl_fcu.array_index, posebone.name)
smartfcurve = get_smartfcurve(ctrl_fcu, smartfcus, posebone, origin_data_path, ctrl_fcu.array_index, scene.btc.smartbake)
if smartfcurve : smartfcurve.fcu = fcu
#getting the group name from posebone instead of the ctrl group name
smartfcurve.group = posebone.name
if (origin_data_path, ctrl_fcu.array_index) not in smartfcus:
continue
#get only the modifiers from the controlers to the existing fcurves
smartfcurve = smartfcus[(origin_data_path, ctrl_fcu.array_index)]
for mod in ctrl_fcu.modifiers:
smartfcurve.modifiers.append(mod)
#Remove frames to not include in the bake, in case of custom frame range
if frames_remove and smartfcurve:
smartframes = {frame for frame in smartframes if round(frame) not in frames_remove}
smartfcurve.frames = [frame for frame in smartfcurve.frames if round(frame) not in frames_remove]
smartfcurve.interpolations = {frame : interpolation for frame, interpolation in smartfcurve.interpolations.items() if round(frame) not in frames_remove}
#need to add the fcurves
add_missing_fcurves(org_action, posebone, smartfcus)
smartframes = get_smartframes(scene, smartfcus)
smartframes = sorted(smartframes)
#write all the keyframes
smartbake_write_keyframes(scene, smartframes, smartfcus, posebones = posebones)
add_interpolations(smartfcus)
scene.frame_set(frame_current)
def remove_tempctrls(context, self, controllers, controlled_objs, ids):
'''reusing these sequence of rebake and remove ctrls both for cleanup and during bake'''
scene = context.scene
if scene.btc.target == 'SELECTED':
self.smartbake = context.scene.btc.smartbake
for rig in controllers:
if rig.type != 'ARMATURE':
continue
#Hide non relevant collections for a faster bake
temp_col = find_temp_ctrl_collection(scene, controllers)
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
included_collections = exclude_collections(temp_col, context, controlled_objs)
rebake_bones, root_bone = get_rebake_bones(rig, ids)
if context.scene.btc.rebake_to_org:
rebake_connection_to_org(self, context, ids, rebake_bones, root_bone)
else:
rebake_connection_ctrls(self, context, rig, rebake_bones, root_bone, ids = ids)
include_collections(included_collections, hidden_objs, temp_col, controlled_objs)
# rebake_connection_to_org(self, context, controllers, ids)
remove_selected(context, ids, controllers)
else:
remove_ctrls(context)
remove_collection()
switch_to_original_obj(context, controlled_objs)
def remove_collection():
for col in bpy.data.collections:
if 'Ctrl_Helpers' in col.name and not len(col.objects):
bpy.data.collections.remove(col)
def remove_empty(btc, empty, ctrl_rig_bones):
'''Remove worldspace empty, and it's action or slot and registration from ctrl items'''
action = None
if empty.animation_data:
action = empty.animation_data.action
empty_index = next((i for i, item in enumerate(btc.ctrl_items) if empty == item.controller), None)
if empty_index != None:
btc.ctrl_items.remove(empty_index)
if empty.children:
child_index = next((i for i, item in enumerate(btc.ctrl_items) if empty.children[0] == item.controller), None)
btc.ctrl_items.remove(child_index)
bpy.data.objects.remove(empty.children[0])
ctrl_rig_bones.pop(empty)
bpy.data.objects.remove(empty)
if action:
if hasattr(action, 'slots'):
Tools.remove_empty_slots(action)
if len(action.slots):
return
bpy.data.actions.remove(action)
def get_relative_selected_objects(context):
#get the selected object from the bones or empties
selected_objects = set()
if context.selected_pose_bones:
selected_objects = {bone.id_data for bone in context.selected_pose_bones}
if context.selected_objects:
#get selected objects with empties
selected_objects = selected_objects.union({obj for obj in context.selected_objects if obj.animtoolbox.setup_id})
for obj in selected_objects.copy():
if obj.animtoolbox.controller:
selected_objects.add(obj.animtoolbox.controller)
elif obj.animtoolbox.controlled:
selected_objects.add(obj.animtoolbox.controlled)
for item in context.scene.btc.ctrl_items:
if item.controlled in selected_objects:
selected_objects.add(item.controller)
if item.controller in selected_objects:
selected_objects.add(item.controlled)
return selected_objects
def remove_ctrls(context, selected = None):
scene = context.scene
i = 0
selected_objects = get_relative_selected_objects(context)
#for ctrl in bpy.data.objects:
while i < len(scene.btc.ctrl_items):
ctrl_item = scene.btc.ctrl_items[i]
if selected:
if ctrl_item.controller != selected:
i += 1
continue
if ctrl_item.controller is None:
i += 1
continue
if scene.btc.target == 'RELATIVE':
if ctrl_item.controller not in selected_objects and ctrl_item.controlled not in selected_objects:
i += 1
continue
ctrl_rig = ctrl_item.controller
if ctrl_rig is not None:
org_rig = ctrl_item.controlled
all_ids = ctrl_rig.animtoolbox.ctrl_setups.keys()
#remove the controller from the scene btc list
scene.btc.ctrl_items.remove(i)
if ctrl_rig is None:
continue
#remove the related action
if ctrl_rig.animation_data is not None:
if ctrl_rig.animation_data.action is not None:
bpy.data.actions.remove(ctrl_rig.animation_data.action)
if ctrl_rig.type == 'ARMATURE':
bpy.data.armatures.remove(ctrl_rig.data)
else:
bpy.data.objects.remove(ctrl_rig)
#Reset all the org_ids from the original objects
if org_rig is None:
continue
if org_rig.type != 'ARMATURE':
continue
for bone in org_rig.pose.bones:
if bone is None:
continue
if str(bone.btc.org_id) in all_ids:
bone.btc.org_id = 0
if str(bone.btc.setup_id) in all_ids:
bone.btc.setup_id = 0
bone.btc.setup = 'NONE'
bone.btc.org = 'NONE'
def remove_constraints(context, ids, controllers, controlled_objs):
#add children to controllers
ctrls = list(controllers) + [ctrl.children[0] for ctrl in controllers if ctrl.children]
scene = context.scene
for obj in controlled_objs:
if obj.type != 'ARMATURE':
continue
for bone in obj.pose.bones:
if scene.btc.target == 'SELECTED':
if bone.btc.setup_id not in ids:
continue
else:
#check if the bone has anything assigned to it
if not bone.btc.setup_id:
continue
#reset the setup_id
bone.btc.setup_id = 0
bone.btc.org_id = 0
bone.btc.org = bone.btc.setup ='NONE'
for con in bone.constraints:
if not hasattr(con, 'target'):
continue
if con.target not in ctrls:
continue
con_path = con.path_from_id()
#Removing target because of a bug in Blender 4.4, otherwise it crashes when switching mode
con.target = None
bone.constraints.remove(con)
#select the original bone
obj.data.bones[bone.name].select = True
#remove any fcurve channels of the constraints
if obj.animation_data is None:
continue
if obj.animation_data.action is None:
continue
#remove fcurves related to the constraint
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
con_fcu = fcurves.find(con_path)
if con_fcu is not None:
fcurves.remove(con_fcu)
# remove drivers connected to the constraint
for drv in obj.animation_data.drivers:
if con_path in drv.data_path:
obj.animation_data.drivers.remove(drv)
def add_empty_controller(obj, controllers):
'''add the related empty controllers such as a parent or a child controller'''
if obj.type != 'EMPTY':
return controllers.add(obj)
if obj.animtoolbox.child:
if obj.animtoolbox.setup_id == obj.parent.animtoolbox.setup_id:
return controllers.add(obj.parent)
elif obj.children:
for child in obj.children:
if not child.animtoolbox.child:
continue
if child.animtoolbox.setup_id == obj.animtoolbox.setup_id:
return controllers.add(child)
return controllers.add(obj)
def get_controllers_controlled(context):
#get all the selected control and controlled rigs
#if it's during invoke and using setup ids then use it always as selected
select_filter = 'SELECTED' if 'setup_ids' in globals() else context.scene.btc.target
controllers = set()
controlled_objs = set()
scene = context.scene
if select_filter in ['RELATIVE', 'SELECTED']:
clean_empty_ctrl_items(context.scene)
selected_objects = get_relative_selected_objects(context) if select_filter =='RELATIVE' else context.selected_objects
# selected_objects = context.selected_objects if len(context.selected_objects) else [context.object]
for obj in selected_objects:
if obj is None:
continue
if obj.animtoolbox.controller:
#skip the root empty controller
if obj.animtoolbox.controller.animtoolbox.root:
continue
#if it has a controller then it's controlled object
controlled_objs.add(obj)
controllers.add(obj.animtoolbox.controller)
add_empty_controller(obj.animtoolbox.controller, controllers)
elif obj.animtoolbox.controlled:
#skip the root empty controller
if obj.animtoolbox.root:
continue
controlled_objs.add(obj.animtoolbox.controlled)
controllers.add(obj)
#check if it's a child empty selected, then add also it's parent
add_empty_controller(obj, controllers)
if not context.selected_pose_bones:
return controllers, controlled_objs
#in case it is using EMPTIES and the bones are selected. then check for related controls using setup ids
for bone in context.selected_pose_bones:
if bone.btc.setup not in {'EMPTY', 'TRACK_TO_EMPTY'} or not bone.btc.setup_id:
continue
id = bone.btc.setup_id
for ctrl_item in scene.btc.ctrl_items:
if not ctrl_item.controller:
continue
if ctrl_item.controlled not in selected_objects:
continue
if id == ctrl_item.controller.animtoolbox.setup_id:
add_empty_controller(ctrl_item.controller, controllers)
controlled_objs.add(ctrl_item.controlled)
else:
#collect all the controlled objects
controlled_objs = {item.controlled for item in scene.btc.ctrl_items if item.controlled}
controllers = {item.controller for item in scene.btc.ctrl_items if item.controller}
#In case controlled objs are not found and there are still bones with ids
#The add the object of this bones to reset them
if not controlled_objs and context.selected_pose_bones:
for bone in context.selected_pose_bones:
if bone.btc.setup_id or bone.btc.org_id:
controlled_objs.add(bone.id_data)
return controllers, controlled_objs
def unhide_org(obj):
#Make sure the original armature is not hidden
obj_vis = obj.hide_get()
obj_hide = obj.hide_viewport
if obj_vis or obj_hide:
obj.hide_set(False)
obj.hide_viewport = False
return obj_vis, obj_hide
def switch_to_original_obj(context, objs):
'''Switching back to the original object posemode after cleanup or bake'''
for obj in objs:
if obj is None:
continue
if obj not in context.view_layer.objects.values():
continue
unhide_org(obj)
obj.select_set(True)
context.view_layer.objects.active = obj
if obj.animtoolbox.controller:
controller_obj = obj.animtoolbox.controller
obj_vis, obj_hide = unhide_org(controller_obj)
if obj_vis or obj_hide:
if context.scene.animtoolbox.isolate_pose_mode:
context.scene.animtoolbox.isolate_pose_mode = False
bpy.ops.object.mode_set(mode = 'POSE')
def clean_empty_ctrl_items(scene):
'''If there are ctrl items that are missing controller or controlled then remove them'''
i=0
while i < len(scene.btc.ctrl_items):
ctrl_item = scene.btc.ctrl_items[i]
if not ctrl_item.controller or not ctrl_item.controlled:
scene.btc.ctrl_items.remove(i)
else:
i += 1
def remove_selected(context, ids, controllers):
'''Remove selected temp ctrls'''
btc = context.scene.btc
#dictionary of all the ctrl rigs with a list of the selected bones for converting to edit bones
ctrl_rig_bones = {ctrl_item.controller : set() for ctrl_item in btc.ctrl_items}
rig = None
selected = context.selected_pose_bones if context.selected_pose_bones else []
#Get all the empty controls to remove the root control in case it is the only control left
clean_empty_ctrl_items(context.scene)
empty_ctrls = [ctrl.controller for ctrl in context.scene.btc.ctrl_items if ctrl.controller.type == 'EMPTY' and not ctrl.controller.animtoolbox.child]
for rig in controllers:
#check if the rig is actually an empty with setup id
if rig is None:
continue
if rig.animtoolbox.setup_id in ids and rig.type =='EMPTY': #hasattr(rig, 'animtoolbox') and
remove_empty(btc, rig, ctrl_rig_bones)
if rig in empty_ctrls:
empty_ctrls.remove(rig)
#check if only the root is left
if len(empty_ctrls) == 1:
if empty_ctrls[0] is None:
continue
if empty_ctrls[0].animtoolbox.root:
remove_empty_item(context, empty_ctrls[0])
remove_collection()
continue
#get the rest of the selected ctrl bones and ids
for posebone in rig.pose.bones:
if posebone.btc.setup_id not in ids and posebone not in selected:
continue
ids.add(posebone.btc.setup_id)
ids.add(posebone.btc.org_id)
ctrl_rig_bones[rig].add(posebone.name)
#check if there is still controllers (in case they were already removed)
if not any(len(value) > 0 for value in ctrl_rig_bones.values()):
return {'CANCELLED'}
if rig is None:
return {'CANCELLED'}
#assign the last control rig as active object for going to edit mode
if rig in context.view_layer.objects.values():
context.view_layer.objects.active = rig
#remove channels of the control bones from the action
for rig, bonenames in ctrl_rig_bones.items():
# if rig.type != 'ARMATURE':
# continue
if rig not in context.view_layer.objects.values():
continue
# rig.select_set(True)
if not rig.animation_data:
continue
if not rig.animation_data.action:
continue
fcurves = Tools.get_fcurves_channelbag(rig, rig.animation_data.action)
for fcu in fcurves:
if len(fcu.data_path.split('"')) < 2:
continue
if fcu.data_path.split('"')[1] in bonenames:
fcurves.remove(fcu)
bpy.ops.object.mode_set(mode = 'EDIT')
#remove the bones in edit mode
for rig in controllers:
for bone_name in ctrl_rig_bones[rig]:
if bone_name not in rig.data.edit_bones:
continue
edit_bone = rig.data.edit_bones[bone_name]
rig.data.edit_bones.remove(edit_bone)
# if the rig is empty or has only root left then remove it completly
if not len(rig.data.edit_bones) or (len(rig.data.edit_bones) == 1 and rig.pose.bones[0].btc.setup == 'ROOT'):
remove_ctrls(context, rig)
remove_collection()
continue
#remove the ctrl setups from the object
for id in ids:
remove_id(rig, id)
#Switch controller back to posemode
if context.active_object in controllers:
bpy.ops.object.mode_set(mode = 'POSE')
def find_existing_ids(bones):
ids = {bone.btc.setup_id for bone in bones}
return ids
def clean_bone_before_setup(context, bones):
'''Cleaning old setups before creating new setup on a bone'''
ids = {bone.btc.setup_id for bone in bones}
if not any(ids):
return False
if context.preferences.addons[__package__].preferences.clear_setup:
controllers, controlled_objs = get_controllers_controlled(context)
remove_constraints(context, ids, controllers, controlled_objs)
remove_selected(context, ids, controllers)
return False
else:
#Return True to just cancel the operator instead of clearing the old setup
return True
def remove_id(rig, id):
'''remove the ids from the ctrl setups'''
#find the index
index = rig.animtoolbox.ctrl_setups.find(str(id))
if index is None:
return
rig.animtoolbox.ctrl_setups.remove(index)
def get_bone_ids(context, id = 'setup_id'):
select_filter = 'SELECTED' if 'setup_ids' in globals() else context.scene.btc.target
scene = context.scene
ids = set()
if select_filter == 'ALL':
#get ids from All the controller rigs and bones
for obj in scene.objects:
if not obj.animtoolbox.controlled and obj not in context.selected_objects:
#still checking selected objects in case animtoolbox.controlled is empty
continue
if obj.type == 'ARMATURE':
for bone in obj.pose.bones:
if getattr(bone.btc, id):
ids.add(getattr(bone.btc, id))
elif obj.type == 'EMPTY' and id == 'setup_id':
if getattr(obj.animtoolbox, id):
ids.add(getattr(obj.animtoolbox, id))
return ids
if not context.selected_objects:
return ids
selected_objects = get_relative_selected_objects(context)
if select_filter == 'RELATIVE':
clean_empty_ctrl_items(context.scene)
#get all controlled or controller objects
clean_empty_ctrl_items(context.scene)
objs = [obj for item in scene.btc.ctrl_items for obj in (item.controlled, item.controller)]
#get all bones from the object that are selected and are ctrl or controlled
#ids = {getattr(bone.btc, id) for obj in objs if obj in selected_objects for bone in obj.pose.bones if getattr(bone.btc, id)}
ids = set()
for obj in objs:
if obj not in selected_objects:
continue
if obj is None:
continue
if obj.type =='ARMATURE':
for bone in obj.pose.bones:
if getattr(bone.btc, id):
ids.add(getattr(bone.btc, id))
elif getattr(obj.animtoolbox, id):
ids.add(getattr(obj.animtoolbox, id))
return ids
if select_filter == 'SELECTED':
if context.selected_pose_bones: #context.object.type == 'ARMATURE' and
ids = {getattr(bone.btc, id) for bone in context.selected_pose_bones if getattr(bone.btc, id)}
else:
ids = {getattr(obj.animtoolbox, 'setup_id') for obj in selected_objects if obj and getattr(obj.animtoolbox, 'setup_id')}
return ids
def rebake_connection_ctrls(self, context, obj, rebake_bones, parent, root_name = None, base_parent_name = None, ids = None):
#reconnecting to a new parent while constrained in world space
if not rebake_bones:
return
bones_ctrl = dict()
restore_selection = set()
#deselect objects and keep only controllers, to be able to go into edit mode when the org object is linked
for obj in context.selected_objects:
if obj.animtoolbox.controller:
#select the object back after finished rebaking
if obj.select_get: restore_selection.add(obj)
obj.select_set(False)
elif obj.animtoolbox.controlled:
context.view_layer.objects.active = obj
obj.select_set(True)
#if there is no active controller selected, then get from the controlled.
if not context.view_layer.objects.active and restore_selection:
controlled = next(iter(restore_selection))
ctrl_obj = controlled.animtoolbox.controller
if not ctrl_obj:
return
context.view_layer.objects.active = ctrl_obj
ctrl_obj.select_set(True)
bpy.ops.object.mode_set(mode = 'EDIT')
#Bones in this case are the original Temp ctrls, adding a duplicate as a new ctrl
for bone in rebake_bones:
obj = bone.id_data
edit_bone = obj.data.edit_bones[bone.name]
# TEMPIK connection has an exception because of the linking mch bone
if bone.btc.org == 'MCH' and bone.btc.setup == 'TEMPIK' and parent:
edit_bone.parent = obj.data.edit_bones[parent.name]
continue
if obj.pose.bones[bone.name].btc.child:
continue
ctrl = add_ctrl_bone(obj, edit_bone, None)
bones_ctrl.update({bone : ctrl.name})
bpy.ops.object.mode_set(mode = 'POSE')
if not bones_ctrl:
for obj in restore_selection:
obj.select_set(True)
return
#constraining the duplicate ctrl to the original ctrl to copy animation in worldspace
for bone, ctrlname in bones_ctrl.items():
ctrl = obj.pose.bones[ctrlname]
constraint_add(ctrl, bone)
#Adding all the ids for the smartbake to work
ctrl.btc.org_id = bone.btc.org_id
ctrl.btc.setup_id = bone.btc.setup_id
ctrl.rotation_mode = bone.rotation_mode
bones_ctrl.update({bone : ctrl})
bones, ctrls = zip(*bones_ctrl.items())
#Bake the new temporary ctrls of the connection ctrl
smartbake_to_ctrls(context, self, bones, ctrls, obj)
constraints_clear(ctrls)
#setup the new parent for the original connection bone
bpy.ops.object.mode_set(mode = 'EDIT')
if parent:
parent_editbone = obj.data.edit_bones[parent.name]
if root_name:
#assign root to the base bone
if root_name in obj.data.edit_bones:
root_bone = obj.data.edit_bones[root_name]
#remove parent from base bone
if base_parent_name:
if base_parent_name in obj.data.edit_bones:
edit_bone = obj.data.edit_bones[base_parent_name]
edit_bone.parent = root_bone
for bone in bones:
edit_bone = obj.data.edit_bones[bone.name]
if parent:
#if the root parent is being removed
if ids and parent.btc.setup_id in ids:
edit_bone.parent = None
continue
if edit_bone == parent_editbone:
continue
edit_bone.parent = parent_editbone
else:
edit_bone.parent = None
bpy.ops.object.mode_set(mode = 'POSE')
#constrain back to the temporary temporary ctrls
for bone, ctrl in bones_ctrl.items():
constraint_add(bone, ctrl)
#Adding all the ids for the smartbake to work
smartbake_to_ctrls(context, self, ctrls, bones, obj)
#remove only the constraints with the duplicated ctrls included inside
for bone, ctrl in zip(bones, ctrls):
for con in bone.constraints:
if ctrl.id_data == con.target and ctrl.name == con.subtarget:
bone.constraints.remove(con)
# constraints_clear(ctrls)
remove_tempctrls_channels(obj, ctrls)
#removing the temporary ctrls
bpy.ops.object.mode_set(mode = 'EDIT')
for ctrl in ctrls:
edit_ctrl = obj.data.edit_bones[ctrl.name]
obj.data.edit_bones.remove(edit_ctrl)
if obj.animtoolbox.controlled:
obj.animtoolbox.controlled.select_set(True)
bpy.ops.object.mode_set(mode = 'POSE')
#restoring unselected objects
for obj in restore_selection:
obj.select_set(True)
return
def remove_tempctrls_channels(obj, ctrls):
#remove extra channels from the temporary ctrls before removing them
if not obj.animation_data:
return
if not obj.animation_data.action:
return
action = obj.animation_data.action
ctrl_paths = [ctrl.path_from_id() for ctrl in ctrls]
fcurves = Tools.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
path = fcu.data_path.split('"].')[0] + ('"]')
if path in ctrl_paths:
fcurves.remove(fcu)
def get_rebake_bones(rig, ids):
#get all the bones with connection (Ctrls and their children)
rebake_bones = []
rebake_org_ids = set()
root_bone = None
# for rig in controllers:
for ctrlbone in rig.pose.bones:
if ctrlbone.btc.setup == 'ROOT':
root_bone = ctrlbone
if ctrlbone.btc.setup_id in ids:
continue
if not ctrlbone.btc.setup_id:
continue
if not ctrlbone.parent:
continue
if ctrlbone.parent.btc.setup_id in ids:
rebake_bones.append(ctrlbone)
rebake_org_ids.add(ctrlbone.btc.org_id)
# get child bones with the same org_id
for child in ctrlbone.children:
if child.btc.org_id == ctrlbone.btc.org_id:
rebake_bones.append(child)
return rebake_bones, root_bone
def rebake_connection_to_org(self, context, ids, rebake_bones, root_bone):
#checking for bones that need to be baked in case they are connected to a removed chain
#get the bone that is directly connected
# get_rebake_bones(controllers, ids)
if not rebake_bones:
return
self.smartbake = context.scene.btc.smartbake
#Relink rebaked bones to the world or root
for ctrlbone in rebake_bones:
if not ctrlbone.parent:
continue
if ctrlbone.parent.btc.setup_id == ctrlbone.btc.setup_id:
continue
rig = ctrlbone.id_data
if context.mode != 'EDIT':
bpy.ops.object.mode_set(mode = 'EDIT')
if root_bone:
root_editbone = rig.data.edit_bones[root_bone.name]
ctrl_editbone = rig.data.edit_bones[ctrlbone.name]
if root_bone:
ctrl_editbone.parent = root_editbone if root_bone.btc.setup_id not in ids else None
if context.mode != 'POSE':
bpy.ops.object.mode_set(mode = 'POSE')
#relations between bones that are contrained to bones
con_bones_to_ctrls = {}
#Relations between ctrls that are baked from a certain bone
bake_bones_to_ctrl = {}
#Find the relative bones and constrain the ctrls back to the original armature bones
for ctrlbone in rebake_bones:
org_id = ctrlbone.btc.org_id
rig = ctrlbone.id_data
org_obj = rig.animtoolbox.controlled
found_org_bone = False
#find the original bone from the original armature
for org_bone in org_obj.pose.bones:
if org_bone.btc.org_id == org_id:
found_org_bone = True
break
if not found_org_bone:
continue
#remove constraints from the original bone
for con in org_bone.constraints:
if con.subtarget == ctrlbone.name and con.target == rig:
org_bone.constraints.remove(con)
con_bones_to_ctrls.update({org_bone : ctrlbone})
#constrain the control back to the bone
if ctrlbone.btc.setup == 'TEMPIK':
if ctrlbone.btc.org == 'MCH':
con = constraint_add(ctrlbone, org_bone)
#for flipped fk constrain the org bones to the original armature bones
elif ctrlbone.btc.setup == 'TEMPFK_FLIP':
if ctrlbone.btc.org == 'ORG':
con = constraint_add(ctrlbone, org_bone)
else:
if not ctrlbone.btc.child:
con = constraint_add(ctrlbone, org_bone)
bake_bones_to_ctrl.update({org_bone : ctrlbone})
if not con_bones_to_ctrls:
return
smartbake_to_ctrls(context, self, bake_bones_to_ctrl.keys(), bake_bones_to_ctrl.values())
# remove constraint from the ctrls and constraint the original bones back
for ctrlbone in bake_bones_to_ctrl.values():
for con in ctrlbone.constraints:
ctrlbone.constraints.remove(con)
for org_bone, ctrlbone in con_bones_to_ctrls.items():
con = constraint_add(org_bone, ctrlbone)
def controllers_remove_child(ctrl, controllers):
#If both child and parent are inside then keep only the parent
if not ctrl.parent:
return controllers
if ctrl.parent.animtoolbox.root:
return controllers
if ctrl.parent in controllers:
controllers.remove(ctrl)
return controllers
class Cleanup(bpy.types.Operator):
"""Select all the bones that are constrained to the empty"""
bl_idname = "anim.remove_bones_constraints"
bl_label = "Remove_Constrains_from_Bones"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
scene = context.scene
ids = get_bone_ids(context)
controllers, controlled_objs = get_controllers_controlled(context)
if not scene.btc.clean_constraints:
return {'FINISHED'}
#in case the controllers are empties add their ids and if the root is selected
for ctrl in list(controllers):
if not ctrl.animtoolbox.setup_id:
continue
ids.add(ctrl.animtoolbox.setup_id)
controllers_remove_child(ctrl, controllers)
remove_constraints(context, ids, controllers, controlled_objs)
if not scene.btc.clean_ctrls:
return {'FINISHED'}
if not ids:
return {'CANCELLED'}
remove_tempctrls(context, self, controllers, controlled_objs, ids)
return {'FINISHED'}
def ctrl_shape_items(self, context):
'''Add all the display items for bones and empties'''
shapes = ['Cube','Sphere','Circle','Arrows','Plain_Axes','Single_Arrow','Cone']
if context.scene.btc.ctrl_type == 'BONE':
shapes.insert(0, 'Original')
shapes.append('Bone')
#create an item similiar to ('CIRCLE', 'Circle', 'Circle display type', 1)
items = [(shape.upper(), shape, shape + ' display type', i) for i, shape in enumerate(shapes)]
return items
def add_enabled_driver(obj, rig, con):
datapath = con.path_from_id() + '.enabled'
if not obj.animation_data:
obj.animation_data_create()
#adding the driver
drv = obj.animation_data.drivers.find(datapath)
if drv is None:
drv = obj.animation_data.drivers.new(datapath)
#If variables exiwst already from before then use it
if len(drv.driver.variables):
var = drv.driver.variables[0]
else:
var = drv.driver.variables.new()
var.name = con.name
var.targets[0].id = rig
var.targets[0].data_path = 'animtoolbox.ctrls_enabled'
return drv
class UnlinkChains(bpy.types.Operator):
"""Unparent Temp Ctrl chains while keeping the original animation"""
bl_idname = "anim.unlink_temp_chains"
bl_label = "Unlink_Temp_Chain"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return len(context.selected_objects) and context.object.mode == 'POSE'
def execute(self, context):
# obj = context.object
clean_empty_ctrl_items(context.scene)
controllers = {item.controller for item in context.scene.btc.ctrl_items if item.controller}
controlled_objs = {item.controlled for item in context.scene.btc.ctrl_items if item.controlled}
if not controllers:
return {'CANCELLED'}
self.smartbake = True
#get all the setup ids from the selected bones
setup_ids = {getattr(bone.btc, 'setup_id') for bone in context.selected_pose_bones}
#create a dictionary with the setup ids and a list of the bone for each chain
id_chains = dict()
for obj in controllers:
if obj is None:
continue
controlled_selected = obj.animtoolbox.controlled.select_get()
for bone in obj.pose.bones:
setup_id = bone.btc.setup_id
if bone.id_data not in controllers:
continue
if setup_id not in setup_ids:
continue
if bone.btc.setup == 'POLE':
continue
if bone.btc.org == 'MCH' and bone.btc.setup != 'TEMPIK':
continue
if bone.btc.org == 'ORG':
continue
if setup_id not in id_chains:
id_chains.update({setup_id : [bone]})
else:
id_chains[setup_id].append(bone)
if not len(id_chains):
return {'CANCELLED'}
temp_col = find_temp_ctrl_collection(context.scene, controllers)
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
included_collections = exclude_collections(temp_col, context, controlled_objs)
#get the bones that need to be parented
bones_to_link = []
for setup_id, bones in id_chains.items():
hierarchies = get_hierarchies(bones)
hierarchy = [bone for hierarchy in hierarchies for bone in hierarchy]
if hierarchy[-1].parent is None:
continue
if not hierarchy[-1].parent.btc.setup_id:
continue
bones_to_link.append(hierarchy[-1])
rebake_connection_ctrls(self, context, obj, bones_to_link, None)
if obj.animtoolbox.controlled:
obj.animtoolbox.controlled.select_set(controlled_selected)
include_collections(included_collections, hidden_objs, temp_col, controlled_objs)
return {'FINISHED'}
class LinkTempChains(bpy.types.Operator):
"""Connect different chains of temp ctrls connected with each other while keeping the original animation"""
bl_idname = "anim.link_temp_chains"
bl_label = "Link_Temp_Chain"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return len(context.selected_objects) and context.object.mode == 'POSE'
def execute(self, context):
# obj = context.object
clean_empty_ctrl_items(context.scene)
controllers = {item.controller for item in context.scene.btc.ctrl_items if item.controller}
controlled_objs = {item.controlled for item in context.scene.btc.ctrl_items if item.controlled}
if not controllers:
return {'CANCELLED'}
self.smartbake = True
temp_col = find_temp_ctrl_collection(context.scene, controllers)
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
included_collections = exclude_collections(temp_col, context, controlled_objs)
#get all the setup ids from the selected bones
setup_ids = {getattr(bone.btc, 'setup_id') for bone in context.selected_pose_bones}
root_bone_name = None
#create a dictionary with the setup ids and a list of the bone for each chain
id_chains = dict()
for obj in controllers:
if obj is None:
continue
controlled_selected = obj.animtoolbox.controlled.select_get()
for bone in obj.pose.bones:
setup_id = bone.btc.setup_id
if bone.id_data not in controllers:
continue
if bone.btc.setup == 'ROOT':
root_bone_name = bone.name
continue
if setup_id not in setup_ids:
continue
if bone.btc.setup == 'POLE':
continue
if bone.btc.org == 'MCH' and bone.btc.setup != 'TEMPIK':
continue
if bone.btc.org == 'ORG':
continue
if setup_id not in id_chains:
id_chains.update({setup_id : [bone]})
else:
id_chains[setup_id].append(bone)
if len(id_chains) < 2:
include_collections(included_collections, hidden_objs, self.temp_col, controlled_objs)
return {'CANCELLED'}
#Get the parent of the active chain
active_bone = obj.data.bones.active.name
parent_posebone = obj.pose.bones[active_bone]
parent_chain_id = parent_posebone.btc.setup_id
parent_chain = id_chains[parent_chain_id]
hierarchies = get_hierarchies(parent_chain)
# hierarchy = [bone for hierarchy in hierarchies for bone in hierarchy]
if parent_posebone.btc.setup == 'TEMPIK' and context.scene.btc.link_to == 'BASE':
#get hierarchy only from Mechanical bones in case of TempIK
hierarchy = [bone for hierarchy in hierarchies for bone in hierarchy if bone.btc.org =='MCH']
else:
#normally get the hierarchy only from the ctrl bones
hierarchy = [bone for hierarchy in hierarchies for bone in hierarchy if bone.btc.org =='CTRL']
#parent chains to the head or the tip of
parent_bone = hierarchy[0] if context.scene.btc.link_to == 'TIP' else hierarchy[-1]
if parent_bone.name not in obj.pose.bones:
continue
#get the bones that need to be parented
bones_to_link = []
base_parent_name = None
for setup_id, bones in id_chains.items():
hierarchies = get_hierarchies(bones)
hierarchy = [bone for hierarchy in hierarchies for bone in hierarchy]
if setup_id == parent_chain_id:
if not hierarchy[-1].parent:
continue
#
if hierarchy[-1].parent.btc.setup_id not in id_chains:
continue
base_parent_name = hierarchy[-1].name
#Get the base bone of the hierarchy, unless it's ik setup than get the IK Ctrl
if hierarchy[-1].btc.setup == 'TEMPIK':
bone_to_link = hierarchy[-1] if hierarchy[-1].btc.org == 'CTRL' else hierarchy[0]
else:
bone_to_link = hierarchy[-1]
bones_to_link.append(bone_to_link)
rebake_connection_ctrls(self, context, obj, bones_to_link, parent_bone, root_bone_name, base_parent_name)
if obj.animtoolbox.controlled:
obj.animtoolbox.controlled.select_set(controlled_selected)
include_collections(included_collections, hidden_objs, temp_col, controlled_objs)
return {'FINISHED'}
def target_bone_position(self, context):
'''get the bone position for track constraint depending on the Axis'''
global rig, ctrls, child_names, selected
if not 'ctrls' in globals():
return
for ctrl, bone in zip(ctrls, selected):
if child_names:
#get the child before changing the ctrl to edit bone
childname = child_names[ctrls.index(ctrl)]
if rig.mode == 'EDIT':
ctrl = rig.data.edit_bones[ctrl.name]
org_rig = bone.id_data
if bone.name in org_rig.data.edit_bones:
bone = org_rig.data.edit_bones[bone.name]
else:
bone = org_rig.data.bones[bone.name]
matrix = get_bone_matrix(bone)
if type(bone.id_data) == bpy.types.Object:
matrix = bone.id_data.matrix_world @ matrix
ctrl.matrix = matrix
if self.bake_loc != 'TRACK':
continue
dir = -1 if self.axis[0] == '-' else 1
axis = self.axis[1]
axes = {'X' : 0, 'Y' : 1, 'Z' :2}
value = [0, 0, 0]
value[axes[axis]] = ctrl.length * self.offset * dir
local_translation = mathutils.Vector(value)
if rig.mode == 'EDIT':
world_translation = ctrl.matrix.to_3x3() @ local_translation
ctrl.tail += world_translation
ctrl.head += world_translation
elif rig.mode == 'POSE':
world_translation = ctrl.matrix_basis.to_3x3() @ local_translation
ctrl.location += world_translation
if not child_names:
continue
#apply the position to the children
child = rig.data.edit_bones[childname] if rig.mode == 'EDIT' else rig.pose.bones[childname]
child.matrix = ctrl.matrix
def remove_child_ctrls(rig, child_names):
if not child_names:
return
for child_name in child_names:
child = rig.data.edit_bones[child_name]
rig.data.edit_bones.remove(child)
return []
def child_prop_edit(self):
global ctrls, child_names, rig
child_ctrls = []
if not self.child:# and child_names:
#Remove child ctrls
child_names = remove_child_ctrls(rig, child_names)
return child_ctrls
#Add child ctrls
child_names = []
for ctrl_posebone in ctrls:
ctrl = rig.data.edit_bones[ctrl_posebone.name]
child_ctrl = add_ctrl_bone(rig, ctrl, ctrl, 'Child_', 'HEAD', 0.5, orient_world = self.orientation)
child_ctrl.matrix = ctrl.matrix
child_ctrls.append(child_ctrl)
add_bone_to_collection(rig, child_ctrl, col_name = 'Child Ctrls', visible = True)
child_names.append(child_ctrl.name)
return child_ctrls
def child_prop_pose(context):
#If there are new child bones then assign them the setup id and the shapes
global ctrls, child_names, rig
if not child_names:
return
for child_name, ctrl in zip(child_names, ctrls):
child = rig.pose.bones[child_name]
add_bone_setup_id(child, True, ctrl.btc.setup, 'CTRL', ctrl.btc.setup_id, ctrl.btc.org_id)
child.rotation_mode = ctrl.rotation_mode
if bpy.app.version < (4, 0, 0):
child.bone_group = ctrl.bone_group
else:
child.color.palette = context.scene.btc.color_set
tempctrl_shape_type(context.scene.btc, context)
def child_prop(self, context):
'''Add or remove child ctrls using the property in the ui panel'''
global ctrls, child_names, rig
# rig = self.rig
if rig.mode != 'EDIT':
bpy.ops.object.mode_set(mode = 'EDIT')
child_ctrls = child_prop_edit(self)
bpy.ops.object.mode_set(mode = 'POSE')
child_prop_pose(context)
def flip_prop(self, context):
global ctrls, selected, rig, child_names
if self.flip:
bpy.ops.object.mode_set(mode = 'EDIT')
for ctrl in ctrls:
ctrl_edit = rig.data.edit_bones[ctrl.name]
flip_editbone(self, ctrl_edit)
if self.child:
child_name = child_names[ctrls.index(ctrl)]
#Flip only the head since the tail is in the middle
child_edit = rig.data.edit_bones[child_name]
child_edit.head = ctrl_edit.head
bpy.ops.object.mode_set(mode = 'POSE')
for ctrl, bone in zip(ctrls, selected):
flip_posebone(context, ctrl, bone)
else:
bpy.ops.object.mode_set(mode = 'EDIT')
for ctrl, bone in zip(ctrls, selected):
ctrl_edit = rig.data.edit_bones[ctrl.name]
bone_matrix = get_bone_matrix(bone.bone)
# bone_edit = bone.id_data.data.edit_bones[bone.name]
ctrl_edit.matrix = bone_matrix
if self.child:
child_name = child_names[ctrls.index(ctrl)]
#Flip only the head since the tail is in the middle
child_edit = rig.data.edit_bones[child_name]
child_edit.head = ctrl_edit.head
bpy.ops.object.mode_set(mode = 'POSE')
for ctrl, bone in zip(ctrls, selected):
ctrl.matrix = bone.id_data.matrix_world @ bone.matrix
class TempFK(bpy.types.Operator):
"""Copy and bake the animation to a temporary FK chain"""
bl_idname = "anim.bake_to_temp_fk"
bl_label = "Bake_To_Temp_FK"
bl_options = {'REGISTER', 'UNDO'}
bake: bpy.props.BoolProperty(name = 'Bake Temp Ctrls', description = "Bakes Temp Ctrls, keeps them constrained when unchecked", default = True)
smartbake: bpy.props.BoolProperty(name = 'Smart Bake', description = "Keeps keyframes count from selection and recalculate interpolation", default = False)
bake_loc: bpy.props.EnumProperty(name = 'Type', description="Select what type of constraints will be used", default = 'COPY',
items = [('COPY', 'Copy Transforms','Bake to the bones heads', 0), ('TRACK', 'Track Target', 'Track the bone to an axis', 1)])
child: bpy.props.BoolProperty(name = 'Add Extra Child Ctrls', description = "Add an child control for an overlay control", default = False, update = child_prop)
con_back: bpy.props.BoolProperty(name = 'Constrain selected to Temp Ctrls', description = "Constraint the selected bones back to the Temp controls", default = True)
orientation: bpy.props.BoolProperty(name = 'Use World Space Orientation', description = "Orient the bones to world space instead of to the original bones", default = False)
flip: bpy.props.BoolProperty(name = 'Flip Temp Ctrls', description = "Flipping chain bones when using reversed hierarchy", default = False, update = flip_prop)
rotation_mode: bpy.props.EnumProperty(name = 'Rotation Mode', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
items = [('PRESERVE', 'Preserve','Keep Rotation Order from the original selected bones', 0),
('QUATERNION', 'Quaternion','Quaternion Rotation Order - No Gimbal Lock', 1),
('XYZ', 'XYZ', 'XYZ Rotation Order', 2), ('XZY', 'XZY','XZY Rotation Order', 3),
('YXZ', 'YXZ','YXZ Rotation Order', 4), ('YZX', 'YZX', 'YZX Rotation Order', 5),
('ZXY', 'ZXY', 'ZXY Rotation Order', 6), ('ZYX', 'ZYX', 'ZYX Rotation Order', 7),
('AXIS_ANGLE', 'AXIS_ANGLE', 'Axis Angle Rotation Order', 8)])
filter_bool: bpy.props.BoolProperty(name = 'Filter Properties', description = "Extend all the filter elements", default = False)
filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(False, False, False), size = 3, options={'HIDDEN'})
filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(False, False, False), size = 3, options={'HIDDEN'})
filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(False, False, False), size = 3, options={'HIDDEN'})
@classmethod
def poll(cls, context):
return len(context.selected_objects) and context.object.mode == 'POSE'
def invoke(self, context, event):
obj = context.object
self.org_obj = obj
if obj.type != 'ARMATURE':
self.report({'ERROR'},'Currently works only with Armature objects')
return {'CANCELLED'}
if obj.animtoolbox.controlled:
self.report({'ERROR'},'Selected object is already a control rig')
return {'CANCELLED'}
global ctrls, child_names, selected, root_name, rig, setup_ids
context.scene.btc.ctrl_type = 'BONE'
selected = context.selected_pose_bones if obj.mode == 'POSE' else []
if not selected:
return {'CANCELLED'}
if clean_bone_before_setup(context, selected):
#Clear setup is turned off, then cancelling this operator
self.report({'ERROR'},'Selected bones are already connected to temp ctrls. Clear current ctrls before continuing')
return {'CANCELLED'}
self.bone_selection = Tools.bone_selection.copy()
#check if bones need to flipped
flip = False
for i, bone in enumerate(self.bone_selection[:-1]):
bone_next = self.bone_selection[i+1]
# head_dist = mathutils.Vector([abs(axis) for axis in (bone.bone.head - bone_next.bone.tail)])
# tail_dist = mathutils.Vector([abs(axis) for axis in (bone.bone.tail - bone_next.bone.head)])
head_dist = (bone.bone.head_local - bone_next.bone.tail_local).length
tail_dist = (bone.bone.tail_local - bone_next.bone.head_local).length
if head_dist < tail_dist:
flip = True
break
root_name = None
#if there are no display shapes then use the cube as the default
if not any([bone.custom_shape for bone in selected]):
# context.scene.btc.shape_type = 'CUBE' <--- is running tempctrl_shape_type too early
context.scene.btc['shape_type'] = 1
wm = context.window_manager
bake_layers_default(obj, context.scene)
get_org_action(obj, self)
########################################
if not selected:
return {'CANCELLED'}
self.shape_type = context.scene.btc.shape_type
self.shape_size = context.scene.btc.shape_size
root_ctrl = None
rig, self.temp_col = add_ctrl_rig(selected)
#self.rig = rig
bpy.ops.object.mode_set(mode = 'EDIT')
root_update_edit()
ctrls, child_ctrls = add_control_bones(self, selected, rig, None, self.child)
ctrl_names = [ctrl.name for ctrl in ctrls]
child_names = [child.name for child in child_ctrls]
for ctrl in ctrls:
add_bone_to_collection(rig, ctrl, col_name = 'FK Ctrls', visible = True)
if self.child:
child_ctrl = child_ctrls[ctrls.index(ctrl)]
#Flip only the head since the tail is in the middle
child_ctrl.head = ctrl.head
ctrl_names = [ctrl.name for ctrl in ctrls]
child_names = [child.name for child in child_ctrls]
# root_update(context.scene.btc, context)
###################################
bpy.ops.object.mode_set(mode = 'POSE')
ctrls = [rig.pose.bones[name] for name in ctrl_names]
child_ctrls = [rig.pose.bones[name] for name in child_names]
ctrl_group = add_ctrl_group(rig) if bpy.app.version < (4, 0, 0) else None
setup = 'TEMPFK'
setup_ids = set()
#assign id to root control and the position
if context.scene.btc.root:
root_update_pose(context.scene.btc, context)
setup_id = add_id(rig, setup)
setup_ids.add(setup_id)
assign_setup_ids(self, context, ctrls, selected, child_ctrls, setup, setup_id, ctrl_group)
self.flip = flip
# flip_prop(self, context)
tempctrl_shape_type(self, context)
if not self.org_action:
self.orientation = False
return wm.invoke_props_dialog(self, width = 200)
return wm.invoke_props_dialog(self, width = 250)
def draw(self, context):
layout = self.layout
btc = context.scene.btc
#Frame Range
split = layout.split(factor=0.5, align = True)
split.prop(btc, 'bake_range_type', text = '')
split.prop(btc, 'bake_layers', text = 'Include All Layers')
if btc.bake_range_type == 'CUSTOM':
row = layout.row()
row.prop(btc, 'bake_range', text = '')
layout.separator()
row = layout.row()
row.prop(btc, 'shape_size', text = 'Size')
row.prop(btc, 'shape_type', text = '')
layout.separator()
row = layout.row()
row.label(text = 'Rotation Mode:')
row.prop(self, 'rotation_mode', text = '')
layout.separator()
col = layout.column()
col.prop(self, 'smartbake')
if not self.flip:
col.prop(self, 'orientation')
col.prop(self, 'flip')
split = layout.split(factor = 0.9)
split.prop(self, 'con_back')
split.prop(self, 'filter_bool', icon = 'FILTER')
if self.filter_bool:
split = layout.split(factor = 0.75)
split.label(text = "Filter Properties")
split.label(text = "X Y Z")
Tools.filter_draw_ui(self, self, titel = False)
layout.separator()
row = layout.row()
row.label(text = 'Add Hierarchy:')
row = layout.row()
row.prop(btc, 'root', text = 'Root Ctrl')
row.prop(self, 'child', text = 'Extra Child Ctrl')
if btc.root:
# row = box.row(align = True)
row = layout.row()
row.prop(btc, 'root_object', text = '')
if hasattr(btc.root_object, 'type'):
if btc.root_object.type == 'ARMATURE':
# row = box.row(align = True)
row = layout.row()
row.prop_search(btc, 'root_bone', btc.root_object.pose, 'bones', text = '')
def execute(self, context):
# sec = time.time()
global ctrls, child_names, selected, root_name, rig, setup_ids
objs = {bone.id_data for bone in selected}
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
included_collections = exclude_collections(self.temp_col, context, objs)
if 'ctrls' not in globals():
self.report({'INFO'},'Not available. Please update the Temp Ctrls from the addon panel')
return {'CANCELLED'}
#create dictionary between selected bones and the temp ctrls
selected_ctrls = dict(zip(selected, ctrls))
if not context.active_object:
context.view_layer.objects.active = rig
bpy.ops.object.mode_set(mode = 'EDIT')
# root_ctrl = rig.data.edit_bones[root_name] if root_name else None
root_ctrl = parent_ctrls_to_root(root_name, rig, ctrls)
if not self.flip:
ctrls_orientation(self, ctrls, child_names)
#setup hierarchies
self.create_hierarchy(selected, selected_ctrls, rig)
#adding helper bones
if self.flip:
orgs, rest = add_control_bones(self, selected, rig, root_ctrl, False, prefix = 'ORG_')
mchs, rest = add_control_bones(self, selected, rig, root_ctrl, False, prefix = 'MCH_')
for ctrl, mch, org in zip(ctrls, mchs, orgs):
add_bone_to_collection(rig, mch, col_name = 'MCH', visible = False)
add_bone_to_collection(rig, org, col_name = 'ORG', visible = False)
flip_editbone(self, mch)
mch.parent = org
org_names = [org.name for org in orgs]
mch_names = [mch.name for mch in mchs]
##################################
bpy.ops.object.mode_set(mode = 'POSE')
con_type = 'COPY_TRANSFORMS'
#Assigning rotation modes
for ctrl, bone in zip(ctrls, selected):
ctrl.rotation_mode = bone.rotation_mode if self.rotation_mode == 'PRESERVE' else self.rotation_mode
#apply the rotation mode to the childs
if child_names:
for ctrl, childname in zip(ctrls, child_names):
rig.pose.bones[childname].rotation_mode = ctrl.rotation_mode
#if it's not a flipped fk then just bake and finish
if not self.flip:
for ctrl, bone in zip(ctrls, selected):
constraint_add(ctrl, bone, con_type)
root_ctrl, root_org = add_root_to_bake(self, context, rig, root_name, ctrls, selected)
action = create_anim_data_action(rig, self.org_obj)
if action is not None:
smartbake_to_ctrls(context, self, selected, ctrls)
else:
set_ctrl_matrix(selected, ctrls)
constraints_clear(ctrls)
custom_range_switch(context.scene, action, rig)
#if it's using root ctrl reference then remove them from ctrls and selected, to not include anymore in the bake
if root_org:
ctrls.remove(root_ctrl)
selected.remove(root_org)
constraint_org_to_ctrl(self, ctrls, selected)
include_collections(included_collections, hidden_objs, self.temp_col, objs)
del ctrls, child_names, selected, root_name, rig, setup_ids
return {'FINISHED'}
orgs = [rig.pose.bones[name] for name in org_names]
mchs = [rig.pose.bones[name] for name in mch_names]
for ctrl, mch, org, bone in zip(ctrls, mchs, orgs, selected):
#copy the ids to the helper bones
mch.btc.org_id = org.btc.org_id = ctrl.btc.org_id
mch.btc.setup_id = org.btc.setup_id = ctrl.btc.setup_id
ctrl.btc.org = 'CTRL'
org.btc.org = 'ORG'
mch.btc.org = 'MCH'
ctrl.btc.setup = org.btc.setup = mch.btc.setup = 'TEMPFK_FLIP'
ctrl.color.palette = context.scene.btc.color_set
org.custom_shape = None
#constraining the ctrls to the flipped mechanical bones that are parented to the org bones
constraint_add(org, bone, con_type)
constraint_add(ctrl, mch, con_type)
#add the root to a copy of the ctrls and selected
root_ctrl, root_org = add_root_to_bake(self, context, rig, root_name, ctrls, selected)
action = create_anim_data_action(rig, self.org_obj)
self.bake_loc = 'COPY'
if action is not None:
smartbake_to_ctrls(context, self, selected, ctrls)
else:
#If there is no action then just set the bones matrices
set_ctrl_matrix(selected, ctrls)
constraints_clear(ctrls)
constraints_clear(orgs)
custom_range_switch(context.scene, action, rig)
#if it's using root ctrl reference then remove them from ctrls and selected
if root_org:
ctrls.remove(root_ctrl)
selected.remove(root_org)
#reseting values of the org
for org in orgs:
org.location = (0,0,0)
org.rotation_euler = (0.0, 0.0, 0.0)
org.rotation_quaternion = (1.0, 0.0, 0.0, 0.0)
org.scale = (1,1,1)
bpy.ops.object.mode_set(mode = 'EDIT')
child_ctrls = [rig.data.edit_bones[name] for name in child_names]
#switch to the child controling the original bone and reparenting the original to the child ctrl
#In this case the hierarchy and relations with the org bones
for ctrl, org, bone in zip(ctrls, orgs, selected):
ctrl_edit = rig.data.edit_bones[ctrl.name]
org_edit = rig.data.edit_bones[org.name]
if self.child and child_ctrls:
ctrl_edit = child_ctrls[ctrls.index(ctrl)]
org_edit.parent = ctrl_edit
bpy.ops.object.mode_set(mode = 'POSE')
include_collections(included_collections, hidden_objs, self.temp_col, objs)
if not self.con_back:
del ctrls, child_names, selected, root_name, rig, setup_ids
return {'FINISHED'}
#constraint the original bones to the org bones
for org, bone in zip(orgs, selected):
if org.btc.setup == 'ROOT': #dont need to add constrain for the root bone
continue
# add constraints but with attribute filter
constraints_filter(self, bone, org, con_type)
del ctrls, child_names, selected, root_name, rig, setup_ids
return {'FINISHED'}
def cancel(self, context):
cancel_tempctrls(context)
global ctrls, child_names, selected, root_name, rig, setup_ids
del ctrls, child_names, selected, root_name, rig, setup_ids
return None
def create_hierarchy(self, bone_selection, selected_ctrls, rig):
#Create hierachy from the selection order
selection_order = bone_selection if self.flip else list(reversed(bone_selection))
for i, bone in enumerate(selection_order[:-1]):
if bone not in selected_ctrls:
continue
#use selected ctrls dictionary to find the ctrl
ctrl = selected_ctrls[bone]
# add_bone_to_collection(rig, ctrl, col_name = 'FK Ctrls', visible = True)
parent_bone = selection_order[i+1]
parent_ctrl = selected_ctrls[parent_bone]
if self.child:
child = selected_ctrls[parent_bone].children[0]
parent_ctrl = child
rig.data.edit_bones[ctrl.name].parent = rig.data.edit_bones[parent_ctrl.name]
def flip_editbone(self, bone):
if not self.flip:
return
bone_head, bone_tail = bone.head.copy(), bone.tail.copy()
bone.tail, bone.head = bone_head, bone_tail
def flip_posebone(context, ctrl, bone):
org_obj_mat = bone.id_data.matrix_world
ctrl.matrix = org_obj_mat @ bone.matrix
context.view_layer.update()
new_loc = ctrl.tail
rotation_matrix = mathutils.Matrix.Rotation(math.pi, 4, 'Y')
new_matrix = ctrl.matrix_basis @ rotation_matrix
ctrl.matrix_basis = new_matrix
ctrl.matrix.translation = new_loc
def fcurves_remove_bones(rig, bones):
bone_paths = [bone.path_from_id() for bone in bones]
fcurves = Tools.get_fcurves_channelbag(rig, rig.animation_data.action)
for fcu in fcurves:
path = fcu.data_path.split('"].')[0] + ('"]')
if path in bone_paths:
fcurves.remove(fcu)
def reset_root_bone_ref(self):
if not self.root_object:
return True
if not hasattr(self.root_object, 'type'):
return True
if self.root_object.type != 'ARMATURE':
return True
if self.root_bone not in self.root_object.data.bones:
return True
return False
def root_update_edit():
'''Breaking down properties during edit mode and pose mode
to be able to use it during invoke without switching so often'''
global root_name, rig
if not root_name:
root_ctrl = add_root_bone(rig)
if root_ctrl:
root_name = root_ctrl.name
# move_root_editbone(root_ctrl)
else:
root_ctrl= rig.data.edit_bones[root_name]
return root_name
def root_update_pose(self, context):
global root_name, rig, setup_ids
if reset_root_bone_ref(self):
self['root_bone'] = ''
if root_name not in rig.pose.bones:
return
root_ctrl = rig.pose.bones[root_name]
obj = self.root_object
#check if root ctrl already exists and connected to other bones
if len(root_ctrl.children):
return
#selecting the bone for the shape to apply to it as well
root_ctrl.bone.select = True
#if it doesn't have already setup id then add it in order to get the shape
if not root_ctrl.btc.setup_id:
root_id = assign_id_root(self, rig, root_ctrl)
setup_ids.add(root_id)
root_ctrl.color.palette = context.scene.btc.color_set
if not obj:
#get the cursor position also in pose mode
root_ctrl.matrix = Matrix.Identity(4)
root_ctrl.matrix.translation = context.scene.cursor.matrix.translation
return
if obj.type == 'ARMATURE' and self.root_bone in obj.data.bones:
#get the bone ref position
ref_bone = obj.pose.bones[self.root_bone]
root_ctrl.matrix = obj.matrix_world @ ref_bone.matrix
ref_bone.btc.setup_id = root_ctrl.btc.setup_id
context.view_layer.update()
else:
#get the object position
root_ctrl.matrix = obj.matrix_world
def root_update(self, context):
'''checks if the previous selected bone is included in the current selected object'''
global ctrls, root_name, rig
if not self.root:
return
#For empties run a different function
if ctrls[0].id_data.type == 'EMPTY':
empty_root_update(self, context)
return
bpy.ops.object.mode_set(mode = 'EDIT')
if root_name:
remove_root_ctrl()
#switching to pose mode to regsiter the bone
bpy.ops.object.mode_set(mode = 'POSE')
bpy.ops.object.mode_set(mode = 'EDIT')
root_name = None
root_update_edit()
bpy.ops.object.mode_set(mode = 'POSE')
root_update_pose(self, context)
tempctrl_shape_type(self, context)
def remove_root_ctrl():
'''remove the root bone and the ids connected to it'''
global rig, root_name
if root_name not in rig.data.edit_bones:
return
if rig.data.edit_bones[root_name].children:
return
root_id = rig.pose.bones[root_name].btc.setup_id
remove_id(rig, rig.pose.bones[root_name].btc.setup_id)
#remove the id from the original bone if it exist
root_org = next((bone for bone in rig.animtoolbox.controlled.pose.bones if bone.btc.setup_id == root_id), None)
if root_org:
root_org.btc.setup_id = 0
rig.data.edit_bones.remove(rig.data.edit_bones[root_name])
def root_prop(self, context):
#Add or remove the root control using the property self.root
global ctrls, child_names, root_name, rig
#For empties run a different function
if ctrls[0].id_data.type == 'EMPTY':
empty_root_prop(self, context)
return
#Adding or finiding a root bone
if self.root:
root_update(self, context)
tempctrl_shape_type(self, context)
# root_ctrl.color.palette = context.scene.btc.color_set
return
#elif not self.root then remove the root bone
if context.mode != 'EDIT':
bpy.ops.object.mode_set(mode = 'EDIT')
# if not root_name:
# root_name = next((bone.name for bone in rig.pose.bones if bone.btc.setup == 'ROOT'), None)
# else:
if root_name:
root_ctrl = rig.data.edit_bones[root_name]
if not root_ctrl:
root_name = None
bpy.ops.object.mode_set(mode = 'POSE')
return
#if no one is using the root, then REMOVE it
if root_name:
remove_root_ctrl()
bpy.ops.object.mode_set(mode = 'POSE')
root_name = None
def assign_id_root(self, rig, root_ctrl):
root_id = add_id(rig, 'ROOT')
add_bone_setup_id(root_ctrl, False, 'ROOT', 'CTRL', setup_id = root_id)
root_ctrl.btc.setup = 'ROOT'
#add the org id to the root bone and the original bone
if self.root_bone == '':
return root_id
org_id = 0
org_bone = self.root_object.pose.bones[self.root_bone]
if org_bone.btc.org_id:
org_id = org_bone.btc.org_id
else:
org_id = add_id(rig, 'NONE', True)
org_bone.btc.org_id = root_ctrl.btc.org_id = org_id
org_bone.btc.setup = root_ctrl.btc.setup
org_bone.btc.org = root_ctrl.btc.org
return root_id
def copy_setup_properties(target, source):
'''Copy all the setup properties between bones'''
target.btc.setup = source.btc.setup
target.btc.setup_id = source.btc.setup_id
target.btc.org_id = source.btc.org_id
target.btc.org = source.btc.org
target.rotation_mode = source.rotation_mode
target.matrix = source.matrix
def assign_setup_ids(self, context, ctrls, selected, child_ctrls, setup, setup_id, ctrl_group):
'''Assign Ids and properties in Pose mode'''
#Assign group, color, matrix and setup ids in pose mode
for ctrl, bone in zip(ctrls, selected):
obj_mat = bone.id_data.matrix_world
ctrl.matrix = obj_mat @ bone.matrix
#assign the controller to a group
if bpy.app.version < (4, 0, 0):
ctrl.bone_group = ctrl_group
else:
ctrl.color.palette = context.scene.btc.color_set
# rig.data.bones[ctrl.name].color.palette = context.scene.btc.color_set
add_bone_to_collection(rig, ctrl, col_name = 'World Space', visible = True)
org_id = add_id(rig, setup)
#Only WorldSpace controls need a new setup id on every iteration
# (since every iteration is a new chain)
if setup in {'WORLDSPACE', 'TRACK_TO'}:
setup_id = add_id(rig, setup)
add_bone_setup_id(ctrl, False, setup, 'CTRL', setup_id , org_id)
add_bone_setup_id(bone, False, setup, 'NONE', setup_id, org_id)
if self.child and child_ctrls:
child_ctrl = child_ctrls[ctrls.index(ctrl)]
# child_ctrl = ctrl.children[0]
if bpy.app.version < (4, 0, 0):
child_ctrl.bone_group = ctrl_group
else:
child_ctrl.color.palette = context.scene.btc.color_set
# rig.data.bones[child_ctrl.name].color.palette = context.scene.btc.color_set
add_bone_setup_id(child_ctrl, True, setup, 'CTRL', setup_id, org_id)
#get custom shape and assign the matrix
if bone.custom_shape_transform and self.shape_type == 'ORIGINAL':
ot_bone = bone.custom_shape_transform.name
#Get the original override transform bone
target = bone.id_data.pose.bones[ot_bone]
ctrl.custom_shape_transform = target
ctrl.custom_shape_transform.btc.setup_id = ctrl.btc.setup_id
ctrl.custom_shape_transform.matrix = obj_mat @ bone.custom_shape_transform.matrix
def parent_ctrls_to_root(root_name, rig, ctrls):
'parent the bones to root ctrl'
if not root_name:
return None
if root_name not in rig.data.edit_bones:
return None
root_ctrl = rig.data.edit_bones[root_name]
for ctrl in ctrls:
rig.data.edit_bones[ctrl.name].parent = root_ctrl
return root_ctrl
def bake_layers_default(obj, scene):
#Turn bake layers automatically if there is no active action but the layers are turned on
if not obj.animation_data:
return
if not obj.animation_data.use_nla:
return
strips = [strip for track in obj.animation_data.nla_tracks for strip in track.strips]
if obj.animation_data.action is None and len(strips):
scene.btc.bake_layers = True
#check if there is an offset, then use scene frame range by default
# for strip in strips:
# start_offset = strip.action.frame_range[0] - strip.frame_start
# strip_offset = (strip.action_frame_start - strip.frame_start - start_offset) * strip.scale + start_offset
# offset = strip.action_frame_start * strip.scale - strip.action_frame_start * strip.scale + strip_offset
# if offset:
# scene.btc.bake_range_type = 'SCENE'
# break
def find_temp_ctrl_collection(scene, controllers):
'''Find the Temp Ctrl collection'''
for ctrl in controllers:
for temp_col in scene.collection.children:
if ctrl in temp_col.objects.values():
return temp_col
return None
def exclude_collections(temp_col, context, objs):
'''Excluding all collections during bake except
for the control rig and controlled object collection.
Controlled object is moved temporarly to temp ctrl collection during bake'''
if not temp_col:
return
#set of collection that were still turned on
included_collections = set()
scene_collections = context.scene.collection.children
view_collections = context.view_layer.layer_collection.children
#Add the controlled object to the temp ctrl collection
for obj in objs:
if obj not in temp_col.objects.values():
temp_col.objects.link(obj)
#iterating parallel to scene and view collection because of different options
for scene_col, view_col in zip(scene_collections, view_collections):
#if it's temp ctrl, or temp ctrls are child collection then skip to avoid disable it
if scene_col == temp_col or temp_col in scene_col.children_recursive:
view_col.exclude = False
continue
elif not view_col.exclude:
included_collections.add(view_col)
view_col.exclude = True
return included_collections
def include_collections(included_collections, hidden_objs, temp_col, org_objs):
'''Resume collections to initial setup after bake'''
if not temp_col:
return
for col in included_collections:
col.exclude = False
for obj in hidden_objs:
if not obj.hide_viewport:
obj.hide_set(True)
#controlled object was included in the temp ctrl collection and now is being unlinked
for obj in org_objs:
if obj in temp_col.objects.values():
temp_col.objects.unlink(obj)
#In case the object is not available in any collection anymore then link it to the main collection
bpy.context.view_layer.update()
if obj not in bpy.context.view_layer.objects.values():
bpy.context.scene.collection.objects.link(obj)
def filter_draw_ui(self):
layout = self.layout
box = layout.box()
row = box.row()
if self.bake_loc != 'TRACK':
row.label(text = 'Location')
row.prop(self, 'filter_location', text = '')
row = box.row()
row.label(text = 'Rotation')
row.prop(self, 'filter_rotation', text = '')
row = box.row()
if self.bake_loc != 'TRACK':
row.label(text = 'Scale')
row.prop(self, 'filter_scale', text = '')
class WorldSpaceProperties:
'''Shared properties between worldspace controls'''
bake: bpy.props.BoolProperty(name = 'Bake Temp Ctrls', description = "Bakes Temp Ctrls, keeps them constrained when unchecked", default = True)
smartbake: bpy.props.BoolProperty(name = 'Smart Bake', description = "Keeps keyframes count from selection and recalculate interpolation", default = False)
twist: bpy.props.BoolProperty(name = 'Include Twist', description = "Use Track To constraint to include the twist rotation", default = True)
con_back: bpy.props.BoolProperty(name = 'Constrain selected to Temp Ctrls', description = "Constraint the selected bones back to the Temp controls", default = True)
orientation: bpy.props.BoolProperty(name = 'Use World Space Orientation', description = "Orient the bones to world space instead of to the original bones", default = True)
rotation_mode: bpy.props.EnumProperty(name = 'Rotation Mode', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
items = [('PRESERVE', 'Preserve','Keep Rotation Order from the original selected bones', 0),
('QUATERNION', 'Quaternion','Quaternion Rotation Order - No Gimbal Lock', 1),
('XYZ', 'XYZ', 'XYZ Rotation Order', 2), ('XZY', 'XZY','XZY Rotation Order', 3),
('YXZ', 'YXZ','YXZ Rotation Order', 4), ('YZX', 'YZX', 'YZX Rotation Order', 5),
('ZXY', 'ZXY', 'ZXY Rotation Order', 6), ('ZYX', 'ZYX', 'ZYX Rotation Order', 7),
('AXIS_ANGLE', 'AXIS_ANGLE', 'Axis Angle Rotation Order', 8)])
filter_bool: bpy.props.BoolProperty(name = 'Filter Properties', description = "Extend all the filter elements", default = False)
filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(False, False, False), size = 3, options={'HIDDEN'})
filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(False, False, False), size = 3, options={'HIDDEN'})
filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(False, False, False), size = 3, options={'HIDDEN'})
class WorldSpaceCtrls(bpy.types.Operator, WorldSpaceProperties):
"""Copy and bake the animation to temporary controls"""
bl_idname = "anim.bake_to_ctrl"
bl_label = "Bake_To_Controls"
bl_options = {'REGISTER', 'UNDO'}
bake_loc: bpy.props.EnumProperty(name = 'Type', description="Select what type of constraints will be used", update = target_bone_position,
items = [('COPY', 'Copy Transforms','Bake to the bones heads', 0), ('TRACK', 'Track Target', 'Track the bone to an axis', 1)])
axis: bpy.props.EnumProperty(name = 'Axis', description="Which direction of the original bone the target should point", update = target_bone_position, default = 1,
items = [('+X', '+X','Target Bone the +X Axis', 0), ('+Y', '+Y', 'Target Bone the +Y Axis', 1), ('+Z', '+Z', 'Target Bone the +Z Axis', 2),
('-X', '-X','Target Bone the -X Axis', 3), ('-Y', '-Y', 'Target Bone the -Y Axis', 4), ('-Z', '-Z', 'Target Bone the -Z Axis', 5)])
offset: bpy.props.FloatProperty(name="Offset", description="Offset the bone in the axis direction", default=1.0, update = target_bone_position)
child: bpy.props.BoolProperty(name = 'Add Extra Child Ctrls', description = "Add an child control for an overlay control", default = False, update = child_prop)
@classmethod
def poll(cls, context):
return len(context.selected_objects) and context.object.mode == 'POSE'
def invoke(self, context, event):
obj = context.object
self.org_obj = obj
scene = context.scene
if obj.type != 'ARMATURE':
self.report({'ERROR'},'Currently works only with Armature objects')
return {'CANCELLED'}
if obj.animtoolbox.controlled:
self.report({'ERROR'},'Selected object is already a control rig')
return {'CANCELLED'}
# if any(bone.btc.setup_id for bone in context.selected_pose_bones):
# self.report({'ERROR'},'Selected bones already have a chain applied to them')
# return {'CANCELLED'}
global ctrls, child_names, selected, root_name, rig, setup_ids
context.scene.btc.ctrl_type = 'BONE'
selected = context.selected_pose_bones if obj.mode == 'POSE' else []
if not selected:
return {'CANCELLED'}
if clean_bone_before_setup(context, selected):
#Clear setup is turned off, then cancelling this operator
self.report({'ERROR'},'The selected bones are already connected to temp ctrls. Clean the current ctrls before proceeding')
return {'CANCELLED'}
setup_ids = set()
#if there are no display shapes then use the cube as the default
if not any([bone.custom_shape for bone in selected]):
# scene.btc.shape_type = 'CUBE'
scene.btc['shape_type'] = 1
root_name = None
self.message = None
if any([posebone.bone.use_connect for posebone in selected]):
# if 'textwrap' not in locals():
# import textwrap
self.bake_loc = 'TRACK'
self.message = 'Some of the bones are connected, using Type: Track by default.'
bake_layers_default(obj, scene)
wm = context.window_manager
get_org_action(obj, self)
self.shape_type = scene.btc.shape_type
self.shape_size = scene.btc.shape_size
root_ctrl = None
#getting the rig and its collection
rig, self.temp_col = add_ctrl_rig(selected)
bpy.ops.object.mode_set(mode = 'EDIT')
root_ctrl = add_root_bone(rig)
if root_ctrl:
root_name = root_ctrl.name
ctrls, child_ctrls = add_control_bones(self, selected, rig, None, self.child)
ctrl_names = [ctrl.name for ctrl in ctrls]
child_names = [child.name for child in child_ctrls]
# root_update(context.scene.btc, context)
if scene.btc.root:
root_name = root_update_edit()
###################################
bpy.ops.object.mode_set(mode = 'POSE')
if scene.btc.root:
root_update_pose(scene.btc, context)
ctrls = [rig.pose.bones[name] for name in ctrl_names]
child_ctrls = [rig.pose.bones[name] for name in child_names]
ctrl_group = add_ctrl_group(rig) if bpy.app.version < (4, 0, 0) else None
setup = 'WORLDSPACE' if self.bake_loc == 'COPY' else 'TRACK_TO'
setup_id = None
assign_setup_ids(self, context, ctrls, selected, child_ctrls, setup, setup_id, ctrl_group)
tempctrl_shape_type(self, context)
target_bone_position(self, context)
return wm.invoke_props_dialog(self, width = 250)
def draw(self, context):
layout = self.layout
btc = context.scene.btc
#Frame Range
split = layout.split(factor=0.5, align = True)
split.prop(btc, 'bake_range_type', text = '')
split.prop(btc, 'bake_layers', text = 'Include All Layers')
if btc.bake_range_type == 'CUSTOM':
row = layout.row()
row.prop(btc, 'bake_range', text = '')
layout.separator()
row = layout.row()
row.prop(btc, 'shape_size', text = 'Size')
row.prop(btc, 'shape_type', text = '')
layout.separator()
if context.object.mode == 'POSE':
if self.message:
box = layout.box()
col = box.column(align = True)
col.alignment = 'CENTER'
col.label(text = self.message[:33])
col.label(text = self.message[33:])
row = layout.row()
row.label(text = 'Rotation Mode:')
row.prop(self, 'rotation_mode', text = '')
split = layout.split(factor = 0.4)
split.label(text = 'Type: ')
split.prop(self, 'bake_loc', text = '')
if self.bake_loc == 'TRACK':
split = layout.split(factor = 0.4)
split.label(text = 'Axis: ')
split.prop(self, 'axis', text = '')
split.prop(self, 'offset', text = '')
col = layout.column()
col.prop(self, 'twist', text = 'Include Twist')
layout.separator()
col = layout.column()
col.prop(self, 'smartbake')
col.prop(self, 'orientation')
split = layout.split(factor = 0.9)
split.prop(self, 'con_back')
split.prop(self, 'filter_bool', icon = 'FILTER')
if self.filter_bool:
split = layout.split(factor = 0.75)
split.label(text = "Filter Properties")
split.label(text = "X Y Z")
filter_draw_ui(self)
layout.separator()
row = layout.row()
row.label(text = 'Add Hierarchy:')
row = layout.row()
row.prop(btc, 'root', text = 'Root Ctrl')
row.prop(self, 'child', text = 'Extra Child Ctrl')
if btc.root:
# row = box.row(align = True)
row = layout.row()
row.prop(btc, 'root_object', text = '')
if hasattr(btc.root_object, 'type'):
if btc.root_object.type == 'ARMATURE':
# row = box.row(align = True)
row = layout.row()
row.prop_search(btc, 'root_bone', btc.root_object.pose, 'bones', text = '')
def execute(self, context):
# sec = time.time()
# obj = context.object
global ctrls, child_names, selected, root_name, rig, setup_ids
if 'ctrls' not in globals():
self.report({'INFO'},'Not available. Please update the Temp Ctrls from the addon panel')
return {'CANCELLED'}
if not selected:
return {'CANCELLED'}
objs = {bone.id_data for bone in selected}
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
included_collections = exclude_collections(self.temp_col, context, objs)
#store the matrices for the target ctrls before changing their position in edit mode
ctrl_matrices = dict()
for ctrl in ctrls:
ctrl_matrices.update({ctrl: ctrl.matrix.copy()})
bpy.ops.object.mode_set(mode = 'EDIT')
parent_ctrls_to_root(root_name, rig, ctrls)
if self.bake_loc == 'TRACK':
target_bone_position(self, context)
ctrls_orientation(self, ctrls, child_names)
mch_names = add_twist_helpers(self, selected, root_name)
bpy.ops.object.mode_set(mode = 'POSE')
mch_bones = [rig.pose.bones[name] for name in mch_names]
setup = 'WORLDSPACE' if self.bake_loc == 'COPY' else 'TRACK_TO'
#Add the constraints to the ctrls
for ctrl, bone in zip(ctrls, selected):
#update the setup in case it was changed to
ctrl.btc.setup = bone.btc.setup = setup
#Rotation mode works only in Pose mode, Needs to be in execute and not invoke
ctrl.rotation_mode = bone.rotation_mode if self.rotation_mode == 'PRESERVE' else self.rotation_mode
ctrl.matrix = ctrl_matrices[ctrl]
#add constraint to the custom shape bones
if ctrl.custom_shape_transform is None:
continue
if bone.custom_shape_transform and self.shape_type == 'ORIGINAL':
ot_bone = bone.custom_shape_transform.name
#Get the original override transform bone
target = bone.id_data.pose.bones[ot_bone]
ctrl.custom_shape_transform.btc.setup_id = ctrl.btc.setup_id
constraint_add(ctrl.custom_shape_transform, target, 'COPY_TRANSFORMS')
#apply the rotation mode to the childs
if child_names:
for ctrl, childname in zip(ctrls, child_names):
rig.pose.bones[childname].rotation_mode = ctrl.rotation_mode
context.view_layer.update()
setup_twist_helpers(mch_bones, selected, ctrls)
#constrain the root bone
root_ctrl, root_org = add_root_to_bake(self, context, rig, root_name, ctrls, selected)
action = create_anim_data_action(rig, self.org_obj)
if action is not None:
smartbake_to_ctrls(context, self, selected, ctrls, distance = True)
constraints_clear(ctrls)
include_collections(included_collections, hidden_objs, self.temp_col, objs)
#Constrains the selected bones back to the controls
if not self.con_back:
del ctrls, child_names, selected, root_name, rig, setup_ids
return {'FINISHED'}
#if it's using root ctrl reference then remove them from ctrls and selected
if root_org:
ctrls.remove(root_ctrl)
selected.remove(root_org)
constraint_org_to_ctrl(self, ctrls, selected)
rig.select_set(True)
context.view_layer.objects.active = rig
rig.data.bones.active = rig.data.bones[ctrl.name]
if root_name:
if root_name in rig.data.bones:
rig.data.bones[root_name].select = False
custom_range_switch(context.scene, action, rig)
if hasattr(Tools, 'bone_selection'):
#Reset selection order in order to use context.selected_pose_bone[-1].bone
Tools.bone_selection = list(context.selected_pose_bones)
Tools.obj_selection = list(context.selected_objects)
# print('\nseconds\n', time.time() - sec)
del ctrls, child_names, selected, root_name, rig, setup_ids
return {'FINISHED'}
def cancel(self, context):
cancel_tempctrls(context)
global ctrls, child_names, selected, root_name, rig, setup_ids
del ctrls, child_names, selected, root_name, rig, setup_ids
return None
def cancel_tempctrls(context):
global rig, setup_ids, root_name, selected
#remove control rigs and empties
controllers, controlled_objs = get_controllers_controlled(context)
remove_selected(context, setup_ids, controllers)
remove_org_bone_id(selected, setup_ids)
# remove_ctrls(context)
remove_collection()
switch_to_original_obj(context, controlled_objs)
'''
def ToggleSwitch(context, toggle):
scene = context.scene
controlled_objs = set([item.controlled for item in scene.btc.ctrl_items if item.controlled])
controllers = set([item.controller for item in scene.btc.ctrl_items if item.controller])
ids = get_bone_ids(context)
for obj in controlled_objs:
for bone in obj.pose.bones:
if bone.btc.setup_id not in ids:
continue
for con in bone.constraints:
if not hasattr(con, 'target'):
continue
if con.target not in controllers:
continue
con.enabled = toggle
'''
def custom_range_switch(scene, action, rig):
'''Adding on and off keyframes to start and end of Custom Frame Range'''
if scene.btc.bake_range_type != 'CUSTOM':
return
frame_start, frame_end = scene.btc.bake_range
fcurves = Tools.get_fcurves_channelbag(rig, action)
fcu = fcurves.find(data_path = 'animtoolbox.ctrls_enabled', index = 0)
if fcu is None:
fcu = fcurves.new('animtoolbox.ctrls_enabled')
fcu.keyframe_points.add(4)
fcu.keyframe_points[-1].co = (frame_start-1, 0)
fcu.keyframe_points[-2].co = (frame_start, 1)
fcu.keyframe_points[-3].co = (frame_end, 1)
fcu.keyframe_points[-4].co = (frame_end+1, 0)
fcu.update()
def empty_child_prop(self, context):
'''Add or remove EMPTY child ctrls using the property in the ui panel'''
global ctrls, child_ctrls
if self.child:
for ctrl in ctrls:
child_empty = add_empty(context, ctrl, 'Child_')
child_empty.parent = ctrl
child_empty.matrix_parent_inverse = ctrl.matrix_world.inverted()
child_empty.empty_display_size = ctrl.empty_display_size * 0.5
child_empty.animtoolbox.setup_id = ctrl.animtoolbox.setup_id
child_empty.animtoolbox.child = True
#define controller and controlled
child_empty.animtoolbox.controlled = ctrl.animtoolbox.controlled
register_ctrl(child_empty, ctrl.animtoolbox.controlled)
child_ctrls.append(child_empty)
elif child_ctrls:
for child in child_ctrls:
remove_empty_item(context, child)
child_ctrls = []
def empty_root_update(self, context):
global root_ctrl
if not self.root_object:
#get the cursor position also in pose mode
root_ctrl.matrix_world = Matrix.Identity(4)
root_ctrl.matrix_world.translation = context.scene.cursor.matrix.translation
return
if self.root_bone in self.root_object.data.bones:
#get the bone ref position
ref_bone = self.root_object.pose.bones[self.root_bone]
root_ctrl.matrix_world = ref_bone.matrix
ref_bone.btc.setup_id = root_ctrl.animtoolbox.setup_id
context.view_layer.update()
else:
#get the object position
root_ctrl.matrix_world = self.root_object.matrix_world
def empty_root_prop(self, context):
'''add or remove root ctrl for worldspace empties'''
global ctrls, root_ctrl
setup = 'EMPTY'
btc = context.scene.btc
obj = context.object
root = None
if btc.root:
if btc.root_object:
if btc.root_object.type == 'ARMATURE' and btc.root_bone:
root = btc.root_object.pose.bones[btc.root_bone]
else:
root = btc.root_object
root_ctrl = add_empty(context, root, nameextension = '')
root_ctrl.empty_display_size *= 2
root_ctrl.animtoolbox.controlled = obj
root_ctrl.animtoolbox.root = True
register_ctrl(root_ctrl, obj)
if btc.root_bone:
root_ctrl.select_set(True)
setup_id = add_id(root_ctrl, setup)
setup_ids.add(setup_id)
root_ctrl.animtoolbox.setup_id = setup_id
if root:
if type(root) == bpy.types.PoseBone:
root.btc.setup_id = setup_id
root.btc.setup = setup
elif root_ctrl:
remove_empty_item(context, root_ctrl)
root_ctrl = None
def target_empty_position(self, context):
global ctrls, child_ctrls, selected
for ctrl, bone in zip(ctrls, selected):
matrix = get_bone_matrix(bone)
ctrl.matrix_world = matrix
if self.bake_loc != 'TRACK':
continue
dir = -1 if self.axis[0] == '-' else 1
axis = self.axis[1]
axes = {'X' : 0, 'Y' : 1, 'Z' :2}
value = [0, 0, 0]
value[axes[axis]] = bone.length * self.offset * dir
local_translation = mathutils.Vector(value)
bone_local_translation = bone.matrix.to_3x3() @ local_translation
ctrl.matrix_world.translation = bone.matrix.translation + bone_local_translation
def remove_empty_item(context, empty):
'''remove empty from objects and from ctrl items'''
btc = context.scene.btc
empty_index = next((i for i, item in enumerate(btc.ctrl_items) if empty == item.controller), None)
if empty_index != None:
btc.ctrl_items.remove(empty_index)
bpy.data.objects.remove(empty)
class BakeToEmpties(bpy.types.Operator, WorldSpaceProperties):
"""Copy and bake the animation to temporary controls"""
bl_idname = "anim.bake_to_empties"
bl_label = "Bake_To_Empties"
bl_options = {'REGISTER', 'UNDO'}
bake_loc: bpy.props.EnumProperty(name = 'Type', description="Select what type of constraints will be used", update = target_empty_position,
items = [('COPY', 'Copy Transforms','Bake to the bones heads', 0), ('TRACK', 'Track Target', 'Track the bone to an axis', 1)])
axis: bpy.props.EnumProperty(name = 'Axis', description="Which direction of the original bone the target should point", update = target_empty_position, default = 1,
items = [('+X', '+X','Target Bone the +X Axis', 0), ('+Y', '+Y', 'Target Bone the +Y Axis', 1), ('+Z', '+Z', 'Target Bone the +Z Axis', 2),
('-X', '-X','Target Bone the -X Axis', 3), ('-Y', '-Y', 'Target Bone the -Y Axis', 4), ('-Z', '-Z', 'Target Bone the -Z Axis', 5)])
offset: bpy.props.FloatProperty(name="Offset", description="Offset the bone in the axis direction", default=1.0, update = target_empty_position)
child: bpy.props.BoolProperty(name = 'Extra Child', description = "Add an child control for an overlay control", default = False, update = empty_child_prop)
@classmethod
def poll(cls, context):
return len(context.selected_objects) #and context.object.mode == 'POSE'
def draw(self, context):
layout = self.layout
row = layout.row()
btc = context.scene.btc
row.prop(btc, 'shape_size', text = 'Size')
row.prop(btc, 'shape_type', text = '')
layout.separator()
row = layout.row()
row.label(text = 'Rotation Mode:')
row.prop(self, 'rotation_mode', text = '')
split = layout.split(factor = 0.4)
split.label(text = 'Type: ')
split.prop(self, 'bake_loc', text = '')
if self.bake_loc == 'TRACK':
split = layout.split(factor = 0.4)
split.label(text = 'Axis: ')
split.prop(self, 'axis', text = '')
split.prop(self, 'offset', text = '')
col = layout.column()
col.prop(self, 'twist', text = 'Include Twist')
layout.separator()
col = layout.column()
col.prop(self, 'smartbake')
split = layout.split(factor = 0.9)
split.prop(self, 'con_back')
split.prop(self, 'filter_bool', icon = 'FILTER')
if self.filter_bool:
split = layout.split(factor = 0.65)
split.label(text = "Filter Properties")
split.label(text = "X Y Z")
Tools.filter_draw_ui(self, self, titel = False)
layout.separator()
row = layout.row()
row.label(text = 'Add Hierarchy:')
row = layout.row()
row.prop(btc, 'root', text = 'Root Ctrl')
row.prop(self, 'child')
if btc.root:
row = layout.row()
row.prop(btc, 'root_object', text = '')
if hasattr(btc.root_object, 'type'):
if btc.root_object.type == 'ARMATURE':
row = layout.row()
row.prop_search(btc, 'root_bone', context.scene.btc.root_object.pose, 'bones', text = '')
def invoke(self, context, event):
obj = context.object
self.org_obj = obj
if obj.type != 'ARMATURE':
self.report({'ERROR'},'Currently works only with Armature objects')
return {'CANCELLED'}
if obj.animtoolbox.controlled:
self.report({'ERROR'},'Selected object is already a control rig')
return {'CANCELLED'}
global ctrls, selected, root_ctrl, setup_ids, child_ctrls
context.scene.btc.ctrl_type = 'EMPTY'
selected = context.selected_pose_bones if obj.mode == 'POSE' else context.selected_objects
child_ctrls = []
bake_layers_default(obj, context.scene)
get_org_action(obj, self)
if not selected:
return {'CANCELLED'}
if clean_bone_before_setup(context, selected):
#Clear setup is turned off, then cancelling this operator
self.report({'ERROR'},'Selected bones are already connected to temp ctrls. Clean the current ctrls before proceeding')
return {'CANCELLED'}
if not context.active_object:
#After cleaning the object is not active and set to object mode
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode = 'OBJECT')
setup_ids = set()
ctrls, root_ctrl, setup_ids = add_control_empties(self, context, selected)
self.temp_col = find_temp_ctrl_collection(context.scene, ctrls)
target_empty_position(self, context)
empty_root_prop(self, context)
empty_child_prop(self, context)
# tempctrl_shape_type(self, context)
wm = context.window_manager
#launch the dialogue box
if not self.org_action:
self.orientation = False
# return wm.invoke_props_dialog(self, width = 200)
return wm.invoke_props_dialog(self, width = 200)
def execute(self, context):
global ctrls, child_ctrls, selected, root_ctrl, rig, setup_ids
if 'ctrls' not in globals():
self.report({'INFO'},'Not available. Please update the Temp Ctrls from the addon panel')
return {'CANCELLED'}
objs = {bone.id_data for bone in selected}
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
included_collections = exclude_collections(self.temp_col, context, objs)
con_type = 'COPY_TRANSFORMS'
obj = self.org_obj
for ctrl, bone in zip(ctrls, selected):
# constraints = constraint_ctrl_to_bone(ctrl, bone, self.bake_loc, con_type)
if root_ctrl:
matrix = ctrl.matrix_world.copy()
ctrl.parent = root_ctrl
ctrl.matrix_world = matrix
ctrl.select_set(True)
#apply Rotation Mode
ctrl.rotation_mode = bone.rotation_mode if self.rotation_mode == 'PRESERVE' else self.rotation_mode
#constrain the root bone
if root_ctrl and context.scene.btc.root_bone:
root_ref = obj.pose.bones[context.scene.btc.root_bone]
root_contsraint = constraint_add(root_ctrl, root_ref)
#Assign the root setup to the original bone
root_ref.btc.setup = 'ROOT'
bpy.ops.object.mode_set(mode = 'OBJECT')
obj.select_set(False)
action = None
for ctrl in ctrls:
action = create_anim_data_action(ctrl, self.org_obj, action)
if action:
smartbake_to_ctrls(context, self, selected, ctrls, distance = True)
include_collections(included_collections, hidden_objs, self.temp_col, objs)
if not self.con_back:
del ctrls, selected, root_ctrl, setup_ids, child_ctrls
return {'FINISHED'}
#select which constraints are going to be added
con_type = 'DAMPED_TRACK' if self.bake_loc == 'TRACK' else 'COPY_TRANSFORMS'
#for empty, emptychild, bone in zip(empties, child_empties, selected_bones):
for ctrl, bone in zip(ctrls, selected):
#assigning also the setup
if self.bake_loc == 'TRACK':
ctrl.animtoolbox.setup = bone.btc.setup = 'TRACK_TO_EMPTY'
#switch to the child controling the original bone
if self.child:
ctrl = ctrl.children[0]
if self.bake_loc == 'TRACK' : ctrl.animtoolbox.setup = 'TRACK_TO_EMPTY'
constraints = constraints_filter(self, bone, ctrl, con_type)
for constraint in constraints:
if constraint.type == 'DAMPED_TRACK':
direction = 'NEGATIVE_' if '-' == self.axis[0] else ''
constraint.track_axis = 'TRACK_' + direction + self.axis[1]
add_enabled_driver(bone.id_data, bone.id_data, constraint)
context.view_layer.objects.active = ctrl
del ctrls, selected, root_ctrl, setup_ids, child_ctrls
return {'FINISHED'}
def cancel(self, context):
global ctrls, selected, root_ctrl, setup_ids, child_ctrls
btc = context.scene.btc
controllers = [item.controller for item in btc.ctrl_items]
for ctrl in ctrls:
btc.ctrl_items.remove(controllers.index(ctrl))
bpy.data.objects.remove(ctrl)
if child_ctrls:
for child in child_ctrls:
bpy.data.objects.remove(child)
#remove all the setup ids
remove_org_bone_id(self.org_obj.pose.bones, setup_ids)
if root_ctrl:
remove_empty_item(context, root_ctrl)
remove_collection()
del ctrls, selected, root_ctrl, setup_ids, child_ctrls
return None
def remove_org_bone_id(bones, setup_ids):
'''removing the ids from the original bones'''
for bone in bones:
if bone.btc.setup_id not in setup_ids:
continue
remove_id(bone.id_data, bone.btc.setup_id)
bone.btc.setup_id = 0
bone.btc.org_id = 0
bone.btc.setup = bone.btc.org = 'NONE'
class AddEmpties(bpy.types.Operator):
"""Add Empties in the location of the selected bones"""
bl_idname = "anim.empties_to_bones"
bl_label = "Add_Empties"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
obj = context.object
if obj.type != 'ARMATURE':
self.report({'ERROR'},'Currently works only with Armature objects')
return {'CANCELLED'}
context.scene.btc.ctrl_type = 'EMPTY'
selected = context.selected_pose_bones if obj.mode == 'POSE' else context.selected_objects
if not selected:
return {'CANCELLED'}
add_control_empties(self, context, selected)
return {'FINISHED'}
def add_bone_to_collection(rig, bone, col_name = 'MCH', visible = False):
'''Add bone to a collection'''
if bpy.app.version < (4, 0, 0):
return
collections = rig.data.collections
if col_name in collections:
col = rig.data.collections[col_name]
else:
col = rig.data.collections.new(col_name)
col.assign(bone)
col.is_visible = visible
def switch_layers(rig, bone, layer_indexes):
'''switch bone layers'''
if bpy.app.version >= (4, 0, 0):
return
bone.layers = [True if i in layer_indexes else False for i in range(32)]
def get_children(root, selected_bones):
'''Get all the childrens within a selection chain'''
children = []
for child in root.children_recursive:
if child not in selected_bones:
continue
parents = set(child.parent_recursive[:child.parent_index(root)])
if not parents.issubset(selected_bones):
continue
children.append(child)
return set(children)
def get_hierarchies(selected_bones):
'''Get the all the hierarchial chains from all the selected bones'''
# selected_bones = set(bpy.context.selected_pose_bones)
selected_bones = set(selected_bones)
root_bones = [bone for bone in selected_bones if bone.parent not in selected_bones]
hierarchies = []
for root in root_bones:
end_chains = []
#check for separate selections with children recursive
children = get_children(root, selected_bones)
#if there is no end chains, in case of just one bone then append the root as a chain
if not children:
hierarchies.append([root])
continue
#find the end chain from all the childrens
for child in children:
grandchildren = get_children(child, selected_bones)
#check if the children of the children are still part of the hierarchy or the end of the chain
if len(grandchildren) and grandchildren.issubset(children):
continue
end_chains.append(child)
end_chains = set(end_chains)
#start from the end of the chain and go up until the root
for end_chain in end_chains:
chain = [end_chain]
bone = end_chain
while bone.parent in selected_bones:
chain.append(bone.parent)
bone = bone.parent
#make sure the last parent bone is the root bone of the chain
if chain[-1] == root:
hierarchies.append(chain)
return hierarchies
#### Pole Angle Functions ####
def signed_angle(vector_u, vector_v, normal):
# Normal specifies orientation
if vector_v == mathutils.Vector((0, 0, 0)):
#put some random number, because 0 will give an error for the angle
vector_v = mathutils.Vector((0, -2, 0))
angle = vector_u.angle(vector_v)
if vector_u.cross(vector_v).angle(normal) < 1:
angle = -angle
return angle
def get_pole_angle(base_bone, ik_bone, pole_location):
pole_normal = (ik_bone.tail - base_bone.head).cross(pole_location - base_bone.head)
projected_pole_axis = pole_normal.cross(base_bone.tail - base_bone.head)
if projected_pole_axis == mathutils.Vector((0,0,0)):
return None
return signed_angle(base_bone.x_axis, projected_pole_axis, base_bone.tail - base_bone.head)
def create_anim_data_action(rig, obj, action = None):
#creating and adding the action
if obj.animation_data is None:
return
if rig.animation_data is not None:
if rig.animation_data.action is not None:
return rig.animation_data.action
name = obj.name
actions = set()
if obj.animation_data.action:
name = obj.animation_data.action.name
actions.add(obj.animation_data.action)
get_bone_layers_actions(obj, actions)
if not len(actions):
return None
anim_data = rig.animation_data_create() if rig.animation_data is None else rig.animation_data
#Check if we are using the same action with a new slot or a new action
if action is None or not hasattr(anim_data, 'action_slot'):
action = bpy.data.actions.new(name = 'Temp_Ctrls_' + name)
anim_data.action = action
slot = Tools.add_slot_to_animdata(anim_data)
if slot:
layer = action.layers.new('Temp_Ctrls') if not len(action.layers) else action.layers[0]
strip = layer.strips.new() if not len(layer.strips) else layer.strips[0]
strip.channelbags.new(slot)
return action
def constraints_clear(ctrls):
#remove constraints from the temp controls
for ctrl in ctrls:
for con in ctrl.constraints:
ctrl.constraints.remove(con)
def find_pole_vector(base_bone, ik_tip, pole_bone):
# Get the vector bisecting each FK control (object space)
PV_normal = ((ik_tip.vector.normalized() + base_bone.vector.normalized() * -1)).normalized()
offset = bpy.context.scene.btc.pole_offset
# We push the pole control in the opposite direction of the FK bisecting vector (object space)
PV_matrix_loc = ik_tip.matrix.to_translation() + (PV_normal * -1) * offset
PV_matrix = Matrix.LocRotScale(PV_matrix_loc, pole_bone.matrix.to_quaternion(), None)
return PV_matrix
def pole_prop_edit(self, context):
global rig, ik_chains, child_names, ctrls
btc = context.scene.btc
for chain in ik_chains:
#add pole target only for more then 2 bones
if btc.pole_target:
add_pole_bone(self, context, rig, chain)
else:
add_bone_to_collection(rig, rig.data.edit_bones[chain.base_bone.name], col_name = 'IK Ctrls', visible = True)
#remove the pole bone and switch to base bone
if not hasattr(chain, 'pole_bone'):
continue
rig.data.edit_bones.remove(rig.data.edit_bones[chain.pole_bone_name])
#remove from ctrls and append base bone instead
if chain.pole_bone in ctrls:
ctrls.remove(chain.pole_bone)
ctrls.append(chain.base_bone)
del chain.pole_bone, chain.pole_bone_name
#Remove child ctrls
if btc.child:
child_names = remove_child_ctrls(rig, child_names)
def pole_prop_pose(self, context):
global ik_chains, ctrls, child_names
btc = context.scene.btc
for chain in ik_chains:
if not btc.pole_target:
chain.base_bone.btc.org = 'CTRL'
if not hasattr(chain, 'pole_bone'):
continue
del chain.pole_bone
if hasattr(chain,'pole_bone_name'):
chain.pole_bone_name
continue
pole_posebone_update(chain, ctrls)
if chain.base_bone in ctrls:
ctrls.remove(chain.base_bone)
def pole_prop(self, context):
global rig, ik_chains, ctrls, child_names
bpy.ops.object.mode_set(mode = 'EDIT')
pole_prop_edit(self, context)
bpy.ops.object.mode_set(mode = 'POSE')
pole_prop_pose(self, context)
if self.child:
child_prop(self, context)
tempctrl_shape_type(context.scene.btc, context)
# update_bone_color(context.scene.btc, self)
def pole_posebone_update(chain, ctrls):
'''update pole bone properties in the pole prop and add ik prop'''
if not hasattr(chain, 'pole_bone_name'):
return
chain.pole_bone = rig.pose.bones[chain.pole_bone_name]
add_bone_setup_id(chain.pole_bone , False, 'POLE', 'CTRL', setup_id = chain.setup_id, org_id = add_id(rig, 'CTRL', True))
chain.pole_bone.matrix = find_pole_vector(chain.base_bone, chain.ik_tip, chain.pole_bone)
# chain.pole_bone.scale = chain.ik_ctrl.scale
chain.pole_bone.color.palette = bpy.context.scene.btc.color_set
if chain.pole_bone not in ctrls:
ctrls.append(chain.pole_bone)
def add_pole_bone(self, context, rig, chain):
'''Add the pole bone if it's possible'''
btc = context.scene.btc
base_bone = rig.data.edit_bones[chain.base_bone_name]
ik_tip = rig.data.edit_bones[chain.ik_tip_name]
#If the angle between the bones is 0 then move a bit in the Y direction to be able to get a pole vector
if not base_bone.vector.angle(ik_tip.vector):
base_bone.tail += mathutils.Vector((0, 0.001, 0))
ik_tip.head += mathutils.Vector((0, 0.001, 0))
pole_matrix = find_pole_vector(base_bone, ik_tip, ik_tip)
#pole matrix orientation to world by default
loc, rot, scale = pole_matrix.decompose()
new_rot = mathutils.Euler((0, 0, 0), 'XYZ')
pole_matrix = Matrix.LocRotScale(loc, new_rot, scale)
#In case of a flat IK skip adding a pole bone and use base bone instead
if pole_matrix.translation == chain.ik_tip.matrix.translation:
self.report({'ERROR'}, "Can't add Pole Ctrl because of a flat IK chain")
chain.pole_bone = None
add_bone_to_collection(rig, chain.base_bone, col_name = 'IK Ctrls', visible = True)
btc['pole_target'] = False
return
pole_bone_name = chain.org_bones[-2].name + '_Pole' if len(chain.org_bones) > 1 else chain.org_bones[0].name + '_Pole'
chain.pole_bone = rig.data.edit_bones.new(pole_bone_name)
#get always the original length when it's using ik_ctrl posebone or editbone
length = chain.ik_ctrl.bone.length if hasattr(chain.ik_ctrl, 'bone') else chain.ik_ctrl.length
chain.pole_bone.length = length
chain.pole_bone.matrix = pole_matrix
chain.pole_angle = get_pole_angle(base_bone, ik_tip, pole_matrix.translation)
chain.pole_bone_name = chain.pole_bone.name
chain.pole_bone.select = True
switch_layers(rig, base_bone, [31])
add_bone_to_collection(rig, base_bone)
rig.data.collections['IK Ctrls'].unassign(base_bone)
add_bone_to_collection(rig, chain.pole_bone, col_name = 'IK Ctrls', visible = True)
def pole_offset(self, context):
global ik_chains, ctrls, child_names
for chain in ik_chains:
if not hasattr(chain, 'pole_bone_name'):
continue
chain.pole_bone = rig.pose.bones[chain.pole_bone_name]
chain.pole_bone.matrix = find_pole_vector(chain.base_bone, chain.ik_tip, chain.pole_bone)
# chain.pole_bone.scale = chain.ik_ctrl.scale
def add_ik_prop_edit(self, context):
global rig, ik_chains, ctrls, child_names, root_name
btc = context.scene.btc
root_ctrl = None
if root_name:
if root_name in rig.data.edit_bones:
root_ctrl = rig.data.edit_bones[root_name]
for chain in ik_chains:
if btc.add_ik_ctrl:
#Add an extra ik ctrl bone to the tip of the chain
chain.ik_tip = chain.ctrls[0]
chain.ik_tip_name = chain.ik_tip.name
ik_tip_edit = rig.data.edit_bones[chain.ik_tip_name]
chain.ik_ctrl = rig.data.edit_bones.new(chain.org_bones[0].name + '_IK_Ctrl')
chain.ik_ctrl.head = ik_tip_edit.tail
chain.ik_ctrl.length = ik_tip_edit.length * 0.5
chain.ik_ctrl.align_orientation(ik_tip_edit)
chain.ik_ctrl.parent = root_ctrl
chain.ik_ctrl.select = True
chain.ik_ctrl_name = chain.ik_ctrl.name
else:
#remove the extra ik control
if chain.ik_ctrl not in chain.ctrls:
ctrls.remove(chain.ik_ctrl)
rig.data.edit_bones.remove(rig.data.edit_bones[chain.ik_ctrl_name])
chain.ik_ctrl = chain.ctrls[0]
chain.ik_tip = chain.ctrls[1] if len(chain.ctrls) > 1 else chain.ctrls[0]
ctrls.append(chain.ik_ctrl)
chain.ik_ctrl_name = chain.ik_ctrl.name
chain.ik_tip_name = chain.ik_tip.name
#remove the pole bone
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
#removing and adding the pole_target without calling the whole pole prop update
ctrls.remove(chain.pole_bone)
rig.data.edit_bones.remove(rig.data.edit_bones[chain.pole_bone_name])
add_pole_bone(self, context, rig, chain)
def add_ik_prop_pose(self, context):
btc = context.scene.btc
for chain in ik_chains:
if btc.add_ik_ctrl:
#assign the ik ctrl to the ctrls
chain.ik_ctrl = rig.pose.bones[chain.ik_ctrl_name]
if chain.ik_ctrl not in ctrls:
ctrls.append(chain.ik_ctrl)
if chain.ik_tip in ctrls:
ctrls.remove(chain.ik_tip)
add_bone_setup_id(chain.ik_ctrl, False, 'TEMPIK', 'CTRL', setup_id = chain.ik_tip.btc.setup_id, org_id = chain.ik_tip.btc.org_id)
rig.data.collections['IK Ctrls'].unassign(chain.ik_tip)
add_bone_to_collection(rig, chain.ik_tip)
chain.ik_tip.btc.org = 'ORG'
chain.ik_ctrl.btc.org = 'CTRL'
chain.ik_tip.custom_shape = None
chain.ik_tip.color.palette = 'DEFAULT'
chain.ik_ctrl.matrix = chain.ik_tip.matrix
context.view_layer.update()
chain.ik_ctrl.matrix.translation = chain.ik_tip.tail
else:
chain.ik_ctrl.btc.org = 'CTRL'
chain.ik_ctrl.color.palette = context.scene.btc.color_set
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
pole_posebone_update(chain, ctrls)
add_bone_to_collection(rig, chain.ik_ctrl, col_name = 'IK Ctrls', visible = True)
def add_ik_prop(self, context):
'''add an extra ik bone, normally for chains with only 2 bones selected'''
global rig, ik_chains, ctrls, child_names
bpy.ops.object.mode_set(mode = 'EDIT')
add_ik_prop_edit(self, context)
#Remove child ctrls
if self.child:
child_names = remove_child_ctrls(rig, child_names)
bpy.ops.object.mode_set(mode = 'POSE')
add_ik_prop_pose(self, context)
if self.child:
child_prop(self, context)
tempctrl_shape_type(context.scene.btc, context)
# update_bone_color(context.scene.btc, self)
class TempIK(bpy.types.Operator):
"""Copy and bake the animation to temporary controls"""
bl_idname = "anim.bake_to_temp_ik"
bl_label = "Bake_To_Temp_IK"
bl_options = {'REGISTER', 'UNDO'}
# frame_start: bpy.props.IntProperty(name = 'Start', description="The start frame for the bake")
# frame_end: bpy.props.IntProperty(name = 'End', description="The end frame for the bake")
bake_loc: bpy.props.EnumProperty(name = 'Bake Location', description="Select which point on the bone to bake to", items = [('HEAD', 'Heads','Bake to the bones heads', 0), ('TAIL', 'Tails', 'Bake to the bones tails', 1)])
con_back: bpy.props.BoolProperty(name = 'Constraint bones to Ctrls', description = "Constraint the selected bones back to the controls", default = True)
use_hierarchies: bpy.props.BoolProperty(name = 'Keep Separate Hierarchies', description = "Use all the different hierarchies included in the selection", default = False)
smartbake: bpy.props.BoolProperty(name = 'Smart Bake', description = "Keeps keyframes count from selection and recalculate interpolation", default = False)
rotation_mode: bpy.props.EnumProperty(name = 'Rotation Mode', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
items = [('PRESERVE', 'Preserve','Keep Rotation Order from the original selected bones', 0),
('QUATERNION', 'Quaternion','Quaternion Rotation Order - No Gimbal Lock', 1),
('XYZ', 'XYZ', 'XYZ Rotation Order', 2), ('XZY', 'XZY','XZY Rotation Order', 3),
('YXZ', 'YXZ','YXZ Rotation Order', 4), ('YZX', 'YZX', 'YZX Rotation Order', 5),
('ZXY', 'ZXY', 'ZXY Rotation Order', 6), ('ZYX', 'ZYX', 'ZYX Rotation Order', 7),
('AXIS_ANGLE', 'AXIS_ANGLE', 'Axis Angle Rotation Order', 8)])
@classmethod
def poll(cls, context):
return len(context.selected_objects) and context.object.mode == 'POSE'
def invoke(self, context, event):
# sec = time.time()
obj = context.object
if obj.type != 'ARMATURE':
self.report({'ERROR'},'Currently works only with Armature objects')
return {'CANCELLED'}
if obj.animtoolbox.controlled:
self.report({'ERROR'},'Selected object is already a control rig')
return {'CANCELLED'}
global ik_chains, child_names, ctrls, root_name, rig, setup_ids, selected
context.scene.btc.ctrl_type = 'BONE'
selected = context.selected_pose_bones if obj.mode == 'POSE' else []
if not selected:
return {'CANCELLED'}
if clean_bone_before_setup(context, selected):
#Clear setup is turned off, then cancelling this operator
self.report({'ERROR'},'Selected bones are already connected to temp ctrls. Clean the current ctrls before proceeding')
return {'CANCELLED'}
btc = context.scene.btc
#if there are no display shapes then use the cube as the default
if not any([bone.custom_shape for bone in selected]):
# btc.shape_type = 'CUBE'
btc['shape_type'] = 1
self.hierarchies = get_hierarchies(selected)
#check if there is multiple hierarchies that are also longer then one bone
self.use_hierarchies = any(len(hierarchy) > 1 for hierarchy in self.hierarchies) and len(self.hierarchies) > 1 and len(selected) > 3
# If less then two bones add ik ctrl checked by default
if self.use_hierarchies:
add_ik_ctrl = any(len(hierarchy) < 3 for hierarchy in self.hierarchies)
else:
add_ik_ctrl = len(selected) < 3
bake_layers_default(obj, context.scene)
########################
get_org_action(obj, self)
self.org_obj = obj
self.shape_type = btc.shape_type
self.shape_size = btc.shape_size
root_name = None
root_ctrl = None
#add separate hierachies within the selection or one entire hierarchy from the selection
hierarchies = self.hierarchies if self.use_hierarchies else [list(reversed(selected))]
ik_chains = []
rig, self.temp_col = add_ctrl_rig(selected)
bpy.ops.object.mode_set(mode = 'EDIT')
for hierarchy in hierarchies:
ctrls, child_ctrls = add_control_bones(self, hierarchy, rig, root_ctrl) #, orient_world = self.orientation
child_names = []
# for ctrl in ctrls:
# add_bone_to_collection(rig, ctrl, col_name = 'ORG', visible = True)
chain = IK_Chain(hierarchy, ctrls, root_ctrl)
#if only one bone is selected in the chain then add an extra control automatically
if len(hierarchy) == 1:
add_ik_ctrl = True
#setup t tail to head in edit mode
for i, ctrl in enumerate(ctrls[:-1]):
ctrls[i+1].tail = ctrl.head
context.view_layer.update()
switch_layers(rig, chain.ik_tip, [31])
add_bone_to_collection(rig, chain.ik_tip)
add_bone_to_collection(rig, chain.ik_ctrl, col_name = 'IK Ctrls', visible = True)
# if bpy.app.version >= (4, 0, 0):
# rig.data.collections['ORG'].is_visible = False
ik_chains.append(chain)
btc['add_ik_ctrl'] = add_ik_ctrl
if add_ik_ctrl:
add_ik_prop_edit(self, context)
pole_prop_edit(self, context)
#updating the ctrls (with either base bone or pole)before adding the children
ctrl_names = []
for chain in ik_chains:
ctrl_names += [chain.ik_ctrl.name, chain.base_bone.name] if not btc.pole_target else [chain.ik_ctrl.name, chain.pole_bone.name]
# ctrl_names = [ctrl.name for ctrl in ctrls]
if btc.root:
root_name = root_update_edit()
##################################
bpy.ops.object.mode_set(mode = 'POSE')
setup_ids = set()
if btc.root:
root_update_pose(btc, context)
#converting ctrls to posebones
ctrls = [rig.pose.bones[ctrlname] for ctrlname in ctrl_names]
for chain in ik_chains:
if bpy.app.version < (4, 0, 0) : ctrl_group = add_ctrl_group(rig)
setup_id = add_id(rig, 'TEMPIK')
setup = 'TEMPIK'
chain.setup_id = setup_id
# org_id = add_id(rig, 'TEMPIK')
ctrl_group = add_ctrl_group(rig) if bpy.app.version < (4, 0, 0) else None
chain.ctrl_group = ctrl_group
chain.bonenames_to_posebones(rig)
context.view_layer.update()
#setup the IDs and current frame matrix
org_id = add_id(rig, 'TEMPIK', True)
for ctrl, bone in zip(reversed(chain.ctrls), reversed(chain.org_bones)):
#Assign ID and setup to the ctrls
add_bone_setup_id(ctrl, False, setup, 'ORG', setup_id, org_id)
add_bone_setup_id(bone, False, 'NONE','NONE', setup_id, org_id)
# #assign the matrix
ctrl.matrix = bone.id_data.matrix_world @ bone.matrix
ctrl.color.palette = btc.color_set
add_bone_setup_id(chain.ik_ctrl, False, setup, 'CTRL', setup_id, org_id = chain.ik_tip.btc.org_id)
if add_ik_ctrl:
add_ik_prop_pose(self, context)
context.view_layer.update()
pole_prop_pose(self, context)
# child_prop_pose(context)
if context.scene.btc.child:
child_prop(context.scene.btc, context)
self.ik_chains = ik_chains
tempctrl_shape_type(self, context)
wm = context.window_manager
# print('\nseconds\n', time.time() - sec)
return wm.invoke_props_dialog(self, width = 200)
def execute(self, context):
# obj = context.object
global ik_chains, child_names, ctrls, root_name, rig, setup_ids, selected
if 'ctrls' not in globals():
self.report({'INFO'},'Not available. Please update the Temp Ctrls from the addon panel')
return {'CANCELLED'}
btc = context.scene.btc
controlled_objs = {item.controlled for item in context.scene.btc.ctrl_items if item.controlled}
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
included_collections = exclude_collections(self.temp_col, context, controlled_objs)
bpy.ops.object.mode_set(mode = 'EDIT')
#Add root control to all ctrls and parent bones
bones_to_root = ctrls + [chain.parent for chain in ik_chains if hasattr(chain, 'parent')]
root_ctrl = parent_ctrls_to_root(root_name, rig, bones_to_root)
child_names_to_orient = None
if child_names:
#creating ctrl child relation dictionary
ctrls_childs = {ctrl.name : childname for ctrl, childname in zip(ctrls, child_names)}
#Only the IK Ctrl gets the World orientation
#Exclude base bone from orientation, pole bone is anyway always world orientation
child_names_to_orient = [ctrls_childs[chain.ik_ctrl.name] for chain in ik_chains]
#update to world orientation if necessery
ctrls_to_orient = [chain.ik_ctrl for chain in ik_chains]
ctrls_orientation(btc, ctrls_to_orient, child_names_to_orient)
#Create the IK chain parenting
for chain in ik_chains:
if chain.org_bones[-1].parent:
chain_parent = add_ctrl_bone(rig, chain.org_bones[-1].parent, root_ctrl, nameextension = '')
chain.parent = chain_parent
chain.parent_name = chain.parent.name
#parent the base bone to the chain parent
rig.data.edit_bones[chain.base_bone_name].parent = chain.parent
switch_layers(rig, chain.parent, [31])
add_bone_to_collection(rig, chain.parent)
else:
chain.parent = None
#adding the original bones again as reference bones in case they are not orientated in IK setup direction
chain.ref_org_names = []
for ctrl, org in zip(chain.ctrls, chain.org_bones):
#Adding the reference bones that are aligned with org bones and parenting them to the ctrl bones of the IK
if child_names and ctrl.name in ctrls_childs:
parent_org = rig.data.edit_bones[ctrls_childs[ctrl.name]]
else:
parent_org = rig.data.edit_bones[ctrl.name]
ref_org = add_ctrl_bone(rig, org, parent_org , 'ORG_', 'HEAD', orient_world = False) #rig.data.edit_bones[ctrl.name]
add_bone_to_collection(rig, ref_org, col_name = 'ORG', visible = False)
chain.ref_org_names.append(ref_org.name)
for i, ctrl in enumerate(chain.ctrls[:-1]):
#skip parenting for the first ik control bone
if ctrl == chain.base_bone and hasattr(chain, 'parent'):
rig.data.edit_bones[ctrl.name].parent = rig.data.edit_bones[chain.ctrls[i+1].name]
continue
if ctrl == chain.ik_ctrl:
continue
#get the parent from the next bone but in edit bones
rig.data.edit_bones[ctrl.name].parent = rig.data.edit_bones[chain.ctrls[i+1].name]
#updating the polebone position with the offset
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
pole_bone = rig.data.edit_bones[chain.pole_bone_name]
base_bone = rig.data.edit_bones[chain.base_bone_name]
ik_tip = rig.data.edit_bones[chain.ik_tip_name]
pole_bone.matrix = find_pole_vector(base_bone, ik_tip, pole_bone)
if child_names:
pole_child = rig.data.edit_bones[ctrls_childs[pole_bone.name]]
#somehow assigning the matrix here didnt work so I use head and tail
pole_length = pole_child.tail - pole_child.head
pole_child.head = pole_bone.head
pole_child.tail = pole_bone.head + pole_length
# ik_chains = self.ik_chains
constraints = []
################################## Switching to Pose Mode ##################################
con_type = 'COPY_TRANSFORMS'
bpy.ops.object.mode_set(mode = 'POSE')
#Iterate over all the ctrl bones in pose mode
for chain in ik_chains:
chain.bonenames_to_posebones(rig)
chain.ctrls_to_bake = [chain.ik_ctrl, chain.base_bone] if not btc.pole_target else [chain.ik_ctrl, chain.pole_bone, chain.base_bone]
setup_id = chain.ik_ctrl.btc.setup_id
#Org ID is not very important and relevant for Temp IK
org_id = chain.ik_ctrl.btc.org_id
#assigning the bone setup
if chain.parent:
org_obj = chain.ik_ctrl.id_data
org_id = add_id(rig, 'TEMPIK', True)
add_bone_setup_id(chain.parent, False, 'TEMPIK', 'MCH', setup_id, org_id)
chain.parent.matrix = org_obj.matrix_world @ chain.org_bones[-1].parent.matrix
if child_names:
chain.assign_child_ctrls(rig, ctrls_childs)
#constrain all the ctrls to the original bones
for ctrl, bone in zip(reversed(chain.ctrls), reversed(chain.org_bones)):
#reset all bones pose position
ctrl.location.zero()
ctrl.rotation_euler.zero()
ctrl.rotation_quaternion.identity()
ctrl.scale = bone.scale
#Rotation mode works only in Pose mode
ctrl.rotation_mode = bone.rotation_mode if self.rotation_mode == 'PRESERVE' else self.rotation_mode
#switch to the child controling the original bone
if btc.child and ctrl == chain.ik_ctrl:
chain.ik_child_ctrl.rotation_mode = ctrl.rotation_mode
constraint_add(ctrl, bone, con_type)
if ctrl.btc.setup == 'POLE':
continue
# #constraint custom_shape_transform to original rig bone
if bone.custom_shape_transform and self.shape_type == 'ORIGINAL':
ot_bone = bone.custom_shape_transform.name
ctrl.custom_shape_transform = ctrl.id_data.pose.bones[ot_bone]
#Get the original override transform bone
target = bone.id_data.pose.bones[ot_bone]
ctrl.custom_shape_transform.btc.setup_id = ctrl.btc.setup_id
constraint_add(ctrl.custom_shape_transform, target, con_type)
#if an ik control was added, constrain it and include in the chain class
if btc.add_ik_ctrl:
constraint_ctrl_to_bone(chain.ik_ctrl, chain.ctrls[0], 'TAIL', con_type, constraints)
if chain.parent:
constraint_ctrl_to_bone(chain.parent, chain.org_bones[-1].parent, self.bake_loc, con_type, constraints)
root_to_bake = False
if root_name:
root_ctrl = rig.pose.bones[root_name]
#if the root is constrained then add it to the bake
root_to_bake = constrain_root(context, rig, self.org_obj, root_ctrl)
action = create_anim_data_action(rig, self.org_obj)
if root_to_bake:
org_root = btc.root_object.pose.bones[btc.root_bone]
if action is not None:
smartbake_to_ctrls(context, self, [org_root], [root_ctrl])
constraints_clear([root_ctrl])
for chain in ik_chains:
if action is not None:
smartbake_to_ik(context, selected, chain, self.smartbake)
#remove the constraints from the temp ctrls
constraints_clear(set(chain.ctrls_to_bake + chain.ctrls))
custom_range_switch(context.scene, action, rig)
# constraining the original bones to the temp ik control bones
for chain in ik_chains:
#add the ik constraint to the ctrls
ik_con = constraint_add(chain.ik_tip, chain.ik_ctrl if not btc.child else chain.ik_child_ctrl, 'IK')
ik_con.chain_count = chain.length(btc.add_ik_ctrl)
#setup the ik constraint
if btc.pole_target and len(chain.ctrls) > 1:
ik_con.pole_target = rig
ik_con.pole_subtarget = chain.pole_bone_name if not btc.child else chain.pole_child_ctrl_name
if chain.pole_angle:
ik_con.pole_angle = chain.pole_angle
#Need to refresh evaluation so that the ref org bones match the org bones
context.view_layer.update()
for ctrl, bone, ref_org in zip(chain.ctrls, chain.org_bones, chain.ref_orgs):
#Aligning the reference bones to the original bones and assigning them to the chain id
ref_org.matrix = bone.id_data.matrix_world @ bone.matrix.copy()
add_bone_setup_id(ref_org, False, 'TEMPIK', 'ORG', bone.btc.setup_id, bone.btc.org_id)
#switch to the child controling the original bone
if btc.child and ctrl == chain.ik_ctrl:
ctrl = chain.ik_child_ctrl
#add the transform constraints to the original bones to match the temp ik
constraint = constraint_add(bone, ref_org, con_type)
add_enabled_driver(bone.id_data, ctrl.id_data, constraint)
include_collections(included_collections, hidden_objs, self.temp_col, controlled_objs)
del ik_chains, child_names, ctrls, root_name, rig, setup_ids, selected
return {'FINISHED'}
def draw(self, context):
layout = self.layout
btc = context.scene.btc
#Frame Range
split = layout.split(factor=0.5, align = True)
split.prop(btc, 'bake_range_type', text = '')
split.prop(btc, 'bake_layers', text = 'Include All Layers')
if btc.bake_range_type == 'CUSTOM':
row = layout.row()
row.prop(btc, 'bake_range', text = '')
layout.separator()
row = layout.row()
row.prop(context.scene.btc, 'shape_size', text = 'Size')
row.prop(context.scene.btc, 'shape_type', text = '')
layout.separator()
row = layout.row()
row.label(text = 'Rotation Mode:')
row.prop(self, 'rotation_mode', text = '')
layout.separator()
col = layout.column()
col.prop(self, 'smartbake')
col.prop(btc, 'add_ik_ctrl')
split = layout.split(factor=0.58, align = True)
split.prop(btc, 'pole_target')
if btc.pole_target:
split.prop(btc, 'pole_offset', text = '')
col.prop(btc, 'orientation')
# col.prop(self, 'use_hierarchies')
#col.prop(self, 'con_back')
#row = layout.row()
layout.separator()
row = layout.row()
row.label(text = 'Add Hierarchy:')
row = layout.row()
row.prop(context.scene.btc, 'root', text = 'Root Ctrl')
row.prop(btc, 'child', text = 'Extra Child Ctrl')
if context.scene.btc.root:
# row = box.row(align = True)
row = layout.row()
row.prop(context.scene.btc, 'root_object', text = '')
if hasattr(context.scene.btc.root_object, 'type'):
if context.scene.btc.root_object.type == 'ARMATURE':
# row = box.row(align = True)
row = layout.row()
row.prop_search(context.scene.btc, 'root_bone', context.scene.btc.root_object.pose, 'bones', text = '')
def cancel(self, context):
cancel_tempctrls(context)
global ik_chains, child_names, ctrls, root_name, rig, setup_ids, selected
del ik_chains, child_names, ctrls, root_name, rig, setup_ids, selected
return None
def get_org_action(obj, self):
'''get the original action into self.org_action'''
self.org_action = None
if obj.animation_data is not None:
if obj.animation_data.action:
self.org_action = obj.animation_data.action
def add_twist_helpers(self, selected, root_name):
'''In EDIT MODE adding helper MCH and ORG bones for twisting world space controls'''
#First check if we need twist at all
#Cancel twist if using only copy transforms
if self.bake_loc == 'COPY' : self.twist = False
attr_dict = {'x' : 0, 'y' : 1, 'z' : 2}
index = attr_dict[self.axis[1].lower()]
if self.filter_rotation[index]:
self.twist = False
self.filter_rotation[index] = False
####Adding track helpers in EDIT mode###
if self.bake_loc != 'TRACK' or not self.twist:
return []
mch_bones = []
#Making sure there is no double parent
mch_parents = dict()
#Get the root in case there is no parent
edit_root = None if not root_name else rig.data.edit_bones[root_name]
#In case there is no parent add the root
mch_parent = edit_root
for bone in selected:
if bone.parent:
if bone.parent in mch_parents:
mch_parent = mch_parents[bone.parent]
else:
mch_parent = add_ctrl_bone(rig, bone.parent, edit_root, 'MCH_', 'HEAD', orient_world = False)
mch_parents.update({bone.parent : mch_parent})
add_bone_to_collection(rig, mch_parent, col_name = 'MCH', visible = False)
mch_bone = add_ctrl_bone(rig, bone, mch_parent, 'ORG_ROT_', 'HEAD', orient_world = False)
add_bone_to_collection(rig, mch_bone, col_name = 'MCH', visible = False)
mch_bones.append(mch_bone)
return [bone.name for bone in mch_bones]
def setup_twist_helpers(mch_bones, selected, ctrls):
'''In Pose Mode Setup the twist helper bones with constraints'''
if not mch_bones:
return
for mch, bone in zip(mch_bones, selected):
copy_setup_properties(mch, bone)
mch.btc.org = 'ORG'
# constraint_add(mch, bone, 'COPY_LOCATION')
# ctrls.append(mch)
#Constraint the parent of the helper bone to the original parent of the bone
if mch.parent:
copy_setup_properties(mch.parent, bone)
mch.parent.btc.org = 'MCH'
constraint_add(mch.parent, bone.parent, 'COPY_TRANSFORMS')
def add_root_to_bake(self, context, rig, root_name, ctrls, selected):
if not root_name:
return None, None
root_ctrl = rig.pose.bones[root_name]
if constrain_root(context, rig, self.org_obj, root_ctrl):
root_org = self.org_obj.pose.bones[context.scene.btc.root_bone]
ctrls.insert(0, root_ctrl)
selected.insert(0, root_org)
else:
return None, None
return root_ctrl, root_org
def constrain_root(context, rig, obj, root_ctrl):
#make sure there is not already animation data on the rig
if context.scene.btc.root_bone not in obj.pose.bones:
return False
action = rig.animation_data.action if rig.animation_data is not None else None
if action:
root_fcus = get_bone_fcurves(root_ctrl)
if root_fcus:
return False
root_org = obj.pose.bones[context.scene.btc.root_bone]
#constrain the root bone to the referenced root bone
constraint_add(root_ctrl, root_org)
return True
class IK_Chain:
def __init__(self, hierarchy, ctrls, root):
# self.ctrls = ctrls
self.org_bones = hierarchy
self.base_bone = ctrls[-1]
self.ik_tip = ctrls[1] if len(ctrls) > 1 else ctrls[0]
self.ik_ctrl = ctrls[0]
#need to check if I still have any use for chain.root_ctrl
self.root_ctrl = root
self.ctrls = ctrls
#Add all the names for converting to between edit and pose bones
self.ctrl_names = [ctrl.name for ctrl in ctrls]
if self.root_ctrl:
self.root_name = self.root_ctrl.name
self.ik_ctrl_name = self.ik_ctrl.name
self.base_bone_name = self.base_bone.name
self.ik_tip_name = self.ik_tip.name
def length(self, added_ik_ctrl):
return len(self.ctrls)-1 if not added_ik_ctrl else len(self.ctrls)
def bonenames_to_posebones(self, rig):
'''converting bone names to pose bones'''
self.ik_ctrl = rig.pose.bones[self.ik_ctrl_name]
self.base_bone = rig.pose.bones[self.base_bone_name]
self.ik_tip = rig.pose.bones[self.ik_tip_name]
if hasattr(self, 'parent_name'):
self.parent = rig.pose.bones[self.parent_name]
if hasattr(self, 'root_name'):
self.root_ctrl = rig.pose.bones[self.root_name]
if hasattr(self, 'pole_bone_name'):
self.pole_bone = rig.pose.bones[self.pole_bone_name]
# if hasattr(self, 'pole_bone_ref_name'):
# self.pole_bone_ref = rig.pose.bones[self.pole_bone_ref_name]
if hasattr(self, 'ik_child_ctrl_name'):
self.ik_child_ctrl = rig.pose.bones[self.ik_child_ctrl_name]
if hasattr(self, 'pole_child_ctrl_name'):
self.pole_child_ctrl = rig.pose.bones[self.pole_child_ctrl_name]
if hasattr(self, 'base_child_name'):
self.base_child_ctrl = rig.pose.bones[self.base_child_name]
if hasattr(self, 'ref_org_names'):
self.ref_orgs = [rig.pose.bones[name] for name in self.ref_org_names if type(name) == str]
self.ctrls = [rig.pose.bones[name] for name in self.ctrl_names if type(name) == str]
def assign_child_ctrls(self, rig, ctrls_childs):
'''assign all the child bones to the chain class'''
posebones = rig.pose.bones
btc = bpy.context.scene.btc
if self.ik_ctrl.name in ctrls_childs:
self.ik_child_ctrl = posebones[ctrls_childs[self.ik_ctrl.name]]
self.ik_child_ctrl_name = ctrls_childs[self.ik_ctrl.name]
if self.base_bone.name in ctrls_childs:
self.base_child_ctrl = posebones[ctrls_childs[self.base_bone.name]]
self.base_child_name = ctrls_childs[self.base_bone.name]
if not hasattr(self, 'pole_bone'):
return
if self.pole_bone.name in ctrls_childs:
self.pole_child_ctrl = posebones[ctrls_childs[self.pole_bone.name]]
self.pole_child_ctrl_name = ctrls_childs[self.pole_bone.name]
classes = (WorldSpaceCtrls, TempFK, TempIK, BakeToEmpties, AddEmpties, LinkTempChains, UnlinkChains,
BtcSelections, Cleanup, BakeTempCtrls)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)