diff --git a/examples-experimental/potree/package.json b/examples-experimental/potree/package.json
index f8a535387d..51324db85d 100644
--- a/examples-experimental/potree/package.json
+++ b/examples-experimental/potree/package.json
@@ -17,8 +17,8 @@
"@deck.gl/layers": "^9.0.32",
"@deck.gl/mesh-layers": "^9.0.32",
"@deck.gl/react": "^9.0.32",
- "@loaders.gl/potree": "4.4.0-alpha.0",
- "@loaders.gl/loader-utils": "4.4.0-alpha.0",
+ "@loaders.gl/potree": "4.4.0-alpha.1",
+ "@loaders.gl/loader-utils": "4.4.0-alpha.1",
"maplibre-gl": "^4.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/examples-experimental/potree/src/app.tsx b/examples-experimental/potree/src/app.tsx
index 97717c80a0..7e7d803bb1 100644
--- a/examples-experimental/potree/src/app.tsx
+++ b/examples-experimental/potree/src/app.tsx
@@ -1,11 +1,11 @@
-import React, {useState, useEffect} from 'react';
+import React, {useState} from 'react';
import {createRoot} from 'react-dom/client';
-import Map from 'react-map-gl/maplibre';
+import {Map} from 'react-map-gl/maplibre';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
-import DeckGL from '@deck.gl/react';
+import {DeckGL} from '@deck.gl/react';
import {MapViewState} from '@deck.gl/core';
import {PotreeSource} from '@loaders.gl/potree';
@@ -14,14 +14,14 @@ import {PotreeTile3DLayer} from './potree-tile-3d-layer';
export const TRANSITION_DURAITON = 4000;
const INITIAL_VIEW_STATE = {
- longitude: -90,
- latitude: 34,
+ longitude: 5.9822,
+ latitude: 51.805,
pitch: 0,
maxPitch: 90,
bearing: 0,
minZoom: 2,
maxZoom: 30,
- zoom: 3
+ zoom: 15
};
export default function App() {
@@ -30,6 +30,7 @@ export default function App() {
function renderLayers() {
const layers = new PotreeTile3DLayer({
data: 'https://raw.githubusercontent.com/visgl/deck.gl-data/refs/heads/master/formats/potree/1.8/3dm_32_291_5744_1_nw-converted',
+ pointSize: 5,
source: PotreeSource
});
return [layers];
@@ -41,8 +42,7 @@ export default function App() {
diff --git a/examples-experimental/potree/src/pointcloud-tileset.ts b/examples-experimental/potree/src/pointcloud-tileset.ts
index 6ea29daef3..7cee88b563 100644
--- a/examples-experimental/potree/src/pointcloud-tileset.ts
+++ b/examples-experimental/potree/src/pointcloud-tileset.ts
@@ -1,5 +1,7 @@
import {Vector3} from '@math.gl/core';
import {DataSource} from '@loaders.gl/loader-utils';
+import {POTreeNode} from '@loaders.gl/potree';
+import {PotreeTraverser} from './potree-traverser';
/** Deck.gl Viewport instance type.
* We can't import it from Deck.gl to avoid circular reference */
@@ -19,24 +21,35 @@ export type Viewport = {
};
type PointcloudTilesetProps = {
- /** Delay time before the tileset traversal. It prevents traversal requests spam.*/
- debounceTime: number;
- };
-
- const DEFAULT_PROPS: PointcloudTilesetProps = {
- debounceTime: 0,
- };
+ /** Delay time before the tileset traversal. It prevents traversal requests spam.*/
+ debounceTime: number;
+
+ onTileLoaded?: (node: POTreeNode) => void;
+};
+
+const DEFAULT_PROPS: PointcloudTilesetProps = {
+ debounceTime: 0
+};
export class PointcloudTileset {
private frameNumber: number = 0;
private lastUpdatedVieports: Viewport[] | Viewport | null = null;
private updatePromise: Promise | null = null;
+ private _selectedNodes: POTreeNode[] = [];
+ private traverser: PotreeTraverser;
public options: PointcloudTilesetProps;
- constructor(public dataSource: DataSource, options?: Partial) {
- // PUBLIC MEMBERS
+ constructor(
+ public dataSource: DataSource,
+ options?: Partial
+ ) {
this.options = {...DEFAULT_PROPS, ...options};
+ this.traverser = new PotreeTraverser();
+
+ this.dataSource.init().then(() => {
+ this.traverser.root = this.dataSource.root;
+ });
}
get isReady() {
@@ -45,7 +58,7 @@ export class PointcloudTileset {
}
get tiles() {
- return [];
+ return this._selectedNodes;
}
get isLoaded() {
@@ -61,12 +74,42 @@ export class PointcloudTileset {
if (!this.updatePromise) {
this.updatePromise = new Promise((resolve) => {
setTimeout(() => {
- console.log("update viewport");
- resolve(this.frameNumber++);
+ if (this.lastUpdatedVieports) {
+ this.doUpdate(this.lastUpdatedVieports);
+ }
+
+ resolve(this.frameNumber);
this.updatePromise = null;
}, this.options.debounceTime);
});
}
return this.updatePromise;
}
+
+ doUpdate(viewports: Viewport[] | Viewport): void {
+ const preparedViewports = viewports instanceof Array ? viewports : [viewports];
+
+ this.frameNumber++;
+ this._selectedNodes = this.traverser.traverse(preparedViewports);
+ this.loadNodesContent(this.traverser.nodesToLoad);
+ }
+
+ loadNodesContent(nodes: POTreeNode[]) {
+ for (const node of nodes) {
+ this.loadNodeContent(node);
+ }
+ }
+
+ async loadNodeContent(node: POTreeNode) {
+ node.isContentLoading = true;
+ node.content = await this.dataSource.loadNodeContent(node.name);
+ this.onTileLoaded(node);
+ node.isContentLoading = false;
+ }
+
+ onTileLoaded(node: POTreeNode) {
+ if (this.options.onTileLoaded) {
+ this.options.onTileLoaded(node);
+ }
+ }
}
diff --git a/examples-experimental/potree/src/potree-tile-3d-layer.ts b/examples-experimental/potree/src/potree-tile-3d-layer.ts
index 846439984c..d43562c2de 100644
--- a/examples-experimental/potree/src/potree-tile-3d-layer.ts
+++ b/examples-experimental/potree/src/potree-tile-3d-layer.ts
@@ -1,8 +1,8 @@
import {Tile3DLayer, Tile3DLayerProps} from '@deck.gl/geo-layers';
-import {UpdateParameters} from '@deck.gl/core/typed';
-import {Viewport} from '@deck.gl/core';
+import {Viewport, UpdateParameters, COORDINATE_SYSTEM} from '@deck.gl/core';
import {Source} from '@loaders.gl/loader-utils';
import {PointcloudTileset} from './pointcloud-tileset';
+import { PointCloudLayer } from '@deck.gl/layers';
export type PotreeTile3DLayerProps = {
source: Source;
@@ -76,4 +76,54 @@ export class PotreeTile3DLayer<
}
});
}
+
+ private _makePointCloudLayer(
+ tileHeader: Tile3D,
+ oldLayer?: PointCloudLayer
+ ): PointCloudLayer | null {
+ const {
+ attributes,
+ pointCount,
+ constantRGBA,
+ cartographicOrigin,
+ modelMatrix,
+ coordinateSystem = COORDINATE_SYSTEM.METER_OFFSETS
+ } = tileHeader.content;
+ const {positions, normals, colors} = attributes;
+
+ if (!positions) {
+ return null;
+ }
+ const data = (oldLayer && oldLayer.props.data) || {
+ header: {
+ vertexCount: pointCount
+ },
+ attributes: {
+ POSITION: positions,
+ NORMAL: normals,
+ COLOR_0: colors
+ }
+ };
+
+ const {pointSize, getPointColor} = this.props;
+ const SubLayerClass = this.getSubLayerClass('pointcloud', PointCloudLayer);
+ return new SubLayerClass(
+ {
+ pointSize
+ },
+ this.getSubLayerProps({
+ id: 'pointcloud'
+ }),
+ {
+ id: `${this.id}-pointcloud-${tileHeader.id}`,
+ tile: tileHeader,
+ data,
+ coordinateSystem,
+ coordinateOrigin: cartographicOrigin,
+ modelMatrix,
+ getColor: constantRGBA || getPointColor,
+ _offset: 0
+ }
+ );
+ }
}
diff --git a/examples-experimental/potree/src/potree-traverser.ts b/examples-experimental/potree/src/potree-traverser.ts
new file mode 100644
index 0000000000..62dd96a854
--- /dev/null
+++ b/examples-experimental/potree/src/potree-traverser.ts
@@ -0,0 +1,37 @@
+import {POTreeNode} from "@loaders.gl/potree";
+
+export class PotreeTraverser {
+ root?: POTreeNode;
+ nodesToLoad: POTreeNode[] = [];
+
+ traverse(viewports: {id: string}[]) {
+ if (!this.root) {
+ return [];
+ }
+ const viewportIds = viewports.map(item => item.id);
+
+ this.nodesToLoad = [];
+ const result: POTreeNode[] = [];
+ const stack: POTreeNode[] = [this.root];
+
+ while (stack.length > 0) {
+ const node = stack.pop();
+
+ if (!node) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ for (const child of node?.children ?? []) {
+ stack.push(child);
+ }
+ node.selected = true;
+ node.viewportIds = viewportIds;
+ result.push(node);
+ if (!node.content && !node.isContentLoading) {
+ this.nodesToLoad.push(node);
+ }
+ }
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/examples-experimental/potree/tsconfig.json b/examples-experimental/potree/tsconfig.json
index e4a0bcf5e2..8cd945d3c7 100644
--- a/examples-experimental/potree/tsconfig.json
+++ b/examples-experimental/potree/tsconfig.json
@@ -2,5 +2,9 @@
"compilerOptions": {
"jsx": "react-jsx"
},
- "include": ["./app.js"]
+ "include": ["./app.js"],
+ "baseUrl": "../..",
+ "paths": {
+ "@loaders.gl/potree": ["modules/potree/src"]
+ }
}
diff --git a/examples-experimental/potree/vite.config.ts b/examples-experimental/potree/vite.config.ts
index 0dfc6a2510..58f7bc79a6 100644
--- a/examples-experimental/potree/vite.config.ts
+++ b/examples-experimental/potree/vite.config.ts
@@ -17,6 +17,6 @@ const getAliases = async (frameworkName, frameworkRootDir) => {
// https://vitejs.dev/config/
export default defineConfig(async () => ({
- resolve: {alias: await getAliases('@loaders.gl', `${__dirname}/../../..`)},
+ resolve: {alias: await getAliases('@loaders.gl', `${__dirname}/../..`)},
server: {open: true}
}));
diff --git a/modules/potree/package.json b/modules/potree/package.json
index 72323c6bc7..59945ebfbe 100644
--- a/modules/potree/package.json
+++ b/modules/potree/package.json
@@ -47,7 +47,8 @@
"@loaders.gl/las": "4.4.0-alpha.1",
"@loaders.gl/math": "4.4.0-alpha.1",
"@loaders.gl/schema": "4.4.0-alpha.1",
- "@math.gl/core": "^4.1.0"
+ "@math.gl/core": "^4.1.0",
+ "@math.gl/proj4": "^4.1.0"
},
"peerDependencies": {
"@loaders.gl/core": "4.4.0-alpha.0"
diff --git a/modules/potree/src/index.ts b/modules/potree/src/index.ts
index 15cb6b7dbe..1e8bf05466 100644
--- a/modules/potree/src/index.ts
+++ b/modules/potree/src/index.ts
@@ -2,3 +2,5 @@ export {PotreeLoader} from './potree-loader';
export {PotreeHierarchyChunkLoader} from './potree-hierarchy-chunk-loader';
export {PotreeBinLoader} from './potree-bin-loader';
export {PotreeSource} from './potree-source';
+
+export {type POTreeNode} from './parsers/parse-potree-hierarchy-chunk';
diff --git a/modules/potree/src/lib/potree-node-source.ts b/modules/potree/src/lib/potree-node-source.ts
index e4ec79762b..6eb49aab3c 100644
--- a/modules/potree/src/lib/potree-node-source.ts
+++ b/modules/potree/src/lib/potree-node-source.ts
@@ -2,16 +2,45 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
+import type {PotreeSourceOptions} from '../potree-source';
import {load} from '@loaders.gl/core';
import {Mesh} from '@loaders.gl/schema';
import {DataSource, resolvePath} from '@loaders.gl/loader-utils';
import {LASLoader} from '@loaders.gl/las';
-import {PotreeMetadata} from '../types/potree-metadata';
+import {PotreeBoundingBox, PotreeMetadata} from '../types/potree-metadata';
import {POTreeNode} from '../parsers/parse-potree-hierarchy-chunk';
import {PotreeHierarchyChunkLoader} from '../potree-hierarchy-chunk-loader';
import {PotreeLoader} from '../potree-loader';
import {parseVersion} from '../utils/parse-version';
-import type {PotreeSourceOptions} from '../potree-source';
+import {Proj4Projection} from '@math.gl/proj4';
+
+// https://github.com/visgl/deck.gl/blob/9548f43cba2234a1f4877b6b17f6c88eb35b2e08/modules/core/src/lib/constants.js#L27
+// Describes the format of positions
+export enum COORDINATE_SYSTEM {
+ /**
+ * `LNGLAT` if rendering into a geospatial viewport, `CARTESIAN` otherwise
+ */
+ DEFAULT = -1,
+ /**
+ * Positions are interpreted as [lng, lat, elevation]
+ * lng lat are degrees, elevation is meters. distances as meters.
+ */
+ LNGLAT = 1,
+ /**
+ * Positions are interpreted as meter offsets, distances as meters
+ */
+ METER_OFFSETS = 2,
+ /**
+ * Positions are interpreted as lng lat offsets: [deltaLng, deltaLat, elevation]
+ * deltaLng, deltaLat are delta degrees, elevation is meters.
+ * distances as meters.
+ */
+ LNGLAT_OFFSETS = 3,
+ /**
+ * Non-geospatial
+ */
+ CARTESIAN = 0
+}
/**
* A Potree data source
@@ -28,6 +57,8 @@ export class PotreeNodesSource extends DataSource {
root: POTreeNode | null = null;
/** Is data source ready to use after initial loading */
isReady = false;
+ /** The data set minimum bounding box */
+ boundingBox?: PotreeBoundingBox;
private initPromise: Promise | null = null;
@@ -51,6 +82,8 @@ export class PotreeNodesSource extends DataSource {
return;
}
this.metadata = await load(`${this.baseUrl}/cloud.js`, PotreeLoader);
+ this.parseBoundingVolume();
+
await this.loadHierarchy();
this.isReady = true;
}
@@ -84,36 +117,86 @@ export class PotreeNodesSource extends DataSource {
/**
* Load octree node content
- * @param path array of numbers between 0-7 specifying successive octree divisions.
+ * @param nodeName name of a node, string of numbers in range 0..7
* @return node content geometry or null if the node doesn't exist
*/
- async loadNodeContent(path: number[]): Promise {
+ // eslint-disable-next-line max-statements
+ async loadNodeContent(nodeName: string): Promise {
await this.initPromise;
if (!this.isSupported()) {
return null;
}
- const isAvailable = await this.isNodeAvailable(path);
+ const isAvailable = await this.isNodeAvailable(nodeName);
if (isAvailable) {
- return load(
- `${this.baseUrl}/${this.metadata?.octreeDir}/r/r${path.join(
- ''
- )}.${this.getContentExtension()}`,
+ const result = await load(
+ `${this.baseUrl}/${this.metadata?.octreeDir}/r/r${nodeName}.${this.getContentExtension()}`,
LASLoader
);
+
+ if (result) {
+ let projection;
+ if (this.metadata?.projection) {
+ projection = new Proj4Projection({
+ from: this.metadata.projection,
+ to: 'WGS84'
+ });
+ }
+
+ result.cartographicOrigin = [0, 0, 0];
+ const tileBoundingBox = result.header?.boundingBox;
+ if (tileBoundingBox) {
+ const [minXOriginal, minYOriginal, minZ] = tileBoundingBox[0];
+ const [maxXOriginal, maxYOriginal, maxZ] = tileBoundingBox[1];
+ let minX = minXOriginal;
+ let minY = minYOriginal;
+ let maxX = maxXOriginal;
+ let maxY = maxYOriginal;
+ if (projection) {
+ [minX, minY] = projection.project([minX, minY]);
+ [maxX, maxY] = projection.project([maxX, maxY]);
+ }
+ result.cartographicOrigin = [
+ minX + (maxX - minX) / 2,
+ minY + (maxY - minY) / 2,
+ minZ + (maxZ - minZ) / 2
+ ];
+ }
+
+ const position = result.attributes.POSITION.value as Float32Array;
+ for (let i = 0; i < (result.header?.vertexCount ?? 0); i++) {
+ let vertex = position.slice(i * 3, i * 3 + 2);
+ if (projection) {
+ vertex = projection.project(Array.from(vertex));
+ }
+
+ const offsets = [
+ vertex[0] - result.cartographicOrigin[0],
+ vertex[1] - result.cartographicOrigin[1],
+ position[i * 3 + 2] - result.cartographicOrigin[2]
+ ];
+ position.set(offsets, i * 3);
+ }
+ result.attributes.positions = result.attributes.POSITION;
+ result.attributes.colors = result.attributes.COLOR_0;
+ result.attributes.normals = result.attributes.NORMAL;
+
+ result.coordinateSystem = COORDINATE_SYSTEM.LNGLAT_OFFSETS;
+ return result;
+ }
}
return null;
}
/**
* Check if a node exists in the octree
- * @param path array of numbers between 0-7 specifying successive octree divisions
+ * @param nodeName name of a node, string of numbers in range 0..7
* @returns true - the node does exist, false - the nodes doesn't exist
*/
- async isNodeAvailable(path: number[]): Promise {
+ async isNodeAvailable(nodeName: string): Promise {
if (this.metadata?.hierarchy) {
- return this.metadata.hierarchy.findIndex((item) => item[0] === `r${path.join()}`) !== -1;
+ return this.metadata.hierarchy.findIndex((item) => item[0] === `r${nodeName}`) !== -1;
}
if (!this.root) {
@@ -122,8 +205,8 @@ export class PotreeNodesSource extends DataSource {
let currentParent = this.root;
let name = '';
let result = true;
- for (const nodeLevel of path) {
- const newName = `${name}${nodeLevel}`;
+ for (const char of nodeName) {
+ const newName = `${name}${char}`;
const node = currentParent.children.find((child) => child.name === newName);
if (node) {
currentParent = node;
@@ -159,4 +242,30 @@ export class PotreeNodesSource extends DataSource {
this.baseUrl = this.baseUrl.substring(0, -1);
}
}
+
+ private parseBoundingVolume(): void {
+ if (this.metadata?.projection && this.metadata.tightBoundingBox) {
+ const projection = new Proj4Projection({
+ from: this.metadata.projection,
+ to: 'WGS84'
+ });
+
+ const {lx, ly, ux, uy} = this.metadata.tightBoundingBox;
+ const lCoord = [lx, ly];
+ const wgs84LCood = projection.project(lCoord);
+
+ const uCoord = [ux, uy];
+ const wgs84UCood = projection.project(uCoord);
+
+ this.boundingBox = {
+ ...this.metadata.tightBoundingBox,
+ lx: wgs84LCood[0],
+ ly: wgs84LCood[1],
+ ux: wgs84UCood[0],
+ uy: wgs84UCood[1]
+ };
+ } else {
+ this.boundingBox = this.metadata?.tightBoundingBox;
+ }
+ }
}
diff --git a/modules/potree/src/parsers/parse-potree-hierarchy-chunk.ts b/modules/potree/src/parsers/parse-potree-hierarchy-chunk.ts
index 6d685038fc..3cb4f34069 100644
--- a/modules/potree/src/parsers/parse-potree-hierarchy-chunk.ts
+++ b/modules/potree/src/parsers/parse-potree-hierarchy-chunk.ts
@@ -66,6 +66,8 @@ export type POTreeTileHeader = {
/** Hierarchical potree node structure */
export type POTreeNode = {
+ id: string;
+ type: 'pointcloud';
/** Index data */
header: POTreeTileHeader;
/** Human readable name */
@@ -82,6 +84,14 @@ export type POTreeNode = {
children: POTreeNode[];
/** All children including unavailable */
childrenByIndex: POTreeNode[];
+ /** Is tile selected for rendering */
+ selected: boolean;
+ /** Points content data */
+ content?: unknown;
+ /** Is content loading */
+ isContentLoading?: boolean;
+ /** Viewport Ids */
+ viewportIds: unknown[];
};
/**
@@ -178,6 +188,8 @@ function buildHierarchy(flatNodes: POTreeNode[], options: {spacing?: number} = {
node.children = [];
node.childrenByIndex = new Array(8).fill(null);
node.spacing = (options?.spacing || 0) / Math.pow(2, level);
+ node.type = 'pointcloud';
+ node.id = node.name;
// tileHeader.boundingVolume = Utils.createChildAABB(parentNode.boundingBox, index);
if (parentNode) {
diff --git a/modules/potree/test/potree-source.spec.ts b/modules/potree/test/potree-source.spec.ts
index f2ec681f65..68da219d8b 100644
--- a/modules/potree/test/potree-source.spec.ts
+++ b/modules/potree/test/potree-source.spec.ts
@@ -23,7 +23,7 @@ test('PotreeSource#loadNodeContent - should return null for unsupported source',
const DS = PotreeSource;
const source = DS.createDataSource(POTREE_BIN_URL, {});
- const existingNodeContent = await source.loadNodeContent([3, 6, 0]);
+ const existingNodeContent = await source.loadNodeContent('360');
t.equals(existingNodeContent, null);
t.end();
@@ -37,7 +37,7 @@ test('PotreeSource#loadNodeContent', async (t) => {
t.ok(source.isSupported());
- const existingNodeContent = await source.loadNodeContent([2, 4, 6]);
+ const existingNodeContent = await source.loadNodeContent('246');
t.equals(existingNodeContent?.header?.vertexCount, 9933);
t.end();
diff --git a/yarn.lock b/yarn.lock
index 29df162903..c76bf64d7e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5265,6 +5265,7 @@ __metadata:
"@loaders.gl/math": "npm:4.4.0-alpha.1"
"@loaders.gl/schema": "npm:4.4.0-alpha.1"
"@math.gl/core": "npm:^4.1.0"
+ "@math.gl/proj4": "npm:^4.1.0"
peerDependencies:
"@loaders.gl/core": 4.4.0-alpha.0
languageName: unknown