# Part D Report: Fix Rigs in Blender

Completed at: 2026-10-18 17:32 CET (Europe/Paris)
Worktree: `/home/claude-agent/forge-build/forge-assets/.forge-worktrees/1792334889-3ce117`
Branch: `task/1792334889-3ce117`

## 1. What Changed

We rigged 25 active 3D assets (15 vehicles and 10 openable furniture pieces) in Blender 5.2, exported 25 new content-addressed GLBs to `store/blobs/<sha256>.glb`, wrote 25 immutable `v2.json` cards via `lib/card.py` with `provenance.parent` and `provenance.variant = {"kind": "rig", ...}`, rendered before/after evidence PNGs (`<300 KB` each) into `rigs-evidence/`, and verified all 38 `character.*` assets against `motion.clips.core/cast-clips-ual-core` (recorded in `rigs-evidence/summary.json`).

### Part 1: 15 Vehicles Fixed (`v1` -> `v2`)

All 15 vehicles now have separated/standardized `Body`, `Wheel_FL`, `Wheel_FR`, `Wheel_RL`, `Wheel_RR` nodes with each wheel's origin centered at its geometric hub center, measured `gameplay.wheelbase` and `gameplay.track`, and hinge axes in Three.js (`+Y` up, `+Z` forward) space where doors or articulated arms are separable:

| Asset ID | Version | New GLB SHA256 (prefix) | Wheelbase (m) | Track (m) | Extra Moving Nodes & Hinge Axes (Three.js) |
| --- | --- | --- | --- | --- | --- |
| `vehicle.kart.kart/forge-kart-standard` | v2 | `04d98185a3be` | 1.384 | 1.157 | `SteeringWheel` (separated from fused mesh; Draco preserved) |
| `vehicle.service.ambulance/kenney-ambulance` | v2 | `4aaafca9caac` | 1.950 | 1.300 | `Door_L` (`[0,-1,0]`, 75 deg), `Door_R` (`[0,1,0]`, 75 deg) |
| `vehicle.utility.delivery-truck/kenney-delivery` | v2 | `dd71c9221d1a` | 1.950 | 1.300 | `Door_Rear` (`[1,0,0]` top hinge, 85 deg) |
| `vehicle.service.garbage-truck/kenney-garbage-truck` | v2 | `02d2ce4aa1e0` | 2.094 | 1.351 | `Arm` (`[1,0,0]`, 45 deg) |
| `vehicle.utility.loader/kenney-tractor-shovel` | v2 | `358aa949dc8a` | 1.300 | 1.250 | `Shovel` (`[-1,0,0]`, 35 deg) |
| `vehicle.car.hatchback/kenney-hatchback-sports` | v2 | `6da2a9d2218c` | 1.500 | 1.200 | None (fused doors) |
| `vehicle.car.race/kenney-race` | v2 | `6bb03007cc09` | 1.490 | 1.300 | None (open cockpit) |
| `vehicle.car.race/kenney-race-future` | v2 | `02c0314b7667` | 1.600 | 1.300 | None (closed canopy) |
| `vehicle.car.sedan/kenney-sedan` | v2 | `de5367eb4164` | 1.400 | 1.200 | None (fused doors) |
| `vehicle.car.sports/kenney-sedan-sports` | v2 | `35ca7c053d79` | 1.440 | 1.200 | None (fused doors) |
| `vehicle.car.suv/kenney-suv` | v2 | `8569771d8a3f` | 1.410 | 1.200 | None (fused doors) |
| `vehicle.service.fire-truck/kenney-firetruck` | v2 | `4bf8b7561717` | 2.150 | 1.300 | None (fused body) |
| `vehicle.service.police/kenney-police` | v2 | `c054341d80cf` | 1.450 | 1.200 | None (fused doors) |
| `vehicle.service.taxi/kenney-taxi` | v2 | `be82b2ceca2c` | 1.450 | 1.200 | None (fused doors) |
| `vehicle.utility.pickup/kenney-truck` | v2 | `a0e0e08dd2be` | 1.550 | 1.200 | None (fused doors) |

