Rotate object always at same speed on screen, no matter camera distance?

I am rotating a globe like XCOM's hologlobe,

I found a good value for maxDegreesDelta, in my case it is 5.0f.

There is a limit on how close or far the camera can be, let's assume clos is 1.0f and far is 2.0f.

I want to be able to zoom into the globe, but obviously when I do, it rotates a bit too fast then.

When zoomed out, rotation speed is satisfying:

When zoomed in, rotation is too fast, making it more difficult to manipulate:

And the problem is even more evident as game view size gets bigger, i.e. fullscreen.

Using Mathf.Lerp and Mathf.InverseLerp, I've tried to make maxDegreesDelta and mouse delta proportional to the distance the camera is but it's hardly convincing.

Note: I rotate the globe, not the camera.

Question:

How can I ensure object rotates at same speed on screen, no matter how close or far camera is ?

Code:

(assumes there is a sphere of radius 0.5 at Vector3.zero and camera is positioned at Vector3.back)

using UnityEngine;
using UnityEngine.InputSystem;

namespace Test2
{
public class GeoScapeController : MonoBehaviour
{
#region Public

public Transform TargetObject;

public Camera TargetCamera;

[Range(0.01f, 1.0f)]
public float Sensitivity;

public bool Smooth;

public float ZoomDistanceMin;

public float ZoomDistanceMax;

#endregion

#region Private

[SerializeField]
[HideInInspector]
private Quaternion TargetRotation;

[SerializeField]
[HideInInspector]
private int ZoomLevel;

[SerializeField]
[HideInInspector]
private int ZoomLevels;

[SerializeField]
[HideInInspector]
private Vector3 ZoomVector;

[SerializeField]
[HideInInspector]
private Vector3 ZoomVelocity;

private void Reset()
{
Sensitivity = 0.05f;
Smooth = true;

ZoomDistanceMin = 0.625F;
ZoomDistanceMax = 1.25F;
ZoomLevels = 10;
}

private void Start()
{
TargetRotation = Quaternion.LookRotation(TargetObject.forward, TargetObject.up);
ZoomVector = TargetCamera.transform.position;
}

private void Update()
{
var mouse = Mouse.current;
var scale = new Vector4(-1.0f, +1.0f, +1.0f);

var x = mouse.leftButton.isPressed ? delta.y * scale.y * Sensitivity : 0.0f;
var y = mouse.leftButton.isPressed ? delta.x * scale.x * Sensitivity : 0.0f;
var z = mouse.middleButton.isPressed ? delta.x * scale.z * Sensitivity : 0.0f;
var r = Quaternion.AngleAxis(x, Vector3.right) *
Quaternion.AngleAxis(y, Vector3.up) *
Quaternion.AngleAxis(z, Vector3.forward);

if (Smooth) // hopefully, quaternion limits won't be crossed !
{
TargetRotation = Quaternion.RotateTowards(TargetRotation, r * TargetRotation, 2.5f);

TargetObject.rotation = Quaternion.Slerp(TargetObject.rotation, TargetRotation, Time.deltaTime * 5.0f);
}
else
{
TargetObject.rotation = TargetRotation = r * TargetRotation;
}

var wheel = (int) mouse.scroll.ReadValue().y / 120;
if (wheel != 0)
{
ZoomLevel = Mathf.Clamp(ZoomLevel + wheel, 0, ZoomLevels);
var lerp1 = Mathf.InverseLerp(ZoomLevels, 0, ZoomLevel);
var lerp2 = Mathf.Lerp(ZoomDistanceMin, ZoomDistanceMax, lerp1);
ZoomVector = Vector3.back * lerp2;
}

TargetCamera.transform.position = Smooth
? Vector3.SmoothDamp(TargetCamera.transform.position, ZoomVector, ref ZoomVelocity, 0.3f)
: ZoomVector;
}

#endregion
}
}

