diff --git a/Sledge.Formats.Model/Source/MdlFile.cs b/Sledge.Formats.Model/Source/MdlFile.cs new file mode 100644 index 0000000..b329fde --- /dev/null +++ b/Sledge.Formats.Model/Source/MdlFile.cs @@ -0,0 +1,177 @@ +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 +{ + 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 StudioSequenceDescription[] Sequences { get; set; } + 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 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() + { + 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 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 GetMaterialIndex(int meshIndex = 0, int modelIndex = 0, int bodyPart = 0) => Bodyparts[bodyPart].Models[modelIndex].Meshes[meshIndex].Data.material; + + public MdlFile(Stream stream) + { + 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); + } + 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); + } + + 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]; + 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++) + { + Materials[i] = br.ReadNullTerminatedString(); + } + stream.Seek(Header.texturedir_offset, SeekOrigin.Begin); + var dirOffset = br.ReadInt32(); + stream.Seek(dirOffset, SeekOrigin.Begin); + MaterialDirectory = br.ReadNullTerminatedString(); + handle.Free(); + } + + } + + 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 + { + } + } + } +} diff --git a/Sledge.Formats.Model/Source/MdlStructs.cs b/Sledge.Formats.Model/Source/MdlStructs.cs new file mode 100644 index 0000000..d8c4dbf --- /dev/null +++ b/Sledge.Formats.Model/Source/MdlStructs.cs @@ -0,0 +1,800 @@ +using System.IO; +using System.Numerics; +using System.Runtime.InteropServices; + +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 + { + 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 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 + + /* + * 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 + */ + }; + 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; } + 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 Quaternion 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 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 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; + [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 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 + { + 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; + } + + 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(); + } + } + } + } +} 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 + { + } + } + } +} diff --git a/Sledge.Formats.Model/Source/VtxStructs.cs b/Sledge.Formats.Model/Source/VtxStructs.cs new file mode 100644 index 0000000..f057795 --- /dev/null +++ b/Sledge.Formats.Model/Source/VtxStructs.cs @@ -0,0 +1,244 @@ +using System.Runtime.InteropServices; + +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 + { + // 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; + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ModelHeader + { + //LOD mesh array + public int numLODs; //This is also specified in FileHeader_t + public int lodOffset; + }; + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct ModelLODHeader + { + //Mesh array + public int numMeshes; + public int meshOffset; + public float switchPoint; + }; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct MeshHeader + { + public int numStripGroups; + public int stripGroupHeaderOffset; + public byte flags; + }; + [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 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 + { + public int numIndices; + public int indexOffset; + public int numVerts; + public int vertOffset; + public short numBones; + public StripFlags flags; + public int numBoneStateChanges; + public int boneStateChangeOffset; + + // MDL Version 49 and up only + //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 + { + // 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; + }; +} diff --git a/Sledge.Formats.Model/Source/VvdFile.cs b/Sledge.Formats.Model/Source/VvdFile.cs new file mode 100644 index 0000000..50349f2 --- /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 VertexFixup[] Fixups { get; set; } + public VvdFile(Stream stream) + { + var buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + var vertexSize = Marshal.SizeOf(); + Header = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + + var vertexCount = Header.numLODVertexes[0]; + Vertices = new StudioVertex[vertexCount]; + for(int i = 0; i < vertexCount; i++) + { + var offset = i * vertexSize; + Vertices[i] = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset + Header.vertexDataStart); + } + var tangentSize = Marshal.SizeOf(); + 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(handle.AddrOfPinnedObject() + offset + tangentOffset); + } + + var fixupSize = Marshal.SizeOf(); + Fixups = new VertexFixup[Header.numFixups]; + for (int i = 0; i < Header.numFixups; i++) + { + var offset = i * fixupSize; + Fixups[i] = Marshal.PtrToStructure(handle.AddrOfPinnedObject() + offset + Header.fixupTableStart); + } + handle.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 + { + } + } + + } +} diff --git a/Sledge.Formats.Model/Source/VvdStructs.cs b/Sledge.Formats.Model/Source/VvdStructs.cs new file mode 100644 index 0000000..cfca938 --- /dev/null +++ b/Sledge.Formats.Model/Source/VvdStructs.cs @@ -0,0 +1,50 @@ +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; + }; + 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 + { + 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; + + }; +}