Files
fparkan/docs/specs/msh.md

17 KiB
Raw Blame History

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 — pervertex stream, stride 8 (семантика не подтверждена).
  • Res16 — pervertex stream, stride 8, при этом движок создаёт два “под‑потока” по 4 байта (см. ниже).
  • Res18 — pervertex 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) с материалами/узлами (кто именно как индексирует строки).