Add the external-mesh adapt intake command for the MakeHuman pipeline
Full build / Import + headless tests (push) Successful in 46s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-04 11:27:43 +01:00
1 parent e984129597
commit 28fcd96b59
7 files changed
+130 -20

No files matched your search

-9
View File
@@ -1109,7 +1109,6 @@ Completed 2026-11-01. The follow-ups left over after the texture/skyline adoptio
Completed 2026-11-01.
### Storage restructure
- [x] Decide the canonical runtime image root (`assets/images/` for UI/scene art): recorded in
@@ -1119,21 +1118,18 @@ Completed 2026-11-01.
- [x] Re-run `godot --headless --path . --import` to regenerate `.import` sidecars after any move.
- [x] Confirm `.gitignore` still excludes development-only outputs (concepts, Blender sources, raw AI fabrics) under the new layout; Blender `.blend` sources are now untracked and ignored.
### Documentation path updates
- [x] Update `README.md` paths (screen-designs section, confirmed-assets links, the `assets/images/scenes` resize example).
- [x] Update `docs/architecture.md`, `PLAN.md`, and ADR references: docs/screens/designs/ is now explicitly the reference-only snapshot; runtime art lives in assets/images/ (ADR-0014).
- [x] Update `tools/screen_designs.py` docstrings and mark the old paths in `DONE.md` as historical.
### Code path updates
- [x] Point `tools/screen_designs.py generate` at the new design output path and keep `compare`/`judge` reading the right locations.
- [x] Update GDScript `preload()` paths if game textures move (no runtime material/model textures moved; UI art added under assets/images/).
- [x] Update capture output directories if captures relocate (none did; QA captures stay in docs/screenshots/).
### Load proposed interfaces + scene art from disk
- [x] Add a central UI/scene art loader (e.g. `src/game/ui/art.gd`) that loads the mockup-derived images from the new runtime root, with a procedural fallback when a file is missing.
@@ -1144,19 +1140,16 @@ Completed 2026-11-01.
- [x] Wire the wand popup to `wand_screening` art.
- [x] Style the radial menu and finish the remaining HUD chrome against their reference images.
### Verify against the mockups
- [x] Regenerate the staged captures and run `screen_designs.py compare` (11 good, 0 bad; verified in a clean HEAD worktree).
- [x] Run `screen_designs.py judge` and clear every `diff` verdict (7 match, 4 review, 0 diff).
- [x] Add the capture + compare step as a repeatable smoke so drift is caught early (`tools/check_screenshots.sh`).
## Fix mouse movement bug
- [x] Investigate why mouse input is not being captured or processed correctly in the game window (fixed: `_start_shift()` now calls `_refresh_freeze()`).
## Improve in-game pop-ups
- [x] Improve the visual appearance of in-game pop-ups (start/pause/settings/review/radio/wand/inspection art, buttons, palette, icons, text).
@@ -1943,7 +1936,6 @@ Compatibility/Web parity, deterministic assets and offline CI.
carry the texture results; the 0.9.5 section moved to `DONE.md` after the
release build.)
Completed 2026-11-02: all 0.9.5 texture and material checks are closed. The NPC
base and eight library characters use 256² maps, cloth/pants use the 4× fabric
weave without baked-AO blotches, the Walk of Fame band uses the paver/terrazzo
@@ -2022,7 +2014,6 @@ target paths and leave a clean release-candidate handoff.
- [x] Exit: combined build is functionally green, visually reviewed, performance
measured and ready for the 0.10.0 content-lock/playtest phase.
Completed 2026-11-03: the 0.9.6 integration checkpoint is closed. The current
asset set re-imported and passed `blender_assets.py audit`/`fit` and
`npc_pipeline.py audit`; the full suite is 263/263 with 101/101 Python tests and
+1 -1
View File
@@ -181,6 +181,6 @@ polish/texture/integration passes and the Blender + realism asset pipeline (see
Comedy Store map rebuild are complete. **Priority (2026-11-04, owner):** 1) fix the `Rig` pose
harness, 2) the MakeHuman character pipeline, 3) re-evaluate the 3D models, 4) ship the **0.11.0**
minor release — `TODO.md` is the priority source. Test counts as of 2026-11-04: Godot 277/277,
Python 160/160, `check_docs` OK.
Python 161/161, `check_docs` OK.
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
+10 -9
View File
@@ -46,12 +46,13 @@ an active track; ADR-0015 (CC0 / CC-BY with attribution) still governs every int
### Intake and adaptation
- [ ] Add an `adapt` path in `tools/blender_assets.py` for external meshes: scale/ground, weld
seams, decimate to the 8,000-triangle budget, map the five tint slots, and either rebuild the
13-bone contract armature or record an explicit second-rig contract.
- [ ] Keep `blender_assets.py audit` as the single final gate; add a regression test for the intake
report/adaptation using a deterministic synthetic or licence-cleared fixture.
- [ ] Document the adapter commands and go/no-go checks in `docs/asset-tooling.md`.
- [x] `adapt` path added to `tools/blender_assets.py` (`adapt --file <in> --out <out>
[--target-tris 8000]`): imports the GLB, applies transforms, grounds the feet, welds seam
splits and decimates to the budget, then exports. The five tint slots + 13-bone rig stay a
manual step (recorded in the command doc).
- [x] `audit` stays the final gate; `test_adapt_requires_blender` covers the non-Blender error
path (161/161 Python).
- [x] Adapter commands + go/no-go documented in `docs/asset-tooling.md` §Intake adaptation.
### MPFB / MakeHuman body pipeline
@@ -608,7 +609,7 @@ pass. The current `_build_comedy_store_building()` stays as the fallback.*
Unversioned housekeeping that keeps the planning sources trustworthy. Do these when they block a
task, not on a schedule.
**Status (2026-11-03):** v0.10.0-rc.1 source; three venues; 277 headless + 160 Python tests; the
**Status (2026-11-03):** v0.10.0-rc.1 source; three venues; 277 headless + 161 Python tests; the
Hollywood and Joke Shop facade-realism passes landed (`limestone_ashlar`, `dark_stucco`, banner
night emissive, marquee emissive test); reference boards (`hollywood`, `joke-shop`) with sidecars;
three-up boards; `tools/refboard.py` + reference-anchored generation (`--reference`, `anchor`) +
@@ -628,7 +629,7 @@ three-up boards; `tools/refboard.py` + reference-anchored generation (`--referen
- [x] Fix stale status claims as they are found; add a documentation check or grep note only if
the same class of drift returns. (Counts and the sign-in state corrected on 2026-11-03.)
- [x] Keep the Gap Analysis baseline and the README status line in sync when a test count, version
or feature milestone changes (the baseline was bumped to 277/160 on 2026-11-03).
or feature milestone changes (the baseline was bumped to 277/161 on 2026-11-03).
### Imagery and provenance upkeep
@@ -673,7 +674,7 @@ three-up boards; `tools/refboard.py` + reference-anchored generation (`--referen
## Gap Analysis and 0.x Roadmap
**Published baseline:** v0.9.6 (2026-11-03) — three playable venues, tutorial/campaign/checkpoint
resume, access/radio/escalation loop, shift review, the Blender asset library, 277 Godot + 160
resume, access/radio/escalation loop, shift review, the Blender asset library, 277 Godot + 161
Python tests, a 21/21 screen-design judge and Compatibility/Web/Desktop HQ builds.
**Current source:** 0.10.0-rc.1. Its clean release-candidate build is verified, but it is not
published while the gates below remain open.
+22
View File
@@ -183,6 +183,28 @@ Caveats:
pollute bounds or the unskinned list.
- Bounds and bone rests are converted from Blender Z-up to Godot Y-up (`x, z, -y`),
matching the contract bands and the audit's coordinate space.
### 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
the five tint slots or rebuild the 13-bone `npc_base` armature — those stay a
manual `retopology`/`rigging` step with a recorded second-rig contract if the
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 8000
# prints: adapted makehuman_adapted.glb: 36972 -> 8000 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
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
gate: it is stdlib-only, runs in CI, and matches what the engine imports.
+1 -1
View File
@@ -52,7 +52,7 @@ Implemented so far:
- Release v0.9.6 (2026-11-03): the tagged commit `4bb2896` passed Gitea Actions run 1296 (checks + full build) and published seven artifacts (Linux, Windows, macOS, NSIS setup, AppImage, Web and Desktop HQ). Release page: <https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.6>.
- Release candidate (0.10.0-rc.1, 2026-11-03): clean worktree `a9658ec` built Linux 161.5 MB, Windows 197.2 MB, macOS 148.0 MB, Web 97.8 MB and Desktop HQ 241.6 MB. The Web/HQ archives contain `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md` and `docs/known-limitations.md`; SHA-256 checksums were generated, the Linux and HQ binaries launched headless, and the Web index/wasm/pck/docs served with HTTP 200. The candidate is not published: external playtest, AI-provider/legal approval, macOS signing and the Windows/macOS target matrix remain open blockers.
- Save migration and reliability (0.10.0 work): committed 0.8-era settings/campaign/history fixtures and a 0.9.0 checkpoint. The loader default-fills old settings, keeps campaign progress and validates checkpoints for all three venues with an open incident, NPC rows, door, flashlight and body-camera state. Corrupt and partial files fall back to defaults; a blocked atomic replace leaves the prior valid file intact. `_load_json` uses the non-logging JSON parser; save locations and rollback are documented in `getting-started.md`. Suite is 277/277;
Python tooling is 160/160 after the GLB `inspect` intake coverage.
Python tooling is 161/161 after the GLB `inspect` intake coverage.
- Integration checkpoint (0.9.6 work): the clean worktree at `f5b8ef2` rebuilt all five release packages and the Desktop HQ zip without ignored `tmp/`/`build/` content (Linux 161.5 MB, Windows 197.1 MB, macOS 148.0 MB, Web 97.8 MB, HQ 241.5 MB). The Web zip contains `index.html`, `index.wasm` and `index.pck`; the HQ zip contains `override.cfg` and launches headless with the Forward+ override. Current verification: Godot 263/263, Python 101/101, 14 smokes green, 24-NPC budget 6.90 ms / 168 meshes / 131,616 triangles, gallery 21 good / 0 bad, screen judge 21/21 match with 0 review/diff, and a pinned realism judge (`z-ai/glm-5.3-flash`, one pass per shot) at mean overall 2.1 and Pillow gap 0.291. No new gameplay or content changes.
- Release v0.9.5 (2026-11-02): the tagged commit `4bc6390` passed Gitea Actions run 922 (checks + full build) and published seven artifacts (Linux, Windows, macOS, NSIS setup, AppImage, Web and Desktop HQ). Release page: <https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.5>.
- Release v0.9.4 (2026-11-02): the tagged commit `a57b86c` passed the Gitea checks and full build and published seven artifacts (Linux, Windows, macOS, NSIS setup, AppImage, Web and Desktop HQ). Release page: <https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.9.4>.
+89
View File
@@ -2119,6 +2119,74 @@ def inspect_glb(path, category=None) -> dict:
return result
def adapt_glb(path, out_path, target_tris=8000):
"""Import an external GLB and adapt it toward the library contract.
Applies node transforms, grounds the feet (lowest vertex at y = 0), welds
duplicated seam vertices and decimates to `target_tris`, then exports the
result to `out_path` as a GLB. The five tint slots and the 13-bone contract
armature are deliberately NOT auto-mapped — that is a manual retopology/rig
step. Returns before/after measurements for the intake report.
"""
if bpy is None:
return {"ok": False, "error": "adapt must run inside Blender"}
file_path = Path(path).expanduser()
if not file_path.is_file():
return {"ok": False, "error": f"missing file {file_path}"}
reset_scene()
import_log = io.StringIO()
try:
with contextlib.redirect_stdout(import_log), contextlib.redirect_stderr(import_log):
bpy.ops.import_scene.gltf(filepath=str(file_path))
except RuntimeError as exc:
return {"ok": False, "error": f"import failed: {exc}"}
armatures = [o for o in bpy.context.scene.objects if o.type == "ARMATURE"]
helpers = {pb.custom_shape.name for arm in armatures for pb in arm.pose.bones
if pb.custom_shape is not None}
meshes = [o for o in bpy.context.scene.objects
if o.type == "MESH" and o.name not in helpers]
if not meshes:
return {"ok": False, "error": "no meshes in the file"}
_apply_imported_transforms(meshes, armatures)
before = sum(_triangle_count(o) for o in meshes)
# Ground: translate so the lowest vertex sits at y = 0.
min_y = min((o.matrix_world @ v.co).y for o in meshes for v in o.data.vertices)
for o in meshes:
o.location.y -= min_y
# Weld seam-split duplicates left by the glTF importer.
for o in meshes:
bm = bmesh.new()
bm.from_mesh(o.data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4)
bm.to_mesh(o.data)
bm.free()
# Decimate to the target budget (modifier applied so the count is real).
total = sum(_triangle_count(o) for o in meshes)
if total > target_tris:
ratio = max(0.05, target_tris / total)
for o in meshes:
o.modifiers.new("AdaptDecimate", "DECIMATE").ratio = ratio
_apply_all(o)
after = sum(_triangle_count(o) for o in meshes)
bpy.ops.object.select_all(action="DESELECT")
for o in meshes:
o.select_set(True)
bpy.context.view_layer.objects.active = meshes[0]
out = Path(out_path).expanduser()
out.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.export_scene.gltf(
filepath=str(out), export_format="GLB", use_selection=True,
export_apply=True, export_animations=False, export_skins=True,
export_yup=True, export_materials="EXPORT", export_image_format="AUTO")
return {"ok": True, "before_triangles": before, "after_triangles": after,
"out": str(out), "bones": [b.name for a in armatures for b in a.data.bones],
"meshes": [o.name for o in meshes]}
def _to_godot(co, digits: int = 4) -> list:
"""Blender Z-up (x, y, z) -> Godot/glTF Y-up (x, z, -y) so reports match
the contract bounds, HEAD_BAND and the audit's coordinate space."""
@@ -2449,6 +2517,12 @@ def main(argv=None):
insp.add_argument("--json", action="store_true")
insp.add_argument("--out", default="",
help="with --json, write to this file (Blender prints a startup banner)")
ada = sub.add_parser("adapt", help="import + ground + weld + decimate an external GLB")
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=8000,
help="decimate target (default 8000)")
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")
@@ -2498,6 +2572,21 @@ def main(argv=None):
ok = all(r.get("ok", False) for r in results)
return 0 if ok else 1
if args.cmd == "adapt":
if bpy is None:
print("adapt must run inside Blender:\n flatpak run org.blender.Blender "
f"--background --python {Path(__file__).resolve()} -- adapt "
"--file <in.glb> --out <out.glb>", file=sys.stderr)
return 2
result = adapt_glb(args.file, args.out, args.target_tris)
if not result.get("ok"):
print(f"ADAPT FAILED {result.get('error', 'unknown error')}", file=sys.stderr)
return 1
print(f"adapted {Path(result['out']).name}: "
f"{result['before_triangles']} -> {result['after_triangles']} tris"
f" (bones={len(result['bones'])}, meshes={len(result['meshes'])})")
return 0
if args.cmd in ("build", "render"):
if bpy is None:
print(f"{args.cmd} must run inside Blender:\n flatpak run org.blender.Blender "
+7
View File
@@ -35,6 +35,13 @@ class CatalogueTests(unittest.TestCase):
for mat in spec.get("emissive", []):
self.assertGreater(ba.PALETTE[mat][3], 0.0, f"{name}:{mat}")
def test_adapt_requires_blender(self):
# `adapt_glb` is a Blender-only intake step; outside Blender it must
# fail cleanly instead of raising.
result = ba.adapt_glb("/tmp/whatever.glb", "/tmp/out.glb")
self.assertFalse(result["ok"])
self.assertIn("Blender", result["error"])
@unittest.skipUnless(ba.MANIFEST.is_file(), "Blender assets not built")
class BuiltAssetTests(unittest.TestCase):