Claude Skill

scenario-blender-rigging

Use when rigging a character, creature or prop in Blender through Python: "rig this character", Rigify metarig fitting or generation errors, automatic weights or "Bone Heat Weighting failed", weight painting fixes, clothes that do not follow, IK/FK switch, foot roll, twist bones,

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download scenario-labs-skills-skills_dcc_blender_scenario-blender-rigging-dc592af.zip · 80 KB
Part of scenario-labs/skills — 97 skills

Install

skills CLI npx skills add https://github.com/scenario-labs/skills/tree/main/skills/dcc/blender/scenario-blender-rigging
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install scenario-labs-skills@llmmart
Git git clone https://github.com/scenario-labs/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole scenario-labs/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Blender rigging

Expert rigging means the rig is specified by bone placement, proven by deformation under stress poses, and shaped around what the animator will actually use. The stance of every source expert: place joints precisely, generate or build the simplest mechanism that works, then pose it, measure it, render it and fix it before calling it done. If a sibling skill named here is missing from your available skills, ask the user to install it (npx skills add scenario-labs/skills --skill <name>); unattended, proceed from tool schemas and flag the gap.

REQUIRED BACKGROUND: scenario-blender-expert (execution channel, review loop, 5.2 API changes).

Module: scripts/bx_rig.py (tested on 5.2.1, headless and GUI). import sys; sys.path.append("<skills>/scenario-blender-rigging/scripts"); import bx_rig as R.

Stance (the expert delta)

  • Placement is the specification. Joints sit in the center of the volume they move, not on the anatomical skeleton ("bones simulate bone, muscle and tissue"); knees and elbows get a slight pre-bend because the chain's curvature IS the IK plane; rolls put each hinge on one local axis so the animator uses one curve (CGDive; Demeter: the bend, not the roll, decided where his pole landed).
  • The metarig is the source, the generated rig is disposable. Every fix goes into metarig bones, Rigify parameters or a post-generation script; regenerating keeps control names, so weights and animation survive (CGDive, Demeter, Ivan Cappiello).
  • Automatic weights fail silently. parent_set returns FINISHED while a warning says bone heat failed; measure unweighted vertices and empty deform groups, and fall back to a voxel proxy plus Data Transfer (CGDive; verified). They also leak across the crotch and armpits: strip .L weights from the right half.
  • Weights are judged in motion, firm beats mushy. Paint in pose or against a test animation; limit each seam to 2 or 3 bones ("bones fighting for influence"); slight intersection is better than rubbery transitions; rigid parts are assigned at 1.0, never painted (CGDive, Demeter, Dikko).
  • Everything that deforms together gets weights from the same source, transferred and applied, so garments cannot drift while animating (Dikko, CGDive).
  • Export decides the deformation tools. Four twist bones per segment instead of Preserve Volume; Preserve Volume, Corrective Smooth and B-bones are Blender-only, which is why Rigify segments its DEF bones (CGDive, Ivan).
  • The animator is the client. Cut features nobody uses, restrict only what breaks the rig, keep automated bones hand-animatable (constraint plus child, Mix Add, Transformation instead of drivers), stop when fit for purpose (Wayne Dixon, Demeter, CGDive).
  • Faces are per-character hybrids. There is no generic stylized face (Demeter); production faces mix joints, ribbons, shape keys and lattices (Rik Schutte).

Establish first

Input Changes Default when silent
Target: Blender render vs game/FBX segmented DEF bones, max 4 influences, no Preserve Volume / Corrective Smooth / B-bone deformation Blender-only keyframe animation
Character: biped, quadruped, mechanical, prop metarig preset or custom samples; rigid islands biped, Rigify
Face need and style none / Rigify face / bones + shape keys + on-face controls body rig + eye and jaw bones only
Fingers Rigify Human (fingers + face) vs Basic Human Basic Human (CGDive: first rig)
Pose, pieces, scale T or A pose, separate eyes/teeth/clothes, real scale measure; fix in the model check
Animator conventions FK/IK defaults, side colors, finger controls legs IK, arms FK, follow 0; left blue, right red (ask)

Choose the path

Rigify Manual / scripted rig
Humans and animals with standard anatomy, many characters sharing controls, fast production-grade result Unusual anatomy, mechanical parts, strict game-engine hierarchies, rigs that must stay small, or when the brief asks for a custom control scheme
add_metarig then fit_metarig, generate new_bone, twist_chain, fk_ik_switch, ik_chain, foot_roll, make_def_layer, organize
Metarig types: arm exactly 3 bones, leg 4 + heel, spine 3+, super_copy 1 bone DEF / MCH / CTRL: only DEF deforms, MCH is never animated, everything else is a control

Workflow

  1. Model check. R.model_check([body, *parts], fix=True): transforms applied (shape keys included), feet on Z=0, centered on X, facing -Y, about 1.8 m for an adult human. Apply Mirror modifiers or keep their mirror object. GATE: model_check(...)["problems"] == [].
  2. Landmarks. lm = R.landmarks(body, eyes=eyes): cross-sections find crotch, hips, ankle, neck, armpit, wrist; eye centers from the eye mesh; nose tip and chin from the face profile. Override any joint with overrides={...}. GATE: joint order sane, lm["notes"] empty or explained.
  3. Fit. Rigify: meta = R.add_metarig("basic"|"human"), R.fit_metarig(meta, lm) (joints as shared points, pre-bends, hinge rolls, left side symmetrized; hands and face by similarity, coarse). Manual: build the deform skeleton with new_bone from the same landmarks. GATE: R.check_metarig(meta, body) == [] AND R.placement_sheet([body, eyes], out, arm_obj=meta, views=("front","right","top")) looked at; zoom the head with center=, size=.
  4. Face (Rigify Human). Fit, then R.upgrade_face(meta) (irreversible; eyelid follow Z 0.3, bottom lip 0.9, Overwrite Widget Meshes on). Stylized hero face: bones for jaw, eyes, tongue plus shape keys instead (stage 7).
  5. Generate or build. rig, err = R.generate(meta); err is the first Rigify error line naming the bone ("Cannot connect chain - bone position is disjoint", "Expected 2 eye corners"): fix the metarig, regenerate. Manual: twist chains, FK/IK layers with settings properties, IK with solved pole angle, foot roll, symmetrize, mirror_drivers, make_def_layer, organize, widgets. GATE (manual): static_cycles == [], cycle_check() == [], root_test < 1e-4, FK and IK coincide at rest, pole error < 1e-3.
  6. Bind. rep = R.bind(rig, body, garments=[shirt, pants], rigid={eyes: None, teeth: "DEF-teeth.T"}): auto weights, verification, proxy fallback, cross-side strip, garment transfer (applied), rigid islands, clean and normalize, Armature after Mirror and before Subdivision. GATE: rep["ok"], unweighted == 0, no non-deform groups; for export max_influences=4.
  7. Range of motion. dr = R.deformation_report(rig, [body, *garments], R.RIGIFY_POSES), R.flag_report(dr), then R.pose_sheet(...) and R.joint_sheet(...) close-ups and LOOK at them. GATE: no leaks, no unexplained collapse, renders acceptable at armpit, elbow, wrist twist, groin, knee, collar.
  8. Fix, in this order. Placement or roll problems: back to the metarig and regenerate. Weights: targeted data edits (strip_opposite_side, assign_rigid, smooth_weights with a vertex mask, clean_weights). Volume at deep bends: corrective shape keys (volume_restore_targets, corrective_from_targets, corrective_driver). Blender-only candy wrapper: preserve_volume_mask. Re-run stage 7 after every fix; re-check earlier poses (CGDive).
  9. Usability and delivery. Controls visible with In Front off, widgets, side colors, MCH/DEF hidden, at least one Rigify UI row, rig_ui.py needs Auto Run in a GUI, switches exactly 0 or 1, rest pose clean, test action removed. Report numbers, renders and what was not verified.

Numbers

Value Relative to Source
Knee/elbow pre-bend 1 to 3 deg (fit: 1.2% of limb length, 2.7 deg); check 1 to 15 chain in the bend view CGDive; check [added]
4 twist bones, Copy Rotation 1.0 / 0.66 / 0.33 / 0.1 (thigh 0.05) from the driving end CGDive
IK chain length 2; IK Stretch 0.1; lock IK Y/Z on shin and forearm IK bone Wayne, CGDive
Foot roll: heel X [-90, 0], toe X [0, 90], inside/outside Z one-sided pivot local, roll 0, pointing up CGDive (verified)
Weight brush Add, strength 0.1 to 0.2, Auto Normalize; seams Mix 0.5 at strength 1 Weight Paint CGDive
Clean near-zero weights before blurring (Demeter); limit 0.001 (CGDive), 0.01 in bind [added]; 4 influences for games per vertex Demeter, CGDive
Eyelid follow 0.15 (bone face); Rigify eyelid follow Z 0.3 (default 0.7); jaw bottom lip 0.9; mouth corners follow jaw 0.5 face CGDive
Palm 0.5 / 0.25; elbow plate 0.2 of forearm rotation Copy Rotation Local, Mix Add CGDive
Finger curl: Transformation Scale Z 0..1 to Rot Z 90..0 deg, Extrapolate finger control CGDive
Action constraint: keys frames 1 and 10, LINEAR, max = measured control travel control local Y CGDive, Demeter
Shoulder test range about 20 to 30 deg of clavicle raise anatomy CGDive
Face: about 16 Stop Staring shapes; 52 ARKit for mocap shape keys CGDive
Proxy voxel H/180, then H/90, H/45 character height [added, verified]
Collapse < 0.85, bulge > 1.25 (joint band), leak > 0.001 H, strain 0.5 to 1.6 rest [added]

Quality gates

Code (all in bx_rig): check_metarig, weight_report (unweighted, empty_major, non_deform_groups, over4, sum_dev, mirror_err), deformation_report + flag_report (strain, joint volume bands, cross-side leak, new self-intersections via bx_audit), static_cycles, cycle_check, root_test, twist_angles, solve_pole_angle, rigid_check.

Visual: placement sheet (front, right, top; head zoom) before generating; pose sheet of RIGIFY_POSES (front, right, three-quarter); joint close-ups (arm joints front/top/three-quarter, others front/right/three-quarter) at every flagged pose. Look for armpit collapse, pointy or pinched elbows and knees, candy-wrapper wrists, groin and butt volume, collar clipping, garments sliding or intersecting, teeth and eyes lagging, widgets hidden inside the mesh. Numbers passed while a render showed broken arms or crease spikes during testing: always open the render. Rubric: references/critique.md.

Common mistakes

Mistake What it looks like Fix
Metarig scale or pose not applied generated rig differs in size or pose apply scale, Apply Pose as Rest Pose
Neck moved without the spine tip "bone position is disjoint" move joints as units (move_points)
Straight knee or elbow IK bends sideways or backward pre-bend in the bend view
Parented to the metarig mesh follows nothing useful bind to the generated rig
Heat failure unnoticed mesh does not move weight_report; proxy route
Armature below Subdivision, or before Mirror slow; mirrored half copies the posed half armature_first
Left thigh weights on the right leg opposite side moves strip_opposite_side
Eyes and teeth auto-weighted drift, poke through assign_rigid
Live Data Transfer on clothes weights change while animating apply it
Driver on an animatable bone channel cannot be keyed Transformation or Copy Rotation Mix Add
Inherit Rotation off for isolation root rotation breaks the limb follow bones (World Copy Loc/Rot/Scale)
IK target parented inside its chain jitter, flips clear parent; static_cycles
Re-parenting a connected bone head jumps to the new parent's tail reparent clears use_connect first
Symmetrize expected to copy drivers; .L.001 names right side dead mirror_drivers; side_of
IK/FK left at 0.86 mixed, unreadable deformation exactly 0 or 1

Blender 5.2 notes

  • Rigify ships disabled on factory startup (enable_rigify); the Human metarig still has the legacy face (upgrade it); Overwrite Widget Meshes still defaults off.
  • Bone collections and per-bone colors replace layers and bone groups; edit_bones.new() joins no collection; pose selection is pose.bones[n].select.
  • parent_set appends the Armature modifier last; weight operators (vertex_group_smooth, _clean) need Weight Paint mode headless; bx_rig does clean, normalize and limit on data instead.
  • shape_key_add leaves new keys at value 1.0 in 5.2.1: set 0 before driving [verified]. VertexGroup.remove with out-of-range indices crashes 5.2.1 [verified].
  • Setting a parent's tail moves a connected child's head; EditBone.transform on a connected group double-moves shared ends [verified]: use transform_bones.
  • Python drivers start with keys (0,0),(1,1); remap them (remap_driver). Action constraints pick the action slot automatically. Only Insert Available is on by default; keyframe_insert from Python is unaffected.
  • Custom shapes: Affect Gizmo (use_transform_at_custom_shape), Use As Pivot (use_transform_around_custom_shape). 5.2 adds Duplicate and Rename and Add Bone align Axes.

References

  • references/procedures.md: full tested code for each stage and the test script behind it; load before writing rig code.
  • references/expert-notes.md: judgment by expert (placement, Rigify internals, weighting, mechanisms, faces) with timestamps; load when a decision is not covered above.
  • references/critique.md: the review rubric; load at every gate.
  • references/sources.md: videos, experts, best timestamps.
