three
Version:
JavaScript 3D library
662 lines (443 loc) • 19.2 kB
JavaScript
import { DoubleSide } from 'three/webgpu';
import { MaterialXLogCodes } from './MaterialXLog.js';
import { float, color, mul, clamp, vec2, cos, sin, pow, mix, element, transformNormalToView } from 'three/tsl';
const mappedStandardSurfaceInputs = new Set( [
'base',
'base_color',
'specular_roughness',
'metalness',
'specular',
'specular_color',
'specular_anisotropy',
'specular_rotation',
'transmission',
'transmission_color',
'transmission_depth',
'thin_film_thickness',
'thin_film_IOR',
'sheen',
'sheen_color',
'sheen_roughness',
'coat',
'coat_color',
'coat_roughness',
'coat_normal',
'normal',
'opacity',
'specular_IOR',
'emission',
'emission_color',
] );
const mappedGltfPbrInputs = new Set( [
'base_color',
'occlusion',
'roughness',
'metallic',
'normal',
'transmission',
'specular',
'specular_color',
'ior',
'alpha',
'alpha_mode',
'alpha_cutoff',
'iridescence',
'iridescence_ior',
'iridescence_thickness',
'sheen_color',
'sheen_roughness',
'clearcoat',
'clearcoat_roughness',
'clearcoat_normal',
'emissive',
'emissive_strength',
'attenuation_distance',
'attenuation_color',
'thickness',
'dispersion',
'anisotropy_strength',
'anisotropy_rotation',
] );
const mappedOpenPbrInputs = new Set( [
'base_weight',
'base_color',
'specular_weight',
'specular_color',
'specular_roughness',
'base_metalness',
'specular_roughness_anisotropy',
'specular_ior',
'coat_weight',
'coat_ior',
'coat_color',
'coat_roughness',
'geometry_coat_normal',
'fuzz_weight',
'fuzz_color',
'fuzz_roughness',
'transmission_weight',
'transmission_color',
'transmission_depth',
'transmission_dispersion_scale',
'transmission_dispersion_abbe_number',
'geometry_normal',
'geometry_opacity',
'geometry_thin_walled',
'thin_film_weight',
'thin_film_thickness',
'thin_film_ior',
'emission_color',
'emission_luminance',
] );
function warnIgnoredInputs( inputs, mappedInputs, log, surfaceCategory, nodeName ) {
for ( const inputName of Object.keys( inputs ) ) {
if ( mappedInputs.has( inputName ) === false ) {
log.add(
MaterialXLogCodes.IGNORED_SURFACE_INPUT,
`${surfaceCategory} input "${inputName}" is currently ignored in MaterialX translation.`,
nodeName,
);
}
}
}
function hasNodeValue( value ) {
return value !== undefined && value !== null;
}
function getConstNumber( node ) {
if ( typeof node === 'number' ) return node;
if ( ! node || typeof node !== 'object' ) return null;
let cursor = node;
const visited = new Set();
while ( cursor && typeof cursor === 'object' ) {
if ( visited.has( cursor ) ) break;
visited.add( cursor );
if ( typeof cursor.value === 'number' ) return cursor.value;
cursor = cursor.node;
}
return null;
}
function isConstNear( node, target, epsilon = 1e-6 ) {
const value = getConstNumber( node );
if ( value === null ) return false;
return Math.abs( value - target ) <= epsilon;
}
function isEffectivelyZero( node, epsilon = 1e-6 ) {
return isConstNear( node, 0, epsilon );
}
function isEffectivelyOne( node, epsilon = 1e-6 ) {
return isConstNear( node, 1, epsilon );
}
function isEnabledWeightNode( node ) {
if ( hasNodeValue( node ) === false ) return false;
return isEffectivelyZero( node ) === false;
}
function setAnisotropy( material, strengthNode, rotationNode ) {
if ( ! hasNodeValue( strengthNode ) && ! hasNodeValue( rotationNode ) ) return;
if ( isEffectivelyZero( strengthNode ) && isEffectivelyZero( rotationNode ) ) return;
const strength = hasNodeValue( strengthNode ) ? strengthNode : float( 0 );
const rotation = hasNodeValue( rotationNode ) ? rotationNode : float( 0 );
material.anisotropyNode = vec2( cos( rotation ), sin( rotation ) ).mul( strength );
material.anisotropyRotationNode = rotation;
}
function setTransmissionFlags( material, transmissionNode, opacityNode, allowOpacityTransparency = true ) {
if ( allowOpacityTransparency && hasNodeValue( opacityNode ) && isEffectivelyOne( opacityNode ) === false ) {
material.transparent = true;
}
if ( isEnabledWeightNode( transmissionNode ) ) {
material.side = DoubleSide;
material.transparent = true;
}
}
function toAttenuationDistance( distanceNode, hasAttenuationColorInput ) {
if ( hasNodeValue( distanceNode ) ) return distanceNode;
// When attenuation tint is authored without a distance, default to a
// finite value so absorption tinting is visible.
return hasAttenuationColorInput ? float( 1 ) : undefined;
}
function buildGltfOpacityNode( alphaNode, alphaModeNode ) {
const alphaMode = getConstNumber( alphaModeNode );
const roundedMode = alphaMode === null ? 2 : Math.round( alphaMode );
const alpha = alphaNode ?? float( 1 );
if ( roundedMode === 0 ) return float( 1 );
if ( roundedMode === 1 ) return alpha;
return alpha;
}
function applyStandardSurface( material, inputs, log, nodeName ) {
let colorNode = null;
if ( inputs.base && inputs.base_color ) colorNode = mul( inputs.base, inputs.base_color );
else if ( inputs.base ) colorNode = inputs.base;
else if ( inputs.base_color ) colorNode = inputs.base_color;
if ( inputs.coat_color ) {
colorNode = colorNode ? mul( colorNode, inputs.coat_color ) : colorNode;
}
const roughnessNode = inputs.specular_roughness;
const opacityNode = hasNodeValue( inputs.opacity ) ? element( inputs.opacity, 0 ) : undefined;
const transmissionNode = inputs.transmission;
const transmissionColorNode = inputs.transmission_color;
const transmissionEnabled = isEnabledWeightNode( transmissionNode );
const sheenEnabled = isEnabledWeightNode( inputs.sheen );
const clearcoatEnabled = isEnabledWeightNode( inputs.coat );
let emissiveNode = inputs.emission;
const emissionColorNode = inputs.emission_color;
if ( hasNodeValue( emissionColorNode ) ) {
emissiveNode = emissiveNode ? mul( emissiveNode, emissionColorNode ) : emissionColorNode;
}
const thinFilmThicknessNode = inputs.thin_film_thickness;
const thinFilmIorNode = clamp( inputs.thin_film_IOR || float( 1.5 ), float( 1.0 ), float( 2.333 ) );
const thinFilmEnabled = isEnabledWeightNode( thinFilmThicknessNode );
const transmissionTintNode = hasNodeValue( transmissionColorNode ) ? transmissionColorNode : color( 1, 1, 1 );
if ( transmissionEnabled ) {
const baseForTransmissionMix = colorNode || color( 0.8, 0.8, 0.8 );
// Suppress diffuse/base tint as transmission ramps up.
colorNode = mix( baseForTransmissionMix, transmissionTintNode, transmissionNode );
}
material.colorNode = colorNode || color( 0.8, 0.8, 0.8 );
if ( hasNodeValue( opacityNode ) && isEffectivelyOne( opacityNode ) === false ) {
material.opacityNode = opacityNode;
}
material.roughnessNode = roughnessNode || float( 0.2 );
if ( hasNodeValue( inputs.metalness ) && isEffectivelyZero( inputs.metalness ) === false ) {
material.metalnessNode = inputs.metalness;
}
if ( hasNodeValue( inputs.specular ) && isEffectivelyOne( inputs.specular ) === false ) {
material.specularIntensityNode = inputs.specular;
}
material.specularColorNode = inputs.specular_color || color( 1, 1, 1 );
const iorNode = inputs.specular_IOR;
if ( hasNodeValue( iorNode ) && isConstNear( iorNode, 1.5 ) === false ) {
material.iorNode = iorNode;
}
setAnisotropy( material, inputs.specular_anisotropy, inputs.specular_rotation );
if ( transmissionEnabled ) {
material.transmissionNode = transmissionNode;
if ( hasNodeValue( transmissionColorNode ) ) material.transmissionColorNode = transmissionColorNode;
if ( hasNodeValue( inputs.transmission_depth ) && isEffectivelyZero( inputs.transmission_depth ) === false ) {
material.thicknessNode = inputs.transmission_depth;
} else {
// Keep transmissive standard_surface materials volumetric when
// transmission_depth is omitted or authored as zero.
material.thickness = 1;
}
}
if ( thinFilmEnabled ) {
material.iridescenceThicknessNode = thinFilmThicknessNode;
material.iridescenceIORNode = thinFilmIorNode;
material.iridescenceNode = float( 1 );
}
const sheenColor = inputs.sheen_color || color( 1, 1, 1 );
if ( sheenEnabled ) {
const sheenRoughness = hasNodeValue( inputs.sheen_roughness ) ? inputs.sheen_roughness : float( 0.3 );
material.sheenNode = mul( inputs.sheen, sheenColor );
material.sheenRoughnessNode = sheenRoughness;
}
if ( clearcoatEnabled ) {
material.clearcoatNode = inputs.coat;
if ( hasNodeValue( inputs.coat_roughness ) ) {
material.clearcoatRoughnessNode = inputs.coat_roughness;
}
}
if ( clearcoatEnabled && hasNodeValue( inputs.coat_normal ) ) {
material.clearcoatNormalNode = transformNormalToView( inputs.coat_normal );
}
if ( hasNodeValue( inputs.normal ) ) material.normalNode = transformNormalToView( inputs.normal );
if ( hasNodeValue( emissiveNode ) ) material.emissiveNode = emissiveNode;
setTransmissionFlags( material, transmissionNode, opacityNode );
warnIgnoredInputs( inputs, mappedStandardSurfaceInputs, log, 'standard_surface', nodeName );
}
function applyGltfPbrSurface( material, inputs, log, nodeName ) {
const alphaModeLiteral = getConstNumber( inputs.alpha_mode );
const alphaMode = alphaModeLiteral === null ? 2 : Math.round( alphaModeLiteral );
const isAlphaMaskMode = alphaMode === 1;
const isAlphaBlendMode = alphaMode === 2;
const opacityNode = buildGltfOpacityNode( inputs.alpha, inputs.alpha_mode );
const alphaCutoffNode = inputs.alpha_cutoff ?? float( 0.5 );
const hasAttenuationColorInput = Object.prototype.hasOwnProperty.call( inputs, 'attenuation_color' );
const transmissionEnabled = isEnabledWeightNode( inputs.transmission );
const clearcoatEnabled = isEnabledWeightNode( inputs.clearcoat );
const sheenEnabled = hasNodeValue( inputs.sheen_color ) || isEnabledWeightNode( inputs.sheen_roughness );
const iridescenceEnabled = isEnabledWeightNode( inputs.iridescence );
material.colorNode = inputs.base_color || color( 1, 1, 1 );
if ( hasNodeValue( inputs.occlusion ) ) material.aoNode = inputs.occlusion;
material.roughnessNode = inputs.roughness || float( 1 );
material.metalnessNode = inputs.metallic || float( 1 );
if ( hasNodeValue( inputs.specular ) && isEffectivelyOne( inputs.specular ) === false ) {
material.specularIntensityNode = inputs.specular;
}
material.specularColorNode = inputs.specular_color || color( 1, 1, 1 );
if ( hasNodeValue( inputs.ior ) && isConstNear( inputs.ior, 1.5 ) === false ) {
material.iorNode = inputs.ior;
}
if ( hasNodeValue( opacityNode ) && isEffectivelyOne( opacityNode ) === false ) {
material.opacityNode = opacityNode;
}
if ( isAlphaMaskMode ) {
material.alphaTestNode = alphaCutoffNode;
const alphaCutoff = getConstNumber( alphaCutoffNode );
if ( alphaCutoff !== null ) material.alphaTest = alphaCutoff;
}
if ( transmissionEnabled ) {
material.transmissionNode = inputs.transmission;
}
if ( clearcoatEnabled ) {
material.clearcoatNode = inputs.clearcoat;
if ( hasNodeValue( inputs.clearcoat_roughness ) && isEffectivelyZero( inputs.clearcoat_roughness ) === false ) {
material.clearcoatRoughnessNode = inputs.clearcoat_roughness;
}
}
if ( sheenEnabled ) {
const sheenColor = hasNodeValue( inputs.sheen_color ) ? inputs.sheen_color : color( 0, 0, 0 );
const sheenRoughness = hasNodeValue( inputs.sheen_roughness ) ? inputs.sheen_roughness : float( 0 );
material.sheenRoughnessNode = sheenRoughness;
material.sheenNode = sheenColor;
}
if ( iridescenceEnabled ) {
material.iridescenceNode = inputs.iridescence;
if ( hasNodeValue( inputs.iridescence_ior ) && isConstNear( inputs.iridescence_ior, 1.3 ) === false ) {
material.iridescenceIORNode = inputs.iridescence_ior;
}
if ( hasNodeValue( inputs.iridescence_thickness ) && isConstNear( inputs.iridescence_thickness, 100 ) === false ) {
material.iridescenceThicknessNode = inputs.iridescence_thickness;
}
}
material.attenuationDistanceNode = toAttenuationDistance( inputs.attenuation_distance, hasAttenuationColorInput );
material.attenuationColorNode = inputs.attenuation_color || color( 1, 1, 1 );
if ( hasNodeValue( inputs.thickness ) ) {
if ( isEffectivelyZero( inputs.thickness ) === false ) {
material.thicknessNode = inputs.thickness;
}
} else if ( transmissionEnabled ) {
// Keep transmissive glTF materials volumetric even when thickness is omitted.
material.thickness = 1;
}
if ( hasNodeValue( inputs.dispersion ) && isEffectivelyZero( inputs.dispersion ) === false ) {
material.dispersionNode = inputs.dispersion;
}
const anisotropyStrength = inputs.anisotropy_strength;
const anisotropyRotation = inputs.anisotropy_rotation;
setAnisotropy( material, anisotropyStrength, anisotropyRotation );
if ( hasNodeValue( inputs.normal ) ) material.normalNode = transformNormalToView( inputs.normal );
if ( hasNodeValue( inputs.clearcoat_normal ) ) {
material.clearcoatNormalNode = transformNormalToView( inputs.clearcoat_normal );
}
if ( hasNodeValue( inputs.emissive ) && hasNodeValue( inputs.emissive_strength ) )
material.emissiveNode = mul( inputs.emissive, inputs.emissive_strength );
else if ( hasNodeValue( inputs.emissive ) ) material.emissiveNode = inputs.emissive;
setTransmissionFlags( material, inputs.transmission, opacityNode, isAlphaBlendMode );
warnIgnoredInputs( inputs, mappedGltfPbrInputs, log, 'gltf_pbr', nodeName );
}
function applyOpenPbrSurface( material, inputs, log, nodeName ) {
const baseWeight = inputs.base_weight || float( 1 );
const baseColor = inputs.base_color || color( 0.8, 0.8, 0.8 );
const coatEnabled = isEnabledWeightNode( inputs.coat_weight );
const fuzzEnabled = isEnabledWeightNode( inputs.fuzz_weight );
const transmissionEnabled = isEnabledWeightNode( inputs.transmission_weight );
const thinFilmEnabled = isEnabledWeightNode( inputs.thin_film_weight );
material.colorNode = mul( baseWeight, baseColor );
if ( hasNodeValue( inputs.base_metalness ) && isEffectivelyZero( inputs.base_metalness ) === false ) {
material.metalnessNode = inputs.base_metalness;
}
material.roughnessNode = inputs.specular_roughness || float( 0.3 );
if ( hasNodeValue( inputs.specular_weight ) && isEffectivelyOne( inputs.specular_weight ) === false ) {
material.specularIntensityNode = inputs.specular_weight;
}
material.specularColorNode = inputs.specular_color || color( 1, 1, 1 );
const openPbrIorNode = inputs.specular_ior;
if ( hasNodeValue( openPbrIorNode ) && isConstNear( openPbrIorNode, 1.5 ) === false ) {
material.iorNode = openPbrIorNode;
}
setAnisotropy( material, inputs.specular_roughness_anisotropy, float( 0 ) );
if ( coatEnabled ) {
const coatWeightNode = inputs.coat_weight || float( 0 );
if ( hasNodeValue( inputs.coat_ior ) ) {
const coatIorNode = inputs.coat_ior;
const coatIorMinusOne = coatIorNode.sub( float( 1 ) );
const coatIorPlusOne = coatIorNode.add( float( 1 ) );
const coatF0Node = coatIorMinusOne.div( coatIorPlusOne );
const normalizedClearcoatNode = coatF0Node.mul( coatF0Node ).div( float( 0.04 ) );
material.clearcoatNode = clamp( coatWeightNode.mul( normalizedClearcoatNode ), float( 0 ), float( 1 ) );
} else {
material.clearcoatNode = coatWeightNode;
}
if ( hasNodeValue( inputs.coat_roughness ) && isEffectivelyZero( inputs.coat_roughness ) === false ) {
material.clearcoatRoughnessNode = inputs.coat_roughness;
}
if ( hasNodeValue( inputs.geometry_coat_normal ) ) {
material.clearcoatNormalNode = transformNormalToView( inputs.geometry_coat_normal );
}
}
const fuzzWeight = inputs.fuzz_weight || float( 0 );
const fuzzColor = inputs.fuzz_color || color( 1, 1, 1 );
if ( fuzzEnabled ) {
material.sheenNode = mul( fuzzWeight, fuzzColor );
if ( hasNodeValue( inputs.fuzz_roughness ) ) {
material.sheenRoughnessNode = pow( inputs.fuzz_roughness, float( 1.5 ) );
}
}
if ( transmissionEnabled ) {
material.transmissionNode = inputs.transmission_weight;
if ( hasNodeValue( inputs.transmission_color ) ) material.attenuationColorNode = inputs.transmission_color;
}
const transmissionDepthNode = inputs.transmission_depth;
if ( transmissionEnabled && hasNodeValue( transmissionDepthNode ) && isEffectivelyZero( transmissionDepthNode ) === false ) {
material.thicknessNode = hasNodeValue( inputs.geometry_thin_walled )
? inputs.geometry_thin_walled.select( float( 0 ), transmissionDepthNode )
: transmissionDepthNode;
material.attenuationDistanceNode = transmissionDepthNode;
} else if ( transmissionEnabled ) {
// Keep transmissive OpenPBR materials volumetric even when depth is omitted.
material.thickness = 1;
}
const transmissionDispersionAbbe = inputs.transmission_dispersion_abbe_number || float( 20 );
if ( transmissionEnabled && hasNodeValue( inputs.transmission_dispersion_scale ) && isEffectivelyZero( inputs.transmission_dispersion_scale ) === false ) {
material.dispersionNode = inputs.transmission_dispersion_scale.mul( float( 20 ) ).div( transmissionDispersionAbbe );
}
if ( hasNodeValue( inputs.geometry_opacity ) && isEffectivelyOne( inputs.geometry_opacity ) === false ) {
material.opacityNode = inputs.geometry_opacity;
}
if ( hasNodeValue( inputs.geometry_normal ) ) material.normalNode = transformNormalToView( inputs.geometry_normal );
if ( thinFilmEnabled ) {
material.iridescenceNode = inputs.thin_film_weight;
if ( hasNodeValue( inputs.thin_film_thickness ) && isConstNear( inputs.thin_film_thickness, 0.5 ) === false ) {
material.iridescenceThicknessNode = inputs.thin_film_thickness.mul( float( 1000 ) );
}
if ( hasNodeValue( inputs.thin_film_ior ) && isConstNear( inputs.thin_film_ior, 1.4 ) === false ) {
material.iridescenceIORNode = inputs.thin_film_ior;
}
}
const emissionColor = hasNodeValue( inputs.emission_color ) ? inputs.emission_color : color( 1, 1, 1 );
const emissionLuminance = hasNodeValue( inputs.emission_luminance ) ? inputs.emission_luminance : float( 0 );
if ( isEffectivelyZero( emissionLuminance ) === false ) {
material.emissiveNode = mul( emissionColor, emissionLuminance );
}
if ( hasNodeValue( inputs.geometry_opacity ) && isEffectivelyOne( inputs.geometry_opacity ) === false ) material.transparent = true;
if ( transmissionEnabled ) material.transparent = true;
setTransmissionFlags( material, inputs.transmission_weight, inputs.geometry_opacity );
warnIgnoredInputs( inputs, mappedOpenPbrInputs, log, 'open_pbr_surface', nodeName );
}
const MaterialXSurfaceMappings = {
standard_surface: applyStandardSurface,
gltf_pbr: applyGltfPbrSurface,
open_pbr_surface: applyOpenPbrSurface,
};
const surfaceMapperRegistry = new Map( Object.entries( MaterialXSurfaceMappings ).map( ( [ category, apply ] ) => [
category,
{ category, apply },
] ) );
function getSurfaceMapper( category ) {
return surfaceMapperRegistry.get( category );
}
function getSupportedSurfaceCategories() {
return [ ...surfaceMapperRegistry.keys() ];
}
export {
MaterialXSurfaceMappings,
surfaceMapperRegistry,
getSurfaceMapper,
getSupportedSurfaceCategories,
applyStandardSurface,
applyGltfPbrSurface,
applyOpenPbrSurface,
mappedStandardSurfaceInputs,
mappedGltfPbrInputs,
mappedOpenPbrInputs,
};