Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-04-17 08:35:32

0001 /// \file BVHsurfCreator.h
0002 /// Creator of BVH for a volume
0003 
0004 #ifndef VECGEOM_SURFACES_BVHCREATOR_H_
0005 #define VECGEOM_SURFACES_BVHCREATOR_H_
0006 
0007 #include <fstream>
0008 #include <VecGeom/management/ABBoxManager.h>
0009 #include <VecGeom/surfaces/Model.h>
0010 #include <VecGeom/base/BVH.h>
0011 
0012 namespace vgbrep {
0013 namespace bvh {
0014 
0015 enum class ConstructionAlgorithm : unsigned int {
0016   SplitLongestAxis         = 0,
0017   LargestDistanceAlongAxis = 1,
0018   SurfaceAreaHeuristic     = 2,
0019 };
0020 
0021 namespace {
0022 using namespace vecgeom;
0023 template <typename Real_b>
0024 int *splitAlongLongestAxis(const vecgeom::AABB<Real_b> *primitiveBoxes, int *begin, int *end,
0025                            const vecgeom::AABB<Real_b> &currentBVHNode)
0026 {
0027   const Vector3D<Precision> basis[] = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}};
0028   auto closestAxis                  = [](Vector3D<Precision> v) {
0029     v = v.Abs();
0030     return v[0] > v[2] ? (v[0] > v[1] ? 0 : 1) : (v[1] > v[2] ? 1 : 2);
0031   };
0032   Vector3D<Precision> p = currentBVHNode.Center();
0033   Vector3D<Precision> v = basis[closestAxis(currentBVHNode.Size())];
0034 
0035   return std::partition(begin, end, [&](size_t i) {
0036     Vector3D<Precision> center = static_cast<Vector3D<Precision>>(primitiveBoxes[i].Center());
0037     return Vector3D<Precision>::Dot(center - p, v) < 0.0;
0038   });
0039 }
0040 
0041 template <typename Real_b>
0042 int *largestDistanceAlongAxis(const vecgeom::AABB<Real_b> *primitiveBoxes, int *begin, int *end,
0043                               const vecgeom::AABB<Real_b> & /*currentBVHNode*/)
0044 {
0045   // Compute maximum extension of lower-left front corners along all axes
0046   float extension[3][2] = {{0.f, 0.f}, {0.f, 0.f}, {0.f, 0.f}};
0047   for (int axis = 0; axis <= 2; ++axis) {
0048     auto minMaxIt = std::minmax_element(
0049         begin, end, [=](size_t a, size_t b) { return primitiveBoxes[a].Min()[axis] < primitiveBoxes[b].Min()[axis]; });
0050     extension[axis][0] = primitiveBoxes[*minMaxIt.first].Min()[axis];
0051     extension[axis][1] = primitiveBoxes[*minMaxIt.second].Min()[axis];
0052   }
0053 
0054   const int splitAxis = std::distance(extension, std::max_element(extension, extension + 3, [](float a[], float b[]) {
0055                                         return a[1] - a[0] < b[1] - b[0];
0056                                       }));
0057   const float middlePoint = (extension[splitAxis][1] + extension[splitAxis][0]) / 2.f;
0058   return std::partition(begin, end, [=](size_t i) { return primitiveBoxes[i].Min()[splitAxis] < middlePoint; });
0059 }
0060 
0061 /**
0062  * In order to achieve stable splitting for the BVH, we cannot only sort by one axis. There needs
0063  * to be a strict order, so elements are not silently considered equal by the STL algorithms.
0064  * @param left,right Compute `left < right`.
0065  * @param sortAxis Principal axis to sort by.
0066  */
0067 template <typename T>
0068 bool less3D(const T &left, const T &right, const int sortAxis)
0069 {
0070   return left[sortAxis] < right[sortAxis] ||
0071          (left[sortAxis] == right[sortAxis] && (left[(sortAxis + 1) % 3] < right[(sortAxis + 1) % 3] ||
0072                                                 (left[(sortAxis + 1) % 3] == right[(sortAxis + 1) % 3] &&
0073                                                  left[(sortAxis + 2) % 3] < right[(sortAxis + 2) % 3])));
0074 }
0075 
0076 /**
0077  * Compute the surface areas of bounding boxes that surround the given primitives,
0078  * sweeping from left to right and vice-versa.
0079  *
0080  * For three objects 0 1 2, the vector contains the following surface areas:
0081  * ( | 0+1+2)  (0 | 1+2)   (0+1 | 2)
0082  *
0083  * That is, if the object N is intended to be the pivot object, the surface area of
0084  * - everything left of N is `areas[N].first`
0085  * - everything right of N + N itself is `areas[N].second`
0086  *
0087  * @param primitiveBoxes Array of bounding boxes of primitives.
0088  * @param begin Index of first primitive to be considered.
0089  * @param end   Past-the-end index of primitives to be considered.
0090  */
0091 template <typename Real_b>
0092 std::vector<std::pair<double, double>> sweepSurfaceArea(const vecgeom::AABB<Real_b> *primitiveBoxes, int const *begin,
0093                                                         int const *end)
0094 {
0095   if (begin >= end) return {};
0096 
0097   std::vector<std::pair<double, double>> areas(std::distance(begin, end), {0., 0.});
0098 
0099   vecgeom::AABB<Real_b> box{primitiveBoxes[*begin]};
0100   for (auto it = begin + 1; it < end; ++it) {
0101     areas[it - begin].first = box.SurfaceArea();
0102     box                     = vecgeom::AABB<Real_b>::Union(box, primitiveBoxes[*it]);
0103   }
0104 
0105   vecgeom::AABB<Real_b> box2{primitiveBoxes[*(end - 1)]};
0106   for (auto it = end - 1; it >= begin; --it) {
0107     box2                     = vecgeom::AABB<Real_b>::Union(box2, primitiveBoxes[*(it)]);
0108     areas[it - begin].second = box2.SurfaceArea();
0109   }
0110 
0111   return areas;
0112 }
0113 
0114 /**
0115  * Use the surface area heuristic to construct a BVH tree.
0116  * This algorithm tries to split the primitives such that they form clusters that have a minimal surface
0117  * area, as this decreases the likelihood that a BVH node is intersected by a ray.
0118  * Contrary to what's used in standard graphics, the cost function has an additional term that prevents
0119  * very large clusters. For the conventional SAH, a long line of equally spaced primitives would does not
0120  * yield an obvious splitting point, as all splits lead to the same total surface area for both child nodes.
0121  * To prevent this, there is an extra term, which will encourage a 50:50 split.
0122  * @param primitveBoxes Array of bounding boxes of primitives.
0123  * @param begin Index of first primitive to be considered.
0124  * @param end   Past-the-end index of primitives to be considered.
0125  * @return Index of the first element of the second group. If this is `end`, no good split was found.
0126  */
0127 template <typename Real_b>
0128 int *surfaceAreaHeuristic(const vecgeom::AABB<Real_b> *primitiveBoxes, int *begin, int *end,
0129                           const vecgeom::AABB<Real_b> & /*currentBVHNode*/)
0130 {
0131   int bestSplitAxis          = -1;
0132   double bestTraversalMetric = std::distance(begin, end);
0133   int bestSplitObject        = -1;
0134   const auto nObj            = std::distance(begin, end);
0135 
0136   int currentSortAxis = 0;
0137   auto sorter         = [primitiveBoxes, &currentSortAxis](int a, int b) {
0138     const auto centroidA   = primitiveBoxes[a].Center();
0139     const auto centroidB   = primitiveBoxes[b].Center();
0140     constexpr double shift = 0.01;
0141     return less3D(centroidA + shift * (centroidA - primitiveBoxes[a].Min()),
0142                           centroidB + shift * (centroidB - primitiveBoxes[b].Min()), currentSortAxis);
0143   };
0144 
0145   for (int axis = 0; axis <= 2; ++axis) {
0146     // Sort centroids along axis
0147     currentSortAxis = axis;
0148     std::sort(begin, end, sorter);
0149 
0150     // Sweep axis looking for best split
0151     const std::vector<std::pair<double, double>> surfaceSweep = sweepSurfaceArea(primitiveBoxes, begin, end);
0152     const auto totSurfArea                                    = surfaceSweep.front().second;
0153 
0154     for (int *splitObject = begin; splitObject < end; ++splitObject) {
0155       const auto left  = surfaceSweep[splitObject - begin].first / NonZero(totSurfArea);
0156       const auto right = surfaceSweep[splitObject - begin].second / NonZero(totSurfArea);
0157       VECGEOM_ASSERT(left <= 1. && right <= 1.);
0158 
0159       // Original heuristic
0160       const auto splitMetric = left * std::distance(begin, splitObject) + right * std::distance(splitObject, end) +
0161                                0.1 * abs(nObj / 2 - std::distance(begin, splitObject) / nObj); // Prefer balanced splits
0162 
0163       if (splitMetric < bestTraversalMetric) {
0164         bestTraversalMetric = splitMetric;
0165         bestSplitAxis       = axis;
0166         bestSplitObject     = *splitObject;
0167       }
0168     }
0169   }
0170 
0171   if (bestSplitAxis == -1) return end;
0172 
0173   currentSortAxis = bestSplitAxis;
0174   auto result = std::partition(begin, end, [sorter, bestSplitObject](size_t i) { return sorter(i, bestSplitObject); });
0175 
0176   return result;
0177 }
0178 
0179 /**
0180  * Array of splitting functions that can be used to construct the BVH tree.
0181  * @see ConstructionAlgorithm
0182  */
0183 template <typename Real_b>
0184 int *(*splittingFunction[])(const vecgeom::AABB<Real_b> * /*primitveAABBs*/, int * /*firstPrimitive*/,
0185                             int * /*lastPrimitive*/, const vecgeom::AABB<Real_b> & /*currentBVHNode*/) = {
0186     &splitAlongLongestAxis,
0187     &largestDistanceAlongAxis,
0188     &surfaceAreaHeuristic,
0189 };
0190 
0191 } // anonymous namespace
0192 
0193 /*
0194  * BVH::ComputeNodes() initializes nodes of the BVH. It first computes the number of children that
0195  * belong to the current node based on the iterator range that is passed as input, as well as the
0196  * offset where the children of this node start. Then, it computes the overall bounding box of the
0197  * current node as the union of all bounding boxes of its child volumes. Then, if recursion should
0198  * continue, a splitting plane is chosen based on the longest dimension of the bounding box for the
0199  * current node, and the children are sorted such that all children on each side of the splitting
0200  * plane are stored contiguously. Then the function is called recursively with the iterator
0201  * sub-ranges for volumes on each side of the splitting plane to construct its left and right child
0202  * nodes. Recursion stops if a child node is deeper than the maximum depth, if the iterator range
0203  * is empty (i.e. no volumes on this node, maybe because all child volumes' centroids are on the
0204  * same side of the splitting plane), or if the node contains only a single volume.
0205  */
0206 
0207 template <typename Real_b>
0208 void ComputeNodes(unsigned int id, int *first, int *last, unsigned int nodes, int *aPrimId, int *aNChild, int *aOffset,
0209                   vecgeom::AABB<Real_b> *aNodes, vecgeom::AABB<Real_b> *aAABBs,
0210                   ConstructionAlgorithm constructionAlgorithm)
0211 {
0212   if (id >= nodes) return;
0213 
0214   aNChild[id] = std::distance(first, last);
0215   aOffset[id] = std::distance(aPrimId, first);
0216 
0217   // Node without children. Stop recursing here.
0218   if (first == last) return;
0219 
0220   aNodes[id] = aAABBs[*first];
0221   for (auto it = std::next(first); it != last; ++it)
0222     aNodes[id] = vecgeom::AABB<Real_b>::Union(aNodes[id], aAABBs[*it]);
0223 
0224   // Only one child. No need to continue
0225   if (std::next(first) == last) return;
0226 
0227   const auto algo = static_cast<unsigned int>(constructionAlgorithm);
0228   // VECGEOM_ASSERT(algo < sizeof(splittingFunction<Real_b>));
0229 
0230   int *pivot = splittingFunction<Real_b>[algo](aAABBs, first, last, aNodes[id]);
0231   VECGEOM_ASSERT(first <= pivot && pivot <= last);
0232 
0233   ComputeNodes(2 * id + 1, first, pivot, nodes, aPrimId, aNChild, aOffset, aNodes, aAABBs, constructionAlgorithm);
0234   ComputeNodes(2 * id + 2, pivot, last, nodes, aPrimId, aNChild, aOffset, aNodes, aAABBs, constructionAlgorithm);
0235 }
0236 
0237 /// @brief Compute a BVH tree for the specified LogicalVolume
0238 /// @tparam Real_t
0239 /// @param ivol ID of the logical volume for which this BVH is built
0240 /// @param bvh BVH instance that will be initialized
0241 /// @param surfData
0242 /// @param surfacesBVH If false, build the BVH from the AABBs of the daughter volumes, instead of the AABBs of the entering and exiting surfaces
0243 /// @param depth Optionally, the depth of the binary tree
0244 template <typename Real_t>
0245 static void InitBVH(int ivol, BVH<typename vgbrep::SurfData<Real_t>::Real_b> &bvh,
0246                     vgbrep::CPUsurfData<Precision> const &cpudata,
0247                     Vector3D<typename vgbrep::SurfData<Real_t>::Real_b> *boxes, int nBoxes, int depth = 0)
0248 {
0249   using Real_b = typename vgbrep::SurfData<Real_t>::Real_b;
0250   uint aRootId = ivol;
0251 
0252   if (nBoxes <= 0) throw std::logic_error("Cannot construct BVH for volume with no surfaces!");
0253 
0254   auto aRootNChild = nBoxes;
0255 
0256   auto aAABBs = new vecgeom::AABB<Real_b>[nBoxes];
0257   for (auto i = 0; i < nBoxes; ++i)
0258     aAABBs[i] = vecgeom::AABB(boxes[2 * i], boxes[2 * i + 1]);
0259 
0260   /* Initialize map of primitive ids (i.e. child volume ids) as {0, 1, 2, ...}. */
0261   auto aPrimId = new int[nBoxes];
0262   std::iota(aPrimId, aPrimId + nBoxes, 0);
0263 
0264   /*
0265    * If depth = 0, choose depth dynamically based on the number of child volumes, up to the fixed
0266    * maximum depth. We use n/2 here because that creates a tree with roughly one node for every two
0267    * volumes, or roughly at most 4 children per leaf node. For example, for 1000 volumes, the
0268    * default depth would be log2(500) = 8.96 -> 8, with 2^8 - 1 = 511 nodes, and 256 leaf nodes.
0269    */
0270   int aDepth = std::min(depth ? depth : std::max(0, (int)std::log2(nBoxes / 2)), BVH<Real_b>::BVH_MAX_DEPTH);
0271 
0272   unsigned int nodes = (2 << aDepth) - 1;
0273 
0274   auto aNChild = new int[nodes];
0275   auto aOffset = new int[nodes];
0276   auto aNodes  = new vecgeom::AABB<Real_b>[nodes];
0277   std::fill(aNChild, aNChild + nodes, 0);
0278   std::fill(aOffset, aOffset + nodes, -1);
0279 
0280   /* Recursively initialize BVH nodes starting at the root node */
0281   ComputeNodes(0, aPrimId, aPrimId + nBoxes, nodes, aPrimId, aNChild, aOffset, aNodes, aAABBs,
0282                ConstructionAlgorithm::SurfaceAreaHeuristic);
0283 
0284   /* Mark internal nodes with a negative number of children to simplify traversal */
0285   for (unsigned int id = 0; id < nodes / 2; ++id)
0286     if (aNChild[id] > 8 && (aNChild[id] == aNChild[2 * id + 1] + aNChild[2 * id + 2])) aNChild[id] = -1;
0287 
0288   /* Fill the BVH */
0289   bvh.Set(aRootId, aRootNChild, aDepth, aPrimId, aAABBs, aOffset, aNChild, aNodes);
0290 }
0291 
0292 // Dump the BVH in binary format
0293 template <typename Real_b>
0294 static void DumpBVH(const BVH<Real_b> &bvh, const char *filename)
0295 {
0296   auto fillAABBbuffer = [](const vecgeom::AABB<Real_b> *aabbs, int n, double *buffer) {
0297     for (auto i = 0; i < n; ++i) {
0298       buffer[6 * i]     = aabbs[i].Min()[0];
0299       buffer[6 * i + 1] = aabbs[i].Min()[1];
0300       buffer[6 * i + 2] = aabbs[i].Min()[2];
0301       buffer[6 * i + 3] = aabbs[i].Max()[0];
0302       buffer[6 * i + 4] = aabbs[i].Max()[1];
0303       buffer[6 * i + 5] = aabbs[i].Max()[2];
0304     }
0305   };
0306 
0307   std::ofstream stm(filename, std::ios::binary);
0308   if (!stm.is_open()) {
0309     std::cout << "bad file " << filename << "\n";
0310     return;
0311   }
0312   auto aRootId = bvh.GetRootId();
0313   stm.write(reinterpret_cast<const char *>(&aRootId), sizeof(aRootId));
0314   auto aRootNChild = bvh.GetRootNChild();
0315   stm.write(reinterpret_cast<const char *>(&aRootNChild), sizeof(aRootNChild));
0316   auto aDepth = bvh.GetDepth();
0317   stm.write(reinterpret_cast<const char *>(&aDepth), sizeof(aDepth));
0318   unsigned int nodes = (2 << aDepth) - 1;
0319   auto aPrimId       = bvh.GetPrimId();
0320   stm.write(reinterpret_cast<const char *>(aPrimId), aRootNChild * sizeof(int));
0321   auto aOffset = bvh.GetOffset();
0322   stm.write(reinterpret_cast<const char *>(aOffset), nodes * sizeof(int));
0323   auto aNChild = bvh.GetNChild();
0324   stm.write(reinterpret_cast<const char *>(aNChild), nodes * sizeof(int));
0325   double *buffer = new double[6 * (nodes + aRootNChild)];
0326   fillAABBbuffer(bvh.GetNodes(), nodes, buffer);
0327   stm.write(reinterpret_cast<const char *>(buffer), 6 * nodes * sizeof(double));
0328   fillAABBbuffer(bvh.GetAABBs(), aRootNChild, buffer + 6 * nodes);
0329   stm.write(reinterpret_cast<const char *>(buffer + 6 * nodes), 6 * aRootNChild * sizeof(double));
0330   stm.close();
0331   delete[] buffer;
0332 }
0333 
0334 } // namespace bvh
0335 } // namespace vgbrep
0336 
0337 #endif