@needle-tools/usd 0.0.2-next.de2e82b → 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.
- package/CHANGELOG.md +12 -1
- package/README.md +52 -9
- package/package.json +42 -10
- package/src/bindings/emHdBindings.js +5 -12227
- package/src/bindings/emHdBindings.wasm +0 -0
- package/src/bindings/index.js +130 -47
- package/src/bindings/openusd-build-info.json +40 -0
- package/src/create.three.js +362 -53
- package/src/hydra/ThreeJsRenderDelegate.js +959 -73
- package/src/plugins/index.js +1 -2
- package/src/plugins/needle.js +37 -2
- package/src/types/bindings.d.ts +296 -3
- package/src/types/create.three.d.ts +78 -7
- package/src/types/hydra.d.ts +7 -5
- package/src/types/plugins.d.ts +2 -0
- package/src/types/usd-core-bindings.d.ts +240 -0
- package/src/utils.js +3 -3
- package/examples/index.html +0 -58
- package/examples/package-lock.json +0 -1548
- package/examples/package.json +0 -24
- package/examples/public/HttpReferences copy.usda +0 -46
- package/examples/public/HttpReferences.usda +0 -44
- package/examples/public/gingerbread/GingerbreadHouse.usda +0 -35
- package/examples/public/gingerbread/house/GingerBreadHouse.usdc +0 -0
- package/examples/public/gingerbread/house/textures/color.jpg +0 -0
- package/examples/public/gingerbread/house/textures/metallic_roughness.jpg +0 -0
- package/examples/public/gingerbread/house/textures/normal.jpg +0 -0
- package/examples/public/gingerbread/snowman/Snowman.usdc +0 -0
- package/examples/public/gingerbread/snowman/textures/color.jpg +0 -0
- package/examples/public/gingerbread/snowman/textures/metallic_roughness.jpg +0 -0
- package/examples/public/gingerbread/snowman/textures/normal.jpg +0 -0
- package/examples/public/test.usdz +0 -0
- package/examples/public/vite.svg +0 -1
- package/examples/src/fileHandling.ts +0 -256
- package/examples/src/main.ts +0 -167
- package/examples/src/three.ts +0 -140
- package/examples/src/vite-env.d.ts +0 -1
- package/examples/tsconfig.json +0 -23
- package/examples/vite.config.js +0 -21
- package/src/bindings/emHdBindings.data +0 -19331
- package/src/bindings/emHdBindings.worker.js +0 -124
|
@@ -1,7 +1,33 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
53
|
-
if (
|
|
253
|
+
const extension = this.getResourceExtension(resourcePath);
|
|
254
|
+
if (extension === 'png') {
|
|
54
255
|
filetype = 'image/png';
|
|
55
|
-
} else if (
|
|
256
|
+
} else if (extension === 'jpg') {
|
|
56
257
|
filetype = 'image/jpeg';
|
|
57
|
-
} else if (
|
|
258
|
+
} else if (extension === 'jpeg') {
|
|
58
259
|
filetype = 'image/jpeg';
|
|
59
|
-
} else if (
|
|
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 (
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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 == '
|
|
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
|
-
|
|
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
|
-
|
|
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]
|