/******************** * MD2 Model Animator R2.0 * * Author(s): Matthew Lynch * * Copyright: * * This class is released under the * Creative Commons Attribution-Share Alike 3.0 License. * * For more information please see: * http://creativecommons.org/licenses/by-sa/3.0/ * ********************/ #region Standard Using Statements using System; using System.IO; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace XnaMD2 { public class MD2Model { private const int m_MD2ValidID = ('I' + ('D' << 8) + ('P' << 16) + ('2' << 24)); #region Stored Model Structures /*** * These structures store the 'raw' MD2 model data, prior to the * conversion needed to render the model in an XNA space. ***/ private struct MD2FileHeader { // File identification information. public int ID; public int Version; // Texture dimensions. public int SkinWidth; public int SkinHeight; // How many bytes in each frame. public int FrameSize; // How many of each type of object. public int CountVertices; // How many vertices per frame. (Other counts are total for model.) public int CountTriangles; public int CountFrames; public int CountSkins; public int CountTextureMaps; public int CountOGLCommands; // Offsets to where data starts in the model file. public int OffsetTriangles; public int OffsetFrames; public int OffsetSkins; public int OffsetTextureMaps; public int OffsetOGLCommands; public int OffsetEOF; } private struct MD2Skin { public char[] TexturePath; // Path to texture image. Not used here yet, see release notes. } private struct MD2TextureCoordinate { public short X; // Compressed X ('U') texture coordinate. public short Y; // Compressed Y ('V') texture coordinate. } private struct MD2Triangle { public ushort[] Vertex; // Index into the model's vertex arrays. public ushort[] TextureCoordinate; // Index into the model's texture coordinates array. } private struct MD2Vertex { public byte[] Coordinate; // Compressed vector coordinates. public byte LightingNormal; // Index into Quake II Lighting Normals table. Not used here yet, see release notes. } private struct MD2Frame { public char[] Name; // Name of frame, useful for identifying animation sets. public float[] Scale; // Scale data for decompression of vertices data. public float[] Translation; // Translation data for decompression of vertices data. public MD2Vertex[] Vertex; // Compressed vertex data for frame. public Vector3[] DecompressedVertices; } #endregion private MD2FileHeader m_MD2FileHeader; private MD2Skin[] m_MD2Skins; private MD2TextureCoordinate[] m_MD2TextureMaps; private MD2Triangle[] m_MD2Triangles; private MD2Frame[] m_MD2Frames; private struct TranslatedFrame { public string Name; public VertexPositionTexture[] VertexBuffer; } private struct MD2Animation { public string Name; public int StartFrame; public int FinishFrame; } private TranslatedFrame[] m_Frames; private Dictionary m_Animations; private Vector3 m_Position; private float m_Scale; private float m_Rotation; private float m_RotationOffset; private Matrix m_WorldMatrix; private Matrix m_ScaleMatrix; private Matrix m_RotationMatrix; private bool m_FirstBufferCall = true; private string m_CurrentAnimation; private int m_CurrentFrame; private double m_AnimationClock; private int m_FrameRate; public int TriangleCount { get { return m_MD2FileHeader.CountTriangles; } } public string CurrentAnimation { get { return m_CurrentAnimation; } set { if (m_Animations.ContainsKey(value)) { m_CurrentAnimation = value; } else { throw new KeyNotFoundException("No animation of the name '" + value + "' was found in the model."); } } } public int FrameRate { get { return m_FrameRate; } set { m_FrameRate = value; } } public int CurrentFrame { get { return m_CurrentFrame; } set { m_CurrentFrame = value; } } public Matrix WorldMatrix { get { return m_WorldMatrix; } } public Vector3 Position { get { return m_Position; } set { m_Position = value; m_WorldMatrix = m_ScaleMatrix * m_RotationMatrix * Matrix.CreateTranslation(m_Position); } } public float Scale { get { return this.m_Scale; } set { this.m_Scale = value; this.m_ScaleMatrix = Matrix.CreateScale(Scale / 100f); m_WorldMatrix = m_ScaleMatrix * m_RotationMatrix * Matrix.CreateTranslation(m_Position); } } public float RotationOffset { get { return this.m_RotationOffset; } set { m_RotationOffset = value; Rotation = 0; } } public float Rotation { get { return this.m_Rotation; } set { m_Rotation = value; if (m_Rotation < 0) { m_Rotation = 360; } if (m_Rotation > 360) { m_Rotation = 0; } m_RotationMatrix = Matrix.CreateFromAxisAngle(Vector3.Up, MathHelper.ToRadians(m_Rotation)) * Matrix.CreateFromAxisAngle(Vector3.Up, MathHelper.ToRadians(-m_RotationOffset)); m_WorldMatrix = m_ScaleMatrix * m_RotationMatrix * Matrix.CreateTranslation(m_Position); } } public MD2Model() { m_Animations = new Dictionary(); m_AnimationClock = 0; m_FrameRate = 12; Scale = 100; RotationOffset = 0; Rotation = 0; } public MD2Model(MD2Model model) { m_MD2FileHeader = model.m_MD2FileHeader; m_MD2Skins = model.m_MD2Skins; m_MD2TextureMaps = model.m_MD2TextureMaps; m_MD2Triangles = model.m_MD2Triangles; m_MD2Frames = model.m_MD2Frames; m_Frames = model.m_Frames; m_Animations = model.m_Animations; m_AnimationClock = 0; m_FrameRate = model.m_FrameRate; m_Position = model.m_Position; m_Scale = model.m_Scale; RotationOffset = model.m_RotationOffset; Rotation = model.m_Rotation; m_WorldMatrix = model.m_WorldMatrix; m_ScaleMatrix = model.m_ScaleMatrix; m_RotationMatrix = model.m_RotationMatrix; } public bool LoadFromFile(string path) { m_Animations.Clear(); m_FirstBufferCall = true; System.IO.FileStream stream = null; System.IO.BinaryReader reader = null; try { // Open the specified file as a stream. stream = new System.IO.FileStream(path, FileMode.Open, FileAccess.Read); // Pipe the stream in to a binary reader so we can work at the byte-by-byte level. reader = new System.IO.BinaryReader(stream); /*** LOAD FILE HEADER ***/ this.m_MD2FileHeader = new MD2FileHeader(); // If the file is a valid MD2 model the first 4B of data will be an ID. // The ID is a standard integer which should match _MD2VALIDID. this.m_MD2FileHeader.ID = reader.ReadInt32(); if (this.m_MD2FileHeader.ID == m_MD2ValidID) { /*** LOAD HEADER DATA ***/ // After the magic number the next 64B of data contains header information. // For more information see the Struct MD2Header definition. this.m_MD2FileHeader.Version = reader.ReadInt32(); this.m_MD2FileHeader.SkinWidth = reader.ReadInt32(); this.m_MD2FileHeader.SkinHeight = reader.ReadInt32(); this.m_MD2FileHeader.FrameSize = reader.ReadInt32(); this.m_MD2FileHeader.CountSkins = reader.ReadInt32(); this.m_MD2FileHeader.CountVertices = reader.ReadInt32(); this.m_MD2FileHeader.CountTextureMaps = reader.ReadInt32(); this.m_MD2FileHeader.CountTriangles = reader.ReadInt32(); this.m_MD2FileHeader.CountOGLCommands = reader.ReadInt32(); this.m_MD2FileHeader.CountFrames = reader.ReadInt32(); this.m_MD2FileHeader.OffsetSkins = reader.ReadInt32(); this.m_MD2FileHeader.OffsetTextureMaps = reader.ReadInt32(); this.m_MD2FileHeader.OffsetTriangles = reader.ReadInt32(); this.m_MD2FileHeader.OffsetFrames = reader.ReadInt32(); this.m_MD2FileHeader.OffsetOGLCommands = reader.ReadInt32(); this.m_MD2FileHeader.OffsetEOF = reader.ReadInt32(); /*** LOAD SKIN DEFINITIONS ***/ // Initialise data array. this.m_MD2Skins = new MD2Skin[this.m_MD2FileHeader.CountSkins]; // Jump to the position in file where data starts. // This is defined in the header data. reader.BaseStream.Seek(this.m_MD2FileHeader.OffsetSkins, SeekOrigin.Begin); // Loop for each entry. for (int i = 0; i < this.m_MD2FileHeader.CountSkins; i++) { this.m_MD2Skins[i].TexturePath = reader.ReadChars(64); } /*** LOAD TEXTURE MAPS ***/ this.m_MD2TextureMaps = new MD2TextureCoordinate[this.m_MD2FileHeader.CountTextureMaps]; reader.BaseStream.Seek(this.m_MD2FileHeader.OffsetTextureMaps, SeekOrigin.Begin); for (int i = 0; i < this.m_MD2FileHeader.CountTextureMaps; i++) { this.m_MD2TextureMaps[i].X = reader.ReadInt16(); this.m_MD2TextureMaps[i].Y = reader.ReadInt16(); } /*** LOAD TRIANGLE DATA ***/ this.m_MD2Triangles = new MD2Triangle[this.m_MD2FileHeader.CountTriangles]; reader.BaseStream.Seek(this.m_MD2FileHeader.OffsetTriangles, SeekOrigin.Begin); for (int i = 0; i < this.m_MD2FileHeader.CountTriangles; i++) { this.m_MD2Triangles[i].Vertex = new ushort[3]; this.m_MD2Triangles[i].Vertex[0] = reader.ReadUInt16(); this.m_MD2Triangles[i].Vertex[1] = reader.ReadUInt16(); this.m_MD2Triangles[i].Vertex[2] = reader.ReadUInt16(); this.m_MD2Triangles[i].TextureCoordinate = new ushort[3]; this.m_MD2Triangles[i].TextureCoordinate[0] = reader.ReadUInt16(); this.m_MD2Triangles[i].TextureCoordinate[1] = reader.ReadUInt16(); this.m_MD2Triangles[i].TextureCoordinate[2] = reader.ReadUInt16(); } /*** LOAD FRAMES ***/ this.m_MD2Frames = new MD2Frame[this.m_MD2FileHeader.CountFrames]; for (int i = 0; i < this.m_MD2FileHeader.CountFrames; i++) { // Unlike other data, frame data is of a defined size not fixed. // As such the jump to the start of the data must happen for each frame. reader.BaseStream.Seek((this.m_MD2FileHeader.OffsetFrames + (i * this.m_MD2FileHeader.FrameSize)), SeekOrigin.Begin); this.m_MD2Frames[i].Scale = new float[3]; this.m_MD2Frames[i].Translation = new float[3]; // The binary reader does not read floats; 'scale' and 'translate' are foat[].. // We need to read the data to a byte[] and convert it. byte[] buffer; buffer = reader.ReadBytes(4); this.m_MD2Frames[i].Scale[0] = System.BitConverter.ToSingle(buffer, 0); buffer = reader.ReadBytes(4); this.m_MD2Frames[i].Scale[1] = System.BitConverter.ToSingle(buffer, 0); buffer = reader.ReadBytes(4); this.m_MD2Frames[i].Scale[2] = System.BitConverter.ToSingle(buffer, 0); buffer = reader.ReadBytes(4); this.m_MD2Frames[i].Translation[0] = System.BitConverter.ToSingle(buffer, 0); buffer = reader.ReadBytes(4); this.m_MD2Frames[i].Translation[1] = System.BitConverter.ToSingle(buffer, 0); buffer = reader.ReadBytes(4); this.m_MD2Frames[i].Translation[2] = System.BitConverter.ToSingle(buffer, 0); this.m_MD2Frames[i].Name = reader.ReadChars(16); /*** LOAD VERTEX DATA (FOR FRAME) ***/ this.m_MD2Frames[i].Vertex = new MD2Vertex[this.m_MD2FileHeader.CountVertices]; this.m_MD2Frames[i].DecompressedVertices = new Vector3[this.m_MD2FileHeader.CountVertices]; for (int j = 0; j < this.m_MD2FileHeader.CountVertices; j++) { this.m_MD2Frames[i].Vertex[j].Coordinate = reader.ReadBytes(3); this.m_MD2Frames[i].Vertex[j].LightingNormal = reader.ReadByte(); } } /*** DECOMPRESS AND TRANSLATE FRAMES ***/ this.m_Frames = new TranslatedFrame[this.m_MD2FileHeader.CountFrames]; for (int i = 0; i < this.m_MD2FileHeader.CountFrames; i++) { this.m_Frames[i].Name = new string(this.m_MD2Frames[i].Name); this.m_Frames[i].VertexBuffer = new VertexPositionTexture[this.m_MD2FileHeader.CountTriangles * 3]; int j = 0; // position in vertex buffer for (int k = 0; k < this.m_MD2FileHeader.CountTriangles; k++) { // We need to invert the vertices of the triangle, // switch the Y and Z coordinates of the vertices, // decompress the vertices coordinates (coord * scale + translation) and // convert the texture coordinates (x / width, y / height). this.m_Frames[i].VertexBuffer[j].Position.X = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[2]].Coordinate[0] * this.m_MD2Frames[i].Scale[0] + this.m_MD2Frames[i].Translation[0]; this.m_Frames[i].VertexBuffer[j].Position.Y = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[2]].Coordinate[2] * this.m_MD2Frames[i].Scale[2] + this.m_MD2Frames[i].Translation[2]; this.m_Frames[i].VertexBuffer[j].Position.Z = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[2]].Coordinate[1] * this.m_MD2Frames[i].Scale[1] + this.m_MD2Frames[i].Translation[1]; this.m_Frames[i].VertexBuffer[j].TextureCoordinate.X = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[2]].X / (float)this.m_MD2FileHeader.SkinWidth; this.m_Frames[i].VertexBuffer[j].TextureCoordinate.Y = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[2]].Y / (float)this.m_MD2FileHeader.SkinHeight; j += 1; this.m_Frames[i].VertexBuffer[j].Position.X = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[1]].Coordinate[0] * this.m_MD2Frames[i].Scale[0] + this.m_MD2Frames[i].Translation[0]; this.m_Frames[i].VertexBuffer[j].Position.Y = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[1]].Coordinate[2] * this.m_MD2Frames[i].Scale[2] + this.m_MD2Frames[i].Translation[2]; this.m_Frames[i].VertexBuffer[j].Position.Z = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[1]].Coordinate[1] * this.m_MD2Frames[i].Scale[1] + this.m_MD2Frames[i].Translation[1]; this.m_Frames[i].VertexBuffer[j].TextureCoordinate.X = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[1]].X / (float)this.m_MD2FileHeader.SkinWidth; this.m_Frames[i].VertexBuffer[j].TextureCoordinate.Y = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[1]].Y / (float)this.m_MD2FileHeader.SkinHeight; j += 1; this.m_Frames[i].VertexBuffer[j].Position.X = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[0]].Coordinate[0] * this.m_MD2Frames[i].Scale[0] + this.m_MD2Frames[i].Translation[0]; this.m_Frames[i].VertexBuffer[j].Position.Y = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[0]].Coordinate[2] * this.m_MD2Frames[i].Scale[2] + this.m_MD2Frames[i].Translation[2]; this.m_Frames[i].VertexBuffer[j].Position.Z = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[0]].Coordinate[1] * this.m_MD2Frames[i].Scale[1] + this.m_MD2Frames[i].Translation[1]; this.m_Frames[i].VertexBuffer[j].TextureCoordinate.X = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[0]].X / (float)this.m_MD2FileHeader.SkinWidth; this.m_Frames[i].VertexBuffer[j].TextureCoordinate.Y = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[0]].Y / (float)this.m_MD2FileHeader.SkinHeight; j += 1; } } string animationName = ""; string frameName = ""; int currentIndex = 1; int start = 1; int finish = 1; for (int i = 0; i < m_Frames.Length; i++) { frameName = m_Frames[i].Name; if (frameName.IndexOf("\0") > 0) { frameName = frameName.Substring(0, frameName.IndexOf("\0")); } if (frameName.EndsWith("0") && !currentIndex.ToString().EndsWith("0")) { frameName = frameName.Substring(0, frameName.Length - 1); } frameName = frameName.Substring(0, frameName.Length - currentIndex.ToString().Length); if (frameName.EndsWith("0")) { frameName = frameName.Substring(0, frameName.Length - 1); } if (frameName != animationName) { if (animationName != "" && frameName != "") { finish = i; MD2Animation animation = new MD2Animation(); animation.Name = animationName; animation.StartFrame = start; animation.FinishFrame = finish; m_Animations.Add(animationName, animation); } currentIndex = 1; frameName = m_Frames[i].Name; if (frameName.IndexOf("\0") > 0) { frameName = frameName.Substring(0, frameName.IndexOf("\0")); } if (frameName.EndsWith("0") && !currentIndex.ToString().EndsWith("0")) { frameName = frameName.Substring(0, frameName.Length - 1); } frameName = frameName.Substring(0, frameName.Length - currentIndex.ToString().Length); if (frameName.EndsWith("0")) { frameName = frameName.Substring(0, frameName.Length - 1); } animationName = frameName; start = i + 1; finish = i + 1; currentIndex += 1; } else { currentIndex += 1; } if (i == m_Frames.Length - 1 && animationName != "" && frameName != "") { finish = i + 1; MD2Animation animation = new MD2Animation(); animation.Name = animationName; animation.StartFrame = start; animation.FinishFrame = finish; m_Animations.Add(animationName, animation); } } return true; } } catch (Exception ex) { throw new Exception("Error while loading MD2 model from definition file ( " + path + " ).", ex); } finally { if (reader != null) { reader.Close(); } if (stream != null) { stream.Close(); stream.Dispose(); } } return false; } public VertexPositionTexture[] GetVertexBuffer(int frame) { return this.m_Frames[frame].VertexBuffer; } public VertexPositionTexture[] GetVertexBuffer(GameTime gameTime) { MD2Animation currentAnimation = m_Animations[m_CurrentAnimation]; if (m_FirstBufferCall) { m_CurrentFrame = currentAnimation.StartFrame; m_AnimationClock = 0; m_FirstBufferCall = false; } int timePerFrame = 1000 / m_FrameRate; m_AnimationClock += gameTime.ElapsedGameTime.TotalMilliseconds; if (m_AnimationClock >= timePerFrame) { m_AnimationClock = 0; m_CurrentFrame++; m_CurrentFrame = (m_CurrentFrame >= currentAnimation.FinishFrame) ? currentAnimation.StartFrame : m_CurrentFrame; } return this.m_Frames[m_CurrentFrame].VertexBuffer; } } }