@needle-tools/materialx 1.6.0-next.2af5fc1 → 1.7.0-next.0d06218
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 +15 -1
- package/README.md +33 -1
- package/bin/JsMaterialXCore.js +5 -13
- package/bin/JsMaterialXCore.wasm +0 -0
- package/bin/JsMaterialXGenShader.data.txt +2127 -2453
- package/bin/JsMaterialXGenShader.js +5 -13
- package/bin/JsMaterialXGenShader.wasm +0 -0
- package/bin/revision.json +3 -3
- package/package.json +10 -6
- package/src/index.d.ts +1 -2
- package/src/loader/loader.three.d.ts +10 -1
- package/src/loader/loader.three.js +26 -19
- package/src/materialx.d.ts +9 -5
- package/src/materialx.helper.d.ts +1 -1
- package/src/materialx.helper.js +97 -42
- package/src/materialx.js +83 -24
- package/src/materialx.material.d.ts +11 -2
- package/src/materialx.material.js +512 -10
- package/src/materialx.types.d.ts +65 -10
- package/src/utils.texture.d.ts +11 -0
- package/src/utils.texture.js +194 -37
- /package/bin/{SHA_0e7685f37737511f2816949b9486d511a5fa71bd → SHA_ab218c56f016a9a2d398e8d306f3aeb439ae9e9e} +0 -0
|
@@ -1,11 +1,223 @@
|
|
|
1
|
-
import { BufferGeometry, Camera, FrontSide, GLSL3, Group,
|
|
1
|
+
import { BufferGeometry, Camera, Euler, FrontSide, GLSL3, Group, 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
4
|
import { generateMaterialPropertiesForUniforms, getUniformValues, getLightTypeIds } from "./materialx.helper.js";
|
|
5
5
|
import { cloneUniforms, cloneUniformsGroups, mergeUniforms } from "three/src/renderers/shaders/UniformsUtils.js";
|
|
6
6
|
|
|
7
|
+
export const DEFAULT_ENVIRONMENT_RADIANCE_MODE = "three-pmrem";
|
|
8
|
+
|
|
7
9
|
// Add helper matrices for uniform updates (similar to MaterialX example)
|
|
8
|
-
const
|
|
10
|
+
const worldInverseMat = new Matrix4();
|
|
11
|
+
const worldTransposeMat = new Matrix4();
|
|
12
|
+
const worldInverseTransposeMat = new Matrix4();
|
|
13
|
+
const worldViewMat = new Matrix4();
|
|
14
|
+
const worldViewProjectionMat = new Matrix4();
|
|
15
|
+
const envMat = new Matrix4();
|
|
16
|
+
const envRotation = new Euler();
|
|
17
|
+
|
|
18
|
+
const CUBE_UV_REFLECTION_FUNCTIONS = `
|
|
19
|
+
float mx_cubeuv_getFace(vec3 direction) {
|
|
20
|
+
vec3 absDirection = abs(direction);
|
|
21
|
+
float face = -1.0;
|
|
22
|
+
if (absDirection.x > absDirection.z) {
|
|
23
|
+
if (absDirection.x > absDirection.y)
|
|
24
|
+
face = direction.x > 0.0 ? 0.0 : 3.0;
|
|
25
|
+
else
|
|
26
|
+
face = direction.y > 0.0 ? 1.0 : 4.0;
|
|
27
|
+
} else {
|
|
28
|
+
if (absDirection.z > absDirection.y)
|
|
29
|
+
face = direction.z > 0.0 ? 2.0 : 5.0;
|
|
30
|
+
else
|
|
31
|
+
face = direction.y > 0.0 ? 1.0 : 4.0;
|
|
32
|
+
}
|
|
33
|
+
return face;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
vec2 mx_cubeuv_getUV(vec3 direction, float face) {
|
|
37
|
+
vec2 uv;
|
|
38
|
+
if (face == 0.0) {
|
|
39
|
+
uv = vec2(direction.z, direction.y) / abs(direction.x);
|
|
40
|
+
} else if (face == 1.0) {
|
|
41
|
+
uv = vec2(-direction.x, -direction.z) / abs(direction.y);
|
|
42
|
+
} else if (face == 2.0) {
|
|
43
|
+
uv = vec2(-direction.x, direction.y) / abs(direction.z);
|
|
44
|
+
} else if (face == 3.0) {
|
|
45
|
+
uv = vec2(-direction.z, direction.y) / abs(direction.x);
|
|
46
|
+
} else if (face == 4.0) {
|
|
47
|
+
uv = vec2(-direction.x, direction.z) / abs(direction.y);
|
|
48
|
+
} else {
|
|
49
|
+
uv = vec2(direction.x, direction.y) / abs(direction.z);
|
|
50
|
+
}
|
|
51
|
+
return 0.5 * (uv + 1.0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
vec3 mx_cubeuv_bilinear(sampler2D envMap, vec3 direction, float mipInt) {
|
|
55
|
+
const float cubeUV_minMipLevel = 4.0;
|
|
56
|
+
const float cubeUV_minTileSize = 16.0;
|
|
57
|
+
float face = mx_cubeuv_getFace(direction);
|
|
58
|
+
float filterInt = max(cubeUV_minMipLevel - mipInt, 0.0);
|
|
59
|
+
mipInt = max(mipInt, cubeUV_minMipLevel);
|
|
60
|
+
float faceSize = exp2(mipInt);
|
|
61
|
+
highp vec2 uv = mx_cubeuv_getUV(direction, face) * (faceSize - 2.0) + 1.0;
|
|
62
|
+
if (face > 2.0) {
|
|
63
|
+
uv.y += faceSize;
|
|
64
|
+
face -= 3.0;
|
|
65
|
+
}
|
|
66
|
+
uv.x += face * faceSize;
|
|
67
|
+
uv.x += filterInt * 3.0 * cubeUV_minTileSize;
|
|
68
|
+
uv.y += 4.0 * (exp2(u_envRadianceCubeUVMaxMip) - faceSize);
|
|
69
|
+
uv.x *= u_envRadianceCubeUVTexelWidth;
|
|
70
|
+
uv.y *= u_envRadianceCubeUVTexelHeight;
|
|
71
|
+
return texture(envMap, uv).rgb;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
float mx_cubeuv_roughnessToMip(float roughness) {
|
|
75
|
+
const float cubeUV_r0 = 1.0;
|
|
76
|
+
const float cubeUV_m0 = -2.0;
|
|
77
|
+
const float cubeUV_r1 = 0.8;
|
|
78
|
+
const float cubeUV_m1 = -1.0;
|
|
79
|
+
const float cubeUV_r4 = 0.4;
|
|
80
|
+
const float cubeUV_m4 = 2.0;
|
|
81
|
+
const float cubeUV_r5 = 0.305;
|
|
82
|
+
const float cubeUV_m5 = 3.0;
|
|
83
|
+
const float cubeUV_r6 = 0.21;
|
|
84
|
+
const float cubeUV_m6 = 4.0;
|
|
85
|
+
float mip = 0.0;
|
|
86
|
+
if (roughness >= cubeUV_r1) {
|
|
87
|
+
mip = (cubeUV_r0 - roughness) * (cubeUV_m1 - cubeUV_m0) / (cubeUV_r0 - cubeUV_r1) + cubeUV_m0;
|
|
88
|
+
} else if (roughness >= cubeUV_r4) {
|
|
89
|
+
mip = (cubeUV_r1 - roughness) * (cubeUV_m4 - cubeUV_m1) / (cubeUV_r1 - cubeUV_r4) + cubeUV_m1;
|
|
90
|
+
} else if (roughness >= cubeUV_r5) {
|
|
91
|
+
mip = (cubeUV_r4 - roughness) * (cubeUV_m5 - cubeUV_m4) / (cubeUV_r4 - cubeUV_r5) + cubeUV_m4;
|
|
92
|
+
} else if (roughness >= cubeUV_r6) {
|
|
93
|
+
mip = (cubeUV_r5 - roughness) * (cubeUV_m6 - cubeUV_m5) / (cubeUV_r5 - cubeUV_r6) + cubeUV_m5;
|
|
94
|
+
} else {
|
|
95
|
+
mip = -2.0 * log2(1.16 * roughness);
|
|
96
|
+
}
|
|
97
|
+
return mip;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
vec4 mx_cubeuv_texture(sampler2D envMap, vec3 sampleDir, float roughness) {
|
|
101
|
+
float mip = clamp(mx_cubeuv_roughnessToMip(roughness), -2.0, u_envRadianceCubeUVMaxMip);
|
|
102
|
+
float mipF = fract(mip);
|
|
103
|
+
float mipInt = floor(mip);
|
|
104
|
+
vec3 color0 = mx_cubeuv_bilinear(envMap, sampleDir, mipInt);
|
|
105
|
+
if (mipF == 0.0) {
|
|
106
|
+
return vec4(color0, 1.0);
|
|
107
|
+
}
|
|
108
|
+
vec3 color1 = mx_cubeuv_bilinear(envMap, sampleDir, mipInt + 1.0);
|
|
109
|
+
return vec4(mix(color0, color1, mipF), 1.0);
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
const STANDARD_SURFACE_CLOSURE_GATES = [
|
|
114
|
+
{ prefix: 'mx_dielectric_bsdf(closureData, coat,', predicate: 'coat >= M_FLOAT_EPS' },
|
|
115
|
+
{ prefix: 'mx_conductor_bsdf(closureData, metalness,', predicate: 'metalness >= M_FLOAT_EPS' },
|
|
116
|
+
{ prefix: 'mx_dielectric_bsdf(closureData, specular,', predicate: 'specular >= M_FLOAT_EPS' },
|
|
117
|
+
{ prefix: 'mx_dielectric_bsdf(closureData, transmission,', predicate: 'transmission >= M_FLOAT_EPS' },
|
|
118
|
+
{ prefix: 'mx_sheen_bsdf(closureData, sheen,', predicate: 'sheen >= M_FLOAT_EPS' },
|
|
119
|
+
{ prefix: 'mx_translucent_bsdf(closureData, subsurface,', predicate: 'subsurface >= M_FLOAT_EPS' },
|
|
120
|
+
{ prefix: 'mx_subsurface_bsdf(closureData, subsurface,', predicate: 'subsurface >= M_FLOAT_EPS' },
|
|
121
|
+
{ prefix: 'mx_mix_bsdf(closureData, translucent_bsdf_out, subsurface_bsdf_out,', predicate: 'subsurface >= M_FLOAT_EPS' },
|
|
122
|
+
{ prefix: 'mx_uniform_edf(closureData, emission_weight_out,', predicate: 'emission >= M_FLOAT_EPS' },
|
|
123
|
+
{ prefix: 'mx_multiply_edf_color3(closureData, emission_edf_out,', predicate: 'emission >= M_FLOAT_EPS' },
|
|
124
|
+
{ prefix: 'mx_generalized_schlick_edf(closureData, emission_color0_out,', predicate: 'emission >= M_FLOAT_EPS' },
|
|
125
|
+
{ prefix: 'mx_mix_edf(closureData, coat_emission_edf_out, emission_edf_out,', predicate: 'emission >= M_FLOAT_EPS' },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* MaterialX's optimized standard_surface graph still emits every closure call,
|
|
130
|
+
* even when a closure weight is a uniform that is zero for the whole draw. These
|
|
131
|
+
* uniform branches let ANGLE/Metal skip zero-weight branches before entering the
|
|
132
|
+
* heavier BSDF/EDF helpers.
|
|
133
|
+
* @param {string} fragmentShader
|
|
134
|
+
* @returns {string}
|
|
135
|
+
*/
|
|
136
|
+
export function optimizeMaterialXClosureBranches(fragmentShader) {
|
|
137
|
+
if (!fragmentShader.includes('NG_standard_surface_surfaceshader_optim')) return fragmentShader;
|
|
138
|
+
|
|
139
|
+
let optimized = fragmentShader;
|
|
140
|
+
for (const { prefix, predicate } of STANDARD_SURFACE_CLOSURE_GATES) {
|
|
141
|
+
optimized = gateClosureCall(optimized, prefix, predicate);
|
|
142
|
+
}
|
|
143
|
+
return optimized;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {string} source
|
|
148
|
+
* @param {string} callPrefix
|
|
149
|
+
* @param {string} predicate
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
function gateClosureCall(source, callPrefix, predicate) {
|
|
153
|
+
const pattern = new RegExp(`^(\\s*)${escapeRegExp(callPrefix)}([^\\n]*\\);)$`, 'gm');
|
|
154
|
+
return source.replace(pattern, (_match, indent, rest) => {
|
|
155
|
+
return `${indent}if (${predicate}) {\n${indent} ${callPrefix}${rest}\n${indent}}`;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} value
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
function escapeRegExp(value) {
|
|
164
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const TEXTURE_FLIP_Y_FUNCTIONS = `
|
|
168
|
+
vec2 mx_three_flip_y(vec2 uv, bool flipY) {
|
|
169
|
+
return flipY ? vec2(uv.x, 1.0 - uv.y) : uv;
|
|
170
|
+
}
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* ShaderMaterial bypasses Three.js' built-in texture transform chunks, so
|
|
175
|
+
* MaterialX file texture samples need to account for THREE.Texture.flipY here.
|
|
176
|
+
* @param {string} fragmentShader
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
function patchTextureFlipY(fragmentShader) {
|
|
180
|
+
const textureNames = [...fragmentShader.matchAll(/\buniform\s+sampler2D\s+([A-Za-z_][A-Za-z0-9_]*)\s*;/g)]
|
|
181
|
+
.map(match => match[1])
|
|
182
|
+
.filter(name => !name.startsWith('u_') && name !== 'albedoTable');
|
|
183
|
+
|
|
184
|
+
if (!textureNames.length) return fragmentShader;
|
|
185
|
+
|
|
186
|
+
for (const name of textureNames) {
|
|
187
|
+
const escapedName = escapeRegExp(name);
|
|
188
|
+
fragmentShader = fragmentShader.replace(
|
|
189
|
+
new RegExp(`\\btexture\\s*\\(\\s*${escapedName}\\s*,\\s*([^,\\)]+)\\)`, 'g'),
|
|
190
|
+
`texture(${name}, mx_three_flip_y($1, ${name}_flipY))`
|
|
191
|
+
);
|
|
192
|
+
fragmentShader = fragmentShader.replace(
|
|
193
|
+
new RegExp(`\\btextureLod\\s*\\(\\s*${escapedName}\\s*,\\s*([^,\\)]+)\\s*,`, 'g'),
|
|
194
|
+
`textureLod(${name}, mx_three_flip_y($1, ${name}_flipY),`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const uniforms = textureNames.map(name => `uniform bool ${name}_flipY;`).join('\n');
|
|
199
|
+
return fragmentShader.replace(
|
|
200
|
+
/(precision\s+\w+\s+float;)/,
|
|
201
|
+
`$1\n${uniforms}\n${TEXTURE_FLIP_Y_FUNCTIONS}`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The stock WebGL image node implementations rely on sampler wrapping, but the
|
|
207
|
+
* MaterialX "constant" address mode returns the node default outside [0, 1].
|
|
208
|
+
* @param {string} fragmentShader
|
|
209
|
+
* @returns {string}
|
|
210
|
+
*/
|
|
211
|
+
function patchImageAddressModes(fragmentShader) {
|
|
212
|
+
return fragmentShader.replace(
|
|
213
|
+
/(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,
|
|
214
|
+
(_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))) {
|
|
215
|
+
result = defaultval;
|
|
216
|
+
} else {
|
|
217
|
+
result = texture(${sampler}, uv)${swizzle};
|
|
218
|
+
}`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
9
221
|
|
|
10
222
|
/**
|
|
11
223
|
* @typedef {Object} MaterialXMaterialInitParameters
|
|
@@ -15,6 +227,8 @@ const normalMat = new Matrix3();
|
|
|
15
227
|
* @property {import('./materialx.helper.js').Callbacks} loaders
|
|
16
228
|
* @property {import('./materialx.js').MaterialXContext} context
|
|
17
229
|
* @property {import('three').MaterialParameters} [parameters] - Optional parameters
|
|
230
|
+
* @property {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} [environmentRadianceMode]
|
|
231
|
+
* @property {boolean} [specularAntialiasing] - Match Three.js glossy specular antialiasing. Defaults to true.
|
|
18
232
|
* @property {boolean} [debug] - Debug flag
|
|
19
233
|
*/
|
|
20
234
|
|
|
@@ -43,6 +257,10 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
43
257
|
this.uniformsGroups = cloneUniformsGroups(source.uniformsGroups);
|
|
44
258
|
this.envMapIntensity = source.envMapIntensity;
|
|
45
259
|
this.envMap = source.envMap;
|
|
260
|
+
this.envMapRotation.copy(source.envMapRotation);
|
|
261
|
+
this.environmentRadianceMode = source.environmentRadianceMode;
|
|
262
|
+
this.specularAntialiasing = source.specularAntialiasing;
|
|
263
|
+
this.ready = source.ready;
|
|
46
264
|
generateMaterialPropertiesForUniforms(this, this._shader.getStage('pixel'));
|
|
47
265
|
generateMaterialPropertiesForUniforms(this, this._shader.getStage('vertex'));
|
|
48
266
|
this.needsUpdate = true;
|
|
@@ -55,6 +273,8 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
55
273
|
_shader = null;
|
|
56
274
|
/** @type {boolean} */
|
|
57
275
|
_needsTangents = false;
|
|
276
|
+
/** @type {Promise<void>} */
|
|
277
|
+
ready = Promise.resolve();
|
|
58
278
|
|
|
59
279
|
/**
|
|
60
280
|
* @param {MaterialXMaterialInitParameters} [init]
|
|
@@ -69,6 +289,9 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
69
289
|
let fragmentShader = "";
|
|
70
290
|
/** @type {Record<string, string>} */
|
|
71
291
|
let defines = {};
|
|
292
|
+
/** @type {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} */
|
|
293
|
+
let environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
|
|
294
|
+
let specularAntialiasing = true;
|
|
72
295
|
|
|
73
296
|
if (init) {
|
|
74
297
|
|
|
@@ -79,6 +302,9 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
79
302
|
|
|
80
303
|
vertexShader = vertexShader.replace(/^#version.*$/gm, '').trim();
|
|
81
304
|
fragmentShader = fragmentShader.replace(/^#version.*$/gm, '').trim();
|
|
305
|
+
fragmentShader = optimizeMaterialXClosureBranches(fragmentShader);
|
|
306
|
+
fragmentShader = patchImageAddressModes(fragmentShader);
|
|
307
|
+
fragmentShader = patchTextureFlipY(fragmentShader);
|
|
82
308
|
|
|
83
309
|
// MaterialX uses different attribute names than js defaults,
|
|
84
310
|
// so we patch the MaterialX shaders to match the js standard names.
|
|
@@ -120,6 +346,14 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
120
346
|
// This lets us combine material.envMapIntensity * scene.environmentIntensity
|
|
121
347
|
// the same way MeshStandardMaterial does.
|
|
122
348
|
fragmentShader = fragmentShader.replace(/\bu_envLightIntensity\b/g, 'envMapIntensity');
|
|
349
|
+
specularAntialiasing = init.specularAntialiasing ?? true;
|
|
350
|
+
if (specularAntialiasing) {
|
|
351
|
+
fragmentShader = patchEnvironmentSpecularAntialiasing(fragmentShader);
|
|
352
|
+
}
|
|
353
|
+
environmentRadianceMode = normalizeEnvironmentRadianceMode(init.environmentRadianceMode);
|
|
354
|
+
if (environmentRadianceMode === "three-pmrem") {
|
|
355
|
+
fragmentShader = patchPrefilteredEnvironmentLookup(fragmentShader);
|
|
356
|
+
}
|
|
123
357
|
|
|
124
358
|
// Capture some vertex shader properties
|
|
125
359
|
// Detect whether each UV was originally vec2 or vec3 before removing declarations.
|
|
@@ -177,6 +411,10 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
177
411
|
const isVec3 = new RegExp(`\\bvec3\\s+${name}\\b`).test(shader);
|
|
178
412
|
return isVec3 ? `${name} = vec3(${uvName}, 0.0);` : match;
|
|
179
413
|
});
|
|
414
|
+
shader = shader.replace(new RegExp(`(\\w+) = vec2\\(${uvName}\\.x,\\s*1\\.0 - ${uvName}\\.y\\);`, 'g'), (match, name) => {
|
|
415
|
+
const isVec3 = new RegExp(`\\bvec3\\s+${name}\\b`).test(shader);
|
|
416
|
+
return isVec3 ? `${name} = vec3(${uvName}.x, 1.0 - ${uvName}.y, 0.0);` : match;
|
|
417
|
+
});
|
|
180
418
|
return shader;
|
|
181
419
|
}
|
|
182
420
|
vertexShader = patchUvAssignments(vertexShader, 'uv');
|
|
@@ -354,7 +592,7 @@ $2`
|
|
|
354
592
|
);
|
|
355
593
|
} // end hasShadowUniforms
|
|
356
594
|
|
|
357
|
-
const
|
|
595
|
+
const threeParameters = { ...init.parameters };
|
|
358
596
|
materialParameters = {
|
|
359
597
|
name: init.name,
|
|
360
598
|
uniforms: {},
|
|
@@ -362,10 +600,9 @@ $2`
|
|
|
362
600
|
fragmentShader: fragmentShader,
|
|
363
601
|
glslVersion: GLSL3,
|
|
364
602
|
depthTest: true,
|
|
365
|
-
depthWrite: !isTransparent,
|
|
366
603
|
defines: defines,
|
|
367
604
|
lights: true, // Enable Three.js light uniforms
|
|
368
|
-
...
|
|
605
|
+
...threeParameters, // Spread any additional parameters passed to the material
|
|
369
606
|
};
|
|
370
607
|
}
|
|
371
608
|
|
|
@@ -377,34 +614,47 @@ $2`
|
|
|
377
614
|
}
|
|
378
615
|
|
|
379
616
|
const searchPath = ""; // Could be derived from the asset path if needed
|
|
617
|
+
/** @type {Array<Promise<unknown>>} */
|
|
618
|
+
const pendingTextureLoads = [];
|
|
380
619
|
this.shaderName = init.shaderName || null;
|
|
381
620
|
this._context = init.context;
|
|
382
621
|
this._shader = init.shader;
|
|
383
622
|
this._needsTangents = vertexShader.includes('in vec4 tangent;') || vertexShader.includes('in vec3 tangent;');
|
|
623
|
+
this.environmentRadianceMode = environmentRadianceMode;
|
|
624
|
+
this.specularAntialiasing = specularAntialiasing;
|
|
384
625
|
|
|
385
626
|
Object.assign(this.uniforms, {
|
|
386
627
|
// Three.js light uniforms (required when lights: true)
|
|
387
628
|
...UniformsLib.lights,
|
|
388
629
|
|
|
389
|
-
...getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath),
|
|
390
|
-
...getUniformValues(init.shader.getStage('pixel'), init.loaders, searchPath),
|
|
630
|
+
...getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath, pendingTextureLoads),
|
|
631
|
+
...getUniformValues(init.shader.getStage('pixel'), init.loaders, searchPath, pendingTextureLoads),
|
|
391
632
|
|
|
392
633
|
u_worldMatrix: { value: new Matrix4() },
|
|
634
|
+
u_worldInverseMatrix: { value: new Matrix4() },
|
|
635
|
+
u_worldTransposeMatrix: { value: new Matrix4() },
|
|
636
|
+
u_worldInverseTransposeMatrix: { value: new Matrix4() },
|
|
637
|
+
u_worldViewMatrix: { value: new Matrix4() },
|
|
393
638
|
u_viewProjectionMatrix: { value: new Matrix4() },
|
|
639
|
+
u_worldViewProjectionMatrix: { value: new Matrix4() },
|
|
394
640
|
u_viewPosition: { value: new Vector3() },
|
|
395
|
-
u_worldInverseTransposeMatrix: { value: new Matrix4() },
|
|
396
641
|
|
|
397
642
|
u_envMatrix: { value: new Matrix4() },
|
|
398
643
|
u_envRadiance: { value: null, type: 't' },
|
|
399
644
|
u_envRadianceMips: { value: 8, type: 'i' },
|
|
645
|
+
u_envRadianceCubeUVTexelWidth: { value: 1 / 768 },
|
|
646
|
+
u_envRadianceCubeUVTexelHeight: { value: 1 / 1024 },
|
|
647
|
+
u_envRadianceCubeUVMaxMip: { value: 8 },
|
|
400
648
|
// TODO we need to figure out how we can set a PMREM here... doing many texture samples is prohibitively expensive
|
|
401
649
|
u_envRadianceSamples: { value: 8, type: 'i' },
|
|
402
650
|
u_envIrradiance: { value: null, type: 't' },
|
|
403
651
|
envMapIntensity: { value: 1.0 },
|
|
404
652
|
u_refractionEnv: { value: true },
|
|
653
|
+
u_refractionTwoSided: { value: false },
|
|
405
654
|
u_numActiveLightSources: { value: 0 },
|
|
406
655
|
u_lightData: { value: [], needsUpdate: false }, // Array of light data. We need to set needsUpdate to false until we actually update it
|
|
407
656
|
});
|
|
657
|
+
this.ready = Promise.all(pendingTextureLoads).then(() => undefined);
|
|
408
658
|
|
|
409
659
|
generateMaterialPropertiesForUniforms(this, init.shader.getStage('pixel'));
|
|
410
660
|
generateMaterialPropertiesForUniforms(this, init.shader.getStage('vertex'));
|
|
@@ -452,6 +702,12 @@ $2`
|
|
|
452
702
|
envMapIntensity = 1.0; // Default intensity for environment map
|
|
453
703
|
/** @type {Texture | null} */
|
|
454
704
|
envMap = null; // Environment map texture, can be set externally
|
|
705
|
+
/** @type {Euler} */
|
|
706
|
+
envMapRotation = new Euler();
|
|
707
|
+
/** @type {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"} */
|
|
708
|
+
environmentRadianceMode = DEFAULT_ENVIRONMENT_RADIANCE_MODE;
|
|
709
|
+
/** @type {boolean} */
|
|
710
|
+
specularAntialiasing = true;
|
|
455
711
|
|
|
456
712
|
/**
|
|
457
713
|
* @param {WebGLRenderer} _renderer
|
|
@@ -473,6 +729,18 @@ $2`
|
|
|
473
729
|
uniforms.u_worldMatrix.needsUpdate = true;
|
|
474
730
|
}
|
|
475
731
|
|
|
732
|
+
if (uniforms.u_worldInverseMatrix) {
|
|
733
|
+
uniforms.u_worldInverseMatrix.value.copy(worldInverseMat.copy(object.matrixWorld).invert());
|
|
734
|
+
// @ts-ignore
|
|
735
|
+
uniforms.u_worldInverseMatrix.needsUpdate = true;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (uniforms.u_worldTransposeMatrix) {
|
|
739
|
+
uniforms.u_worldTransposeMatrix.value.copy(worldTransposeMat.copy(object.matrixWorld).transpose());
|
|
740
|
+
// @ts-ignore
|
|
741
|
+
uniforms.u_worldTransposeMatrix.needsUpdate = true;
|
|
742
|
+
}
|
|
743
|
+
|
|
476
744
|
// Update view position
|
|
477
745
|
if (uniforms.u_viewPosition) {
|
|
478
746
|
uniforms.u_viewPosition.value.setFromMatrixPosition(camera.matrixWorld);
|
|
@@ -486,8 +754,21 @@ $2`
|
|
|
486
754
|
uniforms.u_viewProjectionMatrix.needsUpdate = true;
|
|
487
755
|
}
|
|
488
756
|
|
|
757
|
+
if (uniforms.u_worldViewMatrix) {
|
|
758
|
+
uniforms.u_worldViewMatrix.value.copy(worldViewMat.multiplyMatrices(camera.matrixWorldInverse, object.matrixWorld));
|
|
759
|
+
// @ts-ignore
|
|
760
|
+
uniforms.u_worldViewMatrix.needsUpdate = true;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (uniforms.u_worldViewProjectionMatrix) {
|
|
764
|
+
worldViewMat.multiplyMatrices(camera.matrixWorldInverse, object.matrixWorld);
|
|
765
|
+
uniforms.u_worldViewProjectionMatrix.value.copy(worldViewProjectionMat.multiplyMatrices(camera.projectionMatrix, worldViewMat));
|
|
766
|
+
// @ts-ignore
|
|
767
|
+
uniforms.u_worldViewProjectionMatrix.needsUpdate = true;
|
|
768
|
+
}
|
|
769
|
+
|
|
489
770
|
if (uniforms.u_worldInverseTransposeMatrix) {
|
|
490
|
-
uniforms.u_worldInverseTransposeMatrix.value.
|
|
771
|
+
uniforms.u_worldInverseTransposeMatrix.value.copy(worldInverseTransposeMat.copy(object.matrixWorld).invert().transpose());
|
|
491
772
|
// @ts-ignore
|
|
492
773
|
uniforms.u_worldInverseTransposeMatrix.needsUpdate = true;
|
|
493
774
|
}
|
|
@@ -549,7 +830,18 @@ $2`
|
|
|
549
830
|
if (prev != textures.radianceTexture) uniforms.u_envRadiance.needsUpdate = true;
|
|
550
831
|
}
|
|
551
832
|
if (uniforms.u_envRadianceMips) {
|
|
552
|
-
|
|
833
|
+
const radianceWidth = textures.radianceTexture?.source.data.width ?? textures.radianceTexture?.image?.width ?? 0;
|
|
834
|
+
const radianceHeight = textures.radianceTexture?.source.data.height ?? textures.radianceTexture?.image?.height ?? 0;
|
|
835
|
+
const materialXRadianceMips = textures.radianceTexture?.userData?.materialXRadianceMips;
|
|
836
|
+
uniforms.u_envRadianceMips.value = Number.isFinite(materialXRadianceMips)
|
|
837
|
+
? Math.max(1, Math.trunc(materialXRadianceMips))
|
|
838
|
+
: Math.max(1, Math.trunc(Math.log2(Math.max(radianceWidth, radianceHeight, 1))) + 1);
|
|
839
|
+
}
|
|
840
|
+
if (uniforms.u_envRadianceCubeUVTexelWidth || uniforms.u_envRadianceCubeUVTexelHeight || uniforms.u_envRadianceCubeUVMaxMip) {
|
|
841
|
+
const cubeUVSize = getCubeUVSize(textures.radianceTexture);
|
|
842
|
+
if (uniforms.u_envRadianceCubeUVTexelWidth) uniforms.u_envRadianceCubeUVTexelWidth.value = cubeUVSize.texelWidth;
|
|
843
|
+
if (uniforms.u_envRadianceCubeUVTexelHeight) uniforms.u_envRadianceCubeUVTexelHeight.value = cubeUVSize.texelHeight;
|
|
844
|
+
if (uniforms.u_envRadianceCubeUVMaxMip) uniforms.u_envRadianceCubeUVMaxMip.value = cubeUVSize.maxMip;
|
|
553
845
|
}
|
|
554
846
|
if (uniforms.u_envIrradiance) {
|
|
555
847
|
const prev = uniforms.u_envIrradiance.value;
|
|
@@ -557,6 +849,23 @@ $2`
|
|
|
557
849
|
// @ts-ignore
|
|
558
850
|
if (prev != textures.irradianceTexture) uniforms.u_envIrradiance.needsUpdate = true;
|
|
559
851
|
}
|
|
852
|
+
if (uniforms.u_envMatrix) {
|
|
853
|
+
const rotation = scene.environment && !this.envMap ? scene.environmentRotation : this.envMapRotation;
|
|
854
|
+
const texture = scene.environment && !this.envMap ? scene.environment : this.envMap;
|
|
855
|
+
envRotation.copy(rotation);
|
|
856
|
+
// Match Three.js WebGLMaterials: environment rotations are applied in
|
|
857
|
+
// the shader after converting from Three's left-handed env frame.
|
|
858
|
+
envRotation.x *= -1;
|
|
859
|
+
envRotation.y *= -1;
|
|
860
|
+
envRotation.z *= -1;
|
|
861
|
+
if (texture?.isCubeTexture && texture.isRenderTargetTexture === false) {
|
|
862
|
+
envRotation.y *= -1;
|
|
863
|
+
envRotation.z *= -1;
|
|
864
|
+
}
|
|
865
|
+
uniforms.u_envMatrix.value.copy(envMat.makeRotationFromEuler(envRotation));
|
|
866
|
+
// @ts-ignore
|
|
867
|
+
uniforms.u_envMatrix.needsUpdate = true;
|
|
868
|
+
}
|
|
560
869
|
|
|
561
870
|
// Sync environment intensity: combine per-material envMapIntensity with scene.environmentIntensity
|
|
562
871
|
// (mirrors MeshStandardMaterial behaviour in Three.js)
|
|
@@ -569,3 +878,196 @@ $2`
|
|
|
569
878
|
this.uniformsNeedUpdate = true;
|
|
570
879
|
}
|
|
571
880
|
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* MaterialX's "prefiltered" GLSL target expects a latlong texture with roughness
|
|
884
|
+
* in mip levels. Three.js already stores the scene environment as a prefiltered
|
|
885
|
+
* CubeUV PMREM, so sample that texture directly and translate MaterialX's lod
|
|
886
|
+
* back to the alpha/roughness value that produced it.
|
|
887
|
+
* @param {string} fragmentShader
|
|
888
|
+
* @returns {string}
|
|
889
|
+
*/
|
|
890
|
+
function patchPrefilteredEnvironmentLookup(fragmentShader) {
|
|
891
|
+
if (!fragmentShader.includes('mx_latlong_alpha_to_lod') || !fragmentShader.includes('mx_latlong_map_lookup')) {
|
|
892
|
+
return fragmentShader;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const uniforms = `
|
|
896
|
+
uniform float u_envRadianceCubeUVTexelWidth;
|
|
897
|
+
uniform float u_envRadianceCubeUVTexelHeight;
|
|
898
|
+
uniform float u_envRadianceCubeUVMaxMip;
|
|
899
|
+
`;
|
|
900
|
+
fragmentShader = fragmentShader.replace(/(uniform\s+sampler2D\s+u_envRadiance;\s*)/, `$1${uniforms}`);
|
|
901
|
+
fragmentShader = fragmentShader.replace(
|
|
902
|
+
/(vec3 mx_latlong_map_lookup\(vec3 dir, mat4 transform, float lod, sampler2D tex_sampler\)\s*\{[\s\S]*?\n\})/,
|
|
903
|
+
`$1
|
|
904
|
+
|
|
905
|
+
${CUBE_UV_REFLECTION_FUNCTIONS}
|
|
906
|
+
float mx_materialx_lod_to_alpha(float lod)
|
|
907
|
+
{
|
|
908
|
+
float lodBias = lod / max(float(u_envRadianceMips - 1), 1.0);
|
|
909
|
+
return (lodBias < 0.5) ? lodBias * lodBias : 2.0 * (lodBias - 0.375);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
vec3 mx_cubeuv_map_lookup(vec3 dir, mat4 transform, float lod, sampler2D tex_sampler)
|
|
913
|
+
{
|
|
914
|
+
vec3 envDir = normalize((transform * vec4(dir, 0.0)).xyz);
|
|
915
|
+
float roughness = sqrt(clamp(mx_materialx_lod_to_alpha(lod), 0.0, 1.0));
|
|
916
|
+
return mx_cubeuv_texture(tex_sampler, envDir, roughness).rgb;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
vec3 mx_cubeuv_irradiance_map_lookup(vec3 dir, mat4 transform, float lod, sampler2D tex_sampler)
|
|
920
|
+
{
|
|
921
|
+
vec3 envDir = normalize((transform * vec4(dir, 0.0)).xyz);
|
|
922
|
+
return mx_cubeuv_texture(u_envRadiance, envDir, 1.0).rgb;
|
|
923
|
+
}`
|
|
924
|
+
);
|
|
925
|
+
fragmentShader = replaceCubeUVLatlongLookups(fragmentShader);
|
|
926
|
+
return fragmentShader;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Three.js avoids sharp IBL aliasing on smooth glossy silhouettes by applying a
|
|
931
|
+
* minimum environment roughness and derivative-based geometry roughness before
|
|
932
|
+
* sampling the prefiltered environment. MaterialX receives squared roughness
|
|
933
|
+
* (alpha) at this point, so convert to roughness, apply the same adjustment,
|
|
934
|
+
* and square it again for the stock MaterialX environment code.
|
|
935
|
+
* @param {string} fragmentShader
|
|
936
|
+
* @returns {string}
|
|
937
|
+
*/
|
|
938
|
+
function patchEnvironmentSpecularAntialiasing(fragmentShader) {
|
|
939
|
+
if (fragmentShader.includes('mx_three_antialias_specular_alpha')) {
|
|
940
|
+
return fragmentShader;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
const helper = `vec2 mx_three_antialias_specular_alpha(vec3 N, vec2 alpha)
|
|
944
|
+
{
|
|
945
|
+
vec3 normal = normalize(N);
|
|
946
|
+
vec3 dxy = max(abs(dFdx(normal)), abs(dFdy(normal)));
|
|
947
|
+
float geometryRoughness = max(max(dxy.x, dxy.y), dxy.z);
|
|
948
|
+
vec2 roughness = sqrt(clamp(alpha, vec2(0.0), vec2(1.0)));
|
|
949
|
+
roughness = min(vec2(1.0), max(roughness, vec2(0.0525)) + vec2(geometryRoughness));
|
|
950
|
+
return roughness * roughness;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
`;
|
|
954
|
+
|
|
955
|
+
const patched = fragmentShader.replace(
|
|
956
|
+
/\bvec2\s+safeAlpha\s*=\s*clamp\(roughness,\s*M_FLOAT_EPS,\s*1\.0\);/g,
|
|
957
|
+
'vec2 safeAlpha = mx_three_antialias_specular_alpha(N, clamp(roughness, M_FLOAT_EPS, 1.0));'
|
|
958
|
+
);
|
|
959
|
+
if (patched === fragmentShader) {
|
|
960
|
+
return fragmentShader;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const precisionMatch = patched.match(/precision\s+\w+\s+float;\s*/);
|
|
964
|
+
if (!precisionMatch || precisionMatch.index === undefined) {
|
|
965
|
+
return helper + patched;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const insertAt = precisionMatch.index + precisionMatch[0].length;
|
|
969
|
+
return patched.slice(0, insertAt) + '\n' + helper + patched.slice(insertAt);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* In direct mode both specular radiance and diffuse irradiance should sample the
|
|
974
|
+
* same Three.js CubeUV PMREM. Radiance uses MaterialX's requested lod; irradiance
|
|
975
|
+
* samples the fully-blurred roughness-1 level, matching Three.js IBL behavior.
|
|
976
|
+
* @param {string} source
|
|
977
|
+
* @returns {string}
|
|
978
|
+
*/
|
|
979
|
+
function replaceCubeUVLatlongLookups(source) {
|
|
980
|
+
const needle = 'mx_latlong_map_lookup(';
|
|
981
|
+
let result = '';
|
|
982
|
+
let offset = 0;
|
|
983
|
+
|
|
984
|
+
while (offset < source.length) {
|
|
985
|
+
const start = source.indexOf(needle, offset);
|
|
986
|
+
if (start === -1) {
|
|
987
|
+
result += source.slice(offset);
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const argsStart = start + needle.length;
|
|
992
|
+
const end = findMatchingParen(source, argsStart - 1);
|
|
993
|
+
if (end === -1) {
|
|
994
|
+
result += source.slice(offset);
|
|
995
|
+
break;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const args = splitTopLevelArguments(source.slice(argsStart, end));
|
|
999
|
+
const samplerName = args.at(-1)?.trim();
|
|
1000
|
+
let functionName = 'mx_latlong_map_lookup';
|
|
1001
|
+
if (samplerName === 'u_envRadiance') {
|
|
1002
|
+
functionName = 'mx_cubeuv_map_lookup';
|
|
1003
|
+
} else if (samplerName === 'u_envIrradiance') {
|
|
1004
|
+
functionName = 'mx_cubeuv_irradiance_map_lookup';
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
result += source.slice(offset, start) + functionName + source.slice(start + 'mx_latlong_map_lookup'.length, end + 1);
|
|
1008
|
+
offset = end + 1;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return result;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* @param {string} source
|
|
1016
|
+
* @param {number} openParenIndex
|
|
1017
|
+
* @returns {number}
|
|
1018
|
+
*/
|
|
1019
|
+
function findMatchingParen(source, openParenIndex) {
|
|
1020
|
+
let depth = 0;
|
|
1021
|
+
for (let i = openParenIndex; i < source.length; i++) {
|
|
1022
|
+
const char = source[i];
|
|
1023
|
+
if (char === '(') depth++;
|
|
1024
|
+
else if (char === ')') {
|
|
1025
|
+
depth--;
|
|
1026
|
+
if (depth === 0) return i;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return -1;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* @param {string} source
|
|
1034
|
+
* @returns {string[]}
|
|
1035
|
+
*/
|
|
1036
|
+
function splitTopLevelArguments(source) {
|
|
1037
|
+
const args = [];
|
|
1038
|
+
let depth = 0;
|
|
1039
|
+
let start = 0;
|
|
1040
|
+
for (let i = 0; i < source.length; i++) {
|
|
1041
|
+
const char = source[i];
|
|
1042
|
+
if (char === '(') depth++;
|
|
1043
|
+
else if (char === ')') depth--;
|
|
1044
|
+
else if (char === ',' && depth === 0) {
|
|
1045
|
+
args.push(source.slice(start, i));
|
|
1046
|
+
start = i + 1;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
args.push(source.slice(start));
|
|
1050
|
+
return args;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* @param {Texture | null | undefined} texture
|
|
1055
|
+
*/
|
|
1056
|
+
function getCubeUVSize(texture) {
|
|
1057
|
+
const imageHeight = texture?.image?.height ?? texture?.source?.data?.height ?? 1024;
|
|
1058
|
+
const maxMip = Math.max(0, Math.log2(imageHeight) - 2);
|
|
1059
|
+
return {
|
|
1060
|
+
maxMip,
|
|
1061
|
+
texelHeight: 1 / imageHeight,
|
|
1062
|
+
texelWidth: 1 / (3 * Math.max(Math.pow(2, maxMip), 7 * 16)),
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* @param {unknown} value
|
|
1068
|
+
* @returns {"three-pmrem" | "materialx-prefiltered" | "materialx-fis"}
|
|
1069
|
+
*/
|
|
1070
|
+
function normalizeEnvironmentRadianceMode(value) {
|
|
1071
|
+
if (value === "materialx-fis") return "materialx-fis";
|
|
1072
|
+
return value === "materialx-prefiltered" ? "materialx-prefiltered" : DEFAULT_ENVIRONMENT_RADIANCE_MODE;
|
|
1073
|
+
}
|