@threenative/assets 0.3.5 → 0.3.7

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.
@@ -388,6 +388,8 @@ def skipped_collections(layer, under_hidden=False):
388
388
 
389
389
  def terrain_layer_texture(table_dir, textures, layer_id, kind):
390
390
  """The source file of one texture set's map, searched in the table's `textures.search` dirs."""
391
+ if kind not in textures:
392
+ fail("terrain layers: the table names no '%s' texture for a layer that asks for one" % kind)
391
393
  name = textures[kind].replace("{id}", layer_id)
392
394
  for relative in textures.get("search", ["."]):
393
395
  base = os.path.normpath(os.path.join(table_dir, relative))
@@ -422,7 +424,9 @@ def export_terrain_layers(out, table_path, root=None):
422
424
  The table (the game's; its DCC shader can read the same file) names its masks, texture sets and
423
425
  blend values. This writes `terrain/layers.json` (the table plus the splat layout),
424
426
  `terrain/splat.rgba8` (masks packed into RGBA8 planes, raw so no cook converts them) and
425
- `terrain/tex/<id>_diff.jpg` / `<id>_nrm.jpg`, and returns the `terrain.layers` paths.
427
+ `terrain/tex/<id>_diff.jpg`, `<id>_nrm.jpg` and `<id>_orm.jpg`, and returns the
428
+ `terrain.layers` paths. A layer's `normal` and `orm` flags say which maps it ships, and its
429
+ `roughness`/`metalness` say how it answers light when it ships no ORM map.
426
430
  Relative paths in the table resolve against `root` when given, else the table's folder.
427
431
  """
428
432
  import numpy as np
@@ -476,6 +480,19 @@ def export_terrain_layers(out, table_path, root=None):
476
480
  terrain_layer_texture(table_dir, textures, layer["id"], "nrm"),
477
481
  os.path.join(tex, "%s_nrm.jpg" % layer["id"]),
478
482
  )
483
+ if layer.get("orm"):
484
+ # A material with a roughness input of its own ships that input as occlusion,
485
+ # roughness and metalness; the table's numbers stand in for one without it.
486
+ shutil.copyfile(
487
+ terrain_layer_texture(table_dir, textures, layer["id"], "orm"),
488
+ os.path.join(tex, "%s_orm.jpg" % layer["id"]),
489
+ )
490
+ if layer.get("height"):
491
+ # The layer's own height, which the game's `layerWeight` reads; fails closed like orm.
492
+ shutil.copyfile(
493
+ terrain_layer_texture(table_dir, textures, layer["id"], "h"),
494
+ os.path.join(tex, "%s_h.jpg" % layer["id"]),
495
+ )
479
496
 
480
497
  shipped = {key: table[key] for key in ("base", "breakup", "macro", "layers")}
481
498
  shipped["splat"] = {"masks": placed, "planes": len(planes), "size": size}
package/dist/index.d.ts CHANGED
@@ -1,12 +1,123 @@
1
1
  import { Document, Material } from '@gltf-transform/core';
2
2
 
3
+ interface IImageQualityFloor {
4
+ readonly ssim: number;
5
+ readonly meanDeltaE00: number;
6
+ }
7
+ interface IImageQuality {
8
+ readonly version: string;
9
+ readonly width: number;
10
+ readonly height: number;
11
+ readonly ssim: number;
12
+ readonly meanDeltaE00: number | null;
13
+ readonly slots: readonly string[];
14
+ readonly floor: IImageQualityFloor;
15
+ readonly status: "pass" | "below-floor" | "unvalidated-slots";
16
+ readonly alpha: {
17
+ readonly ssim: number;
18
+ readonly meanAbsoluteError: number;
19
+ readonly coverage: readonly {
20
+ readonly threshold: number;
21
+ readonly source: number;
22
+ readonly decoded: number;
23
+ readonly changedPixels: number;
24
+ }[];
25
+ };
26
+ }
27
+ interface ITextureQuality extends IImageQuality {
28
+ readonly rung?: string;
29
+ readonly compressionSkipped?: "block-size" | "not-smaller" | "below-floor";
30
+ readonly sourceWidth: number;
31
+ readonly sourceHeight: number;
32
+ readonly codec: string;
33
+ }
34
+
35
+ /**
36
+ * Compresses the images embedded *inside* a `.glb` — the half of a model the geometry passes
37
+ * never touched. A prop carrying three 2048x2048 JPEGs is a small file and 67 MB of VRAM once
38
+ * the driver decodes it; meshopt shrinks the other, cheaper half. This stage transcodes each
39
+ * embedded image to KTX2/Basis and declares `KHR_texture_basisu`, so the GPU stores blocks
40
+ * instead of RGBA, and caps the resolution so a 4K texture an image model produced cannot
41
+ * silently eat a phone's whole budget.
42
+ *
43
+ * Slot constraints feed one measured ladder: normal/data slots retain UASTC/none, while
44
+ * colour escalates from ETC1S through RDO and plain UASTC. Alpha coverage is checked separately;
45
+ * a forced codec must still meet the floor. Colour space comes from the slot too — `getTextureColorSpace`
46
+ * knows base colour and emissive are sRGB and that metallic-roughness and normals are data —
47
+ * so a linear map is never encoded through a perceptual metric.
48
+ *
49
+ * The encoder is the same in-process Basis WASM the standalone texture pass uses, so no new
50
+ * dependency and nothing extra to install. Everything fails closed: an image no decoder reads,
51
+ * an image whose dimensions are not a whole number of 4x4 blocks, and a texture whose binding
52
+ * did not survive the round trip all throw naming the model and the image.
53
+ */
54
+ interface IModelTextureOverride {
55
+ readonly codec: TextureCodec;
56
+ /** glTF slot the override applies to, e.g. `normalTexture` or `baseColorTexture`. */
57
+ readonly slot: string;
58
+ }
59
+ type TextureMaxSize = number | Readonly<Record<string, number>>;
60
+ interface IModelTexturesOptions {
61
+ /** Internal target-selected raw ASTC output. */
62
+ readonly astc?: boolean;
63
+ readonly floor?: Partial<IImageQualityFloor>;
64
+ /** Internal reporting switch; selection still measures and enforces the floor. */
65
+ readonly measureQuality?: boolean;
66
+ /**
67
+ * Resolved by the compiler from the target's runtime capabilities, never by a project: the
68
+ * target has no KTX2 decoder, so an over-cap embedded image is box-resampled and re-emitted as
69
+ * a PNG instead of being encoded. Everything else — authored-byte retention under the cap,
70
+ * per-image deduplication, colour space — is unchanged.
71
+ */
72
+ readonly decoderFree?: boolean;
73
+ /**
74
+ * Longest edge an embedded image may keep, default 2048. Larger images are box-resampled
75
+ * down preserving aspect ratio; smaller ones are never upscaled.
76
+ */
77
+ readonly maxSize?: TextureMaxSize;
78
+ readonly overrides?: readonly IModelTextureOverride[];
79
+ /** ETC1S encoder quality 1–255, default 150. Ignored for UASTC. */
80
+ readonly quality?: number;
81
+ }
82
+ /** What the stage did, recorded in the manifest and printed by the size report. */
83
+ interface IEmbeddedTextureSummary {
84
+ readonly quality: Readonly<Record<string, ITextureQuality>>;
85
+ readonly bytesAfter: number;
86
+ readonly bytesBefore: number;
87
+ readonly count: number;
88
+ /** Chosen codec per embedded image, keyed by texture name. */
89
+ readonly formats: Readonly<Record<string, string>>;
90
+ readonly gpuBytesAfter: number;
91
+ readonly gpuBytesBefore: number;
92
+ /** How many images the resolution cap actually downsampled. */
93
+ readonly resized: number;
94
+ /** Images retained at their authored bytes, by name, with the reason each one was not cooked. */
95
+ readonly skippedCompression: Readonly<Record<string, TextureSkipReason>>;
96
+ }
97
+ /** One embedded image as the verification compares it, source against re-read output. */
98
+ interface IModelTextureBinding {
99
+ readonly height: number;
100
+ readonly key: string;
101
+ /** KTX2 mip levels; 1 for a still-uncompressed source image. */
102
+ readonly levels: number;
103
+ readonly mimeType: string;
104
+ readonly slots: readonly string[];
105
+ readonly texCoords: readonly number[];
106
+ readonly width: number;
107
+ }
108
+ interface IModelTextureBindings {
109
+ /** `material -> slot=textureIndex@uvN` for the five core glTF slots, in document order. */
110
+ readonly materials: readonly string[];
111
+ readonly textures: readonly IModelTextureBinding[];
112
+ }
113
+
3
114
  /**
4
115
  * Encodes compiled textures to KTX2/Basis so the GPU stores them compressed instead of as
5
116
  * decoded RGBA (a 2048x2048 PNG is 16 MB in VRAM; BC7/ASTC hold it in a quarter of that).
6
117
  *
7
118
  * Codec choice is a declared property of the asset, never a guess: a config override wins,
8
- * then the `*_normal.*` / `_nrm.*` filename convention, then alpha presence (UASTC keeps
9
- * exact alpha and survives normal-map data; ETC1S does not). Mip chains are generated at
119
+ * then the `*_normal.*` / `_nrm.*` filename convention constrains the shared measured ladder.
120
+ * Colour escalates through ETC1S/RDO/UASTC/none; unknown alpha cutoffs require exact alpha. Mip chains are generated at
10
121
  * encode time — always — because an uploaded compressed texture without mips looks worse
11
122
  * than the PNG it replaced.
12
123
  *
@@ -18,10 +129,10 @@ type TextureCodec = "etc1s" | "none" | "uastc";
18
129
  /**
19
130
  * Why a source shipped uncompressed although its pass ran. `not-smaller` means encoding would
20
131
  * have grown the download; `block-size` means no block codec can address the source's
21
- * dimensions. Both keep the authored bytes; neither is a silent decision — the manifest carries
132
+ * dimensions; `below-floor` means no compressed rung passed. A capped source retains its resized pixels as PNG; none is a silent decision — the manifest carries
22
133
  * the reason and the report prints it.
23
134
  */
24
- type TextureSkipReason = "block-size" | "not-smaller";
135
+ type TextureSkipReason = "block-size" | "not-smaller" | "below-floor";
25
136
  interface ITextureOverride {
26
137
  readonly codec: TextureCodec;
27
138
  /** First matching override wins; matched against the logical path, e.g. "ui/x.png". */
@@ -30,8 +141,13 @@ interface ITextureOverride {
30
141
  readonly quality?: number;
31
142
  }
32
143
  interface ITexturePassOptions {
144
+ /** Compiler-selected mobile output; the quality ladder measures final ASTC bytes. */
145
+ readonly astc?: boolean;
146
+ readonly floor?: Partial<IImageQualityFloor>;
147
+ /** Internal reporting control; selection still measures and enforces the floor. */
148
+ readonly measureQuality?: boolean;
33
149
  /** Longest edge to retain; larger sources are downsampled without upscaling. */
34
- readonly maxSize?: number;
150
+ readonly maxSize?: TextureMaxSize;
35
151
  readonly overrides?: readonly ITextureOverride[];
36
152
  /** ETC1S encoder quality 1–255, default 150. Ignored for UASTC. */
37
153
  readonly quality?: number;
@@ -409,78 +525,6 @@ interface IModelVirtualOptions {
409
525
  readonly simplifyRatio?: number;
410
526
  }
411
527
 
412
- /**
413
- * Compresses the images embedded *inside* a `.glb` — the half of a model the geometry passes
414
- * never touched. A prop carrying three 2048x2048 JPEGs is a small file and 67 MB of VRAM once
415
- * the driver decodes it; meshopt shrinks the other, cheaper half. This stage transcodes each
416
- * embedded image to KTX2/Basis and declares `KHR_texture_basisu`, so the GPU stores blocks
417
- * instead of RGBA, and caps the resolution so a 4K texture an image model produced cannot
418
- * silently eat a phone's whole budget.
419
- *
420
- * Codec choice follows the declared use of the image, never a guess: a config override on the
421
- * glTF slot wins, then normal-map slots (UASTC survives normal data; ETC1S does not), then
422
- * alpha in the decoded pixels. Colour space comes from the slot too — `getTextureColorSpace`
423
- * knows base colour and emissive are sRGB and that metallic-roughness and normals are data —
424
- * so a linear map is never encoded through a perceptual metric.
425
- *
426
- * The encoder is the same in-process Basis WASM the standalone texture pass uses, so no new
427
- * dependency and nothing extra to install. Everything fails closed: an image no decoder reads,
428
- * an image whose dimensions are not a whole number of 4x4 blocks, and a texture whose binding
429
- * did not survive the round trip all throw naming the model and the image.
430
- */
431
- interface IModelTextureOverride {
432
- readonly codec: TextureCodec;
433
- /** glTF slot the override applies to, e.g. `normalTexture` or `baseColorTexture`. */
434
- readonly slot: string;
435
- }
436
- interface IModelTexturesOptions {
437
- /**
438
- * Resolved by the compiler from the target's runtime capabilities, never by a project: the
439
- * target has no KTX2 decoder, so an over-cap embedded image is box-resampled and re-emitted as
440
- * a PNG instead of being encoded. Everything else — authored-byte retention under the cap,
441
- * per-image deduplication, colour space — is unchanged.
442
- */
443
- readonly decoderFree?: boolean;
444
- /**
445
- * Longest edge an embedded image may keep, default 2048. Larger images are box-resampled
446
- * down preserving aspect ratio; smaller ones are never upscaled.
447
- */
448
- readonly maxSize?: number;
449
- readonly overrides?: readonly IModelTextureOverride[];
450
- /** ETC1S encoder quality 1–255, default 150. Ignored for UASTC. */
451
- readonly quality?: number;
452
- }
453
- /** What the stage did, recorded in the manifest and printed by the size report. */
454
- interface IEmbeddedTextureSummary {
455
- readonly bytesAfter: number;
456
- readonly bytesBefore: number;
457
- readonly count: number;
458
- /** Chosen codec per embedded image, keyed by texture name. */
459
- readonly formats: Readonly<Record<string, string>>;
460
- readonly gpuBytesAfter: number;
461
- readonly gpuBytesBefore: number;
462
- /** How many images the resolution cap actually downsampled. */
463
- readonly resized: number;
464
- /** Images retained at their authored bytes, by name, with the reason each one was not cooked. */
465
- readonly skippedCompression: Readonly<Record<string, TextureSkipReason>>;
466
- }
467
- /** One embedded image as the verification compares it, source against re-read output. */
468
- interface IModelTextureBinding {
469
- readonly height: number;
470
- readonly key: string;
471
- /** KTX2 mip levels; 1 for a still-uncompressed source image. */
472
- readonly levels: number;
473
- readonly mimeType: string;
474
- readonly slots: readonly string[];
475
- readonly texCoords: readonly number[];
476
- readonly width: number;
477
- }
478
- interface IModelTextureBindings {
479
- /** `material -> slot=textureIndex@uvN` for the five core glTF slots, in document order. */
480
- readonly materials: readonly string[];
481
- readonly textures: readonly IModelTextureBinding[];
482
- }
483
-
484
528
  /**
485
529
  * Images that models share, written once and referenced from every model that carries them.
486
530
  *
@@ -495,6 +539,7 @@ interface IModelTextureBindings {
495
539
  * bytes resolve to one URL and the browser cache, the bundle and the delete-test all see one file.
496
540
  */
497
541
  interface ISharedImage {
542
+ readonly quality?: ITextureQuality;
498
543
  readonly buffer: Buffer;
499
544
  /** `uastc`, `etc1s` or `none`, spelled into the filename so a cache hit knows what it found. */
500
545
  readonly codec: string;
@@ -624,6 +669,7 @@ declare function modelPass(options?: IModelPassOptions): IAssetPass;
624
669
  * so tests can pin the exact lines a build prints.
625
670
  */
626
671
  interface ITextureSizeRow {
672
+ readonly quality?: ITextureQuality;
627
673
  readonly after: number;
628
674
  readonly before: number;
629
675
  /** Set when the bytes shipped as authored; printed so a flat row is never left unexplained. */
@@ -634,6 +680,7 @@ interface ITextureSizeRow {
634
680
  declare function formatTextureSizes(rows: readonly ITextureSizeRow[]): readonly string[];
635
681
  /** What the model pass did to the images inside one `.glb`. */
636
682
  interface IEmbeddedTextureRow {
683
+ readonly quality?: Readonly<Record<string, ITextureQuality>>;
637
684
  readonly skippedCompression?: Readonly<Record<string, TextureSkipReason>>;
638
685
  readonly bytesAfter: number;
639
686
  readonly bytesBefore: number;
@@ -648,6 +695,8 @@ interface IEmbeddedTextureRow {
648
695
  interface IVirtualRow {
649
696
  readonly bakeSeconds: number;
650
697
  readonly clusters: number;
698
+ /** Primitives handed back to the discrete ladder because their DAG reduced nothing. */
699
+ readonly declined: number;
651
700
  readonly levels: number;
652
701
  readonly payloadBytes: number;
653
702
  readonly primitives: number;
@@ -972,6 +1021,14 @@ interface IAssetSourceConfig {
972
1021
  * default-on decision is a later phase (PRD-377 §8).
973
1022
  */
974
1023
  readonly lod?: boolean | IModelLodOptions | "none";
1024
+ /**
1025
+ * Also writes the native cooked package (`<output>/native/assets.tnpk`, TNPK v1) beside the
1026
+ * existing outputs: one Buffer entry per cooked binary buffer and one Texture entry per cooked
1027
+ * PNG or JPEG (decoded to RGBA8). Mesh, material and scene entries are out of scope until their
1028
+ * native loaders land.
1029
+ * Absent means off, so a web-only project pays nothing.
1030
+ */
1031
+ readonly nativePackage?: boolean;
975
1032
  readonly output?: string;
976
1033
  readonly source?: string;
977
1034
  readonly targets?: IAssetTargets;
@@ -1021,7 +1078,8 @@ interface IAudioConfig {
1021
1078
  readonly seamMaxRatio?: number;
1022
1079
  }
1023
1080
  interface ITexturesConfig {
1024
- readonly maxSize?: number;
1081
+ readonly floor?: ITexturePassOptions["floor"];
1082
+ readonly maxSize?: TextureMaxSize;
1025
1083
  readonly overrides?: readonly ITextureOverride[];
1026
1084
  readonly quality?: number;
1027
1085
  }
@@ -1046,31 +1104,16 @@ interface IAssetCompileOptions {
1046
1104
  */
1047
1105
  readonly processingOrder?: "reversed" | "sorted";
1048
1106
  /**
1049
- * The platform this bake is for, which decides whether compression can ship at all.
1050
- *
1051
- * Android and iOS run the native host without WebAssembly, so they carry no Basis transcoder
1052
- * and no Meshopt decoder: a `.ktx2` texture or a meshopt-compressed mesh in a mobile bundle is
1053
- * a black screen, and `threenative build` refuses one with `TN_NATIVE_KTX2_UNSUPPORTED`. Web
1054
- * and desktop decode both.
1055
- *
1056
- * Absent means web — a direct `compileAssets` call, or a project that compiles once and serves
1057
- * the result. `threenative build` always names its `--target`, so the passes that a platform
1058
- * cannot decode drop for that build and stay on for every other one. This is the whole reason
1059
- * `assets.textures: "none"` used to be pinned in the scaffolded config: the author was asked to
1060
- * choose one constant for four targets, and every game that wanted Android shipped its web
1061
- * build uncompressed too. The build knows its target; it decides.
1107
+ * The conservative default decoder policy: web/desktop keep the existing loaders;
1108
+ * Android/iOS stay decoder-free until their packaged loaders are qualified. A V8 engine
1109
+ * alone does not establish a working Basis, Meshopt or Draco path. Absent means web.
1110
+ * Native builds provide runtimeDecoders from their selected artifact instead.
1062
1111
  */
1063
1112
  readonly platform?: "android" | "desktop" | "ios" | "web";
1064
- /**
1065
- * What the runtime that will execute this bake can decode, when the caller has probed it.
1066
- *
1067
- * `platform` is a proxy: it answers "android and iOS have no WebAssembly" for the targets this
1068
- * repository ships. A desktop host can also lack WebAssembly — the Linux arm64 lane builds
1069
- * QuickJS over wgpu-native — and then the same meshopt/KTX2 passes it would keep produce an
1070
- * asset nothing can decode. `threenative build` reads the engine from the runtime binary it will
1071
- * package and passes the real capability here; absent, the platform-derived default stands.
1072
- */
1113
+ /** Which compressed outputs the selected runtime's bundle can decode, independently of WASM. */
1073
1114
  readonly runtimeDecoders?: IAssetRuntimeDecoderCapabilities;
1115
+ /** Exact selected runtime/cohort identity; changing it invalidates cooked output. */
1116
+ readonly runtimeIdentity?: string;
1074
1117
  readonly source?: string;
1075
1118
  /** Overrides resolution of three's Basis transcoder for the copy into the output root. */
1076
1119
  readonly transcoder?: IBasisTranscoder;