@needle-tools/materialx 1.6.0-next.2af5fc1 → 1.7.0-next.0d06218

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": "0e7685f37737511f2816949b9486d511a5fa71bd",
3
- "version": "1.39.4",
4
- "buildDate": "2026-04-01 14:56:42"
2
+ "commit": "ab218c56f016a9a2d398e8d306f3aeb439ae9e9e",
3
+ "version": "1.39.5",
4
+ "buildDate": "2026-05-21 15:22:48"
5
5
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
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.6.0-next.2af5fc1",
4
+ "version": "1.7.0-next.0d06218",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "types": "index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./index.d.ts",
10
11
  "import": "./index.js",
11
- "require": "./index.js",
12
- "types": "./index.d.ts"
12
+ "require": "./index.js"
13
13
  },
14
14
  "./needle": {
15
+ "types": "./needle.d.ts",
15
16
  "import": "./needle.js",
16
- "require": "./needle.js",
17
- "types": "./needle.d.ts"
17
+ "require": "./needle.js"
18
18
  },
19
19
  "./package.json": "./package.json",
20
20
  "./codegen/register_types.ts": {
@@ -66,5 +66,9 @@
66
66
  "pbr",
67
67
  "3d",
68
68
  "wasm"
69
- ]
69
+ ],
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "https://github.com/needle-tools/needle-engine-materialx.git"
73
+ }
70
74
  }
package/src/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { ready, type MaterialXContext, preloadWasm } from "./materialx.js";
1
+ export { ready, type MaterialXContext, type MaterialXEnvironmentRadianceMode, preloadWasm } from "./materialx.js";
2
2
  export { MaterialXEnvironment } from "./materialx.js";
3
3
  export { MaterialXMaterial } from "./materialx.material.js";
4
4
  export { MaterialXLoader } from "./loader/loader.three.js";
@@ -10,4 +10,3 @@ declare const Experimental_API: {
10
10
  };
11
11
 
12
12
  export { Experimental_API };
13
-
@@ -1,6 +1,7 @@
1
1
  import { Material, MaterialParameters } from "three";
