Rotate around a fixed point in 3d space?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Sancarn

Rotating the camera around a point in 3d space

This is a problem which has been boggling my mind for the past few weeks. I wanted to click on an object in the viewport and drag my mouse around the screen meanwhile rotating around the point in 3d space.

The technique I settled with was normalizing the camera position, converting it into polar coordinates, add a specific angle to yaw and pitch, and then calculating the new x,y,z co-ordinates from the new polar co-ordinates.

I built the functions below to accomplish this:

func rotateV3AroundPoint(toRotate=Vector3(0,0,0), aroundPoint=Vector3(0,0,0), theta=0, phi=0):
    #Get normalized cartesian co-ordinates
    var p_normalized = toRotate - aroundPoint
    
    #Get p_normalized as polar coordinates
    var p_polar = getPolarCoordinates(p_normalized)
    
    #Increment polar coordinates given theta and phi
    var np_polar = Vector3(p_polar.x,p_polar.y-theta,p_polar.z-phi)
    
    #Translate new polar to cartesian
    var np_normalized = getCartesianCoordinates(np_polar)
    
    #De-normalize polar coordinate
    var np = np_normalized + aroundPoint
    return np

func getCartesianCoordinates(polar3=Vector3()):
    var r      = polar3.x
    var theta  = polar3.y
    var phi    = polar3.z
    var x = r*cos(theta)*sin(phi)
    var y = r*cos(phi)
    var z = r*sin(theta)*sin(phi)
    return Vector3(x,y,z)

func getPolarCoordinates(cartesian3=Vector3()):
    var x = cartesian3.x
    var y = cartesian3.y
    var z = cartesian3.z
    var r = sqrt(pow(x,2)+pow(y,2)+pow(z,2))
    if r==0:
        return Vector3(0,0,0)
    var theta = 0
    if x!=0:
        theta = atan(z/x)+PI if x<0 else 0
    else:
        theta = (PI/2 * (1 if z>=0 else -1))
    var phi  = acos(y/r)
    return Vector3(r,theta,phi)

I ran some tests and it seemed to work (at least for unit directions):

if do_debug_log:
    print(Vector3(1,0,0),"-->",rotateV3AroundPoint(Vector3(1,0,0),Vector3(0,0,0),PI/2))
    print(Vector3(-1,0,0),"-->",rotateV3AroundPoint(Vector3(-1,0,0),Vector3(0,0,0),PI/2))
    print(Vector3(0,0,1),"-->",rotateV3AroundPoint(Vector3(0,0,1),Vector3(0,0,0),PI/2))
    print(Vector3(0,0,-1),"-->",rotateV3AroundPoint(Vector3(0,0,-1),Vector3(0,0,0),PI/2))
    print(Vector3(0,1,0),"-->",rotateV3AroundPoint(Vector3(0,1,0),Vector3(0,0,0),PI/2))
    print(Vector3(0,-1,0),"-->",rotateV3AroundPoint(Vector3(0,-1,0),Vector3(0,0,0),PI/2))

#(1, 0, 0)-->(-0, -0, -1)
#(-1, 0, 0)-->(-0, -0, 1)
#(0, 0, 1)-->(1, -0, 0)
#(0, 0, -1)-->(-1, -0, 0)
#(0, 1, 0)-->(0, 1, 0)
#(0, -1, 0)-->(-0, -1, -0)

So now to actually rotate, in _event() I use the following code:

   if event is InputEventMouseButton:
        if event.button_index == key_rotStart:
            if !self.movement:
                #Rotation initiation
                if !self.rotation_initialised:
                    if do_debug_log:
                        print("ROT_INIT")
                    self.rotation_initialMousePos = event.position
                    self.rotation_initialPos = self.get_translation()
                    self.rotation_initialRotation = self.rotation
                    self.rotation_pivot = self.project_ray_normal(event.position)
                    self.rotation_initialised = true
                
                #This is required to be last, otherwise we initialise twice, which leads to issues.
                #Are we rotating?
                if event.is_pressed():
                    self.rotation_mode = true
                else:
                    self.rotation_mode = false
                    self.rotation_initialised = false

And in _process I use:

