1
\$\begingroup\$

I started working on a small raytracer project, and i decided to reuse my already existing openGL renderer to do this, i'm using GLM to manage transforms/positions.

However, i stumbled upon a very annoying issue: when transforming my rays from screen coordinates to world coordinates, i get very much wrong results. For instance, if i try to transform a vector v(0, 0, 0, 1) from screen to world by doing inverse(viewport * projection * view) * v, i am expecting to retrieve a point at the top left of my camera's near plane, but instead i am getting a point exetremely far away from it.

I don't understand what is happening, since the renderer works very well. I tracked the problem to be either the view() or projection() functions of my camera, but everything looks perfectly fine, i am missing a very important point that i am not aware of.

For reference, the camera class:

#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <sprout_engine/shader.h>
#include "inspectable.h"

using namespace glm;

enum CAMERA_DIR
{
    FORWARD,
    BACKWARD,
    LEFT,
    RIGHT,
    UP,
    DOWN
};

struct Plane
{
    glm::vec3 n;
    float d; // distance from origin to nearest point of the plane

    Plane() = default;
    Plane(const glm::vec3& p, const glm::vec3& norm) : n(glm::normalize(norm)), d(glm::dot(n, p)) {}

    inline float getSignedDistanceToPlane(const glm::vec3& p) const { return glm::dot(n, p) - d; };
};

struct Frustum
{
    Plane topFace;
    Plane bottomFace;

    Plane rightFace;
    Plane leftFace;

    Plane farFace;
    Plane nearFace;
};

const float CAMERA_SPEED = 5.f;
const float CAMERA_SENSITIVITY = .1f;

class Camera : public Inspectable
{
protected:
    glm::vec3 pos;
    glm::vec3 dir;
    glm::vec3 up;
    glm::vec3 worldUp;
    glm::vec3 right;

    float m_zNear;
    float m_zFar;
    float m_fov;
    float m_aspectRatio;

    float pitch;
    float yaw;

    glm::mat4 m_view{};
    glm::mat4 m_projection{};

    Frustum m_frustum;

    void update_dir();

public:
    Camera();
    Camera(const glm::vec3 &pos, const glm::vec3 &up, float pitch, float yaw, float p_znear, float p_zfar, float p_fov, float p_aspectRatio);

    void drawInspector() override;

    [[nodiscard]] glm::mat4 view() const;
    [[nodiscard]] glm::mat4 projection() const;

    [[nodiscard]] inline glm::vec3 get_position() const { return pos; };
    [[nodiscard]] inline Frustum getFrustum() const { return m_frustum; };

    void setZNear(float mZNear);
    void setZFar(float mZFar);
    void setFov(float mFov);
    void setAspectRatio(float mAspectRatio);

    void updateView();
    void updateProjection();
    void updateFrustum();

    void process_input(CAMERA_DIR direction, float delta_time);

    void process_mouse_movement(float xoffset, float yoffset);
};

and its implementation:

//
// Created by Bellaedris on 28/04/2024.
//

#include "camera.h"
#include "imgui/imgui.h"

void Camera::update_dir() {
    vec3 new_dir;
    float yawRad = radians(yaw);
    float pitchRad = radians(pitch);
    new_dir.x = std::cos(yawRad) * std::cos(pitchRad);
    new_dir.y = std::sin(pitchRad);
    new_dir.z = std::sin(yawRad) * std::cos(pitchRad);

    dir = normalize(new_dir);
    right = normalize(cross(dir, worldUp));
    up = normalize(cross(right, dir));

    updateView();
}

Camera::Camera()
        : dir(vec3(0., 0., -1.)), pitch(0.), yaw(0.)
{
    pos = vec3(0., 0., 0.);
    worldUp = vec3(0., 1., 0.);

    right = normalize(cross(dir, worldUp));
}

Camera::Camera(const vec3 &pos, const vec3 &up, float pitch, float yaw, float p_znear, float p_zfar, float p_fov,
               float p_aspectRatio)
        : dir(vec3(0., 0., -1.)), pos(pos), worldUp(up), up(up), pitch(pitch), yaw(yaw), m_zNear(p_znear), m_zFar(p_zfar), m_fov(p_fov), m_aspectRatio(p_aspectRatio)
{
    right = normalize(cross(dir, up));
    update_dir();

    updateView();
    updateProjection();
    updateFrustum();
}

glm::mat4 Camera::view() const {
    return m_view;
}

glm::mat4 Camera::projection() const {
    return m_projection;
}

void Camera::setZNear(float mZNear) {
    m_zNear = mZNear;
    updateProjection();
}

void Camera::setZFar(float mZFar) {
    m_zFar = mZFar;
    updateProjection();
}

void Camera::setFov(float mFov) {
    m_fov = mFov;
    updateProjection();
}

void Camera::setAspectRatio(float mAspectRatio) {
    m_aspectRatio = mAspectRatio;
    updateProjection();
}

void Camera::updateView() {
    m_view = glm::lookAt(pos, pos + dir, up);
    updateFrustum();
}

