@needle-tools/materialx 1.0.1-next.31390e3 → 1.0.1-next.7bd39cb

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/src/materialx.ts CHANGED
@@ -1,14 +1,15 @@
1
- import { Context, delay, isDevEnvironment, ObjectUtils, GameObject } from "@needle-tools/engine";
1
+ import { Context, delay, isDevEnvironment, ObjectUtils, GameObject, onBeforeRender } from "@needle-tools/engine";
2
+ import type { MaterialX as MX } from "./materialx.types.js";
2
3
  import MaterialX from "../bin/JsMaterialXGenShader.js";
3
4
  import { debug } from "./utils.js";
4
5
  import { renderPMREMToEquirect } from "./textureHelper.js";
5
- import { Light, MeshBasicMaterial, Object3D, PMREMGenerator } from "three";
6
- import { registerLights } from "./helper.js";
6
+ import { Light, Material, MeshBasicMaterial, Object3D, PMREMGenerator, Texture } from "three";
7
+ import { registerLights, getLightData } from "./materialx.helper.js";
8
+ import type { MaterialXMaterial } from "./materialx.material.js";
7
9
 
8
10
 
9
- // Global MaterialX module instance - initialized lazily
10
11
  export const state = new class {
11
- materialXModule: typeof MaterialX | null = null;
12
+ materialXModule: MX.MODULE | null = null;
12
13
  materialXGenerator: any = null;
13
14
  materialXGenContext: any = null;
14
15
  materialXStdLib: any = null;
@@ -22,7 +23,7 @@ export const state = new class {
22
23
  }
23
24
  }
24
25
 
25
- // Initialize MaterialX WASM module lazily
26
+ /** Initialize the MaterialX module. Must be awaited before trying to create materials */
26
27
  export async function ready(): Promise<void> {
27
28
  if (state.materialXInitPromise) {
28
29
  return state.materialXInitPromise;
@@ -60,7 +61,7 @@ export async function ready(): Promise<void> {
60
61
  },
61
62
  });
62
63
  if (debug) console.log("[MaterialX] module loaded", module);
63
- state.materialXModule = module;
64
+ state.materialXModule = module as MX.MODULE
64
65
 
65
66
  // Initialize shader generator and context
66
67
  state.materialXGenerator = module.EsslShaderGenerator.create();
@@ -72,7 +73,6 @@ export async function ready(): Promise<void> {
72
73
  tempDoc.setDataLibrary(state.materialXStdLib);
73
74
 
74
75
  // TODO ShaderInterfaceType.SHADER_INTERFACE_REDUCED would be better, but doesn't actually seem to be supported in the MaterialX javascript bindings
75
- const options = state.materialXGenContext.getOptions();
76
76
  state.materialXGenContext.getOptions().shaderInterfaceType = state.materialXModule.ShaderInterfaceType.SHADER_INTERFACE_COMPLETE;
77
77
 
78
78
  // SPECULAR_ENVIRONMENT_NONE: Do not use specular environment maps.
@@ -96,7 +96,7 @@ export async function ready(): Promise<void> {
96
96
  state.materialXGenContext.getOptions().hwMaxActiveLightSources = 4;
97
97
 
98
98
  // This prewarms the shader generation context to have all light types
99
- await registerLights(state.materialXModule, [], state.materialXGenContext);
99
+ await registerLights(state.materialXModule, state.materialXGenContext);
100
100
 
101
101
  if (debug) console.log("[MaterialX] generator initialized successfully");
102
102
  } catch (error) {
@@ -106,123 +106,156 @@ export async function ready(): Promise<void> {
106
106
  })();
107
107
  }
108
108
 
109
+ type EnvironmentTextureSet = {
110
+ radianceTexture: Texture | null;
111
+ irradianceTexture: Texture | null;
112
+ }
113
+
109
114
  // MaterialX Environment Manager - handles lighting and environment setup
