using System; using System.Collections.Generic; using System.Globalization; using System.IO; namespace Oni.Dae.IO { internal class ObjReader { private struct ObjVertex : IEquatable { public int PointIndex; public int TexCoordIndex; public int NormalIndex; public ObjVertex(int pointIndex, int uvIndex, int normalIndex) { PointIndex = pointIndex; TexCoordIndex = uvIndex; NormalIndex = normalIndex; } public static bool operator ==(ObjVertex v1, ObjVertex v2) => v1.PointIndex == v2.PointIndex && v1.TexCoordIndex == v2.TexCoordIndex && v1.NormalIndex == v2.NormalIndex; public static bool operator !=(ObjVertex v1, ObjVertex v2) => v1.PointIndex != v2.PointIndex || v1.TexCoordIndex != v2.TexCoordIndex || v1.NormalIndex != v2.NormalIndex; public bool Equals(ObjVertex v) => this == v; public override bool Equals(object obj) => obj is ObjVertex && Equals((ObjVertex)obj); public override int GetHashCode() => PointIndex ^ TexCoordIndex ^ NormalIndex; } private class ObjFace { public string ObjectName; public string[] GroupsNames; public ObjVertex[] Vertices; } private class ObjMaterial { private readonly string name; private string textureFilePath; private Material material; public ObjMaterial(string name) { this.name = name; } public string Name => name; public string TextureFilePath { get { return textureFilePath; } set { textureFilePath = value; } } public Material Material { get { if (material == null && TextureFilePath != null) CreateMaterial(); return material; } } private void CreateMaterial() { var image = new Image { FilePath = TextureFilePath, Name = name + "_img" }; var effectSurface = new EffectSurface(image); var effectSampler = new EffectSampler(effectSurface); var effectTexture = new EffectTexture { Sampler = effectSampler, Channel = EffectTextureChannel.Diffuse, TexCoordSemantic = "diffuse_TEXCOORD" }; material = new Material { Id = name, Name = name, Effect = new Effect { Id = name + "_fx", DiffuseValue = effectTexture, Parameters = { new EffectParameter("surface", effectSurface), new EffectParameter("sampler", effectSampler) } } }; } } private class ObjPrimitives { public ObjMaterial Material; public readonly List Faces = new List(4); } #region Private data private static readonly string[] emptyStrings = new string[0]; private static readonly char[] whiteSpaceChars = new char[] { ' ', '\t' }; private static readonly char[] vertexSeparator = new char[] { '/' }; private Scene mainScene; private readonly List points = new List(); private readonly List texCoords = new List(); private readonly List normals = new List(); private int pointCount; private int normalCount; private int texCoordCount; private readonly Dictionary pointIndex = new Dictionary(); private readonly Dictionary normalIndex = new Dictionary(); private readonly Dictionary texCoordIndex = new Dictionary(); private readonly List pointRemap = new List(); private readonly List normalRemap = new List(); private readonly List texCoordRemap = new List(); private readonly Dictionary materials = new Dictionary(StringComparer.Ordinal); private string currentObjectName; private string[] currentGroupNames; private readonly List primitives = new List(); private ObjPrimitives currentPrimitives; #endregion public static Scene ReadFile(string filePath) { var reader = new ObjReader(); reader.ReadObjFile(filePath); reader.ImportObjects(); return reader.mainScene; } private void ReadObjFile(string filePath) { mainScene = new Scene(); foreach (string line in ReadLines(filePath)) { var tokens = line.Split(whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries); switch (tokens[0]) { case "o": ReadObject(tokens); break; case "g": ReadGroup(tokens); break; case "v": ReadPoint(tokens); break; case "vn": ReadNormal(tokens); break; case "vt": ReadTexCoord(tokens); break; case "f": case "fo": ReadFace(tokens); break; case "mtllib": ReadMtlLib(filePath, tokens); break; case "usemtl": ReadUseMtl(tokens); break; } } } private void ReadPoint(string[] tokens) { var point = new Vector3( float.Parse(tokens[1], CultureInfo.InvariantCulture), float.Parse(tokens[2], CultureInfo.InvariantCulture), float.Parse(tokens[3], CultureInfo.InvariantCulture)); AddPoint(point); pointCount++; } private void AddPoint(Vector3 point) { int newIndex; if (pointIndex.TryGetValue(point, out newIndex)) { pointRemap.Add(newIndex); } else { pointRemap.Add(points.Count); pointIndex.Add(point, points.Count); points.Add(point); } } private void ReadNormal(string[] tokens) { var normal = new Vector3( float.Parse(tokens[1], CultureInfo.InvariantCulture), float.Parse(tokens[2], CultureInfo.InvariantCulture), float.Parse(tokens[3], CultureInfo.InvariantCulture)); AddNormal(normal); normalCount++; } private void AddNormal(Vector3 normal) { int newIndex; if (normalIndex.TryGetValue(normal, out newIndex)) { normalRemap.Add(newIndex); } else { normalRemap.Add(normals.Count); normalIndex.Add(normal, normals.Count); normals.Add(normal); } } private void ReadTexCoord(string[] tokens) { var texCoord = new Vector2( float.Parse(tokens[1], CultureInfo.InvariantCulture), 1.0f - float.Parse(tokens[2], CultureInfo.InvariantCulture)); AddTexCoord(texCoord); texCoordCount++; } private void AddTexCoord(Vector2 texCoord) { int newIndex; if (texCoordIndex.TryGetValue(texCoord, out newIndex)) { texCoordRemap.Add(newIndex); } else { texCoordRemap.Add(texCoords.Count); texCoordIndex.Add(texCoord, texCoords.Count); texCoords.Add(texCoord); } } private void ReadFace(string[] tokens) { var faceVertices = ReadVertices(tokens); if (currentPrimitives == null) ReadUseMtl(emptyStrings); currentPrimitives.Faces.Add(new ObjFace { ObjectName = currentObjectName, GroupsNames = currentGroupNames, Vertices = faceVertices }); } private ObjVertex[] ReadVertices(string[] tokens) { var vertices = new ObjVertex[tokens.Length - 1]; for (int i = 0; i < vertices.Length; i++) { // // Read a point/texture/normal index pair // var indices = tokens[i + 1].Split(vertexSeparator); if (indices.Length == 0 || indices.Length > 3) throw new InvalidDataException(); // // Extract indices from file: 0 means "not specified" // int pointIndex = int.Parse(indices[0], CultureInfo.InvariantCulture); int texCoordIndex = (indices.Length > 1 && indices[1].Length > 0) ? int.Parse(indices[1], CultureInfo.InvariantCulture) : 0; int normalIndex = (indices.Length > 2 && indices[2].Length > 0) ? int.Parse(indices[2], CultureInfo.InvariantCulture) : 0; // // Adjust for negative indices // if (pointIndex < 0) pointIndex = pointCount + pointIndex + 1; if (texCoordIndex < 0) texCoordIndex = texCoordCount + texCoordIndex + 1; if (normalIndex < 0) normalIndex = normalCount + normalIndex + 1; // // Convert indices to internal representation: range 0..n and -1 means "not specified". // pointIndex = pointIndex - 1; texCoordIndex = texCoordIndex - 1; normalIndex = normalIndex - 1; // // Remap indices // pointIndex = pointRemap[pointIndex]; if (texCoordIndex < 0 || texCoordRemap.Count <= texCoordIndex) texCoordIndex = -1; else texCoordIndex = texCoordRemap[texCoordIndex]; if (normalIndex < 0 || normalRemap.Count <= normalIndex) normalIndex = -1; else normalIndex = normalRemap[normalIndex]; vertices[i] = new ObjVertex { PointIndex = pointIndex, TexCoordIndex = texCoordIndex, NormalIndex = normalIndex }; } return vertices; } private void ReadObject(string[] tokens) { currentObjectName = tokens[1]; } private void ReadGroup(string[] tokens) { currentGroupNames = tokens; } private void ReadUseMtl(string[] tokens) { currentPrimitives = new ObjPrimitives(); if (tokens.Length > 0) materials.TryGetValue(tokens[1], out currentPrimitives.Material); primitives.Add(currentPrimitives); } private void ReadMtlLib(string objFilePath, string[] tokens) { string materialLibraryFilePath = tokens[1]; if (Path.GetExtension(materialLibraryFilePath).Length == 0) materialLibraryFilePath += ".mtl"; var dirPath = Path.GetDirectoryName(objFilePath); var mtlFilePath = Path.Combine(dirPath, materialLibraryFilePath); if (!File.Exists(mtlFilePath)) { Console.Error.WriteLine("Material file {0} does not exist", mtlFilePath); return; } ReadMtlFile(mtlFilePath); } private void ReadMtlFile(string filePath) { var dirPath = Path.GetDirectoryName(filePath); ObjMaterial currentMaterial = null; foreach (string line in ReadLines(filePath)) { var tokens = line.Split(whiteSpaceChars, StringSplitOptions.RemoveEmptyEntries); switch (tokens[0]) { case "newmtl": currentMaterial = new ObjMaterial(tokens[1]); materials[currentMaterial.Name] = currentMaterial; break; case "map_Kd": string textureFilePath = Path.GetFullPath(Path.Combine(dirPath, tokens[1])); if (File.Exists(textureFilePath)) currentMaterial.TextureFilePath = textureFilePath; break; } } } private void ImportObjects() { var inputs = new List(); IndexedInput positionInput, texCoordInput, normalInput; positionInput = new IndexedInput(Semantic.Position, new Source(points)); inputs.Add(positionInput); if (texCoords.Count > 0) { texCoordInput = new IndexedInput(Semantic.TexCoord, new Source(texCoords)); inputs.Add(texCoordInput); } else { texCoordInput = null; } if (normals.Count > 0) { normalInput = new IndexedInput(Semantic.Normal, new Source(normals)); inputs.Add(normalInput); } else { normalInput = null; } var geometry = new Geometry { Vertices = { positionInput } }; var geometryInstance = new GeometryInstance { Target = geometry }; foreach (var primitive in primitives.Where(p => p.Faces.Count > 0)) { var meshPrimtives = new MeshPrimitives(MeshPrimitiveType.Polygons, inputs); foreach (var face in primitive.Faces) { meshPrimtives.VertexCounts.Add(face.Vertices.Length); foreach (var vertex in face.Vertices) { positionInput.Indices.Add(vertex.PointIndex); if (texCoordInput != null) texCoordInput.Indices.Add(vertex.TexCoordIndex); if (normalInput != null) normalInput.Indices.Add(vertex.NormalIndex); } } geometry.Primitives.Add(meshPrimtives); if (primitive.Material != null && primitive.Material.Material != null) { meshPrimtives.MaterialSymbol = "mat" + geometryInstance.Materials.Count; geometryInstance.Materials.Add(new MaterialInstance { Symbol = meshPrimtives.MaterialSymbol, Target = primitive.Material.Material, Bindings = { new MaterialBinding("diffuse_TEXCOORD", texCoordInput) } }); } } mainScene.Nodes.Add(new Node { Instances = { geometryInstance } }); } private Vector3 ComputeFaceNormal(ObjVertex[] vertices) { if (vertices.Length < 3) return Vector3.Up; var v1 = points[vertices[0].PointIndex]; var v2 = points[vertices[1].PointIndex]; var v3 = points[vertices[2].PointIndex]; return Vector3.Normalize(Vector3.Cross(v2 - v1, v3 - v1)); } private static IEnumerable ReadLines(string filePath) { using (var reader = File.OpenText(filePath)) { for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) { line = line.Trim(); if (line.Length == 0) continue; int commentStart = line.IndexOf('#'); if (commentStart != -1) { line = line.Substring(0, commentStart).Trim(); if (line.Length == 0) continue; } yield return line; } } } } }