К новым вершинам

В конце прошлого поста мне стало очевидно (остальным стало очевидно гораздо раньше), что расшифрованные модельки надо конвертировать во что‑то помоложе 90х годов и поуниверсальней двоичного дампа с кастомными командами. Иными словами, надо хоть как‑то вытащить геометрию из кастомного бинарного формата во что‑то, что поддерживается хоть одним 3D‑редактором. Далеко я не пошел — решил сконвертировать результаты в простой как палка.obj‑формат (а он из середины 90х, как оказывается…).

Не-расписывание OBJ-формата

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

  1. Файл формата OBJ — это текстовый файл.

  2. Каждый элемент геометрии как отдельная plain‑text строка.

  3. Точка (vertex) описывается как «v X Y Z», где v — это именно буква v, а символы X, Y и Z — координаты точки (очевидно (надеюсь)). Строка с описанием точки может идти в любом месте файла.

  4. Формат описания полигона (face) позволяет задавать полигон с произвольным количеством вершин. Определяется полигон через точки (в виде их номеров, конечно же). Можно опционально задавать для него и координаты текстур и даже координаты нормалей (и то, и другое это тоже точки, которые надо определить в файле, только не через v, а через vt и vn). У меня не было ни того, ни другого, так что я использовал описание в виде «f n1 n2 n3 … nm». Опять же, f — это именно буква f, а n1..nm — это номера точек, задающим полигон. Важные нюансы:

    1. Все точки, описанные в файле ДО этого описания полигона, формируют собственно список доступных для перечисления в этом полигоне точек. Номер точки в описании — это номер именно в списке, это не номер строки. Отсчет идёт с 1.

    2. Неочевидный трюк. Если номер указанной точки положительный — то берется N‑ая точка с начала списка. А вот если он отрицательный — то точка отсчитывается с конца списка(!).

    3. Полигон формируется линиями обхода точек в указанном порядке.

      1. Финальная линия полигона всегда идет от последней перечисленной точки к первой — и потому не указывается.

      2. Если не учесть порядок обхода, то будет трэш (красная область на картинке — это вообще на откуп программе отрисовки, никаких гарантий одинаковой трактовки разными программами).

      3. OBJ предполагает, что у полигона есть только одна сторона. И если смотреть на него с другой стороны — он прозрачен. В OBJ считается, что если смотреть перпендикулярно на видимую сторону, то точки перечисляются по часовой стрелке.

  5. Полигоны можно не только затягивать текстурами, но и просто красить. Эпитет «просто» — это небольшое (небольшое) преувеличение. Для покраски понадобится:

    1. Текстовый файл формата.mtl, в котором указываются параметры (их немало) «красок». Основные — название краски (строкой), цвет (не в RGB, а в значениях параметров модели отражения Фонга) и прозрачность.

    2. Указание этого MTL файла в файле OBJ.

    3. Указание названия краски в OBJ перед блоком с описанием геометрии, которую нужно покрасить.

  6. Так как формат все же из 90-х, то он позволяет указывать отличные от полигонов примитивы, опирающиеся на точки. А именно — точки и линии. Только. На самом деле еще и сплайны (тоже ведь линии в каком‑то роде) и даже поверхности, но пишут, что никто особо не парился с реализацией поддержки. Так что линии и точки. Сфер нету. Чуть позже я к этому вернусь.

Переучет

Из поддерживаемого в OBJ у меня были:

  1. Списки точек

  2. Указания геометрии со ссылками на номера точек в списке:

    1. Полигоны

    2. Линии

  3. Цвета и прозрачность

Из неподдерживаемого в OBJ у меня были:

  1. Указания геометрии со ссылками на номера точек в списке:

    1. Двусторонние полигоны

    2. Невидимые полигоны

    3. Полигоны с инвертированной нормалью

    4. Линии, лежащие на полигоне

    5. Точки

    6. Сферы

  2. Были бы еще и анимации, если бы я в них разобрался. Но я, к счастью, надавал по рукам своему перфекционисту — и не разобрался. Но, скорее всего, анимации задаются не в самой модели, а в коде.

А еще у меня было личное требование — чтобы в пределе любую модельку можно было бы посмотреть в любом современном просмотрщике/редакторе.

Замечательный Kaitai Struct предлагает возможность сгенерировать листинг парсера на целой охапке языков из описания на Kaitai Struct DSL. То есть, в случае винды:

  • скачиваешь установщик, 

  • устанавливаешь, 

  • переходишь в папку с сочиненным описанием формата на языке Kaitai 

  • вызываешь программу с указанием интересующего тебя языка программирования - и он выдает программу парсера на этом язык. Если не указать желаемый язык - выхлоп будет на всех возможных.  Я выбрал C#, вбил вот такую команду в командную строку и kaitai выдал мне желаемое:

kaitai-struct-compiler -t csharp lhx_points.ksy

Получившийся парсер берет на вход исходные байты из указанного файла, парсит их согласно предоставленному kaitai‑описанию формата и предоставляет разложенные по полочкам данные. Подключаешь этот парсер в свое приложение — и оно начинает поддерживать этот формат.

Таким образом, у меня на руках было все, чтобы написать конвертер из формата LHX в формат OBJ (естественно, с кучей компромиссов и надуманных проблем).

Про конвертер

После некоторого количества времени, посвященного вдохновенному кодингу, получилась утилита‑конвертер (ссылка на гитхаб), которая делает следующее:

Шаг 1. Получает на вход параметры конвертации + путь к исходным файлам (их 2 ‑.pnt с точками и.bin с описанием геометрии) без указания расширений + путь к выходному файлу (их будет 2 или 4 — в зависимости от количества детализаций в модели — каждая получит свой obj и mtl файл, хайрез получит суффикс ‑med к имени — потому что будем честны, детализации там максимум на medium тянет). Если что‑то не так — выдаст хелп. Пример:

/code lhx2obj ‑infile init‑models\apache ‑outfile output‑models\apache

Шаг 2. Складывает все точки в obj (меняя знак координаты Y, потому что LHX рисует модельки зеркально — и этот факт было довольно сложно просто увидеть в игре, потому что практически все модельки или симметричны по Y, или хаотичны). Еще можно указать утилите, как трактовать исходные координаты — как XYZ или как XZY

Шаг 3. Транслирует геометрию из bin в obj с учетом выставленных пользователем предпочтений. Попутно, вдобавок к obj, генерится mtl файл с корректными цветами и значениями прозрачности материала.

Шаг 4. Сохраняет результат в итоговые файлы.

Особенности конвертации хочется осветить отдельно.

Освещенные отдельно особенности конвертации

Во‑первых, хоть OBJ и предполагает односторонность полигона, многие программы, которые его отображают, так не считают. Простой полигон часто рисуется 2-сторонним. А 2 полигона с противоположным направлением нормалей, но одинаковыми координатами — трактуются как 2 наложившихся 2х‑сторонних полигона. Выглядит не очень.

Не очень
Не очень

Во‑вторых, как я уже упоминал, линии как примитив в OBJ вполне себе поддерживаются и вполне себе игнорятся современными отрисовщиками. Blender их, конечно, покажет, но не отрендерит. Потому что а что рисовать‑то?

Моделька с примитивами line в Blender в окне моделирования
Моделька с примитивами line в Blender в окне моделирования
Моделька с примитивами line в Blender в окне рендера. Красные стрелки акцентируют внимание.
Моделька с примитивами line в Blender в окне рендера. Красные стрелки акцентируют внимание.

В‑третьих, в OBJ нету сфер совсем вообще. А в командах LHX и, как следствие, в модельках есть.

По этим и парочке других причин мне пришлось также освоить C#‑библиотеку поддержки параметров командной строки (которая незайтеливо называется CommandLine) — потому что вышеописанные нюансы требовали хоть какой‑то настройки.

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

Конвертация полигонов.

  1. Если указаны просто полигоны — то конвертер соответственно просто генерит запись обычного полигона (f n1 n2 n3...).

  2. Полигоны с инвертированной нормалью — смотря по параметрам:

    1. Или скипает его полностью. Порой в модельках встречались именно прямой и инвертированный полигоны — чтобы сделать двусторонний. И если их оба транслировать — то они будут наслаиваться друг на друга.

    2. (По умолчанию) Или честно его добавляет как обычный полигон с нужным направлением обхода.

  3. Двусторонние полигоны — смотря по параметрам:

    1. Или просто генерит один полигон с порядком обхода, указанным в изначальной команде. Куда будет смотреть нормаль — в общем случае неизвестно, поэтому она считается случайно выставленной.

    2. (По умолчанию) Или все же генерит 2 полигона с противоположными порядками обхода, но располагает их на некотором (указываемом в параметрах запуска утилиты) расстоянии, чтобы избежать наложения и этих ужасных, ужасных артефактов.

  4. Невидимые полигоны — тут все по‑честному: раз уж они невидимые, то ни цвета, ни нормали у них нету. Поэтому они транслируются просто как набор точек (здесь точка — это именно примитив, а не набор координат), на которые они опираются: «p n1, n2, n3…nm». Стандарт позволяет перечислять координаты нескольких графических примитивов «точка» в одной строке.

