forked from Eauldane/SnowcloakClient
259 lines
8.5 KiB
C#
259 lines
8.5 KiB
C#
using Lumina.Data;
|
|
using Lumina.Extensions;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using static Lumina.Data.Parsing.MdlStructs;
|
|
|
|
namespace MareSynchronos.Interop.GameModel;
|
|
|
|
#pragma warning disable S1104 // Fields should not have public accessibility
|
|
|
|
// This code is completely and shamelessly borrowed from Penumbra to load V5 and V6 model files.
|
|
// Original Source: https://github.com/Ottermandias/Penumbra.GameData/blob/main/Files/MdlFile.cs
|
|
public class MdlFile
|
|
{
|
|
public const int V5 = 0x01000005;
|
|
public const int V6 = 0x01000006;
|
|
public const uint NumVertices = 17;
|
|
public const uint FileHeaderSize = 0x44;
|
|
|
|
// Raw data to write back.
|
|
public uint Version = 0x01000005;
|
|
public float Radius;
|
|
public float ModelClipOutDistance;
|
|
public float ShadowClipOutDistance;
|
|
public byte BgChangeMaterialIndex;
|
|
public byte BgCrestChangeMaterialIndex;
|
|
public ushort CullingGridCount;
|
|
public byte Flags3;
|
|
public byte Unknown6;
|
|
public ushort Unknown8;
|
|
public ushort Unknown9;
|
|
|
|
// Offsets are stored relative to RuntimeSize instead of file start.
|
|
public uint[] VertexOffset = [0, 0, 0];
|
|
public uint[] IndexOffset = [0, 0, 0];
|
|
|
|
public uint[] VertexBufferSize = [0, 0, 0];
|
|
public uint[] IndexBufferSize = [0, 0, 0];
|
|
public byte LodCount;
|
|
public bool EnableIndexBufferStreaming;
|
|
public bool EnableEdgeGeometry;
|
|
|
|
public ModelFlags1 Flags1;
|
|
public ModelFlags2 Flags2;
|
|
|
|
public VertexDeclarationStruct[] VertexDeclarations = [];
|
|
public ElementIdStruct[] ElementIds = [];
|
|
public MeshStruct[] Meshes = [];
|
|
public BoundingBoxStruct[] BoneBoundingBoxes = [];
|
|
public LodStruct[] Lods = [];
|
|
public ExtraLodStruct[] ExtraLods = [];
|
|
|
|
public MdlFile(string filePath)
|
|
{
|
|
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
|
using var r = new LuminaBinaryReader(stream);
|
|
|
|
var header = LoadModelFileHeader(r);
|
|
LodCount = header.LodCount;
|
|
VertexBufferSize = header.VertexBufferSize;
|
|
IndexBufferSize = header.IndexBufferSize;
|
|
VertexOffset = header.VertexOffset;
|
|
IndexOffset = header.IndexOffset;
|
|
|
|
var dataOffset = FileHeaderSize + header.RuntimeSize + header.StackSize;
|
|
for (var i = 0; i < LodCount; ++i)
|
|
{
|
|
VertexOffset[i] -= dataOffset;
|
|
IndexOffset[i] -= dataOffset;
|
|
}
|
|
|
|
VertexDeclarations = new VertexDeclarationStruct[header.VertexDeclarationCount];
|
|
for (var i = 0; i < header.VertexDeclarationCount; ++i)
|
|
VertexDeclarations[i] = VertexDeclarationStruct.Read(r);
|
|
|
|
_ = LoadStrings(r);
|
|
|
|
var modelHeader = LoadModelHeader(r);
|
|
ElementIds = new ElementIdStruct[modelHeader.ElementIdCount];
|
|
for (var i = 0; i < modelHeader.ElementIdCount; i++)
|
|
ElementIds[i] = ElementIdStruct.Read(r);
|
|
|
|
Lods = new LodStruct[3];
|
|
for (var i = 0; i < 3; i++)
|
|
{
|
|
var lod = r.ReadStructure<LodStruct>();
|
|
if (i < LodCount)
|
|
{
|
|
lod.VertexDataOffset -= dataOffset;
|
|
lod.IndexDataOffset -= dataOffset;
|
|
}
|
|
|
|
Lods[i] = lod;
|
|
}
|
|
|
|
ExtraLods = modelHeader.Flags2.HasFlag(ModelFlags2.ExtraLodEnabled)
|
|
? r.ReadStructuresAsArray<ExtraLodStruct>(3)
|
|
: [];
|
|
|
|
Meshes = new MeshStruct[modelHeader.MeshCount];
|
|
for (var i = 0; i < modelHeader.MeshCount; i++)
|
|
Meshes[i] = MeshStruct.Read(r);
|
|
}
|
|
|
|
private ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r)
|
|
{
|
|
var header = ModelFileHeader.Read(r);
|
|
Version = header.Version;
|
|
EnableIndexBufferStreaming = header.EnableIndexBufferStreaming;
|
|
EnableEdgeGeometry = header.EnableEdgeGeometry;
|
|
return header;
|
|
}
|
|
|
|
private ModelHeader LoadModelHeader(BinaryReader r)
|
|
{
|
|
var modelHeader = r.ReadStructure<ModelHeader>();
|
|
Radius = modelHeader.Radius;
|
|
Flags1 = modelHeader.Flags1;
|
|
Flags2 = modelHeader.Flags2;
|
|
ModelClipOutDistance = modelHeader.ModelClipOutDistance;
|
|
ShadowClipOutDistance = modelHeader.ShadowClipOutDistance;
|
|
CullingGridCount = modelHeader.CullingGridCount;
|
|
Flags3 = modelHeader.Flags3;
|
|
Unknown6 = modelHeader.Unknown6;
|
|
Unknown8 = modelHeader.Unknown8;
|
|
Unknown9 = modelHeader.Unknown9;
|
|
BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex;
|
|
BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex;
|
|
|
|
return modelHeader;
|
|
}
|
|
|
|
private static (uint[], string[]) LoadStrings(BinaryReader r)
|
|
{
|
|
var stringCount = r.ReadUInt16();
|
|
r.ReadUInt16();
|
|
var stringSize = (int)r.ReadUInt32();
|
|
var stringData = r.ReadBytes(stringSize);
|
|
var start = 0;
|
|
var strings = new string[stringCount];
|
|
var offsets = new uint[stringCount];
|
|
for (var i = 0; i < stringCount; ++i)
|
|
{
|
|
var span = stringData.AsSpan(start);
|
|
var idx = span.IndexOf((byte)'\0');
|
|
strings[i] = Encoding.UTF8.GetString(span[..idx]);
|
|
offsets[i] = (uint)start;
|
|
start = start + idx + 1;
|
|
}
|
|
|
|
return (offsets, strings);
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public unsafe struct ModelHeader
|
|
{
|
|
// MeshHeader
|
|
public float Radius;
|
|
public ushort MeshCount;
|
|
public ushort AttributeCount;
|
|
public ushort SubmeshCount;
|
|
public ushort MaterialCount;
|
|
public ushort BoneCount;
|
|
public ushort BoneTableCount;
|
|
public ushort ShapeCount;
|
|
public ushort ShapeMeshCount;
|
|
public ushort ShapeValueCount;
|
|
public byte LodCount;
|
|
public ModelFlags1 Flags1;
|
|
public ushort ElementIdCount;
|
|
public byte TerrainShadowMeshCount;
|
|
public ModelFlags2 Flags2;
|
|
public float ModelClipOutDistance;
|
|
public float ShadowClipOutDistance;
|
|
public ushort CullingGridCount;
|
|
public ushort TerrainShadowSubmeshCount;
|
|
public byte Flags3;
|
|
public byte BGChangeMaterialIndex;
|
|
public byte BGCrestChangeMaterialIndex;
|
|
public byte Unknown6;
|
|
public ushort BoneTableArrayCountTotal;
|
|
public ushort Unknown8;
|
|
public ushort Unknown9;
|
|
private fixed byte _padding[6];
|
|
}
|
|
|
|
public struct ShapeStruct
|
|
{
|
|
public uint StringOffset;
|
|
public ushort[] ShapeMeshStartIndex;
|
|
public ushort[] ShapeMeshCount;
|
|
|
|
public static ShapeStruct Read(LuminaBinaryReader br)
|
|
{
|
|
ShapeStruct ret = new ShapeStruct();
|
|
ret.StringOffset = br.ReadUInt32();
|
|
ret.ShapeMeshStartIndex = br.ReadUInt16Array(3);
|
|
ret.ShapeMeshCount = br.ReadUInt16Array(3);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
[Flags]
|
|
public enum ModelFlags1 : byte
|
|
{
|
|
DustOcclusionEnabled = 0x80,
|
|
SnowOcclusionEnabled = 0x40,
|
|
RainOcclusionEnabled = 0x20,
|
|
Unknown1 = 0x10,
|
|
LightingReflectionEnabled = 0x08,
|
|
WavingAnimationDisabled = 0x04,
|
|
LightShadowDisabled = 0x02,
|
|
ShadowDisabled = 0x01,
|
|
}
|
|
|
|
[Flags]
|
|
public enum ModelFlags2 : byte
|
|
{
|
|
Unknown2 = 0x80,
|
|
BgUvScrollEnabled = 0x40,
|
|
EnableForceNonResident = 0x20,
|
|
ExtraLodEnabled = 0x10,
|
|
ShadowMaskEnabled = 0x08,
|
|
ForceLodRangeEnabled = 0x04,
|
|
EdgeGeometryEnabled = 0x02,
|
|
Unknown3 = 0x01
|
|
}
|
|
|
|
public struct VertexDeclarationStruct
|
|
{
|
|
// There are always 17, but stop when stream = -1
|
|
public VertexElement[] VertexElements;
|
|
|
|
public static VertexDeclarationStruct Read(LuminaBinaryReader br)
|
|
{
|
|
VertexDeclarationStruct ret = new VertexDeclarationStruct();
|
|
|
|
var elems = new List<VertexElement>();
|
|
|
|
// Read the vertex elements that we need
|
|
var thisElem = br.ReadStructure<VertexElement>();
|
|
do
|
|
{
|
|
elems.Add(thisElem);
|
|
thisElem = br.ReadStructure<VertexElement>();
|
|
} while (thisElem.Stream != 255);
|
|
|
|
// Skip the number of bytes that we don't need to read
|
|
// We skip elems.Count * 9 because we had to read the invalid element
|
|
int toSeek = 17 * 8 - (elems.Count + 1) * 8;
|
|
br.Seek(br.BaseStream.Position + toSeek);
|
|
|
|
ret.VertexElements = elems.ToArray();
|
|
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
#pragma warning restore S1104 // Fields should not have public accessibility |