From 52b0639803aa1d243779a2d43fc2f7ab48f1ea91 Mon Sep 17 00:00:00 2001 From: ftoromanoff Date: Wed, 2 Oct 2024 14:09:16 +0200 Subject: [PATCH] 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 | 169 ++++++++++++++++++++++------- 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, 461 insertions(+), 104 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 9e0c9cb5ae..e0209ccd9f 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 @@ -55,34 +52,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 94cef36161..4decc613f3 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; } } } @@ -153,7 +176,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(); @@ -243,17 +271,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) { @@ -262,16 +303,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({ @@ -283,8 +346,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 @@ -301,9 +365,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) { @@ -375,16 +446,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(); } } } @@ -392,6 +478,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 4dcee04670..67c65b2bb3 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..b61ab079e3 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', 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 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/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);