@needle-tools/materialx 1.4.6 → 1.5.0

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.
Binary file
package/bin/revision.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "commit": "309ccca5d7788f90d773248c88498ddc203dc260",
2
+ "commit": "6d829c8998eac38f6a1bcc2adf7c4851a62e69c4",
3
3
  "version": "1.39.4",
4
- "buildDate": "2026-02-27 14:35:28"
4
+ "buildDate": "2026-03-19 09:55:05"
5
5
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@needle-tools/materialx",
3
3
  "description": "MaterialX material support for three.js and Needle Engine – render physically based MaterialX shaders in the browser via WebAssembly",
4
- "version": "1.4.6",
4
+ "version": "1.5.0",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
@@ -310,9 +310,21 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
310
310
 
311
311
  if (debug) console.log("[MaterialX] Using renderable element for shader generation");
312
312
 
313
- // Check transparency and set context options like the reference
314
- const isTransparent = state.materialXModule.isTransparentSurface(renderableElement, state.materialXGenerator.getTarget());
315
- state.materialXGenContext.getOptions().hwTransparency = isTransparent;
313
+ // Check transparency and alpha mode.
314
+ // getAlphaMode() is the source of truth: it checks both direct gltf_pbr nodes
315
+ // and inner gltf_pbr nodes inside custom shader graph nodegraphs.
316
+ // isTransparentSurface() is a fallback for non-gltf_pbr shaders.
317
+ const target = state.materialXGenerator.getTarget();
318
+ const alphaMode = typeof state.materialXModule.getAlphaMode === "function"
319
+ ? state.materialXModule.getAlphaMode(renderableElement, target)
320
+ : (state.materialXModule.isTransparentSurface(renderableElement, target) ? "blend" : "opaque");
321
+ const isMask = alphaMode === "mask";
322
+ const isBlend = alphaMode === "blend";
323
+ // Both MASK and BLEND need alpha handling in the generated shader.
324
+ const needsAlpha = isMask || isBlend;
325
+
326
+ // hwTransparency must be true for both MASK and BLEND so the shader includes alpha handling code.
327
+ state.materialXGenContext.getOptions().hwTransparency = needsAlpha;
316
328
 
317
329
  // Generate shaders using the element's name path
318
330
  if (debug) console.log("[MaterialX] Generating MaterialX shaders...");
@@ -326,7 +338,10 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
326
338
  shader,
327
339
  context: context || {},
328
340
  parameters: {
329
- transparent: isTransparent,
341
+ // MASK mode: no GL blending (discard handles cutout), BLEND mode: GL blending
342
+ transparent: isBlend,
343
+ // For MASK mode, set alphaTest so Three.js enables alpha testing
344
+ alphaTest: isMask ? 0.0001 : 0,
330
345
  ...options?.parameters,
331
346
  },
332
347
  loaders: loaders,
@@ -337,8 +352,21 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
337
352
  return shaderMaterial;
338
353
 
