UNPKG

three

Version:

JavaScript 3D library

396 lines (267 loc) 10.3 kB
import { InstancedBufferAttribute } from '../core/InstancedBufferAttribute.js'; import { Mesh } from './Mesh.js'; import { Box3 } from '../math/Box3.js'; import { Matrix4 } from '../math/Matrix4.js'; import { Sphere } from '../math/Sphere.js'; import { DataTexture } from '../textures/DataTexture.js'; import { FloatType, RedFormat } from '../constants.js'; const _instanceLocalMatrix = /*@__PURE__*/ new Matrix4(); const _instanceWorldMatrix = /*@__PURE__*/ new Matrix4(); const _instanceIntersects = []; const _box3 = /*@__PURE__*/ new Box3(); const _identity = /*@__PURE__*/ new Matrix4(); const _mesh = /*@__PURE__*/ new Mesh(); const _sphere = /*@__PURE__*/ new Sphere(); /** * A special version of a mesh with instanced rendering support. Use * this class if you have to render a large number of objects with the same * geometry and material(s) but with different world transformations. The usage * of 'InstancedMesh' will help you to reduce the number of draw calls and thus * improve the overall rendering performance in your application. * * @augments Mesh */ class InstancedMesh extends Mesh { /** * Constructs a new instanced mesh. * * @param {BufferGeometry} [geometry] - The mesh geometry. * @param {Material|Array<Material>} [material] - The mesh material. * @param {number} count - The number of instances. */ constructor( geometry, material, count ) { super( geometry, material ); /** * This flag can be used for type testing. * * @type {boolean} * @readonly * @default true */ this.isInstancedMesh = true; /** * Represents the local transformation of all instances. You have to set its * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data * via {@link InstancedMesh#setMatrixAt}. * * @type {InstancedBufferAttribute} */ this.instanceMatrix = new InstancedBufferAttribute( new Float32Array( count * 16 ), 16 ); /** * Represents the color of all instances. You have to set its * {@link BufferAttribute#needsUpdate} flag to true if you modify instanced data * via {@link InstancedMesh#setColorAt}. * * @type {?InstancedBufferAttribute} * @default null */ this.instanceColor = null; /** * Represents the morph target weights of all instances. You have to set its * {@link Texture#needsUpdate} flag to true if you modify instanced data * via {@link InstancedMesh#setMorphAt}. * * @type {?DataTexture} * @default null */ this.morphTexture = null; /** * The number of instances. * * @type {number} */ this.count = count; /** * The bounding box of the instanced mesh. Can be computed via {@link InstancedMesh#computeBoundingBox}. * * @type {?Box3} * @default null */ this.boundingBox = null; /** * The bounding sphere of the instanced mesh. Can be computed via {@link InstancedMesh#computeBoundingSphere}. * * @type {?Sphere} * @default null */ this.boundingSphere = null; for ( let i = 0; i < count; i ++ ) { this.setMatrixAt( i, _identity ); } } /** * Computes the bounding box of the instanced mesh, and updates {@link InstancedMesh#boundingBox}. * The bounding box is not automatically computed by the engine; this method must be called by your app. * You may need to recompute the bounding box if an instance is transformed via {@link InstancedMesh#setMatrixAt}. */ computeBoundingBox() { const geometry = this.geometry; const count = this.count; if ( this.boundingBox === null ) { this.boundingBox = new Box3(); } if ( geometry.boundingBox === null ) { geometry.computeBoundingBox(); } this.boundingBox.makeEmpty(); for ( let i = 0; i < count; i ++ ) { this.getMatrixAt( i, _instanceLocalMatrix ); _box3.copy( geometry.boundingBox ).applyMatrix4( _instanceLocalMatrix ); this.boundingBox.union( _box3 ); } } /** * Computes the bounding sphere of the instanced mesh, and updates {@link InstancedMesh#boundingSphere} * The engine automatically computes the bounding sphere when it is needed, e.g., for ray casting or view frustum culling. * You may need to recompute the bounding sphere if an instance is transformed via {@link InstancedMesh#setMatrixAt}. */ computeBoundingSphere() { const geometry = this.geometry; const count = this.count; if ( this.boundingSphere === null ) { this.boundingSphere = new Sphere(); } if ( geometry.boundingSphere === null ) { geometry.computeBoundingSphere(); } this.boundingSphere.makeEmpty(); for ( let i = 0; i < count; i ++ ) { this.getMatrixAt( i, _instanceLocalMatrix ); _sphere.copy( geometry.boundingSphere ).applyMatrix4( _instanceLocalMatrix ); this.boundingSphere.union( _sphere ); } } copy( source, recursive ) { super.copy( source, recursive ); this.instanceMatrix.copy( source.instanceMatrix ); if ( source.morphTexture !== null ) this.morphTexture = source.morphTexture.clone(); if ( source.instanceColor !== null ) this.instanceColor = source.instanceColor.clone(); this.count = source.count; if ( source.boundingBox !== null ) this.boundingBox = source.boundingBox.clone(); if ( source.boundingSphere !== null ) this.boundingSphere = source.boundingSphere.clone(); return this; } /** * Gets the color of the defined instance. * * @param {number} index - The instance index. * @param {Color} color - The target object that is used to store the method's result. */ getColorAt( index, color ) { color.fromArray( this.instanceColor.array, index * 3 ); } /** * Gets the local transformation matrix of the defined instance. * * @param {number} index - The instance index. * @param {Matrix4} matrix - The target object that is used to store the method's result. */ getMatrixAt( index, matrix ) { matrix.fromArray( this.instanceMatrix.array, index * 16 ); } /** * Gets the morph target weights of the defined instance. * * @param {number} index - The instance index. * @param {Mesh} object - The target object that is used to store the method's result. */ getMorphAt( index, object ) { const objectInfluences = object.morphTargetInfluences; const array = this.morphTexture.source.data.data; const len = objectInfluences.length + 1; // All influences + the baseInfluenceSum const dataIndex = index * len + 1; // Skip the baseInfluenceSum at the beginning for ( let i = 0; i < objectInfluences.length; i ++ ) { objectInfluences[ i ] = array[ dataIndex + i ]; } } raycast( raycaster, intersects ) { const matrixWorld = this.matrixWorld; const raycastTimes = this.count; _mesh.geometry = this.geometry; _mesh.material = this.material; if ( _mesh.material === undefined ) return; // test with bounding sphere first if ( this.boundingSphere === null ) this.computeBoundingSphere(); _sphere.copy( this.boundingSphere ); _sphere.applyMatrix4( matrixWorld ); if ( raycaster.ray.intersectsSphere( _sphere ) === false ) return; // now test each instance for ( let instanceId = 0; instanceId < raycastTimes; instanceId ++ ) { // calculate the world matrix for each instance this.getMatrixAt( instanceId, _instanceLocalMatrix ); _instanceWorldMatrix.multiplyMatrices( matrixWorld, _instanceLocalMatrix ); // the mesh represents this single instance _mesh.matrixWorld = _instanceWorldMatrix; _mesh.raycast( raycaster, _instanceIntersects ); // process the result of raycast for ( let i = 0, l = _instanceIntersects.length; i < l; i ++ ) { const intersect = _instanceIntersects[ i ]; intersect.instanceId = instanceId; intersect.object = this; intersects.push( intersect ); } _instanceIntersects.length = 0; } } /** * Sets the given color to the defined instance. Make sure you set the `needsUpdate` flag of * {@link InstancedMesh#instanceColor} to `true` after updating all the colors. * * @param {number} index - The instance index. * @param {Color} color - The instance color. */ setColorAt( index, color ) { if ( this.instanceColor === null ) { this.instanceColor = new InstancedBufferAttribute( new Float32Array( this.instanceMatrix.count * 3 ).fill( 1 ), 3 ); } color.toArray( this.instanceColor.array, index * 3 ); } /** * Sets the given local transformation matrix to the defined instance. Make sure you set the `needsUpdate` flag of * {@link InstancedMesh#instanceMatrix} to `true` after updating all the colors. * * @param {number} index - The instance index. * @param {Matrix4} matrix - The local transformation. */ setMatrixAt( index, matrix ) { matrix.toArray( this.instanceMatrix.array, index * 16 ); } /** * Sets the morph target weights to the defined instance. Make sure you set the `needsUpdate` flag of * {@link InstancedMesh#morphTexture} to `true` after updating all the influences. * * @param {number} index - The instance index. * @param {Mesh} object - A mesh which `morphTargetInfluences` property containing the morph target weights * of a single instance. */ setMorphAt( index, object ) { const objectInfluences = object.morphTargetInfluences; const len = objectInfluences.length + 1; // morphBaseInfluence + all influences if ( this.morphTexture === null ) { this.morphTexture = new DataTexture( new Float32Array( len * this.count ), len, this.count, RedFormat, FloatType ); } const array = this.morphTexture.source.data.data; let morphInfluencesSum = 0; for ( let i = 0; i < objectInfluences.length; i ++ ) { morphInfluencesSum += objectInfluences[ i ]; } const morphBaseInfluence = this.geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum; const dataIndex = len * index; array[ dataIndex ] = morphBaseInfluence; array.set( objectInfluences, dataIndex + 1 ); } updateMorphTargets() { } /** * Frees the GPU-related resources allocated by this instance. Call this * method whenever this instance is no longer used in your app. */ dispose() { this.dispatchEvent( { type: 'dispose' } ); if ( this.morphTexture !== null ) { this.morphTexture.dispose(); this.morphTexture = null; } } } export { InstancedMesh };