@needle-tools/materialx 1.0.1-next.b9638d9 → 1.0.1-next.c1bbe8d

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/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Needle MaterialX
2
2
 
3
+ Load and display MaterialX materials in Needle Engine
4
+
5
+ ## Installation
6
+ `npm i @needle-tools/materialx`
7
+
3
8
  ## How to use
4
9
 
5
10
  To use with Needle Engine simply import the module
@@ -2,5 +2,7 @@
2
2
  import { TypeStore } from "@needle-tools/engine"
3
3
 
4
4
  // Import types
5
+ import { MaterialXUniformUpdate } from "../src/loader/loader.needle.js";
5
6
 
6
7
  // Register types
8
+ TypeStore.add("MaterialXUniformUpdate", MaterialXUniformUpdate);
package/index.ts CHANGED
@@ -2,4 +2,4 @@ import { registerNeedleLoader } from "./src/loader/loader.needle.js";
2
2
 
3
3
  registerNeedleLoader();
4
4
 
5
- export { initializeMaterialX, getMaterialXEnvironment } from "./src/index.js";
5
+ export { ready, getMaterialXEnvironment } from "./src/index.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/materialx",
3
- "version": "1.0.1-next.b9638d9",
3
+ "version": "1.0.1-next.c1bbe8d",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "exports": {
@@ -8,7 +8,11 @@
8
8
  "import": "./index.ts",
9
9
  "require": "./index.js"
10
10
  },
11
- "./package.json": "./package.json"
11
+ "./package.json": "./package.json",
12
+ "./codegen/register_types.ts": {
13
+ "import": "./codegen/register_types.ts",
14
+ "require": "./codegen/register_types.js"
15
+ }
12
16
  },
13
17
  "peerDependencies": {
14
18
  "@needle-tools/engine": "4.x",
@@ -23,5 +27,16 @@
23
27
  "publishConfig": {
24
28
  "access": "public",
25
29
  "registry": "https://registry.npmjs.org/"
26
- }
30
+ },
31
+ "keywords": [
32
+ "needle",
33
+ "materialx",
34
+ "material",
35
+ "shader",
36
+ "threejs",
37
+ "three.js",
38
+ "webgl",
39
+ "mtlx",
40
+ "rendering"
41
+ ]
27
42
  }
package/src/helper.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // SPDX-License-Identifier: Apache-2.0
4
4
  //
5
5
 
6
- import { getParam } from '@needle-tools/engine';
6
+ import { getParam, getWorldDirection } from '@needle-tools/engine';
7
7
  import * as THREE from 'three';
8
8
 
9
9
  const debug = getParam("debugmaterialx");
@@ -88,7 +88,7 @@ function fromMatrix(matrix, dimension)
88
88
  * @param {mx.Uniform.value} value
89
89
  * @param {mx.Uniform.name} name
90
90
  * @param {mx.Uniforms} uniforms
91
- * @param {THREE.textureLoader} textureLoader
91
+ * @param {THREE.TextureLoader} textureLoader
92
92
  * @param {string} searchPath
93
93
  * @param {boolean} flipY
94
94
  */
@@ -291,19 +291,21 @@ export function findLights(doc)
291
291
  return lights;
292
292
  }
293
293
 
294
+ let lightTypesBound = {};
295
+
294
296
  /**
295
297
  * Register lights in shader generation context
296
298
  * @param {Object} mx MaterialX Module
297
- * @param {Array.<mx.Node>} lights Light nodes
298
299
  * @param {mx.GenContext} genContext Shader generation context
299
300
  * @returns {Array.<mx.Node>}
300
301
  */
301
- export async function registerLights(mx, lights, genContext)
302
+ export async function registerLights(mx, genContext)
302
303
  {
304
+ lightTypesBound = {};
305
+ const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
306
+
303
307
  mx.HwShaderGenerator.unbindLightShaders(genContext);
304
308
 
305
- const lightTypesBound = {};
306
- const lightData = [];
307
309
  let lightId = 1;
308
310
 
309
311
  // All light types so that we have NodeDefs for them
@@ -329,7 +331,11 @@ export async function registerLights(mx, lights, genContext)
329
331
  document.setDataLibrary(stdlib);
330
332
  document.importLibrary(lightRigDoc);
331
333
  const defaultLights = findLights(document);
332
- if (debug) console.log("Default lights in MaterialX document", defaultLights);
334
+ // if (debug)
335
+ console.log("Default lights in MaterialX document", defaultLights);
336
+
337
+ // Loading a document seems to reset this option for some reason, so we set it again
338
+ genContext.getOptions().hwMaxActiveLightSources = maxLightCount;
333
339
 
334
340
  // Register types only – we get these from the default light rig XML above
335
341
  // This is needed to ensure that the light shaders are bound for each light type
@@ -351,52 +357,34 @@ export async function registerLights(mx, lights, genContext)
351
357
  }
352
358
 
353
359
  if (debug) console.log("Light types bound in MaterialX context", lightTypesBound);
360
+ }
354
361
 
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
- });
362
+ // Converts Three.js light type to MaterialX node name
363
+ function threeLightTypeToMaterialXNodeName(threeLightType) {
364
+ switch (threeLightType) {
365
+ case 'PointLight':
366
+ return 'ND_point_light';
367
+ case 'DirectionalLight':
368
+ return 'ND_directional_light';
369
+ case 'SpotLight':
370
+ return 'ND_spot_light';
371
+ default:
372
+ console.warn('MaterialX: Unsupported light type: ' + threeLightType);
373
+ return 'ND_point_light'; // Default to point light
383
374
  }
375
+ };
384
376
 
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);
377
+ /**
378
+ * Update light data for shader uniforms
379
+ * @param {Object} mx MaterialX Module
380
+ * @param {Array.<mx.Node>} lights Light nodes
381
+ * @param {mx.GenContext} genContext Shader generation context
382
+ * @returns {{ lightData: Array<any>, lightCount: number }}
383
+ */
384
+ export function getLightData(mx, lights, genContext)
385
+ {
386
+ const lightData = [];
387
+ const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
400
388
 
401
389
  // Three.js lights
402
390
  for (let light of lights) {
@@ -408,24 +396,51 @@ export async function registerLights(mx, lights, genContext)
408
396
  const lightDefinitionName = threeLightTypeToMaterialXNodeName(light.type);
409
397
 
410
398
  if (!lightTypesBound[lightDefinitionName])
411
- {
412
- lightTypesBound[lightDefinitionName] = lightId;
413
- const nodeDef = null;
414
- mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
415
- }
399
+ console.error("MaterialX: Light type not registered in context. Make sure to register light types before using them.", lightDefinitionName);
400
+
401
+ const wp = light.getWorldPosition(new THREE.Vector3());
402
+ const wd = getWorldDirection(light, new THREE.Vector3(0,0,-1));
403
+
404
+ // console.log("Registering light", light.penumbra);
416
405
 
417
406
  lightData.push({
418
407
  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,
408
+ position: wp.clone(),
409
+ direction: wd.clone(),
410
+ color: new THREE.Color().fromArray(light.color.toArray()),
411
+ // Luminous efficacy for converting radiant power in watts (W) to luminous flux in lumens (lm) at a wavelength of 555 nm.
412
+ // Also, three.js lights don't have PI scale baked in, but MaterialX does, so we need to divide by PI for point and spot lights.
413
+ intensity: light.intensity * (light.isPointLight ? 683.0 / 3.1415 : light.isSpotLight ? 683.0 / 3.1415: 1.0),
414
+ decay_rate: 2.0,
415
+ // Approximations for testing – the relevant light has 61.57986...129.4445 as inner/outer spot angle
416
+ inner_angle: 0.9,
417
+ outer_angle: 0.4,
422
418
  });
423
419
  }
424
420
 
425
- // Make sure max light count is large enough
426
- genContext.getOptions().hwMaxActiveLightSources = Math.max(genContext.getOptions().hwMaxActiveLightSources, lightData.length);
421
+ // Count the number of lights that are not empty
422
+ const lightCount = lightData.length;
423
+
424
+ // If we don't have enough entries in lightData, fill with empty lights
425
+ while (lightData.length < maxLightCount)
426
+ {
427
+ const emptyLight = {
428
+ type: 0, // Default light type
429
+ position: new THREE.Vector3(0, 0, 0),
430
+ direction: new THREE.Vector3(0, 0, -1),
431
+ color: new THREE.Color(0, 0, 0),
432
+ intensity: 0.0,
433
+ decay_rate: 2.0,
434
+ inner_angle: 0.0,
435
+ outer_angle: 0.0,
436
+ };
437
+ lightData.push(emptyLight);
438
+ }
439
+
440
+ if (debug)
441
+ console.log("Registered lights in MaterialX context", lightTypesBound, lightData);
427
442
 
428
- return lightData;
443
+ return { lightData, lightCount };
429
444
  }
