3D-ассеты через агента Экспорт в GLB: оси, ориджины и честная проверка файла
0%

Экспорт в GLB: оси, ориджины и честная проверка файла

Экспорт в GLB: оси, ориджины и честная проверка файла

Модель готова и хорошо выглядит в рендере. Это ещё ничего не значит: движок получит не вашу сцену, а файл. Всё, чего в файле не окажется, для игры не существует.

Формат

Для веба и игровых движков стандарт — glTF 2.0, а практический выбор внутри него — .glb: один бинарный файл со встроенными текстурами. Вариант .gltf раскладывает JSON, бинарь и картинки по отдельным файлам — больше сущностей, больше способов потерять текстуру по дороге.

bpy.ops.export_scene.gltf(
    filepath="/абсолютный/путь/asset.glb",
    export_format='GLB',
    export_apply=True,
    export_yup=True,
)

Абсолютный путь обязателен. Рабочий каталог Blender — тот, из которого его запустили, а запускала его скрипт-обвязка. Относительный путь приведёт файл куда угодно, и искать вы его будете долго.

export_apply=True применяет модификаторы. По умолчанию он выключен (проверено на 4.5.11), и забыть его — самая обидная ошибка экспорта: в файл уедет исходный цилиндр без Solidify, без фасок, без подразделения. В Blender модель выглядит правильно (модификаторы считаются на лету), в движке — нет.

export_yup, наоборот, по умолчанию включён — его достаточно не трогать.

Оси: Z-up против Y-up

Blender считает верхом Z. glTF считает верхом Y. Договорённости исторические, спорить не с кем.

export_yup=True (по умолчанию включено) конвертирует при экспорте. Выключив его и повернув модель в рантайме, вы перекладываете исправление на каждого потребителя файла — и на всех, кто придёт после.

Правило: чините на границе, один раз, там, где различие возникает.

Ориджин — решение, а не умолчание

Ориджин — начало локальных координат объекта, точка, за которую его «держит» движок.

  • предмет, стоящий на поверхности — ориджин в основании, тогда установка на клетку это просто позиция, без поправки на половину высоты;
  • сцена или арена целиком — ориджин в центре на уровне пола;
  • всё, что движок вращает, — ориджин на оси вращения.
bpy.context.scene.cursor.location = (0, 0, 0)
bpy.ops.object.origin_set(type='ORIGIN_CURSOR')

Заодно примените трансформации, иначе масштаб уедет узловым преобразованием, которое часть импортёров молча игнорирует:

bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)

И зафиксируйте единицу измерения словами: 1 единица Blender = 1 клетка поля = 1 единица рантайма. Незадокументированный масштаб — самая частая причина модели, приехавшей стократ больше нужного.

Проверка файла, а не намерений

Экспортёр отчитался об успехе. Мы уже знаем из https://courses.digitable.life/post/3d-pipeline/04-seeing/, чего стоит success. Проверять надо файл.

GLB — бинарный контейнер, в начале которого лежит JSON-описание. Прочитать его можно без единой 3D-библиотеки:

import json, struct

with open("asset.glb", "rb") as f:
    magic, version, _ = struct.unpack("<III", f.read(12))
    assert magic == 0x46546C67, "не GLB"        # 'glTF' little-endian
    length, ctype = struct.unpack("<II", f.read(8))
    gltf = json.loads(f.read(length))

acc = gltf.get("accessors", [])
tris = sum(acc[p["indices"]]["count"] // 3
           for m in gltf.get("meshes", []) for p in m["primitives"]
           if p.get("indices") is not None and p.get("mode", 4) == 4)

print("меши:      ", [m["name"] for m in gltf.get("meshes", [])])
print("материалы: ", [m["name"] for m in gltf.get("materials", [])])
print("клипы:     ", [a.get("name") for a in gltf.get("animations", [])])
print("кости:     ", sum(len(s["joints"]) for s in gltf.get("skins", [])))
print("треугольники:", tris)

Это честная проверка: она читает то же, что прочитает движок, а не то, что Blender думал, будто записал.

Что показала кружка

Прогоним на кружке из https://courses.digitable.life/post/3d-pipeline/03-first-model/:

меши:       ['Cylinder']
материалы:  ['Ceramic']
клипы:      []
кости:      0
треугольники: 407884

407 884 треугольника. Файл — 9,4 МБ.

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

Прежде чем верить числу, проверим его вторым способом — иначе мы поступим ровно так, как эта статья не велит. У замкнутой триангулированной поверхности вершин примерно вдвое меньше, чем треугольников (следствие формулы Эйлера). В файле 203 942 вершины на 407 884 треугольника — отношение ровно 0,50. Сходится, и это уже не одно измерение, а два независимых.

Почему меш зовётся Cylinder, а объект — Mug

В выводе выше меш называется Cylinder, хотя скрипт делал body.name = "Mug". Это не ошибка экспорта. В Blender объект и его данные — разные сущности с разными именами: primitive_cylinder_add создаёт объект Cylinder с блоком данных меша Cylinder, а body.name переименовывает только объект. Экспортёр кладёт имя объекта в узел (nodes), а имя блока данных — в меш (meshes), и в файле честно лежат оба: узел Mug, меш Cylinder.

Переименовывать надо оба, если рантайм ищет по имени:

body.name = "Mug"
body.data.name = "Mug"

Откуда столько? Из цепочки модификаторов. Цилиндр на 96 сегментов, Solidify удваивает оболочку, Bevel добавляет по три сегмента на ребро, Subsurf учетверяет всё это на каждом уровне. Каждый шаг разумен, произведение — катастрофа. В рендере это незаметно: 400 тысяч треугольников Cycles переварит и не поморщится.

Заметно станет в игре, когда таких объектов на сцене двадцать.

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

Сокращать есть чем: понизить render_levels у Subsurf, взять цилиндр на 32 сегмента вместо 96, применить Decimate. Начинать — с измерения.

Сжатие

glTF умеет Draco:

    export_draco_mesh_compression_enable=True,

Обычно даёт кратное сокращение геометрии, но требует декодера на стороне рантайма и стоит времени при загрузке. Измеряйте на своём ассете и своём движке — «Draco всегда лучше» такое же суеверие, как «EEVEE всегда быстрее» из https://courses.digitable.life/post/3d-pipeline/05-materials/.

Имена — это контракт

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

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

Дальше — https://courses.digitable.life/post/3d-pipeline/07-game-assets/: как это выглядело на настоящей игре, включая то, что не сработало.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

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

Доска запросов
Дальше