From e049b6a74df83421c31caa0afcf101cac8ce1f9e Mon Sep 17 00:00:00 2001 From: Duude92 Date: Thu, 21 Aug 2025 09:28:38 +0300 Subject: [PATCH 01/22] Added structs for VVD file --- Sledge.Formats.Model/Source/VvdStructs.cs | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Sledge.Formats.Model/Source/VvdStructs.cs diff --git a/Sledge.Formats.Model/Source/VvdStructs.cs b/Sledge.Formats.Model/Source/VvdStructs.cs new file mode 100644 index 0000000..f7d6124 --- /dev/null +++ b/Sledge.Formats.Model/Source/VvdStructs.cs @@ -0,0 +1,45 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Sledge.Formats.Model.Source +{ + public struct VvdHeader + { + const int MAX_NUM_LODS = 8; // max number of LODs supported by the engine + const int IDSV_SIZE = 4; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = IDSV_SIZE)] + public char[] id; // MODEL_VERTEX_FILE_ID + public int version; // MODEL_VERTEX_FILE_VERSION + public int checksum; // same as studiohdr_t, ensures sync ( Note: maybe long instead of int in versions other than 4. ) + public int numLODs; // num of valid lods + [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_NUM_LODS)] + public int[] numLODVertexes; // num verts for desired root lod + public int numFixups; // num of vertexFileFixup_t + public int fixupTableStart; // offset from base to fixup table + public int vertexDataStart; // offset from base to vertex block + public int tangentDataStart; // offset from base to tangent block + } + // NOTE: This is exactly 48 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StudioVertex + { + public StudioBoneWeight m_BoneWeights; + public Vector3 m_vecPosition; + public Vector3 m_vecNormal; + public Vector2 m_vecTexCoord; + }; + + // 16 bytes + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StudioBoneWeight + { + const int MAX_NUM_BONES_PER_VERT = 3; // max number of bones per vertex supported by the engine + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_NUM_BONES_PER_VERT)] + public float[] weight; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_NUM_BONES_PER_VERT)] + public char[] bone; + public byte numbones; + + }; +} From 053fbdd3517994568ec1dac20dd029d85824c242 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Thu, 21 Aug 2025 10:08:25 +0300 Subject: [PATCH 02/22] Basic loader VVD --- Sledge.Formats.Model/Source/VvdFile.cs | 75 ++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 Sledge.Formats.Model/Source/VvdFile.cs diff --git a/Sledge.Formats.Model/Source/VvdFile.cs b/Sledge.Formats.Model/Source/VvdFile.cs new file mode 100644 index 0000000..c8194e0 --- /dev/null +++ b/Sledge.Formats.Model/Source/VvdFile.cs @@ -0,0 +1,75 @@ +using Sledge.Formats.FileSystem; +using System.IO; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Sledge.Formats.Model.Source +{ + public class VvdFile + { + public VvdHeader Header { get; set; } + public StudioVertex[] Vertices { get; set; } + public Vector4[] TangentData { get; set; } + public VvdFile(Stream stream) + { + var headerBuf = new byte[Marshal.SizeOf()]; + var vertexSize = Marshal.SizeOf(); + stream.Read(headerBuf, 0, headerBuf.Length); + var handle = GCHandle.Alloc(headerBuf, GCHandleType.Pinned); + Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + handle.Free(); + + var vertexCount = (Header.tangentDataStart - Header.vertexDataStart) / vertexSize; + var vertexBuf = new byte[vertexCount * vertexSize]; + stream.Seek(Header.vertexDataStart, SeekOrigin.Begin); + stream.Read(vertexBuf, 0, vertexBuf.Length); + var vertexHandle = GCHandle.Alloc(vertexBuf, GCHandleType.Pinned); + Vertices = new StudioVertex[vertexCount]; + for(int i = 0; i < vertexCount; i++) + { + var offset = i * vertexSize; + Vertices[i] = Marshal.PtrToStructure(vertexHandle.AddrOfPinnedObject() + offset); + } + vertexHandle.Free(); + + var tangentBuf = new byte[Marshal.SizeOf()]; + var tangentSize = Marshal.SizeOf(); + stream.Read(tangentBuf, 0, tangentBuf.Length); + var tangHandle = GCHandle.Alloc(tangentBuf, GCHandleType.Pinned); + TangentData = new Vector4[vertexCount]; + for (int i = 0; i < vertexCount; i++) + { + var offset = i * tangentSize; + TangentData[i] = Marshal.PtrToStructure(tangHandle.AddrOfPinnedObject() + offset); + } + tangHandle.Free(); + } + + public static VvdFile FromFile(string path) + { + var dir = Path.GetDirectoryName(path); + var fname = Path.GetFileName(path); + + var resolver = new DiskFileResolver(dir); + return FromFile(resolver, fname); + } + public static VvdFile FromFile(IFileResolver resolver, string path) + { + var basedir = (Path.GetDirectoryName(path) ?? "").Replace('\\', '/'); + if (basedir.Length > 0 && !basedir.EndsWith("/")) basedir += "/"; + var basepath = basedir + Path.GetFileNameWithoutExtension(path); + var ext = Path.GetExtension(path); + + try + { + var stream = resolver.OpenFile(path); + + return new VvdFile(stream); + } + finally + { + } + } + + } +} From 278e9d522c6b0cfdf38d7d59371634eb3bcad8c8 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 22 Aug 2025 14:17:39 +0300 Subject: [PATCH 03/22] Structs for vtx file --- Sledge.Formats.Model/Source/VtxStructs.cs | 127 ++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 Sledge.Formats.Model/Source/VtxStructs.cs diff --git a/Sledge.Formats.Model/Source/VtxStructs.cs b/Sledge.Formats.Model/Source/VtxStructs.cs new file mode 100644 index 0000000..0705608 --- /dev/null +++ b/Sledge.Formats.Model/Source/VtxStructs.cs @@ -0,0 +1,127 @@ +using System.Runtime.InteropServices; + +namespace Sledge.Formats.Model.Source +{ + // this structure is in /src/public/optimize.h + public struct VtxHeader + { + // file version as defined by OPTIMIZED_MODEL_FILE_VERSION (currently 7) + public int version; + + // hardware params that affect how the model is to be optimized. + public int vertCacheSize; + public ushort maxBonesPerStrip; + public ushort maxBonesPerTri; + public int maxBonesPerVert; + + // must match checkSum in the .mdl + public int checkSum; + + public int numLODs; // Also specified in ModelHeader_t's and should match + + // Offset to materialReplacementList Array. one of these for each LOD, 8 in total + public int materialReplacementListOffset; + + //Defines the size and location of the body part array + public int numBodyParts; + public int bodyPartOffset; + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct BodyPartHeader + { + //Model array + public int numModels; + public int modelOffset; + public ModelHeader ModelHeader; + + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ModelHeader + { + //LOD mesh array + public int numLODs; //This is also specified in FileHeader_t + public int lodOffset; + public ModelLODHeader ModelLOD; + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ModelLODHeader + { + //Mesh array + public int numMeshes; + public int meshOffset; + public float switchPoint; + public MeshHeader MeshHeader; + }; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct MeshHeader + { + public int numStripGroups; + public int stripGroupHeaderOffset; + public byte flags; + public StripGroupHeader StripGroupHeader; + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StripGroupHeader + { + // These are the arrays of all verts and indices for this mesh. strips index into this. + public int numVerts; + public int vertOffset; + + public int numIndices; + public int indexOffset; + + public int numStrips; + public int stripOffset; + + + public byte flags; + public StripHeader StripHeader; + + // The following fields are only present if MDL version is >=49 + // Points to an array of unsigned shorts (16 bits each) + //public int numTopologyIndices; + //public int topologyOffset; + }; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StripHeader + { + public int numIndices; + public int indexOffset; + public int numVerts; + public int vertOffset; + public short numBones; + public byte flags; + public int numBoneStateChanges; + public int boneStateChangeOffset; + + //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)] + //public Vertex[] verts; + //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 36)] + //public ushort[] indices; + + // MDL Version 49 and up only + //public int numTopologyIndices; + //public int topologyOffset; + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Vertex + { + // these index into the mesh's vert[origMeshVertID]'s bones + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] boneWeightIndex; + public byte numBones; + public ushort origMeshVertID; + + // for sw skinned verts, these are indices into the global list of bones + // for hw skinned verts, these are hardware bone indices + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public byte[] boneID; + }; + public struct MaterialReplacementListHeader + { + public int numReplacements; + public int replacementOffset; + }; +} From 39bc2caf3818a3386081f7a047a215ef9d2654fa Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 22 Aug 2025 14:18:27 +0300 Subject: [PATCH 04/22] Added mdl header --- Sledge.Formats.Model/Source/MdlStructs.cs | 203 ++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 Sledge.Formats.Model/Source/MdlStructs.cs diff --git a/Sledge.Formats.Model/Source/MdlStructs.cs b/Sledge.Formats.Model/Source/MdlStructs.cs new file mode 100644 index 0000000..fa62801 --- /dev/null +++ b/Sledge.Formats.Model/Source/MdlStructs.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; + +namespace Sledge.Formats.Model.Source +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct Studiohdr + { + public int id; // Model format ID, such as "IDST" (0x49 0x44 0x53 0x54) + public int version; // Format version number, such as 48 (0x30,0x00,0x00,0x00) + public int checksum; // This has to be the same in the phy and vtx files to load! + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)] + public char[] name; // The internal name of the model, padding with null bytes. + // Typically "my_model.mdl" will have an internal name of "my_model" + public int dataLength; // Data size of MDL file in bytes. + + // A vector is 12 bytes, three 4-byte float-values in a row. + public Vector3 eyeposition; // Position of player viewpoint relative to model origin + public Vector3 illumposition; // Position (relative to model origin) used to calculate ambient light contribution and cubemap reflections for the entire model. + public Vector3 hull_min; // Corner of model hull box with the least X/Y/Z values + public Vector3 hull_max; // Opposite corner of model hull box + public Vector3 view_bbmin; // Same, but for bounding box, + public Vector3 view_bbmax; // which is used for view culling + + public int flags; // Binary flags in little-endian order. + // ex (0x010000C0) means flags for position 0, 30, and 31 are set. + // Set model flags section for more information + + /* + * After this point, the header contains many references to offsets + * within the MDL file and the number of items at those offsets. + * + * Offsets are from the very beginning of the file. + * + * Note that indexes/counts are not always paired and ordered consistently. + */ + + // mstudiobone_t + public int bone_count; // Number of data sections (of type mstudiobone_t) + public int bone_offset; // Offset of first data section + + // mstudiobonecontroller_t + public int bonecontroller_count; + public int bonecontroller_offset; + + // mstudiohitboxset_t + public int hitbox_count; + public int hitbox_offset; + + // mstudioanimdesc_t + public int localanim_count; + public int localanim_offset; + + // mstudioseqdesc_t + public int localseq_count; + public int localseq_offset; + + public int activitylistversion; // ?? + public int eventsindexed; // ?? + + // VMT texture filenames + // mstudiotexture_t + public int texture_count; + public int texture_offset; + + // This offset points to a series of ints. + // Each int value, in turn, is an offset relative to the start of this header/the-file, + // At which there is a null-terminated string. + public int texturedir_count; + public int texturedir_offset; + + // Each skin-family assigns a texture-id to a skin location + public int skinreference_count; + public int skinrfamily_count; + public int skinreference_index; + + // mstudiobodyparts_t + public int bodypart_count; + public int bodypart_offset; + + // Local attachment points + // mstudioattachment_t + public int attachment_count; + public int attachment_offset; + + // Node values appear to be single bytes, while their names are null-terminated strings. + public int localnode_count; + public int localnode_index; + public int localnode_name_index; + + // mstudioflexdesc_t + public int flexdesc_count; + public int flexdesc_index; + + // mstudioflexcontroller_t + public int flexcontroller_count; + public int flexcontroller_index; + + // mstudioflexrule_t + public int flexrules_count; + public int flexrules_index; + + // IK probably referse to inverse kinematics + // mstudioikchain_t + public int ikchain_count; + public int ikchain_index; + + // Information about any "mouth" on the model for speech animation + // More than one sounds pretty creepy. + // mstudiomouth_t + public int mouths_count; + public int mouths_index; + + // mstudioposeparamdesc_t + public int localposeparam_count; + public int localposeparam_index; + + /* + * For anyone trying to follow along, as of this writing, + * the next "surfaceprop_index" value is at position 0x0134 (308) + * from the start of the file. + */ + + // Surface property value (single null-terminated string) + public int surfaceprop_index; + + // Unusual: In this one index comes first, then count. + // Key-value data is a series of strings. If you can't find + // what you're interested in, check the associated PHY file as well. + public int keyvalue_index; + public int keyvalue_count; + + // More inverse-kinematics + // mstudioiklock_t + public int iklock_count; + public int iklock_index; + + + public float mass; // Mass of object (4-bytes) in kilograms + + public int contents; // contents flag, as defined in bspflags.h + // not all content types are valid; see + // documentation on $contents QC command + + // Other models can be referenced for re-used sequences and animations + // (See also: The $includemodel QC option.) + // mstudiomodelgroup_t + public int includemodel_count; + public int includemodel_index; + + public int virtualModel; // Placeholder for mutable-void* + // Note that the SDK only compiles as 32-bit, so an int and a pointer are the same size (4 bytes) + + // mstudioanimblock_t + public int animblocks_name_index; + public int animblocks_count; + public int animblocks_index; + + public int animblockModel; // Placeholder for mutable-void* + + // Points to a series of bytes? + public int bonetablename_index; + + public int vertex_base; // Placeholder for void* + public int offset_base; // Placeholder for void* + + // Used with $constantdirectionallight from the QC + // Model should have flag #13 set if enabled + public byte directionaldotproduct; + + public byte rootLod; // Preferred rather than clamped + + // 0 means any allowed, N means Lod 0 -> (N-1) + public byte numAllowedRootLods; + + public byte unused0; // ?? + public int unused1; // ?? + + // mstudioflexcontrollerui_t + public int flexcontrollerui_count; + public int flexcontrollerui_index; + + public float vertAnimFixedPointScale; // ?? + public int unused2; + + /** + * Offset for additional header information. + * May be zero if not present, or also 408 if it immediately + * follows this studiohdr_t + */ + // studiohdr2_t + public int studiohdr2index; + + public int unused3; // ?? + + /** + * As of this writing, the header is 408 bytes long in total + */ + }; +} From f782cfb3ddb7dfd2095cfc95824f26229e40bba9 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 22 Aug 2025 17:37:37 +0300 Subject: [PATCH 05/22] Added classes to represent vtx objects --- Sledge.Formats.Model/Source/VtxStructs.cs | 131 ++++++++++++++++++++-- 1 file changed, 119 insertions(+), 12 deletions(-) diff --git a/Sledge.Formats.Model/Source/VtxStructs.cs b/Sledge.Formats.Model/Source/VtxStructs.cs index 0705608..38d104c 100644 --- a/Sledge.Formats.Model/Source/VtxStructs.cs +++ b/Sledge.Formats.Model/Source/VtxStructs.cs @@ -1,7 +1,125 @@ -using System.Runtime.InteropServices; +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.ComTypes; namespace Sledge.Formats.Model.Source { + public class BodyPart + { + public BodyPartHeader BodyPartHeader; + public Model[] Models; + internal void ReadObjects(GCHandle handle, int parentOffset) + { + Models = new Model[BodyPartHeader.numModels]; + var position = BodyPartHeader.modelOffset; + for (int i = 0; i < BodyPartHeader.numModels; i++) + { + Models[i] = new Model(); + var offset = i * Marshal.SizeOf(); + Models[i].ModelHeader = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + parentOffset + position + offset); + Models[i].ReadObjects(handle, parentOffset + position + offset); + } + } + public class Model + { + public ModelHeader ModelHeader; + public ModelLOD[] LOD; + + internal void ReadObjects(GCHandle handle, int parentOffset) + { + LOD = new ModelLOD[ModelHeader.numLODs]; + var position = ModelHeader.lodOffset; + for (int i = 0; i < ModelHeader.numLODs; i++) + { + LOD[i] = new ModelLOD(); + var offset = i * Marshal.SizeOf(); + LOD[i].ModelLODHeader = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + parentOffset + position + offset); + LOD[i].ReadObjects(handle, parentOffset + position + offset); + } + } + + public class ModelLOD + { + public ModelLODHeader ModelLODHeader; + public Mesh[] Meshes; + + internal void ReadObjects(GCHandle handle, int parentOffset) + { + Meshes = new Mesh[ModelLODHeader.numMeshes]; + var position = ModelLODHeader.meshOffset; + for (int i = 0; i < ModelLODHeader.numMeshes; i++) + { + Meshes[i] = new Mesh(); + var offset = i * Marshal.SizeOf(); + Meshes[i].MeshHeader = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + parentOffset + position + offset); + Meshes[i].ReadObjects(handle, parentOffset + position + offset); + } + } + + public class Mesh + { + public MeshHeader MeshHeader; + public StripGroup[] StripGroups; + + internal void ReadObjects(GCHandle handle, int parentOffset) + { + StripGroups = new StripGroup[MeshHeader.numStripGroups]; + var position = MeshHeader.stripGroupHeaderOffset; + for (int i = 0; i < MeshHeader.numStripGroups; i++) + { + StripGroups[i] = new StripGroup(); + var offset = i * Marshal.SizeOf(); + StripGroups[i].StripGroupHeader = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + parentOffset + position + offset); + StripGroups[i].ReadObjects(handle, parentOffset + position + offset); + } + + } + + public class StripGroup + { + public StripGroupHeader StripGroupHeader; + public Strip[] Strips; + + internal void ReadObjects(GCHandle handle, int parentOffset) + { + Strips = new Strip[StripGroupHeader.numStrips]; + var position = StripGroupHeader.stripOffset; + for (int i = 0; i < StripGroupHeader.numStrips; i++) + { + Strips[i] = new Strip(); + var offset = i * Marshal.SizeOf(); + Strips[i].StripHeader = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + parentOffset + position + offset); + // Read vertices + Strips[i].Verts = new Vertex[Strips[i].StripHeader.numVerts]; + var vertPosition = parentOffset + StripGroupHeader.vertOffset + Strips[i].StripHeader.vertOffset; + for (int v = 0; v < Strips[i].StripHeader.numVerts; v++) + { + var vertOffset = v * Marshal.SizeOf(); + Strips[i].Verts[v] = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + vertPosition + vertOffset); + } + // Read indices + Strips[i].Indices = new ushort[Strips[i].StripHeader.numIndices]; + var indexPosition = StripGroupHeader.indexOffset + Strips[i].StripHeader.indexOffset + parentOffset; + for (int idx = 0; idx < Strips[i].StripHeader.numIndices; idx++) + { + var indexOffset = idx * sizeof(ushort); + Strips[i].Indices[idx] = (ushort)Marshal.ReadInt16(handle.AddrOfPinnedObject() + indexPosition + indexOffset); + } + } + + } + + public class Strip + { + public StripHeader StripHeader; + public Vertex[] Verts; + public ushort[] Indices; + } + } + } + } + } + } // this structure is in /src/public/optimize.h public struct VtxHeader { @@ -32,8 +150,6 @@ public struct BodyPartHeader //Model array public int numModels; public int modelOffset; - public ModelHeader ModelHeader; - }; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ModelHeader @@ -41,7 +157,6 @@ public struct ModelHeader //LOD mesh array public int numLODs; //This is also specified in FileHeader_t public int lodOffset; - public ModelLODHeader ModelLOD; }; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ModelLODHeader @@ -50,7 +165,6 @@ public struct ModelLODHeader public int numMeshes; public int meshOffset; public float switchPoint; - public MeshHeader MeshHeader; }; [StructLayout(LayoutKind.Sequential, Pack = 1)] @@ -59,7 +173,6 @@ public struct MeshHeader public int numStripGroups; public int stripGroupHeaderOffset; public byte flags; - public StripGroupHeader StripGroupHeader; }; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct StripGroupHeader @@ -76,7 +189,6 @@ public struct StripGroupHeader public byte flags; - public StripHeader StripHeader; // The following fields are only present if MDL version is >=49 // Points to an array of unsigned shorts (16 bits each) @@ -96,11 +208,6 @@ public struct StripHeader public int numBoneStateChanges; public int boneStateChangeOffset; - //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)] - //public Vertex[] verts; - //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 36)] - //public ushort[] indices; - // MDL Version 49 and up only //public int numTopologyIndices; //public int topologyOffset; From 84421e3934d78da189f66a5b52b848e85db3bf19 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 22 Aug 2025 19:03:12 +0300 Subject: [PATCH 06/22] Added vtx file reader --- Sledge.Formats.Model/Source/VtxFile.cs | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 Sledge.Formats.Model/Source/VtxFile.cs diff --git a/Sledge.Formats.Model/Source/VtxFile.cs b/Sledge.Formats.Model/Source/VtxFile.cs new file mode 100644 index 0000000..272b91f --- /dev/null +++ b/Sledge.Formats.Model/Source/VtxFile.cs @@ -0,0 +1,61 @@ +using Sledge.Formats.FileSystem; +using System.IO; +using System.Runtime.InteropServices; + +namespace Sledge.Formats.Model.Source +{ + public class VtxFile + { + public VtxHeader Header; + public BodyPart[] BodyParts; + + public VtxFile(Stream stream) + { + var buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + + var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + + BodyParts = new BodyPart[Header.numBodyParts]; + var position = Header.bodyPartOffset; + + for (int i = 0; i < Header.numBodyParts; i++) + { + BodyParts[i] = new BodyPart(); + var offset = i * Marshal.SizeOf(); + BodyParts[i].BodyPartHeader = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + position + offset); + BodyParts[i].ReadObjects(handle, position + offset); + } + + handle.Free(); + } + + public static VtxFile FromFile(string path) + { + var dir = Path.GetDirectoryName(path); + var fname = Path.GetFileName(path); + + var resolver = new DiskFileResolver(dir); + return FromFile(resolver, fname); + } + public static VtxFile FromFile(IFileResolver resolver, string path) + { + var basedir = (Path.GetDirectoryName(path) ?? "").Replace('\\', '/'); + if (basedir.Length > 0 && !basedir.EndsWith("/")) basedir += "/"; + var basepath = basedir + Path.GetFileNameWithoutExtension(path); + var ext = Path.GetExtension(path); + + try + { + using (var stream = resolver.OpenFile(path)) + { + return new VtxFile(stream); + } + } + finally + { + } + } + } +} From 2e0dc57471bbde635b357435c1601f27e8559b6e Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 22 Aug 2025 19:24:05 +0300 Subject: [PATCH 07/22] Added minimal mdl file reader --- Sledge.Formats.Model/Source/MdlFile.cs | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Sledge.Formats.Model/Source/MdlFile.cs diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs new file mode 100644 index 0000000..b95b339 --- /dev/null +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -0,0 +1,83 @@ +using Sledge.Formats.FileSystem; +using System.IO; +using System.Runtime.InteropServices; + +namespace Sledge.Formats.Model.Source +{ + public class MdlFile + { + public Studiohdr Header { get; set; } + public string[] Materials { get; set; } + public string MaterialDirectory { get; set; } + + public VtxFile VtxFile { get; set; } + public VvdFile VvdFile { get; set; } + + + public MdlFile(Stream stream) + { + var headerBuf = new byte[Marshal.SizeOf()]; + var vertexSize = Marshal.SizeOf(); + stream.Read(headerBuf, 0, headerBuf.Length); + var handle = GCHandle.Alloc(headerBuf, GCHandleType.Pinned); + Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + handle.Free(); + var materialCount = Header.texture_count; + Materials = new string[materialCount]; + stream.Seek(Header.texture_offset, SeekOrigin.Begin); + + using (var br = new BinaryReader(stream)) + { + int texturesOffset = br.ReadInt32(); + stream.Seek(texturesOffset + Header.texture_offset, SeekOrigin.Begin); + for (int i = 0; i < materialCount; i++) + { + Materials[i] = br.ReadNullTerminatedString(); + } + stream.Seek(Header.texturedir_offset, SeekOrigin.Begin); + var dirOffset = br.ReadInt32(); + stream.Seek(dirOffset, SeekOrigin.Begin); + MaterialDirectory = br.ReadNullTerminatedString(); + } + } + + public static MdlFile FromFile(string path) + { + var dir = Path.GetDirectoryName(path); + var fname = Path.GetFileName(path); + + var resolver = new DiskFileResolver(dir); + var file = FromFile(resolver, fname); + // TODO: Prioritize dx90 + var vtxPath = Path.ChangeExtension(path, ".dx90.vtx"); + if (resolver.FileExists(vtxPath)) + { + file.VtxFile = VtxFile.FromFile(resolver, vtxPath); + } + var vvdPath = Path.ChangeExtension(path, ".vvd"); + if (resolver.FileExists(vvdPath)) + { + file.VvdFile = VvdFile.FromFile(resolver, vvdPath); + } + + return file; + } + public static MdlFile FromFile(IFileResolver resolver, string path) + { + var basedir = (Path.GetDirectoryName(path) ?? "").Replace('\\', '/'); + if (basedir.Length > 0 && !basedir.EndsWith("/")) basedir += "/"; + var basepath = basedir + Path.GetFileNameWithoutExtension(path); + var ext = Path.GetExtension(path); + + try + { + var stream = resolver.OpenFile(path); + + return new MdlFile(stream); + } + finally + { + } + } + } +} From 76276bf108140081a3d59820382791f155529ffe Mon Sep 17 00:00:00 2001 From: Duude92 Date: Sat, 23 Aug 2025 10:01:17 +0300 Subject: [PATCH 08/22] Added StripFlags enum --- Sledge.Formats.Model/Source/VtxStructs.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sledge.Formats.Model/Source/VtxStructs.cs b/Sledge.Formats.Model/Source/VtxStructs.cs index 38d104c..a0467a6 100644 --- a/Sledge.Formats.Model/Source/VtxStructs.cs +++ b/Sledge.Formats.Model/Source/VtxStructs.cs @@ -204,7 +204,7 @@ public struct StripHeader public int numVerts; public int vertOffset; public short numBones; - public byte flags; + public StripFlags flags; public int numBoneStateChanges; public int boneStateChangeOffset; @@ -212,6 +212,11 @@ public struct StripHeader //public int numTopologyIndices; //public int topologyOffset; }; + public enum StripFlags : byte + { + STRIP_IS_TRILIST = 0x01, + STRIP_IS_TRISTRIP = 0x02, + }; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct Vertex { From fd5a052be6a03a0b92fd9ffa48e56d246a11555b Mon Sep 17 00:00:00 2001 From: Duude92 Date: Sat, 23 Aug 2025 16:00:17 +0300 Subject: [PATCH 09/22] Update StripGroupHeader flags --- Sledge.Formats.Model/Source/VtxStructs.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sledge.Formats.Model/Source/VtxStructs.cs b/Sledge.Formats.Model/Source/VtxStructs.cs index a0467a6..06342c2 100644 --- a/Sledge.Formats.Model/Source/VtxStructs.cs +++ b/Sledge.Formats.Model/Source/VtxStructs.cs @@ -188,13 +188,20 @@ public struct StripGroupHeader public int stripOffset; - public byte flags; + public StripGroupFlags flags; // The following fields are only present if MDL version is >=49 // Points to an array of unsigned shorts (16 bits each) //public int numTopologyIndices; //public int topologyOffset; }; + public enum StripGroupFlags : byte + { + STRIPGROUP_IS_FLEXED = 0x01, + STRIPGROUP_IS_HWSKINNED = 0x02, + STRIPGROUP_IS_DELTA_FLEXED = 0x04, + STRIPGROUP_SUPPRESS_HW_MORPH = 0x08, // NOTE: This is a temporary flag used at run time. + }; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct StripHeader From 5ada9494406aad69f53891b7f1bd202cc211e1a9 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Sat, 23 Aug 2025 16:00:42 +0300 Subject: [PATCH 10/22] Add couple new structs --- Sledge.Formats.Model/Source/MdlStructs.cs | 146 +++++++++++++++++++++- 1 file changed, 143 insertions(+), 3 deletions(-) diff --git a/Sledge.Formats.Model/Source/MdlStructs.cs b/Sledge.Formats.Model/Source/MdlStructs.cs index fa62801..7c18919 100644 --- a/Sledge.Formats.Model/Source/MdlStructs.cs +++ b/Sledge.Formats.Model/Source/MdlStructs.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.IO; using System.Numerics; using System.Runtime.InteropServices; -using System.Text; namespace Sledge.Formats.Model.Source { @@ -200,4 +198,146 @@ public struct Studiohdr * As of this writing, the header is 408 bytes long in total */ }; + public class Bone + { + public StudioHdrBone Data { get; set; } + public string BoneName { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + br.BaseStream.Seek(offset + Data.bone_name_offset, SeekOrigin.Begin); + BoneName = br.ReadNullTerminatedString(); + } + } + public struct StudioHdrBone + { + public uint bone_name_offset; + public int parent; // parent bone + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public int[] bonecontroller; // bone controller index, -1 == none + // default values + public Vector3 pos; + public Vector4 quat; + public Vector3 rot; + // compression scale + public Vector3 posscale; + public Vector3 rotscale; + public Matrix3x4 poseToBone; + public Vector4 qAlignment; + public int flags; + public int proctype; + public int procindex; // procedural rule + public int physicsbone; // index into physically simulated bone + + public int surfacepropidx; // index into string tablefor property name + + public int contents; // See BSPFlags.h for the contents flags + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public int[] unused; // remove as appropriate + } + public class HitboxSet + { + public StudioHdrHitboxSet Header { get; set; } + public string Name { get; set; } + public Hitbox[] Hitboxes { get; set; } + + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + br.BaseStream.Seek(offset + Header.sznameindex, SeekOrigin.Begin); + Name = br.ReadNullTerminatedString(); + Hitboxes = new Hitbox[Header.numhitboxes]; + for (int i = 0; i < Header.numhitboxes; i++) + { + Hitboxes[i] = new Hitbox(); + var hitboxOffset = offset + Header.hitboxindex + i * Marshal.SizeOf(); + Hitboxes[i].ReadObjects(handle, br, hitboxOffset); + } + } + public class Hitbox + { + public StudioHdrHitbox Data { get; set; } + public string Name { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + br.BaseStream.Seek(Data.szhitboxnameindex, SeekOrigin.Begin); + Name = br.ReadNullTerminatedString(); + } + } + + } + + public struct StudioHdrHitbox + { + public int bone; + public int group; // intersection group + public Vector3 bbmin; // bounding box + public Vector3 bbmax; + public int szhitboxnameindex; // offset to the name of the hitbox. + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public int[] unused; + + }; + public struct StudioHdrHitboxSet + { + public int sznameindex; + public int numhitboxes; + public int hitboxindex; + }; + + public class AnimDescription + { + public StudioHdrAnimDesc Data { get; set; } + public string Name { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + br.BaseStream.Seek(offset + Data.sznameindex, SeekOrigin.Begin); + Name = br.ReadNullTerminatedString(); + } + } + public struct StudioHdrAnimDesc + { + public int baseptr; + public int sznameindex; + public float fps; // frames per second + public int flags; // looping/non-looping flags + public int numframes; + + // piecewise movement + public int nummovements; + public int movementindex; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public int[] unused1; // remove as appropriate (and zero if loading older versions) + public int animblock; + public int animindex; // non-zero when anim data isn't in sections + public int numikrules; + public int ikruleindex; // non-zero when IK data is stored in the mdl + public int animblockikruleindex; // non-zero when IK data is stored in animblock file + public int numlocalhierarchy; + public int localhierarchyindex; + public int sectionindex; + public int sectionframes; // number of frames used in each fast lookup section, zero if not used + public short zeroframespan; // frames per span + public short zeroframecount; // number of spans + public int zeroframeindex; + public float zeroframestalltime; // saved during read stalls + }; + + public struct Matrix3x4 + { + public float m11; + public float m12; + public float m13; + public float m21; + public float m22; + public float m23; + public float m31; + public float m32; + public float m33; + public float m41; + public float m42; + public float m43; + } } From e5514c7fbfc69aac367d03144cff4d1b0f42cfd2 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Sat, 23 Aug 2025 16:44:29 +0300 Subject: [PATCH 11/22] Read of added structs --- Sledge.Formats.Model/Source/MdlFile.cs | 53 +++++++++++++++++++++----- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs index b95b339..6641ed8 100644 --- a/Sledge.Formats.Model/Source/MdlFile.cs +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -7,6 +7,13 @@ namespace Sledge.Formats.Model.Source public class MdlFile { public Studiohdr Header { get; set; } + public Bone[] Bones { get; set; } + // TODO: BoneControllers + public HitboxSet[] HitboxSets { get; set; } + public AnimDescription[] AnimDescriptions { get; set; } + + + public string[] Materials { get; set; } public string MaterialDirectory { get; set; } @@ -16,18 +23,42 @@ public class MdlFile public MdlFile(Stream stream) { - var headerBuf = new byte[Marshal.SizeOf()]; - var vertexSize = Marshal.SizeOf(); - stream.Read(headerBuf, 0, headerBuf.Length); - var handle = GCHandle.Alloc(headerBuf, GCHandleType.Pinned); - Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); - handle.Free(); - var materialCount = Header.texture_count; - Materials = new string[materialCount]; - stream.Seek(Header.texture_offset, SeekOrigin.Begin); - using (var br = new BinaryReader(stream)) { + var buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + + Bones = new Bone[Header.bone_count]; + for (int i = 0; i < Header.bone_count; i++) + { + Bones[i] = new Bone(); + var offset = Header.bone_offset + i * Marshal.SizeOf(); + + Bones[i].ReadObjects(handle, br, offset); + } + HitboxSets = new HitboxSet[Header.hitbox_count]; + for (int i = 0; i < Header.hitbox_count; i++) + { + HitboxSets[i] = new HitboxSet(); + var offset = Header.hitbox_offset + i * Marshal.SizeOf(); + HitboxSets[i].ReadObjects(handle, br, offset); + } + AnimDescriptions = new AnimDescription[Header.localanim_count]; + for (int i = 0; i < Header.localanim_count; i++) + { + AnimDescriptions[i] = new AnimDescription(); + var offset = Header.localanim_offset + i * Marshal.SizeOf(); + AnimDescriptions[i].ReadObjects(handle, br, offset); + } + + + var materialCount = Header.texture_count; + Materials = new string[materialCount]; + stream.Seek(Header.texture_offset, SeekOrigin.Begin); + + int texturesOffset = br.ReadInt32(); stream.Seek(texturesOffset + Header.texture_offset, SeekOrigin.Begin); for (int i = 0; i < materialCount; i++) @@ -38,7 +69,9 @@ public MdlFile(Stream stream) var dirOffset = br.ReadInt32(); stream.Seek(dirOffset, SeekOrigin.Begin); MaterialDirectory = br.ReadNullTerminatedString(); + handle.Free(); } + } public static MdlFile FromFile(string path) From 6babbc9fb603ae294016fa827cb9801676abe3a3 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Sun, 24 Aug 2025 13:43:04 +0300 Subject: [PATCH 12/22] Fix quaternion type --- Sledge.Formats.Model/Source/MdlStructs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sledge.Formats.Model/Source/MdlStructs.cs b/Sledge.Formats.Model/Source/MdlStructs.cs index 7c18919..49846a2 100644 --- a/Sledge.Formats.Model/Source/MdlStructs.cs +++ b/Sledge.Formats.Model/Source/MdlStructs.cs @@ -217,7 +217,7 @@ public struct StudioHdrBone public int[] bonecontroller; // bone controller index, -1 == none // default values public Vector3 pos; - public Vector4 quat; + public Quaternion quat; public Vector3 rot; // compression scale public Vector3 posscale; From 2af115780ca9db7c5b070a77c16d8063e7097e8e Mon Sep 17 00:00:00 2001 From: Duude92 Date: Tue, 26 Aug 2025 19:59:38 +0300 Subject: [PATCH 13/22] Added bit more structs//added header flags enum --- Sledge.Formats.Model/Source/MdlFile.cs | 8 + Sledge.Formats.Model/Source/MdlStructs.cs | 228 +++++++++++++++++++++- 2 files changed, 233 insertions(+), 3 deletions(-) diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs index 6641ed8..f11b7d9 100644 --- a/Sledge.Formats.Model/Source/MdlFile.cs +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -11,6 +11,7 @@ public class MdlFile // TODO: BoneControllers public HitboxSet[] HitboxSets { get; set; } public AnimDescription[] AnimDescriptions { get; set; } + public StudioSequenceDescription[] Sequences { get; set; } @@ -52,6 +53,13 @@ public MdlFile(Stream stream) var offset = Header.localanim_offset + i * Marshal.SizeOf(); AnimDescriptions[i].ReadObjects(handle, br, offset); } + Sequences = new StudioSequenceDescription[Header.localseq_count]; + for (int i = 0; i < Header.localseq_count; i++) + { + Sequences[i] = new StudioSequenceDescription(); + var offset = Header.localseq_offset + i * Marshal.SizeOf(); + Sequences[i].ReadObjects(handle, br, offset); + } var materialCount = Header.texture_count; diff --git a/Sledge.Formats.Model/Source/MdlStructs.cs b/Sledge.Formats.Model/Source/MdlStructs.cs index 49846a2..425315f 100644 --- a/Sledge.Formats.Model/Source/MdlStructs.cs +++ b/Sledge.Formats.Model/Source/MdlStructs.cs @@ -4,6 +4,79 @@ namespace Sledge.Formats.Model.Source { + public enum StudioHdrFlags : int + { + // This flag is set if no hitbox information was specified + STUDIOHDR_FLAGS_AUTOGENERATED_HITBOX = 0x00000001, + + //NOTE,: This flag is set at loadtime, not mdl build time so that we don't have to rebuild + // = models ,when we change materials. + STUDIOHDR_FLAGS_USES_ENV_CUBEMAP = 0x00000002, + + //Use ,this when there are translucent parts to the model but we're not going to sort it + STUDIOHDR_FLAGS_FORCE_OPAQUE = 0x00000004, + + //Use ,this when we want to render the opaque parts during the opaque pass + // = and ,the translucent parts during the translucent pass + STUDIOHDR_FLAGS_TRANSLUCENT_TWOPASS = 0x00000008, + + //This ,is set any time the .qc files has $staticprop in it + // = Means ,there's no bones and no transforms + STUDIOHDR_FLAGS_STATIC_PROP = 0x00000010, + + //NOTE,: This flag is set at loadtime, not mdl build time so that we don't have to rebuild + // = models ,when we change materials. + STUDIOHDR_FLAGS_USES_FB_TEXTURE = 0x00000020, + + //This ,flag is set by studiomdl.exe if a separate "$shadowlod" entry was present + // = for ,the .mdl (the shadow lod is the last entry in the lod list if present) + STUDIOHDR_FLAGS_HASSHADOWLOD = 0x00000040, + + //NOTE,: This flag is set at loadtime, not mdl build time so that we don't have to rebuild + // = models ,when we change materials. + STUDIOHDR_FLAGS_USES_BUMPMAPPING = 0x00000080, + + //NOTE,: This flag is set when we should use the actual materials on the shadow LOD + // = instead ,of overriding them with the default one (necessary for translucent shadows) + STUDIOHDR_FLAGS_USE_SHADOWLOD_MATERIALS = 0x00000100, + + //NOTE,: This flag is set when we should use the actual materials on the shadow LOD + // = instead ,of overriding them with the default one (necessary for translucent shadows) + STUDIOHDR_FLAGS_OBSOLETE = 0x00000200, + + STUDIOHDR_FLAGS_UNUSED = 0x00000400, + + //NOTE,: This flag is set at mdl build time + STUDIOHDR_FLAGS_NO_FORCED_FADE = 0x00000800, + + //NOTE,: The npc will lengthen the viseme check to always include two phonemes + STUDIOHDR_FLAGS_FORCE_PHONEME_CROSSFADE = 0x00001000, + + //This ,flag is set when the .qc has $constantdirectionallight in it + // = If ,set, we use constantdirectionallightdot to calculate light intensity + // = rather ,than the normal directional dot product + // = only ,valid if STUDIOHDR_FLAGS_STATIC_PROP is also set + STUDIOHDR_FLAGS_CONSTANT_DIRECTIONAL_LIGHT_DOT = 0x00002000, + + //Flag ,to mark delta flexes as already converted from disk format to memory format + STUDIOHDR_FLAGS_FLEXES_CONVERTED = 0x00004000, + + //Indicates ,the studiomdl was built in preview mode + STUDIOHDR_FLAGS_BUILT_IN_PREVIEW_MODE = 0x00008000, + + //Ambient ,boost (runtime flag) + STUDIOHDR_FLAGS_AMBIENT_BOOST = 0x00010000, + + //Don,'t cast shadows from this model (useful on first-person models) + STUDIOHDR_FLAGS_DO_NOT_CAST_SHADOWS = 0x00020000, + + // alpha ,textures should cast shadows in vrad on this model (ONLY prop_static!) + STUDIOHDR_FLAGS_CAST_TEXTURE_SHADOWS = 0x00040000, + + + // ,= flagged ,on load to indicate no animation events on this model + STUDIOHDR_FLAGS_VERT_ANIM_FIXED_POINT_SCALE = 0x00200000, + } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct Studiohdr { @@ -23,7 +96,7 @@ public struct Studiohdr public Vector3 view_bbmin; // Same, but for bounding box, public Vector3 view_bbmax; // which is used for view culling - public int flags; // Binary flags in little-endian order. + public StudioHdrFlags flags; // Binary flags in little-endian order. // ex (0x010000C0) means flags for position 0, 30, and 31 are set. // Set model flags section for more information @@ -198,6 +271,127 @@ public struct Studiohdr * As of this writing, the header is 408 bytes long in total */ }; + public class StudioSequenceDescription + { + public StudioSequenceDesc Data { get; set; } + public string Label { get; set; } + public string ActivityName { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + br.BaseStream.Seek(offset + Data.szlabelindex, SeekOrigin.Begin); + Label = br.ReadNullTerminatedString(); + br.BaseStream.Seek(offset + Data.szactivitynameindex, SeekOrigin.Begin); + ActivityName = br.ReadNullTerminatedString(); + } + + } + public struct StudioSequenceDesc + { + public int baseptr; + //inline studiohdr_t *pStudiohdr( void ) const { return (studiohdr_t *)(((byte *)this) + baseptr); } + + public int szlabelindex; + + public int szactivitynameindex; + + public int flags; // looping/non-looping flags + + public int activity; // initialized at loadtime to game DLL values + public int actweight; + + public int numevents; + public int eventindex; + //inline mstudioevent_t *pEvent( int i ) const { Assert( i >= 0 && i < numevents); return (mstudioevent_t *)(((byte *)this) + eventindex) + i; }; + + public Vector3 bbmin; // per sequence bounding box + public Vector3 bbmax; + + public int numblends; + + // Index into array of shorts which is groupsize[0] x groupsize[1] in length + public int animindexindex; + + //inline int anim( int x, int y ) const + //{ + // if ( x >= groupsize[0] ) + // { + // x = groupsize[0] - 1; + // } + + // if ( y >= groupsize[1] ) + // { + // y = groupsize[ 1 ] - 1; + // } + + // int offset = y * groupsize[0] + x; + // short *blends = (short *)(((byte *)this) + animindexindex); + // int value = (int)blends[ offset ]; + // return value; + //} + + public int movementindex; // [blend] float array for blended movement + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public int[] groupsize; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + + public int[] paramindex; // X, Y, Z, XR, YR, ZR + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + + public float[] paramstart; // local (0..1) starting value + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + + public float[] paramend; // local (0..1) ending value + public int paramparent; + + public float fadeintime; // ideal cross fate in time (0.2 default) + public float fadeouttime; // ideal cross fade out time (0.2 default) + + public int localentrynode; // transition node at entry + public int localexitnode; // transition node at exit + public int nodeflags; // transition rules + + public float entryphase; // used to match entry gait + public float exitphase; // used to match exit gait + + public float lastframe; // frame that should generation EndOfSequence + + public int nextseq; // auto advancing sequences + public int pose; // index of delta animation between end and nextseq + + public int numikrules; + + public int numautolayers; // + public int autolayerindex; + //inline mstudioautolayer_t *pAutolayer( int i ) const { Assert( i >= 0 && i < numautolayers); return (mstudioautolayer_t *)(((byte *)this) + autolayerindex) + i; }; + + public int weightlistindex; + //inline float *pBoneweight( int i ) const { return ((float *)(((byte *)this) + weightlistindex) + i); }; + //inline float weight( int i ) const { return *(pBoneweight( i)); }; + + // FIXME: make this 2D instead of 2x1D arrays + public int posekeyindex; + //float *pPoseKey( int iParam, int iAnim ) const { return (float *)(((byte *)this) + posekeyindex) + iParam * groupsize[0] + iAnim; } + //float poseKey( int iParam, int iAnim ) const { return *(pPoseKey( iParam, iAnim )); } + + public int numiklocks; + public int iklockindex; + //inline mstudioiklock_t *pIKLock( int i ) const { Assert( i >= 0 && i < numiklocks); return (mstudioiklock_t *)(((byte *)this) + iklockindex) + i; }; + + // Key values + public int keyvalueindex; + public int keyvaluesize; + //inline const char * KeyValueText( void ) const { return keyvaluesize != 0 ? ((char *)this) + keyvalueindex : NULL; } + + public int cycleposeindex; // index of pose parameter to use as cycle index + + public int activitymodifierindex; + public int numactivitymodifiers; + //inline mstudioactivitymodifier_t *pActivityModifier( int i ) const { Assert( i >= 0 && i < numactivitymodifiers); return activitymodifierindex != 0 ? (mstudioactivitymodifier_t *)(((byte *)this) + activitymodifierindex) + i : NULL; }; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] + int[] unused; // remove/add as appropriate (grow back to 8 ints on version change!) + + }; public class Bone { public StudioHdrBone Data { get; set; } @@ -289,22 +483,34 @@ public struct StudioHdrHitboxSet public class AnimDescription { public StudioHdrAnimDesc Data { get; set; } + public StudioMovement[] Movement { get; set; } + public StudioAnimSection[] Frames { get; set; } public string Name { get; set; } internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) { Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); br.BaseStream.Seek(offset + Data.sznameindex, SeekOrigin.Begin); + Movement = new StudioMovement[Data.nummovements]; + Frames = new StudioAnimSection[Data.sectionframes]; + for (int i = 0; i < Data.nummovements; i++) + { + Movement[i] = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset + Data.movementindex); + } + for (int i = 0; i < Data.sectionframes; i++) + { + Frames[i] = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset + Data.sectionindex); + } Name = br.ReadNullTerminatedString(); } } public struct StudioHdrAnimDesc { - public int baseptr; + public uint baseptr; public int sznameindex; public float fps; // frames per second public int flags; // looping/non-looping flags public int numframes; - + // piecewise movement public int nummovements; public int movementindex; @@ -324,6 +530,22 @@ public struct StudioHdrAnimDesc public int zeroframeindex; public float zeroframestalltime; // saved during read stalls }; + public struct StudioAnimSection + { + public int animblock; + public int animindex; + }; + + public struct StudioMovement + { + public int endframe; + public int motionflags; + public float v0; // velocity at start of block + public float v1; // velocity at end of block + public float angle; // YAW rotation at end of this blocks movement + public Vector3 vector; // movement vector relative to this blocks initial angle + public Vector3 position; // relative to start of animation??? + }; public struct Matrix3x4 { From 0fc440abbb8180342d6fa5cb66a2ca7f97cc3bb4 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Wed, 27 Aug 2025 15:19:45 +0300 Subject: [PATCH 14/22] Fix vvd vertex count --- Sledge.Formats.Model/Source/VvdFile.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sledge.Formats.Model/Source/VvdFile.cs b/Sledge.Formats.Model/Source/VvdFile.cs index c8194e0..32ace7e 100644 --- a/Sledge.Formats.Model/Source/VvdFile.cs +++ b/Sledge.Formats.Model/Source/VvdFile.cs @@ -19,7 +19,7 @@ public VvdFile(Stream stream) Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); handle.Free(); - var vertexCount = (Header.tangentDataStart - Header.vertexDataStart) / vertexSize; + var vertexCount = Header.numLODVertexes[0]; var vertexBuf = new byte[vertexCount * vertexSize]; stream.Seek(Header.vertexDataStart, SeekOrigin.Begin); stream.Read(vertexBuf, 0, vertexBuf.Length); From 6b77ee3252a4615a80b29cdce38d74e5ec688f70 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Thu, 28 Aug 2025 13:37:37 +0300 Subject: [PATCH 15/22] Added structs for bodyparts/meshes --- Sledge.Formats.Model/Source/MdlFile.cs | 10 + Sledge.Formats.Model/Source/MdlStructs.cs | 239 +++++++++++++++++++++- 2 files changed, 247 insertions(+), 2 deletions(-) diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs index f11b7d9..25ee32b 100644 --- a/Sledge.Formats.Model/Source/MdlFile.cs +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -12,6 +12,8 @@ public class MdlFile public HitboxSet[] HitboxSets { get; set; } public AnimDescription[] AnimDescriptions { get; set; } public StudioSequenceDescription[] Sequences { get; set; } + public short[] SkinRef { get; set; } //?? + public Bodypart[] Bodyparts { get; set; } @@ -61,6 +63,14 @@ public MdlFile(Stream stream) Sequences[i].ReadObjects(handle, br, offset); } + Bodyparts = new Bodypart[Header.bodypart_count]; + for (int i = 0; i < Header.bodypart_count; i++) + { + Bodyparts[i] = new Bodypart(); + var offset = Header.bodypart_offset + i * Marshal.SizeOf(); + Bodyparts[i].ReadObjects(handle, br, offset); + } + var materialCount = Header.texture_count; Materials = new string[materialCount]; diff --git a/Sledge.Formats.Model/Source/MdlStructs.cs b/Sledge.Formats.Model/Source/MdlStructs.cs index 425315f..d8c4dbf 100644 --- a/Sledge.Formats.Model/Source/MdlStructs.cs +++ b/Sledge.Formats.Model/Source/MdlStructs.cs @@ -97,8 +97,8 @@ public struct Studiohdr public Vector3 view_bbmax; // which is used for view culling public StudioHdrFlags flags; // Binary flags in little-endian order. - // ex (0x010000C0) means flags for position 0, 30, and 31 are set. - // Set model flags section for more information + // ex (0x010000C0) means flags for position 0, 30, and 31 are set. + // Set model flags section for more information /* * After this point, the header contains many references to offsets @@ -562,4 +562,239 @@ public struct Matrix3x4 public float m42; public float m43; } + + public struct StudioMeshData + { + // indirection to this mesh's model's vertex data + //#ifndef PLATFORM_64BITS + public int modelVertexDataIndex; + // const mstudio_modelvertexdata_t *modelvertexdata; + //#else + // int unused_modelvertexdata; + //#endif + + // used for fixup calcs when culling top level lods + // expected number of mesh verts at desired lod + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public int[] numLODVertexes; + + //#ifdef PLATFORM_64BITS + // serializedstudioptr_t< const mstudio_modelvertexdata_t > modelvertexdata; + //#endif + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StudioFlex + { + public int flexdesc; // input value + public float target0; // zero + public float target1; // one + public float target2; // one + public float target3; // zero + public int numverts; + public int vertindex; + public int flexpair; // second flex desc + public char vertanimtype; // See StudioVertAnimType_t + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + char[] unusedchar; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + int[] unused; + }; + + public struct StudioMesh + { + public int material; + public int modelindex; + public int numvertices; // number of unique vertices/normals/texcoords + public int vertexoffset; // vertex mstudiovertex_t + public int numflexes; // vertex animation + public int flexindex; + + // special codes for material operations + public int materialtype; + public int materialparam; + + // a unique ordinal for this mesh + public int meshid; + public Vector3 center; + public StudioMeshData vertexdata; + + //#ifdef PLATFORM_64BITS + // int unused[6]; // remove as appropriate + //#else + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + int[] unused; // remove as appropriate + //#endif + + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StudioEyeball + { + public int sznameindex; + public int bone; + public Vector3 org; + public float zoffset; + public float radius; + public Vector3 up; + public Vector3 forward; + public int texture; + public int unused1; + public float iris_scale; + public int unused2; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public int[] upperflexdesc; // index of raiser, neutral, and lowerer flexdesc that is set by flex controllers + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public int[] lowerflexdesc; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public float[] uppertarget; // angle (radians) of raised, neutral, and lowered lid positions + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public float lowertarget; + public int[] upperlidflexdesc; // index of flex desc that actual lid flexes look to + public int lowerlidflexdesc; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public int[] unused; // These were used before, so not guaranteed to be 0 + public bool m_bNonFACS; // Never used before version 44 + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] + public char[] unused3; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 7)] + public int[] unused4; + }; + public struct mstudio_modelvertexdata_t + { + + // base of external vertex data stores + int pVertexData; + int pTangentData; + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StudioModel + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)] + public char[] name; + public int type; + public float boundingradius; + public int nummeshes; + public int meshindex; + + // cache purposes + public int numvertices; // number of unique vertices/normals/texcoords + public int vertexindex; // vertex Vector + public int tangentsindex; // tangents Vector + public int numattachments; + public int attachmentindex; + public int numeyeballs; + public int eyeballindex; + + public mstudio_modelvertexdata_t vertexdata; + + //#ifdef PLATFORM_64BITS + // int unused[6]; // mstudio_modelvertexdata_t has 2 naked ptrs + //#else + //int unused[8]; // remove as appropriate + //#endif + }; + + public struct StudioBodypart + { + public int sznameindex; + public int nummodels; + public int baseIndex; + public int modelindex; // index into models array + }; + + public class Bodypart + { + public StudioBodypart Header { get; set; } + public string Name { get; set; } + public StudioModel[] Models { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + br.BaseStream.Seek(offset + Header.sznameindex, SeekOrigin.Begin); + Name = br.ReadNullTerminatedString(); + Models = new StudioModel[Header.nummodels]; + for (int i = 0; i < Header.nummodels; i++) + { + Models[i] = new StudioModel(); + var modelOffset = offset + Header.modelindex + i * Marshal.SizeOf(); + Models[i].ReadObjects(handle, br, modelOffset); + } + } + + public class StudioModel + { + public Source.StudioModel Data { get; set; } + public string Name { get; set; } + public StudioMesh[] Meshes { get; set; } + public StudioEyeball[] Eyeballs { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + Name = new string(Data.name).TrimEnd('\0'); + Meshes = new StudioMesh[Data.nummeshes]; + for (int i = 0; i < Data.nummeshes; i++) + { + Meshes[i] = new StudioMesh(); + var meshOffset = offset + Data.meshindex + i * Marshal.SizeOf(); + Meshes[i].ReadObjects(handle, br, meshOffset); + } + Eyeballs = new StudioEyeball[Data.numeyeballs]; + for (int i = 0; i < Data.numeyeballs; i++) + { + Eyeballs[i] = new StudioEyeball(); + var eyeballOffset = offset + Data.eyeballindex + i * Marshal.SizeOf(); + Eyeballs[i].ReadObjects(handle, br, eyeballOffset); + } + } + public class StudioMesh + { + public Source.StudioMesh Data { get; set; } + public string MaterialName { get; set; } + public StudioFlex[] Flexes { get; set; } + public StudioMeshData VertexData { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + br.BaseStream.Seek(offset + Data.material, SeekOrigin.Begin); + MaterialName = br.ReadNullTerminatedString(); + Flexes = new StudioFlex[Data.numflexes]; + for (int i = 0; i < Data.numflexes; i++) + { + Flexes[i] = new StudioFlex(); + var flexOffset = offset + Data.flexindex + i * Marshal.SizeOf(); + Flexes[i].ReadObjects(handle, br, flexOffset); + } + VertexData = new StudioMeshData(); + var vertexDataOffset = offset + Marshal.SizeOf(); + VertexData.ReadObjects(handle, br, vertexDataOffset); + } + public class StudioFlex + { + public Source.StudioFlex Data { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + } + } + public class StudioMeshData + { + public Source.StudioMeshData Data { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + } + } + } + public class StudioEyeball + { + public Source.StudioEyeball Data { get; set; } + public string Name { get; set; } + internal void ReadObjects(GCHandle handle, BinaryReader br, int offset) + { + Data = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset); + br.BaseStream.Seek(offset + Data.sznameindex, SeekOrigin.Begin); + Name = br.ReadNullTerminatedString(); + } + } + } + } } From 610c655d8774a3276f59cb725d63fda4e5d26053 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Thu, 28 Aug 2025 17:51:17 +0300 Subject: [PATCH 16/22] Added fixup table --- Sledge.Formats.Model/Source/VvdFile.cs | 14 ++++++++++++++ Sledge.Formats.Model/Source/VvdStructs.cs | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Sledge.Formats.Model/Source/VvdFile.cs b/Sledge.Formats.Model/Source/VvdFile.cs index 32ace7e..a33ff94 100644 --- a/Sledge.Formats.Model/Source/VvdFile.cs +++ b/Sledge.Formats.Model/Source/VvdFile.cs @@ -10,6 +10,7 @@ public class VvdFile public VvdHeader Header { get; set; } public StudioVertex[] Vertices { get; set; } public Vector4[] TangentData { get; set; } + public VertexFixup[] Fixups { get; set; } public VvdFile(Stream stream) { var headerBuf = new byte[Marshal.SizeOf()]; @@ -43,6 +44,19 @@ public VvdFile(Stream stream) TangentData[i] = Marshal.PtrToStructure(tangHandle.AddrOfPinnedObject() + offset); } tangHandle.Free(); + + var fixupSize = Marshal.SizeOf(); + var fixupBuf = new byte[Header.numFixups * fixupSize]; + stream.Seek(Header.fixupTableStart, SeekOrigin.Begin); + stream.Read(fixupBuf, 0, fixupBuf.Length); + var fixupHandle = GCHandle.Alloc(fixupBuf, GCHandleType.Pinned); + Fixups = new VertexFixup[Header.numFixups]; + for (int i = 0; i < Header.numFixups; i++) + { + var offset = i * fixupSize; + Fixups[i] = Marshal.PtrToStructure(fixupHandle.AddrOfPinnedObject() + offset); + } + fixupHandle.Free(); } public static VvdFile FromFile(string path) diff --git a/Sledge.Formats.Model/Source/VvdStructs.cs b/Sledge.Formats.Model/Source/VvdStructs.cs index f7d6124..cfca938 100644 --- a/Sledge.Formats.Model/Source/VvdStructs.cs +++ b/Sledge.Formats.Model/Source/VvdStructs.cs @@ -28,7 +28,12 @@ public struct StudioVertex public Vector3 m_vecNormal; public Vector2 m_vecTexCoord; }; - + public struct VertexFixup + { + public int lod; // used to skip culled root lod + public int sourceVertexID; // absolute index from start of vertex/tangent blocks + public int numVertexes; + }; // 16 bytes [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct StudioBoneWeight From 5019a4ef438ddfcf5362c3bc3e8533fb176884be Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 29 Aug 2025 18:16:07 +0300 Subject: [PATCH 17/22] Added method to retrieve vertices from MdlFile --- Sledge.Formats.Model/Source/MdlFile.cs | 34 ++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs index 25ee32b..0a6b636 100644 --- a/Sledge.Formats.Model/Source/MdlFile.cs +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -1,5 +1,8 @@ using Sledge.Formats.FileSystem; +using Sledge.Formats.Model.Goldsource; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Runtime.InteropServices; namespace Sledge.Formats.Model.Source @@ -15,14 +18,41 @@ public class MdlFile public short[] SkinRef { get; set; } //?? public Bodypart[] Bodyparts { get; set; } - - public string[] Materials { get; set; } public string MaterialDirectory { get; set; } public VtxFile VtxFile { get; set; } public VvdFile VvdFile { get; set; } + public MeshVertex[] GetVertices() + { + if (VvdFile.Header.numFixups != 0) + { + var vertices = new List(); + foreach (var fixup in VvdFile.Fixups) + { + for (var vi = 0; vi < fixup.numVertexes; vi++) + { + var v = VvdFile.Vertices[vi + fixup.sourceVertexID]; + vertices.Add(new MeshVertex + { + Vertex = v.m_vecPosition, + Normal = v.m_vecNormal, + Texture = v.m_vecTexCoord, + VertexBone = v.m_BoneWeights.bone[0], + }); + } + } + return vertices.ToArray(); + } + return VvdFile.Vertices.Select(v => new MeshVertex + { + Vertex = v.m_vecPosition, + Normal = v.m_vecNormal, + Texture = v.m_vecTexCoord, + VertexBone = v.m_BoneWeights.bone[0], + }).ToArray(); + } public MdlFile(Stream stream) { From 824d2fd48c63b17b9286af404b251f4e6e039553 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 29 Aug 2025 18:51:31 +0300 Subject: [PATCH 18/22] Added method to retrieve indices and LOD count --- Sledge.Formats.Model/Source/MdlFile.cs | 7 +++++++ Sledge.Formats.Model/Source/VtxStructs.cs | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs index 0a6b636..37d2e52 100644 --- a/Sledge.Formats.Model/Source/MdlFile.cs +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -53,6 +53,13 @@ public MeshVertex[] GetVertices() VertexBone = v.m_BoneWeights.bone[0], }).ToArray(); } + public ushort[] GetIndices(int meshIndex = 0, int lodIndex = 0, int modelIndex = 0, int bodyPart = 0) + { + var mesh = VtxFile.BodyParts[bodyPart].Models[modelIndex].LOD[lodIndex].Meshes[meshIndex]; + var vertexOffset = Bodyparts[bodyPart].Models[modelIndex].Meshes[meshIndex].Data.vertexoffset; + return mesh.StripGroups.SelectMany(sg => sg.Strips.SelectMany(s => s.Indices.Select(x => (ushort)(s.Verts[x].origMeshVertID + vertexOffset)))).ToArray(); + } + public int GetLodCount() => VtxFile.Header.numLODs; public MdlFile(Stream stream) { diff --git a/Sledge.Formats.Model/Source/VtxStructs.cs b/Sledge.Formats.Model/Source/VtxStructs.cs index 06342c2..f057795 100644 --- a/Sledge.Formats.Model/Source/VtxStructs.cs +++ b/Sledge.Formats.Model/Source/VtxStructs.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; +using System.Runtime.InteropServices; namespace Sledge.Formats.Model.Source { From d17ee99a5e45384bc193d913c70556a7bd044cd2 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 29 Aug 2025 18:51:31 +0300 Subject: [PATCH 19/22] Added method to retrieve indices and LOD count --- Sledge.Formats.Model/Source/MdlFile.cs | 8 ++++++++ Sledge.Formats.Model/Source/VtxStructs.cs | 4 +--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs index 0a6b636..6b84420 100644 --- a/Sledge.Formats.Model/Source/MdlFile.cs +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -23,6 +23,8 @@ public class MdlFile public VtxFile VtxFile { get; set; } public VvdFile VvdFile { get; set; } + public int LodCount => VtxFile.Header.numLODs; + public MeshVertex[] GetVertices() { @@ -53,6 +55,12 @@ public MeshVertex[] GetVertices() VertexBone = v.m_BoneWeights.bone[0], }).ToArray(); } + public ushort[] GetIndices(int meshIndex = 0, int lodIndex = 0, int modelIndex = 0, int bodyPart = 0) + { + var mesh = VtxFile.BodyParts[bodyPart].Models[modelIndex].LOD[lodIndex].Meshes[meshIndex]; + var vertexOffset = Bodyparts[bodyPart].Models[modelIndex].Meshes[meshIndex].Data.vertexoffset; + return mesh.StripGroups.SelectMany(sg => sg.Strips.SelectMany(s => s.Indices.Select(x => (ushort)(s.Verts[x].origMeshVertID + vertexOffset)))).ToArray(); + } public MdlFile(Stream stream) { diff --git a/Sledge.Formats.Model/Source/VtxStructs.cs b/Sledge.Formats.Model/Source/VtxStructs.cs index 06342c2..f057795 100644 --- a/Sledge.Formats.Model/Source/VtxStructs.cs +++ b/Sledge.Formats.Model/Source/VtxStructs.cs @@ -1,6 +1,4 @@ -using System.IO; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.ComTypes; +using System.Runtime.InteropServices; namespace Sledge.Formats.Model.Source { From fb17eb0faff5a96b013a723e59eb1bae5101d8ba Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 29 Aug 2025 19:22:31 +0300 Subject: [PATCH 20/22] Added bp properties // add methods --- Sledge.Formats.Model/Source/MdlFile.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs index 6b84420..fae85c2 100644 --- a/Sledge.Formats.Model/Source/MdlFile.cs +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -23,7 +23,11 @@ public class MdlFile public VtxFile VtxFile { get; set; } public VvdFile VvdFile { get; set; } + + public int BodypartCount => Header.bodypart_count; public int LodCount => VtxFile.Header.numLODs; + public int GetModelCount(int bodypart) => Bodyparts[bodypart].Header.nummodels; + public int GetMeshCount(int bodypart, int model) => Bodyparts[bodypart].Models[model].Data.nummeshes; public MeshVertex[] GetVertices() From 78abbbb3911e422e9da43e33ed8735e11c406f31 Mon Sep 17 00:00:00 2001 From: Duude92 Date: Fri, 29 Aug 2025 19:27:37 +0300 Subject: [PATCH 21/22] Add method to retrieve material index per mesh --- Sledge.Formats.Model/Source/MdlFile.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs index fae85c2..b329fde 100644 --- a/Sledge.Formats.Model/Source/MdlFile.cs +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -65,6 +65,7 @@ public ushort[] GetIndices(int meshIndex = 0, int lodIndex = 0, int modelIndex = var vertexOffset = Bodyparts[bodyPart].Models[modelIndex].Meshes[meshIndex].Data.vertexoffset; return mesh.StripGroups.SelectMany(sg => sg.Strips.SelectMany(s => s.Indices.Select(x => (ushort)(s.Verts[x].origMeshVertID + vertexOffset)))).ToArray(); } + public int GetMaterialIndex(int meshIndex = 0, int modelIndex = 0, int bodyPart = 0) => Bodyparts[bodyPart].Models[modelIndex].Meshes[meshIndex].Data.material; public MdlFile(Stream stream) { From bc5e3e4579cf9de2a184fd7631c1799599f0fc3e Mon Sep 17 00:00:00 2001 From: Duude92 Date: Sat, 30 Aug 2025 15:44:28 +0300 Subject: [PATCH 22/22] Refactor gchandle --- Sledge.Formats.Model/Source/VvdFile.cs | 30 +++++++------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/Sledge.Formats.Model/Source/VvdFile.cs b/Sledge.Formats.Model/Source/VvdFile.cs index a33ff94..50349f2 100644 --- a/Sledge.Formats.Model/Source/VvdFile.cs +++ b/Sledge.Formats.Model/Source/VvdFile.cs @@ -13,50 +13,36 @@ public class VvdFile public VertexFixup[] Fixups { get; set; } public VvdFile(Stream stream) { - var headerBuf = new byte[Marshal.SizeOf()]; + var buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); var vertexSize = Marshal.SizeOf(); - stream.Read(headerBuf, 0, headerBuf.Length); - var handle = GCHandle.Alloc(headerBuf, GCHandleType.Pinned); Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); - handle.Free(); var vertexCount = Header.numLODVertexes[0]; - var vertexBuf = new byte[vertexCount * vertexSize]; - stream.Seek(Header.vertexDataStart, SeekOrigin.Begin); - stream.Read(vertexBuf, 0, vertexBuf.Length); - var vertexHandle = GCHandle.Alloc(vertexBuf, GCHandleType.Pinned); Vertices = new StudioVertex[vertexCount]; for(int i = 0; i < vertexCount; i++) { var offset = i * vertexSize; - Vertices[i] = Marshal.PtrToStructure(vertexHandle.AddrOfPinnedObject() + offset); + Vertices[i] = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset + Header.vertexDataStart); } - vertexHandle.Free(); - - var tangentBuf = new byte[Marshal.SizeOf()]; var tangentSize = Marshal.SizeOf(); - stream.Read(tangentBuf, 0, tangentBuf.Length); - var tangHandle = GCHandle.Alloc(tangentBuf, GCHandleType.Pinned); + var tangentOffset = Header.vertexDataStart + vertexCount * vertexSize; TangentData = new Vector4[vertexCount]; for (int i = 0; i < vertexCount; i++) { var offset = i * tangentSize; - TangentData[i] = Marshal.PtrToStructure(tangHandle.AddrOfPinnedObject() + offset); + TangentData[i] = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset + tangentOffset); } - tangHandle.Free(); var fixupSize = Marshal.SizeOf(); - var fixupBuf = new byte[Header.numFixups * fixupSize]; - stream.Seek(Header.fixupTableStart, SeekOrigin.Begin); - stream.Read(fixupBuf, 0, fixupBuf.Length); - var fixupHandle = GCHandle.Alloc(fixupBuf, GCHandleType.Pinned); Fixups = new VertexFixup[Header.numFixups]; for (int i = 0; i < Header.numFixups; i++) { var offset = i * fixupSize; - Fixups[i] = Marshal.PtrToStructure(fixupHandle.AddrOfPinnedObject() + offset); + Fixups[i] = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset + Header.fixupTableStart); } - fixupHandle.Free(); + handle.Free(); } public static VvdFile FromFile(string path)