mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
msh improvements
This commit is contained in:
+120
-38
@@ -5,10 +5,16 @@ namespace ParkanPlayground;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MSH-компонент 0x01: таблица узлов модели.
|
/// MSH-компонент 0x01: таблица узлов модели.
|
||||||
|
/// Для обычного AniMesh node имеет size 0x26.
|
||||||
|
/// У ландшафта metadata/magic1 может иметь другой смысл, например grid_x_count.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// У ландшафта magic1 - grid_x_count
|
|
||||||
public static class Msh0x01
|
public static class Msh0x01
|
||||||
{
|
{
|
||||||
|
public const int NormalElementSize = 0x26;
|
||||||
|
public const int LodCount = 3;
|
||||||
|
public const int GroupCount = 5;
|
||||||
|
public const int SlotCount = LodCount * GroupCount;
|
||||||
|
|
||||||
public static Msh0x01Component ReadComponent(FileStream mshFs, NResArchive archive)
|
public static Msh0x01Component ReadComponent(FileStream mshFs, NResArchive archive)
|
||||||
{
|
{
|
||||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00");
|
var entry = archive.Files.FirstOrDefault(x => x.FileType == "01 00 00 00");
|
||||||
@@ -28,71 +34,147 @@ public static class Msh0x01
|
|||||||
throw new Exception("Node table component (0x01) payload size is not divisible by element size");
|
throw new Exception("Node table component (0x01) payload size is not divisible by element size");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entry.ElementSize < 8)
|
||||||
|
{
|
||||||
|
throw new Exception("Node table component (0x01) element size is too small");
|
||||||
|
}
|
||||||
|
|
||||||
var elementCount = entry.FileLength / entry.ElementSize;
|
var elementCount = entry.FileLength / entry.ElementSize;
|
||||||
var data = new byte[entry.FileLength];
|
var data = new byte[entry.FileLength];
|
||||||
|
|
||||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||||
mshFs.ReadExactly(data, 0, data.Length);
|
mshFs.ReadExactly(data, 0, data.Length);
|
||||||
|
|
||||||
var dataSpan = data.AsSpan();
|
var dataSpan = data.AsSpan();
|
||||||
|
|
||||||
var elements = new List<Node>(elementCount);
|
var nodes = new List<Node>(elementCount);
|
||||||
|
|
||||||
for (var i = 0; i < elementCount; i++)
|
for (var i = 0; i < elementCount; i++)
|
||||||
{
|
{
|
||||||
var baseOffset = i * entry.ElementSize;
|
var baseOffset = i * entry.ElementSize;
|
||||||
var rawBytes = dataSpan.Slice(baseOffset, entry.ElementSize).ToArray();
|
var elementSpan = dataSpan.Slice(baseOffset, entry.ElementSize);
|
||||||
var slots = new ushort[15];
|
|
||||||
Array.Fill(slots, ushort.MaxValue);
|
var rawBytes = elementSpan.ToArray();
|
||||||
var slotWords = Math.Min(slots.Length, Math.Max(0, (entry.ElementSize - 8) / 2));
|
|
||||||
for (var slotIndex = 0; slotIndex < slotWords; slotIndex++)
|
var slotIndices = new ushort[SlotCount];
|
||||||
|
Array.Fill(slotIndices, ushort.MaxValue);
|
||||||
|
|
||||||
|
var slotWordCount = Math.Min(
|
||||||
|
slotIndices.Length,
|
||||||
|
Math.Max(0, (entry.ElementSize - 8) / 2));
|
||||||
|
|
||||||
|
for (var slotIndex = 0; slotIndex < slotWordCount; slotIndex++)
|
||||||
{
|
{
|
||||||
slots[slotIndex] =
|
slotIndices[slotIndex] =
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 8 + slotIndex * 2));
|
BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x08 + slotIndex * 2, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.Add(new Node(
|
nodes.Add(new Node(
|
||||||
rawBytes,
|
RawBytes: rawBytes,
|
||||||
(NodeFlags)BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset)),
|
Flags: (NodeFlags)BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x00, 2)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 2)),
|
ParentIndexOrLink: BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x02, 2)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 4)),
|
AnimMapStart0x13: BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x04, 2)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(dataSpan.Slice(baseOffset + 6)),
|
FallbackKey0x08: BinaryPrimitives.ReadUInt16LittleEndian(elementSpan.Slice(0x06, 2)),
|
||||||
slots));
|
Msh02SlotIndicesByLodAndGroup: slotIndices));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Msh0x01Component(elements);
|
return new Msh0x01Component(entry.ElementSize, nodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>Результат чтения MSH-компонента 0x01.</summary>
|
/// <summary>Результат чтения MSH-компонента 0x01.</summary>
|
||||||
/// <param name="Elements">Узлы компонента 0x01.</param>
|
/// <param name="ElementSize">Размер node entry из NRes metadata.</param>
|
||||||
public record Msh0x01Component(List<Node> Elements);
|
/// <param name="Nodes">Узлы компонента 0x01.</param>
|
||||||
|
public sealed record Msh0x01Component(
|
||||||
|
int ElementSize,
|
||||||
|
List<Node> Nodes)
|
||||||
|
{
|
||||||
|
public bool IsNormalAniMeshNodeTable => ElementSize == NormalElementSize;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Узел 0x01.</summary>
|
/// <summary>Узел MSH 0x01.</summary>
|
||||||
/// <param name="RawBytes">Сырые байты узла (length = attr3). Нужны для copy-through редких вариантов, например attr3 = 24.</param>
|
/// <param name="RawBytes">Сырые байты узла. Нужны для copy-through нестандартных вариантов.</param>
|
||||||
/// <param name="Flags">[0x00..0x02] Флаги узла</param>
|
/// <param name="Flags">[0x00..0x02] Флаги узла.</param>
|
||||||
/// <param name="ParentOrLink_possibly_0x02_index">[0x02..0x04] Индекс родителя или связанного узла</param>
|
/// <param name="ParentIndexOrLink">[0x02..0x04] Parent node index. 0xFFFF обычно значит root/no parent.</param>
|
||||||
/// <param name="AnimMapStart">[0x04..0x06] Начало блока в карте анимации Msh0x13 или 0xFFFF</param>
|
/// <param name="AnimMapStart0x13">[0x04..0x06] Начало блока в MSH 0x13 animation map или 0xFFFF.</param>
|
||||||
/// <param name="FallbackKey">[0x06..0x08] Индекс fallback-ключа в пуле ключей Msh0x08</param>
|
/// <param name="FallbackKey0x08">[0x06..0x08] Fallback key / index в MSH 0x08.</param>
|
||||||
/// <param name="Msh02SlotIndicesByLodAndGroupSlotIndex">[0x08..0x26] Индексы slot в Msh0x02 по формуле lod * 5 + group</param>
|
/// <param name="Msh02SlotIndicesByLodAndGroup">
|
||||||
public record Node(
|
/// [0x08..0x26] Индексы geometry slot в MSH 0x02.
|
||||||
|
/// Формула: slot = lod * 5 + group. 0xFFFF значит отсутствует.
|
||||||
|
/// </param>
|
||||||
|
public sealed record Node(
|
||||||
byte[] RawBytes,
|
byte[] RawBytes,
|
||||||
NodeFlags Flags,
|
NodeFlags Flags,
|
||||||
ushort ParentOrLink_possibly_0x02_index,
|
ushort ParentIndexOrLink,
|
||||||
ushort AnimMapStart,
|
ushort AnimMapStart0x13,
|
||||||
ushort FallbackKey,
|
ushort FallbackKey0x08,
|
||||||
ushort[] Msh02SlotIndicesByLodAndGroupSlotIndex)
|
ushort[] Msh02SlotIndicesByLodAndGroup)
|
||||||
{
|
{
|
||||||
|
public bool IsRoot => ParentIndexOrLink == ushort.MaxValue;
|
||||||
|
|
||||||
|
public int ParentIndexOrMinusOne =>
|
||||||
|
ParentIndexOrLink == ushort.MaxValue ? -1 : ParentIndexOrLink;
|
||||||
|
|
||||||
public ushort ResolveSlotIndex(int lod, int group = 0)
|
public ushort ResolveSlotIndex(int lod, int group = 0)
|
||||||
{
|
{
|
||||||
var index = lod * 5 + group;
|
var index = lod * GroupCount + group;
|
||||||
return index >= 0 && index < Msh02SlotIndicesByLodAndGroupSlotIndex.Length ? Msh02SlotIndicesByLodAndGroupSlotIndex[index] : ushort.MaxValue;
|
|
||||||
|
return index >= 0 && index < Msh02SlotIndicesByLodAndGroup.Length
|
||||||
|
? Msh02SlotIndicesByLodAndGroup[index]
|
||||||
|
: ushort.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasGeometrySlot(int lod, int group = 0) =>
|
||||||
|
ResolveSlotIndex(lod, group) != ushort.MaxValue;
|
||||||
|
|
||||||
|
public int CountLodsForGroup(int group = 0)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
for (var lod = 0; lod < LodCount; lod++)
|
||||||
|
{
|
||||||
|
if (!HasGeometrySlot(lod, group))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
public enum NodeFlags : ushort
|
public enum NodeFlags : ushort
|
||||||
{
|
{
|
||||||
// very uncertain
|
None = 0,
|
||||||
MSH01_NODE_FLAG_UNKNOWN_SKIP_RECURSE_0x04 = 0x4,
|
|
||||||
MSH01_NODE_FLAG_STOP_CHILD_TRAVERSAL = 0x10,
|
/// <summary>
|
||||||
MSH01_NODE_FLAG_NO_SHADOW = 0x40
|
/// Still uncertain. In recursive bounds/intersection paths this can suppress/alter child recursion.
|
||||||
|
/// Seen as child_node.flags & 0x04.
|
||||||
|
/// </summary>
|
||||||
|
UnknownSkipChild0x04 = 0x0004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops recursive traversal into children for bounds/render/intersection helpers.
|
||||||
|
/// </summary>
|
||||||
|
StopChildTraversal = 0x0010,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Special render-group-4 mode bit. In render group 4, selects alternate piece render mode.
|
||||||
|
/// Earlier also seen in special render/clip behavior.
|
||||||
|
/// </summary>
|
||||||
|
Group4AltRenderMode = 0x0020,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exclude from shadow / no shadow. CAniMesh tracks has_any_shadow_casting_piece when this bit is absent.
|
||||||
|
/// </summary>
|
||||||
|
NoShadow = 0x0040,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used during attached MSH load: if parent/root description contains "central", piece gets hidden/excluded flag.
|
||||||
|
/// Exact semantic name still provisional.
|
||||||
|
/// </summary>
|
||||||
|
CheckParentDescriptionCentral = 0x0800,
|
||||||
}
|
}
|
||||||
+122
-143
@@ -5,189 +5,168 @@ using NResLib;
|
|||||||
namespace ParkanPlayground;
|
namespace ParkanPlayground;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MSH-компонент 0x02: общий заголовок и таблица slot.
|
/// MSH-компонент 0x02: общий bounds header и таблица geometry slots.
|
||||||
|
/// Header size = 0x8C, slot size = 0x44.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Msh0x02
|
public static class Msh0x02
|
||||||
{
|
{
|
||||||
|
public const int HeaderSize = 0x8C;
|
||||||
|
public const int SlotSize = 0x44;
|
||||||
|
|
||||||
public static Msh0x02Component ReadComponent(FileStream mshFs, NResArchive archive)
|
public static Msh0x02Component ReadComponent(FileStream mshFs, NResArchive archive)
|
||||||
{
|
{
|
||||||
var fileEntry = archive.Files.FirstOrDefault(x => x.FileType == "02 00 00 00");
|
var entry = archive.Files.FirstOrDefault(x => x.FileType == "02 00 00 00");
|
||||||
|
|
||||||
if (fileEntry is null)
|
if (entry is null)
|
||||||
{
|
{
|
||||||
throw new Exception("Archive doesn't contain slots component (0x02)");
|
throw new Exception("Archive doesn't contain geometry slot component (0x02)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileEntry.FileLength < 0x8c)
|
if (entry.FileLength < HeaderSize)
|
||||||
{
|
{
|
||||||
throw new Exception("Slots component (0x02) is smaller than the 0x8C-byte header");
|
throw new Exception("Geometry slot component (0x02) is smaller than the 0x8C-byte header");
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((fileEntry.FileLength - 0x8c) % 68 != 0)
|
if ((entry.FileLength - HeaderSize) % SlotSize != 0)
|
||||||
{
|
{
|
||||||
throw new Exception("Slots component (0x02) payload after header is not divisible by 68");
|
throw new Exception("Geometry slot component (0x02) payload after header is not divisible by 0x44");
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = new byte[fileEntry.FileLength];
|
var data = new byte[entry.FileLength];
|
||||||
mshFs.Seek(fileEntry.OffsetInFile, SeekOrigin.Begin);
|
|
||||||
|
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||||
mshFs.ReadExactly(data, 0, data.Length);
|
mshFs.ReadExactly(data, 0, data.Length);
|
||||||
|
|
||||||
var header = data.AsSpan(0, 0x8c); // заголовок (length = 0x8C)
|
var span = data.AsSpan();
|
||||||
|
|
||||||
var center = new Vector3(
|
var header = ReadHeader(span.Slice(0, HeaderSize));
|
||||||
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(
|
var slotBytes = span.Slice(HeaderSize);
|
||||||
new Vector3(
|
var slotCount = slotBytes.Length / SlotSize;
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0)),
|
var slots = new List<GeometrySlot>(slotCount);
|
||||||
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 / 0x44;
|
|
||||||
|
|
||||||
for (var i = 0; i < slotCount; i++)
|
for (var i = 0; i < slotCount; i++)
|
||||||
{
|
{
|
||||||
var baseOffset = 0x44 * i;
|
var slot = slotBytes.Slice(i * SlotSize, SlotSize);
|
||||||
|
|
||||||
var opaque = new uint[3];
|
slots.Add(new GeometrySlot(
|
||||||
for (var opaqueIndex = 0; opaqueIndex < opaque.Length; opaqueIndex++)
|
TriStart0x07: BinaryPrimitives.ReadUInt16LittleEndian(slot.Slice(0x00, 2)),
|
||||||
{
|
TriCount0x07: BinaryPrimitives.ReadUInt16LittleEndian(slot.Slice(0x02, 2)),
|
||||||
opaque[opaqueIndex] =
|
BatchStart0x0D: BinaryPrimitives.ReadUInt16LittleEndian(slot.Slice(0x04, 2)),
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(
|
BatchCount0x0D: BinaryPrimitives.ReadUInt16LittleEndian(slot.Slice(0x06, 2)),
|
||||||
skippedHeader.Slice(baseOffset + 0x38 + opaqueIndex * 4)
|
LocalMinimum: ReadVector3(slot, 0x08),
|
||||||
);
|
LocalMaximum: ReadVector3(slot, 0x14),
|
||||||
}
|
BoundingSphere: ReadSphere(slot, 0x20),
|
||||||
|
BaseXyArea: BinaryPrimitives.ReadSingleLittleEndian(slot.Slice(0x30, 4)),
|
||||||
elements.Add(new Slot(
|
BaseVolume: BinaryPrimitives.ReadSingleLittleEndian(slot.Slice(0x34, 4)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 0x00)),
|
Opaque38: BinaryPrimitives.ReadUInt32LittleEndian(slot.Slice(0x38, 4)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 0x02)),
|
Opaque3C: BinaryPrimitives.ReadUInt32LittleEndian(slot.Slice(0x3C, 4)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 0x04)),
|
Opaque40: BinaryPrimitives.ReadUInt32LittleEndian(slot.Slice(0x40, 4))));
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(skippedHeader.Slice(baseOffset + 0x06)),
|
|
||||||
new Vector3(
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x08)),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x0c)),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x10))
|
|
||||||
),
|
|
||||||
new Vector3(
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x14)),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x18)),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x1c))
|
|
||||||
),
|
|
||||||
new Sphere(
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x20)),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x24)),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x28)),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x2c))),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x30)),
|
|
||||||
BinaryPrimitives.ReadSingleLittleEndian(skippedHeader.Slice(baseOffset + 0x34)),
|
|
||||||
opaque
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Msh0x02Component(
|
return new Msh0x02Component(header, slots);
|
||||||
new Msh02Header(bb, new Sphere(center.X, center.Y, center.Z, centerW), bottom, top, xyRadius),
|
}
|
||||||
elements);
|
|
||||||
|
private static Msh02Header ReadHeader(ReadOnlySpan<byte> header)
|
||||||
|
{
|
||||||
|
var bbox = new BoundingBox(
|
||||||
|
BottomFrontLeft: ReadVector3(header, 0x00),
|
||||||
|
BottomFrontRight: ReadVector3(header, 0x0C),
|
||||||
|
BottomBackRight: ReadVector3(header, 0x18),
|
||||||
|
BottomBackLeft: ReadVector3(header, 0x24),
|
||||||
|
TopFrontLeft: ReadVector3(header, 0x30),
|
||||||
|
TopFrontRight: ReadVector3(header, 0x3C),
|
||||||
|
TopBackRight: ReadVector3(header, 0x48),
|
||||||
|
TopBackLeft: ReadVector3(header, 0x54));
|
||||||
|
|
||||||
|
return new Msh02Header(
|
||||||
|
BoundingBox: bbox,
|
||||||
|
BoundingSphere: ReadSphere(header, 0x60),
|
||||||
|
Bottom: ReadVector3(header, 0x70),
|
||||||
|
Top: ReadVector3(header, 0x7C),
|
||||||
|
XyRadius: BinaryPrimitives.ReadSingleLittleEndian(header.Slice(0x88, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3 ReadVector3(ReadOnlySpan<byte> data, int offset)
|
||||||
|
{
|
||||||
|
return new Vector3(
|
||||||
|
BinaryPrimitives.ReadSingleLittleEndian(data.Slice(offset + 0x00, 4)),
|
||||||
|
BinaryPrimitives.ReadSingleLittleEndian(data.Slice(offset + 0x04, 4)),
|
||||||
|
BinaryPrimitives.ReadSingleLittleEndian(data.Slice(offset + 0x08, 4)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Sphere ReadSphere(ReadOnlySpan<byte> data, int offset)
|
||||||
|
{
|
||||||
|
return new Sphere(
|
||||||
|
BinaryPrimitives.ReadSingleLittleEndian(data.Slice(offset + 0x00, 4)),
|
||||||
|
BinaryPrimitives.ReadSingleLittleEndian(data.Slice(offset + 0x04, 4)),
|
||||||
|
BinaryPrimitives.ReadSingleLittleEndian(data.Slice(offset + 0x08, 4)),
|
||||||
|
BinaryPrimitives.ReadSingleLittleEndian(data.Slice(offset + 0x0C, 4)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Результат чтения MSH-компонента 0x02.</summary>
|
/// <summary>Результат чтения MSH-компонента 0x02.</summary>
|
||||||
/// <param name="Header">Заголовок 0x02 (length = 0x8C).</param>
|
/// <param name="Header">Header 0x02, length = 0x8C.</param>
|
||||||
/// <param name="Elements">Slot records после заголовка.</param>
|
/// <param name="Slots">Geometry slots после header.</param>
|
||||||
public record class Msh0x02Component(Msh02Header Header, List<Slot> Elements);
|
public sealed record Msh0x02Component(
|
||||||
|
Msh02Header Header,
|
||||||
|
List<GeometrySlot> Slots)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Backward-compatible alias, если старый код ещё использует Elements.
|
||||||
|
/// </summary>
|
||||||
|
public List<GeometrySlot> Elements => Slots;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Заголовок 0x02 (length = 0x8C).</summary>
|
/// <summary>Заголовок MSH 0x02, length = 0x8C.</summary>
|
||||||
/// <param name="BoundingBox">[0x00..0x60] Bounding box из 8 точек.</param>
|
/// <param name="BoundingBox">[0x00..0x60] Bounding box из 8 точек.</param>
|
||||||
/// <param name="BoundingSphere">[0x60..0x70] Bounding sphere.</param>
|
/// <param name="BoundingSphere">[0x60..0x70] Bounding sphere: xyz = center, w = radius.</param>
|
||||||
/// <param name="Bottom">[0x70..0x7C] Нижняя точка.</param>
|
/// <param name="Bottom">[0x70..0x7C] Нижняя/опорная точка меша.</param>
|
||||||
/// <param name="Top">[0x7C..0x88] Верхняя точка.</param>
|
/// <param name="Top">[0x7C..0x88] Верхняя точка меша.</param>
|
||||||
/// <param name="XYRadius">[0x88..0x8C] Радиус в плоскости XY.</param>
|
/// <param name="XyRadius">[0x88..0x8C] Радиус/extent в плоскости XY.</param>
|
||||||
public record class Msh02Header(
|
public sealed record Msh02Header(
|
||||||
BoundingBox BoundingBox,
|
BoundingBox BoundingBox,
|
||||||
Sphere BoundingSphere,
|
Sphere BoundingSphere,
|
||||||
Vector3 Bottom,
|
Vector3 Bottom,
|
||||||
Vector3 Top,
|
Vector3 Top,
|
||||||
float XYRadius);
|
float XyRadius);
|
||||||
|
|
||||||
/// <summary>Geometry Slot 0x02 (length = 0x44).</summary>
|
/// <summary>Geometry slot MSH 0x02, length = 0x44.</summary>
|
||||||
/// <param name="TriStart">[0x00..0x02] Первый triangle descriptor в Msh0x07. Для terrain-гипотезы может быть диапазоном Msh0x15.</param>
|
/// <param name="TriStart0x07">[0x00..0x02] Первый triangle descriptor в MSH 0x07.</param>
|
||||||
/// <param name="TriCount">[0x02..0x04] Количество triangle descriptor в Msh0x07. Для terrain-гипотезы может быть count для Msh0x15.</param>
|
/// <param name="TriCount0x07">[0x02..0x04] Количество triangle descriptor / triangle range count.</param>
|
||||||
/// <param name="BatchStart">[0x04..0x06] Первый batch в таблице 0x0D.</param>
|
/// <param name="BatchStart0x0D">[0x04..0x06] Первый batch в MSH 0x0D.</param>
|
||||||
/// <param name="BatchCount">[0x06..0x08] Количество batch в таблице 0x0D</param>
|
/// <param name="BatchCount0x0D">[0x06..0x08] Количество batch в MSH 0x0D.</param>
|
||||||
/// <param name="LocalMinimum">[0x08..0x14] Минимум локального AABB</param>
|
/// <param name="LocalMinimum">[0x08..0x14] Local AABB minimum.</param>
|
||||||
/// <param name="LocalMaximum">[0x14..0x20] Максимум локального AABB</param>
|
/// <param name="LocalMaximum">[0x14..0x20] Local AABB maximum.</param>
|
||||||
/// <param name="BoundingSphere">[0x20..0x30] Bounding sphere</param>
|
/// <param name="BoundingSphere">[0x20..0x30] Local bounding sphere.</param>
|
||||||
/// <param name="BaseXyArea">[0x30..0x34] Базовая XY-площадь / footprint area до масштабирования.</param>
|
/// <param name="BaseXyArea">[0x30..0x34] Базовая XY-площадь / footprint area до mesh scale.</param>
|
||||||
/// <param name="BaseVolume">[0x34..0x38] Базовый объём до масштабирования.</param>
|
/// <param name="BaseVolume">[0x34..0x38] Базовый объём до mesh scale.</param>
|
||||||
/// <param name="Opaque">[0x38..0x44] 3 opaque dword</param>
|
/// <param name="Opaque38">[0x38..0x3C] Opaque dword.</param>
|
||||||
public record Slot(
|
/// <param name="Opaque3C">[0x3C..0x40] Opaque dword.</param>
|
||||||
ushort TriStart,
|
/// <param name="Opaque40">[0x40..0x44] Opaque dword.</param>
|
||||||
ushort TriCount,
|
public readonly record struct GeometrySlot(
|
||||||
ushort BatchStart,
|
ushort TriStart0x07,
|
||||||
ushort BatchCount,
|
ushort TriCount0x07,
|
||||||
|
ushort BatchStart0x0D,
|
||||||
|
ushort BatchCount0x0D,
|
||||||
Vector3 LocalMinimum,
|
Vector3 LocalMinimum,
|
||||||
Vector3 LocalMaximum,
|
Vector3 LocalMaximum,
|
||||||
Sphere BoundingSphere,
|
Sphere BoundingSphere,
|
||||||
float BaseXyArea,
|
float BaseXyArea,
|
||||||
float BaseVolume,
|
float BaseVolume,
|
||||||
uint[] Opaque);
|
uint Opaque38,
|
||||||
|
uint Opaque3C,
|
||||||
|
uint Opaque40)
|
||||||
|
{
|
||||||
|
public int BatchEndExclusive0x0D => BatchStart0x0D + BatchCount0x0D;
|
||||||
|
|
||||||
/// <summary>Bounding box заголовка: 8 точек по 3 float (length = 0x60).</summary>
|
public int TriEndExclusive0x07 => TriStart0x07 + TriCount0x07;
|
||||||
|
|
||||||
|
public bool HasBatches => BatchCount0x0D != 0;
|
||||||
|
|
||||||
|
public bool HasTriangles => TriCount0x07 != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Bounding box заголовка: 8 точек по 3 float, length = 0x60.</summary>
|
||||||
/// <param name="BottomFrontLeft">[0x00..0x0C] Нижняя передняя левая точка.</param>
|
/// <param name="BottomFrontLeft">[0x00..0x0C] Нижняя передняя левая точка.</param>
|
||||||
/// <param name="BottomFrontRight">[0x0C..0x18] Нижняя передняя правая точка.</param>
|
/// <param name="BottomFrontRight">[0x0C..0x18] Нижняя передняя правая точка.</param>
|
||||||
/// <param name="BottomBackRight">[0x18..0x24] Нижняя задняя правая точка.</param>
|
/// <param name="BottomBackRight">[0x18..0x24] Нижняя задняя правая точка.</param>
|
||||||
@@ -196,7 +175,7 @@ public static class Msh0x02
|
|||||||
/// <param name="TopFrontRight">[0x3C..0x48] Верхняя передняя правая точка.</param>
|
/// <param name="TopFrontRight">[0x3C..0x48] Верхняя передняя правая точка.</param>
|
||||||
/// <param name="TopBackRight">[0x48..0x54] Верхняя задняя правая точка.</param>
|
/// <param name="TopBackRight">[0x48..0x54] Верхняя задняя правая точка.</param>
|
||||||
/// <param name="TopBackLeft">[0x54..0x60] Верхняя задняя левая точка.</param>
|
/// <param name="TopBackLeft">[0x54..0x60] Верхняя задняя левая точка.</param>
|
||||||
public record BoundingBox(
|
public sealed record BoundingBox(
|
||||||
Vector3 BottomFrontLeft,
|
Vector3 BottomFrontLeft,
|
||||||
Vector3 BottomFrontRight,
|
Vector3 BottomFrontRight,
|
||||||
Vector3 BottomBackRight,
|
Vector3 BottomBackRight,
|
||||||
|
|||||||
+59
-13
@@ -4,42 +4,88 @@ using NResLib;
|
|||||||
namespace ParkanPlayground;
|
namespace ParkanPlayground;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MSH-компонент 0x06: индексный буфер
|
/// MSH-компонент 0x06: индексный буфер.
|
||||||
|
/// Используется batch-ами 0x0D через Batch.IndexStart / Batch.IndexCount.
|
||||||
|
/// Индексы являются ushort и обычно читаются тройками как triangle indices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Msh0x06
|
public static class Msh0x06
|
||||||
{
|
{
|
||||||
public static List<ushort> ReadComponent(
|
public const int ElementSize = 2;
|
||||||
FileStream mshFs, NResArchive archive)
|
|
||||||
|
public static List<ushort> ReadComponent(FileStream mshFs, NResArchive archive)
|
||||||
{
|
{
|
||||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "06 00 00 00");
|
var entry = archive.Files.FirstOrDefault(x => x.FileType == "06 00 00 00");
|
||||||
|
|
||||||
if (entry is null)
|
if (entry is null)
|
||||||
{
|
{
|
||||||
throw new Exception("Archive doesn't contain file (06)");
|
throw new Exception("Archive doesn't contain index buffer component (0x06)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.ElementSize != 2)
|
if (entry.ElementSize != ElementSize)
|
||||||
{
|
{
|
||||||
throw new Exception("Index buffer component (0x06) element size is not 2");
|
throw new Exception("Index buffer component (0x06) element size is not 2");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.FileLength % entry.ElementSize != 0)
|
if (entry.FileLength % ElementSize != 0)
|
||||||
{
|
{
|
||||||
throw new Exception("Index buffer component (0x06) payload size is not divisible by element size");
|
throw new Exception("Index buffer component (0x06) payload size is not divisible by 2");
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = new byte[entry.FileLength];
|
var data = new byte[entry.FileLength];
|
||||||
|
|
||||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||||
mshFs.ReadExactly(data, 0, data.Length);
|
mshFs.ReadExactly(data, 0, data.Length);
|
||||||
|
|
||||||
var elements = new List<ushort>(entry.FileLength / entry.ElementSize);
|
var span = data.AsSpan();
|
||||||
for (var i = 0; i < entry.FileLength / entry.ElementSize; i++)
|
var indices = new List<ushort>(entry.FileLength / ElementSize);
|
||||||
|
|
||||||
|
for (var offset = 0; offset < span.Length; offset += ElementSize)
|
||||||
{
|
{
|
||||||
elements.Add(
|
indices.Add(BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(offset, ElementSize)));
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(i * 2))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements;
|
return indices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает triangle triplets из диапазона индексов.
|
||||||
|
/// Удобно для обхода batch.IndexStart / batch.IndexCount из 0x0D.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<TriangleIndices> EnumerateTriangles(
|
||||||
|
IReadOnlyList<ushort> indices,
|
||||||
|
int indexStart,
|
||||||
|
int indexCount)
|
||||||
|
{
|
||||||
|
if (indexStart < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(indexStart));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexCount < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(indexCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexStart + indexCount > indices.Count)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Index range is outside the 0x06 index buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i + 2 < indexCount; i += 3)
|
||||||
|
{
|
||||||
|
yield return new TriangleIndices(
|
||||||
|
indices[indexStart + i + 0],
|
||||||
|
indices[indexStart + i + 1],
|
||||||
|
indices[indexStart + i + 2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Тройка индексов triangle из MSH 0x06.</summary>
|
||||||
|
/// <param name="A">Первый vertex index внутри batch/base vertex range.</param>
|
||||||
|
/// <param name="B">Второй vertex index внутри batch/base vertex range.</param>
|
||||||
|
/// <param name="C">Третий vertex index внутри batch/base vertex range.</param>
|
||||||
|
public readonly record struct TriangleIndices(
|
||||||
|
ushort A,
|
||||||
|
ushort B,
|
||||||
|
ushort C);
|
||||||
}
|
}
|
||||||
+67
-32
@@ -1,71 +1,88 @@
|
|||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using Common;
|
||||||
using NResLib;
|
using NResLib;
|
||||||
|
|
||||||
namespace ParkanPlayground;
|
namespace ParkanPlayground;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MSH-компонент 0x07: описатели треугольников для коллизии/пикинга
|
/// MSH-компонент 0x07: triangle descriptors.
|
||||||
|
/// Используется geometry walker-ами для raycast / point-inside / фильтрации triangle flags.
|
||||||
|
/// Геометрические vertex indices лежат не здесь, а в MSH 0x06.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Msh0x07
|
public static class Msh0x07
|
||||||
{
|
{
|
||||||
public static List<TriangleDescriptor> ReadComponent(
|
public const int ElementSize = 0x10;
|
||||||
FileStream mshFs, NResArchive archive)
|
public const float PackedNormalScale = 1.0f / 32767.0f;
|
||||||
|
|
||||||
|
public static List<TriangleDescriptor> ReadComponent(FileStream mshFs, NResArchive archive)
|
||||||
{
|
{
|
||||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "07 00 00 00");
|
var entry = archive.Files.FirstOrDefault(x => x.FileType == "07 00 00 00");
|
||||||
|
|
||||||
if (entry is null)
|
if (entry is null)
|
||||||
{
|
{
|
||||||
throw new Exception("Archive doesn't contain file (07)");
|
throw new Exception("Archive doesn't contain triangle descriptor component (0x07)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.ElementSize != 16)
|
if (entry.ElementSize != ElementSize)
|
||||||
{
|
{
|
||||||
throw new Exception("Triangle descriptor component (0x07) element size is not 16");
|
throw new Exception("Triangle descriptor component (0x07) element size is not 16");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.FileLength % entry.ElementSize != 0)
|
if (entry.FileLength % ElementSize != 0)
|
||||||
{
|
{
|
||||||
throw new Exception("Triangle descriptor component (0x07) payload size is not divisible by element size");
|
throw new Exception("Triangle descriptor component (0x07) payload size is not divisible by 16");
|
||||||
}
|
}
|
||||||
|
|
||||||
var data = new byte[entry.FileLength];
|
var data = new byte[entry.FileLength];
|
||||||
|
|
||||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||||
mshFs.ReadExactly(data, 0, data.Length);
|
mshFs.ReadExactly(data, 0, data.Length);
|
||||||
|
|
||||||
var elementBytes = data.Chunk(16);
|
var span = data.AsSpan();
|
||||||
|
var descriptors = new List<TriangleDescriptor>(entry.FileLength / ElementSize);
|
||||||
|
|
||||||
var elements = elementBytes.Select(x => new TriangleDescriptor(
|
for (var offset = 0; offset < span.Length; offset += ElementSize)
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
|
{
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
|
var element = span.Slice(offset, ElementSize);
|
||||||
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;
|
descriptors.Add(new TriangleDescriptor(
|
||||||
|
Flags: (TriangleFlags)BinaryPrimitives.ReadUInt16LittleEndian(element.Slice(0x00, 2)),
|
||||||
|
Link0: BinaryPrimitives.ReadUInt16LittleEndian(element.Slice(0x02, 2)),
|
||||||
|
Link1: BinaryPrimitives.ReadUInt16LittleEndian(element.Slice(0x04, 2)),
|
||||||
|
Link2: BinaryPrimitives.ReadUInt16LittleEndian(element.Slice(0x06, 2)),
|
||||||
|
PackedNormalX: BinaryPrimitives.ReadInt16LittleEndian(element.Slice(0x08, 2)),
|
||||||
|
PackedNormalY: BinaryPrimitives.ReadInt16LittleEndian(element.Slice(0x0A, 2)),
|
||||||
|
PackedNormalZ: BinaryPrimitives.ReadInt16LittleEndian(element.Slice(0x0C, 2)),
|
||||||
|
SelectorPacked: BinaryPrimitives.ReadUInt16LittleEndian(element.Slice(0x0E, 2))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return descriptors;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Описатель треугольника 0x07 (length = 0x10).</summary>
|
/// <summary>Описатель triangle MSH 0x07, length = 0x10.</summary>
|
||||||
/// <param name="TriFlags">[0x00..0x02] Флаги треугольника</param>
|
/// <param name="Flags">[0x00..0x02] Triangle flags. Используются GeometryWalkFilter require/exclude.</param>
|
||||||
/// <param name="Link0">[0x02..0x04] Связь/opaque поле 0</param>
|
/// <param name="Link0">[0x02..0x04] Opaque/link поле 0.</param>
|
||||||
/// <param name="Link1">[0x04..0x06] Связь/opaque поле 1</param>
|
/// <param name="Link1">[0x04..0x06] Opaque/link поле 1.</param>
|
||||||
/// <param name="Link2">[0x06..0x08] Связь/opaque поле 2</param>
|
/// <param name="Link2">[0x06..0x08] Opaque/link поле 2.</param>
|
||||||
/// <param name="NormalX">[0x08..0x0A] Упакованная X-компонента нормали</param>
|
/// <param name="PackedNormalX">[0x08..0x0A] Packed normal X, int16, scale = 1 / 32767.</param>
|
||||||
/// <param name="NormalY">[0x0A..0x0C] Упакованная Y-компонента нормали</param>
|
/// <param name="PackedNormalY">[0x0A..0x0C] Packed normal Y, int16, scale = 1 / 32767.</param>
|
||||||
/// <param name="NormalZ">[0x0C..0x0E] Упакованная Z-компонента нормали</param>
|
/// <param name="PackedNormalZ">[0x0C..0x0E] Packed normal Z, int16, scale = 1 / 32767.</param>
|
||||||
/// <param name="SelectorPacked">[0x0E..0x10] Три 2-битных селектора; значение 3 трактуется как 0xFFFF</param>
|
/// <param name="SelectorPacked">[0x0E..0x10] Packed selectors. Старое наблюдение: 3 трактуется как 0xFFFF.</param>
|
||||||
public readonly record struct TriangleDescriptor(
|
public readonly record struct TriangleDescriptor(
|
||||||
ushort TriFlags,
|
TriangleFlags Flags,
|
||||||
ushort Link0,
|
ushort Link0,
|
||||||
ushort Link1,
|
ushort Link1,
|
||||||
ushort Link2,
|
ushort Link2,
|
||||||
short NormalX,
|
short PackedNormalX,
|
||||||
short NormalY,
|
short PackedNormalY,
|
||||||
short NormalZ,
|
short PackedNormalZ,
|
||||||
ushort SelectorPacked)
|
ushort SelectorPacked)
|
||||||
{
|
{
|
||||||
|
public Vector3 Normal => new(
|
||||||
|
PackedNormalX * PackedNormalScale,
|
||||||
|
PackedNormalY * PackedNormalScale,
|
||||||
|
PackedNormalZ * PackedNormalScale);
|
||||||
|
|
||||||
public ushort GetSelector(int index)
|
public ushort GetSelector(int index)
|
||||||
{
|
{
|
||||||
if (index is < 0 or > 2)
|
if (index is < 0 or > 2)
|
||||||
@@ -74,7 +91,25 @@ public static class Msh0x07
|
|||||||
}
|
}
|
||||||
|
|
||||||
var selector = (SelectorPacked >> (index * 2)) & 0b11;
|
var selector = (SelectorPacked >> (index * 2)) & 0b11;
|
||||||
return selector == 3 ? ushort.MaxValue : (ushort)selector;
|
|
||||||
|
return selector == 3
|
||||||
|
? ushort.MaxValue
|
||||||
|
: (ushort)selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MatchesFilter(TriangleFlags required, TriangleFlags excluded)
|
||||||
|
{
|
||||||
|
return (Flags & required) == required
|
||||||
|
&& (Flags & excluded) == 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum TriangleFlags : ushort
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
// Пока не называем отдельные bits, потому что мы видели только require/exclude фильтрацию.
|
||||||
|
// Добавляй конкретные имена по мере нахождения usage sites.
|
||||||
|
}
|
||||||
+78
-38
@@ -4,14 +4,14 @@ using NResLib;
|
|||||||
namespace ParkanPlayground;
|
namespace ParkanPlayground;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MSH-компонент 0x0D: таблица batch.
|
/// MSH-компонент 0x0D: таблица render/intersection batches.
|
||||||
|
/// Используется через MSH_02_geometry_slot.batch_start_0x0d / batch_count_0x0d.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Msh0x0D
|
public static class Msh0x0D
|
||||||
{
|
{
|
||||||
public const int ElementSize = 20;
|
public const int ElementSize = 20;
|
||||||
|
|
||||||
public static List<Batch> ReadComponent(
|
public static List<Batch> ReadComponent(FileStream mshFs, NResArchive archive)
|
||||||
FileStream mshFs, NResArchive archive)
|
|
||||||
{
|
{
|
||||||
var entry = archive.Files.FirstOrDefault(x => x.FileType == "0D 00 00 00");
|
var entry = archive.Files.FirstOrDefault(x => x.FileType == "0D 00 00 00");
|
||||||
|
|
||||||
@@ -31,53 +31,93 @@ public static class Msh0x0D
|
|||||||
}
|
}
|
||||||
|
|
||||||
var data = new byte[entry.FileLength];
|
var data = new byte[entry.FileLength];
|
||||||
|
|
||||||
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
mshFs.Seek(entry.OffsetInFile, SeekOrigin.Begin);
|
||||||
mshFs.ReadExactly(data, 0, data.Length);
|
mshFs.ReadExactly(data, 0, data.Length);
|
||||||
|
|
||||||
var elementBytes = data.Chunk(ElementSize);
|
return data
|
||||||
|
.Chunk(ElementSize)
|
||||||
var elements = elementBytes.Select(x => new Batch(
|
.Select(x => new Batch(
|
||||||
(BatchFlags)BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0)),
|
(BatchFlags)BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0x00)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(2)),
|
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0x02)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(4)),
|
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0x04)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(6)),
|
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0x06)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(8)),
|
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0x08)),
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0xA)),
|
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0x0A)),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0xE)),
|
BinaryPrimitives.ReadUInt16LittleEndian(x.AsSpan(0x0E)),
|
||||||
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0x10)))).ToList();
|
BinaryPrimitives.ReadUInt32LittleEndian(x.AsSpan(0x10))))
|
||||||
|
.ToList();
|
||||||
return elements;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Batch 0x0D</summary>
|
/// <summary>MSH batch 0x0D, sizeof 0x14.</summary>
|
||||||
/// <param name="BatchFlags">[0x00..0x02] Флаги batch. У FParkan: batchFlags.</param>
|
/// <param name="Flags">[0x00..0x02] Флаги batch.</param>
|
||||||
/// <param name="MaterialIndex">[0x02..0x04] Индекс material slot, резолвится через WEAR/MAT0 pipeline.</param>
|
/// <param name="MaterialIndex">[0x02..0x04] Индекс material slot. Может быть overridden CAniMesh::forced_material_index.</param>
|
||||||
/// <param name="Opaque4">[0x04..0x06] Opaque поле. Старое локальное имя: TriangleCount; не подтверждено.</param>
|
/// <param name="Opaque04">[0x04..0x06] Opaque. В GetBatchRenderData попадает в packed field вместе с material/lightmap данными.</param>
|
||||||
/// <param name="Opaque6">[0x06..0x08] Opaque поле. Сохранять побайтно в writer.</param>
|
/// <param name="LocalBatchIndex">[0x06..0x08] Локальный batch index. В GetBatchRenderData складывается с MSH_piece.local_index_base / batch base.</param>
|
||||||
/// <param name="IndexCount">[0x08..0x0A] Количество индексов в индексном буфере 0x06.</param>
|
/// <param name="IndexCount">[0x08..0x0A] Количество индексов из component 0x06.</param>
|
||||||
/// <param name="IndexStart">[0x0A..0x0E] Первый индекс в индексном буфере 0x06.</param>
|
/// <param name="IndexStart">[0x0A..0x0E] Первый индекс в component 0x06.</param>
|
||||||
/// <param name="Opaque14">[0x0E..0x10] Opaque поле. Старое локальное имя: CountOf03; не подтверждено.</param>
|
/// <param name="VertexCount">[0x0E..0x10] Количество вершин для render primitive.</param>
|
||||||
/// <param name="BaseVertex">[0x10..0x14] Базовая вершина в position stream 0x03. У FParkan: baseVertex.</param>
|
/// <param name="BaseVertex">[0x10..0x14] Base vertex в vertex streams, включая position stream 0x03.</param>
|
||||||
public readonly record struct Batch(
|
public readonly record struct Batch(
|
||||||
BatchFlags BatchFlags,
|
BatchFlags Flags,
|
||||||
ushort MaterialIndex,
|
ushort MaterialIndex,
|
||||||
ushort Opaque4,
|
ushort Opaque04,
|
||||||
ushort Opaque6,
|
ushort LocalBatchIndex,
|
||||||
ushort IndexCount,
|
ushort IndexCount,
|
||||||
uint IndexStart,
|
uint IndexStart,
|
||||||
ushort Opaque14,
|
ushort VertexCount,
|
||||||
uint BaseVertex);
|
uint BaseVertex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
public enum BatchFlags : ushort
|
public enum BatchFlags : ushort
|
||||||
{
|
{
|
||||||
MSH0D_BATCH_SPECIAL_SUBMIT = 1,
|
None = 0,
|
||||||
MSH0D_BATCH_INTERSECT_TWO_SIDED = 2,
|
|
||||||
MSH0D_BATCH_SPECIAL_PASS_OR_CONDITIONAL = 5,
|
/// <summary>
|
||||||
MSH0D_BATCH_USE_MATERIAL_PHASE = 8,
|
/// Special/immediate submit path in CShade::SubmitMeshPieceBatches.
|
||||||
MSH0D_BATCH_SUPPRESS_BATCH = 32,
|
/// </summary>
|
||||||
MSH0D_BATCH_FORCE_PASS2 = 256,
|
SpecialSubmit = 0x0001,
|
||||||
MSH0D_BATCH_EMULATE_POINT_LIGHTS = 2048,
|
|
||||||
MSH0D_BATCH_HAS_LIGHTMAP_OR_TEXCOORD1 = 8192,
|
/// <summary>
|
||||||
MSH0D_BATCH_FACING_TEST_EARLY_OUT = 16384,
|
/// Intersection path tries both facing directions / disables facing test.
|
||||||
|
/// Also likely related to two-sided handling.
|
||||||
|
/// </summary>
|
||||||
|
DisableFacingTest = 0x0002,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Use material phase path instead of normal material animation frame.
|
||||||
|
/// </summary>
|
||||||
|
UseMaterialPhase = 0x0004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Special pass / conditional batch logic.
|
||||||
|
/// In GetBatchRenderData this participates in conditional suppression logic.
|
||||||
|
/// </summary>
|
||||||
|
SpecialPassOrConditional = 0x0008,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Batch is suppressed when paired with SpecialPassOrConditional.
|
||||||
|
/// </summary>
|
||||||
|
SuppressBatch = 0x0020,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forces second/translucent-ish render pass. Exact render-state meaning still provisional.
|
||||||
|
/// </summary>
|
||||||
|
ForcePass2 = 0x0100,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables point-light emulation path.
|
||||||
|
/// </summary>
|
||||||
|
EmulatePointLights = 0x0800,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Batch has lightmap / secondary texcoord related data.
|
||||||
|
/// </summary>
|
||||||
|
HasLightmapOrTexcoord1 = 0x2000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Facing/culling mode bit. Exact render meaning is still provisional.
|
||||||
|
/// </summary>
|
||||||
|
FacingTestEarlyOut = 0x4000,
|
||||||
}
|
}
|
||||||
+210
-227
@@ -7,325 +7,308 @@ namespace ParkanPlayground;
|
|||||||
public enum MshType
|
public enum MshType
|
||||||
{
|
{
|
||||||
Unknown,
|
Unknown,
|
||||||
Model, // Has component 06 (indices), 0D (batches), 07
|
/// <summary>
|
||||||
Landscape // Has component 0B (per-triangle material), uses 15 directly
|
/// Для обычной модели минимальный геометрический путь сейчас выглядит так:
|
||||||
|
/// 0x01 node
|
||||||
|
/// -> 0x02 geometry slot
|
||||||
|
/// -> 0x0D batch
|
||||||
|
/// -> 0x06 indices
|
||||||
|
/// -> 0x03 positions
|
||||||
|
/// </summary>
|
||||||
|
Model,
|
||||||
|
Landscape
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MshConverter
|
public sealed class MshConverter
|
||||||
{
|
{
|
||||||
/// <summary>Определяет тип MSH по набору hex-компонентов архива.</summary>
|
public void Convert(string mshPath, string? outputPath = null, int lod = 0, int group = 0)
|
||||||
public static MshType DetectMeshType(NResArchive archive)
|
|
||||||
{
|
{
|
||||||
bool hasComponent06 = archive.Files.Any(f => f.FileType == "06 00 00 00");
|
var result = NResParser.ReadFile(mshPath);
|
||||||
bool hasComponent0B = archive.Files.Any(f => f.FileType == "0B 00 00 00");
|
if (result.Archive is null)
|
||||||
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>Конвертирует .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)
|
|
||||||
{
|
{
|
||||||
Console.WriteLine($"ERROR: Failed to read NRes archive: {mshNresResult.Error}");
|
Console.WriteLine($"ERROR: Failed to read NRes archive: {result.Error}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var archive = mshNresResult.Archive;
|
var archive = result.Archive;
|
||||||
var meshType = DetectMeshType(archive);
|
var meshType = DetectMeshType(archive);
|
||||||
|
|
||||||
outputPath ??= Path.ChangeExtension(mshPath, ".obj");
|
outputPath ??= Path.ChangeExtension(mshPath, ".obj");
|
||||||
|
|
||||||
Console.WriteLine($"Converting: {Path.GetFileName(mshPath)}");
|
Console.WriteLine($"Converting: {Path.GetFileName(mshPath)}");
|
||||||
Console.WriteLine($"Detected type: {meshType}");
|
Console.WriteLine($"Detected type: {meshType}");
|
||||||
|
Console.WriteLine($"LOD: {lod}, group: {group}");
|
||||||
|
|
||||||
using var fs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
using var fs = new FileStream(mshPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
|
||||||
switch (meshType)
|
switch (meshType)
|
||||||
{
|
{
|
||||||
case MshType.Model:
|
case MshType.Model:
|
||||||
ConvertModel(fs, archive, outputPath, lodLevel, group);
|
ConvertModel(fs, archive, outputPath, lod, group);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MshType.Landscape:
|
case MshType.Landscape:
|
||||||
ConvertLandscape(fs, archive, outputPath, lodLevel, group);
|
ConvertLandscape(fs, archive, outputPath, lod, group);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Console.WriteLine("ERROR: Unknown mesh type, cannot convert.");
|
Console.WriteLine("ERROR: Unknown or unsupported MSH type.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static MshType DetectMeshType(NResArchive archive)
|
||||||
/// Конвертирует обычную модель в OBJ.
|
|
||||||
/// Путь данных: 0x01 -> 0x02 -> 0x0D -> 0x06 -> 0x03.
|
|
||||||
/// </summary>
|
|
||||||
private void ConvertModel(FileStream fs, NResArchive archive, string outputPath, int lodLevel, int group)
|
|
||||||
{
|
{
|
||||||
var component01 = Msh0x01.ReadComponent(fs, archive);
|
var has03 = HasComponent(archive, "03");
|
||||||
var component02 = Msh0x02.ReadComponent(fs, archive);
|
var has06 = HasComponent(archive, "06");
|
||||||
var component03 = Msh0x03.ReadComponent(fs, archive);
|
var has0D = HasComponent(archive, "0D");
|
||||||
var component06 = Msh0x06.ReadComponent(fs, archive);
|
var has15 = HasComponent(archive, "15");
|
||||||
var component07 = Msh0x07.ReadComponent(fs, archive);
|
|
||||||
var component0D = Msh0x0D.ReadComponent(fs, archive);
|
|
||||||
|
|
||||||
Console.WriteLine($"Vertices: {component03.Count}");
|
if (has03 && has06 && has0D)
|
||||||
Console.WriteLine($"Pieces: {component01.Elements.Count}");
|
|
||||||
Console.WriteLine($"Submeshes: {component02.Elements.Count}");
|
|
||||||
|
|
||||||
using var sw = new StreamWriter(outputPath, false, new UTF8Encoding(false));
|
|
||||||
sw.WriteLine($"# Model mesh converted from {Path.GetFileName(outputPath)}");
|
|
||||||
sw.WriteLine($"# LOD level: {lodLevel}");
|
|
||||||
|
|
||||||
foreach (var v in component03)
|
|
||||||
{
|
{
|
||||||
sw.WriteLine(FormattableString.Invariant($"v {v.X:F6} {v.Y:F6} {v.Z:F6}"));
|
return MshType.Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
int exportedFaces = 0;
|
if (has03 && has15 && !has06)
|
||||||
|
|
||||||
for (var pieceIndex = 0; pieceIndex < component01.Elements.Count; pieceIndex++)
|
|
||||||
{
|
{
|
||||||
var piece = component01.Elements[pieceIndex];
|
return MshType.Landscape;
|
||||||
|
}
|
||||||
|
|
||||||
var submeshIdx = piece.ResolveSlotIndex(lodLevel, group);
|
return MshType.Unknown;
|
||||||
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
|
}
|
||||||
continue;
|
|
||||||
|
|
||||||
sw.WriteLine($"g piece_{pieceIndex}");
|
private static bool HasComponent(NResArchive archive, string hexType)
|
||||||
|
{
|
||||||
|
return archive.Files.Any(x => x.FileType.Equals($"{hexType} 00 00 00", StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
var submesh = component02.Elements[submeshIdx];
|
private static void ConvertModel(
|
||||||
var batchStart = submesh.BatchStart;
|
FileStream fs,
|
||||||
var batchCount = submesh.BatchCount;
|
NResArchive archive,
|
||||||
if (batchStart + batchCount > component0D.Count)
|
string outputPath,
|
||||||
|
int lod,
|
||||||
|
int group)
|
||||||
|
{
|
||||||
|
var nodes = Msh0x01.ReadComponent(fs, archive);
|
||||||
|
var geometry = Msh0x02.ReadComponent(fs, archive);
|
||||||
|
var vertices = Msh0x03.ReadComponent(fs, archive);
|
||||||
|
var indices = Msh0x06.ReadComponent(fs, archive);
|
||||||
|
var batches = Msh0x0D.ReadComponent(fs, archive);
|
||||||
|
|
||||||
|
using var writer = CreateObjWriter(outputPath);
|
||||||
|
|
||||||
|
writer.WriteLine($"# MSH model converted from {Path.GetFileName(outputPath)}");
|
||||||
|
writer.WriteLine($"# LOD: {lod}, group: {group}");
|
||||||
|
writer.WriteLine($"# Nodes: {nodes.Nodes.Count}");
|
||||||
|
writer.WriteLine($"# Geometry slots: {geometry.Slots.Count}");
|
||||||
|
writer.WriteLine($"# Batches: {batches.Count}");
|
||||||
|
writer.WriteLine($"# Vertices: {vertices.Count}");
|
||||||
|
writer.WriteLine();
|
||||||
|
|
||||||
|
WriteVertices(writer, vertices);
|
||||||
|
|
||||||
|
var exportedFaces = 0;
|
||||||
|
var skippedSlots = 0;
|
||||||
|
var skippedBatches = 0;
|
||||||
|
var skippedFaces = 0;
|
||||||
|
|
||||||
|
for (var pieceIndex = 0; pieceIndex < nodes.Nodes.Count; pieceIndex++)
|
||||||
|
{
|
||||||
|
var node = nodes.Nodes[pieceIndex];
|
||||||
|
var slotIndex = node.ResolveSlotIndex(lod, group);
|
||||||
|
|
||||||
|
if (slotIndex == ushort.MaxValue)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"WARNING: Batch range {batchStart}:{batchCount} out of range for piece {pieceIndex}");
|
skippedSlots++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var batchIdx = 0; batchIdx < batchCount; batchIdx++)
|
if (slotIndex >= geometry.Slots.Count)
|
||||||
{
|
{
|
||||||
var batch = component0D[batchStart + batchIdx];
|
Warn($"Piece {pieceIndex}: geometry slot {slotIndex} out of range");
|
||||||
var baseVertex = (int)batch.BaseVertex;
|
skippedSlots++;
|
||||||
var indexStart = (int)batch.IndexStart;
|
continue;
|
||||||
var indexCount = batch.IndexCount;
|
}
|
||||||
if (indexStart + indexCount > component06.Count)
|
|
||||||
|
var slot = geometry.Slots[slotIndex];
|
||||||
|
|
||||||
|
if (!slot.HasBatches)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot.BatchEndExclusive0x0D > batches.Count)
|
||||||
|
{
|
||||||
|
Warn($"Piece {pieceIndex}: batch range {slot.BatchStart0x0D}:{slot.BatchCount0x0D} out of range");
|
||||||
|
skippedBatches++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine($"g piece_{pieceIndex}_slot_{slotIndex}");
|
||||||
|
|
||||||
|
for (var batchIndex = slot.BatchStart0x0D; batchIndex < slot.BatchEndExclusive0x0D; batchIndex++)
|
||||||
|
{
|
||||||
|
var batch = batches[batchIndex];
|
||||||
|
|
||||||
|
if (batch.IndexStart + batch.IndexCount > indices.Count)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"WARNING: Index range {indexStart}:{indexCount} out of range for piece {pieceIndex}");
|
Warn($"Piece {pieceIndex}, batch {batchIndex}: index range {batch.IndexStart}:{batch.IndexCount} out of range");
|
||||||
|
skippedBatches++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < indexCount; i += 3)
|
writer.WriteLine($"# batch {batchIndex}, material {batch.MaterialIndex}, flags {batch.Flags}");
|
||||||
{
|
|
||||||
if (i + 2 >= indexCount)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"WARNING: Batch has non-triangle index tail in piece {pieceIndex}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var i1 = baseVertex + component06[indexStart + i];
|
for (var i = 0; i + 2 < batch.IndexCount; i += 3)
|
||||||
var i2 = baseVertex + component06[indexStart + i + 1];
|
{
|
||||||
var i3 = baseVertex + component06[indexStart + i + 2];
|
var indexBase = (int)batch.IndexStart + i;
|
||||||
if (i1 >= component03.Count || i2 >= component03.Count || i3 >= component03.Count)
|
|
||||||
|
var v1 = checked((int)batch.BaseVertex + indices[indexBase + 0]);
|
||||||
|
var v2 = checked((int)batch.BaseVertex + indices[indexBase + 1]);
|
||||||
|
var v3 = checked((int)batch.BaseVertex + indices[indexBase + 2]);
|
||||||
|
|
||||||
|
if (!IsValidTriangle(vertices.Count, v1, v2, v3))
|
||||||
{
|
{
|
||||||
Console.WriteLine($"WARNING: Vertex index out of range in piece {pieceIndex}");
|
skippedFaces++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.WriteLine($"f {i1 + 1} {i2 + 1} {i3 + 1}");
|
WriteFace(writer, v1, v2, v3);
|
||||||
exportedFaces++;
|
exportedFaces++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (batch.IndexCount % 3 != 0)
|
||||||
|
{
|
||||||
|
Warn($"Piece {pieceIndex}, batch {batchIndex}: index count {batch.IndexCount} is not divisible by 3");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
|
Console.WriteLine($"Exported: {vertices.Count} vertices, {exportedFaces} faces");
|
||||||
|
Console.WriteLine($"Skipped slots: {skippedSlots}, skipped batches: {skippedBatches}, skipped faces: {skippedFaces}");
|
||||||
Console.WriteLine($"Output: {outputPath}");
|
Console.WriteLine($"Output: {outputPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static void ConvertLandscape(
|
||||||
/// Конвертирует terrain mesh в OBJ.
|
FileStream fs,
|
||||||
/// Путь данных является terrain-гипотезой проекта: 0x01 -> 0x02 -> 0x15.
|
NResArchive archive,
|
||||||
/// </summary>
|
string outputPath,
|
||||||
private void ConvertLandscape(FileStream fs, NResArchive archive, string outputPath, int lodLevel, int group)
|
int lod,
|
||||||
|
int group)
|
||||||
{
|
{
|
||||||
var component01 = Msh0x01.ReadComponent(fs, archive);
|
var nodes = Msh0x01.ReadComponent(fs, archive);
|
||||||
var component02 = Msh0x02.ReadComponent(fs, archive);
|
var geometry = Msh0x02.ReadComponent(fs, archive);
|
||||||
var component03 = Msh0x03.ReadComponent(fs, archive);
|
var vertices = Msh0x03.ReadComponent(fs, archive);
|
||||||
var component15 = Msh0x15.ReadComponent(fs, archive);
|
var triangles = Msh0x15.ReadComponent(fs, archive);
|
||||||
|
|
||||||
Console.WriteLine($"Vertices: {component03.Count}");
|
using var writer = CreateObjWriter(outputPath);
|
||||||
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));
|
writer.WriteLine($"# MSH landscape converted from {Path.GetFileName(outputPath)}");
|
||||||
sw.WriteLine($"# Landscape mesh converted from {Path.GetFileName(outputPath)}");
|
writer.WriteLine($"# LOD: {lod}, group: {group}");
|
||||||
sw.WriteLine($"# LOD level: {lodLevel}");
|
writer.WriteLine($"# Nodes/tiles: {nodes.Nodes.Count}");
|
||||||
sw.WriteLine($"# Tile grid: {(int)Math.Sqrt(component01.Elements.Count)}x{(int)Math.Sqrt(component01.Elements.Count)}");
|
writer.WriteLine($"# Geometry slots: {geometry.Slots.Count}");
|
||||||
|
writer.WriteLine($"# Triangles 0x15: {triangles.Count}");
|
||||||
|
writer.WriteLine($"# Vertices: {vertices.Count}");
|
||||||
|
writer.WriteLine();
|
||||||
|
|
||||||
foreach (var v in component03)
|
WriteVertices(writer, vertices);
|
||||||
|
|
||||||
|
var exportedFaces = 0;
|
||||||
|
var skippedSlots = 0;
|
||||||
|
var skippedFaces = 0;
|
||||||
|
|
||||||
|
for (var tileIndex = 0; tileIndex < nodes.Nodes.Count; tileIndex++)
|
||||||
{
|
{
|
||||||
sw.WriteLine(FormattableString.Invariant($"v {v.X:F6} {v.Y:F6} {v.Z:F6}"));
|
var node = nodes.Nodes[tileIndex];
|
||||||
}
|
var slotIndex = node.ResolveSlotIndex(lod, group);
|
||||||
|
|
||||||
int exportedFaces = 0;
|
if (slotIndex == ushort.MaxValue)
|
||||||
|
|
||||||
for (var tileIdx = 0; tileIdx < component01.Elements.Count; tileIdx++)
|
|
||||||
{
|
|
||||||
var tile = component01.Elements[tileIdx];
|
|
||||||
|
|
||||||
var submeshIdx = tile.ResolveSlotIndex(lodLevel, group);
|
|
||||||
if (submeshIdx == 0xFFFF || submeshIdx >= component02.Elements.Count)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
sw.WriteLine($"g tile_{tileIdx}");
|
|
||||||
|
|
||||||
var submesh = component02.Elements[submeshIdx];
|
|
||||||
|
|
||||||
var triangleStart = submesh.TriStart;
|
|
||||||
var triangleCount = submesh.TriCount;
|
|
||||||
|
|
||||||
for (var triOffset = 0; triOffset < triangleCount; triOffset++)
|
|
||||||
{
|
{
|
||||||
var triIdx = triangleStart + triOffset;
|
skippedSlots++;
|
||||||
if (triIdx >= component15.Count)
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slotIndex >= geometry.Slots.Count)
|
||||||
|
{
|
||||||
|
Warn($"Tile {tileIndex}: geometry slot {slotIndex} out of range");
|
||||||
|
skippedSlots++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var slot = geometry.Slots[slotIndex];
|
||||||
|
|
||||||
|
if (!slot.HasTriangles)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteLine();
|
||||||
|
writer.WriteLine($"g tile_{tileIndex}_slot_{slotIndex}");
|
||||||
|
|
||||||
|
for (var triIndex = slot.TriStart0x07; triIndex < slot.TriEndExclusive0x07; triIndex++)
|
||||||
|
{
|
||||||
|
if (triIndex >= triangles.Count)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"WARNING: Triangle index {triIdx} out of range for tile {tileIdx}");
|
Warn($"Tile {tileIndex}: triangle {triIndex} out of range");
|
||||||
|
skippedFaces++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tri = component15[triIdx];
|
var tri = triangles[triIndex];
|
||||||
if (tri.Vertex1Index >= component03.Count || tri.Vertex2Index >= component03.Count || tri.Vertex3Index >= component03.Count)
|
|
||||||
|
var v1 = tri.Vertex1Index;
|
||||||
|
var v2 = tri.Vertex2Index;
|
||||||
|
var v3 = tri.Vertex3Index;
|
||||||
|
|
||||||
|
if (!IsValidTriangle(vertices.Count, v1, v2, v3))
|
||||||
{
|
{
|
||||||
Console.WriteLine($"WARNING: Vertex index out of range for tile {tileIdx}");
|
skippedFaces++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
sw.WriteLine($"f {tri.Vertex1Index + 1} {tri.Vertex2Index + 1} {tri.Vertex3Index + 1}");
|
WriteFace(writer, v1, v2, v3);
|
||||||
exportedFaces++;
|
exportedFaces++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"Exported: {component03.Count} vertices, {exportedFaces} faces");
|
Console.WriteLine($"Exported: {vertices.Count} vertices, {exportedFaces} faces");
|
||||||
|
Console.WriteLine($"Skipped slots: {skippedSlots}, skipped faces: {skippedFaces}");
|
||||||
Console.WriteLine($"Output: {outputPath}");
|
Console.WriteLine($"Output: {outputPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Face(Vector3 P1, Vector3 P2, Vector3 P3);
|
private static StreamWriter CreateObjWriter(string outputPath)
|
||||||
|
|
||||||
public static void ExportCube(string filePath, Vector3[] points)
|
|
||||||
{
|
{
|
||||||
if (points.Length != 8)
|
return new StreamWriter(outputPath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||||
throw new ArgumentException("Cube must have exactly 8 points.");
|
}
|
||||||
|
|
||||||
using (StreamWriter writer = new StreamWriter(filePath))
|
private static void WriteVertices(StreamWriter writer, IReadOnlyList<Vector3> vertices)
|
||||||
|
{
|
||||||
|
foreach (var vertex in vertices)
|
||||||
{
|
{
|
||||||
// Запись вершин.
|
writer.WriteLine(FormattableString.Invariant($"v {vertex.X:F6} {vertex.Y:F6} {vertex.Z:F6}"));
|
||||||
foreach (var p in points)
|
|
||||||
{
|
|
||||||
writer.WriteLine($"v {p.X} {p.Y} {p.Z}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Запись граней: каждая грань задается четырьмя вершинами, OBJ использует индексацию с 1.
|
|
||||||
int[][] faces = new int[][]
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
writer.WriteLine($"f {f[0]} {f[1]} {f[2]} {f[3]}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ExportCubesAtPositions(string filePath, List<Vector3> centers, float size = 2f)
|
private static void WriteFace(StreamWriter writer, int v1, int v2, int v3)
|
||||||
{
|
{
|
||||||
float half = size / 2f;
|
writer.WriteLine($"f {v1 + 1} {v2 + 1} {v3 + 1}");
|
||||||
using (StreamWriter writer = new StreamWriter(filePath))
|
|
||||||
{
|
|
||||||
int vertexOffset = 0;
|
|
||||||
|
|
||||||
foreach (var c in centers)
|
|
||||||
{
|
|
||||||
// Генерация восьми вершин куба.
|
|
||||||
Vector3[] vertices = new Vector3[]
|
|
||||||
{
|
|
||||||
new Vector3(c.X - half, c.Y - half, c.Z - half),
|
|
||||||
new Vector3(c.X + half, c.Y - half, c.Z - half),
|
|
||||||
new Vector3(c.X + half, c.Y - half, c.Z + half),
|
|
||||||
new Vector3(c.X - half, c.Y - half, c.Z + half),
|
|
||||||
|
|
||||||
new Vector3(c.X - half, c.Y + half, c.Z - half),
|
|
||||||
new Vector3(c.X + half, c.Y + half, c.Z - half),
|
|
||||||
new Vector3(c.X + half, c.Y + half, c.Z + half),
|
|
||||||
new Vector3(c.X - half, c.Y + half, c.Z + half)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Запись вершин.
|
|
||||||
foreach (var v in vertices)
|
|
||||||
{
|
|
||||||
writer.WriteLine($"v {v.X} {v.Y} {v.Z}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Описание граней: индексация с 1, порядок против часовой стрелки.
|
|
||||||
int[][] faces = new int[][]
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
writer.WriteLine(
|
|
||||||
$"f {f[0] + vertexOffset} {f[1] + vertexOffset} {f[2] + vertexOffset} {f[3] + vertexOffset}");
|
|
||||||
}
|
|
||||||
|
|
||||||
vertexOffset += 8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Export(string filePath, IEnumerable<Vector3> vertices, List<IndexedEdge> edges)
|
private static bool IsValidTriangle(int vertexCount, int v1, int v2, int v3)
|
||||||
{
|
{
|
||||||
using (var writer = new StreamWriter(filePath))
|
return IsValidVertexIndex(vertexCount, v1)
|
||||||
{
|
&& IsValidVertexIndex(vertexCount, v2)
|
||||||
writer.WriteLine("# Exported OBJ file");
|
&& IsValidVertexIndex(vertexCount, v3);
|
||||||
|
}
|
||||||
|
|
||||||
// Запись вершин.
|
private static bool IsValidVertexIndex(int vertexCount, int index)
|
||||||
foreach (var v in vertices)
|
{
|
||||||
{
|
return index >= 0 && index < vertexCount;
|
||||||
writer.WriteLine($"v {v.X:F2} {v.Y:F2} {v.Z:F2}");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Запись ребер как line-элементов OBJ.
|
private static void Warn(string message)
|
||||||
foreach (var e in edges)
|
{
|
||||||
{
|
Console.WriteLine($"WARNING: {message}");
|
||||||
// OBJ использует индексацию с 1.
|
|
||||||
writer.WriteLine($"l {e.Index1 + 1} {e.Index2 + 1}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -392,8 +392,8 @@ color
|
|||||||
- `0x22` - unknown (implement by CLandscape)
|
- `0x22` - unknown (implement by CLandscape)
|
||||||
- `0x23` - IGameSettingsRoot
|
- `0x23` - IGameSettingsRoot
|
||||||
- `0x24` - IGameObject2
|
- `0x24` - IGameObject2
|
||||||
- `0x25` - unknown (implemented by CAniMesh)
|
- `0x25` - ICollisionMesh (придумал сам implemented by CAniMesh)
|
||||||
- `0x26` - unknown (implemented by CAniMesh and CControl and CWizard)
|
- `0x26` - IScalable (придумал сам implemented by CAniMesh and CControl and CWizard)
|
||||||
- `0x28` - ICollObject
|
- `0x28` - ICollObject
|
||||||
- `0x29` - IPhysicalModel
|
- `0x29` - IPhysicalModel
|
||||||
- `0x101` - I3DRender
|
- `0x101` - I3DRender
|
||||||
|
|||||||
Reference in New Issue
Block a user