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

OpenGL 3: класс модели, uniform-переменные и камера

Данная заметка расширяет работу с буферами с помощью класса модели (узел-модель-сцена), а так же рассматривает работу с uniform-переменными и камерой.

Введение

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

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

Содержание заметки:

Матрицы трансформации

Каждая вершина трехмерной сетки хранится в локальной координатной системе — вершины расположены относительно нуля координат. Для перемещения, поворота и масштабирования вершин используются матрицы трансформации.

Матрицы трансформации являются основой для представления и применения различных геометрических трансформаций в компьютерной графике, таких как перемещение (трансляция), поворот, масштабирование.

В контексте трехмерной графики матрицы обычно имеют размерность 4х4. Такая размерность объясняется использованием гомогенных координат — способ представления точек и трансформаций в расширенном координатном пространстве. Любая точка (x,y,z,w) в гомогенных координатах представляет собой ту же точку (x/w,y/w,z/w), при w ≠ 0, где w называется гомогенной компонентой. Преимущества использования гомогенных координат:

  • единообразие трансформаций — используя гомогенные координаты можно представлять трансляции, повороты и масштабирования с помощью одной матрицы;
  • проективные преобразования — гомогенные координаты позволяют легко работать с проективными преобразованиями, которые являются основой для визуализации трехмерных сцен на двухмерном дисплее;
  • бесконечно удаленные точки — если точка бесконечно удалена, то её гомогенная компонента равна нулю w = 0, что полезно для представления точек исчезновения в перспективной проекции.

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

Различные трансформации могут комбинироваться в одну итоговую матрицу путем перемножения.

В заметках будет использован следующий порядок: масштабирование (scale), поворот (rotation), перемещение (translation). Перемножение матриц происходит справа налево: result = translation * rotation * scale. Такой же порядок используется в blender.

Важное замечание: порядок, в котором применяются трансформации, имеет значение. Например, поворот вокруг оси, а затем трансляция, будет давать другой результат по сравнению с трансляцией, затем поворотом.

Матрица перемещения (сдвига, переноса, трансляции) имеет следующий вид:
\begin{pmatrix} 1 & 0 & 0 & T_x \\ 0 & 1 & 0 & T_y \\ 0 & 0 & 1 & T_z \\ 0 & 0 & 0 & 1 \end{pmatrix},
где (T_x, T_y, T_z) — вектор трансляции в пространстве осей XYZ.
Для получения матрицы перемещения используется функция glm::translate. Эта функция принимает следующие аргументы: исходная матрица (к которой будет применено преобразование — обычно это единичная матрица) и вектор перемещения. В результате выполнения функция возвращает результирующую матрицу.
Пример использования glm::translate:

transform = glm::mat4(1.0f);
// Перемещение модели
transform = glm::translate(transform, position);

Матрица масштабирования имеет следующий вид:
\begin{pmatrix} S_x & 0 & 0 & 0 \\ 0 & S_y & 0 & 0 \\ 0 & 0 & S_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix},
где (S_x, S_y, S_z) — вектор с коэффициентами масштабирования по осям XYZ.
Для получения матрицы масштабирования используется функция glm::scale. Эта функция принимает следующие аргументы: исходная матрица (обычно не единичная), к которой будет применено преобразование и вектор масштабирования. В результате выполнения функция возвращает результирующую матрицу.
Пример использования glm::scale:

// Масштабирование
transform = glm::scale(transform, scale);  

Для поворотов используются два подхода:

  • углы Эйлера — последовательные повороты вокруг основных осей XYZ в заданном порядке;
  • кватернионы — система гиперкомплексных чисел, образующая векторное пространство, которая может быть использована как способ описания вращения объекта в пространстве вокруг произвольной оси в пространстве.

Матрица поворота углов Эйлера зависит от оси вокруг которой выполняются повороты на угол θ.
Матрица поворота вокруг оси X:
rotX = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & cosθ & -sinθ & 0 \\ 0 & sinθ & cosθ & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}
Матрица поворота вокруг оси Y:
rotY = \begin{pmatrix} cosθ & 0 & sinθ & 0 \\ 0 & 1 & 0 & 0 \\ - sinθ & 0 & cosθ & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}
Матрица поворота вокруг оси Z:
rotZ = \begin{pmatrix} cosθ & -sinθ & 0 & 0 \\ sinθ & cosθ & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}
Для получения матрицы поворота вокруг заданной оси используется функция glm::rotate. Эта функция принимает следующие аргументы: исходная матрица (обычно не единичная), угол поворота (радиан) и вектор (определяющий ось вращения. В результате выполнения функция возвращает результирующую матрицу.
В результате поворот получается следующим перемножением (пример):
rotation = rotZ * rotY * rotX
Пример использования glm::rotate:

// Поворот модели
transform = glm::rotate(transform, glm::radians(rotation.x), glm::vec3(1.0, 0.0, 0.0));
transform = glm::rotate(transform, glm::radians(rotation.y), glm::vec3(0.0, 1.0, 0.0));
transform = glm::rotate(transform, glm::radians(rotation.z), glm::vec3(0.0, 0.0, 1.0));

В случае использования кватернионов вида q = w + xi + yj + zk матрица поворота будет единственной и будет иметь следующий вид:
rotation = \begin{pmatrix} 1 - 2y^2 - 2z^2 & 2xy - 2wz & 2xz + 2wy & 0 \\ 2xy + 2wz & 1 - 2x^2 - 2z^2 & 2yz - 2wx & 0 \\ 2xz - 2wy & 2yz + 2wx & 1 - 2x^2 - 2y^2 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}
Для получения матрицы поворота на основании кватерниона используется функция glm::mat4_cast. Это функция, которая принимает в качестве аргумента кватернион и возвращает матрицу поворота.

// Поворот модели
transform = transform * glm::mat4_cast(rotation);

Важен порядок преобразований. В большинстве приложений (включая Blender) используется следующий порядок умножения матриц трансформации:

  1. масштабирование;
  2. поворот;
  3. перемещение.

