Hi,
I am currently working on an add-on that is supposed to assign bones to so called “BoneClusters” for easy management and replication (the name BoneGroups was allready taken…)
Why would you need this?
If you wanted to use for example both FK and IK with your armature, some sources (e.g. the first half of this video: https://www.youtube.com/watch?v=WyjJA0v6uCU) suggest that you create multiple identical rigs. One that actually deforms the mesh and one more for each method of animation you want to use (IK, FK, MoCap, etc.). Those access rigs will act as “drivers” for your original rig.
The method demonstrated in the video makes perfect sense and works in theory but has some severe flaws. In reality you would have to copy each and every bone in your armature (bossibly hundreds of bones), possibly rename the copies and then connect the original bones to their respective counterparts via constraints. By hand! And one iteration of this lengthy procedure will only get you one “copy-rig” at a time… Not to speak of the mess that would occur, if you made changes or additions to your original rig afterwards.
So the solution I thought of was to:
-
Assign your bones (doesn’t necessarily have to be your entire rig) to an instance of BoneCluster.
-
Append that instance to a RNA-list inside your armature (no keep it nice and persistent).
-
Give the user some utility options like deleting a cluster, adding or removing bones…
-
And most importantly copying the bones inside your cluster into a new “copy_cluster”. This operator would automatically rename all the copied bones (e.g. some_prefix + original_name) and save a reference to the orginal cluster within the new one, so it can adapt to any changes that you may apply to the original bones
To make my idea mode understandable, here is my full source code (of course work in progress):
import bpy
import os
os.system("cls")
class BoneCluster():
"""Simple class that is used to store groups of bones and some details about them
Bones can be assigned to more than one instance of BoneCluster"""
def __init__(self, name, parentCluster, prefix, bones, constraints):
#The instances name. Will be set and read by the user
self.name = name
#If this instance of BoneCluster is a copy (thus a child) of another instance, a reference to it's "parent" will be stored
self.parentCluster = parentCluster
#If this instance of BoneCluster is a copy (thus a child) of another instance, all of the bones names inside it will begin with this prefix
self.prefix = prefix
#List of all bones, that are assigned to this instance
self.bones = bones
#List of all bone-constraints that link the bones inside this instance of BoneCluster to the corrsponding bones of it's respective parent
self.constraints = constraints
def selectBoneClusterByString(selectorString, boneClusterList):
"""This function finds a certain instance of BoneCluster, inside a given boneClusterList, by it's name
It is used to compensate for the lack of a possibility to pass an instance of BoneCluster directly into an operator"""
blessedCluster = None
for boneCluster in boneClusterList:
if selectorString == boneCluster.name:
blessedCluster = boneCluster
return blessedCluster
def redrawProps(context):
"""Short function that keeps the UI up to date with changes caused by one of the custom operators"""
for region in context.area.regions:
if region.type == 'UI':
region.tag_redraw()
class CreateNewBoneCluster(bpy.types.Operator):
"""This operator creates a new instance of BoneCluster inside the active armatures boneClusterList"""
bl_idname = "multi_rigs.create_new_bone_cluster"
bl_label = "New BoneCluster"
bpy.ops.object.mode_set(mode='EDIT')
scn = bpy.context.scene
name = bpy.props.StringProperty(
name="name",
description="Name of the new BoneCluster",
default="NewBoneCluster")
fromSelected = bpy.props.BoolProperty(
name="fromSelected",
description="Assign selected bones to new cluster?",
default=False)
def execute(self, context):
arm = bpy.context.active_object
validName = True
errorMessage = None
for boneCluster in arm.boneClusterList:
if self.name == boneCluster.name:
validName = False
errorMessage = "Invalid Name: Name allready Taken"
if self.name == "":
validName = False
errorMessage = "Invalid Name: Name empty"
if validName:
if self.fromSelected == False:
newBoneCluster = BoneCluster(
name=self.name,
parentCluster=None,
prefix="",
bones=[],
constraints=[])
else:
tempBones = bpy.context.selected_bones
newBoneCluster = BoneCluster(
name=self.name,
parentCluster=None,
prefix=None,
bones=tempBones,
constraints=[])
arm.boneClusterList.append(newBoneCluster)
redrawProps(context=bpy.context)
self.report({'INFO'}, "Successfully created \"%s\"" %(newBoneCluster.name))
return{'FINISHED'}
else:
self.report({'ERROR'}, errorMessage)
return{'CANCELLED'}
def invoke(self, context, event):
wm = bpy.context.window_manager
return wm.invoke_props_dialog(self)
execute(self, context)
@classmethod
def register(cls):
bpy.types.Object.boneClusterList = []
@classmethod
def unregister(cls):
del bpy.types.Object.boneClusterList
class DeleteBoneCluster(bpy.types.Operator):
"""This operator deletes an instance of BoneCluster out of the active armatures boneClusterList"""
bl_idname = "multi_rigs.delete_bone_cluster"
bl_label = "Delete BoneCluster"
bpy.ops.object.mode_set(mode='EDIT')
pendingKill = None
currentClusterName = bpy.props.StringProperty()
def execute(self, context):
scn = bpy.context.scene
arm = bpy.context.active_object
#this is disgustingly annoying!
self.pendingKill = selectBoneClusterByString(self.currentClusterName, arm.boneClusterList)
if self.pendingKill is not None:
arm.boneClusterList.remove(self.pendingKill)
redrawTools(context=bpy.context)
self.report({'INFO'}, "Successfully deleted %s" %(self.pendingKill.name))
return{'FINISHED'}
else:
self.report({'ERROR'}, "BoneCluster not part of this object")
return{'CANCELLED'}
class MultiRigsFrontend(bpy.types.Panel):
"""Creates a panel inside the properties-window under the category object"""
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_category = "Data"
bl_label = "MultiRigs"
bl_idname = "multi_rigs_frontend"
def draw(self, context):
lay = self.layout
#I would love to use a layout.template_list() where I could display and manipulate my instances of BoneCluster
#unfortunately the list won't accept my BoneCluster()-objects here...
def register():
bpy.utils.register_class(CreateNewBoneCluster)
bpy.utils.register_class(DeleteBoneCluster)
bpy.utils.register_class(MultiRigsFrontend)
def unregister():
bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
try:
unregister()
except Exception as e:
print(e)
pass
register()
Sorry for the long read, this turned out way longer than I wished it to be…
Finally my questions:
-
Am I implementing something that you can allready do in vanilla blender, thus is my add-on redundand? Was switching betwen IK and FK in one rig ever a problem and if so is it stil (note that the link, I shared, is discussing this problem, using a fairly old version of Blender).
-
If you read my code, you’ll notice that I had to implement a really annoying and “unclean” workaround to pass my instances of
BoneCluster
to my operators. Now I’m afraid it is impossible to pass them into alayout.template_list()
(a list that looks and behaves like the material list in you properties panel). Any suggestions how to setup my stucture smarter, so I can circumvent those problems?