fpopov thanks

This commit is contained in:
bird_egop
2026-05-09 20:36:43 +03:00
parent d73a6dde70
commit eb458ad9a8
43 changed files with 1562 additions and 1233 deletions
+9 -4
View File
@@ -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();
+36 -15
View File
@@ -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
+61 -64
View File
@@ -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)
-77
View File
@@ -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];
}
}
-193
View File
@@ -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; }
}
}
-53
View File
@@ -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; }
}
}
-53
View File
@@ -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; }
}
}
+89
View File
@@ -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;
}
}
}
+203
View File
@@ -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;
}
}
}
+54
View File
@@ -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)
;
+51
View File
@@ -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;
}
}
}
+80
View File
@@ -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;
}
}
}
+70
View File
@@ -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);
}
+73
View File
@@ -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);
}
-55
View File
@@ -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; }
}
}
+89 -72
View File
@@ -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}");
}
}
}
}
}