diff --git a/Plugins/API/Components/Runtime/CSGModel.cs b/Plugins/API/Components/Runtime/CSGModel.cs
index aebc135..6338c5e 100644
--- a/Plugins/API/Components/Runtime/CSGModel.cs
+++ b/Plugins/API/Components/Runtime/CSGModel.cs
@@ -20,6 +20,7 @@ public enum ModelSettingsFlags
 		StitchLightmapSeams		= 4096,
 		IgnoreNormals			= 8192,
 		TwoSidedShadows			= 16384,
+		AutoStitchCracks        = 32768
 	}
 
 	[Serializable]
@@ -61,6 +62,7 @@ public sealed class CSGModel : CSGNode
 		public bool	NeedAutoUpdateRigidBody	{ get { return (Settings & ModelSettingsFlags.AutoUpdateRigidBody) == (ModelSettingsFlags)0; } }
 		public bool	PreserveUVs         	{ get { return (Settings & ModelSettingsFlags.PreserveUVs) != (ModelSettingsFlags)0; } }
 		public bool StitchLightmapSeams		{ get { return (Settings & ModelSettingsFlags.StitchLightmapSeams) != (ModelSettingsFlags)0; } }		
+		public bool	AutoStitchCracks       	{ get { return (Settings & ModelSettingsFlags.AutoStitchCracks) != (ModelSettingsFlags)0; } }		
 		public bool	AutoRebuildUVs         	{ get { return (Settings & ModelSettingsFlags.AutoRebuildUVs) != (ModelSettingsFlags)0; } }
 		public bool	IgnoreNormals  			{ get { return (Settings & ModelSettingsFlags.IgnoreNormals) != (ModelSettingsFlags)0; } }
 
