@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.
- package/CHANGELOG.md +36 -1
- package/README.md +245 -28
- package/package.json +46 -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 +368 -53
- package/src/hydra/ThreeJsRenderDelegate.js +1128 -75
- package/src/plugins/index.js +1 -2
- package/src/plugins/needle.js +38 -2
- package/src/types/bindings.d.ts +296 -3
- package/src/types/create.three.d.ts +87 -7
- package/src/types/hydra.d.ts +7 -5
- package/src/types/plugins.d.ts +7 -0
- package/src/types/usd-core-bindings.d.ts +240 -0
- package/src/utils.js +3 -3
- package/src/vite/index.js +13 -1
- 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,35 @@
|
|
|
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
|
+
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
|
-
|
|
53
|
-
if (
|
|
280
|
+
const extension = this.getResourceExtension(resourcePath);
|
|
281
|
+
if (extension === 'png') {
|
|
54
282
|
filetype = 'image/png';
|
|
55
|
-
} else if (
|
|
283
|
+
} else if (extension === 'jpg') {
|
|
56
284
|
filetype = 'image/jpeg';
|
|
57
|
-
} else if (
|
|
285
|
+
} else if (extension === 'jpeg') {
|
|
58
286
|
filetype = 'image/jpeg';
|
|
59
|
-
} else if (
|
|
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 (
|
|
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
|
-
|
|
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
|
-
);
|
|
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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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 == '
|
|
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
|
-
|
|
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
|
-
|
|
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]
|