@needle-tools/usd 0.0.2-next.d90870e → 1.0.0-next.d536d99

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 (41) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/README.md +52 -9
  3. package/package.json +42 -10
  4. package/src/bindings/emHdBindings.js +5 -12227
  5. package/src/bindings/emHdBindings.wasm +0 -0
  6. package/src/bindings/index.js +130 -47
  7. package/src/bindings/openusd-build-info.json +40 -0
  8. package/src/create.three.js +362 -53
  9. package/src/hydra/ThreeJsRenderDelegate.js +959 -73
  10. package/src/plugins/index.js +1 -2
  11. package/src/plugins/needle.js +37 -2
  12. package/src/types/bindings.d.ts +296 -3
  13. package/src/types/create.three.d.ts +78 -7
  14. package/src/types/hydra.d.ts +7 -5
  15. package/src/types/plugins.d.ts +2 -0
  16. package/src/types/usd-core-bindings.d.ts +240 -0
  17. package/src/utils.js +3 -3
  18. package/examples/index.html +0 -58
  19. package/examples/package-lock.json +0 -1548
  20. package/examples/package.json +0 -24
  21. package/examples/public/HttpReferences copy.usda +0 -46
  22. package/examples/public/HttpReferences.usda +0 -44
  23. package/examples/public/gingerbread/GingerbreadHouse.usda +0 -35
  24. package/examples/public/gingerbread/house/GingerBreadHouse.usdc +0 -0
  25. package/examples/public/gingerbread/house/textures/color.jpg +0 -0
  26. package/examples/public/gingerbread/house/textures/metallic_roughness.jpg +0 -0
  27. package/examples/public/gingerbread/house/textures/normal.jpg +0 -0
  28. package/examples/public/gingerbread/snowman/Snowman.usdc +0 -0
  29. package/examples/public/gingerbread/snowman/textures/color.jpg +0 -0
  30. package/examples/public/gingerbread/snowman/textures/metallic_roughness.jpg +0 -0
  31. package/examples/public/gingerbread/snowman/textures/normal.jpg +0 -0
  32. package/examples/public/test.usdz +0 -0
  33. package/examples/public/vite.svg +0 -1
  34. package/examples/src/fileHandling.ts +0 -256
  35. package/examples/src/main.ts +0 -167
  36. package/examples/src/three.ts +0 -140
  37. package/examples/src/vite-env.d.ts +0 -1
  38. package/examples/tsconfig.json +0 -23
  39. package/examples/vite.config.js +0 -21
  40. package/src/bindings/emHdBindings.data +0 -19331
  41. package/src/bindings/emHdBindings.worker.js +0 -124
@@ -1,7 +1,33 @@
1
- import { TextureLoader, BufferGeometry, MeshPhysicalMaterial, DoubleSide, Color, Mesh, Float32BufferAttribute, SRGBColorSpace, RGBAFormat, RepeatWrapping, LinearSRGBColorSpace, Vector2 } from 'three';
1
+ import * as THREE from 'three';
2
2
  import { TGALoader } from 'three/addons/loaders/TGALoader.js';
3
3
  import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
4
4
 
5
+ const {
6
+ TextureLoader,
7
+ BufferGeometry,
8
+ MeshPhysicalMaterial,
9
+ DoubleSide,
10
+ Color,
11
+ Mesh,
12
+ InstancedMesh,
13
+ Matrix4,
14
+ Float32BufferAttribute,
15
+ SRGBColorSpace,
16
+ RGBAFormat,
17
+ RepeatWrapping,
18
+ LinearSRGBColorSpace,
19
+ Vector2,
20
+ CameraHelper,
21
+ DirectionalLight,
22
+ DirectionalLightHelper,
23
+ HemisphereLight,
24
+ OrthographicCamera,
25
+ PerspectiveCamera,
26
+ PointLight,
27
+ PointLightHelper,
28
+ MathUtils,
29
+ } = THREE;
30
+
5
31
  const debugTextures = false;
6
32
  const debugMaterials = false;
7
33
  const debugMeshes = false;
@@ -9,6 +35,88 @@ const debugPrims = false;
9
35
  const disableTextures = false;
10
36
  const disableMaterials = false;
11
37
 
