Calculating the angle of an object orbiting around a sphere

I am working on a 3D orbit simulation, in which an object is moving around the surface of the earth (a sphere).

I managed to get the orbits working using a function from this Stack Overflow answer. Each frame, I increase the distance variable of the object, and pass it into the function along with its start latitude, longitude and angle. The function gives me back a new latitude and longitude, which I convert to cartesian coordinates using this formula.

This works perfectly, and the object orbits around the sphere as desired. The problem I am facing now is aligning / rotating the object to face in the direction it is travelling in.

My attempt at doing this so far has been as follows:

  1. Point the object at the centre of the sphere. This can be easily done using the Object3D.lookAt function. This results in the object being parallel to the surface of the sphere.
  2. Create an arbitrary axis vector from the centre of the sphere to the object.
  3. Rotate the object around said axis using the Object3D.rotateOnWorldAxis function.

This allows the object to be freely rotated, while keeping it flat against the surface of the sphere. If I set the rotation to , it will simply always point north (towards the top of the sphere) regardless of its position.

If I set its rotation equal to its angle + π (angle being one of the paramaters I pass into the orbit function mentioned earlier) it works, but only at the start. As the object travels around the sphere, the angle begins to drift further of its orbit, and when it reaches the opposite side, it is no longer pointing anywhere near to its direction of travel.

Interestingly, I found that setting the rotation equal to angle * -1 gives the opposite result, with it pointing incorrectly at the start, and aligning as it reaches the opposite side of the sphere.

There must be some easy way to do this (most likely using trigonometric functions) which I cannot think of. Any help will be greatly appreciated!

You should be able to easily to that at the same time as you are moving the object:

// Before you calculate new lon and lat
const movingObject = /* ... */;

const originalPosition = new Vector3();
const newPosition = new Vector3();

movingObject.getWorldPosition(originalPosition);

// Move the object to a new position
fancyFormulaFunctionThatCalculatesNewLonAndLat(movingObject);

movingObject.getWorldPosition(newPosition);

// displacement is pointing from the original position to the new one
const displacement = newPosition.sub(originalPosition);

// Make the object look in the direction it's traveling
movingObject.lookAt(movingObject.localToWorld(displacement));

Thank you for the reply.

I need the movingObject to .lookAt(0, 0, 0) since that is the centre of the sphere.
If I don’t do that, the objects will no longer be flat against the surface of the sphere.

Is there any way for the objects to remain flat against the surface, while still pointing in their direction of movement?

Thank you for all the help.

You can place the object inside a Group and rotate them independently - rotate the group to always point towards the centre, then rotate the object to point towards the movement direction.

Okay, I placed the object inside of a Group like you said.
Doing that made my code much simpler because I don’t need to worry about facing the sphere or rotating around axes, all I have to do is change rotation.x.

However, I am still facing my initial problem of actually calculating the angle of rotation. You keep mentioning “pointing towards the movement direction”, but the object (as well as the group it is in) is actually completely stationary. All its doing is rotating, but not moving.

I need a way to point the object towards its previous getWorldPosition, but only using its z rotation.

Once again, thank you so much for all of your help.

Could you share a codepen / preview / video of what you are building? Otherwise it is a bit hard to help.

1 Like

You say that you are increasing the distance of the object and using it along with the start latitude, longitude and angle to compute the new position. What is that start angle? Is it the angle relative to the equator?

The equations that you most likely need are some variation of the equations that pilots use to compute the “Great Circle Route” around the earth. These use the Euler equations which deal largely with right spherical triangles. If your start angle is relative to the equator, you can use those equations to compute the change in your angle as you progress along the hypotenuse.

But I wonder if you couldn’t use a simpler solution? Since your object is apparently locked into following a circular path around the earth, it seems that you could simply move the center of your object downward the same distance as the center of the earth. You could then link that object to the center of the earth and then rotate that object using the rotation functions built into three.js. This is similar to what you would do if you wanted to rotate the moon around the earth.

I have used both approaches in my programs and the latter is far easier.

Hello @phil_crowther, thank you very much for your extensive reply.

I am absolutely in favour of using the simpler approach you described. As you said, my object is in fact always following a circular path / orbit around the earth, so if there is a way to simplify this movement, I am all for it.

Could you describe how your method works in more detail? What exactly do you mean by “move the centre of the object downward”?

Thank you for your help.

I should have said, move the object center to the center of this earth, something like this:
earth center = object center > +===========O < object

I am not sure if you are creating your object using Blender or using the primitive objects available in three.js.

I should have asked, is the earth the center of the display, such that it is located at 0,0,0?
If so, then you will just have to position your object at 0,0,0 and leave it there.
If not, then you just have to either link the object to the earth or just make sure the object center is always located at the same position as the earth center.

After you have done this, you can rotate your object around the earth. For example, if you have saved the object address at a variable named “object2”, you can set the rotation using the command:
object2.rotation.set(X,Y,Z);
where X,Y,Z are your desired rotations (in radians) on the X, Y, or Z axes.

