Preserve restore geometry results while reusing vertex magnitudes
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
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()
|
||||
{
|
||||
_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 int[] _triangles;
|
||||
private readonly float[] _magnitudes;
|
||||
private readonly bool _closed;
|
||||
public bool IsClosed => _closed;
|
||||
public RestoreMesh(MeshCollider collider, Vector3[] vertices, int[] triangles)
|
||||
{
|
||||
_triangles = triangles;
|
||||
_vertices = vertices.Select(collider.transform.TransformPoint).ToArray();
|
||||
_magnitudes = new float[_vertices.Length];
|
||||
var edges = new Dictionary<(Vector3 first, Vector3 second), (int count, int orientation)>();
|
||||
for (int index = 0; index < _triangles.Length; index += 3)
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
distance = Mathf.Sqrt(squared); inside = _closed && Math.Abs(winding) > Math.PI * 2;
|
||||
|
||||
Reference in New Issue
Block a user