614 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			614 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | import { Material, ShaderMaterial } from 'three'; | ||
|  | import { getNodeChildren, getCacheKey } from '../core/NodeUtils.js'; | ||
|  | import { attribute } from '../core/AttributeNode.js'; | ||
|  | import { output, diffuseColor, varyingProperty } from '../core/PropertyNode.js'; | ||
|  | import { materialAlphaTest, materialColor, materialOpacity, materialEmissive, materialNormal } from '../accessors/MaterialNode.js'; | ||
|  | import { modelViewProjection } from '../accessors/ModelViewProjectionNode.js'; | ||
|  | import { transformedNormalView } from '../accessors/NormalNode.js'; | ||
|  | import { instance } from '../accessors/InstanceNode.js'; | ||
|  | import { batch } from '../accessors/BatchNode.js'; | ||
|  | import { materialReference } from '../accessors/MaterialReferenceNode.js'; | ||
|  | import { positionLocal, positionView } from '../accessors/PositionNode.js'; | ||
|  | import { skinningReference } from '../accessors/SkinningNode.js'; | ||
|  | import { morphReference } from '../accessors/MorphNode.js'; | ||
|  | import { texture } from '../accessors/TextureNode.js'; | ||
|  | import { cubeTexture } from '../accessors/CubeTextureNode.js'; | ||
|  | import { lightsNode } from '../lighting/LightsNode.js'; | ||
|  | import { mix } from '../math/MathNode.js'; | ||
|  | import { float, vec3, vec4 } from '../shadernode/ShaderNode.js'; | ||
|  | import AONode from '../lighting/AONode.js'; | ||
|  | import { lightingContext } from '../lighting/LightingContextNode.js'; | ||
|  | import EnvironmentNode from '../lighting/EnvironmentNode.js'; | ||
|  | import IrradianceNode from '../lighting/IrradianceNode.js'; | ||
|  | import { depthPixel } from '../display/ViewportDepthNode.js'; | ||
|  | import { cameraLogDepth } from '../accessors/CameraNode.js'; | ||
|  | import { clipping, clippingAlpha } from '../accessors/ClippingNode.js'; | ||
|  | import { faceDirection } from '../display/FrontFacingNode.js'; | ||
|  | 
 | ||
|  | const NodeMaterials = new Map(); | ||
|  | 
 | ||
