17 KiB
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‑скининг (по узлам), без весов вершин
По коду выборка геометрии делается так:
- Выбирается LOD (в объекте хранится
current_lod, см.sub_100124D0). - Для каждого узла node выбирается slot по
(nodeIndex, group, lod):- Если lod ==
-1, то берётсяcurrent_lod. - Если в node‑таблице хранится
0xFFFF, slot отсутствует.
- Если lod ==
- Slot задаёт диапазон batch’ей (
batch_start,batch_count). - Рендерер получает batch‑диапазон и для каждого batch делает
DrawIndexedPrimitive(абстрактный вызов через графический интерфейс движка), используя:baseVertexindexStartindexCount- материал (индекс материала/шейдера в 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.0v_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 hdr0u16 hdr1u16 hdr2u16 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) Рендер‑проход (рекомендуемая реконструкция)
Минимальный корректный порт рендера может повторять логику:
- Определить
current_lod(0..2) для модели (по дистанции/настройкам). - Для каждого node:
- взять slotIndex = node.slotIndex[current_lod][group=0]
- если
0xFFFF— пропустить - slot = slotTable[slotIndex]
- Для 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
- для i in
- Для 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 + индекс)
- читаем
- for batch in slot.batchRange:
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) Как “создавать” модели (экспортёр / конвертер) — практическая рекомендация
Чтобы собрать совместимый формат (минимум, достаточный для рендера и коллизии), нужно:
-
Сформировать единый массив вершин:
- positions (Res3)
- packed normals (Res4) — если хотите сохранить оригинальную упаковку
- packed uv0 (Res5)
-
Сформировать index buffer (Res6) u16.
-
Сформировать batch table (Res13):
- сгруппировать треугольники по (материал, узел/часть, режим)
- записать
baseVertex,indexStart,indexCount - заполнить неизвестные поля нулями (пока нет доказанной семантики).
-
Сформировать triangle descriptor table (Res7):
- на каждый треугольник 16 байт
- минимум:
triFlags=0 - остальное — 0.
-
Сформировать slot table (Res2+140):
- для каждого (node, lod, group) задать:
- triStart (индекс начала triDesc для обхода)
- batchStart/batchCount
- AABB и bounding sphere в локальных координатах узла/части
- неиспользуемые поля хвоста = 0.
- для каждого (node, lod, group) задать:
-
Сформировать node table (Res1):
- для каждого node:
- 4 заголовочных u16 (пока можно 0)
- 15 slotIndex’ов (LOD0..2 × group0..4),
0xFFFFгде нет слота.
- для каждого node:
-
Анимацию/Res8/Res19/Res11:
- если не нужна — можно отсутствующими, но надо проверить, что загрузчик/движок допускает “статическую” модель без этих ресурсов (в оригинале много логики завязано на них).
11) Что ещё нужно восстановить, чтобы документация стала “закрывающей” на 100%
- Точная семантика
batch.unk6(вероятный nodeIndex) иbatch.unk4/unk14. - Полная раскладка TriDesc16 (кроме triFlags).
- Назначение
slotFlagsOrUnk. - Семантика групп
group=1..4в node‑таблице. - Назначение и декодирование Res15/Res16/Res18/Res20.
- Связь строковой таблицы (Res10) с материалами/узлами (кто именно как индексирует строки).