Repository: NASA-AMMOS/3DTilesRendererJS
Pull request #716 (2024-08-23 08:00:08)

Ellipsoid: Add azimuth elevation getter / setter functions
Related to #699 

**TODO**
- Test
- Consider if the function should assume "z forward" - perhaps an option specifying orientation (camera vs object)
- Update google maps example
- Make all ellipsoid arguments consistent (deg vs rad)


Patch:
diff --git a/example/googleMapsExample.js b/example/googleMapsExample.js
index ceb6829..cf84353 100644
--- a/example/googleMapsExample.js
+++ b/example/googleMapsExample.js
@@ -11,6 +11,8 @@ import {
 	PerspectiveCamera,
 	MathUtils,
 	OrthographicCamera,
+	Matrix4,
+	Euler,
 } from 'three';
 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
 import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
@@ -187,22 +189,34 @@ function updateHash() {
 	}
 
 	const camera = transition.camera;
-	const res = {};
-	const mat = tiles.group.matrixWorld.clone().invert();
-	const vec = camera.position.clone().applyMatrix4( mat );
-	WGS84_ELLIPSOID.getPositionToCartographic( vec, res );
+	const cartographicResult = {};
+	const orientationResult = {};
+	const tilesMatInv = tiles.group.matrixWorld.clone().invert();
+	const localCameraPos = camera.position.clone().applyMatrix4( tilesMatInv );
+
+	// camera offset to get to ENU orientation
+	const cameraForwardMatInv = new Matrix4().makeRotationFromEuler( new Euler( - Math.PI / 2, 0, 0 ) );
 
-	res.lat *= MathUtils.RAD2DEG;
-	res.lon *= MathUtils.RAD2DEG;
+	// oriented ENU frame in local coordinates
+	const orientedENUMat = new Matrix4().copy( camera.matrixWorld ).multiply( cameraForwardMatInv ).premultiply( tilesMatInv );
 
-	const elevation = WGS84_ELLIPSOID.getPositionElevation( vec );
+	// get the data
+	WGS84_ELLIPSOID.getPositionToCartographic( localCameraPos, cartographicResult );
+	WGS84_ELLIPSOID.getAzimuthElevationRollFromRotationFrame( cartographicResult.lat, cartographicResult.lon, orientedENUMat, orientationResult );
+
+	// convert to DEG
+	orientationResult.azimuth *= MathUtils.RAD2DEG;
+	orientationResult.elevation *= MathUtils.RAD2DEG;
+	cartographicResult.lat *= MathUtils.RAD2DEG;
+	cartographicResult.lon *= MathUtils.RAD2DEG;
+
+	// update hash
 	const params = new URLSearchParams();
-	params.set( 'lat', res.lat.toFixed( 4 ) );
-	params.set( 'lon', res.lon.toFixed( 4 ) );
-	params.set( 'el', elevation.toFixed( 4 ) );
-	params.set( 'rx', camera.rotation.x.toFixed( 2 ) );
-	params.set( 'ry', camera.rotation.y.toFixed( 2 ) );
-	params.set( 'rz', camera.rotation.z.toFixed( 2 ) );
+	params.set( 'lat', cartographicResult.lat.toFixed( 4 ) );
+	params.set( 'lon', cartographicResult.lon.toFixed( 4 ) );
+	params.set( 'height', cartographicResult.height.toFixed( 4 ) );
+	params.set( 'az', orientationResult.azimuth.toFixed( 2 ) );
+	params.set( 'el', orientationResult.elevation.toFixed( 2 ) );
 	window.history.replaceState( undefined, undefined, `#${ params }` );
 
 }
@@ -217,25 +231,44 @@ function initFromHash() {
 
 	}
 
+	// update the tiles matrix world so we can use it
+	tiles.group.updateMatrixWorld();
+
+	// get the position fields
+	const camera = transition.camera;
 	const lat = parseFloat( params.get( 'lat' ) );
 	const lon = parseFloat( params.get( 'lon' ) );
-	const el = parseFloat( params.get( 'el' ) ) || 1000;
+	const height = parseFloat( params.get( 'height' ) ) || 1000;
 
-	const camera = transition.camera;
-	WGS84_ELLIPSOID.getCartographicToPosition( lat * MathUtils.DEG2RAD, lon * MathUtils.DEG2RAD, el, camera.position );
-	tiles.group.updateMatrixWorld();
-	camera.position.applyMatrix4( tiles.group.matrixWorld );
+	if ( params.has( 'az' ) && params.has( 'el' ) ) {
+
+		// get the az el fields for rotation if present
+		const az = parseFloat( params.get( 'az' ) );
+		const el = parseFloat( params.get( 'el' ) );
+
+		// get the necessary rotation from East-North-Up frame to camera orientation
+		const cameraForwardMat = new Matrix4().makeRotationFromEuler( new Euler( Math.PI / 2, 0, 0 ) );
 
-	if ( params.has( 'rx' ) && params.has( 'ry' ) && params.has( 'rz' ) ) {
+		// extract the east-north-up frame into matrix world
+		WGS84_ELLIPSOID.getRotationFrameFromAzimuthElevationRoll(
+			lat * MathUtils.DEG2RAD, lon * MathUtils.DEG2RAD,
+			az * MathUtils.DEG2RAD, el * MathUtils.DEG2RAD, 0,
+			camera.matrixWorld,
+		);
 
-		const rx = parseFloat( params.get( 'rx' ) );
-		const ry = parseFloat( params.get( 'ry' ) );
-		const rz = parseFloat( params.get( 'rz' ) );
+		// apply the necessary camera offset and tiles transform
+		camera.matrixWorld.multiply( cameraForwardMat ).premultiply( tiles.group.matrixWorld );
+		camera.matrixWorld.decompose( camera.position, camera.quaternion, camera.scale );
 
-		camera.rotation.set( rx, ry, rz );
+		// get the height
+		WGS84_ELLIPSOID.getCartographicToPosition( lat * MathUtils.DEG2RAD, lon * MathUtils.DEG2RAD, height, camera.position );
+		camera.position.applyMatrix4( tiles.group.matrixWorld );
 
 	} else {
 
+		// default to looking down if no az el are present
+		WGS84_ELLIPSOID.getCartographicToPosition( lat * MathUtils.DEG2RAD, lon * MathUtils.DEG2RAD, height, camera.position );
+		camera.position.applyMatrix4( tiles.group.matrixWorld );
 		camera.lookAt( 0, 0, 0 );
 
 	}
diff --git a/src/base/TilesRendererBase.d.ts b/src/base/TilesRendererBase.d.ts
index 342dc7e..a11352a 100644
--- a/src/base/TilesRendererBase.d.ts
+++ b/src/base/TilesRendererBase.d.ts
@@ -21,6 +21,7 @@ export class TilesRendererBase {
 
 	constructor( url : String );
 	update() : void;
+	registerPlugin( plugin: Object ) : void;
 	traverse(
 		beforeCb : ( ( tile : Object, parent : Object, depth : Number ) => Boolean ) | null,
 		afterCb : ( ( tile : Object, parent : Object, depth : Number ) => Boolean ) | null
diff --git a/src/three/math/Ellipsoid.d.ts b/src/three/math/Ellipsoid.d.ts
index 139a06f..cf264ef 100644
--- a/src/three/math/Ellipsoid.d.ts
+++ b/src/three/math/Ellipsoid.d.ts
@@ -6,11 +6,14 @@ export class Ellipsoid {
 
 	constructor( x: Number, y: Number, z: Number );
 	getCartographicToPosition( lat: Number, lon: Number, height: Number, target: Vector3 ): Vector3;
-	getPositionToCartographic( pos: Vector3, target: Object ): Vector3;
+	getPositionToCartographic( pos: Vector3, target: Object ): { lat: Number, lon: Number, height: Number };
 	getCartographicToNormal( lat: Number, lon: Number, target: Vector3 ): Vector3;
 	getPositionToNormal( pos: Vector3, target: Vector3 ): Vector3;
 	getPositionToSurfacePoint( pos: Vector3, target: Vector3 ): Vector3;
 
+	getAzimuthElevationRollFromRotationFrame( lat: Number, lon: Number, frame: Matrix4, target: Object ): { azimuth: Number, elevation: Number, roll: Number };
+	getRotationFrameFromAzimuthElevationRoll( lat: Number, lon: Number, az: Number, el: Number, roll: Number, target: Matrix4 ): Matrix4;
+
 	getEastNorthUpFrame( lat: Number, lon: Number, target: Matrix4 ): Matrix4;
 	getEastNorthUpAxes( lat: Number, lon: Number, vecEast: Vector3, vecNorth: Vector3, vecUp: Vector3, point?: Vector3 );
 
diff --git a/src/three/math/Ellipsoid.js b/src/three/math/Ellipsoid.js
index a219324..1817bce 100644
--- a/src/three/math/Ellipsoid.js
+++ b/src/three/math/Ellipsoid.js
@@ -1,4 +1,4 @@
-import { Vector3, Spherical, MathUtils, Ray, Matrix4, Sphere } from 'three';
+import { Vector3, Spherical, MathUtils, Ray, Matrix4, Sphere, Euler } from 'three';
 import { swapToGeoFrame, latitudeToSphericalPhi } from './GeoUtils.js';
 
 const _spherical = new Spherical();
@@ -7,7 +7,9 @@ const _vec = new Vector3();
 const _vec2 = new Vector3();
 const _vec3 = new Vector3();
 const _matrix = new Matrix4();
+const _matrix2 = new Matrix4();
 const _sphere = new Sphere();
+const _euler = new Euler();
 
 const _vecX = new Vector3();
 const _vecY = new Vector3();
@@ -61,9 +63,9 @@ export class Ellipsoid {
 	getEastNorthUpAxes( lat, lon, vecEast, vecNorth, vecUp, point = _pos ) {
 
 		this.getCartographicToPosition( lat, lon, 0, point );
-		this.getCartographicToNormal( lat, lon, vecUp );	 // up
-		vecEast.set( - _pos.y, _pos.x, 0 ).normalize(); 	 // east
-		vecNorth.crossVectors( vecUp, vecEast ).normalize(); // north
+		this.getCartographicToNormal( lat, lon, vecUp );		// up
+		vecEast.set( - point.y, point.x, 0 ).normalize();		// east
+		vecNorth.crossVectors( vecUp, vecEast ).normalize();	// north
 
 	}
 
@@ -77,6 +79,36 @@ export class Ellipsoid {
 
 	}
 
+	// azimuth: measured off of true north, increasing towards "east"
+	// elevation: measured off of the horizon, increasing towards sky
+	// roll: rotation around northern axis
+	getAzimuthElevationRollFromRotationFrame( lat, lon, frame, target ) {
+
+		this.getEastNorthUpFrame( lat, lon, _matrix ).invert();
+		_matrix2.copy( frame ).premultiply( _matrix );
+		_euler.setFromRotationMatrix( _matrix2, 'ZXY' );
+
+		target.azimuth = - _euler.z;
+		target.elevation = _euler.x;
+		target.roll = _euler.y;
+		return target;
+
+	}
+
+	getRotationFrameFromAzimuthElevationRoll( lat, lon, az, el, roll, target ) {
+
+		this.getEastNorthUpFrame( lat, lon, _matrix );
+		_euler.set( el, roll, - az, 'ZXY' );
+
+		target
+			.makeRotationFromEuler( _euler )
+			.premultiply( _matrix )
+			.setPosition( 0, 0, 0 );
+
+		return target;
+
+	}
+
 	getCartographicToPosition( lat, lon, height, target ) {
 
 		// From Cesium function Ellipsoid.cartographicToCartesian


Test patch:
diff --git a/test/Ellipsoid.test.js b/test/Ellipsoid.test.js
index ec46c01..8dcffee 100644
--- a/test/Ellipsoid.test.js
+++ b/test/Ellipsoid.test.js
@@ -4,6 +4,7 @@ import { Vector3, MathUtils, Matrix4, Box3, Sphere } from 'three';
 import { EllipsoidRegion } from '../src/three/math/EllipsoidRegion.js';
 import { Ellipsoid } from '../src/three/math/Ellipsoid.js';
 import { WGS84_HEIGHT, WGS84_RADIUS } from '../src/base/constants.js';
+import { WGS84_ELLIPSOID } from '../src/three/math/GeoConstants.js';
 
 function epsCompare( a, b, EPSILON = 1e-10 ) {
 
@@ -277,3 +278,37 @@ describe( 'EllipsoidRegion', () => {
 	} );
 
 } );
+
+describe( 'Ellipsoid Azimuth Elevation', () => {
+
+	it( 'should produce the same values when retrieving data.', () => {
+
+		const mat = new Matrix4();
+		const LAT = 35.6586;
+		const LON = 139.7454;
+		const angles = 25 * MathUtils.DEG2RAD;
+		const res = {};
+		WGS84_ELLIPSOID.getRotationFrameFromAzimuthElevationRoll( LAT, LON, 0, 0, 0, mat );
+		WGS84_ELLIPSOID.getAzimuthElevationRollFromRotationFrame( LAT, LON, mat, res );
+
+		expect( res.elevation ).toBeCloseTo( 0 );
+		expect( res.azimuth ).toBeCloseTo( 0 );
+		expect( res.roll ).toBeCloseTo( 0 );
+
+		WGS84_ELLIPSOID.getRotationFrameFromAzimuthElevationRoll( LAT, LON, angles, angles, angles, mat );
+		WGS84_ELLIPSOID.getAzimuthElevationRollFromRotationFrame( LAT, LON, mat, res );
+
+		expect( res.elevation ).toBeCloseTo( angles );
+		expect( res.azimuth ).toBeCloseTo( angles );
+		expect( res.roll ).toBeCloseTo( angles );
+
+		WGS84_ELLIPSOID.getRotationFrameFromAzimuthElevationRoll( LAT, LON, - angles, - angles, - angles, mat );
+		WGS84_ELLIPSOID.getAzimuthElevationRollFromRotationFrame( LAT, LON, mat, res );
+
+		expect( res.elevation ).toBeCloseTo( - angles );
+		expect( res.azimuth ).toBeCloseTo( - angles );
+		expect( res.roll ).toBeCloseTo( - angles );
+
+	} );
+
+} );