UNPKG

3d-tiles-renderer

Version:

https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification

531 lines (407 loc) 16.7 kB
/** @import { WGS84_ELLIPSOID } from './GeoConstants.js' */ import { Vector3, Spherical, MathUtils, Ray, Matrix4, Sphere, Euler } from 'three'; import { swapToGeoFrame, latitudeToSphericalPhi } from './GeoUtils.js'; const _spherical = /* @__PURE__ */ new Spherical(); const _norm = /* @__PURE__ */ new Vector3(); const _vec = /* @__PURE__ */ new Vector3(); const _vec2 = /* @__PURE__ */ new Vector3(); const _matrix = /* @__PURE__ */ new Matrix4(); const _matrix2 = /* @__PURE__ */ new Matrix4(); const _sphere = /* @__PURE__ */ new Sphere(); const _euler = /* @__PURE__ */ new Euler(); const _vecX = /* @__PURE__ */ new Vector3(); const _vecY = /* @__PURE__ */ new Vector3(); const _vecZ = /* @__PURE__ */ new Vector3(); const _pos = /* @__PURE__ */ new Vector3(); const _ray = /* @__PURE__ */ new Ray(); const EPSILON12 = 1e-12; const CENTER_EPS = 0.1; /** * Frame constant for the East-North-Up (ENU) coordinate frame, with X pointing east, * Y pointing north, and Z pointing up (away from the ellipsoid surface). */ export const ENU_FRAME = 0; /** * Frame constant for a camera-convention frame relative to the ENU frame, oriented with * "+Y" up and "-Z" forward (matching three.js camera conventions). */ export const CAMERA_FRAME = 1; /** * Frame constant for an object-convention frame relative to the ENU frame, oriented with * "+Y" up and "+Z" forward (matching three.js object conventions). */ export const OBJECT_FRAME = 2; /** @typedef { ENU_FRAME | CAMERA_FRAME | OBJECT_FRAME } Frames */ /** * Represents a triaxial ellipsoid defined by three semi-axis radii. Used to model planet-scale * surfaces such as the Earth (see {@link WGS84_ELLIPSOID}). All geographic coordinates use * latitude and longitude in radians. * @param {number} [x=1] Semi-axis radius along the X axis. * @param {number} [y=1] Semi-axis radius along the Y axis. * @param {number} [z=1] Semi-axis radius along the Z axis. */ export class Ellipsoid { constructor( x = 1, y = 1, z = 1 ) { /** * Optional name for this ellipsoid instance. * @type {string} */ this.name = ''; /** * Semi-axis radii of the ellipsoid. * @type {Vector3} */ this.radius = new Vector3( x, y, z ); } /** * Returns the point where the given ray intersects the ellipsoid surface, or null if no * intersection exists. Writes the result into `target`. * @param {Ray} ray * @param {Vector3} target * @returns {Vector3|null} */ intersectRay( ray, target ) { _matrix.makeScale( ...this.radius ).invert(); _sphere.center.set( 0, 0, 0 ); _sphere.radius = 1; _ray.copy( ray ).applyMatrix4( _matrix ); if ( _ray.intersectSphere( _sphere, target ) ) { _matrix.makeScale( ...this.radius ); target.applyMatrix4( _matrix ); return target; } else { return null; } } /** * Returns a Matrix4 representing the East-North-Up (ENU) frame at the given geographic * position: X points east, Y points north, Z points up. Writes the result into `target`. * @param {number} lat Latitude in radians. * @param {number} lon Longitude in radians. * @param {number} height Height above the ellipsoid surface in meters. * @param {Matrix4} target * @returns {Matrix4} */ getEastNorthUpFrame( lat, lon, height, target ) { if ( height.isMatrix4 ) { target = height; height = 0; console.warn( 'Ellipsoid: The signature for "getEastNorthUpFrame" has changed.' ); } this.getEastNorthUpAxes( lat, lon, _vecX, _vecY, _vecZ ); this.getCartographicToPosition( lat, lon, height, _pos ); return target.makeBasis( _vecX, _vecY, _vecZ ).setPosition( _pos ); } /** * Returns a Matrix4 representing the ENU frame at the given position, rotated by the given * azimuth, elevation, and roll. Equivalent to `getObjectFrame` with `ENU_FRAME`. * @param {number} lat Latitude in radians. * @param {number} lon Longitude in radians. * @param {number} height Height above the ellipsoid surface in meters. * @param {number} az Azimuth in radians, measured from true north towards east. * @param {number} el Elevation in radians, measured from the horizon upward. * @param {number} roll Roll in radians around the north axis. * @param {Matrix4} target * @returns {Matrix4} */ getOrientedEastNorthUpFrame( lat, lon, height, az, el, roll, target ) { return this.getObjectFrame( lat, lon, height, az, el, roll, target, ENU_FRAME ); } /** * Returns a Matrix4 representing a frame at the given geographic position, rotated by the * given azimuth, elevation, and roll, and adjusted to match the three.js `frame` convention. * `OBJECT_FRAME` orients with "+Y" up and "+Z" forward; `CAMERA_FRAME` orients with "+Y" up * and "-Z" forward; `ENU_FRAME` returns the raw ENU-relative rotation. * @param {number} lat Latitude in radians. * @param {number} lon Longitude in radians. * @param {number} height Height above the ellipsoid surface in meters. * @param {number} az Azimuth in radians, measured from true north towards east. * @param {number} el Elevation in radians, measured from the horizon upward. * @param {number} roll Roll in radians around the north axis. * @param {Matrix4} target * @param {Frames} [frame=OBJECT_FRAME] * @returns {Matrix4} */ getObjectFrame( lat, lon, height, az, el, roll, target, frame = OBJECT_FRAME ) { this.getEastNorthUpFrame( lat, lon, height, _matrix ); _euler.set( el, roll, - az, 'ZXY' ); target .makeRotationFromEuler( _euler ) .premultiply( _matrix ); // Add in the orientation adjustment for objects and cameras so "forward" and "up" are oriented // correctly if ( frame === CAMERA_FRAME ) { _euler.set( Math.PI / 2, 0, 0, 'XYZ' ); _matrix2.makeRotationFromEuler( _euler ); target.multiply( _matrix2 ); } else if ( frame === OBJECT_FRAME ) { _euler.set( - Math.PI / 2, 0, Math.PI, 'XYZ' ); _matrix2.makeRotationFromEuler( _euler ); target.multiply( _matrix2 ); } return target; } /** * Extracts geographic position and orientation (lat, lon, height, azimuth, elevation, roll) * from the given object/camera frame matrix. The inverse of `getObjectFrame`. Writes the * result into `target` and returns it. * @param {Matrix4} matrix * @param {Object} target * @param {Frames} [frame=OBJECT_FRAME] * @returns {{ lat: number, lon: number, height: number, azimuth: number, elevation: number, roll: number }} */ getCartographicFromObjectFrame( matrix, target, frame = OBJECT_FRAME ) { // if working with a frame that is not the ENU_FRAME then multiply in the // offset for a camera or object so "forward" and "up" are oriented correct if ( frame === CAMERA_FRAME ) { _euler.set( - Math.PI / 2, 0, 0, 'XYZ' ); _matrix2.makeRotationFromEuler( _euler ).premultiply( matrix ); } else if ( frame === OBJECT_FRAME ) { _euler.set( - Math.PI / 2, 0, Math.PI, 'XYZ' ); _matrix2.makeRotationFromEuler( _euler ).premultiply( matrix ); } else { _matrix2.copy( matrix ); } // get the cartographic position of the frame _pos.setFromMatrixPosition( _matrix2 ); this.getPositionToCartographic( _pos, target ); // get the relative rotation this.getEastNorthUpFrame( target.lat, target.lon, 0, _matrix ).invert(); _matrix2.premultiply( _matrix ); _euler.setFromRotationMatrix( _matrix2, 'ZXY' ); target.azimuth = - _euler.z; target.elevation = _euler.x; target.roll = _euler.y; return target; } /** * Fills in the east, north, and up unit vectors for the ENU frame at the given latitude and * longitude. Optionally writes the surface position into `point`. * @param {number} lat Latitude in radians. * @param {number} lon Longitude in radians. * @param {Vector3} vecEast * @param {Vector3} vecNorth * @param {Vector3} vecUp * @param {Vector3} [point] */ getEastNorthUpAxes( lat, lon, vecEast, vecNorth, vecUp, point = _pos ) { this.getCartographicToPosition( lat, lon, 0, point ); this.getCartographicToNormal( lat, lon, vecUp ); // up vecEast.set( - point.y, point.x, 0 ).normalize(); // east vecNorth.crossVectors( vecUp, vecEast ).normalize(); // north } /** * Converts geographic coordinates to a 3D Cartesian position on the ellipsoid surface * (plus the given height offset). Writes the result into `target` and returns it. * @param {number} lat Latitude in radians. * @param {number} lon Longitude in radians. * @param {number} height Height above the ellipsoid surface in meters. * @param {Vector3} target * @returns {Vector3} */ getCartographicToPosition( lat, lon, height, target ) { // From Cesium function Ellipsoid.cartographicToCartesian // https://github.com/CesiumGS/cesium/blob/665ec32e813d5d6fe906ec3e87187f6c38ed5e49/packages/engine/Source/core/renderer/Ellipsoid.js#L396 this.getCartographicToNormal( lat, lon, _norm ); const radius = this.radius; _vec.copy( _norm ); _vec.x *= radius.x ** 2; _vec.y *= radius.y ** 2; _vec.z *= radius.z ** 2; const gamma = Math.sqrt( _norm.dot( _vec ) ); _vec.divideScalar( gamma ); return target.copy( _vec ).addScaledVector( _norm, height ); } /** * Converts a 3D Cartesian position to geographic coordinates (lat, lon, height). Writes the * result into `target` and returns it. * @param {Vector3} pos * @param {Object} target * @returns {{ lat: number, lon: number, height: number }} */ getPositionToCartographic( pos, target ) { // From Cesium function Ellipsoid.cartesianToCartographic // https://github.com/CesiumGS/cesium/blob/665ec32e813d5d6fe906ec3e87187f6c38ed5e49/packages/engine/Source/core/renderer/Ellipsoid.js#L463 this.getPositionToSurfacePoint( pos, _vec ); this.getPositionToNormal( _vec, _norm ); const heightDelta = _vec2.subVectors( pos, _vec ); target.lon = Math.atan2( _norm.y, _norm.x ); target.lat = Math.asin( _norm.z ); target.height = Math.sign( heightDelta.dot( pos ) ) * heightDelta.length(); return target; } /** * Returns the surface normal of the ellipsoid at the given latitude and longitude. Writes the * result into `target` and returns it. * @param {number} lat Latitude in radians. * @param {number} lon Longitude in radians. * @param {Vector3} target * @returns {Vector3} */ getCartographicToNormal( lat, lon, target ) { _spherical.set( 1, latitudeToSphericalPhi( lat ), lon ); target.setFromSpherical( _spherical ).normalize(); // swap frame from the three.js frame to the geo coord frame swapToGeoFrame( target ); return target; } /** * Returns the surface normal of the ellipsoid at the given 3D Cartesian position. Writes the * result into `target` and returns it. * @param {Vector3} pos * @param {Vector3} target * @returns {Vector3} */ getPositionToNormal( pos, target ) { const radius = this.radius; target.copy( pos ); target.x /= radius.x ** 2; target.y /= radius.y ** 2; target.z /= radius.z ** 2; target.normalize(); return target; } /** * Projects the given 3D position onto the ellipsoid surface along the geodetic normal. * Returns null if the position is at or near the center. Writes the result into `target`. * @param {Vector3} pos * @param {Vector3} target * @returns {Vector3|null} */ getPositionToSurfacePoint( pos, target ) { // From Cesium function Ellipsoid.scaleToGeodeticSurface // https://github.com/CesiumGS/cesium/blob/d11b746e5809ac115fcff65b7b0c6bdfe81dcf1c/packages/engine/Source/core/renderer/scaleToGeodeticSurface.js#L25 const radius = this.radius; const invRadiusSqX = 1 / ( radius.x ** 2 ); const invRadiusSqY = 1 / ( radius.y ** 2 ); const invRadiusSqZ = 1 / ( radius.z ** 2 ); const x2 = pos.x * pos.x * invRadiusSqX; const y2 = pos.y * pos.y * invRadiusSqY; const z2 = pos.z * pos.z * invRadiusSqZ; // Compute the squared ellipsoid norm. const squaredNorm = x2 + y2 + z2; const ratio = Math.sqrt( 1.0 / squaredNorm ); // As an initial approximation, assume that the radial intersection is the projection point. const intersection = _vec.copy( pos ).multiplyScalar( ratio ); if ( squaredNorm < CENTER_EPS ) { return ! isFinite( ratio ) ? null : target.copy( intersection ); } // Use the gradient at the intersection point in place of the true unit normal. // The difference in magnitude will be absorbed in the multiplier. const gradient = _vec2.set( intersection.x * invRadiusSqX * 2.0, intersection.y * invRadiusSqY * 2.0, intersection.z * invRadiusSqZ * 2.0 ); // Compute the initial guess at the normal vector multiplier, lambda. let lambda = ( 1.0 - ratio ) * pos.length() / ( 0.5 * gradient.length() ); let correction = 0.0; let func, denominator; let xMultiplier, yMultiplier, zMultiplier; let xMultiplier2, yMultiplier2, zMultiplier2; let xMultiplier3, yMultiplier3, zMultiplier3; do { lambda -= correction; xMultiplier = 1.0 / ( 1.0 + lambda * invRadiusSqX ); yMultiplier = 1.0 / ( 1.0 + lambda * invRadiusSqY ); zMultiplier = 1.0 / ( 1.0 + lambda * invRadiusSqZ ); xMultiplier2 = xMultiplier * xMultiplier; yMultiplier2 = yMultiplier * yMultiplier; zMultiplier2 = zMultiplier * zMultiplier; xMultiplier3 = xMultiplier2 * xMultiplier; yMultiplier3 = yMultiplier2 * yMultiplier; zMultiplier3 = zMultiplier2 * zMultiplier; func = x2 * xMultiplier2 + y2 * yMultiplier2 + z2 * zMultiplier2 - 1.0; // "denominator" here refers to the use of this expression in the velocity and acceleration // computations in the sections to follow. denominator = x2 * xMultiplier3 * invRadiusSqX + y2 * yMultiplier3 * invRadiusSqY + z2 * zMultiplier3 * invRadiusSqZ; const derivative = - 2.0 * denominator; correction = func / derivative; } while ( Math.abs( func ) > EPSILON12 ); return target.set( pos.x * xMultiplier, pos.y * yMultiplier, pos.z * zMultiplier ); } /** * Returns the geometric distance to the horizon from the given latitude and elevation above * the ellipsoid surface. * @param {number} latitude Latitude in degrees. * @param {number} elevation Height above the ellipsoid surface in meters. * @returns {number} */ calculateHorizonDistance( latitude, elevation ) { // from https://aty.sdsu.edu/explain/atmos_refr/horizon.html // OG = sqrt ( 2 R h + h2 ) . const effectiveRadius = this.calculateEffectiveRadius( latitude ); return Math.sqrt( 2 * effectiveRadius * elevation + elevation ** 2 ); } /** * Returns the prime vertical radius of curvature (distance from the center of the ellipsoid * to the surface along the normal) at the given latitude. * @param {number} latitude Latitude in degrees. * @returns {number} */ calculateEffectiveRadius( latitude ) { // This radius represents the distance from the center of the ellipsoid to the surface along the normal at the given latitude. // from https://en.wikipedia.org/wiki/Earth_radius#Prime_vertical // N = a / sqrt(1 - e^2 * sin^2(phi)) const semiMajorAxis = this.radius.x; const semiMinorAxis = this.radius.z; const eSquared = 1 - ( semiMinorAxis ** 2 / semiMajorAxis ** 2 ); const phi = latitude * MathUtils.DEG2RAD; const sinPhiSquared = Math.sin( phi ) ** 2; const N = semiMajorAxis / Math.sqrt( 1 - eSquared * sinPhiSquared ); return N; } /** * Returns the height of the given 3D position above (or below) the ellipsoid surface. * @param {Vector3} pos * @returns {number} */ getPositionElevation( pos ) { // logic from "getPositionToCartographic" this.getPositionToSurfacePoint( pos, _vec ); const heightDelta = _vec2.subVectors( pos, _vec ); return Math.sign( heightDelta.dot( pos ) ) * heightDelta.length(); } /** * Returns an estimate of the closest point on the ellipsoid surface to the given ray. * Returns the exact surface intersection point if the ray intersects the ellipsoid. * @param {Ray} ray * @param {Vector3} target * @returns {Vector3} */ closestPointToRayEstimate( ray, target ) { if ( this.intersectRay( ray, target ) ) { return target; } else { _matrix.makeScale( ...this.radius ).invert(); _ray.copy( ray ).applyMatrix4( _matrix ); _vec.set( 0, 0, 0 ); _ray.closestPointToPoint( _vec, target ).normalize(); _matrix.makeScale( ...this.radius ); return target.applyMatrix4( _matrix ); } } /** * Copies the radius from the given ellipsoid into this one. * @param {Ellipsoid} source * @returns {this} */ copy( source ) { this.radius.copy( source.radius ); return this; } /** * Returns a new Ellipsoid with the same radius as this one. * @returns {Ellipsoid} */ clone() { return new this.constructor().copy( this ); } }