Так как умножение производится в порядке справа налево, то итоговый порядок при использовании функций библиотеки GLM будет следующим:

transform = glm::mat4(1.0f);
// Перемещение модели
transform = glm::translate(transform, position);
// Поворот модели
transform = transform * glm::mat4_cast(rotation);
// Масштабирование
transform = glm::scale(transform, scale);  

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

Небольшой ликбез по организации моделей

В целях оптимизации памяти лучше паковать множество различных объектов в сцену, которую грузить внутрь одного VAO, если есть такая возможность. Если необходимо применить отдельные трансформации к частям такой сцены, то лучше выделять их из общего массива вершин VBO и рисовать в несколько подходов, пример такого рисования:

// ... - задаем параметры трансформации статичной сцены и большинства её объектов
glDrawElements(GL_TRIANGLES, static_objects_count, GL_UNSIGNED_INT, (void*)0); // рисуем вершины выбранного большинства
// ... - задаем параметры трансформации отдельного объекта 1, являющегося частью общей сцены
glDrawElements(GL_TRIANGLES, object1_count, GL_UNSIGNED_INT, (void*)(first_index_byteOffset)); // рисуем вершины объекта 1
// ... - задаем параметры трансформации отдельного объекта 2, являющегося частью общей сцены
glDrawElements(GL_TRIANGLES, object2_count, GL_UNSIGNED_INT, (void*)(second_index_byteOffset)); // рисуем вершины объекта 2

Пример достаточно абстрактный, но возьмем в качестве более точного примера боевой корабль с поворачиваемыми башнями:

  1. отрисовка основной статической части корабля к которой применима простая трансформация к глобальным координатам;
  2. отрисовка башни 1 с дополнительной трансформацией (поворот) относительно координат корабля;
  3. отрисовка башни 2 с дополнительной трансформацией (наклон) относительно координат корабля.

Данный подход является более быстрым в работе, чем переключение между различными VAO для групп вершин одного объекта, которые можно объединить в «сцену» с одним VAO.

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

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

Важно не злоупотреблять одним VAO для кучи полигональных сеток (meshes, мешей) — лучше группировать их по времени жизни, так как если какой-то меш будет не используемым грузом такой сцены, то он будет занимать память. Каждое переключение буферов занимает время, затраченное на вызов к видеокарте.

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

Работа с памятью в крупных проектах может проходить по следующим сценариям хранения вершин:

  • в памяти видеокарты (часто используется для отрисовки);
  • в оперативной памяти (отрисовка пока не требуется, но может понадобиться, а память видеокарты необходимо разгрузить — применяется при работе с медленными жесткими дисками если скорость чтения с диска недостаточна);
  • на диске (редко используемая геометрия, которая в ближайшее время не требуется, хорошо работает с SSD).

Примечание: задача оптимизации скорости загрузки и объемов занимаемой памяти тема для отдельной заметки.

В рамках заметок класс модели будет осуществлять контроль над памятью только в пределах видеокарты. Загрузка с диска остается пока за пользователем или фабрикой, которая будет считывать модели с диска и генерировать объекты. На данном этапе класс только принимает вершины, которые необходимо загрузить в память видеокарты.

Сцена представляет собой иерархическую структуру дерева, которая часто используется в компьютерной графике для организации объектов на сцене. Каждый узел в дереве может иметь множество потомков и представлять собой полигональную сетку, пустой узел-преобразование (которое применяется к его дочерним узлам), камеру или источник света.

В основе реализации сцены будет лежать идея «ленивого дерева» — вычисления трансформаций узлов будут выполняться только тогда, когда это необходимо перед непосредственным использованием. Для такого подхода необходимо хранить две матрицы матрицу трансформаций: собственную и глобальную (произведение собственной на родительскую), а так же два флага изменений: родительских и собственных. В случае, если были собственные изменения — необходимо пересчитать матрицу собственной трансформации, а так же если были собственные или родительские изменения — необходимо пересчитать глобальную матрицу: произведение матриц собственной на родительскую (parent * current).

Класс узла

Класс Node представляет собой узел сцены, который может иметь родительский и дочерние узлы. Узлы могут быть использованы для создания иерархической структуры сцены, где каждый узел может иметь свою позицию, поворот и масштаб. На основании данного класса будут созданы классы-потомки, такие как Model — класс, содержащий полигональную сетку.

Добавим к проекту два файла: include/Model.h и src/Model.cpp.

Описание класса узла в файле include/Model.h:

// Класс узла сцены
class Node 
{
    public:
        Node(Node* parent = NULL); // Конструктор с заданным родителем (по умолчанию NULL)
        Node(const Node& copy); // Конструктор копирования
        Node& operator=(const Node& other); // Оператор присваивания
        virtual ~Node();

        void setParent(Node * parent); // Устанавливает родителя для узла

        virtual const glm::mat4& getTransformMatrix(); // Возвращает матрицу трансформации модели
        bool isChanged(); // Возвращает необходимость пересчета матрицы трансформации

        const glm::vec3& c_position() const; // Константный доступ к позиции
        const glm::quat& c_rotation() const; // Константный доступ к повороту
        const glm::vec3& c_scale() const; // Константный доступ к масштабированию
        virtual glm::vec3& e_position(); // Неконстантная ссылка для изменений позиции
        virtual glm::quat& e_rotation(); // Неконстантная ссылка для изменений поворота
        virtual glm::vec3& e_scale(); // Неконстантная ссылка для изменений масштабирования 

        Node* getParent(); // Возвращает указатель на родителя
        const std::vector<Node*>& getChildren() const; // Возвращает ссылку на вектор дочерних узлов

    protected:
        Node *parent; // Родительский узел
        std::vector<Node*> children; // Узлы-потомки !Не должны указывать на NULL!

        glm::vec3 position; // позиция модели
        glm::quat rotation; // поворот модели
        glm::vec3 scale;    // масштабирование модели

        bool changed; // Флаг необходимости пересчета матрицы трансформации
        glm::mat4 transform; // Матрица трансформации модели
        bool parent_changed; // Флаг изменений у родителя - необходимость пересчета итоговой трансформации
        glm::mat4 result_transform; // Итоговая трансформация с учетом родительской

