@pnext/three-loader
Version:
Potree loader for ThreeJS, converted and adapted to Typescript.
417 lines (349 loc) • 12.7 kB
text/typescript
import {
Box3,
Camera,
Frustum,
Matrix4,
OrthographicCamera,
PerspectiveCamera,
Ray,
Vector2,
Vector3,
WebGLRenderer,
} from 'three';
import {
DEFAULT_POINT_BUDGET,
MAX_LOADS_TO_GPU,
MAX_NUM_NODES_LOADING,
PERSPECTIVE_CAMERA,
} from './constants';
import { FEATURES } from './features';
import { BinaryLoader, GetUrlFn, loadPOC } from './loading';
import { loadOctree } from './loading2/load-octree';
import { ClipMode } from './materials';
import { PointCloudOctree } from './point-cloud-octree';
import { PointCloudOctreeNode } from './point-cloud-octree-node';
import { PickParams, PointCloudOctreePicker } from './point-cloud-octree-picker';
import { isGeometryNode, isTreeNode } from './type-predicates';
import {
IPointCloudGeometryNode,
IPointCloudTreeNode,
IPotree,
IVisibilityUpdateResult,
PCOGeometry,
PickPoint,
} from './types';
import { BinaryHeap } from './utils/binary-heap';
import { Box3Helper } from './utils/box3-helper';
import { LRU } from './utils/lru';
export class QueueItem {
constructor(
public pointCloudIndex: number,
public weight: number,
public node: IPointCloudTreeNode,
public parent?: IPointCloudTreeNode | null,
) {}
}
type GeometryLoader = (
url: string,
getUrl: GetUrlFn,
xhrRequest: (input: RequestInfo, init?: RequestInit) => Promise<Response>,
loadHarmonics: boolean,
) => Promise<PCOGeometry>;
const GEOMETRY_LOADERS = {
v1: loadPOC,
v2: loadOctree,
} satisfies Record<string, GeometryLoader>;
export type PotreeVersion = keyof typeof GEOMETRY_LOADERS;
export class Potree implements IPotree {
private static picker: PointCloudOctreePicker | undefined;
private _pointBudget: number = DEFAULT_POINT_BUDGET;
private _rendererSize: Vector2 = new Vector2();
maxNumNodesLoading: number = MAX_NUM_NODES_LOADING;
features = FEATURES;
lru = new LRU(this._pointBudget);
private readonly loadGeometry: GeometryLoader;
constructor(version: PotreeVersion = 'v1') {
this.loadGeometry = GEOMETRY_LOADERS[version];
}
loadPointCloud(
url: string,
getUrl: GetUrlFn,
xhrRequest = (input: RequestInfo, init?: RequestInit) => fetch(input, init),
loadHarmonics: boolean = false,
): Promise<PointCloudOctree> {
return this.loadGeometry(url, getUrl, xhrRequest, loadHarmonics).then(
(geometry) => new PointCloudOctree(this, geometry, undefined, loadHarmonics),
);
}
updatePointClouds(
pointClouds: PointCloudOctree[],
camera: Camera,
renderer: WebGLRenderer,
callback = () => {},
): IVisibilityUpdateResult {
const result = this.updateVisibility(pointClouds, camera, renderer);
for (let i = 0; i < pointClouds.length; i++) {
const pointCloud = pointClouds[i];
if (pointCloud.disposed) {
continue;
}
pointCloud.material.updateMaterial(pointCloud, pointCloud.visibleNodes, camera, renderer);
pointCloud.updateVisibleBounds();
pointCloud.updateBoundingBoxes();
//For the splats
renderer.getSize(this._rendererSize);
pointCloud.updateSplats(camera, this._rendererSize, callback);
}
this.lru.freeMemory();
return result;
}
static pick(
pointClouds: PointCloudOctree[],
renderer: WebGLRenderer,
camera: Camera,
ray: Ray,
params: Partial<PickParams> = {},
): PickPoint | null {
Potree.picker = Potree.picker || new PointCloudOctreePicker();
return Potree.picker.pick(renderer, camera, ray, pointClouds, params);
}
get pointBudget(): number {
return this._pointBudget;
}
set pointBudget(value: number) {
if (value !== this._pointBudget) {
this._pointBudget = value;
this.lru.pointBudget = value;
this.lru.freeMemory();
}
}
static set maxLoaderWorkers(value: number) {
BinaryLoader.WORKER_POOL.maxWorkers = value;
}
static get maxLoaderWorkers(): number {
return BinaryLoader.WORKER_POOL.maxWorkers;
}
private updateVisibility(
pointClouds: PointCloudOctree[],
camera: Camera,
renderer: WebGLRenderer,
): IVisibilityUpdateResult {
let numVisiblePoints = 0;
const visibleNodes: PointCloudOctreeNode[] = [];
const unloadedGeometry: IPointCloudGeometryNode[] = [];
// calculate object space frustum and cam pos and setup priority queue
const { frustums, cameraPositions, priorityQueue } = this.updateVisibilityStructures(
pointClouds,
camera,
);
let loadedToGPUThisFrame = 0;
let exceededMaxLoadsToGPU = false;
let nodeLoadFailed = false;
let queueItem: QueueItem | undefined;
while ((queueItem = priorityQueue.pop()) !== undefined) {
let node = queueItem.node;
// If we will end up with too many points, we stop right away.
if (numVisiblePoints + node.numPoints > this.pointBudget) {
break;
}
const pointCloudIndex = queueItem.pointCloudIndex;
const pointCloud = pointClouds[pointCloudIndex];
const maxLevel = pointCloud.maxLevel !== undefined ? pointCloud.maxLevel : Infinity;
if (
node.level > maxLevel ||
!frustums[pointCloudIndex].intersectsBox(node.boundingBox) ||
this.shouldClip(pointCloud, node.boundingBox)
) {
continue;
}
numVisiblePoints += node.numPoints;
pointCloud.numVisiblePoints += node.numPoints;
const parentNode = queueItem.parent;
if (isGeometryNode(node) && (!parentNode || isTreeNode(parentNode))) {
if (node.loaded && loadedToGPUThisFrame < MAX_LOADS_TO_GPU) {
node = pointCloud.toTreeNode(node, parentNode);
loadedToGPUThisFrame++;
} else if (!node.failed) {
if (node.loaded && loadedToGPUThisFrame >= MAX_LOADS_TO_GPU) {
exceededMaxLoadsToGPU = true;
}
unloadedGeometry.push(node);
pointCloud.visibleGeometry.push(node);
} else {
nodeLoadFailed = true;
continue;
}
}
if (isTreeNode(node)) {
this.updateTreeNodeVisibility(pointCloud, node, visibleNodes);
pointCloud.visibleGeometry.push(node.geometryNode);
}
const halfHeight =
0.5 * renderer.getSize(this._rendererSize).height * renderer.getPixelRatio();
this.updateChildVisibility(
queueItem,
priorityQueue,
pointCloud,
node,
cameraPositions[pointCloudIndex],
camera,
halfHeight,
);
} // end priority queue loop
const numNodesToLoad = Math.min(this.maxNumNodesLoading, unloadedGeometry.length);
const nodeLoadPromises: Promise<void>[] = [];
for (let i = 0; i < numNodesToLoad; i++) {
nodeLoadPromises.push(unloadedGeometry[i].load());
}
return {
visibleNodes: visibleNodes,
numVisiblePoints: numVisiblePoints,
exceededMaxLoadsToGPU: exceededMaxLoadsToGPU,
nodeLoadFailed: nodeLoadFailed,
nodeLoadPromises: nodeLoadPromises,
};
}
private updateTreeNodeVisibility(
pointCloud: PointCloudOctree,
node: PointCloudOctreeNode,
visibleNodes: IPointCloudTreeNode[],
): void {
this.lru.touch(node.geometryNode);
const sceneNode = node.sceneNode;
sceneNode.visible = true;
sceneNode.material = pointCloud.material;
sceneNode.updateMatrix();
sceneNode.matrixWorld.multiplyMatrices(pointCloud.matrixWorld, sceneNode.matrix);
visibleNodes.push(node);
pointCloud.visibleNodes.push(node);
this.updateBoundingBoxVisibility(pointCloud, node);
}
private updateChildVisibility(
queueItem: QueueItem,
priorityQueue: BinaryHeap<QueueItem>,
pointCloud: PointCloudOctree,
node: IPointCloudTreeNode,
cameraPosition: Vector3,
camera: Camera,
halfHeight: number,
): void {
const children = node.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child === null) {
continue;
}
const sphere = child.boundingSphere;
const distance = sphere.center.distanceTo(cameraPosition);
const radius = sphere.radius;
let projectionFactor = 0.0;
if (camera.type === PERSPECTIVE_CAMERA) {
const perspective = camera as PerspectiveCamera;
const fov = (perspective.fov * Math.PI) / 180.0;
const slope = Math.tan(fov / 2.0);
projectionFactor = halfHeight / (slope * distance);
} else {
const orthographic = camera as OrthographicCamera;
projectionFactor = (2 * halfHeight) / (orthographic.top - orthographic.bottom);
}
const screenPixelRadius = radius * projectionFactor;
// Don't add the node if it'll be too small on the screen.
if (screenPixelRadius < pointCloud.minNodePixelSize) {
continue;
}
// Nodes which are larger will have priority in loading/displaying.
const weight = distance < radius ? Number.MAX_VALUE : screenPixelRadius + 1 / distance;
priorityQueue.push(new QueueItem(queueItem.pointCloudIndex, weight, child, node));
}
}
private updateBoundingBoxVisibility(
pointCloud: PointCloudOctree,
node: PointCloudOctreeNode,
): void {
if (pointCloud.showBoundingBox && !node.boundingBoxNode) {
const boxHelper = new Box3Helper(node.boundingBox);
boxHelper.matrixAutoUpdate = false;
pointCloud.boundingBoxNodes.push(boxHelper);
node.boundingBoxNode = boxHelper;
node.boundingBoxNode.matrix.copy(pointCloud.matrixWorld);
} else if (pointCloud.showBoundingBox && node.boundingBoxNode) {
node.boundingBoxNode.visible = true;
node.boundingBoxNode.matrix.copy(pointCloud.matrixWorld);
} else if (!pointCloud.showBoundingBox && node.boundingBoxNode) {
node.boundingBoxNode.visible = false;
}
}
private shouldClip(pointCloud: PointCloudOctree, boundingBox: Box3): boolean {
const material = pointCloud.material;
if (material.numClipBoxes === 0 || material.clipMode !== ClipMode.CLIP_OUTSIDE) {
return false;
}
const box2 = boundingBox.clone();
pointCloud.updateMatrixWorld(true);
box2.applyMatrix4(pointCloud.matrixWorld);
const clipBoxes = material.clipBoxes;
for (let i = 0; i < clipBoxes.length; i++) {
const clipMatrixWorld = clipBoxes[i].matrix;
const clipBoxWorld = new Box3(
new Vector3(-0.5, -0.5, -0.5),
new Vector3(0.5, 0.5, 0.5),
).applyMatrix4(clipMatrixWorld);
if (box2.intersectsBox(clipBoxWorld)) {
return false;
}
}
return true;
}
private updateVisibilityStructures = (() => {
const frustumMatrix = new Matrix4();
const inverseWorldMatrix = new Matrix4();
const cameraMatrix = new Matrix4();
return (
pointClouds: PointCloudOctree[],
camera: Camera,
): {
frustums: Frustum[];
cameraPositions: Vector3[];
priorityQueue: BinaryHeap<QueueItem>;
} => {
const frustums: Frustum[] = [];
const cameraPositions: Vector3[] = [];
const priorityQueue = new BinaryHeap<QueueItem>((x) => 1 / x.weight);
for (let i = 0; i < pointClouds.length; i++) {
const pointCloud = pointClouds[i];
if (!pointCloud.initialized()) {
continue;
}
pointCloud.numVisiblePoints = 0;
pointCloud.visibleNodes = [];
pointCloud.visibleGeometry = [];
camera.updateMatrixWorld(false);
// Furstum in object space.
const inverseViewMatrix = camera.matrixWorldInverse;
const worldMatrix = pointCloud.matrixWorld;
frustumMatrix
.identity()
.multiply(camera.projectionMatrix)
.multiply(inverseViewMatrix)
.multiply(worldMatrix);
frustums.push(new Frustum().setFromProjectionMatrix(frustumMatrix));
// Camera position in object space
inverseWorldMatrix.copy(worldMatrix).invert();
cameraMatrix.identity().multiply(inverseWorldMatrix).multiply(camera.matrixWorld);
cameraPositions.push(new Vector3().setFromMatrixPosition(cameraMatrix));
if (pointCloud.visible && pointCloud.root !== null) {
const weight = Number.MAX_VALUE;
priorityQueue.push(new QueueItem(i, weight, pointCloud.root));
}
// Hide any previously visible nodes. We will later show only the needed ones.
if (isTreeNode(pointCloud.root)) {
pointCloud.hideDescendants(pointCloud.root.sceneNode);
}
for (const boundingBoxNode of pointCloud.boundingBoxNodes) {
boundingBoxNode.visible = false;
}
}
return { frustums, cameraPositions, priorityQueue };
};
})();
}