xna

XNA MD2Model with Normal calculation

Ive been playing around with the excellent MD2Model class RCade provides on his blog(http://www.varcade.com/blog/).

I did some experiments (nothing too fancy) but I wanted to use dynamic lighting on some old doom 1 models I found on the internet. Unfortunately  I was not able to do so because I did not have access to the Normals on the mesh.

I did a little Google search (my math skills are a little rusty) and I refresh my mind on how normals are calculated so I patched the code to support them.

This is the code that calculates the normal for each frame:

                        for (int p = 0; p < this.m_MD2FileHeader.CountTriangles; p++)
                        {
                            Vector3 first = Vector3.Subtract(this.m_Frames[i].VertexBuffer[p * 3 + 1].Position, this.m_Frames[i].VertexBuffer[p * 3].Position);
                            Vector3 second = Vector3.Subtract(this.m_Frames[i].VertexBuffer[p * 3 + 2].Position, this.m_Frames[i].VertexBuffer[p * 3].Position);

                            Vector3 normal = Vector3.Cross(first, second);
                            normal.Normalize();

                            this.m_Frames[i].VertexBuffer[p * 3].Normal += normal;
                            this.m_Frames[i].VertexBuffer[p * 3 + 1].Normal += normal;
                            this.m_Frames[i].VertexBuffer[p * 3 + 2].Normal += normal;
                        }


                        for (int p = 0; p < this.m_MD2FileHeader.CountTriangles; p++)
                        {
                            this.m_Frames[i].VertexBuffer[p].Normal.Normalize();
                        }

 

This is the complete MD2Model class

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 VertexPositionNormalTexture[] VertexBuffer;
    }

    private struct MD2Animation
    {
        public string Name;
        public int StartFrame;
        public int FinishFrame;
    }

    private TranslatedFrame[] m_Frames;
    private Dictionary<string, MD2Animation> 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;
                m_CurrentFrame = m_Animations.FirstOrDefault(a => a.Key == value).Value.StartFrame;
            }
            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<string, MD2Animation>();
        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 double GetMD2AnimationDuration(string key)
    {
        return ((m_Animations[key].FinishFrame - m_Animations[key].StartFrame) * (1000d / m_FrameRate)) / 1000;
    }


    public bool LoadFromFile(string path)
    {
        m_Animations.Clear();
        m_FirstBufferCall = true;

        System.IO.FileStream stream = null;
        System.IO.BinaryReader reader = null;

        try
        {
            Stream st = TitleContainer.OpenStream(path);


            // 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(TitleContainer.OpenStream(path));

            /*** 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 VertexPositionNormalTexture[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;
                        this.m_Frames[i].VertexBuffer[j].Normal = new Vector3(0, 0, 0);

                        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;
                        this.m_Frames[i].VertexBuffer[j].Normal = new Vector3(0, 0, 0);

                        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;
                        this.m_Frames[i].VertexBuffer[j].Normal = new Vector3(0, 0, 0);

                        j += 1;
                    }


                    for (int p = 0; p < this.m_MD2FileHeader.CountTriangles; p++)
                    {
                        Vector3 first = Vector3.Subtract(this.m_Frames[i].VertexBuffer[p * 3 + 1].Position, this.m_Frames[i].VertexBuffer[p * 3].Position);
                        Vector3 second = Vector3.Subtract(this.m_Frames[i].VertexBuffer[p * 3 + 2].Position, this.m_Frames[i].VertexBuffer[p * 3].Position);

                        Vector3 normal = Vector3.Cross(first, second);
                        normal.Normalize();

                        this.m_Frames[i].VertexBuffer[p * 3].Normal += normal;
                        this.m_Frames[i].VertexBuffer[p * 3 + 1].Normal += normal;
                        this.m_Frames[i].VertexBuffer[p * 3 + 2].Normal += normal;
                    }


                    for (int p = 0; p < this.m_MD2FileHeader.CountTriangles; p++)
                    {
                        this.m_Frames[i].VertexBuffer[p].Normal.Normalize();
                    }

                }



                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) { frameName = frameName.Substring(0, frameName.IndexOf("")); }

                    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) { frameName = frameName.Substring(0, frameName.IndexOf("")); }

                        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 VertexPositionNormalTexture[] GetVertexBuffer(int frame)
    {
        return this.m_Frames[frame].VertexBuffer;
    }

    public VertexPositionNormalTexture[] GetVertexBuffer(TimeSpan elapsedGameTime)
    {
        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 += 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;
    }
}

 

I hope you find this useful and feel free to send me corrections

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s