|  | class NodeMaterial extends ShaderMaterial { | ||
|  | 
 | ||
|  | 	constructor() { | ||
|  | 
 | ||
|  | 		super(); | ||
|  | 
 | ||
|  | 		this.isNodeMaterial = true; | ||
|  | 
 | ||
|  | 		this.type = this.constructor.type; | ||
|  | 
 | ||
|  | 		this.forceSinglePass = false; | ||
|  | 
 | ||
|  | 		this.fog = true; | ||
|  | 		this.lights = true; | ||
|  | 		this.normals = true; | ||
|  | 
 | ||
|  | 		this.lightsNode = null; | ||
|  | 		this.envNode = null; | ||
|  | 		this.aoNode = null; | ||
|  | 
 | ||
|  | 		this.colorNode = null; | ||
|  | 		this.normalNode = null; | ||
|  | 		this.opacityNode = null; | ||
|  | 		this.backdropNode = null; | ||
|  | 		this.backdropAlphaNode = null; | ||
|  | 		this.alphaTestNode = null; | ||
|  | 
 | ||
|  | 		this.positionNode = null; | ||
|  | 
 | ||
|  | 		this.depthNode = null; | ||
|  | 		this.shadowNode = null; | ||
|  | 		this.shadowPositionNode = null; | ||
|  | 
 | ||
|  | 		this.outputNode = null; | ||
|  | 
 | ||
|  | 		this.fragmentNode = null; | ||
|  | 		this.vertexNode = null; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	customProgramCacheKey() { | ||
|  | 
 | ||
|  | 		return this.type + getCacheKey( this ); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	build( builder ) { | ||
|  | 
 | ||
|  | 		this.setup( builder ); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setup( builder ) { | ||
|  | 
 | ||
|  | 		// < VERTEX STAGE >
 | ||
|  | 
 | ||
|  | 		builder.addStack(); | ||
|  | 
 | ||
|  | 		builder.stack.outputNode = this.vertexNode || this.setupPosition( builder ); | ||
|  | 
 | ||
|  | 		builder.addFlow( 'vertex', builder.removeStack() ); | ||
|  | 
 | ||
|  | 		// < FRAGMENT STAGE >
 | ||
|  | 
 | ||
|  | 		builder.addStack(); | ||
|  | 
 | ||
|  | 		let resultNode; | ||
|  | 
 | ||
|  | 		const clippingNode = this.setupClipping( builder ); | ||
|  | 
 | ||
|  | 		if ( this.depthWrite === true ) this.setupDepth( builder ); | ||
|  | 
 | ||
|  | 		if ( this.fragmentNode === null ) { | ||
|  | 
 | ||
|  | 			if ( this.normals === true ) this.setupNormal( builder ); | ||
|  | 
 | ||
|  | 			this.setupDiffuseColor( builder ); | ||
|  | 			this.setupVariants( builder ); | ||
|  | 
 | ||
|  | 			const outgoingLightNode = this.setupLighting( builder ); | ||
|  | 
 | ||
|  | 			if ( clippingNode !== null ) builder.stack.add( clippingNode ); | ||
|  | 
 | ||
|  | 			// force unsigned floats - useful for RenderTargets
 | ||
|  | 
 | ||
|  | 			const basicOutput = vec4( outgoingLightNode, diffuseColor.a ).max( 0 ); | ||
|  | 
 | ||
|  | 			resultNode = this.setupOutput( builder, basicOutput ); | ||
|  | 
 | ||
|  | 			// OUTPUT NODE
 | ||
|  | 
 | ||
|  | 			output.assign( resultNode ); | ||
|  | 
 | ||
|  | 			//
 | ||
|  | 
 | ||
|  | 			if ( this.outputNode !== null ) resultNode = this.outputNode; | ||
|  | 
 | ||
|  | 		} else { | ||
|  | 
 | ||
|  | 			let fragmentNode = this.fragmentNode; | ||
|  | 
 | ||
|  | 			if ( fragmentNode.isOutputStructNode !== true ) { | ||
|  | 
 | ||
|  | 				fragmentNode = vec4( fragmentNode ); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			resultNode = this.setupOutput( builder, fragmentNode ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		builder.stack.outputNode = resultNode; | ||
|  | 
 | ||
|  | 		builder.addFlow( 'fragment', builder.removeStack() ); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupClipping( builder ) { | ||
|  | 
 | ||
|  | 		if ( builder.clippingContext === null ) return null; | ||
|  | 
 | ||
|  | 		const { globalClippingCount, localClippingCount } = builder.clippingContext; | ||
|  | 
 | ||
|  | 		let result = null; | ||
|  | 
 | ||
|  | 		if ( globalClippingCount || localClippingCount ) { | ||
|  | 
 | ||
|  | 			if ( this.alphaToCoverage ) { | ||
|  | 
 | ||
|  | 				// to be added to flow when the color/alpha value has been determined
 | ||
|  | 				result = clippingAlpha(); | ||
|  | 
 | ||
|  | 			} else { | ||
|  | 
 | ||
|  | 				builder.stack.add( clipping() ); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return result; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupDepth( builder ) { | ||
|  | 
 | ||
|  | 		const { renderer } = builder; | ||
|  | 
 | ||
|  | 		// Depth
 | ||
|  | 
 | ||
|  | 		let depthNode = this.depthNode; | ||
|  | 
 | ||
|  | 		if ( depthNode === null && renderer.logarithmicDepthBuffer === true ) { | ||
|  | 
 | ||
|  | 			const fragDepth = modelViewProjection().w.add( 1 ); | ||
|  | 
 | ||
|  | 			depthNode = fragDepth.log2().mul( cameraLogDepth ).mul( 0.5 ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if ( depthNode !== null ) { | ||
|  | 
 | ||
|  | 			depthPixel.assign( depthNode ).append(); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupPosition( builder ) { | ||
|  | 
 | ||
|  | 		const { object } = builder; | ||
|  | 		const geometry = object.geometry; | ||
|  | 
 | ||
|  | 		builder.addStack(); | ||
|  | 
 | ||
|  | 		// Vertex
 | ||
|  | 
 | ||
|  | 		if ( geometry.morphAttributes.position || geometry.morphAttributes.normal || geometry.morphAttributes.color ) { | ||
|  | 
 | ||
|  | 			morphReference( object ).append(); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if ( object.isSkinnedMesh === true ) { | ||
|  | 
 | ||
|  | 			skinningReference( object ).append(); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if ( object.isBatchedMesh ) { | ||
|  | 
 | ||
|  | 			batch( object ).append(); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if ( ( object.instanceMatrix && object.instanceMatrix.isInstancedBufferAttribute === true ) && builder.isAvailable( 'instance' ) === true ) { | ||
|  | 
 | ||
|  | 			instance( object ).append(); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if ( this.positionNode !== null ) { | ||
|  | 
 | ||
|  | 			positionLocal.assign( this.positionNode ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		const mvp = modelViewProjection(); | ||
|  | 
 | ||
|  | 		builder.context.vertex = builder.removeStack(); | ||
|  | 		builder.context.mvp = mvp; | ||
|  | 
 | ||
|  | 		return mvp; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupDiffuseColor( { object, geometry } ) { | ||
|  | 
 | ||
|  | 		let colorNode = this.colorNode ? vec4( this.colorNode ) : materialColor; | ||
|  | 
 | ||
|  | 		// VERTEX COLORS
 | ||
|  | 
 | ||
|  | 		if ( this.vertexColors === true && geometry.hasAttribute( 'color' ) ) { | ||
|  | 
 | ||
|  | 			colorNode = vec4( colorNode.xyz.mul( attribute( 'color', 'vec3' ) ), colorNode.a ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// Instanced colors
 | ||
|  | 
 | ||
|  | 		if ( object.instanceColor ) { | ||
|  | 
 | ||
|  | 			const instanceColor = varyingProperty( 'vec3', 'vInstanceColor' ); | ||
|  | 
 | ||
|  | 			colorNode = instanceColor.mul( colorNode ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// COLOR
 | ||
|  | 
 | ||
|  | 		diffuseColor.assign( colorNode ); | ||
|  | 
 | ||
|  | 		// OPACITY
 | ||
|  | 
 | ||
|  | 		const opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity; | ||
|  | 		diffuseColor.a.assign( diffuseColor.a.mul( opacityNode ) ); | ||
|  | 
 | ||
|  | 		// ALPHA TEST
 | ||
|  | 
 | ||
|  | 		if ( this.alphaTestNode !== null || this.alphaTest > 0 ) { | ||
|  | 
 | ||
|  | 			const alphaTestNode = this.alphaTestNode !== null ? float( this.alphaTestNode ) : materialAlphaTest; | ||
|  | 
 | ||
|  | 			diffuseColor.a.lessThanEqual( alphaTestNode ).discard(); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupVariants( /*builder*/ ) { | ||
|  | 
 | ||
|  | 		// Interface function.
 | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupNormal() { | ||
|  | 
 | ||
|  | 		// NORMAL VIEW
 | ||
|  | 
 | ||
|  | 		if ( this.flatShading === true ) { | ||
|  | 
 | ||
|  | 			const normalNode = positionView.dFdx().cross( positionView.dFdy() ).normalize(); | ||
|  | 
 | ||
|  | 			transformedNormalView.assign( normalNode.mul( faceDirection ) ); | ||
|  | 
 | ||
|  | 		} else { | ||
|  | 
 | ||
|  | 			const normalNode = this.normalNode ? vec3( this.normalNode ) : materialNormal; | ||
|  | 
 | ||
|  | 			transformedNormalView.assign( normalNode.mul( faceDirection ) ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	getEnvNode( builder ) { | ||
|  | 
 | ||
|  | 		let node = null; | ||
|  | 
 | ||
|  | 		if ( this.envNode ) { | ||
|  | 
 | ||
|  | 			node = this.envNode; | ||
|  | 
 | ||
|  | 		} else if ( this.envMap ) { | ||
|  | 
 | ||
|  | 			node = this.envMap.isCubeTexture ? cubeTexture( this.envMap ) : texture( this.envMap ); | ||
|  | 
 | ||
|  | 		} else if ( builder.environmentNode ) { | ||
|  | 
 | ||
|  | 			node = builder.environmentNode; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return node; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupLights( builder ) { | ||
|  | 
 | ||
|  | 		const envNode = this.getEnvNode( builder ); | ||
|  | 
 | ||
|  | 		//
 | ||
|  | 
 | ||
|  | 		const materialLightsNode = []; | ||
|  | 
 | ||
|  | 		if ( envNode ) { | ||
|  | 
 | ||
|  | 			materialLightsNode.push( new EnvironmentNode( envNode ) ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if ( builder.material.lightMap ) { | ||
|  | 
 | ||
|  | 			materialLightsNode.push( new IrradianceNode( materialReference( 'lightMap', 'texture' ) ) ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if ( this.aoNode !== null || builder.material.aoMap ) { | ||
|  | 
 | ||
|  | 			const aoNode = this.aoNode !== null ? this.aoNode : texture( builder.material.aoMap ); | ||
|  | 
 | ||
|  | 			materialLightsNode.push( new AONode( aoNode ) ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		let lightsN = this.lightsNode || builder.lightsNode; | ||
|  | 
 | ||
|  | 		if ( materialLightsNode.length > 0 ) { | ||
|  | 
 | ||
|  | 			lightsN = lightsNode( [ ...lightsN.lightNodes, ...materialLightsNode ] ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return lightsN; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupLightingModel( /*builder*/ ) { | ||
|  | 
 | ||
|  | 		// Interface function.
 | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupLighting( builder ) { | ||
|  | 
 | ||
|  | 		const { material } = builder; | ||
|  | 		const { backdropNode, backdropAlphaNode, emissiveNode } = this; | ||
|  | 
 | ||
|  | 		// OUTGOING LIGHT
 | ||
|  | 
 | ||
|  | 		const lights = this.lights === true || this.lightsNode !== null; | ||
|  | 
 | ||
|  | 		const lightsNode = lights ? this.setupLights( builder ) : null; | ||
|  | 
 | ||
|  | 		let outgoingLightNode = diffuseColor.rgb; | ||
|  | 
 | ||
|  | 		if ( lightsNode && lightsNode.hasLight !== false ) { | ||
|  | 
 | ||
|  | 			const lightingModel = this.setupLightingModel( builder ); | ||
|  | 
 | ||
|  | 			outgoingLightNode = lightingContext( lightsNode, lightingModel, backdropNode, backdropAlphaNode ); | ||
|  | 
 | ||
|  | 		} else if ( backdropNode !== null ) { | ||
|  | 
 | ||
|  | 			outgoingLightNode = vec3( backdropAlphaNode !== null ? mix( outgoingLightNode, backdropNode, backdropAlphaNode ) : backdropNode ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// EMISSIVE
 | ||
|  | 
 | ||
|  | 		if ( ( emissiveNode && emissiveNode.isNode === true ) || ( material.emissive && material.emissive.isColor === true ) ) { | ||
|  | 
 | ||
|  | 			outgoingLightNode = outgoingLightNode.add( vec3( emissiveNode ? emissiveNode : materialEmissive ) ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return outgoingLightNode; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setupOutput( builder, outputNode ) { | ||
|  | 
 | ||
|  | 		// FOG
 | ||
|  | 
 | ||
|  | 		const fogNode = builder.fogNode; | ||
|  | 
 | ||
|  | 		if ( fogNode ) outputNode = vec4( fogNode.mix( outputNode.rgb, fogNode.colorNode ), outputNode.a ); | ||
|  | 
 | ||
|  | 		return outputNode; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	setDefaultValues( material ) { | ||
|  | 
 | ||
|  | 		// This approach is to reuse the native refreshUniforms*
 | ||
|  | 		// and turn available the use of features like transmission and environment in core
 | ||
|  | 
 | ||
|  | 		for ( const property in material ) { | ||
|  | 
 | ||
|  | 			const value = material[ property ]; | ||
|  | 
 | ||
|  | 			if ( this[ property ] === undefined ) { | ||
|  | 
 | ||
|  | 				this[ property ] = value; | ||
|  | 
 | ||
|  | 				if ( value && value.clone ) this[ property ] = value.clone(); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		Object.assign( this.defines, material.defines ); | ||
|  | 
 | ||
|  | 		const descriptors = Object.getOwnPropertyDescriptors( material.constructor.prototype ); | ||
|  | 
 | ||
|  | 		for ( const key in descriptors ) { | ||
|  | 
 | ||
|  | 			if ( Object.getOwnPropertyDescriptor( this.constructor.prototype, key ) === undefined && | ||
|  | 			     descriptors[ key ].get !== undefined ) { | ||
|  | 
 | ||
|  | 				Object.defineProperty( this.constructor.prototype, key, descriptors[ key ] ); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	toJSON( meta ) { | ||
|  | 
 | ||
|  | 		const isRoot = ( meta === undefined || typeof meta === 'string' ); | ||
|  | 
 | ||
|  | 		if ( isRoot ) { | ||
|  | 
 | ||
|  | 			meta = { | ||
|  | 				textures: {}, | ||
|  | 				images: {}, | ||
|  | 				nodes: {} | ||
|  | 			}; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		const data = Material.prototype.toJSON.call( this, meta ); | ||
|  | 		const nodeChildren = getNodeChildren( this ); | ||
|  | 
 | ||
|  | 		data.inputNodes = {}; | ||
|  | 
 | ||
|  | 		for ( const { property, childNode } of nodeChildren ) { | ||
|  | 
 | ||
|  | 			data.inputNodes[ property ] = childNode.toJSON( meta ).uuid; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		// TODO: Copied from Object3D.toJSON
 | ||
|  | 
 | ||
|  | 		function extractFromCache( cache ) { | ||
|  | 
 | ||
|  | 			const values = []; | ||
|  | 
 | ||
|  | 			for ( const key in cache ) { | ||
|  | 
 | ||
|  | 				const data = cache[ key ]; | ||
|  | 				delete data.metadata; | ||
|  | 				values.push( data ); | ||
|  | 
 | ||
|  | 			} | ||
|  | 
 | ||
|  | 			return values; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if ( isRoot ) { | ||
|  | 
 | ||
|  | 			const textures = extractFromCache( meta.textures ); | ||
|  | 			const images = extractFromCache( meta.images ); | ||
|  | 			const nodes = extractFromCache( meta.nodes ); | ||
|  | 
 | ||
|  | 			if ( textures.length > 0 ) data.textures = textures; | ||
|  | 			if ( images.length > 0 ) data.images = images; | ||
|  | 			if ( nodes.length > 0 ) data.nodes = nodes; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return data; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	copy( source ) { | ||
|  | 
 | ||
|  | 		this.lightsNode = source.lightsNode; | ||
|  | 		this.envNode = source.envNode; | ||
|  | 
 | ||
|  | 		this.colorNode = source.colorNode; | ||
|  | 		this.normalNode = source.normalNode; | ||
|  | 		this.opacityNode = source.opacityNode; | ||
|  | 		this.backdropNode = source.backdropNode; | ||
|  | 		this.backdropAlphaNode = source.backdropAlphaNode; | ||
|  | 		this.alphaTestNode = source.alphaTestNode; | ||
|  | 
 | ||
|  | 		this.positionNode = source.positionNode; | ||
|  | 
 | ||
|  | 		this.depthNode = source.depthNode; | ||
|  | 		this.shadowNode = source.shadowNode; | ||
|  | 		this.shadowPositionNode = source.shadowPositionNode; | ||
|  | 
 | ||
|  | 		this.outputNode = source.outputNode; | ||
|  | 
 | ||
|  | 		this.fragmentNode = source.fragmentNode; | ||
|  | 		this.vertexNode = source.vertexNode; | ||
|  | 
 | ||
|  | 		return super.copy( source ); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	static fromMaterial( material ) { | ||
|  | 
 | ||
|  | 		if ( material.isNodeMaterial === true ) { // is already a node material
 | ||
|  | 
 | ||
|  | 			return material; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		const type = material.type.replace( 'Material', 'NodeMaterial' ); | ||
|  | 
 | ||
|  | 		const nodeMaterial = createNodeMaterialFromType( type ); | ||
|  | 
 | ||
|  | 		if ( nodeMaterial === undefined ) { | ||
|  | 
 | ||
|  | 			throw new Error( `NodeMaterial: Material "${ material.type }" is not compatible.` ); | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		for ( const key in material ) { | ||
|  | 
 | ||
|  | 			nodeMaterial[ key ] = material[ key ]; | ||
|  | 
 | ||
|  | 		} | ||
|  | 
 | ||
|  | 		return nodeMaterial; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | export default NodeMaterial; | ||
|  | 
 | ||
|  | export function addNodeMaterial( type, nodeMaterial ) { | ||
|  | 
 | ||
|  | 	if ( typeof nodeMaterial !== 'function' || ! type ) throw new Error( `Node material ${ type } is not a class` ); | ||
|  | 	if ( NodeMaterials.has( type ) ) { | ||
|  | 
 | ||
|  | 		console.warn( `Redefinition of node material ${ type }` ); | ||
|  | 		return; | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	NodeMaterials.set( type, nodeMaterial ); | ||
|  | 	nodeMaterial.type = type; | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | export function createNodeMaterialFromType( type ) { | ||
|  | 
 | ||
|  | 	const Material = NodeMaterials.get( type ); | ||
|  | 
 | ||
|  | 	if ( Material !== undefined ) { | ||
|  | 
 | ||
|  | 		return new Material(); | ||
|  | 
 | ||
|  | 	} | ||
|  | 
 | ||
|  | } | ||
|  | 
 | ||
|  | addNodeMaterial( 'NodeMaterial', NodeMaterial ); |