@needle-tools/materialx 1.1.0-next.bc1b608 → 1.1.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/CHANGELOG.md CHANGED
@@ -4,6 +4,9 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.1.0] - 2025-07-15
8
+ - Add: `useNeedleMaterialX` hooks for vanilla three.js and Needle Engine
9
+
7
10
  ## [1.0.6] - 2025-07-15
8
11
  - Fix: texture/environment sampling on some Android devices
9
12
 
package/README.md CHANGED
@@ -1,18 +1,32 @@
1
1
  # Needle MaterialX
2
2
 
3
- Web runtime support to load and display MaterialX materials in Needle Engine
3
+ Web runtime support to load and display MaterialX materials in Needle Engine and three.js
4
4
 
5
5
  ## Installation
6
6
  `npm i @needle-tools/materialx`
7
7
 
8
+ ## Examples
9
+ - [three.js Example on Stackblitz](https://stackblitz.com/edit/needle-materialx-example?file=main.js,package.json,index.html)
10
+
8
11
  ## How to use
9
12
 
10
- To use with Needle Engine simply import the module
13
+ ### Use with Needle Engine
14
+
15
+ ```ts
16
+ import { useNeedleMaterialX } from "@needle-tools/materialx/needle";
17
+ // Simply call this function in global scope as soon as possible
18
+ useNeedleMaterialX();
19
+ ```
20
+
21
+ ### Use with three.js
11
22
 
12
23
  ```ts
13
- import "@needle-tools/materialx"
24
+ import { useNeedleMaterialX } from "@needle-tools/materialx";
25
+ // Call the function with your GLTFLoader instance
26
+ useNeedleMaterialX(<yourGltfLoaderInstance>);
14
27
  ```
15
28
 
29
+
16
30
  <br />
17
31
 
18
32
  # Contact ✒️
package/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from "./src/index.js";
2
- export { addPluginForThree } from "./src/loader/loader.three.js";
2
+ export { useNeedleMaterialX } from "./src/loader/loader.three.js";
package/needle.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from "./src/index.js";
2
- export { addPluginForNeedleEngine } from "./src/loader/loader.needle.js";
2
+ export { useNeedleMaterialX } from "./src/loader/loader.needle.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/materialx",
3
- "version": "1.1.0-next.bc1b608",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "exports": {
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "peerDependencies": {
22
22
  "@needle-tools/engine": "4.x || ^4.6.0-0",
23
- "three": "npm:@needle-tools/three@^0.169.5"
23
+ "three": ">=0.169.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@needle-tools/engine": "4.x",
@@ -42,4 +42,4 @@
42
42
  "mtlx",
43
43
  "rendering"
44
44
  ]
45
- }
45
+ }
@@ -2,8 +2,9 @@ import { addCustomExtensionPlugin } from "@needle-tools/engine";
2
2
  import { Context, GLTF, INeedleGLTFExtensionPlugin } from "@needle-tools/engine";