        virtual void recalcMatrices(); // Метод пересчета матрицы трансформации по необходимости, должен сбрасывать флаг changed
        void invalidateParent(); // Проход потомков в глубину с изменением флага parent_changed
};

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

  • Node(Node* parent) — конструктор, который создает пустой узел и задает ему родителя (по умолчанию NULL);
  • Node(const Node& copy) — конструктор копирования, который привязывает копию к тому же родителю.

Деструктор является виртуальным для обеспечения возможности полиморфного удаления объектов.

Реализация конструкторов и деструктора в файле src/Model.cpp:

// Конструктор с заданным родителем (по умолчанию NULL)
Node::Node(Node* parent_) : parent(parent_), result_transform(1), parent_changed(false),
position(0), rotation(1.0f, 0.0f, 0.0f, 0.0f), scale(1), changed(false), transform(1)
{
    if (parent)
    {
        // Запишем себя в потомки
        parent->children.push_back(this);
        parent_changed = true;
    }
} 

// Конструктор копирования
Node::Node(const Node& copy): position(copy.position), rotation(copy.rotation), scale(copy.scale),
parent(copy.parent), changed(copy.changed), parent_changed(copy.parent_changed), transform(1), result_transform(1)
{
    // Запишем себя в потомки
    if (parent)
        parent->children.push_back(this);
    // Если у оригинала не было изменений - перепишем матрицу трансформации
    if (!changed)
        transform = copy.transform;
    // Если у родителя не было изменений для оригинала - перепишем результирующую матрицу трансформации
    if (!parent_changed)
        result_transform = copy.result_transform;
}

Node::~Node()
{
    setParent(NULL); // Удаляем себя из потомков
    // Сообщаем потомкам об удалении родителя
    for (Node* child : children)
        child->setParent(NULL);
}

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

  • position, scale — векторы позиции и масштаба;
  • rotation — кватернион поворота;
  • changed — флаг, указывающий на необходимость пересчета матрицы трансформации;
  • parent_changed — флаг, указывающий на изменения в родительском узле;
  • transform — матрица локальной трансформации узла;
  • result_transform — итоговая матрица трансформации с учетом родительской;
  • c_position, c_rotation, c_scale — методы константного доступа к векторам, не меняющие флаг changed;
  • e_position, e_rotation, e_scale — методы, предоставляющие неконстантные ссылки к векторам с изменением флага changed;
  • isChanged — метод, который возвращает состояние флага changed;
  • getTransformMatrix — метод возвращает итоговую пересчитанную (при необходимости) матрицу трансформации узла;
  • recalcMatrices — метод пересчитывает при необходимости матрицы трансформации;
  • invalidateParent — метод обходит дочерние узлы и изменяет значение флага parent_changed на true.

Реализация методов в файле src/Model.cpp:

// Возвращает необходимость пересчета матрицы трансформации
bool Node::isChanged() 
{ 
    return changed; 
} 

// Константный доступ к позиции
const glm::vec3& Node::c_position() const 
{ 
    return position; 
} 

// Константный доступ к повороту
const glm::quat& Node::c_rotation() const 
{ 
    return rotation; 
} 

// Константный доступ к масштабированию
const glm::vec3& Node::c_scale() const 
{ 
    return scale; 
} 

// Неконстантная ссылка для изменений позиции
glm::vec3& Node::e_position() 
{ 
    changed = true; // Флаг о изменении
    invalidateParent(); // Проход потомков в глубину с изменением флага parent_changed
    return position; 
} 

// Неконстантная ссылка для изменений поворота
glm::quat& Node::e_rotation() 
{ 
    changed = true; // Флаг о изменении
    invalidateParent(); // Проход потомков в глубину с изменением флага parent_changed
    return rotation; 
} 

// Неконстантная ссылка для изменений масштабирования         
glm::vec3& Node::e_scale() 
{ 
    changed = true; // Флаг о изменении
    invalidateParent(); // Проход потомков в глубину с изменением флага parent_changed
    return scale; 
} 

// Возвращает матрицу трансформации модели
const glm::mat4& Node::getTransformMatrix() 
{
    // Если требуется - пересчитаем матрицу
    recalcMatrices();
        
    return result_transform;
}

// Пересчет матрицы трансформации модели, если это требуется
void Node::recalcMatrices() 
{
    // Если было изменение по векторам позиции, поворота и масштабирования
    if  (changed)
    {
        transform = glm::mat4(1.0f);
        // Перемещение модели
        transform = glm::translate(transform, position);
        // Поворот модели
        transform = transform * glm::mat4_cast(rotation);
        // Масштабирование
        transform = glm::scale(transform, scale);  
    }

    // Если собственная или родительская матрицы менялись - необходимо пересчитать итоговую
    if (changed || parent_changed)
    {
        if (parent) // Если есть родитель
            result_transform = parent->getTransformMatrix() * transform;
        else // Если нет родителя
            result_transform = transform;

        parent_changed = changed = false; // Изменения применены
    }
} 

// Проход потомков в глубину с изменением флага parent_changed
void Node::invalidateParent()
{
    // Цикл по потомкам
    for (Node* child : children)
    {
        child->parent_changed = true; // Флаг 
        child->invalidateParent(); // Рекурсивный вызов для потомков выбранного потомка
    }
}

Для связи узлов в дереве используются следующие поля и методы:

  • parent — указатель на родительский узел (NULL по умолчанию);
  • children — динамический массив (std::vector) указателей на дочерние узлы;
  • setParent — метод, который позволяет изменить родительский узел;
  • getParent — метод, который возвращает указатель на родительский узел;
  • getChildren — метод, который возвращает константную ссылку на вектор дочерних узлов;
  • operator= — оператор присваивания.

Реализация методов для связи узлов:

// Устанавливает родителя для узла
void Node::setParent(Node * parent)
{
    // Если замена происходит на другого родителя
    if (parent != this->parent)
    {
        Node* tmp = parent;
        // Проверка на зацикливание об самого себя
        while (tmp)
        {
            if (tmp == this)
                return; // Можно выдать exception
            tmp = tmp->parent;
        }
        // Если есть старый родитель - удалим себя из его потомков
        if (this->parent)
        {
            // Поиск в списке родительских потомков
            auto position = std::find(this->parent->children.begin(), this->parent->children.end(), this);
            // Если итератор указывает в конец - ничего не найдено
            if (position != this->parent->children.end()) 
                this->parent->children.erase(position); // Само удаление
        }
        
        this->parent = parent; // Заменяем указатель на родителя
        // Если родитель не NULL - добавляем себя в детей
        if (parent) 
            parent->children.push_back(this);
        // В любом случае необходимо пересчитать собственную итоговую матрицу
        parent_changed = true;
    }
}

// Возвращает указатель на родителя
Node* Node::getParent() 
{ 
    return parent; 
}

// Возвращает ссылку на вектор дочерних узлов
const std::vector<Node*>& Node::getChildren() const 
{ 
    return children; 
}

// Оператор присваивания
Node& Node::operator=(const Node& other)
{
    position = other.position;
    rotation = other.rotation;
    scale = other.scale;
    changed = other.changed;

    if (!changed)
        transform = other.transform;
        
    setParent(other.parent);
    
    // Если у other флаг parent_changed == false, то можно переписать матрицу результата с него
    if (!other.parent_changed)
    {
        result_transform = other.result_transform;
        parent_changed = false; // Сбрасываем флаг после смены родителя
    }

    return *this;
}

Такой подход позволяет снизить нагрузку на центральный процессор для отрисовки сложной сцены.

Важное замечание: для корректной компиляции необходимо подключить в начале файла include/Model.h два заголовочных файла — <GLM/gtc/quaternion.hpp> и <GLM/gtc/matrix_transform.hpp>, а так же <algorithm> в src/Model.cpp

Класс модели

Класс модели (Model) является потомком от класса узла (Node) и представляет собой модель в графическом контексте, которая обеспечивает работу и рисование геометрии. В последующих заметках данный класс будет дополняться текстурами и материалами.

Описание класса модели в файле include/Model.h:

// Класс модели
class Model : public Node
{
    public:
        Model(Node *parent = NULL); // Конструктор по умолчанию
        Model(const Model& copy); // Конструктор копирования
        Model& operator=(const Model& other); // Оператор присваивания
        virtual ~Model();

        void render(); // Вызов отрисовки

        void load_verteces(glm::vec3* verteces, GLuint count); // Загрузка вершин в буфер
        void load_indices(GLuint* indices, GLuint count); // Загрузка индексов в буфер
        void set_index_range(size_t first_byteOffset, size_t count); // Ограничение диапазона из буфера индексов

    private:
        VAO vao;
        BO vertex_vbo, index_vbo; // вершинный и индексный буферы
        GLuint verteces_count; // Количество вершин
        size_t first_index_byteOffset, indices_count; // Сдвиг в байтах для первого и количество индексов
};

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

Реализация конструкторов, оператора присваивания и деструктора в файле src/Model.cpp:

// Конструктор по умолчанию
Model::Model(Node *parent) : Node(parent), verteces_count(0), first_index_byteOffset(0), indices_count(0), 
vertex_vbo(VERTEX), index_vbo(ELEMENT)
{

}

// Конструктор копирования
Model::Model(const Model& copy) : Node(copy),
vao(copy.vao), 
verteces_count(copy.verteces_count), first_index_byteOffset(copy.first_index_byteOffset), indices_count(copy.indices_count), 
vertex_vbo(copy.vertex_vbo), index_vbo(copy.index_vbo)
{

}

// Оператор присваивания
Model& Model::operator=(const Model& other)
{
    Node::operator=(other); // Явный вызов родительского оператора копирования
    
    vao = other.vao; 
    verteces_count = other.verteces_count;
    first_index_byteOffset = other.first_index_byteOffset;
    indices_count = other.indices_count;
    
    vertex_vbo = other.vertex_vbo;
    index_vbo = other.index_vbo;
    
    return *this;
}

Model::~Model()
{

}

После загрузки вершин в буфер, требуется определить атрибут вершинного буфера с помощью функции из прошлой заметки. Перенесем её в файл src/Model.cpp:

// Функция для конфигурации атрибута вершинного буфера
void vertex_attrib_config()
{
    // Определим спецификацию атрибута
    glVertexAttribPointer(  0         // индекс атрибута, должен совпадать с Layout шейдера
                          , 3         // количество компонент одного элемента
                          , GL_FLOAT  // тип
                          , GL_FALSE  // необходимость нормировать значения
                          , 0         // шаг
                          , (void *)0 // отступ с начала массива
                         );
    // Включаем необходимый атрибут у выбранного VAO
    glEnableVertexAttribArray(0);
}

Реализация методов загрузки вершин и индексов:

// Загрузка вершин в буфер
void Model::load_verteces(glm::vec3* verteces, GLuint count)
{
    // Подключаем VAO и вершинный буфер
    vao.use();
    vertex_vbo.use();

    // Загрузка вершин в память буфера
    vertex_vbo.load(verteces, sizeof(glm::vec3)*count);
    vertex_attrib_config();
    // Запоминаем количество вершин для отрисовки
    verteces_count = count;
}

// Загрузка индексов в буфер
void Model::load_indices(GLuint* indices, GLuint count) 
{
    // Подключаем VAO и индексный буфер
    vao.use();
    index_vbo.use();

    // Загрузка вершин в память буфера
    index_vbo.load(indices, sizeof(GLuint)*count);
    // Запоминаем количество вершин для отрисовки
    indices_count = count;
}

// Ограничение диапазона из буфера индексов
void Model::set_index_range(size_t first_byteOffset, size_t count)
{
    first_index_byteOffset = first_byteOffset;
    indices_count = count;
} 

Метод для отрисовки модели:

