2
$\begingroup$

I've developed a simple (Third-Person) Perspective Camera with Orbit controls.

However, my implementation doesn't handle the case where if the pitch goes over 90º it flips upside down. From what I've learned, the reason behind it has to do with using the Look-At function.

My questions are:

  • Is the problem really the Look-at function? How would I fix this, without restricting the camera's pitch?
  • I want to develop a camera class that is control agnostic (can be fly, orbit, ...) and therefore not take into account that it's first/third-person. Are there methods on how to develop such a camera abstraction?
  • I also want it to be able to roll, can the advised solution for the last question support it?
  • I haven't delved into quaternions yet, is the solution based on them? If so, are there also limitations to an implementation like that?

I'm basically looking for theory on developing proper robust cameras (with as few limitations as possible), so if you could point me into the right direction it would be nice, but a full answer is also very much appreciated. Code is not essential, but it would be also very helpful to see a proper (yet simple) implementation.

My camera implementation (in Vulkan):
Camera.hpp:

#pragma once

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <GLFW/glfw3.h>


class CameraControl;

class Camera 
{
private:
    glm::vec3 position = glm::vec3(0.0f, 0.0f, 1.0f);
    glm::vec3 target = glm::vec3(0.0f, 0.0f, 0.0f);

    glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);

    float fov = glm::radians(45.0f);
    float aspectRatio = 1.0f;
    float near = 0.1f;
    float far = 1000.0f;

    glm::mat4 view;
    glm::mat4 projection;

private:
    void updateViewMatrix();
    void updateProjectionMatrix();

public:
    CameraControl *control;

public:
    Camera();
    ~Camera();
    void setPosition(glm::vec3 position);
    void setTarget(glm::vec3 target);
    void setPerspective(float fov, float aspectRatio, float near, float far);

    glm::mat4 inline getView() { return view; };
    glm::mat4 inline getProjection() { return projection; };
    glm::vec3 inline getPosition() { return position; };
    glm::vec3 inline getTarget() { return target; };
};


class CameraControl {
public:
    float orbitSensitivity = 0.2f;
    float zoomSensitivity = 0.0025f;

private:
    Camera *camera;

    bool firstMouse;
    float lastMouseX;
    float lastMouseY;

    float distance;
    float pitch;
    float yaw;

public:
    CameraControl(Camera *camera);
    void handleInput(GLFWwindow *window);
};

Camera.cpp:

#include "Camera.hpp"

Camera::Camera() {
    control = new CameraControl(this);
    updateViewMatrix();
    updateProjectionMatrix();
}

Camera::~Camera() {
    delete control;
}

void Camera::setPosition(glm::vec3 position) {
    this->position = position;
    updateViewMatrix();
}

void Camera::setTarget(glm::vec3 target) {
    this->target = target;
    updateViewMatrix();
}

void Camera::setPerspective(float fov, float aspectRatio, float near, float far) {
    this->fov = fov;
    this->aspectRatio = aspectRatio;
    this->near = near;
    this->far = far;
    updateProjectionMatrix();
}

void Camera::updateViewMatrix() {
    view = glm::lookAt(position, target, up);
}

void Camera::updateProjectionMatrix() {
    projection = glm::perspective(fov, aspectRatio, near, far);
    projection[1][1] *= -1; // Vulkan coordinate system fix
}


CameraControl::CameraControl(Camera* camera) {
    this->camera = camera;
    firstMouse = true;
    distance = 1.0f;
    pitch = 0.0f;
    yaw = 180.0f;
}

void CameraControl::handleInput(GLFWwindow *window) {
    double xpos, ypos;
    glfwGetCursorPos(window, &xpos, &ypos);

    if (firstMouse) {
        lastMouseX = xpos;
        lastMouseY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastMouseX;
    float yoffset = lastMouseY - ypos;

    lastMouseX = xpos;
    lastMouseY = ypos;

    if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT)) {
        pitch -= yoffset * orbitSensitivity;
        yaw -= xoffset * orbitSensitivity;
    }

    if(glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT)) {
        distance += yoffset * zoomSensitivity;
        distance = glm::max(distance, 0.0f);
    }

    float verticalDistance = distance * glm::sin(glm::radians(pitch));
    float horizontalDistance = distance * glm::cos(glm::radians(pitch));

    glm::vec3 position;
    position.x = camera->getTarget().x - horizontalDistance * glm::sin(glm::radians(yaw));
    position.y = camera->getTarget().y + verticalDistance;
    position.z = camera->getTarget().z - horizontalDistance * glm::cos(glm::radians(yaw));
    camera->setPosition(position);
}

Thank you for your attention! Cheers!

$\endgroup$
4
  • $\begingroup$ The problem isn’t lookat (though you could just use the inverse of your camera transform) but that your up is fixed to the y axis. When the pitch crosses +/-90, the fixed up direction causes the camera to rotate 180. You could rotate your up vector, first by pitch then by yaw, just as you transform the relative position of the camera, if I’m understanding your code correctly. $\endgroup$
    – user2500
    Commented Feb 11, 2020 at 19:00
  • $\begingroup$ Do you want to have a camera which can be controlled like an airplane? What I mean is: It can roll pitch yaw? That is quite simple: your camera has a projection and a view-matrix. In this case we need to change the view-matrix. By calculating viewMatrix = viewMatrix * rotationMatrix where rotationMatrix can be X-Rotation (pitch), Y-Rotation (yaw) and Z-Rotation (roll). With this solution you only need the lookat method to initialize the camera. At the rest of the time you only need to multiply a rotation onto it. $\endgroup$
    – Thomas
    Commented Oct 8, 2020 at 9:36
  • $\begingroup$ Changing the up axis when the camera passes a preset angle (like 89 degrees) sides steps the issues all together. Changing the axis at 89 degrees means the new axis is only 1 degree off axis....when it gets to 89 degrees do it again. such as y->x then x->y. This is easy to do with a camera that has roll and pitch ( or any combination of two angles of rotation) but for a camera that has roll, pitch and yaw with 360 degrees of freedom in all directions a quaternion camera is just easier to implement and maintain (but is the hardest to understand) $\endgroup$
    – pmw1234
    Commented Oct 1, 2021 at 22:20
  • $\begingroup$ I will say that once you warp your mind around a quaternion based camera, everything quaternion is suddenly easier to understand. The sandwich product is your friend. $\endgroup$
    – pmw1234
    Commented Oct 1, 2021 at 22:26

1 Answer 1

1
$\begingroup$

Attach your camera to some pivot (in a transformation hierarchical sense). Rotate the pivot rather than the camera (do not use look at). It boils down to using $T_{pivot} R_{pivot} T_{cam} R_{cam}$. Where $T_{pivot}$ is a translation matrix derived from the position of the pivot in world space, $R_{pivot}$ is the rotation matrix of the pivot (what you want), $T_{cam}$ is a translation matrix derived from the position of the camera in the pivot's coord system, similarly for $R_{cam}$.

$\endgroup$
2
  • $\begingroup$ How does this solution compare to a quaternion-based implementation? What are the differences in performance/limitations between the two? $\endgroup$ Commented Sep 12, 2019 at 14:03
  • 2
    $\begingroup$ @DanielMarques They are interchangeable - you can transform between the two representations depending on your needs. As far as performance goes, here is a detailed break-down: geometrictools.com/Documentation/RotationIssues.pdf $\endgroup$
    – lightxbulb
    Commented Sep 12, 2019 at 14:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.