Skip to content

Instantly share code, notes, and snippets.

@Robmaister
Last active October 6, 2015 04:47
Show Gist options
  • Save Robmaister/2938490 to your computer and use it in GitHub Desktop.
Save Robmaister/2938490 to your computer and use it in GitHub Desktop.
C# IQM parser
/*Copyright (c) 2012-2013 Robert Rouhani <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using OpenTK;
using OpenTK.Graphics.OpenGL;
using TopHat.Graphics;
namespace TopHat.IO
{
public static class IqmParser
{
private enum IqmAttributeType
{
Position = 0,
TexCoord = 1,
Normal = 2,
Tangent = 3,
BlendIndexes = 4,
BlendWeights = 5,
Color = 6,
Custom = 0x10
}
private enum IqmDataFormat
{
Byte = 0,
UnsignedByte = 1,
Short = 2,
UnsignedShort = 3,
Int = 4,
UnsignedInt = 5,
Half = 6,
Float = 7,
Double = 8
}
private enum IqmAnimationType
{
Loop = 1 << 0
}
/// <summary>
/// The header at the top of every IQM file.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Ansi)]
private struct IqmHeader
{
/// <summary>
/// The identifier for an IQM file - "INTERQUAKEMODEL\0".
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
public string Magic;
/// <summary>
/// The version of the file. Must be at version 2 to work.
/// </summary>
public uint Version;
/// <summary>
/// Size of the file.
/// </summary>
public uint FileSize;
/// <summary>
/// File flags. There's nothing in the specification about the meaning of this field.
/// </summary>
public uint Flags;
public uint TextCount;
public uint TextOffset;
public uint MeshesCount;
public uint MeshesOffset;
public uint VertexArraysCount;
public uint VertexesCount;
public uint VertexArraysOffset;
public uint TrianglesCount;
public uint TrianglesOffset;
public uint AdjacencyOffset;
public uint JointsCount;
public uint JointsOffset;
public uint PosesCount;
public uint PosesOffset;
public uint AnimationsCount;
public uint AnimationsOffset;
public uint FramesCount;
public uint FrameChannelsCount;
public uint FramesOffset;
public uint BoundsOffset;
public uint CommentCount;
public uint CommentOffset;
public uint ExtensionsCount;
public uint ExtensionsOffset;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmHeader)); } }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct IqmMesh
{
public uint Name;
public uint Material;
public uint FirstVertex;
public uint VertexesCount;
public uint FirstTriangle;
public uint TrianglesCount;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmMesh)); } }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct IqmTriangle
{
public uint Vertex0;
public uint Vertex1;
public uint Vertex2;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmTriangle)); } }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct IqmAdjacency
{
public uint Triangle0;
public uint Triangle1;
public uint Triangle2;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmAdjacency)); } }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct IqmJoint
{
public uint Name;
public int Parent;
public Vector3 Translate;
public Quaternion Rotate;
public Vector3 Scale;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmJoint)); } }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct IqmPose
{
public int Parent;
public uint Mask;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] ChannelOffset;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
public float[] ChannelScale;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmPose)); } }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct IqmAnimation
{
public uint Name;
public uint FirstFrame;
public uint FramesCount;
public float Framerate;
public uint Flags;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmAnimation)); } }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct IqmVertexArray
{
public IqmAttributeType Type;
public uint Flags;
public IqmDataFormat Format;
public uint Size;
public uint Offset;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmVertexArray)); } }
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct IqmBounds
{
public Vector3 BBMin;
public Vector3 BBMax;
public float XYRadius;
public float Radius;
public static int SizeInBytes { get { return Marshal.SizeOf(typeof(IqmBounds)); } }
}
public static Drawable Parse(string path)
{
if (!File.Exists(path))
throw new ArgumentException("The file \"" + path + "\" does not exist.", "path");
List<string> text;
List<IqmMesh> meshes;
List<IqmVertexArray> vertexArrays;
List<IqmTriangle> triangles;
List<IqmAdjacency> adjacency;
List<IqmJoint> joints;
List<IqmPose> poses;
List<IqmAnimation> animations;
//TODO matrix3x4 for frames.
IqmBounds bounds;
List<VertexAttribute> attributes = new List<VertexAttribute>();
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
using (BinaryReader reader = new BinaryReader(fs))
{
//make sure the file is long enough to parse a header before we check the other requirements.
if (fs.Length < IqmHeader.SizeInBytes)
throw new ArgumentException("The file \"" + path + "\" is not an IQM file.", "path");
//parse the header and check for magic number match, file size match, and version match.
IqmHeader header = ParseHeader(reader);
if (header.Magic != "INTERQUAKEMODEL")
throw new ArgumentException("The file \"" + path + "\" is not an IQM file.", "path");
if (header.FileSize != fs.Length)
throw new DataCorruptionException("The file \"" + path + "\" is invalid or corrupted. The file size doesn't match the size reported in the header.");
if (header.Version != 2)
throw new OutdatedClientException("The file \"" + path + "\" is using a different version of the IQM specification. Make sure to compile the model as version 2.");
//Parse text
if (header.TextOffset != 0)
text = ParseText(reader, (int)header.TextOffset, (int)header.TextCount);
else
text = new List<string>();
//Parse meshes
if (header.MeshesOffset != 0)
meshes = ParseMeshes(reader, (int)header.MeshesOffset, (int)header.MeshesCount);
else
meshes = new List<IqmMesh>();
//Parse vertexarrays
if (header.VertexArraysOffset != 0)
vertexArrays = ParseVertexArrays(reader, (int)header.VertexArraysOffset, (int)header.VertexArraysCount);
else
vertexArrays = new List<IqmVertexArray>();
//Parse triangles
if (header.TrianglesOffset != 0)
triangles = ParseTriangles(reader, (int)header.TrianglesOffset, (int)header.TrianglesCount);
else
triangles = new List<IqmTriangle>();
//Parse adjacency
if (header.AdjacencyOffset != 0)
adjacency = ParseAdjacency(reader, (int)header.AdjacencyOffset, (int)header.TrianglesCount);
else
adjacency = new List<IqmAdjacency>();
//Parse joints
if (header.JointsOffset != 0)
joints = ParseJoints(reader, (int)header.JointsOffset, (int)header.JointsCount);
else
joints = new List<IqmJoint>();
//Parse poses
if (header.PosesOffset != 0)
poses = ParsePoses(reader, (int)header.PosesOffset, (int)header.PosesCount);
else
poses = new List<IqmPose>();
//Parse animations
if (header.AnimationsOffset != 0)
animations = ParseAnimations(reader, (int)header.AnimationsOffset, (int)header.AnimationsCount);
else
animations = new List<IqmAnimation>();
//Parse frames
//Parse bounds
if (header.BoundsOffset != 0)
bounds = ParseBounds(reader, (int)header.BoundsOffset);
//Parse vertices
for (int i = 0; i < header.VertexArraysCount; i++)
{
IqmVertexArray va = vertexArrays[i];
if ((int)va.Type > 3) //HACK no animation stuff set up yet.
continue;
IBuffer buf = Resources.CreateBuffer();
reader.BaseStream.Position = va.Offset;
buf.SetData(reader.ReadBytes((int)va.Size * SizeOfIqmFormat(va.Format) * (int)header.VertexesCount), BufferUsageHint.StaticDraw);
attributes.Add(new VertexAttribute(buf, (int)va.Size, 0, 0, ConvertIqmFormat(va.Format), ConvertIqmAttribute(va.Type)));
}
Drawable d = new Drawable(attributes);
d.DrawMode = BeginMode.Triangles;
//Parse indices
IBuffer indBuf = Resources.CreateBuffer();
indBuf.SetData(triangles.ToArray(), BufferUsageHint.StaticDraw);
d.IndexBuffer = indBuf;
d.IndexType = DrawElementsType.UnsignedInt;
d.IndexCount = triangles.Count * 3;
return d;
}
}
}
private unsafe static IqmHeader ParseHeader(BinaryReader reader)
{
byte[] headerData = reader.ReadBytes(IqmHeader.SizeInBytes);
fixed (byte* headerPtr = headerData)
return PtrToStructure<IqmHeader>(headerPtr);
}
private static List<string> ParseText(BinaryReader reader, int position, int count)
{
List<string> text = new List<string>();
StringBuilder builder = new StringBuilder();
reader.BaseStream.Position = position;
for (int i = 0; i < count; i++)
{
byte nextByte = reader.ReadByte();
builder.Append((char)nextByte);
if (nextByte == 0)
{
text.Add(builder.ToString());
builder.Clear();
}
}
return text;
}
private unsafe static List<IqmMesh> ParseMeshes(BinaryReader reader, int position, int count)
{
List<IqmMesh> meshes = new List<IqmMesh>(count);
reader.BaseStream.Position = position;
for (int i = 0; i < count; i++)
fixed (byte* meshPtr = reader.ReadBytes(IqmMesh.SizeInBytes))
meshes.Add(PtrToStructure<IqmMesh>(meshPtr));
return meshes;
}
private unsafe static List<IqmVertexArray> ParseVertexArrays(BinaryReader reader, int position, int count)
{
List<IqmVertexArray> vertexArrays = new List<IqmVertexArray>(count);
reader.BaseStream.Position = position;
for (int i = 0; i < count; i++)
fixed (byte* vertexArrayPtr = reader.ReadBytes(IqmVertexArray.SizeInBytes))
vertexArrays.Add(PtrToStructure<IqmVertexArray>(vertexArrayPtr));
return vertexArrays;
}
private unsafe static List<IqmTriangle> ParseTriangles(BinaryReader reader, int position, int count)
{
List<IqmTriangle> triangles = new List<IqmTriangle>(count);
reader.BaseStream.Position = position;
for (int i = 0; i < count; i++)
fixed (byte* trianglePtr = reader.ReadBytes(IqmTriangle.SizeInBytes))
triangles.Add(PtrToStructure<IqmTriangle>(trianglePtr));
return triangles;
}
private unsafe static List<IqmAdjacency> ParseAdjacency(BinaryReader reader, int position, int count)
{
List<IqmAdjacency> adjacency = new List<IqmAdjacency>(count);
reader.BaseStream.Position = position;
for (int i = 0; i < count; i++)
fixed (byte* adjacencyPtr = reader.ReadBytes(IqmAdjacency.SizeInBytes))
adjacency.Add(PtrToStructure<IqmAdjacency>(adjacencyPtr));
return adjacency;
}
private unsafe static List<IqmJoint> ParseJoints(BinaryReader reader, int position, int count)
{
List<IqmJoint> joints = new List<IqmJoint>(count);
reader.BaseStream.Position = position;
for (int i = 0; i < count; i++)
fixed (byte* jointPtr = reader.ReadBytes(IqmJoint.SizeInBytes))
joints.Add(PtrToStructure<IqmJoint>(jointPtr));
return joints;
}
private unsafe static List<IqmPose> ParsePoses(BinaryReader reader, int position, int count)
{
List<IqmPose> poses = new List<IqmPose>(count);
reader.BaseStream.Position = position;
for (int i = 0; i < count; i++)
fixed (byte* posePtr = reader.ReadBytes(IqmPose.SizeInBytes))
poses.Add(PtrToStructure<IqmPose>(posePtr));
return poses;
}
private unsafe static List<IqmAnimation> ParseAnimations(BinaryReader reader, int position, int count)
{
List<IqmAnimation> animations = new List<IqmAnimation>(count);
reader.BaseStream.Position = position;
for (int i = 0; i < count; i++)
fixed (byte* animationPtr = reader.ReadBytes(IqmAnimation.SizeInBytes))
animations.Add(PtrToStructure<IqmAnimation>(animationPtr));
return animations;
}
private unsafe static IqmBounds ParseBounds(BinaryReader reader, int position)
{
reader.BaseStream.Position = position;
fixed (byte* boundsPtr = reader.ReadBytes(IqmBounds.SizeInBytes))
return PtrToStructure<IqmBounds>(boundsPtr);
}
private unsafe static T PtrToStructure<T>(byte* data)
where T : struct
{
return (T)Marshal.PtrToStructure((IntPtr)data, typeof(T));
}
private static AttributeType ConvertIqmAttribute(IqmAttributeType type)
{
switch (type)
{
case IqmAttributeType.Position:
return AttributeType.Position;
case IqmAttributeType.TexCoord:
return AttributeType.TexCoord;
case IqmAttributeType.Normal:
return AttributeType.Normal;
case IqmAttributeType.Tangent:
return AttributeType.Tangent;
default:
return AttributeType.Position; //return 0.
}
}
private static VertexAttribPointerType ConvertIqmFormat(IqmDataFormat format)
{
switch (format)
{
case IqmDataFormat.Byte:
return VertexAttribPointerType.Byte;
case IqmDataFormat.Double:
return VertexAttribPointerType.Double;
case IqmDataFormat.Float:
return VertexAttribPointerType.Float;
case IqmDataFormat.Half:
return VertexAttribPointerType.HalfFloat;
case IqmDataFormat.Int:
return VertexAttribPointerType.Int;
case IqmDataFormat.Short:
return VertexAttribPointerType.Short;
case IqmDataFormat.UnsignedByte:
return VertexAttribPointerType.UnsignedByte;
case IqmDataFormat.UnsignedInt:
return VertexAttribPointerType.UnsignedInt;
case IqmDataFormat.UnsignedShort:
return VertexAttribPointerType.UnsignedShort;
default:
return VertexAttribPointerType.Float;
}
}
private static int SizeOfIqmFormat(IqmDataFormat format)
{
switch (format)
{
case IqmDataFormat.Byte:
case IqmDataFormat.UnsignedByte:
return 1;
case IqmDataFormat.Half:
case IqmDataFormat.Short:
case IqmDataFormat.UnsignedShort:
return 2;
case IqmDataFormat.Float:
case IqmDataFormat.Int:
case IqmDataFormat.UnsignedInt:
return 4;
case IqmDataFormat.Double:
return 8;
default:
return 0;
}
}
}
}
@kyle-emmerich
Copy link

You've probably forgotten all about this, but there are a lot of issues throughout this code. First off, you can reduce all of your Parse* methods into this:

private unsafe static List<T> ParseItems<T>(BinaryReader reader, int position, int count, int stride) {
        List<T> items = new List<T>(count);

        reader.BaseStream.Position = position;
        for (int i = 0; i < count; i++) {
            fixed (byte* item_ptr = reader.ReadBytes(stride))
                items.Add(Marshal.PtrToStructure<T>((IntPtr)item_ptr));
        }

        return items;
    }

Second, the way you're parsing strings won't really work with how IQM indexes names. The Name index is an offset into the text block by character, not by string.

private static string ParseText(BinaryReader reader, int position, int count) {
        StringBuilder builder = new StringBuilder();

        reader.BaseStream.Position = position;
        for (int i = 0; i < count; i++) {
            byte next_byte = reader.ReadByte();
            builder.Append((char)next_byte);
        }

        return builder.ToString();
    }
    private static string GetName(string all_text, int index) {
        return all_text.Substring(index, all_text.IndexOf('\0', index) - index);
    }

There are a few other issues but those are the ones that really stick out. Also, implementing frames really wasn't all that hard but you're missing a lot of stuff to even begin with that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment