@needle-tools/materialx 1.6.0 → 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,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.6.0",
4
+ "version": "1.7.0-next.0d06218",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "types": "index.d.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
  }
package/src/materialx.js CHANGED
@@ -1,11 +1,22 @@
1
1
  import MaterialX from "../bin/JsMaterialXGenShader.js";
2
2
  import { debug, waitForNetworkIdle } from "./utils.js";
3
- import { renderPMREMToEquirect } from "./utils.texture.js";
4
- import { Light, Mesh, MeshBasicMaterial, Object3D, PlaneGeometry, PMREMGenerator, Scene, Texture, WebGLRenderer } from "three";
3
+ import { renderPMREMToEquirect, renderPMREMToPrefilteredEquirect } from "./utils.texture.js";
4
+ import { CubeUVReflectionMapping, Light, Mesh, MeshBasicMaterial, Object3D, PlaneGeometry, PMREMGenerator, Scene, Texture, WebGLRenderer } from "three";
5
5
  import { registerLights, getLightData } from "./materialx.helper.js";
6
6
  import { whiteTexture } from "./utils.texture.js";
7
7
  import { VERSION } from "./constants.js";
8
8
 
9
+ /**
10
+ * Accept Texture instances from any Three.js copy. Tooling like the fidelity
11
+ * renderer can host scenes with a different Three.js module instance than this
12
+ * package, so `instanceof Texture` is too strict here.
13
+ * @param {unknown} value
14
+ * @returns {value is Texture}
15
+ */
16
+ function isTextureLike(value) {
17
+ return !!value && typeof value === "object" && /** @type {{ isTexture?: unknown }} */(value).isTexture === true;
18
+ }
19
+
9
20
 
10
21
  /**
11
22
  * Preloads the MaterialX WebAssembly module.
@@ -51,7 +62,7 @@ export async function ready() {
51
62
 
52
63
  // NOTE: This must be a plain string literal (not a template) so that the
53
64
  // makeFilesLocal Vite plugin can statically detect and localize this URL.
54
- const defaultBaseUrl = "https://cdn.needle.tools/static/materialx/1.6.0/";
65
+ const defaultBaseUrl = "https://cdn.needle.tools/static/materialx/1.7.0/";
55
66
 
56
67
  /** @type {Array<string>} */
57
68
  let urls;
