# 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) с материалами/узлами (кто именно как индексирует строки).