203 lines
12 KiB
Markdown
203 lines
12 KiB
Markdown
# 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)
|
||
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.
|
||
|
||
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
|
||
координат.
|
||
|
||
## 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
|
||
uint16_t index_count;
|
||
uint32_t index_start;
|
||
uint16_t vertex_count; // +0x0E
|
||
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.
|
||
|
||
## Сторона грани в цветовом проходе
|
||
|
||
В цветном проходе `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 запросы её не используют.
|