mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
fpopov thanks
This commit is contained in:
@@ -25,12 +25,17 @@ public static class FxidReader
|
||||
{
|
||||
EffectHeader h;
|
||||
h.ComponentCount = br.ReadUInt32();
|
||||
h.Unknown1 = br.ReadUInt32();
|
||||
h.TimeMode = br.ReadUInt32();
|
||||
h.Duration = br.ReadSingle();
|
||||
h.Unknown2 = br.ReadSingle();
|
||||
h.PhaseJitter = br.ReadSingle();
|
||||
h.Flags = br.ReadUInt32();
|
||||
h.Unknown3 = br.ReadUInt32();
|
||||
h.Reserved = br.ReadBytes(24);
|
||||
h.SettingsId = br.ReadUInt32();
|
||||
h.RandShiftX = br.ReadSingle();
|
||||
h.RandShiftY = br.ReadSingle();
|
||||
h.RandShiftZ = br.ReadSingle();
|
||||
h.PivotX = br.ReadSingle();
|
||||
h.PivotY = br.ReadSingle();
|
||||
h.PivotZ = br.ReadSingle();
|
||||
h.ScaleX = br.ReadSingle();
|
||||
h.ScaleY = br.ReadSingle();
|
||||
h.ScaleZ = br.ReadSingle();
|
||||
|
||||
@@ -7,18 +7,39 @@ namespace ParkanPlayground.Effects;
|
||||
/// Parsed from CEffect_InitFromDef: defines component count, global duration/flags,
|
||||
/// some unknown control fields, and the uniform scale vector applied to the effect.
|
||||
/// </summary>
|
||||
public struct EffectHeader
|
||||
public record struct EffectHeader
|
||||
{
|
||||
/// <summary>FXID payload offset 0x00: command count.</summary>
|
||||
public uint ComponentCount;
|
||||
public uint Unknown1;
|
||||
/// <summary>FXID payload offset 0x04: time mode used to compute effect alpha.</summary>
|
||||
public uint TimeMode;
|
||||
/// <summary>FXID payload offset 0x08: effect duration in seconds.</summary>
|
||||
public float Duration;
|
||||
public float Unknown2;
|
||||
/// <summary>FXID payload offset 0x0C: random phase shift amplitude.</summary>
|
||||
public float PhaseJitter;
|
||||
/// <summary>FXID payload offset 0x10: effect behavior flags.</summary>
|
||||
public uint Flags;
|
||||
public uint Unknown3;
|
||||
public byte[] Reserved; // 24 bytes
|
||||
/// <summary>FXID payload offset 0x14: settings/profile id.</summary>
|
||||
public uint SettingsId;
|
||||
/// <summary>FXID payload offset 0x18: random spatial shift X.</summary>
|
||||
public float RandShiftX;
|
||||
/// <summary>FXID payload offset 0x1C: random spatial shift Y.</summary>
|
||||
public float RandShiftY;
|
||||
/// <summary>FXID payload offset 0x20: random spatial shift Z.</summary>
|
||||
public float RandShiftZ;
|
||||
/// <summary>FXID payload offset 0x24: local pivot X.</summary>
|
||||
public float PivotX;
|
||||
/// <summary>FXID payload offset 0x28: local pivot Y.</summary>
|
||||
public float PivotY;
|
||||
/// <summary>FXID payload offset 0x2C: local pivot Z.</summary>
|
||||
public float PivotZ;
|
||||
/// <summary>FXID payload offset 0x30: base scale X.</summary>
|
||||
public float ScaleX;
|
||||
/// <summary>FXID payload offset 0x34: base scale Y.</summary>
|
||||
public float ScaleY;
|
||||
/// <summary>FXID payload offset 0x38: base scale Z.</summary>
|
||||
public float ScaleZ;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -26,7 +47,7 @@ public struct EffectHeader
|
||||
/// Used by CBillboardComponent_Initialize/Update/Render to drive size/color/alpha
|
||||
/// curves and sample scattering within a 3D extent volume.
|
||||
/// </summary>
|
||||
public struct BillboardComponentData
|
||||
public record struct BillboardComponentData
|
||||
{
|
||||
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
|
||||
public float Unknown04; // mode / flag-like float, semantics not fully clear
|
||||
@@ -56,7 +77,7 @@ public struct BillboardComponentData
|
||||
/// Used by CSoundComponent_Initialize/Update to drive positional audio, playback
|
||||
/// window, and scalar ranges (e.g. volume / pitch), plus a 0x40-byte sound name tail.
|
||||
/// </summary>
|
||||
public struct SoundComponentData
|
||||
public record struct SoundComponentData
|
||||
{
|
||||
public uint TypeAndFlags; // component type and flags
|
||||
public uint PlayMode; // playback mode (looping, one-shot, etc.)
|
||||
@@ -79,7 +100,7 @@ public struct SoundComponentData
|
||||
/// Prefix layout matches BillboardComponentData and is used to allocate a grid of
|
||||
/// particle objects; the 0x38-byte tail is passed into CFxManager_LoadTexture.
|
||||
/// </summary>
|
||||
public struct AnimParticleComponentData
|
||||
public record struct AnimParticleComponentData
|
||||
{
|
||||
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
|
||||
public float Unknown04; // mode / flag-like float, semantics not fully clear
|
||||
@@ -109,7 +130,7 @@ public struct AnimParticleComponentData
|
||||
/// Shares the same prefix layout as BillboardComponentData, including extents and
|
||||
/// radius/exponent triplets, but uses a 0x3C-byte tail passed to CFxManager_LoadTexture.
|
||||
/// </summary>
|
||||
public struct AnimBillboardComponentData
|
||||
public record struct AnimBillboardComponentData
|
||||
{
|
||||
public uint TypeAndFlags; // type (low byte) and flags as seen in CEffect_InitFromDef
|
||||
public float Unknown04; // mode / flag-like float, semantics not fully clear
|
||||
@@ -139,7 +160,7 @@ public struct AnimBillboardComponentData
|
||||
/// CTrailComponent_Initialize interprets this as segment count, width/alpha/UV
|
||||
/// ranges, timing, and a shared texture name at +0x30.
|
||||
/// </summary>
|
||||
public struct TrailComponentData
|
||||
public record struct TrailComponentData
|
||||
{
|
||||
public uint TypeAndFlags; // component type and flags
|
||||
public byte[] Unknown04To10; // 0x10 bytes at +4..+0x13, used only indirectly; types unknown
|
||||
@@ -157,7 +178,7 @@ public struct TrailComponentData
|
||||
/// Simple point component definition (type 6).
|
||||
/// Definition block is just the 4-byte typeAndFlags header; no extra data on disk.
|
||||
/// </summary>
|
||||
public struct PointComponentData
|
||||
public record struct PointComponentData
|
||||
{
|
||||
public uint TypeAndFlags; // component type and flags; definition block has no payload
|
||||
}
|
||||
@@ -167,7 +188,7 @@ public struct PointComponentData
|
||||
/// Shares the same 0xC8-byte prefix layout as AnimParticleComponentData (type 3),
|
||||
/// followed by two dwords of plane-specific data.
|
||||
/// </summary>
|
||||
public struct PlaneComponentData
|
||||
public record struct PlaneComponentData
|
||||
{
|
||||
public AnimParticleComponentData Base; // shared 0xC8-byte prefix: time window, sample counts, extents, curves
|
||||
public uint ExtraPlaneParam0; // plane-specific parameter, semantics not yet reversed
|
||||
@@ -180,7 +201,7 @@ public struct PlaneComponentData
|
||||
/// time window, instance count, spatial extents/axes, radius triplets, and a
|
||||
/// 0x40-byte texture name tail.
|
||||
/// </summary>
|
||||
public struct ModelComponentData
|
||||
public record struct ModelComponentData
|
||||
{
|
||||
public uint TypeAndFlags; // component type and flags
|
||||
public byte[] Unk04; // 0x14-byte blob at +0x04..+0x17, purpose unclear
|
||||
@@ -206,7 +227,7 @@ public struct ModelComponentData
|
||||
/// Layout derived from CAnimModelComponent_Initialize: time params, direction vectors,
|
||||
/// radius triplets, extent vectors, and a 0x48-byte texture name tail.
|
||||
/// </summary>
|
||||
public struct AnimModelComponentData
|
||||
public record struct AnimModelComponentData
|
||||
{
|
||||
public uint TypeAndFlags; // component type and flags
|
||||
public float AnimSpeed; // animation speed multiplier at +0x04
|
||||
@@ -230,7 +251,7 @@ public struct AnimModelComponentData
|
||||
/// Shares the same 0xCC-byte prefix layout as AnimBillboardComponentData (type 4),
|
||||
/// followed by one dword of cube-specific data.
|
||||
/// </summary>
|
||||
public struct CubeComponentData
|
||||
public record struct CubeComponentData
|
||||
{
|
||||
public AnimBillboardComponentData Base; // shared 0xCC-byte prefix: billboard-style time window, extents, curves
|
||||
public uint ExtraCubeParam0; // cube-specific parameter, semantics not yet reversed
|
||||
|
||||
@@ -25,21 +25,21 @@ MSH файлы — это NRes архивы, содержащие несколь
|
||||
|
||||
| Тип | Название | Размер элемента | Описание |
|
||||
|:---:|----------|:---------------:|----------|
|
||||
| 01 | Pieces | 38 (0x26) | Части меша / тайлы с LOD-ссылками |
|
||||
| 02 | Submeshes | 68 (0x44) | LOD части с баундинг-боксами |
|
||||
| 03 | Vertices | 12 (0x0C) | Позиции вершин (Vector3) |
|
||||
| 04 | неизвестно | 4 | (неизвестно) |
|
||||
| 05 | неизвестно | 4 | (неизвестно) |
|
||||
| 06 | Indices | 2 | Индексы вершин треугольников (только Модель) |
|
||||
| 07 | неизвестно | 16 | (только Модель) |
|
||||
| 08 | Animations | 4 | Кейфреймы анимации меша |
|
||||
| 0A | ExternalRefs | переменный | Внешние ссылки на меши (строки) |
|
||||
| 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 | неизвестно | 20 (0x14) | неизвестно (только Модель) |
|
||||
| 0D | Batch table | 20 (0x14) | Батчи рендера; FParkan Res13 decimal |
|
||||
| 0E | неизвестно | 4 | неизвестно (только Ландшафт) |
|
||||
| 12 | MicrotextureMap | 4 | неизвестно |
|
||||
| 13 | ShortAnims | 2 | Короткие индексы анимаций |
|
||||
| 15 | неизвестно | 28 (0x1C) | неизвестно |
|
||||
| 13 | AnimMap | 2 | Карта кадров анимации, на нее указывает `AnimMapStart` из 0x01 |
|
||||
| 15 | TerrainTriangle table | 28 (0x1C) | Terrain-гипотеза |
|
||||
|
||||
---
|
||||
|
||||
@@ -52,15 +52,15 @@ MSH файлы — это NRes архивы, содержащие несколь
|
||||
│
|
||||
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
|
||||
│
|
||||
├─► StartIndexIn07 ──► Компонент 07 (данные на треугольник)
|
||||
├─► TriStart:TriCount ──► Компонент 07 (данные на треугольник)
|
||||
│
|
||||
└─► StartOffsetIn0d:ByteLengthIn0D ──► Компонент 0D (батчи)
|
||||
└─► BatchStart:BatchCount ──► Компонент 0D (батчи)
|
||||
│
|
||||
├─► IndexInto06:CountOf06 ──► Компонент 06 (индексы)
|
||||
├─► IndexStart:IndexCount ──► Компонент 06 (индексы)
|
||||
│ │
|
||||
│ └─► Компонент 03 (вершины)
|
||||
│
|
||||
└─► IndexInto03 (базовое смещение вершины)
|
||||
└─► BaseVertex (базовое смещение вершины)
|
||||
```
|
||||
|
||||
### Ландшафт (террейн)
|
||||
@@ -70,38 +70,37 @@ MSH файлы — это NRes архивы, содержащие несколь
|
||||
│
|
||||
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
|
||||
│
|
||||
└─► StartIndexIn07:CountIn07 ──► Компонент 15 (треугольники)
|
||||
└─► TriStart:TriCount ──► Компонент 15 (треугольники)
|
||||
│
|
||||
└─► Vertex1/2/3Index ──► Компонент 03 (вершины)
|
||||
|
||||
└─► StartIndexIn07:CountIn07 ──► Компонент 0B (материалы, параллельно 15)
|
||||
└─► TriStart:TriCount ──► Компонент 0B (материалы, параллельно 15)
|
||||
```
|
||||
|
||||
**Важно:** В ландшафтных мешах поля `StartIndexIn07` и `CountIn07` в Компоненте 02
|
||||
**Важно:** В ландшафтных мешах поля `TriStart` и `TriCount` в Компоненте 02
|
||||
используются для индексации в Компонент 15 (треугольники), а не в Компонент 07.
|
||||
|
||||
---
|
||||
|
||||
## Структуры компонентов
|
||||
|
||||
### Компонент 01 - Pieces (0x26 = 38 байт)
|
||||
### Компонент 0x01 - Node table (0x26 = 38 байт)
|
||||
|
||||
Определяет части меша (для моделей) или тайлы террейна (для ландшафтов).
|
||||
Определяет узлы модели или тайлы terrain. Старое локальное имя: Pieces / SubMesh.
|
||||
|
||||
| Смещение | Размер | Тип | Поле | Описание |
|
||||
|:--------:|:------:|:---:|------|----------|
|
||||
| 0x00 | 1 | byte | Type1 | Флаги типа части |
|
||||
| 0x01 | 1 | byte | Type2 | Дополнительные флаги |
|
||||
| 0x02 | 2 | int16 | ParentIndex | Индекс родителя (-1 = корень) |
|
||||
| 0x04 | 2 | int16 | OffsetIntoFile13 | Смещение в короткие анимации |
|
||||
| 0x06 | 2 | int16 | IndexInFile08 | Индекс в анимации |
|
||||
| 0x08 | 30 | ushort[15] | Lod | Индексы сабмешей по LOD-уровням (0xFFFF = не используется) |
|
||||
| 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).
|
||||
|
||||
---
|
||||
|
||||
### Компонент 02 - Submeshes (Заголовок: 0x8C = 140 байт, Элемент: 0x44 = 68 байт)
|
||||
### Компонент 0x02 - Header + slots (Заголовок: 0x8C = 140 байт, slot: 0x44 = 68 байт)
|
||||
|
||||
#### Заголовок (140 байт)
|
||||
|
||||
@@ -118,15 +117,15 @@ MSH файлы — это NRes архивы, содержащие несколь
|
||||
|
||||
| Смещение | Размер | Тип | Поле | Описание |
|
||||
|:--------:|:------:|:---:|------|----------|
|
||||
| 0x00 | 2 | ushort | StartIndexIn07 | **Модель:** Начальный индекс в Компоненте 07<br>**Ландшафт:** Начальный индекс треугольника в Компоненте 15 |
|
||||
| 0x02 | 2 | ushort | CountIn07 | **Модель:** Количество в Компоненте 07<br>**Ландшафт:** Количество треугольников |
|
||||
| 0x04 | 2 | ushort | StartOffsetIn0d | Начальное смещение в Компоненте 0D (только Модель) |
|
||||
| 0x06 | 2 | ushort | ByteLengthIn0D | Количество батчей в Компоненте 0D (только Модель) |
|
||||
| 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 | 12 | Vector3 | Vector4 | Неизвестно |
|
||||
| 0x38 | 12 | Vector3 | Vector5 | Неизвестно |
|
||||
| 0x2C | 4 | float | SphereRadius | Радиус bounding sphere; старый `Vector4` был overlay-гипотезой |
|
||||
| 0x30 | 20 | uint32[5] | Opaque | Непонятый tail, сохранять 1:1; старый `Vector5` был overlay-гипотезой |
|
||||
|
||||
---
|
||||
|
||||
@@ -147,20 +146,20 @@ MSH файлы — это NRes архивы, содержащие несколь
|
||||
|
||||
---
|
||||
|
||||
### Компонент 07 - Triangle Data (0x10 = 16 байт) - Только Модель
|
||||
### Компонент 0x07 - Tri descriptors (0x10 = 16 байт)
|
||||
|
||||
Данные рендеринга на каждый треугольник.
|
||||
Описатели треугольников для коллизии/пикинга.
|
||||
|
||||
| Смещение | Размер | Тип | Поле | Описание |
|
||||
|:--------:|:------:|:---:|------|----------|
|
||||
| 0x00 | 2 | ushort | Flags | Флаги рендера |
|
||||
| 0x02 | 2 | ushort | Magic02 | Неизвестно |
|
||||
| 0x04 | 2 | ushort | Magic04 | Неизвестно |
|
||||
| 0x06 | 2 | ushort | Magic06 | Неизвестно |
|
||||
| 0x08 | 2 | int16 | OffsetX | Нормализованный X (÷32767 для -1..1) |
|
||||
| 0x0A | 2 | int16 | OffsetY | Нормализованный Y (÷32767 для -1..1) |
|
||||
| 0x0C | 2 | int16 | OffsetZ | Нормализованный Z (÷32767 для -1..1) |
|
||||
| 0x0E | 2 | ushort | Magic14 | Неизвестно |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -175,40 +174,38 @@ MSH файлы — это NRes архивы, содержащие несколь
|
||||
|
||||
---
|
||||
|
||||
### Компонент 0D - Draw Batches (0x14 = 20 байт) - Только Модель
|
||||
### Компонент 0x0D - Batch table (0x14 = 20 байт)
|
||||
|
||||
Определяет батчи вызовов отрисовки.
|
||||
Определяет батчи вызовов отрисовки. В терминах FParkan это Res13 decimal.
|
||||
|
||||
| Смещение | Размер | Тип | Поле | Описание |
|
||||
|:--------:|:------:|:---:|------|----------|
|
||||
| 0x00 | 2 | ushort | Flags | Флаги батча |
|
||||
| 0x02 | 2 | - | Padding | - |
|
||||
| 0x04 | 1 | byte | Magic04 | Неизвестно |
|
||||
| 0x05 | 1 | byte | Magic05 | Неизвестно |
|
||||
| 0x06 | 2 | ushort | Magic06 | Неизвестно |
|
||||
| 0x08 | 2 | ushort | CountOf06 | Количество индексов для отрисовки |
|
||||
| 0x0A | 4 | int32 | IndexInto06 | Начальный индекс в Компоненте 06 |
|
||||
| 0x0E | 2 | ushort | CountOf03 | Количество вершин |
|
||||
| 0x10 | 4 | int32 | IndexInto03 | Базовое смещение вершины в Компоненте 03 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
### Компонент 15 - Triangles (0x1C = 28 байт)
|
||||
### Компонент 0x15 - TerrainTriangle table (0x1C = 28 байт)
|
||||
|
||||
Прямые определения треугольников. Используется и Моделью и Ландшафтом,
|
||||
но только Ландшафт использует их напрямую для рендеринга.
|
||||
Прямые определения terrain-треугольников. Это hex-компонент 0x15 проекта, не FParkan Res15 decimal.
|
||||
|
||||
| Смещение | Размер | Тип | Поле | Описание |
|
||||
|:--------:|:------:|:---:|------|----------|
|
||||
| 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) |
|
||||
| 0x04 | 4 | uint32 | MaterialData | Данные материала (см. ниже) |
|
||||
| 0x04 | 4 | uint32 | MaterialData | Данные материала; старое имя: Magic04 |
|
||||
| 0x08 | 2 | ushort | Vertex1Index | Индекс первой вершины |
|
||||
| 0x0A | 2 | ushort | Vertex2Index | Индекс второй вершины |
|
||||
| 0x0C | 2 | ushort | Vertex3Index | Индекс третьей вершины |
|
||||
| 0x0E | 4 | uint32 | Magic0E | Неизвестно |
|
||||
| 0x12 | 4 | uint32 | Magic12 | Неизвестно |
|
||||
| 0x16 | 4 | uint32 | Magic16 | Неизвестно |
|
||||
| 0x1A | 2 | ushort | Magic1A | Неизвестно |
|
||||
| 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) - Структура материала
|
||||
|
||||
@@ -298,7 +295,7 @@ var type = MshConverter.DetectMeshType(archive);
|
||||
|
||||
| Файл | Используется для | Треугольники в Comp15 |
|
||||
|------|------------------|----------------------|
|
||||
| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма CountIn07 для LOD0) |
|
||||
| `Land1.wea` | LOD0 (высокая детализация) | Первые N (сумма TriCount для LOD0) |
|
||||
| `Land2.wea` | LOD1 (низкая детализация) | Остальные |
|
||||
|
||||
### Пример (SC_1)
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
public static class Msh01
|
||||
{
|
||||
public static Msh01Component ReadComponent(FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var headerFileEntry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00");
|
||||
|
||||
if (headerFileEntry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain header file (01)");
|
||||
}
|
||||
|
||||
var data = new byte[headerFileEntry.ElementCount * headerFileEntry.ElementSize];
|
||||
mshFs.Seek(headerFileEntry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var dataSpan = data.AsSpan();
|
||||
|
||||
var elements = new List<SubMesh>((int)headerFileEntry.ElementCount);
|
||||
for (var i = 0; i < headerFileEntry.ElementCount; i++)
|
||||
{
|
||||
var element = new SubMesh()
|
||||
{
|
||||
Type1 = dataSpan[i * headerFileEntry.ElementSize + 0],
|
||||
Type2 = dataSpan[i * headerFileEntry.ElementSize + 1],
|
||||
ParentIndex =
|
||||
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 2)),
|
||||
OffsetIntoFile13 =
|
||||
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 4)),
|
||||
IndexInFile08 =
|
||||
BinaryPrimitives.ReadInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 6))
|
||||
};
|
||||
|
||||
element.Lod[0] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 8));
|
||||
element.Lod[1] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 10));
|
||||
element.Lod[2] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 12));
|
||||
element.Lod[3] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 14));
|
||||
element.Lod[4] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 16));
|
||||
element.Lod[5] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 18));
|
||||
element.Lod[6] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 20));
|
||||
element.Lod[7] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 22));
|
||||
element.Lod[8] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 24));
|
||||
element.Lod[9] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 26));
|
||||
element.Lod[10] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 28));
|
||||
element.Lod[11] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 30));
|
||||
element.Lod[12] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 32));
|
||||
element.Lod[13] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 34));
|
||||
element.Lod[14] = BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(i * headerFileEntry.ElementSize + 36));
|
||||
elements.Add(element);
|
||||
}
|
||||
|
||||
return new Msh01Component()
|
||||
{
|
||||
Elements = elements
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public class Msh01Component
|
||||
{
|
||||
public List<SubMesh> Elements { get; set; }
|
||||
}
|
||||
|
||||
public class SubMesh
|
||||
{
|
||||
public byte Type1 { get; set; }
|
||||
public byte Type2 { get; set; }
|
||||
public short ParentIndex { get; set; }
|
||||
public short OffsetIntoFile13 { get; set; }
|
||||
public short IndexInFile08 { get; set; }
|
||||
public ushort[] Lod { get; set; } = new ushort[15];
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
using System.Buffers.Binary;
|
||||
using Common;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
public static class Msh02
|
||||
{
|
||||
public static Msh02Component ReadComponent(FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var fileEntry = archive.Files.FirstOrDefault(x => x.FileType == "02 00 00 00");
|
||||
|
||||
if (fileEntry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain 02 component");
|
||||
}
|
||||
|
||||
var data = new byte[fileEntry.FileLength];
|
||||
mshFs.Seek(fileEntry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var header = data.AsSpan(0, 0x8c); // 140 bytes header
|
||||
|
||||
var center = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x60)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x64)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x68))
|
||||
);
|
||||
var centerW = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x6c));
|
||||
|
||||
var bb = new BoundingBox();
|
||||
bb.BottomFrontLeft = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(4)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(8))
|
||||
);
|
||||
bb.BottomFrontRight = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(12)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(16)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(20))
|
||||
);
|
||||
bb.BottomBackRight = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(24)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(28)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(32))
|
||||
);
|
||||
bb.BottomBackLeft = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(36)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(40)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(44))
|
||||
);
|
||||
bb.TopFrontLeft = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(48)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(52)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(56))
|
||||
);
|
||||
bb.TopFrontRight = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(60)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(64)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(68))
|
||||
);
|
||||
bb.TopBackRight = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(72)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(76)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(80))
|
||||
);
|
||||
bb.TopBackLeft = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(84)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(88)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(92))
|
||||
);
|
||||
|
||||
var bottom = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(112)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(116)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(120))
|
||||
);
|
||||
|
||||
var top = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(124)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(128)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(132))
|
||||
);
|
||||
|
||||
var xyRadius = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(136));
|
||||
|
||||
|
||||
List<Msh02Element> elements = new List<Msh02Element>();
|
||||
var skippedHeader = data.AsSpan(0x8c); // skip header
|
||||
for (var i = 0; i < fileEntry.ElementCount; i++)
|
||||
{
|
||||
var element = new Msh02Element();
|
||||
element.StartIndexIn07 =
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 0));
|
||||
element.CountIn07 =
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 2));
|
||||
element.StartOffsetIn0d =
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 4));
|
||||
element.ByteLengthIn0D =
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 6));
|
||||
element.LocalMinimum = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 8)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 12)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 16))
|
||||
);
|
||||
element.LocalMaximum = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 20)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 24)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 28))
|
||||
);
|
||||
element.Center = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 32)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 36)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 40))
|
||||
);
|
||||
element.Vector4 = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 44)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 48)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 52))
|
||||
);
|
||||
element.Vector5 = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 56)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 60)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(fileEntry.ElementSize * i + 64))
|
||||
);
|
||||
elements.Add(element);
|
||||
|
||||
_ = 5;
|
||||
}
|
||||
|
||||
return new Msh02Component()
|
||||
{
|
||||
Header = new Msh02Header()
|
||||
{
|
||||
BoundingBox = bb,
|
||||
Center = center,
|
||||
CenterW = centerW,
|
||||
Bottom = bottom,
|
||||
Top = top,
|
||||
XYRadius = xyRadius
|
||||
},
|
||||
Elements = elements
|
||||
};
|
||||
}
|
||||
|
||||
public class Msh02Component
|
||||
{
|
||||
public Msh02Header Header { get; set; }
|
||||
public List<Msh02Element> Elements { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 140 байт в начале файла
|
||||
/// </summary>
|
||||
public class Msh02Header
|
||||
{
|
||||
public BoundingBox BoundingBox { get; set; }
|
||||
public Vector3 Center { get; set; }
|
||||
public float CenterW { get; set; }
|
||||
public Vector3 Bottom { get; set; }
|
||||
public Vector3 Top { get; set; }
|
||||
public float XYRadius { get; set; }
|
||||
}
|
||||
|
||||
public class Msh02Element
|
||||
{
|
||||
public ushort StartIndexIn07 { get; set; }
|
||||
public ushort CountIn07 { get; set; }
|
||||
public ushort StartOffsetIn0d { get; set; }
|
||||
public ushort ByteLengthIn0D { get; set; }
|
||||
public Vector3 LocalMinimum { get; set; }
|
||||
public Vector3 LocalMaximum { get; set; }
|
||||
public Vector3 Center { get; set; }
|
||||
public Vector3 Vector4 { get; set; }
|
||||
public Vector3 Vector5 { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 96 bytes - bounding box (8 points each 3 float = 96 bytes)
|
||||
/// 0x60 bytes or 0x18 by 4 bytes
|
||||
/// </summary>
|
||||
public class BoundingBox
|
||||
{
|
||||
public Vector3 BottomFrontLeft { get; set; }
|
||||
public Vector3 BottomFrontRight { get; set; }
|
||||
public Vector3 BottomBackRight { get; set; }
|
||||
public Vector3 BottomBackLeft { get; set; }
|
||||
public Vector3 TopBackRight { get; set; }
|
||||
public Vector3 TopFrontRight { get; set; }
|
||||
public Vector3 TopBackLeft { get; set; }
|
||||
public Vector3 TopFrontLeft { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
public static class Msh07
|
||||
{
|
||||
public static List<Msh07Element> ReadComponent(
|
||||
FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "07 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain file (07)");
|
||||
}
|
||||
|
||||
var data = new byte[entry.ElementCount * entry.ElementSize];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elementBytes = data.Chunk(16);
|
||||
|
||||
var elements = elementBytes.Select(x => new Msh07Element()
|
||||
{
|
||||
Flags = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
|
||||
Magic02 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
|
||||
Magic04 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
|
||||
Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
|
||||
OffsetX = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(8)),
|
||||
OffsetY = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(10)),
|
||||
OffsetZ = BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(12)),
|
||||
Magic14 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(14)),
|
||||
}).ToList();
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
public class Msh07Element
|
||||
{
|
||||
public ushort Flags { get; set; }
|
||||
public ushort Magic02 { get; set; }
|
||||
public ushort Magic04 { get; set; }
|
||||
public ushort Magic06 { get; set; }
|
||||
// normalized vector X, need to divide by 32767 to get float in range -1..1
|
||||
public short OffsetX { get; set; }
|
||||
// normalized vector Y, need to divide by 32767 to get float in range -1..1
|
||||
public short OffsetY { get; set; }
|
||||
// normalized vector Z, need to divide by 32767 to get float in range -1..1
|
||||
public short OffsetZ { get; set; }
|
||||
public ushort Magic14 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
public static class Msh0D
|
||||
{
|
||||
public const int ElementSize = 20;
|
||||
|
||||
public static List<Msh0DElement> ReadComponent(
|
||||
FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "0D 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain file (0D)");
|
||||
}
|
||||
|
||||
var data = new byte[entry.ElementCount * entry.ElementSize];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elementBytes = data.Chunk(ElementSize);
|
||||
|
||||
var elements = elementBytes.Select(x => new Msh0DElement()
|
||||
{
|
||||
Flags = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)),
|
||||
TriangleCount = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
|
||||
Magic06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
|
||||
CountOf06 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
|
||||
IndexInto06 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0xA)),
|
||||
CountOf03 = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0xE)),
|
||||
IndexInto03 = BinaryPrimitives.ReadInt32LittleEndian(x.AsSpan(0x10)),
|
||||
}).ToList();
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
public class Msh0DElement
|
||||
{
|
||||
public uint Flags { get; set; }
|
||||
|
||||
// Magic04 и Magic06 обрабатываются вместе
|
||||
|
||||
public ushort TriangleCount { get; set; }
|
||||
public ushort Magic06 { get; set; }
|
||||
public ushort CountOf06 { get; set; }
|
||||
public int IndexInto06 { get; set; }
|
||||
public ushort CountOf03 { get; set; }
|
||||
public int IndexInto03 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x01: таблица узлов модели.
|
||||
/// </summary>
|
||||
public static class Msh0x01
|
||||
{
|
||||
public static Msh0x01Component ReadComponent(FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain node table component (0x01)");
|
||||
}
|
||||
|
||||
if (entry.ElementSize <= 0)
|
||||
{
|
||||
throw new Exception("Node table component (0x01) has invalid element size");
|
||||
}
|
||||
|
||||
if (entry.FileLength % entry.ElementSize != 0)
|
||||
{
|
||||
throw new Exception("Node table component (0x01) payload size is not divisible by element size");
|
||||
}
|
||||
|
||||
var elementCount = entry.FileLength / entry.ElementSize;
|
||||
var data = new byte[entry.FileLength];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var dataSpan = data.AsSpan();
|
||||
|
||||
var elements = new List<Node>(elementCount);
|
||||
for (var i = 0; i < elementCount; i++)
|
||||
{
|
||||
var baseOffset = i * entry.ElementSize;
|
||||
var rawBytes = dataSpan.Slice(baseOffset, entry.ElementSize).ToArray();
|
||||
var slots = new ushort[15];
|
||||
Array.Fill(slots, ushort.MaxValue);
|
||||
var slotWords = Math.Min(slots.Length, Math.Max(0, (entry.ElementSize - 8) / 2));
|
||||
for (var slotIndex = 0; slotIndex < slotWords; slotIndex++)
|
||||
{
|
||||
slots[slotIndex] =
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 8 + slotIndex * 2));
|
||||
}
|
||||
|
||||
elements.Add(new Node(
|
||||
rawBytes,
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 2)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 4)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 6)),
|
||||
slots));
|
||||
}
|
||||
|
||||
return new Msh0x01Component(elements);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Результат чтения MSH-компонента 0x01.</summary>
|
||||
/// <param name="Elements">Узлы компонента 0x01.</param>
|
||||
public record Msh0x01Component(List<Node> Elements);
|
||||
|
||||
/// <summary>Узел 0x01.</summary>
|
||||
/// <param name="RawBytes">Сырые байты узла (length = attr3). Нужны для copy-through редких вариантов, например attr3 = 24.</param>
|
||||
/// <param name="Header0">[0x00..0x02] Заголовочное слово узла</param>
|
||||
/// <param name="ParentOrLink">[0x02..0x04] Индекс родителя или связанного узла</param>
|
||||
/// <param name="AnimMapStart">[0x04..0x06] Начало блока в карте анимации Msh0x13 или 0xFFFF</param>
|
||||
/// <param name="FallbackKey">[0x06..0x08] Индекс fallback-ключа в пуле ключей Msh0x08</param>
|
||||
/// <param name="SlotIndex">[0x08..0x26] Индексы slot в Msh0x02 по формуле lod * 5 + group</param>
|
||||
public record Node(
|
||||
byte[] RawBytes,
|
||||
ushort Header0,
|
||||
ushort ParentOrLink,
|
||||
ushort AnimMapStart,
|
||||
ushort FallbackKey,
|
||||
ushort[] SlotIndex)
|
||||
{
|
||||
public ushort ResolveSlotIndex(int lod, int group = 0)
|
||||
{
|
||||
var index = lod * 5 + group;
|
||||
return index >= 0 && index < SlotIndex.Length ? SlotIndex[index] : ushort.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using System.Buffers.Binary;
|
||||
using Common;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x02: общий заголовок и таблица slot.
|
||||
/// </summary>
|
||||
public static class Msh0x02
|
||||
{
|
||||
public static Msh0x02Component ReadComponent(FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var fileEntry = archive.Files.FirstOrDefault(x => x.FileType == "02 00 00 00");
|
||||
|
||||
if (fileEntry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain slots component (0x02)");
|
||||
}
|
||||
|
||||
if (fileEntry.FileLength < 0x8c)
|
||||
{
|
||||
throw new Exception("Slots component (0x02) is smaller than the 0x8C-byte header");
|
||||
}
|
||||
|
||||
if ((fileEntry.FileLength - 0x8c) % 68 != 0)
|
||||
{
|
||||
throw new Exception("Slots component (0x02) payload after header is not divisible by 68");
|
||||
}
|
||||
|
||||
var data = new byte[fileEntry.FileLength];
|
||||
mshFs.Seek(fileEntry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var header = data.AsSpan(0, 0x8c); // заголовок (length = 0x8C)
|
||||
|
||||
var center = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x60)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x64)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x68))
|
||||
);
|
||||
var centerW = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x6c));
|
||||
|
||||
var bb = new BoundingBox(
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(4)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(8))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(12)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(16)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(20))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(24)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(28)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(32))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(36)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(40)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(44))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(48)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(52)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(56))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(60)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(64)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(68))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(72)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(76)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(80))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(84)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(88)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(92))
|
||||
));
|
||||
|
||||
var bottom = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(112)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(116)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(120))
|
||||
);
|
||||
|
||||
var top = new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(124)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(128)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(132))
|
||||
);
|
||||
|
||||
var xyRadius = BinaryPrimitives.ReadSingleLittleEndian(header.Slice(136));
|
||||
|
||||
|
||||
var elements = new List<Slot>();
|
||||
var skippedHeader = data.AsSpan(0x8c);
|
||||
var slotCount = skippedHeader.Length / 68;
|
||||
for (var i = 0; i < slotCount; i++)
|
||||
{
|
||||
var baseOffset = 68 * i;
|
||||
var opaque = new uint[5];
|
||||
for (var opaqueIndex = 0; opaqueIndex < opaque.Length; opaqueIndex++)
|
||||
{
|
||||
opaque[opaqueIndex] =
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(skippedHeader.Slice(baseOffset + 48 + opaqueIndex * 4));
|
||||
}
|
||||
|
||||
elements.Add(new Slot(
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 0)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 2)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 4)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 6)),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 8)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 12)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 16))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 20)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 24)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 28))
|
||||
),
|
||||
new Vector3(
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 32)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 36)),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 40))
|
||||
),
|
||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 44)),
|
||||
opaque));
|
||||
|
||||
}
|
||||
|
||||
return new Msh0x02Component(
|
||||
new Msh02Header(bb, center, centerW, bottom, top, xyRadius),
|
||||
elements);
|
||||
}
|
||||
|
||||
/// <summary>Результат чтения MSH-компонента 0x02.</summary>
|
||||
/// <param name="Header">Заголовок 0x02 (length = 0x8C).</param>
|
||||
/// <param name="Elements">Slot records после заголовка.</param>
|
||||
public record class Msh0x02Component(Msh02Header Header, List<Slot> Elements);
|
||||
|
||||
/// <summary>Заголовок 0x02 (length = 0x8C).</summary>
|
||||
/// <param name="BoundingBox">[0x00..0x60] Bounding box из 8 точек.</param>
|
||||
/// <param name="Center">[0x60..0x6C] Центральная точка.</param>
|
||||
/// <param name="CenterW">[0x6C..0x70] W-компонента центра.</param>
|
||||
/// <param name="Bottom">[0x70..0x7C] Нижняя точка.</param>
|
||||
/// <param name="Top">[0x7C..0x88] Верхняя точка.</param>
|
||||
/// <param name="XYRadius">[0x88..0x8C] Радиус в плоскости XY.</param>
|
||||
public record class Msh02Header(
|
||||
BoundingBox BoundingBox,
|
||||
Vector3 Center,
|
||||
float CenterW,
|
||||
Vector3 Bottom,
|
||||
Vector3 Top,
|
||||
float XYRadius);
|
||||
|
||||
/// <summary>Slot 0x02 (length = 0x44).</summary>
|
||||
/// <param name="TriStart">[0x00..0x02] Первый triangle descriptor в Msh0x07. Для terrain-гипотезы может быть диапазоном Msh0x15.</param>
|
||||
/// <param name="TriCount">[0x02..0x04] Количество triangle descriptor в Msh0x07. Для terrain-гипотезы может быть count для Msh0x15.</param>
|
||||
/// <param name="BatchStart">[0x04..0x06] Первый batch в таблице 0x0D.</param>
|
||||
/// <param name="BatchCount">[0x06..0x08] Количество batch в таблице 0x0D</param>
|
||||
/// <param name="LocalMinimum">[0x08..0x14] Минимум локального AABB</param>
|
||||
/// <param name="LocalMaximum">[0x14..0x20] Максимум локального AABB</param>
|
||||
/// <param name="Center">[0x20..0x2C] Центр bounding sphere</param>
|
||||
/// <param name="SphereRadius">[0x2C..0x30] Радиус bounding sphere</param>
|
||||
/// <param name="Opaque">[0x30..0x44] Пять opaque dword</param>
|
||||
public record class Slot(
|
||||
ushort TriStart,
|
||||
ushort TriCount,
|
||||
ushort BatchStart,
|
||||
ushort BatchCount,
|
||||
Vector3 LocalMinimum,
|
||||
Vector3 LocalMaximum,
|
||||
Vector3 Center,
|
||||
float SphereRadius,
|
||||
uint[] Opaque);
|
||||
|
||||
/// <summary>Bounding box заголовка: 8 точек по 3 float (length = 0x60).</summary>
|
||||
/// <param name="BottomFrontLeft">[0x00..0x0C] Нижняя передняя левая точка.</param>
|
||||
/// <param name="BottomFrontRight">[0x0C..0x18] Нижняя передняя правая точка.</param>
|
||||
/// <param name="BottomBackRight">[0x18..0x24] Нижняя задняя правая точка.</param>
|
||||
/// <param name="BottomBackLeft">[0x24..0x30] Нижняя задняя левая точка.</param>
|
||||
/// <param name="TopFrontLeft">[0x30..0x3C] Верхняя передняя левая точка.</param>
|
||||
/// <param name="TopFrontRight">[0x3C..0x48] Верхняя передняя правая точка.</param>
|
||||
/// <param name="TopBackRight">[0x48..0x54] Верхняя задняя правая точка.</param>
|
||||
/// <param name="TopBackLeft">[0x54..0x60] Верхняя задняя левая точка.</param>
|
||||
public record class BoundingBox(
|
||||
Vector3 BottomFrontLeft,
|
||||
Vector3 BottomFrontRight,
|
||||
Vector3 BottomBackRight,
|
||||
Vector3 BottomBackLeft,
|
||||
Vector3 TopFrontLeft,
|
||||
Vector3 TopFrontRight,
|
||||
Vector3 TopBackRight,
|
||||
Vector3 TopBackLeft);
|
||||
}
|
||||
@@ -4,7 +4,10 @@ using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
public class Msh03
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x03: позиции вершин
|
||||
/// </summary>
|
||||
public class Msh0x03
|
||||
{
|
||||
public static List<Vector3> ReadComponent(FileStream mshFs, NResArchive mshNres)
|
||||
{
|
||||
@@ -20,7 +23,12 @@ public class Msh03
|
||||
throw new Exception("Vertices file (03) element size is not 12");
|
||||
}
|
||||
|
||||
var verticesFile = new byte[verticesFileEntry.ElementCount * verticesFileEntry.ElementSize];
|
||||
if (verticesFileEntry.FileLength % verticesFileEntry.ElementSize != 0)
|
||||
{
|
||||
throw new Exception("Positions component (0x03) payload size is not divisible by element size");
|
||||
}
|
||||
|
||||
var verticesFile = new byte[verticesFileEntry.FileLength];
|
||||
mshFs.Seek(verticesFileEntry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(verticesFile, 0, verticesFile.Length);
|
||||
|
||||
@@ -32,4 +40,4 @@ public class Msh03
|
||||
).ToList();
|
||||
return vertices;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x04: упакованные нормали вершин. clamp(component / 127.0, -1..1).
|
||||
/// </summary>
|
||||
public static class Msh0x04
|
||||
{
|
||||
public static List<Msh04Normal> ReadComponent(FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "04 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain file (04)");
|
||||
}
|
||||
|
||||
if (entry.ElementSize != 4)
|
||||
{
|
||||
throw new Exception("Packed normals file (04) element size is not 4");
|
||||
}
|
||||
|
||||
if (entry.FileLength % entry.ElementSize != 0)
|
||||
{
|
||||
throw new Exception("Packed normals component (0x04) payload size is not divisible by element size");
|
||||
}
|
||||
|
||||
var data = new byte[entry.FileLength];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elements = new List<Msh04Normal>(entry.FileLength / entry.ElementSize);
|
||||
for (var i = 0; i < entry.FileLength / entry.ElementSize; i++)
|
||||
{
|
||||
var offset = i * 4;
|
||||
elements.Add(new Msh04Normal(
|
||||
unchecked((sbyte)data[offset + 0]),
|
||||
unchecked((sbyte)data[offset + 1]),
|
||||
unchecked((sbyte)data[offset + 2]),
|
||||
unchecked((sbyte)data[offset + 3])));
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Упакованная нормаль: четыре int8-компоненты (length = 4).</summary>
|
||||
/// <param name="X">[0x00..0x01] X-компонента.</param>
|
||||
/// <param name="Y">[0x01..0x02] Y-компонента.</param>
|
||||
/// <param name="Z">[0x02..0x03] Z-компонента.</param>
|
||||
/// <param name="W">[0x03..0x04] W-компонента.</param>
|
||||
public readonly record struct Msh04Normal(sbyte X, sbyte Y, sbyte Z, sbyte W)
|
||||
;
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x05: упакованные UV0. component / 1024.0.
|
||||
/// </summary>
|
||||
public static class Msh0x05
|
||||
{
|
||||
public static List<Msh05Uv> ReadComponent(FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "05 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain file (05)");
|
||||
}
|
||||
|
||||
if (entry.ElementSize != 4)
|
||||
{
|
||||
throw new Exception("Packed UV file (05) element size is not 4");
|
||||
}
|
||||
|
||||
if (entry.FileLength % entry.ElementSize != 0)
|
||||
{
|
||||
throw new Exception("Packed UV component (0x05) payload size is not divisible by element size");
|
||||
}
|
||||
|
||||
var data = new byte[entry.FileLength];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elements = new List<Msh05Uv>(entry.FileLength / entry.ElementSize);
|
||||
for (var i = 0; i < entry.FileLength / entry.ElementSize; i++)
|
||||
{
|
||||
var span = data.AsSpan(i * 4, 4);
|
||||
elements.Add(new Msh05Uv(
|
||||
BinaryPrimitives.ReadInt16LittleEndian(span[0..2]),
|
||||
BinaryPrimitives.ReadInt16LittleEndian(span[2..4])));
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Упакованные UV0: две int16-компоненты (length = 4).</summary>
|
||||
/// <param name="U">[0x00..0x02] U-компонента, uv = U / 1024.0.</param>
|
||||
/// <param name="V">[0x02..0x04] V-компонента, uv = V / 1024.0.</param>
|
||||
public readonly record struct Msh05Uv(short U, short V)
|
||||
;
|
||||
@@ -3,7 +3,10 @@ using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
public static class Msh06
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x06: индексный буфер
|
||||
/// </summary>
|
||||
public static class Msh0x06
|
||||
{
|
||||
public static List<ushort> ReadComponent(
|
||||
FileStream mshFs, NResArchive archive)
|
||||
@@ -15,12 +18,22 @@ public static class Msh06
|
||||
throw new Exception("Archive doesn't contain file (06)");
|
||||
}
|
||||
|
||||
var data = new byte[entry.ElementCount * entry.ElementSize];
|
||||
if (entry.ElementSize != 2)
|
||||
{
|
||||
throw new Exception("Index buffer component (0x06) element size is not 2");
|
||||
}
|
||||
|
||||
if (entry.FileLength % entry.ElementSize != 0)
|
||||
{
|
||||
throw new Exception("Index buffer component (0x06) payload size is not divisible by element size");
|
||||
}
|
||||
|
||||
var data = new byte[entry.FileLength];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elements = new List<ushort>((int)entry.ElementCount);
|
||||
for (var i = 0; i < entry.ElementCount; i++)
|
||||
var elements = new List<ushort>(entry.FileLength / entry.ElementSize);
|
||||
for (var i = 0; i < entry.FileLength / entry.ElementSize; i++)
|
||||
{
|
||||
elements.Add(
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(i * 2))
|
||||
@@ -29,4 +42,4 @@ public static class Msh06
|
||||
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x07: описатели треугольников для коллизии/пикинга
|
||||
/// </summary>
|
||||
public static class Msh0x07
|
||||
{
|
||||
public static List<TriangleDescriptor> ReadComponent(
|
||||
FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "07 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain file (07)");
|
||||
}
|
||||
|
||||
if (entry.ElementSize != 16)
|
||||
{
|
||||
throw new Exception("Triangle descriptor component (0x07) element size is not 16");
|
||||
}
|
||||
|
||||
if (entry.FileLength % entry.ElementSize != 0)
|
||||
{
|
||||
throw new Exception("Triangle descriptor component (0x07) payload size is not divisible by element size");
|
||||
}
|
||||
|
||||
var data = new byte[entry.FileLength];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elementBytes = data.Chunk(16);
|
||||
|
||||
var elements = elementBytes.Select(x => new TriangleDescriptor(
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
|
||||
BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(8)),
|
||||
BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(10)),
|
||||
BinaryPrimitives.ReadInt16LittleEndian(x.AsSpan(12)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(14)))).ToList();
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/// <summary>Описатель треугольника 0x07 (length = 0x10).</summary>
|
||||
/// <param name="TriFlags">[0x00..0x02] Флаги треугольника</param>
|
||||
/// <param name="Link0">[0x02..0x04] Связь/opaque поле 0</param>
|
||||
/// <param name="Link1">[0x04..0x06] Связь/opaque поле 1</param>
|
||||
/// <param name="Link2">[0x06..0x08] Связь/opaque поле 2</param>
|
||||
/// <param name="NormalX">[0x08..0x0A] Упакованная X-компонента нормали</param>
|
||||
/// <param name="NormalY">[0x0A..0x0C] Упакованная Y-компонента нормали</param>
|
||||
/// <param name="NormalZ">[0x0C..0x0E] Упакованная Z-компонента нормали</param>
|
||||
/// <param name="SelectorPacked">[0x0E..0x10] Три 2-битных селектора; значение 3 трактуется как 0xFFFF</param>
|
||||
public readonly record struct TriangleDescriptor(
|
||||
ushort TriFlags,
|
||||
ushort Link0,
|
||||
ushort Link1,
|
||||
ushort Link2,
|
||||
short NormalX,
|
||||
short NormalY,
|
||||
short NormalZ,
|
||||
ushort SelectorPacked)
|
||||
{
|
||||
public ushort GetSelector(int index)
|
||||
{
|
||||
if (index is < 0 or > 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
|
||||
var selector = (SelectorPacked >> (index * 2)) & 0b11;
|
||||
return selector == 3 ? ushort.MaxValue : (ushort)selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,11 @@ using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
public class Msh0A
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x0A: строки узлов.
|
||||
/// У FParkan: Res10 / Node strings. Старое локальное имя: ExternalRefs.
|
||||
/// </summary>
|
||||
public class Msh0x0A
|
||||
{
|
||||
public static List<string> ReadComponent(FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
@@ -23,19 +27,32 @@ public class Msh0A
|
||||
var strings = new List<string>();
|
||||
while (pos < data.Length)
|
||||
{
|
||||
if (pos + 4 > data.Length)
|
||||
{
|
||||
throw new Exception("Node strings component (0x0A) has truncated length prefix");
|
||||
}
|
||||
|
||||
var len = BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(pos));
|
||||
if (len < 0 || pos + 4 + len > data.Length)
|
||||
{
|
||||
throw new Exception("Node strings component (0x0A) has invalid string length");
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
pos += 4; // empty entry, no string attached
|
||||
strings.Add(""); // add empty string
|
||||
pos += 4;
|
||||
strings.Add("");
|
||||
}
|
||||
else
|
||||
{
|
||||
// len is not 0, we need to read it
|
||||
var strBytes = data.AsSpan(pos + 4, len);
|
||||
var str = Encoding.UTF8.GetString(strBytes);
|
||||
var str = Encoding.ASCII.GetString(strBytes);
|
||||
strings.Add(str);
|
||||
pos += len + 4 + 1; // skip length prefix and string itself, +1, because it's null-terminated
|
||||
pos += len + 4;
|
||||
if (pos < data.Length && data[pos] == 0)
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,4 +63,4 @@ public class Msh0A
|
||||
|
||||
return strings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x0D: таблица batch.
|
||||
/// </summary>
|
||||
public static class Msh0x0D
|
||||
{
|
||||
public const int ElementSize = 20;
|
||||
|
||||
public static List<Batch> ReadComponent(
|
||||
FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "0D 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain file (0D)");
|
||||
}
|
||||
|
||||
if (entry.ElementSize != ElementSize)
|
||||
{
|
||||
throw new Exception("Batch table component (0x0D) element size is not 20");
|
||||
}
|
||||
|
||||
if (entry.FileLength % entry.ElementSize != 0)
|
||||
{
|
||||
throw new Exception("Batch table component (0x0D) payload size is not divisible by element size");
|
||||
}
|
||||
|
||||
var data = new byte[entry.FileLength];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elementBytes = data.Chunk(ElementSize);
|
||||
|
||||
var elements = elementBytes.Select(x => new Batch(
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0xA)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0xE)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0x10)))).ToList();
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/// <summary>Batch 0x0D</summary>
|
||||
/// <param name="BatchFlags">[0x00..0x02] Флаги batch. У FParkan: batchFlags.</param>
|
||||
/// <param name="MaterialIndex">[0x02..0x04] Индекс material slot, резолвится через WEAR/MAT0 pipeline.</param>
|
||||
/// <param name="Opaque4">[0x04..0x06] Opaque поле. Старое локальное имя: TriangleCount; не подтверждено.</param>
|
||||
/// <param name="Opaque6">[0x06..0x08] Opaque поле. Сохранять побайтно в writer.</param>
|
||||
/// <param name="IndexCount">[0x08..0x0A] Количество индексов в индексном буфере 0x06.</param>
|
||||
/// <param name="IndexStart">[0x0A..0x0E] Первый индекс в индексном буфере 0x06.</param>
|
||||
/// <param name="Opaque14">[0x0E..0x10] Opaque поле. Старое локальное имя: CountOf03; не подтверждено.</param>
|
||||
/// <param name="BaseVertex">[0x10..0x14] Базовая вершина в position stream 0x03. У FParkan: baseVertex.</param>
|
||||
public readonly record struct Batch(
|
||||
ushort BatchFlags,
|
||||
ushort MaterialIndex,
|
||||
ushort Opaque4,
|
||||
ushort Opaque6,
|
||||
ushort IndexCount,
|
||||
uint IndexStart,
|
||||
ushort Opaque14,
|
||||
uint BaseVertex);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
/// <summary>
|
||||
/// MSH-компонент 0x15: terrain-таблица треугольников
|
||||
/// </summary>
|
||||
public static class Msh0x15
|
||||
{
|
||||
public static List<TerrainTriangle> ReadComponent(
|
||||
FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "15 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain file (15)");
|
||||
}
|
||||
|
||||
if (entry.ElementSize != 28)
|
||||
{
|
||||
throw new Exception("Terrain triangle component (0x15) element size is not 28");
|
||||
}
|
||||
|
||||
if (entry.FileLength % entry.ElementSize != 0)
|
||||
{
|
||||
throw new Exception("Terrain triangle component (0x15) payload size is not divisible by element size");
|
||||
}
|
||||
|
||||
var data = new byte[entry.FileLength];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elementBytes = data.Chunk(28);
|
||||
|
||||
var elements = elementBytes.Select(x => new TerrainTriangle(
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(4)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(10)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(12)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(14)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(18)),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(22)),
|
||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(26)))).ToList();
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/// <summary>Terrain-треугольник 0x15 (length = 0x1C)</summary>
|
||||
/// <param name="Flags">[0x00..0x04] Флаги треугольника для terrain path</param>
|
||||
/// <param name="MaterialData">[0x04..0x08] Данные материала terrain</param>
|
||||
/// <param name="Vertex1Index">[0x08..0x0A] Индекс первой вершины в position stream Msh0x03</param>
|
||||
/// <param name="Vertex2Index">[0x0A..0x0C] Индекс второй вершины в position stream Msh0x03</param>
|
||||
/// <param name="Vertex3Index">[0x0C..0x0E] Индекс третьей вершины в position stream Msh0x03</param>
|
||||
/// <param name="Opaque0E">[0x0E..0x12] Opaque поле</param>
|
||||
/// <param name="Opaque12">[0x12..0x16] Opaque поле</param>
|
||||
/// <param name="Opaque16">[0x16..0x1A] Opaque поле</param>
|
||||
/// <param name="Opaque1A">[0x1A..0x1C] Opaque поле</param>
|
||||
public readonly record struct TerrainTriangle(
|
||||
uint Flags,
|
||||
|
||||
uint MaterialData,
|
||||
ushort Vertex1Index,
|
||||
ushort Vertex2Index,
|
||||
ushort Vertex3Index,
|
||||
|
||||
uint Opaque0E,
|
||||
uint Opaque12,
|
||||
uint Opaque16,
|
||||
ushort Opaque1A);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
using System.Buffers.Binary;
|
||||
using NResLib;
|
||||
|
||||
namespace ParkanPlayground;
|
||||
|
||||
public static class Msh15
|
||||
{
|
||||
public static List<Msh15Element> ReadComponent(
|
||||
FileStream mshFs, NResArchive archive)
|
||||
{
|
||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "15 00 00 00");
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
throw new Exception("Archive doesn't contain file (15)");
|
||||
}
|
||||
|
||||
var data = new byte[entry.ElementCount * entry.ElementSize];
|
||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||
mshFs.ReadExactly(data, 0, data.Length);
|
||||
|
||||
var elementBytes = data.Chunk(28);
|
||||
|
||||
var elements = elementBytes.Select(x => new Msh15Element()
|
||||
{
|
||||
Flags = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0)),
|
||||
Magic04 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(4)),
|
||||
Vertex1Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
|
||||
Vertex2Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(10)),
|
||||
Vertex3Index = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(12)),
|
||||
Magic0E = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(14)),
|
||||
Magic12 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(18)),
|
||||
Magic16 = BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(22)),
|
||||
Magic1A = BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(26)),
|
||||
|
||||
}).ToList();
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
public class Msh15Element
|
||||
{
|
||||
public uint Flags { get; set; }
|
||||
|
||||
public uint Magic04 { get; set; }
|
||||
public ushort Vertex1Index { get; set; }
|
||||
public ushort Vertex2Index { get; set; }
|
||||
public ushort Vertex3Index { get; set; }
|
||||
|
||||
public uint Magic0E { get; set; }
|
||||
public uint Magic12 { get; set; }
|
||||
public uint Magic16 { get; set; }
|
||||
public ushort Magic1A { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,7 @@ public enum MshType
|
||||
|
||||
public class MshConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Detects mesh type based on which components are present in the archive.
|
||||
/// </summary>
|
||||
/// <summary>Определяет тип MSH по набору hex-компонентов архива.</summary>
|
||||
public static MshType DetectMeshType(NResArchive archive)
|
||||
{
|
||||
bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00");
|
||||
@@ -33,13 +31,12 @@ public class MshConverter
|
||||
return MshType.Unknown;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a .msh file to OBJ format, auto-detecting mesh type.
|
||||
/// </summary>
|
||||
/// <param name="mshPath">Path to the .msh file</param>
|
||||
/// <param name="outputPath">Output OBJ path (optional, defaults to input name + .obj)</param>
|
||||
/// <param name="lodLevel">LOD level to export (0 = highest detail)</param>
|
||||
public void Convert(string mshPath, string? outputPath = null, int lodLevel = 0)
|
||||
/// <summary>Конвертирует .msh в OBJ с автоопределением типа меша.</summary>
|
||||
/// <param name="mshPath">Путь к .msh файлу.</param>
|
||||
/// <param name="outputPath">Путь к OBJ, по умолчанию рядом с исходным файлом.</param>
|
||||
/// <param name="lodLevel">LOD для экспорта.</param>
|
||||
/// <param name="group">Группа slot внутри LOD. slotIndex[lod * 5 + group].</param>
|
||||
public void Convert(string mshPath, string? outputPath = null, int lodLevel = 0, int group = 0)
|
||||
{
|
||||
var mshNresResult = NResParser.ReadFile(mshPath);
|
||||
if (mshNresResult.Archive is null)
|
||||
@@ -61,10 +58,10 @@ public class MshConverter
|
||||
switch (meshType)
|
||||
{
|
||||
case MshType.Model:
|
||||
ConvertModel(fs, archive, outputPath, lodLevel);
|
||||
ConvertModel(fs, archive, outputPath, lodLevel, group);
|
||||
break;
|
||||
case MshType.Landscape:
|
||||
ConvertLandscape(fs, archive, outputPath, lodLevel);
|
||||
ConvertLandscape(fs, archive, outputPath, lodLevel, group);
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine("ERROR: Unknown mesh type, cannot convert.");
|
||||
@@ -73,17 +70,17 @@ public class MshConverter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a model mesh (robots, buildings, etc.) to OBJ.
|
||||
/// Uses indexed triangles: 01 → 02 → 0D → 06 → 03
|
||||
/// Конвертирует обычную модель в OBJ.
|
||||
/// Путь данных: 0x01 -> 0x02 -> 0x0D -> 0x06 -> 0x03.
|
||||
/// </summary>
|
||||
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
|
||||
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel, int group)
|
||||
{
|
||||
var component01 = Msh01.ReadComponent(fs, archive);
|
||||
var component02 = Msh02.ReadComponent(fs, archive);
|
||||
var component03 = Msh03.ReadComponent(fs, archive);
|
||||
var component06 = Msh06.ReadComponent(fs, archive);
|
||||
var component07 = Msh07.ReadComponent(fs, archive);
|
||||
var component0D = Msh0D.ReadComponent(fs, archive);
|
||||
var component01 = Msh0x01.ReadComponent(fs, archive);
|
||||
var component02 = Msh0x02.ReadComponent(fs, archive);
|
||||
var component03 = Msh0x03.ReadComponent(fs, archive);
|
||||
var component06 = Msh0x06.ReadComponent(fs, archive);
|
||||
var component07 = Msh0x07.ReadComponent(fs, archive);
|
||||
var component0D = Msh0x0D.ReadComponent(fs, archive);
|
||||
|
||||
Console.WriteLine($"Vertices: {component03.Count}");
|
||||
Console.WriteLine($"Pieces: {component01.Elements.Count}");
|
||||
@@ -93,9 +90,10 @@ public class MshConverter
|
||||
sw.WriteLine($"# Model mesh converted from {Path.GetFileName(outputPath)}");
|
||||
sw.WriteLine($"# LOD level: {lodLevel}");
|
||||
|
||||
// Write all vertices
|
||||
foreach (var v in component03)
|
||||
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
|
||||
{
|
||||
sw.WriteLine(FormattableString.Invariant($"v {v.X:F6} {v.Y:F6} {v.Z:F6}"));
|
||||
}
|
||||
|
||||
int exportedFaces = 0;
|
||||
|
||||
@@ -103,32 +101,49 @@ public class MshConverter
|
||||
{
|
||||
var piece = component01.Elements[pieceIndex];
|
||||
|
||||
// Get submesh index for requested LOD
|
||||
if (lodLevel >= piece.Lod.Length)
|
||||
continue;
|
||||
|
||||
var submeshIdx = piece.Lod[lodLevel];
|
||||
var submeshIdx = piece.ResolveSlotIndex(lodLevel, group);
|
||||
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
|
||||
continue;
|
||||
|
||||
sw.WriteLine($"g piece_{pieceIndex}");
|
||||
|
||||
var submesh = component02.Elements[submeshIdx];
|
||||
var batchStart = submesh.StartOffsetIn0d;
|
||||
var batchCount = submesh.ByteLengthIn0D;
|
||||
var batchStart = submesh.BatchStart;
|
||||
var batchCount = submesh.BatchCount;
|
||||
if (batchStart + batchCount > component0D.Count)
|
||||
{
|
||||
Console.WriteLine($"WARNING: Batch range {batchStart}:{batchCount} out of range for piece {pieceIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var batchIdx = 0; batchIdx < batchCount; batchIdx++)
|
||||
{
|
||||
var batch = component0D[batchStart + batchIdx];
|
||||
var baseVertex = batch.IndexInto03;
|
||||
var indexStart = batch.IndexInto06;
|
||||
var indexCount = batch.CountOf06;
|
||||
var baseVertex = (int)batch.BaseVertex;
|
||||
var indexStart = (int)batch.IndexStart;
|
||||
var indexCount = batch.IndexCount;
|
||||
if (indexStart + indexCount > component06.Count)
|
||||
{
|
||||
Console.WriteLine($"WARNING: Index range {indexStart}:{indexCount} out of range for piece {pieceIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < indexCount; i += 3)
|
||||
{
|
||||
if (i + 2 >= indexCount)
|
||||
{
|
||||
Console.WriteLine($"WARNING: Batch has non-triangle index tail in piece {pieceIndex}");
|
||||
break;
|
||||
}
|
||||
|
||||
var i1 = baseVertex + component06[indexStart + i];
|
||||
var i2 = baseVertex + component06[indexStart + i + 1];
|
||||
var i3 = baseVertex + component06[indexStart + i + 2];
|
||||
if (i1 >= component03.Count || i2 >= component03.Count || i3 >= component03.Count)
|
||||
{
|
||||
Console.WriteLine($"WARNING: Vertex index out of range in piece {pieceIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
|
||||
exportedFaces++;
|
||||
@@ -141,15 +156,15 @@ public class MshConverter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a landscape mesh (terrain) to OBJ.
|
||||
/// Uses direct triangles: 01 → 02 → 15 (via StartIndexIn07/CountIn07)
|
||||
/// Конвертирует terrain mesh в OBJ.
|
||||
/// Путь данных является terrain-гипотезой проекта: 0x01 -> 0x02 -> 0x15.
|
||||
/// </summary>
|
||||
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
|
||||
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel, int group)
|
||||
{
|
||||
var component01 = Msh01.ReadComponent(fs, archive);
|
||||
var component02 = Msh02.ReadComponent(fs, archive);
|
||||
var component03 = Msh03.ReadComponent(fs, archive);
|
||||
var component15 = Msh15.ReadComponent(fs, archive);
|
||||
var component01 = Msh0x01.ReadComponent(fs, archive);
|
||||
var component02 = Msh0x02.ReadComponent(fs, archive);
|
||||
var component03 = Msh0x03.ReadComponent(fs, archive);
|
||||
var component15 = Msh0x15.ReadComponent(fs, archive);
|
||||
|
||||
Console.WriteLine($"Vertices: {component03.Count}");
|
||||
Console.WriteLine($"Triangles: {component15.Count}");
|
||||
@@ -161,9 +176,10 @@ public class MshConverter
|
||||
sw.WriteLine($"# LOD level: {lodLevel}");
|
||||
sw.WriteLine($"# Tile grid: {(int)Math.Sqrt(component01.Elements.Count)}x{(int)Math.Sqrt(component01.Elements.Count)}");
|
||||
|
||||
// Write all vertices
|
||||
foreach (var v in component03)
|
||||
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
|
||||
{
|
||||
sw.WriteLine(FormattableString.Invariant($"v {v.X:F6} {v.Y:F6} {v.Z:F6}"));
|
||||
}
|
||||
|
||||
int exportedFaces = 0;
|
||||
|
||||
@@ -171,11 +187,7 @@ public class MshConverter
|
||||
{
|
||||
var tile = component01.Elements[tileIdx];
|
||||
|
||||
// Get submesh index for requested LOD
|
||||
if (lodLevel >= tile.Lod.Length)
|
||||
continue;
|
||||
|
||||
var submeshIdx = tile.Lod[lodLevel];
|
||||
var submeshIdx = tile.ResolveSlotIndex(lodLevel, group);
|
||||
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
|
||||
continue;
|
||||
|
||||
@@ -183,9 +195,8 @@ public class MshConverter
|
||||
|
||||
var submesh = component02.Elements[submeshIdx];
|
||||
|
||||
// For landscape, StartIndexIn07 = triangle start index, CountIn07 = triangle count
|
||||
var triangleStart = submesh.StartIndexIn07;
|
||||
var triangleCount = submesh.CountIn07;
|
||||
var triangleStart = submesh.TriStart;
|
||||
var triangleCount = submesh.TriCount;
|
||||
|
||||
for (var triOffset = 0; triOffset < triangleCount; triOffset++)
|
||||
{
|
||||
@@ -197,6 +208,12 @@ public class MshConverter
|
||||
}
|
||||
|
||||
var tri = component15[triIdx];
|
||||
if (tri.Vertex1Index >= component03.Count || tri.Vertex2Index >= component03.Count || tri.Vertex3Index >= component03.Count)
|
||||
{
|
||||
Console.WriteLine($"WARNING: Vertex index out of range for tile {tileIdx}");
|
||||
continue;
|
||||
}
|
||||
|
||||
sw.WriteLine($"f {tri.Vertex1Index + 1} {tri.Vertex2Index + 1} {tri.Vertex3Index + 1}");
|
||||
exportedFaces++;
|
||||
}
|
||||
@@ -215,21 +232,21 @@ public class MshConverter
|
||||
|
||||
using (StreamWriter writer = new StreamWriter(filePath))
|
||||
{
|
||||
// Write vertices
|
||||
// Запись вершин.
|
||||
foreach (var p in points)
|
||||
{
|
||||
writer.WriteLine($"v {p.X} {p.Y} {p.Z}");
|
||||
}
|
||||
|
||||
// Write faces (each face defined by 4 vertices, using 1-based indices)
|
||||
// Запись граней: каждая грань задается четырьмя вершинами, OBJ использует индексацию с 1.
|
||||
int[][] faces = new int[][]
|
||||
{
|
||||
new int[] { 1, 2, 3, 4 }, // bottom
|
||||
new int[] { 5, 6, 7, 8 }, // top
|
||||
new int[] { 1, 2, 6, 5 }, // front
|
||||
new int[] { 2, 3, 7, 6 }, // right
|
||||
new int[] { 3, 4, 8, 7 }, // back
|
||||
new int[] { 4, 1, 5, 8 } // left
|
||||
new int[] { 1, 2, 3, 4 }, // низ
|
||||
new int[] { 5, 6, 7, 8 }, // верх
|
||||
new int[] { 1, 2, 6, 5 }, // перед
|
||||
new int[] { 2, 3, 7, 6 }, // право
|
||||
new int[] { 3, 4, 8, 7 }, // зад
|
||||
new int[] { 4, 1, 5, 8 } // лево
|
||||
};
|
||||
|
||||
foreach (var f in faces)
|
||||
@@ -248,7 +265,7 @@ public class MshConverter
|
||||
|
||||
foreach (var c in centers)
|
||||
{
|
||||
// Generate 8 vertices for this cube
|
||||
// Генерация восьми вершин куба.
|
||||
Vector3[] vertices = new Vector3[]
|
||||
{
|
||||
new Vector3(c.X - half, c.Y - half, c.Z - half),
|
||||
@@ -262,24 +279,24 @@ public class MshConverter
|
||||
new Vector3(c.X - half, c.Y + half, c.Z + half)
|
||||
};
|
||||
|
||||
// Write vertices
|
||||
// Запись вершин.
|
||||
foreach (var v in vertices)
|
||||
{
|
||||
writer.WriteLine($"v {v.X} {v.Y} {v.Z}");
|
||||
}
|
||||
|
||||
// Define faces (1-based indices, counter-clockwise)
|
||||
// Описание граней: индексация с 1, порядок против часовой стрелки.
|
||||
int[][] faces = new int[][]
|
||||
{
|
||||
new int[] { 1, 2, 3, 4 }, // bottom
|
||||
new int[] { 5, 6, 7, 8 }, // top
|
||||
new int[] { 1, 2, 6, 5 }, // front
|
||||
new int[] { 2, 3, 7, 6 }, // right
|
||||
new int[] { 3, 4, 8, 7 }, // back
|
||||
new int[] { 4, 1, 5, 8 } // left
|
||||
new int[] { 1, 2, 3, 4 }, // низ
|
||||
new int[] { 5, 6, 7, 8 }, // верх
|
||||
new int[] { 1, 2, 6, 5 }, // перед
|
||||
new int[] { 2, 3, 7, 6 }, // право
|
||||
new int[] { 3, 4, 8, 7 }, // зад
|
||||
new int[] { 4, 1, 5, 8 } // лево
|
||||
};
|
||||
|
||||
// Write faces with offset
|
||||
// Запись граней со смещением индексов.
|
||||
foreach (var f in faces)
|
||||
{
|
||||
writer.WriteLine(
|
||||
@@ -297,18 +314,18 @@ public class MshConverter
|
||||
{
|
||||
writer.WriteLine("# Exported OBJ file");
|
||||
|
||||
// Write vertices
|
||||
// Запись вершин.
|
||||
foreach (var v in vertices)
|
||||
{
|
||||
writer.WriteLine($"v {v.X:F2} {v.Y:F2} {v.Z:F2}");
|
||||
}
|
||||
|
||||
// Write edges as lines ("l" elements in .obj format)
|
||||
// Запись ребер как line-элементов OBJ.
|
||||
foreach (var e in edges)
|
||||
{
|
||||
// OBJ uses 1-based indexing
|
||||
// OBJ использует индексацию с 1.
|
||||
writer.WriteLine($"l {e.Index1 + 1} {e.Index2 + 1}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user