Reuse vertex magnitudes during resume geometry certification

This commit is contained in:
NINI
2026-10-10 05:32:59 +09:00
parent 14f89d1248
commit bc1ce03beb
2 changed files with 104 additions and 2 deletions
@@ -99,6 +99,103 @@ namespace MagnetismHeroes.Tests
Debug.Log($"[ProgressRestoreMeshDiagnostic] mesh={collider.sharedMesh.name}, point={point:R}, closestSurface={surface:R}, distance={closest:R}, triangle={closestTriangle}, position={collider.transform.position:R}, rotation={collider.transform.rotation:R}, scale={collider.transform.lossyScale:R}, vertices={vertices.Length}, triangles={triangles.Length/3}"); Debug.Log($"[ProgressRestoreMeshDiagnostic] mesh={collider.sharedMesh.name}, point={point:R}, closestSurface={surface:R}, distance={closest:R}, triangle={closestTriangle}, position={collider.transform.position:R}, rotation={collider.transform.rotation:R}, scale={collider.transform.lossyScale:R}, vertices={vertices.Length}, triangles={triangles.Length/3}");
} }
} }
private delegate void MeshDistance(Vector3 point, out Vector3 nearest, out float distance, out bool inside);
[UnityTest] public IEnumerator ApprovedTreeDistanceMatchesFrozenReferenceAndNativeAndSyntheticEntriesRemainStable()
{
_store.Save.puzzleStates=new[]{new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==2)).ToPuzzleState(GameSession.DefaultCamera()),new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==9)).ToPuzzleState(GameSession.DefaultCamera())};
Reload();_app.StartStage(6);yield return null;
var cache=(PlaygroundRestoreGeometry)new SerializedObject(_app).FindProperty("_restoreGeometry").objectReferenceValue;
var meshType=typeof(PlaygroundResume).GetNestedType("RestoreMesh",System.Reflection.BindingFlags.NonPublic);
var nativePoint=new Vector3(-8.3752327f,1.6241438f,-4.0848165f);
var native=_app.Environment.GetComponentsInChildren<MeshCollider>().Where(mesh=>mesh.enabled&&mesh.sharedMesh.name=="RoundCanopyTree").OrderBy(mesh=>Vector3.Distance(mesh.bounds.center,nativePoint)).First();
Assert.That(Vector3.Distance(native.bounds.center,nativePoint),Is.LessThan(.001f));
foreach(string fixture in new[]{"native-stage6","synthetic-tree"})
{
var collider=native;
if(fixture=="synthetic-tree")
{
foreach(var item in _app.Environment.GetComponentsInChildren<Collider>()) if(!item.isTrigger&&!item.name.Contains("Clearing")) item.enabled=false;
var obstacle=Object.Instantiate(AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/Meadow/RoundCanopyTree.prefab"),_app.Environment.transform);_fixtures.Add(obstacle);
obstacle.transform.localScale=Vector3.one*2;collider=obstacle.GetComponentInChildren<MeshCollider>();Physics.SyncTransforms();
obstacle.transform.position+=new Vector3(0,3,0)-collider.bounds.center;Physics.SyncTransforms();
}
Assert.That(collider.sharedMesh.isReadable,Is.False);Assert.That(cache.TryGet(collider.sharedMesh,out var local,out var triangles),Is.True);
Assert.That(local.Length,Is.EqualTo(4187));Assert.That(triangles.Length/3,Is.EqualTo(4840));
var world=local.Select(collider.transform.TransformPoint).ToArray();
var geometry=System.Activator.CreateInstance(meshType,new object[]{collider,local,triangles});Assert.That(meshType.GetProperty("IsClosed").GetValue(geometry),Is.True);
var distance=(MeshDistance)meshType.GetMethod("Distance").CreateDelegate(typeof(MeshDistance),geometry);
var samples=new List<Vector3>{collider.bounds.center,collider.bounds.min-Vector3.one,collider.bounds.max+Vector3.one};
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 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);
}
int insideSamples=0,outsideSamples=0;
foreach(var point in samples)
{
ReferenceMeshDistance(world,triangles,point,out var expectedNearest,out float expectedDistance,out bool expectedInside,out double winding);
distance(point,out var actualNearest,out float actualDistance,out bool actualInside);
Assert.That(actualNearest.x,Is.EqualTo(expectedNearest.x));Assert.That(actualNearest.y,Is.EqualTo(expectedNearest.y));Assert.That(actualNearest.z,Is.EqualTo(expectedNearest.z));
Assert.That(actualDistance,Is.EqualTo(expectedDistance));Assert.That(actualInside,Is.EqualTo(expectedInside));Assert.That(actualInside,Is.EqualTo(System.Math.Abs(winding)>System.Math.PI*2));
if(expectedInside) insideSamples++;else outsideSamples++;
}
Assert.That(insideSamples,Is.GreaterThan(0));Assert.That(outsideSamples,Is.GreaterThan(0));
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(collider.sharedMesh,out string guid,out long fileId);
Debug.Log($"[ResumeMagnitudeReference] fixture={fixture}, meshGuid={guid}, fileId={fileId}, geometryHash={PlaygroundRestoreGeometry.Hash(local,triangles)}, vertices={local.Length}, triangles={triangles.Length/3}, position={collider.transform.position:R}, rotation={collider.transform.rotation:R}, scale={collider.transform.lossyScale:R}, samples={samples.Count}, inside={insideSamples}, outside={outsideSamples}");
var timingPoints=samples.Take(12).ToArray();
for(int warmup=0;warmup<2;warmup++) foreach(var point in timingPoints) {ReferenceMeshDistance(world,triangles,point,out _,out _,out _,out _);distance(point,out _,out _,out _);}
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) 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}");
}
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;
for(int iteration=0;iteration<4;iteration++)
{
_store.Save.latestFreePlay=new FreePlayState{sourceStageId=6,balls=new[]{new BallState{id="native-tree-interior",position=BoardPresenter.ToPosition(collider.bounds.center)}},groups=new[]{Group("native-group","native-tree-interior")},camera=GameSession.DefaultCamera()};
Reload();int writes=_store.Writes;var timer=System.Diagnostics.Stopwatch.StartNew();_app.ContinueFreePlay();timer.Stop();yield return null;
Assert.That(_app.Session.Screen,Is.EqualTo(SessionScreen.FreePlay));Assert.That(_app.Presenter.IsSafeBoard(_app.Session.Board),Is.True);Assert.That(_store.Writes,Is.EqualTo(writes+1));
Assert.That(_app.Session.Board.Balls.Select(ball=>ball.id),Is.EqualTo(new[]{"native-tree-interior"}));Assert.That(_app.Session.Board.Groups.Single().id,Is.EqualTo("native-group"));
Assert.That(string.Join("|",_store.Save.puzzleStates.Select(state=>JsonUtility.ToJson(state))),Is.EqualTo(puzzles));Assert.That(_store.Save.completedStageIds,Is.EqualTo(completed));Assert.That(_store.Save.seenTutorialIds,Is.EqualTo(tutorials));
string result=JsonUtility.ToJson(_store.Save.latestFreePlay);if(expected==null) expected=result;else Assert.That(result,Is.EqualTo(expected));
Debug.Log($"[ResumeMagnitudeEntryTiming] fixture={fixture}, warmup={iteration==0}, sample={iteration-1}, utc={System.DateTime.UtcNow:O}, entryMs={timer.Elapsed.TotalMilliseconds:R}, result={result}");
}
_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)
{
nearest = Vector3.zero; float squared = float.PositiveInfinity; winding = 0;
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 = ReferenceClosestTriangle(point, a, b, c); float candidateSquared = (candidate - point).sqrMagnitude;
if (candidateSquared < squared) { squared = candidateSquared; nearest = candidate; }
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;
}
private static Vector3 ReferenceClosestTriangle(Vector3 point, Vector3 a, Vector3 b, Vector3 c)
{
var ab = b - a; var ac = c - a; var ap = point - a; float d1 = Vector3.Dot(ab, ap), d2 = Vector3.Dot(ac, ap);
if (d1 <= 0 && d2 <= 0) return a;
var bp = point - b; float d3 = Vector3.Dot(ab, bp), d4 = Vector3.Dot(ac, bp);
if (d3 >= 0 && d4 <= d3) return b;
float vc = d1 * d4 - d3 * d2; if (vc <= 0 && d1 >= 0 && d3 <= 0) return a + ab * (d1 / (d1 - d3));
var cp = point - c; float d5 = Vector3.Dot(ab, cp), d6 = Vector3.Dot(ac, cp);
if (d6 >= 0 && d5 <= d6) return c;
float vb = d5 * d2 - d1 * d6; if (vb <= 0 && d2 >= 0 && d6 <= 0) return a + ac * (d2 / (d2 - d6));
float va = d3 * d6 - d5 * d4; if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) return b + (c - b) * ((d4 - d3) / (d4 - d3 + d5 - d6));
float inverse = 1 / (va + vb + vc); return a + ab * (vb * inverse) + ac * (vc * inverse);
}
[UnityTest] public IEnumerator ActualGardenSplitFiveResumeRepairsWithoutChangingThreeAndTwoMembershipOrOtherSlots() [UnityTest] public IEnumerator ActualGardenSplitFiveResumeRepairsWithoutChangingThreeAndTwoMembershipOrOtherSlots()
{ {
_store.Save.puzzleStates=new[]{new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==2)).ToPuzzleState(GameSession.DefaultCamera()),new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==6)).ToPuzzleState(GameSession.DefaultCamera())}; _store.Save.puzzleStates=new[]{new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==2)).ToPuzzleState(GameSession.DefaultCamera()),new BoardRules(_app.Session.Definitions.First(stage=>stage.Id==6)).ToPuzzleState(GameSession.DefaultCamera())};
@@ -295,12 +295,14 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
{ {
private readonly Vector3[] _vertices; private readonly Vector3[] _vertices;
private readonly int[] _triangles; private readonly int[] _triangles;
private readonly float[] _magnitudes;
private readonly bool _closed; private readonly bool _closed;
public bool IsClosed => _closed; public bool IsClosed => _closed;
public RestoreMesh(MeshCollider collider, Vector3[] vertices, int[] triangles) public RestoreMesh(MeshCollider collider, Vector3[] vertices, int[] triangles)
{ {
_triangles = triangles; _triangles = triangles;
_vertices = vertices.Select(collider.transform.TransformPoint).ToArray(); _vertices = vertices.Select(collider.transform.TransformPoint).ToArray();
_magnitudes = new float[_vertices.Length];
var edges = new Dictionary<(Vector3 first, Vector3 second), (int count, int orientation)>(); var edges = new Dictionary<(Vector3 first, Vector3 second), (int count, int orientation)>();
for (int index = 0; index < _triangles.Length; index += 3) for (int index = 0; index < _triangles.Length; index += 3)
for (int edge = 0; edge < 3; edge++) for (int edge = 0; edge < 3; edge++)
@@ -316,6 +318,8 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
public void Distance(Vector3 point, out Vector3 nearest, out float distance, out bool inside) public void Distance(Vector3 point, out Vector3 nearest, out float distance, out bool inside)
{ {
nearest = Vector3.zero; float squared = float.PositiveInfinity; double winding = 0; nearest = Vector3.zero; float squared = float.PositiveInfinity; double winding = 0;
// 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;
for (int index = 0; index < _triangles.Length; index += 3) 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 a = _vertices[_triangles[index]]; var b = _vertices[_triangles[index + 1]]; var c = _vertices[_triangles[index + 2]];
@@ -324,8 +328,9 @@ namespace MagnetismHeroes.UnityRuntime.Bootstrap
if (!_closed) continue; if (!_closed) continue;
a -= point; b -= point; c -= point; a -= point; b -= point; c -= point;
double numerator = Vector3.Dot(a, Vector3.Cross(b, c)); 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 float aMagnitude = _magnitudes[_triangles[index]], bMagnitude = _magnitudes[_triangles[index + 1]], cMagnitude = _magnitudes[_triangles[index + 2]];
+ (double)Vector3.Dot(b, c) * a.magnitude + (double)Vector3.Dot(c, a) * b.magnitude; 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); winding += 2 * Math.Atan2(numerator, denominator);
} }
distance = Mathf.Sqrt(squared); inside = _closed && Math.Abs(winding) > Math.PI * 2; distance = Mathf.Sqrt(squared); inside = _closed && Math.Abs(winding) > Math.PI * 2;