Skip to content

File camera.hpp

File List > gui > camera.hpp

Go to the documentation of this file

#pragma once

#include <QMatrix4x4>

#include "utilities/coordinate.hpp"

inline double deg2rad(double deg) { return deg * M_PI / 180.0f; }

inline double rad2deg(double rad) { return rad * 180.0f / M_PI; }

class Camera {
  QVector3D m_position;
  QVector3D m_direction;
  QVector3D m_up;

  Coordinate3D<double> m_world_offset;

  int m_width;
  int m_height;

  double m_fov = 45.0f;

 public:
  Camera(int width, int height)
      : m_position(1, 1, 1),
        m_direction(-1, -1, -1),
        m_up(0, 0, 1),
        m_width(width),
        m_height(height) {}

  Camera(const QVector3D& position, const QVector3D& direction, const QVector3D& up)
      : m_position(position), m_direction(direction), m_up(up) {}

  void move(const QVector3D& direction) { m_position += direction; }
  void move_towards(const QVector3D& world_pos, double distance, bool shrink_direction = false) {
    QVector3D direction = world_pos - m_position;
    m_position += direction.normalized() * distance;
    if (shrink_direction) {
      if (distance < 0 || (m_direction.length() > 1 && m_direction.length() > distance))
        m_direction -= m_direction.normalized() * distance;
      else
        m_direction = m_direction.normalized();
    }
  }

  const Coordinate3D<double>& world_offset() const { return m_world_offset; }
  Coordinate3D<double>& world_offset() { return m_world_offset; }

  void set_screen_size(int width, int height) {
    m_width = width;
    m_height = height;
  }

  void reset_to_origin() {
    m_position = QVector3D(1, 1, 1);
    m_direction = QVector3D(-1, -1, -1);
    m_up = QVector3D(0, 0, 1);
  }

  QVector3D view_right() const { return QVector3D::crossProduct(m_up, m_direction).normalized(); }
  QVector3D view_up() const {
    return QVector3D::crossProduct(m_direction, view_right()).normalized();
  }
  QVector3D planar_direction() const {
    return QVector3D(m_direction.x(), m_direction.y(), 0).normalized();
  }

  void pan(double dx, double dy) {
    m_position += (view_right() * dx + view_up() * dy) * m_direction.length();
  }

  void fly(double dx, double dy, double dz) {
    m_position += (planar_direction() * dx + view_right() * dy + m_up * dz) * m_direction.length();
  }

  void zoom_to_fit(const Extent3D& extent) {
    // Coordinate3D<double> center = extent.center();
    double max_extent = extent.max_extent();
    QVector3D qcenter(extent.maxx + extent.minx, extent.maxy + extent.miny,
                      extent.maxz + extent.minz);
    qcenter /= 2;
    m_position = qcenter - 10 * m_direction.normalized() * max_extent;

    QMatrix4x4 proj = proj_matrix();

    float zoom_out_amount = 0.0f;
    for (size_t i = 0; i < 2; i++) {
      for (size_t j = 0; j < 2; j++) {
        for (size_t k = 0; k < 2; k++) {
          QVector3D corner(i == 0 ? 0 : extent.maxx - extent.minx,
                           j == 0 ? 0 : extent.maxy - extent.miny,
                           k == 0 ? 0 : extent.maxz - extent.minz);
          QVector3D screen_pos = proj.map(corner);
          zoom_out_amount = std::max(zoom_out_amount,
                                     std::max(std::abs(screen_pos.x()), std::abs(screen_pos.y())));
        }
      }
    }

    m_position = qcenter - m_direction.normalized() * 10 * max_extent * zoom_out_amount;
    m_direction = qcenter - m_position;
    std::cout << "New position: " << m_position.x() << " " << m_position.y() << " "
              << m_position.z() << std::endl;
    std::cout << "New direction: " << m_direction.x() << " " << m_direction.y() << " "
              << m_direction.z() << std::endl;
  }

  void rotate_view(double dx, double dy) { rotate_around_center(dx, dy, m_position); }

  double current_altitude_angle() const {
    return std::asin(m_direction.z() / m_direction.length());
  }

  double projection_scale() const { return m_height / (2.0f * std::tan(deg2rad(m_fov) / 2)); }

 private:
  double bound_rotation(double current_angle, double angle, double min_angle, double max_angle) {
    std::cout << "Bounding rotation: " << current_angle << " " << angle << " " << min_angle << " "
              << max_angle << std::endl;
    if (current_angle + angle > max_angle) {
      angle = max_angle - current_angle;
    } else if (current_angle + angle < min_angle) {
      angle = min_angle - current_angle;
    }
    return angle;
  }

 public:
  void rotate_around_center(double dx, double dy,
                            const std::optional<QVector3D>& center = std::nullopt) {
    QVector3D cor = center.value_or(m_position + m_direction);

    QMatrix4x4 rotation;
    rotation.rotate(-dx, m_up);
    double angle_to_rotate =
        bound_rotation(rad2deg(current_altitude_angle()), -dy, -90 + 1e-2, 90 - 1e-2);
    rotation.rotate(-angle_to_rotate, view_right());
    m_direction = rotation.map(m_direction);
    m_position = cor - (m_position - cor).length() * m_direction.normalized();
  }

  QMatrix4x4 proj_matrix() const {
    QMatrix4x4 proj;
    proj.perspective(m_fov, (double)m_width / m_height, 1e-2f, 1e5f);
    proj.lookAt(m_position, m_position + m_direction, m_up);
    return proj;
  }

  QVector3D unproject(const QPointF& screen_pos) const {
    QVector3D screen(screen_pos.x(), m_height - screen_pos.y(), 0);
    return screen.unproject(proj_matrix(), QMatrix4x4(), QRect(0, 0, m_width, m_height));
  }

  const QVector3D& position() const { return m_position; }
  const QVector3D& direction() const { return m_direction; }
  const QVector3D& up() const { return m_up; }
};