110
115
  export class MaterialXEnvironment {
111
- context: Context | null = null;
116
+ private _context: Context | null = null;
117
+ private _lights: Array<Light> = [];
118
+ private _lightData: any = null;
119
+ private _lightCount: number = 0;
120
+ private _initializePromise: Promise<boolean> | null = null;
112
121
 
113
- private lightData: any = null;
114
- private lightCount: number = 0;
115
- private radianceTexture: any = null;
116
- private irradianceTexture: any = null;
117
- private initialized: boolean = false;
122
+ private _unsubscribehook: (() => void) | null = null;
118
123
 
119
124
  constructor() {
120
125
  if (debug) console.log("[MaterialX] Environment created");
121
126
  }
122
127
 
123
128
  // Initialize with Needle Engine context
124
- async initializeFromContext(): Promise<void> {
125
- if (!this.context) {
126
- console.warn("[MaterialX] No Needle context available for environment initialization");
127
- return;
129
+ async initialize(context: Context): Promise<boolean> {
130
+ if (this._initializePromise) {
131
+ return this._initializePromise;
128
132
  }
133
+ return this._initializePromise = this._initialize(context);
134
+ }
129
135
 
130
- // Prevent multiple initializations
131
- if (this.initialized) {
132
- if (debug) console.log("[MaterialX] environment already initialized, skipping");
133
- return;
136
+ get lightData() { return this._lightData; }
137
+ get lightCount() { return this._lightCount || 0; }
138
+ getTextures(material: MaterialXMaterial) {
139
+ if (material.envMap) {
140
+ // If the material has its own envMap, we don't use the irradiance texture
141
+ return this._getTextures(material.envMap);
134
142
  }
143
+ return this._getTextures(this._context?.scene.environment);
144
+ }
135
145
 
136
- // Clean up previous textures if they exist
137
- if (this.radianceTexture) {
138
- if (debug) console.log("[MaterialX] Disposing previous radiance texture");
139
- this.radianceTexture.dispose();
140
- this.radianceTexture = null;
141
- }
142
- if (this.irradianceTexture) {
143
- if (debug) console.log("[MaterialX] Disposing previous irradiance texture");
144
- this.irradianceTexture.dispose();
145
- this.irradianceTexture = null;
146
- }
146
+ private _pmremGenerator: PMREMGenerator | null = null;
147
+ private readonly _texturesCache: Map<Texture | null, EnvironmentTextureSet> = new Map();
148
+
149
+ private _initialize: (context: Context) => Promise<boolean> = async (context: Context) => {
150
+
151
+ this._context = context;
152
+ this._pmremGenerator = new PMREMGenerator(context.renderer);
147
153
 
148
- // Get renderer from context
149
- const renderer = this.context.renderer;
154
+ this._unsubscribehook?.();
155
+ this._unsubscribehook = onBeforeRender(() => {
156
+ this.updateLighting(false);
157
+ this._getTextures(context.scene.environment);
158
+ })
150
159
 
151
160
  // TODO remove this delay; we should wait for the scene lighting to be ready
152
161
  // and then update the uniforms
153
- let envMap = this.context.scene.environment;
154
- while (!envMap) {
155
- await delay(200);
156
- envMap = this.context.scene.environment;
162
+ while (!context.scene.environment) {
163
+ await delay(5);
157
164
  }
158
- var pmrem = new PMREMGenerator(renderer);
159
- const target = pmrem.fromEquirectangular(envMap);
160
-
161
- const radianceRenderTarget = renderPMREMToEquirect(renderer, target.texture, 0.0, 1024, 512, target.height);
162
- const irradianceRenderTarget = renderPMREMToEquirect(renderer, target.texture, 1.0, 32, 16, target.height);
163
-
164
- this.radianceTexture = radianceRenderTarget.texture;
165
- this.irradianceTexture = irradianceRenderTarget.texture;
166
-
167
- // Clean up PMREM generator and its render target
168
- target.dispose();
169
- pmrem.dispose();
170
-
171
- if (debug) {
172
- console.log({ radiance: this.radianceTexture, irradiance: this.irradianceTexture });
173
- // Show both of them on cubes in the scene
174
- const unlitMat = new MeshBasicMaterial();
175
- unlitMat.side = 2;
176
- const radianceMat = unlitMat.clone();
177
- radianceMat.map = this.radianceTexture;
178
- const radianceCube = ObjectUtils.createPrimitive("Quad", { material: radianceMat });
179
- const irradianceMat = unlitMat.clone();
180
- irradianceMat.map = this.irradianceTexture;
181
- const irradianceCube = ObjectUtils.createPrimitive("Quad", { material: irradianceMat });
182
- this.context.scene.add(radianceCube);
183
- this.context.scene.add(irradianceCube);
184
- radianceCube.position.set(2, 0, 0);
185
- irradianceCube.position.set(-2, 0, 0);
186
- // await this.initializeLighting(defaultLightRigXml, renderer);
187
- console.log("[MaterialX] environment initialized from Needle context", this, this.context.scene);
188
- }
189
-
190
- // Find lights in scene
191
- let lights = new Array<Light>();
192
- this.context.scene.traverse((object: Object3D) => {
193
- if ((object as Light).isLight && GameObject.isActiveInHierarchy(object))
194
- lights.push(object as Light);
195
- });
196
-
197
- const { lightData, lightCount } = await registerLights(state.materialXModule, lights, state.materialXGenContext);
198
- this.lightData = lightData;
199
- this.lightCount = lightCount;
165
+ this._getTextures(context.scene.environment);
166
+
167
+ // if (debug) {
168
+ // console.log({ radiance: this._radianceTexture, irradiance: this._irradianceTexture });
169
+ // // Show both of them on cubes in the scene
170
+ // const unlitMat = new MeshBasicMaterial();
171
+ // unlitMat.side = 2;
172
+ // const radianceMat = unlitMat.clone();
173
+ // radianceMat.map = this._radianceTexture;
174
+ // const radianceCube = ObjectUtils.createPrimitive("Quad", { material: radianceMat });
175
+ // const irradianceMat = unlitMat.clone();
176
+ // irradianceMat.map = this._irradianceTexture;
177
+ // const irradianceCube = ObjectUtils.createPrimitive("Quad", { material: irradianceMat });
178
+ // context.scene.add(radianceCube);
179
+ // context.scene.add(irradianceCube);
180
+ // radianceCube.position.set(2, 0, 0);
181
+ // irradianceCube.position.set(-2, 0, 0);
182
+ // console.log("[MaterialX] environment initialized from Needle context", this, this._context.scene);
183
+ // }
184
+
185
+ this.updateLighting(true);
200
186
 
201
187
  // Mark as initialized
202
- this.initialized = true;
188
+ return true;
203
189
  }
204
190
 
205
- getLightData() { return this.lightData; }
206
- getLightCount() { return this.lightCount; }
207
- getRadianceTexture() { return this.radianceTexture; }
208
- getIrradianceTexture() { return this.irradianceTexture; }
209
-
210
- setRadianceTexture(texture: any) { this.radianceTexture = texture; }
211
- setIrradianceTexture(texture: any) { this.irradianceTexture = texture; }
212
-
213
191
  // Reset the environment to allow re-initialization
214
192
  reset() {
215
193
  if (debug) console.log("[MaterialX] Resetting environment");
216
- if (this.radianceTexture) {
217
- this.radianceTexture.dispose();
218
- this.radianceTexture = null;
194
+ this._initializePromise = null;
195
+ this._lights = [];
196
+ this._lightData = null;
197
+ this._lightCount = 0;
198
+ this._pmremGenerator?.dispose();
199
+ this._pmremGenerator = null;
200
+ for(const textureSet of this._texturesCache.values()) {
201
+ textureSet.radianceTexture?.dispose();
202
+ textureSet.irradianceTexture?.dispose();
219
203
  }
220
- if (this.irradianceTexture) {
221
- this.irradianceTexture.dispose();
222
- this.irradianceTexture = null;
204
+ this._texturesCache.clear();
205
+
206
+ this._unsubscribehook?.();
207
+ this._unsubscribehook = null;
208
+ }
209
+
210
+ private _getTextures(texture: Texture | null | undefined): {
211
+ radianceTexture: Texture | null,
212
+ irradianceTexture: Texture | null
213
+ } {
214
+
215
+ let res: EnvironmentTextureSet | undefined = this._texturesCache.get(texture || null);
216
+ if (res) {
217
+ return res;
218
+ }
219
+
220
+ if (this._context && this._pmremGenerator && texture) {
221
+ if (debug) console.log("[MaterialX] Generating environment textures", texture.name);
222
+
223
+ const target = this._pmremGenerator.fromEquirectangular(texture);
224
+ const radianceRenderTarget = renderPMREMToEquirect(this._context.renderer, target.texture, 0.0, 1024, 512, target.height);
225
+ const irradianceRenderTarget = renderPMREMToEquirect(this._context.renderer, target.texture, 1.0, 32, 16, target.height);
226
+ target.dispose();
227
+ res = {
228
+ radianceTexture: radianceRenderTarget.texture,
229
+ irradianceTexture: irradianceRenderTarget.texture
230
+ }
231
+ }
232
+ else {
233
+ res = {
234
+ radianceTexture: null,
235
+ irradianceTexture: null
236
+ }
237
+ }
238
+ this._texturesCache.set(texture || null, res);
239
+ return res;
240
+ }
241
+
242
+ private updateLighting = (collectLights: boolean = false) => {
243
+ if (!this._context) return;
244
+
245
+ // Find lights in scene
246
+ if (collectLights) {
247
+ const lights = new Array<Light>();
248
+ this._context.scene.traverse((object: Object3D) => {
249
+ if ((object as Light).isLight && GameObject.isActiveInHierarchy(object))
250
+ lights.push(object as Light);
251
+ });
252
+ this._lights = lights;
253
+ }
254
+
255
+ if (state.materialXGenContext) {
256
+ const { lightData, lightCount } = getLightData(this._lights, state.materialXGenContext);
257
+ this._lightData = lightData;
258
+ this._lightCount = lightCount;
223
259
  }
224
- this.initialized = false;
225
- // this.lights = [];
226
- this.lightData = null;
227
260
  }
228
261
  }
@@ -0,0 +1,50 @@
1
+
2
+
3
+
4
+ export namespace MaterialX {
5
+
6
+ export type MODULE = {
7
+ ShaderInterfaceType: any;
8
+ HwSpecularEnvironmentMethod: any;
9
+ HwShaderGenerator: {
10
+ bindLightShader(def: any, id: number, genContext: GenContext): void;
11
+ unbindLightShaders(context: any): void;
12
+ };
13
+ createDocument(): Document;
14
+ readFromXmlString(doc: Document, xml: string, unknown: string): void;
15
+ loadStandardLibraries(genContext: GenContext): StandardLibrary;
16
+ isTransparentSurface(renderableElement: any, target: string): boolean;
17
+ }
18
+
19
+
20
+ export type GenContext = {
21
+ }
22
+
23
+ export type StandardLibrary = {
24
+
25
+ }
26
+
27
+ // https://github.com/AcademySoftwareFoundation/MaterialX/blob/b74787db6544283dc32afc8085ebc93cabe937cb/source/MaterialXGenShader/ShaderStage.h#L56
28
+ export type ShaderStage = {
29
+ getUniformBlocks(): Record<string, any>;
30
+ }
31
+
32
+ export type Document = {
33
+ setDataLibrary(lib: StandardLibrary): void;
34
+ importLibrary(lib: Document): void;
35
+
36
+ getNodes(): Node[];
37
+ }
38
+
39
+ export type Node = {
40
+ getType(): string;
41
+ }
42
+
43
+ export type Matrix = {
44
+ numRows(): number;
45
+ numColumns(): number;
46
+ get size(): number;
47
+ getItem(row: number, col: number): number;
48
+ }
49
+
50
+ }
@@ -40,7 +40,7 @@ export function renderPMREMToEquirect(renderer: WebGLRenderer, pmremTexture: Tex
40
40
  } else {
41
41
  imageHeight = 256; // Final fallback
42
42
  }
43
-
43
+
44
44
  const maxMip = Math.log2(imageHeight) - 2;
45
45
  const cubeUVHeight = imageHeight;
46
46
  const cubeUVWidth = 3 * Math.max(Math.pow(2, maxMip), 7 * 16);
@@ -129,14 +129,14 @@ export function renderPMREMToEquirect(renderer: WebGLRenderer, pmremTexture: Tex
129
129
  const currentAutoClear = renderer.autoClear;
130
130
  const currentXrEnabled = renderer.xr.enabled;
131
131
  const currentShadowMapEnabled = renderer.shadowMap.enabled;
132
-
132
+
133
133
  renderTarget.texture.generateMipmaps = true;
134
-
134
+
135
135
  try {
136
136
  // Disable XR and shadow mapping during our render to avoid interference
137
137
  renderer.xr.enabled = false;
138
138
  renderer.shadowMap.enabled = false;
139
-
139
+
140
140
  // Render to our target
141
141
  renderer.autoClear = true;
142
142
  renderer.setRenderTarget(renderTarget);
@@ -148,7 +148,7 @@ export function renderPMREMToEquirect(renderer: WebGLRenderer, pmremTexture: Tex
148
148
  renderer.autoClear = currentAutoClear;
149
149
  renderer.xr.enabled = currentXrEnabled;
150
150
  renderer.shadowMap.enabled = currentShadowMapEnabled;
151
-
151
+
152
152
  // Clean up temporary objects
153
153
  geometry.dispose();
154
154
  material.dispose();
@@ -159,7 +159,7 @@ export function renderPMREMToEquirect(renderer: WebGLRenderer, pmremTexture: Tex
159
159
  renderTarget.texture.mapping = EquirectangularReflectionMapping;
160
160
 
161
161
  // Log mipmap infos
162
- if (debug) console.log('PMREM to Equirect Render Target:', {
162
+ if (debug) console.log('[MaterialX] PMREM to Equirect Render Target:', {
163
163
  width: renderTarget.width,
164
164
  height: renderTarget.height,
165
165
  mipmaps: renderTarget.texture.mipmaps?.length,
package/src/utils.ts CHANGED
@@ -2,10 +2,7 @@ import { Context, getParam } from "@needle-tools/engine";
2
2
  import { Mesh } from "three";
3
3
 
4
4
  export const debug = getParam("debugmaterialx");
5
-
6
-
7
-
8
-
5
+ export const debugUpdate = getParam("debugmaterialxupdate");
9
6
 
10
7
  /**
11
8
  * =====================================
@@ -33,6 +30,44 @@ const patchWebGL2 = () => {
33
30
  }
34
31
  return uniform4fv.call(this, location, v);
35
32
  };
33
+
34
+ const uniform3fv = WebGL2RenderingContext.prototype.uniform3fv;
35
+ WebGL2RenderingContext.prototype.uniform3fv = function (location: WebGLUniformLocation | null, v: Float32Array | number[]) {
36
+ if (location) {
37
+ const uniformName = programAndNameToUniformLocation.get(location);
38
+ if (true) console.log("Calling uniform3fv", { location, v, name: uniformName?.name });
39
+ }
40
+ return uniform3fv.call(this, location, v);
41
+ };
42
+
43
+ const uniform3iv = WebGL2RenderingContext.prototype.uniform3iv;
44
+ WebGL2RenderingContext.prototype.uniform3iv = function (location: WebGLUniformLocation | null, v: Int32Array | number[]) {
45
+ if (location) {
46
+ const uniformName = programAndNameToUniformLocation.get(location);
47
+ if (true) console.log("Calling uniform3iv", { location, v, name: uniformName?.name });
48
+ }
49
+ return uniform3iv.call(this, location, v);
50
+ };
51
+
52
+ const uniform3uiv = WebGL2RenderingContext.prototype.uniform3uiv;
53
+ WebGL2RenderingContext.prototype.uniform3uiv = function (location: WebGLUniformLocation | null, v: Uint32Array | number[]) {
54
+ if (location) {
55
+ const uniformName = programAndNameToUniformLocation.get(location);
56
+ if (true) console.log("Calling uniform3uiv", { location, v, name: uniformName?.name });
57
+ }
58
+ return uniform3uiv.call(this, location, v);
59
+ };
60
+
61
+ const uniform3f = WebGL2RenderingContext.prototype.uniform3f;
62
+ WebGL2RenderingContext.prototype.uniform3f = function (location: WebGLUniformLocation
63
+ | null, x: number, y: number, z: number) {
64
+ if (location) {
65
+ const uniformName = programAndNameToUniformLocation.get(location);
66
+ if (uniformName?.name !== "diffuse")
67
+ if (true) console.log("Calling uniform3f", { location, x, y, z, name: uniformName?.name });
68
+ }
69
+ return uniform3f.call(this, location, x, y, z);
70
+ };
36
71
  };
37
72
  // patchWebGL2();
38
73