From af0945c818166803423ef6cfec4f5feb89c903c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Csik=C3=B3s=20Attila?= Date: Sun, 3 Mar 2024 17:29:54 +0100 Subject: [PATCH] Issue: #15 boost/geometry adapter --- README.md | 61 +++--- adaptor.boost.h | 287 ++++++++++++++++++++++++++++ octree.h | 2 +- unittests/adaptor.tests.cpp | 147 ++++++++++++++ unittests/packages.config | 1 + unittests/unittests.vcxproj | 3 + unittests/unittests.vcxproj.filters | 3 + 7 files changed, 473 insertions(+), 31 deletions(-) create mode 100644 adaptor.boost.h diff --git a/README.md b/README.md index a956945..0ebf79a 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ What is the Octree and what is good for? https://en.wikipedia.org/wiki/Octree * Default: `std::array` based structures (`PointND`, `VectorND`, `BoundingBoxND`, `RayND`, `PlaneND`) * Eigen: `Eigen::OctreePoint3d`, `::OctreePointC3d`, `::OctreeBox3d`, `::OctreeBoxC3d`, etc. (adaptor.eigen.h) * Unreal Engine: `FOctreePoint`, `FOctreePointC`, `FOctreeBox`, `FOctreeBoxC`, etc. (adaptor.unreal.h) +* Boost: `boost::geometry::octree_point`, `::geometry::octree_box`, etc. (adaptor.boost.h) * `struct{x,y,z}`: (adaptor.xyz.h) ## Major aliases in OrthoTree @@ -314,36 +315,36 @@ struct AdaptorBasicsCustom }; - using AdaptorCustom = OrthoTree::AdaptorGeneralBase< - 2, - MyPoint2D, - MyBox2D, - MyRay2D, - MyPlane2D, - float, - AdaptorBasicsCustom>; - - - // Tailored Quadtree objects - - using QuadtreePointCustom = OrthoTree::OrthoTreePoint< - 2, - MyPoint2D, - MyBox2D, - MyRay2D, - MyPlane2D, - float, - AdaptorCustom>; - - using QuadtreeBoxCustom = OrthoTree::OrthoTreeBoundingBox< - 2, - MyPoint2D, - MyBox2D, - MyRay2D, - MyPlane2D, - float, - 2, - AdaptorCustom>; +using AdaptorCustom = OrthoTree::AdaptorGeneralBase< + 2, + MyPoint2D, + MyBox2D, + MyRay2D, + MyPlane2D, + float, + AdaptorBasicsCustom>; + + +// Tailored Quadtree objects + +using QuadtreePointCustom = OrthoTree::OrthoTreePoint< + 2, + MyPoint2D, + MyBox2D, + MyRay2D, + MyPlane2D, + float, + AdaptorCustom>; + +using QuadtreeBoxCustom = OrthoTree::OrthoTreeBoundingBox< + 2, + MyPoint2D, + MyBox2D, + MyRay2D, + MyPlane2D, + float, + 2, + AdaptorCustom>; ``` ## Benchmarks diff --git a/adaptor.boost.h b/adaptor.boost.h new file mode 100644 index 0000000..580e367 --- /dev/null +++ b/adaptor.boost.h @@ -0,0 +1,287 @@ +#pragma once +/* +MIT License + +Copyright (c) 2021 Attila Csikós + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "octree.h" +#include + +namespace boost::geometry::model +{ + // Define related elements + + template + using pointNd_t = point; + + template + using boxNd_t = box>; + + template + struct rayNd_t + { + pointNd_t origin; + pointNd_t direction; + }; + + template + struct planeNd_t + { + TGeometry origo_distance; // origo_distance = dot_product(normal, any_point) + pointNd_t normal; // should be normalized + }; +} // namespace boost::geometry::model + + +namespace OrthoTree +{ + namespace BoostAdaptor + { + + template + struct AdaptorGeneralBasics + { + using TVector = boost::geometry::model::pointNd_t; + using TBox = boost::geometry::model::boxNd_t; + using TRay = boost::geometry::model::rayNd_t; + using TPlane = boost::geometry::model::planeNd_t; + + + static constexpr TGeometry GetPointC(TVector const& point, dim_t dimensionID) noexcept + { + static_assert(DIMENSION_NO < 6, "For this dimension, the adaptor is not implemented. Supplement the function!"); + + if constexpr (DIMENSION_NO == 1) + { + return point.get<0>(); + } + else if constexpr (DIMENSION_NO == 2) + { + switch (dimensionID) + { + case 0: return point.get<0>(); + case 1: return point.get<1>(); + } + } + else if constexpr (DIMENSION_NO == 3) + { + switch (dimensionID) + { + case 0: return point.get<0>(); + case 1: return point.get<1>(); + case 2: return point.get<2>(); + } + } + else if constexpr (DIMENSION_NO == 4) + { + switch (dimensionID) + { + case 0: return point.get<0>(); + case 1: return point.get<1>(); + case 2: return point.get<2>(); + case 3: return point.get<3>(); + } + } + else + { + switch (dimensionID) + { + case 0: return point.get<0>(); + case 1: return point.get<1>(); + case 2: return point.get<2>(); + case 3: return point.get<3>(); + case 4: return point.get<4>(); + } + } + + assert(false); + std::terminate(); + } + + + static constexpr void SetPointC(TVector& point, dim_t dimensionID, TGeometry value) noexcept + { + static_assert(DIMENSION_NO < 6, "For this dimension, the adaptor is not implemented. Supplement the function!"); + + if constexpr (DIMENSION_NO == 1) + { + point.set<0>(value); + } + else if constexpr (DIMENSION_NO == 2) + { + switch (dimensionID) + { + case 0: point.set<0>(value); return; + case 1: point.set<1>(value); return; + } + } + else if constexpr (DIMENSION_NO == 3) + { + switch (dimensionID) + { + case 0: point.set<0>(value); return; + case 1: point.set<1>(value); return; + case 2: point.set<2>(value); return; + } + } + else if constexpr (DIMENSION_NO == 4) + { + switch (dimensionID) + { + case 0: point.set<0>(value); return; + case 1: point.set<1>(value); return; + case 2: point.set<2>(value); return; + case 3: point.set<3>(value); return; + } + } + else + { + switch (dimensionID) + { + case 0: point.set<0>(value); return; + case 1: point.set<1>(value); return; + case 2: point.set<2>(value); return; + case 3: point.set<3>(value); return; + case 4: point.set<4>(value); return; + } + } + + assert(false); + std::terminate(); + } + + static constexpr TGeometry GetBoxMinC(TBox const& box, dim_t dimensionID) noexcept { return GetPointC(box.min_corner(), dimensionID); } + static constexpr TGeometry GetBoxMaxC(TBox const& box, dim_t dimensionID) noexcept { return GetPointC(box.max_corner(), dimensionID); } + static constexpr void SetBoxMinC(TBox& box, dim_t dimensionID, TGeometry value) noexcept { SetPointC(box.min_corner(), dimensionID, value); } + static constexpr void SetBoxMaxC(TBox& box, dim_t dimensionID, TGeometry value) noexcept { SetPointC(box.max_corner(), dimensionID, value); } + + static constexpr TVector const& GetRayDirection(TRay const& ray) noexcept { return ray.direction; } + static constexpr TVector const& GetRayOrigin(TRay const& ray) noexcept { return ray.origin; } + + static constexpr TVector const& GetPlaneNormal(TPlane const& plane) noexcept { return plane.normal; } + static constexpr TGeometry GetPlaneOrigoDistance(TPlane const& plane) noexcept { return plane.origo_distance; } + }; + + template + using BoostAdaptorGeneral = AdaptorGeneralBase< + DIMENSION_NO, + boost::geometry::model::pointNd_t, + boost::geometry::model::boxNd_t, + boost::geometry::model::rayNd_t, + boost::geometry::model::planeNd_t, + TGeometry, + AdaptorGeneralBasics>; + + template + using BoostOrthoTreePoint = OrthoTreePoint< + DIMENSION_NO, + boost::geometry::model::pointNd_t, + boost::geometry::model::boxNd_t, + boost::geometry::model::rayNd_t, + boost::geometry::model::planeNd_t, + TGeometry, + BoostAdaptorGeneral>; + + template + using BoostOrthoTreeBoundingBox = OrthoTreeBoundingBox< + DIMENSION_NO, + boost::geometry::model::pointNd_t, + boost::geometry::model::boxNd_t, + boost::geometry::model::rayNd_t, + boost::geometry::model::planeNd_t, + TGeometry, + SPLIT_DEPTH_INCREASEMENT, + BoostAdaptorGeneral>; + } // namespace BoostAdaptor +} // namespace OrthoTree + +namespace boost::geometry +{ + using namespace OrthoTree::BoostAdaptor; + + // Core types + + template + using orthotree_point_t = BoostOrthoTreePoint; + + using quadtree_point_d = BoostOrthoTreePoint<2, double>; + using quadtree_point_f = BoostOrthoTreePoint<2, float>; + using quadtree_point_i = BoostOrthoTreePoint<2, int>; + using quadtree_point = quadtree_point_d; + + using octree_point_d = BoostOrthoTreePoint<3, double>; + using octree_point_f = BoostOrthoTreePoint<3, float>; + using octree_point_i = BoostOrthoTreePoint<3, int>; + using octree_point = octree_point_d; + + + template + using orthotree_box_t = BoostOrthoTreeBoundingBox; + + template + using quadtree_box_ds = BoostOrthoTreeBoundingBox<2, SPLIT_DEPTH_INCREASEMENT, double>; + using quadtree_box_d = BoostOrthoTreeBoundingBox<2, 2, double>; + using quadtree_box_f = BoostOrthoTreeBoundingBox<2, 2, float>; + using quadtree_box_i = BoostOrthoTreeBoundingBox<2, 2, int>; + using quadtree_box = quadtree_box_d; + + template + using octree_box_ds = BoostOrthoTreeBoundingBox<3, SPLIT_DEPTH_INCREASEMENT, double>; + using octree_box_d = BoostOrthoTreeBoundingBox<3, 2, double>; + using octree_box_f = BoostOrthoTreeBoundingBox<3, 2, float>; + using octree_box_i = BoostOrthoTreeBoundingBox<3, 2, int>; + using octree_box = octree_box_d; + + // Container types + + template + using orthotree_point_c_t = OrthoTree::OrthoTreeContainerPoint, model::pointNd_t>; + + using quadtree_point_c_d = orthotree_point_c_t<2, double>; + using quadtree_point_c_f = orthotree_point_c_t<2, float>; + using quadtree_point_c_i = orthotree_point_c_t<2, int>; + using quadtree_point_c = quadtree_point_c_d; + + using octree_point_c_d = orthotree_point_c_t<3, double>; + using octree_point_c_f = orthotree_point_c_t<3, float>; + using octree_point_c_i = orthotree_point_c_t<3, int>; + using octree_point_c = octree_point_c_d; + + + template + using orthotree_box_c_t = + OrthoTree::OrthoTreeContainerBox, model::boxNd_t>; + + template + using quadtree_box_c_ds = orthotree_box_c_t<2, SPLIT_DEPTH_INCREASEMENT, double>; + using quadtree_box_c_d = orthotree_box_c_t<2, 2, double>; + using quadtree_box_c_f = orthotree_box_c_t<2, 2, float>; + using quadtree_box_c_i = orthotree_box_c_t<2, 2, int>; + using quadtree_box_c = quadtree_box_c_d; + + template + using octree_box_c_ds = orthotree_box_c_t<3, SPLIT_DEPTH_INCREASEMENT, double>; + using octree_box_c_d = orthotree_box_c_t<3, 2, double>; + using octree_box_c_f = orthotree_box_c_t<3, 2, float>; + using octree_box_c_i = orthotree_box_c_t<3, 2, int>; + using octree_box_c = octree_box_c_d; +} // namespace boost::geometry \ No newline at end of file diff --git a/octree.h b/octree.h index 82c6832..2052907 100644 --- a/octree.h +++ b/octree.h @@ -229,7 +229,7 @@ namespace OrthoTree template struct AdaptorGeneralBasics { - static constexpr TGeometry const& GetPointC(TVector const& point, dim_t dimensionID) noexcept { return point[dimensionID]; } + static constexpr TGeometry GetPointC(TVector const& point, dim_t dimensionID) noexcept { return point[dimensionID]; } static constexpr void SetPointC(TVector& point, dim_t dimensionID, TGeometry value) noexcept { point[dimensionID] = value; } static constexpr TGeometry GetBoxMinC(TBox const& box, dim_t dimensionID) noexcept { return box.Min[dimensionID]; } diff --git a/unittests/adaptor.tests.cpp b/unittests/adaptor.tests.cpp index 139ddec..d651cc8 100644 --- a/unittests/adaptor.tests.cpp +++ b/unittests/adaptor.tests.cpp @@ -1,6 +1,7 @@ #include "pch.h" #include +#include "../adaptor.boost.h" #include "../adaptor.eigen.h" #include "../adaptor.xyz.h" @@ -516,4 +517,150 @@ namespace AdaptorTest } }; } + + namespace BoostAdaptorTest + { + using namespace boost::geometry; + + TEST_CLASS(BoostTest) + { + public: + TEST_METHOD(PointGeneral3D) + { + using point_t = model::pointNd_t<3, double>; + using box_t = model::boxNd_t<3, double>; + using plane_t = model::planeNd_t<3, double>; + + auto vpt = vector{ + point_t(0.0, 0.0, 0.0), + point_t(1.0, 1.0, 1.0), + point_t(2.0, 2.0, 2.0), + point_t(3.0, 3.0, 3.0), + point_t(4.0, 4.0, 4.0), + point_t(0.0, 0.0, 4.0), + point_t(0.0, 4.0, 0.0), + point_t(4.0, 0.0, 0.0), + point_t(1.5, 1.5, 1.0), + point_t(1.0, 1.5, 1.5), + }; + auto tree = boost::geometry::octree_point(vpt, 3, std::nullopt, 2); + + auto entityIDsInBFS = tree.CollectAllIdInBFS(tree.GetRootKey()); + auto entityIDsInDFS = tree.CollectAllIdInDFS(tree.GetRootKey()); + + auto searchBox = box_t(point_t(0.0, 0.0, 0.0), point_t(2.0, 2.0, 2.0) + ); + auto pointsInSearchBox = tree.RangeSearch(searchBox, vpt); + + auto sqrt3Reciproc = 1.0 / sqrt(3.0); + auto pointsInPlane = tree.PlaneSearch(2.6, point_t(sqrt3Reciproc, sqrt3Reciproc, sqrt3Reciproc), 0.3, vpt); + + auto sqrt2 = float_t(sqrt(2.0)); + auto sqrt2Reciproc = float_t(1.0 / sqrt2); + + auto pointsInFrustum = tree.FrustumCulling( // Cross-point of the planes: 2;2;2 + vector{ + {2 * sqrt2, { sqrt2Reciproc, sqrt2Reciproc, 0.0 }}, + {2 * sqrt2, { 0.0, sqrt2Reciproc, sqrt2Reciproc }}, + { 0.0, { sqrt2Reciproc, -sqrt2Reciproc, 0.0 }} + }, + 0.01f, + vpt); + + autoc n = vpt.size(); + vpt.push_back(point_t(1.0, 1.0, 1.5)); + tree.Insert(n, vpt.back()); + tree.Erase(0, vpt[0]); + auto entityIDsInDFS_AfterErase = tree.CollectAllIdInDFS(); + + auto searchPoint = point_t{ 1.0, 1.0, 1.0 }; + auto entityIDsKNN = tree.GetNearestNeighbors(searchPoint, 3, vpt); + + Assert::IsTrue(std::ranges::is_permutation(vector{ 0, 1, 2, 8, 9 }, pointsInSearchBox)); + Assert::IsTrue(std::ranges::is_permutation(vector{ 5, 6, 7, 8, 9 }, pointsInPlane)); + Assert::IsTrue(std::ranges::is_permutation(vector{ 1, 10, 8 }, entityIDsKNN)); + Assert::IsTrue(std::ranges::is_permutation(vector{ 2, 3, 4, 6, 7 }, pointsInFrustum)); + + Assert::IsTrue(vector{ 7, 6, 5, 0, 2, 1, 8, 9, 3, 4 } == entityIDsInBFS); + Assert::IsTrue(vector{ 0, 1, 8, 9, 7, 6, 5, 2, 3, 4 } == entityIDsInDFS); + Assert::IsTrue(vector{ 1, 8, 9, 10, 7, 6, 5, 2, 3, 4 } == entityIDsInDFS_AfterErase); + } + + TEST_METHOD(BoxGeneral2DC_Example2) + { + using point_t = model::pointNd_t<2, double>; + using box_t = model::boxNd_t<2, double>; + using plane_t = model::planeNd_t<2, double>; + + auto boxes = std::vector + { + box_t(point_t(0.0, 0.0), point_t(1.0, 1.0)), + box_t(point_t(1.0, 1.0), point_t(2.0, 2.0)), + box_t(point_t(2.0, 2.0), point_t(3.0, 3.0)), + box_t(point_t(3.0, 3.0), point_t(4.0, 4.0)), + box_t(point_t(1.2, 1.2), point_t(2.8, 2.8)) + }; + + auto quadtree = boost::geometry::quadtree_box_c( + boxes + , 3 // max depth + , std::nullopt // user-provided bounding Box for all + , 2 // max element in a node + , false // parallel calculation option + ); + + auto collidingIDPairs = quadtree.CollisionDetection(); //: { {1,4}, {2,4} } + + auto searchBox = box_t(point_t(1.0, 1.0), point_t(3.1, 3.1)); + + // Boxes within the range + auto insideBoxIDs = quadtree.RangeSearch(searchBox); //: { 1, 2, 4 } + + // Overlapping Boxes with the range + constexpr bool shouldFullyContain = false; // overlap is enough + auto overlappingBoxIDs = quadtree.RangeSearch(searchBox); //: { 1, 2, 3, 4 } + + // Picked boxes + auto pickPoint = point_t(2.5, 2.5); + auto pickedIDs = quadtree.PickSearch(pickPoint); //: { 2, 4 } + + // Ray intersections + auto rayBasePoint = point_t(1.5, 2.5); + auto rayHeading = point_t(1.5, 0.5); + auto firstIntersectedBox = quadtree.RayIntersectedFirst(rayBasePoint, rayHeading, 0.01); //: 4 + auto intersectedPoints = quadtree.RayIntersectedAll(rayBasePoint, rayHeading, 0.01); //: { 4, 2, 3 } in distance order! + auto sqrt2 = sqrtf(2.0); + auto sqrt2Reciproc = 1.0f / sqrt2; + auto boxesInFrustum = quadtree.FrustumCulling( // Cross-point of the planes: 2;2;2 + vector{ + plane_t{2.0 * sqrt2, point_t{ sqrt2Reciproc, sqrt2Reciproc }}, + plane_t{ 2.0, point_t{ 0.0f, -1.0f }} + }, + 0.01); + + // Collect all IDs in breadth/depth first order + auto entityIDsInDFS = quadtree.CollectAllIdInBFS(); + auto entityIDsInBFS = quadtree.CollectAllIdInDFS(); + + Assert::IsTrue(std::ranges::is_permutation( + vector>{ + {1, 4}, + {2, 4} + }, + collidingIDPairs)); + Assert::IsTrue(std::ranges::is_permutation(vector{ 1, 2, 4 }, insideBoxIDs)); + Assert::IsTrue(std::ranges::is_permutation(vector{ 1, 2, 3, 4 }, overlappingBoxIDs)); + Assert::IsTrue(std::ranges::is_permutation(vector{ 2, 4 }, pickedIDs)); + Assert::IsTrue(firstIntersectedBox.has_value()); + Assert::AreEqual(std::size_t(4), *firstIntersectedBox); + + Assert::IsTrue(std::ranges::is_permutation(vector{ 1, 2, 4 }, boxesInFrustum)); + + Assert::IsTrue(vector{ 4, 2, 3 } == intersectedPoints); + Assert::IsTrue(vector{ 4, 0, 1, 2, 3 } == entityIDsInDFS); + Assert::IsTrue(vector{ 4, 0, 1, 2, 3 } == entityIDsInBFS); + } + }; + } + } \ No newline at end of file diff --git a/unittests/packages.config b/unittests/packages.config index 5e4558c..fd0a622 100644 --- a/unittests/packages.config +++ b/unittests/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/unittests/unittests.vcxproj b/unittests/unittests.vcxproj index fe8e4c8..c183749 100644 --- a/unittests/unittests.vcxproj +++ b/unittests/unittests.vcxproj @@ -191,6 +191,7 @@ + @@ -205,11 +206,13 @@ + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + \ No newline at end of file diff --git a/unittests/unittests.vcxproj.filters b/unittests/unittests.vcxproj.filters index 74c47cc..0f28523 100644 --- a/unittests/unittests.vcxproj.filters +++ b/unittests/unittests.vcxproj.filters @@ -56,6 +56,9 @@ Header Files + + Header Files +