Просмотр исходного кода

Materialx upgrade (#33485)

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: sunag <sunagbrasil@gmail.com>
Ben Houston 2 дней назад
Родитель
Сommit
fef3971954
53 измененных файлов с 5384 добавлено и 1442 удалено
  1. 68 1039
      examples/jsm/loaders/MaterialXLoader.js
  2. 108 0
      examples/jsm/loaders/materialx/MaterialXArchive.js
  3. 878 0
      examples/jsm/loaders/materialx/MaterialXDocument.js
  4. 183 0
      examples/jsm/loaders/materialx/MaterialXHextile.js
  5. 376 0
      examples/jsm/loaders/materialx/MaterialXInterfaceValidation.js
  6. 75 0
      examples/jsm/loaders/materialx/MaterialXLog.js
  7. 1 0
      examples/jsm/loaders/materialx/MaterialXNodeInterfaceRegistry.js
  8. 1021 0
      examples/jsm/loaders/materialx/MaterialXNodeLibrary.js
  9. 661 0
      examples/jsm/loaders/materialx/MaterialXSurfaceMappings.js
  10. 57 0
      examples/jsm/loaders/materialx/MaterialXUtils.js
  11. 997 0
      examples/jsm/loaders/materialx/compile/MaterialXCompileRegistry.js
  12. 28 0
      examples/jsm/loaders/materialx/parse/MaterialXParser.js
  13. 17 0
      examples/materialx/gltf_pbr_glass_dispersion.mtlx
  14. 14 0
      examples/materialx/open_pbr_surface_honey.mtlx
  15. 20 0
      examples/materialx/open_pbr_surface_pearl.mtlx
  16. 13 0
      examples/materialx/open_pbr_surface_velvet.mtlx
  17. 2 1
      examples/materialx/standard_surface_color3_vec3_cm_test.mtlx
  18. 2 2
      examples/materialx/standard_surface_combined_test.mtlx
  19. 0 0
      examples/materialx/standard_surface_conditional_if_float.mtlx
  20. 0 0
      examples/materialx/standard_surface_heightnormal.mtlx
  21. 0 0
      examples/materialx/standard_surface_heighttonormal_normal_input.mtlx
  22. 0 0
      examples/materialx/standard_surface_image_transform.mtlx
  23. 1 1
      examples/materialx/standard_surface_ior_test.mtlx
  24. 2 2
      examples/materialx/standard_surface_opacity_only_test.mtlx
  25. 1 1
      examples/materialx/standard_surface_opacity_test.mtlx
  26. 0 1
      examples/materialx/standard_surface_rotate2d_test.mtlx
  27. 6 7
      examples/materialx/standard_surface_rotate3d_test.mtlx
  28. 1 1
      examples/materialx/standard_surface_roughness_test.mtlx
  29. 0 0
      examples/materialx/standard_surface_sheen_test.mtlx
  30. 0 0
      examples/materialx/standard_surface_specular_test.mtlx
  31. 4 4
      examples/materialx/standard_surface_texture_opacity_test.mtlx
  32. 2 2
      examples/materialx/standard_surface_thin_film_ior_clamp_test.mtlx
  33. 2 2
      examples/materialx/standard_surface_thin_film_rainbow_test.mtlx
  34. 1 1
      examples/materialx/standard_surface_transmission_only_test.mtlx
  35. 1 1
      examples/materialx/standard_surface_transmission_rough.mtlx
  36. 1 1
      examples/materialx/standard_surface_transmission_test.mtlx
  37. BIN
      examples/screenshots/webgpu_backdrop_water.jpg
  38. BIN
      examples/screenshots/webgpu_geometry_loft.jpg
  39. BIN
      examples/screenshots/webgpu_lights_projector.jpg
  40. BIN
      examples/screenshots/webgpu_loader_materialx.jpg
  41. BIN
      examples/screenshots/webgpu_materialx_noise.jpg
  42. BIN
      examples/screenshots/webgpu_portal.jpg
  43. BIN
      examples/screenshots/webgpu_tsl_graph.jpg
  44. 27 10
      examples/webgpu_backdrop_water.html
  45. 1 1
      examples/webgpu_lights_projector.html
  46. 83 23
      examples/webgpu_loader_materialx.html
  47. 191 61
      examples/webgpu_materialx_noise.html
  48. 6 0
      src/Three.TSL.js
  49. 3 6
      src/nodes/materialx/MaterialXColor.js
  50. 3 6
      src/nodes/materialx/MaterialXColorTransform.js
  51. 37 0
      src/nodes/materialx/MaterialXCore.js
  52. 64 72
      src/nodes/materialx/MaterialXNodes.js
  53. 426 197
      src/nodes/materialx/MaterialXNoise.js

+ 68 - 1039
examples/jsm/loaders/MaterialXLoader.js

@@ -1,202 +1,44 @@
-import {
-	FileLoader, Loader, ImageBitmapLoader, Texture, RepeatWrapping, MeshBasicNodeMaterial,
-	MeshPhysicalNodeMaterial, DoubleSide,
-} 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';
-
-const colorSpaceLib = {
-	mx_srgb_texture_to_lin_rec709
-};
-
-class MXElement {
-
-	constructor( name, nodeFunc, params = [] ) {
-
-		this.name = name;
-		this.nodeFunc = nodeFunc;
-		this.params = params;
+import { FileLoader, Loader } from 'three/webgpu';
 
-	}
+import { MaterialXDocument } from './materialx/MaterialXDocument.js';
+import { MaterialXLog } from './materialx/MaterialXLog.js';
+import { isZipBuffer, readMtlxArchive, createArchiveResolver } from './materialx/MaterialXArchive.js';
+
+const _textDecoder = new TextDecoder();
+
+function getResourcePath( loaderPath, url ) {
+
+	if ( loaderPath ) return loaderPath;
+
+	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' ] ),
-
-	// << Channel >>
-	new MXElement( 'combine2', vec2, [ 'in1', 'in2' ] ),
-	new MXElement( 'combine3', vec3, [ 'in1', 'in2', 'in3' ] ),
-	new MXElement( 'combine4', vec4, [ 'in1', 'in2', 'in3', 'in4' ] ),
-
-	// << 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' ] ),
-
-	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' ] ),
-
-	// 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' ] ),
-
-];
-
-const MtlXLibrary = {};
-MXElements.forEach( element => MtlXLibrary[ element.name ] = element );
-
-/**
- * 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 {
 
-	/**
-	 * Constructs a new MaterialX loader.
-	 *
-	 * @param {LoadingManager} [manager] - The loading manager.
-	 */
 	constructor( manager ) {
 
 		super( manager );
 
+		this.archiveDisposer = null;
+
+	}
+
+	dispose() {
+
+		if ( this.archiveDisposer ) {
+
+			this.archiveDisposer();
+			this.archiveDisposer = null;
+
+		}
+
+		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<string,NodeMaterial>)} 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,892 +75,78 @@ 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<string,NodeMaterial>} 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 );
-
-		}
-
-		let loader = this.materialX.textureLoader;
+	loadAsync( url, onProgress, options = {} ) {
 
-		if ( uri ) {
+		if ( onProgress && typeof onProgress === 'object' ) {
 
-			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;
+		return new Promise( ( resolve, reject ) => {
 
-		this.materialX.textureCache.set( uri, texture );
-
-		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 <input name="texcoord" type="vector2" ... />
-		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' ) {
-
-				const space = this.getAttribute( 'space' );
-				node = space === 'world' ? tangentWorld : tangentLocal;
-
-			} else if ( element === 'texcoord' ) {
-
-				const indexNode = this.getChildByName( 'index' );
-				const index = indexNode ? parseInt( indexNode.value ) : 0;
-
-				node = uv( index );
-
-			} else if ( element === 'geomcolor' ) {
-
-				const indexNode = this.getChildByName( 'index' );
-				const index = indexNode ? parseInt( indexNode.value ) : 0;
-
-				node = vertexColor( index );
-
-			} else if ( element === 'tiledimage' ) {
+	parseBuffer( data, url = '', options = {} ) {
 
-				const file = this.getChildByName( 'file' );
+		this.dispose();
 
-				const textureFile = file.getTexture();
-				const uvTiling = mx_transform_uv( ...this.getNodesByNames( [ 'uvtiling', 'uvoffset' ] ) );
+		let text;
+		let archiveResolver = null;
 
-				node = texture( textureFile, uvTiling );
+		if ( data && ( isZipBuffer( data ) || /\.mtlx\.zip$/i.test( url ) ) ) {
 
-				const colorSpaceNode = file.getColorSpaceNode();
+			const archive = readMtlxArchive( data );
+			text = archive.text;
 
-				if ( colorSpaceNode ) {
+			const resolver = createArchiveResolver( archive.files );
+			archiveResolver = resolver.resolve;
+			this.archiveDisposer = resolver.dispose;
 
-					node = colorSpaceNode( node );
+		} else if ( typeof data === 'string' ) {
 
-				}
-
-			} 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 );
-
-		}
-
-		//
+			text = data;
 
-		const nodeToTypeClass = this.getClassFromType( type );
+		} else if ( data instanceof Uint8Array ) {
 
-		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 );
+			text = _textDecoder.decode( new Uint8Array( data ) );
 
 		}
 
-		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;
-
-		}
-
-		//
-
-		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 );
-
-		if ( anisotropyNode !== null ) {
-
-			material.anisotropyNode = anisotropyNode;
-			material.anisotropyRotationNode = anisotropyRotationNode || float( 0 );
-
-		}
-
-		if ( transmissionNode !== null ) {
-
-			material.transmissionNode = transmissionNode;
-			material.transmissionColorNode = transmissionColorNode || color( 1.0, 1.0, 1.0 );
-
-		}
-
-		material.thinFilmThicknessNode = thinFilmThicknessNode || float( 0 );
-		material.thinFilmIorNode = thinFilmIorNode || float( 1.5 );
-
-		if ( sheenNode !== null ) {
-
-			material.sheenNode = sheenNode;
-			material.sheenColorNode = sheenColorNode || color( 1.0, 1.0, 1.0 );
-			material.sheenRoughnessNode = sheenRoughnessNode || float( 0.5 );
-
-		}
-
-		if ( clearcoatNode !== null ) {
-
-			material.clearcoatNode = clearcoatNode;
-			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 );
+		return this.parse( text, {
+			...options,
+			archiveResolver,
+			path: options.path || getResourcePath( this.path, url )
+		} );
 
 	}
 
-	/*getMaterialXNodeFromXML( xmlNode ) {
-
-        return this.nodesXRefLib.get( xmlNode );
+	parse( text, options = {} ) {
 
-    }*/
+		const log = new MaterialXLog();
 
-	getMaterialXNode( ...names ) {
-
-		return this.nodesXLib.get( names.join( '/' ) );
-
-	}
-
-	parseNode( nodeXML, nodePath = '' ) {
-
-		const materialXNode = new MaterialXNode( this, nodeXML, nodePath );
-		if ( materialXNode.nodePath ) this.addMaterialXNode( materialXNode );
+		const document = new MaterialXDocument( this.manager, options.path || this.path, log, options.archiveResolver || null, options.uvSpace );
+		const result = document.parse( text, options.materialName || null, {
+			interfaceValidator: options.interfaceValidator,
+		} );
 
-		for ( const childNodeXML of nodeXML.children ) {
+		if ( options.throwOnErrors !== false && log.errors.length > 0 ) {
 
-			const childMXNode = this.parseNode( childNodeXML, materialXNode.nodePath );
-			materialXNode.add( childMXNode );
+			const details = log.errors.map( ( error ) => error.message ).join( ' ' );
+			throw new Error( `THREE.MaterialXLoader: MaterialX translation failed with ${log.errors.length} error(s). ${details}` );
 
 		}
 
-		return materialXNode;
-
-	}
-
-	parse( text ) {
-
-		const rootXML = new DOMParser().parseFromString( text, 'application/xml' ).documentElement;
-
-		this.textureLoader.setPath( this.path );
-
-		//
-
-		const materials = this.parseNode( rootXML ).toMaterials();
-
-		return { materials };
+		return result;
 
 	}
 

+ 108 - 0
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 };

+ 878 - 0
examples/jsm/loaders/materialx/MaterialXDocument.js

