@needle-tools/materialx 1.0.1 → 1.0.2-next.c468cd8

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.
@@ -0,0 +1,478 @@
1
+ //
2
+ // Copyright Contributors to the MaterialX Project
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ //
5
+
6
+ import { getParam, getWorldDirection } from '@needle-tools/engine';
7
+ import * as THREE from 'three';
8
+ import { debug, debugUpdate } from './utils';
9
+ import { MaterialX } from './materialx.types';
10
+
11
+ const IMAGE_PROPERTY_SEPARATOR = "_";
12
+ const UADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "uaddressmode";
13
+ const VADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "vaddressmode";
14
+ const FILTER_TYPE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "filtertype";
15
+ const IMAGE_PATH_SEPARATOR = "/";
16
+
17
+ /**
18
+ * Initialized the environment texture as MaterialX expects it
19
+ * @param {THREE.Texture} texture
20
+ * @param {Object} capabilities
21
+ * @returns {THREE.Texture}
22
+ */
23
+ export function prepareEnvTexture(texture, capabilities) {
24
+ let newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, texture.format, texture.type);
25
+ newTexture.wrapS = THREE.RepeatWrapping;
26
+ newTexture.anisotropy = capabilities.getMaxAnisotropy();
27
+ newTexture.minFilter = THREE.LinearMipmapLinearFilter;
28
+ newTexture.magFilter = THREE.LinearFilter;
29
+ newTexture.generateMipmaps = true;
30
+ newTexture.needsUpdate = true;
31
+
32
+ return newTexture;
33
+ }
34
+
35
+ /**
36
+ * Get Three uniform from MaterialX vector
37
+ * @param {any} value
38
+ * @param {any} dimension
39
+ * @returns {THREE.Uniform}
40
+ */
41
+ function fromVector(value, dimension) {
42
+ let outValue;
43
+ if (value) {
44
+ outValue = [...value.data()];
45
+ }
46
+ else {
47
+ outValue = [];
48
+ for (let i = 0; i < dimension; ++i)
49
+ outValue.push(0.0);
50
+ }
51
+
52
+ return outValue;
53
+ }
54
+
55
+ /**
56
+ * Get Three uniform from MaterialX matrix
57
+ */
58
+ function fromMatrix(matrix: MaterialX.Matrix, dimension: MaterialX.Matrix["size"]) {
59
+ const vec = new Array(dimension);
60
+ if (matrix) {
61
+ for (let i = 0; i < matrix.numRows(); ++i) {
62
+ for (let k = 0; k < matrix.numColumns(); ++k) {
63
+ vec.push(matrix.getItem(i, k));
64
+ }
65
+ }
66
+ } else {
67
+ for (let i = 0; i < dimension; ++i)
68
+ vec.push(0.0);
69
+ }
70
+
71
+ return vec;
72
+ }
73
+
74
+
75
+ export type Loaders = {
76
+ /**
77
+ * Cache key for the loaders, used to identify and reuse textures
78
+ */
79
+ readonly cacheKey: string;
80
+ /**
81
+ * Get a texture by path
82
+ * @param {string} path - The path to the texture
83
+ * @return {Promise<THREE.Texture>} - A promise that resolves to the texture
84
+ */
85
+ readonly getTexture: (path: string) => Promise<THREE.Texture>;
86
+ }
87
+
88
+ const defaultTexture = new THREE.Texture();
89
+ defaultTexture.needsUpdate = true;
90
+ defaultTexture.image = new Image();
91
+ defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg=="
92
+ // defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAB5QTFRFAAAABAQEw8PD////v7+/vb29Xl5eQEBA+/v7PDw8GPBYkgAAAB1JREFUeJxjZGBgYFQSABIUMlxgDGMGBtaIAnIZAKwQCSDYUEZEAAAAAElFTkSuQmCC";
93
+ // defaultTexture.wrapS = THREE.RepeatWrapping;
94
+ // defaultTexture.wrapT = THREE.RepeatWrapping;
95
+ // defaultTexture.minFilter = THREE.NearestFilter;
96
+ // defaultTexture.magFilter = THREE.NearestFilter;
97
+ // defaultTexture.repeat = new THREE.Vector2(100, 100);
98
+
99
+
100
+ const defaultNormalTexture = new THREE.Texture();
101
+ defaultNormalTexture.needsUpdate = true;
102
+ defaultNormalTexture.image = new Image();
103
+ defaultNormalTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=";
104
+
105
+
106
+ function tryGetFromCache(key: string): any {
107
+ const wasEnabled = THREE.Cache.enabled;
108
+ THREE.Cache.enabled = true;
109
+ const value = THREE.Cache.get(key);
110
+ THREE.Cache.enabled = wasEnabled;
111
+ return value;
112
+ }
113
+ function addToCache(key: string, value: any): void {
114
+ const wasEnabled = THREE.Cache.enabled;
115
+ THREE.Cache.enabled = true;
116
+ THREE.Cache.add(key, value);
117
+ THREE.Cache.enabled = wasEnabled;
118
+ if (debug) console.log('[MaterialX] Added to cache:', key, value);
119
+ }
120
+
121
+ /**
122
+ * Get Three uniform from MaterialX value
123
+ */
124
+ function toThreeUniform(uniforms: any, type: string, value: any, name: string, loaders: Loaders, searchPath: string): THREE.Uniform {
125
+
126
+ const uniform = new THREE.Uniform<any>(null);
127
+
128
+ switch (type) {
129
+ case 'float':
130
+ case 'integer':
131
+ case 'boolean':
132
+ uniform.value = value;
133
+ break;
134
+ case 'vector2':
135
+ uniform.value = fromVector(value, 2);
136
+ break;
137
+ case 'vector3':
138
+ case 'color3':
139
+ uniform.value = fromVector(value, 3);
140
+ break;
141
+ case 'vector4':
142
+ case 'color4':
143
+ uniform.value = fromVector(value, 4);
144
+ break;
145
+ case 'matrix33':
146
+ uniform.value = fromMatrix(value, 9);
147
+ break;
148
+ case 'matrix44':
149
+ uniform.value = fromMatrix(value, 16);
150
+ break;
151
+ case 'filename':
152
+ if (value) {
153
+ // Cache / reuse texture to avoid reload overhead.
154
+ // Note: that data blobs and embedded data textures are not cached as they are transient data.
155
+ let checkCache = true;
156
+ let texturePath = searchPath + IMAGE_PATH_SEPARATOR + value;
157
+ if (value.startsWith('blob:')) {
158
+ texturePath = value;
159
+ checkCache = false;
160
+ }
161
+ else if (value.startsWith('data:')) {
162
+ texturePath = value;
163
+ checkCache = false;
164
+ }
165
+ else if (value.startsWith('http')) {
166
+ texturePath = value;
167
+ checkCache = true;
168
+ }
169
+
170
+ const cacheKey = `${loaders.cacheKey}-${texturePath}`;
171
+ const cacheValue = checkCache ? tryGetFromCache(cacheKey) : null;
172
+ if (cacheValue) {
173
+ if (debug) console.log('[MaterialX] Use cached texture: ', cacheKey, cacheValue);
174
+ if (cacheValue instanceof Promise) {
175
+ cacheValue.then(res => {
176
+ if (res) uniform.value = res;
177
+ else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
178
+ });
179
+ }
180
+ else {
181
+ uniform.value = cacheValue;
182
+ }
183
+ }
184
+ else {
185
+ if (name.toLowerCase().includes("normal")) {
186
+ uniform.value = defaultNormalTexture;
187
+ }
188
+ else {
189
+ uniform.value = defaultTexture;
190
+ }
191
+
192
+ if (debug) console.log('[MaterialX] Load texture:', texturePath);
193
+ // Save the loading promise in the cache
194
+ const promise = loaders.getTexture(texturePath).then(res => {
195
+ if (res) {
196
+ res = res.clone(); // we need to clone the texture once to avoid colorSpace issues with other materials
197
+ res.colorSpace = THREE.LinearSRGBColorSpace;
198
+ setTextureParameters(res, name, uniforms);
199
+ }
200
+ return res;
201
+ });
202
+ if (checkCache) {
203
+ addToCache(cacheKey, promise);
204
+ }
205
+ promise.then(res => {
206
+ if (res) uniform.value = res;
207
+ else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
208
+ });
209
+ }
210
+ }
211
+ break;
212
+ case 'samplerCube':
213
+ case 'string':
214
+ break;
215
+ default:
216
+ const key = type + ':' + name;
217
+ if (!valueTypeWarningMap.has(key)) {
218
+ valueTypeWarningMap.set(key, true);
219
+ console.warn('MaterialX: Unsupported uniform type: ' + type + ' for uniform: ' + name, value);
220
+ }
221
+ break;
222
+ }
223
+
224
+ return uniform;
225
+ }
226
+
227
+ const valueTypeWarningMap = new Map<string, boolean>();
228
+
229
+ /**
230
+ * Get Three wrapping mode
231
+ */
232
+ function getWrapping(mode: number): THREE.Wrapping {
233
+ let wrap;
234
+ switch (mode) {
235
+ case 1:
236
+ wrap = THREE.ClampToEdgeWrapping;
237
+ break;
238
+ case 2:
239
+ wrap = THREE.RepeatWrapping;
240
+ break;
241
+ case 3:
242
+ wrap = THREE.MirroredRepeatWrapping;
243
+ break;
244
+ default:
245
+ wrap = THREE.RepeatWrapping;
246
+ break;
247
+ }
248
+ return wrap;
249
+ }
250
+
251
+
252
+ /**
253
+ * Set Three texture parameters
254
+ */
255
+ function setTextureParameters(texture: THREE.Texture, name: string, uniforms: any, generateMipmaps = true) {
256
+ const idx = name.lastIndexOf(IMAGE_PROPERTY_SEPARATOR);
257
+ const base = name.substring(0, idx) || name;
258
+
259
+ if (uniforms.find(base + UADDRESS_MODE_SUFFIX)) {
260
+ const uaddressmode = uniforms.find(base + UADDRESS_MODE_SUFFIX).getValue().getData();
261
+ texture.wrapS = getWrapping(uaddressmode);
262
+ }
263
+
264
+ if (uniforms.find(base + VADDRESS_MODE_SUFFIX)) {
265
+ const vaddressmode = uniforms.find(base + VADDRESS_MODE_SUFFIX).getValue().getData();
266
+ texture.wrapT = getWrapping(vaddressmode);
267
+ }
268
+
269
+ const mxFilterType = uniforms.find(base + FILTER_TYPE_SUFFIX) ? uniforms.get(base + FILTER_TYPE_SUFFIX).value : -1;
270
+ let minFilter: THREE.TextureFilter = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
271
+ if (mxFilterType === 0) {
272
+ minFilter = generateMipmaps ? THREE.NearestMipMapNearestFilter : THREE.NearestFilter;
273
+ }
274
+ texture.minFilter = minFilter;
275
+ }
276
+
277
+ /**
278
+ * Return the global light rotation matrix
279
+ */
280
+ export function getLightRotation() {
281
+ return new THREE.Matrix4().makeRotationY(Math.PI / 2);
282
+ }
283
+
284
+ /**
285
+ * Returns all lights nodes in a MaterialX document
286
+ * @param {mx.Document} doc
287
+ * @returns {Array.<mx.Node>}
288
+ */
289
+ export function findLights(doc: MaterialX.Document) {
290
+ let lights = new Array<any>;
291
+ for (let node of doc.getNodes()) {
292
+ if (node.getType() === "lightshader")
293
+ lights.push(node);
294
+ }
295
+ return lights;
296
+ }
297
+
298
+ let lightTypesBound = {};
299
+
300
+ /**
301
+ * Register lights in shader generation context
302
+ * @param {MaterialX.MODULE} mx MaterialX Module
303
+ * @param {mx.GenContext} genContext Shader generation context
304
+ */
305
+ export async function registerLights(mx: MaterialX.MODULE, genContext: any): Promise<void> {
306
+ lightTypesBound = {};
307
+ const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
308
+ mx.HwShaderGenerator.unbindLightShaders(genContext);
309
+ let lightId = 1;
310
+ // All light types so that we have NodeDefs for them
311
+ const defaultLightRigXml = `<?xml version="1.0"?>
312
+ <materialx version="1.39">
313
+ <directional_light name="default_directional_light" type="lightshader">
314
+ </directional_light>
315
+ <point_light name="default_point_light" type="lightshader">
316
+ </point_light>
317
+ <spot_light name="default_spot_light" type="lightshader">
318
+ </spot_light>
319
+ <!--
320
+ <area_light name="default_area_light" type="lightshader">
321
+ </area_light>
322
+ -->
323
+ </materialx>`;
324
+
325
+ // Load default light rig XML to ensure we have all light types available
326
+ const lightRigDoc = mx.createDocument();
327
+ await mx.readFromXmlString(lightRigDoc, defaultLightRigXml, "");
328
+ const document = mx.createDocument();
329
+ const stdlib = mx.loadStandardLibraries(genContext);
330
+ document.setDataLibrary(stdlib);
331
+ document.importLibrary(lightRigDoc);
332
+ const defaultLights = findLights(document);
333
+ if (debug) console.log("Default lights in MaterialX document", defaultLights);
334
+
335
+ // Loading a document seems to reset this option for some reason, so we set it again
336
+ genContext.getOptions().hwMaxActiveLightSources = maxLightCount;
337
+
338
+ // Register types only – we get these from the default light rig XML above
339
+ // This is needed to ensure that the light shaders are bound for each light type
340
+ for (let light of defaultLights) {
341
+ const lightDef = light.getNodeDef();
342
+ if (debug) console.log("Default light node definition", lightDef);
343
+ if (!lightDef) continue;
344
+
345
+ const lightName = lightDef.getName();
346
+ if (debug) console.log("Registering default light", { lightName, lightDef });
347
+ if (!lightTypesBound[lightName]) {
348
+ // TODO check if we need to bind light shader for each three.js light instead of once per type
349
+ if (debug) console.log("Bind light shader for node", { lightName, lightId, lightDef });
350
+ lightTypesBound[lightName] = lightId;
351
+ mx.HwShaderGenerator.bindLightShader(lightDef, lightId++, genContext);
352
+ }
353
+ }
354
+
355
+ if (debug) console.log("Light types bound in MaterialX context", lightTypesBound);
356
+ }
357
+
358
+ // Converts Three.js light type to MaterialX node name
359
+ function threeLightTypeToMaterialXNodeName(threeLightType) {
360
+ switch (threeLightType) {
361
+ case 'PointLight':
362
+ return 'ND_point_light';
363
+ case 'DirectionalLight':
364
+ return 'ND_directional_light';
365
+ case 'SpotLight':
366
+ return 'ND_spot_light';
367
+ default:
368
+ console.warn('MaterialX: Unsupported light type: ' + threeLightType);
369
+ return 'ND_point_light'; // Default to point light
370
+ }
371
+ };
372
+
373
+ type LightData = {
374
+ type: number, // Light type ID
375
+ position: THREE.Vector3, // Position in world space
376
+ direction: THREE.Vector3, // Direction in world space
377
+ color: THREE.Color, // Color of the light
378
+ intensity: number, // Intensity of the light
379
+ decay_rate: number, // Decay rate for point and spot lights
380
+ inner_angle: number, // Inner angle for spot lights
381
+ outer_angle: number, // Outer angle for spot lights
382
+ }
383
+
384
+ /**
385
+ * Update light data for shader uniforms
386
+ */
387
+ export function getLightData(lights: any, genContext: any): { lightData: LightData[], lightCount: number } {
388
+ const lightData = new Array();
389
+ const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
390
+
391
+ // Three.js lights
392
+ for (let light of lights) {
393
+ // Skip if light is not a Three.js light
394
+ if (!light.isLight) continue;
395
+
396
+ // Types in MaterialX: point_light, directional_light, spot_light
397
+
398
+ const lightDefinitionName = threeLightTypeToMaterialXNodeName(light.type);
399
+
400
+ if (!lightTypesBound[lightDefinitionName])
401
+ console.error("MaterialX: Light type not registered in context. Make sure to register light types before using them.", lightDefinitionName);
402
+
403
+ const wp = light.getWorldPosition(new THREE.Vector3());
404
+ const wd = getWorldDirection(light, new THREE.Vector3(0, 0, -1));
405
+
406
+ // Shader math from the generated MaterialX shader:
407
+ // float low = min(light.inner_angle, light.outer_angle);
408
+ // float high = light.inner_angle;
409
+ // float cosDir = dot(result.direction, -light.direction);
410
+ // float spotAttenuation = smoothstep(low, high, cosDir);
411
+
412
+ const outerAngleRad = light.angle;
413
+ const innerAngleRad = outerAngleRad * (1 - light.penumbra);
414
+ const inner_angle = Math.cos(innerAngleRad);
415
+ const outer_angle = Math.cos(outerAngleRad);
416
+
417
+ lightData.push({
418
+ type: lightTypesBound[lightDefinitionName],
419
+ position: wp.clone(),
420
+ direction: wd.clone(),
421
+ color: new THREE.Color().fromArray(light.color.toArray()),
422
+ // Luminous efficacy for converting radiant power in watts (W) to luminous flux in lumens (lm) at a wavelength of 555 nm.
423
+ // 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.
424
+ intensity: light.intensity * (light.isPointLight ? 683.0 / 3.1415 : light.isSpotLight ? 683.0 / 3.1415 : 1.0),
425
+ decay_rate: 2.0,
426
+ // Approximations for testing – the relevant light has 61.57986...129.4445 as inner/outer spot angle
427
+ inner_angle: inner_angle,
428
+ outer_angle: outer_angle,
429
+ });
430
+ }
431
+
432
+ // Count the number of lights that are not empty
433
+ const lightCount = lightData.length;
434
+
435
+ // If we don't have enough entries in lightData, fill with empty lights
436
+ while (lightData.length < maxLightCount) {
437
+ const emptyLight = {
438
+ type: 0, // Default light type
439
+ position: new THREE.Vector3(0, 0, 0),
440
+ direction: new THREE.Vector3(0, 0, -1),
441
+ color: new THREE.Color(0, 0, 0),
442
+ intensity: 0.0,
443
+ decay_rate: 2.0,
444
+ inner_angle: 0.0,
445
+ outer_angle: 0.0,
446
+ };
447
+ lightData.push(emptyLight);
448
+ }
449
+
450
+ if (debugUpdate) console.log("Registered lights in MaterialX context", lightTypesBound, lightData);
451
+
452
+ return { lightData, lightCount };
453
+ }
454
+
455
+ /**
456
+ * Get uniform values for a shader
457
+ */
458
+ export function getUniformValues(shaderStage: MaterialX.ShaderStage, loaders: Loaders, searchPath: string) {
459
+ const threeUniforms = {};
460
+
461
+ const uniformBlocks = shaderStage.getUniformBlocks()
462
+ for (const [blockName, uniforms] of Object.entries(uniformBlocks)) {
463
+ // Seems struct uniforms (like in LightData) end up here as well, we should filter those out.
464
+ if (blockName === "LightData") continue;
465
+
466
+ if (!uniforms.empty()) {
467
+ for (let i = 0; i < uniforms.size(); ++i) {
468
+ const variable = uniforms.get(i);
469
+ const value = variable.getValue()?.getData();
470
+ const name = variable.getVariable();
471
+ if (debug) console.log("Adding uniform", { path: variable.getPath(), name: name, value: value, type: variable.getType().getName() });
472
+ threeUniforms[name] = toThreeUniform(uniforms, variable.getType().getName(), value, name, loaders, searchPath);;
473
+ }
474
+ }
475
+ }
476
+
477
+ return threeUniforms;
478
+ }
@@ -0,0 +1,217 @@
1
+ import { Camera, DoubleSide, FrontSide, GLSL3, MaterialParameters, Matrix3, Matrix4, Object3D, ShaderMaterial, Texture, Vector3 } from "three";
2
+ import { debug } from "./utils.js";
3
+ import { MaterialXEnvironment } from "./materialx.js";
4
+ import { getUniformValues, Loaders } from "./materialx.helper.js";
5
+ import { Context } from "@needle-tools/engine";
6
+
7
+
8
+ // Add helper matrices for uniform updates (similar to MaterialX example)
9
+ const identityMatrix = new Matrix4();
10
+ const normalMat = new Matrix3();
11
+ const viewProjMat = new Matrix4();
12
+ const worldViewPos = new Vector3();
13
+
14
+ declare type MaterialXMaterialInitParameters = {
15
+ name: string,
16
+ shader: any,
17
+ loaders: Loaders,
18
+ transparent?: boolean,
19
+ side?: MaterialParameters['side'],
20
+ }
21
+
22
+ export class MaterialXMaterial extends ShaderMaterial {
23
+
24
+ // copy(source: MaterialXMaterial): this {
25
+ // super.copy(source);
26
+ // this.name = source.name;
27
+ // this.uniforms = { ...source.uniforms }; // Shallow copy of uniforms
28
+ // this.envMapIntensity = source.envMapIntensity;
29
+ // this.envMap = source.envMap;
30
+ // this.updateUniforms = source.updateUniforms; // Copy the update function
31
+ // return this;
32
+ // }
33
+
34
+ constructor(init?: MaterialXMaterialInitParameters) {
35
+
36
+ // TODO: we need to properly copy the uniforms and other properties from the source material
37
+ if (!init) {
38
+ super();
39
+ return;
40
+ }
41
+
42
+ // Get vertex and fragment shader source, and remove #version directive for newer js.
43
+ // It's added by three.js glslVersion.
44
+ let vertexShader = init.shader.getSourceCode("vertex");
45
+ let fragmentShader = init.shader.getSourceCode("pixel");
46
+
47
+ vertexShader = vertexShader.replace(/^#version.*$/gm, '').trim();
48
+ fragmentShader = fragmentShader.replace(/^#version.*$/gm, '').trim();
49
+
50
+ // MaterialX uses different attribute names than js defaults,
51
+ // so we patch the MaterialX shaders to match the js standard names.
52
+ // Otherwise, we'd have to modify the mesh attributes (see original MaterialX for reference).
53
+
54
+ // Patch vertexShader
55
+ vertexShader = vertexShader.replace(/\bi_position\b/g, 'position');
56
+ vertexShader = vertexShader.replace(/\bi_normal\b/g, 'normal');
57
+ vertexShader = vertexShader.replace(/\bi_texcoord_0\b/g, 'uv');
58
+ vertexShader = vertexShader.replace(/\bi_texcoord_1\b/g, 'uv1');
59
+ vertexShader = vertexShader.replace(/\bi_tangent\b/g, 'tangent');
60
+ vertexShader = vertexShader.replace(/\bi_color_0\b/g, 'color');
61
+
62
+ // Patch fragmentShader
63
+ fragmentShader = fragmentShader.replace(/\bi_position\b/g, 'position');
64
+ fragmentShader = fragmentShader.replace(/\bi_normal\b/g, 'normal');
65
+ fragmentShader = fragmentShader.replace(/\bi_texcoord_0\b/g, 'uv');
66
+ fragmentShader = fragmentShader.replace(/\bi_texcoord_1\b/g, 'uv1');
67
+ fragmentShader = fragmentShader.replace(/\bi_tangent\b/g, 'tangent');
68
+ fragmentShader = fragmentShader.replace(/\bi_color_0\b/g, 'color');
69
+
70
+ // Remove `in vec3 position;` and so on since they're already declared by ShaderMaterial
71
+ vertexShader = vertexShader.replace(/in\s+vec3\s+position;/g, '');
72
+ vertexShader = vertexShader.replace(/in\s+vec3\s+normal;/g, '');
73
+ vertexShader = vertexShader.replace(/in\s+vec3\s+uv;/g, '');
74
+ vertexShader = vertexShader.replace(/in\s+vec3\s+uv1;/g, '');
75
+ vertexShader = vertexShader.replace(/in\s+vec4\s+tangent;/g, '');
76
+ vertexShader = vertexShader.replace(/in\s+vec4\s+color;/g, '');
77
+
78
+ // Patch uv 2-component to 3-component (`texcoord_0 = uv;` needs to be replaced with `texcoord_0 = vec3(uv, 0.0);`)
79
+ // TODO what if we actually have a 3-component UV? Not sure what three.js does then
80
+ vertexShader = vertexShader.replace(/texcoord_0 = uv;/g, 'texcoord_0 = vec3(uv, 0.0);');
81
+
82
+ // Patch units – seems MaterialX uses different units and we end up with wrong light values?
83
+ // result.direction = light.position - position;
84
+ fragmentShader = fragmentShader.replace(
85
+ /result\.direction\s*=\s*light\.position\s*-\s*position;/g,
86
+ 'result.direction = (light.position - position) * 10.0 / 1.0;');
87
+
88
+ // Add tonemapping and colorspace handling
89
+ // Replace `out vec4 out1;` with `out vec4 gl_FragColor;`
90
+ fragmentShader = fragmentShader.replace(
91
+ /out\s+vec4\s+out1;/,
92
+ 'layout(location = 0) out vec4 pc_fragColor;\n#define gl_FragColor pc_fragColor');
93
+
94
+ // Replace `out1 = vec4(<CAPTURE>)` with `gl_FragColor = vec4(<CAPTURE>)` and tonemapping/colorspace handling
95
+ fragmentShader = fragmentShader.replace(/^\s*out1\s*=\s*vec4\((.*)\);/gm, `
96
+ gl_FragColor = vec4($1);
97
+ #include <tonemapping_fragment>
98
+ #include <colorspace_fragment>`);
99
+
100
+ const searchPath = ""; // Could be derived from the asset path if needed
101
+ const isTransparent = init.transparent ?? false;
102
+ super({
103
+ name: init.name,
104
+ uniforms: {},
105
+ vertexShader: vertexShader,
106
+ fragmentShader: fragmentShader,
107
+ glslVersion: GLSL3,
108
+ transparent: isTransparent,
109
+ side: init.side ? init.side : FrontSide,
110
+ depthTest: true,
111
+ depthWrite: !isTransparent,
112
+ });
113
+
114
+ Object.assign(this.uniforms, {
115
+ ...getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath),
116
+ ...getUniformValues(init.shader.getStage('pixel'), init.loaders, searchPath),
117
+ u_envMatrix: { value: new Matrix4() },
118
+ u_envRadiance: { value: null, type: 't' },
119
+ u_envRadianceMips: { value: 8, type: 'i' },
120
+ // TODO we need to figure out how we can set a PMREM here... doing many texture samples is prohibitively expensive
121
+ u_envRadianceSamples: { value: 8, type: 'i' },
122
+ u_envIrradiance: { value: null, type: 't' },
123
+ u_refractionEnv: { value: true },
124
+ u_numActiveLightSources: { value: 0 },
125
+ u_lightData: { value: [] }, // Array of light data
126
+ });
127
+
128
+ if (debug) {
129
+ // Get lighting and environment data from MaterialX environment
130
+ console.group("[MaterialX]: ", name);
131
+ console.log("Vertex shader length:", vertexShader.length, vertexShader);
132
+ console.log("Fragment shader length:", fragmentShader.length, fragmentShader);
133
+ console.groupEnd();
134
+ }
135
+
136
+ }
137
+
138
+
139
+ envMapIntensity: number = 1.0; // Default intensity for environment map
140
+ envMap: Texture | null = null; // Environment map texture, can be set externally
141
+ updateUniforms = (context: Context, environment: MaterialXEnvironment, object: Object3D, camera: Camera) => {
142
+
143
+ const uniforms = this.uniforms;
144
+
145
+ // TODO remove. Not sure why this is needed, but without it
146
+ // we currently get some "swimming" where matrices are not up to date.
147
+ camera.updateMatrixWorld(true);
148
+
149
+ // Update standard transformation matrices
150
+ if (uniforms.u_worldMatrix) {
151
+ if (!uniforms.u_worldMatrix.value?.isMatrix4) uniforms.u_worldMatrix.value = new Matrix4();
152
+ uniforms.u_worldMatrix.value = object.matrixWorld;
153
+ }
154
+
155
+ if (uniforms.u_viewProjectionMatrix) {
156
+ if (!uniforms.u_viewProjectionMatrix.value?.isMatrix4) uniforms.u_viewProjectionMatrix.value = new Matrix4();
157
+ uniforms.u_viewProjectionMatrix.value.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
158
+ }
159
+
160
+ if (uniforms.u_viewPosition) {
161
+ if (!uniforms.u_viewPosition.value?.isVector3) uniforms.u_viewPosition.value = new Vector3();
162
+ uniforms.u_viewPosition.value.copy(camera.getWorldPosition(worldViewPos));
163
+ }
164
+
165
+ if (uniforms.u_worldInverseTransposeMatrix) {
166
+ if (!uniforms.u_worldInverseTransposeMatrix.value?.isMatrix4) uniforms.u_worldInverseTransposeMatrix.value = new Matrix4();
167
+ uniforms.u_worldInverseTransposeMatrix.value.setFromMatrix3(normalMat.getNormalMatrix(object.matrixWorld));
168
+ }
169
+
170
+ // Update time uniforms
171
+ if (uniforms.u_time) {
172
+ uniforms.u_time.value = context.time.time;
173
+ }
174
+ if (uniforms.u_frame) {
175
+ uniforms.u_frame.value = context.time.frame;
176
+ }
177
+
178
+ // Update light uniforms
179
+ this.updateEnvironmentUniforms(environment);
180
+
181
+ this.uniformsNeedUpdate = true;
182
+ }
183
+
184
+ private updateEnvironmentUniforms = (environment: MaterialXEnvironment) => {
185
+
186
+ // Get lighting data from environment
187
+ const lightData = environment.lightData || null;
188
+ const lightCount = environment.lightCount || 0;
189
+ const textures = environment.getTextures(this) || null;
190
+
191
+ // Update light count
192
+ if (this.uniforms.u_numActiveLightSources && lightCount >= 0) {
193
+ this.uniforms.u_numActiveLightSources.value = lightCount;
194
+ }
195
+
196
+ // Update light data
197
+ if (lightData) {
198
+ this.uniforms.u_lightData.value = lightData;
199
+ }
200
+
201
+ // Update environment uniforms
202
+ if (this.uniforms.u_envMatrix) {
203
+ this.uniforms.u_envMatrix.value = identityMatrix;
204
+ }
205
+ if (this.uniforms.u_envRadiance) {
206
+ this.uniforms.u_envRadiance.value = textures.radianceTexture || null;
207
+ }
208
+ if (this.uniforms.u_envRadianceMips) {
209
+ this.uniforms.u_envRadianceMips.value = Math.trunc(Math.log2(Math.max(textures.radianceTexture?.source.data.width ?? 0, textures.radianceTexture?.source.data.height ?? 0))) + 1;
210
+ }
211
+ if (this.uniforms.u_envIrradiance) {
212
+ this.uniforms.u_envIrradiance.value = textures.irradianceTexture;
213
+ }
214
+
215
+ this.uniformsNeedUpdate = true;
216
+ }
217
+ }