38
+ let materialXModulePromise = null;
39
+
40
+ async function getMaterialXModule() {
41
+ materialXModulePromise ??= import('@needle-tools/materialx').then(module => ({
42
+ MaterialX: module.Experimental_API,
43
+ MaterialXMaterial: module.MaterialXMaterial,
44
+ }));
45
+ return materialXModulePromise;
46
+ }
47
+
48
+ function disposeTexture(texture, disposed = new Set()) {
49
+ if (!texture || disposed.has(texture) || typeof texture.dispose !== 'function') return;
50
+ disposed.add(texture);
51
+ texture.dispose();
52
+ }
53
+
54
+ function disposeMaterialResources(material, disposedMaterials = new Set(), disposedTextures = new Set()) {
55
+ if (!material) return;
56
+ if (Array.isArray(material)) {
57
+ for (const entry of material) disposeMaterialResources(entry, disposedMaterials, disposedTextures);
58
+ return;
59
+ }
60
+ if (disposedMaterials.has(material)) return;
61
+ disposedMaterials.add(material);
62
+
63
+ for (const value of Object.values(material)) {
64
+ if (value && typeof value === 'object' && value.isTexture) {
65
+ disposeTexture(value, disposedTextures);
66
+ }
67
+ }
68
+
69
+ if (material !== defaultMaterial && typeof material.dispose === 'function') {
70
+ material.dispose();
71
+ }
72
+ }
73
+
74
+ function disposeObjectResources(object) {
75
+ if (!object) return;
76
+ object.traverse?.((entry) => {
77
+ entry.geometry?.dispose?.();
78
+ disposeMaterialResources(entry.material);
79
+ });
80
+ object.parent?.remove(object);
81
+ }
82
+
83
+ function primNameFromPath(id, fallback) {
84
+ const path = String(id || "");
85
+ const slash = path.lastIndexOf("/");
86
+ return slash >= 0 ? path.substring(slash + 1) || fallback : path || fallback;
87
+ }
88
+
89
+ function applyHydraTransform(object, matrix) {
90
+ if (!object || !matrix || matrix.length < 16) return;
91
+ object.matrix.set(...Array.from(matrix).slice(0, 16));
92
+ object.matrix.transpose();
93
+ object.matrix.decompose(object.position, object.quaternion, object.scale);
94
+ object.matrixAutoUpdate = true;
95
+ }
96
+
97
+ const defaultScenePrimitiveLightIntensityScale = 0.01;
98
+ const lightSprimTypeIds = new Set([
99
+ "domeLight",
100
+ "cylinderLight",
101
+ "diskLight",
102
+ "distantLight",
103
+ "light",
104
+ "rectLight",
105
+ "simpleLight",
106
+ "sphereLight",
107
+ ]);
108
+
109
+ const usdLightTypeNames = {
110
+ domeLight: "DomeLight",
111
+ cylinderLight: "CylinderLight",
112
+ diskLight: "DiskLight",
113
+ distantLight: "DistantLight",
114
+ light: "LightAPI",
115
+ rectLight: "RectLight",
116
+ simpleLight: "SimpleLight",
117
+ sphereLight: "SphereLight",
118
+ };
119
+
12
120
  class TextureRegistry {
13
121
  /**
14
122
  * @param {import('..').threeJsRenderDelegateConfig} config
@@ -17,6 +125,9 @@ class TextureRegistry {
17
125
  this.config = config;
18
126
  this.allPaths = config.paths;
19
127
  this.textures = [];
128
+ this.loadedTextures = new Set();
129
+ this.objectUrls = new Set();
130
+ this.disposed = false;
20
131
  this.loader = new TextureLoader();
21
132
  this.tgaLoader = new TGALoader();
22
133
  this.exrLoader = new EXRLoader();
@@ -32,7 +143,97 @@ class TextureRegistry {
32
143
  }
33
144
  }
34
145
 
146
+ normalizeResourcePath(resourcePath) {
147
+ const rawPath = String(resourcePath ?? "").replace(/\\/g, "/");
148
+ const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(rawPath);
149
+ if (isUrl) {
150
+ return rawPath;
151
+ }
152
+
153
+ const preserveLeadingSlash = rawPath.startsWith("/");
154
+ const path = rawPath
155
+ .replace(/^\/+/, preserveLeadingSlash ? "/" : "")
156
+ .replace(/^(?:\.\/)+/, "")
157
+ .replace(/\/\.\//g, "/");
158
+ const parts = [];
159
+ for (const part of path.split("/")) {
160
+ if (!part || part === ".") continue;
161
+ if (part === "..") {
162
+ parts.pop();
163
+ continue;
164
+ }
165
+ parts.push(part);
166
+ }
167
+ return (preserveLeadingSlash ? "/" : "") + parts.join("/");
168
+ }
169
+
170
+ materialResourcePathCandidates(resourcePath) {
171
+ const rawPath = String(resourcePath ?? "").replace(/\\/g, "/");
172
+ const normalized = this.normalizeResourcePath(rawPath);
173
+ const withoutLeadingSlash = normalized.replace(/^\/+/, "");
174
+ const candidates = new Set([
175
+ rawPath,
176
+ normalized,
177
+ withoutLeadingSlash,
178
+ withoutLeadingSlash.replace(/^(?:\.\/)+/, ""),
179
+ withoutLeadingSlash.replace(/^(?:\.\.\/)+/, ""),
180
+ ]);
181
+
182
+ const pathParts = withoutLeadingSlash.split("/").filter(Boolean);
183
+ const texturesIndex = pathParts.lastIndexOf("textures");
184
+ if (texturesIndex >= 0) {
185
+ candidates.add(pathParts.slice(texturesIndex).join("/"));
186
+ }
187
+
188
+ return [...candidates].filter(Boolean);
189
+ }
190
+
191
+ getResourceExtension(resourcePath) {
192
+ const path = String(resourcePath ?? "").toLowerCase();
193
+ const extensionMatches = [...path.matchAll(/\.([a-z0-9]+)(?=\]|$|[?#])/g)];
194
+ return extensionMatches.length ? extensionMatches[extensionMatches.length - 1][1] : "";
195
+ }
196
+
197
+ readResolvedResource(resourcePath) {
198
+ if (!resourcePath?.startsWith("/") || typeof this.config.USD?.ReadFile !== "function") {
199
+ return null;
200
+ }
201
+
202
+ try {
203
+ const file = this.config.USD.ReadFile(resourcePath);
204
+ return file?.byteLength ? file : null;
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ resolveResourcePath(resourcePath) {
212
+ const candidates = this.materialResourcePathCandidates(resourcePath);
213
+ if (!candidates.length) return "";
214
+
215
+ const knownPaths = Array.isArray(this.allPaths) ? this.allPaths : [];
216
+ for (const candidate of candidates) {
217
+ const candidateWithoutRoot = candidate.replace(/^needle\//, "");
218
+ for (const knownPath of knownPaths) {
219
+ const known = this.normalizeResourcePath(knownPath);
220
+ const knownWithoutRoot = known.replace(/^needle\//, "");
221
+ if (
222
+ known === candidate ||
223
+ knownWithoutRoot === candidate ||
224
+ knownWithoutRoot === candidateWithoutRoot ||
225
+ knownWithoutRoot.endsWith("/" + candidateWithoutRoot)
226
+ ) {
227
+ return known;
228
+ }
229
+ }
230
+ }
231
+
232
+ return candidates[0];
233
+ }
234
+
35
235
  getTexture(resourcePath) {
236
+ resourcePath = this.resolveResourcePath(resourcePath);
36
237
  if (debugTextures) console.log("get texture", resourcePath);
37
238
  if (this.textures[resourcePath]) {
38
239
  return this.textures[resourcePath];
@@ -49,18 +250,18 @@ class TextureRegistry {
49
250
  }
50
251
 
51
252
  let filetype = undefined;
52
- let lowercaseFilename = resourcePath.toLowerCase();
53
- if (lowercaseFilename.indexOf('.png') >= lowercaseFilename.length - 5) {
253
+ const extension = this.getResourceExtension(resourcePath);
254
+ if (extension === 'png') {
54
255
  filetype = 'image/png';
55
- } else if (lowercaseFilename.indexOf('.jpg') >= lowercaseFilename.length - 5) {
256
+ } else if (extension === 'jpg') {
56
257
  filetype = 'image/jpeg';
57
- } else if (lowercaseFilename.indexOf('.jpeg') >= lowercaseFilename.length - 5) {
258
+ } else if (extension === 'jpeg') {
58
259
  filetype = 'image/jpeg';
59
- } else if (lowercaseFilename.indexOf('.exr') >= lowercaseFilename.length - 4) {
260
+ } else if (extension === 'exr') {
60
261
  console.warn("EXR textures are not fully supported yet", resourcePath);
61
262
  // using EXRLoader explicitly
62
263
  filetype = 'image/x-exr';
63
- } else if (lowercaseFilename.indexOf('.tga') >= lowercaseFilename.length - 4) {
264
+ } else if (extension === 'tga') {
64
265
  console.warn("TGA textures are not fully supported yet", resourcePath);
65
266
  // using TGALoader explicitly
66
267
  filetype = 'image/tga';
@@ -69,48 +270,69 @@ class TextureRegistry {
69
270
  // throw new Error('Unknown filetype');
70
271
  }
71
272
 
72
- this.config.driver().getFile(resourcePath, async (loadedFile) => {
73
- let loader = this.loader;
74
- if (filetype === 'image/tga')
75
- loader = this.tgaLoader;
76
- else if (filetype === 'image/x-exr')
77
- loader = this.exrLoader;
78
-
79
- const baseUrl = this.baseUrl;
80
- function loadFromFile(_loadedFile) {
81
- let url = undefined;
82
- if (debugTextures) console.log("window.driver.getFile", resourcePath, " => ", _loadedFile);
83
- if (_loadedFile) {
84
- let blob = new Blob([_loadedFile.slice(0)], { type: filetype });
85
- url = URL.createObjectURL(blob);
86
- } else {
87
- if (baseUrl)
88
- url = baseUrl + '/' + resourcePath;
89
- else
90
- url = resourcePath;
91
- }
92
- if (debugTextures) console.log("Loading texture from", url, "with loader", loader, "_loadedFile", _loadedFile, "baseUrl", baseUrl, "resourcePath", resourcePath);
93
- // Load the texture
94
- loader.load(
95
- // resource URL
96
- url,
97
-
98
- // onLoad callback
99
- (texture) => {
100
- texture.name = resourcePath;
101
- textureResolve(texture);
102
- },
103
-
104
- // onProgress callback currently not used
105
- undefined,
106
-
107
- // onError callback
108
- (err) => {
109
- textureReject(err);
110
- }
111
- );
273
+ let loader = this.loader;
274
+ if (filetype === 'image/tga')
275
+ loader = this.tgaLoader;
276
+ else if (filetype === 'image/x-exr')
277
+ loader = this.exrLoader;
278
+
279
+ const baseUrl = this.baseUrl;
280
+ const loadFromFile = (_loadedFile) => {
281
+ let url = undefined;
282
+ if (debugTextures) console.log("window.driver.getFile", resourcePath, " => ", _loadedFile);
283
+ if (_loadedFile) {
284
+ let blob = new Blob([_loadedFile.slice(0)], { type: filetype });
285
+ url = URL.createObjectURL(blob);
286
+ this.objectUrls.add(url);
287
+ } else {
288
+ if (baseUrl)
289
+ url = baseUrl + '/' + resourcePath;
290
+ else
291
+ url = resourcePath;
112
292
  }
293
+ if (debugTextures) console.log("Loading texture from", url, "with loader", loader, "_loadedFile", _loadedFile, "baseUrl", baseUrl, "resourcePath", resourcePath);
294
+ // Load the texture
295
+ loader.load(
296
+ // resource URL
297
+ url,
298
+
299
+ // onLoad callback
300
+ (texture) => {
301
+ if (url?.startsWith('blob:')) {
302
+ URL.revokeObjectURL(url);
303
+ this.objectUrls.delete(url);
304
+ }
305
+ texture.name = resourcePath;
306
+ if (this.disposed) {
307
+ texture.dispose();
308
+ }
309
+ else {
310
+ this.loadedTextures.add(texture);
311
+ }
312
+ textureResolve(texture);
313
+ },
314
+
315
+ // onProgress callback currently not used
316
+ undefined,
113
317
 
318
+ // onError callback
319
+ (err) => {
320
+ if (url?.startsWith('blob:')) {
321
+ URL.revokeObjectURL(url);
322
+ this.objectUrls.delete(url);
323
+ }
324
+ textureReject(err);
325
+ }
326
+ );
327
+ };
328
+
329
+ const resolvedFile = this.readResolvedResource(resourcePath);
330
+ if (resolvedFile) {
331
+ loadFromFile(resolvedFile);
332
+ return this.textures[resourcePath];
333
+ }
334
+
335
+ this.config.driver().getFile(resourcePath, async (loadedFile) => {
114
336
  if (!loadedFile) {
115
337
  // if the file is not part of the filesystem, we can still try to fetch it from the network
116
338
  if (baseUrl) {
@@ -127,6 +349,166 @@ class TextureRegistry {
127
349
 
128
350
  return this.textures[resourcePath];
129
351
  }
352
+
353
+ dispose() {
354
+ this.disposed = true;
355
+ for (const url of this.objectUrls) {
356
+ URL.revokeObjectURL(url);
357
+ }
358
+ this.objectUrls.clear();
359
+
360
+ for (const texture of this.loadedTextures) {
361
+ texture.dispose();
362
+ }
363
+ this.loadedTextures.clear();
364
+
365
+ for (const texturePromise of Object.values(this.textures)) {
366
+ Promise.resolve(texturePromise).then(texture => {
367
+ if (texture?.dispose) texture.dispose();
368
+ }).catch(() => {});
369
+ }
370
+ this.textures = [];
371
+ }
372
+ }
373
+
374
+ class HydraScenePrimitive {
375
+ /**
376
+ * @param {string} typeId
377
+ * @param {string} id
378
+ * @param {ThreeRenderDelegateInterface} hydraInterface
379
+ */
380
+ constructor(typeId, id, hydraInterface) {
381
+ this._typeId = typeId;
382
+ this._id = id;
383
+ this._interface = hydraInterface;
384
+ this._object = null;
385
+ this._helper = null;
386
+ }
387
+
388
+ updateCameraState(state) {
389
+ if (!this._id) return;
390
+ const projection = state?.projection || "perspective";
391
+ const isOrthographic = projection === "orthographic";
392
+ if (!this._object || (isOrthographic && !this._object.isOrthographicCamera) || (!isOrthographic && !this._object.isPerspectiveCamera)) {
393
+ this._replaceObject(isOrthographic
394
+ ? new OrthographicCamera(-1, 1, 1, -1, Number(state?.near) || 0.01, Number(state?.far) || 100000)
395
+ : new PerspectiveCamera(45, 1, Number(state?.near) || 0.01, Number(state?.far) || 100000));
396
+ this._object.userData.usdKind = "sprim";
397
+ }
398
+
399
+ const focalLength = Number(state?.focalLength) || 50;
400
+ const verticalAperture = Number(state?.verticalAperture) || 20.955;
401
+ const horizontalAperture = Number(state?.horizontalAperture) || verticalAperture;
402
+ const near = Number(state?.near);
403
+ const far = Number(state?.far);
404
+ if (this._object.isPerspectiveCamera) {
405
+ this._object.fov = MathUtils.radToDeg(2 * Math.atan((verticalAperture * 0.5) / focalLength));
406
+ this._object.aspect = horizontalAperture > 0 && verticalAperture > 0 ? horizontalAperture / verticalAperture : 1;
407
+ }
408
+ if (Number.isFinite(near) && near > 0) this._object.near = near;
409
+ if (Number.isFinite(far) && far > 0) this._object.far = far;
410
+ this._object.name = primNameFromPath(this._id, "UsdCamera");
411
+ this._object.userData.usdPath = this._id;
412
+ this._object.userData.usdTypeName = "Camera";
413
+ applyHydraTransform(this._object, state?.transform);
414
+ this._object.updateProjectionMatrix?.();
415
+ this._syncHelper();
416
+ }
417
+
418
+ updateLightState(state) {
419
+ if (!this._id) return;
420
+ const typeId = String(state?.typeId || this._typeId);
421
+ const LightCtor = typeId === "distantLight"
422
+ ? DirectionalLight
423
+ : typeId === "domeLight"
424
+ ? HemisphereLight
425
+ : PointLight;
426
+
427
+ if (!this._object || !(this._object instanceof LightCtor)) {
428
+ const color = new Color(1, 1, 1);
429
+ this._replaceObject(typeId === "domeLight"
430
+ ? new HemisphereLight(color, new Color(0.2, 0.2, 0.2), 1)
431
+ : new LightCtor(color, 1));
432
+ this._object.userData.usdKind = "sprim";
433
+ }
434
+
435
+ const colorValue = Array.isArray(state?.color) ? state.color : [1, 1, 1];
436
+ this._object.color?.setRGB?.(
437
+ Number(colorValue[0]) || 0,
438
+ Number(colorValue[1]) || 0,
439
+ Number(colorValue[2]) || 0);
440
+ const scale = this._interface.config.scenePrimitiveLightIntensityScale ?? defaultScenePrimitiveLightIntensityScale;
441
+ this._object.intensity = (Number(state?.intensity) || 0) * scale;
442
+ this._object.visible = state?.visible !== false;
443
+ this._object.name = primNameFromPath(this._id, usdLightTypeNames[typeId] || "UsdLight");
444
+ this._object.userData.usdPath = this._id;
445
+ this._object.userData.usdTypeName = usdLightTypeNames[typeId] || typeId;
446
+ applyHydraTransform(this._object, state?.transform);
447
+ this._syncHelper(state);
448
+ }
449
+
450
+ dispose() {
451
+ disposeObjectResources(this._helper);
452
+ disposeObjectResources(this._object);
453
+ this._helper = null;
454
+ this._object = null;
455
+ }
456
+
457
+ _replaceObject(object) {
458
+ disposeObjectResources(this._helper);
459
+ disposeObjectResources(this._object);
460
+ this._helper = null;
461
+ this._object = object;
462
+ this._interface.config.scenePrimitiveRoot?.add(object);
463
+ }
464
+
465
+ _syncHelper(state = {}) {
466
+ const object = this._object;
467
+ if (!object) return;
468
+ const wantHelper = object.isCamera
469
+ ? (this._interface.config.showCameraHelpers || this._interface.config.showScenePrimitiveHelpers)
470
+ : object.isLight
471
+ ? (this._interface.config.showLightHelpers || this._interface.config.showScenePrimitiveHelpers)
472
+ : false;
473
+ if (!wantHelper) {
474
+ disposeObjectResources(this._helper);
475
+ this._helper = null;
476
+ return;
477
+ }
478
+
479
+ const helperCtor = object.isCamera
480
+ ? CameraHelper
481
+ : object.isDirectionalLight
482
+ ? DirectionalLightHelper
483
+ : object.isPointLight
484
+ ? PointLightHelper
485
+ : null;
486
+ if (!helperCtor) {
487
+ disposeObjectResources(this._helper);
488
+ this._helper = null;
489
+ return;
490
+ }
491
+
492
+ const needsNewHelper = !this._helper ||
493
+ (object.isCamera && !this._helper.isCameraHelper) ||
494
+ (object.isDirectionalLight && this._helper.type !== "DirectionalLightHelper") ||
495
+ (object.isPointLight && this._helper.type !== "PointLightHelper");
496
+ if (needsNewHelper) {
497
+ disposeObjectResources(this._helper);
498
+ const color = object.color || new Color(1, 1, 1);
499
+ this._helper = object.isCamera
500
+ ? new CameraHelper(object)
501
+ : object.isDirectionalLight
502
+ ? new DirectionalLightHelper(object, 0.5, color)
503
+ : new PointLightHelper(object, Number(state?.radius) || 0.25, color);
504
+ this._helper.name = `${object.name}Helper`;
505
+ this._helper.userData.usdHelperFor = this._id;
506
+ this._interface.config.scenePrimitiveRoot?.add(this._helper);
507
+ }
508
+ this._helper.name = `${object.name}Helper`;
509
+ this._helper.visible = object.visible;
510
+ this._helper.update?.();
511
+ }
130
512
  }
