Основы формата GLTF и GLB, часть 1

    Что такое GLTF и GLB?


    GLTF (GL Transmission Format) — это формат файла для хранения 3Д сцен и моделей, который является крайне простым в понимании (структура записана в стандарте JSON), расширяемым и легко взаимодействующим с современными веб-технологиями. Данный формат хорошо сжимает трёхмерные сцены и минимизирует обработку во время выполнения приложений, использующих WebGL и другие API. GLTF сейчас активно продвигается Khronos Group как JPEG от мира 3D. На сегодняшний день используется GLTF версии 2.0. Существует и бинарная версия данного формата, которая называется GLB, единственное различие которого в том, что все хранится в одном файле с расширением GLB.


    Эта статья — 1 часть из 2х. В ней мы с вами рассмотрим такие артефакты формата и их атрибуты, как Scene, Node, Buffer, BufferView, Accessor и Mesh. А во второй статье мы рассмотрим оставшиеся: Material, Texture, Animations, Skin и Camera. Больше общей информации о формате можно найти здесь.
    Если в процессе просмотра статьи захочется лично поработать с данным форматом, то можете скачать модели GLTF 2.0 с официального репозитория Khronos на GitHub


    image


    Проблематика и её решение


    Изначально GLTF формат был задуман Khronos Group как решение для передачи 3D контента по интернету и был призван минимизировать количество импортеров и конвертеров, разные виды которых создаются при работе с графическими API.


    image

    На текущий момент GLTF и его бинарный брат GLB используются как унифицированные форматы и в CAD программах (Autodesk Maya, Blender и т. д.), в игровых движках (Unreal Engine, Unity и прочих), AR/VR приложениях, соц. сетях и т.д.


    Представители Khronos Group утрвеждают следующее:


    1. GLTF универсален — может использоваться одинаково хорошо как для простой геометрии, так и для сложных сцен с анимацией, различными материалами и т. д.
    2. Он достаточно компактен. Да, это можно оспорить, ведь всё зависит от от алгоритмов конвертации и я лично знаю случаи, когда GLTF был больше размером, чем оригинальный, к примеру, FBX файл, но в большинстве случаев это так.
    3. Простота анализа данных – это корневой плюс данного формата. GLTF иерархия использует JSON, а геометрия хранится в бинарном виде, никакого декодинга не нужно!

    Система координат и единицы измерения


    GLTF использует правостороннюю систему координат, то есть перекрестное произведение +X и +Y дает +Z, где +Y — верхняя ось. Передняя часть 3D ассета GLTF обращена к оси +Z. Единицами измерения для всех линейных расстояний являются метры, углы же измеряются в радианах а положительное вращение объектов — против часовой стрелки. Node трансформации и channel paths анимаций являются трехмерными векторами или кватернионами со следующими типами данных и семантикой:


    translation: трехмерный вектор, содержащий перевод по осям x, y и z
    rotation: кватернион (x, y, z, w), где w скаляр
    scale: трехмерный вектор, содержащий коэффициенты масштабирования по осям x, y и z
    image


    GLTF — взгляд изнутри


    Как было сказано выше GLTF, как правило, состоит из 2х файлов: 1й с форматом .gltf, который хранит в себе структуру 3D сцены в виде JSON и 2й файл с форматом .bin, который хранит уже непосредственно все данные этой сцены.


    Структура формата строго иерархическая и имеет следующий вид:


    image


    Рассказывая далее о структуре я буду использовать примеры простейшего GLTF файла, который хранит в себе 1 односторонний треугольник с материалом по умолчанию. Если захотите, то вы можете его скопировать и вставить в любой GLTF просмотрщик, чтобы "пощупать" содержимое файла лично. В своей практике я использовал разные, но остановился на этом, который использует Three.js под капотом. Также хорошей опцией будет использование Visual Studio Code с GLTF плагином. Так у вас появится выбор сразу из 3х движков: Babylon.js, Cesium, Three.js


    Scene и Node элементы


    Первым-наперво идет основная нода под названием Scene. Это корневая точка в файле, с которой все и начинается. Данная нода содержит массив сцен, которые хранит GLTF и выбор той, которая будет грузится по умолчанию после открытия файла. Контент же 3D сцены начинается со следующего объекта, который называется “Node”. Массив сцен и нод был упомянут не зря, т.к. возможность хранить несколько сцен в одном файле реализована, но на практике стараются хранить одну сцену в одном файле.


    {
      "scenes" : [
        {
          "nodes" : [ 0 ]
        }
      ],
    
      "nodes" : [
        {
          "mesh" : 0
        }
      ],
    "scene": 0

    Каждая нода является “входной точкой” для описания отдельных объектов. Если объект сложный и состоит из нескольких мешей, то такой объект будет описан «родительской» и «дочерними» нодами. Например, автомобиль, который состоит из корпуса и колес, может быть описан следующим образом: основная нода описывает машину и, в частности, ее корпус. В этой ноде содержится список “дочерних нод”, которые, в свою очередь, описывают уже оставшиеся составные части, такие как, к примеру, колеса. Обработка всех элементов будет осуществляться рекурсивно. Ноды могут иметь TRS (translation, rotation, scale a.k.a. смещени е, поворот и масштабирование) анимации. Кроме того, что такие трансформации влияют непосредственно на сам меш, они точно также воздействуют и на дочерние ноды. В довесок ко всему вышесказанному думаю стоит упомянуть, что внутренние "камеры", если таковые имеются, которые отвечают за отображение для пользователя объекта в кадре, также прикреплены к объекта Node. Объекты ссылаются друг на друга используя соответствующий атрибуты: scene имеет атрибут node, node объект имеет атрибут mesh. Для более простого понимания всё вышесказанное проилюстрировано на следующем рисунке.


    image


    Buffer, BufferView и Accessor


    Под объектом Buffer подразумевается хранилище бинарных, не обработанных, данных без структуры, без наследования, без значения. В буфере хранится информация о геометрии, анимациях и скиннинге. Главное преимущество бинарных данных в том, что они крайне эффективно обрабатываются GPU, т.к. не требуют дополнительного парсинга, кроме, возможно, декомпрессии. Данные в буфере могут быть найдены по атрибуту URI, который явно дает понять где находятся данные и здесь всего 2 варианта: либо данные хранятся во внешнем файле с форматом .bin, либо они встроены внутрь самого JSON. В первом случае URI содержит ссылку на внешний файл, в этом случае папка, в которой находится GLTF файл, считается корневой. Во втором случае файл будет иметь формат .glb, отсылающий нас к более компактному, с точки зрения количества файлов, брату-близнецу GLTF, формату GLB. Данные в бинарном файле хранятся как есть, побайтово.



    JSON в нашем примере с треугольником будет выглядеть следующим образом:
    Пример буфера, закодированного в base64:


    "buffers" : [
        {
          "uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
          "byteLength" : 44
        }
      ],

    Если же у вас будет внеший файл, то JSON преобразует свой вид в следующий:


    "buffers" : [
        {
          "uri" : "duck.bin",
          "byteLength" : 102040
        }
      ],

    Блок Buffers также имеет дополнительный атрибут byteLength, который хранит в себе значение размера буфера.


    Первым шагом в структуризации данных из буфера служит объект BufferView. BufferView можно назвать "срезом" информации из Buffer, который характеризуется определенным сдвигом байт от начала буфера. Данный "срез" описывается при помощи 2х атрибутов: отсчет “сдвига” от начала буфера для считывания и длинной самого среза. Простой пример нескольких объектов BufferView для наглядности их использования на основе нашего примера:


     "bufferViews" : [
        {
          "buffer" : 0,
          "byteOffset" : 0,
          "byteLength" : 6,
          "target" : 34963
        },
        {
          "buffer" : 0,
          "byteOffset" : 8,
          "byteLength" : 36,
          "target" : 34962
        }
      ],

    Как вы видите, в данном примере содержится 4 основных атрибута:


    1. Buffer указывает на индекс буфера (порядковый номер в массиве буферов, начинается с 0).
    2. byteOffset — определяет “сдвиг” начала отсчета в байтах для данного “среза”
    3. byteLength — определяет длину “среза”
    4. target — определяет тип данных, содержащихся в bufferView
      Первый BufferView содержит первые 6 байт буфера и не имеет сдвига. Со вторым "срезом" все немного сложнее: как видите, его сдвиг находится на 8м байте, вместо ожидаемого 6го. Данные 2 байта являются пустыми и были добавлены в процессе формирования буфера благодаря процессу под названием "padding". Оно нужно, чтобы значение подогнать значение байт границы в 4 байта. Такой трюк нужен для более быстрого и легкого считывания данных из буфера.

    image


    Стоит сказать еще пару слов об атрибуте target. Он используется для классификации типа информации на которую ссылается bufferView. Здесь всего 2 варианта: либо это будет значение 34962, которое используется для ссылки на атрибуты вертексов (vertex attributes — 34962 — ARRAY_BUFFER) или же 34963, которое используется для индексов вертексов (vertex indices — 34963 — ELEMENT_ARRAY_BUFFER). Последним штрихом при для понимания и структуризации всей информации в Buffer является объект Accessor.


    Accessor — это объект, который обращается к BufferView и содержит атрибуты, которые определяют тип и расположение данных из BufferView. Тип данных аксессора кодируется в type и componentType. Значением атрибута type является строка и имеет следующие значения: SCALAR для скалярных значений, VEC3 для 3х мерных векторов и MAT4 для матрицы размерностью 4х4 или же кватерниона, который используется для описания rotation (поворота).


    В свою очередь componentType указывает тип компонентов этих данных. Это GL константа, которая может иметь такие значение, как, к примеру, 5126 (FLOAT) или 5123 (UNSIGNED_SHORT), для указания того, что элементы имеют плавающую запятую и т.п.


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


      "accessors" : [
        {
          "bufferView" : 0,
          "byteOffset" : 0,
          "componentType" : 5123,
          "count" : 3,
          "type" : "SCALAR",
          "max" : [ 2 ],
          "min" : [ 0 ]
        },
        {
          "bufferView" : 1,
          "byteOffset" : 0,
          "componentType" : 5126,
          "count" : 3,
          "type" : "VEC3",
          "max" : [ 1.0, 1.0, 0.0 ],
          "min" : [ 0.0, 0.0, 0.0 ]
        }
      ],

    Разберём атрибуты, представленные в JSON:


    1. bufferView — указывает порядковый номер BufferView из массива BufferView, который использует Accessor. BufferView же, в свою очередь, хранит информацию об индексах.
    2. byteOffset — сдвиг байт для начала считывания данных текущим Accessor. На один BufferView может ссылаться несколько объектов типа Accessor.
    3. componentType — константа, указывающая на тип элементов. Может иметь значения 5123, которой соответствует тип данных UNSIGNED_SHORT или же 5126 для FLOAT.
    4. count — отображает как много элементов хранится в buffer.
    5. type — определяет тип данных: скаляр, вектор, матрица.
    6. max и min — атрибуты, которые определяют минимальное и максимальное значение положение данных элементов в пространстве.

    Mesh


    Объект Meshes содержит информацию о мешах, расположенных в сцене. Одна нода (node объект) может хранить только 1 меш. Каждый объект типа mesh содержит массив типа mesh.primitive, в свою очередь примитивы — это примитивные объекты (к примеру треугольники) из которых состоит непосредственно меш. Данный объект содержит много дополнительных атрибутов, но все это служит одной цели — правильному хранению информации об отображении объекта. Основные атрибуты меша:


    1. POSITION — позиция вертексов по осям XYZ
    2. NORMAL — нормализованные XYZ нормали вертексов
    3. TANGENT — XYZW тангентсы вертексов. W указывает куда направлен тангент и имеет значечние либо +1, либо -1.
    4. TEXCOORD_0 — текстурные координаты UV. Может хранится несколько наборов.
    5. COLOR_0 — RGB или RGBA цвета вертексов.
    6. JOINTS_0 — данный атрибут содержит индексы суставов/джоинтов (Joints) из соответствующего массива joints, которые должны влиять на вертекс (вершину).
    7. WEIGHTS_0 — данные этого атрибута определяют веса, указывающие насколько сильно сустав/joint влияет на вершину.
    8. weights — атрибут, отвечающий за веса морфинга.
    9. material — содержит индекс, который является номером материала в массиве Materials

    Данный объект будет иметь следующий вид для нашего случая:


      "meshes" : [
        {
          "primitives" : [ {
            "attributes" : {
              "POSITION" : 1
            },
            "indices" : 0
          } ]
        }
      ],

    К сожалению из-за ограничения весь материал не вместился с одну статью, поэтому оставшуюся часть можно найти во второй статье, в которой мы рассмотрим оставшиеся артефакты: Material, Texture, Animations, Skin и Camera, а также соберём минимальный рабочий GLTF файл.


    Продолжение во 2й части: https://habr.com/ru/post/448298/

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 3

    • UFO just landed and posted this here
        0
        Всё верно, в случае инстансинга нода ссылается на существующий меш, а трансформация ноды уже перемещает инстанс по сцене.
          0
          Это очень нужно, и до GLTF инстансы по сути были реализованы (через правильное место и вариативно) только в FBX.

          А ещё в алембике и USD, а ещё их можно было самостоятельно реализовать в колладе

        Only users with full accounts can post comments. Log in, please.