Camera Orbit Around World Pivot Point Where Camera position and rotation Orbit together

I’ve been searching for days and haven’t quite found what I’m looking for…

I have a GameObject (target) that positions itself in world space based on a cursor location.

This GameObject is linked to a Camera.

If orbit is active, I want to orbit around that point…wherever it is on the screen.

This is almost achieved using 2 transform.rotateAround Functions in a row:

transform.RotateAround(target.position, Vector3.up, mouseDelta.x * .05f);
transform.RotateAround(target.position, Vector3.left, mouseDelta.y *01f);

However, this eventually tilts the camera off the X-Z plane and won’t work

With a quaternion, I can achieve the correct orbit of the camera’s position, but the camera rotation (frustum/field of view doesn’t change)

Vector3 direction = target.position - transform.position;

xEuler = mouseDelta.x * .05f;
yEuler = (mouseDelta.y) * .1f;

transform.position = transform.position + direction;

Quaternion qt = Quaternion.Euler(yEuler, -xEuler, 0);

transform.rotation = Quaternion.Lerp(transform.rotation, qt, Time.deltaTime * 4);

transform.position = qt * (transform.position - direction);

If I add:

transform.LookAt(target);

it ruins it by centering the target in the camera’s field of view

I’m stuck on how to get the camera’s rotation (perspective/frustum) to also orbit around the pivot point

There is a script that comes with Unity called Mouse Orbit that does this. Do you know about it? Or are you looking for something more?

I’ve looked through 20+ scripts including the Camera Scripts in the Standard Asset Package.

Most people that are looking for something like this all seem to be satisfied with transform.RotateAround…but you’re limited to 1 axis. I need 2 axes for a smooth orbit.

I think mouse orbit does use 2 axis. You probably have the wrong Standard assets package. The unity asset store should have the most recent. Just search for standard assets in the store.

There are ways to do it from scratch. Although its best to save work if you don’t have to. Its smart laziness.

Never liked using stuff from SA, they’re always large and never quite what I want.

Is this not what you’re after?

    public float xRot = 0f;
    public float yRot = 0f;

    public float distance = 5f;
    public float sensitivity = 1000f;
    public Transform target;

    void Update()
    {
        xRot += Input.GetAxis("Mouse Y") * sensitivity * Time.deltaTime;
        yRot += Input.GetAxis("Mouse X") * sensitivity * Time.deltaTime;

        if(xRot > 90f)
        {
            xRot = 90f;
        }else if(xRot < -90f)
        {
            xRot = -90f;
        }

        transform.position = target.position + Quaternion.Euler(xRot, yRot, 0f) * (distance * -Vector3.back);
        transform.LookAt(target.position, Vector3.up);
    }

You said your code was “ruined” by keeping the target in centre frame, isn’t that what you want? Is this a 2D game?

No, this is intended for Building Visualization (or BIM) and so I’m trying to mimic the orbit controls found in those types of programs.

However, I got the code “working”…it achieves what I wanted…EXCEPT!..the camera flickers like crazy…not sure why…something about storing variables first? Here’s the code as is…I’ve identified the line that causes the flickering to happen. I will mark this as solved and start a new post to clean things up a bit.

public class new3rdPerson : MonoBehaviour {

    public Transform lookAt;
  
    private Vector3 currentMouse;
    private Vector3 mouseDelta;
    private Vector3 lastMouse;
    private Vector3 cursorDirection;

    // Use this for initialization
    void Start ()
    {
              
    }
   
    private void Update()
    {
        //Camera main is the transform

        currentMouse = Input.mousePosition;

        mouseDelta = lastMouse - currentMouse;

        cursorDirection = lookAt.position - transform.position;
    }

    // Update is called once per frame
    void LateUpdate ()
    {

        float z = transform.eulerAngles.z; //Keeps camera flat to x-z plane

        Quaternion rotation = Quaternion.Euler(mouseDelta.y *.1f, mouseDelta.x * -.05f, -z);

        transform.position = lookAt.position + rotation * cursorDirection; //this line is causing the flickering
        transform.rotation = rotation * transform.rotation;
       
             
        lastMouse = Input.mousePosition;
    }
}

Nah, unstable math. I fixed the flickering but it’s still borked when the camera is looking up steeply.

I’d suggest simply copying the code I posted above. It does the same thing but without the flickering. Anyway here’s the semi-fixed version I have so far:
EDIT: Removed code. Check post below.

Fixed it, and simplified a bit.

    public Transform lookAt;

    private Vector3 currentMouse;
    private Vector3 cursorDirection;
    public float sensitivity = 3f;
    public float distance = 5f;
  
    private void Update()
    {
        currentMouse = Input.mousePosition * sensitivity;
        currentMouse.y *= -1f;

        cursorDirection = lookAt.position - transform.position;

        Quaternion rotation = Quaternion.Euler(currentMouse.y * .1f, currentMouse.x * -.05f, 0);

        transform.position = lookAt.position - rotation * (Vector3.forward * distance);
        transform.rotation = rotation;
    }