131
513
 
132
514
  class HydraMesh {
@@ -140,18 +522,25 @@ class HydraMesh {
140
522
  this._interface = hydraInterface;
141
523
  this._points = undefined;
142
524
  this._normals = undefined;
525
+ this._tangents = undefined;
143
526
  this._colors = undefined;
144
527
  this._uvs = undefined;
145
528
  this._indices = undefined;
146
529
  this._materials = [];
530
+ this._visible = false;
531
+ this._renderTag = 'geometry';
532
+ this._instancedMesh = null;
533
+ this._instanceMatrix = new Matrix4();
147
534
 
148
535
  let material = new MeshPhysicalMaterial({
149
536
  side: DoubleSide,
150
537
  color: new Color(0xB4B4B4),
151
538
  // envMap: hydraInterface.config.envMap,
152
539
  });
540
+ this._ownedMaterial = material;
153
541
  this._materials.push(material);
154
542
  this._mesh = new Mesh(this._geometry, material);
543
+ this._mesh.visible = false;
155
544
  this._mesh.castShadow = true;
156
545
  this._mesh.receiveShadow = true;
157
546
 
@@ -168,6 +557,19 @@ class HydraMesh {
168
557
  hydraInterface.config.usdRoot.add(this._mesh); // FIXME
169
558
  }
170
559
 
560
+ dispose() {
561
+ if (!this._mesh) return;
562
+ this._interface.unassignMeshFromMaterials(this._mesh);
563
+ this._disposeInstancedMesh();
564
+ if (this._mesh.parent) {
565
+ this._mesh.parent.remove(this._mesh);
566
+ }
567
+ this._geometry.dispose();
568
+ disposeMaterialResources(this._ownedMaterial);
569
+ this._ownedMaterial = null;
570
+ this._mesh = null;
571
+ }
572
+
171
573
  updateOrder(attribute, attributeName, dimension = 3) {
172
574
  if (debugMeshes) console.log("updateOrder", attribute, attributeName, dimension);
173
575
  if (attribute && this._indices) {
@@ -179,6 +581,10 @@ class HydraMesh {
179
581
  }
180
582
  }
181
583
  this._geometry.setAttribute(attributeName, new Float32BufferAttribute(values, dimension));
584
+ if (attributeName === 'position') {
585
+ this._geometry.computeBoundingBox();
586
+ this._geometry.computeBoundingSphere();
587
+ }
182
588
  }
183
589
  }
184
590
 
@@ -210,6 +616,65 @@ class HydraMesh {
210
616
  this._mesh.matrixAutoUpdate = false;
211
617
  }
212
618
 
619
+ setInstanceTransforms(matrices, count = 0) {
620
+ if (!this._mesh) return;
621
+ const instanceCount = Number(count) || 0;
622
+ if (instanceCount <= 0) {
623
+ this._disposeInstancedMesh();
624
+ this._mesh.visible = this._visible && this._renderTag !== 'hidden';
625
+ return;
626
+ }
627
+
628
+ if (!this._instancedMesh || this._instancedMesh.count !== instanceCount) {
629
+ this._disposeInstancedMesh();
630
+ this._instancedMesh = new InstancedMesh(this._geometry, this._mesh.material, instanceCount);
631
+ this._instancedMesh.name = `${this._mesh.name}_instances`;
632
+ this._instancedMesh.castShadow = this._mesh.castShadow;
633
+ this._instancedMesh.receiveShadow = this._mesh.receiveShadow;
634
+ this._instancedMesh.matrixAutoUpdate = false;
635
+ this._instancedMesh.userData.usdPath = this._id;
636
+ this._instancedMesh.userData.usdInstanced = true;
637
+ this._interface.config.usdRoot.add(this._instancedMesh);
638
+ }
639
+
640
+ this._instancedMesh.material = this._mesh.material;
641
+ this._instancedMesh.visible = this._visible && this._renderTag !== 'hidden';
642
+ this._instancedMesh.userData.usdRenderTag = this._renderTag;
643
+ this._mesh.visible = false;
644
+
645
+ for (let i = 0; i < instanceCount; i++) {
646
+ const offset = i * 16;
647
+ this._instanceMatrix.set(...Array.from(matrices.slice(offset, offset + 16)));
648
+ this._instanceMatrix.transpose();
649
+ this._instancedMesh.setMatrixAt(i, this._instanceMatrix);
650
+ }
651
+ this._instancedMesh.instanceMatrix.needsUpdate = true;
652
+ this._instancedMesh.computeBoundingBox?.();
653
+ this._instancedMesh.computeBoundingSphere?.();
654
+ }
655
+
656
+ _disposeInstancedMesh() {
657
+ if (!this._instancedMesh) return;
658
+ if (this._instancedMesh.parent) {
659
+ this._instancedMesh.parent.remove(this._instancedMesh);
660
+ }
661
+ this._instancedMesh.dispose?.();
662
+ this._instancedMesh = null;
663
+ }
664
+
665
+ setVisibilityState(visible, renderTag = 'geometry') {
666
+ this._visible = Boolean(visible);
667
+ this._renderTag = String(renderTag || 'geometry');
668
+ if (this._mesh) {
669
+ this._mesh.visible = !this._instancedMesh && this._visible && this._renderTag !== 'hidden';
670
+ this._mesh.userData.usdRenderTag = this._renderTag;
671
+ }
672
+ if (this._instancedMesh) {
673
+ this._instancedMesh.visible = this._visible && this._renderTag !== 'hidden';
674
+ this._instancedMesh.userData.usdRenderTag = this._renderTag;
675
+ }
676
+ }
677
+
213
678
  /**
214
679
  * Sets automatically generated normals on the mesh. Should only be used if there are no authored normals.
215
680
  * @param {} normals
@@ -233,11 +698,21 @@ class HydraMesh {
233
698
  }
234
699
  }
235
700
 
701
+ setTangents(data, dimension, interpolation) {
702
+ if (interpolation === 'facevarying') {
703
+ this._geometry.setAttribute('tangent', new Float32BufferAttribute(data, dimension));
704
+ } else if (interpolation === 'vertex') {
705
+ this._tangents = data.slice(0);
706
+ this.updateOrder(this._tangents, 'tangent', dimension);
707
+ }
708
+ }
709
+
236
710
  // This is always called before prims are updated
237
711
  setMaterial(materialId) {
238
712
  if (debugMaterials) console.log('Setting material on hydra prim: ' + materialId, this._mesh, materialId, this._interface.materials[materialId]);
239
- if (this._interface.materials[materialId]) {
240
- this._mesh.material = this._interface.materials[materialId]._material;
713
+ const hydraMaterial = this._interface.materials[materialId];
714
+ if (hydraMaterial) {
715
+ hydraMaterial.assignToMesh(this._mesh);
241
716
  }
242
717
  else {
243
718
  console.error("Material not found", materialId, this._interface.materials);
@@ -249,14 +724,24 @@ class HydraMesh {
249
724
 
250
725
  for (let i = 0; i < sections.length; i++) {
251
726
  const section = sections[i];
252
- if (this._interface.materials[section.materialId]) {
253
- this._materials.push(this._interface.materials[section.materialId]._material);
727
+ const hydraMaterial = this._interface.materials[section.materialId];
728
+ if (hydraMaterial) {
729
+ this._materials.push(hydraMaterial._material);
254
730
  this._geometry.addGroup(section.start, section.length, i + 1);
255
731
  }
256
732
  }
257
733
 
734
+ if (this._mesh.parent) {
735
+ this._mesh.parent.remove(this._mesh);
736
+ }
258
737
  this._mesh = new Mesh(this._geometry, this._materials);
738
+ this.setVisibilityState(this._visible, this._renderTag);
259
739
  this._interface.config.usdRoot.add(this._mesh);
740
+
741
+ for (let i = 0; i < sections.length; i++) {
742
+ const hydraMaterial = this._interface.materials[sections[i].materialId];
743
+ hydraMaterial?.assignToMesh(this._mesh, i + 1);
744
+ }
260
745
  }
261
746
 
262
747
  setDisplayColor(data, interpolation) {
@@ -340,6 +825,12 @@ class HydraMesh {
340
825
  case "normals":
341
826
  this.setNormals(data, interpolation);
342
827
  break;
828
+ case "tangent":
829
+ case "tangents":
830
+ this.setTangents(data, dimension, interpolation);
831
+ break;
832
+ case "rest":
833
+ break;
343
834
  default:
344
835
  if (warningMessagesToCount.has(name)) {
345
836
  warningMessagesToCount.set(name, warningMessagesToCount.get(name) + 1);
@@ -357,7 +848,9 @@ class HydraMesh {
357
848
  }
358
849
 
359
850
  commit() {
360
- // Nothing to do here. All Three.js resources are already updated during the sync phase.
851
+ if (this._instancedMesh && this._mesh) {
852
+ this._instancedMesh.material = this._mesh.material;
853
+ }
361
854
  }
362
855
 
363
856
  }
@@ -415,6 +908,8 @@ class HydraMaterial {
415
908
  constructor(id, hydraInterface) {
416
909
  this._id = id;
417
910
  this._nodes = {};
911
+ this._resolvedAssetPaths = new Map();
912
+ this._materialXDocuments = [];
418
913
  this._interface = hydraInterface;
419
914
  if (!defaultMaterial) {
420
915
  defaultMaterial = new MeshPhysicalMaterial({
@@ -430,13 +925,126 @@ class HydraMaterial {
430
925
 
431
926
  /** @type {MeshPhysicalMaterial} */
432
927
  this._material = defaultMaterial;
928
+ this._assignments = [];
929
+ this._interface.diagnostics.materialSPrims++;
433
930
 
434
931
  if (debugMaterials) console.log("Hydra Material", this)
435
932
  }
