Rig a mesh in Blender: build an armature with clean bone layout and naming, bind the mesh, paint and clean weights, add constraints and IK, add control bones, and test deformation. Use when preparing a character or prop mesh so it can be posed and animated.
./install.sh blender-rigging-basicsname: blender_rigging_basics description: "Rig a mesh in Blender: build an armature with clean bone layout and naming, bind the mesh, paint and clean weights, add constraints and IK, add control bones, and test deformation. Use when preparing a character or prop mesh so it can be posed and animated." metadata: { "includeInPrompt": true }
Take a finished mesh and make it posable: build an armature inside it, bind the mesh to the bones with weights, add the constraints and IK chains an animator expects, and test that the mesh deforms cleanly at the joints. This skill covers rigging only, from a static mesh to a tested rig ready for animation.
Scope:
blender -b --factory-startup -noaudio --python script.py). Every bpy.ops name quoted below exists in that build, and the parent and weight claims marked as verified were run, not recalled. Blender changes operator and property names between major versions, so re-check a quoted name in the Python console of a newer build before relying on it.bpy.ops.object.armature_add, which exists in Blender 5.1.1, or in Python by creating an armature data block with bpy.data.armatures.new and linking an object for it, as the audit script fixture in the Operating Rules section does.head, tail, and roll, all present in 5.1.1, or with the extrude and move tools in the UI. A bone with near-zero length is a defect: it deforms nothing and confuses mirroring.parent property. Use use_connect only where the child head truly sits on the parent tail, such as elbow to wrist. Leave shoulder and hip children disconnected (use_connect off) so the limb root can sit at its own pivot while still inheriting the parent's motion.bpy.ops.armature.calculate_roll exists in 5.1.1 and can align rolls along a chain; whatever method you use, check roll by rotating each bone on its local axes in Pose Mode before binding.display_type property in 5.1.1 accepts OCTAHEDRAL, STICK, BBONE, ENVELOPE, and WIRE. STICK or OCTAHEDRAL are practical defaults for a body rig; ENVELOPE is useful only while checking envelope binding in step 6..L or .R, with the dot, for example forearm.L and forearm.R. Center bones (hips, spine, chest, neck, head) carry no side suffix. This suffix form matters because Blender's side tools key off it: bpy.ops.armature.autoside_names, bpy.ops.armature.flip_names, and bpy.ops.armature.symmetrize all exist in 5.1.1, and symmetrize builds the opposite side from a .L or .R suffix.use_deform off on every control bone. The property exists on both Bone and EditBone in 5.1.1, and the audit script counts non-deform bones separately so the split is visible.ik_hand.L, pole_elbow.L, or a ctrl_ prefix used consistently. An animator opening the file cold should be able to tell which bones are safe to key by name alone.collections in 5.1.1. A workable split is one collection for deform bones, one for body controls, and one for IK targets and poles. Collections let an animator hide the deform skeleton and work with controls only.bpy.ops.object.parent_set. Its type values in 5.1.1 include ARMATURE_NAME, ARMATURE_ENVELOPE, and ARMATURE_AUTO, and the choice sets the starting weights:bpy.ops.paint.weight_from_bones with type AUTOMATIC or ENVELOPES, verified as enum values in 5.1.1. Use that route when the mesh is already parented and only the weights need redoing.bpy.ops.object.vertex_group_normalize_all, bpy.ops.object.vertex_group_limit_total, and bpy.ops.object.vertex_group_clean all exist in 5.1.1 and handle the mechanical part: normalize totals, cap the number of groups per vertex, and drop near-zero weights. A limit of 4 influences per vertex is a common working target for game-bound rigs, but it is a target choice, not a Blender rule; a film rig may allow more.bpy.ops.paint.weight_set to set an exact weight on selected vertices and bpy.ops.paint.weight_gradient for a linear falloff along a stroke; both exist in 5.1.1. Use the paint sample tool (bpy.ops.paint.weight_sample exists in 5.1.1) to read the weight under the cursor before guessing why a vertex moves wrong.bpy.ops.object.vertex_group_mirror exists in 5.1.1. After mirroring, run the audit and spot-check one mirrored joint in a bent pose, because mirroring copies errors as faithfully as it copies good weights.pose_bone.constraints.new(type), and the type values verified in 5.1.1 include COPY_LOCATION, COPY_ROTATION, COPY_SCALE, COPY_TRANSFORMS, LIMIT_LOCATION, LIMIT_ROTATION, LIMIT_SCALE, DAMPED_TRACK, TRACK_TO, CHILD_OF, FLOOR, IK, and SPLINE_IK. The Pose Mode operators bpy.ops.pose.constraint_add and bpy.ops.pose.ik_add also exist in 5.1.1.constraints.new("IK") on that pose bone, or with bpy.ops.pose.ik_add in Pose Mode with the bone selected.target is the armature object, subtarget is the IK control bone, for example ik_hand.L, and chain_count is how many bones back up the chain the IK solves. A leg or arm usually uses a chain count of 2, which covers the two bones above and including the constrained bone's chain segment; a spine or tail uses a count matched to its chain length. Chain count 0 means the solver runs to the root of the chain, which is rarely what a limb needs, because moving the foot would then bend the hips too.pole_target and pole_subtarget on the constraint in 5.1.1).lock_ik_x, lock_ik_y, and lock_ik_z to lock a bone out of bending on an axis, ik_min_x and the matching min and max properties per axis to bound the bend, and ik_stiffness_x and its per-axis siblings to make a bone resist bending. Lock the axes a hinge joint must not use: a knee bends on one axis, and locking the other two stops the solver from twisting the shin to reach a target.custom_shape property, verified present on PoseBone in 5.1.1. Assign simple wireframe shapes (circles for rotations, boxes or arrows for movers) so controls read at a glance and can be grabbed without hunting for a thin bone inside the mesh.Deliver the rigged Blender file plus a rig summary with these sections, in this order:
.L and .R suffixes. Every mismatch is a deformation fault waiting for a pose to expose it, and the audit script exists to find them before that.use_deform stays off on IK targets, pole targets, and handle bones, so hiding or exporting deform bones never takes a control with it and no control creates a stray vertex group.bpy.ops name and property quoted in this skill was checked in Blender 5.1.1 as described in the Purpose section. When working in a different version, confirm a name in that build's Python console before scripting with it, and soften any instruction that depends on a name you could not confirm.Run: blender -b --factory-startup -noaudio --python rig_audit.py -- --make-fixture reproduces the worked example. Run it on a real file with blender -b your_file.blend -noaudio --python rig_audit.py. On the default factory scene with no armature it prints only armatures: 0.
Expected output for the fixture run:
text
armatures: 1
armature: TestRig
bones: 10
deform_bones: 9
non_deform_bones: 1: ik_hand.L
unpaired_side_bones: 1: ik_hand.L
constraint: forearm.L: IK
constraint: hand.L: LIMIT_ROTATION
constraints: 2
bones_with_constraints: 2
bones_without_constraints: 8
ik_without_target: 0: -
skinned_meshes: 1
mesh: FixtureMesh
vertices: 8
vertex_groups: 9: arm.L, arm.R, forearm.L, forearm.R, hand.L, hand.R, hips, orphan_grp, spine
deform_bones_without_group: 1: upperleg
groups_without_bone: 1: orphan_grp
weighted_vertices: 6
unweighted_vertices: 2
non_normalized_vertices: 2
max_deform_groups_per_vertex: 2
The fixture is built to trip every check once: ik_hand.L is a control bone with no .R partner, upperleg is a deform bone with no vertex group, orphan_grp is a group with no bone, vertex 5 carries no weights, vertex 6 is weighted only to the orphan group so it counts as unweighted for deformation, vertex 4 totals 0.8 and vertex 7 totals 1.2 so both count as non-normalized.
rig_audit.py (run inside Blender 5.1.1, uses only bpy and sys). Save this code as rig_audit.py:
python
#!/usr/bin/env python3
"""Audit armatures and skinning in the current Blender scene. Blender 5.1.1.
Run inside Blender, not with system python:
blender -b --factory-startup -noaudio --python rig_audit.py
blender -b your_file.blend -noaudio --python rig_audit.py
Reproduce the worked example in the skill (builds a fixture first):
blender -b --factory-startup -noaudio --python rig_audit.py -- --make-fixture
What it reports, per armature: bone counts, deform and non-deform bones,
paired .L/.R suffix mismatches, constraints per bone and IK constraints
with no target, and for each mesh skinned to the armature by an Armature
modifier: vertex groups, deform bones with no matching group, groups with
no matching bone, and vertex weight totals. Weight totals count only
groups whose name matches a deform bone, because only those deform.
"""
import sys
import bpy
def make_fixture():
bpy.ops.object.select_all(action="SELECT")
bpy.ops.object.delete()
arm_data = bpy.data.armatures.new("TestRigData")
rig = bpy.data.objects.new("TestRig", arm_data)
bpy.context.collection.objects.link(rig)
bpy.context.view_layer.objects.active = rig
bpy.ops.object.mode_set(mode="EDIT")
# name, head, tail, parent, connected, deform
specs = [
("hips", (0, 0, 1), (0, 0, 2), None, False, True),
("spine", (0, 0, 2), (0, 0, 3), "hips", True, True),
("arm.L", (0, 0, 3), (1, 0, 3), "spine", False, True),
("forearm.L", (1, 0, 3), (2, 0, 3), "arm.L", True, True),
("hand.L", (2, 0, 3), (2.5, 0, 3), "forearm.L", True, True),
("arm.R", (0, 0, 3), (-1, 0, 3), "spine", False, True),
("forearm.R", (-1, 0, 3), (-2, 0, 3), "arm.R", True, True),
("hand.R", (-2, 0, 3), (-2.5, 0, 3), "forearm.R", True, True),
("upperleg", (0, 0, 1), (0, 0, 0), "hips", False, True),
("ik_hand.L", (3, 0, 3), (3.5, 0, 3), None, False, False),
]
made = {}
for name, head, tail, parent, connected, deform in specs:
eb = arm_data.edit_bones.new(name)
eb.head = head
eb.tail = tail
eb.use_connect = connected
eb.use_deform = deform
made[name] = eb
for name, head, tail, parent, connected, deform in specs:
if parent:
made[name].parent = made[parent]
bpy.ops.object.mode_set(mode="OBJECT")
ik = rig.pose.bones["forearm.L"].constraints.new("IK")
ik.target = rig
ik.subtarget = "ik_hand.L"
ik.chain_count = 2
rig.pose.bones["hand.L"].constraints.new("LIMIT_ROTATION")
# Mesh: a cube built from explicit data so vertex order is fixed.
verts = [(-1, -1, -1), (1, -1, -1), (1, 1, -1), (-1, 1, -1),
(-1, -1, 1), (1, -1, 1), (1, 1, 1), (-1, 1, 1)]
faces = [(0, 1, 2, 3), (4, 5, 6, 7), (0, 1, 5, 4),
(2, 3, 7, 6), (1, 2, 6, 5), (0, 3, 7, 4)]
mesh_data = bpy.data.meshes.new("FixtureMeshData")
mesh_data.from_pydata(verts, [], faces)
mesh_data.update()
mesh = bpy.data.objects.new("FixtureMesh", mesh_data)
bpy.context.collection.objects.link(mesh)
mesh.parent = rig
mod = mesh.modifiers.new(name="Armature", type="ARMATURE")
mod.object = rig
groups = {}
for gname in ["hips", "spine", "arm.L", "forearm.L", "hand.L",
"arm.R", "forearm.R", "hand.R", "orphan_grp"]:
groups[gname] = mesh.vertex_groups.new(name=gname)
weights = [
(0, "hips", 1.0),
(1, "spine", 1.0),
(2, "arm.L", 0.5), (2, "forearm.L", 0.5),
(3, "arm.R", 1.0),
(4, "hand.L", 0.8),
(6, "orphan_grp", 1.0),
(7, "hips", 0.6), (7, "spine", 0.6),
]
for idx, gname, w in weights:
groups[gname].add([idx], w, "REPLACE")
return rig, mesh
def audit():
rigs = sorted([o for o in bpy.data.objects if o.type == "ARMATURE"],
key=lambda o: o.name)
print(f"armatures: {len(rigs)}")
for rig in rigs:
bones = rig.data.bones
names = sorted(b.name for b in bones)
deform = sorted(b.name for b in bones if b.use_deform)
non_deform = sorted(b.name for b in bones if not b.use_deform)
print(f"armature: {rig.name}")
print(f" bones: {len(bones)}")
print(f" deform_bones: {len(deform)}")
print(f" non_deform_bones: {len(non_deform)}: {', '.join(non_deform) if non_deform else '-'}")
mismatches = []
for n in names:
if n.endswith(".L") and (n[:-2] + ".R") not in names:
mismatches.append(n)
if n.endswith(".R") and (n[:-2] + ".L") not in names:
mismatches.append(n)
print(f" unpaired_side_bones: {len(mismatches)}: {', '.join(sorted(mismatches)) if mismatches else '-'}")
total_cons = 0
with_cons = []
ik_no_target = []
for pb in rig.pose.bones:
if pb.constraints:
with_cons.append(pb.name)
for c in pb.constraints:
total_cons += 1
print(f" constraint: {pb.name}: {c.type}")
if c.type == "IK" and getattr(c, "target", None) is None:
ik_no_target.append(pb.name)
print(f" constraints: {total_cons}")
print(f" bones_with_constraints: {len(with_cons)}")
print(f" bones_without_constraints: {len(bones) - len(with_cons)}")
print(f" ik_without_target: {len(ik_no_target)}: {', '.join(sorted(ik_no_target)) if ik_no_target else '-'}")
meshes = []
for obj in sorted([o for o in bpy.data.objects if o.type == "MESH"],
key=lambda o: o.name):
for m in obj.modifiers:
if m.type == "ARMATURE" and m.object == rig:
meshes.append(obj)
break
print(f" skinned_meshes: {len(meshes)}")
deform_set = set(deform)
bone_set = set(names)
for mesh in meshes:
gnames = sorted(g.name for g in mesh.vertex_groups)
print(f" mesh: {mesh.name}")
print(f" vertices: {len(mesh.data.vertices)}")
print(f" vertex_groups: {len(gnames)}: {', '.join(gnames)}")
no_group = sorted(deform_set - set(gnames))
no_bone = sorted(set(gnames) - bone_set)
print(f" deform_bones_without_group: {len(no_group)}: {', '.join(no_group) if no_group else '-'}")
print(f" groups_without_bone: {len(no_bone)}: {', '.join(no_bone) if no_bone else '-'}")
gindex = {g.index: g.name for g in mesh.vertex_groups}
unweighted = 0
non_normalized = 0
weighted = 0
max_groups = 0
for v in mesh.data.vertices:
total = 0.0
count = 0
for ge in v.groups:
if gindex.get(ge.group) in deform_set:
total += ge.weight
count += 1
max_groups = max(max_groups, count)
if total == 0.0:
unweighted += 1
else:
weighted += 1
if abs(total - 1.0) > 0.001:
non_normalized += 1
print(f" weighted_vertices: {weighted}")
print(f" unweighted_vertices: {unweighted}")
print(f" non_normalized_vertices: {non_normalized}")
print(f" max_deform_groups_per_vertex: {max_groups}")
def main():
args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
if "--make-fixture" in args:
make_fixture()
audit()
return 0
if __name__ == "__main__":
raise SystemExit(main())
publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.