@threenative/assets 0.3.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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/index.d.ts +502 -0
- package/dist/index.js +3600 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 João Paulo Furtado
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @threenative/assets
|
|
2
|
+
|
|
3
|
+
The asset compile step for ThreeNative games. Walks a game's `assets/` source
|
|
4
|
+
directory, applies ordered passes over each input, and writes content-addressed
|
|
5
|
+
outputs into `public/` alongside an `assets.manifest.json` describing every
|
|
6
|
+
managed file.
|
|
7
|
+
|
|
8
|
+
Node-only. Carries the encoder dependencies the runtime must never inherit.
|
|
9
|
+
|
|
10
|
+
## Embedded model textures
|
|
11
|
+
|
|
12
|
+
The images inside a `.glb` go through the pipeline too, on by default. A prop carrying three
|
|
13
|
+
2048x2048 JPEGs is a small file and about 64 MiB of VRAM once the driver decodes it, so each
|
|
14
|
+
embedded image is transcoded to KTX2/Basis (UASTC for normal maps, ETC1S for colour and
|
|
15
|
+
metallic-roughness) and capped to a maximum resolution. The compiled model declares
|
|
16
|
+
`KHR_texture_basisu`; `ctx.assets.model()` wires three's shared, support-detected `KTX2Loader`
|
|
17
|
+
for exactly those files, and the Basis transcoder is copied next to the compiled assets even in
|
|
18
|
+
a project that has no standalone texture at all.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
assets: {
|
|
22
|
+
models: {
|
|
23
|
+
// Defaults: every image compressed, longest edge capped at 2048.
|
|
24
|
+
textures: { maxSize: 1024, quality: 150, overrides: [{ slot: "normalTexture", codec: "uastc" }] },
|
|
25
|
+
// Off unless declared: the one stage that removes triangles on purpose.
|
|
26
|
+
simplify: { ratio: 0.5, error: 0.001 },
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`models: { textures: "none" }` ships every image exactly as authored. Measured on an 11.2 MB
|
|
32
|
+
sandbox prop with three 2048x2048 JPEGs:
|
|
33
|
+
|
|
34
|
+
| Setting | File | Embedded images | Estimated GPU bytes |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| geometry only (`textures: "none"`) | 10.74 -> 7.74 MiB | untouched | 64.00 MiB |
|
|
37
|
+
| default (`maxSize: 2048`) | 10.74 -> 6.13 MiB | 6.93 -> 5.33 MiB | 10.67 MiB |
|
|
38
|
+
| `maxSize: 1024` | 10.74 -> 2.30 MiB | 6.93 -> 1.49 MiB | 2.67 MiB |
|
|
39
|
+
|
|
40
|
+
The pass verifies its own output: every embedded image is re-read from the written `.glb` and
|
|
41
|
+
compared against what went in for the material slot and UV set it is bound to, so a texture the
|
|
42
|
+
encoder or the writer dropped fails the build instead of shipping.
|
|
43
|
+
|
|
44
|
+
## LOD simplification
|
|
45
|
+
|
|
46
|
+
Off unless `models.simplify` is declared: it is the one stage that removes triangles on purpose,
|
|
47
|
+
so it trades the pass's exact triangle guarantee for a bounded one — joints and clips still
|
|
48
|
+
compared exactly, triangles required to fall but stay above a floor derived from the ratio, and
|
|
49
|
+
the bounding box held to 1% instead of 0.1%.
|
|
50
|
+
|
|
51
|
+
**How far can a ratio go before the silhouette changes?** Measured, not guessed: a 99,482-triangle
|
|
52
|
+
candelabrum rendered at the condition it is played at — scaled to 3 m tall, camera 3 m from its
|
|
53
|
+
centre, the framework's default 60° vertical fov, 1920x1080, twelve azimuths — with the coverage
|
|
54
|
+
mask compared against the unsimplified baseline by exact Euclidean distance transform.
|
|
55
|
+
|
|
56
|
+
| ratio | triangles | worst IoU | mean outline shift | p99 | max |
|
|
57
|
+
| --- | --- | --- | --- | --- | --- |
|
|
58
|
+
| 0.75 | 74,608 | 0.998 | 0.03 px | 1.0 px | 1.0 px |
|
|
59
|
+
| 0.50 | 49,738 | 0.997 | 0.05 px | 1.0 px | 1.0 px |
|
|
60
|
+
| 0.35 | 34,818 | 0.995 | 0.11 px | 1.0 px | 1.4 px |
|
|
61
|
+
| **0.25** | **24,870** | **0.992** | **0.17 px** | **1.0 px** | **3.2 px** |
|
|
62
|
+
| 0.15 | 15,126 | 0.985 | 0.35 px | 3.2 px | 16.0 px |
|
|
63
|
+
| 0.10 | 15,152 | 0.984 | 0.36 px | 5.1 px | 21.0 px |
|
|
64
|
+
|
|
65
|
+
**0.25 is the floor for a hero prop at that distance.** Down to it the outline moves at most one
|
|
66
|
+
pixel over 99% of its length — a quarter of the triangles for a silhouette that cannot be told
|
|
67
|
+
apart. Below it the simplifier starts removing whole features (the 16–21 px maxima are candle
|
|
68
|
+
wicks and tracery openings disappearing), and that is visible without an A/B. A seven-branch bar
|
|
69
|
+
of 42,904 triangles behaves the same way: 0.25 costs 1.0 px, 0.15 costs 4.0 px.
|
|
70
|
+
|
|
71
|
+
`error` is the quality guard, not the target — it is the largest a vertex may move as a fraction
|
|
72
|
+
of mesh extent (default 0.001, so 3 mm on a 3 m prop). It is what holds candela at 15.2% when the
|
|
73
|
+
config asks for 5%; reaching a true 10% needs `error: 0.005`, and no value looser than that
|
|
74
|
+
reduces further. Because that gap is silent by nature, the compile step prints both numbers:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
simplified candela.glb: 99482 -> 15104 triangles (15.2% kept, requested 5.0%) — the error tolerance 0.001 stopped it short
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Mobile native targets still refuse compressed assets — Android QuickJS and iOS JSC have no
|
|
81
|
+
WebAssembly and therefore no Basis transcoder — so an Android or iOS build needs
|
|
82
|
+
`assets.models: "none"`, exactly as it already did for compressed geometry.
|
|
83
|
+
|
|
84
|
+
## Static lightmaps
|
|
85
|
+
|
|
86
|
+
Opt a project's existing model pipeline into deterministic UV2 generation and offline static-light
|
|
87
|
+
baking through `threenative.config.ts`:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
assets: {
|
|
91
|
+
models: { lightmap: { atlasSize: 1024, padding: 4 } },
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Games still load the logical `.glb` with `ctx.assets.model()`. The compiler writes standard
|
|
96
|
+
`TEXCOORD_1` data and a content-addressed KTX2; the runtime attaches it through Three.js
|
|
97
|
+
`material.lightMap`. Removing the `lightmap` config removes the pass. Android and iOS builds still
|
|
98
|
+
fail closed on KTX2 because those native hosts do not yet carry a decoder.
|
|
99
|
+
|
|
100
|
+
| Proof | Result |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| Deterministic compile | Independent fresh directories produced byte-identical GLB, KTX2, and manifest hashes. |
|
|
103
|
+
| Ordinary consumer | Stock `GLTFLoader` + `KTX2Loader` loaded the compiled binary paths without `@threenative/core`. |
|
|
104
|
+
| Web gameplay | Packed-tarball sandbox required `material.lightMap` before reaching the goal could win. |
|
|
105
|
+
| Negative control | Setting `material.lightMap = null` made `staticLightReady` and the win assertion fail. |
|
|
106
|
+
| Native status | Linux desktop rendered the packed GLB/KTX2 with a clean playtest; Android/iOS still fail closed because their hosts have no KTX2 decoder. |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
type AssetFindingGrade = "fail" | "ok" | "warn";
|
|
2
|
+
interface IAssetFinding {
|
|
3
|
+
readonly asset: string;
|
|
4
|
+
readonly grade: AssetFindingGrade;
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly subject?: string;
|
|
8
|
+
readonly target?: number;
|
|
9
|
+
readonly value: boolean | number | string;
|
|
10
|
+
}
|
|
11
|
+
interface ITextureStats {
|
|
12
|
+
readonly alpha: boolean;
|
|
13
|
+
readonly height: number;
|
|
14
|
+
readonly powerOfTwo: boolean;
|
|
15
|
+
readonly width: number;
|
|
16
|
+
}
|
|
17
|
+
interface IModelStats {
|
|
18
|
+
readonly animationClips: number;
|
|
19
|
+
readonly colliderPresent: boolean;
|
|
20
|
+
readonly materials: number;
|
|
21
|
+
readonly rootMotion: boolean;
|
|
22
|
+
readonly textures: readonly ITextureStats[];
|
|
23
|
+
readonly triangles: number;
|
|
24
|
+
}
|
|
25
|
+
interface IAssetHealthEntry {
|
|
26
|
+
readonly asset: string;
|
|
27
|
+
readonly kind: AssetKind;
|
|
28
|
+
readonly license: string;
|
|
29
|
+
readonly model?: IModelStats;
|
|
30
|
+
readonly texture?: ITextureStats;
|
|
31
|
+
}
|
|
32
|
+
interface IAssetHealthReport {
|
|
33
|
+
readonly entries: readonly IAssetHealthEntry[];
|
|
34
|
+
readonly failed: boolean;
|
|
35
|
+
readonly findings: readonly IAssetFinding[];
|
|
36
|
+
readonly summary: Readonly<Record<AssetFindingGrade, number>>;
|
|
37
|
+
}
|
|
38
|
+
interface IAssetHealthInput {
|
|
39
|
+
readonly data: Buffer;
|
|
40
|
+
readonly logicalPath: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Measures every compiled asset and grades findings against the declared targets.
|
|
44
|
+
* A finding only reaches `fail` where a target was declared, so the report stays
|
|
45
|
+
* informational until a project opts into enforcement.
|
|
46
|
+
*/
|
|
47
|
+
declare function runHealthReport(inputs: readonly IAssetHealthInput[], targets?: IAssetTargets): Promise<IAssetHealthReport>;
|
|
48
|
+
/** The person-readable form printed by the compile step, one line per finding plus a summary. */
|
|
49
|
+
declare function formatHealthReport(report: IAssetHealthReport): readonly string[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Atlas integration follows the mesh → xatlas → remapped-attribute split documented by
|
|
53
|
+
* xatlas-three 0.2.1 (MIT, copyright 2022 Palash Bansal). This implementation uses xatlasjs's
|
|
54
|
+
* local Node/WASM API directly; it performs no CDN fetch and owns no runtime dependency.
|
|
55
|
+
*/
|
|
56
|
+
interface ILightmapPassOptions {
|
|
57
|
+
readonly atlasSize: number;
|
|
58
|
+
readonly padding: number;
|
|
59
|
+
}
|
|
60
|
+
declare function lightmapPass(options: ILightmapPassOptions): IAssetPass;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Cluster-DAG bake options, as they appear under `assets.models.virtual` in `threenative.config.ts`.
|
|
64
|
+
*
|
|
65
|
+
* **Absent means the bake runs with defaults**, exactly as `textures` does: a game that imports a
|
|
66
|
+
* body too dense for the screen should not have to know this key exists to avoid drawing all of it.
|
|
67
|
+
* `"none"` ships every primitive as authored. There is no runtime flag either way — a minutes-long
|
|
68
|
+
* bake cannot happen at run time, and a second way to say the same thing is a second thing to get
|
|
69
|
+
* wrong.
|
|
70
|
+
*/
|
|
71
|
+
interface IModelVirtualOptions {
|
|
72
|
+
/** Clusters folded together per group, default 4. */
|
|
73
|
+
readonly groupSize?: number;
|
|
74
|
+
/** Upper bound on a cluster's triangles, default 128. */
|
|
75
|
+
readonly maxTriangles?: number;
|
|
76
|
+
/** Lower bound on a cluster's triangles, default 96. */
|
|
77
|
+
readonly minTriangles?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Primitives below this many triangles are left alone. Default {@link DEFAULT_MIN_SOURCE_TRIANGLES}.
|
|
80
|
+
*
|
|
81
|
+
* The payload costs about 3–4x the primitive's compiled bytes whatever its density, so the
|
|
82
|
+
* default is the density where the cut starts earning them back. Measured on one body at seven
|
|
83
|
+
* densities, drawing at a distance that frames it across a third of the screen:
|
|
84
|
+
*
|
|
85
|
+
* | source triangles | desktop 1080p draws | Pixel 8 (0.44 scale) draws |
|
|
86
|
+
* | --- | --- | --- |
|
|
87
|
+
* | 32,768 | 92% | 57% |
|
|
88
|
+
* | 65,536 | 69% | 27% |
|
|
89
|
+
* | 131,072 | 35% | 16% |
|
|
90
|
+
* | 524,288 | 13% | 8% |
|
|
91
|
+
*
|
|
92
|
+
* Below 32k the cut is the whole mesh on desktop — payload for nothing. 65,536 is where a phone
|
|
93
|
+
* already draws a quarter of the triangles and a desktop a bit over two thirds, and it is the
|
|
94
|
+
* density at which a mesh needs 32-bit indices anyway.
|
|
95
|
+
*/
|
|
96
|
+
readonly minSourceTriangles?: number;
|
|
97
|
+
/** Fraction of a group's triangles kept per level, default 0.5. */
|
|
98
|
+
readonly simplifyRatio?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Encodes compiled textures to KTX2/Basis so the GPU stores them compressed instead of as
|
|
103
|
+
* decoded RGBA (a 2048x2048 PNG is 16 MB in VRAM; BC7/ASTC hold it in a quarter of that).
|
|
104
|
+
*
|
|
105
|
+
* Codec choice is a declared property of the asset, never a guess: a config override wins,
|
|
106
|
+
* then the `*_normal.*` / `_nrm.*` filename convention, then alpha presence (UASTC keeps
|
|
107
|
+
* exact alpha and survives normal-map data; ETC1S does not). Mip chains are generated at
|
|
108
|
+
* encode time — always — because an uploaded compressed texture without mips looks worse
|
|
109
|
+
* than the PNG it replaced.
|
|
110
|
+
*
|
|
111
|
+
* The encoder is Basis Universal through `ktx2-encoder`'s in-process WASM build; users
|
|
112
|
+
* install nothing extra. Sources it cannot decode (anything but PNG/JPEG today) fail the
|
|
113
|
+
* build naming the file rather than shipping uncompressed behind the user's back.
|
|
114
|
+
*/
|
|
115
|
+
type TextureCodec = "etc1s" | "none" | "uastc";
|
|
116
|
+
interface ITextureOverride {
|
|
117
|
+
readonly codec: TextureCodec;
|
|
118
|
+
/** First matching override wins; matched against the logical path, e.g. "ui/x.png". */
|
|
119
|
+
readonly glob: string;
|
|
120
|
+
/** ETC1S encoder quality 1–255. Ignored for UASTC, which uses its own fixed defaults. */
|
|
121
|
+
readonly quality?: number;
|
|
122
|
+
}
|
|
123
|
+
interface ITexturePassOptions {
|
|
124
|
+
readonly overrides?: readonly ITextureOverride[];
|
|
125
|
+
/** ETC1S encoder quality 1–255, default 150. Ignored for UASTC. */
|
|
126
|
+
readonly quality?: number;
|
|
127
|
+
}
|
|
128
|
+
declare function texturePass(options?: ITexturePassOptions): IAssetPass;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compresses the images embedded *inside* a `.glb` — the half of a model the geometry passes
|
|
132
|
+
* never touched. A prop carrying three 2048x2048 JPEGs is a small file and 67 MB of VRAM once
|
|
133
|
+
* the driver decodes it; meshopt shrinks the other, cheaper half. This stage transcodes each
|
|
134
|
+
* embedded image to KTX2/Basis and declares `KHR_texture_basisu`, so the GPU stores blocks
|
|
135
|
+
* instead of RGBA, and caps the resolution so a 4K texture an image model produced cannot
|
|
136
|
+
* silently eat a phone's whole budget.
|
|
137
|
+
*
|
|
138
|
+
* Codec choice follows the declared use of the image, never a guess: a config override on the
|
|
139
|
+
* glTF slot wins, then normal-map slots (UASTC survives normal data; ETC1S does not), then
|
|
140
|
+
* alpha in the decoded pixels. Colour space comes from the slot too — `getTextureColorSpace`
|
|
141
|
+
* knows base colour and emissive are sRGB and that metallic-roughness and normals are data —
|
|
142
|
+
* so a linear map is never encoded through a perceptual metric.
|
|
143
|
+
*
|
|
144
|
+
* The encoder is the same in-process Basis WASM the standalone texture pass uses, so no new
|
|
145
|
+
* dependency and nothing extra to install. Everything fails closed: an image no decoder reads,
|
|
146
|
+
* an image whose dimensions are not a whole number of 4x4 blocks, and a texture whose binding
|
|
147
|
+
* did not survive the round trip all throw naming the model and the image.
|
|
148
|
+
*/
|
|
149
|
+
interface IModelTextureOverride {
|
|
150
|
+
readonly codec: TextureCodec;
|
|
151
|
+
/** glTF slot the override applies to, e.g. `normalTexture` or `baseColorTexture`. */
|
|
152
|
+
readonly slot: string;
|
|
153
|
+
}
|
|
154
|
+
interface IModelTexturesOptions {
|
|
155
|
+
/**
|
|
156
|
+
* Longest edge an embedded image may keep, default 2048. Larger images are box-resampled
|
|
157
|
+
* down preserving aspect ratio; smaller ones are never upscaled.
|
|
158
|
+
*/
|
|
159
|
+
readonly maxSize?: number;
|
|
160
|
+
readonly overrides?: readonly IModelTextureOverride[];
|
|
161
|
+
/** ETC1S encoder quality 1–255, default 150. Ignored for UASTC. */
|
|
162
|
+
readonly quality?: number;
|
|
163
|
+
}
|
|
164
|
+
/** What the stage did, recorded in the manifest and printed by the size report. */
|
|
165
|
+
interface IEmbeddedTextureSummary {
|
|
166
|
+
readonly bytesAfter: number;
|
|
167
|
+
readonly bytesBefore: number;
|
|
168
|
+
readonly count: number;
|
|
169
|
+
/** Chosen codec per embedded image, keyed by texture name. */
|
|
170
|
+
readonly formats: Readonly<Record<string, string>>;
|
|
171
|
+
readonly gpuBytesAfter: number;
|
|
172
|
+
readonly gpuBytesBefore: number;
|
|
173
|
+
/** How many images the resolution cap actually downsampled. */
|
|
174
|
+
readonly resized: number;
|
|
175
|
+
}
|
|
176
|
+
/** One embedded image as the verification compares it, source against re-read output. */
|
|
177
|
+
interface IModelTextureBinding {
|
|
178
|
+
readonly height: number;
|
|
179
|
+
readonly key: string;
|
|
180
|
+
/** KTX2 mip levels; 1 for a still-uncompressed source image. */
|
|
181
|
+
readonly levels: number;
|
|
182
|
+
readonly mimeType: string;
|
|
183
|
+
readonly slots: readonly string[];
|
|
184
|
+
readonly texCoords: readonly number[];
|
|
185
|
+
readonly width: number;
|
|
186
|
+
}
|
|
187
|
+
interface IModelTextureBindings {
|
|
188
|
+
/** `material -> slot=textureIndex@uvN` for the five core glTF slots, in document order. */
|
|
189
|
+
readonly materials: readonly string[];
|
|
190
|
+
readonly textures: readonly IModelTextureBinding[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Optimizes compiled models: dedup → prune → simplify → reorder → quantize → textures →
|
|
195
|
+
* meshopt, in that fixed order, each individually switchable in config. Output declares
|
|
196
|
+
* `KHR_mesh_quantization` and `EXT_meshopt_compression`, plus `KHR_texture_basisu` when it
|
|
197
|
+
* carried images; the runtime lazily wires three's own MeshoptDecoder and the shared,
|
|
198
|
+
* support-detected KTX2Loader for exactly those files (see `core/src/assets.ts`).
|
|
199
|
+
*
|
|
200
|
+
* Geometry was only ever half of a model. The textures embedded inside a `.glb` are the
|
|
201
|
+
* expensive half — three 2048x2048 JPEGs are a small file and ~67 MB of VRAM decoded — so
|
|
202
|
+
* they go through the same Basis encoder the standalone texture pass uses, capped to a
|
|
203
|
+
* declared maximum resolution (`packages/assets/src/passes/model-textures.ts`).
|
|
204
|
+
*
|
|
205
|
+
* The pass verifies its own output before shipping it: the result is re-read and its
|
|
206
|
+
* triangle, vertex, joint and clip counts and bounding box are compared against the source,
|
|
207
|
+
* throwing and naming the drift beyond tolerance, and every embedded image is compared for
|
|
208
|
+
* the material slot and UV set it is bound to. A pipeline that silently loses a mesh — or a
|
|
209
|
+
* texture — is worse than no pipeline. Counts are taken over scene-reachable content only, so
|
|
210
|
+
* `prune` dropping DCC leftovers is not drift — losing *referenced* geometry is. Joint indices
|
|
211
|
+
* and weights are never quantized below what the source declared.
|
|
212
|
+
*
|
|
213
|
+
* Draco stays an input, never an output: a Draco `.glb` is decoded here (the codec loads
|
|
214
|
+
* only when one is seen, so no project pays for it) and re-emitted as Meshopt. Animation
|
|
215
|
+
* channels are compressed but never resampled — resampling changes timing, which is gameplay.
|
|
216
|
+
*/
|
|
217
|
+
interface IModelPassesOptions {
|
|
218
|
+
readonly dedup?: boolean;
|
|
219
|
+
readonly meshopt?: boolean;
|
|
220
|
+
readonly prune?: boolean;
|
|
221
|
+
readonly quantize?: boolean;
|
|
222
|
+
readonly reorder?: boolean;
|
|
223
|
+
}
|
|
224
|
+
interface IModelQuantizeOptions {
|
|
225
|
+
/** Normal precision in bits, default 8. */
|
|
226
|
+
readonly normalBits?: number;
|
|
227
|
+
/** Position precision in bits, default 16. */
|
|
228
|
+
readonly positionBits?: number;
|
|
229
|
+
/** UV precision in bits, default 12. */
|
|
230
|
+
readonly uvBits?: number;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Mesh simplification for LOD. Off unless declared, because it is the one stage that
|
|
234
|
+
* deliberately destroys the triangle count the pass otherwise guarantees: with it on, the
|
|
235
|
+
* self-verify swaps exact triangle equality for a bounded reduction and a wider bounding-box
|
|
236
|
+
* tolerance, and joints and animation clips are still compared exactly.
|
|
237
|
+
*/
|
|
238
|
+
interface IModelSimplifyOptions {
|
|
239
|
+
/** Maximum positional error as a fraction of mesh extent, default 0.001. */
|
|
240
|
+
readonly error?: number;
|
|
241
|
+
/** Target fraction of the source triangle count, 0–1. */
|
|
242
|
+
readonly ratio: number;
|
|
243
|
+
}
|
|
244
|
+
interface IModelPassOptions {
|
|
245
|
+
readonly passes?: IModelPassesOptions;
|
|
246
|
+
/** Preserve generated TEXCOORD_1 data that is consumed by a runtime-attached lightmap. */
|
|
247
|
+
readonly preserveLightmapUv?: boolean;
|
|
248
|
+
readonly quantize?: IModelQuantizeOptions;
|
|
249
|
+
/** LOD simplification; absent means no simplification at all. */
|
|
250
|
+
readonly simplify?: IModelSimplifyOptions;
|
|
251
|
+
/** Embedded-texture compression: options, or `"none"` to ship every image as authored. */
|
|
252
|
+
readonly textures?: IModelTexturesOptions | "none";
|
|
253
|
+
/**
|
|
254
|
+
* Cluster-DAG bake for virtual geometry: options, or `"none"` to ship every primitive as
|
|
255
|
+
* authored. **Absent means the bake runs with defaults**, on the same terms as `textures` — a
|
|
256
|
+
* game that imports a body too dense for the screen should not have to know this key exists.
|
|
257
|
+
* Only primitives at or above `minSourceTriangles` are touched, so an ordinary prop is
|
|
258
|
+
* byte-identical either way. Minutes on a dense body, so the compile cache keys on it — and on
|
|
259
|
+
* {@link VIRTUAL_BAKE_VERSION}, because a better partition changes the output and a stale entry
|
|
260
|
+
* would hide that.
|
|
261
|
+
*/
|
|
262
|
+
readonly virtual?: IModelVirtualOptions | "none";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* What simplification actually delivered. The error tolerance can stop the simplifier well
|
|
267
|
+
* short of the requested ratio — measured on a 99,482-triangle prop, `ratio: 0.05` with the
|
|
268
|
+
* default error lands at 15.2% — so both numbers are reported rather than only the one the
|
|
269
|
+
* config asked for.
|
|
270
|
+
*/
|
|
271
|
+
interface IModelSimplifySummary {
|
|
272
|
+
readonly achievedRatio: number;
|
|
273
|
+
readonly error: number;
|
|
274
|
+
readonly requestedRatio: number;
|
|
275
|
+
readonly trianglesAfter: number;
|
|
276
|
+
readonly trianglesBefore: number;
|
|
277
|
+
}
|
|
278
|
+
declare function modelPass(options?: IModelPassOptions): IAssetPass;
|
|
279
|
+
|
|
280
|
+
type AssetKind = "audio" | "model" | "other" | "texture";
|
|
281
|
+
/** Budgets a project declares for its assets; exceeding one fails the build. */
|
|
282
|
+
interface IAssetTargets {
|
|
283
|
+
readonly maxMaterials?: number;
|
|
284
|
+
readonly maxTriangles?: number;
|
|
285
|
+
readonly maxTextureDimension?: number;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* What a pass produced for one input. A plain `Buffer` return keeps the identity semantics;
|
|
289
|
+
* the richer shape lets a pass rename the output extension and declare manifest fields for
|
|
290
|
+
* the input it transformed (the KTX2 pass records `format` and `transcodeTargets`).
|
|
291
|
+
*/
|
|
292
|
+
interface IAssetPassOutput {
|
|
293
|
+
readonly auxiliaryOutputs?: readonly IAssetAuxiliaryOutput[];
|
|
294
|
+
readonly buffer: Buffer;
|
|
295
|
+
/** Extra manifest fields merged into the entry for the input this output came from. */
|
|
296
|
+
readonly entry?: Readonly<Record<string, unknown>>;
|
|
297
|
+
/** Replaces the source extension in the content-addressed output name when set. */
|
|
298
|
+
readonly outputExtension?: string;
|
|
299
|
+
}
|
|
300
|
+
interface IAssetAuxiliaryOutput {
|
|
301
|
+
readonly buffer: Buffer;
|
|
302
|
+
readonly extension: string;
|
|
303
|
+
/** Manifest array receiving this output, for example `lightmaps`. */
|
|
304
|
+
readonly manifestField: string;
|
|
305
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
306
|
+
/** Stable filename segment, for example `lightmap`. */
|
|
307
|
+
readonly role: string;
|
|
308
|
+
}
|
|
309
|
+
interface IAssetPass {
|
|
310
|
+
/** Deterministic settings that change output bytes; omitted for identity/default passes. */
|
|
311
|
+
readonly cacheKey?: string;
|
|
312
|
+
readonly name: string;
|
|
313
|
+
/**
|
|
314
|
+
* JSON-serializable snapshot of every option that changes this pass's output. Part of the
|
|
315
|
+
* compile cache key: without it, editing a config value (texture quality, quantize bits, an
|
|
316
|
+
* override's codec) leaves the pass name and input hash untouched and the stale output is
|
|
317
|
+
* re-served forever. `undefined` for passes without options.
|
|
318
|
+
*/
|
|
319
|
+
readonly configuration?: Readonly<Record<string, unknown>>;
|
|
320
|
+
apply(input: Buffer, logicalPath: string): Buffer | IAssetPassOutput | Promise<Buffer | IAssetPassOutput>;
|
|
321
|
+
}
|
|
322
|
+
interface IAssetSourceConfig {
|
|
323
|
+
/**
|
|
324
|
+
* Model optimization: an object of options, or the string `"none"` to ship every model
|
|
325
|
+
* exactly as committed. Absent means optimization runs with defaults.
|
|
326
|
+
*/
|
|
327
|
+
readonly models?: IModelsConfig | "none";
|
|
328
|
+
readonly output?: string;
|
|
329
|
+
readonly source?: string;
|
|
330
|
+
readonly targets?: IAssetTargets;
|
|
331
|
+
/**
|
|
332
|
+
* Texture compression: an object of options, or the string `"none"` to ship every texture
|
|
333
|
+
* exactly as committed. Absent means compression runs with defaults.
|
|
334
|
+
*/
|
|
335
|
+
readonly textures?: ITexturesConfig | "none";
|
|
336
|
+
}
|
|
337
|
+
interface IModelsConfig {
|
|
338
|
+
/** Generate standard TEXCOORD_1 lightmap UVs. Absent means no lightmap pass. */
|
|
339
|
+
readonly lightmap?: ILightmapPassOptions;
|
|
340
|
+
readonly passes?: IModelPassesOptions;
|
|
341
|
+
readonly quantize?: IModelQuantizeOptions;
|
|
342
|
+
/** LOD simplification. Absent means none: it is the one lossy stage, and it is opt-in. */
|
|
343
|
+
readonly simplify?: IModelSimplifyOptions;
|
|
344
|
+
/**
|
|
345
|
+
* Compression of the images embedded in a model, or the string `"none"` to ship them
|
|
346
|
+
* exactly as authored. Absent means compression runs with defaults.
|
|
347
|
+
*/
|
|
348
|
+
readonly textures?: IModelTexturesOptions | "none";
|
|
349
|
+
/**
|
|
350
|
+
* Cluster-DAG bake for virtual geometry, or `"none"` to ship every primitive as authored.
|
|
351
|
+
* Absent means the bake runs with defaults, which clusters any primitive of 65,536 triangles
|
|
352
|
+
* or more and leaves everything below it byte-identical.
|
|
353
|
+
*/
|
|
354
|
+
readonly virtual?: IModelVirtualOptions | "none";
|
|
355
|
+
}
|
|
356
|
+
interface ITexturesConfig {
|
|
357
|
+
readonly overrides?: readonly ITextureOverride[];
|
|
358
|
+
readonly quality?: number;
|
|
359
|
+
}
|
|
360
|
+
interface IAssetCompileOptions {
|
|
361
|
+
readonly config?: IAssetSourceConfig;
|
|
362
|
+
readonly cwd?: string;
|
|
363
|
+
/** Includes the machine-readable health report on the result. */
|
|
364
|
+
readonly health?: boolean;
|
|
365
|
+
readonly output?: string;
|
|
366
|
+
/**
|
|
367
|
+
* Replaces the built-in pass registry when provided; left undefined, the built-in
|
|
368
|
+
* registry (the KTX2 texture pass) runs unless `config.textures` is `"none"`.
|
|
369
|
+
*/
|
|
370
|
+
readonly passes?: readonly IAssetPass[];
|
|
371
|
+
readonly source?: string;
|
|
372
|
+
/** Overrides resolution of three's Basis transcoder for the copy into the output root. */
|
|
373
|
+
readonly transcoder?: IBasisTranscoder;
|
|
374
|
+
}
|
|
375
|
+
interface IBasisTranscoder {
|
|
376
|
+
readonly javascriptPath: string;
|
|
377
|
+
readonly wasmPath: string;
|
|
378
|
+
}
|
|
379
|
+
interface IAssetCompileResult {
|
|
380
|
+
readonly report?: IAssetHealthReport;
|
|
381
|
+
readonly skipped: number;
|
|
382
|
+
readonly written: number;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Resolves three's Basis transcoder through the project's own `three` install so a path that
|
|
386
|
+
* moved between three versions fails the build here, as a named error, instead of 404ing at
|
|
387
|
+
* runtime inside the KTX2 loader.
|
|
388
|
+
*/
|
|
389
|
+
declare function resolveBasisTranscoder(cwd: string): IBasisTranscoder;
|
|
390
|
+
declare function compileAssets(options?: IAssetCompileOptions): Promise<IAssetCompileResult>;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The person-readable size report the compile step prints after encoding: one line per
|
|
394
|
+
* compressed texture and optimized model plus a total, before against after. Pure formatting
|
|
395
|
+
* so tests can pin the exact lines a build prints.
|
|
396
|
+
*/
|
|
397
|
+
interface ITextureSizeRow {
|
|
398
|
+
readonly after: number;
|
|
399
|
+
readonly before: number;
|
|
400
|
+
readonly format: string | undefined;
|
|
401
|
+
readonly logicalPath: string;
|
|
402
|
+
}
|
|
403
|
+
declare function formatTextureSizes(rows: readonly ITextureSizeRow[]): readonly string[];
|
|
404
|
+
/** What the model pass did to the images inside one `.glb`. */
|
|
405
|
+
interface IEmbeddedTextureRow {
|
|
406
|
+
readonly bytesAfter: number;
|
|
407
|
+
readonly bytesBefore: number;
|
|
408
|
+
readonly count: number;
|
|
409
|
+
/** Chosen codec per embedded image, keyed by texture name. */
|
|
410
|
+
readonly formats?: Readonly<Record<string, string>>;
|
|
411
|
+
readonly gpuBytesAfter: number;
|
|
412
|
+
readonly gpuBytesBefore: number;
|
|
413
|
+
readonly resized: number;
|
|
414
|
+
}
|
|
415
|
+
/** What the cluster-DAG bake produced for one model. */
|
|
416
|
+
interface IVirtualRow {
|
|
417
|
+
readonly bakeSeconds: number;
|
|
418
|
+
readonly clusters: number;
|
|
419
|
+
readonly levels: number;
|
|
420
|
+
readonly payloadBytes: number;
|
|
421
|
+
readonly primitives: number;
|
|
422
|
+
readonly skipped: number;
|
|
423
|
+
readonly stopReason: string;
|
|
424
|
+
}
|
|
425
|
+
/** What simplification delivered against what the config asked for. */
|
|
426
|
+
interface ISimplifyRow {
|
|
427
|
+
readonly achievedRatio: number;
|
|
428
|
+
readonly error: number;
|
|
429
|
+
readonly requestedRatio: number;
|
|
430
|
+
readonly trianglesAfter: number;
|
|
431
|
+
readonly trianglesBefore: number;
|
|
432
|
+
}
|
|
433
|
+
/** One compiled model plus a total, before against after the optimization pass. */
|
|
434
|
+
interface IModelSizeRow {
|
|
435
|
+
readonly after: number;
|
|
436
|
+
readonly before: number;
|
|
437
|
+
/** Embedded-image compression, when the model carried any. */
|
|
438
|
+
readonly embeddedTextures?: IEmbeddedTextureRow;
|
|
439
|
+
/** glTF extensions declared by the compiled output, e.g. EXT_meshopt_compression. */
|
|
440
|
+
readonly extensions?: readonly string[];
|
|
441
|
+
readonly logicalPath: string;
|
|
442
|
+
readonly lightmap?: {
|
|
443
|
+
readonly atlasHeight: number;
|
|
444
|
+
readonly atlasWidth: number;
|
|
445
|
+
readonly bakeMs: number;
|
|
446
|
+
readonly bytesAfter: number;
|
|
447
|
+
readonly bytesBefore: number;
|
|
448
|
+
readonly dilatedTexels: number;
|
|
449
|
+
readonly occludedTexels: number;
|
|
450
|
+
readonly validTexels: number;
|
|
451
|
+
};
|
|
452
|
+
/** LOD simplification, when it was configured for this model. */
|
|
453
|
+
readonly simplify?: ISimplifyRow;
|
|
454
|
+
/** The cluster-DAG bake, when it was configured for this model. */
|
|
455
|
+
readonly virtual?: IVirtualRow;
|
|
456
|
+
/** Triangle count of the compiled output, recorded in the manifest. */
|
|
457
|
+
readonly triangles?: number;
|
|
458
|
+
}
|
|
459
|
+
declare function formatModelSizes(rows: readonly IModelSizeRow[]): readonly string[];
|
|
460
|
+
|
|
461
|
+
interface IPngInfo {
|
|
462
|
+
readonly width: number;
|
|
463
|
+
readonly height: number;
|
|
464
|
+
readonly hasAlpha: boolean;
|
|
465
|
+
}
|
|
466
|
+
/** Parses the PNG structure needed by config validation and asset health reporting. */
|
|
467
|
+
declare function parsePng(value: Buffer): IPngInfo | undefined;
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Dev-mode compilation: watches the asset source directory and recompiles changed inputs into
|
|
471
|
+
* the output directory, so the dev loop is not "rebuild to see a texture".
|
|
472
|
+
*
|
|
473
|
+
* Watcher mechanism: `node:fs.watch({ recursive: true })` — deliberately neither chokidar nor
|
|
474
|
+
* Vite's own server watcher. This package is Node-only with zero runtime dependencies; Vite's
|
|
475
|
+
* watcher exists only inside a Vite process and would drag in a peer dependency, and chokidar
|
|
476
|
+
* adds a package outright. Recursive `fs.watch` covers Windows, macOS and Linux on the Node
|
|
477
|
+
* versions Vite already requires, keeps `watchAssets` usable outside a dev server, and leaves
|
|
478
|
+
* the template plugin a handful of lines that only start and stop this watcher.
|
|
479
|
+
*/
|
|
480
|
+
interface IAssetWatchSummary {
|
|
481
|
+
readonly compiled: readonly string[];
|
|
482
|
+
readonly failed: readonly string[];
|
|
483
|
+
}
|
|
484
|
+
interface IAssetWatchOptions extends IAssetCompileOptions {
|
|
485
|
+
/** Quiet window that coalesces a burst of saves into one recompile. Default 100 ms. */
|
|
486
|
+
readonly debounceMs?: number;
|
|
487
|
+
/** Called once per settled burst with the logical paths that compiled and that failed. */
|
|
488
|
+
readonly onChange?: (summary: IAssetWatchSummary) => void;
|
|
489
|
+
}
|
|
490
|
+
interface IAssetWatchHandle {
|
|
491
|
+
/**
|
|
492
|
+
* Settles once the initial full compile finished. An initial failure is logged to stderr
|
|
493
|
+
* instead of thrown — a broken asset must not keep a dev server from booting, fixing the file
|
|
494
|
+
* triggers a healing burst, and the build lane still fails closed through `compileAssets`.
|
|
495
|
+
*/
|
|
496
|
+
readonly ready: Promise<void>;
|
|
497
|
+
/** Stops watching and cancels any pending debounced burst. Idempotent. */
|
|
498
|
+
close(): void;
|
|
499
|
+
}
|
|
500
|
+
declare function watchAssets(options?: IAssetWatchOptions): IAssetWatchHandle;
|
|
501
|
+
|
|
502
|
+
export { type AssetFindingGrade, type AssetKind, type IAssetCompileOptions, type IAssetCompileResult, type IAssetFinding, type IAssetHealthEntry, type IAssetHealthInput, type IAssetHealthReport, type IAssetPass, type IAssetPassOutput, type IAssetSourceConfig, type IAssetTargets, type IAssetWatchHandle, type IAssetWatchOptions, type IAssetWatchSummary, type IBasisTranscoder, type IEmbeddedTextureRow, type IEmbeddedTextureSummary, type ILightmapPassOptions, type IModelPassOptions, type IModelPassesOptions, type IModelQuantizeOptions, type IModelSimplifyOptions, type IModelSimplifySummary, type IModelSizeRow, type IModelStats, type IModelTextureBinding, type IModelTextureBindings, type IModelTextureOverride, type IModelTexturesOptions, type IModelsConfig, type IPngInfo, type ISimplifyRow, type ITextureOverride, type ITexturePassOptions, type ITextureSizeRow, type ITextureStats, type ITexturesConfig, type TextureCodec, compileAssets, formatHealthReport, formatModelSizes, formatTextureSizes, lightmapPass, modelPass, parsePng, resolveBasisTranscoder, runHealthReport, texturePass, watchAssets };
|