You will probably want to change the order in the rotations are computed, which you can do with the command:
object2.rotation.order = “ZXY”;
You only have to set this once, when you initialize your object.

Assuming that the starting point for the object is the horizon, I believe that computing Z rotation first will “bank” your object and that computing X rotation second will cause your object to travel upwards along the direction it is banked. You will probably not need to change the Y rotation unless the earth is rotating around the Y axis and you want to keep the object in the same position on the earth.

Let me know if that all makes sense.

Yes, I’ve already done all of that. The object is in a group, the group’s center is at (0, 0, 0) and as it rotates, the object rotates against the surface.

I also had to change the rotation order for it to work correctly, just like you mentioned.

My problem is actually moving the object. How do I calculate the angles for the object to start orbiting the earth in a circular motion?

I need this to work with any start point, and any angle direction.

Is it working as I hoped? For example, if you start on the equator and apply Z rotation, will changes in the X rotation cause the object to travel a circular path around the earth?

If so, then you don’t need to dig into the world of “great circle navigation”, since the program is handling that automatically. To compute the degrees of X rotation, you merely need to divide the distance travelled by the circumference of the earth and multiply that by 360 degrees. You then need to convert that to radians by multiplying the result by Math.PI/180. (In my programs, I compute this value once and save it as a variable (DegRad) so that I merely have to multiply the degrees by DegRad.)

If you want to start at a random location on the globe, the you merely need to use some of the Napier equations relating to “right spherical triangles”. You will also likely want to change the order of rotation to YZX, as discussed below.

sphericaltrig

In the picture above, assume that your starting location is at the red point next to B, that the line that extends through the red points near B and C is a vertical line that passes through both poles and that the line that travels through the red points near A and B is along your path of rotation.

The latitude of your starting location is shown by line a. So what you would like to calculate are angle A (your starting Z rotation), the distance b (your starting Y rotation), and the distance c, which is the distance you have “travelled” along your path (your starting X rotation). Your known values are distance a (the latitude) and angle B (your heading).

To solve for these values, you can use the following series of Napier equations, in order:
cos A = cos a * sin B
tan b = tan B * sin a
cos c = cos a * cos b

You compute the A and c values by computing arc cosine of A and c, respectively. You compute the value of b, by computing the arc stan of b. (Note that when working with spherical right triangles, everything, including lines a, b and c, are expressed as angles.)

The routines you need are all available in three.js:
A = Math.acos(Math.sin(B)*Math.cos(a)); // Angle at Equator
b = -Math.atan((Math.sin(B)/Math.cos(B))*Math.sin(a)); // Longitude Offset
c = Math.acos(Math.cos(a)*Math.cos(b)); // Distance

If you plan to change the direction of travel from time to time, you will need to recompute A and b (your latitude a and longitude c do not change if you are merely changing direction).

Does that help?

EDITS
I have fixed several problems with the original answer, as described below.

I appreciate all of the help.

Could you explain how I could change these equations to use them in a function, like the following?

Inputs:

  • Start Latitude
  • Start Longitude
  • Start Angle
  • Distance Traveled (from start position)

Return values:

  • Current Latitude
  • Current Longitude
  • Current Angle

Thank you very much.

Here is a demo of a direct flight - in this case from Boston to Moscow. I used that route because it is similar to the image posted previously.

To create my object (airobj), I used the following:

airobj = new THREE.Object3D();
geometry = new THREE.SphereBufferGeometry(0.5, 8, 8);
material = new THREE.MeshPhongMaterial({color: 0xff0000});
mesh = new THREE.Mesh(geometry, material);
mesh.position.z = -25.1;			// the radius of the earth plus .1
airobj.add(mesh);
scene.add(airobj);
airobj.rotation.order = "YZX";

To rotate the object, using the information you provided, I used the following:

// Compute Starting Rotations
var a = latitude * DegRad;						// Starting Latitude
var B = heading * DegRad;						// Heading relative to vertical
var A = Math.acos(Math.sin(B)*Math.cos(a));			// Angle at Equator
var b = Math.atan((Math.sin(B)/Math.cos(B))*Math.sin(a));	// Longitude Offset
var c = Math.acos(Math.cos(a)*Math.cos(b));			// Distance
b = Mod360(b * RadDeg);
A = Mod360(A * RadDeg);
// Enter Starting Rotations
airobj.rotation.y = Mod360(longitude - b) * DegRad;		// Longitude of intersection
airobj.rotation.z = Mod360(90 - A) * DegRad;			// Angle at equator
airobj.rotation.x = c;							// Distance

DegRad and RadDeg are used to convert degrees to radians and vice versa. They are defined in the beginning of the program as follows:

var PieVal = Math.PI;					// PI
var DegRad = PieVal / 180;				// Convert Degrees to Radians
var RadDeg = 180 / PieVal;				// Convert Radians to Degrees

The Mod360 is a small subroutine used to keep values within the range of 0 to 360:

function Mod360(deg) {
	if (deg < 0) deg = deg + 360;
	else if (deg == 360 || deg > 360) deg = deg - 360;
return deg;}

