diff --git a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreClient.cs b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreClient.cs index 94d9600f..9ba77e30 100644 --- a/Assets/Scripts/api_clients/objectstore_client/ObjectStoreClient.cs +++ b/Assets/Scripts/api_clients/objectstore_client/ObjectStoreClient.cs @@ -48,7 +48,8 @@ internal CreateMeshWork(Dictionary materials, string obj, public void BackgroundWork() { - successfullyReadMesh = ObjImporter.ImportMeshes(objString, materials, out meshes); + // TODO AB: We need to create mmeshes not MeshVerticesAndTriangles + // successfullyReadMesh = ObjImporter.ImportMeshes(objString, materials, out meshes); } public void PostWork() diff --git a/Assets/Scripts/desktop_app/DebugConsole.cs b/Assets/Scripts/desktop_app/DebugConsole.cs index fc2e92ad..3d8b557c 100644 --- a/Assets/Scripts/desktop_app/DebugConsole.cs +++ b/Assets/Scripts/desktop_app/DebugConsole.cs @@ -15,6 +15,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Text; using UnityEngine; @@ -43,6 +44,7 @@ public class DebugConsole : MonoBehaviour "flag\n lists/sets feature flags\n" + "fuse\n fuses all selected meshes into a single mesh.\n" + "help\n shows this help text\n" + + "import\n insert 3d model (obj, block\n" + "insert\n insert primitives\n" + "insertduration \n sets the mesh insert effect duration (e.g. 0.6).\n" + "loadfile \n loads a model from the given file (use full path).\n" + @@ -63,6 +65,7 @@ public class DebugConsole : MonoBehaviour public GameObject consoleObject; public Text consoleOutput; public InputField consoleInput; + public ObjImportController objImportController; private string lastCommand = ""; @@ -73,6 +76,7 @@ public class DebugConsole : MonoBehaviour public void Start() { + objImportController = gameObject.GetComponent(); consoleOutput.text = "DEBUG CONSOLE\n" + "Blocks version: " + Config.Instance.version + "\n" + "For a list of available commands, type 'help'." + @@ -190,6 +194,9 @@ private void RunCommand(string command) case "tut": CommandTut(parts); break; + case "import": + CommandImport(parts); + break; default: PrintLn("Unrecognized command: " + command); PrintLn("Type 'help' for a list of commands."); @@ -197,6 +204,12 @@ private void RunCommand(string command) } } + private void CommandImport(string[] parts) + { + string userPath = PeltzerMain.Instance.userPath; + objImportController.Import(parts.Skip(1).Select(p => Path.Combine(userPath, p)).ToArray()); + } + private void PrintLn(string message) { consoleOutput.text += message + "\n"; diff --git a/Assets/Scripts/desktop_app/ObjImportController.cs b/Assets/Scripts/desktop_app/ObjImportController.cs index c857504d..e86917f8 100644 --- a/Assets/Scripts/desktop_app/ObjImportController.cs +++ b/Assets/Scripts/desktop_app/ObjImportController.cs @@ -14,6 +14,7 @@ using UnityEngine; using System.IO; +using System.Linq; using System.Text; // using System.Windows.Forms; @@ -25,7 +26,7 @@ namespace com.google.apps.peltzer.client.desktop_app { /// - /// Responsible for handling the button-click to import obj files from the desktop app, and + /// Responsible for handling the button-click to import obj files from the desktop app, and /// loading them into the model. /// public class ObjImportController : MonoBehaviour @@ -37,14 +38,15 @@ public class ObjImportController : MonoBehaviour private const float MIN_IMPORTED_OBJ_DISTANCE_FROM_USER = 2.0f; /// - /// Handles the button-click to import an obj. Opens up a dialog and in the background, waits for the + /// Either handles the button-click to import an obj. Opens up a dialog and in the background, waits for the /// user to hit 'ok' with two files selected. + /// or imports the obj files passed in as arguments. /// - public void SelectObjToImport() + public void Import(string[] filenames = null) { Model model = PeltzerMain.Instance.GetModel(); - BackgroundWork openDialog = new OpenFileDialogAndLoadObj(model); - PeltzerMain.Instance.DoPolyMenuBackgroundWork(openDialog); + BackgroundWork work = new LoadObj(model, filenames); + PeltzerMain.Instance.DoPolyMenuBackgroundWork(work); } /// @@ -69,51 +71,91 @@ private static string FileToString(string filename) } } - class OpenFileDialogAndLoadObj : BackgroundWork + public class LoadObj : BackgroundWork { // A reference to the model. private readonly Model model; // File contents to be passed from a background thread to a foreground thread. string mtlFileContents; string objFileContents; + private string[] filenames; PeltzerFile peltzerFile; - public OpenFileDialogAndLoadObj(Model model) + public LoadObj(Model model, string[] filenames = null) { this.model = model; + this.filenames = filenames; } // In the background we perform all the File I/O to get file contents. There are no graceful failures here, // and there is no feedback to the user in case of failure. public void BackgroundWork() { - // OpenFileDialog dialog = new OpenFileDialog(); - // dialog.Multiselect = true; - // Expect that the user selected two files, one .obj and one .mtl - // if (dialog.ShowDialog() == DialogResult.OK) { - // if (dialog.FileNames.Length == 1) { - // if (dialog.FileNames[0].EndsWith(".peltzer") || dialog.FileNames[0].EndsWith(".poly") - // || dialog.FileNames[0].EndsWith(".blocks")) { - // byte[] peltzerFileBytes = File.ReadAllBytes(dialog.FileNames[0]); - // PeltzerFileHandler.PeltzerFileFromBytes(peltzerFileBytes, out peltzerFile); - // } else if (dialog.FileNames[0].EndsWith(".obj")) { - // objFileContents = FileToString(dialog.FileNames[0]); - // } else { - // Debug.Log("When selecting only one file for OBJ import, it must have a .obj extension"); - // } - // } else if (dialog.FileNames.Length == 2) { - // string objFile = dialog.FileNames[0].EndsWith(".obj") ? dialog.FileNames[0] : dialog.FileNames[1]; - // string mtlFile = dialog.FileNames[0].EndsWith(".mtl") ? dialog.FileNames[0] : dialog.FileNames[1]; - // if (!objFile.EndsWith(".obj") || !mtlFile.EndsWith(".mtl")) { - // Debug.Log("When selecting two files for OBJ import, one must be .obj and the other .mtl"); - // } + if (filenames != null) + { + DoImport(filenames); + } + else + { + // OpenFileDialog dialog = new OpenFileDialog(); + // dialog.Multiselect = true; + // // Expect that the user selected two files, one .obj and one .mtl + // if (dialog.ShowDialog() == DialogResult.OK) + // { + // DoImport(dialog.FileNames); + // } + + } + } + + public void DoImport(string[] filenames) + { + string objFile = null; + string mtlFile = null; + + // Should we retire some of these file extensions? + // Does anything other than blocks exist in the wild? + if (filenames.Length == 1 && + (filenames[0].EndsWith(".peltzer") + || filenames[0].EndsWith(".poly") + || filenames[0].EndsWith(".blocks"))) + { + byte[] peltzerFileBytes = File.ReadAllBytes(filenames[0]); + PeltzerFileHandler.PeltzerFileFromBytes(peltzerFileBytes, out peltzerFile); + return; + } + + if (filenames.Length == 1 && filenames[0].EndsWith(".obj")) + { + objFile = filenames[0]; + mtlFile = filenames[0].Replace(".obj", ".mtl"); + if (!File.Exists(mtlFile)) + { + mtlFile = null; + } + } + else if (filenames.Length == 2) + { + objFile = filenames.FirstOrDefault(f => f.EndsWith(".obj")); + mtlFile = filenames.FirstOrDefault(f => f.EndsWith(".mtl")); + } - // objFileContents = FileToString(objFile); - // mtlFileContents = FileToString(mtlFile); - // } else { - // Debug.Log("Exactly one .obj file or a pair of .obj and .mtl files must be selected for OBJ import"); - // } - // } + if (filenames.Length == 2 && mtlFile == null) + { + Debug.Log("When selecting two files for OBJ import, one must be .obj and the other .mtl"); + } + else if (objFile == null) + { + Debug.Log("Exactly one .obj file or a pair of .obj and .mtl files must be selected for OBJ import"); + } + else + { + objFileContents = FileToString(objFile); + if (mtlFile != null) + { + mtlFileContents = FileToString(mtlFile); + } + } } // In the foreground we add the mesh to the model. diff --git a/Assets/Scripts/model/import/ObjImporter.cs b/Assets/Scripts/model/import/ObjImporter.cs index c29756bb..eee4a702 100644 --- a/Assets/Scripts/model/import/ObjImporter.cs +++ b/Assets/Scripts/model/import/ObjImporter.cs @@ -40,16 +40,29 @@ public static class ObjImporter public static bool MMeshFromObjFile(string objFileContents, string mtlFileContents, int id, out MMesh result) { Dictionary materials = ImportMaterials(mtlFileContents); - Dictionary> materialsAndMeshes; - if (ImportMeshes(objFileContents, materials, out materialsAndMeshes)) + var success = ImportMeshes(id, objFileContents, materials, out MMesh mmesh); + if (success) { - result = MeshHelper.MMeshFromMeshes(id, materialsAndMeshes); + result = mmesh; return true; } result = null; return false; } + private static int TryGetMaterialId(Material material) + { + if (material.name.StartsWith("mat")) + { + int val = 1; + if (int.TryParse(material.name.Substring(3), out val)) + { + return val; + } + } + return 1; + } + public static Dictionary ImportMaterials(string materialsString) { Dictionary materials = new Dictionary(); @@ -63,7 +76,7 @@ public static Dictionary ImportMaterials(string materialsStrin { if (currentText.StartsWith("newmtl")) { - string materialName = currentText.Split(' ')[1]; + string materialName = currentText.Split(new [] {' '}, StringSplitOptions.RemoveEmptyEntries)[1]; Color materialColor = Color.white; currentText = reader.ReadLine(); @@ -72,13 +85,13 @@ public static Dictionary ImportMaterials(string materialsStrin currentText = currentText.Trim(); if (currentText.StartsWith("Ka")) { - string[] colorString = currentText.Split(' '); + string[] colorString = currentText.Split(new [] {' '}, StringSplitOptions.RemoveEmptyEntries); materialColor = new Color(float.Parse(colorString[1]), float.Parse(colorString[2]), float.Parse(colorString[3])); } else if (currentText.StartsWith("Kd")) { - string[] colorString = currentText.Split(' '); + string[] colorString = currentText.Split(new [] {'/'}, StringSplitOptions.RemoveEmptyEntries); materialColor = new Color(float.Parse(colorString[1]), float.Parse(colorString[2]), float.Parse(colorString[3])); } @@ -113,17 +126,17 @@ public static Dictionary ImportMaterials(string materialsStrin return materials; } - private class Face + public class FFace { public List vertexIds = new List(); } - public static bool ImportMeshes(string objFileContents, - Dictionary materials, out Dictionary> meshes) + public static bool ImportMeshes(int id, string objFileContents, Dictionary materials, out MMesh mmesh) { - meshes = new Dictionary>(); - if (objFileContents == null || objFileContents.Length == 0) + + if (string.IsNullOrEmpty(objFileContents)) { + mmesh = null; return false; } @@ -135,9 +148,9 @@ public static bool ImportMeshes(string objFileContents, materials.Add(currentMaterial, MaterialRegistry.GetMaterialAndColorById(0).material); } - List allVertices = new List(); - List allTexVertices = new List(); - Dictionary> faces = new Dictionary>(); + var allVertices = new Dictionary>(); + var allTexVertices = new Dictionary>(); + var allFaces = new Dictionary>(); string[] parts; char[] sep = { ' ' }; @@ -146,55 +159,69 @@ public static bool ImportMeshes(string objFileContents, string[] sep4 = { "//" }; using (StringReader reader = new StringReader(objFileContents)) { + + Dictionary verticesById = new Dictionary(); + Dictionary facesById = new Dictionary(); + string line = reader.ReadLine(); while (line != null) { if (line.StartsWith("v ")) { - parts = line.Trim().Split(sep); - if (parts.Count() < 4) + parts = line.Trim().Split(sep, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 4) { Debug.Log("Not enough vertex values"); Debug.Log(line); + mmesh = null; return false; } try { - allVertices.Add(new Vector3(Convert.ToSingle(parts[1]), Convert.ToSingle(parts[2]), Convert.ToSingle(parts[3]))); + var v = new Vector3(Convert.ToSingle(parts[1]), Convert.ToSingle(parts[2]), Convert.ToSingle(parts[3])); + if (!allVertices.ContainsKey(currentMaterial)) allVertices.Add(currentMaterial, new List()); + int vIndex = verticesById.Count; + var vert = new Vertex(vIndex, v); + verticesById.Add(vIndex, vert); + allVertices[currentMaterial].Add(vert); } catch (FormatException) { Debug.Log("Unexpected vertex value"); Debug.Log(line); + mmesh = null; return false; } } else if (line.StartsWith("vt ")) { - parts = line.Trim().Split(sep); + parts = line.Trim().Split(sep, StringSplitOptions.RemoveEmptyEntries); if (parts.Count() < 3) { Debug.Log("Not enough tex vertex values"); Debug.Log(line); + mmesh = null; return false; } try { - allTexVertices.Add(new Vector2(Convert.ToSingle(parts[1]), Convert.ToSingle(parts[2]))); + if (!allTexVertices.ContainsKey(currentMaterial)) allTexVertices.Add(currentMaterial, new List()); + allTexVertices[currentMaterial].Add(new Vector2(Convert.ToSingle(parts[1]), Convert.ToSingle(parts[2]))); } catch (FormatException) { Debug.Log("Unexpected tex vertex value"); Debug.Log(line); + mmesh = null; return false; } } else if (line.StartsWith("usemtl ") && mtlFileWasSupplied) { - parts = line.Trim().Split(sep); + parts = line.Trim().Split(sep, StringSplitOptions.RemoveEmptyEntries); if (parts[1].Contains(sep2[0])) { - currentMaterial = parts[1].Split(sep2)[1]; + currentMaterial = parts[1].Split(sep2, StringSplitOptions.RemoveEmptyEntries)[1]; } else { @@ -203,72 +230,73 @@ public static bool ImportMeshes(string objFileContents, } else if (line.StartsWith("f ")) { - parts = line.Trim().Split(sep); + parts = line.Trim().Split(sep, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 4) { Debug.Log("Not vertex values in a face"); Debug.Log(line); + mmesh = null; return false; } - Face face = new Face(); + FFace face = new FFace(); for (int i = 1; i < parts.Length; i++) { if (parts[i].Contains(sep4[0])) { // -1 as vertices are 0-indexed when read but 1-indexed when referenced. - face.vertexIds.Add(int.Parse(parts[i].Split(sep4, StringSplitOptions.None)[0]) - 1); + face.vertexIds.Add(int.Parse(parts[i].Split(sep4, StringSplitOptions.RemoveEmptyEntries)[0]) - 1); } else if (parts[i].Contains(sep3[0])) { // -1 as vertices are 0-indexed when read but 1-indexed when referenced. - face.vertexIds.Add(int.Parse(parts[i].Split(sep3)[0]) - 1); + face.vertexIds.Add(int.Parse(parts[i].Split(sep3, StringSplitOptions.RemoveEmptyEntries)[0]) - 1); //face.uvIds.Add(int.Parse(vIds[1])); } else { // -1 as vertices are 0-indexed when read but 1-indexed when referenced. - face.vertexIds.Add(int.Parse(parts[i]) - 1); + string vIndex = parts[i].Split(new [] {'/'}, StringSplitOptions.RemoveEmptyEntries)[0]; // Ignore texture and normal indices. + face.vertexIds.Add(int.Parse(vIndex) - 1); } } - if (!faces.ContainsKey(currentMaterial)) + if (!allFaces.ContainsKey(currentMaterial)) { - faces.Add(currentMaterial, new List()); + allFaces.Add(currentMaterial, new List()); } - faces[currentMaterial].Add(face); + allFaces[currentMaterial].Add(face); } line = reader.ReadLine(); } - } - // Create one mesh per entry in faceList, as all faces will have the same material. - foreach (KeyValuePair> faceList in faces) - { - // A list of vertics in this mesh. - List meshVertices = new List(); - // Used to translate a vertex id from the obj file to an index into meshVertices. - Dictionary localVertexIds = new Dictionary(); - // A list of triangles in this mesh. - List triangles = new List(); - - foreach (Face face in faceList.Value) + // Create one mesh per entry in faceList, as all faces will have the same material. + foreach (KeyValuePair> faceList in allFaces) { - foreach (int idx in face.vertexIds) + int newFaceIndex = 0; + // A list of triangles in this mesh. + var faceIndices = new List>(); + + + foreach (FFace face in faceList.Value) { - if (!localVertexIds.ContainsKey(idx)) + currentMaterial = faceList.Key; + var singleFaceIndices = new List(); + foreach (int idx in face.vertexIds) { - localVertexIds.Add(idx, meshVertices.Count); - meshVertices.Add(allVertices[idx]); + singleFaceIndices.Add(idx); } - } - for (int i = 2; i < face.vertexIds.Count; i++) - { - triangles.Add(localVertexIds[face.vertexIds[i - 2]]); - triangles.Add(localVertexIds[face.vertexIds[i - 1]]); - triangles.Add(localVertexIds[face.vertexIds[i]]); - } - } // foreach face - meshes.Add(materials[faceList.Key], BreakIntoMultipleMeshes(meshVertices, triangles)); - } // foreach facelist + //singleFaceIndices.Reverse(); + var newFace = new Face( + newFaceIndex, + singleFaceIndices.AsReadOnly(), + verticesById, + new FaceProperties(TryGetMaterialId(materials[currentMaterial])) + ); + facesById.Add(newFaceIndex++, newFace); + faceIndices.Add(singleFaceIndices); + } // foreach face + } // foreach facelist + mmesh = new MMesh(id, Vector3.zero, Quaternion.identity, verticesById, facesById); + } return true; } diff --git a/Assets/Scripts/model/render/MeshHelper.cs b/Assets/Scripts/model/render/MeshHelper.cs index e6c54f7d..3732f443 100644 --- a/Assets/Scripts/model/render/MeshHelper.cs +++ b/Assets/Scripts/model/render/MeshHelper.cs @@ -215,7 +215,7 @@ private static List> MeshComponentsForMenu(IEnum MeshGenContext existingContext = contextDict[material]; if (existingContext.verts.Count + newContext.verts.Count > ReMesher.MAX_VERTS_PER_MESH) { - // If adding the new context to this dictionary would exceed the limits of + // If adding the new context to this dictionary would exceed the limits of // a Unity mesh, continue searching. continue; } @@ -349,7 +349,7 @@ private static bool HasMixedFaces(MMesh mmesh) } /// - /// Adds the locations of the vertices of a given face to a given list, applying a 'wiggle' to them to + /// Adds the locations of the vertices of a given face to a given list, applying a 'wiggle' to them to /// avoid z-fighting. /// /// The mesh containing the face @@ -463,12 +463,30 @@ public static void GameObjectFromMMeshesForMenu(WorldSpace worldSpace, Listid for new mesh /// the meshes to construct the mmesh from /// + + + + + + + + + + + + + + + + public static MMesh MMeshFromMeshes(int id, Dictionary> materialsAndMeshes) { Dictionary verticesById = new Dictionary(); Dictionary facesById = new Dictionary(); + int vIdx = 0; int faceIdx = 0; + foreach (KeyValuePair> pair in materialsAndMeshes) { Material material = pair.Key; @@ -499,9 +517,35 @@ public static MMesh MMeshFromMeshes(int id, Dictionary