# ***** 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 mathutils import Matrix, Vector from math import radians import numpy def draw_wgt(boneLength, bone): suffix = bone.id_data.name + '_' + bone.name if 'WGTB_object' + suffix in bpy.data.objects: obj = bpy.data.objects['WGTB_object'] + suffix if 'WGTB_shape' + suffix in obj.data.name: return obj mesh = bpy.data.meshes.new('WGTB_shape_' + suffix) obj = bpy.data.objects.new('WGTB_object_' + suffix, mesh) #coordinates of the sphere widget shape sphere = {"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]], "vertices": [[0.0, 0.10000002384185791, 0.0], [-0.025881901383399963, 0.09659260511398315, 0.0], [-0.050000011920928955, 0.08660250902175903, 0.0], [-0.07071065902709961, 0.07071065902709961, 0.0], [-0.08660256862640381, 0.04999998211860657, 0.0], [-0.09659260511398315, 0.025881901383399963, 0.0], [-0.10000002384185791, 7.549793679118011e-09, 0.0], [-0.09659260511398315, -0.02588188648223877, 0.0], [-0.08660256862640381, -0.04999998211860657, 0.0], [-0.07071071863174438, -0.07071065902709961, 0.0], [-0.050000011920928955, -0.08660250902175903, 0.0], [-0.02588193118572235, -0.09659260511398315, 0.0], [-3.894143674187944e-08, -0.10000002384185791, 0.0], [0.025881856679916382, -0.09659260511398315, 0.0], [0.04999995231628418, -0.08660256862640381, 0.0], [0.07071065902709961, -0.07071071863174438, 0.0], [0.08660250902175903, -0.05000004172325134, 0.0], [0.09659254550933838, -0.025881946086883545, 0.0], [0.10000002384185791, -4.649123752642481e-08, 0.0], [0.09659260511398315, 0.025881856679916382, 0.0], [0.08660256862640381, 0.04999995231628418, 0.0], [0.07071071863174438, 0.07071065902709961, 0.0], [0.05000007152557373, 0.08660250902175903, 0.0], [0.025881975889205933, 0.09659254550933838, 0.0], [0.0, 7.450580596923828e-09, 0.10000002384185791], [-0.025881901383399963, 7.450580596923828e-09, 0.09659260511398315], [-0.050000011920928955, 7.450580596923828e-09, 0.08660250902175903], [-0.07071065902709961, 7.450580596923828e-09, 0.07071065902709961], [-0.08660256862640381, 3.725290298461914e-09, 0.04999998211860657], [-0.09659260511398315, 1.862645149230957e-09, 0.025881901383399963], [-0.10000002384185791, 8.881784197001252e-16, 7.549793679118011e-09], [-0.09659260511398315, -1.862645149230957e-09, -0.02588188648223877], [-0.08660256862640381, -3.725290298461914e-09, -0.04999998211860657], [-0.07071071863174438, -7.450580596923828e-09, -0.07071065902709961], [-0.050000011920928955, -7.450580596923828e-09, -0.08660250902175903], [-0.02588193118572235, -7.450580596923828e-09, -0.09659260511398315], [-3.894143674187944e-08, -7.450580596923828e-09, -0.10000002384185791], [0.025881856679916382, -7.450580596923828e-09, -0.09659260511398315], [0.04999995231628418, -7.450580596923828e-09, -0.08660256862640381], [0.07071065902709961, -7.450580596923828e-09, -0.07071071863174438], [0.08660250902175903, -3.725290298461914e-09, -0.05000004172325134], [0.09659254550933838, -1.862645149230957e-09, -0.025881946086883545], [0.10000002384185791, -3.552713678800501e-15, -4.649123752642481e-08], [0.09659260511398315, 1.862645149230957e-09, 0.025881856679916382], [0.08660256862640381, 3.725290298461914e-09, 0.04999995231628418], [0.07071071863174438, 7.450580596923828e-09, 0.07071065902709961], [0.05000007152557373, 7.450580596923828e-09, 0.08660250902175903], [0.025881975889205933, 7.450580596923828e-09, 0.09659254550933838], [-7.450580596923828e-09, 4.440892098500626e-16, 0.10000002384185791], [-9.313225746154785e-09, -0.025881901383399963, 0.09659260511398315], [-1.1175870895385742e-08, -0.050000011920928955, 0.08660250902175903], [-1.4901161193847656e-08, -0.07071065902709961, 0.07071065902709961], [-7.450580596923828e-09, -0.08660256862640381, 0.04999998211860657], [-7.450580596923828e-09, -0.09659260511398315, 0.025881901383399963], [-7.450580596923828e-09, -0.10000002384185791, 7.549793679118011e-09], [-7.450580596923828e-09, -0.09659260511398315, -0.02588188648223877], [0.0, -0.08660256862640381, -0.04999998211860657], [0.0, -0.07071071863174438, -0.07071065902709961], [3.725290298461914e-09, -0.050000011920928955, -0.08660250902175903], [5.587935447692871e-09, -0.02588193118572235, -0.09659260511398315], [7.450577044210149e-09, -3.894143674187944e-08, -0.10000002384185791], [9.313225746154785e-09, 0.025881856679916382, -0.09659260511398315], [1.1175870895385742e-08, 0.04999995231628418, -0.08660256862640381], [1.4901161193847656e-08, 0.07071065902709961, -0.07071071863174438], [7.450580596923828e-09, 0.08660250902175903, -0.05000004172325134], [7.450580596923828e-09, 0.09659254550933838, -0.025881946086883545], [7.450580596923828e-09, 0.10000002384185791, -4.649123752642481e-08], [7.450580596923828e-09, 0.09659260511398315, 0.025881856679916382], [0.0, 0.08660256862640381, 0.04999995231628418], [0.0, 0.07071071863174438, 0.07071065902709961], [-3.725290298461914e-09, 0.05000007152557373, 0.08660250902175903], [-5.587935447692871e-09, 0.025881975889205933, 0.09659254550933838]], "faces": []} mesh.from_pydata(numpy.array(sphere['vertices'])*[boneLength, boneLength, boneLength] , sphere['edges'], sphere['faces']) return obj def add_driver(obj, posebone, control, target, path, multiply = ''): if isinstance(target, tuple): attr = posebone.driver_add(target[0], target[1]) else: attr = posebone.driver_add(target) var = attr.driver.variables.new() var.targets[0].id = obj var.targets[0].data_path = 'pose.bones["' + control +'"].'+ path attr.driver.expression = var.name + multiply def dup_values(source, target): if hasattr(source, 'parent'): target.parent = source.parent for prop in dir(source): if not hasattr(target, prop): continue value = getattr(source, prop) if type(value) not in {int, float, bool, str, Vector, Matrix, bpy.types.Object}: continue if '__' in prop[:2] and '__' in prop[-2:]: continue if target.is_property_readonly(prop): continue setattr(target, prop, value) return target def dup_constraints(source, target): if not source.constraints.items(): return for source_con in source.constraints: target_con = target.constraints.new(source_con.type) dup_values(source_con, target_con) def add_vis_bone_con(obj, bone_vis_name, bone_wgt_name): bone_vis = obj.pose.bones[bone_vis_name] con = bone_vis.constraints.new('STRETCH_TO') con.target = obj con.subtarget = bone_wgt_name return bone_vis class target: def __init__(self, bone): self.name = bone.name self.point = tuple(bone.tail) self.ctrl = 'TRGT_' + bone.name if bone.parent: self.parent = bone.parent.name #print('assign parent to target ', self.name, self.ctrl, self.parent) def __lt__(self, other): return self.point < other.point def __hash__(self): return hash(self.point) def __eq__(self, other): #if not isinstance(other, type(self)): # return NotImplemented return self.point == other.point class parent: def __init__(self, bone): self.name = bone.name self.point = tuple(bone.head) self.ctrl = 'CTRL_' + bone.name if bone.parent: self.parent = bone.parent.name def __lt__(self, other): return self.point < other.point def __hash__(self): return hash(self.point) def __eq__(self, other): #if not isinstance(other, type(self)): # return NotImplemented return self.point == other.point class constraint_dup: def __init__(self, bone, con): self.name = con.name self.target = con.target self.subtarget = con.subtarget self.bone = bone.name def __hash__(self): return hash(self.bone) def __eq__(self, other): return self.bone == other.bone def bone_orientation(source, target, value): source.align_orientation(target) x, y, z = source.matrix.to_3x3().col R = (Matrix.Translation(source.head) @ Matrix.Rotation(radians(value), 4, x) @ Matrix.Translation(-source.head)) source.transform(R, roll = False) source.align_roll(target.vector) def find_ctrl(bone, controls): i = list(controls).index(bone) bone.ctrl = list(controls)[i].ctrl return bone.ctrl def add_controlers(self, obj, parents, targets): #controls = set(parents).union(targets) controls = set(parents + targets) #create hierarchy for bone in controls: editbone = obj.data.edit_bones[bone.name] if editbone.parent is None: continue parentnames = [bone.name for bone in parents] #if a target and its parent are part of the hierarchy then linked to its own bone parent if bone in targets and bone not in parents and editbone.parent.name in parentnames: parentbone = parent(editbone) else: parentbone = parent(editbone.parent) if parentbone in controls and parentbone != bone: bone.parent = find_ctrl(parentbone, controls) else: bone.parent = editbone.parent.name edit_bones = obj.data.edit_bones for bone in controls: editbone = edit_bones[bone.name] ctrl = obj.data.edit_bones.new(bone.ctrl) ctrl.head = bone.point ctrl.tail = bone.point ctrl.tail[2] = bone.point[2] + (editbone.length / 3) ctrl.bbone_x = editbone.bbone_x ctrl.bbone_z = editbone.bbone_z ctrl.use_deform = False if self.bone_align: angle = 90 if self.align_90 else 0 bone_orientation(ctrl, editbone, angle) if angle == 90: ctrl.align_roll(editbone.vector) else: ctrl.roll = editbone.roll #apply hierarchy for bone in parents: editbone = edit_bones[bone.name] ctrl_name = find_ctrl(bone, controls) ctrl = edit_bones[ctrl_name] editbone.parent = ctrl for bone in controls: ctrl = edit_bones[bone.ctrl] if hasattr(bone, 'parent'): ctrl.parent = edit_bones[bone.parent] return controls def pose_bbone_setup(bone, posebone, bbone_group = None): #add the custom shape to the widget bones custom_shape = draw_wgt(bone['length'], posebone) posebone.custom_shape = custom_shape posebone.use_custom_shape_bone_size = False if bbone_group: posebone.bone_group = bbone_group posebone.rotation_mode = 'XZY' posebone.lock_rotation[0] = True posebone.lock_rotation[2] = True #####MAIN#### class BboneWidgets(bpy.types.Operator): """Add Bbone widget controls to the selected bones""" bl_idname = "armature.add_bbone_widgets" bl_label = "Add_Bbone_widgets" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return bpy.context.object.type == 'ARMATURE' def execute(self, context): obj = context.object obj.data.display_type = 'BBONE' bones = [] parentlayers = [False if i != 24 else True for i in range(32)] wgtlayers = [True if i == 0 else False for i in range(32)] bpy.ops.object.mode_set(mode = 'EDIT') obj.data.use_mirror_x = False for bone in obj.data.edit_bones: if not bone.select: continue if bone.bbone_segments == 1: bone.bbone_segments = 10 bone.bbone_handle_type_start = 'TANGENT' bone.bbone_handle_type_end = 'TANGENT' bone_name = bone.name #add parent bone to the Bbone widgets parent = obj.data.edit_bones.new('WGTB_parent_'+ bone_name) parent_name = parent.name dup_values(bone, parent) parent.name = parent_name parent.select = False parent.select_head = False parent.select_tail = False #change layer of the parent bone if bpy.app.version < (4, 0, 0): parent.layers = parentlayers #add bbone widget bones head_widget = obj.data.edit_bones.new('Bhead_'+ bone.name) head_widget.parent = parent #head_widget.head = bone.head head_widget.head = bone.head + (bone.tail - bone.head) * 0.25 #head_widget.tail = (bone.tail - bone.head)/10 head_widget.length = bone.length * 0.1 head_widget.bbone_x = bone.bbone_x head_widget.bbone_z = bone.bbone_z head_widget.align_orientation(bone) head_widget.inherit_scale = 'NONE' head_name = head_widget.name tail_widget = obj.data.edit_bones.new('Btail_'+ bone.name) tail_widget.parent = parent #tail_widget.head = bone.tail tail_widget.head = bone.head + (bone.tail - bone.head) * 0.75 #tail_widget.tail = bone.tail - (bone.tail - bone.head)/10 tail_widget.length = bone.length * 0.1 tail_widget.bbone_x = bone.bbone_x tail_widget.bbone_z = bone.bbone_z tail_widget.align_orientation(bone) tail_widget.inherit_scale = 'NONE' tail_name = tail_widget.name #add vis bones head_vis = obj.data.edit_bones.new('Bhead_vis_'+ bone.name) head_vis.parent = parent head_vis.head = bone.head head_vis.tail = head_widget.head head_vis.bbone_x = bone.bbone_x*0.1 head_vis.bbone_z = bone.bbone_z*0.1 #head_vis_name = head_vis.name head_vis.hide_select = True head_vis.use_deform = False tail_vis = obj.data.edit_bones.new('Btail_vis_'+ bone.name) tail_vis.parent = parent tail_vis.head = bone.tail tail_vis.tail = tail_widget.head tail_vis.bbone_x = bone.bbone_x*0.1 tail_vis.bbone_z = bone.bbone_z*0.1 #tail_vis_name = tail_vis.name tail_vis.hide_select = True tail_vis.use_deform = False if bpy.app.version < (4, 0, 0): tail_widget.layers = wgtlayers head_widget.layers = wgtlayers head_vis.layers = wgtlayers tail_vis.layers = wgtlayers bones.append({'name': bone_name, 'parent': parent_name, 'head': head_name, 'tail': tail_name, 'head_vis': head_vis.name, 'tail_vis': tail_vis.name, 'length': bone.length}) #####POSE MODE####### bpy.ops.object.mode_set(mode = 'POSE') if bpy.app.version < (4, 0, 0): bone_groups = obj.pose.bone_groups if 'BBone Widgets' not in bone_groups: bbone_group = bone_groups.new(name = 'BBone Widgets') bbone_group.color_set = 'THEME09' else: bbone_group = bone_groups['BBone Widgets'] else: bbone_group = None for bone in bones: posebone = obj.pose.bones[bone['name']] # Prepare parent bone in pose mode poseparent = obj.pose.bones[bone['parent']] #disable use deform obj.data.bones[bone['parent']].use_deform = False obj.data.bones[bone['head']].use_deform = False obj.data.bones[bone['tail']].use_deform = False pose_bbone_setup(bone, obj.pose.bones[bone['head']], bbone_group) pose_bbone_setup(bone, obj.pose.bones[bone['tail']], bbone_group) dup_constraints(posebone, poseparent) #add all the drivers add_driver(obj, posebone, bone['head'], 'bbone_curveinx', 'location.x') add_driver(obj, posebone, bone['head'], 'bbone_curveinz', 'location.z') add_driver(obj, posebone, bone['head'], 'bbone_easein', 'location.y', '*5/'+ str(bone['length'])) add_driver(obj, posebone, bone['head'], 'bbone_rollin', 'rotation_euler.y') add_driver(obj, posebone, bone['head'], ('bbone_scalein', 0), 'scale.x') add_driver(obj, posebone, bone['head'], ('bbone_scalein', 1), 'scale.y') add_driver(obj, posebone, bone['head'], ('bbone_scalein', 2), 'scale.z') add_driver(obj, posebone, bone['tail'], 'bbone_curveoutx', 'location.x') add_driver(obj, posebone, bone['tail'], 'bbone_curveoutz', 'location.z') add_driver(obj, posebone, bone['tail'], 'bbone_easeout', 'location.y', '*-5/'+ str(bone['length'])) add_driver(obj, posebone, bone['tail'], 'bbone_rollout', 'rotation_euler.y') add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 0), 'scale.x') add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 1), 'scale.y') add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 2), 'scale.z') #add constraints to visual bones head_vis = add_vis_bone_con(obj, bone['head_vis'], bone['head']) tail_vis = add_vis_bone_con(obj, bone['tail_vis'], bone['tail']) if bpy.app.version < (4, 0, 0): head_vis.bone_group = bbone_group tail_vis.bone_group = bbone_group return {"FINISHED"} class ChainControls(bpy.types.Operator): """Add parent and target controls to the selected bones to create a chain control""" bl_idname = "armature.add_chain_ctrls" bl_label = "Add_Chain_Controls" bl_options = {'REGISTER', 'UNDO'} parents: bpy.props.BoolProperty(name = 'Add Parents', description = "Align the controls 90 degrees to the original bones", default = True) targets: bpy.props.BoolProperty(name = 'Add Targets', description = "Align the controls 90 degrees to the original bones", default = True) keep_hierarchy: bpy.props.BoolProperty(name = 'Keep Hierarchy', description = "Keep the controls in the hierarchy of the original bones", default = True) bone_align: bpy.props.BoolProperty(name = 'Align to Bones', description = "Align the controls to the original bones", default = True) align_90: bpy.props.BoolProperty(name = '+90°', description = "Align the controls 90 degrees to the original bones", default = True) @classmethod def poll(cls, context): return bpy.context.object.type == 'ARMATURE' def invoke(self, context, event): #obj = context.object wm = context.window_manager return wm.invoke_props_dialog(self, width = 200) def draw(self, context): layout = self.layout row = layout.row() row.label(text = 'Add Control Bones') row = layout.row() row.prop(self, 'parents') #text = 'Size' row.prop(self, 'targets') layout.separator() col = layout.column() col.prop(self, 'keep_hierarchy') row = layout.row() row.prop(self, 'bone_align') if self.bone_align: row.prop(self, 'align_90', toggle=True) def execute(self, context): obj = context.object targets = [] parents = [] bpy.ops.object.mode_set(mode = 'EDIT') edit_bones = bpy.context.selected_editable_bones #create list of parent and target objects for bone in edit_bones: bone.use_connect = False if self.targets: targets.append(target(bone)) if self.parents: parents.append(parent(bone)) controls = add_controlers(self, obj, parents, targets) bpy.ops.object.mode_set(mode = 'POSE') #Add the bone group for the ctrls if doesn't exist if bpy.app.version < (4, 0, 0): bone_groups = obj.pose.bone_groups if 'Ctrl Bones' not in bone_groups: ctrl_group = bone_groups.new(name = 'Ctrl Bones') ctrl_group.color_set = 'THEME01' else: ctrl_group = bone_groups['Ctrl Bones'] for bone in controls: posebone = obj.pose.bones[bone.ctrl] if bpy.app.version < (4, 0, 0): posebone.bone_group = ctrl_group else: posebone.color.palette = 'THEME01' if self.targets: for bone in targets: #update from the controls set ctrl = find_ctrl(bone, controls) posebone = obj.pose.bones[bone.name] con = posebone.constraints.new('STRETCH_TO') con.target = obj con.subtarget = ctrl return {"FINISHED"} class MergeRigs(bpy.types.Operator): """Merge selected rigs to active and keep hierarchy and constraints for shared bones""" bl_idname = "armature.merge" bl_label = "Merge_Rigs" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return bpy.context.object.type == 'ARMATURE' def execute(self, context): target_obj = context.object if target_obj.type != 'ARMATURE': return {"CANCELLED"} target_bones = set([bone.name for bone in target_obj.data.bones]) constraints = [] childrens = {} #Store children and constraints bpy.ops.object.mode_set(mode = 'POSE') for obj in bpy.context.selected_objects: if obj.type != 'ARMATURE': continue if obj == target_obj: continue #create a set of all the similiar bones in all the rigs obj_bones = set([bone.name for bone in obj.data.bones]) shared_bones = target_bones.intersection(obj_bones) #find all the constraints and children for bone in obj.pose.bones: #store all the constraints for con in bone.constraints: if not hasattr(con, 'subtarget'): continue if con.target == obj and con.subtarget in shared_bones: constraints.append(constraint_dup(bone, con)) if bone.name in shared_bones: for child in bone.children: if child.name in childrens: continue childrens.update({child.name : bone.name}) #remove shared bones bpy.ops.object.mode_set(mode = 'EDIT') for obj in bpy.context.selected_objects: if obj.type != 'ARMATURE': continue if obj == target_obj: continue for bone in shared_bones: if bone not in obj.data.edit_bones: continue obj.data.edit_bones.remove(obj.data.edit_bones[bone]) bpy.ops.object.mode_set(mode = 'POSE') bpy.ops.object.join() #restore constraints for con_dup in constraints: if con_dup.bone in target_bones: continue if con_dup.bone not in target_obj.pose.bones: continue #print('constraint on ',con_dup.bone, con_dup.name) posebone = target_obj.pose.bones[con_dup.bone] if con_dup.name not in posebone.constraints: continue con = posebone.constraints[con_dup.name] con.target = target_obj con.subtarget = con_dup.subtarget #reparent all child bones bpy.ops.object.mode_set(mode = 'EDIT') for child, parent in childrens.items(): target_obj.data.edit_bones[child].parent = target_obj.data.edit_bones[parent] return {"FINISHED"} classes = (MergeRigs,BboneWidgets, ChainControls) def register(): from bpy.utils import register_class for cls in classes: register_class(cls) # bpy.utils.register_class(BboneWidgets) # bpy.utils.register_class(ChainControls) # bpy.utils.register_class(RiggerToolBox_PT_Panel) def unregister(): from bpy.utils import unregister_class for cls in classes: unregister_class(cls) # bpy.utils.unregister_class(BboneWidgets) # bpy.utils.unregister_class(ChainControls) # bpy.utils.unregister_class(RiggerToolBox_PT_Panel) if __name__ == "__main__": register()