UNPKG

three

Version:

JavaScript 3D library

1,351 lines (866 loc) • 33 kB
import { Texture, ImageLoader, ImageBitmapLoader, LoaderUtils, Matrix3, Matrix4, MeshBasicNodeMaterial, MeshPhysicalNodeMaterial, } from 'three/webgpu'; import { float, texture, int, bool, sub, vec2, vec3, vec4, color, uv, mat3, mat4, element, mx_transform_uv, mx_srgb_texture_to_lin_rec709, positionLocal, positionWorld, normalLocal, normalWorld, tangentLocal, tangentWorld, bitangentLocal, bitangentWorld, } from 'three/tsl'; import { MaterialXLogCodes } from './MaterialXLog.js'; import { createMaterialXCompileRegistry, compileNodeFromRegistry } from './compile/MaterialXCompileRegistry.js'; import { parseMaterialXNodeTree, parseMaterialXText } from './parse/MaterialXParser.js'; import { getSurfaceMapper } from './MaterialXSurfaceMappings.js'; import { MtlXLibrary } from './MaterialXNodeLibrary.js'; import { resolveNodeDef } from './MaterialXNodeDefs.js'; import { mxHextileCoord, mxHextileComputeBlendWeights } from './MaterialXHextile.js'; import { resolveTextureAddressMode, TEXTURE_ADDRESS_MODE_WRAPPING, 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 NODE_CLASS_BY_TYPE = { integer: int, float, vector2: vec2, vector3: vec3, vector4: vec4, color4: vec4, color3: color, boolean: null, matrix33: mat3, matrix44: mat4, }; // Geometric properties a nodedef input can default to via `defaultgeomprop`. const GEOMPROP_NODES = { Pobject: positionLocal, Pworld: positionWorld, Nobject: normalLocal, Nworld: normalWorld, Tobject: tangentLocal, Tworld: tangentWorld, Bobject: bitangentLocal, Bworld: bitangentWorld, Vworld: MtlXLibrary.viewdirection.nodeFunc( 'world' ), }; function parseValueVector( value ) { const vector = []; for ( const val of value.split( /[,|\s]/ ) ) { if ( val !== '' ) vector.push( Number( val.trim() ) ); } return vector; } function createMatrixNode( size, vector ) { 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 ); } // Empty strings are valid string defaults, but an empty filename means "no texture". function hasDefaultValue( input ) { if ( input.value === undefined ) return false; return input.value !== '' || input.type === 'string'; } // string values stay strings so the compiler can read e.g. space names function createValueNode( type, value ) { const trimmed = value.trim(); if ( type === 'boolean' ) { const normalized = trimmed.toLowerCase(); return bool( normalized === 'true' || normalized === '1' ); } if ( type === 'matrix33' ) return createMatrixNode( 3, parseValueVector( trimmed ) ) || mat3( ...IDENTITY_MAT3_VALUES ); if ( type === 'matrix44' ) return createMatrixNode( 4, parseValueVector( trimmed ) ) || mat4( ...IDENTITY_MAT4_VALUES ); if ( type === 'string' || type === 'filename' ) return trimmed; const nodeClass = NODE_CLASS_BY_TYPE[ type ]; return nodeClass ? nodeClass( ...parseValueVector( trimmed ) ) : float( 0 ); } const OUTPUT_CHANNELS = { outx: 0, outr: 0, outy: 1, outg: 1, outz: 2, outb: 2, outw: 3, outa: 3, }; function mxFlipUvY( uvNode ) { return vec2( element( uvNode, 0 ), sub( 1, element( uvNode, 1 ) ) ); } function mxIdentityUv( uvNode ) { return uvNode; } function getBottomLeftUvSpaceHelpers( uvSpace ) { const helper = uvSpace === 'top-left' ? mxFlipUvY : mxIdentityUv; return { mxToBottomLeftUvSpace: helper, mxFromBottomLeftUvSpace: helper, }; } function normalizeUvSpace( uvSpace ) { if ( uvSpace === undefined || uvSpace === null ) return 'bottom-left'; if ( uvSpace === 'bottom-left' || uvSpace === 'top-left' ) return uvSpace; throw new Error( `Unsupported MaterialX uvSpace "${uvSpace}". Expected "bottom-left" or "top-left".` ); } function isSvgUri( uri ) { if ( typeof uri !== 'string' ) return false; return /\.svg(?:$|[?#])/i.test( uri ); } function 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 = []; this._nodeDef = undefined; } 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; } getReferencedNode() { if ( this.nodeGraph !== null ) { const graph = this.materialX.getMaterialXNode( this.nodeGraph ); return graph ? this.output !== null ? graph.getChildByName( this.output ) || null : graph.children.find( ( child ) => child.element === 'output' ) || null : null; } return this.hasReference ? this.materialX.getMaterialXNode( this.referencePath ) || null : null; } get isConst() { return this.element === 'input' && this.value !== null && this.type !== 'filename'; } /** * The stdlib or document nodedef this node instance resolves to, or `null` when the category is unknown. * * @type {?Object} */ get nodeDef() { if ( this._nodeDef === undefined ) this._nodeDef = resolveNodeDef( this ); return this._nodeDef; } hasDeclaredOutput( name ) { return this.nodeDef !== null && name in this.nodeDef.outputs; } // The declared type of a nodedef input, or `null` when the nodedef does not declare it. getNodeDefInputType( name ) { const input = this.nodeDef ? this.nodeDef.inputs[ name ] : undefined; return input ? input.type : null; } // `undefined` when the nodedef declares no default (e.g. filenames) getDefaultInputNode( name ) { const input = this.nodeDef ? this.nodeDef.inputs[ name ] : undefined; if ( ! input ) return undefined; if ( input.defaultgeomprop ) return this.materialX.compileContext.getGeomPropNode( input.defaultgeomprop ); if ( ! hasDefaultValue( input ) ) return undefined; return createValueNode( input.type, input.value ); } // geometric defaults are left out so surface mappers can tell an authored normal from the default getDefaultInputNodes() { const nodes = {}; if ( this.nodeDef === null ) return nodes; for ( const [ name, input ] of Object.entries( this.nodeDef.inputs ) ) { if ( input.defaultgeomprop || ! hasDefaultValue( input ) ) continue; nodes[ name ] = createValueNode( input.type, input.value ); } return nodes; } getColorSpaceNode() { const csSource = this.getValueInput().getAttribute( 'colorspace' ) || this.getAttribute( 'colorspace' ); const csTarget = this.getRoot().getAttribute( 'colorspace' ); if ( ! csSource || ! csTarget ) return null; const nodeName = `mx_${csSource}_to_${csTarget}`; return colorSpaceLib[ nodeName ] || null; } logUnsupportedNode() { this.materialX.log.add( MaterialXLogCodes.UNSUPPORTED_NODE, `Unsupported MaterialX node category "${this.element}" on "${this.name}".`, this.name, ); } getTextureAddressMode( inputName ) { // e.g. tiledimage, which samples with the image node's default addressing. if ( this.declaresInput( inputName ) === false ) return 'periodic'; const rawMode = this.getNodeByName( inputName ); const mode = resolveTextureAddressMode( rawMode ); if ( mode ) return mode; this.materialX.log.add( MaterialXLogCodes.INVALID_VALUE, `Unsupported texture address mode "${rawMode}" on input "${inputName}". Expected constant, clamp, periodic, or mirror.`, this.name, ); return 'periodic'; } getTextureAddressModes() { return { u: this.getTextureAddressMode( 'uaddressmode' ), v: this.getTextureAddressMode( 'vaddressmode' ), }; } // Follow an interfacename binding to its source input. getValueInput() { if ( this.interfaceName === null ) return this; const boundInput = this.materialX.getInterfaceInput( this ); return boundInput !== null ? boundInput : this; } getTexture() { const valueInput = this.getValueInput(); if ( valueInput.getValue() === '' ) return null; const filePrefix = valueInput.getRecursiveAttribute( 'fileprefix' ) || ''; const sourceURI = filePrefix + valueInput.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; let textureURL = resolvedURI; if ( resolvedURI && ! svgTexture ) { const handler = this.materialX.manager.getHandler( resolvedURI ); if ( handler !== null ) { // The built-in loaders carry the document path; handlers do not. loader = handler; textureURL = LoaderUtils.resolveURL( resolvedURI, this.materialX.path ); } } const textureNode = texture( new Texture() ); textureNode.value.wrapS = TEXTURE_ADDRESS_MODE_WRAPPING[ addressModes.u ]; textureNode.value.wrapT = TEXTURE_ADDRESS_MODE_WRAPPING[ addressModes.v ]; textureNode.value.flipY = false; this.materialX.textureCache.set( textureCacheKey, textureNode ); const nodeName = this.name; const materialX = this.materialX; materialX.pendingResources.push( new Promise( ( resolveLoad ) => { loader.load( textureURL, ( imageData ) => { if ( imageData.isTexture ) { // Clone so the wrapping and orientation set below don't modify the handler's texture. textureNode.value = imageData.clone(); textureNode.value.wrapS = TEXTURE_ADDRESS_MODE_WRAPPING[ addressModes.u ]; textureNode.value.wrapT = TEXTURE_ADDRESS_MODE_WRAPPING[ addressModes.v ]; // MaterialXLoader keeps textures top-first. Loaders such as EXRLoader store rows // bottom-first (flipY = false), so invert the loader's orientation to match. // flipY has no meaning for compressed textures. if ( imageData.isCompressedTexture !== true ) textureNode.value.flipY = ! imageData.flipY; } else { textureNode.value.image = imageData; } textureNode.value.needsUpdate = true; resolveLoad(); }, undefined, () => { materialX.log.add( MaterialXLogCodes.TEXTURE_LOAD_FAILED, `Failed to load texture "${resolvedURI}".`, nodeName, ); resolveLoad(); } ); } ) ); return textureNode; } getClassFromType( type ) { return NODE_CLASS_BY_TYPE[ type ] || null; } toBooleanNode( node ) { return toBooleanNode( node ); } getNode( out = null ) { // A named output must not replace the cached default output. const cacheDefaultOutput = out === null; const cachedNode = this.materialX.getCachedNode( this ); if ( cachedNode !== null && cacheDefaultOutput ) return cachedNode; let node; let implemented = false; // A connection that names no output reads the first one, as in MaterialX. if ( this.element === 'separate2' || this.element === 'separate3' || this.element === 'separate4' ) { const inNode = this.getNodeByName( 'in' ); return element( inNode, getOutputChannel( out ) ); } const type = this.type; // Channel suffixes (outr, outy, ...) extract a component unless the nodedef declares an output of that name. const channelRequested = this.element !== 'input' && isChannelOutput( out ) && this.hasDeclaredOutput( out ) === false; if ( this.isConst ) { node = createValueNode( type, this.getValue() ); } 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 interfaceNode = this.interfaceName !== null ? this.materialX.getInterfaceNode( this ) : null; const referenceNode = interfaceNode === null ? this.materialX.getMaterialXNode( this.referencePath ) : null; if ( interfaceNode !== null ) { node = interfaceNode; } else if ( referenceNode ) { node = referenceNode.getNode( requestedOutput ); } else { this.materialX.log.add( MaterialXLogCodes.MISSING_REFERENCE, `Missing MaterialX reference "${this.referencePath}" from "${this.name}".`, this.name, ); node = float( 0 ); } } else if ( this.element === 'input' && this.getAttribute( 'defaultgeomprop' ) !== null ) { // An unconnected interface input takes its value from its declared geometric property. node = this.materialX.compileContext.getGeomPropNode( this.getAttribute( 'defaultgeomprop' ) ); } else { const resolvedNode = this.materialX.nodeResolver !== null ? this.materialX.nodeResolver( this, out ) : null; implemented = ( resolvedNode === null || resolvedNode === undefined ) && this.materialX.getScope( this ) !== null; node = resolvedNode !== null && resolvedNode !== undefined ? resolvedNode : implemented ? this.materialX.compileImplementation( this, channelRequested ? null : out ) : compileNodeFromRegistry( this, out, this.materialX.compileContext ); } if ( node === null || node === undefined ) { this.logUnsupportedNode(); node = float( 0 ); } if ( channelRequested ) { node = element( node, getOutputChannel( out ) ); } const resolvedType = channelRequested ? 'float' : type; if ( resolvedType === 'boolean' ) { node = this.toBooleanNode( node ); } else if ( resolvedType === 'string' ) { // String-typed inputs (for example transform* fromspace/tospace) are // valid scalar parameters and should pass through without numeric casting. node = typeof node === 'string' ? node : this.getValue(); } else { const nodeToTypeClass = this.getClassFromType( resolvedType ); if ( nodeToTypeClass !== null ) { node = nodeToTypeClass( node ); } else if ( resolvedType !== null && resolvedType !== undefined && resolvedType !== 'multioutput' ) { this.materialX.log.add( MaterialXLogCodes.INVALID_VALUE, `Unexpected type "${resolvedType}" on node "${this.name}".`, this.name, ); node = float( 0 ); } } if ( node && typeof node === 'object' ) { node.name = this.name; } if ( cacheDefaultOutput ) this.materialX.setCachedNode( this, 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; } getInputTypes() { const types = {}; for ( const input of this.children ) { types[ input.name ] = input.type; } return types; } // nodes without a nodedef (e.g. from a host node resolver) accept any input declaresInput( name ) { return this.getChildByName( name ) !== undefined || this.nodeDef === null || name in this.nodeDef.inputs; } getNodeByName( name ) { const child = this.getChildByName( name ); if ( child ) return child.getNode( child.output ); if ( this.declaresInput( name ) === false ) { this.materialX.log.add( MaterialXLogCodes.UNKNOWN_INPUT, `"${this.element}" reads input "${name}", which nodedef "${this.nodeDef.name}" does not declare. Using fallback 0.`, this.name, ); return float( 0 ); } return this.getDefaultInputNode( name ); } 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() { return parseValueVector( this.getValue() ); } 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, out = null ) { if ( this.element === 'input' || this.element === 'output' ) { const shader = this.getReferencedNode(); if ( shader !== null ) shader.setMaterial( material, this.nodeGraph !== null ? null : this.output || out ); return; } if ( this.materialX.setImplementationMaterial( this, material, out ) ) return; const mapper = getSurfaceMapper( this.element ); if ( mapper ) { const authored = this.getNodes(); mapper.apply( material, { ...this.getDefaultInputNodes(), ...authored }, this.materialX.log, this.name, authored, this.getInputTypes() ); } else { this.logUnsupportedNode(); } } toNodeGraphMaterial() { 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 ) { const visited = new Set(); let shader = nodeX || null; let out = null; while ( shader !== null && ( shader.element === 'input' || shader.element === 'output' ) ) { if ( visited.has( shader ) ) return null; visited.add( shader ); out = shader.nodeGraph !== null ? null : shader.output; shader = shader.getReferencedNode(); } return shader !== null ? this.materialX.resolveImplementationShader( shader, out ) : null; } toSurfaceMaterial() { const surfaceShader = this.resolveSurfaceShaderNode( this.getChildByName( 'surfaceshader' ) ); const material = surfaceShader?.nodeDef?.name === 'ND_surface_unlit' ? new MeshBasicNodeMaterial() : new MeshPhysicalNodeMaterial(); material.name = this.name; for ( const nodeX of this.children ) { if ( nodeX.name === 'backsurfaceshader' ) { // MaterialX supports separate back-surface shaders. Their use in practice // is unclear; translating them per face is left for future work. if ( nodeX.hasReference || nodeX.nodeGraph !== null ) { this.materialX.log.add( MaterialXLogCodes.IGNORED_SURFACE_INPUT, 'surfacematerial input "backsurfaceshader" is currently ignored in MaterialX translation.', this.name, ); } continue; } const shaderProperties = this.resolveSurfaceShaderNode( nodeX ); if ( shaderProperties === null ) { this.materialX.log.add( MaterialXLogCodes.MISSING_REFERENCE, `Missing MaterialX reference "${nodeX.referencePath || nodeX.nodeName || '(unknown)'}" from "${nodeX.name}".`, nodeX.name, ); continue; } nodeX.setMaterial( material ); } return material; } toMaterials( materialName = null ) { const materials = {}; const surfaceMaterials = this.children.filter( ( nodeX ) => nodeX.element === 'surfacematerial' ); let selectedSurfaceMaterials = surfaceMaterials; if ( materialName ) { selectedSurfaceMaterials = surfaceMaterials.filter( ( nodeX ) => nodeX.name === materialName ); if ( selectedSurfaceMaterials.length === 0 ) { this.materialX.log.add( MaterialXLogCodes.MISSING_MATERIAL, `Could not find surfacematerial named "${materialName}".`, ); } } for ( const nodeX of selectedSurfaceMaterials ) { const material = nodeX.toSurfaceMaterial(); materials[ material.name ] = material; } if ( Object.keys( materials ).length === 0 ) { for ( const nodeX of this.children ) { // Only standalone nodegraphs should create preview materials. if ( nodeX.element === 'nodegraph' && nodeX.getAttribute( 'nodedef' ) === null && this.materialX.implementationGraphNodes.has( nodeX ) === false ) { const material = nodeX.toNodeGraphMaterial(); materials[ material.name ] = material; } } } return materials; } add( materialXNode ) { materialXNode.parent = this; this.children.push( materialXNode ); } } class MaterialXDocument { constructor( manager, path, log, archiveResolver = null, uvSpace = 'bottom-left' ) { this.manager = manager; this.path = path; this.log = log; this.archiveResolver = archiveResolver; this.uvSpace = normalizeUvSpace( uvSpace ); this.nodesXLib = new Map(); this.imageLoader = new ImageLoader( manager ); this.imageLoader.setPath( path ); this.textureLoader = new ImageBitmapLoader( manager ); this.textureLoader.setOptions( { imageOrientation: 'none' } ); this.textureLoader.setPath( path ); this.textureCache = new Map(); this.pendingResources = []; this.nodeResolver = null; this.documentNodeDefs = null; this.implementationGraphs = new Map(); this.implementationGraphNodes = new Set(); this.scope = null; this.rootScopes = new Map(); const bottomLeftUvSpaceHelpers = getBottomLeftUvSpaceHelpers( this.uvSpace ); this.compileContext = { compileRegistry: COMPILE_REGISTRY, nodeLibrary: MtlXLibrary, ...bottomLeftUvSpaceHelpers, getTexcoordNode: ( index = 0 ) => bottomLeftUvSpaceHelpers.mxToBottomLeftUvSpace( uv( index ) ), getGeomPropNode: ( name ) => { const uvMatch = /^UV(\d+)$/.exec( name ); if ( uvMatch ) return bottomLeftUvSpaceHelpers.mxToBottomLeftUvSpace( uv( parseInt( uvMatch[ 1 ], 10 ) ) ); return GEOMPROP_NODES[ name ]; }, 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; } indexImplementations( rootNode ) { for ( const nodeX of rootNode.children ) { const nodeDefName = nodeX.getAttribute( 'nodedef' ); const graphName = nodeX.element === 'implementation' ? nodeX.getAttribute( 'nodegraph' ) : null; const graph = nodeX.element === 'nodegraph' && nodeDefName !== null ? nodeX : graphName !== null ? this.getMaterialXNode( graphName ) : undefined; if ( graph === undefined ) continue; this.implementationGraphNodes.add( graph ); if ( this.implementationGraphs.has( nodeDefName ) === false ) this.implementationGraphs.set( nodeDefName, graph ); } } // Each custom node instance needs its own inputs and cached TSL nodes. getScope( materialXNode ) { const nodeDef = materialXNode.nodeDef; const graph = nodeDef !== null ? this.implementationGraphs.get( nodeDef.name ) : undefined; if ( graph === undefined ) return null; for ( let parent = this.scope; parent !== null; parent = parent.parent ) { if ( parent.graph === graph ) { this.log.add( MaterialXLogCodes.INVALID_VALUE, `The nodegraph of "${ materialXNode.element }" uses the node it implements.`, materialXNode.name, ); return null; } } const scopes = this.scope !== null ? this.scope.childScopes : this.rootScopes; let scope = scopes.get( materialXNode ); if ( scope === undefined ) { scope = { instance: materialXNode, nodeDef, graph, parent: this.scope, nodes: new Map(), childScopes: new Map() }; scopes.set( materialXNode, scope ); } return scope; } withScope( scope, callback ) { const previous = this.scope; this.scope = scope; try { return callback(); } finally { this.scope = previous; } } isInScope( materialXNode ) { if ( this.scope === null ) return false; for ( let parent = materialXNode.parent; parent !== null; parent = parent.parent ) { if ( parent === this.scope.graph ) return true; } return false; } getCachedNode( materialXNode ) { if ( this.isInScope( materialXNode ) ) return this.scope.nodes.get( materialXNode ) ?? null; return materialXNode.node; } setCachedNode( materialXNode, node ) { if ( this.isInScope( materialXNode ) ) this.scope.nodes.set( materialXNode, node ); else materialXNode.node = node; } getGraphOutput( scope, out ) { // An omitted selector uses the first output declared by the nodedef. const name = out === null ? Object.keys( scope.nodeDef.outputs )[ 0 ] : out; return scope.graph.children.find( ( child ) => child.element === 'output' && child.name === name ) || null; } compileImplementation( materialXNode, out ) { const scope = this.getScope( materialXNode ); if ( scope === null ) return null; const output = this.getGraphOutput( scope, out ); if ( output === null ) { this.log.add( MaterialXLogCodes.MISSING_REFERENCE, `Missing output "${ out || 'out' }" in the nodegraph of "${ materialXNode.name }".`, materialXNode.name, ); return float( 0 ); } // Use the output selector stored on the graph output. return this.withScope( scope, () => output.getNode() ); } setImplementationMaterial( materialXNode, material, out ) { const scope = this.getScope( materialXNode ); if ( scope === null ) return false; const output = this.getGraphOutput( scope, out ); const shaderNode = output !== null && output.hasReference ? this.getMaterialXNode( output.referencePath ) : undefined; if ( shaderNode === undefined ) { this.log.add( MaterialXLogCodes.MISSING_REFERENCE, `Missing shader output "${ out || 'out' }" in the nodegraph of "${ materialXNode.name }".`, materialXNode.name, ); return true; } this.withScope( scope, () => shaderNode.setMaterial( material, output.output ) ); return true; } resolveImplementationShader( materialXNode, out ) { const scope = this.getScope( materialXNode ); if ( scope === null ) return materialXNode; const output = this.getGraphOutput( scope, out ); return this.withScope( scope, () => materialXNode.resolveSurfaceShaderNode( output ) ); } // Resolve interfacename to the node instance's input. // If that input is omitted, use the nodedef default. getInterfaceInput( materialXNode ) { // Regular nodegraphs bind to their own inputs. if ( ! this.isInScope( materialXNode ) ) { const graphInput = this.getMaterialXNode( materialXNode.referencePath ); return graphInput !== undefined && graphInput !== materialXNode ? graphInput : null; } const scope = this.scope; const name = materialXNode.interfaceName; const input = scope.instance.getChildByName( name ); if ( input !== undefined ) return this.withScope( scope.parent, () => input.getValueInput() ); const declaration = this.getMaterialXNode( scope.nodeDef.name ); return declaration ? declaration.getChildByName( name ) ?? null : null; } getInterfaceNode( materialXNode ) { if ( ! this.isInScope( materialXNode ) ) return null; const scope = this.scope; const name = materialXNode.interfaceName; const input = scope.instance.getChildByName( name ); // Evaluate the instance input in the parent scope to resolve nested interface bindings. if ( input !== undefined ) return this.withScope( scope.parent, () => input.getNode( input.output ) ); return this.withScope( null, () => scope.instance.getDefaultInputNode( name ) ?? null ); } waitForResources() { return Promise.all( this.pendingResources ).then( () => undefined ); } addMaterialXNode( materialXNode ) { this.nodesXLib.set( materialXNode.nodePath, materialXNode ); } getMaterialXNode( ...names ) { return this.nodesXLib.get( names.join( '/' ) ); } // Convert document nodedefs to the format used by the stdlib registry. getDocumentNodeDefs() { if ( this.documentNodeDefs !== null ) return this.documentNodeDefs; this.documentNodeDefs = {}; for ( const nodeX of this.nodesXLib.values() ) { if ( nodeX.element !== 'nodedef' ) continue; const nodedef = { node: nodeX.getAttribute( 'node' ), inputs: {}, outputs: {} }; const version = nodeX.getAttribute( 'version' ); if ( version ) nodedef.version = version; if ( nodeX.getAttribute( 'isdefaultversion' ) === 'true' ) nodedef.isdefaultversion = true; if ( nodeX.type && nodeX.type !== 'multioutput' ) nodedef.outputs.out = nodeX.type; for ( const child of nodeX.children ) { if ( child.element === 'output' ) { nodedef.outputs[ child.name ] = child.type; } else if ( child.element === 'input' ) { const input = { type: child.type }; if ( child.value !== null ) input.value = child.value; const geomprop = child.getAttribute( 'defaultgeomprop' ); if ( geomprop ) input.defaultgeomprop = geomprop; nodedef.inputs[ child.name ] = input; } } this.documentNodeDefs[ nodeX.name ] = nodedef; } return this.documentNodeDefs; } parseNode( nodeXML, nodePath = '' ) { return parseMaterialXNodeTree( nodeXML, ( childNodeXML, childNodePath ) => new MaterialXNode( this, childNodeXML, childNodePath ), ( materialXNode ) => this.addMaterialXNode( materialXNode ), nodePath, ); } parse( text, materialName = null, options = {} ) { this.nodeResolver = options.nodeResolver || null; this.documentNodeDefs = null; const rootNode = parseMaterialXText( text, ( childNodeXML, childNodePath ) => new MaterialXNode( this, childNodeXML, childNodePath ), ( materialXNode ) => this.addMaterialXNode( materialXNode ), ); this.indexImplementations( rootNode ); if ( options.interfaceValidator ) { options.interfaceValidator( rootNode, this.log ); } const materials = rootNode.toMaterials( materialName ); return { materials, log: this.log.entries, errors: this.log.errors, warnings: this.log.warnings, }; } } export { MaterialXDocument };