Wade J
Wade J

Reputation: 738

How to get xy screen coordinates from xyz world coordinates?

I'm building a simple app that places a marker on your screen where at the top of certain landmarks in the real world, going to overlay the markers over the camera's view. I have the latitude/longitude/altitude for both the viewing device and the world landmarks, and I convert those to ECEF coordinates. But I am having trouble with the 3D projection math. The point always seems to get placed in the middle of the screen... maybe my scaling is wrong somewhere so it looks like it's hardly moving from the center?

Viewing device GPS coordinates:

GPS:
  lat: 45.492132
  lon: -122.721062
  alt: 124 (meters)

ECEF:
  x: -2421034.078421273
  y: -3768100.560012433
  z: 4525944.676268726

Landmark GPS coordinates:

GPS:
  lat: 45.499278
  lon: -122.708417
  alt: 479 (meters)

ECEF:
  x: -2420030.781624382
  y: -3768367.5284123267
  z: 4526754.604333807

I tried following the math from here to build a function to get me screen coordinates from 3D point coordinates.

When I put those ECEF points into my projection function, with a viewport of 1440x335 I get: x: 721, y: 167

Here is my function:

function projectionCoordinates(origin, destination) {
  const relativeX = destination.x - origin.x;
  const relativeY = destination.y - origin.y;
  const relativeZ = destination.z - origin.z;

  const xPerspective = relativeX / relativeZ;
  const yPerspective = relativeY / relativeZ;

  const xNormalized = (xPerspective + viewPort.width / 2) / viewPort.width;
  const yNormalized = (yPerspective + viewPort.height / 2) / viewPort.height;

  const xRaster = Math.floor(xNormalized * viewPort.width);
  const yRaster = Math.floor((1 - yNormalized) * viewPort.height);

  return { x: xRaster, y: yRaster };
}

I believe the point should be placed much higher on the screen. That article I linked mentions 3x4 matrices which I couldn't follow along with (not sure how to build the 3x4 matrices from the 3D points). Maybe those are important, especially since I will eventually have to take the device's tilt into consideration (looking up or down with phone).

If it's needed, here is my function to convert latitude/longitude/altitude coordinates to ECEF (copy/pasted from another SO answer):

function llaToCartesion({ lat, lon, alt }) {
  const cosLat = Math.cos((lat * Math.PI) / 180.0);
  const sinLat = Math.sin((lat * Math.PI) / 180.0);
  const cosLon = Math.cos((lon * Math.PI) / 180.0);
  const sinLon = Math.sin((lon * Math.PI) / 180.0);
  const rad = 6378137.0;
  const f = 1.0 / 298.257224;
  const C =
    1.0 / Math.sqrt(cosLat * cosLat + (1 - f) * (1 - f) * sinLat * sinLat);
  const S = (1.0 - f) * (1.0 - f) * C;
  const h = alt;

  const x = (rad * C + h) * cosLat * cosLon;
  const y = (rad * C + h) * cosLat * sinLon;
  const z = (rad * S + h) * sinLat;

  return { x, y, z };
}

Upvotes: 1

Views: 1791

Answers (1)

Stephen O'Connor
Stephen O'Connor

Reputation: 1475

Your normalise and raster steps are cancelling out the view port scaling you need. Multiplying out this:

const xNormalized = (xPerspective + viewPort.width / 2) / viewPort.width;

gives you:

const xNormalized = xPerspective / viewPort.width + 0.5;

And applying this line:

const xRaster = Math.floor(xNormalized * viewPort.width);

gives you:

const xRaster = Math.floor(xPerspective + viewPort.width * 0.5);

Your calculation of xPerspective is correct (but see comment below) - however the value is going to be around 1 looking at your numbers. Which is why the point is near the centre of the screen.

The correct way to do this is:

const xRaster = Math.floor(xPerspective * viewPort.width /2 + viewPort.width /2);

You can simplify that. The idea is that xPerspective is the tan of the angle that xRelative subtends at the eye. Multiplying the tan by half the width of the screen gives you the x distance from the centre of the screen. You then add the x position of the centre of the screen to get the screen coordinate.

Your maths uses an implicit camera view which is aligned with the x, y, z axes. To move the view around you need to calculate xRelative etc relative to the camera before doing the perspective divide step (division by zRelative). An easy way to do this is to represent your camera as 3 vectors which are the X,Y,Z of the camera view. You then calculate the projection of the your 3D point on your camera by taking the dot product of the vector [xRelative, yRelative, zRelative] with each of X,Y and Z. This gives you a new [xCamera, yCamera, zCamera] which will change as you move your camera. You can also do this with matrices.

Upvotes: 2

Related Questions