339
354
  } catch (error) {
340
- // This is a wasm error (an int) that we need to resolve
341
- console.error(`[MaterialX v${VERSION}] Error creating MaterialX material (${materialNodeNameOrIndex}):`, typeof error === "number" ? `CODE ${error}` : error, `\n→ This may be caused by invalid MaterialX XML data or a problem in the shader generation process. Please provide the MaterialX code below when reporting an issue:\n`, mtlx);
355
+ // WASM exceptions arrive as integer pointers. Extract the detailed error message
356
+ // which includes shader compilation errors, line numbers, and error logs.
357
+ let errorMessage = error;
358
+ if (typeof error === "number" && state?.materialXModule) {
359
+ try {
360
+ const getDetailed = state.materialXModule.getExceptionDetailedMessage;
361
+ const getBasic = state.materialXModule.getExceptionMessage;
362
+ errorMessage = (typeof getDetailed === "function" ? getDetailed(error) : null)
363
+ || (typeof getBasic === "function" ? getBasic(error) : null)
364
+ || `WASM exception code ${error}`;
365
+ } catch (_) {
366
+ errorMessage = `WASM exception code ${error}`;
367
+ }
368
+ }
369
+ console.error(`[MaterialX v${VERSION}] Error creating MaterialX material (${materialNodeNameOrIndex}):\n${errorMessage}\n→ MaterialX source:\n`, mtlx);
342
370
  // Return a fallback material with stored MaterialX data
343
371
  const fallbackMaterial = new MeshStandardMaterial();
344
372
  fallbackMaterial.color.set(0xff00ff);
@@ -222,6 +222,11 @@ function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
222
222
  break;
223
223
  case 'samplerCube':
224
224
  case 'string':
225
+ case 'surfaceshader':
226
+ case 'displacementshader':
227
+ case 'volumeshader':
228
+ case 'lightshader':
229
+ // MaterialX closure/shader types — not real uniforms, skip silently
225
230
  break;
226
231
  default:
227
232
  const key = type + ':' + name;
package/src/materialx.js CHANGED
@@ -51,7 +51,7 @@ export async function ready() {
51
51
 
52
52
  // NOTE: This must be a plain string literal (not a template) so that the
53
53
  // makeFilesLocal Vite plugin can statically detect and localize this URL.
54
- const defaultBaseUrl = "https://cdn.needle.tools/static/materialx/1.4.3/";
54
+ const defaultBaseUrl = "https://cdn.needle.tools/static/materialx/1.5.0/";
55
55
 
56
56
  /** @type {Array<string>} */
57
57
  let urls;
@@ -122,11 +122,6 @@ export class MaterialXMaterial extends ShaderMaterial {
122
122
  fragmentShader = fragmentShader.replace(/\bu_envLightIntensity\b/g, 'envMapIntensity');
123
123
 
124
124
  // Capture some vertex shader properties
125
- 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
126
- const uv1_is_vec2 = vertexShader.includes('in vec2 uv1;');
127
- const uv2_is_vec2 = vertexShader.includes('in vec2 uv2;');
128
- const uv3_is_vec2 = vertexShader.includes('in vec2 uv3;');
129
-
130
125
  // Remove `in vec3 position;` and so on since they're already declared by ShaderMaterial
131
126
  vertexShader = vertexShader.replace(/in\s+vec3\s+position;/g, '');
132
127
  vertexShader = vertexShader.replace(/in\s+vec3\s+normal;/g, '');
@@ -143,12 +138,19 @@ export class MaterialXMaterial extends ShaderMaterial {
143
138
  var hasColor = vertexShader.includes('in vec4 color;');
144
139
  vertexShader = vertexShader.replace(/in\s+vec4\s+color;/g, '');
145
140
 
146
- // Patch uv 2-component to 3-component (`texcoord_0 = uv;` needs to be replaced with `texcoord_0 = vec3(uv, 0.0);`)
147
- // TODO what if we actually have a 3-component UV? Not sure what three.js does then
148
- if (!uv_is_vec2) vertexShader = vertexShader.replace(/texcoord_0 = uv;/g, 'texcoord_0 = vec3(uv, 0.0);');
149
- if (!uv1_is_vec2) vertexShader = vertexShader.replace(/texcoord_1 = uv1;/g, 'texcoord_1 = vec3(uv1, 0.0);');
150
- if (!uv2_is_vec2) vertexShader = vertexShader.replace(/texcoord_2 = uv2;/g, 'texcoord_2 = vec3(uv2, 0.0);');
151
- if (!uv3_is_vec2) vertexShader = vertexShader.replace(/texcoord_3 = uv3;/g, 'texcoord_3 = vec3(uv3, 0.0);');
141
+ // Patch uv vec2→vec3. After removing `in vecN uv;` declarations above,
142
+ // Three.js always provides uv/uv1/uv2/uv3 as vec2 attributes. Any vec3
143
+ // assignment from these must be wrapped. This applies unconditionally
144
+ // even if MaterialX originally declared them as vec2, compound displacement
145
+ // functions may still reference them as vec3 internally.
146
+ vertexShader = vertexShader.replace(/\bvec3 (\w+) = uv;/g, 'vec3 $1 = vec3(uv, 0.0);');
147
+ vertexShader = vertexShader.replace(/\bvec3 (\w+) = uv1;/g, 'vec3 $1 = vec3(uv1, 0.0);');
148
+ vertexShader = vertexShader.replace(/\bvec3 (\w+) = uv2;/g, 'vec3 $1 = vec3(uv2, 0.0);');
149
+ vertexShader = vertexShader.replace(/\bvec3 (\w+) = uv3;/g, 'vec3 $1 = vec3(uv3, 0.0);');
150
+ // Also handle non-declaration assignments (e.g. `texcoord_0 = uv;`)
151
+ vertexShader = vertexShader.replace(/(\w+) = uv;/g, (match, name) => {
152
+ return match.includes('vec3') ? match : `${name} = vec3(uv, 0.0);`;
153
+ });
152
154
 
153
155
  // Patch units – seems MaterialX uses different units and we end up with wrong light values?
154
156
  // result.direction = light.position - position;
@@ -175,7 +177,12 @@ export class MaterialXMaterial extends ShaderMaterial {
175
177
  if (hasTangent) defines['USE_TANGENT'] = '';
176
178
  if (hasColor) defines['USE_COLOR'] = '';
177
179
 
178
- // Add Three.js shadow support
180
+ // Detect whether the vertex shader declares the inverse-transpose matrix uniform.
181
+ // Unlit shaders omit this uniform, so shadow code that references it would fail.
182
+ const hasShadowUniforms = vertexShader.includes('u_worldInverseTransposeMatrix');
183
+
184
+ // Add Three.js shadow support (only when the vertex shader has the required uniforms)
185
+ if (hasShadowUniforms) {
179
186
  // Insert shadow pars before main() in vertex shader
180
187
  vertexShader = vertexShader.replace(
181
188
  /void\s+main\s*\(\s*\)\s*\{/,
@@ -192,7 +199,7 @@ void main() {`
192
199
  /(\n\s*)\}(\s*)$/,
193
200
  `$1 // Three.js shadow support
194
201
  $1 vec4 worldPosition = u_worldMatrix * vec4(position, 1.0);
195
- $1 vec3 transformedNormal = mat3(viewMatrix) * normalWorld;
202
+ $1 vec3 transformedNormal = normalize(mat3(viewMatrix) * mat3(u_worldInverseTransposeMatrix) * normal);
196
203
  $1 #include <shadowmap_vertex>
197
204
  $1}$2`
198
205
  );
@@ -313,6 +320,7 @@ void sampleLightSource(LightData light, vec3 position, out lightshader result)`
313
320
  }
314
321
  $2`
315
322
  );
323
+ } // end hasShadowUniforms
316
324
 
317
325
  const isTransparent = init.parameters?.transparent ?? false;
318
326
  materialParameters = {
@@ -14,6 +14,13 @@ export namespace MaterialX {
14
14
  readFromXmlString(doc: Document, xml: string, searchPath?: string): void;
15
15
  loadStandardLibraries(genContext: GenContext): StandardLibrary;
16
16
  isTransparentSurface(renderableElement: any, target: string): boolean;
17
+ /** Returns the alpha mode for a renderable element: "opaque", "mask", or "blend".
18
+ * Inspects the shader node (and its nodegraph implementation) for alpha_mode inputs. */
19
+ getAlphaMode?(renderableElement: any, target: string): string;
20
+ /** Extracts a detailed error message from a WASM exception pointer, including error logs. */
21
+ getExceptionDetailedMessage?(exceptionPtr: number): string;
22
+ /** Extracts a basic error message from a WASM exception pointer. */
23
+ getExceptionMessage?(exceptionPtr: number): string;
17
24
  }
18
25
 
19
26
 
@@ -32,6 +39,8 @@ export namespace MaterialX {
32
39
  export type Document = {
33
40
  setDataLibrary(lib: StandardLibrary): void;
34
41
  importLibrary(lib: Document): void;
42
+ /** Validates the document and returns validation result with error messages. */
43
+ validate?(): { valid: boolean; message: string };
35
44
 
36
45
  getNodes(): Node[];
37
46
  }