refactor into projects

This commit is contained in:
bird_egop
2026-05-17 15:40:16 +03:00
parent a5a14ec4ed
commit 28d10f3ffa
25 changed files with 77 additions and 27 deletions
+337
View File
@@ -0,0 +1,337 @@
# CAniMesh / MSH Loading and Joint Bounds — Key Summary
## New findings discovered in this chat
* `tag` is best understood as a runtime **loaded submodel tag** for pieces created from one `.msh` load call.
* `tag == 0` is the primary/root `.msh` model.
* `tag != 0` means an attached/additional `.msh` whose source node `0` is skipped and used as a virtual attach root.
* One `CAniMesh` can aggregate multiple `.msh` resources into a single flat `pieces_vector`.
* Attached `.msh` pieces are not kept as separate models at runtime; their nodes are remapped into absolute `CAniMesh::pieces_vector` indices.
* `attach_parent_absolute_piece_index` is an absolute index in `CAniMesh::pieces_vector`, not a local index inside the attached `.msh`.
* The first runtime piece created by each `.msh` load should be marked as a submodel/subtree root.
* `MSH_PIECE_FLAG_TAG_ROOT` should be renamed to `MSH_PIECE_FLAG_SUBMODEL_ROOT` or `MSH_PIECE_FLAG_LOADED_SUBTREE_ROOT`.
* `ComputeJointBoundingBox` is the authoritative recursive joint/subtree bounds function.
* `ComputeJointBoundingSphere` uses specialized fast paths for single-piece and root whole-mesh bounds, but for non-root subtree bounds it delegates to `ComputeJointBoundingBox` and wraps the resulting AABB in a sphere.
* Geometry-less pieces are valid helper/socket joints: their bounds become a point or zero-radius sphere at the joint transform origin.
* The renamed filter `g_mesh_filter_only_subtree_and_exclude_default_bounds` clarifies that cached “default filter” bounds are really cached bounds for a specific reduced subtree filter.
---
## Key function: `CAniMesh::AppendMshResourcePieces (AniMesh.dll/sub_1000ac70)`
```
typedef struct GmsgAppendResourcePayload_CAniMesh {
char archive_name[32];
char msh_archive_entry_name[32];
uint msh_tag;
uint attach_parent_absolute_piece_index;
uint material_id_hi;
} GmsgAppendResourcePayload_CAniMesh;
```
Core behavior:
```text
One call loads one .msh resource.
All pieces created by that call receive the same tag.
The created pieces are appended to CAniMesh::pieces_vector.
```
For `tag == 0`:
```text
source MSH 0x01 node 0 -> piece[0]
source MSH 0x01 node 1 -> piece[1]
source MSH 0x01 node 2 -> piece[2]
...
```
For `tag != 0`:
```text
source MSH 0x01 node 0 is skipped
source MSH 0x01 node 1 -> first newly created piece
source MSH 0x01 node 2 -> next newly created piece
...
```
Attached parent remap rule:
```text
source parent == 0
-> attach_parent_absolute_piece_index
source parent > 0
-> first_new_piece_index + (source_parent - 1)
source parent == 0xFFFF
-> -1 / no parent
```
This means internal parent hierarchy inside the attached `.msh` is preserved, but all indices are converted to absolute `pieces_vector` indices.
---
Runtime piece flag:
```c
#define MSH_PIECE_FLAG_SUBMODEL_ROOT 0x01000000
```
Meaning:
```text
Set on the first runtime piece created by one .msh load call.
For tag == 0, this is the main model root.
For tag != 0, this is the attached submodel/subtree root.
```
### This function creates a runtime representation of a .msh 0x01 piece
```
typedef struct MSH_piece {
uint msh_tag_0x00;
int local_parent_index_base;
uint msh0x01_node_index;
undefined4 field_12;
undefined4 material_id;
EMshPieceFlags flags;
int parent_piece_index;
EMeshPieceState state;
Matrix4x4 world_pose_matrix;
Matrix4x4 mesh_space_pose_matrix;
Matrix4x4 local_pose_matrix;
Quaternion orientation_blend_start_quat;
Quaternion orientation_blend_end_quat;
float anim1_time_start;
float anim1_time_target;
float anim2_time_start;
float anim2_time_target;
bool is_pose_cache_valid;
bool exclude_from_pose_update_order;
bool uses_local_anim_blend;
undefined1 has_orientation_blend;
float local_anim_transition_progress;
float local_anim_blend_factor;
float cached_anim1_sample_time;
float cached_anim2_sample_time;
float render_phase_0x124??;
undefined4 material_phase_0x128??;
MSH_Reader * msh_reader;
} MSH_piece;
```
---
## Key type: `MSH_0x01_node`
`MSH` component `0x01` is a source node / piece table.
Important fields:
```c
typedef struct MSH_0x01_node {
uint16_t flags;
uint16_t parent_index_or_link;
uint16_t anim_map_start_0x13;
uint16_t fallback_key_0x08;
uint16_t msh02_slot_indices_by_state_and_lod[3][5];
} MSH_0x01_node;
```
Meaning:
```text
MSH 0x01 node
-> becomes an MSH_piece at runtime
-> has parent_index_or_link
-> has MSH01 flags
-> maps LOD/state to MSH 0x02 geometry slots
```
---
## Key type: `MSH_02_geometry_slot`
`MSH` component `0x02` stores geometry slot metadata and local bounds.
Preferred structure:
```c
typedef struct MSH_02_geometry_slot {
uint16_t tri_start_0x07;
uint16_t tri_count_0x07;
uint16_t batch_start_0x0d;
uint16_t batch_count_0x0d;
Vector3 local_minimum;
Vector3 local_maximum;
Sphere bounding_sphere;
float base_xy_area;
float base_volume;
uint32_t opaque_0x38;
uint32_t opaque_0x3C;
uint32_t opaque_0x40;
} MSH_02_geometry_slot;
```
## Key function: `ResolveMsh0x02SlotBy_LOD_and_state`
The bounds functions call it as default geometry lookup:
```c
MSH_02_geometry_slot *
ResolveMsh0x02SlotBy_LOD_and_state(
MSH_piece *this,
EMeshPieceLodLevel lod_level,
EMeshPieceState state
);
typedef enum EMeshPieceLodLevel {
LOD_LEVEL_MAX_0 = 0,
LOD_LEVEL_MINUS_1 = 1,
LOD_LEVEL_MINUS_2 = 2,
LOD_LEVEL_MINUS_3 = 3,
LOD_LEVEL_MINUS_4 = 4,
} EMeshPieceLodLevel;
typedef enum EMeshPieceState {
MODEL_STATE_DEFAULT = -1,
MODEL_STATE_REGULAR = 0,
MODEL_STATE_COLLAPSED = 1,
_MODEL_STATE_UNKNOWN_2 = 2,
} EMeshPieceState;
```
---
## Key function: `IJointMesh_of_AniMesh::ComputeJointBoundingBox`
Preferred name:
```c
AniMesh_IJointMesh::ComputeJointBoundingBox
```
Core behavior:
```text
1. Read the joint/piece placement matrix in requested space.
2. Resolve default geometry slot for the queried piece.
3. If the piece has geometry:
- build local box from slot local_minimum/local_maximum;
- optionally scale by mesh_scale;
- transform all corners by the joint matrix.
4. If the piece has no geometry:
- create a degenerate box at joint transform origin.
5. If scope is single-piece, return.
6. If queried piece is root piece 0, return cached whole-mesh bounds.
7. Otherwise recursively include matching children, controlled by JointBoundsFilter and MSH01 flags.
```
Important meaning:
```text
This is the main recursive piece-tree bounds function.
```
Geometry-less piece meaning:
```text
No MSH 0x02 slot
-> helper/socket joint
-> point-sized bounds at joint transform origin
```
---
## Key function: `IJointMesh_of_AniMesh::ComputeJointBoundingSphere`
Preferred name:
```c
AniMesh_IJointMesh::ComputeJointBoundingSphere
```
Core behavior:
```text
If scope != SINGLE_PIECE and piece != 0:
ComputeJointBoundingBox(...)
Convert resulting AABB to center/radius sphere.
If scope != SINGLE_PIECE and piece == 0:
Use cached whole-mesh or cached filtered mesh sphere.
If scope == SINGLE_PIECE:
Use MSH_02_geometry_slot::bounding_sphere.
If no geometry slot, return zero-radius sphere at joint origin.
```
Important meaning:
```text
Subtree sphere is not a tight recursive sphere.
It is an AABB-derived sphere from ComputeJointBoundingBox.
```
---
## Important conceptual model
```text
CAniMesh
owns one flat pieces_vector
Each loaded .msh
contributes one tagged group of pieces
MSH 0x01
source node hierarchy inside one .msh
MSH_piece
runtime node/piece inside CAniMesh::pieces_vector
parent_piece_index
absolute runtime parent index in CAniMesh::pieces_vector
msh_tag
tells which loaded .msh/submodel this runtime piece came from
```
Runtime result:
```text
Multiple .msh files become one combined piece tree.
The tag preserves source submodel grouping.
The parent indices define the actual runtime hierarchy.
```
---
## Example runtime structure
```text
CAniMesh pieces_vector
piece[0] body_root tag = 0, SUBMODEL_ROOT
├─ piece[1] left_track tag = 0
├─ piece[2] right_track tag = 0
└─ piece[3] turret_socket tag = 0
└─ piece[4] turret_base tag = 1, SUBMODEL_ROOT
└─ piece[5] turret_rotor tag = 1
├─ piece[6] cannon_socket tag = 1
│ └─ piece[8] cannon_body tag = 2, SUBMODEL_ROOT
└─ piece[7] rocket_socket tag = 1
└─ piece[9] launcher_body tag = 3, SUBMODEL_ROOT
```
Key rule:
```text
tag groups pieces by loaded .msh.
parent_piece_index builds the actual hierarchy.
```
+347
View File
@@ -0,0 +1,347 @@
# Документация формата MSH
Формат `.msh` используется игрой Parkan: Железная стратегия (1998) для хранения 3D-мешей.
MSH файлы — это NRes архивы, содержащие несколько типизированных компонентов.
## Обзор
Существует **два варианта** формата MSH:
| Вариант | Применение | Ключевые компоненты | Хранение треугольников |
|---------|------------|---------------------|------------------------|
| **Модель** | Роботы, здания, объекты | 06, 0D, 07 | Индексированные треугольники |
| **Ландшафт** | Террейн | 0B, 15 | Прямые треугольники |
### Автоопределение типа
```
Модель: Есть компонент 06 (индексы) И 0D (батчи)
Ландшафт: Есть компонент 0B (материалы) И НЕТ компонента 06
```
---
## Сводка компонентов
| Тип | Название | Размер элемента | Описание |
|:---:|----------|:---------------:|----------|
| 01 | Node table | 38 (0x26), редко 24 | Узлы модели / тайлы; старое имя: Pieces |
| 02 | Header + slots | 0x8C + n*68 | Общий заголовок и slot records; старое имя: Submeshes |
| 03 | Positions | 12 (0x0C) | Позиции вершин (Vector3); старое имя: Vertices |
| 04 | PackedNormals | 4 | `int8[4]`, normal = clamp(component / 127.0, -1..1) |
| 05 | PackedUV0 | 4 | `int16[2]`, uv = component / 1024.0 |
| 06 | Index buffer | 2 | Индексы вершин треугольников |
| 07 | Tri descriptors | 16 | Описатели треугольников для коллизии/пикинга |
| 08 | AnimKeyPool | 24 | Кейфреймы анимации меша |
| 0A | Node strings | переменный | Строки узлов; старое имя: ExternalRefs |
| 0B | неизвестно | 4 | неизвестно (только Ландшафт) |
| 0D | Batch table | 20 (0x14) | Батчи рендера; FParkan Res13 decimal |
| 0E | неизвестно | 4 | неизвестно (только Ландшафт) |
| 12 | MicrotextureMap | 4 | неизвестно |
| 13 | AnimMap | 2 | Карта кадров анимации, на нее указывает `AnimMapStart` из 0x01 |
| 15 | TerrainTriangle table | 28 (0x1C) | Terrain-гипотеза |
---
## Поток данных
### Модель (роботы, здания)
```
Компонент 01 (Pieces - части)
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
├─► TriStart:TriCount ──► Компонент 07 (данные на треугольник)
└─► BatchStart:BatchCount ──► Компонент 0D (батчи)
├─► IndexStart:IndexCount ──► Компонент 06 (индексы)
│ │
│ └─► Компонент 03 (вершины)
└─► BaseVertex (базовое смещение вершины)
```
### Ландшафт (террейн)
```
Компонент 01 (Тайлы, обычно 16×16 = 256)
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
└─► TriStart:TriCount ──► Компонент 15 (треугольники)
└─► Vertex1/2/3Index ──► Компонент 03 (вершины)
└─► TriStart:TriCount ──► Компонент 0B (материалы, параллельно 15)
```
**Важно:** В ландшафтных мешах поля `TriStart` и `TriCount` в Компоненте 02
используются для индексации в Компонент 15 (треугольники), а не в Компонент 07.
---
## Структуры компонентов
### Компонент 0x01 - Node table (0x26 = 38 байт)
Определяет узлы модели или тайлы terrain. Старое локальное имя: Pieces / SubMesh.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | uint16 | Header0 | Заголовочное слово узла; старые имена: Type1 + Type2 |
| 0x02 | 2 | uint16 | ParentOrLink | Индекс родителя/ссылка; старый локальный тип int16 показывал 0xFFFF как -1 |
| 0x04 | 2 | uint16 | AnimMapStart | Начало блока в 0x13 или 0xFFFF; старое имя: OffsetIntoFile13 |
| 0x06 | 2 | uint16 | FallbackKey | Индекс fallback-ключа в 0x08; старое имя: IndexInFile08 |
| 0x08 | 30 | ushort[15] | SlotIndex | Индексы slot в 0x02 по формуле `lod * 5 + group`; старое имя: Lod |
**Ландшафт:** 256 тайлов в сетке 16×16. Каждый тайл имеет 2 LOD (индексы 0-255 и 256-511).
---
### Компонент 0x02 - Header + slots (Заголовок: 0x8C = 140 байт, slot: 0x44 = 68 байт)
#### Заголовок (140 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 96 | Vector3[8] | BoundingBox | 8-точечный баундинг-бокс |
| 0x60 | 12 | Vector3 | Center | Центральная точка |
| 0x6C | 4 | float | CenterW | W-компонента |
| 0x70 | 12 | Vector3 | Bottom | Нижняя точка |
| 0x7C | 12 | Vector3 | Top | Верхняя точка |
| 0x88 | 4 | float | XYRadius | Радиус в плоскости XY |
#### Элемент (68 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | TriStart | Начальный индекс в Компоненте 07; в landscape-tooling может указывать в 15 |
| 0x02 | 2 | ushort | TriCount | Количество записей в Компоненте 07; в landscape-tooling может быть count для 15 |
| 0x04 | 2 | ushort | BatchStart | Начальное смещение в Компоненте 0D (только Модель) |
| 0x06 | 2 | ushort | BatchCount | Количество батчей в Компоненте 0D (только Модель) |
| 0x08 | 12 | Vector3 | LocalMinimum | Минимум локального баундинг-бокса |
| 0x14 | 12 | Vector3 | LocalMaximum | Максимум локального баундинг-бокса |
| 0x20 | 12 | Vector3 | Center | Центр сабмеша |
| 0x2C | 4 | float | SphereRadius | Радиус bounding sphere; старый `Vector4` был overlay-гипотезой |
| 0x30 | 20 | uint32[5] | Opaque | Непонятый tail, сохранять 1:1; старый `Vector5` был overlay-гипотезой |
---
### Компонент 03 - Vertices (0x0C = 12 байт)
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | float | X | Координата X |
| 0x04 | 4 | float | Y | Координата Y |
| 0x08 | 4 | float | Z | Координата Z |
---
### Компонент 06 - Indices (2 байта) - Только Модель
Массив `ushort` значений — индексы вершин треугольников.
Используются группами по 3 для каждого треугольника. Ссылки через батчи Компонента 0D.
---
### Компонент 0x07 - Tri descriptors (0x10 = 16 байт)
Описатели треугольников для коллизии/пикинга.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | TriFlags | Флаги треугольника; старое имя: Flags |
| 0x02 | 2 | ushort | Link0 | Связь/opaque поле 0; старое имя: Magic02 |
| 0x04 | 2 | ushort | Link1 | Связь/opaque поле 1; старое имя: Magic04 |
| 0x06 | 2 | ushort | Link2 | Связь/opaque поле 2; старое имя: Magic06 |
| 0x08 | 2 | int16 | NormalX | Упакованная X-компонента нормали; старое имя: OffsetX |
| 0x0A | 2 | int16 | NormalY | Упакованная Y-компонента нормали; старое имя: OffsetY |
| 0x0C | 2 | int16 | NormalZ | Упакованная Z-компонента нормали; старое имя: OffsetZ |
| 0x0E | 2 | ushort | SelectorPacked | Три 2-битных селектора; `3` трактуется как `0xFFFF`; старое имя: Magic14 |
---
### Компонент 0B - Material Data (4 байта) - Только Ландшафт
Информация о материале/текстуре на каждый треугольник. Параллельный массив к Компоненту 15.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | HighWord | Индекс материала/текстуры |
| 0x02 | 2 | ushort | LowWord | Индекс треугольника (последовательный) |
---
### Компонент 0x0D - Batch table (0x14 = 20 байт)
Определяет батчи вызовов отрисовки. В терминах FParkan это Res13 decimal.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 2 | ushort | BatchFlags / Flags.low | Флаги батча |
| 0x02 | 2 | ushort | MaterialIndex / Flags.high | Индекс material slot |
| 0x04 | 2 | ushort | Opaque4 | Opaque, старое имя `TriangleCount` не подтверждено |
| 0x06 | 2 | ushort | Opaque6 | Opaque |
| 0x08 | 2 | ushort | IndexCount | Количество индексов для отрисовки в 0x06 |
| 0x0A | 4 | uint32 | IndexStart | Начальный индекс в Компоненте 06 |
| 0x0E | 2 | ushort | Opaque14 | Opaque, старое имя `CountOf03` не подтверждено |
| 0x10 | 4 | uint32 | BaseVertex | Базовое смещение вершины в Компоненте 03 |
---
### Компонент 0x15 - TerrainTriangle table (0x1C = 28 байт)
Прямые определения terrain-треугольников. Это hex-компонент 0x15 проекта, не FParkan Res15 decimal.
| Смещение | Размер | Тип | Поле | Описание |
|:--------:|:------:|:---:|------|----------|
| 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) |
| 0x04 | 4 | uint32 | MaterialData | Данные материала; старое имя: Magic04 |
| 0x08 | 2 | ushort | Vertex1Index | Индекс первой вершины |
| 0x0A | 2 | ushort | Vertex2Index | Индекс второй вершины |
| 0x0C | 2 | ushort | Vertex3Index | Индекс третьей вершины |
| 0x0E | 4 | uint32 | Opaque0E | Opaque; старое имя: Magic0E |
| 0x12 | 4 | uint32 | Opaque12 | Opaque; старое имя: Magic12 |
| 0x16 | 4 | uint32 | Opaque16 | Opaque; старое имя: Magic16 |
| 0x1A | 2 | ushort | Opaque1A | Opaque; старое имя: Magic1A |
#### MaterialData (0x04) - Структура материала
```
MaterialData = 0xFFFF_SSPP
│ │└─ PP: Основной материал (byte 0)
│ └─── SS: Вторичный материал для блендинга (byte 1)
└────── Всегда 0xFFFF (байты 2-3)
```
| Значение SS | Описание |
|:-----------:|----------|
| 0xFF | Сплошной материал (без блендинга) |
| 0x01-0xFE | Индекс вторичного материала для блендинга |
Примеры:
- `0xFFFFFF01` = Сплошной материал 1
- `0xFFFF0203` = Материал 3 с блендингом в материал 2
---
### Компонент 0A - External References (переменный размер)
Таблица строк для внешних ссылок на части меша. Формат:
```
[4 байта: длина] [байты строки] [null-терминатор]
...повтор...
```
Длина 0 означает пустую запись. Строки типа `"central"` имеют особое значение (flag |= 1).
---
## Пример: Ландшафт SC_1
```
Land.msh (SC_1):
├── 01: 256 тайлов (сетка 16×16)
├── 02: 512 сабмешей (256 LOD0 + 256 LOD1)
├── 03: 10 530 вершин
├── 04: 10 530 данных на вершину
├── 05: 10 530 данных на вершину
├── 0B: 7 882 записи материалов
├── 0E: 10 530 данных на вершину
├── 12: 10 530 микротекстурный маппинг
└── 15: 7 882 треугольника
├── LOD 0: 4 993 треугольника (тайлы 0-255 → сабмеши 0-255)
└── LOD 1: 2 889 треугольников (тайлы 0-255 → сабмеши 256-511)
```
---
## Использование
```csharp
var converter = new MshConverter();
// Автоопределение типа и конвертация в OBJ
converter.Convert("Land.msh", "terrain.obj", lodLevel: 0);
converter.Convert("robot.msh", "robot.obj", lodLevel: 0);
// Ручное определение типа
var archive = NResParser.ReadFile("mesh.msh").Archive;
var type = MshConverter.DetectMeshType(archive);
// Возвращает: MshType.Model или MshType.Landscape
```
---
## Формат WEA - Файлы материалов ландшафта
Файлы `.wea` — текстовые файлы, определяющие таблицу материалов для ландшафта.
### Формат
```
{count}
{index} {material_name}
{index} {material_name}
...
```
### Связь с Land.msh
Каждая карта имеет два файла материалов:
| Файл | Используется для | Треугольники в Comp15 |
|------|------------------|----------------------|
| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма TriCount для LOD0) |
| `Land2.wea` | LOD1 (низкая детализация) | Остальные |
### Пример (SC_1)
**Land1.wea:**
```
4
0 B_S0
1 L04
2 L02
3 L00
```
**Land2.wea:**
```
4
0 DEFAULT
1 L05
2 L03
3 L01
```
### Маппинг материалов
Индекс материала в `Comp15.MaterialData & 0xFF` → строка в `.wea` файле.
```
Треугольник с MaterialData = 0xFFFF0102
└─ Основной материал = 02 → Land1.wea[2] = "L02"
└─ Блендинг с материалом = 01 → Land1.wea[1] = "L04"
```
### Типичные имена материалов
| Префикс | Назначение |
|---------|------------|
| L00-L05 | Текстуры ландшафта (grass, dirt, etc.) |
| B_S0 | Базовая текстура |
| DEFAULT | Фолбэк для LOD1 |
| WATER | Вода (поверхность) |
| WATER_BOT | Вода (дно) |
| WATER_M | Вода LOD1 |
---
## Источники
- Реверс-инжиниринг `Terrain.dll` (класс CLandscape)
- Декомпиляция Ghidra: `CLandscape::ctor` и `IMesh2_of_CLandscape::Render`