Visualizing ECEF and ENU coordinate systems with city markers
WGS84, ECEF, and ENU are coordinate systems familiar to GIS professionals, but since GIS software handles the internal processing, there are few opportunities to implement the internals yourself. In this chapter, we will build the coordinate transformations that are typically used as a black box, step by step.
As a 3D engine-specific consideration, there is the axis mapping between Three.js's Y-up convention and ECEF's Z-up (North Pole direction). By clicking on city markers, you can inspect the values in each coordinate system and experience how the transformation pipeline works.
X-axis (red): On the equatorial plane, pointing toward longitude 0° (Greenwich)
Y-axis (green): On the equatorial plane, pointing toward longitude 90°E
Z-axis (blue): Pointing toward the North Pole
Tokyo / London / New York / Sydney / Sao Paulo
E (East) / N (North) / U (Up) — A local coordinate system tangent to the surface
The Earth is not a perfect sphere. Due to centrifugal force from its rotation, it bulges slightly at the equator, forming a shape close to an oblate spheroid. The mathematical model of this shape is called a reference ellipsoid.
WGS84 (World Geodetic System 1984) defines the standard reference ellipsoid that best approximates the Earth's shape. When working with latitude and longitude in GIS, you are effectively working with coordinates on the WGS84 ellipsoid.
The WGS84 ellipsoid is defined by two fundamental parameters.
| Parameter | Symbol | Value | Meaning |
|---|---|---|---|
| Semi-major axis | a | 6,378,137.0 m | Earth's radius at the equator |
| Flattening | f | 1/298.257223563 | Degree of ellipsoidal compression |
| Semi-minor axis | b | a(1-f) = 6,356,752.3 m | Earth's radius at the poles |
| First eccentricity squared | e² | 2f - f² = 0.00669 | Represents the eccentricity of the ellipse |
The difference between the equatorial and polar radii is about 21 km. While this is only about 0.3% of the Earth's diameter (approx. 12,756 km), it cannot be ignored in precise coordinate transformations.
export const WGS84 = {
/** Semi-major axis (equatorial radius) [m] */
a: 6_378_137.0,
/** Flattening */
f: 1 / 298.257_223_563,
/** Semi-minor axis (polar radius) [m] */
get b(): number {
return this.a * (1 - this.f);
},
/** First eccentricity squared */
get e2(): number {
return 2 * this.f - this.f * this.f;
}
} as const;The reason e² is derived directly from f rather than using a and b is to maintain floating-point precision.
In the demo on the left, the Y-axis direction is scaled by WGS84.b / WGS84.a to
reproduce this oblate shape. The transparency is reduced so that the internal structure is visible.
ECEF (Earth-Centered, Earth-Fixed) is a Cartesian coordinate system with its origin at the center of the Earth, rotating together with the Earth. Position information received from GPS satellites is internally processed in ECEF.
Z (toward North Pole)
|
|
o------> X (intersection of lon 0° and equator)
/
Y (intersection of lon 90°E and equator)
The conversion from latitude, longitude, and altitude to ECEF uses the ellipsoid's prime vertical radius of curvature (N). N is the "east-west radius of curvature" at a given latitude on the ellipsoid surface, which is the distance from a point on the ellipsoid surface along the normal direction to the Z-axis (rotation axis).
N(lat) = a / sqrt(1 - e² * sin²(lat))
X = (N + h) * cos(lat) * cos(lon)
Y = (N + h) * cos(lat) * sin(lon)
Z = (N * (1 - e²) + h) * sin(lat)
Note the formula for the Z component. While the X and Y components use (N + h), the Z component has (1 - e²) multiplied. This is due to the oblateness of the ellipsoid. If e² = 0 (a perfect sphere), then (1-e²) = 1, and this difference vanishes.
export function geodeticToEcef(
lat: number, lon: number, alt: number
): EcefCoord {
const phi = degToRad(lat);
const lam = degToRad(lon);
const sinPhi = Math.sin(phi);
const cosPhi = Math.cos(phi);
const sinLam = Math.sin(lam);
const cosLam = Math.cos(lam);
const N = WGS84.a / Math.sqrt(
1 - WGS84.e2 * sinPhi * sinPhi
);
return {
x: (N + alt) * cosPhi * cosLam,
y: (N + alt) * cosPhi * sinLam,
z: (N * (1 - WGS84.e2) + alt) * sinPhi
};
}The inverse conversion (ECEF to geodetic) is not as straightforward as the forward conversion. In practice, Bowring's iterative method is widely used. In this implementation, the initial estimate using the auxiliary angle θ = atan2(z·a, p·b) is sufficiently accurate, so sub-millimeter precision is effectively achieved in a single calculation.
Processing steps:
export function ecefToGeodetic(
x: number, y: number, z: number
): GeodeticCoord {
const p = Math.sqrt(x*x + y*y);
const lam = Math.atan2(y, x);
// Bowring initial estimate
let theta = Math.atan2(z*a, p*b);
let phi = Math.atan2(
z + ep2 * b * Math.sin(theta)**3,
p - e2 * a * Math.cos(theta)**3
);
// 3 iterations are sufficient for precision
for (let i = 0; i < 3; i++) {
theta = Math.atan2(z*a, p*b);
phi = Math.atan2(/* ... */);
}
// ... height calculation
}Column: Why Bowring's Method Was Chosen
There are several methods for the ECEF-to-geodetic inverse conversion, including Ferrari's closed-form solution and Vermeille's method, in addition to Bowring's. Bowring's method was chosen because: (1) the implementation is simple, (2) 3 iterations provide sufficient precision, and (3) the handling of singularities (near the poles) is straightforward.
Click on a city marker in the demo on the left to inspect the values in each coordinate system. Experience how the transformation pipeline works.
Click a city marker to display its coordinates
The ECEF coordinate system and Three.js scene coordinate system have different axis orientations.
ECEF: Z (North Pole) is up
Three.js: Y (up direction) is up
To align these, an axis remapping from ECEF coordinates to Three.js scene coordinates is required.
| ECEF | → | Scene Coord | Meaning |
|---|---|---|---|
| X | → | X | Longitude 0° direction (unchanged) |
| Y | → | -Z | Longitude 90°E direction → forward direction (negated) |
| Z | → | Y | North Pole direction → up direction |
The sign inversion (-Y) is necessary to maintain a right-handed coordinate system. When swapping two axes in the conversion from ECEF (right-handed) to Three.js (right-handed), one axis must be negated to avoid producing a left-handed system.
export function ecefToScenePosition(
x: number, y: number, z: number
): EcefCoord {
return {
x: x, // ECEF X → Scene X
y: z, // ECEF Z → Scene Y (up)
z: -y // ECEF Y → Scene Z (negated)
};
}In this book, 1 Three.js unit = 1 meter, with no scaling applied. CesiumJS also adopts a 1:1 scale. Introducing scaling would make tile texture resolution calculations scale-dependent and complicate coordinate alignment with external data such as 3D Tiles.
The axis remapping can also be expressed as a 4x4 matrix. The 3D Tiles adapter uses this matrix form.
// ECEF → Scene coordinate axis remapping matrix
const ecefToScene = new THREE.Matrix4().set(
1, 0, 0, 0, // X → X
0, 0, 1, 0, // Z → Y
0, -1, 0, 0, // -Y → Z
0, 0, 0, 1
);
this.group.applyMatrix4(ecefToScene);The custom 3D Tiles adapter implemented in Chapter 9 places models in ECEF coordinates, so by applying the axis remapping matrix to the Group, all child objects are automatically transformed into scene coordinates.
ENU (East-North-Up) is a local Cartesian coordinate system centered on a reference point on the Earth's surface.
U (Up = ellipsoid normal)
|
o------> E (East direction)
/
N (North direction)
Since the ENU coordinate system changes when the reference point changes, it is called a "local" coordinate system. ENU is used in the following contexts:
The ECEF-to-ENU conversion is achieved by multiplying the difference vector by a rotation matrix R.
R = | -sin(lam) cos(lam) 0 |
| -sin(phi)cos(lam) -sin(phi)sin(lam) cos(phi) |
| cos(phi)cos(lam) cos(phi)sin(lam) sin(phi) |
Meaning of each row:
The inverse conversion (ENU to ECEF) uses the transpose of the rotation matrix (since the matrix is orthogonal, the transpose equals the inverse).
export function enuToEcef(
e: number, n: number, u: number,
latRef: number, lonRef: number,
altRef: number
): EcefCoord {
const ref = geodeticToEcef(
latRef, lonRef, altRef
);
const phi = degToRad(latRef);
const lam = degToRad(lonRef);
// Transpose of rotation matrix (inverse conversion)
return {
x: ref.x
+ (-sinLam*e - sinPhi*cosLam*n
+ cosPhi*cosLam*u),
y: ref.y
+ (cosLam*e - sinPhi*sinLam*n
+ cosPhi*sinLam*u),
z: ref.z
+ (cosPhi*n + sinPhi*u)
};
}In the demo, enuToEcef converts each axis's unit vector to ECEF, and the direction is determined from the difference with the origin to draw lines.
// Convert the east unit vector to ECEF
const tipEcef = enuToEcef(
1, 0, 0, // E=1, N=0, U=0
tokyoLat, tokyoLon, 0
);
// Get direction from difference with origin
const dir = tip.sub(origin).normalize();
// Multiply direction by enuAxisLen for line endpoint
const end = tokyoOrigin.clone()
.add(dir.multiplyScalar(enuAxisLen));The correctness of the implementation can be verified with round-trip conversion tests. The tests perform round-trip conversions at various points including on the equator, at the poles, Tokyo, and the Southern Hemisphere, confirming that latitude and longitude errors are below 10^-10 degrees.
it('round-trip for Tokyo', () => {
const ecef =
geodeticToEcef(35.6762, 139.6503, 100);
const result =
ecefToGeodetic(ecef.x, ecef.y, ecef.z);
expect(result.lat)
.toBeCloseTo(35.6762, 9);
expect(result.lon)
.toBeCloseTo(139.6503, 9);
expect(result.alt)
.toBeCloseTo(100, 3); // within 1mm
});In this chapter, we implemented the coordinate systems and transformations that form the foundation of GIS.
In the next chapter, we will cover the Web Mercator projection and tile coordinate system, which form the basis for map tiles.