three
Version:
JavaScript 3D library
1,066 lines (747 loc) • 23.6 kB
JavaScript
import { hashArray, hashString } from '../../nodes/core/NodeUtils.js';
let _id = 0;
const _protoKeysCache = new WeakMap();
const _cacheKeyValues = [ 0, 0, 0, 0, 0 ];
const _enumProperties = [ 'side', 'blending', 'combine', 'normalMapType' ];
function getKeys( obj ) {
const keys = Object.keys( obj );
let protoKeys = _protoKeysCache.get( obj.constructor );
if ( protoKeys === undefined ) {
protoKeys = [];
let proto = Object.getPrototypeOf( obj );
while ( proto ) {
const descriptors = Object.getOwnPropertyDescriptors( proto );
for ( const key in descriptors ) {
const descriptor = descriptors[ key ];
if ( descriptor && typeof descriptor.get === 'function' ) {
protoKeys.push( key );
}
}
proto = Object.getPrototypeOf( proto );
}
_protoKeysCache.set( obj.constructor, protoKeys );
}
for ( let i = 0; i < protoKeys.length; i ++ ) keys.push( protoKeys[ i ] );
return keys;
}
/**
* Returns a key for the kind of the given instance data: none, attribute or storage.
*
* @private
* @param {?BufferAttribute} attribute - The instance data.
* @return {string} The key.
*/
function getInstanceDataKey( attribute ) {
if ( attribute === null || attribute === undefined ) return '-';
return attribute.isStorageInstancedBufferAttribute === true ? 's' : 'a';
}
/**
* A render object is the renderer's representation of single entity that gets drawn
* with a draw command. There is no unique mapping of render objects to 3D objects in the
* scene since render objects also depend from the used material, the current render context
* and the current scene's lighting.
*
* In general, the basic process of the renderer is:
*
* - Analyze the 3D objects in the scene and generate render lists containing render items.
* - Process the render lists by calling one or more render commands for each render item.
* - For each render command, request a render object and perform the draw.
*
* The module provides an interface to get data required for the draw command like the actual
* draw parameters or vertex buffers. It also holds a series of caching related methods since
* creating render objects should only be done when necessary.
*
* @private
*/
class RenderObject {
/**
* Constructs a new render object.
*
* @param {NodeManager} nodes - Renderer component for managing nodes related logic.
* @param {Geometries} geometries - Renderer component for managing geometries.
* @param {Renderer} renderer - The renderer.
* @param {Object3D} object - The 3D object.
* @param {Material} material - The 3D object's material.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the object should be rendered with.
* @param {LightsNode} lightsNode - The lights node.
* @param {RenderContext} renderContext - The render context.
* @param {ClippingContext} clippingContext - The clipping context.
*/
constructor( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext ) {
this.id = _id ++;
/**
* Renderer component for managing nodes related logic.
*
* @type {NodeManager}
* @private
*/
this._nodes = nodes;
/**
* Renderer component for managing geometries.
*
* @type {Geometries}
* @private
*/
this._geometries = geometries;
/**
* The renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* The 3D object.
*
* @type {Object3D}
*/
this.object = object;
/**
* The 3D object's material.
*
* @type {Material}
*/
this.material = material;
/**
* The scene the 3D object belongs to.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera the 3D object should be rendered with.
*
* @type {Camera}
*/
this.camera = camera;
/**
* The lights node.
*
* @type {LightsNode}
*/
this.lightsNode = lightsNode;
/**
* The render context.
*
* @type {RenderContext}
*/
this.context = renderContext;
/**
* The 3D object's geometry.
*
* @type {BufferGeometry}
*/
this.geometry = object.geometry;
/**
* The geometry's version. Incremented whenever the geometry is updated.
*
* @type {number}
* @default 0
*/
this.geometryVersion = 0;
/**
* The render object's version.
*
* @type {number}
*/
this.version = material.version;
/**
* The draw range of the geometry.
*
* @type {?Object}
* @default null
*/
this.drawRange = null;
/**
* An array holding the buffer attributes
* of the render object. This entails attribute
* definitions on geometry and node level.
*
* @type {?Array<BufferAttribute>}
* @default null
*/
this.attributes = null;
/**
* An object holding the version of the
* attributes. The keys are the attribute names
* and the values are the attribute versions.
*
* @type {?Object<string, number>}
* @default null
*/
this.attributesId = null;
/**
* A reference to a render pipeline the render
* object is processed with.
*
* @type {RenderPipeline}
* @default null
*/
this.pipeline = null;
/**
* Only relevant for objects using
* multiple materials. This represents a group entry
* from the respective `BufferGeometry`.
*
* @type {?{start: number, count: number}}
* @default null
*/
this.group = null;
/**
* An array holding the vertex buffers which can
* be buffer attributes but also interleaved buffers.
*
* @type {?Array<BufferAttribute|InterleavedBuffer>}
* @default null
*/
this.vertexBuffers = null;
/**
* The parameters for the draw command.
*
* @type {?Object}
* @default null
*/
this.drawParams = null;
/**
* If this render object is used inside a render bundle,
* this property points to the respective bundle group.
*
* @type {?BundleGroup}
* @default null
*/
this.bundle = null;
/**
* The clipping context.
*
* @type {ClippingContext}
*/
this.clippingContext = clippingContext;
/**
* The clipping context's cache key.
*
* @type {string}
*/
this.clippingContextCacheKey = clippingContext !== null ? clippingContext.cacheKey : '';
/**
* The initial node cache key.
*
* @type {number}
*/
this.initialNodesCacheKey = this.getDynamicCacheKey();
/**
* The initial cache key.
*
* @type {number}
*/
this.initialCacheKey = this.getCacheKey();
/**
* The initial geometry cache key.
*
* @type {string}
*/
this.initialGeometryCacheKey = this.getGeometryCacheKey();
/**
* The node builder state.
*
* @type {?NodeBuilderState}
* @private
* @default null
*/
this._nodeBuilderState = null;
/**
* An array of bindings.
*
* @type {?Array<BindGroup>}
* @private
* @default null
*/
this._bindings = null;
/**
* Reference to the node material observer.
*
* @type {?NodeMaterialObserver}
* @private
* @default null
*/
this._monitor = null;
/**
* The object's original material when this render object is drawn with an
* override material.
*
* @type {?Material}
* @private
* @default null
*/
this._sourceMaterial = renderer._currentSourceMaterial;
/**
* The version of the source material this render object was validated against.
*
* @type {number}
*/
this.sourceMaterialVersion = this._sourceMaterial !== null ? this._sourceMaterial.version : 0;
/**
* An event listener which is defined by `RenderObjects`. It performs
* clean up tasks when `dispose()` on this render object.
*
* @method
*/
this.onDispose = null;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRenderObject = true;
/**
* An event listener which is executed when `dispose()` is called on
* the material of this render object.
*
* @method
*/
this.onMaterialDispose = () => {
this.dispose();
};
/**
* An event listener which is executed when `dispose()` is called on
* the geometry of this render object.
*
* @method
*/
this.onGeometryDispose = () => {
this._geometries.deleteNodeAttributes( this );
this._geometries.deleteVertexState( this );
// clear geometry cache attributes
this.attributes = null;
this.attributesId = null;
};
/**
* An event listener which is executed when `dispose()` is called on
* the 3D object of this render object.
*
* @method
*/
this.onObjectDispose = () => {
this.dispose();
};
/**
* An event listener which is executed when `dispose()` is called on
* the lights node of this render object, e.g. when the lighting system
* of the renderer is disposed.
*
* @method
*/
this.onLightsNodeDispose = () => {
this.dispose();
};
this.object.addEventListener( 'dispose', this.onObjectDispose );
this.material.addEventListener( 'dispose', this.onMaterialDispose );
this.geometry.addEventListener( 'dispose', this.onGeometryDispose );
this.lightsNode.addEventListener( 'dispose', this.onLightsNodeDispose );
if ( this._sourceMaterial !== null ) {
this._sourceMaterial.addEventListener( 'dispose', this.onMaterialDispose );
}
}
/**
* Updates the clipping context.
*
* @param {ClippingContext} context - The clipping context to set.
*/
updateClipping( context ) {
this.clippingContext = context;
}
/**
* Whether the clipping requires an update or not.
*
* @type {boolean}
* @readonly
*/
get clippingNeedsUpdate() {
if ( this.clippingContext === null || this.clippingContext.cacheKey === this.clippingContextCacheKey ) return false;
this.clippingContextCacheKey = this.clippingContext.cacheKey;
return true;
}
/**
* The number of clipping planes defined in context of hardware clipping.
*
* @type {number}
* @readonly
*/
get hardwareClippingPlanes() {
return this.getNodeBuilderState().hardwareClipping === true ? this.clippingContext.unionClippingCount : 0;
}
/**
* Returns the node builder state of this render object.
*
* @return {NodeBuilderState} The node builder state.
*/
getNodeBuilderState() {
return this._nodeBuilderState || ( this._nodeBuilderState = this._nodes.getForRender( this ) );
}
/**
* Returns the node material observer of this render object.
*
* @return {NodeMaterialObserver} The node material observer.
*/
getMonitor() {
return this._monitor || ( this._monitor = this.getNodeBuilderState().observer );
}
/**
* Returns an array of bind groups of this render object.
*
* @return {Array<BindGroup>} The bindings.
*/
getBindings() {
return this._bindings || ( this._bindings = this.getNodeBuilderState().createBindings( this.renderer._bindings, this.context ) );
}
/**
* Returns a binding group by group name of this render object.
*
* @param {string} name - The name of the binding group.
* @return {?BindGroup} The bindings.
*/
getBindingGroup( name ) {
for ( const bindingGroup of this.getBindings() ) {
if ( bindingGroup.name === name ) {
return bindingGroup;
}
}
}
/**
* Returns the index of the render object's geometry.
*
* @return {?BufferAttribute} The index. Returns `null` for non-indexed geometries.
*/
getIndex() {
return this._geometries.getIndex( this );
}
/**
* Returns the indirect buffer attribute.
*
* @return {?BufferAttribute} The indirect attribute. `null` if no indirect drawing is used.
*/
getIndirect() {
return this._geometries.getIndirect( this );
}
/**
* Returns the byte offset into the indirect attribute buffer.
*
* @return {number|Array<number>} The byte offset into the indirect attribute buffer.
*/
getIndirectOffset() {
return this._geometries.getIndirectOffset( this );
}
/**
* Returns an array that acts as a key for identifying the render object in a chain map.
*
* @return {Array<Object>} An array with object references.
*/
getChainArray() {
if ( this._sourceMaterial !== null ) {
return [ this.object, this.material, this.context, this.lightsNode, this._sourceMaterial ];
}
return [ this.object, this.material, this.context, this.lightsNode ];
}
/**
* This method is used when the geometry of a 3D object has been exchanged and the
* respective render object now requires an update.
*
* @param {BufferGeometry} geometry - The geometry to set.
*/
setGeometry( geometry ) {
// exchanging the geometry means we must move the dipose handler to the new geometry
this.geometry.removeEventListener( 'dispose', this.onGeometryDispose );
this.geometry = geometry;
this.geometry.addEventListener( 'dispose', this.onGeometryDispose );
this.attributes = null;
this.attributesId = null;
this.geometryVersion ++;
}
/**
* Returns the buffer attributes of the render object. The returned array holds
* attribute definitions on geometry and node level.
*
* @return {Array<BufferAttribute>} An array with buffer attributes.
*/
getAttributes() {
if ( this.attributes !== null ) return this.attributes;
const nodeAttributes = this.getNodeBuilderState().nodeAttributes;
const geometry = this.geometry;
const attributes = [];
const vertexBuffers = new Set();
const attributesId = {};
for ( const nodeAttribute of nodeAttributes ) {
let attribute;
if ( nodeAttribute.node && nodeAttribute.node.attribute ) {
// node attribute
attribute = nodeAttribute.node.getObjectAttribute( this.object );
} else {
// geometry attribute
attribute = geometry.getAttribute( nodeAttribute.name );
if ( attribute !== undefined ) {
if ( attribute.isInterleavedBufferAttribute ) {
attributesId[ nodeAttribute.name ] = attribute.data.uuid;
} else {
attributesId[ nodeAttribute.name ] = attribute.id;
}
}
}
if ( attribute === undefined ) continue;
attributes.push( attribute );
const bufferAttribute = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
vertexBuffers.add( bufferAttribute );
}
this.attributes = attributes;
this.attributesId = attributesId;
this.vertexBuffers = Array.from( vertexBuffers.values() );
return attributes;
}
/**
* Returns the vertex buffers of the render object.
*
* @return {Array<BufferAttribute|InterleavedBuffer>} An array with buffer attribute or interleaved buffers.
*/
getVertexBuffers() {
if ( this.vertexBuffers === null ) this.getAttributes();
return this.vertexBuffers;
}
/**
* Returns the draw parameters for the render object.
*
* @return {?{vertexCount: number, firstVertex: number, instanceCount: number, firstInstance: number}} The draw parameters.
*/
getDrawParameters() {
const { object, material, geometry, group, drawRange } = this;
const drawParams = this.drawParams || ( this.drawParams = {
vertexCount: 0,
firstVertex: 0,
instanceCount: 0,
firstInstance: 0
} );
const index = this.getIndex();
const hasIndex = ( index !== null );
let instanceCount = 1;
if ( geometry.isInstancedBufferGeometry === true ) {
instanceCount = geometry.instanceCount;
} else if ( object.count !== undefined ) {
instanceCount = Math.max( 0, object.count );
}
if ( instanceCount === 0 ) return null;
drawParams.instanceCount = instanceCount;
if ( object.isBatchedMesh === true ) return drawParams;
let rangeFactor = 1;
if ( material.wireframe === true && ! object.isPoints && ! object.isLineSegments && ! object.isLine && ! object.isLineLoop ) {
rangeFactor = 2;
}
let firstVertex = drawRange.start * rangeFactor;
let lastVertex = ( drawRange.start + drawRange.count ) * rangeFactor;
if ( group !== null ) {
firstVertex = Math.max( firstVertex, group.start * rangeFactor );
lastVertex = Math.min( lastVertex, ( group.start + group.count ) * rangeFactor );
}
const position = geometry.attributes.position;
let itemCount = Infinity;
if ( hasIndex ) {
itemCount = index.count;
} else if ( position !== undefined && position !== null ) {
itemCount = position.count;
}
firstVertex = Math.max( firstVertex, 0 );
lastVertex = Math.min( lastVertex, itemCount );
const count = lastVertex - firstVertex;
if ( count < 0 || count === Infinity ) return null;
drawParams.vertexCount = count;
drawParams.firstVertex = firstVertex;
return drawParams;
}
/**
* Returns the render object's geometry cache key.
*
* The geometry cache key is part of the material cache key.
*
* @return {string} The geometry cache key.
*/
getGeometryCacheKey() {
const { geometry } = this;
let cacheKey = '';
for ( const name of Object.keys( geometry.attributes ).sort() ) {
const attribute = geometry.attributes[ name ];
cacheKey += name + ',';
if ( attribute.itemSize ) cacheKey += attribute.itemSize + ',';
if ( attribute.normalized ) cacheKey += 'n,';
}
// structural equality isn't sufficient for morph targets since the
// data are maintained in textures. only if the targets are all equal
// the texture and thus the `morphReference` can be shared.
for ( const name of Object.keys( geometry.morphAttributes ).sort() ) {
const targets = geometry.morphAttributes[ name ];
cacheKey += 'morph-' + name + ',';
for ( let i = 0, l = targets.length; i < l; i ++ ) {
const attribute = targets[ i ];
cacheKey += attribute.id + ',';
}
}
return cacheKey;
}
/**
* Returns the render object's material cache key.
*
* The material cache key is part of the render object cache key.
*
* @return {number} The material cache key.
*/
getMaterialCacheKey() {
const { object, material, renderer, scene } = this;
let cacheKey = this._nodes.getCustomProgramCacheKey( material, scene );
for ( const property of getKeys( material ) ) {
if ( /^(is[A-Z]|_)|^(visible|version|uuid|name|opacity|userData|iridescenceThicknessRange)$/.test( property ) ) continue;
const value = material[ property ];
let valueKey;
if ( value !== null ) {
// some material values require a formatting
const type = typeof value;
if ( type === 'number' ) {
if ( _enumProperties.includes( property ) ) {
// retain enums since their exact value matters
valueKey = String( value );
} else {
// Other numbers are reduced to on/off
valueKey = value !== 0 ? '1' : '0';
}
} else if ( type === 'object' ) {
valueKey = '{';
if ( value.isTexture ) {
valueKey += value.mapping;
valueKey += value.channel;
// WebGPU must honor the sampler data because they are part of the bindings
if ( renderer.backend.isWebGPUBackend === true ) {
valueKey += value.magFilter;
valueKey += value.minFilter;
valueKey += value.wrapS;
valueKey += value.wrapT;
valueKey += value.wrapR;
}
}
valueKey += '}';
} else {
valueKey = String( value );
}
} else {
valueKey = String( value );
}
cacheKey += /*property + ':' +*/ valueKey + ',';
}
cacheKey += this.clippingContextCacheKey + ',';
if ( object.geometry ) {
cacheKey += this.getGeometryCacheKey();
}
if ( object.skeleton ) {
cacheKey += object.skeleton.bones.length + ',';
}
if ( object.isBatchedMesh ) {
cacheKey += object._matricesTexture.uuid + ',';
if ( object._colorsTexture !== null ) {
cacheKey += object._colorsTexture.uuid + ',';
}
}
if ( object.isInstancedMesh ) {
const { instanceMatrix, instanceColor, morphTexture } = object;
cacheKey += getInstanceDataKey( instanceMatrix ) + getInstanceDataKey( instanceColor ) + ( morphTexture !== null ? 'm' : '' ) + ',';
} else if ( object.count > 1 ) {
// TODO: https://github.com/mrdoob/three.js/pull/29066#issuecomment-2269400850
cacheKey += object.uuid + ',';
}
cacheKey += this.context.stateKey + ',';
cacheKey += object.receiveShadow + ',';
return hashString( cacheKey );
}
/**
* Whether the geometry requires an update or not.
*
* @type {boolean}
* @readonly
*/
get needsGeometryUpdate() {
if ( this.geometry.id !== this.object.geometry.id ) return true;
if ( this.attributes !== null ) {
const attributesId = this.attributesId;
for ( const name in attributesId ) {
const attribute = this.geometry.getAttribute( name );
if ( attribute === undefined ) return true;
const id = attribute.isInterleavedBufferAttribute ? attribute.data.uuid : attribute.id;
if ( attributesId[ name ] !== id ) {
return true;
}
}
for ( const { node } of this.getNodeBuilderState().nodeAttributes ) {
if ( node && node.attribute ) {
const attribute = node.getObjectAttribute( this.object );
if ( attribute && this.attributes.includes( attribute ) === false ) return true;
}
}
}
return false;
}
/**
* Whether the render object requires an update or not.
*
* Note: There are two distinct places where render objects are checked for an update.
*
* 1. In `RenderObjects.get()` which is executed when the render object is request. This
* method checks the `needsUpdate` flag and recreates the render object if necessary.
* 2. In `Renderer._renderObjectDirect()` right after getting the render object via
* `RenderObjects.get()`. The render object's NodeMaterialObserver is then used to detect
* a need for a refresh due to material, geometry or object related value changes.
*
* TODO: Investigate if it's possible to merge both steps so there is only a single place
* that performs the 'needsUpdate' check.
*
* @type {boolean}
* @readonly
*/
get needsUpdate() {
return /*this.object.static !== true &&*/ ( this.initialNodesCacheKey !== this.getDynamicCacheKey() || this.clippingNeedsUpdate );
}
/**
* Returns the dynamic cache key which represents a key that is computed per draw command.
*
* @return {number} The cache key.
*/
getDynamicCacheKey() {
let environmentKey = 0;
// `Nodes.getCacheKey()` returns an environment cache key which is not relevant when
// the renderer is inside a shadow pass.
if ( this.material.isShadowPassMaterial !== true ) {
environmentKey = this._nodes.getCacheKey( this.scene, this.lightsNode );
}
_cacheKeyValues[ 0 ] = environmentKey;
_cacheKeyValues[ 1 ] = this.camera.isArrayCamera ? this.camera.cameras.length : 0;
_cacheKeyValues[ 2 ] = this.object.receiveShadow ? 1 : 0;
_cacheKeyValues[ 3 ] = this.renderer.contextNode.id;
_cacheKeyValues[ 4 ] = this.renderer.contextNode.version;
return hashArray( _cacheKeyValues );
}
/**
* Returns the render object's cache key.
*
* @return {number} The cache key.
*/
getCacheKey() {
return this.getMaterialCacheKey() + this.getDynamicCacheKey();
}
/**
* Frees internal resources.
*/
dispose() {
this.object.removeEventListener( 'dispose', this.onObjectDispose );
this.material.removeEventListener( 'dispose', this.onMaterialDispose );
this.geometry.removeEventListener( 'dispose', this.onGeometryDispose );
this.lightsNode.removeEventListener( 'dispose', this.onLightsNodeDispose );
if ( this._sourceMaterial !== null ) {
this._sourceMaterial.removeEventListener( 'dispose', this.onMaterialDispose );
}
this.onDispose();
}
}
export default RenderObject;