@needle-tools/materialx 1.6.0-next.2af5fc1 → 1.7.0-next.043f0c6

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,21 @@
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.043f0c6",
5
5
  "type": "module",
6
+ "license": "PolyForm-Noncommercial-1.0.0",
6
7
  "main": "index.js",
7
8
  "types": "index.d.ts",
8
9
  "exports": {
9
10
  ".": {
11
+ "types": "./index.d.ts",
10
12
  "import": "./index.js",
11
- "require": "./index.js",
12
- "types": "./index.d.ts"
13
+ "require": "./index.js"
13
14
  },
14
15
  "./needle": {
16
+ "types": "./needle.d.ts",
15
17
  "import": "./needle.js",
16
- "require": "./needle.js",
17
- "types": "./needle.d.ts"
18
+ "require": "./needle.js"
18
19
  },
19
20
  "./package.json": "./package.json",
20
21
  "./codegen/register_types.ts": {
@@ -26,7 +27,10 @@
26
27
  "three": ">=0.160.0"
27
28
  },
28
29
  "scripts": {
29
- "test": "node --import=./tests/unit/register-json-loader.js --test tests/unit/**/*.test.js"
30
+ "test": "node --import=./tests/unit/register-json-loader.js --test tests/unit/**/*.test.js",
31
+ "test:runtime:node": "node tests/runtime/materialx-runtime-smoke.mjs",
32
+ "test:runtime:deno": "deno run --allow-read --no-lock --node-modules-dir=auto tests/runtime/materialx-runtime-smoke.mjs",
33
+ "test:runtime:bun": "bun tests/runtime/materialx-runtime-smoke.mjs"
30
34
  },
31
35
  "devDependencies": {
32
36
  "@needle-tools/engine": "4.x",
@@ -35,6 +39,9 @@
35
39
  "jsdom": "^29.0.1",
36
40
  "three": "npm:@needle-tools/three@^0.169.5"
37
41
  },
42
+ "overrides": {
43
+ "@needle-tools/gltf-progressive": "3.6.0-beta.2"
44
+ },
38
45
  "files": [
39
46
  "index.js",
40
47
  "index.d.ts",
@@ -66,5 +73,9 @@
66
73
  "pbr",
67
74
  "3d",
68
75
  "wasm"
69
- ]
76
+ ],
77
+ "repository": {
78
+ "type": "git",
79
+ "url": "https://github.com/needle-tools/needle-engine-materialx.git"
80
+ }
70
81
  }
package/src/constants.js CHANGED
@@ -1,6 +1,5 @@
1
- // Bare JSON import works in all bundlers (Vite, Webpack, Rollup).
2
- // Node.js 22+ requires --experimental-json-modules or import attributes,
3
- // but this file is only loaded in bundler context at runtime.
4
- // Unit tests that run under Node.js use the --import flag in package.json.
5
- import pkg from '../package.json';
1
+ // Keep the runtime version tied to package.json. The import attribute is
2
+ // required by browsers and Node.js for JSON modules; bundlers also understand
3
+ // this shape.
4
+ import pkg from '../package.json' with { type: 'json' };
6
5
  export const VERSION = pkg.version;
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";
@@ -16,7 +16,7 @@ export interface LightData {
16
16
  outer_angle: number;
17
17
  }
18
18
 
19
- export function prepareEnvTexture(texture: THREE.Texture, capabilities: any): THREE.Texture;
19
+ export function prepareEnvTexture(texture: THREE.Texture, capabilities?: any): THREE.Texture;
20
20
 
21
21
  export function getLightRotation(): THREE.Matrix4;
22
22
 
@@ -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;
@@ -21,7 +21,7 @@ const IMAGE_PATH_SEPARATOR = "/";
21
21
  export function prepareEnvTexture(texture, capabilities) {
22
22
  const newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, /** @type {any} */(texture.format), texture.type);
23
23
  newTexture.wrapS = THREE.RepeatWrapping;
24
- newTexture.anisotropy = capabilities.getMaxAnisotropy();
24
+ newTexture.anisotropy = capabilities?.getMaxAnisotropy?.() ?? 1;
25
25
  newTexture.minFilter = THREE.LinearMipmapLinearFilter;
26
26
  newTexture.magFilter = THREE.LinearFilter;
27
27
  newTexture.generateMipmaps = true;