@@ -0,0 +1,878 @@
+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 { MaterialXLogCodes } from './MaterialXLog.js';
+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,
+	outy: 1,
+	outg: 1,
+	outz: 2,
+	outb: 2,
+	outw: 3,
+	outa: 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.log.add(
+			MaterialXLogCodes.INVALID_VALUE,
+			`Unsupported texture address mode "${rawMode}" on input "${inputName}". Expected constant, clamp, periodic, or mirror.`,
+			this.name,
+		);
+		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 ) {
+
+		if ( this.node !== null && out === null ) return this.node;
+
+		let 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.log.add(
+					MaterialXLogCodes.MISSING_REFERENCE,
+					`Missing MaterialX reference "${this.referencePath}" from "${this.name}".`,
+					this.name,
+				);
+				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.log.add(
+				MaterialXLogCodes.UNSUPPORTED_NODE,
+				`Unsupported MaterialX node category "${this.element}" on "${this.name}".`,
+				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.log.add(
+					MaterialXLogCodes.INVALID_VALUE,
+					`Unexpected type "${resolvedType}" on node "${this.name}".`,
+					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.log, this.name );
+
+		} else {
+
+			this.materialX.log.add(
+				MaterialXLogCodes.UNSUPPORTED_NODE,
+				`Unsupported MaterialX node category "${this.element}" on "${this.name}".`,
+				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.log.add(
+					MaterialXLogCodes.MISSING_REFERENCE,
+					`Missing MaterialX reference "${nodeX.referencePath || nodeX.nodeName || '(unknown)'}" from "${nodeX.name}".`,
+					nodeX.name,
+				);
+				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.log.add(
+					MaterialXLogCodes.MISSING_MATERIAL,
+					`Could not find surfacematerial named "${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, log, archiveResolver = null, uvSpace = 'bottom-left' ) {
+
+		this.manager = manager;
+		this.path = path;
+		this.log = log;
+		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, options = {} ) {
+
+		const rootNode = parseMaterialXText(
+			text,
+			( childNodeXML, childNodePath ) => new MaterialXNode( this, childNodeXML, childNodePath ),
+			( materialXNode ) => this.addMaterialXNode( materialXNode ),
+		);
+
+		if ( options.interfaceValidator ) {
+
+			options.interfaceValidator( rootNode, this.log );
+
+		}
+
+		const materials = rootNode.toMaterials( materialName );
+		return {
+			materials,
+			log: this.log.entries,
+			errors: this.log.errors,
+			warnings: this.log.warnings,
+		};
+
+	}
+
+}
+
+export { MaterialXDocument };

+ 183 - 0
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 ),
+	};
+
+}

+ 376 - 0
examples/jsm/loaders/materialx/MaterialXInterfaceValidation.js

@@ -0,0 +1,376 @@
+import { MtlXLibrary } from './MaterialXNodeLibrary.js';
+import { MaterialXLogCodes } from './MaterialXLog.js';
+import registryData from './MaterialXNodeInterfaceRegistry.js';
+
+const SKIP_ELEMENTS = new Set( [ 'materialx', 'input', 'output' ] );
+const CONTAINER_ELEMENTS = new Set( [ 'nodegraph' ] );
+
+const LIBRARY_FALLBACK_OUTPUT = 'out';
+
+function formatInputElement( inputNodeX ) {
+
+	const attrs = [];
+	for ( const name of [ 'name', 'type', 'nodename', 'nodegraph', 'interfacename', 'output', 'value' ] ) {
+
+		const value = inputNodeX.getAttribute( name );
+		if ( value !== null && value !== '' ) {
+
+			attrs.push( `${name}="${value}"` );
+
+		}
+
+	}
+
+	return `<input ${attrs.join( ' ' )}>`;
+
+}
+
+function formatNodeElement( nodeX ) {
+
+	const attrs = [];
+	for ( const name of [ 'name', 'type', 'nodedef' ] ) {
+
+		const value = nodeX.getAttribute( name );
+		if ( value !== null && value !== '' ) {
+
+			attrs.push( `${name}="${value}"` );
+
+		}
+
+	}
+
+	return `<${nodeX.element} ${attrs.join( ' ' )}>`;
+
+}
+
+function scoreNodedefCandidate( candidateName, nodeX ) {
+
+	const candidate = registryData.resolved[ candidateName ];
+	if ( ! candidate ) return - 1;
+
+	let score = 0;
+	const nodeType = nodeX.type;
+	const outputType = candidate.outputs.out || Object.values( candidate.outputs )[ 0 ];
+	if ( nodeType && outputType === nodeType ) {
+
+		score += 4;
+
+	}
+
+	for ( const child of nodeX.children ) {
+
+		if ( child.element !== 'input' ) continue;
+		const expectedType = candidate.inputs[ child.name ];
+		if ( ! expectedType || ! child.type ) continue;
+		if ( expectedType === child.type ) {
+
+			score += 3;
+
+		} else if ( typesCompatible( expectedType, child.type ) ) {
+
+			score += 1;
+
+		} else {
+
+			score -= 2;
+
+		}
+
+	}
+
+	return score;
+
+}
+
+function resolveInterface( nodeX ) {
+
+	const nodedefName = nodeX.getAttribute( 'nodedef' );
+	if ( nodedefName && registryData.resolved[ nodedefName ] ) {
+
+		return { source: 'nodedef', name: nodedefName, ...registryData.resolved[ nodedefName ] };
+
+	}
+
+	const candidates = registryData.byNode[ nodeX.element ] || [];
+	if ( candidates.length > 0 ) {
+
+		let selected = candidates[ 0 ];
+		let bestScore = - Infinity;
+
+		for ( const candidateName of candidates ) {
+
+			const score = scoreNodedefCandidate( candidateName, nodeX );
+			if ( score > bestScore ) {
+
+				bestScore = score;
+				selected = candidateName;
+
+			}
+
+		}
+
+		const resolved = registryData.resolved[ selected ];
+		if ( resolved ) {
+
+			return { source: 'nodedef', name: selected, ...resolved };
+
+		}
+
+	}
+
+	const libraryEntry = MtlXLibrary[ nodeX.element ];
+	if ( libraryEntry ) {
+
+		const inputs = Object.fromEntries( libraryEntry.params.map( ( param ) => [ param, null ] ) );
+		const outputs = nodeX.type ? { [ LIBRARY_FALLBACK_OUTPUT ]: nodeX.type } : {};
+		return { source: 'library', name: nodeX.element, node: nodeX.element, inputs, outputs };
+
+	}
+
+	return null;
+
+}
+
+function resolveReferencedNode( inputNodeX ) {
+
+	const materialX = inputNodeX.materialX;
+	const referencePath = inputNodeX.referencePath;
+	if ( ! referencePath ) return null;
+
+	return materialX.getMaterialXNode( referencePath ) || null;
+
+}
+
+function resolveSourceOutputType( sourceNodeX, outputName ) {
+
+	if ( sourceNodeX.element === 'output' ) {
+
+		return {
+			interface: null,
+			outputType: sourceNodeX.type,
+			outputName: sourceNodeX.name || 'out',
+		};
+
+	}
+
+	const sourceInterface = resolveInterface( sourceNodeX );
+	if ( ! sourceInterface ) return { interface: null, outputType: null, outputName };
+
+	if ( outputName ) {
+
+		if ( sourceInterface.outputs[ outputName ] ) {
+
+			return {
+				interface: sourceInterface,
+				outputType: sourceInterface.outputs[ outputName ],
+				outputName,
+			};
+
+		}
+
+		return {
+			interface: sourceInterface,
+			outputType: null,
+			outputName,
+		};
+
+	}
+
+	if ( sourceNodeX.type && sourceNodeX.type !== 'multioutput' ) {
+
+		return {
+			interface: sourceInterface,
+			outputType: sourceNodeX.type,
+			outputName: 'out',
+		};
+
+	}
+
+	if ( sourceInterface.outputs.out ) {
+
+		return {
+			interface: sourceInterface,
+			outputType: sourceInterface.outputs.out,
+			outputName: 'out',
+		};
+
+	}
+
+	const declaredOutputs = Object.entries( sourceInterface.outputs );
+	if ( declaredOutputs.length === 1 ) {
+
+		const [ singleName, singleType ] = declaredOutputs[ 0 ];
+		return {
+			interface: sourceInterface,
+			outputType: singleType,
+			outputName: singleName,
+		};
+
+	}
+
+	if ( sourceNodeX.type ) {
+
+		return {
+			interface: sourceInterface,
+			outputType: sourceNodeX.type,
+			outputName: null,
+		};
+
+	}
+
+	return { interface: sourceInterface, outputType: null, outputName: null };
+
+}
+
+function typesCompatible( expectedType, actualType ) {
+
+	if ( ! expectedType || ! actualType ) return true;
+	if ( expectedType === actualType ) return true;
+	if ( ( expectedType === 'float' && actualType === 'integer' ) || ( expectedType === 'integer' && actualType === 'float' ) ) {
+
+		return true;
+
+	}
+
+	return false;
+
+}
+
+function validatePortConnection( inputNodeX, parentNodeX, log ) {
+
+	const sourceNodeX = resolveReferencedNode( inputNodeX );
+	if ( ! sourceNodeX ) return;
+
+	const { outputType } = resolveSourceOutputType( sourceNodeX, inputNodeX.output );
+	const inputSnippet = formatInputElement( inputNodeX );
+
+	if ( inputNodeX.output && outputType === null ) {
+
+		log.add(
+			MaterialXLogCodes.INVALID_OUTPUT_CONNECTION,
+			`No output found for port connection: ${inputSnippet}`,
+			parentNodeX.name,
+		);
+		return;
+
+	}
+
+	const inputType = inputNodeX.type;
+	if ( inputType && outputType && typesCompatible( inputType, outputType ) === false ) {
+
+		log.add(
+			MaterialXLogCodes.TYPE_MISMATCH,
+			`Mismatched types in port connection: ${inputSnippet}`,
+			parentNodeX.name,
+		);
+
+	}
+
+}
+
+function validateNodeInputs( nodeX, log ) {
+
+	if ( nodeX.element === 'materialx' ) {
+
+		for ( const child of nodeX.children ) {
+
+			validateNodeInputs( child, log );
+
+		}
+
+		return;
+
+	}
+
+	if ( SKIP_ELEMENTS.has( nodeX.element ) ) return;
+	if ( CONTAINER_ELEMENTS.has( nodeX.element ) ) {
+
+		for ( const child of nodeX.children ) {
+
+			validateNodeInputs( child, log );
+
+		}
+
+		return;
+
+	}
+
+	const nodeInterface = resolveInterface( nodeX );
+	if ( nodeInterface ) {
+
+		for ( const child of nodeX.children ) {
+
+			if ( child.element !== 'input' ) continue;
+
+			const inputName = child.name;
+			if ( ! inputName ) continue;
+
+			if ( nodeInterface.inputs[ inputName ] === undefined ) {
+
+				log.add(
+					MaterialXLogCodes.UNKNOWN_INPUT,
+					`Node interface error: Input '${inputName}' doesn't match declaration: ${formatNodeElement( nodeX )}`,
+					nodeX.name,
+				);
+
+			} else if (
+				child.type &&
+				nodeInterface.inputs[ inputName ] &&
+				child.hasReference === false &&
+				typesCompatible( nodeInterface.inputs[ inputName ], child.type ) === false
+			) {
+
+				log.add(
+					MaterialXLogCodes.TYPE_MISMATCH,
+					`Input '${inputName}' type '${child.type}' doesn't match nodedef type '${nodeInterface.inputs[ inputName ]}' on ${formatNodeElement( nodeX )}`,
+					nodeX.name,
+				);
+
+			}
+
+			if ( child.hasReference ) {
+
+				validatePortConnection( child, nodeX, log );
+
+			}
+
+		}
+
+	} else if ( MtlXLibrary[ nodeX.element ] === undefined && nodeX.element !== 'surfacematerial' ) {
+
+		// Defer unsupported-node warnings to compile-time behavior.
+		for ( const child of nodeX.children ) {
+
+			if ( child.element === 'input' && child.hasReference ) {
+
+				validatePortConnection( child, nodeX, log );
+
+			}
+
+		}
+
+	}
+
+	for ( const child of nodeX.children ) {
+
+		if ( child.element !== 'input' && child.element !== 'output' ) {
+
+			validateNodeInputs( child, log );
+
+		}
+
+	}
+
+}
+
+function createStrictInterfaceValidator() {
+
+	return function validateMaterialXInterfaces( rootNode, log ) {
+
+		validateNodeInputs( rootNode, log );
+
+	};
+
+}
+
+export { createStrictInterfaceValidator };

+ 75 - 0
examples/jsm/loaders/materialx/MaterialXLog.js

@@ -0,0 +1,75 @@
+const MaterialXLogCodes = {
+	UNSUPPORTED_NODE: {
+		label: 'unsupported-node',
+		severity: 'error',
+	},
+	IGNORED_SURFACE_INPUT: {
+		label: 'ignored-surface-input',
+		severity: 'warning',
+	},
+	MISSING_REFERENCE: {
+		label: 'missing-reference',
+		severity: 'error',
+	},
+	MISSING_MATERIAL: {
+		label: 'missing-material',
+		severity: 'error',
+	},
+	INVALID_VALUE: {
+		label: 'invalid-value',
+		severity: 'error',
+	},
+	UNKNOWN_INPUT: {
+		label: 'unknown-input',
+		severity: 'error',
+	},
+	INVALID_OUTPUT_CONNECTION: {
+		label: 'invalid-output-connection',
+		severity: 'error',
+	},
+	TYPE_MISMATCH: {
+		label: 'type-mismatch',
+		severity: 'error',
+	},
+};
+
+class MaterialXLog {
+
+	constructor() {
+
+		this.entries = [];
+
+	}
+
+	get errors() {
+
+		return this.entries.filter( ( entry ) => entry.severity === 'error' );
+
+	}
+
+	get warnings() {
+
+		return this.entries.filter( ( entry ) => entry.severity === 'warning' );
+
+	}
+
+	add( code, message, nodeName ) {
+
+		const entry = {
+			code: code.label,
+			severity: code.severity,
+			message,
+		};
+		if ( nodeName !== undefined && nodeName !== null ) {
+
+			entry.nodeName = nodeName;
+
+		}
+
+		this.entries.push( entry );
+
+	}
+
+}
+
+export { MaterialXLogCodes, MaterialXLog };

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
examples/jsm/loaders/materialx/MaterialXNodeInterfaceRegistry.js


+ 1021 - 0
examples/jsm/loaders/materialx/MaterialXNodeLibrary.js

@@ -0,0 +1,1021 @@
+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 };

+ 661 - 0
examples/jsm/loaders/materialx/MaterialXSurfaceMappings.js

@@ -0,0 +1,661 @@
+import { DoubleSide } from 'three/webgpu';
+import { MaterialXLogCodes } from './MaterialXLog.js';
+import { float, color, mul, clamp, vec2, cos, sin, pow, mix, element, transformNormalToView } from 'three/tsl';
+
+const mappedStandardSurfaceInputs = new Set( [
+	'base',
+	'base_color',
+	'specular_roughness',
+	'metalness',
+	'specular',
+	'specular_color',
+	'specular_anisotropy',
+	'specular_rotation',
+	'transmission',
+	'transmission_color',
+	'transmission_depth',
+	'thin_film_thickness',
+	'thin_film_IOR',
+	'sheen',
+	'sheen_color',
+	'sheen_roughness',
+	'coat',
+	'coat_color',
+	'coat_roughness',
+	'coat_normal',
+	'normal',
+	'opacity',
+	'specular_IOR',
+	'emission',
+	'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',
+	'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, log, surfaceCategory, nodeName ) {
+
+	for ( const inputName of Object.keys( inputs ) ) {
+
+		if ( mappedInputs.has( inputName ) === false ) {
+
+			log.add(
+				MaterialXLogCodes.IGNORED_SURFACE_INPUT,
+				`${surfaceCategory} input "${inputName}" is currently ignored in MaterialX translation.`,
+				nodeName,
+			);
+
+		}
+
+	}
+
+}
+
+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, log, 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;
+	const opacityNode = hasNodeValue( inputs.opacity ) ? element( inputs.opacity, 0 ) : undefined;
+	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;
+	if ( hasNodeValue( emissionColorNode ) ) {
+
+		emissiveNode = emissiveNode ? mul( emissiveNode, emissionColorNode ) : emissionColorNode;
+
+	}
+
+	const thinFilmThicknessNode = inputs.thin_film_thickness;
+	const thinFilmIorNode = clamp( 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;
+	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, log, 'standard_surface', nodeName );
+
+}
+
+function applyGltfPbrSurface( material, inputs, log, 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, log, 'gltf_pbr', nodeName );
+
+}
+
+function applyOpenPbrSurface( material, inputs, log, 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;
+	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, log, '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,
+};

+ 57 - 0
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,
+};

+ 997 - 0
examples/jsm/loaders/materialx/compile/MaterialXCompileRegistry.js

@@ -0,0 +1,997 @@
+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';
+import { MaterialXLogCodes } from '../MaterialXLog.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.log.add(
+			MaterialXLogCodes.INVALID_VALUE,
+			`Texture node "${nodeX.name || nodeX.element}" is missing required input "file".`,
+			nodeX.name,
+		);
+		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;
+
+	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' ) {
+
+		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.log.add(
+				MaterialXLogCodes.INVALID_VALUE,
+				`Matrix input for "${nodeX.name || nodeX.element}" is singular; using identity fallback.`,
+				nodeX.name,
+			);
+			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.log.add(
+			MaterialXLogCodes.INVALID_VALUE,
+			`Missing input "${paramName}" for node "${nodeX.name || nodeX.element}" (${nodeX.element}). Using fallback 0.`,
+			nodeX.name,
+		);
+		args[ i ] = float( 0 );
+
+	}
+
+	return nodeElement.usesNode ? nodeElement.nodeFunc( ...args, nodeX ) : nodeElement.nodeFunc( ...args );
+
+}
+
+export { createMaterialXCompileRegistry, compileNodeFromRegistry };

+ 28 - 0
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 };

+ 17 - 0
examples/materialx/gltf_pbr_glass_dispersion.mtlx

@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<materialx version="1.39" colorspace="lin_rec709">
+  <gltf_pbr name="SR_dispersion_test" type="surfaceshader">
+    <input name="base_color" type="color3" value="1.0, 1.0, 1.0" />
+    <input name="metallic" type="float" value="0" />
+    <input name="roughness" type="float" value="0.02" />
+    <input name="transmission" type="float" value="1" />
+    <input name="ior" type="float" value="1.52" />
+    <input name="dispersion" type="float" value="0.35" />
+    <input name="thickness" type="float" value="0.8" />
+    <input name="attenuation_distance" type="float" value="0.75" />
+    <input name="attenuation_color" type="color3" value="0.85, 0.95, 1.0" />
+  </gltf_pbr>
+  <surfacematerial name="PBR_Dispersion_Test" type="material">
+    <input name="surfaceshader" type="surfaceshader" nodename="SR_dispersion_test" />
+  </surfacematerial>
+</materialx>

+ 14 - 0
examples/materialx/open_pbr_surface_honey.mtlx

@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<materialx version="1.39" colorspace="lin_rec709">
+  <surfacematerial name="Honey" type="material">
+    <input name="surfaceshader" type="surfaceshader" nodename="open_pbr_surface_surfaceshader" />
+  </surfacematerial>
+  <open_pbr_surface name="open_pbr_surface_surfaceshader" type="surfaceshader">
+    <input name="specular_roughness" type="float" value="0.0" />
+    <input name="specular_ior" type="float" value="1.504" />
+    <input name="transmission_weight" type="float" value="1.0" />
+    <input name="transmission_color" type="color3" value="0.83, 0.4, 0.04" />
+    <input name="transmission_depth" type="float" value="2" />
+    <input name="transmission_scatter" type="color3" value="0.9, 0.9, 0.9" />
+  </open_pbr_surface>
+</materialx>

+ 20 - 0
examples/materialx/open_pbr_surface_pearl.mtlx

