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

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 (42) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +245 -28
  3. package/package.json +46 -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 +368 -53
  9. package/src/hydra/ThreeJsRenderDelegate.js +1128 -75
  10. package/src/plugins/index.js +1 -2
  11. package/src/plugins/needle.js +38 -2
  12. package/src/types/bindings.d.ts +296 -3
  13. package/src/types/create.three.d.ts +87 -7
  14. package/src/types/hydra.d.ts +7 -5
  15. package/src/types/plugins.d.ts +7 -0
  16. package/src/types/usd-core-bindings.d.ts +240 -0
  17. package/src/utils.js +3 -3
  18. package/src/vite/index.js +13 -1
  19. package/examples/index.html +0 -58
  20. package/examples/package-lock.json +0 -1548
  21. package/examples/package.json +0 -24
  22. package/examples/public/HttpReferences copy.usda +0 -46
  23. package/examples/public/HttpReferences.usda +0 -44
  24. package/examples/public/gingerbread/GingerbreadHouse.usda +0 -35
  25. package/examples/public/gingerbread/house/GingerBreadHouse.usdc +0 -0
  26. package/examples/public/gingerbread/house/textures/color.jpg +0 -0
  27. package/examples/public/gingerbread/house/textures/metallic_roughness.jpg +0 -0
  28. package/examples/public/gingerbread/house/textures/normal.jpg +0 -0
  29. package/examples/public/gingerbread/snowman/Snowman.usdc +0 -0
  30. package/examples/public/gingerbread/snowman/textures/color.jpg +0 -0
  31. package/examples/public/gingerbread/snowman/textures/metallic_roughness.jpg +0 -0
  32. package/examples/public/gingerbread/snowman/textures/normal.jpg +0 -0
  33. package/examples/public/test.usdz +0 -0
  34. package/examples/public/vite.svg +0 -1
  35. package/examples/src/fileHandling.ts +0 -256
  36. package/examples/src/main.ts +0 -167
  37. package/examples/src/three.ts +0 -140
  38. package/examples/src/vite-env.d.ts +0 -1
  39. package/examples/tsconfig.json +0 -23
  40. package/examples/vite.config.js +0 -21
  41. package/src/bindings/emHdBindings.data +0 -19331
  42. package/src/bindings/emHdBindings.worker.js +0 -124
@@ -1,7 +1,35 @@
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
+ FrontSide,
10
+ BackSide,
11
+ DoubleSide,
12
+ Color,
13
+ Mesh,
14
+ InstancedMesh,
15
+ Matrix4,
16
+ Float32BufferAttribute,
17
+ SRGBColorSpace,
18
+ RGBAFormat,
19
+ RepeatWrapping,
20
+ LinearSRGBColorSpace,
21
+ Vector2,
22
+ CameraHelper,
23
+ DirectionalLight,
24
+ DirectionalLightHelper,
25
+ HemisphereLight,
26
+ OrthographicCamera,
27
+ PerspectiveCamera,
28
+ PointLight,
29
+ PointLightHelper,
30
+ MathUtils,
31
+ } = THREE;
32
+
5
33
  const debugTextures = false;
6
34
  const debugMaterials = false;
7
35
  const debugMeshes = false;
@@ -9,6 +37,113 @@ const debugPrims = false;
9
37
  const disableTextures = false;
10
38
  const disableMaterials = false;
11
39
 
