From d70867c0628d5a4d7be59e2b487abbc2c696b83a Mon Sep 17 00:00:00 2001 From: Andrea Gargaro Date: Sat, 2 Nov 2024 11:07:13 +0100 Subject: [PATCH] InstancedMeshLOD (#23) issue #22 --- examples/LOD.ts | 6 +- examples/trees.ts | 109 +++-------- index.html | 2 +- src/index.ts | 1 - src/objects/InstancedMesh2.ts | 210 ++++++++++++++++++++- src/objects/InstancedMeshLOD.ts | 312 -------------------------------- 6 files changed, 233 insertions(+), 407 deletions(-) delete mode 100644 src/objects/InstancedMeshLOD.ts diff --git a/examples/LOD.ts b/examples/LOD.ts index 014a379..864df86 100644 --- a/examples/LOD.ts +++ b/examples/LOD.ts @@ -1,7 +1,7 @@ import { Main, PerspectiveCameraAuto } from '@three.ez/main'; import { AmbientLight, DirectionalLight, MeshLambertMaterial, Scene, SphereGeometry } from 'three'; import { OrbitControls } from 'three/examples/jsm/Addons.js'; -import { InstancedMeshLOD } from '../src/index.js'; +import { InstancedMesh2 } from '../src/index.js'; import { PRNG } from './objects/random.js'; const spawnRange = 10000; @@ -14,14 +14,14 @@ const controls = new OrbitControls(camera, main.renderer.domElement); const scene = new Scene(); -const instancedMeshLOD = new InstancedMeshLOD(main.renderer, 1000000); +const instancedMeshLOD = new InstancedMesh2(main.renderer, 1000000, new SphereGeometry(5, 30, 15), new MeshLambertMaterial({ color: 'green' })); instancedMeshLOD.addLevel(new SphereGeometry(5, 30, 15), new MeshLambertMaterial({ color: 'green' })); instancedMeshLOD.addLevel(new SphereGeometry(5, 20, 10), new MeshLambertMaterial({ color: 'yellow' }), 50); instancedMeshLOD.addLevel(new SphereGeometry(5, 10, 5), new MeshLambertMaterial({ color: 'orange' }), 500); instancedMeshLOD.addLevel(new SphereGeometry(5, 5, 3), new MeshLambertMaterial({ color: 'red' }), 1000); -instancedMeshLOD.levels[0].object.geometry.computeBoundingSphere(); // improve +instancedMeshLOD._levels[0].object.geometry.computeBoundingSphere(); // improve instancedMeshLOD.updateInstances((object, index) => { object.position.x = random.range(-spawnRange, spawnRange); diff --git a/examples/trees.ts b/examples/trees.ts index ec2f681..cd31a2b 100644 --- a/examples/trees.ts +++ b/examples/trees.ts @@ -1,101 +1,47 @@ import { Asset, Main, PerspectiveCameraAuto } from '@three.ez/main'; -import { ACESFilmicToneMapping, AmbientLight, BufferGeometry, DirectionalLight, FogExp2, Mesh, MeshLambertMaterial, MeshStandardMaterial, PCFSoftShadowMap, PlaneGeometry, Scene, Vector3 } from 'three'; +import { ACESFilmicToneMapping, AmbientLight, BoxGeometry, BufferGeometry, DirectionalLight, FogExp2, Mesh, MeshLambertMaterial, MeshStandardMaterial, PCFSoftShadowMap, PlaneGeometry, Scene, Vector3 } from 'three'; import { MapControls } from 'three/examples/jsm/controls/MapControls.js'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { Sky } from 'three/examples/jsm/objects/Sky.js'; -import { InstancedMeshLOD } from '../src/index.js'; -import { PRNG } from './objects/random.js'; +import { InstancedMesh2 } from '../src/index.js'; const count = 1000000; -const terrainSize = 50000; -const random = new PRNG(10000); +const terrainSize = 125000; -const main = new Main({ rendererParameters: { antialias: true } }); // init renderer and other stuff +const main = new Main(); // init renderer and other stuff main.renderer.toneMapping = ACESFilmicToneMapping; main.renderer.toneMappingExposure = 0.5; main.renderer.shadowMap.enabled = true; main.renderer.shadowMap.type = PCFSoftShadowMap; -const camera = new PerspectiveCameraAuto(70, 0.1, 2000); +const camera = new PerspectiveCameraAuto(50, 0.1, 1000).translateY(1); const scene = new Scene(); -// const treeGLTF = (await Asset.load(GLTFLoader, 'tree.glb')).scene.children[0] as Mesh; +const treeGLTF = (await Asset.load(GLTFLoader, 'tree.glb')).scene.children[0] as Mesh; -// const trees = new InstancedMesh2(main.renderer, count, treeGLTF.geometry, treeGLTF.material); -// trees.castShadow = true; -// trees.cursor = 'pointer'; +const trees = new InstancedMesh2(main.renderer, count, treeGLTF.geometry, treeGLTF.material); +trees.castShadow = true; +trees.cursor = 'pointer'; -// trees.createInstances((obj, index) => { -// obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2); -// obj.scale.setScalar(Math.random() * 0.1 + 0.1); -// obj.rotateY(Math.random() * Math.PI * 2).rotateZ(Math.random() * 0.3 - 0.15); -// }); +trees.addLevel(new BoxGeometry(100, 1000, 100), new MeshLambertMaterial(), 100); +trees._levels[0].object.geometry.computeBoundingSphere(); // improve +trees._levels[1].object.castShadow = true; -// trees.computeBVH(); - -// trees.on('click', (e) => { -// trees.instances[e.intersection.instanceId].visible = false; -// }); - - - -const treeHigh = (await Asset.load(GLTFLoader, 'tree.glb')).scene.children[0]; -const treeMid = (await Asset.load(GLTFLoader, 'tree_mid.glb')).scene.children[0]; -const treeLow = (await Asset.load(GLTFLoader, 'tree_far.glb')).scene.children[0]; - -const trunkHigh = treeHigh.children[0] as Mesh; -const trunkMid = treeMid.children[0] as Mesh; -const trunkLow = treeLow.children[0] as Mesh; - -const leavesHigh = treeHigh.children[1] as Mesh; -const leavesMid = treeMid.children[1] as Mesh; -const leavesLow = treeLow.children[1] as Mesh; - -leavesHigh.material.transparent = leavesMid.material.transparent = leavesLow.material.transparent = false; -leavesHigh.material.alphaTest = leavesMid.material.alphaTest = leavesLow.material.alphaTest = 0.2; -leavesHigh.material.depthWrite = leavesMid.material.depthWrite = leavesLow.material.depthWrite = true; - -const trunkLOD = new InstancedMeshLOD(main.renderer, count); -trunkLOD.addLevel(trunkHigh.geometry, trunkHigh.material); -trunkLOD.addLevel(trunkMid.geometry, trunkMid.material, 100); -trunkLOD.addLevel(trunkLow.geometry, trunkLow.material, 200); -trunkLOD.levels[0].object.geometry.computeBoundingSphere(); // improve - -const leavesLOD = new InstancedMeshLOD(main.renderer, count); -leavesLOD.addLevel(leavesHigh.geometry, leavesHigh.material); -// leavesLOD.addLevel(leavesMid.geometry, leavesMid.material, 500); -leavesLOD.addLevel(leavesLow.geometry, leavesLow.material, 500); -leavesLOD.levels[0].object.geometry.computeBoundingSphere(); // improve - -trunkLOD.levels[0].object.castShadow = true; -trunkLOD.levels[1].object.castShadow = true; -trunkLOD.levels[2].object.castShadow = true; -leavesLOD.levels[0].object.castShadow = true; -leavesLOD.levels[1].object.castShadow = true; -// leavesLOD.levels[2].object.castShadow = true; - - - -trunkLOD.updateInstances((obj, index) => { - obj.position.x = random.range(-terrainSize / 2, terrainSize / 2); - obj.position.z = random.range(-terrainSize / 2, terrainSize / 2); - obj.scale.multiplyScalar(random.range(5, 10)); - obj.rotateY(random.range(0, Math.PI * 2)).rotateZ(random.range(-0.1, 0.1)); +trees.createInstances((obj, index) => { + obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2); + obj.scale.setScalar(Math.random() * 0.01 + 0.01); + obj.rotateY(Math.random() * Math.PI * 2).rotateZ(Math.random() * 0.3 - 0.15); }); -for (let i = 0; i < leavesLOD.maxCount; i++) { - leavesLOD.setMatrixAt(i, trunkLOD.getMatrixAt(i)) -} - -trunkLOD.computeBVH(); -leavesLOD.computeBVH(); // it would be better use only one BVH - - +trees.computeBVH(); +trees.on('click', (e) => { + trees.instances[e.intersection.instanceId].visible = false; +}); const ground = new Mesh(new PlaneGeometry(terrainSize, terrainSize, 10, 10), new MeshLambertMaterial({ color: 0x004622 })); -// ground.interceptByRaycaster = false; +ground.interceptByRaycaster = false; ground.receiveShadow = true; ground.rotateX(Math.PI / -2); @@ -132,20 +78,19 @@ dirLight.on('animate', (e) => { dirLight.target.position.copy(camera.position).sub(sunOffset); }); -scene.add(sky, trunkLOD, leavesLOD, ground, new AmbientLight(), dirLight, dirLight.target); +scene.add(sky, trees, ground, new AmbientLight(), dirLight, dirLight.target); -// main.createView({ scene, camera, onAfterRender: () => treeCount.updateDisplay() }); -main.createView({ scene, camera, enabled: false }); +main.createView({ scene, camera, enabled: false, onAfterRender: () => treeCount.updateDisplay() }); const controls = new MapControls(camera, main.renderer.domElement); controls.maxPolarAngle = Math.PI / 2.1; controls.minDistance = 10; controls.maxDistance = 100; controls.panSpeed = 10; -controls.target.set(-5, 20, 20); +controls.target.set(-25, 10, 10); controls.update(); const gui = new GUI(); -// gui.add(trees.instances as any, 'length').name('instances total').disable(); -// const treeCount = gui.add(trees, 'count').name('instances rendered').disable(); -gui.add(camera, 'far', 500, 10000, 100).name('camera far').onChange(() => camera.updateProjectionMatrix()); +gui.add(trees.instances as any, 'length').name('instances total').disable(); +const treeCount = gui.add(trees, 'count').name('instances rendered').disable(); +gui.add(camera, 'far', 2000, 10000, 100).name('camera far').onChange(() => camera.updateProjectionMatrix()); diff --git a/index.html b/index.html index aedb09a..3fae8f6 100644 --- a/index.html +++ b/index.html @@ -31,7 +31,7 @@ - + diff --git a/src/index.ts b/src/index.ts index 208b877..26c9620 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,6 @@ export * from './shaders/chunks/instanced_pars_vertex.glsl.js'; export * from './shaders/chunks/instanced_vertex.glsl.js'; export * from './utils/createTexture.js'; export * from './utils/createRadixSort.js'; -export * from './objects/InstancedMeshLOD.js'; /** @internal */ declare module 'three' { diff --git a/src/objects/InstancedMesh2.ts b/src/objects/InstancedMesh2.ts index 76b29db..ea06f17 100644 --- a/src/objects/InstancedMesh2.ts +++ b/src/objects/InstancedMesh2.ts @@ -1,22 +1,38 @@ import { BVHNode } from "bvh.js"; import { Box3, BufferAttribute, BufferGeometry, Camera, Color, ColorManagement, ColorRepresentation, DataTexture, FloatType, Frustum, Group, InstancedBufferAttribute, Intersection, Material, Matrix4, Mesh, MeshDepthMaterial, MeshDistanceMaterial, Object3D, Object3DEventMap, RGBADepthPacking, RGFormat, Ray, Raycaster, RedFormat, Scene, ShaderMaterial, Sphere, Vector3, WebGLRenderer } from "three"; import { createTexture_mat4, createTexture_vec4 } from "../utils/createTexture.js"; +import { getMaxScaleOnAxisAt, getPositionAt } from "../utils/matrixUtils.js"; import { GLInstancedBufferAttribute } from "./GLInstancedBufferAttribute.js"; import { InstancedEntity, UniformValue, UniformValueNoNumber } from "./InstancedEntity.js"; import { InstancedMeshBVH } from "./InstancedMeshBVH.js"; -import { InstancedMeshLOD } from "./InstancedMeshLOD.js"; import { InstancedRenderItem, InstancedRenderList } from "./InstancedRenderList.js"; -import { getMaxScaleOnAxisAt, getPositionAt } from "../utils/matrixUtils.js"; // TODO: Add expand and count/maxCount when create? // TODO: partial texture update // TODO: Use BVH only for raycasting // TODO: patchGeometry method +// TODO SOON: sync all textures + + +// TODO SOON: instancedMeshLOD rendering first nearest levels, look out to transparent +// TODO SOON: fix shadow +// TODO SOON: shared matrices and BVH +// public raycastOnlyFrustum = false; +// public override customDepthMaterial = new MeshDepthMaterial({ depthPacking: RGBADepthPacking }); +// public override customDistanceMaterial = new MeshDistanceMaterial(); + + export type Entity = InstancedEntity & T; export type UpdateEntityCallback = (obj: Entity, index: number) => void; export type CustomSortCallback = (list: InstancedRenderItem[]) => void; +export interface LODLevel { + distance: number; + hysteresis: number; + object: InstancedMesh2; +} + export interface BVHParams { margin?: number; highPrecision?: boolean; @@ -52,11 +68,15 @@ export class InstancedMesh2< protected _maxCount: number; protected _material: TMaterial; protected _uniformsSetCallback = new Map void>(); - protected _LOD: InstancedMeshLOD = null; + protected _LOD: InstancedMesh2; protected readonly _instancesUseEuler: boolean; protected readonly _instance: InstancedEntity; protected _visibilityChanged = false; + protected _levels: LODLevel[] = null; + protected _indexes: (Uint16Array | Uint32Array)[] = null; // TODO can be also uin16 + protected _countIndexes: number[] = null; + public override customDepthMaterial = new MeshDepthMaterial({ depthPacking: RGBADepthPacking }); public override customDistanceMaterial = new MeshDistanceMaterial(); @@ -89,7 +109,10 @@ export class InstancedMesh2< public override onBeforeRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group): void { if (!this.instanceIndex) return; - if (!this._LOD) this.frustumCulling(camera); + + if (this._levels?.length > 0) this.frustumCullingLOD(camera); + else if (!this._LOD) this.frustumCulling(camera); + this.instanceIndex.update(renderer, this._count); } @@ -102,7 +125,7 @@ export class InstancedMesh2< } /** THIS MATERIAL AND GEOMETRY CANNOT BE SHARED */ - constructor(renderer: WebGLRenderer, count: number, geometry: TGeometry, material?: TMaterial, LOD?: InstancedMeshLOD, instancesUseEuler = false) { + constructor(renderer: WebGLRenderer, count: number, geometry: TGeometry, material?: TMaterial, LOD?: InstancedMesh2, instancesUseEuler = false) { if (!count || count < 0) throw new Error("'count' must be greater than 0."); if (!geometry) throw new Error("'geometry' is mandatory."); @@ -117,7 +140,7 @@ export class InstancedMesh2< this._count = count; this._material = material; this._LOD = LOD; - this.visibilityArray = this._LOD ? LOD.visibilityArray : new Array(count).fill(true); + this.visibilityArray = LOD?.visibilityArray ?? new Array(count).fill(true); this.initIndexArray(); this.initIndexAttribute(renderer); @@ -341,7 +364,6 @@ export class InstancedMesh2< const len = objectInfluences.length + 1; // morphBaseInfluence + all influences if (this.morphTexture === null) { - // TODO try to use createTexture_float instead? this.morphTexture = new DataTexture(new Float32Array(len * this._maxCount), len, this._maxCount, RedFormat, FloatType); } @@ -508,6 +530,7 @@ export class InstancedMesh2< protected BVHCulling(): void { const array = this._indexArray; + const matrixArray = this._matrixArray; const instancesCount = this.instancesCount; const sortObjects = this._sortObjects; let count = 0; @@ -517,7 +540,7 @@ export class InstancedMesh2< if (index < instancesCount && this.getVisibilityAt(index)) { if (sortObjects) { - _position.setFromMatrixPosition(this.getMatrixAt(index)) + getPositionAt(index, matrixArray, _position); const depth = _position.sub(_cameraPos).dot(_forward); // this can be less precise than sphere.center _renderList.push(depth, index); } else { @@ -647,6 +670,177 @@ export class InstancedMesh2< return this; } + + public addLevel(geometry: BufferGeometry, material: Material, distance = 0, hysteresis = 0): this { + if (this._LOD) { + console.error("Cannot create LOD for this InstancedMesh2."); + return; + } + + if (!this._levels) { + this._levels = [{ distance: 0, hysteresis, object: this }]; + this._countIndexes = [0]; + this._indexes = [this._indexArray]; + } + + const levels = this._levels; + // TODO fix renderer param + const object = new InstancedMesh2(undefined, this._maxCount, geometry, material, this); + distance = Math.abs(distance ** 2); // to avoid to use Math.sqrt every time + let index; + + for (index = 0; index < levels.length; index++) { + if (distance < levels[index].distance) break; + } + + levels.splice(index, 0, { distance, hysteresis, object }); + + this._countIndexes.push(0); + this._indexes.splice(index, 0, object._indexArray); + + this.add(object); // TODO handle render order + return this; + } + + public getObjectLODIndexForDistance(distance: number): number { + const levels = this._levels; + + for (let i = levels.length - 1; i > 0; i--) { + const level = levels[i]; + const levelDistance = level.distance - (level.distance * level.hysteresis); + if (distance >= levelDistance) return i; + } + + return 0; + } + + protected frustumCullingLOD(camera: Camera): void { + const levels = this._levels; + const count = this._countIndexes; + + for (let i = 0; i < levels.length; i++) { + count[i] = 0; + + if (levels[i].object.instanceIndex) { + levels[i].object.instanceIndex._needsUpdate = true; // TODO improve + } + } + + _projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(this.matrixWorld); + _invMatrixWorld.copy(this.matrixWorld).invert(); + _cameraPos.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(_invMatrixWorld); + + if (this.bvh) this.BVHCullingLOD(); + else this.linearCullingLOD(); + + if (this.sortObjects) { + const customSort = this.customSort; + const list = _renderList.list; + const indexes = this._indexes; + let levelIndex = 0; + let levelDistance = levels[1].distance; + + if (customSort === null) { + list.sort(!(levels[0].object.material as Material)?.transparent ? sortOpaque : sortTransparent); + } else { + customSort(list); + } + + for (let i = 0, l = list.length; i < l; i++) { + const item = list[i]; + + if (item.depth > levelDistance) { // > or >= ? capire in base all'altro algoritmo + levelIndex++; + levelDistance = levels[levelIndex + 1]?.distance ?? Infinity; + // for fixa + } + + indexes[levelIndex][count[levelIndex]++] = item.index; // TODO COUNT ARRAY QUI NON SERVE + } + + _renderList.reset(); + } + + for (let i = 0; i < levels.length; i++) { + const object = levels[i].object; + object.visible = i === 0 || count[i] > 0; + object._count = count[i]; + } + } + + protected BVHCullingLOD(): void { + const matrixArray = this._matrixArray; + const instancesCount = this.instancesCount; + const count = this._countIndexes; // reuse the same? also uintarray? + const indexes = this._indexes; + const visibilityArray = this.visibilityArray; + + if (this.sortObjects) { // todo refactor + + this.bvh.frustumCulling(_projScreenMatrix, (node: BVHNode<{}, number>) => { + const index = node.object; + if (index < instancesCount && visibilityArray[index]) { + const distance = getPositionAt(index, matrixArray, _position).distanceToSquared(_cameraPos); + _renderList.push(distance, index); + } + }); + + } else { + + this.bvh.frustumCullingLOD(_projScreenMatrix, _cameraPos, this._levels, (node: BVHNode<{}, number>, level: number) => { + const index = node.object; + if (index < instancesCount && visibilityArray[index]) { + + if (level === null) { + const distance = getPositionAt(index, matrixArray, _position).distanceToSquared(_cameraPos); // distance can be get by BVH + level = this.getObjectLODIndexForDistance(distance); + } + + indexes[level][count[level]++] = index; + } + }); + + } + } + + protected linearCullingLOD(): void { + const sortObjects = this.sortObjects; + const matrixArray = this._matrixArray; + const bSphere = this._levels[this._levels.length - 1].object.geometry.boundingSphere; // TODO check se esiste? + const radius = bSphere.radius; + const center = bSphere.center; + const instancesCount = this.instancesCount; + const geometryCentered = center.x === 0 && center.y === 0 && center.z === 0; + + _frustum.setFromProjectionMatrix(_projScreenMatrix); + + const count = this._countIndexes; + const indexes = this._indexes; + + for (let i = 0; i < instancesCount; i++) { + if (!this.visibilityArray[i]) continue; // opt anche nell'altra classe + + if (geometryCentered) { + getPositionAt(i, matrixArray, _sphere.center); + _sphere.radius = radius * getMaxScaleOnAxisAt(i, matrixArray); + } else { + const matrix = this.getMatrixAt(i); // opt this getting only pos and scale + _sphere.center.copy(center).applyMatrix4(matrix); + _sphere.radius = radius * matrix.getMaxScaleOnAxis(); + } + + if (_frustum.intersectsSphere(_sphere)) { + const distance = _sphere.center.distanceToSquared(_cameraPos); + + if (sortObjects) { + _renderList.push(distance, i); + } else { + const levelIndex = this.getObjectLODIndexForDistance(distance); + indexes[levelIndex][count[levelIndex]++] = i; + } + } + } + } } const _box3 = new Box3(); diff --git a/src/objects/InstancedMeshLOD.ts b/src/objects/InstancedMeshLOD.ts deleted file mode 100644 index 34b6428..0000000 --- a/src/objects/InstancedMeshLOD.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { BVHNode } from "bvh.js"; -import { BufferGeometry, Camera, Color, ColorRepresentation, DataTexture, Frustum, Material, Matrix4, Object3D, Sphere, Vector3, WebGLRenderer } from "three"; -import { createTexture_mat4, createTexture_vec4 } from "../utils/createTexture.js"; -import { BVHParams, Entity, InstancedMesh2, UpdateEntityCallback } from "./InstancedMesh2.js"; -import { InstancedMeshBVH } from "./InstancedMeshBVH.js"; -import { InstancedRenderItem, InstancedRenderList } from "./InstancedRenderList.js"; -import { getMaxScaleOnAxisAt, getPositionAt } from "../utils/matrixUtils.js"; - -export interface LODLevel { - distance: number; - hysteresis: number; - object: InstancedMesh2; -} - -// TODO SOON: instancedMeshLOD rendering first nearest levels, look out to transparent -// TODO SOON: fix shadow -// TODO SOON: shared matrices and BVH - -export class InstancedMeshLOD extends Object3D { - public isInstancedMeshLOD = true; - public instancesCount: number; // TODO handle update from dynamic to static - public perObjectFrustumCulled = true; - public sortObjects = false; // TODO should this be true? - public customSort: (list: InstancedRenderItem[]) => void = null; - public visibilityArray: boolean[]; - public matricesTexture: DataTexture; - public colorsTexture: DataTexture = null; - public morphTexture: DataTexture = null; - public instances: Entity[]; - /** @internal */ public _matrixArray: Float32Array; - /** @internal */ public _colorArray: Float32Array; - protected _renderer: WebGLRenderer; - protected _maxCount: number; - protected _indexes: Uint32Array[] = []; // TODO can be also uin16 - protected _countIndexes: number[] = []; - - // HACK TO MAKE IT WORK WITHOUT UPDATE CORE - private readonly isLOD = true; - public autoUpdate = true; - public levels: LODLevel[] = []; - - public bvh: InstancedMeshBVH; - // public raycastOnlyFrustum = false; - - // public override customDepthMaterial = new MeshDepthMaterial({ depthPacking: RGBADepthPacking }); - // public override customDistanceMaterial = new MeshDistanceMaterial(); - - public get maxCount() { return this._maxCount } - - constructor(renderer: WebGLRenderer, count: number) { - super(); - this._renderer = renderer; - this.instancesCount = count; - this._maxCount = count; - - this.visibilityArray = new Array(count).fill(true); - - this.matricesTexture = createTexture_mat4(count); - this._matrixArray = this.matricesTexture.image.data as unknown as Float32Array; - } - - public addLevel(geometry: BufferGeometry, material: Material, distance = 0, hysteresis = 0): this { - const levels = this.levels; - const object = new InstancedMesh2(this._renderer, this._maxCount, geometry, material, this); - distance = Math.abs(distance ** 2); // to avoid to use Math.sqrt every time - let index; - - for (index = 0; index < levels.length; index++) { - if (distance < levels[index].distance) break; - } - - levels.splice(index, 0, { distance, hysteresis, object }); - - this._countIndexes.push(0); - this._indexes.splice(index, 0, object.instanceIndex.array as Uint32Array); - - this.add(object); - return this; - } - - public removeLevel(): this { - throw new Error("'removeLevel' is not implemented yet."); - } - - public getObjectIndexForDistance(distance: number): number { - const levels = this.levels; - - for (let i = levels.length - 1; i > 0; i--) { - const level = levels[i]; - const levelDistance = level.distance - (level.distance * level.hysteresis); - if (distance >= levelDistance) return i; - } - - return 0; - } - - public update(camera: Camera): void { - if (!this.perObjectFrustumCulled) return; // TODO ovviamente bisogna ricalcolare tutte le distanze - this.frustumCulling(camera); - } - - public updateInstances(onUpdate: UpdateEntityCallback>): void { - this.levels[0].object.updateInstances(onUpdate); // TODO meglio - } - - public computeBVH(config: BVHParams = {}): void { - if (!this.bvh) this.bvh = new InstancedMeshBVH(this, config.margin, config.highPrecision); - this.bvh.clear(); - this.bvh.create(); - } - - public setMatrixAt(id: number, matrix: Matrix4): void { - matrix.toArray(this._matrixArray, id * 16); - - if (this.instances) { - const instance = this.instances[id]; - matrix.decompose(instance.position, instance.quaternion, instance.scale); - } - - this.matricesTexture.needsUpdate = true; // TODO - // this.bvh?.move(id); - } - - public getMatrixAt(id: number, matrix = _tempMat4): Matrix4 { - return matrix.fromArray(this._matrixArray, id * 16); - } - - public setVisibilityAt(id: number, visible: boolean): void { - this.visibilityArray[id] = visible; - } - - public getVisibilityAt(id: number): boolean { - return this.visibilityArray[id]; - } - - public setColorAt(id: number, color: ColorRepresentation): void { - if (this.colorsTexture === null) { - this.colorsTexture = createTexture_vec4(this._maxCount); // we use vec4 because createTexture_vec3 doesn't exist - this._colorArray = this.colorsTexture.image.data as unknown as Float32Array; - } - - if ((color as Color).isColor) { - (color as Color).toArray(this._colorArray, id * 4); // even if is vec3, we need 4 because RGB format is removed from three.js - } else { - _tempCol.set(color).toArray(this._colorArray, id * 4); - } - - this.colorsTexture.needsUpdate = true; // TODO - } - - public getColorAt(id: number, color = _tempCol): Color { - return color.fromArray(this._colorArray, id * 4); - } - - protected frustumCulling(camera: Camera): void { - const levels = this.levels; - const count = this._countIndexes; - - for (let i = 0; i < levels.length; i++) { - count[i] = 0; - } - - _projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(this.matrixWorld); - _invMatrixWorld.copy(this.matrixWorld).invert(); - _cameraPos.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(_invMatrixWorld); - - if (this.bvh) this.BVHCulling(); - else this.linearCulling(); - - if (this.sortObjects) { - const customSort = this.customSort; - const list = _renderList.list; - const indexes = this._indexes; - let levelIndex = 0; - let levelDistance = levels[1].distance; - - if (customSort === null) { - list.sort(!(levels[0].object.material as Material)?.transparent ? sortOpaque : sortTransparent); - } else { - customSort(list); - } - - for (let i = 0, l = list.length; i < l; i++) { - const item = list[i]; - - if (item.depth > levelDistance) { // > or >= ? capire in base all'altro algoritmo - levelIndex++; - levelDistance = levels[levelIndex + 1]?.distance ?? Infinity; - // for fixa - } - - indexes[levelIndex][count[levelIndex]++] = item.index; // TODO COUNT ARRAY QUI NON SERVE - } - - _renderList.reset(); - } - - for (let i = 0; i < levels.length; i++) { - const object = levels[i].object; - object.visible = count[i] > 0; - object._count = count[i]; - } - } - - protected BVHCulling(): void { - const matrixArray = this._matrixArray; - const instancesCount = this.instancesCount; - const count = this._countIndexes; // reuse the same? also uintarray? - const indexes = this._indexes; - const visibilityArray = this.visibilityArray; - - if (this.sortObjects) { // todo refactor - - this.bvh.frustumCulling(_projScreenMatrix, (node: BVHNode<{}, number>) => { - const index = node.object; - if (index < instancesCount && visibilityArray[index]) { - const distance = getPositionAt(index, matrixArray, _position).distanceToSquared(_cameraPos); - _renderList.push(distance, index); - } - }); - - } else { - - this.bvh.frustumCullingLOD(_projScreenMatrix, _cameraPos, this.levels, (node: BVHNode<{}, number>, level: number) => { - const index = node.object; - if (index < instancesCount && visibilityArray[index]) { - - if (level === null) { - const distance = getPositionAt(index, matrixArray, _position).distanceToSquared(_cameraPos); // distance can be get by BVH - level = this.getObjectIndexForDistance(distance); - } - - indexes[level][count[level]++] = index; - } - }); - - } - } - - protected linearCulling(): void { - const sortObjects = this.sortObjects; - const matrixArray = this._matrixArray; - const bSphere = this.levels[this.levels.length - 1].object.geometry.boundingSphere; // TODO check se esiste? - const radius = bSphere.radius; - const center = bSphere.center; - const instancesCount = this.instancesCount; - const geometryCentered = center.x === 0 && center.y === 0 && center.z === 0; - - _frustum.setFromProjectionMatrix(_projScreenMatrix); - - const count = this._countIndexes; - const indexes = this._indexes; - - for (let i = 0; i < instancesCount; i++) { - if (!this.visibilityArray[i]) continue; // opt anche nell'altra classe - - if (geometryCentered) { - getPositionAt(i, matrixArray, _sphere.center); - _sphere.radius = radius * getMaxScaleOnAxisAt(i, matrixArray); - } else { - const matrix = this.getMatrixAt(i); // opt this getting only pos and scale - _sphere.center.copy(center).applyMatrix4(matrix); - _sphere.radius = radius * matrix.getMaxScaleOnAxis(); - } - - if (_frustum.intersectsSphere(_sphere)) { - const distance = _sphere.center.distanceToSquared(_cameraPos); - - if (sortObjects) { - _renderList.push(distance, i); - } else { - const levelIndex = this.getObjectIndexForDistance(distance); - indexes[levelIndex][count[levelIndex]++] = i; - } - } - } - } - - // TODO edit raycast -} - -// const _box3 = new Box3(); -const _sphere = new Sphere(); -const _frustum = new Frustum(); -const _projScreenMatrix = new Matrix4(); -// const _intersections: Intersection[] = []; -// const _mesh = new Mesh(); -// const _ray = new Ray(); -// const _direction = new Vector3(); -// const _worldScale = new Vector3(); -const _invMatrixWorld = new Matrix4(); -const _renderList = new InstancedRenderList(); -// const _forward = new Vector3(); -const _cameraPos = new Vector3(); -const _position = new Vector3(); -const _tempMat4 = new Matrix4(); -const _tempCol = new Color(); -// const _instance = new InstancedEntity(undefined, -1); - - -// // move it and use the same for instancedMesh2 -// function ascSortIntersection(a: Intersection, b: Intersection): number { -// return a.distance - b.distance; -// } - -function sortOpaque(a: InstancedRenderItem, b: InstancedRenderItem) { - return a.depth - b.depth; -} - -function sortTransparent(a: InstancedRenderItem, b: InstancedRenderItem) { - return b.depth - a.depth; -}