Files
fparkan/docs/reference/msh.md
T

202 lines
12 KiB
Markdown
Raw Normal View History

2026-06-22 01:58:51 +04:00
# MSH
Файл `*.msh` является NRes-контейнером. Geometry, узлы, slots, batches,
animation и служебные streams лежат в entries с разными `type_id`.
## Entry map
```text
type 1 nodes and slot selection
type 2 header 0x8C + Slot68 records
type 3 positions float3
type 4 packed normals
type 5 packed UV0
type 6 index buffer u16
type 7 triangle descriptors
type 8 animation keys
type 9 service stream
type 10 strings and node names
type 13 Batch20 records
type 15 auxiliary stream
type 17 auxiliary data
type 18 packed UV1 (optional secondary coordinates)
2026-06-22 01:58:51 +04:00
type 19 animation frame map
type 20 rare auxiliary table
```
Reader ищет entries по type, но сохраняет исходный порядок для roundtrip.
## Node and slot selection
Type 1 обычно состоит из records по 38 bytes:
```c
struct Node38 {
uint16_t hdr0;
uint16_t parent_or_link;
uint16_t anim_map_start;
uint16_t fallback_key;
uint16_t slot_index[15];
};
```
В исходном layout это 3 состояния по 5 LOD: `slot_index[state * 5 + lod]`.
Публичные Rust-обёртки пока сохраняют исторические имена `Lod` и `Group`,
поэтому в `selected_slot` аргумент `Lod` задаёт native state (0..2), а
`Group` задаёт native LOD (0..4). `0xFFFF` означает отсутствие геометрии для
комбинации state/LOD.
2026-06-22 01:58:51 +04:00
Validated `ModelAsset` сохраняет decoded type 8 keys и type 19 map как
`ModelAnimation`. `node38_fallback_pose` возвращает pose по `fallback_key`,
а `node38_fallback_hierarchy` собирает parent-before-child hierarchy.
`node38_sampled_hierarchy` использует type 19 map в пределах объявленного
числа кадров и интерполирует соседние type 8 keys; при отсутствии usable map
или выходе за frame count используется fallback key. Для покомпонентного
sampling `node38_sampled_hierarchy_at_times` принимает отдельный optional float
time для каждого узла: `Some(time)` выбирает type 19 frame по округлению
`time - 0.5` к ближайшему целому с ties-to-even, затем интерполирует выбранный
type 8 key и его следующий key в исходный float time; `None` использует
fallback key. Значения должны быть конечными и неотрицательными. Это
portable-поведение при default-nearest rounding; полного совпадения со всеми
режимами округления x87 оно не заявляет. `parent_or_link == 0xFFFF`
означает root, иначе это parent index, который должен быть меньше индекса
child. Модель с нарушенным порядком не получает придуманную hierarchy.
Type 8 записывает quaternion в порядке WXYZ, после decode `AnimKey24` хранит
его как XYZW. Native AniMesh `+0x12560` строит local matrix через NGI32
`g_FastProc + 0x38`; scalar implementation выдаёт transpose обычной active
`Pose` basis (`M01=2(xy+zw)`, `M02=2(xz-yw)`, `M10=2(xy-zw)`). Поэтому путь
Node38 MSH сопрягает source-local quaternion ровно один раз после sampling и
до hierarchy composition (меняет знаки компонент `x`, `y`, `z`). Для fallback
poses применяется та же граница. Общий animation decoder и другие форматы
анимации не изменяются.
В legacy-camera static preview без `--static-animation-frame <u16>` используются
сохранённые начальные значения из явной ссылки каждого prototype на `.ctl`.
Узлы без control binding получают time `0`. Для каждой component instance
обрабатываются её CTLD bindings, поэтому общий deduplicated MSH не смешивает
controls разных компонентов. Строки идут в source order, и последняя включённая
строка для одного узла заменяет предыдущую. Flag `0x01` выполняет один wrap
значения blend на единицу, иначе значение ограничивается `[0, 1]`; flag `0x02`
инвертирует результат (`1 - blend`), а строки с `0x04` пропускаются. Выбранный
float time равен `(1 - blend) * frame_a + blend * frame_b`. Если он выходит за
type 19 map, MSH sampler использует fallback key; если после одной wrap/clamp и
invert получается отрицательное или нечисловое время, preview завершится
понятной ошибкой. Явный `--static-animation-frame` остаётся global override
для всех компонентов. Parent pose поворачивает child translation, затем
translation суммируется, а rotations умножаются; после полученной global pose
применяется `Rz * Ry * Rx`, scale и mission translation. Геометрия намеренно
дублируется на draw-range узла, потому что один source vertex может быть
нарисован разными node poses. Это только начальная статическая pose: она не
запускает игровой Control/AI и не обещает полного совпадения с runtime или
всеми x87 rounding modes.
## Vertex streams
Основные vertex streams имеют фиксированный source index. У type 3 `attr1`
задаёт количество вершин, а `attr3` — размер записи в байтах. Type 4
содержит четыре signed bytes нормали. Type 5 и optional type 18 содержат по
два little-endian `uint16` на вершину:
```c
struct PackedUv16x2 {
uint16_t u;
uint16_t v;
};
```
Type 5 — primary UV0, type 18 — secondary UV1 для lightmap/detail stage.
Для type 18 loader требует `attr3 == 4`, совпадение `attr1` и длины payload,
а также одинаковое число записей с type 3. Обе пары декодируются как
`packed / 1024.0`; renderer обращается к UV1 по тому же `source_index`, что и
к UV0. Отсутствующий type 18 означает, что secondary stage не имеет authored
координат.
2026-06-22 01:58:51 +04:00
## Slot and batch
Type 2 содержит header `0x8C`, затем `Slot68`:
```c
struct Slot68 {
uint16_t tri_start;
uint16_t tri_count;
uint16_t batch_start;
uint16_t batch_count;
float aabb_min[3];
float aabb_max[3];
float sphere_center[3];
float sphere_radius;
uint32_t opaque[5];
};
```
Type 13 задаёт draw ranges:
```c
#pragma pack(push, 1)
struct Batch20 {
uint16_t batch_flags;
uint16_t material_index_hi; // +0x02, native high selector/slot field
uint8_t material_index; // +0x04, WEAR material selector
uint8_t lightmap_index; // +0x05, 0xFF means no lightmap
uint16_t local_batch_index; // +0x06
2026-06-22 01:58:51 +04:00
uint16_t index_count;
uint32_t index_start;
uint16_t vertex_count; // +0x0E
2026-06-22 01:58:51 +04:00
uint32_t base_vertex;
};
#pragma pack(pop)
```
Byte `material_index` выбирает строку WEAR; high word at `+0x02` сохраняется
отдельно для native overrides. Loader проверяет, что
`index_start + index_count` остаётся внутри type 6, а каждый использованный
индекс через `base_vertex` попадает в position stream. `vertex_count` и
`local_batch_index` доступны renderer для исходного draw contract.
2026-10-18 07:33:56 +04:00
## Сторона грани в цветовом проходе
В цветном проходе `Terrain.dll`, функция `0x100296F0`, сравнивает камеру с
каждой гранью по знаку тройного произведения:
```c
d = dot(eye - v0, cross(v1 - v0, v2 - v0));
```
Проход оставляет грань только при `d > 0`. Raw `Batch20.flags` задаёт два
исключения: `0x0002` пропускает программную проверку стороны, поэтому обратная
грань не отбрасывается этим условием. С `0x4000` проверяется только первая
грань записи: если `d <= 0`, цикл её граней прекращается; если `d > 0`,
оставшиеся грани этой записи уже не проходят повторную программную проверку.
`Terrain.dll!0x100296F0` также настраивает пакет этого цветного пути: flags по
адресу `packet+0x128` равны `0x404` или `0x414`. Путь отправки в
`Terrain.dll!0x1001D660` перед вызовом renderer vtable `+0x48` очищает только
маску `0x1`; `Ngi32.dll!0x10007470` проверяет маску `0x4` этого аргумента. При
установленной маске render state 22 получает `D3DCULL_NONE`, иначе —
`D3DCULL_CW`. Оба значения CShade сохраняют маску `0x4`, поэтому в этом
цветном пути аппаратное отсечение выключено, а набор обычных граней определяет
программный тест `d > 0`. Raw-флаги `0x0002` и `0x4000` меняют этот тест, как
описано выше.
Статический Vulkan preview сохраняет каждую исходную запись `Batch20` одним
диапазоном рисования:
`append_static_preview_component_with_lightmaps` добавляет исходные индексы без
перестановки, а в диапазон переносит `index_count` и исходный `first_index` с
общим смещением компонента. Поэтому `0x4000` выбирает ту же первую грань, даже
если диапазон получил другой материал для lightmap.
Поскольку вершины preview переведены в мировые координаты, знак `d` для MSH
диапазона корректируется знаком детерминанта полной матрицы размещения. Повороты
узлов и mounts — единичные кватернионы с determinant `+1`; отрицательный object
scale меняет знак. Renderer Vulkan задаёт `front_face=CLOCKWISE` и viewport с
положительной высотой. Для используемых матриц legacy и free-flight preview
при положительном determinant native-visible `d > 0` относится к Vulkan
back-face; поэтому preview отсекает `FrontFace`. При отрицательном determinant
режим меняется на `BackFace`.
Для `0x0002` и `0x4000` аппаратное отсечение выключено, чтобы сохранить их
программную политику. Отклонённый `0x4000` диапазон получает число индексов 0;
каждый кадр это число заново вычисляется из исходного значения, поэтому после
изменения положения камеры или opacity материала диапазон может вернуться в
draw queue. Проверка первой грани
применяется только к MSH-диапазонам цветовой мировой очереди; terrain и
shadow/sun запросы её не используют.