1256 lines
42 KiB
C#
1256 lines
42 KiB
C#
using System.Collections;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Tests.Conformance;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.Enums;
|
|
using DatReaderWriter.Options;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.Core.Tests.Physics;
|
|
|
|
public sealed class FlatBspQueryDifferentialTests
|
|
{
|
|
[Fact]
|
|
public void CellContainment_NullOnPlaneRadiusEqualityAndDeepChain_MatchGraph()
|
|
{
|
|
var leaf = new CellBSPNode
|
|
{
|
|
Type = BSPNodeType.Leaf,
|
|
LeafIndex = 77,
|
|
};
|
|
CellBSPNode graph = leaf;
|
|
const int Depth = 256;
|
|
for (int i = 0; i < Depth; i++)
|
|
{
|
|
graph = new CellBSPNode
|
|
{
|
|
Type = BSPNodeType.BPIn,
|
|
SplittingPlane = new Plane(
|
|
Vector3.UnitX,
|
|
i == 0 ? 0f : 1_000f),
|
|
PosNode = graph,
|
|
};
|
|
}
|
|
|
|
FlatCellContainmentBsp flat =
|
|
FlatCollisionAssetBuilder.FlattenCellContainmentBsp(graph);
|
|
Vector3[] points =
|
|
[
|
|
Vector3.Zero,
|
|
new Vector3(0.5f, 0f, 0f),
|
|
new Vector3(-0.5f, 0f, 0f),
|
|
new Vector3(
|
|
BitConverter.Int32BitsToSingle(1),
|
|
0f,
|
|
0f),
|
|
];
|
|
|
|
foreach (Vector3 point in points)
|
|
{
|
|
Assert.Equal(
|
|
BSPQuery.PointInsideCellBsp(graph, point),
|
|
FlatBspQuery.PointInsideCellBsp(flat, point));
|
|
}
|
|
|
|
foreach ((Vector3 center, float radius) in new[]
|
|
{
|
|
(Vector3.Zero, 0f),
|
|
(new Vector3(-0.51f, 0f, 0f), 0.5f),
|
|
(new Vector3(-0.51f, 0f, 0f), 0.49999997f),
|
|
(new Vector3(-0.51f, 0f, 0f), 0.50000006f),
|
|
})
|
|
{
|
|
Assert.Equal(
|
|
BSPQuery.SphereIntersectsCellBsp(graph, center, radius),
|
|
FlatBspQuery.SphereIntersectsCellBsp(flat, center, radius));
|
|
}
|
|
|
|
var empty = FlatCollisionAssetBuilder.FlattenCellContainmentBsp(null);
|
|
Assert.True(FlatBspQuery.PointInsideCellBsp(empty, Vector3.Zero));
|
|
Assert.True(FlatBspQuery.SphereIntersectsCellBsp(
|
|
empty,
|
|
Vector3.Zero,
|
|
0.5f));
|
|
}
|
|
|
|
[Fact]
|
|
public void StaticAndSweptOverlap_MultiPolygonLeafOrderAndBoundaryValues_MatchGraphBits()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildTwoWallLeaf();
|
|
FlatPhysicsBsp flat =
|
|
FlatCollisionAssetBuilder.FlattenPhysicsBsp(root, resolved);
|
|
|
|
var random = new Random(0x4B53_5034);
|
|
for (int i = 0; i < 10_000; i++)
|
|
{
|
|
Vector3 center = new(
|
|
NextFloat(random, -3f, 3f),
|
|
NextFloat(random, -1f, 1f),
|
|
NextFloat(random, -3f, 3f));
|
|
float radius = i % 7 switch
|
|
{
|
|
0 => 0f,
|
|
1 => PhysicsGlobals.EPSILON,
|
|
2 => 0.5f,
|
|
_ => NextFloat(random, 0.01f, 1.5f),
|
|
};
|
|
Vector3 movement = new(
|
|
NextFloat(random, -1f, 1f),
|
|
NextFloat(random, -1f, 1f),
|
|
NextFloat(random, -1f, 1f));
|
|
|
|
bool graphStatic = BSPQuery.SphereIntersectsPoly(
|
|
root,
|
|
resolved,
|
|
center,
|
|
radius,
|
|
out ushort graphStaticId,
|
|
out Vector3 graphStaticNormal);
|
|
bool flatStatic = FlatBspQuery.SphereIntersectsPoly(
|
|
flat,
|
|
center,
|
|
radius,
|
|
out ushort flatStaticId,
|
|
out Vector3 flatStaticNormal);
|
|
|
|
Assert.Equal(graphStatic, flatStatic);
|
|
Assert.Equal(graphStaticId, flatStaticId);
|
|
AssertVectorBits(graphStaticNormal, flatStaticNormal);
|
|
|
|
bool graphSwept = BSPQuery.SphereIntersectsPolyWithTime(
|
|
root,
|
|
resolved,
|
|
center,
|
|
radius,
|
|
movement,
|
|
out ushort graphSweptId,
|
|
out Vector3 graphSweptNormal,
|
|
out float graphTime);
|
|
bool flatSwept = FlatBspQuery.SphereIntersectsPolyWithTime(
|
|
flat,
|
|
center,
|
|
radius,
|
|
movement,
|
|
out ushort flatSweptId,
|
|
out Vector3 flatSweptNormal,
|
|
out float flatTime);
|
|
|
|
Assert.Equal(graphSwept, flatSwept);
|
|
Assert.Equal(graphSweptId, flatSweptId);
|
|
AssertVectorBits(graphSweptNormal, flatSweptNormal);
|
|
AssertFloatBits(graphTime, flatTime);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void FindWalkable_RandomizedAndEqualCandidateDistances_MatchesGraphBits()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildTwoFloorLeaf();
|
|
FlatPhysicsBsp flat =
|
|
FlatCollisionAssetBuilder.FlattenPhysicsBsp(root, resolved);
|
|
var random = new Random(0x5741_4C4B);
|
|
|
|
for (int i = 0; i < 5_000; i++)
|
|
{
|
|
Vector3 center = new(
|
|
NextFloat(random, -2.5f, 2.5f),
|
|
NextFloat(random, -2.5f, 2.5f),
|
|
NextFloat(random, -0.25f, 1.25f));
|
|
float radius = i % 5 == 0
|
|
? 0.48f
|
|
: NextFloat(random, 0.05f, 0.8f);
|
|
float probe = i % 7 == 0
|
|
? 0.5f
|
|
: NextFloat(random, 0.01f, 1.5f);
|
|
|
|
Transition graphTransition = NewTransition();
|
|
Transition flatTransition = NewTransition();
|
|
bool graphFound = BSPQuery.FindWalkableSphere(
|
|
root,
|
|
resolved,
|
|
graphTransition,
|
|
center,
|
|
radius,
|
|
probe,
|
|
Vector3.UnitZ,
|
|
out ResolvedPolygon? graphPolygon,
|
|
out ushort graphId,
|
|
out Vector3 graphCenter);
|
|
bool flatFound = FlatBspQuery.FindWalkableSphere(
|
|
flat,
|
|
flatTransition,
|
|
center,
|
|
radius,
|
|
probe,
|
|
Vector3.UnitZ,
|
|
out int flatPolygonIndex,
|
|
out ushort flatId,
|
|
out Vector3 flatCenter);
|
|
|
|
Assert.Equal(graphFound, flatFound);
|
|
Assert.Equal(graphId, flatId);
|
|
Assert.Equal(graphPolygon is null, flatPolygonIndex < 0);
|
|
AssertVectorBits(graphCenter, flatCenter);
|
|
AssertTransitionEquivalent(graphTransition, flatTransition);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PhysicsTraversal_NullChildRadiusEqualityEqualCandidatesAndDeepTree_Match()
|
|
{
|
|
ResolvedPolygon first = Polygon(
|
|
9,
|
|
new Plane(Vector3.UnitZ, 0f),
|
|
new Vector3(-2f, -2f, 0f),
|
|
new Vector3(2f, -2f, 0f),
|
|
new Vector3(2f, 2f, 0f),
|
|
new Vector3(-2f, 2f, 0f));
|
|
ResolvedPolygon second = Polygon(
|
|
4,
|
|
first.Plane,
|
|
first.Vertices.ToArray());
|
|
PhysicsBSPNode graph = Leaf();
|
|
graph.Polygons.Add(first.Id);
|
|
graph.Polygons.Add(second.Id);
|
|
var resolved = new Dictionary<ushort, ResolvedPolygon>
|
|
{
|
|
[second.Id] = second,
|
|
[first.Id] = first,
|
|
};
|
|
|
|
const int Depth = 128;
|
|
for (int i = 0; i < Depth; i++)
|
|
{
|
|
graph = new PhysicsBSPNode
|
|
{
|
|
Type = BSPNodeType.BPIn,
|
|
SplittingPlane = new Plane(Vector3.UnitX, 1_000f),
|
|
PosNode = graph,
|
|
BoundingSphere = new Sphere
|
|
{
|
|
Origin = Vector3.Zero,
|
|
Radius = 10_000f,
|
|
},
|
|
};
|
|
}
|
|
|
|
FlatPhysicsBsp flat =
|
|
FlatCollisionAssetBuilder.FlattenPhysicsBsp(graph, resolved);
|
|
Vector3 equalityCenter = new(0f, 0f, 0.48f);
|
|
bool graphStatic = BSPQuery.SphereIntersectsPoly(
|
|
graph,
|
|
resolved,
|
|
equalityCenter,
|
|
0.48f,
|
|
out ushort graphStaticId,
|
|
out Vector3 graphStaticNormal);
|
|
bool flatStatic = FlatBspQuery.SphereIntersectsPoly(
|
|
flat,
|
|
equalityCenter,
|
|
0.48f,
|
|
out ushort flatStaticId,
|
|
out Vector3 flatStaticNormal);
|
|
Assert.Equal(graphStatic, flatStatic);
|
|
Assert.Equal(graphStaticId, flatStaticId);
|
|
AssertVectorBits(graphStaticNormal, flatStaticNormal);
|
|
|
|
Transition graphTransition = NewTransition();
|
|
Transition flatTransition = NewTransition();
|
|
bool graphFound = BSPQuery.FindWalkableSphere(
|
|
graph,
|
|
resolved,
|
|
graphTransition,
|
|
new Vector3(0f, 0f, 0.4f),
|
|
0.48f,
|
|
0.5f,
|
|
Vector3.UnitZ,
|
|
out ResolvedPolygon? graphPolygon,
|
|
out ushort graphPolygonId,
|
|
out Vector3 graphAdjustedCenter);
|
|
bool flatFound = FlatBspQuery.FindWalkableSphere(
|
|
flat,
|
|
flatTransition,
|
|
new Vector3(0f, 0f, 0.4f),
|
|
0.48f,
|
|
0.5f,
|
|
Vector3.UnitZ,
|
|
out int flatPolygonIndex,
|
|
out ushort flatPolygonId,
|
|
out Vector3 flatAdjustedCenter);
|
|
Assert.Equal(graphFound, flatFound);
|
|
Assert.Equal(graphPolygonId, flatPolygonId);
|
|
Assert.Equal(graphPolygon is null, flatPolygonIndex < 0);
|
|
AssertVectorBits(graphAdjustedCenter, flatAdjustedCenter);
|
|
AssertTransitionEquivalent(graphTransition, flatTransition);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCollisions_Path1Placement_MatchesGraphStateAndMutations()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildFloorLeaf();
|
|
Sphere sphere = SphereAt(new Vector3(0f, 0f, 0.4f));
|
|
AssertFindCollisionsEquivalent(
|
|
root,
|
|
resolved,
|
|
sphere,
|
|
null,
|
|
sphere.Origin,
|
|
transition => transition.SpherePath.InsertType = InsertType.Placement);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCollisions_Path2CheckWalkable_MatchesGraphStateAndMutations()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildFloorLeaf();
|
|
Sphere sphere = SphereAt(new Vector3(0f, 0f, 0.4f));
|
|
AssertFindCollisionsEquivalent(
|
|
root,
|
|
resolved,
|
|
sphere,
|
|
null,
|
|
sphere.Origin,
|
|
transition => transition.SpherePath.CheckWalkable = true);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCollisions_Path3StepDown_MatchesEveryTransitionOutputBit()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildFloorLeaf();
|
|
Sphere sphere = SphereAt(new Vector3(0f, 0f, 0.4f));
|
|
AssertFindCollisionsEquivalent(
|
|
root,
|
|
resolved,
|
|
sphere,
|
|
null,
|
|
sphere.Origin,
|
|
transition =>
|
|
{
|
|
transition.SpherePath.StepDown = true;
|
|
transition.SpherePath.StepDownAmt = 0.5f;
|
|
},
|
|
worldOrigin: new Vector3(17f, -23f, 94f));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCollisions_Path4Landing_MatchesEveryTransitionOutputBit()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildFloorLeaf();
|
|
Sphere sphere = SphereAt(new Vector3(0f, 0f, 0.4f));
|
|
AssertFindCollisionsEquivalent(
|
|
root,
|
|
resolved,
|
|
sphere,
|
|
null,
|
|
sphere.Origin + new Vector3(0f, 0f, 0.05f),
|
|
transition => transition.SpherePath.Collide = true,
|
|
worldOrigin: new Vector3(17f, -23f, 94f));
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCollisions_Path5FullAndNearMiss_MatchEveryTransitionOutputBit()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildTwoWallLeaf();
|
|
Sphere foot = SphereAt(new Vector3(0f, 0.3f, 0f));
|
|
Sphere head = SphereAt(new Vector3(0f, 0.3f, 1f));
|
|
|
|
AssertFindCollisionsEquivalent(
|
|
root,
|
|
resolved,
|
|
foot,
|
|
head,
|
|
foot.Origin - new Vector3(0.05f, 0f, 0f),
|
|
transition =>
|
|
transition.ObjectInfo.State = ObjectInfoState.Contact);
|
|
|
|
AssertFindCollisionsEquivalent(
|
|
root,
|
|
resolved,
|
|
foot,
|
|
null,
|
|
foot.Origin - new Vector3(0f, -0.05f, 0f),
|
|
transition =>
|
|
transition.ObjectInfo.State = ObjectInfoState.Contact);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCollisions_Path6LandingSteepAndPerfectClip_MatchEveryOutputBit()
|
|
{
|
|
(PhysicsBSPNode floorRoot, Dictionary<ushort, ResolvedPolygon> floorResolved) =
|
|
BuildFloorLeaf();
|
|
Sphere floorSphere = SphereAt(new Vector3(0f, 0f, 0.4f));
|
|
AssertFindCollisionsEquivalent(
|
|
floorRoot,
|
|
floorResolved,
|
|
floorSphere,
|
|
null,
|
|
floorSphere.Origin + new Vector3(0f, 0f, 0.05f),
|
|
configure: null);
|
|
|
|
(PhysicsBSPNode wallRoot, Dictionary<ushort, ResolvedPolygon> wallResolved) =
|
|
BuildTwoWallLeaf();
|
|
Sphere wallSphere = SphereAt(new Vector3(0f, 0.3f, 0f));
|
|
AssertFindCollisionsEquivalent(
|
|
wallRoot,
|
|
wallResolved,
|
|
wallSphere,
|
|
null,
|
|
wallSphere.Origin - new Vector3(0f, -0.05f, 0f),
|
|
transition =>
|
|
{
|
|
transition.ObjectInfo.State =
|
|
ObjectInfoState.PathClipped | ObjectInfoState.PerfectClip;
|
|
});
|
|
}
|
|
|
|
[Fact]
|
|
public void InstalledDat_LargeRandomizedSweep_HasZeroBitMismatch()
|
|
{
|
|
string? datDirectory = ConformanceDats.ResolveDatDir();
|
|
if (datDirectory is null)
|
|
return;
|
|
|
|
using var dats = new DatCollection(datDirectory, DatAccessType.Read);
|
|
var random = new Random(0x4934_4253);
|
|
foreach (uint cellId in new[]
|
|
{
|
|
0x8A02_016Eu,
|
|
0x8A02_017Au,
|
|
0xA9B4_013Fu,
|
|
0xA9B4_0150u,
|
|
0xA9B4_0159u,
|
|
0xA9B4_015Au,
|
|
0xA9B4_0161u,
|
|
0xA9B4_0162u,
|
|
0xA9B4_0164u,
|
|
0xA9B4_0166u,
|
|
})
|
|
{
|
|
var cache = new PhysicsDataCache();
|
|
ConformanceDats.LoadEnvCell(dats, cache, cellId);
|
|
CellPhysics source = Assert.IsType<CellPhysics>(
|
|
cache.GetCellStruct(cellId));
|
|
FlatPhysicsBsp flat =
|
|
FlatCollisionAssetBuilder.FlattenPhysicsBsp(
|
|
source.BSP?.Root,
|
|
source.Resolved);
|
|
FlatCellContainmentBsp flatContainment =
|
|
FlatCollisionAssetBuilder.FlattenCellContainmentBsp(
|
|
source.CellBSP?.Root);
|
|
if (flat.PolygonTable.Polygons.Length == 0)
|
|
continue;
|
|
|
|
for (int iteration = 0; iteration < 2_500; iteration++)
|
|
{
|
|
int polygonIndex =
|
|
random.Next(flat.PolygonTable.Polygons.Length);
|
|
FlatCollisionPolygon polygon =
|
|
flat.PolygonTable.Polygons[polygonIndex];
|
|
Vector3 anchor = flat.PolygonTable.Vertices[
|
|
polygon.VertexRange.Start +
|
|
random.Next(polygon.VertexRange.Count)];
|
|
float radius = (iteration % 11) switch
|
|
{
|
|
0 => PhysicsGlobals.EPSILON,
|
|
1 => 0.01f,
|
|
2 => 0.48f,
|
|
3 => 1f,
|
|
_ => NextFloat(random, 0.05f, 1.25f),
|
|
};
|
|
float signedOffset = (iteration % 13) switch
|
|
{
|
|
0 => radius,
|
|
1 => radius - PhysicsGlobals.EPSILON,
|
|
2 => radius + PhysicsGlobals.EPSILON,
|
|
3 => 0f,
|
|
_ => NextFloat(random, -radius * 1.5f, radius * 1.5f),
|
|
};
|
|
Vector3 center =
|
|
anchor + polygon.Plane.Normal * signedOffset;
|
|
Vector3 movement = (iteration % 7) switch
|
|
{
|
|
0 => Vector3.Zero,
|
|
1 => -polygon.Plane.Normal * 0.05f,
|
|
2 => polygon.Plane.Normal * 0.05f,
|
|
_ => new Vector3(
|
|
NextFloat(random, -0.5f, 0.5f),
|
|
NextFloat(random, -0.5f, 0.5f),
|
|
NextFloat(random, -0.5f, 0.5f)),
|
|
};
|
|
|
|
AssertStaticAndSweptEquivalent(
|
|
source.BSP?.Root,
|
|
source.Resolved,
|
|
flat,
|
|
center,
|
|
radius,
|
|
movement,
|
|
cellId,
|
|
iteration);
|
|
|
|
bool graphInside = BSPQuery.PointInsideCellBsp(
|
|
source.CellBSP?.Root,
|
|
center);
|
|
bool flatInside =
|
|
FlatBspQuery.PointInsideCellBsp(flatContainment, center);
|
|
Assert.True(
|
|
graphInside == flatInside,
|
|
$"cell 0x{cellId:X8}, iteration {iteration}: point containment.");
|
|
|
|
bool graphSphereInside = BSPQuery.SphereIntersectsCellBsp(
|
|
source.CellBSP?.Root,
|
|
center,
|
|
radius);
|
|
bool flatSphereInside =
|
|
FlatBspQuery.SphereIntersectsCellBsp(
|
|
flatContainment,
|
|
center,
|
|
radius);
|
|
Assert.True(
|
|
graphSphereInside == flatSphereInside,
|
|
$"cell 0x{cellId:X8}, iteration {iteration}: sphere containment.");
|
|
|
|
Transition graphWalkable = NewTransition();
|
|
Transition flatWalkable = NewTransition();
|
|
float probeDistance = iteration % 5 == 0
|
|
? 0.5f
|
|
: NextFloat(random, 0.01f, 1.5f);
|
|
bool graphFound = BSPQuery.FindWalkableSphere(
|
|
source.BSP?.Root,
|
|
source.Resolved,
|
|
graphWalkable,
|
|
center,
|
|
radius,
|
|
probeDistance,
|
|
Vector3.UnitZ,
|
|
out ResolvedPolygon? graphPolygon,
|
|
out ushort graphPolygonId,
|
|
out Vector3 graphAdjustedCenter);
|
|
bool flatFound = FlatBspQuery.FindWalkableSphere(
|
|
flat,
|
|
flatWalkable,
|
|
center,
|
|
radius,
|
|
probeDistance,
|
|
Vector3.UnitZ,
|
|
out int flatPolygonIndex,
|
|
out ushort flatPolygonId,
|
|
out Vector3 flatAdjustedCenter);
|
|
Assert.True(
|
|
graphFound == flatFound,
|
|
$"cell 0x{cellId:X8}, iteration {iteration}: walkable result.");
|
|
Assert.Equal(graphPolygonId, flatPolygonId);
|
|
Assert.Equal(graphPolygon is null, flatPolygonIndex < 0);
|
|
AssertVectorBits(graphAdjustedCenter, flatAdjustedCenter);
|
|
AssertTransitionEquivalent(graphWalkable, flatWalkable);
|
|
|
|
Sphere sphere0 = new()
|
|
{
|
|
Origin = center,
|
|
Radius = radius,
|
|
};
|
|
Sphere? sphere1 = iteration % 2 == 0
|
|
? new Sphere
|
|
{
|
|
Origin = center + Vector3.UnitZ * (radius * 1.75f),
|
|
Radius = radius,
|
|
}
|
|
: null;
|
|
Vector3 currentCenter = center - movement;
|
|
Transition graphTransition =
|
|
SeedTransition(sphere0, sphere1, currentCenter);
|
|
Transition flatTransition =
|
|
SeedTransition(sphere0, sphere1, currentCenter);
|
|
ConfigureRandomPath(graphTransition, iteration);
|
|
ConfigureRandomPath(flatTransition, iteration);
|
|
float angle =
|
|
NextFloat(random, -MathF.PI, MathF.PI);
|
|
Quaternion localToWorld =
|
|
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, angle);
|
|
Vector3 localSpaceZ = Vector3.Transform(
|
|
Vector3.UnitZ,
|
|
Quaternion.Conjugate(localToWorld));
|
|
float scale = (iteration % 3) switch
|
|
{
|
|
0 => 0.5f,
|
|
1 => 1f,
|
|
_ => 2f,
|
|
};
|
|
Vector3 worldOrigin = new(
|
|
NextFloat(random, -200f, 200f),
|
|
NextFloat(random, -200f, 200f),
|
|
NextFloat(random, -20f, 200f));
|
|
|
|
TransitionState graphState = BSPQuery.FindCollisions(
|
|
source.BSP?.Root,
|
|
source.Resolved,
|
|
graphTransition,
|
|
sphere0,
|
|
sphere1,
|
|
currentCenter,
|
|
localSpaceZ,
|
|
scale,
|
|
localToWorld,
|
|
engine: null,
|
|
worldOrigin);
|
|
TransitionState flatState = FlatBspQuery.FindCollisions(
|
|
flat,
|
|
flatTransition,
|
|
sphere0,
|
|
sphere1,
|
|
currentCenter,
|
|
localSpaceZ,
|
|
scale,
|
|
localToWorld,
|
|
engine: null,
|
|
worldOrigin);
|
|
Assert.True(
|
|
graphState == flatState,
|
|
$"cell 0x{cellId:X8}, iteration {iteration}: " +
|
|
$"collision state graph={graphState}, flat={flatState}.");
|
|
AssertTransitionEquivalent(graphTransition, flatTransition);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void CompleteResolver_FinalResultBodyAndCellMembership_MatchGraphBits()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildTwoWallLeaf();
|
|
var containmentRoot = new CellBSPNode
|
|
{
|
|
Type = BSPNodeType.Leaf,
|
|
LeafIndex = 3,
|
|
};
|
|
FlatPhysicsBsp flatPhysics =
|
|
FlatCollisionAssetBuilder.FlattenPhysicsBsp(root, resolved);
|
|
FlatCellContainmentBsp flatContainment =
|
|
FlatCollisionAssetBuilder.FlattenCellContainmentBsp(
|
|
containmentRoot);
|
|
const uint CellId = 0xA9B4_0157u;
|
|
|
|
CellPhysics CreateCell() => new()
|
|
{
|
|
BSP = new PhysicsBSPTree { Root = root },
|
|
Resolved = resolved,
|
|
WorldTransform = Matrix4x4.Identity,
|
|
InverseWorldTransform = Matrix4x4.Identity,
|
|
CellBSP = new CellBSPTree { Root = containmentRoot },
|
|
FlatPhysicsBsp = flatPhysics,
|
|
FlatContainmentBsp = flatContainment,
|
|
};
|
|
|
|
PhysicsEngine graphEngine = CreateEngine(
|
|
CellId,
|
|
CreateCell(),
|
|
CollisionTraversalMode.Graph);
|
|
PhysicsEngine flatEngine = CreateEngine(
|
|
CellId,
|
|
CreateCell(),
|
|
CollisionTraversalMode.Flat);
|
|
var random = new Random(0x5245_534F);
|
|
|
|
for (int sequence = 0; sequence < 100; sequence++)
|
|
{
|
|
PhysicsBody graphBody = CreateGroundedBody(CellId);
|
|
PhysicsBody flatBody = CreateGroundedBody(CellId);
|
|
Vector3 position = new(
|
|
NextFloat(random, -1.5f, 1.5f),
|
|
NextFloat(random, 0.15f, 0.8f),
|
|
0f);
|
|
graphBody.SnapToCell(CellId, position, position);
|
|
flatBody.SnapToCell(CellId, position, position);
|
|
|
|
for (int frame = 0; frame < 12; frame++)
|
|
{
|
|
Vector3 movement = (frame % 4) switch
|
|
{
|
|
0 => new Vector3(0.08f, -0.12f, 0f),
|
|
1 => new Vector3(-0.05f, -0.08f, 0f),
|
|
2 => new Vector3(0.03f, 0.1f, 0f),
|
|
_ => new Vector3(
|
|
NextFloat(random, -0.1f, 0.1f),
|
|
NextFloat(random, -0.15f, 0.15f),
|
|
0f),
|
|
};
|
|
Vector3 target = position + movement;
|
|
|
|
ResolveResult graphResult = graphEngine.ResolveWithTransition(
|
|
position,
|
|
target,
|
|
CellId,
|
|
sphereRadius: 0.48f,
|
|
sphereHeight: 1.835f,
|
|
stepUpHeight: 0.1f,
|
|
stepDownHeight: 0.04f,
|
|
isOnGround: true,
|
|
graphBody,
|
|
moverFlags: ObjectInfoState.IsPlayer);
|
|
ResolveResult flatResult = flatEngine.ResolveWithTransition(
|
|
position,
|
|
target,
|
|
CellId,
|
|
sphereRadius: 0.48f,
|
|
sphereHeight: 1.835f,
|
|
stepUpHeight: 0.1f,
|
|
stepDownHeight: 0.04f,
|
|
isOnGround: true,
|
|
flatBody,
|
|
moverFlags: ObjectInfoState.IsPlayer);
|
|
|
|
AssertValueBits(
|
|
$"sequence[{sequence}].frame[{frame}].result",
|
|
graphResult,
|
|
flatResult);
|
|
AssertValueBits(
|
|
$"sequence[{sequence}].frame[{frame}].body",
|
|
graphBody,
|
|
flatBody);
|
|
Assert.Equal(graphResult.CellId, flatResult.CellId);
|
|
Assert.Equal(CellId, flatResult.CellId);
|
|
position = graphResult.Position;
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void WarmedFlatTraversal_AllocatesZeroBytes()
|
|
{
|
|
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
|
|
BuildFloorLeaf();
|
|
FlatPhysicsBsp flat =
|
|
FlatCollisionAssetBuilder.FlattenPhysicsBsp(root, resolved);
|
|
Sphere sphere = SphereAt(new Vector3(0f, 0f, 0.4f));
|
|
var transition = new Transition();
|
|
|
|
void Execute()
|
|
{
|
|
transition.ResetForReuse();
|
|
transition.SpherePath.WalkableAllowance = PhysicsGlobals.FloorZ;
|
|
transition.SpherePath.WalkInterp = 1f;
|
|
transition.SpherePath.StepDown = true;
|
|
transition.SpherePath.StepDownAmt = 0.5f;
|
|
transition.SpherePath.CheckCellId = 0xA9B4_0157u;
|
|
_ = FlatBspQuery.FindCollisions(
|
|
flat,
|
|
transition,
|
|
sphere,
|
|
null,
|
|
sphere.Origin,
|
|
Vector3.UnitZ,
|
|
1f,
|
|
Quaternion.Identity,
|
|
engine: null,
|
|
worldOrigin: Vector3.Zero);
|
|
_ = FlatBspQuery.SphereIntersectsPoly(
|
|
flat,
|
|
sphere.Origin,
|
|
sphere.Radius,
|
|
out _,
|
|
out _);
|
|
_ = FlatBspQuery.SphereIntersectsPolyWithTime(
|
|
flat,
|
|
sphere.Origin,
|
|
sphere.Radius,
|
|
-Vector3.UnitZ * 0.1f,
|
|
out _,
|
|
out _,
|
|
out _);
|
|
}
|
|
|
|
for (int i = 0; i < 100; i++)
|
|
Execute();
|
|
|
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
|
for (int i = 0; i < 10_000; i++)
|
|
Execute();
|
|
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
|
|
|
Assert.Equal(0, allocated);
|
|
}
|
|
|
|
private static PhysicsEngine CreateEngine(
|
|
uint cellId,
|
|
CellPhysics cell,
|
|
CollisionTraversalMode mode)
|
|
{
|
|
var cache = new PhysicsDataCache
|
|
{
|
|
CollisionTraversalMode = mode,
|
|
};
|
|
cache.RegisterCellStructForTest(cellId, cell);
|
|
var engine = new PhysicsEngine
|
|
{
|
|
DataCache = cache,
|
|
};
|
|
var heights = new byte[81];
|
|
var heightTable = new float[256];
|
|
for (int i = 0; i < heightTable.Length; i++)
|
|
heightTable[i] = i;
|
|
engine.AddLandblock(
|
|
0xA9B4_FFFFu,
|
|
new TerrainSurface(heights, heightTable),
|
|
Array.Empty<CellSurface>(),
|
|
Array.Empty<PortalPlane>(),
|
|
0f,
|
|
0f);
|
|
return engine;
|
|
}
|
|
|
|
private static PhysicsBody CreateGroundedBody(uint cellId)
|
|
{
|
|
var body = new PhysicsBody
|
|
{
|
|
ContactPlaneValid = true,
|
|
ContactPlane = new Plane(Vector3.UnitZ, 0f),
|
|
ContactPlaneCellId = cellId,
|
|
TransientState =
|
|
TransientStateFlags.Contact |
|
|
TransientStateFlags.OnWalkable,
|
|
State = PhysicsStateFlags.Gravity,
|
|
};
|
|
return body;
|
|
}
|
|
|
|
private static void AssertStaticAndSweptEquivalent(
|
|
PhysicsBSPNode? root,
|
|
Dictionary<ushort, ResolvedPolygon> resolved,
|
|
FlatPhysicsBsp flat,
|
|
Vector3 center,
|
|
float radius,
|
|
Vector3 movement,
|
|
uint cellId,
|
|
int iteration)
|
|
{
|
|
bool graphStatic = BSPQuery.SphereIntersectsPoly(
|
|
root,
|
|
resolved,
|
|
center,
|
|
radius,
|
|
out ushort graphStaticId,
|
|
out Vector3 graphStaticNormal);
|
|
bool flatStatic = FlatBspQuery.SphereIntersectsPoly(
|
|
flat,
|
|
center,
|
|
radius,
|
|
out ushort flatStaticId,
|
|
out Vector3 flatStaticNormal);
|
|
Assert.True(
|
|
graphStatic == flatStatic,
|
|
$"cell 0x{cellId:X8}, iteration {iteration}: static result.");
|
|
Assert.Equal(graphStaticId, flatStaticId);
|
|
AssertVectorBits(graphStaticNormal, flatStaticNormal);
|
|
|
|
bool graphSwept = BSPQuery.SphereIntersectsPolyWithTime(
|
|
root,
|
|
resolved,
|
|
center,
|
|
radius,
|
|
movement,
|
|
out ushort graphSweptId,
|
|
out Vector3 graphSweptNormal,
|
|
out float graphTime);
|
|
bool flatSwept = FlatBspQuery.SphereIntersectsPolyWithTime(
|
|
flat,
|
|
center,
|
|
radius,
|
|
movement,
|
|
out ushort flatSweptId,
|
|
out Vector3 flatSweptNormal,
|
|
out float flatTime);
|
|
Assert.True(
|
|
graphSwept == flatSwept,
|
|
$"cell 0x{cellId:X8}, iteration {iteration}: swept result.");
|
|
Assert.Equal(graphSweptId, flatSweptId);
|
|
AssertVectorBits(graphSweptNormal, flatSweptNormal);
|
|
AssertFloatBits(graphTime, flatTime);
|
|
}
|
|
|
|
private static void ConfigureRandomPath(
|
|
Transition transition,
|
|
int iteration)
|
|
{
|
|
switch (iteration % 6)
|
|
{
|
|
case 0:
|
|
transition.SpherePath.InsertType = InsertType.Placement;
|
|
break;
|
|
case 1:
|
|
transition.SpherePath.CheckWalkable = true;
|
|
break;
|
|
case 2:
|
|
transition.SpherePath.StepDown = true;
|
|
transition.SpherePath.StepDownAmt = 0.5f;
|
|
break;
|
|
case 3:
|
|
transition.SpherePath.Collide = true;
|
|
break;
|
|
case 4:
|
|
transition.ObjectInfo.State = ObjectInfoState.Contact;
|
|
break;
|
|
default:
|
|
if ((iteration & 12) == 12)
|
|
{
|
|
transition.ObjectInfo.State =
|
|
ObjectInfoState.PathClipped |
|
|
ObjectInfoState.PerfectClip;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static void AssertFindCollisionsEquivalent(
|
|
PhysicsBSPNode root,
|
|
Dictionary<ushort, ResolvedPolygon> resolved,
|
|
Sphere sphere0,
|
|
Sphere? sphere1,
|
|
Vector3 localCurrentCenter,
|
|
Action<Transition>? configure,
|
|
Vector3 worldOrigin = default)
|
|
{
|
|
FlatPhysicsBsp flat =
|
|
FlatCollisionAssetBuilder.FlattenPhysicsBsp(root, resolved);
|
|
Transition graphTransition =
|
|
SeedTransition(sphere0, sphere1, localCurrentCenter);
|
|
Transition flatTransition =
|
|
SeedTransition(sphere0, sphere1, localCurrentCenter);
|
|
configure?.Invoke(graphTransition);
|
|
configure?.Invoke(flatTransition);
|
|
|
|
TransitionState graphState = BSPQuery.FindCollisions(
|
|
root,
|
|
resolved,
|
|
graphTransition,
|
|
sphere0,
|
|
sphere1,
|
|
localCurrentCenter,
|
|
Vector3.UnitZ,
|
|
1f,
|
|
Quaternion.Identity,
|
|
engine: null,
|
|
worldOrigin);
|
|
TransitionState flatState = FlatBspQuery.FindCollisions(
|
|
flat,
|
|
flatTransition,
|
|
sphere0,
|
|
sphere1,
|
|
localCurrentCenter,
|
|
Vector3.UnitZ,
|
|
1f,
|
|
Quaternion.Identity,
|
|
engine: null,
|
|
worldOrigin);
|
|
|
|
Assert.Equal(graphState, flatState);
|
|
AssertTransitionEquivalent(graphTransition, flatTransition);
|
|
}
|
|
|
|
private static Transition SeedTransition(
|
|
Sphere sphere0,
|
|
Sphere? sphere1,
|
|
Vector3 localCurrentCenter)
|
|
{
|
|
Transition transition = NewTransition();
|
|
SpherePath path = transition.SpherePath;
|
|
path.NumSphere = sphere1 is null ? 1 : 2;
|
|
path.LocalSphere[0].Origin = sphere0.Origin;
|
|
path.LocalSphere[0].Radius = sphere0.Radius;
|
|
path.GlobalSphere[0].Origin = sphere0.Origin;
|
|
path.GlobalSphere[0].Radius = sphere0.Radius;
|
|
path.GlobalCurrCenter[0].Origin = localCurrentCenter;
|
|
path.GlobalCurrCenter[0].Radius = sphere0.Radius;
|
|
if (sphere1 is not null)
|
|
{
|
|
path.LocalSphere[1].Origin = sphere1.Origin;
|
|
path.LocalSphere[1].Radius = sphere1.Radius;
|
|
path.GlobalSphere[1].Origin = sphere1.Origin;
|
|
path.GlobalSphere[1].Radius = sphere1.Radius;
|
|
path.GlobalCurrCenter[1].Origin =
|
|
sphere1.Origin - (sphere0.Origin - localCurrentCenter);
|
|
path.GlobalCurrCenter[1].Radius = sphere1.Radius;
|
|
}
|
|
|
|
path.CheckPos = sphere0.Origin;
|
|
path.CheckCellId = 0xA9B4_013Fu;
|
|
path.BackupCheckPos = new Vector3(3f, 5f, 7f);
|
|
path.BackupCheckCellId = 0x8A02_016Eu;
|
|
return transition;
|
|
}
|
|
|
|
private static Transition NewTransition()
|
|
{
|
|
var transition = new Transition();
|
|
transition.SpherePath.WalkableAllowance = PhysicsGlobals.FloorZ;
|
|
transition.SpherePath.WalkInterp = 1f;
|
|
return transition;
|
|
}
|
|
|
|
private static (PhysicsBSPNode, Dictionary<ushort, ResolvedPolygon>)
|
|
BuildFloorLeaf()
|
|
{
|
|
ResolvedPolygon floor = Polygon(
|
|
7,
|
|
new Plane(Vector3.UnitZ, 0f),
|
|
new Vector3(-2f, -2f, 0f),
|
|
new Vector3(2f, -2f, 0f),
|
|
new Vector3(2f, 2f, 0f),
|
|
new Vector3(-2f, 2f, 0f));
|
|
PhysicsBSPNode root = Leaf();
|
|
root.Polygons.Add(floor.Id);
|
|
return (root, new Dictionary<ushort, ResolvedPolygon>
|
|
{
|
|
[floor.Id] = floor,
|
|
});
|
|
}
|
|
|
|
private static (PhysicsBSPNode, Dictionary<ushort, ResolvedPolygon>)
|
|
BuildTwoFloorLeaf()
|
|
{
|
|
ResolvedPolygon lower = Polygon(
|
|
9,
|
|
new Plane(Vector3.UnitZ, 0f),
|
|
new Vector3(-2f, -2f, 0f),
|
|
new Vector3(2f, -2f, 0f),
|
|
new Vector3(2f, 2f, 0f),
|
|
new Vector3(-2f, 2f, 0f));
|
|
ResolvedPolygon upper = Polygon(
|
|
4,
|
|
new Plane(Vector3.UnitZ, -0.75f),
|
|
new Vector3(-2f, -2f, 0.75f),
|
|
new Vector3(2f, -2f, 0.75f),
|
|
new Vector3(2f, 2f, 0.75f),
|
|
new Vector3(-2f, 2f, 0.75f));
|
|
PhysicsBSPNode root = Leaf();
|
|
root.Polygons.Add(lower.Id);
|
|
root.Polygons.Add(upper.Id);
|
|
return (root, new Dictionary<ushort, ResolvedPolygon>
|
|
{
|
|
[upper.Id] = upper,
|
|
[lower.Id] = lower,
|
|
});
|
|
}
|
|
|
|
private static (PhysicsBSPNode, Dictionary<ushort, ResolvedPolygon>)
|
|
BuildTwoWallLeaf()
|
|
{
|
|
ResolvedPolygon first = Polygon(
|
|
9,
|
|
new Plane(Vector3.UnitY, 0f),
|
|
new Vector3(-2f, 0f, -2f),
|
|
new Vector3(-2f, 0f, 2f),
|
|
new Vector3(2f, 0f, 2f),
|
|
new Vector3(2f, 0f, -2f));
|
|
ResolvedPolygon second = Polygon(
|
|
4,
|
|
new Plane(Vector3.UnitY, -0.1f),
|
|
new Vector3(-2f, 0.1f, -2f),
|
|
new Vector3(-2f, 0.1f, 2f),
|
|
new Vector3(2f, 0.1f, 2f),
|
|
new Vector3(2f, 0.1f, -2f));
|
|
PhysicsBSPNode root = Leaf();
|
|
root.Polygons.Add(first.Id);
|
|
root.Polygons.Add(second.Id);
|
|
return (root, new Dictionary<ushort, ResolvedPolygon>
|
|
{
|
|
[second.Id] = second,
|
|
[first.Id] = first,
|
|
});
|
|
}
|
|
|
|
private static PhysicsBSPNode Leaf() => new()
|
|
{
|
|
Type = BSPNodeType.Leaf,
|
|
BoundingSphere = new Sphere
|
|
{
|
|
Origin = Vector3.Zero,
|
|
Radius = 10_000f,
|
|
},
|
|
};
|
|
|
|
private static ResolvedPolygon Polygon(
|
|
ushort id,
|
|
Plane plane,
|
|
params Vector3[] vertices) => new()
|
|
{
|
|
Id = id,
|
|
Plane = plane,
|
|
SidesType = CullMode.None,
|
|
NumPoints = vertices.Length,
|
|
Vertices = vertices,
|
|
};
|
|
|
|
private static Sphere SphereAt(Vector3 center) => new()
|
|
{
|
|
Origin = center,
|
|
Radius = 0.48f,
|
|
};
|
|
|
|
private static float NextFloat(Random random, float minimum, float maximum)
|
|
=> minimum + (float)random.NextDouble() * (maximum - minimum);
|
|
|
|
private static void AssertTransitionEquivalent(
|
|
Transition expected,
|
|
Transition actual)
|
|
{
|
|
AssertValueBits("ObjectInfo", expected.ObjectInfo, actual.ObjectInfo);
|
|
AssertValueBits("SpherePath", expected.SpherePath, actual.SpherePath);
|
|
AssertValueBits("CollisionInfo", expected.CollisionInfo, actual.CollisionInfo);
|
|
Assert.Equal(
|
|
expected.CollisionInfo.ContactPlaneWriteCount,
|
|
actual.CollisionInfo.ContactPlaneWriteCount);
|
|
}
|
|
|
|
private static void AssertValueBits(
|
|
string path,
|
|
object? expected,
|
|
object? actual)
|
|
{
|
|
if (expected is null || actual is null)
|
|
{
|
|
Assert.True(
|
|
expected is null && actual is null,
|
|
$"{path}: one value is null.");
|
|
return;
|
|
}
|
|
|
|
Type expectedType = expected.GetType();
|
|
Assert.Equal(expectedType, actual.GetType());
|
|
|
|
if (expected is float expectedFloat &&
|
|
actual is float actualFloat)
|
|
{
|
|
AssertFloatBits(expectedFloat, actualFloat, path);
|
|
return;
|
|
}
|
|
|
|
if (expected is Vector3 expectedVector &&
|
|
actual is Vector3 actualVector)
|
|
{
|
|
AssertVectorBits(expectedVector, actualVector, path);
|
|
return;
|
|
}
|
|
|
|
if (expected is Quaternion expectedQuaternion &&
|
|
actual is Quaternion actualQuaternion)
|
|
{
|
|
AssertFloatBits(
|
|
expectedQuaternion.X,
|
|
actualQuaternion.X,
|
|
$"{path}.X");
|
|
AssertFloatBits(
|
|
expectedQuaternion.Y,
|
|
actualQuaternion.Y,
|
|
$"{path}.Y");
|
|
AssertFloatBits(
|
|
expectedQuaternion.Z,
|
|
actualQuaternion.Z,
|
|
$"{path}.Z");
|
|
AssertFloatBits(
|
|
expectedQuaternion.W,
|
|
actualQuaternion.W,
|
|
$"{path}.W");
|
|
return;
|
|
}
|
|
|
|
if (expected is Plane expectedPlane &&
|
|
actual is Plane actualPlane)
|
|
{
|
|
AssertVectorBits(
|
|
expectedPlane.Normal,
|
|
actualPlane.Normal,
|
|
$"{path}.Normal");
|
|
AssertFloatBits(expectedPlane.D, actualPlane.D, $"{path}.D");
|
|
return;
|
|
}
|
|
|
|
if (expectedType.IsEnum ||
|
|
expectedType.IsPrimitive ||
|
|
expected is string ||
|
|
expected is decimal)
|
|
{
|
|
Assert.True(
|
|
expected.Equals(actual),
|
|
$"{path}: expected {expected}, actual {actual}.");
|
|
return;
|
|
}
|
|
|
|
if (expected is IEnumerable expectedEnumerable &&
|
|
actual is IEnumerable actualEnumerable)
|
|
{
|
|
object?[] expectedItems = expectedEnumerable.Cast<object?>().ToArray();
|
|
object?[] actualItems = actualEnumerable.Cast<object?>().ToArray();
|
|
Assert.Equal(expectedItems.Length, actualItems.Length);
|
|
for (int i = 0; i < expectedItems.Length; i++)
|
|
{
|
|
AssertValueBits(
|
|
$"{path}[{i}]",
|
|
expectedItems[i],
|
|
actualItems[i]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
FieldInfo[] fields = expectedType
|
|
.GetFields(BindingFlags.Instance | BindingFlags.Public)
|
|
.OrderBy(field => field.Name, StringComparer.Ordinal)
|
|
.ToArray();
|
|
PropertyInfo[] properties = expectedType
|
|
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
|
.Where(property =>
|
|
property.CanRead &&
|
|
property.GetIndexParameters().Length == 0)
|
|
.OrderBy(property => property.Name, StringComparer.Ordinal)
|
|
.ToArray();
|
|
Assert.True(
|
|
fields.Length != 0 || properties.Length != 0,
|
|
$"{path}: unsupported comparison type {expectedType.FullName}.");
|
|
|
|
foreach (FieldInfo field in fields)
|
|
{
|
|
AssertValueBits(
|
|
$"{path}.{field.Name}",
|
|
field.GetValue(expected),
|
|
field.GetValue(actual));
|
|
}
|
|
|
|
foreach (PropertyInfo property in properties)
|
|
{
|
|
AssertValueBits(
|
|
$"{path}.{property.Name}",
|
|
property.GetValue(expected),
|
|
property.GetValue(actual));
|
|
}
|
|
}
|
|
|
|
private static void AssertVectorBits(
|
|
Vector3 expected,
|
|
Vector3 actual,
|
|
string path = "vector")
|
|
{
|
|
AssertFloatBits(expected.X, actual.X, $"{path}.X");
|
|
AssertFloatBits(expected.Y, actual.Y, $"{path}.Y");
|
|
AssertFloatBits(expected.Z, actual.Z, $"{path}.Z");
|
|
}
|
|
|
|
private static void AssertFloatBits(
|
|
float expected,
|
|
float actual,
|
|
string path = "float")
|
|
{
|
|
Assert.True(
|
|
BitConverter.SingleToInt32Bits(expected) ==
|
|
BitConverter.SingleToInt32Bits(actual),
|
|
$"{path}: expected 0x{BitConverter.SingleToInt32Bits(expected):X8}, " +
|
|
$"actual 0x{BitConverter.SingleToInt32Bits(actual):X8}.");
|
|
}
|
|
}
|