From 308895360453541943aa8b8f394c7aee31389a01 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:32:01 +0200 Subject: [PATCH 01/17] refactor(EntwinePointTile): move this.spacing from EntwinePointTileSource 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 0a1add49a4..25ee679183 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -58,19 +58,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 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 c7044e75cc9fcdc7510a378e4bfadc7c3115575e Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Fri, 12 Jul 2024 14:54:11 +0200 Subject: [PATCH 02/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 ad7d2967f8ddf9a86660795843543187509b8a64 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:32:01 +0200 Subject: [PATCH 03/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 25ee679183..67d9c70a15 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(); @@ -57,21 +58,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 16f54f277786b9606deaff831d16e02f8173a479 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 2 Oct 2024 14:09:16 +0200 Subject: [PATCH 04/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 | 99 +++++++++++++---- src/Layer/PointCloudLayer.js | 171 ++++++++++++++++++++++------- src/Layer/PotreeLayer.js | 4 - src/Provider/PointCloudProvider.js | 59 +++++++++- src/Renderer/Camera.js | 82 ++++++++++++++ src/Utils/OBBHelper.js | 60 ++++++++++ utils/debug/PointCloudDebug.js | 3 +- 10 files changed, 462 insertions(+), 105 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..1ef4dfe2ec 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); } load() { diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index 67d9c70a15..5f8c4ac1d0 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -3,10 +3,7 @@ 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(); -bboxMesh.geometry.boundingBox = box3; +import proj4 from 'proj4'; /** * @property {boolean} isEntwinePointTileLayer - Used to checkout whether this @@ -62,34 +59,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 fa128c8395..4aad423654 100644 --- a/src/Layer/PointCloudLayer.js +++ b/src/Layer/PointCloudLayer.js @@ -2,28 +2,46 @@ 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 point = new THREE.Vector3(); -const bboxMesh = new THREE.Mesh(); -const box3 = new THREE.Box3(); -bboxMesh.geometry.boundingBox = box3; +const _vector = /* @__PURE__ */ new THREE.Vector3(); + +const _point = new THREE.Vector3(); + +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 +86,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; } } } @@ -177,8 +200,13 @@ class PointCloudLayer extends GeometryLayer { this.group = group; this.object3d.add(this.group); - this.bboxes = bboxes || new THREE.Group(); + this.bboxes = bboxes; + 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(); @@ -289,17 +317,30 @@ 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; } 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(this.object3d.getWorldPosition(new THREE.Vector3())); + _point.applyQuaternion(this.object3d.getWorldQuaternion(new THREE.Quaternion()).invert()); // only load geometry if this elements has points if (elt.numPoints !== 0) { @@ -308,16 +349,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({ @@ -329,8 +392,9 @@ 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) + // 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 @@ -347,9 +411,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) { @@ -421,16 +492,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(); } } } @@ -438,6 +524,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/Layer/PotreeLayer.js b/src/Layer/PotreeLayer.js index 2940272602..e6c23ea31b 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 diff --git a/src/Provider/PointCloudProvider.js b/src/Provider/PointCloudProvider.js index 57e24bcbf5..dab4222994 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,72 @@ 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 geometryOBB = new THREE.BufferGeometry(); + const pointsOBB = new THREE.Points(geometryOBB); + + const matrix = new THREE.Matrix4(); + const matrixInverse = 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').setFromVector3(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.updateMatrix(); + + matrix.copy(pointsOBB.matrix); + matrixInverse.copy(matrix).invert(); + + const position = geometry.attributes.position.array.slice(); + 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[i * 3] * layer.scale.x, + positions.array[i * 3 + 1] * layer.scale.y, + positions.array[i * 3 + 2] * layer.scale.z, + ).applyMatrix4(matrixInverse); + + 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.matrix); + obb.position = origin; + + points.tightobb = obb; + return points; }); }, diff --git a/src/Renderer/Camera.js b/src/Renderer/Camera.js index 99a29b9ee4..c3e20e1699 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 @@ -207,6 +212,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 @@ -238,6 +247,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. @@ -313,5 +339,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/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 b2a963ad9d..776cbafef0 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', -500, 500).name('Z translation').onChange(() => { layer.object3d.updateMatrixWorld(); view.notifyChange(layer); }); @@ -186,6 +186,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 07c8b2dd1b8b9efa919d49b90c5ade33929e8380 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Mon, 23 Sep 2024 11:57:43 +0200 Subject: [PATCH 05/17] refactor(entwineLayer): name homogeneisation --- src/Layer/EntwinePointTileLayer.js | 54 ++++++++++++++++-------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index 5f8c4ac1d0..8d10234e6f 100644 --- a/src/Layer/EntwinePointTileLayer.js +++ b/src/Layer/EntwinePointTileLayer.js @@ -57,6 +57,13 @@ class EntwinePointTileLayer extends PointCloudLayer { 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'); } + + // 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); let forward = (x => x); @@ -68,20 +75,19 @@ class EntwinePointTileLayer extends PointCloudLayer { } } + this.minElevationRange = this.source.boundsConforming[2]; + this.maxElevationRange = this.source.boundsConforming[5]; + // for BBOX - const boundsConforming = [ + const tightBounds = [ ...forward(this.source.boundsConforming.slice(0, 3)), ...forward(this.source.boundsConforming.slice(3, 6)), ]; this.clamp = { - zmin: boundsConforming[2], - zmax: boundsConforming[5], + zmin: tightBounds[2], + zmax: tightBounds[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)), @@ -90,6 +96,8 @@ class EntwinePointTileLayer extends PointCloudLayer { this.root.bbox.setFromArray(bounds); this.extent = Extent.fromBox3(crs, this.root.bbox); + // for OBB + // 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)); @@ -98,33 +106,33 @@ class EntwinePointTileLayer extends PointCloudLayer { const geometry = new THREE.BufferGeometry(); const points = new THREE.Points(geometry); - const matrixWorld = new THREE.Matrix4(); - const matrixWorldInverse = new THREE.Matrix4(); + const matrix = new THREE.Matrix4(); + const matrixInverse = new THREE.Matrix4(); - let origin = new Coordinates(this.crs); + let origin = new Coordinates(this.source.crs, ...centerZ0); 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'); + origin = origin.as('EPSG:4978'); + const origin4326 = 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)); + alignYtoEast.setFromAxisAngle(axisZ, THREE.MathUtils.degToRad(90 + origin4326.longitude)); points.quaternion.multiply(alignYtoEast); } - points.updateMatrixWorld(); + points.updateMatrix(); - matrixWorld.copy(points.matrixWorld); - matrixWorldInverse.copy(matrixWorld).invert(); + matrix.copy(points.matrix); + matrixInverse.copy(matrix).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); + const coord = new THREE.Vector3(...bounds.slice(i, i + 3)) + .sub(origin.toVector3()); + const coordlocal = coord.applyMatrix4(matrixInverse); boundsLocal.push(...coordlocal); } @@ -135,15 +143,9 @@ class EntwinePointTileLayer extends PointCloudLayer { geometry.computeBoundingBox(); this.root.obb.fromBox3(geometry.boundingBox); - this.root.obb.applyMatrix4(matrixWorld); + this.root.obb.applyMatrix4(matrix); 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(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); }); } From fdf6c61b059d79951efde29263b68d97ad7e62c0 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:33:16 +0200 Subject: [PATCH 06/17] refactor(OtherPointCloudData): potree1 & 2 & copc -> add obbes --- src/Core/CopcNode.js | 23 ++++++++++- src/Core/Potree2Node.js | 5 +++ src/Core/PotreeNode.js | 5 +++ src/Layer/CopcLayer.js | 80 +++++++++++++++++++++++++++++++++++- src/Layer/PointCloudLayer.js | 2 +- src/Layer/PotreeLayer.js | 3 -- 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/src/Core/CopcNode.js b/src/Core/CopcNode.js index 0dd3735b2a..75fef440c1 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,27 @@ 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} childNode - The child node + */ + 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(); + } + /** * 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/CopcLayer.js b/src/Layer/CopcLayer.js index db07495b8b..271d94049e 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'; /** * A layer for [Cloud Optimised Point Cloud](https://copc.io) (COPC) datasets. @@ -42,9 +44,29 @@ class CopcLayer extends PointCloudLayer { const { cube, rootHierarchyPage } = source.info; const { pageOffset, pageLength } = rootHierarchyPage; + 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]; @@ -52,6 +74,60 @@ 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); + + 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 origin4326 = 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 + origin4326.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); }); } diff --git a/src/Layer/PointCloudLayer.js b/src/Layer/PointCloudLayer.js index 4aad423654..1fd459b099 100644 --- a/src/Layer/PointCloudLayer.js +++ b/src/Layer/PointCloudLayer.js @@ -323,7 +323,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 e6c23ea31b..1b9ed41108 100644 --- a/src/Layer/PotreeLayer.js +++ b/src/Layer/PotreeLayer.js @@ -34,9 +34,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 165f0cc8dbc38188beac99a1f207bf5c6810ce0d Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 17 Jul 2024 14:25:23 +0200 Subject: [PATCH 07/17] refactor(AllPointCloudLayer): supp this.extent --- src/Layer/EntwinePointTileLayer.js | 3 --- src/Layer/Potree2Layer.js | 2 -- src/Layer/PotreeLayer.js | 2 -- src/Source/CopcSource.js | 7 ------- 4 files changed, 14 deletions(-) diff --git a/src/Layer/EntwinePointTileLayer.js b/src/Layer/EntwinePointTileLayer.js index 8d10234e6f..832b252317 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'; @@ -55,7 +54,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'); } // NOTE: this spacing is kinda arbitrary here, we take the width and @@ -94,7 +92,6 @@ class EntwinePointTileLayer extends PointCloudLayer { ]; this.root.bbox.setFromArray(bounds); - this.extent = Extent.fromBox3(crs, this.root.bbox); // for OBB // Get the transformation between the data coordinate syteme and the view's. diff --git a/src/Layer/Potree2Layer.js b/src/Layer/Potree2Layer.js index df8e7501d0..30d0d33d30 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'; @@ -186,7 +185,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 1b9ed41108..143452e1e9 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 @@ -63,7 +62,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 9f91f27f57..56e43605c6 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 @@ -119,11 +117,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; }); } From eb535ac73a7603276c93d3a36ab74b7dcfb1f4e1 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 1 Oct 2024 10:43:51 +0200 Subject: [PATCH 08/17] fix(potree): Potree1 and Potree2 -> bbox and obb --- src/Core/Potree2Node.js | 5 ++- src/Core/PotreeNode.js | 5 ++- src/Layer/Potree2Layer.js | 36 +++++++++++---- src/Layer/PotreeLayer.js | 82 ++++++++++++++++++++++++++++++++++- src/Parser/PotreeBinParser.js | 2 - src/Parser/PotreeCinParser.js | 7 --- 6 files changed, 114 insertions(+), 23 deletions(-) diff --git a/src/Core/Potree2Node.js b/src/Core/Potree2Node.js index 5cfb061896..704dc95cd4 100644 --- a/src/Core/Potree2Node.js +++ b/src/Core/Potree2Node.js @@ -100,9 +100,10 @@ class Potree2Node extends PointCloudNode { } } - createChildOBB(node) { + createChildOBB(childNode) { // to check if it's enought - node.obb.fromBox3(node.bbox); + childNode.obb.fromBox3(childNode.bbox); + childNode.obb.position = new THREE.Vector3(); } get octreeIsLoaded() { diff --git a/src/Core/PotreeNode.js b/src/Core/PotreeNode.js index 81f48224f8..cc3e6a8b03 100644 --- a/src/Core/PotreeNode.js +++ b/src/Core/PotreeNode.js @@ -64,9 +64,10 @@ class PotreeNode extends PointCloudNode { } } - createChildOBB(node) { + createChildOBB(childNode) { // to check if it's enought - node.obb.fromBox3(node.bbox); + childNode.obb.fromBox3(childNode.bbox); + childNode.obb.position = new THREE.Vector3(); } get octreeIsLoaded() { diff --git a/src/Layer/Potree2Layer.js b/src/Layer/Potree2Layer.js index 30d0d33d30..00b5572605 100644 --- a/src/Layer/Potree2Layer.js +++ b/src/Layer/Potree2Layer.js @@ -36,6 +36,7 @@ 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 proj4 from 'proj4'; import { PointAttribute, Potree2PointAttributes, PointAttributeTypes } from 'Core/Potree2PointAttributes'; @@ -166,15 +167,7 @@ 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); - const root = new Potree2Node(0, 0, this); - - root.bbox = boundingBox; - root.boundingSphere = boundingBox.getBoundingSphere(new THREE.Sphere()); - root.id = 'r'; root.depth = 0; root.nodeType = 2; @@ -183,6 +176,33 @@ class Potree2Layer extends PointCloudLayer { root.byteOffset = 0; + 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`); + } + } + + this.minElevationRange = metadata.boundingBox.min[2]; + this.maxElevationRange = metadata.boundingBox.max[2]; + + // for BBOX + this.clamp = { + zmin: forward(metadata.boundingBox.min)[2], + zmax: forward(metadata.boundingBox.max)[2], + }; + + const min = new THREE.Vector3(...metadata.boundingBox.min); + const max = new THREE.Vector3(...metadata.boundingBox.max); + root.bbox = new THREE.Box3(min, max); + + // for OBB + root.obb.fromBox3(root.bbox); + root.obb.position = new THREE.Vector3(); + + this.root = root; return this.root.loadOctree().then(resolve); diff --git a/src/Layer/PotreeLayer.js b/src/Layer/PotreeLayer.js index 143452e1e9..68551d01ca 100644 --- a/src/Layer/PotreeLayer.js +++ b/src/Layer/PotreeLayer.js @@ -1,6 +1,8 @@ import * as THREE from 'three'; import PointCloudLayer from 'Layer/PointCloudLayer'; import PotreeNode from 'Core/PotreeNode'; +import Coordinates from 'Core/Geographic/Coordinates'; +import proj4 from 'proj4'; /** * @property {boolean} isPotreeLayer - Used to checkout whether this layer @@ -59,8 +61,84 @@ class PotreeLayer extends PointCloudLayer { this.supportsProgressiveDisplay = (this.source.extension === 'cin'); 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); + + 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`); + } + } + + this.minElevationRange = cloud.tightBoundingBox.lz; + this.maxElevationRange = cloud.tightBoundingBox.uz; + + // for BBOX + const tightBounds = [ + ...forward([cloud.tightBoundingBox.lx, cloud.tightBoundingBox.ly, cloud.tightBoundingBox.lz]), + ...forward([cloud.tightBoundingBox.ux, cloud.tightBoundingBox.uy, cloud.tightBoundingBox.uz]), + ]; + this.clamp = { + zmin: tightBounds[2], + zmax: tightBounds[5], + }; + + const bounds = [ + ...forward([cloud.boundingBox.lx, cloud.boundingBox.ly, cloud.boundingBox.lz]), + ...forward([cloud.boundingBox.ux, cloud.boundingBox.uy, cloud.boundingBox.uz]), + ]; + + this.root.bbox.setFromArray(bounds); + + // for OBB + const centerZ0 = [ + (cloud.tightBoundingBox.lx + cloud.tightBoundingBox.ux) * 0.5, + (cloud.tightBoundingBox.ly + cloud.tightBoundingBox.uy) * 0.5, + 0, + ]; + + const geometry = new THREE.BufferGeometry(); + const points = new THREE.Points(geometry); + + const matrix = new THREE.Matrix4(); + const matrixInverse = new THREE.Matrix4(); + + let origin = new Coordinates(this.source.crs, ...centerZ0); + if (this.crs === 'EPSG:4978') { + const axisZ = new THREE.Vector3(0, 0, 1); + const alignYtoEast = new THREE.Quaternion(); + origin = origin.as('EPSG:4978'); + const origin4326 = 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 + origin4326.longitude)); + points.quaternion.multiply(alignYtoEast); + } + points.updateMatrix(); + + matrix.copy(points.matrix); + matrixInverse.copy(matrix).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(matrixInverse); + 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(matrix); + this.root.obb.position = origin.toVector3(); return this.root.loadOctree().then(resolve); }); diff --git a/src/Parser/PotreeBinParser.js b/src/Parser/PotreeBinParser.js index 996ab8e4eb..3cecf347ed 100644 --- a/src/Parser/PotreeBinParser.js +++ b/src/Parser/PotreeBinParser.js @@ -103,8 +103,6 @@ export default { geometry.setAttribute(attr.attributeName, new THREE.BufferAttribute(array, attr.numElements, attr.normalized)); } - geometry.computeBoundingBox(); - return Promise.resolve(geometry); }, }; diff --git a/src/Parser/PotreeCinParser.js b/src/Parser/PotreeCinParser.js index 169a658da1..21a4e5bc95 100644 --- a/src/Parser/PotreeCinParser.js +++ b/src/Parser/PotreeCinParser.js @@ -13,12 +13,6 @@ export default { throw new Error('No array buffer provided.'); } - // Format: MinX,MinY,MinZ,MaxX,MaxY,MaxZ,X1,Y1,Z1,[...],XN,YN,ZN,R1,G1,B1,A1,[...],RN,GN,BN,AN - const view = new DataView(buffer, 0, 6 * 4); - const min = new THREE.Vector3(view.getFloat32(0, true), view.getFloat32(4, true), view.getFloat32(8, true)); - const max = new THREE.Vector3(view.getFloat32(12, true), view.getFloat32(16, true), view.getFloat32(20, true)); - const box = new THREE.Box3(min, max); - const numPoints = Math.floor((buffer.byteLength - 24) / 16); const positions = new Float32Array(buffer, 24, 3 * numPoints); @@ -27,7 +21,6 @@ export default { const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4, true)); - geometry.boundingBox = box; return Promise.resolve(geometry); }, From edce7e55954fcee45ec6c70a3bd43387003e2ea9 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 24 Sep 2024 15:35:51 +0200 Subject: [PATCH 09/17] fix(PotreeSource): add error message whenno crs --- src/Source/PotreeSource.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Source/PotreeSource.js b/src/Source/PotreeSource.js index 0796f8484a..c72ec6c557 100644 --- a/src/Source/PotreeSource.js +++ b/src/Source/PotreeSource.js @@ -63,6 +63,9 @@ class PotreeSource extends Source { if (!source.file) { throw new Error('New PotreeSource: file is required'); } + if (!source.crs) { + throw new Error('New PotreeSource: crs is required'); + } super(source); this.file = source.file; From f01c577b4c00a0b15cbb3678fde01f279d6a24d2 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Tue, 30 Apr 2024 17:24:47 +0200 Subject: [PATCH 10/17] examples(entwine): change linked to reproj and add GrandLyon on entwine_3d_loader --- examples/entwine_3d_loader.html | 15 +++++++++++---- examples/entwine_simple_loader.html | 7 +++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/entwine_3d_loader.html b/examples/entwine_3d_loader.html index 923dca6675..9c6414e409 100644 --- a/examples/entwine_3d_loader.html +++ b/examples/entwine_3d_loader.html @@ -20,8 +20,8 @@
Specify the URL of a Entwine Point Tree to load: -

If your dataset is not displaying at the right location, - check that it has been converted in EPSG:4978.

+
@@ -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); @@ -100,7 +101,13 @@ itowns.View.prototype.addLayer.call(view, eptLayer).then(onLayerReady); - debug.PointCloudDebug.initTools(view, eptLayer, debugGui); + eptLayer.whenReady + .then(() => debug.PointCloudDebug.initTools(view, eptLayer, debugGui)); + } + + function loadGrandLyon() { + document.getElementById('ept_url').value = 'https://download.data.grandlyon.com/files/grandlyon/imagerie/mnt2018/lidar/ept/'; + readEPTURL(); } readEPTURL(); diff --git a/examples/entwine_simple_loader.html b/examples/entwine_simple_loader.html index deb80d8924..ae2afd7945 100644 --- a/examples/entwine_simple_loader.html +++ b/examples/entwine_simple_loader.html @@ -31,12 +31,11 @@ + + +
Specify the URL of a COPC file to load: + + + +
+
+
+
+ + + + + + + diff --git a/examples/copc_simple_loader.html b/examples/copc_simple_loader.html index 7a9afb6eaf..fea4c770a1 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 @@