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

OpenGL 19: загрузка .glTF ч.1 — статические сцены

Внутреннее устройство и работа с файлами .glTF ч.1 — загрузка моделей без анимации

Введение

Формат .OBJ является «устаревшим», так как не поддерживает анимации и использует старый формат материалов, требующий конвертацию к PBR-материалам. Данная заметка предлагает замену — формат glTF, поддерживаемый KhronosGroup.

Важное замечание: существует две версии стандарта для glTF, но в заметке рассматривается только вторая версия.

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

Формат моделей .gltf

Идея формата glTF заключается в том, что данные хранятся в готовом для загрузки на видеокарту формате с указанием параметров конфигурации атрибутов.

Спецификация формата второй версии доступна на сайте разработчика Khronos Group в двух форматах: HTML и PDF. Данная заметка не будет переводом спецификации, а лишь упрощенным разбором формата с некоторыми возможными подводными камнями.

Данный стандарт использует JSON для описания сцен и используемых в их составе элементов. JSON состоит из двух основных «элементов-контейнеров»:

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

В данных «элементах-контейнерах» могут содержаться как другие контейнеры, так и более простые данные: строки (в двойных кавычках), числа, true/false, null.

Теперь, когда формат JSON понятен, можно рассмотреть формат файлов glTF. Если открыть файл со сценой в текстовом редакторе, поддерживающем сворачивание JSON блоков (например VSCode или Notepad++), то можно увидеть основные компоненты, описывающие сцены и их содержимое. В качестве примера на рисунке 1 представлен файл cube.gltf.

Рисунок 1 — Основные компоненты файла glTF на примере не анимированной сцены cube.gltf

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

  • asset — информация об экспорте и авторе;
  • scene — сцена по умолчанию;
  • scenes — информация о сценах;
  • nodes — информация о узлах сцены, каждый может содержать:
    • массив индексов других узлов,
    • массив индексов сеток (meshes);
  • materials — информация о материалах, обычно в формате PBR metallic-roughness;
  • meshes — информация о трехмерных сетках (состоящих из полигонов);
  • textures — информация о текстурах, ссылающаяся на изображения;
  • images — информация о используемых изображениях в качестве текстур — их расположение и формат;
  • accessors (средство доступа) — информация о используемых привязках вершинных атрибутов (для функции glVertexAttribPointer);
  • bufferViews (границы буфера) — информация о границах вершинных и индексных буферов в одном бинарном буфере:
    • buffer — индекс в массиве бинарных буферов,
    • byteLength — длина в байтах,
    • byteOffset — отступ в байтах,
    • target — тип (вершинный или индексный);
  • samplers — определяет wrapping (поведение при выходе за границы текстуры) и фильтрацию (filtering — Magnification и Minification)
  • buffers — буферы с бинарными данными.

Примечание: про wrapping и filtering можно прочесть в 4 заметке про текстуры.

Содержимое файла следует рассматривать в виде графа (набор связанных между собой вершин). Пример связей в файле до уровня сетки (Mesh) изображен на рисунке 2.

Рисунок 2 — Пример графа связей в файле до уровня сетки (mesh)

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

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

Каждый узел может (не обязан) содержать в себе следующие элементы, отвечающие за трансформацию:

  • вектор смещения (translation);
  • вектор поворота в виде кватерниона (rotation);
  • вектор масштабирования (scale);
  • матрица трансформации (matrix).

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

Рисунок 3 — Пример связей сеток (mesh) и примитивов

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

Материал и связанные с ним элементы изображены на рисунке 4.

Рисунок 4 — Пример связи материалов и остальных элементов

Каждый материал включает в себя параметры PBR, которые по умолчанию задаются в формате MetallicRoughness (металличность-шероховатость), а так же используемые текстуры. Текстура ссылается на параметры сэмплирования (sampler) и изображение (image). Изображение может быть записано в один (формат «glTF со встраиванием») из буферов или в отдельный файл (формат «Разделенный glTF»).

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

Библиотека tinyglTF

Для того, чтобы сэкономить время разработки функции-загрузчика, будет использована библиотека-парсер для формата glTF с названием tinyglTF, доступная в github репозитории.

Для работы библиотеки понадобятся библиотека json (файл json.hpp) и, если потребуется записывать текстуры в файл, расширение библиотеки stb_image: stb_image_write.h (не используется в заметке). Расположим данные файлы в папке dependencies/tinyglTF.

