three
Version:
JavaScript 3D library
975 lines (630 loc) • 22.8 kB
JavaScript
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'
];
/**
* 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;
}
if ( renderObject.material.transmission > 0 ) {
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;
}
if ( materialData.transmission > 0 ) {
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;