Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/core/InstancedMesh2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import { SquareDataTexture } from './utils/SquareDataTexture.js';
* Parameters for configuring an `InstancedMesh2` instance.
*/
export interface InstancedMesh2Params {
/**
* If `true`, LOD uses camera distance; otherwise it uses screen size.
* @default undefined
*/
useDistanceForLOD?: boolean;
/**
* Determines the maximum number of instances that buffers can hold.
* The buffers will be expanded automatically if necessary.
Expand Down Expand Up @@ -254,14 +259,15 @@ export class InstancedMesh2<
if (!geometry) throw new Error('"geometry" is mandatory.');
if (!material) throw new Error('"material" is mandatory.');

const { allowsEuler, renderer, createEntities } = params;
const { allowsEuler, renderer, createEntities, useDistanceForLOD } = params;

super(geometry, null);

const capacity = params.capacity > 0 ? params.capacity : _defaultCapacity;
this._renderer = renderer;
this._capacity = capacity;
this._parentLOD = LOD;
this._useDistanceForLOD = useDistanceForLOD;
this._geometry = geometry;
this.material = material;
this._allowsEuler = allowsEuler ?? false;
Expand Down
2 changes: 1 addition & 1 deletion src/core/InstancedMeshBVH.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class InstancedMeshBVH {

const levelsArray = this.LODsMap.get(levels);
for (let i = 0; i < levels.length; i++) {
levelsArray[i] = levels[i].distance;
levelsArray[i] = levels[i].metricSquared;
}

const camera = this._cameraPos;
Expand Down
94 changes: 78 additions & 16 deletions src/core/feature/FrustumCulling.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BVHNode } from 'bvh.js';
import { Camera, Frustum, Material, Matrix4, Sphere, Vector3 } from 'three';
import { Camera, Frustum, Material, Matrix4, OrthographicCamera, PerspectiveCamera, Sphere, Vector3 } from 'three';
import { sortOpaque, sortTransparent } from '../../utils/SortingUtils.js';
import { InstancedMesh2 } from '../InstancedMesh2.js';
import { InstancedRenderItem, InstancedRenderList } from '../utils/InstancedRenderList.js';
Expand Down Expand Up @@ -53,6 +53,7 @@ const _cameraPos = new Vector3();
const _cameraLODPos = new Vector3();
const _position = new Vector3();
const _sphere = new Sphere();
const _instanceMatrix = new Matrix4();

InstancedMesh2.prototype.performFrustumCulling = function (camera: Camera, cameraLOD = camera) {
const mainMesh = this._parentLOD ?? this;
Expand Down Expand Up @@ -250,11 +251,12 @@ InstancedMesh2.prototype.frustumCullingLOD = function (LODrenderList: LODRenderL
if (this.bvh) this.BVHCullingLOD(LODrenderList, indexes, sortObjects, camera, cameraLOD);
else this.linearCullingLOD(LODrenderList, indexes, sortObjects, camera, cameraLOD);

// todo this doesn't support screen space LOD
if (sortObjects) {
const customSort = this.customSort;
const list = _renderList.array;
let levelIndex = 0;
let levelDistance = levels[1].distance;
let levelDistance = levels[1].metricSquared;

if (customSort === null) {
list.sort(!(levels[0].object.material as Material)?.transparent ? sortOpaque : sortTransparent); // TODO improve multimaterial handling
Expand All @@ -267,7 +269,7 @@ InstancedMesh2.prototype.frustumCullingLOD = function (LODrenderList: LODRenderL

if (item.depth > levelDistance) {
levelIndex++;
levelDistance = levels[levelIndex + 1]?.distance ?? Infinity; // improve this condition and use for of instead
levelDistance = levels[levelIndex + 1]?.metricSquared ?? Infinity; // improve this condition and use for of instead
}

indexes[levelIndex][count[levelIndex]++] = item.index;
Expand All @@ -287,6 +289,13 @@ InstancedMesh2.prototype.BVHCullingLOD = function (LODrenderList: LODRenderList,
const instancesArrayCount = this._instancesArrayCount;
const onFrustumEnter = this.onFrustumEnter;

const orthographicCamera = camera as OrthographicCamera;
const viewSize = (orthographicCamera.top - orthographicCamera.bottom) / orthographicCamera.zoom;
const perspectiveCamera = camera as PerspectiveCamera;
const invProj = (Math.tan(perspectiveCamera.fov * 0.5 * (Math.PI / 180))) ** 2;
const useDistanceForLOD = this._useDistanceForLOD;
const isPerspectiveCamera = perspectiveCamera.isPerspectiveCamera;

if (sortObjects) {
this.bvh.frustumCulling(_projScreenMatrix, (node: BVHNode<{}, number>) => {
const index = node.object;
Expand All @@ -297,22 +306,62 @@ InstancedMesh2.prototype.BVHCullingLOD = function (LODrenderList: LODRenderList,
}
});
} else {
this.bvh.frustumCullingLOD(_projScreenMatrix, _cameraLODPos, levels, (node: BVHNode<{}, number>, level: number) => {
const index = node.object;
if (index < instancesArrayCount && this.getVisibilityAt(index)) {
if (level === null) {
const distance = this.getPositionAt(index).distanceToSquared(_cameraLODPos); // distance can be get by BVH, but is not the distance from center
level = this.getObjectLODIndexForDistance(levels, distance);
if (useDistanceForLOD) {
// take advantage of frustumCullingLOD method to get distances and LOD from the bvh itself
this.bvh.frustumCullingLOD(_projScreenMatrix, _cameraLODPos, levels, (node: BVHNode<{}, number>, level: number) => {
const index = node.object;
if (index < instancesArrayCount && this.getVisibilityAt(index)) {
if (level === null) {
let metric: number;
if (isPerspectiveCamera) {
const distance = this.getPositionAt(index).distanceToSquared(_cameraLODPos); // distance can be get by BVH, but is not the distance from center
metric = getLODMetricPerspective(useDistanceForLOD, _sphere, invProj, distance);
} else {
metric = getLODMetricOrthographic(useDistanceForLOD, _sphere, viewSize);
}
level = this.getObjectLODIndex(levels, metric, isPerspectiveCamera);
}

if (!onFrustumEnter || onFrustumEnter(index, camera, cameraLOD, level)) {
indexes[level][count[level]++] = index;
}
}

if (!onFrustumEnter || onFrustumEnter(index, camera, cameraLOD, level)) {
indexes[level][count[level]++] = index;
});
} else {
this.bvh.frustumCulling(_projScreenMatrix, (node: BVHNode<{}, number>) => {
const index = node.object;
if (index < instancesArrayCount && this.getVisibilityAt(index)) {
let metric: number;
this.getMatrixAt(index, _instanceMatrix);
_sphere.radius = this._geometry.boundingSphere.radius;
_sphere.radius *= _instanceMatrix.getMaxScaleOnAxis();
if (isPerspectiveCamera) {
const distance = this.getPositionAt(index).distanceToSquared(_cameraLODPos); // distance can be get by BVH, but is not the distance from center
metric = getLODMetricPerspective(useDistanceForLOD, _sphere, invProj, distance);
} else {
metric = getLODMetricOrthographic(useDistanceForLOD, _sphere, viewSize);
}
const level = this.getObjectLODIndex(levels, metric, isPerspectiveCamera);

if (!onFrustumEnter || onFrustumEnter(index, camera, cameraLOD, level)) {
indexes[level][count[level]++] = index;
}
}
}
});
});
}
}
};

function getLODMetricPerspective(useDistanceForLOD: boolean, sphere: Sphere, invProj: number, distance: number): number {
return useDistanceForLOD ? distance : ((sphere.radius ** 2) / (distance * invProj));
}

function getLODMetricOrthographic(useDistanceForLOD: boolean, sphere: Sphere, viewHeight: number): number {
// TODO refactor
if (useDistanceForLOD) throw new Error('BatchedMesh: useDistanceForLOD cannot be used with orthographic camera.');
return sphere.radius * 2 / viewHeight;
}

InstancedMesh2.prototype.linearCullingLOD = function (LODrenderList: LODRenderList, indexes: Uint32Array[], sortObjects: boolean, camera: Camera, cameraLOD: Camera) {
const { count, levels } = LODrenderList;
if (!this.geometry.boundingSphere) this.geometry.computeBoundingSphere();
Expand All @@ -325,6 +374,13 @@ InstancedMesh2.prototype.linearCullingLOD = function (LODrenderList: LODRenderLi

_frustum.setFromProjectionMatrix(_projScreenMatrix);

const orthographicCamera = camera as OrthographicCamera;
const viewSize = (orthographicCamera.top - orthographicCamera.bottom) / orthographicCamera.zoom;
const perspectiveCamera = camera as PerspectiveCamera;
const invProj = (Math.tan(perspectiveCamera.fov * 0.5 * (Math.PI / 180))) ** 2;
const useDistanceForLOD = this._useDistanceForLOD;
const isPerspectiveCamera = perspectiveCamera.isPerspectiveCamera;

for (let i = 0; i < instancesArrayCount; i++) {
if (!this.getActiveAndVisibilityAt(i)) continue;

Expand All @@ -342,8 +398,14 @@ InstancedMesh2.prototype.linearCullingLOD = function (LODrenderList: LODRenderLi
const distance = _sphere.center.distanceToSquared(_cameraLODPos);
_renderList.push(distance, i);
} else {
const distance = _sphere.center.distanceToSquared(_cameraLODPos);
const levelIndex = this.getObjectLODIndexForDistance(levels, distance);
let metric: number;
if (isPerspectiveCamera) {
const distance = _sphere.center.distanceToSquared(_cameraLODPos);
metric = getLODMetricPerspective(useDistanceForLOD, _sphere, invProj, distance);
} else {
metric = getLODMetricOrthographic(useDistanceForLOD, _sphere, viewSize);
}
const levelIndex = this.getObjectLODIndex(levels, metric, isPerspectiveCamera);

if (!onFrustumEnter || onFrustumEnter(i, camera, cameraLOD, levelIndex)) {
indexes[levelIndex][count[levelIndex]++] = i;
Expand Down
Loading