diff --git a/package-lock.json b/package-lock.json index 1132cd3..5f6f423 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@eslint/js": "^9.10.0", "@three.ez/main": "^0.5.7", - "@types/three": "^0.168.0", + "@types/three": "^0.169.0", "eslint": "^9.9.0", "typescript": "^5.1.3", "typescript-eslint": "^8.1.0", @@ -1212,11 +1212,10 @@ "license": "MIT" }, "node_modules/@types/three": { - "version": "0.168.0", - "resolved": "https://registry.npmjs.org/@types/three/-/three-0.168.0.tgz", - "integrity": "sha512-qAGLGzbaYgkkonOBfwOr+TZpOskPfFjrDAj801WQSVkUz0/D9zwir4vhruJ/CC/GteywzR9pqeVVfs5th/2oKw==", + "version": "0.169.0", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.169.0.tgz", + "integrity": "sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==", "dev": true, - "license": "MIT", "dependencies": { "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", @@ -3255,10 +3254,9 @@ "license": "MIT" }, "node_modules/three": { - "version": "0.168.0", - "resolved": "https://registry.npmjs.org/three/-/three-0.168.0.tgz", - "integrity": "sha512-6m6jXtDwMJEK/GGMbAOTSAmxNdzKvvBzgd7q8bE/7Tr6m7PaBh5kKLrN7faWtlglXbzj7sVba48Idwx+NRsZXw==", - "license": "MIT", + "version": "0.169.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz", + "integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==", "peer": true }, "node_modules/to-fast-properties": { diff --git a/package.json b/package.json index 97c44cb..eaf6490 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@three.ez/instanced-mesh", - "version": "0.2.1", + "version": "0.2.2", "description": "Simplified and enhanced InstancedMesh with frustum culling, fast raycasting (using BVH), sorting, visibility management and more.", "author": "Andrea Gargaro ", "license": "MIT", @@ -21,7 +21,8 @@ "performance", "BVH", "acceleration", - "raycasting" + "raycasting", + "LOD" ], "scripts": { "start": "vite", @@ -36,7 +37,7 @@ "devDependencies": { "@eslint/js": "^9.10.0", "@three.ez/main": "^0.5.7", - "@types/three": "^0.168.0", + "@types/three": "^0.169.0", "eslint": "^9.9.0", "typescript": "^5.1.3", "typescript-eslint": "^8.1.0", diff --git a/src/objects/InstancedMesh2.ts b/src/objects/InstancedMesh2.ts index 5257782..243f16c 100644 --- a/src/objects/InstancedMesh2.ts +++ b/src/objects/InstancedMesh2.ts @@ -38,7 +38,7 @@ export class InstancedMesh2< > extends Mesh { public override type = 'InstancedMesh2'; - public isInstancedMesh2 = true; + public readonly isInstancedMesh2 = true; public instances: Entity[]; public instanceIndex: GLInstancedBufferAttribute; public matricesTexture: DataTexture; @@ -65,9 +65,9 @@ export class InstancedMesh2< public override customDistanceMaterial = new MeshDistanceMaterial(); // HACK TO MAKE IT WORK WITHOUT UPDATE CORE - private isInstancedMesh = true; // must be set to use instancing rendering - private instanceMatrix = new InstancedBufferAttribute(new Float32Array(0), 16); // must be init to avoid exception - private instanceColor = null; // must be null to avoid exception + private readonly isInstancedMesh = true; // must be set to use instancing rendering + private readonly instanceMatrix = new InstancedBufferAttribute(new Float32Array(0), 16); // must be init to avoid exception + private readonly instanceColor = null; // must be null to avoid exception public get count() { return this._count } public get maxCount() { return this._maxCount } @@ -160,7 +160,7 @@ export class InstancedMesh2< material.onBeforeCompile = (shader: WebGLProgramParametersWithUniforms, renderer) => { if (onBeforeCompile) onBeforeCompile(shader, renderer); - + if (!shader.instancing) return; shader.instancing = false; @@ -521,7 +521,7 @@ export class InstancedMesh2< for (let i = 0; i < instancesCount; i++) { if (!this.getVisibilityAt(i)) continue; - const matrix = this.getMatrixAt(i); + const matrix = this.getMatrixAt(i); // we can optimize this a little avoiding copy? what about using instances if available? if (geometryCentered) _sphere.center.copy(_position.setFromMatrixPosition(matrix)); else _sphere.center.copy(center).applyMatrix4(matrix); _sphere.radius = radius * matrix.getMaxScaleOnAxis(); diff --git a/src/objects/InstancedMeshBVH.ts b/src/objects/InstancedMeshBVH.ts index 8db215a..463ea44 100644 --- a/src/objects/InstancedMeshBVH.ts +++ b/src/objects/InstancedMeshBVH.ts @@ -1,7 +1,8 @@ import { box3ToArray, BVH, BVHNode, FloatArray, HybridBuilder, onFrustumIntersectionCallback, onFrustumIntersectionLODCallback, onIntersectionCallback, onIntersectionRayCallback, vec3ToArray, WebGLCoordinateSystem } from 'bvh.js'; -import { Box3, Matrix4, Raycaster, Vector3 } from 'three'; +import { Box3, Matrix4, Raycaster, Sphere, Vector3 } from 'three'; import { InstancedMesh2 } from './InstancedMesh2.js'; import { InstancedMeshLOD, LODLevel } from './InstancedMeshLOD.js'; +import { getSphereFromMatrix, SphereTarget } from '../utils/matrixUtils.js'; export class InstancedMeshBVH { public target: InstancedMesh2 | InstancedMeshLOD; @@ -15,23 +16,33 @@ export class InstancedMeshBVH { protected _boxArray: FloatArray; protected _cameraPos: FloatArray; protected _levels: FloatArray; // TODO improve this + protected _getBoxFromSphere: boolean; // works only if geometry is centered for now + protected _geoBoundingSphere: Sphere = null; + protected _sphereTarget: SphereTarget = null; - constructor(target: InstancedMesh2 | InstancedMeshLOD, margin = 0, highPrecision = false) { + constructor(target: InstancedMesh2 | InstancedMeshLOD, margin = 0, highPrecision = false, getBoxFromSphere = false) { this._margin = margin; this.target = target; const geometry = (target as InstancedMesh2).isInstancedMesh2 ? (target as InstancedMesh2).geometry : (target as InstancedMeshLOD).levels[(target as InstancedMeshLOD).levels.length - 1].object.geometry; // TODO improve this - - if (!geometry.boundingBox) geometry.computeBoundingBox(); + + if (!geometry.boundingBox) geometry.computeBoundingBox(); this.geoBoundingBox = geometry.boundingBox; + if (getBoxFromSphere) { + if (!geometry.boundingSphere) geometry.computeBoundingSphere(); + this._geoBoundingSphere = geometry.boundingSphere; + this._sphereTarget = { centerX: 0, centerY: 0, centerZ: 0, maxScale: 0 }; + } + this._arrayType = highPrecision ? Float64Array : Float32Array; this.bvh = new BVH(new HybridBuilder(highPrecision), WebGLCoordinateSystem); this._origin = new this._arrayType(3); this._dir = new this._arrayType(3); this._cameraPos = new this._arrayType(3); + this._getBoxFromSphere = getBoxFromSphere; } public create(): void { @@ -108,7 +119,7 @@ export class InstancedMeshBVH { if (this._levels?.length !== levels.length) { // TODO improve this._levels = new this._arrayType(levels.length); } - + const levelsArray = this._levels; for (let i = 0; i < levels.length; i++) { levelsArray[i] = levels[i].distance; @@ -153,8 +164,21 @@ export class InstancedMeshBVH { } protected getBox(id: number, array: FloatArray): FloatArray { - _box3.copy(this.geoBoundingBox).applyMatrix4(this.target.getMatrixAt(id)); - box3ToArray(_box3, array); + // TODO add check if geometry is centered. adjust ref using this._matrixArray insteaad of this.target._matrixArray + if (this._getBoxFromSphere) { + const { centerX, centerY, centerZ, maxScale } = getSphereFromMatrix(id, this.target._matrixArray, this._sphereTarget); + const radius = this._geoBoundingSphere.radius * maxScale; + array[0] = centerX - radius; + array[1] = centerX + radius; + array[2] = centerY - radius; + array[3] = centerY + radius; + array[4] = centerZ - radius; + array[5] = centerZ + radius; + } else { + _box3.copy(this.geoBoundingBox).applyMatrix4(this.target.getMatrixAt(id)); + box3ToArray(_box3, array); + } + return array; } } diff --git a/src/utils/createTexture.ts b/src/utils/createTexture.ts index 6098db8..ee87088 100644 --- a/src/utils/createTexture.ts +++ b/src/utils/createTexture.ts @@ -1,4 +1,4 @@ -import { DataTexture, FloatType, RedFormat, RGBAFormat, RGFormat } from "three"; +import { DataTexture, FloatType, RedFormat, RedIntegerFormat, RGBAFormat, RGFormat, UnsignedIntType } from "three"; export function createTexture_float(count: number): DataTexture { const size = Math.ceil(Math.sqrt(count)); @@ -8,6 +8,14 @@ export function createTexture_float(count: number): DataTexture { return texture; } +export function createTexture_uint(count: number): DataTexture { + const size = Math.ceil(Math.sqrt(count)); + const array = new Uint32Array(size * size); + const texture = new DataTexture(array, size, size, RedIntegerFormat, UnsignedIntType); + texture.needsUpdate = true; + return texture; +} + export function createTexture_vec2(count: number): DataTexture { const size = Math.ceil(Math.sqrt(count)); const array = new Float32Array(size * size * 2); diff --git a/src/utils/matrixUtils.ts b/src/utils/matrixUtils.ts new file mode 100644 index 0000000..a1939dc --- /dev/null +++ b/src/utils/matrixUtils.ts @@ -0,0 +1,37 @@ +import { FloatArray } from "bvh.js"; + +export interface SphereTarget { + centerX: number; + centerY: number; + centerZ: number; + maxScale: number; +} + +// this method works if geometry is centered +export function getSphereFromMatrix(id: number, array: FloatArray, target: SphereTarget): SphereTarget { + const offset = id * 16; + + // get max scale from matrix + const m0 = array[offset + 0]; + const m1 = array[offset + 1]; + const m2 = array[offset + 2]; + const m4 = array[offset + 4]; + const m5 = array[offset + 5]; + const m6 = array[offset + 6]; + const m8 = array[offset + 8]; + const m9 = array[offset + 9]; + const m10 = array[offset + 10]; + + const scaleXSq = m0 * m0 + m1 * m1 + m2 * m2; + const scaleYSq = m4 * m4 + m5 * m5 + m6 * m6; + const scaleZSq = m8 * m8 + m9 * m9 + m10 * m10; + + target.maxScale = Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq)); + + // get position from matrix + target.centerX = array[offset + 12]; + target.centerY = array[offset + 13]; + target.centerZ = array[offset + 14]; + + return target; +}