void Camera::updateProjection() {
    m_projection = glm::perspective(glm::radians(m_fov), m_aspectRatio, m_zNear, m_zFar);
    updateFrustum();
}

void Camera::updateFrustum() {
    Frustum frustum;
    float halfVSide = std::tan(glm::radians(m_fov) * .5f) * m_zFar; // find the half height of the far plane with trigo
    float halfHSide = halfVSide * m_aspectRatio; // aspect = w / h
    vec3 farPlaneCenter = m_zFar * dir;

    frustum.farFace = { pos + farPlaneCenter, -dir };
    frustum.nearFace = { pos + m_zNear * dir, dir };

    frustum.rightFace = { pos , cross(farPlaneCenter - right * halfHSide, up) };
    frustum.leftFace = { pos , cross(up, farPlaneCenter + right * halfHSide) };

    frustum.topFace = { pos , cross(right, farPlaneCenter - up * halfVSide) };
    frustum.bottomFace = { pos , cross(farPlaneCenter + up * halfVSide, right) };

    m_frustum = frustum;
}

void Camera::process_input(CAMERA_DIR direction, float delta_time) {
    float velocity = CAMERA_SPEED * delta_time;
    switch (direction)
    {
        case FORWARD:
            pos += dir * velocity;
            break;
        case BACKWARD:
            pos -= dir * velocity;
            break;
        case LEFT:
            pos -= right * velocity;
            break;
        case RIGHT:
            pos += right * velocity;
            break;
        case UP:
            pos += up * velocity;
            break;
        case DOWN:
            pos -= up * velocity;
            break;
    }
    update_dir();
}

void Camera::process_mouse_movement(float xoffset, float yoffset) {
    xoffset = std::abs(xoffset) <= 1.f ? 0.f : xoffset;
    yoffset = std::abs(yoffset) <= 1.f ? 0.f : yoffset;

    xoffset *= CAMERA_SENSITIVITY;
    yoffset *= CAMERA_SENSITIVITY;

    yaw += xoffset;
    pitch += yoffset;

    // to avoid the lookAt matrix to flip
    if (pitch > 89.f)
        pitch = 89.f;
    if (pitch < -89.f)
        pitch = -89.f;

    update_dir();
}

void Camera::drawInspector() {
    if(ImGui::TreeNode("Camera"))
    {
        if(ImGui::InputFloat3("Position", glm::value_ptr(pos)))
        {
            update_dir();
        }
        if (ImGui::InputFloat("Pitch", &pitch))
        {
            update_dir();
        }
        if (ImGui::InputFloat("Yaw", &yaw))
        {
            update_dir();
        }
        if (ImGui::InputFloat("FoV", &m_fov))
        {
            updateProjection();
        }
        ImGui::TreePop();
    }
}

The function that gives me the viewport matrix (m_width and m_height are the dimensions of the window)

glm::mat4 SproutApp::viewport() const {
    float w = (float)m_width / 2.f;
    float h = (float)m_height / 2.f;

    return {
            w,  0.,  0.,  0,
            0,  h,  0,   0,
            0., 0., .5f, 0,
            w,  h,  .5f,   1
            };
}

And the piece of code that produces wrong transformation:

m_model = Model(resources_path + "models/cornell-box.obj");
        cam = new Camera({0, 2, 5}, {0, 1, 0}, 0, -90.f, 0.1f, 100.f, 70.f, (float)width() / (float)height());
        m_shader = Shader("texture.vs", "texture.fs");
        m_debugShader = Shader("default.vs", "default.fs");

        m_lines = std::make_unique<LineRenderer>(cam);

        glm::mat4 camToWorld = glm::inverse(cam->view());
        glm::mat4 screenToWorld = glm::inverse(viewport() * cam->projection() * cam->view());

        m_lines->addLine({screenToWorld * glm::vec4(0, 0, 0, 1)}, {screenToWorld * glm::vec4(0, 0, 1, 1)});
        m_lines->addLine({screenToWorld * glm::vec4(width(), 0, 0, 1)}, {screenToWorld * glm::vec4(width(), 0, 1, 1)});
        m_lines->addLine({screenToWorld * glm::vec4(0, height(), 0, 1)}, {screenToWorld * glm::vec4(0, height(), 1, 1)});
        m_lines->addLine({screenToWorld * glm::vec4(width(), height(), 0, 1)}, {screenToWorld * glm::vec4(width(), height(), 1, 1)});
```
\$\endgroup\$
2
  • \$\begingroup\$ why do you have a viewport, view and projeciton matrix? what is the viewport supposed to be? You only need the view (world to camera) and the projection (viewspace to clip space) \$\endgroup\$
    – Raildex
    Commented Jun 18 at 8:00
  • \$\begingroup\$ Viewport converts clip space to screen space. I use it because i need to send a ray per pixel of the screen, if my viewport's dimension are x*y, i think it is convenient to be able to loop over x and y when sending my rays. It would indeed be possible to just use the clipspace, but i think it is less clear and my method has no overhead whatsoever (the viewport matrix is computed only once). \$\endgroup\$
    – Bellaedris
    Commented Jun 18 at 8:15

0

You must log in to answer this question.

Browse other questions tagged .