diff --git a/.eslintignore b/.eslintignore
index 6f9eb2bf8a..0da3e0f1cd 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -11,4 +11,3 @@ docs/tmpl/
test/data/
-
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index beeb6668ca..7cb4ac55cf 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -18,7 +18,7 @@ module.exports = {
},
env: {
browser: true,
- es6: true,
+ es2020: true,
amd: true,
commonjs: true,
},
diff --git a/docs/config.json b/docs/config.json
index 01d7369b9c..9ac156c06b 100644
--- a/docs/config.json
+++ b/docs/config.json
@@ -49,6 +49,7 @@
"PointCloudLayer",
"PotreeLayer",
"CopcLayer",
+ "Potree2Layer",
"C3DTilesLayer",
"LabelLayer",
"GlobeLayer",
@@ -72,6 +73,7 @@
"FileSource",
"OrientedImageSource",
"PotreeSource",
+ "Potree2Source",
"VectorTilesSource",
"EntwinePointTileSource",
"CopcSource"
diff --git a/examples/config.json b/examples/config.json
index 8dabb2c9a1..98e388a1da 100644
--- a/examples/config.json
+++ b/examples/config.json
@@ -24,6 +24,7 @@
"Pointcloud": {
"potree_25d_map": "Potree 2.5D map",
+ "potree2_25d_map": "Potree 2.5D map 2.0 format",
"potree_3d_map": "Potree 3D map",
"laz_dragndrop": "LAS/LAZ viewer",
"entwine_simple_loader": "Entwine loader",
diff --git a/examples/potree2_25d_map.html b/examples/potree2_25d_map.html
new file mode 100644
index 0000000000..db76546e03
--- /dev/null
+++ b/examples/potree2_25d_map.html
@@ -0,0 +1,127 @@
+
+
+
+ Point Cloud Viewer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 64b1b2099d..689ab2b161 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@mapbox/vector-tile": "^1.3.1",
"@tmcw/togeojson": "^5.8.1",
"@tweenjs/tween.js": "^18.6.4",
+ "brotli-compress": "^1.3.3",
"copc": "^0.0.6",
"earcut": "^2.2.4",
"js-priority-queue": "^0.1.5",
@@ -2630,9 +2631,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "14.18.48",
- "dev": true,
- "license": "MIT"
+ "version": "17.0.45",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
+ "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.4",
@@ -3823,6 +3824,20 @@
"node": ">=8"
}
},
+ "node_modules/brotli-compress": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/brotli-compress/-/brotli-compress-1.3.3.tgz",
+ "integrity": "sha512-cwKOmzEuKqUmRxXDdZimiNoXRRr7AQKMSubJSbYA9FXk+LTPT3fBGpHU8VZRZZctAJ5OCeXGK9PzPpZ1vD0pDA==",
+ "dependencies": {
+ "@types/node": "^17.0.40",
+ "brotli-wasm": "1.2.0"
+ }
+ },
+ "node_modules/brotli-wasm": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/brotli-wasm/-/brotli-wasm-1.2.0.tgz",
+ "integrity": "sha512-PdDi7awF36zFujZyFJb9UNrP1l+If7iCgXhLKE1SpwqFQSK2yc7w2dysOmME7p325yQaZNvae7ruzypB3YhFxA=="
+ },
"node_modules/browser-stdout": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
diff --git a/package.json b/package.json
index 1b4c1d2235..236833eb6d 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"@mapbox/vector-tile": "^1.3.1",
"@tmcw/togeojson": "^5.8.1",
"@tweenjs/tween.js": "^18.6.4",
+ "brotli-compress": "^1.3.3",
"copc": "^0.0.6",
"earcut": "^2.2.4",
"js-priority-queue": "^0.1.5",
diff --git a/src/Core/Potree2Node.js b/src/Core/Potree2Node.js
new file mode 100644
index 0000000000..77e62a7cec
--- /dev/null
+++ b/src/Core/Potree2Node.js
@@ -0,0 +1,229 @@
+/*
+============
+== POTREE ==
+============
+
+http://potree.org
+
+Copyright (c) 2011-2020, Markus Schütz
+All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+ either expressed or implied, of the FreeBSD Project.
+ */
+
+import * as THREE from 'three';
+import PointCloudNode from 'Core/PointCloudNode';
+
+// Create an A(xis)A(ligned)B(ounding)B(ox) for the child `childIndex` of one aabb.
+// (PotreeConverter protocol builds implicit octree hierarchy by applying the same
+// subdivision algo recursively)
+const dHalfLength = new THREE.Vector3();
+
+const NODE_TYPE = {
+ NORMAL: 0,
+ LEAF: 1,
+ PROXY: 2,
+};
+
+class Potree2Node extends PointCloudNode {
+ constructor(numPoints = 0, childrenBitField = 0, layer) {
+ super(numPoints, layer);
+ this.childrenBitField = childrenBitField;
+ this.id = '';
+ this.depth = 0;
+ this.baseurl = layer.source.baseurl;
+ }
+
+ add(node, indexChild) {
+ super.add(node, indexChild);
+ node.id = this.id + indexChild;
+ node.depth = this.depth + 1;
+ }
+
+ createChildAABB(node, childIndex) {
+ // Code inspired from potree
+ node.bbox.copy(this.bbox);
+ this.bbox.getCenter(node.bbox.max);
+ dHalfLength.copy(node.bbox.max).sub(this.bbox.min);
+
+ if (childIndex === 1) {
+ node.bbox.min.z += dHalfLength.z;
+ node.bbox.max.z += dHalfLength.z;
+ } else if (childIndex === 3) {
+ node.bbox.min.z += dHalfLength.z;
+ node.bbox.max.z += dHalfLength.z;
+ node.bbox.min.y += dHalfLength.y;
+ node.bbox.max.y += dHalfLength.y;
+ } else if (childIndex === 0) {
+ //
+ } else if (childIndex === 2) {
+ node.bbox.min.y += dHalfLength.y;
+ node.bbox.max.y += dHalfLength.y;
+ } else if (childIndex === 5) {
+ node.bbox.min.z += dHalfLength.z;
+ node.bbox.max.z += dHalfLength.z;
+ node.bbox.min.x += dHalfLength.x;
+ node.bbox.max.x += dHalfLength.x;
+ } else if (childIndex === 7) {
+ node.bbox.min.add(dHalfLength);
+ node.bbox.max.add(dHalfLength);
+ } else if (childIndex === 4) {
+ node.bbox.min.x += dHalfLength.x;
+ node.bbox.max.x += dHalfLength.x;
+ } else if (childIndex === 6) {
+ node.bbox.min.y += dHalfLength.y;
+ node.bbox.max.y += dHalfLength.y;
+ node.bbox.min.x += dHalfLength.x;
+ node.bbox.max.x += dHalfLength.x;
+ }
+ }
+
+ get octreeIsLoaded() {
+ return !(this.childrenBitField && this.children.length === 0);
+ }
+
+ get url() {
+ return `${this.baseurl}/octree.bin`;
+ }
+
+ networkOptions(byteOffset, byteSize) {
+ const first = byteOffset;
+ const last = first + byteSize - 1n;
+
+ const networkOptions = {
+ ...this.layer.source.networkOptions,
+ headers: {
+ ...this.layer.source.networkOptions.headers,
+ 'content-type': 'multipart/byteranges',
+ Range: `bytes=${first}-${last}`,
+ },
+ };
+
+ return networkOptions;
+ }
+
+ async load() {
+ // Query octree if we don't have children potreeNode yet.
+ if (!this.octreeIsLoaded) {
+ await this.loadOctree();
+ }
+
+ return this.layer.source.fetcher(this.url, this.networkOptions(this.byteOffset, this.byteSize))
+ .then(file => this.layer.source.parser(file, {
+ in: {
+ source: this.layer.source,
+ bbox: this.bbox,
+ numPoints: this.numPoints,
+ },
+ out: this.layer,
+ }))
+ .then((data) => {
+ this.loaded = true;
+ this.loading = false;
+ return data.geometry;
+ });
+ }
+
+ async loadOctree() {
+ if (this.loaded || this.loading) {
+ return;
+ }
+ this.loading = true;
+ return (this.nodeType === NODE_TYPE.PROXY) ? this.loadHierarchy() : Promise.resolve();
+ }
+
+ async loadHierarchy() {
+ const hierarchyPath = `${this.baseurl}/hierarchy.bin`;
+ const buffer = await this.layer.source.fetcher(hierarchyPath, this.networkOptions(this.hierarchyByteOffset, this.hierarchyByteSize));
+ this.parseHierarchy(buffer);
+ }
+
+ parseHierarchy(buffer) {
+ const view = new DataView(buffer);
+
+ const bytesPerNode = 22;
+ const numNodes = buffer.byteLength / bytesPerNode;
+
+ const stack = [];
+ stack.push(this);
+
+ for (let indexNode = 0; indexNode < numNodes; indexNode++) {
+ const current = stack.shift();
+ const offset = indexNode * bytesPerNode;
+
+ const type = view.getUint8(offset + 0);
+ const childMask = view.getUint8(offset + 1);
+ const numPoints = view.getUint32(offset + 2, true);
+ const byteOffset = view.getBigInt64(offset + 6, true);
+ const byteSize = view.getBigInt64(offset + 14, true);
+
+ if (current.nodeType === NODE_TYPE.PROXY) {
+ // replace proxy with real node
+ current.byteOffset = byteOffset;
+ current.byteSize = byteSize;
+ current.numPoints = numPoints;
+ } else if (type === NODE_TYPE.PROXY) {
+ // load proxy
+ current.hierarchyByteOffset = byteOffset;
+ current.hierarchyByteSize = byteSize;
+ current.numPoints = numPoints;
+ } else {
+ // load real node
+ current.byteOffset = byteOffset;
+ current.byteSize = byteSize;
+ current.numPoints = numPoints;
+ }
+
+ if (current.byteSize === 0n) {
+ // workaround for issue potree/potree#1125
+ // some inner nodes erroneously report >0 points even though have 0 points
+ // however, they still report a byteSize of 0, so based on that we now set node.numPoints to 0
+ current.numPoints = 0;
+ }
+
+ current.nodeType = type;
+
+ if (current.nodeType === NODE_TYPE.PROXY) {
+ continue;
+ }
+
+ for (let childIndex = 0; childIndex < 8; childIndex++) {
+ const childExists = ((1 << childIndex) & childMask) !== 0;
+
+ if (!childExists) {
+ continue;
+ }
+
+ const child = new Potree2Node(numPoints, childMask, this.layer);
+ child.spacing = current.spacing / 2;
+
+ current.add(child, childIndex);
+ stack.push(child);
+ }
+ }
+ }
+}
+
+export default Potree2Node;
diff --git a/src/Core/Potree2PointAttributes.js b/src/Core/Potree2PointAttributes.js
new file mode 100644
index 0000000000..82ae497998
--- /dev/null
+++ b/src/Core/Potree2PointAttributes.js
@@ -0,0 +1,152 @@
+/*
+============
+== POTREE ==
+============
+
+http://potree.org
+
+Copyright (c) 2011-2020, Markus Schütz
+All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+ either expressed or implied, of the FreeBSD Project.
+ */
+
+/**
+ * Some types of possible point attribute data formats
+ *
+ * @class
+ */
+const PointAttributeTypes = {
+ DATA_TYPE_DOUBLE: { name: 'double', size: 8 },
+ DATA_TYPE_FLOAT: { name: 'float', size: 4 },
+ DATA_TYPE_INT8: { name: 'int8', size: 1 },
+ DATA_TYPE_UINT8: { name: 'uint8', size: 1 },
+ DATA_TYPE_INT16: { name: 'int16', size: 2 },
+ DATA_TYPE_UINT16: { name: 'uint16', size: 2 },
+ DATA_TYPE_INT32: { name: 'int32', size: 4 },
+ DATA_TYPE_UINT32: { name: 'uint32', size: 4 },
+ DATA_TYPE_INT64: { name: 'int64', size: 8 },
+ DATA_TYPE_UINT64: { name: 'uint64', size: 8 },
+};
+
+Object.keys(PointAttributeTypes).forEach((type, index) => {
+ PointAttributeTypes[index] = PointAttributeTypes[type];
+});
+
+export { PointAttributeTypes };
+
+class PointAttribute {
+ constructor(name, type, numElements) {
+ this.name = name;
+ this.type = type;
+ this.numElements = numElements;
+ this.byteSize = this.numElements * this.type.size;
+ this.description = '';
+ this.range = [Infinity, -Infinity];
+ }
+}
+
+PointAttribute.POSITION_CARTESIAN = new PointAttribute(
+ 'POSITION_CARTESIAN', PointAttributeTypes.DATA_TYPE_FLOAT, 3);
+
+PointAttribute.RGBA_PACKED = new PointAttribute(
+ 'COLOR_PACKED', PointAttributeTypes.DATA_TYPE_INT8, 4);
+
+PointAttribute.COLOR_PACKED = PointAttribute.RGBA_PACKED;
+
+PointAttribute.RGB_PACKED = new PointAttribute(
+ 'COLOR_PACKED', PointAttributeTypes.DATA_TYPE_INT8, 3);
+
+PointAttribute.NORMAL_FLOATS = new PointAttribute(
+ 'NORMAL_FLOATS', PointAttributeTypes.DATA_TYPE_FLOAT, 3);
+
+PointAttribute.INTENSITY = new PointAttribute(
+ 'INTENSITY', PointAttributeTypes.DATA_TYPE_UINT16, 1);
+
+PointAttribute.CLASSIFICATION = new PointAttribute(
+ 'CLASSIFICATION', PointAttributeTypes.DATA_TYPE_UINT8, 1);
+
+PointAttribute.NORMAL_SPHEREMAPPED = new PointAttribute(
+ 'NORMAL_SPHEREMAPPED', PointAttributeTypes.DATA_TYPE_UINT8, 2);
+
+PointAttribute.NORMAL_OCT16 = new PointAttribute(
+ 'NORMAL_OCT16', PointAttributeTypes.DATA_TYPE_UINT8, 2);
+
+PointAttribute.NORMAL = new PointAttribute(
+ 'NORMAL', PointAttributeTypes.DATA_TYPE_FLOAT, 3);
+
+PointAttribute.RETURN_NUMBER = new PointAttribute(
+ 'RETURN_NUMBER', PointAttributeTypes.DATA_TYPE_UINT8, 1);
+
+PointAttribute.NUMBER_OF_RETURNS = new PointAttribute(
+ 'NUMBER_OF_RETURNS', PointAttributeTypes.DATA_TYPE_UINT8, 1);
+
+PointAttribute.SOURCE_ID = new PointAttribute(
+ 'SOURCE_ID', PointAttributeTypes.DATA_TYPE_UINT16, 1);
+
+PointAttribute.INDICES = new PointAttribute(
+ 'INDICES', PointAttributeTypes.DATA_TYPE_UINT32, 1);
+
+PointAttribute.SPACING = new PointAttribute(
+ 'SPACING', PointAttributeTypes.DATA_TYPE_FLOAT, 1);
+
+PointAttribute.GPS_TIME = new PointAttribute(
+ 'GPS_TIME', PointAttributeTypes.DATA_TYPE_DOUBLE, 1);
+
+export { PointAttribute };
+
+export class Potree2PointAttributes {
+ constructor() {
+ this.attributes = [];
+ this.byteSize = 0;
+ this.size = 0;
+ this.vectors = [];
+ }
+
+ add(pointAttribute) {
+ this.attributes.push(pointAttribute);
+ this.byteSize += pointAttribute.byteSize;
+ this.size++;
+ }
+
+ addVector(vector) {
+ this.vectors.push(vector);
+ }
+
+ hasNormals() {
+ for (let index = 0; index < this.attributes.length; index++) {
+ const name = this.attributes[index];
+ const pointAttribute = this.attributes[name];
+ if (pointAttribute === PointAttribute.NORMAL_SPHEREMAPPED ||
+ pointAttribute === PointAttribute.NORMAL_FLOATS ||
+ pointAttribute === PointAttribute.NORMAL ||
+ pointAttribute === PointAttribute.NORMAL_OCT16) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/Layer/Potree2Layer.js b/src/Layer/Potree2Layer.js
new file mode 100644
index 0000000000..3006cb86c4
--- /dev/null
+++ b/src/Layer/Potree2Layer.js
@@ -0,0 +1,191 @@
+/*
+============
+== POTREE ==
+============
+
+http://potree.org
+
+Copyright (c) 2011-2020, Markus Schütz
+All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+ either expressed or implied, of the FreeBSD Project.
+ */
+
+import * as THREE from 'three';
+import PointCloudLayer from 'Layer/PointCloudLayer';
+import Potree2Node from 'Core/Potree2Node';
+import Extent from 'Core/Geographic/Extent';
+
+import { PointAttribute, Potree2PointAttributes, PointAttributeTypes } from 'Core/Potree2PointAttributes';
+
+const bboxMesh = new THREE.Mesh();
+const box3 = new THREE.Box3();
+bboxMesh.geometry.boundingBox = box3;
+
+const typeNameAttributeMap = {
+ double: PointAttributeTypes.DATA_TYPE_DOUBLE,
+ float: PointAttributeTypes.DATA_TYPE_FLOAT,
+ int8: PointAttributeTypes.DATA_TYPE_INT8,
+ uint8: PointAttributeTypes.DATA_TYPE_UINT8,
+ int16: PointAttributeTypes.DATA_TYPE_INT16,
+ uint16: PointAttributeTypes.DATA_TYPE_UINT16,
+ int32: PointAttributeTypes.DATA_TYPE_INT32,
+ uint32: PointAttributeTypes.DATA_TYPE_UINT32,
+ int64: PointAttributeTypes.DATA_TYPE_INT64,
+ uint64: PointAttributeTypes.DATA_TYPE_UINT64,
+};
+
+function parseAttributes(jsonAttributes) {
+ const attributes = new Potree2PointAttributes();
+
+ const replacements = {
+ rgb: 'rgba',
+ };
+
+ for (const jsonAttribute of jsonAttributes) {
+ const { name, numElements, min, max } = jsonAttribute;
+
+ const type = typeNameAttributeMap[jsonAttribute.type];
+
+ const potreeAttributeName = replacements[name] ? replacements[name] : name;
+
+ const attribute = new PointAttribute(potreeAttributeName, type, numElements);
+
+ if (numElements === 1) {
+ attribute.range = [min[0], max[0]];
+ } else {
+ attribute.range = [min, max];
+ }
+
+ if (name === 'gps-time') { // HACK: Guard against bad gpsTime range in metadata, see potree/potree#909
+ if (attribute.range[0] === attribute.range[1]) {
+ attribute.range[1] += 1;
+ }
+ }
+
+ attribute.initialRange = attribute.range;
+
+ attributes.add(attribute);
+ }
+
+ {
+ // check if it has normals
+ const hasNormals =
+ attributes.attributes.find(a => a.name === 'NormalX') !== undefined &&
+ attributes.attributes.find(a => a.name === 'NormalY') !== undefined &&
+ attributes.attributes.find(a => a.name === 'NormalZ') !== undefined;
+
+ if (hasNormals) {
+ const vector = {
+ name: 'NORMAL',
+ attributes: ['NormalX', 'NormalY', 'NormalZ'],
+ };
+ attributes.addVector(vector);
+ }
+ }
+
+ return attributes;
+}
+
+/**
+ * @property {boolean} isPotreeLayer - Used to checkout whether this layer
+ * is a Potree2Layer. Default is `true`. You should not change this, as it is
+ * used internally for optimisation.
+ */
+class Potree2Layer extends PointCloudLayer {
+ /**
+ * Constructs a new instance of Potree2 layer.
+ *
+ * @constructor
+ * @extends PointCloudLayer
+ *
+ * @example
+ * // Create a new point cloud layer
+ * const points = new Potree2Layer('points',
+ * {
+ * source: new Potree2Source({
+ * url: 'https://pointsClouds/',
+ * file: 'metadata.json',
+ * }
+ * });
+ *
+ * View.prototype.addLayer.call(view, points);
+ *
+ * @param {string} id - The id of the layer, that should be unique. It is
+ * not mandatory, but an error will be emitted if this layer is added a
+ * {@link View} that already has a layer going by that id.
+ * @param {Object} config - Configuration, all elements in it
+ * will be merged as is in the layer. For example, if the configuration
+ * contains three elements `name, protocol, extent`, these elements will be
+ * available using `layer.name` or something else depending on the property
+ * name. See the list of properties to know which one can be specified.
+ * @param {string} [config.crs=ESPG:4326] - The CRS of the {@link View} this
+ * layer will be attached to. This is used to determine the extent of this
+ * layer. Default to `EPSG:4326`.
+ */
+ constructor(id, config) {
+ super(id, config);
+ this.isPotreeLayer = true;
+
+ const resolve = this.addInitializationStep();
+
+ this.source.whenReady.then((metadata) => {
+ this.scale = new THREE.Vector3(1, 1, 1);
+ this.metadata = metadata;
+ this.pointAttributes = parseAttributes(metadata.attributes);
+ this.spacing = metadata.spacing;
+
+ const normal = Array.isArray(this.pointAttributes.attributes) &&
+ this.pointAttributes.attributes.find(elem => elem.name.startsWith('NORMAL'));
+ if (normal) {
+ this.material.defines[normal.name] = 1;
+ }
+
+ const min = new THREE.Vector3(...metadata.boundingBox.min);
+ const max = new THREE.Vector3(...metadata.boundingBox.max);
+ const boundingBox = new THREE.Box3(min, max);
+
+ const root = new Potree2Node(0, 0, this);
+
+ root.bbox = boundingBox;
+ root.boundingSphere = boundingBox.getBoundingSphere(new THREE.Sphere());
+
+ root.id = 'r';
+ root.depth = 0;
+ root.nodeType = 2;
+ root.hierarchyByteOffset = 0n;
+ root.hierarchyByteSize = BigInt(metadata.hierarchy.firstChunkSize);
+
+ root.byteOffset = 0;
+
+ this.root = root;
+
+ this.extent = Extent.fromBox3(this.source.crs || 'EPSG:4326', boundingBox);
+ return this.root.loadOctree().then(resolve);
+ });
+ }
+}
+
+export default Potree2Layer;
diff --git a/src/Loader/Potree2BrotliLoader.js b/src/Loader/Potree2BrotliLoader.js
new file mode 100644
index 0000000000..04e7b8d28d
--- /dev/null
+++ b/src/Loader/Potree2BrotliLoader.js
@@ -0,0 +1,310 @@
+/*
+============
+== POTREE ==
+============
+
+http://potree.org
+
+Copyright (c) 2011-2020, Markus Schütz
+All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+ either expressed or implied, of the FreeBSD Project.
+ */
+
+import { PointAttribute, PointAttributeTypes } from 'Core/Potree2PointAttributes';
+import { decompress } from 'brotli-compress/js.mjs';
+
+const typedArrayMapping = {
+ int8: Int8Array,
+ int16: Int16Array,
+ int32: Int32Array,
+ int64: Float64Array,
+ uint8: Uint8Array,
+ uint16: Uint16Array,
+ uint32: Uint32Array,
+ uint64: Float64Array,
+ float: Float32Array,
+ double: Float64Array,
+};
+
+function dealign24b(mortoncode) {
+ // see https://stackoverflow.com/questions/45694690/how-i-can-remove-all-odds-bits-in-c
+
+ // input alignment of desired bits
+ // ..a..b..c..d..e..f..g..h..i..j..k..l..m..n..o..p
+ let x = mortoncode;
+
+ // ..a..b..c..d..e..f..g..h..i..j..k..l..m..n..o..p ..a..b..c..d..e..f..g..h..i..j..k..l..m..n..o..p
+ // ..a.....c.....e.....g.....i.....k.....m.....o... .....b.....d.....f.....h.....j.....l.....n.....p
+ // ....a.....c.....e.....g.....i.....k.....m.....o. .....b.....d.....f.....h.....j.....l.....n.....p
+ x = ((x & 0b001000001000001000001000) >> 2) | ((x & 0b000001000001000001000001) >> 0);
+ // ....ab....cd....ef....gh....ij....kl....mn....op ....ab....cd....ef....gh....ij....kl....mn....op
+ // ....ab..........ef..........ij..........mn...... ..........cd..........gh..........kl..........op
+ // ........ab..........ef..........ij..........mn.. ..........cd..........gh..........kl..........op
+ x = ((x & 0b000011000000000011000000) >> 4) | ((x & 0b000000000011000000000011) >> 0);
+ // ........abcd........efgh........ijkl........mnop ........abcd........efgh........ijkl........mnop
+ // ........abcd....................ijkl............ ....................efgh....................mnop
+ // ................abcd....................ijkl.... ....................efgh....................mnop
+ x = ((x & 0b000000001111000000000000) >> 8) | ((x & 0b000000000000000000001111) >> 0);
+ // ................abcdefgh................ijklmnop ................abcdefgh................ijklmnop
+ // ................abcdefgh........................ ........................................ijklmnop
+ // ................................abcdefgh........ ........................................ijklmnop
+ x = ((x & 0b000000000000000000000000) >> 16) | ((x & 0b000000000000000011111111) >> 0);
+
+ // sucessfully realigned!
+ // ................................abcdefghijklmnop
+
+ return x;
+}
+
+export default async function load(buffer, options) {
+ const { pointAttributes, scale, min, size, offset, numPoints } = options;
+
+ let bytes;
+ if (numPoints === 0) {
+ bytes = { buffer: new ArrayBuffer(0) };
+ } else {
+ try {
+ bytes = await decompress(new Int8Array(buffer));
+ } catch (e) {
+ bytes = { buffer: new ArrayBuffer(numPoints * (pointAttributes.byteSize + 12)) };
+ console.error(`problem with node ${name}: `, e);
+ }
+ }
+
+ const view = new DataView(bytes.buffer);
+
+ const attributeBuffers = {};
+
+ const gridSize = 32;
+ const grid = new Uint32Array(gridSize ** 3);
+ const toIndex = (x, y, z) => {
+ // min is already subtracted
+ const dx = gridSize * x / size.x;
+ const dy = gridSize * y / size.y;
+ const dz = gridSize * z / size.z;
+
+ const ix = Math.min(parseInt(dx, 10), gridSize - 1);
+ const iy = Math.min(parseInt(dy, 10), gridSize - 1);
+ const iz = Math.min(parseInt(dz, 10), gridSize - 1);
+
+ const index = ix + iy * gridSize + iz * gridSize * gridSize;
+
+ return index;
+ };
+
+ let numOccupiedCells = 0;
+ let byteOffset = 0;
+ for (const pointAttribute of pointAttributes.attributes) {
+ if (['POSITION_CARTESIAN', 'position'].includes(pointAttribute.name)) {
+ const buff = new ArrayBuffer(numPoints * 4 * 3);
+ const positions = new Float32Array(buff);
+
+ for (let j = 0; j < numPoints; j++) {
+ const mc_0 = view.getUint32(byteOffset + 4, true);
+ const mc_1 = view.getUint32(byteOffset + 0, true);
+ const mc_2 = view.getUint32(byteOffset + 12, true);
+ const mc_3 = view.getUint32(byteOffset + 8, true);
+
+ byteOffset += 16;
+
+ let X = dealign24b((mc_3 & 0x00FFFFFF) >>> 0)
+ | (dealign24b(((mc_3 >>> 24) | (mc_2 << 8)) >>> 0) << 8);
+
+ let Y = dealign24b((mc_3 & 0x00FFFFFF) >>> 1)
+ | (dealign24b(((mc_3 >>> 24) | (mc_2 << 8)) >>> 1) << 8);
+
+
+ let Z = dealign24b((mc_3 & 0x00FFFFFF) >>> 2)
+ | (dealign24b(((mc_3 >>> 24) | (mc_2 << 8)) >>> 2) << 8);
+
+
+ if (mc_1 != 0 || mc_2 != 0) {
+ X = X | (dealign24b((mc_1 & 0x00FFFFFF) >>> 0) << 16)
+ | (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 0) << 24);
+
+ Y = Y | (dealign24b((mc_1 & 0x00FFFFFF) >>> 1) << 16)
+ | (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 1) << 24);
+
+ Z = Z | (dealign24b((mc_1 & 0x00FFFFFF) >>> 2) << 16)
+ | (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 2) << 24);
+ }
+
+ const x = parseInt(X, 10) * scale[0] + offset[0] - min.x;
+ const y = parseInt(Y, 10) * scale[1] + offset[1] - min.y;
+ const z = parseInt(Z, 10) * scale[2] + offset[2] - min.z;
+
+ const index = toIndex(x, y, z);
+ const count = grid[index]++;
+ if (count === 0) {
+ numOccupiedCells++;
+ }
+
+ positions[3 * j + 0] = x;
+ positions[3 * j + 1] = y;
+ positions[3 * j + 2] = z;
+ }
+
+ attributeBuffers[pointAttribute.name] = {
+ buffer: buff,
+ attribute: pointAttribute,
+ };
+ } else if (['RGBA', 'rgba'].includes(pointAttribute.name)) {
+ const buff = new ArrayBuffer(numPoints * 4);
+ const colors = new Uint8Array(buff);
+
+ for (let j = 0; j < numPoints; j++) {
+ const mc_0 = view.getUint32(byteOffset + 4, true);
+ const mc_1 = view.getUint32(byteOffset + 0, true);
+ byteOffset += 8;
+
+ const r = dealign24b((mc_1 & 0x00FFFFFF) >>> 0)
+ | (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 0) << 8);
+
+ const g = dealign24b((mc_1 & 0x00FFFFFF) >>> 1)
+ | (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 1) << 8);
+
+ const b = dealign24b((mc_1 & 0x00FFFFFF) >>> 2)
+ | (dealign24b(((mc_1 >>> 24) | (mc_0 << 8)) >>> 2) << 8);
+
+ colors[4 * j + 0] = r > 255 ? r / 256 : r;
+ colors[4 * j + 1] = g > 255 ? g / 256 : g;
+ colors[4 * j + 2] = b > 255 ? b / 256 : b;
+ }
+
+ attributeBuffers[pointAttribute.name] = {
+ buffer: buff,
+ attribute: pointAttribute,
+ };
+ } else {
+ const buff = new ArrayBuffer(numPoints * 4);
+ const f32 = new Float32Array(buff);
+
+ const TypedArray = typedArrayMapping[pointAttribute.type.name];
+ const preciseBuffer = new TypedArray(numPoints);
+
+ let [offset, scale] = [0, 1];
+
+ const getterMap = {
+ int8: view.getInt8,
+ int16: view.getInt16,
+ int32: view.getInt32,
+ uint8: view.getUint8,
+ uint16: view.getUint16,
+ uint32: view.getUint32,
+ float: view.getFloat32,
+ double: view.getFloat64,
+ };
+ const getter = getterMap[pointAttribute.type.name].bind(view);
+
+ // compute offset and scale to pack larger types into 32 bit floats
+ if (pointAttribute.type.size > 4) {
+ const [amin, amax] = pointAttribute.range;
+ offset = amin;
+ scale = 1 / (amax - amin);
+ }
+
+ for (let j = 0; j < numPoints; j++) {
+ const value = getter(byteOffset, true);
+ byteOffset += pointAttribute.byteSize;
+
+ f32[j] = (value - offset) * scale;
+ preciseBuffer[j] = value;
+ }
+
+ attributeBuffers[pointAttribute.name] = {
+ buffer: buff,
+ preciseBuffer,
+ attribute: pointAttribute,
+ offset,
+ scale,
+ };
+ }
+ }
+
+ const occupancy = parseInt(numPoints / numOccupiedCells, 10);
+
+ { // add indices
+ const buff = new ArrayBuffer(numPoints * 4);
+ const indices = new Uint32Array(buff);
+
+ for (let i = 0; i < numPoints; i++) {
+ indices[i] = i;
+ }
+
+ attributeBuffers.INDICES = {
+ buffer: buff,
+ attribute: PointAttribute.INDICES,
+ };
+ }
+
+
+ { // handle attribute vectors
+ const vectors = pointAttributes.vectors;
+
+ for (const vector of vectors) {
+ const {
+ name,
+ attributes,
+ } = vector;
+ const numVectorElements = attributes.length;
+ const buffer = new ArrayBuffer(numVectorElements * numPoints * 4);
+ const f32 = new Float32Array(buffer);
+
+ let iElement = 0;
+ for (const sourceName of attributes) {
+ const sourceBuffer = attributeBuffers[sourceName];
+ const {
+ offset,
+ scale,
+ } = sourceBuffer;
+ const view = new DataView(sourceBuffer.buffer);
+
+ const getter = view.getFloat32.bind(view);
+
+ for (let j = 0; j < numPoints; j++) {
+ const value = getter(j * 4, true);
+
+ f32[j * numVectorElements + iElement] = (value / scale) + offset;
+ }
+
+ iElement++;
+ }
+
+ const vecAttribute = new PointAttribute(name, PointAttributeTypes.DATA_TYPE_FLOAT, 3);
+
+ attributeBuffers[name] = {
+ buffer,
+ attribute: vecAttribute,
+ };
+ }
+ }
+
+ return {
+ buffer,
+ attributeBuffers,
+ density: occupancy,
+ };
+}
diff --git a/src/Loader/Potree2Loader.js b/src/Loader/Potree2Loader.js
new file mode 100644
index 0000000000..b937c1c60e
--- /dev/null
+++ b/src/Loader/Potree2Loader.js
@@ -0,0 +1,225 @@
+/*
+============
+== POTREE ==
+============
+
+http://potree.org
+
+Copyright (c) 2011-2020, Markus Schütz
+All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ The views and conclusions contained in the software and documentation are those
+of the authors and should not be interpreted as representing official policies,
+ either expressed or implied, of the FreeBSD Project.
+ */
+
+import { PointAttribute, PointAttributeTypes } from 'Core/Potree2PointAttributes';
+
+const typedArrayMapping = {
+ int8: Int8Array,
+ int16: Int16Array,
+ int32: Int32Array,
+ int64: Float64Array,
+ uint8: Uint8Array,
+ uint16: Uint16Array,
+ uint32: Uint32Array,
+ uint64: Float64Array,
+ float: Float32Array,
+ double: Float64Array,
+};
+
+export default function load(buffer, options) {
+ const { pointAttributes, scale, min, size, offset, numPoints } = options;
+
+ const view = new DataView(buffer);
+
+ const attributeBuffers = {};
+ let attributeOffset = 0;
+
+ let bytesPerPoint = 0;
+ for (const pointAttribute of pointAttributes.attributes) {
+ bytesPerPoint += pointAttribute.byteSize;
+ }
+
+ const gridSize = 32;
+ const grid = new Uint32Array(gridSize ** 3);
+ const toIndex = (x, y, z) => {
+ // min is already subtracted
+ const dx = gridSize * x / size.x;
+ const dy = gridSize * y / size.y;
+ const dz = gridSize * z / size.z;
+
+ const ix = Math.min(parseInt(dx, 10), gridSize - 1);
+ const iy = Math.min(parseInt(dy, 10), gridSize - 1);
+ const iz = Math.min(parseInt(dz, 10), gridSize - 1);
+
+ const index = ix + iy * gridSize + iz * gridSize * gridSize;
+
+ return index;
+ };
+
+ let numOccupiedCells = 0;
+ for (const pointAttribute of pointAttributes.attributes) {
+ if (['POSITION_CARTESIAN', 'position'].includes(pointAttribute.name)) {
+ const buff = new ArrayBuffer(numPoints * 4 * 3);
+ const positions = new Float32Array(buff);
+
+ for (let j = 0; j < numPoints; j++) {
+ const pointOffset = j * bytesPerPoint;
+
+ const x = (view.getInt32(pointOffset + attributeOffset + 0, true) * scale[0]) + offset[0] - min.x;
+ const y = (view.getInt32(pointOffset + attributeOffset + 4, true) * scale[1]) + offset[1] - min.y;
+ const z = (view.getInt32(pointOffset + attributeOffset + 8, true) * scale[2]) + offset[2] - min.z;
+
+ const index = toIndex(x, y, z);
+ const count = grid[index]++;
+ if (count === 0) {
+ numOccupiedCells++;
+ }
+
+ positions[3 * j + 0] = x;
+ positions[3 * j + 1] = y;
+ positions[3 * j + 2] = z;
+ }
+
+ attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute };
+ } else if (['RGBA', 'rgba'].includes(pointAttribute.name)) {
+ const buff = new ArrayBuffer(numPoints * 4);
+ const colors = new Uint8Array(buff);
+
+ for (let j = 0; j < numPoints; j++) {
+ const pointOffset = j * bytesPerPoint;
+
+ const r = view.getUint16(pointOffset + attributeOffset + 0, true);
+ const g = view.getUint16(pointOffset + attributeOffset + 2, true);
+ const b = view.getUint16(pointOffset + attributeOffset + 4, true);
+
+ colors[4 * j + 0] = r > 255 ? r / 256 : r;
+ colors[4 * j + 1] = g > 255 ? g / 256 : g;
+ colors[4 * j + 2] = b > 255 ? b / 256 : b;
+ }
+
+ attributeBuffers[pointAttribute.name] = { buffer: buff, attribute: pointAttribute };
+ } else {
+ const buff = new ArrayBuffer(numPoints * 4);
+ const f32 = new Float32Array(buff);
+
+ const TypedArray = typedArrayMapping[pointAttribute.type.name];
+ const preciseBuffer = new TypedArray(numPoints);
+
+ let [offset, scale] = [0, 1];
+
+ const getterMap = {
+ int8: view.getInt8,
+ int16: view.getInt16,
+ int32: view.getInt32,
+ uint8: view.getUint8,
+ uint16: view.getUint16,
+ uint32: view.getUint32,
+ float: view.getFloat32,
+ double: view.getFloat64,
+ };
+ const getter = getterMap[pointAttribute.type.name].bind(view);
+
+ // compute offset and scale to pack larger types into 32 bit floats
+ if (pointAttribute.type.size > 4) {
+ const [amin, amax] = pointAttribute.range;
+ offset = amin;
+ scale = 1 / (amax - amin);
+ }
+
+ for (let j = 0; j < numPoints; j++) {
+ const pointOffset = j * bytesPerPoint;
+ const value = getter(pointOffset + attributeOffset, true);
+
+ f32[j] = (value - offset) * scale;
+ preciseBuffer[j] = value;
+ }
+
+ attributeBuffers[pointAttribute.name] = {
+ buffer: buff,
+ preciseBuffer,
+ attribute: pointAttribute,
+ offset,
+ scale,
+ };
+ }
+
+ attributeOffset += pointAttribute.byteSize;
+ }
+
+ const occupancy = parseInt(numPoints / numOccupiedCells, 10);
+
+ { // add indices
+ const buff = new ArrayBuffer(numPoints * 4);
+ const indices = new Uint32Array(buff);
+
+ for (let i = 0; i < numPoints; i++) {
+ indices[i] = i;
+ }
+
+ attributeBuffers.INDICES = { buffer: buff, attribute: PointAttribute.INDICES };
+ }
+
+
+ { // handle attribute vectors
+ const vectors = pointAttributes.vectors;
+
+ for (const vector of vectors) {
+ const { name, attributes } = vector;
+ const numVectorElements = attributes.length;
+ const buffer = new ArrayBuffer(numVectorElements * numPoints * 4);
+ const f32 = new Float32Array(buffer);
+
+ let iElement = 0;
+ for (const sourceName of attributes) {
+ const sourceBuffer = attributeBuffers[sourceName];
+ const { offset, scale } = sourceBuffer;
+ const view = new DataView(sourceBuffer.buffer);
+
+ const getter = view.getFloat32.bind(view);
+
+ for (let j = 0; j < numPoints; j++) {
+ const value = getter(j * 4, true);
+
+ f32[j * numVectorElements + iElement] = (value / scale) + offset;
+ }
+
+ iElement++;
+ }
+
+ const vecAttribute = new PointAttribute(name, PointAttributeTypes.DATA_TYPE_FLOAT, 3);
+
+ attributeBuffers[name] = {
+ buffer,
+ attribute: vecAttribute,
+ };
+ }
+ }
+
+ return {
+ buffer,
+ attributeBuffers,
+ density: occupancy,
+ };
+}
diff --git a/src/Main.js b/src/Main.js
index 4969bbff5c..5ed8f02272 100644
--- a/src/Main.js
+++ b/src/Main.js
@@ -52,6 +52,7 @@ export { default as GeometryLayer } from 'Layer/GeometryLayer';
export { default as FeatureGeometryLayer } from 'Layer/FeatureGeometryLayer';
export { default as PointCloudLayer } from 'Layer/PointCloudLayer';
export { default as PotreeLayer } from 'Layer/PotreeLayer';
+export { default as Potree2Layer } from 'Layer/Potree2Layer';
export { default as C3DTilesLayer, C3DTILES_LAYER_EVENTS } from 'Layer/C3DTilesLayer';
export { default as TiledGeometryLayer } from 'Layer/TiledGeometryLayer';
export { default as OrientedImageLayer } from 'Layer/OrientedImageLayer';
@@ -76,6 +77,7 @@ export { default as WMTSSource } from 'Source/WMTSSource';
export { default as VectorTilesSource } from 'Source/VectorTilesSource';
export { default as OrientedImageSource } from 'Source/OrientedImageSource';
export { default as PotreeSource } from 'Source/PotreeSource';
+export { default as Potree2Source } from 'Source/Potree2Source';
export { default as C3DTilesSource } from 'Source/C3DTilesSource';
export { default as C3DTilesIonSource } from 'Source/C3DTilesIonSource';
export { default as C3DTilesGoogleSource } from 'Source/C3DTilesGoogleSource';
diff --git a/src/Parser/Potree2BinParser.js b/src/Parser/Potree2BinParser.js
new file mode 100644
index 0000000000..b13341d310
--- /dev/null
+++ b/src/Parser/Potree2BinParser.js
@@ -0,0 +1,102 @@
+import * as THREE from 'three';
+import { spawn, Thread, Transfer } from 'threads';
+
+let _thread;
+
+function workerInstance() {
+ return new Worker(
+ /* webpackChunkName: "itowns_potree2worker" */
+ new URL('../Worker/Potree2Worker.js', import.meta.url),
+ { type: 'module' },
+ );
+}
+
+async function loader() {
+ if (_thread) { return _thread; }
+ _thread = await spawn(workerInstance());
+ return _thread;
+}
+
+function decoder(w, metadata) {
+ return metadata.encoding === 'BROTLI' ? w.parseBrotli : w.parse;
+}
+
+export default {
+ /**
+ * @return {Promise}
+ */
+ terminate() {
+ const currentThread = _thread;
+ _thread = undefined;
+ return Thread.terminate(currentThread);
+ },
+
+ /** @module Potree2BinParser */
+ /** Parse .bin PotreeConverter 2.0 format and convert to a THREE.BufferGeometry
+ * @function parse
+ * @param {ArrayBuffer} buffer - the bin buffer.
+ * @param {Object} options
+ * @param {string[]} options.in.pointAttributes - the point attributes information contained in metadata.js
+ * @return {Promise} - a promise that resolves with a THREE.BufferGeometry.
+ *
+ */
+ parse: async function parse(buffer, options) {
+ const metadata = options.in.source.metadata;
+ const layer = options.out;
+
+ const pointAttributes = layer.pointAttributes;
+ const scale = metadata.scale;
+ const box = options.in.bbox;
+ const min = box.min;
+ const size = box.max.clone().sub(box.min);
+ const max = box.max;
+ const offset = metadata.offset;
+ const numPoints = options.in.numPoints;
+
+ const potreeLoader = await loader();
+ const decode = decoder(potreeLoader, metadata);
+ const data = await decode(Transfer(buffer), {
+ pointAttributes,
+ scale,
+ min,
+ max,
+ size,
+ offset,
+ numPoints,
+ });
+
+ const buffers = data.attributeBuffers;
+ const geometry = new THREE.BufferGeometry();
+ Object.keys(buffers).forEach((property) => {
+ const buffer = buffers[property].buffer;
+
+ if (property === 'position') {
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(buffer), 3));
+ } else if (property === 'rgba') {
+ geometry.setAttribute('color', new THREE.BufferAttribute(new Uint8Array(buffer), 4, true));
+ } else if (property === 'NORMAL') {
+ geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(buffer), 3));
+ } else if (property === 'INDICES') {
+ const bufferAttribute = new THREE.BufferAttribute(new Uint8Array(buffer), 4);
+ bufferAttribute.normalized = true;
+ geometry.setAttribute('indices', bufferAttribute);
+ } else {
+ const bufferAttribute = new THREE.BufferAttribute(new Float32Array(buffer), 1);
+
+ const batchAttribute = buffers[property].attribute;
+ bufferAttribute.potree = {
+ offset: buffers[property].offset,
+ scale: buffers[property].scale,
+ preciseBuffer: buffers[property].preciseBuffer,
+ range: batchAttribute.range,
+ };
+
+ geometry.setAttribute(property, bufferAttribute);
+ }
+ });
+
+ geometry.computeBoundingBox();
+
+ return { geometry, density: data.density };
+ },
+};
diff --git a/src/Source/Potree2Source.js b/src/Source/Potree2Source.js
new file mode 100644
index 0000000000..3764642341
--- /dev/null
+++ b/src/Source/Potree2Source.js
@@ -0,0 +1,177 @@
+import Source from 'Source/Source';
+import Fetcher from 'Provider/Fetcher';
+import Potree2BinParser from 'Parser/Potree2BinParser';
+
+/**
+ * @classdesc
+ * Potree2Source are object containing informations on how to fetch potree 2.0 points cloud resources.
+ *
+ *
+ */
+
+class Potree2Source extends Source {
+ /**
+ * @param {Object} source - An object that can contain all properties of a
+ * Potree2Source
+ * @param {string} source.url - folder url.
+ * @param {string} source.file - metadata file name.
+ *
+ * This `metadata` file stores information about the potree cloud 2.0 in JSON format. the structure is :
+ *
+ * * __`version`__ - The metadata.json format may change over time. The version number is
+ * necessary so that parsers know how to interpret the data.
+ * * __`name`__ - Point cloud name.
+ * * __`description`__ - Point cloud description.
+ * * __`points`__ - Total number of points.
+ * * __`projection`__ - Point cloud geographic projection system.
+ * * __`hierarchy`__ - Information about point cloud hierarchy (first chunk size, step size, octree depth).
+ * * __`offset`__ - Position offset used to determine the global point position.
+ * * __`scale`__ - Point cloud scale.
+ * * __`spacing`__ - The minimum distance between points at root level.
+ * * __`boundingBox`__ - Contains the minimum and maximum of the axis aligned bounding box. This bounding box is cubic and aligned to fit to the octree root.
+ * * __`encoding`__ - Encoding type: BROTLI or DEFAULT (uncompressed).
+ * * __`attributes`__ - Array of attributes (position, intensity, return number, number of returns, classification, scan angle rank, user data, point source id, gps-time, rgb).
+ * ```
+ * {
+ * version: '2.0',
+ * name: "sample",
+ * description: "",
+ * points: 534909153,
+ * projection: "",
+ * hierarchy: {
+ * firstChunkSize: 1276,
+ * stepSize: 4,
+ * depth: 16
+ * },
+ * offset: [1339072.07, 7238866.339, 85.281],
+ * scale: [0.001, 0.001, 0.002],
+ * spacing: 24.476062500005355,
+ * boundingBox: {
+ * min: [1339072.07, 7238866.339, 85.281],
+ * max: [1342205.0060000008, 7241999.275, 3218.2170000006854]
+ * },
+ * encoding: "BROTLI",
+ * attributes: [
+ * {
+ * name: "position",
+ * description: "",
+ * size: 12,
+ * numElements: 3,
+ * elementSize: 4,
+ * type: "int32",
+ * min: [-0.74821299314498901, -2.7804059982299805, 2.5478212833404541],
+ * max: [2.4514148223438199, 1.4893437627414672, 7.1957106576508663]
+ * },
+ * {
+ * name: "intensity",
+ * description: "",
+ * size: 2,
+ * numElements: 1,
+ * elementSize: 2,
+ * type: "uint16",
+ * min: [0],
+ * max: [0]
+ * },{
+ * name: "return number",
+ * description: "",
+ * size: 1,
+ * numElements: 1,
+ * elementSize: 1,
+ * type: "uint8",
+ * min: [0],
+ * max: [0]
+ * },{
+ * name: "number of returns",
+ * description: "",
+ * size: 1,
+ * numElements: 1,
+ * elementSize: 1,
+ * type: "uint8",
+ * min: [0],
+ * max: [0]
+ * },{
+ * name: "classification",
+ * description: "",
+ * size: 1,
+ * numElements: 1,
+ * elementSize: 1,
+ * type: "uint8",
+ * min: [0],
+ * max: [0]
+ * },{
+ * name: "scan angle rank",
+ * description: "",
+ * size: 1,
+ * numElements: 1,
+ * elementSize: 1,
+ * type: "uint8",
+ * min: [0],
+ * max: [0]
+ * },{
+ * name: "user data",
+ * description: "",
+ * size: 1,
+ * numElements: 1,
+ * elementSize: 1,
+ * type: "uint8",
+ * min: [0],
+ * max: [0]
+ * },{
+ * name: "point source id",
+ * description: "",
+ * size: 2,
+ * numElements: 1,
+ * elementSize: 2,
+ * type: "uint16",
+ * min: [0],
+ * max: [0]
+ * },{
+ * name: "gps-time",
+ * description: "",
+ * size: 8,
+ * numElements: 1,
+ * elementSize: 8,
+ * type: "double",
+ * min: [0],
+ * max: [0]
+ * },{
+ * name: "rgb",
+ * description: "",
+ * size: 6,
+ * numElements: 3,
+ * elementSize: 2,
+ * type: "uint16",
+ * min: [5632, 5376, 4864],
+ * max: [65280, 65280, 65280]
+ * }
+ * ]
+ * }
+ * ```
+ *
+ * @extends Source
+ *
+ * @constructor
+ */
+ constructor(source) {
+ if (!source.file) {
+ throw new Error('New Potree2Source: file is required');
+ }
+
+ super(source);
+ this.file = source.file;
+ this.fetcher = Fetcher.arrayBuffer;
+
+ this.whenReady = (source.metadata ? Promise.resolve(source.metadata) : Fetcher.json(`${this.url}/${this.file}`, this.networkOptions))
+ .then((metadata) => {
+ this.metadata = metadata;
+ this.pointAttributes = metadata.attributes;
+ this.baseurl = `${this.url}`;
+ this.extension = 'bin';
+ this.parser = Potree2BinParser.parse;
+
+ return metadata;
+ });
+ }
+}
+
+export default Potree2Source;
diff --git a/src/Worker/Potree2Worker.js b/src/Worker/Potree2Worker.js
new file mode 100644
index 0000000000..376c51fd32
--- /dev/null
+++ b/src/Worker/Potree2Worker.js
@@ -0,0 +1,24 @@
+import load from 'Loader/Potree2Loader';
+import loadBrotli from 'Loader/Potree2BrotliLoader';
+import { expose, Transfer } from 'threads/worker';
+
+function transfer(buffer, data) {
+ const transferables = [];
+ Object.keys(data.attributeBuffers).forEach((property) => {
+ transferables.push(data.attributeBuffers[property].buffer);
+ });
+ transferables.push(buffer);
+ return transferables;
+}
+
+expose({
+ async parse(buffer, options) {
+ const data = await load(buffer, options);
+ return Transfer(data, transfer(buffer, data));
+ },
+
+ async parseBrotli(buffer, options) {
+ const data = await loadBrotli(buffer, options);
+ return Transfer(data, transfer(buffer, data));
+ },
+});
diff --git a/test/functional/potree2_25d_map.js b/test/functional/potree2_25d_map.js
new file mode 100644
index 0000000000..eb8c693047
--- /dev/null
+++ b/test/functional/potree2_25d_map.js
@@ -0,0 +1,12 @@
+import assert from 'assert';
+
+describe('potree2_25d_map', function _() {
+ let result;
+ before(async () => {
+ result = await loadExample('examples/potree2_25d_map.html', this.fullTitle());
+ });
+
+ it('should run', async () => {
+ assert.ok(result);
+ });
+});
diff --git a/test/unit/potree2.js b/test/unit/potree2.js
new file mode 100644
index 0000000000..4729ef9f64
--- /dev/null
+++ b/test/unit/potree2.js
@@ -0,0 +1,134 @@
+import assert from 'assert';
+import Potree2Layer from 'Layer/Potree2Layer';
+import Potree2Source from 'Source/Potree2Source';
+import Potree2BinParser from 'Parser/Potree2BinParser';
+import View from 'Core/View';
+import HttpsProxyAgent from 'https-proxy-agent';
+import Potree2Node from 'Core/Potree2Node';
+import PointsMaterial from 'Renderer/PointsMaterial';
+import OrientedImageMaterial from 'Renderer/OrientedImageMaterial';
+import { Vector3 } from 'three';
+import Renderer from './bootstrap';
+
+describe('Potree2', function () {
+ let renderer;
+ let viewer;
+ let potreeLayer;
+ let context;
+ let elt;
+
+ before(function () {
+ renderer = new Renderer();
+ viewer = new View('EPSG:3946', renderer.domElement, { renderer });
+ viewer.camera.camera3D.position.copy(new Vector3(0, 0, 10));
+
+ // Configure Point Cloud layer
+ potreeLayer = new Potree2Layer('lion', {
+ source: new Potree2Source({
+ file: 'metadata.json',
+ url: 'https://blocinbloc-public-test.s3.fr-par.scw.cloud/lion-potree2',
+ networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {},
+ }),
+ onPointsCreated: () => {},
+ crs: viewer.referenceCrs,
+ });
+
+ context = {
+ camera: viewer.camera,
+ engine: viewer.mainLoop.gfxEngine,
+ scheduler: viewer.mainLoop.scheduler,
+ geometryLayer: potreeLayer,
+ view: viewer,
+ };
+ });
+
+ it('Add point potree2 layer', function (done) {
+ View.prototype.addLayer.call(viewer, potreeLayer)
+ .then((layer) => {
+ context.camera.camera3D.updateMatrixWorld();
+ assert.equal(layer.root.children.length, 6);
+ layer.bboxes.visible = true;
+ done();
+ }).catch(done);
+ });
+
+ it('preupdate potree2 layer', function () {
+ elt = potreeLayer.preUpdate(context, new Set([potreeLayer]));
+ assert.equal(elt.length, 1);
+ });
+
+ it('update potree2 layer', function (done) {
+ assert.equal(potreeLayer.group.children.length, 0);
+ potreeLayer.update(context, potreeLayer, elt[0]);
+ elt[0].promise
+ .then(() => {
+ assert.equal(potreeLayer.group.children.length, 1);
+ done();
+ }).catch(done);
+ });
+
+ it('postUpdate potree2 layer', function () {
+ potreeLayer.postUpdate(context, potreeLayer);
+ });
+
+ describe('potree2 Node', function () {
+ const numPoints = 1000;
+ const childrenBitField = 5;
+
+ it('instance', function (done) {
+ const root = new Potree2Node(numPoints, childrenBitField, potreeLayer);
+ root.nodeType = 2;
+ assert.equal(root.numPoints, numPoints);
+ assert.equal(root.childrenBitField, childrenBitField);
+ done();
+ });
+
+ it('load octree', function (done) {
+ const root = new Potree2Node(numPoints, childrenBitField, potreeLayer);
+ root.nodeType = 2;
+ root.hierarchyByteOffset = 0n;
+ root.hierarchyByteSize = 12650n;
+ root.loadOctree()
+ .then(() => {
+ assert.equal(root.children.length, 6);
+ done();
+ }).catch(done);
+ });
+
+ it('load child node', function (done) {
+ const root = new Potree2Node(numPoints, childrenBitField, potreeLayer);
+ root.nodeType = 2;
+ root.hierarchyByteOffset = 0n;
+ root.hierarchyByteSize = 12650n;
+ root.loadOctree()
+ .then(() => root.children[0].load()
+ .then(() => {
+ assert.equal(root.children[0].children.length, 8);
+ done();
+ }),
+ ).catch(done);
+ });
+ });
+
+ describe('Point Material and oriented images', () => {
+ const orientedImageMaterial = new OrientedImageMaterial([]);
+ const pMaterial = new PointsMaterial({ orientedImageMaterial });
+ const pMaterial2 = new PointsMaterial();
+ it('instance', () => {
+ assert.ok(pMaterial);
+ });
+ it('copy', () => {
+ pMaterial2.copy(pMaterial);
+ assert.equal(pMaterial2.uniforms.projectiveTextureAlphaBorder.value, 20);
+ });
+ it('update', () => {
+ pMaterial.visible = false;
+ pMaterial2.update(pMaterial);
+ assert.equal(pMaterial2.visible, false);
+ });
+ });
+
+ after(async function () {
+ await Potree2BinParser.terminate();
+ });
+});
diff --git a/test/unit/potree2BinParser.js b/test/unit/potree2BinParser.js
new file mode 100644
index 0000000000..979aa802cc
--- /dev/null
+++ b/test/unit/potree2BinParser.js
@@ -0,0 +1,184 @@
+import assert from 'assert';
+import Potree2BinParser from 'Parser/Potree2BinParser';
+import * as THREE from 'three';
+
+describe('Potree2BinParser', function () {
+ it('should correctly parse position buffer', function (done) {
+ const nbPoints = 12;
+ const buffer = new ArrayBuffer(nbPoints * 4 * 3);
+ const dv = new DataView(buffer);
+ for (let i = 0; i < nbPoints * 3; i++) {
+ dv.setInt32(i * 4, i * 2, true);
+ }
+
+ const options = {
+ in: {
+ source: {
+ metadata: {
+ encoding: 'DEFAULT',
+ scale: [1, 1, 1],
+ offset: [0, 0, 0],
+ },
+ },
+ bbox: new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 1, 1)),
+ numPoints: nbPoints,
+ },
+ out: {
+ pointAttributes: {
+ attributes: [{
+ name: 'position',
+ type: {
+ name: 'int32',
+ size: 4,
+ },
+ numElements: 3,
+ byteSize: 12,
+ description: '',
+ range: [0, 0],
+ initialRange: [0, 0],
+ }],
+ vectors: [],
+ },
+ offset: new THREE.Vector3(),
+ },
+ node: {
+ bbox: new THREE.Box3(),
+ },
+ };
+
+ Potree2BinParser.parse(buffer, options)
+ .then((data) => {
+ const posAttr = data.geometry.getAttribute('position');
+ assert.equal(posAttr.itemSize, 3);
+ assert.ok(posAttr.array instanceof Float32Array);
+ assert.equal(posAttr.array.length, nbPoints * 3);
+ assert.equal(posAttr.array[0], 0);
+ assert.equal(posAttr.array[11], 22);
+ done();
+ })
+ .catch(done);
+ });
+
+ it('should correctly parse a complex buffer (positions, intensity, rgb and classification)', function (done) {
+ // generate 5 points: positions, intensity, rgba, classification
+ const numbyte = 3 * 4 + 2 + 3 * 2 + 1;
+ const numPoints = 5;
+ const buffer = new ArrayBuffer(numPoints * numbyte);
+ const dv = new DataView(buffer);
+ for (let i = 0; i < numPoints; i++) {
+ // position
+ dv.setInt32(i * numbyte + 0, 3 * i, true);
+ dv.setInt32(i * numbyte + 4, 3 * i + 1, true);
+ dv.setInt32(i * numbyte + 8, 3 * i + 2, true);
+ // intensity
+ dv.setInt16(i * numbyte + 12, 100 + i, true);
+ // color
+ dv.setUint8(i * numbyte + 14, 200 + 4 * i);
+ dv.setUint8(i * numbyte + 16, 201 + 4 * i);
+ dv.setUint8(i * numbyte + 18, 202 + 4 * i);
+ // classification
+ dv.setUint8(i * numbyte + 20, i * 3);
+ }
+
+ const options = {
+ in: {
+ source: {
+ metadata: {
+ encoding: 'DEFAULT',
+ scale: [1, 1, 1],
+ offset: [0, 0, 0],
+ },
+ },
+ bbox: new THREE.Box3(new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 1, 1)),
+ numPoints,
+ },
+ out: {
+ pointAttributes: {
+ attributes: [{
+ name: 'position',
+ type: {
+ name: 'int32',
+ size: 4,
+ },
+ numElements: 3,
+ byteSize: 12,
+ description: '',
+ range: [0, 0],
+ initialRange: [0, 0],
+ }, {
+ name: 'intensity',
+ type: {
+ name: 'uint16',
+ size: 2,
+ },
+ numElements: 1,
+ byteSize: 2,
+ description: '',
+ range: [0, 0],
+ initialRange: [0, 0],
+ }, {
+ name: 'rgba',
+ type: {
+ name: 'uint16',
+ size: 2,
+ },
+ numElements: 3,
+ byteSize: 6,
+ description: '',
+ range: [0, 0],
+ initialRange: [0, 0],
+ }, {
+ name: 'classification',
+ type: {
+ name: 'uint8',
+ size: 1,
+ },
+ numElements: 1,
+ byteSize: 1,
+ description: '',
+ range: [0, 0],
+ initialRange: [0, 0],
+ }],
+ vectors: [],
+ },
+ offset: new THREE.Vector3(),
+ },
+ node: {
+ bbox: new THREE.Box3(),
+ },
+ };
+
+ Potree2BinParser.parse(buffer, options)
+ .then(function (data) {
+ const geom = data.geometry;
+ const posAttr = geom.getAttribute('position');
+ const intensityAttr = geom.getAttribute('intensity');
+ const colorAttr = geom.getAttribute('color');
+ const classificationAttr = geom.getAttribute('classification');
+
+ // check position buffer
+ assert.equal(posAttr.itemSize, 3);
+ assert.deepStrictEqual(posAttr.array, Float32Array.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14));
+ // check intensity
+ assert.equal(intensityAttr.itemSize, 1);
+ assert.deepStrictEqual(intensityAttr.potree.preciseBuffer, Uint16Array.of(100, 101, 102, 103, 104));
+ // check colors
+ assert.equal(colorAttr.itemSize, 4);
+ assert.deepStrictEqual(colorAttr.array, Uint8Array.of(
+ 200, 201, 202, 0,
+ 204, 205, 206, 0,
+ 208, 209, 210, 0,
+ 212, 213, 214, 0,
+ 216, 217, 218, 0));
+ // check classif
+ assert.equal(classificationAttr.itemSize, 1);
+ assert.deepStrictEqual(classificationAttr.potree.preciseBuffer, Uint8Array.of(0, 3, 6, 9, 12));
+ done();
+ })
+ .catch(done);
+ });
+
+ after(async function () {
+ await Potree2BinParser.terminate();
+ });
+});
diff --git a/test/unit/potree2layerparsing.js b/test/unit/potree2layerparsing.js
new file mode 100644
index 0000000000..5777b1a735
--- /dev/null
+++ b/test/unit/potree2layerparsing.js
@@ -0,0 +1,292 @@
+import assert from 'assert';
+import Potree2Layer from 'Layer/Potree2Layer';
+import Potree2Source from 'Source/Potree2Source';
+import Coordinates from 'Core/Geographic/Coordinates';
+import GlobeView from 'Core/Prefab/GlobeView';
+import View from 'Core/View';
+import HttpsProxyAgent from 'https-proxy-agent';
+import Renderer from './bootstrap';
+
+describe('Potree2 Provider', function () {
+ const renderer = new Renderer();
+ const placement = { coord: new Coordinates('EPSG:4326', 1.5, 43), range: 300000 };
+ const view = new GlobeView(renderer.domElement, placement, { renderer });
+
+ it('should correctly parse normal information in metadata', function (done) {
+ // No normals
+ const metadata = {
+ version: '2.0',
+ name: 'lion',
+ points: 534909153,
+ hierarchy: {
+ firstChunkSize: 1276,
+ stepSize: 4,
+ depth: 16,
+ },
+ offset: [0, 0, 0],
+ scale: [1, 1, 1],
+ spacing: 24,
+ boundingBox: {
+ min: [0, 0, 0],
+ max: [1, 1, 1],
+ },
+ encoding: 'BROTLI',
+ attributes: [
+ {
+ name: 'position',
+ description: '',
+ size: 12,
+ numElements: 3,
+ elementSize: 4,
+ type: 'int32',
+ min: [0, 0, 0],
+ max: [0, 0, 0],
+ },
+ ],
+ };
+
+ const layers = [];
+ let source = new Potree2Source({
+ file: 'metadata.json',
+ url: 'https://blocinbloc-public-test.s3.fr-par.scw.cloud/lion-potree2',
+ networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {},
+ metadata,
+ });
+
+ const layer1 = new Potree2Layer('pointsCloud1', { source, crs: view.referenceCrs });
+ layers.push(layer1);
+ const p1 = layer1.whenReady.then((l) => {
+ const normalDefined = l.material.defines.NORMAL || l.material.defines.NORMAL_SPHEREMAPPED || l.material.defines.NORMAL_OCT16;
+ assert.ok(!normalDefined);
+ });
+
+ // // // normals as vector
+ source = new Potree2Source({
+ file: 'metadata.json',
+ url: 'https://blocinbloc-public-test.s3.fr-par.scw.cloud/lion-potree2',
+ networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {},
+ metadata: {
+ version: '2.0',
+ name: 'lion',
+ points: 534909153,
+ hierarchy: {
+ firstChunkSize: 1276,
+ stepSize: 4,
+ depth: 16,
+ },
+ offset: [0, 0, 0],
+ scale: [1, 1, 1],
+ spacing: 24,
+ boundingBox: {
+ min: [0, 0, 0],
+ max: [1, 1, 1],
+ },
+ encoding: 'BROTLI',
+ attributes: [
+ {
+ name: 'position',
+ description: '',
+ size: 12,
+ numElements: 3,
+ elementSize: 4,
+ type: 'int32',
+ min: [0, 0, 0],
+ max: [0, 0, 0],
+ },
+ {
+ name: 'classification',
+ description: '',
+ size: 1,
+ numElements: 1,
+ elementSize: 1,
+ type: 'uint8',
+ min: [1],
+ max: [2],
+ },
+ {
+ name: 'NORMAL',
+ description: '',
+ size: 3,
+ numElements: 3,
+ elementSize: 1,
+ type: 'int8',
+ min: [-127],
+ max: [127],
+ scale: [1],
+ offset: [0],
+ },
+ ],
+ },
+ });
+
+ const layer2 = new Potree2Layer('pointsCloud2', { source, crs: view.referenceCrs });
+ layers.push(layer2);
+ const p2 = layer2.whenReady.then((l) => {
+ assert.ok(l.material.defines.NORMAL);
+ assert.ok(!l.material.defines.NORMAL_SPHEREMAPPED);
+ assert.ok(!l.material.defines.NORMAL_OCT16);
+ });
+
+ // // spheremapped normals
+ source = new Potree2Source({
+ file: 'metadata.json',
+ url: 'https://blocinbloc-public-test.s3.fr-par.scw.cloud/lion-potree2',
+ networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {},
+ metadata: {
+ version: '2.0',
+ name: 'lion',
+ points: 534909153,
+ hierarchy: {
+ firstChunkSize: 1276,
+ stepSize: 4,
+ depth: 16,
+ },
+ offset: [0, 0, 0],
+ scale: [1, 1, 1],
+ spacing: 24,
+ boundingBox: {
+ min: [0, 0, 0],
+ max: [1, 1, 1],
+ },
+ encoding: 'BROTLI',
+ attributes: [
+ {
+ name: 'position',
+ description: '',
+ size: 12,
+ numElements: 3,
+ elementSize: 4,
+ type: 'int32',
+ min: [0, 0, 0],
+ max: [0, 0, 0],
+ },
+ {
+ name: 'classification',
+ description: '',
+ size: 1,
+ numElements: 1,
+ elementSize: 1,
+ type: 'uint8',
+ min: [1],
+ max: [2],
+ },
+ {
+ name: 'NORMAL_SPHEREMAPPED',
+ description: '',
+ size: 3,
+ numElements: 3,
+ elementSize: 1,
+ type: 'int8',
+ min: [-127],
+ max: [127],
+ scale: [1],
+ offset: [0],
+ },
+ ],
+ },
+ });
+ const layer3 = new Potree2Layer('pointsCloud3', { source, crs: view.referenceCrs });
+
+ layers.push(layer3);
+ const p3 = layer3.whenReady.then((l) => {
+ assert.ok(!l.material.defines.NORMAL);
+ assert.ok(l.material.defines.NORMAL_SPHEREMAPPED);
+ assert.ok(!l.material.defines.NORMAL_OCT16);
+ });
+
+ // // oct16 normals
+ source = new Potree2Source({
+ file: 'metadata.json',
+ url: 'https://blocinbloc-public-test.s3.fr-par.scw.cloud/lion-potree2',
+ networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {},
+ metadata: {
+ version: '2.0',
+ name: 'lion',
+ points: 534909153,
+ hierarchy: {
+ firstChunkSize: 1276,
+ stepSize: 4,
+ depth: 16,
+ },
+ offset: [0, 0, 0],
+ scale: [1, 1, 1],
+ spacing: 24,
+ boundingBox: {
+ min: [0, 0, 0],
+ max: [1, 1, 1],
+ },
+ encoding: 'BROTLI',
+ attributes: [
+ {
+ name: 'position',
+ description: '',
+ size: 12,
+ numElements: 3,
+ elementSize: 4,
+ type: 'int32',
+ min: [0, 0, 0],
+ max: [0, 0, 0],
+ },
+ {
+ name: 'classification',
+ description: '',
+ size: 1,
+ numElements: 1,
+ elementSize: 1,
+ type: 'uint8',
+ min: [1],
+ max: [2],
+ },
+ {
+ name: 'NORMAL_OCT16',
+ description: '',
+ size: 3,
+ numElements: 3,
+ elementSize: 1,
+ type: 'int8',
+ min: [-127],
+ max: [127],
+ scale: [1],
+ offset: [0],
+ },
+ ],
+ },
+ });
+ const layer4 = new Potree2Layer('pointsCloud4', { source, crs: view.referenceCrs });
+
+ layers.push(layer4);
+ const p4 = layer4.whenReady
+ .then((l) => {
+ assert.ok(!l.material.defines.NORMAL);
+ assert.ok(!l.material.defines.NORMAL_SPHEREMAPPED);
+ assert.ok(l.material.defines.NORMAL_OCT16);
+ });
+
+ layers.forEach(p => View.prototype.addLayer.call(view, p));
+
+ Promise.all([p1, p2, p3, p4])
+ .then(() => done())
+ .catch(done);
+ });
+});
+
+
+describe('getObjectToUpdateForAttachedLayers', function () {
+ it('should correctly no-parent for the root', function () {
+ const meta = {
+ obj: 'a',
+ };
+ assert.equal(Potree2Layer.prototype.getObjectToUpdateForAttachedLayers(meta).element, 'a');
+ });
+ it('should correctly return the element and its parent', function () {
+ const meta = {
+ obj: 'a',
+ parent: {
+ obj: 'b',
+ },
+ };
+ const result = Potree2Layer.prototype.getObjectToUpdateForAttachedLayers(meta);
+ assert.equal(result.element, 'a');
+ assert.equal(result.parent, 'b');
+ });
+});
diff --git a/test/unit/potree2layerprocessing.js b/test/unit/potree2layerprocessing.js
new file mode 100644
index 0000000000..13bc2dbfff
--- /dev/null
+++ b/test/unit/potree2layerprocessing.js
@@ -0,0 +1,84 @@
+import assert from 'assert';
+import Potree2Layer from 'Layer/Potree2Layer';
+import Potree2Node from 'Core/Potree2Node';
+
+describe('preUpdate Potree2Layer', function () {
+ const context = { camera: { height: 1, camera3D: { fov: 1 } } };
+ const layer = {
+ id: 'a',
+ source: { baseurl: 'server.geo' },
+ hierarchyStepSize: 1,
+ };
+ layer.root = new Potree2Node(4000, 0, layer);
+ layer.root.bbox.setFromArray([1000, 1000, 1000, 0, 0, 0]);
+
+ layer.root.add(new Potree2Node(3000, 0, layer), 1, layer.root);
+ layer.root.children[0].obj = { layer, isPoints: true };
+ layer.root.add(new Potree2Node(3000, 0, layer), 2, layer.root);
+ layer.root.children[1].obj = { layer, isPoints: true };
+ layer.root.add(new Potree2Node(3000, 0, layer), 3, layer.root);
+ layer.root.children[2].obj = { layer, isPoints: true };
+
+ layer.root.children[0].add(new Potree2Node(2000, 0, layer), 1, layer.root);
+ layer.root.children[0].children[0].obj = { layer, isPoints: true };
+ layer.root.children[0].add(new Potree2Node(2000, 0, layer), 2, layer.root);
+ layer.root.children[0].children[1].obj = { layer, isPoints: true };
+ layer.root.children[1].add(new Potree2Node(2000, 0, layer), 1, layer.root);
+ layer.root.children[1].children[0].obj = { layer, isPoints: true };
+ layer.root.children[2].add(new Potree2Node(2000, 0, layer), 2, layer.root);
+ layer.root.children[2].children[0].obj = { layer, isPoints: true };
+ layer.root.children[2].add(new Potree2Node(2000, 0, layer), 3, layer.root);
+ layer.root.children[2].children[1].obj = { layer, isPoints: true };
+
+ layer.root.children[0].children[0].add(new Potree2Node(1000, 0, layer), 1, layer.root);
+ layer.root.children[0].children[0].children[0].obj = { layer, isPoints: true };
+ layer.root.children[0].children[0].add(new Potree2Node(1000, 0, layer), 5, layer.root);
+ layer.root.children[0].children[0].children[1].obj = { layer, isPoints: true };
+ layer.root.children[0].children[1].add(new Potree2Node(1000, 0, layer), 4, layer.root);
+ layer.root.children[0].children[1].children[0].obj = { layer, isPoints: true };
+ layer.root.children[2].children[1].add(new Potree2Node(1000, 0, layer), 1, layer.root);
+ layer.root.children[2].children[1].children[0].obj = { layer, isPoints: true };
+ layer.root.children[2].children[1].add(new Potree2Node(1000, 0, layer), 2, layer.root);
+ layer.root.children[2].children[1].children[1].obj = { layer, isPoints: true };
+ layer.root.children[2].children[1].add(new Potree2Node(1000, 0, layer), 3, layer.root);
+ layer.root.children[2].children[1].children[2].obj = { layer, isPoints: true };
+ layer.root.children[2].children[1].add(new Potree2Node(1000, 0, layer), 4, layer.root);
+ layer.root.children[2].children[1].children[3].obj = { layer, isPoints: true };
+
+ it('should return root if no change source', () => {
+ const sources = new Set();
+ assert.deepStrictEqual(
+ layer.root,
+ Potree2Layer.prototype.preUpdate.call(layer, context, sources)[0]);
+ });
+
+ it('should return root if no common ancestors', () => {
+ const sources = new Set();
+ sources.add(layer.root.children[0].children[0]);
+ sources.add(layer.root.children[2].children[1]);
+ assert.deepStrictEqual(
+ layer.root,
+ Potree2Layer.prototype.preUpdate.call(layer, context, sources)[0]);
+ });
+
+ it('should return common ancestor', () => {
+ const sources = new Set();
+ sources.add(layer.root.children[2].children[0]);
+ sources.add(layer.root.children[2].children[1]);
+ sources.add(layer.root.children[2].children[1].children[2]);
+ sources.add(layer.root.children[2].children[1].children[3]);
+ assert.deepStrictEqual(
+ layer.root.children[2],
+ Potree2Layer.prototype.preUpdate.call(layer, context, sources)[0]);
+ });
+
+ it('should not search ancestors if layer are different root if no common ancestors', () => {
+ const sources = new Set();
+ sources.add(layer.root.children[2].children[0]);
+ sources.add(layer.root.children[2].children[1].children[3]);
+ layer.root.children[2].children[1].children[3].obj = { layer: {}, isPoints: true };
+ assert.deepStrictEqual(
+ layer.root.children[2].children[0],
+ Potree2Layer.prototype.preUpdate.call(layer, context, sources)[0]);
+ });
+});
diff --git a/webpack.config.cjs b/webpack.config.cjs
index ebfa8f8b71..9f06fac8ab 100644
--- a/webpack.config.cjs
+++ b/webpack.config.cjs
@@ -58,6 +58,9 @@ module.exports = () => {
import: './src/Utils/gui/Main.js',
dependOn: 'itowns',
},
+ itowns_potree2worker: {
+ import: './src/Worker/Potree2Worker.js',
+ },
},
devtool: 'source-map',
output: {