diff --git a/Plugins/Editor/Scripts/Control/Helpers/CracksStitching.cs b/Plugins/Editor/Scripts/Control/Helpers/CracksStitching.cs
new file mode 100644
index 0000000..3951558
--- /dev/null
+++ b/Plugins/Editor/Scripts/Control/Helpers/CracksStitching.cs
@@ -0,0 +1,541 @@
+// I know what I'm doing when comparing floats.
+// ReSharper disable CompareOfFloatsByEqualityOperator
+
+// Debug purposes, provides ability to step through the procedure, to inspect and visualize data between each steps
+//#define YIELD_SUBSTEPS
+
+namespace RealtimeCSG
+{
+    using System;
+    using System.Collections;
+    using System.Collections.Generic;
+    using System.Linq;
+    using System.Threading;
+    using UnityEditor;
+    using UnityEngine;
+    using static System.Math;
+    using Vector3 = UnityEngine.Vector3;
+
+    public static class CracksStitching
+    {
+        /// 
+        /// Stitch cracks and small holes in meshes, this can take a significant amount of time depending on the mesh.
+        /// 
+        ///  The mesh 
+        ///  An object which will receive debug information 
+        /// The maximum distance to cover when stitching cracks, larger than this will not be stitched
+        public static void Solve(Mesh mesh, ISolverDebugProvider debug = null, float maxDist = 0.05f)
+        {
+            var subMeshes = new List[mesh.subMeshCount];
+            for (int i = 0; i < mesh.subMeshCount; i++)
+                subMeshes[i] = mesh.GetTriangles(i).ToList();
+
+            foreach (var o in SolveRaw(mesh.vertices, mesh.triangles, subMeshes, debug, maxDist)){ }
+
+            var totalIndices = 0;
+            foreach (var list in subMeshes)
+                totalIndices += list.Count;
+            
+            if(totalIndices > ushort.MaxValue)
+                mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
+
+            for (int i = 0; i < subMeshes.Length; i++)
+                mesh.SetTriangles(subMeshes[i], i);
+        }
+
+        /// 
+        /// Stitch cracks and small holes in meshes, this can take a significant amount of time depending on the mesh.
+        /// 
+        /// All vertices of the mesh
+        /// All geometry indices of the mesh
+        /// Submeshes geometry indices
+        ///  An object which will receive debug information 
+        ///  The maximum distance to cover when stitching cracks, larger than this will not be stitched 
+        ///  Optional cancellation token 
+        ///  Yield while solving if the preprocessor has been enabled, otherwise returns empty 
+        public static IEnumerable SolveRaw(Vector3[] vertices, int[] tris, List[] subMeshes, ISolverDebugProvider debug, float maxDist = 0.05f, CancellationToken pCancellationToken = default)
+        {
+            // Merging duplicate vertices to ignore material-specific topology
+            Merge(vertices, out var newVertices, ref tris);
+            
+            pCancellationToken.ThrowIfCancellationRequested();
+            
+            var nativeVertices = vertices;
+            vertices = newVertices;
+
+            var allEdges = new HashSet();
+            var sharedEdges = new HashSet();
+            
+            // We're thinking of cracks as edges that do not have multiple triangles 
+            for (int i = 0; i < tris.Length; i+=3)
+            {
+                int x = tris[i], y = tris[i + 1], z = tris[i + 2];
+                
+                var edgeA = new EdgeId(x, y);
+                var edgeB = new EdgeId(y, z);
+                var edgeC = new EdgeId(z, x);
+                
+                if (allEdges.Add(edgeA) == false)
+                    sharedEdges.Add(edgeA);
+                if(allEdges.Add(edgeB) == false)
+                    sharedEdges.Add(edgeB);
+                if(allEdges.Add(edgeC) == false)
+                    sharedEdges.Add(edgeC);
+            }
+            
+            pCancellationToken.ThrowIfCancellationRequested();
+            
+            // Only keep edges which do not share multiple triangles
+            var leftToSolve = new HashSet(allEdges);
+            foreach (var edge in sharedEdges)
+                leftToSolve.Remove(edge);
+
+            var trianglesToAdd = new List<(int a, int b, int c)>();
+            var workingData = new WorkingData(trianglesToAdd, allEdges, leftToSolve, vertices);
+            
+            debug?.HookIntoWorkingData(workingData);
+
+            while (leftToSolve.Count > 0)
+            {
+                pCancellationToken.ThrowIfCancellationRequested();
+                
+                // Take one random edge from the hashset
+                EdgeId thisEdge;
+                using (var e = leftToSolve.GetEnumerator())
+                {
+                    e.MoveNext();
+                    thisEdge = e.Current;
+                }
+
+                if (ReturnBestMatchFor(ref thisEdge, ref workingData, out var bestMatch) == false || bestMatch.dist > maxDist)
+                {
+                    if(bestMatch.dist <= maxDist)
+                        throw new InvalidOperationException($"Stray edge ({thisEdge}) could not be solved for");
+                    
+                    debug?.LogWarning($"For edge {thisEdge}, closest match ({bestMatch.edge}) did not satisfy {nameof(maxDist)} constraint {bestMatch.dist}/{maxDist}");
+                    leftToSolve.Remove(thisEdge);
+                    continue;
+                }
+
+                // Found a best match for thisEdge but let's check that they are both best matches for each other
+                int swapCount = 0;
+                do
+                {
+                    pCancellationToken.ThrowIfCancellationRequested();
+                    
+                    ReturnBestMatchFor(ref bestMatch.edge, ref workingData, out var otherBestMatch);
+                    if (otherBestMatch.dist > maxDist)
+                    {
+                        debug?.LogWarning($"For edge {bestMatch.edge}, closest match ({otherBestMatch.edge}) did not satisfy {nameof(maxDist)} constraint {otherBestMatch.dist}/{maxDist}");
+                        leftToSolve.Remove(bestMatch.edge);
+                        break;
+                    }
+
+                    if (otherBestMatch.edge == thisEdge || swapCount > 10)
+                    {
+                        if (swapCount > 10)
+                        {
+                            // swapCount prevents very unlikely infinite loop with weird edge topology in
+                            // cases where X's best match is Y but Y's is Z which itself is X
+                            debug?.LogWarning($"Sub par match for {thisEdge} -> {otherBestMatch.edge}");
+                        }
+
+                        // Both edges are each other's best match
+                        leftToSolve.Remove(thisEdge);
+                        leftToSolve.Remove(bestMatch.edge);
+                        
+                        int index = trianglesToAdd.Count;
+                        CreateTriangles(ref workingData, ref thisEdge, ref bestMatch.edge);
+                        debug?.Log($"{thisEdge} -> {bestMatch.edge}: {trianglesToAdd[index]} {(trianglesToAdd.Count - index > 1 ? trianglesToAdd[index+1].ToString() : "")}");
+                        #if YIELD_SUBSTEPS
+                        yield return null;
+                        #endif
+                        break;
+                    }
+                    else
+                    {
+                        // They don't match, try to see if this new best match matches each other instead
+                        thisEdge = bestMatch.edge;
+                        bestMatch = otherBestMatch;
+                        swapCount++;
+                        #if YIELD_SUBSTEPS
+                        yield return null;
+                        #endif
+                        continue;
+                    }
+                } while (true);
+            }
+
+            var posToSubMesh = new Dictionary();
+            for (int subMeshIndex = 0; subMeshIndex < subMeshes.Length; subMeshIndex++)
+            {
+                var subMesh = subMeshes[subMeshIndex];
+                for (int i = 0; i < subMesh.Count; i++)
+                {
+                    var subMeshVertIndex = subMesh[i];
+                    var pos = nativeVertices[subMeshVertIndex];
+                    // Multiple assignment on the same key would matter
+                    // only for large holes next to multiple materials,
+                    // we do not expect cracks to be large enough to warrant solving for this.
+                    posToSubMesh[pos] = (subMeshIndex, subMeshVertIndex);
+                }
+            }
+            
+            pCancellationToken.ThrowIfCancellationRequested();
+
+            foreach (var (x, y, z) in trianglesToAdd)
+            {
+                var (subMeshIndex, mappingA) = posToSubMesh[vertices[x]];
+                var (           _, mappingB) = posToSubMesh[vertices[y]];
+                var (           _, mappingC) = posToSubMesh[vertices[z]];
+                
+                // Effectively randomly picking subMesh, i.e.: material, those triangles will be assigned to, see comment above
+                var indices = subMeshes[subMeshIndex];
+                
+                // Not sure yet how to properly solve winding order, add both sides for now
+                indices.Add(mappingA);
+                indices.Add(mappingB);
+                indices.Add(mappingC);
+                indices.Add(mappingC);
+                indices.Add(mappingB);
+                indices.Add(mappingA);
+            }
+            
+            pCancellationToken.ThrowIfCancellationRequested();
+            
+            #if !YIELD_SUBSTEPS
+                return Array.Empty();
+            #endif
+        }
+
+        ///  Duplicate positions are stripped and indices are remapped appropriately 
+        static void Merge(Vector3[] positions, out Vector3[] outPos, ref int[] indices)
+        {
+            var newPos = new List();
+            var posToIndex = new Dictionary();
+            for (int i = 0; i < indices.Length; i++)
+            {
+                var oldIndex = indices[i];
+                var pos = positions[oldIndex];
+                if (posToIndex.TryGetValue(pos, out var newIndex) == false)
+                {
+                    newIndex = newPos.Count;
+                    newPos.Add(pos);
+                    posToIndex.Add(pos, newIndex);
+                }
+
+                indices[i] = newIndex;
+            }
+
+            outPos = newPos.ToArray();
+        }
+
+        ///  Create bridge between the given edges, append those new triangles and edges to working data 
+        static void CreateTriangles(ref WorkingData data, ref EdgeId edgeAB, ref EdgeId edgeXY)
+        {
+            (int a, int b) = edgeAB;
+            (int x, int y) = edgeXY;
+        
+            (int a, int b, int x, int dupe)? isTri = null;
+            // Do those two edge share a vertex
+            if (x == a || x == b)
+                isTri = (a, b, y, x);
+            else if (y == a || y == b)
+                isTri = (a, b, x, y);
+            
+            if (isTri.HasValue)
+            {
+                var tri = isTri.Value;
+                data.tris.Add((tri.a, tri.b, tri.x));
+                
+                var newEdge = tri.dupe == a ? new EdgeId(b, tri.x) : new EdgeId(a, tri.x);
+                
+                // Created a triangle from two existing edges, the third one formed by them must now be added to the pool to be solved for
+                if (data.allEdges.Add(newEdge))
+                    data.edgesLeft.Add(newEdge);
+                else
+                    data.edgesLeft.Remove(newEdge);
+            }
+            else
+            {
+                var vertices = data.vertices;
+                var pivot = Vector3.Dot((vertices[b] - vertices[a]).normalized, (vertices[y] - vertices[x]).normalized) >= 0 ? b : a;
+                
+                data.tris.Add((a, b, x));
+                data.tris.Add((x, pivot, y));
+
+                EdgeId newEdge0, newEdge1;
+                if (pivot == b)
+                {
+                    newEdge0 = new EdgeId(a, x);
+                    newEdge1 = new EdgeId(b, y);
+                }
+                else
+                {
+                    newEdge0 = new EdgeId(a, y);
+                    newEdge1 = new EdgeId(b, x);
+                }
+                
+                // Created a quad to bridge those two edges, two new edges were formed through this process and
+                // must now be added to the pool to be solved for.
+                if (data.allEdges.Add(newEdge0))
+                    data.edgesLeft.Add(newEdge0);
+                else            
+                    data.edgesLeft.Remove(newEdge0);
+                if (data.allEdges.Add(newEdge1))
+                    data.edgesLeft.Add(newEdge1);
+                else
+                    data.edgesLeft.Remove(newEdge1);
+            }
+        }
+        
+        static bool ReturnBestMatchFor(ref EdgeId edge, ref WorkingData data, out (float dist, EdgeId edge, Segment seg) output)
+        {
+            var vertices = data.vertices;
+            var edgesLeft = data.edgesLeft;
+            
+            // Prevents testing edge against itself on every iteration of the loop, will be added back lower
+            edgesLeft.Remove(edge);
+            
+            var seg = new Segment(vertices[edge.a], vertices[edge.b]);
+            (float dist, EdgeId edge, Segment seg) closest = (float.PositiveInfinity, default, default);
+            foreach (var otherEdge in edgesLeft)
+            {
+                var otherSeg = new Segment(vertices[otherEdge.a], vertices[otherEdge.b]);
+                ComputeScoreFor(ref seg, ref otherSeg, out var dist);
+                if (dist < closest.dist)
+                    closest = (dist, otherEdge, otherSeg);
+            }
+
+            edgesLeft.Add(edge);
+
+            output = closest;
+            return closest.dist != float.PositiveInfinity;
+        }
+
+        static void ComputeScoreFor(ref Segment segX, ref Segment segY, out float score)
+        {
+            if (segX.lengthSqr == 0f)
+            {
+                // segX is a point
+                if (segY.lengthSqr == 0f)
+                {
+                    // Both segments are points
+                    score = (segX.a - segY.a).sqrMagnitude;
+                }
+                else
+                {
+                    // segX is a point and segY a segment
+                    var aOnSegB = Vector3.Dot(segX.a - segY.a, segY.delta) / segY.lengthSqr;
+                    score = (segX.a - (segY.a + segY.delta * aOnSegB)).sqrMagnitude;
+                }
+                return;
+            }
+            else if (segY.lengthSqr == 0f)
+            {
+                // Swap segments and let recursion handle this
+                ComputeScoreFor(ref segY, ref segX, out score);
+                return;
+            }
+
+            // From here on out, both segments are guaranteed to have a length above zero
+
+            if (segX.lengthSqr > segY.lengthSqr)
+                ComputeScoreInner(ref segX, ref segY, out score);
+            else
+                ComputeScoreInner(ref segY, ref segX, out score);
+        }
+        
+        ///  segX must be longer than segY, swap them if they aren't ! 
+        static void ComputeScoreInner(ref Segment segX, ref Segment segY, out float score)
+        {
+            // this method operates knowing that segY is smaller than segX
+
+            // Find closest point on segmentX from both edges of segmentY
+            // ... now computing factor along segmentX
+            var fA = Vector3.Dot(segY.a - segX.a, segX.delta) / segX.lengthSqr;
+            var fB = Vector3.Dot(segY.b - segX.a, segX.delta) / segX.lengthSqr;
+            // factor may be outside [0,1], meaning that the closest point is outside of the segment along its line
+            var fCA = Mathf.Clamp01(fA);
+            var fCB = Mathf.Clamp01(fB);
+            if (fCA == fA || fCB == fB)
+            {
+                // At least one of the closest pos is on segmentX
+                // Project them both back onto segmentY and find the differences to derive a score
+                // hinting to how skewed the resulting quads/tris bridging those segments would be.
+                // This came mostly through intuition, even if this is flawed, the score is
+                // not nearly as important as validating that both segments are each other's best match.
+                
+                var aOnX = segX.a + segX.delta * fA;
+                var aBack = segY.a + segY.delta * Mathf.Clamp01(Vector3.Dot(aOnX - segY.a, segY.delta) / segY.lengthSqr);
+                var bOnX = segX.a + segX.delta * fB;
+                var bBack = segY.a + segY.delta * Mathf.Clamp01(Vector3.Dot(bOnX - segY.a, segY.delta) / segY.lengthSqr);
+                
+                // Projection to projection distance is rated lower than projection back to vertex
+                // this way edges slightly further away but parallel are preferred over those perpendicular to each other
+                score = ((aOnX - aBack).sqrMagnitude + (bOnX - bBack).sqrMagnitude) * 0.5f
+                        + (segY.a - aBack).sqrMagnitude + (segY.b - bBack).sqrMagnitude;
+                score *= 0.5f;
+            }
+            else
+            {
+                // The segments do not share a plane in common, return distance from edges
+                score = (segX.a - segY.a).sqrMagnitude +
+                        (segX.b - segY.b).sqrMagnitude;
+                score *= 0.5f;
+            }
+        }
+
+        ///  Provides hooks into the stitching procedure for debug purposes 
+        public interface ISolverDebugProvider
+        {
+            /// 
+            /// Provides a way to hook into the data the solver is working with,
+            /// to read while the solver yields for example.
+            /// Writing to those collections will lead to undefined behaviors.
+            /// 
+            void HookIntoWorkingData(WorkingData data);
+            void LogWarning(string str);
+            void Log(string str);
+        }
+
+        ///  Deterministic identity for an edge 
+        public readonly struct EdgeId : IEquatable
+        {
+            /// 
+            ///  is guaranteed to be smaller than ,
+            /// they are indices to the vertex position buffer.
+            /// 
+            public readonly int a, b;
+
+            public EdgeId(int x, int y)
+            {
+                a = Min(x, y);
+                b = Max(x, y);
+            }
+
+            public void Deconstruct(out int oA, out int oB)
+            {
+                oA = a;
+                oB = b;
+            }
+
+            public static bool operator ==(EdgeId x, EdgeId y) => x.a == y.a && x.b == y.b;
+            public static bool operator !=(EdgeId x, EdgeId y) => x.a != y.a || x.b != y.b;
+
+            public bool Equals(EdgeId other) => a == other.a && b == other.b;
+            public override bool Equals(object obj) => obj is EdgeId other && Equals(other);
+            public override int GetHashCode() => (a, b).GetHashCode();
+            public override string ToString() => (a, b).ToString();
+        }
+
+        public readonly struct WorkingData
+        {
+            public readonly List<(int, int, int)> tris;
+            public readonly HashSet allEdges;
+            public readonly HashSet edgesLeft;
+            public readonly Vector3[] vertices;
+
+            public WorkingData(
+                List<(int, int, int)> pTris, 
+                HashSet pAllEdges, 
+                HashSet pEdgesLeft, 
+                Vector3[] pVertices)
+            {
+                tris = pTris;
+                allEdges = pAllEdges;
+                edgesLeft = pEdgesLeft;
+                vertices = pVertices;
+            }
+        }
+
+        readonly struct Segment
+        {
+            public readonly Vector3 a, b, delta;
+            public readonly float lengthSqr;
+
+            public Segment(Vector3 pA, Vector3 pB)
+            {
+                a = pA;
+                b = pB;
+                delta = pB - pA;
+                lengthSqr = delta.sqrMagnitude;
+            }
+        }
+
+        public static void SolveAsync(Mesh[] pMesh, ISolverDebugProvider debug, CancellationToken cancellationToken, Action onFinished, float maxDist = 0.05f)
+        {
+            Mesh combinedMesh = new Mesh();
+
+            var combineInstances = new List();
+            foreach (var mesh1 in pMesh)
+                for (int i = 0; i < mesh1.subMeshCount; i++)
+                    combineInstances.Add(new CombineInstance{ mesh = mesh1, subMeshIndex = i });
+            
+            combinedMesh.CombineMeshes(combineInstances.ToArray(), false, false);
+
+            var verts = combinedMesh.vertices;
+            var indices = combinedMesh.triangles;
+            
+            var subMeshes = new List[combinedMesh.subMeshCount];
+            for (int i = 0; i < combinedMesh.subMeshCount; i++)
+                subMeshes[i] = combinedMesh.GetTriangles(i).ToList();
+            
+            System.Threading.Tasks.Task.Run(() =>
+            {
+                try
+                {
+                    foreach (var o in SolveRaw(verts, indices, subMeshes, debug, maxDist)){ }
+
+                    cancellationToken.ThrowIfCancellationRequested();
+                        
+                    var totalIndices = 0;
+                    foreach (var list in subMeshes)
+                        totalIndices += list.Count;
+                    
+                    // Mesh is not thread safe, we can run the process asynchronously as long as we don't directly interact with mesh.
+                    //   to that end we're relying on the editor update callback to apply those changes back, but inline delegates cannot remove
+                    //   themselves from a callback -> using a class to hold the delegate reference which removes itself after the call to work around this.
+                    var jobWorkAround = new AsyncJobWorkaround();
+                    EditorApplication.update += jobWorkAround.Post = () =>
+                    {
+                        EditorApplication.update -= jobWorkAround.Post;
+                        if(cancellationToken.IsCancellationRequested)
+                            return;
+            
+                        if(totalIndices > ushort.MaxValue)
+                            combinedMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
+
+                        for (int i = 0; i < subMeshes.Length; i++)
+                            combinedMesh.SetTriangles(subMeshes[i], i);
+
+                        // Redistribute data to the right meshes
+                        int submeshIndex = 0;
+                        foreach (var mesh1 in pMesh)
+                        {
+                            var ci = new CombineInstance[mesh1.subMeshCount];
+                            mesh1.Clear(); 
+                            for (int i = 0; i < ci.Length; i++)
+                                ci[i] = new CombineInstance { mesh = combinedMesh, subMeshIndex = submeshIndex++ };
+                            mesh1.CombineMeshes(ci, false, false);
+                            // CombineMeshes dumps all vertex data from all referenced meshes into the resulting mesh
+                            // even if most of the vertex data ends up unused because those vertices' are not referenced in the index/triangle array
+                            mesh1.OptimizeReorderVertexBuffer();
+                        }
+
+                        onFinished?.Invoke();
+                    };
+                }
+                catch (Exception e) when(e is OperationCanceledException == false)
+                {
+                    Debug.LogException(e);
+                }
+            }, cancellationToken);
+        }
+
+        class AsyncJobWorkaround
+        {
+            public EditorApplication.CallbackFunction Post;
+        }
+    }
+}
\ No newline at end of file
diff --git a/Plugins/Editor/Scripts/Control/Helpers/CracksStitching.cs.meta b/Plugins/Editor/Scripts/Control/Helpers/CracksStitching.cs.meta
new file mode 100644
index 0000000..bbbfe37
--- /dev/null
+++ b/Plugins/Editor/Scripts/Control/Helpers/CracksStitching.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c055abffbb9e5ac4faa3ff600910c0b0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Plugins/Editor/Scripts/Control/Managers/MeshInstanceManager.cs b/Plugins/Editor/Scripts/Control/Managers/MeshInstanceManager.cs
index b5de7ac..e29148d 100644
--- a/Plugins/Editor/Scripts/Control/Managers/MeshInstanceManager.cs
+++ b/Plugins/Editor/Scripts/Control/Managers/MeshInstanceManager.cs
@@ -1,6 +1,7 @@
 //#define SHOW_GENERATED_MESHES
 using System.Linq;
 using System.Collections.Generic;
