Реверс-инжиниринг ресурсов игры LHX. Часть 4
К новым вершинам
В конце прошлого поста мне стало очевидно (остальным стало очевидно гораздо раньше), что расшифрованные модельки надо конвертировать во что‑то помоложе 90х годов и поуниверсальней двоичного дампа с кастомными командами. Иными словами, надо хоть как‑то вытащить геометрию из кастомного бинарного формата во что‑то, что поддерживается хоть одним 3D‑редактором. Далеко я не пошел — решил сконвертировать результаты в простой как палка.obj‑формат (а он из середины 90х, как оказывается…).
Не-расписывание OBJ-формата
Я, конечно же, не буду расписывать все подробности простого и известного формата, которому уже в районе 30 лет, но все же упомяну только ключевые особенности, которые я использовал:
Файл формата OBJ — это текстовый файл.
Каждый элемент геометрии как отдельная plain‑text строка.
Точка (vertex) описывается как «v X Y Z», где v — это именно буква v, а символы X, Y и Z — координаты точки (очевидно (надеюсь)). Строка с описанием точки может идти в любом месте файла.
Формат описания полигона (face) позволяет задавать полигон с произвольным количеством вершин. Определяется полигон через точки (в виде их номеров, конечно же). Можно опционально задавать для него и координаты текстур и даже координаты нормалей (и то, и другое это тоже точки, которые надо определить в файле, только не через v, а через vt и vn). У меня не было ни того, ни другого, так что я использовал описание в виде «f n1 n2 n3 … nm». Опять же, f — это именно буква f, а n1…nm — это номера точек, задающим полигон. Важные нюансы:
Все точки, описанные в файле ДО этого описания полигона, формируют собственно список доступных для перечисления в этом полигоне точек. Номер точки в описании — это номер именно в списке, это не номер строки. Отсчет идёт с 1.
Неочевидный трюк. Если номер указанной точки положительный — то берется N‑ая точка с начала списка. А вот если он отрицательный — то точка отсчитывается с конца списка (!).
Полигон формируется линиями обхода точек в указанном порядке.
Финальная линия полигона всегда идет от последней перечисленной точки к первой — и потому не указывается.
Если не учесть порядок обхода, то будет трэш (красная область на картинке — это вообще на откуп программе отрисовки, никаких гарантий одинаковой трактовки разными программами).
OBJ предполагает, что у полигона есть только одна сторона. И если смотреть на него с другой стороны — он прозрачен. В OBJ считается, что если смотреть перпендикулярно на видимую сторону, то точки перечисляются по часовой стрелке.
Полигоны можно не только затягивать текстурами, но и просто красить. Эпитет «просто» — это небольшое (небольшое) преувеличение. Для покраски понадобится:
Текстовый файл формата.mtl, в котором указываются параметры (их немало) «красок». Основные — название краски (строкой), цвет (не в RGB, а в значениях параметров модели отражения Фонга) и прозрачность.
Указание этого MTL файла в файле OBJ.
Указание названия краски в OBJ перед блоком с описанием геометрии, которую нужно покрасить.
Так как формат все же из 90-х, то он позволяет указывать отличные от полигонов примитивы, опирающиеся на точки. А именно — точки и линии. Только. На самом деле еще и сплайны (тоже ведь линии в каком‑то роде) и даже поверхности, но пишут, что никто особо не парился с реализацией поддержки. Так что линии и точки. Сфер нету. Чуть позже я к этому вернусь.
Переучет
Из поддерживаемого в OBJ у меня были:
Списки точек
Указания геометрии со ссылками на номера точек в списке:
Полигоны
Линии
Цвета и прозрачность
Из неподдерживаемого в OBJ у меня были:
Указания геометрии со ссылками на номера точек в списке:
Двусторонние полигоны
Невидимые полигоны
Полигоны с инвертированной нормалью
Линии, лежащие на полигоне
Точки
Сферы
Были бы еще и анимации, если бы я в них разобрался. Но я, к счастью, надавал по рукам своему перфекционисту — и не разобрался. Но, скорее всего, анимации задаются не в самой модели, а в коде.
А еще у меня было личное требование — чтобы в пределе любую модельку можно было бы посмотреть в любом современном просмотрщике/редакторе.
Замечательный 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 в окне рендера. Красные стрелки акцентируют внимание.
В‑третьих, в OBJ нету сфер совсем вообще. А в командах LHX и, как следствие, в модельках есть.
По этим и парочке других причин мне пришлось также освоить C#‑библиотеку поддержки параметров командной строки (которая незайтеливо называется CommandLine) — потому что вышеописанные нюансы требовали хоть какой‑то настройки.
Всё, что хоть немного выбивается из совсем уж (с моей точки зрения) логичного, в результирующем файле комментируется в некоторых подробностях.
Конвертация полигонов.
Если указаны просто полигоны — то конвертер соответственно просто генерит запись обычного полигона (f n1 n2 n3…).
Полигоны с инвертированной нормалью — смотря по параметрам:
Или скипает его полностью. Порой в модельках встречались именно прямой и инвертированный полигоны — чтобы сделать двусторонний. И если их оба транслировать — то они будут наслаиваться друг на друга.
(По умолчанию) Или честно его добавляет как обычный полигон с нужным направлением обхода.
Двусторонние полигоны — смотря по параметрам:
Или просто генерит один полигон с порядком обхода, указанным в изначальной команде. Куда будет смотреть нормаль — в общем случае неизвестно, поэтому она считается случайно выставленной.
(По умолчанию) Или все же генерит 2 полигона с противоположными порядками обхода, но располагает их на некотором (указываемом в параметрах запуска утилиты) расстоянии, чтобы избежать наложения и этих ужасных, ужасных артефактов.
Невидимые полигоны — тут все по‑честному: раз уж они невидимые, то ни цвета, ни нормали у них нету. Поэтому они транслируются просто как набор точек (здесь точка — это именно примитив, а не набор координат), на которые они опираются: «p n1, n2, n3…nm». Стандарт позволяет перечислять координаты нескольких графических примитивов «точка» в одной строке.
Конвертация линий.
Так как примитив «линия» не всегда отрисовывается, а видеть ее хочется всегда, то пришлось сделать линию из того, что отрисовывается всегда — полигонов. Конвертер умеет генерировать вместо линии четыре полигона, которые формируют узенький (ну как узенький — зависит от заданных параметров) параллелепипед квадратного сечения, опирающийся концами на концы линии.
Пришлось повозиться с торцами чтобы сделать их перпендикулярными исходной линии, но все получилось.
По итогу, для обеих команд из LHX — и «линия» и «линия, лежащая на полигоне» конвертер предлагает, в зависимости от параметра сделает следующее:
(По умолчанию) Или просто прописать в obj‑файле примитив line с нужными точками,
Или пропишет там параллелепипед‑замену с толщиной, согласно заданным параметрам.
Иллюстративное наложение линии (оранжевый) и сгенерированного параллелепипеда (черный)
Конвертация сфер
Если с линиями со стороны OBJ была хоть какая-то поддержка, то тут просто четкое и ясное — нет. Я покатился по наклонной и решил, что раз уж линию сымитировал, то и со сферой надо попробовать тоже. Но! Идеальная стопятьсотполигонная сфера — это, конечно, красиво, но не абсолютно не тру. Потому я решил поддержать дух старой школы и сделать сферу, которая минимально (ironic) жрет полигоны, но все же дает приличный результат. И поэтому конвертер умеет на месте сферы генерить полигоны, которые формируют правильный икосаэдр, вписанный в сферу нужного диаметра. По-моему, получилось миленько.
LHX. Голова стрелка — это сфера.
OBJ. Голова стрелка — это как бы сфера.
По итогу, если конвертер натыкается на команду нарисовать сферу, она, согласно параметрам:
(По умолчанию) Или моделирует полигонами икосаэдр нужного диаметра с центром в нужной точке.
Или указывает в строке‑комментарии в файле, что тут хотели сферу такого‑то диаметра с центром в такой‑то точке. И на этом наши полномочия собственно все. Это именно комментарий — и такая сфера нигде никогда не отрисуется.
Конвертация команды нарисовать именно точку.
Ситуация странная. И OBJ точку поддерживает (примитив point), и конвертер уже может просто икосаэдр сгенерить нужного размера, а это никому и не надо — я не помню моделей, которым нужно было рисовать точки. Но для ровного счета конвертер все же по параметрам:
(По умолчанию). Или сгенерит OBJ‑примитив point
Или икосаэдр заданного размера
Результаты
В результате я понакрутил параметры и сконверитровал исходные модели в аж 3 варианта моделек в формате OBJ:
Самые близкие к оригиналу и историческому духу (точки и линии как примитивы OBJ, двусторонние полигоны генерятся оба без промежутка, сферы только упоминаются). Смотреть на это можно только с глубоким уважением к корням.
Самые красиво выглядящие в современных просмотрщиках (точки и линии — как сферы и параллелепипеды, двусторонние полигоны генерятся с обоих сторон, но с промежутком, сферы как икосаэдры). Если хочется сделать что‑то с модельками дальше (например, в слайсер заправить), то это, как по мне, самый подходящий вариант.
Что‑то компромиссное (точки и линии — как сферы и параллелепипеды, один полигон вместо двух у двусторонних полигонов, сферы как икосаэдры). Просто потому что почему бы и нет?)
Стоит так же учесть, что некоторые элементы я не разгадал — и среди них явно был порядок отрисовки полигонов. Поэтому на том же Апаче идет полигон, отображающий часть корпуса, а на половину его точек завязаны полигоны кабины. И оба этих полигона лежат в одной плоскости. И моргают сквозь друг друга. Такова жизнь.
И ещё важный момент про анимацию. Я ее, повторюсь, не раскусил, а она в игре есть — поэтому некоторые объекты содержат все стадии анимации (кроме вращения) одновременно. С этим надо жить.
Шива
И 18-лопастной Ка-50
И вот теперь, после всех этих телодвижений — я наконец по‑настоящему достиг своей цели. Многолетний гештальт наконец закрыт, у меня на руках оказались obj файлы с практически всеми объектами из игры, а я все еще полнейший нубяра в DOS и дизассемблировании — это личный триумф!
Триумфальный коллаж
Cliffhanger 2
Но вот они — kaitai, хексредакторы, vscode, новые знания, а вот они — другие файлы ресурсов.
Может, поковыряться еще?