/// <author>Lukas Eibensteiner</author>
/// <date>19.02.2013</date>
/// <summary>Example of a Wavefront OBJ 3D model importer</summary>
using SlimDX;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Games
{
/// <summary>
/// Class for reading a 3D mesh in the Wavefront OBJ format from a stream.
/// </summary>
public class WavefrontReader
{
/// <summary>
/// Enum for describing the semantic meaning of a line in an OBJ file.
/// </summary>
private enum DataType
{
/// <summary>
/// The line contains nothing or has no or an undefined keyword.
/// </summary>
Empty,
/// <summary>
/// The line contains a comment.
/// </summary>
Comment,
/// <summary>
/// The line contains a group definition.
/// </summary>
Group,
/// <summary>
/// The line contains a smoothing group definitio.
/// </summary>
SmoothingGroup,
/// <summary>
/// The line contains a position vector definition.
/// </summary>
Position,
/// <summary>
/// The line contains a normal vector definition.
/// </summary>
Normal,
/// <summary>
/// The line contains a texture coordinate definition.
/// </summary>
TexCoord,
/// <summary>
/// The line contains a face definition.
/// </summary>
Face,
}
// Dictionary mapping the DataType enumeration to the corresponding keyword.
private static Dictionary<DataType, string> Keywords
{
get
{
return new Dictionary<DataType, string>()
{
{ DataType.Comment, "#" },
{ DataType.Group, "g" },
{ DataType.SmoothingGroup, "s" },
{ DataType.Position, "v" },
{ DataType.TexCoord, "vt" },
{ DataType.Normal, "vn" },
{ DataType.Face, "f" },
};
}
}
/// <summary>
/// Reads a WavefrontObject instance from the stream.
/// </summary>
/// <param name="stream">
/// Stream containing the OBJ file content.
/// </param>
/// <exception cref="ArgumentNullException">
/// <paramref name="stream"/> is <c>null</c>.
/// </exception>
/// <exception cref="IOException">
/// Error while reading from the stream.
/// </exception>
public WavefrontObject Read(Stream stream)
{
if (stream == null)
throw new ArgumentNullException("stream");
// Create the stream reader for the file
var reader = new StreamReader(stream);
// Store the lines here
var lines = new List<string>();
// Store the current line here
var current = string.Empty;
// Read the file line by line and normalize them
while ((current = reader.ReadLine()) != null)
lines.Add(NormalizeLine(current));
// Create empty mesh instance
var obj = new WavefrontObject();
// Iterate over all lines
foreach (string line in lines)
{
// Get line type and content
DataType type = GetType(line);
string content = GetContent(line, type);
// Line is a position
if (type == DataType.Position)
obj.Positions.Add(ParseVector3(content));
// Line is a texture coordinate
if (type == DataType.TexCoord)
obj.Texcoords.Add(ParseVector2(content));
// Line is a normal vector
if (type == DataType.Normal)
obj.Normals.Add(ParseVector3(content));
// Line is a mesh sub group
if (type == DataType.Group)
obj.Groups.Add(new WavefrontFaceGroup() { Name = content });
// Line is a polygon
if (type == DataType.Face)
{
// Create the default group for all faces outside a group
if (obj.Groups.Count == 0)
obj.Groups.Add(new WavefrontFaceGroup());
// Add the face to the last group added
obj.Groups.Last().Faces.Add(ParseFace(content));
}
}
return obj;
}
// Trim beginning and end and collapse all whitespace in a string to single space.
private string NormalizeLine(string line)
{
return System.Text.RegularExpressions.Regex.Replace(line.Trim(), @"\s+", " ");
}
// Get the type of data stored in the specified line.
private DataType GetType(string line)
{
// Iterate over the keywords
foreach (var item in Keywords)
{
var type = item.Key;
var keyword = item.Value;
// Line starts with current keyword
if (line.ToLower().StartsWith(keyword.ToLower() + " "))
{
// Return current type
return type;
}
}
// No type
return DataType.Empty;
}
// Remove the keyword from the start of the line and return the result.
// Returns an empty string if the specified type was DataType.Empty.
private string GetContent(string line, DataType type)
{
// If empty return empty string,
// else remove the keyword from the start
return type == DataType.Empty
? string.Empty
: line.Substring(Keywords[type].Length).Trim();
}
// Create an array of floats of arbitary length from a string representation,
// where the floats are spearated by whitespace.
private static float[] ParseFloatArray(string str, int count)
{
var floats = new float[count];
var segments = str.Split(' ');
for (int i = 0; i < count; i++)
{
if (i < segments.Length)
{
try
{
floats[i] = float.Parse(segments[i], System.Globalization.CultureInfo.InvariantCulture);
}
catch
{
floats[i] = 0;
}
}
}
return floats;
}
// Parse a 3D vector from a string definition in the form of: 2.0 3.0 1.0
private Vector2 ParseVector2(string str)
{
var components = ParseFloatArray(str, 3);
var vec = new Vector2(components[0], components[1]);
return components[2] == 0
? vec
: vec / components[2];
}
// Parse a 3D vector from a string definition in the form of: 1.0 2.0 3.0 1.0
private Vector3 ParseVector3(string str)
{
var components = ParseFloatArray(str, 4);
var vec = new Vector3(components[0], components[1], components[2]);
return components[3] == 0
? vec
: vec / components[3];
}
// Parse a OBJ face from a string definition.
private WavefrontFace ParseFace(string str)
{
// Split the face definition at whitespace
var segments = str.Split(new Char[0], StringSplitOptions.RemoveEmptyEntries);
var vertices = new List<WavefrontVertex>();
// Iterate over the segments
foreach (string segment in segments)
{
// Parse and add the vertex
vertices.Add(ParseVertex(segment));
}
// Create and return the face
return new WavefrontFace()
{
Vertices = vertices,
};
}
// Parse an OBJ vertex from a string definition in the forms of:
// 1/2/3
// 1//3
// 1/2
// 1
private WavefrontVertex ParseVertex(string str)
{
// Split the string definition at the slash separator
var segments = str.Split('/');
// Store the vertex indices here
var indices = new int[3];
// Iterate 3 times
for (int i = 0; i < 3; i++)
{
// If no segment exists at the location or the segment can not be passed to an integer
// Set the index to zero
if (segments.Length <= i || !int.TryParse(segments[i], out indices[i]))
indices[i] = 0;
}
// Create the new vertex
return new WavefrontVertex()
{
Position = indices[0],
Texcoord = indices[1],
Normal = indices[2],
};
}
}
/// <summary>
/// Class representing a Wavefront OBJ 3D mesh.
/// </summary>
public class WavefrontObject
{
public WavefrontObject()
{
Groups = new List<WavefrontFaceGroup>();
Positions = new List<Vector3>();
Texcoords = new List<Vector2>();
Normals = new List<Vector3>();
}
// Lists containing the vertex components
public List<Vector3> Positions { get; private set; }
public List<Vector2> Texcoords { get; private set; }
public List<Vector3> Normals { get; private set; }
// List of sub meshes
public List<WavefrontFaceGroup> Groups { get; private set; }
}
/// <summary>
/// Struct representing an Wavefront OBJ face group.
/// </summary>
/// <remarks>
/// Groups contain faces and subdivide a geometry into smaller objects.
/// </remarks>
public class WavefrontFaceGroup
{
public WavefrontFaceGroup()
{
Faces = new List<WavefrontFace>();
}
// Name of the sub mesh
public string Name { get; set; }
// A list of faces
public List<WavefrontFace> Faces { get; set; }
// Get the total number of triangles
public int TriangleCount
{
get
{
var count = 0;
foreach (var face in Faces)
count += face.TriangleCount;
return count;
}
}
}
/// <summary>
/// A struct representing a Wavefront OBJ geometry face.
/// </summary>
/// <remarks>
/// A face is described through a list of OBJ vertices.
/// It can consist of three or more vertices an can therefore be split up
/// into one or more triangles.
/// </remarks>
public struct WavefrontFace
{
public List<WavefrontVertex> Vertices { get; set; }
// Number of triangles the face (polygon) consists of
public int TriangleCount
{
get
{
return Vertices.Count - 2;
}
}
// Number of vertices
public int VertexCount
{
get { return Vertices.Count; }
}
}
/// <summary>
/// A struct representing an Wavefront OBJ vertex.
/// </summary>
/// <remarks>
/// OBJ vertices are indexed vertices so instead of vectors
/// it has an index for the position, texture coordinate and normal.
/// Each of those indices points to a location in a list of vectors.
/// </remarks>
public struct WavefrontVertex
{
public WavefrontVertex(int position, int texcoord, int normal)
: this()
{
Position = position;
Texcoord = texcoord;
Normal = normal;
}
// Inidices of the vertex components
public int Position { get; set; }
public int Normal { get; set; }
public int Texcoord { get; set; }
}
}