perf: preserve resume positions while reducing closed-mesh winding work

This commit is contained in:
NINI
2026-10-10 06:31:42 +09:00
2 changed files with 120 additions and 12 deletions
@@ -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<MeshFilter>().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<Vector3>();var triangles=new List<int>();
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<MeshCollider>();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<Vector3>{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<triangles.Count;index+=3)
{
var a=world[triangles[index]];var b=world[triangles[index+1]];var c=world[triangles[index+2]];var normal=Vector3.Cross(b-a,c-a).normalized;
foreach(var point in new[]{a,(a+b)*.5f,(a+b+c)/3}) {points.Add(point);points.Add(point+normal*.000001f);points.Add(point-normal*.000001f);}
}
// Component faces just beyond the winding bound, including singular and non-finite query controls.
for(int offset=0;offset<world.Length;offset+=cubeVertices.Length)
{
var part=world.Skip(offset).Take(cubeVertices.Length).ToArray();var min=part.Aggregate(Vector3.Min);var max=part.Aggregate(Vector3.Max);
for(int axis=0;axis<3;axis++) foreach(float face in new[]{min[axis],max[axis]})
{
var point=(min+max)*.5f;point[axis]=face;var delta=Vector3.zero;delta[axis]=.0002f;
points.Add(point);points.Add(point+delta);points.Add(point-delta);
}
}
points.Add(new Vector3(float.NaN,0,0));points.Add(new Vector3(float.PositiveInfinity,0,0));points.Add(new Vector3(float.NegativeInfinity,0,0));
foreach(var point in points)
{
ReferenceMeshDistance(world,triangles.ToArray(),point,out var expectedNearest,out float expectedDistance,out bool expectedInside,out _,closed);
distance(point,out var actualNearest,out float actualDistance,out bool actualInside);
Assert.That(actualNearest,Is.EqualTo(expectedNearest));Assert.That(actualDistance,Is.EqualTo(expectedDistance));Assert.That(actualInside,Is.EqualTo(expectedInside),configuration+" "+point.ToString("R"));
}
Debug.Log($"[ResumeDistancePathology] configuration={configuration}, closed={closed}, samples={points.Count}, exact=True");
obstacle.SetActive(false);
}
yield return null;
}
private static void ReferenceMeshDistance(Vector3[] vertices, int[] triangles, Vector3 point, out Vector3 nearest, out float distance, out bool inside, out double winding, bool closed = true)
{
nearest = Vector3.zero; float squared = float.PositiveInfinity; winding = 0;
for (int index = 0; index < triangles.Length; index += 3)
@@ -174,14 +231,14 @@ namespace MagnetismHeroes.Tests
var a = vertices[triangles[index]]; var b = vertices[triangles[index + 1]]; var c = vertices[triangles[index + 2]];
var candidate = ReferenceClosestTriangle(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));
double denominator = (double)a.magnitude * b.magnitude * c.magnitude + (double)Vector3.Dot(a, b) * c.magnitude
+ (double)Vector3.Dot(b, c) * a.magnitude + (double)Vector3.Dot(c, a) * b.magnitude;
winding += 2 * System.Math.Atan2(numerator, denominator);
}
distance = Mathf.Sqrt(squared); inside = System.Math.Abs(winding) > 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)
{
@@ -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<int>(); _componentMin = _componentMax = Array.Empty<Vector3>();
_windingComponents = Array.Empty<bool>(); 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<Vector3, int>();
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<int, int>(); var minimum = new List<Vector3>(); var maximum = new List<Vector3>();
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)
{