if self.rotation_mode:
    #Let camera = (x1,z1) and pivot = (x2,z2)
    #Using rotation matrix:
    # x' = (x2-x1)*cos(theta) - (z2-z1)*sin(theta) + x1
    # y' = (x2-x1)*sin(theta) + (z2-z1)*cos(theta) + z1
    #It is intended that screen width = 360 degree rotation, and screen hight = 180 degree rotation.
    #That is:
    # deltaTheta_{yaw} = dx_{mouse} * 2*PI/Width_{screen}
    # deltaTheta_{pitch} = dy_{mouse} * 2*PI/Hight_{screen}
    
    #Get change in theta required.
    self.rotation_delta = self.rotation_initialMousePos - self.get_viewport().get_mouse_position()
    var theta = (rotation_delta.x * (2*PI) / self.get_viewport().size.x)
    var phi   = (rotation_delta.y * (PI)   / self.get_viewport().size.y)
    
    var p = rotateV3AroundPoint(self.rotation_initialPos,self.rotation_pivot,theta,phi)
    
    if do_debug_log:
        print(self.rotation_initialPos)
        print(self.rotation_pivot)
        print(p)
    
    self.translation.x = p.x
    self.translation.y = p.y
    self.translation.z = p.z
    
    #Point camera towards point:
    self.rotation.y = self.rotation_initialRotation.y+theta
    self.rotation.x = self.rotation_initialRotation.x-phi
    pass

However when testing this code, everything works fine between certain rotations of yaw, but does entirely random stuff with other yaw rotations…

So I really have 2 questions:

  1. Is there an easier method which does the same thing?
  2. What am I actually doing wrong here?
:bust_in_silhouette: Reply From: MysteryGM

1.) Use the Godot transforms.
Or place a empty object at the target point and rotate it; that would be the easy way.

2.) You are using euler rotations that require very specific steps to solve the action. It’s OK for one time calculations but gets very complex for fluent movement like aiming a camera.

Rotations are a extra dimensional thing. For example a 2D rotation on the X and Y axis is actually a 3D rotation around the Z axis.
As such a easy way to get a 3D rotation is to use 4D and rotate the object.
4D rations are know as quaternion rotations; except the whole concept of axis breaks apart at higher dimensions.

Godot has a long spiel about this in it’s document: Using 3D transforms in Godot — Godot Engine (3.0) documentation in English
That could be summarized as “Don’t try it on your own, use our transforms instead”.

They are right of course, you should use transforms.

I actually figured out in the end that

    theta = atan(z/x)+PI if x<0 else 0

can be changed to:

    theta = atan2(x,z)

… and everything works out fine! Something I really wasn’t expecting. I had never come across atan2 till now… so that is interesting.

This being said, I am well up for an easier solution! How exactly does one use transforms? Are there any tutorials? A few weeks ago I remember thinking that transforms were used to move objects… xD I went on to learn they just did funky things… xD But yeah, would be interesting to learn how to use them properly :slight_smile:

Sancarn | 2018-10-07 23:06

Sorry for the very late reply, I don’t always get emails when someone comments.

I had never come across atan2 till now… so that is interesting.

So in the end you went with a 2D rotation, that isn’t a bad idea as long as you remain fixed to the global axis.

I am just going to explain Atan2 for people who don’t know:

When ever you have a angle and you want to convert it to a Vector2 you use Sin(angle) and Cos(angle).

To convert from Sin or Cos back to a angle we use Asin or Acos.

And because Tan = Sin/Cos we can also use Tan to return a angle. The problem is the division, Atan2() solves this by using Pythagoras theorem (that you should know by heart as a developer) to find the radius.

The advantage is that even if Atan2() is slower it will always return a higher precision angle and using any Vector2 ; except (0,0) that has no rotation.

In Godot you can skip all this and use the LookAt() function.
Also Vector2 has Vector2 .angle() that is just return Math::atan2(y, x); but useful for keeping rotations as vectors.

MysteryGM | 2019-01-10 12:54

:bust_in_silhouette: Reply From: goshinbi

This took me a while to figure out so here’s an example of how to use transforms to rotate around a pivot so others can find it when searching.
the pivot_point can just be a fixed point. I’m using a MeshInstance so I can see it when I run the code.

    var x_axis = Vector3(1, 0, 0)
    var pivot_point = get_node("../PivotBall").translation
    # pivot_radius is the vector from the pivot
    # to the starting position of the object being rotated.
    # this is where the object is relative to the pivot before
    # rotation has been applied.
    var pivot_radius = start_position - pivot_point
    # create a transform centered at the pivot
    pivot_transform = Transform(transform.basis, pivot_point)
    # first we rotate our transform.
    # Because the axes are rotated, 
    # translations will happen along those rotated axes.
    # so we can translate it using pivot_radius.
    # the translation moves the object away from the
    # center of the pivot_transform. 
    transform = pivot_transform.rotated(x_axis, delta).translated(pivot_radius)

Can you be more spesific, Can you share the whole script?

Okan Ozdemir | 2020-04-09 18:10