From 6f978a11b01059e6c62b814488e6035698e3e076 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:32:01 +0200 Subject: [PATCH 01/11] refactor(PointCloudLayer): move spacing from Source to Layer for homogeneity --- src/Layer/CopcLayer.js | 10 ++++------ src/Layer/EntwinePointTileLayer.js | 12 ++++++++---- src/Source/EntwinePointTileSource.js | 6 ------ 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/Layer/CopcLayer.js b/src/Layer/CopcLayer.js index 4395a55808..3fb25cb3c2 100644 --- a/src/Layer/CopcLayer.js +++ b/src/Layer/CopcLayer.js @@ -39,8 +39,10 @@ class CopcLayer extends PointCloudLayer { const resolve = () => this; this.whenReady = this.source.whenReady.then((/** @type {CopcSource} */ source) => { - const { cube, rootHierarchyPage } = source.info; - const { pageOffset, pageLength } = rootHierarchyPage; + const { cube } = source.info; + const { pageOffset, pageLength } = source.info.rootHierarchyPage; + + this.spacing = source.info.spacing; this.root = new CopcNode(0, 0, 0, 0, pageOffset, pageLength, this, -1); this.root.bbox.min.fromArray(cube, 0); @@ -55,10 +57,6 @@ class CopcLayer extends PointCloudLayer { return this.root.loadOctree().then(resolve); }); } - - get spacing() { - return this.source.info.spacing; - } } export default CopcLayer; diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index a12c96524b..8edf3714c8 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -57,20 +57,24 @@ class EntwinePointTileLayer extends PointCloudLayer { const resolve = this.addInitializationStep(); this.whenReady = this.source.whenReady.then(() => { + // NOTE: this spacing is kinda arbitrary here, we take the width and + // length (height can be ignored), and we divide by the specified + // span in ept.json. This needs improvements. + this.spacing = (Math.abs(this.source.bounds[3] - this.source.bounds[0]) + + Math.abs(this.source.bounds[4] - this.source.bounds[1])) / (2 * this.source.span); + this.root = new EntwinePointTileNode(0, 0, 0, 0, this, -1); + this.root.bbox.min.fromArray(this.source.boundsConforming, 0); this.root.bbox.max.fromArray(this.source.boundsConforming, 3); this.minElevationRange = this.minElevationRange ?? this.source.boundsConforming[2]; this.maxElevationRange = this.maxElevationRange ?? this.source.boundsConforming[5]; this.extent = Extent.fromBox3(config.crs || 'EPSG:4326', this.root.bbox); + return this.root.loadOctree().then(resolve); }); } - - get spacing() { - return this.source.spacing; - } } export default EntwinePointTileLayer; diff --git a/src/Source/EntwinePointTileSource.js b/src/Source/EntwinePointTileSource.js index ed113bb3ac..643a9600ca 100644 --- a/src/Source/EntwinePointTileSource.js +++ b/src/Source/EntwinePointTileSource.js @@ -59,12 +59,6 @@ class EntwinePointTileSource extends Source { } } - // NOTE: this spacing is kinda arbitrary here, we take the width and - // length (height can be ignored), and we divide by the specified - // span in ept.json. This needs improvements. - this.spacing = (Math.abs(metadata.boundsConforming[3] - metadata.boundsConforming[0]) - + Math.abs(metadata.boundsConforming[4] - metadata.boundsConforming[1])) / (2 * metadata.span); - this.boundsConforming = metadata.boundsConforming; this.bounds = metadata.bounds; this.span = metadata.span; From c32b7de6f62e8357c803edf7b13a3b8329fff37d Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 12 Jul 2024 14:54:11 +0200 Subject: [PATCH 02/11] refactor(LASLoader): reproj data during parsing --- src/Loader/LASLoader.js | 25 ++++++++------- src/Parser/LASParser.js | 59 ++++++++++++++++++++++++++++++----- src/Utils/OrientationUtils.js | 8 +++-- test/unit/lasparser.js | 26 +++++++++++++-- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/src/Loader/LASLoader.js b/src/Loader/LASLoader.js index ce0ca491df..dc25001080 100644 --- a/src/Loader/LASLoader.js +++ b/src/Loader/LASLoader.js @@ -1,5 +1,6 @@ import { LazPerf } from 'laz-perf'; import { Las } from 'copc'; +import proj4 from 'proj4'; /** * @typedef {Object} Header - Partial LAS header. @@ -47,7 +48,11 @@ class LASLoader { } _parseView(view, options) { - const colorDepth = options.colorDepth ?? 16; + const colorDepth = options.colorDepth ?? defaultColorEncoding(options.header); + + const forward = (options.in.crs !== options.out.crs) ? + proj4(options.in.projDefs, options.out.projDefs).forward : + (x => x); const getPosition = ['X', 'Y', 'Z'].map(view.getter); const getIntensity = view.getter('Intensity'); @@ -60,6 +65,7 @@ class LASLoader { const getScanAngle = view.getter('ScanAngle'); const positions = new Float32Array(view.pointCount * 3); + const intensities = new Uint16Array(view.pointCount); const returnNumbers = new Uint8Array(view.pointCount); const numberOfReturns = new Uint8Array(view.pointCount); @@ -75,13 +81,13 @@ class LASLoader { */ const scanAngles = new Float32Array(view.pointCount); - // For precision we take the first point that will be use as origin for a local referentiel. - const origin = getPosition.map(f => f(0)).map(val => Math.floor(val)); - + const origin = options.out.origin; for (let i = 0; i < view.pointCount; i++) { // `getPosition` apply scale and offset transform to the X, Y, Z // values. See https://github.com/connormanning/copc.js/blob/master/src/las/extractor.ts. - const [x, y, z] = getPosition.map(f => f(i)); + // we thus apply the projection to get values in the Crs of the view. + const point = getPosition.map(f => f(i)); + const [x, y, z] = forward(point); positions[i * 3] = x - origin[0]; positions[i * 3 + 1] = y - origin[1]; positions[i * 3 + 2] = z - origin[2]; @@ -122,7 +128,6 @@ class LASLoader { pointSourceID: pointSourceIDs, color: colors, scanAngle: scanAngles, - origin, }; } @@ -153,8 +158,6 @@ class LASLoader { const { header, eb, pointCount } = options; const { pointDataRecordFormat, pointDataRecordLength } = header; - const colorDepth = options.colorDepth ?? defaultColorEncoding(header); - const bytes = new Uint8Array(data); const pointData = await Las.PointData.decompressChunk(bytes, { pointCount, @@ -163,7 +166,7 @@ class LASLoader { }, this._initDecoder()); const view = Las.View.create(pointData, header, eb); - const attributes = this._parseView(view, { colorDepth }); + const attributes = this._parseView(view, options); return { attributes }; } @@ -182,7 +185,7 @@ class LASLoader { const pointData = await Las.PointData.decompressFile(bytes, this._initDecoder()); const header = Las.Header.parse(bytes); - const colorDepth = options.colorDepth ?? defaultColorEncoding(header); + options.header = header; const getter = async (begin, end) => bytes.slice(begin, end); const vlrs = await Las.Vlr.walk(getter, header); @@ -190,7 +193,7 @@ class LASLoader { const eb = ebVlr && Las.ExtraBytes.parse(await Las.Vlr.fetch(getter, ebVlr)); const view = Las.View.create(pointData, header, eb); - const attributes = this._parseView(view, { colorDepth }); + const attributes = this._parseView(view, options); return { header, attributes, diff --git a/src/Parser/LASParser.js b/src/Parser/LASParser.js index 0ec1b17767..dfa5512dbf 100644 --- a/src/Parser/LASParser.js +++ b/src/Parser/LASParser.js @@ -1,5 +1,7 @@ import * as THREE from 'three'; import { spawn, Thread, Transfer } from 'threads'; +import proj4 from 'proj4'; +import OrientationUtils from 'Utils/OrientationUtils'; let _lazPerf; let _thread; @@ -19,6 +21,21 @@ async function loader() { return _thread; } +function getOrigin(options) { + const center = options.out.center; + const centerCrsIn = proj4(options.out.crs, options.in.crs).forward(center); + return proj4(options.in.crs, options.out.crs).forward([centerCrsIn.x, centerCrsIn.y, 0]); +} + +function getLocalRotation(options, origin) { + const isGeocentric = proj4.defs(options.out.crs).projName === 'geocent'; + let rotation = new THREE.Quaternion(); + if (isGeocentric) { + rotation = OrientationUtils.quaternionFromCRSToCRS(options.out.crs, 'EPSG:4326')(origin); + } + return rotation; +} + function buildBufferGeometry(attributes) { const geometry = new THREE.BufferGeometry(); @@ -47,8 +64,6 @@ function buildBufferGeometry(attributes) { const scanAngle = new THREE.BufferAttribute(attributes.scanAngle, 1); geometry.setAttribute('scanAngle', scanAngle); - geometry.userData.origin = new THREE.Vector3().fromArray(attributes.origin); - return geometry; } @@ -106,15 +121,29 @@ export default { */ async parseChunk(data, options = {}) { const lasLoader = await loader(); + const origin = getOrigin(options); const parsedData = await lasLoader.parseChunk(Transfer(data), { pointCount: options.in.pointCount, header: options.in.header, - eb: options.eb, + eb: options.in.eb, colorDepth: options.in.colorDepth, + in: { + crs: options.in.crs, + projDefs: proj4.defs(options.in.crs), + }, + out: { + crs: options.out.crs, + projDefs: proj4.defs(options.out.crs), + origin, + }, }); + const rotation = getLocalRotation(options, origin); const geometry = buildBufferGeometry(parsedData.attributes); + geometry.applyQuaternion(rotation); geometry.computeBoundingBox(); + geometry.userData.origin = new THREE.Vector3().fromArray(origin); + geometry.userData.rotation = rotation; return geometry; }, @@ -125,9 +154,12 @@ export default { * @param {ArrayBuffer} data - The file content to parse. * @param {Object} [options] * @param {Object} [options.in] - Options to give to the parser. + * @param {String} options.in.crs - Crs of the source. + * @param {String} options.out.crs - Crs of the view. * @param { 8 | 16 } [options.in.colorDepth] - Color depth (in bits). * Defaults to 8 bits for LAS 1.2 and 16 bits for later versions * (as mandatory by the specification) + * * @return {Promise} A promise resolving with a `THREE.BufferGeometry`. The * header of the file is contained in `userData`. @@ -137,16 +169,29 @@ export default { console.warn("Warning: options 'skip' not supported anymore"); } - const input = options.in; - const lasLoader = await loader(); + const origin = getOrigin(options); const parsedData = await lasLoader.parseFile(Transfer(data), { - colorDepth: input?.colorDepth, + colorDepth: options.in.colorDepth, + in: { + crs: options.in.crs, + projDefs: proj4.defs(options.in.crs), + }, + out: { + crs: options.out.crs, + projDefs: proj4.defs(options.out.crs), + origin, + }, }); + const rotation = getLocalRotation(options, origin); const geometry = buildBufferGeometry(parsedData.attributes); - geometry.userData.header = parsedData.header; + geometry.applyQuaternion(rotation); geometry.computeBoundingBox(); + geometry.userData.origin = new THREE.Vector3().fromArray(origin); + geometry.userData.rotation = rotation; + geometry.userData.header = parsedData.header; + return geometry; }, }; diff --git a/src/Utils/OrientationUtils.js b/src/Utils/OrientationUtils.js index 55a9b63c00..11c76f8bd5 100644 --- a/src/Utils/OrientationUtils.js +++ b/src/Utils/OrientationUtils.js @@ -404,7 +404,11 @@ export default { // get rotations from the local East/North/Up (ENU) frame to both CRS. const fromCrs = this.quaternionFromCRSToEnu(crsIn); const toCrs = this.quaternionFromEnuToCRS(crsOut); - return (origin, target = new THREE.Quaternion()) => - toCrs(origin, target).multiply(fromCrs(origin, quat)); + return (origin, target = new THREE.Quaternion()) => { + if (!origin.isCoordinates) { + origin = new Coordinates(crsIn, ...origin); + } + return toCrs(origin, target).multiply(fromCrs(origin, quat)); + }; }, }; diff --git a/test/unit/lasparser.js b/test/unit/lasparser.js index 3c91d8b5ed..50c7fa089a 100644 --- a/test/unit/lasparser.js +++ b/test/unit/lasparser.js @@ -35,7 +35,15 @@ describe('LASParser', function () { it('parses a las file to a THREE.BufferGeometry', async function () { if (!lasData) { this.skip(); } - const bufferGeometry = await LASParser.parse(lasData); + const options = { + in: { + crs: 'EPSG:3857', + }, + out: { + crs: 'EPSG:3857', + }, + }; + const bufferGeometry = await LASParser.parse(lasData, options); const header = bufferGeometry.userData.header; const origin = bufferGeometry.userData.origin; assert.strictEqual(header.pointCount, 106); @@ -55,7 +63,15 @@ describe('LASParser', function () { it('parses a laz file to a THREE.BufferGeometry', async function () { if (!lazV14Data) { this.skip(); } - const bufferGeometry = await LASParser.parse(lazV14Data); + const options = { + in: { + crs: 'EPSG:3857', + }, + out: { + crs: 'EPSG:3857', + }, + }; + const bufferGeometry = await LASParser.parse(lazV14Data, options); const header = bufferGeometry.userData.header; const origin = bufferGeometry.userData.origin; assert.strictEqual(header.pointCount, 100000); @@ -110,8 +126,12 @@ describe('LASParser', function () { in: { pointCount: header.pointCount, header, + // eb, + crs: 'EPSG:3857', + }, + out: { + crs: 'EPSG:3857', }, - // eb, }; const bufferGeometry = await LASParser.parseChunk(copcData, options); From 60d3f74e6a33eed734da562bd897471ada637f27 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:32:01 +0200 Subject: [PATCH 03/11] feat(PointCloudNode): generate bbox in a referentiel local (view Crs tranformed to z normal) --- src/Core/CopcNode.js | 45 ++------ src/Core/EntwinePointTileNode.js | 33 +----- src/Core/PointCloudNode.js | 51 +++++++- src/Layer/CopcLayer.js | 12 +- src/Layer/EntwinePointTileLayer.js | 23 +--- src/Layer/PointCloudLayer.js | 167 ++++++++++++++++++++++----- src/Layer/Potree2Layer.js | 25 +--- src/Layer/PotreeLayer.js | 17 +-- src/Provider/PointCloudProvider.js | 7 +- src/Source/CopcSource.js | 8 +- src/Source/EntwinePointTileSource.js | 12 +- src/Source/Potree2Source.js | 4 + src/Source/PotreeSource.js | 4 + test/unit/entwine.js | 17 +-- test/unit/potree.js | 7 +- test/unit/potree2.js | 22 ++-- test/unit/potree2layerparsing.js | 34 +++--- test/unit/potreelayerparsing.js | 46 +++++--- 18 files changed, 314 insertions(+), 220 deletions(-) diff --git a/src/Core/CopcNode.js b/src/Core/CopcNode.js index 0dd3735b2a..63764074e2 100644 --- a/src/Core/CopcNode.js +++ b/src/Core/CopcNode.js @@ -1,11 +1,6 @@ -import * as THREE from 'three'; import { Hierarchy } from 'copc'; import PointCloudNode from 'Core/PointCloudNode'; -const size = new THREE.Vector3(); -const position = new THREE.Vector3(); -const translation = new THREE.Vector3(); - function buildId(depth, x, y, z) { return `${depth}-${x}-${y}-${z}`; } @@ -59,37 +54,6 @@ class CopcNode extends PointCloudNode { }); } - /** - * Create an (A)xis (A)ligned (B)ounding (B)ox for the given node given - * `this` is its parent. - * @param {CopcNode} node - The child node - */ - createChildAABB(node) { - // factor to apply, based on the depth difference (can be > 1) - const f = 2 ** (node.depth - this.depth); - - // size of the child node bbox (Vector3), based on the size of the - // parent node, and divided by the factor - this.bbox.getSize(size).divideScalar(f); - - // initialize the child node bbox at the location of the parent node bbox - node.bbox.min.copy(this.bbox.min); - - // position of the parent node, if it was at the same depth as the - // child, found by multiplying the tree position by the factor - position.copy(this).multiplyScalar(f); - - // difference in position between the two nodes, at child depth, and - // scale it using the size - translation.subVectors(node, position).multiply(size); - - // apply the translation to the child node bbox - node.bbox.min.add(translation); - - // use the size computed above to set the max - node.bbox.max.copy(node.bbox.min).add(size); - } - /** * Create a CopcNode from the provided subtree and add it as child * of the current node. @@ -130,6 +94,8 @@ class CopcNode extends PointCloudNode { this.layer, pointCount, ); + child._quaternion = this._quaternion; + child._position = this._position; this.add(child); stack.push(child); } @@ -178,13 +144,18 @@ class CopcNode extends PointCloudNode { await this.loadOctree(); } + this.getCenter(); + const buffer = await this._fetch(this.entryOffset, this.entryLength); const geometry = await this.layer.source.parser(buffer, { in: { ...this.layer.source, pointCount: this.numPoints, }, - out: this.layer, + out: { + ...this.layer, + center: this.center, + }, }); return geometry; diff --git a/src/Core/EntwinePointTileNode.js b/src/Core/EntwinePointTileNode.js index fa4efc4ccf..36728b5cf5 100644 --- a/src/Core/EntwinePointTileNode.js +++ b/src/Core/EntwinePointTileNode.js @@ -1,11 +1,6 @@ -import * as THREE from 'three'; import Fetcher from 'Provider/Fetcher'; import PointCloudNode from 'Core/PointCloudNode'; -const size = new THREE.Vector3(); -const position = new THREE.Vector3(); -const translation = new THREE.Vector3(); - function buildId(depth, x, y, z) { return `${depth}-${x}-${y}-${z}`; } @@ -69,32 +64,6 @@ class EntwinePointTileNode extends PointCloudNode { this.url = `${this.layer.source.url}/ept-data/${this.id}.${this.layer.source.extension}`; } - createChildAABB(node) { - // factor to apply, based on the depth difference (can be > 1) - const f = 2 ** (node.depth - this.depth); - - // size of the child node bbox (Vector3), based on the size of the - // parent node, and divided by the factor - this.bbox.getSize(size).divideScalar(f); - - // initialize the child node bbox at the location of the parent node bbox - node.bbox.min.copy(this.bbox.min); - - // position of the parent node, if it was at the same depth than the - // child, found by multiplying the tree position by the factor - position.copy(this).multiplyScalar(f); - - // difference in position between the two nodes, at child depth, and - // scale it using the size - translation.subVectors(node, position).multiply(size); - - // apply the translation to the child node bbox - node.bbox.min.add(translation); - - // use the size computed above to set the max - node.bbox.max.copy(node.bbox.min).add(size); - } - get octreeIsLoaded() { return this.numPoints >= 0; } @@ -131,6 +100,8 @@ class EntwinePointTileNode extends PointCloudNode { if (typeof numPoints == 'number') { const child = new EntwinePointTileNode(depth, x, y, z, this.layer, numPoints); + child._quaternion = this._quaternion; + child._position = this._position; this.add(child); stack.push(child); } diff --git a/src/Core/PointCloudNode.js b/src/Core/PointCloudNode.js index 3042217602..d5a3589d1a 100644 --- a/src/Core/PointCloudNode.js +++ b/src/Core/PointCloudNode.js @@ -1,5 +1,9 @@ import * as THREE from 'three'; +const size = new THREE.Vector3(); +const position = new THREE.Vector3(); +const translation = new THREE.Vector3(); + class PointCloudNode extends THREE.EventDispatcher { constructor(numPoints = 0, layer) { super(); @@ -9,6 +13,9 @@ class PointCloudNode extends THREE.EventDispatcher { this.children = []; this.bbox = new THREE.Box3(); + this._bbox = new THREE.Box3(); + this._position = new THREE.Vector3(); + this._quaternion = new THREE.Quaternion(); this.sse = -1; } @@ -18,14 +25,56 @@ class PointCloudNode extends THREE.EventDispatcher { this.createChildAABB(node, indexChild); } + /** + * Create an (A)xis (A)ligned (B)ounding (B)ox for the given node given + * `this` is its parent. + * @param {CopcNode} childNode - The child node + */ + createChildAABB(childNode) { + // factor to apply, based on the depth difference (can be > 1) + const f = 2 ** (childNode.depth - this.depth); + + // size of the child node bbox (Vector3), based on the size of the + // parent node, and divided by the factor + this._bbox.getSize(size).divideScalar(f); + + // initialize the child node bbox at the location of the parent node bbox + childNode._bbox.min.copy(this._bbox.min); + + // position of the parent node, if it was at the same depth as the + // child, found by multiplying the tree position by the factor + position.copy(this).multiplyScalar(f); + + // difference in position between the two nodes, at child depth, and + // scale it using the size + translation.subVectors(childNode, position).multiply(size); + + // apply the translation to the child node bbox + childNode._bbox.min.add(translation); + + // use the size computed above to set the max + childNode._bbox.max.copy(childNode._bbox.min).add(size); + } + + getCenter() { + // get center of the bbox in the world referentiel + const centerBbox = new THREE.Vector3(); + this._bbox.getCenter(centerBbox); + const rotInv = this._quaternion.clone().invert(); + const origVector = this._position; + this.center = centerBbox.clone().applyQuaternion(rotInv).add(origVector); + } + load() { // Query octree/HRC if we don't have children potreeNode yet. if (!this.octreeIsLoaded) { this.loadOctree(); } + this.getCenter(); + return this.layer.source.fetcher(this.url, this.layer.source.networkOptions) - .then(file => this.layer.source.parse(file, { out: this.layer, in: this.layer.source })); + .then(file => this.layer.source.parse(file, { out: { ...this.layer, center: this.center }, in: this.layer.source })); } findCommonAncestor(node) { diff --git a/src/Layer/CopcLayer.js b/src/Layer/CopcLayer.js index 3fb25cb3c2..e39cc39faf 100644 --- a/src/Layer/CopcLayer.js +++ b/src/Layer/CopcLayer.js @@ -43,16 +43,14 @@ class CopcLayer extends PointCloudLayer { const { pageOffset, pageLength } = source.info.rootHierarchyPage; this.spacing = source.info.spacing; + this.scale = new THREE.Vector3(1.0, 1.0, 1.0); + this.offset = new THREE.Vector3(0.0, 0.0, 0.0); - this.root = new CopcNode(0, 0, 0, 0, pageOffset, pageLength, this, -1); - this.root.bbox.min.fromArray(cube, 0); - this.root.bbox.max.fromArray(cube, 3); + this.setElevationRange(source.header.min[2], source.header.max[2]); - this.minElevationRange = this.minElevationRange ?? source.header.min[2]; - this.maxElevationRange = this.maxElevationRange ?? source.header.max[2]; + this.root = new CopcNode(0, 0, 0, 0, pageOffset, pageLength, this, -1); - this.scale = new THREE.Vector3(1.0, 1.0, 1.0); - this.offset = new THREE.Vector3(0.0, 0.0, 0.0); + this.setRootBbox(cube.slice(0, 3), cube.slice(3, 6)); return this.root.loadOctree().then(resolve); }); diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index 8edf3714c8..14d872f38b 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -1,11 +1,6 @@ import * as THREE from 'three'; import EntwinePointTileNode from 'Core/EntwinePointTileNode'; import PointCloudLayer from 'Layer/PointCloudLayer'; -import Extent from 'Core/Geographic/Extent'; - -const bboxMesh = new THREE.Mesh(); -const box3 = new THREE.Box3(); -bboxMesh.geometry.boundingBox = box3; /** * @property {boolean} isEntwinePointTileLayer - Used to checkout whether this @@ -37,9 +32,6 @@ class EntwinePointTileLayer extends PointCloudLayer { * 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); @@ -56,21 +48,18 @@ class EntwinePointTileLayer extends PointCloudLayer { this.scale = new THREE.Vector3(1, 1, 1); const resolve = this.addInitializationStep(); - this.whenReady = this.source.whenReady.then(() => { + this.whenReady = this.source.whenReady.then((source) => { // NOTE: this spacing is kinda arbitrary here, we take the width and // length (height can be ignored), and we divide by the specified // span in ept.json. This needs improvements. - this.spacing = (Math.abs(this.source.bounds[3] - this.source.bounds[0]) - + Math.abs(this.source.bounds[4] - this.source.bounds[1])) / (2 * this.source.span); + this.spacing = (Math.abs(source.bounds[3] - source.bounds[0]) + + Math.abs(source.bounds[4] - source.bounds[1])) / (2 * source.span); - this.root = new EntwinePointTileNode(0, 0, 0, 0, this, -1); + this.setElevationRange(source.boundsConforming[2], source.boundsConforming[5]); - this.root.bbox.min.fromArray(this.source.boundsConforming, 0); - this.root.bbox.max.fromArray(this.source.boundsConforming, 3); - this.minElevationRange = this.minElevationRange ?? this.source.boundsConforming[2]; - this.maxElevationRange = this.maxElevationRange ?? this.source.boundsConforming[5]; + this.root = new EntwinePointTileNode(0, 0, 0, 0, this, -1); - this.extent = Extent.fromBox3(config.crs || 'EPSG:4326', this.root.bbox); + this.setRootBbox(source.bounds.slice(0, 3), source.bounds.slice(3, 6)); return this.root.loadOctree().then(resolve); }); diff --git a/src/Layer/PointCloudLayer.js b/src/Layer/PointCloudLayer.js index 527bafb6b9..c49a2215ab 100644 --- a/src/Layer/PointCloudLayer.js +++ b/src/Layer/PointCloudLayer.js @@ -2,28 +2,59 @@ import * as THREE from 'three'; import GeometryLayer from 'Layer/GeometryLayer'; import PointsMaterial, { PNTS_MODE } from 'Renderer/PointsMaterial'; import Picking from 'Core/Picking'; +import proj4 from 'proj4'; +import OrientationUtils from 'Utils/OrientationUtils'; const point = new THREE.Vector3(); const bboxMesh = new THREE.Mesh(); const box3 = new THREE.Box3(); bboxMesh.geometry.boundingBox = box3; +function positionFromBbox(bbox) { + const array = new Float32Array(8 * 3); + + const min = bbox.min; + const max = bbox.max; + + array[0] = max.x; array[1] = max.y; array[2] = max.z; + array[3] = min.x; array[4] = max.y; array[5] = max.z; + array[6] = min.x; array[7] = min.y; array[8] = max.z; + array[9] = max.x; array[10] = min.y; array[11] = max.z; + array[12] = max.x; array[13] = max.y; array[14] = min.z; + array[15] = min.x; array[16] = max.y; array[17] = min.z; + array[18] = min.x; array[19] = min.y; array[20] = min.z; + array[21] = max.x; array[22] = min.y; array[23] = min.z; + return array; +} + function initBoundingBox(elt, layer) { - elt.tightbbox.getSize(box3.max); - box3.max.multiplyScalar(0.5); - box3.min.copy(box3.max).negate(); - elt.obj.boxHelper = new THREE.BoxHelper(bboxMesh); - elt.obj.boxHelper.geometry = elt.obj.boxHelper.geometry.toNonIndexed(); - elt.obj.boxHelper.computeLineDistances(); - elt.obj.boxHelper.material = elt.childrenBitField ? new THREE.LineDashedMaterial({ dashSize: 0.25, gapSize: 0.25 }) : new THREE.LineBasicMaterial(); - elt.obj.boxHelper.material.color.setHex(0); - elt.obj.boxHelper.material.linewidth = 2; - elt.obj.boxHelper.frustumCulled = false; - elt.obj.boxHelper.position.copy(elt.tightbbox.min).add(box3.max); - elt.obj.boxHelper.autoUpdateMatrix = false; - layer.bboxes.add(elt.obj.boxHelper); - elt.obj.boxHelper.updateMatrix(); - elt.obj.boxHelper.updateMatrixWorld(); + // bbox in local ref -> cyan + const boxHelper = elt.boxHelper; + elt.obj.boxHelper = boxHelper; + layer.bboxes.add(boxHelper); + boxHelper.updateMatrixWorld(true); + + // tightbbox in local ref -> blue + const tightboxHelper = new THREE.BoxHelper(undefined, 0x0000ff); + tightboxHelper.geometry.attributes.position.array = positionFromBbox(elt.obj.geometry.boundingBox); + tightboxHelper.applyMatrix4(elt.obj.matrixWorld); + elt.obj.tightboxHelper = tightboxHelper; + layer.bboxes.add(tightboxHelper); + tightboxHelper.updateMatrixWorld(true); +} + +function createBoxHelper(bbox, quaternion, origin) { + const boxHelper = new THREE.BoxHelper(undefined, 0x00ffff); + boxHelper.geometry.attributes.position.array = positionFromBbox(bbox); + + boxHelper.position.copy(origin); + boxHelper.quaternion.copy(quaternion.clone().invert()); + boxHelper.updateMatrix(); + boxHelper.updateMatrixWorld(); + + boxHelper.geometry.computeBoundingBox(); + + return boxHelper; } function computeSSEPerspective(context, pointSize, spacing, elt, distance) { @@ -70,6 +101,7 @@ function markForDeletion(elt) { if (__DEBUG__) { if (elt.obj.boxHelper) { elt.obj.boxHelper.visible = false; + elt.obj.tightboxHelper.visible = false; } } } @@ -179,9 +211,12 @@ class PointCloudLayer extends GeometryLayer { this.group = group; this.object3d.add(this.group); + this.bboxes = bboxes || new THREE.Group(); + this.bboxes.name = 'bboxes'; this.bboxes.visible = false; this.object3d.add(this.bboxes); + this.group.updateMatrixWorld(); // default config @@ -231,6 +266,62 @@ class PointCloudLayer extends GeometryLayer { this.root = undefined; } + setRootBbox(min, max) { + let forward = (x => x); + if (this.source.crs !== this.crs) { + try { + forward = proj4(this.source.crs, this.crs).forward; + } catch (err) { + throw new Error(`${err} is not defined in proj4`); + } + } + + const corners = [ + ...forward([max[0], max[1], max[2]]), + ...forward([min[0], max[1], max[2]]), + ...forward([min[0], min[1], max[2]]), + ...forward([max[0], min[1], max[2]]), + ...forward([max[0], max[1], min[2]]), + ...forward([min[0], max[1], min[2]]), + ...forward([min[0], min[1], min[2]]), + ...forward([max[0], min[1], min[2]]), + ]; + + // get center of box at altitude=Z and project it in view crs; + const origin = forward([(min[0] + max[0]) * 0.5, (min[1] + max[1]) * 0.5, 0]); + + // getLocalRotation() + const isGeocentric = proj4.defs(this.crs).projName === 'geocent'; + let rotation = new THREE.Quaternion(); + if (isGeocentric) { + rotation = OrientationUtils.quaternionFromCRSToCRS(this.crs, 'EPSG:4326')(origin); + } + + // project corners in local referentiel + const cornersLocal = []; + for (let i = 0; i < 24; i += 3) { + const cornerLocal = new THREE.Vector3( + corners[i] - origin[0], + corners[i + 1] - origin[1], + corners[i + 2] - origin[2], + ); + cornerLocal.applyQuaternion(rotation); + cornersLocal.push(...cornerLocal.toArray()); + } + + // get the bbox containing all cornersLocal => the bboxLocal + const _bbox = new THREE.Box3().setFromArray(cornersLocal); + this.root._bbox = _bbox; + + this.root._position = new THREE.Vector3(...origin); + this.root._quaternion = rotation; + } + + setElevationRange(zmin, zmax) { + this.minElevationRange = this.minElevationRange ?? zmin; + this.maxElevationRange = this.maxElevationRange ?? zmax; + } + preUpdate(context, changeSources) { // See https://cesiumjs.org/hosted-apps/massiveworlds/downloads/Ring/WorldScaleTerrainRendering.pptx // slide 17 @@ -291,17 +382,29 @@ class PointCloudLayer extends GeometryLayer { return; } - // pick the best bounding box - const bbox = (elt.tightbbox ? elt.tightbbox : elt.bbox); - elt.visible = context.camera.isBox3Visible(bbox, this.object3d.matrixWorld); + // get object on which to measure distance + let obj; + if (elt.obj) { + obj = elt.obj; + } else { + elt.boxHelper = createBoxHelper(elt._bbox, elt._quaternion, elt._position); + obj = elt.boxHelper; + } + const bbox = obj.geometry.boundingBox; + elt.visible = context.camera.isBox3Visible(obj.geometry.boundingBox, obj.matrixWorld); + if (!elt.visible) { markForDeletion(elt); return; } elt.notVisibleSince = undefined; - point.copy(context.camera.camera3D.position).sub(this.object3d.getWorldPosition(new THREE.Vector3())); - point.applyQuaternion(this.object3d.getWorldQuaternion(new THREE.Quaternion()).invert()); + + point.copy(context.camera.camera3D.position) + .sub(obj.getWorldPosition(new THREE.Vector3())) + .applyQuaternion(obj.getWorldQuaternion(new THREE.Quaternion()).invert()); + + const distanceToCamera = bbox.distanceToPoint(point); // only load geometry if this elements has points if (elt.numPoints !== 0) { @@ -314,12 +417,14 @@ class PointCloudLayer extends GeometryLayer { initBoundingBox(elt, layer); } elt.obj.boxHelper.visible = true; - elt.obj.boxHelper.material.color.r = 1 - elt.sse; - elt.obj.boxHelper.material.color.g = elt.sse; + // elt.obj.boxHelper.material.color.r = 1 - elt.sse; + // elt.obj.boxHelper.material.color.g = elt.sse; + + elt.obj.tightboxHelper.visible = true; } } } else if (!elt.promise) { - const distance = Math.max(0.001, bbox.distanceToPoint(point)); + const distance = Math.max(0.001, distanceToCamera); // Increase priority of nearest node const priority = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance) / distance; elt.promise = context.scheduler.execute({ @@ -331,8 +436,6 @@ class PointCloudLayer extends GeometryLayer { earlyDropFunction: cmd => !cmd.requester.visible || !this.visible, }).then((pts) => { elt.obj = pts; - // store tightbbox to avoid ping-pong (bbox = larger => visible, tight => invisible) - elt.tightbbox = pts.tightbbox; // make sure to add it here, otherwise it might never // be added nor cleaned @@ -349,8 +452,7 @@ class PointCloudLayer extends GeometryLayer { } if (elt.children && elt.children.length) { - const distance = bbox.distanceToPoint(point); - elt.sse = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance) / this.sseThreshold; + elt.sse = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distanceToCamera) / this.sseThreshold; if (elt.sse >= 1) { return elt.children; } else { @@ -434,6 +536,17 @@ class PointCloudLayer extends GeometryLayer { } obj.boxHelper.geometry.dispose(); } + if (obj.tightboxHelper) { + obj.tightboxHelper.removeMe = true; + if (Array.isArray(obj.tightboxHelper.material)) { + for (const material of obj.tightboxHelper.material) { + material.dispose(); + } + } else { + obj.tightboxHelper.material.dispose(); + } + obj.tightboxHelper.geometry.dispose(); + } } } } diff --git a/src/Layer/Potree2Layer.js b/src/Layer/Potree2Layer.js index 3e036c52cd..cdbfcbf326 100644 --- a/src/Layer/Potree2Layer.js +++ b/src/Layer/Potree2Layer.js @@ -36,14 +36,9 @@ of the authors and should not be interpreted as representing official policies, 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, @@ -154,8 +149,7 @@ class Potree2Layer extends PointCloudLayer { this.isPotreeLayer = true; const resolve = this.addInitializationStep(); - - this.source.whenReady.then((metadata) => { + this.whenReady = this.source.whenReady.then((metadata) => { this.scale = new THREE.Vector3(1, 1, 1); this.metadata = metadata; this.pointAttributes = parseAttributes(metadata.attributes); @@ -167,29 +161,20 @@ class Potree2Layer extends PointCloudLayer { 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); + // currently the spec on potree2 only have boundingBox and no boundsConforming + this.setElevationRange(metadata.boundingBox.min[2], metadata.boundingBox.max[2]); const root = new Potree2Node(0, 0, this); - - root.bbox = boundingBox; - root.boundingSphere = boundingBox.getBoundingSphere(new THREE.Sphere()); - - this.minElevationRange = this.minElevationRange ?? metadata.boundingBox.min[2]; - this.maxElevationRange = this.maxElevationRange ?? metadata.boundingBox.max[2]; - 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); + this.setRootBbox(metadata.boundingBox.min, metadata.boundingBox.max); + return this.root.loadOctree().then(resolve); }); } diff --git a/src/Layer/PotreeLayer.js b/src/Layer/PotreeLayer.js index 9733da97e3..519b40b40a 100644 --- a/src/Layer/PotreeLayer.js +++ b/src/Layer/PotreeLayer.js @@ -1,11 +1,6 @@ import * as THREE from 'three'; import PointCloudLayer from 'Layer/PointCloudLayer'; import PotreeNode from 'Core/PotreeNode'; -import Extent from 'Core/Geographic/Extent'; - -const bboxMesh = new THREE.Mesh(); -const box3 = new THREE.Box3(); -bboxMesh.geometry.boundingBox = box3; /** * @property {boolean} isPotreeLayer - Used to checkout whether this layer @@ -52,8 +47,7 @@ class PotreeLayer extends PointCloudLayer { this.isPotreeLayer = true; const resolve = this.addInitializationStep(); - - this.source.whenReady.then((cloud) => { + this.whenReady = this.source.whenReady.then((cloud) => { this.scale = new THREE.Vector3().addScalar(cloud.scale); this.spacing = cloud.spacing; this.hierarchyStepSize = cloud.hierarchyStepSize; @@ -66,14 +60,13 @@ class PotreeLayer extends PointCloudLayer { this.supportsProgressiveDisplay = (this.source.extension === 'cin'); + this.setElevationRange(cloud.tightBoundingBox.lz, cloud.tightBoundingBox.uz); + this.root = new PotreeNode(0, 0, this); - this.root.bbox.min.set(cloud.boundingBox.lx, cloud.boundingBox.ly, cloud.boundingBox.lz); - this.root.bbox.max.set(cloud.boundingBox.ux, cloud.boundingBox.uy, cloud.boundingBox.uz); - this.minElevationRange = this.minElevationRange ?? cloud.boundingBox.lz; - this.maxElevationRange = this.maxElevationRange ?? cloud.boundingBox.uz; + this.setRootBbox([cloud.boundingBox.lx, cloud.boundingBox.ly, cloud.boundingBox.lz], + [cloud.boundingBox.ux, cloud.boundingBox.uy, cloud.boundingBox.uz]); - this.extent = Extent.fromBox3(this.source.crs || 'EPSG:4326', this.root.bbox); return this.root.loadOctree().then(resolve); }); } diff --git a/src/Provider/PointCloudProvider.js b/src/Provider/PointCloudProvider.js index 57e24bcbf5..a933fc3826 100644 --- a/src/Provider/PointCloudProvider.js +++ b/src/Provider/PointCloudProvider.js @@ -36,10 +36,13 @@ export default { addPickingAttribute(points); points.frustumCulled = false; points.matrixAutoUpdate = false; - points.position.copy(geometry.userData.origin || node.bbox.min); points.scale.copy(layer.scale); + points.position.copy(geometry.userData.origin || node.bbox.min); + + const quaternion = geometry.userData.rotation.clone().invert(); + points.quaternion.copy(quaternion); points.updateMatrix(); - points.tightbbox = geometry.boundingBox.applyMatrix4(points.matrix); + points.layer = layer; points.extent = Extent.fromBox3(command.view.referenceCrs, node.bbox); points.userData.node = node; diff --git a/src/Source/CopcSource.js b/src/Source/CopcSource.js index 9f91f27f57..f8793315ab 100644 --- a/src/Source/CopcSource.js +++ b/src/Source/CopcSource.js @@ -1,10 +1,8 @@ import proj4 from 'proj4'; import { Binary, Info, Las } from 'copc'; -import Extent from 'Core/Geographic/Extent'; import Fetcher from 'Provider/Fetcher'; import LASParser from 'Parser/LASParser'; import Source from 'Source/Source'; -import * as THREE from 'three'; /** * @param {function(number, number):Promise} fetcher @@ -99,6 +97,7 @@ class CopcSource extends Source { range: `bytes=${begin}-${end - 1}`, }, }).then(buffer => new Uint8Array(buffer)); + this.whenReady = getHeaders(get).then((metadata) => { this.header = metadata.header; this.info = metadata.info; @@ -119,11 +118,6 @@ class CopcSource extends Source { proj4.defs(this.crs, projCS); } - const bbox = new THREE.Box3(); - bbox.min.fromArray(this.info.cube, 0); - bbox.max.fromArray(this.info.cube, 3); - this.extent = Extent.fromBox3(this.crs, bbox); - return this; }); } diff --git a/src/Source/EntwinePointTileSource.js b/src/Source/EntwinePointTileSource.js index 643a9600ca..a01382ec9e 100644 --- a/src/Source/EntwinePointTileSource.js +++ b/src/Source/EntwinePointTileSource.js @@ -31,11 +31,17 @@ class EntwinePointTileSource extends Source { this.isEntwinePointTileSource = true; this.colorDepth = config.colorDepth; + this.fetcher = Fetcher.arrayBuffer; + // Necessary because we use the url without the ept.json part as a base this.url = this.url.replace('/ept.json', ''); // https://entwine.io/entwine-point-tile.html#ept-json this.whenReady = Fetcher.json(`${this.url}/ept.json`, this.networkOptions).then((metadata) => { + this.boundsConforming = metadata.boundsConforming; + this.bounds = metadata.bounds; + this.span = metadata.span; + // Set parser and its configuration from schema this.parse = metadata.dataType === 'laszip' ? LASParser.parse : PotreeBinParser.parse; this.extension = metadata.dataType === 'laszip' ? 'laz' : 'bin'; @@ -59,14 +65,8 @@ class EntwinePointTileSource extends Source { } } - this.boundsConforming = metadata.boundsConforming; - this.bounds = metadata.bounds; - this.span = metadata.span; - return this; }); - - this.fetcher = Fetcher.arrayBuffer; } } diff --git a/src/Source/Potree2Source.js b/src/Source/Potree2Source.js index 75e8cd8836..9bcd3988b7 100644 --- a/src/Source/Potree2Source.js +++ b/src/Source/Potree2Source.js @@ -151,6 +151,10 @@ class Potree2Source extends Source { if (!source.file) { throw new Error('New Potree2Source: file is required'); } + if (!source.crs) { + // with better data and the spec this might be removed + throw new Error('New PotreeSource: crs is required'); + } super(source); this.file = source.file; diff --git a/src/Source/PotreeSource.js b/src/Source/PotreeSource.js index 0796f8484a..4229c50d9c 100644 --- a/src/Source/PotreeSource.js +++ b/src/Source/PotreeSource.js @@ -63,6 +63,10 @@ class PotreeSource extends Source { if (!source.file) { throw new Error('New PotreeSource: file is required'); } + if (!source.crs) { + // with better data and the spec this might be removed + throw new Error('New PotreeSource: crs is required'); + } super(source); this.file = source.file; diff --git a/test/unit/entwine.js b/test/unit/entwine.js index 43fc24237e..a85baeec79 100644 --- a/test/unit/entwine.js +++ b/test/unit/entwine.js @@ -1,13 +1,14 @@ import assert from 'assert'; +import { Vector3 } from 'three'; import View from 'Core/View'; import GlobeView from 'Core/Prefab/GlobeView'; import Coordinates from 'Core/Geographic/Coordinates'; import EntwinePointTileSource from 'Source/EntwinePointTileSource'; import EntwinePointTileLayer from 'Layer/EntwinePointTileLayer'; import EntwinePointTileNode from 'Core/EntwinePointTileNode'; -import LASParser from 'Parser/LASParser'; import sinon from 'sinon'; import Fetcher from 'Provider/Fetcher'; +import LASParser from 'Parser/LASParser'; import Renderer from './bootstrap'; import eptFile from '../data/entwine/ept.json'; @@ -78,18 +79,16 @@ describe('Entwine Point Tile', function () { }); }); - describe('Layer', function () { + describe('Entwine Point Tile Layer', function () { let renderer; - let placement; let view; let layer; let context; before(function (done) { renderer = new Renderer(); - placement = { coord: new Coordinates('EPSG:4326', 0, 0), range: 250 }; - view = new GlobeView(renderer.domElement, placement, { renderer }); - layer = new EntwinePointTileLayer('test', { source }, view); + view = new GlobeView(renderer.domElement, {}, { renderer }); + layer = new EntwinePointTileLayer('test', { source }); context = { camera: view.camera, @@ -117,8 +116,10 @@ describe('Entwine Point Tile', function () { }); it('tries to update on the root and succeeds', function (done) { + const lookAt = new Vector3(); + const coord = new Coordinates(view.referenceCrs).setFromVector3(layer.root.bbox.getCenter(lookAt)); view.controls.lookAtCoordinate({ - coord: source.center, + coord, range: 250, }, false) .then(() => { @@ -135,7 +136,7 @@ describe('Entwine Point Tile', function () { }); }); - describe('Node', function () { + describe('Entwine Point Tile Node', function () { let root; before(function () { const layer = { source: { url: 'http://server.geo', extension: 'laz' } }; diff --git a/test/unit/potree.js b/test/unit/potree.js index b744b603a7..534a0aec16 100644 --- a/test/unit/potree.js +++ b/test/unit/potree.js @@ -60,6 +60,7 @@ describe('Potree', function () { const source = new PotreeSource({ file: fileName, url: baseurl, + crs: 'EPSG:4978', }); // Configure Point Cloud layer @@ -85,10 +86,10 @@ describe('Potree', function () { describe('potree Layer', function () { it('Add point potree layer', function (done) { View.prototype.addLayer.call(viewer, potreeLayer) - .then((layer) => { + .then(() => { context.camera.camera3D.updateMatrixWorld(); - assert.equal(layer.root.children.length, 6); - layer.bboxes.visible = true; + assert.equal(potreeLayer.root.children.length, 6); + potreeLayer.bboxes.visible = true; done(); }).catch(done); }); diff --git a/test/unit/potree2.js b/test/unit/potree2.js index ff1b48a0c7..73d3782ead 100644 --- a/test/unit/potree2.js +++ b/test/unit/potree2.js @@ -17,16 +17,18 @@ describe('Potree2', function () { before(function () { renderer = new Renderer(); - viewer = new View('EPSG:3946', renderer.domElement, { renderer }); + viewer = new View('EPSG:4978', renderer.domElement, { renderer }); viewer.camera.camera3D.position.copy(new Vector3(0, 0, 10)); // Configure Point Cloud layer + const source = new Potree2Source({ + file: 'metadata.json', + url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/potree2.0/lion', + networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, + crs: 'EPSG:4978', + }); potreeLayer = new Potree2Layer('lion', { - source: new Potree2Source({ - file: 'metadata.json', - url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/potree2.0/lion', - networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, - }), + source, crs: viewer.referenceCrs, }); @@ -41,10 +43,10 @@ describe('Potree2', function () { it('Add point potree2 layer', function (done) { View.prototype.addLayer.call(viewer, potreeLayer) - .then((layer) => { + .then(() => { context.camera.camera3D.updateMatrixWorld(); - assert.equal(layer.root.children.length, 6); - layer.bboxes.visible = true; + assert.equal(potreeLayer.root.children.length, 6); + potreeLayer.bboxes.visible = true; done(); }).catch(done); }); @@ -62,7 +64,7 @@ describe('Potree2', function () { assert.equal(potreeLayer.group.children.length, 1); done(); }).catch(done); - }).timeout(5000); + }).timeout(10000); it('postUpdate potree2 layer', function () { potreeLayer.postUpdate(context, potreeLayer); diff --git a/test/unit/potree2layerparsing.js b/test/unit/potree2layerparsing.js index 9f1dc423fc..2ae060fc26 100644 --- a/test/unit/potree2layerparsing.js +++ b/test/unit/potree2layerparsing.js @@ -50,13 +50,16 @@ describe('Potree2 Provider', function () { file: 'metadata.json', url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/potree2.0/lion', networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, + crs: 'EPSG:4978', 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; + const p1 = layer1.whenReady.then(() => { + const normalDefined = layer1.material.defines.NORMAL + || layer1.material.defines.NORMAL_SPHEREMAPPED + || layer1.material.defines.NORMAL_OCT16; assert.ok(!normalDefined); }); @@ -65,6 +68,7 @@ describe('Potree2 Provider', function () { file: 'metadata.json', url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/potree2.0/lion', networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, + crs: 'EPSG:4978', metadata: { version: '2.0', name: 'lion', @@ -121,10 +125,10 @@ describe('Potree2 Provider', function () { 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); + const p2 = layer2.whenReady.then(() => { + assert.ok(layer2.material.defines.NORMAL); + assert.ok(!layer2.material.defines.NORMAL_SPHEREMAPPED); + assert.ok(!layer2.material.defines.NORMAL_OCT16); }); // // spheremapped normals @@ -132,6 +136,7 @@ describe('Potree2 Provider', function () { file: 'metadata.json', url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/potree2.0/lion', networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, + crs: 'EPSG:4978', metadata: { version: '2.0', name: 'lion', @@ -188,10 +193,10 @@ describe('Potree2 Provider', function () { 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); + const p3 = layer3.whenReady.then(() => { + assert.ok(!layer3.material.defines.NORMAL); + assert.ok(layer3.material.defines.NORMAL_SPHEREMAPPED); + assert.ok(!layer3.material.defines.NORMAL_OCT16); }); // // oct16 normals @@ -199,6 +204,7 @@ describe('Potree2 Provider', function () { file: 'metadata.json', url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds/potree2.0/lion', networkOptions: process.env.HTTPS_PROXY ? { agent: new HttpsProxyAgent(process.env.HTTPS_PROXY) } : {}, + crs: 'EPSG:4978', metadata: { version: '2.0', name: 'lion', @@ -256,10 +262,10 @@ describe('Potree2 Provider', function () { 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); + .then(() => { + assert.ok(!layer4.material.defines.NORMAL); + assert.ok(!layer4.material.defines.NORMAL_SPHEREMAPPED); + assert.ok(layer4.material.defines.NORMAL_OCT16); }); layers.forEach(p => View.prototype.addLayer.call(view, p)); diff --git a/test/unit/potreelayerparsing.js b/test/unit/potreelayerparsing.js index 0f1207edb2..88c61ff61f 100644 --- a/test/unit/potreelayerparsing.js +++ b/test/unit/potreelayerparsing.js @@ -42,7 +42,8 @@ describe('Potree Provider', function () { it('cloud with no normal information', function _it(done) { // No normals const cloud = { - boundingBox: { lx: 0, ly: 1, ux: 2, uy: 3 }, + boundingBox: { lx: 10, ly: 20, ux: 30, uy: 40 }, + tightBoundingBox: { lx: 1, ly: 2, ux: 3, uy: 4 }, scale: 1.0, pointAttributes: ['POSITION', 'RGB'], octreeDir: 'data', @@ -51,13 +52,16 @@ describe('Potree Provider', function () { const source = new PotreeSource({ file: fileName, url: baseurl, + crs: 'EPSG:4978', cloud, }); const layer = new PotreeLayer('pointsCloudNoNormal', { source, crs: view.referenceCrs }); View.prototype.addLayer.call(view, layer); - layer.whenReady.then((l) => { - const normalDefined = l.material.defines.NORMAL || l.material.defines.NORMAL_SPHEREMAPPED || l.material.defines.NORMAL_OCT16; + layer.whenReady.then(() => { + const normalDefined = layer.material.defines.NORMAL + || layer.material.defines.NORMAL_SPHEREMAPPED + || layer.material.defines.NORMAL_OCT16; assert.ok(!normalDefined); done(); }).catch(done); @@ -66,7 +70,8 @@ describe('Potree Provider', function () { it('cloud with normals as vector', function _it(done) { // // // // normals as vector const cloud = { - boundingBox: { lx: 0, ly: 1, ux: 2, uy: 3 }, + boundingBox: { lx: 10, ly: 20, ux: 30, uy: 40 }, + tightBoundingBox: { lx: 1, ly: 2, ux: 3, uy: 4 }, scale: 1.0, pointAttributes: ['POSITION', 'NORMAL', 'CLASSIFICATION'], octreeDir: 'data', @@ -75,15 +80,16 @@ describe('Potree Provider', function () { const source = new PotreeSource({ file: fileName, url: baseurl, + crs: 'EPSG:4978', cloud, }); const layer = new PotreeLayer('pointsCloud2', { source, crs: view.referenceCrs }); View.prototype.addLayer.call(view, layer); - layer.whenReady.then((l) => { - assert.ok(l.material.defines.NORMAL); - assert.ok(!l.material.defines.NORMAL_SPHEREMAPPED); - assert.ok(!l.material.defines.NORMAL_OCT16); + layer.whenReady.then(() => { + assert.ok(layer.material.defines.NORMAL); + assert.ok(!layer.material.defines.NORMAL_SPHEREMAPPED); + assert.ok(!layer.material.defines.NORMAL_OCT16); done(); }).catch(done); }); @@ -91,7 +97,8 @@ describe('Potree Provider', function () { it('cloud with spheremapped normals', function _it(done) { // // spheremapped normals const cloud = { - boundingBox: { lx: 0, ly: 1, ux: 2, uy: 3 }, + boundingBox: { lx: 10, ly: 20, ux: 30, uy: 40 }, + tightBoundingBox: { lx: 1, ly: 2, ux: 3, uy: 4 }, scale: 1.0, pointAttributes: ['POSITION', 'COLOR_PACKED', 'NORMAL_SPHEREMAPPED'], octreeDir: 'data', @@ -99,15 +106,16 @@ describe('Potree Provider', function () { const source = new PotreeSource({ file: fileName, url: baseurl, + crs: 'EPSG:4978', cloud, }); const layer = new PotreeLayer('pointsCloud3', { source, crs: view.referenceCrs }); View.prototype.addLayer.call(view, layer); - layer.whenReady.then((l) => { - assert.ok(!l.material.defines.NORMAL); - assert.ok(l.material.defines.NORMAL_SPHEREMAPPED); - assert.ok(!l.material.defines.NORMAL_OCT16); + layer.whenReady.then(() => { + assert.ok(!layer.material.defines.NORMAL); + assert.ok(layer.material.defines.NORMAL_SPHEREMAPPED); + assert.ok(!layer.material.defines.NORMAL_OCT16); done(); }).catch(done); }); @@ -115,7 +123,8 @@ describe('Potree Provider', function () { it('cloud with oct16 normals', function _it(done) { // // // oct16 normals const cloud = { - boundingBox: { lx: 0, ly: 1, ux: 2, uy: 3 }, + boundingBox: { lx: 10, ly: 20, ux: 30, uy: 40 }, + tightBoundingBox: { lx: 1, ly: 2, ux: 3, uy: 4 }, scale: 1.0, pointAttributes: ['POSITION', 'COLOR_PACKED', 'CLASSIFICATION', 'NORMAL_OCT16'], octreeDir: 'data', @@ -124,15 +133,16 @@ describe('Potree Provider', function () { file: fileName, url: baseurl, cloud, + crs: 'EPSG:4978', }); const layer = new PotreeLayer('pointsCloud4', { source, crs: view.referenceCrs }); View.prototype.addLayer.call(view, layer); layer.whenReady - .then((l) => { - assert.ok(!l.material.defines.NORMAL); - assert.ok(!l.material.defines.NORMAL_SPHEREMAPPED); - assert.ok(l.material.defines.NORMAL_OCT16); + .then(() => { + assert.ok(!layer.material.defines.NORMAL); + assert.ok(!layer.material.defines.NORMAL_SPHEREMAPPED); + assert.ok(layer.material.defines.NORMAL_OCT16); done(); }).catch(done); }); From bef3c660df7426c988ba691134d29746e325e5e3 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 26 Feb 2025 15:55:44 +0100 Subject: [PATCH 04/11] fix(PtCloudLayer): fix elevation --- src/Renderer/Shader/PointsVS.glsl | 2 +- utils/debug/PointCloudDebug.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renderer/Shader/PointsVS.glsl b/src/Renderer/Shader/PointsVS.glsl index 6dae914c36..841fef0eac 100644 --- a/src/Renderer/Shader/PointsVS.glsl +++ b/src/Renderer/Shader/PointsVS.glsl @@ -95,7 +95,7 @@ void main() { vec2 uv = vec2(i, (1. - i)); vColor = texture2D(gradientTexture, uv); } else if (mode == PNTS_MODE_ELEVATION) { - float z = (modelMatrix * vec4(position, 1.0)).z; + float z = vec4(position, 1.0).z; float i = (z - elevationRange.x) / (elevationRange.y - elevationRange.x); vec2 uv = vec2(i, (1. - i)); vColor = texture2D(gradientTexture, uv); diff --git a/utils/debug/PointCloudDebug.js b/utils/debug/PointCloudDebug.js index b2a963ad9d..ce8ba5fa34 100644 --- a/utils/debug/PointCloudDebug.js +++ b/utils/debug/PointCloudDebug.js @@ -79,7 +79,7 @@ export default { layer.debugUI.add(layer, 'sseThreshold').name('SSE threshold').onChange(update); layer.debugUI.add(layer, 'octreeDepthLimit', -1, 20).name('Depth limit').onChange(update); layer.debugUI.add(layer, 'pointBudget', 1, 15000000).name('Max point count').onChange(update); - layer.debugUI.add(layer.object3d.position, 'z', -50, 50).name('Z translation').onChange(() => { + layer.debugUI.add(layer.object3d.position, 'z', -5000, 5000).name('Z translation').onChange(() => { layer.object3d.updateMatrixWorld(); view.notifyChange(layer); }); From 9b290e30b3d65cb54fbd905c6cabfa261834f520 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 26 Feb 2025 16:42:50 +0100 Subject: [PATCH 05/11] fix(PtCloudLayer): clamp bbox to get distance to camera --- src/Layer/CopcLayer.js | 5 ++++- src/Layer/EntwinePointTileLayer.js | 5 ++++- src/Layer/PointCloudLayer.js | 18 ++++++++++++++---- src/Layer/Potree2Layer.js | 5 ++++- src/Layer/PotreeLayer.js | 5 ++++- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Layer/CopcLayer.js b/src/Layer/CopcLayer.js index e39cc39faf..5d89f1a059 100644 --- a/src/Layer/CopcLayer.js +++ b/src/Layer/CopcLayer.js @@ -46,7 +46,10 @@ class CopcLayer extends PointCloudLayer { this.scale = new THREE.Vector3(1.0, 1.0, 1.0); this.offset = new THREE.Vector3(0.0, 0.0, 0.0); - this.setElevationRange(source.header.min[2], source.header.max[2]); + this.zmin = source.header.min[2]; + this.zmax = source.header.max[2]; + + this.setElevationRange(); this.root = new CopcNode(0, 0, 0, 0, pageOffset, pageLength, this, -1); diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index 14d872f38b..d239a5032e 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -55,7 +55,10 @@ class EntwinePointTileLayer extends PointCloudLayer { this.spacing = (Math.abs(source.bounds[3] - source.bounds[0]) + Math.abs(source.bounds[4] - source.bounds[1])) / (2 * source.span); - this.setElevationRange(source.boundsConforming[2], source.boundsConforming[5]); + this.zmin = source.boundsConforming[2]; + this.zmax = source.boundsConforming[5]; + + this.setElevationRange(); this.root = new EntwinePointTileNode(0, 0, 0, 0, this, -1); diff --git a/src/Layer/PointCloudLayer.js b/src/Layer/PointCloudLayer.js index c49a2215ab..fea0ca2a2e 100644 --- a/src/Layer/PointCloudLayer.js +++ b/src/Layer/PointCloudLayer.js @@ -159,6 +159,8 @@ function changeAngleRange(layer) { * @property {number} [maxIntensityRange=1] - The maximal intensity of the * layer. Changing this value will affect the material, if it has the * corresponding uniform. The value is normalized between 0 and 1. + * @property {number} zmin - The minimal value for elevation (read from the metadata). + * @property {number} zmax - The maximal value for elevation (read from the metadata). * * @extends GeometryLayer */ @@ -317,9 +319,9 @@ class PointCloudLayer extends GeometryLayer { this.root._quaternion = rotation; } - setElevationRange(zmin, zmax) { - this.minElevationRange = this.minElevationRange ?? zmin; - this.maxElevationRange = this.maxElevationRange ?? zmax; + setElevationRange() { + this.minElevationRange = this.minElevationRange ?? this.zmin; + this.maxElevationRange = this.maxElevationRange ?? this.zmax; } preUpdate(context, changeSources) { @@ -387,7 +389,15 @@ class PointCloudLayer extends GeometryLayer { if (elt.obj) { obj = elt.obj; } else { - elt.boxHelper = createBoxHelper(elt._bbox, elt._quaternion, elt._position); + // get a clamped bbox from the full bbox + const bbox = elt._bbox.clone(); + if (bbox.min.z < layer.maxElevationRange) { + bbox.max.z = Math.min(bbox.max.z, layer.maxElevationRange); + } + if (bbox.max.z > layer.minElevationRange) { + bbox.min.z = Math.max(bbox.min.z, layer.minElevationRange); + } + elt.boxHelper = createBoxHelper(bbox, elt._quaternion, elt._position); obj = elt.boxHelper; } const bbox = obj.geometry.boundingBox; diff --git a/src/Layer/Potree2Layer.js b/src/Layer/Potree2Layer.js index cdbfcbf326..78678daea1 100644 --- a/src/Layer/Potree2Layer.js +++ b/src/Layer/Potree2Layer.js @@ -162,7 +162,10 @@ class Potree2Layer extends PointCloudLayer { } // currently the spec on potree2 only have boundingBox and no boundsConforming - this.setElevationRange(metadata.boundingBox.min[2], metadata.boundingBox.max[2]); + this.zmin = metadata.boundingBox.min[2]; + this.zmax = metadata.boundingBox.max[2]; + + this.setElevationRange(); const root = new Potree2Node(0, 0, this); root.id = 'r'; diff --git a/src/Layer/PotreeLayer.js b/src/Layer/PotreeLayer.js index 519b40b40a..372baf8fbd 100644 --- a/src/Layer/PotreeLayer.js +++ b/src/Layer/PotreeLayer.js @@ -60,7 +60,10 @@ class PotreeLayer extends PointCloudLayer { this.supportsProgressiveDisplay = (this.source.extension === 'cin'); - this.setElevationRange(cloud.tightBoundingBox.lz, cloud.tightBoundingBox.uz); + this.zmin = cloud.tightBoundingBox.lz; + this.zmax = cloud.tightBoundingBox.uz; + + this.setElevationRange(); this.root = new PotreeNode(0, 0, this); From e60462a9265f9d55f06419fcbf36ef6e8a7f1190 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Thu, 6 Feb 2025 17:53:57 +0100 Subject: [PATCH 06/11] example(COPC): add crs for COPC simple loader --- examples/copc_simple_loader.html | 37 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/examples/copc_simple_loader.html b/examples/copc_simple_loader.html index 32c9aae4cb..326bf16eb2 100644 --- a/examples/copc_simple_loader.html +++ b/examples/copc_simple_loader.html @@ -2,7 +2,7 @@ - Itowns - COPC loader + Itowns - COPC simple loader @@ -35,17 +35,20 @@ + + +
Specify the URL of a COPC file to load: + + + +
+
+
+
+ + + + + + + From 51b6ff58b972a14bd22dc37342845c52a37bddd0 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 26 Feb 2025 15:53:38 +0100 Subject: [PATCH 09/11] fix(examples): PointCloud Examples --- examples/copc_3d_loader.html | 5 +- examples/copc_simple_loader.html | 17 ++++--- examples/entwine_3d_loader.html | 7 ++- examples/entwine_simple_loader.html | 75 ++++++++++++++++------------- 4 files changed, 55 insertions(+), 49 deletions(-) diff --git a/examples/copc_3d_loader.html b/examples/copc_3d_loader.html index 7dba220d1f..27c91bbcf4 100644 --- a/examples/copc_3d_loader.html +++ b/examples/copc_3d_loader.html @@ -56,12 +56,11 @@ var layer; function onLayerReady() { - var lookAt = new itowns.THREE.Vector3(); - layer.root.bbox.getCenter(lookAt); + var lookAt = layer.root._position; var coordLookAt = new itowns.Coordinates(view.referenceCrs).setFromVector3(lookAt); var size = new itowns.THREE.Vector3(); - layer.root.bbox.getSize(size); + layer.root._bbox.getSize(size); view.controls.lookAtCoordinate({ coord: coordLookAt, diff --git a/examples/copc_simple_loader.html b/examples/copc_simple_loader.html index 326bf16eb2..def4bf07ef 100644 --- a/examples/copc_simple_loader.html +++ b/examples/copc_simple_loader.html @@ -53,18 +53,19 @@ function onLayerReady(layer) { const camera = view.camera.camera3D; - const lookAt = new itowns.THREE.Vector3(); - const size = new itowns.THREE.Vector3(); - layer.root.bbox.getSize(size); - layer.root.bbox.getCenter(lookAt); + const lookAt = layer.root._position; + lookAt.z = (layer.maxElevationRange + layer.minElevationRange) * 0.5; - camera.far = 2.0 * size.length(); + const size = new itowns.THREE.Vector3(); + layer.root._bbox.getSize(size); - controls.groundLevel = layer.root.bbox.min.z; - const position = layer.root.bbox.min.clone().add( - size.multiply({ x: 1, y: 1, z: size.x / size.z }), + controls.groundLevel = layer.minElevationRange; + var corner = new itowns.THREE.Vector3(...layer.source.header.min); + var position = corner.clone().add( + size.multiply({ x: 0, y: 0, z: (size.x / size.z) }) ); + camera.far = 2.0 * size.length(); camera.position.copy(position); camera.lookAt(lookAt); camera.updateProjectionMatrix(); diff --git a/examples/entwine_3d_loader.html b/examples/entwine_3d_loader.html index 9c6414e409..66e62697d8 100644 --- a/examples/entwine_3d_loader.html +++ b/examples/entwine_3d_loader.html @@ -57,12 +57,11 @@ var eptLayer, eptSource; function onLayerReady() { - var lookAt = new itowns.THREE.Vector3(); - eptLayer.root.bbox.getCenter(lookAt); - var coordLookAt = new itowns.Coordinates(view.referenceCrs, lookAt); + var lookAt = eptLayer.root._position; + var coordLookAt = new itowns.Coordinates(view.referenceCrs).setFromVector3(lookAt); var size = new itowns.THREE.Vector3(); - eptLayer.root.bbox.getSize(size); + eptLayer.root._bbox.getSize(size); view.controls.lookAtCoordinate({ coord: coordLookAt, diff --git a/examples/entwine_simple_loader.html b/examples/entwine_simple_loader.html index deb80d8924..cb54b031bb 100644 --- a/examples/entwine_simple_loader.html +++ b/examples/entwine_simple_loader.html @@ -31,29 +31,48 @@