@needle-tools/gltf-build-pipeline 2.15.0-next.a25e7ea → 2.15.0-next.ca2fe5b

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.
Files changed (33) hide show
  1. package/dist/cache/cache.d.ts +1 -0
  2. package/dist/extensions/NEEDLE_lightmaps/index.d.ts +41 -0
  3. package/dist/extensions/NEEDLE_lightmaps/index.js +249 -0
  4. package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +21 -7
  5. package/dist/extensions/NEEDLE_opaque/index.js +1 -1
  6. package/dist/extensions/NEEDLE_pmrem/index.d.ts +21 -0
  7. package/dist/extensions/NEEDLE_pmrem/index.js +95 -0
  8. package/dist/extensions/index.d.ts +2 -0
  9. package/dist/extensions/index.js +7 -1
  10. package/dist/scripts/pack-gltf.js +17 -5
  11. package/dist/transforms/needle_pmrem.d.ts +16 -0
  12. package/dist/transforms/needle_pmrem.js +185 -0
  13. package/dist/transforms/needle_progressive.js +14 -6
  14. package/dist/transforms/needle_texture_transform.d.ts +2 -0
  15. package/dist/transforms/needle_texture_transform.js +12 -2
  16. package/dist/utils/test-assert.d.ts +8 -0
  17. package/dist/utils/test-assert.js +18 -0
  18. package/dist/utils/validate.js +1 -1
  19. package/dist/utils/version.gen.d.ts +1 -1
  20. package/dist/utils/version.gen.js +1 -1
  21. package/package.json +18 -4
  22. package/tools/pmrem/basis/linux/basisu +0 -0
  23. package/tools/pmrem/basis/osx/basisu +0 -0
  24. package/tools/pmrem/basis/package.json +3 -0
  25. package/tools/pmrem/basis/wasm/basis_encoder_threads.js +6693 -0
  26. package/tools/pmrem/basis/wasm/basis_encoder_threads.wasm +0 -0
  27. package/tools/pmrem/basis/win/basisu.exe +0 -0
  28. package/tools/pmrem/basis/win/basisu_help.txt +0 -0
  29. package/tools/pmrem/basis/win/ocl_kernels.cl +1290 -0
  30. package/tools/pmrem/pkg/pmrem_wasm.d.ts +68 -0
  31. package/tools/pmrem/pkg/pmrem_wasm.js +401 -0
  32. package/tools/pmrem/pkg/pmrem_wasm_bg.wasm +0 -0
  33. package/tsconfig.json +0 -20
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node" resolution-mode="require"/>
2
3
  import { ExtensibleProperty } from "@gltf-transform/core";
3
4
  import { Logger } from "@donmccurdy/caporal";