436
933
 
934
+ assignToMesh(mesh, materialIndex = null) {
935
+ this._interface.diagnostics.materialAssignments++;
936
+ const existing = this._assignments.find(assignment => assignment.mesh === mesh && assignment.materialIndex === materialIndex);
937
+ if (!existing) {
938
+ this._assignments.push({ mesh, materialIndex });
939
+ }
940
+ this._applyMaterialToMesh(mesh, materialIndex);
941
+ }
942
+
943
+ unassignMesh(mesh) {
944
+ this._assignments = this._assignments.filter(assignment => assignment.mesh !== mesh);
945
+ }
946
+
947
+ dispose() {
948
+ this._assignments = [];
949
+ disposeMaterialResources(this._material);
950
+ this._material = null;
951
+ }
952
+
953
+ _applyMaterialToMesh(mesh, materialIndex) {
954
+ if (materialIndex === null || materialIndex === undefined) {
955
+ mesh.material = this._material;
956
+ return;
957
+ }
958
+
959
+ if (Array.isArray(mesh.material)) {
960
+ mesh.material[materialIndex] = this._material;
961
+ return;
962
+ }
963
+
964
+ mesh.material = this._material;
965
+ }
966
+
967
+ _applyMaterialToAssignedMeshes() {
968
+ for (const assignment of this._assignments) {
969
+ this._applyMaterialToMesh(assignment.mesh, assignment.materialIndex);
970
+ }
971
+ }
972
+
973
+ beginMaterialSync() {
974
+ this._nodes = {};
975
+ this._resolvedAssetPaths.clear();
976
+ this._materialXDocuments = [];
977
+ }
978
+
979
+ static canonicalAssetPath(path) {
980
+ return String(path ?? "")
981
+ .replace(/\\/g, "/")
982
+ .replace(/^\/+(?=\.?\/)/, "")
983
+ .replace(/^(?:\.\/)+/, "")
984
+ .replace(/\/\.\//g, "/");
985
+ }
986
+
987
+ _rememberResolvedAssetPath(authoredPath, resolvedPath) {
988
+ if (!authoredPath || !resolvedPath) {
989
+ return;
990
+ }
991
+
992
+ const authored = String(authoredPath);
993
+ const canonical = HydraMaterial.canonicalAssetPath(authored);
994
+ for (const key of [
995
+ authored,
996
+ canonical,
997
+ canonical.replace(/^\/+/, ""),
998
+ canonical.replace(/^\/?(?:\.\.\/)+/, ""),
999
+ `./${canonical}`,
1000
+ `/./${canonical}`,
1001
+ ]) {
1002
+ this._resolvedAssetPaths.set(key, String(resolvedPath));
1003
+ }
1004
+ }
1005
+
1006
+ _resolveMaterialTexturePath(authoredPath) {
1007
+ if (!authoredPath) {
1008
+ return "";
1009
+ }
1010
+
1011
+ const authored = String(authoredPath);
1012
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(authored)) {
1013
+ return authored;
1014
+ }
1015
+
1016
+ const candidates = this._interface.registry.materialResourcePathCandidates(authored);
1017
+ for (const candidate of candidates) {
1018
+ const resolved = this._resolvedAssetPaths.get(candidate)
1019
+ || this._resolvedAssetPaths.get(HydraMaterial.canonicalAssetPath(candidate));
1020
+ if (resolved) return resolved;
1021
+ }
1022
+
1023
+ for (const candidate of candidates) {
1024
+ const resolved = this._interface.registry.resolveResourcePath(candidate);
1025
+ if (resolved) return resolved;
1026
+ }
1027
+
1028
+ return "";
1029
+ }
1030
+
437
1031
  updateNode(networkId, path, parameters) {
438
1032
  if (debugTextures) console.log('Updating Material Node: ' + networkId + ' ' + path, parameters);
1033
+ this._interface.diagnostics.materialNodes++;
439
1034
  this._nodes[path] = parameters;
1035
+ this._rememberResolvedAssetPath(parameters?.file, parameters?.resolvedPath);
1036
+ }
1037
+
1038
+ updateMaterialXDocument(document) {
1039
+ if (debugMaterials) console.log('Updating MaterialX document: ' + this._id, document);
1040
+ this._interface.diagnostics.materialXDocuments++;
1041
+ const existingIndex = this._materialXDocuments.findIndex(existing => existing.terminal === document?.terminal);
1042
+ if (existingIndex >= 0) {
1043
+ this._materialXDocuments[existingIndex] = document;
1044
+ }
1045
+ else {
1046
+ this._materialXDocuments.push(document);
1047
+ }
440
1048
  }
