Introduction to writing custom Rigify types


Posted on Aug. 10, 2021, 5:53 p.m. by nekomata • Last updated on Aug. 15, 2021, 8:10 p.m.

This tutorial assumes basic knowledge of Rigify, Python, the Blender API and programming in general.

Rigify is a powerful addon that allows users to generate complex rigs from simple template rigs, called metarigs. These metarigs are decorated by Rig types, which tell Rigify how to convert the meta rig into a proper rig. Rigify provides it's own set of rig types, but the addon allows one to write your own types and put them into packages which it calls feature sets. In this tutorial we provide an introduction to writing Rifigy feature sets by going step by step through an example type.


The rough outline of this tutorial looks as follows:

  1. Introducing the example type
  2. Brief setup of IDE and project structure
  3. Rig type parameters
  4. Bone manipulation
  5. Drivers and constraints
  6. Custom bone shapes
  7. Samples and metarigs
  8. Further reading

Introducing the example type

For this tutorial we will consider a rig component that is a chain of bbones of which the start and end of each segment is individually controlled by a control bone. Scaling a control bone should scale the beginning and end of the appropriate bbones. Users should be able to specify the amount of bbones per section, the layer on which the control bones are created, and have a simple slider in the N-panel that indicates how much bbones are allowed to stretch in order to reach the positions of the control bones. This type should at least lightly touch upon most things the rigify module has to offer.

example type in action


IDE and project structure

It doesn't really mater what you write this in, but I like my autocompletes, so I'll briefly discuss what i use.

I personally like to use VSCode, you can then either install a "fake" bpy module that gives us auto completion of the blender API see here, or specify an extra auto complete path a la this. In VSCode we can use this same python.autoComplete.extraPaths property to make the rigify modules autocomplete (you can find rigify at blender< version >< version >\scripts\addons\rigify)

The expected structure of a feature set is as follows:

feature_set_name
    __init__.py
    metarigs
        __init__.py
        feature_set
            __init__.py
            feature_set_metarig.py
    rigs
        __init__.py
        feature_set
            __init__.py
            custom_rig.py

which will also be our project structure. feature_set_metarig.py normally contains the code to generate meta rigs, if you're simply adding rig types and have no desire to supply meta rigs, you can safely delete the content of the meterigs folder.

the file that we'll be working in for the remainder of this tutorial is the custom_rig.py file.


Rig type parameters

Before we get into this subject let us first quickly do some imports and create the class containing all the code:

import bpy
from rigify.base_rig import stage, BaseRig
import rigify.utils as utils

class Rig(BaseRig):

rifigy will use all Rig classes in the feature_set module to populate its rig types.

Not only can users select rig types, but they can also fill in additional parameters that influence the generation process, in our case: the number of bbones per segment and on what layer the put the control bones.

    @classmethod
    def add_parameters(self, params):
        params.example_bb_segs = bpy.props.IntProperty(
            name        = 'B-Bone Segments',
            default     = 3,
            min         = 1,
            description = 'Number of B-Bone segments'
        )
        Rig.tweak.add_parameters(params)

    Rig.tweak = utils.layers.ControlLayersOption('Tweak', description="Layers for the tweak controls to be on")


The add_parameters class function allows us to register a set of parameters used by the rig component, this way we can add an integer that will hold the amount of bbone segments. Creating a layer picker is a bit more complex, but luckily rigify provides a utility class that does the hard work for us, because we're going to need that class in other places we create it once as a static value. Rigify also provides two of its own instances of this class at utils.layers.ControlLayersOption.FK and utils.layers.ControlLayersOption.TWEAK. Beware that params is essentially global, any variable that we attach to it should thus be named sufficiently descriptive as to not clash with other variables assigned to it.

To render these params we write the following:

    @classmethod
    def parameters_ui(self, layout, params):
        layout.row().prop(params, 'example_bb_segs')
        Rig.tweak.parameters_ui(layout.row(), params)

Note that for this to be successful the string example_bb_segs needs to be equal to the variable name that we defined earlier. During the generation process we can now access our example_bb_segs prop as self.params.example_bb_segs.


Bone manipulation

We'll first create some utility functions. Because in blender bone references break when switching between modes, rigify mostly works with bone names instead, and provides functions that use these names instead of the actual bones. we're going to define a function that given a bone name generates a b-bone, one that generates a control bone, and one that given the names of a chain of bones, generates a chain of controls.

    def generate_bbone(self, name):
        #create bone
        bone_name = self.copy_bone(name, utils.naming.make_derived_name(name, 'def'))

        #set bbone stuff
        self.get_bone(bone_name).bbone_segments = self.params.example_bb_segs

        return bone_name

