@pnext/three-loader
Version:
Potree loader for ThreeJS, converted and adapted to Typescript.
330 lines (274 loc) • 9.53 kB
text/typescript
import {
Mesh,
PerspectiveCamera,
Scene,
Vector2,
WebGLRenderer,
Raycaster,
WebGLRenderTarget,
NearestFilter,
SphereGeometry,
MeshBasicMaterial,
FloatType,
RGFormat,
Vector3,
} from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { PointCloudOctree, Potree, PotreeVersion } from '../src';
export class Viewer {
public enableUpdate: boolean = true;
/**
* The element where we will insert our canvas.
*/
private targetEl: HTMLElement | undefined;
/**
* The ThreeJS renderer used to render the scene.
*/
private renderer = new WebGLRenderer();
/**
* Our scene which will contain the point cloud.
*/
scene: Scene = new Scene();
globalScene: Scene = new Scene();
/**
* The camera used to view the scene.
*/
camera: PerspectiveCamera = new PerspectiveCamera(45, NaN, 0.1, 1000);
/**
* Controls which update the position of the camera.
*/
cameraControls!: any;
/**
* Out potree instance which handles updating point clouds, keeps track of loaded nodes, etc.
*/
private potree_v1 = new Potree('v1');
private potree_v2 = new Potree('v2');
/**
* Array of point clouds which are in the scene and need to be updated.
*/
pointClouds: PointCloudOctree[] = [];
/**
* The time (milliseconds) when `loop()` was last called.
*/
private prevTime: number | undefined;
/**
* requestAnimationFrame handle we can use to cancel the viewer loop.
*/
private reqAnimationFrameHandle: number | undefined;
/**
* Initializes the viewer into the specified element.
*
* @param targetEl
* The element into which we should add the canvas where we will render the scene.
*/
private IDRenderTarget: any;
private raycastSplat: any;
private raycastSplatDebug: any;
private elapsedTime: number = 0;
private raycaster = new Raycaster();
//Max amount of points available to render harmonics inside a 4096 x 4096 texture
//anything above 2.300.000 particles will require a higher texture and could break.
private pointBudget = 1200000;
async initialize(targetEl: HTMLElement): Promise<void> {
if (this.targetEl || !targetEl) {
return;
}
this.potree_v2.pointBudget = this.pointBudget;
//setup the splats manager
this.globalScene = new Scene();
this.IDRenderTarget = new WebGLRenderTarget(1, 1, {
minFilter: NearestFilter,
magFilter: NearestFilter,
format: RGFormat,
type: FloatType,
});
const mat = new MeshBasicMaterial({ color: '#ffffff' });
mat.transparent = true;
mat.wireframe = true;
const planeGeo = new SphereGeometry(1);
this.raycastSplat = new Mesh(planeGeo, mat);
this.raycastSplat.renderOrder = 100;
const mat2 = new MeshBasicMaterial({ color: '#ff0000' });
mat.transparent = true;
const sphereGeo = new SphereGeometry(0.005);
this.raycastSplatDebug = new Mesh(sphereGeo, mat2);
this.raycastSplatDebug.renderOrder = 10000;
this.targetEl = targetEl;
targetEl.appendChild(this.renderer.domElement);
this.cameraControls = new OrbitControls(this.camera, this.targetEl);
this.cameraControls.target.set(9, 3, -6.5);
this.resize();
window.addEventListener('resize', this.resize);
targetEl.addEventListener('mousedown', (_) => {
this.elapsedTime = Date.now();
});
targetEl.addEventListener('mouseup', this.updateCameraTarget.bind(this));
requestAnimationFrame(this.loop);
}
updateCameraTarget(e: any) {
if (!this.pointClouds[0]?.splatsMesh?.splatsEnabled) return;
let clickTime = Date.now();
let deltaTime = clickTime - this.elapsedTime;
if (deltaTime < 200) {
const rgba = new Float32Array(4);
const DPR = this.renderer?.getPixelRatio() || 1;
this.renderer?.readRenderTargetPixels(
this.IDRenderTarget,
e.clientX * DPR,
DPR * (window.innerHeight - e.clientY),
1,
1,
rgba,
);
const globalID = rgba[0];
const nodeID = rgba[1];
const splatData = this.pointClouds[0].splatsMesh.getSplatData(globalID, nodeID);
if (splatData != null) {
let scale = splatData.scale;
if (scale.x === 0) scale.x = 0.0001;
if (scale.y === 0) scale.y = 0.0001;
if (scale.z === 0) scale.z = 0.0001;
scale.multiplyScalar(2.82842712475);
this.raycastSplat.position.copy(splatData.position);
this.raycastSplat.scale.copy(scale);
this.raycastSplat.quaternion.copy(splatData.orientation);
this.raycastSplat.updateMatrix();
this.raycastSplat.updateMatrixWorld();
let mousePosition = new Vector2(
e.clientX / window.innerWidth,
e.clientY / window.innerHeight,
);
mousePosition.x = 2 * mousePosition.x - 1;
mousePosition.y = -2 * mousePosition.y + 1;
this.raycaster.setFromCamera(mousePosition, this.camera);
const intersects = this.raycaster.intersectObject(this.raycastSplat);
let center = new Vector3(Infinity, Infinity, Infinity);
if (intersects.length > 0) {
center = intersects[0].point;
this.raycastSplatDebug.position.copy(center);
this.cameraControls.target.copy(center);
} else {
this.cameraControls.target.copy(splatData.position);
}
deltaTime = clickTime;
}
}
}
/**
* Performs any cleanup necessary to destroy/remove the viewer from the page.
*/
destroy(): void {
if (this.targetEl) {
this.targetEl.removeChild(this.renderer.domElement);
this.targetEl = undefined;
}
window.removeEventListener('resize', this.resize);
// TODO: clean point clouds or other objects added to the scene.
if (this.reqAnimationFrameHandle !== undefined) {
cancelAnimationFrame(this.reqAnimationFrameHandle);
}
}
/**
* Loads a point cloud into the viewer and returns it.
*
* @param fileName
* The name of the point cloud which is to be loaded.
* @param baseUrl
* The url where the point cloud is located and from where we should load the octree nodes.
*/
async load(
fileName: string,
baseUrl: string,
version: PotreeVersion = 'v1',
loadHarmonics: boolean = false,
): Promise<PointCloudOctree> {
const loader = version === 'v1' ? this.potree_v1 : this.potree_v2;
return loader.loadPointCloud(
// The file name of the point cloud which is to be loaded.
fileName,
// Given the relative URL of a file, should return a full URL.
(url) => `${baseUrl}${url}`,
undefined,
//Load the harmonics if necessary (for desktop only)
loadHarmonics,
);
}
add(pco: PointCloudOctree): void {
this.scene.add(pco);
this.pointClouds.push(pco);
}
disposePointCloud(pointCloud: PointCloudOctree): void {
this.scene.remove(pointCloud);
pointCloud.dispose();
this.pointClouds = this.pointClouds.filter((pco) => pco !== pointCloud);
}
async renderAsSplats(): Promise<Viewer> {
return this;
}
/**
* Updates the point clouds, cameras or any other objects which are in the scene.
*
* @param dt
* The time, in milliseconds, since the last update.
*/
update(_: number): void {
// Alternatively, you could use Three's OrbitControls or any other
// camera control system.
this.cameraControls.update();
// This is where most of the potree magic happens. It updates the
// visiblily of the octree nodes based on the camera frustum and it
// triggers any loads/unloads which are necessary to keep the number
// of visible points in check.
this.potree_v1.updatePointClouds(this.pointClouds, this.camera, this.renderer);
this.potree_v2.updatePointClouds(this.pointClouds, this.camera, this.renderer);
}
/**
* Renders the scene into the canvas.
*/
render(): void {
this.renderer.clear();
//This is used to setup the different nodes of the Octree from Potree
this.renderer.render(this.scene, this.camera);
if (this.pointClouds[0]?.splatsMesh?.splatsEnabled) {
const h = this.renderer.domElement.height || 1;
const w = this.renderer.domElement.width || 1;
this.IDRenderTarget.setSize(w, h);
//Setup the splats to render in ID mode
this.pointClouds[0].splatsMesh.renderSplatsIDs(true);
this.renderer.setRenderTarget(this.IDRenderTarget);
this.renderer.clear();
this.globalScene.add(this.pointClouds[0]);
this.renderer.render(this.globalScene, this.camera);
this.scene.add(this.pointClouds[0]);
this.pointClouds[0].splatsMesh.renderSplatsIDs(false);
this.renderer.setRenderTarget(null);
}
}
/**
* The main loop of the viewer, called at 60FPS, if possible.
*/
loop = (time: number): void => {
this.reqAnimationFrameHandle = requestAnimationFrame(this.loop);
const prevTime = this.prevTime;
this.prevTime = time;
if (prevTime === undefined) {
return;
}
this.update(time - prevTime);
this.render();
};
/**
* Triggered anytime the window gets resized.
*/
resize = () => {
if (!this.targetEl) {
return;
}
const { width, height } = this.targetEl.getBoundingClientRect();
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setSize(width, height);
const size = new Vector2();
this.renderer.getSize(size);
};
}