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