@needle-tools/materialx 1.1.1-next.623fc20 → 1.1.1-next.714bc32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/materialx",
3
- "version": "1.1.1-next.623fc20",
3
+ "version": "1.1.1-next.714bc32",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "exports": {
package/src/index.ts CHANGED
@@ -1,3 +1,13 @@
1
1
  export { ready, type MaterialXContext } from "./materialx.js";
2
2
  export { MaterialXMaterial } from "./materialx.material.js";
3
3
  export { MaterialXLoader } from "./loader/loader.three.js";
4
+
5
+
6
+ import { createMaterialXMaterial } from "./loader/loader.three.js";
7
+
8
+ const Experimental_API = {
9
+ createMaterialXMaterial
10
+ }
11
+
12
+
13
+ export { Experimental_API }
@@ -3,6 +3,7 @@ import { GLTFLoader, GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loa
3
3
  import { ready, state, MaterialXContext } from "../materialx.js";
4
4
  import { debug } from "../utils.js";
5
5
  import { MaterialXMaterial } from "../materialx.material.js";
6
+ import { Callbacks as callbacks } from "../materialx.helper.js";
6
7
 
7
8
  // TypeScript interfaces matching the C# data structures
8
9
  export type MaterialX_root_extension = {
@@ -90,36 +91,6 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
90
91
  return this._loadMaterialAsync(materialIndex);
91
92
  }
92
93
 
93
- // Parse the MaterialX document once and cache it
94
- private async _materialXDocumentReady(): Promise<any> {
95
- if (this._documentReadyPromise) {
96
- return this._documentReadyPromise;
97
- }
98
- return this._documentReadyPromise = (async () => {
99
- if (debug) console.log("[MaterialX] Parsing MaterialX root document...");
100
-
101
- // Ensure MaterialX is initialized
102
- await ready();
103
-
104
- if (!state.materialXModule) {
105
- throw new Error("[MaterialX] module failed to initialize");
106
- }
107
-
108
- // Create MaterialX document and parse ALL the XML data from root
109
- const doc = state.materialXModule.createDocument();
110
- doc.setDataLibrary(state.materialXStdLib);
111
-
112
- // Parse all MaterialX XML strings from the root data
113
- const root = this.materialX_root_data
114
- if (root) {
115
- if (debug) console.log(`[MaterialX] Parsing XML for: ${root.name}`);
116
- await state.materialXModule.readFromXmlString(doc, root.mtlx, "");
117
- }
118
-
119
- if (debug) console.log("[MaterialX] root document parsed successfully");
120
- return doc;
121
- })();
122
- }
123
94
 
124
95
  private async _loadMaterialAsync(materialIndex: number): Promise<Material> {
125
96
 
@@ -129,8 +100,61 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
129
100
  // Handle different types of MaterialX data
130
101
  const ext = materialDef.extensions?.[this.name] as MaterialX_material_extension;
131
102
 
132
- if (ext) {
133
- return this._createMaterialXMaterial(materialDef, ext);
103
+ const mtlx = this.materialX_root_data?.mtlx;
104
+
105
+ if (ext && mtlx) {
106
+
107
+ const materialOptions: MaterialXMaterialOptions = {
108
+ ...this.options,
109
+ }
110
+
111
+ if (!materialOptions.parameters) materialOptions.parameters = {};
112
+
113
+ if (materialOptions.parameters?.side === undefined && materialDef.doubleSided !== undefined) {
114
+ materialOptions.parameters.side = materialDef.doubleSided ? DoubleSide : FrontSide;
115
+ }
116
+
117
+ return createMaterialXMaterial(mtlx, ext.name, {
118
+ cacheKey: this.options.cacheKey || "",
119
+ getTexture: async url => {
120
+ // Find the index of the texture in the parser
121
+ const filenameWithoutExt = url.split('/').pop()?.split('.').shift() || '';
122
+
123
+ // Resolve the texture from the MaterialX root extension
124
+ if (this.materialX_root_data) {
125
+ const textures = this.materialX_root_data.textures || [];
126
+ let index = -1;
127
+ for (const texture of textures) {
128
+ // Find the texture by name and use the pointer string to get the index
129
+ if (texture.name === filenameWithoutExt) {
130
+ const ptr = texture.pointer;
131
+ const indexStr = ptr.substring("/textures/".length);
132
+ index = parseInt(indexStr);
133
+
134
+ if (isNaN(index) || index < 0) {
135
+ console.error("[MaterialX] Invalid texture index in pointer:", ptr);
136
+ return;
137
+ }
138
+ else {
139
+ if (debug) console.log("[MaterialX] Texture index found:", index, "for", filenameWithoutExt);
140
+ }
141
+ }
142
+ }
143
+
144
+ if (index < 0) {
145
+ console.error("[MaterialX] Texture not found in parser:", filenameWithoutExt, this.parser.json);
146
+ return;
147
+ }
148
+ return this.parser.getDependency("texture", index);
149
+ }
150
+ return null;
151
+ }
152
+ }, materialOptions, this.context)
153
+ // Cache and return the generated material
154
+ .then(mat => {
155
+ if (mat instanceof MaterialXMaterial) this._generatedMaterials.push(mat);
156
+ return mat;
157
+ })
134
158
  }
135
159
 
136
160
  // Return fallback material instead of null
@@ -139,199 +163,179 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
139
163
  return fallbackMaterial;
140
164
  }
141
165
 
142
- private async _createMaterialXMaterial(material_def: MaterialDefinition, material_extension: MaterialX_material_extension): Promise<Material> {
143
- try {
144
- if (debug) console.log(`Creating MaterialX material: ${material_extension.name}`);
166
+ }
145
167
 
146
- const doc = await this._materialXDocumentReady();
147
168
 
148
- if (!state.materialXModule || !state.materialXGenerator || !state.materialXGenContext) {
149
- console.warn("[MaterialX] WASM module not ready, returning fallback material");
150
- const fallbackMaterial = new MeshStandardMaterial();
151
- fallbackMaterial.userData.materialX = material_extension;
152
- fallbackMaterial.name = `MaterialX_Fallback_${material_extension.name}`;
153
- return fallbackMaterial;
154
- }
169
+ type MaterialXMaterialOptions = {
170
+ parameters?: MaterialParameters;
171
+ }
155
172
 
156
- // Find the renderable element following MaterialX example pattern exactly
157
- let renderableElement: any = null;
158
- let foundRenderable = false;
159
-
160
- if (debug) console.log("[MaterialX] document", doc);
161
-
162
- // Search for material nodes first (following the reference pattern)
163
- const materialNodes = doc.getMaterialNodes();
164
- if (debug) console.log(`[MaterialX] Found ${materialNodes.length} material nodes in document`, materialNodes);
165
-
166
- // Handle both array and vector-like APIs
167
- const materialNodesLength = materialNodes.length;
168
- for (let i = 0; i < materialNodesLength; ++i) {
169
- const materialNode = materialNodes[i];
170
- if (materialNode) {
171
- const materialName = materialNode.getNamePath();
172
- if (debug) console.log('[MaterialX] Scan material: ', i, materialName);
173
-
174
- // Find the matching material
175
- if (materialName == material_extension.name) {
176
- renderableElement = materialNode;
177
- foundRenderable = true;
178
- if (debug) console.log('[MaterialX] -- add material: ', materialName);
179
- break;
180
- }
173
+ /**
174
+ * Add the MaterialXLoader to the GLTFLoader instance.
175
+ */
176
+ export function useNeedleMaterialX(loader: GLTFLoader, options?: MaterialXLoaderOptions, context?: MaterialXContext) {
177
+ loader.register(p => {
178
+ const loader = new MaterialXLoader(p, options || {}, context || {});
179
+ return loader;
180
+ });
181
+ }
182
+
183
+
184
+
185
+
186
+ // Parse the MaterialX document once and cache it
187
+ async function load(mtlx: string): Promise<any> {
188
+ // Ensure MaterialX is initialized
189
+ await ready();
190
+ if (!state.materialXModule) {
191
+ throw new Error("[MaterialX] module failed to initialize");
192
+ }
193
+ // Create MaterialX document and parse ALL the XML data from root
194
+ const doc = state.materialXModule.createDocument();
195
+ doc.setDataLibrary(state.materialXStdLib);
196
+ // Parse all MaterialX XML strings from the root data
197
+ await state.materialXModule.readFromXmlString(doc, mtlx, "");
198
+ if (debug) console.log("[MaterialX] root document parsed successfully");
199
+ return doc;
200
+ }
201
+
202
+ export async function createMaterialXMaterial(mtlx: string, materialNodeName: string, loaders: callbacks, options?: MaterialXLoaderOptions, context?: MaterialXContext): Promise<Material> {
203
+ try {
204
+ if (debug) console.log(`Creating MaterialX material: ${materialNodeName}`);
205
+
206
+ const doc = await load(mtlx);
207
+
208
+ if (!state.materialXModule || !state.materialXGenerator || !state.materialXGenContext) {
209
+ console.warn("[MaterialX] WASM module not ready, returning fallback material");
210
+ const fallbackMaterial = new MeshStandardMaterial();
211
+ fallbackMaterial.name = `MaterialX_Fallback_${materialNodeName}`;
212
+ return fallbackMaterial;
213
+ }
214
+
215
+ // Find the renderable element following MaterialX example pattern exactly
216
+ let renderableElement: any = null;
217
+ let foundRenderable = false;
218
+
219
+ if (debug) console.log("[MaterialX] document", doc);
220
+
221
+ // Search for material nodes first (following the reference pattern)
222
+ const materialNodes = doc.getMaterialNodes();
223
+ if (debug) console.log(`[MaterialX] Found ${materialNodes.length} material nodes in document`, materialNodes);
224
+
225
+ // Handle both array and vector-like APIs
226
+ for (let i = 0; i < materialNodes.length; ++i) {
227
+ const materialNode = materialNodes[i];
228
+ if (materialNode) {
229
+ const name = materialNode.getNamePath();
230
+ if (debug) console.log(`[MaterialX] Scan material[${i}]: ${name}`);
231
+
232
+ // Find the matching material
233
+ if (materialNodes.length === 1 || name == materialNodeName) {
234
+ materialNodeName = name;
235
+ renderableElement = materialNode;
236
+ foundRenderable = true;
237
+ if (debug) console.log(`[MaterialX] Use material node: '${name}'`);
238
+ break;
181
239
  }
182
240
  }
241
+ }
183
242
 
184
- /*
185
- // If no material nodes found, search nodeGraphs
186
- if (!foundRenderable) {
187
- const nodeGraphs = doc.getNodeGraphs();
188
- console.log(`Found ${nodeGraphs.length} node graphs in document`);
189
- const nodeGraphsLength = nodeGraphs.length;
190
- for (let i = 0; i < nodeGraphsLength; ++i) {
191
- const nodeGraph = nodeGraphs[i];
192
- if (nodeGraph) {
193
- // Skip any nodegraph that has nodedef or sourceUri
194
- if ((nodeGraph as any).hasAttribute('nodedef') || (nodeGraph as any).hasSourceUri()) {
195
- continue;
196
- }
197
- // Skip any nodegraph that is connected to something downstream
198
- if ((nodeGraph as any).getDownstreamPorts().length > 0) {
199
- continue;
200
- }
201
- const outputs = (nodeGraph as any).getOutputs();
202
- for (let j = 0; j < outputs.length; ++j) {
203
- const output = outputs[j];
204
- if (output && !foundRenderable) {
205
- renderableElement = output;
206
- foundRenderable = true;
207
- break;
208
- }
243
+ /*
244
+ // If no material nodes found, search nodeGraphs
245
+ if (!foundRenderable) {
246
+ const nodeGraphs = doc.getNodeGraphs();
247
+ console.log(`Found ${nodeGraphs.length} node graphs in document`);
248
+ const nodeGraphsLength = nodeGraphs.length;
249
+ for (let i = 0; i < nodeGraphsLength; ++i) {
250
+ const nodeGraph = nodeGraphs[i];
251
+ if (nodeGraph) {
252
+ // Skip any nodegraph that has nodedef or sourceUri
253
+ if ((nodeGraph as any).hasAttribute('nodedef') || (nodeGraph as any).hasSourceUri()) {
254
+ continue;
255
+ }
256
+ // Skip any nodegraph that is connected to something downstream
257
+ if ((nodeGraph as any).getDownstreamPorts().length > 0) {
258
+ continue;
259
+ }
260
+ const outputs = (nodeGraph as any).getOutputs();
261
+ for (let j = 0; j < outputs.length; ++j) {
262
+ const output = outputs[j];
263
+ if (output && !foundRenderable) {
264
+ renderableElement = output;
265
+ foundRenderable = true;
266
+ break;
209
267
  }
210
- if (foundRenderable) break;
211
268
  }
269
+ if (foundRenderable) break;
212
270
  }
213
271
  }
214
-
215
- // If still no element found, search document outputs
216
- if (!foundRenderable) {
217
- const outputs = doc.getOutputs();
218
- console.log(`Found ${outputs.length} output nodes in document`);
219
- const outputsLength = outputs.length;
220
- for (let i = 0; i < outputsLength; ++i) {
221
- const output = outputs[i];
222
- if (output && !foundRenderable) {
223
- renderableElement = output;
224
- foundRenderable = true;
225
- break;
226
- }
272
+ }
273
+
274
+ // If still no element found, search document outputs
275
+ if (!foundRenderable) {
276
+ const outputs = doc.getOutputs();
277
+ console.log(`Found ${outputs.length} output nodes in document`);
278
+ const outputsLength = outputs.length;
279
+ for (let i = 0; i < outputsLength; ++i) {
280
+ const output = outputs[i];
281
+ if (output && !foundRenderable) {
282
+ renderableElement = output;
283
+ foundRenderable = true;
284
+ break;
227
285
  }
228
286
  }
229
- */
230
-
231
- if (!renderableElement) {
232
- console.warn(`[MaterialX] No renderable element found in MaterialX document (${material_extension.name})`);
233
- const fallbackMaterial = new MeshStandardMaterial();
234
- fallbackMaterial.color.set(0xff00ff);
235
- fallbackMaterial.userData.materialX = material_extension;
236
- fallbackMaterial.name = `MaterialX_NoRenderable_${material_extension.name}`;
237
- return fallbackMaterial;
238
- }
239
-
240
- if (debug) console.log("[MaterialX] Using renderable element for shader generation");
241
-
242
- // Check transparency and set context options like the reference
243
- const isTransparent = state.materialXModule.isTransparentSurface(renderableElement, state.materialXGenerator.getTarget());
244
- state.materialXGenContext.getOptions().hwTransparency = isTransparent;
245
-
246
- // Generate shaders using the element's name path
247
- if (debug) console.log("[MaterialX] Generating MaterialX shaders...");
248
- const elementName = (renderableElement as any).getNamePath ? (renderableElement as any).getNamePath() : (renderableElement as any).getName();
249
-
250
- const shader = state.materialXGenerator.generate(elementName, renderableElement, state.materialXGenContext);
251
-
252
- const rootExtension = this.materialX_root_data;
253
-
254
- const shaderInfo = rootExtension && material_extension.shader !== undefined && material_extension.shader >= 0
255
- ? rootExtension.shaders?.[material_extension.shader]
256
- : null;
257
-
258
- const shaderMaterial = new MaterialXMaterial({
259
- name: material_extension.name,
260
- shaderName: shaderInfo?.originalName || shaderInfo?.name || null,
261
- shader,
262
- context: this.context,
263
- parameters: {
264
- transparent: isTransparent,
265
- side: material_def.doubleSided ? DoubleSide : FrontSide,
266
- ...this.options.parameters,
267
- },
268
- loaders: {
269
- cacheKey: this.options.cacheKey || "",
270
- getTexture: async url => {
271
- // Find the index of the texture in the parser
272
- const filenameWithoutExt = url.split('/').pop()?.split('.').shift() || '';
273
-
274
- // Resolve the texture from the MaterialX root extension
275
- if (rootExtension) {
276
- const textures = rootExtension.textures || [];
277
- let index = -1;
278
- for (const texture of textures) {
279
- // Find the texture by name and use the pointer string to get the index
280
- if (texture.name === filenameWithoutExt) {
281
- const ptr = texture.pointer;
282
- const indexStr = ptr.substring("/textures/".length);
283
- index = parseInt(indexStr);
284
-
285
- if (isNaN(index) || index < 0) {
286
- console.error("[MaterialX] Invalid texture index in pointer:", ptr);
287
- return;
288
- }
289
- else {
290
- if (debug) console.log("[MaterialX] Texture index found:", index, "for", filenameWithoutExt);
291
- }
292
- }
293
- }
287
+ }
288
+ */
294
289
 
295
- if (index < 0) {
296
- console.error("[MaterialX] Texture not found in parser:", filenameWithoutExt, this.parser.json);
297
- return;
298
- }
299
- return this.parser.getDependency("texture", index).then(tex => {
300
- if (debug) console.log("[MaterialX] Texture loaded:" + tex.name, tex);
301
- return tex;
302
- });
303
- }
304
- return null;
305
- }
306
- }
307
- });
308
- // Track this material for later lighting updates
309
- this._generatedMaterials.push(shaderMaterial);
310
-
311
- // Add debugging to see if the material compiles correctly
312
- if (debug) console.log("[MaterialX] material created:", shaderMaterial.name);
313
- return shaderMaterial;
314
-
315
- } catch (error) {
316
- // This is a wasm error (an int) that we need to resolve
317
- console.error(`[MaterialX] Error creating MaterialX material (${material_extension.name}):`, error);
318
- // Return a fallback material with stored MaterialX data
290
+ if (!renderableElement) {
291
+ console.warn(`[MaterialX] No renderable element found in MaterialX document (${name})`);
319
292
  const fallbackMaterial = new MeshStandardMaterial();
320
293
  fallbackMaterial.color.set(0xff00ff);
321
- fallbackMaterial.userData.materialX = material_extension;
322
- fallbackMaterial.name = `MaterialX_Error_${material_extension.name}`;
294
+ fallbackMaterial.name = `MaterialX_NoRenderable_${name}`;
323
295
  return fallbackMaterial;
324
296
  }
325
- }
326
- }
327
297
 
298
+ if (debug) console.log("[MaterialX] Using renderable element for shader generation");
328
299
 
329
- /**
330
- * Add the MaterialXLoader to the GLTFLoader instance.
331
- */
332
- export function useNeedleMaterialX(loader: GLTFLoader, options?: MaterialXLoaderOptions, context?: MaterialXContext) {
333
- loader.register(p => {
334
- const loader = new MaterialXLoader(p, options || {}, context || {});
335
- return loader;
336
- });
300
+ // Check transparency and set context options like the reference
301
+ const isTransparent = state.materialXModule.isTransparentSurface(renderableElement, state.materialXGenerator.getTarget());
302
+ state.materialXGenContext.getOptions().hwTransparency = isTransparent;
303
+
304
+ // Generate shaders using the element's name path
305
+ if (debug) console.log("[MaterialX] Generating MaterialX shaders...");
306
+ const elementName = (renderableElement as any).getNamePath ? (renderableElement as any).getNamePath() : (renderableElement as any).getName();
307
+
308
+ const shader = state.materialXGenerator.generate(elementName, renderableElement, state.materialXGenContext);
309
+
310
+ // const rootExtension = this.materialX_root_data;
311
+
312
+ // const shaderInfo = rootExtension && material_extension.shader !== undefined && material_extension.shader >= 0
313
+ // ? rootExtension.shaders?.[material_extension.shader]
314
+ // : null;
315
+
316
+ const shaderMaterial = new MaterialXMaterial({
317
+ name: materialNodeName,
318
+ shaderName: null, //shaderInfo?.originalName || shaderInfo?.name || null,
319
+ shader,
320
+ context: context || {},
321
+ parameters: {
322
+ transparent: isTransparent,
323
+ ...options?.parameters,
324
+ },
325
+ loaders: loaders,
326
+ });
327
+
328
+ // Add debugging to see if the material compiles correctly
329
+ if (debug) console.log("[MaterialX] material created:", shaderMaterial.name);
330
+ return shaderMaterial;
331
+
332
+ } catch (error) {
333
+ // This is a wasm error (an int) that we need to resolve
334
+ console.error(`[MaterialX] Error creating MaterialX material (${name}):`, error);
335
+ // Return a fallback material with stored MaterialX data
336
+ const fallbackMaterial = new MeshStandardMaterial();
337
+ fallbackMaterial.color.set(0xff00ff);
338
+ fallbackMaterial.name = `MaterialX_Error_${name}`;
339
+ return fallbackMaterial;
340
+ }
337
341
  }
@@ -70,17 +70,17 @@ function fromMatrix(matrix: MaterialX.Matrix, dimension: MaterialX.Matrix["size"
70
70
  }
71
71
 
72
72
 
73
- export type Loaders = {
73
+ export type Callbacks = {
74
74
  /**
75
75
  * Cache key for the loaders, used to identify and reuse textures
76
76
  */
77
- readonly cacheKey: string;
77
+ readonly cacheKey?: string;
78
78
  /**
79
79
  * Get a texture by path
80
80
  * @param {string} path - The path to the texture
81
81
  * @return {Promise<THREE.Texture>} - A promise that resolves to the texture
82
82
  */
83
- readonly getTexture: (path: string) => Promise<THREE.Texture>;
83
+ readonly getTexture: (path: string) => Promise<THREE.Texture | null | void>;
84
84
  }
85
85
 
86
86
  const defaultTexture = new THREE.Texture();
@@ -119,7 +119,7 @@ function addToCache(key: string, value: any): void {
119
119
  /**
120
120
  * Get Three uniform from MaterialX value
121
121
  */
122
- function toThreeUniform(uniforms: any, type: string, value: any, name: string, loaders: Loaders, searchPath: string): THREE.Uniform {
122
+ function toThreeUniform(uniforms: any, type: string, value: any, name: string, loaders: Callbacks, searchPath: string): THREE.Uniform {
123
123
 
124
124
  const uniform = new THREE.Uniform<any>(null);
125
125
 
@@ -165,7 +165,7 @@ function toThreeUniform(uniforms: any, type: string, value: any, name: string, l
165
165
  checkCache = true;
166
166
  }
167
167
 
168
- const cacheKey = `${loaders.cacheKey}-${texturePath}`;
168
+ const cacheKey = loaders.cacheKey?.length ? `${loaders.cacheKey}-${texturePath}` : texturePath;
169
169
  const cacheValue = checkCache ? tryGetFromCache(cacheKey) : null;
170
170
  if (cacheValue) {
171
171
  if (debug) console.log('[MaterialX] Use cached texture: ', cacheKey, cacheValue);
@@ -180,27 +180,29 @@ function toThreeUniform(uniforms: any, type: string, value: any, name: string, l
180
180
  }
181
181
  }
182
182
  else {
183
- if (name.toLowerCase().includes("normal")) {
184
- uniform.value = defaultNormalTexture;
185
- }
186
- else {
187
- uniform.value = defaultTexture;
188
- }
189
-
190
183
  if (debug) console.log('[MaterialX] Load texture:', texturePath);
184
+
185
+ if (name.toLowerCase().includes("normal")) uniform.value = defaultNormalTexture;
186
+ else uniform.value = defaultTexture;
187
+ const defaultValue = uniform.value;
191
188
  // Save the loading promise in the cache
192
- const promise = loaders.getTexture(texturePath).then(res => {
193
- if (res) {
194
- res = res.clone(); // we need to clone the texture once to avoid colorSpace issues with other materials
195
- res.colorSpace = THREE.LinearSRGBColorSpace;
196
- setTextureParameters(res, name, uniforms);
197
- }
198
- return res;
199
- });
200
- if (checkCache) {
201
- addToCache(cacheKey, promise);
202
- }
203
- promise.then(res => {
189
+ const promise = loaders.getTexture(texturePath)
190
+ ?.then(res => {
191
+ if (res) {
192
+ res = res.clone(); // we need to clone the texture once to avoid colorSpace issues with other materials
193
+ res.colorSpace = THREE.LinearSRGBColorSpace;
194
+ setTextureParameters(res, name, uniforms);
195
+ }
196
+ return res;
197
+ })
198
+ .catch(err => {
199
+ console.error(`[MaterialX] Failed to load texture ${name} '${texturePath}'`, err);
200
+ return defaultValue;
201
+ });
202
+
203
+ if (checkCache) addToCache(cacheKey, promise);
204
+
205
+ promise?.then(res => {
204
206
  if (res) uniform.value = res;
205
207
  else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
206
208
  });
@@ -454,7 +456,7 @@ export function getLightData(lights: Array<THREE.Light>, genContext: any): { lig
454
456
  /**
455
457
  * Get uniform values for a shader
456
458
  */
457
- export function getUniformValues(shaderStage: MaterialX.ShaderStage, loaders: Loaders, searchPath: string) {
459
+ export function getUniformValues(shaderStage: MaterialX.ShaderStage, loaders: Callbacks, searchPath: string) {
458
460
  const threeUniforms = {};
459
461
 
460
462
  const uniformBlocks = shaderStage.getUniformBlocks()
@@ -468,8 +470,8 @@ export function getUniformValues(shaderStage: MaterialX.ShaderStage, loaders: Lo
468
470
  const value = variable.getValue()?.getData();
469
471
  const uniformName = variable.getVariable();
470
472
  const type = variable.getType().getName();
471
- if (debug) console.log("Adding uniform", { path: variable.getPath(), name: uniformName, value: value, type: type });
472
473
  threeUniforms[uniformName] = toThreeUniform(uniforms, type, value, uniformName, loaders, searchPath);
474
+ if (debug) console.log("Adding uniform", { path: variable.getPath(), type: type, name: uniformName, value: threeUniforms[uniformName], },);
473
475
  }
474
476
  }
475
477
  }
@@ -501,22 +503,27 @@ export function generateMaterialPropertiesForUniforms(material: THREE.ShaderMate
501
503
  break;
502
504
  }
503
505
  if (key) {
504
- Object.defineProperty(material, key, {
505
- get: function () {
506
- return this.uniforms?.[uniformName].value
507
- },
508
- set: function (v) {
509
- const uniforms = this.uniforms;
510
- if (!uniforms || !uniforms[uniformName]) {
511
- console.warn(`[MaterialX] Uniform ${uniformName} not found in ${this.name} uniforms`);
512
- return;
506
+ if (material.hasOwnProperty(key)) {
507
+ if (debug) console.warn(`[MaterialX] Uniform ${uniformName} already exists in material as property ${key}, skipping.`);
508
+ }
509
+ else {
510
+ Object.defineProperty(material, key, {
511
+ get: function () {
512
+ return this.uniforms?.[uniformName].value
513
+ },
514
+ set: function (v) {
515
+ const uniforms = this.uniforms;
516
+ if (!uniforms || !uniforms[uniformName]) {
517
+ console.warn(`[MaterialX] Uniform ${uniformName} not found in ${this.name} uniforms`);
518
+ return;
519
+ }
520
+ this.uniforms[uniformName].value = v;
521
+ this.uniformsNeedUpdate = true;
513
522
  }
514
- this.uniforms[uniformName].value = v;
515
- this.uniformsNeedUpdate = true;
516
- }
517
- });
523
+ });
524
+ }
518
525
  }
519
526
  }
520
527
  }
521
528
  }
522
- }
529
+ }
@@ -1,7 +1,7 @@
1
1
  import { BufferGeometry, Camera, FrontSide, GLSL3, Group, IUniform, MaterialParameters, Matrix3, Matrix4, Object3D, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
2
2
  import { debug, getFrame, getTime } from "./utils.js";
3
3
  import { MaterialXContext, MaterialXEnvironment } from "./materialx.js";
4
- import { generateMaterialPropertiesForUniforms, getUniformValues, Loaders } from "./materialx.helper.js";
4
+ import { generateMaterialPropertiesForUniforms, getUniformValues, Callbacks } from "./materialx.helper.js";
5
5
  import { cloneUniforms, cloneUniformsGroups } from "three/src/renderers/shaders/UniformsUtils.js";
6
6
 
7
7
 
@@ -13,10 +13,11 @@ declare type MaterialXMaterialInitParameters = {
13
13
  name: string,
14
14
  shaderName?: string | null, // Optional name of the shader
15
15
  shader: any,
16
- loaders: Loaders,
16
+ loaders: Callbacks,
17
17
  context: MaterialXContext,
18
18
  // Optional parameters
19
19
  parameters?: MaterialParameters,
20
+ debug?: boolean,
20
21
  }
21
22
 
22
23
  type Uniforms = Record<string, IUniform & { needsUpdate?: boolean }>;
@@ -25,7 +26,7 @@ type Precision = "highp" | "mediump" | "lowp";
25
26
  export class MaterialXMaterial extends ShaderMaterial {
26
27
 
27
28
  /** The original name of the shader */
28
- readonly shaderName : string | null = null;
29
+ readonly shaderName: string | null = null;
29
30
 
30
31
  copy(source: MaterialXMaterial): this {
31
32
  super.copy(source);
@@ -43,6 +44,7 @@ export class MaterialXMaterial extends ShaderMaterial {
43
44
 
44
45
  private _context: MaterialXContext | null = null;
45
46
  private _shader: any;
47
+ private _needsTangents: boolean = false;
46
48
 
47
49
  constructor(init?: MaterialXMaterialInitParameters) {
48
50
 
@@ -75,7 +77,8 @@ export class MaterialXMaterial extends ShaderMaterial {
75
77
  vertexShader = vertexShader.replace(/\bi_color_0\b/g, 'color');
76
78
 
77
79
  // Patch fragmentShader
78
- const precision = init.parameters?.precision || "highp" as Precision;
80
+ const precision: Precision = init.parameters?.precision || "highp" as Precision;
81
+ vertexShader = vertexShader.replace(/precision mediump float;/g, `precision ${precision} float;`);
79
82
  fragmentShader = fragmentShader.replace(/precision mediump float;/g, `precision ${precision} float;`);
80
83
  fragmentShader = fragmentShader.replace(/#define M_FLOAT_EPS 1e-8/g, precision === "highp" ? `#define M_FLOAT_EPS 1e-8` : `#define M_FLOAT_EPS 1e-3`);
81
84
 
@@ -88,9 +91,16 @@ export class MaterialXMaterial extends ShaderMaterial {
88
91
  fragmentShader = fragmentShader.replace(/\bi_tangent\b/g, 'tangent');
89
92
  fragmentShader = fragmentShader.replace(/\bi_color_0\b/g, 'color');
90
93
 
94
+ // Capture some vertex shader properties
95
+ const uv_is_vec2 = vertexShader.includes('in vec2 uv;'); // check if uv is vec2; e.g. https://matlib.gpuopen.com/main/materials/all?material=da6ec531-f5c1-4790-ac14-8a5c51d0314e
96
+ const uv1_is_vec2 = vertexShader.includes('in vec2 uv1;');
97
+ const uv2_is_vec2 = vertexShader.includes('in vec2 uv2;');
98
+ const uv3_is_vec2 = vertexShader.includes('in vec2 uv3;');
99
+
91
100
  // Remove `in vec3 position;` and so on since they're already declared by ShaderMaterial
92
101
  vertexShader = vertexShader.replace(/in\s+vec3\s+position;/g, '');
93
102
  vertexShader = vertexShader.replace(/in\s+vec3\s+normal;/g, '');
103
+ vertexShader = vertexShader.replace(/in\s+vec2\s+uv;/g, '');
94
104
  vertexShader = vertexShader.replace(/in\s+vec3\s+uv;/g, '');
95
105
  var hasUv1 = vertexShader.includes('in vec3 uv1;');
96
106
  vertexShader = vertexShader.replace(/in\s+vec3\s+uv1;/g, '');
@@ -105,10 +115,10 @@ export class MaterialXMaterial extends ShaderMaterial {
105
115
 
106
116
  // Patch uv 2-component to 3-component (`texcoord_0 = uv;` needs to be replaced with `texcoord_0 = vec3(uv, 0.0);`)
107
117
  // TODO what if we actually have a 3-component UV? Not sure what three.js does then
108
- vertexShader = vertexShader.replace(/texcoord_0 = uv;/g, 'texcoord_0 = vec3(uv, 0.0);');
109
- vertexShader = vertexShader.replace(/texcoord_1 = uv1;/g, 'texcoord_1 = vec3(uv1, 0.0);');
110
- vertexShader = vertexShader.replace(/texcoord_2 = uv2;/g, 'texcoord_2 = vec3(uv2, 0.0);');
111
- vertexShader = vertexShader.replace(/texcoord_3 = uv3;/g, 'texcoord_3 = vec3(uv3, 0.0);');
118
+ if (!uv_is_vec2) vertexShader = vertexShader.replace(/texcoord_0 = uv;/g, 'texcoord_0 = vec3(uv, 0.0);');
119
+ if (!uv1_is_vec2) vertexShader = vertexShader.replace(/texcoord_1 = uv1;/g, 'texcoord_1 = vec3(uv1, 0.0);');
120
+ if (!uv2_is_vec2) vertexShader = vertexShader.replace(/texcoord_2 = uv2;/g, 'texcoord_2 = vec3(uv2, 0.0);');
121
+ if (!uv3_is_vec2) vertexShader = vertexShader.replace(/texcoord_3 = uv3;/g, 'texcoord_3 = vec3(uv3, 0.0);');
112
122
 
113
123
  // Patch units – seems MaterialX uses different units and we end up with wrong light values?
114
124
  // result.direction = light.position - position;
@@ -148,9 +158,10 @@ export class MaterialXMaterial extends ShaderMaterial {
148
158
  defines: defines,
149
159
  ...init.parameters, // Spread any additional parameters passed to the material
150
160
  });
161
+ this.shaderName = init.shaderName || null;
151
162
  this._context = init.context;
152
163
  this._shader = init.shader;
153
- this.shaderName = init.shaderName || null;
164
+ this._needsTangents = vertexShader.includes('in vec4 tangent;') || vertexShader.includes('in vec3 tangent;');
154
165
 
155
166
  Object.assign(this.uniforms, {
156
167
  ...getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath),
@@ -161,6 +172,9 @@ export class MaterialXMaterial extends ShaderMaterial {
161
172
  u_viewPosition: { value: new Vector3() },
162
173
  u_worldInverseTransposeMatrix: { value: new Matrix4() },
163
174
 
175
+ // u_shadowMatrix: { value: new Matrix4() },
176
+ // u_shadowMap: { value: null, type: 't' }, // Shadow map
177
+
164
178
  u_envMatrix: { value: new Matrix4() },
165
179
  u_envRadiance: { value: null, type: 't' },
166
180
  u_envRadianceMips: { value: 8, type: 'i' },
@@ -176,7 +190,7 @@ export class MaterialXMaterial extends ShaderMaterial {
176
190
  generateMaterialPropertiesForUniforms(this, init.shader.getStage('vertex'));
177
191
 
178
192
 
179
- if (debug) {
193
+ if (debug || init.debug) {
180
194
  // Get lighting and environment data from MaterialX environment
181
195
  console.group("[MaterialX]: ", this.name);
182
196
  console.log(`Vertex shader length: ${vertexShader.length}\n`, vertexShader);
@@ -185,20 +199,28 @@ export class MaterialXMaterial extends ShaderMaterial {
185
199
  }
186
200
  }
187
201
 
188
- onBeforeRender(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, _geometry: BufferGeometry, object: Object3D, _group: Group): void {
202
+ private _missingTangentsWarned: boolean = false;
203
+ onBeforeRender(renderer: WebGLRenderer, _scene: Scene, camera: Camera, geometry: BufferGeometry, object: Object3D, _group: Group): void {
204
+ if (this._needsTangents && !geometry.attributes.tangent) {
205
+ if (!this._missingTangentsWarned) {
206
+ this._missingTangentsWarned = true;
207
+ console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but not present in the geometry.`);
208
+ // TODO: can we compute tangents here?
209
+ }
210
+ }
189
211
  const time = this._context?.getTime?.() || getTime();
190
212
  const frame = this._context?.getFrame?.() || getFrame();
191
213
  const env = MaterialXEnvironment.get(_scene);
192
214
  if (env) {
193
- env.update(frame, _scene, _renderer);
194
- this.updateUniforms(env, object, camera, time, frame);
215
+ env.update(frame, _scene, renderer);
216
+ this.updateUniforms(env, renderer, object, camera, time, frame);
195
217
  }
196
218
  }
197
219
 
198
220
 
199
221
  envMapIntensity: number = 1.0; // Default intensity for environment map
200
222
  envMap: Texture | null = null; // Environment map texture, can be set externally
201
- updateUniforms = (environment: MaterialXEnvironment, object: Object3D, camera: Camera, time?: number, frame?: number) => {
223
+ updateUniforms = (environment: MaterialXEnvironment, renderer: WebGLRenderer, object: Object3D, camera: Camera, time?: number, frame?: number) => {
202
224
 
203
225
  const uniforms = this.uniforms as Uniforms;
204
226
 
@@ -213,15 +235,23 @@ export class MaterialXMaterial extends ShaderMaterial {
213
235
  uniforms.u_viewProjectionMatrix.needsUpdate = true;
214
236
  }
215
237
 
238
+ if (uniforms.u_worldInverseTransposeMatrix) {
239
+ uniforms.u_worldInverseTransposeMatrix.value.setFromMatrix3(normalMat.getNormalMatrix(object.matrixWorld));
240
+ uniforms.u_worldInverseTransposeMatrix.needsUpdate = true;
241
+ }
242
+
216
243
  if (uniforms.u_viewPosition) {
217
244
  uniforms.u_viewPosition.value.copy(camera.getWorldPosition(worldViewPos));
218
245
  uniforms.u_viewPosition.needsUpdate = true;
219
246
  }
220
247
 
221
- if (uniforms.u_worldInverseTransposeMatrix) {
222
- uniforms.u_worldInverseTransposeMatrix.value.setFromMatrix3(normalMat.getNormalMatrix(object.matrixWorld));
223
- uniforms.u_worldInverseTransposeMatrix.needsUpdate = true;
224
- }
248
+ // if (uniforms.u_shadowMap) {
249
+ // const light = environment.lights?.[2] || null;
250
+ // uniforms.u_shadowMatrix.value = light?.shadow?.matrix.clone().premultiply(object.matrixWorld.clone()).invert();
251
+ // uniforms.u_shadowMap.value = light.shadow?.map || null;
252
+ // uniforms.u_shadowMap.needsUpdate = true;
253
+ // console.log("[MaterialX] Renderer shadow map updated", light);
254
+ // }
225
255
 
226
256
  // Update time uniforms
227
257
  if (uniforms.u_time) {
package/src/materialx.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { MaterialX as MX } from "./materialx.types.js";
2
2
  import MaterialX from "../bin/JsMaterialXGenShader.js";
3
3
  import { debug } from "./utils.js";
4
- import { renderPMREMToEquirect } from "./textureHelper.js";
4
+ import { renderPMREMToEquirect } from "./utils.texture.js";
5
5
  import { Light, Mesh, MeshBasicMaterial, Object3D, PlaneGeometry, PMREMGenerator, Scene, Texture, WebGLRenderer } from "three";
6
6
  import { registerLights, getLightData, LightData } from "./materialx.helper.js";
7
7
  import type { MaterialXMaterial } from "./materialx.material.js";
@@ -95,6 +95,8 @@ export async function ready(): Promise<void> {
95
95
  // Set a reasonable default for max active lights
96
96
  state.materialXGenContext.getOptions().hwMaxActiveLightSources = 4;
97
97
 
98
+ // state.materialXGenContext.getOptions().hwShadowMap = true;
99
+
98
100
  // This prewarms the shader generation context to have all light types
99
101
  await registerLights(state.materialXModule, state.materialXGenContext);
100
102
 
@@ -209,6 +211,9 @@ export class MaterialXEnvironment {
209
211
  this._texturesCache.clear();
210
212
  }
211
213
 
214
+ get lights() {
215
+ return this._lights;
216
+ }
212
217
  get lightData() {
213
218
  return this._lightData;
214
219
  }
package/src/utils.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // import { BufferGeometry } from 'three';
2
+ // import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
+
4
+
1
5
  export function getParam(name: string) {
2
6
  const urlParams = new URLSearchParams(window.location.search);
3
7
  const param = urlParams.get(name);
File without changes