Если вы используете Makefile, то необходимо дополнить переменную CFLAGS и изменить стандарт на gnu++11:

CFLAGS += -I../dependencies/tinyglTF

# Опции линкера
LDFLAGS += --std=gnu++11

Если вы работаете со сборщиком VS Code, то в файле .vscode/tasks.json необходимо указать папку с заголовочными файлами для двух задач сборки:

{
    "tasks": [
        {
            "type": "cppbuild",
            "label": "C/C++: g++.exe сборка активного файла",
            "command": "C:/MinGW/bin/g++.exe",
            "args": [

...
                "-I${workspaceFolder}/../dependencies/tinyobjloader",
                "-I${workspaceFolder}/../dependencies/tinyglTF",
                "-static",

...
        },
        {
            "type": "cppbuild",
            "label": "C/C++ x64: g++.exe сборка активного файла",
            "command": "C:/MinGW64/bin/g++.exe",
            "args": [
...
                "-I${workspaceFolder}/../dependencies/tinyobjloader",
                "-I${workspaceFolder}/../dependencies/tinyglTF",
                "-static",

...

А так же для компилятора MinGW x32 следует изменить параметр, определяющий версию стандарта языка C++:

                "--std=c++11",
                "-std=gnu++11",

В файле .vscode/c_cpp_properties.json добавим путь до созданной папки:

{
    "configurations": [
        {
            "name": "some_name",
            "includePath": [
                "${workspaceFolder}/include",
                "${workspaceFolder}/../dependencies/GLFW/include",
                "${workspaceFolder}/../dependencies/glad/include",
                "${workspaceFolder}/../dependencies/glm",
                "${workspaceFolder}/../dependencies/stb",
                "${workspaceFolder}/../dependencies/tinyobjloader",
                "${workspaceFolder}/../dependencies/tinyglTF"
            ],
...

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

#define TINYGLTF_IMPLEMENTATION
#define TINYGLTF_NO_STB_IMAGE_WRITE 
#define TINYGLTF_NOEXCEPTION
#define JSON_NOEXCEPTION 
#include "tiny_gltf.h"

Разберем четыре директивы define, используемые перед подключением заголовочного файла:

  • TINYGLTF_IMPLEMENTATION — указывает, что далее будет следовать определение функций, вложенных в заголовочный файл (объявляется только один раз в одном файле);
  • TINYGLTF_NO_STB_IMAGE_WRITE — указывает, что компонент библиотеки stb_image, отвечающий за сохранение текстур в файл, не требуется;
  • TINYGLTF_NOEXCEPTION — отключает выдачу исключений библиотекой-загрузчиком в случае ошибок;
  • JSON_NOEXCEPTION — отключает выдачу исключений библиотекой-парсером формата JSON.

Доработка класса Model

По умолчанию в методе Model::render в аргументах функции glDrawElements используется GL_UNSIGNED_INT в качестве типа данных элементов массива индексов. Иногда в файлах формата glTF в индексных буферах может использоваться GL_UNSIGNED_SHORT. Для учета типа данных добавим к классу Model приватное поле и модифицируем метод Model::set_index_range в файле include/Model.h:

class Model : public Node
{
    public:
...
        void set_index_range(size_t first_byteOffset, size_t count, size_t type = GL_UNSIGNED_INT); // Ограничение диапазона из буфера индексов
...
        size_t first_index_byteOffset, indices_count, indices_datatype; // Сдвиг в байтах для первого, количество индексов и тип данных индексов
...
};

В файле src/Model.cpp метод Model::set_index_range имеет следующий вид:

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

Дополним конструкторы и метод Model::render новым полем:


// Конструктор без параметров
Model::Model(Node *parent) : Node(parent), verteces_count(0), first_index_byteOffset(0), indices_count(0), indices_datatype(GL_UNSIGNED_INT), 
...
// Конструктор копирования
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), indices_datatype(copy.indices_datatype), 
...
// Вызов отрисовки без uniform-даных
void Model::render()
{
    // Подключаем VAO
    vao.use();
    // Если есть индексы - рисуем с их использованием
    if (indices_count)
    {
        index_vbo.use();
        glDrawElements(GL_TRIANGLES, indices_count, indices_datatype, (void*)(first_index_byteOffset));
    }
    // Если есть вершины - рисуем на основании массива вершин
    else if (verteces_count)
        glDrawArrays(GL_TRIANGLES, 0, verteces_count);
}

Так как формат буферов в файле glTF подразумевает загрузку данных напрямую в память видеокарты, то логично создавать буфер и прикреплять его к существующей модели. Для реализации такой функциональности добавим два публичных метода к классу модели в файле include/Model.h:

class Model : public Node
{
    public:
...
        void setBO(int attribute, BO & bo); // Замена вершинного буфера по номеру его привязки
        void setIndicesBO(BO & data); // Замена индексного буфера
...
};

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

// Замена вершинного буфера по номеру его привязки
void Model::setBO(int attribute, BO & bo)
{
    switch(attribute)
    {
        case 0:
            vertex_vbo = bo;
            break;
        case 1:
            texCoords_vbo = bo;
            break;
        case 2:
            normals_vbo = bo;
            break;
        case 3:
            tangent_vbo = bo;
            break;
        case 4:
            bitangent_vbo = bo;
            break;
        default:
            throw std::runtime_error("Unknown attribute buffer");
    };
}

// Замена индексного буфера
void Model::setIndicesBO(BO & data)
{
    index_vbo = data;
}

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

Доработка класса Camera

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

В имеющейся реализации класса Camera данные параметры константно заданы на этапе компиляции. Для полноценной работы с библиотекой дополним класс камеры в файле include/Camera.h:

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, float near = CAMERA_NEAR, float far = CAMERA_FAR); // Конструктор камеры с проекцией перспективы
        Camera(float width, float height, const glm::vec3 &position = glm::vec3(0.0f), const glm::vec3 &initialRotation = CAMERA_DEFAULT_ROTATION, float near = CAMERA_NEAR, float far = CAMERA_FAR); // Конструктор ортографической камеры
...
        void setPerspective(float fov, float aspect, float near = CAMERA_NEAR, float far = CAMERA_FAR); // Устанавливает заданную матрицу перспективы
        void setOrtho(float width, float height, float near = CAMERA_NEAR, float far = CAMERA_FAR); // Устанавливает заданную ортографическую матрицу
...
};

Изменения конструкторов и методов в файле src/Camera.cpp:

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

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

// Устанавливает заданную матрицу перспективы
void Camera::setPerspective(float fovy, float aspect, float near, float far)
{
    projection = glm::perspective(glm::radians(fovy), aspect, near, far);
    requiredRecalcVP = true;
    for (int cascade = 0; cascade < CAMERA_CASCADE_COUNT; cascade++)
        cascade_proj[cascade] = glm::perspective(glm::radians(fovy), aspect, camera_cascade_distances[cascade], camera_cascade_distances[cascade+1]);
}

// Устанавливает заданную ортографическую матрицу
void Camera::setOrtho(float width, float height, float near, float far)
{
    const float aspect = width / height;
    projection = glm::ortho(-1.0f, 1.0f, -1.0f/aspect, 1.0f/aspect, near, far);
    requiredRecalcVP = true;
    for (int cascade = 0; cascade < CAMERA_CASCADE_COUNT; cascade++)
        cascade_proj[cascade] = glm::ortho(-1.0f, 1.0f, -1.0f/aspect, 1.0f/aspect, camera_cascade_distances[cascade], camera_cascade_distances[cascade+1]);

}

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

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

Функция-загрузчик

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

Перед реализацией функции добавим в include/Model.h заголовочные файлы для glm, отвечающие за работу с кватернионами и их приведению к углам Эйлера:

#include <GLM/glm.hpp>
#include <GLM/gtc/type_ptr.hpp>
#include <GLM/gtc/quaternion.hpp>
#include <GLM/gtx/quaternion.hpp>
#include <GLM/gtx/euler_angles.hpp>

Функции-загрузчику достаточно одного адреса — с именем файла. Добавим в тот же файл объявление функции-загрузчика:

class Scene loadOBJtoScene(const char* filename, const char* mtl_directory = DEFAULT_MTL_DIR, const char* texture_directory = DEFAULT_MTL_DIR);
class Scene loadGLTFtoScene(std::string filename);

В файле src/Model.cpp подключим заголовочный файл tiny_gltf.h с необходимыми директивами препроцессора перед определением функции:

#define TINYGLTF_IMPLEMENTATION
#define TINYGLTF_NO_STB_IMAGE_WRITE 
#define TINYGLTF_NOEXCEPTION
#define JSON_NOEXCEPTION
#include "tiny_gltf.h"

Scene loadGLTFtoScene(std::string filename)
{
    ...

Создадим объект, который будет возвращен в конце функции, две строки для ошибок и предупреждений, а так же объекты загрузчика и модели в формате загрузчика:

    Scene result;

    tinygltf::TinyGLTF loader; // Объект загрузчика
    tinygltf::Model in_model;  // Модель в формате загрузчика
    std::string err;           // Строка под ошибки
    std::string warn;          // Строка под предупреждения

    tinygltf::TinyGLTF loader; // Объект загрузчика
    tinygltf::Model in_model; // Модель в формате загрузчика
    std::string err; // Строка под ошибки
    std::string warn; // Строка под предупреждения

Метод tinygltf::tinyGLTF::LoadASCIIFromFile (объекта loader) принимает в качестве аргументов адреса модели в формате загрузчика, строк под предупреждения и ошибки, а так же строку с именем файла. Данный метод возвращает логическую переменную, определяющую результат работы:

    bool success = loader.LoadASCIIFromFile(&in_model, &err, &warn, filename); // Загрузка из файла

Важное замечание: для бинарных файлов следует использовать

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

    // Если есть ошибки или предупреждения - выдадим исключение
    if (!err.empty() || !warn.empty()) 
        throw std::runtime_error(err + '\n' + warn);


    // Если все успешно считано - продолжаем загрузку
    if (success)
    {
         ...

Так как формат файла подразумевает, что данные хранятся в готовом для записи в буфер формате — пройдемся по всем границам доступа и подготовим буферные объекты (класс BO):


        // Загрузим данные в вершинные и индексные буферы
        std::vector<BO> BOs;
        for (auto & bufferView : in_model.bufferViews)
        {
            auto & buffer = in_model.buffers[bufferView.buffer];
            BOs.push_back(BO((BUFFER_TYPE)bufferView.target, buffer.data.data() + bufferView.byteOffset, bufferView.byteLength));
        }

Здесь используется параметры:

  • отступ с начала файлового буфера в байтах (bufferView.byteOffset);
  • длина буфера в байтах (bufferView.byteLength);
  • адрес начала массива данных (buffer.data.data() — метод у std::vector<char>);
  • тип буфера (bufferView.target — соответствует спецификации OpenGL), который требуется явно привести к перечислению (enum) BUFFER_TYPE из-за особенностей конструктора класса BO.

Так же заранее подготовим используемые текстуры, загрузив их в цикле по изображениям. Текстура может хранится в отдельном файле по указанному адресу или быть частью файлового буфера. В первом случае достаточно вызвать конструктор класса Texture, передав ему адрес изображения. Во втором случае следует определить формат и тип данных пикселя, после чего можно вызвать конструктор класса Texture, передав ему параметры загруженного в оперативную память изображения. В выборе нужного подхода можно отталкиваться от длинны строки с адресом изображения (image.uri.size() > 0)

Важное замечание: адрес изображения в формате glTF хранится в относительном виде от основного файла, так что следует отделить адрес директории от имени файла (filename.substr(0,filename.find_last_of(«/\\»)+1)).

        // Адрес директории для относительных путей изображений
        std::string dir = filename.substr(0,filename.find_last_of("/\\")+1);
        // Загрузим используемые текстуры 
        std::vector<Texture> textures;
        for (auto & image : in_model.images)
        {
            // Если длинна файла больше 0, то текстура в отдельном файле
            if (image.uri.size() > 0)
            {
                Texture tmp(TEX_AVAILABLE_COUNT,(dir + image.uri).c_str());
                textures.push_back(tmp);
            }
            else // иначе она является частью буфера
            {
                GLuint format = GL_RGBA;
                GLenum type = GL_UNSIGNED_BYTE;

                // Формат пикселя
                if (image.component == 1) 
                    format = GL_RED;
                else if (image.component == 2) 
                    format = GL_RG;
                else if (image.component == 3) 
                    format = GL_RGB;

                // Тип данных
                if (image.bits == 16) 
                    type = GL_UNSIGNED_SHORT;
                else if (image.bits == 32) 
                    type = GL_UNSIGNED_INT;

                Texture tmp( image.width, image.height
                            , image.image.data()
                            , 0, format, format
                            , type
                            );
                textures.push_back(tmp);
            }
        }

Для восстановления иерархии сцены сделаем два массива: по указателям на созданные узлы (соответствие с индексами узлов glTF) и по индексам на родительские узлы:

        // Указатели на узлы для построения иерархии родитель-потомок
        std::vector<Node *> pNodes(in_model.nodes.size(), NULL);
        // Индексы родителей (-1 - корневой узел сцены)
        std::vector<int> parents_id(in_model.nodes.size(), -1);

Запустим цикл по сценам:

        // Цикл по сценам
        for (auto &scene : in_model.scenes)
        { 

В формате glTF сцена имеет только узлы, являющиеся потомками для корневого. В целях упрощения работы с узлами выполним рекурсивный обход потомков и соберем их в массив (std::vector):

            // Так как у нас есть информация о потомках корневого узла сцены - пройдем рекурсивно и соберем все узлы из этой сцены:
            std::vector<int> scene_nodes;
            // Цикл по узлам рассматриваемой сцены с рекурсивным проходом потомков
            for (auto &node_id : scene.nodes)
                collectGLTFnodes(node_id, scene_nodes, in_model);

Реализация функции collectGLTFnodes:

void collectGLTFnodes(int node_id, std::vector<int> &nodes, tinygltf::Model &in_model)
{
    nodes.push_back(node_id);
    for (auto& child : in_model.nodes[node_id].children)
        collectGLTFnodes(child, nodes, in_model);
}

Вернемся к функции-загрузчику: далее пройдем по индексам узлов, относящимся к этой сцене (массив scene_nodes):

            // Цикл по всем узлам рассматриваемой сцены
            for (auto &node_id : scene_nodes)
            {
                auto &node = in_model.nodes[node_id];

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

                Node *tmpParent = &result.root; // Указатель на родителя, используется если узел сложный (несколько мешей или камера-меш)
                
                // Запишем текущий узел как родительский для потомков
                for (auto& child : node.children)
                    parents_id[child] = node_id;

                // Проверим наличие сложной сетки
                bool complex_mesh = false;
                // Если у узла есть полигональная сетка
                if (node.mesh > -1)
                    if (in_model.meshes[node.mesh].primitives.size() > 1)
                        complex_mesh = true;

                // Если узел составной: имеет и камеру, и полигональную сетку
                // или узел пустой
                // или имеет сложную полигональную сетку (примитивов больше одного)
                if (node.camera > -1 && node.mesh > -1 
                ||  node.camera == -1 && node.mesh == -1 
                ||  complex_mesh)
                {
                    // Создадим вспомогательный родительский узел для трансформаций
                    result.nodes.push_back(Node(&result.root)); 
                    pNodes[node_id] = tmpParent = &result.nodes.back(); // Сохраним в массив узлов и как родителя
                    // В противном случае дополнительный узел не требуется
                }

Если у узла есть полигональная сетка (меш) — обработаем её:

                // Обработаем полигональную сетку
                if (node.mesh > -1)
                {
                    auto &mesh = in_model.meshes[node.mesh];
                    
                    // Для каждого примитива связанного с полигональной сеткой
                    for (auto &primitive : mesh.primitives)
                    {
                        Model model(tmpParent); // Тут используется либо корневой узел сцены, либо вспомогательный узел

Для каждого примитива обработаем атрибуты, индексы и материалы:

                        // Цикл по атрибутам примитива
                        for (auto &attribute : primitive.attributes)
                        {
                            // Средство доступа
                            auto &accessor = in_model.accessors[attribute.second];
                            // Границы буфера
                            auto &bufferView = in_model.bufferViews[accessor.bufferView];

                            // Индекс привязки на шейдере
                            int attribute_index;
                            if (attribute.first.compare("POSITION") == 0)
                                attribute_index = 0;
                            else if (attribute.first.compare("TEXCOORD_0") == 0)
                                attribute_index = 1;
                            else if (attribute.first.compare("NORMAL") == 0)
                                attribute_index = 2;
                            else
                                continue;
                        
                            // Подключаем вершинный буфер
                            model.setBO(attribute_index, BOs[accessor.bufferView]);
                            BOs[accessor.bufferView].use();

                            glEnableVertexAttribArray(attribute_index);
                            // Определим спецификацию атрибута
                            glVertexAttribPointer( attribute_index // индекс атрибута, должен совпадать с Layout шейдера
                                                , tinygltf::GetNumComponentsInType(accessor.type) // количество компонент одного элемента
                                                , accessor.componentType // тип
                                                , accessor.normalized ? GL_TRUE : GL_FALSE // нормализованность значений
                                                , accessor.ByteStride(bufferView) // шаг
                                                , ((char *)NULL + accessor.byteOffset) // отступ с начала массива
                                                );
                        }

                        // Если есть индексы
                        if (primitive.indices > -1)
                        {
                            // Средство доступа для индексов
                            auto &accessor = in_model.accessors[primitive.indices];
                            // Границы индексного буфера
                            auto &bufferView = in_model.bufferViews[accessor.bufferView];

                            model.setIndicesBO(BOs[accessor.bufferView]);
                            model.set_index_range(accessor.byteOffset, accessor.count, accessor.componentType);
                        }
               
                        // Если есть материал
                        if (primitive.material > -1)
                        {
                            // Параметры материалов
                            auto &material = in_model.materials[primitive.material];
                            model.material.base_color = {material.pbrMetallicRoughness.baseColorFactor[0], material.pbrMetallicRoughness.baseColorFactor[1], material.pbrMetallicRoughness.baseColorFactor[2]};
                            model.material.metallic = material.pbrMetallicRoughness.metallicFactor;
                            model.material.roughness = material.pbrMetallicRoughness.roughnessFactor;
                            model.material.emitted = {material.emissiveFactor[0], material.emissiveFactor[1], material.emissiveFactor[2]};

                            if (material.pbrMetallicRoughness.baseColorTexture.index > -1)
                            {
                                textures[material.pbrMetallicRoughness.baseColorTexture.index].setType(TEX_ALBEDO);
                                model.set_texture(textures[material.pbrMetallicRoughness.baseColorTexture.index]);
                            }
                            if (material.pbrMetallicRoughness.metallicRoughnessTexture.index > -1)
                            {
                                textures[material.pbrMetallicRoughness.metallicRoughnessTexture.index].setType(TEX_METALLIC);
                                model.set_texture(textures[material.pbrMetallicRoughness.metallicRoughnessTexture.index]);
                                model.material.roughness = -2;
                            }
                            if (material.emissiveTexture.index > -1)
                            {
                                textures[material.emissiveTexture.index].setType(TEX_EMITTED);
                                model.set_texture(textures[material.emissiveTexture.index]);
                            }

                            auto specular_ext = material.extensions.find("KHR_materials_specular");
                            if (specular_ext != material.extensions.end())
                            {
                                if (specular_ext->second.Has("specularColorFactor"))
                                {
                                    auto &specular_color = specular_ext->second.Get("specularColorFactor");
                                    model.material.specular = (specular_color.Get(0).GetNumberAsDouble() + specular_color.Get(1).GetNumberAsDouble() + specular_color.Get(2).GetNumberAsDouble()) / 3;
                                }

                                if (specular_ext->second.Has("specularColorTexture"))
                                {
                                    auto &specular_texture = specular_ext->second.Get("specularColorTexture");
                                    int index = specular_texture.Get("index").GetNumberAsInt();
                                    if (index > -1)
                                    {
                                        textures[index].setType(TEX_SPECULAR);
                                        model.set_texture(textures[index]);
                                    }
                                }
                            }
                        }

Важное замечание: в формате glTF в качестве текстур шероховатости и металличности может использоваться одна двухканальная текстура — в таком случае значение шероховатости задается равным -2 для удобства определения на шейдере.

Полученную модель добавим к моделям сцены:

result.models.push_back(model); // Добавляем к сцене
                        // Если ещё не сохранили
                        if (!pNodes[node_id])
                            pNodes[node_id] = &result.models.back(); // Сохраним адрес созданного узла
                    }
                }

После обработки полигональной сетки нужно обработать камеру, если таковая имеется у рассматриваемого узла:

                // Обработаем камеру
                if (in_model.nodes[node_id].camera > -1)
                {
                    auto &in_camera = in_model.cameras[in_model.nodes[node_id].camera];
                    // Если камера использует проекцию перспективы
                    if (in_camera.type == "perspective")
                    {
                        Camera camera(in_camera.perspective.aspectRatio, glm::vec3(0.0f), CAMERA_DEFAULT_ROTATION, in_camera.perspective.yfov, in_camera.perspective.znear, in_camera.perspective.zfar);
                        result.cameras.push_back(camera);
                    }
                    // Иначе ортографическую проекцию
                    else
                    {
                        Camera camera(in_camera.orthographic.xmag, in_camera.orthographic.ymag, glm::vec3(0.0f), CAMERA_DEFAULT_ROTATION, in_camera.orthographic.znear, in_camera.orthographic.zfar);
                        result.cameras.push_back(camera);
                    }

                    // Если у узла есть полигональная сетка - сделаем камеру потомком модели, адрес которой записан в вектор
                    if (in_model.nodes[node_id].mesh > -1)
                        result.cameras.back().setParent(pNodes[node_id]);
                    // Иначе узел является камерой сам по себе
                    else
                    {
                        result.cameras.back().setParent(&result.root);
                        pNodes[node_id] = &result.cameras.back(); // Сохраним адрес созданного узла
                    }
                }
            }
        }

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

        // Зададим трансформацию и родителей для узлов
        // Цикл по всем индексам узлов
        for (int node_id = 0; node_id < in_model.nodes.size(); node_id++)
        {
            // Проверка на нулевой указатель
            if (pNodes[node_id])
            {

Есть два пути хранения данных о трансформации в glTF:

  • в виде матрицы — уже готовые преобразования;
  • в виде векторов смещения и масштабирования, а так же кватерниона поворота.

В случае с матрицей — разложим её на преобразования:

                // Если есть матрица трансформации - разберем её на составляющие
                if (in_model.nodes[node_id].matrix.size() == 16)
                {
                    glm::mat4 transform = glm::make_mat4(in_model.nodes[node_id].matrix.data());
                    pNodes[node_id]->e_position() = glm::vec3(transform[3][0], transform[3][1], transform[3][2]);
                    pNodes[node_id]->e_scale() = {glm::length(glm::vec3(transform[0][0], transform[1][0], transform[2][0])), glm::length(glm::vec3(transform[0][1], transform[1][1], transform[2][1])), glm::length(glm::vec3(transform[0][2], transform[1][2], transform[2][2]))};

                    for (int i = 0; i < 3; i++)
                        transform[i] = glm::normalize(transform[i]);
                    pNodes[node_id]->e_rotation() = glm::quat_cast(transform);
                }

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

                else
                {
                    // Если есть параметры трансформации
                    if (in_model.nodes[node_id].translation.size() == 3)
                        pNodes[node_id]->e_position() = glm::vec3(in_model.nodes[node_id].translation[0], in_model.nodes[node_id].translation[1], in_model.nodes[node_id].translation[2]);
                    if (in_model.nodes[node_id].rotation.size() == 4)
                        pNodes[node_id]->e_rotation() = glm::quat(in_model.nodes[node_id].rotation[3], glm::vec3(in_model.nodes[node_id].rotation[0], in_model.nodes[node_id].rotation[1], in_model.nodes[node_id].rotation[2]));
                    if (in_model.nodes[node_id].scale.size() == 3)
                        pNodes[node_id]->e_scale() = glm::vec3(in_model.nodes[node_id].scale[0], in_model.nodes[node_id].scale[1], in_model.nodes[node_id].scale[2]);
                }

Осталось установить родителя для узла, если таковой имеется.

                // Если индекс родителя > -1, то родитель создан и это не корневой узел сцены
                if (parents_id[node_id] > -1)
                    pNodes[node_id]->setParent(pNodes[parents_id[node_id]]);
            }
        }
    }

В фрагментном шейдере G-буфера (отложенный рендер) shaders/gshader.frag необходимо изменить сохранение параметров материалов с учетом двойной текстуры:

    // Если используется двухканальная текстура    
    if (roughness < -1)
    {
        // Сохранение шероховатости и металличности
        gRMS.rg = texture(tex_metallic, new_texCoord).bg;
    }
    else
    {
        // Сохранение шероховатости
        gRMS.r = roughness<0?texture(tex_roughness, new_texCoord).r:roughness;
        // Сохранение металличности
        gRMS.g = metallic<0?texture(tex_metallic, new_texCoord).r:metallic;
    }

В качестве примера можно взять модель из публичного репозитория Khronos Group / glTF-Sample-Models, либо использовать модель капли из репозитория resources. В качестве примера будет загружена модель капли в файле src/main.cpp:

    // Загрузка сцены из glTF файла
        std::vector<Scene> scenes = loadGLTFtoScene("../resources/models/blob.gltf"); 
    Scene& scene = scenes[0];

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

Заключение

В рамках данной заметки рассмотрен формат моделей glTF 2.0, а так же написана функция для загрузки статических сцен.

Проект доступен в публичном репозитории: 19.
Библиотеки: dependencies
Ресурсы: resources

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

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

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