Accueil / npm / three@0.186.1 / src / materials / nodes / manager / NodeMaterialObserver.js

NodeMaterialObserver.js

import { RenderObjectRefreshType, DynamicDrawUsage } from '../../../constants.js';

const refreshUniforms = [
	'alphaMap',
	'alphaTest',
	'anisotropy',
	'anisotropyMap',
	'anisotropyRotation',
	'aoMap',
	'aoMapIntensity',
	'attenuationColor',
	'attenuationDistance',
	'bumpMap',
	'bumpScale',
	'clearcoat',
	'clearcoatMap',
	'clearcoatNormalMap',
	'clearcoatNormalScale',
	'clearcoatRoughness',
	'color',
	'dashOffset',
	'dashSize',
	'dispersion',
	'displacementBias',
	'displacementMap',
	'displacementScale',
	'emissive',
	'emissiveIntensity',
	'emissiveMap',
	'envMap',
	'envMapIntensity',
	'envMapRotation',
	'gapSize',
	'gradientMap',
	'ior',
	'iridescence',
	'iridescenceIOR',
	'iridescenceMap',
	'iridescenceThickness',
	'iridescenceThicknessMap',
	'lightMap',
	'lightMapIntensity',
	'linewidth',
	'map',
	'matcap',
	'metalness',
	'metalnessMap',
	'normalMap',
	'normalScale',
	'opacity',
	'reflectivity',
	'retroreflectivity',
	'rotation',
	'roughness',
	'roughnessMap',
	'scale',
	'sheen',
	'sheenColor',
	'sheenColorMap',
	'sheenRoughness',
	'sheenRoughnessMap',
	'shininess',
	'size',
	'specular',
	'specularColor',
	'specularColorMap',
	'specularIntensity',
	'specularIntensityMap',
	'specularMap',
	'steps',
	'thickness',
	'transmission',
	'transmissionMap',
	'wireframe'
];


/**
 * A WeakMap to cache lights data for node materials.
 * Cache lights data by render ID to avoid unnecessary recalculations.
 *
 * @private
 * @type {WeakMap<LightsNode,Object>}
 */
const _lightsCache = new WeakMap();

/**
 * Holds the material data for comparison.
 *
 * @private
 * @type {WeakMap<Material,Object>}
 */
const _materialCache = new WeakMap();

/**
 * Holds the geometry data for comparison.
 *
 * @private
 * @type {WeakMap<BufferGeometry,Object>}
 */
const _geometryCache = new WeakMap();

/**
 * Holds the texture data for comparison.
 *
 * @private
 * @type {WeakMap<Texture,Object>}
 */
const _textureCache = new WeakMap();

/**
 * This class is used by {@link WebGPURenderer} as management component.
 * It's primary purpose is to determine whether render objects require a
 * refresh right before they are going to be rendered or not.
 */
class NodeMaterialObserver {

	/**
	 * Constructs a new node material observer.
	 *
	 * @param {NodeBuilder} builder - The node builder.
	 */
	constructor( builder ) {

		/**
		 * A node material can be used by more than one render object so the
		 * monitor must maintain a list of render objects.
		 *
		 * @type {WeakMap<RenderObject,Object>}
		 */
		this.renderObjects = new WeakMap();

		/**
		 * Whether the material uses node objects or not.
		 *
		 * @type {boolean}
		 */
		this.hasNode = this.containsNode( builder );

		/**
		 * Whether the node builder's 3D object is animated or not.
		 *
		 * @type {boolean}
		 */
		this.hasAnimation = builder.object.isSkinnedMesh === true;

		/**
		 * A list of all possible material uniforms
		 *
		 * @type {Array<string>}
		 */
		this.refreshUniforms = refreshUniforms;

		/**
		 * Holds the current render ID from the node frame.
		 *
		 * @type {number}
		 * @default 0
		 */
		this.renderId = 0;

	}

	/**
	 * Returns `true` if the given render object is verified for the first time of this observer.
	 *
	 * @param {RenderObject} renderObject - The render object.
	 * @return {boolean} Whether the given render object is verified for the first time of this observer.
	 */
	firstInitialization( renderObject ) {

		const hasInitialized = this.renderObjects.has( renderObject );

		if ( hasInitialized === false ) {

			this.getRenderObjectData( renderObject );

			return true;

		}

		return false;

	}

	/**
	 * Returns `true` if the given 3D object uses instance buffers with dynamic draw usage.
	 * Such buffers must be uploaded once per render so the render object requires a full refresh.
	 *
	 * @param {Object3D} object - The 3D object.
	 * @return {boolean} Whether the given 3D object uses instance buffers with dynamic draw usage or not.
	 */
	hasDynamicInstancing( object ) {

		return object.isInstancedMesh === true && ( object.instanceMatrix.usage === DynamicDrawUsage ||
			( object.instanceColor !== null && object.instanceColor.usage === DynamicDrawUsage ) );

	}