+using System.Threading;
 using UnityEngine;
 using UnityEngine.SceneManagement;
 using UnityEditor;
@@ -894,8 +895,7 @@ private static void GenerateLightmapUVsForInstance(GeneratedMeshInstance instanc
 			meshRendererComponent.realtimeLightmapIndex = -1;
 			meshRendererComponent.lightmapIndex = -1;
 			
-			var oldVertices		= instance.SharedMesh.vertices;
-			if (oldVertices.Length == 0)
+			if (instance.SharedMesh.vertexCount == 0)
 				return;
 
             var tempMesh = instance.SharedMesh.Clone();
@@ -926,6 +926,167 @@ private static bool NeedToGenerateLightmapUVsForInstance(GeneratedMeshInstance i
 			return !instance.HasUV2 && instance.RenderSurfaceType == RenderSurfaceType.Normal;
 		}
 
+		public static bool NeedToStitchCracksForModel(CSGModel model)
+		{
+			if (!ModelTraits.IsModelEditable(model))
+				return false;
+
+			if (!model.generatedMeshes)
+				return false;
+
+			var container = model.generatedMeshes;
+			if (!container || container.owner != model)
+				return false;
+
+			foreach (var instance in container.MeshInstances)
+			{
+				if (!instance)
+					continue;
+
+				if (NeedToStitchCracksForInstance(instance))
+					return true;
+			}
+			return false;
+		}
+
+		public static void StitchCracksForModel(CSGModel model)
+		{
+			if (!ModelTraits.IsModelEditable(model))
+				return;
+
+			if (!model.generatedMeshes)
+				return;
+
+			var container = model.generatedMeshes;
+			if (!container || !container.owner)
+				return;
+
+			if (!container.HasMeshInstances)
+				return;
+
+			foreach (var instance in container.MeshInstances)
+			{
+				if (!instance)
+					continue;
+				if (!instance.SharedMesh)
+					instance.FindMissingSharedMesh();
+			}
+
+			foreach (var grouping in from x in container.MeshInstances 
+			         where x.SharedMesh != null && x.SharedMesh.vertexCount > 0 
+			         group x by x.RenderSurfaceType == RenderSurfaceType.Collider)
+			{
+				var meshes = new List();
+				foreach (var instance in grouping)
+				{
+					instance.SharedMesh = instance.SharedMesh.Clone();
+					meshes.Add(instance.SharedMesh);
+					instance.CracksSolverCancellation?.Invoke();
+				}
+				
+				if (meshes.Count == 0)
+					continue;
+
+				var tokenSource = new CancellationTokenSource();
+				string key = $"Stitching {container.name}'s {(grouping.Key ? "Colliders" : "Meshes")}";
+				
+				#if UNITY_2020_OR_NEWER
+				int progressId = Progress.Start(key);
+				var progressLogger = new CracksProgressLogger(progressId);
+				EditorApplication.update += progressLogger.Update;
+				foreach (var instance in grouping)
+				{
+					instance.CracksSolverCancellation += () =>
+					{
+						tokenSource.Cancel();
+						Progress.Finish(progressId);
+						instance.CracksSolverCancellation = null;
+						EditorApplication.update -= progressLogger.Update;
+					};
+				}
+				#else
+				foreach (var instance in grouping)
+				{
+					instance.CracksSolverCancellation += () =>
+					{
+						tokenSource.Cancel();
+						instance.CracksSolverCancellation = null;
+					};
+				}
+				CracksStitching.ISolverDebugProvider progressLogger = null;
+				#endif
+				
+				CracksStitching.SolveAsync(meshes.ToArray(), progressLogger, tokenSource.Token,
+					() =>
+					{
+						foreach (var instance in grouping)
+						{
+							EditorSceneManager.MarkSceneDirty(instance.gameObject.scene);
+							instance.CracksSolverCancellation?.Invoke();
+						}
+						#if !UNITY_2020_OR_NEWER
+						Debug.Log($"Finished {key}");
+						#endif
+					});
+
+				foreach (var instance in grouping)
+				{
+					instance.CracksHashValue = instance.MeshDescription.geometryHashValue;
+					instance.HasNoCracks = true;
+				}
+			}
+		}
+
+		#if UNITY_2020_OR_NEWER
+		private class CracksProgressLogger : CracksStitching.ISolverDebugProvider
+		{
+			CracksStitching.WorkingData data;
+			int progressId;
+
+			public CracksProgressLogger(int pProgressId)
+			{
+				progressId = pProgressId;
+			}
+
+			public void Update()
+			{
+				if (data.edgesLeft == null)
+				{
+					Progress.Report(progressId, 0f );
+				}
+				else
+				{
+					// DATA IS NOT THREAD SAFE, BE VERY CAREFUL WITH HOW YOU READ STUFF FROM IT
+					Progress.Report(progressId, 1.0f - ((float)data.edgesLeft.Count / data.allEdges.Count) );
+				}
+			}
+
+			public void HookIntoWorkingData(CracksStitching.WorkingData pData) => data = pData;
+			public void LogWarning(string str){ }
+			public void Log(string str){ }
+		}
+		#endif
+
+		///  Thin helper to debug issues related to crack stitching 
+		private class CracksDebugger : CracksStitching.ISolverDebugProvider
+		{
+			public void HookIntoWorkingData(CracksStitching.WorkingData data){ }
+			public void LogWarning(string str) => Debug.LogWarning(str);
+			public void Log(string str){ }
+		}
+
+		private static bool NeedToStitchCracksForInstance(GeneratedMeshInstance instance)
+		{
+			return !instance.HasNoCracks;
+		}
+
+		public static void ClearCrackStitching(GeneratedMeshInstance instance)
+		{
+			instance.CracksHashValue = 0;
+			instance.HasNoCracks = false;
+			instance.CracksSolverCancellation?.Invoke();
+		}
+
 		private static bool AreBoundsEmpty(GeneratedMeshInstance instance)
 		{
 			return
@@ -1203,6 +1364,22 @@ public static void Refresh(GeneratedMeshInstance instance, CSGModel owner, bool
 						}
 					}
 				}
