Skip exterior closed-component winding during resume
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user