@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
856 lines (739 loc) • 37.8 kB
text/typescript
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<BufferGeometry, number> = 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<BufferGeometry, MeshInformation & { count: number }>();
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.getMeshLODExtension(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;
}