Рубрики
3D мир Программирование

Реализация камеры для приложений с трехмерной графикой на языке C++

В данной заметке описывается реализация класса камеры на языке С++ с использованием библиотеки GLM.

Введение

С теоретической частью устройства камеры можно ознакомиться в соответствующей заметке. Данная реализация не зависит от используемой библиотеки рендера (OpenGL, Vulkan API, DirectX) и лишь предоставляет необходимые матрицы.

Данную заметку можно разделить на следующие этапы:

Сборка статической библиотеки

Класс камеры будет представлен в виде статической библиотеки, которую можно легко подключить к любому проекту. Использование как статической библиотеки не обязательно — можно просто использовать файл Camera.cpp в общей сборке проекта.

Файл tasks.json будет иметь следующий вид:

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "g++",
            "command": "C:/MinGW/bin/g++.exe",
            "args": [
                "-c",
                "${workspaceRoot}/src/*.cpp",
                "-I${workspaceRoot}/include",
                "--std=c++11",
                "-I${workspaceRoot}/../dependencies/glm",
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Задача создана отладчиком."
        },
        {
            "type": "cppbuild",
            "label": "ar",
            "command": "C:/MinGW/bin/ar.exe",
            "args": [
                "rcs",
                "${workspaceRoot}/lib${workspaceFolderBasename}.a",
                "${workspaceRoot}/.vscode/*.o"
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "detail": "Задача создана отладчиком."
        },
        {
            "label": "Сборка статической библиотеки",
            "dependsOn": [
                "g++",
                "ar"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ],
    "version": "2.0.0"
}

Для пользователя доступны три задачи сборки:

  • «g++» — сборка объектных файлов в каталоге .vscode;
  • «ar» — компоновка объектных файлов в статическую библиотеку;
  • «Сборка статической библиотеки» — вызывает поочередно две предыдущих задачи.

Макро-константы

Для обеспечения простоты работы с некоторыми значениями зададим их с помощью константных значений:

// Ближняя граница области отсечения
#define CAMERA_NEAR 0.1f
// Дальняя граница области отсечения
#define CAMERA_FAR 100.0f
// Вектор, задающий верх для камеры
#define CAMERA_UP_VECTOR glm::vec3(0.0f, 1.0f, 0.0f)
// Стандартный угол обзора 
#define CAMERA_FOVy 45.0f

Поля класса

Данный класс следующие поля:

  • position (glm::vec3) — местоположение камеры;
  • target (glm::vec3) — точка или цель, на которую смотрит камера;
  • currentRotation (glm::vec2) — текущий поворот камеры;
  • projection (glm::mat4) — матрица проекции;
  • view (glm::mat4) матрица вида;
  • requiredRecalcView (bool) — определяет необходимость пересчета матрицы вида камеры;
  • sensitivity (float) — чувствительность мыши.

Поля target и currentRotation связаны между собой: первое поле указывает куда на момент пересчета матрицы вида смотрит камера, а второе используется для хранения положения курсора мыши и последующего изменения с учетом чувствительности в соответствующем методе.

Поле requiredRecalcView используется для оптимизации повторных расчетов матрицы вида камеры, так как имеются методы (изменение угла поворота и позиции камеры), которые можно вызвать последовательно в одном кадре — матрица будет рассчитываться в методе, отвечающем за её выдачу, если данный флаг установлен в true.

Пересчет матрицы вида

Метод Camera::recalcView() позволяет принудительно пересчитать матрицу вида. Используется только в том случае, когда поле requiredRecalcView имеет значение true (проверка производится перед вызовом метода). Данный метод представлен:

// Пересчет матрицы вида
void Camera::recalcView()
{
    view = glm::lookAt(position, position + target, CAMERA_UP_VECTOR);
    requiredRecalcView = false;
}

Сумма position + target позволяет достичь эффекта камеры от первого лица.

Конструкторы класса камеры

Класс камеры имеет три конструктора:

  • защищенный (protected) конструктор положения камеры — устанавливает положение камеры в пространстве вне зависимости от типа проекции;
  • публичный конструктор для камеры с перспективной матрицей проекции;
  • публичный конструктор для камеры с ортографической матрицей проекции.

Пользователю класса предлагается выбрать один из двух конструкторов, определяющих тип проекции. При вызове конструктора вызывается защищенный для расположения камеры, который выполняет пересчет матрицы вида.

Для возможности изменить тип матрицы проекции без пересоздания камеры добавим методы Camera::setPerspective и Camera::setOrtho:

// Устанавливает заданную матрицу перспективы
void Camera::setPerspective(float fovy, float aspect)
{
    projection = glm::perspective(glm::radians(fovy), aspect, CAMERA_NEAR, CAMERA_FAR);
}

// Устанавливает заданную ортографическую матрицу
void Camera::setOrtho(float aspect)
{
    projection = glm::ortho(-1.0f, 1.0f, -1.0f/aspect, 1.0f/aspect, CAMERA_NEAR, CAMERA_FAR);
}

Теперь в целях повторного использования кода вызовем эти методы в соответствующих конструкторах:

// Защищенный (protected) конструктор камеры без перспективы 
Camera::Camera(const glm::vec3 &pos, const glm::vec2 &xyOffset) 
: position(pos), currentRotation(xyOffset)
{
    requiredRecalcView = true;
    sensitivity = 0.05;
}

// Конструктор камеры с проекцией перспективы
Camera::Camera(float aspect, const glm::vec3 &position, const glm::vec2 &xyOffset, float fovy)
: Camera(position, xyOffset)
{
    setPerspective(fovy, aspect);
}

// Конструктор ортографической камеры
Camera::Camera(float width, float height, const glm::vec3 &position, const glm::vec2 &xyOffset)
: Camera(position, xyOffset)
{
    setOrtho(width / height);
}

Защищенный метод устанавливает флаг requiredRecalcView для пересчета матрицы вида при следующем её запросе.

Перемещение камеры

Для перемещения камеры в пространстве используются методы Camera::move и Camera::setPosition. Первый метод позволяет сдвинуть камеру на указанный вектор posOffset, а второй задает положение камеры. Реализация:

// Сдвигает камеру на указанный вектор (dx,dy,dz)
void Camera::move(const glm::vec3 &posOffset)
{
    position += posOffset;

    requiredRecalcView = true;
}

// Устанавливает местоположение
void Camera::setPosition(const glm::vec3 &pos)
{
    position = pos;
    
    requiredRecalcView = true;
}

После каждого изменения необходимо изменить значение флага requiredRecalcView на true.

Поворот камеры

Для поворота камеры используются углы Эйлера: тангаж (pitch), рыскание (yaw) и крен (roll). Данные углы наглядно представлены на рисунке 1.

Рисунок 1 — Углы Эйлера

Согласно рисунку 1 тангаж — это угол, характеризующий величину наклона вверх или вниз, рыскание — величина поворота влево или вправо, а крен задает поворот вдоль продольной оси. Совокупность всех трех углов позволяет вычислить любой вектор поворота в трехмерном пространстве.

Класс камеры не использует крен.

Для пересчета координат цели, на которую смотрит камера, требуется метод Camera::recalcTarget:

// Пересчет цели, на которую смотрит камера
void Camera::recalcTarget()
{
    if(currentRotation.y > 89.0f)
        currentRotation.y = 89.0f;
    if(currentRotation.y < -89.0f)
        currentRotation.y = -89.0f;

    target.x = cos(glm::radians(currentRotation.x)) * cos(glm::radians(currentRotation.y));
    target.y = sin(glm::radians(currentRotation.y));
    target.z = sin(glm::radians(currentRotation.x)) * cos(glm::radians(currentRotation.y));
    
    requiredRecalcView = true;
}

Данный метод определяет углы поворота Эйлера на основании координат мыши.

Для вертикального поворота камеры стоит ограничение между +90 и -90 градусами.

Для изменения угла поворота камеры используем методы Camera::rotate и Camera::setRotation:


// Поворачивает камеру на dx и dy пикселей
void Camera::rotate(const glm::vec2 &xyOffset)
{
    currentRotation += xyOffset * sensitivity;

    recalcTarget();
    requiredRecalcView = true;
}

// Устанавливает угол поворота камеры
void Camera::setRotation(const glm::vec2 &xyOffset)
{
    currentRotation = xyOffset;
    recalcTarget();
}

Метод Camera::rotate использует чувствительность мыши, которую можно задать с помощью метода:

// Изменяет чувствительность мыши
void Camera::setSensitivity(float sens)
{
    sensitivity = sens;
}

Дополнение

Для дополнительной оптимизации добавим сохраним произведение матрицы вида на матрицу проекции.

include/Camera.h:

class Camera
{
    public:

...
        const glm::mat4& getVP(); // Возвращает ссылку на константную матрицу произведения матриц вида и проекции
...
    protected:
...
        void recalcVP(); // Пересчет произведения матриц
...
        glm::mat4 vp; // Матрица произведения вида и проекции
        bool requiredRecalcView; // Необходимость пересчета матрицы вида камеры
...
};

src/Camera.cpp:

// Возвращает ссылку на константную матрицу вида
const glm::mat4& Camera::getView()
{
    if (requiredRecalcVP)
    {
        if (requiredRecalcView)
            recalcView();
        recalcVP();
    }
    return vp;
}

// Пересчет произведения матриц
void Camera::recalcVP()
{
    vp = projection * view;

    requiredRecalcVP = false;
}

А так же необходимо дополнить методы Camera::recalcTarget, Camera::rotate, Camera::move, Camera::setPosition, Camera::setPerspective и Camera::setOrtho изменением флага:

    requiredRecalcVP = true;

Вывод

В результате работы с данной заметкой была разработана статическая библиотека для работы с классом камеры в трехмерных изображениях, разобраны углы Эйлера и способ сборки статических библиотек средствами vscode.

Статическая библиотека доступна в репозитории 3D_camera.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.