/// <summary> /// Loads a 3D model as an array of vertices from an OBJ file. /// </summary> /// <typeparam name="T">The type of vertex to load. This defines which vertex data will and will not be loaded.</typeparam> /// <param name="stream">The <see cref="Stream"/> from which the OBJ file will be read from.</param> /// <param name="options">Specifies options that modify how an OBJ file is parsed.</param> /// <returns>An array with the parsed vertex data as a triangle list.</returns> /// <exception cref="ObjLoaderException"/> public static T[] FromStream <T>(Stream stream, ObjLoadOptions options = ObjLoadOptions.None) where T : unmanaged { if (stream == null) { throw new ArgumentNullException(nameof(stream)); } return(FromStream <T>(new StreamReader(stream), options)); }
/// <summary> /// Loads a 3D model as an array of vertices from an OBJ file. /// </summary> /// <typeparam name="T">The type of vertex to load. This defines which vertex data will and will not be loaded.</typeparam> /// <param name="file">The path to the OBJ file on disk.</param> /// <param name="options">Specifies options that modify how an OBJ file is parsed.</param> /// <returns>An array with the parsed vertex data as a triangle list.</returns> /// <exception cref="ObjLoaderException"/> public static T[] FromFile <T>(string file, ObjLoadOptions options = ObjLoadOptions.None) where T : unmanaged { if (string.IsNullOrEmpty(file)) { throw new ArgumentNullException(nameof(file)); } using FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); return(FromStream <T>(new StreamReader(fs), options)); }
public static void ObjLoadOption() { // ExStart:ObjLoadOption // The path to the documents directory. string MyDir = RunExamples.GetDataDir(); // Initialize an object ObjLoadOptions loadObjOpts = new ObjLoadOptions(); // Import materials from external material library file loadObjOpts.EnableMaterials = true; // Flip the coordinate system. loadObjOpts.FlipCoordinateSystem = true; // Configure the look up paths to allow importer to find external dependencies. loadObjOpts.LookupPaths = new List<string>(new string[] { MyDir}); // ExEnd:ObjLoadOption }
private static void ObjLoadOption() { // ExStart:ObjLoadOption // The path to the documents directory. string dataDir = RunExamples.GetDataDir(); // Initialize an object ObjLoadOptions loadObjOpts = new ObjLoadOptions(); // Import materials from external material library file loadObjOpts.EnableMaterials = true; // Flip the coordinate system. loadObjOpts.FlipCoordinateSystem = true; // Configure the look up paths to allow importer to find external dependencies. loadObjOpts.LookupPaths = new List <string>(new string[] { dataDir }); // ExEnd:ObjLoadOption }
/// <summary> /// Loads a 3D model as an array of vertices from an OBJ file. /// </summary> /// <typeparam name="T">The type of vertex to load. This defines which vertex data will and will not be loaded.</typeparam> /// <param name="streamReader">The <see cref="StreamReader"/> from which the OBJ file will be read from.</param> /// <param name="options">Specifies options that modify how an OBJ file is parsed.</param> /// <returns>An array with the parsed vertex data as a triangle list.</returns> /// <exception cref="ObjLoaderException"/> public static T[] FromStream <T>(StreamReader streamReader, ObjLoadOptions options = ObjLoadOptions.None) where T : unmanaged { if (streamReader == null) { throw new ArgumentNullException(nameof(streamReader)); } // tmp will be used to check which vertex data to load with the is pattern at the beginning // and to format the vertex attributes into memory after the file has been parsed. T tmp = default; // We check which vertex attributes to load and which not to depending on the vertex type. bool loadNormals = tmp is VertexNormal || tmp is VertexNormalColor || tmp is VertexNormalTexture || tmp is VertexNormalColorTexture; bool loadColors = tmp is VertexColor || tmp is VertexColorTexture || tmp is VertexNormalColor || tmp is VertexNormalColorTexture; bool loadTexCoords = tmp is VertexTexture || tmp is VertexColorTexture || tmp is VertexNormalTexture || tmp is VertexNormalColorTexture; // Let's ensure the vertex type is valid. if (!loadNormals && !loadColors && !loadTexCoords && !(tmp is VertexPosition || tmp is Vector3)) { throw new ObjLoaderException("Vertex format not supported. Use a library-provided vertex type instead."); } bool largePolygons = !options.HasFlag(ObjLoadOptions.TrianglesOnly); // We're gonna need lists to store temporary data. Let's get them form here. GetLists(loadNormals, loadColors, loadTexCoords, out List <Vector3> positions, out List <Vector3> normals, out List <Color4b> colors, out List <Vector2> texCoords, out List <int> indices); // When something goes wrong, it's gonna be nice to have the line number on the exception's message ;) int currentLineNumber = 0; bool doneParsingData = false; try { // In this variable we will count the amount of vertices int vertexCount = 0; // This is the buffer we'll use to parse numbers. The initial buffer is a relatively // small, stackallocated array but if we need more space a char[] will be allocated // and this span will be replaced to point at that larger array instead. Span <char> charBuffer = stackalloc char[CharBufferLength]; while (!streamReader.EndOfStream) { currentLineNumber++; // This will skip empty lines (or lines that are only whitespaces) and advance // the reader to skip any whitespaces at the beginning of a line. if (SkipWhitespaces(streamReader)) { SkipLine(streamReader); continue; } // We read the next character (it's guaranteed not to be a whitespace). char currentChar = (char)streamReader.Read(); if (currentChar == 'v') { // The line starts with 'v'. Let's read the next character to see what to do next. currentChar = (char)streamReader.Read(); // The entire line might be just "v\n". This should throw an error. if (streamReader.EndOfStream || currentChar == NewlineIndicator) { throw new FormatException("Unexpected end of line"); } // We check that character and if (char.IsWhiteSpace(currentChar)) { // We need to load a Vector3 position positions.Add(new Vector3( ReadNextFloat(streamReader, ref charBuffer), ReadNextFloat(streamReader, ref charBuffer), ReadNextFloat(streamReader, ref charBuffer) )); if (loadColors) { // If we're also loading colors, we load a color4b from 3 floats colors.Add(new Color4b( ReadNextFloat(streamReader, ref charBuffer), ReadNextFloat(streamReader, ref charBuffer), ReadNextFloat(streamReader, ref charBuffer) )); } } else if (loadNormals && currentChar == 'n') { // We need to load a Vector3 normal Vector3 norm = new Vector3( ReadNextFloat(streamReader, ref charBuffer), ReadNextFloat(streamReader, ref charBuffer), ReadNextFloat(streamReader, ref charBuffer) ); normals.Add(norm); } else if (loadTexCoords && currentChar == 't') { // We need to load a Vector2 with texture coordinates Vector2 coords = new Vector2( ReadNextFloat(streamReader, ref charBuffer), ReadNextFloat(streamReader, ref charBuffer) ); texCoords.Add(coords); } } else if (currentChar == 'f') { // This line is specifying a face. We need to read a polygon face. // The way we load faces into triangles is by making a triangle fan. // Example: vertices 0 1 2 3 4 5 will be turned into the triangles: // (0 1 2) (0 2 3) (0 3 4) (0 4 5) // Looking at this you can see that for each triangle, we need to know // the polygon's first vertex, and the last vertex of the previous triangle. // We start by loading the first two vertices and verifying their indices. // The indices of the first vertex will be in "first0", "second0" and "third0". ReadThreeIntegers(streamReader, ref charBuffer, out int first0, out int second0, out int third0); VerifyIndices(first0, second0, third0); ReadThreeIntegers(streamReader, ref charBuffer, out int lastFirst, out int lastSecond, out int lastThird); VerifyIndices(lastFirst, lastSecond, lastThird); // When we add the indices to the list we need to substract one, because OBJ indices // start at 1 (and array indices start at 0, f**k all languages where it doesn't). first0--; second0--; third0--; lastFirst--; lastSecond--; lastThird--; // We now load the third vertex ReadThreeIntegers(streamReader, ref charBuffer, out int first, out int second, out int third); do { // We have to add one more triangle. Let's start by verifying the last indices we read. VerifyIndices(first, second, third); // We add the polygon's first vertex. indices.Add(first0); if (loadNormals) { indices.Add(third0); } if (loadTexCoords) { indices.Add(second0); } // We add the last vertex (taken from the previous triangle) indices.Add(lastFirst); if (loadNormals) { indices.Add(lastThird); } if (loadTexCoords) { indices.Add(lastSecond); } first--; second--; third--; // We add the current vertex and increment vertexCount by 3. indices.Add(first); if (loadNormals) { indices.Add(third); } if (loadTexCoords) { indices.Add(second); } vertexCount += 3; // The next triangle will need to know the last vertex of the previous triangle. lastFirst = first; lastSecond = second; lastThird = third; // If largePolygons is disabled, this process ends after only one triangle. // Otherwise, this process continues while there are more vertices in this line. } while (largePolygons && TryReadThreeIntegers(streamReader, ref charBuffer, out first, out second, out third)); } // The entire while cycle ensures the end of the line isn't reached until now. // This ensures our currentLineNumber is counting correctly. SkipLine(streamReader); } // We're done reading the file. We now need to process the data. doneParsingData = true; T[] vertices = new T[vertexCount]; // Time to get funky unsafe { int ind = 0; for (int i = 0; i < vertexCount; i++) { // We don't know what type of vertex we have, but we know the attributes // are always in the same order: POSITION NORMAL COLOR TEXCOORDS // So the easiest way to set the data of whatever a T is, is to just // grab a pointer and manually set it, since we can't do new T(position, etc). // The data will be set to tmp, which has a fixed address (it's in the stack) float *ptrToTmp = (float *)&tmp; // We write the position onto tmp and advance the pointer int posIndex = indices[ind++]; Vector3 pos = positions[posIndex]; ptrToTmp[0] = pos.X; ptrToTmp[1] = pos.Y; ptrToTmp[2] = pos.Z; ptrToTmp += 3; // If the vertex type has normal, we write the normal and advance the pointer if (loadNormals) { pos = normals[indices[ind++]]; ptrToTmp[0] = pos.X; ptrToTmp[1] = pos.Y; ptrToTmp[2] = pos.Z; ptrToTmp += 3; } // If the vertex type has color, we write the color and advance the pointer if (loadColors) { ((Color4b *)ptrToTmp)[0] = colors[posIndex]; ptrToTmp++; } // If the vertex type has texcoords, we write the texcoords and advance the pointer if (loadTexCoords) { Vector2 tex = texCoords[indices[ind++]]; ptrToTmp[0] = tex.X; ptrToTmp[1] = 1 - tex.Y; ptrToTmp += 2; } // Now T has the value we wanted. We can store it in the array. vertices[i] = tmp; // Yes, we could write directly to the array. But we'd have to fix it in place. } } // Done! return(vertices); } catch (Exception e) { // If the error happened before doneParsingData was set to true, it was an error // in a specific line. Otherwise, it was an error constructing the vertex data. if (doneParsingData) { throw new ObjLoaderException("Error processing obj data.", e); } else { throw new ObjLoaderException("Error in line " + currentLineNumber + ": " + e.Message, e); } } finally { // We're done here. Let's return the lists so they can be reused if needed. ReturnLists(positions, normals, colors, texCoords, indices); } // Verifies that the given indices read from a group of three integers are valid. void VerifyIndices(int first, int second, int third) { // We check that the position index is valid. if (first < 1 || first > positions.Count) { throw new FormatException("Invalid position index integer: " + first.ToString()); } // If we're loading normals, we check that the normal index is valid. if (loadNormals && (third < 1 || third > normals.Count)) { throw new FormatException("Invalid normal index integer: " + third.ToString()); } // If we're loading texcoords, we check that the texcoord index is valid. if (loadTexCoords && (second < 1 || second > texCoords.Count)) { throw new FormatException("Invalid texture coordinates index integer: " + second.ToString()); } } }