@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
192 lines (184 loc) • 7.77 kB
JavaScript
/*
* Copyright (c) 2015-2018, IGN France.
* Copyright (c) 2018-2026, Giro3D team.
* SPDX-License-Identifier: MIT
*/
import { Box3, MathUtils, Matrix3, Matrix4, Plane, Vector2, Vector3 } from 'three';
import { OBB } from 'three/examples/jsm/Addons.js';
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 {
_range = {
min: -1,
max: +1
};
_obb = null;
_corners = null;
_max = 0;
_min = 0;
_origin = undefined;
get extent() {
return this._extent;
}
get ellipsoid() {
return this._ellipsoid;
}
get origin() {
if (this._origin == null) {
const {
x,
y
} = this.extent.centerAsVector2();
this._origin = this.ellipsoid.toCartesian(x, y, 0, this._origin);
}
return this._origin;
}
constructor(options) {
super();
this._extent = options.extent;
this._range = options.range;
this._ellipsoid = options.ellipsoid;
}
getWorldSpaceCorners(matrix) {
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);
let index = 0;
for (let i = 0; i < xCount; i++) {
for (let j = 0; j < yCount; j++) {
const u = i * (1 / (xCount - 1));
const v = j * (1 / (yCount - 1));
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;
}
getOBB() {
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;
// 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;
}
computeLocalBox() {
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()));
}
setElevationRange(range) {
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;
}
}
getWorldSpaceBoundingSphere(target) {
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);
}
}