@developerz.ai/ts23d 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/CLAUDE.md +1 -1
- package/README.md +10 -5
- package/dist/{chunk-a8fxakcj.js → chunk-6b05mnmx.js} +2 -2
- package/dist/{chunk-kgvbpqqw.js → chunk-7rtfjh9f.js} +6 -5
- package/dist/chunk-7rtfjh9f.js.map +10 -0
- package/dist/{chunk-1ypfs7ga.js → chunk-8j4c28jt.js} +790 -276
- package/dist/chunk-8j4c28jt.js.map +46 -0
- package/dist/{chunk-wtqhq529.js → chunk-d1fv6hrm.js} +6 -6
- package/dist/{chunk-wtqhq529.js.map → chunk-d1fv6hrm.js.map} +1 -1
- package/dist/{chunk-gcyp9b4d.js → chunk-dbq4qf52.js} +362 -215
- package/dist/chunk-dbq4qf52.js.map +25 -0
- package/dist/{chunk-xnt2sdhf.js → chunk-drna2rny.js} +2 -2
- package/dist/{chunk-ravbb06q.js → chunk-dxvypr7y.js} +2 -2
- package/dist/{chunk-11v28977.js → chunk-f6yme003.js} +85 -71
- package/dist/{chunk-11v28977.js.map → chunk-f6yme003.js.map} +6 -6
- package/dist/{chunk-4ddzsqtc.js → chunk-hg7pf34j.js} +4 -3
- package/dist/{chunk-4ddzsqtc.js.map → chunk-hg7pf34j.js.map} +5 -5
- package/dist/{chunk-w1zyhj40.js → chunk-jq4e17tj.js} +2 -2
- package/dist/{chunk-kvcep7ft.js → chunk-y63c6zcv.js} +967 -218
- package/dist/chunk-y63c6zcv.js.map +53 -0
- package/dist/cli/args.d.ts +7 -1
- package/dist/cli/bin.js +66 -25
- package/dist/cli/bin.js.map +6 -5
- package/dist/cli/manifest.d.ts +5 -1
- package/dist/cli/texture-cache.d.ts +4 -0
- package/dist/core/definition.d.ts +7 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/export/credits.d.ts +4 -0
- package/dist/export/glb-options.d.ts +48 -0
- package/dist/export/glb.d.ts +5 -35
- package/dist/export/hierarchy.d.ts +23 -0
- package/dist/export/index.d.ts +2 -1
- package/dist/export/index.js +21 -7
- package/dist/export/index.js.map +1 -1
- package/dist/export/lod-coverage.d.ts +12 -0
- package/dist/export/metadata.d.ts +2 -0
- package/dist/export/version.d.ts +2 -2
- package/dist/grammar/index.js +2 -2
- package/dist/index.js +63 -17
- package/dist/index.js.map +1 -1
- package/dist/kinds/head-armour.d.ts +3 -7
- package/dist/kinds/head-bust.d.ts +3 -2
- package/dist/kinds/head-face.d.ts +3 -1
- package/dist/kinds/head-garb.d.ts +4 -0
- package/dist/kinds/head-mesh.d.ts +4 -0
- package/dist/kinds/head-mouth.d.ts +4 -0
- package/dist/kinds/head-params.d.ts +9 -1
- package/dist/kinds/head-tone.d.ts +10 -0
- package/dist/kinds/head-torso.d.ts +35 -0
- package/dist/kinds/index.js +6 -6
- package/dist/material/cc0.d.ts +4 -0
- package/dist/material/define.d.ts +34 -0
- package/dist/material/generator.d.ts +1 -1
- package/dist/material/index.d.ts +8 -3
- package/dist/material/index.js +39 -7
- package/dist/material/index.js.map +1 -1
- package/dist/material/library.d.ts +7 -0
- package/dist/material/material-def.d.ts +30 -0
- package/dist/material/paint/canvas.d.ts +7 -0
- package/dist/material/paint/default-sheet.d.ts +3 -1
- package/dist/material/paint/fabric.d.ts +4 -0
- package/dist/material/paint/image-strip.d.ts +4 -0
- package/dist/material/paint/kit.d.ts +5 -0
- package/dist/material/paint/metal.d.ts +3 -0
- package/dist/material/paint/orm.d.ts +1 -1
- package/dist/material/paint/strip.d.ts +1 -1
- package/dist/material/paint/types.d.ts +32 -2
- package/dist/material/palette.d.ts +2 -0
- package/dist/material/slots.d.ts +5 -0
- package/dist/material/texture-source.d.ts +34 -0
- package/dist/material/trim-sheet.d.ts +20 -4
- package/dist/neural/index.js +2 -2
- package/dist/passes/index.js +2 -2
- package/dist/primitives/index.js +3 -3
- package/dist/qa/index.js +2 -2
- package/dist/render/beauty.d.ts +1 -1
- package/dist/render/index.js +3 -3
- package/dist/render/lighting.d.ts +7 -0
- package/dist/render/shade.d.ts +1 -1
- package/dist/testing/index.js +5 -5
- package/docs/README.md +4 -0
- package/docs/architecture/gltf-capabilities.md +38 -1
- package/docs/architecture/materials.md +151 -0
- package/docs/idea/10-sculpting.md +166 -0
- package/docs/idea/11-materials-authoring.md +132 -0
- package/docs/idea/12-blender-bridge.md +136 -0
- package/docs/idea/13-asset-pipeline-research.md +89 -0
- package/docs/plans/roadmap.md +12 -0
- package/package.json +2 -1
- package/recipes/boxcar/README.md +7 -0
- package/recipes/boxcar/boxcar.ts +118 -34
- package/recipes/goblin_head/README.md +27 -14
- package/recipes/goblin_head/goblin_head.ts +11 -9
- package/recipes/human_head/README.md +8 -4
- package/recipes/human_head/human_head.ts +4 -3
- package/recipes/material_swatches/material_swatches.ts +11 -5
- package/recipes/orc_head/README.md +32 -20
- package/recipes/orc_head/orc_head.ts +9 -7
- package/src/cli/args.ts +16 -3
- package/src/cli/manifest.ts +6 -0
- package/src/cli/run.ts +33 -9
- package/src/cli/texture-cache.ts +24 -0
- package/src/core/asset.ts +1 -0
- package/src/core/definition.ts +7 -0
- package/src/core/errors.ts +6 -1
- package/src/export/credits.ts +25 -0
- package/src/export/glb-options.ts +53 -0
- package/src/export/glb.ts +100 -113
- package/src/export/hierarchy.ts +122 -0
- package/src/export/index.ts +17 -1
- package/src/export/lod-coverage.ts +71 -0
- package/src/export/metadata.ts +3 -0
- package/src/export/version.ts +1 -1
- package/src/kinds/head-armour.ts +103 -73
- package/src/kinds/head-bust.ts +49 -86
- package/src/kinds/head-face.ts +94 -74
- package/src/kinds/head-garb.ts +181 -0
- package/src/kinds/head-hair.ts +74 -33
- package/src/kinds/head-mesh.ts +43 -5
- package/src/kinds/head-mouth.ts +111 -0
- package/src/kinds/head-params.ts +11 -2
- package/src/kinds/head-presets.ts +30 -27
- package/src/kinds/head-tone.ts +52 -0
- package/src/kinds/head-torso.ts +230 -0
- package/src/kinds/head.ts +8 -3
- package/src/material/cc0.ts +41 -0
- package/src/material/define.ts +130 -0
- package/src/material/generator.ts +15 -8
- package/src/material/index.ts +34 -3
- package/src/material/library.ts +196 -0
- package/src/material/material-def.ts +84 -0
- package/src/material/paint/canvas.ts +11 -1
- package/src/material/paint/compose.ts +1 -1
- package/src/material/paint/decode-png.ts +19 -13
- package/src/material/paint/default-sheet.ts +40 -28
- package/src/material/paint/fabric.ts +63 -0
- package/src/material/paint/image-strip.ts +88 -0
- package/src/material/paint/kit.ts +37 -0
- package/src/material/paint/masks.ts +3 -1
- package/src/material/paint/metal.ts +109 -0
- package/src/material/paint/orm.ts +14 -1
- package/src/material/paint/planks.ts +49 -4
- package/src/material/paint/strip.ts +35 -29
- package/src/material/paint/types.ts +45 -2
- package/src/material/paint/validate.ts +19 -3
- package/src/material/palette.ts +3 -0
- package/src/material/slots.ts +18 -0
- package/src/material/texture-source.ts +86 -0
- package/src/material/trim-sheet.ts +79 -13
- package/src/render/beauty.ts +2 -2
- package/src/render/lighting.ts +26 -0
- package/src/render/shade.ts +10 -5
- package/dist/chunk-1ypfs7ga.js.map +0 -42
- package/dist/chunk-gcyp9b4d.js.map +0 -22
- package/dist/chunk-kgvbpqqw.js.map +0 -10
- package/dist/chunk-kvcep7ft.js.map +0 -43
- /package/dist/{chunk-a8fxakcj.js.map → chunk-6b05mnmx.js.map} +0 -0
- /package/dist/{chunk-xnt2sdhf.js.map → chunk-drna2rny.js.map} +0 -0
- /package/dist/{chunk-ravbb06q.js.map → chunk-dxvypr7y.js.map} +0 -0
- /package/dist/{chunk-w1zyhj40.js.map → chunk-jq4e17tj.js.map} +0 -0
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
# Orc warrior bust — original stylised design
|
|
2
2
|
|
|
3
3
|
## Identity
|
|
4
|
-
A brutish orc warlord, read from the 3/4 view by: **
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
A brutish orc warlord, read from the 3/4 view by: a **hunched brute silhouette** (trapezius sloping
|
|
5
|
+
from behind the ears to the shoulders, the neck nearly as wide as the head), a **massive
|
|
6
|
+
underbitten jaw** with **lower tusks rising outside the upper lip**, a **heavy brow shelf** (a
|
|
7
|
+
flattened bar with a glabella knot, scowling down at the centre) shadowing **small deep-set ember
|
|
8
|
+
eyes**, a **flat broad nose**, **cheekbone ridges** running back to swept **pointed ears**, a **bound
|
|
9
|
+
warrior's knot** falling to the nape, war paint, **pectorals and deltoids** on the bare chest, and
|
|
10
|
+
**spiked plate pauldrons** (a domed cap and a shingled lame, hollow shells with worn bright rims)
|
|
11
|
+
over a **fur mantle** — cut as a collectible bust on a turned plinth.
|
|
8
12
|
|
|
9
13
|
## Design spec (head units: 1 = chin to crown = 0.30 m, ≈ 1.3 × human)
|
|
10
14
|
| Feature | Value (units) | Metres | Note |
|
|
11
15
|
|---|---|---|---|
|
|
12
16
|
| chin → crown | 1.00 | 0.300 | the `height` param |
|
|
13
|
-
| overall bust L × W × H | — | 0.
|
|
14
|
-
| cranium width / depth / above eye line | 0.70 / 0.
|
|
17
|
+
| overall bust L × W × H | — | 0.405 × 0.660 × 0.587 | `real`, ±5 %; length includes the knot's tail |
|
|
18
|
+
| cranium width / depth / above eye line | 0.70 / 0.74 / 0.31 | 0.21 / 0.22 / 0.09 | low, sloped dome: the face is the mass |
|
|
15
19
|
| eye line | 0.56 | — | higher than human: the jaw is the mass |
|
|
16
|
-
| eyeball radius / spacing | 0.
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
20
|
+
| eyeball radius / spacing | 0.054 / 0.155 | 0.016 / pupils 0.093 apart | `measures` gate, ±2 % |
|
|
21
|
+
| brow ridge / overhang / scowl | 0.075 / 0.06 / 0.04 | | a shelf, not two bumps |
|
|
22
|
+
| jaw half-width at angle / chin | 0.40 / 1.3 | 0.120 | human 0.22; square rounded-box chin |
|
|
23
|
+
| muzzle (prognathism) / underbite / chin drop | 0.10 / 0.10 / 0.10 | | lower lip thickens with the underbite |
|
|
24
|
+
| tusks length / base radius | 0.22 / 0.055 | 0.066 / 0.017 | 3 cone segments from the lower lip, flared |
|
|
25
|
+
| neck radius | 0.38 | 0.114 | |
|
|
26
|
+
| shoulders (half-span) / mass | 1.00 / 1 | 0.300 | `bust.mass` 1 = brute: torso lifted (hunch), thick traps |
|
|
27
|
+
| plinth | 0.17 + hunch | ≈ 0.08 | disc + socle; cut faces painted plinth stone |
|
|
22
28
|
|
|
23
29
|
## Palette (sRGB)
|
|
24
30
|
| Role | Hex |
|
|
25
31
|
|---|---|
|
|
26
|
-
| skin / shade / lips | `#
|
|
32
|
+
| skin / shade / lips | `#5c8034` / `#33501e` / `#4f6f30` (painted tone: crevices darker, tops warmer) |
|
|
27
33
|
| eyes: sclera / iris (+ emissive glow `#ff7a1a` × 2.5) | `#8a5a1a` / `#ffb030` |
|
|
28
34
|
| tusks + bone spikes | `#e2d6b0` |
|
|
29
|
-
| hair (
|
|
35
|
+
| hair (warrior's knot) / fur mantle (strands alternate ×0.72) | `#231a14` / `#5e4a34` |
|
|
30
36
|
| war paint (cheek claw marks, crest stripe, chin stripes) | `#8a1c12` |
|
|
31
|
-
| pauldron iron (
|
|
37
|
+
| pauldron iron (cap ×1, lame ×0.85, worn rims ×1.3, rivets ×1.35, crest ×0.75) / leather baldric | `#55524d` / `#5a3a22` |
|
|
38
|
+
| plinth / cut faces | `#6f6c6c` (linear 0.16, 0.15, 0.15) / ×1.3 |
|
|
32
39
|
|
|
33
40
|
## Parts the engine needs
|
|
34
41
|
| Part | Pivot | Motion |
|
|
@@ -43,8 +50,10 @@ along the lips, the marionette lines and under the chin. Its cut faces are paint
|
|
|
43
50
|
when it opens; it laps 0.75 cells over the head so no gap shows.
|
|
44
51
|
|
|
45
52
|
## LODs
|
|
46
|
-
Sculpt cell = 2 % / 4.5 % / 14 % of head height. LOD0/1 bake vertex AO across all
|
|
47
|
-
|
|
53
|
+
Sculpt cell = 2 % / 4.5 % / 14 % of head height (bust 1.6 ×). LOD0/1 bake vertex AO across all
|
|
54
|
+
parts (16/10 rays); every LOD carries the painted tone (cavity, warm top, cool under). LOD1/2 drop
|
|
55
|
+
the teeth and tongue; LOD2 drops warts, wiry tufts and the AO bake, floors ear/tusk/nose radii at
|
|
56
|
+
about a cell, makes the pauldron plates solid and the plinth one drum so nothing opens or floats.
|
|
48
57
|
|
|
49
58
|
## References (stylised anatomy, not likeness)
|
|
50
59
|
| Topic | Link | Used for |
|
|
@@ -52,7 +61,8 @@ warts, wiry tufts and the AO bake, and floors ear thickness at 0.7 cells so ears
|
|
|
52
61
|
| Loomis head: eye line at half height, brow/nose/chin thirds | https://finearttutorials.com/guide/loomis-method/ | human eye line 0.5, nose base 0.32, mouth 0.22 |
|
|
53
62
|
| Proportions of the head (ears between brow and nose base, head ≈ 5 eyes wide) | https://artyfactory.com/portraits/pencil-portraits/proportions-of-a-head.html | ear placement, eye size vs head width |
|
|
54
63
|
| Primary → secondary → tertiary forms (block brow/cheek/jaw first, then cut lids and folds) | https://www.creativebloq.com/how-to/how-to-sculpt-and-pose-a-cartoon-head-in-zbrush | the edit order in `head-face.ts` |
|
|
55
|
-
| Orc: widen head and jawline to break human proportion | https://www.creativebloq.com/3d/how-create-orc-zbrush-61411834 | orc jaw width 0.
|
|
64
|
+
| Orc: widen head and jawline to break human proportion; sculpt bony forms (cheekbones) early | https://www.creativebloq.com/3d/how-create-orc-zbrush-61411834 | orc jaw width 0.40 vs human 0.22, zygomatic ridge |
|
|
65
|
+
| Stylised bust method: primary → secondary → tertiary shapes | https://cgzen.gumroad.com/l/stylized-bust | torso masses before gear |
|
|
56
66
|
| Stylised orc head study (silhouette, form language) | https://blenderartists.org/t/orc-head-study-stylized-characters/1655542 | massing check |
|
|
57
67
|
| Stylised hair: large clumps first, hairline → crown, one primary flow direction | https://gamesartist.co.uk/curly-hair-ls/ · https://www.cgchannel.com/2023/03/tutorial-creating-stylized-hairstyles-using-zbrush | `swept` locks in head-hair.ts |
|
|
58
68
|
| Dreams (Media Molecule, Evans, SIGGRAPH 2015): sculptures as ordered SDF edits + smooth-min | https://www.mediamolecule.com/blog/article/siggraph_2015 · slides https://advances.realtimerendering.com/s2015/AlexEvans_SIGGRAPH-2015-sml.pdf | the whole approach |
|
|
@@ -62,5 +72,7 @@ hand-painted stylised fantasy" is the idiom (exaggerated primary forms, generous
|
|
|
62
72
|
vertex colour with baked AO), not a specific source.
|
|
63
73
|
|
|
64
74
|
## Known gaps
|
|
65
|
-
Pauldron
|
|
66
|
-
noise-broken clumps; no skin pores
|
|
75
|
+
Pauldron plates are smooth shells (no hammered dents or scratches); fur strands are cones, not
|
|
76
|
+
noise-broken clumps; no skin pores or wrinkle noise; the jaw part's seam still shows as a faint
|
|
77
|
+
line round the chin up close; the bust's U-cut leaves the belly reading slightly pear-shaped from
|
|
78
|
+
dead front.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Stylised orc warrior bust — an original design in the chunky hand-painted fantasy idiom: massive
|
|
2
|
-
// underbitten jaw with lower tusks, heavy
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// underbitten jaw with lower tusks rising outside the upper lip, a heavy brow shelf over small ember
|
|
3
|
+
// eyes, flat broad nose, swept ears, a bound warrior's knot, war paint; a hunched brute torso
|
|
4
|
+
// (trapezius from ear to shoulder, pectorals, deltoids) under spiked plate pauldrons and a fur
|
|
5
|
+
// mantle, cut as a collectible bust on a turned plinth. The `head` kind sculpts it (race preset
|
|
6
|
+
// `orc`, chin to crown 0.30 m); this file is the spec.
|
|
5
7
|
// Design dossier: recipes/orc_head/README.md.
|
|
6
8
|
|
|
7
9
|
import { defineAsset, head, hex } from '@developerz.ai/ts23d';
|
|
@@ -10,15 +12,15 @@ export const orc_head = defineAsset({
|
|
|
10
12
|
name: 'orc_head',
|
|
11
13
|
kind: 'character',
|
|
12
14
|
description:
|
|
13
|
-
'Stylised orc warrior bust: underbite and lower tusks, heavy brow, ember eyes,
|
|
15
|
+
'Stylised orc warrior bust on a plinth: underbite and lower tusks, heavy brow, ember eyes, warrior knot, hunched trapezius, spiked plate pauldrons, fur mantle',
|
|
14
16
|
tags: ['character', 'fantasy', 'orc', 'bust', 'sculpt'],
|
|
15
17
|
license: 'CC0-1.0',
|
|
16
18
|
sources: ['recipes/orc_head/README.md (design dossier: proportions, palette, references)'],
|
|
17
19
|
// Design spec, not a real-world one: overall bust size and eye spacing from the dossier.
|
|
18
20
|
real: {
|
|
19
|
-
length: 0.
|
|
20
|
-
width: 0.
|
|
21
|
-
height: 0.
|
|
21
|
+
length: 0.405,
|
|
22
|
+
width: 0.66,
|
|
23
|
+
height: 0.587,
|
|
22
24
|
measures: [
|
|
23
25
|
{
|
|
24
26
|
label: 'eye spacing (pupil to pupil)',
|
package/src/cli/args.ts
CHANGED
|
@@ -17,6 +17,12 @@ export interface Args {
|
|
|
17
17
|
readonly sheetSize: 512 | 1024 | 2048;
|
|
18
18
|
/** Which detail levels to build: all three by default, or e.g. only the hero mesh. */
|
|
19
19
|
readonly lods: readonly (0 | 1 | 2)[];
|
|
20
|
+
/**
|
|
21
|
+
* How LODs land on disk: `split` lodN.glb per level · `single` one <asset>.glb with MSFT_lod
|
|
22
|
+
* alternates · `both` (default): split files import everywhere, the single file is for
|
|
23
|
+
* MSFT_lod-aware loaders and one-file pipelines.
|
|
24
|
+
*/
|
|
25
|
+
readonly lodFiles: 'split' | 'single' | 'both';
|
|
20
26
|
/** Beauty-render camera and size (hero, review, and build's hero.png); unset = defaults. */
|
|
21
27
|
readonly render: {
|
|
22
28
|
readonly width?: number;
|
|
@@ -42,8 +48,11 @@ flags:
|
|
|
42
48
|
front, default -35 (front-left 3/4) · --el <deg> elevation, default 18 · --fov <deg> 30
|
|
43
49
|
--sheet <mode> external (default): one shared trim_sheet_{albedo,normal,orm}.png in <out>/,
|
|
44
50
|
referenced by every GLB · embed: self-contained GLBs · none: vertex colour only
|
|
45
|
-
--sheet-size n 512 | 1024 (default)
|
|
51
|
+
--sheet-size n 512 | 1024 | 2048 (default)
|
|
46
52
|
--lods <list> which detail levels to build: 0,1,2 (default) · 0 = hero only · 0,2 = near + far
|
|
53
|
+
--lods <mode> how they are written: both (default) = lod0/1/2.glb + <asset>.glb · split = lodN.glb
|
|
54
|
+
only · single = <asset>.glb only (LOD0 in the scene, LOD1/2 as MSFT_lod alternates
|
|
55
|
+
with screen-coverage switch hints; viewers without MSFT_lod show LOD0 only)
|
|
47
56
|
--json machine-readable output`;
|
|
48
57
|
|
|
49
58
|
export function parseArgs(argv: readonly string[]): Args {
|
|
@@ -53,8 +62,9 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
53
62
|
let animations = true;
|
|
54
63
|
let shot = true;
|
|
55
64
|
let sheet: Args['sheet'] = 'external';
|
|
56
|
-
let sheetSize: Args['sheetSize'] =
|
|
65
|
+
let sheetSize: Args['sheetSize'] = 2048;
|
|
57
66
|
let lods: Args['lods'] = [0, 1, 2];
|
|
67
|
+
let lodFiles: Args['lodFiles'] = 'both';
|
|
58
68
|
const render: {
|
|
59
69
|
width?: number;
|
|
60
70
|
height?: number;
|
|
@@ -81,6 +91,8 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
81
91
|
else if (a === '--sheet') {
|
|
82
92
|
const m = argv[++i];
|
|
83
93
|
sheet = m === 'embed' || m === 'none' ? m : 'external';
|
|
94
|
+
} else if (a === '--lods' && /^(split|single|both)$/.test(argv[i + 1] ?? '')) {
|
|
95
|
+
lodFiles = argv[++i] as Args['lodFiles'];
|
|
84
96
|
} else if (a === '--lods') {
|
|
85
97
|
const picked = (argv[++i] ?? '')
|
|
86
98
|
.split(',')
|
|
@@ -89,7 +101,7 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
89
101
|
lods = picked.length ? [...new Set(picked)].sort() : lods;
|
|
90
102
|
} else if (a === '--sheet-size') {
|
|
91
103
|
const n = Number(argv[++i]);
|
|
92
|
-
sheetSize = n === 512 || n ===
|
|
104
|
+
sheetSize = n === 512 || n === 1024 ? n : 2048;
|
|
93
105
|
} else if (a === '--size') Object.assign(render, parseSize(argv[++i] ?? ''));
|
|
94
106
|
else if (a === '--az') render.azDeg = num(a, argv[++i]);
|
|
95
107
|
else if (a === '--el') render.elDeg = num(a, argv[++i]);
|
|
@@ -107,6 +119,7 @@ export function parseArgs(argv: readonly string[]): Args {
|
|
|
107
119
|
sheet,
|
|
108
120
|
sheetSize,
|
|
109
121
|
lods,
|
|
122
|
+
lodFiles,
|
|
110
123
|
render,
|
|
111
124
|
};
|
|
112
125
|
}
|
package/src/cli/manifest.ts
CHANGED
|
@@ -13,11 +13,15 @@ export interface AssetManifest {
|
|
|
13
13
|
readonly tags: readonly string[];
|
|
14
14
|
readonly files: {
|
|
15
15
|
readonly lods: readonly string[];
|
|
16
|
+
/** All built LODs in one GLB (MSFT_lod), when `--lods single|both`. */
|
|
17
|
+
readonly single?: string;
|
|
16
18
|
readonly hero?: string;
|
|
17
19
|
readonly sheet?: string;
|
|
18
20
|
readonly thumb?: string;
|
|
19
21
|
};
|
|
20
22
|
readonly triangles: readonly number[];
|
|
23
|
+
/** MSFT_screencoverage written into `files.single`: where each LOD hands over (last = cull). */
|
|
24
|
+
readonly lodCoverage?: readonly number[];
|
|
21
25
|
readonly drawCalls: readonly number[];
|
|
22
26
|
readonly pivots: readonly { name: string; parent: string | null; motion?: string }[];
|
|
23
27
|
readonly emitters: readonly { name: string; kind: string; when: string }[];
|
|
@@ -42,6 +46,7 @@ export function manifestOf(
|
|
|
42
46
|
lods: readonly CompiledAsset[],
|
|
43
47
|
files: AssetManifest['files'],
|
|
44
48
|
findings: readonly Finding[],
|
|
49
|
+
lodCoverage?: readonly number[],
|
|
45
50
|
): AssetManifest {
|
|
46
51
|
const top = lods[0];
|
|
47
52
|
const merged = top ? mergeToPivots(top) : null;
|
|
@@ -52,6 +57,7 @@ export function manifestOf(
|
|
|
52
57
|
tags: asset.tags ?? [],
|
|
53
58
|
files,
|
|
54
59
|
triangles: lods.map(tris),
|
|
60
|
+
...(lodCoverage?.length ? { lodCoverage } : {}),
|
|
55
61
|
// One draw per merged mesh, glowing parts in their own emissive layer (as the GLB writes them).
|
|
56
62
|
drawCalls: lods.map(
|
|
57
63
|
(l) =>
|
package/src/cli/run.ts
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
// Command dispatch for `t3d`. Pure function of argv → exit code, so it is testable in-process.
|
|
2
2
|
// Output layout, one folder per asset (stable paths agents and pipelines can rely on):
|
|
3
|
-
// <out>/<asset>/lod0.glb lod1.glb lod2.glb hero.png thumb.png
|
|
3
|
+
// <out>/<asset>/lod0.glb lod1.glb lod2.glb <asset>.glb (all LODs, MSFT_lod) hero.png thumb.png
|
|
4
|
+
// sheet.png manifest.json
|
|
4
5
|
// <out>/<asset>/review/… (t3d review) · <out>/index.json — every asset built, with its QA verdict
|
|
5
6
|
|
|
6
7
|
import { mkdir } from 'node:fs/promises';
|
|
7
8
|
import { join } from 'node:path';
|
|
8
9
|
import { type Asset, compile, type Lod, Ts23dError } from '../core/index.ts';
|
|
9
|
-
import {
|
|
10
|
-
|
|
10
|
+
import {
|
|
11
|
+
exportLodGlb,
|
|
12
|
+
SHEET_MAPS,
|
|
13
|
+
type SheetBinding,
|
|
14
|
+
sheetFileName,
|
|
15
|
+
sheetPngs,
|
|
16
|
+
toGlb,
|
|
17
|
+
} from '../export/index.ts';
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_SHEET,
|
|
20
|
+
type PaintedSheet,
|
|
21
|
+
paintTrimSheet,
|
|
22
|
+
provideTextures,
|
|
23
|
+
} from '../material/index.ts';
|
|
11
24
|
import { type Finding, runQa } from '../qa/index.ts';
|
|
12
25
|
import { beauty, contactSheet, thumbnail } from '../render/index.ts';
|
|
13
26
|
import { type Args, HELP, parseArgs } from './args.ts';
|
|
14
27
|
import { loadAssets } from './load.ts';
|
|
15
28
|
import { type AssetManifest, manifestOf } from './manifest.ts';
|
|
16
29
|
import { heroOne, printRendered, type RenderResult, reviewOne } from './render-cmds.ts';
|
|
30
|
+
import { loadTextureCache } from './texture-cache.ts';
|
|
17
31
|
|
|
18
32
|
async function buildOne(
|
|
19
33
|
asset: Asset,
|
|
@@ -29,16 +43,22 @@ async function buildOne(
|
|
|
29
43
|
runQa(a).map((f) => ({ ...f, message: `LOD${a.lod}: ${f.message}` })),
|
|
30
44
|
);
|
|
31
45
|
const glbs: string[] = [];
|
|
32
|
-
|
|
46
|
+
const glbOptions = { animations: args.animations, ...(texture ? { sheet: texture } : {}) };
|
|
47
|
+
let single: string | undefined;
|
|
48
|
+
let coverage: readonly number[] = [];
|
|
49
|
+
if (args.cmd === 'build' && args.lodFiles !== 'single') {
|
|
33
50
|
for (const a of lods) {
|
|
34
51
|
const path = join(dir, `lod${a.lod}.glb`);
|
|
35
|
-
await Bun.write(
|
|
36
|
-
path,
|
|
37
|
-
toGlb(a, { animations: args.animations, ...(texture ? { sheet: texture } : {}) }),
|
|
38
|
-
);
|
|
52
|
+
await Bun.write(path, toGlb(a, glbOptions));
|
|
39
53
|
glbs.push(path);
|
|
40
54
|
}
|
|
41
55
|
}
|
|
56
|
+
if (args.cmd === 'build' && args.lodFiles !== 'split') {
|
|
57
|
+
single = join(dir, `${asset.name}.glb`);
|
|
58
|
+
const packed = exportLodGlb(lods, glbOptions);
|
|
59
|
+
coverage = packed.coverage;
|
|
60
|
+
await Bun.write(single, packed.glb);
|
|
61
|
+
}
|
|
42
62
|
let sheet: string | undefined;
|
|
43
63
|
let thumb: string | undefined;
|
|
44
64
|
let hero: string | undefined;
|
|
@@ -61,11 +81,13 @@ async function buildOne(
|
|
|
61
81
|
lods,
|
|
62
82
|
{
|
|
63
83
|
lods: glbs,
|
|
84
|
+
...(single ? { single } : {}),
|
|
64
85
|
...(hero ? { hero } : {}),
|
|
65
86
|
...(sheet ? { sheet } : {}),
|
|
66
87
|
...(thumb ? { thumb } : {}),
|
|
67
88
|
},
|
|
68
89
|
findings,
|
|
90
|
+
coverage,
|
|
69
91
|
);
|
|
70
92
|
await Bun.write(join(dir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
71
93
|
return manifest;
|
|
@@ -77,6 +99,7 @@ function print(m: AssetManifest): void {
|
|
|
77
99
|
);
|
|
78
100
|
for (const f of [
|
|
79
101
|
...m.files.lods,
|
|
102
|
+
...(m.files.single ? [m.files.single] : []),
|
|
80
103
|
...(m.files.hero ? [m.files.hero] : []),
|
|
81
104
|
...(m.files.sheet ? [m.files.sheet] : []),
|
|
82
105
|
])
|
|
@@ -108,11 +131,12 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
108
131
|
}
|
|
109
132
|
const built: AssetManifest[] = [];
|
|
110
133
|
const rendered: RenderResult[] = [];
|
|
111
|
-
// Paint the trim sheet once per run (~
|
|
134
|
+
// Paint the trim sheet once per run (~4 s at 2048²). External mode writes it once into <out>/
|
|
112
135
|
// and every GLB in <out>/<asset>/ references ../trim_sheet_*.png — one texture set for all.
|
|
113
136
|
// The beauty renders sample the same painted sheet.
|
|
114
137
|
const renders = args.cmd === 'hero' || args.cmd === 'review';
|
|
115
138
|
const wantsSheet = (args.cmd === 'build' && args.sheet !== 'none') || renders;
|
|
139
|
+
if (wantsSheet) provideTextures(await loadTextureCache());
|
|
116
140
|
const painted = wantsSheet
|
|
117
141
|
? paintTrimSheet({ ...DEFAULT_SHEET, size: args.sheetSize })
|
|
118
142
|
: undefined;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The local CC0 texture cache (`bun run textures` fills it): every file re-verified by its sha256
|
|
2
|
+
// name, handed to `provideTextures` before painting. Missing files are fine here; painting a
|
|
3
|
+
// material that needs one throws T3D_TEXTURE_MISSING with the fetch command.
|
|
4
|
+
|
|
5
|
+
import { readdir } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import type { TextureCache } from '../material/index.ts';
|
|
8
|
+
|
|
9
|
+
export const TEXTURE_CACHE = join('node_modules', '.cache', 'ts23d', 'textures');
|
|
10
|
+
|
|
11
|
+
const sha256 = (b: Uint8Array): string => new Bun.CryptoHasher('sha256').update(b).digest('hex');
|
|
12
|
+
|
|
13
|
+
/** Every cached map whose bytes still match its sha256 name. */
|
|
14
|
+
export async function loadTextureCache(dir = TEXTURE_CACHE): Promise<TextureCache> {
|
|
15
|
+
const out = new Map<string, Uint8Array>();
|
|
16
|
+
const names = await readdir(dir).catch(() => [] as string[]);
|
|
17
|
+
for (const name of names.sort()) {
|
|
18
|
+
const m = /^([0-9a-f]{64})\.png$/.exec(name);
|
|
19
|
+
if (!m?.[1]) continue;
|
|
20
|
+
const bytes = new Uint8Array(await Bun.file(join(dir, name)).arrayBuffer());
|
|
21
|
+
if (sha256(bytes) === m[1]) out.set(m[1], bytes);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
package/src/core/asset.ts
CHANGED
|
@@ -113,6 +113,7 @@ export function compile(asset: Asset, options: { lod?: Lod; seed?: number } = {}
|
|
|
113
113
|
parts: [...parts.values()],
|
|
114
114
|
...metaOf(asset),
|
|
115
115
|
...(asset.glow?.length ? { glow: asset.glow } : {}),
|
|
116
|
+
...(asset.lodCoverage ? { lodCoverage: asset.lodCoverage } : {}),
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
119
|
|
package/src/core/definition.ts
CHANGED
|
@@ -128,6 +128,12 @@ export interface AssetDefinition {
|
|
|
128
128
|
readonly creators?: readonly string[];
|
|
129
129
|
/** Emissive part sets; `t3d build` writes them as glowing materials. */
|
|
130
130
|
readonly glow?: readonly Glow[];
|
|
131
|
+
/**
|
|
132
|
+
* View-distance hints for the single-file GLB (MSFT_lod `MSFT_screencoverage`): the screen-height
|
|
133
|
+
* fraction below which LOD0 → LOD1, LOD1 → LOD2, and LOD2 is culled (0 = never). Strictly
|
|
134
|
+
* descending, 1 > a > b > c >= 0. Omitted: derived from the triangle counts and size.
|
|
135
|
+
*/
|
|
136
|
+
readonly lodCoverage?: readonly [number, number, number];
|
|
131
137
|
build(ctx: BuildContext): void;
|
|
132
138
|
}
|
|
133
139
|
|
|
@@ -146,4 +152,5 @@ export interface CompiledAsset {
|
|
|
146
152
|
/** Provenance carried from the definition (absent when the recipe declares none). */
|
|
147
153
|
readonly meta?: AssetMeta;
|
|
148
154
|
readonly glow?: readonly Glow[];
|
|
155
|
+
readonly lodCoverage?: readonly [number, number, number];
|
|
149
156
|
}
|
package/src/core/errors.ts
CHANGED
|
@@ -13,7 +13,12 @@ export type ErrorCode =
|
|
|
13
13
|
| 'T3D_RENDER_OPTIONS'
|
|
14
14
|
| 'T3D_AMBIENT_RANDOM'
|
|
15
15
|
| 'T3D_RELEASE'
|
|
16
|
-
| 'T3D_BENCH'
|
|
16
|
+
| 'T3D_BENCH'
|
|
17
|
+
| 'T3D_MATERIAL_INVALID'
|
|
18
|
+
| 'T3D_MATERIAL_UNKNOWN'
|
|
19
|
+
| 'T3D_MATERIAL_CONFLICT'
|
|
20
|
+
| 'T3D_SHEET_FULL'
|
|
21
|
+
| 'T3D_TEXTURE_MISSING';
|
|
17
22
|
|
|
18
23
|
export interface ErrorDetails {
|
|
19
24
|
readonly cause: string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Texture credits into the GLB: when an asset samples an image strip (a CC0 texture set registered
|
|
2
|
+
// with defineMaterial), its credit line joins the XMP packet's `dc:contributor`. Assets that use
|
|
3
|
+
// only procedural strips get no change, so their bytes stay identical.
|
|
4
|
+
|
|
5
|
+
import type { CompiledAsset } from '../core/index.ts';
|
|
6
|
+
import { stripCredits } from '../material/index.ts';
|
|
7
|
+
import type { GlbMeta } from './metadata.ts';
|
|
8
|
+
|
|
9
|
+
export function creditsOf(asset: CompiledAsset): string[] {
|
|
10
|
+
const out = new Set<string>();
|
|
11
|
+
for (const p of asset.parts) {
|
|
12
|
+
const uv = p.mesh?.uvs;
|
|
13
|
+
if (!uv) continue;
|
|
14
|
+
const v = new Float32Array(uv.length >> 1);
|
|
15
|
+
for (let i = 0; i < v.length; i++) v[i] = uv[i * 2 + 1] ?? 0;
|
|
16
|
+
for (const c of stripCredits(v)) out.add(c);
|
|
17
|
+
}
|
|
18
|
+
return [...out].sort();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function withCredits(asset: CompiledAsset, meta: GlbMeta | undefined): GlbMeta | undefined {
|
|
22
|
+
const credits = creditsOf(asset);
|
|
23
|
+
if (!credits.length) return meta;
|
|
24
|
+
return { ...meta, credits: [...(meta?.credits ?? []), ...credits] };
|
|
25
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Options and results of the GLB writer (glb.ts): what goes into the file and what comes back.
|
|
2
|
+
|
|
3
|
+
import type { OptimizeOptions } from '../passes/index.ts';
|
|
4
|
+
import type { LodCoverage } from './lod-coverage.ts';
|
|
5
|
+
import type { GlowRule } from './materials.ts';
|
|
6
|
+
import type { GlbMeta } from './metadata.ts';
|
|
7
|
+
import type { ExportReport } from './optimize-parts.ts';
|
|
8
|
+
import type { SheetBinding } from './textures.ts';
|
|
9
|
+
|
|
10
|
+
export interface GlbOptions {
|
|
11
|
+
/** Keep every part as its own node (debugging, per-part picking). Default merges to pivots. */
|
|
12
|
+
readonly keepParts?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Bake each part's `motion` into a looping `run` clip. Off by default: glTF has no autoplay
|
|
15
|
+
* flag and most viewers play the first clip, while engines usually drive the named pivots
|
|
16
|
+
* from game code. Turn it on for previews, cutscenes and ambient props.
|
|
17
|
+
*/
|
|
18
|
+
readonly animations?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Bind the painted trim sheet to every material: a `PaintedSheet` embeds the three PNGs (one
|
|
21
|
+
* self-contained file); `{ external: true, prefix? }` references shared `trim_sheet_*.png` by
|
|
22
|
+
* URI. Omitted: no textures, byte-identical to the untextured output.
|
|
23
|
+
*/
|
|
24
|
+
readonly sheet?: SheetBinding;
|
|
25
|
+
/**
|
|
26
|
+
* Weld + vertex-cache + vertex-fetch optimisation per primitive (passes/optimize). On by
|
|
27
|
+
* default; `false` writes the meshes exactly as compiled (debugging the IR).
|
|
28
|
+
*/
|
|
29
|
+
readonly optimize?: boolean | OptimizeOptions;
|
|
30
|
+
/** Parts that emit light (lamp lenses, lit windows): one emissive material per rule. */
|
|
31
|
+
readonly glow?: readonly GlowRule[];
|
|
32
|
+
/** Provenance: asset.copyright + an XMP packet (title, creators, licence, sources). */
|
|
33
|
+
readonly meta?: GlbMeta;
|
|
34
|
+
/**
|
|
35
|
+
* Single-file LODs only: [LOD0→LOD1, LOD1→LOD2, cull] screen-height fractions written as
|
|
36
|
+
* `MSFT_screencoverage`. Default: the asset's `lodCoverage`, else derived (lod-coverage.ts).
|
|
37
|
+
*/
|
|
38
|
+
readonly lodCoverage?: LodCoverage;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The GLB bytes plus what the optimiser did (vertex counts, ACMR before/after, per part). */
|
|
42
|
+
export interface GlbResult {
|
|
43
|
+
readonly glb: Uint8Array;
|
|
44
|
+
readonly report: ExportReport;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One GLB holding several LODs, plus one optimiser report per LOD (same order as the input). */
|
|
48
|
+
export interface LodGlbResult {
|
|
49
|
+
readonly glb: Uint8Array;
|
|
50
|
+
readonly reports: readonly ExportReport[];
|
|
51
|
+
/** The `MSFT_screencoverage` written (empty for a single LOD). */
|
|
52
|
+
readonly coverage: readonly number[];
|
|
53
|
+
}
|