@needle-tools/materialx 1.0.7 → 1.1.0-next.2d4dc5e
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 +3 -0
- package/README.md +17 -3
- package/index.ts +2 -5
- package/needle.ts +2 -0
- package/package.json +7 -3
- package/src/index.ts +0 -1
- package/src/loader/loader.needle.ts +19 -27
- package/src/loader/loader.three.ts +30 -5
- package/src/materialx.helper.ts +57 -13
- package/src/materialx.material.ts +24 -21
- package/src/materialx.ts +26 -27
- package/src/textureHelper.ts +1 -1
- package/src/utils.ts +22 -87
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,9 @@ All notable changes to this package will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.1.0] - 2025-07-15
|
|
8
|
+
- Add: `useNeedleMaterialX` hooks for vanilla three.js and Needle Engine
|
|
9
|
+
|
|
7
10
|
## [1.0.6] - 2025-07-15
|
|
8
11
|
- Fix: texture/environment sampling on some Android devices
|
|
9
12
|
|
package/README.md
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
# Needle MaterialX
|
|
2
2
|
|
|
3
|
-
Web runtime support to load and display MaterialX materials in Needle Engine
|
|
3
|
+
Web runtime support to load and display MaterialX materials in Needle Engine and three.js
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
`npm i @needle-tools/materialx`
|
|
7
7
|
|
|
8
|
+
## Examples
|
|
9
|
+
- [three.js Example on Stackblitz](https://stackblitz.com/edit/needle-materialx-example?file=main.js,package.json,index.html)
|
|
10
|
+
|
|
8
11
|
## How to use
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
### Use with Needle Engine
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { useNeedleMaterialX } from "@needle-tools/materialx/needle";
|
|
17
|
+
// Simply call this function in global scope as soon as possible
|
|
18
|
+
useNeedleMaterialX();
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Use with three.js
|
|
11
22
|
|
|
12
23
|
```ts
|
|
13
|
-
import "@needle-tools/materialx"
|
|
24
|
+
import { useNeedleMaterialX } from "@needle-tools/materialx";
|
|
25
|
+
// Call the function with your GLTFLoader instance
|
|
26
|
+
useNeedleMaterialX(<yourGltfLoaderInstance>);
|
|
14
27
|
```
|
|
15
28
|
|
|
29
|
+
|
|
16
30
|
<br />
|
|
17
31
|
|
|
18
32
|
# Contact ✒️
|
package/index.ts
CHANGED
package/needle.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/materialx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.1.0-next.2d4dc5e",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -12,11 +12,15 @@
|
|
|
12
12
|
"./codegen/register_types.ts": {
|
|
13
13
|
"import": "./codegen/register_types.ts",
|
|
14
14
|
"require": "./codegen/register_types.js"
|
|
15
|
+
},
|
|
16
|
+
"./needle": {
|
|
17
|
+
"import": "./needle.ts",
|
|
18
|
+
"require": "./needle.js"
|
|
15
19
|
}
|
|
16
20
|
},
|
|
17
21
|
"peerDependencies": {
|
|
18
22
|
"@needle-tools/engine": "4.x || ^4.6.0-0",
|
|
19
|
-
"three": "
|
|
23
|
+
"three": ">=0.169.0"
|
|
20
24
|
},
|
|
21
25
|
"devDependencies": {
|
|
22
26
|
"@needle-tools/engine": "4.x",
|
|
@@ -38,4 +42,4 @@
|
|
|
38
42
|
"mtlx",
|
|
39
43
|
"rendering"
|
|
40
44
|
]
|
|
41
|
-
}
|
|
45
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { Context, GLTF, addCustomExtensionPlugin, INeedleGLTFExtensionPlugin } from "@needle-tools/engine";
|
|
1
|
+
import { addCustomExtensionPlugin } from "@needle-tools/engine";
|
|
2
|
+
import { Context, GLTF, INeedleGLTFExtensionPlugin } from "@needle-tools/engine";
|
|
4
3
|
import type { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
5
4
|
import type { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter.js";
|
|
6
|
-
import { MaterialXLoader } from "./loader.three.js";
|
|
5
|
+
import { useNeedleMaterialX as _useNeedleMaterialX, MaterialXLoader } from "./loader.three.js";
|
|
7
6
|
import { debug } from "../utils.js";
|
|
7
|
+
import { MaterialParameters } from "three";
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
|
|
10
11
|
readonly name = "MaterialXLoaderPlugin";
|
|
@@ -13,39 +14,30 @@ export class MaterialXLoaderPlugin implements INeedleGLTFExtensionPlugin {
|
|
|
13
14
|
|
|
14
15
|
onImport = (loader: GLTFLoader, url: string, context: Context) => {
|
|
15
16
|
if (debug) console.log("MaterialXLoaderPlugin: Registering MaterialX extension for", url);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
getRenderer: () => context.renderer,
|
|
25
|
-
});
|
|
26
|
-
return this.loader;
|
|
17
|
+
_useNeedleMaterialX(loader, {
|
|
18
|
+
cacheKey: url,
|
|
19
|
+
parameters: {
|
|
20
|
+
precision: context.renderer.capabilities.getMaxPrecision("highp") as MaterialParameters["precision"],
|
|
21
|
+
}
|
|
22
|
+
}, {
|
|
23
|
+
getTime: () => context.time.time,
|
|
24
|
+
getFrame: () => context.time.frame,
|
|
27
25
|
});
|
|
28
26
|
};
|
|
29
27
|
|
|
30
28
|
onLoaded = (url: string, gltf: GLTF, _context: Context) => {
|
|
31
29
|
if (debug) console.log("[MaterialX] MaterialXLoaderPlugin: glTF loaded", { url, scene: gltf.scene, materialX_root_data: this.loader?.materialX_root_data });
|
|
32
|
-
|
|
33
|
-
// If we don't have MaterialX data in the loaded glTF we don't need to do anything else here
|
|
34
|
-
if (!this.loader?.materialX_root_data) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (debug) console.log("[MaterialX] Loaded: ", this.loader);
|
|
39
|
-
|
|
40
|
-
|
|
41
30
|
};
|
|
42
31
|
|
|
43
32
|
onExport = (_exporter: GLTFExporter, _context: Context) => {
|
|
44
33
|
console.warn("[MaterialX] Export is not supported");
|
|
45
|
-
// TODO: Add MaterialX export functionality if needed
|
|
46
34
|
};
|
|
47
35
|
}
|
|
48
36
|
|
|
49
|
-
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add the MaterialXLoaderPlugin to the Needle Engine.
|
|
40
|
+
*/
|
|
41
|
+
export async function useNeedleMaterialX() {
|
|
50
42
|
addCustomExtensionPlugin(new MaterialXLoaderPlugin());
|
|
51
|
-
}
|
|
43
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Material, MeshStandardMaterial, DoubleSide, FrontSide } from "three";
|
|
2
|
-
import { GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
1
|
+
import { Material, MeshStandardMaterial, DoubleSide, FrontSide, MaterialParameters } from "three";
|
|
2
|
+
import { GLTFLoader, GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
3
|
import { ready, state, MaterialXContext } from "../materialx.js";
|
|
4
4
|
import { debug } from "../utils.js";
|
|
5
5
|
import { MaterialXMaterial } from "../materialx.material.js";
|
|
@@ -28,6 +28,14 @@ type MaterialDefinition = {
|
|
|
28
28
|
},
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// init.context.getRenderer().capabilities.getMaxPrecision("highp")
|
|
32
|
+
export type MaterialXLoaderOptions = {
|
|
33
|
+
/** The URL of the GLTF file being loaded */
|
|
34
|
+
cacheKey?: string;
|
|
35
|
+
/** Parameters for the MaterialX loader */
|
|
36
|
+
parameters?: Pick<MaterialParameters, "precision">;
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
// MaterialX loader extension for js GLTFLoader
|
|
32
40
|
export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
33
41
|
readonly name = "NEEDLE_materials_mtlx";
|
|
@@ -48,10 +56,14 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
48
56
|
/**
|
|
49
57
|
* MaterialXLoader constructor
|
|
50
58
|
* @param parser The GLTFParser instance
|
|
51
|
-
* @param
|
|
59
|
+
* @param cacheKey The URL of the GLTF file
|
|
52
60
|
* @param context The context for the GLTF loading process
|
|
53
61
|
*/
|
|
54
|
-
constructor(
|
|
62
|
+
constructor(
|
|
63
|
+
private parser: GLTFParser,
|
|
64
|
+
private options: MaterialXLoaderOptions,
|
|
65
|
+
private context: MaterialXContext
|
|
66
|
+
) {
|
|
55
67
|
if (debug) console.log("MaterialXLoader created for parser");
|
|
56
68
|
// Start loading of MaterialX environment if the root extension exists
|
|
57
69
|
if (this.materialX_root_data) {
|
|
@@ -59,6 +71,7 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
59
71
|
}
|
|
60
72
|
}
|
|
61
73
|
|
|
74
|
+
|
|
62
75
|
loadMaterial(materialIndex: number): Promise<Material> | null {
|
|
63
76
|
const materialDef = this.parser.json.materials?.[materialIndex];
|
|
64
77
|
if (!materialDef?.extensions?.[this.name]) {
|
|
@@ -232,8 +245,9 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
232
245
|
transparent: isTransparent,
|
|
233
246
|
side: material_def.doubleSided ? DoubleSide : FrontSide,
|
|
234
247
|
context: this.context,
|
|
248
|
+
precision: this.options.parameters?.precision,
|
|
235
249
|
loaders: {
|
|
236
|
-
cacheKey: this.
|
|
250
|
+
cacheKey: this.options.cacheKey || "",
|
|
237
251
|
getTexture: async url => {
|
|
238
252
|
// Find the index of the texture in the parser
|
|
239
253
|
const filenameWithoutExt = url.split('/').pop()?.split('.').shift() || '';
|
|
@@ -292,3 +306,14 @@ export class MaterialXLoader implements GLTFLoaderPlugin {
|
|
|
292
306
|
}
|
|
293
307
|
}
|
|
294
308
|
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Add the MaterialXLoader to the GLTFLoader instance.
|
|
313
|
+
*/
|
|
314
|
+
export function useNeedleMaterialX(loader: GLTFLoader, options?: MaterialXLoaderOptions, context?: MaterialXContext) {
|
|
315
|
+
loader.register(p => {
|
|
316
|
+
const loader = new MaterialXLoader(p, options || {}, context || {});
|
|
317
|
+
return loader;
|
|
318
|
+
});
|
|
319
|
+
}
|
package/src/materialx.helper.ts
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
// Copyright Contributors to the MaterialX Project
|
|
3
3
|
// SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
//
|
|
5
|
-
|
|
6
|
-
import { getWorldDirection } from '@needle-tools/engine';
|
|
7
5
|
import * as THREE from 'three';
|
|
8
|
-
import { debug, debugUpdate } from './utils';
|
|
9
|
-
import { MaterialX } from './materialx.types';
|
|
6
|
+
import { debug, debugUpdate } from './utils.js';
|
|
7
|
+
import { MaterialX } from './materialx.types.js';
|
|
10
8
|
|
|
11
9
|
const IMAGE_PROPERTY_SEPARATOR = "_";
|
|
12
10
|
const UADDRESS_MODE_SUFFIX = IMAGE_PROPERTY_SEPARATOR + "uaddressmode";
|
|
@@ -384,14 +382,14 @@ export type LightData = {
|
|
|
384
382
|
/**
|
|
385
383
|
* Update light data for shader uniforms
|
|
386
384
|
*/
|
|
387
|
-
export function getLightData(lights:
|
|
385
|
+
export function getLightData(lights: Array<THREE.Light>, genContext: any): { lightData: LightData[], lightCount: number } {
|
|
388
386
|
const lightData = new Array();
|
|
389
387
|
const maxLightCount = genContext.getOptions().hwMaxActiveLightSources;
|
|
390
388
|
|
|
391
389
|
// Three.js lights
|
|
392
390
|
for (let light of lights) {
|
|
393
391
|
// Skip if light is not a Three.js light
|
|
394
|
-
if (!light
|
|
392
|
+
if (!light?.isLight) continue;
|
|
395
393
|
|
|
396
394
|
// Types in MaterialX: point_light, directional_light, spot_light
|
|
397
395
|
|
|
@@ -401,7 +399,8 @@ export function getLightData(lights: any, genContext: any): { lightData: LightDa
|
|
|
401
399
|
console.error("MaterialX: Light type not registered in context. Make sure to register light types before using them.", lightDefinitionName);
|
|
402
400
|
|
|
403
401
|
const wp = light.getWorldPosition(new THREE.Vector3());
|
|
404
|
-
const
|
|
402
|
+
const wq = light.getWorldQuaternion(new THREE.Quaternion());
|
|
403
|
+
const wd = new THREE.Vector3(0, 0, -1).applyQuaternion(wq);
|
|
405
404
|
|
|
406
405
|
// Shader math from the generated MaterialX shader:
|
|
407
406
|
// float low = min(light.inner_angle, light.outer_angle);
|
|
@@ -409,8 +408,8 @@ export function getLightData(lights: any, genContext: any): { lightData: LightDa
|
|
|
409
408
|
// float cosDir = dot(result.direction, -light.direction);
|
|
410
409
|
// float spotAttenuation = smoothstep(low, high, cosDir);
|
|
411
410
|
|
|
412
|
-
const outerAngleRad = light.angle;
|
|
413
|
-
const innerAngleRad = outerAngleRad * (1 - light.penumbra);
|
|
411
|
+
const outerAngleRad = (light as THREE.SpotLight).angle;
|
|
412
|
+
const innerAngleRad = outerAngleRad * (1 - (light as THREE.SpotLight).penumbra);
|
|
414
413
|
const inner_angle = Math.cos(innerAngleRad);
|
|
415
414
|
const outer_angle = Math.cos(outerAngleRad);
|
|
416
415
|
|
|
@@ -421,7 +420,7 @@ export function getLightData(lights: any, genContext: any): { lightData: LightDa
|
|
|
421
420
|
color: new THREE.Color().fromArray(light.color.toArray()),
|
|
422
421
|
// Luminous efficacy for converting radiant power in watts (W) to luminous flux in lumens (lm) at a wavelength of 555 nm.
|
|
423
422
|
// 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),
|
|
423
|
+
intensity: light.intensity * ((light as THREE.PointLight).isPointLight ? 683.0 / 3.1415 : (light as THREE.SpotLight).isSpotLight ? 683.0 / 3.1415 : 1.0),
|
|
425
424
|
decay_rate: 2.0,
|
|
426
425
|
// Approximations for testing – the relevant light has 61.57986...129.4445 as inner/outer spot angle
|
|
427
426
|
inner_angle: inner_angle,
|
|
@@ -467,12 +466,57 @@ export function getUniformValues(shaderStage: MaterialX.ShaderStage, loaders: Lo
|
|
|
467
466
|
for (let i = 0; i < uniforms.size(); ++i) {
|
|
468
467
|
const variable = uniforms.get(i);
|
|
469
468
|
const value = variable.getValue()?.getData();
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
469
|
+
const uniformName = variable.getVariable();
|
|
470
|
+
const type = variable.getType().getName();
|
|
471
|
+
if (debug) console.log("Adding uniform", { path: variable.getPath(), name: uniformName, value: value, type: type });
|
|
472
|
+
threeUniforms[uniformName] = toThreeUniform(uniforms, type, value, uniformName, loaders, searchPath);
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
return threeUniforms;
|
|
478
478
|
}
|
|
479
|
+
|
|
480
|
+
export function generateMaterialPropertiesForUniforms(material: THREE.ShaderMaterial, shaderStage: MaterialX.ShaderStage) {
|
|
481
|
+
|
|
482
|
+
const uniformBlocks = shaderStage.getUniformBlocks()
|
|
483
|
+
for (const [blockName, uniforms] of Object.entries(uniformBlocks)) {
|
|
484
|
+
// Seems struct uniforms (like in LightData) end up here as well, we should filter those out.
|
|
485
|
+
if (blockName === "LightData") continue;
|
|
486
|
+
|
|
487
|
+
if (!uniforms.empty()) {
|
|
488
|
+
for (let i = 0; i < uniforms.size(); ++i) {
|
|
489
|
+
const variable = uniforms.get(i);
|
|
490
|
+
const uniformName = variable.getVariable();
|
|
491
|
+
let key = variable.getPath().split('/').pop();
|
|
492
|
+
switch (key) {
|
|
493
|
+
case "_Color":
|
|
494
|
+
key = "color";
|
|
495
|
+
break;
|
|
496
|
+
case "_Roughness":
|
|
497
|
+
key = "roughness";
|
|
498
|
+
break;
|
|
499
|
+
case "_Metallic":
|
|
500
|
+
key = "metalness";
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
if (key) {
|
|
504
|
+
Object.defineProperty(material, key, {
|
|
505
|
+
get: () => {
|
|
506
|
+
return material.uniforms?.[uniformName].value
|
|
507
|
+
},
|
|
508
|
+
set: (v) => {
|
|
509
|
+
const uniforms = material.uniforms;
|
|
510
|
+
if (!uniforms || !uniforms[uniformName]) {
|
|
511
|
+
console.warn(`[MaterialX] Uniform ${uniformName} not found in ${material.name} uniforms`);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
material.uniforms[uniformName].value = v;
|
|
515
|
+
material.uniformsNeedUpdate = true;
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { BufferGeometry, Camera, FrontSide, GLSL3, Group, IUniform, MaterialParameters, Matrix3, Matrix4, Object3D, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
|
|
2
|
-
import { debug } from "./utils.js";
|
|
2
|
+
import { debug, getFrame, getTime } from "./utils.js";
|
|
3
3
|
import { MaterialXContext, MaterialXEnvironment } from "./materialx.js";
|
|
4
|
-
import { getUniformValues, Loaders } from "./materialx.helper.js";
|
|
4
|
+
import { generateMaterialPropertiesForUniforms, getUniformValues, Loaders } from "./materialx.helper.js";
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
// Add helper matrices for uniform updates (similar to MaterialX example)
|
|
8
|
-
const identityMatrix = new Matrix4();
|
|
9
8
|
const normalMat = new Matrix3();
|
|
10
|
-
const viewProjMat = new Matrix4();
|
|
11
9
|
const worldViewPos = new Vector3();
|
|
12
10
|
|
|
13
11
|
declare type MaterialXMaterialInitParameters = {
|
|
14
12
|
name: string,
|
|
15
13
|
shader: any,
|
|
16
14
|
loaders: Loaders,
|
|
15
|
+
context: MaterialXContext,
|
|
16
|
+
// Optional parameters
|
|
17
17
|
transparent?: boolean,
|
|
18
18
|
side?: MaterialParameters['side'],
|
|
19
|
-
|
|
19
|
+
precision?: MaterialParameters['precision'],
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
type Uniforms = Record<string, IUniform & { needsUpdate?: boolean }>;
|
|
@@ -63,7 +63,7 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
63
63
|
vertexShader = vertexShader.replace(/\bi_color_0\b/g, 'color');
|
|
64
64
|
|
|
65
65
|
// Patch fragmentShader
|
|
66
|
-
const precision = init.
|
|
66
|
+
const precision = init.precision || "highp" as Precision;
|
|
67
67
|
fragmentShader = fragmentShader.replace(/precision mediump float;/g, `precision ${precision} float;`);
|
|
68
68
|
fragmentShader = fragmentShader.replace(/#define M_FLOAT_EPS 1e-8/g, precision === "highp" ? `#define M_FLOAT_EPS 1e-8` : `#define M_FLOAT_EPS 1e-3`);
|
|
69
69
|
|
|
@@ -137,6 +137,7 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
137
137
|
depthWrite: !isTransparent,
|
|
138
138
|
defines: defines,
|
|
139
139
|
});
|
|
140
|
+
this._context = init.context;
|
|
140
141
|
|
|
141
142
|
Object.assign(this.uniforms, {
|
|
142
143
|
...getUniformValues(init.shader.getStage('vertex'), init.loaders, searchPath),
|
|
@@ -158,7 +159,9 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
158
159
|
u_lightData: { value: [], needsUpdate: false }, // Array of light data. We need to set needsUpdate to false until we actually update it
|
|
159
160
|
});
|
|
160
161
|
|
|
161
|
-
this
|
|
162
|
+
generateMaterialPropertiesForUniforms(this, init.shader.getStage('pixel'));
|
|
163
|
+
generateMaterialPropertiesForUniforms(this, init.shader.getStage('vertex'));
|
|
164
|
+
|
|
162
165
|
|
|
163
166
|
if (debug) {
|
|
164
167
|
// Get lighting and environment data from MaterialX environment
|
|
@@ -170,19 +173,19 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
170
173
|
}
|
|
171
174
|
|
|
172
175
|
onBeforeRender(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, _geometry: BufferGeometry, object: Object3D, _group: Group): void {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
const time = this._context?.getTime?.() || getTime();
|
|
177
|
+
const frame = this._context?.getFrame?.() || getFrame();
|
|
178
|
+
const env = MaterialXEnvironment.get(_scene);
|
|
179
|
+
if (env) {
|
|
180
|
+
env.update(frame, _scene, _renderer);
|
|
181
|
+
this.updateUniforms(env, object, camera, time, frame);
|
|
179
182
|
}
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
|
|
183
186
|
envMapIntensity: number = 1.0; // Default intensity for environment map
|
|
184
187
|
envMap: Texture | null = null; // Environment map texture, can be set externally
|
|
185
|
-
updateUniforms = (environment: MaterialXEnvironment, object: Object3D, camera: Camera) => {
|
|
188
|
+
updateUniforms = (environment: MaterialXEnvironment, object: Object3D, camera: Camera, time?: number, frame?: number) => {
|
|
186
189
|
|
|
187
190
|
const uniforms = this.uniforms as Uniforms;
|
|
188
191
|
|
|
@@ -208,13 +211,13 @@ export class MaterialXMaterial extends ShaderMaterial {
|
|
|
208
211
|
}
|
|
209
212
|
|
|
210
213
|
// Update time uniforms
|
|
211
|
-
if (
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
if (uniforms.u_time) {
|
|
215
|
+
if (time === undefined) time = getTime();
|
|
216
|
+
uniforms.u_time.value = time;
|
|
217
|
+
}
|
|
218
|
+
if (uniforms.u_frame) {
|
|
219
|
+
if (frame === undefined) frame = getFrame();
|
|
220
|
+
uniforms.u_frame.value = frame;
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
// Update light uniforms
|
package/src/materialx.ts
CHANGED
|
@@ -7,10 +7,8 @@ import { registerLights, getLightData, LightData } from "./materialx.helper.js";
|
|
|
7
7
|
import type { MaterialXMaterial } from "./materialx.material.js";
|
|
8
8
|
|
|
9
9
|
export type MaterialXContext = {
|
|
10
|
-
getTime(): number,
|
|
11
|
-
getFrame(): number,
|
|
12
|
-
getScene(): Scene,
|
|
13
|
-
getRenderer(): WebGLRenderer,
|
|
10
|
+
getTime?(): number,
|
|
11
|
+
getFrame?(): number,
|
|
14
12
|
}
|
|
15
13
|
|
|
16
14
|
|
|
@@ -114,23 +112,21 @@ type EnvironmentTextureSet = {
|
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
type EnvironmentContext = Pick<MaterialXContext, "getRenderer" | "getScene">;
|
|
119
115
|
/**
|
|
120
116
|
* MaterialXEnvironment manages the environment settings for MaterialX materials.
|
|
121
117
|
*/
|
|
122
118
|
export class MaterialXEnvironment {
|
|
123
119
|
|
|
124
|
-
static get(
|
|
125
|
-
return this.getEnvironment(
|
|
120
|
+
static get(scene: Scene): MaterialXEnvironment | null {
|
|
121
|
+
return this.getEnvironment(scene);
|
|
126
122
|
}
|
|
127
|
-
private static _environments: WeakMap<
|
|
128
|
-
private static getEnvironment(
|
|
129
|
-
if (this._environments.has(
|
|
130
|
-
return this._environments.get(
|
|
123
|
+
private static _environments: WeakMap<Scene, MaterialXEnvironment> = new Map();
|
|
124
|
+
private static getEnvironment(scene: Scene): MaterialXEnvironment {
|
|
125
|
+
if (this._environments.has(scene)) {
|
|
126
|
+
return this._environments.get(scene)!;
|
|
131
127
|
}
|
|
132
|
-
const env = new MaterialXEnvironment(
|
|
133
|
-
this._environments.set(
|
|
128
|
+
const env = new MaterialXEnvironment(scene);
|
|
129
|
+
this._environments.set(scene, env);
|
|
134
130
|
return env;
|
|
135
131
|
}
|
|
136
132
|
|
|
@@ -143,22 +139,22 @@ export class MaterialXEnvironment {
|
|
|
143
139
|
private _isInitialized: boolean = false;
|
|
144
140
|
private _lastUpdateFrame: number = -1;
|
|
145
141
|
|
|
146
|
-
constructor(private
|
|
142
|
+
constructor(private _scene: Scene) {
|
|
147
143
|
if (debug) console.log("[MaterialX] Environment created");
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
// Initialize with Needle Engine context
|
|
151
|
-
async initialize(): Promise<boolean> {
|
|
147
|
+
async initialize(renderer: WebGLRenderer): Promise<boolean> {
|
|
152
148
|
if (this._initializePromise) {
|
|
153
149
|
return this._initializePromise;
|
|
154
150
|
}
|
|
155
|
-
this._initializePromise = this._initialize();
|
|
151
|
+
this._initializePromise = this._initialize(renderer);
|
|
156
152
|
return this._initializePromise;
|
|
157
153
|
}
|
|
158
154
|
|
|
159
|
-
update(frame: number, scene: Scene) {
|
|
155
|
+
update(frame: number, scene: Scene, renderer: WebGLRenderer): void {
|
|
160
156
|
if (!this._initializePromise) {
|
|
161
|
-
this.initialize();
|
|
157
|
+
this.initialize(renderer);
|
|
162
158
|
return;
|
|
163
159
|
}
|
|
164
160
|
if (!this._isInitialized) {
|
|
@@ -205,6 +201,7 @@ export class MaterialXEnvironment {
|
|
|
205
201
|
this._lightCount = 0;
|
|
206
202
|
this._pmremGenerator?.dispose();
|
|
207
203
|
this._pmremGenerator = null;
|
|
204
|
+
this._renderer = null;
|
|
208
205
|
for (const textureSet of this._texturesCache.values()) {
|
|
209
206
|
textureSet.radianceTexture?.dispose();
|
|
210
207
|
textureSet.irradianceTexture?.dispose();
|
|
@@ -221,15 +218,17 @@ export class MaterialXEnvironment {
|
|
|
221
218
|
// If the material has its own envMap, we don't use the irradiance texture
|
|
222
219
|
return this._getTextures(material.envMap);
|
|
223
220
|
}
|
|
224
|
-
return this._getTextures(this.
|
|
221
|
+
return this._getTextures(this._scene.environment);
|
|
225
222
|
}
|
|
226
223
|
|
|
227
224
|
private _pmremGenerator: PMREMGenerator | null = null;
|
|
225
|
+
private _renderer: WebGLRenderer | null = null;
|
|
228
226
|
private readonly _texturesCache: Map<Texture | null, EnvironmentTextureSet> = new Map();
|
|
229
227
|
|
|
230
|
-
private async _initialize(): Promise<boolean> {
|
|
228
|
+
private async _initialize(renderer: WebGLRenderer): Promise<boolean> {
|
|
231
229
|
this._isInitialized = false;
|
|
232
|
-
this._pmremGenerator = new PMREMGenerator(
|
|
230
|
+
this._pmremGenerator = new PMREMGenerator(renderer);
|
|
231
|
+
this._renderer = renderer;
|
|
233
232
|
this.updateLighting(true);
|
|
234
233
|
this._isInitialized = true;
|
|
235
234
|
return true;
|
|
@@ -244,11 +243,11 @@ export class MaterialXEnvironment {
|
|
|
244
243
|
return res;
|
|
245
244
|
}
|
|
246
245
|
|
|
247
|
-
if (this.
|
|
246
|
+
if (this._scene && this._pmremGenerator && this._renderer && texture) {
|
|
248
247
|
if (debug) console.log("[MaterialX] Generating environment textures", texture.name);
|
|
249
248
|
const target = this._pmremGenerator.fromEquirectangular(texture);
|
|
250
|
-
const radianceRenderTarget = renderPMREMToEquirect(this.
|
|
251
|
-
const irradianceRenderTarget = renderPMREMToEquirect(this.
|
|
249
|
+
const radianceRenderTarget = renderPMREMToEquirect(this._renderer, target.texture, 0.0, 1024, 512, target.height);
|
|
250
|
+
const irradianceRenderTarget = renderPMREMToEquirect(this._renderer, target.texture, 1.0, 32, 16, target.height);
|
|
252
251
|
target.dispose();
|
|
253
252
|
res = {
|
|
254
253
|
radianceTexture: radianceRenderTarget.texture,
|
|
@@ -266,11 +265,11 @@ export class MaterialXEnvironment {
|
|
|
266
265
|
}
|
|
267
266
|
|
|
268
267
|
private updateLighting = (collectLights: boolean = false) => {
|
|
269
|
-
if (!this.
|
|
268
|
+
if (!this._scene) return;
|
|
270
269
|
// Find lights in scene
|
|
271
270
|
if (collectLights) {
|
|
272
271
|
const lights = new Array<Light>();
|
|
273
|
-
this.
|
|
272
|
+
this._scene.traverse((object: Object3D) => {
|
|
274
273
|
if ((object as Light).isLight && object.visible)
|
|
275
274
|
lights.push(object as Light);
|
|
276
275
|
});
|
package/src/textureHelper.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebGLRenderer, Scene, WebGLRenderTarget, PlaneGeometry, OrthographicCamera, ShaderMaterial, RGBAFormat, FloatType, LinearFilter, Mesh, EquirectangularReflectionMapping, RepeatWrapping, LinearMipMapLinearFilter, Texture, WebGLUtils } from 'three';
|
|
2
|
-
import { getParam } from '
|
|
2
|
+
import { getParam } from './utils.js';
|
|
3
3
|
|
|
4
4
|
const debug = getParam("debugmaterialx");
|
|
5
5
|
|
package/src/utils.ts
CHANGED
|
@@ -1,94 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export function getParam(name: string) {
|
|
2
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3
|
+
const param = urlParams.get(name);
|
|
4
|
+
if (param == null || param === "0" || param === "false") return false;
|
|
5
|
+
if (param === "") return true;
|
|
6
|
+
return param;
|
|
7
|
+
}
|
|
3
8
|
|
|
4
9
|
export const debug = getParam("debugmaterialx");
|
|
5
10
|
export const debugUpdate = debug === "update";
|
|
6
11
|
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
let time = 0;
|
|
14
|
+
export function getTime() {
|
|
15
|
+
return time;
|
|
16
|
+
}
|
|
17
|
+
let frame = 0;
|
|
18
|
+
export function getFrame() {
|
|
19
|
+
return frame;
|
|
20
|
+
}
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
const performance = window.performance || (window as any).webkitPerformance || (window as any).mozPerformance;
|
|
23
|
+
function updateTime() {
|
|
24
|
+
time = performance.now() / 1000; // Convert to seconds
|
|
25
|
+
frame++;
|
|
26
|
+
window.requestAnimationFrame(updateTime);
|
|
27
|
+
}
|
|
28
|
+
window.requestAnimationFrame(updateTime);
|
|
14
29
|
|
|
15
|
-
const patchWebGL2 = () => {
|
|
16
|
-
const getUniformLocation = WebGL2RenderingContext.prototype.getUniformLocation;
|
|
17
|
-
const programAndNameToUniformLocation = new WeakMap<WebGLUniformLocation, { program: WebGLProgram, name: string }>();
|
|
18
|
-
WebGL2RenderingContext.prototype.getUniformLocation = function (program: WebGLProgram, name: string) {
|
|
19
|
-
const location = getUniformLocation.call(this, program, name);
|
|
20
|
-
if (location) {
|
|
21
|
-
programAndNameToUniformLocation.set(location, { program, name });
|
|
22
|
-
}
|
|
23
|
-
return location;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const uniform4fv = WebGL2RenderingContext.prototype.uniform4fv;
|
|
27
|
-
WebGL2RenderingContext.prototype.uniform4fv = function (location: WebGLUniformLocation | null, v: Float32Array | number[]) {
|
|
28
|
-
if (location) {
|
|
29
|
-
const uniformName = programAndNameToUniformLocation.get(location);
|
|
30
|
-
if (true) console.log("Calling uniform4fv", { location, v, name: uniformName?.name });
|
|
31
|
-
}
|
|
32
|
-
return uniform4fv.call(this, location, v);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const uniform3fv = WebGL2RenderingContext.prototype.uniform3fv;
|
|
36
|
-
WebGL2RenderingContext.prototype.uniform3fv = function (location: WebGLUniformLocation | null, v: Float32Array | number[]) {
|
|
37
|
-
if (location) {
|
|
38
|
-
const uniformName = programAndNameToUniformLocation.get(location);
|
|
39
|
-
if (true) console.log("Calling uniform3fv", { location, v, name: uniformName?.name });
|
|
40
|
-
}
|
|
41
|
-
return uniform3fv.call(this, location, v);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const uniform3iv = WebGL2RenderingContext.prototype.uniform3iv;
|
|
45
|
-
WebGL2RenderingContext.prototype.uniform3iv = function (location: WebGLUniformLocation | null, v: Int32Array | number[]) {
|
|
46
|
-
if (location) {
|
|
47
|
-
const uniformName = programAndNameToUniformLocation.get(location);
|
|
48
|
-
if (true) console.log("Calling uniform3iv", { location, v, name: uniformName?.name });
|
|
49
|
-
}
|
|
50
|
-
return uniform3iv.call(this, location, v);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const uniform3uiv = WebGL2RenderingContext.prototype.uniform3uiv;
|
|
54
|
-
WebGL2RenderingContext.prototype.uniform3uiv = function (location: WebGLUniformLocation | null, v: Uint32Array | number[]) {
|
|
55
|
-
if (location) {
|
|
56
|
-
const uniformName = programAndNameToUniformLocation.get(location);
|
|
57
|
-
if (true) console.log("Calling uniform3uiv", { location, v, name: uniformName?.name });
|
|
58
|
-
}
|
|
59
|
-
return uniform3uiv.call(this, location, v);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const uniform3f = WebGL2RenderingContext.prototype.uniform3f;
|
|
63
|
-
WebGL2RenderingContext.prototype.uniform3f = function (location: WebGLUniformLocation
|
|
64
|
-
| null, x: number, y: number, z: number) {
|
|
65
|
-
if (location) {
|
|
66
|
-
const uniformName = programAndNameToUniformLocation.get(location);
|
|
67
|
-
if (uniformName?.name !== "diffuse")
|
|
68
|
-
if (true) console.log("Calling uniform3f", { location, x, y, z, name: uniformName?.name });
|
|
69
|
-
}
|
|
70
|
-
return uniform3f.call(this, location, x, y, z);
|
|
71
|
-
};
|
|
72
|
-
};
|
|
73
|
-
// patchWebGL2();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// TODO doesn't actually reset yet...
|
|
77
|
-
function resetShaders(ctx: Context) {
|
|
78
|
-
const scene = ctx.scene;
|
|
79
|
-
const gl = ctx.renderer;
|
|
80
|
-
console.log(gl.properties, gl.info)
|
|
81
|
-
scene.traverse(object => {
|
|
82
|
-
if ((object as Mesh).isMesh) {
|
|
83
|
-
const mesh = object as Mesh;
|
|
84
|
-
if (Array.isArray(mesh.material)) {
|
|
85
|
-
mesh.material.forEach(mat => gl.properties.remove(mat));
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
gl.properties.remove(mesh.material);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
})
|
|
92
|
-
if (gl.info?.programs)
|
|
93
|
-
gl.info.programs.length = 0;
|
|
94
|
-
}
|