@needle-tools/materialx 1.7.0-next.8333b4d → 1.7.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.
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.7.0-next.8333b4d",
4
+ "version": "1.7.0",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
7
7
  "main": "index.js",
@@ -27,7 +27,10 @@
27
27
  "three": ">=0.160.0"
28
28
  },
29
29
  "scripts": {
30
- "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"
31
34
  },
32
35
  "devDependencies": {
33
36
  "@needle-tools/engine": "4.x",
@@ -75,4 +78,4 @@
75
78
  "type": "git",
76
79
  "url": "https://github.com/needle-tools/needle-engine-materialx.git"
77
80
  }
78
- }
81
+ }
package/src/constants.js CHANGED
@@ -1,5 +1,5 @@
1
- // Bare JSON import works in bundlers (Vite, Webpack, Rollup) and keeps the
2
- // runtime version tied to package.json. Node.js 22+ requires JSON import
3
- // attributes, so unit tests use tests/unit/register-json-loader.js.
4
- 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' };
5
5
  export const VERSION = pkg.version;
@@ -44,6 +44,8 @@ export interface MaterialXLoaderOptions {
44
44
  environmentRadianceMode?: MaterialXEnvironmentRadianceMode;
45
45
  /** Match Three.js glossy specular antialiasing. Defaults to true. */
46
46
  specularAntialiasing?: boolean;
47
+ /** Generate missing MikkTSpace tangents for tangent-dependent shaders. Defaults to true. */
48
+ generateTangents?: boolean;
47
49
  /** Flip texcoord V at the MaterialX texcoord node output. Defaults to false for standalone .mtlx creation and true for GLTFLoader integration. */
48
50
  hwTexcoordVerticalFlip?: boolean;
49
51
  /** Flip texcoord V inside MaterialX file texture sampling. Defaults to false for standalone .mtlx creation and true for GLTFLoader integration. */
@@ -358,6 +358,7 @@ export async function createMaterialXMaterial(mtlx, materialNodeNameOrIndex, loa
358
358
  context: context || {},
359
359
  environmentRadianceMode: options?.environmentRadianceMode,
360
360
  specularAntialiasing: options?.specularAntialiasing,
361
+ generateTangents: options?.generateTangents,
361
362
  parameters: {
362
363
  // MASK uses discard; BLEND uses Three.js transparent sorting and blending.
363
364
  transparent: renderAsTransparent,
@@ -71,10 +71,22 @@ function fromMatrix(matrix, dimension) {
71
71
  * @property {(path: string) => Promise<THREE.Texture | null | void>} getTexture - Get a texture by path
72
72
  */
73
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
+
74
87
  const defaultTexture = new THREE.Texture();
75
88
  defaultTexture.needsUpdate = true;
76
- defaultTexture.image = new Image();
77
- defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg=="
89
+ defaultTexture.image = createPortableImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg==");
78
90
  // defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAB5QTFRFAAAABAQEw8PD////v7+/vb29Xl5eQEBA+/v7PDw8GPBYkgAAAB1JREFUeJxjZGBgYFQSABIUMlxgDGMGBtaIAnIZAKwQCSDYUEZEAAAAAElFTkSuQmCC";
79
91
  // defaultTexture.wrapS = THREE.RepeatWrapping;
80
92
  // defaultTexture.wrapT = THREE.RepeatWrapping;
@@ -84,8 +96,7 @@ defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAA
84
96
 
85
97
  const defaultNormalTexture = new THREE.Texture();
86
98
  defaultNormalTexture.needsUpdate = true;
87
- defaultNormalTexture.image = new Image();
88
- defaultNormalTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=";
99
+ defaultNormalTexture.image = createPortableImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=");
89
100
 
90
101
  /**
91
102
  * @param {string} key
package/src/materialx.js CHANGED
@@ -17,6 +17,21 @@ function isTextureLike(value) {
17
17
  return !!value && typeof value === "object" && /** @type {{ isTexture?: unknown }} */(value).isTexture === true;
18
18
  }
19
19
 
20
+ function isNodeRuntime() {
21
+ return !!globalThis.process?.versions?.node && globalThis.process?.type !== "renderer";
22
+ }
23
+
24
+ async function getNodePackageAssetPaths() {
25
+ const nodeUrlModule = "node:url";
26
+ const { fileURLToPath } = await import(/* @vite-ignore */ nodeUrlModule);
27
+ const binDirectory = ["..", "bin"].join("/") + "/";
28
+ return [
29
+ "JsMaterialXCore.wasm",
30
+ "JsMaterialXGenShader.wasm",
31
+ "JsMaterialXGenShader.data.txt",
32
+ ].map(fileName => fileURLToPath(new URL(binDirectory + fileName, import.meta.url)));
33
+ }
34
+
20
35
 
21
36
  /**
22
37
  * Preloads the MaterialX WebAssembly module.
@@ -69,7 +84,10 @@ export async function ready() {
69
84
 
70
85
  const location = globalThis.NEEDLE_MATERIALX_LOCATION;
71
86
 
72
- if (location === "package" || location === "bin/" || location === "./bin/" || location === "../bin/") {
87
+ if (isNodeRuntime()) {
88
+ urls = await getNodePackageAssetPaths();
89
+ }
90
+ else if (location === "package" || location === "bin/" || location === "./bin/" || location === "../bin/") {
73
91
  // Use local files from the @needle-tools/materialx npm package.
74
92
  // Vite's ?url suffix copies these files to the output directory
75
93
  // and returns their URL automatically — no CDN download needed.
@@ -12,6 +12,7 @@ declare type MaterialXMaterialInitParameters = {
12
12
  parameters?: MaterialParameters;
13
13
  environmentRadianceMode?: MaterialXEnvironmentRadianceMode;
14
14
  specularAntialiasing?: boolean;
15
+ generateTangents?: boolean;
15
16
  debug?: boolean;
16
17
  }
17
18
 
@@ -38,6 +39,7 @@ export declare class MaterialXMaterial extends ShaderMaterial {
38
39
  envMapRotation: Euler;
39
40
  environmentRadianceMode: MaterialXEnvironmentRadianceMode;
40
41
  specularAntialiasing: boolean;
42
+ generateTangents: boolean;
41
43
  updateUniforms(environment: MaterialXEnvironment, _renderer: WebGLRenderer, object: Object3D, camera: Camera, time?: number, frame?: number): void;
42
44
 
43
45
  private updateEnvironmentUniforms(environment: MaterialXEnvironment, scene: Scene): void;
@@ -5,6 +5,11 @@ import { generateMaterialPropertiesForUniforms, getUniformValues, getLightTypeId
5
5
 
6
6
  export const DEFAULT_ENVIRONMENT_RADIANCE_MODE = "three-pmrem";
7
7
 
8
+ /** @type {Promise<{ computeMikkTSpaceTangents: Function, MikkTSpace: any }> | null} */
9
+ let mikkTSpaceTangentsPromise = null;
10
+ /** @type {WeakMap<BufferGeometry, Promise<boolean>>} */
11
+ const pendingTangentGenerations = new WeakMap();
12
+
8
13
  // Add helper matrices for uniform updates (similar to MaterialX example)
9
14
  const worldInverseMat = new Matrix4();
10
15
  const worldTransposeMat = new Matrix4();
@@ -91,6 +96,60 @@ function createThreeLightUniforms() {
91
96
  };
92
97
  }
93
98
 
99
+ function getMikkTSpaceTangents() {
100
+ mikkTSpaceTangentsPromise ??= Promise.all([
101
+ import('three/examples/jsm/utils/BufferGeometryUtils.js'),
102
+ import('three/examples/jsm/libs/mikktspace.module.js'),
103
+ ]).then(async ([utils, MikkTSpace]) => {
104
+ await MikkTSpace.ready;
105
+ return {
106
+ computeMikkTSpaceTangents: utils.computeMikkTSpaceTangents,
107
+ MikkTSpace,
108
+ };
109
+ });
110
+ return mikkTSpaceTangentsPromise;
111
+ }
112
+
113
+ /**
114
+ * @param {BufferGeometry} geometry
115
+ * @returns {Promise<boolean>}
116
+ */
117
+ function ensureGeometryTangents(geometry) {
118
+ if (geometry.attributes.tangent) return Promise.resolve(true);
119
+
120
+ let pending = pendingTangentGenerations.get(geometry);
121
+ if (pending) return pending;
122
+
123
+ pending = (async () => {
124
+ if (!geometry.attributes.position || !geometry.attributes.uv) {
125
+ console.warn('[MaterialX] Cannot generate tangents: geometry requires position and uv attributes.');
126
+ return false;
127
+ }
128
+
129
+ if (!geometry.attributes.normal) {
130
+ geometry.computeVertexNormals();
131
+ }
132
+
133
+ const { computeMikkTSpaceTangents, MikkTSpace } = await getMikkTSpaceTangents();
134
+ computeMikkTSpaceTangents(geometry, MikkTSpace);
135
+
136
+ if (geometry.attributes.tangent) {
137
+ geometry.attributes.tangent.needsUpdate = true;
138
+ return true;
139
+ }
140
+
141
+ return false;
142
+ })().catch(error => {
143
+ console.warn('[MaterialX] Failed to generate MikkTSpace tangents.', error);
144
+ return false;
145
+ }).finally(() => {
146
+ pendingTangentGenerations.delete(geometry);
147
+ });
148
+
149
+ pendingTangentGenerations.set(geometry, pending);
150
+ return pending;
151
+ }
152
+
94
153
  const CUBE_UV_REFLECTION_FUNCTIONS = `
95
154
  float mx_cubeuv_getFace(vec3 direction) {
96
155
  vec3 absDirection = abs(direction);
@@ -305,6 +364,7 @@ function patchImageAddressModes(fragmentShader) {
305
364
  * @property {import('three').MaterialParameters} [parameters] - Optional parameters
306
365
  * @property {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} [environmentRadianceMode]
307
366
  * @property {boolean} [specularAntialiasing] - Match Three.js glossy specular antialiasing. Defaults to true.
367
+ * @property {boolean} [generateTangents] - Generate missing MikkTSpace tangents for tangent-dependent shaders. Defaults to true.
308
368
  * @property {boolean} [debug] - Debug flag
309
369
  */
310
370
 
@@ -336,6 +396,7 @@ export class MaterialXMaterial extends ShaderMaterial {
336
396
  this.envMapRotation.copy(source.envMapRotation);
337
397
  this.environmentRadianceMode = source.environmentRadianceMode;
338
398
  this.specularAntialiasing = source.specularAntialiasing;
399
+ this.generateTangents = source.generateTangents;
339
400
  this.ready = source.ready;
340
401
  generateMaterialPropertiesForUniforms(this, this._shader.getStage('pixel'));
341
402
  generateMaterialPropertiesForUniforms(this, this._shader.getStage('vertex'));
@@ -349,6 +410,8 @@ export class MaterialXMaterial extends ShaderMaterial {
349
410
  _shader = null;
350
411
  /** @type {boolean} */
351
412
  _needsTangents = false;
413
+ /** @type {WeakSet<BufferGeometry>} */
414
+ _pendingTangentGeometries = new WeakSet();
352
415
  /** @type {Promise<void>} */
353
416
  ready = Promise.resolve();
354
417
 
@@ -368,6 +431,7 @@ export class MaterialXMaterial extends ShaderMaterial {
368
431
  /** @type {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} */
369
432
  let environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
370
433
  let specularAntialiasing = true;
434
+ let generateTangents = true;
371
435
 
372
436
  if (init) {
373
437
 
@@ -423,6 +487,7 @@ export class MaterialXMaterial extends ShaderMaterial {
423
487
  // the same way MeshStandardMaterial does.
424
488
  fragmentShader = fragmentShader.replace(/\bu_envLightIntensity\b/g, 'envMapIntensity');
425
489
  specularAntialiasing = init.specularAntialiasing ?? true;
490
+ generateTangents = init.generateTangents ?? true;
426
491
  if (specularAntialiasing) {
427
492
  fragmentShader = patchEnvironmentSpecularAntialiasing(fragmentShader);
428
493
  }
@@ -698,6 +763,7 @@ $2`
698
763
  this._needsTangents = vertexShader.includes('in vec4 tangent;') || vertexShader.includes('in vec3 tangent;');
699
764
  this.environmentRadianceMode = environmentRadianceMode;
700
765
  this.specularAntialiasing = specularAntialiasing;
766
+ this.generateTangents = generateTangents;
701
767
 
702
768
  Object.assign(this.uniforms,
703
769
  // Three.js light uniforms (required when lights: true). These use
@@ -760,10 +826,23 @@ $2`
760
826
  */
761
827
  onBeforeRender(renderer, _scene, camera, geometry, object, _group) {
762
828
  if (this._needsTangents && !geometry.attributes.tangent) {
763
- if (!this._missingTangentsWarned) {
829
+ if (this.generateTangents) {
830
+ if (!this._pendingTangentGeometries.has(geometry)) {
831
+ this._pendingTangentGeometries.add(geometry);
832
+ ensureGeometryTangents(geometry).then(generated => {
833
+ if (generated) {
834
+ this.needsUpdate = true;
835
+ } else if (!this._missingTangentsWarned) {
836
+ this._missingTangentsWarned = true;
837
+ console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but could not be generated for the geometry.`);
838
+ }
839
+ }).finally(() => {
840
+ this._pendingTangentGeometries.delete(geometry);
841
+ });
842
+ }
843
+ } else if (!this._missingTangentsWarned) {
764
844
  this._missingTangentsWarned = true;
765
- console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but not present in the geometry.`);
766
- // TODO: can we compute tangents here?
845
+ console.warn(`[MaterialX] Tangents are required for this material (${this.name}) but not present in the geometry. Automatic tangent generation is disabled.`);
767
846
  }
768
847
  }
769
848
  const time = this._context?.getTime?.() || getTime();
@@ -786,6 +865,8 @@ $2`
786
865
  environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
787
866
  /** @type {boolean} */
788
867
  specularAntialiasing = true;
868
+ /** @type {boolean} */
869
+ generateTangents = true;
789
870
 
790
871
  /**
791
872
  * @param {import("three").WebGLRenderer} _renderer
package/src/utils.js CHANGED
@@ -7,7 +7,9 @@
7
7
  * @returns {boolean|string} Parameter value or false if not found
8
8
  */
9
9
  export function getParam(name) {
10
- const urlParams = new URLSearchParams(window.location.search);
10
+ const search = globalThis.location?.search ?? globalThis.window?.location?.search;
11
+ if (!search) return false;
12
+ const urlParams = new URLSearchParams(search);
11
13
  const param = urlParams.get(name);
12
14
  if (param == null || param === "0" || param === "false") return false;
13
15
  if (param === "") return true;
@@ -35,22 +37,26 @@ export function getFrame() {
35
37
  return frame;
36
38
  }
37
39
 
38
- const performance = window.performance || /** @type {any} */ (window).webkitPerformance || /** @type {any} */ (window).mozPerformance;
40
+ const performanceApi = globalThis.performance
41
+ || globalThis.window?.performance
42
+ || /** @type {any} */ (globalThis.window)?.webkitPerformance
43
+ || /** @type {any} */ (globalThis.window)?.mozPerformance
44
+ || { now: () => Date.now() };
39
45
 
40
46
  function updateTime() {
41
- time = performance.now() / 1000; // Convert to seconds
47
+ time = performanceApi.now() / 1000; // Convert to seconds
42
48
  frame++;
43
- window.requestAnimationFrame(updateTime);
49
+ globalThis.requestAnimationFrame?.(updateTime);
44
50
  }
45
51
 
46
- window.requestAnimationFrame(updateTime);
52
+ globalThis.requestAnimationFrame?.(updateTime);
47
53
 
48
54
 
49
55
 
50
56
 
51
57
  export async function waitForNetworkIdle() {
52
- if (typeof requestIdleCallback !== "undefined") {
53
- return new Promise(res => requestIdleCallback(res));
58
+ if (typeof globalThis.requestIdleCallback !== "undefined") {
59
+ return new Promise(res => globalThis.requestIdleCallback(res));
54
60
  }
55
61
  else {
56
62
  console.debug("[MaterialX] Can not wait for network idle, using fallback");
@@ -61,5 +67,6 @@ export async function waitForNetworkIdle() {
61
67
 
62
68
  export function isDevEnvironment() {
63
69
  // check if we're in localhost or using an ip address
64
- return window.location.hostname === "localhost" || /^\d{1,3}(\.\d{1,3}){3}$/.test(window.location.hostname);
65
- }
70
+ const hostname = globalThis.location?.hostname ?? globalThis.window?.location?.hostname;
71
+ return hostname === "localhost" || !!hostname && /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
72
+ }