By the way, transform.LookAt can keep the camera flat to the xz plane by adding the world up variable Vector3.up like this:

transform.LookAt(target.position, Vector3.up);
1 Like

Here’s the issue:

In your code, the camera looks at the target (and therefore centers it on the screen). I do not want that. The cursor location needs to serve as the pivot point of the orbit. So the farther the cursor is left or right of the center, the longer the radius of the pivot point to camera is.

EDIT: In the pursuit of simplicity, I left out a key piece of my code…which may have caused confusion earlier. Without it, the pivot point aspect doesn’t work. Below is the current full code (which still flickers)

public class new3rdPerson : MonoBehaviour {

    public Transform lookAt; //A game object that moves to wherever the cursor clicks on screen
 
    private Vector3 currentMouse;
    private Vector3 mouseDelta;
    private Vector3 lastMouse;
    private Vector3 cursorDirection;

    bool activeRotate = false;

    // Use this for initialization
    void Start ()
    {
            
    }
 
    private void Update()
    {

        mouseDelta = lastMouse - currentMouse;

        cursorDirection = lookAt.position - transform.position;

        if (Input.GetKey(KeyCode.LeftShift))
        {
            activeRotate = true;

            currentMouse = Input.mousePosition;
        }


        if (Input.GetKeyUp(KeyCode.LeftShift))
        {
            activeRotate = false;
        }

        

        currentMouse = Input.mousePosition;

        mouseDelta = lastMouse - currentMouse;
    }

    // Update is called once per frame
    void LateUpdate ()
    {

        if (activeRotate)
        {
            float z = transform.eulerAngles.z; //Keeps camera flat to x-z plane

            Quaternion rotation = Quaternion.Euler(mouseDelta.y * .1f, mouseDelta.x * -.05f, -z);

            transform.position = lookAt.position + rotation * cursorDirection; //this line is causing the flickering
            transform.rotation = rotation * transform.rotation;

            lastMouse = Input.mousePosition;
        }
    }
}

Ok yea I’m definitely confused on what effect you’re going for.

The rotation in the script of my last post can be disabled just by commenting out this line:

transform.rotation = rotation;

Since the rotation is never changed (only position), it will never tilt it off the xz plane like in your original post.

Could you post a video of what exactly you’re aiming for?

Sorry, I should’ve done this from post 1. Thanks for sticking with me on this! Below is a video of what I’m trying to do.

Here’s as far as I’ve gotten up to this point:

I got this effect by using the same script for two different axis. One axis is the base that rotates horizontally (horizontal Y) and the other gets parented to the base and rotates up and down (Vertical X)

using UnityEngine;
using System.Collections;

public class lookAroundOrbit: MonoBehaviour {

    public float horizontalSpeed = 2.0F;
    public float verticalSpeed = 2.0F;


    void Update() {

        if(Input.GetMouseButton (2)){
        float h = horizontalSpeed * Input.GetAxis("Mouse X");
        float v = verticalSpeed * Input.GetAxis("Mouse Y");

        transform.Rotate(v, h, 0);


                }
            }
}

Setup the parenting hierarchy like this.

On orbitCenterY, keep vertical speed 0, on orbitCenterX, keep horizontal speed 0.

This is the most basic setup. I built off of it and added things like smoothing and making the view reset when the middle mouse is not being pressed down.

I will throw in my code which I was rocking for a small project.
It orbits a GameObject called target but this could easily be switched out for any Vector3.
Note also how its not the most optimised but hey it worked great for my use. It also doesnt suffer from the ‘tilt’ you experience.
This script goes on your camera.

using UnityEngine;

public class RotateCamera : MonoBehaviour
{
    [SerializeField] GameObject target;

    [Header("Speed")]
    [SerializeField] float moveSpeed = 300f;
    [SerializeField] float zoomSpeed = 100f;

    [Header("Zoom")]
    [SerializeField] float minDistance = 2f;
    [SerializeField] float maxDistance = 5f;

    void Update ()
    {
        CameraControl();
    }

    void CameraControl()
    {
        if (Input.GetMouseButton(0))
        {
            /* Rotate the camera using the input managers mouse axis */
            transform.RotateAround(target.transform.position, Vector3.up, ((Input.GetAxisRaw("Mouse X") * Time.deltaTime) * moveSpeed));
            transform.RotateAround(target.transform.position, transform.right, -((Input.GetAxisRaw("Mouse Y") * Time.deltaTime) * moveSpeed));
        }

        /* Zoom the camera */
        ZoomCamera();
    }