To move the object along the path, you simply increment the value of airobj.rotation.x:

airobj.rotation.x = airobj.rotation.x +.001;

until you have travelled far enough.

I had some problems making the sphere follow certain routes. However, if you are going to pick random headings, this may not be a problem.

In any case, I think this shows that, once you have created and rotated the object, you can simply increase the rotation.x and three.js will automatically move the object along the selected path. No complex math required.

1 Like

Here is a demo of a trip with several legs.

I had to add some additional steps to correctly position and move the object (1) to the south when starting in the northern hemisphere and (2) to the north when starting in the southern hemisphere . When moving north in the northern hemisphere, the object is oriented upwards and increases in the X-rotation work correctly. However, when moving south, the object is upside down and the starting X-rotation should be negative.

So in the routines above, I set a flag (usdflg) if the object is traveling south. Before the computation, if this flag is set, the starting heading (B) is increased by 180 degrees. After the calculation, if this flag is set, the z-rotation is increased by 180 degrees (to rotate the object upside down) and the x-rotation flipped. Here is the revised code for rotating the object for a given origin (lat1/lon1) and destination (lat2/lon2).

compGChdng();		// Computes heading (see below)
compGCdist();			// Computes distance (see below)
// Starting Data
usdflg = 0;											// default = right side up
if (lat1 > 0 && GChdng > 90 && GChdng < 270) usdflg = 1;	// upside down in Northern Hemisphere
if (lat1 < 0 && GChdng > 90 && GChdng < 270) usdflg = 1;	// upside down in Southern Hemisphere
var a = lat1;										// Latitude
var B = GChdng;									// Heading relative to vertical
if (usdflg > 0) B = Mod360(GChdng + 180);				// If upside down
a = a * DegRad;
B = B * DegRad;
// Compute
var A = Math.acos(Math.sin(B)*Math.cos(a));				// Bank
var b = Math.atan((Math.sin(B)/Math.cos(B))*Math.sin(a));	// Longitude Offset
var c = Math.acos(Math.cos(a)*Math.cos(b));				// Pitch
A = Mod360(A * RadDeg);
b = Mod360(b * RadDeg);
// Rotate Object to Starting Position
airobj.rotation.z = Mod360(90 - A) * DegRad;				// Bank
airobj.rotation.y = Mod360(lon1 - b) * DegRad;			// Longitude of intersection
if (lat1 > 0) {
	airobj.rotation.x = c;							// Positive Pitch
	if (usdflg > 0) {									// If upside down
		airobj.rotation.z = Mod360(270 - A) * DegRad;	// Bank
		airobj.rotation.x = -c;						// Negative Pitch
	}
}
else {
	airobj.rotation.x = -c;							// Negative Pitch
	if (usdflg > 0) {									// If upside down
		airobj.rotation.z = Mod360(270 - A) * DegRad;	// Bank
		airobj.rotation.x = c;						// Positive Pitch
	}
}

Below are the routines used to compute heading and distance between the origin and destination. The initial line is the formula written in Excel format:

// Compute Heading
function compGChdng() {
	// ATAN2(SIN(RADIANS(Lon2-Lon1)) * COS(RADIANS(Lat2)), COS(RADIANS(Lat1)) * SIN(RADIANS(Lat2)) - SIN(RADIANS(Lat1)) * COS(RADIANS(Lat2)) * COS(RADIANS(Lon2-Lon1)))
	var a = Math.sin(Mod360(lon2-lon1)*DegRad) * Math.cos(lat2*DegRad);
	var b1 = Math.cos(lat1*DegRad) * Math.sin(lat2*DegRad);
	var b2 = Math.sin(lat1*DegRad) * Math.cos(lat2*DegRad) * Math.cos(Mod360(lon2-lon1)*DegRad);
	var b = b1 - b2;
//	GChdng = Mod360(Math.atan2(b, a) * RadDeg);			// If ATAN2 operators in correct order for three.js (opposite of Excel)
//	GChdng = Mod360(90-GChdng);							// Must do this to get correct directon for plot
	GChdng = Mod360(Math.atan2(a, b) * RadDeg);			// If ATAN2 operators reversed, can save a step
}
// Compute Distance
function compGCdist() {
	// ACOS(COS(RADIANS(90-Lat1)) * COS(RADIANS(90-Lat2)) + SIN(RADIANS(90-Lat1)) * SIN(RADIANS(90-Lat2)) * COS(RADIANS(Lon1-Lon2))) * 3958.756
	var a = Math.cos(Mod360(90-lat1)*DegRad) * Math.cos(Mod360(90-lat2)*DegRad);
	var b = Math.sin(Mod360(90-lat1)*DegRad) * Math.sin(Mod360(90-lat2)*DegRad);
	var c = Math.cos(Mod360(lon1-lon2)*DegRad);
	GCdist = Math.abs(Math.acos(a + b*c)) * 3958.756;	// 3958.756 = statute miles per radian = 
}