Search code examples
unity-game-enginequaternions

Getting compass-like behavior from quaternion


Suppose you have a camera projection matrix, i.e. camera translation vector + rotation quaternion, like every typical camera, it is able to move and rotate in any direction. And independent of it's rotation whether it is looking forward, upward or downward I need to show a compass-like gauge pointing where the camera is targeted at.

The problem is that when the camera is pointed downwards the rotation of camera around it's optical center defines the value of the compass, but when the camera points forward, the rotation of camera around it's center no longer affects the value of compass, in this case the direction of camera defines the value of compass.

It get's more ugly when the camera is tilted downwards only 45 degrees, in this case it is not even clear whether the rotation around camera center affects rotation of compass.

So is there an elegant way of getting the compass value based on arbitrary camera projection matrix / quaternion?

Thank you in advance!


Solution

  • If you want just an arrow pointing at the target its:

    Transform camera = Camera.main.transform;
    Transform target = Target.transform;
    Vector3 relativePosition = target.position - camera.position;
    
    Vector3 targetRelative = Vector3.ProjectOnPlane(relativePosition, camera.forward);
    
    float angle = Angle360(camera.up, targetRelative, camera.forward);  
    
    Compass.transform.rotation = Quaternion.Euler(0, 0, angle);
    

    The angle function is:

    float Angle360(Vector3 from, Vector3 to, Vector3 normal)
    {
        float dot = Vector3.Dot(from, to);
        float det = Vector3.Dot(normal, Vector3.Cross(from, to));
        return Mathf.Atan2(det, dot)*Mathf.Rad2Deg;
    }
    

    Here is how you can get the direction of the compass in worldspace:

    Project the camera direction and target position on the XZ plane

    Transform camera = Camera.main.transform;
    Transform target = Target.transform;
    
    Vector3 cameraWorldDirXZ = Vector3.ProjectOnPlane(camera.forward, Vector3.up).normalized;
    Vector3 targetWorldDirXZ = Vector3.ProjectOnPlane(target.position, Vector3.up).normalized; 
    

    The angle between the cameraWorldDirXZ and targetWorldDirXZ is the angle of your compass needle.

    But i don't think this will behave like you think it will. This gives you the angle that you need to rotate the camera.forward vector around the y axis to face the target. If you rotate around camera.forward you don't change either the camera.forward vector or the y axis so the compass wont change.

    You might want to try a compass in local space. For that you project onto the camera XZ plane:

    Vector3 cameraLocalDirXZ = camera.forward;
    Vector3 targetLocalDirXZ = Vector3.ProjectOnPlane(target.position, camera.up).normalized; 
    

    Again the angle between the cameraLocalDirXZ and targetLocalDirXZ is the angle of your compass needle. This gives you the angle you need to rotate camera.forward around camera.up to face the target. Note that when you rotate around camera.forward it will change camera.up so it will change the compass direction.