@plasius/gpu-shared 0.1.16 → 0.1.17

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createProductStudioMeshes,
3
3
  mountGpuProductStudio
4
- } from "./chunk-6SOHFUOE.js";
4
+ } from "./chunk-BXTHIQOO.js";
5
5
  import {
6
6
  GPU_SHOWCASE_PRODUCT_STUDIO_FEATURE,
7
7
  GPU_SHOWCASE_REALISTIC_MODELS_FEATURE,
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-CH3ZS5TQ.js";
14
14
  import {
15
15
  resolveShowcaseAssetUrl
16
- } from "./chunk-QVNRTWHB.js";
16
+ } from "./chunk-UKCJ2AWJ.js";
17
17
  import "./chunk-2GM64LB6.js";
18
18
  import "./chunk-DGUM43GV.js";
19
19
 
@@ -29,7 +29,7 @@ var showcaseFocusModes = Object.freeze([
29
29
  ]);
30
30
  var showcaseDemoModes = Object.freeze(["harbor", "product-studio"]);
31
31
  async function loadGltfModel(url) {
32
- const module = await import("./gltf-loader-B6VOWGBV.js");
32
+ const module = await import("./gltf-loader-FMRC3OEV.js");
33
33
  return module.loadGltfModel(url);
34
34
  }
35
35
  function isProductStudioFeatureEnabled(featureFlags) {
@@ -58,7 +58,7 @@ async function mountGpuShowcase(options = {}) {
58
58
  `${GPU_SHOWCASE_PRODUCT_STUDIO_FEATURE} must be enabled before Product Studio can mount.`
59
59
  );
60
60
  }
61
- const productRuntimeLoader = typeof options.__productRuntimeLoader === "function" ? options.__productRuntimeLoader : () => import("./product-studio-runtime-BYVBUWIN.js");
61
+ const productRuntimeLoader = typeof options.__productRuntimeLoader === "function" ? options.__productRuntimeLoader : () => import("./product-studio-runtime-CTXA45RJ.js");
62
62
  const productModule = await productRuntimeLoader();
63
63
  if (typeof productModule.mountGpuProductStudio !== "function") {
64
64
  throw new Error("product runtime loader must provide mountGpuProductStudio.");
@@ -69,7 +69,7 @@ async function mountGpuShowcase(options = {}) {
69
69
  delete productOptions.__featureFlags;
70
70
  return productModule.mountGpuProductStudio(productOptions, options.__featureFlags);
71
71
  }
72
- const runtimeLoader = typeof options.__runtimeLoader === "function" ? options.__runtimeLoader : () => import("./showcase-runtime-M6TEUYOG.js");
72
+ const runtimeLoader = typeof options.__runtimeLoader === "function" ? options.__runtimeLoader : () => import("./showcase-runtime-OH3H6ZW2.js");
73
73
  const module = await runtimeLoader();
74
74
  if (typeof module.mountGpuShowcase !== "function") {
75
75
  throw new Error("showcase runtime loader must provide mountGpuShowcase.");
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  createProductStudioMeshes,
3
3
  mountGpuProductStudio
4
- } from "./chunk-6SOHFUOE.js";
5
- import "./chunk-QVNRTWHB.js";
4
+ } from "./chunk-BXTHIQOO.js";
5
+ import "./chunk-UKCJ2AWJ.js";
6
6
  import "./chunk-2GM64LB6.js";
7
7
  import "./chunk-DGUM43GV.js";
8
8
  export {
9
9
  createProductStudioMeshes,
10
10
  mountGpuProductStudio
11
11
  };
12
- //# sourceMappingURL=product-studio-runtime-BYVBUWIN.js.map
12
+ //# sourceMappingURL=product-studio-runtime-CTXA45RJ.js.map
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  loadGltfModel,
8
8
  resolveShowcaseAssetUrl
9
- } from "./chunk-QVNRTWHB.js";
9
+ } from "./chunk-UKCJ2AWJ.js";
10
10
  import "./chunk-2GM64LB6.js";
11
11
  import "./chunk-DGUM43GV.js";
12
12
 
