From c9d81100fa57940c8523cde149502cf2d36931df Mon Sep 17 00:00:00 2001 From: NINI Date: Sat, 10 Oct 2026 06:29:52 +0900 Subject: [PATCH] Skip exterior closed-component winding during resume --- .../PlayMode/PlaygroundResumePlayTests.cs | 65 ++++++++++++++++-- .../Bootstrap/PlaygroundResume.cs | 67 ++++++++++++++++--- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundResumePlayTests.cs b/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundResumePlayTests.cs index 422d998..2ebe470 100644 --- a/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundResumePlayTests.cs +++ b/Assets/MagnetismHeroes/Tests/PlayMode/PlaygroundResumePlayTests.cs @@ -131,6 +131,14 @@ namespace MagnetismHeroes.Tests var point=(a+b+c)/3;var normal=Vector3.Cross(b-a,c-a).normalized; samples.Add(point);samples.Add(point+normal*.002f);samples.Add(point-normal*.002f); } + // Keep the original 75 samples, then cover vertices/edges on both sides of numerical surface boundaries. + for(int index=0;index<24;index++) + { + int triangle=(index*(triangles.Length/3)/24)*3;var a=world[triangles[triangle]];var b=world[triangles[triangle+1]];var c=world[triangles[triangle+2]]; + var normal=Vector3.Cross(b-a,c-a).normalized;var edge=(a+b)*.5f; + foreach(var point in new[]{a,edge}) {samples.Add(point);samples.Add(point+normal*.000001f);samples.Add(point-normal*.000001f);} + } + foreach(var axis in new[]{Vector3.right,Vector3.up,Vector3.forward}) {samples.Add(collider.bounds.center+axis*1000);samples.Add(collider.bounds.center-axis*1000);} int insideSamples=0,outsideSamples=0; foreach(var point in samples) { @@ -148,8 +156,9 @@ namespace MagnetismHeroes.Tests for(int batch=0;batch<5;batch++) { var timer=System.Diagnostics.Stopwatch.StartNew();foreach(var point in timingPoints) ReferenceMeshDistance(world,triangles,point,out _,out _,out _,out _);timer.Stop();double referenceMilliseconds=timer.Elapsed.TotalMilliseconds; + timer.Restart();foreach(var point in timingPoints) ReferenceMeshDistance(world,triangles,point,out _,out _,out _,out _,false);timer.Stop();double nearestMilliseconds=timer.Elapsed.TotalMilliseconds; timer.Restart();foreach(var point in timingPoints) distance(point,out _,out _,out _);timer.Stop(); - Debug.Log($"[ResumeMagnitudeQueryTiming] fixture={fixture}, batch={batch}, queries={timingPoints.Length}, referenceMs={referenceMilliseconds:R}, runtimeMs={timer.Elapsed.TotalMilliseconds:R}"); + Debug.Log($"[ResumeMagnitudeQueryTiming] fixture={fixture}, batch={batch}, queries={timingPoints.Length}, referenceMs={referenceMilliseconds:R}, nearestOnlyMs={nearestMilliseconds:R}, runtimeMs={timer.Elapsed.TotalMilliseconds:R}"); } string puzzles=string.Join("|",_store.Save.puzzleStates.Select(state=>JsonUtility.ToJson(state)));var completed=(int[])_store.Save.completedStageIds.Clone();var tutorials=(string[])_store.Save.seenTutorialIds.Clone(); string expected=null; @@ -166,7 +175,55 @@ namespace MagnetismHeroes.Tests _app.ShowHome();string stable=JsonUtility.ToJson(_store.Save.latestFreePlay);_app.ContinueFreePlay();yield return null;Assert.That(JsonUtility.ToJson(_store.Save.latestFreePlay),Is.EqualTo(stable));Assert.That(_app.Instruction,Is.Not.EqualTo("친구를 놀이터로 다시 보냈어요.")); } } - private static void ReferenceMeshDistance(Vector3[] vertices, int[] triangles, Vector3 point, out Vector3 nearest, out float distance, out bool inside, out double winding) + [UnityTest] public IEnumerator ClosedComponentsAndUnsupportedTopologyMatchTheFrozenWindingAtEdgesOverlapsAndRemotePoints() + { + var primitive=GameObject.CreatePrimitive(PrimitiveType.Cube);_fixtures.Add(primitive); + var cube=primitive.GetComponent().sharedMesh;var cubeVertices=cube.vertices;var cubeTriangles=cube.triangles;primitive.SetActive(false); + var meshType=typeof(PlaygroundResume).GetNestedType("RestoreMesh",System.Reflection.BindingFlags.NonPublic); + foreach(string configuration in new[]{"overlap","opposite","vertex-touch","open","collapsed","thin"}) + { + var vertices=new List();var triangles=new List(); + foreach(var center in new[]{Vector3.zero,configuration=="vertex-touch"?Vector3.one:new Vector3(.3f,.2f,.1f),new Vector3(7,3,-4)}) + { + int offset=vertices.Count;vertices.AddRange(cubeVertices.Select(point=>point+center));var indices=cubeTriangles; + if(configuration=="opposite"&&offset==cubeVertices.Length) indices=cubeTriangles.Reverse().ToArray(); + triangles.AddRange(indices.Select(index=>offset+index)); + } + if(configuration=="open") triangles.RemoveRange(0,3); + var mesh=new Mesh();mesh.vertices=vertices.ToArray();mesh.triangles=triangles.ToArray();mesh.RecalculateBounds();_fixtures.Add(mesh); + var obstacle=new GameObject("test-only-component-distance");_fixtures.Add(obstacle);obstacle.transform.SetPositionAndRotation(new Vector3(3,-2,5),Quaternion.Euler(23,37,11));obstacle.transform.localScale=new Vector3(1.7f,configuration=="collapsed"?0:configuration=="thin"?.0001f:.65f,-1.1f); + var collider=obstacle.AddComponent();collider.sharedMesh=mesh;var world=vertices.Select(collider.transform.TransformPoint).ToArray(); + var geometry=System.Activator.CreateInstance(meshType,new object[]{collider,vertices.ToArray(),triangles.ToArray()});bool closed=(bool)meshType.GetProperty("IsClosed").GetValue(geometry); + Assert.That(closed,Is.EqualTo(configuration!="open"));var distance=(MeshDistance)meshType.GetMethod("Distance").CreateDelegate(typeof(MeshDistance),geometry); + var points=new List{obstacle.transform.TransformPoint(new Vector3(.15f,.1f,.05f)),obstacle.transform.TransformPoint(new Vector3(7,3,-4)),Vector3.one*1000,-Vector3.one*1000}; + for(int index=0;index System.Math.PI * 2; + distance = Mathf.Sqrt(squared); inside = closed && System.Math.Abs(winding) > System.Math.PI * 2; } private static Vector3 ReferenceClosestTriangle(Vector3 point, Vector3 a, Vector3 b, Vector3 c) { diff --git a/Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundResume.cs b/Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundResume.cs index e2fe9a6..b97994f 100644 --- a/Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundResume.cs +++ b/Assets/MagnetismHeroes/UnityRuntime/Bootstrap/PlaygroundResume.cs @@ -296,6 +296,9 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap private readonly Vector3[] _vertices; private readonly int[] _triangles; private readonly float[] _magnitudes; + private readonly int[] _triangleComponents; + private readonly Vector3[] _componentMin, _componentMax; + private readonly bool[] _windingComponents; private readonly bool _closed; public bool IsClosed => _closed; public RestoreMesh(MeshCollider collider, Vector3[] vertices, int[] triangles) @@ -312,28 +315,76 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap edges.TryGetValue(key, out var value); edges[key] = (value.count + 1, value.orientation + (forward ? 1 : -1)); } _closed = edges.Count > 0 && edges.Values.All(value => value.count == 2 && value.orientation == 0); + if (!_closed) + { + _triangleComponents = Array.Empty(); _componentMin = _componentMax = Array.Empty(); + _windingComponents = Array.Empty(); return; + } + // Every welded component inherits the checked closed, oriented edges. Outside its bounds its winding is zero. + var parents = new int[vertices.Length]; var welded = new Dictionary(); + for (int index = 0; index < vertices.Length; index++) + { + if (!welded.TryGetValue(vertices[index], out int first)) { first = index; welded.Add(vertices[index], first); } + parents[index] = first; + } + int Root(int index) + { + while (parents[index] != index) { parents[index] = parents[parents[index]]; index = parents[index]; } + return index; + } + for (int index = 0; index < triangles.Length; index += 3) + { int first = Root(triangles[index]), second = Root(triangles[index + 1]), third = Root(triangles[index + 2]); parents[second] = first; parents[third] = first; } + _triangleComponents = new int[triangles.Length / 3]; + var components = new Dictionary(); var minimum = new List(); var maximum = new List(); + for (int index = 0; index < triangles.Length; index += 3) + { + int root = Root(triangles[index]); + if (!components.TryGetValue(root, out int component)) + { component = components.Count; components.Add(root, component); minimum.Add(_vertices[triangles[index]]); maximum.Add(_vertices[triangles[index]]); } + _triangleComponents[index / 3] = component; + for (int vertex = 0; vertex < 3; vertex++) + { var point = _vertices[triangles[index + vertex]]; minimum[component] = Vector3.Min(minimum[component], point); maximum[component] = Vector3.Max(maximum[component], point); } + } + _componentMin = minimum.ToArray(); _componentMax = maximum.ToArray(); _windingComponents = new bool[components.Count]; } private static int Compare(Vector3 first, Vector3 second) { int value = first.x.CompareTo(second.x); if (value != 0) return value; value = first.y.CompareTo(second.y); return value != 0 ? value : first.z.CompareTo(second.z); } public void Distance(Vector3 point, out Vector3 nearest, out float distance, out bool inside) { nearest = Vector3.zero; float squared = float.PositiveInfinity; double winding = 0; + bool canCull = _closed, skipped = false; // Each query keeps the original float magnitude and triangle summation order, reusing shared vertices. - if (_closed) for (int index = 0; index < _vertices.Length; index++) _magnitudes[index] = (_vertices[index] - point).magnitude; + if (_closed) for (int index = 0; index < _vertices.Length; index++) + { _magnitudes[index] = (_vertices[index] - point).magnitude; canCull &= float.IsFinite(_magnitudes[index]) && _magnitudes[index] > 0; } + for (int index = 0; index < _componentMin.Length; index++) + { + // Winding is zero outside a closed component, independently of the sphere clearance radius. + var min = _componentMin[index] - Vector3.one * .0001f; var max = _componentMax[index] + Vector3.one * .0001f; + _windingComponents[index] = point.x >= min.x && point.y >= min.y && point.z >= min.z && point.x <= max.x && point.y <= max.y && point.z <= max.z; + } for (int index = 0; index < _triangles.Length; index += 3) { var a = _vertices[_triangles[index]]; var b = _vertices[_triangles[index + 1]]; var c = _vertices[_triangles[index + 2]]; var candidate = ClosestTriangle(point, a, b, c); float candidateSquared = (candidate - point).sqrMagnitude; if (candidateSquared < squared) { squared = candidateSquared; nearest = candidate; } if (!_closed) continue; - a -= point; b -= point; c -= point; - double numerator = Vector3.Dot(a, Vector3.Cross(b, c)); - float aMagnitude = _magnitudes[_triangles[index]], bMagnitude = _magnitudes[_triangles[index + 1]], cMagnitude = _magnitudes[_triangles[index + 2]]; - double denominator = (double)aMagnitude * bMagnitude * cMagnitude + (double)Vector3.Dot(a, b) * cMagnitude - + (double)Vector3.Dot(b, c) * aMagnitude + (double)Vector3.Dot(c, a) * bMagnitude; - winding += 2 * Math.Atan2(numerator, denominator); + if (!canCull || _windingComponents[_triangleComponents[index / 3]]) winding += SolidAngle(index, point); + else skipped = true; } - distance = Mathf.Sqrt(squared); inside = _closed && Math.Abs(winding) > Math.PI * 2; + distance = Mathf.Sqrt(squared); + // Surface singularities and the numerical inside threshold retain the original full summation. + if (skipped && (distance <= .0001f || Math.Abs(Math.Abs(winding) - Math.PI * 2) <= .001)) + { winding = 0; for (int index = 0; index < _triangles.Length; index += 3) winding += SolidAngle(index, point); } + inside = _closed && Math.Abs(winding) > Math.PI * 2; + } + private double SolidAngle(int index, Vector3 point) + { + var a = _vertices[_triangles[index]] - point; var b = _vertices[_triangles[index + 1]] - point; var c = _vertices[_triangles[index + 2]] - point; + double numerator = Vector3.Dot(a, Vector3.Cross(b, c)); + float aMagnitude = _magnitudes[_triangles[index]], bMagnitude = _magnitudes[_triangles[index + 1]], cMagnitude = _magnitudes[_triangles[index + 2]]; + double denominator = (double)aMagnitude * bMagnitude * cMagnitude + (double)Vector3.Dot(a, b) * cMagnitude + + (double)Vector3.Dot(b, c) * aMagnitude + (double)Vector3.Dot(c, a) * bMagnitude; + return 2 * Math.Atan2(numerator, denominator); } private static Vector3 ClosestTriangle(Vector3 point, Vector3 a, Vector3 b, Vector3 c) {