2
2
  import { GLTFLoader, GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
  import { MaterialXContext } from "../materialx.js";
4
+ import type { MaterialXEnvironmentRadianceMode } from "../materialx.js";
4
5
  import { MaterialXMaterial } from "../materialx.material.js";
5
6
  import { Callbacks } from "../materialx.helper.js";
6
7
 
@@ -39,6 +40,14 @@ export interface MaterialXLoaderOptions {
39
40
  cacheKey?: string;
40
41
  /** Parameters for the MaterialX loader */
41
42
  parameters?: Pick<MaterialParameters, "precision">;
43
+ /** Environment radiance backend. Defaults to direct Three.js CubeUV PMREM sampling. */
44
+ environmentRadianceMode?: MaterialXEnvironmentRadianceMode;
45
+ /** Match Three.js glossy specular antialiasing. Defaults to true. */
46
+ specularAntialiasing?: boolean;
47
+ /** Flip texcoord V at the MaterialX texcoord node output. Defaults to false for standalone .mtlx creation and true for GLTFLoader integration. */
48
+ hwTexcoordVerticalFlip?: boolean;
49
+ /** Flip texcoord V inside MaterialX file texture sampling. Defaults to false for standalone .mtlx creation and true for GLTFLoader integration. */
50
+ fileTextureVerticalFlip?: boolean;
42
51
  }
43
52
 
44
53
  export declare class MaterialXLoader implements GLTFLoaderPlugin {
@@ -71,7 +80,7 @@ export declare function useNeedleMaterialX(
71
80
  export declare function createMaterialXMaterial(
72
81
  mtlx: string,
73
82
  materialNodeName: string | number,
74
- loaders: Callbacks,
83
+ loaders?: Callbacks,
75
84
  options?: MaterialXLoaderOptions,
76
85
  context?: MaterialXContext
77
86
  ): Promise<Material>;
@@ -61,7 +61,11 @@ export class MaterialXLoader {
61
61
  */
62
62
  constructor(parser, options, context) {
63
63
  this.parser = parser;
64
- this.options = options;
64
+ this.options = {
65
+ ...options,
66
+ hwTexcoordVerticalFlip: options?.hwTexcoordVerticalFlip ?? true,
67
+ fileTextureVerticalFlip: options?.fileTextureVerticalFlip ?? true,
68
+ };
65
69
  this.context = context;
66
70
 
67
71
  if (debug) console.log("MaterialXLoader created for parser");
@@ -182,7 +186,7 @@ export function useNeedleMaterialX(loader, options, context) {
182
186
  /**
183
187
  * Parse the MaterialX document once and cache it
184
188
  * @param {string} mtlx
185
- * @returns {Promise<any>}
189
+ * @returns {Promise<import("../materialx.types.js").MaterialX.Document>}
186
190
  */
187
191
  async function load(mtlx) {
188
192
  // Ensure MaterialX is initialized
@@ -202,7 +206,7 @@ async function load(mtlx) {
202
206
  /**
203
207
  * @param {string} mtlx
204
208
  * @param {string | number} materialNodeNameOrIndex
205
- * @param {import('../materialx.helper.js').Callbacks} loaders
209
+ * @param {import('../materialx.helper.js').Callbacks} [loaders]
206
210
  * @param {MaterialXLoaderOptions} [options]
207
211
  * @param {import('../materialx.js').MaterialXContext} [context]
208
212
  * @returns {Promise<Material>}
@@ -210,6 +214,10 @@ async function load(mtlx) {
210
214
  export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loaders, options, context) {
211
215
  try {
212
216
  if (debug) console.log(`Creating MaterialX material: ${materialNodeNameOrIndex}`);
217
+ loaders ??= {
218
+ getTexture: async () => null,
219
+ };
220
+
213
221
 
214
222
  const doc = await load(mtlx);
215
223
 
@@ -234,7 +242,7 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
234
242
  for (let i = 0; i < materialNodes.length; ++i) {
235
243
  const materialNode = materialNodes[i];
236
244
  if (materialNode) {
237
- const name = materialNode.getNamePath();
245
+ const name = materialNode.getNamePath?.();
238
246
  if (debug) console.log(`[MaterialX] Scan material[${i}]: ${name}`);
239
247
 
240
248
  // Find the matching material
@@ -320,25 +328,21 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
320
328
  : (state.materialXModule.isTransparentSurface(renderableElement, target) ? "blend" : "opaque");
321
329
  const isMask = alphaMode === "mask";
322
330
  const isBlend = alphaMode === "blend";
331
+ const renderAsTransparent = isBlend;
323
332
  // Both MASK and BLEND need alpha handling in the generated shader.
324
333
  const needsAlpha = isMask || isBlend;
325
334
 
326
- // Emscripten's getOptions() returns a temporary wrapper property writes
327
- // don't persist across separate getOptions() calls. Set all options that
328
- // matter for generation in a single block right before generate().
335
+ // Emscripten's getOptions() returns a temporary wrapper; set all options
336
+ // that matter for generation in one block right before generate().
329
337
  {
330
338
  const opts = state.materialXGenContext.getOptions();
331
339
  // MASK and BLEND need alpha handling in the generated shader.
332
340
  opts.hwTransparency = needsAlpha;
333
- // three.js provides UVs in glTF convention (V=0 top), MaterialX expects
334
- // OpenGL convention (V=0 bottom). Flip at the texcoord node output so all
335
- // UV logic (comparisons, noise, displacement) works correctly.
336
- opts.hwTexcoordVerticalFlip = true;
337
- // three.js GLTFLoader sets texture.flipY=false — textures are in glTF
338
- // convention. With hwTexcoordVerticalFlip flipping UV data, texture
339
- // sampling needs to flip back to match. fileTextureVerticalFlip does
340
- // this inside mx_transform_uv.
341
- opts.fileTextureVerticalFlip = true;
341
+ opts.hwTexcoordVerticalFlip = options?.hwTexcoordVerticalFlip ?? false;
342
+ opts.fileTextureVerticalFlip = options?.fileTextureVerticalFlip ?? false;
343
+ opts.hwSpecularEnvironmentMethod = options?.environmentRadianceMode === "materialx-fis"
344
+ ? state.materialXModule.HwSpecularEnvironmentMethod.SPECULAR_ENVIRONMENT_FIS
345
+ : state.materialXModule.HwSpecularEnvironmentMethod.SPECULAR_ENVIRONMENT_PREFILTER;
342
346
  }
343
347
 
344
348
  // Generate shaders using the element's name path
@@ -348,19 +352,22 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
348
352
  const shader = state.materialXGenerator.generate(elementName, renderableElement, state.materialXGenContext);
349
353
 
350
354
  const shaderMaterial = new MaterialXMaterial({
351
- name: materialNodeNameOrIndex,
355
+ name: typeof elementName === "string" ? elementName : `MaterialX_${materialNodeNameOrIndex}`,
352
356
  shaderName: null, //shaderInfo?.originalName || shaderInfo?.name || null,
353
357
  shader,
354
358
  context: context || {},
359
+ environmentRadianceMode: options?.environmentRadianceMode,
360
+ specularAntialiasing: options?.specularAntialiasing,
355
361
  parameters: {
356
- // MASK mode: no GL blending (discard handles cutout), BLEND mode: GL blending
357
- transparent: isBlend,
362
+ // MASK uses discard; BLEND uses Three.js transparent sorting and blending.
363
+ transparent: renderAsTransparent,
358
364
  // For MASK mode, set alphaTest so Three.js enables alpha testing
359
365
  alphaTest: isMask ? 0.0001 : 0,
360
366
  ...options?.parameters,
361
367
  },
362
368
  loaders: loaders,
363
369
  });
370
+ await shaderMaterial.ready;
364
371
 
365
372
  // Add debugging to see if the material compiles correctly
366
373
  if (debug) console.log("[MaterialX] material created:", shaderMaterial.name);
@@ -34,13 +34,14 @@ export type MaterialXContext = {
34
34
  type EnvironmentTextureSet = {
35
35
  radianceTexture: Texture | null;
36
36
  irradianceTexture: Texture | null;
37
+ dispose?: () => void;
37
38
  }
38
39
 
39
40
  export declare const state: {
40
41
  materialXModule: MX.MODULE | null;
41
- materialXGenerator: any | null;
42
- materialXGenContext: any | null;
43
- materialXStdLib: any | null;
42
+ materialXGenerator: MX.MODULE["HwShaderGenerator"] | null;
43
+ materialXGenContext: MX.GenContext | null;
44
+ materialXStdLib: MX.StandardLibrary | null;
44
45
  materialXInitPromise: Promise<void> | null;
45
46
  };
46
47
 
@@ -83,8 +84,11 @@ export declare class MaterialXEnvironment {
83
84
 
84
85
  private _pmremGenerator: any | null;
85
86
  private _renderer: WebGLRenderer | null;
86
- private _texturesCache: Map<Texture | null, EnvironmentTextureSet>;
87
+ private _texturesCache: Map<Texture | null, Map<string, EnvironmentTextureSet>>;
87
88
  private _initialize(renderer: WebGLRenderer): Promise<boolean>;
88
- private _getTextures(texture: Texture | null | undefined): EnvironmentTextureSet;
89
+ private _getTextures(texture: Texture | null | undefined, radianceMode?: MaterialXEnvironmentRadianceMode): EnvironmentTextureSet;
90
+ private _getPMREMGenerator(): any;
89
91
  private updateLighting(collectLights?: boolean): void;
90
92
  }
93
+
94
+ export type MaterialXEnvironmentRadianceMode = "three-pmrem" | "materialx-prefiltered" | "materialx-fis";
@@ -28,6 +28,6 @@ export function registerLights(mx: any, genContext: any): Promise<void>;
28
28
 
29
29
  export function getLightData(lights: Array<THREE.Light>, genContext: any): { lightData: LightData[], lightCount: number };
30
30
 
31
- export function getUniformValues(shaderStage: any, loaders: Callbacks, searchPath: string): Record<string, THREE.Uniform>;
31
+ export function getUniformValues(shaderStage: any, loaders?: Callbacks, searchPath?: string): Record<string, THREE.Uniform>;
32
32
 
33
33
  export function generateMaterialPropertiesForUniforms(material: THREE.ShaderMaterial, shaderStage: any): void;
@@ -36,17 +36,11 @@ export function prepareEnvTexture(texture, capabilities) {
36
36
  * @returns {Array<number>}
37
37
  */
38
38
  function fromVector(value, dimension) {
39
- let outValue;
40
- if (value) {
41
- outValue = [...value.data()];
42
- }
43
- else {
44
- outValue = [];
45
- for (let i = 0; i < dimension; ++i)
46
- outValue.push(0.0);
47
- }
48
-
49
- return outValue;
39
+ if (!value) return Array(dimension).fill(0.0);
40
+ if (typeof value.data === "function") return [...value.data()];
41
+ if (typeof value.getData === "function") return fromVector(value.getData(), dimension);
42
+ if (typeof value !== "string" && typeof value[Symbol.iterator] === "function") return [...value];
43
+ return Array(dimension).fill(0.0);
50
44
  }
51
45
 
52
46
  /**
@@ -56,7 +50,7 @@ function fromVector(value, dimension) {
56
50
  * @returns {Array<number>}
57
51
  */
58
52
  function fromMatrix(matrix, dimension) {
59
- const vec = new Array(dimension);
53
+ const vec = [];
60
54
  if (matrix) {
61
55
  for (let i = 0; i < matrix.numRows(); ++i) {
62
56
  for (let k = 0; k < matrix.numColumns(); ++k) {
@@ -125,9 +119,10 @@ function addToCache(key, value) {
125
119
  * @param {string} name
126
120
  * @param {Callbacks} loaders
127
121
  * @param {string} searchPath
122
+ * @param {Array<Promise<unknown>>} [pendingTextureLoads]
128
123
  * @returns {THREE.Uniform}
129
124
  */
130
- function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
125
+ function toThreeUniform(threeUniforms, uniforms, type, value, name, loaders, searchPath, pendingTextureLoads) {
131
126
 
132
127
  const uniform = new THREE.Uniform(/** @type {any} */(null));
133
128
 
@@ -178,13 +173,22 @@ function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
178
173
  if (cacheValue) {
179
174
  if (debug) console.log('[MaterialX] Use cached texture: ', cacheKey, cacheValue);
180
175
  if (cacheValue instanceof Promise) {
181
- cacheValue.then(res => {
182
- if (res) uniform.value = res;
176
+ const trackedLoad = cacheValue.then(res => {
177
+ if (res) {
178
+ uniform.value = res;
179
+ if (threeUniforms[name + "_flipY"]) threeUniforms[name + "_flipY"].value = !!res.flipY;
180
+ }
183
181
  else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
182
+ return res;
183
+ }).catch(err => {
184
+ console.error(`[MaterialX] Failed to load texture ${name} '${texturePath}'`, err);
185
+ return null;
184
186
  });
187
+ pendingTextureLoads?.push(trackedLoad);
185
188
  }
186
189
  else {
187
190
  uniform.value = cacheValue;
191
+ if (threeUniforms[name + "_flipY"]) threeUniforms[name + "_flipY"].value = !!cacheValue.flipY;
188
192
  }
189
193
  }
190
194
  else {
@@ -197,9 +201,9 @@ function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
197
201
  const promise = loaders.getTexture(texturePath)
198
202
  ?.then(res => {
199
203
  if (res) {
200
- res = res.clone(); // we need to clone the texture once to avoid colorSpace issues with other materials
201
204
  res.colorSpace = THREE.LinearSRGBColorSpace;
202
205
  setTextureParameters(res, name, uniforms);
206
+ res.needsUpdate = true;
203
207
  }
204
208
  return res;
205
209
  })
@@ -210,15 +214,21 @@ function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
210
214
 
211
215
  if (checkCache) addToCache(cacheKey, promise);
212
216
 
213
- promise?.then(res => {
217
+ const trackedLoad = promise?.then(res => {
214
218
  // Replace Promise cache entry with the resolved texture value.
215
219
  // This avoids keeping long-lived promise/closure graphs in THREE.Cache.
216
220
  if (checkCache && res) addToCache(cacheKey, res);
217
- if (res) uniform.value = /** @type {any} */ (res);
221
+ if (res) {
222
+ uniform.value = /** @type {any} */ (res);
223
+ if (threeUniforms[name + "_flipY"]) threeUniforms[name + "_flipY"].value = !!res.flipY;
224
+ }
218
225
  else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
226
+ return res;
219
227
  });
228
+ if (trackedLoad) pendingTextureLoads?.push(trackedLoad);
220
229
  }
221
230
  }
231
+ threeUniforms[name + "_flipY"] = new THREE.Uniform(!!uniform.value?.flipY);
222
232
  break;
223
233
  case 'samplerCube':
224
234
  case 'string':
@@ -248,23 +258,69 @@ const valueTypeWarningMap = new Map();
248
258
  * @param {number} mode
249
259
  * @returns {THREE.Wrapping}
250
260
  */
261
+ function getAddressMode(mode) {
262
+ if (typeof mode === "number") return mode;
263
+ switch (String(mode ?? "").toLowerCase()) {
264
+ case "constant":
265
+ return 0;
266
+ case "clamp":
267
+ return 1;
268
+ case "periodic":
269
+ return 2;
270
+ case "mirror":
271
+ return 3;
272
+ default:
273
+ return 2;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Get Three wrapping mode
279
+ * @param {unknown} mode
280
+ * @returns {THREE.Wrapping}
281
+ */
251
282
  function getWrapping(mode) {
252
- let wrap;
253
- switch (mode) {
283
+ switch (getAddressMode(mode)) {
284
+ case 0:
254
285
  case 1:
255
- wrap = THREE.ClampToEdgeWrapping;
256
- break;
257
- case 2:
258
- wrap = THREE.RepeatWrapping;
259
- break;
286
+ return THREE.ClampToEdgeWrapping;
260
287
  case 3:
261
- wrap = THREE.MirroredRepeatWrapping;
262
- break;
288
+ return THREE.MirroredRepeatWrapping;
289
+ case 2:
263
290
  default:
264
- wrap = THREE.RepeatWrapping;
265
- break;
291
+ return THREE.RepeatWrapping;
292
+ }
293
+ }
294
+
295
+ /**
296
+ * @param {unknown} mode
297
+ * @returns {number}
298
+ */
299
+ function getFilterType(mode) {
300
+ if (typeof mode === "number") return mode;
301
+ switch (String(mode ?? "").toLowerCase()) {
302
+ case "closest":
303
+ return 0;
304
+ case "linear":
305
+ return 1;
306
+ default:
307
+ return -1;
266
308
  }
267
- return wrap;
309
+ }
310
+
311
+ /**
312
+ * @param {any} uniforms
313
+ * @param {string} name
314
+ * @param {any} defaultValue
315
+ * @returns {any}
316
+ */
317
+ function getUniformData(uniforms, name, defaultValue) {
318
+ const uniform = uniforms?.find?.(name);
319
+ const value = uniform?.getValue?.();
320
+ if (!value) return defaultValue;
321
+ if (typeof value.getData === "function") return value.getData();
322
+ if (typeof value.data === "function") return value.data();
323
+ return defaultValue;
268
324
  }
269
325
 
270
326
  /**
@@ -278,22 +334,18 @@ function setTextureParameters(texture, name, uniforms, generateMipmaps = true) {
278
334
  const idx = name.lastIndexOf(IMAGE_PROPERTY_SEPARATOR);
279
335
  const base = name.substring(0, idx) || name;
280
336
 
281
- if (uniforms.find(base + UADDRESS_MODE_SUFFIX)) {
282
- const uaddressmode = uniforms.find(base + UADDRESS_MODE_SUFFIX).getValue().getData();
283
- texture.wrapS = getWrapping(uaddressmode);
284
- }
285
-
286
- if (uniforms.find(base + VADDRESS_MODE_SUFFIX)) {
287
- const vaddressmode = uniforms.find(base + VADDRESS_MODE_SUFFIX).getValue().getData();
288
- texture.wrapT = getWrapping(vaddressmode);
289
- }
337
+ texture.wrapS = getWrapping(getUniformData(uniforms, base + UADDRESS_MODE_SUFFIX, 2));
338
+ texture.wrapT = getWrapping(getUniformData(uniforms, base + VADDRESS_MODE_SUFFIX, 2));
290
339
 
291
- const mxFilterType = uniforms.find(base + FILTER_TYPE_SUFFIX) ? uniforms.get(base + FILTER_TYPE_SUFFIX).value : -1;
340
+ const mxFilterType = getFilterType(getUniformData(uniforms, base + FILTER_TYPE_SUFFIX, -1));
292
341
  let minFilter = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
342
+ let magFilter = THREE.LinearFilter;
293
343
  if (mxFilterType === 0) {
294
344
  minFilter = /** @type {any} */ (generateMipmaps ? THREE.NearestMipMapNearestFilter : THREE.NearestFilter);
345
+ magFilter = THREE.NearestFilter;
295
346
  }
296
347
  texture.minFilter = minFilter;
348
+ texture.magFilter = magFilter;
297
349
  }
298
350
 
299
351
  /**
@@ -528,9 +580,12 @@ export function getLightData(lights, genContext) {
528
580
  * @param {any} shaderStage
529
581
  * @param {Callbacks} loaders
530
582
  * @param {string} searchPath
583
+ * @param {Array<Promise<unknown>>} [pendingTextureLoads]
531
584
  * @returns {Object<string, THREE.Uniform>}
532
585
  */
533
- export function getUniformValues(shaderStage, loaders, searchPath) {
586
+ export function getUniformValues(shaderStage, loaders, searchPath, pendingTextureLoads) {
587
+ loaders ??= { getTexture: async () => null };
588
+ searchPath ??= "";
534
589
  /** @type {Object<string, THREE.Uniform>} */
535
590
  const threeUniforms = {};
536
591
 
@@ -545,7 +600,7 @@ export function getUniformValues(shaderStage, loaders, searchPath) {
545
600
  const value = variable.getValue()?.getData();
546
601
  const uniformName = variable.getVariable();
547
602
  const type = variable.getType().getName();
548
- threeUniforms[uniformName] = toThreeUniform(uniforms, type, value, uniformName, loaders, searchPath);
603
+ threeUniforms[uniformName] = toThreeUniform(threeUniforms, uniforms, type, value, uniformName, loaders, searchPath, pendingTextureLoads);
549
604
  if (debug) console.log("Adding uniform", { path: variable.getPath(), type: type, name: uniformName, value: threeUniforms[uniformName], },);
550
605
  }
551
606
  }