@needle-tools/materialx 1.0.1 → 1.0.2-next.c468cd8
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/CHANGELOG.md +5 -0
- package/README.md +5 -0
- package/codegen/register_types.ts +4 -0
- package/index.ts +1 -1
- package/package.json +19 -4
- package/src/index.ts +2 -2
- package/src/loader/loader.needle.ts +27 -40
- package/src/loader/loader.three.ts +142 -397
- package/src/materialx.helper.ts +478 -0
- package/src/materialx.material.ts +217 -0
- package/src/materialx.ts +161 -128
- package/src/materialx.types.d.ts +50 -0
- package/src/textureHelper.ts +6 -6
- package/src/utils.ts +39 -4
- package/src/helper.js +0 -457
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright Contributors to the MaterialX Project
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
import { getParam, getWorldDirection } from '@needle-tools/engine';
|
|
7
|
+
import * as THREE from 'three';
|
|
8
|
+
import { debug, debugUpdate } from './utils';
|
|
9
|
+
import { MaterialX } from './materialx.types';
|
|
10
|
+
|
|
11
|
+
const IMAGE_PROPERTY_SEPARATOR = "_";
|
|
12
|
+
const UADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "uaddressmode";
|
|
13
|
+
const VADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "vaddressmode";
|
|
14
|
+
const FILTER_TYPE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "filtertype";
|
|
15
|
+
const IMAGE_PATH_SEPARATOR = "/";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialized the environment texture as MaterialX expects it
|
|
19
|
+
* @param {THREE.Texture} texture
|
|
20
|
+
* @param {Object} capabilities
|
|
21
|
+
* @returns {THREE.Texture}
|
|
22
|
+
*/
|
|
23
|
+
export function prepareEnvTexture(texture, capabilities) {
|
|
24
|
+
let newTexture = new THREE.DataTexture(texture.image.data, texture.image.width, texture.image.height, texture.format, texture.type);
|
|
25
|
+
newTexture.wrapS = THREE.RepeatWrapping;
|
|
26
|
+
newTexture.anisotropy = capabilities.getMaxAnisotropy();
|
|
27
|
+
newTexture.minFilter = THREE.LinearMipmapLinearFilter;
|
|
28
|
+
newTexture.magFilter = THREE.LinearFilter;
|
|
29
|
+
newTexture.generateMipmaps = true;
|
|
30
|
+
newTexture.needsUpdate = true;
|
|
31
|
+
|
|
32
|
+
return newTexture;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get Three uniform from MaterialX vector
|
|
37
|
+
* @param {any} value
|
|
38
|
+
* @param {any} dimension
|
|
39
|
+
* @returns {THREE.Uniform}
|
|
40
|
+
*/
|
|
41
|
+
function fromVector(value, dimension) {
|
|
42
|
+
let outValue;
|
|
43
|
+
if (value) {
|
|
44
|
+
outValue = [...value.data()];
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
outValue = [];
|
|
48
|
+
for (let i = 0; i < dimension; ++i)
|
|
49
|
+
outValue.push(0.0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return outValue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get Three uniform from MaterialX matrix
|
|
57
|
+
*/
|
|
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) {
|
|
63
|
+
vec.push(matrix.getItem(i, k));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
for (let i = 0; i < dimension; ++i)
|
|
68
|
+
vec.push(0.0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return vec;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
export type Loaders = {
|
|
76
|
+
/**
|
|
77
|
+
* Cache key for the loaders, used to identify and reuse textures
|
|
78
|
+
*/
|
|
79
|
+
readonly cacheKey: string;
|
|
80
|
+
/**
|
|
81
|
+
* Get a texture by path
|
|
82
|
+
* @param {string} path - The path to the texture
|
|
83
|
+
* @return {Promise<THREE.Texture>} - A promise that resolves to the texture
|
|
84
|
+
*/
|
|
85
|
+
readonly getTexture: (path: string) => Promise<THREE.Texture>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const defaultTexture = new THREE.Texture();
|
|
89
|
+
defaultTexture.needsUpdate = true;
|
|
90
|
+
defaultTexture.image = new Image();
|
|
91
|
+
defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAANQTFRFr6+vGqg52AAAAAxJREFUeJxjZGBEgQAAWAAJLpjsTQAAAABJRU5ErkJggg=="
|
|
92
|
+
// defaultTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAB5QTFRFAAAABAQEw8PD////v7+/vb29Xl5eQEBA+/v7PDw8GPBYkgAAAB1JREFUeJxjZGBgYFQSABIUMlxgDGMGBtaIAnIZAKwQCSDYUEZEAAAAAElFTkSuQmCC";
|
|
93
|
+
// defaultTexture.wrapS = THREE.RepeatWrapping;
|
|
94
|
+
// defaultTexture.wrapT = THREE.RepeatWrapping;
|
|
95
|
+
// defaultTexture.minFilter = THREE.NearestFilter;
|
|
96
|
+
// defaultTexture.magFilter = THREE.NearestFilter;
|
|
97
|
+
// defaultTexture.repeat = new THREE.Vector2(100, 100);
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
const defaultNormalTexture = new THREE.Texture();
|
|
101
|
+
defaultNormalTexture.needsUpdate = true;
|
|
102
|
+
defaultNormalTexture.image = new Image();
|
|
103
|
+
defaultNormalTexture.image.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIBAMAAAA2IaO4AAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAABJQTFRFgYH4gIH4gYH3gIH3gIH5gID4m94ORAAAADFJREFUeJxjZBBkfMdo9P/BB0aBj/8FGB0ufghgFGT4r8wo+P8rD2Pgo3sMjIz8jAwAMLoN0ZjS5hgAAAAASUVORK5CYII=";
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
function tryGetFromCache(key: string): any {
|
|
107
|
+
const wasEnabled = THREE.Cache.enabled;
|
|
108
|
+
THREE.Cache.enabled = true;
|
|
109
|
+
const value = THREE.Cache.get(key);
|
|
110
|
+
THREE.Cache.enabled = wasEnabled;
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
function addToCache(key: string, value: any): void {
|
|
114
|
+
const wasEnabled = THREE.Cache.enabled;
|
|
115
|
+
THREE.Cache.enabled = true;
|
|
116
|
+
THREE.Cache.add(key, value);
|
|
117
|
+
THREE.Cache.enabled = wasEnabled;
|
|
118
|
+
if (debug) console.log('[MaterialX] Added to cache:', key, value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get Three uniform from MaterialX value
|
|
123
|
+
*/
|
|
124
|
+
function toThreeUniform(uniforms: any, type: string, value: any, name: string, loaders: Loaders, searchPath: string): THREE.Uniform {
|
|
125
|
+
|
|
126
|
+
const uniform = new THREE.Uniform<any>(null);
|
|
127
|
+
|
|
128
|
+
switch (type) {
|
|
129
|
+
case 'float':
|
|
130
|
+
case 'integer':
|
|
131
|
+
case 'boolean':
|
|
132
|
+
uniform.value = value;
|
|
133
|
+
break;
|
|
134
|
+
case 'vector2':
|
|
135
|
+
uniform.value = fromVector(value, 2);
|
|
136
|
+
break;
|
|
137
|
+
case 'vector3':
|
|
138
|
+
case 'color3':
|
|
139
|
+
uniform.value = fromVector(value, 3);
|
|
140
|
+
break;
|
|
141
|
+
case 'vector4':
|
|
142
|
+
case 'color4':
|
|
143
|
+
uniform.value = fromVector(value, 4);
|
|
144
|
+
break;
|
|
145
|
+
case 'matrix33':
|
|
146
|
+
uniform.value = fromMatrix(value, 9);
|
|
147
|
+
break;
|
|
148
|
+
case 'matrix44':
|
|
149
|
+
uniform.value = fromMatrix(value, 16);
|
|
150
|
+
break;
|
|
151
|
+
case 'filename':
|
|
152
|
+
if (value) {
|
|
153
|
+
// Cache / reuse texture to avoid reload overhead.
|
|
154
|
+
// Note: that data blobs and embedded data textures are not cached as they are transient data.
|
|
155
|
+
let checkCache = true;
|
|
156
|
+
let texturePath = searchPath + IMAGE_PATH_SEPARATOR + value;
|
|
157
|
+
if (value.startsWith('blob:')) {
|
|
158
|
+
texturePath = value;
|
|
159
|
+
checkCache = false;
|
|
160
|
+
}
|
|
161
|
+
else if (value.startsWith('data:')) {
|
|
162
|
+
texturePath = value;
|
|
163
|
+
checkCache = false;
|
|
164
|
+
}
|
|
165
|
+
else if (value.startsWith('http')) {
|
|
166
|
+
texturePath = value;
|
|
167
|
+
checkCache = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const cacheKey = `${loaders.cacheKey}-${texturePath}`;
|
|
171
|
+
const cacheValue = checkCache ? tryGetFromCache(cacheKey) : null;
|
|
172
|
+
if (cacheValue) {
|
|
173
|
+
if (debug) console.log('[MaterialX] Use cached texture: ', cacheKey, cacheValue);
|
|
174
|
+
if (cacheValue instanceof Promise) {
|
|
175
|
+
cacheValue.then(res => {
|
|
176
|
+
if (res) uniform.value = res;
|
|
177
|
+
else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
uniform.value = cacheValue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
if (name.toLowerCase().includes("normal")) {
|
|
186
|
+
uniform.value = defaultNormalTexture;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
uniform.value = defaultTexture;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (debug) console.log('[MaterialX] Load texture:', texturePath);
|
|
193
|
+
// Save the loading promise in the cache
|
|
194
|
+
const promise = loaders.getTexture(texturePath).then(res => {
|
|
195
|
+
if (res) {
|
|
196
|
+
res = res.clone(); // we need to clone the texture once to avoid colorSpace issues with other materials
|
|
197
|
+
res.colorSpace = THREE.LinearSRGBColorSpace;
|
|
198
|
+
setTextureParameters(res, name, uniforms);
|
|
199
|
+
}
|
|
200
|
+
return res;
|
|
201
|
+
});
|
|
202
|
+
if (checkCache) {
|
|
203
|
+
addToCache(cacheKey, promise);
|
|
204
|
+
}
|
|
205
|
+
promise.then(res => {
|
|
206
|
+
if (res) uniform.value = res;
|
|
207
|
+
else console.warn(`[MaterialX] Failed to load texture ${name} '${texturePath}'`);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
case 'samplerCube':
|
|
213
|
+
case 'string':
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
const key = type + ':' + name;
|
|
217
|
+
if (!valueTypeWarningMap.has(key)) {
|
|
218
|
+
valueTypeWarningMap.set(key, true);
|
|
219
|
+
console.warn('MaterialX: Unsupported uniform type: ' + type + ' for uniform: ' + name, value);
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return uniform;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const valueTypeWarningMap = new Map<string, boolean>();
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get Three wrapping mode
|
|
231
|
+
*/
|
|
232
|
+
function getWrapping(mode: number): THREE.Wrapping {
|
|
233
|
+
let wrap;
|
|
234
|
+
switch (mode) {
|
|
235
|
+
case 1:
|
|
236
|
+
wrap = THREE.ClampToEdgeWrapping;
|
|
237
|
+
break;
|
|
238
|
+
case 2:
|
|
239
|
+
wrap = THREE.RepeatWrapping;
|
|
240
|
+
break;
|
|
241
|
+
case 3:
|
|
242
|
+
wrap = THREE.MirroredRepeatWrapping;
|
|
243
|
+
break;
|
|
244
|
+
default:
|
|
245
|
+
wrap = THREE.RepeatWrapping;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
return wrap;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Set Three texture parameters
|
|
254
|
+
*/
|
|
255
|
+
function setTextureParameters(texture: THREE.Texture, name: string, uniforms: any, generateMipmaps = true) {
|
|
256
|
+
const idx = name.lastIndexOf(IMAGE_PROPERTY_SEPARATOR);
|
|
257
|
+
const base = name.substring(0, idx) || name;
|
|
258
|
+
|
|
259
|
+
if (uniforms.find(base + UADDRESS_MODE_SUFFIX)) {
|
|
260
|
+
const uaddressmode = uniforms.find(base + UADDRESS_MODE_SUFFIX).getValue().getData();
|
|
261
|
+
texture.wrapS = getWrapping(uaddressmode);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (uniforms.find(base + VADDRESS_MODE_SUFFIX)) {
|
|
265
|
+
const vaddressmode = uniforms.find(base + VADDRESS_MODE_SUFFIX).getValue().getData();
|
|
266
|
+
texture.wrapT = getWrapping(vaddressmode);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const mxFilterType = uniforms.find(base + FILTER_TYPE_SUFFIX) ? uniforms.get(base + FILTER_TYPE_SUFFIX).value : -1;
|
|
270
|
+
let minFilter: THREE.TextureFilter = generateMipmaps ? THREE.LinearMipMapLinearFilter : THREE.LinearFilter;
|
|
271
|
+
if (mxFilterType === 0) {
|
|
272
|
+
minFilter = generateMipmaps ? THREE.NearestMipMapNearestFilter : THREE.NearestFilter;
|
|
273
|
+
}
|
|
274
|
+
texture.minFilter = minFilter;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Return the global light rotation matrix
|
|
279
|
+
*/
|
|
280
|
+
export function getLightRotation() {
|
|
281
|
+
return new THREE.Matrix4().makeRotationY(Math.PI / 2);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Returns all lights nodes in a MaterialX document
|
|
286
|
+
* @param {mx.Document} doc
|
|
287
|
+
* @returns {Array.<mx.Node>}
|
|
288
|
+
*/
|
|
289
|
+
export function findLights(doc: MaterialX.Document) {
|
|
290
|
+
let lights = new Array<any>;
|
|
291
|
+
for (let node of doc.getNodes()) {
|
|
292
|
+
if (node.getType() === "lightshader")
|
|
293
|
+
lights.push(node);
|
|
294
|
+
}
|
|
295
|
+
return lights;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let lightTypesBound = {};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Register lights in shader generation context
|
|
302
|
+
* @param {MaterialX.MODULE} mx MaterialX Module
|
|
303
|
+
* @param {mx.GenContext} genContext Shader generation context
|
|
304
|
+
*/
|
|
305
|
+
export async function registerLights(mx: MaterialX.MODULE, genContext: any): Promise<void> {
|
|
306
|
+
lightTypesBound = {};
|
|
307
|
+
const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
|
|
308
|
+
mx.HwShaderGenerator.unbindLightShaders(genContext);
|
|
309
|
+
let lightId = 1;
|
|
310
|
+
// All light types so that we have NodeDefs for them
|
|
311
|
+
const defaultLightRigXml = `<?xml version="1.0"?>
|
|
312
|
+
<materialx version="1.39">
|
|
313
|
+
<directional_light name="default_directional_light" type="lightshader">
|
|
314
|
+
</directional_light>
|
|
315
|
+
<point_light name="default_point_light" type="lightshader">
|
|
316
|
+
</point_light>
|
|
317
|
+
<spot_light name="default_spot_light" type="lightshader">
|
|
318
|
+
</spot_light>
|
|
319
|
+
<!--
|
|
320
|
+
<area_light name="default_area_light" type="lightshader">
|
|
321
|
+
</area_light>
|
|
322
|
+
-->
|
|
323
|
+
</materialx>`;
|
|
324
|
+
|
|
325
|
+
// Load default light rig XML to ensure we have all light types available
|
|
326
|
+
const lightRigDoc = mx.createDocument();
|
|
327
|
+
await mx.readFromXmlString(lightRigDoc, defaultLightRigXml, "");
|
|
328
|
+
const document = mx.createDocument();
|
|
329
|
+
const stdlib = mx.loadStandardLibraries(genContext);
|
|
330
|
+
document.setDataLibrary(stdlib);
|
|
331
|
+
document.importLibrary(lightRigDoc);
|
|
332
|
+
const defaultLights = findLights(document);
|
|
333
|
+
if (debug) console.log("Default lights in MaterialX document", defaultLights);
|
|
334
|
+
|
|
335
|
+
// Loading a document seems to reset this option for some reason, so we set it again
|
|
336
|
+
genContext.getOptions().hwMaxActiveLightSources = maxLightCount;
|
|
337
|
+
|
|
338
|
+
// Register types only – we get these from the default light rig XML above
|
|
339
|
+
// This is needed to ensure that the light shaders are bound for each light type
|
|
340
|
+
for (let light of defaultLights) {
|
|
341
|
+
const lightDef = light.getNodeDef();
|
|
342
|
+
if (debug) console.log("Default light node definition", lightDef);
|
|
343
|
+
if (!lightDef) continue;
|
|
344
|
+
|
|
345
|
+
const lightName = lightDef.getName();
|
|
346
|
+
if (debug) console.log("Registering default light", { lightName, lightDef });
|
|
347
|
+
if (!lightTypesBound[lightName]) {
|
|
348
|
+
// TODO check if we need to bind light shader for each three.js light instead of once per type
|
|
349
|
+
if (debug) console.log("Bind light shader for node", { lightName, lightId, lightDef });
|
|
350
|
+
lightTypesBound[lightName] = lightId;
|
|
351
|
+
mx.HwShaderGenerator.bindLightShader(lightDef, lightId++, genContext);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (debug) console.log("Light types bound in MaterialX context", lightTypesBound);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Converts Three.js light type to MaterialX node name
|
|
359
|
+
function threeLightTypeToMaterialXNodeName(threeLightType) {
|
|
360
|
+
switch (threeLightType) {
|
|
361
|
+
case 'PointLight':
|
|
362
|
+
return 'ND_point_light';
|
|
363
|
+
case 'DirectionalLight':
|
|
364
|
+
return 'ND_directional_light';
|
|
365
|
+
case 'SpotLight':
|
|
366
|
+
return 'ND_spot_light';
|
|
367
|
+
default:
|
|
368
|
+
console.warn('MaterialX: Unsupported light type: ' + threeLightType);
|
|
369
|
+
return 'ND_point_light'; // Default to point light
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
type LightData = {
|
|
374
|
+
type: number, // Light type ID
|
|
375
|
+
position: THREE.Vector3, // Position in world space
|
|
376
|
+
direction: THREE.Vector3, // Direction in world space
|
|
377
|
+
color: THREE.Color, // Color of the light
|
|
378
|
+
intensity: number, // Intensity of the light
|
|
379
|
+
decay_rate: number, // Decay rate for point and spot lights
|
|
380
|
+
inner_angle: number, // Inner angle for spot lights
|
|
381
|
+
outer_angle: number, // Outer angle for spot lights
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Update light data for shader uniforms
|
|
386
|
+
*/
|
|
387
|
+
export function getLightData(lights: any, genContext: any): { lightData: LightData[], lightCount: number } {
|
|
388
|
+
const lightData = new Array();
|
|
389
|
+
const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
|
|
390
|
+
|
|
391
|
+
// Three.js lights
|
|
392
|
+
for (let light of lights) {
|
|
393
|
+
// Skip if light is not a Three.js light
|
|
394
|
+
if (!light.isLight) continue;
|
|
395
|
+
|
|
396
|
+
// Types in MaterialX: point_light, directional_light, spot_light
|
|
397
|
+
|
|
398
|
+
const lightDefinitionName = threeLightTypeToMaterialXNodeName(light.type);
|
|
399
|
+
|
|
400
|
+
if (!lightTypesBound[lightDefinitionName])
|
|
401
|
+
console.error("MaterialX: Light type not registered in context. Make sure to register light types before using them.", lightDefinitionName);
|
|
402
|
+
|
|
403
|
+
const wp = light.getWorldPosition(new THREE.Vector3());
|
|
404
|
+
const wd = getWorldDirection(light, new THREE.Vector3(0, 0, -1));
|
|
405
|
+
|
|
406
|
+
// Shader math from the generated MaterialX shader:
|
|
407
|
+
// float low = min(light.inner_angle, light.outer_angle);
|
|
408
|
+
// float high = light.inner_angle;
|
|
409
|
+
// float cosDir = dot(result.direction, -light.direction);
|
|
410
|
+
// float spotAttenuation = smoothstep(low, high, cosDir);
|
|
411
|
+
|
|
412
|
+
const outerAngleRad = light.angle;
|
|
413
|
+
const innerAngleRad = outerAngleRad * (1 - light.penumbra);
|
|
414
|
+
const inner_angle = Math.cos(innerAngleRad);
|
|
415
|
+
const outer_angle = Math.cos(outerAngleRad);
|
|
416
|
+
|
|
417
|
+
lightData.push({
|
|
418
|
+
type: lightTypesBound[lightDefinitionName],
|
|
419
|
+
position: wp.clone(),
|
|
420
|
+
direction: wd.clone(),
|
|
421
|
+
color: new THREE.Color().fromArray(light.color.toArray()),
|
|
422
|
+
// Luminous efficacy for converting radiant power in watts (W) to luminous flux in lumens (lm) at a wavelength of 555 nm.
|
|
423
|
+
// 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.
|
|
424
|
+
intensity: light.intensity * (light.isPointLight ? 683.0 / 3.1415 : light.isSpotLight ? 683.0 / 3.1415 : 1.0),
|
|
425
|
+
decay_rate: 2.0,
|
|
426
|
+
// Approximations for testing – the relevant light has 61.57986...129.4445 as inner/outer spot angle
|
|
427
|
+
inner_angle: inner_angle,
|
|
428
|
+
outer_angle: outer_angle,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Count the number of lights that are not empty
|
|
433
|
+
const lightCount = lightData.length;
|
|
434
|
+
|
|
435
|
+
// If we don't have enough entries in lightData, fill with empty lights
|
|
436
|
+
while (lightData.length < maxLightCount) {
|
|
437
|
+
const emptyLight = {
|
|
438
|
+
type: 0, // Default light type
|
|
439
|
+
position: new THREE.Vector3(0, 0, 0),
|
|
440
|
+
direction: new THREE.Vector3(0, 0, -1),
|
|
441
|
+
color: new THREE.Color(0, 0, 0),
|
|
442
|
+
intensity: 0.0,
|
|
443
|
+
decay_rate: 2.0,
|
|
444
|
+
inner_angle: 0.0,
|
|
445
|
+
outer_angle: 0.0,
|
|
446
|
+
};
|
|
447
|
+
lightData.push(emptyLight);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (debugUpdate) console.log("Registered lights in MaterialX context", lightTypesBound, lightData);
|
|
451
|
+
|
|
452
|
+
return { lightData, lightCount };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Get uniform values for a shader
|
|
457
|
+
*/
|
|
458
|
+
export function getUniformValues(shaderStage: MaterialX.ShaderStage, loaders: Loaders, searchPath: string) {
|
|
459
|
+
const threeUniforms = {};
|
|
460
|
+
|
|
461
|
+
const uniformBlocks = shaderStage.getUniformBlocks()
|
|
462
|
+
for (const [blockName, uniforms] of Object.entries(uniformBlocks)) {
|
|
463
|
+
// Seems struct uniforms (like in LightData) end up here as well, we should filter those out.
|
|
464
|
+
if (blockName === "LightData") continue;
|
|
465
|
+
|
|
466
|
+
if (!uniforms.empty()) {
|
|
467
|
+
for (let i = 0; i < uniforms.size(); ++i) {
|
|
468
|
+
const variable = uniforms.get(i);
|
|
469
|
+
const value = variable.getValue()?.getData();
|
|
470
|
+
const name = variable.getVariable();
|
|
471
|
+
if (debug) console.log("Adding uniform", { path: variable.getPath(), name: name, value: value, type: variable.getType().getName() });
|
|
472
|
+
threeUniforms[name] = toThreeUniform(uniforms, variable.getType().getName(), value, name, loaders, searchPath);;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return threeUniforms;
|
|
478
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { Camera, DoubleSide, FrontSide, GLSL3, MaterialParameters, Matrix3, Matrix4, Object3D, ShaderMaterial, Texture, Vector3 } from "three";
|
|
2
|
+
import { debug } from "./utils.js";
|
|
3
|
+
import { MaterialXEnvironment } from "./materialx.js";
|
|
4
|
+
import { getUniformValues, Loaders } from "./materialx.helper.js";
|
|
5
|
+
import { Context } from "@needle-tools/engine";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// Add helper matrices for uniform updates (similar to MaterialX example)
|
|
9
|
+
const identityMatrix = new Matrix4();
|
|
10
|
+
const normalMat = new Matrix3();
|
|
11
|
+
const viewProjMat = new Matrix4();
|
|
12
|
+
const worldViewPos = new Vector3();
|
|
13
|
+
|
|
14
|
+
declare type MaterialXMaterialInitParameters = {
|
|
15
|
+
name: string,
|
|
16
|
+
shader: any,
|
|
17
|
+
loaders: Loaders,
|
|
18
|
+
transparent?: boolean,
|
|
19
|
+
side?: MaterialParameters['side'],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class MaterialXMaterial extends ShaderMaterial {
|
|
23
|
+
|
|
24
|
+
// copy(source: MaterialXMaterial): this {
|
|
25
|
+
// super.copy(source);
|
|
26
|
+
// this.name = source.name;
|
|
27
|
+
// this.uniforms = { ...source.uniforms }; // Shallow copy of uniforms
|
|
28
|
+
// this.envMapIntensity = source.envMapIntensity;
|
|
29
|
+
// this.envMap = source.envMap;
|
|
30
|
+
// this.updateUniforms = source.updateUniforms; // Copy the update function
|
|
31
|
+
// return this;
|
|
32
|
+
// }
|
|
33
|
+
|
|
34
|
+
constructor(init?: MaterialXMaterialInitParameters) {
|
|
35
|
+
|
|
36
|
+
// TODO: we need to properly copy the uniforms and other properties from the source material
|
|
37
|
+
if (!init) {
|
|
38
|
+
super();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get vertex and fragment shader source, and remove #version directive for newer js.
|
|
43
|
+
// It's added by three.js glslVersion.
|
|
44
|
+
let vertexShader = init.shader.getSourceCode("vertex");
|
|
45
|
+
let fragmentShader = init.shader.getSourceCode("pixel");
|
|
46
|
+
|
|
47
|
+
vertexShader = vertexShader.replace(/^#version.*$/gm, '').trim();
|
|
48
|
+
fragmentShader = fragmentShader.replace(/^#version.*$/gm, '').trim();
|
|
49
|
+
|
|
50
|
+
// MaterialX uses different attribute names than js defaults,
|
|
51
|
+
// so we patch the MaterialX shaders to match the js standard names.
|
|
52
|
+
// Otherwise, we'd have to modify the mesh attributes (see original MaterialX for reference).
|
|
53
|
+
|
|
54
|
+
// Patch vertexShader
|
|
55
|
+
vertexShader = vertexShader.replace(/\bi_position\b/g, 'position');
|
|
56
|
+
vertexShader = vertexShader.replace(/\bi_normal\b/g, 'normal');
|
|
57
|
+
vertexShader = vertexShader.replace(/\bi_texcoord_0\b/g, 'uv');
|
|
58
|
+
vertexShader = vertexShader.replace(/\bi_texcoord_1\b/g, 'uv1');
|
|
59
|
+
vertexShader = vertexShader.replace(/\bi_tangent\b/g, 'tangent');
|
|
60
|
+
vertexShader = vertexShader.replace(/\bi_color_0\b/g, 'color');
|
|
61
|
+
|
|
62
|
+
// Patch fragmentShader
|
|
63
|
+
fragmentShader = fragmentShader.replace(/\bi_position\b/g, 'position');
|
|
64
|
+
fragmentShader = fragmentShader.replace(/\bi_normal\b/g, 'normal');
|
|
65
|
+
fragmentShader = fragmentShader.replace(/\bi_texcoord_0\b/g, 'uv');
|
|
66
|
+
fragmentShader = fragmentShader.replace(/\bi_texcoord_1\b/g, 'uv1');
|
|
67
|
+
fragmentShader = fragmentShader.replace(/\bi_tangent\b/g, 'tangent');
|
|
68
|
+
fragmentShader = fragmentShader.replace(/\bi_color_0\b/g, 'color');
|
|
69
|
+
|
|
70
|
+
// Remove `in vec3 position;` and so on since they're already declared by ShaderMaterial
|
|
71
|
+
vertexShader = vertexShader.replace(/in\s+vec3\s+position;/g, '');
|
|
72
|
+
vertexShader = vertexShader.replace(/in\s+vec3\s+normal;/g, '');
|
|
73
|
+
vertexShader = vertexShader.replace(/in\s+vec3\s+uv;/g, '');
|
|
74
|
+
vertexShader = vertexShader.replace(/in\s+vec3\s+uv1;/g, '');
|
|
75
|
+
vertexShader = vertexShader.replace(/in\s+vec4\s+tangent;/g, '');
|
|
76
|
+
vertexShader = vertexShader.replace(/in\s+vec4\s+color;/g, '');
|
|
77
|
+
|
|
78
|
+
// Patch uv 2-component to 3-component (`texcoord_0 = uv;` needs to be replaced with `texcoord_0 = vec3(uv, 0.0);`)
|
|
79
|
+
// TODO what if we actually have a 3-component UV? Not sure what three.js does then
|
|
80
|
+
vertexShader = vertexShader.replace(/texcoord_0 = uv;/g, 'texcoord_0 = vec3(uv, 0.0);');
|
|
81
|
+
|
|
82
|
+
// Patch units – seems MaterialX uses different units and we end up with wrong light values?
|
|
83
|
+
// result.direction = light.position - position;
|
|
84
|
+
fragmentShader = fragmentShader.replace(
|
|
85
|
+
/result\.direction\s*=\s*light\.position\s*-\s*position;/g,
|
|
86
|
+
'result.direction = (light.position - position) * 10.0 / 1.0;');
|
|
87
|
+
|
|
88
|
+
// Add tonemapping and colorspace handling
|
|
89
|
+
// Replace `out vec4 out1;` with `out vec4 gl_FragColor;`
|
|
90
|
+
fragmentShader = fragmentShader.replace(
|
|
91
|
+
/out\s+vec4\s+out1;/,
|
|
92
|
+
'layout(location = 0) out vec4 pc_fragColor;\n#define gl_FragColor pc_fragColor');
|
|
93
|
+
|
|
94
|
+
// Replace `out1 = vec4(<CAPTURE>)` with `gl_FragColor = vec4(<CAPTURE>)` and tonemapping/colorspace handling
|
|
95
|
+
fragmentShader = fragmentShader.replace(/^\s*out1\s*=\s*vec4\((.*)\);/gm, `
|
|
96
|
+
gl_FragColor = vec4($1);
|
|
97
|
+
#include <tonemapping_fragment>
|
|
98
|
+
#include <colorspace_fragment>`);
|
|
99
|
+
|
|
100
|
+
const searchPath = ""; // Could be derived from the asset path if needed
|
|
101
|
+
const isTransparent = init.transparent ?? false;
|
|
102
|
+
super({
|
|
103
|
+
name: init.name,
|
|
104
|
+
uniforms: {},
|
|
105
|
+
vertexShader: vertexShader,
|
|
106
|
+
fragmentShader: fragmentShader,
|
|
107
|
+
glslVersion: GLSL3,
|
|
108
|
+
transparent: isTransparent,
|
|
109
|
+
side: init.side ? init.side : FrontSide,
|
|
110
|
+
depthTest: true,
|
|
111
|
+
depthWrite: !isTransparent,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
Object.assign(this.uniforms, {
|
|
115
|
+
...getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath),
|
|
116
|
+
...getUniformValues(init.shader.getStage('pixel'), init.loaders, searchPath),
|
|
117
|
+
u_envMatrix: { value: new Matrix4() },
|
|
118
|
+
u_envRadiance: { value: null, type: 't' },
|
|
119
|
+
u_envRadianceMips: { value: 8, type: 'i' },
|
|
120
|
+
// TODO we need to figure out how we can set a PMREM here... doing many texture samples is prohibitively expensive
|
|
121
|
+
u_envRadianceSamples: { value: 8, type: 'i' },
|
|
122
|
+
u_envIrradiance: { value: null, type: 't' },
|
|
123
|
+
u_refractionEnv: { value: true },
|
|
124
|
+
u_numActiveLightSources: { value: 0 },
|
|
125
|
+
u_lightData: { value: [] }, // Array of light data
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (debug) {
|
|
129
|
+
// Get lighting and environment data from MaterialX environment
|
|
130
|
+
console.group("[MaterialX]: ", name);
|
|
131
|
+
console.log("Vertex shader length:", vertexShader.length, vertexShader);
|
|
132
|
+
console.log("Fragment shader length:", fragmentShader.length, fragmentShader);
|
|
133
|
+
console.groupEnd();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
envMapIntensity: number = 1.0; // Default intensity for environment map
|
|
140
|
+
envMap: Texture | null = null; // Environment map texture, can be set externally
|
|
141
|
+
updateUniforms = (context: Context, environment: MaterialXEnvironment, object: Object3D, camera: Camera) => {
|
|
142
|
+
|
|
143
|
+
const uniforms = this.uniforms;
|
|
144
|
+
|
|
145
|
+
// TODO remove. Not sure why this is needed, but without it
|
|
146
|
+
// we currently get some "swimming" where matrices are not up to date.
|
|
147
|
+
camera.updateMatrixWorld(true);
|
|
148
|
+
|
|
149
|
+
// Update standard transformation matrices
|
|
150
|
+
if (uniforms.u_worldMatrix) {
|
|
151
|
+
if (!uniforms.u_worldMatrix.value?.isMatrix4) uniforms.u_worldMatrix.value = new Matrix4();
|
|
152
|
+
uniforms.u_worldMatrix.value = object.matrixWorld;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (uniforms.u_viewProjectionMatrix) {
|
|
156
|
+
if (!uniforms.u_viewProjectionMatrix.value?.isMatrix4) uniforms.u_viewProjectionMatrix.value = new Matrix4();
|
|
157
|
+
uniforms.u_viewProjectionMatrix.value.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (uniforms.u_viewPosition) {
|
|
161
|
+
if (!uniforms.u_viewPosition.value?.isVector3) uniforms.u_viewPosition.value = new Vector3();
|
|
162
|
+
uniforms.u_viewPosition.value.copy(camera.getWorldPosition(worldViewPos));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (uniforms.u_worldInverseTransposeMatrix) {
|
|
166
|
+
if (!uniforms.u_worldInverseTransposeMatrix.value?.isMatrix4) uniforms.u_worldInverseTransposeMatrix.value = new Matrix4();
|
|
167
|
+
uniforms.u_worldInverseTransposeMatrix.value.setFromMatrix3(normalMat.getNormalMatrix(object.matrixWorld));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Update time uniforms
|
|
171
|
+
if (uniforms.u_time) {
|
|
172
|
+
uniforms.u_time.value = context.time.time;
|
|
173
|
+
}
|
|
174
|
+
if (uniforms.u_frame) {
|
|
175
|
+
uniforms.u_frame.value = context.time.frame;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Update light uniforms
|
|
179
|
+
this.updateEnvironmentUniforms(environment);
|
|
180
|
+
|
|
181
|
+
this.uniformsNeedUpdate = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private updateEnvironmentUniforms = (environment: MaterialXEnvironment) => {
|
|
185
|
+
|
|
186
|
+
// Get lighting data from environment
|
|
187
|
+
const lightData = environment.lightData || null;
|
|
188
|
+
const lightCount = environment.lightCount || 0;
|
|
189
|
+
const textures = environment.getTextures(this) || null;
|
|
190
|
+
|
|
191
|
+
// Update light count
|
|
192
|
+
if (this.uniforms.u_numActiveLightSources && lightCount >= 0) {
|
|
193
|
+
this.uniforms.u_numActiveLightSources.value = lightCount;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Update light data
|
|
197
|
+
if (lightData) {
|
|
198
|
+
this.uniforms.u_lightData.value = lightData;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Update environment uniforms
|
|
202
|
+
if (this.uniforms.u_envMatrix) {
|
|
203
|
+
this.uniforms.u_envMatrix.value = identityMatrix;
|
|
204
|
+
}
|
|
205
|
+
if (this.uniforms.u_envRadiance) {
|
|
206
|
+
this.uniforms.u_envRadiance.value = textures.radianceTexture || null;
|
|
207
|
+
}
|
|
208
|
+
if (this.uniforms.u_envRadianceMips) {
|
|
209
|
+
this.uniforms.u_envRadianceMips.value = Math.trunc(Math.log2(Math.max(textures.radianceTexture?.source.data.width ?? 0, textures.radianceTexture?.source.data.height ?? 0))) + 1;
|
|
210
|
+
}
|
|
211
|
+
if (this.uniforms.u_envIrradiance) {
|
|
212
|
+
this.uniforms.u_envIrradiance.value = textures.irradianceTexture;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.uniformsNeedUpdate = true;
|
|
216
|
+
}
|
|
217
|
+
}
|