From ee56ec70626213390fa200573552ddb5a54a3b94 Mon Sep 17 00:00:00 2001 From: Kevin ETOURNEAU Date: Wed, 5 Jul 2023 16:51:24 +0200 Subject: [PATCH] feat(potree2): Add potree 2.0 loader --- .eslintignore | 1 - .eslintrc.cjs | 2 +- docs/config.json | 2 + examples/config.json | 1 + examples/potree2_25d_map.html | 127 ++++++++++++ package-lock.json | 21 +- package.json | 1 + src/Core/Potree2Node.js | 229 ++++++++++++++++++++ src/Core/Potree2PointAttributes.js | 152 ++++++++++++++ src/Layer/Potree2Layer.js | 191 +++++++++++++++++ src/Loader/Potree2BrotliLoader.js | 310 ++++++++++++++++++++++++++++ src/Loader/Potree2Loader.js | 225 ++++++++++++++++++++ src/Main.js | 2 + src/Parser/Potree2BinParser.js | 102 +++++++++ src/Source/Potree2Source.js | 177 ++++++++++++++++ src/Worker/Potree2Worker.js | 24 +++ test/functional/potree2_25d_map.js | 12 ++ test/unit/potree2.js | 134 ++++++++++++ test/unit/potree2BinParser.js | 184 +++++++++++++++++ test/unit/potree2layerparsing.js | 292 ++++++++++++++++++++++++++ test/unit/potree2layerprocessing.js | 84 ++++++++ webpack.config.cjs | 3 + 22 files changed, 2271 insertions(+), 5 deletions(-) create mode 100644 examples/potree2_25d_map.html create mode 100644 src/Core/Potree2Node.js create mode 100644 src/Core/Potree2PointAttributes.js create mode 100644 src/Layer/Potree2Layer.js create mode 100644 src/Loader/Potree2BrotliLoader.js create mode 100644 src/Loader/Potree2Loader.js create mode 100644 src/Parser/Potree2BinParser.js create mode 100644 src/Source/Potree2Source.js create mode 100644 src/Worker/Potree2Worker.js create mode 100644 test/functional/potree2_25d_map.js create mode 100644 test/unit/potree2.js create mode 100644 test/unit/potree2BinParser.js create mode 100644 test/unit/potree2layerparsing.js create mode 100644 test/unit/potree2layerprocessing.js 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: {