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;