+                
+                if (instance.HasNoCracks && instance.CracksHashValue != instance.MeshDescription.geometryHashValue && meshRendererComponent)
+                {
+	                instance.ResetStitchCracksTime = Time.realtimeSinceStartup;
+	                if(instance.HasNoCracks)
+		                ClearCrackStitching(instance);
+                }
+
+                if (owner.AutoStitchCracks || postProcessScene)
+                {
+	                if ((float.IsPositiveInfinity(instance.ResetStitchCracksTime) || Time.realtimeSinceStartup - instance.ResetStitchCracksTime > 2.0f) &&
+	                    NeedToStitchCracksForModel(owner))
+	                {
+		                StitchCracksForModel(owner);
+	                }
+                }
 
                 if (!postProcessScene &&
                     meshFilterComponent.sharedMesh != instance.SharedMesh)
@@ -1345,6 +1522,7 @@ public static void Refresh(GeneratedMeshInstance instance, CSGModel owner, bool
                     meshRendererComponent.hideFlags = HideFlags.None; UnityEngine.Object.DestroyImmediate(meshRendererComponent); instance.Dirty = true;
                 }
 				instance.LightingHashValue = instance.MeshDescription.geometryHashValue;
+				instance.CracksHashValue = instance.MeshDescription.geometryHashValue;
 				meshFilterComponent = null;
 				meshRendererComponent = null;
 				instance.CachedMeshRendererSO = null;
