aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/msh-notes.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs/msh-notes.md')
-rw-r--r--docs/specs/msh-notes.md321
1 files changed, 81 insertions, 240 deletions
diff --git a/docs/specs/msh-notes.md b/docs/specs/msh-notes.md
index 1bd4808..6e77c4f 100644
--- a/docs/specs/msh-notes.md
+++ b/docs/specs/msh-notes.md
@@ -1,277 +1,118 @@
# 3D implementation notes
-Контрольные заметки, сводки алгоритмов и остаточные семантические вопросы по 3D-подсистемам.
+Контрольная страница с практическими правилами реализации 3D-пайплайна и с перечнем незакрытых зон.
+Документ intentionally high-level: без ссылок на внутренние функции/адреса.
----
+Связанные страницы:
-## 5.1. Порядок байт
+- [MSH core](msh-core.md)
+- [MSH animation](msh-animation.md)
+- [Material (`MAT0`)](material.md)
+- [Texture (`Texm`)](texture.md)
+- [FXID](fxid.md)
+- [Render pipeline](render.md)
-Все значения хранятся в **little‑endian** порядке (платформа x86/Win32).
+## 1. Базовые двоичные правила
-## 5.2. Выравнивание
+1. Все форматы в этой подсистеме little-endian.
+2. Внутри NRes данные ресурсов выравниваются по 8 байт.
+3. Внутри payload таблиц padding между записями обычно отсутствует: записи идут подряд по stride.
-- **NRes‑ресурсы:** данные каждого ресурса внутри NRes‑архива выровнены по границе **8 байт** (0‑padding).
-- **Внутренняя структура ресурсов:** таблицы Res1/Res2/Res7/Res13 не имеют межзаписевого выравнивания — записи идут подряд.
-- **Vertex streams:** stride'ы фиксированы (12/4/8 байт) — вершинные данные идут подряд без паддинга.
+## 2. Быстрая карта stride'ов
-## 5.3. Размеры записей на диске
+| Ресурс | Запись | Stride |
+|---|---|---:|
+| Res1 | Node | 38 |
+| Res2 | Slot | 68 (после header `0x8C`) |
+| Res3 | Position | 12 |
+| Res4 | Normal | 4 |
+| Res5 | UV0 | 4 |
+| Res6 | Index | 2 |
+| Res7 | Tri descriptor | 16 |
+| Res8 | Animation key | 24 |
+| Res13 | Batch | 20 |
+| Res19 | Animation map | 2 |
-| Ресурс | Запись | Размер (байт) | 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 |
+## 3. Декодирование ключевых потоков
-## 5.4. Вычисление количества элементов
+## 3.1. Позиции (Res3)
-Количество записей вычисляется из размера ресурса:
+`float3`, stride `12`.
-```
-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('<f', data, offset)[0]
- y = struct.unpack_from('<f', data, offset + 4)[0]
- z = struct.unpack_from('<f', data, offset + 8)[0]
- return (x, y, z)
-```
-
-### Нормали (Res4)
-
-```python
-def decode_normal(data, vertex_index):
- offset = vertex_index * 4
- nx = struct.unpack_from('<b', data, offset)[0] # int8
- ny = struct.unpack_from('<b', data, offset + 1)[0]
- nz = struct.unpack_from('<b', data, offset + 2)[0]
- # nw = data[offset + 3] # не используется
- return (
- max(-1.0, min(1.0, nx / 127.0)),
- max(-1.0, min(1.0, ny / 127.0)),
- max(-1.0, min(1.0, nz / 127.0)),
- )
-```
+## 3.2. Нормали (Res4)
-### UV‑координаты (Res5)
+`int8[4]`, используются первые 3 компоненты:
-```python
-def decode_uv(data, vertex_index):
- offset = vertex_index * 4
- u = struct.unpack_from('<h', data, offset)[0] # int16
- v = struct.unpack_from('<h', data, offset + 2)[0]
- return (u / 1024.0, v / 1024.0)
+```text
+n = clamp(s8 / 127.0, -1..1)
```
-### Кодирование нормали (для экспортёра)
+## 3.3. UV (Res5)
-```python
-def encode_normal(nx, ny, nz):
- return (
- max(-128, min(127, int(round(nx * 127.0)))),
- max(-128, min(127, int(round(ny * 127.0)))),
- max(-128, min(127, int(round(nz * 127.0)))),
- 0 # nw = 0 (безопасное значение)
- )
-```
-
-### Кодирование UV (для экспортёра)
-
-```python
-def encode_uv(u, v):
- return (
- max(-32768, min(32767, int(round(u * 1024.0)))),
- max(-32768, min(32767, int(round(v * 1024.0))))
- )
-```
+`int16[2]`:
-### Строки узлов (Res10)
-
-```python
-def parse_res10_for_nodes(buf: bytes, node_count: int) -> list[str | None]:
- out = []
- off = 0
- for _ in range(node_count):
- ln = struct.unpack_from('<I', buf, off)[0]
- off += 4
- if ln == 0:
- out.append(None)
- continue
- raw = buf[off:off + ln + 1] # len + '\0'
- out.append(raw[:-1].decode('ascii', errors='replace'))
- off += ln + 1
- return out
+```text
+u = s16 / 1024.0
+v = s16 / 1024.0
```
-### Ключ анимации (Res8) и mapping (Res19)
+## 3.4. Animation key (Res8)
-```python
-def decode_anim_key24(buf: bytes, idx: int):
- o = idx * 24
- px, py, pz, t = struct.unpack_from('<4f', buf, o)
- qx, qy, qz, qw = struct.unpack_from('<4h', buf, o + 16)
- s = 1.0 / 32767.0
- return (px, py, pz), t, (qx * s, qy * s, qz * s, qw * s)
-```
+`pos(float3) + time(float) + quat(int16x4)`:
-### Эффектный поток (FXID)
-
-```python
-FX_CMD_SIZE = {1:224,2:148,3:200,4:204,5:112,6:4,7:208,8:248,9:208,10:208}
-
-def parse_fx_payload(raw: bytes):
- cmd_count = struct.unpack_from('<I', raw, 0)[0]
- ptr = 0x3C
- cmds = []
- for _ in range(cmd_count):
- w = struct.unpack_from('<I', raw, ptr)[0]
- op = w & 0xFF
- enabled = (w >> 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
+```text
+q = s16 / 32767.0
```
-### 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('<I', raw, off + 4)[0]
- page = [struct.unpack_from('<4h', raw, off + 8 + i * 8) for i in range(n)]
- return (w, h, mips, fmt, f4, f5, unk6, page)
-```
-
----
-
-# Часть 6. Остаточные семантические вопросы
-
-Пункты ниже **не блокируют 1:1-парсинг/рендер/интерполяцию** (все бинарные структуры уже определены), но их человеко‑читаемая трактовка может быть уточнена дополнительно.
-
-## 6.1. Batch table — смысл `unk4/unk6/unk14`
-
-Физическое расположение полей известно, но доменное имя/назначение не зафиксировано:
-
-- `unk4` (`+0x04`)
-- `unk6` (`+0x06`)
-- `unk14` (`+0x0E`)
-
-## 6.2. Node flags и имена групп
-
-- Биты в `Res1.hdr0` используются в ряде рантайм‑веток, но их «геймдизайн‑имена» неизвестны.
-- Для group‑индекса `0..4` не найдено текстовых label'ов в ресурсах; для совместимости нужно сохранять числовой индекс как есть.
-
-## 6.3. Slot tail `unk30..unk40`
-
-Хвост слота (`+0x30..+0x43`, `5×uint32`) стабильно присутствует в формате, но движок не делает явной семантической декомпозиции этих пяти слов в path'ах загрузки/рендера/коллизии.
-
-## 6.4. Effect command payload semantics
+## 4. Практический reader-контракт
-Container/stream формально полностью восстановлен (header, opcode, размеры, инстанцирование). Остаётся необязательная задача: дать «человеко‑читаемые» имена каждому полю внутри payload конкретных opcode.
+Для runtime-совместимого чтения модели:
-## 6.5. Поля `TexmHeader.flags4/flags5/unk6`
+1. Найти нужные ресурсы по `type_id` в NRes.
+2. Проверить `size/stride`-инварианты.
+3. Проверить диапазоны ссылок:
+ - slot -> batch/triangles;
+ - batch -> indices;
+ - indices -> vertices;
+ - anim_map -> anim_keys.
+4. Неизвестные поля и неизвестные ресурсы сохранять через copy-through.
-Бинарный layout и декодер известны, но значения этих трёх полей в контенте используются контекстно; для 1:1 достаточно хранить/восстанавливать их без модификации.
+## 5. Практический writer-контракт
-## 6.6. Что пока не хватает для полноценного обратного экспорта (`OBJ -> MSH/NRes`)
+1. Пересчитывать только явно вычислимые поля.
+2. Не нормализовать opaque-данные без уверенной спецификации.
+3. При roundtrip неизмененных данных требовать byte-identical результат.
+4. Для новых ассетов фиксировать отдельную политику «генерация vs preserve».
-Ниже перечислено то, что нужно закрыть для **lossless round-trip** и 1:1‑поведения при импорте внешней геометрии обратно в формат игры.
+## 6. Runtime-связка материалов и текстур
-### 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`): семантика не разложена, поэтому при экспорте новых ассетов нет детерминированной формулы заполнения.
+1. Модель -> wear-таблица (`*.wea`).
+2. Wear-слот -> material name.
+3. Material -> текущая фаза -> `textureName`.
+4. `Texm` ищется в `Textures.lib` (или lightmap-библиотеке для lightmap-ветки).
-### B) Анимационный path ещё не закрыт как writer
+Fallback:
-1. Нужен полный writer для `Res8/Res19`:
- - точная спецификация байтового формата на запись;
- - правила генерации mapping (`Res19`) по узлам/кадрам;
- - жёсткая фиксация округления как в x87 path (включая edge-case на границах кадра).
-2. Правила биндинга узлов/строк (`Res10`) и `slotFlags` к runtime‑сущностям пока описаны частично и требуют формализации именно для импорта новых данных.
+- материал: `DEFAULT`, затем индекс `0`;
+- текстура/lightmap: fallback-слот движка.
-### C) Материалы, текстуры, эффекты для «полного ассета»
+## 7. Что уже закрыто для 1:1
-1. Для `Texm` не завершён writer, покрывающий все используемые режимы (включая palette path, mip-chain, `Page`, и правила заполнения служебных полей).
-2. Для `FXID` известен контейнер/длины команд, но не завершена field-level семантика payload всех opcode для генерации новых эффектов, эквивалентных оригинальному пайплайну.
-3. Экспорт только `OBJ` покрывает геометрию; для игрового ассета нужен sidecar-слой (материалы/текстуры/эффекты/анимация), иначе импорт неизбежно неполный.
+1. Бинарный контракт базовых MSH таблиц.
+2. Контракт animation sampling (`Res8 + Res19`).
+3. Контракт MAT0/WEAR/Texm на уровне чтения и применения в кадре.
+4. Формат FXID-контейнера, командный поток и fixed command sizes.
+5. Валидация на retail-корпусе через `tools/msh_doc_validator.py` (0 ошибок/предупреждений).
-### D) Что это означает на практике
+## 8. Статус покрытия и что осталось до 100%
-1. `OBJ -> MSH` сейчас реалистичен как **ограниченный static-экспорт** (позиции/индексы/часть batch/slot структуры).
-2. `OBJ -> полноценный игровой ресурс` (без потерь, с поведением 1:1) пока недостижим без закрытия пунктов A/B/C.
-3. До закрытия пунктов A/B/C рекомендуется использовать режим:
- - геометрия экспортируется из `OBJ`;
- - неизвестные/служебные поля берутся copy-through из референсного оригинального ассета той же структуры.
+1. Полная field-level семантика части служебных полей:
+ - `Batch20` opaque-поля;
+ - хвостовые служебные поля slot-записей;
+ - часть флагов узлов/групп.
+2. Полный writer-путь для авторинга новых анимированных ассетов (не только roundtrip существующих).
+3. Полная формализация семантики FX payload полей по каждому opcode для генерации новых эффектов, а не только для корректного чтения/исполнения.
+4. Полный канонический writer `Texm` для всех редких форматов и edge-case комбинаций служебных флагов.
+5. Сквозной «импорт внешнего ассета -> игровой пакет» с формальной спецификацией sidecar-метаданных (материал/эффект/анимация).