Rotate a globe with force using latitude and longitude

So after gathering info from people and experimenting and reading i am standing to a point where i can’t finish my main task which seems relatively simple, but in fact it’s a total nightmare.
So i am working with my friend who wants a rotating globe with two rotation functionalities.

  1. When a player swipes with the finger or moves the mouse in some direction the globe needs to rotate to that direction with some force - smoothly.
  2. When the player only clicks or taps ones over the globe, the globe must rotate so the clicked point faces the center of the camera.

So here i go.For the first functionality i added rigidbody to the globe and started rotating the globe adding torque in direction towards the swiped position.Then just clamped the Y axis to a certain angle so it wont flip the globe around.Pretty easy and it works OK.

For the second functionality what i did is get the tapped or clicked point from the globe and create a rotation from it to Vector3.forward which stands in my scene as the camera’s center.Now here is where i found out that when i rotate the globe arbitrary like that at certain point the continents started tilting which does not look appealing and it’s not acceptable for the project.Look at the picture:

Next thing i found out is how to rotate the globe by latitude and longitude which was exactly the thing i was looking for and replaced the last method with this one. So now when the player taps over that location it angles the globe to align perfectly to the camera’s center or the world forward vector.Great!

Then i created a method which gets my current latitude and longitude from the center of the globe so even if i spin the globe with torque i will always know the current latitude and longitude from the globes center and i will interpolate the angles without any problems.Now here is where i got it wrong.

When spinning the globe with torque of course, it does not angle the globe by the lat/lon angles.Here when the player spins the globe with the first functionality and after that taps over the planet the interpolation starts with the correct lat/lon, but it seems that this is not enough to get the correct current rotation.The result goes like:

  1. Globe rotates instantly from the
    tilted angle from the torque to the
    correct lat/lon angles(bad)
  2. After that it interpolates from
    that rotation to the tapped one.(its ok)

Look at the video to see exactly how the rotation happens.I first swipe then i click on a location.

Swipe video

Is it possible to combine the two functionalities:
rotate the rigid body with torque + rotate to latitude/longitude to work together with no conflicts between them and interpolating smoothly?If so how?If not is there any similar way to produce such a thing?

As mentioned in my comment above, from a usability perspective I think maintaining the
‘y’ axis of the globe so that is appears vertical from the camera point of view is a better implementation than an arbitrary Quaternion.FromToRotation() rotation. And as mentioned this can be implemented by pulling the tilting up and down, and the ‘y’ rotation apart as two separate rotations…either on separate game objects, or between the earth and the camera. I was uncertain enough of the geometry and rotations, that I had to do a quick prototype of the whole behavior. Below is a script that implements the two-game-object-rotation concept. I setup as follows:

  • The earth is a child of an empty game object with the script on the empty game object
  • Both earth and the empty game object are at Vector3.zero
  • The camera is perspective and positioned at (0,0,-10) with rotation (0,0,0).
  • The child, visible earth object needs to have the name ‘Earth’.

It is likely that if you move beyond this setup, you’ll find some things that need to be fixed. I wasn’t trying to build a general solution, but a framework to test the rotation code to make sure I had it right. You’ll find the “magic” in the ‘Animate()’ method. After playing with it a bit, I found that, with the now vertical poles, I did not like the straightening of the globe during a spin. I left it in because it was one of the main points of the question, but find the comment about this feature and disable the line of code to see what it feel like without straightening. In addition, the code distinguishes between a move-rotate and a swipe-rotate. If the “swipe” results in a low velocity calculation for the spin, the spin and straighten are not done. This allows the user to rotate the globe to look around without a resulting spin.

You’ll find a package to demonstrate here:

http://www.laughingloops.com/RotateEarth.unitypackage

And here is the script:

using UnityEngine;
using System.Collections;

public class RotateEarth : MonoBehaviour {

