Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,23 @@ Uncooked UE5 Megascans import as LOD0 only. The importer decompresses the larges
## Execution Phases

#### Phase 1: Embedded WebP cooks
**Status:** NOT STARTED
**Status:** DONE 2026-10-03
**Files:** `packages/assets/src/passes/decode-image.ts`, `packages/assets/src/passes/model-textures.ts`; `packages/assets/__tests__/model-texture-pass.spec.ts`
- [ ] [local] A GLB with an `EXT_texture_webp` image cooks to KTX2 on a decoding target and to a capped PNG on the decoder-free path. No output still requires `EXT_texture_webp`. proof: red-green case in `pnpm exec vitest run packages/assets/__tests__/model-texture-pass.spec.ts`.
- [ ] [local] `lumen-hall` builds on web with `models: "none"` deleted, and its asset scenario passes. proof: `npx threenative build` in the game, then `node packages/playtest/dist/runner/cli.js playtests/assets.playtest.json --url <preview> --browser-recipe webgpu`.
- [x] [local] A GLB with an `EXT_texture_webp` image cooks to KTX2 on a decoding target and to a capped PNG on the decoder-free path. No output still requires `EXT_texture_webp`. proof: red-green case in `pnpm exec vitest run packages/assets/__tests__/model-texture-pass.spec.ts`. **Result 2026-10-03 (`5e5ec9829`):** red `TN_ASSETS_TEXTURE_CONTAINER … is not a PNG or JPEG` before; green 3/3 after (KTX2 mip 0 transcodes back to the source within 1, decoder-free capped PNG, unknown container still throws); file 26/26, `packages/assets` 474 passed. **Reopened 2026-10-03:** on `lumen-hall` the cook decodes the WebP but the `not-smaller` guard keeps the original (3× per prop at 1024²), so `lady.glb`/`sanctuary.glb` still require `EXT_texture_webp`. **Closed 2026-10-03:** retention is now conditioned on the container, not only on size — `UNIVERSAL_IMAGE_CONTAINERS` (PNG/JPEG) may keep authored bytes, WebP always converts to KTX2 (KTX2 target) or PNG (decoder-free), so `EXT_texture_webp` always leaves the output and the increase lands in the existing `bytes before -> after` row (+621% on `lady.glb`). Red: 519 -> 519 bytes retained with the extension still required; green: `image/ktx2` only, `extensionsRequired` `[EXT_meshopt_compression, KHR_mesh_quantization, KHR_texture_basisu]`, both GLBs re-verified in `lumen-hall`. `keepSmallerSource` moved from `true` to `"universal-containers"` in the model pass's cache key so a warm cook cannot keep shipping the old output. File 28/28, `packages/assets` 476 passed, `pnpm typecheck` and `pnpm lint` clean.
Implementation: `sharp` decodes WebP in `decode-image.ts` — the first rung of the decision above, already declared at `packages/assets/package.json:57`, so no dependency is added and no new binary is installed for a game (every template already carries `@gltf-transform/cli`, which drags the same native package). It loads in the pass's worker thread and stays external in `dist`. `compressEmbeddedTextures` disposes `EXT_texture_webp` once no image is left in it, because the glTF writer emits every attached extension. The rule that `decode-image.ts` "must never grow a native dependency" is changed here: this is a Node-only build-time package, and `sharp` is the only native thing in it. Spec: the `embedded EXT_texture_webp` block of `model-texture-pass.spec.ts`, with the KTX2 leg decoding mip 0 through the shipped `basis_transcoder`.
- [x] [local] `lumen-hall` builds on web with `models: "none"` deleted, and its asset scenario passes. proof: `npx threenative build` in the game, then `node packages/playtest/dist/runner/cli.js playtests/assets.playtest.json --url <preview> --browser-recipe webgpu`. Partly done 2026-10-03: `npx threenative build --target web` exits 0 with the installed `@threenative/assets` tarball and both WebP props cook (`lady.df65302d.glb` 1562 KB, `sanctuary.9353c235.glb` 2546 KB, `image/ktx2` only). The scenario itself was not run in this lane, so the box stays open. **Result 2026-10-03:** sandbox branch `prd485/lumen-hall-default-cook` (local worktree, engine tarballs from this branch): `npx threenative build --target web` exit 0, all 6 GLBs cooked (33.96 MB → 8.93 MB, -73.7%); `lady.glb`/`sanctuary.glb` now `image/ktx2`, no `EXT_texture_webp` (`c284f2a70`; lady +621% bytes, block-compressed on GPU). The first scenario run crashed in `ClusteredMesh.clone()` (cooked models carry a cluster DAG; three's `clone()` constructs with no arguments) — fixed in core `7ed40dbe5` with red-green specs. Then `playtests/assets.playtest.json` passes 4/4 on nvidia/turing WebGPU. Same-pose before (`models: "none"` + a temporary uncooked-budget override, same tarballs) vs after: 0.01% of pixels differ (mean abs 0.25). On the new engine `models: "none"` alone no longer builds lumen-hall (`TN_ASSETS_BUDGET_EXCEEDED` 68 MB > 64 MB). Known corner: a WebP that is not a whole number of 4×4 blocks keeps its bytes under the block-size guard.

#### Phase 2: The Fab tree cooks by default
**Status:** NOT STARTED
**Status:** PARTIAL
**Files:** `packages/assets/src/lod/eligibility.ts` only if the Hornbeam is declined; `fab-import-proof/threenative.config.ts` (sandbox)
- [ ] [local] `fab-import-proof` builds on web with `assets` reduced to `exclude`. The build report shows the Hornbeam's six embedded textures encoded and 0 uncooked bytes for its GLB. proof: `npx threenative build`, `dist.build-report.json`.
- [ ] [local] The cooked Hornbeam carries a `TN_discrete_lod` chain whose far rung is at most 10% of 755,677 triangles. proof: the build report's LOD row for the GLB.
- [ ] [local] The Hornbeam scenario passes on web and on native desktop from installed tarballs. proof: `playtests/hornbeam.playtest.json --browser-recipe webgpu` and `native-playtests/fab-import-native.playtest.json --target desktop`.
- [x] [local] `fab-import-proof` builds on web with `assets` reduced to `exclude`. The build report shows the Hornbeam's six embedded textures encoded and 0 uncooked bytes for its GLB. proof: `npx threenative build`, `dist.build-report.json`. **Result 2026-10-03:** sandbox branch `prd485/fab-import-default-cook` (local worktree, engine tarballs from this branch), `npx threenative build --target web` exit 0 in 163 s. Hornbeam 126,934,280 -> 26,990,484 B (-78.7%), 6 embedded PNGs -> KTX2 45,378,709 -> 16,382,827 B (GPU 134,217,726 -> 25,165,824 B, 3 ETC1S + 3 UASTC, 0 skipped), budget row `uncooked 0 bytes`, artifact 51,402,535 B. No `packages/` file needed changing.
- [x] [local] The cooked Hornbeam carries a `TN_discrete_lod` chain whose far rung is at most 10% of 755,677 triangles. proof: the build report's LOD row for the GLB. **Result 2026-10-03 (reopened box, closed):** cause `packages/assets/src/virtual/dag.ts:319` — the group fold simplifies each cluster group with `LockBorder` (`dag.ts:259`), and every edge of a disconnected alpha-cutout card is a border, so the first fold simplifies nothing, every level reports the source count and `stopReason: stalled`; the primitive was then `virtual-owned` (`lod/eligibility.ts:182`) and no ladder could take it. Fix `packages/assets/src/virtual/bake.ts`: the bake *declines* a primitive whose DAG stopped `stalled` while still holding >80% of its triangles (`VIRTUAL_BAKE_VERSION` 3 -> 4) and attaches no extension, so the primitive is unowned and the discrete ladder's card + terminal levels take it. Red on `virtual-pass.spec.ts` "hands cards it cannot reduce to the discrete ladder, which does reduce them" (16,384 MASK cards: `extensionsUsed` had `TN_virtual_geometry`, `lod.reasons ["virtual-owned"]`, far level 50%) and on the report line; green after (no `TN_virtual_geometry`, `lod.reasons []`, far level 1,638 = 5.0% of 32,768, `cellCoverage 1` on every level). Same lane, sandbox `prd485/fab-import-default-cook` with the repacked assets tarball: `npx threenative build --target web` exit 0, report row `lod …: 3 primitive(s) to 3 level(s), 755677 -> 38767 triangles, 7304028 payload bytes, 0 skipped` and `TN_virtual_geometry` gone from `extensionsUsed`. Per primitive: material 0 `225310 -> [113601, 56948, 11266]` (5.00%, absolute far error 0.331), material 1 `520018 -> [260988, 130580, 26001]` (5.00%, 0.253), material 2 `10349 -> [5190, 2595, 1500]` (14.49%, 0.211) — far total 38,767 = 5.13% of 755,677. Cooked GLB 26,990,484 -> 26,428,684 B (-561,800 B, the DAG payload). Known gap: `compile.ts` drops `entry.virtual` when it rebuilds the manifest entry, so the new `declined` count is in the pass summary and `formatModelSizes` but prints in neither CLI lane.
- [x] [local] The Hornbeam scenario passes on web and on native desktop from installed tarballs. proof: `playtests/hornbeam.playtest.json --browser-recipe webgpu` and `native-playtests/fab-import-native.playtest.json --target desktop`. **Web leg 2026-10-03:** `playtests/hornbeam.playtest.json` passes on nvidia/turing WebGPU (4 Fab imports, Hornbeam height in range, 5 textures, 755,677 LOD0 triangles near, 0 console/network/runtime errors). An earlier bridge-missing failure was the sandbox worktree lacking the gitignored `assets/fab/soul-cave` GLBs (engine failed closed naming the asset). **Native desktop leg 2026-10-03:** `native-playtests/fab-import-native.playtest.json --target desktop` exit 0, 6/6 (4 imports, 8 textured, 5 masked, movement 4.97 m, 0 errors), 690 frames, adapter `native:vulkan/nvidia/NVIDIA GeForce RTX 2080/turing`, packaged from the installed `@threenative/runtime-native` 0.3.4 prebuilt; screenshot shows the Hornbeam canopy. Its first red was stale counts in the scenario (written before the Hornbeam import), corrected in the game branch.

#### Phase 3: Android and the cold-agent path
**Status:** NOT STARTED
**Status:** PARTIAL
**Files:** `metahuman-lab` asset layout (sandbox); `packages/create-threenative/agent-docs/references/finding-assets.md`
- [ ] [local] `fab-import-proof` packages its cooked KTX2 and Meshopt output for Android V8 and renders the Hornbeam on the emulator. proof: `native-playtests/fab-import-native.playtest.json --target android` on the local emulator.
- [ ] [local] `metahuman-lab` serves `content/` from `assets/` through the cook with no `assets` block, and `face-controls.playtest.json` passes on web. proof: `npx threenative build` plus that scenario with `--browser-recipe webgpu`.
- [ ] [local] `finding-assets.md`'s Fab import step says the default cook handles imported packs and that `"none"` is not a fix. proof: `pnpm exec vitest run scripts/__tests__/primary-docs.spec.ts` and `pnpm check:docs`.
- [x] [local] `metahuman-lab` serves `content/` from `assets/` through the cook with no `assets` block, and `face-controls.playtest.json` passes on web. proof: `npx threenative build` plus that scenario with `--browser-recipe webgpu`. **Result 2026-10-03:** sandbox branch `prd485/metahuman-lab-cook` (local): 301 MB `public/content/` → `assets/content/`, no `assets` block, `npx threenative build --target web` exit 0, uncooked 0 bytes; `.dna`/`.strands.bin` copied through by manifest, 30 PNGs → KTX2. Two engine fixes the cook exposed, each red-green: deforming geometry keeps its vertex order (`d219c5e56`, the library `reorder` renumbered 24,321 of 24,408 head vertices the DNA/brow sidecars address) and its float32 metre positions (`f27cb2299`, quantization had moved the scale into the inverse bind matrices); `specimen.glb` 26.3 MB → 22.5 MB with all 821 morph targets. `playtests/face-controls.playtest.json` passes 15/15 on WebGPU, 0 console errors; the frame shows jaw/smile/blink/brow controls driving the face with hair and skin intact.
Build leg 2026-10-03: sandbox branch `prd485/metahuman-lab-cook` (local worktree, engine tarballs from this branch). `npx threenative build --target web` exit 0 in 189 s, `threenative.config.ts` still carries no `assets` block. The 301 MB moved from `public/content/` to `assets/content/`, and the hand-written vite middleware that served it verbatim (`specimenContentPlugin`) is gone: `ctx.assets.resolve` reads `public/assets.manifest.json` and every load asks for the cooked name. `.dna` and `.strands.bin` are copied through as `kind: "other"` byte for byte (`head.dna` 4,829,103 B, `hair.strands.bin` 85,202,464 B) — an extension no pass owns gets a manifest entry like any other file. `specimen.glb` cooks 26,267,896 -> 16,104,464 B (-38.7%): 5 embedded PNGs 7,801,033 -> 2,957,889 B (3 uastc/etc1s, `T_MH_BentNormal_AO` and `T_Eye_N` kept under the existing `not-smaller` guard), 101,518 triangles and all 821 LOD0 morph targets intact, `EXT_meshopt_compression` + `KHR_mesh_quantization` + `KHR_texture_basisu` on the output, and `lod.reasons ["deforming", "authored-lod"]` — a skinned morph-target head is PRD-486's subject, not this one. `shirt.glb` 50,367,028 -> 543,040 B (-98.9%), `body.glb` 1,215,524 -> 349,680 B, 30 PNGs to KTX2 (total 162,186,687 B from 314,889,680 B of source). Budget `uncooked 0 bytes` against the 64,000,000 default ceiling — first time this game had a manifest at all. The scenario was not run in this lane, so the box stays open: `node packages/playtest/dist/runner/cli.js metahuman-lab/playtests/face-controls.playtest.json --url http://127.0.0.1:5197 --server-command "pnpm exec vite preview --host 127.0.0.1 --port 5197 --strictPort" --browser-recipe webgpu`.
- [x] [local] `finding-assets.md`'s Fab import step says the default cook handles imported packs and that `"none"` is not a fix. proof: `pnpm exec vitest run scripts/__tests__/primary-docs.spec.ts` and `pnpm check:docs`. **Result 2026-10-03:** the Fab import step now states what the default cook does to a converted pack (embedded PNG and WebP to KTX2, quantization and Meshopt, a discrete LOD chain for a `MASK` primitive) with the measured Hornbeam numbers, and that `models: "none"`/`textures: "none"` are not a fix — they ship every source byte, count it all against `assets.budget.uncooked`, and are what makes the build fail with `TN_ASSETS_BUDGET_EXCEEDED`. Green: `primary-docs.spec.ts` 7/7, `check:docs` 2,229 links, `check-doc-links` + `sync-agent-docs` + `evidence-citations` 30/30. No template file changed, so no scaffold hash moved.
Loading
Loading