// Вызов отрисовки
void Model::render() 
{
    // Подключаем VAO
    vao.use();
    // Если есть индексы - рисуем с их использованием
    if (indices_count)
    {
        index_vbo.use();
        glDrawElements(GL_TRIANGLES, indices_count, GL_UNSIGNED_INT, (void*)(first_index_byteOffset));
    }
    // Если есть вершины - рисуем на основании массива вершин
    else if (verteces_count)
        glDrawArrays(GL_TRIANGLES, 0, verteces_count);
}

В зависимости от наличия содержимого индексного буфера выбирается функция для отрисовки геометрии.

В файле src/main.cpp заменим заголовочный файл «Buffers.h» на «Model.h«, а так же в функции main:

    // VAO
    VAO vao;
    // VBO вершинный
    BO vbo_vert(VERTEX);
    attrib_config(); // Конфигурация атрибутов
    // Загрузка вершин в используемый буфер вершин
    vbo_vert.load(verticies, sizeof(verticies));
    // Модель прямоугольника
    Model rectangle; 
    
    // Загрузка вершин модели
    rectangle.load_verteces(verticies, sizeof(verticies)/sizeof(glm::vec3));

...

    // VBO элементный (индексы вершин)
    BO vbo_elem(ELEMENT);
    
    // Загрузка индексов в используемый элементный буфер
    vbo_elem.load(indices, sizeof(indices));
    // Загрузка индексов модели
    rectangle.load_indices(indices, sizeof(indices));
...
        // Тут производится рендер
        vao.use();
        glDrawElements(GL_TRIANGLES, sizeof(indices)/sizeof(GLuint), GL_UNSIGNED_INT, (void*)0);
        vao.disable();
        rectangle.render();

Важное замечание: матрица трансформации на данный момент нигде не применяется — для неё требуется uniform-переменная (после класса сцены).

Класс сцены

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

Создадим два файла: include/Scene.h и src/Scene.cpp.

В файле include/Scene.h добавим описание класса сцены:

// Класс сцены
class Scene
{
    public:
        Scene(); // Конструктор пустой сцены
        Scene(const Scene &copy); // Конструктор копирования
        Scene& operator=(const Scene& other); // Оператор присваивания

        void render(); // Рендер сцены

        Node root; // Корневой узел

        // Списки объектов, выступающих узлами
        std::list<Node> nodes; // Список пустых узлов
        std::list<Model> models; // Список моделей для рендера
    
    protected:
        void rebuld_tree(const Scene& from); // Перестройка дерева после копирования или присваивания
        template <class T>
        void rebuild_Nodes_list(T& nodes, const Scene& from); // Перестройка узлов выбранного списка
        template <class T>
        bool move_pointer(Node& for_node, const std::vector<T>& from_nodes, std::vector<T>& this_nodes); // Сдвигает родителя узла между двумя списками при условии его принадлежности к оригинальному, возвращает признак замены
};

Сцена имеет пустой корневой узел — root, а также два списка (std::list) nodes и models — пустые узлы и модели соответственно.

Для контроля за целостностью дерева реализованы три метода:

  • move_parent — сдвигает родителя узла между двумя списками при условии его принадлежности к оригинальному;
  • rebuild_Nodes_vector — перестройка узлов выбранного списка с использованием оригинального;
  • rebuld_tree — перестройка дерева после копирования или присваивания.

Первые два метода шаблонные для обеспечения универсальности метода для массивов разных типов.

Метод rebuld_tree содержит в себе вызовы rebuild_Nodes_list для всех типов узлов (на данный момент nodes и models).

Метод rebuild_Nodes_list, являясь шаблонным по std::list<тип узла>, в свою очередь проходит по узлам из выбранного списка и ищет их родителя в оригинальной сцене (которая является аргументом конструктора копирования или оператора присваивания). Тип родителя определяется с помощью dynamic_cast, который возвращает нулевой указатель, если приведение невозможно. Первой проверкой является проверка на то, что родителем является корневой узел через сравнение адресов. Далее вызывается метод move_parent в зависимости от успешности dynamic_cast. Если родитель не нашелся, то он не часть сцены — просто переносим его адрес, что, конечно, может вести к непредсказуемому поведению.

Метод move_parent, являясь шаблонным по типу узлов (без вектора для удобства работы арифметикой указателей), позволяет определить является ли указатель частью списка (std::list) и сдвинуть родителя в на новый массив для рассматриваемого узла.

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

Реализация этих методов в файле src/Scene.cpp:

// Перестройка узлов выбранного списка
template <class T>
void Scene::rebuild_Nodes_list(T& nodes, const Scene& from)
{
    for (auto it = nodes.begin(); it != nodes.end(); it++)
    {
        // Берем родителя, который указывает на оригинальный объект
        Node* parent = it->getParent();
        
        // Если родитель - оригинальный корневой узел, то меняем на собственный корневой узел
        if (parent == &from.root) 
        {
            it->setParent(&root);
            continue;
        }

        // Если можно привести к модели, то ищем родителя среди моделей
        if (dynamic_cast<Model*>(parent))
            move_parent(*it, from.models, this->models);
        // Иначе это пустой узел
        else
            move_parent(*it, from.nodes, this->nodes);
            
        // Не нашли родителя - значит он не часть этой сцены 
        // и изменений по нему не требуется
    }
} 

// Сдвигает родителя узла между двумя списками при условии его принадлежности к оригинальному
template <class T>
void Scene::move_parent(Node& for_node, const std::list<T>& from_nodes, std::list<T>& this_nodes)
{
    // Возьмем адрес родителя
    Node* parent = for_node.getParent();
    // Цикл по элементам списков для перемещения родителя
    // Списки в процессе копирования идеинтичные, вторая проверка не требуется
    for (auto it_from = from_nodes.begin(), it_this = this_nodes.begin(); it_from != from_nodes.end(); ++it_from, ++it_this)
        // Если адрес объекта, на который указывает итератор, совпадает с родителем - меняем родителя по второму итератору (it_this)
        if (&(*it_from) == parent)
            for_node.setParent(&(*it_this));
}

// Перестройка дерева после копирования или присваивания
void Scene::rebuld_tree(const Scene& from)
{    
    // Восстановим родителей в пустых узлах для копии
    rebuild_Nodes_list(nodes, from);
    rebuild_Nodes_list(models, from);
}

