From f6f172e5505c6489c9c2f597888849508f0e8c4e Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Thu, 18 Jul 2024 14:06:13 +0200 Subject: [PATCH 01/17] fix(geometryLayer): fix opacity magic number --- src/Layer/GeometryLayer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Layer/GeometryLayer.js b/src/Layer/GeometryLayer.js index eac023c517..5f47c5d43a 100644 --- a/src/Layer/GeometryLayer.js +++ b/src/Layer/GeometryLayer.js @@ -79,7 +79,7 @@ class GeometryLayer extends Layer { configurable: true, }); - this.opacity = 1.0; + this.opacity = config.opacity ?? 1.0; this.wireframe = false; this.attachedLayers = []; From 49f0069ec98818451fa2a0d5ff0c57866a3e2cc5 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:26:55 +0200 Subject: [PATCH 02/17] fix(crs): add support for non EPSG projection and fix proj4 unit 'meter' and add 'foot' --- src/Core/Geographic/Crs.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Core/Geographic/Crs.js b/src/Core/Geographic/Crs.js index b7c40cfc4f..5ba2cafdc9 100644 --- a/src/Core/Geographic/Crs.js +++ b/src/Core/Geographic/Crs.js @@ -33,6 +33,7 @@ function formatToEPSG(crs) { const UNIT = { DEGREE: 1, METER: 2, + FOOT: 3, }; function is4326(crs) { @@ -48,8 +49,10 @@ function isGeocentric(crs) { function _unitFromProj4Unit(projunit) { if (projunit === 'degrees') { return UNIT.DEGREE; - } else if (projunit === 'm') { + } else if (projunit === 'm' || projunit === 'meter') { return UNIT.METER; + } else if (projunit === 'foot') { + return UNIT.FOOT; } else { return undefined; } @@ -61,7 +64,7 @@ function toUnit(crs) { case 'EPSG:4326' : return UNIT.DEGREE; case 'EPSG:4978' : return UNIT.METER; default: { - const p = proj4.defs(formatToEPSG(crs)); + const p = proj4.defs(crs.startsWith('TMS') ? formatToEPSG(crs) : crs); if (!p) { return undefined; } @@ -129,7 +132,7 @@ export default { * Get the unit to use with the CRS. * * @param {string} crs - The CRS to get the unit from. - * @return {number} Either `UNIT.METER`, `UNIT.DEGREE` or `undefined`. + * @return {number} Either `UNIT.METER`, `UNIT.DEGREE`, `UNIT.FOOT` or `undefined`. */ toUnit, From 00f7cb9e0b322effb8b9339646f2bb2ecbefd20c Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:32:01 +0200 Subject: [PATCH 03/17] refactor(EntwinePointTile): move this.spacing from EntwinePointItleSource to EntwinePointTileLayer --- src/Layer/EntwinePointTileLayer.js | 13 +++++++++---- src/Source/EntwinePointTileSource.js | 6 ------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index 841bfb31aa..fe469b10c7 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -52,19 +52,24 @@ class EntwinePointTileLayer extends PointCloudLayer { const resolve = this.addInitializationStep(); this.whenReady = this.source.whenReady.then(() => { 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.source.boundsConforming[2]; this.maxElevationRange = this.source.boundsConforming[5]; this.extent = Extent.fromBox3(config.crs || 'EPSG:4326', this.root.bbox); + + // 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.boundsConforming[3] - this.source.boundsConforming[0]) + + Math.abs(this.source.boundsConforming[4] - this.source.boundsConforming[1])) / (2 * this.source.span); + 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 2efc3ef176..71bb8a37bf 100644 --- a/src/Source/EntwinePointTileSource.js +++ b/src/Source/EntwinePointTileSource.js @@ -54,12 +54,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.span = metadata.span; From 69302ab17473f3118fdf932d1c71e0258eac29be Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:32:01 +0200 Subject: [PATCH 04/17] refactor(EntwinePointTileLayer): proj boundsConforming in view crs --- src/Layer/EntwinePointTileLayer.js | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index fe469b10c7..8cfde0a5b5 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -2,6 +2,7 @@ import * as THREE from 'three'; import EntwinePointTileNode from 'Core/EntwinePointTileNode'; import PointCloudLayer from 'Layer/PointCloudLayer'; import Extent from 'Core/Geographic/Extent'; +import Coordinates from 'Core/Geographic/Coordinates'; const bboxMesh = new THREE.Mesh(); const box3 = new THREE.Box3(); @@ -51,21 +52,38 @@ class EntwinePointTileLayer extends PointCloudLayer { const resolve = this.addInitializationStep(); this.whenReady = this.source.whenReady.then(() => { + const crs = this.crs || 'EPSG:4326'; + if (this.crs !== config.crs) { console.warn('layer.crs is different from View.crs'); } 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); + const coord = new Coordinates(this.source.crs || config.crs, 0, 0, 0); + const coordBoundsMin = new Coordinates(crs, 0, 0, 0); + const coordBoundsMax = new Coordinates(crs, 0, 0, 0); + coord.setFromValues( + this.source.boundsConforming[0], + this.source.boundsConforming[1], + this.source.boundsConforming[2], + ); + coord.as(crs, coordBoundsMin); + coord.setFromValues( + this.source.boundsConforming[3], + this.source.boundsConforming[4], + this.source.boundsConforming[5], + ); + coord.as(crs, coordBoundsMax); + + this.root.bbox.setFromPoints([coordBoundsMin.toVector3(), coordBoundsMax.toVector3()]); this.minElevationRange = this.source.boundsConforming[2]; this.maxElevationRange = this.source.boundsConforming[5]; - this.extent = Extent.fromBox3(config.crs || 'EPSG:4326', this.root.bbox); + this.extent = Extent.fromBox3(crs, this.root.bbox); // 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.boundsConforming[3] - this.source.boundsConforming[0]) - + Math.abs(this.source.boundsConforming[4] - this.source.boundsConforming[1])) / (2 * this.source.span); + this.spacing = (Math.abs(coordBoundsMax.x - coordBoundsMin.x) + + Math.abs(coordBoundsMax.y - coordBoundsMin.y)) / (2 * this.source.span); return this.root.loadOctree().then(resolve); }); From 7e5e6fbb79c22018aba6ed5026df6b3caece9872 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 12 Jul 2024 14:54:11 +0200 Subject: [PATCH 05/17] refactor(LASLoader): reproj data during parsing and add elevation attributs --- src/Loader/LASLoader.js | 34 ++++++++++++++++++++++++++----- src/Parser/LASParser.js | 27 ++++++++++++++++++++---- src/Renderer/Shader/PointsVS.glsl | 4 ++-- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/Loader/LASLoader.js b/src/Loader/LASLoader.js index ce0ca491df..c07c36fbd3 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. @@ -49,6 +50,11 @@ class LASLoader { _parseView(view, options) { const colorDepth = options.colorDepth ?? 16; + const forward = (options.crsIn !== options.crsOut) ? + proj4(options.projDefs[options.crsIn], options.projDefs[options.crsOut]).forward : + (x => x); + const isGeocentric = options.projDefs[options.crsOut].projName === 'geocent'; + const getPosition = ['X', 'Y', 'Z'].map(view.getter); const getIntensity = view.getter('Intensity'); const getReturnNumber = view.getter('ReturnNumber'); @@ -60,6 +66,7 @@ class LASLoader { const getScanAngle = view.getter('ScanAngle'); const positions = new Float32Array(view.pointCount * 3); + const elevations = new Float32Array(view.pointCount); const intensities = new Uint16Array(view.pointCount); const returnNumbers = new Uint8Array(view.pointCount); const numberOfReturns = new Uint8Array(view.pointCount); @@ -75,17 +82,23 @@ 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)); + // For precision we use the first point to define the origin for a local referentiel. + // After projection transformation and only the integer part for simplification. + const origin = forward(getPosition.map(f => f(0))).map(val => Math.floor(val)); 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]; + elevations[i] = z; + // geocentric height to elevation + if (isGeocentric) { elevations[i] = point[2]; } intensities[i] = getIntensity(i); returnNumbers[i] = getReturnNumber(i); numberOfReturns[i] = getNumberOfReturns(i); @@ -115,6 +128,7 @@ class LASLoader { return { position: positions, + elevation: elevations, intensity: intensities, returnNumber: returnNumbers, numberOfReturns, @@ -163,7 +177,12 @@ class LASLoader { }, this._initDecoder()); const view = Las.View.create(pointData, header, eb); - const attributes = this._parseView(view, { colorDepth }); + const attributes = this._parseView(view, { + colorDepth, + crsIn: options.crsIn, + crsOut: options.crsOut, + projDefs: options.projDefs, + }); return { attributes }; } @@ -190,7 +209,12 @@ 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, { + colorDepth, + crsIn: options.crsIn, + crsOut: options.crsOut, + projDefs: options.projDefs, + }); return { header, attributes, diff --git a/src/Parser/LASParser.js b/src/Parser/LASParser.js index 0ec1b17767..a5fb4c444b 100644 --- a/src/Parser/LASParser.js +++ b/src/Parser/LASParser.js @@ -1,5 +1,6 @@ import * as THREE from 'three'; import { spawn, Thread, Transfer } from 'threads'; +import proj4 from 'proj4'; let _lazPerf; let _thread; @@ -24,6 +25,8 @@ function buildBufferGeometry(attributes) { const positionBuffer = new THREE.BufferAttribute(attributes.position, 3); geometry.setAttribute('position', positionBuffer); + const elevationBuffer = new THREE.BufferAttribute(attributes.elevation, 1); + geometry.setAttribute('elevation', elevationBuffer); const intensityBuffer = new THREE.BufferAttribute(attributes.intensity, 1); geometry.setAttribute('intensity', intensityBuffer); @@ -105,16 +108,24 @@ export default { * `THREE.BufferGeometry`. */ async parseChunk(data, options = {}) { + const crsIn = options.in?.crs || 'EPSG:3857'; + const crsOut = options.out?.crs || crsIn; + const lasLoader = await loader(); const parsedData = await lasLoader.parseChunk(Transfer(data), { pointCount: options.in.pointCount, header: options.in.header, eb: options.eb, colorDepth: options.in.colorDepth, + crsIn, + crsOut, + projDefs: { + [crsIn]: proj4.defs(crsIn), + [crsOut]: proj4.defs(crsOut), + }, }); const geometry = buildBufferGeometry(parsedData.attributes); - geometry.computeBoundingBox(); return geometry; }, @@ -128,6 +139,8 @@ export default { * @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) + * @param {String} [options.in.crs = 'EPSG:3857'] - Crs of the source if any. + * @param {String} [options.out.crs = options.in.crs] - Crs of the view if any. * * @return {Promise} A promise resolving with a `THREE.BufferGeometry`. The * header of the file is contained in `userData`. @@ -137,16 +150,22 @@ export default { console.warn("Warning: options 'skip' not supported anymore"); } - const input = options.in; + const crsIn = options.in?.crs || 'EPSG:3857'; + const crsOut = options.out?.crs || crsIn; const lasLoader = await loader(); const parsedData = await lasLoader.parseFile(Transfer(data), { - colorDepth: input?.colorDepth, + colorDepth: options.in?.colorDepth, + crsIn, + crsOut, + projDefs: { + [crsIn]: proj4.defs(crsIn), + [crsOut]: proj4.defs(crsOut), + }, }); const geometry = buildBufferGeometry(parsedData.attributes); geometry.userData.header = parsedData.header; - geometry.computeBoundingBox(); return geometry; }, }; diff --git a/src/Renderer/Shader/PointsVS.glsl b/src/Renderer/Shader/PointsVS.glsl index 6dae914c36..e5ae07159d 100644 --- a/src/Renderer/Shader/PointsVS.glsl +++ b/src/Renderer/Shader/PointsVS.glsl @@ -33,6 +33,7 @@ attribute vec4 unique_id; attribute float intensity; attribute float classification; attribute float pointSourceID; +attribute float elevation; attribute float returnNumber; attribute float numberOfReturns; @@ -95,8 +96,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 i = (z - elevationRange.x) / (elevationRange.y - elevationRange.x); + float i = (elevation - elevationRange.x) / (elevationRange.y - elevationRange.x); vec2 uv = vec2(i, (1. - i)); vColor = texture2D(gradientTexture, uv); } From 43363f1db1fb3e15d0938aa1d1ce664812ee83a7 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:18:29 +0200 Subject: [PATCH 06/17] refactor(test): entwine.js --- test/unit/entwine.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/unit/entwine.js b/test/unit/entwine.js index 7a96be6bdd..04be67783b 100644 --- a/test/unit/entwine.js +++ b/test/unit/entwine.js @@ -1,4 +1,5 @@ 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'; @@ -53,18 +54,16 @@ describe('Entwine Point Tile', function () { }).catch(done); }); - 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, @@ -92,8 +91,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, layer.root.bbox.getCenter(lookAt)); view.controls.lookAtCoordinate({ - coord: source.center, + coord, range: 250, }, false) .then(() => { @@ -110,7 +111,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' } }; From 4a256b1af7057d8e57bdab7a089a9c9c05a220d9 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:24:47 +0200 Subject: [PATCH 07/17] refactor(examples): entwine examples -> change linked to reproj --- examples/entwine_3d_loader.html | 6 ++++-- examples/entwine_simple_loader.html | 9 ++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/entwine_3d_loader.html b/examples/entwine_3d_loader.html index bc75139d36..330d34d460 100644 --- a/examples/entwine_3d_loader.html +++ b/examples/entwine_3d_loader.html @@ -71,7 +71,8 @@ } function readEPTURL() { - var url = document.getElementById('ept_url').value || new URL(location.href).searchParams.get('ept'); + const urlParams = new URL(location.href).searchParams + var url = document.getElementById('ept_url').value || urlParams.get('ept'); if (url) { loadEPT(url); @@ -98,7 +99,8 @@ itowns.View.prototype.addLayer.call(view, eptLayer).then(onLayerReady); - debug.PointCloudDebug.initTools(view, eptLayer, debugGui); + eptLayer.whenReady + .then(() => debug.PointCloudDebug.initTools(view, eptLayer, debugGui)); } readEPTURL(); diff --git a/examples/entwine_simple_loader.html b/examples/entwine_simple_loader.html index 15e436e5e2..ae2afd7945 100644 --- a/examples/entwine_simple_loader.html +++ b/examples/entwine_simple_loader.html @@ -31,12 +31,11 @@ From b2966359384330921e0a2b59fa0ecf8bd0843b5a Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 17 Jul 2024 09:58:47 +0200 Subject: [PATCH 09/17] feat(EntwineData): add obb for reprojection in View crs --- config/threeExamples.mjs | 3 +- src/Core/EntwinePointTileNode.js | 75 ++++++++----- src/Core/PointCloudNode.js | 11 +- src/Layer/EntwinePointTileLayer.js | 95 ++++++++++++---- src/Layer/PointCloudLayer.js | 160 +++++++++++++++++++++------ src/Provider/PointCloudProvider.js | 57 +++++++++- src/Renderer/Camera.js | 82 ++++++++++++++ src/Source/EntwinePointTileSource.js | 9 ++ src/Utils/OBBHelper.js | 60 ++++++++++ utils/debug/PointCloudDebug.js | 3 +- 10 files changed, 465 insertions(+), 90 deletions(-) create mode 100644 src/Utils/OBBHelper.js diff --git a/config/threeExamples.mjs b/config/threeExamples.mjs index 9c1107d8a7..a900e79731 100644 --- a/config/threeExamples.mjs +++ b/config/threeExamples.mjs @@ -9,6 +9,7 @@ export default { './utils/WorkerPool.js', './capabilities/WebGL.js', './libs/ktx-parse.module.js', - './libs/zstddec.module.js' + './libs/zstddec.module.js', + './math/OBB.js', ], }; diff --git a/src/Core/EntwinePointTileNode.js b/src/Core/EntwinePointTileNode.js index fa4efc4ccf..240d12708e 100644 --- a/src/Core/EntwinePointTileNode.js +++ b/src/Core/EntwinePointTileNode.js @@ -69,16 +69,16 @@ class EntwinePointTileNode extends PointCloudNode { this.url = `${this.layer.source.url}/ept-data/${this.id}.${this.layer.source.extension}`; } - createChildAABB(node) { + createChildAABB(childNode) { // factor to apply, based on the depth difference (can be > 1) - const f = 2 ** (node.depth - this.depth); + 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 - node.bbox.min.copy(this.bbox.min); + childNode.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 @@ -86,13 +86,29 @@ class EntwinePointTileNode extends PointCloudNode { // difference in position between the two nodes, at child depth, and // scale it using the size - translation.subVectors(node, position).multiply(size); + translation.subVectors(childNode, position).multiply(size); // apply the translation to the child node bbox - node.bbox.min.add(translation); + childNode.bbox.min.add(translation); // use the size computed above to set the max - node.bbox.max.copy(node.bbox.min).add(size); + childNode.bbox.max.copy(childNode.bbox.min).add(size); + } + + createChildOBB(childNode) { + const f = 2 ** (childNode.depth - this.depth); + + this.obb.getSize(size).divideScalar(f); + + position.copy(this).multiplyScalar(f); + + translation.subVectors(childNode, position).multiply(size); + + childNode.obb = this.obb.clone(); + childNode.obb.halfSize.divideScalar(f); + + childNode.obb.center = this.obb.center.clone().add(this.obb.halfSize.clone().multiplyScalar(-0.5)).add(translation); + childNode.obb.position = this.obb.position.clone(); } get octreeIsLoaded() { @@ -100,29 +116,30 @@ class EntwinePointTileNode extends PointCloudNode { } loadOctree() { - return Fetcher.json(`${this.layer.source.url}/ept-hierarchy/${this.id}.json`, this.layer.source.networkOptions).then((hierarchy) => { - this.numPoints = hierarchy[this.id]; - - const stack = []; - stack.push(this); - - while (stack.length) { - const node = stack.shift(); - const depth = node.depth + 1; - const x = node.x * 2; - const y = node.y * 2; - const z = node.z * 2; - - node.findAndCreateChild(depth, x, y, z, hierarchy, stack); - node.findAndCreateChild(depth, x + 1, y, z, hierarchy, stack); - node.findAndCreateChild(depth, x, y + 1, z, hierarchy, stack); - node.findAndCreateChild(depth, x + 1, y + 1, z, hierarchy, stack); - node.findAndCreateChild(depth, x, y, z + 1, hierarchy, stack); - node.findAndCreateChild(depth, x + 1, y, z + 1, hierarchy, stack); - node.findAndCreateChild(depth, x, y + 1, z + 1, hierarchy, stack); - node.findAndCreateChild(depth, x + 1, y + 1, z + 1, hierarchy, stack); - } - }); + return Fetcher.json(`${this.layer.source.url}/ept-hierarchy/${this.id}.json`, this.layer.source.networkOptions) + .then((hierarchy) => { + this.numPoints = hierarchy[this.id]; + + const stack = []; + stack.push(this); + + while (stack.length) { + const node = stack.shift(); + const depth = node.depth + 1; + const x = node.x * 2; + const y = node.y * 2; + const z = node.z * 2; + + node.findAndCreateChild(depth, x, y, z, hierarchy, stack); + node.findAndCreateChild(depth, x + 1, y, z, hierarchy, stack); + node.findAndCreateChild(depth, x, y + 1, z, hierarchy, stack); + node.findAndCreateChild(depth, x + 1, y + 1, z, hierarchy, stack); + node.findAndCreateChild(depth, x, y, z + 1, hierarchy, stack); + node.findAndCreateChild(depth, x + 1, y, z + 1, hierarchy, stack); + node.findAndCreateChild(depth, x, y + 1, z + 1, hierarchy, stack); + node.findAndCreateChild(depth, x + 1, y + 1, z + 1, hierarchy, stack); + } + }); } findAndCreateChild(depth, x, y, z, hierarchy, stack) { diff --git a/src/Core/PointCloudNode.js b/src/Core/PointCloudNode.js index 3042217602..5367ea5a80 100644 --- a/src/Core/PointCloudNode.js +++ b/src/Core/PointCloudNode.js @@ -1,4 +1,5 @@ import * as THREE from 'three'; +import { OBB } from 'ThreeExtended/math/OBB'; class PointCloudNode extends THREE.EventDispatcher { constructor(numPoints = 0, layer) { @@ -9,13 +10,15 @@ class PointCloudNode extends THREE.EventDispatcher { this.children = []; this.bbox = new THREE.Box3(); + this.obb = new OBB(); this.sse = -1; } - add(node, indexChild) { - this.children.push(node); - node.parent = this; - this.createChildAABB(node, indexChild); + add(childNode, indexChild) { + this.children.push(childNode); + childNode.parent = this; + this.createChildAABB(childNode, indexChild); + this.createChildOBB(childNode, indexChild); } load() { diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index 8cfde0a5b5..aa5b8f26d4 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -3,6 +3,7 @@ import EntwinePointTileNode from 'Core/EntwinePointTileNode'; import PointCloudLayer from 'Layer/PointCloudLayer'; import Extent from 'Core/Geographic/Extent'; import Coordinates from 'Core/Geographic/Coordinates'; +import proj4 from 'proj4'; const bboxMesh = new THREE.Mesh(); const box3 = new THREE.Box3(); @@ -56,34 +57,90 @@ class EntwinePointTileLayer extends PointCloudLayer { if (this.crs !== config.crs) { console.warn('layer.crs is different from View.crs'); } this.root = new EntwinePointTileNode(0, 0, 0, 0, this, -1); - const coord = new Coordinates(this.source.crs || config.crs, 0, 0, 0); - const coordBoundsMin = new Coordinates(crs, 0, 0, 0); - const coordBoundsMax = new Coordinates(crs, 0, 0, 0); - coord.setFromValues( - this.source.boundsConforming[0], - this.source.boundsConforming[1], - this.source.boundsConforming[2], - ); - coord.as(crs, coordBoundsMin); - coord.setFromValues( - this.source.boundsConforming[3], - this.source.boundsConforming[4], - this.source.boundsConforming[5], - ); - coord.as(crs, coordBoundsMax); - - this.root.bbox.setFromPoints([coordBoundsMin.toVector3(), coordBoundsMax.toVector3()]); + 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`); + } + } + + // for BBOX + const boundsConforming = [ + ...forward(this.source.boundsConforming.slice(0, 3)), + ...forward(this.source.boundsConforming.slice(3, 6)), + ]; + this.clamp = { + zmin: boundsConforming[2], + zmax: boundsConforming[5], + }; + this.minElevationRange = this.source.boundsConforming[2]; this.maxElevationRange = this.source.boundsConforming[5]; + const bounds = [ + ...forward(this.source.bounds.slice(0, 3)), + ...forward(this.source.bounds.slice(3, 6)), + ]; + + this.root.bbox.setFromArray(bounds); this.extent = Extent.fromBox3(crs, this.root.bbox); + const centerZ0 = this.source.boundsConforming + .slice(0, 2) + .map((val, i) => Math.floor((val + this.source.boundsConforming[i + 3]) * 0.5)); + centerZ0.push(0); + + const geometry = new THREE.BufferGeometry(); + const points = new THREE.Points(geometry); + + const matrixWorld = new THREE.Matrix4(); + const matrixWorldInverse = new THREE.Matrix4(); + + let origin = new Coordinates(this.crs); + if (this.crs === 'EPSG:4978') { + const axisZ = new THREE.Vector3(0, 0, 1); + const alignYtoEast = new THREE.Quaternion(); + const center = new Coordinates(this.source.crs, centerZ0); + origin = center.as('EPSG:4978'); + const center4326 = origin.as('EPSG:4326'); + + // align Z axe to geodesic normal. + points.quaternion.setFromUnitVectors(axisZ, origin.geodesicNormal); + // align Y axe to East + alignYtoEast.setFromAxisAngle(axisZ, THREE.MathUtils.degToRad(90 + center4326.longitude)); + points.quaternion.multiply(alignYtoEast); + } + points.updateMatrixWorld(); + + matrixWorld.copy(points.matrixWorld); + matrixWorldInverse.copy(matrixWorld).invert(); + + // proj in repere local (apply rotation) to get obb from bbox + const boundsLocal = []; + for (let i = 0; i < bounds.length; i += 3) { + const coord = new THREE.Vector3(...bounds.slice(i, i + 3)).sub(origin.toVector3()); + const coordlocal = coord.applyMatrix4(matrixWorldInverse); + boundsLocal.push(...coordlocal); + } + + const positionsArray = new Float32Array(boundsLocal); + const positionBuffer = new THREE.BufferAttribute(positionsArray, 3); + geometry.setAttribute('position', positionBuffer); + + geometry.computeBoundingBox(); + + this.root.obb.fromBox3(geometry.boundingBox); + this.root.obb.applyMatrix4(matrixWorld); + this.root.obb.position = origin.toVector3(); + // 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(coordBoundsMax.x - coordBoundsMin.x) - + Math.abs(coordBoundsMax.y - coordBoundsMin.y)) / (2 * this.source.span); + 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); return this.root.loadOctree().then(resolve); }); diff --git a/src/Layer/PointCloudLayer.js b/src/Layer/PointCloudLayer.js index a46df7f027..b4da9d1f21 100644 --- a/src/Layer/PointCloudLayer.js +++ b/src/Layer/PointCloudLayer.js @@ -2,28 +2,49 @@ import * as THREE from 'three'; import GeometryLayer from 'Layer/GeometryLayer'; import PointsMaterial, { PNTS_MODE } from 'Renderer/PointsMaterial'; import Picking from 'Core/Picking'; +import OBBHelper from 'Utils/OBBHelper'; + +const _vector = /* @__PURE__ */ new THREE.Vector3(); const point = new THREE.Vector3(); const bboxMesh = new THREE.Mesh(); const box3 = new THREE.Box3(); bboxMesh.geometry.boundingBox = box3; +function clamp(number, min, max) { + return Math.max(min, Math.min(number, max)); +} + 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(); + const newbbox = elt.bbox.clone(); + newbbox.max.z = newbbox.max.z > layer.clamp.zmax ? layer.clamp.zmax : newbbox.max.z; + newbbox.min.z = newbbox.min.z < layer.clamp.zmin ? layer.clamp.zmin : newbbox.min.z; + elt.obj.box3Helper = new THREE.Box3Helper(newbbox, 0x00ffff);// light blue + layer.bboxes.add(elt.obj.box3Helper); + elt.obj.box3Helper.updateMatrixWorld(true); + + const newtightbox = elt.tightbbox.clone(); + elt.obj.tightbox3Helper = new THREE.Box3Helper(newtightbox, 0xffff00);// jaune + layer.bboxes.add(elt.obj.tightbox3Helper); + elt.obj.tightbox3Helper.updateMatrixWorld(); +} + +function initOrientedBox(elt, layer) { + const newobb = elt.obb.clone(); + const zmin = clamp(newobb.center.z - newobb.halfSize.z, layer.minElevationRange, layer.maxElevationRange); + const zmax = clamp(newobb.center.z + newobb.halfSize.z, layer.minElevationRange, layer.maxElevationRange); + newobb.center.z = (zmin + zmax) / 2; + newobb.halfSize.z = Math.abs(zmax - zmin) / 2; + elt.obj.obbHelper = new OBBHelper(newobb, 0xff00ff);// violet + elt.obj.obbHelper.position.copy(elt.obb.position); + layer.obbes.add(elt.obj.obbHelper); + elt.obj.obbHelper.updateMatrixWorld(); + + const newtightobb = elt.tightobb.clone(); + elt.obj.tightobbHelper = new OBBHelper(newtightobb, 0x00ff00);// vert + elt.obj.tightobbHelper.position.copy(elt.tightobb.position); + layer.obbes.add(elt.obj.tightobbHelper); + elt.obj.tightobbHelper.updateMatrixWorld(); } function computeSSEPerspective(context, pointSize, spacing, elt, distance) { @@ -68,8 +89,13 @@ function markForDeletion(elt) { if (elt.obj) { elt.obj.visible = false; if (__DEBUG__) { - if (elt.obj.boxHelper) { - elt.obj.boxHelper.visible = false; + if (elt.obj.box3Helper) { + elt.obj.box3Helper.visible = false; + elt.obj.tightbox3Helper.visible = false; + } + if (elt.obj.obbHelper) { + elt.obj.obbHelper.visible = false; + elt.obj.tightobbHelper.visible = false; } } } @@ -154,7 +180,12 @@ class PointCloudLayer extends GeometryLayer { this.group = config.group || new THREE.Group(); this.object3d.add(this.group); this.bboxes = config.bboxes || new THREE.Group(); + this.bboxes.name = 'bboxes'; this.bboxes.visible = false; + this.obbes = config.obbes || new THREE.Group(); + this.obbes.name = 'obbes'; + this.obbes.visible = false; + this.object3d.add(this.obbes); this.object3d.add(this.bboxes); this.group.updateMatrixWorld(); @@ -244,9 +275,22 @@ 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); + // pick the best oriented box + let obb; + if (elt.tightobb) { + obb = elt.tightobb; + } else { + obb = elt.obb.clone(); + obb.position = elt.obb.position; + // clamp the initial OBB + const zmin = clamp(obb.center.z - obb.halfSize.z, layer.minElevationRange, layer.maxElevationRange); + const zmax = clamp(obb.center.z + obb.halfSize.z, layer.minElevationRange, layer.maxElevationRange); + obb.center.z = (zmin + zmax) / 2; + obb.halfSize.z = Math.abs(zmax - zmin) / 2; + } + + elt.visible = context.camera.isObbVisible(obb, this.object3d.matrixWorld); + if (!elt.visible) { markForDeletion(elt); return; @@ -262,16 +306,38 @@ class PointCloudLayer extends GeometryLayer { if (__DEBUG__) { if (this.bboxes.visible) { - if (!elt.obj.boxHelper) { + if (!elt.obj.box3Helper) { 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.box3Helper.visible = true; + elt.obj.box3Helper.material.color.r = 1 - elt.sse; + elt.obj.box3Helper.material.color.g = elt.sse; + + elt.obj.tightbox3Helper.visible = true; + elt.obj.tightbox3Helper.material.color.r = 1 - elt.sse; + elt.obj.tightbox3Helper.material.color.g = elt.sse; + } + if (this.obbes.visible) { + if (!elt.obj.obbHelper) { + initOrientedBox(elt, layer); + } + + elt.obj.obbHelper.visible = true; + elt.obj.obbHelper.material.color.r = 1 - elt.sse; + elt.obj.obbHelper.material.color.g = elt.sse; + + elt.obj.tightobbHelper.visible = true; + elt.obj.tightobbHelper.material.color.r = 1 - elt.sse; + elt.obj.tightobbHelper.material.color.g = elt.sse; } } } else if (!elt.promise) { - const distance = Math.max(0.001, bbox.distanceToPoint(point)); + const obbWorld = obb.clone(); + obbWorld.center = obb.center.clone().applyMatrix3(obb.rotation).add(obb.position); + const obbDistance = Math.max(0.001, obbWorld.clampPoint(point, _vector).distanceTo(point)); + + const distance = obbDistance; // Increase priority of nearest node const priority = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance) / distance; elt.promise = context.scheduler.execute({ @@ -287,8 +353,9 @@ class PointCloudLayer extends GeometryLayer { } elt.obj = pts; - // store tightbbox to avoid ping-pong (bbox = larger => visible, tight => invisible) + // store tightbbox and tightobb to avoid ping-pong (bbox = larger => visible, tight => invisible) elt.tightbbox = pts.tightbbox; + elt.tightobb = pts.tightobb; // make sure to add it here, otherwise it might never // be added nor cleaned @@ -305,9 +372,16 @@ 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; - if (elt.sse >= 1) { + const obbWorld = obb.clone(); + obbWorld.center = obb.center.clone().applyMatrix3(obb.rotation).add(obb.position); + const obbDistance = Math.max(0.001, obbWorld.clampPoint(point, _vector).distanceTo(point)); + + const distance = obbDistance; + // const sse = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance) / this.sseThreshold; + const sse = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance); + elt.sse = sse; + // if (elt.sse >= 1) { + if (elt.sse >= this.sseThreshold) { return elt.children; } else { for (const child of elt.children) { @@ -379,16 +453,31 @@ class PointCloudLayer extends GeometryLayer { obj.userData.node.obj = null; if (__DEBUG__) { - if (obj.boxHelper) { - obj.boxHelper.removeMe = true; - if (Array.isArray(obj.boxHelper.material)) { - for (const material of obj.boxHelper.material) { + if (obj.box3Helper) { + obj.box3Helper.removeMe = true; + obj.tightbox3Helper.removeMe = true; + if (Array.isArray(obj.box3Helper.material)) { + for (const material of obj.box3Helper.material) { + material.dispose(); + } + } else { + obj.box3Helper.material.dispose(); + } + obj.box3Helper.geometry.dispose(); + obj.tightbox3Helper.geometry.dispose(); + } + if (obj.obbHelper) { + obj.obbHelper.removeMe = true; + obj.tightobbHelper.removeMe = true; + if (Array.isArray(obj.obbHelper.material)) { + for (const material of obj.obbHelper.material) { material.dispose(); } } else { - obj.boxHelper.material.dispose(); + obj.obbHelper.material.dispose(); } - obj.boxHelper.geometry.dispose(); + obj.obbHelper.geometry.dispose(); + obj.tightobbHelper.geometry.dispose(); } } } @@ -396,6 +485,7 @@ class PointCloudLayer extends GeometryLayer { if (__DEBUG__) { this.bboxes.children = this.bboxes.children.filter(b => !b.removeMe); + this.obbes.children = this.obbes.children.filter(b => !b.removeMe); } } diff --git a/src/Provider/PointCloudProvider.js b/src/Provider/PointCloudProvider.js index 57e24bcbf5..cbd8606f4f 100644 --- a/src/Provider/PointCloudProvider.js +++ b/src/Provider/PointCloudProvider.js @@ -1,5 +1,7 @@ import * as THREE from 'three'; import Extent from 'Core/Geographic/Extent'; +import Coordinates from 'Core/Geographic/Coordinates'; +import { OBB } from 'ThreeExtended/math/OBB'; let nextuuid = 1; function addPickingAttribute(points) { @@ -32,17 +34,70 @@ export default { const node = command.requester; return node.load().then((geometry) => { + const origin = geometry.userData.origin || node.bbox.min; const points = new THREE.Points(geometry, layer.material); + addPickingAttribute(points); points.frustumCulled = false; points.matrixAutoUpdate = false; - points.position.copy(geometry.userData.origin || node.bbox.min); + points.position.copy(origin); points.scale.copy(layer.scale); + points.updateMatrix(); + geometry.computeBoundingBox(); points.tightbbox = geometry.boundingBox.applyMatrix4(points.matrix); points.layer = layer; + points.extent = Extent.fromBox3(command.view.referenceCrs, node.bbox); points.userData.node = node; + + // OBB + const position = geometry.attributes.position.array.slice(); + + const geometryOBB = new THREE.BufferGeometry(); + const pointsOBB = new THREE.Points(geometryOBB); + + const matrixWorld = new THREE.Matrix4(); + const matrixWorldInverse = new THREE.Matrix4(); + + if (layer.crs === 'EPSG:4978') { + const axisZ = new THREE.Vector3(0, 0, 1); + const alignYtoEast = new THREE.Quaternion(); + const center4978 = new Coordinates('EPSG:4978', origin);// center + const center4326 = center4978.as('EPSG:4326');// this.center + + // align Z axe to geodesic normal. + pointsOBB.quaternion.setFromUnitVectors(axisZ, center4978.geodesicNormal); + // align Y axe to East + alignYtoEast.setFromAxisAngle(axisZ, THREE.MathUtils.degToRad(90 + center4326.longitude)); + pointsOBB.quaternion.multiply(alignYtoEast); + } + pointsOBB.updateMatrixWorld(); + + matrixWorld.copy(pointsOBB.matrixWorld); + matrixWorldInverse.copy(matrixWorld).invert(); + + const positionBuffer = new THREE.BufferAttribute(position, 3); + geometryOBB.setAttribute('position', positionBuffer); + + const positions = pointsOBB.geometry.attributes.position; + + for (let i = 0; i < positions.count; i++) { + const coord = new THREE.Vector3(...positions.array.subarray(i * 3, i * 3 + 3)) + .applyMatrix4(matrixWorldInverse); + + positions.array[i * 3] = coord.x; + positions.array[i * 3 + 1] = coord.y; + positions.array[i * 3 + 2] = coord.z; + } + + geometryOBB.computeBoundingBox(); + const obb = new OBB().fromBox3(geometryOBB.boundingBox); + obb.applyMatrix4(pointsOBB.matrixWorld); + obb.position = origin; + + points.tightobb = obb; + return points; }); }, diff --git a/src/Renderer/Camera.js b/src/Renderer/Camera.js index 848936cf2b..080a4f0177 100644 --- a/src/Renderer/Camera.js +++ b/src/Renderer/Camera.js @@ -1,6 +1,7 @@ import * as THREE from 'three'; import Coordinates from 'Core/Geographic/Coordinates'; import DEMUtils from 'Utils/DEMUtils'; +import { OBB } from 'ThreeExtended/math/OBB'; /** * @typedef {object} Camera~CAMERA_TYPE @@ -18,12 +19,16 @@ const tmp = { frustum: new THREE.Frustum(), matrix: new THREE.Matrix4(), box3: new THREE.Box3(), + obb: new OBB(), }; +const _vector3 = new THREE.Vector3(); + const ndcBox3 = new THREE.Box3( new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1), ); +const ndcObb = new OBB().fromBox3(ndcBox3); function updatePreSse(camera, height, fov) { // sse = projected geometric error on screen plane from distance @@ -205,6 +210,10 @@ class Camera { return this.box3SizeOnScreen(box3, matrixWorld).intersectsBox(ndcBox3); } + isObbVisible(obb, matrixWorld) { + return this.obbSizeOnScreen(obb, matrixWorld).intersectsOBB(ndcObb); + } + isSphereVisible(sphere, matrixWorld) { if (this.#_viewMatrixNeedsUpdate) { // update visibility testing matrix @@ -236,6 +245,23 @@ class Camera { return tmp.box3.setFromPoints(pts); } + obbSizeOnScreen(obb, matrixWorld) { + const pts = projectObbPointsInCameraSpace(this, obb, matrixWorld); + + // All points are in front of the near plane -> box3 is invisible + if (!pts) { + tmp.obb.halfSize = _vector3; + return tmp.obb; + } + + // Project points on screen + for (let i = 0; i < 8; i++) { + pts[i].applyMatrix4(this.camera3D.projectionMatrix); + } + + return tmp.obb.fromBox3(tmp.box3.setFromPoints(pts)); + } + /** * Test for collision between camera and a geometry layer (DTM/DSM) to adjust camera position. * It could be modified later to handle an array of geometry layers. @@ -311,5 +337,61 @@ function projectBox3PointsInCameraSpace(camera, box3, matrixWorld) { return atLeastOneInFrontOfNearPlane ? points : undefined; } +function projectObbPointsInCameraSpace(camera, obb, matrixWorld) { + // Projects points in camera space + // We don't project directly on screen to avoid artifacts when projecting + // points behind the near plane. + let m = camera.camera3D.matrixWorldInverse; + if (matrixWorld) { + m = tmp.matrix.multiplyMatrices(camera.camera3D.matrixWorldInverse, matrixWorld); + } + points[0].set(obb.center.x + obb.halfSize.x, obb.center.y + obb.halfSize.y, obb.center.z + obb.halfSize.z) + .applyMatrix3(obb.rotation) + .add(obb.position) + .applyMatrix4(m); + points[1].set(obb.center.x + obb.halfSize.x, obb.center.y + obb.halfSize.y, obb.center.z - obb.halfSize.z) + .applyMatrix3(obb.rotation) + .add(obb.position) + .applyMatrix4(m); + points[2].set(obb.center.x + obb.halfSize.x, obb.center.y - obb.halfSize.y, obb.center.z + obb.halfSize.z) + .applyMatrix3(obb.rotation) + .add(obb.position) + .applyMatrix4(m); + points[3].set(obb.center.x + obb.halfSize.x, obb.center.y - obb.halfSize.y, obb.center.z - obb.halfSize.z) + .applyMatrix3(obb.rotation) + .add(obb.position) + .applyMatrix4(m); + points[4].set(obb.center.x - obb.halfSize.x, obb.center.y + obb.halfSize.y, obb.center.z + obb.halfSize.z) + .applyMatrix3(obb.rotation) + .add(obb.position) + .applyMatrix4(m); + points[5].set(obb.center.x - obb.halfSize.x, obb.center.y + obb.halfSize.y, obb.center.z - obb.halfSize.z) + .applyMatrix3(obb.rotation) + .add(obb.position) + .applyMatrix4(m); + points[6].set(obb.center.x - obb.halfSize.x, obb.center.y - obb.halfSize.y, obb.center.z + obb.halfSize.z) + .applyMatrix3(obb.rotation) + .add(obb.position) + .applyMatrix4(m); + points[7].set(obb.center.x - obb.halfSize.x, obb.center.y - obb.halfSize.y, obb.center.z - obb.halfSize.z) + .applyMatrix3(obb.rotation) + .add(obb.position) + .applyMatrix4(m); + + // In camera space objects are along the -Z axis + // So if min.z is > -near, the object is invisible + let atLeastOneInFrontOfNearPlane = false; + for (let i = 0; i < 8; i++) { + if (points[i].z <= -camera.camera3D.near) { + atLeastOneInFrontOfNearPlane = true; + } else { + // Clamp to near plane + points[i].z = -camera.camera3D.near; + } + } + + return atLeastOneInFrontOfNearPlane ? points : undefined; +} + export default Camera; diff --git a/src/Source/EntwinePointTileSource.js b/src/Source/EntwinePointTileSource.js index 71bb8a37bf..f8f6ee1795 100644 --- a/src/Source/EntwinePointTileSource.js +++ b/src/Source/EntwinePointTileSource.js @@ -49,12 +49,21 @@ class EntwinePointTileSource extends Source { proj4.defs(this.crs, metadata.srs.wkt); } + if (metadata.srs.vertical && metadata.srs.vertical !== metadata.srs.horizontal) { + console.warn('EntwinePointTileSource: Vertical coordinates system code is not yet supported.'); + } + } else if (metadata.srs && metadata.srs.wkt) { + proj4.defs('unknown', metadata.srs.wkt); + this.crs = proj4.defs('unknown').name; + proj4.defs(this.crs, proj4.defs('unknown')); + if (metadata.srs.vertical && metadata.srs.vertical !== metadata.srs.horizontal) { console.warn('EntwinePointTileSource: Vertical coordinates system code is not yet supported.'); } } this.boundsConforming = metadata.boundsConforming; + this.bounds = metadata.bounds; this.span = metadata.span; return this; diff --git a/src/Utils/OBBHelper.js b/src/Utils/OBBHelper.js new file mode 100644 index 0000000000..1d9222c3f8 --- /dev/null +++ b/src/Utils/OBBHelper.js @@ -0,0 +1,60 @@ +import { + Vector3, LineSegments, LineBasicMaterial, + BufferAttribute, Float32BufferAttribute, BufferGeometry, +} from 'three'; + + +class OBBHelper extends LineSegments { + constructor(obb, color = 0xffff00) { + const indices = new Uint16Array([0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7, 0, 2, 1, 3, 4, 6, 5, 7]); + + const positions = [1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1]; + + const geometry = new BufferGeometry(); + + geometry.setIndex(new BufferAttribute(indices, 1)); + + geometry.setAttribute('position', new Float32BufferAttribute(positions, 3)); + + super(geometry, new LineBasicMaterial({ color, toneMapped: false })); + + this.obb = obb; + + this.type = 'OBBHelper'; + } + + updateMatrixWorld(force) { + const positions = this.geometry.attributes.position.array; + + const halfSize = this.obb.halfSize; + const center = this.obb.center; + const rotation = this.obb.rotation; + const corners = []; + + for (let i = 0; i < 8; i++) { + const corner = new Vector3(); + corner.x = (i & 1) ? center.x + halfSize.x : center.x - halfSize.x; + corner.y = (i & 2) ? center.y + halfSize.y : center.y - halfSize.y; + corner.z = (i & 4) ? center.z + halfSize.z : center.z - halfSize.z; + corner.applyMatrix3(rotation); + corners.push(corner); + } + + for (let i = 0; i < corners.length; i++) { + const corner = corners[i]; + positions[i * 3] = corner.x; + positions[i * 3 + 1] = corner.y; + positions[i * 3 + 2] = corner.z; + } + + this.geometry.attributes.position.needsUpdate = true; + super.updateMatrixWorld(force); + } + + dispose() { + this.geometry.dispose(); + this.material.dispose(); + } +} + +export default OBBHelper; diff --git a/utils/debug/PointCloudDebug.js b/utils/debug/PointCloudDebug.js index 136f061a7d..6b56148dd3 100644 --- a/utils/debug/PointCloudDebug.js +++ b/utils/debug/PointCloudDebug.js @@ -63,7 +63,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', -500, 500).name('Z translation').onChange(() => { layer.object3d.updateMatrixWorld(); view.notifyChange(layer); }); @@ -156,6 +156,7 @@ export default { // UI const debugUI = layer.debugUI.addFolder('Debug'); debugUI.add(layer.bboxes, 'visible').name('Display Bounding Boxes').onChange(update); + debugUI.add(layer.obbes, 'visible').name('Display Oriented Boxes').onChange(update); debugUI.add(layer, 'dbgStickyNode').name('Sticky node name').onChange(update); debugUI.add(layer, 'dbgDisplaySticky').name('Display sticky node').onChange(update); debugUI.add(layer, 'dbgDisplayChildren').name('Display children of sticky node').onChange(update); From 8a4a33efa4f1d1681917725854ecc572a743be40 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:33:16 +0200 Subject: [PATCH 10/17] refactor: potree1&2Node, copcNode -> add node.createChildOBB() --- src/Core/CopcNode.js | 12 +++++++++++- src/Core/Potree2Node.js | 5 +++++ src/Core/PotreeNode.js | 5 +++++ src/Layer/PointCloudLayer.js | 2 +- src/Layer/PotreeLayer.js | 3 --- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Core/CopcNode.js b/src/Core/CopcNode.js index 0dd3735b2a..3c81cc4eb0 100644 --- a/src/Core/CopcNode.js +++ b/src/Core/CopcNode.js @@ -60,7 +60,7 @@ class CopcNode extends PointCloudNode { } /** - * Create an (A)xis (A)ligned (B)ounding (B)ox for the given node given + * Create an (A)xis (A)ligned (B)ounding (B)ox for the node given * `this` is its parent. * @param {CopcNode} node - The child node */ @@ -90,6 +90,16 @@ class CopcNode extends PointCloudNode { node.bbox.max.copy(node.bbox.min).add(size); } + /** + * Create an (O)riented (B)ounding (B)ox for the node given + * `this` is its parent. + * @param {CopcNode} node - The child node + */ + createChildOBB(node) { + // to improve + node.obb.fromBox3(node.bbox); + } + /** * Create a CopcNode from the provided subtree and add it as child * of the current node. diff --git a/src/Core/Potree2Node.js b/src/Core/Potree2Node.js index 21de2cb172..5cfb061896 100644 --- a/src/Core/Potree2Node.js +++ b/src/Core/Potree2Node.js @@ -100,6 +100,11 @@ class Potree2Node extends PointCloudNode { } } + createChildOBB(node) { + // to check if it's enought + node.obb.fromBox3(node.bbox); + } + get octreeIsLoaded() { return !(this.childrenBitField && this.children.length === 0); } diff --git a/src/Core/PotreeNode.js b/src/Core/PotreeNode.js index 48a9802318..81f48224f8 100644 --- a/src/Core/PotreeNode.js +++ b/src/Core/PotreeNode.js @@ -64,6 +64,11 @@ class PotreeNode extends PointCloudNode { } } + createChildOBB(node) { + // to check if it's enought + node.obb.fromBox3(node.bbox); + } + get octreeIsLoaded() { return !(this.childrenBitField && this.children.length === 0); } diff --git a/src/Layer/PointCloudLayer.js b/src/Layer/PointCloudLayer.js index b4da9d1f21..879ffb2289 100644 --- a/src/Layer/PointCloudLayer.js +++ b/src/Layer/PointCloudLayer.js @@ -281,7 +281,7 @@ class PointCloudLayer extends GeometryLayer { obb = elt.tightobb; } else { obb = elt.obb.clone(); - obb.position = elt.obb.position; + obb.position = elt.obb.position || new THREE.Vector3(); // clamp the initial OBB const zmin = clamp(obb.center.z - obb.halfSize.z, layer.minElevationRange, layer.maxElevationRange); const zmax = clamp(obb.center.z + obb.halfSize.z, layer.minElevationRange, layer.maxElevationRange); diff --git a/src/Layer/PotreeLayer.js b/src/Layer/PotreeLayer.js index a426d45e00..5f5d7d296d 100644 --- a/src/Layer/PotreeLayer.js +++ b/src/Layer/PotreeLayer.js @@ -39,9 +39,6 @@ class PotreeLayer 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); From 2fd2703126bbe9bae908e7df0be536be70f0c5fa Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 17 Jul 2024 10:07:47 +0200 Subject: [PATCH 11/17] refactor(CopcData): add obb --- src/Core/CopcNode.js | 19 +++++++--- src/Layer/CopcLayer.js | 82 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/Core/CopcNode.js b/src/Core/CopcNode.js index 3c81cc4eb0..75fef440c1 100644 --- a/src/Core/CopcNode.js +++ b/src/Core/CopcNode.js @@ -93,11 +93,22 @@ class CopcNode extends PointCloudNode { /** * Create an (O)riented (B)ounding (B)ox for the node given * `this` is its parent. - * @param {CopcNode} node - The child node + * @param {CopcNode} childNode - The child node */ - createChildOBB(node) { - // to improve - node.obb.fromBox3(node.bbox); + createChildOBB(childNode) { + const f = 2 ** (childNode.depth - this.depth); + + this.obb.getSize(size).divideScalar(f); + + position.copy(this).multiplyScalar(f); + + translation.subVectors(childNode, position).multiply(size); + + childNode.obb = this.obb.clone(); + childNode.obb.halfSize.divideScalar(f); + + childNode.obb.center = this.obb.center.clone().add(this.obb.halfSize.clone().multiplyScalar(-0.5)).add(translation); + childNode.obb.position = this.obb.position.clone(); } /** diff --git a/src/Layer/CopcLayer.js b/src/Layer/CopcLayer.js index a40ec1f3ff..a4b8e956ad 100644 --- a/src/Layer/CopcLayer.js +++ b/src/Layer/CopcLayer.js @@ -1,6 +1,8 @@ import * as THREE from 'three'; import CopcNode from 'Core/CopcNode'; import PointCloudLayer from 'Layer/PointCloudLayer'; +import Coordinates from 'Core/Geographic/Coordinates'; +import proj4 from 'proj4'; /** * @classdesc @@ -38,9 +40,30 @@ class CopcLayer extends PointCloudLayer { const { cube, rootHierarchyPage } = source.info; const { pageOffset, pageLength } = rootHierarchyPage; + // const crs = this.crs || 'EPSG:4326'; + if (this.crs !== config.crs) { console.warn('layer.crs is different from View.crs'); } + 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); + + 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`); + } + } + + // for BBOX + const boundsConforming = [ + ...forward(source.header.min), + ...forward(source.header.max), + ]; + + this.clamp = { + zmin: boundsConforming[2], + zmax: boundsConforming[5], + }; this.minElevationRange = source.header.min[2]; this.maxElevationRange = source.header.max[2]; @@ -48,6 +71,61 @@ 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); + const bounds = [ + ...forward(cube.slice(0, 3)), + ...forward(cube.slice(3, 6)), + ]; + + this.root.bbox.setFromArray(bounds); + // this.extent = Extent.fromBox3(crs, this.root.bbox); + + const centerZ0 = source.header.min.slice(0, 2) + .map((val, i) => Math.floor((val + source.header.max[i]) * 0.5)); + centerZ0.push(0); + + const geometry = new THREE.BufferGeometry(); + const points = new THREE.Points(geometry); + + const matrixWorld = new THREE.Matrix4(); + const matrixWorldInverse = new THREE.Matrix4(); + + let origin = new Coordinates(this.crs); + if (this.crs === 'EPSG:4978') { + const axisZ = new THREE.Vector3(0, 0, 1); + const alignYtoEast = new THREE.Quaternion(); + const center = new Coordinates(this.source.crs, centerZ0); + origin = center.as('EPSG:4978'); + const center4326 = origin.as('EPSG:4326'); + + // align Z axe to geodesic normal. + points.quaternion.setFromUnitVectors(axisZ, origin.geodesicNormal); + // align Y axe to East + alignYtoEast.setFromAxisAngle(axisZ, THREE.MathUtils.degToRad(90 + center4326.longitude)); + points.quaternion.multiply(alignYtoEast); + } + points.updateMatrixWorld(); + + matrixWorld.copy(points.matrixWorld); + matrixWorldInverse.copy(matrixWorld).invert(); + + // proj in repere local (apply rotation) to get obb from bbox + const boundsLocal = []; + for (let i = 0; i < bounds.length; i += 3) { + const coord = new THREE.Vector3(...bounds.slice(i, i + 3)).sub(origin.toVector3()); + const coordlocal = coord.applyMatrix4(matrixWorldInverse); + boundsLocal.push(...coordlocal); + } + + const positionsArray = new Float32Array(boundsLocal); + const positionBuffer = new THREE.BufferAttribute(positionsArray, 3); + geometry.setAttribute('position', positionBuffer); + + geometry.computeBoundingBox(); + + this.root.obb.fromBox3(geometry.boundingBox); + this.root.obb.applyMatrix4(matrixWorld); + this.root.obb.position = origin.toVector3(); + return this.root.loadOctree().then(resolve); }); } From 44ec007837f176024f48d6624612cc808b15efea Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 3 May 2024 15:25:33 +0200 Subject: [PATCH 12/17] refactor(test): obb --- src/Layer/PointCloudLayer.js | 10 +++++----- test/unit/entwine.js | 15 ++++++++++++--- test/unit/lasparser.js | 2 ++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Layer/PointCloudLayer.js b/src/Layer/PointCloudLayer.js index 879ffb2289..09709f8dd6 100644 --- a/src/Layer/PointCloudLayer.js +++ b/src/Layer/PointCloudLayer.js @@ -361,12 +361,12 @@ class PointCloudLayer extends GeometryLayer { // be added nor cleaned this.group.add(elt.obj); elt.obj.updateMatrixWorld(true); - - elt.promise = null; - }, (err) => { - if (err.isCancelledCommandException) { - elt.promise = null; + }).catch((err) => { + if (!err.isCancelledCommandException) { + return err; } + }).finally(() => { + elt.promise = null; }); } } diff --git a/test/unit/entwine.js b/test/unit/entwine.js index 04be67783b..a2df7a9bd4 100644 --- a/test/unit/entwine.js +++ b/test/unit/entwine.js @@ -6,14 +6,17 @@ 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 ept from '../data/entwine/ept.json'; import eptHierarchy from '../data/entwine/ept-hierarchy/0-0-0-0.json'; +// LASParser need o be mocked instead of calling it +LASParser.enableLazPerf('./examples/libs/laz-perf'); + const baseurl = 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/pointclouds'; const urlEpt = `${baseurl}/entwine/ept.json`; const urlEptHierarchy = `${baseurl}/entwine/ept-hierarchy/0-0-0-0.json`; @@ -85,9 +88,13 @@ describe('Entwine Point Tile', function () { assert.deepStrictEqual(element[0], layer.root); }); - it('tries to update on the root and fails', function () { + it('tries to update on the root and fails', function (done) { layer.update(context, layer, layer.root); - assert.strictEqual(layer.root.promise, undefined); + layer.root.promise + .then((res) => { + assert.ok(res instanceof Error); + done(); + }).catch(done); }); it('tries to update on the root and succeeds', function (done) { @@ -117,6 +124,8 @@ describe('Entwine Point Tile', function () { const layer = { source: { url: 'http://server.geo', extension: 'laz' } }; root = new EntwinePointTileNode(0, 0, 0, 0, layer, 4000); root.bbox.setFromArray([1000, 1000, 1000, 0, 0, 0]); + root.obb.fromBox3(root.bbox); + root.obb.position = root.obb.center; root.add(new EntwinePointTileNode(1, 0, 0, 0, layer, 3000)); root.add(new EntwinePointTileNode(1, 0, 0, 1, layer, 3000)); diff --git a/test/unit/lasparser.js b/test/unit/lasparser.js index 4ce2226eb0..9bfd6199c4 100644 --- a/test/unit/lasparser.js +++ b/test/unit/lasparser.js @@ -34,6 +34,7 @@ describe('LASParser', function () { assert.strictEqual(bufferGeometry.attributes.classification.count, header.pointCount); assert.strictEqual(bufferGeometry.attributes.color, undefined); + bufferGeometry.computeBoundingBox(); assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x + origin.x, header.min[0], epsilon)); assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y + origin.y, header.min[1], epsilon)); assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z + origin.z, header.min[2], epsilon)); @@ -53,6 +54,7 @@ describe('LASParser', function () { assert.strictEqual(bufferGeometry.attributes.classification.count, header.pointCount); assert.strictEqual(bufferGeometry.attributes.color.count, header.pointCount); + bufferGeometry.computeBoundingBox(); assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.x + origin.x, header.min[0], epsilon)); assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.y + origin.y, header.min[1], epsilon)); assert.ok(compareWithEpsilon(bufferGeometry.boundingBox.min.z + origin.z, header.min[2], epsilon)); From 96942953e062e1328b779b5c2c2ae7cceb9884e6 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 3 May 2024 15:45:56 +0200 Subject: [PATCH 13/17] refactor(PointCloud): supp instanciation of bboxMesh that is not used --- src/Layer/EntwinePointTileLayer.js | 4 ---- src/Layer/PointCloudLayer.js | 11 ++++------- src/Layer/PotreeLayer.js | 4 ---- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index aa5b8f26d4..d90f79cf97 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -5,10 +5,6 @@ import Extent from 'Core/Geographic/Extent'; import Coordinates from 'Core/Geographic/Coordinates'; import proj4 from 'proj4'; -const bboxMesh = new THREE.Mesh(); -const box3 = new THREE.Box3(); -bboxMesh.geometry.boundingBox = box3; - /** * @property {boolean} isEntwinePointTileLayer - Used to checkout whether this * layer is a EntwinePointTileLayer. Default is `true`. You should not change diff --git a/src/Layer/PointCloudLayer.js b/src/Layer/PointCloudLayer.js index 09709f8dd6..657eaffa63 100644 --- a/src/Layer/PointCloudLayer.js +++ b/src/Layer/PointCloudLayer.js @@ -6,10 +6,7 @@ import OBBHelper from 'Utils/OBBHelper'; const _vector = /* @__PURE__ */ new THREE.Vector3(); -const point = new THREE.Vector3(); -const bboxMesh = new THREE.Mesh(); -const box3 = new THREE.Box3(); -bboxMesh.geometry.boundingBox = box3; +const _point = new THREE.Vector3(); function clamp(number, min, max) { return Math.max(min, Math.min(number, max)); @@ -297,7 +294,7 @@ class PointCloudLayer extends GeometryLayer { } elt.notVisibleSince = undefined; - point.copy(context.camera.camera3D.position).sub(this.object3d.position); + _point.copy(context.camera.camera3D.position).sub(this.object3d.position); // only load geometry if this elements has points if (elt.numPoints !== 0) { @@ -335,7 +332,7 @@ class PointCloudLayer extends GeometryLayer { } else if (!elt.promise) { const obbWorld = obb.clone(); obbWorld.center = obb.center.clone().applyMatrix3(obb.rotation).add(obb.position); - const obbDistance = Math.max(0.001, obbWorld.clampPoint(point, _vector).distanceTo(point)); + const obbDistance = Math.max(0.001, obbWorld.clampPoint(_point, _vector).distanceTo(_point)); const distance = obbDistance; // Increase priority of nearest node @@ -374,7 +371,7 @@ class PointCloudLayer extends GeometryLayer { if (elt.children && elt.children.length) { const obbWorld = obb.clone(); obbWorld.center = obb.center.clone().applyMatrix3(obb.rotation).add(obb.position); - const obbDistance = Math.max(0.001, obbWorld.clampPoint(point, _vector).distanceTo(point)); + const obbDistance = Math.max(0.001, obbWorld.clampPoint(_point, _vector).distanceTo(_point)); const distance = obbDistance; // const sse = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance) / this.sseThreshold; diff --git a/src/Layer/PotreeLayer.js b/src/Layer/PotreeLayer.js index 5f5d7d296d..b476294d18 100644 --- a/src/Layer/PotreeLayer.js +++ b/src/Layer/PotreeLayer.js @@ -3,10 +3,6 @@ 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 * is a PotreeLayer. Default is `true`. You should not change this, as it is From 2a591346b2bf8cf127fdb3958db71b901f287fbb Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 17 Jul 2024 14:25:23 +0200 Subject: [PATCH 14/17] refactor(AllPointCloudLayer): supp this.extent --- src/Layer/CopcLayer.js | 2 -- src/Layer/EntwinePointTileLayer.js | 5 +---- src/Layer/Potree2Layer.js | 2 -- src/Layer/PotreeLayer.js | 2 -- src/Source/CopcSource.js | 7 ------- 5 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/Layer/CopcLayer.js b/src/Layer/CopcLayer.js index a4b8e956ad..cae02aaa9d 100644 --- a/src/Layer/CopcLayer.js +++ b/src/Layer/CopcLayer.js @@ -40,7 +40,6 @@ class CopcLayer extends PointCloudLayer { const { cube, rootHierarchyPage } = source.info; const { pageOffset, pageLength } = rootHierarchyPage; - // const crs = this.crs || 'EPSG:4326'; if (this.crs !== config.crs) { console.warn('layer.crs is different from View.crs'); } this.root = new CopcNode(0, 0, 0, 0, pageOffset, pageLength, this, -1); @@ -77,7 +76,6 @@ class CopcLayer extends PointCloudLayer { ]; this.root.bbox.setFromArray(bounds); - // this.extent = Extent.fromBox3(crs, this.root.bbox); const centerZ0 = source.header.min.slice(0, 2) .map((val, i) => Math.floor((val + source.header.max[i]) * 0.5)); diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index d90f79cf97..f06fe28244 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -1,7 +1,6 @@ import * as THREE from 'three'; import EntwinePointTileNode from 'Core/EntwinePointTileNode'; import PointCloudLayer from 'Layer/PointCloudLayer'; -import Extent from 'Core/Geographic/Extent'; import Coordinates from 'Core/Geographic/Coordinates'; import proj4 from 'proj4'; @@ -49,7 +48,6 @@ class EntwinePointTileLayer extends PointCloudLayer { const resolve = this.addInitializationStep(); this.whenReady = this.source.whenReady.then(() => { - const crs = this.crs || 'EPSG:4326'; if (this.crs !== config.crs) { console.warn('layer.crs is different from View.crs'); } this.root = new EntwinePointTileNode(0, 0, 0, 0, this, -1); @@ -72,7 +70,6 @@ class EntwinePointTileLayer extends PointCloudLayer { zmax: boundsConforming[5], }; - this.minElevationRange = this.source.boundsConforming[2]; this.maxElevationRange = this.source.boundsConforming[5]; @@ -82,8 +79,8 @@ class EntwinePointTileLayer extends PointCloudLayer { ]; this.root.bbox.setFromArray(bounds); - this.extent = Extent.fromBox3(crs, this.root.bbox); + // Get the transformation between the data coordinate syteme and the view's. const centerZ0 = this.source.boundsConforming .slice(0, 2) .map((val, i) => Math.floor((val + this.source.boundsConforming[i + 3]) * 0.5)); diff --git a/src/Layer/Potree2Layer.js b/src/Layer/Potree2Layer.js index 3006cb86c4..b54258c5f3 100644 --- a/src/Layer/Potree2Layer.js +++ b/src/Layer/Potree2Layer.js @@ -36,7 +36,6 @@ 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'; @@ -182,7 +181,6 @@ class Potree2Layer extends PointCloudLayer { this.root = root; - this.extent = Extent.fromBox3(this.source.crs || 'EPSG:4326', boundingBox); return this.root.loadOctree().then(resolve); }); } diff --git a/src/Layer/PotreeLayer.js b/src/Layer/PotreeLayer.js index b476294d18..371c01f7a6 100644 --- a/src/Layer/PotreeLayer.js +++ b/src/Layer/PotreeLayer.js @@ -1,7 +1,6 @@ import * as THREE from 'three'; import PointCloudLayer from 'Layer/PointCloudLayer'; import PotreeNode from 'Core/PotreeNode'; -import Extent from 'Core/Geographic/Extent'; /** * @property {boolean} isPotreeLayer - Used to checkout whether this layer @@ -59,7 +58,6 @@ class PotreeLayer extends PointCloudLayer { 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.extent = Extent.fromBox3(this.source.crs || 'EPSG:4326', this.root.bbox); return this.root.loadOctree().then(resolve); }); } diff --git a/src/Source/CopcSource.js b/src/Source/CopcSource.js index 306e84ac64..4fd41ace67 100644 --- a/src/Source/CopcSource.js +++ b/src/Source/CopcSource.js @@ -1,9 +1,7 @@ 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 @@ -108,11 +106,6 @@ class CopcSource extends Source { // TODO: use wkt definition in `metadata.wkt` to infer/define crs this.crs = config.crs || 'EPSG:4326'; - 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; }); } From 8649479a584e9a2195e3cb902115b98c613a4aa1 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Thu, 18 Jul 2024 14:06:50 +0200 Subject: [PATCH 15/17] refactor(CopcSource): use wkt to get source.crs --- src/Source/CopcSource.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Source/CopcSource.js b/src/Source/CopcSource.js index 4fd41ace67..23367cd0fd 100644 --- a/src/Source/CopcSource.js +++ b/src/Source/CopcSource.js @@ -1,3 +1,4 @@ +import proj4 from 'proj4'; import { Binary, Info, Las } from 'copc'; import Fetcher from 'Provider/Fetcher'; import LASParser from 'Parser/LASParser'; @@ -103,8 +104,24 @@ class CopcSource extends Source { this.header = metadata.header; this.info = metadata.info; this.eb = metadata.eb; - // TODO: use wkt definition in `metadata.wkt` to infer/define crs - this.crs = config.crs || 'EPSG:4326'; + + proj4.defs('unknown', metadata.wkt); + this.crs = proj4.defs('unknown').name; + proj4.defs(this.crs, proj4.defs('unknown')); + + if (proj4.defs('unknown').type === 'COMPD_CS') { + console.warn('CopcSource: compound coordinate system is not yet supported.'); + metadata.wkt = metadata.wkt.slice(metadata.wkt.search('PROJCS'), metadata.wkt.search(',VERT_CS')); + proj4.defs('unknown', metadata.wkt); + const projCS = proj4.defs('unknown'); + if (projCS.AUTHORITY) { + const authority = Object.keys(projCS.AUTHORITY)[0]; + this.crs = `${authority}:${projCS.AUTHORITY[authority]}`; + proj4.defs(this.crs, proj4.defs('unknown')); + } else { + this.crs = config.crs || 'EPSG:4326'; + } + } return this; }); From 81a1fb567dbff24d759f3faaa53348e774c0bcba Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Thu, 18 Jul 2024 14:07:32 +0200 Subject: [PATCH 16/17] refactor(example): add copc 3d loader --- examples/copc_3d_loader.html | 144 +++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 examples/copc_3d_loader.html diff --git a/examples/copc_3d_loader.html b/examples/copc_3d_loader.html new file mode 100644 index 0000000000..9751f75afa --- /dev/null +++ b/examples/copc_3d_loader.html @@ -0,0 +1,144 @@ + + + Itowns - Entwine 3D loader + + + + + + + + + + + +
Specify the URL of a Entwine Point Tree to load: + + + +
+
+
+
+ + + + + + + From 10a61e421e49d095ebbe13c0908282f0250429b8 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 19 Jul 2024 10:51:31 +0200 Subject: [PATCH 17/17] example(copc_simpls_loader): change to addapt view crs from layer crs --- examples/copc_simple_loader.html | 35 +++++++++++++++++--------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/examples/copc_simple_loader.html b/examples/copc_simple_loader.html index adc29f35a6..20b9ad2233 100644 --- a/examples/copc_simple_loader.html +++ b/examples/copc_simple_loader.html @@ -35,17 +35,20 @@