441
1049
 
442
1050
  convertWrap(usdWrapMode) {
@@ -468,12 +1076,15 @@ class HydraMaterial {
468
1076
  }
469
1077
  if (mainMaterial[parameterName] && mainMaterial[parameterName].nodeIn) {
470
1078
  const nodeIn = mainMaterial[parameterName].nodeIn;
471
- if (!nodeIn.resolvedPath) {
472
- console.warn("Texture node has no file!", nodeIn);
1079
+ const textureFileName = this._resolveMaterialTexturePath(nodeIn.resolvedPath || nodeIn.file);
1080
+ if (!textureFileName) {
1081
+ if (debugTextures) console.debug("Texture node has no file; skipping optional texture input.", nodeIn);
1082
+ this._material[materialParameterMapName] = undefined;
1083
+ resolve();
1084
+ return;
473
1085
  }
474
1086
  if (debugTextures)
475
- console.log("Assigning texture with resolved path", parameterName, nodeIn.resolvedPath);
476
- const textureFileName = nodeIn.resolvedPath?.replace("./", "");
1087
+ console.log("Assigning texture with resolved path", parameterName, { file: nodeIn.file, resolvedPath: nodeIn.resolvedPath });
477
1088
  const channel = mainMaterial[parameterName].inputName;
478
1089
 
479
1090
  // For debugging
@@ -529,7 +1140,7 @@ class HydraMaterial {
529
1140
  if (materialParameterMapName == 'metalnessMap' && channel != 'b') {
530
1141
  targetSwizzle = '01' + channel + '1';
531
1142
  }
532
- if (materialParameterMapName == 'occlusionMap' && channel != 'r') {
1143
+ if (materialParameterMapName == 'aoMap' && channel != 'r') {
533
1144
  targetSwizzle = channel + '111';
534
1145
  }
535
1146
  if (materialParameterMapName == 'opacityMap' && channel != 'a') {
@@ -703,6 +1314,127 @@ class HydraMaterial {
703
1314
  }
704
1315
  }
705
1316
 
1317
+ static _imageChannelData(image, width, height, channel) {
1318
+ if (!image) return null;
1319
+
1320
+ if ((typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) ||
1321
+ (typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) ||
1322
+ (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap)) {
1323
+
1324
+ const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
1325
+ canvas.width = width;
1326
+ canvas.height = height;
1327
+
1328
+ const context = canvas.getContext('2d');
1329
+ context.drawImage(image, 0, 0, width, height);
1330
+
1331
+ const imageData = context.getImageData(0, 0, width, height);
1332
+ const data = imageData.data;
1333
+ const channelData = new Uint8ClampedArray(width * height);
1334
+ for (let i = 0, j = channel; i < channelData.length; i++, j += 4) {
1335
+ channelData[i] = data[j];
1336
+ }
1337
+ return channelData;
1338
+ }
1339
+
1340
+ return null;
1341
+ }
1342
+
1343
+ static _packMetallicRoughnessMap({ aoMap, roughnessMap, metalnessMap }) {
1344
+ const sourceTexture = roughnessMap || metalnessMap || aoMap;
1345
+ const sourceImage = sourceTexture?.image;
1346
+ const width = sourceImage?.width;
1347
+ const height = sourceImage?.height;
1348
+ if (!sourceTexture || !sourceImage || !width || !height) {
1349
+ return null;
1350
+ }
1351
+
1352
+ const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
1353
+ canvas.width = width;
1354
+ canvas.height = height;
1355
+ const context = canvas.getContext('2d');
1356
+ const imageData = context.createImageData(width, height);
1357
+ const data = imageData.data;
1358
+
1359
+ for (let i = 0; i < data.length; i += 4) {
1360
+ data[i] = 255;
1361
+ data[i + 1] = 255;
1362
+ data[i + 2] = 255;
1363
+ data[i + 3] = 255;
1364
+ }
1365
+
1366
+ const ao = HydraMaterial._imageChannelData(aoMap?.image, width, height, 0);
1367
+ const roughness = HydraMaterial._imageChannelData(roughnessMap?.image, width, height, 1);
1368
+ const metalness = HydraMaterial._imageChannelData(metalnessMap?.image, width, height, 2);
1369
+
1370
+ for (let i = 0, j = 0; i < width * height; i++, j += 4) {
1371
+ if (ao) data[j] = ao[i];
1372
+ if (roughness) data[j + 1] = roughness[i];
1373
+ if (metalness) data[j + 2] = metalness[i];
1374
+ }
1375
+
1376
+ context.putImageData(imageData, 0, 0);
1377
+
1378
+ const packedTexture = sourceTexture.clone();
1379
+ packedTexture.image = canvas;
1380
+ packedTexture.format = RGBAFormat;
1381
+ packedTexture.colorSpace = LinearSRGBColorSpace;
1382
+ packedTexture.name = [
1383
+ "packed-orm",
1384
+ aoMap?.name || "ao:1",
1385
+ roughnessMap?.name || "roughness:1",
1386
+ metalnessMap?.name || "metalness:1",
1387
+ ].join("|");
1388
+ packedTexture.needsUpdate = true;
1389
+ return packedTexture;
1390
+ }
1391
+
1392
+ _packMaterialTextureChannels({ haveOcclusionMap, haveRoughnessMap, haveMetalnessMap }) {
1393
+ if (!haveOcclusionMap && !haveRoughnessMap && !haveMetalnessMap) return;
1394
+
1395
+ const aoMap = this._material.aoMap;
1396
+ const roughnessMap = this._material.roughnessMap;
1397
+ const metalnessMap = this._material.metalnessMap;
1398
+ const existingPackedMap = [aoMap, roughnessMap, metalnessMap]
1399
+ .find(texture => texture?.name?.startsWith?.("packed-orm"));
1400
+ if (existingPackedMap) {
1401
+ if (haveOcclusionMap) this._material.aoMap = existingPackedMap;
1402
+ this._material.roughnessMap = existingPackedMap;
1403
+ this._material.metalnessMap = existingPackedMap;
1404
+ return;
1405
+ }
1406
+
1407
+ if ((haveOcclusionMap && !aoMap) || (haveRoughnessMap && !roughnessMap) || (haveMetalnessMap && !metalnessMap)) {
1408
+ console.error("Something went wrong with the texture promise; a material texture was authored but not loaded.", {
1409
+ haveOcclusionMap,
1410
+ haveRoughnessMap,
1411
+ haveMetalnessMap,
1412
+ aoMap,
1413
+ roughnessMap,
1414
+ metalnessMap,
1415
+ });
1416
+ return;
1417
+ }
1418
+
1419
+ const packedMap = HydraMaterial._packMetallicRoughnessMap({ aoMap, roughnessMap, metalnessMap });
1420
+ if (!packedMap) {
1421
+ console.error("Something went wrong while packing occlusion/metallic/roughness textures.", {
1422
+ aoMap,
1423
+ roughnessMap,
1424
+ metalnessMap,
1425
+ });
1426
+ return;
1427
+ }
1428
+
1429
+ if (haveOcclusionMap) this._material.aoMap = packedMap;
1430
+ this._material.roughnessMap = packedMap;
1431
+ this._material.metalnessMap = packedMap;
1432
+
1433
+ for (const texture of new Set([aoMap, roughnessMap, metalnessMap])) {
1434
+ if (texture && texture !== packedMap) texture.dispose();
1435
+ }
1436
+ }
1437
+
706
1438
  assignProperty(mainMaterial, parameterName) {
707
1439
  const materialParameterName = HydraMaterial.usdPreviewToMeshPhysicalMap[parameterName];
708
1440
  if (materialParameterName === undefined) {
@@ -726,16 +1458,39 @@ class HydraMaterial {
726
1458
  }
727
1459
  }
728
1460
 
729
- async updateFinished(type, relationships) {
1461
+ updateFinished(type, relationships) {
1462
+ this._interface.diagnostics.materialUpdateFinished++;
1463
+ const promise = this._updateFinished(type, relationships);
1464
+ this._interface.trackMaterialUpdate(promise);
1465
+ }
1466
+
1467
+ async _updateFinished(type, relationships) {
730
1468
  for (let relationship of relationships) {
731
1469
  relationship.nodeIn = this._nodes[relationship.inputId];
732
1470
  relationship.nodeOut = this._nodes[relationship.outputId];
1471
+ if (!relationship.nodeIn || !relationship.nodeOut) {
1472
+ console.warn('Material relationship references an unknown node.', relationship);
1473
+ continue;
1474
+ }
733
1475
  relationship.nodeIn[relationship.inputName] = relationship;
734
1476
  relationship.nodeOut[relationship.outputName] = relationship;
735
1477
  }
736
1478
  if (debugMaterials) console.log('Finalizing Material: ' + this._id);
737
1479
  if (debugMaterials) console.log("updateFinished", type, relationships)
738
1480
 
1481
+ const materialXDocument = this._materialXDocuments.find(document => document.terminal === type);
1482
+ if (materialXDocument) {
1483
+ const materialXMaterial = await this.createMaterialXMaterial(materialXDocument);
1484
+ if (!materialXMaterial) {
1485
+ this._material = defaultMaterial;
1486
+ this._applyMaterialToAssignedMeshes();
1487
+ return;
1488
+ }
1489
+ this._material = materialXMaterial;
1490
+ this._applyMaterialToAssignedMeshes();
1491
+ return;
1492
+ }
1493
+
739
1494
  // find the main material node
740
1495
  let mainMaterialNode = undefined;
741
1496
  for (let node of Object.values(this._nodes)) {
@@ -747,6 +1502,7 @@ class HydraMaterial {
747
1502
 
748
1503
  if (!mainMaterialNode || disableMaterials) {
749
1504
  this._material = defaultMaterial;
1505
+ this._applyMaterialToAssignedMeshes();
750
1506
  return;
751
1507
  }
752
1508
 
@@ -783,21 +1539,7 @@ class HydraMaterial {
783
1539
  }
784
1540
  await Promise.all(texturePromises);
785
1541
 
786
- // Need to sanitize metallic/roughness/occlusion maps - if we want to export glTF they need to be identical right now
787
- if (haveRoughnessMap && !haveMetalnessMap) {
788
- if (debugMaterials) console.log(this._material.roughnessMap, this._material);
789
- this._material.metalnessMap = this._material.roughnessMap;
790
- if (this._material.metalnessMap) this._material.metalnessMap.needsUpdate = true;
791
- else console.error("Something went wrong with the texture promise; haveRoughnessMap is true but no roughnessMap was loaded.");
792
- }
793
- else if (haveMetalnessMap && !haveRoughnessMap) {
794
- this._material.roughnessMap = this._material.metalnessMap;
795
- if (this._material.roughnessMap) this._material.roughnessMap.needsUpdate = true;
796
- else console.error("Something went wrong with the texture promise; haveMetalnessMap is true but no metalnessMap was loaded.");
797
- }
798
- else if (haveMetalnessMap && haveRoughnessMap) {
799
- console.warn("TODO: [Three USD] separate metalness and roughness textures need to be merged");
800
- }
1542
+ this._packMaterialTextureChannels({ haveOcclusionMap, haveRoughnessMap, haveMetalnessMap });
801
1543
  }
802
1544
 
803
1545
  // Assign material properties
@@ -806,6 +1548,60 @@ class HydraMaterial {
806
1548
  }
807
1549
 
808
1550
  if (debugMaterials) console.log("Material Node \"" + this._material.name + "\"", mainMaterialNode, "Resulting Material", this._material);
1551
+ this._applyMaterialToAssignedMeshes();
1552
+ }
1553
+
1554
+ async createMaterialXMaterial(materialXDocument) {
1555
+ if (!materialXDocument || disableMaterials) {
1556
+ if (!materialXDocument) {
1557
+ this._interface.diagnostics.materialXSkippedNoDocuments++;
1558
+ }
1559
+ return null;
1560
+ }
1561
+ this._interface.diagnostics.materialXCreateAttempts++;
1562
+
1563
+ const xml = materialXDocument?.xml;
1564
+ if (!xml) {
1565
+ return null;
1566
+ }
1567
+
1568
+ try {
1569
+ const { MaterialX, MaterialXMaterial } = await getMaterialXModule();
1570
+ const materialName = this._id.split('/').pop() || this._id;
1571
+ const materialNodeNameOrIndex = materialXDocument.materialName || materialName || 0;
1572
+ const material = await MaterialX.createMaterialXMaterial(xml, materialNodeNameOrIndex, {
1573
+ cacheKey: `${this._id}:${materialXDocument.terminal}`,
1574
+ getTexture: async (url) => {
1575
+ const texturePath = this._resolveMaterialTexturePath(url);
1576
+ return texturePath ? this._interface.registry.getTexture(texturePath) : null;
1577
+ },
1578
+ }, {
1579
+ parameters: {
1580
+ side: DoubleSide,
1581
+ },
1582
+ }, {});
1583
+
1584
+ if (typeof MaterialXMaterial === 'function' && !(material instanceof MaterialXMaterial)) {
1585
+ this._interface.diagnostics.materialXCreateFailures++;
1586
+ if (debugMaterials) console.debug('MaterialX shader generation returned a non-MaterialX material.', {
1587
+ materialId: this._id,
1588
+ materialName: material?.name,
1589
+ materialType: material?.constructor?.name,
1590
+ });
1591
+ return null;
1592
+ }
1593
+
1594
+ material.name = materialName;
1595
+ material.side = DoubleSide;
1596
+ material.needsUpdate = true;
1597
+ this._interface.diagnostics.materialXCreateSuccess++;
1598
+ return material;
1599
+ }
1600
+ catch (error) {
1601
+ this._interface.diagnostics.materialXCreateFailures++;
1602
+ console.warn('Failed to create MaterialX material.', error);
1603
+ return null;
1604
+ }
809
1605
  }
810
1606
  }
811
1607
 
@@ -828,6 +1624,37 @@ export class ThreeRenderDelegateInterface {
828
1624
  this.registry = new TextureRegistry(config);
829
1625
  this.materials = {};
830
1626
  this.meshes = {};
1627
+ this.scenePrimitives = {};
1628
+ this.pendingMaterialUpdates = new Set();
1629
+ this.diagnostics = {
1630
+ materialSPrims: 0,
1631
+ sceneSPrims: 0,
1632
+ materialAssignments: 0,
1633
+ materialNodes: 0,
1634
+ materialUpdateFinished: 0,
1635
+ materialXDocuments: 0,
1636
+ materialXSkippedNoDocuments: 0,
1637
+ materialXCreateAttempts: 0,
1638
+ materialXCreateSuccess: 0,
1639
+ materialXCreateFailures: 0,
1640
+ materialIds: [],
1641
+ scenePrimitiveIds: [],
1642
+ };
1643
+ }
1644
+
1645
+ trackMaterialUpdate(promise) {
1646
+ this.pendingMaterialUpdates.add(promise);
1647
+ promise.finally(() => this.pendingMaterialUpdates.delete(promise));
1648
+ }
1649
+
1650
+ async waitForMaterialsReady() {
1651
+ while (this.pendingMaterialUpdates.size > 0) {
1652
+ await Promise.allSettled([...this.pendingMaterialUpdates]);
1653
+ }
1654
+ }
1655
+
1656
+ getDiagnostics() {
1657
+ return { ...this.diagnostics };
831
1658
  }
832
1659
 
833
1660
  /**
@@ -839,11 +1666,20 @@ export class ThreeRenderDelegateInterface {
839
1666
  */
840
1667
  createRPrim(typeId, id, instancerId) {
841
1668
  if (debugPrims) console.log('Creating RPrim: ', typeId, id, typeof id);
1669
+ this.destroyRPrim(id);
842
1670
  let mesh = new HydraMesh(id, this);
843
1671
  this.meshes[id] = mesh;
844
1672
  return mesh;
845
1673
  }
846
1674
 
1675
+ destroyRPrim(id) {
1676
+ if (debugPrims) console.log('Destroying RPrim: ', id);
1677
+ const mesh = this.meshes[id];
1678
+ if (!mesh) return;
1679
+ mesh.dispose();
1680
+ delete this.meshes[id];
1681
+ }
1682
+
847
1683
  createBPrim(typeId, id) {
848
1684
  if (debugPrims) console.log('Creating BPrim: ', typeId, id);
849
1685
  /*let mesh = new HydraMesh(id, this);
@@ -855,14 +1691,64 @@ export class ThreeRenderDelegateInterface {
855
1691
  if (debugPrims) console.log('Creating SPrim: ', typeId, id);
856
1692
 
857
1693
  if (typeId === 'material') {
1694
+ this.destroySPrim(id);
858
1695
  let material = new HydraMaterial(id, this);
859
1696
  this.materials[id] = material;
1697
+ if (this.diagnostics.materialIds.length < 20) {
1698
+ this.diagnostics.materialIds.push(id);
1699
+ }
860
1700
  return material;
1701
+ } else if (typeId === 'camera' || lightSprimTypeIds.has(typeId)) {
1702
+ const scenePrimitive = new HydraScenePrimitive(typeId, id, this);
1703
+ if (!id) {
1704
+ return scenePrimitive;
1705
+ }
1706
+ this.destroySPrim(id);
1707
+ this.scenePrimitives[id] = scenePrimitive;
1708
+ this.diagnostics.sceneSPrims++;
1709
+ if (this.diagnostics.scenePrimitiveIds.length < 20) {
1710
+ this.diagnostics.scenePrimitiveIds.push(id);
1711
+ }
1712
+ return scenePrimitive;
861
1713
  } else {
862
1714
  return undefined;
863
1715
  }
864
1716
  }
865
1717
 
1718
+ destroySPrim(id) {
1719
+ if (debugPrims) console.log('Destroying SPrim: ', id);
1720
+ const material = this.materials[id];
1721
+ if (material) {
1722
+ material.dispose();
1723
+ delete this.materials[id];
1724
+ }
1725
+ const scenePrimitive = this.scenePrimitives[id];
1726
+ if (scenePrimitive) {
1727
+ scenePrimitive.dispose();
1728
+ delete this.scenePrimitives[id];
1729
+ }
1730
+ }
1731
+
1732
+ dispose() {
1733
+ for (const id of Object.keys(this.meshes)) {
1734
+ this.destroyRPrim(id);
1735
+ }
1736
+ for (const id of Object.keys(this.materials)) {
1737
+ this.destroySPrim(id);
1738
+ }
1739
+ for (const id of Object.keys(this.scenePrimitives)) {
1740
+ this.destroySPrim(id);
1741
+ }
1742
+ this.registry.dispose();
1743
+ this.pendingMaterialUpdates.clear();
1744
+ }
1745
+
1746
+ unassignMeshFromMaterials(mesh) {
1747
+ for (const material of Object.values(this.materials)) {
1748
+ material.unassignMesh(mesh);
1749
+ }
1750
+ }
1751
+
866
1752
  CommitResources() {
867
1753
  for (const id in this.meshes) {
868
1754
  const hydraMesh = this.meshes[id]