@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<materialx version="1.39" colorspace="lin_rec709">
+  <surfacematerial name="Pearl" type="material">
+    <input name="surfaceshader" type="surfaceshader" nodename="open_pbr_surface_surfaceshader" />
+  </surfacematerial>
+  <open_pbr_surface name="open_pbr_surface_surfaceshader" type="surfaceshader">
+    <input name="base_color" type="color3" value="0.8, 0.75, 0.7" />
+    <input name="specular_roughness" type="float" value="0.35" />
+    <input name="specular_ior" type="float" value="1.5" />
+    <input name="subsurface_weight" type="float" value="1.0" />
+    <input name="subsurface_color" type="color3" value="0.8, 0.75, 0.7" />
+    <input name="subsurface_radius_scale" type="color3" value="0.3, 0.5, 0.3" />
+    <input name="coat_weight" type="float" value="1" />
+    <input name="coat_roughness" type="float" value="0.15" />
+    <input name="coat_ior" type="float" value="1.68" />
+    <input name="thin_film_weight" type="float" value="1.0" />
+    <input name="thin_film_thickness" type="float" value="0.420" />
+    <input name="thin_film_ior" type="float" value="2" />
+  </open_pbr_surface>
+</materialx>

+ 13 - 0
examples/materialx/open_pbr_surface_velvet.mtlx

@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<materialx version="1.39" colorspace="lin_rec709">
+  <surfacematerial name="Velvet" type="material">
+    <input name="surfaceshader" type="surfaceshader" nodename="open_pbr_surface_surfaceshader" />
+  </surfacematerial>
+  <open_pbr_surface name="open_pbr_surface_surfaceshader" type="surfaceshader">
+    <input name="base_color" type="color3" value="0.02, 0.02, 0.02" />
+    <input name="specular_roughness" type="float" value="0.8" />
+    <input name="fuzz_weight" type="float" value="1" />
+    <input name="fuzz_color" type="color3" value="0.4, 0.4, 0.4" />
+    <input name="fuzz_roughness" type="float" value="0.5" />
+  </open_pbr_surface>
+</materialx>

+ 2 - 1
examples/materialx/color3_vec3_cm_test.mtlx → examples/materialx/standard_surface_color3_vec3_cm_test.mtlx

@@ -4,7 +4,8 @@
     <input name="surfaceshader" type="surfaceshader" nodename="surface_shader1" />
   </surfacematerial>
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
-    <input name="base_color" type="color3" output="out" nodegraph="normalmap_cm" />
+    <input name="base_color" type="color3" value="0.8, 0.8, 0.8" />
+    <input name="normal" type="vector3" output="out" nodegraph="normalmap_cm" />
   </standard_surface>
   <!-- create nodegraph to test cm conversion on image node before feeding to heighttonormal  -->
   <nodegraph name="height_to_normal_cm">

+ 2 - 2
examples/materialx/combined_test.mtlx → examples/materialx/standard_surface_combined_test.mtlx

@@ -5,10 +5,10 @@
   </surfacematerial>
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
     <input name="base_color" type="color3" value="0.6, 0.8, 0.4" />
-    <input name="opacity" type="float" value="0.7" />
+    <input name="opacity" type="color3" value="0.7, 0.7, 0.7" />
     <input name="specular" type="float" value="0.9" />
     <input name="specular_color" type="color3" value="0.8, 1.0, 0.8" />
-    <input name="ior" type="float" value="1.8" />
+    <input name="specular_IOR" type="float" value="1.8" />
     <input name="specular_roughness" type="float" value="0.1" />
     <input name="metalness" type="float" value="0.0" />
   </standard_surface>

+ 0 - 0
examples/materialx/conditional_if_float.mtlx → examples/materialx/standard_surface_conditional_if_float.mtlx


+ 0 - 0
examples/materialx/heightnormal.mtlx → examples/materialx/standard_surface_heightnormal.mtlx


+ 0 - 0
examples/materialx/heighttonormal_normal_input.mtlx → examples/materialx/standard_surface_heighttonormal_normal_input.mtlx


+ 0 - 0
examples/materialx/image_transform.mtlx → examples/materialx/standard_surface_image_transform.mtlx


+ 1 - 1
examples/materialx/ior_test.mtlx → examples/materialx/standard_surface_ior_test.mtlx

@@ -5,7 +5,7 @@
   </surfacematerial>
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
     <input name="base_color" type="color3" value="0.9, 0.9, 0.9" />
-    <input name="ior" type="float" value="2.4" />
+    <input name="specular_IOR" type="float" value="2.4" />
     <input name="specular_roughness" type="float" value="0.0" />
     <input name="metalness" type="float" value="0.0" />
   </standard_surface>

+ 2 - 2
examples/materialx/opacity_only_test.mtlx → examples/materialx/standard_surface_opacity_only_test.mtlx

@@ -5,10 +5,10 @@
   </surfacematerial>
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
     <input name="base_color" type="color3" value="0.2, 0.8, 0.2" />
-    <input name="opacity" type="float" value="0.5" />
+    <input name="opacity" type="color3" value="0.5, 0.5, 0.5" />
     <input name="specular_roughness" type="float" value="0.2" />
     <input name="metalness" type="float" value="0.0" />
-    <input name="ior" type="float" value="1.5" />
+    <input name="specular_IOR" type="float" value="1.5" />
     <input name="specular" type="float" value="1.0" />
   </standard_surface>
 </materialx> 

+ 1 - 1
examples/materialx/opacity_test.mtlx → examples/materialx/standard_surface_opacity_test.mtlx

@@ -5,7 +5,7 @@
   </surfacematerial>
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
     <input name="base_color" type="color3" value="0.8, 0.2, 0.2" />
-    <input name="opacity" type="float" value="0.5" />
+    <input name="opacity" type="color3" value="0.5, 0.5, 0.5" />
     <input name="specular_roughness" type="float" value="0.1" />
     <input name="metalness" type="float" value="0.0" />
   </standard_surface>

+ 0 - 1
examples/materialx/rotate2d_test.mtlx → examples/materialx/standard_surface_rotate2d_test.mtlx

@@ -11,7 +11,6 @@
     <rotate2d name="rotate2d_1" type="vector2">
       <input name="in" type="vector2" nodename="texcoord1" />
       <input name="amount" type="float" value="45.0" unittype="angle" unit="degree" />
-      <input name="pivot" type="vector2" value="0.5, 0.5" />
     </rotate2d>
     <image name="rotated_image" type="color3">
       <input name="file" type="filename" value="resources/Images/grid.png" />

+ 6 - 7
examples/materialx/rotate3d_test.mtlx → examples/materialx/standard_surface_rotate3d_test.mtlx

@@ -12,8 +12,8 @@
       <input name="in" type="vector2" nodename="texcoord1" />
     </separate2>
     <combine3 name="texcoord_3d" type="vector3">
-      <input name="in1" type="float" nodename="separate_texcoord" output="x" />
-      <input name="in2" type="float" nodename="separate_texcoord" output="y" />
+      <input name="in1" type="float" nodename="separate_texcoord" output="outx" />
+      <input name="in2" type="float" nodename="separate_texcoord" output="outy" />
       <input name="in3" type="float" value="0.0" />
     </combine3>
     <time name="time1" type="float" />
@@ -29,11 +29,10 @@
     <separate3 name="separate_rotated" type="vector3">
       <input name="in" type="vector3" nodename="rotate3d_1" />
     </separate3>
-    <combine3 name="rotated_texcoord" type="vector3">
-      <input name="in1" type="float" nodename="separate_rotated" output="x" />
-      <input name="in2" type="float" nodename="separate_rotated" output="y" />
-      <input name="in3" type="float" nodename="separate_rotated" output="z" />
-    </combine3>
+    <combine2 name="rotated_texcoord" type="vector2">
+      <input name="in1" type="float" nodename="separate_rotated" output="outx" />
+      <input name="in2" type="float" nodename="separate_rotated" output="outy" />
+    </combine2>
     <image name="rotated_image" type="color3">
       <input name="file" type="filename" value="resources/Images/grid.png" />
       <input name="default" type="color3" value="0.5, 0.5, 0.5" />

+ 1 - 1
examples/materialx/roughness_test.mtlx → examples/materialx/standard_surface_roughness_test.mtlx

@@ -5,7 +5,7 @@
   </surfacematerial>
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
     <input name="base_color" type="color3" value="0.8, 0.8, 0.8" />
-    <input name="roughness" type="float" output="out" nodegraph="roughness_map" />
+    <input name="specular_roughness" type="float" output="out" nodegraph="roughness_map" />
   </standard_surface>
   <nodegraph name="roughness_map">
     <image name="roughness_image" type="float">

+ 0 - 0
examples/materialx/sheen_test.mtlx → examples/materialx/standard_surface_sheen_test.mtlx


+ 0 - 0
examples/materialx/specular_test.mtlx → examples/materialx/standard_surface_specular_test.mtlx


+ 4 - 4
examples/materialx/texture_opacity_test.mtlx → examples/materialx/standard_surface_texture_opacity_test.mtlx

@@ -5,15 +5,15 @@
   </surfacematerial>
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
     <input name="base_color" type="color3" value="0.2, 0.6, 0.8" />
-    <input name="opacity" type="float" output="out" nodegraph="opacity_texture_graph" />
+    <input name="opacity" type="color3" output="out" nodegraph="opacity_texture_graph" />
     <input name="specular_roughness" type="float" value="0.2" />
     <input name="metalness" type="float" value="0.0" />
   </standard_surface>
   <nodegraph name="opacity_texture_graph">
-    <image name="opacity_texture" type="float">
+    <image name="opacity_texture" type="color3">
       <input name="file" type="filename" value="resources/Images/grid.png" />
-      <input name="default" type="float" value="1.0" />
+      <input name="default" type="color3" value="1.0, 1.0, 1.0" />
     </image>
-    <output name="out" type="float" nodename="opacity_texture" />
+    <output name="out" type="color3" nodename="opacity_texture" />
   </nodegraph>
 </materialx> 

+ 2 - 2
examples/materialx/thin_film_ior_clamp_test.mtlx → examples/materialx/standard_surface_thin_film_ior_clamp_test.mtlx

@@ -6,10 +6,10 @@
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
     <input name="base_color" type="color3" value="0.1, 0.1, 0.1" />
     <input name="thin_film_thickness" type="float" value="200.0" />
-    <input name="thin_film_ior" type="float" value="3.5" />
+    <input name="thin_film_IOR" type="float" value="3.5" />
     <input name="specular_roughness" type="float" value="0.1" />
     <input name="metalness" type="float" value="0.0" />
-    <input name="ior" type="float" value="1.5" />
+    <input name="specular_IOR" type="float" value="1.5" />
     <input name="specular" type="float" value="1.0" />
   </standard_surface>
 </materialx> 

+ 2 - 2
examples/materialx/thin_film_rainbow_test.mtlx → examples/materialx/standard_surface_thin_film_rainbow_test.mtlx

@@ -6,10 +6,10 @@
   <standard_surface name="surface_shader1" type="surfaceshader" nodedef="ND_standard_surface_surfaceshader">
     <input name="base_color" type="color3" value="0.05, 0.05, 0.05" />
     <input name="thin_film_thickness" type="float" value="100.0" />
-    <input name="thin_film_ior" type="float" value="2.0" />
+    <input name="thin_film_IOR" type="float" value="2.0" />
     <input name="specular_roughness" type="float" value="0.0" />
     <input name="metalness" type="float" value="0.0" />
-    <input name="ior" type="float" value="1.5" />
+    <input name="specular_IOR" type="float" value="1.5" />
     <input name="specular" type="float" value="1.0" />
   </standard_surface>
 </materialx> 

+ 1 - 1
examples/materialx/transmission_only_test.mtlx → examples/materialx/standard_surface_transmission_only_test.mtlx

@@ -7,7 +7,7 @@
     <input name="base_color" type="color3" value="1.0, 1.0, 1.0" />
     <input name="transmission" type="float" value="1.0" />
     <input name="transmission_color" type="color3" value="1.0, 1.0, 1.0" />
-    <input name="ior" type="float" value="1.5" />
+    <input name="specular_IOR" type="float" value="1.5" />
     <input name="specular_roughness" type="float" value="0.05" />
     <input name="metalness" type="float" value="0.0" />
   </standard_surface>

+ 1 - 1
examples/materialx/transmission_rough.mtlx → examples/materialx/standard_surface_transmission_rough.mtlx

@@ -7,7 +7,7 @@
     <input name="base_color" type="color3" value="0.9, 0.95, 1.0" />
     <input name="transmission" type="float" value="0.95" />
     <input name="transmission_color" type="color3" value="0.95, 0.98, 1.0" />
-    <input name="ior" type="float" value="1.5" />
+    <input name="specular_IOR" type="float" value="1.5" />
     <input name="specular_roughness" type="float" value="0.3" />
     <input name="metalness" type="float" value="0.0" />
   </standard_surface>

+ 1 - 1
examples/materialx/transmission_test.mtlx → examples/materialx/standard_surface_transmission_test.mtlx

@@ -7,7 +7,7 @@
     <input name="base_color" type="color3" value="0.9, 0.95, 1.0" />
     <input name="transmission" type="float" value="0.9" />
     <input name="transmission_color" type="color3" value="0.95, 0.98, 1.0" />
-    <input name="ior" type="float" value="1.5" />
+    <input name="specular_IOR" type="float" value="1.5" />
     <input name="specular_roughness" type="float" value="0.1" />
     <input name="metalness" type="float" value="0.0" />
   </standard_surface>

BIN
examples/screenshots/webgpu_backdrop_water.jpg


BIN
examples/screenshots/webgpu_geometry_loft.jpg


BIN
examples/screenshots/webgpu_lights_projector.jpg


BIN
examples/screenshots/webgpu_loader_materialx.jpg


BIN
examples/screenshots/webgpu_materialx_noise.jpg


BIN
examples/screenshots/webgpu_portal.jpg


BIN
examples/screenshots/webgpu_tsl_graph.jpg


+ 27 - 10
examples/webgpu_backdrop_water.html

@@ -65,6 +65,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 );
@@ -82,6 +92,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 );
 
@@ -136,8 +156,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 ) );
@@ -160,7 +180,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;
 
@@ -172,19 +192,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
 
@@ -192,6 +208,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 );
 

+ 1 - 1
examples/webgpu_lights_projector.html

@@ -94,7 +94,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 );
 

+ 83 - 23
examples/webgpu_loader_materialx.html

@@ -51,7 +51,11 @@
 			import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
 			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
+			import * as MikkTSpace from 'three/addons/libs/mikktspace.module.js';
+			import { computeMikkTSpaceTangents } from 'three/addons/utils/BufferGeometryUtils.js';
+
 			import { MaterialXLoader } from 'three/addons/loaders/MaterialXLoader.js';
+			import { createStrictInterfaceValidator } from 'three/addons/loaders/materialx/MaterialXInterfaceValidation.js';
 
 			import { Inspector } from 'three/addons/inspector/Inspector.js';
 
@@ -82,31 +86,36 @@
 			];
 
 			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;
 			let controls, prefab;
 			const models = [];
+			const strictInterfaceValidator = createStrictInterfaceValidator();
 
 			init();
 
@@ -191,6 +200,7 @@
 						scene.environment = texture;
 
 						prefab = ( await new GLTFLoader().loadAsync( './models/gltf/ShaderBall.glb' ) ).scene;
+						await computePrefabTangents();
 
 						for ( const sample of samples ) {
 
@@ -212,6 +222,30 @@
 
 			}
 
+			async function computePrefabTangents() {
+
+				await MikkTSpace.ready;
+
+				prefab.traverse( function ( node ) {
+
+					if ( node.isMesh === false ) return;
+
+					const geometry = node.geometry;
+
+					if ( geometry === undefined ) return;
+
+					if ( geometry.hasAttribute( 'tangent' ) ) return;
+
+					if ( geometry.hasAttribute( 'position' ) && geometry.hasAttribute( 'normal' ) && geometry.hasAttribute( 'uv' ) ) {
+
+						computeMikkTSpaceTangents( geometry, MikkTSpace );
+
+					}
+
+				} );
+
+			}
+
 			function updateModelsAlign() {
 
 				const COLUMN_COUNT = 6;
@@ -234,6 +268,27 @@
 
 			}
 