	/**
	 * Returns `true` if the current rendering produces motion vectors.
	 *
	 * @param {Renderer} renderer - The renderer.
	 * @return {boolean} Whether the current rendering produces motion vectors or not.
	 */
	needsVelocity( renderer ) {

		const mrt = renderer.getMRT();

		return ( mrt !== null && mrt.has( 'velocity' ) );

	}

	/**
	 * Returns monitoring data for the given render object.
	 *
	 * @param {RenderObject} renderObject - The render object.
	 * @return {Object} The monitoring data.
	 */
	getRenderObjectData( renderObject ) {

		let data = this.renderObjects.get( renderObject );

		if ( data === undefined ) {

			const { geometry, object } = renderObject;

			data = {
				geometryId: geometry.id,
				geometryVersion: this.getGeometryData( geometry )._version,
				materialVersion: this.getMaterialData( renderObject.material )._version,
				worldMatrix: object.matrixWorld.clone()
			};

			if ( object.center ) {

				data.center = object.center.clone();

			}

			if ( object.morphTargetInfluences ) {

				data.morphTargetInfluences = object.morphTargetInfluences.slice();

			}

			if ( object.isInstancedMesh === true ) {

				data.instanceMatrixVersion = object.instanceMatrix.version;
				data.instanceColorVersion = object.instanceColor !== null ? object.instanceColor.version : null;
				data.morphTextureVersion = object.morphTexture !== null ? object.morphTexture.version : null;

			}

			if ( object.isBatchedMesh === true ) {

				data.matricesTextureVersion = object._matricesTexture.version;
				data.colorsTextureVersion = object._colorsTexture !== null ? object._colorsTexture.version : null;
				data.indirectTextureVersion = object._indirectTexture.version;

			}

			if ( renderObject.bundle !== null ) {

				data.version = renderObject.bundle.version;

			}

			// global data

			const { width, height } = renderObject.context;

			data.bufferWidth = width;
			data.bufferHeight = height;

			const { environmentIntensity, environmentRotation } = renderObject.scene;

			data.environmentIntensity = environmentIntensity;
			data.environmentRotation = environmentRotation.clone();

			data.lights = this.getLightsData( renderObject.lightsNode.getBuiltinLights(), [] );

			this.renderObjects.set( renderObject, data );

		}

		return data;

	}

	/**
	 * Returns an attribute data structure holding the attributes versions for
	 * monitoring.
	 *
	 * @param {Object} attributes - The geometry attributes.
	 * @return {Object} An object for monitoring the versions of attributes.
	 */
	getAttributesData( attributes ) {

		const attributesData = {};

		for ( const name in attributes ) {

			const attribute = attributes[ name ];

			attributesData[ name ] = {
				id: attribute.isInterleavedBufferAttribute ? attribute.data.uuid : attribute.id,
				version: attribute.isInterleavedBufferAttribute ? attribute.data.version : attribute.version,
			};

		}

		return attributesData;

	}

	/**
	 * Returns `true` if the node builder's material uses
	 * node properties.
	 *
	 * @param {NodeBuilder} builder - The current node builder.
	 * @return {boolean} Whether the node builder's material uses node properties or not.
	 */
	containsNode( builder ) {

		const material = builder.material;

		for ( const property in material ) {

			if ( material[ property ] && material[ property ].isNode )
				return true;

		}

		if ( builder.context.modelViewMatrix || builder.context.modelNormalViewMatrix || builder.context.getAO || builder.context.getShadow )
			return true;

		return false;

	}

	/**
	 * Returns a geometry data structure holding the geometry property values for
	 * monitoring.
	 *
	 * @param {BufferGeometry} geometry - The geometry.
	 * @return {Object} An object for monitoring geometry properties.
	 */
	getGeometryData( geometry ) {

		let data = _geometryCache.get( geometry );

		if ( data === undefined ) {

			data = {
				_renderId: - 1,
				_version: 0,

				attributes: this.getAttributesData( geometry.attributes ),
				indexId: geometry.index ? geometry.index.id : null,
				indexVersion: geometry.index ? geometry.index.version : null,
				drawRange: { start: geometry.drawRange.start, count: geometry.drawRange.count }
			};

			// force refresh on dispose

			geometry.addEventListener( 'dispose', () => {

				data._version ++;

			} );

			_geometryCache.set( geometry, data );

		}

		return data;

	}

