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