2026-02-11 21:12:05 +00:00
|
|
|
|
# MSH core
|
|
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
`MSH core` описывает геометрию, слоты, батчи и базовые таблицы модели.
|
|
|
|
|
|
Документ покрывает контракт, необходимый для 1:1 воспроизведения рендера и коллизии.
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
Связанные страницы:
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
- [MSH animation](msh-animation.md)
|
|
|
|
|
|
- [Material](material.md)
|
|
|
|
|
|
- [Texture (Texm)](texture.md)
|
|
|
|
|
|
- [Render pipeline](render.md)
|
|
|
|
|
|
- [NRes / RsLi](nres.md)
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
## 1. Общая модель
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
MSH-модель хранится как `NRes`-контейнер.
|
|
|
|
|
|
Связь таблиц строится по `type`, а не по порядку записей.
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
Базовый путь геометрии:
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
1. `Res1` выбирает slot по `(node, lod, group)`.
|
|
|
|
|
|
2. `Res2.slot` задаёт диапазоны треугольников и батчей.
|
|
|
|
|
|
3. `Res13` задаёт диапазон индексов и `baseVertex`.
|
|
|
|
|
|
4. `Res6` даёт `uint16` индексы.
|
|
|
|
|
|
5. `Res3/Res4/Res5` дают вершины, нормали и UV.
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
## 2. Карта core-ресурсов
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
| Type | Ресурс | Обязательность | Stride / layout |
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|---:|---|---|---|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
| 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` (узлы)
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-12 11:07:25 +00:00
|
|
|
|
```c
|
|
|
|
|
|
struct Node38 {
|
2026-02-19 04:46:23 +04:00
|
|
|
|
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
|
2026-02-12 11:07:25 +00:00
|
|
|
|
};
|
|
|
|
|
|
```
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
Формула slot-выбора:
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-12 11:07:25 +00:00
|
|
|
|
```c
|
2026-02-19 04:46:23 +04:00
|
|
|
|
slot = node.slotIndex[lod * 5 + group]
|
2026-02-12 11:07:25 +00:00
|
|
|
|
```
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
`0xFFFF` означает отсутствие слота.
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
### 3.2. `Res2` (header + slot records)
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
|
|
|
|
|
```c
|
|
|
|
|
|
struct Slot68 {
|
2026-02-19 04:46:23 +04:00
|
|
|
|
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];
|
2026-02-11 21:12:05 +00:00
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
`opaque[5]` должны сохраняться 1:1.
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
### 3.3. `Res3`, `Res4`, `Res5`, `Res6`
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
- `Res3`: `float3` позиции (`stride=12`)
|
|
|
|
|
|
- `Res4`: `int8[4]` packed normal (`stride=4`)
|
|
|
|
|
|
- `Res5`: `int16[2]` UV (`stride=4`)
|
|
|
|
|
|
- `Res6`: `uint16` индексы (`stride=2`)
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
|
|
|
|
|
Декодирование:
|
|
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
- normal = `clamp(n / 127.0, -1..1)`
|
|
|
|
|
|
- uv = `packed / 1024.0`
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
### 3.4. `Res7` и `Res13`
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
|
|
|
|
|
```c
|
2026-02-12 11:07:25 +00:00
|
|
|
|
struct TriDesc16 {
|
2026-02-19 04:46:23 +04:00
|
|
|
|
uint16_t triFlags;
|
|
|
|
|
|
uint16_t link0;
|
|
|
|
|
|
uint16_t link1;
|
|
|
|
|
|
uint16_t link2;
|
|
|
|
|
|
int16_t nx;
|
|
|
|
|
|
int16_t ny;
|
|
|
|
|
|
int16_t nz;
|
|
|
|
|
|
uint16_t selPacked;
|
2026-02-11 21:12:05 +00:00
|
|
|
|
};
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
|
|
|
|
|
struct Batch20 {
|
2026-02-19 04:46:23 +04:00
|
|
|
|
uint16_t batchFlags;
|
|
|
|
|
|
uint16_t materialIndex;
|
|
|
|
|
|
uint16_t opaque4;
|
|
|
|
|
|
uint16_t opaque6;
|
|
|
|
|
|
uint16_t indexCount;
|
|
|
|
|
|
uint32_t indexStart;
|
|
|
|
|
|
uint16_t opaque14;
|
|
|
|
|
|
uint32_t baseVertex;
|
2026-02-12 11:07:25 +00:00
|
|
|
|
};
|
2026-02-11 21:12:05 +00:00
|
|
|
|
```
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
`selPacked` хранит 3 селектора по 2 бита; значение `3` трактуется как `0xFFFF`.
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
## 4. Runtime-обход модели
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
Псевдокод рендера:
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
|
|
|
|
|
```c
|
2026-02-12 11:07:25 +00:00
|
|
|
|
for each node:
|
|
|
|
|
|
slot = resolve_slot(node, lod, group)
|
2026-02-19 04:46:23 +04:00
|
|
|
|
if slot == none: continue
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
if culled(slot.bounds, node_transform): continue
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
for b in slot.batchRange:
|
|
|
|
|
|
batch = batches[b]
|
|
|
|
|
|
bind_material(batch.materialIndex)
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
draw_indexed(
|
|
|
|
|
|
baseVertex = batch.baseVertex,
|
|
|
|
|
|
indexStart = batch.indexStart,
|
|
|
|
|
|
indexCount = batch.indexCount
|
|
|
|
|
|
)
|
2026-02-11 21:12:05 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
## 5. Критические инварианты
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
Обязательно проверять:
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
- `Res2.size >= 0x8C`
|
|
|
|
|
|
- `(Res2.size - 0x8C) % 68 == 0`
|
|
|
|
|
|
- `batchStart + batchCount` не выходит за `Res13`
|
|
|
|
|
|
- `triStart + triCount` не выходит за `Res7`
|
|
|
|
|
|
- `indexStart + indexCount` не выходит за `Res6`
|
|
|
|
|
|
- `baseVertex + max(indexSlice) < vertexCount`
|
|
|
|
|
|
- `slotIndex == 0xFFFF` или `< slotCount`
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
## 6. Важные edge-cases
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
- Встречается редкий вариант `Res1.attr3 = 24`; для существующих ассетов нужен copy-through.
|
|
|
|
|
|
- Для строгого writer лучше генерировать `Res1` в основном формате `38` байт/узел.
|
|
|
|
|
|
- Неизвестные поля таблиц нельзя нормализовать или обнулять.
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
## 7. Правила для writer/editor
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
1. Сохранять неизвестные поля и неизвестные `type`-ресурсы.
|
|
|
|
|
|
2. Пересчитывать только явно вычислимые атрибуты (`attr1/attr3` и size-зависимые поля).
|
|
|
|
|
|
3. Не менять порядок/контент opaque-данных без явной цели.
|
|
|
|
|
|
4. Сериализовать little-endian, без внутреннего padding.
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
## 8. Статус валидации
|
2026-02-12 11:07:25 +00:00
|
|
|
|
|
2026-02-19 04:46:23 +04:00
|
|
|
|
- Инварианты формата реализованы в `tools/msh_doc_validator.py`.
|
|
|
|
|
|
- В текущем окружении нет загруженного полного корпуса игровых MSH в `testdata`, поэтому массовый прогон по ассетам здесь не выполнялся.
|
2026-02-11 21:12:05 +00:00
|
|
|
|
|