@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.
- package/dist/cache/cache.d.ts +1 -0
- package/dist/extensions/NEEDLE_lightmaps/index.d.ts +41 -0
- package/dist/extensions/NEEDLE_lightmaps/index.js +249 -0
- package/dist/extensions/NEEDLE_opaque/NEEDLE_opaque.js +21 -7
- package/dist/extensions/NEEDLE_opaque/index.js +1 -1
- package/dist/extensions/NEEDLE_pmrem/index.d.ts +21 -0
- package/dist/extensions/NEEDLE_pmrem/index.js +95 -0
- package/dist/extensions/index.d.ts +2 -0
- package/dist/extensions/index.js +7 -1
- package/dist/scripts/pack-gltf.js +17 -5
- package/dist/transforms/needle_pmrem.d.ts +16 -0
- package/dist/transforms/needle_pmrem.js +185 -0
- package/dist/transforms/needle_progressive.js +14 -6
- package/dist/transforms/needle_texture_transform.d.ts +2 -0
- package/dist/transforms/needle_texture_transform.js +12 -2
- package/dist/utils/test-assert.d.ts +8 -0
- package/dist/utils/test-assert.js +18 -0
- package/dist/utils/validate.js +1 -1
- package/dist/utils/version.gen.d.ts +1 -1
- package/dist/utils/version.gen.js +1 -1
- package/package.json +18 -4
- package/tools/pmrem/basis/linux/basisu +0 -0
- package/tools/pmrem/basis/osx/basisu +0 -0
- package/tools/pmrem/basis/package.json +3 -0
- package/tools/pmrem/basis/wasm/basis_encoder_threads.js +6693 -0
- package/tools/pmrem/basis/wasm/basis_encoder_threads.wasm +0 -0
- package/tools/pmrem/basis/win/basisu.exe +0 -0
- package/tools/pmrem/basis/win/basisu_help.txt +0 -0
- package/tools/pmrem/basis/win/ocl_kernels.cl +1290 -0
- package/tools/pmrem/pkg/pmrem_wasm.d.ts +68 -0
- package/tools/pmrem/pkg/pmrem_wasm.js +401 -0
- package/tools/pmrem/pkg/pmrem_wasm_bg.wasm +0 -0
- package/tsconfig.json +0 -20
package/dist/cache/cache.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
434
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
package/dist/extensions/index.js
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
document
|
|
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
|
+
}
|