	/**
	 * Returns a texture data structure holding the texture state for
	 * monitoring.
	 *
	 * @param {Texture} texture - The texture.
	 * @return {Object} An object for monitoring the texture.
	 */
	getTextureData( texture ) {

		let data = _textureCache.get( texture );

		if ( data === undefined ) {

			data = { _version: 0 };

			// force refresh on dispose

			const onDispose = () => {

				data._version ++;

			};

			if ( texture.renderTarget !== null ) {

				texture.renderTarget.addEventListener( 'dispose', onDispose );

			} else {

				texture.addEventListener( 'dispose', onDispose );

			}

			_textureCache.set( texture, data );

		}

		return data;

	}

	/**
	 * Returns a material data structure holding the material property values for
	 * monitoring.
	 *
	 * @param {Material} material - The material.
	 * @return {Object} An object for monitoring material properties.
	 */
	getMaterialData( material ) {

		let data = _materialCache.get( material );

		if ( data === undefined ) {

			data = { _renderId: - 1, _version: 0 };

			for ( const property of this.refreshUniforms ) {

				const value = material[ property ];

				if ( value === undefined ) continue;

				if ( value === null ) {

					data[ property ] = null; // track unset properties

				} else if ( typeof value === 'object' && value.clone !== undefined ) {

					if ( value.isTexture === true ) {

						data[ property ] = { id: value.id, version: 0, cacheVersion: this.getTextureData( value )._version };

					} else {

						data[ property ] = value.clone();

					}

				} else {

					data[ property ] = value;

				}

			}

			_materialCache.set( material, data );

		}

		return data;

	}

