From 2953f0c8c92bb43fed6065edd52858ebf08be57f Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Tue, 10 Feb 2026 01:47:19 +0400 Subject: feat: добавление документации по модели ресурсов MSH/AniMesh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/specs/msh.md | 314 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 docs/specs/msh.md (limited to 'docs/specs/msh.md') 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) с материалами/узлами (кто именно как индексирует строки). -- cgit v1.2.3