UNPKG

@three.ez/instanced-mesh

Version:

Enhanced InstancedMesh with frustum culling, fast raycasting (using BVH), sorting, visibility management and more.

710 lines 30 kB
import { AttachedBindMode, Box3, Color, ColorManagement, DetachedBindMode, InstancedBufferAttribute, Matrix4, Mesh, Sphere, Vector3 } from 'three'; import { InstancedEntity } from './InstancedEntity.js'; import { InstancedMeshBVH } from './InstancedMeshBVH.js'; import { GLInstancedBufferAttribute } from './utils/GLInstancedBufferAttribute.js'; import { patchProperties, unpatchProperties } from './utils/PropertiesOverride.js'; import { SquareDataTexture } from './utils/SquareDataTexture.js'; /** * Alternative `InstancedMesh` class to support additional features like frustum culling, fast raycasting, LOD and more. * @template TData Type for additional instance data. * @template TGeometry Type extending `BufferGeometry`. * @template TMaterial Type extending `Material` or an array of `Material`. * @template TEventMap Type extending `Object3DEventMap`. */ export class InstancedMesh2 extends Mesh { /** * The capacity of the instance buffers. */ get capacity() { return this._capacity; } /** * The number of active instances. */ get instancesCount() { return this._instancesCount; } /** * Determines if per-instance frustum culling is enabled. * @default true */ get perObjectFrustumCulled() { return this._perObjectFrustumCulled; } set perObjectFrustumCulled(value) { this._perObjectFrustumCulled = value; this._indexArrayNeedsUpdate = true; } /** * Determines if objects should be sorted before rendering. * @default false */ get sortObjects() { return this._sortObjects; } set sortObjects(value) { this._sortObjects = value; this._indexArrayNeedsUpdate = true; } /** * An instance of `BufferGeometry` (or derived classes), defining the object's structure. */ // @ts-expect-error It's defined as a property, but is overridden as an accessor. get geometry() { return this._geometry; } set geometry(value) { this._geometry = value; this.patchGeometry(value); } /** * @remarks Geometry cannot be shared. If reused, it will be cloned. * @param geometry An instance of `BufferGeometry`. * @param material A single or an array of `Material`. * @param params Optional configuration parameters object. See `InstancedMesh2Params` for details. */ constructor(geometry, material, params = {}, LOD) { if (!geometry) throw new Error('"geometry" is mandatory.'); if (!material) throw new Error('"material" is mandatory.'); const { allowsEuler, renderer, createEntities } = params; super(geometry, null); /** * @defaultValue `InstancedMesh2` */ this.type = 'InstancedMesh2'; /** * Indicates if this is an `InstancedMesh2`. */ this.isInstancedMesh2 = true; /** * An array of `Entity` representing individual instances. * This array is only initialized if `createEntities` is set to `true` in the constructor parameters. */ this.instances = null; /** * Attribute storing indices of the instances to be rendered. */ this.instanceIndex = null; /** * Texture storing colors for instances. */ this.colorsTexture = null; /** * Texture storing morph target influences for instances. */ this.morphTexture = null; /** * Texture storing bones for instances. */ this.boneTexture = null; /** * Texture storing custom uniforms per instance. */ this.uniformsTexture = null; /** * This bounding box encloses all instances, which can be calculated with `computeBoundingBox` method. * Bounding box isn't computed by default. It needs to be explicitly computed, otherwise it's `null`. */ this.boundingBox = null; /** * This bounding sphere encloses all instances, which can be calculated with `computeBoundingSphere` method. * Bounding sphere is computed during its first render. You may need to recompute it if an instance is transformed. */ this.boundingSphere = null; /** * BVH structure for optimized culling and intersection testing. * It's possible to create the BVH using the `computeBVH` method. Once created it will be updated automatically. */ this.bvh = null; /** * Custom sort function for instances. * It's possible to create the radix sort using the `createRadixSort` method. * @default null */ this.customSort = null; /** * Flag indicating if raycasting should only consider the last frame frustum culled instances. * This is ignored if the bvh has been created. * @default false */ this.raycastOnlyFrustum = false; /** * Contains data for managing LOD, allowing different levels of detail for rendering and shadow casting. */ this.LODinfo = null; /** * Flag indicating whether to automatically perform frustum culling before rendering. * @default true */ this.autoUpdate = true; /** * Either `AttachedBindMode` or `DetachedBindMode`. `AttachedBindMode` means the skinned mesh shares the same world space as the skeleton. * This is not true when using `DetachedBindMode` which is useful when sharing a skeleton across multiple skinned meshes. * @default `AttachedBindMode` */ this.bindMode = AttachedBindMode; /** * The base matrix that is used for the bound bone transforms. */ this.bindMatrix = null; /** * The base matrix that is used for resetting the bound bone transforms. */ this.bindMatrixInverse = null; /** * Skeleton representing the bone hierarchy of the skinned mesh. */ this.skeleton = null; /** * Flag indicating whether to automatically update the BVH structure (if present). * @default true */ this.autoUpdateBVH = true; /** * Callback function called if an instance is inside the frustum. */ this.onFrustumEnter = null; /** @internal */ this._renderer = null; /** @internal */ this._instancesCount = 0; /** @internal */ this._instancesArrayCount = 0; /** @internal */ this._perObjectFrustumCulled = true; /** @internal */ this._sortObjects = false; /** @internal */ this._indexArrayNeedsUpdate = false; /** @internal */ this._useOpacity = false; this._currentMaterial = null; this._customProgramCacheKeyBase = null; this._onBeforeCompileBase = null; this._definesBase = null; this._freeIds = []; // HACK TO MAKE IT WORK WITHOUT UPDATE CORE /** @internal */ this.isInstancedMesh = true; // must be set to use instancing rendering /** @internal */ this.instanceMatrix = new InstancedBufferAttribute(new Float32Array(0), 16); // must be init to avoid exception /** @internal */ this.instanceColor = null; // must be null to avoid exception this._customProgramCacheKey = () => { return `ez_${!!this.colorsTexture}_${this._useOpacity}_${!!this.boneTexture}_${!!this.uniformsTexture}_${this._customProgramCacheKeyBase.call(this._currentMaterial)}`; }; this._onBeforeCompile = (shader, renderer) => { if (this._onBeforeCompileBase) this._onBeforeCompileBase.call(this._currentMaterial, shader, renderer); shader.defines = { ...shader.defines }; // clone to avoid problem with standard material when used for scene.overrideMaterial shader.defines['USE_INSTANCING_INDIRECT'] = ''; shader.uniforms.matricesTexture = { value: this.matricesTexture }; if (this.uniformsTexture) { shader.uniforms.uniformsTexture = { value: this.uniformsTexture }; const { vertex, fragment } = this.uniformsTexture.getUniformsGLSL('uniformsTexture', 'instanceIndex', 'uint'); shader.vertexShader = shader.vertexShader.replace('void main() {', vertex); shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragment); } if (this.colorsTexture && shader.fragmentShader.includes('#include <color_pars_fragment>')) { shader.defines['USE_INSTANCING_COLOR_INDIRECT'] = ''; shader.uniforms.colorsTexture = { value: this.colorsTexture }; shader.vertexShader = shader.vertexShader.replace('<color_vertex>', '<instanced_color_vertex>'); if (shader.vertexColors) { shader.defines['USE_VERTEX_COLOR'] = ''; } if (this._useOpacity) { shader.defines['USE_COLOR_ALPHA'] = ''; } else { shader.defines['USE_COLOR'] = ''; } } if (this.boneTexture) { shader.defines['USE_SKINNING'] = ''; shader.defines['USE_INSTANCING_SKINNING'] = ''; shader.uniforms.bindMatrix = { value: this.bindMatrix }; shader.uniforms.bindMatrixInverse = { value: this.bindMatrixInverse }; shader.uniforms.bonesPerInstance = { value: this.skeleton.bones.length }; shader.uniforms.boneTexture = { value: this.boneTexture }; } }; const capacity = params.capacity > 0 ? params.capacity : _defaultCapacity; this._renderer = renderer; this._capacity = capacity; this._parentLOD = LOD; this._geometry = geometry; this.material = material; this._allowsEuler = allowsEuler ?? false; this._tempInstance = new InstancedEntity(this, -1, allowsEuler); this.availabilityArray = LOD?.availabilityArray ?? new Array(capacity * 2); this._createEntities = createEntities; this.initLastRenderInfo(); this.initIndexAttribute(); this.initMatricesTexture(); } onBeforeShadow(renderer, scene, camera, shadowCamera, geometry, depthMaterial, group) { this.patchMaterial(renderer, depthMaterial); this.updateTextures(renderer, depthMaterial); const frame = renderer.info.render.frame; if (this.instanceIndex && this.autoUpdate && !this.frustumCullingAlreadyPerformed(frame, camera, shadowCamera)) { this.performFrustumCulling(shadowCamera, camera); } if (this.count === 0) return; this.instanceIndex.update(this._renderer, this.count); this.bindTextures(renderer, depthMaterial); } onBeforeRender(renderer, scene, camera, geometry, material, group) { this.patchMaterial(renderer, material); this.updateTextures(renderer, material); if (!this.instanceIndex) { this._renderer = renderer; return; } const frame = renderer.info.render.frame; if (this.autoUpdate && !this.frustumCullingAlreadyPerformed(frame, camera, null)) { this.performFrustumCulling(camera); } if (this.count === 0) return; this.instanceIndex.update(this._renderer, this.count); this.bindTextures(renderer, material); } onAfterShadow(renderer, scene, camera, shadowCamera, geometry, depthMaterial, group) { this.unpatchMaterial(renderer, depthMaterial); } onAfterRender(renderer, scene, camera, geometry, material, group) { this.unpatchMaterial(renderer, material); if (this.instanceIndex || (group && !this.isLastGroup(group.materialIndex))) return; this.initIndexAttribute(); } updateTextures(renderer, material) { const materialProperties = renderer.properties.get(material); this.matricesTexture.update(renderer, materialProperties, 'matricesTexture'); this.colorsTexture?.update(renderer, materialProperties, 'colorsTexture'); this.uniformsTexture?.update(renderer, materialProperties, 'uniformsTexture'); this.boneTexture?.update(renderer, materialProperties, 'boneTexture'); } bindTextures(renderer, material) { const materialProperties = renderer.properties.get(material); const materialUniforms = materialProperties.uniforms; if (!materialUniforms) return; const currentProgramProperties = materialProperties.currentProgram; const currentProgram = currentProgramProperties?.program; if (!currentProgram) return; const gl = renderer.getContext(); const programUniforms = currentProgramProperties.getUniforms().map; const activeProgram = gl.getParameter(gl.CURRENT_PROGRAM); renderer.state.useProgram(currentProgram); // set the program this.matricesTexture.bindToProgram(renderer, gl, programUniforms, materialUniforms, 'matricesTexture'); this.colorsTexture?.bindToProgram(renderer, gl, programUniforms, materialUniforms, 'colorsTexture'); this.uniformsTexture?.bindToProgram(renderer, gl, programUniforms, materialUniforms, 'uniformsTexture'); this.boneTexture?.bindToProgram(renderer, gl, programUniforms, materialUniforms, 'boneTexture'); renderer.state.useProgram(activeProgram); // restore the old program to make three.js update uniforms correctly } isLastGroup(materialIndex) { const materials = this.material; for (let i = materials.length - 1; i >= materialIndex; i--) { if (materials[i].visible) { return i === materialIndex; } } } initIndexAttribute() { if (!this._renderer) { this.count = 0; return; } const gl = this._renderer.getContext(); const capacity = this._capacity; const array = new Uint32Array(capacity); for (let i = 0; i < capacity; i++) { array[i] = i; } this.instanceIndex = new GLInstancedBufferAttribute(gl, gl.UNSIGNED_INT, 1, 4, array); this._geometry.setAttribute('instanceIndex', this.instanceIndex); } initLastRenderInfo() { if (!this._parentLOD) { this._lastRenderInfo = { frame: -1, camera: null, shadowCamera: null }; } } initMatricesTexture() { if (!this._parentLOD) { this.matricesTexture = new SquareDataTexture(Float32Array, 4, 4, this._capacity); } } initColorsTexture() { if (!this._parentLOD) { this.colorsTexture = new SquareDataTexture(Float32Array, 4, 1, this._capacity); this.colorsTexture.colorSpace = ColorManagement.workingColorSpace; this.colorsTexture._data.fill(1); this.materialsNeedsUpdate(); } } materialsNeedsUpdate() { if (this.material.isMaterial) { this.material.needsUpdate = true; return; } for (const material of this.material) { material.needsUpdate = true; } } patchGeometry(geometry) { const instanceIndex = geometry.getAttribute('instanceIndex'); // TODO fix d.ts if (instanceIndex) { if (instanceIndex === this.instanceIndex) return; console.warn('The geometry has been cloned because it was already used.'); geometry = geometry.clone(); geometry.deleteAttribute('instanceIndex'); // TODO rename it it ez_instancedIndex } if (this.instanceIndex) { geometry.setAttribute('instanceIndex', this.instanceIndex); // TODO fix d.ts } } patchMaterial(renderer, material) { this._currentMaterial = material; this._customProgramCacheKeyBase = material.customProgramCacheKey; // avoid .bind(material); to prevent memory leak this._onBeforeCompileBase = material.onBeforeCompile; this._definesBase = material.defines; material.customProgramCacheKey = this._customProgramCacheKey; material.onBeforeCompile = this._onBeforeCompile; patchProperties(this, renderer, material); } unpatchMaterial(renderer, material) { this._currentMaterial = null; unpatchProperties(renderer); material.defines = this._definesBase; material.onBeforeCompile = this._onBeforeCompileBase; material.customProgramCacheKey = this._customProgramCacheKeyBase; this._onBeforeCompileBase = null; this._customProgramCacheKeyBase = null; this._definesBase = null; } /** * Creates and computes the BVH (Bounding Volume Hierarchy) for the instances. * It's recommended to create it when all the instance matrices have been assigned. * Once created it will be updated automatically. * @param config Optional configuration parameters object. See `BVHParams` for details. */ computeBVH(config = {}) { if (!this.bvh) this.bvh = new InstancedMeshBVH(this, config.margin, config.getBBoxFromBSphere, config.accurateCulling); this.bvh.clear(); this.bvh.create(); } /** * Disposes of the BVH structure. */ disposeBVH() { this.bvh = null; } /** * Sets the local transformation matrix for a specific instance. * @param id The index of the instance. * @param matrix A `Matrix4` representing the local transformation to apply to the instance. */ setMatrixAt(id, matrix) { matrix.toArray(this.matricesTexture._data, id * 16); if (this.instances) { const instance = this.instances[id]; matrix.decompose(instance.position, instance.quaternion, instance.scale); } this.matricesTexture.enqueueUpdate(id); if (this.bvh && this.autoUpdateBVH) { this.bvh.move(id); } } /** * Gets the local transformation matrix of a specific instance. * @param id The index of the instance. * @param matrix Optional `Matrix4` to store the result. * @returns The transformation matrix of the instance. */ getMatrixAt(id, matrix = _tempMat4) { return matrix.fromArray(this.matricesTexture._data, id * 16); } /** * Retrieves the position of a specific instance. * @param index The index of the instance. * @param target Optional `Vector3` to store the result. * @returns The position of the instance as a `Vector3`. */ getPositionAt(index, target = _position) { const offset = index * 16; const array = this.matricesTexture._data; target.x = array[offset + 12]; target.y = array[offset + 13]; target.z = array[offset + 14]; return target; } /** @internal */ getPositionAndMaxScaleOnAxisAt(index, position) { const offset = index * 16; const array = this.matricesTexture._data; const te0 = array[offset + 0]; const te1 = array[offset + 1]; const te2 = array[offset + 2]; const scaleXSq = te0 * te0 + te1 * te1 + te2 * te2; const te4 = array[offset + 4]; const te5 = array[offset + 5]; const te6 = array[offset + 6]; const scaleYSq = te4 * te4 + te5 * te5 + te6 * te6; const te8 = array[offset + 8]; const te9 = array[offset + 9]; const te10 = array[offset + 10]; const scaleZSq = te8 * te8 + te9 * te9 + te10 * te10; position.x = array[offset + 12]; position.y = array[offset + 13]; position.z = array[offset + 14]; return Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq)); } /** @internal */ applyMatrixAtToSphere(index, sphere, center, radius) { const offset = index * 16; const array = this.matricesTexture._data; const te0 = array[offset + 0]; const te1 = array[offset + 1]; const te2 = array[offset + 2]; const te3 = array[offset + 3]; const te4 = array[offset + 4]; const te5 = array[offset + 5]; const te6 = array[offset + 6]; const te7 = array[offset + 7]; const te8 = array[offset + 8]; const te9 = array[offset + 9]; const te10 = array[offset + 10]; const te11 = array[offset + 11]; const te12 = array[offset + 12]; const te13 = array[offset + 13]; const te14 = array[offset + 14]; const te15 = array[offset + 15]; const position = sphere.center; const x = center.x; const y = center.y; const z = center.z; const w = 1 / (te3 * x + te7 * y + te11 * z + te15); position.x = (te0 * x + te4 * y + te8 * z + te12) * w; position.y = (te1 * x + te5 * y + te9 * z + te13) * w; position.z = (te2 * x + te6 * y + te10 * z + te14) * w; const scaleXSq = te0 * te0 + te1 * te1 + te2 * te2; const scaleYSq = te4 * te4 + te5 * te5 + te6 * te6; const scaleZSq = te8 * te8 + te9 * te9 + te10 * te10; sphere.radius = radius * Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq)); } /** * Sets the visibility of a specific instance. * @param id The index of the instance. * @param visible Whether the instance should be visible. */ setVisibilityAt(id, visible) { this.availabilityArray[id * 2] = visible; this._indexArrayNeedsUpdate = true; } /** * Gets the visibility of a specific instance. * @param id The index of the instance. * @returns Whether the instance is visible. */ getVisibilityAt(id) { return this.availabilityArray[id * 2]; } /** * Sets the availability of a specific instance. * @param id The index of the instance. * @param active Whether the instance is active (not deleted). */ setActiveAt(id, active) { this.availabilityArray[id * 2 + 1] = active; this._indexArrayNeedsUpdate = true; } /** * Gets the availability of a specific instance. * @param id The index of the instance. * @returns Whether the instance is active (not deleted). */ getActiveAt(id) { return this.availabilityArray[id * 2 + 1]; } /** * Indicates if a specific instance is visible and active. * @param id The index of the instance. * @returns Whether the instance is visible and active. */ getActiveAndVisibilityAt(id) { const offset = id * 2; const availabilityArray = this.availabilityArray; return availabilityArray[offset] && availabilityArray[offset + 1]; } /** * Set if a specific instance is visible and active. * @param id The index of the instance. * @param value Whether the instance is active and active (not deleted). */ setActiveAndVisibilityAt(id, value) { const offset = id * 2; const availabilityArray = this.availabilityArray; availabilityArray[offset] = value; availabilityArray[offset + 1] = value; this._indexArrayNeedsUpdate = true; } /** * Sets the color of a specific instance. * @param id The index of the instance. * @param color The color to assign to the instance. */ setColorAt(id, color) { if (this.colorsTexture === null) { this.initColorsTexture(); } if (color.isColor) { color.toArray(this.colorsTexture._data, id * 4); } else { _tempCol.set(color).toArray(this.colorsTexture._data, id * 4); } this.colorsTexture.enqueueUpdate(id); } /** * Gets the color of a specific instance. * @param id The index of the instance. * @param color Optional `Color` to store the result. * @returns The color of the instance. */ getColorAt(id, color = _tempCol) { return color.fromArray(this.colorsTexture._data, id * 4); } /** * Sets the opacity of a specific instance. * @param id The index of the instance. * @param value The opacity value to assign. */ setOpacityAt(id, value) { if (!this._useOpacity) { if (this.colorsTexture === null) { this.initColorsTexture(); } else { this.materialsNeedsUpdate(); } this._useOpacity = true; } this.colorsTexture._data[id * 4 + 3] = value; this.colorsTexture.enqueueUpdate(id); } /** * Gets the opacity of a specific instance. * @param id The index of the instance. * @returns The opacity of the instance. */ getOpacityAt(id) { if (!this._useOpacity) return 1; return this.colorsTexture._data[id * 4 + 3]; } /** * Copies `position`, `quaternion`, and `scale` of a specific instance to the specified target `Object3D`. * @param id The index of the instance. * @param target The `Object3D` where to copy transformation data. */ copyTo(id, target) { this.getMatrixAt(id, target.matrix).decompose(target.position, target.quaternion, target.scale); } /** * Computes the bounding box that encloses all instances, and updates the `boundingBox` attribute. */ computeBoundingBox() { const geometry = this._geometry; const count = this._instancesArrayCount; this.boundingBox ?? (this.boundingBox = new Box3()); if (geometry.boundingBox === null) geometry.computeBoundingBox(); const geoBoundingBox = geometry.boundingBox; const boundingBox = this.boundingBox; boundingBox.makeEmpty(); for (let i = 0; i < count; i++) { if (!this.getActiveAt(i)) continue; _box3.copy(geoBoundingBox).applyMatrix4(this.getMatrixAt(i)); boundingBox.union(_box3); } } /** * Computes the bounding sphere that encloses all instances, and updates the `boundingSphere` attribute. */ computeBoundingSphere() { const geometry = this._geometry; const count = this._instancesArrayCount; this.boundingSphere ?? (this.boundingSphere = new Sphere()); if (geometry.boundingSphere === null) geometry.computeBoundingSphere(); const geoBoundingSphere = geometry.boundingSphere; const boundingSphere = this.boundingSphere; boundingSphere.makeEmpty(); for (let i = 0; i < count; i++) { if (!this.getActiveAt(i)) continue; _sphere.copy(geoBoundingSphere).applyMatrix4(this.getMatrixAt(i)); boundingSphere.union(_sphere); } } clone(recursive) { const params = { capacity: this._capacity, renderer: this._renderer, allowsEuler: this._allowsEuler, createEntities: this._createEntities }; return new this.constructor(this.geometry, this.material, params).copy(this, recursive); } copy(source, recursive) { super.copy(source, recursive); this.count = source._capacity; this._instancesCount = source._instancesCount; this._instancesArrayCount = source._instancesArrayCount; this._capacity = source._capacity; if (source.boundingBox !== null) this.boundingBox = source.boundingBox.clone(); if (source.boundingSphere !== null) this.boundingSphere = source.boundingSphere.clone(); this.matricesTexture = source.matricesTexture.clone(); // TODO we can avoid cloning it because it already exists this.matricesTexture.image.data = this.matricesTexture.image.data.slice(); if (source.colorsTexture !== null) { this.colorsTexture = source.colorsTexture.clone(); this.colorsTexture.image.data = this.colorsTexture.image.data.slice(); } if (source.uniformsTexture !== null) { this.uniformsTexture = source.uniformsTexture.clone(); this.uniformsTexture.image.data = this.uniformsTexture.image.data.slice(); } if (source.morphTexture !== null) { this.morphTexture = source.morphTexture.clone(); this.morphTexture.image.data = this.morphTexture.image.data.slice(); } if (source.boneTexture !== null) { this.boneTexture = source.boneTexture.clone(); this.boneTexture.image.data = this.boneTexture.image.data.slice(); // TODO check if they fix d.ts } // TODO copies and handle LOD? return this; } /** * Frees the GPU-related resources allocated. */ dispose() { this.dispatchEvent({ type: 'dispose' }); this.matricesTexture.dispose(); this.colorsTexture?.dispose(); this.morphTexture?.dispose(); this.boneTexture?.dispose(); this.uniformsTexture?.dispose(); } updateMatrixWorld(force) { super.updateMatrixWorld(force); if (!this.bindMatrixInverse) return; if (this.bindMode === AttachedBindMode) { this.bindMatrixInverse.copy(this.matrixWorld).invert(); } else if (this.bindMode === DetachedBindMode) { this.bindMatrixInverse.copy(this.bindMatrix).invert(); } else { console.warn('Unrecognized bindMode: ' + this.bindMode); } } } const _defaultCapacity = 1000; const _box3 = new Box3(); const _sphere = new Sphere(); const _tempMat4 = new Matrix4(); const _tempCol = new Color(); const _position = new Vector3(); //# sourceMappingURL=InstancedMesh2.js.map