| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878 |
- import {
- Texture,
- RepeatWrapping,
- ClampToEdgeWrapping,
- MirroredRepeatWrapping,
- ImageLoader,
- ImageBitmapLoader,
- Matrix3,
- Matrix4,
- MeshBasicNodeMaterial,
- MeshPhysicalNodeMaterial,
- } from 'three/webgpu';
- import {
- float,
- int,
- bool,
- sub,
- vec2,
- vec3,
- vec4,
- color,
- uv,
- mat3,
- mat4,
- element,
- mx_transform_uv,
- mx_srgb_texture_to_lin_rec709,
- } from 'three/tsl';
- import { MaterialXLogCodes } from './MaterialXLog.js';
- import { createMaterialXCompileRegistry, compileNodeFromRegistry } from './compile/MaterialXCompileRegistry.js';
- import { parseMaterialXNodeTree, parseMaterialXText } from './parse/MaterialXParser.js';
- import { getSurfaceMapper } from './MaterialXSurfaceMappings.js';
- import { MtlXLibrary } from './MaterialXNodeLibrary.js';
- import { mxHextileCoord, mxHextileComputeBlendWeights } from './MaterialXHextile.js';
- import { toBooleanNode } from './MaterialXUtils.js';
- const colorSpaceLib = {
- mx_srgb_texture_to_lin_rec709,
- };
- const DEFAULT_DOCUMENT_COLOR_SPACE = 'lin_rec709';
- const IDENTITY_MAT3_VALUES = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
- const IDENTITY_MAT4_VALUES = [ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ];
- const MATRIX_INVERSE_EPSILON = 1e-8;
- const COMPILE_REGISTRY = createMaterialXCompileRegistry();
- const TEXTURE_ADDRESS_MODE_WRAPPING = {
- constant: ClampToEdgeWrapping,
- clamp: ClampToEdgeWrapping,
- periodic: RepeatWrapping,
- mirror: MirroredRepeatWrapping,
- };
- const NODE_CLASS_BY_TYPE = {
- integer: int,
- float,
- vector2: vec2,
- vector3: vec3,
- vector4: vec4,
- color4: vec4,
- color3: color,
- boolean: null,
- matrix33: mat3,
- matrix44: mat4,
- };
- const OUTPUT_CHANNELS = {
- outx: 0,
- outr: 0,
- outy: 1,
- outg: 1,
- outz: 2,
- outb: 2,
- outw: 3,
- outa: 3,
- };
- function mxFlipUvY( uvNode ) {
- return vec2( element( uvNode, 0 ), sub( 1, element( uvNode, 1 ) ) );
- }
- function mxIdentityUv( uvNode ) {
- return uvNode;
- }
- function getBottomLeftUvSpaceHelpers( uvSpace ) {
- const helper = uvSpace === 'top-left' ? mxFlipUvY : mxIdentityUv;
- return {
- mxToBottomLeftUvSpace: helper,
- mxFromBottomLeftUvSpace: helper,
- };
- }
- function normalizeUvSpace( uvSpace ) {
- if ( uvSpace === undefined || uvSpace === null ) return 'bottom-left';
- if ( uvSpace === 'bottom-left' || uvSpace === 'top-left' ) return uvSpace;
- throw new Error( `Unsupported MaterialX uvSpace "${uvSpace}". Expected "bottom-left" or "top-left".` );
- }
- function isSvgUri( uri ) {
- if ( typeof uri !== 'string' ) return false;
- return /\.svg(?:$|[?#])/i.test( uri );
- }
- function normalizeTextureAddressMode( value ) {
- if ( value === null || value === undefined || value === '' ) return 'periodic';
- const mode = value.trim().toLowerCase();
- return mode in TEXTURE_ADDRESS_MODE_WRAPPING ? mode : null;
- }
- function invertConstantMatrixValues( values, size ) {
- if ( ! Array.isArray( values ) || values.length !== size * size ) return null;
- if ( size === 3 ) {
- const matrix = new Matrix3().setFromArray( values );
- if ( Math.abs( matrix.determinant() ) < MATRIX_INVERSE_EPSILON ) return null;
- matrix.invert();
- // Convert Three.js internal column-major storage back to row-major literal order.
- return matrix.transpose().elements;
- }
- if ( size === 4 ) {
- const matrix = new Matrix4().setFromArray( values );
- if ( Math.abs( matrix.determinant() ) < MATRIX_INVERSE_EPSILON ) return null;
- matrix.invert();
- // Convert Three.js internal column-major storage back to row-major literal order.
- return matrix.transpose().elements;
- }
- return null;
- }
- function getOutputChannel( outputName ) {
- return OUTPUT_CHANNELS[ outputName ] || 0;
- }
- function isChannelOutput( outputName ) {
- return outputName in OUTPUT_CHANNELS;
- }
- class MaterialXNode {
- constructor( materialX, nodeXML, nodePath = '' ) {
- this.materialX = materialX;
- this.nodeXML = nodeXML;
- this.nodePath = nodePath ? nodePath + '/' + this.name : this.name;
- this.parent = null;
- this.node = null;
- this.children = [];
- }
- get element() {
- return this.nodeXML.nodeName;
- }
- get nodeGraph() {
- return this.getAttribute( 'nodegraph' );
- }
- get nodeName() {
- return this.getAttribute( 'nodename' );
- }
- get interfaceName() {
- return this.getAttribute( 'interfacename' );
- }
- get output() {
- return this.getAttribute( 'output' );
- }
- get name() {
- return this.getAttribute( 'name' );
- }
- get type() {
- return this.getAttribute( 'type' );
- }
- get value() {
- return this.getAttribute( 'value' );
- }
- getNodeGraph() {
- let nodeX = this;
- while ( nodeX !== null ) {
- if ( nodeX.element === 'nodegraph' ) break;
- nodeX = nodeX.parent;
- }
- return nodeX;
- }
- getRoot() {
- let nodeX = this;
- while ( nodeX.parent !== null ) {
- nodeX = nodeX.parent;
- }
- return nodeX;
- }
- get referencePath() {
- let referencePath = null;
- if ( this.nodeGraph !== null && this.output !== null ) {
- referencePath = this.nodeGraph + '/' + this.output;
- } else if ( this.nodeName !== null || this.interfaceName !== null ) {
- const graphNode = this.getNodeGraph();
- const scopedReference = this.nodeName || this.interfaceName;
- if ( graphNode && scopedReference ) {
- referencePath = graphNode.nodePath + '/' + scopedReference;
- } else if ( this.nodeName !== null ) {
- // Surface-level nodename links can legitimately target top-level siblings.
- referencePath = this.nodeName;
- }
- }
- return referencePath;
- }
- get hasReference() {
- return this.referencePath !== null;
- }
- get isConst() {
- return this.element === 'input' && this.value !== null && this.type !== 'filename';
- }
- getColorSpaceNode() {
- const csSource = this.getAttribute( 'colorspace' );
- const csTarget = this.getRoot().getAttribute( 'colorspace' );
- if ( ! csSource || ! csTarget ) return null;
- const nodeName = `mx_${csSource}_to_${csTarget}`;
- return colorSpaceLib[ nodeName ] || null;
- }
- getTextureAddressMode( inputName ) {
- const rawMode = this.getInputValueByName( inputName );
- const mode = normalizeTextureAddressMode( rawMode );
- if ( mode ) return mode;
- this.materialX.log.add(
- MaterialXLogCodes.INVALID_VALUE,
- `Unsupported texture address mode "${rawMode}" on input "${inputName}". Expected constant, clamp, periodic, or mirror.`,
- this.name,
- );
- return 'periodic';
- }
- getTextureAddressModes() {
- return {
- u: this.getTextureAddressMode( 'uaddressmode' ),
- v: this.getTextureAddressMode( 'vaddressmode' ),
- };
- }
- getTexture() {
- const filePrefix = this.getRecursiveAttribute( 'fileprefix' ) || '';
- const sourceURI = filePrefix + this.value;
- const resolvedURI = this.materialX.resolveTextureURI( sourceURI );
- const svgTexture = isSvgUri( resolvedURI );
- const textureSourceNode = this.parent && typeof this.parent.getTextureAddressModes === 'function' ? this.parent : this;
- const addressModes = textureSourceNode.getTextureAddressModes();
- const textureCacheKey = `${resolvedURI}|${addressModes.u}|${addressModes.v}`;
- if ( this.materialX.textureCache.has( textureCacheKey ) ) {
- return this.materialX.textureCache.get( textureCacheKey );
- }
- let loader = svgTexture ? this.materialX.imageLoader : this.materialX.textureLoader;
- if ( resolvedURI && ! svgTexture ) {
- const handler = this.materialX.manager.getHandler( resolvedURI );
- if ( handler !== null ) loader = handler;
- }
- const textureNode = new Texture();
- textureNode.wrapS = TEXTURE_ADDRESS_MODE_WRAPPING[ addressModes.u ];
- textureNode.wrapT = TEXTURE_ADDRESS_MODE_WRAPPING[ addressModes.v ];
- textureNode.flipY = false;
- this.materialX.textureCache.set( textureCacheKey, textureNode );
- loader.load( resolvedURI, ( imageData ) => {
- textureNode.image = imageData;
- textureNode.needsUpdate = true;
- }, undefined, () => {
- throw new Error( `Failed to load texture "${resolvedURI}".` );
- } );
- return textureNode;
- }
- getClassFromType( type ) {
- return NODE_CLASS_BY_TYPE[ type ] || null;
- }
- toBooleanNode( node ) {
- return toBooleanNode( node );
- }
- getNode( out = null ) {
- if ( this.node !== null && out === null ) return this.node;
- let node;
- if ( ( this.element === 'separate2' || this.element === 'separate3' || this.element === 'separate4' ) && out ) {
- const inNode = this.getNodeByName( 'in' );
- return element( inNode, getOutputChannel( out ) );
- }
- const type = this.type;
- const channelRequested = this.element !== 'input' && this.element !== 'gltf_colorimage' && isChannelOutput( out );
- if ( this.isConst ) {
- if ( type === 'boolean' ) {
- const normalized = this.getValue().trim().toLowerCase();
- node = bool( normalized === 'true' || normalized === '1' );
- } else if ( type === 'matrix33' ) {
- node = this.getMatrix( 3 ) || mat3( 1, 0, 0, 0, 1, 0, 0, 0, 1 );
- } else if ( type === 'matrix44' ) {
- node = this.getMatrix( 4 ) || mat4( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 );
- } else if ( type === 'string' ) {
- node = this.getValue();
- } else {
- const nodeClass = this.getClassFromType( type );
- node = nodeClass ? nodeClass( ...this.getVector() ) : float( 0 );
- }
- } else if ( this.hasReference ) {
- if ( this.element === 'output' && this.output && out === null ) out = this.output;
- let requestedOutput = out;
- // For nodegraph references, this input's `output` attribute selects the graph output
- // itself and should not be forwarded as an output selector on the resolved node.
- if ( this.element === 'input' && this.nodeGraph !== null && this.output !== null ) {
- requestedOutput = null;
- }
- const referenceNode = this.materialX.getMaterialXNode( this.referencePath );
- if ( referenceNode ) {
- node = referenceNode.getNode( requestedOutput );
- } else {
- this.materialX.log.add(
- MaterialXLogCodes.MISSING_REFERENCE,
- `Missing MaterialX reference "${this.referencePath}" from "${this.name}".`,
- this.name,
- );
- node = float( 0 );
- }
- } else if ( this.element === 'input' && this.name === 'texcoord' && this.type === 'vector2' ) {
- let index = 0;
- const defaultGeomProp = this.getAttribute( 'defaultgeomprop' );
- if ( defaultGeomProp && /^UV(\d+)$/.test( defaultGeomProp ) ) {
- index = parseInt( defaultGeomProp.match( /^UV(\d+)$/ )[ 1 ], 10 );
- }
- node = this.materialX.compileContext.mxToBottomLeftUvSpace( uv( index ) );
- } else {
- node = compileNodeFromRegistry( this, out, this.materialX.compileContext );
- }
- if ( node === null || node === undefined ) {
- this.materialX.log.add(
- MaterialXLogCodes.UNSUPPORTED_NODE,
- `Unsupported MaterialX node category "${this.element}" on "${this.name}".`,
- this.name,
- );
- node = float( 0 );
- }
- if ( channelRequested ) {
- node = element( node, getOutputChannel( out ) );
- }
- const resolvedType = channelRequested ? 'float' : type;
- if ( resolvedType === 'boolean' ) {
- node = this.toBooleanNode( node );
- } else if ( resolvedType === 'string' ) {
- // String-typed inputs (for example transform* fromspace/tospace) are
- // valid scalar parameters and should pass through without numeric casting.
- node = typeof node === 'string' ? node : this.getValue();
- } else {
- const nodeToTypeClass = this.getClassFromType( resolvedType );
- if ( nodeToTypeClass !== null ) {
- node = nodeToTypeClass( node );
- } else if ( resolvedType !== null && resolvedType !== undefined && resolvedType !== 'multioutput' ) {
- this.materialX.log.add(
- MaterialXLogCodes.INVALID_VALUE,
- `Unexpected type "${resolvedType}" on node "${this.name}".`,
- this.name,
- );
- node = float( 0 );
- }
- }
- if ( node && typeof node === 'object' ) {
- node.name = this.name;
- }
- this.node = node;
- return node;
- }
- getChildByName( name ) {
- for ( const input of this.children ) {
- if ( input.name === name ) return input;
- }
- }
- getNodes() {
- const nodes = {};
- for ( const input of this.children ) {
- const value = input.getNode( input.output );
- nodes[ input.name ] = value;
- }
- return nodes;
- }
- getNodeByName( name ) {
- const child = this.getChildByName( name );
- return child ? child.getNode( child.output ) : undefined;
- }
- getInputValueByName( name ) {
- const child = this.getChildByName( name );
- return child ? child.value : null;
- }
- getNodesByNames( ...names ) {
- const nodes = [];
- for ( const name of names ) {
- const nodeValue = this.getNodeByName( name );
- nodes.push( nodeValue );
- }
- return nodes;
- }
- getValue() {
- return this.value ? this.value.trim() : '';
- }
- getVector() {
- const vector = [];
- for ( const val of this.getValue().split( /[,|\s]/ ) ) {
- if ( val !== '' ) vector.push( Number( val.trim() ) );
- }
- return vector;
- }
- getMatrix( size ) {
- const vector = this.getVector();
- const expectedLength = size * size;
- if ( vector.length !== expectedLength ) return null;
- // MaterialX matrix values are serialized in column-major order.
- // Reorder to row-major before constructing TSL matrix nodes so
- // transformmatrix semantics match MaterialXJS and MaterialXView.
- const reordered = [];
- for ( let row = 0; row < size; row += 1 ) {
- for ( let column = 0; column < size; column += 1 ) {
- reordered.push( vector[ column * size + row ] );
- }
- }
- return size === 3 ? mat3( ...reordered ) : mat4( ...reordered );
- }
- getAttribute( name ) {
- const value = this.nodeXML.getAttribute( name );
- if ( value === null && this.element === 'materialx' && name === 'colorspace' ) {
- return DEFAULT_DOCUMENT_COLOR_SPACE;
- }
- return value;
- }
- getRecursiveAttribute( name ) {
- let attribute = this.nodeXML.getAttribute( name );
- if ( attribute === null && this.parent !== null ) {
- attribute = this.parent.getRecursiveAttribute( name );
- }
- return attribute;
- }
- setMaterial( material ) {
- const mapper = getSurfaceMapper( this.element );
- if ( mapper ) {
- mapper.apply( material, this.getNodes(), this.materialX.log, this.name );
- } else {
- this.materialX.log.add(
- MaterialXLogCodes.UNSUPPORTED_NODE,
- `Unsupported MaterialX node category "${this.element}" on "${this.name}".`,
- this.name,
- );
- }
- }
- toBasicMaterial() {
- const material = new MeshBasicNodeMaterial();
- material.name = this.name;
- for ( const nodeX of this.children.toReversed() ) {
- if ( nodeX.name === 'out' ) {
- material.colorNode = nodeX.getNode();
- break;
- }
- }
- return material;
- }
- resolveSurfaceShaderNode( nodeX ) {
- if ( nodeX.hasReference ) {
- return this.materialX.getMaterialXNode( nodeX.referencePath ) || null;
- }
- if ( nodeX.nodeName ) {
- return this.materialX.getMaterialXNode( nodeX.nodeName ) || null;
- }
- return null;
- }
- toPhysicalMaterial() {
- const material = new MeshPhysicalNodeMaterial();
- material.name = this.name;
- for ( const nodeX of this.children ) {
- const shaderProperties = this.resolveSurfaceShaderNode( nodeX );
- if ( shaderProperties === null ) {
- this.materialX.log.add(
- MaterialXLogCodes.MISSING_REFERENCE,
- `Missing MaterialX reference "${nodeX.referencePath || nodeX.nodeName || '(unknown)'}" from "${nodeX.name}".`,
- nodeX.name,
- );
- continue;
- }
- shaderProperties.setMaterial( material );
- }
- return material;
- }
- toMaterials( materialName = null ) {
- const materials = {};
- const surfaceMaterials = this.children.filter( ( nodeX ) => nodeX.element === 'surfacematerial' );
- let selectedSurfaceMaterials = surfaceMaterials;
- if ( materialName ) {
- selectedSurfaceMaterials = surfaceMaterials.filter( ( nodeX ) => nodeX.name === materialName );
- if ( selectedSurfaceMaterials.length === 0 ) {
- this.materialX.log.add(
- MaterialXLogCodes.MISSING_MATERIAL,
- `Could not find surfacematerial named "${materialName}".`,
- );
- }
- }
- for ( const nodeX of selectedSurfaceMaterials ) {
- const material = nodeX.toPhysicalMaterial();
- materials[ material.name ] = material;
- }
- if ( Object.keys( materials ).length === 0 ) {
- for ( const nodeX of this.children ) {
- if ( nodeX.element === 'nodegraph' ) {
- const material = nodeX.toBasicMaterial();
- materials[ material.name ] = material;
- }
- }
- }
- return materials;
- }
- add( materialXNode ) {
- materialXNode.parent = this;
- this.children.push( materialXNode );
- }
- }
- class MaterialXDocument {
- constructor( manager, path, log, archiveResolver = null, uvSpace = 'bottom-left' ) {
- this.manager = manager;
- this.path = path;
- this.log = log;
- this.archiveResolver = archiveResolver;
- this.uvSpace = normalizeUvSpace( uvSpace );
- this.nodesXLib = new Map();
- this.imageLoader = new ImageLoader( manager );
- this.imageLoader.setPath( path );
- this.textureLoader = new ImageBitmapLoader( manager );
- this.textureLoader.setOptions( { imageOrientation: 'none' } );
- this.textureLoader.setPath( path );
- this.textureCache = new Map();
- const bottomLeftUvSpaceHelpers = getBottomLeftUvSpaceHelpers( this.uvSpace );
- this.compileContext = {
- compileRegistry: COMPILE_REGISTRY,
- nodeLibrary: MtlXLibrary,
- ...bottomLeftUvSpaceHelpers,
- mxTransformUv: mx_transform_uv,
- mxHextileCoord,
- mxHextileComputeBlendWeights,
- invertConstantMatrixValues,
- IDENTITY_MAT3_VALUES,
- IDENTITY_MAT4_VALUES,
- };
- }
- resolveTextureURI( uri ) {
- if ( this.archiveResolver ) {
- const archiveURI = this.archiveResolver( uri );
- if ( archiveURI ) return archiveURI;
- }
- return uri;
- }
- addMaterialXNode( materialXNode ) {
- this.nodesXLib.set( materialXNode.nodePath, materialXNode );
- }
- getMaterialXNode( ...names ) {
- return this.nodesXLib.get( names.join( '/' ) );
- }
- parseNode( nodeXML, nodePath = '' ) {
- return parseMaterialXNodeTree(
- nodeXML,
- ( childNodeXML, childNodePath ) => new MaterialXNode( this, childNodeXML, childNodePath ),
- ( materialXNode ) => this.addMaterialXNode( materialXNode ),
- nodePath,
- );
- }
- parse( text, materialName = null, options = {} ) {
- const rootNode = parseMaterialXText(
- text,
- ( childNodeXML, childNodePath ) => new MaterialXNode( this, childNodeXML, childNodePath ),
- ( materialXNode ) => this.addMaterialXNode( materialXNode ),
- );
- if ( options.interfaceValidator ) {
- options.interfaceValidator( rootNode, this.log );
- }
- const materials = rootNode.toMaterials( materialName );
- return {
- materials,
- log: this.log.entries,
- errors: this.log.errors,
- warnings: this.log.warnings,
- };
- }
- }
- export { MaterialXDocument };
|