diff --git a/Plugins/Editor/Scripts/View/GUI/ComponentEditorWindows/CSGModelComponent.Inspector.GUI.cs b/Plugins/Editor/Scripts/View/GUI/ComponentEditorWindows/CSGModelComponent.Inspector.GUI.cs
index b346168..c818d22 100644
--- a/Plugins/Editor/Scripts/View/GUI/ComponentEditorWindows/CSGModelComponent.Inspector.GUI.cs
+++ b/Plugins/Editor/Scripts/View/GUI/ComponentEditorWindows/CSGModelComponent.Inspector.GUI.cs
@@ -212,6 +212,7 @@ public static void OnInspectorGUI(UnityEngine.Object[] targets)
             bool?	DoNotRender				= (Settings & ModelSettingsFlags.DoNotRender) == ModelSettingsFlags.DoNotRender;
             bool?	TwoSidedShadows			= (Settings & ModelSettingsFlags.TwoSidedShadows) == ModelSettingsFlags.TwoSidedShadows;
 //			bool?	ReceiveShadows			= !((settings & ModelSettingsFlags.DoNotReceiveShadows) == ModelSettingsFlags.DoNotReceiveShadows);
+            bool?	AutoStitchCracks        = (Settings & ModelSettingsFlags.AutoStitchCracks) == ModelSettingsFlags.AutoStitchCracks;
             bool?	AutoRebuildUVs          = (Settings & ModelSettingsFlags.AutoRebuildUVs) == ModelSettingsFlags.AutoRebuildUVs;
             bool?	PreserveUVs             = (Settings & ModelSettingsFlags.PreserveUVs) == ModelSettingsFlags.PreserveUVs;
             bool?	StitchLightmapSeams     = (Settings & ModelSettingsFlags.StitchLightmapSeams) == ModelSettingsFlags.StitchLightmapSeams;