The BaseRig class provides us with some useful functions to manipulate bones, one of which is copy_bone(name, new_name) which copies a bone given it's name, and renames it to a given new name. Rigify keeps track of 4 different kinds of bones: original (ORG), deform (DEF), mechanical (MCH), and control (CTRL). ORG, DEF and MCH bones automatically get assigned in their appropriate bone layers, and only DEF bones have their deform setting turned on. To make our copied bone a deform bone we prepend it with the 'DEF-' prefix, utils.naming.make_derived_name takes any bone name and makes it into the type of our pleasing.

There is no utility function (that i know of) that allows us to set bbone settings, fortunately we can do so ourselves, we can get any pose or edit bone with a given name with self.get_bone(name).

    def generate_control(self, name):
        #create bone
        bone_name = self.copy_bone(name, utils.naming.make_derived_name(name, 'ctrl'))

        #set orientation and size
        utils.bones.align_bone_to_axis(self.obj, bone_name, 'y', length=self.scale)
        return bone_name
		

To create a control for a bone, we again copy the bone, and rotate the copy such that it is aligned with the world matrix. Util functions that manipulate a bone usually require not just the bone name but also the rig object, the rig object is stored in self.obj. We'll get back to the self.scale variable.

    def generate_control_chain(self, names, include_head, include_tail):
        if include_head:
            chain_names = [self.generate_control(b) for b in names]
        else:
            chain_names = [self.generate_control(b) for b in names[1:]]
        if include_tail:
            chain_names.append(self.generate_control(names[-1]))
            utils.bones.put_bone(self.obj, chain_names[-1], self.get_bone(names[-1]).tail)
        return chain_names
		

Creating a single control bone is nice, but it'd be nicer if we could create an entire chain of control bones, the function above does just that, in- or excluding the controls at either end of the chain.

getting and storing bone names

Rigify uses self.bones.org, self.bones.mch, self.bones.ctrl and self.bones.deform to store the bone names. To these, we can bind strings, lists of string, dictionaries of strings, or bind a variable name of one of those types, so the following are all valid:

self.bones.deform = "bone.001"
self.bones.deform = ["bone.001", "bone.002"]
self.bones.deform = {'l': ["bone.L.001"], 'r' : ["bone.R.001"]}
self.bones.deform.face = {'l': ["bone.L.001"], 'r' : ["bone.R.001"]}
etc...

(You can probably do more, but the underlying BoneDict class is a bit of a mystery to me still, anyhoo, all of the above work and is usually enough to organize things)

