@needle-tools/materialx 1.6.0 → 1.7.0-next.0d06218

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,21 +2,44 @@
2
2
 
3
3
 
4
4
  export namespace MaterialX {
5
+ export type Collection<T> = T[] | {
6
+ size(): number;
7
+ get(index: number): T | null | undefined;
8
+ };
9
+
10
+ export type AttributeElement = {
11
+ getName?(): string;
12
+ getType?(): string;
13
+ getCategory?(): string;
14
+ getNodeString?(): string;
15
+ getNodeGroup?(): string;
16
+ getValueString?(): string;
17
+ getNodeName?(): string;
18
+ getInterfaceName?(): string;
19
+ getAttribute?(name: string): string | null | undefined;
20
+ hasAttribute?(name: string): boolean;
21
+ getAttributeNames?(): Iterable<string>;
22
+ };
23
+
24
+ export type VariableBlock = Collection<{
25
+ getPath?(): string;
26
+ getVariable?(): string;
27
+ }>;
5
28
 
6
29
  export type MODULE = {
7
- ShaderInterfaceType: any;
8
- HwSpecularEnvironmentMethod: any;
30
+ ShaderInterfaceType: Record<string, number>;
31
+ HwSpecularEnvironmentMethod: Record<string, number>;
9
32
  HwShaderGenerator: {
10
- bindLightShader(def: any, id: number, genContext: GenContext): void;
11
- unbindLightShaders(context: any): void;
33
+ bindLightShader(def: NodeDef, id: number, genContext: GenContext): void;
34
+ unbindLightShaders(context: GenContext): void;
12
35
  };
13
36
  createDocument(): Document;
14
37
  readFromXmlString(doc: Document, xml: string, searchPath?: string): void;
15
38
  loadStandardLibraries(genContext: GenContext): StandardLibrary;
16
- isTransparentSurface(renderableElement: any, target: string): boolean;
39
+ isTransparentSurface(renderableElement: Node, target: string): boolean;
17
40
  /** Returns the alpha mode for a renderable element: "opaque", "mask", or "blend".
18
41
  * Inspects the shader node (and its nodegraph implementation) for alpha_mode inputs. */
19
- getAlphaMode?(renderableElement: any, target: string): string;
42
+ getAlphaMode?(renderableElement: Node, target: string): string;
20
43
  /** Extracts a detailed error message from a WASM exception pointer, including error logs. */
21
44
  getExceptionDetailedMessage?(exceptionPtr: number): string;
22
45
  /** Extracts a basic error message from a WASM exception pointer. */
@@ -28,12 +51,13 @@ export namespace MaterialX {
28
51
  }
29
52
 
30
53
  export type StandardLibrary = {
31
-
54
+ getNodeDefs?(): Collection<NodeDef>;
55
+ getNodeGraphs?(): Collection<NodeGraph>;
32
56
  }
33
57
 
34
58
  // https://github.com/AcademySoftwareFoundation/MaterialX/blob/b74787db6544283dc32afc8085ebc93cabe937cb/source/MaterialXGenShader/ShaderStage.h#L56
35
59
  export type ShaderStage = {
36
- getUniformBlocks(): Record<string, any>;
60
+ getUniformBlocks(): Record<string, VariableBlock>;
37
61
  }
38
62
 
39
63
  export type Document = {
@@ -43,10 +67,35 @@ export namespace MaterialX {
43
67
  validate?(): { valid: boolean; message: string };
44
68
 
45
69
  getNodes(): Node[];
70
+ getMaterialNodes(): MaterialXNode[];
71
+ getNodeGraphs(): NodeGraph[];
46
72
  }
47
73
 
48
- export type Node = {
74
+ export type Input = AttributeElement;
75
+
76
+ export type Output = AttributeElement;
77
+
78
+ export type Node = AttributeElement & {
79
+ getName(): string;
49
80
  getType(): string;
81
+ getNodeDef?(): NodeDef | null;
82
+ getNodeDefString?(): string;
83
+ getInputs?(): Collection<Input>;
84
+ getOutputs?(): Collection<Output>;
85
+ }
86
+
87
+ export type NodeDef = AttributeElement & {
88
+ getName(): string;
89
+ getNodeString?(): string;
90
+ getActiveInputs?(): Collection<Input>;
91
+ getActiveOutputs?(): Collection<Output>;
92
+ }
93
+
94
+ export type NodeGraph = AttributeElement & {
95
+ getName(): string;
96
+ getNodes(): Node[];
97
+ getOutputs?(): Collection<Output>;
98
+ getNodeDefString?(): string;
50
99
  }
51
100
 
52
101
  export type Matrix = {
@@ -56,4 +105,10 @@ export namespace MaterialX {
56
105
  getItem(row: number, col: number): number;
57
106
  }
58
107
 
59
- }
108
+ export type MaterialXNode = Node & {
109
+ getNamePath?: () => string;
110
+ }
111
+
112
+ export type MaterilaXNode = MaterialXNode;
113
+
114
+ }
@@ -13,3 +13,14 @@ export function renderPMREMToEquirect(
13
13
  height?: number,
14
14
  renderTargetHeight?: number
15
15
  ): WebGLRenderTarget;
16
+
17
+ /**
18
+ * Renders a PMREM environment map to an equirectangular texture with roughness encoded in mip levels.
19
+ */
20
+ export function renderPMREMToPrefilteredEquirect(
21
+ renderer: WebGLRenderer,
22
+ pmremTexture: Texture,
23
+ width?: number,
24
+ height?: number,
25
+ renderTargetHeight?: number
26
+ ): WebGLRenderTarget;
@@ -1,12 +1,96 @@
1
- import { WebGLRenderer, Scene, WebGLRenderTarget, PlaneGeometry, OrthographicCamera, ShaderMaterial, RGBAFormat, FloatType, LinearFilter, Mesh, EquirectangularReflectionMapping, RepeatWrapping, LinearMipMapLinearFilter, Texture } from 'three';
1
+ import { WebGLRenderer, Scene, WebGLRenderTarget, PlaneGeometry, OrthographicCamera, ShaderMaterial, RGBAFormat, FloatType, LinearFilter, Mesh, EquirectangularReflectionMapping, RepeatWrapping, LinearMipMapLinearFilter, DataTexture, UnsignedByteType, Vector4 } from 'three';
2
2
  import { getParam } from './utils.js';
3
3
 
4
4
  const debug = getParam("debugmaterialx");
5
+ const _viewport = new Vector4();
5
6
 
6
- export const whiteTexture = new Texture();
7
+ export const whiteTexture = new DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1, RGBAFormat, UnsignedByteType);
7
8
  whiteTexture.needsUpdate = true;
8
- whiteTexture.image = new Image();
9
- whiteTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg=="
9
+
10
+ function toGlslFloat(value) {
11
+ if (!Number.isFinite(value)) return "0.0";
12
+ const result = Number(value).toPrecision(12).replace(/\.?0+($|e)/, "$1");
13
+ return result.includes(".") || result.includes("e") ? result : result + ".0";
14
+ }
15
+
16
+ function getPMREMCubeUVSize(pmremTexture, renderTargetHeight) {
17
+ renderTargetHeight ??= pmremTexture?.userData?.pmremRenderTargetHeight;
18
+ if (Number.isFinite(renderTargetHeight) && renderTargetHeight > 0) {
19
+ const imageHeight = renderTargetHeight;
20
+ const maxMip = Math.max(0, Math.log2(imageHeight) - 2);
21
+ return {
22
+ imageHeight,
23
+ maxMip,
24
+ texelWidth: 1.0 / (3 * Math.max(Math.pow(2, maxMip), 7 * 16)),
25
+ texelHeight: 1.0 / imageHeight,
26
+ };
27
+ }
28
+ const imageHeight = pmremTexture.image?.height;
29
+ const resolvedImageHeight = Number.isFinite(imageHeight) && imageHeight > 0 ? imageHeight : 256;
30
+ const maxMip = Math.max(0, Math.log2(resolvedImageHeight) - 2);
31
+ return {
32
+ imageHeight: resolvedImageHeight,
33
+ maxMip,
34
+ texelWidth: 1.0 / (3 * Math.max(Math.pow(2, maxMip), 7 * 16)),
35
+ texelHeight: 1.0 / resolvedImageHeight,
36
+ };
37
+ }
38
+
39
+ function createPrefilteredEquirectMaterial(pmremTexture, cubeUVSize) {
40
+ return new ShaderMaterial({
41
+ defines: {
42
+ USE_ENVMAP: '',
43
+ ENVMAP_TYPE_CUBE_UV: '',
44
+ CUBEUV_TEXEL_WIDTH: toGlslFloat(cubeUVSize.texelWidth),
45
+ CUBEUV_TEXEL_HEIGHT: toGlslFloat(cubeUVSize.texelHeight),
46
+ CUBEUV_MAX_MIP: toGlslFloat(cubeUVSize.maxMip),
47
+ },
48
+ uniforms: {
49
+ envMap: { value: pmremTexture },
50
+ roughness: { value: 0.0 },
51
+ },
52
+ vertexShader: `
53
+ varying vec2 vUv;
54
+
55
+ void main() {
56
+ vUv = uv;
57
+ gl_Position = vec4(position.xy, 0.0, 1.0);
58
+ }
59
+ `,
60
+ fragmentShader: `
61
+ uniform sampler2D envMap;
62
+ uniform float roughness;
63
+ varying vec2 vUv;
64
+
65
+ #include <common>
66
+ #include <cube_uv_reflection_fragment>
67
+
68
+ vec3 materialXLatlongDirection(vec2 uv) {
69
+ float longitude = (uv.x - 0.5) * (2.0 * PI);
70
+ float latitude = (uv.y - 0.5) * PI;
71
+ float cosLatitude = cos(latitude);
72
+
73
+ return vec3(
74
+ cosLatitude * sin(longitude),
75
+ -sin(latitude),
76
+ -cosLatitude * cos(longitude)
77
+ );
78
+ }
79
+
80
+ void main() {
81
+ vec3 direction = materialXLatlongDirection(vUv);
82
+
83
+ #ifdef ENVMAP_TYPE_CUBE_UV
84
+ vec4 envColor = textureCubeUV(envMap, direction, roughness);
85
+ #else
86
+ vec4 envColor = vec4(1.0, 0.0, 1.0, 1.0);
87
+ #endif
88
+
89
+ gl_FragColor = vec4(envColor.rgb, 1.0);
90
+ }
91
+ `
92
+ });
93
+ }
10
94
 
11
95
 
12
96
 
@@ -37,21 +121,7 @@ export function renderPMREMToEquirect(renderer, pmremTexture, roughness = 0.0, w
37
121
  // TODO Validate inputs
38
122
  // console.log(renderer, pmremTexture);
39
123
 
40
- // Calculate PMREM parameters
41
- // For PMREM CubeUV layout, we need the cube face size to calculate proper parameters
42
- // Use renderTargetHeight if provided, otherwise try to derive from texture
43
- let imageHeight;
44
- if (renderTargetHeight) {
45
- imageHeight = renderTargetHeight;
46
- } else if (pmremTexture.image) {
47
- imageHeight = pmremTexture.image.height / 4; // Fallback: assume CubeUV layout height / 4
48
- } else {
49
- imageHeight = 256; // Final fallback
50
- }
51
-
52
- const maxMip = Math.log2(imageHeight) - 2;
53
- const cubeUVHeight = imageHeight;
54
- const cubeUVWidth = 3 * Math.max(Math.pow(2, maxMip), 7 * 16);
124
+ const cubeUVSize = getPMREMCubeUVSize(pmremTexture, renderTargetHeight);
55
125
 
56
126
  // Create render target for equirectangular output
57
127
  const renderTarget = new WebGLRenderTarget(width, height, {
@@ -63,7 +133,6 @@ export function renderPMREMToEquirect(renderer, pmremTexture, roughness = 0.0, w
63
133
  wrapS: RepeatWrapping,
64
134
  anisotropy: renderer.capabilities.getMaxAnisotropy(),
65
135
  });
66
-
67
136
  // Create fullscreen quad geometry and camera
68
137
  const geometry = new PlaneGeometry(2, 2);
69
138
  const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
@@ -73,9 +142,9 @@ export function renderPMREMToEquirect(renderer, pmremTexture, roughness = 0.0, w
73
142
  defines: {
74
143
  USE_ENVMAP: '',
75
144
  ENVMAP_TYPE_CUBE_UV: '',
76
- CUBEUV_TEXEL_WIDTH: 1.0 / cubeUVWidth,
77
- CUBEUV_TEXEL_HEIGHT: 1.0 / cubeUVHeight,
78
- CUBEUV_MAX_MIP: (maxMip + 0) + '.0',
145
+ CUBEUV_TEXEL_WIDTH: toGlslFloat(cubeUVSize.texelWidth),
146
+ CUBEUV_TEXEL_HEIGHT: toGlslFloat(cubeUVSize.texelHeight),
147
+ CUBEUV_MAX_MIP: toGlslFloat(cubeUVSize.maxMip),
79
148
  },
80
149
  uniforms: {
81
150
  envMap: { value: pmremTexture },
@@ -98,21 +167,17 @@ export function renderPMREMToEquirect(renderer, pmremTexture, roughness = 0.0, w
98
167
  #include <cube_uv_reflection_fragment>
99
168
 
100
169
  void main() {
101
- // Convert UV coordinates to equirectangular direction
102
- vec2 uv = vUv;
103
-
104
- // Map UV (0,1) to spherical coordinates
105
- // Longitude: to π, Latitude: 0 to π
106
- float phi = uv.x * 2.0 * PI - PI; // Longitude (-π to π)
107
- float theta = uv.y * PI; // Latitude (0 to π)
108
- // Rotate 90° around Y
109
- phi -= PI / 2.0; // Adjust to match Three.js convention
110
-
111
- // Convert spherical to cartesian coordinates
170
+ // Use the inverse of MaterialX's mx_latlong_projection().
171
+ // MaterialX samples u_envRadiance through mx_latlong_map_lookup,
172
+ // so this conversion must write the same latlong convention.
173
+ float longitude = (vUv.x - 0.5) * (2.0 * PI);
174
+ float latitude = (vUv.y - 0.5) * PI;
175
+ float cosLatitude = cos(latitude);
176
+
112
177
  vec3 direction = vec3(
113
- sin(theta) * cos(phi), // x
114
- cos(theta), // y
115
- sin(theta) * sin(phi) // z
178
+ cosLatitude * sin(longitude),
179
+ -sin(latitude),
180
+ -cosLatitude * cos(longitude)
116
181
  );
117
182
 
118
183
  // Sample the PMREM cube texture using the direction and roughness
@@ -137,6 +202,7 @@ export function renderPMREMToEquirect(renderer, pmremTexture, roughness = 0.0, w
137
202
  const currentAutoClear = renderer.autoClear;
138
203
  const currentXrEnabled = renderer.xr.enabled;
139
204
  const currentShadowMapEnabled = renderer.shadowMap.enabled;
205
+ const currentViewport = renderer.getViewport(_viewport);
140
206
 
141
207
  renderTarget.texture.generateMipmaps = true;
142
208
 
@@ -147,12 +213,15 @@ export function renderPMREMToEquirect(renderer, pmremTexture, roughness = 0.0, w
147
213
 
148
214
  // Render to our target
149
215
  renderer.autoClear = true;
216
+ renderTarget.viewport.set(0, 0, width, height);
217
+ renderTarget.scissor.set(0, 0, width, height);
150
218
  renderer.setRenderTarget(renderTarget);
151
219
  renderer.clear(); // Explicitly clear the render target
152
220
  renderer.render(tempScene, camera);
153
221
  } finally {
154
222
  // Restore renderer state completely
155
223
  renderer.setRenderTarget(currentRenderTarget);
224
+ renderer.setViewport(currentViewport);
156
225
  renderer.autoClear = currentAutoClear;
157
226
  renderer.xr.enabled = currentXrEnabled;
158
227
  renderer.shadowMap.enabled = currentShadowMapEnabled;
@@ -176,3 +245,91 @@ export function renderPMREMToEquirect(renderer, pmremTexture, roughness = 0.0, w
176
245
 
177
246
  return renderTarget;
178
247
  }
248
+
249
+ /**
250
+ * Renders a Three.js PMREM CubeUV texture to an equirectangular texture whose mip
251
+ * levels encode increasing roughness for MaterialX SPECULAR_ENVIRONMENT_PREFILTER.
252
+ * @param {WebGLRenderer} renderer
253
+ * @param {Texture} pmremTexture
254
+ * @param {number} [width=2048]
255
+ * @param {number} [height=1024]
256
+ * @param {number} [renderTargetHeight]
257
+ * @returns {WebGLRenderTarget}
258
+ */
259
+ export function renderPMREMToPrefilteredEquirect(renderer, pmremTexture, width = 2048, height = 1024, renderTargetHeight) {
260
+ const cubeUVSize = getPMREMCubeUVSize(pmremTexture, renderTargetHeight);
261
+ const mipCount = Math.floor(Math.log2(Math.max(width, height))) + 1;
262
+ const materialXRadianceMipCount = Math.min(8, mipCount);
263
+
264
+ const renderTarget = new WebGLRenderTarget(width, height, {
265
+ format: RGBAFormat,
266
+ type: FloatType,
267
+ minFilter: LinearMipMapLinearFilter,
268
+ magFilter: LinearFilter,
269
+ generateMipmaps: false,
270
+ wrapS: RepeatWrapping,
271
+ anisotropy: renderer.capabilities.getMaxAnisotropy(),
272
+ });
273
+ renderTarget.texture.mipmaps = Array.from({ length: mipCount }, (_, mip) => ({
274
+ width: Math.max(1, width >> mip),
275
+ height: Math.max(1, height >> mip),
276
+ }));
277
+
278
+ const geometry = new PlaneGeometry(2, 2);
279
+ const camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);
280
+ const material = createPrefilteredEquirectMaterial(pmremTexture, cubeUVSize);
281
+
282
+ const tempScene = new Scene();
283
+ const mesh = new Mesh(geometry, material);
284
+ tempScene.add(mesh);
285
+
286
+ const currentRenderTarget = renderer.getRenderTarget();
287
+ const currentAutoClear = renderer.autoClear;
288
+ const currentXrEnabled = renderer.xr.enabled;
289
+ const currentShadowMapEnabled = renderer.shadowMap.enabled;
290
+ const currentViewport = renderer.getViewport(_viewport);
291
+
292
+ try {
293
+ renderer.xr.enabled = false;
294
+ renderer.shadowMap.enabled = false;
295
+ renderer.autoClear = true;
296
+
297
+ for (let mip = 0; mip < mipCount; mip++) {
298
+ const materialXMip = Math.min(mip, materialXRadianceMipCount - 1);
299
+ const lodBias = materialXMip / Math.max(1, materialXRadianceMipCount - 1);
300
+ const alpha = lodBias < 0.5 ? lodBias * lodBias : 2.0 * (lodBias - 0.375);
301
+ const roughness = Math.sqrt(Math.min(1, Math.max(0, alpha)));
302
+ const mipWidth = Math.max(1, width >> mip);
303
+ const mipHeight = Math.max(1, height >> mip);
304
+ material.uniforms.roughness.value = Math.min(1, Math.max(0, roughness));
305
+ renderTarget.viewport.set(0, 0, mipWidth, mipHeight);
306
+ renderTarget.scissor.set(0, 0, mipWidth, mipHeight);
307
+ renderer.setRenderTarget(renderTarget, 0, mip);
308
+ renderer.clear();
309
+ renderer.render(tempScene, camera);
310
+ }
311
+ } finally {
312
+ renderer.setRenderTarget(currentRenderTarget);
313
+ renderer.setViewport(currentViewport);
314
+ renderer.autoClear = currentAutoClear;
315
+ renderer.xr.enabled = currentXrEnabled;
316
+ renderer.shadowMap.enabled = currentShadowMapEnabled;
317
+
318
+ geometry.dispose();
319
+ material.dispose();
320
+ tempScene.remove(mesh);
321
+ }
322
+
323
+ renderTarget.texture.name = 'PMREM_Prefiltered_Equirectangular_Texture';
324
+ renderTarget.texture.mapping = EquirectangularReflectionMapping;
325
+ renderTarget.texture.generateMipmaps = false;
326
+ renderTarget.texture.userData.materialXRadianceMips = materialXRadianceMipCount;
327
+
328
+ if (debug) console.log('[MaterialX] PMREM to prefiltered equirect render target:', {
329
+ width: renderTarget.width,
330
+ height: renderTarget.height,
331
+ mipCount,
332
+ });
333
+
334
+ return renderTarget;
335
+ }