Теперь можно рассмотреть конструкторы и оператор присваивания:

// Конструктор пустой сцены
Scene::Scene()
{
    
}

// Конструктор копирования
Scene::Scene(const Scene &copy): root(copy.root), 
nodes(copy.nodes), models(copy.models)
{
    rebuld_tree(copy);
} 

// Оператор присваивания
Scene& Scene::operator=(const Scene& other) 
{
    root = other.root;
    nodes = other.nodes;
    models = other.models;

    rebuld_tree(other);

    return *this;
} 

В методе Scene::render есть цикл для всех объектов, которые могут быть отрисованы на экране (класс Model::render):

// Рендер сцены
void Scene::render() 
{
    for (auto & model : models)
        model.render();
}

Текущая версия доступна на теге v0.1 в репозитории 03.

Uniform-переменная

Uniform-переменная — это данные доступные для шейдерных программ только в режиме чтения. Данные переменные используются для передачи «универсальных» данных, которые применяются для всех вершин/фрагментов, например: трансформация модели и камеры, дескриптор используемой текстуры и так далее.

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

Функция glGetUniformLocation принимает в качестве параметров дескриптор шейдерной программы и имя переменной в виде си-строки, а возвращает местоположение запрашиваемой переменной представленное целым числом.

Функция glGetUniformLocation принимает в качестве параметров дескриптор шейдерной программы и имя переменной в виде си-строки, а возвращает местоположение запрашиваемой переменной представленное целым числом.

Пример использования:

GLuint model_uniform = glGetUniformLocation(shaderProgram, "model");

Для записи данных в uniform-переменную используются следующие функции:

  • glUniformNf — для вектора из N float;
  • glUniformNi — для вектора из N int;
  • glUniformNui — для вектора из N unsigned int;
  • glUniformNfv — для вектора из N float по адресу;
  • glUniformNiv — для вектора из N int по адресу;
  • glUniformNuiv — для вектора из N unsigned int по адресу;
  • glUniformMatrixNfv — для матрицы NxN float по адресу;
  • glUniformMatrixNxMfv — для матрицы NxM float по адресу.

Важное замечание: функции с буквой v на конце отвечают за отправку данных из массива, а функции без — принимает нужное количество переменных как аргументы. Все функции принимают в качестве первого аргумента местоположение необходимой uniform-переменной.

Класс камеры

В рамках заметки был рассмотрен принцип устройства камеры в приложениях с трехмерной графики.

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

Камера в текущей реализации может быть частью ленивого дерева сцены, но при этом она должна обладать матрицей проекции (перспективной или ортогональной), что требует дополнительной итоговой матрицы и флага на необходимость пересчета. Матрица вида так же нуждается в хранении, так как в ней не будет использоваться вектор масштабирования.

Добавим к проекту два файла: include/Camera.h и src/Camera.cpp.

В файле include/Scene.h добавим описание класса Camera:

class Camera : public Node
{
    public:
        Camera(float aspect, const glm::vec3 &position = glm::vec3(0.0f), const glm::vec3 &initialRotation = CAMERA_DEFAULT_ROTATION, float fovy = CAMERA_FOVy); // Конструктор камеры с проекцией перспективы
        Camera(float width, float height, const glm::vec3 &position = glm::vec3(0.0f), const glm::vec3 &initialRotation = CAMERA_DEFAULT_ROTATION); // Конструктор ортографической камеры
        Camera(const Camera& copy); // Конструктор копирования камеры
        Camera& operator=(const Camera& other); // Оператор присваивания
        virtual ~Camera(); // Деструктор

        const glm::mat4& getVP(); // Возвращает ссылку на константную матрицу произведения матриц вида и проекции
        const glm::mat4& getProjection(); // Возвращает ссылку на константную матрицу проекции 
        const glm::mat4& getView(); // Возвращает ссылку на константную матрицу вида
        
        void rotate(const glm::vec2 &xyOffset); // Поворачивает камеру на dx и dy пикселей с учетом чувствительности
        
        void setPerspective(float fov, float aspect); // Устанавливает заданную матрицу перспективы
        void setOrtho(float width, float height); // Устанавливает заданную ортографическую матрицу
        void setSensitivity(float sensitivity); // Изменяет чувствительность мыши
        const float& getSensitivity() const; // Возвращает чувствительность мыши
        
    protected:
        Camera(const glm::vec3 &position, const glm::vec3 &initialRotation); // Защищенный (protected) конструктор камеры без перспективы 

        glm::mat4 view; // Матрица вида
        glm::mat4 projection; // Матрица проекции
        glm::mat4 vp; // Матрица произведения вида и проекции
        bool requiredRecalcVP; // Необходимость пересчета матрицы вида и проекции камеры
        
        float sensitivity; // Чувствительность мыши
        
        virtual void recalcMatrices(); // Метод пересчета матрицы вида и произведения Вида*Проекции по необходимости, должен сбрасывать флаг changed
};

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

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

В файле src/Camera.cpp добавим реализацию конструкторов, оператора присваивания и деструктора:

