@three.ez/instanced-mesh
Version:
Enhanced InstancedMesh with frustum culling, fast raycasting (using BVH), sorting, visibility management and more.
278 lines • 11.8 kB
JavaScript
import { Frustum, Matrix4, Sphere, Vector3 } from 'three';
import { sortOpaque, sortTransparent } from '../../utils/SortingUtils.js';
import { InstancedMesh2 } from '../InstancedMesh2.js';
import { InstancedRenderList } from '../utils/InstancedRenderList.js';
const _frustum = new Frustum();
const _renderList = new InstancedRenderList();
const _projScreenMatrix = new Matrix4();
const _invMatrixWorld = new Matrix4();
const _forward = new Vector3();
const _cameraPos = new Vector3();
const _cameraLODPos = new Vector3();
const _position = new Vector3();
const _sphere = new Sphere();
InstancedMesh2.prototype.performFrustumCulling = function (camera, cameraLOD = camera) {
const mainMesh = this._parentLOD ?? this;
const LODinfo = mainMesh.LODinfo;
let LODrenderList;
if (LODinfo) {
const isShadowRendering = camera !== cameraLOD;
LODrenderList = !isShadowRendering ? LODinfo.render : (LODinfo.shadowRender ?? LODinfo.render);
for (const object of LODinfo.objects) {
object.count = 0;
}
}
else {
mainMesh.count = 0;
}
if (mainMesh._instancesArrayCount === 0)
return;
if (LODrenderList?.levels.length > 0)
mainMesh.frustumCullingLOD(LODrenderList, camera, cameraLOD);
else
mainMesh.frustumCulling(camera);
};
InstancedMesh2.prototype.frustumCullingAlreadyPerformed = function (frame, camera, shadowCamera) {
const lastRenderInfo = this._lastRenderInfo;
if (lastRenderInfo.frame === frame && lastRenderInfo.camera === camera && lastRenderInfo.shadowCamera === shadowCamera) {
return true;
}
lastRenderInfo.frame = frame;
lastRenderInfo.camera = camera;
lastRenderInfo.shadowCamera = shadowCamera;
return false;
};
InstancedMesh2.prototype.frustumCulling = function (camera) {
const sortObjects = this._sortObjects;
const perObjectFrustumCulled = this._perObjectFrustumCulled;
const array = this.instanceIndex.array;
this.instanceIndex._needsUpdate = true; // TODO improve
if (!perObjectFrustumCulled && !sortObjects) {
this.updateIndexArray();
return;
}
if (sortObjects) {
_invMatrixWorld.copy(this.matrixWorld).invert();
_cameraPos.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(_invMatrixWorld);
_forward.set(0, 0, -1).transformDirection(camera.matrixWorld).transformDirection(_invMatrixWorld);
}
if (!perObjectFrustumCulled) {
this.updateRenderList();
}
else {
_projScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse).multiply(this.matrixWorld);
if (this.bvh)
this.BVHCulling(camera);
else
this.linearCulling(camera);
}
if (sortObjects) {
const customSort = this.customSort;
if (customSort === null) {
_renderList.array.sort(!this.material?.transparent ? sortOpaque : sortTransparent);
}
else {
customSort(_renderList.array);
}
const list = _renderList.array;
const count = list.length;
for (let i = 0; i < count; i++) {
array[i] = list[i].index;
}
this.count = count;
_renderList.reset();
}
};
InstancedMesh2.prototype.updateIndexArray = function () {
if (!this._indexArrayNeedsUpdate)
return;
const array = this.instanceIndex.array;
const instancesArrayCount = this._instancesArrayCount;
let count = 0;
for (let i = 0; i < instancesArrayCount; i++) {
if (this.getActiveAndVisibilityAt(i)) {
array[count++] = i;
}
}
this.count = count;
this._indexArrayNeedsUpdate = false;
};
InstancedMesh2.prototype.updateRenderList = function () {
const instancesArrayCount = this._instancesArrayCount;
for (let i = 0; i < instancesArrayCount; i++) {
if (this.getActiveAndVisibilityAt(i)) {
const depth = this.getPositionAt(i).sub(_cameraPos).dot(_forward);
_renderList.push(depth, i);
}
}
};
InstancedMesh2.prototype.BVHCulling = function (camera) {
const array = this.instanceIndex.array;
const instancesArrayCount = this._instancesArrayCount;
const sortObjects = this._sortObjects;
const onFrustumEnter = this.onFrustumEnter;
let count = 0;
this.bvh.frustumCulling(_projScreenMatrix, (node) => {
const index = node.object;
// TODO check if (index < instancesArrayCount) is still necessary after last update
// we don't check if active because we remove inactive instances from BVH
if (index < instancesArrayCount && this.getVisibilityAt(index) && (!onFrustumEnter || onFrustumEnter(index, camera))) {
if (sortObjects) {
const depth = this.getPositionAt(index).sub(_cameraPos).dot(_forward);
_renderList.push(depth, index);
}
else {
array[count++] = index;
}
}
});
this.count = count;
};
InstancedMesh2.prototype.linearCulling = function (camera) {
const array = this.instanceIndex.array;
if (!this.geometry.boundingSphere)
this.geometry.computeBoundingSphere();
const bSphere = this._geometry.boundingSphere;
const radius = bSphere.radius;
const center = bSphere.center;
const instancesArrayCount = this._instancesArrayCount;
const geometryCentered = center.x === 0 && center.y === 0 && center.z === 0;
const sortObjects = this._sortObjects;
const onFrustumEnter = this.onFrustumEnter;
let count = 0;
_frustum.setFromProjectionMatrix(_projScreenMatrix);
for (let i = 0; i < instancesArrayCount; i++) {
if (!this.getActiveAndVisibilityAt(i))
continue;
if (geometryCentered) {
const maxScale = this.getPositionAndMaxScaleOnAxisAt(i, _sphere.center);
_sphere.radius = radius * maxScale;
}
else {
this.applyMatrixAtToSphere(i, _sphere, center, radius);
}
if (_frustum.intersectsSphere(_sphere) && (!onFrustumEnter || onFrustumEnter(i, camera))) {
if (sortObjects) {
const depth = _position.subVectors(_sphere.center, _cameraPos).dot(_forward);
_renderList.push(depth, i);
}
else {
array[count++] = i;
}
}
}
this.count = count;
};
InstancedMesh2.prototype.frustumCullingLOD = function (LODrenderList, camera, cameraLOD) {
const { count, levels } = LODrenderList;
const isShadowRendering = camera !== cameraLOD;
const sortObjects = !isShadowRendering && this._sortObjects; // sort is disabled when render shadows
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);
_cameraLODPos.setFromMatrixPosition(cameraLOD.matrixWorld).applyMatrix4(_invMatrixWorld);
const indexes = LODrenderList.levels.map((x) => x.object.instanceIndex.array);
if (this.bvh)
this.BVHCullingLOD(LODrenderList, indexes, sortObjects, camera, cameraLOD);
else
this.linearCullingLOD(LODrenderList, indexes, sortObjects, camera, cameraLOD);
if (sortObjects) {
const customSort = this.customSort;
const list = _renderList.array;
let levelIndex = 0;
let levelDistance = levels[1].distance;
if (customSort === null) {
list.sort(!levels[0].object.material?.transparent ? sortOpaque : sortTransparent); // TODO improve multimaterial handling
}
else {
customSort(list);
}
for (let i = 0, l = list.length; i < l; i++) {
const item = list[i];
if (item.depth > levelDistance) {
levelIndex++;
levelDistance = levels[levelIndex + 1]?.distance ?? Infinity; // improve this condition and use for of instead
}
indexes[levelIndex][count[levelIndex]++] = item.index;
}
_renderList.reset();
}
for (let i = 0; i < levels.length; i++) {
const object = levels[i].object;
object.count = count[i];
}
};
InstancedMesh2.prototype.BVHCullingLOD = function (LODrenderList, indexes, sortObjects, camera, cameraLOD) {
const { count, levels } = LODrenderList;
const instancesArrayCount = this._instancesArrayCount;
const onFrustumEnter = this.onFrustumEnter;
if (sortObjects) {
this.bvh.frustumCulling(_projScreenMatrix, (node) => {
const index = node.object;
// we don't check if active because we remove inactive instances from BVH
if (index < instancesArrayCount && this.getVisibilityAt(index) && (!onFrustumEnter || onFrustumEnter(index, camera, cameraLOD))) {
const distance = this.getPositionAt(index).distanceToSquared(_cameraLODPos);
_renderList.push(distance, index);
}
});
}
else {
this.bvh.frustumCullingLOD(_projScreenMatrix, _cameraLODPos, levels, (node, level) => {
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 (!onFrustumEnter || onFrustumEnter(index, camera, cameraLOD, level)) {
indexes[level][count[level]++] = index;
}
}
});
}
};
InstancedMesh2.prototype.linearCullingLOD = function (LODrenderList, indexes, sortObjects, camera, cameraLOD) {
const { count, levels } = LODrenderList;
if (!this.geometry.boundingSphere)
this.geometry.computeBoundingSphere();
const bSphere = this._geometry.boundingSphere;
const radius = bSphere.radius;
const center = bSphere.center;
const instancesArrayCount = this._instancesArrayCount;
const geometryCentered = center.x === 0 && center.y === 0 && center.z === 0;
const onFrustumEnter = this.onFrustumEnter;
_frustum.setFromProjectionMatrix(_projScreenMatrix);
for (let i = 0; i < instancesArrayCount; i++) {
if (!this.getActiveAndVisibilityAt(i))
continue;
if (geometryCentered) {
const maxScale = this.getPositionAndMaxScaleOnAxisAt(i, _sphere.center);
_sphere.radius = radius * maxScale;
}
else {
this.applyMatrixAtToSphere(i, _sphere, center, radius);
}
if (_frustum.intersectsSphere(_sphere)) {
if (sortObjects) {
if (!onFrustumEnter || onFrustumEnter(i, camera, cameraLOD))
continue;
const distance = _sphere.center.distanceToSquared(_cameraLODPos);
_renderList.push(distance, i);
}
else {
const distance = _sphere.center.distanceToSquared(_cameraLODPos);
const levelIndex = this.getObjectLODIndexForDistance(levels, distance);
if (!onFrustumEnter || onFrustumEnter(i, camera, cameraLOD, levelIndex)) {
indexes[levelIndex][count[levelIndex]++] = i;
}
}
}
}
};
//# sourceMappingURL=FrustumCulling.js.map