diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5c7247b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,7 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/TileBakeLibrary/BinaryMeshReader.cs b/TileBakeLibrary/BinaryMeshReader.cs index d4d385a..5a02a80 100644 --- a/TileBakeLibrary/BinaryMeshReader.cs +++ b/TileBakeLibrary/BinaryMeshReader.cs @@ -27,7 +27,7 @@ namespace TileBakeLibrary { - class BinaryMeshReader + public class BinaryMeshReader { public static MeshData ReadBinaryMesh(string filename) { diff --git a/TileBakeLibrary/CityJSONParsing/CityJSON.cs b/TileBakeLibrary/CityJSONParsing/CityJSON.cs index e60cfd9..470fcf2 100644 --- a/TileBakeLibrary/CityJSONParsing/CityJSON.cs +++ b/TileBakeLibrary/CityJSONParsing/CityJSON.cs @@ -43,10 +43,15 @@ public class CityJSON public Vector3Double TransformOffset { get => transformOffset; } - public CityJSON(string filepath, bool applyTransformScale = true, bool applyTransformOffset = true) + private int minHoleVertices = 3; + private float minHoleSize = 1; + + public CityJSON(string filepath, bool applyTransformScale = true, bool applyTransformOffset = true, int minHoleVertices = 3, float minHoleSize = 1) { sourceFilePath = filepath; cityJsonNode = JSON.StreamParse(filepath); + this.minHoleVertices = minHoleVertices; + this.minHoleSize = minHoleSize; if (cityJsonNode == null || cityJsonNode["CityObjects"] == null) { @@ -179,26 +184,16 @@ private CityObject ReadCityObject(JSONNode node, string filter = "") { if (geometrynode["type"] == "Solid") { - JSONNode exteriorshell = geometrynode["boundaries"][0]; - foreach (JSONNode surfacenode in exteriorshell) - { - surfaces.Add(ReadSurfaceVectors(surfacenode)); - } - JSONNode interiorshell = geometrynode[0][1]; - if (interiorshell != null) - { - foreach (JSONNode surfacenode in interiorshell) - { - surfaces.Add(ReadSurfaceVectors(surfacenode)); - } - } + JSONNode boundaries = geometrynode["boundaries"]; + surfaces = ReadSolid(geometrynode, cityObject); } - if (geometrynode["type"] == "MultiSurface") + else if (geometrynode["type"] == "MultiSurface") { - foreach (JSONNode surfacenode in geometrynode["boundaries"]) - { - surfaces.Add(ReadSurfaceVectors(surfacenode)); - } + surfaces = ReadMultiSurface(geometrynode, cityObject); + } + else + { + Console.WriteLine($"Unknown geometry type: {geometrynode["type"]}"); } //read textureValues @@ -235,7 +230,6 @@ private CityObject ReadCityObject(JSONNode node, string filter = "") } } - //read SurfaceAttributes JSONNode semanticsnode = geometrynode["semantics"]; if (semanticsnode != null) @@ -294,59 +288,21 @@ private CityObject ReadCityObject(JSONNode node, string filter = "") return cityObject; } - private List ReadSolid(JSONNode geometrynode) + private List ReadSolid(JSONNode geometrynode, CityObject sourceCityObject) { JSONNode boundariesNode = geometrynode["boundaries"]; List result = new List(); - foreach (JSONNode node in boundariesNode[0]) - { - Surface surf = new Surface(); - foreach (JSONNode vertexnode in node[0]) - { - surf.outerRing.Add(vertices[vertexnode.AsInt]); - } - result.Add(surf); - } - JSONNode semanticsnode = geometrynode["semantics"]; - JSONNode ValuesNode = semanticsnode["values"][0]; - for (int i = 0; i < ValuesNode.Count; i++) - { - result[i].SurfaceType = geometrynode["semantics"]["surfaces"][ValuesNode[i].AsInt]["type"]; - result[i].semantics = ReadSemantics(geometrynode["semantics"]["surfaces"][ValuesNode[i].AsInt]); + //Read exterior shell + foreach (JSONNode surfacenode in boundariesNode[0]) + { + result.Add(ReadSurfaceVectors(surfacenode,sourceCityObject,true,true)); } - if (geometrynode["texture"] != null) - { - Surfacetexture surfacematerial = null; - int counter = 0; - foreach (JSONNode node in geometrynode["texture"][0][0]) - { - List indices = new List(); - for (int i = 0; i < node[0][0].Count; i++) - { - JSONNode item = node[0][0][i]; - - if (surfacematerial is null) - { - surfacematerial = Textures[item.AsInt]; - result[i].surfacetexture = surfacematerial; - } - else - { - indices.Add(textureVertices[item.AsInt]); - } - - } - indices.Reverse(); - result[counter].outerringUVs = indices; - counter++; - } - } return result; } - private List ReadMultiSurface(JSONNode geometrynode) + private List ReadMultiSurface(JSONNode geometrynode, CityObject sourceCityObject) { JSONNode boundariesNode = geometrynode["boundaries"]; List result = new List(); @@ -356,7 +312,6 @@ private List ReadMultiSurface(JSONNode geometrynode) foreach (JSONNode vertexnode in node[0]) { surf.outerRing.Add(vertices[vertexnode.AsInt]); - } for (int i = 1; i < node.Count; i++) { @@ -420,16 +375,26 @@ private Surface AddSurfaceUVs(JSONNode UVValueNode, Surface surf) } return surf; } - private Surface ReadSurfaceVectors(JSONNode surfacenode) + private Surface ReadSurfaceVectors(JSONNode surfacenode, CityObject sourceCityObject, bool createOuterRing = true, bool createInnerRings = true) { Surface surf = new Surface(); //read exteriorRing List verts = new List(); - foreach (JSONNode vectornode in surfacenode[0]) + + if(createOuterRing) { - verts.Add(vertices[vectornode.AsInt]); - } - surf.outerRing = verts; + foreach (JSONNode vectornode in surfacenode[0]) + { + verts.Add(vertices[vectornode.AsInt]); + } + surf.outerRing = verts; + } + + if(!createInnerRings) + return surf; + + Vector3Double v1; + Vector3Double v2; for (int i = 1; i < surfacenode.Count; i++) { verts = new List(); @@ -437,9 +402,33 @@ private Surface ReadSurfaceVectors(JSONNode surfacenode) { verts.Add(vertices[vectornode.AsInt]); } - surf.innerRings.Add(verts); - } + //Skip certain holes if they are too small, of dont have enough vertices + if(verts.Count < minHoleVertices) + { + sourceCityObject.holeWarnings += $"- Found a hole with less than {minHoleVertices} vertices, skipping this hole.\n"; + } + else + { + //Calculate the area of the hole + double holeSize = 0; + for (int j = 0; j < verts.Count; j++) + { + v1 = verts[j]; + v2 = verts[(j + 1) % verts.Count]; + holeSize += (v1.X * v2.Y - v1.Y * v2.X); + } + holeSize = Math.Abs(holeSize) / 2; + if(holeSize < minHoleSize) + { + sourceCityObject.holeWarnings += $"- Found a hole smaller than {minHoleSize}. Hole size is {holeSize}, skipping this hole.\n"; + continue; + } + + surf.innerRings.Add(verts); + } + } + return surf; } @@ -466,6 +455,9 @@ public class CityObject public string cityObjectType; public string keyName = ""; + public string holeWarnings = ""; + public string triangulationWarnings = ""; + public CityObject() { semantics = new(); diff --git a/TileBakeLibrary/CityJSONToTileConverter.cs b/TileBakeLibrary/CityJSONToTileConverter.cs index 6f5500a..40d7fe5 100644 --- a/TileBakeLibrary/CityJSONToTileConverter.cs +++ b/TileBakeLibrary/CityJSONToTileConverter.cs @@ -34,6 +34,7 @@ namespace TileBakeLibrary { public class CityJSONToTileConverter { + private string logFileName = ""; private string sourcePath = ""; private string outputPath = ""; private string identifier = ""; @@ -58,6 +59,8 @@ public class CityJSONToTileConverter private float spikeCeiling = 0; private float spikeFloor = 0; + private int minHoleVertices = 3; + private float minHoleSize = 0.0001f; private int filecounter = 0; private int totalFiles = 0; @@ -85,6 +88,24 @@ public void SetClipSpikes(bool setFunction, float ceiling, float floor) spikeFloor = floor; } + /// + /// Sets the minimum amount of vertices a hole should have to be considered a hole + /// + /// Min vertex count of the hole loop. Defaults to 3 ( a triangle ) + public void SetMinHoleVertices(int vertices) + { + minHoleVertices = vertices; + } + + /// + /// Sets the minimum size of a hole in square meters + /// + /// Hole min size in square meters + public void SetMinHoleSize(float size) + { + minHoleSize = size; + } + /// /// Sets the square tile size /// @@ -177,6 +198,13 @@ public void SetObjectFilters(CityObjectFilter[] cityObjectFilters) /// public void Convert() { + //Create log file (overwrite) + var readableDateTime = DateTime.Now.ToString("yyyy-MM-dd_HH_mm"); + var currentPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + + logFileName = currentPath + "/bakelog" + readableDateTime + ".txt"; + File.WriteAllText(logFileName, string.Empty); + //If no specific filename or wildcard was supplied, default to .json files var filter = Path.GetFileName(sourcePath); if (filter == "") filter = "*.json"; @@ -203,7 +231,7 @@ public void Convert() totalFiles = sourceFiles.Length; if (sourceFiles.Length > 0) { - cityJson = new CityJSON(sourceFiles[0], true, true); + cityJson = new CityJSON(sourceFiles[0], true, true, minHoleVertices, minHoleSize); } for (int i = 0; i < sourceFiles.Length; i++) { @@ -215,7 +243,7 @@ public void Convert() nextJsonID = i; } Thread thread; - thread = new Thread(() => { nextCityJSON = new CityJSON(sourceFiles[nextJsonID], true, true); }); + thread = new Thread(() => { nextCityJSON = new CityJSON(sourceFiles[nextJsonID], true, true, minHoleVertices, minHoleSize); }); thread.Start(); //Start reading current CityJSON @@ -227,12 +255,14 @@ public void Convert() thread.Join(); cityJson = nextCityJSON; } + Console.WriteLine($"Log file: {currentPath}/bakelog{readableDateTime}.txt"); //Optional compressed variant if (brotliCompress) { CompressFiles(); } + Console.WriteLine($"Log file: {currentPath}/bakelog{readableDateTime}.txt"); } /// @@ -244,11 +274,24 @@ private void ReadCityJSON() watch.Start(); tiles = new List(); - var cityObjects = CityJSONParseProcess(cityJson); + //Warnings bag + var warnings = new ConcurrentBag(); + var cityObjects = CityJSONParseProcess(cityJson, warnings); allSubObjects.Clear(); allSubObjects = cityObjects; Console.WriteLine($"\n{allSubObjects.Count} CityObjects with LOD{lod} were imported"); + + //write warnings to log newlines + if(warnings.Count > 0) + File.AppendAllText(logFileName, $"Warnings for {cityJson.sourceFilePath}\n"); + foreach (var warning in warnings) + { + File.AppendAllText(logFileName, warning + "\n"); + } + File.AppendAllText(logFileName, "\n"); + + //Tile the objects PrepareTiles(); WriteTileData(); @@ -430,7 +473,7 @@ private void ParseExistingBinaryTile(Tile tile) bmd = null; } - private List CityJSONParseProcess(CityJSON cityJson) + private List CityJSONParseProcess(CityJSON cityJson, ConcurrentBag warnings) { List filteredObjects = new List(); Console.WriteLine(""); @@ -444,6 +487,7 @@ private List CityJSONParseProcess(CityJSON cityJson) int simplifying = 0; int tiling = 0; var filterObjectsBucket = new ConcurrentBag(); + var failedSubObjects = new ConcurrentBag(); int[] indices = Enumerable.Range(0, cityObjectCount).ToArray(); //Turn cityobjects (and their children) into SubObject mesh data @@ -455,9 +499,17 @@ private List CityJSONParseProcess(CityJSON cityJson) CityObject cityObject = cityJson.LoadCityObjectByIndex(i, lod); var subObject = ToSubObjectMeshData(cityObject); + + //Collect warnings for log + if(!string.IsNullOrEmpty(cityObject.holeWarnings)) + warnings.Add(cityObject.keyName + ":\n" + cityObject.holeWarnings); + + if(!string.IsNullOrEmpty(cityObject.triangulationWarnings)) + warnings.Add(cityObject.keyName + ":\n" + cityObject.holeWarnings); + cityObject = null; Interlocked.Decrement(ref parsing); - cityObject = null; + if (subObject == null) { Interlocked.Increment(ref done); @@ -654,7 +706,6 @@ private static void AppendCityObjectGeometry(CityObject cityObject, SubObject su { normallist.AddRange(defaultnormalList); } - continue; } @@ -707,7 +758,7 @@ private static void AppendCityObjectGeometry(CityObject cityObject, SubObject su continue; } //Poly2Mesh takes care of calculating normals, using a right-handed coordinate system - Poly2Mesh.CreateMeshData(poly, out surfaceVertices, out surfaceNormals, out surfaceIndices, out surfaceUvs); + Poly2Mesh.CreateMeshData(poly, out surfaceVertices, out surfaceNormals, out surfaceIndices, out surfaceUvs, cityObject); var offset = vertexlist.Count + subObject.vertices.Count; diff --git a/TileBakeLibrary/Config/ConfigFile.cs b/TileBakeLibrary/Config/ConfigFile.cs index 4cd383f..c1dde1c 100644 --- a/TileBakeLibrary/Config/ConfigFile.cs +++ b/TileBakeLibrary/Config/ConfigFile.cs @@ -32,6 +32,8 @@ public class ConfigFile //Optional settings with predefined default values: public int tileSize { get; set; } = 1000; public float mergeVerticesBelowAngle { get; set; } = 5; + public int minHoleVertices { get; set; } = 3; + public float minHoleSize { get; set; } = 0; public bool brotliCompression { get; set; } = false; public bool removeSpikes { get; set; } = false; public float removeSpikesAbove { get; set; } = 25; diff --git a/TileBakeLibrary/SubObject.cs b/TileBakeLibrary/SubObject.cs index 23bd47d..253dda8 100644 --- a/TileBakeLibrary/SubObject.cs +++ b/TileBakeLibrary/SubObject.cs @@ -50,7 +50,6 @@ public void MergeSimilarVertices() Vector2 uv = new Vector2(0,0); int oldIndex = 0; int newIndex = 0; - Dictionary vertexNormalCombinations = new Dictionary(); for (int i = 0; i < triangleIndices.Count; i++) @@ -101,6 +100,7 @@ private void CreateMesh() { mesh.AppendTriangle(triangleIndices[i], triangleIndices[i + 1], triangleIndices[i + 2]); } + if(skipTrianglesBelowArea > 0) { @@ -151,15 +151,10 @@ public void SimplifyMesh() MeshNormals.QuickCompute(mesh); MergeCoincidentEdges merg = new MergeCoincidentEdges(mesh); merg.Apply(); - //if (!mesh.CheckValidity(true, FailMode.ReturnOnly)) - //{ - // return; - //} - + // setup up the reducer Reducer reducer = new Reducer(mesh); // set reducer to preserve bounds - reducer.SetExternalConstraints(new MeshConstraints()); MeshConstraintUtil.FixAllBoundaryEdges(reducer.Constraints, mesh); @@ -235,19 +230,18 @@ public List ClipSubobject(Vector2 size) } else { - for (int y = localYmin; y < localYmax; y += (int)size.Y) - { - SubObject newSubobject = ClipMesh(columnMesh, x, y,size.Y); - if (newSubobject != null) - { + for (int y = localYmin; y < localYmax; y += (int)size.Y) + { + SubObject newSubobject = ClipMesh(columnMesh, x, y,size.Y); + if (newSubobject != null) + { - subObjects.Add(newSubobject); - } - } + subObjects.Add(newSubobject); + } + } } } - return subObjects; } diff --git a/TileBakeLibrary/Triangulation/Poly2Mesh.cs b/TileBakeLibrary/Triangulation/Poly2Mesh.cs index 030bbf5..a2c5e2b 100644 --- a/TileBakeLibrary/Triangulation/Poly2Mesh.cs +++ b/TileBakeLibrary/Triangulation/Poly2Mesh.cs @@ -18,6 +18,7 @@ // 11-10-2021 - Option to add thickness to poly was added by 3D Amsterdam // 11-10-2021 - Added namespace to guard from conflicts in Unity3D by 3D Amsterdam +using Netherlands3D.CityJSON; using Poly2Tri; using System; using System.Collections.Generic; @@ -223,7 +224,7 @@ public static List ConvertPoints(List points, Quaternion /// /// The freshly minted mesh. /// Polygon you want to triangulate. - public static void CreateMeshData(Polygon polygon, out Vector3[] vertices, out Vector3[] normals, out int[] triangles, out Vector2[] uvs, float thickness = 0) + public static void CreateMeshData(Polygon polygon, out Vector3[] vertices, out Vector3[] normals, out int[] triangles, out Vector2[] uvs, CityObject cityObject = null, float thickness = 0) { //make sure we out something in case of failure vertices = new Vector3[0]; @@ -263,7 +264,9 @@ public static void CreateMeshData(Polygon polygon, out Vector3[] vertices, out V } catch (System.Exception e) { - Console.WriteLine(e.Message); + Console.WriteLine("Failed to triangulate polygon: " + e.Message); + Console.WriteLine("With name: " + cityObject.keyName); + cityObject.triangulationWarnings += "- Failed to triangulate polygon: " + e.Message + "\n"; return; } // Now, to get back to our original positions, use our code-to-position map. We do diff --git a/TileBakeTool/Constants.cs b/TileBakeTool/Constants.cs index e6c320b..f23acd2 100644 --- a/TileBakeTool/Constants.cs +++ b/TileBakeTool/Constants.cs @@ -43,6 +43,7 @@ Seperate metadata files contain the description and geometry location of sub-obj --source --output --lod +--bin Pipeline example 1 TileBakeTool.exe --config Buildings.json diff --git a/TileBakeTool/Program.cs b/TileBakeTool/Program.cs index 560813e..276c34e 100644 --- a/TileBakeTool/Program.cs +++ b/TileBakeTool/Program.cs @@ -22,13 +22,13 @@ using System.Text.Json; using System.Threading; using TileBakeLibrary; +using TileBakeLibrary.BinaryMesh; namespace TileBakeTool { class Program { private static ConfigFile configFile; - private static string sourcePathOverride = ""; private static string outputPathOverride = ""; private static float lodOverride = 1; @@ -45,11 +45,18 @@ static void Main(string[] args) { ShowHelp(); } - //One parameter? Assume its a config file path. (Dragging file on .exe) + //One parameter? Inspect .bin or else assume its a config file else if (args.Length == 1) { waitForUserInputOnFinish = true; - ApplyConfigFileSettings(args[0]); + if (args[0].Contains(".bin")) + { + InspectBinaryMesh(args[0]); + } + else + { + ApplyConfigFileSettings(args[0]); + } } //More parameters? Parse them else @@ -129,9 +136,12 @@ private static void ApplySetting(string argument, string value) lodOverride = float.Parse(value, System.Globalization.CultureInfo.InvariantCulture); Console.WriteLine($"LOD filter: {lodOverride}"); break; - case "--peak": + case "--peek": PeakInFile(value); break; + case "--bin": + InspectBinaryMesh(value); + break; default: break; } @@ -139,9 +149,9 @@ private static void ApplySetting(string argument, string value) private static void PeakInFile(string filename) { - if(!File.Exists(filename)) + if (!File.Exists(filename)) { - Console.WriteLine(filename + " does not exist. Cant peak."); + Console.WriteLine(filename + " does not exist. Cant peek."); return; } using var stream = File.OpenRead(filename); @@ -156,11 +166,57 @@ private static void PeakInFile(string filename) Console.WriteLine("....."); } + private static void InspectBinaryMesh(string filename) + { + if (!File.Exists(filename)) + { + Console.WriteLine(filename + " does not exist. Cant inspect."); + return; + } + MeshData mesh = BinaryMeshReader.ReadBinaryMesh(filename); + + // Write results to a text file + string outputFileName = Path.ChangeExtension(filename, ".txt"); + using (StreamWriter writer = new StreamWriter(outputFileName)) + { + writer.WriteLine("Vertices: " + mesh.vertexCount); + writer.WriteLine("Normals: " + mesh.normalsCount); + writer.WriteLine("UVs: " + mesh.uvCount); + writer.WriteLine("Indices: " + mesh.indexCount); + writer.WriteLine("Submeshes: " + mesh.submeshCount); + writer.WriteLine("Vertices:"); + foreach (var vertex in mesh.vertices) + { + writer.WriteLine(vertex); + } + writer.WriteLine("Normals:"); + foreach (var normal in mesh.normals) + { + writer.WriteLine(normal); + } + writer.WriteLine("UVs:"); + foreach (var uv in mesh.uvs) + { + writer.WriteLine(uv); + } + writer.WriteLine("Indices:"); + foreach (var index in mesh.indices) + { + writer.WriteLine(index); + } + } + + //Log the log file path name + Console.WriteLine("Inspected " + filename + ". Results saved to " + outputFileName); + } + /// /// Start the converting process using the current configuration /// private static void StartConverting() { + //Print config to console + Console.WriteLine(JsonSerializer.Serialize(configFile, new JsonSerializerOptions() { WriteIndented = true })); Console.WriteLine("Starting..."); //Here we use the .dll. This way we may use this library in Unity, or an Azure C# Function @@ -169,6 +225,8 @@ private static void StartConverting() tileBaker.SetTargetPath((outputPathOverride != "") ? outputPathOverride : configFile.outputFolder); tileBaker.SetLOD((lodOverride != 1) ? lodOverride : configFile.lod); tileBaker.SetVertexMergeAngleThreshold(configFile.mergeVerticesBelowAngle); + tileBaker.SetMinHoleVertices(configFile.minHoleVertices); + tileBaker.SetMinHoleSize(configFile.minHoleSize); tileBaker.SetID(configFile.identifier, configFile.removePartOfIdentifier); tileBaker.SetReplace(configFile.replaceExistingObjects); tileBaker.SetExportUV(configFile.exportUVCoordinates); diff --git a/TileBakeTool/Properties/launchSettings.json b/TileBakeTool/Properties/launchSettings.json index 945227d..1f8e800 100644 --- a/TileBakeTool/Properties/launchSettings.json +++ b/TileBakeTool/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "TileBakeTool": { "commandName": "Project", - "commandLineArgs": "--config \"E:/brondata/pandenTUDelft/ahn4/panden Amsterdam TUDelftLod2-2.json\"" + "commandLineArgs": "--config \"C:/Users/samba/Desktop/TileBakeTool_SourceData/3DBag.json\"" } } } \ No newline at end of file