diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1e21e86438b..7a301669d2c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,12 @@ include_directories(BEFORE "${PROJECT_BINARY_DIR}/data" ) +# Tracy profiler integration (optional, propagates to all OBJECT libraries) +if(COIN_USE_TRACY) + add_definitions(-DTRACY_ENABLE -DCOIN_USE_TRACY) + include_directories("${COIN_TRACY_INCLUDE_DIR}") +endif() + # add include dirs for non targets if(NOT "${COIN_TARGET_INCLUDE_DIRECTORIES}" STREQUAL "") include_directories(${COIN_TARGET_INCLUDE_DIRECTORIES}) @@ -264,6 +270,20 @@ target_include_directories(${PROJECT_NAME} $ ) target_link_libraries(${PROJECT_NAME} PRIVATE ${COIN_TARGET_LINK_LIBRARIES}) + +# Tracy profiler integration (optional) +# Use -undefined dynamic_lookup on macOS so Tracy symbols are resolved lazily +# at runtime. When loaded by FreeCAD (TracyClient present), profiling is active. +# When loaded by pivy standalone, CoinTracyConfig.h's runtime check via dlsym +# detects TracyClient's absence and disables zones. +if(COIN_USE_TRACY) + target_include_directories(${PROJECT_NAME} PRIVATE "${COIN_TRACY_INCLUDE_DIR}") + target_compile_definitions(${PROJECT_NAME} PRIVATE TRACY_ENABLE COIN_USE_TRACY) + if(APPLE) + target_link_options(${PROJECT_NAME} PRIVATE "-undefined" "dynamic_lookup") + endif() +endif() + if(NOT COIN_BUILD_MAC_FRAMEWORK) target_include_directories(${PROJECT_NAME} INTERFACE $) endif() diff --git a/src/CoinTracyConfig.h b/src/CoinTracyConfig.h new file mode 100644 index 00000000000..a248d3281d7 --- /dev/null +++ b/src/CoinTracyConfig.h @@ -0,0 +1,11 @@ +#ifndef COIN_TRACY_CONFIG_H +#define COIN_TRACY_CONFIG_H + +#ifdef COIN_USE_TRACY +#include +#define CoinZoneScopedN(name) ZoneScopedN(name) +#else +#define CoinZoneScopedN(x) +#endif + +#endif // COIN_TRACY_CONFIG_H diff --git a/src/actions/SoHandleEventAction.cpp b/src/actions/SoHandleEventAction.cpp index be52485aeea..0c49c178ffa 100644 --- a/src/actions/SoHandleEventAction.cpp +++ b/src/actions/SoHandleEventAction.cpp @@ -49,6 +49,8 @@ #include +#include "CoinTracyConfig.h" + #include #include #include @@ -61,6 +63,7 @@ #include #include "actions/SoSubActionP.h" +#include "caches/SoSceneBVH.h" // ************************************************************************* @@ -357,6 +360,7 @@ SoHandleEventAction::getPickedPoint(void) const SoPickedPointList & SoHandleEventAction::getPickedPointList(void) { + CoinZoneScopedN("SoHandleEventAction::getPickedPointList"); SoRayPickAction * ra = PRIVATE(this)->getPickAction(); if (!PRIVATE(this)->pickvalid || !PRIVATE(this)->didpickall) { ra->setPickAll(TRUE); @@ -411,10 +415,20 @@ SoHandleEventActionP::getPickAction(void) const void SoHandleEventActionP::doPick(SoRayPickAction * ra) { + CoinZoneScopedN("SoHandleEventAction::doPick"); if (!this->event || !this->pickroot) return; SbBool didapply = FALSE; ra->setPoint(this->event->getPosition()); + + // Scene-level BVH: set the active BVH for SoShape::rayPick() to use. + // On first pick: shapes collect their world-space bboxes. + // On subsequent picks: the first shape triggers a BVH query to populate + // the candidate set, then all shapes self-filter. + SoSceneBVH * sceneBVH = SoSceneBVH::getForRoot(this->pickroot); + SoSceneBVH::setCollecting(sceneBVH); + SoSceneBVH::setActiveCandidates(NULL); // reset from previous pick + if (this->owner->getWhatAppliedTo() == SoAction::PATH) { const SoPath * path = this->owner->getPathAppliedTo(); if (path->getHead() == this->pickroot) { @@ -436,6 +450,14 @@ SoHandleEventActionP::doPick(SoRayPickAction * ra) } } if (!didapply) ra->apply(this->pickroot); + + SoSceneBVH::setCollecting(NULL); + + // Build scene BVH from collected shapes (first pick only) + if (sceneBVH && !sceneBVH->isBuilt() && sceneBVH->numShapes() > 0) { + sceneBVH->build(); + } + this->didpickall = ra->isPickAll(); this->pickvalid = TRUE; } diff --git a/src/caches/CMakeLists.txt b/src/caches/CMakeLists.txt index a4a8077192c..5004d3ab39c 100644 --- a/src/caches/CMakeLists.txt +++ b/src/caches/CMakeLists.txt @@ -1,6 +1,9 @@ # source files set(COIN_CACHES_FILES SoBoundingBoxCache.cpp + SoBVHCache.cpp + SoChildBVH.cpp + SoSceneBVH.cpp SoCache.cpp SoConvexDataCache.cpp SoGLCacheList.cpp @@ -15,6 +18,12 @@ set(COIN_CACHES_FILES # Files excluded from public API documentation, included in complete documentation. set(COIN_CACHES_INTERNAL_FILES + SoBVHCache.h + SoBVHCache.cpp + SoChildBVH.h + SoChildBVH.cpp + SoSceneBVH.h + SoSceneBVH.cpp SoGlyphCache.h SoGlyphCache.cpp SoShaderProgramCache.h diff --git a/src/caches/SoBVHCache.cpp b/src/caches/SoBVHCache.cpp new file mode 100644 index 00000000000..f26c728b01e --- /dev/null +++ b/src/caches/SoBVHCache.cpp @@ -0,0 +1,583 @@ +/**************************************************************************\ + * Copyright (c) Kongsberg Oil & Gas Technologies AS + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +\**************************************************************************/ + +#include "SoBVHCache.h" +#include "CoinTracyConfig.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +// ----------------------------------------------------------------------- +// BVH node: 32 bytes, cache-line friendly flat layout +// ----------------------------------------------------------------------- + +struct SbBVHNode { + SbBox3f bbox; + union { + int32_t rightChild; // internal node: index of right child + int32_t firstTriangle; // leaf node: offset into triIndices + }; + int16_t numTriangles; // 0 = internal, >0 = leaf + int16_t splitAxis; // 0=X, 1=Y, 2=Z +}; + +// ----------------------------------------------------------------------- +// SAH binning constants +// ----------------------------------------------------------------------- + +static const int NUM_BINS = 16; +static const int MAX_LEAF_TRIS = 8; +static const float TRAVERSAL_COST = 1.0f; +static const float INTERSECT_COST = 1.0f; + +// ----------------------------------------------------------------------- +// Private implementation +// ----------------------------------------------------------------------- + +class SoBVHCacheP { +public: + SoBVHCacheP() + : vertices(NULL), normals(NULL), texcoords(NULL), colors(NULL), + indices(NULL), numVertices(0), numTriangleIndices(0), built(FALSE) {} + + ~SoBVHCacheP() { + delete[] vertices; + delete[] normals; + delete[] texcoords; + delete[] colors; + delete[] indices; + } + + // Copied vertex data (independent of PVC lifetime) + SbVec3f * vertices; + SbVec3f * normals; + SbVec4f * texcoords; + uint8_t * colors; + GLint * indices; + int numVertices; + int numTriangleIndices; + + // BVH structure + std::vector nodes; + std::vector triIndices; + + SbBool built; + + // Build helpers + void buildBVH(void); + int buildRecursive(int begin, int end, std::vector & centroids); + SbBox3f computeTriangleBBox(int triIdx) const; + SbVec3f computeTriangleCentroid(int triIdx) const; + float computeSurfaceArea(const SbBox3f & box) const; +}; + +// ----------------------------------------------------------------------- +// SoBVHCache implementation +// ----------------------------------------------------------------------- + +SoBVHCache::SoBVHCache(SoState * state) + : inherited(state) +{ + this->pimpl = new SoBVHCacheP; +} + +SoBVHCache::~SoBVHCache() +{ + delete this->pimpl; +} + +void +SoBVHCache::build(const SbVec3f * vertices, + const SbVec3f * normals, + const SbVec4f * texcoords, + const uint8_t * colors, + int numVertices, + const GLint * indices, + int numTriangleIndices) +{ + SoBVHCacheP * p = this->pimpl; + + // Clean up any previous data + delete[] p->vertices; + delete[] p->normals; + delete[] p->texcoords; + delete[] p->colors; + delete[] p->indices; + + p->numVertices = numVertices; + p->numTriangleIndices = numTriangleIndices; + + // Copy vertex data so we're independent of PVC lifetime + p->vertices = new SbVec3f[numVertices]; + std::memcpy(p->vertices, vertices, numVertices * sizeof(SbVec3f)); + + if (normals) { + p->normals = new SbVec3f[numVertices]; + std::memcpy(p->normals, normals, numVertices * sizeof(SbVec3f)); + } else { + p->normals = NULL; + } + + if (texcoords) { + p->texcoords = new SbVec4f[numVertices]; + std::memcpy(p->texcoords, texcoords, numVertices * sizeof(SbVec4f)); + } else { + p->texcoords = NULL; + } + + if (colors) { + // 4 bytes per vertex (RGBA) + p->colors = new uint8_t[numVertices * 4]; + std::memcpy(p->colors, colors, numVertices * 4); + } else { + p->colors = NULL; + } + + p->indices = new GLint[numTriangleIndices]; + std::memcpy(p->indices, indices, numTriangleIndices * sizeof(GLint)); + + p->buildBVH(); +} + +void +SoBVHCache::buildFlat(const SbVec3f * triVertices, + const SbVec3f * triNormals, + int numTriangles) +{ + SoBVHCacheP * p = this->pimpl; + + // Clean up any previous data + delete[] p->vertices; + delete[] p->normals; + delete[] p->texcoords; + delete[] p->colors; + delete[] p->indices; + + int numVerts = numTriangles * 3; + p->numVertices = numVerts; + p->numTriangleIndices = numVerts; + + // Copy flat vertex data (3 vertices per triangle, no deduplication) + p->vertices = new SbVec3f[numVerts]; + std::memcpy(p->vertices, triVertices, numVerts * sizeof(SbVec3f)); + + if (triNormals) { + p->normals = new SbVec3f[numVerts]; + std::memcpy(p->normals, triNormals, numVerts * sizeof(SbVec3f)); + } else { + p->normals = NULL; + } + + p->texcoords = NULL; + p->colors = NULL; + + // Generate identity indices: [0,1,2, 3,4,5, ...] + p->indices = new GLint[numVerts]; + for (int i = 0; i < numVerts; i++) { + p->indices[i] = i; + } + + p->buildBVH(); +} + +SbBool +SoBVHCache::rayPick(SoRayPickAction * action, SoShape * shape) const +{ + CoinZoneScopedN("SoBVHCache::rayPick"); + SoBVHCacheP * p = this->pimpl; + + if (!p->built || p->nodes.empty()) return FALSE; + + const SbBool cwFlip = + (SoShapeHintsElement::getVertexOrdering(action->getState()) == + SoShapeHintsElement::CLOCKWISE); + + // Stack-based BVH traversal + int stack[64]; + int stackPtr = 0; + stack[stackPtr++] = 0; // root node + + while (stackPtr > 0) { + const int nodeIdx = stack[--stackPtr]; + const SbBVHNode & node = p->nodes[nodeIdx]; + + // AABB test using action->intersect(box) which handles both ray and cone picks + SbBox3f bbox = node.bbox; + if (!action->intersect(bbox, TRUE)) { + continue; + } + + if (node.numTriangles > 0) { + // Leaf node: test triangles + const int end = node.firstTriangle + node.numTriangles; + for (int i = node.firstTriangle; i < end; i++) { + const int triIdx = p->triIndices[i]; + const int baseIdx = triIdx * 3; + const SbVec3f & v0 = p->vertices[p->indices[baseIdx]]; + const SbVec3f & v1 = p->vertices[p->indices[baseIdx + 1]]; + const SbVec3f & v2 = p->vertices[p->indices[baseIdx + 2]]; + + SbVec3f intersection, barycentric; + SbBool front; + + if (action->intersect(v0, v1, v2, intersection, barycentric, front)) { + if (action->isBetweenPlanes(intersection)) { + if (cwFlip) front = !front; + + SoPickedPoint * pp = action->addIntersection(intersection, front); + if (pp) { + // Interpolate normal using barycentric coordinates + if (p->normals) { + SbVec3f n = + p->normals[p->indices[baseIdx]] * barycentric[0] + + p->normals[p->indices[baseIdx + 1]] * barycentric[1] + + p->normals[p->indices[baseIdx + 2]] * barycentric[2]; + n.normalize(); + pp->setObjectNormal(n); + } + + // Interpolate texture coordinates + if (p->texcoords) { + SbVec4f tc = + p->texcoords[p->indices[baseIdx]] * barycentric[0] + + p->texcoords[p->indices[baseIdx + 1]] * barycentric[1] + + p->texcoords[p->indices[baseIdx + 2]] * barycentric[2]; + pp->setObjectTextureCoords(tc); + } + + // Material index: use vertex with largest barycentric weight + float maxval = barycentric[0]; + int maxidx = p->indices[baseIdx]; + if (barycentric[1] > maxval) { + maxval = barycentric[1]; + maxidx = p->indices[baseIdx + 1]; + } + if (barycentric[2] > maxval) { + maxidx = p->indices[baseIdx + 2]; + } + pp->setMaterialIndex(maxidx); + } + } + } + } + } + else { + // Internal node: push children + // Push right child first so left child (nodeIdx+1) is processed first + if (node.rightChild > 0) { + if (stackPtr < 64) stack[stackPtr++] = node.rightChild; + } + if (stackPtr < 64) stack[stackPtr++] = nodeIdx + 1; + } + } + + return TRUE; +} + +int +SoBVHCache::getNumTriangles(void) const +{ + return this->pimpl->numTriangleIndices / 3; +} + +SbBool +SoBVHCache::isBuilt(void) const +{ + return this->pimpl->built; +} + +// ----------------------------------------------------------------------- +// BVH build implementation +// ----------------------------------------------------------------------- + +SbBox3f +SoBVHCacheP::computeTriangleBBox(int triIdx) const +{ + const int baseIdx = triIdx * 3; + const SbVec3f & v0 = this->vertices[this->indices[baseIdx]]; + const SbVec3f & v1 = this->vertices[this->indices[baseIdx + 1]]; + const SbVec3f & v2 = this->vertices[this->indices[baseIdx + 2]]; + + SbBox3f box; + box.extendBy(v0); + box.extendBy(v1); + box.extendBy(v2); + return box; +} + +SbVec3f +SoBVHCacheP::computeTriangleCentroid(int triIdx) const +{ + const int baseIdx = triIdx * 3; + const SbVec3f & v0 = this->vertices[this->indices[baseIdx]]; + const SbVec3f & v1 = this->vertices[this->indices[baseIdx + 1]]; + const SbVec3f & v2 = this->vertices[this->indices[baseIdx + 2]]; + + return SbVec3f((v0[0] + v1[0] + v2[0]) / 3.0f, + (v0[1] + v1[1] + v2[1]) / 3.0f, + (v0[2] + v1[2] + v2[2]) / 3.0f); +} + +float +SoBVHCacheP::computeSurfaceArea(const SbBox3f & box) const +{ + if (box.isEmpty()) return 0.0f; + SbVec3f size = box.getMax() - box.getMin(); + return 2.0f * (size[0] * size[1] + size[0] * size[2] + size[1] * size[2]); +} + +void +SoBVHCacheP::buildBVH(void) +{ + const int numTris = this->numTriangleIndices / 3; + if (numTris == 0) { + this->built = FALSE; + return; + } + + // Initialize triangle index array + this->triIndices.resize(numTris); + for (int i = 0; i < numTris; i++) { + this->triIndices[i] = i; + } + + // Precompute centroids + std::vector centroids(numTris); + for (int i = 0; i < numTris; i++) { + centroids[i] = this->computeTriangleCentroid(i); + } + + // Reserve space for nodes (roughly 2*N/leafSize nodes) + this->nodes.clear(); + this->nodes.reserve(numTris * 2 / MAX_LEAF_TRIS + 1); + + this->buildRecursive(0, numTris, centroids); + this->built = TRUE; +} + +int +SoBVHCacheP::buildRecursive(int begin, int end, + std::vector & centroids) +{ + const int numTris = end - begin; + const int nodeIdx = static_cast(this->nodes.size()); + this->nodes.push_back(SbBVHNode()); + + // Compute bounding box of all triangles in this range + SbBox3f nodeBBox; + for (int i = begin; i < end; i++) { + SbBox3f triBBox = this->computeTriangleBBox(this->triIndices[i]); + nodeBBox.extendBy(triBBox.getMin()); + nodeBBox.extendBy(triBBox.getMax()); + } + + this->nodes[nodeIdx].bbox = nodeBBox; + + // Create leaf if few enough triangles + if (numTris <= MAX_LEAF_TRIS) { + this->nodes[nodeIdx].firstTriangle = begin; + this->nodes[nodeIdx].numTriangles = static_cast(numTris); + this->nodes[nodeIdx].splitAxis = 0; + return nodeIdx; + } + + // Compute centroid bounding box for SAH binning + SbBox3f centroidBBox; + for (int i = begin; i < end; i++) { + centroidBBox.extendBy(centroids[this->triIndices[i]]); + } + + SbVec3f centroidExtent = centroidBBox.getMax() - centroidBBox.getMin(); + + // Find longest axis + int bestAxis = 0; + if (centroidExtent[1] > centroidExtent[bestAxis]) bestAxis = 1; + if (centroidExtent[2] > centroidExtent[bestAxis]) bestAxis = 2; + + // Degenerate case: all centroids at the same point + if (centroidExtent[bestAxis] < FLT_EPSILON) { + this->nodes[nodeIdx].firstTriangle = begin; + this->nodes[nodeIdx].numTriangles = static_cast(numTris); + this->nodes[nodeIdx].splitAxis = 0; + return nodeIdx; + } + + // SAH binning + struct Bin { + SbBox3f bbox; + int count; + }; + + float bestCost = FLT_MAX; + int bestSplit = -1; + int bestSplitAxis = bestAxis; + + // Try all 3 axes, starting with the longest + for (int axisIter = 0; axisIter < 3; axisIter++) { + int axis = (bestAxis + axisIter) % 3; + if (centroidExtent[axis] < FLT_EPSILON) continue; + + Bin bins[NUM_BINS]; + for (int i = 0; i < NUM_BINS; i++) { + bins[i].count = 0; + } + + float axisMin = centroidBBox.getMin()[axis]; + float axisRange = centroidExtent[axis]; + float invRange = (NUM_BINS - 0.001f) / axisRange; + + // Bin triangles + for (int i = begin; i < end; i++) { + int triIdx = this->triIndices[i]; + float c = centroids[triIdx][axis]; + int binIdx = static_cast((c - axisMin) * invRange); + if (binIdx < 0) binIdx = 0; + if (binIdx >= NUM_BINS) binIdx = NUM_BINS - 1; + + bins[binIdx].count++; + SbBox3f triBBox = this->computeTriangleBBox(triIdx); + bins[binIdx].bbox.extendBy(triBBox.getMin()); + bins[binIdx].bbox.extendBy(triBBox.getMax()); + } + + // Sweep from left to compute prefix surface areas and counts + SbBox3f leftBox; + int leftCount[NUM_BINS]; + float leftArea[NUM_BINS]; + + int runningCount = 0; + for (int i = 0; i < NUM_BINS - 1; i++) { + if (bins[i].count > 0 && !bins[i].bbox.isEmpty()) { + leftBox.extendBy(bins[i].bbox.getMin()); + leftBox.extendBy(bins[i].bbox.getMax()); + } + runningCount += bins[i].count; + leftCount[i] = runningCount; + leftArea[i] = this->computeSurfaceArea(leftBox); + } + + // Sweep from right and evaluate SAH + SbBox3f rightBox; + runningCount = 0; + for (int i = NUM_BINS - 1; i >= 1; i--) { + if (bins[i].count > 0 && !bins[i].bbox.isEmpty()) { + rightBox.extendBy(bins[i].bbox.getMin()); + rightBox.extendBy(bins[i].bbox.getMax()); + } + runningCount += bins[i].count; + + float rightArea = this->computeSurfaceArea(rightBox); + float cost = TRAVERSAL_COST + + INTERSECT_COST * (leftCount[i - 1] * leftArea[i - 1] + + runningCount * rightArea) / + this->computeSurfaceArea(nodeBBox); + + if (cost < bestCost && leftCount[i - 1] > 0 && runningCount > 0) { + bestCost = cost; + bestSplit = i; + bestSplitAxis = axis; + } + } + } + + // If SAH split isn't better than a leaf, make a leaf + float leafCost = INTERSECT_COST * numTris; + if (bestSplit == -1 || bestCost >= leafCost) { + // But cap leaf size to prevent overly large leaves + if (numTris <= MAX_LEAF_TRIS * 4) { + this->nodes[nodeIdx].firstTriangle = begin; + this->nodes[nodeIdx].numTriangles = static_cast(numTris); + this->nodes[nodeIdx].splitAxis = 0; + return nodeIdx; + } + // Force a median split on the best axis + bestSplitAxis = bestAxis; + bestSplit = NUM_BINS / 2; + } + + // Partition triangles around the split + float axisMin = centroidBBox.getMin()[bestSplitAxis]; + float axisRange = centroidExtent[bestSplitAxis]; + float invRange = (NUM_BINS - 0.001f) / axisRange; + + // Stable partition: triangles with bin < bestSplit go left + auto midIt = std::partition( + this->triIndices.begin() + begin, + this->triIndices.begin() + end, + [&](int32_t triIdx) { + float c = centroids[triIdx][bestSplitAxis]; + int binIdx = static_cast((c - axisMin) * invRange); + if (binIdx < 0) binIdx = 0; + if (binIdx >= NUM_BINS) binIdx = NUM_BINS - 1; + return binIdx < bestSplit; + } + ); + + int mid = static_cast(midIt - this->triIndices.begin()); + + // Safety: if partition failed to split, force a median split + if (mid == begin || mid == end) { + mid = (begin + end) / 2; + std::nth_element( + this->triIndices.begin() + begin, + this->triIndices.begin() + mid, + this->triIndices.begin() + end, + [&](int32_t a, int32_t b) { + return centroids[a][bestSplitAxis] < centroids[b][bestSplitAxis]; + } + ); + } + + this->nodes[nodeIdx].numTriangles = 0; // internal node + this->nodes[nodeIdx].splitAxis = static_cast(bestSplitAxis); + + // Build left child (will be at nodeIdx + 1 due to depth-first order) + this->buildRecursive(begin, mid, centroids); + + // Build right child + int rightIdx = this->buildRecursive(mid, end, centroids); + this->nodes[nodeIdx].rightChild = rightIdx; + + return nodeIdx; +} diff --git a/src/caches/SoBVHCache.h b/src/caches/SoBVHCache.h new file mode 100644 index 00000000000..ea02e6b15f6 --- /dev/null +++ b/src/caches/SoBVHCache.h @@ -0,0 +1,76 @@ +#ifndef COIN_SOBVHCACHE_H +#define COIN_SOBVHCACHE_H + +/**************************************************************************\ + * Copyright (c) Kongsberg Oil & Gas Technologies AS + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +\**************************************************************************/ + +#include +#include +#include +#include +#include + +class SoRayPickAction; +class SoShape; +class SoBVHCacheP; + +class SoBVHCache : public SoCache { + typedef SoCache inherited; +public: + SoBVHCache(SoState * state); + virtual ~SoBVHCache(); + + void build(const SbVec3f * vertices, + const SbVec3f * normals, + const SbVec4f * texcoords, + const uint8_t * colors, + int numVertices, + const GLint * indices, + int numTriangleIndices); + + void buildFlat(const SbVec3f * triVertices, + const SbVec3f * triNormals, + int numTriangles); + + SbBool rayPick(SoRayPickAction * action, SoShape * shape) const; + + int getNumTriangles(void) const; + SbBool isBuilt(void) const; + +private: + SoBVHCacheP * pimpl; + + SoBVHCache(const SoBVHCache & rhs); + SoBVHCache & operator = (const SoBVHCache & rhs); +}; + +#endif // COIN_SOBVHCACHE_H diff --git a/src/caches/SoChildBVH.cpp b/src/caches/SoChildBVH.cpp new file mode 100644 index 00000000000..1f35f2a7524 --- /dev/null +++ b/src/caches/SoChildBVH.cpp @@ -0,0 +1,295 @@ +/**************************************************************************\ + * Copyright (c) Kongsberg Oil & Gas Technologies AS + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +\**************************************************************************/ + +#include "SoChildBVH.h" +#include "CoinTracyConfig.h" + +#include +#include + +#include +#include + +// SAH constants +static const int NUM_BINS = 16; +static const int MAX_LEAF_CHILDREN = 4; +static const float TRAVERSAL_COST = 1.0f; +static const float INTERSECT_COST = 1.0f; + +SoChildBVH::SoChildBVH() = default; +SoChildBVH::~SoChildBVH() = default; + +float +SoChildBVH::surfaceArea(const SbBox3f & box) +{ + if (box.isEmpty()) return 0.0f; + SbVec3f s = box.getMax() - box.getMin(); + return 2.0f * (s[0] * s[1] + s[0] * s[2] + s[1] * s[2]); +} + +void +SoChildBVH::build(const std::vector> & entries, int totalChildren) +{ + CoinZoneScopedN("SoChildBVH::build"); + + builtChildCount = totalChildren; + + const int n = static_cast(entries.size()); + if (n == 0) { + nodes.clear(); + return; + } + + // Initialize entry index permutation + entryIndices.resize(n); + for (int i = 0; i < n; i++) { + entryIndices[i] = i; + } + + // Precompute centroids (center of each child's bbox) + std::vector centroids(n); + for (int i = 0; i < n; i++) { + if (!entries[i].second.isEmpty()) { + centroids[i] = (entries[i].second.getMin() + entries[i].second.getMax()) * 0.5f; + } + } + + nodes.clear(); + nodes.reserve(n * 2 + 1); + + buildRecursive(0, n, entries, centroids); +} + +int +SoChildBVH::buildRecursive(int begin, int end, + const std::vector> & entries, + std::vector & centroids) +{ + const int count = end - begin; + const int nodeIdx = static_cast(nodes.size()); + nodes.push_back(SoChildBVHNode()); + + // Compute bounding box of all entries in this range + SbBox3f nodeBBox; + for (int i = begin; i < end; i++) { + const SbBox3f & box = entries[entryIndices[i]].second; + if (!box.isEmpty()) { + nodeBBox.extendBy(box.getMin()); + nodeBBox.extendBy(box.getMax()); + } + } + nodes[nodeIdx].bbox = nodeBBox; + + // Leaf node + if (count <= MAX_LEAF_CHILDREN) { + // For leaf, store the first child index. We'll mark it as leaf with childIndex >= 0. + // But a leaf can have multiple entries. We handle this by making single-entry leaves + // and letting the recursive build create them naturally. + if (count == 1) { + nodes[nodeIdx].childIndex = static_cast(entries[entryIndices[begin]].first); + nodes[nodeIdx].rightChild = -1; + nodes[nodeIdx].splitAxis = 0; + return nodeIdx; + } + // For multi-entry leaves with few children, just split in half + // (no SAH needed, the cost is negligible) + } + + // SAH binning to find best split + SbBox3f centroidBBox; + for (int i = begin; i < end; i++) { + centroidBBox.extendBy(centroids[entryIndices[i]]); + } + + SbVec3f extent = centroidBBox.getMax() - centroidBBox.getMin(); + + // Find longest axis + int bestAxis = 0; + if (extent[1] > extent[bestAxis]) bestAxis = 1; + if (extent[2] > extent[bestAxis]) bestAxis = 2; + + // Degenerate case: all centroids at same point + if (extent[bestAxis] < FLT_EPSILON) { + // Split in half + int mid = begin + count / 2; + nodes[nodeIdx].childIndex = -1; + nodes[nodeIdx].splitAxis = static_cast(bestAxis); + + // Left child (depth-first, immediately follows this node) + buildRecursive(begin, mid, entries, centroids); + // Right child + int rightIdx = buildRecursive(mid, end, entries, centroids); + nodes[nodeIdx].rightChild = rightIdx; + return nodeIdx; + } + + // Bin the centroids + float axisMin = centroidBBox.getMin()[bestAxis]; + float axisMax = centroidBBox.getMax()[bestAxis]; + float scale = NUM_BINS / (axisMax - axisMin); + + struct Bin { SbBox3f bbox; int count = 0; }; + Bin bins[NUM_BINS]; + + for (int i = begin; i < end; i++) { + int idx = entryIndices[i]; + int binIdx = static_cast((centroids[idx][bestAxis] - axisMin) * scale); + if (binIdx >= NUM_BINS) binIdx = NUM_BINS - 1; + if (binIdx < 0) binIdx = 0; + const SbBox3f & box = entries[idx].second; + if (!box.isEmpty()) { + bins[binIdx].bbox.extendBy(box.getMin()); + bins[binIdx].bbox.extendBy(box.getMax()); + } + bins[binIdx].count++; + } + + // Evaluate SAH cost for each split position + float nodeSA = surfaceArea(nodeBBox); + if (nodeSA < FLT_EPSILON) nodeSA = FLT_EPSILON; + + float bestCost = FLT_MAX; + int bestSplit = -1; + + // Prefix scan from left + SbBox3f leftBox; + int leftCount = 0; + float leftAreas[NUM_BINS - 1]; + int leftCounts[NUM_BINS - 1]; + + for (int i = 0; i < NUM_BINS - 1; i++) { + if (!bins[i].bbox.isEmpty()) { + leftBox.extendBy(bins[i].bbox.getMin()); + leftBox.extendBy(bins[i].bbox.getMax()); + } + leftCount += bins[i].count; + leftAreas[i] = surfaceArea(leftBox); + leftCounts[i] = leftCount; + } + + // Suffix scan from right + SbBox3f rightBox; + int rightCount = 0; + + for (int i = NUM_BINS - 1; i >= 1; i--) { + if (!bins[i].bbox.isEmpty()) { + rightBox.extendBy(bins[i].bbox.getMin()); + rightBox.extendBy(bins[i].bbox.getMax()); + } + rightCount += bins[i].count; + float cost = TRAVERSAL_COST + + INTERSECT_COST * (leftCounts[i - 1] * leftAreas[i - 1] + + rightCount * surfaceArea(rightBox)) / nodeSA; + if (cost < bestCost && leftCounts[i - 1] > 0 && rightCount > 0) { + bestCost = cost; + bestSplit = i; + } + } + + // If no good split found, split in half + if (bestSplit < 0) { + int mid = begin + count / 2; + nodes[nodeIdx].childIndex = -1; + nodes[nodeIdx].splitAxis = static_cast(bestAxis); + buildRecursive(begin, mid, entries, centroids); + int rightIdx = buildRecursive(mid, end, entries, centroids); + nodes[nodeIdx].rightChild = rightIdx; + return nodeIdx; + } + + // Partition entries based on best split + float splitPos = axisMin + bestSplit / scale; + int mid = begin; + for (int i = begin; i < end; i++) { + if (centroids[entryIndices[i]][bestAxis] < splitPos) { + std::swap(entryIndices[i], entryIndices[mid]); + std::swap(centroids[entryIndices[i]], centroids[entryIndices[mid]]); + // Actually we swapped indices, centroids are indexed by entry index not position + mid++; + } + } + + // Ensure non-empty partitions + if (mid == begin || mid == end) { + mid = begin + count / 2; + } + + nodes[nodeIdx].childIndex = -1; + nodes[nodeIdx].splitAxis = static_cast(bestAxis); + + // Left child (depth-first) + buildRecursive(begin, mid, entries, centroids); + // Right child + int rightIdx = buildRecursive(mid, end, entries, centroids); + nodes[nodeIdx].rightChild = rightIdx; + + return nodeIdx; +} + +void +SoChildBVH::query(SoRayPickAction * action, + std::vector & hitChildren) const +{ + CoinZoneScopedN("SoChildBVH::query"); + + if (nodes.empty()) return; + + // Stack-based BVH traversal + int stack[64]; + int stackPtr = 0; + stack[stackPtr++] = 0; // root node + + while (stackPtr > 0) { + int nodeIdx = stack[--stackPtr]; + const SoChildBVHNode & node = nodes[nodeIdx]; + + // Test ray against node bbox + if (node.bbox.isEmpty() || !action->intersect(node.bbox, TRUE)) { + continue; + } + + if (node.childIndex >= 0) { + // Leaf node — mark child as hit + hitChildren[node.childIndex] = true; + } else { + // Internal node — push children + // Right child first (so left is processed first from stack) + if (node.rightChild > 0 && stackPtr < 64) { + stack[stackPtr++] = node.rightChild; + } + // Left child is always nodeIdx + 1 (depth-first layout) + if (nodeIdx + 1 < static_cast(nodes.size()) && stackPtr < 64) { + stack[stackPtr++] = nodeIdx + 1; + } + } + } +} diff --git a/src/caches/SoChildBVH.h b/src/caches/SoChildBVH.h new file mode 100644 index 00000000000..ed5bd96a836 --- /dev/null +++ b/src/caches/SoChildBVH.h @@ -0,0 +1,97 @@ +/**************************************************************************\ + * Copyright (c) Kongsberg Oil & Gas Technologies AS + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +\**************************************************************************/ + +#ifndef COIN_SOCHILDBVH_H +#define COIN_SOCHILDBVH_H + +#include +#include +#include + +class SoRayPickAction; + +/*! + \class SoChildBVH SoChildBVH.h + \brief BVH over a separator's direct children's bounding boxes. + + Used by SoSeparator::rayPick() to accelerate child culling. Instead + of testing each child's bbox linearly, the ray is tested against a + spatial tree, visiting only children whose bboxes actually intersect. + + Build from (childIndex, bbox) pairs. Query returns a bitset of which + children the ray intersects. State-affecting children (transforms, + materials) are not in the BVH and are always traversed. +*/ + +struct SoChildBVHNode { + SbBox3f bbox; + int32_t rightChild; // internal: index of right child node; leaf: -1 + int16_t childIndex; // leaf: index into SoChildList; internal: -1 + int16_t splitAxis; // 0=X, 1=Y, 2=Z +}; + +class SoChildBVH { +public: + SoChildBVH(); + ~SoChildBVH(); + + /// Build from an array of (childIndex, bbox) pairs. + /// Only children with non-empty bboxes should be included. + /// totalChildren is the total child count for validity checking. + void build(const std::vector> & entries, int totalChildren); + + /// Query: fill hitChildren[i] = true for each child whose bbox + /// intersects the ray. The action must have its object-space ray set. + void query(SoRayPickAction * action, + std::vector & hitChildren) const; + + bool isBuilt() const { return !nodes.empty(); } + + /// Check if the BVH is still valid for the given child count + bool isValid(int currentChildCount) const { + return !nodes.empty() && currentChildCount == builtChildCount; + } + + int numNodes() const { return static_cast(nodes.size()); } + +private: + std::vector nodes; + std::vector entryIndices; // permutation for SAH partitioning + int builtChildCount = 0; // child count at build time for validity + + int buildRecursive(int begin, int end, + const std::vector> & entries, + std::vector & centroids); + static float surfaceArea(const SbBox3f & box); +}; + +#endif // COIN_SOCHILDBVH_H diff --git a/src/caches/SoSceneBVH.cpp b/src/caches/SoSceneBVH.cpp new file mode 100644 index 00000000000..d240dd90804 --- /dev/null +++ b/src/caches/SoSceneBVH.cpp @@ -0,0 +1,326 @@ +/**************************************************************************\ + * Copyright (c) Kongsberg Oil & Gas Technologies AS + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +\**************************************************************************/ + +#include "SoSceneBVH.h" +#include "CoinTracyConfig.h" + +#include +#include + +#include +#include +#include +#include + +// SAH constants +static const int NUM_BINS = 16; +static const int MAX_LEAF_SHAPES = 4; + +// ----------------------------------------------------------------------- +// Global scene BVH cache: one per scene root node +// ----------------------------------------------------------------------- + +static std::mutex g_sceneBVHMutex; +static std::map g_sceneBVHMap; + +SoSceneBVH * +SoSceneBVH::getForRoot(SoNode * root) +{ + std::lock_guard lock(g_sceneBVHMutex); + auto it = g_sceneBVHMap.find(root); + if (it != g_sceneBVHMap.end()) { + return it->second; + } + auto * bvh = new SoSceneBVH(); + g_sceneBVHMap[root] = bvh; + return bvh; +} + +void +SoSceneBVH::invalidateForRoot(SoNode * root) +{ + std::lock_guard lock(g_sceneBVHMutex); + auto it = g_sceneBVHMap.find(root); + if (it != g_sceneBVHMap.end()) { + delete it->second; + g_sceneBVHMap.erase(it); + } +} + +// ----------------------------------------------------------------------- +// Thread-local state for current pick operation +// ----------------------------------------------------------------------- + +static std::set * g_activeCandidates = NULL; +static SoSceneBVH * g_collectingBVH = NULL; + +void SoSceneBVH::setActiveCandidates(std::set * candidates) { + g_activeCandidates = candidates; +} + +std::set * SoSceneBVH::getActiveCandidates() { + return g_activeCandidates; +} + +void SoSceneBVH::setCollecting(SoSceneBVH * bvh) { + g_collectingBVH = bvh; +} + +SoSceneBVH * SoSceneBVH::getCollecting() { + return g_collectingBVH; +} + +// ----------------------------------------------------------------------- +// SoSceneBVH implementation +// ----------------------------------------------------------------------- + +SoSceneBVH::SoSceneBVH() = default; +SoSceneBVH::~SoSceneBVH() = default; + +void +SoSceneBVH::addShape(SoShape * shape, const SbBox3f & worldBBox) +{ + if (!worldBBox.isEmpty()) { + entries.push_back({shape, worldBBox}); + } +} + +float +SoSceneBVH::surfaceArea(const SbBox3f & box) +{ + if (box.isEmpty()) return 0.0f; + SbVec3f s = box.getMax() - box.getMin(); + return 2.0f * (s[0] * s[1] + s[0] * s[2] + s[1] * s[2]); +} + +void +SoSceneBVH::build() +{ + CoinZoneScopedN("SoSceneBVH::build"); + + const int n = static_cast(entries.size()); + if (n == 0) { + built = false; + return; + } + + entryIndices.resize(n); + for (int i = 0; i < n; i++) { + entryIndices[i] = i; + } + + std::vector centroids(n); + for (int i = 0; i < n; i++) { + const SbBox3f & box = entries[i].worldBBox; + centroids[i] = (box.getMin() + box.getMax()) * 0.5f; + } + + nodes.clear(); + nodes.reserve(n * 2 + 1); + + buildRecursive(0, n, centroids); + built = true; +} + +int +SoSceneBVH::buildRecursive(int begin, int end, std::vector & centroids) +{ + const int count = end - begin; + const int nodeIdx = static_cast(nodes.size()); + nodes.push_back(SoSceneBVHNode()); + + // Compute bounding box of all entries in this range + SbBox3f nodeBBox; + for (int i = begin; i < end; i++) { + const SbBox3f & box = entries[entryIndices[i]].worldBBox; + nodeBBox.extendBy(box.getMin()); + nodeBBox.extendBy(box.getMax()); + } + nodes[nodeIdx].bbox = nodeBBox; + + // Leaf + if (count <= MAX_LEAF_SHAPES) { + if (count == 1) { + nodes[nodeIdx].shapeIndex = entryIndices[begin]; + nodes[nodeIdx].rightChild = -1; + return nodeIdx; + } + } + + // SAH binning + SbBox3f centroidBBox; + for (int i = begin; i < end; i++) { + centroidBBox.extendBy(centroids[entryIndices[i]]); + } + + SbVec3f extent = centroidBBox.getMax() - centroidBBox.getMin(); + int bestAxis = 0; + if (extent[1] > extent[bestAxis]) bestAxis = 1; + if (extent[2] > extent[bestAxis]) bestAxis = 2; + + // Degenerate + if (extent[bestAxis] < FLT_EPSILON) { + int mid = begin + count / 2; + nodes[nodeIdx].shapeIndex = -1; + buildRecursive(begin, mid, centroids); + int rightIdx = buildRecursive(mid, end, centroids); + nodes[nodeIdx].rightChild = rightIdx; + return nodeIdx; + } + + float axisMin = centroidBBox.getMin()[bestAxis]; + float axisMax = centroidBBox.getMax()[bestAxis]; + float scale = NUM_BINS / (axisMax - axisMin); + + struct Bin { SbBox3f bbox; int count = 0; }; + Bin bins[NUM_BINS]; + + for (int i = begin; i < end; i++) { + int idx = entryIndices[i]; + int binIdx = static_cast((centroids[idx][bestAxis] - axisMin) * scale); + if (binIdx >= NUM_BINS) binIdx = NUM_BINS - 1; + if (binIdx < 0) binIdx = 0; + const SbBox3f & box = entries[idx].worldBBox; + bins[binIdx].bbox.extendBy(box.getMin()); + bins[binIdx].bbox.extendBy(box.getMax()); + bins[binIdx].count++; + } + + float nodeSA = surfaceArea(nodeBBox); + if (nodeSA < FLT_EPSILON) nodeSA = FLT_EPSILON; + + float bestCost = FLT_MAX; + int bestSplit = -1; + + SbBox3f leftBox; + int leftCount = 0; + float leftAreas[NUM_BINS - 1]; + int leftCounts[NUM_BINS - 1]; + + for (int i = 0; i < NUM_BINS - 1; i++) { + if (!bins[i].bbox.isEmpty()) { + leftBox.extendBy(bins[i].bbox.getMin()); + leftBox.extendBy(bins[i].bbox.getMax()); + } + leftCount += bins[i].count; + leftAreas[i] = surfaceArea(leftBox); + leftCounts[i] = leftCount; + } + + SbBox3f rightBox; + int rightCount = 0; + for (int i = NUM_BINS - 1; i >= 1; i--) { + if (!bins[i].bbox.isEmpty()) { + rightBox.extendBy(bins[i].bbox.getMin()); + rightBox.extendBy(bins[i].bbox.getMax()); + } + rightCount += bins[i].count; + float cost = 1.0f + (leftCounts[i-1] * leftAreas[i-1] + + rightCount * surfaceArea(rightBox)) / nodeSA; + if (cost < bestCost && leftCounts[i-1] > 0 && rightCount > 0) { + bestCost = cost; + bestSplit = i; + } + } + + if (bestSplit < 0) { + int mid = begin + count / 2; + nodes[nodeIdx].shapeIndex = -1; + buildRecursive(begin, mid, centroids); + int rightIdx = buildRecursive(mid, end, centroids); + nodes[nodeIdx].rightChild = rightIdx; + return nodeIdx; + } + + float splitPos = axisMin + bestSplit / scale; + int mid = begin; + for (int i = begin; i < end; i++) { + if (centroids[entryIndices[i]][bestAxis] < splitPos) { + std::swap(entryIndices[i], entryIndices[mid]); + mid++; + } + } + + if (mid == begin || mid == end) { + mid = begin + count / 2; + } + + nodes[nodeIdx].shapeIndex = -1; + buildRecursive(begin, mid, centroids); + int rightIdx = buildRecursive(mid, end, centroids); + nodes[nodeIdx].rightChild = rightIdx; + + return nodeIdx; +} + +void +SoSceneBVH::query(SoRayPickAction * action, std::set & candidates) const +{ + CoinZoneScopedN("SoSceneBVH::query"); + + if (nodes.empty()) return; + + // The ray must be in world space for world-space bbox tests. + // SoRayPickAction::intersect(SbBox3f) tests in whatever space was last + // set via setObjectSpace(). We need to test in world space, so we + // use the world-space ray directly. + + // Stack-based traversal + int stack[64]; + int stackPtr = 0; + stack[stackPtr++] = 0; + + while (stackPtr > 0) { + int nodeIdx = stack[--stackPtr]; + const SoSceneBVHNode & node = nodes[nodeIdx]; + + if (node.bbox.isEmpty()) continue; + + // Test ray against world-space AABB using the action's world-space ray + if (!action->intersect(node.bbox, TRUE)) { + continue; + } + + if (node.shapeIndex >= 0) { + // Leaf — add shape as candidate + candidates.insert(entries[node.shapeIndex].shape); + } else { + // Internal — push children + if (node.rightChild > 0 && stackPtr < 64) { + stack[stackPtr++] = node.rightChild; + } + if (nodeIdx + 1 < static_cast(nodes.size()) && stackPtr < 64) { + stack[stackPtr++] = nodeIdx + 1; + } + } + } +} diff --git a/src/caches/SoSceneBVH.h b/src/caches/SoSceneBVH.h new file mode 100644 index 00000000000..6fbc5fbec42 --- /dev/null +++ b/src/caches/SoSceneBVH.h @@ -0,0 +1,111 @@ +/**************************************************************************\ + * Copyright (c) Kongsberg Oil & Gas Technologies AS + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +\**************************************************************************/ + +#ifndef COIN_SOSCENEBVH_H +#define COIN_SOSCENEBVH_H + +#include +#include +#include +#include + +class SoNode; +class SoShape; +class SoRayPickAction; + +/*! + \class SoSceneBVH SoSceneBVH.h + \brief Flat scene-level BVH over all shapes' world-space bounding boxes. + + Built from a collection of (shapeNode, worldSpaceBBox) pairs gathered + during the first ray pick traversal. Subsequent picks query the BVH + to find which shapes the ray might intersect, producing a candidate + set. SoShape::rayPick() checks membership in this set to skip shapes + the ray cannot hit. + + Stored as a static cache associated with a scene root node. + Invalidated when the scene graph changes (via node notification). +*/ + +struct SoSceneBVHNode { + SbBox3f bbox; // world-space AABB + int32_t rightChild; // internal: right child index; leaf: -1 + int32_t shapeIndex; // leaf: index into shapes array; internal: -1 +}; + +struct SoSceneBVHEntry { + SoShape * shape; + SbBox3f worldBBox; +}; + +class SoSceneBVH { +public: + SoSceneBVH(); + ~SoSceneBVH(); + + /// Add a shape with its world-space bbox during collection phase + void addShape(SoShape * shape, const SbBox3f & worldBBox); + + /// Build the BVH from collected shapes + void build(); + + /// Query: find all shapes whose world-space bbox intersects the ray. + /// Returns a set of SoShape pointers that are candidates for picking. + void query(SoRayPickAction * action, std::set & candidates) const; + + bool isBuilt() const { return built; } + int numShapes() const { return static_cast(entries.size()); } + + /// Global scene BVH management — one BVH per scene root + static SoSceneBVH * getForRoot(SoNode * root); + static void invalidateForRoot(SoNode * root); + + /// Active candidate set for current pick (set by doPick, read by SoShape::rayPick) + static void setActiveCandidates(std::set * candidates); + static std::set * getActiveCandidates(); + + /// Collection mode: shapes register themselves during first pick + static void setCollecting(SoSceneBVH * bvh); + static SoSceneBVH * getCollecting(); + +private: + std::vector entries; + std::vector nodes; + std::vector entryIndices; + bool built = false; + bool collecting = false; + + int buildRecursive(int begin, int end, std::vector & centroids); + static float surfaceArea(const SbBox3f & box); +}; + +#endif // COIN_SOSCENEBVH_H diff --git a/src/nodes/SoSeparator.cpp b/src/nodes/SoSeparator.cpp index 2401c6c3a27..9a432569c0a 100644 --- a/src/nodes/SoSeparator.cpp +++ b/src/nodes/SoSeparator.cpp @@ -76,6 +76,8 @@ #include #include #include +#include "caches/SoChildBVH.h" +#include "CoinTracyConfig.h" #include #include #include @@ -307,9 +309,11 @@ class SoSeparatorP { soseparator_storage_construct, soseparator_storage_destruct); this->pub = NULL; + this->childbvh = NULL; } ~SoSeparatorP() { delete this->glcachestorage; + delete this->childbvh; } SoSeparator * pub; @@ -318,6 +322,8 @@ class SoSeparatorP { uint32_t bboxcache_usecount; uint32_t bboxcache_destroycount; + SoChildBVH * childbvh; + #ifdef COIN_THREADSAFE // FIXME: a mutex for every SoSeparator instance seems a bit // excessive, especially since Microsoft Windows might have rather strict @@ -853,6 +859,10 @@ ray_intersect(SoRayPickAction * action, const SbBox3f &box) return action->intersect(box, TRUE); } +// Minimum children to activate per-child BVH culling during ray pick. +// Low threshold ensures BVH is used at most separator levels. +static const int SOSEPARATOR_RAYPICK_CULL_THRESHOLD = 4; + // Doc from superclass. void SoSeparator::rayPick(SoRayPickAction * action) @@ -861,7 +871,111 @@ SoSeparator::rayPick(SoRayPickAction * action) !PRIVATE(this)->bboxcache || !PRIVATE(this)->bboxcache->isValid(action->getState()) || !action->hasWorldSpaceRay() || ray_intersect(action, PRIVATE(this)->bboxcache->getProjectedBox())) { - SoSeparator::doAction(action); + + SoChildList * children = this->getChildren(); + int n = children->getLength(); + + // For small child counts, use standard traversal (no culling overhead) + if (n <= SOSEPARATOR_RAYPICK_CULL_THRESHOLD) { + SoSeparator::doAction(action); + return; + } + + // Build child BVH lazily. Rebuild only when child count changes + // (not on every notify, which fires for selection state changes too). + if (!PRIVATE(this)->childbvh || !PRIVATE(this)->childbvh->isValid(n)) { + delete PRIVATE(this)->childbvh; + PRIVATE(this)->childbvh = NULL; + CoinZoneScopedN("SoSeparator::rayPick/buildChildBVH"); + std::vector> entries; + entries.reserve(n); + + SoGetBoundingBoxAction bboxAction(action->getViewportRegion()); + + for (int i = 0; i < n; i++) { + SoNode * child = (*children)[i]; + + // Skip state-affecting nodes — they have no bbox and are never culled + if (child->affectsState()) { + continue; + } + + SbBox3f childBox; + + // For SoSeparator children, use their cached bbox (cheap) + if (child->isOfType(SoSeparator::getClassTypeId())) { + SoSeparator * sep = static_cast(child); + SoBoundingBoxCache * childCache = PRIVATE(sep)->bboxcache; + if (childCache && childCache->isValid(action->getState())) { + childBox = childCache->getProjectedBox(); + } + } + + // For other non-state children, compute bbox + if (childBox.isEmpty()) { + bboxAction.apply(child); + childBox = bboxAction.getBoundingBox(); + } + + if (!childBox.isEmpty()) { + entries.push_back({i, childBox}); + } + } + + if (static_cast(entries.size()) > SOSEPARATOR_RAYPICK_CULL_THRESHOLD) { + PRIVATE(this)->lock(); + PRIVATE(this)->childbvh = new SoChildBVH(); + PRIVATE(this)->childbvh->build(entries, n); + PRIVATE(this)->unlock(); + } + } + + // Traverse with BVH-accelerated child culling + action->getState()->push(); + action->pushCurPath(); + + if (PRIVATE(this)->childbvh && PRIVATE(this)->childbvh->isValid(n)) { + // BVH path: query to find which children the ray hits + CoinZoneScopedN("SoSeparator::rayPick/queryChildBVH"); + std::vector hitChildren(n, false); + PRIVATE(this)->childbvh->query(action, hitChildren); + + for (int i = 0; i < n && !action->hasTerminated(); i++) { + SoNode * child = (*children)[i]; + + // Always traverse state-affecting nodes (transforms, materials, etc.) + // Skip non-state children that the BVH says don't intersect + if (!hitChildren[i] && !child->affectsState()) { + continue; + } + + action->popPushCurPath(i, child); + action->traverse(child); + } + } + else { + // Fallback: linear traversal with SoSeparator-only culling + for (int i = 0; i < n && !action->hasTerminated(); i++) { + SoNode * child = (*children)[i]; + + if (child->isOfType(SoSeparator::getClassTypeId())) { + SoSeparator * sep = static_cast(child); + SoBoundingBoxCache * childCache = PRIVATE(sep)->bboxcache; + if (childCache && childCache->isValid(action->getState())) { + SbBox3f childBox = childCache->getProjectedBox(); + if (!childBox.isEmpty() && !action->intersect(childBox, TRUE)) { + continue; + } + } + } + + action->popPushCurPath(i, child); + action->traverse(child); + } + } + + action->popCurPath(); + action->getState()->pop(); } } @@ -954,6 +1068,9 @@ SoSeparator::notify(SoNotList * nl) // are valid while reading them PRIVATE(this)->lock(); if (PRIVATE(this)->bboxcache) PRIVATE(this)->bboxcache->invalidate(); + // Note: childbvh is NOT invalidated here. Selection/preselection state + // changes trigger notify() frequently but don't affect child bboxes. + // The childbvh is invalidated lazily by checking child count at query time. PRIVATE(this)->invalidateGLCaches(); PRIVATE(this)->hassoundchild = SoSeparatorP::MAYBE; PRIVATE(this)->unlock(); diff --git a/src/shapenodes/SoShape.cpp b/src/shapenodes/SoShape.cpp index 963348437f7..448ea242e67 100644 --- a/src/shapenodes/SoShape.cpp +++ b/src/shapenodes/SoShape.cpp @@ -47,6 +47,7 @@ #include #include +#include #ifdef HAVE_CONFIG_H #include @@ -120,6 +121,9 @@ #include "threads/threadsutilp.h" #include "tidbitsp.h" #include "rendering/SoVBO.h" +#include "caches/SoBVHCache.h" +#include "caches/SoSceneBVH.h" +#include "CoinTracyConfig.h" #include "coindefs.h" // COIN_OBSOLETED() // SoShape.cpp grew too big, so I had to move some code into new @@ -175,6 +179,8 @@ class SoShapeP { SoShapeP() { this->bboxcache = NULL; this->pvcache = NULL; + this->bvhcache = NULL; + this->bvhCollecting = FALSE; this->bumprender = NULL; this->rendercnt = 0; this->flags = 0; @@ -182,6 +188,7 @@ class SoShapeP { ~SoShapeP() { if (this->bboxcache) { this->bboxcache->unref(); } if (this->pvcache) { this->pvcache->unref(); } + if (this->bvhcache) { this->bvhcache->unref(); } delete this->bumprender; } enum { @@ -198,6 +205,10 @@ class SoShapeP { static double bboxcachetimelimit; SoBoundingBoxCache * bboxcache; SoPrimitiveVertexCache * pvcache; + SoBVHCache * bvhcache; + SbBool bvhCollecting; + std::vector bvhCollectVertices; + std::vector bvhCollectNormals; soshape_bumprender * bumprender; uint32_t flags : FLAG_BITS; // stores the number of frames rendered with no node changes @@ -445,6 +456,11 @@ soshape_ray_intersect(SoRayPickAction * action, const SbBox3f & box) return action->intersect(box, TRUE); } +// Minimum triangle count to justify building a BVH. +// Low threshold ensures most shapes get BVH acceleration after first pick. +// The BVH build cost for small shapes is negligible. +static const int SOSHAPE_BVH_MIN_TRIANGLES = 8; + /*! Calculates picked point based on primitives generated by subclasses. @@ -452,13 +468,88 @@ soshape_ray_intersect(SoRayPickAction * action, const SbBox3f & box) void SoShape::rayPick(SoRayPickAction * action) { + CoinZoneScopedN("SoShape::rayPick"); + + // Scene-level BVH acceleration + { + SoSceneBVH * sceneBVH = SoSceneBVH::getCollecting(); + if (sceneBVH) { + if (!sceneBVH->isBuilt()) { + // First pick: register this shape with its world-space bbox + if (PRIVATE(this)->bboxcache && PRIVATE(this)->bboxcache->isValid(action->getState())) { + SbBox3f localBox = PRIVATE(this)->bboxcache->getProjectedBox(); + if (!localBox.isEmpty()) { + SbMatrix modelMatrix = SoModelMatrixElement::get(action->getState()); + SbVec3f bmin = localBox.getMin(), bmax = localBox.getMax(); + SbBox3f worldBox; + for (int c = 0; c < 8; c++) { + SbVec3f corner( + (c & 1) ? bmax[0] : bmin[0], + (c & 2) ? bmax[1] : bmin[1], + (c & 4) ? bmax[2] : bmin[2] + ); + SbVec3f worldCorner; + modelMatrix.multVecMatrix(corner, worldCorner); + worldBox.extendBy(worldCorner); + } + sceneBVH->addShape(this, worldBox); + } + } + } + else { + // BVH is built: lazily query on first shape visit in this pick + std::set * candidates = SoSceneBVH::getActiveCandidates(); + if (!candidates) { + // Trigger BVH query — world ray is available by now + static std::set s_candidateSet; + s_candidateSet.clear(); + sceneBVH->query(action, s_candidateSet); + SoSceneBVH::setActiveCandidates(&s_candidateSet); + candidates = &s_candidateSet; + } + if (candidates->find(this) == candidates->end()) { + return; // Ray misses this shape — skip + } + } + } + } + if (this->shouldRayPick(action)) { this->computeObjectSpaceRay(action); if (!PRIVATE(this)->bboxcache || !PRIVATE(this)->bboxcache->isValid(action->getState()) || soshape_ray_intersect(action, PRIVATE(this)->bboxcache->getProjectedBox())) { - this->generatePrimitives(action); + + if (PRIVATE(this)->bvhcache && PRIVATE(this)->bvhcache->isBuilt()) { + // BVH exists from a previous pick — use accelerated path + PRIVATE(this)->bvhcache->rayPick(action, this); + } else { + // First pick: brute-force, but collect triangles for BVH building + PRIVATE(this)->bvhCollecting = TRUE; + PRIVATE(this)->bvhCollectVertices.clear(); + PRIVATE(this)->bvhCollectNormals.clear(); + { + CoinZoneScopedN("SoShape::rayPick/generatePrimitives"); + this->generatePrimitives(action); + } + PRIVATE(this)->bvhCollecting = FALSE; + + // Build BVH from collected triangles if enough were gathered + int numTris = static_cast(PRIVATE(this)->bvhCollectVertices.size()) / 3; + if (numTris >= SOSHAPE_BVH_MIN_TRIANGLES) { + if (PRIVATE(this)->bvhcache) { PRIVATE(this)->bvhcache->unref(); } + PRIVATE(this)->bvhcache = new SoBVHCache(action->getState()); + PRIVATE(this)->bvhcache->ref(); + PRIVATE(this)->bvhcache->buildFlat( + PRIVATE(this)->bvhCollectVertices.data(), + PRIVATE(this)->bvhCollectNormals.data(), + numTris); + } + // Free collection memory + std::vector().swap(PRIVATE(this)->bvhCollectVertices); + std::vector().swap(PRIVATE(this)->bvhCollectNormals); + } } } } @@ -1044,6 +1135,15 @@ SoShape::invokeTriangleCallbacks(SoAction * const action, } } } + // Collect triangle data for BVH building (first pick on this shape) + if (PRIVATE(this)->bvhCollecting) { + PRIVATE(this)->bvhCollectVertices.push_back(v1->getPoint()); + PRIVATE(this)->bvhCollectVertices.push_back(v2->getPoint()); + PRIVATE(this)->bvhCollectVertices.push_back(v3->getPoint()); + PRIVATE(this)->bvhCollectNormals.push_back(v1->getNormal()); + PRIVATE(this)->bvhCollectNormals.push_back(v2->getNormal()); + PRIVATE(this)->bvhCollectNormals.push_back(v3->getNormal()); + } } else if (action->getTypeId().isDerivedFrom(SoCallbackAction::getClassTypeId())) { SoCallbackAction * ca = (SoCallbackAction *) action; @@ -1433,6 +1533,10 @@ SoShape::notify(SoNotList * nl) if (PRIVATE(this)->pvcache) { PRIVATE(this)->pvcache->invalidate(); } + if (PRIVATE(this)->bvhcache) { + PRIVATE(this)->bvhcache->unref(); + PRIVATE(this)->bvhcache = NULL; + } PRIVATE(this)->flags &= ~SoShapeP::SHOULD_BBOX_CACHE; PRIVATE(this)->rendercnt = 0; PRIVATE(this)->unlock();