• To answer this, we'd need to see your code for generating the destination quaternion, so we can see how this relates to the zoom level. – DMGregory Jan 21 '20 at 12:29
• I've added the code, about the quaternion ... well ... I struggled quite a bit then realized it will simply break when moving too fast because of how quaternions work, any insights about this are welcome too, thanks :) – aybe Jan 21 '20 at 13:55
• Try using animation curve. There is no magic bullet for this, as it depends on person preferences on how fast it should move. Try graphing curve to a suitable speed at every distance or find a formula that suits your needs. Maybe there is some kind of formula that does it the best, but I doubt that it's easy to achieve. – Candid Moon _Max_ Jan 21 '20 at 14:20
• @n_plum It 'appears' to move faster, for the frame reference I was looking at frustum but seems like FOV might be easier, looking into it! – aybe Jan 22 '20 at 0:14
• @n_plum the FOV approach did it, max angular delta and mouse speed are proportional to how much camera is zooming, when close now it's really good, thanks for pointing me out in the right direction ! – aybe Jan 23 '20 at 19:07

This is my solution, Google Earth like control with smooth animation (or not), when zoomed in it is user friendly and slows panning down.

The only thing I am not satisfied about is the quaternion slerp, at these defaults there won't be any sudden jumps but I would have loved to understand how to sub-interpolate quaternions so it works even when rotating very fast.

Also the other thing is speed is different when at fullscreen but it should be reasonably easy to address that.

Right now however it is quite satisfying.

Feel free to improve and sharing improvements are welcome !

using UnityEngine;
using UnityEngine.InputSystem;

namespace Test2
{
public class GeoScapeController : MonoBehaviour
{
#region Public

public Transform Target;

public Camera Camera;

[Range(0.01f, 1.0f)]
public float MouseSensitivity = 0.01f;

public bool Smooth = true;

[Range(0.1f, 90.0f)]
public float RotationMaxDelta = 5.0f;

[Range(0.1f, 90.0f)]
public float RotationSpeed = 5.0f;

[Range(2, 100)]
public int ZoomGrain = 20;

[Range(1, 100)]
public float ZoomSpeed = 20.0f;

[Range(1.0f, 100.0f)]
public float ZoomMin = 10.0f;

[Range(1.0f, 100.0f)]
public float ZoomMax = 60.0f;

#endregion

#region Private

[SerializeField]
[HideInInspector]
private Quaternion Rotation;

[SerializeField]
[HideInInspector]
private float Zoom;

[SerializeField]
[HideInInspector]
private int ZoomSlot;

[SerializeField]
[HideInInspector]
private float ZoomVelocity;

private void Start()
{
Rotation = Quaternion.LookRotation(Target.forward, Target.up);
}

private void Update()
{
var mouse = Mouse.current;

if (position.x >= 0.0f && position.y >= 0.0f && position.x < Screen.width && position.y < Screen.height)
{
var n = new Vector3(-1.0f, +1.0f, +1.0f);
var m = Zoom / ZoomMax * RotationMaxDelta;
var s = MouseSensitivity * m;
var x = mouse.leftButton.isPressed ? d.y * n.y * s : 0.0f;
var y = mouse.leftButton.isPressed ? d.x * n.x * s : 0.0f;
var z = mouse.middleButton.isPressed ? d.x * n.z * s : 0.0f;
var r = Quaternion.AngleAxis(x, Vector3.right) *
Quaternion.AngleAxis(y, Vector3.up) *
Quaternion.AngleAxis(z, Vector3.forward);

Rotation = Smooth
? Quaternion.RotateTowards(Rotation, r * Rotation, m)
: r * Rotation;

var wheel = (int) mouse.scroll.ReadValue().y / 120;
ZoomSlot = Mathf.Clamp(ZoomSlot + wheel, 1, ZoomGrain);

var lerp = Mathf.InverseLerp(1, ZoomGrain, ZoomSlot);
Zoom = Mathf.Lerp(ZoomMax, ZoomMin, lerp);
}

Target.rotation = Smooth
? Quaternion.Slerp(Target.rotation, Rotation, Time.deltaTime * RotationSpeed)
: Rotation;

Camera.fieldOfView = Smooth
? Mathf.SmoothDamp(Camera.fieldOfView, Zoom, ref ZoomVelocity, Time.deltaTime * ZoomSpeed)
: Zoom;
}

#endregion
}
}


Previously this zoom level used to be very difficult to navigate, now it's much better:

Here's an alternative approach: scale your mouse delta down if you zoom in and up if you zoom out. Now my math is a bit rusty, so I am not 100% sure, but I think if you just lerp between whatever speed that "looks" right at max and min zoom, you should achieve something that looks right all the way.

• The thing is, I was looking for some proportional 'angular' speed, just got something without having to tweak min and max. – aybe Jan 23 '20 at 20:41