@inweb/viewer-three 27.4.1 → 27.4.3

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": "@inweb/viewer-three",
3
- "version": "27.4.1",
3
+ "version": "27.4.3",
4
4
  "description": "JavaScript library for rendering CAD and BIM files in a browser using Three.js",
5
5
  "homepage": "https://cloud.opendesign.com/docs/index.html",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -35,10 +35,10 @@
35
35
  "docs": "typedoc"
36
36
  },
37
37
  "dependencies": {
38
- "@inweb/client": "~27.4.1",
39
- "@inweb/eventemitter2": "~27.4.1",
40
- "@inweb/markup": "~27.4.1",
41
- "@inweb/viewer-core": "~27.4.1"
38
+ "@inweb/client": "~27.4.3",
39
+ "@inweb/eventemitter2": "~27.4.3",
40
+ "@inweb/markup": "~27.4.3",
41
+ "@inweb/viewer-core": "~27.4.3"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@streamparser/json": "^0.0.22",
@@ -141,13 +141,6 @@ export class DynamicGltfLoader {
141
141
  this.visibilityMaterials = new Set(); // Keep track of materials to update uniforms
142
142
  }
143
143
 
144
- // layout (1 matrix = 4 pixels)
145
- // RGBA RGBA RGBA RGBA (=> column1, column2, column3, column4)
146
- // with 8x8 pixel texture max 16 matrices * 4 pixels = (8 * 8)
147
- // 16x16 pixel texture max 64 matrices * 4 pixels = (16 * 16)
148
- // 32x32 pixel texture max 256 matrices * 4 pixels = (32 * 32)
149
- // 64x64 pixel texture max 1024 matrices * 4 pixels = (64 * 64)
150
-
151
144
  createDummyTexture() {
152
145
  // Create 1x1 dummy texture with identity matrix to prevent shader crash/black screen
153
146
  const data = new Float32Array(16); // 4x4 matrix
@@ -226,8 +219,6 @@ export class DynamicGltfLoader {
226
219
 
227
220
  updateMaterialUniforms() {
228
221
  // Only update if the texture actually changed
229
- // In three.js, setting `.value = this.transformTexture` triggers uniformity checks
230
- // We can avoid looping over all materials if the texture reference hasn't changed
231
222
  if (
232
223
  this._lastTransformTexture === this.transformTexture &&
233
224
  this._lastTransformTextureSize === this.transformTextureSize
@@ -272,7 +263,7 @@ export class DynamicGltfLoader {
272
263
 
273
264
  console.log(`Available memory set to ${Math.round(memoryLimit / (1024 * 1024 * 1024))}GB`);
274
265
  } catch (error) {
275
- console.warn("Error detecting available memory:", error);
266
+ console.warn("DynamicLoader: Error detecting available memory:", error);
276
267
  }
277
268
 
278
269
  return memoryLimit;
@@ -470,7 +461,7 @@ export class DynamicGltfLoader {
470
461
  if (entry?.lines?.uuid) uniqueMaterialIds.add(entry.lines.uuid);
471
462
  }
472
463
  } catch (exp) {
473
- console.error("Error adding material to uniqueMaterialIds", exp);
464
+ console.warn("DynamicLoader: Error collecting material stats", exp);
474
465
  }
475
466
  }
476
467
  }
@@ -492,7 +483,7 @@ export class DynamicGltfLoader {
492
483
  this._webglInfoCache = { renderer: null, vendor: null };
493
484
  }
494
485
  } catch (e) {
495
- console.error("Error getting webgl info", e);
486
+ console.warn("DynamicLoader: Error getting webgl info", e);
496
487
  this._webglInfoCache = { renderer: null, vendor: null };
497
488
  }
498
489
  }
@@ -778,7 +769,7 @@ export class DynamicGltfLoader {
778
769
  return;
779
770
  }
780
771
 
781
- console.error(`Error loading node ${nodeId}:`, error);
772
+ console.warn(`DynamicLoader: Error loading node ${nodeId}:`, error);
782
773
  }
783
774
  }
784
775
 
@@ -842,7 +833,7 @@ export class DynamicGltfLoader {
842
833
  await structure.loadTextures();
843
834
  await structure.loadMaterials();
844
835
  } catch (error) {
845
- console.error("Error loading materials:", error);
836
+ console.error("DynamicLoader: Error loading materials:", error);
846
837
  throw error;
847
838
  }
