@needle-tools/materialx 1.4.2 → 1.4.3

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
@@ -0,0 +1,5 @@
1
+ {
2
+ "commit": "309ccca5d7788f90d773248c88498ddc203dc260",
3
+ "version": "1.39.4",
4
+ "buildDate": "2026-02-27 14:35:28"
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/materialx",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -24,6 +24,9 @@
24
24
  "peerDependencies": {
25
25
  "three": ">=0.160.0"
26
26
  },
27
+ "scripts": {
28
+ "test": "node --test tests/unit/**/*.test.js"
29
+ },
27
30
  "devDependencies": {
28
31
  "@needle-tools/engine": "4.x",
29
32
  "@types/three": "0.169.0",
package/src/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { ready, type MaterialXContext, preloadWasm } from "./materialx.js";
2
+ export { MaterialXEnvironment } from "./materialx.js";
2
3
  export { MaterialXMaterial } from "./materialx.material.js";
3
4
  export { MaterialXLoader } from "./loader/loader.three.js";
4
5
 
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createMaterialXMaterial } from "./loader/loader.three.js";
2
2
 
3
3
  export { ready, preloadWasm } from "./materialx.js";
4
+ export { MaterialXEnvironment } from "./materialx.js";
4
5
  export { MaterialXMaterial } from "./materialx.material.js";
5
6
  export { MaterialXLoader } from "./loader/loader.three.js";
6
7
 
@@ -1,6 +1,29 @@
1
1
  import { Light, Scene, Texture, WebGLRenderer } from "three";
2
2
  import type { MaterialX as MX } from "./materialx.types.js";
3
3
 
4
+ /**
5
+ * Override the base URL for MaterialX WASM files.
6
+ * Set this global **before** any MaterialX code runs to load WASM from a custom location.
7
+ *
8
+ * Special values:
9
+ * - `"package"`: Use local files bundled with the npm package
10
+ * - `"/custom/path/"`: Use a custom base URL (must end with `/`)
11
+ * - `undefined`: Use default CDN
12
+ *
13
+ * @default `https://cdn.needle.tools/static/materialx/<version>/`
14
+ * @example
15
+ * ```js
16
+ * // Use package-local files (same as "bin/")
17
+ * globalThis.NEEDLE_MATERIALX_LOCATION = "package";
18
+ *
19
+ * // Use custom CDN or self-hosted files
20
+ * globalThis.NEEDLE_MATERIALX_LOCATION = "/assets/materialx/";
21
+ * ```
22
+ */
23
+ declare global {
24
+ var NEEDLE_MATERIALX_LOCATION: string | undefined;
25
+ }
26
+
4
27
  export function preloadWasm(trigger: "immediately" | "network_idle"): Promise<void>;
5
28
 
6
29
  export type MaterialXContext = {
@@ -47,6 +70,11 @@ export declare class MaterialXEnvironment {
47
70
  initialize(renderer: WebGLRenderer): Promise<boolean>;
48
71
  update(frame: number, scene: Scene, renderer: WebGLRenderer): void;
49
72
  reset(): void;
73
+ /**
74
+ * Re-collect lights from the scene and rebuild light data.
75
+ * Call this after adding/removing lights, toggling visibility, or changing light properties.
76
+ */
77
+ refreshLights(): void;
50
78
 
51
79
  get lights(): Array<Light>;
52
80
  get lightData(): any[] | null;
@@ -22,6 +22,8 @@ export function getLightRotation(): THREE.Matrix4;
22
22
 
23
23
  export function findLights(doc: any): Array<any>;
24
24
 
25
+ export function getLightTypeIds(): { directional: number, point: number, spot: number };
26
+
25
27
  export function registerLights(mx: any, genContext: any): Promise<void>;
26
28
 
27
29
  export function getLightData(lights: Array<THREE.Light>, genContext: any): { lightData: LightData[], lightCount: number };
@@ -211,6 +211,9 @@ function toThreeUniform(uniforms, type, value, name, loaders, searchPath) {
211
211
  if (checkCache) addToCache(cacheKey, promise);
212
212
 
213
213
  promise?.then(res => {
214
+ // Replace Promise cache entry with the resolved texture value.
215
+ // This avoids keeping long-lived promise/closure graphs in THREE.Cache.
216
+ if (checkCache && res) addToCache(cacheKey, res);
214
217
  if (res) uniform.value = /** @type {any} */ (res);
215
218
  else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
216
219
  });
@@ -313,6 +316,19 @@ export function findLights(doc) {
313
316
  /** @type {Object<string, number>} */
314
317
  let lightTypesBound = {};
315
318
 
319
+ /**
320
+ * Returns the current mapping of MaterialX light definition names to their type IDs.
321
+ * These IDs are used in the shader's LightData.type field.
322
+ * @returns {{ directional: number, point: number, spot: number }}
323
+ */
324
+ export function getLightTypeIds() {
325
+ return {
326
+ directional: lightTypesBound['ND_directional_light'] || 1,
327
+ point: lightTypesBound['ND_point_light'] || 2,
328
+ spot: lightTypesBound['ND_spot_light'] || 3,
329
+ };
330
+ }
331
+
316
332
  /**
317
333
  * Register lights in shader generation context
318
334
  * @param {any} mx - MaterialX Module
@@ -373,6 +389,19 @@ export async function registerLights(mx, genContext) {
373
389
  }
374
390
 
375
391
  const _lightTypeWarnings = {}
392
+ const _emptyLightPosition = new THREE.Vector3(0, 0, 0);
393
+ const _emptyLightDirection = new THREE.Vector3(0, 0, -1);
394
+ const _emptyLightColor = new THREE.Color(0, 0, 0);
395
+ const _emptyLight = Object.freeze({
396
+ type: 0,
397
+ position: _emptyLightPosition,
398
+ direction: _emptyLightDirection,
399
+ color: _emptyLightColor,
400
+ intensity: 0.0,
401
+ decay_rate: 2.0,
402
+ inner_angle: 0.0,
403
+ outer_angle: 0.0,
404
+ });
376
405
  /**
377
406
  * Converts Three.js light type to MaterialX node name
378
407
  * @param {string} threeLightType
@@ -434,8 +463,21 @@ export function getLightData(lights, genContext) {
434
463
  }
435
464
 
436
465
  const wp = light.getWorldPosition(new THREE.Vector3());
437
- const wq = light.getWorldQuaternion(new THREE.Quaternion());
438
- const wd = new THREE.Vector3(0, 0, -1).applyQuaternion(wq);
466
+ // For target-based lights (SpotLight, DirectionalLight), compute direction from position to target.
467
+ // Using getWorldQuaternion() doesn't work because Three.js target-based lights don't set rotation.
468
+ let wd;
469
+ const directionalOrSpot = (/** @type {THREE.DirectionalLight | THREE.SpotLight | null} */ (
470
+ (/** @type {THREE.DirectionalLight} */ (light)).isDirectionalLight || (/** @type {THREE.SpotLight} */ (light)).isSpotLight
471
+ ? light
472
+ : null
473
+ ));
474
+ if (directionalOrSpot) {
475
+ const targetPos = directionalOrSpot.target.getWorldPosition(new THREE.Vector3());
476
+ wd = targetPos.sub(wp).normalize();
477
+ } else {
478
+ const wq = light.getWorldQuaternion(new THREE.Quaternion());
479
+ wd = new THREE.Vector3(0, 0, -1).applyQuaternion(wq);
480
+ }
439
481
 
440
482
  // Shader math from the generated MaterialX shader:
441
483
  // float low = min(light.inner_angle, light.outer_angle);
@@ -452,7 +494,7 @@ export function getLightData(lights, genContext) {
452
494
  type: lightTypesBound[lightDefinitionName],
453
495
  position: wp.clone(),
454
496
  direction: wd.clone(),
455
- color: new THREE.Color().fromArray(light.color.toArray()),
497
+ color: light.color.clone(),
456
498
  // Luminous efficacy for converting radiant power in watts (W) to luminous flux in lumens (lm) at a wavelength of 555 nm.
457
499
  // Also, three.js lights don't have PI scale baked in, but MaterialX does, so we need to divide by PI for point and spot lights.
458
500
  intensity: light.intensity * (/** @type {THREE.PointLight} */ (light).isPointLight ? 683.0 / 3.1415 : /** @type {THREE.SpotLight} */ (light).isSpotLight ? 683.0 / 3.1415 : 1.0),
@@ -468,17 +510,7 @@ export function getLightData(lights, genContext) {
468
510
 
469
511
  // If we don't have enough entries in lightData, fill with empty lights
470
512
  while (lightData.length < maxLightCount) {
471
- const emptyLight = {
472
- type: 0, // Default light type
473
- position: new THREE.Vector3(0, 0, 0),
474
- direction: new THREE.Vector3(0, 0, -1),
475
- color: new THREE.Color(0, 0, 0),
476
- intensity: 0.0,
477
- decay_rate: 2.0,
478
- inner_angle: 0.0,
479
- outer_angle: 0.0,
480
- };
481
- lightData.push(emptyLight);
513
+ lightData.push(_emptyLight);
482
514
  }
483
515
 
484
516
  if (debugUpdate) console.log("Registered lights in MaterialX context", lightTypesBound, lightData);
package/src/materialx.js CHANGED
@@ -48,29 +48,41 @@ export async function ready() {
48
48
  if (debug) console.log("[MaterialX] Initializing WASM module...");
49
49
  try {
50
50
 
51
- const useRemoteUrls = true;
52
- const remoteVersion = "1.4.0";
51
+ // NOTE: This must be a plain string literal (not a template) so that the
52
+ // makeFilesLocal Vite plugin can statically detect and localize this URL.
53
+ const defaultBaseUrl = "https://cdn.needle.tools/static/materialx/1.4.3/";
53
54
 
54
55
  /** @type {Array<string>} */
55
56
  let urls;
56
57
 
57
- if (useRemoteUrls) {
58
- urls = [
59
- `https://cdn.needle.tools/static/materialx/${remoteVersion}/JsMaterialXCore.wasm`,
60
- `https://cdn.needle.tools/static/materialx/${remoteVersion}/JsMaterialXGenShader.wasm`,
61
- `https://cdn.needle.tools/static/materialx/${remoteVersion}/JsMaterialXGenShader.data.txt`,
62
- ];
63
- } else {
64
- // For local paths, import the files as assets so Vite copies them to the dist folder
58
+ const location = globalThis.NEEDLE_MATERIALX_LOCATION;
59
+
60
+ if (location === "package" || location === "bin/" || location === "./bin/" || location === "../bin/") {
61
+ // Use local files from the @needle-tools/materialx npm package.
62
+ // Vite's ?url suffix copies these files to the output directory
63
+ // and returns their URL automatically — no CDN download needed.
65
64
  urls = await Promise.all([
66
- /** @ts-ignore */
67
- import( /* @vite-ignore */ `../bin/JsMaterialXCore.wasm?url`).then(m => m.default || m),
68
- /** @ts-ignore */
69
- import( /* @vite-ignore */ `../bin/JsMaterialXGenShader.wasm?url`).then(m => m.default || m),
70
- /** @ts-ignore */
71
- import( /* @vite-ignore */ `../bin/JsMaterialXGenShader.data.txt?url`).then(m => m.default || m),
65
+ import('../bin/JsMaterialXCore.wasm?url').then(m => m.default || m),
66
+ import('../bin/JsMaterialXGenShader.wasm?url').then(m => m.default || m),
67
+ import('../bin/JsMaterialXGenShader.data.txt?url').then(m => m.default || m),
72
68
  ]);
73
69
  }
70
+ else if (location) {
71
+ // Custom path: use as base URL for CDN or self-hosted files
72
+ urls = [
73
+ location + "JsMaterialXCore.wasm",
74
+ location + "JsMaterialXGenShader.wasm",
75
+ location + "JsMaterialXGenShader.data.txt",
76
+ ];
77
+ }
78
+ else {
79
+ // Default: fetch from CDN (or from local files if makeFilesLocal rewrites this URL)
80
+ urls = [
81
+ defaultBaseUrl + "JsMaterialXCore.wasm",
82
+ defaultBaseUrl + "JsMaterialXGenShader.wasm",
83
+ defaultBaseUrl + "JsMaterialXGenShader.data.txt",
84
+ ];
85
+ }
74
86
  const [JsMaterialXCore, JsMaterialXGenShader, JsMaterialXGenShader_data] = urls;
75
87
 
76
88
  const module = await MaterialX({
@@ -125,6 +137,7 @@ export async function ready() {
125
137
  // Set a reasonable default for max active lights
126
138
  state.materialXGenContext.getOptions().hwMaxActiveLightSources = 4;
127
139
 
140
+ // We use Three.js shadow maps instead of MaterialX's own shadow implementation
128
141
  // state.materialXGenContext.getOptions().hwShadowMap = true;
129
142
 
130
143
  // This prewarms the shader generation context to have all light types
@@ -159,7 +172,7 @@ export class MaterialXEnvironment {
159
172
  }
160
173
 
161
174
  /** @type {WeakMap<Scene, MaterialXEnvironment>} */
162
- static _environments = new Map();
175
+ static _environments = new WeakMap();
163
176
 
164
177
  /**
165
178
  * @param {Scene} scene
@@ -370,6 +383,9 @@ export class MaterialXEnvironment {
370
383
  if ((/** @type {Light} */ (object)).isLight && object.visible)
371
384
  lights.push(/** @type {Light} */(object));
372
385
  });
386
+ // Keep the same ordering strategy as Three.js (shadow casters first)
387
+ // and do this only when re-collecting lights to avoid per-frame sort allocations.
388
+ lights.sort((a, b) => (b.castShadow ? 1 : 0) - (a.castShadow ? 1 : 0));
373
389
  this._lights = lights;
374
390
  }
375
391
 
@@ -377,6 +393,15 @@ export class MaterialXEnvironment {
377
393
  const { lightData, lightCount } = getLightData(this._lights, state.materialXGenContext);
378
394
  this._lightData = lightData;
379
395
  this._lightCount = lightCount;
396
+ // Note: Shadow data is now handled by Three.js when lights: true is set on the material
380
397
  }
381
398
  }
399
+
400
+ /**
401
+ * Re-collect lights from the scene and rebuild light data.
402
+ * Call this after adding/removing lights, toggling visibility, or changing light properties.
403
+ */
404
+ refreshLights() {
405
+ this.updateLighting(true);
406
+ }
382
407
  }
@@ -1,8 +1,8 @@
1
- import { BufferGeometry, Camera, FrontSide, GLSL3, Group, Matrix3, Matrix4, Object3D, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
1
+ import { BufferGeometry, Camera, FrontSide, GLSL3, Group, Matrix3, Matrix4, Object3D, Scene, ShaderMaterial, Texture, UniformsLib, Vector3, WebGLRenderer } from "three";
2
2
  import { debug, getFrame, getTime } from "./utils.js";
3
3
  import { MaterialXEnvironment } from "./materialx.js";
4
- import { generateMaterialPropertiesForUniforms, getUniformValues } from "./materialx.helper.js";
5
- import { cloneUniforms, cloneUniformsGroups } from "three/src/renderers/shaders/UniformsUtils.js";
4
+ import { generateMaterialPropertiesForUniforms, getUniformValues, getLightTypeIds } from "./materialx.helper.js";
5
+ import { cloneUniforms, cloneUniformsGroups, mergeUniforms } from "three/src/renderers/shaders/UniformsUtils.js";
6
6
 
7
7
  // Add helper matrices for uniform updates (similar to MaterialX example)
8
8
  const normalMat = new Matrix3();
@@ -35,8 +35,10 @@ export class MaterialXMaterial extends ShaderMaterial {
35
35
  */
36
36
  copy(source) {
37
37
  super.copy(source);
38
+ this.shaderName = source.shaderName;
38
39
  this._context = source._context;
39
40
  this._shader = source._shader;
41
+ this._needsTangents = source._needsTangents;
40
42
  this.uniforms = cloneUniforms(source.uniforms);
41
43
  this.uniformsGroups = cloneUniformsGroups(source.uniformsGroups);
42
44
  this.envMapIntensity = source.envMapIntensity;
@@ -59,16 +61,21 @@ export class MaterialXMaterial extends ShaderMaterial {
59
61
  */
60
62
  constructor(init) {
61
63
 
62
- // TODO: we need to properly copy the uniforms and other properties from the source material
63
- if (!init) {
64
- super();
65
- return;
66
- }
64
+ /** @type {import('three').ShaderMaterialParameters | undefined} */
65
+ let materialParameters = undefined;
66
+ /** @type {string} */
67
+ let vertexShader = "";
68
+ /** @type {string} */
69
+ let fragmentShader = "";
70
+ /** @type {Record<string, string>} */
71
+ let defines = {};
72
+
73
+ if (init) {
67
74
 
68
75
  // Get vertex and fragment shader source, and remove #version directive for newer js.
69
76
  // It's added by three.js glslVersion.
70
- let vertexShader = init.shader.getSourceCode("vertex");
71
- let fragmentShader = init.shader.getSourceCode("pixel");
77
+ vertexShader = init.shader.getSourceCode("vertex");
78
+ fragmentShader = init.shader.getSourceCode("pixel");
72
79
 
73
80
  vertexShader = vertexShader.replace(/^#version.*$/gm, '').trim();
74
81
  fragmentShader = fragmentShader.replace(/^#version.*$/gm, '').trim();
@@ -108,6 +115,12 @@ export class MaterialXMaterial extends ShaderMaterial {
108
115
  fragmentShader = fragmentShader.replace(/\bi_tangent\b/g, 'tangent');
109
116
  fragmentShader = fragmentShader.replace(/\bi_color_0\b/g, 'color');
110
117
 
118
+ // Patch env intensity uniform to match Three.js naming convention.
119
+ // MaterialX generates `u_envLightIntensity`; Three.js uses `envMapIntensity`.
120
+ // This lets us combine material.envMapIntensity * scene.environmentIntensity
121
+ // the same way MeshStandardMaterial does.
122
+ fragmentShader = fragmentShader.replace(/\bu_envLightIntensity\b/g, 'envMapIntensity');
123
+
111
124
  // Capture some vertex shader properties
112
125
  const uv_is_vec2 = vertexShader.includes('in vec2 uv;'); // check if uv is vec2; e.g. https://matlib.gpuopen.com/main/materials/all?material=da6ec531-f5c1-4790-ac14-8a5c51d0314e
113
126
  const uv1_is_vec2 = vertexShader.includes('in vec2 uv1;');
@@ -155,16 +168,154 @@ export class MaterialXMaterial extends ShaderMaterial {
155
168
  #include <tonemapping_fragment>
156
169
  #include <colorspace_fragment>`);
157
170
 
158
- const defines = {};
171
+ defines = {};
159
172
  if (hasUv1) defines['USE_UV1'] = '';
160
173
  if (hasUv2) defines['USE_UV2'] = '';
161
174
  if (hasUv3) defines['USE_UV3'] = '';
162
175
  if (hasTangent) defines['USE_TANGENT'] = '';
163
176
  if (hasColor) defines['USE_COLOR'] = '';
164
177
 
165
- const searchPath = ""; // Could be derived from the asset path if needed
178
+ // Add Three.js shadow support
179
+ // Insert shadow pars before main() in vertex shader
180
+ vertexShader = vertexShader.replace(
181
+ /void\s+main\s*\(\s*\)\s*\{/,
182
+ `#include <common>
183
+ #include <shadowmap_pars_vertex>
184
+ void main() {`
185
+ );
186
+
187
+ // Insert shadow vertex calculation at the end of vertex main (before the closing brace)
188
+ // We need to compute worldPosition and transformedNormal for shadow coords
189
+ // Note: Three.js shadowmap_vertex expects transformedNormal in VIEW space:
190
+ // it does `inverseTransformDirection(transformedNormal, viewMatrix)` to get world-space normal
191
+ vertexShader = vertexShader.replace(
192
+ /(\n\s*)\}(\s*)$/,
193
+ `$1 // Three.js shadow support
194
+ $1 vec4 worldPosition = u_worldMatrix * vec4(position, 1.0);
195
+ $1 vec3 transformedNormal = mat3(viewMatrix) * normalWorld;
196
+ $1 #include <shadowmap_vertex>
197
+ $1}$2`
198
+ );
199
+
200
+ // Insert shadow includes at the very beginning of the fragment shader (after precision)
201
+ // This ensures DirectionalLightShadow struct is defined before getMxShadow uses it
202
+ fragmentShader = fragmentShader.replace(
203
+ /(precision\s+\w+\s+float;)/,
204
+ `$1
205
+
206
+ #include <common>
207
+ #include <packing>
208
+ #include <shadowmap_pars_fragment>`
209
+ );
210
+
211
+ // Get MaterialX light type IDs for shadow dispatch
212
+ const lightTypeIds = getLightTypeIds();
213
+
214
+ // Generate GLSL helper functions that sample shadow maps using constant indices.
215
+ // Sampler arrays require constant integral expression indices in GLSL ES 3.0,
216
+ // so we use if/else chains with literal constants (guarded by preprocessor).
217
+ const MAX_SHADOW_LIGHTS = 4; // max shadow-casting lights per type
218
+
219
+ let dirShadowCases = '';
220
+ for (let i = 0; i < MAX_SHADOW_LIGHTS; i++) {
221
+ dirShadowCases += `
222
+ #if NUM_DIR_LIGHT_SHADOWS > ${i}
223
+ ${i > 0 ? 'else ' : ''}if (idx == ${i}) {
224
+ DirectionalLightShadow s = directionalLightShadows[${i}];
225
+ return getShadow(directionalShadowMap[${i}], s.shadowMapSize, s.shadowIntensity, s.shadowBias, s.shadowRadius, vDirectionalShadowCoord[${i}]);
226
+ }
227
+ #endif`;
228
+ }
229
+
230
+ let spotShadowCases = '';
231
+ for (let i = 0; i < MAX_SHADOW_LIGHTS; i++) {
232
+ spotShadowCases += `
233
+ #if NUM_SPOT_LIGHT_SHADOWS > ${i}
234
+ ${i > 0 ? 'else ' : ''}if (idx == ${i}) {
235
+ SpotLightShadow s = spotLightShadows[${i}];
236
+ return getShadow(spotShadowMap[${i}], s.shadowMapSize, s.shadowIntensity, s.shadowBias, s.shadowRadius, vSpotLightCoord[${i}]);
237
+ }
238
+ #endif`;
239
+ }
240
+
241
+ let pointShadowCases = '';
242
+ for (let i = 0; i < MAX_SHADOW_LIGHTS; i++) {
243
+ pointShadowCases += `
244
+ #if NUM_POINT_LIGHT_SHADOWS > ${i}
245
+ ${i > 0 ? 'else ' : ''}if (idx == ${i}) {
246
+ PointLightShadow s = pointLightShadows[${i}];
247
+ return getPointShadow(pointShadowMap[${i}], s.shadowMapSize, s.shadowIntensity, s.shadowBias, s.shadowRadius, vPointShadowCoord[${i}], s.shadowCameraNear, s.shadowCameraFar);
248
+ }
249
+ #endif`;
250
+ }
251
+
252
+ // Insert getMxShadow helper function BEFORE sampleLightSource (so it's defined when used)
253
+ // Supports directional, spot, and point light shadows.
254
+ // Uses global per-type counters to track which shadow map index to use.
255
+ fragmentShader = fragmentShader.replace(
256
+ /void sampleLightSource\(LightData light, vec3 position, out lightshader result\)/,
257
+ `// MaterialX light type IDs (from registerLights)
258
+ #define MX_LIGHT_TYPE_DIRECTIONAL ${lightTypeIds.directional}
259
+ #define MX_LIGHT_TYPE_POINT ${lightTypeIds.point}
260
+ #define MX_LIGHT_TYPE_SPOT ${lightTypeIds.spot}
261
+
262
+ // Per-type shadow index counters (global so they persist across sampleLightSource calls)
263
+ int mxDirShadowIdx = 0;
264
+ int mxSpotShadowIdx = 0;
265
+ int mxPointShadowIdx = 0;
266
+
267
+ // Shadow sampling helpers using constant indices (required for sampler arrays in GLSL ES 3.0)
268
+ float sampleMxDirShadow(int idx) {
269
+ #ifdef USE_SHADOWMAP
270
+ #if NUM_DIR_LIGHT_SHADOWS > 0
271
+ ${dirShadowCases}
272
+ #endif
273
+ #endif
274
+ return 1.0;
275
+ }
276
+
277
+ float sampleMxSpotShadow(int idx) {
278
+ #ifdef USE_SHADOWMAP
279
+ #if NUM_SPOT_LIGHT_SHADOWS > 0
280
+ ${spotShadowCases}
281
+ #endif
282
+ #endif
283
+ return 1.0;
284
+ }
285
+
286
+ float sampleMxPointShadow(int idx) {
287
+ #ifdef USE_SHADOWMAP
288
+ #if NUM_POINT_LIGHT_SHADOWS > 0
289
+ ${pointShadowCases}
290
+ #endif
291
+ #endif
292
+ return 1.0;
293
+ }
294
+
295
+ void sampleLightSource(LightData light, vec3 position, out lightshader result)`
296
+ );
297
+
298
+ // Find the sampleLightSource function and add shadow + counter increment at the end.
299
+ // The per-type counters track which Three.js shadow map index to use for each light type.
300
+ // Lights must be sorted (shadow-casting first per type) to match Three.js shadow map ordering.
301
+ fragmentShader = fragmentShader.replace(
302
+ /(void sampleLightSource\(LightData light, vec3 position, out lightshader result\)\s*\{[\s\S]*?)(^\})/m,
303
+ `$1 // Apply Three.js shadow and increment per-type shadow counters
304
+ if (light.type == MX_LIGHT_TYPE_DIRECTIONAL) {
305
+ result.intensity *= sampleMxDirShadow(mxDirShadowIdx);
306
+ mxDirShadowIdx++;
307
+ } else if (light.type == MX_LIGHT_TYPE_SPOT) {
308
+ result.intensity *= sampleMxSpotShadow(mxSpotShadowIdx);
309
+ mxSpotShadowIdx++;
310
+ } else if (light.type == MX_LIGHT_TYPE_POINT) {
311
+ result.intensity *= sampleMxPointShadow(mxPointShadowIdx);
312
+ mxPointShadowIdx++;
313
+ }
314
+ $2`
315
+ );
316
+
166
317
  const isTransparent = init.parameters?.transparent ?? false;
167
- super({
318
+ materialParameters = {
168
319
  name: init.name,
169
320
  uniforms: {},
170
321
  vertexShader: vertexShader,
@@ -173,14 +324,28 @@ export class MaterialXMaterial extends ShaderMaterial {
173
324
  depthTest: true,
174
325
  depthWrite: !isTransparent,
175
326
  defines: defines,
327
+ lights: true, // Enable Three.js light uniforms
176
328
  ...init.parameters, // Spread any additional parameters passed to the material
177
- });
329
+ };
330
+ }
331
+
332
+ super(materialParameters);
333
+
334
+ // Constructor can be called without init during clone() paths.
335
+ if (!init) {
336
+ return;
337
+ }
338
+
339
+ const searchPath = ""; // Could be derived from the asset path if needed
178
340
  this.shaderName = init.shaderName || null;
179
341
  this._context = init.context;
180
342
  this._shader = init.shader;
181
343
  this._needsTangents = vertexShader.includes('in vec4 tangent;') || vertexShader.includes('in vec3 tangent;');
182
344
 
183
345
  Object.assign(this.uniforms, {
346
+ // Three.js light uniforms (required when lights: true)
347
+ ...UniformsLib.lights,
348
+
184
349
  ...getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath),
185
350
  ...getUniformValues(init.shader.getStage('pixel'), init.loaders, searchPath),
186
351
 
@@ -189,15 +354,13 @@ export class MaterialXMaterial extends ShaderMaterial {
189
354
  u_viewPosition: { value: new Vector3() },
190
355
  u_worldInverseTransposeMatrix: { value: new Matrix4() },
191
356
 
192
- // u_shadowMatrix: { value: new Matrix4() },
193
- // u_shadowMap: { value: null, type: 't' }, // Shadow map
194
-
195
357
  u_envMatrix: { value: new Matrix4() },
196
358
  u_envRadiance: { value: null, type: 't' },
197
359
  u_envRadianceMips: { value: 8, type: 'i' },
198
360
  // TODO we need to figure out how we can set a PMREM here... doing many texture samples is prohibitively expensive
199
361
  u_envRadianceSamples: { value: 8, type: 'i' },
200
362
  u_envIrradiance: { value: null, type: 't' },
363
+ envMapIntensity: { value: 1.0 },
201
364
  u_refractionEnv: { value: true },
202
365
  u_numActiveLightSources: { value: 0 },
203
366
  u_lightData: { value: [], needsUpdate: false }, // Array of light data. We need to set needsUpdate to false until we actually update it
@@ -240,7 +403,7 @@ export class MaterialXMaterial extends ShaderMaterial {
240
403
  const env = MaterialXEnvironment.get(_scene);
241
404
  if (env) {
242
405
  env.update(frame, _scene, renderer);
243
- this.updateEnvironmentUniforms(env);
406
+ this.updateEnvironmentUniforms(env, _scene);
244
407
  }
245
408
  this.updateUniforms(renderer, object, camera, time, frame);
246
409
  }
@@ -312,9 +475,10 @@ export class MaterialXMaterial extends ShaderMaterial {
312
475
 
313
476
  /**
314
477
  * @private
315
- * @param {MaterialXEnvironment} environment
478
+ * @param {MaterialXEnvironment} environment
479
+ * @param {Scene} scene
316
480
  */
317
- updateEnvironmentUniforms = (environment) => {
481
+ updateEnvironmentUniforms = (environment, scene) => {
318
482
 
319
483
  const uniforms = this.uniforms;
320
484
 
@@ -354,6 +518,14 @@ export class MaterialXMaterial extends ShaderMaterial {
354
518
  if (prev != textures.irradianceTexture) uniforms.u_envIrradiance.needsUpdate = true;
355
519
  }
356
520
 
521
+ // Sync environment intensity: combine per-material envMapIntensity with scene.environmentIntensity
522
+ // (mirrors MeshStandardMaterial behaviour in Three.js)
523
+ if (uniforms.envMapIntensity) {
524
+ uniforms.envMapIntensity.value = (this.envMapIntensity ?? 1.0) * (scene.environmentIntensity ?? 1.0);
525
+ }
526
+
527
+ // Note: Shadow uniforms are handled by Three.js when lights: true is set
528
+
357
529
  this.uniformsNeedUpdate = true;
358
530
  }
359
531
  }
package/src/utils.d.ts CHANGED
@@ -17,4 +17,6 @@ export function getTime(): number;
17
17
  export function getFrame(): number;
18
18
 
19
19
 
20
- export function waitForNetworkIdle(): Promise<void>;
20
+ export function waitForNetworkIdle(): Promise<void>;
21
+
22
+ export function isDevEnvironment(): boolean;
@@ -0,0 +1,9 @@
1
+ declare module '*.wasm?url' {
2
+ const url: string;
3
+ export default url;
4
+ }
5
+
6
+ declare module '*.txt?url' {
7
+ const url: string;
8
+ export default url;
9
+ }
package/bin/README.md DELETED
@@ -1,6 +0,0 @@
1
- Source: https://github.com/AcademySoftwareFoundation/MaterialX/tree/gh-pages
2
-
3
- Edits:
4
-
5
- - In `JsMaterialXGenShader.js` added `export default MaterialX;` at bottom
6
- - Renamed `JsMaterialXGenShader.data` to `JsMaterialXGenShader.data.txt` so it can be loaded by vite etc