- PVSM.RU - https://www.pvsm.ru -

Как я разбирал нестандартный формат 3D-моделей, чтобы показывать Лего у себя на сайте

Как я разбирал нестандартный формат 3D-моделей, чтобы показывать Лего у себя на сайте - 1


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

Я перепробовал несколько редакторов 3D-моделей Лего (моим главным условием была работа на Linux, либо в вебе), и остановился на онлайн-редакторе Mecabricks [1]. Но, уже перенеся туда несколько из моих творений, понял, что с задачей «показывать всем друзьям» всё будет сложнее: у Mecabricks довольно скудные возможности экспорта, а его собственный формат с расширением .zmbx понимает только он и его плагин для Blender.

Поэтому я решил посмотреть, как этот формат устроен, и написать свой конвертер во что-то более общепринятое. В качестве целевого формата я выбрал glTF, а инструмент незатейливо назвал zmbx2gltf [2].

В этой статье я расскажу, как постепенно разбирал этот непонятный .zmbx, про устройство и преимущества glTF как формата передачи 3D-ассетов между разными инструментами, и про то, какие проблемы я решал, конвертируя одно в другое.


Исходники zmbx2gltf есть на GitHub [2], а 3D-модельки можно посмотреть у меня на сайте [3].

Часть 1: разбираем .zmbx

Описание всего, что мне удалось выяснить про этот формат, можно найти в репозитории [4] zmbx2glTF, в виде описания типов TypeScript. Здесь немного расскажу про то, как мне удалось всё это выяснить.

▍ Общая структура

Если мне в Unix-подобной системе попадается файл непонятного внутреннего устройства, то первое, что я делаю — скармливаю его утилите file [5]. Она умеет по различным «волшебным числам» и прочим косвенным признакам определять довольно много форматов файлов. Для моего .zmbx она вывела следующее:

$ file cab.zmbx
cab.zmbx: Zip archive data, at least v1.0 to extract, compression method=deflate

Файл .zmbx оказался ZIP-архивом. Вероятно, буква zcode> в расширении указывала именно на это, а mbx — сокращение от Mecabricks.

Заглянем внутрь этого архива:

$ unzip cab.zmbx
Archive:  cab.zmbx
  inflating: scene.mbx

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

А что внутри него самого?

$ file scene.mbx
scene.mbx: JSON text data

Кажется, нам повезло второй раз! Формат .mbx оказался основан на JSON, а это значит, что препарировать его будет чуть легче, чем какой-то бинарный файл.

Для разбора незнакомых JSON (да и знакомых тоже) я использую Visual Studio Code. В частности, там есть полезная фича «свернуть все блоки кода, но развернуть первый уровень». Для этого нужно с зажатым Shift нажать на стрелочку слева, которой блоки обычно сворачиваются. Перед этим нужно сказать VS Code, что .mbx — это на самом деле JSON (F1 - Change Language Mode - JSON), а также отформатировать файл, чтобы заработала подсветка кода.

Вот так выглядит файл после этих манипуляций:

Как я разбирал нестандартный формат 3D-моделей, чтобы показывать Лего у себя на сайте - 2

В поле metadata — объект с базовой информацией о файле.

{
  // всё, о чём я говорю, будет применимо только для этой версии:
  "version": [2, 0, 0],
  "date": "2023-01-11T09:43:49.552Z",
  "generator": "mecabricks" // о других генераторах мне не известно
}

Из остальных полей верхнего уровня, плюс-минус понятными выглядят только geometries и textures. Начнём с них.

▍ Текстуры

Объект textures содержит два поля — 1 и 2. Я предположил, что это номера версий форматов этих полей. Во всех моих экспериментах они отличались только тем, что в версии 2 есть дополнительное разделение на official — и custom-текстуры.

Внутри всё оказалось достаточно просто: текстуры разделены на категории (bump/normal/mask/color/data), внутри каждой категории — словарь «имя файла → base64-данные». Файлы всегда имели расширение .png, а формат base64-данных можно проверить моим любимым способом:

$ (base64 -d | file -) <<EOF
iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPm...
EOF
/dev/stdin: PNG image data, 128 x 128, 8-bit/color RGBA, non-interlaced

С color, bump, normal и metalness текстурами всё более-менее понятно; разбор остальных я решил отложить на потом.

▍ Геометрия

Поле geometries также поделено на две версии, но между ними мне также не удалось найти значимых отличий. За исключением одного: в версии 1 также присутствует объект metadata:

{
  "version": 3,
  "type": "Geometry",
  "vertices": 704,
  "generator": "io_three",
  "faces": 352,
  "normals": 162
}

Поле generator дало большую подсказку: io_three [6] — это инструмент для экспорта из Blender в формат, пригодный для three.js [7]. По коду этого инструмента можно понять формат хранения данных. Если коротко, массив faces хранит все грани в таком виде: сначала число, означающее флаги [8]: треугольная грань или четырёхугольная, есть ли данные о нормалях, UV-координатах и материалах; затем индексы в другие массивы для задания вершин, нормалей и UV-координат.

▍ Детали и конфигурации

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

Версий формата конфигураций тоже две, но из значимых для меня различий был только нейминг: конфигурации версии 1 названы в формате %id%.json, версии 2 — просто %id%.

Деталь (part) в этом формате — уже конкретный инстанс детали, заданный конфигурацией, материалом и матрицей аффинной трансформации (row-major). Окончательная модель составлена из этого набора деталей.

▍ Материалы

Материалы оказались единственными данными, которые не были указаны непосредственно в файле. Вместо них, там были только их числовые id. Я отправился гуглить, нашёл на просторах интернета куда больше одного списка цветов деталей Лего — конечно же, у всех были разные id. Путём перебора нашёл нужные данные в репозитории pnichols04/lego_colors [9] на GitHub. Примерно те же данные, только представленные немного по-другому, теперь хранятся и в моём репозитории [10].

Часть 2: выбираем, куда конвертировать

В мире уже существует очень много форматов 3D-моделей. Какой именно мне нужен, мне не было очевидно сразу — возможно, потому что я довольно далёк от сферы 3D-графики. Но я наметил к нему несколько основных требований:

  • Быть достаточно широко поддерживаемым.

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

  • Иметь спецификацию в открытом доступе.

    Тут всё просто: мне не очень хотелось реверс-инжинирить ещё один формат.

  • Быть текстовым, либо иметь текстовое представление.

    Текстовые форматы намного проще отлаживать. Как мы уже убедились в части 1, достаточно любой IDE, чтобы иметь возможность залезть к ним внутрь и посмотреть, что именно преобразовалось не так.

  • Поддерживать инстансинг геометрии.

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

  • Поддерживать текстуры (specular, normal, bump) как часть основного файла.

    Это не слишком критичное требование — большинство деталей всё-таки однотонные — но с ними получится всё-таки красивее.

Пройдясь по списку форматов [11] на Википедии, я обнаружил подходящий мне формат: glTF [12]. Он подходил под все мои требования. В частности, его текстовая форма была устроена довольно просто: это JSON-файл, в котором содержится несколько массивов сущностей — меши, текстуры, узлы графа сцены; если им нужно ссылаться друг на друга, они используют индексы в этих массивах.

Довольно понятная и подробная спецификация glTF есть в официальном репозитории [13]; можно также заглянуть в репозиторий [14] ко мне — там есть TypeScript-типы для JSON-формы glTF. Здесь я не буду его описывать подробно; расскажу лишь о значимых отличиях его от .mbx, и трудностях, которые возникли у меня при конвертации.

Часть 3: из .zmbx в glTF

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

Как я писал выше, в .mbx матрицы трансформации задаются в виде массива из 16 чисел, в row-major порядке. glTF же использует [15] column-major порядок. Превратить один в другой довольно несложно — нужно транспонировать [16] матрицу.

▍ PNG-картинки в Base64

В файле .mbx все изображения-текстуры заданы в формате PNG и закодированы в Base64. glTF тоже позволяет [17] использовать такое представление, но его нужно оформить в виде data URI [18]. Сделать это тоже несложно — фактически, нужно просто добавить в начало [19] префикс data:image/png;base64,.

▍ Цвета плюс декали

Для некоторых деталей Лего в .mbx-файлах указаны и основной цвет, и декаль (specular-текстура). Обычно основной цвет — это цвет пластик детали, а декаль представляет наклейку на ней. В glTF с этим строже — либо цвет, либо текстура. Поэтому понадобилось декодировать PNG [20], смешивать [21] его с основным цветом, а затем упаковывать обратно. Для этого я использовал библиотеку PNG.js [22].

▍ Нерешённая проблема: bump map + normal map

