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):

#pragma once

#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <GLFW/glfw3.h>

class CameraControl;

class Camera 
    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;

    void updateViewMatrix();
    void updateProjectionMatrix();

    CameraControl *control;

    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 {
    float orbitSensitivity = 0.2f;
    float zoomSensitivity = 0.0025f;

    Camera *camera;

    bool firstMouse;
    float lastMouseX;
    float lastMouseY;

    float distance;
    float pitch;
    float yaw;

    CameraControl(Camera *camera);
    void handleInput(GLFWwindow *window);


#include "Camera.hpp"

Camera::Camera() {
    control = new CameraControl(this);

Camera::~Camera() {
    delete control;

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

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

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

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));

Thank you for your attention! Cheers!

  • $\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$ – Daniel M Gessel Feb 11 at 19:00

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}$.

| improve this answer | |
  • $\begingroup$ How does this solution compare to a quaternion-based implementation? What are the differences in performance/limitations between the two? $\endgroup$ – Daniel Marques Sep 12 '19 at 14:03
  • 1
    $\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 Sep 12 '19 at 14:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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