### Part 2: 10 Openable Furniture Pieces Fixed (`v1` -> `v2`)

All 10 furniture pieces have separated `Body` plus named door/drawer nodes (`Door_L`, `Door_R`, `Door_2`, `Door_3`, `Drawer_1..4`) with origins placed on the hinge edge or drawer slide start, signed axes and limits in `nodes.axes` and `nodes.limits` (verified in Three.js space via `tools/space/axes_space.py`), and exported glTF animation clips `open` and `close` (authored on NLA tracks with rest pose closed at frame 1):

| Asset ID | Version | New GLB SHA256 (prefix) | Moving Nodes | Axes (Three.js) | Limits | Clips |
| --- | --- | --- | --- | --- | --- | --- |
| `prop.furniture.wardrobe/store-wardrobe-paxa` | v2 | `991cf04f96a6` | `Door_L`, `Door_R` | `[0,-1,0]`, `[0,1,0]` | 85 deg | `close`, `open` |
| `prop.furniture.wardrobe/store-wardrobe-paxa-b` | v2 | `f6110e58165e` | `Door_L`, `Door_R` | `[0,-1,0]`, `[0,1,0]` | 85 deg | `close`, `open` |
| `prop.furniture.drawers/store-chest-drawers` | v2 | `3da7df606e90` | `Drawer_1..4` | `[0,0,1]` (slide) | 0.28 m | `close`, `open` |
| `prop.furniture.kitchen/ikea-rakall` | v2 | `4be62da003c7` | `Door_L`, `Door_R` | `[0,1,0]`, `[0,1,0]` | 80 deg | `close`, `open` |
| `prop.furniture.kitchen/ikea-mattradition` | v2 | `4ca9a1c3f1fc` | `Door_L` (oven door) | `[1,0,0]` (bottom hinge) | 75 deg | `close`, `open` |
| `prop.furniture.bath/store-bathroom-vanity` | v2 | `aa6fa9a7fa97` | `Door_L`, `Door_R` | `[0,-1,0]`, `[0,1,0]` | 80 deg | `close`, `open` |
| `prop.furniture.bath/store-toilet-vanity-set` | v2 | `134cd139b06d` | `Door_L`, `Door_R` | `[0,-1,0]`, `[0,1,0]` | 80 deg | `close`, `open` |
| `prop.furniture.table/store-tv-bench` | v2 | `70454341e353` | `Door_L`, `Door_R` | `[0,-1,0]`, `[0,1,0]` | 80 deg | `close`, `open` |
| `prop.furniture.table/store-desk` | v2 | `f7b0f3e6087d` | `Drawer_1..3` | `[0,0,1]` (slide) | 0.25 m | `close`, `open` |
| `prop.furniture.kitchen/store-kitchen-run-2-4` | v2 | `6b942b87836e` | `Door_L`, `Door_R`, `Door_2`, `Door_3` | `[0,-1,0]`, `[0,1,0]`, `[0,-1,0]`, `[0,1,0]` | 80 deg | `close`, `open` |

### Part 3: Character Verification (`character.*`, 38 Active Assets, Read-Only)

