Let's talk coordinate systems! (warning, super technical)
The Earth is, unfortunately, not flat. So we can't just use a simple x,y,z cartesian coordinate system. For simply designating a position there's Latitude, Longitude, Altitude (LLA) which is great for that, but horrible for doing math.
Then there's ECEF (Earth-Centered, Earth-Fixed), which puts the origin in the middle of the Earth and ignores rotation (of the Earth). That's usable, but everything ends up will really large numbers, as everything is so far away from the center of the Earth, and direction vectors are very unintuitive.
A happy medium is ENU (East, North, Up), aka "
Local tangent plane coordinates" where you start from any given LLA position and then use x = East, y = North, and z = Up, where the x/y plane (the nearby ground, the green square in the image below) is assumed to be flat. This makes the math easy (North is just 0,1,0, instead of something like 0.23868293661, 0.234566334545, 0.9423423425) and this works just fine for anything under a mile.
ECEF, ENU, and LLA are shown here, the angles at the center represent latitude and longitude (altitude is not shown).
The 3D software I use for rendering on a web page, Three.js, has y=up, and z=south, so I'm using an EUS (East, Up, South) system, which is a bit of a pain as lots of library functions exist for LLA->ENU or ECEF->ENU. But ENU->EUS is a trivial operation.
So I use EUS, but with the curve of the Earth. This means the surface drops away from the x/z plane. It also means that things like a jet have to be reoriented for changes in local up (i.e. local gravity. So the jet has a local frame of reference (a rotation matrix) so that North (-z) and East (x) are parallel to the surface of the sphere, and Up (y) is perpendicular to it.
This is maintained when moving by a series of cross-products. In the simplest example:
JavaScript:
LocalFrame.matrix.extractBasis(_x, _y, _z)
var localUp = getLocalUpVector(LocalFrame.position, jetAltitudeNode.v0, feetFromMiles(radiusNode.v0))
_y.copy(localUp)
_x.crossVectors(_y, _z)
_z.crossVectors(_x, _y)
var m = new THREE.Matrix4()
m.makeBasis(_x, _y, _z)
LocalFrame.quaternion.setFromRotationMatrix(m);
A frame of reference is actually stored as a quaternion, with a matching matrix. I take the matrix and devolve it into its basis vectors, then the up (y) vector is set to the worlds up vector at that point, in EUS coodinates.
Then _x.crossVectors(_y, _z) calculates East (x) as being perpendicular to Up and South
finally _z.crossVectors(_x, _y) calculates the new South (z) vector.
Then subsequent movement and calculation of lines of sight is in this frame of reference.
One additional complication is that I'm using 7/6 * radius of the earth, to simulate refraction.