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;