UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

265 lines (210 loc) 9.74 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { Box3, MathUtils, Matrix3, Matrix4, Plane, type Sphere, Vector2, Vector3 } from 'three'; import { OBB } from 'three/examples/jsm/Addons.js'; import type ElevationRange from '../../core/ElevationRange'; import type Ellipsoid from '../../core/geographic/Ellipsoid'; import type Extent from '../../core/geographic/Extent'; import Coordinates from '../../core/geographic/Coordinates'; import CoordinateSystem from '../../core/geographic/CoordinateSystem'; import TileVolume from './TileVolume'; const vec3 = new Vector3(); const vec2 = new Vector2(); const coord = new Coordinates(CoordinateSystem.epsg4326, 0, 0); const matrix4 = new Matrix4(); export default class EllipsoidTileVolume extends TileVolume { private readonly _ellipsoid: Ellipsoid; private readonly _extent: Extent; private _range: ElevationRange = { min: -1, max: +1 }; private _obb: OBB | null = null; private _corners: Vector3[] | null = null; private _max = 0; private _min = 0; private _origin: Vector3 | undefined = undefined; public get extent(): Readonly<Extent> { return this._extent; } public get ellipsoid(): Readonly<Ellipsoid> { return this._ellipsoid; } public get origin(): Vector3 { if (this._origin == null) { const { x, y } = this.extent.centerAsVector2(); this._origin = this.ellipsoid.toCartesian(x, y, 0, this._origin); } return this._origin; } public constructor(options: { extent: Extent; range: ElevationRange; ellipsoid: Ellipsoid }) { super(); this._extent = options.extent; this._range = options.range; this._ellipsoid = options.ellipsoid; } public getWorldSpaceCorners(matrix?: Matrix4): Vector3[] { if (this._corners == null) { const dims = this._extent.dimensions(vec2); const xCount = MathUtils.clamp(Math.round(dims.width / 5) + 1, 2, 6); const yCount = MathUtils.clamp(Math.round(dims.height / 5) + 1, 2, 6); this._corners = new Array(xCount * yCount); const uStep = 1 / (xCount - 1); const jStep = 1 / (yCount - 1); let index = 0; for (let i = 0; i < xCount; i++) { for (let j = 0; j < yCount; j++) { const u = i * uStep; const v = j * jStep; const { latitude, longitude } = this._extent.sampleUV(u, v, coord); const p0 = this._ellipsoid.toCartesian(latitude, longitude, this._min); const p1 = this._ellipsoid.toCartesian(latitude, longitude, this._max); if (matrix) { p0.applyMatrix4(matrix); p1.applyMatrix4(matrix); } this._corners[index++] = p0; this._corners[index++] = p1; } } } return this._corners; } public override getOBB(): OBB { if (this._obb == null) { const center = this._extent.center(coord); const cartesianCenter = this.ellipsoid.toCartesian( center.latitude, center.longitude, 0, ); // To help us compute the thickness (on the local Z axis) of the box, // we create a tangent plane on the ellipsoid, then measure the distance from // the corners to this plane. const distanceFromOrigin = cartesianCenter.length(); const normal = this.ellipsoid.getNormalFromCartesian(cartesianCenter); const plane = new Plane(normal, -distanceFromOrigin); const corners = this.getWorldSpaceCorners(); let localMax = 0; let localMin = 0; for (let i = 0; i < corners.length; i++) { const element = corners[i]; const distance = plane.distanceToPoint(element); if (distance >= 0) { localMax = Math.max(localMax, distance); } else { localMin = Math.min(localMin, distance); } } const thickness = Math.abs(localMax - localMin); let horizontalDistance: number; // To compute the width and height of the box, we // measure the arc chords associated to the vertical // and horizontal sides of the extent (which looks like a curved sheet). const sw = this.extent.sampleUV(0, 0); const se = this.extent.sampleUV(1, 0); const nw = this.extent.sampleUV(0, 1); const ne = this.extent.sampleUV(1, 1); // We have to use the greatest chord length for the horizontal chord, // since tiles that touch the pole have a chord length of zero at the pole. if (center.latitude > 0) { // Northern hemisphere horizontalDistance = this._ellipsoid .toCartesian(sw.latitude, sw.longitude, 0) .distanceTo(this._ellipsoid.toCartesian(se.latitude, se.longitude, 0)); } else { // Southern hemisphere horizontalDistance = this._ellipsoid .toCartesian(nw.latitude, nw.longitude, 0) .distanceTo(this._ellipsoid.toCartesian(ne.latitude, ne.longitude, 0)); } // The vertical distance is simpler, since // it is the same on both sides of the extent. const verticalDistance = this._ellipsoid .toCartesian(sw.latitude, sw.longitude, 0) .distanceTo(this._ellipsoid.toCartesian(nw.latitude, nw.longitude, 0)); const halfSize = new Vector3( horizontalDistance / 2, verticalDistance / 2, thickness / 2, ); // The center is located on the tangent plane, but it's incorrect: // we have to move it down so that it is located at the actual // center of the computed volume. const midHeight = MathUtils.lerp(localMax, localMin, 0.5); cartesianCenter.addScaledVector(normal, midHeight); // Finally, the rotation component of the OBB is simply the ENU matrix at the center. const rotation = new Matrix3().setFromMatrix4( this.ellipsoid.getEastNorthUpMatrixFromCartesian(cartesianCenter, matrix4), ); this._obb = new OBB(cartesianCenter, halfSize, rotation); } return this._obb; } protected override computeLocalBox(): Box3 { const extent = this._extent; const min = this._range.min; const max = this._range.max; const p0 = this._ellipsoid.toCartesian(extent.maxY, extent.minX, min); const p1 = this._ellipsoid.toCartesian(extent.maxY, extent.minX, max); const p2 = this._ellipsoid.toCartesian(extent.minY, extent.minX, min); const p3 = this._ellipsoid.toCartesian(extent.minY, extent.minX, max); const p4 = this._ellipsoid.toCartesian(extent.minY, extent.maxX, min); const p5 = this._ellipsoid.toCartesian(extent.minY, extent.maxX, max); const p6 = this._ellipsoid.toCartesian(extent.maxY, extent.maxX, min); const p7 = this._ellipsoid.toCartesian(extent.maxY, extent.maxX, max); const center = extent.center(coord); const p8 = this._ellipsoid.toCartesian(center.latitude, center.longitude, min); const p9 = this._ellipsoid.toCartesian(center.latitude, center.longitude, max); const p10 = this._ellipsoid.toCartesian(extent.north, center.longitude, min); const p11 = this._ellipsoid.toCartesian(extent.south, center.longitude, max); const p12 = this._ellipsoid.toCartesian(center.latitude, extent.minX, min); const p13 = this._ellipsoid.toCartesian(center.latitude, extent.maxX, max); const worldBox = new Box3().setFromPoints([ p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, ]); return worldBox.setFromCenterAndSize( worldBox.getCenter(vec3).sub(p0), worldBox.getSize(new Vector3()), ); } public setElevationRange(range: ElevationRange): void { let { min, max } = range; if (!Number.isFinite(min) || !Number.isFinite(max)) { min = 0; max = 0; } this._range = { min, max }; if (this._min !== min || this._max !== max) { this._min = min; this._max = max; this._localBox = this.computeLocalBox(); this._corners = null; this._obb = null; } } public override getWorldSpaceBoundingSphere(target: Sphere): Sphere { const obb = this.getOBB(); // Technically not a bounding sphere because we are selecting // the smallest side for the radius, but since this is used // for LOD computation rather than culling, it's fine. // The reason we are picking the smallest radius is to avoid having // giant spheres for very elongated tiles near the poles, leading to // very incorrect LOD selection for those regions. const radius = Math.min(obb.halfSize.x, obb.halfSize.y); return target.set(obb.center, radius); } }