mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2025-12-11 04:51:21 +04:00
feat: Add landscape mesh support to MshConverter
- Auto-detect Model vs Landscape mesh types - Model: uses indexed triangles (06, 0D, 07) - Landscape: uses direct triangles (0B, 15) - Add MSH_FORMAT.md documentation (Russian) - Add Ghidra decompiled code for CLandscape
This commit is contained in:
267
ParkanPlayground/MSH_FORMAT.md
Normal file
267
ParkanPlayground/MSH_FORMAT.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Документация формата MSH
|
||||||
|
|
||||||
|
Формат `.msh` используется игрой Parkan: Железная стратегия (1998) для хранения 3D-мешей.
|
||||||
|
MSH файлы — это NRes архивы, содержащие несколько типизированных компонентов.
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Существует **два варианта** формата MSH:
|
||||||
|
|
||||||
|
| Вариант | Применение | Ключевые компоненты | Хранение треугольников |
|
||||||
|
|---------|------------|---------------------|------------------------|
|
||||||
|
| **Модель** | Роботы, здания, объекты | 06, 0D, 07 | Индексированные треугольники |
|
||||||
|
| **Ландшафт** | Террейн | 0B, 15 | Прямые треугольники |
|
||||||
|
|
||||||
|
### Автоопределение типа
|
||||||
|
|
||||||
|
```
|
||||||
|
Модель: Есть компонент 06 (индексы) И 0D (батчи)
|
||||||
|
Ландшафт: Есть компонент 0B (материалы) И НЕТ компонента 06
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сводка компонентов
|
||||||
|
|
||||||
|
| Тип | Название | Размер элемента | Описание |
|
||||||
|
|:---:|----------|:---------------:|----------|
|
||||||
|
| 01 | Pieces | 38 (0x26) | Части меша / тайлы с LOD-ссылками |
|
||||||
|
| 02 | Submeshes | 68 (0x44) | Сабмеши с баундинг-боксами |
|
||||||
|
| 03 | Vertices | 12 (0x0C) | Позиции вершин (Vector3) |
|
||||||
|
| 04 | VertexData1 | 4 | Данные на вершину (неизвестно) |
|
||||||
|
| 05 | VertexData2 | 4 | Данные на вершину (неизвестно) |
|
||||||
|
| 06 | Indices | 2 | Индексы вершин треугольников (только Модель) |
|
||||||
|
| 07 | TriangleData | 16 | Данные рендера на треугольник (только Модель) |
|
||||||
|
| 08 | Animations | 4 | Кейфреймы анимации меша |
|
||||||
|
| 0A | ExternalRefs | переменный | Внешние ссылки на меши (строки) |
|
||||||
|
| 0B | MaterialData | 4 | Материалы на треугольник (только Ландшафт) |
|
||||||
|
| 0D | DrawBatches | 20 (0x14) | Батчи отрисовки (только Модель) |
|
||||||
|
| 0E | VertexData3 | 4 | Данные на вершину (только Ландшафт) |
|
||||||
|
| 12 | MicrotextureMap | 4 | Микротекстурный маппинг на вершину |
|
||||||
|
| 13 | ShortAnims | 2 | Короткие индексы анимаций |
|
||||||
|
| 15 | Triangles | 28 (0x1C) | Прямые определения треугольников |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поток данных
|
||||||
|
|
||||||
|
### Модель (роботы, здания)
|
||||||
|
|
||||||
|
```
|
||||||
|
Компонент 01 (Pieces - части)
|
||||||
|
│
|
||||||
|
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
|
||||||
|
│
|
||||||
|
├─► StartIndexIn07 ──► Компонент 07 (данные на треугольник)
|
||||||
|
│
|
||||||
|
└─► StartOffsetIn0d:ByteLengthIn0D ──► Компонент 0D (батчи)
|
||||||
|
│
|
||||||
|
├─► IndexInto06:CountOf06 ──► Компонент 06 (индексы)
|
||||||
|
│ │
|
||||||
|
│ └─► Компонент 03 (вершины)
|
||||||
|
│
|
||||||
|
└─► IndexInto03 (базовое смещение вершины)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ландшафт (террейн)
|
||||||
|
|
||||||
|
```
|
||||||
|
Компонент 01 (Тайлы, обычно 16×16 = 256)
|
||||||
|
│
|
||||||
|
└─► Lod[n] ──► Компонент 02 (индекс сабмеша)
|
||||||
|
│
|
||||||
|
└─► StartIndexIn07:CountIn07 ──► Компонент 15 (треугольники)
|
||||||
|
│
|
||||||
|
└─► Vertex1/2/3Index ──► Компонент 03 (вершины)
|
||||||
|
|
||||||
|
└─► StartIndexIn07:CountIn07 ──► Компонент 0B (материалы, параллельно 15)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Важно:** В ландшафтных мешах поля `StartIndexIn07` и `CountIn07` в Компоненте 02
|
||||||
|
используются для индексации в Компонент 15 (треугольники), а не в Компонент 07.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Структуры компонентов
|
||||||
|
|
||||||
|
### Компонент 01 - Pieces (0x26 = 38 байт)
|
||||||
|
|
||||||
|
Определяет части меша (для моделей) или тайлы террейна (для ландшафтов).
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Поле | Описание |
|
||||||
|
|:--------:|:------:|:---:|------|----------|
|
||||||
|
| 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 = не используется) |
|
||||||
|
|
||||||
|
**Ландшафт:** 256 тайлов в сетке 16×16. Каждый тайл имеет 2 LOD (индексы 0-255 и 256-511).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Компонент 02 - Submeshes (Заголовок: 0x8C = 140 байт, Элемент: 0x44 = 68 байт)
|
||||||
|
|
||||||
|
#### Заголовок (140 байт)
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Поле | Описание |
|
||||||
|
|:--------:|:------:|:---:|------|----------|
|
||||||
|
| 0x00 | 96 | Vector3[8] | BoundingBox | 8-точечный баундинг-бокс |
|
||||||
|
| 0x60 | 12 | Vector3 | Center | Центральная точка |
|
||||||
|
| 0x6C | 4 | float | CenterW | W-компонента |
|
||||||
|
| 0x70 | 12 | Vector3 | Bottom | Нижняя точка |
|
||||||
|
| 0x7C | 12 | Vector3 | Top | Верхняя точка |
|
||||||
|
| 0x88 | 4 | float | XYRadius | Радиус в плоскости XY |
|
||||||
|
|
||||||
|
#### Элемент (68 байт)
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Поле | Описание |
|
||||||
|
|:--------:|:------:|:---:|------|----------|
|
||||||
|
| 0x00 | 2 | ushort | StartIndexIn07 | **Модель:** Начальный индекс в Компоненте 07<br>**Ландшафт:** Начальный индекс треугольника в Компоненте 15 |
|
||||||
|
| 0x02 | 2 | ushort | CountIn07 | **Модель:** Количество в Компоненте 07<br>**Ландшафт:** Количество треугольников |
|
||||||
|
| 0x04 | 2 | ushort | StartOffsetIn0d | Начальное смещение в Компоненте 0D (только Модель) |
|
||||||
|
| 0x06 | 2 | ushort | ByteLengthIn0D | Количество батчей в Компоненте 0D (только Модель) |
|
||||||
|
| 0x08 | 12 | Vector3 | LocalMinimum | Минимум локального баундинг-бокса |
|
||||||
|
| 0x14 | 12 | Vector3 | LocalMaximum | Максимум локального баундинг-бокса |
|
||||||
|
| 0x20 | 12 | Vector3 | Center | Центр сабмеша |
|
||||||
|
| 0x2C | 12 | Vector3 | Vector4 | Неизвестно |
|
||||||
|
| 0x38 | 12 | Vector3 | Vector5 | Неизвестно |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Компонент 03 - Vertices (0x0C = 12 байт)
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Поле | Описание |
|
||||||
|
|:--------:|:------:|:---:|------|----------|
|
||||||
|
| 0x00 | 4 | float | X | Координата X |
|
||||||
|
| 0x04 | 4 | float | Y | Координата Y |
|
||||||
|
| 0x08 | 4 | float | Z | Координата Z |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Компонент 06 - Indices (2 байта) - Только Модель
|
||||||
|
|
||||||
|
Массив `ushort` значений — индексы вершин треугольников.
|
||||||
|
Используются группами по 3 для каждого треугольника. Ссылки через батчи Компонента 0D.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Компонент 07 - Triangle Data (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 | Неизвестно |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Компонент 0B - Material Data (4 байта) - Только Ландшафт
|
||||||
|
|
||||||
|
Информация о материале/текстуре на каждый треугольник. Параллельный массив к Компоненту 15.
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Поле | Описание |
|
||||||
|
|:--------:|:------:|:---:|------|----------|
|
||||||
|
| 0x00 | 2 | ushort | HighWord | Индекс материала/текстуры |
|
||||||
|
| 0x02 | 2 | ushort | LowWord | Индекс треугольника (последовательный) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Компонент 0D - Draw Batches (0x14 = 20 байт) - Только Модель
|
||||||
|
|
||||||
|
Определяет батчи вызовов отрисовки.
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Поле | Описание |
|
||||||
|
|:--------:|:------:|:---:|------|----------|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Компонент 15 - Triangles (0x1C = 28 байт)
|
||||||
|
|
||||||
|
Прямые определения треугольников. Используется и Моделью и Ландшафтом,
|
||||||
|
но только Ландшафт использует их напрямую для рендеринга.
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Поле | Описание |
|
||||||
|
|:--------:|:------:|:---:|------|----------|
|
||||||
|
| 0x00 | 4 | uint32 | Flags | Флаги треугольника (0x20000 = коллизия) |
|
||||||
|
| 0x04 | 4 | uint32 | Magic04 | Неизвестно (часто 0xFFFFFF01) |
|
||||||
|
| 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 | Неизвестно |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Компонент 0A - External References (переменный размер)
|
||||||
|
|
||||||
|
Таблица строк для внешних ссылок на части меша. Формат:
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 байта: длина] [байты строки] [null-терминатор]
|
||||||
|
...повтор...
|
||||||
|
```
|
||||||
|
|
||||||
|
Длина 0 означает пустую запись. Строки типа `"central"` имеют особое значение (flag |= 1).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Пример: Ландшафт SC_1
|
||||||
|
|
||||||
|
```
|
||||||
|
Land.msh (SC_1):
|
||||||
|
├── 01: 256 тайлов (сетка 16×16)
|
||||||
|
├── 02: 512 сабмешей (256 LOD0 + 256 LOD1)
|
||||||
|
├── 03: 10 530 вершин
|
||||||
|
├── 04: 10 530 данных на вершину
|
||||||
|
├── 05: 10 530 данных на вершину
|
||||||
|
├── 0B: 7 882 записи материалов
|
||||||
|
├── 0E: 10 530 данных на вершину
|
||||||
|
├── 12: 10 530 микротекстурный маппинг
|
||||||
|
└── 15: 7 882 треугольника
|
||||||
|
├── LOD 0: 4 993 треугольника (тайлы 0-255 → сабмеши 0-255)
|
||||||
|
└── LOD 1: 2 889 треугольников (тайлы 0-255 → сабмеши 256-511)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Использование
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var converter = new MshConverter();
|
||||||
|
|
||||||
|
// Автоопределение типа и конвертация в OBJ
|
||||||
|
converter.Convert("Land.msh", "terrain.obj", lodLevel: 0);
|
||||||
|
converter.Convert("robot.msh", "robot.obj", lodLevel: 0);
|
||||||
|
|
||||||
|
// Ручное определение типа
|
||||||
|
var archive = NResParser.ReadFile("mesh.msh").Archive;
|
||||||
|
var type = MshConverter.DetectMeshType(archive);
|
||||||
|
// Возвращает: MshType.Model или MshType.Landscape
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Источники
|
||||||
|
|
||||||
|
- Реверс-инжиниринг `Terrain.dll` (класс CLandscape)
|
||||||
|
- Декомпиляция Ghidra: `CLandscape::ctor` и `IMesh2_of_CLandscape::Render`
|
||||||
@@ -4,123 +4,206 @@ using NResLib;
|
|||||||
|
|
||||||
namespace ParkanPlayground;
|
namespace ParkanPlayground;
|
||||||
|
|
||||||
|
public enum MshType
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
Model, // Has component 06 (indices), 0D (batches), 07
|
||||||
|
Landscape // Has component 0B (per-triangle material), uses 15 directly
|
||||||
|
}
|
||||||
|
|
||||||
public class MshConverter
|
public class MshConverter
|
||||||
{
|
{
|
||||||
public void Convert(string mshPath)
|
/// <summary>
|
||||||
|
/// Detects mesh type based on which components are present in the archive.
|
||||||
|
/// </summary>
|
||||||
|
public static MshType DetectMeshType(NResArchive archive)
|
||||||
|
{
|
||||||
|
bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00");
|
||||||
|
bool hasComponent0B = archive.Files.Any(f => f.FileType == "0B 00 00 00");
|
||||||
|
bool hasComponent0D = archive.Files.Any(f => f.FileType == "0D 00 00 00");
|
||||||
|
|
||||||
|
// Model: Uses indexed triangles via component 06 and batches via 0D
|
||||||
|
if (hasComponent06 && hasComponent0D)
|
||||||
|
return MshType.Model;
|
||||||
|
|
||||||
|
// Landscape: Uses direct triangles in component 15, with material data in 0B
|
||||||
|
if (hasComponent0B && !hasComponent06)
|
||||||
|
return MshType.Landscape;
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
var mshNresResult = NResParser.ReadFile(mshPath);
|
var mshNresResult = NResParser.ReadFile(mshPath);
|
||||||
var mshNres = mshNresResult.Archive!;
|
if (mshNresResult.Archive is null)
|
||||||
|
{
|
||||||
using var mshFs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
Console.WriteLine($"ERROR: Failed to read NRes archive: {mshNresResult.Error}");
|
||||||
|
return;
|
||||||
var component01 = Msh01.ReadComponent(mshFs, mshNres);
|
}
|
||||||
var component02 = Msh02.ReadComponent(mshFs, mshNres);
|
|
||||||
var component15 = Msh15.ReadComponent(mshFs, mshNres);
|
|
||||||
|
|
||||||
var component0A = Msh0A.ReadComponent(mshFs, mshNres);
|
var archive = mshNresResult.Archive;
|
||||||
var component07 = Msh07.ReadComponent(mshFs, mshNres);
|
var meshType = DetectMeshType(archive);
|
||||||
var component0D = Msh0D.ReadComponent(mshFs, mshNres);
|
|
||||||
|
outputPath ??= Path.ChangeExtension(mshPath, ".obj");
|
||||||
|
|
||||||
|
Console.WriteLine($"Converting: {Path.GetFileName(mshPath)}");
|
||||||
|
Console.WriteLine($"Detected type: {meshType}");
|
||||||
|
|
||||||
|
using var fs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
|
||||||
|
switch (meshType)
|
||||||
|
{
|
||||||
|
case MshType.Model:
|
||||||
|
ConvertModel(fs, archive, outputPath, lodLevel);
|
||||||
|
break;
|
||||||
|
case MshType.Landscape:
|
||||||
|
ConvertLandscape(fs, archive, outputPath, lodLevel);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Console.WriteLine("ERROR: Unknown mesh type, cannot convert.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Triangle Vertex Indices
|
/// <summary>
|
||||||
var component06 = Msh06.ReadComponent(mshFs, mshNres);
|
/// Converts a model mesh (robots, buildings, etc.) to OBJ.
|
||||||
|
/// Uses indexed triangles: 01 → 02 → 0D → 06 → 03
|
||||||
|
/// </summary>
|
||||||
|
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
// vertices
|
Console.WriteLine($"Vertices: {component03.Count}");
|
||||||
var component03 = Msh03.ReadComponent(mshFs, mshNres);
|
Console.WriteLine($"Pieces: {component01.Elements.Count}");
|
||||||
|
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
|
||||||
_ = 5;
|
|
||||||
|
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
|
||||||
// --- Write OBJ ---
|
sw.WriteLine($"# Model mesh converted from {Path.GetFileName(outputPath)}");
|
||||||
using var sw = new StreamWriter("test.obj", false, new UTF8Encoding(false));
|
sw.WriteLine($"# LOD level: {lodLevel}");
|
||||||
|
|
||||||
|
// Write all vertices
|
||||||
foreach (var v in component03)
|
foreach (var v in component03)
|
||||||
sw.WriteLine($"v {v.X:F8} {v.Y:F8} {v.Z:F8}");
|
sw.WriteLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}");
|
||||||
|
|
||||||
var vertices = new List<Vector3>();
|
int exportedFaces = 0;
|
||||||
var faces = new List<(int, int, int)>(); // store indices into vertices list
|
|
||||||
|
|
||||||
// 01 - это части меша (Piece)
|
|
||||||
for (var pieceIndex = 0; pieceIndex < component01.Elements.Count; pieceIndex++)
|
for (var pieceIndex = 0; pieceIndex < component01.Elements.Count; pieceIndex++)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Piece {pieceIndex}");
|
var piece = component01.Elements[pieceIndex];
|
||||||
var piece01 = component01.Elements[pieceIndex];
|
|
||||||
// var state = (piece.State00 == 0xffff) ? 0 : piece.State00;
|
// Get submesh index for requested LOD
|
||||||
|
if (lodLevel >= piece.Lod.Length)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var submeshIdx = piece.Lod[lodLevel];
|
||||||
|
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
|
||||||
|
continue;
|
||||||
|
|
||||||
for (var lodIndex = 0; lodIndex < piece01.Lod.Length; lodIndex++)
|
sw.WriteLine($"g piece_{pieceIndex}");
|
||||||
|
|
||||||
|
var submesh = component02.Elements[submeshIdx];
|
||||||
|
var batchStart = submesh.StartOffsetIn0d;
|
||||||
|
var batchCount = submesh.ByteLengthIn0D;
|
||||||
|
|
||||||
|
for (var batchIdx = 0; batchIdx < batchCount; batchIdx++)
|
||||||
{
|
{
|
||||||
var lod = piece01.Lod[lodIndex];
|
var batch = component0D[batchStart + batchIdx];
|
||||||
if (lod == 0xffff)
|
var baseVertex = batch.IndexInto03;
|
||||||
|
var indexStart = batch.IndexInto06;
|
||||||
|
var indexCount = batch.CountOf06;
|
||||||
|
|
||||||
|
for (int i = 0; i < indexCount; i += 3)
|
||||||
{
|
{
|
||||||
// Console.WriteLine($"Piece {pieceIndex} has lod -1 at {lodIndex}. Skipping");
|
var i1 = baseVertex + component06[indexStart + i];
|
||||||
continue;
|
var i2 = baseVertex + component06[indexStart + i + 1];
|
||||||
}
|
var i3 = baseVertex + component06[indexStart + i + 2];
|
||||||
|
|
||||||
sw.WriteLine($"o piece_{pieceIndex}_lod_{lodIndex}");
|
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
|
||||||
// 02 - Submesh
|
exportedFaces++;
|
||||||
var part02 = component02.Elements[lod];
|
|
||||||
|
|
||||||
int indexInto07 = part02.StartIndexIn07;
|
|
||||||
var comp07 = component07[indexInto07];
|
|
||||||
Console.WriteLine($"Lod {lodIndex}");
|
|
||||||
Console.WriteLine($"Comp07: {comp07.OffsetX}, {comp07.OffsetY}, {comp07.OffsetZ}");
|
|
||||||
|
|
||||||
var element0Dstart = part02.StartOffsetIn0d;
|
|
||||||
var element0Dcount = part02.ByteLengthIn0D;
|
|
||||||
|
|
||||||
// Console.WriteLine($"Started piece {pieceIndex}. LOD={lod}. 0D start={element0Dstart}, count={element0Dcount}");
|
|
||||||
|
|
||||||
for (var comp0Dindex = 0; comp0Dindex < element0Dcount; comp0Dindex++)
|
|
||||||
{
|
|
||||||
var element0D = component0D[element0Dstart + comp0Dindex];
|
|
||||||
|
|
||||||
var indexInto03 = element0D.IndexInto03;
|
|
||||||
var indexInto06 = element0D.IndexInto06; // indices
|
|
||||||
|
|
||||||
uint maxIndex = element0D.CountOf03;
|
|
||||||
uint indicesCount = element0D.CountOf06;
|
|
||||||
|
|
||||||
|
|
||||||
// Convert IndexInto06 to ushort array index (3 ushorts per triangle)
|
|
||||||
// Console.WriteLine($"Processing 0D element[{element0Dstart + comp0Dindex}]. IndexInto03={indexInto03}, IndexInto06={indexInto06}. Number of triangles={indicesCount}");
|
|
||||||
|
|
||||||
if (indicesCount != 0)
|
|
||||||
{
|
|
||||||
// sw.WriteLine($"o piece_{pieceIndex}_of_mesh_{comp0Dindex}");
|
|
||||||
|
|
||||||
for (int ind = 0; ind < indicesCount; ind += 3)
|
|
||||||
{
|
|
||||||
// Each triangle uses 3 consecutive ushorts in component06
|
|
||||||
|
|
||||||
// sw.WriteLine($"o piece_{pieceIndex}_of_mesh_{comp0Dindex}_tri_{ind}");
|
|
||||||
|
|
||||||
|
|
||||||
var i1 = indexInto03 + component06[indexInto06];
|
|
||||||
var i2 = indexInto03 + component06[indexInto06 + 1];
|
|
||||||
var i3 = indexInto03 + component06[indexInto06 + 2];
|
|
||||||
|
|
||||||
var v1 = component03[i1];
|
|
||||||
var v2 = component03[i2];
|
|
||||||
var v3 = component03[i3];
|
|
||||||
|
|
||||||
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
|
|
||||||
|
|
||||||
// push vertices to global list
|
|
||||||
vertices.Add(v1);
|
|
||||||
vertices.Add(v2);
|
|
||||||
vertices.Add(v3);
|
|
||||||
|
|
||||||
int baseIndex = vertices.Count;
|
|
||||||
// record face (OBJ is 1-based indexing!)
|
|
||||||
faces.Add((baseIndex - 2, baseIndex - 1, baseIndex));
|
|
||||||
|
|
||||||
indexInto07++;
|
|
||||||
indexInto06 += 3; // step by 3 since each triangle uses 3 ushorts
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = 5;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
|
||||||
|
Console.WriteLine($"Output: {outputPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a landscape mesh (terrain) to OBJ.
|
||||||
|
/// Uses direct triangles: 01 → 02 → 15 (via StartIndexIn07/CountIn07)
|
||||||
|
/// </summary>
|
||||||
|
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel)
|
||||||
|
{
|
||||||
|
var component01 = Msh01.ReadComponent(fs, archive);
|
||||||
|
var component02 = Msh02.ReadComponent(fs, archive);
|
||||||
|
var component03 = Msh03.ReadComponent(fs, archive);
|
||||||
|
var component15 = Msh15.ReadComponent(fs, archive);
|
||||||
|
|
||||||
|
Console.WriteLine($"Vertices: {component03.Count}");
|
||||||
|
Console.WriteLine($"Triangles: {component15.Count}");
|
||||||
|
Console.WriteLine($"Tiles: {component01.Elements.Count}");
|
||||||
|
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
|
||||||
|
|
||||||
|
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
|
||||||
|
sw.WriteLine($"# Landscape mesh converted from {Path.GetFileName(outputPath)}");
|
||||||
|
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}");
|
||||||
|
|
||||||
|
int exportedFaces = 0;
|
||||||
|
|
||||||
|
for (var tileIdx = 0; tileIdx < component01.Elements.Count; tileIdx++)
|
||||||
|
{
|
||||||
|
var tile = component01.Elements[tileIdx];
|
||||||
|
|
||||||
|
// Get submesh index for requested LOD
|
||||||
|
if (lodLevel >= tile.Lod.Length)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var submeshIdx = tile.Lod[lodLevel];
|
||||||
|
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sw.WriteLine($"g tile_{tileIdx}");
|
||||||
|
|
||||||
|
var submesh = component02.Elements[submeshIdx];
|
||||||
|
|
||||||
|
// For landscape, StartIndexIn07 = triangle start index, CountIn07 = triangle count
|
||||||
|
var triangleStart = submesh.StartIndexIn07;
|
||||||
|
var triangleCount = submesh.CountIn07;
|
||||||
|
|
||||||
|
for (var triOffset = 0; triOffset < triangleCount; triOffset++)
|
||||||
|
{
|
||||||
|
var triIdx = triangleStart + triOffset;
|
||||||
|
if (triIdx >= component15.Count)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"WARNING: Triangle index {triIdx} out of range for tile {tileIdx}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tri = component15[triIdx];
|
||||||
|
sw.WriteLine($"f {tri.Vertex1Index + 1} {tri.Vertex2Index + 1} {tri.Vertex3Index + 1}");
|
||||||
|
exportedFaces++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
|
||||||
|
Console.WriteLine($"Output: {outputPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Face(Vector3 P1, Vector3 P2, Vector3 P3);
|
public record Face(Vector3 P1, Vector3 P2, Vector3 P3);
|
||||||
|
|||||||
33
ParkanPlayground/Program.cs
Normal file
33
ParkanPlayground/Program.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using ParkanPlayground;
|
||||||
|
|
||||||
|
// ========== MSH CONVERTER TEST - AUTO-DETECTING MODEL VS LANDSCAPE ==========
|
||||||
|
|
||||||
|
var converter = new MshConverter();
|
||||||
|
|
||||||
|
// Test with landscape
|
||||||
|
Console.WriteLine("=".PadRight(60, '='));
|
||||||
|
converter.Convert(
|
||||||
|
@"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_1\Land.msh",
|
||||||
|
"landscape_lod0.obj",
|
||||||
|
lodLevel: 0
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// Test with landscape LOD 1 (lower detail)
|
||||||
|
Console.WriteLine("=".PadRight(60, '='));
|
||||||
|
converter.Convert(
|
||||||
|
@"C:\Program Files (x86)\Nikita\Iron Strategy\DATA\MAPS\SC_1\Land.msh",
|
||||||
|
"landscape_lod1.obj",
|
||||||
|
lodLevel: 1
|
||||||
|
);
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// Test with model
|
||||||
|
Console.WriteLine("=".PadRight(60, '='));
|
||||||
|
converter.Convert(
|
||||||
|
@"E:\ParkanUnpacked\fortif.rlb\133_fr_m_bunker.msh",
|
||||||
|
"bunker_lod0.obj",
|
||||||
|
lodLevel: 0
|
||||||
|
);
|
||||||
10
ParkanPlayground/from_ghidra/BoundingBox_typedef.c
Normal file
10
ParkanPlayground/from_ghidra/BoundingBox_typedef.c
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
typedef struct BoundingBox {
|
||||||
|
Vector3 BottomFrontLeft;
|
||||||
|
Vector3 BottomFrontRight;
|
||||||
|
Vector3 BottomBackRight;
|
||||||
|
Vector3 BottomBackLeft;
|
||||||
|
Vector3 TopBackRight;
|
||||||
|
Vector3 TopFrontRight;
|
||||||
|
Vector3 TopBackLeft;
|
||||||
|
Vector3 TopFrontLeft;
|
||||||
|
} BoundingBox;
|
||||||
2481
ParkanPlayground/from_ghidra/CLandscape_ctor.c
Normal file
2481
ParkanPlayground/from_ghidra/CLandscape_ctor.c
Normal file
File diff suppressed because it is too large
Load Diff
60
ParkanPlayground/from_ghidra/CLandscape_typedef.c
Normal file
60
ParkanPlayground/from_ghidra/CLandscape_typedef.c
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
typedef struct CLandscape {
|
||||||
|
IComponent * IComponent;
|
||||||
|
CGameObject game_object;
|
||||||
|
ITerrain * ITerrain;
|
||||||
|
IMesh2 * IMesh2;
|
||||||
|
I0x1 * I0x1;
|
||||||
|
IControl * IControl;
|
||||||
|
I0x22 * IUnknown_0x22;
|
||||||
|
undefined4 01_data_ptr;
|
||||||
|
MSH_02_file * msh_02_data_ptr;
|
||||||
|
undefined4 0b_data_ptr;
|
||||||
|
// collapse unknown fields
|
||||||
|
int[3][5] field_532;
|
||||||
|
// collapse unknown fields
|
||||||
|
undefined4 IComponent_owner;
|
||||||
|
KindaArray array_0x278;
|
||||||
|
int _count_unk_obj_array_elemsize_212;
|
||||||
|
undefined4 vertices_count;
|
||||||
|
uint 15_element_count;
|
||||||
|
// collapse unknown fields
|
||||||
|
Vector3 * 03_vertices_data_ptr;
|
||||||
|
int 03_vertices_stride;
|
||||||
|
undefined4 04_data_ptr;
|
||||||
|
undefined4 04_stride;
|
||||||
|
// collapse unknown fields
|
||||||
|
undefined4 05_data_ptr;
|
||||||
|
undefined4 05_stride;
|
||||||
|
undefined4 12_microtexture_mapping_data_ptr;
|
||||||
|
undefined4 12_microtexture_mapping_stride;
|
||||||
|
// collapse unknown fields
|
||||||
|
MshMetaForLandscape * mshmeta_ptr;
|
||||||
|
// collapse unknown fields
|
||||||
|
KindaArray array_0x7a40;
|
||||||
|
BoundingBox bounding_box_from_msh_02;
|
||||||
|
IMatManager * * IMatManager;
|
||||||
|
undefined4 wear_ptr;
|
||||||
|
IEffectManager * * ResolvedIEffectManager;
|
||||||
|
ISystemArealMap * * ResolvedISystemArealMap;
|
||||||
|
ICollManager * * ResolvedICollManager;
|
||||||
|
ISoundPool * * ISoundPool;
|
||||||
|
I3DSound * * I3DSound;
|
||||||
|
ILightManager * * ILightManager_owned;
|
||||||
|
KindaArray array_0x7c00;
|
||||||
|
KindaArray array_0x7c0c;
|
||||||
|
CShade * Shade;
|
||||||
|
undefined4 I3DRender??;
|
||||||
|
undefined4 field_31776;
|
||||||
|
undefined4 flags_of_mesh_part;
|
||||||
|
INResFile * * INResFile;
|
||||||
|
KindaArray array_0x7c2c;
|
||||||
|
undefined4 current_visible_primitives_count;
|
||||||
|
IMesh2 * * meshes;
|
||||||
|
IGameObject * * game_objects;
|
||||||
|
// collapse unknown fields
|
||||||
|
KindaArray array_0x7c84;
|
||||||
|
undefined4 * field_31888;
|
||||||
|
undefined4 _CheckMaxBasementAngleStep;
|
||||||
|
CollisionContext m_CollisionContext;
|
||||||
|
BoundingBox bounding_box;
|
||||||
|
} CLandscape;
|
||||||
1232
ParkanPlayground/from_ghidra/IMesh2_of_CLandscape_Render.c
Normal file
1232
ParkanPlayground/from_ghidra/IMesh2_of_CLandscape_Render.c
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user