@needle-tools/materialx 1.0.1-next.b9638d9 → 1.0.1-next.c1bbe8d
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/README.md +5 -0
- package/codegen/register_types.ts +2 -0
- package/index.ts +1 -1
- package/package.json +18 -3
- package/src/helper.js +79 -62
- package/src/index.ts +2 -2
- package/src/loader/loader.needle.ts +13 -22
- package/src/loader/loader.three.ts +131 -145
- package/src/materialx.ts +112 -107
package/README.md
CHANGED
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/materialx",
|
|
3
|
-
"version": "1.0.1-next.
|
|
3
|
+
"version": "1.0.1-next.c1bbe8d",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
"import": "./index.ts",
|
|
9
9
|
"require": "./index.js"
|
|
10
10
|
},
|
|
11
|
-
"./package.json": "./package.json"
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./codegen/register_types.ts": {
|
|
13
|
+
"import": "./codegen/register_types.ts",
|
|
14
|
+
"require": "./codegen/register_types.js"
|
|
15
|
+
}
|
|
12
16
|
},
|
|
13
17
|
"peerDependencies": {
|
|
14
18
|
"@needle-tools/engine": "4.x",
|
|
@@ -23,5 +27,16 @@
|
|
|
23
27
|
"publishConfig": {
|
|
24
28
|
"access": "public",
|
|
25
29
|
"registry": "https://registry.npmjs.org/"
|
|
26
|
-
}
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"needle",
|
|
33
|
+
"materialx",
|
|
34
|
+
"material",
|
|
35
|
+
"shader",
|
|
36
|
+
"threejs",
|
|
37
|
+
"three.js",
|
|
38
|
+
"webgl",
|
|
39
|
+
"mtlx",
|
|
40
|
+
"rendering"
|
|
41
|
+
]
|
|
27
42
|
}
|
package/src/helper.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
//
|
|
5
5
|
|
|
6
|
-
import { getParam } from '@needle-tools/engine';
|
|
6
|
+
import { getParam, getWorldDirection } from '@needle-tools/engine';
|
|
7
7
|
import * as THREE from 'three';
|
|
8
8
|
|
|
9
9
|
const debug = getParam("debugmaterialx");
|
|
@@ -88,7 +88,7 @@ function fromMatrix(matrix, dimension)
|
|
|
88
88
|
* @param {mx.Uniform.value} value
|
|
89
89
|
* @param {mx.Uniform.name} name
|
|
90
90
|
* @param {mx.Uniforms} uniforms
|
|
91
|
-
* @param {THREE.
|
|
91
|
+
* @param {THREE.TextureLoader} textureLoader
|
|
92
92
|
* @param {string} searchPath
|
|
93
93
|
* @param {boolean} flipY
|
|
94
94
|
*/
|
|
@@ -291,19 +291,21 @@ export function findLights(doc)
|
|
|
291
291
|
return lights;
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
let lightTypesBound = {};
|
|
295
|
+
|
|
294
296
|
/**
|
|
295
297
|
* Register lights in shader generation context
|
|
296
298
|
* @param {Object} mx MaterialX Module
|
|
297
|
-
* @param {Array.<mx.Node>} lights Light nodes
|
|
298
299
|
* @param {mx.GenContext} genContext Shader generation context
|
|
299
300
|
* @returns {Array.<mx.Node>}
|
|
300
301
|
*/
|
|
301
|
-
export async function registerLights(mx,
|
|
302
|
+
export async function registerLights(mx, genContext)
|
|
302
303
|
{
|
|
304
|
+
lightTypesBound = {};
|
|
305
|
+
const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
|
|
306
|
+
|
|
303
307
|
mx.HwShaderGenerator.unbindLightShaders(genContext);
|
|
304
308
|
|
|
305
|
-
const lightTypesBound = {};
|
|
306
|
-
const lightData = [];
|
|
307
309
|
let lightId = 1;
|
|
308
310
|
|
|
309
311
|
// All light types so that we have NodeDefs for them
|
|
@@ -329,7 +331,11 @@ export async function registerLights(mx, lights, genContext)
|
|
|
329
331
|
document.setDataLibrary(stdlib);
|
|
330
332
|
document.importLibrary(lightRigDoc);
|
|
331
333
|
const defaultLights = findLights(document);
|
|
332
|
-
if (debug)
|
|
334
|
+
// if (debug)
|
|
335
|
+
console.log("Default lights in MaterialX document", defaultLights);
|
|
336
|
+
|
|
337
|
+
// Loading a document seems to reset this option for some reason, so we set it again
|
|
338
|
+
genContext.getOptions().hwMaxActiveLightSources = maxLightCount;
|
|
333
339
|
|
|
334
340
|
// Register types only – we get these from the default light rig XML above
|
|
335
341
|
// This is needed to ensure that the light shaders are bound for each light type
|
|
@@ -351,52 +357,34 @@ export async function registerLights(mx, lights, genContext)
|
|
|
351
357
|
}
|
|
352
358
|
|
|
353
359
|
if (debug) console.log("Light types bound in MaterialX context", lightTypesBound);
|
|
360
|
+
}
|
|
354
361
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
{
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const lightDirection = light.getValueElement("direction").getValue().getData().data();
|
|
371
|
-
const lightColor = light.getValueElement("color").getValue().getData().data();
|
|
372
|
-
const lightIntensity = light.getValueElement("intensity").getValue().getData();
|
|
373
|
-
|
|
374
|
-
let rotatedLightDirection = new THREE.Vector3(...lightDirection)
|
|
375
|
-
rotatedLightDirection.transformDirection(getLightRotation())
|
|
376
|
-
|
|
377
|
-
lightData.push({
|
|
378
|
-
type: lightTypesBound[nodeName],
|
|
379
|
-
direction: rotatedLightDirection,
|
|
380
|
-
color: new THREE.Vector3(...lightColor),
|
|
381
|
-
intensity: lightIntensity,
|
|
382
|
-
});
|
|
362
|
+
// Converts Three.js light type to MaterialX node name
|
|
363
|
+
function threeLightTypeToMaterialXNodeName(threeLightType) {
|
|
364
|
+
switch (threeLightType) {
|
|
365
|
+
case 'PointLight':
|
|
366
|
+
return 'ND_point_light';
|
|
367
|
+
case 'DirectionalLight':
|
|
368
|
+
return 'ND_directional_light';
|
|
369
|
+
case 'SpotLight':
|
|
370
|
+
return 'ND_spot_light';
|
|
371
|
+
default:
|
|
372
|
+
console.warn('MaterialX: Unsupported light type: ' + threeLightType);
|
|
373
|
+
return 'ND_point_light'; // Default to point light
|
|
383
374
|
}
|
|
375
|
+
};
|
|
384
376
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
if (debug) console.log("Registering lights in MaterialX context", lights, lightData);
|
|
377
|
+
/**
|
|
378
|
+
* Update light data for shader uniforms
|
|
379
|
+
* @param {Object} mx MaterialX Module
|
|
380
|
+
* @param {Array.<mx.Node>} lights Light nodes
|
|
381
|
+
* @param {mx.GenContext} genContext Shader generation context
|
|
382
|
+
* @returns {{ lightData: Array<any>, lightCount: number }}
|
|
383
|
+
*/
|
|
384
|
+
export function getLightData(mx, lights, genContext)
|
|
385
|
+
{
|
|
386
|
+
const lightData = [];
|
|
387
|
+
const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
|
|
400
388
|
|
|
401
389
|
// Three.js lights
|
|
402
390
|
for (let light of lights) {
|
|
@@ -408,24 +396,51 @@ export async function registerLights(mx, lights, genContext)
|
|
|
408
396
|
const lightDefinitionName = threeLightTypeToMaterialXNodeName(light.type);
|
|
409
397
|
|
|
410
398
|
if (!lightTypesBound[lightDefinitionName])
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
399
|
+
console.error("MaterialX: Light type not registered in context. Make sure to register light types before using them.", lightDefinitionName);
|
|
400
|
+
|
|
401
|
+
const wp = light.getWorldPosition(new THREE.Vector3());
|
|
402
|
+
const wd = getWorldDirection(light, new THREE.Vector3(0,0,-1));
|
|
403
|
+
|
|
404
|
+
// console.log("Registering light", light.penumbra);
|
|
416
405
|
|
|
417
406
|
lightData.push({
|
|
418
407
|
type: lightTypesBound[lightDefinitionName],
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
408
|
+
position: wp.clone(),
|
|
409
|
+
direction: wd.clone(),
|
|
410
|
+
color: new THREE.Color().fromArray(light.color.toArray()),
|
|
411
|
+
// Luminous efficacy for converting radiant power in watts (W) to luminous flux in lumens (lm) at a wavelength of 555 nm.
|
|
412
|
+
// 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.
|
|
413
|
+
intensity: light.intensity * (light.isPointLight ? 683.0 / 3.1415 : light.isSpotLight ? 683.0 / 3.1415: 1.0),
|
|
414
|
+
decay_rate: 2.0,
|
|
415
|
+
// Approximations for testing – the relevant light has 61.57986...129.4445 as inner/outer spot angle
|
|
416
|
+
inner_angle: 0.9,
|
|
417
|
+
outer_angle: 0.4,
|
|
422
418
|
});
|
|
423
419
|
}
|
|
424
420
|
|
|
425
|
-
//
|
|
426
|
-
|
|
421
|
+
// Count the number of lights that are not empty
|
|
422
|
+
const lightCount = lightData.length;
|
|
423
|
+
|
|
424
|
+
// If we don't have enough entries in lightData, fill with empty lights
|
|
425
|
+
while (lightData.length < maxLightCount)
|
|
426
|
+
{
|
|
427
|
+
const emptyLight = {
|
|
428
|
+
type: 0, // Default light type
|
|
429
|
+
position: new THREE.Vector3(0, 0, 0),
|
|
430
|
+
direction: new THREE.Vector3(0, 0, -1),
|
|
431
|
+
color: new THREE.Color(0, 0, 0),
|
|
432
|
+
intensity: 0.0,
|
|
433
|
+
decay_rate: 2.0,
|
|
434
|
+
inner_angle: 0.0,
|
|
435
|
+
outer_angle: 0.0,
|
|
436
|
+
};
|
|
437
|
+
lightData.push(emptyLight);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (debug)
|
|
441
|
+
console.log("Registered lights in MaterialX context", lightTypesBound, lightData);
|
|
427
442
|
|
|
428
|
-
return lightData;
|
|
443
|
+
return { lightData, lightCount };
|
|
429
444
|
}
|
|
430
445
|
|
|
431
446
|
/**
|
|
@@ -435,11 +450,12 @@ export async function registerLights(mx, lights, genContext)
|
|
|
435
450
|
*/
|
|
436
451
|
export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
|
|
437
452
|
{
|
|
438
|
-
|
|
453
|
+
const threeUniforms = {};
|
|
439
454
|
|
|
440
455
|
const uniformBlocks = Object.values(shaderStage.getUniformBlocks());
|
|
441
456
|
uniformBlocks.forEach(uniforms =>
|
|
442
457
|
{
|
|
458
|
+
// TODO Seems struct uniforms (like in LightData) end up here as well, we should filter those out.
|
|
443
459
|
if (!uniforms.empty())
|
|
444
460
|
{
|
|
445
461
|
for (let i = 0; i < uniforms.size(); ++i)
|
|
@@ -447,6 +463,7 @@ export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
|
|
|
447
463
|
const variable = uniforms.get(i);
|
|
448
464
|
const value = variable.getValue()?.getData();
|
|
449
465
|
const name = variable.getVariable();
|
|
466
|
+
if (debug) console.log("Adding uniform", { name, value, type: variable.getType().getName() });
|
|
450
467
|
threeUniforms[name] = new THREE.Uniform(toThreeUniform(variable.getType().getName(), value, name, uniforms,
|
|
451
468
|
textureLoader, searchPath, flipY));
|
|
452
469
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ready, state } from "./materialx.js";
|
|
2
2
|
|
|
3
3
|
const getMaterialXEnvironment = () => state.materialXEnvironment;
|
|
4
4
|
|
|
5
|
-
export {
|
|
5
|
+
export { ready, getMaterialXEnvironment };
|
|
@@ -38,23 +38,15 @@ export class MaterialXUniformUpdate extends Component {
|
|
|
38
38
|
|
|
39
39
|
const camera = this.context.mainCamera;
|
|
40
40
|
if (!camera) return;
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
MaterialXUniformUpdate.updateMaterial(material, gameObject, camera);
|
|
43
|
-
|
|
44
|
-
// If this is a Group, we need to update all direct children
|
|
45
|
-
if ((gameObject as any as Group).isGroup) {
|
|
46
|
-
gameObject.children.forEach((child: Object3D) => {
|
|
47
|
-
if (child instanceof Mesh && child.material)
|
|
48
|
-
MaterialXUniformUpdate.updateMaterial(child.material, child, camera);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
43
|
}
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
|
|
55
|
-
name = "MaterialXLoaderPlugin";
|
|
47
|
+
readonly name = "MaterialXLoaderPlugin";
|
|
56
48
|
|
|
57
|
-
|
|
49
|
+
private loader: MaterialXLoader | null = null;
|
|
58
50
|
|
|
59
51
|
onImport = (loader: GLTFLoader, url: string, context: Context) => {
|
|
60
52
|
if (debug) console.log("MaterialXLoaderPlugin: Registering MaterialX extension for", url);
|
|
@@ -62,13 +54,13 @@ export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
|
|
|
62
54
|
// Register the MaterialX loader extension
|
|
63
55
|
// Environment initialization is now handled in the MaterialXLoader constructor
|
|
64
56
|
loader.register(p => {
|
|
65
|
-
this.
|
|
66
|
-
return this.
|
|
57
|
+
this.loader = new MaterialXLoader(p, context);
|
|
58
|
+
return this.loader;
|
|
67
59
|
});
|
|
68
60
|
};
|
|
69
61
|
|
|
70
|
-
onLoaded = (url: string, gltf: GLTF,
|
|
71
|
-
if (debug) console.log("MaterialXLoaderPlugin: glTF loaded", url, gltf.scene);
|
|
62
|
+
onLoaded = (url: string, gltf: GLTF, context: Context) => {
|
|
63
|
+
if (debug) console.log("[MaterialX] MaterialXLoaderPlugin: glTF loaded", url, gltf.scene);
|
|
72
64
|
|
|
73
65
|
// Set up onBeforeRender callbacks for objects with MaterialX materials
|
|
74
66
|
// This ensures uniforms are updated properly during rendering
|
|
@@ -78,25 +70,24 @@ export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
|
|
|
78
70
|
const material = mesh.material as Material;
|
|
79
71
|
|
|
80
72
|
if (material?.userData?.updateUniforms) {
|
|
81
|
-
if (debug) console.log("Adding MaterialX uniform update component to:", child.name);
|
|
73
|
+
if (debug) console.log("[MaterialX] Adding MaterialX uniform update component to:", child.name);
|
|
82
74
|
child.addComponent(MaterialXUniformUpdate);
|
|
83
75
|
}
|
|
84
76
|
}
|
|
85
77
|
});
|
|
86
78
|
|
|
87
|
-
if (debug) console.log("Loaded: ", this.
|
|
79
|
+
if (debug) console.log("[MaterialX] Loaded: ", this.loader);
|
|
88
80
|
|
|
89
81
|
// Initialize MaterialX lighting system with scene data
|
|
90
82
|
const environment = state.materialXEnvironment;
|
|
91
|
-
environment.initializeFromContext().then(() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
83
|
+
environment.initializeFromContext(context).then(() => {
|
|
84
|
+
console.warn("[MaterialX] Environment initialized...");
|
|
85
|
+
this.loader?.updateLightingFromEnvironment(environment);
|
|
95
86
|
});
|
|
96
87
|
};
|
|
97
88
|
|
|
98
89
|
onExport = (_exporter: GLTFExporter, _context: Context) => {
|
|
99
|
-
console.log("TODO: MaterialXLoaderPlugin: Setting up export extensions");
|
|
90
|
+
console.log("[MaterialX] TODO: MaterialXLoaderPlugin: Setting up export extensions");
|
|
100
91
|
// TODO: Add MaterialX export functionality if needed
|
|
101
92
|
};
|
|
102
93
|
}
|
|
@@ -1,91 +1,59 @@
|
|
|
1
|
-
import { Context } from "@needle-tools/engine";
|
|
2
|
-
import {
|
|
1
|
+
import { Context, GameObject } from "@needle-tools/engine";
|
|
2
|
+
import { ShaderMaterial, Material, MeshStandardMaterial, LoadingManager, TextureLoader, Texture, NearestFilter, Matrix4, GLSL3, AddEquation, OneMinusSrcAlphaFactor, SrcAlphaFactor, DoubleSide, Matrix3, Vector3, Object3D, Camera, Uniform } from "three";
|
|
3
3
|
import { GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
4
4
|
import { getUniformValues } from "../helper.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ready, MaterialXEnvironment, state } from "../materialx.js";
|
|
6
6
|
import { debug } from "../utils.js";
|
|
7
7
|
|
|
8
8
|
// TypeScript interfaces matching the C# data structures
|
|
9
|
-
interface
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
interface MaterialX_root_extension {
|
|
10
|
+
/** e.g. 1.39 */
|
|
11
|
+
version: string;
|
|
12
|
+
/** e.g. "Material" */
|
|
13
|
+
name: string;
|
|
14
|
+
/** MaterialX xml content */
|
|
15
|
+
mtlx: string;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
|
-
interface
|
|
16
|
-
name: string;
|
|
18
|
+
interface MaterialX_material_extension {
|
|
19
|
+
name: string; // Material name reference
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
// MaterialX loader extension for js GLTFLoader
|
|
20
23
|
export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
21
24
|
name = "NEEDLE_materials_mtlx";
|
|
22
25
|
|
|
23
|
-
private rootMaterialXData:
|
|
24
|
-
private
|
|
25
|
-
private documentParsePromise: Promise<any> | null = null;
|
|
26
|
-
private rootDataInitialized = false;
|
|
26
|
+
// private rootMaterialXData: MaterialX_root_extension | null = null;
|
|
27
|
+
private _documentReadyPromise: Promise<any> | null = null;
|
|
27
28
|
private environmentInitialized = false;
|
|
28
|
-
private generatedMaterials:
|
|
29
|
+
private generatedMaterials: ShaderMaterial[] = [];
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Initialize MaterialX environment after MaterialX is ready
|
|
33
|
-
this.initializeEnvironment();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Initialize MaterialX environment - called once after MaterialX is ready
|
|
37
|
-
private async initializeEnvironment(): Promise<void> {
|
|
38
|
-
if (this.environmentInitialized) return;
|
|
39
|
-
|
|
40
|
-
if (debug) console.log("MaterialXLoader: Initializing MaterialX environment...");
|
|
41
|
-
|
|
42
|
-
// Ensure MaterialX is initialized first
|
|
43
|
-
await initializeMaterialX();
|
|
44
|
-
|
|
45
|
-
// Set up environment with context
|
|
46
|
-
const environment = state.materialXEnvironment;
|
|
47
|
-
environment.setContext(this.context);
|
|
48
|
-
|
|
49
|
-
// Initialize the environment from context (properly awaited)
|
|
50
|
-
try {
|
|
51
|
-
await environment.initializeFromContext();
|
|
52
|
-
this.environmentInitialized = true;
|
|
53
|
-
if (debug) console.log("MaterialXLoader: Environment initialized successfully");
|
|
54
|
-
} catch (error) {
|
|
55
|
-
console.warn("MaterialXLoader: Failed to initialize MaterialX environment:", error);
|
|
56
|
-
}
|
|
31
|
+
get materialX_root_data() {
|
|
32
|
+
return this.parser.json.extensions?.[this.name] as MaterialX_root_extension | null;
|
|
57
33
|
}
|
|
58
34
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (debug) console.log("MaterialX extension found in root:", gltfExtensions[this.name]);
|
|
66
|
-
|
|
67
|
-
const materialXExtension = gltfExtensions[this.name];
|
|
68
|
-
this.rootMaterialXData = materialXExtension as MaterialXData;
|
|
35
|
+
constructor(private parser: GLTFParser, private context: Context) {
|
|
36
|
+
if (debug) console.log("MaterialXLoader created for parser");
|
|
37
|
+
// Start loading of MaterialX environment if the root extension exists
|
|
38
|
+
const hasMaterialXExtension = this.parser.json.extensions?.[this.name] != null;
|
|
39
|
+
if (hasMaterialXExtension) {
|
|
40
|
+
ready();
|
|
69
41
|
}
|
|
70
|
-
this.rootDataInitialized = true;
|
|
71
42
|
}
|
|
72
43
|
|
|
73
44
|
// Parse the MaterialX document once and cache it
|
|
74
|
-
private async
|
|
75
|
-
if (this.
|
|
76
|
-
return this.
|
|
45
|
+
private async _materialXDocumentReady(): Promise<any> {
|
|
46
|
+
if (this._documentReadyPromise) {
|
|
47
|
+
return this._documentReadyPromise;
|
|
77
48
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (this.parsedDocument) return this.parsedDocument;
|
|
81
|
-
|
|
82
|
-
if (debug) console.log("Parsing MaterialX root document...");
|
|
49
|
+
return this._documentReadyPromise = (async () => {
|
|
50
|
+
if (debug) console.log("[MaterialX] Parsing MaterialX root document...");
|
|
83
51
|
|
|
84
52
|
// Ensure MaterialX is initialized
|
|
85
|
-
await
|
|
53
|
+
await ready();
|
|
86
54
|
|
|
87
55
|
if (!state.materialXModule) {
|
|
88
|
-
throw new Error("MaterialX module failed to initialize");
|
|
56
|
+
throw new Error("[MaterialX] module failed to initialize");
|
|
89
57
|
}
|
|
90
58
|
|
|
91
59
|
// Create MaterialX document and parse ALL the XML data from root
|
|
@@ -93,18 +61,15 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
93
61
|
doc.setDataLibrary(state.materialXStdLib);
|
|
94
62
|
|
|
95
63
|
// Parse all MaterialX XML strings from the root data
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
64
|
+
const root = this.materialX_root_data
|
|
65
|
+
if (root) {
|
|
66
|
+
if (debug) console.log(`[MaterialX] Parsing XML for: ${root.name}`);
|
|
67
|
+
await state.materialXModule.readFromXmlString(doc, root.mtlx, "");
|
|
99
68
|
}
|
|
100
69
|
|
|
101
|
-
if (debug) console.log("MaterialX root document parsed successfully");
|
|
102
|
-
|
|
103
|
-
this.parsedDocument = doc;
|
|
70
|
+
if (debug) console.log("[MaterialX] root document parsed successfully");
|
|
104
71
|
return doc;
|
|
105
72
|
})();
|
|
106
|
-
|
|
107
|
-
return this.documentParsePromise;
|
|
108
73
|
}
|
|
109
74
|
|
|
110
75
|
loadMaterial(materialIndex: number): Promise<Material> | null {
|
|
@@ -118,14 +83,12 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
118
83
|
}
|
|
119
84
|
|
|
120
85
|
private async _loadMaterialAsync(materialIndex: number): Promise<Material> {
|
|
121
|
-
// Initialize root data first
|
|
122
|
-
this.initializeRootData();
|
|
123
86
|
|
|
124
87
|
const materialDef = this.parser.json.materials?.[materialIndex];
|
|
125
|
-
if (debug) console.log("MaterialX extension found in material:", materialDef.extensions[this.name]);
|
|
88
|
+
if (debug) console.log("[MaterialX] extension found in material:", materialDef.extensions[this.name]);
|
|
126
89
|
|
|
127
90
|
// Handle different types of MaterialX data
|
|
128
|
-
const dataIndex = materialDef.extensions[this.name] as
|
|
91
|
+
const dataIndex = materialDef.extensions[this.name] as MaterialX_material_extension;
|
|
129
92
|
|
|
130
93
|
if (dataIndex) {
|
|
131
94
|
// Create a new material and process MaterialX - AWAIT THIS!
|
|
@@ -138,21 +101,14 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
138
101
|
return fallbackMaterial;
|
|
139
102
|
}
|
|
140
103
|
|
|
141
|
-
private
|
|
142
|
-
private async createMaterialXMaterial(materialXData: MaterialXDataIndex): Promise<Material> {
|
|
104
|
+
private async createMaterialXMaterial(materialXData: MaterialX_material_extension): Promise<Material> {
|
|
143
105
|
try {
|
|
144
106
|
if (debug) console.log(`Creating MaterialX material: ${materialXData.name}`);
|
|
145
107
|
|
|
146
|
-
|
|
147
|
-
await initializeMaterialX();
|
|
148
|
-
|
|
149
|
-
if (!this.rootDocument) {
|
|
150
|
-
this.rootDocument = this.parseRootDocument();
|
|
151
|
-
}
|
|
152
|
-
const doc = await this.rootDocument;
|
|
108
|
+
const doc = await this._materialXDocumentReady();
|
|
153
109
|
|
|
154
110
|
if (!state.materialXModule || !state.materialXGenerator || !state.materialXGenContext) {
|
|
155
|
-
console.warn("MaterialX WASM module not ready, returning fallback material");
|
|
111
|
+
console.warn("[MaterialX] WASM module not ready, returning fallback material");
|
|
156
112
|
const fallbackMaterial = new MeshStandardMaterial();
|
|
157
113
|
fallbackMaterial.userData.materialX = materialXData;
|
|
158
114
|
fallbackMaterial.name = `MaterialX_Fallback_${materialXData.name}`;
|
|
@@ -163,11 +119,11 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
163
119
|
let renderableElement = null;
|
|
164
120
|
let foundRenderable = false;
|
|
165
121
|
|
|
166
|
-
if (debug) console.log("
|
|
122
|
+
if (debug) console.log("[MaterialX] document", doc);
|
|
167
123
|
|
|
168
124
|
// Search for material nodes first (following the reference pattern)
|
|
169
125
|
const materialNodes = doc.getMaterialNodes();
|
|
170
|
-
if (debug) console.log(`Found ${materialNodes.length} material nodes in document`, materialNodes);
|
|
126
|
+
if (debug) console.log(`[MaterialX] Found ${materialNodes.length} material nodes in document`, materialNodes);
|
|
171
127
|
|
|
172
128
|
// Handle both array and vector-like APIs
|
|
173
129
|
const materialNodesLength = materialNodes.length;
|
|
@@ -175,13 +131,13 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
175
131
|
const materialNode = materialNodes[i];
|
|
176
132
|
if (materialNode) {
|
|
177
133
|
const materialName = materialNode.getNamePath();
|
|
178
|
-
if (debug) console.log('Scan material: ', i, materialName);
|
|
134
|
+
if (debug) console.log('[MaterialX] Scan material: ', i, materialName);
|
|
179
135
|
|
|
180
136
|
// Find the matching material
|
|
181
137
|
if (materialName == materialXData.name) {
|
|
182
138
|
renderableElement = materialNode;
|
|
183
139
|
foundRenderable = true;
|
|
184
|
-
if (debug) console.log('-- add material: ', materialName);
|
|
140
|
+
if (debug) console.log('[MaterialX] -- add material: ', materialName);
|
|
185
141
|
break;
|
|
186
142
|
}
|
|
187
143
|
}
|
|
@@ -235,35 +191,35 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
235
191
|
*/
|
|
236
192
|
|
|
237
193
|
if (!renderableElement) {
|
|
238
|
-
console.warn("No renderable element found in MaterialX document");
|
|
194
|
+
console.warn("[MaterialX] No renderable element found in MaterialX document");
|
|
239
195
|
const fallbackMaterial = new MeshStandardMaterial();
|
|
240
196
|
fallbackMaterial.userData.materialX = materialXData;
|
|
241
197
|
fallbackMaterial.name = `MaterialX_NoRenderable_${materialXData.name}`;
|
|
242
198
|
return fallbackMaterial;
|
|
243
199
|
}
|
|
244
200
|
|
|
245
|
-
if (debug) console.log("Using renderable element for shader generation");
|
|
201
|
+
if (debug) console.log("[MaterialX] Using renderable element for shader generation");
|
|
246
202
|
|
|
247
203
|
// Check transparency and set context options like the reference
|
|
248
|
-
|
|
204
|
+
const isTransparent = state.materialXModule.isTransparentSurface(renderableElement, state.materialXGenerator.getTarget());
|
|
249
205
|
state.materialXGenContext.getOptions().hwTransparency = isTransparent;
|
|
250
|
-
state.materialXGenContext.getOptions().shaderInterfaceType = state.materialXModule.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
|
|
251
|
-
state.materialXGenContext.getOptions().hwSrgbEncodeOutput = true; // Like the reference
|
|
252
206
|
|
|
253
207
|
// Generate shaders using the element's name path
|
|
254
|
-
if (debug) console.log("Generating MaterialX shaders...");
|
|
208
|
+
if (debug) console.log("[MaterialX] Generating MaterialX shaders...");
|
|
255
209
|
const elementName = (renderableElement as any).getNamePath ? (renderableElement as any).getNamePath() : (renderableElement as any).getName();
|
|
256
210
|
|
|
257
211
|
const shader = state.materialXGenerator.generate(elementName, renderableElement, state.materialXGenContext);
|
|
258
212
|
|
|
259
|
-
// Get vertex and fragment shader source
|
|
260
|
-
//
|
|
213
|
+
// Get vertex and fragment shader source, and remove #version directive for newer js.
|
|
214
|
+
// It's added by three.js glslVersion.
|
|
261
215
|
let vertexShader = shader.getSourceCode("vertex").replace(/^#version.*$/gm, '').trim();
|
|
262
216
|
let fragmentShader = shader.getSourceCode("pixel").replace(/^#version.*$/gm, '').trim();
|
|
263
217
|
|
|
264
218
|
// MaterialX uses different attribute names than js defaults,
|
|
265
219
|
// so we patch the MaterialX shaders to match the js standard names.
|
|
266
|
-
// Otherwise, we'd have to modify the mesh attributes (see
|
|
220
|
+
// Otherwise, we'd have to modify the mesh attributes (see original MaterialX for reference).
|
|
221
|
+
|
|
222
|
+
// Patch vertexShader
|
|
267
223
|
vertexShader = vertexShader.replace(/\bi_position\b/g, 'position');
|
|
268
224
|
vertexShader = vertexShader.replace(/\bi_normal\b/g, 'normal');
|
|
269
225
|
vertexShader = vertexShader.replace(/\bi_texcoord_0\b/g, 'uv');
|
|
@@ -271,6 +227,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
271
227
|
vertexShader = vertexShader.replace(/\bi_tangent\b/g, 'tangent');
|
|
272
228
|
vertexShader = vertexShader.replace(/\bi_color_0\b/g, 'color');
|
|
273
229
|
|
|
230
|
+
// Patch fragmentShader
|
|
274
231
|
fragmentShader = fragmentShader.replace(/\bi_position\b/g, 'position');
|
|
275
232
|
fragmentShader = fragmentShader.replace(/\bi_normal\b/g, 'normal');
|
|
276
233
|
fragmentShader = fragmentShader.replace(/\bi_texcoord_0\b/g, 'uv');
|
|
@@ -278,19 +235,39 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
278
235
|
fragmentShader = fragmentShader.replace(/\bi_tangent\b/g, 'tangent');
|
|
279
236
|
fragmentShader = fragmentShader.replace(/\bi_color_0\b/g, 'color');
|
|
280
237
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
238
|
+
// Remove `in vec3 position;` and so on since they're already declared by ShaderMaterial
|
|
239
|
+
vertexShader = vertexShader.replace(/in\s+vec3\s+position;/g, '');
|
|
240
|
+
vertexShader = vertexShader.replace(/in\s+vec3\s+normal;/g, '');
|
|
241
|
+
vertexShader = vertexShader.replace(/in\s+vec3\s+uv;/g, '');
|
|
242
|
+
vertexShader = vertexShader.replace(/in\s+vec3\s+uv1;/g, '');
|
|
243
|
+
vertexShader = vertexShader.replace(/in\s+vec4\s+tangent;/g, '');
|
|
244
|
+
vertexShader = vertexShader.replace(/in\s+vec4\s+color;/g, '');
|
|
245
|
+
|
|
246
|
+
// Patch uv 2-component to 3-component (`texcoord_0 = uv;` needs to be replaced with `texcoord_0 = vec3(uv, 0.0);`)
|
|
247
|
+
// TODO what if we actually have a 3-component UV? Not sure what three.js does then
|
|
248
|
+
vertexShader = vertexShader.replace(/texcoord_0 = uv;/g, 'texcoord_0 = vec3(uv, 0.0);');
|
|
249
|
+
|
|
250
|
+
// Patch units – seems MaterialX uses different units and we end up with wrong light values?
|
|
251
|
+
// result.direction = light.position - position;
|
|
252
|
+
fragmentShader = fragmentShader.replace(
|
|
253
|
+
/result\.direction\s*=\s*light\.position\s*-\s*position;/g,
|
|
254
|
+
'result.direction = (light.position - position) * 10.0 / 1.0;');
|
|
255
|
+
|
|
256
|
+
// Add tonemapping and colorspace handling
|
|
257
|
+
// Replace `out vec4 out1;` with `out vec4 gl_FragColor;`
|
|
258
|
+
fragmentShader = fragmentShader.replace(
|
|
259
|
+
/out\s+vec4\s+out1;/,
|
|
260
|
+
'layout(location = 0) out vec4 pc_fragColor;\n#define gl_FragColor pc_fragColor');
|
|
261
|
+
|
|
262
|
+
// Replace `out1 = vec4(<CAPTURE>)` with `gl_FragColor = vec4(<CAPTURE>)` and tonemapping/colorspace handling
|
|
263
|
+
fragmentShader = fragmentShader.replace(/^\s*out1\s*=\s*vec4\((.*)\);/gm,
|
|
264
|
+
`
|
|
265
|
+
gl_FragColor = vec4($1);
|
|
266
|
+
#include <tonemapping_fragment>
|
|
267
|
+
#include <colorspace_fragment>`);
|
|
291
268
|
|
|
292
269
|
if (debug) {
|
|
293
|
-
console.group("
|
|
270
|
+
console.group("[MaterialX]: ", materialXData.name);
|
|
294
271
|
console.log("Vertex shader length:", vertexShader.length, vertexShader);
|
|
295
272
|
console.log("Fragment shader length:", fragmentShader.length, fragmentShader);
|
|
296
273
|
console.groupEnd();
|
|
@@ -303,7 +280,8 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
303
280
|
// Find the texture in the textures in the parser and load it from there
|
|
304
281
|
return url;
|
|
305
282
|
});
|
|
306
|
-
|
|
283
|
+
|
|
284
|
+
const textureLoader = new TextureLoader(loadingManager);
|
|
307
285
|
|
|
308
286
|
// Override the textureLoader.load method to use the parser's loadTexture directly,
|
|
309
287
|
// since we want to load the textures from the glTF document and not from disk.
|
|
@@ -329,17 +307,17 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
329
307
|
const textures = ext?.textures || [];
|
|
330
308
|
|
|
331
309
|
const index = textures.findIndex(tex => {
|
|
332
|
-
if (debug) console.log("Checking texture:", tex.name, "against URL:", filenameWithoutExt);
|
|
310
|
+
if (debug) console.log("[MaterialX] Checking texture:", tex.name, "against URL:", filenameWithoutExt);
|
|
333
311
|
return tex.name === filenameWithoutExt;
|
|
334
312
|
});
|
|
335
313
|
|
|
336
314
|
if (index < 0) {
|
|
337
|
-
console.warn("Texture not found in parser:", filenameWithoutExt, this.parser.json);
|
|
315
|
+
console.warn("[MaterialX] Texture not found in parser:", filenameWithoutExt, this.parser.json);
|
|
338
316
|
onError?.(new Error(`Texture not found: ${filenameWithoutExt}`));
|
|
339
317
|
return;
|
|
340
318
|
}
|
|
341
319
|
this.parser.getDependency("texture", index).then(tex => {
|
|
342
|
-
if (debug) console.log("Texture loaded:", tex);
|
|
320
|
+
if (debug) console.log("[MaterialX] Texture loaded:", tex);
|
|
343
321
|
// update the checkerboard texture with the loaded texture
|
|
344
322
|
checkerboardTexture.image = tex.image;
|
|
345
323
|
checkerboardTexture.needsUpdate = true;
|
|
@@ -355,7 +333,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
355
333
|
const searchPath = ""; // Could be derived from the asset path if needed
|
|
356
334
|
const flipV = false; // Set based on your geometry requirements
|
|
357
335
|
|
|
358
|
-
|
|
336
|
+
const uniforms = {
|
|
359
337
|
...getUniformValues(shader.getStage('vertex'), textureLoader, searchPath, flipV),
|
|
360
338
|
...getUniformValues(shader.getStage('pixel'), textureLoader, searchPath, flipV),
|
|
361
339
|
};
|
|
@@ -371,20 +349,12 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
371
349
|
return new Matrix4();
|
|
372
350
|
};
|
|
373
351
|
|
|
374
|
-
if (debug) console.log({ lightData, radianceTexture, irradianceTexture });
|
|
375
|
-
|
|
376
|
-
Object.assign(uniforms, {
|
|
377
|
-
u_numActiveLightSources: { value: lightData?.length || 0, type: 'i' },
|
|
378
|
-
});
|
|
352
|
+
if (debug) console.log("Lights", { lightData, radianceTexture, irradianceTexture });
|
|
379
353
|
|
|
380
|
-
if (
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
});
|
|
354
|
+
if (debug) {
|
|
355
|
+
const mips = Math.trunc(Math.log2(Math.max(radianceTexture?.width ?? 0, radianceTexture?.height ?? 0))) + 1;
|
|
356
|
+
console.log("[MaterialX] Radiance texture mips:", mips, "for texture size:", radianceTexture?.width, "x", radianceTexture?.height);
|
|
384
357
|
}
|
|
385
|
-
|
|
386
|
-
const mips = Math.trunc(Math.log2(Math.max(radianceTexture?.width ?? 0, radianceTexture?.height ?? 0))) + 1;
|
|
387
|
-
if (debug) console.log("Radiance texture mips:", mips, "for texture size:", radianceTexture?.width, "x", radianceTexture?.height);
|
|
388
358
|
Object.assign(uniforms, {
|
|
389
359
|
u_envMatrix: { value: getLightRotation() },
|
|
390
360
|
u_envRadiance: { value: radianceTexture, type: 't' },
|
|
@@ -393,6 +363,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
393
363
|
u_envRadianceSamples: { value: 8, type: 'i' },
|
|
394
364
|
u_envIrradiance: { value: irradianceTexture, type: 't' },
|
|
395
365
|
u_refractionEnv: { value: true },
|
|
366
|
+
u_lightData: { value: [] },
|
|
396
367
|
});
|
|
397
368
|
|
|
398
369
|
// console.log("NEW MATERIAL UNIFORMS", uniforms);
|
|
@@ -402,7 +373,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
402
373
|
// console.log("Generated fragment shader:", fragmentShader.substring(0, 500) + "...");
|
|
403
374
|
|
|
404
375
|
// Create js RawShaderMaterial (with GLSL3 for MaterialX compatibility)
|
|
405
|
-
const shaderMaterial = new
|
|
376
|
+
const shaderMaterial = new ShaderMaterial({
|
|
406
377
|
uniforms: uniforms,
|
|
407
378
|
vertexShader: vertexShader,
|
|
408
379
|
fragmentShader: fragmentShader,
|
|
@@ -466,14 +437,32 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
466
437
|
uniforms.u_frame.value = time;
|
|
467
438
|
}
|
|
468
439
|
|
|
440
|
+
// Update light uniforms
|
|
441
|
+
const environment = state.materialXEnvironment;
|
|
442
|
+
const lightData = environment.getLightData() || null;
|
|
443
|
+
const lightCount = environment.getLightCount() || 0;
|
|
444
|
+
if (uniforms.u_numActiveLightSources) {
|
|
445
|
+
uniforms.u_numActiveLightSources.value = lightCount;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (lightData && uniforms.u_lightData) {
|
|
449
|
+
uniforms.u_lightData.value = lightData;
|
|
450
|
+
if (debug) console.log("Updating light data for material", shaderMaterial.name, lightData, shaderMaterial.uniforms);
|
|
451
|
+
}
|
|
452
|
+
|
|
469
453
|
// Mark uniforms as needing update
|
|
470
454
|
shaderMaterial.uniformsNeedUpdate = true;
|
|
471
455
|
};
|
|
472
456
|
|
|
457
|
+
this.context.pre_update_callbacks.push(() => {
|
|
458
|
+
const environment = state.materialXEnvironment;
|
|
459
|
+
environment.updateLighting(false);
|
|
460
|
+
});
|
|
461
|
+
|
|
473
462
|
shaderMaterial.name = `MaterialX_Generated_${materialXData.name}`;
|
|
474
463
|
|
|
475
464
|
// Add debugging to see if the material compiles correctly
|
|
476
|
-
if (debug) console.log("MaterialX
|
|
465
|
+
if (debug) console.log("[MaterialX] material created successfully:", shaderMaterial.name);
|
|
477
466
|
// if (debug) console.log("Material uniforms keys:", Object.keys(shaderMaterial.uniforms || {}));
|
|
478
467
|
// if (debug) console.log("Material transparent:", shaderMaterial.transparent);
|
|
479
468
|
// if (debug) console.log("Material side:", shaderMaterial.side);
|
|
@@ -485,7 +474,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
485
474
|
|
|
486
475
|
} catch (error) {
|
|
487
476
|
// This is a wasm error (an int) that we need to resolve
|
|
488
|
-
console.error("Error creating MaterialX material:", error);
|
|
477
|
+
console.error("[MaterialX] Error creating MaterialX material:", error);
|
|
489
478
|
// Return a fallback material with stored MaterialX data
|
|
490
479
|
const fallbackMaterial = new MeshStandardMaterial();
|
|
491
480
|
fallbackMaterial.userData.materialX = materialXData;
|
|
@@ -499,31 +488,35 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
499
488
|
updateLightingFromEnvironment(environment: MaterialXEnvironment): void {
|
|
500
489
|
|
|
501
490
|
// Get lighting data from environment
|
|
502
|
-
// const lights = environment.getLights() || [];
|
|
503
491
|
const lightData = environment.getLightData() || null;
|
|
492
|
+
const lightCount = environment.getLightCount() || 0;
|
|
504
493
|
const radianceTexture = environment.getRadianceTexture() || null;
|
|
505
494
|
const irradianceTexture = environment.getIrradianceTexture() || null;
|
|
506
495
|
|
|
507
|
-
if (debug)
|
|
508
|
-
|
|
509
|
-
|
|
496
|
+
if (debug) {
|
|
497
|
+
console.log(`[MaterialX] Updating lighting for ${this.generatedMaterials.length} MaterialX materials`, {
|
|
498
|
+
lightData, radianceTexture, irradianceTexture,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
510
501
|
|
|
511
502
|
// Update each generated material's lighting uniforms
|
|
512
503
|
this.generatedMaterials.forEach((material, _index) => {
|
|
513
504
|
if (!material.uniforms) return;
|
|
514
505
|
|
|
506
|
+
console.warn(material.name, material.uniforms, lightCount)
|
|
507
|
+
|
|
515
508
|
// Update light count
|
|
516
|
-
if (material.uniforms.u_numActiveLightSources &&
|
|
517
|
-
material.uniforms.u_numActiveLightSources.value =
|
|
509
|
+
if (material.uniforms.u_numActiveLightSources && lightCount >= 0) {
|
|
510
|
+
material.uniforms.u_numActiveLightSources.value = lightCount;
|
|
518
511
|
}
|
|
519
512
|
|
|
520
513
|
// Update light data if we have lights
|
|
521
514
|
if (lightData) {
|
|
522
|
-
|
|
523
|
-
material.uniforms.u_lightData = { value: null };
|
|
524
|
-
}
|
|
515
|
+
material.uniforms.u_lightData ??= new Uniform(null)
|
|
525
516
|
material.uniforms.u_lightData.value = lightData;
|
|
517
|
+
if (debug) console.log("[MaterialX] Updated light data for material", material.name, lightData, material.uniforms,);
|
|
526
518
|
}
|
|
519
|
+
else if (debug) console.warn("[MaterialX] No light data available to update uniforms for material", material.name);
|
|
527
520
|
|
|
528
521
|
// Update environment uniforms
|
|
529
522
|
if (material.uniforms.u_envMatrix) {
|
|
@@ -542,14 +535,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
542
535
|
// Mark uniforms as needing update
|
|
543
536
|
// console.log("Light data in uniforms", material.uniforms, material.fragmentShader);
|
|
544
537
|
material.uniformsNeedUpdate = true;
|
|
538
|
+
console.log(material)
|
|
545
539
|
});
|
|
546
|
-
|
|
547
|
-
/* TODO not working yet
|
|
548
|
-
this.generatedMaterials.forEach((material, index) => {
|
|
549
|
-
console.log("Regenerating shaders for MaterialX material:", index, material.name);
|
|
550
|
-
material.userData.regenerateShaders();
|
|
551
|
-
resetShaders(this.context);
|
|
552
|
-
});
|
|
553
|
-
*/
|
|
554
540
|
}
|
|
555
541
|
}
|
package/src/materialx.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { Context, delay, isDevEnvironment, ObjectUtils } from "@needle-tools/engine";
|
|
1
|
+
import { Context, delay, isDevEnvironment, ObjectUtils, GameObject } from "@needle-tools/engine";
|
|
2
2
|
import MaterialX from "../bin/JsMaterialXGenShader.js";
|
|
3
3
|
import { debug } from "./utils.js";
|
|
4
4
|
import { renderPMREMToEquirect } from "./textureHelper.js";
|
|
5
|
-
import { Light, MeshBasicMaterial, Object3D, PMREMGenerator } from "three";
|
|
6
|
-
import { registerLights } from "./helper.js";
|
|
5
|
+
import { Light, MeshBasicMaterial, Object3D, PMREMGenerator, Texture } from "three";
|
|
6
|
+
import { registerLights, getLightData } from "./helper.js";
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
// Global MaterialX module instance - initialized lazily
|
|
10
9
|
export const state = new class {
|
|
11
|
-
materialXModule:
|
|
10
|
+
materialXModule: typeof MaterialX | null = null;
|
|
12
11
|
materialXGenerator: any = null;
|
|
13
12
|
materialXGenContext: any = null;
|
|
14
13
|
materialXStdLib: any = null;
|
|
@@ -22,14 +21,14 @@ export const state = new class {
|
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
export async function
|
|
24
|
+
/** Initialize the MaterialX module. Must be awaited before trying to create materials */
|
|
25
|
+
export async function ready(): Promise<void> {
|
|
27
26
|
if (state.materialXInitPromise) {
|
|
28
27
|
return state.materialXInitPromise;
|
|
29
28
|
}
|
|
30
29
|
return state.materialXInitPromise = (async () => {
|
|
31
30
|
if (state.materialXModule) return; // Already initialized
|
|
32
|
-
if (debug) console.log("Initializing
|
|
31
|
+
if (debug) console.log("[MaterialX] Initializing WASM module...");
|
|
33
32
|
try {
|
|
34
33
|
|
|
35
34
|
const urls: Array<string> = await Promise.all([
|
|
@@ -44,7 +43,7 @@ export async function initializeMaterialX(): Promise<void> {
|
|
|
44
43
|
|
|
45
44
|
const module = await MaterialX({
|
|
46
45
|
locateFile: (path: string, scriptDirectory: string) => {
|
|
47
|
-
if (debug) console.debug("MaterialX
|
|
46
|
+
if (debug) console.debug("[MaterialX] locateFile called:", { path, scriptDirectory });
|
|
48
47
|
|
|
49
48
|
if (path.includes("JsMaterialXCore.wasm")) {
|
|
50
49
|
return JsMaterialXCore; // Use the URL for the core WASM file
|
|
@@ -59,7 +58,7 @@ export async function initializeMaterialX(): Promise<void> {
|
|
|
59
58
|
return scriptDirectory + path;
|
|
60
59
|
},
|
|
61
60
|
});
|
|
62
|
-
if (debug) console.log("
|
|
61
|
+
if (debug) console.log("[MaterialX] module loaded", module);
|
|
63
62
|
state.materialXModule = module;
|
|
64
63
|
|
|
65
64
|
// Initialize shader generator and context
|
|
@@ -71,23 +70,36 @@ export async function initializeMaterialX(): Promise<void> {
|
|
|
71
70
|
state.materialXStdLib = module.loadStandardLibraries(state.materialXGenContext);
|
|
72
71
|
tempDoc.setDataLibrary(state.materialXStdLib);
|
|
73
72
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
//
|
|
73
|
+
// TODO ShaderInterfaceType.SHADER_INTERFACE_REDUCED would be better, but doesn't actually seem to be supported in the MaterialX javascript bindings
|
|
74
|
+
state.materialXGenContext.getOptions().shaderInterfaceType = state.materialXModule.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
|
|
75
|
+
|
|
76
|
+
// SPECULAR_ENVIRONMENT_NONE: Do not use specular environment maps.
|
|
77
|
+
// SPECULAR_ENVIRONMENT_FIS: Use Filtered Importance Sampling for specular environment/indirect lighting.
|
|
78
|
+
// SPECULAR_ENVIRONMENT_PREFILTER: Use pre-filtered environment maps for specular environment/indirect lighting.
|
|
79
|
+
state.materialXGenContext.getOptions().hwSpecularEnvironmentMethod = state.materialXModule.HwSpecularEnvironmentMethod.SPECULAR_ENVIRONMENT_FIS;
|
|
80
|
+
|
|
81
|
+
// TRANSMISSION_REFRACTION: Use a refraction approximation for transmission rendering.
|
|
82
|
+
// TRANSMISSION_OPACITY: Use opacity for transmission rendering.
|
|
83
|
+
// state.materialXGenContext.getOptions().hwTransmissionRenderMethod = state.materialXModule.HwTransmissionRenderMethod.TRANSMISSION_REFRACTION;
|
|
84
|
+
|
|
85
|
+
// Turned off because we're doing color space conversion the three.js way
|
|
86
|
+
state.materialXGenContext.getOptions().hwSrgbEncodeOutput = false;
|
|
87
|
+
|
|
88
|
+
// Enables the generation of a prefiltered environment map.
|
|
89
|
+
// TODO Would be great to use but requires setting more uniforms (like u_envPrefilterMip).
|
|
90
|
+
// When set to true, the u_envRadiance map is expected to be a prefiltered environment map.
|
|
91
|
+
// state.materialXGenContext.getOptions().hwWriteEnvPrefilter = true;
|
|
92
|
+
|
|
93
|
+
// Set a reasonable default for max active lights
|
|
94
|
+
state.materialXGenContext.getOptions().hwMaxActiveLightSources = 4;
|
|
84
95
|
|
|
85
96
|
// This prewarms the shader generation context to have all light types
|
|
86
|
-
await registerLights(state.materialXModule,
|
|
97
|
+
await registerLights(state.materialXModule, state.materialXGenContext);
|
|
98
|
+
// getLightData(state.materialXModule, [], state.materialXGenContext);
|
|
87
99
|
|
|
88
|
-
if (debug) console.log("MaterialX
|
|
100
|
+
if (debug) console.log("[MaterialX] generator initialized successfully");
|
|
89
101
|
} catch (error) {
|
|
90
|
-
console.error("Failed to load MaterialX module:", error);
|
|
102
|
+
console.error("[MaterialX] Failed to load MaterialX module:", error);
|
|
91
103
|
throw error;
|
|
92
104
|
}
|
|
93
105
|
})();
|
|
@@ -95,134 +107,127 @@ export async function initializeMaterialX(): Promise<void> {
|
|
|
95
107
|
|
|
96
108
|
// MaterialX Environment Manager - handles lighting and environment setup
|
|
97
109
|
export class MaterialXEnvironment {
|
|
98
|
-
private
|
|
99
|
-
private
|
|
100
|
-
private
|
|
101
|
-
private
|
|
102
|
-
private
|
|
103
|
-
private
|
|
110
|
+
private _context: Context | null = null;
|
|
111
|
+
private _lights: Array<Light> = [];
|
|
112
|
+
private _lightData: any = null;
|
|
113
|
+
private _lightCount: number = 0;
|
|
114
|
+
private _radianceTexture: Texture | null = null;
|
|
115
|
+
private _irradianceTexture: Texture | null = null;
|
|
116
|
+
private _initializePromise: Promise<boolean> | null = null;
|
|
104
117
|
|
|
105
118
|
constructor() {
|
|
106
|
-
if (debug) console.log("MaterialX Environment created");
|
|
119
|
+
if (debug) console.log("[MaterialX] Environment created");
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
// Initialize with Needle Engine context
|
|
123
|
+
async initializeFromContext(context: Context): Promise<boolean> {
|
|
124
|
+
if (this._initializePromise) {
|
|
125
|
+
return this._initializePromise;
|
|
126
|
+
}
|
|
127
|
+
return this._initializePromise = this._initialize(context);
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
async initializeLighting(lightRigXml: string, renderer?: any, radianceTexture?: any, irradianceTexture?: any): Promise<void> {
|
|
116
|
-
if (!materialXModule || !materialXGenContext) {
|
|
117
|
-
console.warn("MaterialX module not initialized, skipping lighting setup");
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
130
|
+
getLightData() { return this._lightData; }
|
|
131
|
+
getLightCount() { return this._lightCount; }
|
|
120
132
|
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
*/
|
|
133
|
+
setRadianceTexture(texture: Texture) { this._radianceTexture = texture; }
|
|
134
|
+
getRadianceTexture() { return this._radianceTexture; }
|
|
124
135
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!this.context) {
|
|
128
|
-
console.warn("No Needle context available for MaterialX environment initialization");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
136
|
+
getIrradianceTexture() { return this._irradianceTexture; }
|
|
137
|
+
setIrradianceTexture(texture: Texture) { this._irradianceTexture = texture; }
|
|
131
138
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
private _initialize: (context: Context) => Promise<boolean> = async (context: Context) => {
|
|
140
|
+
|
|
141
|
+
this._context = context;
|
|
142
|
+
|
|
143
|
+
// Get renderer from context
|
|
144
|
+
const renderer = context.renderer;
|
|
137
145
|
|
|
138
146
|
// Clean up previous textures if they exist
|
|
139
|
-
if (this.
|
|
140
|
-
if (debug) console.log("Disposing previous radiance texture");
|
|
141
|
-
this.
|
|
142
|
-
this.
|
|
147
|
+
if (this._radianceTexture) {
|
|
148
|
+
if (debug) console.log("[MaterialX] Disposing previous radiance texture");
|
|
149
|
+
this._radianceTexture.dispose();
|
|
150
|
+
this._radianceTexture = null;
|
|
143
151
|
}
|
|
144
|
-
if (this.
|
|
145
|
-
if (debug) console.log("Disposing previous irradiance texture");
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
152
|
+
if (this._irradianceTexture) {
|
|
153
|
+
if (debug) console.log("[MaterialX] Disposing previous irradiance texture");
|
|
154
|
+
this._irradianceTexture.dispose();
|
|
155
|
+
this._irradianceTexture = null;
|
|
148
156
|
}
|
|
149
157
|
|
|
150
|
-
// Get renderer from context
|
|
151
|
-
const renderer = this.context.renderer;
|
|
152
|
-
|
|
153
158
|
// TODO remove this delay; we should wait for the scene lighting to be ready
|
|
154
159
|
// and then update the uniforms
|
|
155
|
-
let envMap =
|
|
160
|
+
let envMap = context.scene.environment;
|
|
156
161
|
while (!envMap) {
|
|
157
|
-
await delay(
|
|
158
|
-
envMap =
|
|
162
|
+
await delay(1000);
|
|
163
|
+
envMap = context.scene.environment;
|
|
159
164
|
}
|
|
160
|
-
|
|
165
|
+
const pmrem = new PMREMGenerator(renderer);
|
|
161
166
|
const target = pmrem.fromEquirectangular(envMap);
|
|
162
|
-
|
|
163
167
|
const radianceRenderTarget = renderPMREMToEquirect(renderer, target.texture, 0.0, 1024, 512, target.height);
|
|
164
168
|
const irradianceRenderTarget = renderPMREMToEquirect(renderer, target.texture, 1.0, 32, 16, target.height);
|
|
165
|
-
|
|
166
|
-
this.
|
|
167
|
-
this.irradianceTexture = irradianceRenderTarget.texture;
|
|
169
|
+
this._radianceTexture = radianceRenderTarget.texture;
|
|
170
|
+
this._irradianceTexture = irradianceRenderTarget.texture;
|
|
168
171
|
|
|
169
172
|
// Clean up PMREM generator and its render target
|
|
170
173
|
target.dispose();
|
|
171
174
|
pmrem.dispose();
|
|
172
175
|
|
|
173
176
|
if (debug) {
|
|
174
|
-
console.log({ radiance: this.
|
|
177
|
+
console.log({ radiance: this._radianceTexture, irradiance: this._irradianceTexture });
|
|
175
178
|
// Show both of them on cubes in the scene
|
|
176
179
|
const unlitMat = new MeshBasicMaterial();
|
|
180
|
+
unlitMat.side = 2;
|
|
177
181
|
const radianceMat = unlitMat.clone();
|
|
178
|
-
radianceMat.map = this.
|
|
179
|
-
const radianceCube = ObjectUtils.createPrimitive("
|
|
182
|
+
radianceMat.map = this._radianceTexture;
|
|
183
|
+
const radianceCube = ObjectUtils.createPrimitive("Quad", { material: radianceMat });
|
|
180
184
|
const irradianceMat = unlitMat.clone();
|
|
181
|
-
irradianceMat.map = this.
|
|
182
|
-
const irradianceCube = ObjectUtils.createPrimitive("
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
irradianceMat.map = this._irradianceTexture;
|
|
186
|
+
const irradianceCube = ObjectUtils.createPrimitive("Quad", { material: irradianceMat });
|
|
187
|
+
context.scene.add(radianceCube);
|
|
188
|
+
context.scene.add(irradianceCube);
|
|
185
189
|
radianceCube.position.set(2, 0, 0);
|
|
186
|
-
radianceCube.scale.y = 0.00001;
|
|
187
190
|
irradianceCube.position.set(-2, 0, 0);
|
|
188
|
-
|
|
189
|
-
// await this.initializeLighting(defaultLightRigXml, renderer);
|
|
190
|
-
console.log("MaterialX environment initialized from Needle context", this, this.context.scene);
|
|
191
|
+
console.log("[MaterialX] environment initialized from Needle context", this, this._context.scene);
|
|
191
192
|
}
|
|
192
193
|
|
|
193
|
-
|
|
194
|
-
let lights = new Array<Light>();
|
|
195
|
-
this.context.scene.traverse((object: Object3D) => {
|
|
196
|
-
if ((object as Light).isLight) lights.push(object as Light);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
this.lightData = await registerLights(state.materialXModule, lights, state.materialXGenContext);
|
|
194
|
+
this.updateLighting(true);
|
|
200
195
|
|
|
201
196
|
// Mark as initialized
|
|
202
|
-
|
|
197
|
+
return true;
|
|
203
198
|
}
|
|
204
199
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
200
|
+
updateLighting(collectLights: boolean) {
|
|
201
|
+
if (!this._context) return;
|
|
202
|
+
|
|
203
|
+
// Find lights in scene
|
|
204
|
+
if (collectLights) {
|
|
205
|
+
const lights = new Array<Light>();
|
|
206
|
+
this._context.scene.traverse((object: Object3D) => {
|
|
207
|
+
if ((object as Light).isLight && GameObject.isActiveInHierarchy(object))
|
|
208
|
+
lights.push(object as Light);
|
|
209
|
+
});
|
|
210
|
+
this._lights = lights;
|
|
211
|
+
}
|
|
209
212
|
|
|
210
|
-
|
|
211
|
-
|
|
213
|
+
const { lightData, lightCount } = getLightData(state.materialXModule, this._lights, state.materialXGenContext);
|
|
214
|
+
this._lightData = lightData;
|
|
215
|
+
this._lightCount = lightCount;
|
|
216
|
+
}
|
|
212
217
|
|
|
213
218
|
// Reset the environment to allow re-initialization
|
|
214
219
|
reset() {
|
|
215
|
-
if (debug) console.log("Resetting
|
|
216
|
-
if (this.
|
|
217
|
-
this.
|
|
218
|
-
this.
|
|
220
|
+
if (debug) console.log("[MaterialX] Resetting environment");
|
|
221
|
+
if (this._radianceTexture) {
|
|
222
|
+
this._radianceTexture.dispose();
|
|
223
|
+
this._radianceTexture = null;
|
|
219
224
|
}
|
|
220
|
-
if (this.
|
|
221
|
-
this.
|
|
222
|
-
this.
|
|
225
|
+
if (this._irradianceTexture) {
|
|
226
|
+
this._irradianceTexture.dispose();
|
|
227
|
+
this._irradianceTexture = null;
|
|
223
228
|
}
|
|
224
|
-
this.
|
|
225
|
-
|
|
226
|
-
this.
|
|
229
|
+
this._initializePromise = null;
|
|
230
|
+
this._lights = [];
|
|
231
|
+
this._lightData = null;
|
|
227
232
|
}
|
|
228
233
|
}
|