Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3acf370fc | ||
|
|
04d211b90b | ||
|
|
a6ccd86f85 | ||
|
|
1ce173e9d1 | ||
|
|
90f189a269 | ||
|
|
fb3e75aba3 | ||
|
|
cd285b3a39 | ||
|
|
68f49253fe | ||
|
|
b012c6a675 | ||
|
|
2a6f29f5d0 | ||
|
|
bb54ef3eec | ||
|
|
11f53f66a3 | ||
|
|
9b65a93587 | ||
|
|
56a14747d4 | ||
|
|
8b1bf736af | ||
|
|
f121167e52 | ||
|
|
140a7ada0f | ||
|
|
5508f5510f | ||
|
|
7baa2c4f54 | ||
|
|
0cd12af994 | ||
|
|
271f10ac4d | ||
|
|
e2f52fbfd1 | ||
|
|
f5fa321a8f | ||
|
|
a2ae9e03c3 | ||
|
|
cff3937c16 | ||
|
|
1ed9e05d9b | ||
|
|
8c69f367b0 | ||
|
|
2a7921bae6 | ||
|
|
b419923637 | ||
|
|
c7fd4b59cc | ||
|
|
3fa3482608 | ||
|
|
1049d8631e | ||
|
|
deb74c53af | ||
|
|
9968bc0a4c | ||
|
|
2e1bb19e2c | ||
|
|
5450dbf88a | ||
|
|
79224c5acf | ||
|
|
f0d4b81239 | ||
|
|
48a9795248 | ||
|
|
a5e62534ce | ||
|
|
cd7697cba4 | ||
|
|
d2f773c7ad | ||
|
|
a3c3a37270 | ||
|
|
8dd4f77901 | ||
|
|
ba444152c9 | ||
|
|
d56f14c6dd | ||
|
|
b297c4543f | ||
|
|
958d3ae0ff |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: gig-line-blender-asset-contract
|
||||
description: Use when adding, rebuilding, auditing or fixing any Gig Line Blender-authored character, NPC base, gear, prop or venue geometry. Covers the required 13 core bones (optional extra bones allowed), five tint slots, 16,000/16,000 character budgets, gear attach space, 256² texture/AO rules, headless flatpak Blender commands, optional MCP guidance, and the inspect/audit/fit/proportion/capture gates. Do not use for pure GDScript/UI work or release publishing.
|
||||
description: Use when adding, rebuilding, auditing or fixing any Gig Line Blender-authored character, NPC base, gear, prop or venue geometry. Covers the required 13 core bones (optional extra bones allowed), five tint slots, 24,000/24,000 character budgets, gear attach space, 256² texture/AO rules, headless flatpak Blender commands, optional MCP guidance, and the inspect/audit/fit/proportion/capture gates. Do not use for pure GDScript/UI work or release publishing.
|
||||
compatibility: Gig Line repository checkout. Godot 4.7.2 on PATH; Blender 5.2 LTS via the org.blender.Blender flatpak for build/bake/render. Audit, fit, inspect and proportion checks are stdlib-only.
|
||||
metadata:
|
||||
project: Gig Line
|
||||
@@ -51,8 +51,8 @@ and resolve the source of truth before editing assets.
|
||||
| Bones | The 13 required core bones: `Head`, `HipL/R`, `KneeL/R`, `ShoulderL/R`, `ElbowL/R`, `HandL/R`, `Spine`, `Neck`. Optional extra bones (fingers, face, toes) are allowed; they must not move the core rest positions or break animation proxies. |
|
||||
| Tint slots | `skin`, `cloth`, `pants`, `accessory`, `hair`. |
|
||||
| Orientation | Front is Blender −Y / Godot +Z; feet grounded near y = 0; metres. |
|
||||
| Character budget | **16,000 triangles / 16,000 glTF vertices** per character (`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS`). |
|
||||
| Frozen fallback | `npc_base` remains the low-poly fallback; it shares the 16,000/16,000 cap but is not required to use it. Do not silently lower the cap. |
|
||||
| Character budget | **24,000 triangles / 24,000 glTF vertices** per character (`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS`). |
|
||||
| Frozen fallback | `npc_base` remains the low-poly fallback; it shares the 24,000/24,000 cap but is not required to use it. Do not silently lower the cap. |
|
||||
| Gear budget | **1,500 triangles** per gear piece; authored in attach-point space. |
|
||||
| Prop budget | **4,000 triangles** per prop. |
|
||||
| Gear attach | Use the catalogued attach points; verify with `fit`. Runtime `GEAR_OFFSETS` (lanyard forward, firefighter helmet up) and `HAND_ITEM_FORWARD` are intentional, not asset-contract changes. |
|
||||
@@ -167,7 +167,7 @@ At minimum:
|
||||
- `npc_proportions.py` passes the stylized guide.
|
||||
- `npc_pipeline.py audit` passes if the base/textures changed.
|
||||
- texel density is inside the authored/fabric bands.
|
||||
- No character exceeds 16,000/16,000; gear/prop caps hold.
|
||||
- No character exceeds 24,000/24,000; gear/prop caps hold.
|
||||
- 24-NPC budget and Web/Compatibility path remain green.
|
||||
- Contract tests, headless suite, capture gates and docs check pass.
|
||||
- The diff contains only the intended assets, tools, tests and docs.
|
||||
|
||||
@@ -59,7 +59,7 @@ skin, cloth, pants, accessory, hair
|
||||
|
||||
| Asset | Limit |
|
||||
| --- | --- |
|
||||
| Character (library and base fallback) | 16,000 triangles / 16,000 glTF vertices |
|
||||
| Character (library and base fallback) | 24,000 triangles / 24,000 glTF vertices |
|
||||
| Gear piece | 1,500 triangles |
|
||||
| Prop | 4,000 triangles |
|
||||
| Library prop/venue total | must stay inside the venue tests and library budget |
|
||||
|
||||
@@ -90,12 +90,16 @@ specialists the task needs:
|
||||
|
||||
### Project rules that override a skill
|
||||
|
||||
1. **The project contract wins.** `npc_base` mesh node, the 13 required core bones at their rest
|
||||
positions, five tint slots, front on Blender −Y / Godot +Z, grounded feet, 16,000
|
||||
triangles / 16,000 glTF vertices per character, 1,500 per gear, 4,000 per prop. Optional
|
||||
extra bones (fingers, face, toes) are allowed if they do not move the core rest positions. A skill
|
||||
may recommend another topology, naming or budget scheme; do not adopt it unless the
|
||||
change is explicitly planned and the contract, tests and docs are updated together.
|
||||
1. **The project contract wins.** `npc_base` mesh node, five tint slots, front on Blender −Y /
|
||||
Godot +Z, grounded feet, 24,000 triangles / 24,000 glTF vertices per character, 1,500 per
|
||||
gear, 4,000 per prop. The shipped procedural library uses the 13 required core bones at their
|
||||
rest positions; optional extra bones (fingers, face, toes) are allowed if they do not move the
|
||||
core rest positions. The one documented exception is the accepted Quaternius runtime path
|
||||
([ADR-0018](docs/adr/0018-quaternius-stage2-runtime-rig.md)): it keeps the 62-joint source rig
|
||||
at runtime while the 0.9.9 face/silhouette gate is open, and it must still supply the five tint
|
||||
slots, valid gear attach points and the same budgets. Both paths stay tested until that gate
|
||||
closes. A skill may recommend another topology, naming or budget scheme; do not adopt it unless
|
||||
the change is explicitly planned and the contract, tests and docs are updated together.
|
||||
2. **Blender MCP is optional interactive tooling.** The headless flatpak scripts
|
||||
(`tools/blender_assets.py`, `tools/npc_pipeline.py`) remain authoritative. MCP may be used
|
||||
for local part exploration; setup and safety are in [`docs/asset-tooling.md#optional-interactive-blender-mcp`](docs/asset-tooling.md#optional-interactive-blender-mcp).
|
||||
@@ -104,8 +108,9 @@ specialists the task needs:
|
||||
3. **CI stays offline and skill-free.** Skills are local guidance only; every gate must still
|
||||
work from a fresh checkout with committed `.glb`/`.png`/`.tres` files and Godot.
|
||||
4. **No new asset source without the licence gate.** Skills may suggest reference packs or
|
||||
generated meshes; ADR-0015 (CC0, or CC-BY with attribution in `CREDITS.md`) and the
|
||||
provenance rules in `docs/npc-model-sourcing.md` still apply.
|
||||
generated meshes; ADR-0017 (the CC0 Quaternius NPC base) supersedes the generator-first
|
||||
decision in ADR-0015, and the CC0/CC-BY rule in ADR-0015 (attribution in `CREDITS.md`) plus
|
||||
the provenance rules in `docs/npc-model-sourcing.md` still apply to any new source.
|
||||
5. **Evidence, not vibes.** After a skill-informed change, run `blender_assets.py inspect`,
|
||||
`audit` and `fit` plus the relevant capture/smoke gates, and say in the handoff which
|
||||
skills informed the change and what the measurements were.
|
||||
|
||||
@@ -4,6 +4,36 @@ All notable user-facing changes to Gig Line. The format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions use
|
||||
Semantic Versioning.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.9.9] — 2026-11-06
|
||||
|
||||
### Changed
|
||||
|
||||
- Raised the character mesh budget from 16,000 to 24,000 triangles and glTF
|
||||
vertices so the Quaternius base and the role-dressed variants fit without
|
||||
heavy decimation. Gear and prop budgets are unchanged.
|
||||
- Replaced the whole NPC roster with the CC0 Quaternius characters. All twelve
|
||||
characters (tourist, jacket, coat, vest, dress, hoodie, firefighter, guard and
|
||||
the four crowd variants) now use one of three source outfits — casual tee,
|
||||
hoodie or work clothes — dressed by role with the five tint slots and the gear
|
||||
library. The procedural library and the MakeHuman head remain in the build as
|
||||
the fallback.
|
||||
- Characters stand with their arms down instead of a T/A pose, and the seated,
|
||||
idle and walking poses swing from the shoulder and hip the way the animation
|
||||
code intends on the new skeleton.
|
||||
- Head gear (cap, hood, sunglasses, firefighter helmet) and the camera were
|
||||
resized for the new head and arm. The camera now hangs from a wrist strap
|
||||
instead of a neck strap that floated next to the head.
|
||||
- The backpack straps were pulled in so they sit on the chest instead of
|
||||
hovering in front of it.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Characters no longer render with black legs or black patches: the new base
|
||||
carried a collapsed UV map that made the ambient-occlusion bake come out
|
||||
black, and the roster is now unwrapped and baked correctly.
|
||||
|
||||
## [0.9.8] — 2026-11-04
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -15,6 +15,10 @@ been cleared for public distribution.
|
||||
|
||||
## 3D characters, gear and props
|
||||
|
||||
The local MakeHuman head prototype uses the CC0 MakeHuman/MPFB base mesh
|
||||
(see [`docs/makehuman-head-review.md`](docs/makehuman-head-review.md)); it is a
|
||||
development prototype, not a shipped character.
|
||||
|
||||
The character, gear and prop library is generated by project-owned scripts
|
||||
(`tools/blender_assets.py`, `tools/npc_pipeline.py`, `tools/model_pipeline.py`)
|
||||
using Blender. The `.blend` working sources are developer artifacts and are not
|
||||
@@ -42,12 +46,22 @@ shipped derivative of CesiumMan or RiggedFigure must keep the attribution above.
|
||||
|
||||
### MakeHuman head source
|
||||
|
||||
The decimated head used by the character head prototype is derived from the
|
||||
MakeHuman / MPFB base mesh, **CC0** (`assets/models/blender_src/makehuman_head.glb`;
|
||||
source `tmp/npc-research/makehuman/base.obj` with its bundled
|
||||
`LICENSE.md`/`LICENSE.ASSETS.md`). It is retargeted, decimated and re-weighted by
|
||||
project scripts; the exported character GLBs remain project assets. See ADR-0015
|
||||
and `docs/npc-model-sourcing.md`.
|
||||
The standalone MakeHuman head prototype is derived from the MakeHuman / MPFB base
|
||||
mesh, **CC0** (`assets/models/blender_src/makehuman_head.glb`; source
|
||||
<https://static.makehumancommunity.org/makehuman.html>, MakeHuman base mesh,
|
||||
CC0 1.0). It is cut, retopologised and re-weighted by project scripts; the exported
|
||||
character GLBs remain project assets. The development source files were removed
|
||||
from `tmp/` after the 2026-11-05 pivot to Quaternius. The head is retained as a
|
||||
tested fallback, not the active NPC base. See ADR-0017 and
|
||||
`docs/npc-model-sourcing.md`.
|
||||
|
||||
### Quaternius NPC base source
|
||||
|
||||
The active NPC base migration uses **Quaternius Ultimate Modular Characters** and
|
||||
**Ultimate Modular Women** (CC0 1.0, <https://quaternius.com/>). The three
|
||||
researched GLBs are development inputs and are not shipped directly; the baked,
|
||||
retargeted character GLBs are project assets. See ADR-0017 and
|
||||
`docs/npc-model-sourcing.md` for the decision, source hashes and rollout status.
|
||||
|
||||
The venue geometry is authored procedurally in `src/game/world_builder.gd` as a
|
||||
stylized reconstruction. The Joke Shop is a fictional stand-in for the real
|
||||
|
||||
@@ -2143,3 +2143,52 @@ must inherit the reviewed face/hand/gear baseline instead of multiplying whateve
|
||||
project-owned clothing; keep out of CI/build.)
|
||||
- [x] Body-shape diversity decision (2026-11-04, owner): **yes** — use MakeHuman for the next
|
||||
character pass. Promoted to the **MakeHuman character pipeline** track.
|
||||
|
||||
## NPC pose harness (v0.9.8)
|
||||
|
||||
Completed 2026-11-04. The pose harness bug that made every staged capture save a rest frame
|
||||
was found and fixed; it unblocked the weights/pose evidence and the seated/walking deformation
|
||||
fixes. User-facing changes are recorded under 0.9.7 and 0.9.8 in `CHANGELOG.md`.
|
||||
|
||||
Goal: `src/test/capture_npc_pose.gd` reports a pose but saves rest frames; the root cause is
|
||||
narrowed to the skinned mesh ignoring its `Rig` parent transform. Fixing it unblocks the
|
||||
weights/pose column of the 3D review; the evidence now feeds the Quaternius base bake.
|
||||
|
||||
*Skill route: `rigging`; evidence in `docs/3d-model-review.md` §"Pose evidence is blocked".*
|
||||
|
||||
- [x] Root cause found (2026-11-04): the harness never disabled the avatar's `_physics_process`,
|
||||
so `_advance_fsm` + `_move_toward_target` ran every tick and reset `arrived`/velocity, which
|
||||
made `theater_pose()` return "idle" and reverted the pose to rest. (The earlier "moving the
|
||||
`Rig` child does not render" probe was a red herring: it set `rig.position.y` inside `_shoot()`
|
||||
in the same frame as `root.get_texture().get_image()`, so it never got a rendered frame.)
|
||||
- [x] Harness rebuilt: `capture_npc_pose.gd` now calls `set_physics_process(false)`/
|
||||
`set_process(false)` after staging the avatar; the pose survives because only the direct
|
||||
`_animate()` ticks drive it. Verified: sit vs idle diff 5.63, walk vs idle diff 2.29, and the
|
||||
vision judge confirms the seated frame reads as crouched.
|
||||
- [x] Captured all poses for all 12 variants (idle / walk_contact / walk_pass / sit × full +
|
||||
shoulder views = 96 frames) and built `sheet_pose.jpg`/`sheet_shoulder.jpg` under `tmp/`.
|
||||
(No separate "wave" theater pose exists; the escalation raise-hand gesture is not a distinct
|
||||
capture in this harness.)
|
||||
|
||||
## Milestone 0.9.7 — Character development (patch)
|
||||
|
||||
Completed 2026-11-04 and published as v0.9.7. The detailed character-development record already
|
||||
lives under "Character development" above; this milestone checklist is kept verbatim for the
|
||||
release record.
|
||||
|
||||
Goal: release the finished procedural character development — the realistic face on all eight
|
||||
variants, the four crowd-variety characters and the per-role pools — as a **patch** on top of
|
||||
0.9.6. This keeps the 3D model and map improvements out of the patch; they land together in the
|
||||
0.10.0 minor.
|
||||
|
||||
- [x] Rig pose harness fixed and the weights/pose evidence captured (see the top track).
|
||||
- [x] Character work done: 12-character library, realistic face, crowd variants, per-role pools,
|
||||
24-NPC budget 6.94 ms (152,464 tris / 168 meshes).
|
||||
- [x] QA + local build for 0.9.7: Godot 277/277, Python 161/161, audit 49 OK, fit/proportions
|
||||
12/12, release smoke matrix green, gallery 21/0, clean worktree build (Linux 172 MB,
|
||||
Windows 206 MB, macOS 159 MB, Web 111 MB, HQ 266 MB), checksums verified, Linux binary
|
||||
launches headless. Tagged `v0.9.7` (CI build/publish follows).
|
||||
|
||||
**Exit:** met — 0.9.7 ships the 12-character library (face realism + crowd variety) with the pose
|
||||
harness fixed; the runtime contract is unchanged. The Quaternius NPC base and the map rebuild are
|
||||
not in this patch (they target 0.10.0).
|
||||
@@ -10,6 +10,11 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
the licence gate. Downloads live in `tmp/research/` (gitignored): DDG image search, street-view
|
||||
frames, neighbouring buildings — never committed, never shipped. Promotion into
|
||||
`docs/screens/refs/` always passes the sidecar licence + privacy gate and `CREDITS.md`.
|
||||
- **AI-assisted asset review loop (2026-11-04):** head/face shape decisions use a concept +
|
||||
judge loop — generate a target concept (`generate_textures.generate_image`, Seedream) and use
|
||||
`tools/describe_image.py` as the vision judge, iterating until the judge's specific fixes
|
||||
converge. Review pages: `docs/screens/head-review/README.md`, `docs/3d-model-review.md`
|
||||
§AI-driven head rework.
|
||||
- **Project agent skills (2026-11-03):** two tracked skills live in `.agents/skills/`:
|
||||
`gig-line-blender-asset-contract` and `gig-line-release-qa`. They are guidance only,
|
||||
add no runtime dependency and are referenced from `tools/README.md`. The third-party
|
||||
@@ -45,11 +50,6 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
(94 skills + shared `references/`), gitignored, kept out of Godot's scanner by
|
||||
`.agents/.gdignore`. Keep the pack flat when updating (`cp -a <pack>/.claude/skills/. ...`).
|
||||
Pi gates project `.agents/skills` behind project trust.
|
||||
- **NPC model sourcing (ADR-0015):** CC0 acceptable for shipped assets; CC-BY only with
|
||||
attribution in `CREDITS.md`; no service assets with redistribution restrictions. The procedural
|
||||
generator is the primary path. Quaternius and MakeHuman were evaluated as references only, not
|
||||
shipped bases (Quaternius: 62-bone T-pose rig mismatch; MakeHuman: needs retopology + custom
|
||||
rig + clothing).
|
||||
- **Gaussian splatting (unversioned):** offline/development-only until an ADR proves licence,
|
||||
privacy, Compatibility/Web cost and mesh collision. Accepted photo channels: team captures,
|
||||
permissioned user/volunteer photos, clearly licensed downloads; every set needs a
|
||||
@@ -62,11 +62,33 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
`docs/asset-tooling.md#optional-interactive-blender-mcp`. The headless flatpak scripts remain authoritative; MCP is
|
||||
never a build/CI/test/runtime dependency, and anything MCP touched must pass the
|
||||
CLI audit/fit gates.
|
||||
- **Character mesh cap (2026-11-04):** all characters use 16,000 tris / 16,000 glTF vertices
|
||||
- **Character mesh cap (2026-11-05):** all characters use 24,000 tris / 24,000 glTF vertices
|
||||
(`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in `tools/blender_assets.py`; `npc_pipeline.MAX_TRIANGLES`/
|
||||
`MAX_VERTICES`). The required 13 core bones stay, optional extra bones are allowed, and the
|
||||
low-poly `npc_base` fallback remains the shipped fallback. Measure the 24-NPC/Web budget after
|
||||
low-poly `npc_base` fallback remains the shipped fallback. The raise funds the Quaternius base bake and
|
||||
the 12-variant clothing detail. Measure the 24-NPC/Web budget after
|
||||
any geometry change; never silently lower the cap without a new decision.
|
||||
- **Quaternius is the NPC base (2026-11-05, owner):** ADR-0017 supersedes the generator-first
|
||||
sourcing decision. The CC0 Quaternius Ultimate Modular character pack is the primary NPC mesh
|
||||
and animation source. Stage 1 bakes a proportion-warped, 13-core-contract `npc_base` so
|
||||
`NpcAvatar`, gear and animations keep working; the 62-joint/24-clip source rig is authoring
|
||||
input, not a runtime dependency. If the warp fails the silhouette gate, Stage 2 needs a new ADR
|
||||
for a 62-joint runtime, not a hybrid. The MakeHuman head and procedural `npc_base` remain
|
||||
tested fallbacks until the Quaternius rollout ships. Quaternius source: CC0, attribution in
|
||||
`CREDITS.md`; the three researched GLBs and licence notes are kept in
|
||||
`tmp/npc-research/quaternius/`. MPFB/MakeHuman body work is closed; do not add new work there.
|
||||
- **Quaternius bake keeps source detail (2026-11-05, owner):** during the contract bake (§2),
|
||||
keep the Quaternius mesh as close to source as possible — no unnecessary decimation or LOD
|
||||
reduction. The canonical base is only 3,199 verts / 6,206 tris, far under the 24,000 cap, so
|
||||
there is no budget reason to decimate before the silhouette gate. Do not reuse the old
|
||||
7,000-triangle or MPFB decimation targets.
|
||||
- **Quaternius runtime rig (2026-11-05, owner, ADR-0018 option 2):** the Stage 1 proportion warp
|
||||
failed the silhouette gate (arm ×1.63 / leg ×0.74 / torso ×1.61; vision judge: paddle hands,
|
||||
lost forearms, stretched shins). The owner chose to **adopt the Quaternius 62-joint rig at
|
||||
runtime** instead of warping: `NpcAvatar` maps the Quaternius joint names, gear attach points
|
||||
are re-derived from the Quaternius skeleton (hip ≈0.87, shoulder ≈0.148/1.39, wrist ≈0.99,
|
||||
knee ≈0.52), the mesh keeps source LOD (arm-down rest pose baked), and Idle/Walk play directly
|
||||
on the 62-joint rig (no retarget).
|
||||
- **Engine baseline:** Godot 4.7.2 stable (official editor `~/.local/bin/godot`). The system's
|
||||
`godot-runner` cannot load arbitrary project paths (compiled without path overrides); use the
|
||||
official editor binary.
|
||||
@@ -85,6 +107,26 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
cleared, storefront/rating approval pending, external playtests and full hardware matrix pending.
|
||||
Recorded in `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md`, `docs/known-limitations.md`.
|
||||
|
||||
- **Release roadmap (2026-11-05, owner):** `0.9.9` Quaternius pilot base (rig/export fix,
|
||||
rest-relative pose math, owner face/silhouette gate), `0.9.10` 12-role roster, `0.9.11`
|
||||
scheduled venue/prop/lighting polish, `0.9.12` performance/Web/accessibility verification,
|
||||
`0.10.0` public-readiness minor, then `0.10.1`–`0.10.3` fixes, `0.11.0` replay + fourth venue +
|
||||
localization foundation, `0.12.0` signed/storefront builds + save v2, `0.13.x` content complete
|
||||
and release candidates, `1.0.0` stable. The full gap analysis and roadmap live in `PLAN.md`; the
|
||||
character track is versioned now, so a blocked rig cannot block the release. Planning only —
|
||||
none of these versions is built yet.
|
||||
- **Quaternius face/readability gate (2026-11-05):** the base is 6.2k tris total and replaces a
|
||||
library with a sculpted realistic face, so the 0.9.9 gate is a product decision, not just an
|
||||
art check. Owner picks full swap / crowd-only / extra head detail. Do not lower character
|
||||
fidelity silently.
|
||||
- **Pose composition is rest-relative (resolved 2026-11-05):** Godot's `set_bone_pose_rotation`
|
||||
offsets from the bone rest, so the correct swing around a world axis is
|
||||
`rest * Quaternion(rest.inverse() * axis, angle)`. For the old 180° rest this reduces to the
|
||||
legacy `rest * Quaternion(axis, angle)`, so the 13-core fallback is unchanged (commit `8b1bf73`).
|
||||
- **One character source, one fallback:** no new NPC source without an ADR. MPFB body work stays
|
||||
closed, the Stage-1 proportion warp is not reopened, and the procedural `npc_base` + MakeHuman
|
||||
head stay tested until the Quaternius base passes its gate.
|
||||
|
||||
## Gotchas / Known Issues
|
||||
|
||||
- **Two character builders — do not confuse them.** `tools/blender_assets.py` is the real builder
|
||||
@@ -105,6 +147,44 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
out of a portrait frame (park the rest of the crowd away instead).
|
||||
- Blender: `bisect_plane` and thin bevels leave zero-area triangles (Godot "non-finite normal in
|
||||
LOD generation"); `_clean()` + a weld after bisect fixes them and the audit rejects them.
|
||||
- Quaternius arm-down bake (working recipe, `tmp/npc-research/bake_armdown_v5.py`): pose-rotate
|
||||
`UpperArm.L/R` −90° local X → read the pose matrices → apply the Armature modifier → **reset the
|
||||
pose to identity** → set each arm bone's rest matrix from its pose matrix → re-add the modifier →
|
||||
export `export_apply=False`. Missing the pose reset double-applies the arm-down (mesh ±0.53
|
||||
instead of ±0.23). `armature_apply` in Blender 5.2 is broken (deforms the mesh but does not bake
|
||||
the rest). The canonical base must be **join-only** (`build_base_minimal.py`: no `remove_doubles`
|
||||
weld, no scale/ground transform, keep the Armature modifiers) — the weld and the scale/ground
|
||||
transform corrupt the skin weights (arm poses to ±0.51 instead of −0.23).
|
||||
- Quaternius `NpcAvatar` wiring (commits `2a6f29f`, `cd285b3`): `RIG_BONES` +
|
||||
`NpcAvatar.bone_name(joint, side)` hold the **only** side mapping — the Quaternius labels `L`/`R`
|
||||
opposite the project, so proxy side 0 (`-x`) drives `UpperLeg.R`/`UpperArm.R` while side 1 drives
|
||||
the `.L` bones. The arm/elbow swing axis is `Vector3.LEFT` (legs stay `RIGHT`). The hand proxy is
|
||||
`global_wrist − global_elbow` (subtracting the parent-relative `elbow.position` misplaces
|
||||
handheld gear). Old 13-core names resolve through the same table.
|
||||
- **Bone posing must write the global pose** (`NpcAvatar._swing_bone`): `reset_bone_pose()` then
|
||||
`set_bone_global_pose(Transform3D(Basis(swing * global_rot), current_origin))`, conjugated with
|
||||
`get_bone_global_rest()`. Both rejected alternatives fail: the rest-relative
|
||||
`set_bone_pose_rotation(rest * Quaternion(rest⁻¹·axis, angle))` swings about the parent-relative
|
||||
rest (the glTF importer already folded the axis conversion into it) and splayed the Quaternius
|
||||
arms sideways; the plain `rest⁻¹ * swing * rest` conjugation flips the limbs 180°. Verified by
|
||||
measuring `get_bone_global_pose()` after the call — posed bone axis matches the proxy pivot at
|
||||
dot 1.000 on the Quaternius and the procedural rigs render unchanged.
|
||||
Two caveats: children need the parent's swing passed in (the write is absolute), and
|
||||
`set_bone_global_pose()` is a **no-op outside the scene tree**, so the headless runner (tests run
|
||||
in `_initialize()`) cannot assert on it.
|
||||
- **The Quaternius source UV map is degenerate** — every loop at (0,0), flat-colour materials, no
|
||||
atlas. The AO/texture bake silently produces a 100 % black map, which tints every AO'd slot
|
||||
(skin, hair, accessory) black in Godot: a black leg is the symptom. `build_base_minimal.py` now
|
||||
Smart-UV-unwraps and repacks the layout (texel density 0 → 73–74 px/m on 256²), and the base
|
||||
pipeline is two explicit steps with distinct files: `build_base_minimal.py`
|
||||
(`quaternius/Casual_Hoodie.glb` → `quaternius_tpose.glb`, joins without welding, renames the
|
||||
materials to the five tint slots, unwraps) then `bake_armdown_v5.py` (`quaternius_tpose.glb` →
|
||||
`quaternius_base.glb` + `assets/models/blender_src/npc_casual.blend`). Re-running v5 on its own
|
||||
output double-bakes the rest.
|
||||
- Hand gear must be self-contained: the camera carried a 0.42 m neck-strap loop that floats when the
|
||||
item is parented to the wrist. Hand items get a short wrist strap, not a neck strap. The backpack
|
||||
straps were authored for the wider procedural chest and floated ~5 cm proud of the Quaternius
|
||||
torso; the chest points now sit at x 0.12 / Godot z 0.15, which fits both.
|
||||
- Image models have per-model size floors/caps (Seedream needs 1920+; `GEN_SIZE=2048`, do not
|
||||
lower; gpt-5-image-mini caps lower). `generate_image` applies model-specific sizing; concept
|
||||
generation defaults to `--request-size 1536`.
|
||||
@@ -169,6 +249,8 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
- Character checks: `python3 tools/blender_assets.py inspect|audit|fit`,
|
||||
`python3 tools/npc_proportions.py`
|
||||
- Docs: `python3 tools/check_docs.py` (every tracked image must be linked from its folder README)
|
||||
- Head/part pipeline: `python3 tools/npc_parts.py build|extract|assemble|inspect|sheet|compare`
|
||||
(Blender subcommands run under flatpak). Head sources: `build_character(head="face"|"proto"|"mh")`.
|
||||
- Reference research: `python3 tools/refcollect.py search <q> --slug <block> --limit N`
|
||||
(local-only), `tools/refboard.py` (Commons), `tools/three_up.py compose/rows`,
|
||||
`tools/scene_concepts.py generate/anchor/realism`
|
||||
@@ -176,18 +258,32 @@ canonical implemented-features list in `docs/development-status.md`.
|
||||
## Open Questions
|
||||
|
||||
- Whether the responder avatar should be playable/controllable or purely scripted.
|
||||
- Localization target languages beyond English.
|
||||
- macOS notarization approach for broad distribution.
|
||||
- Quaternius scope: full 12-role swap, crowd-only, or extra head detail (owner decision at 0.9.9).
|
||||
- Localization target languages beyond English (0.11.0/0.12.0).
|
||||
- macOS notarization approach for broad distribution (0.12.0).
|
||||
- Fourth-venue choice for 0.11.0 (candidate: TCL Chinese Theatre forecourt).
|
||||
|
||||
## Current State
|
||||
|
||||
The playable build covers the three venues, tutorial/campaign/checkpoints, the 0.9.3–0.9.6
|
||||
polish/texture/integration passes and the Blender + realism asset pipeline (see `DONE.md` and
|
||||
`CHANGELOG.md`). The 12-character library (realistic face + four crowd variants) and the
|
||||
Comedy Store map rebuild are complete. **Priority (2026-11-04, owner):** 1) the **Blender NPC
|
||||
model overhaul** (head/face/hands, local part pipeline), 2) finish the MakeHuman character + the
|
||||
3D model and map improvements, 3) ship it all together in the **0.10.0 minor**. The 0.9.7/0.9.8
|
||||
character patches are published; `TODO.md` is the priority source. Test counts as of 2026-11-04:
|
||||
Godot 277/277, Python 162/162, `check_docs` OK.
|
||||
v0.9.8 is published. The build covers the three venues, the shift loop, tutorial/campaign/
|
||||
checkpoints, the 0.9.3–0.9.8 polish/texture/integration/character passes and the Blender +
|
||||
realism asset pipeline (see `DONE.md` and `CHANGELOG.md`). The 12-character procedural library
|
||||
(realistic face + four crowd variants) and the Comedy Store map rebuild are complete.
|
||||
|
||||
**Planning pivot (2026-11-05, owner):** the character and map work was re-sequenced into
|
||||
patches (`0.9.9`–`0.9.12`) with `0.10.0` as the public-readiness minor. The gap analysis and the
|
||||
three release roadmaps now live in `PLAN.md`; `TODO.md` is the executable task list. ADR-0017
|
||||
Stage 1 (proportion warp) is dead; ADR-0018 option 2 (62-joint runtime) is accepted.
|
||||
|
||||
**Quaternius pilot (2026-11-05, commits `cd285b3`, `fb3e75a`, `90f189a`):** `npc_casual` ships as
|
||||
the Quaternius base. It renders arm-down, carries the five tint slots, plays the Idle/Walk/Sit pose
|
||||
code on the 62-joint rig, and the procedural rigs are visually unchanged. Verified: `inspect`
|
||||
(no issues), `audit` OK (6,206 tris / 12,988 verts, 62 bones), `fit` head 0.201 m, texel density
|
||||
73–74 px/m, `npc_proportions` measured, 24-NPC budget smoke OK (168 meshes, 162,304 tris,
|
||||
0.82 ms avg frame). The face/silhouette gate sheet and the three options are in
|
||||
`docs/screens/npc-face-gate/`; the **owner decision is the only thing left before the 0.9.9
|
||||
rollout**.
|
||||
|
||||
Test counts as of 2026-11-05: Python 167/167, Godot 278/278, `check_docs` OK.
|
||||
|
||||
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
|
||||
@@ -16,20 +16,91 @@ The game is about **judgment, communication, access verification, and proportion
|
||||
|
||||
## Development Priorities
|
||||
|
||||
The active planning order is set in [`TODO.md`](TODO.md) and in `AGENTS.md`:
|
||||
The active task order is in [`TODO.md`](TODO.md); the gap analysis and the release roadmap are
|
||||
below. The current order is:
|
||||
|
||||
1. **Character development (unversioned).** Expand the owned procedural character library with
|
||||
crowd, age, build, hair and outfit variety. Keep the frozen bone/tint/gear contract; see
|
||||
[ADR-0015](docs/adr/0015-npc-model-variety-sourcing.md) and
|
||||
[`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md). Quaternius is a reference;
|
||||
MakeHuman is deferred to a separate local pipeline.
|
||||
2. **Local map development (unversioned).** Move beyond stylized venue reconstruction with
|
||||
licensed OpenStreetMap and 3D building data, downloaded reference imagery for photoreal
|
||||
facades, AI-assisted composition, and a Gaussian-splatting evaluation that stays in the
|
||||
development pipeline unless it proves runtime-safe.
|
||||
3. **Versioned release hardening.** The 0.10.0 release candidate and later semantic versions come
|
||||
after the unversioned development tracks in planning order. Versions are assigned only when a
|
||||
track's scope is accepted into a release.
|
||||
1. **0.9.9–0.9.12 patches.** Finish the character base (the accepted Quaternius 62-joint runtime),
|
||||
roll it out to the 12 roles, close the scheduled venue/prop/lighting polish, and verify
|
||||
performance, Web and accessibility.
|
||||
2. **0.10.0 minor.** Public readiness: external playtests, balance acceptance, legal/storefront
|
||||
approval, the platform matrix and the documentation/picture lock.
|
||||
3. **0.10.x patches → 0.11.0.** Content and systems depth: live body-camera replay, a fourth
|
||||
venue and the localization foundation.
|
||||
4. **0.12.0 → 1.0.0.** Platform/distribution, content complete, release candidates and the stable
|
||||
release.
|
||||
|
||||
Versions are assigned only when a track's scope is accepted into a release (see
|
||||
[Release Roadmap](#release-roadmap)). Unversioned tracks — the 3D review evidence base,
|
||||
maintenance and the Gaussian-splatting study — do not receive a placeholder version.
|
||||
|
||||
## Plan Evaluation (2026-11-05)
|
||||
|
||||
The game plan itself is sound and largely built: three venues, the escalation ladder, access
|
||||
artifacts, the radio/report system, the shift review and the campaign loop all exist and are
|
||||
tested. What needed fixing was the **delivery plan**, not the design.
|
||||
|
||||
- **Character sourcing churned.** The project moved procedural → MakeHuman → Quaternius, and the
|
||||
Quaternius Stage-1 warp failed its own gate. That leaves a correct decision (ADR-0018) with a
|
||||
half-finished implementation. The plan now makes the rig correction one small version, gates the
|
||||
roster behind a face/readability decision, and keeps exactly one fallback.
|
||||
- **One milestone carried too much.** The old 0.10.0 mixed character work, map work and every
|
||||
public-release gate, so one blocked export blocked the release. Character and venue polish are
|
||||
now patches; 0.10.0 is only the release gate.
|
||||
- **Unversioned work had no deadline.** The 3D review backlog could grow without bound. It is now
|
||||
capped: the ranked venue/prop/lighting items are scheduled into 0.9.11, the budget checks into
|
||||
0.9.12, and everything else is an explicit accept or deferred. Gaussian splatting stays
|
||||
unscheduled because no product need justifies its risk yet.
|
||||
- **Legal, signing and storefront work is release work, not polish.** It now has named checkboxes
|
||||
in 0.10.0 and 1.0.0 instead of living in prose.
|
||||
- **What is deliberately not planned:** new genres or mechanics, a live-service model, or a
|
||||
runtime splat renderer. The de-escalation-first, rare-emergency design stays frozen.
|
||||
|
||||
Feasibility: the remaining character work is small in geometry terms (6.2k triangles against a
|
||||
24k cap) and larger in rig/export terms; the venue work is polish; most release gates are
|
||||
evidence gathering rather than engineering. The main schedule risk is external review and
|
||||
approval, not code.
|
||||
|
||||
## Character Creation Re-Evaluation (2026-11-05)
|
||||
|
||||
**Where it stands.** The shipped library is 12 procedural Blender variants (4.7k–5.8k triangles
|
||||
each) with a sculpted face, five tint slots and 18 gear pieces. The owner then chose the CC0
|
||||
**Quaternius Ultimate Modular Characters** as the primary source. Stage 1 (warp the Quaternius
|
||||
mesh onto the 13-core contract) failed its silhouette gate — arm ×1.63, leg ×0.74, torso ×1.61,
|
||||
judged distorted. [ADR-0018](docs/adr/0018-quaternius-stage2-runtime-rig.md) therefore adopts the
|
||||
source **62-joint rig at runtime**: no warp, source LOD (3,199 verts / 6,206 tris), source
|
||||
Idle/Walk clips, and gear pivots re-derived from the Quaternius skeleton.
|
||||
|
||||
**What is already built.** The canonical `quaternius_base.glb` (welded, grounded, arm-down rest),
|
||||
the 62→13 joint map, the material→five-slot map, the clip inventory (24 clips; Idle and Walk in
|
||||
scope) and the `NpcAvatar` joint-name mapping with rest-derived gear pivots.
|
||||
|
||||
**What actually blocks it.** Two technical items about the export and the pose math, not the art:
|
||||
|
||||
1. the arm-down GLB renders in A-pose in Godot because the bake writes wrong inverse bind
|
||||
matrices; and
|
||||
2. `_pose_bone()` composes rotations as `rest * Quaternion(axis, angle)`, which only cancels for
|
||||
the old 180° rest, not the Quaternius ~76° rest.
|
||||
|
||||
**Re-evaluation findings.**
|
||||
|
||||
- Triangle budget is not the problem (6.2k against a 24k cap). Do not decimate the source.
|
||||
- **Face fidelity is the real risk.** The 0.9.7 library has a sculpted realistic face; the
|
||||
Quaternius head is low-poly. A blanket swap would regress every close-up. The plan therefore
|
||||
makes the owner choose at 0.9.9 between (a) a full swap, (b) Quaternius for crowd roles with the
|
||||
procedural library for named roles, or (c) extra head subdivision/hair cards on the Quaternius
|
||||
head.
|
||||
- **Animation** becomes clip-driven, which removes the hand-built pose composition as the main
|
||||
path. Poses the clips do not cover (seated, raise-hand, head turn) must be decided explicitly
|
||||
rather than patched per scene.
|
||||
- **One fallback, no third source.** The procedural `npc_base` and the MakeHuman head stay tested
|
||||
until the new base passes the gate. MPFB body work stays closed and the Stage-1 warp is not
|
||||
reopened. A new character source needs a new ADR.
|
||||
- **Sequence matters.** Fix the rig first (0.9.9), then the roster (0.9.10). Rolling a broken rig
|
||||
across 12 variants would waste the whole pass.
|
||||
|
||||
**Reversal condition.** If the skin cannot be exported correctly without a new exporter path, or
|
||||
if the owner rejects the low-poly face in every option, the plan falls back to the procedural
|
||||
library and retires Quaternius with a recorded ADR. That decision is made at 0.9.9, not later.
|
||||
|
||||
## Core Gameplay Loop
|
||||
|
||||
@@ -169,7 +240,7 @@ The action wheel prioritizes de-escalation: Stop, Look, Point, No, Ask, Inspect,
|
||||
|
||||
The visual direction is readable stylized realism:
|
||||
|
||||
- **Character direction:** 8 skinned NPC variants, 18 gear pieces and 20 street/venue props, with four crowd-variety variants prototyped. The generator-first expansion is the active character track ([ADR-0015](docs/adr/0015-npc-model-variety-sourcing.md)); Quaternius is a reference and MakeHuman is deferred to a local pipeline.
|
||||
- **Character direction:** 12 skinned NPC variants, 18 gear pieces and 20 street/venue props. The active character track is the Quaternius base at runtime ([ADR-0018](docs/adr/0018-quaternius-stage2-runtime-rig.md)): the CC0 source mesh keeps its proportions on the 62-joint rig, Idle/Walk run as clips, and the procedural library stays as the tested fallback until the face/silhouette gate passes. See [Character Creation Re-Evaluation](#character-creation-re-evaluation-2026-11-05).
|
||||
- **Realism pass:** AI-assisted per-phase skyline panoramas, v2 surface tiles with normal maps, wet asphalt at night, a warm day haze, phase-specific sun/exposure/fog, a tight flashlight spot (no beam geometry), and denser foliage.
|
||||
- **Local map direction:** the venues are currently stylized reconstructions. The local-map track evaluates licensed OpenStreetMap/3D building data, licensed reference photography, AI-assisted composition and Gaussian splatting as a development tool; online data and splat rendering are not runtime dependencies.
|
||||
- **References and QA:** runtime UI art lives in `assets/images/ui/`; reference/art-direction sets live in `docs/screens/designs/` and `docs/screens/concepts/`; captures live in `docs/screenshots/`; realism runs live in `docs/screens/realism/`. `screen_designs.py compare/judge` and `tools/check_screenshots.sh` guard drift.
|
||||
@@ -177,7 +248,7 @@ The visual direction is readable stylized realism:
|
||||
|
||||
The active planning order — unversioned character and local-map development first, then the versioned 0.10.0 release-candidate hardening and later releases — is tracked in [`TODO.md`](TODO.md); this plan stays at the product/design level.
|
||||
|
||||
Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [ADR-0008](docs/adr/0008-cone-focus-and-head-ring.md), [ADR-0009](docs/adr/0009-radial-action-menu.md), [ADR-0010](docs/adr/0010-procedural-npc-presentation.md), [ADR-0011](docs/adr/0011-aim-free-selection-and-precision-walk.md), [ADR-0012](docs/adr/0012-surface-materials-and-generated-props.md), [ADR-0013](docs/adr/0013-blender-authored-npc-characters.md), [ADR-0015](docs/adr/0015-npc-model-variety-sourcing.md).
|
||||
Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [ADR-0008](docs/adr/0008-cone-focus-and-head-ring.md), [ADR-0009](docs/adr/0009-radial-action-menu.md), [ADR-0010](docs/adr/0010-procedural-npc-presentation.md), [ADR-0011](docs/adr/0011-aim-free-selection-and-precision-walk.md), [ADR-0012](docs/adr/0012-surface-materials-and-generated-props.md), [ADR-0013](docs/adr/0013-blender-authored-npc-characters.md), [ADR-0015](docs/adr/0015-npc-model-variety-sourcing.md), [ADR-0017](docs/adr/0017-quaternius-npc-base.md).
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -196,6 +267,106 @@ Primary decisions: [ADR-0007](docs/adr/0007-locomotion-and-input-layers.md), [AD
|
||||
|
||||
`config/version` in `project.godot` is the single version source (currently `0.9.8`). Releases are tagged `v<version>`; the CI pipeline builds and publishes the standard artifacts plus the optional Desktop HQ zip. Version bumps follow [Semantic Versioning](https://semver.org/): every major, minor and patch increment needs a changelog entry, a local `tools/build_releases.sh` build, and artifact/checksum verification before tagging. Unversioned development tracks do not receive a version until their scope enters a release. See [`docs/RELEASING.md`](docs/RELEASING.md) and [`README.md`](README.md) for commands.
|
||||
|
||||
## Gap Analysis
|
||||
|
||||
**Published baseline:** v0.9.8 (2026-11-04) — three playable venues, tutorial/campaign/checkpoint
|
||||
resume, the access/radio/escalation loop, shift review, the 12-character Blender library, 277
|
||||
Godot + 162 Python tests, a 21/21 screen-design judge and Compatibility/Web/Desktop HQ builds.
|
||||
|
||||
**Current source:** 0.9.8. The 0.10.0-rc.1 package built on 2026-11-03 is stale and is superseded
|
||||
by the patch sequence below; it is not a published release.
|
||||
|
||||
| Area | Remaining gap | Milestone |
|
||||
| --- | --- | --- |
|
||||
| Character base | Quaternius arm-down skin export, rest-relative pose composition, face/silhouette acceptance | 0.9.9 |
|
||||
| Character roster | 12-role mapping, modular outfits, gear fit, AO/texels, budget evidence | 0.9.10 |
|
||||
| Venue and asset polish | Ranked prop/foliage/lighting/set-dressing fixes, collider verification | 0.9.11 |
|
||||
| Performance and Web | Frame/memory/load evidence after the content changes, LOD/impostor verification | 0.9.12 |
|
||||
| Onboarding and balance | External first-time-player evidence, balance sweeps/tuning, content lock | 0.10.0 |
|
||||
| Accessibility and localization | Acceptance pass, caption coverage, pseudo-locale/length checks | 0.9.12 / 0.10.0 |
|
||||
| QA and release engineering | Full smoke matrix, clean build/checksum verification, target-matrix launches | 0.10.0 |
|
||||
| Legal, signing, storefront | macOS signing/notarization, AI-provider rights, storefront/rating approval, privacy sign-off | 0.10.0 / 0.12.0 |
|
||||
| Systems and content depth | Live incident replay, fourth venue, localization foundation | 0.11.0 |
|
||||
| Distribution | Signed storefront builds, frozen save contract, launch languages, hardware matrix | 0.12.0 |
|
||||
| Stable release | Content complete, accessibility audit, release candidates, 1.0.0 decision | 0.13.0 / 1.0.0 |
|
||||
| Future | More venues/seasonal content, image-to-mesh props, other deferred ideas | Unscheduled |
|
||||
|
||||
The 0.9.3 UI, 0.9.4 animation, 0.9.5 texture, 0.9.6 integration, 0.9.7 character development and
|
||||
0.9.8 deformation fixes are complete in [`DONE.md`](DONE.md); they are not remaining gaps. The
|
||||
2026-11-01 documentation audit is also closed.
|
||||
|
||||
## Release Roadmap
|
||||
|
||||
Every version below follows the release procedure in `AGENTS.md`. Patch versions contain fixes and
|
||||
polish; minor versions add user-facing features; the major version is the stable release with a
|
||||
compatibility promise. Each version's executable checklist is in [`TODO.md`](TODO.md).
|
||||
|
||||
### Roadmap to 0.10.0
|
||||
|
||||
The goal of this line is to finish the character and venue work and clear every public-release
|
||||
gate. The character track is versioned rather than unversioned so a blocked rig cannot hold the
|
||||
whole release.
|
||||
|
||||
| Version | Type | Scope | Exit criterion |
|
||||
| --- | --- | --- | --- |
|
||||
| 0.9.9 | patch | Quaternius pilot: correct skin export, rest-relative pose math, Idle/Walk verification, owner face/silhouette gate, one-role (or crowd-only) rollout | A Quaternius NPC renders and animates correctly and ships behind a tested procedural fallback |
|
||||
| 0.9.10 | patch | 12-role roster, modular outfits, five slots, AO/texels, gear fit, 12×6 baseline | The full roster passes contract, gear, budget and owner-baseline gates |
|
||||
| 0.9.11 | patch | Scheduled 3D review fixes: props/foliage, gear silhouette, venue shape language, lighting, set dressing, colliders | Every scheduled P1/P2 item is fixed or accepted with a measurement |
|
||||
| 0.9.12 | patch | Performance/Web/LOD/memory verification plus accessibility and pseudo-locale checks | No regression against the pre-character baselines |
|
||||
| 0.10.0 | minor | Public readiness: external playtests, balance acceptance, legal/storefront/signing decisions, platform matrix, docs and picture lock | 0.10.0 is content-locked, externally played, legally reviewed, documented and reproducibly built |
|
||||
|
||||
### Roadmap to 0.11.0
|
||||
|
||||
This line turns a stable 0.10.x into a deeper game: the incident timeline becomes a live replay, a
|
||||
fourth venue enters the campaign, and the text catalogue becomes a real localization pipeline.
|
||||
|
||||
| Version | Type | Scope | Exit criterion |
|
||||
| --- | --- | --- | --- |
|
||||
| 0.10.1 | patch | Onboarding and accessibility issues deferred from the 0.10.0 playtests | First-hour and accessibility defects closed |
|
||||
| 0.10.2 | patch | Balance/fairness sweeps, review-text and difficulty-progression tuning | Every balance-acceptance cell is in target or explicitly accepted |
|
||||
| 0.10.3 | patch | Save/checkpoint edge cases, gamepad/remap, pause/resume, small quality-of-life fixes | No known P1 stability defect remains |
|
||||
| 0.11.0 | minor | Live body-camera replay, fourth venue (candidate: TCL Chinese Theatre forecourt), localization foundation with one secondary language | The new systems ship without regressing performance, accessibility or balance |
|
||||
|
||||
### Roadmap to 1.0.0
|
||||
|
||||
This line is about distribution, contracts and completeness: signed builds on the launch stores, a
|
||||
frozen save format, finished content and release candidates.
|
||||
|
||||
| Version | Type | Scope | Exit criterion |
|
||||
| --- | --- | --- | --- |
|
||||
| 0.11.1 | patch | Replay/venue/localization fixes from wider play | No P1 defect in the 0.11.0 feature set |
|
||||
| 0.11.2 | patch | New-venue balance, campaign pacing and content readability | The 0.11.x line is stable enough to freeze its contracts |
|
||||
| 0.12.0 | minor | macOS signing/notarization, storefront packages/metadata/ratings, save-format v2 freeze, hardware matrix, launch languages | Signed, storefront-ready builds exist on every launch platform |
|
||||
| 0.12.1 | patch | Signing, installer, metadata and save-migration fixes from storefront dry runs | Every storefront build installs, launches and migrates cleanly |
|
||||
| 0.13.0 | minor | Content complete: all planned venues/events/dialogue, audio decision, final tutorial, accessibility audit, balance freeze | All planned content is implemented and frozen |
|
||||
| 0.13.1 | patch | Release candidate 1: full smoke/performance/save/accessibility matrix | A candidate passes every gate except final approval |
|
||||
| 0.13.2 | patch | Release candidate 2: verify fixes, freeze release notes/credits/privacy/known limitations | The final candidate is reproducible and documented |
|
||||
| 1.0.0 | major | Stable release: legal/storefront/rating approval, hardware/accessibility/localization sign-off, no P0/P1, publish on every platform | 1.0.0 is published with verified checksums and no open blocker |
|
||||
|
||||
## Roadmap Dependencies and Risks
|
||||
|
||||
- Content volume is the largest schedule risk; tutorial and campaign work must not outrun the
|
||||
event/dialogue matrix.
|
||||
- The vision judge is a dev-time aid with an external key and budget; CI must never make it a hard
|
||||
gate.
|
||||
- Godot engine changes can alter lighting and imports; do not combine an engine baseline move with
|
||||
a content or feature milestone.
|
||||
- Signed macOS builds, AI provenance and storefront approval are release blockers, not polish.
|
||||
- The character pipeline is the current critical path. The rig/export fix must be proven on one
|
||||
variant before the roster rollout; otherwise 0.9.10 multiplies a defect.
|
||||
- Photo-derived map assets add a provenance risk: downloaded or user-supplied photos can carry
|
||||
unclear licences, identifiable people, plates and property restrictions. No photo- or
|
||||
splat-derived asset ships without a completed sidecar, consent/licence review and redaction.
|
||||
- Gaussian-splat training is GPU/disk-heavy and the toolchain churns; keep it offline,
|
||||
version-pinned and outside CI, and never make a runtime splat renderer a release dependency.
|
||||
- The Blender skill pack is local, unversioned guidance; it can drift from the project contract or
|
||||
assume Blender MCP. Re-verify every adopted technique with `inspect`/`audit`/`fit` and never make
|
||||
a skill, asset pack or tool version a build or CI dependency.
|
||||
- The 3D re-evaluation can expand without bound. Cap it with the scorecard ranking: P0/P1 only, one
|
||||
asset family per commit, and an explicit accept for everything else.
|
||||
- Keep the de-escalation-first rules and rare-emergency design unchanged while adding progression,
|
||||
difficulty and feedback.
|
||||
|
||||
## Design Direction
|
||||
|
||||
The game should feel like a tense workplace simulation with occasional crisis moments. The most satisfying outcome is not overpowering an NPC; it is recognizing the situation early and resolving it calmly.
|
||||
@@ -206,4 +377,5 @@ The game should feel like a tense workplace simulation with occasional crisis mo
|
||||
|
||||
- A rare Bob Dobbs cameo (Easter-egg appearance; placement note in `MEMORY.md`).
|
||||
- Specific unusual NPC behaviors, such as slapping their own face, using hands as scissors, or interacting with props unexpectedly.
|
||||
- Live 3D body-camera replay; additional venues or seasonal events; real voice/dialogue audio; an image-to-mesh prop pipeline.
|
||||
- A fifth venue or seasonal events after the 0.11.0 fourth venue proves the venue pipeline.
|
||||
- Real voice/dialogue audio if the 0.13.0 audio decision does not fund it; an image-to-mesh prop pipeline remains unscheduled.
|
||||
@@ -8,11 +8,12 @@ The central design goal is simple:
|
||||
|
||||
## Development Priorities
|
||||
|
||||
The active planning order lives in [`TODO.md`](TODO.md) and [`AGENTS.md`](AGENTS.md):
|
||||
The active planning order lives in [`PLAN.md`](PLAN.md) (gap analysis and release roadmap), [`TODO.md`](TODO.md) (executable checklist) and [`AGENTS.md`](AGENTS.md):
|
||||
|
||||
1. **3D model and local map re-evaluation** — score every shipped asset, then evaluate licensed OpenStreetMap/3D data, reference-derived photoreal facades, AI-assisted composition and a Gaussian-splatting development evaluation.
|
||||
2. **Character development** — crowd, age, build, hair and outfit variety on the owned procedural library, keeping the frozen NPC contract. Source evaluation and the licence decision are in [`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md).
|
||||
3. **Versioned release hardening** — the 0.10.0 release candidate and later semantic versions follow the unversioned development tracks.
|
||||
1. **`0.9.9`–`0.9.12` patches** — finish the Quaternius NPC base on the accepted 62-joint runtime, roll it out to the 12 roles, close the scheduled venue/prop/lighting polish, then verify performance, Web and accessibility.
|
||||
2. **`0.10.0` minor** — public readiness: external playtests, balance acceptance, legal/storefront/signing decisions, the platform matrix and the documentation/picture lock.
|
||||
3. **`0.10.x` → `0.11.0`** — live incident replay, a fourth venue and the localization foundation.
|
||||
4. **`0.12.0` → `1.0.0`** — signed storefront builds, a frozen save contract, content complete, release candidates and the stable release.
|
||||
|
||||
Versions are assigned only when a track's scope is accepted into a release. Every major, minor and patch version requires a changelog entry, a local release build and artifact verification before tagging.
|
||||
|
||||
@@ -33,14 +34,16 @@ Versions are assigned only when a track's scope is accepted into a release. Ever
|
||||
> Downloads are public (no sign-in required) since the host's instance-wide
|
||||
> `REQUIRE_SIGNIN_VIEW` was disabled on 2026-10-31.
|
||||
>
|
||||
> **Latest published: v0.9.8** — the character-development patch: the
|
||||
> **Latest published: v0.9.9** — the character-roster patch: all twelve NPCs now
|
||||
> use the CC0 Quaternius base with the five tint slots, role gear and baked maps.
|
||||
> Superseded: v0.9.8, the character-development patch: the
|
||||
> 12-character library, realistic face pass, four crowd-variety variants and the
|
||||
> wrist/hip/knee deformation fixes. Previous 0.9.x patches added the
|
||||
> Hollywood/Joke Shop maps, UI/accessibility polish, NPC visual optimization,
|
||||
> texture/material refinement and integration checkpoint.
|
||||
>
|
||||
> **Current source: v0.9.8** — tagged from the same commit. The 0.10.0
|
||||
> development track remains gated on the NPC overhaul, MakeHuman/local-map work
|
||||
> development track remains gated on the Quaternius NPC rollout, local-map work
|
||||
> and release approvals.
|
||||
> Releases are built automatically when a `vX.Y.Z` tag is pushed
|
||||
> (see [`docs/RELEASING.md`](docs/RELEASING.md)); manual builds use
|
||||
@@ -95,12 +98,14 @@ Every capture is rendered from the current build by [`tools/check_screenshots.sh
|
||||
|
||||
### 3D asset progress
|
||||
|
||||
The character, gear and prop libraries are authored procedurally in Blender and audited before use;
|
||||
the sheets below track their visual progress.
|
||||
The character roster is built from the CC0 Quaternius bases (ADR-0017/ADR-0018) and audited
|
||||
before use; the procedural library and the MakeHuman head stay as the tested fallback. The
|
||||
sheets below show the
|
||||
current library and will be replaced after the 12 × 6 baseline review.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
|  | **Character library.** Eight skinned variants (4.5–5.5k triangles each) with five tint slots, attach-point gear and baked maps. |
|
||||
|  | **Character library.** Twelve role-dressed variants (4.5–6.4k triangles each) on three CC0 Quaternius bases, with five tint slots, attach-point gear and baked maps. |
|
||||
|  | **Street and venue props.** Twenty library props with colliders, navigation avoidance and colour atlases. |
|
||||
|  | **Neutral baseline.** 8 characters × 6 views (front, three-quarter, profile, back, full body, crowd) used for the 0.9.4 pass and the current re-evaluation. |
|
||||
|  | **Texture refinement.** 128 → 256 px NPC maps and a 4× fabric weave, shown before/after on the same camera. |
|
||||
@@ -131,7 +136,7 @@ decisions are recorded as ADRs in [`docs/adr/`](docs/adr/).
|
||||
## Tech Stack
|
||||
|
||||
- **Engine:** Godot 4.7.2 · **Language:** GDScript
|
||||
- **NPCs:** Blender-authored skinned library (`assets/models/blender/`) with procedural fallbacks; lightweight FSMs on `NpcProfile`
|
||||
- **NPCs:** CC0 Quaternius skinned library (`assets/models/blender/`, ADR-0018) with a tested procedural fallback; lightweight FSMs on `NpcProfile`
|
||||
- **World:** procedural `WorldBuilder` + generated prop models
|
||||
- **Art:** v2 skyline/surface textures in `assets/textures/ai/game_ready/v2/picks/`; UI/scene art in `assets/images/`
|
||||
- **Asset tooling:** `asset_pipeline.py`, `model_pipeline.py`, `npc_pipeline.py`, `blender_assets.py`, `scene_concepts.py`, `screen_designs.py`, `check_screenshots.sh`
|
||||
@@ -160,6 +165,7 @@ Full setup, every run/test/smoke command, and the controls table are in
|
||||
- [`TODO.md`](TODO.md) — Priority-ordered tasks: character and local-map development, release milestones, and future work.
|
||||
- [`docs/README.md`](docs/README.md) — Documentation index, current priorities and visual previews.
|
||||
- [`docs/npc-model-sourcing.md`](docs/npc-model-sourcing.md) — Character-source evaluation, licence rules and the character-development plan.
|
||||
- [`docs/makehuman-head-review.md`](docs/makehuman-head-review.md) — Local MakeHuman head prototype, provenance and review verdict.
|
||||
- [`AGENTS.md`](AGENTS.md) — Development, commit, build and collaboration workflow.
|
||||
- [`DONE.md`](DONE.md) — Completed task sections.
|
||||
- [`MEMORY.md`](MEMORY.md) — Persistent session notes.
|
||||
@@ -189,9 +195,9 @@ v0.9.8 is the current source version: the character-development patch with the
|
||||
and save-migration/release hardening. It sits on the 0.9.6 integration
|
||||
checkpoint, 0.9.5 texture refinement, 0.9.4 NPC visual optimization, 0.9.3
|
||||
UI/accessibility polish, 0.9.2 Hollywood/Joke Shop maps and the 0.9.x engine
|
||||
baseline. The active priorities are the Blender NPC head/face/hand overhaul,
|
||||
MakeHuman body intake and the 3D model/map re-evaluation; the 0.10.0 release
|
||||
candidate follows those tracks. See
|
||||
baseline. The active work is the `0.9.9`–`0.9.12` patch line (Quaternius NPC
|
||||
base, roster, venue/asset polish, performance) followed by the `0.10.0`
|
||||
public-readiness minor. See
|
||||
[`docs/development-status.md`](docs/development-status.md) for the implemented list
|
||||
and [`TODO.md`](TODO.md) for the priority-ordered roadmap. Manual verification uses
|
||||
[`docs/release-qa.md`](docs/release-qa.md).
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
"attach": "back",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.161,
|
||||
0.15,
|
||||
0.296,
|
||||
0.212
|
||||
0.172
|
||||
],
|
||||
"min": [
|
||||
-0.161,
|
||||
-0.15,
|
||||
-0.12,
|
||||
-0.319
|
||||
]
|
||||
@@ -46,7 +46,7 @@
|
||||
],
|
||||
"path": "assets/models/blender/npc_gear/backpack.glb",
|
||||
"triangles": 380,
|
||||
"vertices": 408
|
||||
"vertices": 410
|
||||
},
|
||||
"bench": {
|
||||
"bounds_godot": {
|
||||
@@ -98,14 +98,14 @@
|
||||
"attach": "hand",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.106,
|
||||
0.376,
|
||||
0.065,
|
||||
0.06,
|
||||
0.143
|
||||
],
|
||||
"min": [
|
||||
-0.106,
|
||||
-0.065,
|
||||
-0.053,
|
||||
-0.055
|
||||
0.005
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
@@ -114,21 +114,21 @@
|
||||
"lens"
|
||||
],
|
||||
"path": "assets/models/blender/npc_gear/camera.glb",
|
||||
"triangles": 318,
|
||||
"vertices": 406
|
||||
"triangles": 312,
|
||||
"vertices": 422
|
||||
},
|
||||
"cap": {
|
||||
"attach": "head",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.168,
|
||||
0.209,
|
||||
0.258
|
||||
0.108,
|
||||
0.204,
|
||||
0.165
|
||||
],
|
||||
"min": [
|
||||
-0.168,
|
||||
0.037,
|
||||
-0.186
|
||||
-0.108,
|
||||
0.094,
|
||||
-0.119
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
@@ -264,14 +264,14 @@
|
||||
"attach": "head",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.26,
|
||||
0.245,
|
||||
0.26
|
||||
0.166,
|
||||
0.227,
|
||||
0.166
|
||||
],
|
||||
"min": [
|
||||
-0.26,
|
||||
0.035,
|
||||
-0.351
|
||||
-0.166,
|
||||
0.092,
|
||||
-0.225
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
@@ -333,14 +333,14 @@
|
||||
"attach": "head",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.184,
|
||||
0.236,
|
||||
0.123
|
||||
0.118,
|
||||
0.221,
|
||||
0.079
|
||||
],
|
||||
"min": [
|
||||
-0.184,
|
||||
-0.146,
|
||||
-0.225
|
||||
-0.118,
|
||||
-0.024,
|
||||
-0.144
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
@@ -428,16 +428,17 @@
|
||||
"vertices": 648
|
||||
},
|
||||
"npc_casual": {
|
||||
"base": "hoodie",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.321,
|
||||
1.824,
|
||||
0.189
|
||||
0.228,
|
||||
1.866,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.321,
|
||||
0,
|
||||
-0.178
|
||||
-0.228,
|
||||
-0.004,
|
||||
-0.091
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -445,11 +446,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Eyebrows",
|
||||
"Eye",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_casual.glb",
|
||||
@@ -466,20 +467,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5034,
|
||||
"vertices": 4607
|
||||
"triangles": 6206,
|
||||
"vertices": 12988
|
||||
},
|
||||
"npc_coat": {
|
||||
"base": "hoodie",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.332,
|
||||
1.824,
|
||||
0.189
|
||||
0.228,
|
||||
1.866,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.332,
|
||||
0,
|
||||
-0.18
|
||||
-0.228,
|
||||
-0.004,
|
||||
-0.091
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -487,11 +489,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Eyebrows",
|
||||
"Eye",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_coat.glb",
|
||||
@@ -507,20 +509,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 6046,
|
||||
"vertices": 5645
|
||||
"triangles": 6206,
|
||||
"vertices": 12988
|
||||
},
|
||||
"npc_crowd_a": {
|
||||
"base": "casual",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.288,
|
||||
1.825,
|
||||
0.189
|
||||
0.18,
|
||||
1.843,
|
||||
0.271
|
||||
],
|
||||
"min": [
|
||||
-0.288,
|
||||
0,
|
||||
-0.195
|
||||
-0.18,
|
||||
-0.009,
|
||||
-0.099
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -528,11 +531,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Hair_Brown",
|
||||
"Brown",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_crowd_a.glb",
|
||||
@@ -547,20 +550,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5284,
|
||||
"vertices": 4908
|
||||
"triangles": 6424,
|
||||
"vertices": 13440
|
||||
},
|
||||
"npc_crowd_b": {
|
||||
"base": "casual",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.358,
|
||||
1.809,
|
||||
0.189
|
||||
0.18,
|
||||
1.843,
|
||||
0.271
|
||||
],
|
||||
"min": [
|
||||
-0.358,
|
||||
0,
|
||||
-0.155
|
||||
-0.18,
|
||||
-0.009,
|
||||
-0.099
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -568,11 +572,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Hair_Brown",
|
||||
"Brown",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_crowd_b.glb",
|
||||
@@ -587,20 +591,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 4812,
|
||||
"vertices": 4550
|
||||
"triangles": 6424,
|
||||
"vertices": 13440
|
||||
},
|
||||
"npc_crowd_c": {
|
||||
"base": "hoodie",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.332,
|
||||
1.824,
|
||||
0.189
|
||||
0.228,
|
||||
1.866,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.332,
|
||||
0,
|
||||
-0.18
|
||||
-0.228,
|
||||
-0.004,
|
||||
-0.091
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -608,11 +613,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Eyebrows",
|
||||
"Eye",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_crowd_c.glb",
|
||||
@@ -627,20 +632,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5690,
|
||||
"vertices": 5510
|
||||
"triangles": 6206,
|
||||
"vertices": 12988
|
||||
},
|
||||
"npc_crowd_d": {
|
||||
"base": "worker",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.293,
|
||||
1.856,
|
||||
0.189
|
||||
0.217,
|
||||
1.826,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.293,
|
||||
0,
|
||||
-0.225
|
||||
-0.217,
|
||||
-0.004,
|
||||
-0.065
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -648,11 +654,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"hair",
|
||||
"rubber"
|
||||
"Moustache",
|
||||
"Eye",
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_crowd_d.glb",
|
||||
@@ -667,20 +673,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5810,
|
||||
"vertices": 5444
|
||||
"triangles": 4458,
|
||||
"vertices": 9370
|
||||
},
|
||||
"npc_dress": {
|
||||
"base": "casual",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.288,
|
||||
1.825,
|
||||
0.208
|
||||
0.18,
|
||||
1.843,
|
||||
0.271
|
||||
],
|
||||
"min": [
|
||||
-0.288,
|
||||
0,
|
||||
-0.208
|
||||
-0.18,
|
||||
-0.009,
|
||||
-0.099
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -688,11 +695,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Hair_Brown",
|
||||
"Brown",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_dress.glb",
|
||||
@@ -706,20 +713,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5540,
|
||||
"vertices": 5073
|
||||
"triangles": 6424,
|
||||
"vertices": 13440
|
||||
},
|
||||
"npc_firefighter": {
|
||||
"base": "worker",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.37,
|
||||
1.819,
|
||||
0.213
|
||||
0.217,
|
||||
1.826,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.37,
|
||||
0,
|
||||
-0.213
|
||||
-0.217,
|
||||
-0.004,
|
||||
-0.065
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -727,13 +735,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"hair",
|
||||
"hivis",
|
||||
"reflective",
|
||||
"rubber"
|
||||
"Moustache",
|
||||
"Eye",
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_firefighter.glb",
|
||||
@@ -747,20 +753,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5888,
|
||||
"vertices": 5785
|
||||
"triangles": 4458,
|
||||
"vertices": 9370
|
||||
},
|
||||
"npc_guard": {
|
||||
"base": "worker",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.361,
|
||||
1.819,
|
||||
0.189
|
||||
0.217,
|
||||
1.826,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.361,
|
||||
0,
|
||||
-0.172
|
||||
-0.217,
|
||||
-0.004,
|
||||
-0.065
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -768,15 +775,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"hair",
|
||||
"rubber",
|
||||
"leather",
|
||||
"steel",
|
||||
"patch",
|
||||
"plastic_black"
|
||||
"Moustache",
|
||||
"Eye",
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_guard.glb",
|
||||
@@ -791,20 +794,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5670,
|
||||
"vertices": 5671
|
||||
"triangles": 4458,
|
||||
"vertices": 9370
|
||||
},
|
||||
"npc_hoodie": {
|
||||
"base": "hoodie",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.326,
|
||||
1.856,
|
||||
0.189
|
||||
0.228,
|
||||
1.866,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.326,
|
||||
0,
|
||||
-0.225
|
||||
-0.228,
|
||||
-0.004,
|
||||
-0.091
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -812,11 +816,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Eyebrows",
|
||||
"Eye",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_hoodie.glb",
|
||||
@@ -832,20 +836,21 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5864,
|
||||
"vertices": 5325
|
||||
"triangles": 6206,
|
||||
"vertices": 12988
|
||||
},
|
||||
"npc_jacket": {
|
||||
"base": "casual",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.332,
|
||||
1.824,
|
||||
0.189
|
||||
0.18,
|
||||
1.843,
|
||||
0.271
|
||||
],
|
||||
"min": [
|
||||
-0.332,
|
||||
0,
|
||||
-0.178
|
||||
-0.18,
|
||||
-0.009,
|
||||
-0.099
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
@@ -853,11 +858,11 @@
|
||||
"materials": [
|
||||
"skin",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"Hair_Brown",
|
||||
"Brown",
|
||||
"hair",
|
||||
"rubber"
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_jacket.glb",
|
||||
@@ -873,34 +878,35 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5432,
|
||||
"vertices": 5225
|
||||
"triangles": 6424,
|
||||
"vertices": 13440
|
||||
},
|
||||
"npc_vest": {
|
||||
"base": "worker",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.321,
|
||||
1.819,
|
||||
0.189
|
||||
0.217,
|
||||
1.863,
|
||||
0.287
|
||||
],
|
||||
"min": [
|
||||
-0.321,
|
||||
0,
|
||||
-0.172
|
||||
-0.217,
|
||||
-0.004,
|
||||
-0.103
|
||||
]
|
||||
},
|
||||
"category": "npc",
|
||||
"layer": "vest",
|
||||
"materials": [
|
||||
"skin",
|
||||
"Worker_Yellow",
|
||||
"Worker_Vest",
|
||||
"cloth",
|
||||
"pants",
|
||||
"accessory",
|
||||
"eye_white",
|
||||
"hair",
|
||||
"rubber",
|
||||
"hivis",
|
||||
"reflective"
|
||||
"Moustache",
|
||||
"Eye",
|
||||
"pants"
|
||||
],
|
||||
"mesh_node": "npc_base",
|
||||
"path": "assets/models/blender/npc/npc_vest.glb",
|
||||
@@ -914,8 +920,8 @@
|
||||
"accessory",
|
||||
"hair"
|
||||
],
|
||||
"triangles": 5532,
|
||||
"vertices": 5195
|
||||
"triangles": 5240,
|
||||
"vertices": 10936
|
||||
},
|
||||
"palm_tree": {
|
||||
"bounds_godot": {
|
||||
@@ -1239,14 +1245,14 @@
|
||||
"attach": "head",
|
||||
"bounds_godot": {
|
||||
"max": [
|
||||
0.101,
|
||||
0.059,
|
||||
0.158
|
||||
0.065,
|
||||
0.108,
|
||||
0.101
|
||||
],
|
||||
"min": [
|
||||
-0.101,
|
||||
0.011,
|
||||
0.003
|
||||
-0.065,
|
||||
0.077,
|
||||
0.002
|
||||
]
|
||||
},
|
||||
"category": "npc_gear",
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,33 @@
|
||||
# Quaternius build sources (CC0 1.0)
|
||||
|
||||
These are the **build inputs** for the shipped NPC roster, not runtime assets.
|
||||
`blender_src/.gdignore` keeps Godot from importing this directory.
|
||||
|
||||
Source: Quaternius *Ultimate Modular Characters* and *Ultimate Modular Women*
|
||||
(<https://quaternius.com/packs/ultimatemodularcharacters.html>), licence CC0 1.0,
|
||||
attribution recorded in the repository `CREDITS.md`.
|
||||
|
||||
| File | SHA-256 | Bones | Meshes |
|
||||
| --- | --- | --- | --- |
|
||||
| `Casual.glb` | `bc2d84fba249ebaa0629cb9d133ad37234196e4b8004aa205a6397abed04e05c` | 62 | 4 skinned + Icosphere helper |
|
||||
| `Casual_Hoodie.glb` | `f5b722199f7628b24550989af4230f65d5230727df9c326fd2b3e360effe5249` | 62 | 4 skinned + Icosphere helper |
|
||||
| `Worker.glb` | `8d0f454336c2effc5c08f63a177af3a82d1a7af025ca9588b40e4c90c4703534` | 62 | 4 skinned + Icosphere helper |
|
||||
|
||||
Each file also carries 24 animation clips; the build exports **no** animation data, so
|
||||
the shipped GLBs contain the mesh, the 62-joint skin and the material slots only.
|
||||
|
||||
Reproducing the shipped characters:
|
||||
|
||||
flatpak run org.blender.Blender --background \
|
||||
--python "$PWD/tools/blender_assets.py" -- build --only npc
|
||||
|
||||
Upstream quirks the build compensates for (see `docs/npc-model-sourcing.md`):
|
||||
|
||||
1. every mesh ships a **degenerate UV map** (all loops at `0,0`) with flat-colour
|
||||
materials, so the build Smart-UV-unwraps and repacks before export;
|
||||
2. the meshes must be joined **without welding** — `remove_doubles` corrupts the
|
||||
skin weights;
|
||||
3. the rest pose is a T-pose, so the arm-down rest is baked by
|
||||
`_bake_quaternius_rest()`;
|
||||
4. the upstream material names differ per file, so each source carries a slot map
|
||||
down to the project's five tint slots.
|
||||
@@ -85,10 +85,12 @@ Readings:
|
||||
|
||||
## Baseline inventory (2026-11-04)
|
||||
|
||||
Recorded for the Blender NPC model overhaul track: the exact starting point before the local
|
||||
part pipeline is built. `npc_base.blend` is the frozen fallback; the 12 characters are assembled
|
||||
procedurally by `tools/blender_assets.py`; the MakeHuman prototypes live under `tmp/npc-research/`
|
||||
and are not the shipped path.
|
||||
Recorded for the character baseline before the Quaternius migration. `npc_base.blend` is the
|
||||
frozen fallback; the 12 characters are currently assembled procedurally by
|
||||
`tools/blender_assets.py`. The active plan is now the CC0 Quaternius base bake
|
||||
([ADR-0017](adr/0017-quaternius-npc-base.md)); this section and the MakeHuman notes below are the
|
||||
pre-migration record. The MPFB/MakeHuman development files were removed from `tmp/` after the
|
||||
2026-11-05 pivot.
|
||||
|
||||
### `assets/models/npc/npc_base.blend` (frozen fallback)
|
||||
|
||||
@@ -143,7 +145,7 @@ width scale.
|
||||
287–440 px/m via the 4× repeat; runtime tint carries the look. This is a recorded note until a
|
||||
portrait shows texture starvation.
|
||||
|
||||
### MakeHuman prototypes (not shipped; `tmp/npc-research/makehuman/`)
|
||||
### MakeHuman prototypes (historical, not shipped; research files removed from `tmp/`)
|
||||
|
||||
| File | Verts / tris | Bones | Slots | Note |
|
||||
| --- | ---: | ---: | --- | --- |
|
||||
@@ -193,7 +195,7 @@ capture). Pass conditions:
|
||||
|
||||
### Contract (both, every prototype)
|
||||
|
||||
- 16,000 triangles / 16,000 glTF vertices per character; gear stays at 1,500.
|
||||
- 24,000 triangles / 24,000 glTF vertices per character; gear stays at 1,500.
|
||||
- Optional extra bones are allowed; the required 13 core bones and their rest positions must not
|
||||
move; the `npc_base` mesh node name and the five tint slots stay unchanged.
|
||||
|
||||
@@ -242,7 +244,7 @@ nose/lip/chin relief cleanly.
|
||||
|
||||
The judge then compared the displacement head against the **decimated MakeHuman head**
|
||||
(extracted from `makehuman_retargeted.glb`, 2,009 verts / 1,573 tris, saved as
|
||||
`tmp/npc-parts/makehuman_head.glb`) and returned a decisive result: **ship the MakeHuman head** —
|
||||
the local `makehuman_head.glb`) and returned a decisive result: **ship the MakeHuman head** —
|
||||
it keeps the nose/chin/jaw landmarks at a fraction of the cost, while the procedural head's
|
||||
"clean ellipse outline instantly screams procedural sphere".
|
||||
|
||||
@@ -257,6 +259,16 @@ at the MakeHuman landmarks. `build_character(name, head="mh")` assembles it; the
|
||||
`inspect` (14 bones, skinned, no issues). The judge reads the face ~3.5/5; remaining polish is
|
||||
welding the added eye/brow pieces into the head shell and a skull-conforming hair cap.
|
||||
|
||||
**Standalone head re-review (2026-11-04):** the separate
|
||||
`assets/models/blender_src/makehuman_head.glb` prototype (12,641 tris / 7,690 split verts,
|
||||
with a skin eyelid mask, recessed globes, flat iris/pupil discs and surface-anchored brows)
|
||||
was **accepted by the owner** as a basic human head. The automated vision gate still returns
|
||||
REJECT on the eye/lid region (globe depth, lid overlap, iris at three-quarter) as deferred
|
||||
polish. The assembled `npc_casual --head mh` now adds a skull-conforming hair cap, a hidden
|
||||
scalp trim and a narrowed neck/shoulder body: 14,753 tris / 12,823 seam-split verts /
|
||||
14 bones / 9 materials, `inspect` clean. All 12 variants count 14,693–15,987 tris. See
|
||||
[`makehuman-head-review.md`](makehuman-head-review.md).
|
||||
|
||||
### Head subdivision comparison (2026-11-04)
|
||||
|
||||
Three `head_proto` variants at UV-sphere segments 24/48/64 were rendered at 1.85 m and 1.5 m in
|
||||
@@ -485,6 +497,9 @@ and the full carded-cluster foliage pass is the one remaining owner-gated P1. No
|
||||
|
||||
## Next actions
|
||||
|
||||
- Review the assembled [`makehuman-head-review.md`](makehuman-head-review.md) prototype (hair cap and full-body silhouette) before rebuilding the 12 shipped character GLBs.
|
||||
|
||||
|
||||
Tracked in [`TODO.md`](../TODO.md) `3D model re-evaluation`: the remaining `A. Baseline…` item is
|
||||
this owner review; `B`/`C`/`D` carry the per-family work, and `E` adds the pose/weight,
|
||||
collider-coverage and scorecard-coverage gates this document found missing.
|
||||
@@ -4,8 +4,8 @@ This folder holds the project documentation, visual references and generated QA
|
||||
|
||||
## Current priorities
|
||||
|
||||
1. [Blender NPC model overhaul](../TODO.md#blender-npc-model-overhaul-top-priority) — part-based head/face/hands pipeline and 16,000-budget contract.
|
||||
2. [MakeHuman and 3D model/map improvements](../TODO.md#makehuman-character-pipeline) — body intake, asset re-evaluation and local map reconstruction.
|
||||
1. [Quaternius NPC base migration](../TODO.md#quaternius-npc-base-migration-top-priority) — CC0 Quaternius mesh/animation source baked to the 24,000-budget runtime contract.
|
||||
2. [3D model and local map improvements](../TODO.md#3d-model-and-local-map-re-evaluation) — asset re-evaluation and local map reconstruction.
|
||||
3. [0.10.0 release candidate](../TODO.md#milestone-0100--3d-model-and-map-improvements-minor) — versioned release hardening after the unversioned development tracks.
|
||||
|
||||
## Start here
|
||||
@@ -13,6 +13,7 @@ This folder holds the project documentation, visual references and generated QA
|
||||
- [`../PLAN.md`](../PLAN.md) — product, design intent and development priorities
|
||||
- [`../TODO.md`](../TODO.md) — priority-ordered active tasks and roadmap
|
||||
- [`npc-model-sourcing.md`](npc-model-sourcing.md) — character-source evaluation, licence rules and the character-development plan
|
||||
- [`makehuman-head-review.md`](makehuman-head-review.md) — retained MakeHuman fallback head, provenance and review verdict
|
||||
- [`architecture.md`](architecture.md) — arc42 system architecture
|
||||
- [`getting-started.md`](getting-started.md) — setup, run/test commands and controls
|
||||
- [`asset-tooling.md`](asset-tooling.md) — image/model pipelines and screenshot gates
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
# ADR-0015: NPC model variety sourcing
|
||||
|
||||
- **Status:** Accepted (planning)
|
||||
- **Status:** Superseded by [ADR-0017](0017-quaternius-npc-base.md) (2026-11-05)
|
||||
- **Date:** 2026-11-03
|
||||
|
||||
## Superseded
|
||||
|
||||
ADR-0017 replaces the generator-first decision below: Quaternius is now the
|
||||
primary NPC base. The evaluation, licence rules and consequences remain as the
|
||||
record of the 2026-11-03 decision.
|
||||
|
||||
## Context
|
||||
|
||||
The library has eight skinned characters, and role/seed selection reuses them
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# ADR-0016: 24,000-triangle character budget
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-11-05
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0015 raised the character contract to 16,000 triangles / 16,000 glTF
|
||||
vertices for the MakeHuman head, hands and crowd variety. The local MPFB
|
||||
full-body prototype now uses that head, project hands and shoes, and a fused
|
||||
neck: it measures 19,747 triangles before any clothing variation. The 16,000 cap
|
||||
leaves no room for the firefighter and hoodie gear, and it forces the body and
|
||||
neck into a low decimation that produces visible facets.
|
||||
|
||||
## Decision
|
||||
|
||||
Raise the character contract to **24,000 triangles / 24,000 glTF vertices** per
|
||||
character (`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS`, `npc_pipeline.MAX_TRIANGLES`/
|
||||
`MAX_VERTICES`, and the `adapt` defaults). The 13 core bones, five tint slots,
|
||||
front orientation, grounding and single `npc_base` skinned mesh are unchanged.
|
||||
Gear stays at 1,500 triangles and props at 4,000.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The MPFB body, accepted head and neck fit with headroom for the 12 clothing
|
||||
variants and future jacket/hoodie detail.
|
||||
- The 24-NPC and Web frame-time budgets must be re-measured before shipping any
|
||||
variant built at the new cap; the extra geometry is not free.
|
||||
- The procedural generator and the low-poly `npc_base` fallback remain valid;
|
||||
they are not required to use the new headroom.
|
||||
- MPFB stays a local development tool. Production must bake a committed source
|
||||
GLB; CI and the build must not depend on MPFB.
|
||||
- Historical reviews and measurements that state 16,000 remain accurate for the
|
||||
date they were recorded; current contract documents state 24,000.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Keep 16,000 and decimate the MPFB body further:** rejected because the neck
|
||||
and shoulder already show facets and the clothing variants would exceed the
|
||||
cap.
|
||||
- **Separate caps for the MPFB prototype and the shipped characters:** rejected
|
||||
because it creates a second contract and makes the rollout ambiguous.
|
||||
@@ -0,0 +1,69 @@
|
||||
# ADR-0017: Quaternius NPC base
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-11-05
|
||||
- **Supersedes:** ADR-0015 (generator-first character sourcing)
|
||||
|
||||
## Context
|
||||
|
||||
The current NPC library is built by the project-owned procedural generator. It
|
||||
keeps the contract and ships, but the body, neck, hands and shoes need a
|
||||
bespoke part pipeline to read as human. The MakeHuman head and MPFB body
|
||||
prototypes improved detail but added a second authoring path, a 19,747-triangle
|
||||
prototype and unresolved neck/shoulder seams.
|
||||
|
||||
The CC0 **Quaternius Ultimate Modular Characters** pack already provides a
|
||||
coherent low-poly human silhouette, 21 modular outfits and 24 animation clips.
|
||||
The researched GLBs (`Casual`, `Casual_Hoodie`, `Worker`) measure 4 meshes, 62
|
||||
joints and 24 clips each. The owner directed that Quaternius become the NPC
|
||||
base. The full evaluation is in [`docs/npc-model-sourcing.md`](../npc-model-sourcing.md).
|
||||
|
||||
## Decision
|
||||
|
||||
1. **Quaternius is the primary NPC mesh and animation source.** Keep the
|
||||
licence and SHA-256 provenance for the three researched GLBs.
|
||||
2. **Stage 1 keeps the runtime contract.** Bake a canonical Quaternius base onto
|
||||
the existing 13-core contract, five tint slots, 24,000/24,000 budget,
|
||||
grounded −Y front and one `npc_base` skinned mesh. Retarget Idle and Walk;
|
||||
discard the combat/gun/sword clips. `NpcAvatar`, gear attach points and the
|
||||
pose composition remain the runtime interface.
|
||||
3. **The 62-joint rig is an authoring source, not a runtime dependency.** It is
|
||||
not shipped and Quaternius is not loaded by CI, build or runtime.
|
||||
4. **Stage 2 needs its own ADR.** If the proportion warp fails the silhouette,
|
||||
deformation or gear gate, stop and decide whether to adopt the 62-joint rig
|
||||
at runtime. Do not ship a hybrid rig or a second gear offset system.
|
||||
5. **Close the MakeHuman/MPFB body work.** The accepted MakeHuman head and the
|
||||
procedural `npc_base` stay as tested fallbacks until the Quaternius rollout
|
||||
ships. No new MPFB development.
|
||||
6. **Commit baked assets only.** The release consumes committed GLBs, maps and
|
||||
the manifest; Quaternius and Blender remain development tools.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- A human base, modular outfits and a coherent animation set are already
|
||||
authored and CC0.
|
||||
- The bespoke neck, hand and shoe pipelines are no longer on the critical path.
|
||||
- The runtime contract and gear stay stable during the mesh migration.
|
||||
|
||||
### Negative
|
||||
|
||||
- A 62-joint T-pose does not share the arm-down 13-core rest pose or
|
||||
proportions; the proportion warp is the main risk.
|
||||
- Quaternius' animation quality is only available after retargeting, unless
|
||||
Stage 2 is accepted.
|
||||
- The migration touches all 12 variants, their AO maps, manifest entries and
|
||||
baseline captures at once.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Keep the procedural generator as the primary path.** Rejected by the owner:
|
||||
the human silhouette remains the weak point and the bespoke part pipeline is
|
||||
larger than the Quaternius bake.
|
||||
- **Adopt the 62-joint rig immediately.** Deferred to Stage 2 because it changes
|
||||
`NpcAvatar`, pose composition and gear offsets at the same time as the mesh.
|
||||
- **Continue the MPFB body.** Rejected: it needs a separate head/neck/hand
|
||||
pipeline and still has unresolved surface seams.
|
||||
- **Use a service character generator (Mixamo, Ready Player Me).** Rejected:
|
||||
redistribution and runtime terms are not a CC0 basis.
|
||||
@@ -0,0 +1,96 @@
|
||||
# ADR-0018: Quaternius Stage 2 — adopt source proportions at runtime
|
||||
|
||||
- **Status:** Accepted (2026-11-05, owner chose option 2)
|
||||
- **Date:** 2026-11-05
|
||||
- **Supersedes:** ADR-0017 decision 2 (Stage 1 keeps the 13-core contract)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0017 made Quaternius the NPC base and staged the migration: Stage 1 bakes a
|
||||
proportion-warped mesh onto the existing 13-core contract; Stage 2 adopts the
|
||||
62-joint rig at runtime only if the warp fails.
|
||||
|
||||
The Stage 1 proportion warp was executed and measured (`docs/npc-model-sourcing.md`
|
||||
§Rest-pose and proportion-warp measurement). The arm-down rest pose bakes cleanly, but the
|
||||
warp to the contract proportions does not:
|
||||
|
||||
| Joint | Quaternius | Contract | Delta |
|
||||
| --- | --- | --- | --- |
|
||||
| Hips height | 0.869 | 0.62 | −0.249 |
|
||||
| Shoulder x | 0.148 | 0.27 | +0.122 |
|
||||
| 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 distorted: paddle/claw hands, forearms lost into the
|
||||
torso, stretched shins, "long legs + short torso". The arm either stays short (a shortened arm
|
||||
against the contract hand) or stretches 1.63× (a distorted deltoid + hand). Both are the gate's
|
||||
rejection conditions.
|
||||
|
||||
## Decision
|
||||
|
||||
Stop the Stage 1 rollout and **adopt the Quaternius 62-joint rig at runtime** (owner chose
|
||||
option 2). The mesh is not proportion-warped: it keeps its source LOD (3,199 welded verts /
|
||||
6,206 tris) and only the arm-down rest pose is baked.
|
||||
|
||||
- `NpcAvatar` maps the Quaternius joint names instead of the 13-core names.
|
||||
- Gear attach points are re-derived from the Quaternius skeleton (hip ≈0.87, shoulder ≈0.148/1.39,
|
||||
wrist ≈0.99, knee ≈0.52).
|
||||
- The Quaternius Idle and Walk clips are used directly — they are authored for the 62-joint rig,
|
||||
so no retargeting pass is needed. Combat/gun/sword clips stay unused.
|
||||
|
||||
Option 1 (keep the 13-core names with Quaternius rest positions) is rejected in favour of the
|
||||
full source rig.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- The Quaternius silhouette, deltoid, hand and leg proportions are preserved — no paddle hands,
|
||||
no shortened arm, no torso/leg distortion.
|
||||
- The mesh stays at source LOD (far under the 24,000 cap), so no decimation is needed.
|
||||
|
||||
### Negative
|
||||
|
||||
- The runtime contract numbers (hip 0.62, shoulder 0.27, hand 0.78) change; every gear attach
|
||||
point, `npc_proportions` guide, fit measurement and pose-composition constant must be
|
||||
re-derived and re-tested.
|
||||
- `NpcAvatar`'s bone mapping and pose composition are rewritten for the 62-joint rig, a larger
|
||||
change than the mesh swap.
|
||||
- The 12-variant rollout and the 12×6 baseline are still blocked until the new rest positions
|
||||
and gear fit are owner-approved.
|
||||
|
||||
## Alternatives
|
||||
|
||||
- **Ship the warped mesh.** Rejected by the gate: the 1.63× arm / 0.74× leg warp visibly
|
||||
distorts and the owner directed keeping the Quaternius as close to source as possible.
|
||||
- **Keep the old contract and accept a short arm.** Rejected for the same reason: the hand ends
|
||||
at ≈0.99 instead of 0.78, so handheld gear floats unless attach points move — which is option 1.
|
||||
- **Abandon Quaternius and keep the procedural generator.** Rejected in ADR-0017.
|
||||
|
||||
## Open question
|
||||
|
||||
Resolved: option 2 (62-joint runtime) is the owner's choice. The first implementation step is
|
||||
the `NpcAvatar` bone-map + gear-attach re-derivation, then the slot/AO bake and the 12-variant
|
||||
rollout.
|
||||
|
||||
## Follow-up decision: full roster swap
|
||||
|
||||
- **Date:** 2026-11-05 (owner, at the `0.9.9` face/silhouette gate)
|
||||
|
||||
The owner chose **option (a) full swap** from
|
||||
[`docs/screens/npc-face-gate/`](../../screens/npc-face-gate/README.md): every roster character
|
||||
moves onto the Quaternius base, and the procedural generator plus the MakeHuman head retire to
|
||||
the tested fallback rather than staying in the shipped roster.
|
||||
|
||||
Scope consequences:
|
||||
|
||||
- The shipped roster keeps its twelve public character names and `CHARACTER_POOLS` mapping; the
|
||||
geometry behind them comes from the three CC0 full-body sources already researched
|
||||
(`Casual`, `Casual_Hoodie`, `Worker`), aliased per role.
|
||||
- Variety now comes from the five tint slots, the gear library and the per-role outfit rules
|
||||
instead of twelve distinct meshes. Roster silhouettes are therefore *less* varied than the
|
||||
procedural library until the modular part library (hair, outfits) is added — tracked as a
|
||||
`0.9.10` task, not hidden.
|
||||
- The procedural library, `_build_body()` and the MakeHuman head stay in the tree and tested as
|
||||
the instant fallback; no MPFB or Stage-1 proportion-warp work reopens.
|
||||
@@ -19,3 +19,6 @@ Each ADR records the context, decision, consequences and alternatives for a dura
|
||||
- [0013-blender-authored-npc-characters](0013-blender-authored-npc-characters.md)
|
||||
- [0014-runtime-image-root](0014-runtime-image-root.md)
|
||||
- [0015-npc-model-variety-sourcing](0015-npc-model-variety-sourcing.md)
|
||||
- [0016-npc-character-budget-24k](0016-npc-character-budget-24k.md)
|
||||
- [0017-quaternius-npc-base](0017-quaternius-npc-base.md)
|
||||
- [0018-quaternius-stage2-runtime-rig](0018-quaternius-stage2-runtime-rig.md)
|
||||
@@ -81,7 +81,7 @@ The player interacts with NPCs, venue entrances, access artifacts, equipment, st
|
||||
- `WorldBuilder` assembles the procedural venue from a per-venue `VENUE_SPECS` table (facade, windows, stairs, street, nav bounds, guard spawn, feature flags and prop placements), prefers the Blender prop library in `assets/models/blender/props/`, and falls back to `assets/models/generated/` or primitives. `GameManager` rebuilds only the `World` container when the venue changes, so the guard/UI and their state survive. It also wires the phase-driven library emissives and street dressing.
|
||||
- `src/game/ui/art.gd` loads runtime UI/scene art from `assets/images/ui/` and `assets/images/scenes/`, with null fallbacks so panels keep their procedural layouts.
|
||||
- `tools/model_pipeline.py` builds and audits the street-prop `.glb` models and packs their colour atlases.
|
||||
- `tools/npc_pipeline.py` builds and audits the skinned NPC base model (`assets/models/generated/npc_base.glb`) and its per-slot material maps, with an optional AI fabric override (ADR-0013).
|
||||
- `tools/npc_pipeline.py` builds and audits the skinned NPC base model (`assets/models/generated/npc_base.glb`) and its per-slot material maps, with an optional AI fabric override (ADR-0013). The active character migration bakes the CC0 Quaternius base onto the same contract (ADR-0017); the procedural base remains the fallback.
|
||||
- `tools/blender_assets.py` builds, renders and audits the Blender asset library (`assets/models/blender/{npc,npc_gear,props}/` + `manifest.json`): NPC character variants that are drop-in for `npc_base`, attach-point gear, and street/venue props. `NpcAvatar` selects variants/gear by role/seed and `WorldBuilder` loads the prop replacements and new street dressing, with procedural fallbacks (see `docs/blender-assets.md`).
|
||||
- `tools/scene_concepts.py` generates realism-pass references (`docs/screens/concepts/`), skyline backdrops and surface tiles (raw `assets/textures/ai/v2/raw/` → candidates `assets/textures/ai/game_ready/v2/`), promotes the validated ones to the tracked `v2/picks/` that `SurfaceMaterials`/`WorldBuilder` preload, audits them, and scores `src/test/capture_realism.gd` A/B captures (six stages, runtime texture swaps) against the concepts (`docs/screens/realism/<run>/`).
|
||||
- `tools/asset_pipeline.py` generates and audits role-aware textures; `tools/screen_designs.py` generates screen references and judges captures; `tools/check_screenshots.sh` renders the staged gallery and runs the compare gate.
|
||||
@@ -139,7 +139,7 @@ The runtime NPC contract is intentionally narrow. It is enforced by
|
||||
| Core bones | `Head`, `HipL/R`, `KneeL/R`, `ShoulderL/R`, `ElbowL/R`, `HandL/R`, `Spine`, `Neck` at the frozen rest positions. Optional extra bones (fingers, face, toes) are allowed if they do not move the core rest positions or break `NpcAvatar` proxies. |
|
||||
| Tint slots | `skin`, `cloth`, `pants`, `accessory`, `hair`; fixed detail materials keep their authored colours. |
|
||||
| Orientation | Blender −Y / Godot +Z front, feet grounded near `y = 0`, metre scale. |
|
||||
| Budget | 16,000 triangles / 16,000 glTF vertices per character; 1,500 per gear piece; 4,000 per prop. The low-poly `npc_base` remains a fallback, not a second contract. |
|
||||
| Budget | 24,000 triangles / 24,000 glTF vertices per character; 1,500 per gear piece; 4,000 per prop. The low-poly `npc_base` remains a fallback, not a second contract. |
|
||||
| Textures | 256² albedo + normal + packed ORM per slot; cloth/pants use the 4× `NPC_FABRIC_UV_SCALE` and skip per-character baked AO, while skin/hair/gear keep AO. |
|
||||
| Gear attach | Authored in attach-point space: head `Rig/Head` (0, 1.62, 0), back (0, 1.10, 0), chest (0, 1.30, 0), hand (±0.27, 0.78, 0.02); grip 5 cm below the origin. `GEAR_OFFSETS` and `HAND_ITEM_FORWARD` are runtime fit corrections, not authored-geometry changes. |
|
||||
| Fallback chain | Library variant → `npc_base.glb` → procedural `NpcAvatar` mesh. Committed GLBs and maps must keep CI offline and Blender-free. |
|
||||
@@ -206,19 +206,19 @@ transient input state.
|
||||
|
||||
### Character development tooling
|
||||
|
||||
Characters have two authored sources and one procedural fallback. The eight-variant
|
||||
Blender library (`assets/models/blender/npc/`) is the normal presentation path; the
|
||||
older skinned `npc_base` (`assets/models/generated/npc_base.glb`) remains a frozen
|
||||
contract and fallback; `NpcAvatar`'s procedural rig is the offline last resort.
|
||||
Four crowd-variety prototypes have been built and audited locally but are not yet in
|
||||
the shipped catalogue; generator-first expansion is the active character track
|
||||
(ADR-0015). Blender is a developer tool only — runtime, tests and CI consume committed
|
||||
GLBs and maps.
|
||||
Characters have one active migration, two committed fallbacks and one procedural last
|
||||
resort. The Quaternius base migration (ADR-0017) bakes the CC0 Quaternius mesh and
|
||||
Idle/Walk clips onto the existing 13-core contract and produces the next 12-variant
|
||||
library. Until that ships, the 12-variant Blender library
|
||||
(`assets/models/blender/npc/`) is the normal presentation path, the older skinned
|
||||
`npc_base` (`assets/models/generated/npc_base.glb`) is the frozen contract fallback,
|
||||
and `NpcAvatar`'s procedural rig is the offline last resort. Blender and Quaternius are
|
||||
developer tools only — runtime, tests and CI consume committed GLBs and maps.
|
||||
|
||||
| Tool | Role | Committed artifact | Verification gate |
|
||||
| --- | --- | --- | --- |
|
||||
| `tools/npc_pipeline.py` | Builds the skinned base avatar, five-slot material maps, palette and AI-fabric preparation (`prepare`/`palette`) | `npc_base.glb`, `assets/textures/npc/*_{albedo,normal,orm}.png`, `palette.json` | `audit_glb` against the 16,000-triangle / 16,000-vertex base contract (13 core bones required; extras allowed) |
|
||||
| `tools/blender_assets.py` | Builds/renders the library: 8 characters, 18 gear pieces, 20 props, plus `manifest.json`; stdlib `audit`/`fit`/`sheet`/`list` | `assets/models/blender/{npc,npc_gear,props}/*.glb` | Character cap 16,000 tris / 16,000 verts, 1,500 per gear, 4,000 per prop; grounding, UV, degenerate-triangle, attach-fit and manifest coverage checks |
|
||||
| `tools/npc_pipeline.py` | Builds the skinned base avatar, five-slot material maps, palette and AI-fabric preparation (`prepare`/`palette`) | `npc_base.glb`, `assets/textures/npc/*_{albedo,normal,orm}.png`, `palette.json` | `audit_glb` against the 24,000-triangle / 24,000-vertex base contract (13 core bones required; extras allowed) |
|
||||
| `tools/blender_assets.py` | Builds/renders the library: 8 characters, 18 gear pieces, 20 props, plus `manifest.json`; stdlib `audit`/`fit`/`sheet`/`list` | `assets/models/blender/{npc,npc_gear,props}/*.glb` | Character cap 24,000 tris / 24,000 verts, 1,500 per gear, 4,000 per prop; grounding, UV, degenerate-triangle, attach-fit and manifest coverage checks |
|
||||
| `tools/bake_ao.py` | Bakes per-asset AO maps | `<asset>_ao.png` (256²) | Wired through `Materials.apply_ao()`/`SurfaceMaterials`; cloth/pants opt out (see below) |
|
||||
| `tools/npc_proportions.py` | Measures exported GLB rest pose (height, head, shoulders, hips, torso, limbs, feet) against the stylized guide | report / JSON in `docs/npc-visual-review.md#proportion-audit` | All eight variants must stay inside the guide after a body change |
|
||||
| `tools/npc_baseline.py` | Assembles neutral-baseline sheets and before/after comparisons | `docs/blender_previews/sheet_npc_baseline*.jpg` | Human review against `docs/npc-visual-review.md` |
|
||||
@@ -388,6 +388,7 @@ Architecture decisions are recorded as ADRs:
|
||||
- [ADR-0008: Cone focus and focus circle](adr/0008-cone-focus-and-head-ring.md)
|
||||
- [ADR-0009: Radial action menu](adr/0009-radial-action-menu.md)
|
||||
- [ADR-0010: Procedural NPC presentation](adr/0010-procedural-npc-presentation.md)
|
||||
- [ADR-0017: Quaternius NPC base](adr/0017-quaternius-npc-base.md)
|
||||
- [ADR-0011: Aim-free selection and precision walk](adr/0011-aim-free-selection-and-precision-walk.md)
|
||||
- [ADR-0012: Surface materials and generated props](adr/0012-surface-materials-and-generated-props.md)
|
||||
- [ADR-0013: Blender-authored NPC characters and baked materials](adr/0013-blender-authored-npc-characters.md)
|
||||
|
||||
@@ -82,6 +82,8 @@ tonemapping, glow, fog, colour grading) that the phase lighting applies.
|
||||
|
||||
NPCs are moving from the procedural capsule rig to a skinned, UV-mapped base
|
||||
character authored in Blender ([ADR-0013](adr/0013-blender-authored-npc-characters.md)).
|
||||
The active base is the CC0 Quaternius modular character, baked to the runtime contract
|
||||
([ADR-0017](adr/0017-quaternius-npc-base.md)).
|
||||
`tools/npc_pipeline.py` builds `assets/models/generated/npc_base.glb` plus the
|
||||
per-slot material maps. `NpcAvatar` instantiates that skinned model, tints its
|
||||
five material slots from the seed-driven outfit, and drives the bones from the
|
||||
@@ -129,10 +131,10 @@ with colliders/avoidance and drives their emissive parts from the day/night
|
||||
cycle. The single `npc_base.glb` and primitive props remain offline fallbacks.
|
||||
Contract and integration notes: [`blender-assets.md`](blender-assets.md).
|
||||
|
||||
Sourcing and expansion research (no external model is shipped yet):
|
||||
[`npc-model-sourcing.md`](npc-model-sourcing.md) compares Quaternius, Kenney, Khronos and
|
||||
MakeHuman sources against the character contract and records the 2026-11-03 direction
|
||||
(procedural generator first, Quaternius reference, MakeHuman deferred).
|
||||
Sourcing and migration plan: [`npc-model-sourcing.md`](npc-model-sourcing.md) records the
|
||||
2026-11-05 direction (ADR-0017). Quaternius Ultimate Modular (CC0) is the primary NPC mesh
|
||||
and animation source, baked to the existing 13-core contract; the procedural generator is
|
||||
the fallback. Kenney/Khronos/MakeHuman remain evaluated references, not active bases.
|
||||
|
||||
| Characters | Props |
|
||||
| --- | --- |
|
||||
@@ -140,11 +142,13 @@ MakeHuman sources against the character contract and records the 2026-11-03 dire
|
||||
|
||||
```bash
|
||||
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- build
|
||||
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- build --head mh
|
||||
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- render
|
||||
python3 tools/blender_assets.py sheet # docs/blender_previews/sheet_*.jpg
|
||||
python3 tools/blender_assets.py audit # no Blender needed
|
||||
python3 tools/blender_assets.py fit # gear-vs-body fit report
|
||||
python3 tools/blender_assets.py texels # per-slot texel density (px/m) of the characters
|
||||
# `build --head mh` selects the retained MakeHuman fallback head; the default is still `face`.
|
||||
godot --headless --path . --import
|
||||
```
|
||||
|
||||
@@ -186,9 +190,9 @@ Caveats:
|
||||
|
||||
### Intake adaptation (`adapt`)
|
||||
|
||||
`adapt` is the mechanical first half of an external-mesh intake (MakeHuman,
|
||||
MPFB, downloaded GLB): import, apply transforms, ground the feet, weld seam
|
||||
splits, decimate to the character budget, and export a GLB. It does **not** map
|
||||
`adapt` is the mechanical first half of an external-mesh intake (Quaternius,
|
||||
MakeHuman fallback, downloaded GLB): import, apply transforms, ground the feet,
|
||||
weld seam splits, decimate to the character budget, and export a GLB. It does **not** map
|
||||
the five tint slots or rebuild the 13-core-bone `npc_base` armature (optional extra bones are allowed) — those stay a
|
||||
manual `retopology`/`rigging` step with a recorded second-rig contract if the
|
||||
source rig cannot be retargeted:
|
||||
@@ -196,14 +200,14 @@ source rig cannot be retargeted:
|
||||
```bash
|
||||
flatpak run org.blender.Blender --background \
|
||||
--python "$PWD/tools/blender_assets.py" -- adapt \
|
||||
--file "$PWD/tmp/npc-research/makehuman_base.glb" \
|
||||
--out "$PWD/tmp/npc-research/makehuman_adapted.glb" \
|
||||
--target-tris 16000
|
||||
# prints: adapted makehuman_adapted.glb: 36972 -> 16000 tris (bones=N, meshes=1)
|
||||
--file "$PWD/tmp/npc-research/quaternius/Casual.glb" \
|
||||
--out "$PWD/tmp/npc-research/quaternius_adapted.glb" \
|
||||
--target-tris 24000
|
||||
# prints: adapted quaternius_adapted.glb: <source> -> 24000 tris (bones=N, meshes=1)
|
||||
```
|
||||
|
||||
Run `inspect` on the adapted GLB before and after, and keep the procedural
|
||||
generator as the fallback until a MakeHuman variant passes `audit`/`fit` and the
|
||||
generator as the fallback until a Quaternius variant passes `audit`/`fit` and the
|
||||
24-NPC budget.
|
||||
- Numbers are Blender-side measurements. Triangles match `audit` on project assets; the seam-split
|
||||
vertex count can differ by ±1 because the importer rounds normals. `audit` stays the single final
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
Architecture context: the asset contracts and fallback path are summarized in
|
||||
[`architecture.md`](architecture.md#content-and-asset-pipeline-architecture).
|
||||
|
||||
Status: built, audited, and integrated (2026-11-01). `NpcAvatar` selects the
|
||||
Status: built, audited, and integrated (2026-11-01). The active character migration
|
||||
replaces the procedural body with a baked CC0 Quaternius base
|
||||
([ADR-0017](adr/0017-quaternius-npc-base.md)); the current library remains the shipped
|
||||
fallback until the Quaternius 12-variant baseline passes review. `NpcAvatar` selects the
|
||||
character variants and attaches the gear; `WorldBuilder` loads the prop library
|
||||
with colliders and phase-driven emissives. The 0.9.4 neutral baseline and its
|
||||
acceptance criteria are in [`npc-visual-review.md`](npc-visual-review.md);
|
||||
@@ -63,7 +66,7 @@ flatpak run org.blender.Blender --background \
|
||||
flatpak run org.blender.Blender --background \
|
||||
--python "$PWD/tools/npc_parts.py" -- extract --part hand_l --variant npc_casual \
|
||||
--out "$PWD/tmp/npc-parts/npc_casual/hand_l.glb"
|
||||
# Assemble a variant (currently rebuilds the procedural character)
|
||||
# Assemble a variant (current fallback path; Quaternius migration in progress)
|
||||
flatpak run org.blender.Blender --background \
|
||||
--python "$PWD/tools/npc_parts.py" -- assemble --variant npc_casual \
|
||||
--out "$PWD/tmp/npc-parts/npc_casual/assembled.glb"
|
||||
@@ -139,10 +142,22 @@ render about 2.5 min.
|
||||
|
||||
## Characters
|
||||
|
||||
All library variants share the [character and gear asset contract](architecture.md#character-and-gear-asset-contract):
|
||||
`npc_base` mesh node, required 13 core bones (optional extras allowed), five
|
||||
tint slots, +Z visual front, 16,000/16,000 character budget and the same 256²
|
||||
slot-map/AO rules. Role selection and look notes:
|
||||
All library variants share the
|
||||
[character and gear asset contract](architecture.md#character-and-gear-asset-contract):
|
||||
`npc_base` mesh node, five tint slots, +Z visual front, 24,000/24,000 character budget and the
|
||||
same 256² slot-map/AO rules. The shipped roster is on the 62-joint Quaternius skeleton
|
||||
(ADR-0018); the procedural fallback built by `build --source procedural` keeps the 13 core bones.
|
||||
Both are accepted by `audit`, `inspect`, the runtime bone map and the tests.
|
||||
|
||||
The three Quaternius sources (`casual`, `hoodie`, `worker`) live in
|
||||
`assets/models/blender_src/quaternius/` as build inputs. `build` (default
|
||||
`--source quaternius`) does a two-pass bake per character: a cached T-pose round trip that joins
|
||||
without welding, maps the upstream materials onto the five slots and unwraps the degenerate
|
||||
source UVs, then an arm-down rest bake exported with `export_apply=False`. The per-character slot
|
||||
map, triangle counts and measurements are in
|
||||
[`npc-model-sourcing.md#shipped-roster-099-owner-chose-the-full-swap`](npc-model-sourcing.md#shipped-roster-099-owner-chose-the-full-swap).
|
||||
|
||||
Role selection and look notes (tint + gear now carry the role, not the mesh):
|
||||
|
||||
| Character | `layer_for()` / look | Suggested roles | Preview gear |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -161,6 +176,11 @@ colour texture. The current `_apply_outfit_materials()` sets
|
||||
`albedo_color = WHITE` on any slot it does not recognise, and these keep their
|
||||
colour through that. When integrating, tint only the five contract slots.
|
||||
|
||||
**Head gear fit.** Head gear is authored for the shipped Quaternius head:
|
||||
`HEAD_GEAR_SCALE = 0.64` (0.201 m base head against the 0.315 m procedural one) and
|
||||
`HEAD_GEAR_LIFT = 0.07` (its centre sits higher). The procedural fallback therefore wears slightly
|
||||
undersized caps — cosmetic and fallback-only.
|
||||
|
||||
**Runtime tint notes (implemented).** Only the five contract slots are tinted;
|
||||
fixed detail materials keep their authored colour. On `npc_vest` the vest is the
|
||||
fixed `hivis` material and `cloth` is the undershirt, so the yellow override is
|
||||
@@ -200,7 +220,7 @@ carry baked AO and the shared neutral material maps described above.
|
||||
|
||||
- `python3 tools/blender_assets.py audit`: container, `POSITION`/`NORMAL`/
|
||||
`TEXCOORD_0`, UVs in [0, 1], no zero-area triangles (Godot warns about them
|
||||
during LOD generation), budgets (characters 16000/16000, gear 1500, props 4000
|
||||
during LOD generation), budgets (characters 24000/24000, gear 1500, props 4000
|
||||
triangles), grounding, and an editable `.blend`. For characters it also
|
||||
checks skinning, bones, slots, the `npc_base` mesh node and the height.
|
||||
- `tools/tests/test_blender_assets.py`: catalogue and contract consistency with
|
||||
|
||||
@@ -9,6 +9,7 @@ capture harnesses.
|
||||
| --- | --- |
|
||||
| [](sheet_npc.jpg) <br> `sheet_npc.jpg` | Eight skinned character variants (casual, jacket, coat, hi-vis vest, dress, hoodie, firefighter, guard) with their gear attach points. |
|
||||
| [](sheet_npc_baseline.jpg) <br> `sheet_npc_baseline.jpg` | Neutral studio baseline: 8 characters x front, three-quarter, profile, back, full body and crowd views. |
|
||||
| [](sheet_makehuman_head.jpg) <br> `sheet_makehuman_head.jpg` | MakeHuman head: front, three-quarter and profile review views. The owner accepted the basic head; the automated vision gate still rejects the eye/lid region as deferred polish. See [`../makehuman-head-review.md`](../makehuman-head-review.md). |
|
||||
| [](sheet_npc_baseline_compare-body.jpg) <br> `sheet_npc_baseline_compare-body.jpg` | Neck/shoulder transition: fuller neck base and a lower shoulder root. |
|
||||
| [](sheet_npc_baseline_compare-face-detail.jpg) <br> `sheet_npc_baseline_compare-face-detail.jpg` | Face detail: nose ala wings, a single rounded mouth and a darker ear concha. |
|
||||
| [](sheet_npc_baseline_compare-face.jpg) <br> `sheet_npc_baseline_compare-face.jpg` | Face-depth pass: recessed sclera, upper lid and sculpted chin/cheek planes. |
|
||||
|
||||
|
After Width: | Height: | Size: 48 KiB |
@@ -34,8 +34,14 @@ and the active order/gaps in [`TODO.md`](../TODO.md).
|
||||
## Known gaps
|
||||
|
||||
- External first-time-player sessions and balance sweeps are not completed.
|
||||
- The NPC head/face/hand overhaul and MakeHuman body pipeline are planned, not
|
||||
shipped.
|
||||
- The Quaternius roster is shipped (`0.9.9`): all twelve characters are baked from the three CC0
|
||||
sources (casual tee, hoodie, work clothes), 4,458–6,424 triangles each, 62-joint rig at source
|
||||
proportions, arm-down rest, five tint slots, unwrapped UVs and baked 256² AO. The procedural
|
||||
library and the MakeHuman head remain the tested fallback.
|
||||
- Roster silhouette variety is narrower than the procedural library (three meshes for twelve
|
||||
roles, differentiated by tint and gear) until the modular part library is added.
|
||||
- Head gear is authored for the shipped Quaternius head; the procedural fallback therefore wears
|
||||
slightly undersized caps. Cosmetic, fallback-only.
|
||||
- Accessibility/localization acceptance, target-hardware profiling, macOS
|
||||
signing/notarization and AI/storefront legal approval remain open.
|
||||
- Live 3D replay, recorded VO/music, more venues and seasonal content are future
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# MakeHuman head prototype
|
||||
|
||||
Status: **retained fallback; superseded as the active NPC head by the Quaternius
|
||||
base (ADR-0017).** The owner accepted the basic human head on 2026-11-04. The automated vision gate
|
||||
still records REJECT on the eye/lid region as deferred polish. The asset is a
|
||||
standalone head/neck extracted from the CC0 MakeHuman base. The assembled path
|
||||
adds hair, a hidden scalp trim and the procedural body. The 12 shipped GLBs have
|
||||
not been rebuilt with it, and the Quaternius migration replaces it as the active
|
||||
base. The development source files were removed from `tmp/` on 2026-11-05.
|
||||
|
||||
## Asset
|
||||
|
||||
- GLB: [`assets/models/blender_src/makehuman_head.glb`](../assets/models/blender_src/makehuman_head.glb)
|
||||
- Review sheet: [`blender_previews/sheet_makehuman_head.jpg`](blender_previews/sheet_makehuman_head.jpg)
|
||||
- Source workflow (development-only, files removed after the Quaternius pivot;
|
||||
retained here as the historical method) — extracts the MakeHuman head, cuts it at the neck, keeps the head/eye/mouth
|
||||
components, cuts an almond aperture through a skin eyelid mask, adds recessed
|
||||
eyeballs with flat iris/pupil discs, anchors brows to the face surface, renders
|
||||
front/three-quarter/profile and exports the standalone GLB.
|
||||
- Measurements (final standalone build, `inspect`): **12,641 triangles / 7,690
|
||||
split vertices**; bounds approximately `x ±0.094 m`, `y -0.180…0.042 m`,
|
||||
`z 1.530…1.803 m` in project Z-up space.
|
||||
- Assembled `npc_casual --head mh`: **14,753 triangles / 12,823 seam-split
|
||||
vertices / 14 bones / 9 materials**, `inspect` clean. All 12 variants count
|
||||
14,693–15,987 triangles with the same head.
|
||||
- Materials used: `skin`, `eye_white`, `iris`, `pupil`, `hair`. No textures are
|
||||
baked into this prototype.
|
||||
|
||||

|
||||
|
||||
## What was done
|
||||
|
||||
1. Imported the CC0 MakeHuman base OBJ and cleared the importer axis rotation.
|
||||
2. Applied the project scale/axis transform: 1.82 m body, Z-up, metres,
|
||||
front −Y.
|
||||
3. Cut the head at the neck (`z ≈ 1.53`) rather than keeping the cut shoulders;
|
||||
capped the neck opening.
|
||||
4. Built an eyelid mask by subtracting an almond aperture from a skin sphere
|
||||
centred on each eye (`solver = "EXACT"`), then recessed the globes so the
|
||||
cornea sits behind the lid plane.
|
||||
5. Added flat iris/pupil discs on the globe axis; disabled backface culling so
|
||||
the discs stay visible at three-quarter angles.
|
||||
6. Anchored brow strips to the skin with object-space ray casts and added a
|
||||
shallow supraorbital ridge so they do not float, then reduced the crown
|
||||
height to improve the profile silhouette.
|
||||
7. Fixed a material-remap bug introduced by joining the extra eye objects: the
|
||||
joined head faces were pointing at the `eye_white` slot, making the whole
|
||||
head render as white clay. The largest connected component is now forced
|
||||
back onto `skin` after the join.
|
||||
8. Rendered front/three-quarter/profile views in a neutral review studio and
|
||||
exported the standalone GLB.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Base mesh: MakeHuman / MPFB base, CC0, from the local research set
|
||||
(development source files removed after the 2026-11-05 Quaternius pivot); see
|
||||
[`CREDITS.md`](../CREDITS.md).
|
||||
- The head is a **derived prototype**, not a shipped NPC. It does not enter
|
||||
`assets/models/blender/npc/`, `npc_base`, the runtime `NpcAvatar` path, CI, or
|
||||
a release package. It is retained as a fallback, not the active NPC base.
|
||||
- Source photographs/proportions are not used as textures here; this is a
|
||||
geometry-only head built from the researched model data.
|
||||
|
||||
## Review criteria
|
||||
|
||||
Accept only if:
|
||||
|
||||
- the head reads as human at front/three-quarter/profile and the neck is a
|
||||
clean head/bust silhouette, not a shoulder cut;
|
||||
- eyes, brow, nose, mouth and ears are readable without spikes, holes or
|
||||
floating shells;
|
||||
- the asset is inside the character budget and has no script/contract violation;
|
||||
- the prototype remains clearly local and un-integrated until the part pipeline
|
||||
is approved.
|
||||
|
||||
## Judge result
|
||||
|
||||
**Owner verdict: ACCEPT (basic human head, 2026-11-04).**
|
||||
**Automated vision gate: REJECT** (vision judge `z-ai/glm-5.3-flash`, final
|
||||
standalone build render set above). The eye/lid findings below remain deferred
|
||||
polish; they are no longer a rollout blocker because the owner accepted the head.
|
||||
|
||||
Blocking findings:
|
||||
|
||||
- **P0 — globe depth is inconsistent across views.** The front/three-quarter
|
||||
reads still show a bead-like iris with a pale sclera halo, while the profile
|
||||
reduces the eye to a brown sliver. The judge reads this as a faked recess
|
||||
rather than a socketed globe under a lid.
|
||||
- **P0 — no convincing lid overlap.** The iris remains nearly circular in front
|
||||
and the upper lid does not visibly clip the globe; the skin lid mask reads as
|
||||
a crater around a bead.
|
||||
- **P0 — iris orientation reads decal-locked** at three-quarter, so the gaze
|
||||
appears to drift off the head axis.
|
||||
- **P0 — the aperture/lid mask is too circular** and lacks medial/lateral
|
||||
canthi, so the eye still reads as a doll eye at portrait distance.
|
||||
|
||||
Should-fix findings:
|
||||
|
||||
- the brow tubes are better anchored but the medial ends still detach in
|
||||
three-quarter/profile;
|
||||
- nose lacks a defined tip/wing and nostrils;
|
||||
- mouth is a faint geometric seam with no lip volume;
|
||||
- crown/back-of-skull is still faceted and egg-like in profile;
|
||||
- the catchlight is baked geometry and is not consistent between eyes.
|
||||
|
||||
What passed: the skin/eye material remap is correct, the eye openings are
|
||||
closed, the standalone GLB reports **12,641 triangles / 7,690 split vertices**,
|
||||
and an assembled `npc_casual` with this head passes `inspect` at **14,753 /
|
||||
16,000 triangles, 12,823 seam-split vertices, 14 bones, no issues**.
|
||||
|
||||
Known limits: bald mannequin-like presentation, stylized geometry-only
|
||||
iris/pupil, no rig, no UV texture pass, and the neck cap is a review cut rather
|
||||
than a body-welded seam.
|
||||
|
||||
## Assembled prototype (2026-11-04)
|
||||
|
||||
- `_makehuman_head` imports the GLB and adds a **skull-conforming hair cap**
|
||||
derived from the closed head shell (scaled shell + boolean front-sphere
|
||||
cut + decimate). Hair skips the old `hair_normal.png` because the skull UVs
|
||||
produced large dark tangent patches.
|
||||
- Head faces fully covered by the cap are deleted to recover ~950 triangles;
|
||||
boundary faces stay as a safety band, so the accepted face geometry is
|
||||
untouched. The hidden neck plug uses 12 segments.
|
||||
- The procedural body was narrowed at the neck and shoulders (chest 0.158,
|
||||
neck base 0.060, neck top 0.054, shoulder root z 1.345) and its skin decimate
|
||||
dropped to 0.21 so all 12 variants fit the 16,000 cap.
|
||||
- Local review renders were removed with the 2026-11-05 `tmp/` cleanup; the
|
||||
committed review sheet above is the retained evidence.
|
||||
|
||||
## Deferred polish
|
||||
|
||||
- Replace the radial lid mask with real upper/lower lid masses that fold over
|
||||
the globe, with pointed canthi, and pass the profile view.
|
||||
- Move the iris onto a curved cornea cap so its orientation follows the globe at
|
||||
three-quarter angles, and use a lighting-driven catchlight instead of baked
|
||||
white geometry.
|
||||
- Refine the hair cap silhouette (current short cap reads as a helmet) and
|
||||
improve the full-body shoulder/arm shape before the 12-variant rollout.
|
||||
- Add a clean UV/material pass and a contract armature/weights when the head
|
||||
becomes part of the shipped library characters.
|
||||
@@ -1,65 +1,324 @@
|
||||
# NPC Model Sourcing
|
||||
|
||||
This page records the source evaluation behind [ADR-0015](adr/0015-npc-model-variety-sourcing.md).
|
||||
The shipped path is the project-owned Blender/procedural library; external sources
|
||||
remain references or local prototypes unless a later ADR accepts them.
|
||||
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).
|
||||
|
||||
## Evaluation
|
||||
## Active direction
|
||||
|
||||
The library needs variety beyond the original eight silhouettes while preserving
|
||||
the `npc_base` contract: mesh node, 13 required core bones (optional extras
|
||||
allowed), five tint slots, −Y/+Z front, grounded feet and the 16,000/16,000
|
||||
character budget.
|
||||
**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.
|
||||
|
||||
| Source | Licence | Result |
|
||||
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 |
|
||||
| --- | --- | --- |
|
||||
| Quaternius Ultimate Modular | CC0 | Good silhouette/decimation material, but a 62-bone T-pose realistic rig and different proportions; not a drop-in. Kept as a reference/prototype. |
|
||||
| Kenney Blocky Characters | CC0 | No matching rig; style is much blockier than the project look. |
|
||||
| Khronos CesiumMan / RiggedFigure | CC BY 4.0 | Different bone names/rest pose and no five-slot materials; not a drop-in. |
|
||||
| MakeHuman / MPFB base | CC0 | Useful body-shape base, but requires retopology, a custom rig, clothing and part integration. Active local pipeline, not the shipped path. |
|
||||
| `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 |
|
||||
|
||||
The contract mismatch is the expensive part, not decimation: retargeting a
|
||||
different rig or rebuilding slots/weights can distort the body and invalidate
|
||||
gear placement. Any future second-rig path needs its own ADR and animation/gear
|
||||
offsets.
|
||||
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.
|
||||
|
||||
## Decision and outcome
|
||||
**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.
|
||||
|
||||
- Extend the project-owned procedural generator as the primary shipped path
|
||||
(12-character library, crowd variants, role pools).
|
||||
- Keep Quaternius as a studied reference, not a shipped base.
|
||||
- MakeHuman is a local development pipeline for body-shape diversity; it must
|
||||
consume the same head/hand parts as the procedural library and never become a
|
||||
CI/build dependency.
|
||||
- Record CC0/CC-BY provenance; CC-BY previews require attribution in
|
||||
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 and silhouette gate sheet is in [`screens/npc-face-gate/`](screens/npc-face-gate/README.md).
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## MakeHuman prototype notes
|
||||
## Closed tracks
|
||||
|
||||
The local prototype (`tmp/npc-research/`, never committed) scaled the base to
|
||||
1.82 m, grounded it, triangulated/decimated it and retargeted it to the 13-core
|
||||
armature. The first retarget measured **6,444 / 16,000 tris and**
|
||||
**7,726 / 16,000 verts**, 14 bones, skinned, arm-down, five slots. It exposed
|
||||
the expected issues: a 1.63× arm-scale warp at the deltoid/shoulder, blocky
|
||||
decimated hands, rough slot boundaries and a head/neck shadow gap. The later
|
||||
project head/hands swap reduced this to **6,407 / 16,000 tris and**
|
||||
**6,651 / 16,000 verts**, but manual shoulder/neck retopology and part cleanup
|
||||
remain before it can be considered shippable.
|
||||
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.
|
||||
|
||||
The head/face/hand overhaul in [`TODO.md`](../TODO.md) is the prerequisite: the
|
||||
MakeHuman body should reuse the approved local head/hand parts rather than
|
||||
forking a second face implementation.
|
||||
## Related documents
|
||||
|
||||
## Provenance rules
|
||||
|
||||
- CC0 preferred; CC-BY only with attribution in `CREDITS.md`; no service asset
|
||||
whose terms restrict redistribution.
|
||||
- Downloaded research and generated previews stay in `tmp/`/gitignored folders;
|
||||
only final audited assets and committed review sheets enter the repository.
|
||||
- Record source URL, licence, author, date and what decision the reference
|
||||
informed. Never treat a generated concept as licensed map/model data.
|
||||
|
||||
Measurements, visual decisions and accepted limits are in
|
||||
[`3d-model-review.md`](3d-model-review.md) and
|
||||
[`npc-visual-review.md`](npc-visual-review.md). Commands are in
|
||||
[`asset-tooling.md`](asset-tooling.md).
|
||||
- [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), [`npc-visual-review.md`](npc-visual-review.md)
|
||||
- [`asset-tooling.md`](asset-tooling.md)
|
||||
@@ -71,8 +71,8 @@ The criteria below are for the stylized target, not photorealism. A change must
|
||||
pass every applicable item before a library rebuild is accepted.
|
||||
|
||||
1. **Asset contract:** `npc_base` mesh node, the required bones, five tint slots,
|
||||
front +Z, feet within 3 cm of y=0, 1.6–2.1 m height, at most 16,000 triangles
|
||||
and 16,000 glTF vertices per character. The required 13 core bones stay fixed;
|
||||
front +Z, feet within 3 cm of y=0, 1.6–2.1 m height, at most 24,000 triangles
|
||||
and 24,000 glTF vertices per character. The required 13 core bones stay fixed;
|
||||
optional extra bones are allowed. Gear remains at most 1,500 triangles.
|
||||
2. **Proportion and silhouette:** head, neck, shoulders, torso, hips, limbs and
|
||||
feet read as one body; front/three-quarter/profile silhouettes agree with the
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Head rework — review page (2026-11-04)
|
||||
|
||||
> **Historical page / changed source (2026-11-04).** The numbers below describe the
|
||||
> earlier decimated MakeHuman head (1,548-tri skin). The shared source
|
||||
> `assets/models/blender_src/makehuman_head.glb` now holds the higher-resolution
|
||||
> standalone prototype. The owner accepted the basic head; the automated vision gate
|
||||
> still records REJECT on the eye/lid region as deferred polish. See
|
||||
> [`../../makehuman-head-review.md`](../../makehuman-head-review.md). Re-run the
|
||||
> library head build before relying on the comparison counts below.
|
||||
|
||||
This page is the review checkpoint for the NPC head rework. The goal was to replace the
|
||||
sphere-and-floating-primitive face with a head that has real nose / chin / jaw landmarks at
|
||||
portrait distance.
|
||||
@@ -52,8 +60,9 @@ Left to right: front, three-quarter, profile.
|
||||
4. The head skin is 1.5k tris (smooth but low-poly); a retopology pass could add resolution if a
|
||||
portrait shows faceting.
|
||||
|
||||
## Decision needed
|
||||
## Decision (2026-11-04, owner)
|
||||
|
||||
Approve or reject the MakeHuman-head direction. If approved, the next step is rolling it into the
|
||||
other 11 variants (one asset family at a time) with a before/after sheet per batch, and re-running
|
||||
the 24-NPC budget + AO bake.
|
||||
The MakeHuman-head direction is approved for the basic human head. The remaining review is the
|
||||
assembled prototype's hair cap and full-body silhouette. After that, roll the head into the other
|
||||
11 variants (one asset family at a time) with a before/after sheet per batch, then re-run the
|
||||
24-NPC budget + AO bake.
|
||||
@@ -0,0 +1,66 @@
|
||||
# NPC face and silhouette gate — Quaternius pilot (2026-11-05)
|
||||
|
||||
This page is the owner decision point for
|
||||
[ADR-0017](../../adr/0017-quaternius-npc-base.md) + [ADR-0018](../../adr/0018-quaternius-stage2-runtime-rig.md)
|
||||
stage 2. It answers one question: **which head ships?**
|
||||
|
||||
`npc_casual` is now the CC0 Quaternius `Casual_Hoodie` base baked to the project contract
|
||||
(arm-down rest, five tint slots, 62-joint rig at source proportions). The procedural library with
|
||||
the MakeHuman head is unchanged and still ships for the other eleven characters.
|
||||
|
||||
## The sheet
|
||||
|
||||

|
||||
|
||||
Rows: the Quaternius base, the procedural library with the MakeHuman head, and a procedural crowd
|
||||
variant. Columns: front, three-quarter, profile, and the crowd-distance silhouette the player
|
||||
actually sees from 8 m.
|
||||
|
||||
Source frames: `godot --path . --script res://src/test/capture_npc_baseline.gd -- --out=res://tmp/npc-gate`.
|
||||
|
||||
## What the sheet shows
|
||||
|
||||
| | Head height / body | Face read at portrait | Crowd-distance read |
|
||||
| --- | --- | --- | --- |
|
||||
| Quaternius base | 0.134 | clean low-poly brows, nose, jaw, layered hair | slim, upright, clearly a person |
|
||||
| procedural + MakeHuman head | ~0.20 | oversized head, soft nose, beard slab reads as a bar | bobblehead proportions |
|
||||
| procedural crowd variant | ~0.20 | oversized head, flat cheeks | bobblehead proportions |
|
||||
|
||||
Measured (`python3 -m pytest tools/tests/test_npc_proportions.py -q`,
|
||||
`python3 tools/npc_proportions.py`):
|
||||
|
||||
- `npc_casual` (Quaternius): head 13.4 % of height, arm-down rest, 6,206 tris, 12,988 glTF
|
||||
vertices, texel density 73–74 px/m on the 256² maps.
|
||||
- The procedural library: head 16–22 % of height (the stylized band in
|
||||
`tools/npc_proportions.GUIDE`).
|
||||
|
||||
The Quaternius head is *smaller and more realistic* than the stylized library head, which is
|
||||
exactly the trade the owner has to accept or reject. The vision judge that reviewed the earlier
|
||||
head rework reads the Quaternius face as the cleaner silhouette of the three at both distances.
|
||||
|
||||
## The three options
|
||||
|
||||
1. **Full swap.** Every character rebaked onto the Quaternius base in `0.9.10`; the procedural
|
||||
library and the MakeHuman head retire to the tested fallback. Consistent look, real anatomy,
|
||||
no further head work.
|
||||
2. **Crowd only.** Quaternius for the anonymous crowd roles, procedural + MakeHuman for the named
|
||||
roles the player talks to. Keeps two art styles in one frame.
|
||||
3. **Add head detail.** Keep the Quaternius body and author a higher-detail head (subdivision and
|
||||
hair cards) inside the 24,000-triangle / 24,000-vertex budget. Most work; the current head
|
||||
already reads well at gameplay distance.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Option 1. The Quaternius head beats the MakeHuman head at portrait distance in this sheet, and it
|
||||
costs no further authoring; the budget headroom (6.2k of 24k triangles) stays available for the
|
||||
hands and gear that the review still flags as weak.
|
||||
|
||||
## Recorded decision
|
||||
|
||||
Status: **option 1 (full swap) accepted by the owner, 2026-11-05.** Every roster character moves
|
||||
onto the Quaternius base; the procedural library and the MakeHuman head become the tested
|
||||
fallback. See the follow-up decision section in
|
||||
[ADR-0018](../../adr/0018-quaternius-stage2-runtime-rig.md#follow-up-decision-full-roster-swap).
|
||||
|
||||
Roster silhouettes are less varied than the procedural library until the modular part library
|
||||
(hair, outfits) is added in the `0.9.10` roster workstream — recorded rather than hidden.
|
||||
|
After Width: | Height: | Size: 130 KiB |
@@ -8,7 +8,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tmp/*, tmp/**, build/*, build/**, docs/*, docs/**, tools/*, tools/**, src/test/*, src/test/**, .agents/*, .agents/**, packaging/*, packaging/**"
|
||||
export_path="build/gig-line-linux.x86_64"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
@@ -39,7 +39,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tmp/*, tmp/**, build/*, build/**, docs/*, docs/**, tools/*, tools/**, src/test/*, src/test/**, .agents/*, .agents/**, packaging/*, packaging/**"
|
||||
export_path="build/gig-line-windows.exe"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
@@ -75,7 +75,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tmp/*, tmp/**, build/*, build/**, docs/*, docs/**, tools/*, tools/**, src/test/*, src/test/**, .agents/*, .agents/**, packaging/*, packaging/**"
|
||||
export_path="build/gig-line-macos.zip"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
@@ -114,7 +114,7 @@ dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
exclude_filter="tmp/*, tmp/**, build/*, build/**, docs/*, docs/**, tools/*, tools/**, src/test/*, src/test/**, .agents/*, .agents/**, packaging/*, packaging/**"
|
||||
export_path="build/web/index.html"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
|
||||
@@ -12,7 +12,7 @@ config_version=5
|
||||
|
||||
config/name="Gig Line"
|
||||
config/description="Workplace security simulation about observation, access verification, and proportionate escalation."
|
||||
config/version="0.9.8"
|
||||
config/version="0.9.9"
|
||||
run/main_scene="res://scenes/main.tscn"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
|
||||
|
||||
@@ -413,38 +413,45 @@ func _build_skinned_body(npc_type: int) -> bool:
|
||||
|
||||
# Animation proxies: the existing _animate()/idle code drives these Node3D
|
||||
# pivots; _sync_skeleton() copies their rotations onto the armature bones.
|
||||
_bind_bones()
|
||||
_head = Node3D.new()
|
||||
_head.name = "Head"
|
||||
_head.position = Vector3(0, 1.62, 0)
|
||||
_head.position = _rest_origin(["Head"])
|
||||
_rig.add_child(_head)
|
||||
for side in [-1.0, 1.0]:
|
||||
var suffix := "L" if side < 0.0 else "R"
|
||||
for i in 2:
|
||||
var suffix := "L" if i == 0 else "R"
|
||||
# The proxy pivots sit on the resolved rig bones (see RIG_BONES), so the
|
||||
# proxy chirality and the exported joint naming never have to agree.
|
||||
var hip_bone := bone_name("hip", i)
|
||||
var knee_bone := bone_name("knee", i)
|
||||
var shoulder_bone := bone_name("shoulder", i)
|
||||
var elbow_bone := bone_name("elbow", i)
|
||||
var hand_bone := bone_name("hand", i)
|
||||
var hip := Node3D.new()
|
||||
hip.name = "Hip" + suffix
|
||||
hip.position = Vector3(0.13 * side, 0.64, 0)
|
||||
hip.position = _rest_origin([hip_bone])
|
||||
_rig.add_child(hip)
|
||||
var knee := Node3D.new()
|
||||
knee.name = "Knee"
|
||||
knee.position = Vector3(0, -0.34, 0)
|
||||
knee.position = _rest_origin([knee_bone]) - hip.position
|
||||
hip.add_child(knee)
|
||||
_legs.append(hip)
|
||||
_knees.append(knee)
|
||||
var shoulder := Node3D.new()
|
||||
shoulder.name = "Shoulder" + suffix
|
||||
shoulder.position = Vector3(0.27 * side, 1.38, 0)
|
||||
shoulder.position = _rest_origin([shoulder_bone])
|
||||
_rig.add_child(shoulder)
|
||||
var elbow := Node3D.new()
|
||||
elbow.name = "Elbow"
|
||||
elbow.position = Vector3(0, -0.30, 0)
|
||||
elbow.position = _rest_origin([elbow_bone]) - shoulder.position
|
||||
shoulder.add_child(elbow)
|
||||
var hand := Node3D.new()
|
||||
hand.name = "Hand"
|
||||
hand.position = Vector3(0, -0.30, 0.02)
|
||||
hand.position = _rest_origin([hand_bone]) - _rest_origin([elbow_bone])
|
||||
elbow.add_child(hand)
|
||||
_arms.append(shoulder)
|
||||
_elbows.append(elbow)
|
||||
_hands.append(hand)
|
||||
_bind_bones()
|
||||
|
||||
_add_accessory(str(outfit["accessory"]), cloth)
|
||||
_add_character_gear()
|
||||
@@ -495,35 +502,84 @@ func _bind_bones() -> void:
|
||||
for index in _skeleton.get_bone_count():
|
||||
_bone_indices[_skeleton.get_bone_name(index)] = index
|
||||
|
||||
## First-found rest origin for a list of candidate bone names, in the skeleton's
|
||||
## space (global rest), so proxy pivots sit on the actual joints.
|
||||
func _rest_origin(names: Array) -> Vector3:
|
||||
if _skeleton == null:
|
||||
return Vector3.ZERO
|
||||
for bone_name in names:
|
||||
if _bone_indices.has(bone_name):
|
||||
return _skeleton.get_bone_global_rest(_bone_indices[bone_name]).origin
|
||||
return Vector3.ZERO
|
||||
|
||||
## Bone-name candidates per animation joint, in proxy order: index 0 is the
|
||||
## avatar's left (-x) and index 1 its right (+x). The Quaternius rig labels its
|
||||
## dot-suffixed joints with the opposite side (its `L` sits at +x), so the pairs
|
||||
## carry the rig-specific mapping and nothing else has to know about it.
|
||||
const RIG_BONES := {
|
||||
"head": [["Head"], ["Head"]],
|
||||
"hip": [["UpperLeg.R", "HipL"], ["UpperLeg.L", "HipR"]],
|
||||
"knee": [["LowerLeg.R", "KneeL"], ["LowerLeg.L", "KneeR"]],
|
||||
"shoulder": [["UpperArm.R", "ShoulderL"], ["UpperArm.L", "ShoulderR"]],
|
||||
"elbow": [["LowerArm.R", "ElbowL"], ["LowerArm.L", "ElbowR"]],
|
||||
"hand": [["Wrist.R", "HandL"], ["Wrist.L", "HandR"]],
|
||||
}
|
||||
|
||||
## Resolved skeleton bone name for an animation joint on one side. Returns an
|
||||
## empty string when the rig has neither candidate, so both the Quaternius
|
||||
## 62-joint rig and the 13-core fallback resolve through the same call.
|
||||
func bone_name(joint: String, side: int = 0) -> String:
|
||||
if not RIG_BONES.has(joint):
|
||||
return ""
|
||||
var pair: Array = RIG_BONES[joint]
|
||||
return _bone_of(pair[0], pair[1], side)
|
||||
|
||||
## Copy the proxy pivot rotations onto armature bones. Limb bones point down, so
|
||||
## an X rotation swings them forward/back; the Head bone points up, so a Y
|
||||
## rotation is the look yaw.
|
||||
func _sync_skeleton() -> void:
|
||||
if _skeleton == null or _bone_indices.is_empty():
|
||||
return
|
||||
var names := ["HipL", "HipR"]
|
||||
for i in 2:
|
||||
_pose_bone(names[i], _legs[i].rotation.x, Vector3.RIGHT)
|
||||
names = ["KneeL", "KneeR"]
|
||||
for i in 2:
|
||||
_pose_bone(names[i], _knees[i].rotation.x, Vector3.RIGHT)
|
||||
names = ["ShoulderL", "ShoulderR"]
|
||||
for i in 2:
|
||||
_pose_bone(names[i], _arms[i].rotation.x, Vector3.RIGHT)
|
||||
names = ["ElbowL", "ElbowR"]
|
||||
for i in 2:
|
||||
_pose_bone(names[i], _elbows[i].rotation.x, Vector3.RIGHT)
|
||||
var hip_swing := Quaternion(Vector3.RIGHT, _legs[i].rotation.x)
|
||||
_swing_bone(bone_name("hip", i), hip_swing)
|
||||
# `_swing_bone` writes the absolute global rotation, so a child bone has to
|
||||
# receive the swing it inherits from its parent as well as its own.
|
||||
_swing_bone(bone_name("knee", i), hip_swing * Quaternion(Vector3.RIGHT, _knees[i].rotation.x))
|
||||
var shoulder_swing := Quaternion(Vector3.LEFT, _arms[i].rotation.x)
|
||||
_swing_bone(bone_name("shoulder", i), shoulder_swing)
|
||||
_swing_bone(bone_name("elbow", i), shoulder_swing * Quaternion(Vector3.LEFT, _elbows[i].rotation.x))
|
||||
if _head != null:
|
||||
_pose_bone("Head", _head.rotation.y, Vector3.UP)
|
||||
var head := bone_name("head")
|
||||
if head != "":
|
||||
_swing_bone(head, Quaternion(Vector3.UP, _head.rotation.y))
|
||||
|
||||
func _pose_bone(bone_name: String, angle: float, axis: Vector3) -> void:
|
||||
if not _bone_indices.has(bone_name):
|
||||
## First bone present from the left list (index 0) or the right list (index 1).
|
||||
func _bone_of(left: Array, right: Array, index: int) -> String:
|
||||
for bone_name in (left if index == 0 else right):
|
||||
if _bone_indices.has(bone_name):
|
||||
return str(bone_name)
|
||||
return ""
|
||||
|
||||
## Swing a bone about a world axis through its head, keeping its rest orientation
|
||||
## as the neutral. Writing the *global* pose is rig-agnostic: it does not depend
|
||||
## on how the armature pack exported its rest rolls or on the pose/rest split the
|
||||
## glTF importer chose, both of which differ between the 62-joint Quaternius rig
|
||||
## and the 13-core fallback. Measuring `get_bone_global_pose()` after the call is
|
||||
## the only reliable check (see the pose-capture gate).
|
||||
##
|
||||
## `rest` must be the **global** rest. `get_bone_rest()` is parent-relative and
|
||||
## already carries the importer's axis conversion, so conjugating with it swung
|
||||
## the limbs about the wrong axis and splayed the arms.
|
||||
func _swing_bone(bone_name: String, swing: Quaternion) -> void:
|
||||
if bone_name == "":
|
||||
return
|
||||
var index: int = _bone_indices[bone_name]
|
||||
# Bone pose rotation is the absolute local orientation, not an offset from
|
||||
# rest; compose with the rest rotation so identity preserves the bind pose.
|
||||
var rest := _skeleton.get_bone_rest(index).basis.get_rotation_quaternion()
|
||||
_skeleton.set_bone_pose_rotation(index, rest * Quaternion(axis, angle))
|
||||
_skeleton.reset_bone_pose(index)
|
||||
var rest := _skeleton.get_bone_global_rest(index).basis.get_rotation_quaternion()
|
||||
var current := _skeleton.get_bone_global_pose(index)
|
||||
var target := Basis((swing * rest).normalized()).scaled(current.basis.get_scale())
|
||||
_skeleton.set_bone_global_pose(index, Transform3D(target, current.origin))
|
||||
|
||||
func _build_body(npc_type: int) -> void:
|
||||
_rig = Node3D.new()
|
||||
|
||||
@@ -9,6 +9,17 @@ const NPC_BASE := "res://assets/models/generated/npc_base.glb"
|
||||
const SLOTS := ["skin", "cloth", "pants", "accessory", "hair"]
|
||||
const BONES := ["Head", "HipL", "HipR", "KneeL", "KneeR", "ShoulderL", "ShoulderR",
|
||||
"ElbowL", "ElbowR", "HandL", "HandR"]
|
||||
## The Quaternius rig (ADR-0018) is a 62-joint armature. `NpcAvatar` maps these
|
||||
## joints onto the animation proxies, so they are the drop-in requirement for a
|
||||
## Quaternius-derived character instead of the 13-core names.
|
||||
const QUATERNIUS_BONES := ["Head", "UpperLeg.L", "UpperLeg.R", "LowerLeg.L",
|
||||
"LowerLeg.R", "UpperArm.L", "UpperArm.R", "LowerArm.L", "LowerArm.R",
|
||||
"Wrist.L", "Wrist.R"]
|
||||
|
||||
## Quaternius-derived characters are detected by a dot-suffixed UpperArm joint;
|
||||
## the procedural library uses ShoulderL/ShoulderR instead.
|
||||
func _is_quaternius(skeleton: Skeleton3D) -> bool:
|
||||
return skeleton != null and skeleton.find_bone("UpperArm.L") >= 0
|
||||
|
||||
func _manifest() -> Dictionary:
|
||||
if not FileAccess.file_exists(MANIFEST):
|
||||
@@ -76,7 +87,8 @@ func test_characters_match_the_npc_base_contract() -> bool:
|
||||
problems.append("%s lacks Skeleton3D/npc_base" % name)
|
||||
node.free()
|
||||
continue
|
||||
for bone in BONES:
|
||||
var required: Array = QUATERNIUS_BONES if _is_quaternius(skeleton) else BONES
|
||||
for bone in required:
|
||||
if skeleton.find_bone(bone) < 0:
|
||||
problems.append("%s missing bone %s" % [name, bone])
|
||||
var slots := {}
|
||||
@@ -105,6 +117,11 @@ func test_character_joints_line_up_with_npc_base() -> bool:
|
||||
var skeleton := node.find_child("Skeleton3D", true, false) as Skeleton3D if node else null
|
||||
if skeleton == null:
|
||||
problems.append("%s has no skeleton" % name)
|
||||
elif _is_quaternius(skeleton):
|
||||
# ADR-0018: a Quaternius character keeps its own 62-joint skeleton and
|
||||
# NpcAvatar re-derives the proxy pivots from it, so the joints are not
|
||||
# expected to sit on npc_base's 13-core positions.
|
||||
pass
|
||||
else:
|
||||
for bone in BONES:
|
||||
var a := _global_rest(base_skel, base_skel.find_bone(bone)).origin
|
||||
@@ -127,10 +144,18 @@ func test_character_bones_accept_the_avatar_pose_math() -> bool:
|
||||
if skeleton == null:
|
||||
problems.append(name)
|
||||
else:
|
||||
for bone in BONES:
|
||||
var bones: Array = QUATERNIUS_BONES if _is_quaternius(skeleton) else BONES
|
||||
for bone in bones:
|
||||
var index := skeleton.find_bone(bone)
|
||||
var rest := skeleton.get_bone_rest(index).basis.get_rotation_quaternion()
|
||||
skeleton.set_bone_pose_rotation(index, rest * Quaternion(Vector3.RIGHT, 0.4))
|
||||
if index < 0:
|
||||
problems.append("%s missing %s" % [name, bone])
|
||||
continue
|
||||
# NpcAvatar._swing_bone(): pose = rest^-1 * swing * rest, applied
|
||||
# through the global pose. The composition must stay a valid
|
||||
# rotation on every limb bone of either rig.
|
||||
var rest := skeleton.get_bone_global_rest(index).basis.get_rotation_quaternion()
|
||||
var swing := Quaternion(Vector3.RIGHT, 0.4)
|
||||
skeleton.set_bone_pose_rotation(index, rest.inverse() * swing * rest)
|
||||
if not skeleton.get_bone_pose_rotation(index).is_normalized():
|
||||
problems.append("%s.%s" % [name, bone])
|
||||
if node:
|
||||
|
||||
@@ -141,8 +141,8 @@ func test_skinned_model_loads_and_keeps_the_head_contract() -> bool:
|
||||
"a skinned library character is used")
|
||||
var b := TestUtils.check(avatar.get_node_or_null("Rig/Head") != null,
|
||||
"Rig/Head survives the skinned path")
|
||||
var c := TestUtils.check(avatar._skeleton != null and avatar._bone_indices.has("Head")
|
||||
and avatar._bone_indices.has("HipL") and avatar._bone_indices.has("HandR"),
|
||||
var c := TestUtils.check(avatar._skeleton != null and avatar.bone_name("head") != ""
|
||||
and avatar.bone_name("hip", 0) != "" and avatar.bone_name("hand", 1) != "",
|
||||
"armature bones are bound")
|
||||
avatar.free()
|
||||
return a and b and c
|
||||
@@ -152,10 +152,10 @@ func test_skinned_model_loads_and_keeps_the_head_contract() -> bool:
|
||||
func test_pose_reaches_the_skeleton_bones() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 55)
|
||||
var sk := avatar._skeleton
|
||||
var hip: int = int(avatar._bone_indices["HipL"])
|
||||
var knee: int = int(avatar._bone_indices["KneeL"])
|
||||
var rest_hip := sk.get_bone_rest(hip).basis.get_rotation_quaternion()
|
||||
var rest_knee := sk.get_bone_rest(knee).basis.get_rotation_quaternion()
|
||||
var hip: int = int(avatar._bone_indices[avatar.bone_name("hip", 0)])
|
||||
var knee: int = int(avatar._bone_indices[avatar.bone_name("knee", 0)])
|
||||
var rest_hip := sk.get_bone_pose_rotation(hip)
|
||||
var rest_knee := sk.get_bone_pose_rotation(knee)
|
||||
avatar.arrived = true
|
||||
avatar.npc.intent = "sit"
|
||||
for _i in 45:
|
||||
@@ -179,6 +179,49 @@ func test_pose_reaches_the_skeleton_bones() -> bool:
|
||||
avatar.free()
|
||||
return a and b and c and d
|
||||
|
||||
## Regression (ADR-0018): the shipped base must resolve every animation joint on
|
||||
## the rig it actually uses, keep the baked arm-down rest, and put each proxy on
|
||||
## the matching body side. The Quaternius rig labels its dot-suffixed joints with
|
||||
## the opposite side (its `L` sits at +x), so `RIG_BONES` is the single mapping and
|
||||
## a silent side swap would splay the arms across the torso.
|
||||
##
|
||||
## The swing itself is proven by the pose-capture gate; `_swing_bone()` writes the
|
||||
## global pose, which Godot only computes for a skeleton inside the scene tree and
|
||||
## the headless runner executes tests before the tree is ready.
|
||||
func test_shipped_rig_resolves_the_animation_joints() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 91)
|
||||
var sk := avatar._skeleton
|
||||
var problems: Array[String] = []
|
||||
for joint in NpcAvatar.RIG_BONES:
|
||||
for side in 2:
|
||||
var bone := avatar.bone_name(joint, side)
|
||||
if bone == "" or not avatar._bone_indices.has(bone):
|
||||
problems.append("%s/%d unresolved" % [joint, side])
|
||||
var a := TestUtils.check(problems.is_empty(),
|
||||
"the shipped rig resolves every animation joint (%s)" % str(problems))
|
||||
var wrist := avatar.bone_name("hand", 0)
|
||||
var shoulder := avatar.bone_name("shoulder", 0)
|
||||
var b := TestUtils.check(wrist != "" and shoulder != "", "shoulder/hand joints resolve")
|
||||
if not b:
|
||||
avatar.free()
|
||||
return false
|
||||
var shoulder_rest := sk.get_bone_global_rest(int(avatar._bone_indices[shoulder]))
|
||||
var wrist_rest := sk.get_bone_global_rest(int(avatar._bone_indices[wrist]))
|
||||
var c := TestUtils.check(shoulder_rest.origin.y - wrist_rest.origin.y > 0.3,
|
||||
"the shipped base is baked arm-down (%.3f below the shoulder)"
|
||||
% (shoulder_rest.origin.y - wrist_rest.origin.y))
|
||||
# A straight-down arm axis is what makes an X swing read as forward/back.
|
||||
var arm_down := shoulder_rest.basis.y.normalized().dot(Vector3.DOWN)
|
||||
var d := TestUtils.check(arm_down > 0.95,
|
||||
"the arm rest axis points down (dot %.3f)" % arm_down)
|
||||
# Proxy side 0 must drive the -x arm and side 1 the +x arm on either rig.
|
||||
var left := sk.get_bone_global_rest(int(avatar._bone_indices[avatar.bone_name("shoulder", 0)]))
|
||||
var right := sk.get_bone_global_rest(int(avatar._bone_indices[avatar.bone_name("shoulder", 1)]))
|
||||
var e := TestUtils.check(left.origin.x < right.origin.x,
|
||||
"proxy side 0 drives the -x arm (%.3f vs %.3f)" % [left.origin.x, right.origin.x])
|
||||
avatar.free()
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_character_selection_is_deterministic_and_complete() -> bool:
|
||||
# Seed 5 resolves each role to a fixed pool member; the routine crowd roles
|
||||
# now pick from the crowd-variety pools instead of a single npc_casual.
|
||||
|
||||
@@ -53,14 +53,20 @@ MANIFEST = OUT_DIR / "manifest.json"
|
||||
SLOTS = ["skin", "cloth", "pants", "accessory", "hair"]
|
||||
BONES = ["Head", "HipL", "HipR", "KneeL", "KneeR", "ShoulderL", "ShoulderR",
|
||||
"ElbowL", "ElbowR", "HandL", "HandR"]
|
||||
# Character cap raised to 16000/16000 on 2026-11-04 for the head/face/hand
|
||||
# overhaul and possible MakeHuman parts. The required 13 core bones stay, but
|
||||
# optional extra bones (fingers, face, toes) are allowed; they must not move the
|
||||
# core rest positions. The glTF vertex count is after UV-seam splits; each
|
||||
# avatar is still one skinned mesh, and `smoke_npc_budget.gd` must confirm the
|
||||
# 24-NPC frame time before accepting the increase.
|
||||
CHAR_MAX_TRIS = 16000
|
||||
CHAR_MAX_VERTS = 16000
|
||||
# ADR-0018: a Quaternius-derived character keeps the upstream 62-joint armature
|
||||
# instead of the 13-core skeleton, so these are the joints `NpcAvatar` maps onto
|
||||
# the animation proxies and the ones the audit requires.
|
||||
QUATERNIUS_BONES = ["Head", "UpperLeg.L", "UpperLeg.R", "LowerLeg.L", "LowerLeg.R",
|
||||
"UpperArm.L", "UpperArm.R", "LowerArm.L", "LowerArm.R",
|
||||
"Wrist.L", "Wrist.R"]
|
||||
# Character cap raised to 24000/24000 on 2026-11-05 for the MPFB body, the
|
||||
# accepted MakeHuman head and a seamless neck/shoulder junction. The required
|
||||
# 13 core bones stay, but optional extra bones (fingers, face, toes) are
|
||||
# allowed; they must not move the core rest positions. The glTF vertex count is
|
||||
# after UV-seam splits; each avatar is still one skinned mesh, and the 24-NPC
|
||||
# smoke test must confirm frame time before shipping.
|
||||
CHAR_MAX_TRIS = 24000
|
||||
CHAR_MAX_VERTS = 24000
|
||||
GEAR_MAX_TRIS = 1500
|
||||
PROP_MAX_TRIS = 4000
|
||||
|
||||
@@ -79,37 +85,47 @@ except ImportError: # pragma: no cover - audit/sheet/list run outside Blender
|
||||
# Characters map onto NpcAvatar.layer_for() / ACCESSORIES so the runtime can
|
||||
# pick one per avatar later. `tint` lists the slots the runtime should tint;
|
||||
# every other material keeps its authored colour.
|
||||
# `base` selects the CC0 Quaternius source the character is baked from
|
||||
# (ADR-0017/ADR-0018, owner chose the full roster swap); `drop` removes upstream
|
||||
# materials that do not belong to the role (the Worker's hivis vest and hard hat).
|
||||
# Rebuild one procedurally with `build --source procedural --head mh`.
|
||||
CHARACTERS = {
|
||||
"npc_casual": {"layer": "none", "hair": "short", "gear": ["backpack", "camera"],
|
||||
"base": "hoodie",
|
||||
"roles": ["tourist", "confused_visitor", "intoxicated", "guest"]},
|
||||
"npc_jacket": {"layer": "jacket", "hair": "short", "gear": ["lanyard", "clipboard"],
|
||||
"base": "casual",
|
||||
"roles": ["authorized_staff", "protester", "guest"]},
|
||||
"npc_coat": {"layer": "coat", "hair": "long",
|
||||
"npc_coat": {"layer": "coat", "hair": "long", "base": "hoodie",
|
||||
"gear": ["sunglasses", "shoulder_bag", "phone"],
|
||||
"roles": ["influencer", "trespasser", "violent_threat"]},
|
||||
"npc_vest": {"layer": "vest", "hair": "buzz", "gear": ["cap", "clipboard"],
|
||||
"base": "worker",
|
||||
"roles": ["delivery"]},
|
||||
"npc_dress": {"layer": "dress", "hair": "bun", "build": "slim",
|
||||
"npc_dress": {"layer": "dress", "hair": "bun", "build": "slim", "base": "casual",
|
||||
"gear": ["shoulder_bag"],
|
||||
"roles": ["performer"]},
|
||||
"npc_hoodie": {"layer": "hood", "hair": "short", "gear": ["cup"],
|
||||
"npc_hoodie": {"layer": "hood", "hair": "short", "gear": ["cup"], "base": "hoodie",
|
||||
"roles": ["trespasser", "mental_crisis", "violent_threat"]},
|
||||
"npc_firefighter": {"layer": "responder", "hair": "buzz", "build": "broad",
|
||||
"base": "worker", "drop": ["Worker_Vest", "Worker_Yellow"],
|
||||
"gear": ["firefighter_helmet", "scba_tank", "flashlight"],
|
||||
"roles": ["responder"]},
|
||||
"npc_guard": {"layer": "guard", "hair": "buzz", "build": "broad",
|
||||
"base": "worker", "drop": ["Worker_Vest", "Worker_Yellow"],
|
||||
"gear": ["cap", "wand"],
|
||||
"roles": ["player_guard", "venue_security"]},
|
||||
# Crowd-variety prototypes (character development): vary build, head
|
||||
# silhouette, hair/beard and outer layer without labelling behaviour.
|
||||
"npc_crowd_a": {"layer": "none", "hair": "bun", "build": "slim",
|
||||
# Crowd-variety prototypes (character development): the three Quaternius
|
||||
# sources are spread across the crowd pools so a crowd never shows one mesh.
|
||||
"npc_crowd_a": {"layer": "none", "hair": "bun", "build": "slim", "base": "casual",
|
||||
"gear": ["sunglasses"], "roles": ["tourist", "guest"]},
|
||||
"npc_crowd_b": {"layer": "none", "hair": "bald", "build": "broad",
|
||||
"npc_crowd_b": {"layer": "none", "hair": "bald", "build": "broad", "base": "casual",
|
||||
"beard": True, "gear": ["shoulder_bag"],
|
||||
"roles": ["confused_visitor", "guest"]},
|
||||
"npc_crowd_c": {"layer": "coat", "hair": "short",
|
||||
"npc_crowd_c": {"layer": "coat", "hair": "short", "base": "hoodie",
|
||||
"gear": ["umbrella"], "roles": ["intoxicated", "guest"]},
|
||||
"npc_crowd_d": {"layer": "hood", "hair": "short", "build": "slim",
|
||||
"base": "worker", "drop": ["Worker_Vest", "Worker_Yellow"],
|
||||
"gear": ["cup"], "roles": ["tourist", "guest"]},
|
||||
}
|
||||
|
||||
@@ -127,6 +143,11 @@ ATTACH_BLENDER = {"head": (0.0, 0.0, 1.62), "hand": (0.27, -0.02, 0.78),
|
||||
ATTACH_GODOT = {"head": (0.0, 1.62, 0.0), "hand": (0.27, 0.78, 0.02),
|
||||
"back": (0.0, 1.10, 0.0), "chest": (0.0, 1.30, 0.0)}
|
||||
HEAD_BAND = (1.55, 1.81) # face/headgear height band in Godot metres
|
||||
# Head gear is authored at the procedural library's head size; the shipped
|
||||
# Quaternius roster has a 0.20 m head whose centre sits higher (see
|
||||
# `build_gear`). 0.64 = 0.201 / 0.315.
|
||||
HEAD_GEAR_SCALE = 0.64
|
||||
HEAD_GEAR_LIFT = 0.07
|
||||
FIT_HEAD_MATERIALS = ("skin",)
|
||||
FIT_CLOTHING_MATERIAL = "cloth"
|
||||
FIT_CLOTHING_BAND = (0.30, 0.52) # hip/skirt band; captures coat/dress flare
|
||||
@@ -276,13 +297,17 @@ def material(name, textured_solid=False):
|
||||
check_existing=True)
|
||||
albedo.image.colorspace_settings.name = "sRGB"
|
||||
links.new(albedo.outputs["Color"], bsdf.inputs["Base Color"])
|
||||
normal_tex = nodes.new("ShaderNodeTexImage")
|
||||
normal_tex.image = bpy.data.images.load(
|
||||
str(NPC_TEX_DIR / f"{name}_normal.png"), check_existing=True)
|
||||
normal_tex.image.colorspace_settings.name = "Non-Color"
|
||||
nmap = nodes.new("ShaderNodeNormalMap")
|
||||
links.new(normal_tex.outputs["Color"], nmap.inputs["Color"])
|
||||
links.new(nmap.outputs["Normal"], bsdf.inputs["Normal"])
|
||||
# The hair normal map was authored against the old cap UVs. Reusing it
|
||||
# on the skull-conforming MakeHuman cap produces large dark tangent
|
||||
# patches, so hair keeps its albedo but takes flat shading normals.
|
||||
if name != "hair":
|
||||
normal_tex = nodes.new("ShaderNodeTexImage")
|
||||
normal_tex.image = bpy.data.images.load(
|
||||
str(NPC_TEX_DIR / f"{name}_normal.png"), check_existing=True)
|
||||
normal_tex.image.colorspace_settings.name = "Non-Color"
|
||||
nmap = nodes.new("ShaderNodeNormalMap")
|
||||
links.new(normal_tex.outputs["Color"], nmap.inputs["Color"])
|
||||
links.new(nmap.outputs["Normal"], bsdf.inputs["Normal"])
|
||||
rough = {"skin": 0.6, "cloth": 0.86, "pants": 0.92,
|
||||
"accessory": 0.5, "hair": 0.7}[name]
|
||||
bsdf.inputs["Roughness"].default_value = rough
|
||||
@@ -550,22 +575,21 @@ def _skin_body(spec):
|
||||
"guard": 0.05}.get(layer, 0.0)
|
||||
leg = 1.0 + {"responder": 0.1, "dress": -0.1}.get(layer, 0.0)
|
||||
nodes = [ # (pos, radius_x, radius_y)
|
||||
((0, 0, 0.80), 0.168 * w, 0.116 * d), # 0 pelvis
|
||||
((0, 0, 0.96), 0.146 * w * bulk, 0.103 * d * bulk), # 1 waist
|
||||
((0, 0.0, 1.18), 0.168 * w * bulk, 0.114 * d * bulk), # 2 chest
|
||||
((0, 0.0, 1.355), 0.170 * w * bulk, 0.108 * d * bulk), # 3 upper chest
|
||||
# A slightly fuller neck base and a lower shoulder root give the
|
||||
# head/neck/shoulder line a trapezius slope instead of a thin post on
|
||||
# a hanger. The head and collar still sit above this band.
|
||||
((0, 0, 1.43), 0.074, 0.072), # 4 neck base
|
||||
((0, -0.005, 1.53), 0.060, 0.060), # 5 neck top
|
||||
((0, 0, 0.80), 0.158 * w, 0.112 * d), # 0 pelvis
|
||||
((0, 0, 0.96), 0.142 * w * bulk, 0.101 * d * bulk), # 1 waist
|
||||
((0, 0.0, 1.18), 0.155 * w * bulk, 0.110 * d * bulk), # 2 chest
|
||||
((0, 0.0, 1.335), 0.158 * w * bulk, 0.105 * d * bulk), # 3 upper chest
|
||||
# Narrow neck and a lower, sloped shoulder root. The old wide neck base
|
||||
# and horizontal shoulder shelf made the accepted head read as small.
|
||||
((0, 0, 1.42), 0.060, 0.060), # 4 neck base
|
||||
((0, -0.005, 1.53), 0.054, 0.054), # 5 neck top
|
||||
]
|
||||
edges = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]
|
||||
for s in (-1.0, 1.0):
|
||||
base = len(nodes)
|
||||
nodes += [
|
||||
((0.20 * s * w, 0, 1.37), 0.080 * t * sleeve, 0.074 * t * sleeve), # shoulder
|
||||
((0.27 * s * w, 0, 1.23), 0.058 * t * sleeve, 0.058 * t * sleeve), # upper arm
|
||||
((0.19 * s * w, 0, 1.345), 0.070 * t * sleeve, 0.068 * t * sleeve), # shoulder
|
||||
((0.27 * s * w, 0, 1.22), 0.054 * t * sleeve, 0.055 * t * sleeve), # upper arm
|
||||
((0.275 * s * w, 0.005, 1.06), 0.046 * t * sleeve, 0.048 * t * sleeve), # elbow
|
||||
((0.275 * s * w, -0.01, 0.78), 0.034 * t, 0.030 * t), # wrist
|
||||
]
|
||||
@@ -591,7 +615,7 @@ def _skin_body(spec):
|
||||
sub = obj.modifiers.new("Sub", "SUBSURF")
|
||||
sub.levels = 2
|
||||
dec = obj.modifiers.new("Decimate", "DECIMATE")
|
||||
dec.ratio = 0.26
|
||||
dec.ratio = 0.21
|
||||
dec.use_symmetry = True
|
||||
dec.symmetry_axis = "X"
|
||||
_apply_all(obj)
|
||||
@@ -716,6 +740,102 @@ def _hair_cap(name, mat, front_z, back_z, radius=0.163, center=(0, 0.008, 1.645)
|
||||
return _finish(_link(name, bm), mat, smooth=True, sharp_angle=70)
|
||||
|
||||
|
||||
def _hair_curve(style):
|
||||
"""Hairline endpoints and skull offset for a hair style."""
|
||||
if style == "buzz":
|
||||
return 1.734, 1.608, 1.035
|
||||
return 1.714, 1.588, 1.055
|
||||
|
||||
|
||||
def _trim_hidden_scalp(head, style):
|
||||
"""Delete the scalp that sits under the hair cap to free triangle budget.
|
||||
|
||||
The cap is a scaled copy of the skull, so any head face whose scaled
|
||||
vertices all fall outside the hairline sphere is fully covered. Keeping
|
||||
boundary-touching faces leaves a safety band under the hair rim; the open
|
||||
hole is never visible and the accepted face geometry is untouched.
|
||||
"""
|
||||
front_z, back_z, scale = _hair_curve(style)
|
||||
center = Vector((0.0, 0.0, 1.665))
|
||||
cutter_z = (front_z + back_z) * 0.5 - 0.396
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(head.data)
|
||||
|
||||
def covered(p):
|
||||
q = center + (p - center) * scale
|
||||
return (q.x * q.x + (q.y + 0.41) ** 2
|
||||
+ (q.z - cutter_z) ** 2) > 0.56 ** 2
|
||||
|
||||
doomed = [f for f in bm.faces if all(covered(v.co) for v in f.verts)]
|
||||
if doomed:
|
||||
bmesh.ops.delete(bm, geom=doomed, context="FACES")
|
||||
loose = [v for v in bm.verts if not v.link_faces]
|
||||
if loose:
|
||||
bmesh.ops.delete(bm, geom=loose, context="VERTS")
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
bm.to_mesh(head.data)
|
||||
bm.free()
|
||||
|
||||
|
||||
def _hair_from_head(source, style, mat_hair):
|
||||
"""Build the hair cap as a solid shell derived from the skull.
|
||||
|
||||
A separate hair ellipsoid cannot follow the broad MakeHuman crown, and an
|
||||
open, face-deleted copy produces a sawtooth hairline. Scaling the closed
|
||||
head shell outwards and boolean-cutting the face with a large front sphere
|
||||
gives a watertight cap whose curved hairline is smooth from every angle.
|
||||
"""
|
||||
front_z, back_z, scale = _hair_curve(style)
|
||||
cap = source.copy()
|
||||
cap.data = source.data.copy()
|
||||
cap.name = "HairCap"
|
||||
bpy.context.scene.collection.objects.link(cap)
|
||||
center = Vector((0.0, 0.0, 1.665))
|
||||
for v in cap.data.vertices:
|
||||
v.co = center + (v.co - center) * scale
|
||||
# The cutter is a big sphere in front of the face: its upper surface rises
|
||||
# toward the brow and falls toward the nape, which is the hairline curve.
|
||||
mid_z = (front_z + back_z) * 0.5
|
||||
cutter_z = mid_z - 0.396
|
||||
bpy.ops.mesh.primitive_uv_sphere_add(segments=32, ring_count=20, radius=0.56,
|
||||
location=(0.0, -0.41, cutter_z))
|
||||
cutter = bpy.context.object
|
||||
bpy.context.view_layer.objects.active = cap
|
||||
mod = cap.modifiers.new("Hairline", "BOOLEAN")
|
||||
mod.operation = "DIFFERENCE"
|
||||
mod.solver = "EXACT"
|
||||
mod.object = cutter
|
||||
bpy.ops.object.modifier_apply(modifier=mod.name)
|
||||
bpy.data.objects.remove(cutter, do_unlink=True)
|
||||
# The boolean result is still head-density. Hair needs silhouette, not
|
||||
# pores, and the character budget has to stay under 16k triangles.
|
||||
dec = cap.modifiers.new("HairReduce", "DECIMATE")
|
||||
dec.ratio = 0.34
|
||||
dec.use_collapse_triangulate = True
|
||||
bpy.ops.object.modifier_apply(modifier=dec.name)
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(cap.data)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
bm.to_mesh(cap.data)
|
||||
bm.free()
|
||||
cap.data.materials.clear()
|
||||
cap.data.materials.append(mat_hair)
|
||||
cap.data.shade_smooth()
|
||||
cap.data.set_sharp_from_angle(angle=math.radians(55))
|
||||
|
||||
parts = [cap]
|
||||
if style == "long":
|
||||
parts.append(sphere("HairBack", 0.095, (0, 0.060, 1.560), mat_hair,
|
||||
seg=14, rings=9, scale=(0.98, 0.55, 1.55)))
|
||||
for s in (-1.0, 1.0):
|
||||
parts.append(sphere("Lock", 0.048, (0.089 * s, -0.012, 1.585),
|
||||
mat_hair, seg=8, rings=6,
|
||||
scale=(0.55, 0.85, 2.05)))
|
||||
if style == "bun":
|
||||
parts.append(sphere("Bun", 0.060, (0, 0.100, 1.775), mat_hair,
|
||||
seg=12, rings=8, scale=(1.0, 0.9, 0.95)))
|
||||
return parts
|
||||
|
||||
def _hair(style, mat_hair, broad=False):
|
||||
parts = []
|
||||
if style == "buzz":
|
||||
@@ -968,11 +1088,12 @@ def _head_proto(mats, spec, subdiv=48):
|
||||
def _makehuman_head(mats, spec):
|
||||
"""MakeHuman head shape plus the project's eye/mouth/brow pieces.
|
||||
|
||||
The AI-judged head rework (2026-11-04) picked the decimated MakeHuman head
|
||||
over the displacement shell: it keeps the nose/chin/jaw landmarks at 1,573
|
||||
tris. The MakeHuman mesh has no eyeballs, so the project's sclera/iris/lid/
|
||||
brow pieces are added at the MakeHuman landmarks (eyes ≈ ±0.052, −0.160,
|
||||
1.664) for readability. Blender Z-up, face front −Y.
|
||||
The AI-judged head rework (2026-11-04) picked the MakeHuman head over the
|
||||
displacement shell: it keeps the nose/chin/jaw landmarks. Newer part builds
|
||||
carry their own embedded eye/iris/pupil/mouth geometry, so those materials
|
||||
are preserved; the older shell-only asset still gets the project's
|
||||
sclera/iris/lid/brow pieces added at the MakeHuman landmarks. Blender Z-up,
|
||||
face front −Y.
|
||||
"""
|
||||
skin, acc, hair = mats["skin"], mats["accessory"], mats["hair"]
|
||||
white = material("eye_white", textured_solid=True)
|
||||
@@ -991,9 +1112,32 @@ def _makehuman_head(mats, spec):
|
||||
if head is None:
|
||||
raise SystemExit(f"no head mesh in {src}")
|
||||
head.name = "HeadShell"
|
||||
# The decimated MakeHuman "hair" scalp slot reads as shaggy shards; collapse
|
||||
# the head to one skin slot so the join does not collide with the body's
|
||||
# cloth/pants slots (the project `_hair` cap provides hair later).
|
||||
# Newer MakeHuman part builds carry their own eye/mouth/brow materials and
|
||||
# geometry. Use those instead of adding the project's primitive face pieces
|
||||
# a second time; only the old shell-only asset falls through to that path.
|
||||
embedded = {m.name.split(".")[0] for m in head.data.materials if m}
|
||||
if "eye_white" in embedded:
|
||||
# Preserve the embedded iris/pupil/mouth materials; only map the
|
||||
# project contract slots onto the imported head.
|
||||
remap = {"skin": skin, "eye_white": white, "hair": hair}
|
||||
for index, mat in enumerate(list(head.data.materials)):
|
||||
if mat:
|
||||
target = remap.get(mat.name.split(".")[0])
|
||||
if target is not None:
|
||||
head.data.materials[index] = target
|
||||
# Bridge the 1.48-1.53 m neck gap between the MakeHuman cut and the
|
||||
# procedurally built torso. The plug sits inside both shells, so only
|
||||
# the visible band reads as neck and no seam ring reaches the surface.
|
||||
plug = cyl("NeckPlug", 0.062, 0.052, 0.095, (0, 0.002, 1.500), skin,
|
||||
seg=12, scale=(1.10, 1.0, 1.0))
|
||||
hair_style = "buzz" if spec["layer"] == "hood" else spec["hair"]
|
||||
hair_parts = _hair_from_head(head, hair_style, hair)
|
||||
_trim_hidden_scalp(head, hair_style)
|
||||
return [head, plug] + hair_parts
|
||||
|
||||
# The old decimated MakeHuman "hair" scalp slot reads as shaggy shards;
|
||||
# collapse the head to one skin slot so the join does not collide with the
|
||||
# body's cloth/pants slots (the project `_hair` cap provides hair later).
|
||||
head.data.materials.clear()
|
||||
head.data.materials.append(skin)
|
||||
# Clean the ragged neck cut left by the Head-group extraction: bisect below
|
||||
@@ -1229,7 +1373,8 @@ def build_character(name, head="face"):
|
||||
else:
|
||||
head_parts = _face(mats, spec)
|
||||
hair_style = spec["hair"] if spec["layer"] != "hood" else "buzz"
|
||||
head_parts += _hair(hair_style, mats["hair"])
|
||||
if head != "mh":
|
||||
head_parts += _hair(hair_style, mats["hair"])
|
||||
head = join(head_parts, "head")
|
||||
_auto_weights(head, layout, rigid=lambda p: "Head")
|
||||
|
||||
@@ -1309,6 +1454,312 @@ def build_character(name, head="face"):
|
||||
return [arm, mesh]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Quaternius roster (ADR-0017 / ADR-0018)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# The CC0 sources live under `blender_src/quaternius/` (gitignored for Godot via
|
||||
# the parent `.gdignore`) and are build inputs, not runtime assets. Every source
|
||||
# is 4 skinned meshes on the same 62-joint armature; the build joins them
|
||||
# **without welding** (a weld corrupts the skin weights), renames the upstream
|
||||
# materials onto the five tint slots, unwraps the degenerate source UVs and bakes
|
||||
# the arm-down rest. See docs/npc-model-sourcing.md for the measurements.
|
||||
|
||||
QUATERNIUS_SRC = SRC_DIR / "quaternius"
|
||||
|
||||
# Material -> contract slot per source. `"keep"` leaves the authored colour
|
||||
# (face, hair detail and hivis fabrics); anything not listed is kept too, so a
|
||||
# new upstream material shows up as an extra fixed-colour slot instead of
|
||||
# silently disappearing. The maps were derived from the per-material height bands
|
||||
# measured in docs/npc-model-sourcing.md.
|
||||
QUATERNIUS_SOURCES = {
|
||||
"casual": {
|
||||
"file": "Casual.glb",
|
||||
"slots": {"Skin": "skin", "White": "cloth", "Orange": "pants",
|
||||
"Grey": "accessory", "Hair_Blond": "hair",
|
||||
"Brown": "keep", "Hair_Brown": "keep"},
|
||||
"notes": "white tee + orange trousers + grey shoes; blond hair",
|
||||
},
|
||||
"hoodie": {
|
||||
"file": "Casual_Hoodie.glb",
|
||||
"slots": {"Skin": "skin", "Purple": "cloth", "LightBlue": "pants",
|
||||
"Hair": "hair", "White": "accessory",
|
||||
"Eye": "keep", "Eyebrows": "keep"},
|
||||
"notes": "hoodie + shorts + white shoes (the hoodie covers the shoe uppers)",
|
||||
},
|
||||
"worker": {
|
||||
"file": "Worker.glb",
|
||||
"slots": {"Skin": "skin", "LightBrown": "cloth", "Brown": "pants",
|
||||
"Brown2": "pants", "Grey": "accessory", "Black": "accessory",
|
||||
# The Worker's head is bare apart from a hard hat, so the
|
||||
# brows carry the hair tint and the slot keeps a surface.
|
||||
"Eyebrows": "hair",
|
||||
"Eye": "keep", "Moustache": "keep",
|
||||
"Worker_Vest": "keep", "Worker_Yellow": "keep"},
|
||||
"notes": "work shirt + trousers + hivis vest + hard hat",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _quaternius_slot_map(obj, slot_map, drop):
|
||||
"""Rename the upstream materials onto the contract slots.
|
||||
|
||||
Several upstream materials can share a slot (trousers + patches), so the
|
||||
polygons are repointed at one Blender material instead of relying on a
|
||||
rename that Blender would de-duplicate to `pants.001`.
|
||||
"""
|
||||
materials = [m for m in obj.data.materials if m is not None]
|
||||
dropped = {i for i, m in enumerate(materials) if m.name in drop}
|
||||
kept = []
|
||||
target = {}
|
||||
remap = {}
|
||||
for index, m in enumerate(materials):
|
||||
slot = slot_map.get(m.name, "keep")
|
||||
if slot == "keep":
|
||||
if m.name not in target:
|
||||
target[m.name] = len(kept)
|
||||
kept.append(m)
|
||||
remap[index] = target[m.name]
|
||||
continue
|
||||
if slot in target:
|
||||
remap[index] = target[slot]
|
||||
continue
|
||||
m.name = slot
|
||||
target[slot] = len(kept)
|
||||
kept.append(m)
|
||||
remap[index] = target[slot]
|
||||
# Faces that only exist for a dropped material go away with it.
|
||||
if dropped:
|
||||
doomed = [p.index for p in obj.data.polygons
|
||||
if p.material_index in dropped]
|
||||
if doomed:
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
bm.faces.ensure_lookup_table()
|
||||
bmesh.ops.delete(bm, geom=[bm.faces[i] for i in doomed],
|
||||
context="FACES")
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
# Loose vertices left behind by the helmet/vest would export as an
|
||||
# unreferenced blob, so drop them before the UV pass.
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(obj.data)
|
||||
loose = [v for v in bm.verts if not v.link_faces]
|
||||
if loose:
|
||||
bmesh.ops.delete(bm, geom=loose, context="VERTS")
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
# `materials.clear()` resets every polygon to slot 0, so record the source
|
||||
# indices first or the whole mesh collapses onto the first slot.
|
||||
source_index = [poly.material_index for poly in obj.data.polygons]
|
||||
obj.data.materials.clear()
|
||||
for m in kept:
|
||||
obj.data.materials.append(m)
|
||||
for poly, index in zip(obj.data.polygons, source_index):
|
||||
poly.material_index = remap.get(index, 0)
|
||||
|
||||
|
||||
def _bake_quaternius_rest(arm, mesh):
|
||||
"""Bake the T-pose into an arm-down rest pose.
|
||||
|
||||
This runs on the re-imported T-pose export, not on the raw upstream file.
|
||||
The order matters: pose, read the posed matrices, apply the Armature
|
||||
modifier, **reset the pose to identity**, write the rest matrices, re-add the
|
||||
modifier. Skipping the reset double-applies the arm-down (mesh width ±0.53 m
|
||||
instead of ±0.23 m); `bpy.ops.object.armature_apply()` in Blender 5.2
|
||||
deforms the mesh without baking the rest and is not usable here.
|
||||
"""
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
for side in ("L", "R"):
|
||||
pose_bone = arm.pose.bones.get("UpperArm." + side)
|
||||
if pose_bone is None:
|
||||
continue
|
||||
pose_bone.rotation_mode = "XYZ"
|
||||
# Local X swings the arm in the frontal plane. This is only symmetric on
|
||||
# *our* exported rest: the raw upstream file mirrors the bone rolls, so
|
||||
# the build always round-trips through `_quaternius_tpose_glb()` first.
|
||||
pose_bone.rotation_euler = (-math.pi / 2, 0, 0)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.update()
|
||||
for side in ("L", "R"):
|
||||
wrist = arm.pose.bones.get("Wrist." + side)
|
||||
if wrist is not None:
|
||||
head = arm.matrix_world @ wrist.head
|
||||
print(f" wrist.{side} at x={head.x:+.3f} y={head.y:+.3f} z={head.z:+.3f}")
|
||||
|
||||
fingers = ("Thumb", "Index", "Middle", "Ring", "Pinky")
|
||||
posed = {}
|
||||
for side in ("L", "R"):
|
||||
for bone in arm.data.bones:
|
||||
name = bone.name
|
||||
if not name.endswith(side):
|
||||
continue
|
||||
if name.startswith(("UpperArm", "LowerArm", "Wrist")) or name.startswith(fingers):
|
||||
posed[name] = arm.pose.bones[name].matrix.copy()
|
||||
|
||||
arm_modifier = next(m for m in mesh.modifiers if m.type == "ARMATURE")
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
bpy.ops.object.modifier_apply(modifier=arm_modifier.name)
|
||||
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="POSE")
|
||||
for pose_bone in arm.pose.bones:
|
||||
pose_bone.matrix_basis.identity()
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
bpy.context.view_layer.objects.active = arm
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
for name, matrix in posed.items():
|
||||
arm.data.edit_bones[name].matrix = matrix
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
modifier = mesh.modifiers.new("Armature", "ARMATURE")
|
||||
modifier.object = arm
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = mesh.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
xs = [v.co.x for v in evaluated.data.vertices]
|
||||
print(f" arm-down rest baked, deformed x {min(xs):.3f}..{max(xs):.3f}")
|
||||
|
||||
|
||||
def _export_preserving_skin(objs, category, name):
|
||||
"""Export a skinned GLB without touching the rest pose.
|
||||
|
||||
`export_apply=True` (what `export()` uses for the procedural library) bakes
|
||||
the evaluated deformation a second time and writes wrong inverse bind
|
||||
matrices, so Godot then renders the A-pose.
|
||||
"""
|
||||
path = asset_path(category, name)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
for o in bpy.context.selected_objects:
|
||||
o.select_set(False)
|
||||
for o in objs:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = objs[0]
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=str(path), export_format="GLB", use_selection=True,
|
||||
export_apply=False, export_animations=False, export_skins=True,
|
||||
export_yup=True, export_materials="EXPORT", export_image_format="AUTO")
|
||||
SRC_DIR.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=str(SRC_DIR / f"{name}.blend"),
|
||||
compress=True)
|
||||
return path
|
||||
|
||||
|
||||
def _quaternius_work_dir():
|
||||
path = ROOT / "tmp" / "npc-build"
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def _quaternius_tpose_glb(base, drop):
|
||||
"""Export (and cache) the joined, slot-mapped, unwrapped T-pose base.
|
||||
|
||||
The arm-down bake only behaves symmetrically on our own export, because the
|
||||
upstream file mirrors the bone rolls; this round trip normalises them. The
|
||||
result depends only on the source and the dropped materials, so it is cached
|
||||
for the run.
|
||||
"""
|
||||
key = base + ("-" + "-".join(sorted(drop)) if drop else "")
|
||||
path = _quaternius_work_dir() / f"tpose_{key}.glb"
|
||||
if path.is_file():
|
||||
return path
|
||||
source = QUATERNIUS_SOURCES[base]
|
||||
source_path = QUATERNIUS_SRC / source["file"]
|
||||
if not source_path.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"{source_path} is missing; see {QUATERNIUS_SRC / 'README.md'}")
|
||||
reset_scene()
|
||||
bpy.ops.import_scene.gltf(filepath=str(source_path))
|
||||
# The importer brings an Icosphere helper along; it is not part of the body
|
||||
# and has no UVs or weights.
|
||||
for o in list(bpy.context.scene.objects):
|
||||
if o.name.startswith("Icosphere"):
|
||||
bpy.data.objects.remove(o, do_unlink=True)
|
||||
meshes = [o for o in bpy.context.scene.objects if o.type == "MESH"]
|
||||
if not meshes:
|
||||
raise RuntimeError(f"{source_path} imported no skinned meshes")
|
||||
arm = next(o for o in bpy.context.scene.objects if o.type == "ARMATURE")
|
||||
|
||||
# Join without welding: remove_doubles merges vertices across weight groups
|
||||
# and the resulting skinning is wrong (see MEMORY.md).
|
||||
for o in bpy.context.scene.objects:
|
||||
o.select_set(False)
|
||||
for o in meshes:
|
||||
o.select_set(True)
|
||||
bpy.context.view_layer.objects.active = meshes[0]
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.object
|
||||
obj.name = "npc_base"
|
||||
obj.data.name = "npc_base"
|
||||
|
||||
_quaternius_slot_map(obj, source["slots"], drop)
|
||||
|
||||
# Upstream UVs are degenerate (every loop at 0,0), so an AO bake would write
|
||||
# an all-black map. Repack a real layout at a tight island margin (the
|
||||
# default 0.01 wastes most of the square on a dense body), then scale it to
|
||||
# fill the square so the 256-square maps keep a usable texel density.
|
||||
for o in bpy.context.selected_objects:
|
||||
o.select_set(False)
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.mesh.select_all(action="SELECT")
|
||||
bpy.ops.uv.smart_project(angle_limit=math.radians(66.0), island_margin=0.0005)
|
||||
bpy.ops.uv.average_islands_scale()
|
||||
bpy.ops.uv.pack_islands(margin=0.0015, rotate=True)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
uv_layer = obj.data.uv_layers.active
|
||||
loops = uv_layer.data
|
||||
us = [d.uv[0] for d in loops]
|
||||
vs = [d.uv[1] for d in loops]
|
||||
span = max(max(us) - min(us), max(vs) - min(vs))
|
||||
if span > 1e-6:
|
||||
scale = 0.98 / span
|
||||
low_u, low_v = min(us), min(vs)
|
||||
for d in loops:
|
||||
d.uv = ((d.uv[0] - low_u) * scale + 0.01,
|
||||
(d.uv[1] - low_v) * scale + 0.01)
|
||||
|
||||
# Only the five contract slots are tinted at runtime; make sure a source that
|
||||
# does not paint with one of them still declares it.
|
||||
present = {m.name for m in obj.data.materials if m}
|
||||
for slot in SLOTS:
|
||||
if slot not in present:
|
||||
obj.data.materials.append(bpy.data.materials.get(slot)
|
||||
or material(slot))
|
||||
|
||||
for o in bpy.context.scene.objects:
|
||||
o.select_set(False)
|
||||
arm.select_set(True)
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
bpy.ops.export_scene.gltf(
|
||||
filepath=str(path), export_format="GLB", use_selection=True,
|
||||
export_apply=False, export_animations=False, export_skins=True,
|
||||
export_yup=True, export_materials="EXPORT", export_image_format="AUTO")
|
||||
print(f" T-pose base {path.relative_to(ROOT)}")
|
||||
return path
|
||||
|
||||
|
||||
def build_quaternius_character(name):
|
||||
"""Bake one roster character from its CC0 Quaternius source."""
|
||||
spec = CHARACTERS[name]
|
||||
tpose = _quaternius_tpose_glb(spec["base"], spec.get("drop", []))
|
||||
reset_scene()
|
||||
bpy.ops.import_scene.gltf(filepath=str(tpose))
|
||||
arm = next(o for o in bpy.context.scene.objects if o.type == "ARMATURE")
|
||||
mesh = next(o for o in bpy.context.scene.objects if o.type == "MESH")
|
||||
mesh.name = "npc_base"
|
||||
mesh.data.name = "npc_base"
|
||||
_bake_quaternius_rest(arm, mesh)
|
||||
return [arm, mesh]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gear (static, attach-point space; see GEAR_ATTACH)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1362,13 +1813,14 @@ def build_gear(name):
|
||||
bevel=0.02, segs=2))
|
||||
p.append(box("Zip", (0.24, 0.005, 0.01), (0, 0.317, 0.05), M("steel")))
|
||||
for s in (-1.0, 1.0):
|
||||
# Strap path: over the shoulder, then down the chest outside the
|
||||
# torso front (Godot z≈0.21). The old chest/waist points sat at
|
||||
# z≈0.13 inside the body, so only thin tube fragments read as two
|
||||
# rigid black bars from the front.
|
||||
p.append(tube("Strap", [(0.09 * s, 0.13, 0.27), (0.11 * s, -0.02, 0.29),
|
||||
(0.12 * s, -0.06, 0.25),
|
||||
(0.14 * s, -0.19, 0.18), (0.145 * s, -0.19, -0.08)],
|
||||
# Strap path: over the shoulder, then down the chest. The chest points
|
||||
# are pulled in to x 0.12 / Godot z 0.15 so the straps hug the slimmer
|
||||
# Quaternius torso (ADR-0018) as well as the wider procedural one;
|
||||
# the earlier x 0.145 / z 0.19 path floated ~5 cm proud of the
|
||||
# Quaternius chest and read as two detached blue bars.
|
||||
p.append(tube("Strap", [(0.075 * s, 0.13, 0.27), (0.095 * s, -0.02, 0.29),
|
||||
(0.105 * s, -0.06, 0.25),
|
||||
(0.12 * s, -0.15, 0.18), (0.125 * s, -0.15, -0.08)],
|
||||
0.016, M("denim_bag"), seg=8, width_scale=1.35,
|
||||
thickness_scale=0.42))
|
||||
elif name == "shoulder_bag":
|
||||
@@ -1431,9 +1883,13 @@ def build_gear(name):
|
||||
rot=(math.radians(90), 0, 0)))
|
||||
p.append(box("Prism", (0.05, 0.05, 0.03), (0, -0.04, 0.095), M("plastic_black"),
|
||||
bevel=0.006))
|
||||
p.append(tube("Strap", [(-0.065, -0.04, 0.06), (-0.10, 0.0, 0.25), (0.0, 0.05, 0.42),
|
||||
(0.10, 0.0, 0.25), (0.065, -0.04, 0.06)], 0.006,
|
||||
M("plastic_black"), seg=5))
|
||||
# The hand attach point hangs off the wrist, so a full neck strap would
|
||||
# float beside the head instead of resting on the neck. A short wrist
|
||||
# loop keeps every hand item self-contained and follows the hand.
|
||||
p.append(tube("WristStrap", [(-0.045, -0.02, 0.02), (-0.05, -0.035, 0.075),
|
||||
(0.0, -0.045, 0.10), (0.05, -0.035, 0.075),
|
||||
(0.045, -0.02, 0.02)], 0.005, M("plastic_black"),
|
||||
seg=5, caps=False))
|
||||
elif name == "protest_sign":
|
||||
p.append(box("Pole", (0.03, 0.03, 1.1), (0, 0, 0.35), M("wood")))
|
||||
p.append(box("Board", (0.62, 0.012, 0.42), (0, -0.025, 0.82), M("cardboard"),
|
||||
@@ -1467,7 +1923,19 @@ def build_gear(name):
|
||||
else:
|
||||
raise KeyError(name)
|
||||
obj = join(p, name)
|
||||
if GEAR[name] == "hand": # origin = Hand proxy (wrist); grip 5 cm lower
|
||||
if GEAR[name] == "head":
|
||||
# Head gear is authored around the head *centre*. The shipped roster is
|
||||
# the Quaternius base (ADR-0018): its head is 0.20 m wide against the
|
||||
# procedural library's 0.315 m, and its centre sits 0.07 m higher, so the
|
||||
# head gear is scaled and lifted to match. The procedural fallback then
|
||||
# wears slightly small hats — cosmetic, and recorded in
|
||||
# docs/blender-assets.md.
|
||||
centre = Vector((0.0, 0.0, HEAD_GEAR_LIFT))
|
||||
obj.data.transform(
|
||||
Matrix.Translation(centre)
|
||||
@ Matrix.Diagonal((HEAD_GEAR_SCALE, HEAD_GEAR_SCALE,
|
||||
HEAD_GEAR_SCALE, 1.0)))
|
||||
elif GEAR[name] == "hand": # origin = Hand proxy (wrist); grip 5 cm lower
|
||||
obj.data.transform(Matrix.Translation((0, 0, -0.05)))
|
||||
uv_unwrap(obj)
|
||||
return [obj]
|
||||
@@ -1795,15 +2263,18 @@ def build_prop(name):
|
||||
# Build + render entry points (inside Blender)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_one(category, name):
|
||||
def _build_one(category, name, head="face", source="quaternius"):
|
||||
reset_scene()
|
||||
quaternius = (category == "npc" and source == "quaternius"
|
||||
and "base" in CHARACTERS[name])
|
||||
if category == "npc":
|
||||
objs = build_character(name)
|
||||
objs = (build_quaternius_character(name) if quaternius
|
||||
else build_character(name, head=head))
|
||||
elif category == "npc_gear":
|
||||
objs = build_gear(name)
|
||||
else:
|
||||
objs = build_prop(name)
|
||||
path = export(objs, category, name)
|
||||
path = (_export_preserving_skin if quaternius else export)(objs, category, name)
|
||||
print(f"wrote {path.relative_to(ROOT)}")
|
||||
return path
|
||||
|
||||
@@ -2040,7 +2511,8 @@ def audit_asset(category, name):
|
||||
nodes = gltf.get("nodes", [])
|
||||
bones = [nodes[j].get("name", "") for s in gltf.get("skins", [])
|
||||
for j in s.get("joints", [])]
|
||||
for b in BONES:
|
||||
required = QUATERNIUS_BONES if "UpperArm.L" in bones else BONES
|
||||
for b in required:
|
||||
if b not in bones:
|
||||
res["issues"].append(f"missing bone {b}")
|
||||
if not any(n.get("name") == "npc_base" and "mesh" in n for n in nodes):
|
||||
@@ -2202,7 +2674,13 @@ def analyse_inspection(stats: dict) -> dict:
|
||||
bones = list(stats.get("bones", []))
|
||||
materials = list(stats.get("materials", []))
|
||||
bones_ok = {b for b in BONES if b in bones}
|
||||
needed = set(BONES) | {"Hips", "Spine", "Neck"}
|
||||
# ADR-0018: a Quaternius-derived character keeps the 62-joint upstream
|
||||
# armature, so the required set is the joints `NpcAvatar` maps instead of the
|
||||
# 13-core skeleton.
|
||||
if "UpperArm.L" in bones:
|
||||
needed = set(QUATERNIUS_BONES)
|
||||
else:
|
||||
needed = set(BONES) | {"Hips", "Spine", "Neck"}
|
||||
|
||||
if not meshes:
|
||||
issues.append("no mesh objects")
|
||||
@@ -2362,7 +2840,7 @@ def inspect_glb(path, category=None) -> dict:
|
||||
return result
|
||||
|
||||
|
||||
def adapt_glb(path, out_path, target_tris=16000):
|
||||
def adapt_glb(path, out_path, target_tris=24000):
|
||||
"""Import an external GLB and adapt it toward the library contract.
|
||||
|
||||
Applies node transforms, grounds the feet (lowest vertex at y = 0), welds
|
||||
@@ -2698,7 +3176,8 @@ def write_manifest(results):
|
||||
if r["category"] == "npc":
|
||||
spec = CHARACTERS[r["name"]]
|
||||
entry.update({"layer": spec["layer"], "roles": spec["roles"],
|
||||
"tint_slots": SLOTS, "mesh_node": "npc_base"})
|
||||
"tint_slots": SLOTS, "mesh_node": "npc_base",
|
||||
"base": spec.get("base", "procedural")})
|
||||
elif r["category"] == "npc_gear":
|
||||
entry["attach"] = GEAR[r["name"]]
|
||||
else:
|
||||
@@ -2748,6 +3227,12 @@ def main(argv=None):
|
||||
for cmd in ("build", "render"):
|
||||
s = sub.add_parser(cmd)
|
||||
s.add_argument("--only", default="")
|
||||
if cmd == "build":
|
||||
s.add_argument("--head", default="face", choices=("face", "proto", "mh"),
|
||||
help="procedural character head source (default: face)")
|
||||
s.add_argument("--source", default="quaternius",
|
||||
choices=("quaternius", "procedural"),
|
||||
help="character mesh source (default: the Quaternius roster)")
|
||||
a = sub.add_parser("audit")
|
||||
a.add_argument("--json", action="store_true")
|
||||
fit = sub.add_parser("fit")
|
||||
@@ -2765,8 +3250,8 @@ def main(argv=None):
|
||||
ada.add_argument("--file", required=True,
|
||||
help="external GLB to adapt (absolute path; runs in Blender)")
|
||||
ada.add_argument("--out", required=True, help="where to write the adapted GLB")
|
||||
ada.add_argument("--target-tris", type=int, default=16000,
|
||||
help="decimate target (default 16000)")
|
||||
ada.add_argument("--target-tris", type=int, default=24000,
|
||||
help="decimate target (default 24000)")
|
||||
tex = sub.add_parser("texels", help="per-slot texel density of the character GLBs")
|
||||
tex.add_argument("--only", default="")
|
||||
tex.add_argument("--json", action="store_true")
|
||||
@@ -2838,7 +3323,10 @@ def main(argv=None):
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
for cat, name in _selected(args.only):
|
||||
(_build_one if args.cmd == "build" else _render_one)(cat, name)
|
||||
if args.cmd == "build":
|
||||
_build_one(cat, name, head=args.head, source=args.source)
|
||||
else:
|
||||
_render_one(cat, name)
|
||||
if args.cmd == "build":
|
||||
results = [audit_asset(c, n) for c, n in catalogue()]
|
||||
write_manifest([r for r in results if r["vertices"]])
|
||||
|
||||
@@ -90,5 +90,9 @@ echo ""
|
||||
echo "Artifacts:"
|
||||
ls -la build/*"${VERSION}"*
|
||||
echo ""
|
||||
echo "Checksums:"
|
||||
shasum -a 256 build/*"${VERSION}"* > "build/SHA256SUMS-${VERSION}.txt"
|
||||
cat "build/SHA256SUMS-${VERSION}.txt"
|
||||
echo ""
|
||||
echo "Upload to: https://git.allucanget.biz/zwitschi/security-game/releases"
|
||||
echo "See docs/RELEASING.md for the full release checklist."
|
||||
@@ -23,7 +23,7 @@ Contract (frozen in ADR-0013 and docs/blender-assets.md):
|
||||
(plus Hips/Spine/Neck as parents)
|
||||
* material slots: skin, cloth, pants, accessory, hair
|
||||
* neutral albedo + normal + packed ORM maps, albedo tinted at runtime
|
||||
* <= 16000 triangles / 16000 vertices, UVs in [0,1], feet at y ~= 0
|
||||
* <= 24000 triangles / 24000 vertices, UVs in [0,1], feet at y ~= 0
|
||||
* the 13 core bones are required; extra optional bones are allowed
|
||||
"""
|
||||
|
||||
@@ -45,8 +45,8 @@ BONES = [
|
||||
"Head", "HipL", "HipR", "KneeL", "KneeR",
|
||||
"ShoulderL", "ShoulderR", "ElbowL", "ElbowR", "HandL", "HandR",
|
||||
]
|
||||
MAX_TRIANGLES = 16000
|
||||
MAX_VERTICES = 16000
|
||||
MAX_TRIANGLES = 24000
|
||||
MAX_VERTICES = 24000
|
||||
TEX_SIZE = 256
|
||||
AI_DIR = TEX_DIR / "ai"
|
||||
# Base material slots that get an AI fabric albedo (`prepare` layers them over
|
||||
|
||||
@@ -78,8 +78,18 @@ def joint_positions(gltf):
|
||||
for child in node.get("children", []):
|
||||
walk(child, world, child_q)
|
||||
|
||||
# The Blender library exports the armature root as "Armature"; the Quaternius
|
||||
# sources call it "CharacterArmature". Resolve it from the skin instead of the
|
||||
# name so a rebuilt rig cannot silently return an empty joint map.
|
||||
skin_joints = set(gltf["skins"][0]["joints"]) if gltf.get("skins") else set()
|
||||
|
||||
def holds_joints(index):
|
||||
if index in skin_joints:
|
||||
return True
|
||||
return any(holds_joints(child) for child in nodes[index].get("children", []))
|
||||
|
||||
for root in roots:
|
||||
if nodes[root].get("name") == "Armature":
|
||||
if root in skin_joints or holds_joints(root):
|
||||
walk(root)
|
||||
return out
|
||||
|
||||
@@ -108,13 +118,39 @@ def mesh_vertices(gltf, binary):
|
||||
return out
|
||||
|
||||
|
||||
## ADR-0018: a Quaternius-derived character keeps its 62-joint armature, so the
|
||||
## 13-core joint names are aliased onto the upstream ones before measuring.
|
||||
QUATERNIUS_JOINTS = {
|
||||
"ShoulderL": "UpperArm.L", "ShoulderR": "UpperArm.R",
|
||||
"ElbowL": "LowerArm.L", "ElbowR": "LowerArm.R",
|
||||
"HandL": "Wrist.L", "HandR": "Wrist.R",
|
||||
"HipL": "UpperLeg.L", "HipR": "UpperLeg.R",
|
||||
"KneeL": "LowerLeg.L", "KneeR": "LowerLeg.R",
|
||||
"Spine": "Hips",
|
||||
}
|
||||
|
||||
|
||||
def _alias_joints(joints):
|
||||
"""Return a joint map that answers both rig namings."""
|
||||
out = dict(joints)
|
||||
for core, upstream in QUATERNIUS_JOINTS.items():
|
||||
if core not in out and upstream in joints:
|
||||
out[core] = joints[upstream]
|
||||
return out
|
||||
|
||||
|
||||
def _span(values):
|
||||
return max(values) - min(values) if values else 0.0
|
||||
|
||||
|
||||
def measure(name):
|
||||
_magic, _version, _length, _size, gltf, binary = _read_glb(asset_path("npc", name))
|
||||
joints = joint_positions(gltf)
|
||||
raw_joints = joint_positions(gltf)
|
||||
# ADR-0018: the Quaternius base ships its upstream (semi-realistic)
|
||||
# proportions, so the stylized guide band does not apply to it. The row still
|
||||
# reports every measurement and ratio.
|
||||
rig = "quaternius" if "UpperArm.L" in raw_joints else "core"
|
||||
joints = _alias_joints(raw_joints)
|
||||
verts = mesh_vertices(gltf, binary)
|
||||
ys = [p[1] for p, _bone, _material in verts]
|
||||
height = _span(ys)
|
||||
@@ -123,9 +159,10 @@ def measure(name):
|
||||
shoulder = [p for p, _bone, _material in verts
|
||||
if shoulder_y - 0.08 <= p[1] <= shoulder_y + 0.06]
|
||||
hip = [p for p, bone, material in verts
|
||||
if bone in ("HipL", "HipR") and material == "pants"]
|
||||
if bone in ("HipL", "HipR", "UpperLeg.L", "UpperLeg.R")
|
||||
and material == "pants"]
|
||||
feet = [p for p, bone, _material in verts
|
||||
if bone in ("KneeL", "KneeR") and p[1] < 0.2]
|
||||
if bone in ("KneeL", "KneeR", "LowerLeg.L", "LowerLeg.R") and p[1] < 0.2]
|
||||
head_h = _span([p[1] for p in head])
|
||||
head_w = _span([p[0] for p in head])
|
||||
shoulder_w = _span([p[0] for p in shoulder])
|
||||
@@ -165,6 +202,7 @@ def measure(name):
|
||||
}
|
||||
return {
|
||||
"character": name,
|
||||
"rig": rig,
|
||||
"measurements": {key: round(value, 3) for key, value in values.items()},
|
||||
"ratios": {key: round(value, 3) for key, value in ratios.items()},
|
||||
"checks": checks,
|
||||
|
||||
@@ -9,10 +9,10 @@ class CatalogueTests(unittest.TestCase):
|
||||
def test_contract_matches_npc_pipeline(self):
|
||||
self.assertEqual(ba.SLOTS, npc_pipeline.SLOTS)
|
||||
self.assertEqual(sorted(ba.BONES), sorted(npc_pipeline.BONES))
|
||||
# The character cap was raised to 16000/16000 for the head/face/hand
|
||||
# The character cap was raised to 24000/24000 for the MPFB body and the head
|
||||
# overhaul; the 13 core bones stay required and extra bones are optional.
|
||||
self.assertLessEqual(ba.CHAR_MAX_TRIS, 16000)
|
||||
self.assertLessEqual(ba.CHAR_MAX_VERTS, 16000)
|
||||
self.assertLessEqual(ba.CHAR_MAX_TRIS, 24000)
|
||||
self.assertLessEqual(ba.CHAR_MAX_VERTS, 24000)
|
||||
|
||||
def test_gear_uses_known_attach_points(self):
|
||||
for name, attach in ba.GEAR.items():
|
||||
@@ -56,12 +56,17 @@ class BuiltAssetTests(unittest.TestCase):
|
||||
report = {entry["character"]: entry for entry in ba.fit_report()}
|
||||
self.assertIn("npc_coat", report)
|
||||
coat = report["npc_coat"]
|
||||
self.assertAlmostEqual(coat["reference"]["head_width"], 0.315, places=2)
|
||||
self.assertAlmostEqual(coat["reference"]["clothing_radius"], 0.225, places=3)
|
||||
# Reference dimensions come from the shipped mesh, so the roster swap
|
||||
# (ADR-0018: the Quaternius base, 0.20 m head) moves them. The gear is
|
||||
# authored for the shipped roster, so the ratio checks below are what
|
||||
# must stay true; the absolute numbers are a drift guard.
|
||||
self.assertAlmostEqual(coat["reference"]["head_width"], 0.201, places=2)
|
||||
self.assertAlmostEqual(coat["reference"]["clothing_radius"], 0.19, places=3)
|
||||
sunglasses = next(row for row in coat["gear"] if row["name"] == "sunglasses")
|
||||
self.assertEqual(sunglasses["attach"], "head")
|
||||
self.assertLessEqual(sunglasses["width"],
|
||||
0.70 * coat["reference"]["head_width"])
|
||||
self.assertAlmostEqual(sunglasses["width_ratio"], 0.65, places=2)
|
||||
bag = next(row for row in coat["gear"] if row["name"] == "shoulder_bag")
|
||||
self.assertEqual(bag["attach"], "back")
|
||||
self.assertGreaterEqual(bag["x_extent"], 0.2)
|
||||
@@ -73,7 +78,7 @@ class BuiltAssetTests(unittest.TestCase):
|
||||
self.assertEqual(len(row["max"]), 3)
|
||||
self.assertIn("npc_dress", report)
|
||||
dress = report["npc_dress"]
|
||||
self.assertAlmostEqual(dress["reference"]["clothing_radius"], 0.26, places=3)
|
||||
self.assertAlmostEqual(dress["reference"]["clothing_radius"], 0.18, places=3)
|
||||
dress_bag = next(row for row in dress["gear"] if row["name"] == "shoulder_bag")
|
||||
self.assertGreater(dress_bag["x_clearance"], 0)
|
||||
|
||||
@@ -106,7 +111,9 @@ class TexelDensityTests(unittest.TestCase):
|
||||
def test_character_slots_report_density_and_area(self):
|
||||
report = ba.texel_density("npc_casual")
|
||||
self.assertEqual(report["resolution"], 256)
|
||||
self.assertEqual(set(report["slots"]), set(ba.SLOTS) | {"eye_white", "rubber"})
|
||||
# The five tint slots are the contract; rebuilds may add fixed-colour
|
||||
# detail materials (the Quaternius base keeps `Eye` and `Eyebrows`).
|
||||
self.assertLessEqual(set(ba.SLOTS), set(report["slots"]))
|
||||
for slot in ba.SLOTS:
|
||||
row = report["slots"][slot]
|
||||
self.assertGreater(row["triangles"], 0, slot)
|
||||
@@ -197,7 +204,7 @@ class InspectionTests(unittest.TestCase):
|
||||
def test_character_contract_failures_are_listed(self):
|
||||
stats = self.stats(mesh_nodes=["Armature"], skinned=False, unskinned=["Armature"],
|
||||
materials=["cloth"], bones=["Head"], meshes_without_uv=1,
|
||||
triangles=17000, split_vertices=17000,
|
||||
triangles=25000, split_vertices=25000,
|
||||
world_bounds={"min": [-0.3, -0.4, -0.2], "max": [0.3, 1.2, 0.2]})
|
||||
issues = "\n".join(ba.analyse_inspection(stats)["issues"])
|
||||
self.assertIn("no mesh node named npc_base", issues)
|
||||
@@ -205,8 +212,8 @@ class InspectionTests(unittest.TestCase):
|
||||
self.assertIn("missing material slot skin", issues)
|
||||
self.assertIn("mesh not skinned", issues)
|
||||
self.assertIn("mesh(es) without UVs", issues)
|
||||
self.assertIn(f"triangles 17000 > {ba.CHAR_MAX_TRIS}", issues)
|
||||
self.assertIn(f"glTF vertices 17000 > {ba.CHAR_MAX_VERTS}", issues)
|
||||
self.assertIn(f"triangles 25000 > {ba.CHAR_MAX_TRIS}", issues)
|
||||
self.assertIn(f"glTF vertices 25000 > {ba.CHAR_MAX_VERTS}", issues)
|
||||
self.assertIn("feet not on the ground", issues)
|
||||
self.assertIn("unexpected height", issues)
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ class NpcContractTests(unittest.TestCase):
|
||||
"ShoulderL", "ShoulderR", "ElbowL", "ElbowR",
|
||||
"HandL", "HandR"]:
|
||||
self.assertIn(bone, npc_pipeline.BONES)
|
||||
self.assertLessEqual(npc_pipeline.MAX_TRIANGLES, 16000)
|
||||
self.assertLessEqual(npc_pipeline.MAX_VERTICES, 16000)
|
||||
self.assertLessEqual(npc_pipeline.MAX_TRIANGLES, 24000)
|
||||
self.assertLessEqual(npc_pipeline.MAX_VERTICES, 24000)
|
||||
|
||||
def test_textures_exist_for_every_slot(self):
|
||||
for slot in npc_pipeline.SLOTS:
|
||||
|
||||
@@ -10,9 +10,19 @@ class NpcProportionTests(unittest.TestCase):
|
||||
self.assertEqual(len({row["character"] for row in rows}), 12)
|
||||
for row in rows:
|
||||
self.assertGreater(row["measurements"]["height"], 1.6)
|
||||
self.assertIn(row["rig"], ("core", "quaternius"))
|
||||
# Both rigs must resolve every measurement; a missing joint would
|
||||
# collapse a ratio to zero.
|
||||
for key, value in row["measurements"].items():
|
||||
self.assertGreater(value, 0.0, f"{row['character']} {key}")
|
||||
|
||||
def test_current_library_stays_inside_the_stylized_guide(self):
|
||||
for row in npc_proportions.report():
|
||||
# ADR-0018: the Quaternius base is accepted at its upstream
|
||||
# (semi-realistic) proportions, so the stylized band does not apply.
|
||||
# The measurement stays in the report for the face/silhouette gate.
|
||||
if row["rig"] != "core":
|
||||
continue
|
||||
for key, check in row["checks"].items():
|
||||
self.assertEqual(check["status"], "ok",
|
||||
f"{row['character']} {key}={check['value']}")
|
||||
|
||||