На некоторых деталях висит сразу и bump map, и normal map; glTF поддерживает только normal map. В целом, можно было бы преобразовать первую во вторую и смешать их, если бы не одно «но»: UV-координаты для этих текстур почти всегда разные. Здесь я решил сдаться; как смешивать текстуры с разными развёртками, я не придумал.

▍ Удаление неиспользуемых сущностей

Из-за того, что я в итоге поддерживаю не все фичи .mbx, в итоговый файл попадали сущности, которые нигде не использовались. Например, я мог конвертировать bump map, и только потом понять, что его не получится использовать. Я решил удалять такие сущности из выходного файла. Но нельзя было просто убрать сущности из массивов: тогда поехали бы индексы-ссылки. Поэтому я реализовал [23] обобщённый алгоритм перенумеровывания сущностей.

Для этого я позаимствовал из C++ идею ссылок, реализовав [24] их как пару «геттер»-«сеттер», которые в замыкании хранили объект и ключ поля, который они представляли. С помощью них же работает и дедупликация [25] сущностей — как оказалось, одна и та же текстура в .mbx-файле может быть продублирована для нескольких деталей.

Итоги

В итоге у меня получился инструмент [26] для преобразования .zmbx-файлов в .gltf-файлы. Преобразование вышло с потерями, но этого мне было, в целом, достаточно. Для своего сайта [3] я использовал Online3DViewer [27]; для меня его киллер-фичей стала возможность рисовать линиями ребра моделей — почти как в настоящих инструкциях Лего.

В плане реверс-инжиниринга, .zmbx оказался довольно простым, но это всё равно был ценный для меня опыт. Я надеюсь, что описание формата в этой статье и в репозитории [28] — насколько мне известно, единственное публично доступное — поможет и другим людям делать и другие инструменты.

Результат в виде гифки:

Как я разбирал нестандартный формат 3D-моделей, чтобы показывать Лего у себя на сайте - 3

И ещё

Как я разбирал нестандартный формат 3D-моделей, чтобы показывать Лего у себя на сайте - 4
Как я разбирал нестандартный формат 3D-моделей, чтобы показывать Лего у себя на сайте - 5

Автор: Илья Поздняков

Источник [29]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/lego/387110

Ссылки в тексте:

[1] Mecabricks: https://www.mecabricks.com/

[2] zmbx2gltf: https://github.com/iliazeus/zmbx2glTF

[3] у меня на сайте: https://iliazeus.github.io/legos/

[4] в репозитории: https://github.com/iliazeus/zmbx2glTF/blob/6729df3e/src/mbx/types.ts

[5] file: https://linux.die.net/man/1/file

[6] io_three: https://github.com/repsac/io_three

[7] three.js: https://threejs.org/

[8] флаги: https://github.com/iliazeus/zmbx2glTF/blob/6729df3e/src/mbx/types.ts#L122

[9] pnichols04/lego_colors: https://github.com/pnichols04/lego_colors

[10] моём репозитории: https://github.com/iliazeus/zmbx2glTF/blob/6729df3e/src/convert/data/colors.ts

[11] по списку форматов: https://en.wikipedia.org/wiki/List_of_file_formats#3D_graphics

[12] glTF: https://en.wikipedia.org/wiki/glTF

[13] в официальном репозитории: https://github.com/KhronosGroup/glTF

[14] в репозиторий: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/gltf/types.ts

[15] использует: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#data-alignment

[16] транспонировать: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/convert/utils.ts#L9

[17] позволяет: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#images

[18] data URI: https://en.wikipedia.org/wiki/Data_URI_scheme

[19] добавить в начало: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/convert/utils.ts#L3

[20] декодировать PNG: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/convert/materials.ts#L83

[21] смешивать: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/convert/materials.ts#L88

[22] PNG.js: https://www.npmjs.com/package/pngjs

[23] реализовал: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/gltf/optimizer.ts#L21

[24] реализовав: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/gltf/optimizer.ts#L3

[25] дедупликация: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/gltf/optimizer.ts#L56

[26] инструмент: https://github.com/iliazeus/zmbx2gltf

[27] Online3DViewer: https://github.com/kovacsv/Online3DViewer

[28] в репозитории: https://github.com/iliazeus/zmbx2gltf/blob/6729df3e/src/mbx/types.ts

[29] Источник: https://habr.com/ru/companies/ruvds/articles/759300/?utm_source=habrahabr&utm_medium=rss&utm_campaign=759300