19 KiB
NPC Model Sourcing
This page records the source evaluation and the active character plan. The decision is ADR-0017, which supersedes the generator-first sourcing decision in ADR-0015.
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_baseskinned 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)
- Import the researched Quaternius GLBs and choose a canonical base.
- Map the 62 source joints to the 13-core contract and bake a proportion warp.
- Skin the mesh, retarget Idle and Walk, and verify seated/arm-down poses.
- Map the five slots, unwrap and bake 256² maps.
- Fit the existing gear and render the silhouette gate.
- 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 and Ultimate Modular Women, 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) plusPT.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_basemesh — 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;
inspectclean (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/RandRoot → 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 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:
build_base_minimal.py— importquaternius/Casual_Hoodie.glb, join the four skinned meshes without welding, rename the upstream materials to the project tint slots, Smart-UV-unwrap and repack, exportquaternius_tpose.glb.bake_armdown_v5.py— poseUpperArm.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, exportquaternius_base.glband saveassets/models/blender_src/npc_casual.blendfor 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; 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:
_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._bake_quaternius_rest()— poseUpperArm.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 withexport_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:
- each of the four FBX files carries its own copy of the armature — one is kept and the meshes are
repointed at it, otherwise
joinleaves three meshes bound to deleted armatures; - the FBX rig adds zero-length
_endleaf bones and aneutral_boneroot; Godot's importer fails the exported GLB with them (resource_format_binaryerror), so they are stripped back to the accepted 62 joints; - 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;
- the pack reuses one fabric material for jacket and trousers, so the leg meshes get their own
*_legscopies that the slot map sends topants; - 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 |
Adventurer.glb |
Adventurer | 062caf69… |
npc_crowd_c |
Beach.glb |
Beach | 81c69837… |
npc_crowd_a |
Casual_2.glb |
Casual_2 | fe4edb73… |
npc_jacket |
Farmer.glb |
Farmer | b9b67058… |
npc_crowd_b (hair slot patched) |
King.glb |
King | 85aae2a0… |
npc_dress |
Punk.glb |
Punk | 56993274… |
npc_hoodie (the mohawk is the Red material) |
Swat.glb |
Swat | 43659684… |
not used — authored facing the other way; see below |
The roster therefore runs eleven distinct outfits across twelve roles (the worker base serves
npc_vest, npc_firefighter, npc_guard and npc_crowd_d).
Open: the Swat base faces backwards
Swat.glb is authored facing Godot −Z. Rotating the exported base about world Z rotates the skin
with the armature node and the character renders headless, and rotating the mesh as well collapses
it into a spike; the reliable fix is to yaw the four FBX parts before the join and re-bake. Until
then npc_guard stays on the worker base.
Slots a family cannot paint
Farmer has no hair shell and Swat/SpaceSuit have neither hair nor skin. The audit and the
runtime both need the five slots as used materials, so patch_slots buries a 4 cm triangle
inside the skull or the pelvis for each missing slot (_patch_missing_slots). It is invisible, it
keeps the material in the export, and it means a helmeted head still satisfies the contract.
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_basemesh 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. - 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, MEMORY.md and the review docs.