// Copyright Epic Games, Inc. All Rights Reserved. #include "HeadlessChaosTestConstraints.h" #include "HeadlessChaos.h" #include "HeadlessChaosTestUtility.h" #include "Modules/ModuleManager.h" #include "Chaos/GeometryQueries.h" #include "Chaos/HeightField.h" namespace ChaosTest { using namespace Chaos; void Raycast() { const int32 Columns = 10; const int32 Rows = 10; const FReal CountToWorldScale = 1; { TArray Heights; Heights.AddZeroed(Rows * Columns); FReal Count = 0; for(int32 Row = 0; Row < Rows; ++Row) { for(int32 Col = 0; Col < Columns; ++Col, ++Count) { Heights[Row * Columns + Col] = CountToWorldScale * Count; } } auto ComputeExpectedNormal = [&](const FVec3& Scale) { //Compute expected normal const FVec3 A(0,0,0); const FVec3 B(Scale[0],0,CountToWorldScale * Scale[2]); const FVec3 C(0,Scale[1],Columns*CountToWorldScale * Scale[2]); FVec3 ExpectedNormal = FVec3::CrossProduct((B - A), (C - A)); ExpectedNormal.SafeNormalize(); return ExpectedNormal; }; auto AlongZTest = [&](const FVec3& Scale) { TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy),TArray(),Rows,Columns,Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds //test straight down raycast Count = 0; FReal TOI; FVec3 Position,Normal; int32 FaceIdx; const FVec3 ExpectedNormal = ComputeExpectedNormal(Scale); int32 ExpectedFaceIdx = 0; for(int32 Row = 0; Row < Rows; ++Row) { for(int32 Col = 0; Col < Columns; ++Col) { const FVec3 Start(Col*Scale[0],Row * Scale[1],1000*Scale[2]); EXPECT_TRUE(Heightfield.Raycast(Start,FVec3(0,0,-1),2000*Scale[2],0,TOI,Position,Normal,FaceIdx)); EXPECT_NEAR(TOI,(1000 - Heights[Row*Columns+Col])*Scale[2],1e-2); EXPECT_VECTOR_NEAR(Position,FVec3(Col*Scale[0],Row * Scale[1],Heights[Row*Columns+Col] * Scale[2]),1e-2); EXPECT_VECTOR_NEAR(Normal,ExpectedNormal,1e-2); const FVec3 RayStart = Start + FVec3(0.2 * Scale[0], 0.1 * Scale[1], 0); const FVec3 RayDir = FVec3(0, 0, -1); const FReal RayLength = 2000 * Scale[2]; const FVec3 RayEnd = RayStart + (RayDir * RayLength); //offset in from border ever so slightly to get a clear face const bool bResult = Heightfield.Raycast(RayStart, RayDir, RayLength,0,TOI,Position,Normal,FaceIdx); if(Col + 1 == Columns || Row + 1 == Rows) { EXPECT_FALSE(bResult); //went past edge so no hit //Went past column so do not increment expected face idx } else { check(bResult == true); EXPECT_TRUE(bResult); EXPECT_EQ(FaceIdx,ExpectedFaceIdx); //each quad has two triangles, so for each column we pass two faces ExpectedFaceIdx += 2; //We hit the first triangle in the quad. Since we are going 1 quad at a time we skip 2 } // reverse the ray to test double sided FVec3 ReversePosition; FReal ReverseTOI; const bool bReverseResult = Heightfield.Raycast(RayEnd, -RayDir, RayLength, 0, ReverseTOI, ReversePosition, Normal, FaceIdx); if (Col + 1 == Columns || Row + 1 == Rows) { EXPECT_FALSE(bReverseResult); //went past edge so no hit //Went past column so do not increment expected face idx } else { check(bReverseResult == true); EXPECT_TRUE(bReverseResult); EXPECT_NEAR(RayLength, (TOI + ReverseTOI), SMALL_NUMBER); EXPECT_VECTOR_NEAR(Position, ReversePosition, SMALL_NUMBER); } } } }; AlongZTest(FVec3(1)); AlongZTest(FVec3(1,1,3)); AlongZTest(FVec3(1,1,.3)); AlongZTest(FVec3(3,1,.3)); AlongZTest(FVec3(2,.1,.3)); auto AlongXTest = [&](const FVec3& Scale) { TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy),TArray(),Rows,Columns,Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds //test along x axis Count = 0; FReal TOI; FVec3 Position,Normal; int32 FaceIdx; const FVec3 ExpectedNormal = ComputeExpectedNormal(Scale); //move from left to right and raycast down the x-axis. The Row idx indicates which cell we expect to hit for(int32 Row = 0; Row < Rows; ++Row) { for(int32 Col = 0; Col < Columns; ++Col) { const FVec3 Start(-Scale[0],Row * Scale[1],Heights[Row*Columns + Col] * Scale[2] + 0.01 * Scale[2]); const FVec3 RayStart = Start; const FVec3 RayDir = FVec3(1, 0, 0); const FReal RayLength = 2000 * Scale[0]; const FVec3 RayEnd = RayStart + (RayDir * RayLength); const bool bResult = Heightfield.Raycast(RayStart, RayDir, RayLength,0,TOI,Position,Normal,FaceIdx); if(Col + 1 == Columns) { EXPECT_FALSE(bResult); //No more columns so we shot over the final edge } else { check(bResult); EXPECT_TRUE(bResult); EXPECT_NEAR(TOI,(Scale[0] * (1 + Col)),1e-1); EXPECT_VECTOR_NEAR(Position,(Start + FVec3{TOI,0,0}),1e-2); EXPECT_VECTOR_NEAR(Normal,ExpectedNormal,1e-1); } // reverse ray to test double sided FVec3 ReversePosition; FReal ReverseTOI; const bool bReverseResult = Heightfield.Raycast(RayEnd, -RayDir, RayLength, 0, ReverseTOI, ReversePosition, Normal, FaceIdx); if (Col + 1 == Columns) { EXPECT_FALSE(bReverseResult); //No more columns so we shot over the final edge } else { check(bReverseResult); EXPECT_TRUE(bReverseResult); EXPECT_VECTOR_NEAR(Normal, (ExpectedNormal*-1), 1e-1); } } } }; AlongXTest(FVec3(1)); AlongXTest(FVec3(1,1,3)); AlongXTest(FVec3(1,1,.3)); AlongXTest(FVec3(3,1,.3)); AlongXTest(FVec3(2,.1,.3)); auto AlongYTest = [&](const FVec3& Scale) { TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy),TArray(),Rows,Columns,Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds //test along y axis Count = 0; FReal TOI; FVec3 Position,Normal; int32 FaceIdx; const FVec3 ExpectedNormal = ComputeExpectedNormal(Scale); for(int32 Row = 0; Row < Rows; ++Row) { for(int32 Col = 0; Col < Columns; ++Col) { const FVec3 Start(Col * Scale[0],-Scale[1],Heights[Row*Columns + Col] * Scale[2] + 0.01 * Scale[2]); const FVec3 RayStart = Start; const FVec3 RayDir = FVec3(0, 1, 0); const FReal RayLength = 2000 * Scale[0]; const FVec3 RayEnd = RayStart + (RayDir * RayLength); const bool bResult = Heightfield.Raycast(RayStart, RayDir, RayLength,0,TOI,Position,Normal,FaceIdx); if(Row + 1 == Rows) { EXPECT_FALSE(bResult); //No more columns so we shot over the final edge } else { check(bResult == true); EXPECT_TRUE(bResult); EXPECT_NEAR(TOI,(Scale[1] * (1 + Row)),1e-1); EXPECT_VECTOR_NEAR(Position,(Start + FVec3{0,TOI,0}),1e-2); EXPECT_VECTOR_NEAR(Normal,ExpectedNormal,1e-1); } // reverse ray to test double sided FReal ReverseTOI; const bool bReverseResult = Heightfield.Raycast(RayEnd, -RayDir, RayLength, 0, ReverseTOI, Position, Normal, FaceIdx); if (Row + 1 == Rows) { EXPECT_FALSE(bReverseResult); //No more columns so we shot over the final edge } else { check(bReverseResult == true); EXPECT_TRUE(bReverseResult); EXPECT_VECTOR_NEAR(Normal, (ExpectedNormal * -1), 1e-1); } } } }; AlongYTest(FVec3(1)); AlongYTest(FVec3(1,1,3)); AlongYTest(FVec3(1,1,.3)); AlongYTest(FVec3(3,1,.3)); AlongYTest(FVec3(2,.1,.3)); } { //For diagonal test simply increase height on the y axis TArray Heights2; Heights2.AddZeroed(Rows*Columns); for(int32 Row = 0; Row < Rows; ++Row) { for(int32 Col = 0; Col < Columns; ++Col) { Heights2[Row * Columns + Col] = CountToWorldScale * Row; } } auto ComputeExpectedNormal2 = [&](const FVec3& Scale) { //Compute expected normal const FVec3 A(0,0,0); const FVec3 B(Scale[0],0,0); const FVec3 C(0,Scale[1],CountToWorldScale * Scale[2]); const FVec3 ExpectedNormal = FVec3::CrossProduct((B-A),(C-A)).GetUnsafeNormal(); return ExpectedNormal; }; auto AlongXYTest = [&](const FVec3& Scale) { TArray HeightsCopy = Heights2; FHeightField Heightfield(MoveTemp(HeightsCopy),TArray(),Rows,Columns,Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds //test along x-y axis FReal TOI; FVec3 Position,Normal; int32 FaceIdx; const FVec3 ExpectedNormal = ComputeExpectedNormal2(Scale); for(int32 Row = 0; Row < Rows; ++Row) { for(int32 Col = 0; Col < Columns; ++Col) { const FVec3 Start(Col * Scale[0],0,Heights2[Row*Columns + Col] * Scale[2] + 0.01 * Scale[2]); const FVec3 Dir = FVec3(1,1,0).GetUnsafeNormal(); const bool bResult = Heightfield.Raycast(Start,Dir,2000*Scale[0],0,TOI,Position,Normal,FaceIdx); //As we increase the row, fewer columns will hit because the ray will exit the heightfield const bool bShouldHit = Col + Row + 1 < Columns; EXPECT_EQ(bShouldHit,bResult); if(bResult) { EXPECT_VECTOR_NEAR(Normal,ExpectedNormal,1e-1); } } } }; AlongXYTest(FVec3(1)); auto ToCellsTest = [&](const FVec3& Scale) { //Pick cells and shoot ray at them //This should always succeed because 0,0 is the lowest and n,n is the heighest TArray HeightsCopy = Heights2; FHeightField Heightfield(MoveTemp(HeightsCopy),TArray(),Rows,Columns,Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds FReal TOI; FVec3 Position,Normal; int32 FaceIdx; const FVec3 ExpectedNormal = ComputeExpectedNormal2(Scale); const FVec3 Start(0,0,Heights2.Last() * Scale[2]); for(int32 TargetIdx = 0; TargetIdx < Heights2.Num(); ++TargetIdx) { const int32 Col = TargetIdx % Columns; const int32 Row = TargetIdx / Columns; const FVec3 EndUnscaled(Col,Row,Heights2[TargetIdx]); FVec3 End = EndUnscaled * Scale; if(Col + 1 == Columns) { //pull back slightly to avoid precision issues at edge End[0] -= 0.1 * Scale[0]; } if(Row + 1 == Rows) { //pulling back row affects Z so just skip continue; } const FVec3 Dir = (End - Start).GetUnsafeNormal(); const bool bResult = Heightfield.Raycast(Start,Dir,2000,0,TOI,Position,Normal,FaceIdx); check(bResult); EXPECT_TRUE(bResult); EXPECT_VECTOR_NEAR(Normal,ExpectedNormal,1e-1); EXPECT_VECTOR_NEAR(Position,End,1e-1); } }; ToCellsTest(FVec3(1)); ToCellsTest(FVec3(1,1,10)); ToCellsTest(FVec3(1,1,.1)); ToCellsTest(FVec3(3,1,.1)); ToCellsTest(FVec3(.3,1,.1)); } } void RaycastOnFlatHeightField() { int32 Rows = 64; int32 Columns = 64; FVec3 Scale(100.0, 100.0, 100.0); TArray Heights; Heights.AddZeroed(Rows * Columns); TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds FReal TOI; FVec3 Position, Normal; int32 FaceIdx = 0; const FVec3 Start(8224.6524537283822, 2631.7542424011549, 2265.2052028112184); const FVec3 Dir(-0.92887444870972680, -0.11019885226781448, -0.35362193299208372); EXPECT_TRUE(Heightfield.Raycast(Start, Dir, 2097152.0000000000, 0, TOI, Position, Normal, FaceIdx)); } void RaycastVariousWalkOnHeightField() { { constexpr int32 Rows = 64; constexpr int32 Columns = Rows; FVec3 Scale(1.0, 1.0, 1.0); TArray Heights; Heights.AddZeroed(Rows * Columns); // Add a mountain on the diagonal for (int32 Index = 0; Index < Columns; ++Index) { Heights[Index * Columns + Index] = 20; } constexpr int32 BigMountainIndex = Columns / 2; Heights[BigMountainIndex * Columns + BigMountainIndex] = 40; TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds FReal TOI; FVec3 Position, Normal; int32 FaceIdx = 0; // Hit the diagonal, used directly the three walks (LowRes / Fast / Slow) { const FVec3 Start(2.0, 0.0, 10.0); FVec3 Dir(0.0, 1.0, 0.0); Dir.Normalize(); const bool bResult = Heightfield.Raycast(Start, Dir, 100.0, 0, TOI, Position, Normal, FaceIdx); EXPECT_TRUE(bResult); } // Miss the diagonal, used the three walks but miss them (LowRes / Fast / Slow) (worse case) { const FVec3 Start(0.6, 0.0, 17.0); FVec3 Dir(1.0, 1.0, 0.0); Dir.Normalize(); const bool bResult = Heightfield.Raycast(Start, Dir, 100.0, 0, TOI, Position, Normal, FaceIdx); EXPECT_FALSE(bResult); } // Hit the diagonal in the middle used Low Res for few steps then Fast walk for few step and Slow walks (optimal use case) { const FVec3 Start(64.0, 0.0, 10.0); FVec3 Dir(-1.0, 1.0, 0.0); Dir.Normalize(); const bool bResult = Heightfield.Raycast(Start, Dir, 100.0, 0, TOI, Position, Normal, FaceIdx); EXPECT_TRUE(bResult); } // Miss the diagonal in the middle used Low Res for few steps then Fast walk and Slow walks, then miss, then use LowRes until the end { const FVec3 Start(63.0, 0.0, 36.0); FVec3 Dir(-1.0, 1.0, 0.0); Dir.Normalize(); const bool bResult = Heightfield.Raycast(Start, Dir, 100.0, 0, TOI, Position, Normal, FaceIdx); EXPECT_FALSE(bResult); } // Hit at the HeightField end boundary testing the LowRes Heightfield not fully filled // (HeightField size) % LowResolution => 64 % 6 = 4 { const FVec3 Start(20.0, 63.0, 10.0); FVec3 Dir(1.0, 0.0, 0.0); Dir.Normalize(); const bool bResult = Heightfield.Raycast(Start, Dir, 100.0, 0, TOI, Position, Normal, FaceIdx); EXPECT_TRUE(bResult); } // Along the diagonal without hitting, navigate in FastWalk { const FVec3 Start(4.0, 0.0, 10.0); FVec3 Dir(1.0, 1.0, 0.0); Dir.Normalize(); const bool bResult = Heightfield.Raycast(Start, Dir, 100.0, 0, TOI, Position, Normal, FaceIdx); EXPECT_FALSE(bResult); } } { constexpr int32 Rows = 64; constexpr int32 Columns = Rows; FVec3 Scale(1.0, 1.0, 1.0); TArray Heights; Heights.AddZeroed(Rows * Columns); // Add a mountain close to the edge for (int32 Index = 0; Index < Columns; ++Index) { Heights[Index * Columns + 62] = 20; } TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds FReal TOI; FVec3 Position, Normal; int32 FaceIdx = 0; // Hit at the HeightField end boundary testing the LowRes Heightfield not fully filled // (HeightField size) % LowResolution => 64 % 6 = 4 // Jira UE-162256 { const FVec3 Start(58.0, 63.0, 10.0); FVec3 Dir(1.0, 0.0, 0.0); Dir.Normalize(); const bool bResult = Heightfield.Raycast(Start, Dir, 100.0, 0, TOI, Position, Normal, FaceIdx); EXPECT_TRUE(bResult); } } // Bug found in Fortnite: The raycast is falling in infinite loop if raycast in mode WalkFast // and leave the bounding volume of the HeightField { constexpr int32 Rows = 64; constexpr int32 Columns = Rows; FVec3 Scale(1.0, 1.0, 1.0); TArray Heights; Heights.AddZeroed(Rows * Columns); // Add a mountain in the middle for (int32 Index = 0; Index < Columns; ++Index) { Heights[Index * Columns + 32] = 20; } TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds FReal TOI; FVec3 Position, Normal; int32 FaceIdx = 0; { const FVec3 Start(34.0, 0.0, 10.0); FVec3 Dir(0.0, 1.0, 0.0); Dir.Normalize(); const bool bResult = Heightfield.Raycast(Start, Dir, 100.0, 0, TOI, Position, Normal, FaceIdx); EXPECT_FALSE(bResult); } } } void EditHeights() { const int32 Columns = 10; const int32 Rows = 10; const uint16 InitialHeight = 32768; // Real height = 0, (half of uint16 max of 65535) TArray Heights; Heights.AddZeroed(Rows * Columns); // Stolen from Heightfield.cpp TUniqueFunction ConversionFunc = [](const FReal InVal) -> FReal { return (FReal)((int32)InVal - 32768); }; for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { Heights[Row * Columns + Col] = InitialHeight; } } FVec3 Scale(1, 1, 1); FHeightField Heightfield(MoveTemp(Heights),TArray(),Rows,Columns,Scale); // Test is intended to catch trivial regressions in EditGeomData, // specifically handling Landscape module providing buffer with column index inverted from heightfield whe editing. int32 InRows = 1; int32 InCols = 3; TArray ModifiedHeights; ModifiedHeights.Init(InitialHeight, InRows * InCols); int32 Row = 0; int32 Col = 0; ModifiedHeights[Row * (InCols) + Col] = 35000; Col = 1; ModifiedHeights[Row * (InCols) + Col] = 40000; Col = 2; ModifiedHeights[Row * (InCols) + Col] = 45000; FReal ExpectedMaxRealHeight = ConversionFunc(45000); FReal ExpectedMinRealHeight = ConversionFunc(InitialHeight); FReal ExpectedRange = ExpectedMaxRealHeight - ExpectedMinRealHeight; int32 RowBegin = 3; int32 ColBegin = 4; // Expectation is that all values are at default, and ModifiedHeights is applied to heightfield // starting at (ColBegin, RowBegin) to (ColBegin + InCols, RowBegin + InRows). // Landscape module provides buffer with col idx inverted, so they are expected to be written reverse order over columns within this range. // The Begin indices however are not inverted. These match heightfield. // If this seems confusing, that's because it is. -MaxW Heightfield.EditHeights(ModifiedHeights, RowBegin, ColBegin, InRows, InCols); auto& GeomData = Heightfield.GeomData; // Validate heights using 2d iteration scheme for (int32 RowIdx = 0; RowIdx < Rows; ++RowIdx) { for (int32 ColIdx = 0; ColIdx < Columns; ++ColIdx) { if (RowIdx >= RowBegin && RowIdx < RowBegin + InRows && ColIdx >= ColBegin && ColIdx < ColBegin + InCols) { // This is in modified range int32 ModifiedRowIdx = RowIdx - RowBegin; int32 ModifiedColIdx = ColIdx - ColBegin; int32 ModifiedIdx = ModifiedRowIdx * InCols + ModifiedColIdx; int32 HeightIdx = RowIdx * Columns + (Columns - 1 - ColIdx); // Remember that modified heights buffer uses inverted col index FReal HeightReal = GeomData.MinValue + GeomData.Heights[HeightIdx] * GeomData.HeightPerUnit; uint16 ModifiedHeight = ModifiedHeights[ModifiedIdx]; FReal ModifiedHeightReal = ConversionFunc(ModifiedHeight); EXPECT_NEAR(ModifiedHeightReal, HeightReal, 1); } else { int32 HeightIdx = RowIdx * Columns + (Columns - 1 - ColIdx); // Remember that modified heights buffer uses inverted col index float HeightReal = GeomData.MinValue + GeomData.Heights[HeightIdx] * GeomData.HeightPerUnit; float InitialHeightReal = ConversionFunc(InitialHeight); EXPECT_NEAR(InitialHeightReal, HeightReal, 0.0001f); } } } EXPECT_EQ(GeomData.MinValue, ExpectedMinRealHeight); EXPECT_EQ(GeomData.MaxValue, ExpectedMaxRealHeight); EXPECT_EQ(GeomData.Range, ExpectedRange); EXPECT_EQ(GeomData.HeightPerUnit, (ExpectedRange) / TNumericLimits::Max()); // Range over uint16 max (65535) } void SweepSmallSphereTest() { const int32 Columns = 10; const int32 Rows = 10; const FReal CountToWorldScale = 1; Chaos::FSphere Sphere(FVec3(0.0, 0.0, 0.0), 1.0); { TArray Heights; Heights.AddZeroed(Rows * Columns); FReal Count = 0; for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col, ++Count) { Heights[Row * Columns + Col] = CountToWorldScale * Count; } } auto AlongZTest = [&](const FVec3& Scale) { TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds //test straight down sweep Count = 0; FReal TOI; FVec3 Position, Normal, FaceNormal; int32 FaceIdx; int32 ExpectedFaceIdx = 0; for (int32 Row = 0; Row < Rows-1; ++Row) { for (int32 Col = 0; Col < Columns-1; ++Col) { const FVec3 Start(Col * Scale[0] + 0.5, Row * Scale[1]+0.5, 1000 * Scale[2]); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(0, 0, -1); bool Result = Heightfield.SweepGeom(Sphere, StartTM, Dir, 2000 * Scale[2], TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } } }; AlongZTest(FVec3(1)); AlongZTest(FVec3(1, 1, 3)); AlongZTest(FVec3(1, 1, .3)); auto AlongXTest = [&](const FVec3& Scale) { TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds //test straight down sweep Count = 0; FReal TOI; FVec3 Position, Normal, FaceNormal; int32 FaceIdx; int32 ExpectedFaceIdx = 0; for (int32 Row = 0; Row < Rows - 1; ++Row) { for (int32 Col = 0; Col < Columns - 1; ++Col) { const FVec3 Start(-Scale[0], Row * Scale[1], Heights[Row * Columns + Col] * Scale[2] + 0.01 * Scale[2]); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1, 0, 0); bool Result = Heightfield.SweepGeom(Sphere, StartTM, Dir, 2000 * Scale[0], TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } } }; AlongXTest(FVec3(1)); AlongXTest(FVec3(1, 1, 3)); AlongXTest(FVec3(1, 1, .3)); } } void SweepBigSphereTest() { const int32 Columns = 10; const int32 Rows = 10; const FReal CountToWorldScale = 100; Chaos::FSphere Sphere(FVec3(0.0, 0.0, 0.0), 250.0); TArray Heights; Heights.AddZeroed(Rows * Columns); for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { if (Col == 5) { Heights[Row * Columns + Col] = 10; } else { Heights[Row * Columns + Col] = 0; } } } TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, FVec3(100, 100, 100)); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds FReal TOI; FVec3 Position, Normal, FaceNormal; int32 FaceIdx; { const FVec3 Start(200, 500, 700); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1.0, -0.1, 0.0); Dir.Normalize(); bool Result = Heightfield.SweepGeom(Sphere, StartTM, Dir, 50, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } // Test extent in the opposite direction of the sweep { const FVec3 Start(350, 500, 700); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(-1.0, 0.1, 0.0); Dir.Normalize(); bool Result = Heightfield.SweepGeom(Sphere, StartTM, Dir, 50, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } { const FVec3 Start(180, 500, 700); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1.0, 0.0, -1.0); Dir.Normalize(); bool Result = Heightfield.SweepGeom(Sphere, StartTM, Dir, 50, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } // Test extent in the opposite direction of the sweep { const FVec3 Start(220, 500, 700); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(-1.0, 0.0, 1.0); Dir.Normalize(); bool Result = Heightfield.SweepGeom(Sphere, StartTM, Dir, 50, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } } void SweepSmallBoxTest() { const int32 Columns = 10; const int32 Rows = 10; const FReal CountToWorldScale = 1; TBox Box(FVec3(-0.05, -0.05, -0.05), FVec3(0.05, 0.05, 0.05)); { TArray Heights; Heights.AddZeroed(Rows * Columns); FReal Count = 0; for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col, ++Count) { Heights[Row * Columns + Col] = CountToWorldScale * Count; } } auto AlongZTest = [&](const FVec3& Scale) { TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds //test straight down sweep Count = 0; FReal TOI; FVec3 Position, Normal, FaceNormal; int32 FaceIdx; int32 ExpectedFaceIdx = 0; for (int32 Row = 0; Row < Rows - 1; ++Row) { for (int32 Col = 0; Col < Columns - 1; ++Col) { const FVec3 Start(Col * Scale[0] + 0.5, Row * Scale[1] + 0.5, 1000 * Scale[2]); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(0, 0, -1); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 2000 * Scale[2], TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } } }; AlongZTest(FVec3(1)); AlongZTest(FVec3(1, 1, 3)); AlongZTest(FVec3(1, 1, .3)); auto AlongXTest = [&](const FVec3& Scale) { TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds //test straight down sweep Count = 0; FReal TOI; FVec3 Position, Normal, FaceNormal; int32 FaceIdx; int32 ExpectedFaceIdx = 0; for (int32 Row = 0; Row < Rows - 1; ++Row) { for (int32 Col = 0; Col < Columns - 1; ++Col) { const FVec3 Start(-Scale[0], Row * Scale[1], Heights[Row * Columns + Col] * Scale[2] + 0.01 * Scale[2]); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1, 0, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 2000 * Scale[0], TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } } }; AlongXTest(FVec3(1)); AlongXTest(FVec3(1, 1, 3)); AlongXTest(FVec3(1, 1, .3)); } } TArray CreateMountain(int32 Columns, int32 Rows) { TArray Heights; Heights.AddZeroed(Rows * Columns); // Plane for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { Heights[Row * Columns + Col] = 0.0; } } // Mountain for (int32 Row = Rows / 2 - 1; Row <= Rows / 2 + 1; ++Row) { for (int32 Col = Columns / 2 - 1; Col <= Columns / 2 + 1; ++Col) { Heights[Row * Columns + Col] = 5.0; } } // Summit Heights[(Rows / 2) * Columns + Columns / 2] = 10.0; return Heights; } void SweepBoxTest() { const FReal CountToWorldScale = 1; const int32 Columns = 10; const int32 Rows = 10; TBox Box(FVec3(-1.0, -1.0, -1.0), FVec3(1.0, 1.0, 1.0)); TArray Heights = CreateMountain(Columns, Rows); TArray HeightsCopy = Heights; FVec3 Scale(1.0, 1.0, 1.0); FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds FReal TOI; FVec3 Position, Normal, FaceNormal; int32 FaceIdx; int32 ExpectedFaceIdx = 0; { // Miss on one side of the mountain const FVec3 Start(0.0, 3.5, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1, 0, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_FALSE(Result); } { // Miss on the other side of the mountain const FVec3 Start(0.0, 7.0, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1, 0, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_FALSE(Result); } // TODO This unit test is not working, to investigate { // Same missing in Y const FVec3 Start(3.0, 0.0, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(0, 1, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_FALSE(Result); } { const FVec3 Start(7.0, 0.0, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(0, 1, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_FALSE(Result); } // Hit on the side of the box { const FVec3 Start(0.0, 4.0, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1, 0, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } { const FVec3 Start(0.0, 6.0, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1, 0, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } { const FVec3 Start(4.0, 0.0, 10.0); FRigidTransform3 StartTM(Start, TRotation()); FVec3 Dir(0, 1, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } { const FVec3 Start(6.0, 0.0, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(0, 1, 0); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } // Hit in Diagonal { const FVec3 Start(0.5, 0.5, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(1, 1, 0); Dir.SafeNormalize(); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } { const FVec3 Start(10.5, 10.5, 10.0); FRigidTransform3 StartTM(Start, TRotation::Identity); FVec3 Dir(-1, -1, 0); Dir.SafeNormalize(); bool Result = Heightfield.SweepGeom(Box, StartTM, Dir, 100, TOI, Position, Normal, FaceIdx, FaceNormal, 0.0, true); EXPECT_TRUE(Result); } } void SweepTest() { SweepSmallSphereTest(); SweepBigSphereTest(); SweepSmallBoxTest(); SweepBoxTest(); } void OverlapConsistentTest() { const FReal CountToWorldScale = 1; const int32 Columns = 10; const int32 Rows = 10; TArray Heights = CreateMountain(Columns, Rows); TArray HeightsCopy = Heights; FVec3 Scale(1.0, 1.0, 1.0); FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds { TBox Box(FVec3(-0.5, -0.5, -0.5), FVec3(0.5, 0.5, 0.5)); FCapsule Capsule(FVec3(0.0, 0.0, 0.0), FVec3(0.0, 0.0, 9.0), 1.14); Chaos::FSphere Sphere1(FVec3(0.0, 0.0, -2.0), 0.6); FMTDInfo OutBoxMTD; FMTDInfo OutCapsuleMTD; FMTDInfo OutSphereMTD; for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { for (FReal Height = 0; Height < 12; Height+=0.5) { const FVec3 Translation(Col, Row, Height); FRigidTransform3 QueryTM(Translation, TRotation::Identity); FVec3 Dir(1, 0, 0); bool BoxResult = Heightfield.OverlapGeom(Box, QueryTM, 0.0, nullptr); bool BoxResultMTD = Heightfield.OverlapGeom(Box, QueryTM, 0.0, &OutBoxMTD); EXPECT_EQ(BoxResult, BoxResultMTD); bool CapsuleResult = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, nullptr); bool CapsuleResultMTD = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, &OutCapsuleMTD); EXPECT_EQ(CapsuleResult, CapsuleResultMTD); bool SphereResult = Heightfield.OverlapGeom(Sphere1, QueryTM, 0.0, nullptr); bool SphereResultMTD = Heightfield.OverlapGeom(Sphere1, QueryTM, 0.0, &OutSphereMTD); EXPECT_EQ(SphereResult, SphereResultMTD); } } } } // Testing capsule upside down { FCapsule Capsule(FVec3(0.0, 0.0, 9.0), FVec3(0.0, 0.0, 0.0), 1.14); FMTDInfo OutCapsuleMTD; for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { for (FReal Height = 0; Height < 12; Height += 0.5) { const FVec3 Translation(Col, Row, Height); FRigidTransform3 QueryTM(Translation, TRotation::Identity); FVec3 Dir(1, 0, 0); bool CapsuleResult = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, nullptr); bool CapsuleResultMTD = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, &OutCapsuleMTD); EXPECT_EQ(CapsuleResult, CapsuleResultMTD); } } } } } void OverlapTest() { const FReal CountToWorldScale = 1; const int32 Columns = 10; const int32 Rows = 10; TArray Heights = CreateMountain(Columns, Rows); TArray HeightsCopy = Heights; FVec3 Scale(1.0, 1.0, 1.0); FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds // Long box { TBox Box(FVec3(-1.0, -1.0, -1.0), FVec3(1.0, 1.0, 10.0)); FCapsule Capsule(FVec3(0.0, 0.0, 0.0), FVec3(0.0, 0.0, 9.0), 1.14); Chaos::FSphere Sphere1(FVec3(0.0, 0.0, -2.0), 0.6); for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { const FVec3 Translation(Col, Row, 1.5); FRigidTransform3 QueryTM(Translation, TRotation::Identity); FVec3 Dir(1, 0, 0); bool BoxResult = Heightfield.OverlapGeom(Box, QueryTM, 0.0, nullptr); bool CapsuleResult = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, nullptr); bool SphereResult = Heightfield.OverlapGeom(Sphere1, QueryTM, 0.0, nullptr); // No collision on the side of the mountain if ((Col < 3 || Col > 7) && (Row < 3 || Row > 7)) { EXPECT_FALSE(BoxResult); EXPECT_FALSE(CapsuleResult); // Sphere on the floor EXPECT_TRUE(SphereResult); } // Collision with the mountain else if ((Col >= 3 && Col <= 7) && (Row >= 3 && Row <= 7)) { EXPECT_TRUE(BoxResult); EXPECT_TRUE(CapsuleResult); } else { EXPECT_FALSE(BoxResult); } // Inside the mountain the sphere shouldn't collide if ((Col > 3 && Col < 7) && (Row > 3 && Row < 7)) { EXPECT_FALSE(SphereResult); } } } } // Box rotated in X { TBox Box(FVec3(-1.0, -1.0, -20.0), FVec3(1.0, 1.0, 20.0)); FCapsule Capsule(FVec3(0.0, 0.0, -19.0), FVec3(0.0, 0.0, 19.0), 1.14); for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { const FVec3 Translation(Col, Row, 2.0); FRigidTransform3 QueryTM(Translation, TRotation(UE::Math::TQuat(TVector(1.0, 0.0, 0.0), 3.14159/2.0))); FVec3 Dir(1, 0, 0); bool BoxResult = Heightfield.OverlapGeom(Box, QueryTM, 0.0, nullptr); bool CapsuleResult = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, nullptr); // Collision with the mountain if (Col >= 3 && Col <= 7) { EXPECT_TRUE(BoxResult); EXPECT_TRUE(CapsuleResult); } else { EXPECT_FALSE(BoxResult); EXPECT_FALSE(CapsuleResult); } } } } // Box rotated in Y { TBox Box(FVec3(-1.0, -1.0, -20.0), FVec3(1.0, 1.0, 20.0)); FCapsule Capsule(FVec3(0.0, 0.0, -19.0), FVec3(0.0, 0.0, 19.0), 1.14); for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { const FVec3 Translation(Col, Row, 2.0); FRigidTransform3 QueryTM(Translation, TRotation(UE::Math::TQuat(TVector(0.0, 1.0, 0.0), 3.14159 / 2.0))); FVec3 Dir(1, 0, 0); bool BoxResult = Heightfield.OverlapGeom(Box, QueryTM, 0.0, nullptr); bool CapsuleResult = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, nullptr); // Collision with the mountain if (Row >= 3 && Row <= 7) { EXPECT_TRUE(BoxResult); EXPECT_TRUE(CapsuleResult); } else { EXPECT_FALSE(BoxResult); EXPECT_FALSE(CapsuleResult); } } } } // Thin Box { TBox Box(FVec3(-10.0, -10.0, -0.001), FVec3(10.0, 10.0, 0.001)); FCapsule Capsule(FVec3(0.0, 0.0, -0.001), FVec3(0.0, 0.0, 0.001), 11.4); for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { const FVec3 Translation(Col, Row, 2.0); FRigidTransform3 QueryTM(Translation, TRotation(UE::Math::TQuat(TVector(1.0, 0.0, 0.0), 0.0))); FVec3 Dir(1, 0, 0); bool BoxResult = Heightfield.OverlapGeom(Box, QueryTM, 0.0, nullptr); bool CapsuleResult = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, nullptr); EXPECT_TRUE(BoxResult); EXPECT_TRUE(CapsuleResult); } } } // Thin Box on the top { TBox Box(FVec3(-10.0, -10.0, -0.001), FVec3(10.0, 10.0, 0.001)); FCapsule Capsule(FVec3(0.0, 0.0, -0.001), FVec3(0.0, 0.0, 0.001), 11.4); for (int32 Row = 0; Row < Rows; ++Row) { for (int32 Col = 0; Col < Columns; ++Col) { const FVec3 Translation(Col, Row, 9.5); FRigidTransform3 QueryTM(Translation, TRotation(UE::Math::TQuat(TVector(1.0, 0.0, 0.0), 0.0))); FVec3 Dir(1, 0, 0); bool BoxResult = Heightfield.OverlapGeom(Box, QueryTM, 0.0, nullptr); bool CapsuleResult = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, nullptr); EXPECT_TRUE(BoxResult); EXPECT_TRUE(CapsuleResult); } } } // Inclined Box { TBox Box(FVec3(-1.0, -1.0, -0.0001), FVec3(1.0, 1.0, 10.0)); FCapsule Capsule(FVec3(0.0, 0.0, 1.0-0.0001), FVec3(0.0, 0.0, 9.0), 1.0); const FVec3 Translation(5.0, 0.0, 15.0); FRigidTransform3 QueryTM(Translation, TRotation(UE::Math::TQuat(TVector(1.0, 0.0, 0.0), -3*3.1415926 / 4.0))); FVec3 Dir(1, 0, 0); bool BoxResult = Heightfield.OverlapGeom(Box, QueryTM, 0.0, nullptr); bool CapsuleResult = Heightfield.OverlapGeom(Capsule, QueryTM, 0.0, nullptr); EXPECT_TRUE(BoxResult); EXPECT_TRUE(CapsuleResult); } HeightsCopy = Heights; const float UniformScale = 10.0f; FHeightField HeightfieldScaled(MoveTemp(HeightsCopy), TArray(), Rows, Columns, FVec3(UniformScale, UniformScale, UniformScale)); const auto& Bounds2 = HeightfieldScaled.BoundingBox(); //Current API forces us to do this to cache the bounds // Small sphere intersecting plane area of mountain with a scaled heightfield { Chaos::FSphere Sphere1(FVec3(0.0, 0.0, 0.0), 0.2); int32 Row = 2; int32 Col = 2; const FVec3 Translation(Col* UniformScale, Row * UniformScale, 0.1f); FRigidTransform3 QueryTM(Translation, TRotation::Identity); bool SphereResult = HeightfieldScaled.OverlapGeom(Sphere1, QueryTM, 0.0, nullptr); EXPECT_TRUE(SphereResult); } // Small convex intersecting plane area of mountain with a scaled heightfield { //Tetrahedron TArray HullParticles; HullParticles.SetNum(4); HullParticles[0] = { -1,-1,-1 }; HullParticles[1] = { 1,-1,-1 }; HullParticles[2] = { 0,1,-1 }; HullParticles[3] = { 0,0,1 }; FConvex Tet(HullParticles, 0.0f); int32 Row = 2; int32 Col = 2; const FVec3 Translation(Col * UniformScale, Row * UniformScale, 1.1f); FRigidTransform3 QueryTM(Translation, TRotation::Identity); bool TetResult = HeightfieldScaled.OverlapGeom(Tet, QueryTM, 0.2, nullptr); EXPECT_TRUE(TetResult); } } TEST(ChaosTests, Heightfield) { ChaosTest::Raycast(); ChaosTest::RaycastOnFlatHeightField(); ChaosTest::RaycastVariousWalkOnHeightField(); ChaosTest::SweepTest(); ChaosTest::OverlapTest(); ChaosTest::OverlapConsistentTest(); EditHeights(); SUCCEED(); } struct SimpleSweepFixture : public testing::Test { void ConstructFlatHeightFieldAndTestSweep(const FVec3& SweepStart, const FVec3& SweepDirection, const bool bExpectedResult, const FReal ExpectedTOI, const FVec3& ExpectedPosition, const FVec3& ExpectedNormal, const int32 ExpectedFaceId) { TArray Heights; Heights.AddZeroed(Rows * Columns); FHeightField Heightfield(MoveTemp(Heights), TArray(), Rows, Columns, Scale); const FAABB3 Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds const Chaos::FSphere Sphere(FVec3::ZeroVector, SphereRadius); const FRigidTransform3 StartTM(SweepStart, TRotation::Identity); FReal ResultTOI; FVec3 ResultPosition, ResultNormal, ResultFaceNormal; int32 ResultFaceId; bool bResult = Heightfield.SweepGeom(Sphere, StartTM, SweepDirection, SweepLength, ResultTOI, ResultPosition, ResultNormal, ResultFaceId, ResultFaceNormal, 0.0, true); EXPECT_EQ(bResult, bExpectedResult); EXPECT_VECTOR_NEAR(ResultPosition, ExpectedPosition, 0.01); EXPECT_VECTOR_NEAR(ResultNormal, ExpectedNormal, 0.01); EXPECT_EQ(ResultFaceId, ExpectedFaceId); EXPECT_NEAR(ResultTOI, ExpectedTOI, 0.01); } int32 Rows = 3; int32 Columns = 3; FVec3 Scale = FVec3(100, 100, 100); FReal SphereRadius = 25.0; FReal SweepLength = 100; }; // These tests check that when a sweep has multiple initial overlaps, the "best" result is returned instead of the first. TEST_F(SimpleSweepFixture, FlatHeightField_TestInitialOverlapSweeps) { // Test border of two triangles within the same cell ConstructFlatHeightFieldAndTestSweep(FVec3(60, 50, 10), -FVec3::ZAxisVector, true, -15, FVec3(60, 50, 0), FVec3(0, 0, 1), 0); ConstructFlatHeightFieldAndTestSweep(FVec3(50, 60, 10), -FVec3::ZAxisVector, true, -15, FVec3(50, 60, 0), FVec3(0, 0, 1), 1); // Test borders between the 4 cells ConstructFlatHeightFieldAndTestSweep(FVec3(90, 90, 10), -FVec3::ZAxisVector, true, -15, FVec3(90, 90, 0), FVec3(0, 0, 1), 0); ConstructFlatHeightFieldAndTestSweep(FVec3(110, 90, 10), -FVec3::ZAxisVector, true, -15, FVec3(110, 90, 0), FVec3(0, 0, 1), 3); ConstructFlatHeightFieldAndTestSweep(FVec3(90, 110, 10), -FVec3::ZAxisVector, true, -15, FVec3(90, 110, 0), FVec3(0, 0, 1), 4); ConstructFlatHeightFieldAndTestSweep(FVec3(110, 110, 10), -FVec3::ZAxisVector, true, -15, FVec3(110, 110, 0), FVec3(0, 0, 1), 6); // Is it worth doing test with mixed TOI / non-zero height? } // A flat heightfield and a sphere swept downwards using the CCD API. // Check that the IgnoreThreshold is being used to ignore sweeps that have a penetration depth // less that that threshold at T=1. GTEST_TEST(HeightfieldCCDTests, TestSphere) { int32 Rows = 64; int32 Columns = 64; FVec3 Scale(100.0, 100.0, 100.0); TArray Heights; Heights.AddZeroed(Rows * Columns); TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds Chaos::FSphere Sphere(FVec3(0.0, 0.0, 0.0), 50.0); FReal TOI, Phi; FVec3 Position, Normal, FaceNormal; int32 FaceIdx = 0; const FRigidTransform3 Start(FVec3(0, 0, 60), FRotation3::FromIdentity()); const FVec3 Dir(0, 0, -1); const FReal CCDIgnorePenetration = 30; const FReal CCDTargetPenetration = 0; // The first sweep should be less than the ignore threshold so get no hit const FReal CCDSweepLengthA = 30; const bool bHitA = Heightfield.SweepGeomCCD(Sphere, Start, Dir, CCDSweepLengthA, CCDIgnorePenetration, CCDTargetPenetration, TOI, Phi, Position, Normal, FaceIdx, FaceNormal); EXPECT_FALSE(bHitA); // The second sweep should be greater than the ignore threshold so we get a hit const FReal CCDSweepLengthB = 50; const bool bHitB = Heightfield.SweepGeomCCD(Sphere, Start, Dir, CCDSweepLengthB, CCDIgnorePenetration, CCDTargetPenetration, TOI, Phi, Position, Normal, FaceIdx, FaceNormal); EXPECT_TRUE(bHitB); EXPECT_NEAR(TOI, 0.2, UE_KINDA_SMALL_NUMBER); // 10 / 50 EXPECT_NEAR(Phi, 0.0, UE_KINDA_SMALL_NUMBER); // We are not initially penetrating } // Same as TestSphere except the sphere has a local offset built in. // CCD sweeps have an early out if the depth at T=1 is less than some threshold, and there was a bug in this. // related to the fact that the sweep is effectively an AABB sweep to find the overlapping heightfield cells, // and a Shape sweep to find the shape-triangle overlaps. The bug was that the AABB sweep position was being // used in the early-rejection code that should be using the shape position. GTEST_TEST(HeightfieldCCDTests, TestSphereWithOffset) { int32 Rows = 64; int32 Columns = 64; FVec3 Scale(100.0, 100.0, 100.0); TArray Heights; Heights.AddZeroed(Rows * Columns); TArray HeightsCopy = Heights; FHeightField Heightfield(MoveTemp(HeightsCopy), TArray(), Rows, Columns, Scale); const auto& Bounds = Heightfield.BoundingBox(); //Current API forces us to do this to cache the bounds // NOTE: Sphere center 50cm up Chaos::FSphere Sphere(FVec3(0.0, 0.0, 50.0), 50.0); FReal TOI, Phi; FVec3 Position, Normal, FaceNormal; int32 FaceIdx = 0; // NOTE: Start height at 10, rather than 60 in TestSphere const FRigidTransform3 Start(FVec3(0, 0, 10), FRotation3::FromIdentity()); const FVec3 Dir(0, 0, -1); const FReal CCDIgnorePenetration = 30; const FReal CCDTargetPenetration = 0; // The first sweep should be less than the ignore threshold so get no hit const FReal CCDSweepLengthA = 30; const bool bHitA = Heightfield.SweepGeomCCD(Sphere, Start, Dir, CCDSweepLengthA, CCDIgnorePenetration, CCDTargetPenetration, TOI, Phi, Position, Normal, FaceIdx, FaceNormal); EXPECT_FALSE(bHitA); // The second sweep should be greater than the ignore threshold so we get a hit const FReal CCDSweepLengthB = 50; const bool bHitB = Heightfield.SweepGeomCCD(Sphere, Start, Dir, CCDSweepLengthB, CCDIgnorePenetration, CCDTargetPenetration, TOI, Phi, Position, Normal, FaceIdx, FaceNormal); EXPECT_TRUE(bHitB); EXPECT_NEAR(TOI, 0.2, UE_KINDA_SMALL_NUMBER); // 10 / 50 EXPECT_NEAR(Phi, 0.0, UE_KINDA_SMALL_NUMBER); // We are not initially penetrating } }