diff --git a/src/Lumina/Data/Files/MdlFile.cs b/src/Lumina/Data/Files/MdlFile.cs new file mode 100644 index 00000000..84df29c9 --- /dev/null +++ b/src/Lumina/Data/Files/MdlFile.cs @@ -0,0 +1,98 @@ +using System; +using Lumina.Data.Parsing; +using Lumina.Data.Structs; +using Lumina.Extensions; + +namespace Lumina.Data.Files { + public class MdlFile : FileResource { + public MdlStructs.ModelFileHeader FileHeader; + public MdlStructs.VertexDeclarationStruct[] VertexDeclarations; + public MdlStructs.ModelHeader ModelHeader; + public MdlStructs.ElementIdStruct[] ElementIds; + public MdlStructs.LodStruct[] Lods; + public MdlStructs.ExtraLodStruct[] ExtraLods; + public MdlStructs.MeshStruct[] Meshes; + public uint[] AttributeNameOffsets; + public MdlStructs.SubmeshStruct[] Submeshes; + + public MdlStructs.TerrainShadowMeshStruct[] TerrainShadowMeshes; + public MdlStructs.TerrainShadowSubmeshStruct[] TerrainShadowSubmeshes; + + public uint[] MaterialNameOffsets; + public uint[] BoneNameOffsets; + public MdlStructs.BoneTableStruct[] BoneTables; + public MdlStructs.ShapeStruct[] Shapes; + public MdlStructs.ShapeMeshStruct[] ShapeMeshes; + public MdlStructs.ShapeValueStruct[] ShapeValues; + + public ushort[] SubmeshBoneMap; + public MdlStructs.BoundingBoxStruct BoundingBoxes; + public MdlStructs.BoundingBoxStruct ModelBoundingBoxes; + public MdlStructs.BoundingBoxStruct WaterBoundingBoxes; + public MdlStructs.BoundingBoxStruct VerticalFogBoundingBoxes; + public MdlStructs.BoundingBoxStruct[] BoneBoundingBoxes; + + public ushort StringCount; + public byte[] Strings; + + public override void LoadFile() + { + // We can ensure based on content-type that files are models + if( FileInfo.Type != FileType.Model ) + { + Console.WriteLine( $"Attempted to load {FilePath} of content type {FileInfo.Type} as a model, returning..." ); + return; + } + + FileHeader = MdlStructs.ModelFileHeader.Read( Reader ); + + VertexDeclarations = new MdlStructs.VertexDeclarationStruct[FileHeader.VertexDeclarationCount]; + for( int i = 0; i < FileHeader.VertexDeclarationCount; i++ ) VertexDeclarations[ i ] = MdlStructs.VertexDeclarationStruct.Read( Reader ); + + StringCount = Reader.ReadUInt16(); + Reader.ReadUInt16(); + uint stringSize = Reader.ReadUInt32(); + Strings = Reader.ReadBytes( (int) stringSize ); + + ModelHeader = Reader.ReadStructure(); + ElementIds = new MdlStructs.ElementIdStruct[ModelHeader.ElementIdCount]; + Meshes = new MdlStructs.MeshStruct[ModelHeader.MeshCount]; + BoneTables = new MdlStructs.BoneTableStruct[ModelHeader.BoneTableCount]; + Shapes = new MdlStructs.ShapeStruct[ModelHeader.ShapeCount]; + BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[ModelHeader.BoneCount]; + + for( int i = 0; i < ModelHeader.ElementIdCount; i++ ) ElementIds[ i ] = MdlStructs.ElementIdStruct.Read( Reader ); + Lods = Reader.ReadStructuresAsArray< MdlStructs.LodStruct >( 3 ); + + if( ModelHeader.ExtraLodEnabled ) + ExtraLods = Reader.ReadStructuresAsArray< MdlStructs.ExtraLodStruct >( 3 ); + + for( int i = 0; i < ModelHeader.MeshCount; i++ ) Meshes[ i ] = MdlStructs.MeshStruct.Read( Reader ); + AttributeNameOffsets = Reader.ReadStructures< UInt32 >( ModelHeader.AttributeCount ).ToArray(); + TerrainShadowMeshes = Reader.ReadStructuresAsArray< MdlStructs.TerrainShadowMeshStruct >( ModelHeader.TerrainShadowMeshCount ); + Submeshes = Reader.ReadStructuresAsArray< MdlStructs.SubmeshStruct >( ModelHeader.SubmeshCount ); + TerrainShadowSubmeshes = Reader.ReadStructuresAsArray< MdlStructs.TerrainShadowSubmeshStruct >( ModelHeader.TerrainShadowSubmeshCount ); + + MaterialNameOffsets = Reader.ReadStructures< UInt32 >( ModelHeader.MaterialCount ).ToArray(); + BoneNameOffsets = Reader.ReadStructures< UInt32 >( ModelHeader.BoneCount ).ToArray(); + for( int i = 0; i < ModelHeader.BoneTableCount; i++ ) BoneTables[ i ] = MdlStructs.BoneTableStruct.Read( Reader ); + + for( int i = 0; i < ModelHeader.ShapeCount; i++ ) Shapes[ i ] = MdlStructs.ShapeStruct.Read( Reader ); + ShapeMeshes = Reader.ReadStructuresAsArray< MdlStructs.ShapeMeshStruct >( ModelHeader.ShapeMeshCount ); + ShapeValues = Reader.ReadStructuresAsArray< MdlStructs.ShapeValueStruct >( ModelHeader.ShapeValueCount ); + + uint submeshBoneMapSize = Reader.ReadUInt32(); + SubmeshBoneMap = Reader.ReadStructures< UInt16 >( (int) submeshBoneMapSize / 2 ).ToArray(); + + byte paddingAmount = Reader.ReadByte(); + Reader.Seek( Reader.BaseStream.Position + paddingAmount ); + + // Dunno what this first one is for? + BoundingBoxes = MdlStructs.BoundingBoxStruct.Read( Reader ); + ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( Reader ); + WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( Reader ); + VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( Reader ); + for( int i = 0; i < ModelHeader.BoneCount; i++ ) BoneBoundingBoxes[ i ] = MdlStructs.BoundingBoxStruct.Read( Reader ); + } + } +} \ No newline at end of file diff --git a/src/Lumina/Data/Files/MtrlFile.cs b/src/Lumina/Data/Files/MtrlFile.cs new file mode 100644 index 00000000..89fd756d --- /dev/null +++ b/src/Lumina/Data/Files/MtrlFile.cs @@ -0,0 +1,56 @@ +using System; +using Lumina.Data.Parsing; +using Lumina.Extensions; + +namespace Lumina.Data.Files +{ + public class MtrlFile : FileResource + { + public MaterialFileHeader FileHeader; + public MaterialHeader MaterialHeader; + public UvColorSet[] UvColorSets; + public TextureOffset[] TextureOffsets; + public int[] ColorSetOffsets; + + // Will have to double check this, this is from TexTools + public ColorSetInfo ColorSetInfo; + public ColorSetDyeInfo ColorSetDyeInfo; + + public ShaderKey[] ShaderKeys; + public Constant[] Constants; + public Sampler[] Samplers; + + public float[] ShaderValues; + public byte[] Strings; + + public override void LoadFile() + { + FileHeader = Reader.ReadStructure(); + TextureOffsets = Reader.ReadStructuresAsArray< TextureOffset >( FileHeader.TextureCount ); + + UvColorSets = Reader.ReadStructuresAsArray< UvColorSet >( FileHeader.UvSetCount ); + + ColorSetOffsets = Reader.ReadStructuresAsArray< Int32 >( FileHeader.ColorSetCount ); + + Strings = Reader.ReadBytes( FileHeader.StringTableSize ); + + // This seems to be a struct - do not know what it is + Reader.Seek(Reader.BaseStream.Position + FileHeader.AdditionalDataSize); + + if( FileHeader.DataSetSize > 0 ) + { + ColorSetInfo = Reader.ReadStructure< ColorSetInfo >(); + if (FileHeader.DataSetSize > 512) + ColorSetDyeInfo = Reader.ReadStructure< ColorSetDyeInfo >(); + } + + MaterialHeader = Reader.ReadStructure< MaterialHeader >(); + + ShaderKeys = Reader.ReadStructuresAsArray< ShaderKey >( MaterialHeader.ShaderKeyCount ); + Constants = Reader.ReadStructuresAsArray< Constant >( MaterialHeader.ConstantCount ); + Samplers = Reader.ReadStructuresAsArray< Sampler >( MaterialHeader.SamplerCount ); + + ShaderValues = Reader.ReadStructuresAsArray< float >( MaterialHeader.ShaderValueListSize / 4 ); + } + } +} \ No newline at end of file diff --git a/src/Lumina/Data/Files/TeraFile.cs b/src/Lumina/Data/Files/TeraFile.cs new file mode 100644 index 00000000..23119505 --- /dev/null +++ b/src/Lumina/Data/Files/TeraFile.cs @@ -0,0 +1,48 @@ +using System.Numerics; +using Lumina.Extensions; + +namespace Lumina.Data.Files +{ + struct PlatePos + { + public short x; + public short y; + } + + public class TeraFile : FileResource + { + public uint Version; + public uint PlateCount; + public uint PlateSize; + public float ClipDistance; + public float Unknown; + public byte[] Padding; + + private PlatePos[] _positions; + + public override void LoadFile() + { + Version = Reader.ReadUInt32(); + PlateCount = Reader.ReadUInt32(); + PlateSize = Reader.ReadUInt32(); + ClipDistance = Reader.ReadSingle(); + Unknown = Reader.ReadSingle(); + Padding = Reader.ReadBytes( 32 ); + + _positions = Reader.ReadStructuresAsArray< PlatePos >( (int) PlateCount ); + } + + /// + /// Retrieve the X and Z coordinates of the specified plate index. Note that + /// the Y coordinate is unnecessary as bg plates each contain all necessary vertical + /// data in their respective plate. + /// + /// The index of the bg plate to obtain the coordinates for. + /// + public Vector2 GetPlatePosition( int plateIndex ) + { + var pos = _positions[ plateIndex ]; + return new Vector2( PlateSize * ( pos.x + 0.5f ), PlateSize * ( pos.y + 0.5f ) ); + } + } +} \ No newline at end of file diff --git a/src/Lumina/Data/Parsing/MdlStructs.cs b/src/Lumina/Data/Parsing/MdlStructs.cs new file mode 100644 index 00000000..afa84322 --- /dev/null +++ b/src/Lumina/Data/Parsing/MdlStructs.cs @@ -0,0 +1,359 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Lumina.Extensions; + +namespace Lumina.Data.Parsing +{ + public static class MdlStructs + { + [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 ModelFileHeader + { + public uint Version; + public uint StackSize; + public uint RuntimeSize; + public ushort VertexDeclarationCount; + public ushort MaterialCount; + public uint[] VertexOffset; + public uint[] IndexOffset; + public uint[] VertexBufferSize; + public uint[] IndexBufferSize; + public byte LodCount; + public bool EnableIndexBufferStreaming; + public bool EnableEdgeGeometry; + private byte Padding; + + public static ModelFileHeader Read( BinaryReader br ) + { + ModelFileHeader ret = new ModelFileHeader(); + ret.Version = br.ReadUInt32(); + ret.StackSize = br.ReadUInt32(); + ret.RuntimeSize = br.ReadUInt32(); + ret.VertexDeclarationCount = br.ReadUInt16(); + ret.MaterialCount = br.ReadUInt16(); + ret.VertexOffset = br.ReadStructures< UInt32 >( 3 ).ToArray(); + ret.IndexOffset = br.ReadStructures< UInt32 >( 3 ).ToArray(); + ret.VertexBufferSize = br.ReadStructures< UInt32 >( 3 ).ToArray(); + ret.IndexBufferSize = br.ReadStructures< UInt32 >( 3 ).ToArray(); + ret.LodCount = br.ReadByte(); + ret.EnableIndexBufferStreaming = br.ReadBoolean(); + ret.EnableEdgeGeometry = br.ReadBoolean(); + ret.Padding = br.ReadByte(); + if( ret.EnableEdgeGeometry ) + Console.WriteLine( "Win32 file with EdgeGeometry enabled?" ); + return ret; + } + } + + public struct VertexDeclarationStruct + { + // There are always 17, but stop when stream = -1 + public VertexElement[] VertexElements; + + public static VertexDeclarationStruct Read( BinaryReader br ) + { + VertexDeclarationStruct ret = new VertexDeclarationStruct(); + + var elems = new List< VertexElement >(); + + // Read the vertex elements that we need + var thisElem = br.ReadStructure(); + do + { + elems.Add( thisElem ); + thisElem = br.ReadStructure(); + } 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; + } + } + + public unsafe struct VertexElement + { + public byte Stream; + public byte Offset; + public byte Type; + public byte Usage; + public byte UsageIndex; // D3D9 remnant? + private fixed byte Padding[3]; + } + + 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; + + private ModelFlags1 Flags1; + + public bool DustOcclusionEnabled => Flags1.HasFlag(ModelFlags1.DustOcclusionEnabled); + public bool SnowOcclusionEnabled => Flags1.HasFlag(ModelFlags1.SnowOcclusionEnabled); + public bool RainOcclusionEnabled => Flags1.HasFlag(ModelFlags1.RainOcclusionEnabled); + public bool Unknown1 => Flags1.HasFlag(ModelFlags1.Unknown1); + public bool BgLightingReflectionEnabled => Flags1.HasFlag(ModelFlags1.LightingReflectionEnabled); + public bool WavingAnimationDisabled => Flags1.HasFlag(ModelFlags1.WavingAnimationDisabled); + public bool LightShadowDisabled => Flags1.HasFlag(ModelFlags1.LightShadowDisabled); + public bool ShadowDisabled => Flags1.HasFlag(ModelFlags1.ShadowDisabled); + + public ushort ElementIdCount; + public byte TerrainShadowMeshCount; + + private ModelFlags2 Flags2; + + public bool Unknown2 => Flags2.HasFlag(ModelFlags2.Unknown2); + public bool BgUvScrollEnabled => Flags2.HasFlag(ModelFlags2.BgUvScrollEnabled); + public bool EnableForceNonResident => Flags2.HasFlag(ModelFlags2.EnableForceNonResident); + public bool ExtraLodEnabled => Flags2.HasFlag(ModelFlags2.ExtraLodEnabled); + public bool ShadowMaskEnabled => Flags2.HasFlag(ModelFlags2.ShadowMaskEnabled); + public bool ForceLodRangeEnabled => Flags2.HasFlag(ModelFlags2.ForceLodRangeEnabled); + public bool EdgeGeometryEnabled => Flags2.HasFlag(ModelFlags2.EdgeGeometryEnabled); + public bool Unknown3 => Flags2.HasFlag(ModelFlags2.Unknown3); + + public float ModelClipOutDistance; + public float ShadowClipOutDistance; + public ushort Unknown4; + public ushort TerrainShadowSubmeshCount; + + private byte Unknown5; + + public byte BGChangeMaterialIndex; + public byte BGCrestChangeMaterialIndex; + public byte Unknown6; + public ushort Unknown7; + public ushort Unknown8; + public ushort Unknown9; + private fixed byte Padding[6]; + } + + public struct ElementIdStruct + { + public uint ElementId; + public uint ParentBoneName; + public float[] Translate; + public float[] Rotate; + + public static ElementIdStruct Read( BinaryReader br ) + { + ElementIdStruct ret = new ElementIdStruct(); + ret.ElementId = br.ReadUInt32(); + ret.ParentBoneName = br.ReadUInt32(); + ret.Translate = br.ReadStructures< Single >( 3 ).ToArray(); + ret.Rotate = br.ReadStructures< Single >( 3 ).ToArray(); + return ret; + } + } + + public struct LodStruct + { + public ushort MeshIndex; + public ushort MeshCount; + public float ModelLodRange; + public float TextureLodRange; + public ushort WaterMeshIndex; + public ushort WaterMeshCount; + public ushort ShadowMeshIndex; + public ushort ShadowMeshCount; + public ushort TerrainShadowMeshIndex; + public ushort TerrainShadowMeshCount; + public ushort VerticalFogMeshIndex; + public ushort VerticalFogMeshCount; + // Yell at me if this ever exists on Win32 + public uint EdgeGeometrySize; + public uint EdgeGeometryDataOffset; + public uint PolygonCount; + public uint Unknown1; + public uint VertexBufferSize; + public uint IndexBufferSize; + public uint VertexDataOffset; + public uint IndexDataOffset; + } + + public struct ExtraLodStruct + { + public ushort LightShaftMeshIndex; + public ushort LightShaftMeshCount; + public ushort GlassMeshIndex; + public ushort GlassMeshCount; + public ushort MaterialChangeMeshIndex; + public ushort MaterialChangeMeshCount; + public ushort CrestChangeMeshIndex; + public ushort CrestChangeMeshCount; + public ushort Unknown1; + public ushort Unknown2; + public ushort Unknown3; + public ushort Unknown4; + public ushort Unknown5; + public ushort Unknown6; + public ushort Unknown7; + public ushort Unknown8; + public ushort Unknown9; + public ushort Unknown10; + public ushort Unknown11; + public ushort Unknown12; + } + + public struct MeshStruct + { + public ushort VertexCount; + private ushort Padding; + public uint IndexCount; + public ushort MaterialIndex; + public ushort SubMeshIndex; + public ushort SubMeshCount; + public ushort BoneTableIndex; + public uint StartIndex; + public uint[] VertexBufferOffset; + public byte[] VertexBufferStride; + + public byte VertexStreamCount; + + public static MeshStruct Read( BinaryReader br ) + { + MeshStruct ret = new MeshStruct(); + ret.VertexCount = br.ReadUInt16(); + ret.Padding = br.ReadUInt16(); + ret.IndexCount = br.ReadUInt32(); + ret.MaterialIndex = br.ReadUInt16(); + ret.SubMeshIndex = br.ReadUInt16(); + ret.SubMeshCount = br.ReadUInt16(); + ret.BoneTableIndex = br.ReadUInt16(); + ret.StartIndex = br.ReadUInt32(); + ret.VertexBufferOffset = br.ReadStructures< UInt32 >( 3 ).ToArray(); + ret.VertexBufferStride = br.ReadBytes( 3 ); + ret.VertexStreamCount = br.ReadByte(); + return ret; + } + } + + public struct SubmeshStruct + { + public uint IndexOffset; + public uint IndexCount; + public uint AttributeIndexMask; + public ushort BoneStartIndex; + public ushort BoneCount; + } + + public struct TerrainShadowMeshStruct + { + public uint IndexCount; + public uint StartIndex; + public uint VertexBufferOffset; + public ushort VertexCount; + public ushort SubMeshIndex; + public ushort SubMeshCount; + public byte VertexBufferStride; + private byte Padding; + } + + public struct TerrainShadowSubmeshStruct + { + public uint IndexOffset; + public uint IndexCount; + public ushort Unknown1; + public ushort Unknown2; + } + + public struct BoneTableStruct + { + public ushort[] BoneIndex; + public byte BoneCount; + private byte[] Padding; + + public static BoneTableStruct Read( BinaryReader br ) + { + BoneTableStruct ret = new BoneTableStruct(); + ret.BoneIndex = br.ReadStructures< UInt16 >( 64 ).ToArray(); + ret.BoneCount = br.ReadByte(); + ret.Padding = br.ReadBytes( 3 ); + return ret; + } + } + + public struct ShapeStruct + { + public uint StringOffset; + public ushort[] ShapeMeshStartIndex; + public ushort[] ShapeMeshCount; + + public static ShapeStruct Read( BinaryReader br ) + { + ShapeStruct ret = new ShapeStruct(); + ret.StringOffset = br.ReadUInt32(); + ret.ShapeMeshStartIndex = br.ReadStructures< UInt16 >( 3 ).ToArray(); + ret.ShapeMeshCount = br.ReadStructures< UInt16 >( 3 ).ToArray(); + return ret; + } + } + + public struct ShapeMeshStruct + { + public uint StartIndex; + public uint ShapeValueCount; + public uint ShapeValueOffset; + } + + public struct ShapeValueStruct + { + public ushort Offset; + public ushort Value; + } + + public struct BoundingBoxStruct + { + public float[] Min; + public float[] Max; + + public static BoundingBoxStruct Read( BinaryReader br ) + { + BoundingBoxStruct ret = new BoundingBoxStruct(); + ret.Min = br.ReadStructures< Single >( 4 ).ToArray(); + ret.Max = br.ReadStructures< Single >( 4 ).ToArray(); + return ret; + } + } + } +} \ No newline at end of file diff --git a/src/Lumina/Data/Parsing/MtrlStructs.cs b/src/Lumina/Data/Parsing/MtrlStructs.cs new file mode 100644 index 00000000..d566c037 --- /dev/null +++ b/src/Lumina/Data/Parsing/MtrlStructs.cs @@ -0,0 +1,100 @@ +namespace Lumina.Data.Parsing +{ + /** + * These values are actually CRC values used by SE in order to + * coordinate mappings to shaders. Textures do not actually store + * whether they are diffuse, specular, etc. They store the shader + * that this texture is input for, in CRC form. + * + * That was my long way of explaining "these are linked manually." + */ + public enum TextureUsage : uint + { + Sampler = 0x88408C04, + Sampler0 = 0x213CB439, + Sampler1 = 0x563B84AF, + SamplerCatchlight = 0xFEA0F3D2, + SamplerColorMap0 = 0x1E6FEF9C, + SamplerColorMap1 = 0x6968DF0A, + SamplerDiffuse = 0x115306BE, + SamplerEnvMap = 0xF8D7957A, + SamplerMask = 0x8A4E82B6, + SamplerNormal = 0x0C5EC1F1, + SamplerNormalMap0 = 0xAAB4D9E9, + SamplerNormalMap1 = 0xDDB3E97F, + SamplerReflection = 0x87F6474D, + SamplerSpecular = 0x2B99E025, + SamplerSpecularMap0 = 0x1BBC2F12, + SamplerSpecularMap1 = 0x6CBB1F84, + SamplerWaveMap = 0xE6321AFC, + SamplerWaveletMap0 = 0x574E22D6, + SamplerWaveletMap1 = 0x20491240, + SamplerWhitecapMap = 0x95E1F64D + } + + public struct MaterialFileHeader + { + public uint Version; + public ushort FileSize; + public ushort DataSetSize; + public ushort StringTableSize; + public ushort ShaderPackageNameOffset; + public byte TextureCount; + public byte UvSetCount; + public byte ColorSetCount; + public byte AdditionalDataSize; + } + + public struct MaterialHeader + { + public ushort ShaderValueListSize; + public ushort ShaderKeyCount; + public ushort ConstantCount; + public ushort SamplerCount; + public ushort Unknown1; + public ushort Unknown2; + } + + public struct TextureOffset + { + public ushort Offset; + public ushort Flags; // This is an assumption; has always been 32768 (0x8000) + } + + public struct Constant + { + public uint ConstantId; + public ushort ValueOffset; + public ushort ValueSize; + } + + public unsafe struct Sampler + { + public uint SamplerId; + public uint Flags; // Bitfield; values unknown + public byte TextureIndex; + private fixed byte Padding[3]; + } + + public struct ShaderKey + { + public uint Category; + public uint Value; + } + + public struct UvColorSet + { + public ushort NameOffset; + public ushort Index; + } + + public unsafe struct ColorSetInfo + { + public fixed ushort Data[256]; + } + + public unsafe struct ColorSetDyeInfo + { + public fixed ushort Data[16]; + } +} \ No newline at end of file diff --git a/src/Lumina/Extensions/BinaryReaderExtensions.cs b/src/Lumina/Extensions/BinaryReaderExtensions.cs index 6301e91f..4382ac8a 100644 --- a/src/Lumina/Extensions/BinaryReaderExtensions.cs +++ b/src/Lumina/Extensions/BinaryReaderExtensions.cs @@ -105,6 +105,19 @@ public static byte[] ReadRawOffsetData( this BinaryReader br, long offset ) return chars.ToArray(); } + + public static string ReadStringData( this BinaryReader br ) + { + var chars = new List< byte >(); + + byte current; + while( ( current = br.ReadByte() ) != 0 ) + { + chars.Add( current ); + } + + return Encoding.UTF8.GetString( chars.ToArray(), 0, chars.Count ); + } /// /// Seeks this BinaryReader's position to the given offset. Syntactic sugar. diff --git a/src/Lumina/Models/Materials/Material.cs b/src/Lumina/Models/Materials/Material.cs new file mode 100644 index 00000000..8477f53c --- /dev/null +++ b/src/Lumina/Models/Materials/Material.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Lumina.Data.Files; +using Lumina.Data.Parsing; +using Lumina.Extensions; +using Lumina.Models.Models; + +namespace Lumina.Models.Materials +{ + public class Material + { + /// + /// The path to this Material. May be relative or absolute. + /// + public string MaterialPath { get; private set; } + + /// + /// The resolved path to this Material. Guaranteed to not be initialized + /// in the case that MaterialPath.StartsWith("/") == true. + /// + public string ResolvedPath { get; private set; } + + /// + /// A convenience reference to the Model that instantiated this Material. + /// + public Model Parent { get; private set; } + + /// + /// The MtrlFile backing this Material. May not be initialized. + /// + public MtrlFile File { get; private set; } + + /// + /// The Textures for this Material. May not be initialized. + /// + public Texture[] Textures { get; private set; } + + /// + /// The shader package name used by this Material. + /// + public string ShaderPack { get; private set; } + + /// + /// The variant ID for this Material. This is specified by the caller + /// for relative paths and inferred for absolute paths or instantiation + /// via the MtrlFile constructor. + /// + public int VariantId { get; set; } + + private Dictionary< int, string > StringOffsetToStringMap; + + /// + /// Creates a new Material instance without resolving any game data. + /// + /// The path, relative or absolute, to this Material. + /// The variant ID for this material. This parameter is + /// ignored if an absolute path is provided; the variant ID will be inferred. + public Material( string path, int variantId = 1 ) + { + MaterialPath = path; + VariantId = !path.StartsWith( "/" ) ? GetVariantIdFromPath( path ) : variantId; + } + + /// + /// Creates a new Material instance without resolving any data. + /// This constructor is used by the class to + /// provide a reference to the model within the material. + /// + /// The Model this Material has been instantiated for. + /// The path, relative or absolute, to this Material. + /// The variant ID for this material. Default is 1. + public Material( Model parent, string path, int variantId = 1 ) + { + Parent = parent; + MaterialPath = path; + VariantId = variantId; + } + + /// + /// Creates a new Material instance using the provided MtrlFile. + /// Variant ID is inferred from the provided MtrlFile's path. + /// + /// The MtrlFile to back this Material. + public Material( MtrlFile file ) + { + File = file; + VariantId = GetVariantIdFromPath( file.FilePath ); + BuildMaterial(); + } + + /// + /// Creates a new Material instance using the provided path and + /// reference to game data. The Material will be built and then + /// updated with game data. + /// + /// A reference to game data access. + /// The path, relative or absolute, to this Material. + /// The variant ID for this material. Default is 1. + public Material( GameData data, string path, int variantId = 1 ) + { + MaterialPath = path; + BuildMaterial(); + Update( data ); + } + + /// + /// Update this Material using game data. If instantiated without a MtrlFile or + /// GameData, this method will retrieve the MtrlFile referenced by this Material and use + /// that file to update local fields. This method is not guaranteed to load the MtrlFile. + /// + /// A reference to game data access. + /// The existing Material instance, for method chaining. + public Material Update( GameData data ) + { + if( MaterialPath.StartsWith( "/" ) ) + { + ResolvedPath = ResolveRelativeMaterialPath( MaterialPath, VariantId ); + File = data.GetFile< MtrlFile >( ResolvedPath ); + } + else + { + File = data.GetFile< MtrlFile >( MaterialPath ); + } + + if( File != null ) + BuildMaterial(); + + return this; + } + + /// + /// Resolves a relative material path in the form /mt_c0101e0001_top_a.mtrl + /// into its full path, chara/equipment/e0001/material/v{variantId}/mt_c0101e0001_top_a.mtrl. + ///
This method will successfully resolve all known relative material paths. + ///
+ /// The relative path of the provided material. + /// The variant to use in material resolution. + /// The resolved, absolute path to the requested material, or null if unsuccessful. + public static string ResolveRelativeMaterialPath( string relativePath, int variantId ) + { + var id1 = relativePath[4]; + var val1 = relativePath.Substring( 5, 4 ); + var id2 = relativePath[9]; + var val2 = relativePath.Substring( 10, 4 ); + + return ( id1, id2 ) switch + { + ('c', 'a') => $"chara/accessory/a{val2}/material/v{variantId:D4}{relativePath}", + ('c', 'b') => $"chara/human/c{val1}/obj/body/b{val2}/material/v{variantId:D4}{relativePath}", + ('c', 'e') => $"chara/equipment/e{val2}/material/v{variantId:D4}{relativePath}", + ('c', 'f') => $"chara/human/c{val1}/obj/face/f{val2}/material{relativePath}", + ('c', 'h') => $"chara/human/c{val1}/obj/hair/h{val2}/material/v{variantId:D4}{relativePath}", + ('c', 't') => $"chara/human/c{val1}/obj/tail/t{val2}/material/v{variantId:D4}{relativePath}", + ('c', 'z') => $"chara/human/c{val1}/obj/zear/z{val2}/material{relativePath}", + ('d', 'e') => $"chara/demihuman/d{val1}/obj/equipment/e{val2}/material/v{variantId:D4}{relativePath}", + ('m', 'b') => $"chara/monster/m{val1}/obj/body/b{val2}/material/v{variantId:D4}{relativePath}", + ('w', 'b') => $"chara/weapon/w{val1}/obj/body/b{val2}/material/v{variantId:D4}{relativePath}", + (_, _) => null + }; + } + + /// + /// Parse the variant ID out of an existing absolute path to a .mtrl file. + /// + /// The absolute path to an existing .mtrl file. + /// The variant ID for the given .mtrl path. In case of error, 1. + public static int GetVariantIdFromPath(string matPath) { + var v = 1; + var vStart = matPath.IndexOf("/v", StringComparison.Ordinal ); + if( vStart == -1 ) + return v; + var vSub = matPath.Substring(vStart + 2, 4); + try + { + v = int.Parse( vSub ); + } + catch( FormatException ) { } + + return v; + } + + private void BuildMaterial() + { + ReadStrings(); + ReadTextures(); + + ShaderPack = StringOffsetToStringMap[ File.FileHeader.ShaderPackageNameOffset ]; + } + + private void ReadTextures() + { + Textures = new Texture[File.TextureOffsets.Length]; + + for( int i = 0; i < File.TextureOffsets.Length; i++ ) + { + TextureUsage raw = (TextureUsage) File.Samplers[ i ].SamplerId; + var texIndex = File.Samplers[ i ].TextureIndex; + var texOffset = File.TextureOffsets[ texIndex ].Offset; + var texPath = StringOffsetToStringMap[ texOffset ]; + Textures[ i ] = new Texture( this, raw, texPath ); + } + } + + private void ReadStrings() + { + StringOffsetToStringMap = new Dictionary< int, string >(); + var mr = new MemoryStream( File.Strings ); + var br = new BinaryReader( mr ); + + // They re-use offsets, so the number of offsets is not equal to the number of unique members + var uniqueTextureCount = File.TextureOffsets.Select( t => t.Offset ).Distinct().Count(); + var uniqueUvColorSetCount = File.UvColorSets.Select( t => t.NameOffset ).Distinct().Count(); + var uniqueColorOffsetCount = File.ColorSetOffsets.Select( t => t ).Distinct().Count(); + + // Add one for the shader package name at the end + var stringCount = uniqueTextureCount + uniqueUvColorSetCount + uniqueColorOffsetCount + 1; + for( int i = 0; i < stringCount; i++ ) + { + long startOffset = br.BaseStream.Position; + string tmp = br.ReadStringData(); + StringOffsetToStringMap[ (int) startOffset ] = tmp; + } + + br.Dispose(); + mr.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Lumina/Models/Materials/Texture.cs b/src/Lumina/Models/Materials/Texture.cs new file mode 100644 index 00000000..102020af --- /dev/null +++ b/src/Lumina/Models/Materials/Texture.cs @@ -0,0 +1,126 @@ +using System; +using Lumina.Data.Files; +using Lumina.Data.Parsing; + +namespace Lumina.Models.Materials +{ + public class Texture + { + /// + /// Less specific, and easier to use texture usage enumeration + /// See for a more complex enumeration. + /// + public enum Usage + { + Catchlight, + Diffuse, + Envmap, + Mask, + Normal, + Reflection, + Specular, + Wave, + Whitecap, + } + + /// + /// A convenience reference to the Material that instantiated this Texture. + /// + public Material Parent { get; private set; } + + /// + /// The raw shader ID this Texture is used as input for. + /// + public TextureUsage TextureUsageRaw { get; private set; } + + /// + /// The more user-friendly usage type of this Texture. + /// + public Usage TextureUsageSimple { get; private set; } + + /// + /// The path to this Texture. + /// + public string TexturePath { get; private set; } + + private bool _isLoaded = false; + private TexFile _tex; + + /// + /// Creates a new Texture instance without resolving any game data. + /// + /// The Material this Texture has been instantiated for. + /// The this Texture is input for. + /// The path to this Texture. + public Texture( Material parent, TextureUsage raw, string texturePath ) + { + Parent = parent; + TextureUsageRaw = raw; + TextureUsageSimple = GetUsage( raw ); + TexturePath = texturePath; + } + + /// + /// Creates a new Texture instance without resolving any game data. + /// + /// The this Texture is input for. + /// The path to this Texture. + public Texture( TextureUsage raw, string texturePath ) : this( null, raw, texturePath ) { } + + /// + /// Retrieve the TexFile referenced by this Texture, and store it + /// in this Texture for future use. + /// + /// A reference to game data access. + /// The TexFile referenced by this Texture if found, null otherwise. + public TexFile GetTexture(GameData data) + { + if( _isLoaded ) return _tex; + if( data == null ) return null; + + _tex = data.GetFile< TexFile >( TexturePath ); + _isLoaded = true; + + return _tex; + } + + /// + /// Retrieve the TexFile referenced by this Texture without storing it + /// or retrieving it from this Texture's local reference. + /// + /// A reference to game data access. + /// The TexFile referenced by this Texture if found, null otherwise. + public TexFile GetTextureNc(GameData data) + { + return data.GetFile< TexFile >( TexturePath ); + } + + private static Usage GetUsage(TextureUsage usage) + { + return usage switch + { + TextureUsage.Sampler => Usage.Diffuse, + TextureUsage.Sampler0 => Usage.Diffuse, + TextureUsage.Sampler1 => Usage.Diffuse, + TextureUsage.SamplerCatchlight => Usage.Catchlight, + TextureUsage.SamplerColorMap0 => Usage.Diffuse, + TextureUsage.SamplerColorMap1 => Usage.Diffuse, + TextureUsage.SamplerDiffuse => Usage.Diffuse, + TextureUsage.SamplerEnvMap => Usage.Envmap, + TextureUsage.SamplerMask => Usage.Mask, + TextureUsage.SamplerNormal => Usage.Normal, + TextureUsage.SamplerNormalMap0 => Usage.Normal, + TextureUsage.SamplerNormalMap1 => Usage.Normal, + TextureUsage.SamplerReflection => Usage.Reflection, + TextureUsage.SamplerSpecular => Usage.Specular, + TextureUsage.SamplerSpecularMap0 => Usage.Specular, + TextureUsage.SamplerSpecularMap1 => Usage.Specular, + TextureUsage.SamplerWaveMap => Usage.Wave, + TextureUsage.SamplerWaveletMap0 => Usage.Wave, + TextureUsage.SamplerWaveletMap1 => Usage.Wave, + TextureUsage.SamplerWhitecapMap => Usage.Whitecap, + _ => throw new ArgumentOutOfRangeException( nameof( usage ), usage, null ) + }; + } + } +} \ No newline at end of file diff --git a/src/Lumina/Models/Models/Mesh.cs b/src/Lumina/Models/Models/Mesh.cs new file mode 100644 index 00000000..8cc56e53 --- /dev/null +++ b/src/Lumina/Models/Models/Mesh.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Text; +using Lumina.Data.Parsing; +using Lumina.Extensions; +using Lumina.Models.Materials; + +namespace Lumina.Models.Models { + public class Mesh { + + public enum MeshType + { + Main, + Water, + Shadow, + TerrainShadow, + VerticalFog, + LightShaft, + Glass, + MaterialChange, + CrestChange + } + + /// + /// A convenience reference to the Model that instantiated this Mesh. + /// + public Model Parent { get; private set; } + + /// + /// The mesh index within the Model it belongs to. + /// + public int MeshIndex { get; private set; } + + /// + /// The mesh type categories this mesh can be used for. + /// + public MeshType[] Types { get; private set; } + + /// + /// The list of mesh attributes applied to this mesh. + /// + public string[] Attributes { get; private set; } + + // TODO: build submeshes from the index data + // the information is available to build these, but it'd be nice + // to have access to Mesh objects with pre-read submesh meshes + // public Mesh SubmeshesAsMesh { get; private set; } + + /// + /// The submeshes that make up this mesh. + /// + public Submesh[] Submeshes { get; private set; } + + /// + /// The bone remapping table for this mesh. + /// + public ushort[] BoneTable { get; private set; } + + /// + /// The array of all vertices for this mesh. + /// + public Vertex[] Vertices; + + /// + /// The array of all indices for this mesh. + /// + public ushort[] Indices; + + /// + /// A reference to the Material used by this mesh. + /// + public Material Material { get; private set; } + + public Mesh( Model model, int index, MeshType[] types ) { + Parent = model; + MeshIndex = index; + Types = types; + BuildMesh(); + } + + private void BuildMesh() { + ReadBoneTable(); + ReadIndices(); + ReadVertices(); + ReadSubmeshes(); + + var matIndex = Parent.File.Meshes[ MeshIndex ].MaterialIndex; + Material = Parent.Materials[ matIndex ]; + } + + private void ReadSubmeshes() + { + var currentMesh = Parent.File.Meshes[ MeshIndex ]; + Submeshes = new Submesh[ currentMesh.SubMeshCount ]; + for( int i = 0; i < currentMesh.SubMeshCount; i++ ) + Submeshes[i] = new Submesh(Parent, MeshIndex, i); + } + + //TODO is this necessary? + private void ReadBoneTable() + { + var currentMesh = Parent.File.Meshes[ MeshIndex ]; + + // Copy over the bone table + int boneTableIndex = currentMesh.BoneTableIndex; + if (boneTableIndex != 255) + BoneTable = Parent.File.BoneTables[ boneTableIndex ].BoneIndex; + } + + private void ReadIndices() + { + var currentMesh = Parent.File.Meshes[ MeshIndex ]; + + BinaryReader reader = new BinaryReader( new MemoryStream( Parent.File.Data, + (int) Parent.File.FileHeader.IndexOffset[ (int) Parent.Lod ], + (int) Parent.File.FileHeader.IndexBufferSize[ (int) Parent.Lod ] ) ); + reader.Seek( currentMesh.StartIndex * 2 ); + Indices = reader.ReadStructures< UInt16 >( (int) currentMesh.IndexCount ).ToArray(); + } + + private void ReadVertices() + { + MdlStructs.MeshStruct currentMesh = Parent.File.Meshes[ MeshIndex ]; + MdlStructs.VertexDeclarationStruct currentDecl = Parent.File.VertexDeclarations[ MeshIndex ]; + + // We have to go through these in order by offset with this implementation + var orderedElements = currentDecl.VertexElements.ToList(); + orderedElements.Sort( ( e1, e2 ) => e1.Offset.CompareTo( e2.Offset ) ); + Vertices = new Vertex[currentMesh.VertexCount]; + + // Vertices may be defined across at most 3 streams defined by a Mesh's VertexDeclarations + var vertexStreamReader = new BinaryReader[3]; + for( int i = 0; i < 3; i++ ) { + vertexStreamReader[ i ] = new BinaryReader( new MemoryStream( Parent.File.Data, + (int) Parent.File.FileHeader.VertexOffset[ (int) Parent.Lod ], + (int) Parent.File.FileHeader.VertexBufferSize[ (int) Parent.Lod ] ) ); + vertexStreamReader[ i ].Seek( currentMesh.VertexBufferOffset[ i ] ); + } + + for( int i = 0; i < Vertices.Length; i++ ) { + Vertices[i] = new Vertex(); + + foreach( var element in orderedElements ) + SetElementField( ref Vertices[i], element, vertexStreamReader[element.Stream] ); + } + } + + private void SetElementField( ref Vertex v, MdlStructs.VertexElement element, BinaryReader br ) { + var type = (Vertex.VertexType) element.Type; + var usage = (Vertex.VertexUsage) element.Usage; + + object data = type switch { + Vertex.VertexType.Single3 => new Vector3( br.ReadSingle(), br.ReadSingle(), br.ReadSingle() ), + Vertex.VertexType.Single4 => new Vector4( br.ReadSingle(), br.ReadSingle(), br.ReadSingle(), br.ReadSingle() ), + Vertex.VertexType.UInt => br.ReadBytes( 4 ), + Vertex.VertexType.ByteFloat4 => new Vector4( br.ReadByte() / 255f, br.ReadByte() / 255f, br.ReadByte() / 255f, br.ReadByte() / 255f ), + Vertex.VertexType.Half2 => new Vector2( br.ReadUInt16().Unpack(), br.ReadUInt16().Unpack() ), + Vertex.VertexType.Half4 => new Vector4( br.ReadUInt16().Unpack(), br.ReadUInt16().Unpack(), + br.ReadUInt16().Unpack(), br.ReadUInt16().Unpack() ), + _ => throw new ArgumentOutOfRangeException() + }; + + switch( usage ) { + case Vertex.VertexUsage.Position: v.Position = GetVector4(data); break; + case Vertex.VertexUsage.BlendWeights: v.BlendWeights = GetVector4(data); break; + case Vertex.VertexUsage.BlendIndices: v.BlendIndices = (byte[]) data; break; + case Vertex.VertexUsage.Normal: v.Normal = GetVector3(data); break; + case Vertex.VertexUsage.UV: v.UV = GetVector4(data); break; + case Vertex.VertexUsage.Tangent2: v.Tangent2 = GetVector4(data); break; + case Vertex.VertexUsage.Tangent1: v.Tangent1 = GetVector4(data); break; + case Vertex.VertexUsage.Color: v.Color = GetVector4(data); break; + default: throw new ArgumentOutOfRangeException(); + } + } + + private static Vector3 GetVector3( object value ) { + switch( value ) { + case Vector2 v2: return new Vector3( v2.X, v2.Y, 0 ); + case Vector3 v3: return v3; + case Vector4 v4: return new Vector3( v4.X, v4.Y, v4.Z ); + } + throw new ArgumentOutOfRangeException(); + } + + private static Vector4 GetVector4( object value ) { + switch( value ) { + case Vector2 v2: return new Vector4( v2.X, v2.Y, 0, 0 ); + case Vector3 v3: return new Vector4( v3.X, v3.Y, v3.Z, 1 ); + case Vector4 v4: return v4; + } + throw new ArgumentOutOfRangeException(); + } + + /// + /// Writes a Mesh into a single Wavefront Object file. This is not recommended + /// for general use as the .obj format has very limited information, and does not + /// include vertex colors, materials, or textures. + /// + /// A string containing the entire .obj file, including newlines. + public string GetObjectFileText() + { + string nl = Environment.NewLine; + + var vsList = new List< Vector3 >(); + var vcList = new List< Vector4 >(); + var vtList = new List< Vector4 >(); + var nmList = new List< Vector3 >(); + var inList = new List< Vector3 >(); + + foreach( var vert in Vertices ) + { + Vector3 vs = new Vector3(); + Vector4 vc = new Vector4(); + Vector4 vt = new Vector4(); + Vector3 nm = new Vector3(); + + vs.X = vert.Position.Value.X; + vs.Y = vert.Position.Value.Y; + vs.Z = vert.Position.Value.Z; + + if( vert.Color == null ) + { + vc.X = 0; + vc.Y = 0; + vc.W = 0; + vc.Z = 0; + } + else + { + vc.X = vert.Color.Value.X; + vc.Y = vert.Color.Value.Y; + vc.W = vert.Color.Value.W; + vc.Z = vert.Color.Value.Z; + } + + if( vert.UV.HasValue ) + { + vt.X = vert.UV.Value.X; + vt.Y = -1 * vert.UV.Value.Y; + vt.W = ( vert.UV.Value.W == 0 ) ? vt.X : vert.UV.Value.W; + vt.Z = ( vert.UV.Value.Z == 0 ) ? vt.Y : vert.UV.Value.Z; + } + + if( vert.Normal.HasValue ) + { + nm.X = vert.Normal.Value.X; + nm.Y = vert.Normal.Value.Y; + nm.Z = vert.Normal.Value.Z; + } + + vsList.Add( vs ); + vcList.Add( vc ); + vtList.Add( vt ); + nmList.Add( nm ); + } + + for( int j = 0; j < Indices.Length; j += 3 ) + { + Vector3 ind = new Vector3 + { + X = Indices[ j + 0 ] + 1, + Y = Indices[ j + 1 ] + 1, + Z = Indices[ j + 2 ] + 1 + }; + inList.Add( ind ); + } + + var sb = new StringBuilder(); + foreach( var vert in vsList ) + sb.Append( $"v {(decimal) vert.X} {(decimal) vert.Y} {(decimal) vert.Z}{nl}" ); + + foreach( var color in vcList ) + sb.Append( $"vc {(decimal) color.W} {(decimal) color.X} {(decimal) color.Y} {(decimal) color.Z}{nl}" ); + + foreach( var texCoord in vtList ) + sb.Append( $"vt {(decimal) texCoord.X} {(decimal) texCoord.Y} {(decimal) texCoord.W} {(decimal) texCoord.Z}{nl}" ); + + foreach( var norm in nmList ) + sb.Append( $"vn {(decimal) norm.X} {(decimal) norm.Y} {(decimal) norm.Z}{nl}" ); + + foreach( var ind in inList ) + { + sb.Append( String.Format( "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}{3}", + (ushort) ind.X, + (ushort) ind.Y, + (ushort) ind.Z, + nl ) ); + } + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Lumina/Models/Models/Model.cs b/src/Lumina/Models/Models/Model.cs new file mode 100644 index 00000000..e794f5d2 --- /dev/null +++ b/src/Lumina/Models/Models/Model.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Lumina.Data.Files; +using Lumina.Data.Parsing; +using Lumina.Extensions; +using Lumina.Models.Materials; + +namespace Lumina.Models.Models +{ + public class Model + { + /// + /// Model levels of details start at 0 for high, and + /// go up to indicate lowering levels of detail. + /// This means that models with only one level of detail will + /// only have a "high quality" model. + /// + public enum ModelLod + { + High, + Med, + Low + } + + public MdlFile File { get; private set; } + public ModelLod Lod { get; private set; } + public Mesh[] Meshes { get; private set; } + public Material[] Materials { get; private set; } + public Dictionary Shapes { get; private set; } + public Dictionary< int, string > StringOffsetToStringMap { get; private set; } + public int VariantId { get; private set; } + + /// + /// Creates a new Model instance using the provided MdlFile, level of detail, + /// and variant ID, without resolving any game data. + /// + /// The MdlFile to back this Model. + /// The quality of the loaded Model. + /// The variant ID of the Model to load. This will be used + /// to specify a Material variant, and may be used by the caller to limit attributes + /// depending on data from an ImcFile. + /// The specified MdlFile does not contain + /// the specified ModelLod. + public Model( MdlFile mdlFile, ModelLod lod = ModelLod.High, int variantId = 1) + { + if( (uint) lod > mdlFile.FileHeader.LodCount ) + throw new ArgumentException( "The given model does not have the requested LoD.", nameof( lod ) ); + + File = mdlFile; + Lod = lod; + VariantId = variantId; + BuildModel(); + } + + /// + /// Creates a new Model instance using the provided path, access to game data, + /// level of detail, and variant ID. The Model will be built and then updated + /// with game data. + /// + /// A reference to game data access. + /// The path to this Model. + /// The quality of the loaded Model. + /// The variant ID of the Model to load. This will be used + /// to specify a Material variant, and may be used by the caller to limit attributes + /// depending on data from an ImcFile. + /// The specified MdlFile does not contain + /// the specified ModelLod. + public Model( GameData data, string path, ModelLod lod = ModelLod.High, int variantId = 1 ) + { + var mdlFile = data.GetFile< MdlFile >( path ); + + if( (uint) lod > mdlFile.FileHeader.LodCount ) + throw new ArgumentException( "The given model does not have the requested LoD.", nameof( lod ) ); + + File = mdlFile; + Lod = lod; + VariantId = variantId; + BuildModel(); + Update(data); + } + + private void BuildModel() + { + // Every valid MDL has VertexDeclarations + if( File.VertexDeclarations == null ) + { + Console.WriteLine( "BuildModel() called on an empty MdlFile." ); + return; + } + + ReadStrings(); + ReadMaterials(); + ReadMeshes(); + ReadShapes(); + } + + /// + /// Update this Model using game data. The Model's references + /// to Materials will be resolved and the Material files fully + /// loaded to be used to resolve textures and other information. + /// + /// A reference to game data access. + /// The existing Model instance, for method chaining. + public Model Update(GameData data) + { + foreach (var mat in Materials) + mat.Update( data ); + return this; + } + + private void ReadMaterials() + { + Materials = new Material[File.FileHeader.MaterialCount]; + + for( int i = 0; i < File.FileHeader.MaterialCount; i++) + { + var pathOffset = File.MaterialNameOffsets[ i ]; + var path = StringOffsetToStringMap[ (int) pathOffset ]; + Materials[ i ] = new Material( this, path, VariantId ); + } + } + + private void ReadShapes() + { + Shapes = new Dictionary< string, Shape >(); + for( int i = 0; i < File.Shapes.Length; i++ ) + { + // We will need more info in the constructor here... eventually + var shape = new Shape( this, i ); + Shapes[ shape.ShapeName ] = shape; + } + } + + private void ReadStrings() + { + StringOffsetToStringMap = new Dictionary< int, string >(); + var mr = new MemoryStream( File.Strings ); + var br = new BinaryReader( mr ); + for( int i = 0; i < File.StringCount; i++ ) + { + long startOffset = br.BaseStream.Position; + string tmp = br.ReadStringData(); + StringOffsetToStringMap[ (int) startOffset ] = tmp; + } + br.Dispose(); + mr.Dispose(); + } + + private void ReadMeshes() + { + // My brain is fried + var ranges = new List< (int s, int e ) >(); + + MdlStructs.LodStruct currentLod = File.Lods[ (int) Lod ]; + ranges.Add( (currentLod.MeshIndex, currentLod.MeshIndex + currentLod.MeshCount) ); + ranges.Add( (currentLod.WaterMeshIndex, currentLod.WaterMeshIndex + currentLod.WaterMeshCount) ); + ranges.Add( (currentLod.ShadowMeshIndex, currentLod.ShadowMeshIndex + currentLod.ShadowMeshCount) ); + ranges.Add( (currentLod.TerrainShadowMeshIndex, currentLod.TerrainShadowMeshIndex + currentLod.TerrainShadowMeshCount) ); + ranges.Add( (currentLod.VerticalFogMeshIndex, currentLod.VerticalFogMeshIndex + currentLod.VerticalFogMeshCount) ); + + if( File.ModelHeader.ExtraLodEnabled ) + { + MdlStructs.ExtraLodStruct extraLod = File.ExtraLods[ (int) Lod ]; + ranges.Add( ( extraLod.LightShaftMeshIndex, extraLod.LightShaftMeshIndex + extraLod.LightShaftMeshCount ) ); + ranges.Add( ( extraLod.GlassMeshIndex, extraLod.GlassMeshIndex + extraLod.GlassMeshCount ) ); + ranges.Add( ( extraLod.MaterialChangeMeshIndex, extraLod.MaterialChangeMeshIndex + extraLod.MaterialChangeMeshCount ) ); + ranges.Add( ( extraLod.CrestChangeMeshIndex, extraLod.CrestChangeMeshIndex + extraLod.CrestChangeMeshCount ) ); + } + + int totalMeshes = 0; + bool atIndex = true; + while (atIndex) + { + atIndex = false; + foreach( var range in ranges ) + { + if( range.s <= totalMeshes && totalMeshes < range.e ) + { + atIndex = true; + totalMeshes++; + break; + } + } + } + + Meshes = new Mesh[totalMeshes]; + + for( int i = 0; i < Meshes.Length; i++ ) + { + Meshes[ i ] = new Mesh( this, i, GetMeshTypes( i ) ); + } + } + + private Mesh.MeshType[] GetMeshTypes( int index ) + { + // I could create arrays for the ranges for cleaner code, but all this is doing is checking ranges + var types = new List< Mesh.MeshType >(); + if( index >= File.Lods[ (int) Lod ].MeshIndex && + index < File.Lods[ (int) Lod ].MeshIndex + File.Lods[ (int) Lod ].MeshCount && + File.Lods[ (int) Lod ].MeshCount > 0) + types.Add( Mesh.MeshType.Main); + if( index >= File.Lods[ (int) Lod ].WaterMeshIndex && + index < File.Lods[ (int) Lod ].WaterMeshIndex + File.Lods[ (int) Lod ].WaterMeshCount && + File.Lods[ (int) Lod ].WaterMeshCount > 0) + types.Add( Mesh.MeshType.Water); + if( index >= File.Lods[ (int) Lod ].ShadowMeshIndex && + index < File.Lods[ (int) Lod ].ShadowMeshIndex + File.Lods[ (int) Lod ].ShadowMeshCount && + File.Lods[ (int) Lod ].ShadowMeshCount > 0) + types.Add( Mesh.MeshType.Shadow); + if( index >= File.Lods[ (int) Lod ].TerrainShadowMeshIndex && + index < File.Lods[ (int) Lod ].TerrainShadowMeshIndex + File.Lods[ (int) Lod ].TerrainShadowMeshCount && + File.Lods[ (int) Lod ].TerrainShadowMeshCount > 0) + types.Add( Mesh.MeshType.TerrainShadow); + if( index >= File.Lods[ (int) Lod ].VerticalFogMeshIndex && + index < File.Lods[ (int) Lod ].VerticalFogMeshIndex + File.Lods[ (int) Lod ].VerticalFogMeshCount && + File.Lods[ (int) Lod ].VerticalFogMeshCount > 0) + types.Add( Mesh.MeshType.VerticalFog); + + if( !File.ModelHeader.ExtraLodEnabled ) return types.ToArray(); + + if( index >= File.ExtraLods[ (int) Lod ].LightShaftMeshIndex && + index < File.ExtraLods[ (int) Lod ].LightShaftMeshIndex + File.ExtraLods[ (int) Lod ].LightShaftMeshCount && + File.ExtraLods[ (int) Lod ].LightShaftMeshCount > 0) + types.Add( Mesh.MeshType.LightShaft); + if( index >= File.ExtraLods[ (int) Lod ].GlassMeshIndex && + index < File.ExtraLods[ (int) Lod ].GlassMeshIndex + File.ExtraLods[ (int) Lod ].GlassMeshCount && + File.ExtraLods[ (int) Lod ].GlassMeshCount > 0) + types.Add( Mesh.MeshType.Glass); + if( index >= File.ExtraLods[ (int) Lod ].MaterialChangeMeshIndex && + index < File.ExtraLods[ (int) Lod ].MaterialChangeMeshIndex + File.ExtraLods[ (int) Lod ].MaterialChangeMeshCount && + File.ExtraLods[ (int) Lod ].MaterialChangeMeshCount > 0) + types.Add( Mesh.MeshType.MaterialChange); + if( index >= File.ExtraLods[ (int) Lod ].CrestChangeMeshIndex && + index < File.ExtraLods[ (int) Lod ].CrestChangeMeshIndex + File.ExtraLods[ (int) Lod ].CrestChangeMeshCount && + File.ExtraLods[ (int) Lod ].CrestChangeMeshCount > 0 ) + types.Add( Mesh.MeshType.CrestChange ); + + + return types.ToArray(); + } + + public Mesh[] GetMeshesByType( Mesh.MeshType mp ) + { + return Meshes.Where( m => m.Types.Contains(mp) ).ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Lumina/Models/Models/Shape.cs b/src/Lumina/Models/Models/Shape.cs new file mode 100644 index 00000000..8476930e --- /dev/null +++ b/src/Lumina/Models/Models/Shape.cs @@ -0,0 +1,16 @@ +namespace Lumina.Models.Models +{ + public class Shape + { + public string ShapeName { get; private set; } + + public Shape(Model model, int shapeIndex) + { + var currentShape = model.File.Shapes[ shapeIndex ]; + ShapeName = model.StringOffsetToStringMap[ (int) currentShape.StringOffset ]; + + // TODO: shape index modifier things + // someone else do this pls + } + } +} \ No newline at end of file diff --git a/src/Lumina/Models/Models/Submesh.cs b/src/Lumina/Models/Models/Submesh.cs new file mode 100644 index 00000000..642a1234 --- /dev/null +++ b/src/Lumina/Models/Models/Submesh.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using Lumina.Data.Parsing; + +namespace Lumina.Models.Models +{ + public class Submesh + { + /// + /// The offset to the index that this submesh begins. + /// + public uint IndexOffset; + + /// + /// The number of indices present in this submesh. + /// + public uint IndexNum; + + /// + /// The attributes that are enabled for this submesh. + /// + public string[] Attributes; + + /// + /// The bones referenced by this submesh. + /// + public string[] Bones; + + public Submesh( Model model, int meshIndex, int subMeshIndex ) + { + var currentMesh = model.File.Meshes[ meshIndex ]; + int subMeshListIndex = currentMesh.SubMeshIndex + subMeshIndex; + var currentSubMesh = model.File.Submeshes[ subMeshListIndex ]; + + IndexOffset = currentSubMesh.IndexOffset; + IndexNum = currentSubMesh.IndexCount; + + // AttributeIndexMask is a bit-based index mask + // i.e. "5" is 0101 so it applies attrs 0 and 2 + var attributeList = new List< string >(); + for( int i = 0; i < model.File.ModelHeader.AttributeCount; i++ ) + { + if( ( ( 1 << i ) & currentSubMesh.AttributeIndexMask ) > 0 ) + { + uint nameOffset = model.File.AttributeNameOffsets[ i ]; + attributeList.Add( model.StringOffsetToStringMap[ (int) nameOffset ] ); + } + } + + Attributes = attributeList.ToArray(); + + // I don't know what this is for + if( currentSubMesh.BoneStartIndex == 65535 ) return; + var affectedBoneTable = new List< string >(); + int boneEndIndex = currentSubMesh.BoneStartIndex + currentSubMesh.BoneCount; + for( int i = currentSubMesh.BoneStartIndex; i < boneEndIndex; i++ ) + { + var boneIndex = model.File.SubmeshBoneMap[ i ]; + var boneOffset = model.File.BoneNameOffsets[ boneIndex ]; + string boneName = model.StringOffsetToStringMap[ (int) boneOffset ]; + affectedBoneTable.Add( boneName ); + } + + Bones = affectedBoneTable.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Lumina/Models/Models/Vertex.cs b/src/Lumina/Models/Models/Vertex.cs new file mode 100644 index 00000000..d82c5342 --- /dev/null +++ b/src/Lumina/Models/Models/Vertex.cs @@ -0,0 +1,40 @@ +using System.IO; +using System.Linq; +using System.Numerics; +using Lumina.Data.Files; +using Lumina.Data.Parsing; + +namespace Lumina.Models.Models { + + public struct Vertex { + + public enum VertexType : byte { + Single3 = 2, + Single4 = 3, + UInt = 5, + ByteFloat4 = 8, + Half2 = 13, + Half4 = 14 + } + + public enum VertexUsage : byte { + Position = 0, + BlendWeights = 1, + BlendIndices = 2, + Normal = 3, + UV = 4, + Tangent2 = 5, + Tangent1 = 6, + Color = 7, + } + + public Vector4? Position; + public Vector4? BlendWeights; + public byte[] BlendIndices; + public Vector3? Normal; + public Vector4? UV; + public Vector4? Color; + public Vector4? Tangent2; + public Vector4? Tangent1; + } +} \ No newline at end of file