40
+ let materialXModulePromise = null;
41
+
42
+ async function getMaterialXModule() {
43
+ materialXModulePromise ??= import('@needle-tools/materialx').then(module => ({
44
+ MaterialX: module.Experimental_API,
45
+ MaterialXMaterial: module.MaterialXMaterial,
46
+ }));
47
+ return materialXModulePromise;
48
+ }
49
+
50
+ function disposeTexture(texture, disposed = new Set()) {
51
+ if (!texture || disposed.has(texture) || typeof texture.dispose !== 'function') return;
52
+ disposed.add(texture);
53
+ texture.dispose();
54
+ }
55
+
56
+ function disposeMaterialResources(material, disposedMaterials = new Set(), disposedTextures = new Set()) {
57
+ if (!material) return;
58
+ if (Array.isArray(material)) {
59
+ for (const entry of material) disposeMaterialResources(entry, disposedMaterials, disposedTextures);
60
+ return;
61
+ }
62
+ if (disposedMaterials.has(material)) return;
63
+ disposedMaterials.add(material);
64
+
65
+ for (const value of Object.values(material)) {
66
+ if (value && typeof value === 'object' && value.isTexture) {
67
+ disposeTexture(value, disposedTextures);
68
+ }
69
+ }
70
+
71
+ if (material !== defaultMaterial && typeof material.dispose === 'function') {
72
+ material.dispose();
73
+ }
74
+ }
75
+
76
+ function disposeObjectResources(object) {
77
+ if (!object) return;
78
+ object.traverse?.((entry) => {
79
+ entry.geometry?.dispose?.();
80
+ disposeMaterialResources(entry.material);
81
+ });
82
+ object.parent?.remove(object);
83
+ }
84
+
85
+ function isFiniteArray(values, dimension = 3) {
86
+ if (!values || values.length === 0 || values.length % dimension !== 0) return false;
87
+ for (let i = 0; i < values.length; i++) {
88
+ if (!Number.isFinite(values[i])) return false;
89
+ }
90
+ return true;
91
+ }
92
+
93
+ function cullStyleToThreeSide(doubleSided, cullStyle) {
94
+ switch (cullStyle) {
95
+ case "nothing":
96
+ return DoubleSide;
97
+ case "back":
98
+ return FrontSide;
99
+ case "front":
100
+ return BackSide;
101
+ case "frontUnlessDoubleSided":
102
+ return doubleSided ? DoubleSide : BackSide;
103
+ case "backUnlessDoubleSided":
104
+ case "dontCare":
105
+ default:
106
+ return doubleSided ? DoubleSide : FrontSide;
107
+ }
108
+ }
109
+
110
+ function primNameFromPath(id, fallback) {
111
+ const path = String(id || "");
112
+ const slash = path.lastIndexOf("/");
113
+ return slash >= 0 ? path.substring(slash + 1) || fallback : path || fallback;
114
+ }
115
+
116
+ function applyHydraTransform(object, matrix) {
117
+ if (!object || !matrix || matrix.length < 16) return;
118
+ object.matrix.set(...Array.from(matrix).slice(0, 16));
119
+ object.matrix.transpose();
120
+ object.matrix.decompose(object.position, object.quaternion, object.scale);
121
+ object.matrixAutoUpdate = true;
122
+ }
123
+
124
+ const defaultScenePrimitiveLightIntensityScale = 0.01;
125
+ const lightSprimTypeIds = new Set([
126
+ "domeLight",
127
+ "cylinderLight",
128
+ "diskLight",
129
+ "distantLight",
130
+ "light",
131
+ "rectLight",
132
+ "simpleLight",
133
+ "sphereLight",
134
+ ]);
135
+
136
+ const usdLightTypeNames = {
137
+ domeLight: "DomeLight",
138
+ cylinderLight: "CylinderLight",
139
+ diskLight: "DiskLight",
140
+ distantLight: "DistantLight",
141
+ light: "LightAPI",
142
+ rectLight: "RectLight",
143
+ simpleLight: "SimpleLight",
144
+ sphereLight: "SphereLight",
145
+ };
146
+
12
147
  class TextureRegistry {
13
148
  /**
14
149
  * @param {import('..').threeJsRenderDelegateConfig} config
@@ -17,6 +152,9 @@ class TextureRegistry {
17
152
  this.config = config;
18
153
  this.allPaths = config.paths;
19
154
  this.textures = [];
155
+ this.loadedTextures = new Set();
156
+ this.objectUrls = new Set();
157
+ this.disposed = false;
20
158
  this.loader = new TextureLoader();
21
159
  this.tgaLoader = new TGALoader();
22
160
  this.exrLoader = new EXRLoader();
@@ -32,7 +170,97 @@ class TextureRegistry {
32
170
  }
33
171
  }
34
172
 
173
+ normalizeResourcePath(resourcePath) {
174
+ const rawPath = String(resourcePath ?? "").replace(/\\/g, "/");
175
+ const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(rawPath);
176
+ if (isUrl) {
177
+ return rawPath;
178
+ }
179
+
180
+ const preserveLeadingSlash = rawPath.startsWith("/");
181
+ const path = rawPath
182
+ .replace(/^\/+/, preserveLeadingSlash ? "/" : "")
183
+ .replace(/^(?:\.\/)+/, "")
184
+ .replace(/\/\.\//g, "/");
185
+ const parts = [];
186
+ for (const part of path.split("/")) {
187
+ if (!part || part === ".") continue;
188
+ if (part === "..") {
189
+ parts.pop();
190
+ continue;
191
+ }
192
+ parts.push(part);
193
+ }
194
+ return (preserveLeadingSlash ? "/" : "") + parts.join("/");
195
+ }
196
+
197
+ materialResourcePathCandidates(resourcePath) {
198
+ const rawPath = String(resourcePath ?? "").replace(/\\/g, "/");
199
+ const normalized = this.normalizeResourcePath(rawPath);
200
+ const withoutLeadingSlash = normalized.replace(/^\/+/, "");
201
+ const candidates = new Set([
202
+ rawPath,
203
+ normalized,
204
+ withoutLeadingSlash,
205
+ withoutLeadingSlash.replace(/^(?:\.\/)+/, ""),
206
+ withoutLeadingSlash.replace(/^(?:\.\.\/)+/, ""),
207
+ ]);
208
+
209
+ const pathParts = withoutLeadingSlash.split("/").filter(Boolean);
210
+ const texturesIndex = pathParts.lastIndexOf("textures");
211
+ if (texturesIndex >= 0) {
212
+ candidates.add(pathParts.slice(texturesIndex).join("/"));
213
+ }
214
+
215
+ return [...candidates].filter(Boolean);
216
+ }
217
+
218
+ getResourceExtension(resourcePath) {
219
+ const path = String(resourcePath ?? "").toLowerCase();
220
+ const extensionMatches = [...path.matchAll(/\.([a-z0-9]+)(?=\]|$|[?#])/g)];
221
+ return extensionMatches.length ? extensionMatches[extensionMatches.length - 1][1] : "";
222
+ }
223
+
224
+ readResolvedResource(resourcePath) {
225
+ if (!resourcePath?.startsWith("/") || typeof this.config.USD?.ReadFile !== "function") {
226
+ return null;
227
+ }
228
+
229
+ try {
230
+ const file = this.config.USD.ReadFile(resourcePath);
231
+ return file?.byteLength ? file : null;
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
237
+
238
+ resolveResourcePath(resourcePath) {
239
+ const candidates = this.materialResourcePathCandidates(resourcePath);
240
+ if (!candidates.length) return "";
241
+
242
+ const knownPaths = Array.isArray(this.allPaths) ? this.allPaths : [];
243
+ for (const candidate of candidates) {
244
+ const candidateWithoutRoot = candidate.replace(/^needle\//, "");
245
+ for (const knownPath of knownPaths) {
246
+ const known = this.normalizeResourcePath(knownPath);
247
+ const knownWithoutRoot = known.replace(/^needle\//, "");
248
+ if (
249
+ known === candidate ||
250
+ knownWithoutRoot === candidate ||
251
+ knownWithoutRoot === candidateWithoutRoot ||
252
+ knownWithoutRoot.endsWith("/" + candidateWithoutRoot)
253
+ ) {
254
+ return known;
255
+ }
256
+ }
257
+ }
258
+
259
+ return candidates[0];
260
+ }
261
+
35
262
  getTexture(resourcePath) {
263
+ resourcePath = this.resolveResourcePath(resourcePath);
36
264
  if (debugTextures) console.log("get texture", resourcePath);
37
265
  if (this.textures[resourcePath]) {
38
266
  return this.textures[resourcePath];
@@ -49,18 +277,16 @@ class TextureRegistry {
49
277
  }
50
278
 
51
279
  let filetype = undefined;
52
- let lowercaseFilename = resourcePath.toLowerCase();
53
- if (lowercaseFilename.indexOf('.png') >= lowercaseFilename.length - 5) {
280
+ const extension = this.getResourceExtension(resourcePath);
281
+ if (extension === 'png') {
54
282
  filetype = 'image/png';
55
- } else if (lowercaseFilename.indexOf('.jpg') >= lowercaseFilename.length - 5) {
283
+ } else if (extension === 'jpg') {
56
284
  filetype = 'image/jpeg';
57
- } else if (lowercaseFilename.indexOf('.jpeg') >= lowercaseFilename.length - 5) {
285
+ } else if (extension === 'jpeg') {
58
286
  filetype = 'image/jpeg';
59
- } else if (lowercaseFilename.indexOf('.exr') >= lowercaseFilename.length - 4) {
60
- console.warn("EXR textures are not fully supported yet", resourcePath);
61
- // using EXRLoader explicitly
287
+ } else if (extension === 'exr') {
62
288
  filetype = 'image/x-exr';
63
- } else if (lowercaseFilename.indexOf('.tga') >= lowercaseFilename.length - 4) {
289
+ } else if (extension === 'tga') {
64
290
  console.warn("TGA textures are not fully supported yet", resourcePath);
65
291
  // using TGALoader explicitly
66
292
  filetype = 'image/tga';
@@ -69,48 +295,69 @@ class TextureRegistry {
69
295
  // throw new Error('Unknown filetype');
70
296
  }
71
297
 
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
- );
298
+ let loader = this.loader;
299
+ if (filetype === 'image/tga')
300
+ loader = this.tgaLoader;
301
+ else if (filetype === 'image/x-exr')
302
+ loader = this.exrLoader;
303
+
304
+ const baseUrl = this.baseUrl;
305
+ const loadFromFile = (_loadedFile) => {
306
+ let url = undefined;
307
+ if (debugTextures) console.log("window.driver.getFile", resourcePath, " => ", _loadedFile);
308
+ if (_loadedFile) {
309
+ let blob = new Blob([_loadedFile.slice(0)], { type: filetype });
310
+ url = URL.createObjectURL(blob);
311
+ this.objectUrls.add(url);
312
+ } else {
313
+ if (baseUrl)
314
+ url = baseUrl + '/' + resourcePath;
315
+ else
316
+ url = resourcePath;
112
317
  }
318
+ if (debugTextures) console.log("Loading texture from", url, "with loader", loader, "_loadedFile", _loadedFile, "baseUrl", baseUrl, "resourcePath", resourcePath);
319
+ // Load the texture
320
+ loader.load(
321
+ // resource URL
322
+ url,
323
+
324
+ // onLoad callback
325
+ (texture) => {
326
+ if (url?.startsWith('blob:')) {
327
+ URL.revokeObjectURL(url);
328
+ this.objectUrls.delete(url);
329
+ }
330
+ texture.name = resourcePath;
331
+ if (this.disposed) {
332
+ texture.dispose();
333
+ }
334
+ else {
335
+ this.loadedTextures.add(texture);
336
+ }
337
+ textureResolve(texture);
338
+ },
339
+
340
+ // onProgress callback currently not used
341
+ undefined,
113
342
 
343
+ // onError callback
344
+ (err) => {
345
+ if (url?.startsWith('blob:')) {
346
+ URL.revokeObjectURL(url);
347
+ this.objectUrls.delete(url);
348
+ }
349
+ textureReject(err);
350
+ }
351
+ );
352
+ };
353
+
354
+ const resolvedFile = this.readResolvedResource(resourcePath);
355
+ if (resolvedFile) {
356
+ loadFromFile(resolvedFile);
357
+ return this.textures[resourcePath];
358
+ }
359
+
360
+ this.config.driver().getFile(resourcePath, async (loadedFile) => {
114
361
  if (!loadedFile) {
115
362
  // if the file is not part of the filesystem, we can still try to fetch it from the network
116
363
  if (baseUrl) {
@@ -127,6 +374,166 @@ class TextureRegistry {
127
374
 
128
375
  return this.textures[resourcePath];
129
376
  }
377
+
378
+ dispose() {
379
+ this.disposed = true;
380
+ for (const url of this.objectUrls) {
381
+ URL.revokeObjectURL(url);
382
+ }
383
+ this.objectUrls.clear();
384
+
385
+ for (const texture of this.loadedTextures) {
386
+ texture.dispose();
387
+ }
388
+ this.loadedTextures.clear();
389
+
390
+ for (const texturePromise of Object.values(this.textures)) {
391
+ Promise.resolve(texturePromise).then(texture => {
392
+ if (texture?.dispose) texture.dispose();
393
+ }).catch(() => {});
394
+ }
395
+ this.textures = [];
396
+ }
397
+ }
398
+
399
+ class HydraScenePrimitive {
400
+ /**
401
+ * @param {string} typeId
402
+ * @param {string} id
403
+ * @param {ThreeRenderDelegateInterface} hydraInterface
404
+ */
405
+ constructor(typeId, id, hydraInterface) {
406
+ this._typeId = typeId;
407
+ this._id = id;
408
+ this._interface = hydraInterface;
409
+ this._object = null;
410
+ this._helper = null;
411
+ }
412
+
413
+ updateCameraState(state) {
414
+ if (!this._id) return;
415
+ const projection = state?.projection || "perspective";
416
+ const isOrthographic = projection === "orthographic";
417
+ if (!this._object || (isOrthographic && !this._object.isOrthographicCamera) || (!isOrthographic && !this._object.isPerspectiveCamera)) {
418
+ this._replaceObject(isOrthographic
419
+ ? new OrthographicCamera(-1, 1, 1, -1, Number(state?.near) || 0.01, Number(state?.far) || 100000)
420
+ : new PerspectiveCamera(45, 1, Number(state?.near) || 0.01, Number(state?.far) || 100000));
421
+ this._object.userData.usdKind = "sprim";
422
+ }
423
+
424
+ const focalLength = Number(state?.focalLength) || 50;
425
+ const verticalAperture = Number(state?.verticalAperture) || 20.955;
426
+ const horizontalAperture = Number(state?.horizontalAperture) || verticalAperture;
427
+ const near = Number(state?.near);
428
+ const far = Number(state?.far);
429
+ if (this._object.isPerspectiveCamera) {
430
+ this._object.fov = MathUtils.radToDeg(2 * Math.atan((verticalAperture * 0.5) / focalLength));
431
+ this._object.aspect = horizontalAperture > 0 && verticalAperture > 0 ? horizontalAperture / verticalAperture : 1;
432
+ }
433
+ if (Number.isFinite(near) && near > 0) this._object.near = near;
434
+ if (Number.isFinite(far) && far > 0) this._object.far = far;
435
+ this._object.name = primNameFromPath(this._id, "UsdCamera");
436
+ this._object.userData.usdPath = this._id;
437
+ this._object.userData.usdTypeName = "Camera";
438
+ applyHydraTransform(this._object, state?.transform);
439
+ this._object.updateProjectionMatrix?.();
440
+ this._syncHelper();
441
+ }
442
+
443
+ updateLightState(state) {
444
+ if (!this._id) return;
445
+ const typeId = String(state?.typeId || this._typeId);
446
+ const LightCtor = typeId === "distantLight"
447
+ ? DirectionalLight
448
+ : typeId === "domeLight"
449
+ ? HemisphereLight
450
+ : PointLight;
451
+
452
+ if (!this._object || !(this._object instanceof LightCtor)) {
453
+ const color = new Color(1, 1, 1);
454
+ this._replaceObject(typeId === "domeLight"
455
+ ? new HemisphereLight(color, new Color(0.2, 0.2, 0.2), 1)
456
+ : new LightCtor(color, 1));
457
+ this._object.userData.usdKind = "sprim";
458
+ }
459
+
460
+ const colorValue = Array.isArray(state?.color) ? state.color : [1, 1, 1];
461
+ this._object.color?.setRGB?.(
462
+ Number(colorValue[0]) || 0,
463
+ Number(colorValue[1]) || 0,
464
+ Number(colorValue[2]) || 0);
465
+ const scale = this._interface.config.scenePrimitiveLightIntensityScale ?? defaultScenePrimitiveLightIntensityScale;
466
+ this._object.intensity = (Number(state?.intensity) || 0) * scale;
467
+ this._object.visible = state?.visible !== false;
468
+ this._object.name = primNameFromPath(this._id, usdLightTypeNames[typeId] || "UsdLight");
469
+ this._object.userData.usdPath = this._id;
470
+ this._object.userData.usdTypeName = usdLightTypeNames[typeId] || typeId;
471
+ applyHydraTransform(this._object, state?.transform);
472
+ this._syncHelper(state);
473
+ }
474
+
475
+ dispose() {
476
+ disposeObjectResources(this._helper);
477
+ disposeObjectResources(this._object);
478
+ this._helper = null;
479
+ this._object = null;
480
+ }
481
+
482
+ _replaceObject(object) {
483
+ disposeObjectResources(this._helper);
484
+ disposeObjectResources(this._object);
485
+ this._helper = null;
486
+ this._object = object;
487
+ this._interface.config.scenePrimitiveRoot?.add(object);
488
+ }
489
+
490
+ _syncHelper(state = {}) {
491
+ const object = this._object;
492
+ if (!object) return;
493
+ const wantHelper = object.isCamera
494
+ ? (this._interface.config.showCameraHelpers || this._interface.config.showScenePrimitiveHelpers)
495
+ : object.isLight
496
+ ? (this._interface.config.showLightHelpers || this._interface.config.showScenePrimitiveHelpers)
497
+ : false;
498
+ if (!wantHelper) {
499
+ disposeObjectResources(this._helper);
500
+ this._helper = null;
501
+ return;
502
+ }
503
+
504
+ const helperCtor = object.isCamera
505
+ ? CameraHelper
506
+ : object.isDirectionalLight
507
+ ? DirectionalLightHelper
508
+ : object.isPointLight
509
+ ? PointLightHelper
510
+ : null;
511
+ if (!helperCtor) {
512
+ disposeObjectResources(this._helper);
513
+ this._helper = null;
514
+ return;
515
+ }
516
+
517
+ const needsNewHelper = !this._helper ||
518
+ (object.isCamera && !this._helper.isCameraHelper) ||
519
+ (object.isDirectionalLight && this._helper.type !== "DirectionalLightHelper") ||
520
+ (object.isPointLight && this._helper.type !== "PointLightHelper");
521
+ if (needsNewHelper) {
522
+ disposeObjectResources(this._helper);
523
+ const color = object.color || new Color(1, 1, 1);
524
+ this._helper = object.isCamera
525
+ ? new CameraHelper(object)
526
+ : object.isDirectionalLight
527
+ ? new DirectionalLightHelper(object, 0.5, color)
528
+ : new PointLightHelper(object, Number(state?.radius) || 0.25, color);
529
+ this._helper.name = `${object.name}Helper`;
530
+ this._helper.userData.usdHelperFor = this._id;
531
+ this._interface.config.scenePrimitiveRoot?.add(this._helper);
532
+ }
533
+ this._helper.name = `${object.name}Helper`;
534
+ this._helper.visible = object.visible;
535
+ this._helper.update?.();
536
+ }
130
537
  }
