streaming-gltf 1.0.2 → 1.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "streaming-gltf",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Streaming progressive glTF LOD renderer (BatchedMesh/InstancedMesh tiers, network-lazy GPU-eager LOD streaming, on-GPU position lerping) plus the local bake/convert + streaming-download pipeline (tools/bake-*.mjs).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -204,6 +204,54 @@ async function rewriteGlbJson(filePath, mutator) {
204
204
  await writeFile(filePath, out);
205
205
  }
206
206
 
207
+ // Normalize a GLB so @gltf-transform's reader can ingest it. The reader resolves
208
+ // a texture's image via the top-level `texture.source`, but GLBs that use
209
+ // EXT_texture_webp / EXT_texture_avif carry the image index ONLY under
210
+ // `texture.extensions.<ext>.source` and may omit the top-level `source`. The
211
+ // reader then maps a material's textureInfo to a null texture and throws
212
+ // (`setTextureInfo` -> `setMagFilter` on null). We copy the extension's source
213
+ // up to the top level (a valid glTF fallback) so the read succeeds; the webp
214
+ // extension still carries its own source for extension-aware consumers. Returns
215
+ // the path to a normalized temp GLB, or the original path if no change was
216
+ // needed. Caller is responsible for cleaning up the temp file.
217
+ async function normalizeForRead(filePath) {
218
+ const { json } = await readGlbParts(filePath);
219
+ const textures = json.textures || [];
220
+ const haveImage = (json.images || []).length > 0;
221
+ let changed = false;
222
+ for (const tex of textures) {
223
+ if (tex.source !== undefined) continue;
224
+ const extSource = tex.extensions?.EXT_texture_webp?.source
225
+ ?? tex.extensions?.EXT_texture_avif?.source
226
+ ?? tex.extensions?.KHR_texture_basisu?.source;
227
+ if (extSource !== undefined) {
228
+ tex.source = extSource;
229
+ changed = true;
230
+ } else if (haveImage) {
231
+ // A texture with NO image source anywhere (top-level or extension) makes
232
+ // the reader map a material's textureInfo to a null texture and throw.
233
+ // Point it at image 0 so the read succeeds; such a texture is already
234
+ // broken in the source asset, so the visual impact is nil.
235
+ tex.source = 0;
236
+ changed = true;
237
+ }
238
+ }
239
+ if (!changed) return filePath;
240
+ const tmpPath = filePath + '.normalized.glb';
241
+ // Copy the original then rewrite its JSON chunk in place with the patched defs.
242
+ await writeFile(tmpPath, await readFile(filePath));
243
+ await rewriteGlbJson(tmpPath, (j) => {
244
+ const t = j.textures || [];
245
+ for (let i = 0; i < t.length; i++) {
246
+ if (t[i].source === undefined && textures[i] && textures[i].source !== undefined) {
247
+ t[i].source = textures[i].source;
248
+ }
249
+ }
250
+ });
251
+ console.log(`[bake] normalized ${path.basename(filePath)}: populated top-level texture.source from texture extensions`);
252
+ return tmpPath;
253
+ }
254
+
207
255
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
208
256
  const repoRoot = path.resolve(__dirname, '..');
209
257
 
@@ -263,7 +311,13 @@ export async function bakeProgressive(INPUT, OUT_DIR) {
263
311
  'draco3d.decoder': await draco3dgltf.createDecoderModule(),
264
312
  'draco3d.encoder': await draco3dgltf.createEncoderModule(),
265
313
  });
266
- const doc = await io.read(INPUT);
314
+ const readPath = await normalizeForRead(INPUT);
315
+ let doc;
316
+ try {
317
+ doc = await io.read(readPath);
318
+ } finally {
319
+ if (readPath !== INPUT) { try { await rm(readPath, { force: true }); } catch (_) {} }
320
+ }
267
321
  const root = doc.getRoot();
268
322
 
269
323
  console.log(`[bake] meshes=${root.listMeshes().length} textures=${root.listTextures().length}`);
@@ -423,9 +477,20 @@ export async function bakeProgressive(INPUT, OUT_DIR) {
423
477
  let target = Math.min(FAR_TRI_CAP * 3, u32.length);
424
478
  target -= target % 3;
425
479
  target = Math.max(96, target); // >=32 tris
426
- const res = MeshoptSimplifier.simplifySloppy(u32, f32, 3, null, target, 1e9);
427
- const out = Array.isArray(res) ? res[0] : res;
428
- idxAcc.setArray(out instanceof Uint32Array ? out : new Uint32Array(out));
480
+ // simplifySloppy asserts on some inputs (degenerate / tiny / non-
481
+ // manifold index buffers). The unskinned LOD is an optimization,
482
+ // not a correctness requirement: on failure keep the meshopt-
483
+ // decoded indices as-is (a heavier-but-correct far dot) rather
484
+ // than aborting the whole bake.
485
+ try {
486
+ const res = MeshoptSimplifier.simplifySloppy(u32, f32, 3, null, target, 1e9);
487
+ const out = Array.isArray(res) ? res[0] : res;
488
+ if (out && out.length >= 3) {
489
+ idxAcc.setArray(out instanceof Uint32Array ? out : new Uint32Array(out));
490
+ }
491
+ } catch (e) {
492
+ console.warn(`[bake] simplifySloppy skipped for unskinned LOD (${u32.length / 3} tris): ${e.message}`);
493
+ }
429
494
  }
430
495
  } else {
431
496
  await cloneDoc.transform(
@@ -443,6 +508,10 @@ export async function bakeProgressive(INPUT, OUT_DIR) {
443
508
  }
444
509
 
445
510
  const simp = cloneMesh.listPrimitives()[0];
511
+ // A primitive can be emptied/removed by aggressive simplification (or a
512
+ // degenerate source mesh). Skip the vertex-color bake for this LOD
513
+ // rather than crash the whole asset bake.
514
+ if (!simp) { console.warn(`[bake] mesh ${mi} prim ${pi}: no primitive after simplify, skipping LOD stage`); continue; }
446
515
  const posAcc = simp.getAttribute('POSITION');
447
516
  const uvAcc = simp.getAttribute('TEXCOORD_0');
448
517
  const vertCount = posAcc?.getCount() ?? 0;