357 lines
18 KiB
Markdown
357 lines
18 KiB
Markdown
# NPC Model Sourcing
|
||
|
||
This page records the source evaluation and the active character plan. The
|
||
decision is [ADR-0017](adr/0017-quaternius-npc-base.md), which supersedes the
|
||
generator-first sourcing decision in [ADR-0015](adr/0015-npc-model-variety-sourcing.md).
|
||
|
||
## Active direction
|
||
|
||
**Quaternius Ultimate Modular Characters (CC0) is the primary NPC mesh and
|
||
animation source.** The project-owned procedural library remains the shipped
|
||
fallback until the Quaternius migration passes the silhouette and gear gates.
|
||
|
||
The migration keeps the runtime contract that Godot, gear and tests already
|
||
consume:
|
||
|
||
- one `npc_base` skinned mesh under the 13 required core bones (optional extras
|
||
allowed);
|
||
- five tint slots: `skin`, `cloth`, `pants`, `accessory`, `hair`;
|
||
- front on Blender −Y / Godot +Z, feet grounded, height about 1.81 m;
|
||
- 24,000 triangles / 24,000 glTF vertices per character;
|
||
- gear stays at 1,500 triangles and props at 4,000;
|
||
- committed GLBs and maps only; no Quaternius, Blender or network dependency in
|
||
CI, build or runtime.
|
||
|
||
### Stage 1 (default)
|
||
|
||
1. Import the researched Quaternius GLBs and choose a canonical base.
|
||
2. Map the 62 source joints to the 13-core contract and bake a proportion warp.
|
||
3. Skin the mesh, retarget Idle and Walk, and verify seated/arm-down poses.
|
||
4. Map the five slots, unwrap and bake 256² maps.
|
||
5. Fit the existing gear and render the silhouette gate.
|
||
6. Build the 12 variants, audit, and capture the 12 × 6 baseline.
|
||
|
||
The Quaternius 62-joint rig and its 24 animation clips are an **authoring
|
||
source**. They are not loaded at runtime.
|
||
|
||
### Stage 2 gate
|
||
|
||
The 62-joint T-pose source does not share the project's arm-down rest pose or
|
||
proportions. If the Stage 1 proportion warp cannot preserve the Quaternius
|
||
silhouette and deformation, stop the rollout and write a new ADR for adopting
|
||
the 62-joint rig at runtime. Do not ship a hybrid rig, a second gear offset
|
||
system or a silent `NpcAvatar` change.
|
||
|
||
## Research findings
|
||
|
||
The researched files are the three converted Quaternius outfits used by the
|
||
Ducz Character Creator: `Casual`, `Casual_Hoodie` and `Worker`. They are kept
|
||
locally in `tmp/npc-research/quaternius/` with the licence and mirror notes.
|
||
|
||
| File | SHA-256 | Structure |
|
||
| --- | --- | --- |
|
||
| `Casual.glb` | `bc2d84fba249ebaa0629cb9d133ad37234196e4b8004aa205a6397abed04e05c` | 4 meshes, 62 joints, 24 clips, 7 materials |
|
||
| `Casual_Hoodie.glb` | `f5b722199f7628b24550989af4230f65d5230727df9c326fd2b3e360effe5249` | 4 meshes, 62 joints, 24 clips, 7 materials |
|
||
| `Worker.glb` | `8d0f454336c2effc5c08f63a177af3a82d1a7af025ca9588b40e4c90c4703534` | 4 meshes, 62 joints, 24 clips, 11 materials |
|
||
|
||
Source: [Quaternius Ultimate Modular Characters](https://quaternius.com/packs/ultimatemodularcharacters.html)
|
||
and [Ultimate Modular Women](https://quaternius.com/packs/ultimatemodularwomen.html),
|
||
CC0 1.0. Download date: 2026-11-03 (via the Ducz Character Creator browser export).
|
||
The Ducz mirror is a browser builder over the same packs; it adds 21
|
||
outfits and a shared skeleton. The three full GLBs are the initial scope; the
|
||
21-part builder is a fallback for role coverage, not the default.
|
||
|
||
**Initial outfit scope (2026-11-05):** the three full GLBs (`Casual`,
|
||
`Casual_Hoodie`, `Worker`) are the canonical-base and silhouette-gate scope.
|
||
They cover only ~3 of the 12 roles (casual, hood, vest); the jacket, coat,
|
||
dress, responder (firefighter) and guard looks are not in the three GLBs, so
|
||
§4 roster coverage must pull those from the 21-part modular builder or author
|
||
new clothing. Do not start the modular builder until the canonical base passes
|
||
§3.
|
||
|
||
The first prototype measured 12,518 imported vertices, 3,199 after welding and
|
||
2,423 vertices / 4,654 triangles after a 0.75 decimate. Idle and Walk read well.
|
||
The source is a T-pose with a 62-joint realistic rig, roughly 0.96 m hip,
|
||
1.68 m arm span and 0.153 m shoulder offset, so a contract bake is required.
|
||
|
||
The 24,000 cap leaves room to stay much closer to the source LOD than the
|
||
prototype's 4,654 triangles. Do not decimate to the old 7,000-triangle target
|
||
before the silhouette gate.
|
||
|
||
## Quaternius GLB inventory (2026-11-05)
|
||
|
||
All three GLBs share one 62-joint rig and the same 24 animation clips; only the meshes and
|
||
materials differ. Each import also adds a 42-vert/80-tri `Icosphere` bone-display helper — ignore it.
|
||
|
||
### Meshes (verts / tris)
|
||
|
||
| GLB | Body | Feet | Head | Legs | Total |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| `Casual` | 5,338 / 2,654 | 1,082 / 538 | 5,122 / 2,526 | 1,426 / 706 | 12,968 / 6,424 |
|
||
| `Casual_Hoodie` | 6,132 / 3,048 | 1,024 / 512 | 3,836 / 1,888 | 1,526 / 758 | 12,518 / 6,206 |
|
||
| `Worker` | 6,074 / 3,018 | 928 / 464 | 1,862 / 936 | 1,670 / 822 | 10,534 / 5,240 |
|
||
|
||
### Joints (62)
|
||
|
||
- Spine: `Root, Hips, Torso, Body, Abdomen, Chest, Neck, Head` (8).
|
||
- Arms: `Shoulder.L/R, UpperArm.L/R, LowerArm.L/R, Wrist.L/R` (8) plus `PT.L/R` (2 pole targets).
|
||
- Fingers: `Thumb×3, Index×4, Middle×4, Ring×4, Pinky×4` × L/R (30).
|
||
- Legs: `UpperLeg.L/R, LowerLeg.L/R, Foot.L/R` (6).
|
||
|
||
### Animation clips (24)
|
||
|
||
`Idle, Idle_Neutral, Walk, Run, Run_Back, Run_Left, Run_Right, Wave, Interact, Death,
|
||
Gun_Shoot, Idle_Gun, Idle_Gun_Pointing, Idle_Gun_Shoot, Idle_Sword, Run_Shoot, Sword_Slash,
|
||
HitRecieve, HitRecieve_2, Kick_Left, Kick_Right, Punch_Left, Punch_Right, Roll`.
|
||
|
||
The retarget scope is Idle and Walk; combat/gun/sword clips are discarded (see §2).
|
||
|
||
### Material → slot map
|
||
|
||
The five project slots (`skin`, `cloth`, `pants`, `accessory`, `hair`) are assigned by mesh
|
||
region first, then material — the same material name can mean `cloth` on the body and
|
||
`accessory` on the feet.
|
||
|
||
| Material | Where | Project slot |
|
||
| --- | --- | --- |
|
||
| `Skin` | all meshes | `skin` |
|
||
| `Hair_Blond`, `Hair_Brown`, `Hair`, `Brown` (head), `Eyebrows`, `Moustache` | Head | `hair` |
|
||
| `Eye` | Head | `accessory` |
|
||
| `Worker_Yellow` | Worker body (vest stripe) + head (helmet) | `cloth` / `accessory` |
|
||
| `White`, `Grey`, `Purple`, `Black` | Body / Feet | `cloth` (body) / `accessory` (feet) |
|
||
| `Orange`, `LightBlue`, `LightBrown`, `Brown`, `Brown2` | Legs | `pants` |
|
||
| `Worker_Vest` | Worker body | `cloth` |
|
||
|
||
### Canonical base (built 2026-11-05)
|
||
|
||
`tmp/npc-research/quaternius_base.glb` is the welded, triangulated, grounded, animation-free
|
||
canonical base built from `Casual_Hoodie.glb` (the most detailed body) by
|
||
`tmp/npc-research/build_quaternius_base.py`:
|
||
|
||
- one `npc_base` mesh — 3,199 welded verts / 6,206 tris (Body + Feet + Head + Legs joined);
|
||
- 62 joints, skinned (no unskinned faces);
|
||
- height 1.81 m, feet at y 0, front on Godot +Z / Blender −Y (T-pose rest);
|
||
- no animation clips; `inspect` clean (no issues).
|
||
|
||
This is the §2 contract-bake input: the 62-joint rig is authoring-only and is mapped to the
|
||
13-core contract next, not shipped as-is.
|
||
|
||
## 62-joint → 13-core joint map (2026-11-05)
|
||
|
||
The Quaternius hierarchy (measured from `quaternius_base.glb`):
|
||
|
||
- `Root → Body → Hips → Abdomen → Torso → Chest → Neck → Head` (spine);
|
||
- `Chest → Shoulder.L/R → UpperArm.L/R → LowerArm.L/R → Wrist.L/R → fingers` (arms);
|
||
- `Body → UpperLeg.L/R → LowerLeg.L/R` and `Root → Foot.L/R` (legs — the foot is a separate
|
||
child of Root, not of LowerLeg);
|
||
- `Root → PT.L/R` (IK pole targets, no deforming weights).
|
||
|
||
The contract is 14 bones (13 core + `Hips` root): `Hips, Spine, Neck, Head, Shoulder.L/R,
|
||
Elbow.L/R, Hand.L/R, Hip.L/R, Knee.L/R`. The map:
|
||
|
||
| Contract bone | Quaternius source | Notes |
|
||
| --- | --- | --- |
|
||
| `Hips` | `Hips` | `Root` + `Body` collapse into `Hips` |
|
||
| `Spine` | `Abdomen`, `Torso`, `Chest` | three collapse into one |
|
||
| `Neck` | `Neck` | |
|
||
| `Head` | `Head` | |
|
||
| `Shoulder.L/R` | `UpperArm.L/R` | the collar `Shoulder.L/R` collapses into `Spine` |
|
||
| `Elbow.L/R` | `LowerArm.L/R` | |
|
||
| `Hand.L/R` | `Wrist.L/R` | |
|
||
| `Hip.L/R` | `UpperLeg.L/R` | |
|
||
| `Knee.L/R` | `LowerLeg.L/R` | |
|
||
|
||
Optional extras (allowed by the contract; kept if they survive within 24,000 vertices):
|
||
|
||
- the 30 finger bones (`Thumb×3, Index×4, Middle×4, Ring×4, Pinky×4` × L/R) → optional finger
|
||
bones;
|
||
- `Foot.L/R` → optional toe bones.
|
||
|
||
Dropped (never shipped): `PT.L/R` (pole targets carry no deforming weights), and the
|
||
`Root`/`Body`/collar-`Shoulder` collapse bones above.
|
||
|
||
## Rest-pose and proportion-warp measurement (2026-11-05)
|
||
|
||
The rest-pose bake (T-pose → arm-down) works: rotate the `UpperArm.L/R` pose bones −90° around
|
||
their local X, apply the Armature modifier to bake the deformation, then `armature_apply` to bake
|
||
it as the new rest pose (see `tmp/npc-research/warp_full.py`). The mesh deforms cleanly to the
|
||
arm-down pose (deformed bounds x ±0.221 vs the T-pose ±0.811).
|
||
|
||
The proportion warp to the contract positions is measured and it **distorts the silhouette**:
|
||
|
||
| Joint | Quaternius | Contract | Delta |
|
||
| --- | --- | --- | --- |
|
||
| Hips height | 0.869 | 0.62 | −0.249 (lower hip) |
|
||
| Head top | 1.614 | 1.82 | +0.206 (raise head) |
|
||
| Shoulder x | 0.148 | 0.27 | +0.122 (widen) |
|
||
| Arm (shoulder→wrist) | 0.402 | 0.60 | ×1.49 |
|
||
| Arm (shoulder→hand tip) | 0.429 | 0.70 | ×1.63 |
|
||
| Leg (hip→ankle) | 0.835 | 0.62 | ×0.74 |
|
||
| Torso (hip→head) | 0.745 | 1.20 | ×1.61 |
|
||
|
||
The vision judge reads the warped mesh as: paddle/claw hands (the 1.63× arm stretch blows the
|
||
hand out), forearms lost into the torso, stretched shins, and "long legs + short torso". The arm
|
||
must either stay short (a "shortened arm" vs the contract hand) or stretch 1.63× (a distorted
|
||
deltoid + hand) — both are the gate's rejection conditions.
|
||
|
||
**Finding:** the Stage 1 proportion warp cannot preserve the Quaternius silhouette at the
|
||
contract proportions; the gate fails and Stage 2 (a 62-joint runtime ADR) is indicated. Do not
|
||
start the 12-variant rebuild on the warped base.
|
||
|
||
[ADR-0018](adr/0018-quaternius-stage2-runtime-rig.md) records the Stage 2 decision (accepted
|
||
2026-11-05, owner chose option 2): adopt the **Quaternius 62-joint rig at runtime**, keep the
|
||
mesh at source LOD, re-derive gear attach points from the Quaternius skeleton, and use the
|
||
Quaternius Idle/Walk clips directly (no retarget).
|
||
|
||
## Pilot bake and shipped state (2026-11-05)
|
||
|
||
`npc_casual` is the first Quaternius-derived character. The reproducible pipeline lives in
|
||
`tmp/npc-research/` (local tooling, not shipped) and has two steps:
|
||
|
||
1. `build_base_minimal.py` — import `quaternius/Casual_Hoodie.glb`, join the four skinned meshes
|
||
**without welding**, rename the upstream materials to the project tint slots, Smart-UV-unwrap
|
||
and repack, export `quaternius_tpose.glb`.
|
||
2. `bake_armdown_v5.py` — pose `UpperArm.L/R` −90° local X, read the posed matrices, apply the
|
||
Armature modifier, **reset the pose to identity**, set the rest matrices, re-add the modifier,
|
||
export `quaternius_base.glb` and save `assets/models/blender_src/npc_casual.blend` for the AO
|
||
bake.
|
||
|
||
Then `tools/bake_ao.py --only npc_casual` and a copy of the GLB to
|
||
`assets/models/blender/npc/npc_casual.glb`.
|
||
|
||
| Measurement | npc_casual (Quaternius) | procedural library |
|
||
| --- | --- | --- |
|
||
| Triangles | 6,206 | 4,812–6,046 |
|
||
| glTF vertices | 12,988 | 4,908–5,785 |
|
||
| Bones | 62 | 13 |
|
||
| Head height / body height | 0.134 | 0.16–0.22 (stylized band) |
|
||
| Shoulder width | 0.201 m head width, `fit` OK | `fit` OK |
|
||
| Texel density (256² maps) | 73–74 px/m | 75–96 px/m |
|
||
| Materials | `skin`, `cloth`, `pants`, `hair`, `accessory` + fixed `Eye`, `Eyebrows` | five slots + fixed detail |
|
||
|
||
Two source quirks shaped the bake:
|
||
|
||
- the upstream UV map is degenerate (every loop at 0,0) and its materials are flat colours, so the
|
||
AO/texture bake must follow a real unwrap or it silently writes a black map;
|
||
- the upstream materials are named `Skin`/`Purple`/`LightBlue`/`White`/`Hair`, so the five-slot
|
||
contract is applied as a rename, not a re-author.
|
||
|
||
The face/silhouette gate that chose the full swap is recorded in the follow-up decision of
|
||
[ADR-0018](adr/0018-quaternius-stage2-runtime-rig.md); its comparison sheets were retired with the
|
||
review pages.
|
||
|
||
## Shipped roster (0.9.9, owner chose the full swap)
|
||
|
||
Every public character name is baked from one of the three CC0 sources. The build is
|
||
`tools/blender_assets.py build` (default `--source quaternius`); the sources live in
|
||
`assets/models/blender_src/quaternius/` and the geometry pipeline is two passes:
|
||
|
||
1. `_quaternius_tpose_glb()` — import, drop the Icosphere helper, join **without welding**,
|
||
repoint the upstream materials onto the five tint slots, Smart-UV-unwrap + repack, export an
|
||
intermediate T-pose GLB. The round trip also normalises the mirrored upstream bone rolls,
|
||
which is what makes the next pass symmetric.
|
||
2. `_bake_quaternius_rest()` — pose `UpperArm.L/R` -90° local X, read the posed matrices, apply
|
||
the Armature modifier, reset the pose, write the rest matrices, re-add the modifier, export
|
||
with `export_apply=False`.
|
||
|
||
Then `tools/bake_ao.py --only <name>`.
|
||
|
||
| Character | Source | Tris | Material slots |
|
||
| --- | --- | --- | --- |
|
||
| `npc_casual` | hoodie | 6,206 | skin, cloth, pants, hair, accessory + Eye, Eyebrows |
|
||
| `npc_jacket` | casual | 6,424 | five slots + eyes/brows |
|
||
| `npc_coat` | hoodie | 6,206 | five slots + eyes/brows |
|
||
| `npc_vest` | worker | 5,240 | five slots + hivis vest/hat, eyes, moustache |
|
||
| `npc_dress` | casual | 6,424 | five slots + eyes/brows |
|
||
| `npc_hoodie` | hoodie | 6,206 | five slots + eyes/brows |
|
||
| `npc_firefighter` | worker (vest + hat dropped) | 4,458 | five slots + eyes, moustache |
|
||
| `npc_guard` | worker (vest + hat dropped) | 4,458 | five slots + eyes, moustache |
|
||
| `npc_crowd_a` | casual | 6,424 | five slots + eyes/brows |
|
||
| `npc_crowd_b` | casual | 6,424 | five slots + eyes/brows |
|
||
| `npc_crowd_c` | hoodie | 6,206 | five slots + eyes/brows |
|
||
| `npc_crowd_d` | worker (vest + hat dropped) | 4,458 | five slots + eyes, moustache |
|
||
|
||
Source slot maps (`QUATERNIUS_SOURCES`): the `casual` tee is `Skin`/`White`/`Orange`/`Grey`/
|
||
`Hair_Blond`; the hoodie is `Skin`/`Purple`/`LightBlue`/`Hair`/`White`; the worker is `Skin`/
|
||
`LightBrown`/`Brown`+`Brown2`/`Grey`+`Black`/`Eyebrows`. Everything not in the map keeps its
|
||
authored colour (eyes, brows, moustache, hivis vest and hard hat), so the contract still holds if
|
||
upstream adds a material — it shows up as an extra fixed-colour slot instead of vanishing.
|
||
|
||
Head gear is authored for this head: `HEAD_GEAR_SCALE = 0.64` and `HEAD_GEAR_LIFT = 0.07` map the
|
||
0.315 m procedural head onto the 0.201–0.214 m Quaternius head. The procedural fallback therefore
|
||
wears slightly small hats.
|
||
|
||
## Modular outfit intake (2026-11-06)
|
||
|
||
The owner downloaded the *Ultimate Modular Men* pack once (CC0 1.0, 54.5 MB zip, 15 outfit families
|
||
plus weapons and a horse) into the gitignored `tmp/quaternius-intake/`. The pack ships FBX, not GLB,
|
||
so the intake composes each family into a single skinned base:
|
||
|
||
python3 tools/quaternius_parts.py make-base \
|
||
--family "tmp/quaternius-intake/Ultimate Modular Men- Feb 2022/Separate Skeletal Meshes and Animations/Suit" \
|
||
--out assets/models/blender_src/quaternius/Suit.glb
|
||
|
||
What `make-base` has to undo, and why it is not a straight import:
|
||
|
||
1. each of the four FBX files carries its own copy of the armature — one is kept and the meshes are
|
||
repointed at it, otherwise `join` leaves three meshes bound to deleted armatures;
|
||
2. the FBX rig adds zero-length `_end` leaf bones and a `neutral_bone` root; Godot's importer fails
|
||
the exported GLB with them (`resource_format_binary` error), so they are stripped back to the
|
||
accepted 62 joints;
|
||
3. the object transforms are baked before export, or the glTF node transforms change the rest pose
|
||
on import and the character renders with a giant head;
|
||
4. the pack reuses one fabric material for jacket and trousers, so the leg meshes get their own
|
||
`*_legs` copies that the slot map sends to `pants`;
|
||
5. imported parts share seams, which leaves zero-area triangles the audit rejects.
|
||
|
||
The pack itself is never committed. Only the composed bases are, with their SHA-256 here and in
|
||
`CREDITS.md`.
|
||
|
||
| Base | Source family | SHA-256 | Used by |
|
||
| --- | --- | --- | --- |
|
||
| `Suit.glb` | Suit | `674f2a63…` | `npc_coat` |
|
||
|
||
## Contract mismatch
|
||
|
||
The expensive part is the rig and proportions, not the mesh. The source has a
|
||
T-pose and a realistic skeleton; the runtime expects an arm-down 13-core rig,
|
||
fixed gear attach points and the existing pose composition. The Stage 1 bake
|
||
must therefore:
|
||
|
||
- rotate the arms down and match the contract wrist positions;
|
||
- warp the hips, knees, shoulders and head without collapsing the deltoid,
|
||
shortening the arms or changing the silhouette;
|
||
- keep one skinned `npc_base` mesh and the five slots;
|
||
- verify gear against the new shoulder, hip and head positions.
|
||
|
||
If those constraints distort the model, Stage 2 is the correct next decision,
|
||
not a local workaround.
|
||
|
||
## Licence and provenance
|
||
|
||
- CC0 preferred. CC-BY is allowed only with attribution in
|
||
[`CREDITS.md`](../CREDITS.md).
|
||
- No service asset whose terms restrict redistribution may be shipped.
|
||
- Keep the source URL, author, licence, download date and SHA-256 in this page
|
||
and in `CREDITS.md`.
|
||
- Research files and generated previews stay in `tmp/` (gitignored). Only final
|
||
audited assets and committed review sheets enter the repository.
|
||
- Quaternius models and animations are CC0; the existing credits entry already
|
||
names Quaternius. Keep it accurate if the shipped source set changes.
|
||
|
||
## Closed tracks
|
||
|
||
The MakeHuman/MPFB body pipeline is closed for the NPC base. The accepted
|
||
MakeHuman head and the procedural `npc_base` remain tested fallbacks until the
|
||
Quaternius rollout is complete. Their research scripts and renders were removed
|
||
from `tmp/` after the pivot; the measurements and decisions remain in
|
||
[`DONE.md`](../DONE.md), [`MEMORY.md`](../MEMORY.md) and the review docs.
|
||
|
||
## Related documents
|
||
|
||
- [ADR-0017: Quaternius NPC base](adr/0017-quaternius-npc-base.md)
|
||
- [ADR-0016: 24,000-triangle character budget](adr/0016-npc-character-budget-24k.md)
|
||
- [Blender asset contract](architecture.md#character-and-gear-asset-contract)
|
||
- [`3d-model-review.md`](3d-model-review.md)
|
||
- [`asset-tooling.md`](asset-tooling.md)
|