aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/msh.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs/msh.md')
-rw-r--r--docs/specs/msh.md314
1 files changed, 314 insertions, 0 deletions
diff --git a/docs/specs/msh.md b/docs/specs/msh.md
new file mode 100644
index 0000000..ae52919
--- /dev/null
+++ b/docs/specs/msh.md
@@ -0,0 +1,314 @@
+# 3D модели (MSH / AniMesh)
+
+Документ описывает **модельные ресурсы** старого движка по результатам анализа `AniMesh.dll` и сопутствующих библиотек.
+
+---
+
+## 0) Термины
+
+- **Модель** — набор геометрии + иерархия узлов (node/bone) + дополнительные таблицы (батчи/слоты/треки).
+- **Node** — узел иерархии (часть/кость). Визуально: “кусок” модели, которому можно применять transform (rigid).
+- **LOD** — уровень детализации. В коде обнаружены **3 уровня LOD: 0..2** (и “текущий” LOD через `-1`).
+- **Slot** — связка “(node, LOD, group) → диапазоны геометрии + bounds”.
+- **Batch** — рендер‑пакет: “материал + диапазон индексов + baseVertex”.
+
+---
+
+## 1) Архитектура модели в движке (как это реально рисуется)
+
+### 1.1 Рендер‑модель: rigid‑скининг (по узлам), без весов вершин
+
+По коду выборка геометрии делается так:
+
+1. Выбирается **LOD** (в объекте хранится `current_lod`, см. `sub_100124D0`).
+2. Для каждого узла **node** выбирается **slot** по `(nodeIndex, group, lod)`:
+ - Если lod == `-1`, то берётся `current_lod`.
+ - Если в node‑таблице хранится `0xFFFF`, slot отсутствует.
+3. Slot задаёт **диапазон batch’ей** (`batch_start`, `batch_count`).
+4. Рендерер получает batch‑диапазон и для каждого batch делает `DrawIndexedPrimitive` (абстрактный вызов через графический интерфейс движка), используя:
+ - `baseVertex`
+ - `indexStart`
+ - `indexCount`
+ - материал (индекс материала/шейдера в batch’е)
+
+**Важно:** в “модельном” формате не видно классических skin weights (4 bone indices + 4 weights). Это очень похоже на “rigid parts”: каждый batch/часть привязан к одному узлу (или группе узлов) и рендерится с матрицей этого узла.
+
+---
+
+## 2) Набор ресурсов модели (что лежит внутри “файла модели”)
+
+Ниже перечислены ресурсы, которые гарантированно встречаются в загрузчике `AniMesh`:
+
+- **Res1** — node table (таблица узлов и LOD‑слотов).
+- **Res2** — header + slot table (слоты и bounds).
+- **Res3** — vertex positions (float3).
+- **Res4** — packed normals (4 байта на вершину; s8‑компоненты).
+- **Res5** — packed UV0 (4 байта на вершину; s16 U,V).
+- **Res6** — index buffer (u16 индексы).
+- **Res7** — triangle descriptors (по 16 байт на треугольник).
+- **Res8** — keyframes / anim track data (используется в интерполяции).
+- **Res10** — string table (имена: материалов/узлов/частей — точный маппинг зависит от вызывающей стороны).
+- **Res13** — batch table (по 20 байт на batch).
+- **Res19** — дополнительная таблица для анимации/маппинга (используется вместе с Res8; точная семантика пока не восстановлена).
+
+Опциональные (встречаются условно, если ресурс присутствует):
+
+- **Res15** — per‑vertex stream, stride 8 (семантика не подтверждена).
+- **Res16** — per‑vertex stream, stride 8, при этом движок создаёт **два “под‑потока” по 4 байта** (см. ниже).
+- **Res18** — per‑vertex stream, stride 4 (семантика не подтверждена).
+- **Res20** — дополнительный массив + отдельное “count/meta” поле из заголовка ресурса.
+
+---
+
+## 3) Декодирование базовой геометрии
+
+### 3.1 Positions (Res3)
+
+- Структура: массив `float3`.
+- Stride: `12`.
+- Использование: `pos = *(float3*)(res3 + 12*vertexIndex)`.
+
+### 3.2 UV0 (Res5) — packed s16
+
+- Stride: `4`.
+- Формат: `int16 u, int16 v`
+- Нормализация (из кода): `uv = (u, v) * (1/1024)`
+
+То есть:
+
+- `u_float = (int16)u / 1024.0`
+- `v_float = (int16)v / 1024.0`
+
+### 3.3 Normals (Res4) — packed s8
+
+- Stride: `4`.
+- Формат (минимально подтверждено): `int8 nx, int8 ny, int8 nz, int8 nw(?)`
+- Нормализация (из кода): множитель `1/128 = 0.0078125`
+
+То есть:
+
+- `n = (nx, ny, nz) / 128.0`
+
+4‑й байт пока не подтверждён (встречается как паддинг/знак/индекс — нужно дальше копать).
+
+---
+
+## 4) Таблицы, задающие разбиение геометрии
+
+### 4.1 Batch table (Res13), запись 20 байт
+
+Batch используется в рендере и в обходе треугольников. Из обхода достоверно:
+
+- `indexCount` читается как `u16` по смещению `+8`.
+- `indexStart` используется как **u32 по смещению `+10`** (движок читает dword и умножает на 2 для смещения в u16‑индексах).
+- `baseVertex` читается как `u32` по смещению `+16`.
+
+Рекомендуемая реконструкция:
+
+- `+0 u16 batchFlags` — используется для фильтрации (битовая маска).
+- `+2 u16 materialIndex` — очень похоже на индекс материала/шейдера.
+- `+4 u16 unk4`
+- `+6 u16 unk6` — **возможный** `nodeIndex` (часто именно здесь держат привязку батча к кости).
+- `+8 u16 indexCount` — число индексов (кратно 3 для треугольников).
+- `+10 u32 indexStart` — стартовый индекс в общем index buffer (в элементах u16).
+- `+14 u16 unk14` — возможно “primitive/strip mode” или ещё один флаг.
+- `+16 u32 baseVertex` — смещение вершинного индекса (в вершинах).
+
+### 4.2 Triangle descriptors (Res7), запись 16 байт
+
+Треугольные дескрипторы используются при итерации треугольников (коллизии/выбор/тесты):
+
+- `+0 u16 triFlags` — используется для фильтрации (битовая маска)
+- Остальные поля пока не подтверждены (вероятно: доп. флаги, группа, precomputed normal, ID поверхности и т.п.)
+
+**Важно:** индексы вершин треугольника берутся **из index buffer (Res6)** через `indexStart/indexCount` batch’а. TriDesc не хранит сами индексы.
+
+---
+
+## 5) Slot table (Res2 + смещение 140), запись 68 байт
+
+Slot — ключевая структура, по которой движок:
+
+- получает bounds (AABB + sphere),
+- получает диапазон batch’ей для рендера/обхода,
+- получает стартовый индекс треугольников (triStart) в TriDesc.
+
+В коде Slot читается как `u16`‑поля + как `float`‑поля (AABB/sphere). Подтверждённая раскладка:
+
+### 5.1 Заголовок slot (первые 8 байт)
+
+- `+0 u16 triStart` — индекс первого треугольника в `Res7` (TriDesc), используемый в обходе.
+- `+2 u16 slotFlagsOrUnk` — пока не восстановлено (не путать с batchFlags/triFlags).
+- `+4 u16 batchStart` — индекс первого batch’а в `Res13`.
+- `+6 u16 batchCount` — количество batch’ей.
+
+### 5.2 AABB (локальные границы, 24 байта)
+
+- `+8 float aabbMin.x`
+- `+12 float aabbMin.y`
+- `+16 float aabbMin.z`
+- `+20 float aabbMax.x`
+- `+24 float aabbMax.y`
+- `+28 float aabbMax.z`
+
+### 5.3 Bounding sphere (локальные границы, 16 байт)
+
+- `+32 float sphereCenter.x`
+- `+36 float sphereCenter.y`
+- `+40 float sphereCenter.z`
+- `+44 float sphereRadius`
+
+### 5.4 Хвост (20 байт)
+
+- `+48..+67` — не используется в найденных вызовах bounds/рендера; назначение неизвестно. Возможные кандидаты: LOD‑дистанции, доп. bounds, служебные поля экспортёра.
+
+---
+
+## 6) Node table (Res1), запись 19 \* u16 на узел (38 байт)
+
+Node table — это не “матрицы узлов”, а компактная карта слотов по LOD и группам.
+
+Движок вычисляет адрес слова так:
+
+`wordIndex = nodeIndex * 19 + lod * 5 + group + 4`
+
+где:
+
+- `lod` в диапазоне `0..2` (**три уровня LOD**)
+- `group` в диапазоне `0..4` (**пять групп слотов**)
+- если вместо `lod` передать `-1`, движок подставит `current_lod` из инстанса.
+
+Из этого следует структура узла:
+
+### 6.1 Заголовок узла (первые 4 u16)
+
+- `u16 hdr0`
+- `u16 hdr1`
+- `u16 hdr2`
+- `u16 hdr3`
+
+Семантика заголовка узла **пока не восстановлена** (кандидаты: parent/firstChild/nextSibling/flags).
+
+### 6.2 SlotIndex‑матрица: 3 LOD \* 5 groups = 15 u16
+
+Дальше идут 15 слов:
+
+- для `lod=0`: `slotIndex[group0..4]`
+- для `lod=1`: `slotIndex[group0..4]`
+- для `lod=2`: `slotIndex[group0..4]`
+
+`slotIndex` — это индекс в slot table (`Res2+140`), либо `0xFFFF` если слота нет.
+
+**Группы (0..4)**: в коде чаще всего используется `group=0`. Остальные группы встречаются как параметр обхода, но назначение (например, “коллизия”, “тени”, “декали”, “альфа‑геометрия” и т.п.) пока не доказано. В документации ниже они называются просто `group`.
+
+---
+
+## 7) Рендер‑проход (рекомендуемая реконструкция)
+
+Минимальный корректный порт рендера может повторять логику:
+
+1. Определить `current_lod` (0..2) для модели (по дистанции/настройкам).
+2. Для каждого node:
+ - взять slotIndex = node.slotIndex[current_lod][group=0]
+ - если `0xFFFF` — пропустить
+ - slot = slotTable[slotIndex]
+3. Для slot’а:
+ - для i in `0 .. slot.batchCount-1`:
+ - batch = batchTable[slot.batchStart + i]
+ - применить материал `materialIndex`
+ - применить transform узла (как минимум: rootTransform \* nodeTransform)
+ - нарисовать индексированную геометрию:
+ - baseVertex = batch.baseVertex
+ - indexStart = batch.indexStart
+ - indexCount = batch.indexCount
+4. Для culling:
+ - использовать slot AABB/sphere, трансформируя их матрицей узла/инстанса.
+ - при неравномерном scale радиус сферы масштабируется по `max(scaleX, scaleY, scaleZ)` (так делает оригинальный код).
+
+---
+
+## 8) Обход треугольников (коллизия/пикинг/дебаг)
+
+В движке есть универсальный обход:
+
+- Идём по slot’ам (node, lod, group).
+- Для каждого slot:
+ - for batch in slot.batchRange:
+ - получаем индексы из Res6 (indexStart/indexCount)
+ - triCount = (indexCount + 2) / 3
+ - параллельно двигаем указатель TriDesc начиная с `triStart`
+ - для каждого треугольника:
+ - читаем `triFlags` (TriDesc[0])
+ - фильтруем по маскам
+ - вызываем callback, которому доступны:
+ - triDesc (16 байт)
+ - три индекса (из index buffer)
+ - три позиции (из Res3 через baseVertex + индекс)
+
+---
+
+## 9) Опциональные vertex streams (Res15/16/18/20) — текущий статус
+
+Эти ресурсы загружаются, но в найденных местах пока **нет однозначного декодера**. Что точно видно по загрузчику:
+
+- **Res15**: stride 8, массив на вершину.
+ - кандидаты: `float2 uv1` (lightmap), либо 4×`int16` (2 UV‑пары), либо что‑то иное.
+
+- **Res16**: stride 8, но движок создаёт два “под‑потока”:
+ - streamA = res16 + 0, stride 8
+ - streamB = res16 + 4, stride 8 Это сильно похоже на “два packed‑вектора по 4 байта”, например `tangent` и `bitangent` (s8×4).
+
+- **Res18**: stride 4, массив на вершину.
+ - кандидаты: `D3DCOLOR` (RGBA), либо packed‑параметры освещения/окклюзии.
+
+- **Res20**: присутствует не всегда; отдельно читается `count/meta` поле из заголовка ресурса.
+ - кандидаты: дополнительная таблица соответствий (vertex remap), либо ускорение для эффектов/деформаций.
+
+---
+
+## 10) Как “создавать” модели (экспортёр / конвертер) — практическая рекомендация
+
+Чтобы собрать совместимый формат (минимум, достаточный для рендера и коллизии), нужно:
+
+1. Сформировать единый массив вершин:
+ - positions (Res3)
+ - packed normals (Res4) — если хотите сохранить оригинальную упаковку
+ - packed uv0 (Res5)
+
+2. Сформировать index buffer (Res6) u16.
+
+3. Сформировать batch table (Res13):
+ - сгруппировать треугольники по (материал, узел/часть, режим)
+ - записать `baseVertex`, `indexStart`, `indexCount`
+ - заполнить неизвестные поля нулями (пока нет доказанной семантики).
+
+4. Сформировать triangle descriptor table (Res7):
+ - на каждый треугольник 16 байт
+ - минимум: `triFlags=0`
+ - остальное — 0.
+
+5. Сформировать slot table (Res2+140):
+ - для каждого (node, lod, group) задать:
+ - triStart (индекс начала triDesc для обхода)
+ - batchStart/batchCount
+ - AABB и bounding sphere в локальных координатах узла/части
+ - неиспользуемые поля хвоста = 0.
+
+6. Сформировать node table (Res1):
+ - для каждого node:
+ - 4 заголовочных u16 (пока можно 0)
+ - 15 slotIndex’ов (LOD0..2 × group0..4), `0xFFFF` где нет слота.
+
+7. Анимацию/Res8/Res19/Res11:
+ - если не нужна — можно отсутствующими, но надо проверить, что загрузчик/движок допускает “статическую” модель без этих ресурсов (в оригинале много логики завязано на них).
+
+---
+
+## 11) Что ещё нужно восстановить, чтобы документация стала “закрывающей” на 100%
+
+1. Точная семантика `batch.unk6` (вероятный nodeIndex) и `batch.unk4/unk14`.
+2. Полная раскладка TriDesc16 (кроме triFlags).
+3. Назначение `slotFlagsOrUnk`.
+4. Семантика групп `group=1..4` в node‑таблице.
+5. Назначение и декодирование Res15/Res16/Res18/Res20.
+6. Связь строковой таблицы (Res10) с материалами/узлами (кто именно как индексирует строки).