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