	/**
	 * Returns `true` if the given render object has not changed its state.
	 *
	 * @param {RenderObject} renderObject - The render object.
	 * @param {Array<Light>} lightsData - The current material lights.
	 * @param {number} renderId - The current render ID.
	 * @return {boolean} Whether the given render object is equal to its cached state or not.
	 */
	equals( renderObject, lightsData, renderId ) {

		const { object, material, geometry } = renderObject;

		const renderObjectData = this.getRenderObjectData( renderObject );

		// world matrix

		if ( renderObjectData.worldMatrix.equals( object.matrixWorld ) !== true ) {

			renderObjectData.worldMatrix.copy( object.matrixWorld );

			return false;

		}

		// material

		const materialData = this.getMaterialData( renderObject.material );

		// check the material properties just once per render for all render objects

		if ( materialData._renderId !== renderId ) {

			materialData._renderId = renderId;

			let changed = false;

			for ( const property in materialData ) {

				const value = materialData[ property ];
				const mtlValue = material[ property ];

				if ( property === '_renderId' ) continue;
				if ( property === '_version' ) continue;

				if ( value === null || mtlValue === null || mtlValue === undefined ) {

					// a property was assigned or removed since the last observation so a new snapshot is required

					if ( value !== ( mtlValue === undefined ? null : mtlValue ) ) {

						if ( mtlValue === null || mtlValue === undefined ) {

							materialData[ property ] = null;

						} else if ( mtlValue.isTexture === true ) {

							materialData[ property ] = { id: mtlValue.id, version: mtlValue.version, cacheVersion: this.getTextureData( mtlValue )._version };

						} else if ( typeof mtlValue === 'object' && mtlValue.clone !== undefined ) {

							materialData[ property ] = mtlValue.clone();

						} else {

							materialData[ property ] = mtlValue;

						}

						changed = true;

					}

				} else if ( value.equals !== undefined ) {

					if ( value.equals( mtlValue ) === false ) {

						value.copy( mtlValue );

						changed = true;

					}

				} else if ( mtlValue.isTexture === true ) {

					const textureData = this.getTextureData( mtlValue );

					if ( value.id !== mtlValue.id || value.version !== mtlValue.version || value.cacheVersion !== textureData._version ) {

						value.id = mtlValue.id;
						value.version = mtlValue.version;
						value.cacheVersion = textureData._version;

						changed = true;

					}

				} else if ( value !== mtlValue ) {

					materialData[ property ] = mtlValue;

					changed = true;

				}

			}

			if ( changed === true ) materialData._version ++;

		}

		// a version mismatch means the material has changed since this render object was last refreshed

		if ( renderObjectData.materialVersion !== materialData._version ) {

			renderObjectData.materialVersion = materialData._version;

			return false;

		}

		const { width, height } = renderObject.context;

		if ( renderObjectData.bufferWidth !== width || renderObjectData.bufferHeight !== height ) {

			renderObjectData.bufferWidth = width;
			renderObjectData.bufferHeight = height;

			return false;

		}

		// geometry

		if ( renderObjectData.geometryId !== geometry.id ) {

			renderObjectData.geometryId = geometry.id;
			renderObjectData.geometryVersion = this.getGeometryData( geometry )._version;

			return false;

		}

		const geometryData = this.getGeometryData( renderObject.geometry );

		// check the geometry properties just once per render for all render objects

		if ( geometryData._renderId !== renderId ) {

			geometryData._renderId = renderId;

			let changed = false;

			// attributes

			const attributes = geometry.attributes;
			const storedAttributes = geometryData.attributes;

			let currentAttributeCount = 0;
			let storedAttributeCount = 0;

			for ( const _ in attributes ) currentAttributeCount ++; // eslint-disable-line no-unused-vars

			for ( const name in storedAttributes ) {

				storedAttributeCount ++;

				const storedAttributeData = storedAttributes[ name ];
				const attribute = attributes[ name ];

				if ( attribute === undefined ) {

					// attribute was removed
					delete storedAttributes[ name ];

					changed = true;
					continue;

				}

				const id = attribute.isInterleavedBufferAttribute ? attribute.data.uuid : attribute.id;
				const version = attribute.isInterleavedBufferAttribute ? attribute.data.version : attribute.version;

				if ( storedAttributeData.id !== id || storedAttributeData.version !== version ) {

					storedAttributeData.id = id;
					storedAttributeData.version = version;

					changed = true;

				}

			}

			if ( storedAttributeCount !== currentAttributeCount ) {

				geometryData.attributes = this.getAttributesData( attributes );

				changed = true;

			}

			// check index

			const index = geometry.index;
			const currentIndexId = index ? index.id : null;
			const currentIndexVersion = index ? index.version : null;

			if ( geometryData.indexId !== currentIndexId || geometryData.indexVersion !== currentIndexVersion ) {

				geometryData.indexId = currentIndexId;
				geometryData.indexVersion = currentIndexVersion;

				changed = true;

			}

			// check drawRange

			if ( geometryData.drawRange.start !== geometry.drawRange.start || geometryData.drawRange.count !== geometry.drawRange.count ) {

				geometryData.drawRange.start = geometry.drawRange.start;
				geometryData.drawRange.count = geometry.drawRange.count;

				changed = true;

			}

			if ( changed === true ) geometryData._version ++;

		}

		// a version mismatch means the geometry has changed since this render object was last refreshed

		if ( renderObjectData.geometryVersion !== geometryData._version ) {

			renderObjectData.geometryVersion = geometryData._version;

			return false;

		}

		// morph targets

		if ( renderObjectData.morphTargetInfluences ) {

			let morphChanged = false;

			for ( let i = 0; i < renderObjectData.morphTargetInfluences.length; i ++ ) {

				if ( renderObjectData.morphTargetInfluences[ i ] !== object.morphTargetInfluences[ i ] ) {

					renderObjectData.morphTargetInfluences[ i ] = object.morphTargetInfluences[ i ];
					morphChanged = true;

				}

			}

			if ( morphChanged ) return false;

		}

		// instancing

		if ( object.isInstancedMesh === true ) {

			const instanceColorVersion = object.instanceColor !== null ? object.instanceColor.version : null;
			const morphTextureVersion = object.morphTexture !== null ? object.morphTexture.version : null;

			if ( renderObjectData.instanceMatrixVersion !== object.instanceMatrix.version ||
				renderObjectData.instanceColorVersion !== instanceColorVersion ||
				renderObjectData.morphTextureVersion !== morphTextureVersion ) {

				renderObjectData.instanceMatrixVersion = object.instanceMatrix.version;
				renderObjectData.instanceColorVersion = instanceColorVersion;
				renderObjectData.morphTextureVersion = morphTextureVersion;

				return false;

			}

		}

		// batching

		if ( object.isBatchedMesh === true ) {

			const colorsTextureVersion = object._colorsTexture !== null ? object._colorsTexture.version : null;

			if ( renderObjectData.matricesTextureVersion !== object._matricesTexture.version ||
				renderObjectData.colorsTextureVersion !== colorsTextureVersion ||
				renderObjectData.indirectTextureVersion !== object._indirectTexture.version ) {

				renderObjectData.matricesTextureVersion = object._matricesTexture.version;
				renderObjectData.colorsTextureVersion = colorsTextureVersion;
				renderObjectData.indirectTextureVersion = object._indirectTexture.version;

				return false;

			}

		}

		// lights

		if ( renderObjectData.lights ) {

			for ( let i = 0; i < lightsData.length; i ++ ) {

				const lightData = renderObjectData.lights[ i ];
				const currentLightData = lightsData[ i ];

				if ( lightData.map !== currentLightData.map || lightData.cacheVersion !== currentLightData.cacheVersion ||
					lightData.shadowMapWidth !== currentLightData.shadowMapWidth || lightData.shadowMapHeight !== currentLightData.shadowMapHeight ) {

					lightData.map = currentLightData.map;
					lightData.cacheVersion = currentLightData.cacheVersion;
					lightData.shadowMapWidth = currentLightData.shadowMapWidth;
					lightData.shadowMapHeight = currentLightData.shadowMapHeight;

					return false;

				}

			}

		}

		// scene

		const scene = renderObject.scene;

		if ( scene.environment !== null && material.envMap === null ) {

			if ( renderObjectData.environmentIntensity !== scene.environmentIntensity ||
					renderObjectData.environmentRotation.equals( scene.environmentRotation ) === false ) {

				renderObjectData.environmentIntensity = scene.environmentIntensity;
				renderObjectData.environmentRotation.copy( scene.environmentRotation );

				return false;

			}

		}

		// center

		if ( renderObjectData.center ) {

			if ( renderObjectData.center.equals( object.center ) === false ) {

				renderObjectData.center.copy( object.center );

				return false;

			}

		}

		// bundle

		if ( renderObject.bundle !== null ) {

			renderObjectData.version = renderObject.bundle.version;

		}

		return true;

	}

