// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Threading.Tasks; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Utilities { /// /// Utility for generating and saving OBJ files from GameObjects and their Meshes /// public static class OBJWriterUtility { /// /// Export mesh data of provided GameObject, and children if enabled, to file provided in OBJ format /// /// /// Traversal of GameObject mesh data is done via Coroutine on main Unity thread due to limitations by Unity. /// If a file does not exist at given file path, a new one is automatically created /// If applicable, children Mesh data will be bundled into same OBJ file. /// public static async Task ExportOBJAsync(GameObject root, string filePath, bool includeChildren = true) { if (string.IsNullOrEmpty(filePath)) { throw new Exception("Invalid file path"); } Debug.Log($"Exporting GameObject {root.name} to {filePath} OBJ file"); // Await coroutine that must execute on Unity's main thread string getObjData = await CreateOBJFileContentAsync(root, includeChildren); using (FileStream fs = new FileStream(filePath, FileMode.Create)) { using (StreamWriter sw = new StreamWriter(fs)) { await sw.WriteAsync(getObjData); } } } /// /// Coroutine async method that generates string in OBJ file format of provided GameObject's Mesh, and possibly children. /// /// GameObject to target for pulling MeshFilter data /// Include Mesh data of children GameObjects as sub-meshes in output /// string of all mesh data (no materials) in OBJ file format public static IEnumerator CreateOBJFileContentAsync(GameObject target, bool includeChildren) { StringBuilder objBuffer = new StringBuilder(); objBuffer.Append($"# {target.name}").AppendNewLine(); var dt = DateTime.Now; objBuffer.Append($"# {dt.ToString(CultureInfo.InvariantCulture)}").AppendNewLine(); Stack processStack = new Stack(); processStack.Push(target.transform); // If including sub-meshes, need to track vertex indices in relation to entire file int startVertexIndex = 0; // DFS processing routine to add Mesh data to OBJ while (processStack.Count != 0) { var current = processStack.Pop(); MeshFilter meshFilter = current.GetComponent(); if (meshFilter != null) { CreateOBJDataForMesh(meshFilter, objBuffer, ref startVertexIndex); } if (includeChildren) { for (int i = 0; i < current.childCount; i++) { processStack.Push(current.GetChild(i)); } } yield return null; } yield return objBuffer.ToString(); } private static void CreateOBJDataForMesh(MeshFilter meshFilter, StringBuilder buffer, ref int startVertexIndex) { Mesh mesh = meshFilter.sharedMesh; if (!mesh) { return; } var transform = meshFilter.transform; buffer.AppendNewLine().Append("g ").Append(transform.name).AppendNewLine(); foreach (Vector3 vertex in mesh.vertices) { Vector3 v = transform.TransformPoint(vertex); buffer.Append(FormattableString.Invariant($"v {-1 * v.x} {v.y} {v.z}\n")); } buffer.AppendNewLine(); foreach (Vector3 normal in mesh.normals) { Vector3 vn = transform.TransformDirection(normal); buffer.Append(FormattableString.Invariant($"vn {-1 * vn.x} {vn.y} {vn.z}\n")); } buffer.AppendNewLine(); foreach (Vector3 uv in mesh.uv) { buffer.Append(FormattableString.Invariant($"vt {uv.x} {uv.y}\n")); } for (int idx = 0; idx < mesh.subMeshCount; idx++) { buffer.AppendNewLine(); int[] triangles = mesh.GetTriangles(idx); for (int i = 0; i < triangles.Length; i += 3) { buffer.Append(string.Format("f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}\n", triangles[i + 2] + 1 + startVertexIndex, triangles[i + 1] + 1 + startVertexIndex, triangles[i] + 1 + startVertexIndex)); } } startVertexIndex += mesh.vertexCount; } } }