We can find the name of the bone that got assigned the rig type in self.base_bone, usually that is enough to create our rig component, but if we want the names of all bones owned by the component we can use self.rigify_org_bones (beware, these aren't in order) or we can use the self.bones.org BoneDict.

generation stages

Rigs in Rigify are generated in stages: initialize, prepare_bones, generate_bones, parent_bones, configure_bones, preapply_bones, apply_bones, rig_bones, generate_widgets and finalize.

One stage only starts when all component rigs have finished the previous stage. Each stage takes place in the appropriate mode: prepare_bones, generate_bones, parent_bones and apply_bones are called in edit mode, and all other stages are called in object mode. there are two ways to let a function run in one of these stages, in this tutorial we will come across both.

In the initialize stage we can initialize things, do assertions upon the metarig, etc. we'll use this stage to set a scale parameter that the generate_control function uses the set the size of control bones:

    def initialize(self):
        self.scale = self.get_bone(self.base_bone).length * 0.25

This also shows the first way to run functions within a stage, the BaseRig class has a set of functions named after each stage which we can override to run within those stages.

Next we want to generate some bones:

    @stage.generate_bones
    def generate_chain_bones(self):
        base_chain = [self.base_bone] + utils.connected_children_names(self.obj, self.base_bone)
        self.bones.deform = [self.generate_bbone(b) for b in base_chain]
        self.bones.ctrl = self.generate_control_chain(base_chain, True, True)

This shows the second way to let functions run in a specific stage, the @stage decorator allows us to run any function in any of the stages (including multiple functions), this is a great way to better organize our code.

To get the entire chain of bones we call utils.connected_children_names(rig_obj, name) which returns a list of bone names of all children in a connected chain (until it splits or stops). Next we simply invoke the utility functions we made earlier and store the resulting names in the appropriate place.

    @stage.parent_bones
    def set_bone_relations(self):
        self.parent_bone_chain(self.bones.deform, use_connect=True, inherit_scale="NONE")

Although we could have parented the bones in the function above, it is generally good practice to only perform actions relevant to the stage. Other rig components might depend on your rig generating in a well behaved manner. Also note that we do not parent the control bones to anything. any bones that are left unparented at the end, automatically become parented to the root bone. We can turn this behavior off for specific bones using self.generator.disable_auto_parent(bone_name).

    @stage.configure_bones
    def set_layers(self):
        Rig.tweak.assign_rig(self, self.bones.ctrl) 

To move the control bones to the right layer we can use the ControlLayersOption class we added as a static variable earlier. Note that most UI things in rigify need the rig class (not object) which is simply self.


Drivers and constraints

Now that all of the relevant bones have been generated, parented and put in the correct layers, it is time to add drivers and constraints to make all of the complex things actually work

At this point we can just use the blender API to do so, but rigify provides some wrappers which are a bit shorter in case of bones.

Remember that we wanted users to specify how much the bbones should be allowed to stretch to reach the control bones. Let us first define a property and UI panel that allows the user to specify this:

    @stage.rig_bones
    def add_constraints_and_drivers(self):
        prop_b = self.bones.ctrl[0]
        self.make_property(prop_b, 'stretch', default=1.0, min=0.0, max=1.0)

        panel = self.script.panel_with_selected_check(self, self.bones.ctrl)
        panel.custom_prop (prop_b, 'stretch', text='stretch', slider=True)
        ...

We first add a property to the first ctrl bone, we're going to link that property to the rigify UI panel. We create a UI panel which shows up when the user has any of the control bones selected, and then we add the property to the panel. Multiple calls to self.script.panel_with_selected_check(rig, names) will return the same panel when the names are identical. This means that if we split our code among different functions that need the same panel this isn't an issue.

Now we can use the property in drivers, which the user in turn controls via the n-panel.

Next, we create the damped track and stretch to constraints that make the deform bones follow the control bones:

        ....
        for ctrl, deform in zip(self.bones.ctrl[1:], self.bones.deform):
            self.make_constraint(deform, "DAMPED_TRACK", ctrl)
            self.make_constraint(deform, "STRETCH_TO", ctrl, name=f'{deform}_stretch')
            self.make_driver(self.get_bone(deform).constraints[f'{deform}_stretch'], "influence", variables=[(prop_b, 'stretch')])
        ....

the self.make_constraint function provides a simple wrapper to assign constraints in a single line, it takes a bone_name, constraint type, and a target bone name. It has some more optional parameters (i'll link the relevant reading material at the end of this tutorial) and passes on any other constraint parameters onto the constraint. Note that we give the stretch to constraint a specific name so we can easily find it back to attach a driver to it. In the next line we immediately attach the driver with self.make_driver, the variables argument takes a variety of different structures, In this tutorial we'll see two, but i highly recommend reading up on the details. In this case we just need a custom property we defined earlier on one of our bones, in which case a simple tuple of the bone name and property name is sufficient for rigify to work with.

Lastly we must make sure that the beginning and ends of bbone segments scale with the scale of the control:

        ....
        for b, c1, c2 in zip(self.bones.deform, self.bones.ctrl, self.bones.ctrl[1:]):
            self.make_driver(self.get_bone(b), 'bbone_scaleinx', variables=[utils.mechanism.driver_var_transform(self.obj, c1, type='SCALE_X', space='WORLD')])
            self.make_driver(self.get_bone(b), 'bbone_scaleiny', variables=[utils.mechanism.driver_var_transform(self.obj, c1, type='SCALE_Y', space='WORLD')])
            self.make_driver(self.get_bone(b), 'bbone_scaleoutx', variables=[utils.mechanism.driver_var_transform(self.obj, c2, type='SCALE_X', space='WORLD')])
            self.make_driver(self.get_bone(b), 'bbone_scaleouty', variables=[utils.mechanism.driver_var_transform(self.obj, c2, type='SCALE_Y', space='WORLD')])

In this case rigify provides a utility function to create the appropriate transform channel variables.


Custom bone shapes

With all functionality set up, we only need to deal with bone shapes:

    @stage.generate_widgets
    def generate_control_widgets(self):
        for ctrl in self.bones.ctrl: 
            utils.widgets_basic.create_cube_widget(self.obj, ctrl)

rigify provides a set of basic shapes, but if you want to do something more custom you can do that too:

obj = utils.widgets.create_widget(self.obj, bone_name)
if obj != None:
    verts = [] #define verts
    edges = [] # define edges
    mesh = obj.data
    mesh.from_pydata(verts, edges, [])
    mesh.update()


Samples and metarigs

We can now zip up our project folder and feed it into rigify, and everything should work. If you want the uninterrupted code file you can find that here. As a finishing touch we can go into blender, create a metarig containing just a sample of our newly created type, and then in edit mode go to the rigify n-panel and click encode sample to python this will create a text document in the blender file with a function that creates a sample when a user presses the add sample button. Simply copy the function into the codefile and reinstall the feature set. The Encode metarig to python creates a file that you can put into the metarigs folder in the project structure which then adds that metarig to the add rig submenu (in this specific case that's a bit useless though).


Further reading

In this tutorial we did the somewhat of a hello world equivalent for custom Rigify types. There are essentially 3 resources to consult when you want to dive deeper into this. Firstly, the dev-wiki of rigify, which you can find here. Secondly, the blender API manual, found here. And lastly, the rigify code itself.