Camera Movement Using Quaternions

Okay, so I’ve been trying to wrap my head around this, but the more I think about it, the more I get a headache. Even using https://eater.net/quaternions/video/intro to help me it’s still difficult. Making the camera move up and down and around, or in other words pitch and turn, is a rather simple matter, but trying to make the camera turn after raising the camera, aka turning it about the i axis and then about some other axis, results in movement I don’t want. Heres the relevant code I have so far.

if amount_turn_will_change != 0 and amount_pitch_will_change != 0:
  total_degrees = sqrt(self.turn_radians ** 2 + self.pitch_radians ** 2)
  
elif amount_pitch_will_change != 0:
  
  if self.pitch_radians != pi:
    self.pitch = (-sin(self.pitch_radians % pi / 2))
    self.__past_pitch = self.pitch
    
  else:
    self.pitch = -sin(pi / 2)
    self.__past_pitch = self.pitch
  
  real_compontent = cos(self.pitch_radians / 2)
  
  if self.__past_turn != 0:
    self.quaternion = (real_compontent, 1 * self.pitch, 0, self.__past_turn * self.pitch)
    
  else:
    self.quaternion = (real_compontent, 1 * self.pitch, 0, 0)
  
elif amount_turn_will_change != 0:
  self.turn = (sin(self.turn_radians / 2))
  self.__past_turn = self.turn
  real_compontent = cos(self.turn_radians / 2)
  
  if self.__past_pitch != 0:
    i_coefficient = 1 - self.__past_pitch
    k_coefficient = self.__past_pitch
    pitch = k_coefficient * self.turn
    turn = i_coefficient * self.turn
    self.quaternion = (real_compontent, pitch, 0, turn)
    
  else:
    self.quaternion = (real_compontent, 0, 0, 1 * self.turn)

If anyone has any suggestions as to how I can move forward I greatly appreciate them.

Not sure if it’ll help you, but you can go about it another way using two NodePaths to achieve gimbal lock free rotation. One to keep track of the accumulated rotation and the other to get the needed Quat for the local rotation. To rotate you could even use HPR if you like (I had less trouble wrapping my head around it this way):

  1. Rotate the nested NodePath the way you want it to rotate
  2. Do something like OrientationNP.setQuat(RotationNP.getQuat(self.render))
  3. Reset the rotation NodePath to it’s neutral state (e.g. RotationNP.setHpr(0))
  4. Repeat

Hope this helps

Edit:
What also helped me to achieve some sensible camera movement, was to imagine I had a virtual selfie stick, constructed out of 2-3 nested NodePaths. One that would be your Hand, holding it and rotating it around yourself (what the camera is looking at…) a nested one (or two) that handle zoom as in how far away it is and if using 3 another nested one, where for example pitch of the camera would be adjusted…

Is there a difference between keeping the local and accumulated Quat in simple variables versus in two nodes? Originally I was thinking of getting all of the axes involved because it would result in what I want, but I don’t know how I would get them to change in a consistent and reasonable manner. So, I’ll give what you’re saying a try, though I don’t understand how two nodes solves the problem.

I just noticed that the code I started with from a tutorial had the node path as render/Camera orbit/Camera pitch. Would I use this form to make render/OrientationNP/RotationNP?

Instead of using multiple nodes, you can just combine (multiply) quaternions and/or rotation matrices. In any case, there really shouldn’t be any need for all that low-level math.

Perhaps I’ll write up some example code later, when I have some more time.

If I understand correctly, the problem you’re having is with the order in which rotation components are applied to form the resulting orientation; in Panda3D this order is always:

  1. roll (angle about the Y-axis);
  2. pitch (angle about the X-axis);
  3. heading (angle about the Z-axis, aka “yaw” or what you call “turn”).

(This actually makes me wonder if the methods used to set/get an orientation in Panda3D shouldn’t have been called set_rph/get_rph instead of set_hpr/get_hpr.)

