Planning a Modular Rig: My Final Degree Project
This one is a nostalgic post. I want to go back to my final university degree project, because the ideas in it became the foundation for the rigging toolkits I have built ever since. So here we go.
It was 2018, the start of my last year at university, and the brief was not small. Four different projects, each with its own characters: bipeds, quadrupeds, and robots, and all of them had to work with motion capture. The schedule was tight, the asset list was long, the models were not ready, and we needed to start animating as soon as possible. AI tools, of course, did not exist yet. The rigging team for the entire course was tiny: just me, plus a colleague helping with some of the manual tasks. Thank goodness she was there.
The only way to keep up with that pace was to stop rigging by hand and build a modular rigging pipeline from scratch in Python.
A procedural mindset
The core idea was simple to state and demanding to build. Each module would be generated from a small set of input pivot points that defined where the main bones should sit. A single build action would then reconstruct the entire rig from those inputs, from nothing, every time. Everything lived inside a procedural workflow: each model change just produced a freshly generated rig.
The part that mattered most for a four-project course with constant model updates: the controls always came out with the same names, the same orientations, and the same hierarchy. That meant animation done on an earlier version of a character still applied cleanly after the rig was rebuilt. Nothing had to be re-authored just because the mesh changed.
The first job was to analyse every character across the four projects and work out the minimum set of rigging modules I actually needed to assemble them.
The modules
These eight modules were the building blocks. Everything on every character was assembled from some combination of them.
FK
- Leaf and hinge chains
- Quick one-off additions
IK
- Mechanical setups
- Pistons and linkages
Limb
- Compound IK and FK chain
- Soft effector, reverse roll
- Pole-vector pinning, stretch
- Curvature and bendy system
Finger
- FK chain
- Set-driven-key attributes
Spine
- Spline: pelvis, lumbar, chest
- Tangent curvature control
- Underlying FK layer
- Stretch and volume preservation
Neck
- Spline, two controls
- Neck and head, linear motion
Tail
- Spline, multiple IK controls
- Two layers: high and low detail
Sliding
- Bones sliding on a NURBS surface
- Useful for facial setups
Those modules were the foundation to start working with, but a module catalogue is not a pipeline. To make it production-ready, the rest of the system had to handle one thing well: data.
Serializing everything
The principle was to serialize everything, whether the natural home for that data was a Maya ASCII scene, an XML file, or JSON. For each file type I wrote a small translator that knew how to save and load that data, and every translator exposed its own input and output functions.
JSON did most of the work. It is convenient, it serializes custom objects, and json.dumps covers the common cases. For heavy, binary-friendly data such as deformer weights I leaned on cPickle to store compressed dictionaries. Several Maya exporters write their data as XML by default, and Python already ships a parser for that in xml.etree.ElementTree, with functions to write and read the tree. For whole scenes, cmds.file() with exportAll or exportSelected did the job.
These were the six elements the system needed to round-trip.
Pivot points
The inputs that mark where the main bones go. Stored as a Maya scene to keep the early system simple rather than over-engineer it from day one.
Control shapes
Per control and per shape: CV positions, knot count, colour, line width, and any custom DCC properties.
Settings
Building a module is something you plan. It cannot work for one asset only, it has to be configurable: how far a bone rotates, the finger master attributes, limb length, squash and stretch limits and relations, how much a bone slides on a NURBS. None of this is hardcoded in the build. It is configured per character and saved as that character's configuration file.
Deformer weights
Maya writes weights as XML by default, though it can also write JSON. Either way the file can get buggy or bloated. A skinCluster holds a value per vertex and per influence, which is a very large matrix, so it was worth optimising that into a custom compressed serialization. I also stored vertex positions and UVs, so weights could be transferred onto updated geometry when loading.
Characterization templates
HumanIK characterizations are XML, since the logic comes from MotionBuilder. I kept them as XML so they stayed compatible, which mattered for the mocap side of the brief.
Blend shapes
The absolute vertex positions, or the deltas of those positions for a direct transfer onto a mesh with matching topology.
Structuring the code
Serialization and IO were only half of it. The pipeline also needed utility modules that smoothed out the day-to-day rigging work: creating geometry layers, rivets, space switches, cleanup passes, and so on. To keep all of this maintainable I split the code into clear areas, each with a single responsibility.
rig_toolkit/
-
core/
attributes · character actions · constants · controllers and control shapes · files · node creator · weights. The input and output backbone, with nothing rig-specific in it.
-
functions/
attached FK systems · volume preservation · matrix actions · space switches · curve actions · math actions.
-
modules/
every rig module above: FK, IK, limb, finger, spine, neck, tail, sliding.
-
presets/
saved configurations, control shapes, and interpolations.
-
utilities/
widgets to ease rig creation · shape management · marking menus · menu bars · shelves.
-
icons/
the PNG and SVG sets.
The split is the point. core knows how to read and write things but nothing about rigging. modules knows how to build rigs but never touches a file directly. Anything reusable that was not a full system landed in functions. That separation is exactly what let me keep extending the toolkit for years afterwards without the whole thing collapsing under its own weight.
Built on OOP
All of this leads back to the four principles of object-oriented programming: encapsulation, abstraction, inheritance, and polymorphism. Every system in the toolkit had to line up with them. Encapsulation kept each module’s internals hidden behind a clean surface. Abstraction let the rest of the code talk to a module without caring how it was wired inside. Polymorphism meant the same call could do the right thing on very different characters. And inheritance was the one that stopped me from writing the same rig logic twice.
That last principle carried the most weight on a four-project deadline. To avoid duplicating logic and to keep everything template-based, the build system was an inheritance tree. At the root sat a base Character class that declared the callable methods defining the build execution, each one raising NotImplementedError. It was a contract, not an implementation: every child class then had to implement, extend, or override those steps.
class Character: """The contract. Every character knows how to build itself, but the base class refuses to guess how.""" def load_base_setup(self): raise NotImplementedError def build(self): raise NotImplementedError def create_relations(self): raise NotImplementedError def load_custom_data(self): raise NotImplementedError def cleanup(self): raise NotImplementedError
class BaseBiped(Character): """Implements the contract for a generic biped: spine, neck, two arms, two legs, and the shared control logic.""" def load_base_setup(self): ... def build(self): ... def create_relations(self): ... def load_custom_data(self): ... def cleanup(self): ...
class Ecko(BaseBiped): """A specific character. Only what makes Ecko different from a base biped lives here.""" def build(self): super().build() # everything a biped already does # then the extras: extra appendages, custom space switches, facial setupThe chain reads top to bottom. Character defines the steps, BaseBiped implements them once for every two-legged character, and a concrete character like Ecko only has to describe how it differs from a base biped. A character could even inherit from another character, so a shared trait was written once and never repeated. When a model changed, the rig rebuilt itself through the same build call, and each class in the chain contributed exactly its own part.
This is the piece that turned a pile of modules into an actual pipeline. The modules were the vocabulary; the class hierarchy was the grammar that let me assemble bipeds, quadrupeds, and robots from one shared foundation without rewriting any of it.
The document
If you want the specifics, here is my final degree project document. The original is in Catalan; the English and Spanish versions were translated with AI to reach a few more people. They are long reads, but everything above is in there in full detail.
The results
And here is what the pipeline was actually for: the four student shorts it rigged, all finished in 2019. Click any of them to watch.
Looking back, the deadline was the best thing that could have happened. It forced a procedural, data-first way of thinking that I would never have reached by rigging those characters one at a time. The toolkit grew up a lot since 2018, but the spine of it is still right here.