430
445
 
431
446
  /**
@@ -435,11 +450,12 @@ export async function registerLights(mx, lights, genContext)
435
450
  */
436
451
  export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
437
452
  {
438
- let threeUniforms = {};
453
+ const threeUniforms = {};
439
454
 
440
455
  const uniformBlocks = Object.values(shaderStage.getUniformBlocks());
441
456
  uniformBlocks.forEach(uniforms =>
442
457
  {
458
+ // TODO Seems struct uniforms (like in LightData) end up here as well, we should filter those out.
443
459
  if (!uniforms.empty())
444
460
  {
445
461
  for (let i = 0; i < uniforms.size(); ++i)
@@ -447,6 +463,7 @@ export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
447
463
  const variable = uniforms.get(i);
448
464
  const value = variable.getValue()?.getData();
449
465
  const name = variable.getVariable();
466
+ if (debug) console.log("Adding uniform", { name, value, type: variable.getType().getName() });
450
467
  threeUniforms[name] = new THREE.Uniform(toThreeUniform(variable.getType().getName(), value, name, uniforms,
451
468
  textureLoader, searchPath, flipY));
452
469
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { initializeMaterialX, state } from "./materialx.js";
1
+ import { ready, state } from "./materialx.js";
2
2
 
3
3
  const getMaterialXEnvironment = () => state.materialXEnvironment;
4
4
 
5
- export { initializeMaterialX, getMaterialXEnvironment };
5
+ export { ready, getMaterialXEnvironment };
@@ -38,23 +38,15 @@ export class MaterialXUniformUpdate extends Component {
38
38
 
39
39
  const camera = this.context.mainCamera;
40
40
  if (!camera) return;
41
-
41
+
42
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
43
  }
52
44
  }
53
45
 
54
46
  export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
55
- name = "MaterialXLoaderPlugin";
47
+ readonly name = "MaterialXLoaderPlugin";
56
48
 
57
- mtlxLoader: MaterialXLoader | null = null;
49
+ private loader: MaterialXLoader | null = null;
58
50
 
59
51
  onImport = (loader: GLTFLoader, url: string, context: Context) => {
60
52
  if (debug) console.log("MaterialXLoaderPlugin: Registering MaterialX extension for", url);
@@ -62,13 +54,13 @@ export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
62
54
  // Register the MaterialX loader extension
63
55
  // Environment initialization is now handled in the MaterialXLoader constructor
64
56
  loader.register(p => {
65
- this.mtlxLoader = new MaterialXLoader(p, context);
66
- return this.mtlxLoader;
57
+ this.loader = new MaterialXLoader(p, context);
58
+ return this.loader;
67
59
  });
68
60
  };
69
61
 
70
- onLoaded = (url: string, gltf: GLTF, _context: Context) => {
71
- if (debug) console.log("MaterialXLoaderPlugin: glTF loaded", url, gltf.scene);
62
+ onLoaded = (url: string, gltf: GLTF, context: Context) => {
63
+ if (debug) console.log("[MaterialX] MaterialXLoaderPlugin: glTF loaded", url, gltf.scene);
72
64
 
73
65
  // Set up onBeforeRender callbacks for objects with MaterialX materials
74
66
  // This ensures uniforms are updated properly during rendering
@@ -78,25 +70,24 @@ export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
78
70
  const material = mesh.material as Material;
79
71
 
80
72
  if (material?.userData?.updateUniforms) {
81
- if (debug) console.log("Adding MaterialX uniform update component to:", child.name);
73
+ if (debug) console.log("[MaterialX] Adding MaterialX uniform update component to:", child.name);
82
74
  child.addComponent(MaterialXUniformUpdate);
83
75
  }
84
76
  }
85
77
  });
86
78
 
87
- if (debug) console.log("Loaded: ", this.mtlxLoader);
79
+ if (debug) console.log("[MaterialX] Loaded: ", this.loader);
88
80
 
89
81
  // Initialize MaterialX lighting system with scene data
90
82
  const environment = state.materialXEnvironment;
91
- environment.initializeFromContext().then(() => {
92
- if (this.mtlxLoader) {
93
- this.mtlxLoader.updateLightingFromEnvironment(environment);
94
- }
83
+ environment.initializeFromContext(context).then(() => {
84
+ console.warn("[MaterialX] Environment initialized...");
85
+ this.loader?.updateLightingFromEnvironment(environment);
95
86
  });
96
87
  };
97
88
 
98
89
  onExport = (_exporter: GLTFExporter, _context: Context) => {
99
- console.log("TODO: MaterialXLoaderPlugin: Setting up export extensions");
90
+ console.log("[MaterialX] TODO: MaterialXLoaderPlugin: Setting up export extensions");
100
91
  // TODO: Add MaterialX export functionality if needed
101
92
  };
102
93
  }
@@ -1,91 +1,59 @@
1
- import { Context } from "@needle-tools/engine";
2
- import { RawShaderMaterial, Material, MeshStandardMaterial, LoadingManager, TextureLoader, Texture, NearestFilter, Matrix4, GLSL3, AddEquation, OneMinusSrcAlphaFactor, SrcAlphaFactor, DoubleSide, Matrix3, Vector3, Object3D, Camera } from "three";
1
+ import { Context, GameObject } from "@needle-tools/engine";
2
+ import { ShaderMaterial, Material, MeshStandardMaterial, LoadingManager, TextureLoader, Texture, NearestFilter, Matrix4, GLSL3, AddEquation, OneMinusSrcAlphaFactor, SrcAlphaFactor, DoubleSide, Matrix3, Vector3, Object3D, Camera, Uniform } from "three";
3
3
  import { GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
4
4
  import { getUniformValues } from "../helper.js";
5
- import { initializeMaterialX, MaterialXEnvironment, state } from "../materialx.js";
5
+ import { ready, MaterialXEnvironment, state } from "../materialx.js";
6
6
  import { debug } from "../utils.js";
7
7
 
8
8
  // TypeScript interfaces matching the C# data structures
9
- interface MaterialXData {
10
- version: string; // e.g. "1.39"
11
- name: string; // e.g. "Material"
12
- mtlx: string; // MaterialX XML content
9
+ interface MaterialX_root_extension {
10
+ /** e.g. 1.39 */
11
+ version: string;
12
+ /** e.g. "Material" */
13
+ name: string;
14
+ /** MaterialX xml content */
15
+ mtlx: string;
13
16
  }
14
17
 
15
- interface MaterialXDataIndex {
16
- name: string; // Material name reference
18
+ interface MaterialX_material_extension {
19
+ name: string; // Material name reference
17
20
  }
18
21
 
19
22
  // MaterialX loader extension for js GLTFLoader
20
23
  export class MaterialXLoader implements GLTFLoaderPlugin {
21
24
  name = "NEEDLE_materials_mtlx";
22
25
 
23
- private rootMaterialXData: MaterialXData | null = null;
24
- private parsedDocument: any = null;
25
- private documentParsePromise: Promise<any> | null = null;
26
- private rootDataInitialized = false;
26
+ // private rootMaterialXData: MaterialX_root_extension | null = null;
27
+ private _documentReadyPromise: Promise<any> | null = null;
27
28
  private environmentInitialized = false;
28
- private generatedMaterials: RawShaderMaterial[] = [];
29
+ private generatedMaterials: ShaderMaterial[] = [];
29
30
 
30
- constructor(private parser: GLTFParser, private context: Context) {
31
- if (debug) console.log("MaterialXLoader created for parser");
32
- // Initialize MaterialX environment after MaterialX is ready
33
- this.initializeEnvironment();
34
- }
35
-
36
- // Initialize MaterialX environment - called once after MaterialX is ready
37
- private async initializeEnvironment(): Promise<void> {
38
- if (this.environmentInitialized) return;
39
-
40
- if (debug) console.log("MaterialXLoader: Initializing MaterialX environment...");
41
-
42
- // Ensure MaterialX is initialized first
43
- await initializeMaterialX();
44
-
45
- // Set up environment with context
46
- const environment = state.materialXEnvironment;
47
- environment.setContext(this.context);
48
-
49
- // Initialize the environment from context (properly awaited)
50
- try {
51
- await environment.initializeFromContext();
52
- this.environmentInitialized = true;
53
- if (debug) console.log("MaterialXLoader: Environment initialized successfully");
54
- } catch (error) {
55
- console.warn("MaterialXLoader: Failed to initialize MaterialX environment:", error);
56
- }
31
+ get materialX_root_data() {
32
+ return this.parser.json.extensions?.[this.name] as MaterialX_root_extension | null;
57
33
  }
58
34
 
59
- // Initialize root data from parser.json.extensions once
60
- private initializeRootData(): void {
61
- if (this.rootDataInitialized) return;
62
-
63
- const gltfExtensions = this.parser.json.extensions;
64
- if (gltfExtensions?.[this.name]) {
65
- if (debug) console.log("MaterialX extension found in root:", gltfExtensions[this.name]);
66
-
67
- const materialXExtension = gltfExtensions[this.name];
68
- this.rootMaterialXData = materialXExtension as MaterialXData;
35
+ constructor(private parser: GLTFParser, private context: Context) {
36
+ if (debug) console.log("MaterialXLoader created for parser");
37
+ // Start loading of MaterialX environment if the root extension exists
38
+ const hasMaterialXExtension = this.parser.json.extensions?.[this.name] != null;
39
+ if (hasMaterialXExtension) {
40
+ ready();
69
41
  }
70
- this.rootDataInitialized = true;
71
42
  }
72
43
 
73
44
  // Parse the MaterialX document once and cache it
74
- private async parseRootDocument(): Promise<any> {
75
- if (this.documentParsePromise) {
76
- return this.documentParsePromise;
45
+ private async _materialXDocumentReady(): Promise<any> {
46
+ if (this._documentReadyPromise) {
47
+ return this._documentReadyPromise;
77
48
  }
78
-
79
- this.documentParsePromise = (async () => {
80
- if (this.parsedDocument) return this.parsedDocument;
81
-
82
- if (debug) console.log("Parsing MaterialX root document...");
49
+ return this._documentReadyPromise = (async () => {
50
+ if (debug) console.log("[MaterialX] Parsing MaterialX root document...");
83
51
 
84
52
  // Ensure MaterialX is initialized
85
- await initializeMaterialX();
53
+ await ready();
86
54
 
87
55
  if (!state.materialXModule) {
88
- throw new Error("MaterialX module failed to initialize");
56
+ throw new Error("[MaterialX] module failed to initialize");
89
57
  }
90
58
 
91
59
  // Create MaterialX document and parse ALL the XML data from root
@@ -93,18 +61,15 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
93
61
  doc.setDataLibrary(state.materialXStdLib);
94
62
 
95
63
  // Parse all MaterialX XML strings from the root data
96
- if (this.rootMaterialXData) {
97
- if (debug) console.log(`Parsing MaterialX XML for: ${this.rootMaterialXData.name}`);
98
- await state.materialXModule.readFromXmlString(doc, this.rootMaterialXData.mtlx, "");
64
+ const root = this.materialX_root_data
65
+ if (root) {
66
+ if (debug) console.log(`[MaterialX] Parsing XML for: ${root.name}`);
67
+ await state.materialXModule.readFromXmlString(doc, root.mtlx, "");
99
68
  }
100
69
 
101
- if (debug) console.log("MaterialX root document parsed successfully");
102
-
103
- this.parsedDocument = doc;
70
+ if (debug) console.log("[MaterialX] root document parsed successfully");
104
71
  return doc;
105
72
  })();
106
-
107
- return this.documentParsePromise;
108
73
  }
109
74
 
110
75
  loadMaterial(materialIndex: number): Promise<Material> | null {
@@ -118,14 +83,12 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
118
83
  }
119
84
 
120
85
  private async _loadMaterialAsync(materialIndex: number): Promise<Material> {
121
- // Initialize root data first
122
- this.initializeRootData();
123
86
 
124
87
  const materialDef = this.parser.json.materials?.[materialIndex];
125
- if (debug) console.log("MaterialX extension found in material:", materialDef.extensions[this.name]);
88
+ if (debug) console.log("[MaterialX] extension found in material:", materialDef.extensions[this.name]);
126
89
 
127
90
  // Handle different types of MaterialX data
128
- const dataIndex = materialDef.extensions[this.name] as MaterialXDataIndex;
91
+ const dataIndex = materialDef.extensions[this.name] as MaterialX_material_extension;
129
92
 
130
93
  if (dataIndex) {
131
94
  // Create a new material and process MaterialX - AWAIT THIS!
@@ -138,21 +101,14 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
138
101
  return fallbackMaterial;
139
102
  }
140
103
 
141
- private rootDocument: Promise<any> | null = null;
142
- private async createMaterialXMaterial(materialXData: MaterialXDataIndex): Promise<Material> {
104
+ private async createMaterialXMaterial(materialXData: MaterialX_material_extension): Promise<Material> {
143
105
  try {
144
106
  if (debug) console.log(`Creating MaterialX material: ${materialXData.name}`);
145
107
 
146
- // Ensure MaterialX is initialized and document is parsed
147
- await initializeMaterialX();
148
-
149
- if (!this.rootDocument) {
150
- this.rootDocument = this.parseRootDocument();
151
- }
152
- const doc = await this.rootDocument;
108
+ const doc = await this._materialXDocumentReady();
153
109
 
154
110
  if (!state.materialXModule || !state.materialXGenerator || !state.materialXGenContext) {
155
- console.warn("MaterialX WASM module not ready, returning fallback material");
111
+ console.warn("[MaterialX] WASM module not ready, returning fallback material");
156
112
  const fallbackMaterial = new MeshStandardMaterial();
157
113
  fallbackMaterial.userData.materialX = materialXData;
158
114
  fallbackMaterial.name = `MaterialX_Fallback_${materialXData.name}`;
@@ -163,11 +119,11 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
163
119
  let renderableElement = null;
164
120
  let foundRenderable = false;
165
121
 
166
- if (debug) console.log("Mtlx doc", doc);
122
+ if (debug) console.log("[MaterialX] document", doc);
167
123
 
168
124
  // Search for material nodes first (following the reference pattern)
169
125
  const materialNodes = doc.getMaterialNodes();
170
- if (debug) console.log(`Found ${materialNodes.length} material nodes in document`, materialNodes);
126
+ if (debug) console.log(`[MaterialX] Found ${materialNodes.length} material nodes in document`, materialNodes);
171
127
 
172
128
  // Handle both array and vector-like APIs
173
129
  const materialNodesLength = materialNodes.length;
@@ -175,13 +131,13 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
175
131
  const materialNode = materialNodes[i];
176
132
  if (materialNode) {
177
133
  const materialName = materialNode.getNamePath();
178
- if (debug) console.log('Scan material: ', i, materialName);
134
+ if (debug) console.log('[MaterialX] Scan material: ', i, materialName);
179
135
 
180
136
  // Find the matching material
181
137
  if (materialName == materialXData.name) {
182
138
  renderableElement = materialNode;
183
139
  foundRenderable = true;
184
- if (debug) console.log('-- add material: ', materialName);
140
+ if (debug) console.log('[MaterialX] -- add material: ', materialName);
185
141
  break;
186
142
  }
187
143
  }
@@ -235,35 +191,35 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
235
191
  */
236
192
 
237
193
  if (!renderableElement) {
238
- console.warn("No renderable element found in MaterialX document");
194
+ console.warn("[MaterialX] No renderable element found in MaterialX document");
239
195
  const fallbackMaterial = new MeshStandardMaterial();
240
196
  fallbackMaterial.userData.materialX = materialXData;
241
197
  fallbackMaterial.name = `MaterialX_NoRenderable_${materialXData.name}`;
242
198
  return fallbackMaterial;
243
199
  }
244
200
 
245
- if (debug) console.log("Using renderable element for shader generation");
201
+ if (debug) console.log("[MaterialX] Using renderable element for shader generation");
246
202
 
247
203
  // Check transparency and set context options like the reference
248
- let isTransparent = state.materialXModule.isTransparentSurface(renderableElement, state.materialXGenerator.getTarget());
204
+ const isTransparent = state.materialXModule.isTransparentSurface(renderableElement, state.materialXGenerator.getTarget());
249
205
  state.materialXGenContext.getOptions().hwTransparency = isTransparent;
250
- state.materialXGenContext.getOptions().shaderInterfaceType = state.materialXModule.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
251
- state.materialXGenContext.getOptions().hwSrgbEncodeOutput = true; // Like the reference
252
206
 
253
207
  // Generate shaders using the element's name path
254
- if (debug) console.log("Generating MaterialX shaders...");
208
+ if (debug) console.log("[MaterialX] Generating MaterialX shaders...");
255
209
  const elementName = (renderableElement as any).getNamePath ? (renderableElement as any).getNamePath() : (renderableElement as any).getName();
256
210
 
257
211
  const shader = state.materialXGenerator.generate(elementName, renderableElement, state.materialXGenContext);
258
212
 
259
- // Get vertex and fragment shader source
260
- // Remove #version directive for newer js. It's added by RawShaderMaterial glslVersion.
213
+ // Get vertex and fragment shader source, and remove #version directive for newer js.
214
+ // It's added by three.js glslVersion.
261
215
  let vertexShader = shader.getSourceCode("vertex").replace(/^#version.*$/gm, '').trim();
262
216
  let fragmentShader = shader.getSourceCode("pixel").replace(/^#version.*$/gm, '').trim();
263
217
 
264
218
  // MaterialX uses different attribute names than js defaults,
265
219
  // so we patch the MaterialX shaders to match the js standard names.
266
- // Otherwise, we'd have to modify the mesh attributes (see below).
220
+ // Otherwise, we'd have to modify the mesh attributes (see original MaterialX for reference).
221
+
222
+ // Patch vertexShader
267
223
  vertexShader = vertexShader.replace(/\bi_position\b/g, 'position');
268
224
  vertexShader = vertexShader.replace(/\bi_normal\b/g, 'normal');
269
225
  vertexShader = vertexShader.replace(/\bi_texcoord_0\b/g, 'uv');
@@ -271,6 +227,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
271
227
  vertexShader = vertexShader.replace(/\bi_tangent\b/g, 'tangent');
272
228
  vertexShader = vertexShader.replace(/\bi_color_0\b/g, 'color');
273
229
 
230
+ // Patch fragmentShader
274
231
  fragmentShader = fragmentShader.replace(/\bi_position\b/g, 'position');
275
232
  fragmentShader = fragmentShader.replace(/\bi_normal\b/g, 'normal');
276
233
  fragmentShader = fragmentShader.replace(/\bi_texcoord_0\b/g, 'uv');
@@ -278,19 +235,39 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
278
235
  fragmentShader = fragmentShader.replace(/\bi_tangent\b/g, 'tangent');
279
236
  fragmentShader = fragmentShader.replace(/\bi_color_0\b/g, 'color');
280
237
 
281
- // From the original code:
282
-
283
- /* PATCHING THE SHADER IS BETTER – THIS IS JUST FOR REFERENCE
284
- // Use default MaterialX naming convention.
285
- var startStreamTime = performance.now();
286
- child.geometry.attributes.i_position = child.geometry.attributes.position;
287
- child.geometry.attributes.i_normal = child.geometry.attributes.normal;
288
- child.geometry.attributes.i_tangent = child.geometry.attributes.tangent;
289
- child.geometry.attributes.i_texcoord_0 = child.geometry.attributes.uv;
290
- */
238
+ // Remove `in vec3 position;` and so on since they're already declared by ShaderMaterial
239
+ vertexShader = vertexShader.replace(/in\s+vec3\s+position;/g, '');
240
+ vertexShader = vertexShader.replace(/in\s+vec3\s+normal;/g, '');
241
+ vertexShader = vertexShader.replace(/in\s+vec3\s+uv;/g, '');
242
+ vertexShader = vertexShader.replace(/in\s+vec3\s+uv1;/g, '');
243
+ vertexShader = vertexShader.replace(/in\s+vec4\s+tangent;/g, '');
244
+ vertexShader = vertexShader.replace(/in\s+vec4\s+color;/g, '');
245
+
246
+ // Patch uv 2-component to 3-component (`texcoord_0 = uv;` needs to be replaced with `texcoord_0 = vec3(uv, 0.0);`)
247
+ // TODO what if we actually have a 3-component UV? Not sure what three.js does then
248
+ vertexShader = vertexShader.replace(/texcoord_0 = uv;/g, 'texcoord_0 = vec3(uv, 0.0);');
249
+
250
+ // Patch units – seems MaterialX uses different units and we end up with wrong light values?
251
+ // result.direction = light.position - position;
252
+ fragmentShader = fragmentShader.replace(
253
+ /result\.direction\s*=\s*light\.position\s*-\s*position;/g,
254
+ 'result.direction = (light.position - position) * 10.0 / 1.0;');
255
+
256
+ // Add tonemapping and colorspace handling
257
+ // Replace `out vec4 out1;` with `out vec4 gl_FragColor;`
258
+ fragmentShader = fragmentShader.replace(
259
+ /out\s+vec4\s+out1;/,
260
+ 'layout(location = 0) out vec4 pc_fragColor;\n#define gl_FragColor pc_fragColor');
261
+
262
+ // Replace `out1 = vec4(<CAPTURE>)` with `gl_FragColor = vec4(<CAPTURE>)` and tonemapping/colorspace handling
263
+ fragmentShader = fragmentShader.replace(/^\s*out1\s*=\s*vec4\((.*)\);/gm,
264
+ `
265
+ gl_FragColor = vec4($1);
266
+ #include <tonemapping_fragment>
267
+ #include <colorspace_fragment>`);
291
268
 
292
269
  if (debug) {
293
- console.group("Material: ", materialXData.name);
270
+ console.group("[MaterialX]: ", materialXData.name);
294
271
  console.log("Vertex shader length:", vertexShader.length, vertexShader);
295
272
  console.log("Fragment shader length:", fragmentShader.length, fragmentShader);
296
273
  console.groupEnd();
@@ -303,7 +280,8 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
303
280
  // Find the texture in the textures in the parser and load it from there
304
281
  return url;
305
282
  });
306
- let textureLoader = new TextureLoader(loadingManager);
283
+
284
+ const textureLoader = new TextureLoader(loadingManager);
307
285
 
308
286
  // Override the textureLoader.load method to use the parser's loadTexture directly,
309
287
  // since we want to load the textures from the glTF document and not from disk.
@@ -329,17 +307,17 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
329
307
  const textures = ext?.textures || [];
330
308
 
331
309
  const index = textures.findIndex(tex => {
332
- if (debug) console.log("Checking texture:", tex.name, "against URL:", filenameWithoutExt);
310
+ if (debug) console.log("[MaterialX] Checking texture:", tex.name, "against URL:", filenameWithoutExt);
333
311
  return tex.name === filenameWithoutExt;
334
312
  });
335
313
 
336
314
  if (index < 0) {
337
- console.warn("Texture not found in parser:", filenameWithoutExt, this.parser.json);
315
+ console.warn("[MaterialX] Texture not found in parser:", filenameWithoutExt, this.parser.json);
338
316
  onError?.(new Error(`Texture not found: ${filenameWithoutExt}`));
339
317
  return;
340
318
  }
341
319
  this.parser.getDependency("texture", index).then(tex => {
342
- if (debug) console.log("Texture loaded:", tex);
320
+ if (debug) console.log("[MaterialX] Texture loaded:", tex);
343
321
  // update the checkerboard texture with the loaded texture
344
322
  checkerboardTexture.image = tex.image;
345
323
  checkerboardTexture.needsUpdate = true;
@@ -355,7 +333,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
355
333
  const searchPath = ""; // Could be derived from the asset path if needed
356
334
  const flipV = false; // Set based on your geometry requirements
357
335
 
358
- let uniforms = {
336
+ const uniforms = {
359
337
  ...getUniformValues(shader.getStage('vertex'), textureLoader, searchPath, flipV),
360
338
  ...getUniformValues(shader.getStage('pixel'), textureLoader, searchPath, flipV),
361
339
  };
@@ -371,20 +349,12 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
371
349
  return new Matrix4();
372
350
  };
373
351
 
374
- if (debug) console.log({ lightData, radianceTexture, irradianceTexture });
375
-
376
- Object.assign(uniforms, {
377
- u_numActiveLightSources: { value: lightData?.length || 0, type: 'i' },
378
- });
352
+ if (debug) console.log("Lights", { lightData, radianceTexture, irradianceTexture });
379
353
 
380
- if (lightData?.length) {
381
- Object.assign(uniforms, {
382
- u_lightData: { value: lightData, type: 'v4v' },
383
- });
354
+ if (debug) {
355
+ const mips = Math.trunc(Math.log2(Math.max(radianceTexture?.width ?? 0, radianceTexture?.height ?? 0))) + 1;
356
+ console.log("[MaterialX] Radiance texture mips:", mips, "for texture size:", radianceTexture?.width, "x", radianceTexture?.height);
384
357
  }
385
-
386
- const mips = Math.trunc(Math.log2(Math.max(radianceTexture?.width ?? 0, radianceTexture?.height ?? 0))) + 1;
387
- if (debug) console.log("Radiance texture mips:", mips, "for texture size:", radianceTexture?.width, "x", radianceTexture?.height);
388
358
  Object.assign(uniforms, {
389
359
  u_envMatrix: { value: getLightRotation() },
390
360
  u_envRadiance: { value: radianceTexture, type: 't' },
@@ -393,6 +363,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
393
363
  u_envRadianceSamples: { value: 8, type: 'i' },
394
364
  u_envIrradiance: { value: irradianceTexture, type: 't' },
395
365
  u_refractionEnv: { value: true },
366
+ u_lightData: { value: [] },
396
367
  });
397
368
 
398
369
  // console.log("NEW MATERIAL UNIFORMS", uniforms);
@@ -402,7 +373,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
402
373
  // console.log("Generated fragment shader:", fragmentShader.substring(0, 500) + "...");
403
374
 
404
375
  // Create js RawShaderMaterial (with GLSL3 for MaterialX compatibility)
405
- const shaderMaterial = new RawShaderMaterial({
376
+ const shaderMaterial = new ShaderMaterial({
406
377
  uniforms: uniforms,
407
378
  vertexShader: vertexShader,
408
379
  fragmentShader: fragmentShader,
@@ -466,14 +437,32 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
466
437
  uniforms.u_frame.value = time;
467
438
  }
468
439
 
440
+ // Update light uniforms
441
+ const environment = state.materialXEnvironment;
442
+ const lightData = environment.getLightData() || null;
443
+ const lightCount = environment.getLightCount() || 0;
444
+ if (uniforms.u_numActiveLightSources) {
445
+ uniforms.u_numActiveLightSources.value = lightCount;
446
+ }
447
+
448
+ if (lightData && uniforms.u_lightData) {
449
+ uniforms.u_lightData.value = lightData;
450
+ if (debug) console.log("Updating light data for material", shaderMaterial.name, lightData, shaderMaterial.uniforms);
451
+ }
452
+
469
453
  // Mark uniforms as needing update
470
454
  shaderMaterial.uniformsNeedUpdate = true;
471
455
  };
472
456
 
457
+ this.context.pre_update_callbacks.push(() => {
458
+ const environment = state.materialXEnvironment;
459
+ environment.updateLighting(false);
460
+ });
461
+
473
462
  shaderMaterial.name = `MaterialX_Generated_${materialXData.name}`;
474
463
 
475
464
  // Add debugging to see if the material compiles correctly
476
- if (debug) console.log("MaterialX material created successfully:", shaderMaterial.name);
465
+ if (debug) console.log("[MaterialX] material created successfully:", shaderMaterial.name);
477
466
  // if (debug) console.log("Material uniforms keys:", Object.keys(shaderMaterial.uniforms || {}));
478
467
  // if (debug) console.log("Material transparent:", shaderMaterial.transparent);
479
468
  // if (debug) console.log("Material side:", shaderMaterial.side);
@@ -485,7 +474,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
485
474
 
486
475
  } catch (error) {
487
476
  // This is a wasm error (an int) that we need to resolve
488
- console.error("Error creating MaterialX material:", error);
477
+ console.error("[MaterialX] Error creating MaterialX material:", error);
489
478
  // Return a fallback material with stored MaterialX data
490
479
  const fallbackMaterial = new MeshStandardMaterial();
491
480
  fallbackMaterial.userData.materialX = materialXData;
@@ -499,31 +488,35 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
499
488
  updateLightingFromEnvironment(environment: MaterialXEnvironment): void {
500
489
 
501
490
  // Get lighting data from environment
502
- // const lights = environment.getLights() || [];
503
491
  const lightData = environment.getLightData() || null;
492
+ const lightCount = environment.getLightCount() || 0;
504
493
  const radianceTexture = environment.getRadianceTexture() || null;
505
494
  const irradianceTexture = environment.getIrradianceTexture() || null;
506
495
 
507
- if (debug) console.log(`Updating lighting for ${this.generatedMaterials.length} MaterialX materials`, {
508
- lightData, radianceTexture, irradianceTexture,
509
- });
496
+ if (debug) {
497
+ console.log(`[MaterialX] Updating lighting for ${this.generatedMaterials.length} MaterialX materials`, {
498
+ lightData, radianceTexture, irradianceTexture,
499
+ });
500
+ }
510
501
 
511
502
  // Update each generated material's lighting uniforms
512
503
  this.generatedMaterials.forEach((material, _index) => {
513
504
  if (!material.uniforms) return;
514
505
 
506
+ console.warn(material.name, material.uniforms, lightCount)
507
+
515
508
  // Update light count
516
- if (material.uniforms.u_numActiveLightSources && lightData) {
517
- material.uniforms.u_numActiveLightSources.value = lightData.length;
509
+ if (material.uniforms.u_numActiveLightSources && lightCount >= 0) {
510
+ material.uniforms.u_numActiveLightSources.value = lightCount;
518
511
  }
519
512
 
520
513
  // Update light data if we have lights
521
514
  if (lightData) {
522
- if (!material.uniforms.u_lightData) {
523
- material.uniforms.u_lightData = { value: null };
524
- }
515
+ material.uniforms.u_lightData ??= new Uniform(null)
525
516
  material.uniforms.u_lightData.value = lightData;
517
+ if (debug) console.log("[MaterialX] Updated light data for material", material.name, lightData, material.uniforms,);
526
518
  }
519
+ else if (debug) console.warn("[MaterialX] No light data available to update uniforms for material", material.name);
527
520
 
528
521
  // Update environment uniforms
529
522
  if (material.uniforms.u_envMatrix) {
@@ -542,14 +535,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
542
535
  // Mark uniforms as needing update
543
536
  // console.log("Light data in uniforms", material.uniforms, material.fragmentShader);
544
537
  material.uniformsNeedUpdate = true;
538
+ console.log(material)
545
539
  });
546
-
547
- /* TODO not working yet
548
- this.generatedMaterials.forEach((material, index) => {
549
- console.log("Regenerating shaders for MaterialX material:", index, material.name);
550
- material.userData.regenerateShaders();
551
- resetShaders(this.context);
552
- });
553
- */
554
540
  }
555
541
  }
package/src/materialx.ts CHANGED
@@ -1,14 +1,13 @@
1
- import { Context, delay, isDevEnvironment, ObjectUtils } from "@needle-tools/engine";
1
+ import { Context, delay, isDevEnvironment, ObjectUtils, GameObject } from "@needle-tools/engine";
2
2
  import MaterialX from "../bin/JsMaterialXGenShader.js";
3
3
  import { debug } from "./utils.js";
4
4
  import { renderPMREMToEquirect } from "./textureHelper.js";
5
- import { Light, MeshBasicMaterial, Object3D, PMREMGenerator } from "three";
6
- import { registerLights } from "./helper.js";
5
+ import { Light, MeshBasicMaterial, Object3D, PMREMGenerator, Texture } from "three";
6
+ import { registerLights, getLightData } from "./helper.js";
7
7
 
8
8
 
9
- // Global MaterialX module instance - initialized lazily
10
9
  export const state = new class {
11
- materialXModule: any = null;
10
+ materialXModule: typeof MaterialX | null = null;
12
11
  materialXGenerator: any = null;
13
12
  materialXGenContext: any = null;
14
13
  materialXStdLib: any = null;
@@ -22,14 +21,14 @@ export const state = new class {
22
21
  }
23
22
  }
24
23
 
25
- // Initialize MaterialX WASM module lazily
26
- export async function initializeMaterialX(): Promise<void> {
24
+ /** Initialize the MaterialX module. Must be awaited before trying to create materials */
25
+ export async function ready(): Promise<void> {
27
26
  if (state.materialXInitPromise) {
28
27
  return state.materialXInitPromise;
29
28
  }
30
29
  return state.materialXInitPromise = (async () => {
31
30
  if (state.materialXModule) return; // Already initialized
32
- if (debug) console.log("Initializing MaterialX WASM module...");
31
+ if (debug) console.log("[MaterialX] Initializing WASM module...");
33
32
  try {
34
33
 
35
34
  const urls: Array<string> = await Promise.all([
@@ -44,7 +43,7 @@ export async function initializeMaterialX(): Promise<void> {
44
43
 
45
44
  const module = await MaterialX({
46
45
  locateFile: (path: string, scriptDirectory: string) => {
47
- if (debug) console.debug("MaterialX locateFile called:", { path, scriptDirectory });
46
+ if (debug) console.debug("[MaterialX] locateFile called:", { path, scriptDirectory });
48
47
 
49
48
  if (path.includes("JsMaterialXCore.wasm")) {
50
49
  return JsMaterialXCore; // Use the URL for the core WASM file
@@ -59,7 +58,7 @@ export async function initializeMaterialX(): Promise<void> {
59
58
  return scriptDirectory + path;
60
59
  },
61
60
  });
62
- if (debug) console.log("MaterialXLoader module loaded", module);
61
+ if (debug) console.log("[MaterialX] module loaded", module);
63
62
  state.materialXModule = module;
64
63
 
65
64
  // Initialize shader generator and context
@@ -71,23 +70,36 @@ export async function initializeMaterialX(): Promise<void> {
71
70
  state.materialXStdLib = module.loadStandardLibraries(state.materialXGenContext);
72
71
  tempDoc.setDataLibrary(state.materialXStdLib);
73
72
 
74
- // Initialize basic lighting with default light rig
75
- // const defaultLightRigXml = `<?xml version="1.0"?>
76
- // <materialx version="1.39">
77
- // <!-- Default directional light -->
78
- // <directional_light name="default_light" type="lightshader">
79
- // <input name="direction" type="vector3" value="0.0, -1.0, -0.5" />
80
- // <input name="color" type="color3" value="1.0, 1.0, 1.0" />
81
- // <input name="intensity" type="float" value="1.0" />
82
- // </directional_light>
83
- // </materialx>`;
73
+ // TODO ShaderInterfaceType.SHADER_INTERFACE_REDUCED would be better, but doesn't actually seem to be supported in the MaterialX javascript bindings
74
+ state.materialXGenContext.getOptions().shaderInterfaceType = state.materialXModule.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
75
+
76
+ // SPECULAR_ENVIRONMENT_NONE: Do not use specular environment maps.
77
+ // SPECULAR_ENVIRONMENT_FIS: Use Filtered Importance Sampling for specular environment/indirect lighting.
78
+ // SPECULAR_ENVIRONMENT_PREFILTER: Use pre-filtered environment maps for specular environment/indirect lighting.
79
+ state.materialXGenContext.getOptions().hwSpecularEnvironmentMethod = state.materialXModule.HwSpecularEnvironmentMethod.SPECULAR_ENVIRONMENT_FIS;
80
+
81
+ // TRANSMISSION_REFRACTION: Use a refraction approximation for transmission rendering.
82
+ // TRANSMISSION_OPACITY: Use opacity for transmission rendering.
83
+ // state.materialXGenContext.getOptions().hwTransmissionRenderMethod = state.materialXModule.HwTransmissionRenderMethod.TRANSMISSION_REFRACTION;
84
+
85
+ // Turned off because we're doing color space conversion the three.js way
86
+ state.materialXGenContext.getOptions().hwSrgbEncodeOutput = false;
87
+
88
+ // Enables the generation of a prefiltered environment map.
89
+ // TODO Would be great to use but requires setting more uniforms (like u_envPrefilterMip).
90
+ // When set to true, the u_envRadiance map is expected to be a prefiltered environment map.
91
+ // state.materialXGenContext.getOptions().hwWriteEnvPrefilter = true;
92
+
93
+ // Set a reasonable default for max active lights
94
+ state.materialXGenContext.getOptions().hwMaxActiveLightSources = 4;
84
95
 
85
96
  // This prewarms the shader generation context to have all light types
86
- await registerLights(state.materialXModule, [], state.materialXGenContext);
97
+ await registerLights(state.materialXModule, state.materialXGenContext);
98
+ // getLightData(state.materialXModule, [], state.materialXGenContext);
87
99
 
88
- if (debug) console.log("MaterialX generator initialized successfully");
100
+ if (debug) console.log("[MaterialX] generator initialized successfully");
89
101
  } catch (error) {
90
- console.error("Failed to load MaterialX module:", error);
102
+ console.error("[MaterialX] Failed to load MaterialX module:", error);
91
103
  throw error;
92
104
  }
93
105
  })();
@@ -95,134 +107,127 @@ export async function initializeMaterialX(): Promise<void> {
95
107
 
96
108
  // MaterialX Environment Manager - handles lighting and environment setup
97
109
  export class MaterialXEnvironment {
98
- private lights: any[] = [];
99
- private lightData: any = null;
100
- private radianceTexture: any = null;
101
- private irradianceTexture: any = null;
102
- private context: Context | null = null;
103
- private initialized: boolean = false;
110
+ private _context: Context | null = null;
111
+ private _lights: Array<Light> = [];
112
+ private _lightData: any = null;
113
+ private _lightCount: number = 0;
114
+ private _radianceTexture: Texture | null = null;
115
+ private _irradianceTexture: Texture | null = null;
116
+ private _initializePromise: Promise<boolean> | null = null;
104
117
 
105
118
  constructor() {
106
- if (debug) console.log("MaterialX Environment created");
119
+ if (debug) console.log("[MaterialX] Environment created");
107
120
  }
108
121
 
109
- setContext(context: Context) {
110
- this.context = context;
122
+ // Initialize with Needle Engine context
123
+ async initializeFromContext(context: Context): Promise<boolean> {
124
+ if (this._initializePromise) {
125
+ return this._initializePromise;
126
+ }
127
+ return this._initializePromise = this._initialize(context);
111
128
  }
112
129
 
113
- /*
114
- // Initialize MaterialX lighting system based on the reference implementation
115
- async initializeLighting(lightRigXml: string, renderer?: any, radianceTexture?: any, irradianceTexture?: any): Promise<void> {
116
- if (!materialXModule || !materialXGenContext) {
117
- console.warn("MaterialX module not initialized, skipping lighting setup");
118
- return;
119
- }
130
+ getLightData() { return this._lightData; }
131
+ getLightCount() { return this._lightCount; }
120
132
 
121
- registerLights(materialXModule, this.lights, materialXGenContext);
122
- }
123
- */
133
+ setRadianceTexture(texture: Texture) { this._radianceTexture = texture; }
134
+ getRadianceTexture() { return this._radianceTexture; }
124
135
 
125
- // Initialize with Needle Engine context
126
- async initializeFromContext(): Promise<void> {
127
- if (!this.context) {
128
- console.warn("No Needle context available for MaterialX environment initialization");
129
- return;
130
- }
136
+ getIrradianceTexture() { return this._irradianceTexture; }
137
+ setIrradianceTexture(texture: Texture) { this._irradianceTexture = texture; }
131
138
 
132
- // Prevent multiple initializations
133
- if (this.initialized) {
134
- if (debug) console.log("MaterialX environment already initialized, skipping");
135
- return;
136
- }
139
+ private _initialize: (context: Context) => Promise<boolean> = async (context: Context) => {
140
+
141
+ this._context = context;
142
+
143
+ // Get renderer from context
144
+ const renderer = context.renderer;
137
145
 
138
146
  // Clean up previous textures if they exist
139
- if (this.radianceTexture) {
140
- if (debug) console.log("Disposing previous radiance texture");
141
- this.radianceTexture.dispose();
142
- this.radianceTexture = null;
147
+ if (this._radianceTexture) {
148
+ if (debug) console.log("[MaterialX] Disposing previous radiance texture");
149
+ this._radianceTexture.dispose();
150
+ this._radianceTexture = null;
143
151
  }
144
- if (this.irradianceTexture) {
145
- if (debug) console.log("Disposing previous irradiance texture");
146
- this.irradianceTexture.dispose();
147
- this.irradianceTexture = null;
152
+ if (this._irradianceTexture) {
153
+ if (debug) console.log("[MaterialX] Disposing previous irradiance texture");
154
+ this._irradianceTexture.dispose();
155
+ this._irradianceTexture = null;
148
156
  }
149
157
 
150
- // Get renderer from context
151
- const renderer = this.context.renderer;
152
-
153
158
  // TODO remove this delay; we should wait for the scene lighting to be ready
154
159
  // and then update the uniforms
155
- let envMap = this.context.scene.environment;
160
+ let envMap = context.scene.environment;
156
161
  while (!envMap) {
157
- await delay(200);
158
- envMap = this.context.scene.environment;
162
+ await delay(1000);
163
+ envMap = context.scene.environment;
159
164
  }
160
- var pmrem = new PMREMGenerator(renderer);
165
+ const pmrem = new PMREMGenerator(renderer);
161
166
  const target = pmrem.fromEquirectangular(envMap);
162
-
163
167
  const radianceRenderTarget = renderPMREMToEquirect(renderer, target.texture, 0.0, 1024, 512, target.height);
164
168
  const irradianceRenderTarget = renderPMREMToEquirect(renderer, target.texture, 1.0, 32, 16, target.height);
165
-
166
- this.radianceTexture = radianceRenderTarget.texture;
167
- this.irradianceTexture = irradianceRenderTarget.texture;
169
+ this._radianceTexture = radianceRenderTarget.texture;
170
+ this._irradianceTexture = irradianceRenderTarget.texture;
168
171
 
169
172
  // Clean up PMREM generator and its render target
170
173
  target.dispose();
171
174
  pmrem.dispose();
172
175
 
173
176
  if (debug) {
174
- console.log({ radiance: this.radianceTexture, irradiance: this.irradianceTexture });
177
+ console.log({ radiance: this._radianceTexture, irradiance: this._irradianceTexture });
175
178
  // Show both of them on cubes in the scene
176
179
  const unlitMat = new MeshBasicMaterial();
180
+ unlitMat.side = 2;
177
181
  const radianceMat = unlitMat.clone();
178
- radianceMat.map = this.radianceTexture;
179
- const radianceCube = ObjectUtils.createPrimitive("Cube", { material: radianceMat });
182
+ radianceMat.map = this._radianceTexture;
183
+ const radianceCube = ObjectUtils.createPrimitive("Quad", { material: radianceMat });
180
184
  const irradianceMat = unlitMat.clone();
181
- irradianceMat.map = this.irradianceTexture;
182
- const irradianceCube = ObjectUtils.createPrimitive("Cube", { material: irradianceMat });
183
- this.context.scene.add(radianceCube);
184
- this.context.scene.add(irradianceCube);
185
+ irradianceMat.map = this._irradianceTexture;
186
+ const irradianceCube = ObjectUtils.createPrimitive("Quad", { material: irradianceMat });
187
+ context.scene.add(radianceCube);
188
+ context.scene.add(irradianceCube);
185
189
  radianceCube.position.set(2, 0, 0);
186
- radianceCube.scale.y = 0.00001;
187
190
  irradianceCube.position.set(-2, 0, 0);
188
- irradianceCube.scale.y = 0.00001;
189
- // await this.initializeLighting(defaultLightRigXml, renderer);
190
- console.log("MaterialX environment initialized from Needle context", this, this.context.scene);
191
+ console.log("[MaterialX] environment initialized from Needle context", this, this._context.scene);
191
192
  }
192
193
 
193
- // Find lights in scene
194
- let lights = new Array<Light>();
195
- this.context.scene.traverse((object: Object3D) => {
196
- if ((object as Light).isLight) lights.push(object as Light);
197
- });
198
-
199
- this.lightData = await registerLights(state.materialXModule, lights, state.materialXGenContext);
194
+ this.updateLighting(true);
200
195
 
201
196
  // Mark as initialized
202
- this.initialized = true;
197
+ return true;
203
198
  }
204
199
 
205
- // getLights() { return this.lights; }
206
- getLightData() { return this.lightData; }
207
- getRadianceTexture() { return this.radianceTexture; }
208
- getIrradianceTexture() { return this.irradianceTexture; }
200
+ updateLighting(collectLights: boolean) {
201
+ if (!this._context) return;
202
+
203
+ // Find lights in scene
204
+ if (collectLights) {
205
+ const lights = new Array<Light>();
206
+ this._context.scene.traverse((object: Object3D) => {
207
+ if ((object as Light).isLight && GameObject.isActiveInHierarchy(object))
208
+ lights.push(object as Light);
209
+ });
210
+ this._lights = lights;
211
+ }
209
212
 
210
- setRadianceTexture(texture: any) { this.radianceTexture = texture; }
211
- setIrradianceTexture(texture: any) { this.irradianceTexture = texture; }
213
+ const { lightData, lightCount } = getLightData(state.materialXModule, this._lights, state.materialXGenContext);
214
+ this._lightData = lightData;
215
+ this._lightCount = lightCount;
216
+ }
212
217
 
213
218
  // Reset the environment to allow re-initialization
214
219
  reset() {
215
- if (debug) console.log("Resetting MaterialX environment");
216
- if (this.radianceTexture) {
217
- this.radianceTexture.dispose();
218
- this.radianceTexture = null;
220
+ if (debug) console.log("[MaterialX] Resetting environment");
221
+ if (this._radianceTexture) {
222
+ this._radianceTexture.dispose();
223
+ this._radianceTexture = null;
219
224
  }
220
- if (this.irradianceTexture) {
221
- this.irradianceTexture.dispose();
222
- this.irradianceTexture = null;
225
+ if (this._irradianceTexture) {
226
+ this._irradianceTexture.dispose();
227
+ this._irradianceTexture = null;
223
228
  }
224
- this.initialized = false;
225
- // this.lights = [];
226
- this.lightData = null;
229
+ this._initializePromise = null;
230
+ this._lights = [];
231
+ this._lightData = null;
227
232
  }
228
233
  }