    void ZoomCamera()
    {
        /* If we are already close enough for the min distance and we try to zoom in, dont, return instead */
        /* Similarly for zooming out */
        if (Vector3.Distance(transform.position, target.transform.position) <= minDistance && Input.GetAxis("Mouse ScrollWheel") > 0f) { return; }
        if (Vector3.Distance(transform.position, target.transform.position) >= maxDistance && Input.GetAxis("Mouse ScrollWheel") < 0f) { return; }

        /* Only move in the Z relative to the Camera (so forward and back) */
        transform.Translate(
            0f,
            0f,
            (Input.GetAxis("Mouse ScrollWheel") * Time.deltaTime) * zoomSpeed,
            Space.Self
        );
    }   
}

Ah! I see now. I have a general idea for how to do this, but I’ll have to think about it a bit.

Right, I think this is what you’re looking for:

public class New3rdPerson : MonoBehaviour
{
    public Transform pivot;
    public float sensitivity = 3f;
    public float distance = 5f;

    float xRot = 0f;
    float yRot = 0f;

    Quaternion adjustmentRotation;
    Quaternion rotation;

    private void LateUpdate()
    {
        if(Input.GetMouseButtonDown(0))
        {
            // Set distance to the current distance of the target
            distance = Vector3.Distance(transform.position, pivot.position);

            // Set the x and y rotation to the new rotation relative to the pivot
            Vector3 pivotToHere = transform.position - pivot.position;
            Vector3 tempVec = Vector3.ProjectOnPlane(pivotToHere, Vector3.up);
            if (pivotToHere.x > 0f)
                yRot = Vector3.Angle(Vector3.forward, tempVec) + 180f;
            else
                yRot = -Vector3.Angle(Vector3.forward, tempVec) + 180f;

            if(pivotToHere.y > 0f)
                xRot = Vector3.Angle(tempVec, pivotToHere);
            else
                xRot = -Vector3.Angle(tempVec, pivotToHere);

            rotation = Quaternion.Euler(xRot, yRot, 0);
            adjustmentRotation = Quaternion.FromToRotation(rotation * Vector3.forward, transform.forward);
         
        }

        if (Input.GetMouseButton(0))
        {
            xRot -= Input.GetAxis("Mouse Y") * Time.deltaTime * sensitivity;
            yRot += Input.GetAxis("Mouse X") * Time.deltaTime * sensitivity;

            rotation = Quaternion.Euler(xRot, yRot, 0);

            transform.position = pivot.position - rotation * (Vector3.forward * distance);

            transform.rotation = Quaternion.LookRotation(adjustmentRotation* (rotation * Vector3.forward), Vector3.up);
        }
    }
}

Note I put the movement in LateUpdate so it’ll register the move of the pivot in case you’re setting the pivot position in Update and that script runs after the camera script, in which case it’ll bork your rotation.
The if/else statements for the x/y rotations are because the Vector3.Angle method returns the smallest angle between the two vectors.

The general idea is that you’re getting the rotation that makes the camera look at the pivot, then get a rotation from that rotation to the current rotation, which I call “adjustmentRotation”.

Then you rotate normally towards the pivot, and then add onto that the adjustment rotation.

It’s a lot of vector/quaternion math, but rotations are tricky buggers. It might be simpler to understand using the hierarchy method @roger0 posted above, because Unity will do all the local/global transformations for you, but I like little challenges like these, so that’s my input lol

the problem you’re having is from gimble lock and because you are using Euler and Angles calls instead of just Quaternion. Even worse, the tiny imperfections from the gimble and float percision are adding up across the frames which is causing the visible tilt.

Before you rotate around, inside the OnMouseButtonDown the save the from-to rotation which goes from looking at the pivot to the camera’s current foward.

var pivotDirection = pivotPosition - transform.postion;

//save the camera's rotation relative to the look-at-pivot rotation
offsetRotation = Quaternion.FromToRotation(pivotDirection, transform.forward);

now, while you are dragging, and after you’ve Rotated around the pivot this frame (which updated your position), you then focus on correcting the rotation.

//get the current direction to the pivot which we will use as the origin.
var pivotDirection= pivotPostion - transform.postion;

//we add in the offset rotation to find the rotation with the correct forward direction but the up direction might be tilted.
var targetLook = Quaternion.LookDirection(pivotDirection) * offsetRotation;

//build a new rotation using targetLook's correct foward combined with world Up as the up rotation
trasnform.rotation = Quaternion.LookRotation(Vector3.foward * targetLook, Vector3.up);

this way the rotations don’t build on itself. its always rebuilt on the current frame which should prevent visible tilt.

Ok! Success!

Thanks once more to everyone for their feedback and patience.

The first approach I tried worked…it was TaleOf4Gamers approach, which utilized 2 transform.RotateAround function calls.

I had tried this at the beginning, but was getting the tilt and thought Quat’s were the only way to go. However, I was using Vector.right as the X axis. If I change that to “transform.right” it works with no tilt!

I will work through everyone’s replies and see if they all achieve the same result.

You could share the final script, I’m trying to do the same.

Hi,

…why not take the code you got for free, and take it further yourself?

This isn’t get code for free service, but a community… there’s Unity Connect for contract work.

Besides you necro’ed a thread that is 2 years old :slight_smile: