@needle-tools/materialx 1.6.0 → 1.7.0-next.57183d6

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.
@@ -1,11 +1,299 @@
1
- import { BufferGeometry, Camera, FrontSide, GLSL3, Group, Matrix3, Matrix4, Object3D, Scene, ShaderMaterial, Texture, UniformsLib, Vector3, WebGLRenderer } from "three";
1
+ import { BufferGeometry, Camera, Euler, FrontSide, GLSL3, Group, Matrix4, Object3D, Scene, ShaderMaterial, Texture, Vector3 } from "three";
2
2
  import { debug, getFrame, getTime } from "./utils.js";
3
3
  import { MaterialXEnvironment } from "./materialx.js";
4
4
  import { generateMaterialPropertiesForUniforms, getUniformValues, getLightTypeIds } from "./materialx.helper.js";
5
- import { cloneUniforms, cloneUniformsGroups, mergeUniforms } from "three/src/renderers/shaders/UniformsUtils.js";
5
+
6
+ export const DEFAULT_ENVIRONMENT_RADIANCE_MODE = "three-pmrem";
6
7
 
7
8
  // Add helper matrices for uniform updates (similar to MaterialX example)
8
- const normalMat = new Matrix3();
9
+ const worldInverseMat = new Matrix4();
10
+ const worldTransposeMat = new Matrix4();
11
+ const worldInverseTransposeMat = new Matrix4();
12
+ const worldViewMat = new Matrix4();
13
+ const worldViewProjectionMat = new Matrix4();
14
+ const envMat = new Matrix4();
15
+ const envRotation = new Euler();
16
+
17
+ // Local copy of Three.js UniformsUtils.cloneUniforms from
18
+ // three/src/renderers/shaders/UniformsUtils.js. Keep this in sync with the
19
+ // minimum supported Three.js version when Three changes uniform cloning.
20
+ function cloneUniforms(src) {
21
+ const dst = {};
22
+ for (const uniformName in src) {
23
+ dst[uniformName] = {};
24
+ for (const propertyName in src[uniformName]) {
25
+ const property = src[uniformName][propertyName];
26
+ if (property && (
27
+ property.isColor ||
28
+ property.isMatrix3 || property.isMatrix4 ||
29
+ property.isVector2 || property.isVector3 || property.isVector4 ||
30
+ property.isTexture || property.isQuaternion
31
+ )) {
32
+ dst[uniformName][propertyName] = property.isRenderTargetTexture ? null : property.clone();
33
+ }
34
+ else if (Array.isArray(property)) {
35
+ dst[uniformName][propertyName] = property.slice();
36
+ }
37
+ else {
38
+ dst[uniformName][propertyName] = property;
39
+ }
40
+ }
41
+ }
42
+ return dst;
43
+ }
44
+
45
+ // Local copy of Three.js UniformsUtils.mergeUniforms from
46
+ // three/src/renderers/shaders/UniformsUtils.js. Copied to avoid importing from
47
+ // three/src, which breaks package consumers using Three's public ESM surface.
48
+ function mergeUniforms(uniforms) {
49
+ const merged = {};
50
+ for (const uniformSet of uniforms) {
51
+ const tmp = cloneUniforms(uniformSet);
52
+ for (const propertyName in tmp) {
53
+ merged[propertyName] = tmp[propertyName];
54
+ }
55
+ }
56
+ return merged;
57
+ }
58
+
59
+ // Local copy of Three.js UniformsUtils.cloneUniformsGroups from
60
+ // three/src/renderers/shaders/UniformsUtils.js. Keep alongside cloneUniforms
61
+ // and mergeUniforms so ShaderMaterial.clone() stays behavior-compatible.
62
+ function cloneUniformsGroups(src) {
63
+ return src.map(group => group.clone());
64
+ }
65
+
66
+ function createThreeLightUniforms() {
67
+ return {
68
+ ambientLightColor: { value: [] },
69
+ lightProbe: { value: [] },
70
+ directionalLights: { value: [], properties: { direction: {}, color: {} } },
71
+ directionalLightShadows: { value: [], properties: { shadowIntensity: 1, shadowBias: {}, shadowNormalBias: {}, shadowRadius: {}, shadowMapSize: {} } },
72
+ directionalShadowMap: { value: [] },
73
+ directionalShadowMatrix: { value: [] },
74
+ spotLights: { value: [], properties: { color: {}, position: {}, direction: {}, distance: {}, coneCos: {}, penumbraCos: {}, decay: {} } },
75
+ spotLightShadows: { value: [], properties: { shadowIntensity: 1, shadowBias: {}, shadowNormalBias: {}, shadowRadius: {}, shadowMapSize: {} } },
76
+ spotLightMap: { value: [] },
77
+ spotShadowMap: { value: [] },
78
+ spotLightMatrix: { value: [] },
79
+ pointLights: { value: [], properties: { color: {}, position: {}, decay: {}, distance: {} } },
80
+ pointLightShadows: { value: [], properties: { shadowIntensity: 1, shadowBias: {}, shadowNormalBias: {}, shadowRadius: {}, shadowMapSize: {}, shadowCameraNear: {}, shadowCameraFar: {} } },
81
+ pointShadowMap: { value: [] },
82
+ pointShadowMatrix: { value: [] },
83
+ hemisphereLights: { value: [], properties: { direction: {}, skyColor: {}, groundColor: {} } },
84
+ rectAreaLights: { value: [], properties: { color: {}, position: {}, width: {}, height: {} } },
85
+ ltc_1: { value: null },
86
+ ltc_2: { value: null },
87
+ probesSH: { value: null },
88
+ probesMin: { value: new Vector3() },
89
+ probesMax: { value: new Vector3() },
90
+ probesResolution: { value: new Vector3() },
91
+ };
92
+ }
93
+
94
+ const CUBE_UV_REFLECTION_FUNCTIONS = `
95
+ float mx_cubeuv_getFace(vec3 direction) {
96
+ vec3 absDirection = abs(direction);
97
+ float face = -1.0;
98
+ if (absDirection.x > absDirection.z) {
99
+ if (absDirection.x > absDirection.y)
100
+ face = direction.x > 0.0 ? 0.0 : 3.0;
101
+ else
102
+ face = direction.y > 0.0 ? 1.0 : 4.0;
103
+ } else {
104
+ if (absDirection.z > absDirection.y)
105
+ face = direction.z > 0.0 ? 2.0 : 5.0;
106
+ else
107
+ face = direction.y > 0.0 ? 1.0 : 4.0;
108
+ }
109
+ return face;
110
+ }
111
+
112
+ vec2 mx_cubeuv_getUV(vec3 direction, float face) {
113
+ vec2 uv;
114
+ if (face == 0.0) {
115
+ uv = vec2(direction.z, direction.y) / abs(direction.x);
116
+ } else if (face == 1.0) {
117
+ uv = vec2(-direction.x, -direction.z) / abs(direction.y);
118
+ } else if (face == 2.0) {
119
+ uv = vec2(-direction.x, direction.y) / abs(direction.z);
120
+ } else if (face == 3.0) {
121
+ uv = vec2(-direction.z, direction.y) / abs(direction.x);
122
+ } else if (face == 4.0) {
123
+ uv = vec2(-direction.x, direction.z) / abs(direction.y);
124
+ } else {
125
+ uv = vec2(direction.x, direction.y) / abs(direction.z);
126
+ }
127
+ return 0.5 * (uv + 1.0);
128
+ }
129
+
130
+ vec3 mx_cubeuv_bilinear(sampler2D envMap, vec3 direction, float mipInt) {
131
+ const float cubeUV_minMipLevel = 4.0;
132
+ const float cubeUV_minTileSize = 16.0;
133
+ float face = mx_cubeuv_getFace(direction);
134
+ float filterInt = max(cubeUV_minMipLevel - mipInt, 0.0);
135
+ mipInt = max(mipInt, cubeUV_minMipLevel);
136
+ float faceSize = exp2(mipInt);
137
+ highp vec2 uv = mx_cubeuv_getUV(direction, face) * (faceSize - 2.0) + 1.0;
138
+ if (face > 2.0) {
139
+ uv.y += faceSize;
140
+ face -= 3.0;
141
+ }
142
+ uv.x += face * faceSize;
143
+ uv.x += filterInt * 3.0 * cubeUV_minTileSize;
144
+ uv.y += 4.0 * (exp2(u_envRadianceCubeUVMaxMip) - faceSize);
145
+ uv.x *= u_envRadianceCubeUVTexelWidth;
146
+ uv.y *= u_envRadianceCubeUVTexelHeight;
147
+ return texture(envMap, uv).rgb;
148
+ }
149
+
150
+ float mx_cubeuv_roughnessToMip(float roughness) {
151
+ const float cubeUV_r0 = 1.0;
152
+ const float cubeUV_m0 = -2.0;
153
+ const float cubeUV_r1 = 0.8;
154
+ const float cubeUV_m1 = -1.0;
155
+ const float cubeUV_r4 = 0.4;
156
+ const float cubeUV_m4 = 2.0;
157
+ const float cubeUV_r5 = 0.305;
158
+ const float cubeUV_m5 = 3.0;
159
+ const float cubeUV_r6 = 0.21;
160
+ const float cubeUV_m6 = 4.0;
161
+ float mip = 0.0;
162
+ if (roughness >= cubeUV_r1) {
163
+ mip = (cubeUV_r0 - roughness) * (cubeUV_m1 - cubeUV_m0) / (cubeUV_r0 - cubeUV_r1) + cubeUV_m0;
164
+ } else if (roughness >= cubeUV_r4) {
165
+ mip = (cubeUV_r1 - roughness) * (cubeUV_m4 - cubeUV_m1) / (cubeUV_r1 - cubeUV_r4) + cubeUV_m1;
166
+ } else if (roughness >= cubeUV_r5) {
167
+ mip = (cubeUV_r4 - roughness) * (cubeUV_m5 - cubeUV_m4) / (cubeUV_r4 - cubeUV_r5) + cubeUV_m4;
168
+ } else if (roughness >= cubeUV_r6) {
169
+ mip = (cubeUV_r5 - roughness) * (cubeUV_m6 - cubeUV_m5) / (cubeUV_r5 - cubeUV_r6) + cubeUV_m5;
170
+ } else {
171
+ mip = -2.0 * log2(1.16 * roughness);
172
+ }
173
+ return mip;
174
+ }
175
+
176
+ vec4 mx_cubeuv_texture(sampler2D envMap, vec3 sampleDir, float roughness) {
177
+ float mip = clamp(mx_cubeuv_roughnessToMip(roughness), -2.0, u_envRadianceCubeUVMaxMip);
178
+ float mipF = fract(mip);
179
+ float mipInt = floor(mip);
180
+ vec3 color0 = mx_cubeuv_bilinear(envMap, sampleDir, mipInt);
181
+ if (mipF == 0.0) {
182
+ return vec4(color0, 1.0);
183
+ }
184
+ vec3 color1 = mx_cubeuv_bilinear(envMap, sampleDir, mipInt + 1.0);
185
+ return vec4(mix(color0, color1, mipF), 1.0);
186
+ }
187
+ `;
188
+
189
+ const STANDARD_SURFACE_CLOSURE_GATES = [
190
+ { prefix: 'mx_dielectric_bsdf(closureData, coat,', predicate: 'coat >= M_FLOAT_EPS' },
191
+ { prefix: 'mx_conductor_bsdf(closureData, metalness,', predicate: 'metalness >= M_FLOAT_EPS' },
192
+ { prefix: 'mx_dielectric_bsdf(closureData, specular,', predicate: 'specular >= M_FLOAT_EPS' },
193
+ { prefix: 'mx_dielectric_bsdf(closureData, transmission,', predicate: 'transmission >= M_FLOAT_EPS' },
194
+ { prefix: 'mx_sheen_bsdf(closureData, sheen,', predicate: 'sheen >= M_FLOAT_EPS' },
195
+ { prefix: 'mx_translucent_bsdf(closureData, subsurface,', predicate: 'subsurface >= M_FLOAT_EPS' },
196
+ { prefix: 'mx_subsurface_bsdf(closureData, subsurface,', predicate: 'subsurface >= M_FLOAT_EPS' },
197
+ { prefix: 'mx_mix_bsdf(closureData, translucent_bsdf_out, subsurface_bsdf_out,', predicate: 'subsurface >= M_FLOAT_EPS' },
198
+ { prefix: 'mx_uniform_edf(closureData, emission_weight_out,', predicate: 'emission >= M_FLOAT_EPS' },
199
+ { prefix: 'mx_multiply_edf_color3(closureData, emission_edf_out,', predicate: 'emission >= M_FLOAT_EPS' },
200
+ { prefix: 'mx_generalized_schlick_edf(closureData, emission_color0_out,', predicate: 'emission >= M_FLOAT_EPS' },
201
+ { prefix: 'mx_mix_edf(closureData, coat_emission_edf_out, emission_edf_out,', predicate: 'emission >= M_FLOAT_EPS' },
202
+ ];
203
+
204
+ /**
205
+ * MaterialX's optimized standard_surface graph still emits every closure call,
206
+ * even when a closure weight is a uniform that is zero for the whole draw. These
207
+ * uniform branches let ANGLE/Metal skip zero-weight branches before entering the
208
+ * heavier BSDF/EDF helpers.
209
+ * @param {string} fragmentShader
210
+ * @returns {string}
211
+ */
212
+ export function optimizeMaterialXClosureBranches(fragmentShader) {
213
+ if (!fragmentShader.includes('NG_standard_surface_surfaceshader_optim')) return fragmentShader;
214
+
215
+ let optimized = fragmentShader;
216
+ for (const { prefix, predicate } of STANDARD_SURFACE_CLOSURE_GATES) {
217
+ optimized = gateClosureCall(optimized, prefix, predicate);
218
+ }
219
+ return optimized;
220
+ }
221
+
222
+ /**
223
+ * @param {string} source
224
+ * @param {string} callPrefix
225
+ * @param {string} predicate
226
+ * @returns {string}
227
+ */
228
+ function gateClosureCall(source, callPrefix, predicate) {
229
+ const pattern = new RegExp(`^(\\s*)${escapeRegExp(callPrefix)}([^\\n]*\\);)$`, 'gm');
230
+ return source.replace(pattern, (_match, indent, rest) => {
231
+ return `${indent}if (${predicate}) {\n${indent} ${callPrefix}${rest}\n${indent}}`;
232
+ });
233
+ }
234
+
235
+ /**
236
+ * @param {string} value
237
+ * @returns {string}
238
+ */
239
+ function escapeRegExp(value) {
240
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
241
+ }
242
+
243
+ const TEXTURE_FLIP_Y_FUNCTIONS = `
244
+ vec2 mx_three_flip_y(vec2 uv, bool flipY) {
245
+ return flipY ? vec2(uv.x, 1.0 - uv.y) : uv;
246
+ }
247
+ `;
248
+
249
+ /**
250
+ * ShaderMaterial bypasses Three.js' built-in texture transform chunks, so
251
+ * MaterialX file texture samples need to account for THREE.Texture.flipY here.
252
+ * @param {string} fragmentShader
253
+ * @returns {string}
254
+ */
255
+ function patchTextureFlipY(fragmentShader) {
256
+ const textureNames = [...fragmentShader.matchAll(/\buniform\s+sampler2D\s+([A-Za-z_][A-Za-z0-9_]*)\s*;/g)]
257
+ .map(match => match[1])
258
+ .filter(name => !name.startsWith('u_') && name !== 'albedoTable');
259
+
260
+ if (!textureNames.length) return fragmentShader;
261
+
262
+ for (const name of textureNames) {
263
+ const escapedName = escapeRegExp(name);
264
+ fragmentShader = fragmentShader.replace(
265
+ new RegExp(`\\btexture\\s*\\(\\s*${escapedName}\\s*,\\s*([^,\\)]+)\\)`, 'g'),
266
+ `texture(${name}, mx_three_flip_y($1, ${name}_flipY))`
267
+ );
268
+ fragmentShader = fragmentShader.replace(
269
+ new RegExp(`\\btextureLod\\s*\\(\\s*${escapedName}\\s*,\\s*([^,\\)]+)\\s*,`, 'g'),
270
+ `textureLod(${name}, mx_three_flip_y($1, ${name}_flipY),`
271
+ );
272
+ }
273
+
274
+ const uniforms = textureNames.map(name => `uniform bool ${name}_flipY;`).join('\n');
275
+ return fragmentShader.replace(
276
+ /(precision\s+\w+\s+float;)/,
277
+ `$1\n${uniforms}\n${TEXTURE_FLIP_Y_FUNCTIONS}`
278
+ );
279
+ }
280
+
281
+ /**
282
+ * The stock WebGL image node implementations rely on sampler wrapping, but the
283
+ * MaterialX "constant" address mode returns the node default outside [0, 1].
284
+ * @param {string} fragmentShader
285
+ * @returns {string}
286
+ */
287
+ function patchImageAddressModes(fragmentShader) {
288
+ return fragmentShader.replace(
289
+ /(void\s+mx_image_\w+\([^)]*\bdefaultval\b[^)]*\buaddressmode\b[^)]*\bvaddressmode\b[^)]*\)\s*\{\s*vec2\s+uv\s*=\s*mx_transform_uv\(texcoord,\s*uv_scale,\s*uv_offset\);\s*)result\s*=\s*texture\(([^,]+),\s*uv\)(\.[A-Za-z]+)?;/g,
290
+ (_match, prefix, sampler, swizzle = "") => `${prefix}if ((uaddressmode == 0 && (uv.x < 0.0 || uv.x > 1.0)) || (vaddressmode == 0 && (uv.y < 0.0 || uv.y > 1.0))) {
291
+ result = defaultval;
292
+ } else {
293
+ result = texture(${sampler}, uv)${swizzle};
294
+ }`
295
+ );
296
+ }
9
297
 
