Why is my FPS camera rolling, once and for all?

2019-07-21 16:09发布

问题:

If I ignore the sordid details of quaternions algebra I think I understand the maths behind rotation and translation transformations. But still fail to understand what I am doing wrong.

Why is my camera rolling once and for all!? :)

And to be a bit more specific, how should I compute the camera View matrix from its orientation (rotation matrix)?

I am writing a minimalistic 3d engine in Python with a scene Node class that handles the mechanics of rotation and translation of 3d objects. It has methods that expose the Rotation and Translation matrices as well as the Model matrix.

There is also a CameraNode class, a subclass of Node, that also exposes the View and Projection matrices (projection is not the problem, so we can ignore it).

Model Matrix

In order to correctly apply the transformations I multiply the matrices as follows:

PxVxM x v

i.e first the Model, then the View and finally the Projection.

Where M is computed by first applying the rotation and then the translation:

M = TxR

Here's the code:

class Node():
    # ...

    def model_mat(self):
        return self.translation_mat() @ self.rotation_mat() @ self.scaling_mat()

    def translation_mat(self):
        translation = np.eye(4, dtype=np.float32)
        translation[:-1, -1] = self.position  # self.position is an ndarray
        return translation

    def rotation_mat(self):
        rotation = np.eye(4, dtype=np.float32)
        rotation[:-1, :-1] = qua.as_rotation_matrix(self.orientation)  # self.orientation is a quaternion object
        return rotation

View Matrix

I am computing the View matrix based on the camera position and orientation, as follows:

class CameraNode(Node):
    # ...

    def view_mat(self):
        trans = self.translation_mat()
        rot = self.rotation_mat()
        trans[:-1, 3] = -trans[:-1, 3]  # <-- efficient matrix inversion
        rot = rot.T                     # <-- efficient matrix inversion
        self.view = rot @ trans
        return self.view

Please correct me if I am wrong. Since we can only move and rotate the world geometry (as opposed to moving/rotating the camera) I have to multiply the matrices in the reverse order and also the oposite transformation (effectively the inverse of each transformation matrix). In other words, moving the camera away from an object can also be seen as moving the object away from the camera.

Input to rotation

Now, here's how I convert keyboard input into camera rotation. When I press the right/left/up/down arrow keys I am calling the following methods with some pitch/yaw angle:

def rotate_in_xx(self, pitch):
    rot = qua.from_rotation_vector((pitch, 0.0, 0.0))
    self.orientation *= rot

def rotate_in_yy(self, yaw):
    rot = qua.from_rotation_vector((0.0, yaw, 0.0))
    self.orientation *= rot

Behaves wrong but rotation matrix is correct

And this is what I get:

Now, confusingly, if I change the above methods to:

class CameraNode(Node):

    def view_mat(self):
        view = np.eye(4)
        trans = self.translation_mat()
        rot = self.rotation_mat()
        trans[:-1, 3] = -trans[:-1, 3]
        # rot = rot.T                     # <-- COMMENTED OUT
        self.view = rot @ trans
        return self.view

    def rotate_in_xx(self, pitch):
        rot = qua.from_rotation_vector((pitch, 0.0, 0.0))
        self.orientation = rot * self.orientation  # <-- CHANGE

I can make the camera behave correctly as an FPS camera, but the rotation matrix does not seem right.

Please could someone shed some light? Thanks in advance.

回答1:

In my last answer to your issue, I told you why it's not a good idea to reuse your view matrix, because pitching and yawing don't commute. You're using quaternions now, but again, pitch and yaw quaternions don't commute. Just store the pitch value and the yaw value, and recalculate the orientation from pitch and yaw whenever you need it.

def rotate_in_xx(self, pitch):
    self.pitch += pitch

def rotate_in_yy(self, yaw):
    self.yaw += yaw

def get_orientation():
    pitch_rotation = qua.from_rotation_vector((self.pitch, 0.0, 0.0))
    yaw_rotation = qua.from_rotation_vector((0.0, self.yaw, 0.0))
    return yaw_rotation * pitch_rotation

A note on how in your last screenshot the camera rotation matrix and object rotation matrix aren't identical: The object rotation and translation matrices (together the model matrix) describe the transformation from object coordinates to world coordinates, while the view matrix describes the transformation from world coordinates to camera coordinates.

So in order for the tripod to be displayed axis-aligned relative to your viewport, then the view rotation matrix must be the inverse of the model rotation matrix.



回答2:

You should not accumulate all your Euler angle in one rotationMatrix, directionVector or Quaternion.

Doing stuff like:

vec3 euler = vec3(yaw, pitch, roll);
rot *= quaternion(euler)
or
self.orientation = quaternion(euler) * self.orientation

Each frame you add a rotation to an already existing rotation stored in your structure.

float deltaYaw = getInput();
float deltaPitch = getInput();
m_rotation = m_rotation * euler(deltaYaw, deltaRoll, 0.0f) 
is not equal to
m_rotation = rotationMatrix(m_yaw + deltaYaw, m_pitch + deltaRoll, 0.0f);

In one case you rotate an already rotated object with his new 3D frame by deltaYaw. The yaw you apply now will take in to account the roll you did previusly.

oldYaw * oldRoll * deltaYaw != (oldYaw * deltaYaw) * oldRoll

On the other you build the rotation from your mesh position to the requested euler angles.

Yes, you are right it's not a convenient way of handling camera, keeping yaw, pitch and roll in variables will lead to issues later on (gimBall lock, camera animation will get tricky ...). I would recommend to look at arcBall camera https://en.wikibooks.org/wiki/OpenGL_Programming/Modern_OpenGL_Tutorial_Arcball