@needle-tools/materialx 1.0.0 → 1.0.1-next.19d0723

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.
@@ -3,10 +3,10 @@
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
-
9
- const debug = getParam("debugmaterialx");
8
+ import { debug, debugUpdate } from './utils';
9
+ import { MaterialX } from './materialx.types';
10
10
 
11
11
  const IMAGE_PROPERTY_SEPARATOR = "_";
12
12
  const UADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "uaddressmode";
@@ -20,8 +20,7 @@ const IMAGE_PATH_SEPARATOR = "/";
20
20
  * @param {Object} capabilities
21
21
  * @returns {THREE.Texture}
22
22
  */
23
- export function prepareEnvTexture(texture, capabilities)
24
- {
23
+ export function prepareEnvTexture(texture, capabilities) {
25
24
  let newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, texture.format, texture.type);
26
25
  newTexture.wrapS = THREE.RepeatWrapping;
27
26
  newTexture.anisotropy = capabilities.getMaxAnisotropy();
@@ -39,15 +38,12 @@ export function prepareEnvTexture(texture, capabilities)
39
38
  * @param {any} dimension
40
39
  * @returns {THREE.Uniform}
41
40
  */
42
- function fromVector(value, dimension)
43
- {
41
+ function fromVector(value, dimension) {
44
42
  let outValue;
45
- if (value)
46
- {
43
+ if (value) {
47
44
  outValue = [...value.data()];
48
45
  }
49
- else
50
- {
46
+ else {
51
47
  outValue = [];
52
48
  for (let i = 0; i < dimension; ++i)
53
49
  outValue.push(0.0);
@@ -58,23 +54,16 @@ function fromVector(value, dimension)
58
54
 
59
55
  /**
60
56
  * Get Three uniform from MaterialX matrix
61
- * @param {mx.matrix} matrix
62
- * @param {mx.matrix.size} dimension
63
57
  */
64
- function fromMatrix(matrix, dimension)
65
- {
66
- let vec = [];
67
- if (matrix)
68
- {
69
- for (let i = 0; i < matrix.numRows(); ++i)
70
- {
71
- for (let k = 0; k < matrix.numColumns(); ++k)
72
- {
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) {
73
63
  vec.push(matrix.getItem(i, k));
74
64
  }
75
65
  }
76
- } else
77
- {
66
+ } else {
78
67
  for (let i = 0; i < dimension; ++i)
79
68
  vec.push(0.0);
80
69
  }
@@ -82,125 +71,99 @@ function fromMatrix(matrix, dimension)
82
71
  return vec;
83
72
  }
84
73
 
74
+
75
+ export type Loaders = {
76
+ getTexture: (path: string) => THREE.Texture;
77
+ }
78
+
85
79
  /**
86
80
  * Get Three uniform from MaterialX value
87
81
  * @param {mx.Uniform.type} type
88
82
  * @param {mx.Uniform.value} value
89
83
  * @param {mx.Uniform.name} name
90
84
  * @param {mx.Uniforms} uniforms
91
- * @param {THREE.textureLoader} textureLoader
85
+ * @param {Loaders} loaders
92
86
  * @param {string} searchPath
93
87
  * @param {boolean} flipY
94
88
  */
95
- function toThreeUniform(type, value, name, uniforms, textureLoader, searchPath, flipY)
96
- {
97
- let outValue = null;
98
- switch (type)
99
- {
89
+ function toThreeUniform(type: string, value: any, name: string, uniforms: any, loaders: Loaders, searchPath, flipY: boolean) {
90
+
91
+ switch (type) {
100
92
  case 'float':
101
93
  case 'integer':
102
94
  case 'boolean':
103
- outValue = value;
95
+ return value;
104
96
  break;
105
97
  case 'vector2':
106
- outValue = fromVector(value, 2);
98
+ return fromVector(value, 2);
107
99
  break;
108
100
  case 'vector3':
109
101
  case 'color3':
110
- outValue = fromVector(value, 3);
111
- break;
102
+ return fromVector(value, 3);
112
103
  case 'vector4':
113
104
  case 'color4':
114
- outValue = fromVector(value, 4);
115
- break;
105
+ return fromVector(value, 4);
116
106
  case 'matrix33':
117
- outValue = fromMatrix(value, 9);
118
- break;
107
+ return fromMatrix(value, 9);
119
108
  case 'matrix44':
120
- outValue = fromMatrix(value, 16);
121
- break;
109
+ return fromMatrix(value, 16);
122
110
  case 'filename':
123
- if (value)
124
- {
111
+ if (value) {
125
112
  // Cache / reuse texture to avoid reload overhead.
126
113
  // Note: that data blobs and embedded data textures are not cached as they are transient data.
127
114
  let checkCache = false;
128
115
  let texturePath = searchPath + IMAGE_PATH_SEPARATOR + value;
129
- if (value.startsWith('blob:'))
130
- {
116
+ if (value.startsWith('blob:')) {
131
117
  texturePath = value;
132
118
  if (debug) console.log('Load blob URL:', texturePath);
133
119
  checkCache = false;
134
120
  }
135
- else if (value.startsWith('http'))
136
- {
121
+ else if (value.startsWith('http')) {
137
122
  texturePath = value;
138
123
  if (debug) console.log('Load HTTP URL:', texturePath);
139
124
  }
140
- else if (value.startsWith('data:'))
141
- {
125
+ else if (value.startsWith('data:')) {
142
126
  texturePath = value;
143
127
  checkCache = false;
144
128
  if (debug) console.log('Load data URL:', texturePath);
145
129
  }
146
130
  const cachedTexture = checkCache && THREE.Cache.get(texturePath);
147
- if (cachedTexture)
148
- {
149
- // Get texture from cache
150
- outValue = cachedTexture;
151
- if (debug) console.log('Use cached texture: ', texturePath, outValue);
131
+ if (cachedTexture) {
132
+ if (debug) console.log('Use cached texture: ', texturePath, cachedTexture);
133
+ return cachedTexture;
152
134
  }
153
- else
154
- {
155
- outValue = textureLoader.load(
156
- texturePath,
157
- function (texture) {
158
- if (debug) console.log('Load new texture: ' + texturePath, texture);
159
- outValue = texture;
160
-
161
- // Add texture to ThreeJS cache
162
- if (checkCache)
163
- THREE.Cache.add(texturePath, texture);
164
- },
165
- undefined,
166
- function (error) {
167
- console.error('Error loading texture: ', error);
168
- });
169
-
135
+ else {
136
+ const texture = loaders.getTexture(texturePath);
137
+ if (checkCache) THREE.Cache.add(texturePath, texture);
170
138
  // Set address & filtering mode
171
- if (outValue)
172
- setTextureParameters(outValue, name, uniforms, flipY);
139
+ if (texture) setTextureParameters(texture, name, uniforms, flipY);
140
+ return texture;
173
141
  }
174
142
  }
175
143
  break;
176
144
  case 'samplerCube':
177
145
  case 'string':
178
- break;
146
+ return null;
179
147
  default:
180
148
  const key = type + ':' + name;
181
- if (!valueTypeWarningMap.has(key))
182
- {
149
+ if (!valueTypeWarningMap.has(key)) {
183
150
  valueTypeWarningMap.set(key, true);
184
151
  console.warn('MaterialX: Unsupported uniform type: ' + type + ' for uniform: ' + name, value);
185
152
  }
186
- outValue = null;
153
+ return null;
187
154
  }
188
-
189
- return outValue;
190
155
  }
191
156
 
192
- const valueTypeWarningMap = new Map();
157
+ const valueTypeWarningMap = new Map<string, boolean>();
193
158
 
194
159
  /**
195
160
  * Get Three wrapping mode
196
161
  * @param {mx.TextureFilter.wrap} mode
197
162
  * @returns {THREE.Wrapping}
198
163
  */
199
- function getWrapping(mode)
200
- {
164
+ function getWrapping(mode) {
201
165
  let wrap;
202
- switch (mode)
203
- {
166
+ switch (mode) {
204
167
  case 1:
205
168
  wrap = THREE.ClampToEdgeWrapping;
206
169
  break;
@@ -222,12 +185,9 @@ function getWrapping(mode)
222
185
  * @param {mx.TextureFilter.minFilter} type
223
186
  * @param {mx.TextureFilter.generateMipmaps} generateMipmaps
224
187
  */
225
- function getMinFilter(type, generateMipmaps)
226
- {
227
- /** @type {THREE.TextureFilter} */
228
- let filterType = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
229
- if (type === 0)
230
- {
188
+ function getMinFilter(type, generateMipmaps) {
189
+ let filterType: THREE.TextureFilter = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
190
+ if (type === 0) {
231
191
  filterType = generateMipmaps ? THREE.NearestMipMapNearestFilter : THREE.NearestFilter;
232
192
  }
233
193
  return filterType;
@@ -240,25 +200,22 @@ function getMinFilter(type, generateMipmaps)
240
200
  * @param {mx.Uniforms} uniforms
241
201
  * @param {mx.TextureFilter.generateMipmaps} generateMipmaps
242
202
  */
243
- function setTextureParameters(texture, name, uniforms, flipY = true, generateMipmaps = true)
244
- {
203
+ function setTextureParameters(texture, name, uniforms, flipY = true, generateMipmaps = true) {
245
204
  const idx = name.lastIndexOf(IMAGE_PROPERTY_SEPARATOR);
246
205
  const base = name.substring(0, idx) || name;
247
206
 
248
- texture.generateMipmaps = generateMipmaps;
249
- texture.wrapS = THREE.RepeatWrapping;
250
- texture.wrapT = THREE.RepeatWrapping;
251
- texture.magFilter = THREE.LinearFilter;
252
- texture.flipY = flipY;
207
+ // texture.generateMipmaps = generateMipmaps;
208
+ // texture.wrapS = THREE.RepeatWrapping;
209
+ // texture.wrapT = THREE.RepeatWrapping;
210
+ // texture.magFilter = THREE.LinearFilter;
211
+ // texture.flipY = flipY;
253
212
 
254
- if (uniforms.find(base + UADDRESS_MODE_SUFFIX))
255
- {
213
+ if (uniforms.find(base + UADDRESS_MODE_SUFFIX)) {
256
214
  const uaddressmode = uniforms.find(base + UADDRESS_MODE_SUFFIX).getValue().getData();
257
215
  texture.wrapS = getWrapping(uaddressmode);
258
216
  }
259
217
 
260
- if (uniforms.find(base + VADDRESS_MODE_SUFFIX))
261
- {
218
+ if (uniforms.find(base + VADDRESS_MODE_SUFFIX)) {
262
219
  const vaddressmode = uniforms.find(base + VADDRESS_MODE_SUFFIX).getValue().getData();
263
220
  texture.wrapT = getWrapping(vaddressmode);
264
221
  }
@@ -270,8 +227,7 @@ function setTextureParameters(texture, name, uniforms, flipY = true, generateMip
270
227
  /**
271
228
  * Return the global light rotation matrix
272
229
  */
273
- export function getLightRotation()
274
- {
230
+ export function getLightRotation() {
275
231
  return new THREE.Matrix4().makeRotationY(Math.PI / 2);
276
232
  }
277
233
 
@@ -280,32 +236,27 @@ export function getLightRotation()
280
236
  * @param {mx.Document} doc
281
237
  * @returns {Array.<mx.Node>}
282
238
  */
283
- export function findLights(doc)
284
- {
285
- let lights = [];
286
- for (let node of doc.getNodes())
287
- {
239
+ export function findLights(doc: MaterialX.Document) {
240
+ let lights = new Array<any>;
241
+ for (let node of doc.getNodes()) {
288
242
  if (node.getType() === "lightshader")
289
243
  lights.push(node);
290
244
  }
291
245
  return lights;
292
246
  }
293
247
 
248
+ let lightTypesBound = {};
249
+
294
250
  /**
295
251
  * Register lights in shader generation context
296
- * @param {Object} mx MaterialX Module
297
- * @param {Array.<mx.Node>} lights Light nodes
252
+ * @param {MaterialX.MODULE} mx MaterialX Module
298
253
  * @param {mx.GenContext} genContext Shader generation context
299
- * @returns {Array.<mx.Node>}
300
254
  */
301
- export async function registerLights(mx, lights, genContext)
302
- {
255
+ export async function registerLights(mx: MaterialX.MODULE, genContext: any): Promise<void> {
256
+ lightTypesBound = {};
257
+ const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
303
258
  mx.HwShaderGenerator.unbindLightShaders(genContext);
304
-
305
- const lightTypesBound = {};
306
- const lightData = [];
307
259
  let lightId = 1;
308
-
309
260
  // All light types so that we have NodeDefs for them
310
261
  const defaultLightRigXml = `<?xml version="1.0"?>
311
262
  <materialx version="1.39">
@@ -323,7 +274,7 @@ export async function registerLights(mx, lights, genContext)
323
274
 
324
275
  // Load default light rig XML to ensure we have all light types available
325
276
  const lightRigDoc = mx.createDocument();
326
- await mx.readFromXmlString(lightRigDoc, defaultLightRigXml);
277
+ await mx.readFromXmlString(lightRigDoc, defaultLightRigXml, "");
327
278
  const document = mx.createDocument();
328
279
  const stdlib = mx.loadStandardLibraries(genContext);
329
280
  document.setDataLibrary(stdlib);
@@ -331,18 +282,19 @@ export async function registerLights(mx, lights, genContext)
331
282
  const defaultLights = findLights(document);
332
283
  if (debug) console.log("Default lights in MaterialX document", defaultLights);
333
284
 
285
+ // Loading a document seems to reset this option for some reason, so we set it again
286
+ genContext.getOptions().hwMaxActiveLightSources = maxLightCount;
287
+
334
288
  // Register types only – we get these from the default light rig XML above
335
289
  // This is needed to ensure that the light shaders are bound for each light type
336
- for (let light of defaultLights)
337
- {
290
+ for (let light of defaultLights) {
338
291
  const lightDef = light.getNodeDef();
339
292
  if (debug) console.log("Default light node definition", lightDef);
340
293
  if (!lightDef) continue;
341
294
 
342
295
  const lightName = lightDef.getName();
343
296
  if (debug) console.log("Registering default light", { lightName, lightDef });
344
- if (!lightTypesBound[lightName])
345
- {
297
+ if (!lightTypesBound[lightName]) {
346
298
  // TODO check if we need to bind light shader for each three.js light instead of once per type
347
299
  if (debug) console.log("Bind light shader for node", { lightName, lightId, lightDef });
348
300
  lightTypesBound[lightName] = lightId;
@@ -351,52 +303,40 @@ export async function registerLights(mx, lights, genContext)
351
303
  }
352
304
 
353
305
  if (debug) console.log("Light types bound in MaterialX context", lightTypesBound);
306
+ }
354
307
 
355
- // MaterialX light nodes
356
- for (let light of lights)
357
- {
358
- // Skip if light does not have a node definition
359
- if (!("getNodeDef" in light)) continue;
360
-
361
- let nodeDef = light.getNodeDef();
362
- let nodeName = nodeDef.getName();
363
- if (!lightTypesBound[nodeName])
364
- {
365
- if (debug) console.log("bind light shader for node", { nodeName, lightId, nodeDef });
366
- lightTypesBound[nodeName] = lightId;
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
- });
308
+ // Converts Three.js light type to MaterialX node name
309
+ function threeLightTypeToMaterialXNodeName(threeLightType) {
310
+ switch (threeLightType) {
311
+ case 'PointLight':
312
+ return 'ND_point_light';
313
+ case 'DirectionalLight':
314
+ return 'ND_directional_light';
315
+ case 'SpotLight':
316
+ return 'ND_spot_light';
317
+ default:
318
+ console.warn('MaterialX: Unsupported light type: ' + threeLightType);
319
+ return 'ND_point_light'; // Default to point light
383
320
  }
321
+ };
322
+
323
+ type LightData = {
324
+ type: number, // Light type ID
325
+ position: THREE.Vector3, // Position in world space
326
+ direction: THREE.Vector3, // Direction in world space
327
+ color: THREE.Color, // Color of the light
328
+ intensity: number, // Intensity of the light
329
+ decay_rate: number, // Decay rate for point and spot lights
330
+ inner_angle: number, // Inner angle for spot lights
331
+ outer_angle: number, // Outer angle for spot lights
332
+ }
384
333
 
385
- const threeLightTypeToMaterialXNodeName = (threeLightType) => {
386
- switch (threeLightType) {
387
- case 'PointLight':
388
- return 'ND_point_light';
389
- case 'DirectionalLight':
390
- return 'ND_directional_light';
391
- case 'SpotLight':
392
- return 'ND_spot_light';
393
- default:
394
- console.warn('MaterialX: Unsupported light type: ' + threeLightType);
395
- return 'ND_point_light'; // Default to point light
396
- }
397
- };
398
-
399
- if (debug) console.log("Registering lights in MaterialX context", lights, lightData);
334
+ /**
335
+ * Update light data for shader uniforms
336
+ */
337
+ export function getLightData(lights: any, genContext: any): { lightData: LightData[], lightCount: number } {
338
+ const lightData = new Array();
339
+ const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
400
340
 
401
341
  // Three.js lights
402
342
  for (let light of lights) {
@@ -408,50 +348,82 @@ export async function registerLights(mx, lights, genContext)
408
348
  const lightDefinitionName = threeLightTypeToMaterialXNodeName(light.type);
409
349
 
410
350
  if (!lightTypesBound[lightDefinitionName])
411
- {
412
- lightTypesBound[lightDefinitionName] = lightId;
413
- const nodeDef = null;
414
- mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
415
- }
351
+ console.error("MaterialX: Light type not registered in context. Make sure to register light types before using them.", lightDefinitionName);
352
+
353
+ const wp = light.getWorldPosition(new THREE.Vector3());
354
+ const wd = getWorldDirection(light, new THREE.Vector3(0, 0, -1));
355
+
356
+ // Shader math from the generated MaterialX shader:
357
+ // float low = min(light.inner_angle, light.outer_angle);
358
+ // float high = light.inner_angle;
359
+ // float cosDir = dot(result.direction, -light.direction);
360
+ // float spotAttenuation = smoothstep(low, high, cosDir);
361
+
362
+ const outerAngleRad = light.angle;
363
+ const innerAngleRad = outerAngleRad * (1 - light.penumbra);
364
+ const inner_angle = Math.cos(innerAngleRad);
365
+ const outer_angle = Math.cos(outerAngleRad);
416
366
 
417
367
  lightData.push({
418
368
  type: lightTypesBound[lightDefinitionName],
419
- direction: light.direction?.clone() || new THREE.Vector3(0, -1, 0),
420
- color: new THREE.Vector3().fromArray(light.color.toArray()),
421
- intensity: light.intensity,
369
+ position: wp.clone(),
370
+ direction: wd.clone(),
371
+ color: new THREE.Color().fromArray(light.color.toArray()),
372
+ // Luminous efficacy for converting radiant power in watts (W) to luminous flux in lumens (lm) at a wavelength of 555 nm.
373
+ // 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.
374
+ intensity: light.intensity * (light.isPointLight ? 683.0 / 3.1415 : light.isSpotLight ? 683.0 / 3.1415 : 1.0),
375
+ decay_rate: 2.0,
376
+ // Approximations for testing – the relevant light has 61.57986...129.4445 as inner/outer spot angle
377
+ inner_angle: inner_angle,
378
+ outer_angle: outer_angle,
422
379
  });
423
380
  }
424
381
 
425
- // Make sure max light count is large enough
426
- genContext.getOptions().hwMaxActiveLightSources = Math.max(genContext.getOptions().hwMaxActiveLightSources, lightData.length);
382
+ // Count the number of lights that are not empty
383
+ const lightCount = lightData.length;
384
+
385
+ // If we don't have enough entries in lightData, fill with empty lights
386
+ while (lightData.length < maxLightCount) {
387
+ const emptyLight = {
388
+ type: 0, // Default light type
389
+ position: new THREE.Vector3(0, 0, 0),
390
+ direction: new THREE.Vector3(0, 0, -1),
391
+ color: new THREE.Color(0, 0, 0),
392
+ intensity: 0.0,
393
+ decay_rate: 2.0,
394
+ inner_angle: 0.0,
395
+ outer_angle: 0.0,
396
+ };
397
+ lightData.push(emptyLight);
398
+ }
399
+
400
+ if (debugUpdate) console.log("Registered lights in MaterialX context", lightTypesBound, lightData);
427
401
 
428
- return lightData;
402
+ return { lightData, lightCount };
429
403
  }
430
404
 
431
405
  /**
432
406
  * Get uniform values for a shader
433
- * @param {mx.shaderStage} shaderStage
434
- * @param {THREE.TextureLoader} textureLoader
435
407
  */
436
- export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
437
- {
438
- let threeUniforms = {};
439
-
440
- const uniformBlocks = Object.values(shaderStage.getUniformBlocks());
441
- uniformBlocks.forEach(uniforms =>
442
- {
443
- if (!uniforms.empty())
444
- {
445
- for (let i = 0; i < uniforms.size(); ++i)
446
- {
408
+ export function getUniformValues(shaderStage: MaterialX.ShaderStage, loaders: Loaders, searchPath: string, flipY: boolean) {
409
+ const threeUniforms = {};
410
+
411
+ const uniformBlocks = shaderStage.getUniformBlocks()
412
+ for (const [blockName, uniforms] of Object.entries(uniformBlocks)) {
413
+ // Seems struct uniforms (like in LightData) end up here as well, we should filter those out.
414
+ if (blockName === "LightData") continue;
415
+
416
+ if (!uniforms.empty()) {
417
+ for (let i = 0; i < uniforms.size(); ++i) {
447
418
  const variable = uniforms.get(i);
448
419
  const value = variable.getValue()?.getData();
449
420
  const name = variable.getVariable();
421
+ if (debug) console.log("Adding uniform", { path: variable.getPath(), name, value, type: variable.getType().getName() });
450
422
  threeUniforms[name] = new THREE.Uniform(toThreeUniform(variable.getType().getName(), value, name, uniforms,
451
- textureLoader, searchPath, flipY));
423
+ loaders, searchPath, flipY));
452
424
  }
453
425
  }
454
- });
426
+ }
455
427
 
456
428
  return threeUniforms;
457
429
  }