10
298
  /**
11
299
  * @typedef {Object} MaterialXMaterialInitParameters
@@ -15,6 +303,8 @@ const normalMat = new Matrix3();
15
303
  * @property {import('./materialx.helper.js').Callbacks} loaders
16
304
  * @property {import('./materialx.js').MaterialXContext} context
17
305
  * @property {import('three').MaterialParameters} [parameters] - Optional parameters
306
+ * @property {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} [environmentRadianceMode]
307
+ * @property {boolean} [specularAntialiasing] - Match Three.js glossy specular antialiasing. Defaults to true.
18
308
  * @property {boolean} [debug] - Debug flag
19
309
  */
20
310
 
@@ -43,6 +333,10 @@ export class MaterialXMaterial extends ShaderMaterial {
43
333
  this.uniformsGroups = cloneUniformsGroups(source.uniformsGroups);
44
334
  this.envMapIntensity = source.envMapIntensity;
45
335
  this.envMap = source.envMap;
336
+ this.envMapRotation.copy(source.envMapRotation);
337
+ this.environmentRadianceMode = source.environmentRadianceMode;
338
+ this.specularAntialiasing = source.specularAntialiasing;
339
+ this.ready = source.ready;
46
340
  generateMaterialPropertiesForUniforms(this, this._shader.getStage('pixel'));
47
341
  generateMaterialPropertiesForUniforms(this, this._shader.getStage('vertex'));
48
342
  this.needsUpdate = true;
@@ -55,6 +349,8 @@ export class MaterialXMaterial extends ShaderMaterial {
55
349
  _shader = null;
56
350
  /** @type {boolean} */
57
351
  _needsTangents = false;
352
+ /** @type {Promise<void>} */
353
+ ready = Promise.resolve();
58
354
 
59
355
  /**
60
356
  * @param {MaterialXMaterialInitParameters} [init]
@@ -69,6 +365,9 @@ export class MaterialXMaterial extends ShaderMaterial {
69
365
  let fragmentShader = "";
70
366
  /** @type {Record<string, string>} */
71
367
  let defines = {};
368
+ /** @type {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} */
369
+ let environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
370
+ let specularAntialiasing = true;
72
371
 
73
372
  if (init) {
74
373
 
@@ -79,6 +378,9 @@ export class MaterialXMaterial extends ShaderMaterial {
79
378
 
80
379
  vertexShader = vertexShader.replace(/^#version.*$/gm, '').trim();
81
380
  fragmentShader = fragmentShader.replace(/^#version.*$/gm, '').trim();
381
+ fragmentShader = optimizeMaterialXClosureBranches(fragmentShader);
382
+ fragmentShader = patchImageAddressModes(fragmentShader);
383
+ fragmentShader = patchTextureFlipY(fragmentShader);
82
384
 
83
385
  // MaterialX uses different attribute names than js defaults,
84
386
  // so we patch the MaterialX shaders to match the js standard names.
@@ -120,6 +422,14 @@ export class MaterialXMaterial extends ShaderMaterial {
120
422
  // This lets us combine material.envMapIntensity * scene.environmentIntensity
121
423
  // the same way MeshStandardMaterial does.
122
424
  fragmentShader = fragmentShader.replace(/\bu_envLightIntensity\b/g, 'envMapIntensity');
425
+ specularAntialiasing = init.specularAntialiasing ?? true;
426
+ if (specularAntialiasing) {
427
+ fragmentShader = patchEnvironmentSpecularAntialiasing(fragmentShader);
428
+ }
429
+ environmentRadianceMode = normalizeEnvironmentRadianceMode(init.environmentRadianceMode);
430
+ if (environmentRadianceMode === "three-pmrem") {
431
+ fragmentShader = patchPrefilteredEnvironmentLookup(fragmentShader);
432
+ }
123
433
 
124
434
  // Capture some vertex shader properties
125
435
  // Detect whether each UV was originally vec2 or vec3 before removing declarations.
@@ -177,6 +487,10 @@ export class MaterialXMaterial extends ShaderMaterial {
177
487
  const isVec3 = new RegExp(`\\bvec3\\s+${name}\\b`).test(shader);
178
488
  return isVec3 ? `${name} = vec3(${uvName}, 0.0);` : match;
179
489
  });
490
+ shader = shader.replace(new RegExp(`(\\w+) = vec2\\(${uvName}\\.x,\\s*1\\.0 - ${uvName}\\.y\\);`, 'g'), (match, name) => {
491
+ const isVec3 = new RegExp(`\\bvec3\\s+${name}\\b`).test(shader);
492
+ return isVec3 ? `${name} = vec3(${uvName}.x, 1.0 - ${uvName}.y, 0.0);` : match;
493
+ });
180
494
  return shader;
181
495
  }
182
496
  vertexShader = patchUvAssignments(vertexShader, 'uv');
@@ -354,7 +668,7 @@ $2`
354
668
  );
355
669
  } // end hasShadowUniforms
356
670
 
357
- const isTransparent = init.parameters?.transparent ?? false;
671
+ const threeParameters = { ...init.parameters };
358
672
  materialParameters = {
359
673
  name: init.name,
360
674
  uniforms: {},
@@ -362,10 +676,9 @@ $2`
362
676
  fragmentShader: fragmentShader,
363
677
  glslVersion: GLSL3,
364
678
  depthTest: true,
365
- depthWrite: !isTransparent,
366
679
  defines: defines,
367
680
  lights: true, // Enable Three.js light uniforms
368
- ...init.parameters, // Spread any additional parameters passed to the material
681
+ ...threeParameters, // Spread any additional parameters passed to the material
369
682
  };
370
683
  }
371
684
 
@@ -377,34 +690,49 @@ $2`
377
690
  }
378
691
 
379
692
  const searchPath = ""; // Could be derived from the asset path if needed
693
+ /** @type {Array<Promise<unknown>>} */
694
+ const pendingTextureLoads = [];
380
695
  this.shaderName = init.shaderName || null;
381
696
  this._context = init.context;
382
697
  this._shader = init.shader;
383
698
  this._needsTangents = vertexShader.includes('in vec4 tangent;') || vertexShader.includes('in vec3 tangent;');
384
-
385
- Object.assign(this.uniforms, {
386
- // Three.js light uniforms (required when lights: true)
387
- ...UniformsLib.lights,
388
-
389
- ...getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath),
390
- ...getUniformValues(init.shader.getStage('pixel'), init.loaders, searchPath),
391
-
392
- u_worldMatrix: { value: new Matrix4() },
393
- u_viewProjectionMatrix: { value: new Matrix4() },
394
- u_viewPosition: { value: new Vector3() },
395
- u_worldInverseTransposeMatrix: { value: new Matrix4() },
396
-
397
- u_envMatrix: { value: new Matrix4() },
398
- u_envRadiance: { value: null, type: 't' },
399
- u_envRadianceMips: { value: 8, type: 'i' },
400
- // TODO we need to figure out how we can set a PMREM here... doing many texture samples is prohibitively expensive
401
- u_envRadianceSamples: { value: 8, type: 'i' },
402
- u_envIrradiance: { value: null, type: 't' },
403
- envMapIntensity: { value: 1.0 },
404
- u_refractionEnv: { value: true },
405
- u_numActiveLightSources: { value: 0 },
406
- u_lightData: { value: [], needsUpdate: false }, // Array of light data. We need to set needsUpdate to false until we actually update it
407
- });
699
+ this.environmentRadianceMode = environmentRadianceMode;
700
+ this.specularAntialiasing = specularAntialiasing;
701
+
702
+ Object.assign(this.uniforms,
703
+ // Three.js light uniforms (required when lights: true). These use
704
+ // Three's merge/clone semantics; generated MaterialX texture
705
+ // uniforms below must keep their original object identity so async
706
+ // texture resolution updates the same uniform object.
707
+ mergeUniforms([createThreeLightUniforms()]),
708
+ getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath, pendingTextureLoads),
709
+ getUniformValues(init.shader.getStage('pixel'), init.loaders, searchPath, pendingTextureLoads),
710
+ {
711
+ u_worldMatrix: { value: new Matrix4() },
712
+ u_worldInverseMatrix: { value: new Matrix4() },
713
+ u_worldTransposeMatrix: { value: new Matrix4() },
714
+ u_worldInverseTransposeMatrix: { value: new Matrix4() },
715
+ u_worldViewMatrix: { value: new Matrix4() },
716
+ u_viewProjectionMatrix: { value: new Matrix4() },
717
+ u_worldViewProjectionMatrix: { value: new Matrix4() },
718
+ u_viewPosition: { value: new Vector3() },
719
+
720
+ u_envMatrix: { value: new Matrix4() },
721
+ u_envRadiance: { value: null, type: 't' },
722
+ u_envRadianceMips: { value: 8, type: 'i' },
723
+ u_envRadianceCubeUVTexelWidth: { value: 1 / 768 },
724
+ u_envRadianceCubeUVTexelHeight: { value: 1 / 1024 },
725
+ u_envRadianceCubeUVMaxMip: { value: 8 },
726
+ // TODO we need to figure out how we can set a PMREM here... doing many texture samples is prohibitively expensive
727
+ u_envRadianceSamples: { value: 8, type: 'i' },
728
+ u_envIrradiance: { value: null, type: 't' },
729
+ envMapIntensity: { value: 1.0 },
730
+ u_refractionEnv: { value: true },
731
+ u_refractionTwoSided: { value: false },
732
+ u_numActiveLightSources: { value: 0 },
733
+ u_lightData: { value: [], needsUpdate: false }, // Array of light data. We need to set needsUpdate to false until we actually update it
734
+ });
735
+ this.ready = Promise.all(pendingTextureLoads).then(() => undefined);
408
736
 
409
737
  generateMaterialPropertiesForUniforms(this, init.shader.getStage('pixel'));
410
738
  generateMaterialPropertiesForUniforms(this, init.shader.getStage('vertex'));
@@ -423,7 +751,7 @@ $2`
423
751
  _missingTangentsWarned = false;
424
752
 
425
753
  /**
426
- * @param {WebGLRenderer} renderer
754
+ * @param {import("three").WebGLRenderer} renderer
427
755
  * @param {Scene} _scene
428
756
  * @param {Camera} camera
429
757
  * @param {BufferGeometry} geometry
@@ -452,9 +780,15 @@ $2`
452
780
  envMapIntensity = 1.0; // Default intensity for environment map
453
781
  /** @type {Texture | null} */
454
782
  envMap = null; // Environment map texture, can be set externally
783
+ /** @type {Euler} */
784
+ envMapRotation = new Euler();
785
+ /** @type {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} */
786
+ environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
787
+ /** @type {boolean} */
788
+ specularAntialiasing = true;
455
789
 
456
790
  /**
457
- * @param {WebGLRenderer} _renderer
791
+ * @param {import("three").WebGLRenderer} _renderer
458
792
  * @param {Object3D} object
459
793
  * @param {Camera} camera
460
794
  * @param {number} [time]
@@ -473,6 +807,18 @@ $2`
473
807
  uniforms.u_worldMatrix.needsUpdate = true;
474
808
  }
475
809
 
810
+ if (uniforms.u_worldInverseMatrix) {
811
+ uniforms.u_worldInverseMatrix.value.copy(worldInverseMat.copy(object.matrixWorld).invert());
812
+ // @ts-ignore
813
+ uniforms.u_worldInverseMatrix.needsUpdate = true;
814
+ }
815
+
816
+ if (uniforms.u_worldTransposeMatrix) {
817
+ uniforms.u_worldTransposeMatrix.value.copy(worldTransposeMat.copy(object.matrixWorld).transpose());
818
+ // @ts-ignore
819
+ uniforms.u_worldTransposeMatrix.needsUpdate = true;
820
+ }
821
+
476
822
  // Update view position
477
823
  if (uniforms.u_viewPosition) {
478
824
  uniforms.u_viewPosition.value.setFromMatrixPosition(camera.matrixWorld);
@@ -486,8 +832,21 @@ $2`
486
832
  uniforms.u_viewProjectionMatrix.needsUpdate = true;
487
833
  }
488
834
 
835
+ if (uniforms.u_worldViewMatrix) {
836
+ uniforms.u_worldViewMatrix.value.copy(worldViewMat.multiplyMatrices(camera.matrixWorldInverse, object.matrixWorld));
837
+ // @ts-ignore
838
+ uniforms.u_worldViewMatrix.needsUpdate = true;
839
+ }
840
+
841
+ if (uniforms.u_worldViewProjectionMatrix) {
842
+ worldViewMat.multiplyMatrices(camera.matrixWorldInverse, object.matrixWorld);
843
+ uniforms.u_worldViewProjectionMatrix.value.copy(worldViewProjectionMat.multiplyMatrices(camera.projectionMatrix, worldViewMat));
844
+ // @ts-ignore
845
+ uniforms.u_worldViewProjectionMatrix.needsUpdate = true;
846
+ }
847
+
489
848
  if (uniforms.u_worldInverseTransposeMatrix) {
490
- uniforms.u_worldInverseTransposeMatrix.value.setFromMatrix3(normalMat.getNormalMatrix(object.matrixWorld));
849
+ uniforms.u_worldInverseTransposeMatrix.value.copy(worldInverseTransposeMat.copy(object.matrixWorld).invert().transpose());
491
850
  // @ts-ignore
492
851
  uniforms.u_worldInverseTransposeMatrix.needsUpdate = true;
493
852
  }
@@ -549,7 +908,18 @@ $2`
549
908
  if (prev != textures.radianceTexture) uniforms.u_envRadiance.needsUpdate = true;
550
909
  }
551
910
  if (uniforms.u_envRadianceMips) {
552
- uniforms.u_envRadianceMips.value = Math.trunc(Math.log2(Math.max(textures.radianceTexture?.source.data.width ?? 0, textures.radianceTexture?.source.data.height ?? 0))) + 1;
911
+ const radianceWidth = textures.radianceTexture?.source.data.width ?? textures.radianceTexture?.image?.width ?? 0;
912
+ const radianceHeight = textures.radianceTexture?.source.data.height ?? textures.radianceTexture?.image?.height ?? 0;
913
+ const materialXRadianceMips = textures.radianceTexture?.userData?.materialXRadianceMips;
914
+ uniforms.u_envRadianceMips.value = Number.isFinite(materialXRadianceMips)
915
+ ? Math.max(1, Math.trunc(materialXRadianceMips))
916
+ : Math.max(1, Math.trunc(Math.log2(Math.max(radianceWidth, radianceHeight, 1))) + 1);
917
+ }
918
+ if (uniforms.u_envRadianceCubeUVTexelWidth || uniforms.u_envRadianceCubeUVTexelHeight || uniforms.u_envRadianceCubeUVMaxMip) {
919
+ const cubeUVSize = getCubeUVSize(textures.radianceTexture);
920
+ if (uniforms.u_envRadianceCubeUVTexelWidth) uniforms.u_envRadianceCubeUVTexelWidth.value = cubeUVSize.texelWidth;
921
+ if (uniforms.u_envRadianceCubeUVTexelHeight) uniforms.u_envRadianceCubeUVTexelHeight.value = cubeUVSize.texelHeight;
922
+ if (uniforms.u_envRadianceCubeUVMaxMip) uniforms.u_envRadianceCubeUVMaxMip.value = cubeUVSize.maxMip;
553
923
  }
554
924
  if (uniforms.u_envIrradiance) {
555
925
  const prev = uniforms.u_envIrradiance.value;
@@ -557,6 +927,23 @@ $2`
557
927
  // @ts-ignore
558
928
  if (prev != textures.irradianceTexture) uniforms.u_envIrradiance.needsUpdate = true;
559
929
  }
930
+ if (uniforms.u_envMatrix) {
931
+ const rotation = scene.environment && !this.envMap ? scene.environmentRotation : this.envMapRotation;
932
+ const texture = scene.environment && !this.envMap ? scene.environment : this.envMap;
933
+ envRotation.copy(rotation);
934
+ // Match Three.js WebGLMaterials: environment rotations are applied in
935
+ // the shader after converting from Three's left-handed env frame.
936
+ envRotation.x *= -1;
937
+ envRotation.y *= -1;
938
+ envRotation.z *= -1;
939
+ if (texture?.isCubeTexture && texture.isRenderTargetTexture === false) {
940
+ envRotation.y *= -1;
941
+ envRotation.z *= -1;
942
+ }
943
+ uniforms.u_envMatrix.value.copy(envMat.makeRotationFromEuler(envRotation));
944
+ // @ts-ignore
945
+ uniforms.u_envMatrix.needsUpdate = true;
946
+ }
560
947
 
561
948
  // Sync environment intensity: combine per-material envMapIntensity with scene.environmentIntensity
562
949
  // (mirrors MeshStandardMaterial behaviour in Three.js)
@@ -569,3 +956,196 @@ $2`
569
956
  this.uniformsNeedUpdate = true;
570
957
  }
571
958
  }
959
+
960
+ /**
961
+ * MaterialX's "prefiltered" GLSL target expects a latlong texture with roughness
962
+ * in mip levels. Three.js already stores the scene environment as a prefiltered
963
+ * CubeUV PMREM, so sample that texture directly and translate MaterialX's lod
964
+ * back to the alpha/roughness value that produced it.
965
+ * @param {string} fragmentShader
966
+ * @returns {string}
967
+ */
968
+ function patchPrefilteredEnvironmentLookup(fragmentShader) {
969
+ if (!fragmentShader.includes('mx_latlong_alpha_to_lod') || !fragmentShader.includes('mx_latlong_map_lookup')) {
970
+ return fragmentShader;
971
+ }
972
+
973
+ const uniforms = `
974
+ uniform float u_envRadianceCubeUVTexelWidth;
975
+ uniform float u_envRadianceCubeUVTexelHeight;
976
+ uniform float u_envRadianceCubeUVMaxMip;
977
+ `;
978
+ fragmentShader = fragmentShader.replace(/(uniform\s+sampler2D\s+u_envRadiance;\s*)/, `$1${uniforms}`);
979
+ fragmentShader = fragmentShader.replace(
980
+ /(vec3 mx_latlong_map_lookup\(vec3 dir, mat4 transform, float lod, sampler2D tex_sampler\)\s*\{[\s\S]*?\n\})/,
981
+ `$1
982
+
983
+ ${CUBE_UV_REFLECTION_FUNCTIONS}
984
+ float mx_materialx_lod_to_alpha(float lod)
985
+ {
986
+ float lodBias = lod / max(float(u_envRadianceMips - 1), 1.0);
987
+ return (lodBias < 0.5) ? lodBias * lodBias : 2.0 * (lodBias - 0.375);
988
+ }
989
+
990
+ vec3 mx_cubeuv_map_lookup(vec3 dir, mat4 transform, float lod, sampler2D tex_sampler)
991
+ {
992
+ vec3 envDir = normalize((transform * vec4(dir, 0.0)).xyz);
993
+ float roughness = sqrt(clamp(mx_materialx_lod_to_alpha(lod), 0.0, 1.0));
994
+ return mx_cubeuv_texture(tex_sampler, envDir, roughness).rgb;
995
+ }
996
+
997
+ vec3 mx_cubeuv_irradiance_map_lookup(vec3 dir, mat4 transform, float lod, sampler2D tex_sampler)
998
+ {
999
+ vec3 envDir = normalize((transform * vec4(dir, 0.0)).xyz);
1000
+ return mx_cubeuv_texture(u_envRadiance, envDir, 1.0).rgb;
1001
+ }`
1002
+ );
1003
+ fragmentShader = replaceCubeUVLatlongLookups(fragmentShader);
1004
+ return fragmentShader;
1005
+ }
1006
+
1007
+ /**
1008
+ * Three.js avoids sharp IBL aliasing on smooth glossy silhouettes by applying a
1009
+ * minimum environment roughness and derivative-based geometry roughness before
1010
+ * sampling the prefiltered environment. MaterialX receives squared roughness
1011
+ * (alpha) at this point, so convert to roughness, apply the same adjustment,
1012
+ * and square it again for the stock MaterialX environment code.
1013
+ * @param {string} fragmentShader
1014
+ * @returns {string}
1015
+ */
1016
+ function patchEnvironmentSpecularAntialiasing(fragmentShader) {
1017
+ if (fragmentShader.includes('mx_three_antialias_specular_alpha')) {
1018
+ return fragmentShader;
1019
+ }
1020
+
1021
+ const helper = `vec2 mx_three_antialias_specular_alpha(vec3 N, vec2 alpha)
1022
+ {
1023
+ vec3 normal = normalize(N);
1024
+ vec3 dxy = max(abs(dFdx(normal)), abs(dFdy(normal)));
1025
+ float geometryRoughness = max(max(dxy.x, dxy.y), dxy.z);
1026
+ vec2 roughness = sqrt(clamp(alpha, vec2(0.0), vec2(1.0)));
1027
+ roughness = min(vec2(1.0), max(roughness, vec2(0.0525)) + vec2(geometryRoughness));
1028
+ return roughness * roughness;
1029
+ }
1030
+
1031
+ `;
1032
+
1033
+ const patched = fragmentShader.replace(
1034
+ /\bvec2\s+safeAlpha\s*=\s*clamp\(roughness,\s*M_FLOAT_EPS,\s*1\.0\);/g,
1035
+ 'vec2 safeAlpha = mx_three_antialias_specular_alpha(N, clamp(roughness, M_FLOAT_EPS, 1.0));'
1036
+ );
1037
+ if (patched === fragmentShader) {
1038
+ return fragmentShader;
1039
+ }
1040
+
1041
+ const precisionMatch = patched.match(/precision\s+\w+\s+float;\s*/);
1042
+ if (!precisionMatch || precisionMatch.index === undefined) {
1043
+ return helper + patched;
1044
+ }
1045
+
1046
+ const insertAt = precisionMatch.index + precisionMatch[0].length;
1047
+ return patched.slice(0, insertAt) + '\n' + helper + patched.slice(insertAt);
1048
+ }
1049
+
1050
+ /**
1051
+ * In direct mode both specular radiance and diffuse irradiance should sample the
1052
+ * same Three.js CubeUV PMREM. Radiance uses MaterialX's requested lod; irradiance
1053
+ * samples the fully-blurred roughness-1 level, matching Three.js IBL behavior.
1054
+ * @param {string} source
1055
+ * @returns {string}
1056
+ */
1057
+ function replaceCubeUVLatlongLookups(source) {
1058
+ const needle = 'mx_latlong_map_lookup(';
1059
+ let result = '';
1060
+ let offset = 0;
1061
+
1062
+ while (offset < source.length) {
1063
+ const start = source.indexOf(needle, offset);
1064
+ if (start === -1) {
1065
+ result += source.slice(offset);
1066
+ break;
1067
+ }
1068
+
1069
+ const argsStart = start + needle.length;
1070
+ const end = findMatchingParen(source, argsStart - 1);
1071
+ if (end === -1) {
1072
+ result += source.slice(offset);
1073
+ break;
1074
+ }
1075
+
1076
+ const args = splitTopLevelArguments(source.slice(argsStart, end));
1077
+ const samplerName = args.at(-1)?.trim();
1078
+ let functionName = 'mx_latlong_map_lookup';
1079
+ if (samplerName === 'u_envRadiance') {
1080
+ functionName = 'mx_cubeuv_map_lookup';
1081
+ } else if (samplerName === 'u_envIrradiance') {
1082
+ functionName = 'mx_cubeuv_irradiance_map_lookup';
1083
+ }
1084
+
1085
+ result += source.slice(offset, start) + functionName + source.slice(start + 'mx_latlong_map_lookup'.length, end + 1);
1086
+ offset = end + 1;
1087
+ }
1088
+
1089
+ return result;
1090
+ }
1091
+
1092
+ /**
1093
+ * @param {string} source
1094
+ * @param {number} openParenIndex
1095
+ * @returns {number}
1096
+ */
1097
+ function findMatchingParen(source, openParenIndex) {
1098
+ let depth = 0;
1099
+ for (let i = openParenIndex; i < source.length; i++) {
1100
+ const char = source[i];
1101
+ if (char === '(') depth++;
1102
+ else if (char === ')') {
1103
+ depth--;
1104
+ if (depth === 0) return i;
1105
+ }
1106
+ }
1107
+ return -1;
1108
+ }
1109
+
1110
+ /**
1111
+ * @param {string} source
1112
+ * @returns {string[]}
1113
+ */
1114
+ function splitTopLevelArguments(source) {
1115
+ const args = [];
1116
+ let depth = 0;
1117
+ let start = 0;
1118
+ for (let i = 0; i < source.length; i++) {
1119
+ const char = source[i];
1120
+ if (char === '(') depth++;
1121
+ else if (char === ')') depth--;
1122
+ else if (char === ',' && depth === 0) {
1123
+ args.push(source.slice(start, i));
1124
+ start = i + 1;
1125
+ }
1126
+ }
1127
+ args.push(source.slice(start));
1128
+ return args;
1129
+ }
1130
+
1131
+ /**
1132
+ * @param {Texture | null | undefined} texture
1133
+ */
1134
+ function getCubeUVSize(texture) {
1135
+ const imageHeight = texture?.image?.height ?? texture?.source?.data?.height ?? 1024;
1136
+ const maxMip = Math.max(0, Math.log2(imageHeight) - 2);
1137
+ return {
1138
+ maxMip,
1139
+ texelHeight: 1 / imageHeight,
1140
+ texelWidth: 1 / (3 * Math.max(Math.pow(2, maxMip), 7 * 16)),
1141
+ };
1142
+ }
1143
+
1144
+ /**
1145
+ * @param {unknown} value
1146
+ * @returns {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"}
1147
+ */
1148
+ function normalizeEnvironmentRadianceMode(value) {
1149
+ if (value === "materialx-fis") return "materialx-fis";
1150
+ return value === "materialx-prefiltered" ? "materialx-prefiltered" : DEFAULT_ENVIRONMENT_RADIANCE_MODE;
1151
+ }