@needle-tools/materialx 1.0.0-alpha

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/src/helper.js ADDED
@@ -0,0 +1,457 @@
1
+ //
2
+ // Copyright Contributors to the MaterialX Project
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import { getParam } from '@needle-tools/engine';
7
+ import * as THREE from 'three';
8
+
9
+ const debug = getParam("debugmaterialx");
10
+
11
+ const IMAGE_PROPERTY_SEPARATOR = "_";
12
+ const UADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "uaddressmode";
13
+ const VADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "vaddressmode";
14
+ const FILTER_TYPE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "filtertype";
15
+ const IMAGE_PATH_SEPARATOR = "/";
16
+
17
+ /**
18
+ * Initialized the environment texture as MaterialX expects it
19
+ * @param {THREE.Texture} texture
20
+ * @param {Object} capabilities
21
+ * @returns {THREE.Texture}
22
+ */
23
+ export function prepareEnvTexture(texture, capabilities)
24
+ {
25
+ let newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, texture.format, texture.type);
26
+ newTexture.wrapS = THREE.RepeatWrapping;
27
+ newTexture.anisotropy = capabilities.getMaxAnisotropy();
28
+ newTexture.minFilter = THREE.LinearMipmapLinearFilter;
29
+ newTexture.magFilter = THREE.LinearFilter;
30
+ newTexture.generateMipmaps = true;
31
+ newTexture.needsUpdate = true;
32
+
33
+ return newTexture;
34
+ }
35
+
36
+ /**
37
+ * Get Three uniform from MaterialX vector
38
+ * @param {any} value
39
+ * @param {any} dimension
40
+ * @returns {THREE.Uniform}
41
+ */
42
+ function fromVector(value, dimension)
43
+ {
44
+ let outValue;
45
+ if (value)
46
+ {
47
+ outValue = [...value.data()];
48
+ }
49
+ else
50
+ {
51
+ outValue = [];
52
+ for (let i = 0; i < dimension; ++i)
53
+ outValue.push(0.0);
54
+ }
55
+
56
+ return outValue;
57
+ }
58
+
59
+ /**
60
+ * Get Three uniform from MaterialX matrix
61
+ * @param {mx.matrix} matrix
62
+ * @param {mx.matrix.size} dimension
63
+ */
64
+ function fromMatrix(matrix, dimension)
65
+ {
66
+ let vec = [];
67
+ if (matrix)
68
+ {
69
+ for (let i = 0; i < matrix.numRows(); ++i)
70
+ {
71
+ for (let k = 0; k < matrix.numColumns(); ++k)
72
+ {
73
+ vec.push(matrix.getItem(i, k));
74
+ }
75
+ }
76
+ } else
77
+ {
78
+ for (let i = 0; i < dimension; ++i)
79
+ vec.push(0.0);
80
+ }
81
+
82
+ return vec;
83
+ }
84
+
85
+ /**
86
+ * Get Three uniform from MaterialX value
87
+ * @param {mx.Uniform.type} type
88
+ * @param {mx.Uniform.value} value
89
+ * @param {mx.Uniform.name} name
90
+ * @param {mx.Uniforms} uniforms
91
+ * @param {THREE.textureLoader} textureLoader
92
+ * @param {string} searchPath
93
+ * @param {boolean} flipY
94
+ */
95
+ function toThreeUniform(type, value, name, uniforms, textureLoader, searchPath, flipY)
96
+ {
97
+ let outValue = null;
98
+ switch (type)
99
+ {
100
+ case 'float':
101
+ case 'integer':
102
+ case 'boolean':
103
+ outValue = value;
104
+ break;
105
+ case 'vector2':
106
+ outValue = fromVector(value, 2);
107
+ break;
108
+ case 'vector3':
109
+ case 'color3':
110
+ outValue = fromVector(value, 3);
111
+ break;
112
+ case 'vector4':
113
+ case 'color4':
114
+ outValue = fromVector(value, 4);
115
+ break;
116
+ case 'matrix33':
117
+ outValue = fromMatrix(value, 9);
118
+ break;
119
+ case 'matrix44':
120
+ outValue = fromMatrix(value, 16);
121
+ break;
122
+ case 'filename':
123
+ if (value)
124
+ {
125
+ // Cache / reuse texture to avoid reload overhead.
126
+ // Note: that data blobs and embedded data textures are not cached as they are transient data.
127
+ let checkCache = false;
128
+ let texturePath = searchPath + IMAGE_PATH_SEPARATOR + value;
129
+ if (value.startsWith('blob:'))
130
+ {
131
+ texturePath = value;
132
+ if (debug) console.log('Load blob URL:', texturePath);
133
+ checkCache = false;
134
+ }
135
+ else if (value.startsWith('http'))
136
+ {
137
+ texturePath = value;
138
+ if (debug) console.log('Load HTTP URL:', texturePath);
139
+ }
140
+ else if (value.startsWith('data:'))
141
+ {
142
+ texturePath = value;
143
+ checkCache = false;
144
+ if (debug) console.log('Load data URL:', texturePath);
145
+ }
146
+ const cachedTexture = checkCache && THREE.Cache.get(texturePath);
147
+ if (cachedTexture)
148
+ {
149
+ // Get texture from cache
150
+ outValue = cachedTexture;
151
+ if (debug) console.log('Use cached texture: ', texturePath, outValue);
152
+ }
153
+ else
154
+ {
155
+ outValue = textureLoader.load(
156
+ texturePath,
157
+ function (texture) {
158
+ if (debug) console.log('Load new texture: ' + texturePath, texture);
159
+ outValue = texture;
160
+
161
+ // Add texture to ThreeJS cache
162
+ if (checkCache)
163
+ THREE.Cache.add(texturePath, texture);
164
+ },
165
+ undefined,
166
+ function (error) {
167
+ console.error('Error loading texture: ', error);
168
+ });
169
+
170
+ // Set address & filtering mode
171
+ if (outValue)
172
+ setTextureParameters(outValue, name, uniforms, flipY);
173
+ }
174
+ }
175
+ break;
176
+ case 'samplerCube':
177
+ case 'string':
178
+ break;
179
+ default:
180
+ const key = type + ':' + name;
181
+ if (!valueTypeWarningMap.has(key))
182
+ {
183
+ valueTypeWarningMap.set(key, true);
184
+ console.warn('MaterialX: Unsupported uniform type: ' + type + ' for uniform: ' + name, value);
185
+ }
186
+ outValue = null;
187
+ }
188
+
189
+ return outValue;
190
+ }
191
+
192
+ const valueTypeWarningMap = new Map();
193
+
194
+ /**
195
+ * Get Three wrapping mode
196
+ * @param {mx.TextureFilter.wrap} mode
197
+ * @returns {THREE.Wrapping}
198
+ */
199
+ function getWrapping(mode)
200
+ {
201
+ let wrap;
202
+ switch (mode)
203
+ {
204
+ case 1:
205
+ wrap = THREE.ClampToEdgeWrapping;
206
+ break;
207
+ case 2:
208
+ wrap = THREE.RepeatWrapping;
209
+ break;
210
+ case 3:
211
+ wrap = THREE.MirroredRepeatWrapping;
212
+ break;
213
+ default:
214
+ wrap = THREE.RepeatWrapping;
215
+ break;
216
+ }
217
+ return wrap;
218
+ }
219
+
220
+ /**
221
+ * Get Three minification filter
222
+ * @param {mx.TextureFilter.minFilter} type
223
+ * @param {mx.TextureFilter.generateMipmaps} generateMipmaps
224
+ */
225
+ function getMinFilter(type, generateMipmaps)
226
+ {
227
+ /** @type {THREE.TextureFilter} */
228
+ let filterType = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
229
+ if (type === 0)
230
+ {
231
+ filterType = generateMipmaps ? THREE.NearestMipMapNearestFilter : THREE.NearestFilter;
232
+ }
233
+ return filterType;
234
+ }
235
+
236
+ /**
237
+ * Set Three texture parameters
238
+ * @param {THREE.Texture} texture
239
+ * @param {mx.Uniform.name} name
240
+ * @param {mx.Uniforms} uniforms
241
+ * @param {mx.TextureFilter.generateMipmaps} generateMipmaps
242
+ */
243
+ function setTextureParameters(texture, name, uniforms, flipY = true, generateMipmaps = true)
244
+ {
245
+ const idx = name.lastIndexOf(IMAGE_PROPERTY_SEPARATOR);
246
+ const base = name.substring(0, idx) || name;
247
+
248
+ texture.generateMipmaps = generateMipmaps;
249
+ texture.wrapS = THREE.RepeatWrapping;
250
+ texture.wrapT = THREE.RepeatWrapping;
251
+ texture.magFilter = THREE.LinearFilter;
252
+ texture.flipY = flipY;
253
+
254
+ if (uniforms.find(base + UADDRESS_MODE_SUFFIX))
255
+ {
256
+ const uaddressmode = uniforms.find(base + UADDRESS_MODE_SUFFIX).getValue().getData();
257
+ texture.wrapS = getWrapping(uaddressmode);
258
+ }
259
+
260
+ if (uniforms.find(base + VADDRESS_MODE_SUFFIX))
261
+ {
262
+ const vaddressmode = uniforms.find(base + VADDRESS_MODE_SUFFIX).getValue().getData();
263
+ texture.wrapT = getWrapping(vaddressmode);
264
+ }
265
+
266
+ const filterType = uniforms.find(base + FILTER_TYPE_SUFFIX) ? uniforms.get(base + FILTER_TYPE_SUFFIX).value : -1;
267
+ texture.minFilter = getMinFilter(filterType, generateMipmaps);
268
+ }
269
+
270
+ /**
271
+ * Return the global light rotation matrix
272
+ */
273
+ export function getLightRotation()
274
+ {
275
+ return new THREE.Matrix4().makeRotationY(Math.PI / 2);
276
+ }
277
+
278
+ /**
279
+ * Returns all lights nodes in a MaterialX document
280
+ * @param {mx.Document} doc
281
+ * @returns {Array.<mx.Node>}
282
+ */
283
+ export function findLights(doc)
284
+ {
285
+ let lights = [];
286
+ for (let node of doc.getNodes())
287
+ {
288
+ if (node.getType() === "lightshader")
289
+ lights.push(node);
290
+ }
291
+ return lights;
292
+ }
293
+
294
+ /**
295
+ * Register lights in shader generation context
296
+ * @param {Object} mx MaterialX Module
297
+ * @param {Array.<mx.Node>} lights Light nodes
298
+ * @param {mx.GenContext} genContext Shader generation context
299
+ * @returns {Array.<mx.Node>}
300
+ */
301
+ export async function registerLights(mx, lights, genContext)
302
+ {
303
+ mx.HwShaderGenerator.unbindLightShaders(genContext);
304
+
305
+ const lightTypesBound = {};
306
+ const lightData = [];
307
+ let lightId = 1;
308
+
309
+ // All light types so that we have NodeDefs for them
310
+ const defaultLightRigXml = `<?xml version="1.0"?>
311
+ <materialx version="1.39">
312
+ <directional_light name="default_directional_light" type="lightshader">
313
+ </directional_light>
314
+ <point_light name="default_point_light" type="lightshader">
315
+ </point_light>
316
+ <spot_light name="default_spot_light" type="lightshader">
317
+ </spot_light>
318
+ <!--
319
+ <area_light name="default_area_light" type="lightshader">
320
+ </area_light>
321
+ -->
322
+ </materialx>`;
323
+
324
+ // Load default light rig XML to ensure we have all light types available
325
+ const lightRigDoc = mx.createDocument();
326
+ await mx.readFromXmlString(lightRigDoc, defaultLightRigXml);
327
+ const document = mx.createDocument();
328
+ const stdlib = mx.loadStandardLibraries(genContext);
329
+ document.setDataLibrary(stdlib);
330
+ document.importLibrary(lightRigDoc);
331
+ const defaultLights = findLights(document);
332
+ if (debug) console.log("Default lights in MaterialX document", defaultLights);
333
+
334
+ // Register types only – we get these from the default light rig XML above
335
+ // This is needed to ensure that the light shaders are bound for each light type
336
+ for (let light of defaultLights)
337
+ {
338
+ const lightDef = light.getNodeDef();
339
+ if (debug) console.log("Default light node definition", lightDef);
340
+ if (!lightDef) continue;
341
+
342
+ const lightName = lightDef.getName();
343
+ if (debug) console.log("Registering default light", { lightName, lightDef });
344
+ if (!lightTypesBound[lightName])
345
+ {
346
+ // TODO check if we need to bind light shader for each three.js light instead of once per type
347
+ if (debug) console.log("Bind light shader for node", { lightName, lightId, lightDef });
348
+ lightTypesBound[lightName] = lightId;
349
+ mx.HwShaderGenerator.bindLightShader(lightDef, lightId++, genContext);
350
+ }
351
+ }
352
+
353
+ if (debug) console.log("Light types bound in MaterialX context", lightTypesBound);
354
+
355
+ // MaterialX light nodes
356
+ for (let light of lights)
357
+ {
358
+ // Skip if light does not have a node definition
359
+ if (!("getNodeDef" in light)) continue;
360
+
361
+ let nodeDef = light.getNodeDef();
362
+ let nodeName = nodeDef.getName();
363
+ if (!lightTypesBound[nodeName])
364
+ {
365
+ if (debug) console.log("bind light shader for node", { nodeName, lightId, nodeDef });
366
+ lightTypesBound[nodeName] = lightId;
367
+ mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
368
+ }
369
+
370
+ const lightDirection = light.getValueElement("direction").getValue().getData().data();
371
+ const lightColor = light.getValueElement("color").getValue().getData().data();
372
+ const lightIntensity = light.getValueElement("intensity").getValue().getData();
373
+
374
+ let rotatedLightDirection = new THREE.Vector3(...lightDirection)
375
+ rotatedLightDirection.transformDirection(getLightRotation())
376
+
377
+ lightData.push({
378
+ type: lightTypesBound[nodeName],
379
+ direction: rotatedLightDirection,
380
+ color: new THREE.Vector3(...lightColor),
381
+ intensity: lightIntensity,
382
+ });
383
+ }
384
+
385
+ const threeLightTypeToMaterialXNodeName = (threeLightType) => {
386
+ switch (threeLightType) {
387
+ case 'PointLight':
388
+ return 'ND_point_light';
389
+ case 'DirectionalLight':
390
+ return 'ND_directional_light';
391
+ case 'SpotLight':
392
+ return 'ND_spot_light';
393
+ default:
394
+ console.warn('MaterialX: Unsupported light type: ' + threeLightType);
395
+ return 'ND_point_light'; // Default to point light
396
+ }
397
+ };
398
+
399
+ if (debug) console.log("Registering lights in MaterialX context", lights, lightData);
400
+
401
+ // Three.js lights
402
+ for (let light of lights) {
403
+ // Skip if light is not a Three.js light
404
+ if (!light.isLight) continue;
405
+
406
+ // Types in MaterialX: point_light, directional_light, spot_light
407
+
408
+ const lightDefinitionName = threeLightTypeToMaterialXNodeName(light.type);
409
+
410
+ if (!lightTypesBound[lightDefinitionName])
411
+ {
412
+ lightTypesBound[lightDefinitionName] = lightId;
413
+ const nodeDef = null;
414
+ mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
415
+ }
416
+
417
+ lightData.push({
418
+ type: lightTypesBound[lightDefinitionName],
419
+ direction: light.direction?.clone() || new THREE.Vector3(0, -1, 0),
420
+ color: new THREE.Vector3().fromArray(light.color.toArray()),
421
+ intensity: light.intensity,
422
+ });
423
+ }
424
+
425
+ // Make sure max light count is large enough
426
+ genContext.getOptions().hwMaxActiveLightSources = Math.max(genContext.getOptions().hwMaxActiveLightSources, lightData.length);
427
+
428
+ return lightData;
429
+ }
430
+
431
+ /**
432
+ * Get uniform values for a shader
433
+ * @param {mx.shaderStage} shaderStage
434
+ * @param {THREE.TextureLoader} textureLoader
435
+ */
436
+ export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
437
+ {
438
+ let threeUniforms = {};
439
+
440
+ const uniformBlocks = Object.values(shaderStage.getUniformBlocks());
441
+ uniformBlocks.forEach(uniforms =>
442
+ {
443
+ if (!uniforms.empty())
444
+ {
445
+ for (let i = 0; i < uniforms.size(); ++i)
446
+ {
447
+ const variable = uniforms.get(i);
448
+ const value = variable.getValue()?.getData();
449
+ const name = variable.getVariable();
450
+ threeUniforms[name] = new THREE.Uniform(toThreeUniform(variable.getType().getName(), value, name, uniforms,
451
+ textureLoader, searchPath, flipY));
452
+ }
453
+ }
454
+ });
455
+
456
+ return threeUniforms;
457
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { initializeMaterialX, state } from "./materialx.js";
2
+
3
+ const getMaterialXEnvironment = () => state.materialXEnvironment;
4
+
5
+ export { initializeMaterialX, getMaterialXEnvironment };
@@ -0,0 +1,106 @@
1
+
2
+
3
+ import { Group, Camera, Material, Mesh, Object3D } from "three";
4
+ import { Context, GLTF, addCustomExtensionPlugin, Component, INeedleGLTFExtensionPlugin } from "@needle-tools/engine";
5
+ import type { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
6
+ import type { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
7
+ import { MaterialXLoader } from "./loader.three.js";
8
+ import { debug } from "../utils.js";
9
+ import { state } from "../materialx.js";
10
+
11
+ //@dont-generate-component
12
+ export class MaterialXUniformUpdate extends Component {
13
+
14
+ static updateMaterial(mat: Material | Material[], object: Object3D, camera: Camera) {
15
+ if (Array.isArray(mat)) {
16
+ mat.forEach(m => {
17
+ if (m.userData?.updateUniforms) {
18
+ m.userData.updateUniforms(object, camera);
19
+ }
20
+ });
21
+ } else if (mat.userData?.updateUniforms) {
22
+ mat.userData.updateUniforms(object, camera);
23
+ }
24
+ }
25
+
26
+ onEnable(): void {
27
+ this.context.addBeforeRenderListener(this.gameObject, this._onBeforeRender);
28
+ }
29
+
30
+ onDisable(): void {
31
+ this.context.removeBeforeRenderListener(this.gameObject, this._onBeforeRender);
32
+ }
33
+
34
+ _onBeforeRender = () => {
35
+ // Update uniforms or perform any pre-render logic here
36
+ const gameObject = this.gameObject as any as Mesh;
37
+ const material = gameObject?.material;
38
+
39
+ const camera = this.context.mainCamera;
40
+ if (!camera) return;
41
+
42
+ MaterialXUniformUpdate.updateMaterial(material, gameObject, camera);
43
+
44
+ // If this is a Group, we need to update all direct children
45
+ if ((gameObject as any as Group).isGroup) {
46
+ gameObject.children.forEach((child: Object3D) => {
47
+ if (child instanceof Mesh && child.material)
48
+ MaterialXUniformUpdate.updateMaterial(child.material, child, camera);
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
55
+ name = "MaterialXLoaderPlugin";
56
+
57
+ mtlxLoader: MaterialXLoader | null = null;
58
+
59
+ onImport = (loader: GLTFLoader, url: string, context: Context) => {
60
+ if (debug) console.log("MaterialXLoaderPlugin: Registering MaterialX extension for", url);
61
+
62
+ // Register the MaterialX loader extension
63
+ // Environment initialization is now handled in the MaterialXLoader constructor
64
+ loader.register(p => {
65
+ this.mtlxLoader = new MaterialXLoader(p, context);
66
+ return this.mtlxLoader;
67
+ });
68
+ };
69
+
70
+ onLoaded = (url: string, gltf: GLTF, _context: Context) => {
71
+ if (debug) console.log("MaterialXLoaderPlugin: glTF loaded", url, gltf.scene);
72
+
73
+ // Set up onBeforeRender callbacks for objects with MaterialX materials
74
+ // This ensures uniforms are updated properly during rendering
75
+ gltf.scene.traverse((child) => {
76
+ if ((child as any).isMesh) {
77
+ const mesh = child as Mesh;
78
+ const material = mesh.material as Material;
79
+
80
+ if (material?.userData?.updateUniforms) {
81
+ if (debug) console.log("Adding MaterialX uniform update component to:", child.name);
82
+ child.addComponent(MaterialXUniformUpdate);
83
+ }
84
+ }
85
+ });
86
+
87
+ if (debug) console.log("Loaded: ", this.mtlxLoader);
88
+
89
+ // Initialize MaterialX lighting system with scene data
90
+ const environment = state.materialXEnvironment;
91
+ environment.initializeFromContext().then(() => {
92
+ if (this.mtlxLoader) {
93
+ this.mtlxLoader.updateLightingFromEnvironment(environment);
94
+ }
95
+ });
96
+ };
97
+
98
+ onExport = (_exporter: GLTFExporter, _context: Context) => {
99
+ console.log("TODO: MaterialXLoaderPlugin: Setting up export extensions");
100
+ // TODO: Add MaterialX export functionality if needed
101
+ };
102
+ }
103
+
104
+ export function registerNeedleLoader() {
105
+ addCustomExtensionPlugin(new MaterialXLoaderPlugin());
106
+ }