@@ -3783,4 +3783,4 @@ export {
3783
3783
  mountGpuShowcase,
3784
3784
  showcaseFocusModes
3785
3785
  };
3786
- //# sourceMappingURL=showcase-runtime-M6TEUYOG.js.map
3786
+ //# sourceMappingURL=showcase-runtime-OH3H6ZW2.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-shared",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Shared browser-safe demo runtime and asset helpers for the Plasius gpu-* package family.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -55,6 +55,35 @@ function getTypeSize(type) {
55
55
  }
56
56
  }
57
57
 
58
+ function getComponentByteSize(componentType) {
59
+ switch (componentType) {
60
+ case 5121:
61
+ return 1;
62
+ case 5123:
63
+ return 2;
64
+ case 5125:
65
+ case 5126:
66
+ return 4;
67
+ default:
68
+ throw new Error(`Unsupported glTF componentType: ${componentType}`);
69
+ }
70
+ }
71
+
72
+ function readComponentValue(view, componentType, byteOffset) {
73
+ switch (componentType) {
74
+ case 5121:
75
+ return view.getUint8(byteOffset);
76
+ case 5123:
77
+ return view.getUint16(byteOffset, true);
78
+ case 5125:
79
+ return view.getUint32(byteOffset, true);
80
+ case 5126:
81
+ return view.getFloat32(byteOffset, true);
82
+ default:
83
+ throw new Error(`Unsupported glTF componentType: ${componentType}`);
84
+ }
85
+ }
86
+
58
87
  function readAccessor(document, accessorIndex, buffers) {
59
88
  const accessor = document.accessors?.[accessorIndex];
60
89
  if (!accessor) {
@@ -69,10 +98,30 @@ function readAccessor(document, accessorIndex, buffers) {
69
98
  const buffer = buffers[bufferView.buffer];
70
99
  const componentCount = getTypeSize(accessor.type);
71
100
  const byteOffset = (bufferView.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
72
- const valueCount = accessor.count * componentCount;
73
- const values = Array.from(
74
- getComponentArray(accessor.componentType, buffer, byteOffset, valueCount)
75
- );
101
+ const componentByteSize = getComponentByteSize(accessor.componentType);
102
+ const packedElementByteLength = componentCount * componentByteSize;
103
+ const byteStride = Math.max(bufferView.byteStride ?? packedElementByteLength, packedElementByteLength);
104
+ let values;
105
+
106
+ if (byteStride === packedElementByteLength) {
107
+ const valueCount = accessor.count * componentCount;
108
+ values = Array.from(
109
+ getComponentArray(accessor.componentType, buffer, byteOffset, valueCount)
110
+ );
111
+ } else {
112
+ const view = new DataView(buffer, byteOffset);
113
+ values = new Array(accessor.count * componentCount);
114
+ for (let index = 0; index < accessor.count; index += 1) {
115
+ const elementOffset = index * byteStride;
116
+ for (let componentIndex = 0; componentIndex < componentCount; componentIndex += 1) {
117
+ values[index * componentCount + componentIndex] = readComponentValue(
118
+ view,
119
+ accessor.componentType,
120
+ elementOffset + componentIndex * componentByteSize
121
+ );
122
+ }
123
+ }
124
+ }
76
125
 
77
126
  if (accessor.normalized) {
78
127
  const scale = getNormalizationScale(accessor.componentType);
@@ -82,11 +131,258 @@ function readAccessor(document, accessorIndex, buffers) {
82
131
  return values;
83
132
  }
84
133
 
85
- function getMaterialInfo(document, primitive) {
134
+ async function decodeImagePixels(blob, urlLabel = "glTF texture") {
135
+ if (typeof createImageBitmap === "function") {
136
+ const bitmap = await createImageBitmap(blob);
137
+ try {
138
+ const canvas =
139
+ typeof OffscreenCanvas === "function"
140
+ ? new OffscreenCanvas(bitmap.width, bitmap.height)
141
+ : typeof document !== "undefined"
142
+ ? Object.assign(document.createElement("canvas"), {
143
+ width: bitmap.width,
144
+ height: bitmap.height,
145
+ })
146
+ : null;
147
+ const context = canvas?.getContext?.("2d", { willReadFrequently: true });
148
+ if (!context) {
149
+ throw new Error("Unable to create 2D context for glTF texture decode.");
150
+ }
151
+ context.drawImage(bitmap, 0, 0);
152
+ const imageData = context.getImageData(0, 0, bitmap.width, bitmap.height);
153
+ return Object.freeze({
154
+ width: bitmap.width,
155
+ height: bitmap.height,
156
+ data: imageData.data,
157
+ });
158
+ } finally {
159
+ bitmap.close?.();
160
+ }
161
+ }
162
+
163
+ if (typeof document === "undefined") {
164
+ throw new Error(`Unable to decode ${urlLabel}: browser image decode APIs are unavailable.`);
165
+ }
166
+
167
+ const objectUrl = URL.createObjectURL(blob);
168
+ try {
169
+ const image = await new Promise((resolve, reject) => {
170
+ const element = new Image();
171
+ element.onload = () => resolve(element);
172
+ element.onerror = () => reject(new Error(`Failed to decode ${urlLabel}.`));
173
+ element.src = objectUrl;
174
+ });
175
+ const canvas = Object.assign(document.createElement("canvas"), {
176
+ width: image.naturalWidth || image.width,
177
+ height: image.naturalHeight || image.height,
178
+ });
179
+ const context = canvas.getContext("2d", { willReadFrequently: true });
180
+ if (!context) {
181
+ throw new Error("Unable to create 2D context for glTF texture decode.");
182
+ }
183
+ context.drawImage(image, 0, 0);
184
+ const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
185
+ return Object.freeze({
186
+ width: canvas.width,
187
+ height: canvas.height,
188
+ data: imageData.data,
189
+ });
190
+ } finally {
191
+ URL.revokeObjectURL(objectUrl);
192
+ }
193
+ }
194
+
195
+ function sliceBufferView(document, bufferViewIndex, buffers) {
196
+ const bufferView = document.bufferViews?.[bufferViewIndex];
197
+ if (!bufferView) {
198
+ throw new Error(`glTF bufferView ${bufferViewIndex} is missing.`);
199
+ }
200
+ const buffer = buffers[bufferView.buffer];
201
+ if (!buffer) {
202
+ throw new Error(`glTF buffer ${bufferView.buffer} is missing.`);
203
+ }
204
+ const start = bufferView.byteOffset ?? 0;
205
+ const end = start + (bufferView.byteLength ?? 0);
206
+ return buffer.slice(start, end);
207
+ }
208
+
209
+ async function loadImageResource(document, image, index, buffers, baseUrl) {
210
+ if (typeof image?.uri === "string") {
211
+ const response = await fetch(new URL(image.uri, baseUrl));
212
+ if (!response.ok) {
213
+ throw new Error(`Failed to load glTF texture: ${response.status} ${response.statusText}`);
214
+ }
215
+ return decodeImagePixels(
216
+ await response.blob(),
217
+ `glTF texture ${index}${image.uri ? ` (${image.uri})` : ""}`
218
+ );
219
+ }
220
+
221
+ if (typeof image?.bufferView === "number") {
222
+ const bytes = sliceBufferView(document, image.bufferView, buffers);
223
+ return decodeImagePixels(
224
+ new Blob([bytes], { type: image.mimeType ?? "application/octet-stream" }),
225
+ `glTF texture ${index}`
226
+ );
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ function normalizeTextureTransformPair(value, fallback) {
233
+ if (!Array.isArray(value) || value.length < 2) {
234
+ return fallback;
235
+ }
236
+ return [
237
+ Number.isFinite(value[0]) ? Number(value[0]) : fallback[0],
238
+ Number.isFinite(value[1]) ? Number(value[1]) : fallback[1],
239
+ ];
240
+ }
241
+
242
+ function readTextureTransform(textureRef) {
243
+ const transformExtension = textureRef?.extensions?.KHR_texture_transform ?? null;
244
+ return {
245
+ texCoord:
246
+ typeof transformExtension?.texCoord === "number"
247
+ ? transformExtension.texCoord
248
+ : textureRef?.texCoord ?? 0,
249
+ offset: normalizeTextureTransformPair(transformExtension?.offset, [0, 0]),
250
+ scale: normalizeTextureTransformPair(transformExtension?.scale, [1, 1]),
251
+ rotation: Number.isFinite(transformExtension?.rotation) ? Number(transformExtension.rotation) : 0,
252
+ };
253
+ }
254
+
255
+ function wrapTextureCoordinate(value) {
256
+ return ((value % 1) + 1) % 1;
257
+ }
258
+
259
+ function transformTextureCoordinate(uv, transform) {
260
+ const scaledU = uv[0] * transform.scale[0];
261
+ const scaledV = uv[1] * transform.scale[1];
262
+ const cosine = Math.cos(transform.rotation);
263
+ const sine = Math.sin(transform.rotation);
264
+ return [
265
+ scaledU * cosine - scaledV * sine + transform.offset[0],
266
+ scaledU * sine + scaledV * cosine + transform.offset[1],
267
+ ];
268
+ }
269
+
270
+ function readTexturePixel(data, width, height, x, y) {
271
+ const clampedX = Math.min(width - 1, Math.max(0, x));
272
+ const clampedY = Math.min(height - 1, Math.max(0, y));
273
+ const offset = (clampedY * width + clampedX) * 4;
274
+ return [
275
+ data[offset] ?? 0,
276
+ data[offset + 1] ?? 0,
277
+ data[offset + 2] ?? 0,
278
+ data[offset + 3] ?? 255,
279
+ ];
280
+ }
281
+
282
+ function mixChannel(a, b, weight) {
283
+ return a + (b - a) * weight;
284
+ }
285
+
286
+ function sampleTexturePixel(data, width, height, uv) {
287
+ const u = wrapTextureCoordinate(uv[0]);
288
+ const v = wrapTextureCoordinate(uv[1]);
289
+ const sourceX = u * Math.max(width - 1, 0);
290
+ const sourceY = (1 - v) * Math.max(height - 1, 0);
291
+ const x0 = Math.floor(sourceX);
292
+ const y0 = Math.floor(sourceY);
293
+ const x1 = Math.min(width - 1, x0 + 1);
294
+ const y1 = Math.min(height - 1, y0 + 1);
295
+ const tx = sourceX - x0;
296
+ const ty = sourceY - y0;
297
+ const topLeft = readTexturePixel(data, width, height, x0, y0);
298
+ const topRight = readTexturePixel(data, width, height, x1, y0);
299
+ const bottomLeft = readTexturePixel(data, width, height, x0, y1);
300
+ const bottomRight = readTexturePixel(data, width, height, x1, y1);
301
+ return [0, 1, 2, 3].map((channelIndex) => {
302
+ const top = mixChannel(topLeft[channelIndex], topRight[channelIndex], tx);
303
+ const bottom = mixChannel(bottomLeft[channelIndex], bottomRight[channelIndex], tx);
304
+ return mixChannel(top, bottom, ty);
305
+ });
306
+ }
307
+
308
+ function applyTextureTransformToPixels(pixels, transform) {
309
+ const isIdentityTransform =
310
+ transform.offset[0] === 0 &&
311
+ transform.offset[1] === 0 &&
312
+ transform.scale[0] === 1 &&
313
+ transform.scale[1] === 1 &&
314
+ transform.rotation === 0;
315
+ if (isIdentityTransform) {
316
+ return pixels;
317
+ }
318
+
319
+ const transformedData = new Uint8ClampedArray(pixels.data.length);
320
+ for (let y = 0; y < pixels.height; y += 1) {
321
+ const outputV = pixels.height > 1 ? 1 - y / (pixels.height - 1) : 0;
322
+ for (let x = 0; x < pixels.width; x += 1) {
323
+ const outputU = pixels.width > 1 ? x / (pixels.width - 1) : 0;
324
+ const sourcePixel = sampleTexturePixel(
325
+ pixels.data,
326
+ pixels.width,
327
+ pixels.height,
328
+ transformTextureCoordinate([outputU, outputV], transform)
329
+ );
330
+ const offset = (y * pixels.width + x) * 4;
331
+ transformedData[offset] = sourcePixel[0];
332
+ transformedData[offset + 1] = sourcePixel[1];
333
+ transformedData[offset + 2] = sourcePixel[2];
334
+ transformedData[offset + 3] = sourcePixel[3];
335
+ }
336
+ }
337
+
338
+ return Object.freeze({
339
+ width: pixels.width,
340
+ height: pixels.height,
341
+ data: transformedData,
342
+ });
343
+ }
344
+
345
+ function getMaterialTexture(document, textureRef, imageResources) {
346
+ if (!textureRef || typeof textureRef.index !== "number") {
347
+ return null;
348
+ }
349
+ const texture = document.textures?.[textureRef.index] ?? null;
350
+ const sourceIndex = texture?.source;
351
+ if (typeof sourceIndex !== "number") {
352
+ return null;
353
+ }
354
+ const pixels = imageResources.get(sourceIndex) ?? null;
355
+ if (!pixels) {
356
+ return null;
357
+ }
358
+
359
+ const transform = readTextureTransform(textureRef);
360
+ const transformedPixels = applyTextureTransformToPixels(pixels, transform);
361
+ return Object.freeze({
362
+ texCoord: transform.texCoord,
363
+ scale: textureRef.scale,
364
+ strength: textureRef.strength,
365
+ width: transformedPixels.width,
366
+ height: transformedPixels.height,
367
+ data: transformedPixels.data,
368
+ });
369
+ }
370
+
371
+ function getMaterialInfo(document, primitive, imageResources) {
86
372
  const material = document.materials?.[primitive.material] ?? null;
87
- const factor =
88
- material?.pbrMetallicRoughness?.baseColorFactor ?? [0.56, 0.33, 0.22, 1];
89
- const emissive = material?.emissiveFactor ?? [0, 0, 0];
373
+ const pbr = material?.pbrMetallicRoughness ?? null;
374
+ const factor = pbr?.baseColorFactor ?? [0.56, 0.33, 0.22, 1];
375
+ const emissive = Array.isArray(material?.emissiveFactor) ? material.emissiveFactor : [0, 0, 0];
376
+ const extensions = material?.extensions ?? {};
377
+ const specular = extensions.KHR_materials_specular ?? null;
378
+ const transmission = extensions.KHR_materials_transmission ?? null;
379
+ const ior = extensions.KHR_materials_ior ?? null;
380
+ const clearcoat = extensions.KHR_materials_clearcoat ?? null;
381
+ const sheen = extensions.KHR_materials_sheen ?? null;
382
+ const volume = extensions.KHR_materials_volume ?? null;
383
+ const iridescence = extensions.KHR_materials_iridescence ?? null;
384
+ const anisotropy = extensions.KHR_materials_anisotropy ?? null;
385
+ const dispersion = extensions.KHR_materials_dispersion ?? null;
90
386
 
91
387
  return Object.freeze({
92
388
  name: material?.name ?? "default-material",
@@ -97,18 +393,139 @@ function getMaterialInfo(document, primitive) {
97
393
  a: factor[3] ?? 1,
98
394
  }),
99
395
  roughness:
100
- typeof material?.pbrMetallicRoughness?.roughnessFactor === "number"
101
- ? material.pbrMetallicRoughness.roughnessFactor
396
+ typeof pbr?.roughnessFactor === "number"
397
+ ? pbr.roughnessFactor
102
398
  : 0.92,
103
399
  metallic:
104
- typeof material?.pbrMetallicRoughness?.metallicFactor === "number"
105
- ? material.pbrMetallicRoughness.metallicFactor
400
+ typeof pbr?.metallicFactor === "number"
401
+ ? pbr.metallicFactor
106
402
  : 0.08,
403
+ opacity: factor[3] ?? 1,
107
404
  emissive: Object.freeze({
108
405
  r: emissive[0] ?? 0,
109
406
  g: emissive[1] ?? 0,
110
407
  b: emissive[2] ?? 0,
408
+ a: 1,
111
409
  }),
410
+ baseColorTexture: getMaterialTexture(document, pbr?.baseColorTexture, imageResources),
411
+ metallicRoughnessTexture: getMaterialTexture(
412
+ document,
413
+ pbr?.metallicRoughnessTexture,
414
+ imageResources
415
+ ),
416
+ normalTexture: getMaterialTexture(document, material?.normalTexture, imageResources),
417
+ occlusionTexture: getMaterialTexture(document, material?.occlusionTexture, imageResources),
418
+ emissiveTexture: getMaterialTexture(document, material?.emissiveTexture, imageResources),
419
+ specular:
420
+ typeof specular?.specularFactor === "number"
421
+ ? specular.specularFactor
422
+ : 1,
423
+ specularColor: Object.freeze(
424
+ Array.isArray(specular?.specularColorFactor)
425
+ ? [...specular.specularColorFactor]
426
+ : [1, 1, 1]
427
+ ),
428
+ specularTexture: getMaterialTexture(document, specular?.specularTexture, imageResources),
429
+ specularColorTexture: getMaterialTexture(
430
+ document,
431
+ specular?.specularColorTexture,
432
+ imageResources
433
+ ),
434
+ transmission:
435
+ typeof transmission?.transmissionFactor === "number"
436
+ ? transmission.transmissionFactor
437
+ : 0,
438
+ transmissionTexture: getMaterialTexture(
439
+ document,
440
+ transmission?.transmissionTexture,
441
+ imageResources
442
+ ),
443
+ ior: typeof ior?.ior === "number" ? ior.ior : 1.45,
444
+ attenuationDistance:
445
+ typeof volume?.attenuationDistance === "number"
446
+ ? volume.attenuationDistance
447
+ : null,
448
+ attenuationColor: Object.freeze(
449
+ Array.isArray(volume?.attenuationColor)
450
+ ? [...volume.attenuationColor]
451
+ : [1, 1, 1]
452
+ ),
453
+ thickness:
454
+ typeof volume?.thicknessFactor === "number"
455
+ ? volume.thicknessFactor
456
+ : 0,
457
+ thicknessTexture: getMaterialTexture(document, volume?.thicknessTexture, imageResources),
458
+ clearcoat:
459
+ typeof clearcoat?.clearcoatFactor === "number"
460
+ ? clearcoat.clearcoatFactor
461
+ : 0,
462
+ clearcoatTexture: getMaterialTexture(document, clearcoat?.clearcoatTexture, imageResources),
463
+ clearcoatRoughness:
464
+ typeof clearcoat?.clearcoatRoughnessFactor === "number"
465
+ ? clearcoat.clearcoatRoughnessFactor
466
+ : 0.08,
467
+ clearcoatRoughnessTexture: getMaterialTexture(
468
+ document,
469
+ clearcoat?.clearcoatRoughnessTexture,
470
+ imageResources
471
+ ),
472
+ clearcoatNormalTexture: getMaterialTexture(
473
+ document,
474
+ clearcoat?.clearcoatNormalTexture,
475
+ imageResources
476
+ ),
477
+ sheenColor: Object.freeze(
478
+ Array.isArray(sheen?.sheenColorFactor) ? [...sheen.sheenColorFactor] : [0, 0, 0]
479
+ ),
480
+ sheenColorTexture: getMaterialTexture(document, sheen?.sheenColorTexture, imageResources),
481
+ sheenRoughness:
482
+ typeof sheen?.sheenRoughnessFactor === "number"
483
+ ? sheen.sheenRoughnessFactor
484
+ : 0,
485
+ sheenRoughnessTexture: getMaterialTexture(
486
+ document,
487
+ sheen?.sheenRoughnessTexture,
488
+ imageResources
489
+ ),
490
+ iridescence:
491
+ typeof iridescence?.iridescenceFactor === "number"
492
+ ? iridescence.iridescenceFactor
493
+ : 0,
494
+ iridescenceTexture: getMaterialTexture(
495
+ document,
496
+ iridescence?.iridescenceTexture,
497
+ imageResources
498
+ ),
499
+ iridescenceIor:
500
+ typeof iridescence?.iridescenceIor === "number"
501
+ ? iridescence.iridescenceIor
502
+ : 1.3,
503
+ iridescenceThicknessMinimum:
504
+ typeof iridescence?.iridescenceThicknessMinimum === "number"
505
+ ? iridescence.iridescenceThicknessMinimum
506
+ : 100,
507
+ iridescenceThicknessMaximum:
508
+ typeof iridescence?.iridescenceThicknessMaximum === "number"
509
+ ? iridescence.iridescenceThicknessMaximum
510
+ : 400,
511
+ iridescenceThicknessTexture: getMaterialTexture(
512
+ document,
513
+ iridescence?.iridescenceThicknessTexture,
514
+ imageResources
515
+ ),
516
+ anisotropy:
517
+ typeof anisotropy?.anisotropyStrength === "number"
518
+ ? anisotropy.anisotropyStrength
519
+ : 0,
520
+ anisotropyRotation:
521
+ typeof anisotropy?.anisotropyRotation === "number"
522
+ ? anisotropy.anisotropyRotation
523
+ : 0,
524
+ anisotropyTexture: getMaterialTexture(document, anisotropy?.anisotropyTexture, imageResources),
525
+ dispersion:
526
+ typeof dispersion?.dispersion === "number"
527
+ ? dispersion.dispersion
528
+ : 0,
112
529
  });
113
530
  }
114
531
 
@@ -269,7 +686,7 @@ function transformNormal(normal, matrix) {
269
686
  return [transformed[0] / length, transformed[1] / length, transformed[2] / length];
270
687
  }
271
688
 
272
- function collectScenePrimitives(document, buffers) {
689
+ function collectScenePrimitives(document, buffers, imageResources) {
273
690
  const scene = document.scenes?.[document.scene ?? 0];
274
691
  if (!scene || !Array.isArray(scene.nodes) || scene.nodes.length === 0) {
275
692
  throw new Error("glTF demo asset must expose a default scene with at least one node.");
@@ -312,6 +729,10 @@ function collectScenePrimitives(document, buffers) {
312
729
  typeof primitive.attributes.COLOR_0 === "number"
313
730
  ? readAccessor(document, primitive.attributes.COLOR_0, buffers)
314
731
  : null;
732
+ const uvs =
733
+ typeof primitive.attributes.TEXCOORD_0 === "number"
734
+ ? readAccessor(document, primitive.attributes.TEXCOORD_0, buffers)
735
+ : null;
315
736
  const transformedPositions = [];
316
737
  const transformedNormals = [];
317
738
 
@@ -335,7 +756,7 @@ function collectScenePrimitives(document, buffers) {
335
756
  typeof primitive.indices === "number"
336
757
  ? readAccessor(document, primitive.indices, buffers).map((value) => Number(value))
337
758
  : Array.from({ length: transformedPositions.length / 3 }, (_, index) => index);
338
- const material = getMaterialInfo(document, primitive);
759
+ const material = getMaterialInfo(document, primitive, imageResources);
339
760
  const primitiveName =
340
761
  `${node.name ?? mesh.name ?? "mesh"}-${primitiveIndex}`;
341
762
 
@@ -348,6 +769,7 @@ function collectScenePrimitives(document, buffers) {
348
769
  transformedNormals.length > 0
349
770
  ? Object.freeze(transformedNormals)
350
771
  : null,
772
+ uvs: uvs ? Object.freeze(uvs) : null,
351
773
  colors: colors ? Object.freeze(colors) : null,
352
774
  material,
353
775
  bounds: computeBounds(transformedPositions),
@@ -412,7 +834,17 @@ async function buildGltfModel(document, baseUrl) {
412
834
  })
413
835
  );
414
836
 
415
- const scene = collectScenePrimitives(document, buffers);
837
+ const imageResources = new Map();
838
+ await Promise.all(
839
+ (document.images ?? []).map(async (image, index) => {
840
+ const pixels = await loadImageResource(document, image, index, buffers, baseUrl);
841
+ if (pixels) {
842
+ imageResources.set(index, pixels);
843
+ }
844
+ })
845
+ );
846
+
847
+ const scene = collectScenePrimitives(document, buffers, imageResources);
416
848
  const aggregatePositions = [];
417
849
  const aggregateIndices = [];
418
850