diff --git a/docs/pages/MaterialXLoader.html.md b/docs/pages/MaterialXLoader.html.md
index 3f9c6e87f88f49..9183e564c9d7c6 100644
--- a/docs/pages/MaterialXLoader.html.md
+++ b/docs/pages/MaterialXLoader.html.md
@@ -10,7 +10,7 @@ The node materials loaded with this loader can only be used with [WebGPURenderer
```js
const loader = new MaterialXLoader().setPath( SAMPLE_PATH );
-const materials = await loader.loadAsync( 'standard_surface_brass_tiled.mtlx' );
+const { materials, report } = await loader.loadAsync( 'standard_surface_brass_tiled.mtlx' );
```
## Import
@@ -57,7 +57,29 @@ Executed when errors occur.
**Returns:** A reference to this loader.
-### .parse( text : string ) : Object.
+### .loadAsync( url : string, onProgress : onProgressCallback, options : Object ) : Promise
+
+Asynchronously loads a MaterialX asset.
+
+**url**
+
+The path/URL of the file to be loaded. This can be a `.mtlx` file, a `.mtlx.zip` package, or a data URI.
+
+**onProgress**
+
+Executed while the loading is in progress.
+
+**options**
+
+Optional MaterialX translation options.
+
+* `materialName`: Selects one `surfacematerial` by name from a multi-material document.
+* `issuePolicy`: Controls issue handling. Supported values are `warn`, `error-core`, and `error-all`.
+* `onWarning`: Callback executed for structured translation warnings.
+
+**Returns:** A promise resolving with an object containing `materials` and `report`.
+
+### .parse( text : string, options : Object ) : Object
Parses the given MaterialX data and returns the resulting materials.
@@ -82,9 +104,23 @@ Supported standard\_surface inputs:
The raw MaterialX data as a string.
+**options**
+
+Optional MaterialX translation options. See [loadAsync](#loadAsync).
+
**Overrides:** [Loader#parse](Loader.html#parse)
-**Returns:** A dictionary holding the parse node materials.
+**Returns:** An object containing `materials` and `report`. `materials` is a dictionary holding the parsed node materials.
+
+### .parseBuffer( data : ArrayBuffer, url : string, options : Object ) : Object
+
+Parses raw MaterialX data from an `ArrayBuffer`, `Uint8Array`, or string. ZIP buffers are detected automatically.
+
+**Returns:** An object containing `materials` and `report`.
+
+### .dispose() : MaterialXLoader
+
+Releases object URLs created while loading `.mtlx.zip` package resources.
## Source
diff --git a/examples/jsm/loaders/MaterialXLoader.js b/examples/jsm/loaders/MaterialXLoader.js
index 8c4271c5142589..230ac5419bae37 100644
--- a/examples/jsm/loaders/MaterialXLoader.js
+++ b/examples/jsm/loaders/MaterialXLoader.js
@@ -1,202 +1,44 @@
-import {
- FileLoader, Loader, ImageBitmapLoader, Texture, RepeatWrapping, MeshBasicNodeMaterial,
- MeshPhysicalNodeMaterial, DoubleSide,
-} from 'three/webgpu';
+import { FileLoader, Loader } from 'three/webgpu';
-import {
- float, bool, int, vec2, vec3, vec4, color, texture,
- positionLocal, positionWorld, uv, vertexColor,
- normalLocal, normalWorld, tangentLocal, tangentWorld,
- mul, abs, sign, floor, ceil, round, sin, cos, tan,
- asin, acos, sqrt, exp, clamp, min, max, normalize, length, dot, cross, normalMap,
- remap, smoothstep, luminance, mx_rgbtohsv, mx_hsvtorgb,
- mix, saturation, transpose, determinant, inverse, log, reflect, refract, element,
- mx_ramplr, mx_ramptb, mx_splitlr, mx_splittb,
- mx_fractal_noise_float, mx_noise_float, mx_cell_noise_float, mx_worley_noise_float,
- mx_transform_uv,
- mx_safepower, mx_contrast,
- mx_srgb_texture_to_lin_rec709,
- mx_add, mx_atan2, mx_divide, mx_modulo, mx_multiply, mx_power, mx_subtract,
- mx_timer, mx_frame, mat3, mx_ramp4,
- mx_invert, mx_ifgreater, mx_ifgreatereq, mx_ifequal, distance,
- mx_separate, mx_place2d, mx_rotate2d, mx_rotate3d, mx_heighttonormal,
- mx_unifiednoise2d, mx_unifiednoise3d
-} from 'three/tsl';
+import { MaterialXDocument } from './materialx/MaterialXDocument.js';
+import { MaterialXIssueCollector } from './materialx/MaterialXWarnings.js';
+import { isZipBuffer, readMtlxArchive, createArchiveResolver } from './materialx/MaterialXArchive.js';
-const colorSpaceLib = {
- mx_srgb_texture_to_lin_rec709
-};
+const _textDecoder = new TextDecoder();
-class MXElement {
+function getResourcePath( loaderPath, url ) {
- constructor( name, nodeFunc, params = [] ) {
+ if ( loaderPath ) return loaderPath;
- this.name = name;
- this.nodeFunc = nodeFunc;
- this.params = params;
-
- }
+ const index = url.lastIndexOf( '/' );
+ return index === - 1 ? '' : url.slice( 0, index + 1 );
}
-// Ref: https://github.com/mrdoob/three.js/issues/24674
-
-// Enhanced separate node to support multi-output referencing (outx, outy, outz, outw)
-
-// Type/arity-aware MaterialX node wrappers
-
-const MXElements = [
-
- // << Math >>
- new MXElement( 'add', mx_add, [ 'in1', 'in2' ] ),
- new MXElement( 'subtract', mx_subtract, [ 'in1', 'in2' ] ),
- new MXElement( 'multiply', mx_multiply, [ 'in1', 'in2' ] ),
- new MXElement( 'divide', mx_divide, [ 'in1', 'in2' ] ),
- new MXElement( 'modulo', mx_modulo, [ 'in1', 'in2' ] ),
- new MXElement( 'absval', abs, [ 'in1', 'in2' ] ),
- new MXElement( 'sign', sign, [ 'in1', 'in2' ] ),
- new MXElement( 'floor', floor, [ 'in1', 'in2' ] ),
- new MXElement( 'ceil', ceil, [ 'in1', 'in2' ] ),
- new MXElement( 'round', round, [ 'in1', 'in2' ] ),
- new MXElement( 'power', mx_power, [ 'in1', 'in2' ] ),
- new MXElement( 'sin', sin, [ 'in' ] ),
- new MXElement( 'cos', cos, [ 'in' ] ),
- new MXElement( 'tan', tan, [ 'in' ] ),
- new MXElement( 'asin', asin, [ 'in' ] ),
- new MXElement( 'acos', acos, [ 'in' ] ),
- new MXElement( 'atan2', mx_atan2, [ 'in1', 'in2' ] ),
- new MXElement( 'sqrt', sqrt, [ 'in' ] ),
- new MXElement( 'ln', log, [ 'in' ] ),
- new MXElement( 'exp', exp, [ 'in' ] ),
- new MXElement( 'clamp', clamp, [ 'in', 'low', 'high' ] ),
- new MXElement( 'min', min, [ 'in1', 'in2' ] ),
- new MXElement( 'max', max, [ 'in1', 'in2' ] ),
- new MXElement( 'normalize', normalize, [ 'in' ] ),
- new MXElement( 'magnitude', length, [ 'in1', 'in2' ] ),
- new MXElement( 'dotproduct', dot, [ 'in1', 'in2' ] ),
- new MXElement( 'crossproduct', cross, [ 'in' ] ),
- new MXElement( 'distance', distance, [ 'in1', 'in2' ] ),
- new MXElement( 'invert', mx_invert, [ 'in', 'amount' ] ),
- //new MtlXElement( 'transformpoint', ... ),
- //new MtlXElement( 'transformvector', ... ),
- //new MtlXElement( 'transformnormal', ... ),
- new MXElement( 'transformmatrix', mul, [ 'in1', 'in2' ] ),
- new MXElement( 'normalmap', normalMap, [ 'in', 'scale' ] ),
- new MXElement( 'transpose', transpose, [ 'in' ] ),
- new MXElement( 'determinant', determinant, [ 'in' ] ),
- new MXElement( 'invertmatrix', inverse, [ 'in' ] ),
- new MXElement( 'creatematrix', mat3, [ 'in1', 'in2', 'in3' ] ),
- //new MtlXElement( 'rotate2d', rotateUV, [ 'in', radians( 'amount' )** ] ),
- //new MtlXElement( 'rotate3d', ... ),
- //new MtlXElement( 'arrayappend', ... ),
- //new MtlXElement( 'dot', ... ),
-
- new MXElement( 'length', length, [ 'in' ] ),
- new MXElement( 'crossproduct', cross, [ 'in1', 'in2' ] ),
- new MXElement( 'floor', floor, [ 'in' ] ),
- new MXElement( 'ceil', ceil, [ 'in' ] ),
-
- // << Adjustment >>
- new MXElement( 'remap', remap, [ 'in', 'inlow', 'inhigh', 'outlow', 'outhigh' ] ),
- new MXElement( 'smoothstep', smoothstep, [ 'in', 'low', 'high' ] ),
- //new MtlXElement( 'curveadjust', ... ),
- //new MtlXElement( 'curvelookup', ... ),
- new MXElement( 'luminance', luminance, [ 'in', 'lumacoeffs' ] ),
- new MXElement( 'rgbtohsv', mx_rgbtohsv, [ 'in' ] ),
- new MXElement( 'hsvtorgb', mx_hsvtorgb, [ 'in' ] ),
-
- // << Mix >>
- new MXElement( 'mix', mix, [ 'bg', 'fg', 'mix' ] ),
+class MaterialXLoader extends Loader {
- // << Channel >>
- new MXElement( 'combine2', vec2, [ 'in1', 'in2' ] ),
- new MXElement( 'combine3', vec3, [ 'in1', 'in2', 'in3' ] ),
- new MXElement( 'combine4', vec4, [ 'in1', 'in2', 'in3', 'in4' ] ),
+ constructor( manager ) {
- // << Procedural >>
- new MXElement( 'ramplr', mx_ramplr, [ 'valuel', 'valuer', 'texcoord' ] ),
- new MXElement( 'ramptb', mx_ramptb, [ 'valuet', 'valueb', 'texcoord' ] ),
- new MXElement( 'ramp4', mx_ramp4, [ 'valuetl', 'valuetr', 'valuebl', 'valuebr', 'texcoord' ] ),
- new MXElement( 'splitlr', mx_splitlr, [ 'valuel', 'valuer', 'texcoord' ] ),
- new MXElement( 'splittb', mx_splittb, [ 'valuet', 'valueb', 'texcoord' ] ),
- new MXElement( 'noise2d', mx_noise_float, [ 'texcoord', 'amplitude', 'pivot' ] ),
- new MXElement( 'noise3d', mx_noise_float, [ 'texcoord', 'amplitude', 'pivot' ] ),
- new MXElement( 'fractal3d', mx_fractal_noise_float, [ 'position', 'octaves', 'lacunarity', 'diminish', 'amplitude' ] ),
- new MXElement( 'cellnoise2d', mx_cell_noise_float, [ 'texcoord' ] ),
- new MXElement( 'cellnoise3d', mx_cell_noise_float, [ 'texcoord' ] ),
- new MXElement( 'worleynoise2d', mx_worley_noise_float, [ 'texcoord', 'jitter' ] ),
- new MXElement( 'worleynoise3d', mx_worley_noise_float, [ 'texcoord', 'jitter' ] ),
- new MXElement( 'unifiednoise2d', mx_unifiednoise2d, [ 'type', 'texcoord', 'freq', 'offset', 'jitter', 'outmin', 'outmax', 'clampoutput', 'octaves', 'lacunarity', 'diminish' ] ),
- new MXElement( 'unifiednoise3d', mx_unifiednoise3d, [ 'type', 'texcoord', 'freq', 'offset', 'jitter', 'outmin', 'outmax', 'clampoutput', 'octaves', 'lacunarity', 'diminish' ] ),
- // << Supplemental >>
- //new MtlXElement( 'tiledimage', ... ),
- //new MtlXElement( 'triplanarprojection', triplanarTextures, [ 'filex', 'filey', 'filez' ] ),
- //new MtlXElement( 'ramp4', ... ),
- new MXElement( 'place2d', mx_place2d, [ 'texcoord', 'pivot', 'scale', 'rotate', 'offset', 'operationorder' ] ),
- new MXElement( 'safepower', mx_safepower, [ 'in1', 'in2' ] ),
- new MXElement( 'contrast', mx_contrast, [ 'in', 'amount', 'pivot' ] ),
- //new MtlXElement( 'hsvadjust', ... ),
- new MXElement( 'saturate', saturation, [ 'in', 'amount' ] ),
- new MXElement( 'extract', element, [ 'in', 'index' ] ),
- new MXElement( 'separate2', mx_separate, [ 'in' ] ),
- new MXElement( 'separate3', mx_separate, [ 'in' ] ),
- new MXElement( 'separate4', mx_separate, [ 'in' ] ),
- new MXElement( 'reflect', reflect, [ 'in', 'normal' ] ),
- new MXElement( 'refract', refract, [ 'in', 'normal', 'ior' ] ),
+ super( manager );
- new MXElement( 'time', mx_timer ),
- new MXElement( 'frame', mx_frame ),
- new MXElement( 'ifgreater', mx_ifgreater, [ 'value1', 'value2', 'in1', 'in2' ] ),
- new MXElement( 'ifgreatereq', mx_ifgreatereq, [ 'value1', 'value2', 'in1', 'in2' ] ),
- new MXElement( 'ifequal', mx_ifequal, [ 'value1', 'value2', 'in1', 'in2' ] ),
+ this.archiveDisposer = null;
- // Placeholder implementations for unsupported nodes
- new MXElement( 'rotate2d', mx_rotate2d, [ 'in', 'amount' ] ),
- new MXElement( 'rotate3d', mx_rotate3d, [ 'in', 'amount', 'axis' ] ),
- new MXElement( 'heighttonormal', mx_heighttonormal, [ 'in', 'scale', 'texcoord' ] ),
+ }
-];
+ dispose() {
-const MtlXLibrary = {};
-MXElements.forEach( element => MtlXLibrary[ element.name ] = element );
+ if ( this.archiveDisposer ) {
-/**
- * A loader for the MaterialX format.
- *
- * The node materials loaded with this loader can only be used with {@link WebGPURenderer}.
- *
- * ```js
- * const loader = new MaterialXLoader().setPath( SAMPLE_PATH );
- * const materials = await loader.loadAsync( 'standard_surface_brass_tiled.mtlx' );
- * ```
- *
- * @augments Loader
- * @three_import import { MaterialXLoader } from 'three/addons/loaders/MaterialXLoader.js';
- */
-class MaterialXLoader extends Loader {
+ this.archiveDisposer();
+ this.archiveDisposer = null;
- /**
- * Constructs a new MaterialX loader.
- *
- * @param {LoadingManager} [manager] - The loading manager.
- */
- constructor( manager ) {
+ }
- super( manager );
+ return this;
}
- /**
- * Starts loading from the given URL and passes the loaded MaterialX asset
- * to the `onLoad()` callback.
- *
- * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI.
- * @param {function(Object)} onLoad - Executed when the loading process has been finished.
- * @param {onProgressCallback} onProgress - Executed while the loading is in progress.
- * @param {onErrorCallback} onError - Executed when errors occur.
- * @return {MaterialXLoader} A reference to this loader.
- */
- load( url, onLoad, onProgress, onError ) {
+ load( url, onLoad, onProgress, onError, options = {} ) {
const _onError = function ( e ) {
@@ -214,11 +56,12 @@ class MaterialXLoader extends Loader {
new FileLoader( this.manager )
.setPath( this.path )
- .load( url, async ( text ) => {
+ .setResponseType( 'arraybuffer' )
+ .load( url, ( data ) => {
try {
- onLoad( this.parse( text ) );
+ onLoad( this.parseBuffer( data, url, options ) );
} catch ( e ) {
@@ -232,870 +75,72 @@ class MaterialXLoader extends Loader {
}
- /**
- * Parses the given MaterialX data and returns the resulting materials.
- *
- * Supported standard_surface inputs:
- * - base, base_color: Base color/albedo
- * - opacity: Alpha/transparency
- * - specular_roughness: Surface roughness
- * - metalness: Metallic property
- * - specular: Specular reflection intensity
- * - specular_color: Specular reflection color
- * - ior: Index of refraction
- * - specular_anisotropy, specular_rotation: Anisotropic reflection
- * - transmission, transmission_color: Transmission properties
- * - thin_film_thickness, thin_film_ior: Thin film interference
- * - sheen, sheen_color, sheen_roughness: Sheen properties
- * - normal: Normal map
- * - coat, coat_roughness, coat_color: Clearcoat properties
- * - emission, emissionColor: Emission properties
- *
- * @param {string} text - The raw MaterialX data as a string.
- * @return {Object} A dictionary holding the parse node materials.
- */
- parse( text ) {
-
- return new MaterialX( this.manager, this.path ).parse( text );
-
- }
-
-}
-
-class MaterialXNode {
-
- constructor( materialX, nodeXML, nodePath = '' ) {
-
- if ( ! materialX || typeof materialX !== 'object' ) {
-
- console.warn( 'MaterialXNode: materialX argument is not an object!', { materialX, nodeXML, nodePath } );
-
- }
-
- this.materialX = materialX;
- this.nodeXML = nodeXML;
- this.nodePath = nodePath ? nodePath + '/' + this.name : this.name;
-
- this.parent = null;
-
- this.node = null;
-
- this.children = [];
-
- }
-
- get element() {
-
- return this.nodeXML.nodeName;
-
- }
-
- get nodeGraph() {
-
- return this.getAttribute( 'nodegraph' );
-
- }
-
- get nodeName() {
-
- return this.getAttribute( 'nodename' );
-
- }
-
- get interfaceName() {
-
- return this.getAttribute( 'interfacename' );
-
- }
-
- get output() {
-
- return this.getAttribute( 'output' );
-
- }
-
- get name() {
-
- return this.getAttribute( 'name' );
-
- }
-
- get type() {
-
- return this.getAttribute( 'type' );
-
- }
-
- get value() {
-
- return this.getAttribute( 'value' );
-
- }
-
- getNodeGraph() {
-
- let nodeX = this;
-
- while ( nodeX !== null ) {
-
- if ( nodeX.element === 'nodegraph' ) {
-
- break;
-
- }
-
- nodeX = nodeX.parent;
-
- }
-
- return nodeX;
-
- }
-
- getRoot() {
-
- let nodeX = this;
-
- while ( nodeX.parent !== null ) {
-
- nodeX = nodeX.parent;
-
- }
-
- return nodeX;
-
- }
-
- get referencePath() {
-
- let referencePath = null;
-
- if ( this.nodeGraph !== null && this.output !== null ) {
-
- referencePath = this.nodeGraph + '/' + this.output;
-
- } else if ( this.nodeName !== null || this.interfaceName !== null ) {
-
- referencePath = this.getNodeGraph().nodePath + '/' + ( this.nodeName || this.interfaceName );
-
- }
-
- return referencePath;
-
- }
-
- get hasReference() {
-
- return this.referencePath !== null;
-
- }
-
- get isConst() {
-
- return this.element === 'input' && this.value !== null && this.type !== 'filename';
-
- }
-
- getColorSpaceNode() {
-
- const csSource = this.getAttribute( 'colorspace' );
- const csTarget = this.getRoot().getAttribute( 'colorspace' );
-
- const nodeName = `mx_${ csSource }_to_${ csTarget }`;
-
- return colorSpaceLib[ nodeName ];
-
- }
-
- getTexture() {
-
- const filePrefix = this.getRecursiveAttribute( 'fileprefix' ) || '';
- const uri = filePrefix + this.value;
-
- if ( this.materialX.textureCache.has( uri ) ) {
-
- return this.materialX.textureCache.get( uri );
+ loadAsync( url, onProgress, options = {} ) {
- }
-
- let loader = this.materialX.textureLoader;
+ if ( onProgress && typeof onProgress === 'object' ) {
- if ( uri ) {
-
- const handler = this.materialX.manager.getHandler( uri );
- if ( handler !== null ) loader = handler;
+ options = onProgress;
+ onProgress = undefined;
}
- const texture = new Texture();
- texture.wrapS = texture.wrapT = RepeatWrapping;
-
- this.materialX.textureCache.set( uri, texture );
+ return new Promise( ( resolve, reject ) => {
- loader.load( uri, function ( imageBitmap ) {
-
- texture.image = imageBitmap;
- texture.needsUpdate = true;
+ this.load( url, resolve, onProgress, reject, options );
} );
- return texture;
-
}
- getClassFromType( type ) {
-
- let nodeClass = null;
-
- if ( type === 'integer' ) nodeClass = int;
- else if ( type === 'float' ) nodeClass = float;
- else if ( type === 'vector2' ) nodeClass = vec2;
- else if ( type === 'vector3' ) nodeClass = vec3;
- else if ( type === 'vector4' || type === 'color4' ) nodeClass = vec4;
- else if ( type === 'color3' ) nodeClass = color;
- else if ( type === 'boolean' ) nodeClass = bool;
-
- return nodeClass;
-
- }
-
- getNode( out = null ) {
-
- let node = this.node;
-
- if ( node !== null && out === null ) {
-
- return node;
-
- }
-
- // Handle
- if (
- this.element === 'input' &&
- this.name === 'texcoord' &&
- this.type === 'vector2'
- ) {
-
- // Try to get index from defaultgeomprop (e.g., "UV0" => 0)
- let index = 0;
- const defaultGeomProp = this.getAttribute( 'defaultgeomprop' );
- if ( defaultGeomProp && /^UV(\d+)$/.test( defaultGeomProp ) ) {
-
- index = parseInt( defaultGeomProp.match( /^UV(\d+)$/ )[ 1 ], 10 );
-
- }
-
- node = uv( index );
-
- }
-
- // Multi-output support for separate/separate3
- if (
- ( this.element === 'separate3' || this.element === 'separate2' || this.element === 'separate4' ) &&
- out && typeof out === 'string' && out.startsWith( 'out' )
- ) {
-
- const inNode = this.getNodeByName( 'in' );
- return mx_separate( inNode, out );
-
- }
-
- //
-
- const type = this.type;
-
- if ( this.isConst ) {
-
- const nodeClass = this.getClassFromType( type );
-
- node = nodeClass( ...this.getVector() );
-
- } else if ( this.hasReference ) {
-
- if ( this.element === 'output' && this.output && out === null ) {
-
- out = this.output;
-
- }
-
- node = this.materialX.getMaterialXNode( this.referencePath ).getNode( out );
-
- } else {
-
- const element = this.element;
-
- if ( element === 'convert' ) {
-
- const nodeClass = this.getClassFromType( type );
-
- node = nodeClass( this.getNodeByName( 'in' ) );
-
- } else if ( element === 'constant' ) {
-
- node = this.getNodeByName( 'value' );
-
- } else if ( element === 'position' ) {
-
- const space = this.getAttribute( 'space' );
- node = space === 'world' ? positionWorld : positionLocal;
-
- } else if ( element === 'normal' ) {
-
- const space = this.getAttribute( 'space' );
- node = space === 'world' ? normalWorld : normalLocal;
-
- } else if ( element === 'tangent' ) {
+ parseBuffer( data, url = '', options = {} ) {
- const space = this.getAttribute( 'space' );
- node = space === 'world' ? tangentWorld : tangentLocal;
+ this.dispose();
- } else if ( element === 'texcoord' ) {
+ let text;
+ let archiveResolver = null;
- const indexNode = this.getChildByName( 'index' );
- const index = indexNode ? parseInt( indexNode.value ) : 0;
+ if ( data && ( isZipBuffer( data ) || /\.mtlx\.zip$/i.test( url ) ) ) {
- node = uv( index );
+ const archive = readMtlxArchive( data );
+ text = archive.text;
+ const resolver = createArchiveResolver( archive.files );
+ archiveResolver = resolver.resolve;
+ this.archiveDisposer = resolver.dispose;
- } else if ( element === 'geomcolor' ) {
+ } else if ( typeof data === 'string' ) {
- const indexNode = this.getChildByName( 'index' );
- const index = indexNode ? parseInt( indexNode.value ) : 0;
+ text = data;
- node = vertexColor( index );
+ } else if ( data instanceof Uint8Array ) {
- } else if ( element === 'tiledimage' ) {
-
- const file = this.getChildByName( 'file' );
-
- const textureFile = file.getTexture();
- const uvTiling = mx_transform_uv( ...this.getNodesByNames( [ 'uvtiling', 'uvoffset' ] ) );
-
- node = texture( textureFile, uvTiling );
-
- const colorSpaceNode = file.getColorSpaceNode();
-
- if ( colorSpaceNode ) {
-
- node = colorSpaceNode( node );
-
- }
-
- } else if ( element === 'image' ) {
-
- const file = this.getChildByName( 'file' );
- const uvNode = this.getNodeByName( 'texcoord' );
-
- const textureFile = file.getTexture();
-
- node = texture( textureFile, uvNode );
-
- const colorSpaceNode = file.getColorSpaceNode();
-
- if ( colorSpaceNode ) {
-
- node = colorSpaceNode( node );
-
- }
-
- } else if ( MtlXLibrary[ element ] !== undefined ) {
-
- const nodeElement = MtlXLibrary[ element ];
-
- if ( ! nodeElement ) {
-
- throw new Error( `THREE.MaterialXLoader: Unexpected node ${ new XMLSerializer().serializeToString( this.nodeXML ) }.` );
-
- }
-
- if ( ! nodeElement.nodeFunc ) {
-
- throw new Error( `THREE.MaterialXLoader: Unexpected node 2 ${ new XMLSerializer().serializeToString( this.nodeXML ) }.` );
-
- }
-
- if ( out !== null ) {
-
- node = nodeElement.nodeFunc( ...this.getNodesByNames( ...nodeElement.params ), out );
-
- } else {
-
- node = nodeElement.nodeFunc( ...this.getNodesByNames( ...nodeElement.params ) );
-
- }
-
- }
-
- }
-
- //
-
- if ( node === null ) {
-
- console.warn( `THREE.MaterialXLoader: Unexpected node ${ new XMLSerializer().serializeToString( this.nodeXML ) }.` );
-
- node = float( 0 );
-
- }
-
- //
-
- const nodeToTypeClass = this.getClassFromType( type );
-
- if ( nodeToTypeClass !== null ) {
-
- node = nodeToTypeClass( node );
+ text = _textDecoder.decode( data );
} else {
- console.warn( `THREE.MaterialXLoader: Unexpected node ${ new XMLSerializer().serializeToString( this.nodeXML ) }.` );
- node = float( 0 );
-
- }
-
- node.name = this.name;
-
- this.node = node;
-
- return node;
-
- }
-
- getChildByName( name ) {
-
- for ( const input of this.children ) {
-
- if ( input.name === name ) {
-
- return input;
-
- }
-
- }
-
- }
-
- getNodes() {
-
- const nodes = {};
-
- for ( const input of this.children ) {
-
- const node = input.getNode();
-
- nodes[ node.name ] = node;
-
- }
-
- return nodes;
-
- }
-
- getNodeByName( name ) {
-
- const child = this.getChildByName( name );
-
- return child ? child.getNode( child.output ) : undefined;
-
- }
-
- getNodesByNames( ...names ) {
-
- const nodes = [];
-
- for ( const name of names ) {
-
- const node = this.getNodeByName( name );
-
- if ( node ) nodes.push( node );
-
- }
-
- return nodes;
-
- }
-
- getValue() {
-
- return this.value.trim();
-
- }
-
- getVector() {
-
- const vector = [];
-
- for ( const val of this.getValue().split( /[,|\s]/ ) ) {
-
- if ( val !== '' ) {
-
- vector.push( Number( val.trim() ) );
-
- }
-
- }
-
- return vector;
-
- }
-
- getAttribute( name ) {
-
- return this.nodeXML.getAttribute( name );
-
- }
-
- getRecursiveAttribute( name ) {
-
- let attribute = this.nodeXML.getAttribute( name );
-
- if ( attribute === null && this.parent !== null ) {
-
- attribute = this.parent.getRecursiveAttribute( name );
-
- }
-
- return attribute;
-
- }
-
- setStandardSurfaceToGltfPBR( material ) {
-
- const inputs = this.getNodes();
-
- //
-
- let colorNode = null;
-
- if ( inputs.base && inputs.base_color ) colorNode = mul( inputs.base, inputs.base_color );
- else if ( inputs.base ) colorNode = inputs.base;
- else if ( inputs.base_color ) colorNode = inputs.base_color;
-
- //
-
- let opacityNode = null;
-
- if ( inputs.opacity ) opacityNode = inputs.opacity;
-
- //
-
- let roughnessNode = null;
-
- if ( inputs.specular_roughness ) roughnessNode = inputs.specular_roughness;
-
- //
-
- let metalnessNode = null;
-
- if ( inputs.metalness ) metalnessNode = inputs.metalness;
-
- //
-
- let specularIntensityNode = null;
-
- if ( inputs.specular ) specularIntensityNode = inputs.specular;
-
- //
-
- let specularColorNode = null;
-
- if ( inputs.specular_color ) specularColorNode = inputs.specular_color;
-
- //
-
- let iorNode = null;
-
- if ( inputs.ior ) iorNode = inputs.ior;
-
- //
-
- let anisotropyNode = null;
- let anisotropyRotationNode = null;
-
- if ( inputs.specular_anisotropy ) anisotropyNode = inputs.specular_anisotropy;
- if ( inputs.specular_rotation ) anisotropyRotationNode = inputs.specular_rotation;
-
- //
-
- let transmissionNode = null;
- let transmissionColorNode = null;
-
- if ( inputs.transmission ) transmissionNode = inputs.transmission;
- if ( inputs.transmission_color ) transmissionColorNode = inputs.transmission_color;
-
- //
-
- let thinFilmThicknessNode = null;
- let thinFilmIorNode = null;
-
- if ( inputs.thin_film_thickness ) thinFilmThicknessNode = inputs.thin_film_thickness;
-
- if ( inputs.thin_film_ior ) {
-
- // Clamp IOR to valid range for Three.js (1.0 to 2.333)
- thinFilmIorNode = clamp( inputs.thin_film_ior, float( 1.0 ), float( 2.333 ) );
-
- }
-
- //
-
- let sheenNode = null;
- let sheenColorNode = null;
- let sheenRoughnessNode = null;
-
- if ( inputs.sheen ) sheenNode = inputs.sheen;
- if ( inputs.sheen_color ) sheenColorNode = inputs.sheen_color;
- if ( inputs.sheen_roughness ) sheenRoughnessNode = inputs.sheen_roughness;
-
- //
-
- let clearcoatNode = null;
- let clearcoatRoughnessNode = null;
-
- if ( inputs.coat ) clearcoatNode = inputs.coat;
- if ( inputs.coat_roughness ) clearcoatRoughnessNode = inputs.coat_roughness;
-
- if ( inputs.coat_color ) {
-
- colorNode = colorNode ? mul( colorNode, inputs.coat_color ) : colorNode;
-
- }
-
- //
-
- let normalNode = null;
-
- if ( inputs.normal ) normalNode = inputs.normal;
-
- //
-
- let emissiveNode = null;
-
- if ( inputs.emission ) emissiveNode = inputs.emission;
- if ( inputs.emissionColor ) {
-
- emissiveNode = emissiveNode ? mul( emissiveNode, inputs.emissionColor ) : emissiveNode;
+ text = _textDecoder.decode( new Uint8Array( data ) );
}
- //
-
- material.colorNode = colorNode || color( 0.8, 0.8, 0.8 );
- material.opacityNode = opacityNode || float( 1.0 );
- material.roughnessNode = roughnessNode || float( 0.2 );
- material.metalnessNode = metalnessNode || float( 0 );
- material.specularIntensityNode = specularIntensityNode || float( 0.5 );
- material.specularColorNode = specularColorNode || color( 1.0, 1.0, 1.0 );
- material.iorNode = iorNode || float( 1.5 );
- material.anisotropyNode = anisotropyNode || float( 0 );
- material.anisotropyRotationNode = anisotropyRotationNode || float( 0 );
- material.transmissionNode = transmissionNode || float( 0 );
- material.transmissionColorNode = transmissionColorNode || color( 1.0, 1.0, 1.0 );
- material.thinFilmThicknessNode = thinFilmThicknessNode || float( 0 );
- material.thinFilmIorNode = thinFilmIorNode || float( 1.5 );
- material.sheenNode = sheenNode || float( 0 );
- material.sheenColorNode = sheenColorNode || color( 1.0, 1.0, 1.0 );
- material.sheenRoughnessNode = sheenRoughnessNode || float( 0.5 );
- material.clearcoatNode = clearcoatNode || float( 0 );
- material.clearcoatRoughnessNode = clearcoatRoughnessNode || float( 0 );
- if ( normalNode ) material.normalNode = normalNode;
- if ( emissiveNode ) material.emissiveNode = emissiveNode;
-
- // Auto-enable iridescence when thin film parameters are present
- if ( thinFilmThicknessNode && thinFilmThicknessNode.value !== undefined && thinFilmThicknessNode.value > 0 ) {
-
- material.iridescence = 1.0;
-
- }
-
- if ( opacityNode !== null ) {
-
- material.transparent = true;
-
- }
-
- if ( transmissionNode !== null ) {
-
- material.side = DoubleSide;
- material.transparent = true;
-
- }
-
- }
-
- /*setGltfPBR( material ) {
-
- const inputs = this.getNodes();
-
- console.log( inputs );
-
- }*/
-
- setMaterial( material ) {
-
- const element = this.element;
-
- if ( element === 'gltf_pbr' ) {
-
- //this.setGltfPBR( material );
-
- } else if ( element === 'standard_surface' ) {
-
- this.setStandardSurfaceToGltfPBR( material );
-
- }
-
- }
-
- toBasicMaterial() {
-
- const material = new MeshBasicNodeMaterial();
- material.name = this.name;
-
- for ( const nodeX of this.children.toReversed() ) {
-
- if ( nodeX.name === 'out' ) {
-
- material.colorNode = nodeX.getNode();
-
- break;
-
- }
-
- }
-
- return material;
-
- }
-
- toPhysicalMaterial() {
-
- const material = new MeshPhysicalNodeMaterial();
- material.name = this.name;
-
- for ( const nodeX of this.children ) {
-
- const shaderProperties = this.materialX.getMaterialXNode( nodeX.nodeName );
- shaderProperties.setMaterial( material );
-
- }
-
- return material;
-
- }
-
- toMaterials() {
-
- const materials = {};
-
- let isUnlit = true;
-
- for ( const nodeX of this.children ) {
-
- if ( nodeX.element === 'surfacematerial' ) {
-
- const material = nodeX.toPhysicalMaterial();
-
- materials[ material.name ] = material;
-
- isUnlit = false;
-
- }
-
- }
-
- if ( isUnlit ) {
-
- for ( const nodeX of this.children ) {
-
- if ( nodeX.element === 'nodegraph' ) {
-
- const material = nodeX.toBasicMaterial();
-
- materials[ material.name ] = material;
-
- }
-
- }
-
- }
-
- return materials;
-
- }
-
- add( materialXNode ) {
-
- materialXNode.parent = this;
-
- this.children.push( materialXNode );
-
- }
-
-}
-
-class MaterialX {
-
- constructor( manager, path ) {
-
- this.manager = manager;
- this.path = path;
- this.resourcePath = '';
-
- this.nodesXLib = new Map();
- //this.nodesXRefLib = new WeakMap();
-
- this.textureLoader = new ImageBitmapLoader( manager );
- this.textureLoader.setOptions( { imageOrientation: 'flipY' } );
-
- this.textureCache = new Map();
-
- }
-
- addMaterialXNode( materialXNode ) {
-
- this.nodesXLib.set( materialXNode.nodePath, materialXNode );
-
- }
-
- /*getMaterialXNodeFromXML( xmlNode ) {
-
- return this.nodesXRefLib.get( xmlNode );
-
- }*/
-
- getMaterialXNode( ...names ) {
-
- return this.nodesXLib.get( names.join( '/' ) );
-
- }
-
- parseNode( nodeXML, nodePath = '' ) {
-
- const materialXNode = new MaterialXNode( this, nodeXML, nodePath );
- if ( materialXNode.nodePath ) this.addMaterialXNode( materialXNode );
-
- for ( const childNodeXML of nodeXML.children ) {
-
- const childMXNode = this.parseNode( childNodeXML, materialXNode.nodePath );
- materialXNode.add( childMXNode );
-
- }
-
- return materialXNode;
+ return this.parse( text, {
+ ...options,
+ archiveResolver,
+ path: options.path || getResourcePath( this.path, url )
+ } );
}
- parse( text ) {
+ parse( text, options = {} ) {
- const rootXML = new DOMParser().parseFromString( text, 'application/xml' ).documentElement;
-
- this.textureLoader.setPath( this.path );
-
- //
+ const issueCollector = new MaterialXIssueCollector( {
+ issuePolicy: options.issuePolicy,
+ onWarning: options.onWarning || options.warningCallback
+ } );
- const materials = this.parseNode( rootXML ).toMaterials();
+ const document = new MaterialXDocument( this.manager, options.path || this.path, issueCollector, options.archiveResolver || null, options.uvSpace );
+ const result = document.parse( text, options.materialName || null );
- return { materials };
+ issueCollector.throwIfNeeded();
+ return result;
}
diff --git a/examples/jsm/loaders/materialx/MaterialXArchive.js b/examples/jsm/loaders/materialx/MaterialXArchive.js
new file mode 100644
index 00000000000000..da98c6108c2570
--- /dev/null
+++ b/examples/jsm/loaders/materialx/MaterialXArchive.js
@@ -0,0 +1,108 @@
+import { unzipSync } from '../../libs/fflate.module.js';
+
+const _textDecoder = new TextDecoder();
+
+function normalizePath( path ) {
+
+ return path
+ .split( '\\' ).join( '/' )
+ .replace( /^\.?\//, '' )
+ .replace( /^\/+/, '' );
+
+}
+
+function isZipBuffer( buffer ) {
+
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array( buffer );
+ return bytes.length >= 4 && bytes[ 0 ] === 0x50 && bytes[ 1 ] === 0x4b;
+
+}
+
+function readMtlxArchive( buffer ) {
+
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array( buffer );
+ const archive = unzipSync( bytes );
+
+ const fileMap = new Map();
+ let mtlxPath = null;
+
+ for ( const path in archive ) {
+
+ const normalizedPath = normalizePath( path );
+ fileMap.set( normalizedPath, archive[ path ] );
+
+ if ( normalizedPath.toLowerCase().endsWith( '.mtlx' ) ) {
+
+ if ( mtlxPath !== null ) {
+
+ throw new Error( 'THREE.MaterialXLoader: Invalid .mtlx.zip package. Exactly one .mtlx file is required.' );
+
+ }
+
+ mtlxPath = normalizedPath;
+
+ }
+
+ }
+
+ if ( mtlxPath === null ) {
+
+ throw new Error( 'THREE.MaterialXLoader: Invalid .mtlx.zip package. Missing .mtlx file.' );
+
+ }
+
+ const text = _textDecoder.decode( fileMap.get( mtlxPath ) );
+ return { text, mtlxPath, files: fileMap };
+
+}
+
+function createArchiveResolver( files ) {
+
+ const objectUrlCache = new Map();
+
+ const getFile = ( uri ) => {
+
+ const normalized = normalizePath( decodeURI( uri ) );
+ if ( files.has( normalized ) ) return files.get( normalized );
+
+ for ( const [ path, bytes ] of files ) {
+
+ if ( path.endsWith( normalized ) ) return bytes;
+
+ }
+
+ return null;
+
+ };
+
+ const resolve = ( uri ) => {
+
+ if ( objectUrlCache.has( uri ) ) return objectUrlCache.get( uri );
+
+ const bytes = getFile( uri );
+ if ( ! bytes ) return null;
+
+ const blob = new Blob( [ bytes ], { type: 'application/octet-stream' } );
+ const objectUrl = URL.createObjectURL( blob );
+ objectUrlCache.set( uri, objectUrl );
+ return objectUrl;
+
+ };
+
+ const dispose = () => {
+
+ for ( const objectUrl of objectUrlCache.values() ) {
+
+ URL.revokeObjectURL( objectUrl );
+
+ }
+
+ objectUrlCache.clear();
+
+ };
+
+ return { resolve, dispose };
+
+}
+
+export { isZipBuffer, readMtlxArchive, createArchiveResolver };
diff --git a/examples/jsm/loaders/materialx/MaterialXDocument.js b/examples/jsm/loaders/materialx/MaterialXDocument.js
new file mode 100644
index 00000000000000..3ea64e3b2ff65e
--- /dev/null
+++ b/examples/jsm/loaders/materialx/MaterialXDocument.js
@@ -0,0 +1,848 @@
+import {
+ Texture,
+ RepeatWrapping,
+ ClampToEdgeWrapping,
+ MirroredRepeatWrapping,
+ ImageLoader,
+ ImageBitmapLoader,
+ Matrix3,
+ Matrix4,
+ MeshBasicNodeMaterial,
+ MeshPhysicalNodeMaterial,
+} from 'three/webgpu';
+
+import {
+ float,
+ int,
+ bool,
+ sub,
+ vec2,
+ vec3,
+ vec4,
+ color,
+ uv,
+ mat3,
+ mat4,
+ element,
+ mx_transform_uv,
+ mx_srgb_texture_to_lin_rec709,
+} from 'three/tsl';
+
+import { createMaterialXCompileRegistry, compileNodeFromRegistry } from './compile/MaterialXCompileRegistry.js';
+import { parseMaterialXNodeTree, parseMaterialXText } from './parse/MaterialXParser.js';
+import { getSurfaceMapper } from './MaterialXSurfaceMappings.js';
+import { MtlXLibrary } from './MaterialXNodeLibrary.js';
+import { mxHextileCoord, mxHextileComputeBlendWeights } from './MaterialXHextile.js';
+import { toBooleanNode } from './MaterialXUtils.js';
+
+const colorSpaceLib = {
+ mx_srgb_texture_to_lin_rec709,
+};
+
+const DEFAULT_DOCUMENT_COLOR_SPACE = 'lin_rec709';
+const IDENTITY_MAT3_VALUES = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
+const IDENTITY_MAT4_VALUES = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ];
+const MATRIX_INVERSE_EPSILON = 1e-8;
+const COMPILE_REGISTRY = createMaterialXCompileRegistry();
+const TEXTURE_ADDRESS_MODE_WRAPPING = {
+ constant: ClampToEdgeWrapping,
+ clamp: ClampToEdgeWrapping,
+ periodic: RepeatWrapping,
+ mirror: MirroredRepeatWrapping,
+};
+const NODE_CLASS_BY_TYPE = {
+ integer: int,
+ float,
+ vector2: vec2,
+ vector3: vec3,
+ vector4: vec4,
+ color4: vec4,
+ color3: color,
+ boolean: null,
+ matrix33: mat3,
+ matrix44: mat4,
+};
+const OUTPUT_CHANNELS = {
+ outx: 0,
+ outr: 0,
+ r: 0,
+ outy: 1,
+ outg: 1,
+ g: 1,
+ outz: 2,
+ outb: 2,
+ b: 2,
+ outw: 3,
+ outa: 3,
+ a: 3,
+};
+
+function mxFlipUvY( uvNode ) {
+
+ return vec2( element( uvNode, 0 ), sub( 1, element( uvNode, 1 ) ) );
+
+}
+
+function mxIdentityUv( uvNode ) {
+
+ return uvNode;
+
+}
+
+function getBottomLeftUvSpaceHelpers( uvSpace ) {
+
+ const helper = uvSpace === 'top-left' ? mxFlipUvY : mxIdentityUv;
+ return {
+ mxToBottomLeftUvSpace: helper,
+ mxFromBottomLeftUvSpace: helper,
+ };
+
+}
+
+function normalizeUvSpace( uvSpace ) {
+
+ if ( uvSpace === undefined || uvSpace === null ) return 'bottom-left';
+ if ( uvSpace === 'bottom-left' || uvSpace === 'top-left' ) return uvSpace;
+ throw new Error( `Unsupported MaterialX uvSpace "${uvSpace}". Expected "bottom-left" or "top-left".` );
+
+}
+
+function isSvgUri( uri ) {
+
+ if ( typeof uri !== 'string' ) return false;
+ return /\.svg(?:$|[?#])/i.test( uri );
+
+}
+
+function normalizeTextureAddressMode( value ) {
+
+ if ( value === null || value === undefined || value === '' ) return 'periodic';
+
+ const mode = value.trim().toLowerCase();
+ return mode in TEXTURE_ADDRESS_MODE_WRAPPING ? mode : null;
+
+}
+
+function invertConstantMatrixValues( values, size ) {
+
+ if ( ! Array.isArray( values ) || values.length !== size * size ) return null;
+
+ if ( size === 3 ) {
+
+ const matrix = new Matrix3().setFromArray( values );
+ if ( Math.abs( matrix.determinant() ) < MATRIX_INVERSE_EPSILON ) return null;
+ matrix.invert();
+ // Convert Three.js internal column-major storage back to row-major literal order.
+ return matrix.transpose().elements;
+
+ }
+
+ if ( size === 4 ) {
+
+ const matrix = new Matrix4().setFromArray( values );
+ if ( Math.abs( matrix.determinant() ) < MATRIX_INVERSE_EPSILON ) return null;
+ matrix.invert();
+ // Convert Three.js internal column-major storage back to row-major literal order.
+ return matrix.transpose().elements;
+
+ }
+
+ return null;
+
+}
+
+function getOutputChannel( outputName ) {
+
+ return OUTPUT_CHANNELS[ outputName ] || 0;
+
+}
+
+function isChannelOutput( outputName ) {
+
+ return outputName in OUTPUT_CHANNELS;
+
+}
+
+class MaterialXNode {
+
+ constructor( materialX, nodeXML, nodePath = '' ) {
+
+ this.materialX = materialX;
+ this.nodeXML = nodeXML;
+ this.nodePath = nodePath ? nodePath + '/' + this.name : this.name;
+ this.parent = null;
+ this.node = null;
+ this.children = [];
+
+ }
+
+ get element() {
+
+ return this.nodeXML.nodeName;
+
+ }
+ get nodeGraph() {
+
+ return this.getAttribute( 'nodegraph' );
+
+ }
+ get nodeName() {
+
+ return this.getAttribute( 'nodename' );
+
+ }
+ get interfaceName() {
+
+ return this.getAttribute( 'interfacename' );
+
+ }
+ get output() {
+
+ return this.getAttribute( 'output' );
+
+ }
+ get name() {
+
+ return this.getAttribute( 'name' );
+
+ }
+ get type() {
+
+ return this.getAttribute( 'type' );
+
+ }
+ get value() {
+
+ return this.getAttribute( 'value' );
+
+ }
+
+ getNodeGraph() {
+
+ let nodeX = this;
+ while ( nodeX !== null ) {
+
+ if ( nodeX.element === 'nodegraph' ) break;
+ nodeX = nodeX.parent;
+
+ }
+
+ return nodeX;
+
+ }
+
+ getRoot() {
+
+ let nodeX = this;
+ while ( nodeX.parent !== null ) {
+
+ nodeX = nodeX.parent;
+
+ }
+
+ return nodeX;
+
+ }
+
+ get referencePath() {
+
+ let referencePath = null;
+ if ( this.nodeGraph !== null && this.output !== null ) {
+
+ referencePath = this.nodeGraph + '/' + this.output;
+
+ } else if ( this.nodeName !== null || this.interfaceName !== null ) {
+
+ const graphNode = this.getNodeGraph();
+ const scopedReference = this.nodeName || this.interfaceName;
+ if ( graphNode && scopedReference ) {
+
+ referencePath = graphNode.nodePath + '/' + scopedReference;
+
+ } else if ( this.nodeName !== null ) {
+
+ // Surface-level nodename links can legitimately target top-level siblings.
+ referencePath = this.nodeName;
+
+ }
+
+ }
+
+ return referencePath;
+
+ }
+
+ get hasReference() {
+
+ return this.referencePath !== null;
+
+ }
+ get isConst() {
+
+ return this.element === 'input' && this.value !== null && this.type !== 'filename';
+
+ }
+
+ getColorSpaceNode() {
+
+ const csSource = this.getAttribute( 'colorspace' );
+ const csTarget = this.getRoot().getAttribute( 'colorspace' );
+ if ( ! csSource || ! csTarget ) return null;
+ const nodeName = `mx_${csSource}_to_${csTarget}`;
+ return colorSpaceLib[ nodeName ] || null;
+
+ }
+
+ getTextureAddressMode( inputName ) {
+
+ const rawMode = this.getInputValueByName( inputName );
+ const mode = normalizeTextureAddressMode( rawMode );
+ if ( mode ) return mode;
+
+ this.materialX.issueCollector.addInvalidValue(
+ this.name,
+ `Unsupported texture address mode "${rawMode}" on input "${inputName}". Expected constant, clamp, periodic, or mirror.`,
+ );
+ return 'periodic';
+
+ }
+
+ getTextureAddressModes() {
+
+ return {
+ u: this.getTextureAddressMode( 'uaddressmode' ),
+ v: this.getTextureAddressMode( 'vaddressmode' ),
+ };
+
+ }
+
+ getTexture() {
+
+ const filePrefix = this.getRecursiveAttribute( 'fileprefix' ) || '';
+ const sourceURI = filePrefix + this.value;
+ const resolvedURI = this.materialX.resolveTextureURI( sourceURI );
+ const svgTexture = isSvgUri( resolvedURI );
+ const textureSourceNode = this.parent && typeof this.parent.getTextureAddressModes === 'function' ? this.parent : this;
+ const addressModes = textureSourceNode.getTextureAddressModes();
+ const textureCacheKey = `${resolvedURI}|${addressModes.u}|${addressModes.v}`;
+
+ if ( this.materialX.textureCache.has( textureCacheKey ) ) {
+
+ return this.materialX.textureCache.get( textureCacheKey );
+
+ }
+
+ let loader = svgTexture ? this.materialX.imageLoader : this.materialX.textureLoader;
+ if ( resolvedURI && ! svgTexture ) {
+
+ const handler = this.materialX.manager.getHandler( resolvedURI );
+ if ( handler !== null ) loader = handler;
+
+ }
+
+ const textureNode = new Texture();
+ textureNode.wrapS = TEXTURE_ADDRESS_MODE_WRAPPING[ addressModes.u ];
+ textureNode.wrapT = TEXTURE_ADDRESS_MODE_WRAPPING[ addressModes.v ];
+ textureNode.flipY = false;
+ this.materialX.textureCache.set( textureCacheKey, textureNode );
+
+ loader.load( resolvedURI, ( imageData ) => {
+
+ textureNode.image = imageData;
+ textureNode.needsUpdate = true;
+
+ }, undefined, () => {
+
+ throw new Error( `Failed to load texture "${resolvedURI}".` );
+
+ } );
+
+ return textureNode;
+
+ }
+
+ getClassFromType( type ) {
+
+ return NODE_CLASS_BY_TYPE[ type ] || null;
+
+ }
+
+ toBooleanNode( node ) {
+
+ return toBooleanNode( node );
+
+ }
+
+ getNode( out = null ) {
+
+ let node = this.node;
+ if ( node !== null && out === null ) return node;
+
+
+ if ( ( this.element === 'separate2' || this.element === 'separate3' || this.element === 'separate4' ) && out ) {
+
+ const inNode = this.getNodeByName( 'in' );
+ return element( inNode, getOutputChannel( out ) );
+
+ }
+
+ const type = this.type;
+ const channelRequested = this.element !== 'input' && this.element !== 'gltf_colorimage' && isChannelOutput( out );
+
+ if ( this.isConst ) {
+
+ if ( type === 'boolean' ) {
+
+ const normalized = this.getValue().trim().toLowerCase();
+ node = bool( normalized === 'true' || normalized === '1' );
+
+ } else if ( type === 'matrix33' ) {
+
+ node = this.getMatrix( 3 ) || mat3( 1, 0, 0, 0, 1, 0, 0, 0, 1 );
+
+ } else if ( type === 'matrix44' ) {
+
+ node = this.getMatrix( 4 ) || mat4( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 );
+
+ } else if ( type === 'string' ) {
+
+ node = this.getValue();
+
+ } else {
+
+ const nodeClass = this.getClassFromType( type );
+ node = nodeClass ? nodeClass( ...this.getVector() ) : float( 0 );
+
+ }
+
+ } else if ( this.hasReference ) {
+
+ if ( this.element === 'output' && this.output && out === null ) out = this.output;
+ let requestedOutput = out;
+ // For nodegraph references, this input's `output` attribute selects the graph output
+ // itself and should not be forwarded as an output selector on the resolved node.
+ if ( this.element === 'input' && this.nodeGraph !== null && this.output !== null ) {
+
+ requestedOutput = null;
+
+ }
+
+ const referenceNode = this.materialX.getMaterialXNode( this.referencePath );
+
+ if ( referenceNode ) {
+
+ node = referenceNode.getNode( requestedOutput );
+
+ } else {
+
+ this.materialX.issueCollector.addMissingReference( this.name, this.referencePath );
+ node = float( 0 );
+
+ }
+
+ } else if ( this.element === 'input' && this.name === 'texcoord' && this.type === 'vector2' ) {
+
+ let index = 0;
+ const defaultGeomProp = this.getAttribute( 'defaultgeomprop' );
+ if ( defaultGeomProp && /^UV(\d+)$/.test( defaultGeomProp ) ) {
+
+ index = parseInt( defaultGeomProp.match( /^UV(\d+)$/ )[ 1 ], 10 );
+
+ }
+
+ node = this.materialX.compileContext.mxToBottomLeftUvSpace( uv( index ) );
+
+ } else {
+
+ node = compileNodeFromRegistry( this, out, this.materialX.compileContext );
+
+ }
+
+ if ( node === null || node === undefined ) {
+
+ this.materialX.issueCollector.addUnsupportedNode( this.element, this.name );
+ node = float( 0 );
+
+ }
+
+ if ( channelRequested ) {
+
+ node = element( node, getOutputChannel( out ) );
+
+ }
+
+ const resolvedType = channelRequested ? 'float' : type;
+ if ( resolvedType === 'boolean' ) {
+
+ node = this.toBooleanNode( node );
+
+ } else if ( resolvedType === 'string' ) {
+
+ // String-typed inputs (for example transform* fromspace/tospace) are
+ // valid scalar parameters and should pass through without numeric casting.
+ node = typeof node === 'string' ? node : this.getValue();
+
+ } else {
+
+ const nodeToTypeClass = this.getClassFromType( resolvedType );
+ if ( nodeToTypeClass !== null ) {
+
+ node = nodeToTypeClass( node );
+
+ } else if ( resolvedType !== null && resolvedType !== undefined && resolvedType !== 'multioutput' ) {
+
+ this.materialX.issueCollector.addInvalidValue( this.name, `Unexpected type "${resolvedType}" on node "${this.name}".` );
+ node = float( 0 );
+
+ }
+
+ }
+
+ if ( node && typeof node === 'object' ) {
+
+ node.name = this.name;
+
+ }
+
+ this.node = node;
+ return node;
+
+ }
+
+ getChildByName( name ) {
+
+ for ( const input of this.children ) {
+
+ if ( input.name === name ) return input;
+
+ }
+
+ }
+
+ getNodes() {
+
+ const nodes = {};
+ for ( const input of this.children ) {
+
+ const value = input.getNode( input.output );
+ nodes[ input.name ] = value;
+
+ }
+
+ return nodes;
+
+ }
+
+ getNodeByName( name ) {
+
+ const child = this.getChildByName( name );
+ return child ? child.getNode( child.output ) : undefined;
+
+ }
+
+ getInputValueByName( name ) {
+
+ const child = this.getChildByName( name );
+ return child ? child.value : null;
+
+ }
+
+ getNodesByNames( ...names ) {
+
+ const nodes = [];
+ for ( const name of names ) {
+
+ const nodeValue = this.getNodeByName( name );
+ nodes.push( nodeValue );
+
+ }
+
+ return nodes;
+
+ }
+
+ getValue() {
+
+ return this.value ? this.value.trim() : '';
+
+ }
+
+ getVector() {
+
+ const vector = [];
+ for ( const val of this.getValue().split( /[,|\s]/ ) ) {
+
+ if ( val !== '' ) vector.push( Number( val.trim() ) );
+
+ }
+
+ return vector;
+
+ }
+
+ getMatrix( size ) {
+
+ const vector = this.getVector();
+ const expectedLength = size * size;
+ if ( vector.length !== expectedLength ) return null;
+ // MaterialX matrix values are serialized in column-major order.
+ // Reorder to row-major before constructing TSL matrix nodes so
+ // transformmatrix semantics match MaterialXJS and MaterialXView.
+ const reordered = [];
+ for ( let row = 0; row < size; row += 1 ) {
+
+ for ( let column = 0; column < size; column += 1 ) {
+
+ reordered.push( vector[ column * size + row ] );
+
+ }
+
+ }
+
+ return size === 3 ? mat3( ...reordered ) : mat4( ...reordered );
+
+ }
+
+ getAttribute( name ) {
+
+ const value = this.nodeXML.getAttribute( name );
+ if ( value === null && this.element === 'materialx' && name === 'colorspace' ) {
+
+ return DEFAULT_DOCUMENT_COLOR_SPACE;
+
+ }
+
+ return value;
+
+ }
+
+ getRecursiveAttribute( name ) {
+
+ let attribute = this.nodeXML.getAttribute( name );
+ if ( attribute === null && this.parent !== null ) {
+
+ attribute = this.parent.getRecursiveAttribute( name );
+
+ }
+
+ return attribute;
+
+ }
+
+ setMaterial( material ) {
+
+ const mapper = getSurfaceMapper( this.element );
+ if ( mapper ) {
+
+ mapper.apply( material, this.getNodes(), this.materialX.issueCollector, this.name );
+
+ } else {
+
+ this.materialX.issueCollector.addUnsupportedNode( this.element, this.name );
+
+ }
+
+ }
+
+ toBasicMaterial() {
+
+ const material = new MeshBasicNodeMaterial();
+ material.name = this.name;
+
+ for ( const nodeX of this.children.toReversed() ) {
+
+ if ( nodeX.name === 'out' ) {
+
+ material.colorNode = nodeX.getNode();
+ break;
+
+ }
+
+ }
+
+ return material;
+
+ }
+
+ resolveSurfaceShaderNode( nodeX ) {
+
+ if ( nodeX.hasReference ) {
+
+ return this.materialX.getMaterialXNode( nodeX.referencePath ) || null;
+
+ }
+
+ if ( nodeX.nodeName ) {
+
+ return this.materialX.getMaterialXNode( nodeX.nodeName ) || null;
+
+ }
+
+ return null;
+
+ }
+
+ toPhysicalMaterial() {
+
+ const material = new MeshPhysicalNodeMaterial();
+ material.name = this.name;
+
+ for ( const nodeX of this.children ) {
+
+ const shaderProperties = this.resolveSurfaceShaderNode( nodeX );
+ if ( shaderProperties === null ) {
+
+ this.materialX.issueCollector.addMissingReference(
+ nodeX.name,
+ nodeX.referencePath || nodeX.nodeName || '(unknown)',
+ );
+ continue;
+
+ }
+
+ shaderProperties.setMaterial( material );
+
+ }
+
+ return material;
+
+ }
+
+ toMaterials( materialName = null ) {
+
+ const materials = {};
+ const surfaceMaterials = this.children.filter( ( nodeX ) => nodeX.element === 'surfacematerial' );
+
+ let selectedSurfaceMaterials = surfaceMaterials;
+ if ( materialName ) {
+
+ selectedSurfaceMaterials = surfaceMaterials.filter( ( nodeX ) => nodeX.name === materialName );
+
+ if ( selectedSurfaceMaterials.length === 0 ) {
+
+ this.materialX.issueCollector.addMissingMaterial( materialName );
+
+ }
+
+ }
+
+ for ( const nodeX of selectedSurfaceMaterials ) {
+
+ const material = nodeX.toPhysicalMaterial();
+ materials[ material.name ] = material;
+
+ }
+
+ if ( Object.keys( materials ).length === 0 ) {
+
+ for ( const nodeX of this.children ) {
+
+ if ( nodeX.element === 'nodegraph' ) {
+
+ const material = nodeX.toBasicMaterial();
+ materials[ material.name ] = material;
+
+ }
+
+ }
+
+ }
+
+ return materials;
+
+ }
+
+ add( materialXNode ) {
+
+ materialXNode.parent = this;
+ this.children.push( materialXNode );
+
+ }
+
+}
+
+class MaterialXDocument {
+
+ constructor( manager, path, issueCollector, archiveResolver = null, uvSpace = 'bottom-left' ) {
+
+ this.manager = manager;
+ this.path = path;
+ this.issueCollector = issueCollector;
+ this.archiveResolver = archiveResolver;
+ this.uvSpace = normalizeUvSpace( uvSpace );
+
+ this.nodesXLib = new Map();
+ this.imageLoader = new ImageLoader( manager );
+ this.imageLoader.setPath( path );
+ this.textureLoader = new ImageBitmapLoader( manager );
+ this.textureLoader.setOptions( { imageOrientation: 'none' } );
+ this.textureLoader.setPath( path );
+ this.textureCache = new Map();
+ const bottomLeftUvSpaceHelpers = getBottomLeftUvSpaceHelpers( this.uvSpace );
+
+ this.compileContext = {
+ compileRegistry: COMPILE_REGISTRY,
+ nodeLibrary: MtlXLibrary,
+ ...bottomLeftUvSpaceHelpers,
+ mxTransformUv: mx_transform_uv,
+ mxHextileCoord,
+ mxHextileComputeBlendWeights,
+ invertConstantMatrixValues,
+ IDENTITY_MAT3_VALUES,
+ IDENTITY_MAT4_VALUES,
+ };
+
+ }
+
+ resolveTextureURI( uri ) {
+
+ if ( this.archiveResolver ) {
+
+ const archiveURI = this.archiveResolver( uri );
+ if ( archiveURI ) return archiveURI;
+
+ }
+
+ return uri;
+
+ }
+
+ addMaterialXNode( materialXNode ) {
+
+ this.nodesXLib.set( materialXNode.nodePath, materialXNode );
+
+ }
+
+ getMaterialXNode( ...names ) {
+
+ return this.nodesXLib.get( names.join( '/' ) );
+
+ }
+
+ parseNode( nodeXML, nodePath = '' ) {
+
+ return parseMaterialXNodeTree(
+ nodeXML,
+ ( childNodeXML, childNodePath ) => new MaterialXNode( this, childNodeXML, childNodePath ),
+ ( materialXNode ) => this.addMaterialXNode( materialXNode ),
+ nodePath,
+ );
+
+ }
+
+ parse( text, materialName = null ) {
+
+ const rootNode = parseMaterialXText(
+ text,
+ ( childNodeXML, childNodePath ) => new MaterialXNode( this, childNodeXML, childNodePath ),
+ ( materialXNode ) => this.addMaterialXNode( materialXNode ),
+ );
+ const materials = rootNode.toMaterials( materialName );
+ const report = this.issueCollector.buildReport();
+ return { materials, report };
+
+ }
+
+}
+
+export { MaterialXDocument };
diff --git a/examples/jsm/loaders/materialx/MaterialXHextile.js b/examples/jsm/loaders/materialx/MaterialXHextile.js
new file mode 100644
index 00000000000000..97a880c7f17e38
--- /dev/null
+++ b/examples/jsm/loaders/materialx/MaterialXHextile.js
@@ -0,0 +1,183 @@
+import {
+ abs,
+ add,
+ clamp,
+ cos,
+ dFdx,
+ dFdy,
+ div,
+ dot,
+ element,
+ floor,
+ fract,
+ max,
+ mix,
+ mul,
+ pow,
+ sin,
+ step,
+ sub,
+ vec2,
+ vec3,
+} from 'three/tsl';
+
+const HEXTILE_SQRT3_2 = Math.sqrt( 3 ) * 2;
+const HEXTILE_EPSILON = 1e-6;
+const HEXTILE_PI_OVER_180 = Math.PI / 180;
+
+function toRadians( degrees ) {
+
+ return mul( degrees, HEXTILE_PI_OVER_180 );
+
+}
+
+function mxHextileHash( point ) {
+
+ const x = element( point, 0 );
+ const y = element( point, 1 );
+ const p3Base = vec3( x, y, x );
+ const p3Scaled = mul( p3Base, vec3( 0.1031, 0.103, 0.0973 ) );
+ const p3Fract = fract( p3Scaled );
+ const p3YZX = vec3( element( p3Fract, 1 ), element( p3Fract, 2 ), element( p3Fract, 0 ) );
+ const p3Offset = add( p3YZX, 33.33 );
+ const p3 = add( p3Fract, dot( p3Fract, p3Offset ) );
+ const lhs = add( vec2( element( p3, 0 ), element( p3, 0 ) ), vec2( element( p3, 1 ), element( p3, 2 ) ) );
+ const rhs = vec2( element( p3, 2 ), element( p3, 1 ) );
+ return fract( mul( lhs, rhs ) );
+
+}
+
+function mxSchlickGain( x, r ) {
+
+ const rr = clamp( r, 0.001, 0.999 );
+ const a = mul( sub( div( 1, rr ), 2 ), sub( 1, mul( 2, x ) ) );
+ const low = div( x, add( a, 1 ) );
+ const high = div( sub( a, x ), sub( a, 1 ) );
+ return mix( low, high, step( 0.5, x ) );
+
+}
+
+function normalizeBlendWeights( weights ) {
+
+ const wx = element( weights, 0 );
+ const wy = element( weights, 1 );
+ const wz = element( weights, 2 );
+ const sum = add( add( wx, wy ), wz );
+ return div( weights, sum );
+
+}
+
+function mxRotate2d( point, sine, cosine ) {
+
+ return vec2( sub( mul( cosine, element( point, 0 ) ), mul( sine, element( point, 1 ) ) ), add( mul( sine, element( point, 0 ) ), mul( cosine, element( point, 1 ) ) ) );
+
+}
+
+function toTileCenter( tileId ) {
+
+ const scaled = div( tileId, HEXTILE_SQRT3_2 );
+ const sx = element( scaled, 0 );
+ const sy = element( scaled, 1 );
+ return vec2( add( sx, mul( 0.5, sy ) ), mul( 0.8660254, sy ) );
+
+}
+
+export function mxHextileComputeBlendWeights( luminanceWeights, tileWeights, falloff ) {
+
+ const weighted = mul( luminanceWeights, pow( tileWeights, vec3( 7, 7, 7 ) ) );
+ const normalized = normalizeBlendWeights( weighted );
+ const gained = vec3(
+ mxSchlickGain( element( normalized, 0 ), falloff ),
+ mxSchlickGain( element( normalized, 1 ), falloff ),
+ mxSchlickGain( element( normalized, 2 ), falloff ),
+ );
+ const gainedNormalized = normalizeBlendWeights( gained );
+ const applyFalloff = step( HEXTILE_EPSILON, abs( sub( falloff, 0.5 ) ) );
+ return mix( normalized, gainedNormalized, applyFalloff );
+
+}
+
+export function mxHextileCoord( coord, rotation, rotationRange, scale, scaleRange, offset, offsetRange ) {
+
+ const st = mul( coord, HEXTILE_SQRT3_2 );
+ const stSkewed = vec2( add( element( st, 0 ), mul( - 0.57735027, element( st, 1 ) ) ), mul( 1.15470054, element( st, 1 ) ) );
+ const stFrac = fract( stSkewed );
+ const tx = element( stFrac, 0 );
+ const ty = element( stFrac, 1 );
+ const tz = sub( sub( 1, tx ), ty );
+ const s = step( 0, sub( 0, tz ) );
+ const s2 = sub( mul( 2, s ), 1 );
+ const w1 = mul( sub( 0, tz ), s2 );
+ const w2 = sub( s, mul( ty, s2 ) );
+ const w3 = sub( s, mul( tx, s2 ) );
+ const baseId = floor( stSkewed );
+ const oneMinusS = sub( 1, s );
+ const id1 = add( baseId, vec2( s, s ) );
+ const id2 = add( baseId, vec2( s, oneMinusS ) );
+ const id3 = add( baseId, vec2( oneMinusS, s ) );
+
+ const ctr1 = toTileCenter( id1 );
+ const ctr2 = toTileCenter( id2 );
+ const ctr3 = toTileCenter( id3 );
+
+ const seedOffset = vec2( 0.12345, 0.12345 );
+ const rand1 = mxHextileHash( add( id1, seedOffset ) );
+ const rand2 = mxHextileHash( add( id2, seedOffset ) );
+ const rand3 = mxHextileHash( add( id3, seedOffset ) );
+
+ const rr = vec2( toRadians( element( rotationRange, 0 ) ), toRadians( element( rotationRange, 1 ) ) );
+ const rrMin = element( rr, 0 );
+ const rrMax = element( rr, 1 );
+ const randX = vec3( element( rand1, 0 ), element( rand2, 0 ), element( rand3, 0 ) );
+ const rotations = mix( vec3( rrMin, rrMin, rrMin ), vec3( rrMax, rrMax, rrMax ), mul( randX, rotation ) );
+ const randY = vec3( element( rand1, 1 ), element( rand2, 1 ), element( rand3, 1 ) );
+ const scaleMin = element( scaleRange, 0 );
+ const scaleMax = element( scaleRange, 1 );
+ const randomScale = mix( vec3( scaleMin, scaleMin, scaleMin ), vec3( scaleMax, scaleMax, scaleMax ), randY );
+ const scales = mix( vec3( 1, 1, 1 ), randomScale, scale );
+ const offsetMin = element( offsetRange, 0 );
+ const offsetMax = element( offsetRange, 1 );
+ const offset1 = mix( vec2( offsetMin, offsetMin ), vec2( offsetMax, offsetMax ), mul( rand1, offset ) );
+ const offset2 = mix( vec2( offsetMin, offsetMin ), vec2( offsetMax, offsetMax ), mul( rand2, offset ) );
+ const offset3 = mix( vec2( offsetMin, offsetMin ), vec2( offsetMax, offsetMax ), mul( rand3, offset ) );
+
+ const sampleCoord = ( center, randomOffset, rotationValue, sampleScale ) => {
+
+ const delta = sub( coord, center );
+ const rotated = mxRotate2d( delta, sin( rotationValue ), cos( rotationValue ) );
+ const safeScale = max( sampleScale, HEXTILE_EPSILON );
+ return add( add( div( rotated, vec2( safeScale, safeScale ) ), center ), randomOffset );
+
+ };
+
+ const sampleDerivative = ( derivative, rotationValue, sampleScale ) => {
+
+ const rotated = mxRotate2d( derivative, sin( rotationValue ), cos( rotationValue ) );
+ const safeScale = max( sampleScale, HEXTILE_EPSILON );
+ return div( rotated, vec2( safeScale, safeScale ) );
+
+ };
+
+ const ddx = dFdx( coord );
+ const ddy = dFdy( coord );
+
+ return {
+ coords: [
+ sampleCoord( ctr1, offset1, element( rotations, 0 ), element( scales, 0 ) ),
+ sampleCoord( ctr2, offset2, element( rotations, 1 ), element( scales, 1 ) ),
+ sampleCoord( ctr3, offset3, element( rotations, 2 ), element( scales, 2 ) ),
+ ],
+ ddx: [
+ sampleDerivative( ddx, element( rotations, 0 ), element( scales, 0 ) ),
+ sampleDerivative( ddx, element( rotations, 1 ), element( scales, 1 ) ),
+ sampleDerivative( ddx, element( rotations, 2 ), element( scales, 2 ) ),
+ ],
+ ddy: [
+ sampleDerivative( ddy, element( rotations, 0 ), element( scales, 0 ) ),
+ sampleDerivative( ddy, element( rotations, 1 ), element( scales, 1 ) ),
+ sampleDerivative( ddy, element( rotations, 2 ), element( scales, 2 ) ),
+ ],
+ weights: vec3( w1, w2, w3 ),
+ };
+
+}
diff --git a/examples/jsm/loaders/materialx/MaterialXNodeLibrary.js b/examples/jsm/loaders/materialx/MaterialXNodeLibrary.js
new file mode 100644
index 00000000000000..7b29d1cee4c03e
--- /dev/null
+++ b/examples/jsm/loaders/materialx/MaterialXNodeLibrary.js
@@ -0,0 +1,1019 @@
+import {
+ abs,
+ add,
+ clamp,
+ floor,
+ ceil,
+ round,
+ sign,
+ sin,
+ cos,
+ tan,
+ asin,
+ acos,
+ sqrt,
+ log,
+ exp,
+ min,
+ max,
+ normalize,
+ length,
+ dot,
+ cross,
+ mul,
+ div,
+ pow,
+ distance,
+ remap,
+ luminance,
+ mx_rgbtohsv,
+ mx_hsvtorgb,
+ mix,
+ saturation as mx_saturation,
+ transpose,
+ determinant,
+ inverse,
+ mat3,
+ mx_ramp4,
+ mx_ramplr,
+ mx_ramptb,
+ mx_splitlr,
+ mx_splittb,
+ mx_fractal_noise_float,
+ mx_fractal_noise_vec3,
+ mx_noise_float,
+ mx_noise_vec3,
+ mx_cell_noise_float,
+ mx_cell_noise_vec3,
+ mx_smoothstep,
+ mx_worley_noise_float_2d,
+ mx_worley_noise_float_3d,
+ mx_worley_noise_vec3_style,
+ mx_unifiednoise2d,
+ mx_unifiednoise3d,
+ mx_modulo,
+ mx_invert,
+ mx_place2d,
+ mx_rotate2d,
+ mx_rotate3d,
+ mx_safepower,
+ mx_contrast,
+ element,
+ reflect,
+ refract,
+ mx_timer,
+ mx_frame,
+ mx_ifgreater,
+ mx_ifgreatereq,
+ mx_ifequal,
+ mx_atan2,
+ positionLocal,
+ positionWorld,
+ normalWorld,
+ tangentWorld,
+ bitangentWorld,
+ cameraPosition,
+ mx_heighttonormal,
+ float,
+ int,
+ bool,
+ color,
+ modelNormalMatrix,
+ modelWorldMatrix,
+ modelWorldMatrixInverse,
+ vec2,
+ vec3,
+ vec4,
+ fract,
+ sub,
+ step,
+ and as tslAnd,
+ or as tslOr,
+ xor as tslXor,
+ not as tslNot,
+ Fn,
+ Loop,
+} from 'three/tsl';
+import { normalizeSpaceName, toBooleanNode, toVec3Channels } from './MaterialXUtils.js';
+
+const createMXElement = ( name, nodeFunc, params = [], defaults = {}, usesNode = false ) => ( { name, nodeFunc, params, defaults, usesNode } );
+
+const mx_range = ( inNode, inLow, inHigh, outLow, outHigh, gamma = 1 ) => {
+
+ const inSpan = max( sub( inHigh, inLow ), 1e-6 );
+ const normalized = div( sub( inNode, inLow ), inSpan );
+ const reciprocalGamma = div( 1, gamma );
+ const gammaApplied = mul( pow( abs( normalized ), reciprocalGamma ), sign( normalized ) );
+ return add( outLow, mul( gammaApplied, sub( outHigh, outLow ) ) );
+
+};
+
+const mx_open_pbr_anisotropy = ( roughness = 0, anisotropy = 0 ) => {
+
+ const anisoInvert = sub( float( 1 ), anisotropy );
+ const anisoInvertSq = mul( anisoInvert, anisoInvert );
+ const denom = add( anisoInvertSq, float( 1 ) );
+ const fraction = div( float( 2 ), denom );
+ const sqrtFraction = sqrt( fraction );
+ const roughSq = mul( roughness, roughness );
+ const alphaX = mul( roughSq, sqrtFraction );
+ const alphaY = mul( anisoInvert, alphaX );
+ return vec2( alphaX, alphaY );
+
+};
+
+const mx_boolean = ( inNode ) => toBooleanNode( inNode );
+const mx_and = ( in1, in2 ) => tslAnd( mx_boolean( in1 ), mx_boolean( in2 ) );
+const mx_or = ( in1, in2 ) => tslOr( mx_boolean( in1 ), mx_boolean( in2 ) );
+const mx_xor = ( in1, in2 ) => tslXor( mx_boolean( in1 ), mx_boolean( in2 ) );
+const mx_not = ( inNode ) => tslNot( mx_boolean( inNode ) );
+const mx_checkerboard = ( color1, color2, uvtiling, uvoffset, texcoord ) => {
+
+ const tiledUv = sub( mul( texcoord, uvtiling ), uvoffset );
+ const checkerMix = mx_modulo( dot( floor( tiledUv ), vec2( 1, 1 ) ), float( 2 ) );
+ return mix( color2, color1, checkerMix );
+
+};
+
+const mx_circle = ( texcoord, center, radius ) => {
+
+ const delta = sub( texcoord, center );
+ const distanceSquared = dot( delta, delta );
+ const radiusSquared = mul( radius, radius );
+ return mx_ifgreater( distanceSquared, radiusSquared, 0, 1 );
+
+};
+
+const mx_normalmap = (
+ value = vec3( 0.5, 0.5, 1.0 ),
+ scale = 1.0,
+ normal = vec3( 0.0, 0.0, 1.0 ),
+ tangent = vec3( 1.0, 0.0, 0.0 ),
+ bitangent = vec3( 0.0, 1.0, 0.0 ),
+) => {
+
+ const mapValue = toVec3Channels( value );
+ const decoded = sub( mul( mapValue, 2.0 ), vec3( 1.0, 1.0, 1.0 ) );
+ const safeValue = dot( mapValue, mapValue ).equal( 0 ).mix( decoded, vec3( 0.0, 0.0, 1.0 ) );
+ const normalScale = vec2( scale );
+ const blended =
+ add(
+ add(
+ mul( tangent, mul( element( safeValue, 0 ), element( normalScale, 0 ) ) ),
+ mul( bitangent, mul( element( safeValue, 1 ), element( normalScale, 1 ) ) ),
+ ),
+ mul( normal, element( safeValue, 2 ) ),
+ );
+ return normalize( blended );
+
+};
+
+const mx_bump = (
+ height,
+ scale = 1,
+ normal = vec3( 0.0, 0.0, 1.0 ),
+ tangent = vec3( 1.0, 0.0, 0.0 ),
+ bitangent = vec3( 0.0, 1.0, 0.0 ),
+) => mx_normalmap( mx_heighttonormal( height, 1 ), scale, normal, tangent, bitangent );
+const mx_dot = ( inNode ) => inNode;
+const mx_viewdirection = ( space = 'world' ) => {
+
+ const outputSpace = normalizeSpaceName( space, 'world' );
+ const worldDirection = normalize( sub( positionWorld, cameraPosition ) );
+
+ if ( outputSpace === 'world' ) {
+
+ return worldDirection;
+
+ }
+
+ return mx_transformnormal( worldDirection, 'world', outputSpace );
+
+};
+const mx_blackbody = ( temperature = 5000 ) => {
+
+ const temperatureKelvin = clamp( temperature, float( 800 ), float( 25000 ) );
+ const t = div( float( 1000 ), temperatureKelvin );
+ const t2 = mul( t, t );
+ const t3 = mul( t2, t );
+ const lowX = add( add( mul( float( - 0.2661239 ), t3 ), mul( float( - 0.234358 ), t2 ) ), add( mul( float( 0.8776956 ), t ), float( 0.17991 ) ) );
+ const highX = add(
+ add( mul( float( - 3.0258469 ), t3 ), mul( float( 2.1070379 ), t2 ) ),
+ add( mul( float( 0.2226347 ), t ), float( 0.24039 ) ),
+ );
+ const xc = mx_ifgreatereq( temperatureKelvin, float( 4000 ), highX, lowX );
+ const xc2 = mul( xc, xc );
+ const xc3 = mul( xc2, xc );
+ const ycLow = add(
+ add( mul( float( - 1.1063814 ), xc3 ), mul( float( - 1.3481102 ), xc2 ) ),
+ add( mul( float( 2.18555832 ), xc ), float( - 0.20219683 ) ),
+ );
+ const ycMid = add(
+ add( mul( float( - 0.9549476 ), xc3 ), mul( float( - 1.37418593 ), xc2 ) ),
+ add( mul( float( 2.09137015 ), xc ), float( - 0.16748867 ) ),
+ );
+ const ycHigh = add(
+ add( mul( float( 3.081758 ), xc3 ), mul( float( - 5.8733867 ), xc2 ) ),
+ add( mul( float( 3.75112997 ), xc ), float( - 0.37001483 ) ),
+ );
+ const ycLowMid = mx_ifgreatereq( temperatureKelvin, float( 2222 ), ycMid, ycLow );
+ const yc = mx_ifgreatereq( temperatureKelvin, float( 4000 ), ycHigh, ycLowMid );
+ const safeYc = max( yc, float( 1e-6 ) );
+ const xyz = vec3( div( xc, safeYc ), float( 1 ), div( sub( sub( float( 1 ), xc ), yc ), safeYc ) );
+ const rgb = vec3(
+ add( add( mul( float( 3.2406 ), element( xyz, 0 ) ), mul( float( - 1.5372 ), element( xyz, 1 ) ) ), mul( float( - 0.4986 ), element( xyz, 2 ) ) ),
+ add( add( mul( float( - 0.9689 ), element( xyz, 0 ) ), mul( float( 1.8758 ), element( xyz, 1 ) ) ), mul( float( 0.0415 ), element( xyz, 2 ) ) ),
+ add( add( mul( float( 0.0557 ), element( xyz, 0 ) ), mul( float( - 0.204 ), element( xyz, 1 ) ) ), mul( float( 1.057 ), element( xyz, 2 ) ) ),
+ );
+ const clampedRgb = max( rgb, vec3( 0, 0, 0 ) );
+ const validYcMask = step( float( 1e-6 ), yc );
+ return mix( vec3( 1, 1, 1 ), clampedRgb, validYcMask );
+
+};
+
+const mx_unpremult = ( input ) => {
+
+ const alpha = element( input, 3 );
+ const rgb = toVec3Channels( input );
+ const unpremultiplied = alpha.equal( 0 ).mix( rgb, div( rgb, alpha ) );
+ return vec4( unpremultiplied, alpha );
+
+};
+
+const mx_colorcorrect = (
+ input,
+ hue = 0,
+ saturationAmount = 1,
+ gamma = 1,
+ lift = 0,
+ gain = 1,
+ contrast = 1,
+ contrastPivot = 0.5,
+ exposure = 0,
+) => {
+
+ const rgbInput = toVec3Channels( input );
+ const hsv = mx_rgbtohsv( rgbInput );
+ const hueAdjusted = mx_hsvtorgb( add( hsv, vec3( hue, 0, 0 ) ) );
+ const saturationAdjusted = mx_saturation( hueAdjusted, saturationAmount );
+ const gammaAdjusted = mx_range( saturationAdjusted, 0, 1, 0, 1, gamma );
+ const liftApplied = add( mul( gammaAdjusted, sub( 1, lift ) ), lift );
+ const gainApplied = mul( liftApplied, gain );
+ const contrastApplied = mx_contrast( gainApplied, contrast, contrastPivot );
+ const exposureApplied = mul( contrastApplied, pow( 2, exposure ) );
+ const preserveAlpha = input && ( input.nodeType === 'vec4' || input.nodeType === 'color4' );
+ return preserveAlpha ? vec4( exposureApplied, element( input, 3 ) ) : exposureApplied;
+
+};
+
+const mx_minus = ( fg, bg, mixval = 1 ) => add( mul( mixval, sub( bg, fg ) ), mul( sub( 1, mixval ), bg ) );
+const mx_difference = ( fg, bg, mixval = 1 ) => add( mul( mixval, abs( sub( bg, fg ) ) ), mul( sub( 1, mixval ), bg ) );
+const mx_screen = ( fg, bg, mixval = 1 ) => {
+
+ const screened = sub( 1, mul( sub( 1, fg ), sub( 1, bg ) ) );
+ return mix( bg, screened, mixval );
+
+};
+
+const mx_overlay = ( fg, bg, mixval = 1 ) => {
+
+ const lowBranch = mul( mul( 2, fg ), bg );
+ const highBranch = sub( 1, mul( mul( 2, sub( 1, fg ) ), sub( 1, bg ) ) );
+ const overlayed = mix( lowBranch, highBranch, step( 0.5, bg ) );
+ return mix( bg, overlayed, mixval );
+
+};
+
+const mx_transformnormal = ( inNode = vec3( 0, 0, 1 ), fromspace = 'world', tospace = 'world' ) => {
+
+ const from = normalizeSpaceName( fromspace, 'world' );
+ const to = normalizeSpaceName( tospace, 'world' );
+ const inNormal = vec3( inNode );
+
+ if ( from === to ) {
+
+ return normalize( inNormal );
+
+ }
+
+ if ( from === 'object' && to === 'world' ) {
+
+ return normalize( mul( inNormal, modelNormalMatrix ) );
+
+ }
+
+ return normalize( mul( inNormal, mat3( modelWorldMatrix ) ) );
+
+};
+
+const mx_transformvector = ( inNode = vec3( 0, 0, 0 ), fromspace = 'world', tospace = 'world' ) => {
+
+ const from = normalizeSpaceName( fromspace, 'world' );
+ const to = normalizeSpaceName( tospace, 'world' );
+ const inVector = vec3( inNode );
+
+ if ( from === to ) {
+
+ return inVector;
+
+ }
+
+ if ( from === 'object' && to === 'world' ) {
+
+ return mul( inVector, mat3( modelWorldMatrix ) );
+
+ }
+
+ return mul( inVector, mat3( modelWorldMatrixInverse ) );
+
+};
+
+const mx_transformpoint = ( inNode = vec3( 0, 0, 0 ), fromspace = 'world', tospace = 'world' ) => {
+
+ const from = normalizeSpaceName( fromspace, 'world' );
+ const to = normalizeSpaceName( tospace, 'world' );
+ const inPoint = vec3( inNode );
+
+ if ( from === to ) {
+
+ return inPoint;
+
+ }
+
+ const point4 = vec4( inPoint, 1 );
+ const matrix = from === 'object' && to === 'world' ? modelWorldMatrix : modelWorldMatrixInverse;
+ const transformed4 = mul( matrix, point4 );
+ return vec3( element( transformed4, 0 ), element( transformed4, 1 ), element( transformed4, 2 ) );
+
+};
+
+const mx_burn_channel = ( fg, bg, mixval = 1 ) => {
+
+ const composed = add( mul( mixval, sub( 1, div( sub( 1, bg ), fg ) ) ), mul( sub( 1, mixval ), bg ) );
+ return mul( composed, step( float( 1e-6 ), abs( fg ) ) );
+
+};
+
+const mx_dodge_channel = ( fg, bg, mixval = 1 ) => {
+
+ const composed = add( mul( mixval, div( bg, sub( 1, fg ) ) ), mul( sub( 1, mixval ), bg ) );
+ return mul( composed, step( float( 1e-6 ), abs( sub( 1, fg ) ) ) );
+
+};
+
+const isVec3Like = ( node ) =>
+ node && ( node.nodeType === 'vec3' || node.nodeType === 'color' || node.nodeType === 'color3' );
+const isVec4Like = ( node ) => node && ( node.nodeType === 'vec4' || node.nodeType === 'color4' );
+
+const applyBlendByChannel = ( channelFunc, fg, bg, mixval = 1 ) => {
+
+ if ( isVec4Like( fg ) || isVec4Like( bg ) ) {
+
+ return vec4(
+ channelFunc( element( fg, 0 ), element( bg, 0 ), mixval ),
+ channelFunc( element( fg, 1 ), element( bg, 1 ), mixval ),
+ channelFunc( element( fg, 2 ), element( bg, 2 ), mixval ),
+ channelFunc( element( fg, 3 ), element( bg, 3 ), mixval ),
+ );
+
+ }
+
+ if ( isVec3Like( fg ) || isVec3Like( bg ) ) {
+
+ return vec3(
+ channelFunc( element( fg, 0 ), element( bg, 0 ), mixval ),
+ channelFunc( element( fg, 1 ), element( bg, 1 ), mixval ),
+ channelFunc( element( fg, 2 ), element( bg, 2 ), mixval ),
+ );
+
+ }
+
+ return channelFunc( fg, bg, mixval );
+
+};
+
+const mx_burn = ( fg, bg, mixval = 1 ) => applyBlendByChannel( mx_burn_channel, fg, bg, mixval );
+const mx_dodge = ( fg, bg, mixval = 1 ) => applyBlendByChannel( mx_dodge_channel, fg, bg, mixval );
+
+const mixColor4 = ( bg, fg, factor ) =>
+ vec4(
+ mix( element( bg, 0 ), element( fg, 0 ), factor ),
+ mix( element( bg, 1 ), element( fg, 1 ), factor ),
+ mix( element( bg, 2 ), element( fg, 2 ), factor ),
+ mix( element( bg, 3 ), element( fg, 3 ), factor ),
+ );
+
+const mxRampSegment = ( x, color1, color2, interval1, interval2, interpolation ) => {
+
+ const linearClamped = clamp( x, interval1, interval2 );
+ const rangeSize = sub( interval2, interval1 );
+ const safeRange = max( rangeSize, float( 1e-6 ) );
+ const linearRemap = div( sub( linearClamped, interval1 ), safeRange );
+ const smoothVal = mx_smoothstep( x, interval1, interval2 );
+ const interpolationDistanceToLinear = abs( sub( interpolation, float( 0 ) ) );
+ const useLinear = sub( float( 1 ), step( float( 0.5 ), interpolationDistanceToLinear ) );
+ const interpFactor = mix( smoothVal, linearRemap, useLinear );
+ const mixedColor = mixColor4( color1, color2, interpFactor );
+ const stepColor = mixColor4( color1, color2, step( interval2, x ) );
+ const interpolationDistanceToStep = abs( sub( interpolation, float( 2 ) ) );
+ const useStep = sub( float( 1 ), step( float( 0.5 ), interpolationDistanceToStep ) );
+ return mixColor4( mixedColor, stepColor, useStep );
+
+};
+
+const mx_ramp_gradient = (
+ x = 0,
+ interval1 = 0,
+ interval2 = 1,
+ color1 = vec4( 0, 0, 0, 1 ),
+ color2 = vec4( 1, 1, 1, 1 ),
+ interpolation = 1,
+ prevColor = vec4( 0, 0, 0, 1 ),
+ intervalNum = 1,
+ numIntervals = 2,
+) => {
+
+ const xFloat = float( x );
+ const interval1Float = float( interval1 );
+ const interval2Float = float( interval2 );
+ const interpolationFloat = float( interpolation );
+ const intervalNumFloat = float( intervalNum );
+ const numIntervalsFloat = float( numIntervals );
+ const interpolated = mxRampSegment( xFloat, color1, color2, interval1Float, interval2Float, interpolationFloat );
+ const withinInterval = mixColor4( prevColor, interpolated, step( add( interval1Float, float( 1e-6 ) ), xFloat ) );
+ return mixColor4( withinInterval, prevColor, step( numIntervalsFloat, intervalNumFloat ) );
+
+};
+
+const mx_ramp = ( texcoord = vec2( 0, 0 ), type = 0, interpolation = 1, numIntervals = 2, ...rest ) => {
+
+ const rampTypeFloat = float( type );
+ const interpolationFloat = float( interpolation );
+ const numIntervalsFloat = float( numIntervals );
+
+ const clamped = clamp( texcoord, vec2( 0, 0 ), vec2( 1, 1 ) );
+ const s = element( clamped, 0 );
+ const t = element( clamped, 1 );
+
+ const centeredS = sub( s, float( 0.5 ) );
+ const centeredT = sub( t, float( 0.5 ) );
+
+ const radialDist = sqrt( add( mul( centeredS, centeredS ), mul( centeredT, centeredT ) ) );
+ const radialVal = clamp( mul( radialDist, float( 2 ) ), float( 0 ), float( 1 ) );
+ const circularAngle = add( div( mx_atan2( centeredT, centeredS ), float( Math.PI * 2 ) ), float( 0.5 ) );
+ const boxVal = clamp( mul( max( abs( centeredS ), abs( centeredT ) ), float( 2 ) ), float( 0 ), float( 1 ) );
+
+ const typeDistanceToRadial = abs( sub( rampTypeFloat, float( 1 ) ) );
+ const typeDistanceToCircular = abs( sub( rampTypeFloat, float( 2 ) ) );
+ const typeDistanceToBox = abs( sub( rampTypeFloat, float( 3 ) ) );
+ const useRadial = sub( float( 1 ), step( float( 0.5 ), typeDistanceToRadial ) );
+ const useCircular = sub( float( 1 ), step( float( 0.5 ), typeDistanceToCircular ) );
+ const useBox = sub( float( 1 ), step( float( 0.5 ), typeDistanceToBox ) );
+ const afterRadial = mix( s, radialVal, useRadial );
+ const afterCircular = mix( afterRadial, circularAngle, useCircular );
+ const rampX = mix( afterCircular, boxVal, useBox );
+
+ const intervals = [];
+ const colors = [];
+ for ( let i = 0; i < 10; i += 1 ) {
+
+ const intervalInput = rest[ i * 2 ];
+ const colorInput = rest[ i * 2 + 1 ];
+ intervals.push( intervalInput ?? float( i <= 1 ? i : 1 ) );
+ colors.push( colorInput ?? vec4( i === 0 ? 0 : 1, i === 0 ? 0 : 1, i === 0 ? 0 : 1, 1 ) );
+
+ }
+
+ let result = colors[ 0 ];
+ for ( let i = 0; i < 9; i += 1 ) {
+
+ const iv1 = intervals[ i ];
+ const iv2 = intervals[ i + 1 ];
+ const c1 = colors[ i ];
+ const c2 = colors[ i + 1 ];
+ const intNum = float( i + 1 );
+
+ const interpolated = mxRampSegment( rampX, c1, c2, iv1, iv2, interpolationFloat );
+ const withinInterval = mixColor4( result, interpolated, step( add( iv1, float( 1e-6 ) ), rampX ) );
+ result = mixColor4( withinInterval, result, step( numIntervalsFloat, intNum ) );
+
+ }
+
+ return result;
+
+};
+
+const defaultFloat = ( value ) => () => float( value );
+const defaultInt = ( value ) => () => int( value );
+const defaultBool = ( value ) => () => bool( value );
+const defaultColor = ( r, g, b ) => () => color( r, g, b );
+const defaultVec2 = ( x, y ) => () => vec2( x, y );
+const defaultVec3 = ( x, y, z ) => () => vec3( x, y, z );
+const defaultVec4 = ( x, y, z, w ) => () => vec4( x, y, z, w );
+const usesVec2Noise = ( nodeX ) => nodeX && nodeX.type === 'vector2';
+const usesVec3Noise = ( nodeX ) => nodeX && ( nodeX.type === 'vector3' || nodeX.type === 'color3' );
+
+const mx_noise_materialx = ( texcoord, amplitude, pivot, nodeX ) =>
+ usesVec3Noise( nodeX ) ? mx_noise_vec3( texcoord, vec3( amplitude ), pivot ) : mx_noise_float( texcoord, amplitude, pivot );
+
+const mx_fractal_noise_materialx_2d = ( texcoord, octaves, lacunarity, diminish, amplitude, nodeX ) =>
+ usesVec3Noise( nodeX ) ? mx_fractal_noise_vec3_materialx_2d( texcoord, octaves, lacunarity, diminish, amplitude ) : mx_fractal_noise_float_materialx_2d( texcoord, octaves, lacunarity, diminish, amplitude );
+
+const mx_fractal_noise_materialx_3d = ( position, octaves, lacunarity, diminish, amplitude, nodeX ) =>
+ usesVec3Noise( nodeX ) ? mx_fractal_noise_vec3( position, octaves, lacunarity, diminish, vec3( amplitude ) ) : mx_fractal_noise_float( position, octaves, lacunarity, diminish, amplitude );
+
+const mx_cell_noise_materialx = ( position, nodeX ) =>
+ usesVec3Noise( nodeX ) ? mx_cell_noise_vec3( position ) : mx_cell_noise_float( position );
+
+const mx_worley_noise_vec2_style = ( position, jitter, style ) => {
+
+ const result = mx_worley_noise_vec3_style( position, jitter, style, 0 );
+ return vec2( element( result, 0 ), element( result, 1 ) );
+
+};
+
+const mx_worley_noise_materialx_2d = ( texcoord, jitter, style, nodeX ) =>
+ usesVec3Noise( nodeX ) ? mx_worley_noise_vec3_style( texcoord, jitter, style, 0 ) : usesVec2Noise( nodeX ) ? mx_worley_noise_vec2_style( texcoord, jitter, style ) : mx_worley_noise_float_2d( texcoord, jitter, style );
+
+const mx_worley_noise_materialx_3d = ( position, jitter, style, nodeX ) =>
+ usesVec3Noise( nodeX ) ? mx_worley_noise_vec3_style( position, jitter, style, 0 ) : usesVec2Noise( nodeX ) ? mx_worley_noise_vec2_style( position, jitter, style ) : mx_worley_noise_float_3d( position, jitter, style );
+
+const mx_fractal_noise_float_materialx_2d = Fn( ( [ texcoordInput, octavesInput, lacunarityInput, diminishInput, amplitudeInput ] ) => {
+
+ const texcoord = vec2( texcoordInput ).toVar();
+ const octaves = int( octavesInput ).toVar();
+ const lacunarity = float( lacunarityInput ).toVar();
+ const diminish = float( diminishInput ).toVar();
+ const amplitude = float( amplitudeInput ).toVar();
+ const result = float( 0 ).toVar();
+ const octaveAmplitude = float( 1 ).toVar();
+
+ Loop( octaves, () => {
+
+ result.addAssign( mul( octaveAmplitude, mx_noise_float( texcoord, float( 1 ), float( 0 ) ) ) );
+ octaveAmplitude.mulAssign( diminish );
+ texcoord.mulAssign( lacunarity );
+
+ } );
+
+ return mul( result, amplitude );
+
+} );
+
+const mx_fractal_noise_vec3_materialx_2d = Fn( ( [ texcoordInput, octavesInput, lacunarityInput, diminishInput, amplitudeInput ] ) => {
+
+ const texcoord = vec2( texcoordInput ).toVar();
+ const octaves = int( octavesInput ).toVar();
+ const lacunarity = float( lacunarityInput ).toVar();
+ const diminish = float( diminishInput ).toVar();
+ const amplitude = vec3( amplitudeInput ).toVar();
+ const result = vec3( 0 ).toVar();
+ const octaveAmplitude = float( 1 ).toVar();
+
+ Loop( octaves, () => {
+
+ result.addAssign( mul( octaveAmplitude, mx_noise_vec3( texcoord, vec3( 1 ), float( 0 ) ) ) );
+ octaveAmplitude.mulAssign( diminish );
+ texcoord.mulAssign( lacunarity );
+
+ } );
+
+ return mul( result, amplitude );
+
+} );
+
+const MXElements = [
+ createMXElement( 'add', add, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 0 ) } ),
+ createMXElement( 'subtract', sub, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 0 ) } ),
+ createMXElement( 'multiply', mul, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 1 ) } ),
+ createMXElement( 'divide', div, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 1 ) } ),
+ createMXElement( 'modulo', mx_modulo, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 1 ) } ),
+ createMXElement( 'absval', abs, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'sign', sign, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'floor', floor, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'ceil', ceil, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'round', round, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'power', pow, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 1 ) } ),
+ createMXElement( 'sin', sin, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'cos', cos, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'tan', tan, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'asin', asin, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'acos', acos, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'atan2', mx_atan2, [ 'iny', 'inx' ], { iny: defaultFloat( 0 ), inx: defaultFloat( 1 ) } ),
+ createMXElement( 'sqrt', sqrt, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'ln', log, [ 'in' ], { in: defaultFloat( 1 ) } ),
+ createMXElement( 'exp', exp, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'fract', fract, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'clamp', clamp, [ 'in', 'low', 'high' ], {
+ in: defaultFloat( 0 ),
+ low: defaultFloat( 0 ),
+ high: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'min', min, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 0 ) } ),
+ createMXElement( 'max', max, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 0 ) } ),
+ createMXElement( 'normalize', normalize, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'magnitude', length, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'length', length, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'dot', mx_dot, [ 'in' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'dotproduct', dot, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 0 ) } ),
+ createMXElement( 'viewdirection', mx_viewdirection, [ 'space' ], { space: () => 'world' } ),
+ createMXElement( 'crossproduct', cross, [ 'in1', 'in2' ], { in1: defaultVec3( 0, 0, 0 ), in2: defaultVec3( 0, 0, 0 ) } ),
+ createMXElement( 'distance', distance, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 0 ) } ),
+ createMXElement( 'invert', mx_invert, [ 'in', 'amount' ], { in: defaultFloat( 0 ), amount: defaultFloat( 1 ) } ),
+ createMXElement( 'transformmatrix', mul, [ 'in', 'mat' ], { in: defaultFloat( 0 ) } ),
+ createMXElement( 'transformnormal', mx_transformnormal, [ 'in', 'fromspace', 'tospace' ], {
+ in: defaultVec3( 0, 0, 1 ),
+ fromspace: () => 'world',
+ tospace: () => 'world',
+ } ),
+ createMXElement( 'transformpoint', mx_transformpoint, [ 'in', 'fromspace', 'tospace' ], {
+ in: defaultVec3( 0, 0, 0 ),
+ fromspace: () => 'world',
+ tospace: () => 'world',
+ } ),
+ createMXElement( 'transformvector', mx_transformvector, [ 'in', 'fromspace', 'tospace' ], {
+ in: defaultVec3( 0, 0, 0 ),
+ fromspace: () => 'world',
+ tospace: () => 'world',
+ } ),
+ createMXElement( 'normalmap', mx_normalmap, [ 'in', 'scale', 'normal', 'tangent', 'bitangent' ], {
+ in: defaultVec3( 0.5, 0.5, 1.0 ),
+ scale: defaultFloat( 1 ),
+ normal: () => normalWorld,
+ tangent: () => tangentWorld,
+ bitangent: () => bitangentWorld,
+ } ),
+ createMXElement( 'transpose', transpose, [ 'in' ] ),
+ createMXElement( 'determinant', determinant, [ 'in' ] ),
+ createMXElement( 'invertmatrix', inverse, [ 'in' ] ),
+ createMXElement( 'creatematrix', mat3, [ 'in1', 'in2', 'in3' ], {
+ in1: defaultVec3( 1, 0, 0 ),
+ in2: defaultVec3( 0, 1, 0 ),
+ in3: defaultVec3( 0, 0, 1 ),
+ } ),
+ createMXElement( 'remap', remap, [ 'in', 'inlow', 'inhigh', 'outlow', 'outhigh' ], {
+ in: defaultFloat( 0 ),
+ inlow: defaultFloat( 0 ),
+ inhigh: defaultFloat( 1 ),
+ outlow: defaultFloat( 0 ),
+ outhigh: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'range', mx_range, [ 'in', 'inlow', 'inhigh', 'outlow', 'outhigh', 'gamma' ], {
+ in: defaultFloat( 0 ),
+ inlow: defaultFloat( 0 ),
+ inhigh: defaultFloat( 1 ),
+ outlow: defaultFloat( 0 ),
+ outhigh: defaultFloat( 1 ),
+ gamma: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'open_pbr_anisotropy', mx_open_pbr_anisotropy, [ 'roughness', 'anisotropy' ], {
+ roughness: defaultFloat( 0 ),
+ anisotropy: defaultFloat( 0 ),
+ } ),
+ createMXElement( 'smoothstep', mx_smoothstep, [ 'in', 'low', 'high' ], {
+ in: defaultFloat( 0 ),
+ low: defaultFloat( 0 ),
+ high: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'luminance', luminance, [ 'in', 'lumacoeffs' ], {
+ in: defaultColor( 0, 0, 0 ),
+ lumacoeffs: defaultColor( 0.2722287, 0.6740818, 0.0536895 ),
+ } ),
+ createMXElement( 'rgbtohsv', mx_rgbtohsv, [ 'in' ], { in: defaultColor( 0, 0, 0 ) } ),
+ createMXElement( 'hsvtorgb', mx_hsvtorgb, [ 'in' ], { in: defaultColor( 0, 0, 0 ) } ),
+ createMXElement( 'mix', mix, [ 'bg', 'fg', 'mix' ], { bg: defaultFloat( 0 ), fg: defaultFloat( 0 ), mix: defaultFloat( 0 ) } ),
+ createMXElement( 'minus', mx_minus, [ 'fg', 'bg', 'mix' ], {
+ fg: defaultFloat( 0 ),
+ bg: defaultFloat( 0 ),
+ mix: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'difference', mx_difference, [ 'fg', 'bg', 'mix' ], {
+ fg: defaultFloat( 0 ),
+ bg: defaultFloat( 0 ),
+ mix: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'screen', mx_screen, [ 'fg', 'bg', 'mix' ], {
+ fg: defaultFloat( 0 ),
+ bg: defaultFloat( 0 ),
+ mix: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'overlay', mx_overlay, [ 'fg', 'bg', 'mix' ], {
+ fg: defaultFloat( 0 ),
+ bg: defaultFloat( 0 ),
+ mix: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'burn', mx_burn, [ 'fg', 'bg', 'mix' ], {
+ fg: defaultFloat( 0 ),
+ bg: defaultFloat( 0 ),
+ mix: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'dodge', mx_dodge, [ 'fg', 'bg', 'mix' ], {
+ fg: defaultFloat( 0 ),
+ bg: defaultFloat( 0 ),
+ mix: defaultFloat( 1 ),
+ } ),
+ createMXElement(
+ 'colorcorrect',
+ mx_colorcorrect,
+ [ 'in', 'hue', 'saturation', 'gamma', 'lift', 'gain', 'contrast', 'contrastpivot', 'exposure' ],
+ {
+ in: defaultColor( 1, 1, 1 ),
+ hue: defaultFloat( 0 ),
+ saturation: defaultFloat( 1 ),
+ gamma: defaultFloat( 1 ),
+ lift: defaultFloat( 0 ),
+ gain: defaultFloat( 1 ),
+ contrast: defaultFloat( 1 ),
+ contrastpivot: defaultFloat( 0.5 ),
+ exposure: defaultFloat( 0 ),
+ },
+ ),
+ createMXElement( 'unpremult', mx_unpremult, [ 'in' ], { in: defaultVec4( 0, 0, 0, 1 ) } ),
+ createMXElement( 'combine2', vec2, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 0 ) } ),
+ createMXElement( 'combine3', vec3, [ 'in1', 'in2', 'in3' ], {
+ in1: defaultFloat( 0 ),
+ in2: defaultFloat( 0 ),
+ in3: defaultFloat( 0 ),
+ } ),
+ createMXElement( 'combine4', vec4, [ 'in1', 'in2', 'in3', 'in4' ], {
+ in1: defaultFloat( 0 ),
+ in2: defaultFloat( 0 ),
+ in3: defaultFloat( 0 ),
+ in4: defaultFloat( 0 ),
+ } ),
+ createMXElement( 'ramplr', mx_ramplr, [ 'valuel', 'valuer', 'texcoord' ], {
+ valuel: defaultFloat( 0 ),
+ valuer: defaultFloat( 0 ),
+ } ),
+ createMXElement( 'ramptb', mx_ramptb, [ 'valueb', 'valuet', 'texcoord' ], {
+ valueb: defaultFloat( 0 ),
+ valuet: defaultFloat( 0 ),
+ } ),
+ createMXElement( 'ramp4', mx_ramp4, [ 'valuetl', 'valuetr', 'valuebl', 'valuebr', 'texcoord' ], {
+ valuetl: defaultColor( 0, 0, 0 ),
+ valuetr: defaultColor( 0, 0, 0 ),
+ valuebl: defaultColor( 0, 0, 0 ),
+ valuebr: defaultColor( 0, 0, 0 ),
+ texcoord: defaultVec2( 0, 0 ),
+ } ),
+ createMXElement(
+ 'ramp_gradient',
+ mx_ramp_gradient,
+ [ 'x', 'interval1', 'interval2', 'color1', 'color2', 'interpolation', 'prev_color', 'interval_num', 'num_intervals' ],
+ {
+ x: defaultFloat( 0 ),
+ interval1: defaultFloat( 0 ),
+ interval2: defaultFloat( 1 ),
+ color1: defaultVec4( 0, 0, 0, 1 ),
+ color2: defaultVec4( 1, 1, 1, 1 ),
+ interpolation: defaultFloat( 1 ),
+ prev_color: defaultVec4( 0, 0, 0, 1 ),
+ interval_num: defaultFloat( 1 ),
+ num_intervals: defaultFloat( 2 ),
+ },
+ ),
+ createMXElement(
+ 'ramp',
+ mx_ramp,
+ [
+ 'texcoord',
+ 'type',
+ 'interpolation',
+ 'num_intervals',
+ 'interval1',
+ 'color1',
+ 'interval2',
+ 'color2',
+ 'interval3',
+ 'color3',
+ 'interval4',
+ 'color4',
+ 'interval5',
+ 'color5',
+ 'interval6',
+ 'color6',
+ 'interval7',
+ 'color7',
+ 'interval8',
+ 'color8',
+ 'interval9',
+ 'color9',
+ 'interval10',
+ 'color10',
+ ],
+ {
+ texcoord: defaultVec2( 0, 0 ),
+ type: defaultFloat( 0 ),
+ interpolation: defaultFloat( 1 ),
+ num_intervals: defaultFloat( 2 ),
+ interval1: defaultFloat( 0 ),
+ color1: defaultVec4( 0, 0, 0, 1 ),
+ interval2: defaultFloat( 1 ),
+ color2: defaultVec4( 1, 1, 1, 1 ),
+ interval3: defaultFloat( 1 ),
+ color3: defaultVec4( 1, 1, 1, 1 ),
+ interval4: defaultFloat( 1 ),
+ color4: defaultVec4( 1, 1, 1, 1 ),
+ interval5: defaultFloat( 1 ),
+ color5: defaultVec4( 1, 1, 1, 1 ),
+ interval6: defaultFloat( 1 ),
+ color6: defaultVec4( 1, 1, 1, 1 ),
+ interval7: defaultFloat( 1 ),
+ color7: defaultVec4( 1, 1, 1, 1 ),
+ interval8: defaultFloat( 1 ),
+ color8: defaultVec4( 1, 1, 1, 1 ),
+ interval9: defaultFloat( 1 ),
+ color9: defaultVec4( 1, 1, 1, 1 ),
+ interval10: defaultFloat( 1 ),
+ color10: defaultVec4( 1, 1, 1, 1 ),
+ },
+ ),
+ createMXElement( 'splitlr', mx_splitlr, [ 'valuel', 'valuer', 'center', 'texcoord' ], {
+ valuel: defaultFloat( 0 ),
+ valuer: defaultFloat( 0 ),
+ center: defaultFloat( 0.5 ),
+ } ),
+ createMXElement( 'splittb', mx_splittb, [ 'valueb', 'valuet', 'center', 'texcoord' ], {
+ valueb: defaultFloat( 0 ),
+ valuet: defaultFloat( 0 ),
+ center: defaultFloat( 0.5 ),
+ } ),
+ createMXElement( 'noise2d', mx_noise_materialx, [ 'texcoord', 'amplitude', 'pivot' ], {
+ texcoord: defaultVec2( 0, 0 ),
+ amplitude: defaultFloat( 1 ),
+ pivot: defaultFloat( 0 ),
+ }, true ),
+ createMXElement( 'noise3d', mx_noise_materialx, [ 'position', 'amplitude', 'pivot' ], {
+ position: () => positionLocal,
+ amplitude: defaultFloat( 1 ),
+ pivot: defaultFloat( 0 ),
+ }, true ),
+ createMXElement( 'fractal2d', mx_fractal_noise_materialx_2d, [ 'texcoord', 'octaves', 'lacunarity', 'diminish', 'amplitude' ], {
+ texcoord: defaultVec2( 0, 0 ),
+ octaves: defaultInt( 3 ),
+ lacunarity: defaultFloat( 2.0 ),
+ diminish: defaultFloat( 0.5 ),
+ amplitude: defaultFloat( 1.0 ),
+ }, true ),
+ createMXElement( 'fractal3d', mx_fractal_noise_materialx_3d, [ 'position', 'octaves', 'lacunarity', 'diminish', 'amplitude' ], {
+ position: () => positionLocal,
+ octaves: defaultInt( 3 ),
+ lacunarity: defaultFloat( 2.0 ),
+ diminish: defaultFloat( 0.5 ),
+ amplitude: defaultFloat( 1.0 ),
+ }, true ),
+ createMXElement( 'cellnoise2d', mx_cell_noise_materialx, [ 'texcoord' ], { texcoord: defaultVec2( 0, 0 ) }, true ),
+ createMXElement( 'cellnoise3d', mx_cell_noise_materialx, [ 'position' ], { position: () => positionLocal }, true ),
+ createMXElement( 'worleynoise2d', mx_worley_noise_materialx_2d, [ 'texcoord', 'jitter', 'style' ], {
+ texcoord: defaultVec2( 0, 0 ),
+ jitter: defaultFloat( 1 ),
+ style: defaultInt( 0 ),
+ }, true ),
+ createMXElement( 'worleynoise3d', mx_worley_noise_materialx_3d, [ 'position', 'jitter', 'style' ], {
+ position: () => positionLocal,
+ jitter: defaultFloat( 1 ),
+ style: defaultInt( 0 ),
+ }, true ),
+ createMXElement(
+ 'unifiednoise2d',
+ mx_unifiednoise2d,
+ [
+ 'type',
+ 'texcoord',
+ 'freq',
+ 'offset',
+ 'jitter',
+ 'outmin',
+ 'outmax',
+ 'clampoutput',
+ 'octaves',
+ 'lacunarity',
+ 'diminish',
+ 'style',
+ ],
+ {
+ type: defaultInt( 0 ),
+ texcoord: defaultVec2( 0, 0 ),
+ freq: defaultVec2( 1, 1 ),
+ offset: defaultVec2( 0, 0 ),
+ jitter: defaultFloat( 1 ),
+ outmin: defaultFloat( 0 ),
+ outmax: defaultFloat( 1 ),
+ clampoutput: defaultBool( true ),
+ octaves: defaultInt( 3 ),
+ lacunarity: defaultFloat( 2 ),
+ diminish: defaultFloat( 0.5 ),
+ style: defaultInt( 0 ),
+ },
+ ),
+ createMXElement(
+ 'unifiednoise3d',
+ mx_unifiednoise3d,
+ [
+ 'type',
+ 'position',
+ 'freq',
+ 'offset',
+ 'jitter',
+ 'outmin',
+ 'outmax',
+ 'clampoutput',
+ 'octaves',
+ 'lacunarity',
+ 'diminish',
+ 'style',
+ ],
+ {
+ type: defaultInt( 0 ),
+ position: () => positionLocal,
+ freq: defaultVec3( 1, 1, 1 ),
+ offset: defaultVec3( 0, 0, 0 ),
+ jitter: defaultFloat( 1 ),
+ outmin: defaultFloat( 0 ),
+ outmax: defaultFloat( 1 ),
+ clampoutput: defaultBool( true ),
+ octaves: defaultInt( 3 ),
+ lacunarity: defaultFloat( 2 ),
+ diminish: defaultFloat( 0.5 ),
+ style: defaultInt( 0 ),
+ },
+ ),
+ createMXElement( 'place2d', mx_place2d, [ 'texcoord', 'pivot', 'scale', 'rotate', 'offset', 'operationorder' ], {
+ texcoord: defaultVec2( 0, 0 ),
+ pivot: defaultVec2( 0, 0 ),
+ scale: defaultVec2( 1, 1 ),
+ rotate: defaultFloat( 0 ),
+ offset: defaultVec2( 0, 0 ),
+ operationorder: defaultInt( 0 ),
+ } ),
+ createMXElement( 'safepower', mx_safepower, [ 'in1', 'in2' ], { in1: defaultFloat( 0 ), in2: defaultFloat( 1 ) } ),
+ createMXElement( 'contrast', mx_contrast, [ 'in', 'amount', 'pivot' ], {
+ in: defaultFloat( 0 ),
+ amount: defaultFloat( 1 ),
+ pivot: defaultFloat( 0.5 ),
+ } ),
+ createMXElement( 'saturate', mx_saturation, [ 'in', 'amount' ], { in: defaultColor( 0, 0, 0 ), amount: defaultFloat( 1 ) } ),
+ createMXElement( 'extract', element, [ 'in', 'index' ], { in: defaultFloat( 0 ), index: defaultInt( 0 ) } ),
+ createMXElement( 'separate2', element, [ 'in' ], { in: defaultVec2( 0, 0 ) } ),
+ createMXElement( 'separate3', element, [ 'in' ], { in: defaultVec3( 0, 0, 0 ) } ),
+ createMXElement( 'separate4', element, [ 'in' ], { in: defaultVec4( 0, 0, 0, 0 ) } ),
+ createMXElement( 'reflect', reflect, [ 'in', 'normal' ], { in: defaultVec3( 1, 0, 0 ) } ),
+ createMXElement( 'refract', refract, [ 'in', 'normal', 'ior' ], { in: defaultVec3( 1, 0, 0 ), ior: defaultFloat( 1 ) } ),
+ createMXElement( 'time', mx_timer ),
+ createMXElement( 'frame', mx_frame ),
+ createMXElement( 'ifgreater', mx_ifgreater, [ 'value1', 'value2', 'in1', 'in2' ], {
+ value1: defaultFloat( 1 ),
+ value2: defaultFloat( 0 ),
+ in1: defaultFloat( 0 ),
+ in2: defaultFloat( 0 ),
+ } ),
+ createMXElement( 'ifgreatereq', mx_ifgreatereq, [ 'value1', 'value2', 'in1', 'in2' ], {
+ value1: defaultFloat( 1 ),
+ value2: defaultFloat( 0 ),
+ in1: defaultFloat( 0 ),
+ in2: defaultFloat( 0 ),
+ } ),
+ createMXElement( 'ifequal', mx_ifequal, [ 'value1', 'value2', 'in1', 'in2' ], {
+ value1: defaultFloat( 0 ),
+ value2: defaultFloat( 0 ),
+ in1: defaultFloat( 0 ),
+ in2: defaultFloat( 0 ),
+ } ),
+ createMXElement( 'rotate2d', mx_rotate2d, [ 'in', 'amount' ], { in: defaultVec2( 0, 0 ), amount: defaultFloat( 0 ) } ),
+ createMXElement( 'rotate3d', mx_rotate3d, [ 'in', 'amount', 'axis' ], {
+ in: defaultVec3( 0, 0, 0 ),
+ amount: defaultFloat( 0 ),
+ axis: defaultVec3( 0, 1, 0 ),
+ } ),
+ createMXElement( 'heighttonormal', mx_heighttonormal, [ 'in', 'scale', 'texcoord' ], {
+ in: defaultFloat( 0 ),
+ scale: defaultFloat( 1 ),
+ } ),
+ createMXElement( 'and', mx_and, [ 'in1', 'in2' ], { in1: defaultBool( false ), in2: defaultBool( false ) } ),
+ createMXElement( 'or', mx_or, [ 'in1', 'in2' ], { in1: defaultBool( false ), in2: defaultBool( false ) } ),
+ createMXElement( 'xor', mx_xor, [ 'in1', 'in2' ], { in1: defaultBool( false ), in2: defaultBool( false ) } ),
+ createMXElement( 'not', mx_not, [ 'in' ], { in: defaultBool( false ) } ),
+ createMXElement( 'checkerboard', mx_checkerboard, [ 'color1', 'color2', 'uvtiling', 'uvoffset', 'texcoord' ], {
+ color1: defaultColor( 1, 1, 1 ),
+ color2: defaultColor( 0, 0, 0 ),
+ uvtiling: defaultVec2( 8, 8 ),
+ uvoffset: defaultVec2( 0, 0 ),
+ texcoord: defaultVec2( 0, 0 ),
+ } ),
+ createMXElement( 'circle', mx_circle, [ 'texcoord', 'center', 'radius' ], {
+ center: defaultVec2( 0, 0 ),
+ radius: defaultFloat( 0.5 ),
+ } ),
+ createMXElement( 'bump', mx_bump, [ 'height', 'scale', 'normal', 'tangent', 'bitangent' ], {
+ height: defaultFloat( 0 ),
+ scale: defaultFloat( 1 ),
+ normal: () => normalWorld,
+ tangent: () => tangentWorld,
+ bitangent: () => bitangentWorld,
+ } ),
+ createMXElement( 'blackbody', mx_blackbody, [ 'temperature' ], { temperature: defaultFloat( 5000 ) } ),
+];
+
+const MtlXLibrary = Object.fromEntries( MXElements.map( ( entry ) => [ entry.name, entry ] ) );
+
+export { MtlXLibrary };
diff --git a/examples/jsm/loaders/materialx/MaterialXSurfaceMappings.js b/examples/jsm/loaders/materialx/MaterialXSurfaceMappings.js
new file mode 100644
index 00000000000000..46714617c681b1
--- /dev/null
+++ b/examples/jsm/loaders/materialx/MaterialXSurfaceMappings.js
@@ -0,0 +1,661 @@
+import { DoubleSide } from 'three/webgpu';
+import { float, color, mul, clamp, vec2, cos, sin, pow, mix, transformNormalToView } from 'three/tsl';
+
+const mappedStandardSurfaceInputs = new Set( [
+ 'base',
+ 'base_color',
+ 'roughness',
+ 'specular_roughness',
+ 'metalness',
+ 'specular',
+ 'specular_color',
+ 'specular_anisotropy',
+ 'specular_rotation',
+ 'transmission',
+ 'transmission_color',
+ 'transmission_depth',
+ 'thin_film_thickness',
+ 'thin_film_ior',
+ 'thin_film_IOR',
+ 'sheen',
+ 'sheen_color',
+ 'sheen_roughness',
+ 'coat',
+ 'coat_color',
+ 'coat_roughness',
+ 'coat_normal',
+ 'normal',
+ 'opacity',
+ 'ior',
+ 'specular_IOR',
+ 'emission',
+ 'emissionColor',
+ 'emission_color',
+] );
+
+const mappedGltfPbrInputs = new Set( [
+ 'base_color',
+ 'occlusion',
+ 'roughness',
+ 'metallic',
+ 'normal',
+ 'transmission',
+ 'specular',
+ 'specular_color',
+ 'ior',
+ 'alpha',
+ 'alpha_mode',
+ 'alpha_cutoff',
+ 'iridescence',
+ 'iridescence_ior',
+ 'iridescence_thickness',
+ 'sheen_color',
+ 'sheen_roughness',
+ 'clearcoat',
+ 'clearcoat_roughness',
+ 'clearcoat_normal',
+ 'emissive',
+ 'emissive_strength',
+ 'attenuation_distance',
+ 'attenuation_color',
+ 'thickness',
+ 'dispersion',
+ 'anisotropy_strength',
+ 'anisotropy_rotation',
+] );
+
+const mappedOpenPbrInputs = new Set( [
+ 'base_weight',
+ 'base_color',
+ 'specular_weight',
+ 'specular_color',
+ 'specular_roughness',
+ 'base_metalness',
+ 'specular_roughness_anisotropy',
+ 'specular_ior',
+ 'specular_ior_level',
+ 'coat_weight',
+ 'coat_ior',
+ 'coat_color',
+ 'coat_roughness',
+ 'geometry_coat_normal',
+ 'fuzz_weight',
+ 'fuzz_color',
+ 'fuzz_roughness',
+ 'transmission_weight',
+ 'transmission_color',
+ 'transmission_depth',
+ 'transmission_dispersion_scale',
+ 'transmission_dispersion_abbe_number',
+ 'geometry_normal',
+ 'geometry_opacity',
+ 'geometry_thin_walled',
+ 'thin_film_weight',
+ 'thin_film_thickness',
+ 'thin_film_ior',
+ 'emission_color',
+ 'emission_luminance',
+] );
+
+function warnIgnoredInputs( inputs, mappedInputs, issueCollector, surfaceCategory, nodeName ) {
+
+ for ( const inputName of Object.keys( inputs ) ) {
+
+ if ( mappedInputs.has( inputName ) === false ) {
+
+ issueCollector.addIgnoredSurfaceInput( surfaceCategory, nodeName, inputName );
+
+ }
+
+ }
+
+}
+
+function hasNodeValue( value ) {
+
+ return value !== undefined && value !== null;
+
+}
+
+function getConstNumber( node ) {
+
+ if ( typeof node === 'number' ) return node;
+ if ( ! node || typeof node !== 'object' ) return null;
+
+ let cursor = node;
+ const visited = new Set();
+ while ( cursor && typeof cursor === 'object' ) {
+
+ if ( visited.has( cursor ) ) break;
+ visited.add( cursor );
+ if ( typeof cursor.value === 'number' ) return cursor.value;
+ cursor = cursor.node;
+
+ }
+
+ return null;
+
+}
+
+function isConstNear( node, target, epsilon = 1e-6 ) {
+
+ const value = getConstNumber( node );
+ if ( value === null ) return false;
+ return Math.abs( value - target ) <= epsilon;
+
+}
+
+function isEffectivelyZero( node, epsilon = 1e-6 ) {
+
+ return isConstNear( node, 0, epsilon );
+
+}
+
+function isEffectivelyOne( node, epsilon = 1e-6 ) {
+
+ return isConstNear( node, 1, epsilon );
+
+}
+
+function isEnabledWeightNode( node ) {
+
+ if ( hasNodeValue( node ) === false ) return false;
+ return isEffectivelyZero( node ) === false;
+
+}
+
+function setAnisotropy( material, strengthNode, rotationNode ) {
+
+ if ( ! hasNodeValue( strengthNode ) && ! hasNodeValue( rotationNode ) ) return;
+ if ( isEffectivelyZero( strengthNode ) && isEffectivelyZero( rotationNode ) ) return;
+ const strength = hasNodeValue( strengthNode ) ? strengthNode : float( 0 );
+ const rotation = hasNodeValue( rotationNode ) ? rotationNode : float( 0 );
+ material.anisotropyNode = vec2( cos( rotation ), sin( rotation ) ).mul( strength );
+ material.anisotropyRotationNode = rotation;
+
+}
+
+function setTransmissionFlags( material, transmissionNode, opacityNode, allowOpacityTransparency = true ) {
+
+ if ( allowOpacityTransparency && hasNodeValue( opacityNode ) && isEffectivelyOne( opacityNode ) === false ) {
+
+ material.transparent = true;
+
+ }
+
+ if ( isEnabledWeightNode( transmissionNode ) ) {
+
+ material.side = DoubleSide;
+ material.transparent = true;
+
+ }
+
+}
+
+function toAttenuationDistance( distanceNode, hasAttenuationColorInput ) {
+
+ if ( hasNodeValue( distanceNode ) ) return distanceNode;
+ // When attenuation tint is authored without a distance, default to a
+ // finite value so absorption tinting is visible.
+ return hasAttenuationColorInput ? float( 1 ) : undefined;
+
+}
+
+function buildGltfOpacityNode( alphaNode, alphaModeNode ) {
+
+ const alphaMode = getConstNumber( alphaModeNode );
+ const roundedMode = alphaMode === null ? 2 : Math.round( alphaMode );
+ const alpha = alphaNode ?? float( 1 );
+
+ if ( roundedMode === 0 ) return float( 1 );
+ if ( roundedMode === 1 ) return alpha;
+ return alpha;
+
+}
+
+function applyStandardSurface( material, inputs, issueCollector, nodeName ) {
+
+ let colorNode = null;
+ if ( inputs.base && inputs.base_color ) colorNode = mul( inputs.base, inputs.base_color );
+ else if ( inputs.base ) colorNode = inputs.base;
+ else if ( inputs.base_color ) colorNode = inputs.base_color;
+
+ if ( inputs.coat_color ) {
+
+ colorNode = colorNode ? mul( colorNode, inputs.coat_color ) : colorNode;
+
+ }
+
+ const roughnessNode = inputs.specular_roughness ?? inputs.roughness;
+ const opacityNode = inputs.opacity;
+ const transmissionNode = inputs.transmission;
+ const transmissionColorNode = inputs.transmission_color;
+ const transmissionEnabled = isEnabledWeightNode( transmissionNode );
+ const sheenEnabled = isEnabledWeightNode( inputs.sheen );
+ const clearcoatEnabled = isEnabledWeightNode( inputs.coat );
+
+ let emissiveNode = inputs.emission;
+ const emissionColorNode = inputs.emission_color ?? inputs.emissionColor;
+ if ( hasNodeValue( emissionColorNode ) ) {
+
+ emissiveNode = emissiveNode ? mul( emissiveNode, emissionColorNode ) : emissionColorNode;
+
+ }
+
+ const thinFilmThicknessNode = inputs.thin_film_thickness;
+ const thinFilmIorNode = clamp( inputs.thin_film_ior || inputs.thin_film_IOR || float( 1.5 ), float( 1.0 ), float( 2.333 ) );
+ const thinFilmEnabled = isEnabledWeightNode( thinFilmThicknessNode );
+ const transmissionTintNode = hasNodeValue( transmissionColorNode ) ? transmissionColorNode : color( 1, 1, 1 );
+
+ if ( transmissionEnabled ) {
+
+ const baseForTransmissionMix = colorNode || color( 0.8, 0.8, 0.8 );
+ // Suppress diffuse/base tint as transmission ramps up.
+ colorNode = mix( baseForTransmissionMix, transmissionTintNode, transmissionNode );
+
+ }
+
+ material.colorNode = colorNode || color( 0.8, 0.8, 0.8 );
+ if ( hasNodeValue( opacityNode ) && isEffectivelyOne( opacityNode ) === false ) {
+
+ material.opacityNode = opacityNode;
+
+ }
+
+ material.roughnessNode = roughnessNode || float( 0.2 );
+ if ( hasNodeValue( inputs.metalness ) && isEffectivelyZero( inputs.metalness ) === false ) {
+
+ material.metalnessNode = inputs.metalness;
+
+ }
+
+ if ( hasNodeValue( inputs.specular ) && isEffectivelyOne( inputs.specular ) === false ) {
+
+ material.specularIntensityNode = inputs.specular;
+
+ }
+
+ material.specularColorNode = inputs.specular_color || color( 1, 1, 1 );
+ const iorNode = inputs.specular_IOR || inputs.ior;
+ if ( hasNodeValue( iorNode ) && isConstNear( iorNode, 1.5 ) === false ) {
+
+ material.iorNode = iorNode;
+
+ }
+
+ setAnisotropy( material, inputs.specular_anisotropy, inputs.specular_rotation );
+
+ if ( transmissionEnabled ) {
+
+ material.transmissionNode = transmissionNode;
+ if ( hasNodeValue( transmissionColorNode ) ) material.transmissionColorNode = transmissionColorNode;
+ if ( hasNodeValue( inputs.transmission_depth ) && isEffectivelyZero( inputs.transmission_depth ) === false ) {
+
+ material.thicknessNode = inputs.transmission_depth;
+
+ } else {
+
+ // Keep transmissive standard_surface materials volumetric when
+ // transmission_depth is omitted or authored as zero.
+ material.thickness = 1;
+
+ }
+
+ }
+
+ if ( thinFilmEnabled ) {
+
+ material.iridescenceThicknessNode = thinFilmThicknessNode;
+ material.iridescenceIORNode = thinFilmIorNode;
+ material.iridescenceNode = float( 1 );
+
+ }
+
+ const sheenColor = inputs.sheen_color || color( 1, 1, 1 );
+ if ( sheenEnabled ) {
+
+ const sheenRoughness = hasNodeValue( inputs.sheen_roughness ) ? inputs.sheen_roughness : float( 0.3 );
+ material.sheenNode = mul( inputs.sheen, sheenColor );
+ material.sheenRoughnessNode = sheenRoughness;
+
+ }
+
+ if ( clearcoatEnabled ) {
+
+ material.clearcoatNode = inputs.coat;
+ if ( hasNodeValue( inputs.coat_roughness ) ) {
+
+ material.clearcoatRoughnessNode = inputs.coat_roughness;
+
+ }
+
+ }
+
+ if ( clearcoatEnabled && hasNodeValue( inputs.coat_normal ) ) {
+
+ material.clearcoatNormalNode = transformNormalToView( inputs.coat_normal );
+
+ }
+
+ if ( hasNodeValue( inputs.normal ) ) material.normalNode = transformNormalToView( inputs.normal );
+ if ( hasNodeValue( emissiveNode ) ) material.emissiveNode = emissiveNode;
+
+ setTransmissionFlags( material, transmissionNode, opacityNode );
+ warnIgnoredInputs( inputs, mappedStandardSurfaceInputs, issueCollector, 'standard_surface', nodeName );
+
+}
+
+function applyGltfPbrSurface( material, inputs, issueCollector, nodeName ) {
+
+ const alphaModeLiteral = getConstNumber( inputs.alpha_mode );
+ const alphaMode = alphaModeLiteral === null ? 2 : Math.round( alphaModeLiteral );
+ const isAlphaMaskMode = alphaMode === 1;
+ const isAlphaBlendMode = alphaMode === 2;
+ const opacityNode = buildGltfOpacityNode( inputs.alpha, inputs.alpha_mode );
+ const alphaCutoffNode = inputs.alpha_cutoff ?? float( 0.5 );
+ const hasAttenuationColorInput = Object.prototype.hasOwnProperty.call( inputs, 'attenuation_color' );
+ const transmissionEnabled = isEnabledWeightNode( inputs.transmission );
+ const clearcoatEnabled = isEnabledWeightNode( inputs.clearcoat );
+ const sheenEnabled = hasNodeValue( inputs.sheen_color ) || isEnabledWeightNode( inputs.sheen_roughness );
+ const iridescenceEnabled = isEnabledWeightNode( inputs.iridescence );
+
+ material.colorNode = inputs.base_color || color( 1, 1, 1 );
+ if ( hasNodeValue( inputs.occlusion ) ) material.aoNode = inputs.occlusion;
+ material.roughnessNode = inputs.roughness || float( 1 );
+ material.metalnessNode = inputs.metallic || float( 1 );
+ if ( hasNodeValue( inputs.specular ) && isEffectivelyOne( inputs.specular ) === false ) {
+
+ material.specularIntensityNode = inputs.specular;
+
+ }
+
+ material.specularColorNode = inputs.specular_color || color( 1, 1, 1 );
+ if ( hasNodeValue( inputs.ior ) && isConstNear( inputs.ior, 1.5 ) === false ) {
+
+ material.iorNode = inputs.ior;
+
+ }
+
+ if ( hasNodeValue( opacityNode ) && isEffectivelyOne( opacityNode ) === false ) {
+
+ material.opacityNode = opacityNode;
+
+ }
+
+ if ( isAlphaMaskMode ) {
+
+ material.alphaTestNode = alphaCutoffNode;
+ const alphaCutoff = getConstNumber( alphaCutoffNode );
+ if ( alphaCutoff !== null ) material.alphaTest = alphaCutoff;
+
+ }
+
+ if ( transmissionEnabled ) {
+
+ material.transmissionNode = inputs.transmission;
+
+ }
+
+ if ( clearcoatEnabled ) {
+
+ material.clearcoatNode = inputs.clearcoat;
+ if ( hasNodeValue( inputs.clearcoat_roughness ) && isEffectivelyZero( inputs.clearcoat_roughness ) === false ) {
+
+ material.clearcoatRoughnessNode = inputs.clearcoat_roughness;
+
+ }
+
+ }
+
+ if ( sheenEnabled ) {
+
+ const sheenColor = hasNodeValue( inputs.sheen_color ) ? inputs.sheen_color : color( 0, 0, 0 );
+ const sheenRoughness = hasNodeValue( inputs.sheen_roughness ) ? inputs.sheen_roughness : float( 0 );
+ material.sheenRoughnessNode = sheenRoughness;
+ material.sheenNode = sheenColor;
+
+ }
+
+ if ( iridescenceEnabled ) {
+
+ material.iridescenceNode = inputs.iridescence;
+ if ( hasNodeValue( inputs.iridescence_ior ) && isConstNear( inputs.iridescence_ior, 1.3 ) === false ) {
+
+ material.iridescenceIORNode = inputs.iridescence_ior;
+
+ }
+
+ if ( hasNodeValue( inputs.iridescence_thickness ) && isConstNear( inputs.iridescence_thickness, 100 ) === false ) {
+
+ material.iridescenceThicknessNode = inputs.iridescence_thickness;
+
+ }
+
+ }
+
+ material.attenuationDistanceNode = toAttenuationDistance( inputs.attenuation_distance, hasAttenuationColorInput );
+ material.attenuationColorNode = inputs.attenuation_color || color( 1, 1, 1 );
+ if ( hasNodeValue( inputs.thickness ) ) {
+
+ if ( isEffectivelyZero( inputs.thickness ) === false ) {
+
+ material.thicknessNode = inputs.thickness;
+
+ }
+
+ } else if ( transmissionEnabled ) {
+
+ // Keep transmissive glTF materials volumetric even when thickness is omitted.
+ material.thickness = 1;
+
+ }
+
+ if ( hasNodeValue( inputs.dispersion ) && isEffectivelyZero( inputs.dispersion ) === false ) {
+
+ material.dispersionNode = inputs.dispersion;
+
+ }
+
+ const anisotropyStrength = inputs.anisotropy_strength;
+ const anisotropyRotation = inputs.anisotropy_rotation;
+ setAnisotropy( material, anisotropyStrength, anisotropyRotation );
+
+ if ( hasNodeValue( inputs.normal ) ) material.normalNode = transformNormalToView( inputs.normal );
+ if ( hasNodeValue( inputs.clearcoat_normal ) ) {
+
+ material.clearcoatNormalNode = transformNormalToView( inputs.clearcoat_normal );
+
+ }
+
+ if ( hasNodeValue( inputs.emissive ) && hasNodeValue( inputs.emissive_strength ) )
+ material.emissiveNode = mul( inputs.emissive, inputs.emissive_strength );
+ else if ( hasNodeValue( inputs.emissive ) ) material.emissiveNode = inputs.emissive;
+
+ setTransmissionFlags( material, inputs.transmission, opacityNode, isAlphaBlendMode );
+ warnIgnoredInputs( inputs, mappedGltfPbrInputs, issueCollector, 'gltf_pbr', nodeName );
+
+}
+
+function applyOpenPbrSurface( material, inputs, issueCollector, nodeName ) {
+
+ const baseWeight = inputs.base_weight || float( 1 );
+ const baseColor = inputs.base_color || color( 0.8, 0.8, 0.8 );
+ const coatEnabled = isEnabledWeightNode( inputs.coat_weight );
+ const fuzzEnabled = isEnabledWeightNode( inputs.fuzz_weight );
+ const transmissionEnabled = isEnabledWeightNode( inputs.transmission_weight );
+ const thinFilmEnabled = isEnabledWeightNode( inputs.thin_film_weight );
+ material.colorNode = mul( baseWeight, baseColor );
+
+ if ( hasNodeValue( inputs.base_metalness ) && isEffectivelyZero( inputs.base_metalness ) === false ) {
+
+ material.metalnessNode = inputs.base_metalness;
+
+ }
+
+ material.roughnessNode = inputs.specular_roughness || float( 0.3 );
+ if ( hasNodeValue( inputs.specular_weight ) && isEffectivelyOne( inputs.specular_weight ) === false ) {
+
+ material.specularIntensityNode = inputs.specular_weight;
+
+ }
+
+ material.specularColorNode = inputs.specular_color || color( 1, 1, 1 );
+ const openPbrIorNode = inputs.specular_ior || inputs.specular_ior_level;
+ if ( hasNodeValue( openPbrIorNode ) && isConstNear( openPbrIorNode, 1.5 ) === false ) {
+
+ material.iorNode = openPbrIorNode;
+
+ }
+
+ setAnisotropy( material, inputs.specular_roughness_anisotropy, float( 0 ) );
+
+ if ( coatEnabled ) {
+
+ const coatWeightNode = inputs.coat_weight || float( 0 );
+ if ( hasNodeValue( inputs.coat_ior ) ) {
+
+ const coatIorNode = inputs.coat_ior;
+ const coatIorMinusOne = coatIorNode.sub( float( 1 ) );
+ const coatIorPlusOne = coatIorNode.add( float( 1 ) );
+ const coatF0Node = coatIorMinusOne.div( coatIorPlusOne );
+ const normalizedClearcoatNode = coatF0Node.mul( coatF0Node ).div( float( 0.04 ) );
+ material.clearcoatNode = clamp( coatWeightNode.mul( normalizedClearcoatNode ), float( 0 ), float( 1 ) );
+
+ } else {
+
+ material.clearcoatNode = coatWeightNode;
+
+ }
+
+ if ( hasNodeValue( inputs.coat_roughness ) && isEffectivelyZero( inputs.coat_roughness ) === false ) {
+
+ material.clearcoatRoughnessNode = inputs.coat_roughness;
+
+ }
+
+ if ( hasNodeValue( inputs.geometry_coat_normal ) ) {
+
+ material.clearcoatNormalNode = transformNormalToView( inputs.geometry_coat_normal );
+
+ }
+
+ }
+
+ const fuzzWeight = inputs.fuzz_weight || float( 0 );
+ const fuzzColor = inputs.fuzz_color || color( 1, 1, 1 );
+ if ( fuzzEnabled ) {
+
+ material.sheenNode = mul( fuzzWeight, fuzzColor );
+ if ( hasNodeValue( inputs.fuzz_roughness ) ) {
+
+ material.sheenRoughnessNode = pow( inputs.fuzz_roughness, float( 1.5 ) );
+
+ }
+
+ }
+
+ if ( transmissionEnabled ) {
+
+ material.transmissionNode = inputs.transmission_weight;
+ if ( hasNodeValue( inputs.transmission_color ) ) material.attenuationColorNode = inputs.transmission_color;
+
+ }
+
+ const transmissionDepthNode = inputs.transmission_depth;
+ if ( transmissionEnabled && hasNodeValue( transmissionDepthNode ) && isEffectivelyZero( transmissionDepthNode ) === false ) {
+
+ material.thicknessNode = hasNodeValue( inputs.geometry_thin_walled )
+ ? inputs.geometry_thin_walled.select( float( 0 ), transmissionDepthNode )
+ : transmissionDepthNode;
+ material.attenuationDistanceNode = transmissionDepthNode;
+
+ } else if ( transmissionEnabled ) {
+
+ // Keep transmissive OpenPBR materials volumetric even when depth is omitted.
+ material.thickness = 1;
+
+ }
+
+ const transmissionDispersionAbbe = inputs.transmission_dispersion_abbe_number || float( 20 );
+ if ( transmissionEnabled && hasNodeValue( inputs.transmission_dispersion_scale ) && isEffectivelyZero( inputs.transmission_dispersion_scale ) === false ) {
+
+ material.dispersionNode = inputs.transmission_dispersion_scale.mul( float( 20 ) ).div( transmissionDispersionAbbe );
+
+ }
+
+ if ( hasNodeValue( inputs.geometry_opacity ) && isEffectivelyOne( inputs.geometry_opacity ) === false ) {
+
+ material.opacityNode = inputs.geometry_opacity;
+
+ }
+
+ if ( hasNodeValue( inputs.geometry_normal ) ) material.normalNode = transformNormalToView( inputs.geometry_normal );
+
+ if ( thinFilmEnabled ) {
+
+ material.iridescenceNode = inputs.thin_film_weight;
+ if ( hasNodeValue( inputs.thin_film_thickness ) && isConstNear( inputs.thin_film_thickness, 0.5 ) === false ) {
+
+ material.iridescenceThicknessNode = inputs.thin_film_thickness.mul( float( 1000 ) );
+
+ }
+
+ if ( hasNodeValue( inputs.thin_film_ior ) && isConstNear( inputs.thin_film_ior, 1.4 ) === false ) {
+
+ material.iridescenceIORNode = inputs.thin_film_ior;
+
+ }
+
+ }
+
+ const emissionColor = hasNodeValue( inputs.emission_color ) ? inputs.emission_color : color( 1, 1, 1 );
+ const emissionLuminance = hasNodeValue( inputs.emission_luminance ) ? inputs.emission_luminance : float( 0 );
+ if ( isEffectivelyZero( emissionLuminance ) === false ) {
+
+ material.emissiveNode = mul( emissionColor, emissionLuminance );
+
+ }
+
+ if ( hasNodeValue( inputs.geometry_opacity ) && isEffectivelyOne( inputs.geometry_opacity ) === false ) material.transparent = true;
+ if ( transmissionEnabled ) material.transparent = true;
+
+ setTransmissionFlags( material, inputs.transmission_weight, inputs.geometry_opacity );
+ warnIgnoredInputs( inputs, mappedOpenPbrInputs, issueCollector, 'open_pbr_surface', nodeName );
+
+}
+
+const MaterialXSurfaceMappings = {
+ standard_surface: applyStandardSurface,
+ gltf_pbr: applyGltfPbrSurface,
+ open_pbr_surface: applyOpenPbrSurface,
+};
+
+const surfaceMapperRegistry = new Map( Object.entries( MaterialXSurfaceMappings ).map( ( [ category, apply ] ) => [
+ category,
+ { category, apply },
+] ) );
+
+function getSurfaceMapper( category ) {
+
+ return surfaceMapperRegistry.get( category );
+
+}
+
+function getSupportedSurfaceCategories() {
+
+ return [ ...surfaceMapperRegistry.keys() ];
+
+}
+
+export {
+ MaterialXSurfaceMappings,
+ surfaceMapperRegistry,
+ getSurfaceMapper,
+ getSupportedSurfaceCategories,
+ applyStandardSurface,
+ applyGltfPbrSurface,
+ applyOpenPbrSurface,
+ mappedStandardSurfaceInputs,
+ mappedGltfPbrInputs,
+ mappedOpenPbrInputs,
+};
diff --git a/examples/jsm/loaders/materialx/MaterialXUtils.js b/examples/jsm/loaders/materialx/MaterialXUtils.js
new file mode 100644
index 00000000000000..b0bf44129794b0
--- /dev/null
+++ b/examples/jsm/loaders/materialx/MaterialXUtils.js
@@ -0,0 +1,57 @@
+import {
+ bool,
+ element,
+ float,
+ vec3,
+} from 'three/tsl';
+
+const BOOLEAN_OPERATOR_OPS = new Set( [ '&&', '||', '^^', '!', '==', '!=', '<', '>', '<=', '>=' ] );
+
+function normalizeSpaceName( value, fallback = 'world' ) {
+
+ if ( typeof value !== 'string' ) return fallback;
+ const normalized = value.trim().toLowerCase();
+ if ( normalized === '' ) return fallback;
+ if ( normalized === 'world' ) return 'world';
+ if ( normalized === 'object' || normalized === 'model' ) return 'object';
+ return fallback;
+
+}
+
+function isBooleanNode( node ) {
+
+ return node && ( node.nodeType === 'bool' || ( node.isOperatorNode && BOOLEAN_OPERATOR_OPS.has( node.op ) ) );
+
+}
+
+function toBooleanNode( node ) {
+
+ if ( ! node ) return bool( false );
+ if ( typeof node === 'boolean' ) return bool( node );
+ if ( typeof node === 'number' ) return bool( node !== 0 );
+ if ( isBooleanNode( node ) ) return node;
+ return node.notEqual( float( 0 ) );
+
+}
+
+function getComponentCountForType( type ) {
+
+ if ( type === 'vector2' ) return 2;
+ if ( type === 'vector3' || type === 'color3' ) return 3;
+ return 4;
+
+}
+
+function toVec3Channels( input ) {
+
+ return vec3( element( input, 0 ), element( input, 1 ), element( input, 2 ) );
+
+}
+
+export {
+ getComponentCountForType,
+ isBooleanNode,
+ normalizeSpaceName,
+ toBooleanNode,
+ toVec3Channels,
+};
diff --git a/examples/jsm/loaders/materialx/MaterialXWarnings.js b/examples/jsm/loaders/materialx/MaterialXWarnings.js
new file mode 100644
index 00000000000000..784f67f1acdc40
--- /dev/null
+++ b/examples/jsm/loaders/materialx/MaterialXWarnings.js
@@ -0,0 +1,205 @@
+const ISSUE_CODES = {
+ UNSUPPORTED_NODE: 'unsupported-node',
+ IGNORED_SURFACE_INPUT: 'ignored-surface-input',
+ MISSING_REFERENCE: 'missing-reference',
+ MISSING_MATERIAL: 'missing-material',
+ INVALID_VALUE: 'invalid-value',
+};
+
+const ISSUE_POLICIES = {
+ WARN: 'warn',
+ ERROR_CORE: 'error-core',
+ ERROR_ALL: 'error-all',
+};
+
+const LEGACY_POLICY_ALIASES = {
+ error: ISSUE_POLICIES.ERROR_CORE,
+};
+
+function normalizeIssuePolicy( policy ) {
+
+ const normalized = policy && typeof policy === 'string' ? policy : ISSUE_POLICIES.WARN;
+ if ( normalized in LEGACY_POLICY_ALIASES ) {
+
+ return LEGACY_POLICY_ALIASES[ normalized ];
+
+ }
+
+ if ( normalized === ISSUE_POLICIES.WARN || normalized === ISSUE_POLICIES.ERROR_CORE || normalized === ISSUE_POLICIES.ERROR_ALL ) {
+
+ return normalized;
+
+ }
+
+ return ISSUE_POLICIES.WARN;
+
+}
+
+class MaterialXIssueCollector {
+
+ constructor( options = {} ) {
+
+ this.issuePolicy = normalizeIssuePolicy( options.issuePolicy || options.unsupportedPolicy );
+ this.onWarning = options.onWarning || null;
+ this.issues = [];
+
+ }
+
+ addIssue( issue ) {
+
+ const normalizedIssue = {
+ code: issue.code || ISSUE_CODES.INVALID_VALUE,
+ message: issue.message || 'Unknown MaterialX issue.',
+ category: issue.category,
+ nodeName: issue.nodeName,
+ severity: issue.severity || 'warning',
+ };
+
+ this.issues.push( normalizedIssue );
+
+ if ( normalizedIssue.severity === 'warning' ) {
+
+ if ( this.issuePolicy === ISSUE_POLICIES.WARN ) {
+
+ console.warn( `THREE.MaterialXLoader: ${normalizedIssue.message}` );
+
+ }
+
+ if ( this.onWarning ) {
+
+ this.onWarning( normalizedIssue );
+
+ }
+
+ }
+
+ }
+
+ addUnsupportedNode( category, nodeName ) {
+
+ this.addIssue( {
+ code: ISSUE_CODES.UNSUPPORTED_NODE,
+ category,
+ nodeName,
+ message: `Unsupported MaterialX node category "${category}"${nodeName ? ` on "${nodeName}"` : ''}.`,
+ } );
+
+ }
+
+ addIgnoredSurfaceInput( category, nodeName, inputName ) {
+
+ this.addIssue( {
+ code: ISSUE_CODES.IGNORED_SURFACE_INPUT,
+ category,
+ nodeName,
+ message: `${category} input "${inputName}" is currently ignored in MaterialX translation.`,
+ } );
+
+ }
+
+ addMissingReference( nodeName, referencePath ) {
+
+ this.addIssue( {
+ code: ISSUE_CODES.MISSING_REFERENCE,
+ nodeName,
+ message: `Missing MaterialX reference "${referencePath}"${nodeName ? ` from "${nodeName}"` : ''}.`,
+ } );
+
+ }
+
+ addInvalidValue( nodeName, message ) {
+
+ this.addIssue( {
+ code: ISSUE_CODES.INVALID_VALUE,
+ nodeName,
+ message,
+ } );
+
+ }
+
+ addMissingMaterial( materialName ) {
+
+ this.addIssue( {
+ code: ISSUE_CODES.MISSING_MATERIAL,
+ message: materialName
+ ? `Could not find surfacematerial named "${materialName}".`
+ : 'Document does not include a surfacematerial node.',
+ } );
+
+ }
+
+ buildReport() {
+
+ const ignoredSurfaceInputs = this.issues.filter( ( issue ) => issue.code === ISSUE_CODES.IGNORED_SURFACE_INPUT );
+ const missingReferences = this.issues.filter( ( issue ) => issue.code === ISSUE_CODES.MISSING_REFERENCE );
+ const invalidValues = this.issues.filter( ( issue ) => issue.code === ISSUE_CODES.INVALID_VALUE );
+
+ return {
+ issues: this.issues,
+ warnings: this.issues,
+ ignoredSurfaceInputs,
+ missingReferences,
+ invalidValues,
+ };
+
+ }
+
+ throwIfNeeded() {
+
+ if ( this.issuePolicy === ISSUE_POLICIES.WARN ) return;
+
+ const coreCodes = new Set( [ ISSUE_CODES.UNSUPPORTED_NODE, ISSUE_CODES.MISSING_REFERENCE, ISSUE_CODES.INVALID_VALUE ] );
+ const fatalIssues = this.issues.filter( ( issue ) =>
+ this.issuePolicy === ISSUE_POLICIES.ERROR_ALL ? true : coreCodes.has( issue.code ) );
+ if ( fatalIssues.length === 0 ) return;
+
+ const detailsByCode = new Map();
+ for ( const issue of fatalIssues ) {
+
+ const count = detailsByCode.get( issue.code ) || 0;
+ detailsByCode.set( issue.code, count + 1 );
+
+ }
+
+ const details = [];
+ const unsupportedNodes = this.issues.filter( ( issue ) => issue.code === ISSUE_CODES.UNSUPPORTED_NODE );
+ if ( detailsByCode.has( ISSUE_CODES.UNSUPPORTED_NODE ) ) {
+
+ const categoryList = [ ...new Set( unsupportedNodes.map( ( issue ) => issue.category ).filter( Boolean ) ) ].sort().join( ', ' );
+ details.push( `unsupported node categories${categoryList ? `: ${categoryList}` : ''} (${detailsByCode.get( ISSUE_CODES.UNSUPPORTED_NODE )})` );
+
+ }
+
+ if ( detailsByCode.has( ISSUE_CODES.MISSING_REFERENCE ) ) {
+
+ details.push( `missing references (${detailsByCode.get( ISSUE_CODES.MISSING_REFERENCE )})` );
+
+ }
+
+ if ( detailsByCode.has( ISSUE_CODES.INVALID_VALUE ) ) {
+
+ details.push( `invalid values (${detailsByCode.get( ISSUE_CODES.INVALID_VALUE )})` );
+
+ }
+
+ if ( detailsByCode.has( ISSUE_CODES.IGNORED_SURFACE_INPUT ) ) {
+
+ details.push( `ignored surface inputs (${detailsByCode.get( ISSUE_CODES.IGNORED_SURFACE_INPUT )})` );
+
+ }
+
+ if ( detailsByCode.has( ISSUE_CODES.MISSING_MATERIAL ) ) {
+
+ details.push( `missing materials (${detailsByCode.get( ISSUE_CODES.MISSING_MATERIAL )})` );
+
+ }
+
+ throw new Error(
+ `THREE.MaterialXLoader: MaterialX translation failed in ${this.issuePolicy} mode; ${details.join( '; ' )}.`,
+ );
+
+ }
+
+}
+
+export { ISSUE_CODES, ISSUE_POLICIES, MaterialXIssueCollector, normalizeIssuePolicy };
diff --git a/examples/jsm/loaders/materialx/compile/MaterialXCompileRegistry.js b/examples/jsm/loaders/materialx/compile/MaterialXCompileRegistry.js
new file mode 100644
index 00000000000000..318e76ec917b6d
--- /dev/null
+++ b/examples/jsm/loaders/materialx/compile/MaterialXCompileRegistry.js
@@ -0,0 +1,989 @@
+import {
+ element,
+ float,
+ int,
+ mat3,
+ mat4,
+ mul,
+ normalMap,
+ texture,
+ uv,
+ vec2,
+ vec3,
+ vec4,
+ vertexColor,
+ positionLocal,
+ positionWorld,
+ normalLocal,
+ normalWorld,
+ tangentLocal,
+ tangentWorld,
+ bitangentLocal,
+ bitangentWorld,
+ clamp,
+ add,
+ abs,
+ min,
+ sub,
+ floor,
+ mix,
+ dot,
+ div,
+ max,
+ normalize,
+ mx_atan2,
+ pow,
+ sqrt,
+ sign,
+} from 'three/tsl';
+import {
+ getComponentCountForType,
+ normalizeSpaceName,
+ toBooleanNode,
+ toVec3Channels,
+} from '../MaterialXUtils.js';
+
+const register = ( registry, categories, handler ) => {
+
+ for ( const category of categories ) {
+
+ registry.set( category, handler );
+
+ }
+
+};
+
+const UV_FALLBACK_CATEGORIES = new Set( [ 'checkerboard', 'noise2d', 'fractal2d', 'cellnoise2d', 'worleynoise2d', 'unifiednoise2d', 'heighttonormal' ] );
+const SCALAR_TYPES = new Set( [ 'boolean', 'integer', 'float' ] );
+const THREE_COMPONENT_TYPES = new Set( [ 'vector2', 'vector3', 'vector4', 'color3', 'color4' ] );
+const TEXTURE_ADDRESS_MODES = new Set( [ 'constant', 'clamp', 'periodic', 'mirror' ] );
+const SWITCH_MIN_INDEX = 1;
+const SWITCH_MAX_INDEX = 10;
+
+const getDefaultUvNode = ( compileContext ) => compileContext.mxToBottomLeftUvSpace( uv( 0 ) );
+
+const toBooleanMaskNode = ( node ) => toBooleanNode( node ).select( float( 1 ), float( 0 ) );
+
+const getTextureAddressMode = ( nodeX, inputName ) => {
+
+ const value = nodeX.getInputValueByName( inputName );
+ if ( value === null || value === undefined || value === '' ) return 'periodic';
+
+ const mode = value.trim().toLowerCase();
+ return TEXTURE_ADDRESS_MODES.has( mode ) ? mode : 'periodic';
+
+};
+
+const getTextureAddressModes = ( nodeX ) => ( {
+ u: getTextureAddressMode( nodeX, 'uaddressmode' ),
+ v: getTextureAddressMode( nodeX, 'vaddressmode' ),
+} );
+
+const getZeroNodeForType = ( type ) => {
+
+ if ( type === 'vector2' ) return vec2( 0, 0 );
+ if ( type === 'vector3' || type === 'color3' ) return vec3( 0, 0, 0 );
+ if ( type === 'vector4' || type === 'color4' ) return vec4( 0, 0, 0, 0 );
+ return float( 0 );
+
+};
+
+const toTextureDefaultNode = ( node, type ) => {
+
+ if ( type === 'vector2' ) return vec4( node, 0, 1 );
+ if ( type === 'vector3' || type === 'color3' ) return vec4( node, 1 );
+ if ( type === 'vector4' || type === 'color4' ) return vec4( node );
+ return vec4( node, 0, 0, 1 );
+
+};
+
+const getTextureInputs = ( nodeX, compileContext ) => {
+
+ const file = nodeX.getChildByName( 'file' );
+ const uvNode = nodeX.getNodeByName( 'texcoord' ) || getDefaultUvNode( compileContext );
+ const textureFile = file ? file.getTexture() : null;
+ const defaultNode = nodeX.getNodeByName( 'default' ) || getZeroNodeForType( nodeX.type );
+ const addressModes = getTextureAddressModes( nodeX );
+ return { file, uvNode, textureFile, defaultNode, addressModes };
+
+};
+
+const applyTextureAddressModeDefault = ( node, uvNode, addressModes, defaultNode ) => {
+
+ let outsideBounds = null;
+
+ if ( addressModes.u === 'constant' ) {
+
+ const u = element( uvNode, 0 );
+ outsideBounds = u.lessThan( 0 ).or( u.greaterThan( 1 ) );
+
+ }
+
+ if ( addressModes.v === 'constant' ) {
+
+ const v = element( uvNode, 1 );
+ const vOutsideBounds = v.lessThan( 0 ).or( v.greaterThan( 1 ) );
+ outsideBounds = outsideBounds ? outsideBounds.or( vOutsideBounds ) : vOutsideBounds;
+
+ }
+
+ return outsideBounds ? outsideBounds.select( defaultNode, node ) : node;
+
+};
+
+const sampleTexture = ( textureFile, uvNode, compileContext, fallback, addressModes = null, defaultNode = fallback ) => {
+
+ if ( ! textureFile ) return fallback;
+
+ const textureUvNode = compileContext.mxFromBottomLeftUvSpace( uvNode );
+ const sampled = texture( textureFile, textureUvNode );
+ return addressModes ? applyTextureAddressModeDefault( sampled, textureUvNode, addressModes, defaultNode ) : sampled;
+
+};
+
+const applyTextureColorSpace = ( node, file ) => {
+
+ const colorSpaceNode = file ? file.getColorSpaceNode() : null;
+ return colorSpaceNode ? colorSpaceNode( node ) : node;
+
+};
+
+const compileConvertNode = ( nodeX ) => {
+
+ const input = nodeX.getNodeByName( 'in' );
+ const inputElement = nodeX.getChildByName( 'in' );
+ const inputType = inputElement ? inputElement.type : null;
+ const nodeClass = nodeX.getClassFromType( nodeX.type ) || float;
+
+ if ( nodeX.type === 'boolean' ) {
+
+ return toBooleanNode( input );
+
+ }
+
+ if ( inputType === 'boolean' ) {
+
+ const inputMask = toBooleanMaskNode( input );
+ if ( THREE_COMPONENT_TYPES.has( nodeX.type ) ) {
+
+ const componentCount = getComponentCountForType( nodeX.type );
+ return nodeClass( ...Array( componentCount ).fill( inputMask ) );
+
+ }
+
+ return nodeClass( inputMask );
+
+ }
+
+ if ( SCALAR_TYPES.has( inputType ) && THREE_COMPONENT_TYPES.has( nodeX.type ) ) {
+
+ const componentCount = getComponentCountForType( nodeX.type );
+ return nodeClass( ...Array( componentCount ).fill( input ) );
+
+ }
+
+ if ( THREE_COMPONENT_TYPES.has( inputType ) && SCALAR_TYPES.has( nodeX.type ) ) {
+
+ return nodeClass( element( input, 0 ) );
+
+ }
+
+ return nodeClass( input );
+
+};
+
+const compileConstantNode = ( nodeX ) => nodeX.getNodeByName( 'value' );
+
+const compileArtisticIorNode = ( nodeX, out ) => {
+
+ const reflectivity = clamp(
+ nodeX.getNodeByName( 'reflectivity' ) || vec3( 0.944, 0.776, 0.373 ),
+ vec3( 0, 0, 0 ),
+ vec3( 0.99, 0.99, 0.99 ),
+ );
+ const edgeColor = nodeX.getNodeByName( 'edge_color' ) || vec3( 0.998, 0.981, 0.751 );
+ const one = vec3( 1, 1, 1 );
+ const nMin = div( sub( one, reflectivity ), add( one, reflectivity ) );
+ const nMax = div( add( one, sqrt( reflectivity ) ), sub( one, sqrt( reflectivity ) ) );
+ const ior = mix( nMax, nMin, edgeColor );
+ const iorPlusOne = add( ior, one );
+ const iorMinusOne = sub( ior, one );
+ const k2 = max(
+ div(
+ sub(
+ mul( mul( iorPlusOne, iorPlusOne ), reflectivity ),
+ mul( iorMinusOne, iorMinusOne ),
+ ),
+ sub( one, reflectivity ),
+ ),
+ vec3( 0, 0, 0 ),
+ );
+ const extinction = sqrt( k2 );
+ return out === 'extinction' ? extinction : ior;
+
+};
+
+const compileBooleanConditionalNode = ( nodeX ) => {
+
+ if ( nodeX.type !== 'boolean' ) return null;
+
+ const value1Default = nodeX.element === 'ifequal' ? float( 0 ) : float( 1 );
+ const value2Default = float( 0 );
+ const value1 = nodeX.getNodeByName( 'value1' ) || value1Default;
+ const value2 = nodeX.getNodeByName( 'value2' ) || value2Default;
+
+ if ( nodeX.element === 'ifgreater' ) return value1.greaterThan( value2 );
+ if ( nodeX.element === 'ifgreatereq' ) return value1.greaterThanEqual( value2 );
+ if ( nodeX.element === 'ifequal' ) return value1.equal( value2 );
+
+ return null;
+
+};
+
+const compileClampNode = ( nodeX ) => {
+
+ const inNode = nodeX.getNodeByName( 'in' ) || float( 0 );
+ const low = nodeX.getNodeByName( 'low' ) || float( 0 );
+ const high = nodeX.getNodeByName( 'high' ) || float( 1 );
+ return min( max( inNode, low ), high );
+
+};
+
+const compileNormalizeNode = ( nodeX ) => {
+
+ const inNode = nodeX.getNodeByName( 'in' ) || getZeroNodeForType( nodeX.type );
+ const zeroNode = getZeroNodeForType( nodeX.type );
+ const lengthSquared = dot( inNode, inNode );
+ const safeLengthSquared = max( lengthSquared, float( 1e-8 ) );
+ const normalized = mul( inNode, div( float( 1 ), sqrt( safeLengthSquared ) ) );
+ return abs( lengthSquared ).lessThan( float( 1e-8 ) ).select( zeroNode, normalized );
+
+};
+
+const compileRemapNode = ( nodeX ) => {
+
+ const inNode = nodeX.getNodeByName( 'in' ) || float( 0 );
+ const inLow = nodeX.getNodeByName( 'inlow' ) || float( 0 );
+ const inHigh = nodeX.getNodeByName( 'inhigh' ) || float( 1 );
+ const outLow = nodeX.getNodeByName( 'outlow' ) || float( 0 );
+ const outHigh = nodeX.getNodeByName( 'outhigh' ) || float( 1 );
+ const denominator = sub( inHigh, inLow );
+ const isDegenerate = abs( denominator ).lessThan( float( 1e-8 ) );
+ const safeDenominator = isDegenerate.select( float( 1 ), denominator );
+ const remapped = add(
+ mul( div( sub( inNode, inLow ), safeDenominator ), sub( outHigh, outLow ) ),
+ outLow,
+ );
+ return isDegenerate.select( outLow, remapped );
+
+};
+
+const compileRangeNode = ( nodeX ) => {
+
+ const inNode = nodeX.getNodeByName( 'in' ) || float( 0 );
+ const inLow = nodeX.getNodeByName( 'inlow' ) || float( 0 );
+ const inHigh = nodeX.getNodeByName( 'inhigh' ) || float( 1 );
+ const outLow = nodeX.getNodeByName( 'outlow' ) || float( 0 );
+ const outHigh = nodeX.getNodeByName( 'outhigh' ) || float( 1 );
+ const gamma = nodeX.getNodeByName( 'gamma' ) || float( 1 );
+ const doClamp = nodeX.getNodeByName( 'doclamp' ) || int( 0 );
+
+ const denominator = sub( inHigh, inLow );
+ const isDegenerate = abs( denominator ).lessThan( float( 1e-8 ) );
+ const safeDenominator = isDegenerate.select( float( 1 ), denominator );
+ const normalized = div( sub( inNode, inLow ), safeDenominator );
+ // Match stdlib range nodegraph semantics:
+ // gamma stage is sign(normalized) * pow(abs(normalized), 1 / gamma).
+ const reciprocalGamma = div( float( 1 ), gamma );
+ const gammaApplied = mul( pow( abs( normalized ), reciprocalGamma ), sign( normalized ) );
+ const remapped = add( mul( gammaApplied, sub( outHigh, outLow ) ), outLow );
+ const result = isDegenerate.select( outLow, remapped );
+ const clamped = min( max( result, outLow ), outHigh );
+
+ return toBooleanNode( doClamp ).select( clamped, result );
+
+};
+
+const getSwitchBranchNode = ( nodeX, index ) => nodeX.getNodeByName( `in${index}` ) || getZeroNodeForType( nodeX.type );
+
+const compileSwitchNode = ( nodeX ) => {
+
+ const fallbackNode = getSwitchBranchNode( nodeX, SWITCH_MIN_INDEX );
+ const whichInput = nodeX.getNodeByName( 'which' );
+ const switchIndex = add( floor( float( whichInput === undefined || whichInput === null ? 0 : whichInput ) ), 1 );
+ const whichNode = clamp(
+ float( switchIndex ),
+ float( SWITCH_MIN_INDEX ),
+ float( SWITCH_MAX_INDEX ),
+ );
+
+ let result = fallbackNode;
+ for ( let branchIndex = SWITCH_MIN_INDEX + 1; branchIndex <= SWITCH_MAX_INDEX; branchIndex += 1 ) {
+
+ const branchNode = getSwitchBranchNode( nodeX, branchIndex );
+ result = whichNode.equal( float( branchIndex ) ).select( branchNode, result );
+
+ }
+
+ return result;
+
+};
+
+const compileSpaceInputNode = ( nodeX, objectNode, worldNode ) => {
+
+ const rawSpace = nodeX.getInputValueByName( 'space' ) ?? nodeX.getAttribute( 'space' );
+ const space = normalizeSpaceName( rawSpace, 'object' );
+ return space === 'world' ? worldNode : objectNode;
+
+};
+
+const compileNormalizedSpaceInputNode = ( nodeX, objectNode, worldNode ) => normalize( compileSpaceInputNode( nodeX, objectNode, worldNode ) );
+
+const compileTexcoordNode = ( nodeX, compileContext ) => {
+
+ const indexNode = nodeX.getChildByName( 'index' );
+ const index = indexNode ? parseInt( indexNode.value, 10 ) : 0;
+ return compileContext.mxToBottomLeftUvSpace( uv( index ) );
+
+};
+
+const compileGeomColorNode = ( nodeX ) => {
+
+ const indexNode = nodeX.getChildByName( 'index' );
+ const index = indexNode ? parseInt( indexNode.value, 10 ) : 0;
+ return vertexColor( index );
+
+};
+
+const compileImageLikeNode = ( nodeX, compileContext ) => {
+
+ const { file, uvNode, textureFile, defaultNode, addressModes } = getTextureInputs( nodeX, compileContext );
+ const textureDefault = toTextureDefaultNode( defaultNode, nodeX.type );
+ const node = sampleTexture( textureFile, uvNode, compileContext, textureDefault, addressModes, textureDefault );
+ return applyTextureColorSpace( node, file );
+
+};
+
+const compileTiledImageNode = ( nodeX, compileContext ) => {
+
+ const { file, uvNode, textureFile, defaultNode, addressModes } = getTextureInputs( nodeX, compileContext );
+ const textureDefault = toTextureDefaultNode( defaultNode, nodeX.type );
+ if ( ! textureFile ) {
+
+ return textureDefault;
+
+ }
+
+ const uvTiling = nodeX.getNodeByName( 'uvtiling' );
+ const uvOffset = nodeX.getNodeByName( 'uvoffset' );
+ const transformedUv = compileContext.mxTransformUv( uvTiling, uvOffset, uvNode );
+ const node = sampleTexture( textureFile, transformedUv, compileContext, textureDefault, addressModes, textureDefault );
+ return applyTextureColorSpace( node, file );
+
+};
+
+const compileHexTiledNormalMapNode = ( nodeX, compileContext, sampleNode ) => {
+
+ const normalMapNodeElement = compileContext.nodeLibrary.normalmap;
+ const strengthNode = nodeX.getNodeByName( 'strength' ) || float( 1 );
+
+ if ( ! normalMapNodeElement || typeof normalMapNodeElement.nodeFunc !== 'function' ) {
+
+ return normalMap( vec4( sampleNode, 1 ), strengthNode );
+
+ }
+
+ const args = normalMapNodeElement.params.map( ( paramName ) => {
+
+ if ( paramName === 'in' ) return sampleNode;
+ if ( paramName === 'scale' ) return strengthNode;
+ return nodeX.getNodeByName( paramName );
+
+ } );
+
+ for ( let i = 0; i < normalMapNodeElement.params.length; i += 1 ) {
+
+ if ( args[ i ] !== undefined && args[ i ] !== null ) {
+
+ continue;
+
+ }
+
+ const paramName = normalMapNodeElement.params[ i ];
+ const defaultValue = normalMapNodeElement.defaults ? normalMapNodeElement.defaults[ paramName ] : undefined;
+ args[ i ] = defaultValue !== undefined ? ( typeof defaultValue === 'function' ? defaultValue() : float( defaultValue ) ) : float( 0 );
+
+ }
+
+ return normalMapNodeElement.nodeFunc( ...args );
+
+};
+
+const compileHexTiledTextureNode = ( nodeX, compileContext, category ) => {
+
+ const file = nodeX.getChildByName( 'file' );
+ if ( ! file ) {
+
+ nodeX.materialX.issueCollector.addInvalidValue(
+ nodeX.name,
+ `Texture node "${nodeX.name || nodeX.element}" is missing required input "file".`,
+ );
+ if ( category === 'hextilednormalmap' ) {
+
+ return normalize( nodeX.getNodeByName( 'normal' ) || normalLocal );
+
+ }
+ return vec4( 0, 0, 0, 1 );
+
+ }
+
+ const textureFile = file.getTexture();
+ if ( ! textureFile ) {
+
+ if ( category === 'hextilednormalmap' ) {
+
+ return normalize( nodeX.getNodeByName( 'normal' ) || normalLocal );
+
+ }
+ return vec4( 0, 0, 0, 1 );
+
+ }
+
+ const uvNode = nodeX.getNodeByName( 'texcoord' ) || getDefaultUvNode( compileContext );
+ const tiling = nodeX.getNodeByName( 'tiling' ) || vec2( 1, 1 );
+ const rotation = nodeX.getNodeByName( 'rotation' ) || float( 1 );
+ const rotationRange = nodeX.getNodeByName( 'rotationrange' ) || vec2( 0, 360 );
+ const scale = nodeX.getNodeByName( 'scale' ) || float( 1 );
+ const scaleRange = nodeX.getNodeByName( 'scalerange' ) || vec2( 0.5, 2 );
+ const offset = nodeX.getNodeByName( 'offset' ) || float( 1 );
+ const offsetRange = nodeX.getNodeByName( 'offsetrange' ) || vec2( 0, 1 );
+ const falloff = nodeX.getNodeByName( 'falloff' ) || float( 0.5 );
+ const falloffContrast = nodeX.getNodeByName( 'falloffcontrast' ) || float( 0.5 );
+ let lumaCoeffs = nodeX.getNodeByName( 'lumacoeffs' ) || vec3( 0.2722287, 0.6740818, 0.0536895 );
+ const lumaCoeffsInput = nodeX.getChildByName( 'lumacoeffs' );
+ if ( lumaCoeffsInput && lumaCoeffsInput.isConst ) {
+
+ const lumaCoeffValues = lumaCoeffsInput.getVector();
+ if ( lumaCoeffValues.length === 3 ) {
+
+ // Treat luminance coefficients as raw numeric values, not display colors.
+ lumaCoeffs = vec3( lumaCoeffValues[ 0 ], lumaCoeffValues[ 1 ], lumaCoeffValues[ 2 ] );
+
+ }
+
+ }
+ const transformedUv = compileContext.mxFromBottomLeftUvSpace( mul( uvNode, tiling ) );
+ const tileData = compileContext.mxHextileCoord( transformedUv, rotation, rotationRange, scale, scaleRange, offset, offsetRange );
+
+ let sample0 = texture( textureFile, tileData.coords[ 0 ] ).grad(
+ tileData.ddx[ 0 ],
+ tileData.ddy[ 0 ],
+ );
+ let sample1 = texture( textureFile, tileData.coords[ 1 ] ).grad(
+ tileData.ddx[ 1 ],
+ tileData.ddy[ 1 ],
+ );
+ let sample2 = texture( textureFile, tileData.coords[ 2 ] ).grad(
+ tileData.ddx[ 2 ],
+ tileData.ddy[ 2 ],
+ );
+ const sample0Raw = sample0;
+ const sample1Raw = sample1;
+ const sample2Raw = sample2;
+
+ const colorSpaceNode = file.getColorSpaceNode();
+ if ( colorSpaceNode ) {
+
+ sample0 = colorSpaceNode( sample0 );
+ sample1 = colorSpaceNode( sample1 );
+ sample2 = colorSpaceNode( sample2 );
+
+ }
+
+ const c0 = toVec3Channels( sample0 );
+ const c1 = toVec3Channels( sample1 );
+ const c2 = toVec3Channels( sample2 );
+ const falloffContrastWeight = mul( falloffContrast, 0.5 );
+ const cw = mix(
+ vec3( 1, 1, 1 ),
+ vec3( dot( c0, lumaCoeffs ), dot( c1, lumaCoeffs ), dot( c2, lumaCoeffs ) ),
+ vec3( falloffContrastWeight, falloffContrastWeight, falloffContrastWeight ),
+ );
+ const blendWeights = compileContext.mxHextileComputeBlendWeights( cw, tileData.weights, falloff );
+ const alphaWeights = compileContext.mxHextileComputeBlendWeights( vec3( 1, 1, 1 ), tileData.weights, falloff );
+ const blendedRgb = add( add( mul( element( blendWeights, 0 ), c0 ), mul( element( blendWeights, 1 ), c1 ) ), mul( element( blendWeights, 2 ), c2 ) );
+ const blendedAlpha = add(
+ add( mul( element( alphaWeights, 0 ), element( sample0Raw, 3 ) ), mul( element( alphaWeights, 1 ), element( sample1Raw, 3 ) ) ),
+ mul( element( alphaWeights, 2 ), element( sample2Raw, 3 ) ),
+ );
+ const blended = vec4( blendedRgb, blendedAlpha );
+
+ if ( category === 'hextilednormalmap' ) {
+
+ const flipGNode = nodeX.getNodeByName( 'flip_g' );
+ let normalSample = blended;
+
+ if ( flipGNode ) {
+
+ const flippedSample = vec4(
+ element( blended, 0 ),
+ sub( 1, element( blended, 1 ) ),
+ element( blended, 2 ),
+ element( blended, 3 ),
+ );
+ normalSample = toBooleanNode( flipGNode ).select( flippedSample, blended );
+
+ }
+
+ return compileHexTiledNormalMapNode( nodeX, compileContext, toVec3Channels( normalSample ) );
+
+ }
+
+ return blended;
+
+};
+
+const compileGltfTextureNode = ( nodeX, compileContext, category ) => {
+
+ const { file, uvNode, textureFile, addressModes } = getTextureInputs( nodeX, compileContext );
+ let transformedUv = uvNode;
+ const place2d = compileContext.nodeLibrary.place2d;
+
+ if ( place2d ) {
+
+ const pivot = nodeX.getNodeByName( 'pivot' ) || vec2( 0, 1 );
+ const scale = nodeX.getNodeByName( 'scale' ) || vec2( 1, 1 );
+ const rotate = nodeX.getNodeByName( 'rotate' ) || float( 0 );
+ const offset = nodeX.getNodeByName( 'offset' ) || vec2( 0, 0 );
+ const operationorder = nodeX.getNodeByName( 'operationorder' ) || int( 0 );
+ transformedUv = place2d.nodeFunc(
+ uvNode,
+ pivot,
+ div( vec2( 1, 1 ), scale ),
+ mul( rotate, - 1 ),
+ mul( offset, vec2( - 1, 1 ) ),
+ operationorder,
+ );
+
+ }
+
+ const defaultInput = nodeX.getNodeByName( 'default' );
+ let fallback = float( 0 );
+
+ if ( nodeX.type === 'color3' || nodeX.type === 'vector3' ) {
+
+ const defaultColor = defaultInput || vec3( 0, 0, 0 );
+ fallback = vec4( element( defaultColor, 0 ), element( defaultColor, 1 ), element( defaultColor, 2 ), 1 );
+
+ } else if ( nodeX.type === 'color4' || nodeX.type === 'vector4' ) {
+
+ fallback = defaultInput || vec4( 0, 0, 0, 1 );
+
+ } else {
+
+ const defaultValue = defaultInput || float( 0 );
+ fallback = vec4( defaultValue, 0, 0, 1 );
+
+ }
+
+ const node = applyTextureColorSpace( sampleTexture( textureFile, transformedUv, compileContext, fallback, addressModes, fallback ), file );
+
+ if ( category === 'gltf_normalmap' ) {
+
+ const normalScale = nodeX.getNodeByName( 'scale' ) || float( 1 );
+ return normalMap( node, normalScale );
+
+ }
+
+ const factor = nodeX.getNodeByName( 'factor' );
+
+ if ( factor ) {
+
+ if ( nodeX.type === 'color3' || nodeX.type === 'vector3' ) {
+
+ return mul( factor, toVec3Channels( node ) );
+
+ }
+
+ return mul( factor, node );
+
+ }
+
+ return node;
+
+};
+
+const compileGltfColorImageNode = ( nodeX, out, compileContext ) => {
+
+ const { file, uvNode, textureFile, addressModes } = getTextureInputs( nodeX, compileContext );
+ let transformedUv = uvNode;
+ const place2d = compileContext.nodeLibrary.place2d;
+
+ if ( place2d ) {
+
+ const pivot = nodeX.getNodeByName( 'pivot' ) || vec2( 0, 1 );
+ const scale = nodeX.getNodeByName( 'scale' ) || vec2( 1, 1 );
+ const rotate = nodeX.getNodeByName( 'rotate' ) || float( 0 );
+ const offset = nodeX.getNodeByName( 'offset' ) || vec2( 0, 0 );
+ // Match MaterialX's ND_gltf_colorimage graph implementation, which wires gltf_image with operationorder=0.
+ const operationorder = int( 0 );
+ transformedUv = place2d.nodeFunc(
+ uvNode,
+ pivot,
+ div( vec2( 1, 1 ), scale ),
+ mul( rotate, - 1 ),
+ mul( offset, vec2( - 1, 1 ) ),
+ operationorder,
+ );
+
+ }
+
+ const defaultInput = nodeX.getNodeByName( 'default' ) || vec4( 0, 0, 0, 0 );
+ const fallback = vec4( defaultInput );
+ const sampled = sampleTexture( textureFile, transformedUv, compileContext, fallback, addressModes, fallback );
+ const converted = applyTextureColorSpace( sampled, file );
+ const color = nodeX.getNodeByName( 'color' ) || vec4( 1, 1, 1, 1 );
+ const geomcolor = nodeX.getNodeByName( 'geomcolor' ) || vec4( 1, 1, 1, 1 );
+ const modulated = mul( mul( converted, color ), geomcolor );
+
+ if ( out === 'outa' || out === 'a' ) {
+
+ return element( modulated, 3 );
+
+ }
+
+ return toVec3Channels( modulated );
+
+};
+
+const compileGltfAnisotropyImageNode = ( nodeX, out, compileContext ) => {
+
+ const { uvNode, textureFile, addressModes } = getTextureInputs( nodeX, compileContext );
+ const defaultInput = nodeX.getNodeByName( 'default' ) || vec3( 1, 0.5, 1 );
+ const fallback = vec4( element( defaultInput, 0 ), element( defaultInput, 1 ), element( defaultInput, 2 ), 1 );
+ const sampled = sampleTexture( textureFile, uvNode, compileContext, fallback, addressModes, fallback );
+ const anisotropyStrengthFactor = nodeX.getNodeByName( 'anisotropy_strength' ) || float( 1 );
+ const anisotropyRotationFactor = nodeX.getNodeByName( 'anisotropy_rotation' ) || float( 0 );
+ const encodedDirection = vec2( sub( mul( element( sampled, 0 ), 2 ), 1 ), sub( mul( element( sampled, 1 ), 2 ), 1 ) );
+ const textureRotation = mx_atan2( element( encodedDirection, 1 ), element( encodedDirection, 0 ) );
+ const anisotropyStrengthOut = clamp( mul( anisotropyStrengthFactor, element( sampled, 2 ) ), 0, 1 );
+ const anisotropyRotationOut = add( anisotropyRotationFactor, textureRotation );
+
+ if ( out === 'anisotropy_rotation_out' ) {
+
+ return anisotropyRotationOut;
+
+ }
+
+ return anisotropyStrengthOut;
+
+};
+
+const compileGltfIridescenceThicknessNode = ( nodeX, compileContext ) => {
+
+ const { uvNode, textureFile, addressModes } = getTextureInputs( nodeX, compileContext );
+ const fallback = vec4( 0, 0, 0, 1 );
+ const sampled = sampleTexture( textureFile, uvNode, compileContext, fallback, addressModes, fallback );
+ const sampledThickness = element( sampled, 0 );
+ const thicknessMin = nodeX.getNodeByName( 'thicknessMin' ) || float( 100 );
+ const thicknessMax = nodeX.getNodeByName( 'thicknessMax' ) || float( 400 );
+ return add( thicknessMin, mul( sampledThickness, sub( thicknessMax, thicknessMin ) ) );
+
+};
+
+const compileTransformMatrixNode = ( nodeX, compileContext ) => {
+
+ const nodeDefName = nodeX.getAttribute( 'nodedef' );
+ const inNode = nodeX.getNodeByName( 'in' ) || float( 0 );
+ const matrixNode =
+ nodeX.getNodeByName( 'mat' ) ||
+ ( nodeDefName === 'ND_transformmatrix_vector2M3' || nodeDefName === 'ND_transformmatrix_vector3'
+ ? mat3( ...compileContext.IDENTITY_MAT3_VALUES )
+ : mat4( ...compileContext.IDENTITY_MAT4_VALUES ) );
+
+ if ( nodeDefName === 'ND_transformmatrix_vector2M3' ) {
+
+ const transformed = mul( matrixNode, vec3( element( inNode, 0 ), element( inNode, 1 ), 1 ) );
+ return vec2( element( transformed, 0 ), element( transformed, 1 ) );
+
+ }
+
+ if ( nodeDefName === 'ND_transformmatrix_vector3' ) {
+
+ return mul( matrixNode, vec3( element( inNode, 0 ), element( inNode, 1 ), element( inNode, 2 ) ) );
+
+ }
+
+ if ( nodeDefName === 'ND_transformmatrix_vector3M4' ) {
+
+ const transformed = mul( matrixNode, vec4( element( inNode, 0 ), element( inNode, 1 ), element( inNode, 2 ), 1 ) );
+ return vec3( element( transformed, 0 ), element( transformed, 1 ), element( transformed, 2 ) );
+
+ }
+
+ return mul( matrixNode, vec4( element( inNode, 0 ), element( inNode, 1 ), element( inNode, 2 ), element( inNode, 3 ) ) );
+
+};
+
+const compileCreateMatrixNode = ( nodeX ) => {
+
+ if ( nodeX.type === 'matrix44' ) {
+
+ const vector3Input = nodeX.getAttribute( 'nodedef' ) === 'ND_creatematrix_vector3_matrix44';
+ const toVec4Input = ( name, fallback, w ) => {
+
+ const input = nodeX.getNodeByName( name ) || fallback;
+ return vector3Input ? vec4( element( input, 0 ), element( input, 1 ), element( input, 2 ), w ) : input;
+
+ };
+ const in1 = toVec4Input( 'in1', vector3Input ? vec3( 1, 0, 0 ) : vec4( 1, 0, 0, 0 ), 0 );
+ const in2 = toVec4Input( 'in2', vector3Input ? vec3( 0, 1, 0 ) : vec4( 0, 1, 0, 0 ), 0 );
+ const in3 = toVec4Input( 'in3', vector3Input ? vec3( 0, 0, 1 ) : vec4( 0, 0, 1, 0 ), 0 );
+ const in4 = toVec4Input( 'in4', vector3Input ? vec3( 0, 0, 0 ) : vec4( 0, 0, 0, 1 ), 1 );
+ return mat4( in1, in2, in3, in4 );
+
+ }
+
+ const in1 = nodeX.getNodeByName( 'in1' ) || vec3( 1, 0, 0 );
+ const in2 = nodeX.getNodeByName( 'in2' ) || vec3( 0, 1, 0 );
+ const in3 = nodeX.getNodeByName( 'in3' ) || vec3( 0, 0, 1 );
+ return mat3( in1, in2, in3 );
+
+};
+
+const getMatrixElement = ( matrixNode, row, column ) => element( element( matrixNode, column ), row );
+
+const determinant2 = ( a, b, c, d ) => sub( mul( a, d ), mul( b, c ) );
+
+const determinant3 = ( m00, m01, m02, m10, m11, m12, m20, m21, m22 ) =>
+ add(
+ sub( mul( m00, determinant2( m11, m12, m21, m22 ) ), mul( m01, determinant2( m10, m12, m20, m22 ) ) ),
+ mul( m02, determinant2( m10, m11, m20, m21 ) ),
+ );
+
+const compileInvertMatrix3Node = ( matrixNode ) => {
+
+ const m00 = getMatrixElement( matrixNode, 0, 0 );
+ const m01 = getMatrixElement( matrixNode, 0, 1 );
+ const m02 = getMatrixElement( matrixNode, 0, 2 );
+ const m10 = getMatrixElement( matrixNode, 1, 0 );
+ const m11 = getMatrixElement( matrixNode, 1, 1 );
+ const m12 = getMatrixElement( matrixNode, 1, 2 );
+ const m20 = getMatrixElement( matrixNode, 2, 0 );
+ const m21 = getMatrixElement( matrixNode, 2, 1 );
+ const m22 = getMatrixElement( matrixNode, 2, 2 );
+
+ const inv00 = determinant2( m11, m12, m21, m22 );
+ const inv01 = determinant2( m02, m01, m22, m21 );
+ const inv02 = determinant2( m01, m02, m11, m12 );
+ const inv10 = determinant2( m12, m10, m22, m20 );
+ const inv11 = determinant2( m00, m02, m20, m22 );
+ const inv12 = determinant2( m02, m00, m12, m10 );
+ const inv20 = determinant2( m10, m11, m20, m21 );
+ const inv21 = determinant2( m01, m00, m21, m20 );
+ const inv22 = determinant2( m00, m01, m10, m11 );
+ const determinant = add( add( mul( m00, inv00 ), mul( m01, inv10 ) ), mul( m02, inv20 ) );
+
+ return mat3(
+ div( inv00, determinant ), div( inv10, determinant ), div( inv20, determinant ),
+ div( inv01, determinant ), div( inv11, determinant ), div( inv21, determinant ),
+ div( inv02, determinant ), div( inv12, determinant ), div( inv22, determinant ),
+ );
+
+};
+
+const compileInvertMatrix4Node = ( matrixNode ) => {
+
+ const m = [];
+ for ( let row = 0; row < 4; row ++ ) {
+
+ m[ row ] = [];
+ for ( let column = 0; column < 4; column ++ ) {
+
+ m[ row ][ column ] = getMatrixElement( matrixNode, row, column );
+
+ }
+
+ }
+
+ const getMinor3 = ( skipRow, skipColumn ) => {
+
+ const rows = [ 0, 1, 2, 3 ].filter( ( row ) => row !== skipRow );
+ const columns = [ 0, 1, 2, 3 ].filter( ( column ) => column !== skipColumn );
+ return determinant3(
+ m[ rows[ 0 ] ][ columns[ 0 ] ], m[ rows[ 0 ] ][ columns[ 1 ] ], m[ rows[ 0 ] ][ columns[ 2 ] ],
+ m[ rows[ 1 ] ][ columns[ 0 ] ], m[ rows[ 1 ] ][ columns[ 1 ] ], m[ rows[ 1 ] ][ columns[ 2 ] ],
+ m[ rows[ 2 ] ][ columns[ 0 ] ], m[ rows[ 2 ] ][ columns[ 1 ] ], m[ rows[ 2 ] ][ columns[ 2 ] ],
+ );
+
+ };
+
+ const cofactors = [];
+ for ( let row = 0; row < 4; row ++ ) {
+
+ cofactors[ row ] = [];
+ for ( let column = 0; column < 4; column ++ ) {
+
+ const minor = getMinor3( row, column );
+ cofactors[ row ][ column ] = ( row + column ) % 2 === 0 ? minor : mul( minor, - 1 );
+
+ }
+
+ }
+
+ const determinant = add(
+ add( mul( m[ 0 ][ 0 ], cofactors[ 0 ][ 0 ] ), mul( m[ 0 ][ 1 ], cofactors[ 0 ][ 1 ] ) ),
+ add( mul( m[ 0 ][ 2 ], cofactors[ 0 ][ 2 ] ), mul( m[ 0 ][ 3 ], cofactors[ 0 ][ 3 ] ) ),
+ );
+ const values = [];
+ for ( let column = 0; column < 4; column ++ ) {
+
+ for ( let row = 0; row < 4; row ++ ) {
+
+ values.push( div( cofactors[ column ][ row ], determinant ) );
+
+ }
+
+ }
+
+ return mat4( ...values );
+
+};
+
+const compileInvertMatrixNode = ( nodeX, compileContext ) => {
+
+ const inInput = nodeX.getChildByName( 'in' );
+ const matrixType = inInput ? inInput.type : null;
+ const isMatrixType = matrixType === 'matrix33' || matrixType === 'matrix44';
+
+ if ( inInput && inInput.isConst && isMatrixType ) {
+
+ const size = matrixType === 'matrix33' ? 3 : 4;
+ const identityValues = size === 3 ? compileContext.IDENTITY_MAT3_VALUES : compileContext.IDENTITY_MAT4_VALUES;
+ const matrixValues = inInput.getVector();
+ const invertedValues = compileContext.invertConstantMatrixValues( matrixValues, size );
+
+ if ( invertedValues === null ) {
+
+ nodeX.materialX.issueCollector.addInvalidValue(
+ nodeX.name,
+ `Matrix input for "${nodeX.name || nodeX.element}" is singular; using identity fallback.`,
+ );
+ return size === 3 ? mat3( ...identityValues ) : mat4( ...identityValues );
+
+ }
+
+ return size === 3 ? mat3( ...invertedValues ) : mat4( ...invertedValues );
+
+ }
+
+ const inNode = nodeX.getNodeByName( 'in' );
+ if ( isMatrixType ) {
+
+ const size = matrixType === 'matrix33' ? 3 : 4;
+ const fallback = size === 3 ? mat3( ...compileContext.IDENTITY_MAT3_VALUES ) : mat4( ...compileContext.IDENTITY_MAT4_VALUES );
+ const matrixNode = inNode === undefined || inNode === null ? fallback : inNode;
+ return size === 3 ? compileInvertMatrix3Node( matrixNode ) : compileInvertMatrix4Node( matrixNode );
+
+ }
+
+ return inNode === undefined || inNode === null ? float( 0 ) : inNode;
+
+};
+
+function createMaterialXCompileRegistry() {
+
+ const registry = new Map();
+ register( registry, [ 'convert' ], ( nodeX ) => compileConvertNode( nodeX ) );
+ register( registry, [ 'constant' ], ( nodeX ) => compileConstantNode( nodeX ) );
+ register( registry, [ 'artistic_ior' ], ( nodeX, out ) => compileArtisticIorNode( nodeX, out ) );
+ register( registry, [ 'clamp' ], ( nodeX ) => compileClampNode( nodeX ) );
+ register( registry, [ 'normalize' ], ( nodeX ) => compileNormalizeNode( nodeX ) );
+ register( registry, [ 'range' ], ( nodeX ) => compileRangeNode( nodeX ) );
+ register( registry, [ 'remap' ], ( nodeX ) => compileRemapNode( nodeX ) );
+ register( registry, [ 'position' ], ( nodeX ) => compileSpaceInputNode( nodeX, positionLocal, positionWorld ) );
+ register( registry, [ 'normal' ], ( nodeX ) => compileNormalizedSpaceInputNode( nodeX, normalLocal, normalWorld ) );
+ register( registry, [ 'tangent' ], ( nodeX ) => compileNormalizedSpaceInputNode( nodeX, tangentLocal, tangentWorld ) );
+ register( registry, [ 'bitangent' ], ( nodeX ) => compileNormalizedSpaceInputNode( nodeX, bitangentLocal, bitangentWorld ) );
+ register( registry, [ 'texcoord' ], ( nodeX, out, compileContext ) => compileTexcoordNode( nodeX, compileContext ) );
+ register( registry, [ 'geomcolor' ], ( nodeX ) => compileGeomColorNode( nodeX ) );
+ register( registry, [ 'tiledimage' ], ( nodeX, out, compileContext ) => compileTiledImageNode( nodeX, compileContext ) );
+ register( registry, [ 'image' ], ( nodeX, out, compileContext ) => compileImageLikeNode( nodeX, compileContext ) );
+ register( registry, [ 'hextiledimage', 'hextilednormalmap' ], ( nodeX, out, compileContext ) =>
+ compileHexTiledTextureNode( nodeX, compileContext, nodeX.element ) );
+ register( registry, [ 'gltf_image', 'gltf_normalmap' ], ( nodeX, out, compileContext ) =>
+ compileGltfTextureNode( nodeX, compileContext, nodeX.element ) );
+ register( registry, [ 'gltf_colorimage' ], ( nodeX, out, compileContext ) => compileGltfColorImageNode( nodeX, out, compileContext ) );
+ register( registry, [ 'gltf_anisotropy_image' ], ( nodeX, out, compileContext ) =>
+ compileGltfAnisotropyImageNode( nodeX, out, compileContext ) );
+ register( registry, [ 'gltf_iridescence_thickness' ], ( nodeX, out, compileContext ) =>
+ compileGltfIridescenceThicknessNode( nodeX, compileContext ) );
+ register( registry, [ 'switch' ], ( nodeX ) => compileSwitchNode( nodeX ) );
+ register( registry, [ 'transformmatrix' ], ( nodeX, out, compileContext ) => compileTransformMatrixNode( nodeX, compileContext ) );
+ register( registry, [ 'creatematrix' ], ( nodeX ) => compileCreateMatrixNode( nodeX ) );
+ register( registry, [ 'invertmatrix' ], ( nodeX, out, compileContext ) => compileInvertMatrixNode( nodeX, compileContext ) );
+ return registry;
+
+}
+
+function compileNodeFromRegistry( nodeX, out, compileContext ) {
+
+ const handler = compileContext.compileRegistry.get( nodeX.element );
+ if ( handler ) {
+
+ return handler( nodeX, out, compileContext );
+
+ }
+
+ const booleanConditional = compileBooleanConditionalNode( nodeX );
+ if ( booleanConditional ) {
+
+ return booleanConditional;
+
+ }
+
+ const nodeElement = compileContext.nodeLibrary[ nodeX.element ];
+ if ( ! nodeElement ) {
+
+ return undefined;
+
+ }
+
+ const args = nodeX.getNodesByNames( ...nodeElement.params );
+ for ( let i = 0; i < nodeElement.params.length; i += 1 ) {
+
+ if ( args[ i ] !== undefined && args[ i ] !== null ) {
+
+ continue;
+
+ }
+
+ const paramName = nodeElement.params[ i ];
+ if ( paramName === 'texcoord' && UV_FALLBACK_CATEGORIES.has( nodeX.element ) ) {
+
+ args[ i ] = getDefaultUvNode( compileContext );
+ continue;
+
+ }
+
+ const defaultValue = nodeElement.defaults ? nodeElement.defaults[ paramName ] : undefined;
+ if ( defaultValue !== undefined ) {
+
+ args[ i ] = typeof defaultValue === 'function' ? defaultValue() : float( defaultValue );
+ continue;
+
+ }
+
+ nodeX.materialX.issueCollector.addInvalidValue(
+ nodeX.name,
+ `Missing input "${paramName}" for node "${nodeX.name || nodeX.element}" (${nodeX.element}). Using fallback 0.`,
+ );
+ args[ i ] = float( 0 );
+
+ }
+
+ return nodeElement.usesNode ? nodeElement.nodeFunc( ...args, nodeX ) : nodeElement.nodeFunc( ...args );
+
+}
+
+export { createMaterialXCompileRegistry, compileNodeFromRegistry };
diff --git a/examples/jsm/loaders/materialx/parse/MaterialXParser.js b/examples/jsm/loaders/materialx/parse/MaterialXParser.js
new file mode 100644
index 00000000000000..e276a578144984
--- /dev/null
+++ b/examples/jsm/loaders/materialx/parse/MaterialXParser.js
@@ -0,0 +1,28 @@
+function parseMaterialXNodeTree( nodeXML, createNode, addNode, nodePath = '' ) {
+
+ const materialXNode = createNode( nodeXML, nodePath );
+ if ( materialXNode.nodePath ) {
+
+ addNode( materialXNode );
+
+ }
+
+ for ( const childNodeXML of nodeXML.children ) {
+
+ const childMXNode = parseMaterialXNodeTree( childNodeXML, createNode, addNode, materialXNode.nodePath );
+ materialXNode.add( childMXNode );
+
+ }
+
+ return materialXNode;
+
+}
+
+function parseMaterialXText( text, createNode, addNode ) {
+
+ const rootXML = new DOMParser().parseFromString( text, 'application/xml' ).documentElement;
+ return parseMaterialXNodeTree( rootXML, createNode, addNode );
+
+}
+
+export { parseMaterialXNodeTree, parseMaterialXText };
diff --git a/examples/materialx/gltf_pbr_glass_dispersion.mtlx b/examples/materialx/gltf_pbr_glass_dispersion.mtlx
new file mode 100644
index 00000000000000..4a7725318d5bfa
--- /dev/null
+++ b/examples/materialx/gltf_pbr_glass_dispersion.mtlx
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/materialx/open_pbr_surface_honey.mtlx b/examples/materialx/open_pbr_surface_honey.mtlx
new file mode 100644
index 00000000000000..aff78bee1a0370
--- /dev/null
+++ b/examples/materialx/open_pbr_surface_honey.mtlx
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/materialx/open_pbr_surface_pearl.mtlx b/examples/materialx/open_pbr_surface_pearl.mtlx
new file mode 100644
index 00000000000000..cc716420cb3d73
--- /dev/null
+++ b/examples/materialx/open_pbr_surface_pearl.mtlx
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/materialx/open_pbr_surface_velvet.mtlx b/examples/materialx/open_pbr_surface_velvet.mtlx
new file mode 100644
index 00000000000000..61c83e2ab273be
--- /dev/null
+++ b/examples/materialx/open_pbr_surface_velvet.mtlx
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/materialx/color3_vec3_cm_test.mtlx b/examples/materialx/standard_surface_color3_vec3_cm_test.mtlx
similarity index 100%
rename from examples/materialx/color3_vec3_cm_test.mtlx
rename to examples/materialx/standard_surface_color3_vec3_cm_test.mtlx
diff --git a/examples/materialx/combined_test.mtlx b/examples/materialx/standard_surface_combined_test.mtlx
similarity index 100%
rename from examples/materialx/combined_test.mtlx
rename to examples/materialx/standard_surface_combined_test.mtlx
diff --git a/examples/materialx/conditional_if_float.mtlx b/examples/materialx/standard_surface_conditional_if_float.mtlx
similarity index 100%
rename from examples/materialx/conditional_if_float.mtlx
rename to examples/materialx/standard_surface_conditional_if_float.mtlx
diff --git a/examples/materialx/heightnormal.mtlx b/examples/materialx/standard_surface_heightnormal.mtlx
similarity index 100%
rename from examples/materialx/heightnormal.mtlx
rename to examples/materialx/standard_surface_heightnormal.mtlx
diff --git a/examples/materialx/heighttonormal_normal_input.mtlx b/examples/materialx/standard_surface_heighttonormal_normal_input.mtlx
similarity index 100%
rename from examples/materialx/heighttonormal_normal_input.mtlx
rename to examples/materialx/standard_surface_heighttonormal_normal_input.mtlx
diff --git a/examples/materialx/image_transform.mtlx b/examples/materialx/standard_surface_image_transform.mtlx
similarity index 100%
rename from examples/materialx/image_transform.mtlx
rename to examples/materialx/standard_surface_image_transform.mtlx
diff --git a/examples/materialx/ior_test.mtlx b/examples/materialx/standard_surface_ior_test.mtlx
similarity index 100%
rename from examples/materialx/ior_test.mtlx
rename to examples/materialx/standard_surface_ior_test.mtlx
diff --git a/examples/materialx/opacity_only_test.mtlx b/examples/materialx/standard_surface_opacity_only_test.mtlx
similarity index 100%
rename from examples/materialx/opacity_only_test.mtlx
rename to examples/materialx/standard_surface_opacity_only_test.mtlx
diff --git a/examples/materialx/opacity_test.mtlx b/examples/materialx/standard_surface_opacity_test.mtlx
similarity index 100%
rename from examples/materialx/opacity_test.mtlx
rename to examples/materialx/standard_surface_opacity_test.mtlx
diff --git a/examples/materialx/rotate2d_test.mtlx b/examples/materialx/standard_surface_rotate2d_test.mtlx
similarity index 100%
rename from examples/materialx/rotate2d_test.mtlx
rename to examples/materialx/standard_surface_rotate2d_test.mtlx
diff --git a/examples/materialx/rotate3d_test.mtlx b/examples/materialx/standard_surface_rotate3d_test.mtlx
similarity index 100%
rename from examples/materialx/rotate3d_test.mtlx
rename to examples/materialx/standard_surface_rotate3d_test.mtlx
diff --git a/examples/materialx/roughness_test.mtlx b/examples/materialx/standard_surface_roughness_test.mtlx
similarity index 100%
rename from examples/materialx/roughness_test.mtlx
rename to examples/materialx/standard_surface_roughness_test.mtlx
diff --git a/examples/materialx/sheen_test.mtlx b/examples/materialx/standard_surface_sheen_test.mtlx
similarity index 100%
rename from examples/materialx/sheen_test.mtlx
rename to examples/materialx/standard_surface_sheen_test.mtlx
diff --git a/examples/materialx/specular_test.mtlx b/examples/materialx/standard_surface_specular_test.mtlx
similarity index 100%
rename from examples/materialx/specular_test.mtlx
rename to examples/materialx/standard_surface_specular_test.mtlx
diff --git a/examples/materialx/texture_opacity_test.mtlx b/examples/materialx/standard_surface_texture_opacity_test.mtlx
similarity index 100%
rename from examples/materialx/texture_opacity_test.mtlx
rename to examples/materialx/standard_surface_texture_opacity_test.mtlx
diff --git a/examples/materialx/thin_film_ior_clamp_test.mtlx b/examples/materialx/standard_surface_thin_film_ior_clamp_test.mtlx
similarity index 100%
rename from examples/materialx/thin_film_ior_clamp_test.mtlx
rename to examples/materialx/standard_surface_thin_film_ior_clamp_test.mtlx
diff --git a/examples/materialx/thin_film_rainbow_test.mtlx b/examples/materialx/standard_surface_thin_film_rainbow_test.mtlx
similarity index 100%
rename from examples/materialx/thin_film_rainbow_test.mtlx
rename to examples/materialx/standard_surface_thin_film_rainbow_test.mtlx
diff --git a/examples/materialx/transmission_only_test.mtlx b/examples/materialx/standard_surface_transmission_only_test.mtlx
similarity index 100%
rename from examples/materialx/transmission_only_test.mtlx
rename to examples/materialx/standard_surface_transmission_only_test.mtlx
diff --git a/examples/materialx/transmission_rough.mtlx b/examples/materialx/standard_surface_transmission_rough.mtlx
similarity index 100%
rename from examples/materialx/transmission_rough.mtlx
rename to examples/materialx/standard_surface_transmission_rough.mtlx
diff --git a/examples/materialx/transmission_test.mtlx b/examples/materialx/standard_surface_transmission_test.mtlx
similarity index 100%
rename from examples/materialx/transmission_test.mtlx
rename to examples/materialx/standard_surface_transmission_test.mtlx
diff --git a/examples/screenshots/webgpu_backdrop_water.jpg b/examples/screenshots/webgpu_backdrop_water.jpg
index 9d65952cf1d645..0295a49eb026de 100644
Binary files a/examples/screenshots/webgpu_backdrop_water.jpg and b/examples/screenshots/webgpu_backdrop_water.jpg differ
diff --git a/examples/screenshots/webgpu_lights_projector.jpg b/examples/screenshots/webgpu_lights_projector.jpg
index b8467c6d78c4f9..ab74e28bef0632 100644
Binary files a/examples/screenshots/webgpu_lights_projector.jpg and b/examples/screenshots/webgpu_lights_projector.jpg differ
diff --git a/examples/screenshots/webgpu_loader_materialx.jpg b/examples/screenshots/webgpu_loader_materialx.jpg
index 0be4f775e78110..94a2b49ca9eeda 100644
Binary files a/examples/screenshots/webgpu_loader_materialx.jpg and b/examples/screenshots/webgpu_loader_materialx.jpg differ
diff --git a/examples/screenshots/webgpu_tsl_graph.jpg b/examples/screenshots/webgpu_tsl_graph.jpg
index 884b19c65040e9..46651b3855e7f5 100644
Binary files a/examples/screenshots/webgpu_tsl_graph.jpg and b/examples/screenshots/webgpu_tsl_graph.jpg differ
diff --git a/examples/webgpu_backdrop_water.html b/examples/webgpu_backdrop_water.html
index eda0616752cb9e..afa54e7ab00315 100644
--- a/examples/webgpu_backdrop_water.html
+++ b/examples/webgpu_backdrop_water.html
@@ -61,6 +61,16 @@
const sunLight = new THREE.DirectionalLight( 0xFFE499, 5 );
sunLight.position.set( .5, 3, .5 );
+ sunLight.castShadow = true;
+ sunLight.shadow.mapSize.width = 512;
+ sunLight.shadow.mapSize.height = 512;
+ sunLight.shadow.camera.near = 0.5;
+ sunLight.shadow.camera.far = 15;
+ sunLight.shadow.camera.left = - 1.5;
+ sunLight.shadow.camera.right = 1.5;
+ sunLight.shadow.camera.top = 1.5;
+ sunLight.shadow.camera.bottom = - 1.5;
+ sunLight.shadow.bias = - 0.001;
const waterAmbientLight = new THREE.HemisphereLight( 0x333366, 0x74ccf4, 5 );
const skyAmbientLight = new THREE.HemisphereLight( 0x74ccf4, 0, 1 );
@@ -78,6 +88,16 @@
loader.load( 'models/gltf/Michelle.glb', function ( gltf ) {
model = gltf.scene;
+ model.traverse( ( child ) => {
+
+ if ( child.isMesh ) {
+
+ child.castShadow = true;
+ child.receiveShadow = true;
+
+ }
+
+ } );
mixer = new THREE.AnimationMixer( model );
@@ -132,8 +152,8 @@
const t = time.mul( .8 );
const floorUV = positionWorld.xzy;
- const waterLayer0 = mx_worley_noise_float( floorUV.mul( 4 ).add( t ) );
- const waterLayer1 = mx_worley_noise_float( floorUV.mul( 2 ).add( t ) );
+ const waterLayer0 = mx_worley_noise_float( floorUV.mul( 6 ).add( t ) ).pow( 2 );
+ const waterLayer1 = mx_worley_noise_float( floorUV.mul( 3 ).add( t ) ).pow( 2 );
const waterIntensity = waterLayer0.mul( waterLayer1 );
const waterColor = waterIntensity.mul( 1.4 ).mix( color( 0x0487e2 ), color( 0x74ccf4 ) );
@@ -156,7 +176,7 @@
const waterMaterial = new THREE.MeshBasicNodeMaterial();
waterMaterial.colorNode = waterColor.toInspector( 'Water / Color' );
- waterMaterial.backdropNode = depthEffect.mix( viewportSharedTexture(), viewportTexture.mul( depthRefraction.mix( 1, waterColor ) ) );
+ waterMaterial.backdropNode = depthEffect.mix( viewportSharedTexture(), viewportTexture.mul( depthRefraction.mix( 1, waterColor ) ) ).mul( color( 0xd3ebf8 ) );
waterMaterial.backdropAlphaNode = depthRefraction.oneMinus();
waterMaterial.transparent = true;
@@ -168,19 +188,15 @@
floor = new THREE.Mesh( new THREE.CylinderGeometry( 1.1, 1.1, 10 ), new THREE.MeshStandardNodeMaterial( { colorNode: iceColorNode } ) );
floor.position.set( 0, - 5, 0 );
+ floor.receiveShadow = true;
scene.add( floor );
// caustics
- const waterPosY = positionWorld.y.sub( water.position.y );
-
- let transition = waterPosY.add( .1 ).saturate().oneMinus();
- transition = waterPosY.lessThan( 0 ).select( transition, normalWorld.y.mix( transition, 0 ) ).toVar();
-
- const colorNode = transition.mix( material.colorNode, material.colorNode.add( waterLayer0 ) );
+ const causticFade = normalWorld.y.mix( positionWorld.y.distance( 0 ).oneMinus().saturate(), 0 );
//material.colorNode = colorNode;
- floor.material.colorNode = colorNode;
+ floor.material.colorNode = causticFade.mix( material.colorNode, material.colorNode.add( waterLayer0 ) );
// renderer
@@ -188,6 +204,7 @@
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.setAnimationLoop( animate );
+ renderer.shadowMap.enabled = true;
renderer.inspector = new Inspector();
document.body.appendChild( renderer.domElement );
diff --git a/examples/webgpu_lights_projector.html b/examples/webgpu_lights_projector.html
index e4e67aab5efc04..380841e4e8adf2 100644
--- a/examples/webgpu_lights_projector.html
+++ b/examples/webgpu_lights_projector.html
@@ -91,7 +91,7 @@
const causticEffect = Fn( ( [ projectorUV ] ) => {
- const waterLayer0 = mx_worley_noise_float( projectorUV.mul( 10 ).add( time ) );
+ const waterLayer0 = mx_worley_noise_float( projectorUV.mul( 10 ).add( time ) ).pow( 2 );
const caustic = waterLayer0.mul( color( 0x5abcd8 ) ).mul( 2 );
diff --git a/examples/webgpu_loader_materialx.html b/examples/webgpu_loader_materialx.html
index 1f3dbd769accb3..26b8d78f369b70 100644
--- a/examples/webgpu_loader_materialx.html
+++ b/examples/webgpu_loader_materialx.html
@@ -78,26 +78,30 @@
];
const localSamples = [
- 'heightnormal.mtlx',
- 'conditional_if_float.mtlx',
- 'image_transform.mtlx',
- 'color3_vec3_cm_test.mtlx',
- 'rotate2d_test.mtlx',
- 'rotate3d_test.mtlx',
- 'heighttonormal_normal_input.mtlx',
- 'roughness_test.mtlx',
- 'opacity_test.mtlx',
- 'opacity_only_test.mtlx',
- 'specular_test.mtlx',
- 'ior_test.mtlx',
- 'combined_test.mtlx',
- 'texture_opacity_test.mtlx',
- 'transmission_test.mtlx',
- 'transmission_only_test.mtlx',
- 'transmission_rough.mtlx',
- 'thin_film_rainbow_test.mtlx',
- 'thin_film_ior_clamp_test.mtlx',
- 'sheen_test.mtlx',
+ 'standard_surface_heightnormal.mtlx',
+ 'standard_surface_conditional_if_float.mtlx',
+ 'standard_surface_image_transform.mtlx',
+ 'standard_surface_color3_vec3_cm_test.mtlx',
+ 'standard_surface_rotate2d_test.mtlx',
+ 'standard_surface_rotate3d_test.mtlx',
+ 'standard_surface_heighttonormal_normal_input.mtlx',
+ 'standard_surface_roughness_test.mtlx',
+ 'standard_surface_opacity_test.mtlx',
+ 'standard_surface_opacity_only_test.mtlx',
+ 'standard_surface_specular_test.mtlx',
+ 'standard_surface_ior_test.mtlx',
+ 'standard_surface_combined_test.mtlx',
+ 'standard_surface_texture_opacity_test.mtlx',
+ 'standard_surface_transmission_test.mtlx',
+ 'standard_surface_transmission_only_test.mtlx',
+ 'standard_surface_transmission_rough.mtlx',
+ 'standard_surface_thin_film_rainbow_test.mtlx',
+ 'standard_surface_thin_film_ior_clamp_test.mtlx',
+ 'standard_surface_sheen_test.mtlx',
+ 'gltf_pbr_glass_dispersion.mtlx',
+ 'open_pbr_surface_velvet.mtlx',
+ 'open_pbr_surface_pearl.mtlx',
+ 'open_pbr_surface_honey.mtlx',
];
let camera, scene, renderer;
diff --git a/examples/webgpu_materialx_noise.html b/examples/webgpu_materialx_noise.html
index b4213737495381..b8d9f9f0462e18 100644
--- a/examples/webgpu_materialx_noise.html
+++ b/examples/webgpu_materialx_noise.html
@@ -32,12 +32,25 @@