@@ -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) {
@@ -77,10 +71,22 @@ function fromMatrix(matrix, dimension) {
77
71
  * @property {(path: string) => Promise<THREE.Texture | null | void>} getTexture - Get a texture by path
78
72
  */
79
73
 
74
+ /**
75
+ * @param {string} src
76
+ * @returns {HTMLImageElement | { src: string }}
77
+ */
78
+ function createPortableImage(src) {
79
+ if (typeof Image !== 'undefined') {
80
+ const image = new Image();
81
+ image.src = src;
82
+ return image;
83
+ }
84
+ return { src };
85
+ }
86
+
80
87
  const defaultTexture = new THREE.Texture();
81
88
  defaultTexture.needsUpdate = true;
82
- defaultTexture.image = new Image();
83
- defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg=="
89
+ defaultTexture.image = createPortableImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg==");
84
90
  // defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAB5QTFRFAAAABAQEw8PD////v7+/vb29Xl5eQEBA+/v7PDw8GPBYkgAAAB1JREFUeJxjZGBgYFQSABIUMlxgDGMGBtaIAnIZAKwQCSDYUEZEAAAAAElFTkSuQmCC";
85
91
  // defaultTexture.wrapS = THREE.RepeatWrapping;
86
92
  // defaultTexture.wrapT = THREE.RepeatWrapping;
@@ -90,8 +96,7 @@ defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAA
90
96
 
91
97
  const defaultNormalTexture = new THREE.Texture();
92
98
  defaultNormalTexture.needsUpdate = true;
93
- defaultNormalTexture.image = new Image();
94
- defaultNormalTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=";
99
+ defaultNormalTexture.image = createPortableImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=");
95
100
 
96
101
  /**
97
102
  * @param {string} key
@@ -125,9 +130,10 @@ function addToCache(key, value) {
125
130
  * @param {string} name
126
131
  * @param {Callbacks} loaders
127
132
  * @param {string} searchPath
133
+ * @param {Array<Promise<unknown>>} [pendingTextureLoads]
128
134
  * @returns {THREE.Uniform}
129
135
  */
130
- function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
136
+ function toThreeUniform(threeUniforms, uniforms, type, value, name, loaders, searchPath, pendingTextureLoads) {
131
137
 
132
138
  const uniform = new THREE.Uniform(/** @type {any} */(null));
133
139
 
@@ -178,13 +184,22 @@ function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
178
184
  if (cacheValue) {
179
185
  if (debug) console.log('[MaterialX] Use cached texture: ', cacheKey, cacheValue);
180
186
  if (cacheValue instanceof Promise) {
181
- cacheValue.then(res => {
182
- if (res) uniform.value = res;
187
+ const trackedLoad = cacheValue.then(res => {
188
+ if (res) {
189
+ uniform.value = res;
190
+ if (threeUniforms[name + "_flipY"]) threeUniforms[name + "_flipY"].value = !!res.flipY;
191
+ }
183
192
  else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
193
+ return res;
194
+ }).catch(err => {
195
+ console.error(`[MaterialX] Failed to load texture ${name} '${texturePath}'`, err);
196
+ return null;
184
197
  });
198
+ pendingTextureLoads?.push(trackedLoad);
185
199
  }
186
200
  else {
187
201
  uniform.value = cacheValue;
202
+ if (threeUniforms[name + "_flipY"]) threeUniforms[name + "_flipY"].value = !!cacheValue.flipY;
188
203
  }
189
204
  }
190
205
  else {
@@ -197,9 +212,9 @@ function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
197
212
  const promise = loaders.getTexture(texturePath)
198
213
  ?.then(res => {
199
214
  if (res) {
200
- res = res.clone(); // we need to clone the texture once to avoid colorSpace issues with other materials
201
215
  res.colorSpace = THREE.LinearSRGBColorSpace;
202
216
  setTextureParameters(res, name, uniforms);
217
+ res.needsUpdate = true;
203
218
  }
204
219
  return res;
205
220
  })
@@ -210,15 +225,21 @@ function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
210
225
 
211
226
  if (checkCache) addToCache(cacheKey, promise);
212
227
 
213
- promise?.then(res => {
228
+ const trackedLoad = promise?.then(res => {
214
229
  // Replace Promise cache entry with the resolved texture value.
215
230
  // This avoids keeping long-lived promise/closure graphs in THREE.Cache.
216
231
  if (checkCache && res) addToCache(cacheKey, res);
217
- if (res) uniform.value = /** @type {any} */ (res);
232
+ if (res) {
233
+ uniform.value = /** @type {any} */ (res);
234
+ if (threeUniforms[name + "_flipY"]) threeUniforms[name + "_flipY"].value = !!res.flipY;
235
+ }
218
236
  else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
237
+ return res;
219
238
  });
239
+ if (trackedLoad) pendingTextureLoads?.push(trackedLoad);
220
240
  }
221
241
  }
242
+ threeUniforms[name + "_flipY"] = new THREE.Uniform(!!uniform.value?.flipY);
222
243
  break;
223
244
  case 'samplerCube':
224
245
  case 'string':
@@ -248,23 +269,69 @@ const valueTypeWarningMap = new Map();
248
269
  * @param {number} mode
249
270
  * @returns {THREE.Wrapping}
250
271
  */
272
+ function getAddressMode(mode) {
273
+ if (typeof mode === "number") return mode;
274
+ switch (String(mode ?? "").toLowerCase()) {
275
+ case "constant":
276
+ return 0;
277
+ case "clamp":
278
+ return 1;
279
+ case "periodic":
280
+ return 2;
281
+ case "mirror":
282
+ return 3;
283
+ default:
284
+ return 2;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Get Three wrapping mode
290
+ * @param {unknown} mode
291
+ * @returns {THREE.Wrapping}
292
+ */
251
293
  function getWrapping(mode) {
252
- let wrap;
253
- switch (mode) {
294
+ switch (getAddressMode(mode)) {
295
+ case 0:
254
296
  case 1:
255
- wrap = THREE.ClampToEdgeWrapping;
256
- break;
257
- case 2:
258
- wrap = THREE.RepeatWrapping;
259
- break;
297
+ return THREE.ClampToEdgeWrapping;
260
298
  case 3:
261
- wrap = THREE.MirroredRepeatWrapping;
262
- break;
299
+ return THREE.MirroredRepeatWrapping;
300
+ case 2:
263
301
  default:
264
- wrap = THREE.RepeatWrapping;
265
- break;
302
+ return THREE.RepeatWrapping;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * @param {unknown} mode
308
+ * @returns {number}
309
+ */
310
+ function getFilterType(mode) {
311
+ if (typeof mode === "number") return mode;
312
+ switch (String(mode ?? "").toLowerCase()) {
313
+ case "closest":
314
+ return 0;
315
+ case "linear":
316
+ return 1;
317
+ default:
318
+ return -1;
266
319
  }
267
- return wrap;
320
+ }
321
+
322
+ /**
323
+ * @param {any} uniforms
324
+ * @param {string} name
325
+ * @param {any} defaultValue
326
+ * @returns {any}
327
+ */
328
+ function getUniformData(uniforms, name, defaultValue) {
329
+ const uniform = uniforms?.find?.(name);
330
+ const value = uniform?.getValue?.();
331
+ if (!value) return defaultValue;
332
+ if (typeof value.getData === "function") return value.getData();
333
+ if (typeof value.data === "function") return value.data();
334
+ return defaultValue;
268
335
  }
269
336
 
270
337
  /**
@@ -278,22 +345,18 @@ function setTextureParameters(texture, name, uniforms, generateMipmaps = true) {
278
345
  const idx = name.lastIndexOf(IMAGE_PROPERTY_SEPARATOR);
279
346
  const base = name.substring(0, idx) || name;
280
347
 
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
- }
348
+ texture.wrapS = getWrapping(getUniformData(uniforms, base + UADDRESS_MODE_SUFFIX, 2));
349
+ texture.wrapT = getWrapping(getUniformData(uniforms, base + VADDRESS_MODE_SUFFIX, 2));
290
350
 
291
- const mxFilterType = uniforms.find(base + FILTER_TYPE_SUFFIX) ? uniforms.get(base + FILTER_TYPE_SUFFIX).value : -1;
351
+ const mxFilterType = getFilterType(getUniformData(uniforms, base + FILTER_TYPE_SUFFIX, -1));
292
352
  let minFilter = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
353
+ let magFilter = THREE.LinearFilter;
293
354
  if (mxFilterType === 0) {
294
355
  minFilter = /** @type {any} */ (generateMipmaps ? THREE.NearestMipMapNearestFilter : THREE.NearestFilter);
356
+ magFilter = THREE.NearestFilter;
295
357
  }
296
358
  texture.minFilter = minFilter;
359
+ texture.magFilter = magFilter;
297
360
  }
298
361
 
299
362
  /**
@@ -528,9 +591,12 @@ export function getLightData(lights, genContext) {
528
591
  * @param {any} shaderStage
529
592
  * @param {Callbacks} loaders
530
593
  * @param {string} searchPath
594
+ * @param {Array<Promise<unknown>>} [pendingTextureLoads]
531
595
  * @returns {Object<string, THREE.Uniform>}
532
596
  */
533
- export function getUniformValues(shaderStage, loaders, searchPath) {
597
+ export function getUniformValues(shaderStage, loaders, searchPath, pendingTextureLoads) {
598
+ loaders ??= { getTexture: async () => null };
599
+ searchPath ??= "";
534
600
  /** @type {Object<string, THREE.Uniform>} */
535
601
  const threeUniforms = {};
536
602
 
@@ -545,7 +611,7 @@ export function getUniformValues(shaderStage, loaders, searchPath) {
545
611
  const value = variable.getValue()?.getData();
546
612
  const uniformName = variable.getVariable();
547
613
  const type = variable.getType().getName();
548
- threeUniforms[uniformName] = toThreeUniform(uniforms, type, value, uniformName, loaders, searchPath);
614
+ threeUniforms[uniformName] = toThreeUniform(threeUniforms, uniforms, type, value, uniformName, loaders, searchPath, pendingTextureLoads);
549
615
  if (debug) console.log("Adding uniform", { path: variable.getPath(), type: type, name: uniformName, value: threeUniforms[uniformName], },);
550
616
  }
551
617
  }