If you want different behaviour, e.g. after setting a pitch of 30 degrees, you would like to be able to turn about a local Z-axis that is tilted 30 degrees relative to the world, then you can create a quaternion which represents a local turn and multiply that by a quaternion which represents that pitch of 30 degrees:

pitch = 30.
heading = 40.
pitch_quat = Quat()
pitch_quat.set_hpr((0., pitch, 0.))
turn_quat = Quat()
turn_quat.set_hpr((heading, 0., 0.))
quat = turn_quat * pitch_quat

To visualize the difference between the resulting rotation using the above code and a call to set_hpr(pitch, heading, 0.) with a constant pitch and changing heading, imagine a straight line (which makes an angle of 30 degrees with the world Z-axis) going through the node you’re rotating; calling set_hpr would make the rotation of the line result in a cone, while the quaternion code would make it result in a plane whose normal points in the direction of the tilted local Z-axis.

Try the following example:

from panda3d.core import *
from direct.showbase.ShowBase import ShowBase
from math import pi, sin, cos


class MyApp(ShowBase):

    def __init__(self):

        ShowBase.__init__(self)

        self.model = self.loader.load_model("smiley")
        self.model.reparent_to(self.render)
        self.model.set_pos(0., 10., .5)
        self.pitch = 50.
        self.heading = 0.
        self.heading_first = True
        self.accept("t", self.toggle_rotation_order)
        self.task_mgr.add(self.rotate_model, "rotate_model")

    def toggle_rotation_order(self):

        self.heading_first = not self.heading_first

    def rotate_model(self, task):

        self.heading += 1.
        pitch_quat = Quat()
        pitch_quat.set_hpr((0., self.pitch, 0.))
        turn_quat = Quat()
        turn_quat.set_hpr((self.heading, 0., 0.))
        quat = turn_quat * pitch_quat

        if self.heading_first:
            self.model.set_quat(quat)
        else:
            self.model.set_hpr(self.heading, self.pitch, 0.)

        return task.cont


app = MyApp()
app.run()

When running this code, press T on the keyboard to toggle the order of rotation components.

Hope this helps.

Okay, so I just figured out how to make the camera rotate after being pitched up, it’s actually really simple mathwise.

if self.__past_pitch != 0:
    
    real_compontent\
    = self.__past_real_component\
    * cos(-self.__past_pitch * self.turn_radians)
  
    i_component\
    = self.__past_pitch\
    * cos(-self.__past_pitch * self.turn_radians)
  
    j_component\
    = self.__past_pitch\
    * sin(-self.__past_pitch * self.turn_radians)
  
    k_component\
    = self.__past_real_component\
    * sin(-self.__past_pitch * self.turn_radians)

    self.quaternion\
    = (real_compontent, i_component, j_component, k_component)
    
  else:
    real_compontent = cos(self.turn_radians / 2)
    self.__past_real_component = real_compontent
    self.turn = (sin(self.turn_radians / 2))
    self.__past_turn = self.turn
    self.quaternion = (real_compontent, 0, 0, self.turn)

How did I figure this out? Simple, I used that aid I mentioned in my first post and plotted the points on a graph and it turned out they all did have harmonic motion and even more interesting than that is that the period is based on the negative value of the i compontent. I think the hiccup is due to the fact that the real component, and probably the others too, goes from trending to 0 to jumping all the way up to 1 or changes signs.

Edit 0: I just figure out what the hiccup was caused by. I got rid of this line:

self.turn_degrees = self.turn_degrees % 360

now it works without that problem.

Edit 1: I realized that 360 is the period, 2pi. So, doing this:

self.__period = (2 / self.pitch) * 180

then in another function:

self.turn_degrees = self.turn_degrees % self.__period

Thank you for taking time out of your day for the code example, but it wasn’t quite what I looking for. I found a way that mostly works though, at least for camera orbit.