// Защищенный (protected) конструктор камеры без перспективы 
Camera::Camera(const glm::vec3 &pos, const glm::vec3 &initialRotation) : Node(NULL) // Пусть по умолчанию камера не относится к сцене
{
    sensitivity = CAMERA_DEFAULT_SENSIVITY;
    position = pos; // задаем позицию
    // Определяем начальный поворот
    glm::quat rotationAroundX = glm::angleAxis( glm::radians(initialRotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
    glm::quat rotationAroundY = glm::angleAxis(-glm::radians(initialRotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
    glm::quat rotationAroundZ = glm::angleAxis( glm::radians(initialRotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
    rotation = rotationAroundX * rotationAroundY * rotationAroundZ;
    // Признак изменения
    changed = true;
}

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

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

// Конструктор копирования камеры
Camera::Camera(const Camera& copy) 
: Node(copy), projection(copy.projection), requiredRecalcVP(copy.requiredRecalcVP), sensitivity(copy.sensitivity)
{
    // Если у оригинала не было изменений - перепишем матрицу вида-проекции
    if (!requiredRecalcVP)
        vp = copy.vp;
}

// Оператор присваивания
Camera& Camera::operator=(const Camera& other)
{
    Node::operator=(other); // Вызов родительского оператора= для переноса узла

    projection = other.projection;
    requiredRecalcVP = other.requiredRecalcVP;
    sensitivity = other.sensitivity;

    // Если у оригинала не было изменений - перепишем матрицу вида-проекции
    if (!requiredRecalcVP)
        vp = other.vp;

    return *this;
}

// Деструктор
Camera::~Camera() 
{ 
    
}

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

Работа с чувствительностью мыши ведется с помощью:

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

// Возвращает чувствительность мыши
const float& Camera::getSensitivity() const
{
    return sensitivity; 
}

Реализация метода Camera::rotate:

// Поворачивает камеру на dx и dy пикселей с учетом чувствительности
void Camera::rotate(const glm::vec2 &xyOffset)
{
    // xyOffset - сдвиги координат мыши, xyOffset.x означает поворот вокруг оси Y, а xyOffset.y - поворот вокруг оси X
    
    // Вращение вокруг оси Y
    glm::quat qY = glm::angleAxis(-xyOffset.x * sensitivity, glm::vec3(0.0f, 1.0f, 0.0f));
    
    // Вращение вокруг оси X
    glm::quat qX = glm::angleAxis(xyOffset.y * sensitivity, glm::vec3(1.0f, 0.0f, 0.0f));

    // Сначала применяем вращение вокруг Y, затем вокруг X
    rotation = qY * rotation * qX;

    changed = true;
    invalidateParent(); // Проход потомков в глубину с изменением флага parent_changed
}

Для изменения проекции на перспективную используется метод Camera::setPerspective, а для ортогональной Camera::setOrtho. Оба метода изменяют флаг requiredRecalcVP на true, что указывает о необходимости пересчета произведения матриц вида и проекции.
Реализация этих методов:

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

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

Матрицу проекции можно получить с помощью следующего метода:

// Возвращает ссылку на константную матрицу проекции
const glm::mat4& Camera::getProjection()
{
    return projection;
}

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

Для доступа к матрицам вида и произведения проекции*вида реализованы методы:

// Возвращает ссылку на константную матрицу вида
const glm::mat4& Camera::getView()
{
    recalcMatrices();

    return view;
}

// Возвращает ссылку на константную матрицу вида
const glm::mat4& Camera::getVP()
{
    recalcMatrices();

    return vp;
}

Реализация метода пересчета матриц:

// Метод пересчета матрицы вида и произведения Вида*Проекции по необходимости, должен сбрасывать флаг changed
void Camera::recalcMatrices()
{
    if (changed || parent_changed)
    {
        glm::vec3 _position = position;
        glm::quat _rotation = rotation;
        if (parent) // Если есть родитель
        {
            glm::mat4 normalized_transform = parent->getTransformMatrix();
            for (int i = 0; i < 3; i++) 
            {
                glm::vec3 axis = glm::vec3(normalized_transform[i]);
                normalized_transform[i] = glm::vec4(glm::normalize(axis), normalized_transform[i].w);
            }
            glm::vec4 tmp = normalized_transform * glm::vec4(_position, 1.0f);
            tmp /= tmp.w;
            _position = glm::vec3(tmp);
            _rotation = glm::quat_cast(normalized_transform) * _rotation;
        }
        glm::mat4 rotationMatrix = glm::mat4_cast(glm::conjugate(_rotation));
        glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), -_position);
        view = rotationMatrix * translationMatrix;
    }
  
    Node::recalcMatrices();

    if (requiredRecalcVP)
    {
        vp = projection * view;
        requiredRecalcVP = false; // Изменения применены
    }
}

В случае, если изменилась камера или один из родителей, то следует пересчитать матрицу вида. Матрица вида зависит от вектора положения и кватерниона поворота. Если есть родитель у камеры, то откинем операцию масштабирования путем нормирования столбцов. Далее применим родительскую трансформацию без учета масштабирования родителя к позиции и повороту. При расчетах матрицы сдвига камеры следует инвертировать значение позиции, а в расчетах матрицы поворота использовать сопряженный кватернион (glm::conjugate).

Важное замечание: в заметке используется камера от первого лица, что определяется порядком произведения view = rotationMatrix * translationMatrix, если требуется использовать камеру от третьего лица, множители следует поменять местами (произведение матриц некоммутативно).

Камера может выполнять роль узла, так что требуется вызвать родительский метод пересчета матрицы трансформации Node::recalcMatrices.

После всех вычислений следует обновить произведение матриц проекции*вида на основании флага requiredRecalcVP.

Камера как часть сцены

Как говорилось ранее — камера может выполнять роль узла (часть ленивого дерева сцены), в связи с чем требуется дополнить класс сцены. В файле include/Scene.h добавим два новых поля к классу:

// Класс сцены
class Scene
{
    public:
...
        // Списки объектов, выступающих узлами
        std::list<Node> nodes; // Список пустых узлов
        std::list<Model> models; // Список моделей для рендера
        std::list<Camera> cameras; // Список камер
...
};

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

В файле src/Scene.cpp дополним конструкторы:

// Конструктор копирования
Scene::Scene(const Scene &copy): root(copy.root), 
nodes(copy.nodes), models(copy.models), cameras(copy.cameras)
{
    rebuld_tree(copy);
} 

// Оператор присваивания
Scene& Scene::operator=(const Scene& other) 
{
    root = other.root;
    nodes = other.nodes;
    models = other.models;
    cameras = other.cameras;

    rebuld_tree(other);

    return *this;
} 

Так же необходимо дополнить методы Scene::rebuld_tree и Scene::rebuild_Nodes_list:

// Перестройка узлов выбранного списка
template <class T>
void Scene::rebuild_Nodes_list(T& nodes, const Scene& from)
{
    for (auto it = nodes.begin(); it != nodes.end(); it++)
    {
...
        // Если можно привести к модели, то ищем родителя среди моделей
        if (dynamic_cast<Model*>(parent))
            move_parent(*it, from.models, this->models);
        else
        // Иначе проверяем на принадлежность к камерам
        if (dynamic_cast<Camera*>(parent))
            move_parent(*it, from.cameras, this->cameras);
        // Иначе это пустой узел
        else
            move_parent(*it, from.nodes, this->nodes);
...
    }
}

// Перестройка дерева после копирования или присваивания
void Scene::rebuld_tree(const Scene& from)
{    
    // Восстановим родителей в пустых узлах для копии
    rebuild_Nodes_list(nodes, from);
    rebuild_Nodes_list(models, from);
    rebuild_Nodes_list(cameras, from);
}

Текущая версия доступна на теге v0.2 в репозитории 03.

Доступ к текущей камере

В случае, если в приложении доступно несколько камер — существует проблема доступа к текущей используемой, а так же проблема контроля за её существованием.

Для решения данной задачи добавим статический метод Camera::current, нестатический метод Camera::use, а так же статическое поле-указатель Camera::p_current в файле include/Camera.h:

class Camera : public Node
{
    public:
...
        void use(); // Использование этой камеры как текущей
        static Camera& current(); // Ссылка на текущую используемую камеру
    protected:
   ...
        static Camera* p_current; // Указатель на текущую используемую камеру
};      

В файле src/Camera.cpp добавим реализацию методов и объявление статического поля:

// Указатель на текущую используемую камеру
Camera* Camera::p_current = NULL; 

// Использование этой камеры как текущей
void Camera::use()
{
    p_current = this;
}

// Ссылка на текущую используемую камеру
Camera& Camera::current()
{
    static Camera default_cam(800.0f/600.0f);

    if (!p_current)
        return default_cam;
    else
        return *p_current;
}

Помимо этого необходимо дополнить реализацию деструктора проверкой указателя во избежание доступа к удаленному объекту:

// Деструктор
Camera::~Camera() 
{ 
    if (p_current == this)
        p_current = NULL;
}

Доработка шейдеров

В файле вершинного шейдера shaders/shader.vert добавим две uniform-переменные:

#version 330 core 

layout(location = 0) in vec3 pos; 

uniform mat4 vp;
uniform mat4 model;

void main() 
{ 
    gl_Position = vp * model * vec4(pos, 1.0);
} 

Для загрузки моделью своей матрицы трансформации в uniform-переменную следует доработать классы Scene и Model, добавив к методам render аргумент с расположением uniform-переменной.

В файле include/Model.h:

// Класс модели
class Model : public Node
{
    public:
...
        void render(const GLuint &model_uniform); // Вызов отрисовки
...
};

В файле src/Model.cpp:

// Вызов отрисовки
void Model::render(const GLuint &model_uniform) 
{
    // Загрузим матрицу трансформации
    glUniformMatrix4fv(model_uniform, 1, GL_FALSE, &getTransformMatrix()[0][0]);

    // Подключаем VAO
...
}

В файле include/Scene.h:

class Scene
{
    public:
...
        void render(const GLuint &model_uniform); // Рендер сцены
...
};

В файле src/Scene.cpp:

// Рендер сцены
void Scene::render(const GLuint &model_uniform) 
{
    for (auto & model : models)
        model.render(model_uniform);
}

В файле src/main.cpp осталось дополнить функцию main определением расположения uniform-переменных, загрузкой матриц произведения проекции*вида и модели:

int main(void)
{
...
    // Расположение Uniform-переменной
    GLuint vp_uniform = glGetUniformLocation(shaderProgram, "vp");
    GLuint model_uniform = glGetUniformLocation(shaderProgram, "model");

    // Пока не произойдет событие запроса закрытия окна
    while(!glfwWindowShouldClose(window))
    {
        // Загрузим матрицу проекции*вида
        glUniformMatrix4fv(vp_uniform, 1, GL_FALSE, &Camera::current().getVP()[0][0]);

        // Очистка буфера цвета
        glClear(GL_COLOR_BUFFER_BIT);

        // Тут производится рендер
        rectangle.render(model_uniform);

        // Представление содержимого буфера цепочки показа на окно
        glfwSwapBuffers(window);
        // Обработка системных событий
        glfwPollEvents();
    }
...
}

Для интереса изменим позицию и поворот прямоугольника перед циклом:

    rectangle.e_position().z = 2;
    rectangle.e_rotation() = {0.733f, 0.462f, 0.191f, 0.462f};

Если все верно то на экране должен рисоваться прямоугольник, изображенный на рисунке 1.

Рисунок 1 — Трансформированный прямоугольник

Текущая версия доступна на теге v0.3 в репозитории 03.

Обработка движений мыши

Для вращения камеры необходимо добавить callback-функцию в файле src/main.cpp:

bool firstMouse = true;
float lastX, lastY;

void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }
  
    glm::vec2 offset(xpos - lastX, lastY - ypos); 
    lastX = xpos;
    lastY = ypos;

    Camera::current().rotate(offset);
}  

Данная функция запоминает координаты мыши при первом входе на окно после запуска приложения и считает сдвиг камеры относительно них, после чего считает сдвиг по осям, который передается методу Camera::rotate.

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

    // Установка callback-функции для мыши и камеры
    glfwSetCursorPosCallback(window, mouse_callback);

Важное замечание: так как камера расположена в нуле и смотрит по оси Z, то точка по оси X с увеличением координат будет сдвигаться влево относительно центра экрана (пример изображен на рисунке 2).

Рисунок 2 — пример расположения камеры в пространстве

Текущая версия доступна на теге v0.4 в репозитории 03.

Заключение

В данной заметке были разработаны классы узла, камеры, сцены, а так же модели, использующий ранее созданные классы VAO и VBO. Были рассмотрены способы работы с uniform-переменными и трансформация модели, а так же реализовано управление камерой с помощью движений мыши.

Проект доступен в публичном репозитории: 03
Библиотеки: dependencies

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

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

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