@@ -266,6 +267,7 @@ public static void OnInspectorGUI(UnityEngine.Object[] targets)
                 bool	currDoNotRender				= (Settings & ModelSettingsFlags.DoNotRender) == ModelSettingsFlags.DoNotRender;
                 bool	currTwoSidedShadows			= (Settings & ModelSettingsFlags.TwoSidedShadows) == ModelSettingsFlags.TwoSidedShadows;
 //				bool	currReceiveShadows			= !((settings & ModelSettingsFlags.DoNotReceiveShadows) == ModelSettingsFlags.DoNotReceiveShadows);
+                bool	currAutoStitchCracks		= (Settings & ModelSettingsFlags.AutoStitchCracks) == ModelSettingsFlags.AutoStitchCracks;
                 bool	currAutoRebuildUVs			= (Settings & ModelSettingsFlags.AutoRebuildUVs) == ModelSettingsFlags.AutoRebuildUVs;
                 bool	currPreserveUVs				= (Settings & ModelSettingsFlags.PreserveUVs) == ModelSettingsFlags.PreserveUVs;
                 bool	currStitchLightmapSeams		= (Settings & ModelSettingsFlags.StitchLightmapSeams) == ModelSettingsFlags.StitchLightmapSeams;
@@ -303,6 +305,7 @@ public static void OnInspectorGUI(UnityEngine.Object[] targets)
                 if (TwoSidedShadows			.HasValue && TwoSidedShadows		.Value != currTwoSidedShadows		) TwoSidedShadows = null;
 //				if (ReceiveShadows			.HasValue && ReceiveShadows			.Value != currReceiveShadows		) ReceiveShadows = null;
 //				if (ShadowCastingMode		.HasValue && ShadowCastingMode		.Value != currShadowCastingMode		) ShadowCastingMode = null;