+			function reportMaterialXLog( sample, materialName, log ) {
+
+				if ( log.length === 0 ) return;
+
+				const label = materialName ? `"${materialName}"` : `"${sample}"`;
+				const errorCount = log.filter( ( entry ) => entry.severity === 'error' ).length;
+				const warningCount = log.filter( ( entry ) => entry.severity === 'warning' ).length;
+				console.groupCollapsed( `THREE.MaterialXLoader: ${errorCount} error(s), ${warningCount} warning(s) for ${label} (${sample})` );
+
+				for ( const entry of log ) {
+
+					const nodeName = entry.nodeName ? ` [${entry.nodeName}]` : '';
+					const style = entry.severity === 'warning' ? 'color: #c90' : 'color: #c00';
+					console[ entry.severity === 'warning' ? 'warn' : 'error' ]( `%c${entry.severity}: ${entry.code}${nodeName}: ${entry.message}`, style );
+
+				}
+
+				console.groupEnd();
+
+			}
+
 			async function addSample( sample, path ) {
 
 				const model = prefab.clone();
@@ -246,10 +301,15 @@
 
 				//
 
-				const material = await new MaterialXLoader()
+				const { materials, log } = await new MaterialXLoader()
 					.setPath( path )
-					.loadAsync( sample )
-					.then( ( { materials } ) => Object.values( materials ).pop() );
+					.loadAsync( sample, {
+						uvSpace: 'top-left',
+						interfaceValidator: strictInterfaceValidator,
+						throwOnErrors: false,
+					} );
+				const material = Object.values( materials ).pop();
+				reportMaterialXLog( sample, material ? material.name : null, log );
 
 				const calibrationMesh = model.getObjectByName( 'Calibration_Mesh' );
 				calibrationMesh.material = material;

+ 191 - 61
examples/webgpu_materialx_noise.html

@@ -36,12 +36,25 @@
 		<script type="module">
 
 			import * as THREE from 'three/webgpu';
-			import { normalWorld, time, mx_noise_vec3, mx_worley_noise_vec3, mx_cell_noise_float, mx_fractal_noise_vec3 } from 'three/tsl';
+			import {
+				Fn, add, dot, element, float, floor, int, mix, mul, normalWorld, sub, time, uv, vec2, vec3,
+				mx_cell_noise_float,
+				mx_fractal_noise_vec3,
+				mx_modulo,
+				mx_noise_vec3,
+				mx_unifiednoise2d,
+				mx_unifiednoise3d,
+				mx_worley_noise_float_2d,
+				mx_worley_noise_float_3d
+			} from 'three/tsl';
 
 			import { Inspector } from 'three/addons/inspector/Inspector.js';
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
 			import { HDRCubeTextureLoader } from 'three/addons/loaders/HDRCubeTextureLoader.js';
+			import { FontLoader } from 'three/addons/loaders/FontLoader.js';
+			import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
+			import { mxHextileComputeBlendWeights, mxHextileCoord } from 'three/addons/loaders/materialx/MaterialXHextile.js';
 
 			let container;
 
@@ -49,79 +62,120 @@
 
 			let particleLight;
 			let group;
+			const spheres = [];
 
 			init();
 
-			function init() {
+			async function init() {
 
 				container = document.createElement( 'div' );
 				document.body.appendChild( container );
 
 				camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 1, 1000 );
-				camera.position.z = 100;
+				camera.position.z = 190;
 
 				scene = new THREE.Scene();
 
 				group = new THREE.Group();
 				scene.add( group );
 
-				new HDRCubeTextureLoader()
-					.setPath( 'textures/cube/pisaHDR/' )
-					.load( [ 'px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr' ],
-						function ( hdrTexture ) {
-
-							const geometry = new THREE.SphereGeometry( 8, 64, 32 );
-
-							const customUV = normalWorld.mul( 10 ).add( time );
-
-							// left top
-
-							let material = new THREE.MeshPhysicalNodeMaterial();
-							material.colorNode = mx_noise_vec3( customUV );
-
-							let mesh = new THREE.Mesh( geometry, material );
-							mesh.position.x = - 10;
-							mesh.position.y = 10;
-							group.add( mesh );
-
-							// right top
-
-							material = new THREE.MeshPhysicalNodeMaterial();
-							material.colorNode = mx_cell_noise_float( customUV );
-
-							mesh = new THREE.Mesh( geometry, material );
-							mesh.position.x = 10;
-							mesh.position.y = 10;
-							group.add( mesh );
-
-							// left bottom
-
-							material = new THREE.MeshPhysicalNodeMaterial();
-							material.colorNode = mx_worley_noise_vec3( customUV );
-
-							mesh = new THREE.Mesh( geometry, material );
-							mesh.position.x = - 10;
-							mesh.position.y = - 10;
-							group.add( mesh );
-
-							// right bottom
-
-							material = new THREE.MeshPhysicalNodeMaterial();
-							material.colorNode = mx_fractal_noise_vec3( customUV.mul( .2 ) );
-
-							mesh = new THREE.Mesh( geometry, material );
-							mesh.position.x = 10;
-							mesh.position.y = - 10;
-							group.add( mesh );
-
-							//
-
-							scene.background = hdrTexture;
-							scene.environment = hdrTexture;
-
-						}
-
+				const [ hdrTexture, font ] = await Promise.all( [
+					new HDRCubeTextureLoader()
+						.setPath( 'textures/cube/pisaHDR/' )
+						.loadAsync( [ 'px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr' ] ),
+					new FontLoader().loadAsync( 'fonts/helvetiker_regular.typeface.json' )
+				] );
+
+				const geometry = new THREE.SphereGeometry( 5, 64, 32 );
+				const labelMaterial = new THREE.MeshBasicMaterial( { color: 0xffffff } );
+
+				const position2D = uv( 0 ).mul( 12 ).add( vec2( time.mul( .35 ), time.mul( .19 ) ) );
+				const position3D = normalWorld.mul( 4 ).add( vec3( time.mul( .35 ), time.mul( .19 ), time.mul( .27 ) ) );
+				const fractal2D = vec3( position2D.x, position2D.y, 0 ).mul( .35 );
+				const fractal3D = position3D.mul( .35 );
+				const unifiedOffset2D = vec2( time.mul( .35 ), time.mul( .19 ) );
+				const unifiedOffset3D = vec3( time.mul( .35 ), time.mul( .19 ), time.mul( .27 ) );
+				const unifiedNoise3D = ( noiseType, position, freq, offset, jitter = 1, outmin = 0, outmax = 1, clampoutput = false, octaves = 1, lacunarity = 2, diminish = .5, style = 0 ) => Fn( () => mx_unifiednoise3d( noiseType, position, freq, offset, jitter, outmin, outmax, clampoutput, octaves, lacunarity, diminish, style ) )();
+				const textileTexcoord = ( speedX, speedY ) => uv( 0 ).mul( 3 ).add( vec2( time.mul( speedX ), time.mul( speedY ) ) );
+				const 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 hextiledCheckerboard = ( texcoord, {
+					color1 = vec3( 1, 1, 1 ),
+					color2 = vec3( 0, 0, 0 ),
+					uvtiling = vec2( 4, 4 ),
+					uvoffset = vec2( 0, 0 ),
+					tiling = vec2( 1, 1 ),
+					rotation = 0,
+					rotationrange = vec2( 0, 360 ),
+					scale = 0,
+					scalerange = vec2( .5, 2 ),
+					offset = 0,
+					offsetrange = vec2( 0, 1 ),
+					falloff = .5,
+					falloffcontrast = .5,
+					lumacoeffs = vec3( 0.2722287, 0.6740818, 0.0536895 )
+				} = {} ) => {
+
+					const tileData = mxHextileCoord( mul( texcoord, tiling ), rotation, rotationrange, scale, scalerange, offset, offsetrange );
+					const c0 = checkerboard( color1, color2, uvtiling, uvoffset, tileData.coords[ 0 ] );
+					const c1 = checkerboard( color1, color2, uvtiling, uvoffset, tileData.coords[ 1 ] );
+					const c2 = checkerboard( color1, color2, uvtiling, uvoffset, tileData.coords[ 2 ] );
+					const luminanceWeights = mix(
+						vec3( 1, 1, 1 ),
+						vec3( dot( c0, lumacoeffs ), dot( c1, lumacoeffs ), dot( c2, lumacoeffs ) ),
+						vec3( falloffcontrast, falloffcontrast, falloffcontrast )
 					);
+					const blendWeights = mxHextileComputeBlendWeights( luminanceWeights, tileData.weights, falloff );
+					return add( add( mul( element( blendWeights, 0 ), c0 ), mul( element( blendWeights, 1 ), c1 ) ), mul( element( blendWeights, 2 ), c2 ) );
+
+				};
+
+				const noiseExamples = [
+					{ label: [ 'Noise', '2D' ], createNode: () => mx_noise_vec3( position2D ) },
+					{ label: [ 'Noise', '3D' ], createNode: () => mx_noise_vec3( position3D ) },
+					{ label: [ 'Cellnoise', '2D' ], createNode: () => mx_cell_noise_float( position2D ) },
+					{ label: [ 'Cellnoise', '3D' ], createNode: () => mx_cell_noise_float( position3D ) },
+					{ label: [ 'Worley', '2D style 0' ], createNode: () => mx_worley_noise_float_2d( position2D, 1, 0 ) },
+					{ label: [ 'Worley', '3D style 0' ], createNode: () => mx_worley_noise_float_3d( position3D, 1, 0 ) },
+					{ label: [ 'Worley', '2D style 1' ], createNode: () => mx_worley_noise_float_2d( position2D, 1, 1 ) },
+					{ label: [ 'Worley', '3D style 1' ], createNode: () => mx_worley_noise_float_3d( position3D, 1, 1 ) },
+					{ label: [ 'Fractal', '2D' ], createNode: () => mx_fractal_noise_vec3( fractal2D ) },
+					{ label: [ 'Fractal', '3D' ], createNode: () => mx_fractal_noise_vec3( fractal3D ) },
+					{ label: [ 'Unified Perlin', '2D' ], createNode: () => mx_unifiednoise2d( int( 0 ), position2D, vec2( 1, 1 ), unifiedOffset2D ) },
+					{ label: [ 'Unified Perlin', '3D' ], createNode: () => unifiedNoise3D( int( 0 ), position3D, vec3( 1, 1, 1 ), unifiedOffset3D ) },
+					{ label: [ 'Unified Cell', '2D' ], createNode: () => mx_unifiednoise2d( int( 1 ), position2D, vec2( 1, 1 ), unifiedOffset2D ) },
+					{ label: [ 'Unified Cell', '3D' ], createNode: () => unifiedNoise3D( int( 1 ), position3D, vec3( 1, 1, 1 ), unifiedOffset3D ) },
+					{ label: [ 'Unified Worley', '2D style 0' ], createNode: () => mx_unifiednoise2d( int( 2 ), position2D, vec2( 1, 1 ), unifiedOffset2D, 1, 0, 1, false, 1, 2, .5, 0 ) },
+					{ label: [ 'Unified Worley', '3D style 0' ], createNode: () => unifiedNoise3D( int( 2 ), position3D, vec3( 1, 1, 1 ), unifiedOffset3D, 1, 0, 1, false, 1, 2, .5, 0 ) },
+					{ label: [ 'Unified Worley', '2D style 1' ], createNode: () => mx_unifiednoise2d( int( 2 ), position2D, vec2( 1, 1 ), unifiedOffset2D, 1, 0, 1, false, 1, 2, .5, 1 ) },
+					{ label: [ 'Unified Worley', '3D style 1' ], createNode: () => unifiedNoise3D( int( 2 ), position3D, vec3( 1, 1, 1 ), unifiedOffset3D, 1, 0, 1, false, 1, 2, .5, 1 ) },
+					{ label: [ 'Unified Fractal', '2D' ], createNode: () => mx_unifiednoise2d( int( 3 ), position2D, vec2( 1, 1 ), unifiedOffset2D, 1, 0, 1, false, 3 ) },
+					{ label: [ 'Unified Fractal', '3D' ], createNode: () => unifiedNoise3D( int( 3 ), position3D, vec3( 1, 1, 1 ), unifiedOffset3D, 1, 0, 1, false, 3 ) },
+					{
+						label: [ 'Hex Tiled', 'checkerboard' ],
+						createNode: () => hextiledCheckerboard( textileTexcoord( .08, .04 ), {
+							color1: vec3( 1, 1, 1 ),
+							color2: vec3( 0, 0, 0 ),
+							uvtiling: vec2( 8, 8 ),
+							tiling: vec2( 2.2, 2.2 ),
+							rotation: .25,
+							scale: .1,
+							offset: .2,
+							falloff: .35,
+							falloffcontrast: .25
+						} )
+					}
+				];
+
+				createNoiseGrid( noiseExamples, geometry, font, labelMaterial );
+
+				scene.background = hdrTexture;
+				scene.environment = hdrTexture;
 
 				// LIGHTS
 
@@ -177,9 +231,9 @@
 				particleLight.position.y = Math.cos( timer * 5 ) * 40;
 				particleLight.position.z = Math.cos( timer * 3 ) * 30;
 
-				for ( let i = 0; i < group.children.length; i ++ ) {
+				for ( let i = 0; i < spheres.length; i ++ ) {
 
-					const child = group.children[ i ];
+					const child = spheres[ i ];
 					child.rotation.y += 0.005;
 
 				}
@@ -188,6 +242,82 @@
 
 			}
 
+			function createNoiseGrid( noiseExamples, geometry, font, labelMaterial ) {
+
+				const columns = 7;
+				const spacingX = 15.5;
+				const spacingY = 18;
+				const rowOffset = ( Math.ceil( noiseExamples.length / columns ) - 1 ) * spacingY * .5;
+				const columnOffset = ( columns - 1 ) * spacingX * .5;
+				const errorMaterial = new THREE.MeshBasicMaterial( { color: 0xff3333 } );
+
+				for ( let i = 0; i < noiseExamples.length; i ++ ) {
+
+					const example = noiseExamples[ i ];
+					const column = i % columns;
+					const row = Math.floor( i / columns );
+					const x = column * spacingX - columnOffset;
+					const y = rowOffset - row * spacingY;
+					const labelLines = example.label.slice();
+
+					const material = new THREE.MeshPhysicalNodeMaterial();
+
+					try {
+
+						material.colorNode = example.createNode();
+
+					} catch ( error ) {
+
+						console.error( `Failed to create MaterialX noise sample: ${ example.label.join( ' ' ) }`, error );
+						labelLines.push( 'Failed' );
+
+					}
+
+					const mesh = new THREE.Mesh( geometry, material.colorNode ? material : errorMaterial );
+					mesh.position.set( x, y, 0 );
+					group.add( mesh );
+					spheres.push( mesh );
+
+					const label = createLabel( labelLines, font, labelMaterial );
+					label.position.set( x, y - 7.5, 0 );
+					group.add( label );
+
+				}
+
+			}
+
+			function createLabel( lines, font, material ) {
+
+				const label = new THREE.Group();
+				const lineHeight = 1.55;
+				const startY = ( lines.length - 1 ) * lineHeight * .5;
+
+				for ( let i = 0; i < lines.length; i ++ ) {
+
+					const textGeometry = new TextGeometry( lines[ i ], {
+						font: font,
+						size: 1.15,
+						depth: 0.02,
+						curveSegments: 2
+					} );
+
+					textGeometry.computeBoundingBox();
+
+					const boundingBox = textGeometry.boundingBox;
+					const offsetX = - 0.5 * ( boundingBox.max.x - boundingBox.min.x );
+					const offsetY = - 0.5 * ( boundingBox.max.y - boundingBox.min.y );
+					textGeometry.translate( offsetX, offsetY, 0 );
+
+					const textMesh = new THREE.Mesh( textGeometry, material );
+					textMesh.position.y = startY - i * lineHeight;
+					label.add( textMesh );
+
+				}
+
+				return label;
+
+			}
+
 		</script>
 	</body>
 </html>

+ 6 - 0
src/Three.TSL.js

@@ -337,8 +337,10 @@ export const mx_aastep = TSL.mx_aastep;
 export const mx_add = TSL.mx_add;
 export const mx_atan2 = TSL.mx_atan2;
 export const mx_cell_noise_float = TSL.mx_cell_noise_float;
+export const mx_cell_noise_vec3 = TSL.mx_cell_noise_vec3;
 export const mx_contrast = TSL.mx_contrast;
 export const mx_divide = TSL.mx_divide;
+export const mx_fractal_noise_float_2d = TSL.mx_fractal_noise_float_2d;
 export const mx_fractal_noise_float = TSL.mx_fractal_noise_float;
 export const mx_fractal_noise_vec2 = TSL.mx_fractal_noise_vec2;
 export const mx_fractal_noise_vec3 = TSL.mx_fractal_noise_vec3;
@@ -365,6 +367,7 @@ export const mx_rotate2d = TSL.mx_rotate2d;
 export const mx_rotate3d = TSL.mx_rotate3d;
 export const mx_safepower = TSL.mx_safepower;
 export const mx_separate = TSL.mx_separate;
+export const mx_smoothstep = TSL.mx_smoothstep;
 export const mx_splitlr = TSL.mx_splitlr;
 export const mx_splittb = TSL.mx_splittb;
 export const mx_srgb_texture_to_lin_rec709 = TSL.mx_srgb_texture_to_lin_rec709;
@@ -374,8 +377,11 @@ export const mx_transform_uv = TSL.mx_transform_uv;
 export const mx_unifiednoise2d = TSL.mx_unifiednoise2d;
 export const mx_unifiednoise3d = TSL.mx_unifiednoise3d;
 export const mx_worley_noise_float = TSL.mx_worley_noise_float;
+export const mx_worley_noise_float_2d = TSL.mx_worley_noise_float_2d;
+export const mx_worley_noise_float_3d = TSL.mx_worley_noise_float_3d;
 export const mx_worley_noise_vec2 = TSL.mx_worley_noise_vec2;
 export const mx_worley_noise_vec3 = TSL.mx_worley_noise_vec3;
+export const mx_worley_noise_vec3_style = TSL.mx_worley_noise_vec3_style;
 export const negate = TSL.negate;
 export const negateOnBackSide = TSL.negateOnBackSide;
 export const neutralToneMapping = TSL.neutralToneMapping;

+ 3 - 6
src/nodes/materialx/lib/mx_hsv.js → src/nodes/materialx/MaterialXColor.js

@@ -1,9 +1,6 @@
-// Three.js Transpiler
-// https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/stdlib/genglsl/lib/mx_hsv.glsl
-
-import { int, float, vec3, If, Fn } from '../../tsl/TSLBase.js';
-import { add } from '../../math/OperatorNode.js';
-import { floor, trunc, max, min } from '../../math/MathNode.js';
+import { int, float, vec3, If, Fn } from '../tsl/TSLBase.js';
+import { add } from '../math/OperatorNode.js';
+import { floor, trunc, max, min } from '../math/MathNode.js';
 
 export const mx_hsvtorgb = /*@__PURE__*/ Fn( ( [ hsv ] ) => {
 

+ 3 - 6
src/nodes/materialx/lib/mx_transform_color.js → src/nodes/materialx/MaterialXColorTransform.js

@@ -1,9 +1,6 @@
-// Three.js Transpiler
-// https://github.com/AcademySoftwareFoundation/MaterialX/blob/main/libraries/stdlib/genglsl/lib/mx_transform_color.glsl
-
-import { bvec3, vec3, Fn } from '../../tsl/TSLBase.js';
-import { greaterThan } from '../../math/OperatorNode.js';
-import { max, pow, mix } from '../../math/MathNode.js';
+import { bvec3, vec3, Fn } from '../tsl/TSLBase.js';
+import { greaterThan } from '../math/OperatorNode.js';
+import { max, pow, mix } from '../math/MathNode.js';
 
 export const mx_srgb_texture_to_lin_rec709 = /*@__PURE__*/ Fn( ( [ color_immutable ] ) => {
 

+ 37 - 0
src/nodes/materialx/MaterialXCore.js

@@ -0,0 +1,37 @@
+import { float, vec2, vec3, add, sub, mul, sin, cos, normalize } from '../tsl/TSLBase.js';
+
+export const mx_rotate2d = ( input, amount = 0 ) => {
+
+	input = vec2( input );
+	amount = float( amount );
+
+	const rotationRadians = mul( amount, Math.PI / 180.0 );
+	const sa = sin( rotationRadians );
+	const ca = cos( rotationRadians );
+	const x = input.x;
+	const y = input.y;
+
+	return vec2( add( mul( ca, x ), mul( sa, y ) ), sub( mul( ca, y ), mul( sa, x ) ) );
+
+};
+
+export const mx_rotate3d = ( input, amount = 0, axis = vec3( 0, 1, 0 ) ) => {
+
+	input = vec3( input );
+	amount = float( amount );
+	axis = vec3( axis );
+
+	const normalizedAxis = normalize( axis );
+	const rotationRadians = mul( amount, Math.PI / 180.0 );
+	const s = sin( rotationRadians );
+	const c = cos( rotationRadians );
+	const oc = sub( 1, c );
+
+	// based on https://en.wikipedia.org/wiki/Rodrigues%27_rotation_formula
+	// but the code in the Wikipedia article is for v' = M * v, where as
+	// MaterialX is v' = v * M, thus the order of parameters into the first cross are reversed
+	return input.mul( c )
+		.add( input.cross( normalizedAxis ).mul( s ) )
+		.add( normalizedAxis.mul( normalizedAxis.dot( input ).mul( oc ) ) );
+
+};

+ 64 - 72
src/nodes/materialx/MaterialXNodes.js

@@ -1,17 +1,22 @@
 import {
 	mx_perlin_noise_float, mx_perlin_noise_vec3,
-	mx_worley_noise_float as worley_noise_float, mx_worley_noise_vec2 as worley_noise_vec2, mx_worley_noise_vec3 as worley_noise_vec3,
-	mx_cell_noise_float as cell_noise_float,
+	mx_worley_noise_float_2d as worley_noise_float_2d, mx_worley_noise_float_3d as worley_noise_float_3d,
+	mx_worley_noise_vec2 as worley_noise_vec2, mx_worley_noise_vec3 as worley_noise_vec3,
+	mx_worley_noise_vec3_style as worley_noise_vec3_style,
+	mx_cell_noise_float as cell_noise_float, mx_cell_noise_vec3 as cell_noise_vec3,
 	mx_unifiednoise2d as unifiednoise2d, mx_unifiednoise3d as unifiednoise3d,
+	mx_fractal_noise_float_2d as fractal_noise_float_2d,
 	mx_fractal_noise_float as fractal_noise_float, mx_fractal_noise_vec2 as fractal_noise_vec2, mx_fractal_noise_vec3 as fractal_noise_vec3, mx_fractal_noise_vec4 as fractal_noise_vec4
-} from './lib/mx_noise.js';
-import { mx_hsvtorgb, mx_rgbtohsv } from './lib/mx_hsv.js';
-import { mx_srgb_texture_to_lin_rec709 } from './lib/mx_transform_color.js';
+} from './MaterialXNoise.js';
+import { mx_rotate2d, mx_rotate3d } from './MaterialXCore.js';
+import { mx_hsvtorgb, mx_rgbtohsv } from './MaterialXColor.js';
+import { mx_srgb_texture_to_lin_rec709 } from './MaterialXColorTransform.js';
 
-import { float, vec2, vec3, vec4, int, add, sub, mul, div, mod, atan, mix, pow, smoothstep } from '../tsl/TSLBase.js';
+import {
+	float, vec2, vec3, vec4, int, add, sub, mul, div, atan, mix, pow, smoothstep,
+	floor, abs, max, clamp, step, cross, dot, normalize
+} from '../tsl/TSLBase.js';
 import { uv } from '../accessors/UV.js';
-import { bumpMap } from '../display/BumpMapNode.js';
-import { rotate } from '../utils/RotateNode.js';
 import { frameId, time } from '../utils/Timer.js';
 
 export const mx_aastep = ( threshold, value ) => {
@@ -27,7 +32,7 @@ export const mx_aastep = ( threshold, value ) => {
 
 const _ramp = ( a, b, uv, p ) => mix( a, b, uv[ p ].clamp() );
 export const mx_ramplr = ( valuel, valuer, texcoord = uv() ) => _ramp( valuel, valuer, texcoord, 'x' );
-export const mx_ramptb = ( valuet, valueb, texcoord = uv() ) => _ramp( valuet, valueb, texcoord, 'y' );
+export const mx_ramptb = ( valueb, valuet, texcoord = uv() ) => _ramp( valueb, valuet, texcoord, 'y' );
 
 // Bilinear ramp: interpolate between four corners (tl, tr, bl, br) using texcoord.x and texcoord.y
 export const mx_ramp4 = (
@@ -38,13 +43,13 @@ export const mx_ramp4 = (
 	const v = texcoord.y.clamp();
 	const top = mix( valuetl, valuetr, u );
 	const bottom = mix( valuebl, valuebr, u );
-	return mix( top, bottom, v );
+	return mix( bottom, top, v );
 
 };
 
 const _split = ( a, b, center, uv, p ) => mix( a, b, mx_aastep( center, uv[ p ] ) );
 export const mx_splitlr = ( valuel, valuer, center, texcoord = uv() ) => _split( valuel, valuer, center, texcoord, 'x' );
-export const mx_splittb = ( valuet, valueb, center, texcoord = uv() ) => _split( valuet, valueb, center, texcoord, 'y' );
+export const mx_splittb = ( valueb, valuet, center, texcoord = uv() ) => _split( valueb, valuet, center, texcoord, 'y' );
 
 export const mx_transform_uv = ( uv_scale = 1, uv_offset = 0, uv_geo = uv() ) => uv_geo.mul( uv_scale ).add( uv_offset );
 
@@ -71,20 +76,38 @@ export const mx_noise_vec4 = ( texcoord = uv(), amplitude = 1, pivot = 0 ) => {
 
 };
 
-export const mx_unifiednoise2d = ( noiseType, texcoord = uv(), freq = vec2( 1, 1 ), offset = vec2( 0, 0 ), jitter = 1, outmin = 0, outmax = 1, clampoutput = false, octaves = 1, lacunarity = 2, diminish = .5 ) => unifiednoise2d( noiseType, texcoord.convert( 'vec2|vec3' ), freq, offset, jitter, outmin, outmax, clampoutput, octaves, lacunarity, diminish );
-export const mx_unifiednoise3d = ( noiseType, texcoord = uv(), freq = vec2( 1, 1 ), offset = vec2( 0, 0 ), jitter = 1, outmin = 0, outmax = 1, clampoutput = false, octaves = 1, lacunarity = 2, diminish = .5 ) => unifiednoise3d( noiseType, texcoord.convert( 'vec2|vec3' ), freq, offset, jitter, outmin, outmax, clampoutput, octaves, lacunarity, diminish );
+export const mx_smoothstep = ( inNode, low = 0, high = 1 ) => {
+
+	const range = sub( high, low );
+	const safeRange = max( abs( range ), float( 1e-6 ) );
+	const t = clamp( div( sub( inNode, low ), safeRange ), float( 0 ), float( 1 ) );
+	const hermite = mul( mul( t, t ), sub( float( 3 ), mul( float( 2 ), t ) ) );
+	const fallback = step( high, inNode );
+	const useFallback = step( high, low );
+	return mix( hermite, fallback, useFallback );
+
+};
+
+export const mx_cell_noise_vec3 = ( texcoord = uv() ) => cell_noise_vec3( texcoord.convert( 'vec2|vec3' ) );
+export const mx_worley_noise_float_2d = ( texcoord = uv(), jitter = 1, style = 0 ) => worley_noise_float_2d( texcoord, jitter, style );
+export const mx_worley_noise_float_3d = ( texcoord = uv(), jitter = 1, style = 0 ) => worley_noise_float_3d( texcoord, jitter, style );
+export const mx_unifiednoise2d = ( noiseType, texcoord = uv(), freq = vec2( 1, 1 ), offset = vec2( 0, 0 ), jitter = 1, outmin = 0, outmax = 1, clampoutput = false, octaves = 1, lacunarity = 2, diminish = .5, style = 0 ) => unifiednoise2d( noiseType, texcoord, freq, offset, jitter, outmin, outmax, clampoutput, octaves, lacunarity, diminish, style );
+export const mx_unifiednoise3d = ( noiseType, texcoord = uv(), freq = vec3( 1, 1, 1 ), offset = vec3( 0, 0, 0 ), jitter = 1, outmin = 0, outmax = 1, clampoutput = false, octaves = 1, lacunarity = 2, diminish = .5, style = 0 ) => unifiednoise3d( noiseType, texcoord, freq, offset, jitter, outmin, outmax, clampoutput, octaves, lacunarity, diminish, style );
 
-export const mx_worley_noise_float = ( texcoord = uv(), jitter = 1 ) => worley_noise_float( texcoord.convert( 'vec2|vec3' ), jitter, int( 1 ) );
+export const mx_worley_noise_float = ( texcoord = uv(), jitter = 1, style = 0 ) => mx_worley_noise_float_3d( texcoord.convert( 'vec2|vec3' ), jitter, style );
 export const mx_worley_noise_vec2 = ( texcoord = uv(), jitter = 1 ) => worley_noise_vec2( texcoord.convert( 'vec2|vec3' ), jitter, int( 1 ) );
-export const mx_worley_noise_vec3 = ( texcoord = uv(), jitter = 1 ) => worley_noise_vec3( texcoord.convert( 'vec2|vec3' ), jitter, int( 1 ) );
+export const mx_worley_noise_vec3 = ( texcoord = uv(), jitter = 1, metric = 1 ) => worley_noise_vec3( texcoord.convert( 'vec2|vec3' ), jitter, int( metric ) );
+export const mx_worley_noise_vec3_style = ( texcoord = uv(), jitter = 1, style = 0, metric = 0 ) => worley_noise_vec3_style( texcoord.convert( 'vec2|vec3' ), jitter, int( style ), int( metric ) );
 
 export const mx_cell_noise_float = ( texcoord = uv() ) => cell_noise_float( texcoord.convert( 'vec2|vec3' ) );
 
+export const mx_fractal_noise_float_2d = ( texcoord = uv(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => fractal_noise_float_2d( texcoord, int( octaves ), lacunarity, diminish ).mul( amplitude );
 export const mx_fractal_noise_float = ( position = uv(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => fractal_noise_float( position, int( octaves ), lacunarity, diminish ).mul( amplitude );
 export const mx_fractal_noise_vec2 = ( position = uv(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => fractal_noise_vec2( position, int( octaves ), lacunarity, diminish ).mul( amplitude );
 export const mx_fractal_noise_vec3 = ( position = uv(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => fractal_noise_vec3( position, int( octaves ), lacunarity, diminish ).mul( amplitude );
 export const mx_fractal_noise_vec4 = ( position = uv(), octaves = 3, lacunarity = 2, diminish = .5, amplitude = 1 ) => fractal_noise_vec4( position, int( octaves ), lacunarity, diminish ).mul( amplitude );
 
+export { mx_rotate2d, mx_rotate3d };
 export { mx_hsvtorgb, mx_rgbtohsv, mx_srgb_texture_to_lin_rec709 };
 
 // === Moved from MaterialXLoader.js ===
@@ -94,15 +117,15 @@ export const mx_add = ( in1, in2 = float( 0 ) ) => add( in1, in2 );
 export const mx_subtract = ( in1, in2 = float( 0 ) ) => sub( in1, in2 );
 export const mx_multiply = ( in1, in2 = float( 1 ) ) => mul( in1, in2 );
 export const mx_divide = ( in1, in2 = float( 1 ) ) => div( in1, in2 );
-export const mx_modulo = ( in1, in2 = float( 1 ) ) => mod( in1, in2 );
+export const mx_modulo = ( in1, in2 = float( 1 ) ) => sub( in1, mul( in2, floor( div( in1, in2 ) ) ) );
 export const mx_power = ( in1, in2 = float( 1 ) ) => pow( in1, in2 );
 export const mx_atan2 = ( in1 = float( 0 ), in2 = float( 1 ) ) => atan( in1, in2 );
 export const mx_timer = () => time;
 export const mx_frame = () => frameId;
 export const mx_invert = ( in1, amount = float( 1 ) ) => sub( amount, in1 );
-export const mx_ifgreater = ( value1, value2, in1, in2 ) => value1.greaterThan( value2 ).mix( in1, in2 );
-export const mx_ifgreatereq = ( value1, value2, in1, in2 ) => value1.greaterThanEqual( value2 ).mix( in1, in2 );
-export const mx_ifequal = ( value1, value2, in1, in2 ) => value1.equal( value2 ).mix( in1, in2 );
+export const mx_ifgreater = ( value1, value2, in1, in2 ) => value1.greaterThan( value2 ).mix( in2, in1 );
+export const mx_ifgreatereq = ( value1, value2, in1, in2 ) => value1.greaterThanEqual( value2 ).mix( in2, in1 );
+export const mx_ifequal = ( value1, value2, in1, in2 ) => value1.equal( value2 ).mix( in2, in1 );
 
 // Enhanced separate node to support multi-output referencing (outx, outy, outz, outw)
 export const mx_separate = ( in1, channelOrOut = null ) => {
@@ -133,65 +156,34 @@ export const mx_separate = ( in1, channelOrOut = null ) => {
 };
 
 export const mx_place2d = (
-	texcoord, pivot = vec2( 0.5, 0.5 ), scale = vec2( 1, 1 ), rotate = float( 0 ), offset = vec2( 0, 0 )/*, operationorder = int( 0 )*/
+	texcoord, pivot = vec2( 0, 0 ), scale = vec2( 1, 1 ), rotate = float( 0 ), offset = vec2( 0, 0 ), operationorder = int( 0 )
 ) => {
 
-	let uv = texcoord;
-	if ( pivot ) uv = uv.sub( pivot );
-	if ( scale ) uv = uv.mul( scale );
-	if ( rotate ) {
-
-		const rad = rotate.mul( Math.PI / 180.0 );
-		const cosR = rad.cos();
-		const sinR = rad.sin();
-		uv = vec2(
-			uv.x.mul( cosR ).sub( uv.y.mul( sinR ) ),
-			uv.x.mul( sinR ).add( uv.y.mul( cosR ) )
-		);
-
-	}
-
-	if ( pivot ) uv = uv.add( pivot );
-	if ( offset ) uv = uv.add( offset );
-	return uv;
-
-};
-
-export const mx_rotate2d = ( input, amount ) => {
-
-	input = vec2( input );
-	amount = float( amount );
-
-	const radians = amount.mul( Math.PI / 180.0 );
-	return rotate( input, radians );
-
-};
-
-export const mx_rotate3d = ( input, amount, axis ) => {
-
-	input = vec3( input );
-	amount = float( amount );
-	axis = vec3( axis );
+	const centered = sub( texcoord, pivot );
+	const srt = add( sub( mx_rotate2d( div( centered, scale ), rotate ), offset ), pivot );
+	const trs = add( div( mx_rotate2d( sub( centered, offset ), rotate ), scale ), pivot );
 
+	if ( typeof operationorder === 'number' ) return Math.abs( operationorder ) > Number.EPSILON ? trs : srt;
 
-	const radians = amount.mul( Math.PI / 180.0 );
-	const nAxis = axis.normalize();
-	const cosA = radians.cos();
-	const sinA = radians.sin();
-	const oneMinusCosA = float( 1 ).sub( cosA );
-	const rot =
-		input.mul( cosA )
-			.add( nAxis.cross( input ).mul( sinA ) )
-			.add( nAxis.mul( nAxis.dot( input ) ).mul( oneMinusCosA ) );
-	return rot;
+	return mix( srt, trs, step( 0.5, float( operationorder ) ) );
 
 };
 
-export const mx_heighttonormal = ( input, scale/*, texcoord*/ ) => {
-
-	input = vec3( input );
-	scale = float( scale );
-
-	return bumpMap( input, scale );
+export const mx_heighttonormal = ( input, scale = 1, texcoord = uv() ) => {
+
+	const sobelScale = float( 1.0 / 16.0 );
+	const height = float( input );
+	const uvNode = vec2( texcoord );
+	const dHdS = vec2( height.dFdx(), height.dFdy() ).mul( float( scale ) ).mul( sobelScale );
+	const dUdS = vec2( uvNode.x.dFdx(), uvNode.x.dFdy() );
+	const dVdS = vec2( uvNode.y.dFdx(), uvNode.y.dFdy() );
+	const tangent = vec3( dUdS.x, dVdS.x, dHdS.x );
+	const bitangent = vec3( dUdS.y, dVdS.y, dHdS.y );
+	let n = cross( tangent, bitangent );
+	const invalid = dot( n, n ).lessThan( float( 1e-12 ) );
+	n = invalid.mix( n, vec3( 0, 0, 1 ) );
+	const mirrored = n.z.lessThan( float( 0 ) );
+	n = mirrored.mix( n, n.mul( - 1 ) );
+	return normalize( n ).mul( 0.5 ).add( 0.5 );
 
 };

+ 426 - 197
src/nodes/materialx/lib/mx_noise.js → src/nodes/materialx/MaterialXNoise.js

@@ -1,12 +1,10 @@
-// Three.js Transpiler
-// https://raw.githubusercontent.com/AcademySoftwareFoundation/MaterialX/main/libraries/stdlib/genglsl/lib/mx_noise.glsl
-
-import { int, uint, float, vec3, bool, uvec3, vec2, vec4, If, Fn } from '../../tsl/TSLBase.js';
-import { select } from '../../math/ConditionalNode.js';
-import { sub, mul } from '../../math/OperatorNode.js';
-import { floor, abs, max, dot, min, sqrt, clamp } from '../../math/MathNode.js';
-import { overloadingFn } from '../../utils/FunctionOverloadingNode.js';
-import { Loop } from '../../utils/LoopNode.js';
+import { int, uint, float, vec3, bool, uvec3, vec2, vec4, If, Fn } from '../tsl/TSLBase.js';
+import { select } from '../math/ConditionalNode.js';
+import { add, sub, mul } from '../math/OperatorNode.js';
+import { floor, abs, max, dot, sqrt, clamp, fract } from '../math/MathNode.js';
+import { overloadingFn } from '../utils/FunctionOverloadingNode.js';
+import { Loop } from '../utils/LoopNode.js';
+import { mx_rotate2d, mx_rotate3d } from './MaterialXCore.js';
 
 
 export const mx_select = /*@__PURE__*/ Fn( ( [ b_immutable, t_immutable, f_immutable ] ) => {
@@ -66,6 +64,22 @@ export const mx_floorfrac = /*@__PURE__*/ Fn( ( [ x_immutable, i ] ) => {
 
 } );
 
+const mxBilerpValue = ( v0, v1, v2, v3, s, t ) => {
+
+	const s1 = float( sub( 1.0, s ) ).toVar();
+	return sub( 1.0, t ).mul( v0.mul( s1 ).add( v1.mul( s ) ) ).add( t.mul( v2.mul( s1 ).add( v3.mul( s ) ) ) );
+
+};
+
+const mxTrilerpValue = ( v0, v1, v2, v3, v4, v5, v6, v7, s, t, r ) => {
+
+	const s1 = float( sub( 1.0, s ) ).toVar();
+	const t1 = float( sub( 1.0, t ) ).toVar();
+	const r1 = float( sub( 1.0, r ) ).toVar();
+	return r1.mul( t1.mul( v0.mul( s1 ).add( v1.mul( s ) ) ).add( t.mul( v2.mul( s1 ).add( v3.mul( s ) ) ) ) ).add( r.mul( t1.mul( v4.mul( s1 ).add( v5.mul( s ) ) ).add( t.mul( v6.mul( s1 ).add( v7.mul( s ) ) ) ) ) );
+
+};
+
 export const mx_bilerp_0 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2_immutable, v3_immutable, s_immutable, t_immutable ] ) => {
 
 	const t = float( t_immutable ).toVar();
@@ -74,9 +88,8 @@ export const mx_bilerp_0 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2_
 	const v2 = float( v2_immutable ).toVar();
 	const v1 = float( v1_immutable ).toVar();
 	const v0 = float( v0_immutable ).toVar();
-	const s1 = float( sub( 1.0, s ) ).toVar();
 
-	return sub( 1.0, t ).mul( v0.mul( s1 ).add( v1.mul( s ) ) ).add( t.mul( v2.mul( s1 ).add( v3.mul( s ) ) ) );
+	return mxBilerpValue( v0, v1, v2, v3, s, t );
 
 } ).setLayout( {
 	name: 'mx_bilerp_0',
@@ -99,9 +112,8 @@ export const mx_bilerp_1 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2_
 	const v2 = vec3( v2_immutable ).toVar();
 	const v1 = vec3( v1_immutable ).toVar();
 	const v0 = vec3( v0_immutable ).toVar();
-	const s1 = float( sub( 1.0, s ) ).toVar();
 
-	return sub( 1.0, t ).mul( v0.mul( s1 ).add( v1.mul( s ) ) ).add( t.mul( v2.mul( s1 ).add( v3.mul( s ) ) ) );
+	return mxBilerpValue( v0, v1, v2, v3, s, t );
 
 } ).setLayout( {
 	name: 'mx_bilerp_1',
@@ -131,11 +143,8 @@ export const mx_trilerp_0 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2
 	const v2 = float( v2_immutable ).toVar();
 	const v1 = float( v1_immutable ).toVar();
 	const v0 = float( v0_immutable ).toVar();
-	const s1 = float( sub( 1.0, s ) ).toVar();
-	const t1 = float( sub( 1.0, t ) ).toVar();
-	const r1 = float( sub( 1.0, r ) ).toVar();
 
-	return r1.mul( t1.mul( v0.mul( s1 ).add( v1.mul( s ) ) ).add( t.mul( v2.mul( s1 ).add( v3.mul( s ) ) ) ) ).add( r.mul( t1.mul( v4.mul( s1 ).add( v5.mul( s ) ) ).add( t.mul( v6.mul( s1 ).add( v7.mul( s ) ) ) ) ) );
+	return mxTrilerpValue( v0, v1, v2, v3, v4, v5, v6, v7, s, t, r );
 
 } ).setLayout( {
 	name: 'mx_trilerp_0',
@@ -168,11 +177,8 @@ export const mx_trilerp_1 = /*@__PURE__*/ Fn( ( [ v0_immutable, v1_immutable, v2
 	const v2 = vec3( v2_immutable ).toVar();
 	const v1 = vec3( v1_immutable ).toVar();
 	const v0 = vec3( v0_immutable ).toVar();
-	const s1 = float( sub( 1.0, s ) ).toVar();
-	const t1 = float( sub( 1.0, t ) ).toVar();
-	const r1 = float( sub( 1.0, r ) ).toVar();
 
-	return r1.mul( t1.mul( v0.mul( s1 ).add( v1.mul( s ) ) ).add( t.mul( v2.mul( s1 ).add( v3.mul( s ) ) ) ) ).add( r.mul( t1.mul( v4.mul( s1 ).add( v5.mul( s ) ) ).add( t.mul( v6.mul( s1 ).add( v7.mul( s ) ) ) ) ) );
+	return mxTrilerpValue( v0, v1, v2, v3, v4, v5, v6, v7, s, t, r );
 
 } ).setLayout( {
 	name: 'mx_trilerp_1',
@@ -358,6 +364,9 @@ export const mx_rotl32 = /*@__PURE__*/ Fn( ( [ x_immutable, k_immutable ] ) => {
 
 export const mx_bjmix = /*@__PURE__*/ Fn( ( [ a, b, c ] ) => {
 
+	a = uint( a ).toVar();
+	b = uint( b ).toVar();
+	c = uint( c ).toVar();
 	a.subAssign( c );
 	a.bitXorAssign( mx_rotl32( c, int( 4 ) ) );
 	c.addAssign( b );
@@ -377,6 +386,16 @@ export const mx_bjmix = /*@__PURE__*/ Fn( ( [ a, b, c ] ) => {
 	c.bitXorAssign( mx_rotl32( b, int( 4 ) ) );
 	b.addAssign( a );
 
+	return uvec3( a, b, c );
+
+} ).setLayout( {
+	name: 'mx_bjmix',
+	type: 'uvec3',
+	inputs: [
+		{ name: 'a', type: 'uint' },
+		{ name: 'b', type: 'uint' },
+		{ name: 'c', type: 'uint' }
+	]
 } );
 
 export const mx_bjfinal = /*@__PURE__*/ Fn( ( [ a_immutable, b_immutable, c_immutable ] ) => {
@@ -415,7 +434,7 @@ export const mx_bits_to_01 = /*@__PURE__*/ Fn( ( [ bits_immutable ] ) => {
 
 	const bits = uint( bits_immutable ).toVar();
 
-	return float( bits ).div( float( uint( int( 0xffffffff ) ) ) );
+	return float( bits ).div( float( uint( 0xffffffff ) ) );
 
 } ).setLayout( {
 	name: 'mx_bits_to_01',
@@ -443,7 +462,7 @@ export const mx_hash_int_0 = /*@__PURE__*/ Fn( ( [ x_immutable ] ) => {
 
 	const x = int( x_immutable ).toVar();
 	const len = uint( uint( 1 ) ).toVar();
-	const seed = uint( uint( int( 0xdeadbeef ) ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ).toVar();
+	const seed = uint( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ).toVar();
 
 	return mx_bjfinal( seed.add( uint( x ) ), seed, seed );
 
@@ -461,7 +480,7 @@ export const mx_hash_int_1 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable ] )
 	const x = int( x_immutable ).toVar();
 	const len = uint( uint( 2 ) ).toVar();
 	const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
-	a.assign( b.assign( c.assign( uint( int( 0xdeadbeef ) ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
+	a.assign( b.assign( c.assign( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
 	a.addAssign( uint( x ) );
 	b.addAssign( uint( y ) );
 
@@ -483,7 +502,7 @@ export const mx_hash_int_2 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable, z_i
 	const x = int( x_immutable ).toVar();
 	const len = uint( uint( 3 ) ).toVar();
 	const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
-	a.assign( b.assign( c.assign( uint( int( 0xdeadbeef ) ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
+	a.assign( b.assign( c.assign( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
 	a.addAssign( uint( x ) );
 	b.addAssign( uint( y ) );
 	c.addAssign( uint( z ) );
@@ -508,11 +527,14 @@ export const mx_hash_int_3 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable, z_i
 	const x = int( x_immutable ).toVar();
 	const len = uint( uint( 4 ) ).toVar();
 	const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
-	a.assign( b.assign( c.assign( uint( int( 0xdeadbeef ) ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
+	a.assign( b.assign( c.assign( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
 	a.addAssign( uint( x ) );
 	b.addAssign( uint( y ) );
 	c.addAssign( uint( z ) );
-	mx_bjmix( a, b, c );
+	const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
+	a.assign( mixed.x );
+	b.assign( mixed.y );
+	c.assign( mixed.z );
 	a.addAssign( uint( xx ) );
 
 	return mx_bjfinal( a, b, c );
@@ -537,11 +559,14 @@ export const mx_hash_int_4 = /*@__PURE__*/ Fn( ( [ x_immutable, y_immutable, z_i
 	const x = int( x_immutable ).toVar();
 	const len = uint( uint( 5 ) ).toVar();
 	const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
-	a.assign( b.assign( c.assign( uint( int( 0xdeadbeef ) ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
+	a.assign( b.assign( c.assign( uint( 0xdeadbeef ).add( len.shiftLeft( uint( 2 ) ) ).add( uint( 13 ) ) ) ) );
 	a.addAssign( uint( x ) );
 	b.addAssign( uint( y ) );
 	c.addAssign( uint( z ) );
-	mx_bjmix( a, b, c );
+	const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
+	a.assign( mixed.x );
+	b.assign( mixed.y );
+	c.assign( mixed.z );
 	a.addAssign( uint( xx ) );
 	b.addAssign( uint( yy ) );
 
@@ -768,7 +793,11 @@ export const mx_cell_noise_vec3_0 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
 	const p = float( p_immutable ).toVar();
 	const ix = int( mx_floor( p ) ).toVar();
 
-	return vec3( mx_bits_to_01( mx_hash_int( ix, int( 0 ) ) ), mx_bits_to_01( mx_hash_int( ix, int( 1 ) ) ), mx_bits_to_01( mx_hash_int( ix, int( 2 ) ) ) );
+	return vec3(
+		mx_bits_to_01( mx_hash_int( ix, int( 0 ) ) ),
+		mx_bits_to_01( mx_hash_int( ix, int( 1 ) ) ),
+		mx_bits_to_01( mx_hash_int( ix, int( 2 ) ) )
+	);
 
 } ).setLayout( {
 	name: 'mx_cell_noise_vec3_0',
@@ -784,7 +813,11 @@ export const mx_cell_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
 	const ix = int( mx_floor( p.x ) ).toVar();
 	const iy = int( mx_floor( p.y ) ).toVar();
 
-	return vec3( mx_bits_to_01( mx_hash_int( ix, iy, int( 0 ) ) ), mx_bits_to_01( mx_hash_int( ix, iy, int( 1 ) ) ), mx_bits_to_01( mx_hash_int( ix, iy, int( 2 ) ) ) );
+	return vec3(
+		mx_bits_to_01( mx_hash_int( ix, iy, int( 0 ) ) ),
+		mx_bits_to_01( mx_hash_int( ix, iy, int( 1 ) ) ),
+		mx_bits_to_01( mx_hash_int( ix, iy, int( 2 ) ) )
+	);
 
 } ).setLayout( {
 	name: 'mx_cell_noise_vec3_1',
@@ -794,14 +827,29 @@ export const mx_cell_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
 	]
 } );
 
-export const mx_cell_noise_vec3_2 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
+export const mx_cell_noise_vec3_2 = /*@__PURE__*/ Fn( ( [ positionInput ] ) => {
 
-	const p = vec3( p_immutable ).toVar();
-	const ix = int( mx_floor( p.x ) ).toVar();
-	const iy = int( mx_floor( p.y ) ).toVar();
-	const iz = int( mx_floor( p.z ) ).toVar();
-
-	return vec3( mx_bits_to_01( mx_hash_int( ix, iy, iz, int( 0 ) ) ), mx_bits_to_01( mx_hash_int( ix, iy, iz, int( 1 ) ) ), mx_bits_to_01( mx_hash_int( ix, iy, iz, int( 2 ) ) ) );
+	const position = vec3( positionInput ).toVar();
+	const ix = int( floor( position.x ) ).toVar();
+	const iy = int( floor( position.y ) ).toVar();
+	const iz = int( floor( position.z ) ).toVar();
+	const seed = uint( 0xdeadbeef + ( 4 << 2 ) + 13 ).toVar();
+	const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
+	a.assign( b.assign( c.assign( seed ) ) );
+	a.addAssign( uint( ix ) );
+	b.addAssign( uint( iy ) );
+	c.addAssign( uint( iz ) );
+
+	const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
+	const hash0 = mx_bjfinal( mixed.x, mixed.y, mixed.z );
+	const hash1 = mx_bjfinal( add( mixed.x, uint( 1 ) ), mixed.y, mixed.z );
+	const hash2 = mx_bjfinal( add( mixed.x, uint( 2 ) ), mixed.y, mixed.z );
+
+	return vec3(
+		mx_bits_to_01( hash0 ),
+		mx_bits_to_01( hash1 ),
+		mx_bits_to_01( hash2 )
+	);
 
 } ).setLayout( {
 	name: 'mx_cell_noise_vec3_2',
@@ -818,8 +866,24 @@ export const mx_cell_noise_vec3_3 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
 	const iy = int( mx_floor( p.y ) ).toVar();
 	const iz = int( mx_floor( p.z ) ).toVar();
 	const iw = int( mx_floor( p.w ) ).toVar();
-
-	return vec3( mx_bits_to_01( mx_hash_int( ix, iy, iz, iw, int( 0 ) ) ), mx_bits_to_01( mx_hash_int( ix, iy, iz, iw, int( 1 ) ) ), mx_bits_to_01( mx_hash_int( ix, iy, iz, iw, int( 2 ) ) ) );
+	const seed = uint( 0xdeadbeef + ( 5 << 2 ) + 13 ).toVar();
+	const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
+	a.assign( b.assign( c.assign( seed ) ) );
+	a.addAssign( uint( ix ) );
+	b.addAssign( uint( iy ) );
+	c.addAssign( uint( iz ) );
+
+	const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
+	a.assign( mixed.x );
+	b.assign( mixed.y );
+	c.assign( mixed.z );
+	a.addAssign( uint( iw ) );
+
+	return vec3(
+		mx_bits_to_01( mx_bjfinal( a, b, c ) ),
+		mx_bits_to_01( mx_bjfinal( a, add( b, uint( 1 ) ), c ) ),
+		mx_bits_to_01( mx_bjfinal( a, add( b, uint( 2 ) ), c ) )
+	);
 
 } ).setLayout( {
 	name: 'mx_cell_noise_vec3_3',
@@ -831,6 +895,62 @@ export const mx_cell_noise_vec3_3 = /*@__PURE__*/ Fn( ( [ p_immutable ] ) => {
 
 export const mx_cell_noise_vec3 = /*@__PURE__*/ overloadingFn( [ mx_cell_noise_vec3_0, mx_cell_noise_vec3_1, mx_cell_noise_vec3_2, mx_cell_noise_vec3_3 ] );
 
+const mx_cell_noise_vec3_3d = /*@__PURE__*/ Fn( ( [ positionInput ] ) => {
+
+	const position = vec3( positionInput ).toVar();
+	const ix = int( floor( position.x ) ).toVar();
+	const iy = int( floor( position.y ) ).toVar();
+	const iz = int( floor( position.z ) ).toVar();
+	const seed = uint( 0xdeadbeef + ( 4 << 2 ) + 13 ).toVar();
+	const a = uint().toVar(), b = uint().toVar(), c = uint().toVar();
+	a.assign( b.assign( c.assign( seed ) ) );
+	a.addAssign( uint( ix ) );
+	b.addAssign( uint( iy ) );
+	c.addAssign( uint( iz ) );
+
+	const mixed = uvec3( mx_bjmix( a, b, c ) ).toVar();
+	const hash0 = mx_bjfinal( mixed.x, mixed.y, mixed.z );
+	const hash1 = mx_bjfinal( add( mixed.x, uint( 1 ) ), mixed.y, mixed.z );
+	const hash2 = mx_bjfinal( add( mixed.x, uint( 2 ) ), mixed.y, mixed.z );
+
+	return vec3(
+		mx_bits_to_01( hash0 ),
+		mx_bits_to_01( hash1 ),
+		mx_bits_to_01( hash2 )
+	);
+
+} );
+
+export const mx_fractal_noise_float_2d = /*@__PURE__*/ Fn( ( [ p_immutable, octaves_immutable, lacunarity_immutable, diminish_immutable ] ) => {
+
+	const diminish = float( diminish_immutable ).toVar();
+	const lacunarity = float( lacunarity_immutable ).toVar();
+	const octaves = int( octaves_immutable ).toVar();
+	const p = vec2( p_immutable ).toVar();
+	const result = float( 0.0 ).toVar();
+	const amplitude = float( 1.0 ).toVar();
+
+	Loop( octaves, () => {
+
+		result.addAssign( amplitude.mul( mx_perlin_noise_float( p ) ) );
+		amplitude.mulAssign( diminish );
+		p.mulAssign( lacunarity );
+
+	} );
+
+	return result;
+
+} ).setLayout( {
+	name: 'mx_fractal_noise_float_2d',
+	type: 'float',
+	inputs: [
+		{ name: 'p', type: 'vec2' },
+		{ name: 'octaves', type: 'int' },
+		{ name: 'lacunarity', type: 'float' },
+		{ name: 'diminish', type: 'float' }
+	]
+} );
+
 export const mx_fractal_noise_float = /*@__PURE__*/ Fn( ( [ p_immutable, octaves_immutable, lacunarity_immutable, diminish_immutable ] ) => {
 
 	const diminish = float( diminish_immutable ).toVar();
@@ -989,7 +1109,7 @@ export const mx_worley_distance_1 = /*@__PURE__*/ Fn( ( [ p_immutable, x_immutab
 	const y = int( y_immutable ).toVar();
 	const x = int( x_immutable ).toVar();
 	const p = vec3( p_immutable ).toVar();
-	const off = vec3( mx_cell_noise_vec3( vec3( x.add( xoff ), y.add( yoff ), z.add( zoff ) ) ) ).toVar();
+	const off = vec3( mx_cell_noise_vec3_3d( vec3( x.add( xoff ), y.add( yoff ), z.add( zoff ) ) ) ).toVar();
 	off.subAssign( 0.5 );
 	off.mulAssign( jitter );
 	off.addAssign( 0.5 );
@@ -1028,27 +1148,49 @@ export const mx_worley_distance_1 = /*@__PURE__*/ Fn( ( [ p_immutable, x_immutab
 
 export const mx_worley_distance = /*@__PURE__*/ overloadingFn( [ mx_worley_distance_0, mx_worley_distance_1 ] );
 
-export const mx_worley_noise_float_0 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
+const mx_perlin_noise_float_scaled = ( texcoord, amplitude = 1, pivot = 0 ) => mx_perlin_noise_float( texcoord ).mul( amplitude ).add( pivot );
 
-	const metric = int( metric_immutable ).toVar();
-	const jitter = float( jitter_immutable ).toVar();
-	const p = vec2( p_immutable ).toVar();
-	const X = int().toVar(), Y = int().toVar();
-	const localpos = vec2( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ) ).toVar();
+export const mx_worley_noise_float_3d = /*@__PURE__*/ Fn( ( [ positionInput, jitterInput, styleInput ] ) => {
+
+	const position = vec3( positionInput ).toVar();
+	const jitter = float( jitterInput ).toVar();
+	const style = int( styleInput ).toVar();
+	const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
+	const localpos = vec3( mx_floorfrac( position.x, X ), mx_floorfrac( position.y, Y ), mx_floorfrac( position.z, Z ) ).toVar();
 	const sqdist = float( 1e6 ).toVar();
+	const minpos = vec3( 0, 0, 0 ).toVar();
 
 	Loop( { start: - 1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
 
 		Loop( { start: - 1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
 
-			const dist = float( mx_worley_distance( localpos, x, y, X, Y, jitter, metric ) ).toVar();
-			sqdist.assign( min( sqdist, dist ) );
+			Loop( { start: - 1, end: int( 1 ), name: 'z', condition: '<=' }, ( { z } ) => {
+
+				const dist = float( mx_worley_distance( localpos, x, y, z, X, Y, Z, jitter, int( 0 ) ) ).toVar();
+				const off = vec3( mx_cell_noise_vec3_3d( vec3( X.add( x ), Y.add( y ), Z.add( z ) ) ) ).toVar();
+				off.subAssign( 0.5 );
+				off.mulAssign( jitter );
+				off.addAssign( 0.5 );
+				const cellpos = vec3( vec3( float( x ), float( y ), float( z ) ).add( off ).sub( localpos ) ).toVar();
+
+				If( dist.lessThan( sqdist ), () => {
+
+					sqdist.assign( dist );
+					minpos.assign( cellpos );
+
+				} );
+
+			} );
 
 		} );
 
 	} );
 
-	If( metric.equal( int( 0 ) ), () => {
+	If( style.equal( int( 1 ) ), () => {
+
+		sqdist.assign( mx_cell_noise_float( minpos.add( position ) ) );
+
+	} ).Else( () => {
 
 		sqdist.assign( sqrt( sqdist ) );
 
@@ -1056,16 +1198,58 @@ export const mx_worley_noise_float_0 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter
 
 	return sqdist;
 
-} ).setLayout( {
-	name: 'mx_worley_noise_float_0',
-	type: 'float',
-	inputs: [
-		{ name: 'p', type: 'vec2' },
-		{ name: 'jitter', type: 'float' },
-		{ name: 'metric', type: 'int' }
-	]
 } );
 
+export const mx_worley_noise_float_2d = /*@__PURE__*/ Fn( ( [ texcoordInput, jitterInput, styleInput ] ) => {
+
+	const texcoord = vec2( texcoordInput ).toVar();
+	const jitter = float( jitterInput ).toVar();
+	const style = int( styleInput ).toVar();
+	const floorPos = floor( texcoord ).toVar();
+	const localpos = vec2( fract( texcoord.x ), fract( texcoord.y ) ).toVar();
+	const sqdist = float( 1e6 ).toVar();
+	const minpos = vec2( 0, 0 ).toVar();
+
+	Loop( { start: - 1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
+
+		Loop( { start: - 1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
+
+			const cell = vec2( float( x ), float( y ) ).toVar();
+			const seed = vec2( cell.x.add( floorPos.x ), cell.y.add( floorPos.y ) ).toVar();
+			const off = vec2( mx_cell_noise_float( vec3( seed.x, seed.y, 0 ) ), mx_cell_noise_float( vec3( seed.x, seed.y, 1 ) ) ).toVar();
+			off.subAssign( 0.5 );
+			off.mulAssign( jitter );
+			off.addAssign( 0.5 );
+			const cellpos = vec2( cell.add( off ).sub( localpos ) ).toVar();
+			const dist = dot( cellpos, cellpos ).toVar();
+
+			If( dist.lessThan( sqdist ), () => {
+
+				sqdist.assign( dist );
+				minpos.assign( cellpos );
+
+			} );
+
+		} );
+
+	} );
+
+	If( style.equal( int( 1 ) ), () => {
+
+		sqdist.assign( mx_cell_noise_float( minpos.add( texcoord ) ) );
+
+	} ).Else( () => {
+
+		sqdist.assign( sqrt( sqdist ) );
+
+	} );
+
+	return sqdist;
+
+} );
+
+export const mx_worley_noise_float = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_float_2d, mx_worley_noise_float_3d ] );
+
 export const mx_worley_noise_vec2_0 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
 
 	const metric = int( metric_immutable ).toVar();
@@ -1168,14 +1352,14 @@ export const mx_worley_noise_vec3_0 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_
 	]
 } );
 
-export const mx_worley_noise_float_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
+export const mx_worley_noise_vec2_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
 
 	const metric = int( metric_immutable ).toVar();
 	const jitter = float( jitter_immutable ).toVar();
 	const p = vec3( p_immutable ).toVar();
 	const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
 	const localpos = vec3( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ), mx_floorfrac( p.z, Z ) ).toVar();
-	const sqdist = float( 1e6 ).toVar();
+	const sqdist = vec2( 1e6, 1e6 ).toVar();
 
 	Loop( { start: - 1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
 
@@ -1184,7 +1368,17 @@ export const mx_worley_noise_float_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter
 			Loop( { start: - 1, end: int( 1 ), name: 'z', condition: '<=' }, ( { z } ) => {
 
 				const dist = float( mx_worley_distance( localpos, x, y, z, X, Y, Z, jitter, metric ) ).toVar();
-				sqdist.assign( min( sqdist, dist ) );
+
+				If( dist.lessThan( sqdist.x ), () => {
+
+					sqdist.y.assign( sqdist.x );
+					sqdist.x.assign( dist );
+
+				} ).ElseIf( dist.lessThan( sqdist.y ), () => {
+
+					sqdist.y.assign( dist );
+
+				} );
 
 			} );
 
@@ -1201,8 +1395,8 @@ export const mx_worley_noise_float_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter
 	return sqdist;
 
 } ).setLayout( {
-	name: 'mx_worley_noise_float_1',
-	type: 'float',
+	name: 'mx_worley_noise_vec2_1',
+	type: 'vec2',
 	inputs: [
 		{ name: 'p', type: 'vec3' },
 		{ name: 'jitter', type: 'float' },
@@ -1210,16 +1404,16 @@ export const mx_worley_noise_float_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter
 	]
 } );
 
-export const mx_worley_noise_float = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_float_0, mx_worley_noise_float_1 ] );
+export const mx_worley_noise_vec2 = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_vec2_0, mx_worley_noise_vec2_1 ] );
 
-export const mx_worley_noise_vec2_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
+export const mx_worley_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
 
 	const metric = int( metric_immutable ).toVar();
 	const jitter = float( jitter_immutable ).toVar();
 	const p = vec3( p_immutable ).toVar();
 	const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
 	const localpos = vec3( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ), mx_floorfrac( p.z, Z ) ).toVar();
-	const sqdist = vec2( 1e6, 1e6 ).toVar();
+	const sqdist = vec3( 1e6, 1e6, 1e6 ).toVar();
 
 	Loop( { start: - 1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
 
@@ -1231,13 +1425,19 @@ export const mx_worley_noise_vec2_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_
 
 				If( dist.lessThan( sqdist.x ), () => {
 
+					sqdist.z.assign( sqdist.y );
 					sqdist.y.assign( sqdist.x );
 					sqdist.x.assign( dist );
 
 				} ).ElseIf( dist.lessThan( sqdist.y ), () => {
 
+					sqdist.z.assign( sqdist.y );
 					sqdist.y.assign( dist );
 
+				} ).ElseIf( dist.lessThan( sqdist.z ), () => {
+
+					sqdist.z.assign( dist );
+
 				} );
 
 			} );
@@ -1255,8 +1455,8 @@ export const mx_worley_noise_vec2_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_
 	return sqdist;
 
 } ).setLayout( {
-	name: 'mx_worley_noise_vec2_1',
-	type: 'vec2',
+	name: 'mx_worley_noise_vec3_1',
+	type: 'vec3',
 	inputs: [
 		{ name: 'p', type: 'vec3' },
 		{ name: 'jitter', type: 'float' },
@@ -1264,16 +1464,72 @@ export const mx_worley_noise_vec2_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_
 	]
 } );
 
-export const mx_worley_noise_vec2 = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_vec2_0, mx_worley_noise_vec2_1 ] );
+export const mx_worley_noise_vec3 = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_vec3_0, mx_worley_noise_vec3_1 ] );
 
-export const mx_worley_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, metric_immutable ] ) => {
+export const mx_worley_noise_vec3_style_0 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, style_immutable, metric_immutable ] ) => {
 
 	const metric = int( metric_immutable ).toVar();
+	const style = int( style_immutable ).toVar();
+	const jitter = float( jitter_immutable ).toVar();
+	const p = vec2( p_immutable ).toVar();
+	const X = int().toVar(), Y = int().toVar();
+	const localpos = vec2( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ) ).toVar();
+	const sqdist = float( 1e6 ).toVar();
+	const minpos = vec2( 0, 0 ).toVar();
+
+	Loop( { start: - 1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
+
+		Loop( { start: - 1, end: int( 1 ), name: 'y', condition: '<=' }, ( { y } ) => {
+
+			const dist = float( mx_worley_distance( localpos, x, y, X, Y, jitter, metric ) ).toVar();
+			const tmp = vec3( mx_cell_noise_vec3( vec2( X.add( x ), Y.add( y ) ) ) ).toVar();
+			const off = vec2( tmp.x, tmp.y ).toVar();
+			off.subAssign( 0.5 );
+			off.mulAssign( jitter );
+			off.addAssign( 0.5 );
+			const cellpos = vec2( vec2( float( x ), float( y ) ).add( off ).sub( localpos ) ).toVar();
+
+			If( dist.lessThan( sqdist ), () => {
+
+				sqdist.assign( dist );
+				minpos.assign( cellpos );
+
+			} );
+
+		} );
+
+	} );
+
+	const result = vec3( mx_worley_noise_vec3( p, jitter, metric ) ).toVar();
+	If( style.equal( int( 1 ) ), () => {
+
+		result.assign( mx_cell_noise_vec3( minpos.add( p ) ) );
+
+	} );
+
+	return result;
+
+} ).setLayout( {
+	name: 'mx_worley_noise_vec3_style_0',
+	type: 'vec3',
+	inputs: [
+		{ name: 'p', type: 'vec2' },
+		{ name: 'jitter', type: 'float' },
+		{ name: 'style', type: 'int' },
+		{ name: 'metric', type: 'int' }
+	]
+} );
+
+export const mx_worley_noise_vec3_style_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_immutable, style_immutable, metric_immutable ] ) => {
+
+	const metric = int( metric_immutable ).toVar();
+	const style = int( style_immutable ).toVar();
 	const jitter = float( jitter_immutable ).toVar();
 	const p = vec3( p_immutable ).toVar();
 	const X = int().toVar(), Y = int().toVar(), Z = int().toVar();
 	const localpos = vec3( mx_floorfrac( p.x, X ), mx_floorfrac( p.y, Y ), mx_floorfrac( p.z, Z ) ).toVar();
-	const sqdist = vec3( 1e6, 1e6, 1e6 ).toVar();
+	const sqdist = float( 1e6 ).toVar();
+	const minpos = vec3( 0, 0, 0 ).toVar();
 
 	Loop( { start: - 1, end: int( 1 ), name: 'x', condition: '<=' }, ( { x } ) => {
 
@@ -1282,21 +1538,16 @@ export const mx_worley_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_
 			Loop( { start: - 1, end: int( 1 ), name: 'z', condition: '<=' }, ( { z } ) => {
 
 				const dist = float( mx_worley_distance( localpos, x, y, z, X, Y, Z, jitter, metric ) ).toVar();
+				const off = vec3( mx_cell_noise_vec3_3d( vec3( X.add( x ), Y.add( y ), Z.add( z ) ) ) ).toVar();
+				off.subAssign( 0.5 );
+				off.mulAssign( jitter );
+				off.addAssign( 0.5 );
+				const cellpos = vec3( vec3( float( x ), float( y ), float( z ) ).add( off ).sub( localpos ) ).toVar();
 
-				If( dist.lessThan( sqdist.x ), () => {
-
-					sqdist.z.assign( sqdist.y );
-					sqdist.y.assign( sqdist.x );
-					sqdist.x.assign( dist );
+				If( dist.lessThan( sqdist ), () => {
 
-				} ).ElseIf( dist.lessThan( sqdist.y ), () => {
-
-					sqdist.z.assign( sqdist.y );
-					sqdist.y.assign( dist );
-
-				} ).ElseIf( dist.lessThan( sqdist.z ), () => {
-
-					sqdist.z.assign( dist );
+					sqdist.assign( dist );
+					minpos.assign( cellpos );
 
 				} );
 
@@ -1306,186 +1557,164 @@ export const mx_worley_noise_vec3_1 = /*@__PURE__*/ Fn( ( [ p_immutable, jitter_
 
 	} );
 
-	If( metric.equal( int( 0 ) ), () => {
+	const result = vec3( mx_worley_noise_vec3( p, jitter, metric ) ).toVar();
+	If( style.equal( int( 1 ) ), () => {
 
-		sqdist.assign( sqrt( sqdist ) );
+		result.assign( mx_cell_noise_vec3_3d( minpos.add( p ) ) );
 
 	} );
 
-	return sqdist;
+	return result;
 
 } ).setLayout( {
-	name: 'mx_worley_noise_vec3_1',
+	name: 'mx_worley_noise_vec3_style_1',
 	type: 'vec3',
 	inputs: [
 		{ name: 'p', type: 'vec3' },
 		{ name: 'jitter', type: 'float' },
+		{ name: 'style', type: 'int' },
 		{ name: 'metric', type: 'int' }
 	]
 } );
 
-export const mx_worley_noise_vec3 = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_vec3_0, mx_worley_noise_vec3_1 ] );
+export const mx_worley_noise_vec3_style = /*@__PURE__*/ overloadingFn( [ mx_worley_noise_vec3_style_0, mx_worley_noise_vec3_style_1 ] );
 
 // Unified Noise 2D
 export const mx_unifiednoise2d = /*@__PURE__*/ Fn( ( [
-	noiseType_immutable, texcoord_immutable, freq_immutable, offset_immutable,
-	jitter_immutable, outmin_immutable, outmax_immutable, clampoutput_immutable,
-	octaves_immutable, lacunarity_immutable, diminish_immutable
+	noiseTypeInput,
+	texcoordInput,
+	freqInput,
+	offsetInput,
+	jitterInput,
+	outminInput,
+	outmaxInput,
+	clampoutputInput,
+	octavesInput,
+	lacunarityInput,
+	diminishInput,
+	styleInput
 ] ) => {
 
-	const noiseType = int( noiseType_immutable ).toVar();
-	const texcoord = vec2( texcoord_immutable ).toVar();
-	const freq = vec2( freq_immutable ).toVar();
-	const offset = vec2( offset_immutable ).toVar();
-	const jitter = float( jitter_immutable ).toVar();
-	const outmin = float( outmin_immutable ).toVar();
-	const outmax = float( outmax_immutable ).toVar();
-	const clampoutput = bool( clampoutput_immutable ).toVar();
-	const octaves = int( octaves_immutable ).toVar();
-	const lacunarity = float( lacunarity_immutable ).toVar();
-	const diminish = float( diminish_immutable ).toVar();
-
-	// Compute input position
-	const p = texcoord.mul( freq ).add( offset );
-
-	const result = float( 0.0 ).toVar();
+	const noiseType = int( noiseTypeInput ).toVar();
+	const texcoord = vec2( texcoordInput ).toVar();
+	const freq = vec2( freqInput ).toVar();
+	const offset = vec2( offsetInput ).toVar();
+	const jitter = float( jitterInput ).toVar();
+	const outmin = float( outminInput ).toVar();
+	const outmax = float( outmaxInput ).toVar();
+	const clampoutput = float( clampoutputInput ).toVar();
+	const octaves = int( octavesInput ).toVar();
+	const lacunarity = float( lacunarityInput ).toVar();
+	const diminish = float( diminishInput ).toVar();
+	const style = int( styleInput ).toVar();
+
+	const applyFreq = mul( texcoord, freq ).toVar();
+	const applyOffset = add( applyFreq, offset ).toVar();
+	const cellJitterMult = mul( sub( jitter, 1 ), 90000 ).toVar();
+	const applyCellJitter = mx_rotate2d( applyOffset, cellJitterMult ).toVar();
+	const fractalInput = vec3( applyOffset.x, applyOffset.y, cellJitterMult ).toVar();
+	const result = float( 0 ).toVar();
 
-	// Perlin
 	If( noiseType.equal( int( 0 ) ), () => {
 
-		result.assign( mx_perlin_noise_vec3( p ) );
+		result.assign( mx_perlin_noise_float_scaled( applyCellJitter, 0.5, 0.5 ) );
 
 	} );
-
-	// Cell
 	If( noiseType.equal( int( 1 ) ), () => {
 
-		result.assign( mx_cell_noise_vec3( p ) );
+		result.assign( mx_cell_noise_float( applyCellJitter ) );
 
 	} );
-
-	// Worley (metric=0 = euclidean)
 	If( noiseType.equal( int( 2 ) ), () => {
 
-		result.assign( mx_worley_noise_vec3( p, jitter, int( 0 ) ) );
+		result.assign( mx_worley_noise_float_2d( applyOffset, jitter, style ) );
 
 	} );
-
-	// Fractal (use vec3(p, 0.0) for 2D input)
 	If( noiseType.equal( int( 3 ) ), () => {
 
-		result.assign( mx_fractal_noise_vec3( vec3( p, 0.0 ), octaves, lacunarity, diminish ) );
+		result.assign( mx_fractal_noise_float( fractalInput, octaves, lacunarity, diminish ) );
 
 	} );
 
-	// Remap output to [outmin, outmax]
-	result.assign( result.mul( outmax.sub( outmin ) ).add( outmin ) );
+	const ranged = add( outmin, mul( result, sub( outmax, outmin ) ) ).toVar();
+	const clamped = clamp( ranged, outmin, outmax ).toVar();
+	const output = ranged.toVar();
 
-	// Clamp if requested
-	If( clampoutput, () => {
+	If( clampoutput.equal( float( 1 ) ), () => {
 
-		result.assign( clamp( result, outmin, outmax ) );
+		output.assign( clamped );
 
 	} );
 
-	return result;
+	return output;
 
-} ).setLayout( {
-	name: 'mx_unifiednoise2d',
-	type: 'float',
-	inputs: [
-		{ name: 'noiseType', type: 'int' },
-		{ name: 'texcoord', type: 'vec2' },
-		{ name: 'freq', type: 'vec2' },
-		{ name: 'offset', type: 'vec2' },
-		{ name: 'jitter', type: 'float' },
-		{ name: 'outmin', type: 'float' },
-		{ name: 'outmax', type: 'float' },
-		{ name: 'clampoutput', type: 'bool' },
-		{ name: 'octaves', type: 'int' },
-		{ name: 'lacunarity', type: 'float' },
-		{ name: 'diminish', type: 'float' }
-	]
 } );
 
 // Unified Noise 3D
 export const mx_unifiednoise3d = /*@__PURE__*/ Fn( ( [
-	noiseType_immutable, position_immutable, freq_immutable, offset_immutable,
-	jitter_immutable, outmin_immutable, outmax_immutable, clampoutput_immutable,
-	octaves_immutable, lacunarity_immutable, diminish_immutable
+	noiseTypeInput,
+	positionInput,
+	freqInput,
+	offsetInput,
+	jitterInput,
+	outminInput,
+	outmaxInput,
+	clampoutputInput,
+	octavesInput,
+	lacunarityInput,
+	diminishInput,
+	styleInput
 ] ) => {
 
-	const noiseType = int( noiseType_immutable ).toVar();
-	const position = vec3( position_immutable ).toVar();
-	const freq = vec3( freq_immutable ).toVar();
-	const offset = vec3( offset_immutable ).toVar();
-	const jitter = float( jitter_immutable ).toVar();
-	const outmin = float( outmin_immutable ).toVar();
-	const outmax = float( outmax_immutable ).toVar();
-	const clampoutput = bool( clampoutput_immutable ).toVar();
-	const octaves = int( octaves_immutable ).toVar();
-	const lacunarity = float( lacunarity_immutable ).toVar();
-	const diminish = float( diminish_immutable ).toVar();
-
-	// Compute input position
-	const p = position.mul( freq ).add( offset );
-
-	const result = float( 0.0 ).toVar();
-
-	// Perlin
-	If( noiseType.equal( int( 0 ) ), () => {
-
-		result.assign( mx_perlin_noise_vec3( p ) );
-
-	} );
+	const noiseType = int( noiseTypeInput ).toVar();
+	const position = vec3( positionInput ).toVar();
+	const freq = vec3( freqInput ).toVar();
+	const offset = vec3( offsetInput ).toVar();
+	const jitter = float( jitterInput ).toVar();
+	const outmin = float( outminInput ).toVar();
+	const outmax = float( outmaxInput ).toVar();
+	const clampoutput = float( clampoutputInput ).toVar();
+	const octaves = int( octavesInput ).toVar();
+	const lacunarity = float( lacunarityInput ).toVar();
+	const diminish = float( diminishInput ).toVar();
+	const style = int( styleInput ).toVar();
+
+	const applyFreq = mul( position, freq ).toVar();
+	const applyOffset = add( applyFreq, offset ).toVar();
+	const cellJitterMult = mul( sub( jitter, 1 ), 90000 ).toVar();
+	const applyCellJitter = mx_rotate3d( applyOffset, cellJitterMult, vec3( 0.1, 1, 0 ) ).toVar();
+	const perlin = mx_perlin_noise_float_scaled( applyCellJitter, 0.5, 0.5 );
+	const cell = mx_cell_noise_float( applyCellJitter );
+	const worley = mx_worley_noise_float_3d( applyOffset, jitter, style );
+	const fractal = mx_fractal_noise_float( applyCellJitter, octaves, lacunarity, diminish );
+	const result = perlin.toVar();
 
-	// Cell
 	If( noiseType.equal( int( 1 ) ), () => {
 
-		result.assign( mx_cell_noise_vec3( p ) );
+		result.assign( cell );
 
 	} );
-
-	// Worley (metric=0 = euclidean)
 	If( noiseType.equal( int( 2 ) ), () => {
 
-		result.assign( mx_worley_noise_vec3( p, jitter, int( 0 ) ) );
+		result.assign( worley );
 
 	} );
-
-	// Fractal
 	If( noiseType.equal( int( 3 ) ), () => {
 
-		result.assign( mx_fractal_noise_vec3( p, octaves, lacunarity, diminish ) );
+		result.assign( fractal );
 
 	} );
 
-	// Remap output to [outmin, outmax]
-	result.assign( result.mul( outmax.sub( outmin ) ).add( outmin ) );
+	const ranged = add( outmin, mul( result, sub( outmax, outmin ) ) ).toVar();
+	const clamped = clamp( ranged, outmin, outmax ).toVar();
+	const output = ranged.toVar();
 
-	// Clamp if requested
-	If( clampoutput, () => {
+	If( clampoutput.equal( float( 1 ) ), () => {
 
-		result.assign( clamp( result, outmin, outmax ) );
+		output.assign( clamped );
 
 	} );
 
-	return result;
+	return output;
 
-} ).setLayout( {
-	name: 'mx_unifiednoise3d',
-	type: 'float',
-	inputs: [
-		{ name: 'noiseType', type: 'int' },
-		{ name: 'position', type: 'vec3' },
-		{ name: 'freq', type: 'vec3' },
-		{ name: 'offset', type: 'vec3' },
-		{ name: 'jitter', type: 'float' },
-		{ name: 'outmin', type: 'float' },
-		{ name: 'outmax', type: 'float' },
-		{ name: 'clampoutput', type: 'bool' },
-		{ name: 'octaves', type: 'int' },
-		{ name: 'lacunarity', type: 'float' },
-		{ name: 'diminish', type: 'float' }
-	]
 } );

Некоторые файлы не были показаны из-за большого количества измененных файлов

粤ICP备19079148号