# 3D implementation notes Контрольные заметки, сводки алгоритмов и остаточные семантические вопросы по 3D-подсистемам. --- ## 5.1. Порядок байт Все значения хранятся в **little‑endian** порядке (платформа x86/Win32). ## 5.2. Выравнивание - **NRes‑ресурсы:** данные каждого ресурса внутри NRes‑архива выровнены по границе **8 байт** (0‑padding). - **Внутренняя структура ресурсов:** таблицы Res1/Res2/Res7/Res13 не имеют межзаписевого выравнивания — записи идут подряд. - **Vertex streams:** stride'ы фиксированы (12/4/8 байт) — вершинные данные идут подряд без паддинга. ## 5.3. Размеры записей на диске | Ресурс | Запись | Размер (байт) | Stride | |--------|-----------|---------------|-------------------------| | Res1 | Node | 38 | 38 (19×u16) | | Res2 | Slot | 68 | 68 | | Res3 | Position | 12 | 12 (3×f32) | | Res4 | Normal | 4 | 4 (4×s8) | | Res5 | UV0 | 4 | 4 (2×s16) | | Res6 | Index | 2 | 2 (u16) | | Res7 | TriDesc | 16 | 16 | | Res8 | AnimKey | 24 | 24 | | Res10 | StringRec | переменный | `4 + (len ? len+1 : 0)` | | Res13 | Batch | 20 | 20 | | Res19 | AnimMap | 2 | 2 (u16) | | Res15 | VtxStr | 8 | 8 | | Res16 | VtxStr | 8 | 8 (2×4) | | Res18 | VtxStr | 4 | 4 | ## 5.4. Вычисление количества элементов Количество записей вычисляется из размера ресурса: ``` count = resource_data_size / record_stride ``` Например: - `vertex_count = res3_size / 12` - `index_count = res6_size / 2` - `batch_count = res13_size / 20` - `slot_count = (res2_size - 140) / 68` - `node_count = res1_size / 38` - `tri_desc_count = res7_size / 16` - `anim_key_count = res8_size / 24` - `anim_map_count = res19_size / 2` Для Res10 нет фиксированного stride: нужно последовательно проходить записи `u32 len` + `(len ? len+1 : 0)` байт. ## 5.5. Идентификация ресурсов в NRes Ресурсы модели идентифицируются по полю `type` (смещение 0) в каталожной записи NRes. Загрузчик использует `niFindRes(archive, type, subtype)` для поиска, где `type` — число (1, 2, 3, ... 20), а `subtype` (byte) — уточнение (из аргумента загрузчика). ## 5.6. Минимальный набор для рендера Для статической модели без анимации достаточно: | Ресурс | Обязательность | |--------|------------------------------------------------| | Res1 | Да | | Res2 | Да | | Res3 | Да | | Res4 | Рекомендуется | | Res5 | Рекомендуется | | Res6 | Да | | Res7 | Для коллизии | | Res13 | Да | | Res10 | Желательно (узловые имена/поведенческие ветки) | | Res8 | Нет (анимация) | | Res19 | Нет (анимация) | | Res15 | Нет | | Res16 | Нет | | Res18 | Нет | | Res20 | Нет | ## 5.7. Сводка алгоритмов декодирования ### Позиции (Res3) ```python def decode_position(data, vertex_index): offset = vertex_index * 12 x = struct.unpack_from(' list[str | None]: out = [] off = 0 for _ in range(node_count): ln = struct.unpack_from('> 8) & 1 size = FX_CMD_SIZE[op] cmds.append((op, enabled, ptr, size)) ptr += size if ptr != len(raw): raise ValueError('tail bytes after command stream') return cmds ``` ### Texm (header + mips + Page) ```python def parse_texm(raw: bytes): magic, w, h, mips, f4, f5, unk6, fmt = struct.unpack_from('<8I', raw, 0) assert magic == 0x6D786554 # 'Texm' bpp = 1 if fmt == 0 else (2 if fmt in (565, 556, 4444) else 4) pix_sum = 0 mw, mh = w, h for _ in range(mips): pix_sum += mw * mh mw = max(1, mw >> 1) mh = max(1, mh >> 1) off = 32 + (1024 if fmt == 0 else 0) + bpp * pix_sum page = None if off + 8 <= len(raw) and raw[off:off+4] == b'Page': n = struct.unpack_from(' MSH/NRes`) Ниже перечислено то, что нужно закрыть для **lossless round-trip** и 1:1‑поведения при импорте внешней геометрии обратно в формат игры. ### A) Неполная «авторская» семантика бинарных таблиц 1. `Res2` header (`первые 0x8C`): не зафиксированы все поля и правила их вычисления при генерации нового файла (а не copy-through из оригинала). 2. `Res7` tri-descriptor: для 16‑байтной записи декодирован базовый каркас, но остаётся неформализованной часть служебных бит/полей, нужных для стабильной генерации adjacency/служебной топологии. 3. `Res13` поля `unk4/unk6/unk14`: для парсинга достаточно, но для генерации «канонических» значений из голого `OBJ` правила не определены. 4. `Res2` slot tail (`unk30..unk40`): семантика не разложена, поэтому при экспорте новых ассетов нет детерминированной формулы заполнения. ### B) Анимационный path ещё не закрыт как writer 1. Нужен полный writer для `Res8/Res19`: - точная спецификация байтового формата на запись; - правила генерации mapping (`Res19`) по узлам/кадрам; - жёсткая фиксация округления как в x87 path (включая edge-case на границах кадра). 2. Правила биндинга узлов/строк (`Res10`) и `slotFlags` к runtime‑сущностям пока описаны частично и требуют формализации именно для импорта новых данных. ### C) Материалы, текстуры, эффекты для «полного ассета» 1. Для `Texm` не завершён writer, покрывающий все используемые режимы (включая palette path, mip-chain, `Page`, и правила заполнения служебных полей). 2. Для `FXID` известен контейнер/длины команд, но не завершена field-level семантика payload всех opcode для генерации новых эффектов, эквивалентных оригинальному пайплайну. 3. Экспорт только `OBJ` покрывает геометрию; для игрового ассета нужен sidecar-слой (материалы/текстуры/эффекты/анимация), иначе импорт неизбежно неполный. ### D) Что это означает на практике 1. `OBJ -> MSH` сейчас реалистичен как **ограниченный static-экспорт** (позиции/индексы/часть batch/slot структуры). 2. `OBJ -> полноценный игровой ресурс` (без потерь, с поведением 1:1) пока недостижим без закрытия пунктов A/B/C. 3. До закрытия пунктов A/B/C рекомендуется использовать режим: - геометрия экспортируется из `OBJ`; - неизвестные/служебные поля берутся copy-through из референсного оригинального ассета той же структуры.