@needle-tools/materialx 1.0.1-next.31390e3 → 1.0.1-next.64f3b67

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.
@@ -5,8 +5,8 @@
5
5
 
6
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,36 +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
- // TODO Remove, not sure why we need that – something resets the value inbetween calls to registerLights
305
- genContext.getOptions().hwMaxActiveLightSources = 4;
306
-
307
- const lightTypesBound = {};
308
- const lightData = [];
309
259
  let lightId = 1;
310
- let lightCount = 0;
311
- const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
312
-
313
260
  // All light types so that we have NodeDefs for them
314
261
  const defaultLightRigXml = `<?xml version="1.0"?>
315
262
  <materialx version="1.39">
@@ -327,7 +274,7 @@ export async function registerLights(mx, lights, genContext)
327
274
 
328
275
  // Load default light rig XML to ensure we have all light types available
329
276
  const lightRigDoc = mx.createDocument();
330
- await mx.readFromXmlString(lightRigDoc, defaultLightRigXml);
277
+ await mx.readFromXmlString(lightRigDoc, defaultLightRigXml, "");
331
278
  const document = mx.createDocument();
332
279
  const stdlib = mx.loadStandardLibraries(genContext);
333
280
  document.setDataLibrary(stdlib);
@@ -340,16 +287,14 @@ export async function registerLights(mx, lights, genContext)
340
287
 
341
288
  // Register types only – we get these from the default light rig XML above
342
289
  // This is needed to ensure that the light shaders are bound for each light type
343
- for (let light of defaultLights)
344
- {
290
+ for (let light of defaultLights) {
345
291
  const lightDef = light.getNodeDef();
346
292
  if (debug) console.log("Default light node definition", lightDef);
347
293
  if (!lightDef) continue;
348
294
 
349
295
  const lightName = lightDef.getName();
350
296
  if (debug) console.log("Registering default light", { lightName, lightDef });
351
- if (!lightTypesBound[lightName])
352
- {
297
+ if (!lightTypesBound[lightName]) {
353
298
  // TODO check if we need to bind light shader for each three.js light instead of once per type
354
299
  if (debug) console.log("Bind light shader for node", { lightName, lightId, lightDef });
355
300
  lightTypesBound[lightName] = lightId;
@@ -358,52 +303,40 @@ export async function registerLights(mx, lights, genContext)
358
303
  }
359
304
 
360
305
  if (debug) console.log("Light types bound in MaterialX context", lightTypesBound);
306
+ }
361
307
 
362
- // MaterialX light nodes
363
- for (let light of lights)
364
- {
365
- // Skip if light does not have a node definition
366
- if (!("getNodeDef" in light)) continue;
367
-
368
- let nodeDef = light.getNodeDef();
369
- let nodeName = nodeDef.getName();
370
- if (!lightTypesBound[nodeName])
371
- {
372
- if (debug) console.log("bind light shader for node", { nodeName, lightId, nodeDef });
373
- lightTypesBound[nodeName] = lightId;
374
- mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
375
- }
376
-
377
- const lightDirection = light.getValueElement("direction").getValue().getData().data();
378
- const lightColor = light.getValueElement("color").getValue().getData().data();
379
- const lightIntensity = light.getValueElement("intensity").getValue().getData();
380
-
381
- let rotatedLightDirection = new THREE.Vector3(...lightDirection)
382
- rotatedLightDirection.transformDirection(getLightRotation())
383
-
384
- lightData.push({
385
- type: lightTypesBound[nodeName],
386
- direction: rotatedLightDirection,
387
- color: new THREE.Vector3(...lightColor),
388
- intensity: lightIntensity,
389
- });
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
390
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
+ }
391
333
 
392
- const threeLightTypeToMaterialXNodeName = (threeLightType) => {
393
- switch (threeLightType) {
394
- case 'PointLight':
395
- return 'ND_point_light';
396
- case 'DirectionalLight':
397
- return 'ND_directional_light';
398
- case 'SpotLight':
399
- return 'ND_spot_light';
400
- default:
401
- console.warn('MaterialX: Unsupported light type: ' + threeLightType);
402
- return 'ND_point_light'; // Default to point light
403
- }
404
- };
405
-
406
- 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;
407
340
 
408
341
  // Three.js lights
409
342
  for (let light of lights) {
@@ -415,76 +348,82 @@ export async function registerLights(mx, lights, genContext)
415
348
  const lightDefinitionName = threeLightTypeToMaterialXNodeName(light.type);
416
349
 
417
350
  if (!lightTypesBound[lightDefinitionName])
418
- {
419
- lightTypesBound[lightDefinitionName] = lightId;
420
- const nodeDef = null;
421
- mx.HwShaderGenerator.bindLightShader(nodeDef, lightId++, genContext);
422
- }
351
+ console.error("MaterialX: Light type not registered in context. Make sure to register light types before using them.", lightDefinitionName);
423
352
 
424
353
  const wp = light.getWorldPosition(new THREE.Vector3());
425
- const wd = getWorldDirection(light, new THREE.Vector3(0,0,-1));
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);
366
+
426
367
  lightData.push({
427
368
  type: lightTypesBound[lightDefinitionName],
428
369
  position: wp.clone(),
429
370
  direction: wd.clone(),
430
- color: new THREE.Vector3().fromArray(light.color.toArray()),
431
- intensity: light.intensity, // Scale intensity for spot lights
432
- decay_rate: 2.0, // physically-based default decay rate
433
- inner_angle: 1.0,
434
- outer_angle: 2.0,
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,
435
379
  });
436
380
  }
437
381
 
438
382
  // Count the number of lights that are not empty
439
- lightCount = lightData.length;
383
+ const lightCount = lightData.length;
440
384
 
441
385
  // If we don't have enough entries in lightData, fill with empty lights
442
- if (lightData.length < maxLightCount)
443
- {
386
+ while (lightData.length < maxLightCount) {
444
387
  const emptyLight = {
445
388
  type: 0, // Default light type
446
389
  position: new THREE.Vector3(0, 0, 0),
447
390
  direction: new THREE.Vector3(0, 0, -1),
448
- color: new THREE.Vector3(0, 0, 0),
449
- intensity: 0,
450
- decay_rate: 2,
451
- inner_angle: 0,
452
- outer_angle: 0,
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,
453
396
  };
454
- while (lightData.length < maxLightCount) {
455
- lightData.push(emptyLight);
456
- }
397
+ lightData.push(emptyLight);
457
398
  }
458
399
 
459
- if (debug) console.log("Registered lights in MaterialX context", lightTypesBound, lightData);
400
+ if (debugUpdate) console.log("Registered lights in MaterialX context", lightTypesBound, lightData);
460
401
 
461
402
  return { lightData, lightCount };
462
403
  }
463
404
 
464
405
  /**
465
406
  * Get uniform values for a shader
466
- * @param {mx.shaderStage} shaderStage
467
- * @param {THREE.TextureLoader} textureLoader
468
407
  */
469
- export function getUniformValues(shaderStage, textureLoader, searchPath, flipY)
470
- {
471
- let threeUniforms = {};
472
-
473
- const uniformBlocks = Object.values(shaderStage.getUniformBlocks());
474
- uniformBlocks.forEach(uniforms =>
475
- {
476
- if (!uniforms.empty())
477
- {
478
- for (let i = 0; i < uniforms.size(); ++i)
479
- {
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) {
480
418
  const variable = uniforms.get(i);
481
419
  const value = variable.getValue()?.getData();
482
420
  const name = variable.getVariable();
421
+ if (debug) console.log("Adding uniform", { path: variable.getPath(), name, value, type: variable.getType().getName() });
483
422
  threeUniforms[name] = new THREE.Uniform(toThreeUniform(variable.getType().getName(), value, name, uniforms,
484
- textureLoader, searchPath, flipY));
423
+ loaders, searchPath, flipY));
485
424
  }
486
425
  }
487
- });
426
+ }
488
427
 
489
428
  return threeUniforms;
490
429
  }