4
5
  /**
@@ -0,0 +1,41 @@
1
+ import { Extension, PropertyType, ReaderContext, WriterContext } from '@gltf-transform/core';
2
+ /**
3
+ * NEEDLE_lightmaps gltf-transform extension.
4
+ *
5
+ * Handles both embedded (`/textures/N` pointer) and external (`sunrise.exr` pointer) lightmaps.
6
+ *
7
+ * - **preread(TEXTURE)**: Injects external EXR files into the JSON document so the core reader
8
+ * creates proper Texture objects from them.
9
+ * - **read()**: Resolves `/textures/N` pointers to Texture objects. Sets references so that
10
+ * gltf-transform doesn't prune/dedup referenced textures.
11
+ * - **write()**: Updates pointer strings:
12
+ * - Embedded textures: resolves to the new `/textures/N` index in the output.
13
+ * - External textures: uses the Texture's current URI (e.g. updated by PMREM to `.pmrem.ktx2`).
14
+ */
15
+ export declare class NEEDLE_lightmaps_ext extends Extension {
16
+ static readonly EXTENSION_NAME = "NEEDLE_lightmaps";
17
+ readonly extensionName = "NEEDLE_lightmaps";
18
+ readonly prereadTypes: PropertyType[];
19
+ readonly prewriteTypes: PropertyType[];
20
+ /** Set before io.read() to allow resolving external file paths in preread */
21
+ static inputFile: string;
22
+ /** The raw extension JSON from the input (mutated in-place during write) */
23
+ private extensionData;
24
+ /** Entries with resolved Texture references */
25
+ private resolved;
26
+ /** Image indices injected during preread (for external EXR files) */
27
+ private injectedImageIndices;
28
+ /** The ExtensionProperty that holds references to prevent dedup/prune */
29
+ private lightmapProp;
30
+ preread(context: ReaderContext, _propertyType: PropertyType): this;
31
+ read(context: ReaderContext): this;
32
+ prewrite(context: WriterContext, _propertyType: PropertyType): this;
33
+ write(context: WriterContext): this;
34
+ private pointersResolved;
35
+ private resolvePointers;
36
+ /**
37
+ * Find (or create) a texture def in the output JSON that references the given Texture.
38
+ * This replicates the logic from the opaque system's TextureIndexResolver.
39
+ */
40
+ private findOrCreateTextureIndex;
41
+ }
@@ -0,0 +1,249 @@
1
+ import { Extension, ExtensionProperty, PropertyType } from '@gltf-transform/core';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+ import { isLightmap } from '../../utils/texture.js';
5
+ const EXTENSION_NAME = 'NEEDLE_lightmaps';
6
+ /**
7
+ * Opaque-like ExtensionProperty that holds NEEDLE_lightmaps data on the Root.
8
+ * This keeps gltf-transform from garbage-collecting referenced textures.
9
+ */
10
+ class LightmapProperty extends ExtensionProperty {
11
+ static EXTENSION_NAME = EXTENSION_NAME;
12
+ propertyType = 'LightmapProperty';
13
+ parentTypes = [PropertyType.ROOT];
14
+ init() {
15
+ this.extensionName = EXTENSION_NAME;
16
+ }
17
+ getDefaults() {
18
+ return Object.assign(super.getDefaults(), {});
19
+ }
20
+ /** Public wrapper around protected setRef to register references */
21
+ setReference(name, value) {
22
+ this.setRef(name, value);
23
+ }
24
+ }
25
+ /**
26
+ * NEEDLE_lightmaps gltf-transform extension.
27
+ *
28
+ * Handles both embedded (`/textures/N` pointer) and external (`sunrise.exr` pointer) lightmaps.
29
+ *
30
+ * - **preread(TEXTURE)**: Injects external EXR files into the JSON document so the core reader
31
+ * creates proper Texture objects from them.
32
+ * - **read()**: Resolves `/textures/N` pointers to Texture objects. Sets references so that
33
+ * gltf-transform doesn't prune/dedup referenced textures.
34
+ * - **write()**: Updates pointer strings:
35
+ * - Embedded textures: resolves to the new `/textures/N` index in the output.
36
+ * - External textures: uses the Texture's current URI (e.g. updated by PMREM to `.pmrem.ktx2`).
37
+ */
38
+ export class NEEDLE_lightmaps_ext extends Extension {
39
+ static EXTENSION_NAME = EXTENSION_NAME;
40
+ extensionName = EXTENSION_NAME;
41
+ prereadTypes = [PropertyType.TEXTURE];
42
+ prewriteTypes = [PropertyType.MATERIAL];
43
+ /** Set before io.read() to allow resolving external file paths in preread */
44
+ static inputFile = '';
45
+ /** The raw extension JSON from the input (mutated in-place during write) */
46
+ extensionData = null;
47
+ /** Entries with resolved Texture references */
48
+ resolved = [];
49
+ /** Image indices injected during preread (for external EXR files) */
50
+ injectedImageIndices = new Map();
51
+ /** The ExtensionProperty that holds references to prevent dedup/prune */
52
+ lightmapProp = null;
53
+ // ── preread ──────────────────────────────────────────────────
54
+ preread(context, _propertyType) {
55
+ const json = context.jsonDoc.json;
56
+ const ext = json.extensions?.[EXTENSION_NAME];
57
+ if (!ext?.textures)
58
+ return this;
59
+ const inputFile = NEEDLE_lightmaps_ext.inputFile;
60
+ const inputDir = inputFile ? dirname(inputFile) : '';
61
+ for (const entry of ext.textures) {
62
+ // Skip embedded pointers — handled in read()
63
+ if (entry.pointer.startsWith('/') || entry.pointer.startsWith('textures/'))
64
+ continue;
65
+ // External file path (e.g. "sunrise.exr")
66
+ if (!entry.pointer.match(/\.exr$/i))
67
+ continue;
68
+ const exrPath = inputDir ? join(inputDir, entry.pointer) : '';
69
+ if (!exrPath || !existsSync(exrPath)) {
70
+ this.document.getLogger().warn(`NEEDLE_lightmaps: External EXR not found: ${exrPath || entry.pointer}`);
71
+ continue;
72
+ }
73
+ // Read EXR bytes and inject into JSON so the core reader creates a Texture
74
+ const exrBytes = readFileSync(exrPath);
75
+ json.images = json.images || [];
76
+ json.textures = json.textures || [];
77
+ const imageIndex = json.images.length;
78
+ json.images.push({
79
+ uri: entry.pointer,
80
+ mimeType: 'image/exr',
81
+ name: entry.pointer,
82
+ });
83
+ json.textures.push({
84
+ source: imageIndex,
85
+ name: entry.pointer,
86
+ });
87
+ context.jsonDoc.resources[entry.pointer] = new Uint8Array(exrBytes);
88
+ this.injectedImageIndices.set(entry.pointer, imageIndex);
89
+ this.document.getLogger().info(`NEEDLE_lightmaps: Registered external EXR "${entry.pointer}" as image[${imageIndex}]`);
90
+ }
91
+ return this;
92
+ }
93
+ // ── read ─────────────────────────────────────────────────────
94
+ read(context) {
95
+ const json = context.jsonDoc.json;
96
+ const ext = json.extensions?.[EXTENSION_NAME];
97
+ if (!ext?.textures)
98
+ return this;
99
+ // Store the raw extension data — we'll mutate the pointers during write
100
+ this.extensionData = ext;
101
+ // Create the property and attach to root
102
+ const root = this.document.getRoot();
103
+ this.lightmapProp = new LightmapProperty(this.document.getGraph());
104
+ root.setExtension(EXTENSION_NAME, this.lightmapProp);
105
+ const textureDefs = json.textures || [];
106
+ const logger = this.document.getLogger();
107
+ for (const entry of ext.textures) {
108
+ let texture = null;
109
+ let inputTextureDef;
110
+ if (entry.pointer.startsWith('/textures/') || entry.pointer.startsWith('textures/')) {
111
+ // Embedded pointer — resolve to Texture via the JSON texture definition
112
+ const prefix = entry.pointer.startsWith('/') ? '/textures/' : 'textures/';
113
+ const texIndex = parseInt(entry.pointer.substring(prefix.length));
114
+ const texDef = textureDefs[texIndex];
115
+ if (texDef) {
116
+ inputTextureDef = texDef;
117
+ const imageIndex = texDef.source
118
+ ?? texDef.extensions?.['EXT_texture_exr']?.source
119
+ ?? texDef.extensions?.['KHR_texture_basisu']?.source;
120
+ if (imageIndex !== undefined) {
121
+ texture = context.textures[imageIndex] ?? null;
122
+ }
123
+ }
124
+ }
125
+ else if (entry.pointer.match(/\.exr$/i)) {
126
+ // External pointer — find the Texture created from our injected image
127
+ const imageIndex = this.injectedImageIndices.get(entry.pointer);
128
+ if (imageIndex !== undefined) {
129
+ texture = context.textures[imageIndex] ?? null;
130
+ inputTextureDef = textureDefs.find(t => t.source === imageIndex);
131
+ }
132
+ }
133
+ if (texture) {
134
+ this.resolved.push({ entry, texture, inputTextureDef });
135
+ // Set a reference so gltf-transform won't prune/dedup this texture
136
+ const refName = `lightmap_${entry.pointer}`;
137
+ this.lightmapProp.setReference(refName, texture);
138
+ // Also keep a reference to the buffer
139
+ const buffer = this.document.getRoot().listBuffers()[0];
140
+ if (buffer) {
141
+ this.lightmapProp.setReference('buffer', buffer);
142
+ }
143
+ }
144
+ else {
145
+ logger.warn(`NEEDLE_lightmaps: Could not resolve texture for pointer "${entry.pointer}"`);
146
+ }
147
+ }
148
+ // Lightmap workaround: prevent compression on lightmap textures (non-EXR)
149
+ const textures = this.document.getRoot().listTextures();
150
+ for (const tex of textures) {
151
+ if (isLightmap(tex) && tex.getMimeType() !== 'image/exr') {
152
+ const refName = `lightmapTexture_${tex.getName()}`;
153
+ this.lightmapProp.setReference(refName, tex);
154
+ logger.info(`Found lightmap texture "${tex.getName()}" (${tex.getMimeType()}) in ${EXTENSION_NAME}`);
155
+ }
156
+ }
157
+ return this;
158
+ }
159
+ // ── prewrite ─────────────────────────────────────────────────
160
+ prewrite(context, _propertyType) {
161
+ // Resolve pointers early so json.textures is populated for textures
162
+ // not used by any material (same pattern as opaque system)
163
+ this.resolvePointers(context);
164
+ return this;
165
+ }
166
+ // ── write ────────────────────────────────────────────────────
167
+ write(context) {
168
+ if (!this.extensionData)
169
+ return this;
170
+ // Resolve any remaining pointers
171
+ this.resolvePointers(context);
172
+ // Write the extension data to the output JSON
173
+ context.jsonDoc.json.extensions = context.jsonDoc.json.extensions || {};
174
+ context.jsonDoc.json.extensions[EXTENSION_NAME] = this.extensionData;
175
+ return this;
176
+ }
177
+ // ── private ──────────────────────────────────────────────────
178
+ pointersResolved = false;
179
+ resolvePointers(context) {
180
+ if (this.pointersResolved)
181
+ return;
182
+ this.pointersResolved = true;
183
+ for (const { entry, texture, inputTextureDef } of this.resolved) {
184
+ if (entry.pointer.startsWith('/textures/') || entry.pointer.startsWith('textures/')) {
185
+ // Embedded: find the new texture index in the output JSON
186
+ const newIndex = this.findOrCreateTextureIndex(context, texture, inputTextureDef);
187
+ if (newIndex !== undefined) {
188
+ entry.pointer = `/textures/${newIndex}`;
189
+ }
190
+ }
191
+ else {
192
+ // External: use the texture's current URI (may have been updated by PMREM)
193
+ const uri = texture.getURI();
194
+ if (uri) {
195
+ entry.pointer = uri;
196
+ // Ensure the image definition in the output JSON has the URI set.
197
+ // When PMREM clears image data (setImage(null)) for external textures,
198
+ // gltf-transform's core writer won't set a uri or bufferView on the image.
199
+ const imageIndex = context.imageIndexMap.get(texture);
200
+ if (imageIndex !== undefined) {
201
+ const images = context.jsonDoc.json.images;
202
+ if (images && images[imageIndex]) {
203
+ images[imageIndex].uri = uri;
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ /**
211
+ * Find (or create) a texture def in the output JSON that references the given Texture.
212
+ * This replicates the logic from the opaque system's TextureIndexResolver.
213
+ */
214
+ findOrCreateTextureIndex(context, texture, inputTextureDef) {
215
+ const imageIndex = context.imageIndexMap.get(texture);
216
+ if (imageIndex === undefined)
217
+ return undefined;
218
+ const textures = context.jsonDoc.json.textures = context.jsonDoc.json.textures || [];
219
+ // Try to find an existing texture def pointing to the same image
220
+ for (let i = 0; i < textures.length; i++) {
221
+ const texDef = textures[i];
222
+ const source = texDef.source
223
+ ?? texDef.extensions?.['KHR_texture_basisu']?.source
224
+ ?? texDef.extensions?.['NEEDLE_pmrem']?.source;
225
+ if (source === imageIndex) {
226
+ return i;
227
+ }
228
+ }
229
+ // Not found — create a new texture definition
230
+ let extensions = inputTextureDef?.extensions ? { ...inputTextureDef.extensions } : undefined;
231
+ if (extensions) {
232
+ // Remove consumed extensions
233
+ delete extensions['NEEDLE_progressive_texture_settings'];
234
+ // Remove EXT_texture_exr if texture was converted
235
+ if (extensions['EXT_texture_exr'] && texture.getMimeType() !== 'image/exr') {
236
+ delete extensions['EXT_texture_exr'];
237
+ }
238
+ if (Object.keys(extensions).length === 0)
239
+ extensions = undefined;
240
+ }
241
+ const hasExtSource = extensions && Object.values(extensions).some((v) => v?.source !== undefined);
242
+ const texDef = {
243
+ source: hasExtSource ? undefined : imageIndex,
244
+ extensions,
245
+ };
246
+ textures.push(texDef);
247
+ return textures.length - 1;
248
+ }
249
+ }
@@ -2,6 +2,7 @@ import { Extension, ExtensionProperty, PropertyType, Node, TextureChannel } from
2
2
  import { getTexture, tryGetInArray, writeExtension } from '../utils.js';
3
3
  import { isLightmap } from '../../utils/texture.js';
4
4
  import { NEEDLE_progressive_texture_settings } from '../NEEDLE_progressive_texture_settings/index.js';
5
+ import { testAssert } from '../../utils/test-assert.js';
5
6
  const ALL_PROPERTY_TYPES = [];
6
7
  for (const key in PropertyType) {
7
8
  ALL_PROPERTY_TYPES.push(PropertyType[key]);
@@ -414,11 +415,16 @@ class TextureHandler {
414
415
  }
415
416
  }
416
417
  write(ctx) {
418
+ // If the texture was converted (e.g. EXR → KTX2 via PMREM), skip writing the old extension
419
+ if (this.key === "EXT_texture_exr" && this.textureRef.getMimeType() !== "image/exr") {
420
+ return;
421
+ }
417
422
  // Resolve the current image index from the gltf-transform graph reference.
418
423
  // This is stable even after dedup/prune operations that re-index images.
419
424
  const imageIndex = ctx.imageIndexMap.get(this.textureRef);
425
+ testAssert(imageIndex !== undefined || this.key === "EXT_texture_exr", `imageIndexMap missing texture ref for "${this.key}": ${this.textureRef.getName() || "<unnamed>"}`);
420
426
  if (imageIndex === undefined) {
421
- if (this.key !== "EXT_texture_exr" && this.debug) {
427
+ if (this.debug) {
422
428
  console.warn("WARN: failed to resolve image index for texture via imageIndexMap:", this.key, this.textureRef.getName() || "<unnamed>");
423
429
  }
424
430
  return;
@@ -430,10 +436,10 @@ class TextureHandler {
430
436
  return;
431
437
  const textureImageIndex = this.tryGetSourceIndex(textureDef);
432
438
  if (imageIndex === textureImageIndex) {
433
- if (!textureDef.extensions || !textureDef.extensions[this.key]) {
434
- // assign texture extension again
439
+ foundTexture = true;
440
+ if (!textureDef.extensions?.[this.key]) {
441
+ // assign texture extension
435
442
  textureDef.extensions = textureDef.extensions || {};
436
- foundTexture = true;
437
443
  if (this.debug)
438
444
  console.log("Re-Assign extension:", this.key, "at", i);
439
445
  const newExt = { ...this.ext };
@@ -441,10 +447,13 @@ class TextureHandler {
441
447
  newExt.source = imageIndex;
442
448
  textureDef.extensions[this.key] = newExt;
443
449
  }
450
+ // else: extension already written by another handler for the same image
451
+ // (happens when gltf-transform dedup merges texture defs that shared the same source)
444
452
  }
445
453
  });
454
+ testAssert(foundTexture || this.key === "EXT_texture_exr", `failed to match texture for extension "${this.key}", imageIndex=${imageIndex}, textures=${textureDefs.length}, name=${this.textureRef.getName() || "<unnamed>"}, type=${this.textureRef.getMimeType()}`);
446
455
  if (foundTexture === false) {
447
- if (this.key !== "EXT_texture_exr" && this.debug) {
456
+ if (this.debug) {
448
457
  console.warn("WARN: failed to re-assign extension:", this.key, "imageIndex", imageIndex, textureDefs.length, this.textureRef.getName() || "<unnamed>", this.textureDefinition);
449
458
  }
450
459
  }
@@ -862,7 +871,8 @@ class TextureIndexResolver extends PointerResolver {
862
871
  resolve(context) {
863
872
  const imgIndex = context.imageIndexMap.get(this.inputObject);
864
873
  if (imgIndex === undefined) {
865
- console.error("ERR: image is missing", imgIndex);
874
+ testAssert(false, `imageIndexMap missing texture: ${this.inputObject.getName() || "<unnamed>"}`);
875
+ console.error("ERR: image is missing");
866
876
  return -1;
867
877
  }
868
878
  // ensure we have a texture array
@@ -883,10 +893,14 @@ class TextureIndexResolver extends PointerResolver {
883
893
  // create a new texture def entry
884
894
  if (this.debug)
885
895
  console.log("Could not find texture, creating a new one: " + this._debugName);
886
- // Remove settings-only extensions that are consumed during processing and should not appear in the output.
896
+ // Remove extensions that are consumed during processing and should not appear in the output.
887
897
  let extensions = this.inputDefinition.extensions ? { ...this.inputDefinition.extensions } : undefined;
888
898
  if (extensions) {
889
899
  delete extensions[NEEDLE_progressive_texture_settings.EXTENSION_NAME];
900
+ // Remove EXT_texture_exr if the texture was converted (e.g. EXR → KTX2 via PMREM)
901
+ if (extensions["EXT_texture_exr"] && this.inputObject.getMimeType() !== "image/exr") {
902
+ delete extensions["EXT_texture_exr"];
903
+ }
890
904
  if (Object.keys(extensions).length === 0)
891
905
  extensions = undefined;
892
906
  }
@@ -10,7 +10,7 @@ export const ALL_EXTENSIONS = [
10
10
  createOpaqueExtension("NEEDLE_gameobject_data", PropertyType.NODE, { ...opaqueOptions }),
11
11
  createOpaqueExtension("NEEDLE_components", PropertyType.NODE, { ...opaqueOptions }),
12
12
  createOpaqueExtension("NEEDLE_persistent_assets", PropertyType.ROOT, { ...opaqueOptions }),
13
- createOpaqueExtension("NEEDLE_lightmaps", PropertyType.ROOT, { ...opaqueOptions }),
13
+ // NEEDLE_lightmaps is now handled by its own dedicated extension (NEEDLE_lightmaps/index.ts)
14
14
  createOpaqueExtension("NEEDLE_lighting_settings", PropertyType.ROOT, { ...opaqueOptions }),
15
15
  createOpaqueExtension("NEEDLE_render_objects", PropertyType.ROOT, { ...opaqueOptions }),
16
16
  createOpaqueExtension("NEEDLE_progressive", [PropertyType.TEXTURE, PropertyType.MATERIAL, PropertyType.MESH], { ...opaqueOptions }),
@@ -0,0 +1,21 @@
1
+ import { Extension, PropertyType, ReaderContext, Texture, WriterContext } from '@gltf-transform/core';
2
+ /**
3
+ * NEEDLE_pmrem extension for gltf-transform.
4
+ * Marks textures that have been pre-processed with PMREM (EXR → KTX2 HDR).
5
+ * At write time, adds `NEEDLE_pmrem: { source: imageIndex }` to each marked texture
6
+ * so the Needle Engine runtime can load them correctly with CubeUVReflectionMapping.
7
+ */
8
+ export declare class NEEDLE_pmrem extends Extension {
9
+ static readonly EXTENSION_NAME = "NEEDLE_pmrem";
10
+ readonly extensionName = "NEEDLE_pmrem";
11
+ /** Textures that were converted from EXR via PMREM */
12
+ private readonly pmremTextures;
13
+ /** Mark a texture as PMREM-converted */
14
+ addTexture(texture: Texture): void;
15
+ /** @hidden */
16
+ preread(context: ReaderContext, _propertyType: PropertyType): this;
17
+ /** @hidden */
18
+ read(context: ReaderContext): this;
19
+ /** @hidden */
20
+ write(context: WriterContext): this;
21
+ }
@@ -0,0 +1,95 @@
1
+ import { Extension } from '@gltf-transform/core';
2
+ const EXTENSION_NAME = 'NEEDLE_pmrem';
3
+ /**
4
+ * NEEDLE_pmrem extension for gltf-transform.
5
+ * Marks textures that have been pre-processed with PMREM (EXR → KTX2 HDR).
6
+ * At write time, adds `NEEDLE_pmrem: { source: imageIndex }` to each marked texture
7
+ * so the Needle Engine runtime can load them correctly with CubeUVReflectionMapping.
8
+ */
9
+ export class NEEDLE_pmrem extends Extension {
10
+ static EXTENSION_NAME = EXTENSION_NAME;
11
+ extensionName = EXTENSION_NAME;
12
+ /** Textures that were converted from EXR via PMREM */
13
+ pmremTextures = new Set();
14
+ /** Mark a texture as PMREM-converted */
15
+ addTexture(texture) {
16
+ this.pmremTextures.add(texture);
17
+ }
18
+ /** @hidden */
19
+ preread(context, _propertyType) {
20
+ const json = context.jsonDoc.json;
21
+ // For each texture with NEEDLE_pmrem, ensure `source` is set so the core reader
22
+ // links the texture to its image (we removed KHR_texture_basisu.source at write time).
23
+ for (const textureDef of json.textures ?? []) {
24
+ const pmrem = textureDef.extensions?.[EXTENSION_NAME];
25
+ if (pmrem?.source !== undefined && textureDef.source === undefined) {
26
+ textureDef.source = pmrem.source;
27
+ }
28
+ }
29
+ // Also ensure the image has the correct mime type
30
+ for (const textureDef of json.textures ?? []) {
31
+ const pmrem = textureDef.extensions?.[EXTENSION_NAME];
32
+ if (pmrem?.source !== undefined) {
33
+ const imageDef = json.images?.[pmrem.source];
34
+ if (imageDef && !imageDef.mimeType) {
35
+ imageDef.mimeType = 'image/ktx2';
36
+ }
37
+ }
38
+ }
39
+ return this;
40
+ }
41
+ /** @hidden */
42
+ read(context) {
43
+ const json = context.jsonDoc.json;
44
+ const textureDefs = json.textures ?? [];
45
+ const textures = this.document.getRoot().listTextures();
46
+ for (let i = 0; i < textureDefs.length; i++) {
47
+ const textureDef = textureDefs[i];
48
+ const pmrem = textureDef.extensions?.[EXTENSION_NAME];
49
+ if (pmrem?.source === undefined)
50
+ continue;
51
+ const texture = textures[i];
52
+ if (!texture)
53
+ continue;
54
+ texture.setMimeType('image/ktx2');
55
+ this.pmremTextures.add(texture);
56
+ }
57
+ return this;
58
+ }
59
+ /** @hidden */
60
+ write(context) {
61
+ const jsonDoc = context.jsonDoc;
62
+ for (const texture of this.pmremTextures) {
63
+ const imageIndex = context.imageIndexMap.get(texture);
64
+ if (imageIndex === undefined)
65
+ continue;
66
+ // Find texture defs that reference this image
67
+ for (const textureDef of jsonDoc.json.textures ?? []) {
68
+ const source = textureDef.source
69
+ ?? textureDef.extensions?.['KHR_texture_basisu']?.source;
70
+ if (source === imageIndex) {
71
+ textureDef.extensions = textureDef.extensions || {};
72
+ textureDef.extensions[EXTENSION_NAME] = { source: imageIndex };
73
+ // Remove KHR_texture_basisu so the runtime NEEDLE_pmrem plugin handles loading
74
+ // (otherwise three.js's built-in KHR_texture_basisu handler intercepts it first)
75
+ delete textureDef.extensions['KHR_texture_basisu'];
76
+ }
77
+ }
78
+ }
79
+ // If no textures still use KHR_texture_basisu, remove it from extensionsUsed/Required
80
+ // to prevent KHRTextureBasisu.preread() from crashing when re-reading the GLB
81
+ const anyBasisu = (jsonDoc.json.textures ?? []).some((t) => t.extensions?.['KHR_texture_basisu']);
82
+ if (!anyBasisu) {
83
+ const remove = (arr) => {
84
+ if (!arr)
85
+ return;
86
+ const idx = arr.indexOf('KHR_texture_basisu');
87
+ if (idx >= 0)
88
+ arr.splice(idx, 1);
89
+ };
90
+ remove(jsonDoc.json.extensionsUsed);
91
+ remove(jsonDoc.json.extensionsRequired);
92
+ }
93
+ return this;
94
+ }
95
+ }
@@ -4,4 +4,6 @@ export * from "./NEEDLE_compression_texture/index.js";
4
4
  export * from "./NEEDLE_progressive/index.js";
5
5
  export * from "./NEEDLE_progressive_texture_settings/index.js";
6
6
  export * from "./NEEDLE_progressive_mesh_settings/index.js";
7
+ export * from "./NEEDLE_pmrem/index.js";
8
+ export * from "./NEEDLE_lightmaps/index.js";
7
9
  export declare function registerExtensions(node: any): void;
@@ -5,13 +5,19 @@ export * from "./NEEDLE_compression_texture/index.js";
5
5
  export * from "./NEEDLE_progressive/index.js";
6
6
  export * from "./NEEDLE_progressive_texture_settings/index.js";
7
7
  export * from "./NEEDLE_progressive_mesh_settings/index.js";
8
+ export * from "./NEEDLE_pmrem/index.js";
9
+ export * from "./NEEDLE_lightmaps/index.js";
8
10
  import { ALL_EXTENSIONS } from '@gltf-transform/extensions';
9
11
  import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS } from "./NEEDLE_opaque/index.js";
10
12
  import { NEEDLE_progressive } from './NEEDLE_progressive/index.js';
13
+ import { NEEDLE_pmrem } from './NEEDLE_pmrem/index.js';
14
+ import { NEEDLE_lightmaps_ext } from './NEEDLE_lightmaps/index.js';
11
15
  export function registerExtensions(node) {
12
16
  node.registerExtensions(ALL_EXTENSIONS);
13
17
  node.registerExtensions(NEEDLE_EXTENSIONS);
14
18
  node.registerExtensions([
15
- NEEDLE_progressive
19
+ NEEDLE_progressive,
20
+ NEEDLE_pmrem,
21
+ NEEDLE_lightmaps_ext,
16
22
  ]);
17
23
  }
@@ -5,10 +5,11 @@ import draco3d from 'draco3dgltf';
5
5
  import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
6
6
  import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer';
7
7
  import { dedup, metalRough, prune, resample } from '@gltf-transform/functions';
8
- import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE_mesh_compression, } from '../extensions/index.js';
8
+ import { ALL_EXTENSIONS as NEEDLE_EXTENSIONS, NEEDLE_compression_texture, NEEDLE_mesh_compression, NEEDLE_pmrem, NEEDLE_lightmaps_ext, } from '../extensions/index.js';
9
9
  import { isLOD, isMeshLOD, needle_animation_transform, needle_asset, needle_mesh_transform, needle_texture_transform, } from '../transforms/index.js';
10
10
  import { getOutputPath, getVersion, ioTryReadWithMissingResources, writeNodeIO } from "../utils/index.js";
11
11
  import { addToCache, getHash, tryGetFromCache, } from "../cache/index.js";
12
+ import { TestAssertionError } from '../utils/test-assert.js';
12
13
  /** Compress a glTF file
13
14
  */
14
15
  export async function packGLTF(inputFile, outputFile, options) {
@@ -56,6 +57,8 @@ export async function packGLTF(inputFile, outputFile, options) {
56
57
  .registerExtensions([
57
58
  NEEDLE_compression_texture,
58
59
  NEEDLE_mesh_compression,
60
+ NEEDLE_pmrem,
61
+ NEEDLE_lightmaps_ext,
59
62
  ])
60
63
  .registerDependencies({
61
64
  'draco3d.decoder': await draco3d.createDecoderModule(),
@@ -63,9 +66,16 @@ export async function packGLTF(inputFile, outputFile, options) {
63
66
  'meshopt.decoder': MeshoptDecoder,
64
67
  'meshopt.encoder': MeshoptEncoder,
65
68
  });
66
- const document = await ioTryReadWithMissingResources(io, inputFile); // await io.read(inputFile);
67
- /** @ts-ignore */
68
- document.getLogger().verbosity = options.verbose ? Verbosity.DEBUG : options.debug ? Verbosity.INFO : Verbosity.WARN;
69
+ // Set input file path so NEEDLE_lightmaps can resolve external EXR files during preread
70
+ NEEDLE_lightmaps_ext.inputFile = inputFile;
71
+ const document = await ioTryReadWithMissingResources(io, inputFile);
72
+ if (options.logger) {
73
+ document.setLogger(options.logger);
74
+ }
75
+ else {
76
+ /** @ts-ignore */
77
+ document.getLogger().verbosity = options.verbose ? Verbosity.DEBUG : options.debug ? Verbosity.INFO : Verbosity.WARN;
78
+ }
69
79
  let totalGPUTextureMemory = 0;
70
80
  /** @type {Array<import('@gltf-transform/core').Transform>} */
71
81
  const transforms = [
@@ -128,8 +138,10 @@ export async function packGLTF(inputFile, outputFile, options) {
128
138
  return true;
129
139
  }
130
140
  catch (err) {
141
+ if (err instanceof TestAssertionError)
142
+ throw err;
131
143
  const shortedStackTrace = err instanceof Error && err.stack ? err.stack.split('\n').slice(0, 2).join('\n') : '';
132
- logger.error(`Packing of "${path.basename(inputFile)}" using version ${getVersion()} failed with error:\n${err.message}\n${shortedStackTrace}`);
144
+ logger.error(`Packing of "${path.basename(inputFile)}" using version ${getVersion()} failed with error:\n${err instanceof Error ? err.message : String(err)}\n${shortedStackTrace}`);
133
145
  if (debug)
134
146
  throw err;
135
147
  return false;
@@ -0,0 +1,16 @@
1
+ import { Document, Texture } from '@gltf-transform/core';
2
+ import { NEEDLE_compression_texture_schema } from '../extensions/index.js';
3
+ import { INeedleTextureTransformContext, NeedleTransformContext } from './needle_common.js';
4
+ export declare class NeedlePmremContext implements INeedleTextureTransformContext {
5
+ private wasmModule;
6
+ private pmremDir;
7
+ private basisuExe;
8
+ private tmpDir;
9
+ private processed;
10
+ private available;
11
+ private logger;
12
+ private readonly convertedTextures;
13
+ prepare(document: Document): Promise<boolean>;
14
+ process(index: number, texture: Texture, _settings: NEEDLE_compression_texture_schema | null, context: NeedleTransformContext): Promise<boolean>;
15
+ finalize(document: Document): Promise<void>;
16
+ }