Экспорт в 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/: как это выглядело на настоящей игре, включая то, что не сработало.