// Copyright Epic Games, Inc. All Rights Reserved. #include "HeadlessChaosTestBroadphase.h" #include "HeadlessChaos.h" #include "Chaos/Box.h" #include "Chaos/BoundingVolume.h" #include "Chaos/ParticleHandle.h" #include "Chaos/PBDRigidsSOAs.h" #include "Chaos/PBDRigidsEvolutionGBF.h" #include "Chaos/AABBTree.h" #include "ChaosLog.h" #include "PBDRigidsSolver.h" #include "Chaos/SpatialAccelerationCollection.h" namespace ChaosTest { using namespace Chaos; /*In general we want to test the following for each broadphase type: - simple intersection test as used by sim (IntersectAll) - ray, sweep, overlap - miss entire structure - stop mid structure - multi overlap - multi block (adjust length) - any */ struct FVisitor : ISpatialVisitor { const FGeometryParticles& Boxes; const FVec3 Start; const FVec3 Dir; FVec3 HalfExtents; const FReal Thickness; int32 BlockAfterN; bool bAny; FVisitor(const FVec3& InStart, const FVec3& InDir, const FReal InThickness, const FGeometryParticles& InBoxes) : Boxes(InBoxes) , Start(InStart) , Dir(InDir) , HalfExtents(0) , Thickness(InThickness) , BlockAfterN(TNumericLimits::Max()) , bAny(false) {} enum class SQType { Raycast, Sweep, Overlap }; template bool Visit(int32 Idx, FQueryFastData& CurData) { const FRigidTransform3 BoxTM(Boxes.GetX(Idx), Boxes.GetR(Idx)); FAABB3 Box = static_cast*>(Boxes.GetGeometry(Idx).GetReference())->BoundingBox().TransformedAABB(BoxTM); FAABB3 ThicknedBox(Box.Min() - HalfExtents, Box.Max() + HalfExtents); FReal NewLength; FVec3 Position; FVec3 Normal; int32 FaceIndex; const FReal OldLength = CurData.CurrentLength; if (ThicknedBox.Raycast(Start, Dir, CurData.CurrentLength, 0, NewLength, Position, Normal, FaceIndex)) { Instances.Add(Idx); if (bAny) { return false; } } return true; } bool VisitRaycast(TSpatialVisitorData Idx, FQueryFastData& CurData) { return Visit(Idx.Payload, CurData); } bool VisitSweep(TSpatialVisitorData Idx, FQueryFastData& CurData) { return Visit(Idx.Payload, CurData); } bool VisitOverlap(TSpatialVisitorData Idx) { check(false); return false; } virtual bool Overlap(const TSpatialVisitorData& Instance) override { return VisitOverlap(Instance); } virtual bool Raycast(const TSpatialVisitorData& Instance, FQueryFastData& CurData) override { return VisitRaycast(Instance, CurData); } virtual bool Sweep(const TSpatialVisitorData& Instance, FQueryFastData& CurData) override { return VisitSweep(Instance, CurData); } virtual bool HasBlockingHit() const override { return Instances.Num() >= BlockAfterN; } TArray Instances; }; struct FOverlapVisitor : public ISpatialVisitor { const FGeometryParticles& Boxes; const FAABB3 Bounds; bool bAny; FOverlapVisitor(const FAABB3& InBounds, const FGeometryParticles& InBoxes) : Boxes(InBoxes) , Bounds(InBounds) , bAny(false) {} bool VisitOverlap(TSpatialVisitorData Instance) { const int32 Idx = Instance.Payload; const FRigidTransform3 BoxTM(Boxes.GetX(Idx), Boxes.GetR(Idx)); FAABB3 Box = static_cast*>(Boxes.GetGeometry(Idx).GetReference())->BoundingBox().TransformedAABB(BoxTM); if (Box.Intersects(Bounds)) { Instances.Add(Idx); if (bAny) { return false; } } return true; } bool VisitRaycast(TSpatialVisitorData Idx, FQueryFastData&) { check(false); return false; } bool VisitSweep(TSpatialVisitorData Idx, FQueryFastData&) { check(false); return false; } virtual bool Overlap(const TSpatialVisitorData& Instance) override { return VisitOverlap(Instance); } virtual bool Raycast(const TSpatialVisitorData& Instance, FQueryFastData& CurData) override { return VisitRaycast(Instance, CurData); } virtual bool Sweep(const TSpatialVisitorData& Instance, FQueryFastData& CurData) override { return VisitSweep(Instance, CurData); } TArray Instances; }; struct FStressTestVisitor : ISpatialVisitor { using FPayload = FAccelerationStructureHandle; FStressTestVisitor() {} enum class SQType { Raycast, Sweep, Overlap }; bool VisitRaycast(const TSpatialVisitorData& Data, FQueryFastData& CurData) { return true; } bool VisitSweep(const TSpatialVisitorData& Data, FQueryFastData& CurData) { return true; } bool VisitOverlap(const TSpatialVisitorData& Data) { return true; } virtual bool Overlap(const TSpatialVisitorData& Instance) override { return VisitOverlap(Instance); } virtual bool Raycast(const TSpatialVisitorData& Instance, FQueryFastData& CurData) override { return VisitRaycast(Instance, CurData); } virtual bool Sweep(const TSpatialVisitorData& Instance, FQueryFastData& CurData) override { return VisitSweep(Instance, CurData); } }; auto BuildBoxes(FImplicitObjectPtr& Box, FReal BoxSize = 100, const FVec3& BoxGridDimensions = FVec3(10,10,10), const FVec3 Offset = FVec3(0, 0, 0)) { Box = MakeImplicitObjectPtr>(FVec3(0, 0, 0), FVec3(BoxSize, BoxSize, BoxSize)); auto Boxes = MakeUnique(); const int32 NumCols = BoxGridDimensions.X; const int32 NumRows = BoxGridDimensions.Y; const int32 NumHeight = BoxGridDimensions.Z; Boxes->AddParticles(NumRows * NumCols * NumHeight); int32 Idx = 0; for (int32 Height = 0; Height < NumHeight; ++Height) { for (int32 Row = 0; Row < NumRows; ++Row) { for (int32 Col = 0; Col < NumCols; ++Col) { Boxes->SetX(Idx, FVec3(Col * 100, Row * 100, Height * 100) + Offset); Boxes->SetR(Idx, FRotation3::Identity); Boxes->SetGeometry(Idx, Box); ++Idx; } } } return Boxes; } template void SpatialTestHelper(TSpatial& Spatial, FGeometryParticles* Boxes, FImplicitObjectPtr& Box, FSpatialAccelerationIdx SpatialIdx = FSpatialAccelerationIdx()) { //raycast //miss { FVisitor Visitor(FVec3(-100, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial.Raycast(Visitor.Start, Visitor.Dir, 1000, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 0); } //gather along ray { FVisitor Visitor(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial.Raycast(Visitor.Start, Visitor.Dir, 1000, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 10); } //gather along ray and then make modifications { auto Spatial2 = Spatial.Copy(); FVisitor Visitor(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor.Start, Visitor.Dir, 1000, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 10); //remove from structure Spatial2->RemoveElementFrom(Visitor.Instances[0], SpatialIdx); FVisitor Visitor2(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor2.Start, Visitor2.Dir, 1000, Visitor2); EXPECT_EQ(Visitor2.Instances.Num(), 9); //move instance away { const int32 MoveIdx = Visitor2.Instances[0]; Boxes->SetX(MoveIdx, Boxes->GetX(MoveIdx) + FVec3(1000, 0, 0)); FAABB3 NewBounds = Boxes->GetGeometry(MoveIdx)->template GetObject>()->BoundingBox().TransformedAABB(FRigidTransform3(Boxes->GetX(MoveIdx), Boxes->GetR(MoveIdx))); Spatial2->UpdateElementIn(MoveIdx, NewBounds, true, SpatialIdx); FVisitor Visitor3(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor3.Start, Visitor3.Dir, 1000, Visitor3); EXPECT_EQ(Visitor3.Instances.Num(), 8); //move instance back Boxes->SetX(MoveIdx, Boxes->GetX(MoveIdx) - FVec3(1000, 0, 0)); NewBounds = Boxes->GetGeometry(MoveIdx)->template GetObject>()->BoundingBox().TransformedAABB(FRigidTransform3(Boxes->GetX(MoveIdx), Boxes->GetR(MoveIdx))); Spatial2->UpdateElementIn(MoveIdx, NewBounds, true, SpatialIdx); FVisitor Visitor4(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor4.Start, Visitor4.Dir, 1000, Visitor4); EXPECT_EQ(Visitor4.Instances.Num(), 9); } //move other instance into view { const int32 MoveIdx = 5 * 5 * 5; const FVec3 OldPos = Boxes->GetX(MoveIdx); Boxes->SetX(MoveIdx, FVec3(0, 0, 0)); FAABB3 NewBounds = Boxes->GetGeometry(MoveIdx)->template GetObject>()->BoundingBox().TransformedAABB(FRigidTransform3(Boxes->GetX(MoveIdx), Boxes->GetR(MoveIdx))); Spatial2->UpdateElementIn(MoveIdx, NewBounds, true, SpatialIdx); FVisitor Visitor3(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor3.Start, Visitor3.Dir, 1000, Visitor3); EXPECT_EQ(Visitor3.Instances.Num(), 10); //move instance back Boxes->SetX(MoveIdx, OldPos); NewBounds = Boxes->GetGeometry(MoveIdx)->template GetObject>()->BoundingBox().TransformedAABB(FRigidTransform3(Boxes->GetX(MoveIdx), Boxes->GetR(MoveIdx))); Spatial2->UpdateElementIn(MoveIdx, NewBounds, true, SpatialIdx); } //move instance outside of grid bounds { const int32 MoveIdx = 5 * 5 * 5; const FVec3 OldPos = Boxes->GetX(MoveIdx); Boxes->SetX(MoveIdx, FVec3(-50, 0, 0)); FAABB3 NewBounds = Boxes->GetGeometry(MoveIdx)->template GetObject>()->BoundingBox().TransformedAABB(FRigidTransform3(Boxes->GetX(MoveIdx), Boxes->GetR(MoveIdx))); Spatial2->UpdateElementIn(MoveIdx, NewBounds, true, SpatialIdx); FVisitor Visitor3(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor3.Start, Visitor3.Dir, 1000, Visitor3); EXPECT_EQ(Visitor3.Instances.Num(), 10); //try ray outside of bounds which should hit FVisitor Visitor4(FVec3(-20, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor4.Start, Visitor4.Dir, 1000, Visitor4); EXPECT_EQ(Visitor4.Instances.Num(), 1); //delete dirty instance Spatial2->RemoveElementFrom(MoveIdx, SpatialIdx); FVisitor Visitor5(FVec3(-20, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor5.Start, Visitor5.Dir, 1000, Visitor4); EXPECT_EQ(Visitor5.Instances.Num(), 0); //move instance back Boxes->SetX(MoveIdx, OldPos); //create a new box const int32 NewIdx = Boxes->Size(); Boxes->AddParticles(1); Boxes->SetX(NewIdx, FVec3(-20, 0, 0)); Boxes->SetR(NewIdx, FRotation3::Identity); Boxes->SetGeometry(NewIdx, Box); NewBounds = Boxes->GetGeometry(NewIdx)->template GetObject>()->BoundingBox().TransformedAABB(FRigidTransform3(Boxes->GetX(NewIdx), Boxes->GetR(NewIdx))); Spatial2->UpdateElementIn(NewIdx, NewBounds, true, SpatialIdx); FVisitor Visitor6(FVec3(-20, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial2->Raycast(Visitor6.Start, Visitor6.Dir, 1000, Visitor6); EXPECT_EQ(Visitor6.Instances.Num(), 1); } } //stop half way through { FVisitor Visitor(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial.Raycast(Visitor.Start, Visitor.Dir, 499, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 5); } //any { FVisitor Visitor(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Visitor.bAny = true; Spatial.Raycast(Visitor.Start, Visitor.Dir, 1000, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 1); } //sweep //miss { FVisitor Visitor(FVec3(-100, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Visitor.HalfExtents = FVec3(10, 0, 0); Spatial.Sweep(Visitor.Start, Visitor.Dir, 1000, Visitor.HalfExtents, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 0); } //gather along ray { FVisitor Visitor(FVec3(-100, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Visitor.HalfExtents = FVec3(110, 0, 0); Spatial.Sweep(Visitor.Start, Visitor.Dir, 1000, Visitor.HalfExtents, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 10); } //stop half way through { FVisitor Visitor(FVec3(-100, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Visitor.HalfExtents = FVec3(110, 0, 0); Spatial.Sweep(Visitor.Start, Visitor.Dir, 499, Visitor.HalfExtents, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 5); } //right on edge and corner { FVisitor Visitor(FVec3(100, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Visitor.HalfExtents = FVec3(10, 0, 0); Spatial.Sweep(Visitor.Start, Visitor.Dir, 499, Visitor.HalfExtents, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 10); } //overlap //miss { FOverlapVisitor Visitor(FAABB3(FVec3(-100, 0, 0), FVec3(-10, 0, 0)), *Boxes); Spatial.Overlap(Visitor.Bounds, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 0); } //overlap some { FOverlapVisitor Visitor(FAABB3(FVec3(-100, 0, -10), FVec3(110, 110, 10)), *Boxes); Spatial.Overlap(Visitor.Bounds, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 4); } //overlap any { FOverlapVisitor Visitor(FAABB3(FVec3(-100, 0, -10), FVec3(110, 110, 10)), *Boxes); Visitor.bAny = true; Spatial.Overlap(Visitor.Bounds, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 1); } } void GridBPTest() { FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); TBoundingVolume Spatial(MakeParticleView(Boxes.Get())); SpatialTestHelper(Spatial, Boxes.Get(), Box); } void GridBPEarlyExitTest() { FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); TBoundingVolume Spatial(MakeParticleView(Boxes.Get())); // SpatialTestHelper(Spatial, Boxes.Get(), Box); //gather along ray { FVisitor Visitor(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Spatial.Raycast(Visitor.Start, Visitor.Dir, 1000, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 10); EXPECT_EQ(Visitor.Instances[0], 0); EXPECT_EQ(Visitor.Instances[9], 90); } // Stop after first hits in the first cell { FVisitor Visitor(FVec3(10, 0, 0), FVec3(0, 1, 0), 0, *Boxes); Visitor.BlockAfterN = 1; Spatial.Raycast(Visitor.Start, Visitor.Dir, 1000, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 1); EXPECT_EQ(Visitor.Instances[0], 0); } // Stop after first hits in the first cell, going backward { FVisitor Visitor(FVec3(10, 1000, 0), FVec3(0, -1, 0), 0, *Boxes); Visitor.BlockAfterN = 1; Spatial.Raycast(Visitor.Start, Visitor.Dir, 1000, Visitor); EXPECT_EQ(Visitor.Instances.Num(), 1); EXPECT_EQ(Visitor.Instances[0], 90); } } void GridBPTest2() { FImplicitObjectPtr Box( new TBox(FVec3(0, 0, 0), FVec3(100, 100, 100))); FParticleUniqueIndicesMultithreaded UniqueIndices; FPBDRigidsSOAs SOAs(UniqueIndices); const int32 NumRows = 10; const int32 NumCols = 10; const int32 NumHeight = 10; SOAs.CreateStaticParticles(NumRows * NumCols * NumHeight); auto& Boxes = SOAs.GetNonDisabledStaticParticles(); int32 Idx = 0; for (int32 Height = 0; Height < NumHeight; ++Height) { for (int32 Row = 0; Row < NumRows; ++Row) { for (int32 Col = 0; Col < NumCols; ++Col) { Boxes.SetX(Idx, FVec3(Col * 100, Row * 100, Height * 100)); Boxes.SetR(Idx, FRotation3::Identity); Boxes.SetGeometry(Idx, Box); ++Idx; } } } TArray> TmpArray = { &Boxes }; TBoundingVolume BV(MakeParticleView(MoveTemp(TmpArray))); TArray Handles = BV.FindAllIntersections(FAABB3(FVec3(0), FVec3(10))); EXPECT_EQ(Handles.Num(), 1); EXPECT_EQ(Handles[0], Boxes.Handle(0)); Handles = BV.FindAllIntersections(FAABB3(FVec3(0), FVec3(0, 0, 110))); EXPECT_EQ(Handles.Num(), 2); //create BV with an array of handles instead (useful for partial structures) { TBoundingVolume BV2(MakeHandleView(Handles)); TArray Handles2 = BV2.FindAllIntersections(FAABB3(FVec3(0), FVec3(10))); EXPECT_EQ(Handles2.Num(), 1); EXPECT_EQ(Handles2[0], Boxes.Handle(0)); Handles2 = BV2.FindAllIntersections(FAABB3(FVec3(0), FVec3(0, 0, 110))); EXPECT_EQ(Handles2.Num(), 2); } } void AABBTreeTest() { using TreeType = TAABBTree>; { FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); TreeType Spatial(MakeParticleView(Boxes.Get())); while (!Spatial.IsAsyncTimeSlicingComplete()) { Spatial.ProgressAsyncTimeSlicing(false); } SpatialTestHelper(Spatial, Boxes.Get(), Box); } { FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); TreeType Spatial(MakeParticleView(Boxes.Get())); while (!Spatial.IsAsyncTimeSlicingComplete()) { Spatial.ProgressAsyncTimeSlicing(false); } SpatialTestHelper(Spatial, Boxes.Get(), Box); } { //too many boxes so reoptimize FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); TreeType Spatial(MakeParticleView(Boxes.Get())); while(!Spatial.IsAsyncTimeSlicingComplete()) { Spatial.ProgressAsyncTimeSlicing(false); } EXPECT_EQ(Spatial.NumDirtyElements(),0); //fill up until dirty limit int32 Count; for(Count = 1; Count <= 10; ++Count) { auto Boxes2 = BuildBoxes(Box); for(uint32 Idx = 0; Idx < Boxes2->Size(); ++Idx) { Spatial.UpdateElement(Idx + Boxes->Size() * Count,Boxes2->WorldSpaceInflatedBounds(Idx),true); } EXPECT_EQ(Spatial.NumDirtyElements(), (Count)*Boxes->Size()); } //finally pass dirty limit so reset to 0 and then add the remaining new boxes auto Boxes2 = BuildBoxes(Box); for(uint32 Idx = 0; Idx < Boxes2->Size(); ++Idx) { Spatial.UpdateElement(Idx + Boxes->Size() * (Count),Boxes2->WorldSpaceInflatedBounds(Idx),true); } EXPECT_EQ(Spatial.NumDirtyElements(),Boxes->Size() - 1); } } void AABBTreeTestDynamic() { using TreeType = TAABBTree, true>; { FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box, 100, FVec3(10,10,10)); TreeType Spatial(MakeParticleView(Boxes.Get()), TreeType::DefaultMaxChildrenInLeaf, TreeType::DefaultMaxTreeDepth, TreeType::DefaultMaxPayloadBounds, TreeType::DefaultMaxNumToProcess, true); EXPECT_EQ(Spatial.NumDirtyElements(), 0); SpatialTestHelper(Spatial, Boxes.Get(), Box); EXPECT_EQ(Spatial.NumDirtyElements(), 0); } } void AABBTreeDirtyTreeTest() { using TreeType = TAABBTree, true>; // Do the standard tests { FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); TArray> EmptyArray; TreeType Spatial{ MakeParticleView(MoveTemp(EmptyArray)),5, 5, 10000.0f, 1000, false, true}; int32 Idx; for (Idx = 0; Idx < (int32)Boxes->Size(); ++Idx) { Spatial.UpdateElement(Idx, Boxes->WorldSpaceInflatedBounds(Idx), true); } SpatialTestHelper(Spatial, Boxes.Get(), Box); } } void AABBTreeDirtyGridTest() { using TreeType = TAABBTree>; // Save CVARS int32 DirtyElementGridCellSize = FAABBTreeDirtyGridCVars::DirtyElementGridCellSize; int32 DirtyElementMaxGridCellQueryCount = FAABBTreeDirtyGridCVars::DirtyElementMaxGridCellQueryCount; int32 DirtyElementMaxPhysicalSizeInCells = FAABBTreeDirtyGridCVars::DirtyElementMaxPhysicalSizeInCells; int32 DirtyElementMaxCellCapacity = FAABBTreeDirtyGridCVars::DirtyElementMaxCellCapacity; // Set CVARS to known values FAABBTreeDirtyGridCVars::DirtyElementGridCellSize = 100; FAABBTreeDirtyGridCVars::DirtyElementMaxGridCellQueryCount = 10000; FAABBTreeDirtyGridCVars::DirtyElementMaxPhysicalSizeInCells = 20; FAABBTreeDirtyGridCVars::DirtyElementMaxCellCapacity = 20; // Do the standard tests { FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); TreeType Spatial{}; int32 Idx; for (Idx = 0; Idx < (int32)Boxes->Size(); ++Idx) { Spatial.UpdateElement(Idx, Boxes->WorldSpaceInflatedBounds(Idx), true); } EXPECT_EQ(Spatial.NumDirtyElements(), Boxes->Size()); SpatialTestHelper(Spatial, Boxes.Get(), Box); } // Repeat the standard tests with low cell capacity and different cell sizes { // Set CVARS to known values FAABBTreeDirtyGridCVars::DirtyElementGridCellSize = 44; FAABBTreeDirtyGridCVars::DirtyElementMaxCellCapacity = 2; FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); TreeType Spatial{}; int32 Idx; for (Idx = 0; Idx < (int32)Boxes->Size(); ++Idx) { Spatial.UpdateElement(Idx, Boxes->WorldSpaceInflatedBounds(Idx), true); } EXPECT_EQ(Spatial.NumDirtyElements(), Boxes->Size()); SpatialTestHelper(Spatial, Boxes.Get(), Box); } // Make sure we get the same results, with and without the grid for sweeps and raycasts { FAABBTreeDirtyGridCVars::DirtyElementMaxCellCapacity = 7; FImplicitObjectPtr Box; FVec3 LargeOffset(10000000, 10000000, 10000000); // Test for floating point precision errors at large world offsets auto Boxes = BuildBoxes(Box, 100, FVec3(40, 40, 1), FVec3(-2000, -2000, -50) + LargeOffset); for (float Angle = 0.0; Angle < 2 * PI; Angle += (10.0f / 360.0f) * 2.0f * PI) { FVec3 Direction{ FMath::Cos(Angle), FMath::Sin(Angle), 0 }; // With the grid FVisitor VisitorGrid(FVec3(53, 27, 0) + LargeOffset, Direction, 0, *Boxes); VisitorGrid.HalfExtents = FVec3(102, 20, 2); { FAABBTreeDirtyGridCVars::DirtyElementGridCellSize = 100; TreeType Spatial{}; int32 Idx; for (Idx = 0; Idx < (int32)Boxes->Size(); ++Idx) { Spatial.UpdateElement(Idx, Boxes->WorldSpaceInflatedBounds(Idx), true); } EXPECT_EQ(Spatial.NumDirtyElements(), Boxes->Size()); Spatial.Raycast(VisitorGrid.Start, VisitorGrid.Dir, 1900, VisitorGrid); Spatial.Sweep(VisitorGrid.Start, VisitorGrid.Dir, 1800, VisitorGrid.HalfExtents, VisitorGrid); } // Without the grid FVisitor VisitorNoGrid(FVec3(53, 27, 0) + LargeOffset, Direction, 0, *Boxes); VisitorNoGrid.HalfExtents = FVec3(102, 20, 2); { FAABBTreeDirtyGridCVars::DirtyElementGridCellSize = 0; TreeType Spatial{}; int32 Idx; for (Idx = 0; Idx < (int32)Boxes->Size(); ++Idx) { Spatial.UpdateElement(Idx, Boxes->WorldSpaceInflatedBounds(Idx), true); } EXPECT_EQ(Spatial.NumDirtyElements(), Boxes->Size()); Spatial.Raycast(VisitorNoGrid.Start, VisitorNoGrid.Dir, 1900, VisitorNoGrid); Spatial.Sweep(VisitorNoGrid.Start, VisitorNoGrid.Dir, 1800, VisitorNoGrid.HalfExtents, VisitorNoGrid); } // These will be in the same order, but we can drop this requirement in the future EXPECT_TRUE(VisitorNoGrid.Instances == VisitorGrid.Instances); } } // Test a case that failed before (with an assert) { FAABBTreeDirtyGridCVars::DirtyElementGridCellSize = 1000; FAABBTreeDirtyGridCVars::DirtyElementMaxCellCapacity = 7; FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box, 100, FVec3(1, 1, 1), FVec3(-3000, -1000, -50)); // Just one box TreeType Spatial{}; Spatial.UpdateElement(0, Boxes->WorldSpaceInflatedBounds(0), true); // Move the Box Boxes = BuildBoxes(Box, 100, FVec3(1, 1, 1), FVec3(-4000, -1000, -50)); // Change position of box Spatial.UpdateElement(0, Boxes->WorldSpaceInflatedBounds(0), true); // Check for no ensures // Move the Box Boxes = BuildBoxes(Box, 100, FVec3(1, 1, 1), FVec3(3000, 1000, -50)); // Change position of box Spatial.UpdateElement(0, Boxes->WorldSpaceInflatedBounds(0), true); Boxes = BuildBoxes(Box, 100, FVec3(1, 1, 1), FVec3(4000, 1000, -50)); // Change position of box Spatial.UpdateElement(0, Boxes->WorldSpaceInflatedBounds(0), true); // Check for no ensures // Move the Box Boxes = BuildBoxes(Box, 100, FVec3(1, 1, 1), FVec3(-10000003000.0f, -1000, -50)); // Change position of box Spatial.UpdateElement(0, Boxes->WorldSpaceInflatedBounds(0), true); // Check for no ensures // Move the Box Boxes = BuildBoxes(Box, 100, FVec3(1, 1, 1), FVec3(-10000004000.0f, -1000, -50)); // Change position of box Spatial.UpdateElement(0, Boxes->WorldSpaceInflatedBounds(0), true); // Check for no ensures } // Restore CVARS FAABBTreeDirtyGridCVars::DirtyElementGridCellSize = DirtyElementGridCellSize; FAABBTreeDirtyGridCVars::DirtyElementMaxGridCellQueryCount = DirtyElementMaxGridCellQueryCount; FAABBTreeDirtyGridCVars::DirtyElementMaxPhysicalSizeInCells = DirtyElementMaxPhysicalSizeInCells; FAABBTreeDirtyGridCVars::DirtyElementMaxCellCapacity = DirtyElementMaxCellCapacity; } void DoForSweepIntersectCellsImpTest() { { int32 NumFuncCalled = 0; int32 XArray[2]; int32 YArray[2]; DoForSweepIntersectCellsImp(1.4484817992026819, 1.4432470701435705, 1251.1886035677471, -1183.6311465697545, -866.67708504199993, -747.83750413730752, 1000.0, 0.001, [&](auto X, auto Y) { XArray[NumFuncCalled] = X; YArray[NumFuncCalled] = Y; ++NumFuncCalled; }); EXPECT_EQ(NumFuncCalled, 2); EXPECT_EQ(XArray[0], 1000); EXPECT_EQ(YArray[0], -2000); EXPECT_EQ(XArray[1], 0); EXPECT_EQ(YArray[1], -2000); } { int32 NumFuncCalled = 0; int32 XArray[2]; int32 YArray[2]; DoForSweepIntersectCellsImp(1.4484817992026819, 1.4432470701435705, 1251.1886035677471, -1183.6311465697545, 866.67708504199993, -747.83750413730752, 1000.0, 0.001, [&](auto X, auto Y) { XArray[NumFuncCalled] = X; YArray[NumFuncCalled] = Y; ++NumFuncCalled; }); EXPECT_EQ(NumFuncCalled, 2); EXPECT_EQ(XArray[0], 1000); EXPECT_EQ(YArray[0], -2000); EXPECT_EQ(XArray[1], 2000); EXPECT_EQ(YArray[1], -2000); } { int32 NumFuncCalled = 0; int32 XArray[3]; int32 YArray[3]; DoForSweepIntersectCellsImp(1.2928878353696973, 1.2928878353697257, -1013.1421764369597, 210.55865232178132, 712.84350045280678, -265.39563631071809, 1000.0, 0.001, [&](auto X, auto Y) { XArray[NumFuncCalled] = X; YArray[NumFuncCalled] = Y; ++NumFuncCalled; }); EXPECT_EQ(NumFuncCalled, 3); EXPECT_EQ(XArray[0], -2000); EXPECT_EQ(YArray[0], 0); EXPECT_EQ(XArray[1], -1000); EXPECT_EQ(YArray[1], 0); EXPECT_EQ(XArray[2], -1000); EXPECT_EQ(YArray[2], -1000); } { int32 NumFuncCalled = 0; DoForSweepIntersectCellsImp(4000, 4000, 0, 0, 7000 - 0.01, 3000 - 0.01, 1000.0, 0.001, [&](auto X, auto Y) { ++NumFuncCalled; }); // This was verified manually on a paper grid EXPECT_EQ(NumFuncCalled, 153); } } void AABBTreeTimesliceTest() { using TreeType = TAABBTree>; FImplicitObjectPtr Box; // If we are time slicing by a milliseconds budget, create a large tree so it takes time to process auto Boxes = FAABBTimeSliceCVars::bUseTimeSliceMillisecondBudget ? BuildBoxes(Box, 50, FVec3(50.0f,50.0f,50.0f)) : BuildBoxes(Box) ; // build AABB in one go TreeType SpatialBuildImmediate( MakeParticleView(Boxes.Get()) , TreeType::DefaultMaxChildrenInLeaf , TreeType::DefaultMaxTreeDepth , TreeType::DefaultMaxPayloadBounds , 0); // build entire tree in one go, no timeslicing EXPECT_TRUE(SpatialBuildImmediate.IsAsyncTimeSlicingComplete()); const double SlicedTreeGenerationStartTime = FPlatformTime::Seconds(); // build AABB in time-sliced sections TreeType SpatialTimesliced( MakeParticleView(Boxes.Get()) , TreeType::DefaultMaxChildrenInLeaf , TreeType::DefaultMaxTreeDepth , TreeType::DefaultMaxPayloadBounds , 20); // build in small iteration steps, 20 iterations per call to ProgressAsyncTimeSlicing EXPECT_FALSE(!FAABBTimeSliceCVars::bUseTimeSliceMillisecondBudget && SpatialTimesliced.IsAsyncTimeSlicingComplete()); // This is far from accurate, but give us some wiggle room to test it with default settings without needed to implement code to simulate a precise pause inside the Tree implementation. const float MaxSliceDurationWithErrorMargin = FAABBTimeSliceCVars::MaxProcessingTimePerSliceSeconds + 0.01f; double LargestSliceDuration = 0; int32 IterationNumber = 1; bool bSliceDoneWithinBudget = true; while (!SpatialTimesliced.IsAsyncTimeSlicingComplete()) { const double SliceStartTime = FPlatformTime::Seconds(); SpatialTimesliced.ProgressAsyncTimeSlicing(false); if (FAABBTimeSliceCVars::bUseTimeSliceMillisecondBudget) { const double ElapsedTime = FPlatformTime::Seconds() - SliceStartTime; bSliceDoneWithinBudget &= ElapsedTime < MaxSliceDurationWithErrorMargin; LargestSliceDuration = FMath::Max(LargestSliceDuration, ElapsedTime); } IterationNumber++; } EXPECT_TRUE(bSliceDoneWithinBudget); const FStringView TimeSliceMode = FAABBTimeSliceCVars::bUseTimeSliceMillisecondBudget ? TEXT("MillisecondsBudget") : TEXT("AmountOfWorkToDo"); const double TotalGenerationTime = FPlatformTime::Seconds() - SlicedTreeGenerationStartTime; UE_LOG(LogHeadlessChaos, Verbose, TEXT("Time Sliced Tree Generation took [%f] seconds | Using Mode [%s] | In [%d] Iterations | LargestSliceDuration [%f] | EvaluatedMaxTimeSlicedDurarion [%f]"), TotalGenerationTime, TimeSliceMode.GetData(), IterationNumber, LargestSliceDuration, MaxSliceDurationWithErrorMargin); // now check both AABBs have the same hierarchy // (indices will be different but walking tree should give same results) FAABB3 Tmp = FAABB3::ZeroAABB(); TArray AllBoundsBuildImmediate; SpatialBuildImmediate.GetAsBoundsArray(AllBoundsBuildImmediate, 0, -1, Tmp); TArray AllBoundsTimesliced; SpatialTimesliced.GetAsBoundsArray(AllBoundsTimesliced, 0, -1, Tmp); EXPECT_EQ(AllBoundsBuildImmediate.Num(), AllBoundsTimesliced.Num()); for (int i=0; i> VisitedCells; // this should report only be as much as 4 hits as teh grid is 2D and the ray downward vertical and the halfextends are smaller than a cell size { DoForSweepIntersectCells(EdgeCase::QueryHalfExtents, EdgeCase::StartPoint, EdgeCase::Dir, EdgeCase::CurrentLength, DirtyElementGridCellSize, DirtyElementGridCellSizeInv, [&VisitedCells](FReal X, FReal Y) { VisitedCells.Add({ X,Y }); EXPECT_TRUE(VisitedCells.Num() <= 4 ); }); EXPECT_TRUE(VisitedCells.Num() <= 4); } // 50 trillions unit long sweep test reporting that there's not too many cells { const bool bTooManyCell = TooManySweepQueryCells(LargeSweep::QueryHalfExtents, LargeSweep::StartPoint, LargeSweep::Dir, LargeSweep::CurrentLength, DirtyElementGridCellSizeInv, DirtyElementMaxGridCellQueryCount); EXPECT_TRUE(bTooManyCell); } } void BroadphaseCollectionTest() { using TreeType = TAABBTree>; { FImplicitObjectPtr Box; auto Boxes = BuildBoxes(Box); auto Spatial = MakeUnique(MakeParticleView(Boxes.Get())); while (!Spatial->IsAsyncTimeSlicingComplete()) { Spatial->ProgressAsyncTimeSlicing(false); } TSpatialAccelerationCollection AccelerationCollection; AccelerationCollection.AddSubstructure(MoveTemp(Spatial), 0, 0); FSpatialAccelerationIdx SpatialIdx = { 0,0 }; SpatialTestHelper(AccelerationCollection, Boxes.Get(), Box, SpatialIdx); } { using BVType = TBoundingVolume; FImplicitObjectPtr Box; auto Boxes0 = BuildBoxes(Box); auto Spatial0 = MakeUnique(MakeParticleView(Boxes0.Get())); while (!Spatial0->IsAsyncTimeSlicingComplete()) { Spatial0->ProgressAsyncTimeSlicing(false); } FGeometryParticles EmptyBoxes; auto Spatial1 = MakeUnique(MakeParticleView(&EmptyBoxes)); while (!Spatial1->IsAsyncTimeSlicingComplete()) { Spatial1->ProgressAsyncTimeSlicing(false); } TSpatialAccelerationCollection AccelerationCollection; AccelerationCollection.AddSubstructure(MoveTemp(Spatial0), 0, 0); AccelerationCollection.AddSubstructure(MoveTemp(Spatial1), 1, 0); FSpatialAccelerationIdx SpatialIdx = { 0,0 }; SpatialTestHelper(AccelerationCollection, Boxes0.Get(), Box, SpatialIdx); } { using BVType = TBoundingVolume; FImplicitObjectPtr Box; auto Boxes1 = BuildBoxes(Box); FGeometryParticles EmptyBoxes; auto Spatial0 = MakeUnique(MakeParticleView(&EmptyBoxes)); auto Spatial1 = MakeUnique(MakeParticleView(Boxes1.Get())); TSpatialAccelerationCollection AccelerationCollection; AccelerationCollection.AddSubstructure(MoveTemp(Spatial0), 0, 0); AccelerationCollection.AddSubstructure(MoveTemp(Spatial1), 1, 0); FSpatialAccelerationIdx SpatialIdx = { 1,0 }; SpatialTestHelper(AccelerationCollection, Boxes1.Get(), Box, SpatialIdx); } } // Verify we don't generate a NaN or invalid bounds if we build BoundingVolume with particles that have no bounds. void BoundingVolumeNoBoundsTest() { FImplicitObjectPtr Box( new TBox(FVec3(0, 0, 0), FVec3(100))); auto Boxes = MakeUnique(); Boxes->AddParticles(1); // Construct a particle and set HasBounds to false. int32 Idx = 0; Boxes->SetX(Idx, FVec3(0)); Boxes->SetR(Idx, FRotation3::Identity); Boxes->SetGeometry(Idx, Box); // Tell BV we have no bounds, this used to cause issues. Boxes->HasBounds(Idx) = false; // Make Bounding Volume with only particles that have no bounds. auto Spatial1 = MakeUnique>(MakeParticleView(Boxes.Get())); EXPECT_EQ(Spatial1->GetBounds().Min().ContainsNaN(), false); EXPECT_EQ(Spatial1->GetBounds().Max().ContainsNaN(), false); EXPECT_EQ(Spatial1->GetBounds().Extents().ContainsNaN(), false); } void SpatialAccelerationDirtyAndGlobalQueryStrestTest() { using AABBTreeType = TAABBTree>; // Construct 100000 Particles const int32 NumRows = 100; const int32 NumCols = 100; const int32 NumHeight = 10; const int32 ParticleCount = NumRows * NumCols * NumHeight; const FReal BoxSize = 100; FParticleUniqueIndicesMultithreaded UniqueIndices; FPBDRigidsSOAs Particles(UniqueIndices); TArray ParticleHandles = Particles.CreateDynamicParticles(ParticleCount); for (auto& Handle : ParticleHandles) { Handle->GTGeometryParticle() = FGeometryParticle::CreateParticle().Release(); } const auto& ParticlesView = Particles.GetAllParticlesView(); // ensure these can't be filtered out. FCollisionFilterData FilterData; FilterData.Word0 = TNumericLimits::Max(); FilterData.Word1 = TNumericLimits::Max(); FilterData.Word2 = TNumericLimits::Max(); FilterData.Word3 = TNumericLimits::Max(); Chaos::FImplicitObjectPtr Box( new TBox(FVec3(0, 0, 0), FVec3(BoxSize, BoxSize, BoxSize))); int32 Idx = 0; for (int32 Height = 0; Height < NumHeight; ++Height) { for (int32 Row = 0; Row < NumRows; ++Row) { for (int32 Col = 0; Col < NumCols; ++Col) { FGeometryParticle* GTParticle = ParticleHandles[Idx]->GTGeometryParticle(); FPBDRigidParticleHandle* Handle = ParticleHandles[Idx]; Handle->SetX(FVec3(Col * BoxSize, Row * BoxSize, Height * BoxSize)); GTParticle->SetX(FVec3(Col * BoxSize, Row * BoxSize, Height * BoxSize)); Handle->SetR(FRotation3::Identity); GTParticle->SetR(FRotation3::Identity); Handle->SetGeometry(Box); Handle->ShapesArray()[0]->SetQueryData(FilterData); GTParticle->SetGeometry(Box); GTParticle->ShapesArray()[0]->SetQueryData(FilterData); Handle->SetUniqueIdx(FUniqueIdx(Idx)); GTParticle->SetUniqueIdx(FUniqueIdx(Idx)); ++Idx; } } } int32 DirtyNum = 800; int32 Queries = 500; ensure(DirtyNum < ParticleCount); // Construct tree AABBTreeType Spatial(ParticlesView); // Update DirtyNum elements, so they are pulled out of leaves. for (int32 i = 0; i < DirtyNum; ++i) { FAccelerationStructureHandle Payload(ParticleHandles[i]->GTGeometryParticle()); FAABB3 Bounds = ParticleHandles[i]->WorldSpaceInflatedBounds(); Spatial.UpdateElement(Payload, Bounds, true); } // RAYCASTS { // Setup raycast params const FVec3 Start(500, 500, 500); const FVec3 Dir(1, 0, 0); const FReal Length = 1000; FStressTestVisitor Visitor; // Measure raycasts uint32 Cycles = 0.0; for (int32 Query = 0; Query < Queries; ++Query) { uint32 StartTime = FPlatformTime::Cycles(); Spatial.Raycast(Start, Dir, Length, Visitor); Cycles += FPlatformTime::Cycles() - StartTime; } float Milliseconds = FPlatformTime::ToMilliseconds(Cycles); float AvgMicroseconds = (Milliseconds * 1000) / Queries; UE_LOG(LogHeadlessChaos, Warning, TEXT("Raycast Test: Dirty Particles: %d, Queries: %d, Avg Query Time: %f(us), Total:%f(ms)"), DirtyNum, Queries, AvgMicroseconds, Milliseconds); } // SWEEPS { // Setup Sweep params const FVec3 Start(500, 500, 500); const FVec3 Dir(1, 0, 0); const FReal Length = 1000; const FVec3 HalfExtents(50, 50, 50); FStressTestVisitor Visitor; // Measure raycasts uint32 Cycles = 0.0; for (int32 Query = 0; Query < Queries; ++Query) { uint32 StartTime = FPlatformTime::Cycles(); Spatial.Sweep(Start, Dir, Length, HalfExtents, Visitor); Cycles += FPlatformTime::Cycles() - StartTime; } float Milliseconds = FPlatformTime::ToMilliseconds(Cycles); float AvgMicroseconds = (Milliseconds * 1000) / Queries; UE_LOG(LogHeadlessChaos, Warning, TEXT("Sweep Test: Dirty Particles: %d, Queries: %d, Avg Query Time: %f(us), Total:%f(ms)"), DirtyNum, Queries, AvgMicroseconds, Milliseconds); } // OVERLAPS { FStressTestVisitor Visitor; const FAABB3 QueryBounds(FVec3(-50, -50, -50), FVec3(50,50,50)); // Measure raycasts uint32 Cycles = 0.0; for (int32 Query = 0; Query < Queries; ++Query) { uint32 StartTime = FPlatformTime::Cycles(); Spatial.Overlap(QueryBounds, Visitor); Cycles += FPlatformTime::Cycles() - StartTime; } float Milliseconds = FPlatformTime::ToMilliseconds(Cycles); float AvgMicroseconds = (Milliseconds * 1000) / Queries; UE_LOG(LogHeadlessChaos, Error, TEXT("Overlap Test: Dirty Particles: %d, Queries: %d, Avg Query Time: %f(us), Total:%f(ms)"), DirtyNum, Queries, AvgMicroseconds, Milliseconds); } } }