using System.Numerics; using System.Text; namespace KeepersCompound.LGS; // TODO: Remove all the things that don't actually need to be stored public class ModelFile { public readonly struct BHeader { public string Signature { get; } public int Version { get; } public BHeader(BinaryReader reader) { Signature = reader.ReadNullString(4); Version = reader.ReadInt32(); } } public readonly struct MHeader { public string Name { get; } public float Radius { get; } public float MaxPolygonRadius { get; } public Vector3 MaxBounds { get; } public Vector3 MinBounds { get; } public Vector3 Center { get; } public ushort PolygonCount { get; } public ushort VertexCount { get; } public ushort ParameterCount { get; } public byte MaterialCount { get; } public byte VCallCount { get; } public byte VHotCount { get; } public byte ObjectCount { get; } public uint ObjectOffset { get; } public uint MaterialOffset { get; } public uint UvOffset { get; } public uint VHotOffset { get; } public uint VertexOffset { get; } public uint LightOffset { get; } public uint NormalOffset { get; } public uint PolygonOffset { get; } public uint NodeOffset { get; } public uint ModelSize { get; } public uint AuxMaterialFlags { get; } public uint AuxMaterialOffset { get; } public uint AuxMaterialSize { get; } public MHeader(BinaryReader reader, int version) { Name = reader.ReadNullString(8); Radius = reader.ReadSingle(); MaxPolygonRadius = reader.ReadSingle(); MaxBounds = reader.ReadVec3(); MinBounds = reader.ReadVec3(); Center = reader.ReadVec3(); PolygonCount = reader.ReadUInt16(); VertexCount = reader.ReadUInt16(); ParameterCount = reader.ReadUInt16(); MaterialCount = reader.ReadByte(); VCallCount = reader.ReadByte(); VHotCount = reader.ReadByte(); ObjectCount = reader.ReadByte(); ObjectOffset = reader.ReadUInt32(); MaterialOffset = reader.ReadUInt32(); UvOffset = reader.ReadUInt32(); VHotOffset = reader.ReadUInt32(); VertexOffset = reader.ReadUInt32(); LightOffset = reader.ReadUInt32(); NormalOffset = reader.ReadUInt32(); PolygonOffset = reader.ReadUInt32(); NodeOffset = reader.ReadUInt32(); ModelSize = reader.ReadUInt32(); if (version == 4) { AuxMaterialFlags = reader.ReadUInt32(); AuxMaterialOffset = reader.ReadUInt32(); AuxMaterialSize = reader.ReadUInt32(); } else { AuxMaterialFlags = 0; AuxMaterialOffset = 0; AuxMaterialSize = 0; } } } public struct SubObject { public string Name; public byte Type; public int Joint; public float MinJointValue; public float MaxJointValue; public Matrix4x4 Transform; public short Child; public short Next; public ushort VhotIdx; public ushort VhotCount; public ushort PointIdx; public ushort PointCount; public ushort LightIdx; public ushort LightCount; public ushort NormalIdx; public ushort NormalCount; public ushort NodeIdx; public ushort NodeCount; public SubObject(BinaryReader reader) { Name = reader.ReadNullString(8); Type = reader.ReadByte(); Joint = reader.ReadInt32(); MinJointValue = reader.ReadSingle(); MaxJointValue = reader.ReadSingle(); var v1 = reader.ReadVec3(); var v2 = reader.ReadVec3(); var v3 = reader.ReadVec3(); var v4 = reader.ReadVec3(); Transform = new Matrix4x4(v1.X, v1.Y, v1.Z, 0, v2.X, v2.Y, v2.Z, 0, v3.X, v3.Y, v3.Z, 0, v4.X, v4.Y, v4.Z, 1); Child = reader.ReadInt16(); Next = reader.ReadInt16(); VhotIdx = reader.ReadUInt16(); VhotCount = reader.ReadUInt16(); PointIdx = reader.ReadUInt16(); PointCount = reader.ReadUInt16(); LightIdx = reader.ReadUInt16(); LightCount = reader.ReadUInt16(); NormalIdx = reader.ReadUInt16(); NormalCount = reader.ReadUInt16(); NodeIdx = reader.ReadUInt16(); NodeCount = reader.ReadUInt16(); } } public struct Polygon { public ushort Index; public ushort Data; public byte Type; public byte VertexCount; public ushort Normal; public float D; public ushort[] VertexIndices; public ushort[] LightIndices; public ushort[] UvIndices; public byte Material; public Polygon(BinaryReader reader, int version) { Index = reader.ReadUInt16(); Data = reader.ReadUInt16(); Type = reader.ReadByte(); VertexCount = reader.ReadByte(); Normal = reader.ReadUInt16(); D = reader.ReadSingle(); VertexIndices = new ushort[VertexCount]; for (var i = 0; i < VertexCount; i++) { VertexIndices[i] = reader.ReadUInt16(); } LightIndices = new ushort[VertexCount]; for (var i = 0; i < VertexCount; i++) { LightIndices[i] = reader.ReadUInt16(); } UvIndices = new ushort[Type == 0x1B ? VertexCount : 0]; for (var i = 0; i < UvIndices.Length; i++) { UvIndices[i] = reader.ReadUInt16(); } Material = version == 4 ? reader.ReadByte() : (byte)0; } } public struct Material { public string Name; public byte Type; public byte Slot; public uint Handle; public float Uv; public Material(BinaryReader reader) { Name = reader.ReadNullString(16); Type = reader.ReadByte(); Slot = reader.ReadByte(); Handle = reader.ReadUInt32(); Uv = reader.ReadSingle(); } } public enum VhotId { LightPosition = 1, LightDirection = 8, Anchor = 2, Particle1 = 3, Particle2 = 4, Particle3 = 5, Particle4 = 6, Particle5 = 7, } public struct VHot { public int Id; public Vector3 Position; public VHot(BinaryReader reader) { Id = reader.ReadInt32(); Position = reader.ReadVec3(); } } public BHeader BinHeader { get; set; } public MHeader Header { get; set; } public Vector3[] Vertices { get; } public Vector2[] Uvs { get; } public Vector3[] Normals { get; } public Polygon[] Polygons { get; } public Material[] Materials { get; } public VHot[] VHots { get; } public SubObject[] Objects { get; } public ModelFile(string filename) { if (!File.Exists(filename)) return; using MemoryStream stream = new(File.ReadAllBytes(filename)); using BinaryReader reader = new(stream, Encoding.UTF8, false); BinHeader = new BHeader(reader); if (BinHeader.Signature != "LGMD") return; Header = new MHeader(reader, BinHeader.Version); stream.Seek(Header.VertexOffset, SeekOrigin.Begin); Vertices = new Vector3[Header.VertexCount]; for (var i = 0; i < Vertices.Length; i++) { Vertices[i] = reader.ReadVec3(); } stream.Seek(Header.UvOffset, SeekOrigin.Begin); Uvs = new Vector2[(Header.VHotOffset - Header.UvOffset) / 8]; for (var i = 0; i < Uvs.Length; i++) { Uvs[i] = reader.ReadVec2(); } stream.Seek(Header.NormalOffset, SeekOrigin.Begin); Normals = new Vector3[(Header.PolygonOffset - Header.NormalOffset) / 12]; for (var i = 0; i < Normals.Length; i++) { Normals[i] = reader.ReadVec3(); } stream.Seek(Header.PolygonOffset, SeekOrigin.Begin); Polygons = new Polygon[Header.PolygonCount]; for (var i = 0; i < Polygons.Length; i++) { Polygons[i] = new Polygon(reader, BinHeader.Version); } stream.Seek(Header.MaterialOffset, SeekOrigin.Begin); Materials = new Material[Header.MaterialCount]; for (var i = 0; i < Materials.Length; i++) { Materials[i] = new Material(reader); } stream.Seek(Header.VHotOffset, SeekOrigin.Begin); VHots = new VHot[Header.VHotCount]; for (var i = 0; i < VHots.Length; i++) { VHots[i] = new VHot(reader); } stream.Seek(Header.ObjectOffset, SeekOrigin.Begin); Objects = new SubObject[Header.ObjectCount]; for (var i = 0; i < Objects.Length; i++) { Objects[i] = new SubObject(reader); } } // TODO: Apply transforms to normals and stuff public void ApplyJoints(float[] joints) { // Build map of objects to their parent id var objCount = Objects.Length; var parentIds = new int[objCount]; for (var i = 0; i < objCount; i++) { parentIds[i] = -1; } for (var i = 0; i < objCount; i++) { var subObj = Objects[i]; var childIdx = subObj.Child; while (childIdx != -1) { parentIds[childIdx] = i; childIdx = Objects[childIdx].Next; } } // Calculate base transforms for every subobj (including joint) var subObjTransforms = new Matrix4x4[objCount]; for (var i = 0; i < objCount; i++) { var subObj = Objects[i]; var objTrans = Matrix4x4.Identity; if (subObj.Joint != -1) { var ang = subObj.Joint >= joints.Length ? 0 : float.DegreesToRadians(joints[subObj.Joint]); // TODO: Is this correct? Should I use a manual rotation matrix? var jointRot = Matrix4x4.CreateFromYawPitchRoll(0, ang, 0); objTrans = jointRot * subObj.Transform; } subObjTransforms[i] = objTrans; } // Apply sub object transforms for (var i = 0; i < objCount; i++) { var subObj = Objects[i]; var transform = subObjTransforms[i]; // Build compound transformation var parentId = parentIds[i]; while (parentId != -1) { transform *= subObjTransforms[parentId]; parentId = parentIds[parentId]; } for (var j = 0; j < subObj.VhotCount; j++) { var v = VHots[subObj.VhotIdx + j]; v.Position = Vector3.Transform(v.Position, transform); VHots[subObj.VhotIdx + j] = v; } for (var j = 0; j < subObj.PointCount; j++) { var v = Vertices[subObj.PointIdx + j]; Vertices[subObj.PointIdx + j] = Vector3.Transform(v, transform); } } } public bool TryGetVhot(VhotId id, out VHot vhot) { foreach (var v in VHots) { if (v.Id == (int)id) { vhot = v; return true; } } vhot = new VHot(); return false; } }