131
538
 
132
539
  class HydraMesh {
@@ -140,18 +547,28 @@ class HydraMesh {
140
547
  this._interface = hydraInterface;
141
548
  this._points = undefined;
142
549
  this._normals = undefined;
550
+ this._tangents = undefined;
143
551
  this._colors = undefined;
144
552
  this._uvs = undefined;
145
553
  this._indices = undefined;
146
554
  this._materials = [];
555
+ this._materialSideClones = new Map();
556
+ this._side = DoubleSide;
557
+ this._visible = false;
558
+ this._renderTag = 'geometry';
559
+ this._instancedMesh = null;
560
+ this._instanceMatrix = new Matrix4();
147
561
 
148
562
  let material = new MeshPhysicalMaterial({
149
563
  side: DoubleSide,
150
564
  color: new Color(0xB4B4B4),
151
565
  // envMap: hydraInterface.config.envMap,
152
566
  });
567
+ this._ownedMaterial = material;
153
568
  this._materials.push(material);
154
569
  this._mesh = new Mesh(this._geometry, material);
570
+ this._installMeshHooks(this._mesh);
571
+ this._mesh.visible = false;
155
572
  this._mesh.castShadow = true;
156
573
  this._mesh.receiveShadow = true;
157
574
 
@@ -168,17 +585,47 @@ class HydraMesh {
168
585
  hydraInterface.config.usdRoot.add(this._mesh); // FIXME
169
586
  }
170
587
 
588
+ dispose() {
589
+ if (!this._mesh) return;
590
+ this._interface.unassignMeshFromMaterials(this._mesh);
591
+ this._disposeInstancedMesh();
592
+ this._disposeMaterialSideClones();
593
+ if (this._mesh.parent) {
594
+ this._mesh.parent.remove(this._mesh);
595
+ }
596
+ this._geometry.dispose();
597
+ disposeMaterialResources(this._ownedMaterial);
598
+ this._ownedMaterial = null;
599
+ this._mesh = null;
600
+ }
601
+
171
602
  updateOrder(attribute, attributeName, dimension = 3) {
172
603
  if (debugMeshes) console.log("updateOrder", attribute, attributeName, dimension);
173
604
  if (attribute && this._indices) {
605
+ if (!isFiniteArray(attribute, dimension)) {
606
+ this._geometry.deleteAttribute(attributeName);
607
+ return;
608
+ }
174
609
  let values = [];
175
610
  for (let i = 0; i < this._indices.length; i++) {
176
611
  let index = this._indices[i]
612
+ if (!Number.isInteger(index) || index < 0 || (dimension * index + dimension) > attribute.length) {
613
+ this._geometry.deleteAttribute(attributeName);
614
+ return;
615
+ }
177
616
  for (let j = 0; j < dimension; ++j) {
178
617
  values.push(attribute[dimension * index + j]);
179
618
  }
180
619
  }
620
+ if (!isFiniteArray(values, dimension)) {
621
+ this._geometry.deleteAttribute(attributeName);
622
+ return;
623
+ }
181
624
  this._geometry.setAttribute(attributeName, new Float32BufferAttribute(values, dimension));
625
+ if (attributeName === 'position') {
626
+ this._geometry.computeBoundingBox();
627
+ this._geometry.computeBoundingSphere();
628
+ }
182
629
  }
183
630
  }
184
631
 
@@ -210,6 +657,138 @@ class HydraMesh {
210
657
  this._mesh.matrixAutoUpdate = false;
211
658
  }
212
659
 
660
+ setInstanceTransforms(matrices, count = 0) {
661
+ if (!this._mesh) return;
662
+ const instanceCount = Number(count) || 0;
663
+ if (instanceCount <= 0) {
664
+ this._disposeInstancedMesh();
665
+ this._mesh.visible = this._visible && this._renderTag !== 'hidden';
666
+ return;
667
+ }
668
+
669
+ if (!this._instancedMesh || this._instancedMesh.count !== instanceCount) {
670
+ this._disposeInstancedMesh();
671
+ this._instancedMesh = new InstancedMesh(this._geometry, this._mesh.material, instanceCount);
672
+ this._installMeshHooks(this._instancedMesh);
673
+ this._instancedMesh.name = `${this._mesh.name}_instances`;
674
+ this._instancedMesh.castShadow = this._mesh.castShadow;
675
+ this._instancedMesh.receiveShadow = this._mesh.receiveShadow;
676
+ this._instancedMesh.matrixAutoUpdate = false;
677
+ this._instancedMesh.userData.usdPath = this._id;
678
+ this._instancedMesh.userData.usdInstanced = true;
679
+ this._interface.config.usdRoot.add(this._instancedMesh);
680
+ }
681
+
682
+ this._instancedMesh.material = this._mesh.material;
683
+ this._instancedMesh.visible = this._visible && this._renderTag !== 'hidden';
684
+ this._instancedMesh.userData.usdRenderTag = this._renderTag;
685
+ this._mesh.visible = false;
686
+
687
+ for (let i = 0; i < instanceCount; i++) {
688
+ const offset = i * 16;
689
+ this._instanceMatrix.set(...Array.from(matrices.slice(offset, offset + 16)));
690
+ this._instanceMatrix.transpose();
691
+ this._instancedMesh.setMatrixAt(i, this._instanceMatrix);
692
+ }
693
+ this._instancedMesh.instanceMatrix.needsUpdate = true;
694
+ this._instancedMesh.computeBoundingBox?.();
695
+ this._instancedMesh.computeBoundingSphere?.();
696
+ }
697
+
698
+ _disposeInstancedMesh() {
699
+ if (!this._instancedMesh) return;
700
+ if (this._instancedMesh.parent) {
701
+ this._instancedMesh.parent.remove(this._instancedMesh);
702
+ }
703
+ this._instancedMesh.dispose?.();
704
+ this._instancedMesh = null;
705
+ }
706
+
707
+ _installMeshHooks(mesh) {
708
+ mesh.userData.usdPath = this._id;
709
+ mesh.userData.usdHydraMaterialSide = this._side;
710
+ mesh.userData.usdHydraApplyMaterialSide = (material, hydraMaterial) => this._applyMaterialSide(material, hydraMaterial);
711
+ }
712
+
713
+ _disposeMaterialSideClones() {
714
+ for (const material of this._materialSideClones.values()) {
715
+ material.dispose?.();
716
+ }
717
+ this._materialSideClones.clear();
718
+ }
719
+
720
+ _updateMeshSideState() {
721
+ if (this._mesh) {
722
+ this._mesh.userData.usdHydraMaterialSide = this._side;
723
+ }
724
+ if (this._instancedMesh) {
725
+ this._instancedMesh.userData.usdHydraMaterialSide = this._side;
726
+ }
727
+ }
728
+
729
+ _applyMaterialSide(material, hydraMaterial = null) {
730
+ if (!material) return material;
731
+ if (Array.isArray(material)) {
732
+ return material.map(entry => this._applyMaterialSide(entry, hydraMaterial));
733
+ }
734
+ if (material === this._ownedMaterial || material.userData?.usdHydraMeshOwner === this._id) {
735
+ material.side = this._side;
736
+ material.needsUpdate = true;
737
+ return material;
738
+ }
739
+
740
+ if (!hydraMaterial?.requiresMaterialSideVariants?.()) {
741
+ material.side = this._side;
742
+ material.needsUpdate = true;
743
+ return material;
744
+ }
745
+
746
+ let clone = this._materialSideClones.get(material);
747
+ if (!clone) {
748
+ clone = material.clone();
749
+ clone.userData.usdHydraSideCloneOf = material.uuid;
750
+ this._materialSideClones.set(material, clone);
751
+ } else {
752
+ clone.copy(material);
753
+ clone.userData.usdHydraSideCloneOf = material.uuid;
754
+ }
755
+ clone.side = this._side;
756
+ clone.needsUpdate = true;
757
+ return clone;
758
+ }
759
+
760
+ _applyCullSideToMeshes() {
761
+ this._updateMeshSideState();
762
+ let refreshedMaterialAssignments = false;
763
+ if (this._mesh) {
764
+ refreshedMaterialAssignments = this._interface.refreshMeshMaterialAssignments(this._mesh);
765
+ if (!refreshedMaterialAssignments) {
766
+ this._mesh.material = this._applyMaterialSide(this._mesh.material);
767
+ }
768
+ }
769
+ if (this._instancedMesh) {
770
+ this._instancedMesh.material = this._mesh?.material;
771
+ }
772
+ }
773
+
774
+ setCullStyle(doubleSided, cullStyle) {
775
+ this._side = cullStyleToThreeSide(Boolean(doubleSided), String(cullStyle || "dontCare"));
776
+ this._applyCullSideToMeshes();
777
+ }
778
+
779
+ setVisibilityState(visible, renderTag = 'geometry') {
780
+ this._visible = Boolean(visible);
781
+ this._renderTag = String(renderTag || 'geometry');
782
+ if (this._mesh) {
783
+ this._mesh.visible = !this._instancedMesh && this._visible && this._renderTag !== 'hidden';
784
+ this._mesh.userData.usdRenderTag = this._renderTag;
785
+ }
786
+ if (this._instancedMesh) {
787
+ this._instancedMesh.visible = this._visible && this._renderTag !== 'hidden';
788
+ this._instancedMesh.userData.usdRenderTag = this._renderTag;
789
+ }
790
+ }
791
+
213
792
  /**
214
793
  * Sets automatically generated normals on the mesh. Should only be used if there are no authored normals.
215
794
  * @param {} normals
@@ -222,6 +801,14 @@ class HydraMesh {
222
801
  this.updateOrder(this._normals, 'normal');
223
802
  }
224
803
 
804
+ updateOrderedNormals(normals) {
805
+ // don't apply automatically generated normals if there are already authored normals.
806
+ if (this._geometry.hasAttribute('normal')) return;
807
+
808
+ this._normals = normals.slice(0);
809
+ this._geometry.setAttribute('normal', new Float32BufferAttribute(this._normals, 3));
810
+ }
811
+
225
812
  setNormals(data, interpolation) {
226
813
  if (interpolation === 'facevarying') {
227
814
  // The UV buffer has already been prepared on the C++ side, so we just set it
@@ -233,11 +820,21 @@ class HydraMesh {
233
820
  }
234
821
  }
235
822
 
823
+ setTangents(data, dimension, interpolation) {
824
+ if (interpolation === 'facevarying') {
825
+ this._geometry.setAttribute('tangent', new Float32BufferAttribute(data, dimension));
826
+ } else if (interpolation === 'vertex') {
827
+ this._tangents = data.slice(0);
828
+ this.updateOrder(this._tangents, 'tangent', dimension);
829
+ }
830
+ }
831
+
236
832
  // This is always called before prims are updated
237
833
  setMaterial(materialId) {
238
834
  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;
835
+ const hydraMaterial = this._interface.materials[materialId];
836
+ if (hydraMaterial) {
837
+ hydraMaterial.assignToMesh(this._mesh);
241
838
  }
242
839
  else {
243
840
  console.error("Material not found", materialId, this._interface.materials);
@@ -249,14 +846,26 @@ class HydraMesh {
249
846
 
250
847
  for (let i = 0; i < sections.length; i++) {
251
848
  const section = sections[i];
252
- if (this._interface.materials[section.materialId]) {
253
- this._materials.push(this._interface.materials[section.materialId]._material);
849
+ const hydraMaterial = this._interface.materials[section.materialId];
850
+ if (hydraMaterial) {
851
+ this._materials.push(hydraMaterial._material);
254
852
  this._geometry.addGroup(section.start, section.length, i + 1);
255
853
  }
256
854
  }
257
855
 
856
+ if (this._mesh.parent) {
857
+ this._mesh.parent.remove(this._mesh);
858
+ }
258
859
  this._mesh = new Mesh(this._geometry, this._materials);
860
+ this._installMeshHooks(this._mesh);
861
+ this.setVisibilityState(this._visible, this._renderTag);
862
+ this._applyCullSideToMeshes();
259
863
  this._interface.config.usdRoot.add(this._mesh);
864
+
865
+ for (let i = 0; i < sections.length; i++) {
866
+ const hydraMaterial = this._interface.materials[sections[i].materialId];
867
+ hydraMaterial?.assignToMesh(this._mesh, i + 1);
868
+ }
260
869
  }
261
870
 
262
871
  setDisplayColor(data, interpolation) {
@@ -265,8 +874,10 @@ class HydraMesh {
265
874
  let wasDefaultMaterial = false;
266
875
  if (this._mesh.material === defaultMaterial) {
267
876
  this._mesh.material = this._mesh.material.clone();
877
+ this._mesh.material.userData.usdHydraMeshOwner = this._id;
268
878
  wasDefaultMaterial = true;
269
879
  }
880
+ this._mesh.material = this._applyMaterialSide(this._mesh.material);
270
881
 
271
882
  this._colors = null;
272
883
 
@@ -340,6 +951,12 @@ class HydraMesh {
340
951
  case "normals":
341
952
  this.setNormals(data, interpolation);
342
953
  break;
954
+ case "tangent":
955
+ case "tangents":
956
+ this.setTangents(data, dimension, interpolation);
957
+ break;
958
+ case "rest":
959
+ break;
343
960
  default:
344
961
  if (warningMessagesToCount.has(name)) {
345
962
  warningMessagesToCount.set(name, warningMessagesToCount.get(name) + 1);
@@ -357,7 +974,9 @@ class HydraMesh {
357
974
  }
358
975
 
359
976
  commit() {
360
- // Nothing to do here. All Three.js resources are already updated during the sync phase.
977
+ if (this._instancedMesh && this._mesh) {
978
+ this._instancedMesh.material = this._mesh.material;
979
+ }
361
980
  }
362
981
 
363
982
  }
@@ -415,6 +1034,8 @@ class HydraMaterial {
415
1034
  constructor(id, hydraInterface) {
416
1035
  this._id = id;
417
1036
  this._nodes = {};
1037
+ this._resolvedAssetPaths = new Map();
1038
+ this._materialXDocuments = [];
418
1039
  this._interface = hydraInterface;
419
1040
  if (!defaultMaterial) {
420
1041
  defaultMaterial = new MeshPhysicalMaterial({
@@ -430,13 +1051,157 @@ class HydraMaterial {
430
1051
 
431
1052
  /** @type {MeshPhysicalMaterial} */
432
1053
  this._material = defaultMaterial;
1054
+ this._assignments = [];
1055
+ this._interface.diagnostics.materialSPrims++;
433
1056
 
434
1057
  if (debugMaterials) console.log("Hydra Material", this)
435
1058
  }
436
1059
 
1060
+ assignToMesh(mesh, materialIndex = null) {
1061
+ this._interface.diagnostics.materialAssignments++;
1062
+ const existing = this._assignments.find(assignment => assignment.mesh === mesh && assignment.materialIndex === materialIndex);
1063
+ if (!existing) {
1064
+ this._assignments.push({ mesh, materialIndex });
1065
+ }
1066
+ this._applyMaterialToAssignedMeshes();
1067
+ }
1068
+
1069
+ unassignMesh(mesh) {
1070
+ const previousLength = this._assignments.length;
1071
+ this._assignments = this._assignments.filter(assignment => assignment.mesh !== mesh);
1072
+ if (this._assignments.length !== previousLength) {
1073
+ this._applyMaterialToAssignedMeshes();
1074
+ }
1075
+ }
1076
+
1077
+ dispose() {
1078
+ this._assignments = [];
1079
+ disposeMaterialResources(this._material);
1080
+ this._material = null;
1081
+ }
1082
+
1083
+ _applyMaterialToMesh(mesh, materialIndex) {
1084
+ const applyMaterialSide = mesh.userData?.usdHydraApplyMaterialSide;
1085
+ const material = typeof applyMaterialSide === 'function'
1086
+ ? applyMaterialSide(this._material, this)
1087
+ : this._material;
1088
+
1089
+ if (materialIndex === null || materialIndex === undefined) {
1090
+ mesh.material = material;
1091
+ return;
1092
+ }
1093
+
1094
+ if (Array.isArray(mesh.material)) {
1095
+ mesh.material[materialIndex] = material;
1096
+ return;
1097
+ }
1098
+
1099
+ mesh.material = material;
1100
+ }
1101
+
1102
+ _applyMaterialToAssignedMeshes() {
1103
+ for (const assignment of this._assignments) {
1104
+ this._applyMaterialToMesh(assignment.mesh, assignment.materialIndex);
1105
+ }
1106
+ }
1107
+
1108
+ hasMeshAssignment(mesh) {
1109
+ return this._assignments.some(assignment => assignment.mesh === mesh);
1110
+ }
1111
+
1112
+ requiresMaterialSideVariants() {
1113
+ const sides = new Set();
1114
+ for (const assignment of this._assignments) {
1115
+ const side = assignment.mesh?.userData?.usdHydraMaterialSide;
1116
+ if (typeof side === 'number') {
1117
+ sides.add(side);
1118
+ }
1119
+ if (sides.size > 1) {
1120
+ return true;
1121
+ }
1122
+ }
1123
+ return false;
1124
+ }
1125
+
1126
+ refreshAssignments() {
1127
+ this._applyMaterialToAssignedMeshes();
1128
+ }
1129
+
1130
+ beginMaterialSync() {
1131
+ this._nodes = {};
1132
+ this._resolvedAssetPaths.clear();
1133
+ this._materialXDocuments = [];
1134
+ }
1135
+
1136
+ static canonicalAssetPath(path) {
1137
+ return String(path ?? "")
1138
+ .replace(/\\/g, "/")
1139
+ .replace(/^\/+(?=\.?\/)/, "")
1140
+ .replace(/^(?:\.\/)+/, "")
1141
+ .replace(/\/\.\//g, "/");
1142
+ }
1143
+
1144
+ _rememberResolvedAssetPath(authoredPath, resolvedPath) {
1145
+ if (!authoredPath || !resolvedPath) {
1146
+ return;
1147
+ }
1148
+
1149
+ const authored = String(authoredPath);
1150
+ const canonical = HydraMaterial.canonicalAssetPath(authored);
1151
+ for (const key of [
1152
+ authored,
1153
+ canonical,
1154
+ canonical.replace(/^\/+/, ""),
1155
+ canonical.replace(/^\/?(?:\.\.\/)+/, ""),
1156
+ `./${canonical}`,
1157
+ `/./${canonical}`,
1158
+ ]) {
1159
+ this._resolvedAssetPaths.set(key, String(resolvedPath));
1160
+ }
1161
+ }
1162
+
1163
+ _resolveMaterialTexturePath(authoredPath) {
1164
+ if (!authoredPath) {
1165
+ return "";
1166
+ }
1167
+
1168
+ const authored = String(authoredPath);
1169
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(authored)) {
1170
+ return authored;
1171
+ }
1172
+
1173
+ const candidates = this._interface.registry.materialResourcePathCandidates(authored);
1174
+ for (const candidate of candidates) {
1175
+ const resolved = this._resolvedAssetPaths.get(candidate)
1176
+ || this._resolvedAssetPaths.get(HydraMaterial.canonicalAssetPath(candidate));
1177
+ if (resolved) return resolved;
1178
+ }
1179
+
1180
+ for (const candidate of candidates) {
1181
+ const resolved = this._interface.registry.resolveResourcePath(candidate);
1182
+ if (resolved) return resolved;
1183
+ }
1184
+
1185
+ return "";
1186
+ }
1187
+
437
1188
  updateNode(networkId, path, parameters) {
438
1189
  if (debugTextures) console.log('Updating Material Node: ' + networkId + ' ' + path, parameters);
1190
+ this._interface.diagnostics.materialNodes++;
439
1191
  this._nodes[path] = parameters;
1192
+ this._rememberResolvedAssetPath(parameters?.file, parameters?.resolvedPath);
1193
+ }
1194
+
1195
+ updateMaterialXDocument(document) {
1196
+ if (debugMaterials) console.log('Updating MaterialX document: ' + this._id, document);
1197
+ this._interface.diagnostics.materialXDocuments++;
1198
+ const existingIndex = this._materialXDocuments.findIndex(existing => existing.terminal === document?.terminal);
1199
+ if (existingIndex >= 0) {
1200
+ this._materialXDocuments[existingIndex] = document;
1201
+ }
1202
+ else {
1203
+ this._materialXDocuments.push(document);
1204
+ }
440
1205
  }
441
1206
 
442
1207
  convertWrap(usdWrapMode) {
@@ -468,12 +1233,15 @@ class HydraMaterial {
468
1233
  }
469
1234
  if (mainMaterial[parameterName] && mainMaterial[parameterName].nodeIn) {
470
1235
  const nodeIn = mainMaterial[parameterName].nodeIn;
471
- if (!nodeIn.resolvedPath) {
472
- console.warn("Texture node has no file!", nodeIn);
1236
+ const textureFileName = this._resolveMaterialTexturePath(nodeIn.resolvedPath || nodeIn.file);
1237
+ if (!textureFileName) {
1238
+ if (debugTextures) console.debug("Texture node has no file; skipping optional texture input.", nodeIn);
1239
+ this._material[materialParameterMapName] = undefined;
1240
+ resolve();
1241
+ return;
473
1242
  }
474
1243
  if (debugTextures)
475
- console.log("Assigning texture with resolved path", parameterName, nodeIn.resolvedPath);
476
- const textureFileName = nodeIn.resolvedPath?.replace("./", "");
1244
+ console.log("Assigning texture with resolved path", parameterName, { file: nodeIn.file, resolvedPath: nodeIn.resolvedPath });
477
1245
  const channel = mainMaterial[parameterName].inputName;
478
1246
 
479
1247
  // For debugging
@@ -529,7 +1297,7 @@ class HydraMaterial {
529
1297
  if (materialParameterMapName == 'metalnessMap' && channel != 'b') {
530
1298
  targetSwizzle = '01' + channel + '1';
531
1299
  }
532
- if (materialParameterMapName == 'occlusionMap' && channel != 'r') {
1300
+ if (materialParameterMapName == 'aoMap' && channel != 'r') {
533
1301
  targetSwizzle = channel + '111';
534
1302
  }
535
1303
  if (materialParameterMapName == 'opacityMap' && channel != 'a') {
@@ -703,6 +1471,127 @@ class HydraMaterial {
703
1471
  }
704
1472
  }
705
1473
 
1474
+ static _imageChannelData(image, width, height, channel) {
1475
+ if (!image) return null;
1476
+
1477
+ if ((typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) ||
1478
+ (typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) ||
1479
+ (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap)) {
1480
+
1481
+ const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
1482
+ canvas.width = width;
1483
+ canvas.height = height;
1484
+
1485
+ const context = canvas.getContext('2d');
1486
+ context.drawImage(image, 0, 0, width, height);
1487
+
1488
+ const imageData = context.getImageData(0, 0, width, height);
1489
+ const data = imageData.data;
1490
+ const channelData = new Uint8ClampedArray(width * height);
1491
+ for (let i = 0, j = channel; i < channelData.length; i++, j += 4) {
1492
+ channelData[i] = data[j];
1493
+ }
1494
+ return channelData;
1495
+ }
1496
+
1497
+ return null;
1498
+ }
1499
+
1500
+ static _packMetallicRoughnessMap({ aoMap, roughnessMap, metalnessMap }) {
1501
+ const sourceTexture = roughnessMap || metalnessMap || aoMap;
1502
+ const sourceImage = sourceTexture?.image;
1503
+ const width = sourceImage?.width;
1504
+ const height = sourceImage?.height;
1505
+ if (!sourceTexture || !sourceImage || !width || !height) {
1506
+ return null;
1507
+ }
1508
+
1509
+ const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
1510
+ canvas.width = width;
1511
+ canvas.height = height;
1512
+ const context = canvas.getContext('2d');
1513
+ const imageData = context.createImageData(width, height);
1514
+ const data = imageData.data;
1515
+
1516
+ for (let i = 0; i < data.length; i += 4) {
1517
+ data[i] = 255;
1518
+ data[i + 1] = 255;
1519
+ data[i + 2] = 255;
1520
+ data[i + 3] = 255;
1521
+ }
1522
+
1523
+ const ao = HydraMaterial._imageChannelData(aoMap?.image, width, height, 0);
1524
+ const roughness = HydraMaterial._imageChannelData(roughnessMap?.image, width, height, 1);
1525
+ const metalness = HydraMaterial._imageChannelData(metalnessMap?.image, width, height, 2);
1526
+
1527
+ for (let i = 0, j = 0; i < width * height; i++, j += 4) {
1528
+ if (ao) data[j] = ao[i];
1529
+ if (roughness) data[j + 1] = roughness[i];
1530
+ if (metalness) data[j + 2] = metalness[i];
1531
+ }
1532
+
1533
+ context.putImageData(imageData, 0, 0);
1534
+
1535
+ const packedTexture = sourceTexture.clone();
1536
+ packedTexture.image = canvas;
1537
+ packedTexture.format = RGBAFormat;
1538
+ packedTexture.colorSpace = LinearSRGBColorSpace;
1539
+ packedTexture.name = [
1540
+ "packed-orm",
1541
+ aoMap?.name || "ao:1",
1542
+ roughnessMap?.name || "roughness:1",
1543
+ metalnessMap?.name || "metalness:1",
1544
+ ].join("|");
1545
+ packedTexture.needsUpdate = true;
1546
+ return packedTexture;
1547
+ }
1548
+
1549
+ _packMaterialTextureChannels({ haveOcclusionMap, haveRoughnessMap, haveMetalnessMap }) {
1550
+ if (!haveOcclusionMap && !haveRoughnessMap && !haveMetalnessMap) return;
1551
+
1552
+ const aoMap = this._material.aoMap;
1553
+ const roughnessMap = this._material.roughnessMap;
1554
+ const metalnessMap = this._material.metalnessMap;
1555
+ const existingPackedMap = [aoMap, roughnessMap, metalnessMap]
1556
+ .find(texture => texture?.name?.startsWith?.("packed-orm"));
1557
+ if (existingPackedMap) {
1558
+ if (haveOcclusionMap) this._material.aoMap = existingPackedMap;
1559
+ this._material.roughnessMap = existingPackedMap;
1560
+ this._material.metalnessMap = existingPackedMap;
1561
+ return;
1562
+ }
1563
+
1564
+ if ((haveOcclusionMap && !aoMap) || (haveRoughnessMap && !roughnessMap) || (haveMetalnessMap && !metalnessMap)) {
1565
+ console.error("Something went wrong with the texture promise; a material texture was authored but not loaded.", {
1566
+ haveOcclusionMap,
1567
+ haveRoughnessMap,
1568
+ haveMetalnessMap,
1569
+ aoMap,
1570
+ roughnessMap,
1571
+ metalnessMap,
1572
+ });
1573
+ return;
1574
+ }
1575
+
1576
+ const packedMap = HydraMaterial._packMetallicRoughnessMap({ aoMap, roughnessMap, metalnessMap });
1577
+ if (!packedMap) {
1578
+ console.error("Something went wrong while packing occlusion/metallic/roughness textures.", {
1579
+ aoMap,
1580
+ roughnessMap,
1581
+ metalnessMap,
1582
+ });
1583
+ return;
1584
+ }
1585
+
1586
+ if (haveOcclusionMap) this._material.aoMap = packedMap;
1587
+ this._material.roughnessMap = packedMap;
1588
+ this._material.metalnessMap = packedMap;
1589
+
1590
+ for (const texture of new Set([aoMap, roughnessMap, metalnessMap])) {
1591
+ if (texture && texture !== packedMap) texture.dispose();
1592
+ }
1593
+ }
1594
+
706
1595
  assignProperty(mainMaterial, parameterName) {
707
1596
  const materialParameterName = HydraMaterial.usdPreviewToMeshPhysicalMap[parameterName];
708
1597
  if (materialParameterName === undefined) {
@@ -726,16 +1615,39 @@ class HydraMaterial {
726
1615
  }
727
1616
  }
728
1617
 
729
- async updateFinished(type, relationships) {
1618
+ updateFinished(type, relationships) {
1619
+ this._interface.diagnostics.materialUpdateFinished++;
1620
+ const promise = this._updateFinished(type, relationships);
1621
+ this._interface.trackMaterialUpdate(promise);
1622
+ }
1623
+
1624
+ async _updateFinished(type, relationships) {
730
1625
  for (let relationship of relationships) {
731
1626
  relationship.nodeIn = this._nodes[relationship.inputId];
732
1627
  relationship.nodeOut = this._nodes[relationship.outputId];
1628
+ if (!relationship.nodeIn || !relationship.nodeOut) {
1629
+ console.warn('Material relationship references an unknown node.', relationship);
1630
+ continue;
1631
+ }
733
1632
  relationship.nodeIn[relationship.inputName] = relationship;
734
1633
  relationship.nodeOut[relationship.outputName] = relationship;
735
1634
  }
736
1635
  if (debugMaterials) console.log('Finalizing Material: ' + this._id);
737
1636
  if (debugMaterials) console.log("updateFinished", type, relationships)
738
1637
 
1638
+ const materialXDocument = this._materialXDocuments.find(document => document.terminal === type);
1639
+ if (materialXDocument) {
1640
+ const materialXMaterial = await this.createMaterialXMaterial(materialXDocument);
1641
+ if (!materialXMaterial) {
1642
+ this._material = defaultMaterial;
1643
+ this._applyMaterialToAssignedMeshes();
1644
+ return;
1645
+ }
1646
+ this._material = materialXMaterial;
1647
+ this._applyMaterialToAssignedMeshes();
1648
+ return;
1649
+ }
1650
+
739
1651
  // find the main material node
740
1652
  let mainMaterialNode = undefined;
741
1653
  for (let node of Object.values(this._nodes)) {
@@ -747,6 +1659,7 @@ class HydraMaterial {
747
1659
 
748
1660
  if (!mainMaterialNode || disableMaterials) {
749
1661
  this._material = defaultMaterial;
1662
+ this._applyMaterialToAssignedMeshes();
750
1663
  return;
751
1664
  }
752
1665
 
@@ -783,21 +1696,7 @@ class HydraMaterial {
783
1696
  }
784
1697
  await Promise.all(texturePromises);
785
1698
 
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
- }
1699
+ this._packMaterialTextureChannels({ haveOcclusionMap, haveRoughnessMap, haveMetalnessMap });
801
1700
  }
802
1701
 
803
1702
  // Assign material properties
@@ -806,6 +1705,60 @@ class HydraMaterial {
806
1705
  }
807
1706
 
808
1707
  if (debugMaterials) console.log("Material Node \"" + this._material.name + "\"", mainMaterialNode, "Resulting Material", this._material);
1708
+ this._applyMaterialToAssignedMeshes();
1709
+ }
1710
+
1711
+ async createMaterialXMaterial(materialXDocument) {
1712
+ if (!materialXDocument || disableMaterials) {
1713
+ if (!materialXDocument) {
1714
+ this._interface.diagnostics.materialXSkippedNoDocuments++;
1715
+ }
1716
+ return null;
1717
+ }
1718
+ this._interface.diagnostics.materialXCreateAttempts++;
1719
+
1720
+ const xml = materialXDocument?.xml;
1721
+ if (!xml) {
1722
+ return null;
1723
+ }
1724
+
1725
+ try {
1726
+ const { MaterialX, MaterialXMaterial } = await getMaterialXModule();
1727
+ const materialName = this._id.split('/').pop() || this._id;
1728
+ const materialNodeNameOrIndex = materialXDocument.materialName || materialName || 0;
1729
+ const material = await MaterialX.createMaterialXMaterial(xml, materialNodeNameOrIndex, {
1730
+ cacheKey: `${this._id}:${materialXDocument.terminal}`,
1731
+ getTexture: async (url) => {
1732
+ const texturePath = this._resolveMaterialTexturePath(url);
1733
+ return texturePath ? this._interface.registry.getTexture(texturePath) : null;
1734
+ },
1735
+ }, {
1736
+ parameters: {
1737
+ side: DoubleSide,
1738
+ },
1739
+ }, {});
1740
+
1741
+ if (typeof MaterialXMaterial === 'function' && !(material instanceof MaterialXMaterial)) {
1742
+ this._interface.diagnostics.materialXCreateFailures++;
1743
+ if (debugMaterials) console.debug('MaterialX shader generation returned a non-MaterialX material.', {
1744
+ materialId: this._id,
1745
+ materialName: material?.name,
1746
+ materialType: material?.constructor?.name,
1747
+ });
1748
+ return null;
1749
+ }
1750
+
1751
+ material.name = materialName;
1752
+ material.side = DoubleSide;
1753
+ material.needsUpdate = true;
1754
+ this._interface.diagnostics.materialXCreateSuccess++;
1755
+ return material;
1756
+ }
1757
+ catch (error) {
1758
+ this._interface.diagnostics.materialXCreateFailures++;
1759
+ console.warn('Failed to create MaterialX material.', error);
1760
+ return null;
1761
+ }
809
1762
  }
810
1763
  }
811
1764
 
@@ -828,6 +1781,37 @@ export class ThreeRenderDelegateInterface {
828
1781
  this.registry = new TextureRegistry(config);
829
1782
  this.materials = {};
830
1783
  this.meshes = {};
1784
+ this.scenePrimitives = {};
1785
+ this.pendingMaterialUpdates = new Set();
1786
+ this.diagnostics = {
1787
+ materialSPrims: 0,
1788
+ sceneSPrims: 0,
1789
+ materialAssignments: 0,
1790
+ materialNodes: 0,
1791
+ materialUpdateFinished: 0,
1792
+ materialXDocuments: 0,
1793
+ materialXSkippedNoDocuments: 0,
1794
+ materialXCreateAttempts: 0,
1795
+ materialXCreateSuccess: 0,
1796
+ materialXCreateFailures: 0,
1797
+ materialIds: [],
1798
+ scenePrimitiveIds: [],
1799
+ };
1800
+ }
1801
+
1802
+ trackMaterialUpdate(promise) {
1803
+ this.pendingMaterialUpdates.add(promise);
1804
+ promise.finally(() => this.pendingMaterialUpdates.delete(promise));
1805
+ }
1806
+
1807
+ async waitForMaterialsReady() {
1808
+ while (this.pendingMaterialUpdates.size > 0) {
1809
+ await Promise.allSettled([...this.pendingMaterialUpdates]);
1810
+ }
1811
+ }
1812
+
1813
+ getDiagnostics() {
1814
+ return { ...this.diagnostics };
831
1815
  }
832
1816
 
833
1817
  /**
@@ -839,11 +1823,20 @@ export class ThreeRenderDelegateInterface {
839
1823
  */
840
1824
  createRPrim(typeId, id, instancerId) {
841
1825
  if (debugPrims) console.log('Creating RPrim: ', typeId, id, typeof id);
1826
+ this.destroyRPrim(id);
842
1827
  let mesh = new HydraMesh(id, this);
843
1828
  this.meshes[id] = mesh;
844
1829
  return mesh;
845
1830
  }
846
1831
 
1832
+ destroyRPrim(id) {
1833
+ if (debugPrims) console.log('Destroying RPrim: ', id);
1834
+ const mesh = this.meshes[id];
1835
+ if (!mesh) return;
1836
+ mesh.dispose();
1837
+ delete this.meshes[id];
1838
+ }
1839
+
847
1840
  createBPrim(typeId, id) {
848
1841
  if (debugPrims) console.log('Creating BPrim: ', typeId, id);
849
1842
  /*let mesh = new HydraMesh(id, this);
@@ -855,14 +1848,74 @@ export class ThreeRenderDelegateInterface {
855
1848
  if (debugPrims) console.log('Creating SPrim: ', typeId, id);
856
1849
 
857
1850
  if (typeId === 'material') {
1851
+ this.destroySPrim(id);
858
1852
  let material = new HydraMaterial(id, this);
859
1853
  this.materials[id] = material;
1854
+ if (this.diagnostics.materialIds.length < 20) {
1855
+ this.diagnostics.materialIds.push(id);
1856
+ }
860
1857
  return material;
1858
+ } else if (typeId === 'camera' || lightSprimTypeIds.has(typeId)) {
1859
+ const scenePrimitive = new HydraScenePrimitive(typeId, id, this);
1860
+ if (!id) {
1861
+ return scenePrimitive;
1862
+ }
1863
+ this.destroySPrim(id);
1864
+ this.scenePrimitives[id] = scenePrimitive;
1865
+ this.diagnostics.sceneSPrims++;
1866
+ if (this.diagnostics.scenePrimitiveIds.length < 20) {
1867
+ this.diagnostics.scenePrimitiveIds.push(id);
1868
+ }
1869
+ return scenePrimitive;
861
1870
  } else {
862
1871
  return undefined;
863
1872
  }
864
1873
  }
865
1874
 
1875
+ destroySPrim(id) {
1876
+ if (debugPrims) console.log('Destroying SPrim: ', id);
1877
+ const material = this.materials[id];
1878
+ if (material) {
1879
+ material.dispose();
1880
+ delete this.materials[id];
1881
+ }
1882
+ const scenePrimitive = this.scenePrimitives[id];
1883
+ if (scenePrimitive) {
1884
+ scenePrimitive.dispose();
1885
+ delete this.scenePrimitives[id];
1886
+ }
1887
+ }
1888
+
1889
+ dispose() {
1890
+ for (const id of Object.keys(this.meshes)) {
1891
+ this.destroyRPrim(id);
1892
+ }
1893
+ for (const id of Object.keys(this.materials)) {
1894
+ this.destroySPrim(id);
1895
+ }
1896
+ for (const id of Object.keys(this.scenePrimitives)) {
1897
+ this.destroySPrim(id);
1898
+ }
1899
+ this.registry.dispose();
1900
+ this.pendingMaterialUpdates.clear();
1901
+ }
1902
+
1903
+ unassignMeshFromMaterials(mesh) {
1904
+ for (const material of Object.values(this.materials)) {
1905
+ material.unassignMesh(mesh);
1906
+ }
1907
+ }
1908
+
1909
+ refreshMeshMaterialAssignments(mesh) {
1910
+ let refreshed = false;
1911
+ for (const material of Object.values(this.materials)) {
1912
+ if (!material.hasMeshAssignment(mesh)) continue;
1913
+ material.refreshAssignments();
1914
+ refreshed = true;
1915
+ }
1916
+ return refreshed;
1917
+ }
1918
+
866
1919
  CommitResources() {
867
1920
  for (const id in this.meshes) {
868
1921
  const hydraMesh = this.meshes[id]