feat: complete map viewer scene and static CTL pose preview
Complete the interactive mission viewer with environment rendering, audio events, dynamic shadows, free-flight camera controls, and per-component CTL pose sampling.
This commit is contained in:
+79
-21
@@ -19,7 +19,7 @@ type 10 strings and node names
|
||||
type 13 Batch20 records
|
||||
type 15 auxiliary stream
|
||||
type 17 auxiliary data
|
||||
type 18 rare stream
|
||||
type 18 packed UV1 (optional secondary coordinates)
|
||||
type 19 animation frame map
|
||||
type 20 rare auxiliary table
|
||||
```
|
||||
@@ -40,23 +40,77 @@ struct Node38 {
|
||||
};
|
||||
```
|
||||
|
||||
`slot_index[lod * 5 + group]` выбирает geometry slot. `0xFFFF` означает
|
||||
отсутствие геометрии для комбинации LOD/group.
|
||||
В исходном 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 как
|
||||
Validated `ModelAsset` сохраняет decoded type 8 keys и type 19 map как
|
||||
`ModelAnimation`. `node38_fallback_pose` возвращает pose по `fallback_key`,
|
||||
то есть доказанный static input. `parent_or_link == 0xFFFF` означает root;
|
||||
иначе это parent index, обязательно меньший индекса child. Этот контракт
|
||||
подтверждён на тестах анимации с оригинальными ресурсами обеих частей и защищён fallback-ом:
|
||||
модель с нарушенным порядком не получает придуманную hierarchy.
|
||||
а `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.
|
||||
|
||||
В legacy-camera static preview стандартный узел уже получает свой fallback pose
|
||||
до внешнего TMA/Iron3D transform. Parent pose поворачивает child translation,
|
||||
затем translation суммируется, а rotations умножаются; после полученной global
|
||||
pose применяется `Rz * Ry * Rx`, scale и mission translation. Геометрия
|
||||
намеренно дублируется на draw-range узла, потому что один source vertex может
|
||||
быть нарисован разными node poses. Это static fallback hierarchy, а не полная
|
||||
animation parity: dynamic type-19 frame-map sampling остаётся отдельной задачей.
|
||||
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
|
||||
|
||||
@@ -82,16 +136,20 @@ Type 13 задаёт draw ranges:
|
||||
#pragma pack(push, 1)
|
||||
struct Batch20 {
|
||||
uint16_t batch_flags;
|
||||
uint16_t material_index;
|
||||
uint16_t opaque4;
|
||||
uint16_t opaque6;
|
||||
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 opaque14;
|
||||
uint16_t vertex_count; // +0x0E
|
||||
uint32_t base_vertex;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Index check выполняется как `base_vertex + index < vertex_count` для всего
|
||||
используемого slice.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user