- **Skinning & vertex weights**: All 38 `character.*` assets have a valid Armature, skinned mesh(es), and **0 unweighted vertices**.
- **4 PASS (`cast-ual-ubc-female`, `cast-ual-ubc-male`, `cast-outfit-ubc-female-snow`, `cast-outfit-ubc-male-snow`)**: Use the 65-bone Quaternius UAL `Armature` skeleton and match `motion.clips.core/cast-clips-ual-core` bone names and rest rotations (`max_rest_angle_diff_deg` <= 5.1 deg).
- **34 FAIL (all `cast-crowd-*`, `cast-football-*`, `cast-real-*`, `cast-tennis-*`, `cast-paddle-*`)**: Use the 53-bone MPFB/MakeHuman `Human.rig` skeleton and fail direct playback of `motion.clips.core/cast-clips-ual-core` for two reasons:
  1. **Bone name casing mismatch**: Root bone is `Root` (vs `root` in clips) and head bone is `head` (vs `Head` in clips), plus 12 `*_04_leaf_*` finger leaf bones are absent.
  2. **Rest-pose bone roll/orientation mismatch**: Local bone rest rotations differ from `cast-clips-ual-core` by 85.8 to 91.2 deg on `upperarm_l/r`, 32.7 to 61.6 deg on `spine_01`, 34.3 to 51.6 deg on `pelvis`, and 38.4 to 52.2 deg on `thigh_l/r`. Playing `cast-clips-ual-core` directly without rest-pose retargeting twists the spine, pelvis, thighs, and arms.
- **Blender glTF importer bug on `cast-clips-ual-core`**: `cast-clips-ual-core` (`a63ecae6...glb`) triggers `ValueError: attempt to assign sequence of size 2 to extended slice of size 1` in Blender 5.2's `io_scene_gltf2/blender/imp/animation_node.py:158` due to a 1-frame step channel in the meshopt clip file.

## 2. Evidence

- `python3 tools/build_index.py` -> `index.json: 454 assets, 967 KB` (all 454 active cards pass `lib/card.py:validate`).
- `tools/space/axes_space.py` verified on exported GLBs (`kenney-ambulance`, `kenney-delivery`, `store-wardrobe-paxa`, `store-chest-drawers`, `ikea-mattradition`): all exported nodes have `rest_rot: None` at rest and hinge/slide axes in Three.js `+Y` up, `+Z` forward space.
- Before/after renders (512x256 PNG, 105 KB to 166 KB each, all under the 300 KB cap) saved in `rigs-evidence/<id>.png` and `rigs-evidence/<slug>.png`, plus full machine-readable report in `rigs-evidence/summary.json`.

## 3. What Would Break

- Any consumer reading `v1` of the 25 updated assets continues to work because `v1.json` and its blobs are untouched; `index.json` now points to `v2.json` and the new blobs in `store/blobs/<sha256>.glb`.
- When merging this worktree into the main/integrate checkout, the 25 new `.glb` files in `store/blobs/` must be copied into the shared `store/blobs/` directory alongside the git commits.
- To roll back any asset, delete its `v2.json` card and re-run `python3 tools/build_index.py`.

## 4. What Was Skipped (and Why)

- **Skipped Vehicles (15)**:
  - Fused single-primitive meshes where wheels share continuous topology with fenders/body: `vehicle.car.sedan/store-sedan-car`, `vehicle.car.sports/store-sports-car-b`, `vehicle.car.suv/store-suv-car`, `vehicle.utility.van/store-van`, `vehicle.two-wheel.bicycle/store-bicycle`, `vehicle.two-wheel.scooter/store-scooter-vespa`, `vehicle.two-wheel.motorbike/cc0-motorbike-enduro`.
  - Watercraft (`vehicle.sea.*`, 7 assets: `kenney-boat-fishing`, `kenney-boat-row-small`, `kenney-boat-sail-a`, `kenney-boat-tow-a`, `forge-kayak-slalom`, `store-speedboat`, `cc0-submarine-modular`): no wheels/steering wheel applicable.
  - `vehicle.two-wheel.bicycle/psx-bicycle`: already has separated `rear_wheel`, `front_fork`, `handlebars`, `pedals` nodes, and the 15-vehicle cap was reached.
- **Skipped Furniture (4)**:
  - Fused single-primitive meshes with no interior cabinet walls or drawer boxes behind the front face (cutting seams exposes hollow backfaces/void): `prop.furniture.wardrobe/store-wardrobe-sliding`, `prop.furniture.sideboard/store-sideboard`, `prop.furniture.drawers/store-dresser`, `prop.furniture.storage/psx-cardboard-box`.
- **Characters**: Verified only (no edits made, per instructions).
