import { BatchedMesh, BufferGeometry, Color, Material, Matrix4, Mesh, MeshStandardMaterial, Object3D, RawShaderMaterial } from "three"; import { isDevEnvironment, showBalloonError } from "../engine/debug/index.js"; import { Gizmos } from "../engine/engine_gizmos.js"; import { $instancingAutoUpdateBounds, $instancingRenderer, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js"; import { Context } from "../engine/engine_setup.js"; import { getParam, makeIdFromRandomWords } from "../engine/engine_utils.js"; import { NEEDLE_progressive } from "../engine/extensions/index.js"; import { GameObject } from "./Component.js"; import type { Renderer } from "./Renderer.js"; const debugInstancing = getParam("debuginstancing"); declare class InstancingSetupArgs { rend: Renderer; foundMeshes: number; useMatrixWorldAutoUpdate: boolean; }; /** * Handles instancing for Needle Engine. */ export class InstancingHandler { static readonly instance: InstancingHandler = new InstancingHandler(); /** This is the initial instance count when creating a new instancing structure. * Override this and the number of max instances that you expect for a given object. * The larger the value the more objects can be added without having to resize but it will also consume more memory. * (The instancing mesh renderer will grow x2 if the max instance count is reached) * @default 4 * @returns The initial instance count * */ static getStartInstanceCount = (_obj: Object3D) => { return 4; }; public objs: InstancedMeshRenderer[] = []; public setup(renderer: Renderer, obj: Object3D, context: Context, handlesArray: InstanceHandle[] | null, args: InstancingSetupArgs, level: number = 0) : InstanceHandle[] | null { // make sure setting casting settings are applied so when we add the mesh to the InstancedMesh we can ask for the correct cast shadow setting renderer.applySettings(obj); const res = this.tryCreateOrAddInstance(obj, context, args); if (res) { if (handlesArray === null) handlesArray = []; handlesArray.push(res); // load texture lods NEEDLE_progressive.assignTextureLOD(res.renderer.material, 0); // Load mesh lods // TODO: technically for multi meshes we do this work multiple times (we search for meshes in children and then use the renderer sharedMeshes... that doesnt make sense) for (let i = 0; i < renderer.sharedMeshes.length; i++) { const mesh = renderer.sharedMeshes[i]; const geometry = mesh.geometry; NEEDLE_progressive.assignMeshLOD(mesh, 0).then(lod => { if (lod && renderer.activeAndEnabled && geometry != lod) { res.setGeometry(lod); } }); } } else if (level <= 0 && obj.type !== "Mesh") { const nextLevel = level + 1; for (const ch of obj.children) { handlesArray = this.setup(renderer, ch, context, handlesArray, args, nextLevel); } } if (level === 0) { // For multi material objects we only want to track the root object's matrix if (args.useMatrixWorldAutoUpdate && handlesArray && handlesArray.length >= 0) { this.autoUpdateInstanceMatrix(obj); } } return handlesArray; } private tryCreateOrAddInstance(obj: Object3D, context: Context, args: InstancingSetupArgs): InstanceHandle | null { if (obj.type === "Mesh") { const index = args.foundMeshes; args.foundMeshes += 1; if (!args.rend.enableInstancing) return null; if (args.rend.enableInstancing === true) { // instancing is enabled globally // continue.... } else { if (index >= args.rend.enableInstancing.length) { if (debugInstancing) console.error("Something is wrong with instance setup", obj, args.rend.enableInstancing, index); return null; } if (!args.rend.enableInstancing[index]) { // instancing is disabled // console.log("Instancing is disabled", obj); return null; } } // instancing is enabled: const mesh = obj as Mesh; // const geo = mesh.geometry as BufferGeometry; const mat = mesh.material as Material; for (const i of this.objs) { if (!i.canAdd(mesh.geometry, mat)) continue; const handle = i.addInstance(mesh); return handle; } let maxInstances = InstancingHandler.getStartInstanceCount(obj); if (!maxInstances || maxInstances < 0) { maxInstances = 4; } let name = obj.name; if (!name?.length) name = makeIdFromRandomWords(); const i = new InstancedMeshRenderer(name, mesh.geometry, mat, maxInstances, context); this.objs.push(i); const handle = i.addInstance(mesh); return handle; } return null; } private autoUpdateInstanceMatrix(obj: Object3D) { const original = obj.matrixWorld["multiplyMatrices"].bind(obj.matrixWorld); const previousMatrix: Matrix4 = obj.matrixWorld.clone(); const matrixChangeWrapper = (a: Matrix4, b: Matrix4) => { const newMatrixWorld = original(a, b); if (obj[NEED_UPDATE_INSTANCE_KEY] || previousMatrix.equals(newMatrixWorld) === false) { previousMatrix.copy(newMatrixWorld) obj[NEED_UPDATE_INSTANCE_KEY] = true; } return newMatrixWorld; }; obj.matrixWorld["multiplyMatrices"] = matrixChangeWrapper; // wrap matrixWorldNeedsUpdate // let originalMatrixWorldNeedsUpdate = obj.matrixWorldNeedsUpdate; // Object.defineProperty(obj, "matrixWorldNeedsUpdate", { // get: () => { // return originalMatrixWorldNeedsUpdate; // }, // set: (value: boolean) => { // if(value) console.warn("SET MATRIX WORLD NEEDS UPDATE"); // originalMatrixWorldNeedsUpdate = value; // } // }); } } /** * The instance handle is used to interface with the mesh that is rendered using instancing. */ export class InstanceHandle { static readonly all: InstanceHandle[] = []; /** The name of the object */ get name(): string { return this.object.name; } get isActive() { return this.__instanceIndex >= 0; } get vertexCount() { return this.object.geometry.attributes.position.count; } get maxVertexCount() { // we get the max of the estimated MAX lod vertex count and the actual vertex count return Math.max(this.meshInformation.vertexCount, this.vertexCount); } get reservedVertexCount() { return this.__reservedVertexRange; } get indexCount() { return this.object.geometry.index ? this.object.geometry.index.count : 0; } get maxIndexCount() { // we get the max of the estimated MAX lod index count and the actual index count return Math.max(this.meshInformation.indexCount, this.indexCount); } get reservedIndexCount() { return this.__reservedIndexRange; } /** The object that is being instanced */ readonly object: Mesh; /** The instancer/BatchedMesh that is rendering this object*/ readonly renderer: InstancedMeshRenderer; /** @internal */ __instanceIndex: number = -1; /** @internal */ __reservedVertexRange: number = 0; /** @internal */ __reservedIndexRange: number = 0; __geometryIndex: number = -1; /** The mesh information of the object - this tries to also calculate the LOD info */ readonly meshInformation: MeshInformation; constructor(originalObject: Mesh, instancer: InstancedMeshRenderer) { this.__instanceIndex = -1; this.object = originalObject; this.renderer = instancer; originalObject[$instancingRenderer] = instancer; // TODO: this doesn't have LOD information *yet* in some cases - hence we can not rely on it for the max vertex counts this.meshInformation = getMeshInformation(originalObject.geometry); InstanceHandle.all.push(this); } /** Calculates the mesh information again * @returns true if the vertex count or index count has changed */ updateMeshInformation(): boolean { const newMeshInformation = getMeshInformation(this.object.geometry); const oldVertexCount = this.meshInformation.vertexCount; const oldIndexCount = this.meshInformation.indexCount; Object.assign(this.meshInformation, newMeshInformation); return oldVertexCount !== this.meshInformation.vertexCount || oldIndexCount !== this.meshInformation.indexCount; } /** Updates the matrix from the rendered object. Will also call updateWorldMatrix internally */ updateInstanceMatrix(updateChildren: boolean = false, updateMatrix: boolean = true) { if (this.__instanceIndex < 0) return; if (updateMatrix) this.object.updateWorldMatrix(true, updateChildren); this.renderer.updateInstance(this.object.matrixWorld, this.__instanceIndex); } /** Updates the matrix of the instance */ setMatrix(matrix: Matrix4) { if (this.__instanceIndex < 0) return; this.renderer.updateInstance(matrix, this.__instanceIndex); } /** Can be used to change the geometry of this instance */ setGeometry(geo: BufferGeometry) { if (this.__geometryIndex < 0) return false; const self = this; if (this.vertexCount > this.__reservedVertexRange) { return handleInvalidRange(`Instancing: Can not update geometry (${this.name}), reserved vertex range is too small: ${this.__reservedVertexRange.toLocaleString()} < ${this.vertexCount.toLocaleString()} vertices for ${this.name}`); } if (this.indexCount > this.__reservedIndexRange) { return handleInvalidRange(`Instancing: Can not update geometry (${this.name}), reserved index range is too small: ${this.__reservedIndexRange.toLocaleString()} < ${this.indexCount.toLocaleString()} indices for ${this.name}`); } return this.renderer.updateGeometry(geo, this.__geometryIndex); function handleInvalidRange(error: string): boolean { if (self.updateMeshInformation()) { // Gizmos.DrawWireSphere(self.object.worldPosition, .5, 0xff0000, 5); self.renderer.remove(self, true); // Gizmos.DrawWireSphere(self.object.worldPosition, .5, 0x33ff00, 5); // self.object.scale.multiplyScalar(10); if (self.renderer.add(self)) { return true; } } if (isDevEnvironment() || debugInstancing) { console.error(error); } return false; } } /** Adds this object to the instancing renderer (effectively activating instancing) */ add() { if (this.__instanceIndex >= 0) return; this.renderer.add(this); GameObject.markAsInstancedRendered(this.object, true); } /** Removes this object from the instancing renderer * @param delete_ If true, the instance handle will be removed from the global list */ remove(delete_: boolean) { if (this.__instanceIndex < 0) return; this.renderer.remove(this, delete_); GameObject.markAsInstancedRendered(this.object, false); if (delete_) { const i = InstanceHandle.all.indexOf(this); if (i >= 0) { InstanceHandle.all.splice(i, 1); } } } } class InstancedMeshRenderer { /** The three instanced mesh * @link https://threejs.org/docs/#api/en/objects/InstancedMesh */ get batchedMesh() { return this._batchedMesh; } get visible(): boolean { return this._batchedMesh.visible; } set visible(val: boolean) { this._batchedMesh.visible = val; } get castShadow(): boolean { return this._batchedMesh.castShadow; } set castShadow(val: boolean) { this._batchedMesh.castShadow = val; } set receiveShadow(val: boolean) { this._batchedMesh.receiveShadow = val; } /** If true, the instancer is allowed to grow when the max instance count is reached */ allowResize: boolean = true; /** The name of the instancer */ name: string = ""; /** The added geometry */ readonly geometry: BufferGeometry; /** The material used for the instanced mesh */ readonly material: Material; /** The current number of instances */ get count(): number { return this._currentInstanceCount; } /** Update the bounding box and sphere of the instanced mesh * @param box If true, update the bounding box * @param sphere If true, update the bounding sphere */ updateBounds(box: boolean = true, sphere: boolean = true) { this._needUpdateBounds = false; if (box) this._batchedMesh.computeBoundingBox(); if (sphere) this._batchedMesh.computeBoundingSphere(); if (debugInstancing && this._batchedMesh.boundingSphere) { const sphere = this._batchedMesh.boundingSphere; // const worldPos = this._batchedMesh.worldPosition.add(sphere.center); // const worldRadius = sphere!.radius; Gizmos.DrawWireSphere(sphere.center, sphere.radius, 0x00ff00); } } private _context: Context; private _batchedMesh: BatchedMesh; private _handles: (InstanceHandle | null)[] = []; private readonly _geometryIds: Map = new Map(); private _maxInstanceCount: number; private _currentInstanceCount = 0; private _currentVertexCount = 0; private _currentIndexCount = 0; private _maxVertexCount: number; private _maxIndexCount: number; private static nullMatrix: Matrix4 = new Matrix4(); /** Check if the geometry can be added to this instancer * @param geometry The geometry to check * @param material The material of the geometry * @returns true if the geometry can be added */ canAdd(geometry: BufferGeometry, material: Material): boolean { if (this._maxVertexCount > 10_000_000) return false; // The material instance must match // perhaps at some point later we *could* check if it's the same shader and properties but this would be risky if (material !== this.material) { const canMergeMaterial = false; // if (material.type === this.material.type) { // switch (material.type) { // case "MeshStandardMaterial": // // check if the material properties are the same // const m0 = this.material as MeshStandardMaterial; // const m1 = material as MeshStandardMaterial; // if(m0.map !== m1.map) return false; // // if(m0.color.equals(m1.color) === false) return false; // if(m0.roughness !== m1.roughness) return false; // if(m0.metalness !== m1.metalness) return false; // if(m0.envMap !== m1.envMap) return false; // if(m0.envMapIntensity !== m1.envMapIntensity) return false; // if(m0.lightMap !== m1.lightMap) return false; // if(m0.lightMapIntensity !== m1.lightMapIntensity) return false; // if(m0.aoMap !== m1.aoMap) return false; // if(m0.aoMapIntensity !== m1.aoMapIntensity) return false; // if(m0.emissive.equals(m1.emissive) === false) return false; // if(m0.emissiveIntensity !== m1.emissiveIntensity) return false; // if(m0.emissiveMap !== m1.emissiveMap) return false; // if(m0.bumpMap !== m1.bumpMap) return false; // if(m0.bumpScale !== m1.bumpScale) return false; // if(m0.normalMap !== m1.normalMap) return false; // if(m0.normalScale.equals(m1.normalScale) === false) return false; // if(m0.displacementMap !== m1.displacementMap) return false; // if(m0.displacementScale !== m1.displacementScale) return false; // if(m0.displacementBias !== m1.displacementBias) return false; // if(m0.roughnessMap !== m1.roughnessMap) return false; // if(m0.metalnessMap !== m1.metalnessMap) return false; // if(m0.alphaMap !== m1.alphaMap) return false; // if(m0.envMapIntensity !== m1.envMapIntensity) return false; // canMergeMaterial = true; // break; // } // } if (!canMergeMaterial) { return false; } } // if(this.geometry !== _geometry) return false; // console.log(geometry.name, geometry.uuid); if (!this.validateGeometry(geometry)) return false; // const validationMethod = this.inst["_validateGeometry"]; // if (!validationMethod) throw new Error("InstancedMesh does not have a _validateGeometry method"); // try { // validationMethod.call(this.inst, _geometry); // } // catch (err) { // // console.error(err); // return false; // } const hasSpace = !this.mustGrow(geometry); if (hasSpace) return true; if (this.allowResize) return true; return false; } private _needUpdateBounds: boolean = false; private _debugMaterial: MeshStandardMaterial | null = null; constructor(name: string, geo: BufferGeometry, material: Material, initialMaxCount: number, context: Context) { this.name = name; this.geometry = geo; this.material = material; this._context = context; this._maxInstanceCount = Math.max(2, initialMaxCount); if (debugInstancing) { this._debugMaterial = createDebugMaterial(); } const estimate = this.tryEstimateVertexCountSize(this._maxInstanceCount, [geo], initialMaxCount); this._maxVertexCount = estimate.vertexCount; this._maxIndexCount = estimate.indexCount; this._batchedMesh = new BatchedMesh(this._maxInstanceCount, this._maxVertexCount, this._maxIndexCount, this._debugMaterial ?? this.material); // this.inst = new InstancedMesh(geo, material, count); this._batchedMesh[$instancingAutoUpdateBounds] = true; // this.inst.count = 0; this._batchedMesh.visible = true; this._context.scene.add(this._batchedMesh); // Not handled by RawShaderMaterial, so we need to set the define explicitly. // Edge case: theoretically some users of the material could use it in an // instanced fashion, and some others not. In that case, the material would not // be able to be shared between the two use cases. We could probably add a // onBeforeRender call for the InstancedMesh and set the define there. // Same would apply if we support skinning - // there we would have to split instanced batches so that the ones using skinning // are all in the same batch. if (material instanceof RawShaderMaterial) { material.defines["USE_INSTANCING"] = true; material.needsUpdate = true; } context.pre_render_callbacks.push(this.onBeforeRender); context.post_render_callbacks.push(this.onAfterRender); if (debugInstancing) { console.log(`Instanced renderer created with ${this._maxInstanceCount} instances, ${this._maxVertexCount} max vertices and ${this._maxIndexCount} max indices for \"${name}\"`) } } dispose() { if (debugInstancing) console.warn("Dispose instanced renderer", this.name); this._context.scene.remove(this._batchedMesh); this._batchedMesh.dispose(); this._batchedMesh = null as any; this._handles = []; } addInstance(obj: Mesh): InstanceHandle | null { const handle = new InstanceHandle(obj, this); if (obj.castShadow === true && this._batchedMesh.castShadow === false) { this._batchedMesh.castShadow = true; } if (obj.receiveShadow === true && this._batchedMesh.receiveShadow === false) { this._batchedMesh.receiveShadow = true; } try { this.add(handle); } catch (e) { console.error(`Failed adding mesh to instancing (object name: \"${obj.name}\", instances: ${this._currentInstanceCount.toLocaleString()}/${this._maxInstanceCount.toLocaleString()}, vertices: ${this._currentVertexCount.toLocaleString()}/${this._maxVertexCount.toLocaleString()}, indices: ${this._currentIndexCount.toLocaleString()}/${this._maxIndexCount.toLocaleString()})\n`, e); if (isDevEnvironment()) { showBalloonError("Failed instancing mesh. See the browser console for details."); debugger; } return null; } return handle; } add(handle: InstanceHandle) { const geo = handle.object.geometry as BufferGeometry; if (!geo || !geo.attributes) { console.error("Cannot add object to instancing without geometry", handle.name); return false; } if (this.mustGrow(geo)) { if (this.allowResize) { this.grow(geo); } else { console.error("Cannot add instance, max count reached", this.name, this.count, this._maxInstanceCount); return false; } } handle.object.updateWorldMatrix(true, true); this.addGeometry(handle); this._handles[handle.__instanceIndex] = handle; this._currentInstanceCount += 1; this.markNeedsUpdate(); if (this._currentInstanceCount > 0) this._batchedMesh.visible = true; return true; } remove(handle: InstanceHandle, delete_: boolean) { if (!handle) { return; } if (handle.__instanceIndex < 0 || this._handles[handle.__instanceIndex] != handle || this._currentInstanceCount <= 0) { // console.warn("Cannot remove instance, handle is invalid", handle.name); return; } this.removeGeometry(handle, delete_); this._handles[handle.__instanceIndex] = null; handle.__instanceIndex = -1; if (this._currentInstanceCount > 0) { this._currentInstanceCount -= 1; } if (this._currentInstanceCount <= 0) this._batchedMesh.visible = false; this.markNeedsUpdate(); } updateInstance(mat: Matrix4, index: number) { this._batchedMesh.setMatrixAt(index, mat); this.markNeedsUpdate(); } updateGeometry(geo: BufferGeometry, geometryIndex: number): boolean { if (!this.validateGeometry(geo)) { return false; } if (this.mustGrow()) { this.grow(geo); } if (debugInstancing) console.debug("[Instancing] UPDATE GEOMETRY at " + geometryIndex, this._batchedMesh["_geometryCount"], geo.name, getMeshInformation(geo), geo.attributes.position.count, geo.index ? geo.index.count : 0); this._batchedMesh.setGeometryAt(geometryIndex, geo); // for LOD mesh updates we need to make sure to save the geometry index this._geometryIds.set(geo, geometryIndex); this.markNeedsUpdate(); return true; } private onBeforeRender = () => { // ensure the instanced mesh is rendered / has correct layers this._batchedMesh.layers.enableAll(); if (this._needUpdateBounds && this._batchedMesh[$instancingAutoUpdateBounds] === true) { if (debugInstancing === "verbose") console.log("Update instancing bounds", this.name, this._batchedMesh.matrixWorldNeedsUpdate); this.updateBounds(); } } private onAfterRender = () => { // hide the instanced mesh again when its not being rendered (for raycasting we still use the original object) this._batchedMesh.layers.disableAll(); } private validateGeometry(geometry: BufferGeometry): boolean { const batchGeometry = this.geometry; for (const attributeName in batchGeometry.attributes) { if (attributeName === "batchId") { continue; } if (!geometry.hasAttribute(attributeName)) { if (isDevEnvironment()) console.warn(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`); // geometry.setAttribute(attributeName, batchGeometry.getAttribute(attributeName).clone()); return false; // throw new Error(`BatchedMesh: Added geometry missing "${attributeName}". All geometries must have consistent attributes.`); } // const srcAttribute = geometry.getAttribute(attributeName); // const dstAttribute = batchGeometry.getAttribute(attributeName); // if (srcAttribute.itemSize !== dstAttribute.itemSize || srcAttribute.normalized !== dstAttribute.normalized) { // if (debugInstancing) throw new Error('BatchedMesh: All attributes must have a consistent itemSize and normalized value.'); // return false; // } } return true; } private markNeedsUpdate() { if (debugInstancing === "verbose") { console.warn("Marking instanced mesh dirty", this.name); } this._needUpdateBounds = true; // this.inst.instanceMatrix.needsUpdate = true; } /** * @param geo The geometry to add (if none is provided it means the geometry is already added and just updated) */ private mustGrow(geo?: BufferGeometry): boolean { if (this.count >= this._maxInstanceCount) return true; if (!geo || !geo.attributes) return false; const meshInfo = getMeshInformation(geo); const newVertexCount = meshInfo.vertexCount; const newIndexCount = meshInfo.indexCount; return this._currentVertexCount + newVertexCount > this._maxVertexCount || this._currentIndexCount + newIndexCount > this._maxIndexCount; } private grow(geometry: BufferGeometry) { const growFactor = 2; const newSize = Math.ceil(this._maxInstanceCount * growFactor); // create a new BatchedMesh instance const estimatedSpace = this.tryEstimateVertexCountSize(newSize, [geometry]);// geometry.attributes.position.count; // const indices = geometry.index ? geometry.index.count : 0; const newMaxVertexCount = Math.max(this._maxVertexCount, estimatedSpace.vertexCount); const newMaxIndexCount = Math.max(this._maxIndexCount, estimatedSpace.indexCount, Math.ceil(this._maxVertexCount * growFactor)); if (debugInstancing) { const geometryInfo = getMeshInformation(geometry); console.warn(`[Instancing] Growing Buffer\nMesh: \"${this.name}${geometry.name?.length ? "/" + geometry.name : ""}\"\n${geometryInfo.vertexCount} vertices, ${geometryInfo.indexCount} indices\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`); this._debugMaterial = createDebugMaterial(); } else if (isDevEnvironment()) { console.debug(`[Instancing] Growing Buffer\nMesh: \"${this.name}${geometry.name?.length ? "/" + geometry.name : ""}\"\nMax count ${this._maxInstanceCount} → ${newSize}\nMax vertex count ${this._maxVertexCount} -> ${newMaxVertexCount}\nMax index count ${this._maxIndexCount} -> ${newMaxIndexCount}`); } this._maxVertexCount = newMaxVertexCount; this._maxIndexCount = newMaxIndexCount; const newInst = new BatchedMesh(newSize, this._maxVertexCount, this._maxIndexCount, this._debugMaterial ?? this.material); newInst.layers = this._batchedMesh.layers; newInst.castShadow = this._batchedMesh.castShadow; newInst.receiveShadow = this._batchedMesh.receiveShadow; newInst.visible = this._batchedMesh.visible; newInst[$instancingAutoUpdateBounds] = this._batchedMesh[$instancingAutoUpdateBounds]; newInst.matrixAutoUpdate = this._batchedMesh.matrixAutoUpdate; newInst.matrixWorldNeedsUpdate = this._batchedMesh.matrixWorldNeedsUpdate; newInst.matrixAutoUpdate = this._batchedMesh.matrixAutoUpdate; newInst.matrixWorld.copy(this._batchedMesh.matrixWorld); newInst.matrix.copy(this._batchedMesh.matrix); // dispose the old batched mesh this._batchedMesh.dispose(); this._batchedMesh.removeFromParent(); this._geometryIds.clear(); this._batchedMesh = newInst; this._maxInstanceCount = newSize; // since we have a new batched mesh we need to re-add all the instances // fixes https://linear.app/needle/issue/NE-5711 // add current instances to new instanced mesh const original = [...this._handles]; this._handles = []; for (const handle of original) { if (handle && handle.__instanceIndex >= 0) { this.addGeometry(handle); this._handles[handle.__instanceIndex] = handle; } } this._context.scene.add(newInst); } private tryEstimateVertexCountSize(newMaxInstances: number, _newGeometries?: BufferGeometry[], newGeometriesFactor: number = 1): MeshInformation { /** Used geometries and how many instances use them */ const usedGeometries = new Map(); for (const handle of this._handles) { if (handle && handle.__instanceIndex >= 0 && handle.object.geometry) { if (!usedGeometries.has(handle.object.geometry as BufferGeometry)) { const data = getMeshInformation(handle.object.geometry as BufferGeometry); const meshinfo = { count: 1, ...data }; usedGeometries.set(handle.object.geometry as BufferGeometry, meshinfo); } else { const entry = usedGeometries.get(handle.object.geometry as BufferGeometry)!; entry.count += 1; } } } // then calculate the total vertex count let totalVertices = 0; let totalIndices = 0; // let maxVertices = 0; for (const [_geo, data] of usedGeometries) { totalVertices += data.vertexCount * data.count; totalIndices += data.indexCount * data.count; // maxVertices = Math.max(maxVertices, geo.attributes.position.count * count); } // we calculate the average to make an educated guess of how many vertices will be needed with the new buffer count const averageVerts = Math.ceil(totalVertices / Math.max(1, this._currentInstanceCount)); let maxVertexCount = averageVerts * newMaxInstances; const averageIndices = Math.ceil(totalIndices / Math.max(1, this._currentInstanceCount)); let maxIndexCount = averageIndices * newMaxInstances * 2; // if new geometries are provided we *know* that they will be added // so we make sure to include them in the calculation if (_newGeometries) { for (const geo of _newGeometries) { const meshinfo = getMeshInformation(geo); if (meshinfo != null) { maxVertexCount += meshinfo.vertexCount * newGeometriesFactor; maxIndexCount += meshinfo.indexCount * newGeometriesFactor; } } } return { vertexCount: maxVertexCount, indexCount: maxIndexCount }; } private addGeometry(handle: InstanceHandle) { const obj = handle.object; const geo = obj.geometry as BufferGeometry; if (!geo) { // if the geometry is null we cannot add it return; } // otherwise add more geometry / instances let geometryId = this._geometryIds.get(geo); if (geometryId === undefined || geometryId === null) { if (debugInstancing) console.debug(`[Instancing] > ADD NEW GEOMETRY \"${handle.name} (${geo.name}; ${geo.uuid})\"\n${this._currentInstanceCount} instances, ${handle.maxVertexCount} max vertices, ${handle.maxIndexCount} max indices`); geometryId = this._batchedMesh.addGeometry(geo, handle.maxVertexCount, handle.maxIndexCount); this._geometryIds.set(geo, geometryId); } else { if (debugInstancing === "verbose") console.log(`[Instancing] > ADD INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances`); } this._currentVertexCount += handle.maxVertexCount; this._currentIndexCount += handle.maxIndexCount; const i = this._batchedMesh.addInstance(geometryId); handle.__geometryIndex = geometryId; handle.__instanceIndex = i; handle.__reservedVertexRange = handle.maxVertexCount; handle.__reservedIndexRange = handle.maxIndexCount; this._batchedMesh.setMatrixAt(i, handle.object.matrixWorld); if (debugInstancing) console.debug(`[Instancing] > ADDED INSTANCE \"${handle.name}\"\nGEOMETRY_ID=${geometryId}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`); } private removeGeometry(handle: InstanceHandle, _del: boolean) { if (handle.__instanceIndex < 0) { console.warn("Cannot remove geometry, instance index is invalid", handle.name); return; } // deleteGeometry is currently not useable since there's no optimize method // https://github.com/mrdoob/three.js/issues/27985 // if (del) // this.inst.deleteGeometry(handle.__instanceIndex); // else // this._batchedMesh.setVisibleAt(handle.__instanceIndex, false); if(debugInstancing) { console.debug(`[Instancing] < REMOVE INSTANCE \"${handle.name}\" at [${handle.__instanceIndex}]\nGEOMETRY_ID=${handle.__geometryIndex}\n${this._currentInstanceCount} instances\nIndex: ${handle.__instanceIndex}`); } this._batchedMesh.deleteInstance(handle.__instanceIndex); } } declare type BucketInfo = { geometryIndex: number; vertexCount: number; indexCount: number; } declare type MeshInformation = { vertexCount: number; indexCount: number; } function getMeshInformation(geo: BufferGeometry): MeshInformation { if (!geo) { if (isDevEnvironment()) console.error("Cannot get mesh information from null geometry"); return { vertexCount: 0, indexCount: 0 }; } let vertexCount = geo.attributes?.position?.count || 0; let indexCount = geo.index ? geo.index.count : 0; const lodInfo = NEEDLE_progressive.getMeshLODInformation(geo); if (lodInfo) { const lod0 = lodInfo.lods[0]; let lod0Count = lod0.vertexCount; let lod0IndexCount = lod0.indexCount; // add some wiggle room: https://linear.app/needle/issue/NE-4505 const extra = Math.min(200, Math.ceil(lod0Count * .05)); lod0Count += extra; lod0IndexCount += 20; vertexCount = Math.max(vertexCount, lod0Count); indexCount = Math.max(indexCount, lod0IndexCount); } vertexCount = Math.ceil(vertexCount); indexCount = Math.ceil(indexCount); return { vertexCount, indexCount }; } function createDebugMaterial() { const mat = new MeshStandardMaterial({ color: new Color(Math.random(), Math.random(), Math.random()) }); mat.emissive = mat.color; mat.emissiveIntensity = .3; if (getParam("wireframe")) mat.wireframe = true; return mat; }