aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/msh-core.md
blob: 6a33049e2db3338d51efd42fc5f743aefd455142 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# MSH core

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

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

- [MSH animation](msh-animation.md)
- [Material](material.md)
- [Texture (Texm)](texture.md)
- [Render pipeline](render.md)
- [NRes / RsLi](nres.md)

## 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` (узлы)

```c
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-выбора:

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

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

### 3.2. `Res2` (header + slot records)

```c
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`

```c
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-обход модели

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

```c
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`, поэтому массовый прогон по ассетам здесь не выполнялся.