3
3
  import type { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
4
4
  import type { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
5
- import { addPluginForThree, MaterialXLoader } from "./loader.three.js";
5
+ import { useNeedleMaterialX as _useNeedleMaterialX, MaterialXLoader } from "./loader.three.js";
6
6
  import { debug } from "../utils.js";
7
+ import { MaterialParameters } from "three";
7
8
 
8
9
 
9
10
  export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
@@ -13,13 +14,14 @@ export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
13
14
 
14
15
  onImport = (loader: GLTFLoader, url: string, context: Context) => {
15
16
  if (debug) console.log("MaterialXLoaderPlugin: Registering MaterialX extension for", url);
16
- addPluginForThree(loader, {
17
+ _useNeedleMaterialX(loader, {
18
+ cacheKey: url,
19
+ parameters: {
20
+ precision: context.renderer.capabilities.getMaxPrecision("highp") as MaterialParameters["precision"],
21
+ }
22
+ }, {
17
23
  getTime: () => context.time.time,
18
24
  getFrame: () => context.time.frame,
19
- getScene: () => context.scene,
20
- getRenderer: () => context.renderer,
21
- }, {
22
- cacheKey: url
23
25
  });
24
26
  };
25
27
 
@@ -36,6 +38,6 @@ export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
36
38
  /**
37
39
  * Add the MaterialXLoaderPlugin to the Needle Engine.
38
40
  */
39
- export async function addPluginForNeedleEngine() {
41
+ export async function useNeedleMaterialX() {
40
42
  addCustomExtensionPlugin(new MaterialXLoaderPlugin());
41
43
  }
@@ -1,4 +1,4 @@
1
- import { Material, MeshStandardMaterial, DoubleSide, FrontSide } from "three";
1
+ import { Material, MeshStandardMaterial, DoubleSide, FrontSide, MaterialParameters } from "three";
2
2
  import { GLTFLoader, GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
  import { ready, state, MaterialXContext } from "../materialx.js";
4
4
  import { debug } from "../utils.js";
@@ -28,6 +28,14 @@ type MaterialDefinition = {
28
28
  },
29
29
  }
30
30
 
31
+ // init.context.getRenderer().capabilities.getMaxPrecision("highp")
32
+ export type MaterialXLoaderOptions = {
33
+ /** The URL of the GLTF file being loaded */
34
+ cacheKey?: string;
35
+ /** Parameters for the MaterialX loader */
36
+ parameters?: Pick<MaterialParameters, "precision">;
37
+ }
38
+
31
39
  // MaterialX loader extension for js GLTFLoader
32
40
  export class MaterialXLoader implements GLTFLoaderPlugin {
33
41
  readonly name = "NEEDLE_materials_mtlx";
@@ -51,7 +59,11 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
51
59
  * @param cacheKey The URL of the GLTF file
52
60
  * @param context The context for the GLTF loading process
53
61
  */
54
- constructor(private parser: GLTFParser, private cacheKey: string, private context: MaterialXContext) {
62
+ constructor(
63
+ private parser: GLTFParser,
64
+ private options: MaterialXLoaderOptions,
65
+ private context: MaterialXContext
66
+ ) {
55
67
  if (debug) console.log("MaterialXLoader created for parser");
56
68
  // Start loading of MaterialX environment if the root extension exists
57
69
  if (this.materialX_root_data) {
@@ -233,8 +245,9 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
233
245
  transparent: isTransparent,
234
246
  side: material_def.doubleSided ? DoubleSide : FrontSide,
235
247
  context: this.context,
248
+ precision: this.options.parameters?.precision,
236
249
  loaders: {
237
- cacheKey: this.cacheKey,
250
+ cacheKey: this.options.cacheKey || "",
238
251
  getTexture: async url => {
239
252
  // Find the index of the texture in the parser
240
253
  const filenameWithoutExt = url.split('/').pop()?.split('.').shift() || '';
@@ -296,11 +309,11 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
296
309
 
297
310
 
298
311
  /**
299
- * Add the MaterialXLoaderPlugin to the Needle Engine.
312
+ * Add the MaterialXLoader to the GLTFLoader instance.
300
313
  */
301
- export function addPluginForThree(loader: GLTFLoader, context: MaterialXContext, options: { cacheKey?: string } = {}) {
314
+ export function useNeedleMaterialX(loader: GLTFLoader, options?: MaterialXLoaderOptions, context?: MaterialXContext) {
302
315
  loader.register(p => {
303
- const loader = new MaterialXLoader(p, options.cacheKey || "", context);
316
+ const loader = new MaterialXLoader(p, options || {}, context || {});
304
317
  return loader;
305
318
  });
306
319
  }
@@ -1,22 +1,22 @@
1
1
  import { BufferGeometry, Camera, FrontSide, GLSL3, Group, IUniform, MaterialParameters, Matrix3, Matrix4, Object3D, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
2
- import { debug } from "./utils.js";
2
+ import { debug, getFrame, getTime } from "./utils.js";
3
3
  import { MaterialXContext, MaterialXEnvironment } from "./materialx.js";
4
4
  import { getUniformValues, Loaders } from "./materialx.helper.js";
5
5
 
6
6
 
7
7
  // Add helper matrices for uniform updates (similar to MaterialX example)
8
- const identityMatrix = new Matrix4();
9
8
  const normalMat = new Matrix3();
10
- const viewProjMat = new Matrix4();
11
9
  const worldViewPos = new Vector3();
12
10
 
13
11
  declare type MaterialXMaterialInitParameters = {
14
12
  name: string,
15
13
  shader: any,
16
14
  loaders: Loaders,
15
+ context: MaterialXContext,
16
+ // Optional parameters
17
17
  transparent?: boolean,
18
18
  side?: MaterialParameters['side'],
19
- context: MaterialXContext,
19
+ precision?: MaterialParameters['precision'],
20
20
  }
21
21
 
22
22
  type Uniforms = Record<string, IUniform & { needsUpdate?: boolean }>;
@@ -63,7 +63,7 @@ export class MaterialXMaterial extends ShaderMaterial {
63
63
  vertexShader = vertexShader.replace(/\bi_color_0\b/g, 'color');
64
64
 
65
65
  // Patch fragmentShader
66
- const precision = init.context.getRenderer().capabilities.getMaxPrecision("highp") as Precision;
66
+ const precision = init.precision || "highp" as Precision;
67
67
  fragmentShader = fragmentShader.replace(/precision mediump float;/g, `precision ${precision} float;`);
68
68
  fragmentShader = fragmentShader.replace(/#define M_FLOAT_EPS 1e-8/g, precision === "highp" ? `#define M_FLOAT_EPS 1e-8` : `#define M_FLOAT_EPS 1e-3`);
69
69
 
@@ -170,19 +170,19 @@ export class MaterialXMaterial extends ShaderMaterial {
170
170
  }
171
171
 
172
172
  onBeforeRender(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, _geometry: BufferGeometry, object: Object3D, _group: Group): void {
173
- if (this._context) {
174
- const env = MaterialXEnvironment.get(this._context);
175
- if (env) {
176
- env.update(this._context.getFrame(), this._context.getScene());
177
- this.updateUniforms(env, object, camera);
178
- }
173
+ const time = this._context?.getTime?.() || getTime();
174
+ const frame = this._context?.getFrame?.() || getFrame();
175
+ const env = MaterialXEnvironment.get(_scene);
176
+ if (env) {
177
+ env.update(frame, _scene, _renderer);
178
+ this.updateUniforms(env, object, camera, time, frame);
179
179
  }
180
180
  }
181
181
 
182
182
 
183
183
  envMapIntensity: number = 1.0; // Default intensity for environment map
184
184
  envMap: Texture | null = null; // Environment map texture, can be set externally
185
- updateUniforms = (environment: MaterialXEnvironment, object: Object3D, camera: Camera) => {
185
+ updateUniforms = (environment: MaterialXEnvironment, object: Object3D, camera: Camera, time?: number, frame?: number) => {
186
186
 
187
187
  const uniforms = this.uniforms as Uniforms;
188
188
 
@@ -208,15 +208,15 @@ export class MaterialXMaterial extends ShaderMaterial {
208
208
  }
209
209
 
210
210
  // Update time uniforms
211
- if (this._context) {
212
- if (uniforms.u_time) {
213
- uniforms.u_time.value = this._context.getTime();
214
- }
215
- if (uniforms.u_frame) {
216
- uniforms.u_frame.value = this._context.getFrame();
217
- }
211
+ if (uniforms.u_time) {
212
+ if (time === undefined) time = getTime();
213
+ uniforms.u_time.value = time;
218
214
  }
219
-
215
+ if (uniforms.u_frame) {
216
+ if (frame === undefined) frame = getFrame();
217
+ uniforms.u_frame.value = frame;
218
+ }
219
+
220
220
  // Update light uniforms
221
221
  this.updateEnvironmentUniforms(environment);
222
222
 
package/src/materialx.ts CHANGED
@@ -7,10 +7,8 @@ import { registerLights, getLightData, LightData } from "./materialx.helper.js";
7
7
  import type { MaterialXMaterial } from "./materialx.material.js";
8
8
 
9
9
  export type MaterialXContext = {
10
- getTime(): number,
11
- getFrame(): number,
12
- getScene(): Scene,
13
- getRenderer(): WebGLRenderer,
10
+ getTime?(): number,
11
+ getFrame?(): number,
14
12
  }
15
13
 
16
14
 
@@ -114,23 +112,21 @@ type EnvironmentTextureSet = {
114
112
  }
115
113
 
116
114
 
117
-
118
- type EnvironmentContext = Pick<MaterialXContext, "getRenderer" | "getScene">;
119
115
  /**
120
116
  * MaterialXEnvironment manages the environment settings for MaterialX materials.
121
117
  */
122
118
  export class MaterialXEnvironment {
123
119
 
124
- static get(context: EnvironmentContext): MaterialXEnvironment | null {
125
- return this.getEnvironment(context);
120
+ static get(scene: Scene): MaterialXEnvironment | null {
121
+ return this.getEnvironment(scene);
126
122
  }
127
- private static _environments: WeakMap<EnvironmentContext, MaterialXEnvironment> = new Map();
128
- private static getEnvironment(context: EnvironmentContext): MaterialXEnvironment {
129
- if (this._environments.has(context)) {
130
- return this._environments.get(context)!;
123
+ private static _environments: WeakMap<Scene, MaterialXEnvironment> = new Map();
124
+ private static getEnvironment(scene: Scene): MaterialXEnvironment {
125
+ if (this._environments.has(scene)) {
126
+ return this._environments.get(scene)!;
131
127
  }
132
- const env = new MaterialXEnvironment(context);
133
- this._environments.set(context, env);
128
+ const env = new MaterialXEnvironment(scene);
129
+ this._environments.set(scene, env);
134
130
  return env;
135
131
  }
136
132
 
@@ -143,22 +139,22 @@ export class MaterialXEnvironment {
143
139
  private _isInitialized: boolean = false;
144
140
  private _lastUpdateFrame: number = -1;
145
141
 
146
- constructor(private _context: EnvironmentContext) {
142
+ constructor(private _scene: Scene) {
147
143
  if (debug) console.log("[MaterialX] Environment created");
148
144
  }
149
145
 
150
146
  // Initialize with Needle Engine context
151
- async initialize(): Promise<boolean> {
147
+ async initialize(renderer: WebGLRenderer): Promise<boolean> {
152
148
  if (this._initializePromise) {
153
149
  return this._initializePromise;
154
150
  }
155
- this._initializePromise = this._initialize();
151
+ this._initializePromise = this._initialize(renderer);
156
152
  return this._initializePromise;
157
153
  }
158
154
 
159
- update(frame: number, scene: Scene) {
155
+ update(frame: number, scene: Scene, renderer: WebGLRenderer): void {
160
156
  if (!this._initializePromise) {
161
- this.initialize();
157
+ this.initialize(renderer);
162
158
  return;
163
159
  }
164
160
  if (!this._isInitialized) {
@@ -205,6 +201,7 @@ export class MaterialXEnvironment {
205
201
  this._lightCount = 0;
206
202
  this._pmremGenerator?.dispose();
207
203
  this._pmremGenerator = null;
204
+ this._renderer = null;
208
205
  for (const textureSet of this._texturesCache.values()) {
209
206
  textureSet.radianceTexture?.dispose();
210
207
  textureSet.irradianceTexture?.dispose();
@@ -221,15 +218,17 @@ export class MaterialXEnvironment {
221
218
  // If the material has its own envMap, we don't use the irradiance texture
222
219
  return this._getTextures(material.envMap);
223
220
  }
224
- return this._getTextures(this._context?.getScene().environment);
221
+ return this._getTextures(this._scene.environment);
225
222
  }
226
223
 
227
224
  private _pmremGenerator: PMREMGenerator | null = null;
225
+ private _renderer: WebGLRenderer | null = null;
228
226
  private readonly _texturesCache: Map<Texture | null, EnvironmentTextureSet> = new Map();
229
227
 
230
- private async _initialize(): Promise<boolean> {
228
+ private async _initialize(renderer: WebGLRenderer): Promise<boolean> {
231
229
  this._isInitialized = false;
232
- this._pmremGenerator = new PMREMGenerator(this._context.getRenderer());
230
+ this._pmremGenerator = new PMREMGenerator(renderer);
231
+ this._renderer = renderer;
233
232
  this.updateLighting(true);
234
233
  this._isInitialized = true;
235
234
  return true;
@@ -244,11 +243,11 @@ export class MaterialXEnvironment {
244
243
  return res;
245
244
  }
246
245
 
247
- if (this._context && this._pmremGenerator && texture) {
246
+ if (this._scene && this._pmremGenerator && this._renderer && texture) {
248
247
  if (debug) console.log("[MaterialX] Generating environment textures", texture.name);
249
248
  const target = this._pmremGenerator.fromEquirectangular(texture);
250
- const radianceRenderTarget = renderPMREMToEquirect(this._context.getRenderer(), target.texture, 0.0, 1024, 512, target.height);
251
- const irradianceRenderTarget = renderPMREMToEquirect(this._context.getRenderer(), target.texture, 1.0, 32, 16, target.height);
249
+ const radianceRenderTarget = renderPMREMToEquirect(this._renderer, target.texture, 0.0, 1024, 512, target.height);
250
+ const irradianceRenderTarget = renderPMREMToEquirect(this._renderer, target.texture, 1.0, 32, 16, target.height);
252
251
  target.dispose();
253
252
  res = {
254
253
  radianceTexture: radianceRenderTarget.texture,
@@ -266,11 +265,11 @@ export class MaterialXEnvironment {
266
265
  }
267
266
 
268
267
  private updateLighting = (collectLights: boolean = false) => {
269
- if (!this._context) return;
268
+ if (!this._scene) return;
270
269
  // Find lights in scene
271
270
  if (collectLights) {
272
271
  const lights = new Array<Light>();
273
- this._context.getScene().traverse((object: Object3D) => {
272
+ this._scene.traverse((object: Object3D) => {
274
273
  if ((object as Light).isLight && object.visible)
275
274
  lights.push(object as Light);
276
275
  });
package/src/utils.ts CHANGED
@@ -10,70 +10,20 @@ export const debug = getParam("debugmaterialx");
10
10
  export const debugUpdate = debug === "update";
11
11
 
12
12
 
13
- /**
14
- * =====================================
15
- * Unused
16
- */
17
-
18
- // Patch WebGL2 methods for debugging purposes
19
-
20
- // const patchWebGL2 = () => {
21
- // const getUniformLocation = WebGL2RenderingContext.prototype.getUniformLocation;
22
- // const programAndNameToUniformLocation = new WeakMap<WebGLUniformLocation, { program: WebGLProgram, name: string }>();
23
- // WebGL2RenderingContext.prototype.getUniformLocation = function (program: WebGLProgram, name: string) {
24
- // const location = getUniformLocation.call(this, program, name);
25
- // if (location) {
26
- // programAndNameToUniformLocation.set(location, { program, name });
27
- // }
28
- // return location;
29
- // };
30
-
31
- // const uniform4fv = WebGL2RenderingContext.prototype.uniform4fv;
32
- // WebGL2RenderingContext.prototype.uniform4fv = function (location: WebGLUniformLocation | null, v: Float32Array | number[]) {
33
- // if (location) {
34
- // const uniformName = programAndNameToUniformLocation.get(location);
35
- // if (true) console.log("Calling uniform4fv", { location, v, name: uniformName?.name });
36
- // }
37
- // return uniform4fv.call(this, location, v);
38
- // };
39
-
40
- // const uniform3fv = WebGL2RenderingContext.prototype.uniform3fv;
41
- // WebGL2RenderingContext.prototype.uniform3fv = function (location: WebGLUniformLocation | null, v: Float32Array | number[]) {
42
- // if (location) {
43
- // const uniformName = programAndNameToUniformLocation.get(location);
44
- // if (true) console.log("Calling uniform3fv", { location, v, name: uniformName?.name });
45
- // }
46
- // return uniform3fv.call(this, location, v);
47
- // };
48
-
49
- // const uniform3iv = WebGL2RenderingContext.prototype.uniform3iv;
50
- // WebGL2RenderingContext.prototype.uniform3iv = function (location: WebGLUniformLocation | null, v: Int32Array | number[]) {
51
- // if (location) {
52
- // const uniformName = programAndNameToUniformLocation.get(location);
53
- // if (true) console.log("Calling uniform3iv", { location, v, name: uniformName?.name });
54
- // }
55
- // return uniform3iv.call(this, location, v);
56
- // };
57
-
58
- // const uniform3uiv = WebGL2RenderingContext.prototype.uniform3uiv;
59
- // WebGL2RenderingContext.prototype.uniform3uiv = function (location: WebGLUniformLocation | null, v: Uint32Array | number[]) {
60
- // if (location) {
61
- // const uniformName = programAndNameToUniformLocation.get(location);
62
- // if (true) console.log("Calling uniform3uiv", { location, v, name: uniformName?.name });
63
- // }
64
- // return uniform3uiv.call(this, location, v);
65
- // };
13
+ let time = 0;
14
+ export function getTime() {
15
+ return time;
16
+ }
17
+ let frame = 0;
18
+ export function getFrame() {
19
+ return frame;
20
+ }
66
21
 
67
- // const uniform3f = WebGL2RenderingContext.prototype.uniform3f;
68
- // WebGL2RenderingContext.prototype.uniform3f = function (location: WebGLUniformLocation
69
- // | null, x: number, y: number, z: number) {
70
- // if (location) {
71
- // const uniformName = programAndNameToUniformLocation.get(location);
72
- // if (uniformName?.name !== "diffuse")
73
- // if (true) console.log("Calling uniform3f", { location, x, y, z, name: uniformName?.name });
74
- // }
75
- // return uniform3f.call(this, location, x, y, z);
76
- // };
77
- // };
78
- // // patchWebGL2();
22
+ const performance = window.performance || (window as any).webkitPerformance || (window as any).mozPerformance;
23
+ function updateTime() {
24
+ time = performance.now() / 1000; // Convert to seconds
25
+ frame++;
26
+ window.requestAnimationFrame(updateTime);
27
+ }
28
+ window.requestAnimationFrame(updateTime);
79
29