Конвертация линий

Так как примитив «линия» не всегда отрисовывается, а видеть ее хочется всегда, то пришлось сделать линию из того, что отрисовывается всегда — полигонов. Конвертер умеет генерировать вместо линии четыре полигона, которые формируют узенький (ну как узенький — зависит от заданных параметров) параллелепипед квадратного сечения, опирающийся концами на концы линии.

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

По итогу, для обеих команд из LHX — и «линия» и «линия, лежащая на полигоне» конвертер предлагает, в зависимости от параметра сделает следующее:

  1. (По умолчанию) Или просто прописать в obj‑файле примитив line с нужными точками,

  2. Или пропишет там параллелепипед‑замену с толщиной, согласно заданным параметрам.

Иллюстративное наложение линии (оранжевый) и сгенерированного параллелепипеда (черный) 
Иллюстративное наложение линии (оранжевый) и сгенерированного параллелепипеда (черный) 

Конвертация сфер

Если с линиями со стороны OBJ была хоть какая‑то поддержка, то тут просто четкое и ясное — нет. Я покатился по наклонной и решил, что раз уж линию сымитировал, то и со сферой надо попробовать тоже. Но! Идеальная стопятьсотполигонная сфера — это, конечно, красиво, но абсолютно не тру. Потому я решил поддержать дух старой школы и сделать сферу, которая минимально (ironic) жрет полигоны, но все же дает приличный результат. И поэтому конвертер умеет на месте сферы генерить полигоны, которые формируют правильный икосаэдр, вписанный в сферу нужного диаметра. По‑моему, получилось миленько.

LHX. Голова стрелка - это сфера.
LHX. Голова стрелка - это сфера.
OBJ. Голова стрелка - это как бы сфера. 
OBJ. Голова стрелка - это как бы сфера. 

По итогу, если конвертер натыкается на команду нарисовать сферу, она, согласно параметрам:

  1. (По умолчанию) Или моделирует полигонами икосаэдр нужного диаметра с центром в нужной точке.

  2. Или указывает в строке‑комментарии в файле, что тут хотели сферу такого‑то диаметра с центром в такой‑то точке. И на этом наши полномочия собственно все. Это именно комментарий — и такая сфера нигде никогда не отрисуется.

Конвертация команды нарисовать именно точку.

Ситуация странная. И OBJ точку поддерживает (примитив point), и конвертер уже может просто икосаэдр сгенерить нужного размера, а это никому и не надо — я не помню моделей, которым нужно было рисовать точки. Но для ровного счета конвертер все же по параметрам:

  1. (По умолчанию). Или сгенерит OBJ‑примитив point

  2. Или икосаэдр заданного размера

Результаты

В результате я понакрутил параметры и сконверитровал исходные модели в аж 3 варианта моделек в формате OBJ:

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

  2. Самые красиво выглядящие в современных просмотрщиках (точки и линии — как сферы и параллелепипеды, двусторонние полигоны генерятся с обоих сторон, но с промежутком, сферы как икосаэдры). Если хочется сделать что‑то с модельками дальше (например, в слайсер заправить), то это, как по мне, самый подходящий вариант.

  3. Что‑то компромиссное (точки и линии — как сферы и параллелепипеды, один полигон вместо двух у двусторонних полигонов, сферы как икосаэдры). Просто потому что почему бы и нет?)

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

И ещё важный момент про анимацию. Я ее, повторюсь, не раскусил, а она в игре есть — поэтому некоторые объекты содержат все стадии анимации (кроме вращения) одновременно. С этим тоже надо жить.

Шива
Шива
И 18-лопастной Ка-50
И 18-лопастной Ка-50

И вот теперь, после всех этих телодвижений — я наконец по‑настоящему достиг своей цели. Многолетний гештальт наконец закрыт, у меня на руках оказались obj файлы с практически всеми объектами из игры, а я все еще полнейший нубяра в DOS и дизассемблировании — это личный триумф!

Триумфальный коллаж
Триумфальный коллаж

Cliffhanger 2

Но вот они — kaitai, хексредакторы, vscode, новые знания, а вот они — другие файлы ресурсов. 

Может, поковыряться еще?

И я поковырялся еще.

Комментарии (2)


  1. Yoti
    20.09.2024 16:01
    +1

    Полигон формируется линиями обхода точек в указанном порядке.

    Что-то мне подсказывает, что серый текст под зелёным квадратом должен быть иным.


    1. unbalanced Автор
      20.09.2024 16:01

      Подправил картинку