+                if (AutoStitchCracks     	.HasValue && AutoStitchCracks     	.Value != currAutoStitchCracks		) AutoStitchCracks = null;
                 if (AutoRebuildUVs     		.HasValue && AutoRebuildUVs     	.Value != currAutoRebuildUVs		) AutoRebuildUVs = null;
                 if (PreserveUVs     		.HasValue && PreserveUVs     		.Value != currPreserveUVs	    	) PreserveUVs = null;
                 if (StitchLightmapSeams		.HasValue && StitchLightmapSeams	.Value != currStitchLightmapSeams	) StitchLightmapSeams = null;
@@ -1130,6 +1133,27 @@ public static void OnInspectorGUI(UnityEngine.Object[] targets)
                             EditorApplication.RepaintHierarchyWindow();
                             EditorApplication.DirtyHierarchyWindowSorting();
                         }
+                        {
+                            var autoStitchCracks = AutoStitchCracks ?? false;
+                            EditorGUI.BeginChangeCheck();
+                            {
+                                EditorGUI.showMixedValue = !AutoStitchCracks.HasValue;
+                                autoStitchCracks = EditorGUILayout.Toggle(StitchCracksContent, autoStitchCracks);
+                            }
+                            if (EditorGUI.EndChangeCheck())
+                            {
+                                for (int i = 0; i < models.Length; i++)
+                                {
+                                    if (autoStitchCracks)
+                                        models[i].Settings |= ModelSettingsFlags.AutoStitchCracks;
+                                    else
+                                        models[i].Settings &= ~ModelSettingsFlags.AutoStitchCracks;
+                                    MeshInstanceManager.Refresh(models[i], onlyFastRefreshes: false);
+                                }
+                                GUI.changed = true;
+                                AutoStitchCracks = autoStitchCracks;
+                            }
+                        }
 
 #if UNITY_2017_3_OR_NEWER
                         GUILayout.Space(10);