	public float spinFactor = 10.0f;       // Maps finger movement to rotation
	public float tapTimeThreshold = 0.2f;  // Tap vs swipe determiner
	public float tapDistThreshold = 0.2f;  // Tap vs swipe determiner
	public float spinSpeedFactor = 5.0f;   // Maps velocity to free spin speed
	public float frictionFactor = 0.98f;   // Lower numbers mean faster stoping on free spin
	public float straightenSpeed = 0.5f;   // How fast the axis straightens
	public float noSpinThreshold = 75.0f;  // Below this velocity threshold, no free spin

	private Transform world;
	private RaycastHit hit;
	private bool moving = false;
	private bool freeRotate = false;
	private bool animating = false;
	private Vector3 startPoint = Vector3.zero;
	private float startTime = 0.0f;
	private Quaternion startRotation;
	private float spinRate = 0.0f;

	void Start() {
		world = transform.Find("Earth");
	}
	
	void Update () {
		if (!animating && Input.GetMouseButtonDown (0)) {
			Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
			if (world.collider.Raycast(ray, out hit, 100f)) {
				freeRotate = false;
				moving = true;
				startPoint = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10);
				startPoint = Camera.main.ScreenToWorldPoint(startPoint);
				startTime = Time.time;
				startRotation = world.localRotation;
			}
		}
		
		if (Input.GetMouseButton (0)) {
			if (moving) {
				Vector3 pt = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10);
				pt = Camera.main.ScreenToWorldPoint(pt);
				world.localRotation = Quaternion.AngleAxis((startPoint.x - pt.x) * spinFactor, Vector3.up) * startRotation;
			}
		}

		if (Input.GetMouseButtonUp(0)) {
			if (moving) {
				Vector3 pt = new Vector3(Input.mousePosition.x, Input.mousePosition.y, 10);
				pt = Camera.main.ScreenToWorldPoint(pt);
				moving = false;

				if (((Time.time - startTime) < tapTimeThreshold) && ((startPoint - pt).magnitude < tapDistThreshold)) {
					// Do the tap
					Debug.Log ("Do the tap");
					StartCoroutine(Animate(hit.point, 1.2f));
				}
				else {
					spinRate = (startPoint.x - pt.x) / (Time.time - startTime) * spinSpeedFactor;
					Debug.Log (spinRate);
					if (Mathf.Abs (spinRate) > noSpinThreshold) {
						freeRotate = true;
					}
				}
			}
		}

		if (freeRotate) {

			world.localRotation = Quaternion.AngleAxis (spinRate * Time.deltaTime, Vector3.up) * world.localRotation;
			spinRate *= frictionFactor;
			// Comment out the following line to disable straightening of the axis during spin
			transform.rotation = Quaternion.Slerp (transform.rotation, Quaternion.identity, Time.deltaTime * straightenSpeed);
			if (Mathf.Abs (spinRate) < 0.5f) {
				freeRotate = false;
			}

		}
	}

	IEnumerator Animate(Vector3 hitPoint, float time) {
		animating = true;

		Quaternion qFromWorld = world.localRotation;
		Vector3 h = world.InverseTransformPoint(hitPoint);
		h.y = 0.0f;
		Quaternion qToWorld = Quaternion.FromToRotation(h, Vector3.back);
	
		Quaternion qFrom = transform.rotation;
		h = hitPoint;
		h = world.InverseTransformPoint (hitPoint);
		float up = h.y;
		h.y = 0;
		Vector3 v = Vector3.back * h.magnitude + Vector3.up * up;
		Quaternion qTo = Quaternion.FromToRotation (v, Vector3.back);

		float timer = 0.0f;
		while (timer <= time) {
			float t = Mathf.Sin (timer / time * Mathf.PI * 0.5f);
			transform.rotation = Quaternion.Slerp(qFrom, qTo, t);
			world.localRotation = Quaternion.Slerp(qFromWorld, qToWorld, t);
			timer += Time.deltaTime;
			yield return null;
		}

		animating = false;
	}
}

if you are using physics, addtorque. if you are using transform position, quaternion rotate around axis going through centre and Vector3Cross of swipe direction.