848
839
  }
@@ -852,7 +843,7 @@ export class DynamicGltfLoader {
852
843
 
853
844
  async processSceneHierarchy() {
854
845
  if (this.structures.length === 0) {
855
- throw new Error("No GLTF structures loaded");
846
+ throw new Error("DynamicLoader: No GLTF structures loaded");
856
847
  }
857
848
 
858
849
  this.nodesToLoad = [];
@@ -863,7 +854,7 @@ export class DynamicGltfLoader {
863
854
  const gltf = structure.getJson();
864
855
 
865
856
  if (!gltf.scenes || !gltf.scenes.length) {
866
- console.warn("No scenes found in GLTF structure");
857
+ console.warn("DynamicLoader: No scenes found in GLTF structure");
867
858
  continue;
868
859
  }
869
860
 
@@ -1383,7 +1374,6 @@ export class DynamicGltfLoader {
1383
1374
 
1384
1375
  //console.log("!vertex", shader.vertexShader);
1385
1376
  };
1386
-
1387
1377
  // Ensure the material recompiles to pick up changes
1388
1378
  material.needsUpdate = true;
1389
1379
  return material;
@@ -1653,7 +1643,6 @@ export class DynamicGltfLoader {
1653
1643
  // Clear previous optimization data
1654
1644
  this.objectIdToIndex.clear();
1655
1645
  this.maxObjectId = 0;
1656
-
1657
1646
  const structureGroups = new Map();
1658
1647
 
1659
1648
  this.dispatchEvent("optimizationprogress", {
@@ -1709,7 +1698,6 @@ export class DynamicGltfLoader {
1709
1698
  // This ensures that as we create merged objects, the texture is large enough
1710
1699
  // and populated with identity matrices, so objects don't disappear (scale 0).
1711
1700
  if (totalObjectsToMerge > 0) {
1712
- console.log(`Pre-allocating transform texture for ${totalObjectsToMerge} objects`);
1713
1701
  this.maxObjectId = totalObjectsToMerge;
1714
1702
  this.initTransformTexture();
1715
1703
  this.initializeObjectVisibility();
@@ -1776,7 +1764,6 @@ export class DynamicGltfLoader {
1776
1764
  });
1777
1765
 
1778
1766
  // Texture and visibility initialized at start of optimization
1779
-
1780
1767
  console.log(`Optimization complete. Total objects: ${this.maxObjectId}`);
1781
1768
 
1782
1769
  this.dispatchEvent("optimizationprogress", {
@@ -1793,7 +1780,7 @@ export class DynamicGltfLoader {
1793
1780
  let processedGroups = 0;
1794
1781
  for (const group of materialGroups) {
1795
1782
  if (!group.material) {
1796
- console.warn("Skipping mesh group with null material");
1783
+ console.warn("DynamicLoader: Skipping mesh group with null material");
1797
1784
  continue;
1798
1785
  }
1799
1786
 
@@ -1908,7 +1895,7 @@ export class DynamicGltfLoader {
1908
1895
  await this.yieldToUI();
1909
1896
  }
1910
1897
  } catch (error) {
1911
- console.error("Failed to merge meshes for material:", error);
1898
+ console.warn("DynamicLoader: Failed to merge meshes for material:", error);
1912
1899
  group.objects.forEach((mesh) => {
1913
1900
  mesh.visible = true;
1914
1901
  });
@@ -1922,7 +1909,7 @@ export class DynamicGltfLoader {
1922
1909
  if (group.objects.length === 0) continue;
1923
1910
 
1924
1911
  if (!group.material) {
1925
- console.warn("Skipping line group with null material");
1912
+ console.warn("DynamicLoader: Skipping line group with null material");
1926
1913
  continue;
1927
1914
  }
1928
1915
 
@@ -2058,7 +2045,7 @@ export class DynamicGltfLoader {
2058
2045
  let processedGroups = 0;
2059
2046
  for (const group of materialGroups) {
2060
2047
  if (!group.material) {
2061
- console.warn("Skipping line segment group with null material");
2048
+ console.warn("DynamicLoader: Skipping line segment group with null material");
2062
2049
  continue;
2063
2050
  }
2064
2051
 
@@ -2171,7 +2158,7 @@ export class DynamicGltfLoader {
2171
2158
  await this.yieldToUI();
2172
2159
  }
2173
2160
  } catch (error) {
2174
- console.warn("Failed to merge line segments for material:", error);
2161
+ console.warn("DynamicLoader: Failed to merge line segments for material:", error);
2175
2162
  group.objects.forEach((line) => {
2176
2163
  line.visible = true;
2177
2164
  });
@@ -2183,7 +2170,7 @@ export class DynamicGltfLoader {
2183
2170
  let processedGroups = 0;
2184
2171
  for (const group of materialGroups) {
2185
2172
  if (!group.material) {
2186
- console.warn("Skipping points group with null material");
2173
+ console.warn("DynamicLoader: Skipping points group with null material");
2187
2174
  continue;
2188
2175
  }
2189
2176
 
@@ -2221,7 +2208,6 @@ export class DynamicGltfLoader {
2221
2208
  const totalVertices = mergedGeometry.attributes.position.count;
2222
2209
  const objectIds = new Float32Array(totalVertices);
2223
2210
  let vertexOffset = 0;
2224
-
2225
2211
  group.objects.forEach((points) => {
2226
2212
  const handle = points.userData.handle;
2227
2213
  if (!this.objectIdToIndex.has(handle)) {
@@ -2275,7 +2261,7 @@ export class DynamicGltfLoader {
2275
2261
  await this.yieldToUI();
2276
2262
  }
2277
2263
  } catch (error) {
2278
- console.warn("Failed to merge points for material:", error);
2264
+ console.warn("DynamicLoader: Failed to merge points for material:", error);
2279
2265
  group.objects.forEach((points) => {
2280
2266
  points.visible = true;
2281
2267
  });
@@ -2326,7 +2312,6 @@ export class DynamicGltfLoader {
2326
2312
  });
2327
2313
 
2328
2314
  const finalGeometry = mergeGeometries(geometriesWithIndex, false);
2329
-
2330
2315
  // Add objectId attribute
2331
2316
  const totalVertices = finalGeometry.attributes.position.count;
2332
2317
  const objectIds = new Float32Array(totalVertices);
@@ -2373,7 +2358,7 @@ export class DynamicGltfLoader {
2373
2358
  obj.geometry.dispose();
2374
2359
  });
2375
2360
  } catch (error) {
2376
- console.error("Failed to merge geometries:", error);
2361
+ console.warn("DynamicLoader: Failed to merge geometries:", error);
2377
2362
  lineSegmentsArray.forEach((obj) => {
2378
2363
  obj.visible = true;
2379
2364
  rootGroup.add(obj);
@@ -2501,12 +2486,12 @@ export class DynamicGltfLoader {
2501
2486
 
2502
2487
  applyObjectTransforms(objectTransformMap) {
2503
2488
  if (this.mergedObjectMap.size === 0) {
2504
- console.warn("No merged objects to transform");
2489
+ console.warn("DynamicLoader: No merged objects to transform");
2505
2490
  return;
2506
2491
  }
2507
2492
 
2508
2493
  if (!this.transformData) {
2509
- console.warn("Transform texture not initialized");
2494
+ console.warn("DynamicLoader: Transform texture not initialized");
2510
2495
  return;
2511
2496
  }
2512
2497
 
@@ -2609,15 +2594,9 @@ export class DynamicGltfLoader {
2609
2594
 
2610
2595
  // Convert offset from structure-root-local to world space,
2611
2596
  // then from world space to parent-local space.
2612
- // The structure root's matrixWorld converts local→world.
2613
- // The parent's inverse matrixWorld converts world→parent-local.
2614
- // But we only need to transform a direction/offset (not a point),
2615
- // so we transform two points and subtract.
2616
2597
  if (object.userData.structureId) {
2617
2598
  const rootGroup = this.structureRoots.get(object.userData.structureId);
2618
2599
  if (rootGroup && object.parent && object.parent !== rootGroup) {
2619
- // Transform offset: structureRoot-local → world → parent-local
2620
- // For a vector (not point): apply rotation/scale only
2621
2600
  const origin = new Vector3(0, 0, 0);
2622
2601
  origin.applyMatrix4(rootGroup.matrixWorld);
2623
2602
  _offset.applyMatrix4(rootGroup.matrixWorld);
@@ -2633,7 +2612,6 @@ export class DynamicGltfLoader {
2633
2612
 
2634
2613
  object.position.add(_offset);
2635
2614
 
2636
- // Also update highlight wireframe if it exists right now
2637
2615
  if (object.userData.highlight) {
2638
2616
  object.userData.highlight.position.copy(object.position);
2639
2617
  }
@@ -2702,6 +2680,7 @@ export class DynamicGltfLoader {
2702
2680
  // Cache it
2703
2681
  obj.userData.explodeVector = explodeVector;
2704
2682
  }
2683
+
2705
2684
  const explodeVector = obj.userData.explodeVector;
2706
2685
  const distance = explodeVector.length();
2707
2686
 
@@ -2772,7 +2751,7 @@ export class DynamicGltfLoader {
2772
2751
 
2773
2752
  syncHiddenObjects() {
2774
2753
  if (this.mergedObjectMap.size === 0) {
2775
- console.log("No merged objects to sync");
2754
+ console.warn("DynamicLoader: No merged objects to sync");
2776
2755
  return;
2777
2756
  }
2778
2757
 
@@ -4,10 +4,36 @@ import {
4
4
  Color,
5
5
  DoubleSide,
6
6
  MeshPhongMaterial,
7
+ MeshStandardMaterial,
7
8
  PointsMaterial,
8
9
  LineBasicMaterial,
10
+ RepeatWrapping,
11
+ ClampToEdgeWrapping,
12
+ MirroredRepeatWrapping,
13
+ NearestFilter,
14
+ NearestMipmapNearestFilter,
15
+ NearestMipmapLinearFilter,
16
+ LinearFilter,
17
+ LinearMipmapNearestFilter,
18
+ LinearMipmapLinearFilter,
19
+ SRGBColorSpace,
9
20
  } from "three";
10
21
 
22
+ const GL_TO_THREE_WRAP = {
23
+ 33071: ClampToEdgeWrapping,
24
+ 33648: MirroredRepeatWrapping,
25
+ 10497: RepeatWrapping,
26
+ };
27
+
28
+ const GL_TO_THREE_FILTER = {
29
+ 9728: NearestFilter,
30
+ 9729: LinearFilter,
31
+ 9984: NearestMipmapNearestFilter,
32
+ 9985: LinearMipmapNearestFilter,
33
+ 9986: NearestMipmapLinearFilter,
34
+ 9987: LinearMipmapLinearFilter,
35
+ };
36
+
11
37
  export const GL_COMPONENT_TYPES = {
12
38
  5120: Int8Array,
13
39
  5121: Uint8Array,
@@ -54,6 +80,7 @@ export class GltfStructure {
54
80
  this.pendingRequests = [];
55
81
  this.batchTimeout = null;
56
82
  this.textureLoader = new TextureLoader();
83
+ this.textureLoader.crossOrigin = "anonymous";
57
84
  this.materials = new Map();
58
85
  this.textureCache = new Map();
59
86
  this.materialCache = new Map();
@@ -67,7 +94,7 @@ export class GltfStructure {
67
94
  const json = await this.loadController.loadJson();
68
95
 
69
96
  if (json.asset === undefined || json.asset.version[0] < 2) {
70
- throw new Error("GltfStructure: Unsupported asset. glTF versions >=2.0 are supported.");
97
+ throw new Error("DynamicLoader: Unsupported asset. glTF versions >=2.0 are supported.");
71
98
  }
72
99
 
73
100
  this.json = json;
@@ -101,7 +128,9 @@ export class GltfStructure {
101
128
  scheduleRequest(request) {
102
129
  return new Promise((resolve, reject) => {
103
130
  if (this.loadingAborted) {
104
- reject(this.criticalError || new Error("Structure loading has been aborted due to critical error"));
131
+ reject(
132
+ this.criticalError || new Error("DynamicLoader: Structure loading has been aborted due to critical error")
133
+ );
105
134
  return;
106
135
  }
107
136
  this.pendingRequests.push({
@@ -150,7 +179,7 @@ export class GltfStructure {
150
179
  }
151
180
 
152
181
  console.error(
153
- `❌ Critical error for structure "${this.id}". All further loading aborted.`,
182
+ `DynamicLoader: Critical error for structure "${this.id}". All further loading aborted.`,
154
183
  `\n Error: ${error.message || error}`,
155
184
  `\n Rejected ${requests.length} pending chunk requests.`
156
185
  );
@@ -224,7 +253,7 @@ export class GltfStructure {
224
253
  const promises = finalRanges.map(async (range, index) => {
225
254
  if (this.loadingAborted) {
226
255
  for (const req of range.requests) {
227
- req._reject(this.criticalError || new Error("Structure loading aborted"));
256
+ req._reject(this.criticalError || new Error("DynamicLoader: Structure loading aborted"));
228
257
  }
229
258
  return;
230
259
  }
@@ -251,7 +280,10 @@ export class GltfStructure {
251
280
  if (this.isCriticalHttpError(error)) {
252
281
  this.abortLoading(error);
253
282
  } else {
254
- console.warn(`Failed to load chunk ${index + 1}/${finalRanges.length} (${range.start}-${range.end}):`, error);
283
+ console.warn(
284
+ `DynamicLoader: Failed to load chunk ${index + 1}/${finalRanges.length} (${range.start}-${range.end}):`,
285
+ error
286
+ );
255
287
  }
256
288
  } finally {
257
289
  this.loader.releaseChunkSlot();
@@ -274,7 +306,7 @@ export class GltfStructure {
274
306
  createTypedArray(buffer, offset, length, componentType) {
275
307
  try {
276
308
  if (!buffer || !(buffer instanceof ArrayBuffer)) {
277
- throw new Error("Invalid buffer");
309
+ throw new Error("DynamicLoader: Invalid buffer");
278
310
  }
279
311
 
280
312
  let elementSize;
@@ -292,23 +324,25 @@ export class GltfStructure {
292
324
  elementSize = 4;
293
325
  break; // UNSIGNED_INT, FLOAT
294
326
  default:
295
- throw new Error(`Unsupported component type: ${componentType}`);
327
+ throw new Error(`DynamicLoader: Unsupported component type: ${componentType}`);
296
328
  }
297
329
 
298
330
  const numElements = length / elementSize;
299
331
  if (!Number.isInteger(numElements)) {
300
- throw new Error(`Invalid length ${length} for component type ${componentType}`);
332
+ throw new Error(`DynamicLoader: Invalid length ${length} for component type ${componentType}`);
301
333
  }
302
334
 
303
335
  if (length > buffer.byteLength) {
304
- throw new Error(`Buffer too small: need ${length} bytes, but buffer is ${buffer.byteLength} bytes`);
336
+ throw new Error(
337
+ `DynamicLoader: Buffer too small: need ${length} bytes, but buffer is ${buffer.byteLength} bytes`
338
+ );
305
339
  }
306
340
 
307
341
  const ArrayType = GL_COMPONENT_TYPES[componentType];
308
342
  return new ArrayType(buffer, offset, numElements);
309
343
  } catch (error) {
310
344
  if (error.name !== "AbortError") {
311
- console.error("Error creating typed array:", {
345
+ console.warn("DynamicLoader: Error creating typed array:", {
312
346
  bufferSize: buffer?.byteLength,
313
347
  offset,
314
348
  length,
@@ -322,7 +356,7 @@ export class GltfStructure {
322
356
 
323
357
  async createBufferAttribute(accessorIndex) {
324
358
  if (!this.json) {
325
- throw new Error("No GLTF structure loaded");
359
+ throw new Error("DynamicLoader: No GLTF structure loaded");
326
360
  }
327
361
 
328
362
  const gltf = this.json;
@@ -345,7 +379,7 @@ export class GltfStructure {
345
379
  return attribute;
346
380
  } catch (error) {
347
381
  if (error.name !== "AbortError") {
348
- console.error("Error creating buffer attribute:", {
382
+ console.warn("DynamicLoader: Error creating buffer attribute:", {
349
383
  error,
350
384
  accessor,
351
385
  bufferView,
@@ -368,7 +402,7 @@ export class GltfStructure {
368
402
  case 5126: // FLOAT
369
403
  return 4;
370
404
  default:
371
- throw new Error(`Unknown component type: ${componentType}`);
405
+ throw new Error(`DynamicLoader: Unknown component type: ${componentType}`);
372
406
  }
373
407
  }
374
408
 
@@ -389,7 +423,7 @@ export class GltfStructure {
389
423
  case "MAT4":
390
424
  return 16;
391
425
  default:
392
- throw new Error(`Unknown type: ${type}`);
426
+ throw new Error(`DynamicLoader: Unknown type: ${type}`);
393
427
  }
394
428
  }
395
429
 
@@ -397,27 +431,52 @@ export class GltfStructure {
397
431
  if (!this.json.textures) return;
398
432
 
399
433
  const loadTexture = async (imageIndex) => {
400
- const image = this.json.images[imageIndex];
401
-
402
- if (image.uri) {
403
- const fullUrl = await this.loadController.resolveURL(image.uri);
404
- return this.textureLoader.loadAsync(fullUrl);
405
- } else if (image.bufferView !== undefined) {
406
- const bufferView = this.json.bufferViews[image.bufferView];
407
- const array = await this.getBufferView(bufferView.byteOffset || 0, bufferView.byteLength, 5121);
408
- const blob = new Blob([array], { type: image.mimeType });
409
- const url = URL.createObjectURL(blob);
410
- const texture = await this.textureLoader.loadAsync(url);
411
- URL.revokeObjectURL(url);
412
- texture.flipY = false;
413
- return texture;
434
+ let textureName = `index_${imageIndex}`;
435
+ try {
436
+ const image = this.json.images[imageIndex];
437
+ textureName = image.uri || image.name || textureName;
438
+
439
+ if (image.uri) {
440
+ const fullUrl = await this.loadController.resolveURL(image.uri);
441
+ return await this.textureLoader.loadAsync(fullUrl);
442
+ } else if (image.bufferView !== undefined) {
443
+ const bufferView = this.json.bufferViews[image.bufferView];
444
+ const array = await this.getBufferView(bufferView.byteOffset || 0, bufferView.byteLength, 5121);
445
+ const blob = new Blob([array], { type: image.mimeType });
446
+ const url = URL.createObjectURL(blob);
447
+ const texture = await this.textureLoader.loadAsync(url);
448
+ URL.revokeObjectURL(url);
449
+ return texture;
450
+ }
451
+ } catch {
452
+ console.warn(`DynamicLoader: Error loading texture ${textureName}`);
414
453
  }
415
454
  };
416
455
 
417
456
  const texturePromises = [];
418
457
  for (let i = 0; i < this.json.textures.length; i++) {
419
458
  texturePromises.push(
420
- loadTexture(this.json.textures[i].source).then((texture) => this.textureCache.set(i, texture))
459
+ loadTexture(this.json.textures[i].source).then((texture) => {
460
+ if (texture) {
461
+ // Apply sampler settings, mapping WebGL constants to Three.js constants
462
+ const samplerDef =
463
+ this.json.textures[i].sampler !== undefined && this.json.samplers
464
+ ? this.json.samplers[this.json.textures[i].sampler]
465
+ : {};
466
+
467
+ texture.magFilter = GL_TO_THREE_FILTER[samplerDef.magFilter] || LinearFilter;
468
+ texture.minFilter = GL_TO_THREE_FILTER[samplerDef.minFilter] || LinearMipmapLinearFilter;
469
+
470
+ // glTF spec: default wrap is REPEAT (10497) when not specified
471
+ texture.wrapS = GL_TO_THREE_WRAP[samplerDef.wrapS] || RepeatWrapping;
472
+ texture.wrapT = GL_TO_THREE_WRAP[samplerDef.wrapT] || RepeatWrapping;
473
+
474
+ // glTF textures use non-flipped Y convention
475
+ texture.flipY = false;
476
+ texture.needsUpdate = true;
477
+ }
478
+ this.textureCache.set(i, texture);
479
+ })
421
480
  );
422
481
  }
423
482
  await Promise.all(texturePromises);
@@ -455,8 +514,17 @@ export class GltfStructure {
455
514
  params.opacity = pbr.baseColorFactor[3];
456
515
  if (params.opacity < 1.0) params.transparent = true;
457
516
  }
458
- if (pbr.baseColorTexture) {
459
- params.map = this.textureCache.get(pbr.baseColorTexture.index);
517
+
518
+ if (pbr.baseColorTexture !== undefined) {
519
+ const texture = this.textureCache.get(pbr.baseColorTexture.index);
520
+ if (texture) {
521
+ params.map = texture;
522
+ // Set color to white if map is present and baseColorFactor is not,
523
+ // or else texture will be tinted with default color
524
+ if (!pbr.baseColorFactor) {
525
+ params.color = new Color(0xffffff);
526
+ }
527
+ }
460
528
  }
461
529
  }
462
530
 
@@ -491,11 +559,73 @@ export class GltfStructure {
491
559
  params.polygonOffsetFactor = 1;
492
560
  params.polygonOffsetUnits = 1;
493
561
 
494
- if (materialDef.normalTexture) {
495
- params.normalMap = this.textureCache.get(materialDef.normalTexture.index);
562
+ // Ensure color is white if only map is present so it renders the texture properly
563
+ if (params.map && !materialDef.pbrMetallicRoughness?.baseColorFactor) {
564
+ params.color = new Color(0xffffff);
496
565
  }
497
566
 
498
- material = new MeshPhongMaterial(params);
567
+ if (materialDef.normalTexture !== undefined) {
568
+ const normalMap = this.textureCache.get(materialDef.normalTexture.index);
569
+ if (normalMap) {
570
+ params.normalMap = normalMap;
571
+ }
572
+ }
573
+
574
+ // Emissive texture
575
+ if (materialDef.emissiveTexture !== undefined) {
576
+ const emissiveMap = this.textureCache.get(materialDef.emissiveTexture.index);
577
+ if (emissiveMap) {
578
+ params.emissiveMap = emissiveMap;
579
+ }
580
+ }
581
+
582
+ // Metallic-roughness texture
583
+ // Check if we need MeshStandardMaterial for full PBR support or we can use MeshPhongMaterial
584
+ // Use MeshStandardMaterial only if there are PBR textures present to optimize rendering performance.
585
+ // If only factors are present, MeshPhongMaterial is enough for basic lighting.
586
+ const usePBR = materialDef.pbrMetallicRoughness?.metallicRoughnessTexture !== undefined;
587
+
588
+ if (usePBR) {
589
+ // Apply default values if not specified
590
+ params.metalness =
591
+ materialDef.pbrMetallicRoughness?.metallicFactor !== undefined
592
+ ? materialDef.pbrMetallicRoughness.metallicFactor
593
+ : 1.0;
594
+ params.roughness =
595
+ materialDef.pbrMetallicRoughness?.roughnessFactor !== undefined
596
+ ? materialDef.pbrMetallicRoughness.roughnessFactor
597
+ : 1.0;
598
+
599
+ if (materialDef.pbrMetallicRoughness?.metallicRoughnessTexture !== undefined) {
600
+ const map = this.textureCache.get(materialDef.pbrMetallicRoughness.metallicRoughnessTexture.index);
601
+ if (map) {
602
+ params.metalnessMap = map;
603
+ params.roughnessMap = map;
604
+ }
605
+ }
606
+ material = new MeshStandardMaterial(params);
607
+ } else {
608
+ material = new MeshPhongMaterial(params);
609
+ }
610
+
611
+ // Additional maps setup (needs to happen after instantiation for some properties)
612
+ if (material.map) material.map.colorSpace = SRGBColorSpace;
613
+ if (material.emissiveMap) material.emissiveMap.colorSpace = SRGBColorSpace;
614
+
615
+ if (material.normalMap) {
616
+ if (materialDef.normalTexture?.scale !== undefined) {
617
+ material.normalScale.set(materialDef.normalTexture.scale, materialDef.normalTexture.scale);
618
+ }
619
+ }
620
+ if (materialDef.occlusionTexture !== undefined) {
621
+ const aoMap = this.textureCache.get(materialDef.occlusionTexture.index);
622
+ if (aoMap) {
623
+ material.aoMap = aoMap;
624
+ if (materialDef.occlusionTexture.strength !== undefined) {
625
+ material.aoMapIntensity = materialDef.occlusionTexture.strength;
626
+ }
627
+ }
628
+ }
499
629
  }
500
630
 
501
631
  return material;