@needle-tools/usd 0.0.1-412624

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 (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +100 -0
  3. package/examples/index.html +58 -0
  4. package/examples/package-lock.json +1548 -0
  5. package/examples/package.json +24 -0
  6. package/examples/public/HttpReferences copy.usda +46 -0
  7. package/examples/public/HttpReferences.usda +44 -0
  8. package/examples/public/gingerbread/GingerbreadHouse.usda +35 -0
  9. package/examples/public/gingerbread/house/GingerBreadHouse.usdc +0 -0
  10. package/examples/public/gingerbread/house/textures/color.jpg +0 -0
  11. package/examples/public/gingerbread/house/textures/metallic_roughness.jpg +0 -0
  12. package/examples/public/gingerbread/house/textures/normal.jpg +0 -0
  13. package/examples/public/gingerbread/snowman/Snowman.usdc +0 -0
  14. package/examples/public/gingerbread/snowman/textures/color.jpg +0 -0
  15. package/examples/public/gingerbread/snowman/textures/metallic_roughness.jpg +0 -0
  16. package/examples/public/gingerbread/snowman/textures/normal.jpg +0 -0
  17. package/examples/public/test.usdz +0 -0
  18. package/examples/public/vite.svg +1 -0
  19. package/examples/src/fileHandling.ts +256 -0
  20. package/examples/src/main.ts +167 -0
  21. package/examples/src/three.ts +140 -0
  22. package/examples/src/vite-env.d.ts +1 -0
  23. package/examples/tsconfig.json +23 -0
  24. package/examples/vite.config.js +21 -0
  25. package/package.json +50 -0
  26. package/src/bindings/.gitattributes +2 -0
  27. package/src/bindings/emHdBindings.data +19331 -0
  28. package/src/bindings/emHdBindings.js +12227 -0
  29. package/src/bindings/emHdBindings.wasm +0 -0
  30. package/src/bindings/emHdBindings.worker.js +124 -0
  31. package/src/bindings/index.js +124 -0
  32. package/src/create.three.js +252 -0
  33. package/src/hydra/ThreeJsRenderDelegate.js +872 -0
  34. package/src/hydra/consoleRenderDelegate.js +47 -0
  35. package/src/hydra/index.js +5 -0
  36. package/src/index.d.ts +1 -0
  37. package/src/index.js +5 -0
  38. package/src/plugins/index.js +2 -0
  39. package/src/plugins/needle.js +158 -0
  40. package/src/types/bindings.d.ts +82 -0
  41. package/src/types/create.three.d.ts +65 -0
  42. package/src/types/hydra.d.ts +32 -0
  43. package/src/types/index.d.ts +5 -0
  44. package/src/types/plugins.d.ts +9 -0
  45. package/src/types/vite.d.ts +19 -0
  46. package/src/utils.js +24 -0
  47. package/src/vite/index.js +22 -0
@@ -0,0 +1,872 @@
1
+ import { TextureLoader, BufferGeometry, MeshPhysicalMaterial, DoubleSide, Color, Mesh, Float32BufferAttribute, SRGBColorSpace, RGBAFormat, RepeatWrapping, LinearSRGBColorSpace, Vector2 } from 'three';
2
+ import { TGALoader } from 'three/addons/loaders/TGALoader.js';
3
+ import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
4
+
5
+ const debugTextures = false;
6
+ const debugMaterials = false;
7
+ const debugMeshes = false;
8
+ const debugPrims = false;
9
+ const disableTextures = false;
10
+ const disableMaterials = false;
11
+
12
+ class TextureRegistry {
13
+ /**
14
+ * @param {import('..').threeJsRenderDelegateConfig} config
15
+ */
16
+ constructor(config) {
17
+ this.config = config;
18
+ this.allPaths = config.paths;
19
+ this.textures = [];
20
+ this.loader = new TextureLoader();
21
+ this.tgaLoader = new TGALoader();
22
+ this.exrLoader = new EXRLoader();
23
+
24
+ // HACK get URL ?file parameter again
25
+ let urlParams = new URLSearchParams(window.location.search);
26
+ let fileParam = urlParams.get('file');
27
+ if (fileParam) {
28
+ let lastSlash = fileParam.lastIndexOf('/');
29
+ if (lastSlash >= 0)
30
+ fileParam = fileParam.substring(0, lastSlash);
31
+ this.baseUrl = fileParam;
32
+ }
33
+ }
34
+
35
+ getTexture(resourcePath) {
36
+ if (debugTextures) console.log("get texture", resourcePath);
37
+ if (this.textures[resourcePath]) {
38
+ return this.textures[resourcePath];
39
+ }
40
+
41
+ let textureResolve, textureReject;
42
+ this.textures[resourcePath] = new Promise((resolve, reject) => {
43
+ textureResolve = resolve;
44
+ textureReject = reject;
45
+ });
46
+
47
+ if (!resourcePath) {
48
+ return Promise.reject(new Error('Empty resource path for file: ' + resourcePath));
49
+ }
50
+
51
+ let filetype = undefined;
52
+ let lowercaseFilename = resourcePath.toLowerCase();
53
+ if (lowercaseFilename.indexOf('.png') >= lowercaseFilename.length - 5) {
54
+ filetype = 'image/png';
55
+ } else if (lowercaseFilename.indexOf('.jpg') >= lowercaseFilename.length - 5) {
56
+ filetype = 'image/jpeg';
57
+ } else if (lowercaseFilename.indexOf('.jpeg') >= lowercaseFilename.length - 5) {
58
+ 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
62
+ filetype = 'image/x-exr';
63
+ } else if (lowercaseFilename.indexOf('.tga') >= lowercaseFilename.length - 4) {
64
+ console.warn("TGA textures are not fully supported yet", resourcePath);
65
+ // using TGALoader explicitly
66
+ filetype = 'image/tga';
67
+ } else {
68
+ console.error("Error when loading texture: unknown filetype", resourcePath);
69
+ // throw new Error('Unknown filetype');
70
+ }
71
+
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
+ );
112
+ }
113
+
114
+ if (!loadedFile) {
115
+ // if the file is not part of the filesystem, we can still try to fetch it from the network
116
+ if (baseUrl) {
117
+ console.log("File not found in filesystem, trying to fetch", resourcePath);
118
+ }
119
+ else {
120
+ textureReject(new Error('Unknown file: ' + resourcePath));
121
+ return;
122
+ }
123
+ }
124
+
125
+ loadFromFile(loadedFile);
126
+ });
127
+
128
+ return this.textures[resourcePath];
129
+ }
130
+ }
131
+
132
+ class HydraMesh {
133
+ /**
134
+ * @param {string} id
135
+ * @param {ThreeRenderDelegateInterface} hydraInterface
136
+ */
137
+ constructor(id, hydraInterface) {
138
+ this._geometry = new BufferGeometry();
139
+ this._id = id;
140
+ this._interface = hydraInterface;
141
+ this._points = undefined;
142
+ this._normals = undefined;
143
+ this._colors = undefined;
144
+ this._uvs = undefined;
145
+ this._indices = undefined;
146
+ this._materials = [];
147
+
148
+ let material = new MeshPhysicalMaterial({
149
+ side: DoubleSide,
150
+ color: new Color(0xB4B4B4),
151
+ // envMap: hydraInterface.config.envMap,
152
+ });
153
+ this._materials.push(material);
154
+ this._mesh = new Mesh(this._geometry, material);
155
+ this._mesh.castShadow = true;
156
+ this._mesh.receiveShadow = true;
157
+
158
+ // ID can contain paths, we strip those here
159
+ let _name = id;
160
+ let lastSlash = _name.lastIndexOf('/');
161
+ if (lastSlash >= 0) {
162
+ _name = _name.substring(lastSlash + 1);
163
+ }
164
+ this._mesh.name = _name;
165
+
166
+ // console.log("Creating HydraMesh: " + id + " -> " + _name);
167
+
168
+ hydraInterface.config.usdRoot.add(this._mesh); // FIXME
169
+ }
170
+
171
+ updateOrder(attribute, attributeName, dimension = 3) {
172
+ if (debugMeshes) console.log("updateOrder", attribute, attributeName, dimension);
173
+ if (attribute && this._indices) {
174
+ let values = [];
175
+ for (let i = 0; i < this._indices.length; i++) {
176
+ let index = this._indices[i]
177
+ for (let j = 0; j < dimension; ++j) {
178
+ values.push(attribute[dimension * index + j]);
179
+ }
180
+ }
181
+ this._geometry.setAttribute(attributeName, new Float32BufferAttribute(values, dimension));
182
+ }
183
+ }
184
+
185
+ updateIndices(indices) {
186
+ if (debugMeshes) console.log("updateIndices", indices);
187
+ this._indices = [];
188
+ for (let i = 0; i < indices.length; i++) {
189
+ this._indices.push(indices[i]);
190
+ }
191
+ //this._geometry.setIndex( indicesArray );
192
+ this.updateOrder(this._points, 'position');
193
+ this.updateOrder(this._normals, 'normal');
194
+ if (this._colors) {
195
+ this.updateOrder(this._colors, 'color');
196
+ }
197
+ if (this._uvs) {
198
+ this.updateOrder(this._uvs, 'uv', 2);
199
+ this._geometry.attributes.uv2 = this._geometry.attributes.uv;
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Sets the transform of the mesh.
205
+ * @param {Iterable<number>} matrix - The 4x4 matrix to set on the mesh.
206
+ */
207
+ setTransform(matrix) {
208
+ this._mesh.matrix.set(...matrix);
209
+ this._mesh.matrix.transpose();
210
+ this._mesh.matrixAutoUpdate = false;
211
+ }
212
+
213
+ /**
214
+ * Sets automatically generated normals on the mesh. Should only be used if there are no authored normals.
215
+ * @param {} normals
216
+ */
217
+ updateNormals(normals) {
218
+ // don't apply automatically generated normals if there are already authored normals.
219
+ if (this._geometry.hasAttribute('normal')) return;
220
+
221
+ this._normals = normals.slice(0);
222
+ this.updateOrder(this._normals, 'normal');
223
+ }
224
+
225
+ setNormals(data, interpolation) {
226
+ if (interpolation === 'facevarying') {
227
+ // The UV buffer has already been prepared on the C++ side, so we just set it
228
+ this._geometry.setAttribute('normal', new Float32BufferAttribute(data, 3));
229
+ } else if (interpolation === 'vertex') {
230
+ // We have per-vertex UVs, so we need to sort them accordingly
231
+ this._normals = data.slice(0);
232
+ this.updateOrder(this._normals, 'normal');
233
+ }
234
+ }
235
+
236
+ // This is always called before prims are updated
237
+ setMaterial(materialId) {
238
+ 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;
241
+ }
242
+ else {
243
+ console.error("Material not found", materialId, this._interface.materials);
244
+ }
245
+ }
246
+
247
+ setGeomSubsetMaterial(sections) {
248
+ //console.log("setting subset material: ", this._id, sections)
249
+
250
+ for (let i = 0; i < sections.length; i++) {
251
+ const section = sections[i];
252
+ if (this._interface.materials[section.materialId]) {
253
+ this._materials.push(this._interface.materials[section.materialId]._material);
254
+ this._geometry.addGroup(section.start, section.length, i + 1);
255
+ }
256
+ }
257
+
258
+ this._mesh = new Mesh(this._geometry, this._materials);
259
+ this._interface.config.usdRoot.add(this._mesh);
260
+ }
261
+
262
+ setDisplayColor(data, interpolation) {
263
+ if (disableMaterials) return;
264
+
265
+ let wasDefaultMaterial = false;
266
+ if (this._mesh.material === defaultMaterial) {
267
+ this._mesh.material = this._mesh.material.clone();
268
+ wasDefaultMaterial = true;
269
+ }
270
+
271
+ this._colors = null;
272
+
273
+ if (interpolation === 'constant') {
274
+ this._mesh.material.color = new Color().fromArray(data);
275
+ } else if (interpolation === 'vertex') {
276
+ // Per-vertex buffer attribute
277
+ this._mesh.material.vertexColors = true;
278
+ if (wasDefaultMaterial) {
279
+ // Reset the pink debugging color
280
+ this._mesh.material.color = new Color(0xffffff);
281
+ }
282
+ this._colors = data.slice(0);
283
+ this.updateOrder(this._colors, 'color');
284
+ } else {
285
+ if (warningMessagesToCount.has(interpolation)) {
286
+ warningMessagesToCount.set(interpolation, warningMessagesToCount.get(interpolation) + 1);
287
+ }
288
+ else {
289
+ warningMessagesToCount.set(interpolation, 1);
290
+ console.warn(`Unsupported displayColor interpolation type '${interpolation}'.`);
291
+ }
292
+ }
293
+ }
294
+
295
+ setUV(data, dimension, interpolation) {
296
+ // TODO: Support multiple UVs. For now, we simply set uv = uv2, which is required when a material has an aoMap.
297
+ this._uvs = null;
298
+
299
+ if (interpolation === 'facevarying') {
300
+ // The UV buffer has already been prepared on the C++ side, so we just set it
301
+ this._geometry.setAttribute('uv', new Float32BufferAttribute(data, dimension));
302
+ } else if (interpolation === 'vertex') {
303
+ // We have per-vertex UVs, so we need to sort them accordingly
304
+ this._uvs = data.slice(0);
305
+ this.updateOrder(this._uvs, 'uv', 2);
306
+ }
307
+
308
+ if (this._geometry.hasAttribute('uv'))
309
+ this._geometry.attributes.uv2 = this._geometry.attributes.uv;
310
+ }
311
+
312
+ updatePrimvar(name, data, dimension, interpolation) {
313
+ if (!name) return;
314
+
315
+ if (name === 'points') { // || name === 'normals') {
316
+ // Points and normals are set separately
317
+ return;
318
+ }
319
+
320
+ // console.log('Setting PrimVar: ' + name + ", interpolation: " + interpolation);
321
+
322
+ // TODO: Support multiple UVs. For now, we simply set uv = uv2, which is required when a material has an aoMap.
323
+ if (name.startsWith('st')) {
324
+ name = 'uv';
325
+ }
326
+
327
+ switch (name) {
328
+ case 'displayColor':
329
+ this.setDisplayColor(data, interpolation);
330
+ break;
331
+ case 'uv':
332
+ case "UVMap":
333
+ case "uvmap":
334
+ case "uv0":
335
+ case "UVW":
336
+ case "uvw":
337
+ case "map1":
338
+ this.setUV(data, dimension, interpolation);
339
+ break;
340
+ case "normals":
341
+ this.setNormals(data, interpolation);
342
+ break;
343
+ default:
344
+ if (warningMessagesToCount.has(name)) {
345
+ warningMessagesToCount.set(name, warningMessagesToCount.get(name) + 1);
346
+ }
347
+ else {
348
+ warningMessagesToCount.set(name, 1);
349
+ console.warn('Unsupported primvar: ', name);
350
+ }
351
+ }
352
+ }
353
+
354
+ updatePoints(points) {
355
+ this._points = points.slice(0);
356
+ this.updateOrder(this._points, 'position');
357
+ }
358
+
359
+ commit() {
360
+ // Nothing to do here. All Three.js resources are already updated during the sync phase.
361
+ }
362
+
363
+ }
364
+
365
+ let warningMessagesToCount = new Map();
366
+
367
+ /** @type {MeshPhysicalMaterial} */
368
+ let defaultMaterial;
369
+
370
+ class HydraMaterial {
371
+ // Maps USD preview material texture names to Three.js MeshPhysicalMaterial names
372
+ static usdPreviewToMeshPhysicalTextureMap = {
373
+ 'diffuseColor': 'map',
374
+ 'clearcoat': 'clearcoatMap',
375
+ 'clearcoatRoughness': 'clearcoatRoughnessMap',
376
+ 'emissiveColor': 'emissiveMap',
377
+ 'occlusion': 'aoMap',
378
+ 'roughness': 'roughnessMap',
379
+ 'metallic': 'metalnessMap',
380
+ 'normal': 'normalMap',
381
+ 'opacity': 'alphaMap'
382
+ };
383
+
384
+ static usdPreviewToColorSpaceMap = {
385
+ 'diffuseColor': SRGBColorSpace,
386
+ 'emissiveColor': SRGBColorSpace,
387
+ 'opacity': SRGBColorSpace,
388
+ };
389
+
390
+ static channelMap = {
391
+ // Three.js expects many 8bit values such as roughness or metallness in a specific RGB texture channel.
392
+ // We could write code to combine multiple 8bit texture files into different channels of one RGB texture where it
393
+ // makes sense, but that would complicate this loader a lot. Most Three.js loaders don't seem to do it either.
394
+ // Instead, we simply provide the 8bit image as an RGBA texture, even though this might be less efficient.
395
+ 'r': RGBAFormat,
396
+ 'g': RGBAFormat,
397
+ 'b': RGBAFormat,
398
+ 'rgb': RGBAFormat,
399
+ 'rgba': RGBAFormat
400
+ };
401
+
402
+ // Maps USD preview material property names to Three.js MeshPhysicalMaterial names
403
+ static usdPreviewToMeshPhysicalMap = {
404
+ 'clearcoat': 'clearcoat',
405
+ 'clearcoatRoughness': 'clearcoatRoughness',
406
+ 'diffuseColor': 'color',
407
+ 'emissiveColor': 'emissive',
408
+ 'ior': 'ior',
409
+ 'metallic': 'metalness',
410
+ 'opacity': 'opacity',
411
+ 'roughness': 'roughness',
412
+ 'opacityThreshold': 'alphaTest',
413
+ };
414
+
415
+ constructor(id, hydraInterface) {
416
+ this._id = id;
417
+ this._nodes = {};
418
+ this._interface = hydraInterface;
419
+ if (!defaultMaterial) {
420
+ defaultMaterial = new MeshPhysicalMaterial({
421
+ side: DoubleSide,
422
+ color: new Color(0xff2997), // a bright pink color to indicate a missing material
423
+ // envMap: window.envMap,
424
+ name: 'DefaultMaterial',
425
+ });
426
+ }
427
+ // proper color when materials are disabled
428
+ if (disableMaterials)
429
+ defaultMaterial.color = new Color(0x999999);
430
+
431
+ /** @type {MeshPhysicalMaterial} */
432
+ this._material = defaultMaterial;
433
+
434
+ if (debugMaterials) console.log("Hydra Material", this)
435
+ }
436
+
437
+ updateNode(networkId, path, parameters) {
438
+ if (debugTextures) console.log('Updating Material Node: ' + networkId + ' ' + path, parameters);
439
+ this._nodes[path] = parameters;
440
+ }
441
+
442
+ convertWrap(usdWrapMode) {
443
+ if (usdWrapMode === undefined)
444
+ return RepeatWrapping;
445
+
446
+ const WRAPPINGS = {
447
+ 'repeat': 1000, // RepeatWrapping
448
+ 'clamp': 1001, // ClampToEdgeWrapping
449
+ 'mirror': 1002 // MirroredRepeatWrapping
450
+ };
451
+
452
+ if (WRAPPINGS[usdWrapMode])
453
+ return WRAPPINGS[usdWrapMode];
454
+
455
+ return RepeatWrapping;
456
+ }
457
+
458
+ /**
459
+ * @return {Promise<void>}
460
+ */
461
+ assignTexture(mainMaterial, parameterName) {
462
+ return new Promise((resolve, reject) => {
463
+ const materialParameterMapName = HydraMaterial.usdPreviewToMeshPhysicalTextureMap[parameterName];
464
+ if (materialParameterMapName === undefined) {
465
+ console.warn(`Unsupported material texture parameter '${parameterName}'.`);
466
+ resolve();
467
+ return;
468
+ }
469
+ if (mainMaterial[parameterName] && mainMaterial[parameterName].nodeIn) {
470
+ const nodeIn = mainMaterial[parameterName].nodeIn;
471
+ if (!nodeIn.resolvedPath) {
472
+ console.warn("Texture node has no file!", nodeIn);
473
+ }
474
+ if (debugTextures)
475
+ console.log("Assigning texture with resolved path", parameterName, nodeIn.resolvedPath);
476
+ const textureFileName = nodeIn.resolvedPath?.replace("./", "");
477
+ const channel = mainMaterial[parameterName].inputName;
478
+
479
+ // For debugging
480
+ const matName = Object.keys(this._nodes).find(key => this._nodes[key] === mainMaterial);
481
+ if (debugTextures) console.log(`Setting texture '${materialParameterMapName}' (${textureFileName}) of material '${matName}'... with channel '${channel}'`);
482
+
483
+ this._interface.registry.getTexture(textureFileName).then(texture => {
484
+ if (!this._material) {
485
+ console.error("Material not set when trying to assign texture, this is likely a bug");
486
+ resolve();
487
+ }
488
+ // console.log("getTexture", texture, nodeIn);
489
+ if (materialParameterMapName === 'alphaMap') {
490
+ // If this is an opacity map, check if it's using the alpha channel of the diffuse map.
491
+ // If so, simply change the format of that diffuse map to RGBA and make the material transparent.
492
+ // If not, we need to copy the alpha channel into a new texture's green channel, because that's what Three.js
493
+ // expects for alpha maps (not supported at the moment).
494
+ // NOTE that this only works if diffuse maps are always set before opacity maps, so the order of
495
+ // 'assingTexture' calls for a material matters.
496
+ if (nodeIn.file === mainMaterial.diffuseColor?.nodeIn?.file && channel === 'a') {
497
+ this._material.map.format = RGBAFormat;
498
+ } else {
499
+ // TODO: Extract the alpha channel into a new RGB texture.
500
+ console.warn("Separate alpha channel is currently not supported.", nodeIn.file, mainMaterial.diffuseColor?.nodeIn?.file, channel);
501
+ }
502
+ if (!this._material.alphaClip)
503
+ this._material.transparent = true;
504
+
505
+ this._material.needsUpdate = true;
506
+ resolve();
507
+ return;
508
+ } else if (materialParameterMapName === 'metalnessMap') {
509
+ this._material.metalness = 1.0;
510
+ } else if (materialParameterMapName === 'roughnessMap') {
511
+ this._material.roughness = 1.0;
512
+ } else if (materialParameterMapName === 'emissiveMap') {
513
+ this._material.emissive = new Color(0xffffff);
514
+ } else if (!HydraMaterial.channelMap[channel]) {
515
+ console.warn(`Unsupported texture channel '${channel}'!`);
516
+ resolve();
517
+ return;
518
+ }
519
+ // TODO need to apply bias/scale to the texture in some cases.
520
+ // May be able to extract that for metalness/roughness/opacity/normalScale
521
+
522
+ // Clone texture and set the correct format.
523
+ const clonedTexture = texture.clone();
524
+ let targetSwizzle = 'rgba';
525
+
526
+ if (materialParameterMapName == 'roughnessMap' && channel != 'g') {
527
+ targetSwizzle = '0' + channel + '11';
528
+ }
529
+ if (materialParameterMapName == 'metalnessMap' && channel != 'b') {
530
+ targetSwizzle = '01' + channel + '1';
531
+ }
532
+ if (materialParameterMapName == 'occlusionMap' && channel != 'r') {
533
+ targetSwizzle = channel + '111';
534
+ }
535
+ if (materialParameterMapName == 'opacityMap' && channel != 'a') {
536
+ targetSwizzle = channel + channel + channel + channel;
537
+ }
538
+
539
+ clonedTexture.colorSpace = HydraMaterial.usdPreviewToColorSpaceMap[parameterName] || LinearSRGBColorSpace;
540
+
541
+ // console.log("Cloned texture", clonedTexture, "swizzled with", targetSwizzle);
542
+ // clonedTexture.image = HydraMaterial._swizzleImageChannels(clonedTexture.image, targetSwizzle);
543
+ // if (materialParameterToTargetChannel[materialParameterMapName] && channel != materialParameterToTargetChannel[materialParameterMapName])
544
+ if (targetSwizzle != 'rgba') {
545
+ clonedTexture.image = HydraMaterial._swizzleImageChannels(clonedTexture.image, targetSwizzle);
546
+ }
547
+ // clonedTexture.image = HydraMaterial._swizzleImageChannels(clonedTexture.image, channel, 'g')
548
+
549
+ clonedTexture.format = HydraMaterial.channelMap[channel];
550
+ clonedTexture.needsUpdate = true;
551
+ if (nodeIn.st && nodeIn.st.nodeIn) {
552
+ const uvData = nodeIn.st.nodeIn;
553
+ // console.log("Tiling data", uvData);
554
+
555
+ // TODO this is messed up but works for scale and translation, not really for rotation.
556
+ // Refer to https://github.com/mrdoob/three.js/blob/e5426b0514a1347d7aafca69aa34117503c1be88/examples/jsm/exporters/USDZExporter.js#L461
557
+ // (which is also not perfect but close)
558
+
559
+ const rotation = uvData.rotation ? (uvData.rotation / 180 * Math.PI) : 0;
560
+ const offset = uvData.translation ? new Vector2(uvData.translation[0], uvData.translation[1]) : new Vector2(0, 0);
561
+ const repeat = uvData.scale ? new Vector2(uvData.scale[0], uvData.scale[1]) : new Vector2(1, 1);
562
+
563
+ const xRotationOffset = Math.sin(rotation);
564
+ const yRotationOffset = Math.cos(rotation);
565
+ offset.y = offset.y - (1 - yRotationOffset) * repeat.y;
566
+ offset.x = offset.x - xRotationOffset * repeat.x;
567
+ // offset.y = 1 - offset.y - repeat.y;
568
+ /*
569
+ if (uvData.scale)
570
+ clonedTexture.repeat.set(uvData.scale[0], uvData.scale[1]);
571
+ if (uvData.translation)
572
+ clonedTexture.offset.set(uvData.translation[0], uvData.translation[1]);
573
+ if (uvData.rotation)
574
+ clonedTexture.rotation = uvData.rotation / 180 * Math.PI;
575
+ */
576
+
577
+ clonedTexture.repeat.set(repeat.x, repeat.y);
578
+ clonedTexture.offset.set(offset.x, offset.y);
579
+ clonedTexture.rotation = rotation;
580
+ }
581
+
582
+ // TODO use nodeIn.wrapS and wrapT and map to THREE
583
+ clonedTexture.wrapS = this.convertWrap(nodeIn.wrapS);
584
+ clonedTexture.wrapT = this.convertWrap(nodeIn.wrapT);
585
+ if (debugTextures) console.log("Setting texture " + materialParameterMapName + " to", clonedTexture)
586
+ this._material[materialParameterMapName] = clonedTexture;
587
+ this._material.needsUpdate = true;
588
+
589
+ if (debugTextures) console.log("RESOLVED TEXTURE", clonedTexture.name, matName, parameterName);
590
+ resolve();
591
+ return;
592
+ }).catch(err => {
593
+ console.warn("Error when loading texture", err);
594
+ resolve();
595
+ return;
596
+ });
597
+ } else {
598
+ this._material[materialParameterMapName] = undefined;
599
+ resolve();
600
+ return;
601
+ }
602
+ });
603
+ }
604
+
605
+ // from https://github.com/mrdoob/three.js/blob/dev/src/math/ColorManagement.js
606
+ static SRGBToLinear(c) {
607
+ return (c < 0.04045) ? c * 0.0773993808 : Math.pow(c * 0.9478672986 + 0.0521327014, 2.4);
608
+ }
609
+
610
+ static LinearToSRGB(c) {
611
+ return (c < 0.0031308) ? c * 12.92 : 1.055 * (Math.pow(c, 0.41666)) - 0.055;
612
+ }
613
+
614
+ /**
615
+ * Swizzle image channels (e.g. move red channel to green channel)
616
+ * @param {*} image three.js image
617
+ * @param {string} swizzle For example, "rgga". Must have max. 4 components. Can contain 0 and 1, e.g. "rgba1" is valid.
618
+ * @returns three.js image
619
+ */
620
+ static _swizzleImageChannels(image, swizzle) {
621
+ if ((typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement) ||
622
+ (typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement) ||
623
+ (typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap)) {
624
+
625
+ const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
626
+
627
+ canvas.width = image.width;
628
+ canvas.height = image.height;
629
+
630
+ const context = canvas.getContext('2d');
631
+ context.drawImage(image, 0, 0, image.width, image.height);
632
+
633
+ const imageData = context.getImageData(0, 0, image.width, image.height);
634
+ const data = imageData.data;
635
+
636
+ // console.log(data);
637
+
638
+ const swizzleToIndex = {
639
+ 'r': 0,
640
+ 'g': 1,
641
+ 'b': 2,
642
+ 'a': 3,
643
+ 'x': 0,
644
+ 'y': 1,
645
+ 'z': 2,
646
+ 'w': 3,
647
+ '0': 4, // set to 0
648
+ '1': 5, // set to 1
649
+ '-': -1, // passthrough
650
+ };
651
+ const arrayAccessBySwizzle = [4, 4, 4, 4]; // empty value if nothing defined in the swizzle pattern
652
+ for (let i = 0; i < swizzle.length; i++) {
653
+ arrayAccessBySwizzle[i] = swizzleToIndex[swizzle[i]];
654
+ }
655
+
656
+ const dataEntry = data.slice(0);
657
+ for (let i = 0; i < data.length; i += 4) {
658
+ dataEntry[0] = data[i];
659
+ dataEntry[1] = data[i + 1];
660
+ dataEntry[2] = data[i + 2];
661
+ dataEntry[3] = data[i + 3];
662
+ dataEntry[4] = 0; // empty value
663
+ dataEntry[5] = 1;
664
+
665
+ const rAccess = arrayAccessBySwizzle[0];
666
+ const gAccess = arrayAccessBySwizzle[1];
667
+ const bAccess = arrayAccessBySwizzle[2];
668
+ const aAccess = arrayAccessBySwizzle[3];
669
+
670
+ if (rAccess !== -1)
671
+ data[i] = dataEntry[rAccess];
672
+ if (gAccess !== -1)
673
+ data[i + 1] = dataEntry[gAccess];
674
+ if (bAccess !== -1)
675
+ data[i + 2] = dataEntry[bAccess];
676
+ if (aAccess !== -1)
677
+ data[i + 3] = dataEntry[aAccess];
678
+ }
679
+
680
+ context.putImageData(imageData, 0, 0);
681
+ return canvas;
682
+
683
+ } else if (image.data) {
684
+ const data = image.data.slice(0);
685
+
686
+ for (let i = 0; i < data.length; i++) {
687
+ if (data instanceof Uint8Array || data instanceof Uint8ClampedArray) {
688
+ data[i] = Math.floor(this.SRGBToLinear(data[i] / 255) * 255);
689
+ } else {
690
+ // assuming float
691
+ data[i] = this.SRGBToLinear(data[i]);
692
+ }
693
+ }
694
+
695
+ return {
696
+ data: data,
697
+ width: image.width,
698
+ height: image.height
699
+ };
700
+ } else {
701
+ console.warn('ImageUtils.sRGBToLinear(): Unsupported image type. No color space conversion applied.');
702
+ return image;
703
+ }
704
+ }
705
+
706
+ assignProperty(mainMaterial, parameterName) {
707
+ const materialParameterName = HydraMaterial.usdPreviewToMeshPhysicalMap[parameterName];
708
+ if (materialParameterName === undefined) {
709
+ console.warn(`Unsupported material parameter '${parameterName}'.`);
710
+ return;
711
+ }
712
+ if (mainMaterial[parameterName] !== undefined && !mainMaterial[parameterName].nodeIn) {
713
+ // console.log(`Assigning property ${parameterName}: ${mainMaterial[parameterName]}`);
714
+ if (Array.isArray(mainMaterial[parameterName])) {
715
+ this._material[materialParameterName] = new Color().fromArray(mainMaterial[parameterName]);
716
+ } else {
717
+ this._material[materialParameterName] = mainMaterial[parameterName];
718
+ if (materialParameterName === 'opacity' && mainMaterial[parameterName] < 1.0) {
719
+ this._material.transparent = true;
720
+ }
721
+ if (parameterName == 'opacityThreshold' && mainMaterial[parameterName] > 0.0) {
722
+ this._material.transparent = false;
723
+ this._material.alphaClip = true;
724
+ }
725
+ }
726
+ }
727
+ }
728
+
729
+ async updateFinished(type, relationships) {
730
+ for (let relationship of relationships) {
731
+ relationship.nodeIn = this._nodes[relationship.inputId];
732
+ relationship.nodeOut = this._nodes[relationship.outputId];
733
+ relationship.nodeIn[relationship.inputName] = relationship;
734
+ relationship.nodeOut[relationship.outputName] = relationship;
735
+ }
736
+ if (debugMaterials) console.log('Finalizing Material: ' + this._id);
737
+ if (debugMaterials) console.log("updateFinished", type, relationships)
738
+
739
+ // find the main material node
740
+ let mainMaterialNode = undefined;
741
+ for (let node of Object.values(this._nodes)) {
742
+ if (node.diffuseColor) {
743
+ mainMaterialNode = node;
744
+ break;
745
+ }
746
+ }
747
+
748
+ if (!mainMaterialNode || disableMaterials) {
749
+ this._material = defaultMaterial;
750
+ return;
751
+ }
752
+
753
+ // TODO: Ideally, we don't recreate the material on every update.
754
+ // Creating a new one requires to also update any meshes that reference it. So we're relying on the C++ side to
755
+ // call this before also calling `setMaterial` on the affected meshes.
756
+ this._material = new MeshPhysicalMaterial({});
757
+ this._material.side = DoubleSide;
758
+ // split _id
759
+ let _name = this._id;
760
+ let lastSlash = _name.lastIndexOf('/');
761
+ if (lastSlash >= 0)
762
+ _name = _name.substring(lastSlash + 1);
763
+ this._material.name = _name;
764
+
765
+ // Assign textures
766
+ const haveRoughnessMap = !!(mainMaterialNode.roughness && mainMaterialNode.roughness.nodeIn);
767
+ const haveMetalnessMap = !!(mainMaterialNode.metallic && mainMaterialNode.metallic.nodeIn);
768
+ const haveOcclusionMap = !!(mainMaterialNode.occlusion && mainMaterialNode.occlusion.nodeIn);
769
+
770
+ if (debugMaterials) {
771
+ console.log('Creating Material: ' + this._id, mainMaterialNode, {
772
+ haveRoughnessMap,
773
+ haveMetalnessMap,
774
+ haveOcclusionMap
775
+ });
776
+ }
777
+
778
+ if (!disableTextures) {
779
+ /** @type {Array<Promise<any>>} */
780
+ const texturePromises = [];
781
+ for (let key in HydraMaterial.usdPreviewToMeshPhysicalTextureMap) {
782
+ texturePromises.push(this.assignTexture(mainMaterialNode, key));
783
+ }
784
+ await Promise.all(texturePromises);
785
+
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
+ }
801
+ }
802
+
803
+ // Assign material properties
804
+ for (let key in HydraMaterial.usdPreviewToMeshPhysicalMap) {
805
+ this.assignProperty(mainMaterialNode, key);
806
+ }
807
+
808
+ if (debugMaterials) console.log("Material Node \"" + this._material.name + "\"", mainMaterialNode, "Resulting Material", this._material);
809
+ }
810
+ }
811
+
812
+ /*
813
+ class SdfPath {
814
+ get name() { return this.GetName(); }
815
+ get absoluteRootPath() { return this.AbsoluteRootPath(); }
816
+ get reflexiveRelativePath() { return this.ReflexiveRelativePath(); }
817
+ }
818
+ */
819
+
820
+ export class ThreeRenderDelegateInterface {
821
+
822
+ /**
823
+ * @param {import('..').threeJsRenderDelegateConfig} config
824
+ */
825
+ constructor(config) {
826
+ this.config = config;
827
+ if (debugMaterials) console.log("RenderDelegateInterface", config);
828
+ this.registry = new TextureRegistry(config);
829
+ this.materials = {};
830
+ this.meshes = {};
831
+ }
832
+
833
+ /**
834
+ * Render Prims. See webRenderDelegate.h and webRenderDelegate.cpp
835
+ * @param {string} typeId // translated from TfToken
836
+ * @param {string} id // SdfPath.GetAsString()
837
+ * @param {*} instancerId
838
+ * @returns
839
+ */
840
+ createRPrim(typeId, id, instancerId) {
841
+ if (debugPrims) console.log('Creating RPrim: ', typeId, id, typeof id);
842
+ let mesh = new HydraMesh(id, this);
843
+ this.meshes[id] = mesh;
844
+ return mesh;
845
+ }
846
+
847
+ createBPrim(typeId, id) {
848
+ if (debugPrims) console.log('Creating BPrim: ', typeId, id);
849
+ /*let mesh = new HydraMesh(id, this);
850
+ this.meshes[id] = mesh;
851
+ return mesh;*/
852
+ }
853
+
854
+ createSPrim(typeId, id) {
855
+ if (debugPrims) console.log('Creating SPrim: ', typeId, id);
856
+
857
+ if (typeId === 'material') {
858
+ let material = new HydraMaterial(id, this);
859
+ this.materials[id] = material;
860
+ return material;
861
+ } else {
862
+ return undefined;
863
+ }
864
+ }
865
+
866
+ CommitResources() {
867
+ for (const id in this.meshes) {
868
+ const hydraMesh = this.meshes[id]
869
+ hydraMesh.commit();
870
+ }
871
+ }
872
+ }