	/**
	 * Returns the lights data for the given material lights.
	 *
	 * @param {Array<Light>} materialLights - The material lights.
	 * @return {Array<Object>} The lights data for the given material lights.
	 */
	getLightsData( materialLights, lights ) {

		lights.length = 0;

		for ( const light of materialLights ) {

			let data = null;

			if ( light.isSpotLight === true && light.map !== null ) {

				// only add lights that have a map

				data = { map: light.map.version, cacheVersion: this.getTextureData( light.map )._version };

			}

			if ( light.castShadow === true && light.shadow !== undefined ) {

				// resizing a shadow map recreates its textures so the bindings
				// of all related render objects must be updated

				if ( data === null ) data = {};

				data.shadowMapWidth = light.shadow.mapSize.width;
				data.shadowMapHeight = light.shadow.mapSize.height;

			}

			if ( data !== null ) lights.push( data );

		}

		return lights;

	}

	/**
	 * Returns the lights for the given lights node and render ID.
	 *
	 * @param {LightsNode} lightsNode - The lights node.
	 * @param {number} renderId - The render ID.
	 * @return {Array<Object>} The lights for the given lights node and render ID.
	 */
	getLights( lightsNode, renderId ) {

		let cached = _lightsCache.get( lightsNode );

		if ( cached === undefined ) {

			cached = { renderId: - 1, lightsData: [] };
			_lightsCache.set( lightsNode, cached );

		}

		if ( cached.renderId === renderId ) {

			return cached.lightsData;

		}

		cached.renderId = renderId;
		this.getLightsData( lightsNode.getBuiltinLights(), cached.lightsData );

		return cached.lightsData;

	}

	/**
	 * Checks if the given render object requires a refresh.
	 *
	 * @param {RenderObject} renderObject - The render object.
	 * @param {NodeFrame} nodeFrame - The current node frame.
	 * @return {number} The refresh type, see {@link RenderObjectRefreshType}.
	 */
	needsRefresh( renderObject, nodeFrame ) {

		if ( this.hasNode || this.hasAnimation || this.hasDynamicInstancing( renderObject.object ) || this.firstInitialization( renderObject ) || this.needsVelocity( nodeFrame.renderer ) )
			return RenderObjectRefreshType.FULL;

		const { renderId } = nodeFrame;

		let refreshType = RenderObjectRefreshType.NONE;

		// shared UBOs are potentially never updated when objects don't change. Below block
		// make sure these UBOs are updated at least once.

		if ( this.renderId !== renderId ) {

			this.renderId = renderId;

			// no early out here. instead, use the equals() code path below so the internal cache state gets synched

			refreshType = RenderObjectRefreshType.SHARED;

		}

		const isStatic = renderObject.object.static === true;
		const isBundle = renderObject.bundle !== null && renderObject.bundle.static === true && this.getRenderObjectData( renderObject ).version === renderObject.bundle.version;

		if ( isStatic || isBundle )
			return refreshType;

		const lightsData = this.getLights( renderObject.lightsNode, renderId );

		if ( this.equals( renderObject, lightsData, renderId ) === false ) {

			refreshType = RenderObjectRefreshType.FULL;

		}

		return refreshType;

	}

}

export default NodeMaterialObserver;