Files (skills)
  • references
    • critique.md 12.8 KB
      # Critique rubric: scenario-blender-rigging
      
      Run at every gate. Severity: **blocker** (rig unusable or silently wrong: fix before anything else), **major** (an animator or a lead would send it back), **minor** (polish; note it in the report). Numbers come from `bx_rig`; visual items need the named render opened with the image reader. Thresholds marked [added] are the skill author's calibration on the test mannequin and Snow, not expert numbers.
      
      ## A. Model and placement (before generating or binding)
      
      | Check                    | Measure                                                                         | Pass                                                                                                                 | Severity                   | Source          |
      | ------------------------ | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------- | --------------- |
      | Model check              | `model_check(objs)["problems"]`                                                 | empty; about 1.8 m adult, feet Z=0, faces -Y                                                                         | blocker                    | CGDive          |
      | Joints in the volume     | `check_metarig(meta, body)` "outside the mesh"; placement sheet front/right/top | spine, limbs, neck inside; spine near the body middle in side view                                                   | major                      | CGDive, Demeter |
      | Knee and elbow pre-bend  | `bend_angle`; `check_metarig`                                                   | 1 to 15 deg, knee forward, elbow back, straight in the other view                                                    | blocker (IK direction)     | CGDive, Demeter |
      | Hinge roll               | `check_metarig` "hinge not on local X"; `hinge` test pose bends the right way   | dot >= 0.98                                                                                                          | major                      | CGDive          |
      | Chain integrity          | `check_metarig` "disjoint" and glue checks                                      | none                                                                                                                 | blocker (generation fails) | CGDive, Ivan    |
      | Finger fist test (Human) | hinge every phalanx on local X in a render                                      | clean fist, knuckles aligned, thumb curls in                                                                         | major                      | Demeter         |
      | Face fit (Human)         | head-zoom placement sheet                                                       | lids around the eyeball, lip bones mid-lip, corners coincident, jaw pivot at the hinge, nose and chin on the surface | major                      | CGDive          |
      | Metarig scale and pose   | `check_metarig`                                                                 | scale 1, pose = rest                                                                                                 | blocker                    | CGDive          |
      
      ## B. Rig structure and usability
      
      | Check                         | Measure                                                                                              | Pass                                                              | Severity                 | Source         |
      | ----------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------ | -------------- |
      | Generation                    | `generate()`                                                                                         | no error; metarig hidden; rig named consistently for regeneration | blocker                  | CGDive         |
      | Deform hygiene                | only DEF bones deform; `weight_report()["non_deform_groups"]`                                        | empty                                                             | major                    | Wayne, CGDive  |
      | Dependency cycles             | `static_cycles`, `cycle_check`                                                                       | none                                                              | blocker ("not optional") | CGDive         |
      | Root follows everything       | `root_test`                                                                                          | error < 1e-4                                                      | blocker                  | CGDive         |
      | IK                            | chain 2, pole angle error < 1e-3, target not in its chain, IK Y/Z locked on realistic hinges         | yes                                                               | major                    | CGDive, Wayne  |
      | FK/IK                         | chains coincide at rest (< 1e-4); switches exactly 0 or 1; defaults set in the property UI           | yes                                                               | major                    | CGDive         |
      | Drivers                       | all `is_valid`; left and right counts equal after Symmetrize                                         | yes                                                               | major                    | CGDive         |
      | Controls visible and readable | render with In Front off; widgets outside the mesh, side colors, MCH/DEF hidden, UI row set (Rigify) | yes                                                               | minor to major           | CGDive, Wayne  |
      | Manual control kept           | automated bones still keyable (constraint plus child, Mix Add, Transformation)                       | yes                                                               | major                    | CGDive         |
      | Fit for purpose               | every feature is one the brief or animator needs                                                     | nothing gratuitous                                                | minor                    | Wayne, Demeter |
      
      ## C. Weights (numbers)
      
      | Check           | Measure                                                                                    | Pass                 | Severity                           |
      | --------------- | ------------------------------------------------------------------------------------------ | -------------------- | ---------------------------------- |
      | Heat failure    | `weight_report`: `unweighted`, `empty_major`                                               | 0, []                | blocker                            |
      | Cross-side leak | `deformation_report` leak on one-sided poses                                               | < 0.001 H [added]    | major                              |
      | Normalization   | `sum_dev`                                                                                  | 0                    | minor in Blender, major for export |
      | Influences      | `max_influences`, `over4`                                                                  | <= 4 for game export | major (export)                     |
      | Rigid parts     | each rigid island exactly one group at 1.0; `rigid_check` < 1e-4                           | yes                  | major                              |
      | Garments        | weights from the same source as the body, Data Transfer applied, no Armature before Mirror | yes                  | major                              |
      | Symmetry        | `mirror_err` on symmetric meshes                                                           | < 0.05               | minor                              |
      | Stack           | Armature after Mirror, before Subdivision / Corrective Smooth                              | yes                  | major                              |
      
      ## D. Deformation (numbers guide, renders decide)
      
      Run `deformation_report` + `flag_report` on `RIGIFY_POSES` (or the rig's own library), then `pose_sheet` and `joint_sheet` at every flagged pose. Joint band ratios below are [added] calibrations: linear skinning on a clean mesh measured 0.47 (arm down, shoulder), 0.68 (elbow 120), 0.73 (knee 120).
      
      | Pose                           | Look for (render)                                                 | Numbers                                  | Severity if bad         | Expert          |
      | ------------------------------ | ----------------------------------------------------------------- | ---------------------------------------- | ----------------------- | --------------- |
      | Arm down                       | chest caving, armpit crushed ("This is hideous")                  | shoulder band < 0.6                      | major                   | CGDive          |
      | Arm up + clavicle 20 to 30 deg | deltoid bulge, shirt sleeve lifting off, back of shoulder tearing | bulge > 1.25, garment self-intersections | major                   | CGDive          |
      | Elbow / knee 120               | pointy or collapsed joint, inner crease interpenetration          | band < 0.85                              | major (fix: corrective) | CGDive          |
      | Wrist twist 80                 | candy wrapper, sudden twist at the wrist                          | band < 0.9                               | major                   | CGDive          |
      | Leg forward / side             | groin collapse, butt loses volume, other leg moving               | leak, band                               | major                   | CGDive          |
      | Squat (IK feet planted)        | feet sliding or sinking, knee popping, pants intersecting         | foot IK displacement 0                   | major                   | CGDive          |
      | Spine twist / bend             | waist gaps, jacket hem breaking                                   | strain outside 0.5 to 1.6                | minor to major          | Demeter         |
      | Head turn / nod                | collar clipping, face dragged by neck weights, teeth poking       | new self-intersections                   | major                   | CGDive          |
      | Jaw open (face)                | chin distortion, teeth and tongue following                       |                                          | major                   | CGDive, Demeter |
      | Blink, smile                   | lid zigzag, creepy smile, lower lid over-following                |                                          | major                   | CGDive          |
      | Fist (Human)                   | knuckles misaligned, thumb through the palm                       |                                          | major                   | Demeter         |
      
      Firm is acceptable: slight intersection with firm deformation beats rubbery weights (CGDive). A pose outside the anatomical range is not a failure (CGDive: test the shoulder within about 20 to 30 deg).
      
      ## E. Correctives and faces
      
      | Check                                                                                                                                      | Pass     | Severity       |
      | ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -------------- |
      | Corrective 0 at rest, 1 at the extreme, partial and late in between (e.g. 0.4 at 70 deg for a 40 to 115 deg ramp)                          | yes      | major          |
      | Corrective driven from bones valid in IK and FK; ball joints from a swing-only bone                                                        | yes      | major          |
      | Corrective render clean at the pose (no crease spikes, no bump outside the joint)                                                          | yes      | major          |
      | Mirrored side exists and fires only on its side                                                                                            | yes      | major          |
      | Face shapes: each 0 at rest, full travel gives 1, opposing keys never both on, L + R at 1 equals the symmetric shape, split seam invisible | yes      | major          |
      | Combinations tested (smile + jaw open, blink + squint)                                                                                     | rendered | major          |
      | On-face controls: locked channels, Limit Location with Affect Transform, depth on its own control for stylized faces                       | yes      | minor to major |
      
      ## F. Report honesty
      
      - State the route taken (auto weights or proxy, and why), the numbers from `weight_report` and `flag_report`, and link the placement, pose and joint sheets.
      - Say what was coarse or unverified: face fit from similarity, fingers not individually fitted, poses outside the tested library, GUI-only checks (rig UI panel with Auto Run, real brush painting).
      - Never call a rig done without having opened a pose sheet and the joint close-ups.
      
    • expert-notes.md 27.8 KB
      # Expert notes: scenario-blender-rigging
      
      Judgment by expert, with source video id and timestamp. Experts: CGDive channel host (Rigify full course FhF54DZY34U, 2026 courses Ldki5txSn5U / FzQVpzXcFX0 / A147p_zDwO4, "Rigging Isn't Scary" lessons), Demeter Dzadik (Blender Studio rigger, CloudRig, f6im_0QVeuo), Rik Schutte (animator, Sony Imageworks, Blender Studio, _Ov0pFIOpug), Ivan Cappiello (former Rigify maintainer, LLnXBvEnG7o), Wayne Dixon (CG Cookie, AymxOGtJoew), Dikko (ljW9YJeA79E). [added] marks the skill author's additions; [verified] facts were run in Blender 5.2.1.
      
      ## 1. Placement, roll, hierarchy
      
      - **Model check first.** Centered at the origin, feet on the floor, facing -Y, about 1.8 m, transforms applied: "A huge number of rigging problems people ask me about are simply due to skipping the model check" (CGDive A147p_zDwO4 [00:03:17]). Two kinds of symmetry: general and true (vertex for vertex); Snap to Symmetry only on parts meant to be symmetric, never on an intentionally asymmetric body (Q21AZnAyOJA [00:07:03] to [00:08:03]). The armature is created at the origin and never moved in Object Mode while rigging, or Symmetrize breaks (FzQVpzXcFX0 [00:31:47]).
      - **Bones stand for bone, muscle and tissue.** Place them in the center of the volume they move; anatomy is a starting point (FzQVpzXcFX0 [00:05:34]). Spine pulled toward the middle of the body in side view instead of the S-curve (q-PRGvE1ico [00:05:02]); spine lengths evened out afterwards (A147p_zDwO4 [00:46:59]). Clavicle bone high and deep, buried between clavicle, traps and scapula (FhF54DZY34U [00:52:56], q-PRGvE1ico [00:07:29]); upper-arm head deep in the socket below the clavicle (q-PRGvE1ico [00:08:21]); head pivot "just below the ear" (q-PRGvE1ico [00:06:46]); finger bases at the knuckle line, deeper than the palm folds (QnCo0hzXKeQ [00:00:00]).
      - **The bend is the IK specification.** Knees slightly forward, elbows slightly back, straight in the other view (CGDive FhF54DZY34U [00:50:42]); Demeter exaggerated Sintel's elbow bend until Rigify's pole landed behind, because roll changes did not fix the bend direction (f6im_0QVeuo [00:11:17], [00:11:49]). "In rigging, it is easier to bend something than to unbend it": straighten a drooping tail or close a mouth in the model before rigging (FhF54DZY34U [02:01:06]).
      - **Roll is an animation-curve decision.** One local axis per hinge so the animator uses one F-curve ("your animator is going to hate you", q-PRGvE1ico [00:13:35]); rotating a new bone 180 deg in Front vs Side view gives opposite axes, so always look at the axes (q-PRGvE1ico [00:15:23]); fingers and thumb must curl on the same axis, roll the thumb 90 then 180 if it curls outward (QnCo0hzXKeQ [00:02:38]). Demeter's finger test: rotating each phalanx on local X must roll a clean fist with aligned knuckles, "regenerating the rig a million times" (f6im_0QVeuo [00:12:54]).
      - **Hierarchy.** Every bone under one world-oriented root; audit by moving, rotating and scaling it (_6eyk1vembQ [00:22:01]). Disconnected parents are fine when connecting would move a pivot (QnCo0hzXKeQ [00:06:20]). Duplicated bones inherit their parent: clear parents and redo when confused (PdTOfJDdzgc [00:10:18]).
      - [added, verified] Connected bones: setting a parent's tail moves the child's head; clear `use_connect` before re-parenting or the head jumps to the new parent's tail.
      
      ## 2. Rigify (CGDive, Demeter, Ivan Cappiello)
      
      - **What Rigify is.** Building blocks, not one click: rig types assembled in a readable metarig that stores what to build and how pieces join (Ivan LLnXBvEnG7o [00:05:21]). The rig type sits on the first bone of a connected chain; names do not matter, connected parenting does (CGDive [02:41:21]); a chain continues until a branch, so the neck needs its own type or the head is swallowed by the spine (Demeter [00:09:07]). Name-based rigging is fragile ("never do that", Ivan [00:34:23]).
      - **Choice of metarig.** Basic Human for a first rig (the full Human "is actually quite complex"), Human for fingers and face, Wolf for dogs, Basic Quadruped has no face or fingers (CGDive [00:48:27], [02:01:39], [02:05:31]). Pick leg types by anatomy: "you cannot rig a horse leg with the human arm" (Ivan [00:24:40]). Keep fitted metarigs in a library .blend and append them for similar characters; animation transfers because control names stay stable (Ivan [00:08:13], [01:13:24]).
      - **Fitting rules.** Scale the metarig in Object Mode, apply scale; fit arms and fingers in Pose Mode and Apply Pose as Rest Pose, otherwise the generated rig differs in size or pose (CGDive [00:49:33], [01:14:47]). Move joints as units: "Always box select over their connection and move them like that. Never move them individually" (CGDive [01:16:24]). Face order: move the `face` parent in Y/Z only, center line first, surface bones with Snap to Face, lids aligned vertically, lip bones in the middle of the lip, jaw pivot "extremely important", eye bone centered on the eyeball's middle loop (CGDive [01:13:41] to [01:26:27]). Breast and pelvis bones only help automatic weights; delete them if you will paint the chest (CGDive [00:53:31]).
      - **Face upgrade.** The Human metarig still ships the legacy monolithic face; Upgrade Face Rig after fitting, before generating; irreversible; extra lip bones only after it (CGDive [01:25:22], [02:21:50]; still true in 5.2.1 [verified]). Glue bones only work on exact overlaps with another control's head or tail (Ivan [00:50:25]).
      - **Generation errors.** Almost all come from a joint that got separated: "bone position is disjoint" (neck or tail off the spine), "mouth corner counts differ" (lip ends apart), UI rows all 0, broken collection references, a limb generating nothing because delete-then-undo stripped its rig type (restore with Copy Type & Parameters) (CGDive [00:57:37] to [03:48:17]). "That should take care of about 90% of possible generation issues" (CGDive [02:23:42]).
      - **Tuning lives on the metarig.** The generated rig is overwritten on regeneration: eyelids follow Z about 0.3, jaw Bottom Lip Influence 0.9, inner mouth chains spherical falloff off with falloff 0.4, B-Bone segments 1 on a distorting chin bone, Overwrite Widget Meshes on, eye bone size sets the eye target size and distance (CGDive [01:54:30] to [01:57:20]). Cosmetic tweaks with no parameter: a post-generation script (Demeter [00:31:30]). Roll the metarig bone to fix a widget's orientation (CGDive [03:57:18]).
      - **Face weaknesses out of the box** (CGDive [01:52:46]): jaw opening without a nice expression until the pivot is right, hard to make a non-creepy smile, eye target too small and close, lower lids following the eye too much.
      - **Organization.** Each component on its own collection, FK and tweak collections on the first bone of each component, a collection named `DEF` in the UI gives a deform-bones button for painting; for colors the top-most colored collection wins (CGDive [03:39:21], [03:55:07], [04:05:18]). Colors: Rigify's function colors, or left red / right blue / center yellow, "popular among professional animators"; ask the animator (CGDive [04:09:21], [04:13:46]). Demeter disables unified selection colors (f6im_0QVeuo [00:07:31]).
      - **Studio iteration.** Generate early and often, toggle metarig and rig, pose constantly (Demeter [00:05:53], [00:11:49]). Cut what animators do not use: fist controls and IK fingers went (Demeter [00:14:33]). Hinge options that let the head stay upright while the torso rotates: "animators tend to ask for this" (Demeter [00:16:41]).
      - **Pose-driven setups.** Rigify's action slots (ported from CloudRig): pose deform bones for "lips wide" in an action keyed at frames 0 and 10 with LINEAR interpolation, "you want it to be linear to what the animator is doing"; a Corrective slot with two triggers fixes combinations (Demeter [00:47:16], [00:52:14]).
      - **Why segmented DEF bones.** Bendy bones are "a crazy mind-blowing feature" but do not export; segmented deform bones behave similarly and export (Ivan [01:04:42]).
      
      ## 3. Manual rig architecture (CGDive, Level 3)
      
      - **Three classes.** DEF (deform), MCH (mechanism, never animated), controls (everything else; no CTRL prefix, Wayne [00:08:24]). Separate DEF completely once deform bones are final: duplicate deformers as DEF-, one Copy Transforms each to the original (now a "connective" bone, never moved afterwards), deform flags become trivial, weight painting shows only DEF bones, and the DEF hierarchy can be rebuilt for engines (twists 2 to 4 under twist 1, hand under the first forearm twist) (A147p_zDwO4 [03:24:48] to [03:46:27]).
      - **Layered limbs.** A main MCH chain with twists follows an FK or an IK copy through two Copy Transforms named FK and IK, the IK influence driven by a 0/1 property; mechanisms can be added per layer without touching the others (A147p_zDwO4 [01:10:56], [01:17:56]). Automate on the shared MCH layer so elbow caps and correctives work in both modes (A147p_zDwO4 [05:59:00]).
      - **Twist chains.** 1 twist not enough, 2 insufficient, 3 nice, 4 really nice, more than 5 invisible; Copy Rotation 1.0, 0.66, 0.33, about 0.1 from the driving end (thigh 0.05); forearm twists copy the hand Local/Local then Damped Track to it, because Damped Track never adds roll and so strips swing and keeps twist (A147p_zDwO4 [00:23:19], [00:29:33]; HhhSjGqetjo [00:07:56]). Upper-arm twists parented to the clavicle, Copy Location above Damped Track (Head/Tail 1), chained Copy Locations after disconnecting (connected parenting overrides Copy Location) (FzQVpzXcFX0 [00:50:11], Q21AZnAyOJA [00:35:38]).
      - **Spine.** Keep deform bones in logical orientation; flip copies as hips and chest controls, Copy Rotation Local/Local Mix Add to curl, torso parents both, torso under root (A147p_zDwO4 [00:49:17] to [00:57:21]).
      - **IK.** Target fully unparented from the chain (a disconnected-but-parented target is a dependency loop), chain 2, pole offset and parented to the target for legs (a foot twist twists the leg) or to the clavicle for arms (hand rotation should not swing the elbow); pole angle is right when toggling IK does not move the rest pose (-89.1 deg on his arm); IK ignores Limit Rotation and locks, so lock IK Y/Z on shins and forearms (QnCo0hzXKeQ [00:09:15]; A147p_zDwO4 [02:27:17], [02:28:27], [01:59:15]). Arm IK target = a copy of the hand (same orientation) and the IK hand copies its transforms (A147p_zDwO4 [02:25:06]). IK where limbs touch something, FK where they swing (A147p_zDwO4 [00:58:37]).
      - **Foot.** Six pivots under one roll control; pivots meant to be used one at a time, enforced by Copy Rotation plus one-sided Limit Rotation; the ball stays manual; the toe stays flat through a Damped Track helper with the animatable toe parented under it ("constraint plus child"); the roll control parented to the ankle control, never under a pivot it drives (A147p_zDwO4 [01:47:24] to [01:57:35]; PdTOfJDdzgc [00:13:10]).
      - **Limb isolation.** Never Inherit Rotation off (it ignores the whole hierarchy including root); use a parent MCH (child of pelvis/clavicle) and a follow MCH (no parent) with World Copy Location/Rotation/Scale, the property driving only Copy Rotation; Copy Scale keeps root scaling (A147p_zDwO4 [01:40:44], [02:07:08], [02:09:53]). Defaults he ships: legs IK, arms FK, follow 0 (A147p_zDwO4 [02:36:25]).
      - **Hands.** Finger control: rotate = rotate, scale = curl (Rigify convention); base phalanx Copy Rotation Mix Add (parenting would scale the finger); middle and distal phalanges Transformation constraint Scale Z 0..1 to Rotation Z 90..0 with Extrapolate, because a driver would lock manual rotation (_6eyk1vembQ [00:08:07] to [00:12:42]). Palm bones 0.5 and 0.25 from both outer palms; "5 deg is already kind of extreme", no limits (_6eyk1vembQ [00:17:58], [00:20:14]).
      - **Custom properties** on a pose bone of a dedicated settings bone (visible in the N panel in Pose Mode), step 1, exactly 0 or 1 in production; renaming a property breaks every driver path (A147p_zDwO4 [01:24:04], [01:36:25], [04:58:05]).
      - **Symmetry.** Build one side, Symmetrize; after renames delete the other side and resymmetrize the whole limb; Symmetrize does not copy drivers; pose-mode X mirror is name-based, edit-mode X mirror enforces geometry (ErR4RysRy0g [00:41:19]; _6eyk1vembQ [00:44:39]). Delete plus a single undo can bring bones back without constraints and properties: undo further and verify, undo steps 256 (ErR4RysRy0g [00:39:18]; untested in 5.2).
      - **Widgets.** Wire meshes in a hidden collection, local Y = bone length, wire width 2, shape where the animator expects to grab (foot rectangle at the sole), Override Transform to draw chest and hips shapes at the deforming bones, move the bone (pole distance, settings location) rather than offsetting the shape (aj_ACAE7K8I; doRG4Sko7Kg [00:45:12], [00:49:11], [00:51:00]).
      
      ## 4. Weighting
      
      - **Recipe.** Blend Add, weight 1, strength about 0.2 (0.1 to 0.15 for blending), Auto Normalize always ("probably the most important option"), mirror only on truly symmetric meshes, Front Faces Only off, Zero Weights Active; click add, Ctrl subtract, Shift blur; "weight painting is just a lot of clicking" (CGDive tNW8uCs77MQ [00:02:06], 6zGVAStZP50 [00:14:40]; Demeter uses only Add, Subtract, Blur [00:23:46]).
      - **Judge in pose.** Demeter paints with the character posed (f6im_0QVeuo [00:24:51]); CGDive scrubs a test animation (stride, full split, back kick, crouch, arm down/forward/up with the shoulder raised about 20 to 30 deg) and re-checks earlier poses after every change (rf3NDKXYpTk [00:13:25], [00:23:28], [00:53:23]); Dikko keys a test animation (ljW9YJeA79E [00:42:16]).
      - **Where auto weights fail.** Waist (Demeter [00:20:33]), torso under the arm and chin (CGDive Ldki5txSn5U [00:28:13]), armpit/chest, crotch "red pants", heel, collar (6zGVAStZP50). Chest compression came from arm twists leaking onto the torso (rf3NDKXYpTk [00:28:02]). The upper arm has no business on the lower torso (CGDive FhF54DZY34U [01:34:30]).
      - **Principles.** Hard over soft: "having your weights a little bit harder, which creates intersection between the geometry, is better than having too smooth weights" (FzQVpzXcFX0 [01:43:01]). Limit each area to 2 or 3 bones: "multiple bones are fighting for influence" (rf3NDKXYpTk [00:20:27]). Lock finished groups, mask the work area (rf3NDKXYpTk [00:16:21]). Exact seams between overlapping meshes with a Mix brush at 0.5, strength 1 (0.75 neck / 0.25 chest also good) (rf3NDKXYpTk [00:34:02], [00:46:47]). Join meshes to paint across them; weights survive join and separate (rf3NDKXYpTk [00:43:11]).
      - **Cleanup habits.** Near-zero weights of far bones make blur explode: Clean Vertex Groups, then blur with the deform-bones subset (the default smooths only the active group, "pretty much always not what you want") (Demeter [00:27:03], [00:39:10]). Smooth does nothing where a bone has zero influence: add a little first (CGDive FhF54DZY34U [02:36:28]). Levels with a negative offset hardens, then Normalize All with Lock Active off (BajO2vgto80 [00:28:23]).
      - **Silent failure.** "Bone Heat Weighting: failed to find solution for one or more bones" appears only in the status bar and the mesh does not move (CGDive FhF54DZY34U [01:29:49], BajO2vgto80 [00:00:33]). Fixes: scale 100x and re-weight (CGDive [02:24:16], partial on a bad mesh [verified]); a low-poly cage with a span at every joint, weighted and transferred (Dikko [00:23:01]); rigid parts assigned manually (BajO2vgto80 [00:01:24]); Voxel Heat Diffuse add-on (paid, merges close appendages; Dikko [00:45:32]). [added, verified] Voxel-remeshed proxy + Data Transfer, coarser voxels if the source has holes.
      - **Clothes and accessories.** Auto weights on separate pieces fail by proximity (bands slide, rings slide, hands pull the shirt); transfer from the same cage or body to every piece, apply the Data Transfer, smooth all pieces with the same settings (Dikko [00:35:34] to [00:43:54]; CGDive whiskers FhF54DZY34U [02:30:39]). Detail meshes riding the body: Surface Deform (Demeter [00:02:09]).
      - **Rigid parts.** Eyes, teeth, lashes, hard-surface islands: Remove from All Groups, assign 1.0 to one bone (CGDive FhF54DZY34U [01:42:31], 6zGVAStZP50 [00:18:02]); tongue on a shared mesh: Assign Automatic From Bones with only tongue vertices and bones selected (FhF54DZY34U [01:45:56]). With a Mirror modifier, a group named exactly `DEF-eye.R` is filled by vertex-group mirroring (Demeter [00:29:50]).
      - **Deform flags** audited before (re)binding: root, IK targets and poles deforming pull the heel (tNW8uCs77MQ [00:05:37]). Undo automatic weights completely: clear parent, delete Armature modifiers, delete all groups (6zGVAStZP50 [00:01:50]). Parent With Empty Groups adds new deform bones without wiping painted weights (ykB4gBQgFOU [00:03:02]).
      - **Modifier stack.** Armature above Subdivision (Demeter [00:20:00], Dikko [00:03:40]). Corrective Smooth: "unironically though I do recommend corrective smooth even for production", disabled while painting (Demeter [00:45:37]); Blender-only (CGDive SSL4mBAVaQc [00:04:06]).
      - **Preserve Volume.** Fixes candy wrappers, bulges armpits and hips; confine it with a second Armature modifier without it, Multi Modifier on, painted `preserve` mask; the older inverted-group method distorts when the root moves (CGDive n_waiAcd8xY [00:00:48], [00:01:53]). Support bones improve auto weights for quick jobs but add groups to manage (rf3NDKXYpTk [00:02:50]).
      
      ## 5. Corrective shape keys
      
      - Shape keys are linear offsets from the Basis and they add, which is what makes combinations work (82R2SjUxH2s [00:02:47], [00:03:21]). Make them after modeling, rigging and weights (82R2SjUxH2s [00:00:28]). A corrective only needs to look right in its pose: "Outside of it, it's nonsense" (7fHpWhM4rtc [00:03:05]).
      - Drive from the bone active in both IK and FK (MCH main chain), shape the driver curve with a flat start so it kicks in late and reaches exactly 1 at the extreme, `-var` when roll makes the value negative, and drive ball joints from a swing-only bone (the first upper-arm twist) to avoid flips under twist (7fHpWhM4rtc [00:04:26], [00:08:40], [00:12:51], [00:18:02]).
      - Sculpting in the posed state needs the Armature modifier shown in Edit Mode / on cage (7fHpWhM4rtc [00:02:29]); Pose Shape Keys (Demeter's add-on) duplicates the posed mesh, you sculpt, it writes the rest-space key and drivers (Rik _Ov0pFIOpug [00:15:27]). [added, verified] Headless equivalent: inverse skinning through the measured Jacobian, then smooth the displacement (the Smooth-brush pass) or the crease spikes.
      
      ## 6. Faces
      
      - **Choose by purpose and style.** Rigify face for realistic or mildly stylized characters and many characters; hybrid bones + shapes for a stylized hero that must stay on model; action-constraint face for studios with a generator (digest synthesis of CGDive, Demeter, Rik). Shape keys are the industry standard for faces; Stop Staring set (about 16 shapes) for hand animation, 52 ARKit shapes for capture (CGDive gsS0qRztOPg [00:00:33], [00:01:08]). Jaw and eyes as bones (gsS0qRztOPg [00:01:40]).
      - **Rik Schutte's production face (Storm).** Never one technique: joints, a lip ribbon guide (an extracted edge loop, "just weighted geometry"), pose-space correctives, lattices for squash and puffs (_Ov0pFIOpug [00:07:45], [00:11:24]). Three-curve "football" weight profile: sharp corner falloff, overlapping linear middles, clean shapes from weights alone (_Ov0pFIOpug [00:18:52]). Lip bones Stretch To the ribbon, eyelid bones Damped Track (constant length, they stay on the eyeball), guide bones pinned by Copy Location to one-vertex groups (_Ov0pFIOpug [00:21:43], [00:25:09], [00:46:37]). Brow and cheek shapes authored with weights on a temporary armature, only correctives sculpted, left side built then mirrored (_Ov0pFIOpug [00:30:48], [00:31:22]). Lock the depth axis on brow and mouth-corner controls and give depth its own control (_Ov0pFIOpug [00:34:11]). Widgets that ride the skin through Override Transform on a helper bone, which breaks the widget-drives-shape cycle; trade-off: F-curves then mix rotation and translation (_Ov0pFIOpug [00:39:32], [00:45:25]). Collections global / local / micro follow the animation workflow (_Ov0pFIOpug [00:47:11]).
      - **Demeter.** There is no generic stylized face: "good luck ... I tried and I failed" (f6im_0QVeuo [00:34:46]). A single ring of lip bones cannot make a good smile: add an outer ring; lower lip fully to the jaw and upper lip to the head first, then lip bones with Multi-Paint; rotate the jaw open and paint so the mouth vertices visibly jump to the jaw; Sphere falloff when the view is inside the mesh (f6im_0QVeuo [00:36:25] to [00:43:27]).
      - **CGDive bone face.** Lids follow the eye at about 0.15 (Copy Rotation Local, Mix Add), mouth corners follow the jaw at 0.5 through an MCH jaw copy, eye master + target with Damped Track, jaw master moved instead of rotated (ykB4gBQgFOU [00:06:55], [00:09:22]; A147p_zDwO4 [07:22:52]). Eye targets too close make the eyes react too fast (A147p_zDwO4 [07:29:45]); extend half-sphere eyes so extreme rotations do not show the edge (gsS0qRztOPg [00:11:21]).
      - **CGDive shape-key face.** Wide follows the teeth horseshoe; smile sculpted on top of wide at 1; brows down as its own key (the skull), inner-brow-down seeded from inner-up at -1 via New Shape From Mix, which copies the whole MIX, so isolate the source first; test combinations (smile + jaw open) (gsS0qRztOPg [00:27:34] to [00:47:20]). L/R split with smoothed half-face groups on duplicated keys (gsS0qRztOPg [00:44:32]). On-face SK- bones flipped so the useful move is +Y, one bone drives several keys by axis and sign (`var` smile, `-var` frown, clamped by the key range), Limit Location with Affect Transform, right side width needs `-var` (gsS0qRztOPg [00:55:59] to [01:02:12]).
      - **Rigify face** needs the parameter tuning in section 2; eyelids "are always problematic": isolate the upper lid, paint lightly on the outer lid bone to remove the zigzag (CGDive FhF54DZY34U [01:40:44]). Collar clipping into the neck: light positive weight on the first neck bone (FhF54DZY34U [01:49:53]).
      
      ## 7. Mechanical and special rigs
      
      - Fundamentals are "no different": pivot, roll, weights; hinge joints XYZ Euler with Y/Z locked (FK) and IK locks (IK) (2XwZWJ_Hnac [00:00:32]; BajO2vgto80 [00:05:30], [00:14:20]). Split hard parts into islands and assign each to one bone; a slight pinch from an unsmoothed two-group seam reads as a hard part under soft material (BajO2vgto80 [00:01:24], [00:07:08]).
      - Before automating, "ask yourself, is it worth it?" (2XwZWJ_Hnac [00:13:37]). Three equal ways to automate an elbow plate at 20% of the forearm: driver (locks the channel), Copy Rotation influence 0.2 Mix Add, Transformation 0..100 to 0..20 deg (2XwZWJ_Hnac [00:14:57] to [00:20:40]).
      - Action constraint: AC_ action, keys 1 and 10, all Linear, fake user, unlinked from the rig, target local Y with max = measured control travel, constrained bones stay animatable; Limit Location with Affect Transform or a control dragged past its limit "goes dead" (2XwZWJ_Hnac [00:24:24], [00:40:08], [00:43:48]).
      - Clean control-bone vertex groups: lock the DEF groups, delete unlocked (2XwZWJ_Hnac [00:05:29]).
      - Ball rig: never rotate a scaled (stretched) bone, rotate a clean bone below the stretch; Euler for multi-turn spins; a quaternion counter-rotation (X/Y/Z = source, W = -source) cancels a parent rotation exactly; cancel inconsistencies are "a clear sign of a dependency loop" (HyD7dI0J_bA [00:17:50], [00:24:00], [00:19:37]; counter-rotation verified).
      - Turret aim widget: Damped Track back to the turret creates a cycle; put the tracking on an MCH copy and show it through the control's Override Transform (Wayne AymxOGtJoew [00:38:28] to [00:42:21]).
      
      ## 8. Fundamentals as pitfalls (Wayne Dixon)
      
      - Three roles: control, deform, mechanism; deform is on by default, turn it off everywhere else (AymxOGtJoew [00:06:17]). Name everything, never leave .001 (it breaks the L/R rhythm, batch renames and scripts) (AymxOGtJoew [00:09:48]).
      - "If your rig is fit for purpose, you're done"; restrict only what breaks the rig or does nothing; do not add a floor constraint the animator must fight (AymxOGtJoew [00:10:53], [00:13:03]).
      - Constraint stacks evaluate top to bottom; midpoint = Copy Location 1.0 then 0.5 ([00:22:16]; for n targets use 1, 1/2, 1/3 [added, verified]). A constrained bone cannot be animated: "Give it a parent, constrain the parent" ([00:28:31]). IK traps: constraint on the wrong bone, target inside the chain, chain length 0 climbs the hierarchy, IK Stretch 0.1 is "always fine" ([00:33:24] to [00:37:14]).
      
      ## 9. Disagreements and the deciding condition
      
      | Topic                      | Positions                                                                                                                                                                                         | Decide by                                                                                                    |
      | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
      | Face system                | Rigify modular face (CGDive, Ivan); hand-built face with action constraints (Demeter); hybrid bones + ribbon + shape keys + lattices (Rik); bones + ~16 shape keys + on-face controls (CGDive L3) | realism and number of characters vs a stylized hero that must stay on model; mocap (ARKit) vs hand animation |
      | Twist                      | 1 twist + Preserve Volume (CGDive L2) vs 4 twists (L3)                                                                                                                                            | export or thick limbs: chains                                                                                |
      | Colors                     | function colors (Rigify) vs left red/right blue (CGDive Rigify course, L1/L2) vs left blue/right red (CGDive L3)                                                                                  | ask the animator; keep one convention per rig                                                                |
      | Finger controls            | master curls, scale to curl (CGDive) vs plain FK only (Demeter)                                                                                                                                   | crowd/fast animation vs feature animators posing fingers                                                     |
      | Connect Chain on neck/tail | on in presets vs off (CGDive: small tweak, sometimes problems)                                                                                                                                    | keep on only with an exactly coincident joint                                                                |
      | Weight blocking            | absolutes then smooth (Dikko) vs low-strength touch-ups (CGDive) vs painting in pose (Demeter)                                                                                                    | from scratch or dense mesh vs fixing auto weights                                                            |
      | Accessories                | Surface Deform (Demeter) vs transfer from body/cage and apply (Dikko, CGDive)                                                                                                                     | loose detail vs tight clothing                                                                               |
      | Pose-driven deformation    | action constraints (Demeter) vs pose-space shape keys, no action constraints (Rik)                                                                                                                | bone-based vs shape-key-based faces                                                                          |
      | Limb isolation             | Mix Add with Local With Parent (one bone fewer) vs follow bone with World constraints (CGDive's choice)                                                                                           | the trusted, root-safe version: follow bone                                                                  |
      
    • procedures.md 20.5 KB
      # Tested procedures: scenario-blender-rigging
      
      Every block ran on Blender 5.2.1 LTS with `blender -b --factory-startup` (and works in a live session). Tests live in `tests/code/blender-rigging/`; `run_all.sh` runs the whole suite (about 2.5 minutes, 16 runs, all passing on 2026-09-24). Outputs and renders land in `tests/code/blender-rigging/out/`.
      
      Import pattern:
      
      ```python
      import sys
      sys.path.append("<skills>/scenario-blender-rigging/scripts"); sys.path.append("<skills>/scenario-blender-expert/scripts")
      import bx_rig as R
      ```
      
      The raw bpy behind each helper is in `scripts/bx_rig.py` (read the function when adapting it); the essential lines are repeated here.
      
      ---
      
      ## P1. Model check and fix
      
      ```python
      rep = R.model_check([body, shirt, pants, eyes, hair])            # problems / warnings
      R.model_check([body, shirt, pants, eyes, hair], fix=True)         # unparent, apply, floor, center
      ```
      
      Core: `o.data.transform(o.matrix_world, shape_keys=True); o.matrix_world = Matrix.Identity(4)` then one translation (floor to Z=0, X center, mid-foot Y=0) for the whole set. `shape_keys=True` matters: without it a mesh with shape keys (Snow's eyes) keeps its old position in the evaluated result. Mirror modifiers mirror about their `mirror_object` or the object origin: keep the mirror object (Snow's shoes and eyes use an empty called Center) or apply the Mirror first.
      Verified on 5.2.1: `test_snow.py` (Snow: 1.45 m, floor 0.366, 3 objects with unapplied transforms, 7 parented to the body; clean after fix).
      
      ## P2. Landmarks from the mesh (volume snapping substitute)
      
      ```python
      lm = R.landmarks(body, eyes=eyes, overrides={"knee": (0.1, -0.01, 0.5)})
      lm["pose"], lm["arm_angle"], lm["notes"]
      S = R.Sections([body]); loops = S.cut(point, normal)   # loops: center, pts, radius, lo, hi
      ```
      
      Method: horizontal cuts until one loop spans x=0 (crotch); hip = 13% of crotch height above it; ankle = first cut where the leg section turns round (y extent < 1.3 x extent); knee halfway, kept on the hip-ankle line from the front; neck = narrowest central section between head and shoulders, head pivot 20% of the head height above it; spine joints at the front/back midpoint at Rigify's height fractions (0.226, 0.437, 0.703); arm traced from 65% of the arm span inward with x-planes until the loop size jumps x1.5 (armpit), then cuts normal to the arm axis: wrist = narrowest section at 60 to 90% of the arm; elbow at 53%; shoulder joint 0.6 arm radii inside the armpit (deep, CGDive); nose tip = most forward face point, chin = lowest point of the front profile still forward of halfway to the neck front; eyes = mean of each eye island.
      Lessons from testing: tracing inward from the fingertip gets lost between the fingers; the narrowest horizontal section is not always the neck on stylized heads (use the neck base, not the neck minimum, for the face profile).
      Verified: `test_landmarks.py -- T|A` (6/6 each), `test_snow.py` (T-pose, 2.6 deg arm angle).
      
      ## P3. Rigify metarig: add, fit, check, face, generate
      
      ```python
      meta = R.add_metarig("basic")          # "human" for fingers + face; wolf/horse/cat/bird/shark/basic_quadruped
      R.fit_metarig(meta, lm)                # joints as shared points, pre-bends, hinge rolls, symmetrize
      probs = R.check_metarig(meta, body)    # [] = ready
      R.placement_sheet([body, eyes], out, arm_obj=meta, views=("front", "right", "top"))
      R.placement_sheet([body, eyes], out, arm_obj=meta, center=head_centre, size=0.4, name="head")
      R.upgrade_face(meta)                   # human only, after fitting: irreversible
      rig, err = R.generate(meta)            # err: "Error: RIGIFY ERROR: Bone 'spine.004': Cannot connect chain ..."
      ```
      
      Core facts used by the fit:
      
      - Enable Rigify: `bpy.ops.preferences.addon_enable(module="rigify")`; add at the origin: `bpy.ops.object.armature_basic_human_metarig_add()` (1.98 m, faces -Y).
      - Limb roll: local X = normal of the chain plane, `n = (u.tail-u.head).cross(l.tail-l.head).normalized(); eb.align_roll(n.cross(y_dir))` (Rigify's own convention: default arm dot 0.976, leg 1.0). Spine and head `align_roll((0,-1,0))`, shoulder and toe `align_roll((0,0,1))`.
      - Pre-bend: knee `y = min(section_y, line_y - 0.012 * leg_length)`, elbow `y += 0.012 * arm_length` (about 2.7 deg between segments).
      - Connected ends propagate: setting a parent's `tail` in Edit Mode moves a connected child's `head`. Group transforms must write each end once (`R.transform_bones`); `EditBone.transform()` bone by bone double-moved shared ends and produced 0.23 m finger bones in testing.
      - Moving a face region: `R.move_points(eb, [(old, new), ...])` moves every coincident end (tails first, then heads of disconnected bones). Moving only some lid bones produced "Bone 'eye.R': Expected 2 eye corners, but found 1".
      - Side names: `lid.T.L.001` does not end with `.L`; select sides with `R.side_of(name)` before `bpy.ops.armature.symmetrize()`, or half the face stays unmirrored.
      - Face (human): piecewise vertical map through chin bottom, nose tip, eye line and head top, width scaled to the head, eyes and lids moved onto the eyeballs. Coarse: jaw and nose bones still need manual placement on a stylized face (see `out/snow_human_face/placement_sheet.png`).
      - `check_metarig` flags: unapplied scale/pose, positional chain breaks (`connect_chain` needs head == parent tail even when `use_connect` is False), knee/elbow bends outside 1 to 15 deg or bending the wrong way, hinges off local X, no Rigify UI row, glue ends not coincident, bone middles outside the mesh (14-ray vote, tolerant of eye holes).
      - Generation failure leaves a partial rig active; `generate()` re-activates the metarig.
        Verified: `test_fit.py -- T|A|human T` (6/6, 6/6, 8/8; 222 bones basic, 918 human, 0.1 to 0.3 s), `test_checks.py` (10/10: each check fires on a broken metarig), `test_snow.py -- basic|human`.
      
      ## P4. Bind with verification and fallback
      
      ```python
      rep = R.bind(rig, body, garments=[shirt, pants, shoes],
                   rigid={eyes: None, hair: "DEF-spine.006"},     # None = nearest deform bone per island
                   max_influences=None, clean=0.01, symmetric=True, route="auto")
      rep["route"], rep["ok"], rep["body"]["unweighted"], rep["body"]["empty_major"], rep["stripped_cross_side"]
      ```
      
      Steps inside: `parent_set(type='ARMATURE_AUTO')` then `weight_report` (heat failure = unweighted vertices or a major bone inside the mesh with no weight; small face bones and teeth/tongue/eye bones may legitimately be empty on the skin); on failure `proxy_weights`: voxel remesh of a copy at H/180, H/90, H/45 until bone heat covers every proxy vertex (an open mesh voxelizes into a thin shell with the bones outside it; the coarser voxel closes the holes), then Data Transfer:
      
      ```python
      m = dst.modifiers.new("BX_weights", "DATA_TRANSFER"); m.object = proxy
      m.use_vert_data = True; m.data_types_verts = {"VGROUP_WEIGHTS"}; m.vert_mapping = "POLYINTERP_NEAREST"
      m.layers_vgroup_select_src = "ALL"; m.layers_vgroup_select_dst = "NAME"
      bpy.ops.object.modifier_move_to_index(modifier=m.name, index=0); bpy.ops.object.modifier_apply(modifier=m.name)
      ```
      
      then `strip_opposite_side` (auto weights put left-thigh weight on 1,888 right-side vertices of the mannequin), garments by the same transfer from the body (applied), `assign_rigid` (islands, 1.0, one bone), `remove_non_deform_groups`, `clean_weights` (below 0.01, optional N largest, normalize), `armature_first` (after Mirror, before Subdivision; Subdivision pinned last).
      Results: mannequin auto route 0 unweighted, mirror error 0.03; raw intersecting primitives: auto left 96 unweighted, proxy route 0; Snow basic and human: auto route clean; Snow with forced proxy: first voxel failed, H/90 succeeded.
      Verified: `test_bind.py -- T|A` (15/15), `test_snow.py`.
      
      ## P5. Test poses, numbers, renders
      
      ```python
      dr = R.deformation_report(rig, [body, shirt], R.RIGIFY_POSES)     # Subdivision hidden while measuring
      for p in R.flag_report(dr): print(p)
      R.pose_sheet(rig, [body, shirt, eyes], R.RIGIFY_POSES, out)        # rows = poses; bx_review Workbench
      R.joint_sheet(rig, [body, shirt], [R.pose_by_name("elbow_120.L")], out)   # close-ups at the joint
      ```
      
      Pose ops are written in world terms so the library works on T and A poses: `("aim", bone, dir)`, `("hinge", bone, deg, toward)` (rotates about the bone's own local X, so a wrong roll shows as a wrong pose), `("twist", bone, deg)`, `("rot", bone, axis, deg)`, `("move", bone, delta_in_H)`, `("prop", bone, key, v)`, `("fk", "arms")`, `("ik", "legs")`. Custom rigs: write poses with their control names and extend `R.JOINTS` with `"name": ("bone:head", "bone:head", "bone:tail")`.
      Report fields: strain p1/p99 and edges outside 0.5 to 1.6 (flagged above 0.2% of edges), joint volume = mean squared distance to the limb axis posed/rest over the joint's flesh (1.8 local radii, half a segment each way) with the worst and best band along the limb, cross-side leak in units of H, new self-intersecting face pairs (bx_audit).
      Typical auto-weight numbers (mannequin, Rigify basic): arm down shoulder band 0.47, elbow 120 0.68, knee 120 0.73, wrist twist 80 1.0 (Rigify B-bone twist), spine and neck about 1.0.
      Pitfall found: squatting with ('ik',) on all limbs raised the arms (IK hands stay put); use ('ik', 'legs').
      Verified: `test_bind.py`, `test_snow.py` (sheets in `out/bind_T/`, `out/snow_basic/`).
      
      ## P6. Manual rig: skeleton, twist chains, FK/IK, IK, foot roll, symmetry, DEF, organization
      
      ```python
      rig = R.new_armature("RIG"); R.activate(rig, "EDIT"); eb = rig.data.edit_bones
      R.new_bone(eb, "root", (0, 0, 0), (0, 0.25 * H, 0), roll=0.0)          # world-aligned, flat
      R.new_bone(eb, "pelvis", P["pelvis"], P["spine1"], "root", deform=True, roll_z=(0, -1, 0))
      R.new_bone(eb, "MCH-upper_arm.L", sh, el, "shoulder.L")
      R.new_bone(eb, "MCH-lower_arm.L", el, wr, "MCH-upper_arm.L", True)
      R._set_limb_roll(eb, ["MCH-upper_arm.L", "MCH-lower_arm.L", "hand.L"])
      bpy.ops.object.mode_set(mode="OBJECT")
      tw = R.twist_chain(rig, "MCH-lower_arm.L", "hand.L")                                  # distal
      R.twist_chain(rig, "MCH-upper_arm.L", None, proximal=True, parent="shoulder.L")        # proximal
      prop = R.settings_prop(rig, "rig_settings", "leg_fk_ik.L", default=1.0)                # 1 = IK
      fk, ik = R.fk_ik_switch(rig, ["MCH-thigh.L", "MCH-shin.L", "foot.L", "toe.L"], prop)
      # (edit mode) IK control, MCH target, pole under the IK control; R.reparent(eb, "foot_IK.L", "MCH-leg_IK_target.L")
      c, (angle, err) = R.ik_chain(rig, "shin_IK.L", "MCH-leg_IK_target.L", "leg_pole.L")    # chain 2, locks, pole solved
      R.con(rig, "foot_IK.L", "COPY_LOCATION", subtarget="shin_IK.L", head_tail=1.0)
      R.foot_roll(rig, "L", "leg_IK.L", "MCH-leg_IK_target.L", heel, toe, inner, outer, ball)
      # symmetrize the .L side, then: R.mirror_drivers(rig); settings_prop for the .R keys
      R.make_def_layer(rig, deformers); R.organize(rig, settings="rig_settings")
      R.set_shape(rig, "upper_arm_FK.L", R.widget("circle"), 0.6)
      ```
      
      Raw essentials:
      
      - Twist (distal): per twist `COPY_ROTATION` from the hand, `target_space = owner_space = 'LOCAL'`, influence 1.0/0.66/0.33/0.1 from the wrist, then `DAMPED_TRACK` to the hand below it. Proximal: parent to the clavicle/pelvis, `COPY_LOCATION` (first from the main bone, then the previous twist with `head_tail=1`), `DAMPED_TRACK` to the main bone with `head_tail=1`, `COPY_ROTATION` Local/Local from the main bone. Measured falloff for a 90 deg hand twist: 9.0, 29.7, 59.4, 90.0 deg.
      - FK/IK: two `COPY_TRANSFORMS` on each main bone named "FK" then "IK"; driver on the IK one's influence: `v.type='SINGLE_PROP'; v.targets[0].id = rig; v.targets[0].data_path = 'pose.bones["rig_settings"]["leg_fk_ik.L"]'`. Property on a pose bone: `pb[key] = 1.0; pb.id_properties_ui(key).update(min=0, max=1, default=1.0, step=1)` (without `default=` Reset to Default flips the switch).
      - Pole angle: set influence 0, record the chain matrices, scan pole angles for the minimum difference with influence 1 (coarse 2 deg, then 0.25, then 0.02): leg -90.0 deg (error 7e-5), arm -90.0 deg (3e-5).
      - Foot roll: pivots pointing up with roll 0; outside > inside > heel > toe > ball > MCH IK target; each pivot `COPY_ROTATION` from foot_roll (Local, `use_y=False`) + `LIMIT_ROTATION` (owner Local): heel X [-90, 0], toe X [0, 90], inside/outside Z one-sided; foot_roll parented to the ankle control. Probes: roll X -30 lifts the toe 0.13 m, X +30 lifts the ankle 0.09 m on the toe pivot, each Z direction lifts one edge; Symmetrize mirrored the Z limits.
      - `reparent`: clear `use_connect` BEFORE changing the parent. Doing it after moved foot_IK's head 0.105 m to the new parent's tail and broke FK/IK coincidence (0.63 matrix error); fixed: 1.6e-5.
      - `mirror_drivers(id)`: Symmetrize copies bones, constraints and limits but no drivers; this recreates each .L driver on .R (paths, bone targets, property paths, keys) and skips targets that do not exist yet.
      - DEF layer: `DEF-` copies, one `COPY_TRANSFORMS` each, deform only on DEF (vertex groups renamed so weights survive).
        Checks run: static cycles none, runtime cycle check clean, root test 1.4e-5, FK/IK coincide 1.6e-5, drivers valid 7 = 7, IK foot planted when the pelvis drops (3.6e-5 m), bind auto 0 unweighted, wrist twist 80 deg band 0.97 (no candy wrapper).
        Verified: `test_manual_rig.py` (18/18).
      
      ## P7. Rig test suite (manual rigs)
      
      ```python
      R.static_cycles(rig)      # constraint target inside the owner's children, IK target inside its chain, mutual constraints
      R.cycle_check()           # saves a copy, opens it in a background Blender, returns 'Dependency cycle detected' lines
      R.root_test(rig)          # (max matrix error, worst bone) after translate + rotate + scale 1.5 of the root
      R.twist_angles(rig, twist_names, "MCH-lower_arm.L")
      ```
      
      Positive controls: two bones damped-tracking each other and a bone copying its own child are caught by both `static_cycles` and `cycle_check` ("WARNING Dependency cycle detected").
      Verified: `test_checks.py`, `test_manual_rig.py`.
      
      ## P8. Weight fixes as data (no brush)
      
      ```python
      R.strip_opposite_side(body, rig)                                # .L weights off the right half, renormalize
      R.assign_rigid(teeth_lower, rig, "DEF-jaw")                     # islands at 1.0
      R.smooth_weights(body, rig, factor=0.5, repeat=2, bones=["DEF-hand.L", "DEF-forearm.L.001"], vertex_indices=band)
      R.clean_weights(body, rig, limit=0.001, max_influences=4)       # export
      W, names = R.weight_matrix(body); R.write_weights(body, W, names)   # arbitrary numpy edits
      R.transfer_weights(body, sleeve, rig)                           # same source, applied
      ```
      
      `smooth_weights` runs `bpy.ops.object.vertex_group_smooth(group_select_mode='BONE_SELECT'|'BONE_DEFORM')` in Weight Paint mode with pose bones selected and `mesh.use_paint_mask_vertex = True` (only selected vertices change). Exact seams (CGDive's Mix 0.5): write equal weight vectors on the seam vertices of both meshes, or join the meshes, edit, separate (weights survive).
      Verified: `test_bind.py` (strip, clean, normalize, garments), `test_mechanical.py` (rigid).
      
      ## P9. Corrective shape key without sculpting
      
      ```python
      R.apply_pose(rig, R.pose_by_name("elbow_120.L"))
      idx, tgt = R.volume_restore_targets(body, rig, "elbow.L")            # push flesh back to rest distance, falloff + smoothing
      kb = R.corrective_from_targets(body, rig, "CS_elbow.L", tgt, idx)     # rest-space offsets via the skinning Jacobian
      R.corrective_driver(body, "CS_elbow.L", rig, "ORG-upper_arm.L", "ORG-forearm.L", 40, 115)
      R.mirror_shape_key(body, "CS_elbow.L"); R.mirror_drivers(body.data.shape_keys)
      ```
      
      Inverse skinning: a temporary shape key with +h offsets on X, Y, Z (three evaluations) measures the 3x3 map from rest offset to posed displacement per vertex; `offset = solve(J, target - posed)`. Exact at the pose (max error 1e-5 m). Driver: `ROTATION_DIFF` between two bones that follow both IK and FK (Rigify ORG bones, a manual rig's MCH chain), keys moved to (40 deg, 0) and (115 deg, 1) with auto-clamped handles so it kicks in late (0.41 at 70 deg), constant extrapolation.
      Lesson: the unsmoothed radial push restored the numbers (band 0.68 to 0.90) but the render showed spikes in the crease; falloff along the limb plus 12 relax passes gave 0.81 with a clean render. The number is a guide, the render decides.
      Verified: `test_correctives.py` (6/6, renders `out/correctives/`).
      
      ## P10. Preserve Volume done right (Blender-only rigs)
      
      ```python
      n = R.preserve_volume_mask(body, rig, pose_fn=lambda: R.apply_pose(rig, R.pose_by_name("arm_up.L")))
      ```
      
      Stack: Armature with Preserve Volume, then a second Armature without it, `use_multi_modifier=True`, `vertex_group="preserve"`. Result = Preserve Volume blended toward the plain result by the mask weight (verified to 1e-7). The mask is seeded where Preserve Volume moves vertices more than 0.4% of the height from the plain result (326 vertices on the mannequin's arm-up pose; mean bulge 12 mm reduced to 3.7 mm).
      Verified: `test_pv_mask.py` (4/4).
      
      ## P11. Face: shape keys, L/R split, on-face controls
      
      ```python
      L, Rk = R.split_shape_lr(face, "mouth_smile")            # mouth_smile.L/.R, groups sum to 1 across the center
      R.face_control(rig, "SK-mouth_corner.L", head, tail, "head", travel=0.02, axes=("Y",), both_ways=True)
      R.drive_shape(face, "mouth_smile.L", rig, "SK-mouth_corner.L", "LOC_Y", 0.02)
      R.drive_shape(face, "mouth_frown.L", rig, "SK-mouth_corner.L", "LOC_Y", 0.02, sign=-1)
      # symmetrize the SK bone, then R.mirror_drivers(face.data.shape_keys)
      ```
      
      `face_control` locks every channel except the used axes, adds `LIMIT_LOCATION` in local space with `use_transform_limit=True` (Affect Transform). Checks: +Y gives smile 1 and frown 0, -Y the reverse (negative values clamp on the key's 0..1 range), 3x the travel stays at 1, right control drives only right keys, L + R at 1 reproduces the symmetric shape exactly. Right-side width drivers on a mirrored X axis need `sign=-1` (pass the path in `mirror_drivers(..., negate=[...])`). New shape keys start at value 1.0 in 5.2.1: the helpers set 0.
      Verified: `test_face.py` (8/8, `out/face/face_sheet.png`).
      
      ## P12. Mechanical / hard surface
      
      ```python
      R.assign_rigid(robot, rig)                       # each island -> nearest deform bone at 1.0
      R.rigid_check(robot)                             # max relative edge change at the current pose (4e-6 measured)
      act = R.key_action(rig, "AC_plates", {"plate_0": {"rotation_euler": (math.radians(-40), 0, 0)}})
      R.action_drive(rig, ["plate_0", "plate_1"], act, "plate_ctrl", "LOCATION_Y", 0.0, 0.1)
      R.con(rig, "plate_ctrl", "LIMIT_LOCATION", owner_space="LOCAL", use_transform_limit=True,
            use_min_y=True, use_max_y=True, min_y=0.0, max_y=0.1)
      ```
      
      `key_action` keys rest on frame 1 and the pose on frame 10, sets LINEAR through the slot channelbag (`bpy_extras.anim_utils.action_get_channelbag_for_slot`), fake user on, unlinks the action. Half the control travel gave exactly half the rotation; driven plates stay hand-animatable. Hinges: `rotation_mode='XYZ'`, `lock_rotation=(False, True, True)`, and `lock_ik_y/z` on IK bones.
      Verified: `test_mechanical.py` (7/7).
      
      ## P13. Test character
      
      ```python
      m = R.mannequin("Mannequin", height=1.8, pose="T"|"A", head_scale=1.0, eyes=True, shirt=True)   # dict body/eyes/shirt
      m = R.mannequin("Raw", raw=True)       # intersecting primitives: bone heat fails on part of it
      ```
      
      Body = primitives joined by `bx_sculpt.union_remesh` (one manifold shell, about 19k vertices), eyes = two sphere islands, shirt = offset copy of the torso region (an overlapping garment). Real production mesh for tests: Snow in `tests/fixtures/retopo_examples.blend` (CC-BY Blender Studio), objects `snow_main`, `GEO-snow_shirt`, `GEO-snow_pants`, `GEO-rain_shoes.002`, `GEO-snow_eye_anim`, hair and brows.
      
      ## P14. Rigify internals worth knowing when scripting
      
      - Post-generation script: `meta.data.rigify_finalize_script = bpy.data.texts["postgen.py"]` (runs with the rig active; Demeter's cosmetic tweaks).
      - Action slots on the metarig: `meta.data.rigify_action_slots`, `bpy.ops.object.rigify_action_create()`.
      - Samples for custom metarigs (Edit Mode): `bpy.ops.armature.metarig_sample_add(metarig_type="limbs.arm")`; parent components with Keep Offset; at least one collection with `rigify_ui_row > 0`; `bpy.ops.armature.rigify_use_standard_colors()`.
      - Face parameters: `pb.rigify_parameters.eyelid_follow_default` (default (0.2, 0.7)), `jaw_master` `jaw_mouth_influence` (default 1.0), `meta.data.rigify_force_widget_update` (default False).
      - Generated control names (5.2.1): `upper_arm_fk.L`, `forearm_fk.L`, `hand_fk.L`, `hand_ik.L`, `upper_arm_parent.L` (props `IK_FK` 1 = FK, `IK_Stretch`, `FK_limb_follow`, `pole_vector`), `thigh_fk.L`, `shin_fk.L`, `foot_ik.L`, `thigh_parent.L`, `torso` (`neck_follow`, `head_follow`), `chest`, `hips`, `neck`, `head`, `shoulder.L`, `root`; deform bones all start with `DEF-`, ORG bones follow IK and FK.
        (From the rigify_studio digest probes, `archive/tests/rigify_probe/`, and this skill's tests.)
      
    • sources.md 19.3 KB
      # Sources: scenario-blender-rigging
      
      One entry per source video (grouped by expert). Timestamps are [hh:mm:ss] into the video. Blender version on screen matters: anything recorded before 4.0 uses armature layers and bone groups (now bone collections and per-bone colors).
      
      ## CGDive channel host (name not stated in the transcripts; Wayne Dixon calls him Toto) (CGDive, Blender Rigging Tuts)
      
      Credential: rigging instructor, author of the free Rigging Isn't Scary course and the CGDive Academy Rigify courses.
      
      - **EASY Rigging with Blender Rigify: a Full Course** (FhF54DZY34U, 4:16:29, uploaded 2026-06-01, Blender 4.0, author says it also works in 5): https://www.youtube.com/watch?v=FhF54DZY34U
        - Best for: the fullest metarig-to-generated-rig Rigify workflow: fitting, generation errors and fixes, weighting, custom metarigs, UI and colors.
        - Best timestamps: [00:23:34] bone placement decides deformation, [00:57:37] fixing a "bone position disjoint" error, [01:30:26] weight-paint crash course, [03:35:23] bone collection UI rows.
      
      - **Blender 5.2: NEW Rigging and Animation Features** (cERh1l2RtfU, 15:57, uploaded 2026-07-09, Blender 5.2 RC vs 5.1): https://www.youtube.com/watch?v=cERh1l2RtfU
        - Best for: a rigger-focused 5.2 changelog: what changed in bone creation, duplication and keying defaults.
        - Best timestamps: [00:00:28] Add Bone redo options, world-aligned Axes, [00:01:16] Duplicate and Rename for parallel chains, [00:02:28] improved Auto IK, [00:08:58] Only Insert Available now default on.
      
      - **BEGINNER Blender Rigging Course (2026)** (Ldki5txSn5U, 54:19, uploaded 2026-04-08, Blender 4.x): https://www.youtube.com/watch?v=Ldki5txSn5U
        - Best for: the canonical build order for a first biped, with the three classic IK-leg bugs fixed inline.
        - Best timestamps: [00:07:48] leg chain placement, [00:17:46] IK leg setup, [00:22:15] foot sink and detach fixes, [00:29:54] root bone and deform flags.
      
      - **NOVICE Blender Rigging Course (2026) [Reupload]** (FzQVpzXcFX0, 2:12:50, uploaded 2026-05-19, Blender 4.2+): https://www.youtube.com/watch?v=FzQVpzXcFX0
        - Best for: bone roll as a hinge-axis decision, wrist and shoulder twist mechanisms, the corrected preserve-volume mask, a bones-only face rig.
        - Best timestamps: [00:12:05] bone roll and hinge axis, [00:34:08] wrist twist with Damped Track override, [01:50:09] preserve-volume bulge fix, [02:01:01] face rig challenge.
      
      - **University Level Rigging Course for Free! (2026)** (A147p_zDwO4, 8:21:01, uploaded 2026-05-26, Blender 4.3, 4.4 for slotted actions): https://www.youtube.com/watch?v=A147p_zDwO4
        - Best for: the single most complete manual biped build in the corpus, with reasons for every mechanism, twists, IK/FK, foot roll, DEF separation, mechanical arm, Action constraint, face.
        - Best timestamps: [01:07:02] IK/FK switch build, [01:41:17] six-pivot foot roll, [03:24:14] DEF bone separation pass, [06:12:07] Action constraint.
      
      - **Rigging Sucks... but it makes your characters move!** (w8J_GnBYE8o, 18:33, uploaded 2025-03-28, Blender 4.x): https://www.youtube.com/watch?v=w8J_GnBYE8o
        - Best for: model check and the minimum-viable biped skeleton before any mechanism exists.
        - Best timestamps: [00:07:29] model check checklist, [00:08:02] armature at world origin, never moved, [00:14:35] parenting with Keep Offset, [00:16:01] automatic-weights bind order.
      
      - **RIGGING L1-2: The No.1 Rigging Tool** (s7h1ECHrcSw, 8:46, uploaded 2025-03-29, Blender 4.x): https://www.youtube.com/watch?v=s7h1ECHrcSw
        - Best for: the minimum leg IK recipe and its two classic failure fixes, foot sink and foot detach.
        - Best timestamps: [00:01:57] IK on the shin, chain length 2, [00:03:05] pole target and pole angle, [00:05:44] foot-sink fix, [00:06:26] foot-detach fix.
      
      - **RIGGING L1-3: Solid Weight Painting Workflow** (tNW8uCs77MQ, 13:45, uploaded 2025-03-30, Blender 4.x, around the 4.3 brush-asset change): https://www.youtube.com/watch?v=tNW8uCs77MQ
        - Best for: the core weight-paint recipe used 80% of the time, plus root bone and deform-flag hygiene before binding.
        - Best timestamps: [00:01:33] brush recipe, [00:03:12] fixing spill via Auto Normalize, [00:04:37] root bone, [00:05:37] deform flags before binding.
      
      - **Animation Basics** (xZSeusPWx6s, 9:36, uploaded 2025-03-31, Blender 4.x): https://www.youtube.com/watch?v=xZSeusPWx6s
        - Best for: a fast rig-acceptance test, a four-pose walk cycle plus a traveling root.
        - Best timestamps: [00:01:22] four-pose walk keys, [00:02:27] Paste Pose Flipped, [00:08:05] Linear interpolation for constant-speed travel.
      
      - **Horse Rigging Challenge** (Sz0FhxaTAvE, 7:19, uploaded 2025-04-01, Blender 4.x): https://www.youtube.com/watch?v=Sz0FhxaTAvE
        - Best for: proof the biped IK recipe transfers unchanged to a quadruped, only anatomy and pole direction differ.
        - Best timestamps: [00:01:05] chain build for a horse, [00:02:53] hind-leg pole points the opposite way, [00:04:44] hoof parenting and Copy Location fix.
      
      - **Rigging FUNDAMENTALS** (q-PRGvE1ico, 18:02, uploaded 2025-04-02, Blender 4.x): https://www.youtube.com/watch?v=q-PRGvE1ico
        - Best for: where joints go, volume-centered rather than anatomical, and why bone roll must map to one hinge axis.
        - Best timestamps: [00:05:02] bone placement in the volume, [00:07:29] clavicle pushed into body depth, [00:12:28] roll rule for hinge joints.
      
      - **IK, Fingers, Bone Roll** (QnCo0hzXKeQ, 16:07, uploaded 2025-04-03, Blender 4.x): https://www.youtube.com/watch?v=QnCo0hzXKeQ
        - Best for: the finger and thumb roll-unification test and the IK-target dependency-loop trap.
        - Best timestamps: [00:02:03] rotate all fingers on Z to test roll, [00:09:15] Clear Parent fully on the IK target, [00:10:21] pole angle reasoning.
      
      - **RIGGING L2-3: Constraints - Your Secret Weapon** (HhhSjGqetjo, 18:32, uploaded 2025-04-04, Blender 4.x): https://www.youtube.com/watch?v=HhhSjGqetjo
        - Best for: the two canonical single-twist-bone recipes, forearm and upper arm, plus the World/World or Local/Local space rule.
        - Best timestamps: [00:03:13] forearm twist Copy Rotation, [00:06:16] Damped Track override reduces flipping, [00:14:26] shoulder twist Damped Track Head/Tail 1.
      
      - **RIGGING L2-4: Foot and Hips Rig** (PdTOfJDdzgc, 17:40, uploaded 2025-04-05, Blender 4.x): https://www.youtube.com/watch?v=PdTOfJDdzgc
        - Best for: the nested-pivot foot roll, IK target through ball, toe, heel pivots, and the constraint-plus-child pattern.
        - Best timestamps: [00:01:07] hips flip via Switch Direction, [00:07:30] nested pivot parenting, [00:12:28] toe Damped Track to pivot, [00:13:43] constraint plus child pattern.
      
      - **RIGGING L2-5: Bone Shapes / Widgets** (aj_ACAE7K8I, 19:09, uploaded 2025-04-06, Blender 4.2+): https://www.youtube.com/watch?v=aj_ACAE7K8I
        - Best for: the three-role bone collection system, control/deform/MCH, widget-building recipes, and the world-oriented root convention.
        - Best timestamps: [00:02:51] three bone roles into collections, [00:06:02] widget mesh recipes, [00:15:36] world-oriented root, [00:17:50] wire width and color coding.
      
      - **RIGGING L2-6: What you don't know about Weight Painting** (6zGVAStZP50, 20:05, uploaded 2025-04-07, Blender 4.x): https://www.youtube.com/watch?v=6zGVAStZP50
        - Best for: what automatic weights actually creates, the full undo-and-redo recipe, and the area-by-area priority list.
        - Best timestamps: [00:01:50] fully undoing automatic weights, [00:03:02] deform-flag audit, [00:11:51] crotch and hips "red pants", [00:18:02] rigid teeth and eye assignment.
      
      - **Cool Rigging Hacks** (SSL4mBAVaQc, 9:43, uploaded 2025-04-08, Blender 4.x): https://www.youtube.com/watch?v=SSL4mBAVaQc
        - Best for: Corrective Smooth as "the magic modifier," subdivision-last ordering, Euler versus quaternion for hinge bones.
        - Best timestamps: [00:01:06] first, flawed, preserve-volume mask attempt, [00:04:06] Corrective Smooth, [00:04:36] subdivision must be last, [00:08:25] gimbal-lock risk on wide-freedom bones.
      
      - **Fix bad deformations with one click (Preserve Volume FIX)** (n_waiAcd8xY, 2:30, uploaded 2025-07-22, Blender 4.x): https://www.youtube.com/watch?v=n_waiAcd8xY
        - Best for: the corrected preserve-volume mask, a second Armature modifier with Multi Modifier plus a painted mask.
        - Best timestamps: [00:00:48] Multi Modifier mask setup, [00:01:24] painting the preserve group, [00:01:53] why the original method broke on root movement.
      
      - **RIGGING L2-8: EASY Face Rigging that even Beginners can do** (ykB4gBQgFOU, 12:54, uploaded 2025-04-09, Blender 4.x): https://www.youtube.com/watch?v=ykB4gBQgFOU
        - Best for: a minimal, fully bone-based face rig (jaw, eyes, lids, brows, mouth corners) buildable entirely from constraint data.
        - Best timestamps: [00:01:25] jaw and eye bone placement, [00:06:55] lids follow the eyeball at 0.15 influence, [00:09:22] mouth corners follow the jaw at 0.5.
      
      - **RIGGING L3-1: Serious Rigging!** (Q21AZnAyOJA, 58:02, uploaded 2025-04-10, Blender 4.3): https://www.youtube.com/watch?v=Q21AZnAyOJA
        - Best for: a real model-check pass on a flawed model, the twist-bone count rule (4 per segment) with falloff numbers.
        - Best timestamps: [00:03:43] model check pass, [00:22:25] twist-bone count rule, [00:26:39] forearm twist chain with falloff, [00:46:25] Rigify-like spine build.
      
      - **RIGGING L3-2: IK-FK Switch** (1nkCc3IhTYs, 39:20, uploaded 2025-04-11, Blender 4.3): https://www.youtube.com/watch?v=1nkCc3IhTYs
        - Best for: the three-chain IK/FK architecture, stacked Copy Transforms, and wiring custom properties and drivers to switch them.
        - Best timestamps: [00:02:18] stacked Copy Transforms principle, [00:25:59] rig_settings bone and custom property, [00:35:58] wiring the IK/FK driver.
      
      - **RIGGING L3-3: Improved Leg Rig** (ErR4RysRy0g, 59:54, uploaded 2025-04-12, Blender 4.3): https://www.youtube.com/watch?v=ErR4RysRy0g
        - Best for: a single-control foot roll, why Inherit Rotation off is a trap, and a robust two-bone limb-isolation method.
        - Best timestamps: [00:10:04] one-control roll with Limit Rotation, [00:20:30] IK axis lock, [00:29:28] follow method 2, parent/follow MCH pair, [00:41:19] Copy Driver to Selected after symmetrize.
      
      - **RIGGING L3-4: Easy Hand Rig** (_6eyk1vembQ, 46:58, uploaded 2025-04-13, Blender 4.3): https://www.youtube.com/watch?v=_6eyk1vembQ
        - Best for: Rigify-style finger curl via a Transformation constraint (scale to rotation) instead of a driver, plus palm falloff automation.
        - Best timestamps: [00:08:52] scale-to-rotation curl, [00:17:58] palm falloff automation, [00:22:01] root parenting audit, [00:35:06] asymmetric alignment with X mirror off.
      
      - **RIGGING L3-5: This makes using your Rig EASIER** (doRG4Sko7Kg, 1:02:25, uploaded 2025-04-14, Blender 4.3): https://www.youtube.com/watch?v=doRG4Sko7Kg
        - Best for: the full DEF-bone separation pass for clean game export, plus the DEF/MCH/CTRL collection plan.
        - Best timestamps: [00:05:49] duplicating deform bones into DEF, [00:16:33] rebuilding a logical DEF hierarchy, [00:42:22] widget Y-axis orientation, [00:49:11] Override Transform for a detaching widget.
      
      - **RIGGING L3-6: Pro Weight Painting** (rf3NDKXYpTk, 54:19, uploaded 2025-04-15, Blender 4.3): https://www.youtube.com/watch?v=rf3NDKXYpTk
        - Best for: the remaining 20% of weight painting, Projected falloff, test-animation-driven fixes, exact-0.5 Mix-brush seams.
        - Best timestamps: [00:06:35] Projected falloff to paint through, [00:17:33] hip and crotch test animation, [00:33:28] Mix brush at weight 0.5 for exact seams.
      
      - **RIGGING L3-7: Hard-Surface Weights** (BajO2vgto80, 30:12, uploaded 2025-04-16, Blender 4.3): https://www.youtube.com/watch?v=BajO2vgto80
        - Best for: what to do when Bone Heat weighting fails, splitting rigid parts, hardening weights with Levels plus Normalize All.
        - Best timestamps: [00:01:24] splitting and assigning rigid parts, [00:14:20] IK and Euler locks for hinge joints, [00:28:56] mandatory Normalize All after Levels.
      
      - **RIGGING L3-8: Mechanical Rigging** (2XwZWJ_Hnac, 48:32, uploaded 2025-04-17, Blender 4.4): https://www.youtube.com/watch?v=2XwZWJ_Hnac
        - Best for: three equivalent ways to automate a mechanism (driver, Copy Rotation, Transformation constraint) and the full Action constraint workflow.
        - Best timestamps: [00:14:57] three automation options compared, [00:22:03] Action constraint theory, [00:42:42] Limit Location with Affect Transform.
      
      - **Shapekeys for Beginners in 7 Minutes** (82R2SjUxH2s, 7:07, uploaded 2025-05-28, Blender 4.4 era): https://www.youtube.com/watch?v=82R2SjUxH2s
        - Best for: the shape-key mechanics that matter for rigging, linear per-vertex offsets, additive mixing, overshoot ranges.
        - Best timestamps: [00:02:47] vertices move linearly from Basis, [00:03:21] mixing is additive, not averaging, [00:04:03] overshoot with Range Max/Min.
      
      - **Improve Deformations with Shapekeys** (7fHpWhM4rtc, 19:47, uploaded 2025-05-29, Blender 4.4 era): https://www.youtube.com/watch?v=7fHpWhM4rtc
        - Best for: the full corrective-shape recipe, sculpt in the bent pose, drive from the shared MCH bone, shape the driver curve.
        - Best timestamps: [00:04:26] driver from the MCH chain bone shared by IK and FK, [00:07:33] shaping the driver F-curve, [00:17:27] driving from a non-twisting bone for stability.
      
      - **All-In-One FACE Rigging Tut for Blender** (gsS0qRztOPg, 1:14:26, uploaded 2025-06-06, Blender 4.4 era): https://www.youtube.com/watch?v=gsS0qRztOPg
        - Best for: the most complete hybrid face rig here, bones for jaw/eyes/tongue plus about 16 shape keys, L/R splitting, on-face controls.
        - Best timestamps: [00:19:01] tongue squash-and-stretch chain, [00:23:33] shape creation order, [00:43:50] L/R shape-key splitting, [00:48:49] on-face SK_ controls and drivers.
      
      - **What makes this the ULTIMATE Ball rig for Blender?** (HyD7dI0J_bA, 29:42, uploaded 2026-03-23, Blender likely 5.x): https://www.youtube.com/watch?v=HyD7dI0J_bA
        - Best for: a compact, fully specified rig, keeping rotation smooth under non-uniform stretch via a counter-rotation quaternion driver.
        - Best timestamps: [00:07:20] Stretch To plus Copy Rotation wiring, [00:08:29] counter-rotation quaternion driver, [00:19:37] dependency-loop diagnosis, [00:28:07] rescaling procedure.
      
      - **The only addon you need for Rig UIs (NO Scripting!)** (AZF5DbR5iWU, 14:49, uploaded 2025-07-29, Blender 4.2 to 4.4): https://www.youtube.com/watch?v=AZF5DbR5iWU
        - Best for: packaging a rig UI with the free Bone Manager add-on, and the Register step that survives reopening the file.
        - Best timestamps: [00:01:26] tick collections and lay out UI rows, [00:04:47] export the UI as a Python script, [00:12:53] Register the text block.
      
      - **Confused by Blender Updates? Watch this.** (DkhW50q0uAg, 14:16, uploaded 2025-07-10, spans 2.49 through the 4.5 era): https://www.youtube.com/watch?v=DkhW50q0uAg
        - Best for: a translation table for reading pre-4.0 rigging tutorials in current Blender.
        - Best timestamps: [00:00:29] bone layers to bone collections, [00:01:49] bone groups to bone colors, [00:04:57] weight-paint click conventions since 4.0, [00:08:40] slotted actions explained.
      
      ## Demeter Dzadik (Blender, BCON22)
      
      Credential: character rigger at the Blender Animation Studio (Rain, the Settlers characters, Sprite Fright, Snow), author of CloudRig, EasyWeight and Pose Shape Keys.
      
      - **Rigging with the Blender Studio tools** (f6im_0QVeuo, 53:37, uploaded 2022-10-29, Blender 3.3/3.4 era): https://www.youtube.com/watch?v=f6im_0QVeuo
        - Best for: a real studio rigger's loop, metarig plus generator, EasyWeight hotkeys, and the action-constraint pose system with correctives.
        - Best timestamps: [00:05:53] generate early and often, [00:10:10] chain curvature decides the IK pole, [00:21:36] EasyWeight weight-paint keymap, [00:46:43] action-constraint pose setup with correctives.
      
      ## Rik Schutte (Blender, BCON25)
      
      Credential: animator 10+ years (Sony Pictures Imageworks: Smallfoot, Vivo, Into the Spider-Verse; Blender Studio since Sprite Fright), teaches a facial animation course at Animschool.
      
      - **Facial Rigging for Stylized Character Animation** (_Ov0pFIOpug, 49:32, uploaded 2025-09-17, Blender 4.5 / 5.0 alpha): https://www.youtube.com/watch?v=_Ov0pFIOpug
        - Best for: a production hybrid face-rig architecture (bones plus ribbon guides plus shape keys plus lattices) from Blender Studio's Storm, and a dependency-cycle-free widget trick.
        - Best timestamps: [00:17:44] mouth ribbon-guide architecture, [00:18:52] three-curve weight falloff principle, [00:25:09] eyelid Damped Track versus lip Stretch To, [00:38:58] override-transform widget breaks a dependency cycle.
      
      ## Ivan Cappiello (Blender, BCON24)
      
      Credential: co-founder of an Italian animation studio (features, TV, VFX), former Rigify maintainer (2016 to 2019) and designer of Rigify's rig types, including the original modular face.
      
      - **Rigify: past, present and future of Blender rigging** (LLnXBvEnG7o, 1:13:44, uploaded 2024-10-24, Blender 4.2/4.3 era slides): https://www.youtube.com/watch?v=LLnXBvEnG7o
        - Best for: the design logic behind Rigify's building-block metarigs, why the modular face replaced the fragile name-based one, and what glue/anchor bones require.
        - Best timestamps: [00:05:21] Rigify as building blocks, not one click, [00:34:23] why name-based rigging is fragile, [00:43:31] anchors and glue bones explained, [00:50:25] glue exactness requirement.
      
      ## Wayne Dixon (Blender, BCON25)
      
      Credential: animation and rigging instructor at CG Cookie, freelance animator and rigger, maintainer of the Bone Widget add-on.
      
      - **Rigging Cheat Codes** (AymxOGtJoew, 52:31, uploaded 2025-09-19, Blender 4.5): https://www.youtube.com/watch?v=AymxOGtJoew
        - Best for: rigging fundamentals framed as beginner mistakes: naming, constraint stack order, intermediary parents, IK pitfalls, override transforms.
        - Best timestamps: [00:06:17] the three bone roles, [00:21:44] constraint stack evaluates top to bottom, [00:25:48] intermediary-parent pattern, [00:38:28] override transform to break a cycle.
      
      ## Dikko (YouTube)
      
      Credential: character artist and rigging tutor (Australian YouTube educator).
      
      - **Character Rigging in Blender 12: Weight Painting Tips** (ljW9YJeA79E, 1:00:22, about 52 minutes of content, uploaded 2021-09-10, Blender 2.93 era): https://www.youtube.com/watch?v=ljW9YJeA79E
        - Best for: a "block with absolutes then smooth" weighting strategy, and a low-poly cage whose weights transfer identically to every clothing piece.
        - Best timestamps: [00:12:25] block absolutes, leave joints unpainted, [00:23:01] low-poly cage design, [00:28:55] Data Transfer modifier workflow, [00:45:00] Voxel Heat Diffuse Skinning shortcut.
      
      ## Coverage gaps
      
      Writer's assessment, based on what the notes' "Value" lines flag as thin or absent:
      
      - Quadruped and non-human Rigify beyond the wolf, cat and horse presets named in passing: no note fits a full animal metarig end to end, only a dog face crossover inside the human course.
      - Game-engine export and retargeting: DEF hierarchies are built "likely compatible with Unity humanoid" but no note walks an actual FBX export or engine-side retarget.
      - Cloth, hair and secondary jiggle motion: only the 5.2 XPBD Cloth/Hair Dynamics modifiers are mentioned in passing; no spring-bone or physics-driven secondary-motion technique is taught.
      - Motion-capture retargeting and cleanup: ARKit's 52 blendshapes are named as a capture target, but no note covers driving a rig from live mocap or retargeting between skeletons.
      - Non-destructive rig regeneration at production scale is discussed at the design level (Ivan Cappiello) but never demonstrated hands-on.
      
  • scripts
    • bx_rig.py 131.5 KB
      """
      bx_rig: tested rigging pieces for an agent driving Blender 5.2 through Python.
      
      Rigging is judged in motion, so this module is built around one loop: fit, generate
      or build, bind, VERIFY THE WEIGHTS, pose, measure, render, fix. Everything here runs
      headless (`blender --background --factory-startup`) and in a live session.
      
        import sys
        sys.path.append("<skills>/scenario-blender-rigging/scripts"); import bx_rig as R
      
        # 0. model check (feet on Z=0, centred on X, faces -Y, transforms applied)
        R.model_check([body, eyes, shirt], fix=True)
        # 1. landmarks from the mesh (cross-sections, not guesses) and a Rigify metarig
        lm   = R.landmarks(body, eyes=eyes)                 # dict of joints + diagnostics
        meta = R.add_metarig("basic")                       # "basic" | "human" (fingers+face)
        R.fit_metarig(meta, lm)                             # joints as units, pre-bends, rolls
        print(R.check_metarig(meta, body))                  # [] when ready to generate
        rig, err = R.generate(meta)                         # err = first line of the Rigify error
        # 2. bind with verification and fallback (auto weights fail silently)
        rep = R.bind(rig, body, garments=[shirt], rigid={eyes: None})
        # 3. test poses, numbers, renders
        dr  = R.deformation_report(rig, [body, shirt], R.RIGIFY_POSES)
        png = R.pose_sheet(rig, [body, shirt, eyes], R.RIGIFY_POSES, "/abs/out")
      
      Placement and checks: model_check, landmarks, Sections, add_metarig, fit_metarig,
      move_points, transform_bones, set_limb_roll, check_metarig, upgrade_face, generate,
      placement_sheet (bones over an X-ray render), closeup, tile_images.
      Weights: bind, weight_report, weight_matrix, write_weights, strip_opposite_side,
      clean_weights, remove_non_deform_groups, smooth_weights, transfer_weights,
      proxy_weights, assign_rigid, armature_first.
      Poses and review: RIGIFY_POSES, JOINTS, apply_pose, reset_pose, aim, hinge, twist,
      rotate_world, deformation_report, flag_report, pose_sheet, joint_sheet.
      Manual / scripted rigs (DEF/MCH/CTRL): new_armature, new_bone, copy_bone, reparent,
      con, twist_chain, twist_angles, ik_chain, solve_pole_angle, foot_roll, settings_prop,
      drive, remap_driver, fk_ik_switch, mirror_drivers, make_def_layer, widget, set_shape,
      organize, root_test, static_cycles, cycle_check.
      Correctives and faces: skin_jacobian, corrective_from_targets, volume_restore_targets,
      smooth_field, corrective_driver, mirror_shape_key, split_shape_lr, face_control,
      drive_shape. Mechanical: rigid_check, key_action, action_drive, preserve_volume_mask.
      Test character: mannequin.
      
      Verified on Blender 5.2.1 LTS by tests/code/blender-rigging/*.py.
      """
      
      __version__ = "0.1"  # Blender Expert Skills v0.1 (2026-09-24)
      import math
      import os
      import subprocess
      import sys
      
      import bmesh
      import bpy
      import numpy as np
      from mathutils import Matrix, Quaternion, Vector
      from mathutils.kdtree import KDTree
      
      HERE = os.path.dirname(os.path.abspath(__file__))
      SKILLS = os.path.dirname(os.path.dirname(HERE))
      
      
      # ----------------------------------------------------------------------------------
      # context helpers
      # ----------------------------------------------------------------------------------
      def activate(obj, mode="OBJECT", select=()):
          """Make obj the only selected, active object and enter `mode`."""
          ctx_obj = bpy.context.view_layer.objects.active
          if ctx_obj is not None and ctx_obj.mode != "OBJECT":
              bpy.ops.object.mode_set(mode="OBJECT")
          for o in bpy.context.view_layer.objects:
              o.select_set(False)
          obj.hide_set(False)
          obj.hide_viewport = False
          for o in select:
              o.hide_set(False)
              o.select_set(True)
          obj.select_set(True)
          bpy.context.view_layer.objects.active = obj
          if mode != "OBJECT":
              bpy.ops.object.mode_set(mode=mode)
          return obj
      
      
      def evaluate(obj=None):
          """Headless: re-evaluate after changing pose values, custom props or drivers."""
          if obj is not None:
              obj.update_tag()
          bpy.context.view_layer.update()
          dg = bpy.context.evaluated_depsgraph_get()
          return obj.evaluated_get(dg) if obj is not None else dg
      
      
      def world_coords(obj, evaluated=True, no_subdiv=False):
          """(N,3) world-space vertex positions (evaluated = after modifiers, shape keys).
          no_subdiv=True hides Subdivision/Multires while evaluating so vertex indices
          match the mesh (needed for per-vertex measurements)."""
          if evaluated and no_subdiv:
              hidden = [m for m in obj.modifiers if m.type in ("SUBSURF", "MULTIRES") and m.show_viewport]
              for m in hidden:
                  m.show_viewport = False
              try:
                  return world_coords(obj, evaluated=True)
              finally:
                  for m in hidden:
                      m.show_viewport = True
          if evaluated:
              dg = evaluate()
              eo = obj.evaluated_get(dg)
              me = eo.to_mesh()
              a = np.empty(len(me.vertices) * 3)
              me.vertices.foreach_get("co", a)
              mw = np.array(eo.matrix_world)
              eo.to_mesh_clear()
          else:
              me = obj.data
              a = np.empty(len(me.vertices) * 3)
              me.vertices.foreach_get("co", a)
              mw = np.array(obj.matrix_world)
          a = a.reshape(-1, 3)
          return a @ mw[:3, :3].T + mw[:3, 3]
      
      
      def _v(a):
          return Vector((float(a[0]), float(a[1]), float(a[2])))
      
      
      def side_of(name):
          """'L', 'R' or '' from Blender's mirror naming (upper_arm.L, lid.T.L.001, f_index.01.R)."""
          import re
          m = re.search(r"[._-]([LlRr])(?=$|[._-]\d*$|\.)", name)
          return m.group(1).upper() if m else ""
      
      
      # ----------------------------------------------------------------------------------
      # 0. model check (CGDive: "a huge number of rigging problems ... skipping the model check")
      # ----------------------------------------------------------------------------------
      def model_check(objs, fix=False, target_height=None):
          """Feet on Z=0, body centred on X, mid-foot at Y=0, transforms applied, faces -Y.
      
          objs[0] is the body (measured); the others move with it. fix=True applies all
          transforms and translates (and scales to target_height if given) the whole set.
          Returns a report dict; 'problems' is empty when the model is ready to rig."""
          body = objs[0]
          rep = {"problems": []}
          for o in objs:
              if any(abs(v) > 1e-6 for v in o.location) or any(abs(v) > 1e-6 for v in o.rotation_euler) \
                      or any(abs(s - 1) > 1e-6 for s in o.scale):
                  rep["problems"].append(f"{o.name}: unapplied transforms")
              if o.parent is not None:
                  rep["problems"].append(f"{o.name}: parented to {o.parent.name}")
          V = world_coords(body, evaluated=False)
          lo, hi = V.min(0), V.max(0)
          H = hi[2] - lo[2]
          low = V[V[:, 2] < lo[2] + 0.05 * H]
          rep.update(height=float(H), floor=float(lo[2]), centre_x=float((lo[0] + hi[0]) / 2),
                     foot_y=float(low[:, 1].mean()) if len(low) else 0.0)
          if abs(rep["floor"]) > 0.01 * H:
              rep["problems"].append(f"floor at z={rep['floor']:.3f}, not 0")
          if abs(rep["centre_x"]) > 0.01 * H:
              rep["problems"].append(f"not centred on X ({rep['centre_x']:.3f})")
          if fix:
              for o in objs:
                  if o.parent is not None:
                      mw = o.matrix_world.copy()
                      o.parent = None
                      o.matrix_world = mw
              for o in objs:
                  if o.type == "MESH":
                      bpy.context.view_layer.update()
                      o.data.transform(o.matrix_world, shape_keys=True)   # keys too, or they stay behind
                      o.matrix_world = Matrix.Identity(4)
                      o.data.update()
              V = world_coords(body, evaluated=False)
              lo, hi = V.min(0), V.max(0)
              low = V[V[:, 2] < lo[2] + 0.05 * (hi[2] - lo[2])]
              shift = Vector((-(lo[0] + hi[0]) / 2, -low[:, 1].mean(), -lo[2]))
              s = (target_height / (hi[2] - lo[2])) if target_height else 1.0
              M = Matrix.Scale(s, 4) @ Matrix.Translation(shift)
              for o in objs:
                  if o.type == "MESH":
                      o.data.transform(M, shape_keys=True)
                      o.data.update()
              rep["fixed"] = {"shift": tuple(shift), "scale": s}
              rep["problems"] = []
              rep["height"] = float((hi[2] - lo[2]) * s)
          return rep
      
      
      # ----------------------------------------------------------------------------------
      # procedural test mannequin (single watertight shell + separate eyes + a shirt)
      # ----------------------------------------------------------------------------------
      def mannequin(name="Mannequin", height=1.8, pose="T", head_scale=1.0, eyes=True, shirt=True,
                    voxel=None, collection=None, raw=False):
          """Build a humanoid test character facing -Y, feet on Z=0.
      
          pose: 'T' (arms horizontal) or 'A' (arms 45 deg down). head_scale > 1 gives a
          stylized big head. The body is primitives joined by voxel remesh (one manifold
          shell, like a sculpt), voxel default H/150. raw=True keeps the intersecting
          primitives instead (a mesh on which bone-heat weighting typically fails, for
          testing fallbacks). Returns dict(body=, eyes=, shirt=)."""
          sys.path.append(os.path.join(SKILLS, "scenario-blender-sculpting", "scripts"))
          import bx_sculpt
          H = height
          col = collection or bpy.context.scene.collection
          bm = bmesh.new()
      
          def capsule(a, b, r1, r2=None, seg=24):
              r2 = r1 if r2 is None else r2
              a, b = Vector(a) * H, Vector(b) * H
              d = b - a
              mat = Matrix.Translation((a + b) / 2) @ d.to_track_quat("Z", "Y").to_matrix().to_4x4()
              bmesh.ops.create_cone(bm, cap_ends=True, segments=seg, radius1=r1 * H, radius2=r2 * H,
                                    depth=d.length, matrix=mat)
              for p, r in ((a, r1), (b, r2)):
                  bmesh.ops.create_uvsphere(bm, u_segments=seg, v_segments=seg // 2, radius=r * H,
                                            matrix=Matrix.Translation(p))
      
          def ellipsoid(c, rad, seg=32):
              m = Matrix.Translation(Vector(c) * H) @ Matrix.Diagonal((rad[0] * H, rad[1] * H, rad[2] * H, 1.0))
              bmesh.ops.create_uvsphere(bm, u_segments=seg, v_segments=seg // 2, radius=1.0, matrix=m)
      
          hs = head_scale
          head_c = (0, -0.008, 0.905 + 0.055 * (hs - 1))
          ellipsoid(head_c, (0.056 * hs, 0.066 * hs, 0.072 * hs))
          capsule((0, 0.004, 0.80), (0, 0.0, 0.87 + 0.03 * (hs - 1)), 0.03)       # neck
          ellipsoid((0, 0.0, 0.715), (0.105, 0.068, 0.095))                       # chest
          ellipsoid((0, 0.004, 0.61), (0.088, 0.06, 0.085))                       # abdomen
          ellipsoid((0, 0.006, 0.525), (0.098, 0.066, 0.07))                      # pelvis
          for s in (1, -1):
              sh = Vector((0.115 * s, 0.0, 0.795))
              ellipsoid(sh, (0.042, 0.042, 0.040))                                # deltoid
              if pose == "A":
                  ang = math.radians(45)
                  dirv = Vector((math.cos(ang) * s, 0.0, -math.sin(ang)))
              else:
                  dirv = Vector((s, 0.0, 0.0))
              elbow = sh + dirv * 0.175
              wrist = elbow + dirv * 0.15
              capsule(sh, elbow, 0.032, 0.026)
              capsule(elbow, wrist, 0.026, 0.019)
              hc = wrist + dirv * 0.05
              hand_m = Matrix.Translation(hc * H) @ dirv.to_track_quat("X", "Z").to_matrix().to_4x4() \
                  @ Matrix.Diagonal((0.052 * H, 0.03 * H, 0.013 * H, 1.0))
              bmesh.ops.create_uvsphere(bm, u_segments=24, v_segments=12, radius=1.0, matrix=hand_m)
              hip = (0.058 * s, 0.004, 0.50)
              knee = (0.062 * s, 0.0, 0.285)
              ankle = (0.064 * s, 0.01, 0.052)
              capsule(hip, knee, 0.058, 0.04)
              capsule(knee, ankle, 0.04, 0.024)
              ellipsoid((0.064 * s, -0.035, 0.024), (0.032, 0.075, 0.024))        # foot
          me = bpy.data.meshes.new(name + "_parts")
          bm.to_mesh(me)
          bm.free()
          parts = bpy.data.objects.new(name, me)
          col.objects.link(parts)
          if raw:
              body = parts
              me.shade_smooth()
          else:
              body = bx_sculpt.union_remesh([parts], voxel or H / 150.0, name=name, relax=2)
          out = {"body": body, "eyes": None, "shirt": None}
          if eyes:
              bm = bmesh.new()
              hz = head_c[2] + 0.012 * hs
              for s in (1, -1):
                  c = Vector((0.024 * s * hs, head_c[1] - 0.058 * hs, hz)) * H
                  bmesh.ops.create_uvsphere(bm, u_segments=16, v_segments=8, radius=0.0125 * hs * H,
                                            matrix=Matrix.Translation(c))
              me = bpy.data.meshes.new(name + "_eyes")
              bm.to_mesh(me)
              bm.free()
              for p in me.polygons:
                  p.use_smooth = True
              e = bpy.data.objects.new(name + "_eyes", me)
              col.objects.link(e)
              out["eyes"] = e
          if shirt:
              V = world_coords(body, evaluated=False)
              keep = (V[:, 2] > 0.555 * H) & (V[:, 2] < 0.80 * H) & (np.abs(V[:, 0]) < 0.24 * H)
              if pose == "A":
                  keep = (V[:, 2] > 0.555 * H) & (V[:, 2] < 0.80 * H) & (np.abs(V[:, 0]) < 0.24 * H) \
                         & ~((np.abs(V[:, 0]) > 0.13 * H) & (V[:, 2] < 0.70 * H))
              sm = body.data.copy()
              sm.name = name + "_shirt"
              sh = bpy.data.objects.new(name + "_shirt", sm)
              col.objects.link(sh)
              bm = bmesh.new()
              bm.from_mesh(sm)
              bm.verts.ensure_lookup_table()
              bmesh.ops.delete(bm, geom=[v for v in bm.verts if not keep[v.index]], context="VERTS")
              bm.normal_update()
              for v in bm.verts:
                  v.co += v.normal * 0.006 * H
              bm.to_mesh(sm)
              bm.free()
              sm.shade_smooth()
              out["shirt"] = sh
          return out
      
      
      # ----------------------------------------------------------------------------------
      # cross-sections (the agent's "snap to volume")
      # ----------------------------------------------------------------------------------
      class Sections:
          """Plane cuts through one or more meshes, split into closed loops by topology.
      
          loops = Sections([body]).cut(point, normal) -> list of dict(center, pts, radius,
          lo, hi) sorted by center x. Centroids of limb loops are joint positions "in the
          centre of the volume" (CGDive), the substitute for Snap to Volume."""
      
          def __init__(self, objs, evaluated=False):
              Vs, Es, LE, LP = [], [], [], []
              voff = eoff = poff = 0
              for o in objs:
                  if evaluated:
                      eo = o.evaluated_get(evaluate())
                      me = eo.to_mesh()
                  else:
                      me = o.data
                  nv, ne, nl, npol = len(me.vertices), len(me.edges), len(me.loops), len(me.polygons)
                  co = np.empty(nv * 3)
                  me.vertices.foreach_get("co", co)
                  mw = np.array(o.matrix_world)
                  Vs.append(co.reshape(-1, 3) @ mw[:3, :3].T + mw[:3, 3])
                  ed = np.empty(ne * 2, dtype=np.int64)
                  me.edges.foreach_get("vertices", ed)
                  Es.append(ed.reshape(-1, 2) + voff)
                  le = np.empty(nl, dtype=np.int64)
                  me.loops.foreach_get("edge_index", le)
                  lt = np.empty(npol, dtype=np.int64)
                  me.polygons.foreach_get("loop_total", lt)
                  LE.append(le + eoff)
                  LP.append(np.repeat(np.arange(npol) + poff, lt))
                  voff, eoff, poff = voff + nv, eoff + ne, poff + npol
                  if evaluated:
                      eo.to_mesh_clear()
              self.V = np.vstack(Vs)
              self.E = np.vstack(Es)
              self.loop_edge = np.concatenate(LE)
              self.loop_poly = np.concatenate(LP)
      
          def cut(self, point, normal, min_pts=4):
              n = np.asarray(normal, dtype=float)
              n = n / np.linalg.norm(n)
              d = (self.V - np.asarray(point, dtype=float)) @ n
              d[d == 0.0] = 1e-12
              s = d > 0
              E0, E1 = self.E[:, 0], self.E[:, 1]
              cross = s[E0] != s[E1]
              idx = np.nonzero(cross)[0]
              if len(idx) == 0:
                  return []
              t = d[E0[idx]] / (d[E0[idx]] - d[E1[idx]])
              pts = self.V[E0[idx]] + (self.V[E1[idx]] - self.V[E0[idx]]) * t[:, None]
              emap = np.full(len(self.E), -1, dtype=np.int64)
              emap[idx] = np.arange(len(idx))
              m = cross[self.loop_edge]
              le = emap[self.loop_edge[m]]
              lp = self.loop_poly[m]
              parent = list(range(len(idx)))
      
              def find(a):
                  while parent[a] != a:
                      parent[a] = parent[parent[a]]
                      a = parent[a]
                  return a
              same = np.nonzero(lp[:-1] == lp[1:])[0]
              for i in same:
                  ra, rb = find(int(le[i])), find(int(le[i + 1]))
                  if ra != rb:
                      parent[ra] = rb
              roots = np.array([find(i) for i in range(len(idx))])
              loops = []
              for r in np.unique(roots):
                  p = pts[roots == r]
                  if len(p) < min_pts:
                      continue
                  c = p.mean(0)
                  loops.append({"center": c, "pts": p, "radius": float(np.linalg.norm(p - c, axis=1).mean()),
                                "lo": p.min(0), "hi": p.max(0), "n": len(p)})
              loops.sort(key=lambda L: L["center"][0])
              return loops
      
      
      # ----------------------------------------------------------------------------------
      # 1. landmarks from the mesh
      # ----------------------------------------------------------------------------------
      def landmarks(body, eyes=None, overrides=None, symmetric=True):
          """Joint positions for a humanoid in T or A pose, facing -Y, feet near Z=min.
      
          Method (agent substitute for volume snapping, CGDive/Demeter placement rules):
          horizontal cuts find legs, crotch, ankle, spine mid-line and the neck (narrowest
          section between shoulders and head); vertical cuts then cuts normal to the arm axis
          find armpit, wrist (narrowest section of the outer arm) and hand tip. Joints sit
          at section centroids; elbow/knee at proportional positions along the limb.
          Returns {name: Vector} for the LEFT (+X) side plus centre joints, 'H', 'floor',
          'pose' ('T'/'A'), 'arm_angle' (deg below horizontal) and 'notes'. Pass
          overrides={name: (x,y,z)} to correct any joint; always check the result with
          check_metarig() and a render."""
          S = Sections([body])
          V = S.V
          zmin, zmax = float(V[:, 2].min()), float(V[:, 2].max())
          H = zmax - zmin
          notes = []
          lm = {"H": H, "floor": zmin}
      
          def hcut(z):
              return S.cut((0, 0, z), (0, 0, 1))
      
          # --- legs and crotch: scan upward until one loop spans x = 0
          crotch = None
          leg_lo = {}
          for z in np.linspace(zmin + 0.03 * H, zmin + 0.7 * H, 90):
              loops = hcut(z)
              span = [L for L in loops if L["lo"][0] < 0 < L["hi"][0] and L["hi"][0] - L["lo"][0] < 0.6 * H]
              if span and z > zmin + 0.15 * H:
                  crotch = float(z)
                  break
              left = [L for L in loops if L["lo"][0] > 0]
              right = [L for L in loops if L["hi"][0] < 0]
              if left:
                  leg_lo[float(z)] = (min(left, key=lambda L: L["center"][0]),
                                      max(right, key=lambda L: L["center"][0]) if right else None)
          if crotch is None:
              raise RuntimeError("landmarks: no crotch found (legs not separated, or not a biped)")
          zs = sorted(leg_lo)
          below = [z for z in zs if z < crotch - 0.02 * H] or zs
          lm["crotch"] = Vector((0, 0, crotch))
      
          def leg_loop(z, side=0):
              loops = hcut(z)
              cand = [L for L in loops if (L["lo"][0] > 0 if side == 0 else L["hi"][0] < 0)]
              if not cand:
                  return None
              return min(cand, key=lambda L: abs(L["center"][0]))
      
          def mirror_avg(Lc, Rc):
              a = np.array(Lc, dtype=float)
              if Rc is not None and symmetric:
                  b = np.array(Rc, dtype=float) * np.array([-1, 1, 1])
                  a = (a + b) / 2
              return a
      
          # hip joint: middle of the hip mass, a little above the crotch
          zl = below[-1]
          Lh, Rh = leg_lo[zl]
          hip_c = mirror_avg(Lh["center"], Rh["center"] if Rh else None)
          hip_z = crotch + 0.13 * (crotch - zmin)
          lm["hip"] = Vector((hip_c[0], hip_c[1], hip_z))
      
          # ankle: first cut (from the floor up) where the leg section becomes round
          ankle_z = None
          for z in np.linspace(zmin + 0.02 * H, zmin + 0.14 * H, 40):
              L = leg_loop(z)
              if L is None:
                  continue
              ex, ey = L["hi"][0] - L["lo"][0], L["hi"][1] - L["lo"][1]
              if ey < 1.3 * ex:
                  ankle_z = float(z)
                  break
          if ankle_z is None:
              ankle_z = zmin + 0.05 * H
              notes.append("ankle: fallback 0.05 H")
          ankle_z = min(max(ankle_z, zmin + 0.03 * H), zmin + 0.12 * H)
          La, Ra = leg_loop(ankle_z), leg_loop(ankle_z, 1)
          a_c = mirror_avg(La["center"], Ra["center"] if Ra else None)
          lm["ankle"] = Vector((a_c[0], a_c[1], ankle_z))
      
          # foot: toe tip, heel, ball from the vertices below the ankle on the left side
          fv = V[(V[:, 2] < ankle_z) & (V[:, 0] > 0)]
          tip = fv[fv[:, 1].argmin()]
          heel_y = float(fv[:, 1].max())
          foot_len = heel_y - tip[1]
          ball_y = tip[1] + 0.33 * foot_len
          near = fv[np.abs(fv[:, 1] - ball_y) < 0.05 * foot_len]
          ball_x = float(near[:, 0].mean()) if len(near) else float(a_c[0])
          ball_z = zmin + 0.22 * (ankle_z - zmin)
          lm["ball"] = Vector((ball_x, ball_y, ball_z))
          lm["toe_tip"] = Vector((ball_x, float(tip[1]), ball_z))
          wv = fv[np.abs(fv[:, 1] - (heel_y - 0.1 * foot_len)) < 0.08 * foot_len]
          hx0, hx1 = (float(wv[:, 0].min()), float(wv[:, 0].max())) if len(wv) else (a_c[0] - 0.02 * H, a_c[0] + 0.02 * H)
          lm["heel_in"] = Vector((hx0, heel_y, zmin))
          lm["heel_out"] = Vector((hx1, heel_y, zmin))
      
          # knee: halfway hip -> ankle, section centre; chain straight in front view
          kz = ankle_z + 0.5 * (hip_z - ankle_z)
          Lk, Rk = leg_loop(kz), leg_loop(kz, 1)
          k_c = mirror_avg(Lk["center"], Rk["center"] if Rk else None) if Lk else np.array(lm["ankle"])
          t = (kz - ankle_z) / (hip_z - ankle_z)
          line = lm["ankle"].lerp(lm["hip"], t)
          lm["knee"] = Vector((line.x, float(k_c[1]), kz))
      
          # --- torso mid-line (front/back surface midpoint at x ~ 0) and the neck
          def central(z):
              loops = hcut(z)
              c = [L for L in loops if L["lo"][0] <= 0.0 <= L["hi"][0]]
              return max(c, key=lambda L: L["n"]) if c else None
      
          def midline_y(L, band):
              p = L["pts"]
              m = p[np.abs(p[:, 0]) < band]
              if len(m) < 2:
                  m = p
              return float((m[:, 1].min() + m[:, 1].max()) / 2), float(m[:, 1].min()), float(m[:, 1].max())
      
          widths = []
          for z in np.linspace(zmax - 0.01 * H, crotch + 0.15 * H, 120):
              L = central(float(z))
              if L is not None:
                  widths.append((float(z), float(L["hi"][0] - L["lo"][0]), L))
          head_max = 0.0
          neck = None
          passed_head = False
          for z, w, L in widths:
              if not passed_head:
                  if w >= head_max:
                      head_max = w
                  elif w < 0.85 * head_max:
                      passed_head = True
                      neck = (z, w, L)
              else:
                  if w < neck[1]:
                      neck = (z, w, L)
                  elif w > 1.6 * neck[1]:
                      break
          if neck is None:
              raise RuntimeError("landmarks: neck not found")
          z_nk, w_nk = neck[0], neck[1]
          neck_base_z = None
          for z, w, L in widths:
              if z < z_nk and w > 1.5 * w_nk:
                  neck_base_z = z
                  break
          if neck_base_z is None:
              neck_base_z = z_nk - 0.04 * H
              notes.append("neck base: fallback")
          neck_base_z = min(max(neck_base_z, z_nk - 0.07 * H), z_nk - 0.01 * H)
          pivot_z = z_nk + 0.2 * (zmax - z_nk)
          Lp = central(pivot_z)
          py = midline_y(Lp, 0.02 * H)[0] if Lp else neck[2]["center"][1]
          lm["head_pivot"] = Vector((0, py, pivot_z))
          lm["head_top"] = Vector((0, py, zmax))
          Lnb = central(neck_base_z)
          nby = midline_y(Lnb, 0.02 * H)[0] if Lnb else py
          lm["neck_base"] = Vector((0, nby, neck_base_z))
          lm["neck_mid"] = Vector((0, (nby + py) / 2, (neck_base_z + pivot_z) / 2))
          pelvis_z = hip_z - 0.03 * H
          Lpv = central(pelvis_z)
          lm["pelvis"] = Vector((0, midline_y(Lpv, 0.03 * H)[0] if Lpv else hip_c[1], pelvis_z))
          for i, f in enumerate((0.226, 0.437, 0.703)):
              z = pelvis_z + f * (neck_base_z - pelvis_z)
              Ls = central(z)
              lm[f"spine{i + 1}"] = Vector((0, midline_y(Ls, 0.03 * H)[0] if Ls else 0.0, z))
          chest_z = lm["spine3"].z
          Lc = central(pelvis_z + 0.55 * (neck_base_z - pelvis_z))
          my, fy, by = midline_y(Lc, 0.03 * H)
          lm["chest_front"] = Vector((0, fy, chest_z))
          lm["chest_back"] = Vector((0, by, chest_z))
      
          # --- arms (left side): start on the forearm (65% of the way to the farthest +X
          # vertex, clear of both torso and fingers), then trace the arm loop inward with
          # x-planes until it merges with the torso: the size jumps (the armpit).
          # Tracing from the fingertip gets lost between the fingers.
          tipv = V[V[:, 0].argmax()]
          lm["hand_tip"] = _v(tipv)
          x0 = 0.65 * float(tipv[0])
          loops = S.cut((x0, 0, 0), (1, 0, 0))
          if not loops:
              raise RuntimeError("landmarks: no arm section at 65% of the arm span")
          start = min(loops, key=lambda L: np.linalg.norm(L["center"][1:] - tipv[1:]))
          arm_loops = []
          prev, prev_size = start["center"], None
          armpit_x = None
          for x in np.linspace(x0, 0.04 * H, 160):
              loops = S.cut((x, 0, 0), (1, 0, 0))
              if not loops:
                  continue
              L = min(loops, key=lambda L: np.linalg.norm(L["center"][1:] - prev[1:]))
              size = max(L["hi"][2] - L["lo"][2], L["hi"][1] - L["lo"][1])
              if prev_size is not None and len(arm_loops) > 4 and (
                      size > 1.5 * prev_size or np.linalg.norm(L["center"][1:] - prev[1:]) > 0.5 * prev_size):
                  armpit_x = float(x)
                  break
              arm_loops.append((float(x), L["center"].copy(), size))
              prev, prev_size = L["center"], size
          if armpit_x is None or len(arm_loops) < 5:
              raise RuntimeError("landmarks: arm does not separate from the torso (arms too close?)")
          # pass 2: cuts normal to the arm axis
          pts = np.array([c for _, c, _ in arm_loops])
          a0, a1 = pts[-1], tipv
          axis = (a1 - a0) / np.linalg.norm(a1 - a0)
          arm_len = float(np.linalg.norm(a1 - a0))
          prof = []
          for f in np.linspace(0.02, 0.97, 80):
              p = a0 + axis * f * arm_len
              loops = S.cut(p, axis)
              near = [L for L in loops if np.linalg.norm(L["center"] - p) < 0.12 * H]
              if not near:
                  continue
              allp = np.vstack([L["pts"] for L in near])
              c = allp.mean(0) if len(near) > 1 else min(near, key=lambda L: np.linalg.norm(L["center"] - p))["center"]
              u = np.cross(axis, [0, 0, 1.0])
              if np.linalg.norm(u) < 1e-6:
                  u = np.array([0, 1.0, 0])
              u /= np.linalg.norm(u)
              w2 = np.cross(axis, u)
              ext = max(np.ptp(allp @ u), np.ptp(allp @ w2))
              prof.append((float(f), c, float(ext)))
          outer = [q for q in prof if 0.6 <= q[0] <= 0.9]
          wf, wc, _ = min(outer, key=lambda q: q[2])
          wrist = _v(wc)
          inner = [q for q in prof if q[0] < 0.12]
          r_arm = float(np.median([q[2] for q in inner])) / 2 if inner else 0.03 * H
          sj = a0 + axis * (-0.6 * r_arm)
          lm["shoulder"] = Vector((sj[0], sj[1], sj[2] + 0.25 * r_arm))
          lm["wrist"] = wrist
          lm["armpit_x"] = armpit_x
          up = lm["wrist"] - lm["shoulder"]
          lm["elbow"] = lm["shoulder"] + up * 0.53
          hand_dir = (lm["hand_tip"] - lm["wrist"])
          lm["hand_end"] = lm["wrist"] + hand_dir * 0.45
          ang = math.degrees(math.atan2(-(lm["wrist"].z - lm["shoulder"].z),
                                        lm["wrist"].x - lm["shoulder"].x))
          lm["arm_angle"] = ang
          lm["pose"] = "T" if ang < 25 else "A"
          # clavicle: high and deep in the shoulder volume (CGDive), from near the mid-line
          depth = lm["chest_back"].y - lm["chest_front"].y
          cz = lm["shoulder"].z + 0.02 * H
          lm["clav_head"] = Vector((0.1 * lm["shoulder"].x, lm["chest_front"].y + 0.3 * depth, cz))
          lm["clav_tail"] = Vector((0.85 * lm["shoulder"].x, lm["shoulder"].y, cz))
          # eyes: centre of each island of the eye object
          if eyes is not None:
              EV = world_coords(eyes, evaluated=True)
              for side, m in (("eye", EV[:, 0] > 0), ("eye_r", EV[:, 0] < 0)):
                  if m.any():
                      lm[side] = _v(EV[m].mean(0))
                      lm[side + "_radius"] = float(np.ptp(EV[m][:, 2]) / 2)
          head = V[V[:, 2] > z_nk]
          lm["head_front_y"] = float(head[np.abs(head[:, 0]) < 0.03 * H][:, 1].min())
          band = head[np.abs(head[:, 2] - (pivot_z + 0.35 * (zmax - pivot_z))) < 0.02 * H]
          lm["head_half_width"] = float(np.abs(band[:, 0]).max()) if len(band) else None
          # front profile of the face (x ~ 0): nose tip = most forward point above the neck,
          # chin bottom = where the profile retreats past halfway back to the neck front
          mid = V[(np.abs(V[:, 0]) < 0.012 * H) & (V[:, 2] > neck_base_z - 0.01 * H)]
          if len(mid) > 20:
              nose = mid[mid[:, 2] > z_nk][mid[mid[:, 2] > z_nk][:, 1].argmin()]
              zs_ = np.linspace(nose[2], neck_base_z, 80)
              prof = []
              for zz in zs_:
                  sl = mid[np.abs(mid[:, 2] - zz) < 0.006 * H]
                  if len(sl):
                      prof.append((float(zz), float(sl[:, 1].min())))
              if prof:
                  neck_front = prof[-1][1]
                  half = (nose[1] + neck_front) / 2
                  fwd = [zz for zz, yy in prof if zz < nose[2] - 0.02 * H and yy < half]
                  chin = min(fwd) if fwd else None      # lowest point still forward = under the chin
                  if chin is not None:
                      if nose[1] > lm["head_pivot"].y:
                          notes.append("nose tip behind the head pivot: does the character face -Y?")
                      lm["nose_tip"] = _v(nose)
                      lm["chin_bottom"] = Vector((0.0, float(half), chin))
          lm["notes"] = notes
          for k, v in (overrides or {}).items():
              lm[k] = Vector(v) if not isinstance(v, (int, float, str)) else v
          return lm
      
      
      # ----------------------------------------------------------------------------------
      # 2. Rigify: metarig, fitting, checks, generation
      # ----------------------------------------------------------------------------------
      METARIGS = {
          "basic": "armature_basic_human_metarig_add",      # no fingers, no face: first rigs
          "human": "armature_human_metarig_add",            # fingers + legacy face (upgrade it)
          "basic_quadruped": "armature_basic_quadruped_metarig_add",
          "wolf": "armature_wolf_metarig_add", "horse": "armature_horse_metarig_add",
          "cat": "armature_cat_metarig_add", "bird": "armature_bird_metarig_add",
          "shark": "armature_shark_metarig_add",
      }
      
      
      def enable_rigify():
          import addon_utils
          if not addon_utils.check("rigify")[1]:
              bpy.ops.preferences.addon_enable(module="rigify")
      
      
      def add_metarig(kind="basic", name=None):
          """Add a Rigify metarig at the world origin (Rigify is off on factory startup)."""
          enable_rigify()
          if bpy.context.view_layer.objects.active and bpy.context.view_layer.objects.active.mode != "OBJECT":
              bpy.ops.object.mode_set(mode="OBJECT")
          bpy.context.scene.cursor.location = (0, 0, 0)
          getattr(bpy.ops.object, METARIGS[kind])()
          meta = bpy.context.active_object
          if name:
              meta.name = name
          return meta
      
      
      def _descendants(eb, root):
          return [b.name for b in eb if b.name == root or any(p.name == root for p in b.parent_recursive)]
      
      
      def _frame(d, n):
          d = d.normalized()
          n = (n - d * n.dot(d)).normalized()
          return Matrix((d, n, d.cross(n))).transposed()
      
      
      def _set_limb_roll(eb, names):
          """Local X = hinge axis = normal of the chain plane (Rigify's own convention)."""
          a, b = eb[names[0]], eb[names[1]]
          n = (a.tail - a.head).cross(b.tail - b.head)
          if n.length < 1e-9:
              return None
          n.normalize()
          for nm in names:
              e = eb[nm]
              e.align_roll(n.cross((e.tail - e.head).normalized()))
          return n
      
      
      def transform_bones(eb, names, M):
          """Edit Mode: apply matrix M to a group of bones (heads, tails, rolls) safely.
      
          EditBone.transform() on connected chains moves shared ends twice (a parent's tail
          is also the child's head); this writes every end once from a snapshot."""
          names = [n for n in names if n in eb]
          R3 = M.to_3x3().normalized()
          snap = {n: (eb[n].head.copy(), eb[n].tail.copy(), eb[n].z_axis.copy(), eb[n].use_connect,
                      eb[n].parent.name if eb[n].parent else None) for n in names}
          for n in names:
              eb[n].tail = M @ snap[n][1]
          for n in names:
              if not (snap[n][3] and snap[n][4] in snap):
                  eb[n].head = M @ snap[n][0]
          for n in names:
              eb[n].align_roll(R3 @ snap[n][2])
      
      
      def move_points(eb, moves, eps=1e-5):
          """Edit Mode: move every bone end sitting on an old point to its new point.
      
          moves = [(old, new), ...]. This is CGDive's "box-select the joint and move all
          ends together": coincident ends stay coincident (Rigify chains, face glue, lid
          corners). Tails first, then heads of disconnected bones only, because setting a
          parent's tail already moves a connected child's head."""
          def target(co):
              for old, new in moves:
                  if (co - old).length < eps:
                      return new
              return None
          snap = {b.name: (b.head.copy(), b.tail.copy(), b.use_connect) for b in eb}
          for b in eb:
              t = target(snap[b.name][1])
              if t is not None:
                  b.tail = t
          for b in eb:
              if not snap[b.name][2]:
                  h = target(snap[b.name][0])
                  if h is not None:
                      b.head = h
      
      
      set_limb_roll = _set_limb_roll
      
      
      def fit_metarig(meta, lm, knee_bend=0.012, elbow_bend=0.012, support_bones=True, move_eyes=True):
          """Place a Rigify human/basic-human metarig on landmarks() output.
      
          Joints are written as shared points so coincident ends stay coincident (CGDive:
          move joints as units). Knees get a forward and elbows a backward pre-bend of
          `*_bend` x limb length at mid-chain (about 2.7 deg between segments), chains stay
          straight in the other view, limb rolls put the hinge on local X, and the left side
          is symmetrized to the right. Human metarig: hand+fingers and face are carried by a
          similarity transform (coarse: check the fist test and face renders), eye bones
          and lids moved onto the eye centres. Call before rigify_upgrade_face()."""
          activate(meta, "EDIT")
          eb = meta.data.edit_bones
          P = {k: Vector(v) for k, v in lm.items() if isinstance(v, Vector)}
          human = "palm.01.L" in eb
          face = "face" in eb
          # defaults captured before editing
          if human:
              hand_grp = _descendants(eb, "hand.L")
              W0, T0 = eb["hand.L"].head.copy(), eb["f_middle.03.L"].tail.copy()
              n0 = (eb["upper_arm.L"].tail - eb["upper_arm.L"].head).cross(eb["forearm.L"].tail - eb["forearm.L"].head)
          if face:
              face_grp = _descendants(eb, "face")
              piv0, top0 = eb["spine.006"].head.copy(), eb["spine.006"].tail.copy()
              eye0 = eb["eye.L"].head.copy() if "eye.L" in eb else None
          # spine, neck, head (centre line)
          chain = ["pelvis", "spine1", "spine2", "spine3", "neck_base"]
          for i, nm in enumerate(("spine", "spine.001", "spine.002", "spine.003")):
              eb[nm].head, eb[nm].tail = P[chain[i]], P[chain[i + 1]]
          eb["spine.004"].head, eb["spine.004"].tail = P["neck_base"], P["neck_mid"]
          eb["spine.005"].head, eb["spine.005"].tail = P["neck_mid"], P["head_pivot"]
          eb["spine.006"].head, eb["spine.006"].tail = P["head_pivot"], P["head_top"]
          for nm in ("spine", "spine.001", "spine.002", "spine.003", "spine.004", "spine.005", "spine.006"):
              eb[nm].align_roll(Vector((0, -1, 0)))
          # leg with forward knee pre-bend, straight from the front
          hip, knee, ankle = P["hip"], P["knee"].copy(), P["ankle"]
          L = (ankle - hip).length
          t = (knee.z - hip.z) / (ankle.z - hip.z)
          line = hip.lerp(ankle, t)
          knee.x = line.x
          knee.y = min(knee.y, line.y - knee_bend * L)
          eb["thigh.L"].head, eb["thigh.L"].tail = hip, knee
          eb["shin.L"].head, eb["shin.L"].tail = knee, ankle
          eb["foot.L"].head, eb["foot.L"].tail = ankle, P["ball"]
          eb["toe.L"].head, eb["toe.L"].tail = P["ball"], P["toe_tip"]
          eb["heel.02.L"].head, eb["heel.02.L"].tail = P["heel_in"], P["heel_out"]
          _set_limb_roll(eb, ["thigh.L", "shin.L"])
          f = eb["foot.L"]
          f.align_roll(Vector((1, 0, 0)).cross((f.tail - f.head).normalized()))
          eb["toe.L"].align_roll(Vector((0, 0, 1)))
          eb["heel.02.L"].align_roll(Vector((0, 0, 1)))
          # arm with backward elbow pre-bend (hinge axis from the bend), shoulder high and deep
          sh, wr = P["shoulder"], P["wrist"]
          el = sh.lerp(wr, 0.53)
          el.y += elbow_bend * (wr - sh).length
          eb["shoulder.L"].head, eb["shoulder.L"].tail = P["clav_head"], P["clav_tail"]
          eb["shoulder.L"].align_roll(Vector((0, 0, 1)))
          eb["upper_arm.L"].head, eb["upper_arm.L"].tail = sh, el
          eb["forearm.L"].head, eb["forearm.L"].tail = el, wr
          if human:
              n1 = (el - sh).cross(wr - el)
              T = P["hand_tip"]
              R = _frame(T - wr, n1) @ _frame(T0 - W0, n0).inverted()
              s = (T - wr).length / (T0 - W0).length
              M = Matrix.Translation(wr) @ R.to_4x4() @ Matrix.Scale(s, 4) @ Matrix.Translation(-W0)
              transform_bones(eb, hand_grp, M)
              eb["forearm.L"].tail = wr
              _set_limb_roll(eb, ["upper_arm.L", "forearm.L"])
          else:
              eb["hand.L"].head, eb["hand.L"].tail = wr, P["hand_end"]
              _set_limb_roll(eb, ["upper_arm.L", "forearm.L"])
              n = (el - sh).cross(wr - el).normalized()
              h = eb["hand.L"]
              h.align_roll(n.cross((h.tail - h.head).normalized()))
          # support bones for automatic weights (CGDive: delete them if you paint the chest)
          if support_bones and "breast.L" in eb:
              depth = P["chest_back"].y - P["chest_front"].y
              z = P["spine3"].z
              x = 0.6 * sh.x
              eb["breast.L"].head = Vector((x, P["chest_front"].y + 0.7 * depth, z))
              eb["breast.L"].tail = Vector((x, P["chest_front"].y + 0.05 * depth, z))
              eb["pelvis.L"].head = P["pelvis"]
              eb["pelvis.L"].tail = Vector((1.13 * hip.x, hip.y - 0.03 * lm["H"], hip.z + 0.04 * lm["H"]))
          elif not support_bones:
              for nm in ("breast.L", "breast.R", "pelvis.L", "pelvis.R"):
                  if nm in eb:
                      eb.remove(eb[nm])
          # face: similarity from the metarig head to the mesh head, then eyes onto the eyeballs
          if face:
              piv, top = P["head_pivot"], P["head_top"]
              s = (top.z - piv.z) / (top0.z - piv0.z)
              w0 = max(max(abs(eb[n].head.x), abs(eb[n].tail.x)) for n in face_grp)
              sx = (lm["head_half_width"] / w0) if lm.get("head_half_width") else s
              if all(k in P for k in ("nose_tip", "chin_bottom", "eye")) and P["chin_bottom"].z < P["nose_tip"].z < P["eye"].z:
                  # piecewise vertical map through chin bottom, nose tip, eye line, head top
                  pts0 = [(n, e) for n in face_grp for e in (eb[n].head, eb[n].tail)]
                  nose0 = min(pts0, key=lambda t: t[1].y)[1].copy()
                  chin0 = eb["chin"].head.copy() if "chin" in eb else None
                  k0 = [chin0.z, nose0.z, eye0.z, top0.z]
                  k1 = [P["chin_bottom"].z, P["nose_tip"].z, P["eye"].z, top.z]
                  sy = (k1[2] - k1[0]) / (k0[2] - k0[0])
      
                  def fz(z):
                      if z <= k0[0]:
                          return k1[0] + (z - k0[0]) * (k1[1] - k1[0]) / (k0[1] - k0[0])
                      if z >= k0[3]:
                          return k1[3] + (z - k0[3]) * (k1[3] - k1[2]) / (k0[3] - k0[2])
                      return float(np.interp(z, k0, k1))
                  snap = {n: (eb[n].head.copy(), eb[n].tail.copy(), eb[n].z_axis.copy(), eb[n].use_connect,
                              eb[n].parent.name if eb[n].parent else None) for n in face_grp}
      
                  def f(p):
                      return Vector((p.x * sx, P["nose_tip"].y + (p.y - nose0.y) * sy, fz(p.z)))
                  for n in face_grp:
                      eb[n].tail = f(snap[n][1])
                  for n in face_grp:
                      if not (snap[n][3] and snap[n][4] in snap):
                          eb[n].head = f(snap[n][0])
                  for n in face_grp:
                      eb[n].align_roll(snap[n][2])
              else:
                  M = Matrix.Translation(piv) @ Matrix.Diagonal((sx, s, s, 1.0)) @ Matrix.Translation(-piv0)
                  transform_bones(eb, face_grp, M)
                  front = min(min(eb[n].head.y, eb[n].tail.y) for n in face_grp)
                  dy = lm.get("head_front_y", front) - front
                  transform_bones(eb, face_grp, Matrix.Translation((0, dy, 0)))
              if move_eyes and "eye" in P and eye0 is not None:
                  d = P["eye"] - eb["eye.L"].head
                  moves = []
                  for nm in [n for n in face_grp if side_of(n) == "L" and n.split(".")[0] in ("eye", "lid")]:
                      moves += [(eb[nm].head.copy(), eb[nm].head + d), (eb[nm].tail.copy(), eb[nm].tail + d)]
                  move_points(eb, moves)
          # mirror the left side onto the right (rig types and parameters included)
          for b in eb:
              b.select = b.select_head = b.select_tail = side_of(b.name) == "L"
          bpy.ops.armature.symmetrize(direction="NEGATIVE_X")
          for b in eb:
              b.select = b.select_head = b.select_tail = False
          bpy.ops.object.mode_set(mode="OBJECT")
          return meta
      
      
      _BVH = {}
      
      
      def _bvh(obj):
          from mathutils.bvhtree import BVHTree
          key = (obj.name, len(obj.data.vertices), tuple(round(v, 6) for v in obj.matrix_world.translation))
          if key not in _BVH:
              V = world_coords(obj, evaluated=False)
              faces = [tuple(p.vertices) for p in obj.data.polygons]
              _BVH[key] = BVHTree.FromPolygons([tuple(v) for v in V], faces)
          return _BVH[key]
      
      
      _DIRS = [Vector(d).normalized() for d in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1),
                                                (1, 1, 1), (1, 1, -1), (1, -1, 1), (1, -1, -1), (-1, 1, 1),
                                                (-1, 1, -1), (-1, -1, 1), (-1, -1, -1))]
      
      
      def inside(obj, p, need=12):
          """True if point p (world) is enclosed by the mesh: rays in >= need of 14 directions
          hit it. Tolerates open holes (eye sockets, neck cuts) that break parity tests."""
          t = _bvh(obj)
          p = Vector(p)
          hits = sum(1 for d in _DIRS if t.ray_cast(p, d)[0] is not None)
          return hits >= need
      
      
      def bend_angle(meta_or_rig, a, b):
          """Angle in degrees between two consecutive bones at rest."""
          bones = meta_or_rig.data.bones
          u = (bones[a].tail_local - bones[a].head_local).normalized()
          v = (bones[b].tail_local - bones[b].head_local).normalized()
          return math.degrees(u.angle(v))
      
      
      def check_metarig(meta, body=None):
          """Problems that break Rigify generation or deformation. [] = ready to generate."""
          probs = []
          if any(abs(s - 1) > 1e-5 for s in meta.scale) or any(abs(r) > 1e-5 for r in meta.rotation_euler):
              probs.append("metarig scale/rotation not applied (generated rig differs in size or pose)")
          if any(pb.matrix_basis != Matrix.Identity(4) for pb in meta.pose.bones):
              probs.append("metarig pose != rest: Apply Pose as Rest Pose (pose.armature_apply)")
          for pb in meta.pose.bones:
              b, p = pb.bone, pb.bone.parent
              if p is None:
                  continue
              need = b.use_connect or bool(pb.rigify_type and getattr(pb.rigify_parameters, "connect_chain", False))
              if need and (b.head_local - p.tail_local).length > 1e-4:
                  probs.append(f"disjoint chain at {b.name} (Rigify: 'bone position is disjoint')")
          bones = meta.data.bones
          for s in ("L", "R"):
              if f"thigh.{s}" in bones:
                  ang = bend_angle(meta, f"thigh.{s}", f"shin.{s}")
                  th, sh = bones[f"thigh.{s}"], bones[f"shin.{s}"]
                  t = (sh.head_local.z - th.head_local.z) / (sh.tail_local.z - th.head_local.z)
                  liney = th.head_local.lerp(sh.tail_local, t).y
                  if not (1.0 <= ang <= 15.0) or sh.head_local.y > liney:
                      probs.append(f"knee.{s}: bend {ang:.1f} deg (want 1-15, forward): IK direction ambiguous")
              if f"upper_arm.{s}" in bones:
                  ang = bend_angle(meta, f"upper_arm.{s}", f"forearm.{s}")
                  if not (1.0 <= ang <= 15.0):
                      probs.append(f"elbow.{s}: bend {ang:.1f} deg (want 1-15, backward)")
              for a, b in (("upper_arm", "forearm"), ("thigh", "shin")):
                  if f"{a}.{s}" in bones:
                      u, v = bones[f"{a}.{s}"], bones[f"{b}.{s}"]
                      n = (u.tail_local - u.head_local).cross(v.tail_local - v.head_local)
                      if n.length > 1e-9:
                          n.normalize()
                          for bb in (u, v):
                              x = bb.matrix_local.col[0].xyz
                              if abs(x.dot(n)) < 0.98:
                                  probs.append(f"{bb.name}: hinge not on local X (roll), dot={x.dot(n):.2f}")
          if not any(getattr(c, "rigify_ui_row", 0) > 0 for c in meta.data.collections_all):
              probs.append("no bone collection has a Rigify UI row ('No bone collections have UI buttons')")
          upgraded = [pb for pb in meta.pose.bones if pb.rigify_type == "skin.glue"]
          ends = [(b.head_local, b.tail_local) for b in bones]
          for pb in upgraded:
              for e in (pb.bone.head_local, pb.bone.tail_local):
                  if sum(1 for h, t in ends if (h - e).length < 1e-5 or (t - e).length < 1e-5) < 2:
                      probs.append(f"glue {pb.name} end not coincident with another bone")
                      break
          if body is not None:
              for nm in ("spine", "spine.002", "spine.004", "spine.006", "thigh.L", "shin.L", "foot.L",
                         "upper_arm.L", "forearm.L", "hand.L"):
                  if nm in bones:
                      b = bones[nm]
                      mid = meta.matrix_world @ ((b.head_local + b.tail_local) / 2)
                      if not inside(body, mid):
                          probs.append(f"{nm}: bone middle outside the mesh volume")
          return probs
      
      
      def upgrade_face(meta, lid_follow_z=0.3, jaw_lip=0.9, overwrite_widgets=True):
          """Legacy face -> modular face (irreversible, after fitting) + CGDive's tuning."""
          activate(meta)
          if any(pb.rigify_type == "faces.super_face" for pb in meta.pose.bones):
              bpy.ops.pose.rigify_upgrade_face()
          for s in "LR":
              pb = meta.pose.bones.get(f"eye.{s}")
              if pb is not None and hasattr(pb.rigify_parameters, "eyelid_follow_default"):
                  x = pb.rigify_parameters.eyelid_follow_default[0]
                  pb.rigify_parameters.eyelid_follow_default = (x, lid_follow_z)
          jm = meta.pose.bones.get("jaw_master")
          if jm is not None and hasattr(jm.rigify_parameters, "jaw_mouth_influence"):
              jm.rigify_parameters.jaw_mouth_influence = jaw_lip
          meta.data.rigify_force_widget_update = overwrite_widgets
          return len(meta.data.bones)
      
      
      def generate(meta):
          """Generate (or regenerate in place) the rig. Returns (rig, None) or (None, message).
      
          On failure Rigify leaves a partial rig active; this re-activates the metarig so
          the named bone can be fixed and generate() called again."""
          activate(meta)
          try:
              bpy.ops.pose.rigify_generate()
          except RuntimeError as e:
              lines = [ln for ln in str(e).splitlines() if ln.strip()]
              msg = next((ln for ln in lines if "RIGIFY ERROR" in ln), lines[0] if lines else str(e))
              activate(meta)
              return None, msg.strip()
          rig = meta.data.rigify_target_rig
          meta.hide_set(True)
          return rig, None
      
      
      # ----------------------------------------------------------------------------------
      # bone placement overlay (X-ray render: are the joints in the volume?)
      # ----------------------------------------------------------------------------------
      def bones_mesh(arm_obj, name="BX_bones", only=None, radius=0.06):
          """Octahedral bone shapes as a mesh (armatures do not render), world space."""
          bm = bmesh.new()
          mw = arm_obj.matrix_world
          for b in arm_obj.data.bones:
              if only and not only(b):
                  continue
              h, t = mw @ b.head_local, mw @ b.tail_local
              L = (t - h).length
              if L < 1e-6:
                  continue
              x = (mw.to_3x3() @ b.matrix_local.col[0].xyz).normalized() * L * radius
              z = (mw.to_3x3() @ b.matrix_local.col[2].xyz).normalized() * L * radius
              m = h + (t - h) * 0.15
              vs = [bm.verts.new(p) for p in (h, m + x, m + z, m - x, m - z, t)]
              for i in range(4):
                  a, c = vs[1 + i], vs[1 + (i + 1) % 4]
                  bm.faces.new((vs[0], c, a))
                  bm.faces.new((vs[5], a, c))
          me = bpy.data.meshes.new(name)
          bm.to_mesh(me)
          bm.free()
          return bpy.data.objects.new(name, me)
      
      
      def placement_sheet(objs, out_dir, arm_obj=None, points=None, views=("front", "right"), res=600,
                          only=None, name="placement", center=None, size=None):
          """Bones (and optional points) drawn over an X-ray render of the meshes.
      
          The agent's version of checking bone placement from front, side and top: are
          joints in the centre of the volume, knees and elbows pre-bent, spine inside?"""
          os.makedirs(out_dir, exist_ok=True)
          sc = bpy.data.scenes.new("BX_Placement")
          sc.render.engine = "BLENDER_WORKBENCH"
          sc.render.resolution_x = sc.render.resolution_y = res
          sc.render.image_settings.media_type = "IMAGE"
          sc.render.image_settings.file_format = "PNG"
          sc.render.image_settings.color_mode = "RGBA"
          sc.view_settings.view_transform = "Standard"
          sh = sc.display.shading
          sh.light, sh.color_type = "STUDIO", "OBJECT"
          world = bpy.data.worlds.new("BX_Placement_W")
          world.color = (0.92, 0.92, 0.92)
          sc.world = world
          body_col = bpy.data.collections.new("BX_body")
          bone_col = bpy.data.collections.new("BX_bonesc")
          sc.collection.children.link(body_col)
          sc.collection.children.link(bone_col)
          temp = []
          for o in objs:
              body_col.objects.link(o)
          saved = {o: tuple(o.color) for o in objs}
          for o in objs:
              o.color = (0.55, 0.55, 0.55, 1)
          if arm_obj is not None:
              for side, col, test in (("L", (0.1, 0.35, 1, 1), lambda b: side_of(b.name) == "L"),
                                      ("R", (1, 0.15, 0.1, 1), lambda b: side_of(b.name) == "R"),
                                      ("C", (1, 0.75, 0.0, 1), lambda b: side_of(b.name) == "")):
                  bo = bones_mesh(arm_obj, "BX_bones_" + side,
                                  only=(lambda b, t=test: t(b) and (only(b) if only else True)))
                  bo.color = col
                  bone_col.objects.link(bo)
                  temp.append(bo)
          if points:
              bm = bmesh.new()
              for p in points:
                  bmesh.ops.create_icosphere(bm, subdivisions=1, radius=0.008, matrix=Matrix.Translation(Vector(p)))
              me = bpy.data.meshes.new("BX_pts")
              bm.to_mesh(me)
              bm.free()
              po = bpy.data.objects.new("BX_pts", me)
              po.color = (0, 0.7, 0.2, 1)
              bone_col.objects.link(po)
              temp.append(po)
          cam = bpy.data.objects.new("BX_PCam", bpy.data.cameras.new("BX_PCam"))
          sc.collection.objects.link(cam)
          sc.camera = cam
          V = np.vstack([world_coords(o, evaluated=False) for o in objs if o.type == "MESH"])
          lo, hi = V.min(0), V.max(0)
          c = Vector(center) if center is not None else Vector(((lo + hi) / 2).tolist())
          size = size or float(max(hi - lo)) * 1.1
          dirs = {"front": Vector((0, -1, 0)), "right": Vector((1, 0, 0)), "left": Vector((-1, 0, 0)),
                  "back": Vector((0, 1, 0)), "top": Vector((0, 0, 1))}
          paths = []
      
          def shot(path, bodies, bones, xray, transparent):
              body_col.hide_render, bone_col.hide_render = not bodies, not bones
              sh.show_xray, sh.xray_alpha = xray, 0.3
              sc.render.film_transparent = transparent
              sc.render.filepath = path
              bpy.ops.render.render(write_still=True, scene=sc.name)
              im = bpy.data.images.load(path, check_existing=False)
              a = np.array(im.pixels[:], dtype=np.float32).reshape(im.size[1], im.size[0], 4)
              bpy.data.images.remove(im)
              return a
          try:
              for v in views:
                  d = dirs[v]
                  cam.data.type, cam.data.ortho_scale = "ORTHO", size
                  cam.location = c + d * size * 3
                  up = Vector((0, 0, 1)) if abs(d.z) < 0.9 else Vector((0, 1, 0))
                  cam.rotation_euler = _look_rot(-d, up).to_euler()
                  cam.data.clip_end = size * 10
                  p = os.path.join(out_dir, f"{name}_{v}.png")
                  A = shot(p, True, False, True, False)
                  if temp:
                      B = shot(p, False, True, False, True)
                      al = B[..., 3:4]
                      A[..., :3] = B[..., :3] * al + A[..., :3] * (1 - al)
                  img = bpy.data.images.new("BX_comp", A.shape[1], A.shape[0], alpha=True)
                  img.pixels = A.ravel()
                  img.filepath_raw = p
                  img.file_format = "PNG"
                  img.save()
                  bpy.data.images.remove(img)
                  paths.append(p)
              sheet = tile_images([paths], os.path.join(out_dir, f"{name}_sheet.png"))
          finally:
              for o, col in saved.items():
                  o.color = col
              for t in temp:
                  me = t.data
                  bpy.data.objects.remove(t)
                  bpy.data.meshes.remove(me)
              cd = cam.data
              bpy.data.objects.remove(cam)
              bpy.data.cameras.remove(cd)
              bpy.data.collections.remove(body_col)
              bpy.data.collections.remove(bone_col)
              bpy.data.scenes.remove(sc)
              bpy.data.worlds.remove(world)
          return sheet
      
      
      def _look_rot(fwd, up):
          fwd = fwd.normalized()
          right = fwd.cross(up).normalized()
          up2 = right.cross(fwd)
          return Matrix((right, up2, -fwd)).transposed()
      
      
      def tile_images(rows, out):
          """Compose a grid of PNG paths (list of rows) into one PNG with numpy."""
          imgs = []
          for row in rows:
              r = []
              for p in row:
                  im = bpy.data.images.load(p, check_existing=False)
                  w, h = im.size
                  r.append(np.array(im.pixels[:], dtype=np.float32).reshape(h, w, 4))
                  bpy.data.images.remove(im)
              imgs.append(r)
          h = max(a.shape[0] for r in imgs for a in r)
          w = max(a.shape[1] for r in imgs for a in r)
          ncol = max(len(r) for r in imgs)
          sheet = np.ones((len(imgs) * h, ncol * w, 4), dtype=np.float32)
          for i, r in enumerate(imgs):
              for j, a in enumerate(r):
                  y0 = (len(imgs) - 1 - i) * h
                  sheet[y0:y0 + a.shape[0], j * w:j * w + a.shape[1]] = a
          img = bpy.data.images.new("BX_tile", ncol * w, len(imgs) * h, alpha=True)
          img.pixels = sheet.ravel()
          img.filepath_raw = out
          img.file_format = "PNG"
          img.save()
          bpy.data.images.remove(img)
          return out
      
      
      # ----------------------------------------------------------------------------------
      # 3. weights: bind, verify, fall back, clean (data-level, mode independent)
      # ----------------------------------------------------------------------------------
      INTERNAL_PARTS = ("teeth", "tongue", "eye")
      
      
      def deform_names(rig):
          return {b.name for b in rig.data.bones if b.use_deform}
      
      
      def weight_matrix(obj, groups=None):
          """(n_verts, n_groups) weight array and the group names (all groups by default)."""
          names = [g.name for g in obj.vertex_groups] if groups is None else list(groups)
          col = {obj.vertex_groups[n].index: j for j, n in enumerate(names) if n in obj.vertex_groups}
          W = np.zeros((len(obj.data.vertices), len(names)), dtype=np.float64)
          for v in obj.data.vertices:
              for g in v.groups:
                  j = col.get(g.group)
                  if j is not None:
                      W[v.index, j] = g.weight
          return W, names
      
      
      def write_weights(obj, W, names, eps=1e-6):
          """Replace the weights of `names` with the columns of W (zeros are removed)."""
          allv = list(range(len(obj.data.vertices)))
          for j, n in enumerate(names):
              vg = obj.vertex_groups.get(n) or obj.vertex_groups.new(name=n)
              vg.remove(allv)
              nz = np.nonzero(W[:, j] > eps)[0]
              if len(nz) == 0:
                  continue
              # vg.add takes one weight per call: batch equal weights
              vals = np.round(W[nz, j], 6)
              order = np.argsort(vals)
              vs, idx = vals[order], nz[order]
              cuts = np.nonzero(np.diff(vs))[0] + 1
              for a, b in zip(np.r_[0, cuts], np.r_[cuts, len(vs)]):
                  vg.add(idx[a:b].tolist(), float(vs[a]), "REPLACE")
      
      
      def weight_report(obj, rig, expect_inside=True):
          """Numbers that catch silent auto-weight failure and export problems.
      
          unweighted: vertices with total deform weight < 1e-4 (they will not move)
          empty_inside: deform groups with no weight although the bone lies inside the mesh
          non_deform_groups: groups for bones that do not deform (or no bone at all)
          max_influences / over4: influences per vertex (game engines usually take 4)
          sum_dev: vertices whose deform weights do not sum to 1 within 0.01
          mirror_err: worst L/R weight mismatch on mirrored vertex pairs (symmetric meshes)"""
          dn = deform_names(rig)
          W, names = weight_matrix(obj)
          is_def = np.array([n in dn for n in names], dtype=bool)
          Wd = W[:, is_def] if is_def.any() else np.zeros((len(W), 0))
          tot = Wd.sum(1) if Wd.size else np.zeros(len(W))
          rep = {"verts": len(W), "groups": len(names)}
          rep["unweighted"] = int((tot < 1e-4).sum())
          rep["sum_dev"] = int((np.abs(tot - 1) > 0.01).sum())
          cnt = (Wd > 1e-4).sum(1) if Wd.size else np.zeros(len(W), int)
          rep["max_influences"] = int(cnt.max()) if len(cnt) else 0
          rep["over4"] = int((cnt > 4).sum())
          rep["non_deform_groups"] = [n for n in names if n not in dn]
          empty = []
          if expect_inside and obj.type == "MESH":
              dnames = [n for n in names if n in dn]
              gsum = dict(zip(dnames, Wd.sum(0))) if Wd.size else {}
              for n in dnames:
                  if gsum.get(n, 0) < 1e-3:
                      b = rig.data.bones[n]
                      mid = rig.matrix_world @ ((b.head_local + b.tail_local) / 2)
                      if inside(obj, mid):
                          empty.append(n)
          rep["empty_inside"] = empty
          # heat failure signature: unweighted vertices, or a MAJOR bone inside the mesh with
          # nothing. Small face bones and internal parts (teeth, tongue, eyes) can be empty
          # on the skin legitimately.
          Hm = float(max(rig.dimensions.z, 1e-6))
          major = [n for n in empty if rig.data.bones[n].length >= 0.05 * Hm
                   and not any(k in n for k in INTERNAL_PARTS)]
          rep["empty_major"] = major
          # mirror consistency (only meaningful for symmetric meshes)
          V = world_coords(obj, evaluated=False)
          kd = KDTree(len(V))
          for i, co in enumerate(V):
              kd.insert(co, i)
          kd.balance()
          col = {n: j for j, n in enumerate(names)}
          worst, pairs = 0.0, 0
          for i in np.nonzero(V[:, 0] > 1e-4)[0][::7]:
              co, j, dist = kd.find((-V[i, 0], V[i, 1], V[i, 2]))
              if dist > 1e-4:
                  continue
              pairs += 1
              for n, c in col.items():
                  s = side_of(n)
                  if not s or W[i, c] < 1e-4:
                      continue
                  m = n.replace(".L", ".R") if s == "L" else n.replace(".R", ".L")
                  if m in col:
                      worst = max(worst, abs(W[i, c] - W[j, col[m]]))
          rep["mirror_pairs_sampled"] = pairs
          rep["mirror_err"] = round(float(worst), 4) if pairs else None
          rep["ok"] = rep["unweighted"] == 0 and not major
          return rep
      
      
      def clean_weights(obj, rig, limit=0.01, max_influences=None, normalize=True):
          """Remove weights below `limit`, optionally keep the N largest, normalize deform
          weights to 1 (Demeter: clean near-zero weights before blurring; games: 4)."""
          dn = deform_names(rig)
          W, names = weight_matrix(obj)
          d = np.array([n in dn for n in names], dtype=bool)
          Wd = W[:, d]
          Wd[Wd < limit] = 0.0
          if max_influences:
              if Wd.shape[1] > max_influences:
                  kth = np.sort(Wd, axis=1)[:, -max_influences][:, None]
                  Wd[Wd < kth] = 0.0
                  # ties can leave more than N: keep first N
                  over = (Wd > 0).sum(1) > max_influences
                  for i in np.nonzero(over)[0]:
                      keep = np.argsort(-Wd[i])[:max_influences]
                      row = np.zeros_like(Wd[i])
                      row[keep] = Wd[i, keep]
                      Wd[i
  • SKILL.md 17.4 KB
    ---
    name: scenario-blender-rigging
    description: 'Use when rigging a character, creature or prop in Blender through Python: "rig this character", Rigify metarig fitting or generation errors, automatic weights or "Bone Heat Weighting failed", weight painting fixes, clothes that do not follow, IK/FK switch, foot roll, twist bones, drivers and custom properties, corrective shape keys, a stylized face rig, a mechanical or hard-surface rig, or testing how a rig deforms.'
    license: MIT
    ---
    
    # Blender rigging
    
    Expert rigging means the rig is specified by bone placement, proven by deformation under stress poses, and shaped around what the animator will actually use. The stance of every source expert: place joints precisely, generate or build the simplest mechanism that works, then pose it, measure it, render it and fix it before calling it done. If a sibling skill named here is missing from your available skills, ask the user to install it (`npx skills add scenario-labs/skills --skill <name>`); unattended, proceed from tool schemas and flag the gap.
    
    **REQUIRED BACKGROUND:** scenario-blender-expert (execution channel, review loop, 5.2 API changes).
    
    Module: [`scripts/bx_rig.py`](scripts/bx_rig.py) (tested on 5.2.1, headless and GUI). `import sys; sys.path.append("<skills>/scenario-blender-rigging/scripts"); import bx_rig as R`.
    
    ## Stance (the expert delta)
    
    - **Placement is the specification.** Joints sit in the center of the volume they move, not on the anatomical skeleton ("bones simulate bone, muscle and tissue"); knees and elbows get a slight pre-bend because the chain's curvature IS the IK plane; rolls put each hinge on one local axis so the animator uses one curve (CGDive; Demeter: the bend, not the roll, decided where his pole landed).
    - **The metarig is the source, the generated rig is disposable.** Every fix goes into metarig bones, Rigify parameters or a post-generation script; regenerating keeps control names, so weights and animation survive (CGDive, Demeter, Ivan Cappiello).
    - **Automatic weights fail silently.** `parent_set` returns FINISHED while a warning says bone heat failed; measure unweighted vertices and empty deform groups, and fall back to a voxel proxy plus Data Transfer (CGDive; verified). They also leak across the crotch and armpits: strip .L weights from the right half.
    - **Weights are judged in motion, firm beats mushy.** Paint in pose or against a test animation; limit each seam to 2 or 3 bones ("bones fighting for influence"); slight intersection is better than rubbery transitions; rigid parts are assigned at 1.0, never painted (CGDive, Demeter, Dikko).
    - **Everything that deforms together gets weights from the same source,** transferred and applied, so garments cannot drift while animating (Dikko, CGDive).
    - **Export decides the deformation tools.** Four twist bones per segment instead of Preserve Volume; Preserve Volume, Corrective Smooth and B-bones are Blender-only, which is why Rigify segments its DEF bones (CGDive, Ivan).
    - **The animator is the client.** Cut features nobody uses, restrict only what breaks the rig, keep automated bones hand-animatable (constraint plus child, Mix Add, Transformation instead of drivers), stop when fit for purpose (Wayne Dixon, Demeter, CGDive).
    - **Faces are per-character hybrids.** There is no generic stylized face (Demeter); production faces mix joints, ribbons, shape keys and lattices (Rik Schutte).
    
    ## Establish first
    
    | Input                                         | Changes                                                                                            | Default when silent                                    |
    | --------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
    | Target: Blender render vs game/FBX            | segmented DEF bones, max 4 influences, no Preserve Volume / Corrective Smooth / B-bone deformation | Blender-only keyframe animation                        |
    | Character: biped, quadruped, mechanical, prop | metarig preset or custom samples; rigid islands                                                    | biped, Rigify                                          |
    | Face need and style                           | none / Rigify face / bones + shape keys + on-face controls                                         | body rig + eye and jaw bones only                      |
    | Fingers                                       | Rigify Human (fingers + face) vs Basic Human                                                       | Basic Human (CGDive: first rig)                        |
    | Pose, pieces, scale                           | T or A pose, separate eyes/teeth/clothes, real scale                                               | measure; fix in the model check                        |
    | Animator conventions                          | FK/IK defaults, side colors, finger controls                                                       | legs IK, arms FK, follow 0; left blue, right red (ask) |
    
    ## Choose the path
    
    | Rigify                                                                                                   | Manual / scripted rig                                                                                                                            |
    | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
    | Humans and animals with standard anatomy, many characters sharing controls, fast production-grade result | Unusual anatomy, mechanical parts, strict game-engine hierarchies, rigs that must stay small, or when the brief asks for a custom control scheme |
    | `add_metarig` then `fit_metarig`, `generate`                                                             | `new_bone`, `twist_chain`, `fk_ik_switch`, `ik_chain`, `foot_roll`, `make_def_layer`, `organize`                                                 |
    | Metarig types: arm exactly 3 bones, leg 4 + heel, spine 3+, super_copy 1 bone                            | DEF / MCH / CTRL: only DEF deforms, MCH is never animated, everything else is a control                                                          |
    
    ## Workflow
    
    1. **Model check.** `R.model_check([body, *parts], fix=True)`: transforms applied (shape keys included), feet on Z=0, centered on X, facing -Y, about 1.8 m for an adult human. Apply Mirror modifiers or keep their mirror object. GATE: `model_check(...)["problems"] == []`.
    2. **Landmarks.** `lm = R.landmarks(body, eyes=eyes)`: cross-sections find crotch, hips, ankle, neck, armpit, wrist; eye centers from the eye mesh; nose tip and chin from the face profile. Override any joint with `overrides={...}`. GATE: joint order sane, `lm["notes"]` empty or explained.
    3. **Fit.** Rigify: `meta = R.add_metarig("basic"|"human")`, `R.fit_metarig(meta, lm)` (joints as shared points, pre-bends, hinge rolls, left side symmetrized; hands and face by similarity, coarse). Manual: build the deform skeleton with `new_bone` from the same landmarks. GATE: `R.check_metarig(meta, body) == []` AND `R.placement_sheet([body, eyes], out, arm_obj=meta, views=("front","right","top"))` looked at; zoom the head with `center=`, `size=`.
    4. **Face (Rigify Human).** Fit, then `R.upgrade_face(meta)` (irreversible; eyelid follow Z 0.3, bottom lip 0.9, Overwrite Widget Meshes on). Stylized hero face: bones for jaw, eyes, tongue plus shape keys instead (stage 7).
    5. **Generate or build.** `rig, err = R.generate(meta)`; `err` is the first Rigify error line naming the bone ("Cannot connect chain - bone position is disjoint", "Expected 2 eye corners"): fix the metarig, regenerate. Manual: twist chains, FK/IK layers with settings properties, IK with solved pole angle, foot roll, symmetrize, `mirror_drivers`, `make_def_layer`, `organize`, widgets. GATE (manual): `static_cycles == []`, `cycle_check() == []`, `root_test < 1e-4`, FK and IK coincide at rest, pole error < 1e-3.
    6. **Bind.** `rep = R.bind(rig, body, garments=[shirt, pants], rigid={eyes: None, teeth: "DEF-teeth.T"})`: auto weights, verification, proxy fallback, cross-side strip, garment transfer (applied), rigid islands, clean and normalize, Armature after Mirror and before Subdivision. GATE: `rep["ok"]`, `unweighted == 0`, no non-deform groups; for export `max_influences=4`.
    7. **Range of motion.** `dr = R.deformation_report(rig, [body, *garments], R.RIGIFY_POSES)`, `R.flag_report(dr)`, then `R.pose_sheet(...)` and `R.joint_sheet(...)` close-ups and LOOK at them. GATE: no leaks, no unexplained collapse, renders acceptable at armpit, elbow, wrist twist, groin, knee, collar.
    8. **Fix, in this order.** Placement or roll problems: back to the metarig and regenerate. Weights: targeted data edits (`strip_opposite_side`, `assign_rigid`, `smooth_weights` with a vertex mask, `clean_weights`). Volume at deep bends: corrective shape keys (`volume_restore_targets`, `corrective_from_targets`, `corrective_driver`). Blender-only candy wrapper: `preserve_volume_mask`. Re-run stage 7 after every fix; re-check earlier poses (CGDive).
    9. **Usability and delivery.** Controls visible with In Front off, widgets, side colors, MCH/DEF hidden, at least one Rigify UI row, `rig_ui.py` needs Auto Run in a GUI, switches exactly 0 or 1, rest pose clean, test action removed. Report numbers, renders and what was not verified.
    
    ## Numbers
    
    | Value                                                                                                                      | Relative to                      | Source                |
    | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | --------------------- |
    | Knee/elbow pre-bend 1 to 3 deg (fit: 1.2% of limb length, 2.7 deg); check 1 to 15                                          | chain in the bend view           | CGDive; check [added] |
    | 4 twist bones, Copy Rotation 1.0 / 0.66 / 0.33 / 0.1 (thigh 0.05)                                                          | from the driving end             | CGDive                |
    | IK chain length 2; IK Stretch 0.1; lock IK Y/Z on shin and forearm                                                         | IK bone                          | Wayne, CGDive         |
    | Foot roll: heel X [-90, 0], toe X [0, 90], inside/outside Z one-sided                                                      | pivot local, roll 0, pointing up | CGDive (verified)     |
    | Weight brush Add, strength 0.1 to 0.2, Auto Normalize; seams Mix 0.5 at strength 1                                         | Weight Paint                     | CGDive                |
    | Clean near-zero weights before blurring (Demeter); limit 0.001 (CGDive), 0.01 in `bind` [added]; 4 influences for games    | per vertex                       | Demeter, CGDive       |
    | Eyelid follow 0.15 (bone face); Rigify eyelid follow Z 0.3 (default 0.7); jaw bottom lip 0.9; mouth corners follow jaw 0.5 | face                             | CGDive                |
    | Palm 0.5 / 0.25; elbow plate 0.2 of forearm rotation                                                                       | Copy Rotation Local, Mix Add     | CGDive                |
    | Finger curl: Transformation Scale Z 0..1 to Rot Z 90..0 deg, Extrapolate                                                   | finger control                   | CGDive                |
    | Action constraint: keys frames 1 and 10, LINEAR, max = measured control travel                                             | control local Y                  | CGDive, Demeter       |
    | Shoulder test range about 20 to 30 deg of clavicle raise                                                                   | anatomy                          | CGDive                |
    | Face: about 16 Stop Staring shapes; 52 ARKit for mocap                                                                     | shape keys                       | CGDive                |
    | Proxy voxel H/180, then H/90, H/45                                                                                         | character height                 | [added, verified]     |
    | Collapse < 0.85, bulge > 1.25 (joint band), leak > 0.001 H, strain 0.5 to 1.6                                              | rest                             | [added]               |
    
    ## Quality gates
    
    Code (all in `bx_rig`): `check_metarig`, `weight_report` (unweighted, empty_major, non_deform_groups, over4, sum_dev, mirror_err), `deformation_report` + `flag_report` (strain, joint volume bands, cross-side leak, new self-intersections via bx_audit), `static_cycles`, `cycle_check`, `root_test`, `twist_angles`, `solve_pole_angle`, `rigid_check`.
    
    Visual: placement sheet (front, right, top; head zoom) before generating; pose sheet of `RIGIFY_POSES` (front, right, three-quarter); joint close-ups (arm joints front/top/three-quarter, others front/right/three-quarter) at every flagged pose. Look for armpit collapse, pointy or pinched elbows and knees, candy-wrapper wrists, groin and butt volume, collar clipping, garments sliding or intersecting, teeth and eyes lagging, widgets hidden inside the mesh. Numbers passed while a render showed broken arms or crease spikes during testing: always open the render. Rubric: [`references/critique.md`](references/critique.md).
    
    ## Common mistakes
    
    | Mistake                                             | What it looks like                        | Fix                                     |
    | --------------------------------------------------- | ----------------------------------------- | --------------------------------------- |
    | Metarig scale or pose not applied                   | generated rig differs in size or pose     | apply scale, Apply Pose as Rest Pose    |
    | Neck moved without the spine tip                    | "bone position is disjoint"               | move joints as units (`move_points`)    |
    | Straight knee or elbow                              | IK bends sideways or backward             | pre-bend in the bend view               |
    | Parented to the metarig                             | mesh follows nothing useful               | bind to the generated rig               |
    | Heat failure unnoticed                              | mesh does not move                        | `weight_report`; proxy route            |
    | Armature below Subdivision, or before Mirror        | slow; mirrored half copies the posed half | `armature_first`                        |
    | Left thigh weights on the right leg                 | opposite side moves                       | `strip_opposite_side`                   |
    | Eyes and teeth auto-weighted                        | drift, poke through                       | `assign_rigid`                          |
    | Live Data Transfer on clothes                       | weights change while animating            | apply it                                |
    | Driver on an animatable bone                        | channel cannot be keyed                   | Transformation or Copy Rotation Mix Add |
    | Inherit Rotation off for isolation                  | root rotation breaks the limb             | follow bones (World Copy Loc/Rot/Scale) |
    | IK target parented inside its chain                 | jitter, flips                             | clear parent; `static_cycles`           |
    | Re-parenting a connected bone                       | head jumps to the new parent's tail       | `reparent` clears use_connect first     |
    | Symmetrize expected to copy drivers; `.L.001` names | right side dead                           | `mirror_drivers`; `side_of`             |
    | IK/FK left at 0.86                                  | mixed, unreadable deformation             | exactly 0 or 1                          |
    
    ## Blender 5.2 notes
    
    - Rigify ships disabled on factory startup (`enable_rigify`); the Human metarig still has the legacy face (upgrade it); Overwrite Widget Meshes still defaults off.
    - Bone collections and per-bone colors replace layers and bone groups; `edit_bones.new()` joins no collection; pose selection is `pose.bones[n].select`.
    - `parent_set` appends the Armature modifier last; weight operators (`vertex_group_smooth`, `_clean`) need Weight Paint mode headless; `bx_rig` does clean, normalize and limit on data instead.
    - `shape_key_add` leaves new keys at value 1.0 in 5.2.1: set 0 before driving [verified]. `VertexGroup.remove` with out-of-range indices crashes 5.2.1 [verified].
    - Setting a parent's `tail` moves a connected child's `head`; `EditBone.transform` on a connected group double-moves shared ends [verified]: use `transform_bones`.
    - Python drivers start with keys (0,0),(1,1); remap them (`remap_driver`). Action constraints pick the action slot automatically. Only Insert Available is on by default; `keyframe_insert` from Python is unaffected.
    - Custom shapes: Affect Gizmo (`use_transform_at_custom_shape`), Use As Pivot (`use_transform_around_custom_shape`). 5.2 adds Duplicate and Rename and Add Bone align Axes.
    
    ## References
    
    - [`references/procedures.md`](references/procedures.md): full tested code for each stage and the test script behind it; load before writing rig code.
    - [`references/expert-notes.md`](references/expert-notes.md): judgment by expert (placement, Rigify internals, weighting, mechanisms, faces) with timestamps; load when a decision is not covered above.
    - `references/critique.md`: the review rubric; load at every gate.
    - [`references/sources.md`](references/sources.md): videos, experts, best timestamps.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related