Tech Art Adventures - Building an Auto Rigger for Blender
A lightweight auto-rigging tool for Blender, designed for simplicity and optimised for low to mid-poly models.

Introduction
Rigging is a crucial step in bringing 3D characters to life, but the process can be time-consuming and complex. While Blender provides robust rigging tools, I wanted a simple solution that balances efficiency and accessibility for low-mid poly rigs. To address this, I developed an auto-rigging tool that streamlines the workflow for both beginners and professionals, offering speed without sacrificing flexibility — meaning that this tool could be extended for personal needs.
The Vision
My goal was to create a tool that:
- Generates a complete humanoid rig with one click
- Provides intuitive controls for customisation
- Handles common tasks like mesh binding automatically
- Estimates bone count before generation
- Can be easily extended to specific needs
Implementation Breakdown
1. Property System (The Brain)
The AutoRigProperties class leverages Blender’s PropertyGroup to create a persistent data container that survives between Blender sessions. Each property (like generate_hands) uses update callbacks (e.g., update_bone_count) to trigger real-time UI feedback. This mirrors how Blender’s native tools handle dynamic updates – for instance, changing a subdivision level immediately updates the mesh. The bone_count estimation is intentionally conservative, counting all potential bones even if some might be removed during manual editing later
class AutoRigProperties(bpy.types.PropertyGroup):
generate_hands: bpy.props.BoolProperty(
name="Generate Hands",
default=True,
description="Generate hand bones with fingers",
update=lambda self, context: self.update_bone_count()
)
generate_feet: bpy.props.BoolProperty(
name="Generate Feet",
default=True,
description="Generate foot bones with toes",
update=lambda self, context: self.update_bone_count()
)
bone_detail: bpy.props.IntProperty(
name="Detail Level",
min=3, max=6,
default=4,
description="Number of spine segments",
update=lambda self, context: self.update_bone_count()
)
bone_count: bpy.props.IntProperty(
name="Estimated Bones",
default=0,
description="Estimated total number of bones"
)
def update_bone_count(self):
try:
self.bone_count = calculate_bone_count(self)
except Exception as e:
print(f"Error calculating bone count: {str(e)}")
Under the hood, Blender stores these properties in the .blend file’s DNA. The update_bone_count method uses a try-catch block because property updates can fire during file loading before dependencies are ready. This defensive coding prevents crashes when opening files with the add-on enabled but missing scene data. The bone calculation formula in utils.py uses fixed weights (e.g., 30 bones for hands) because dynamic counting of actual bones would require entering Edit Mode – an operation that can’t run during property updates.
2. Bone Creation Utilities
The create_bone function abstracts Blender’s low-level edit_bones.new() API with sensible defaults. Key details:
- Vector-based positioning: Uses
mathutils.Vectorfor 3D coordinates instead of raw tuples, enabling vector math operations (e.g.,bone.head = parent.tail + Vector((0, 0, 0.2))). - Parenting workflow: The optional
parentparameter handles both root bones (no parent) and child bones, automatically setting the parent’s tail to match the child’s head where logical.
delete_bones uses a list-comprehension-safe approach by iterating over a static collection (armature.data.edit_bones). This avoids the common pitfall of modifying a collection while iterating over it.
def create_bone(armature, name, head, tail, parent=None, roll=0):
bone = armature.data.edit_bones.new(name)
bone.head = head
bone.tail = tail
bone.roll = roll
if parent:
bone.parent = parent
return bone
def delete_bones(armature):
for bone in armature.data.edit_bones:
armature.data.edit_bones.remove(bone)
3. Rig Generation Pipeline
For safety and clean code, every new rig always deletes existing ones. create_rig uses bpy.ops.object.armature_add() instead of direct data creation to ensure all armature defaults (layers, display types) match user preferences.
def create_rig(props):
try:
# Clean existing rig
if "AutoRig" in bpy.data.objects:
old_rig = bpy.data.objects["AutoRig"]
old_rig.select_set(True)
bpy.ops.object.delete()
# Create new armature
bpy.ops.object.armature_add(enter_editmode=True)
armature = bpy.context.object
armature.name = "AutoRig"
delete_bones(armature)
# Core bones
root = create_bone(armature, "Root", (0, 0, 0), (0, 0, 0.1))
pelvis = create_bone(armature, "Pelvis", (0, 0, 0.1), (0, 0, 0.5), root)
# Spine system
spine_bones = create_spine(armature, pelvis, props.bone_detail)
# Limbs
create_limbs(armature, pelvis, props)
bpy.ops.object.mode_set(mode='OBJECT')
return armature
The spine generation uses linear interpolation between fixed Z-axis positions. While simple, this produces more consistent results than curve-based approaches for beginners. The clamping of bone_detail (3-6 segments) prevents extreme values that could make the rig unwieldy. This constraint is usually used and games rarely exceed 6 spine bones for humanoids.
def create_spine(armature, parent, detail):
try:
detail = max(min(int(detail), 6), 3) # Clamp value
positions = [(0, 0, parent.tail.z + i * 0.3) for i in range(detail + 1)]
spine_bones = []
for i in range(len(positions) - 1):
bone_name = f"Spine_{i:02d}"
bone = create_bone(armature, bone_name,
Vector(positions[i]),
Vector(positions[i + 1]),
spine_bones[-1] if spine_bones else parent)
spine_bones.append(bone)
# Neck & Head
neck = create_bone(armature, "Neck",
spine_bones[-1].tail,
spine_bones[-1].tail + Vector((0, 0, 0.2)),
spine_bones[-1])
create_bone(armature, "Head",
neck.tail,
neck.tail + Vector((0, 0, 0.3)),
neck)
return spine_bones
except IndexError:
raise RuntimeError("Failed to create spine - invalid bone positions")
except Exception as e:
raise RuntimeError(f"Spine creation failed: {str(e)}") from e
The explicit mode='OBJECT' fallback in the try-catch ensures Blender never gets stuck in Edit Mode if generation fails.
4. Limb Generation
Limbs presented an interesting challenge - they needed to be symmetrical but customisable. The solution was to generate them in pairs with side prefixes:
def create_limbs(armature, pelvis, props):
try:
for side in ['L', 'R']:
create_arm(armature, pelvis, side, props)
create_leg(armature, pelvis, side, props)
except Exception as e:
raise RuntimeError(f"Limb creation failed: {str(e)}") from e
It uses parameterized side prefix ('L'/'R') with sign-based mirroring (sign = -1 if side == 'L'). This avoids duplicate code while maintaining clear bone naming conventions. The arm system dynamically locates the last spine bone by name sorting (sorted(spine_bones, key=lambda x: x.name)), making it resilient to changes in spine bone count.
def create_arm(armature, pelvis, side, props):
try:
sign = -1 if side == 'L' else 1
# Find last spine bone dynamically
spine_bones = [b for b in armature.data.edit_bones if b.name.startswith("Spine_")]
if not spine_bones:
raise RuntimeError("No spine bones found for arm attachment")
last_spine = sorted(spine_bones, key=lambda x: x.name)[-1]
# Clavicle
clavicle = create_bone(armature, f"Clavicle_{side}",
last_spine.tail + Vector((sign * 0.2, 0, 0)),
last_spine.tail + Vector((sign * 0.4, 0, 0)),
last_spine)
# Arm chain
upper_arm = create_bone(armature, f"UpperArm_{side}",
clavicle.tail,
clavicle.tail + Vector((sign * 0.3, 0, -0.2)),
clavicle)
lower_arm = create_bone(armature, f"LowerArm_{side}",
upper_arm.tail,
upper_arm.tail + Vector((sign * 0.3, 0, -0.2)),
upper_arm)
if props.generate_hands:
create_hand(armature, lower_arm, side)
except Exception as e:
raise RuntimeError(f"Arm creation failed: {str(e)}") from e
5. User Interface
I tried to follow Blenders’s own UI patterns (e.g., Modifiers Panel) to make the UX familiar to users, especially beginners.
class AR_PT_MainPanel(bpy.types.Panel):
bl_label = "Auto Rigger"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Auto Rigger"
def draw(self, context):
layout = self.layout
props = context.scene.ar_props
# Settings Section
box = layout.box()
box.label(text="Rig Settings", icon='SETTINGS')
row = box.row()
row.prop(props, "generate_hands", toggle=True)
row.prop(props, "generate_feet", toggle=True)
row = box.row()
row.prop(props, "bone_detail")
row.label(text=f"Estimated Bones: {props.bone_count}")
# Generation Controls
layout.separator()
layout.operator("ar.generate_rig", icon='ARMATURE_DATA')
layout.operator("ar.resize_rig", icon='FULLSCREEN_ENTER')
# Binding Controls
layout.separator()
layout.label(text="Mesh Binding", icon='MODIFIER')
row = layout.row()
row.operator("ar.bind_mesh", icon='LINKED')
row.operator("ar.unbind_mesh", icon='UNLINKED')
I included several validations to prevent cryptic errors and guide users toward correct usage. Sometimes simple doesn’t mean clear, especially when users are used to more complex multi stage interfaces.
6. Practical Operators
Every operator inherits bl_options = {'REGISTER', 'UNDO'} to ensure workflow reversibility. The execute() methods wrap logic in try-catch blocks with explicit error reports instead of stack traces — similar how Rigify does it.
class AR_OT_ResizeRig(bpy.types.Operator):
bl_idname = "ar.resize_rig"
bl_label = "Resize to Mesh"
def execute(self, context):
try:
rig = bpy.data.objects.get("AutoRig")
mesh = context.active_object
if not rig:
self.report({'ERROR'}, "No AutoRig found - generate one first")
return {'CANCELLED'}
if not mesh or mesh.type != 'MESH':
self.report({'ERROR'}, "Select a mesh object first")
return {'CANCELLED'}
bbox = [mesh.matrix_world @ Vector(c) for c in mesh.bound_box]
height = max(v.z for v in bbox) - min(v.z for v in bbox)
if height <= 0:
self.report({'ERROR'}, "Invalid mesh dimensions")
return {'CANCELLED'}
scale_factor = height / 2.5
rig.scale = (scale_factor, scale_factor, scale_factor)
bpy.ops.object.transform_apply(scale=True)
self.report({'INFO'}, f"Rig scaled by {scale_factor:.2f}x")
except Exception as e:
self.report({'ERROR'}, f"Resize failed: {str(e)}")
return {'CANCELLED'}
return {'FINISHED'}
7. Binding
The “Bind Mesh” operator (AR_OT_BindMesh) automates the process of attaching a mesh to the armature using Blender’s Automatic Weights system.
- Adds an Armature modifier to the mesh (configured to use the rig).
- Creates vertex groups for each bone (named to match bones).
- Calculates weights based on mesh proximity to bones.
class AR_OT_BindMesh(bpy.types.Operator):
bl_idname = "ar.bind_mesh"
bl_label = "Bind Mesh"
def execute(self, context):
try:
rig = bpy.data.objects.get("AutoRig")
mesh = context.active_object
if not rig:
self.report({'ERROR'}, "Generate a rig first")
return {'CANCELLED'}
if not mesh or mesh.type != 'MESH':
self.report({'ERROR'}, "Select a mesh object first")
return {'CANCELLED'}
# Select the rig and make it active
bpy.ops.object.select_all(action='DESELECT')
mesh.select_set(True)
rig.select_set(True)
context.view_layer.objects.active = rig
# Parent with automatic weights
bpy.ops.object.parent_set(type='ARMATURE_AUTO')
# Reselect the mesh as active
context.view_layer.objects.active = mesh
self.report({'INFO'}, f"Successfully bound {mesh.name} to rig")
except Exception as e:
self.report({'ERROR'}, f"Binding failed: {str(e)}")
return {'CANCELLED'}
return {'FINISHED'}
How weights are determined:
Blender uses a heat diffusion algorithm—vertices closest to a bone get ~1.0 weight, fading smoothly across adjacent bones, which works best for humanoid meshes with uniform topology.
Limitations & Troubleshooting
- Auto-weights may fail for complex shapes (e.g., wings). Use manual weight painting or bone envelopes.
- If binding works but deformations are off, apply mesh scale (
Ctrl+A > Scale). - Rarely, old groups can conflict. Purge them in Object Data Properties > Vertex Groups.
Conclusion
- Blender’s API is powerful but quirky because some operations require specific mode changes or selection states.
- The bone count display and operator reports make the tool much more usable, highlighting the importance of user feedback in custom tooling.
- Modular design pays off — Separating the rig into logical components (spine, arms, legs) made the code more maintainable.
Future Improvements
- IK/FK switching
- Facial Rigging
- Preset systems for different character types
- Improve weight painting automation
- Make Mixamo animations compatible
The complete code is available on GitHub for anyone interested in exploring or contributing to the project.