@@ -121,7 +132,7 @@ export async function ready() {
121
132
  // SPECULAR_ENVIRONMENT_NONE: Do not use specular environment maps.
122
133
  // SPECULAR_ENVIRONMENT_FIS: Use Filtered Importance Sampling for specular environment/indirect lighting.
123
134
  // SPECULAR_ENVIRONMENT_PREFILTER: Use pre-filtered environment maps for specular environment/indirect lighting.
124
- state.materialXGenContext.getOptions().hwSpecularEnvironmentMethod = state.materialXModule.HwSpecularEnvironmentMethod.SPECULAR_ENVIRONMENT_FIS;
135
+ state.materialXGenContext.getOptions().hwSpecularEnvironmentMethod = state.materialXModule.HwSpecularEnvironmentMethod.SPECULAR_ENVIRONMENT_PREFILTER;
125
136
 
126
137
  // TRANSMISSION_REFRACTION: Use a refraction approximation for transmission rendering.
127
138
  // TRANSMISSION_OPACITY: Use opacity for transmission rendering.
@@ -161,6 +172,7 @@ export async function ready() {
161
172
  * @typedef {Object} EnvironmentTextureSet
162
173
  * @property {Texture | null} radianceTexture
163
174
  * @property {Texture | null} irradianceTexture
175
+ * @property {() => void} [dispose]
164
176
  */
165
177
 
166
178
  /**
@@ -285,9 +297,10 @@ export class MaterialXEnvironment {
285
297
  this._pmremGenerator?.dispose();
286
298
  this._pmremGenerator = null;
287
299
  this._renderer = null;
288
- for (const textureSet of this._texturesCache.values()) {
289
- textureSet.radianceTexture?.dispose();
290
- textureSet.irradianceTexture?.dispose();
300
+ for (const textureModeMap of this._texturesCache.values()) {
301
+ for (const textureSet of textureModeMap.values()) {
302
+ textureSet.dispose?.();
303
+ }
291
304
  }
292
305
  this._texturesCache.clear();
293
306
  }
@@ -304,25 +317,26 @@ export class MaterialXEnvironment {
304
317
  * @param {import("./materialx.material.js").MaterialXMaterial} material
305
318
  */
306
319
  getTextures(material) {
320
+ const radianceMode = material.environmentRadianceMode ?? "three-pmrem";
307
321
  if (material.envMap) {
308
322
  // If the material has its own envMap, we don't use the irradiance texture
309
- return this._getTextures(material.envMap);
323
+ return this._getTextures(material.envMap, radianceMode);
310
324
  }
311
325
 
312
326
  // Use the scene background for lighting if no environment is available
313
327
  // If we don't do this we don't see the correct lighting for scenes exported with 'Environment Lighting: Color' and 'Environment Reflections: Skybox'
314
328
  const skybox = this._scene.environment || this._scene.background;
315
- if (skybox instanceof Texture) {
316
- return this._getTextures(skybox);
329
+ if (isTextureLike(skybox)) {
330
+ return this._getTextures(skybox, radianceMode);
317
331
  }
318
- return this._getTextures(null);
332
+ return this._getTextures(null, radianceMode);
319
333
  }
320
334
 
321
335
  /** @type {PMREMGenerator | null} */
322
336
  _pmremGenerator = null;
323
337
  /** @type {WebGLRenderer | null} */
324
338
  _renderer = null;
325
- /** @type {Map<Texture | null, EnvironmentTextureSet>} */
339
+ /** @type {Map<Texture | null, Map<string, EnvironmentTextureSet>>} */
326
340
  _texturesCache = new Map();
327
341
 
328
342
  /**
@@ -331,7 +345,6 @@ export class MaterialXEnvironment {
331
345
  */
332
346
  async _initialize(renderer) {
333
347
  this._isInitialized = false;
334
- this._pmremGenerator = new PMREMGenerator(renderer);
335
348
  this._renderer = renderer;
336
349
  this.updateLighting(true);
337
350
  this._isInitialized = true;
@@ -340,30 +353,65 @@ export class MaterialXEnvironment {
340
353
 
341
354
  /**
342
355
  * @param {Texture | null | undefined} texture
343
- * @returns {{radianceTexture: Texture | null, irradianceTexture: Texture | null}}
356
+ * @param {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} [radianceMode]
357
+ * @returns {EnvironmentTextureSet}
344
358
  */
345
- _getTextures(texture) {
359
+ _getTextures(texture, radianceMode = "three-pmrem") {
346
360
 
347
361
  // Fallback to white texture if no texture is provided
348
362
  if (!texture) {
349
363
  texture = whiteTexture;
350
364
  }
351
365
 
366
+ const cacheKey = texture || null;
367
+ let textureModeMap = this._texturesCache.get(cacheKey);
368
+ if (!textureModeMap) {
369
+ textureModeMap = new Map();
370
+ this._texturesCache.set(cacheKey, textureModeMap);
371
+ }
372
+
352
373
  /** @type {EnvironmentTextureSet | undefined} */
353
- let res = this._texturesCache.get(texture || null);
374
+ let res = textureModeMap.get(radianceMode);
354
375
  if (res) {
355
376
  return res;
356
377
  }
357
378
 
358
- if (this._scene && this._pmremGenerator && this._renderer && texture) {
379
+ const isPmremTexture = texture.mapping === CubeUVReflectionMapping || texture.isRenderTargetTexture === true;
380
+
381
+ if (this._scene && this._renderer && texture) {
359
382
  if (debug) console.log("[MaterialX] Generating environment textures", texture.name);
360
- const target = this._pmremGenerator.fromEquirectangular(texture);
361
- const radianceRenderTarget = renderPMREMToEquirect(this._renderer, target.texture, 0.0, 1024, 512, target.height);
362
- const irradianceRenderTarget = renderPMREMToEquirect(this._renderer, target.texture, 1.0, 32, 16, target.height);
363
- target.dispose();
383
+ let radianceRenderTarget;
384
+ let irradianceRenderTarget;
385
+
386
+ if (isPmremTexture) {
387
+ // Scene.environment is often already PMREM-processed (CubeUV layout).
388
+ // Running PMREMGenerator on it again corrupts the sampling layout.
389
+ radianceRenderTarget = radianceMode === "materialx-prefiltered"
390
+ ? renderPMREMToPrefilteredEquirect(this._renderer, texture)
391
+ : radianceMode === "materialx-fis"
392
+ ? renderPMREMToEquirect(this._renderer, texture, 0.0, 1024, 512)
393
+ : null;
394
+ irradianceRenderTarget = renderPMREMToEquirect(this._renderer, texture, 1.0, 32, 16);
395
+ } else {
396
+ const target = this._getPMREMGenerator().fromEquirectangular(texture);
397
+ radianceRenderTarget = radianceMode === "materialx-prefiltered"
398
+ ? renderPMREMToPrefilteredEquirect(this._renderer, target.texture, undefined, undefined, target.height)
399
+ : radianceMode === "three-pmrem"
400
+ ? target
401
+ : null;
402
+ irradianceRenderTarget = renderPMREMToEquirect(this._renderer, target.texture, 1.0, 32, 16, target.height);
403
+ if (radianceMode !== "three-pmrem") {
404
+ target.dispose();
405
+ }
406
+ }
407
+
364
408
  res = {
365
- radianceTexture: radianceRenderTarget.texture,
366
- irradianceTexture: irradianceRenderTarget.texture
409
+ radianceTexture: radianceRenderTarget?.texture ?? texture,
410
+ irradianceTexture: irradianceRenderTarget.texture,
411
+ dispose: () => {
412
+ radianceRenderTarget?.dispose();
413
+ irradianceRenderTarget?.dispose();
414
+ },
367
415
  }
368
416
  }
369
417
  else {
@@ -372,10 +420,21 @@ export class MaterialXEnvironment {
372
420
  irradianceTexture: null
373
421
  }
374
422
  }
375
- this._texturesCache.set(texture || null, res);
423
+ textureModeMap.set(radianceMode, res);
376
424
  return res;
377
425
  }
378
426
 
427
+ /**
428
+ * @returns {PMREMGenerator}
429
+ */
430
+ _getPMREMGenerator() {
431
+ if (!this._renderer) {
432
+ throw new Error("[MaterialX] Cannot create PMREMGenerator before renderer initialization.");
433
+ }
434
+ this._pmremGenerator ??= new PMREMGenerator(this._renderer);
435
+ return this._pmremGenerator;
436
+ }
437
+
379
438
  /**
380
439
  * @param {boolean} collectLights
381
440
  */