diff --git a/examples/LOD.ts b/examples/LOD.ts index da04370..6e00fd5 100644 --- a/examples/LOD.ts +++ b/examples/LOD.ts @@ -16,10 +16,10 @@ const scene = new Scene(); 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.addLOD(new SphereGeometry(5, 30, 15), new MeshLambertMaterial({ color: 'green' })); +instancedMeshLOD.addLOD(new SphereGeometry(5, 20, 10), new MeshLambertMaterial({ color: 'yellow' }), 50); +instancedMeshLOD.addLOD(new SphereGeometry(5, 10, 5), new MeshLambertMaterial({ color: 'orange' }), 500); +instancedMeshLOD.addLOD(new SphereGeometry(5, 5, 3), new MeshLambertMaterial({ color: 'red' }), 1000); instancedMeshLOD.levels[0].object.geometry.computeBoundingSphere(); // improve diff --git a/examples/trees.ts b/examples/trees.ts index 833aead..d857577 100644 --- a/examples/trees.ts +++ b/examples/trees.ts @@ -1,13 +1,13 @@ import { Asset, Main, PerspectiveCameraAuto } from '@three.ez/main'; -import { ACESFilmicToneMapping, AmbientLight, BoxGeometry, BufferGeometry, DirectionalLight, FogExp2, Mesh, MeshLambertMaterial, MeshStandardMaterial, PCFSoftShadowMap, PlaneGeometry, Scene, Vector3 } from 'three'; +import { ACESFilmicToneMapping, AmbientLight, BoxGeometry, BufferGeometry, DirectionalLight, FogExp2, Mesh, MeshBasicMaterial, 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 { InstancedMesh2 } from '../src/index.js'; -const count = 1000000; -const terrainSize = 20000; +const count = 10000; +const terrainSize = 1000; const main = new Main(); // init renderer and other stuff main.renderer.toneMapping = ACESFilmicToneMapping; @@ -24,9 +24,10 @@ const trees = new InstancedMesh2(main.renderer, count, treeGLTF.geometry, treeGL trees.castShadow = true; trees.cursor = 'pointer'; -trees.addLevel(new BoxGeometry(100, 1000, 100), new MeshLambertMaterial(), 100); -trees.levels[0].object.geometry.computeBoundingSphere(); // improve -trees.levels[1].object.castShadow = true; +trees.addLOD(new BoxGeometry(100, 1000, 100), new MeshLambertMaterial(), 100); +// trees.addShadowLOD(trees.geometry, new MeshBasicMaterial()); +trees.addShadowLOD(new BoxGeometry(100, 1000, 100), new MeshBasicMaterial(), 100); +trees.levels.shadowRender.levels[0].object.castShadow = true; // TODO create utility methods trees.createInstances((obj, index) => { obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2); @@ -62,11 +63,11 @@ scene.on('animate', (e) => scene.fog.color.setHSL(0, 0, sun.y)); const dirLight = new DirectionalLight(); dirLight.castShadow = true; -dirLight.shadow.mapSize.set(1024, 1024); -dirLight.shadow.camera.left = -300; -dirLight.shadow.camera.right = 300; -dirLight.shadow.camera.top = 300; -dirLight.shadow.camera.bottom = -300; +dirLight.shadow.mapSize.set(2048, 2048); +dirLight.shadow.camera.left = -200; +dirLight.shadow.camera.right = 200; +dirLight.shadow.camera.top = 200; +dirLight.shadow.camera.bottom = -200; dirLight.shadow.camera.far = 2000; dirLight.shadow.camera.updateProjectionMatrix(); diff --git a/src/core/InstancedMesh2.ts b/src/core/InstancedMesh2.ts index 8eff476..423d7ab 100644 --- a/src/core/InstancedMesh2.ts +++ b/src/core/InstancedMesh2.ts @@ -4,6 +4,7 @@ import { GLInstancedBufferAttribute } from "./utils/GLInstancedBufferAttribute.j import { InstancedEntity, UniformValue, UniformValueNoNumber } from "./InstancedEntity.js"; import { InstancedMeshBVH } from "./InstancedMeshBVH.js"; import { InstancedRenderItem } from "./utils/InstancedRenderList.js"; +import { LODInfo } from "./feature/LOD.js"; // TODO: Add expand and count/maxCount when create? // TODO: partial texture update @@ -21,12 +22,6 @@ 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; @@ -53,7 +48,7 @@ export class InstancedMesh2< public customSort: CustomSortCallback = null; public raycastOnlyFrustum = false; public visibilityArray: boolean[]; - public levels: LODLevel[] = null; + public levels: LODInfo = null; // TODO rename /** @internal */ public _indexArray: Uint16Array | Uint32Array; /** @internal */ public _matrixArray: Float32Array; /** @internal */ public _colorArray: Float32Array = null; @@ -61,15 +56,12 @@ export class InstancedMesh2< /** @internal */ public _perObjectFrustumCulled = true; /** @internal */ public _sortObjects = false; /** @internal */ public _maxCount: number; + /** @internal */ public _visibilityChanged = false; protected _material: TMaterial; protected _uniformsSetCallback = new Map void>(); protected _LOD: InstancedMesh2; protected readonly _instancesUseEuler: boolean; protected readonly _instance: InstancedEntity; - /** @internal */ public _visibilityChanged = false; - - 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(); diff --git a/src/core/feature/FrustumCulling.ts b/src/core/feature/FrustumCulling.ts index 9eb355a..ac12e69 100644 --- a/src/core/feature/FrustumCulling.ts +++ b/src/core/feature/FrustumCulling.ts @@ -4,6 +4,7 @@ import { getMaxScaleOnAxisAt, getPositionAt } from "../../utils/MatrixUtils.js"; import { sortOpaque, sortTransparent } from "../../utils/SortingUtils.js"; import { InstancedMesh2 } from "../InstancedMesh2.js"; import { InstancedRenderList } from "../utils/InstancedRenderList.js"; +import { LODRenderList } from "./LOD.js"; // TODO: fix shadowMap LOD sorting objects? @@ -17,9 +18,9 @@ declare module '../InstancedMesh2.js' { /** @internal */ BVHCulling(): void; /** @internal */ linearCulling(): void; - /** @internal */ frustumCullingLOD(camera: Camera, cameraLOD?: Camera): void; - /** @internal */ BVHCullingLOD(sortObjects: boolean): void; - /** @internal */ linearCullingLOD(sortObjects: boolean): void; + /** @internal */ frustumCullingLOD(renderList: LODRenderList, objects: InstancedMesh2[], camera: Camera, cameraLOD: Camera): void; + /** @internal */ BVHCullingLOD(renderList: LODRenderList, sortObjects: boolean): void; + /** @internal */ linearCullingLOD(renderList: LODRenderList, sortObjects: boolean): void; } } @@ -34,9 +35,13 @@ const _position = new Vector3(); const _sphere = new Sphere(); InstancedMesh2.prototype.performFrustumCulling = function (renderer: WebGLRenderer, camera: Camera, cameraLOD = camera): void { - if (this.levels?.length > 0) this.frustumCullingLOD(camera, cameraLOD); + const info = this.levels; + const isShadowRendering = camera !== cameraLOD; + const renderList = !isShadowRendering ? info?.render : (info?.shadowRender ?? info?.render); + + if (renderList?.levels.length > 0) this.frustumCullingLOD(renderList, info.objects, camera, cameraLOD); else if (!this._LOD) this.frustumCulling(camera); - + this.instanceIndex.update(renderer, this._count); } @@ -182,11 +187,10 @@ InstancedMesh2.prototype.linearCulling = function (): void { this._count = count; } -InstancedMesh2.prototype.frustumCullingLOD = function (camera: Camera, cameraLOD = camera): void { - const levels = this.levels; - const count = this._countIndexes; +InstancedMesh2.prototype.frustumCullingLOD = function (renderList: LODRenderList, objects: InstancedMesh2[], camera: Camera, cameraLOD: Camera): void { + const { count, levels } = renderList; const isShadowRendering = camera !== cameraLOD; - const sortObjects = !isShadowRendering && this._sortObjects; + const sortObjects = !isShadowRendering && this._sortObjects; // sort is disabled when render shadows for (let i = 0; i < levels.length; i++) { count[i] = 0; @@ -196,18 +200,23 @@ InstancedMesh2.prototype.frustumCullingLOD = function (camera: Camera, cameraLOD } } + for (const object of objects) { + if (object === this) object._count = 0; + else object.visible = false; + } + _projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(this.matrixWorld); _invMatrixWorld.copy(this.matrixWorld).invert(); _cameraPos.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(_invMatrixWorld); _cameraLODPos.setFromMatrixPosition(cameraLOD.matrixWorld).applyMatrix4(_invMatrixWorld); - if (this.bvh) this.BVHCullingLOD(sortObjects); - else this.linearCullingLOD(sortObjects); + if (this.bvh) this.BVHCullingLOD(renderList, sortObjects); + else this.linearCullingLOD(renderList, sortObjects); if (sortObjects) { const customSort = this.customSort; - const list = _renderList.list; - const indexes = this._indexes; + const list = _renderList.list; // TODO better name... + const indexes = renderList.indexes; let levelIndex = 0; let levelDistance = levels[1].distance; @@ -226,7 +235,7 @@ InstancedMesh2.prototype.frustumCullingLOD = function (camera: Camera, cameraLOD // for fixa } - indexes[levelIndex][count[levelIndex]++] = item.index; // TODO COUNT ARRAY QUI NON SERVE + indexes[levelIndex][count[levelIndex]++] = item.index; } _renderList.reset(); @@ -234,16 +243,15 @@ InstancedMesh2.prototype.frustumCullingLOD = function (camera: Camera, cameraLOD for (let i = 0; i < levels.length; i++) { const object = levels[i].object; - object.visible = i === 0 || count[i] > 0; + object.visible = object === this || count[i] > 0; object._count = count[i]; } } -InstancedMesh2.prototype.BVHCullingLOD = function (sortObjects: boolean): void { +InstancedMesh2.prototype.BVHCullingLOD = function (renderList: LODRenderList, sortObjects: boolean): void { + const { count, indexes, levels } = renderList; 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 (sortObjects) { // todo refactor @@ -258,13 +266,13 @@ InstancedMesh2.prototype.BVHCullingLOD = function (sortObjects: boolean): void { } else { - this.bvh.frustumCullingLOD(_projScreenMatrix, _cameraLODPos, this.levels, (node: BVHNode<{}, number>, level: number) => { + this.bvh.frustumCullingLOD(_projScreenMatrix, _cameraLODPos, 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(_cameraLODPos); // distance can be get by BVH - level = this.getObjectLODIndexForDistance(distance); + level = this.getObjectLODIndexForDistance(levels, distance); } indexes[level][count[level]++] = index; @@ -274,9 +282,10 @@ InstancedMesh2.prototype.BVHCullingLOD = function (sortObjects: boolean): void { } } -InstancedMesh2.prototype.linearCullingLOD = function (sortObjects: boolean): void { +InstancedMesh2.prototype.linearCullingLOD = function (renderList: LODRenderList, sortObjects: boolean): void { + const { count, indexes, levels } = renderList; const matrixArray = this._matrixArray; - const bSphere = this.levels[this.levels.length - 1].object.geometry.boundingSphere; // TODO check se esiste? + const bSphere = this.geometry.boundingSphere; // TODO check se esiste? const radius = bSphere.radius; const center = bSphere.center; const instancesCount = this.instancesCount; @@ -284,9 +293,6 @@ InstancedMesh2.prototype.linearCullingLOD = function (sortObjects: boolean): voi _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 @@ -305,7 +311,7 @@ InstancedMesh2.prototype.linearCullingLOD = function (sortObjects: boolean): voi if (sortObjects) { _renderList.push(distance, i); } else { - const levelIndex = this.getObjectLODIndexForDistance(distance); + const levelIndex = this.getObjectLODIndexForDistance(levels, distance); indexes[levelIndex][count[levelIndex]++] = i; } } diff --git a/src/core/feature/LOD.ts b/src/core/feature/LOD.ts index de9ff5d..a6df416 100644 --- a/src/core/feature/LOD.ts +++ b/src/core/feature/LOD.ts @@ -3,51 +3,119 @@ import { InstancedMesh2 } from "../InstancedMesh2.js"; declare module '../InstancedMesh2.js' { interface InstancedMesh2 { - addLevel(geometry: BufferGeometry, material: Material, distance?: number, hysteresis?: number): this - getObjectLODIndexForDistance(distance: number): number + getObjectLODIndexForDistance(levels: LODLevel[], distance: number): number; + setFirstLODDistance(distance?: number, hysteresis?: number): this; + addLOD(geometry: BufferGeometry, material: Material, distance?: number, hysteresis?: number): this; + addShadowLOD(geometry: BufferGeometry, material: Material, distance?: number, hysteresis?: number): this; + /** @internal */ addLevel(renderList: LODRenderList, geometry: BufferGeometry, material: Material, distance: number, hysteresis: number): void; } } +export interface LODInfo { + render: LODRenderList; + shadowRender: LODRenderList; + objects: InstancedMesh2[]; +} + +export interface LODRenderList { + levels: LODLevel[]; + indexes: (Uint16Array | Uint32Array)[]; + count: number[]; +} -InstancedMesh2.prototype.addLevel = function(geometry: BufferGeometry, material: Material, distance = 0, hysteresis = 0): InstancedMesh2 { +export interface LODLevel { + distance: number; + hysteresis: number; + object: InstancedMesh2; +} + +InstancedMesh2.prototype.getObjectLODIndexForDistance = function (levels: LODLevel[], distance: number): number { + 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; +} + +InstancedMesh2.prototype.setFirstLODDistance = function (distance = 0, hysteresis = 0): InstancedMesh2 { 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]; + this.levels = { render: null, shadowRender: null, objects: [this] }; } - 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; + if (!this.levels.render) { + this.levels.render = { + levels: [{ distance, hysteresis, object: this }], + indexes: [this._indexArray], + count: [0] + }; + } - for (index = 0; index < levels.length; index++) { - if (distance < levels[index].distance) break; + return this; +} + +InstancedMesh2.prototype.addLOD = function (geometry: BufferGeometry, material: Material, distance = 0, hysteresis = 0): InstancedMesh2 { + if (this._LOD) { + console.error("Cannot create LOD for this InstancedMesh2."); + return; } - levels.splice(index, 0, { distance, hysteresis, object }); + if (!this.levels?.render && distance === 0) { + console.error("Cannot set distance to 0 for the first LOD. Use 'setFirstLODDistance' before use 'addLOD'."); + return; + } else { + this.setFirstLODDistance(0, hysteresis); + } - this._countIndexes.push(0); - this._indexes.splice(index, 0, object._indexArray); + this.addLevel(this.levels.render, geometry, material, distance, hysteresis); + return this; +} - this.add(object); // TODO handle render order +InstancedMesh2.prototype.addShadowLOD = function (geometry: BufferGeometry, material: Material, distance = 0, hysteresis = 0): InstancedMesh2 { + if (this._LOD) { + console.error("Cannot create LOD for this InstancedMesh2."); + return; + } + + if (!this.levels) { + this.levels = { render: null, shadowRender: null, objects: [] }; + } + + if (!this.levels.shadowRender) { + this.levels.shadowRender = { levels: [], indexes: [], count: [] }; + } + + this.addLevel(this.levels.shadowRender, geometry, material, distance, hysteresis); return this; } -InstancedMesh2.prototype.getObjectLODIndexForDistance = function(distance: number): number { - const levels = this.levels; +InstancedMesh2.prototype.addLevel = function (renderList: LODRenderList, geometry: BufferGeometry, material: Material, distance: number, hysteresis: number): void { + const objectsList = this.levels.objects; + const levels = renderList.levels; + let index; + let object: InstancedMesh2; + distance = distance ** 2; // to avoid to use Math.sqrt every time - 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; + const objIndex = objectsList.indexOf(object) + if (objIndex === -1) { + object = new InstancedMesh2(undefined, this._maxCount, geometry, material, this); // TODO fix renderer param + objectsList.push(object); + this.add(object); // TODO handle render order? + } else { + object = objectsList[objIndex]; } - return 0; + for (index = 0; index < levels.length; index++) { + if (distance < levels[index].distance) break; + } + + levels.splice(index, 0, { distance, hysteresis, object }); + renderList.count.push(0); + renderList.indexes.splice(index, 0, object._indexArray); } \ No newline at end of file diff --git a/src/core/feature/Raycasting.ts b/src/core/feature/Raycasting.ts index 79ec697..96cd015 100644 --- a/src/core/feature/Raycasting.ts +++ b/src/core/feature/Raycasting.ts @@ -16,7 +16,7 @@ const _worldScale = new Vector3(); const _invMatrixWorld = new Matrix4(); const _sphere = new Sphere(); -InstancedMesh2.prototype.raycast = function(raycaster: Raycaster, result: Intersection[]): void { +InstancedMesh2.prototype.raycast = function (raycaster: Raycaster, result: Intersection[]): void { if (this.material === undefined) return; const raycastFrustum = this.raycastOnlyFrustum && this._perObjectFrustumCulled && !this.bvh; @@ -64,7 +64,7 @@ InstancedMesh2.prototype.raycast = function(raycaster: Raycaster, result: Inters raycaster.far = originalFar; } -InstancedMesh2.prototype.checkObjectIntersection = function(raycaster: Raycaster, objectIndex: number, result: Intersection[]): void { +InstancedMesh2.prototype.checkObjectIntersection = function (raycaster: Raycaster, objectIndex: number, result: Intersection[]): void { if (objectIndex > this.instancesCount || !this.getVisibilityAt(objectIndex)) return; this.getMatrixAt(objectIndex, _mesh.matrixWorld);