Files
fparkan/docs/specs/msh-core.md
Valentin Popov 0e19660eb5 Refactor documentation structure and add new specifications
- Updated MSH documentation to reflect changes in material, wear, and texture specifications.
- Introduced new `render.md` file detailing the render pipeline process.
- Removed outdated sections from `runtime-pipeline.md` and redirected to `render.md`.
- Added detailed specifications for `Texm` texture format and `WEAR` wear table.
- Updated navigation in `mkdocs.yml` to align with new documentation structure.
2026-02-19 04:46:23 +04:00

5.8 KiB
Raw Blame History

MSH core

MSH core описывает геометрию, слоты, батчи и базовые таблицы модели.
Документ покрывает контракт, необходимый для 1:1 воспроизведения рендера и коллизии.

Связанные страницы:

1. Общая модель

MSH-модель хранится как NRes-контейнер.
Связь таблиц строится по type, а не по порядку записей.

Базовый путь геометрии:

  1. Res1 выбирает slot по (node, lod, group).
  2. Res2.slot задаёт диапазоны треугольников и батчей.
  3. Res13 задаёт диапазон индексов и baseVertex.
  4. Res6 даёт uint16 индексы.
  5. Res3/Res4/Res5 дают вершины, нормали и UV.

2. Карта core-ресурсов

Type Ресурс Обязательность Stride / layout
1 Node table обязательный обычно 38 байт
2 Header + slots обязательный 0x8C + n*68
3 Positions обязательный 12
4 Packed normals обычно обязательный 4
5 Packed UV0 обычно обязательный 4
6 Index buffer обязательный 2
7 Tri descriptors для коллизии/пикинга 16
8 Anim key pool для анимированных 24
10 Node strings опциональный variable
13 Batch table обязательный 20
15 Доп. stream опциональный 8
16 Доп. stream опциональный 8
18 Доп. stream опциональный 4
19 Anim map для анимированных 2
20 Доп. таблица опциональный variable

3. Основные структуры

3.1. Res1 (узлы)

struct Node38 {
    uint16_t hdr0;
    uint16_t parent_or_link;
    uint16_t anim_map_start;
    uint16_t fallback_key;
    uint16_t slotIndex[15]; // lod0:g0..g4, lod1:g0..g4, lod2:g0..g4
};

Формула slot-выбора:

slot = node.slotIndex[lod * 5 + group]

0xFFFF означает отсутствие слота.

3.2. Res2 (header + slot records)

struct Slot68 {
    uint16_t triStart;
    uint16_t triCount;
    uint16_t batchStart;
    uint16_t batchCount;
    float    aabbMin[3];
    float    aabbMax[3];
    float    sphereCenter[3];
    float    sphereRadius;
    uint32_t opaque[5];
};

opaque[5] должны сохраняться 1:1.

3.3. Res3, Res4, Res5, Res6

  • Res3: float3 позиции (stride=12)
  • Res4: int8[4] packed normal (stride=4)
  • Res5: int16[2] UV (stride=4)
  • Res6: uint16 индексы (stride=2)

Декодирование:

  • normal = clamp(n / 127.0, -1..1)
  • uv = packed / 1024.0

3.4. Res7 и Res13

struct TriDesc16 {
    uint16_t triFlags;
    uint16_t link0;
    uint16_t link1;
    uint16_t link2;
    int16_t  nx;
    int16_t  ny;
    int16_t  nz;
    uint16_t selPacked;
};

struct Batch20 {
    uint16_t batchFlags;
    uint16_t materialIndex;
    uint16_t opaque4;
    uint16_t opaque6;
    uint16_t indexCount;
    uint32_t indexStart;
    uint16_t opaque14;
    uint32_t baseVertex;
};

selPacked хранит 3 селектора по 2 бита; значение 3 трактуется как 0xFFFF.

4. Runtime-обход модели

Псевдокод рендера:

for each node:
    slot = resolve_slot(node, lod, group)
    if slot == none: continue

    if culled(slot.bounds, node_transform): continue

    for b in slot.batchRange:
        batch = batches[b]
        bind_material(batch.materialIndex)

        draw_indexed(
            baseVertex = batch.baseVertex,
            indexStart = batch.indexStart,
            indexCount = batch.indexCount
        )

5. Критические инварианты

Обязательно проверять:

  • Res2.size >= 0x8C
  • (Res2.size - 0x8C) % 68 == 0
  • batchStart + batchCount не выходит за Res13
  • triStart + triCount не выходит за Res7
  • indexStart + indexCount не выходит за Res6
  • baseVertex + max(indexSlice) < vertexCount
  • slotIndex == 0xFFFF или < slotCount

6. Важные edge-cases

  • Встречается редкий вариант Res1.attr3 = 24; для существующих ассетов нужен copy-through.
  • Для строгого writer лучше генерировать Res1 в основном формате 38 байт/узел.
  • Неизвестные поля таблиц нельзя нормализовать или обнулять.

7. Правила для writer/editor

  1. Сохранять неизвестные поля и неизвестные type-ресурсы.
  2. Пересчитывать только явно вычислимые атрибуты (attr1/attr3 и size-зависимые поля).
  3. Не менять порядок/контент opaque-данных без явной цели.
  4. Сериализовать little-endian, без внутреннего padding.

8. Статус валидации

  • Инварианты формата реализованы в tools/msh_doc_validator.py.
  • В текущем окружении нет загруженного полного корпуса игровых MSH в testdata, поэтому массовый прогон по ассетам здесь не выполнялся.