diff --git a/Plugins/Editor/Scripts/View/GUI/ComponentEditorWindows/CSGModelComponent.Inspector.GUIContents.cs b/Plugins/Editor/Scripts/View/GUI/ComponentEditorWindows/CSGModelComponent.Inspector.GUIContents.cs
index f3af4c2..df5773e 100644
--- a/Plugins/Editor/Scripts/View/GUI/ComponentEditorWindows/CSGModelComponent.Inspector.GUIContents.cs
+++ b/Plugins/Editor/Scripts/View/GUI/ComponentEditorWindows/CSGModelComponent.Inspector.GUIContents.cs
@@ -67,6 +67,8 @@ internal sealed partial class CSGModelComponentInspectorGUI
 
 		private static readonly GUIContent MinimumChartSizeContent				= new GUIContent("Min Chart Size", "Specifies the minimum texel size used for a UV chart. If stitching is required, a value of 4 will create a chart of 4x4 texels to store lighting and directionality. If stitching is not required, a value of 2 will reduce the texel density and provide better lighting build times and run time performance.");
 
+		private static readonly GUIContent StitchCracksContent					= new GUIContent("Stitch Cracks", "Fill in cracks that came out through the mesh generation process, increases the amount of triangles.");
+		
 		public static int[] MinimumChartSizeValues = { 2, 4 };
 		public static GUIContent[] MinimumChartSizeStrings =
 		{
diff --git a/Plugins/Runtime/Scripts/Components/GeneratedMeshInstance.cs b/Plugins/Runtime/Scripts/Components/GeneratedMeshInstance.cs
index 6dcf068..ab62107 100644
--- a/Plugins/Runtime/Scripts/Components/GeneratedMeshInstance.cs
+++ b/Plugins/Runtime/Scripts/Components/GeneratedMeshInstance.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Runtime.InteropServices;
+using System.Threading;
 using UnityEngine;
 using UnityEngine.Serialization;
 using MeshQuery = RealtimeCSG.Foundation.MeshQuery;
@@ -130,10 +131,14 @@ public sealed class GeneratedMeshInstance : MonoBehaviour
 
 		[HideInInspector] public bool   HasGeneratedNormals = false;
 		[HideInInspector] public bool	HasUV2				= false;
-        [NonSerialized]
+		[HideInInspector] public bool	HasNoCracks			= false;
+		[NonSerialized]
 		[HideInInspector] public float	ResetUVTime			= float.PositiveInfinity;
+		[HideInInspector] public float  ResetStitchCracksTime = float.PositiveInfinity;
 		[HideInInspector] public Int64	LightingHashValue;
-
+		[HideInInspector] public Int64  CracksHashValue;
+		
+		[NonSerialized] public Action CracksSolverCancellation;
 		[NonSerialized] [HideInInspector] public bool Dirty	= true;
 		[NonSerialized] [HideInInspector] public MeshCollider	CachedMeshCollider;
 		[NonSerialized] [HideInInspector] public MeshFilter		CachedMeshFilter;
@@ -145,11 +150,14 @@ public void Reset()
 		    RenderMaterial          = null;
 		    PhysicsMaterial         = null;
 		    RenderSurfaceType       = (RenderSurfaceType)999;
-        
+		
 		    HasGeneratedNormals     = false;
 		    HasUV2				    = false;
-            ResetUVTime			    = float.PositiveInfinity;
+		    ResetUVTime			    = float.PositiveInfinity;
+		    ResetStitchCracksTime   = float.PositiveInfinity;
 		    LightingHashValue       = 0;
+		    CracksHashValue         = 0;
+		    CracksSolverCancellation?.Invoke();
 
 		    Dirty	                = true;