Skip to content

Add BVH acceleration for ray picking#614

Draft
oursland wants to merge 4 commits into
coin3d:masterfrom
oursland:bvh-pick
Draft

Add BVH acceleration for ray picking#614
oursland wants to merge 4 commits into
coin3d:masterfrom
oursland:bvh-pick

Conversation

@oursland
Copy link
Copy Markdown
Contributor

Per-shape BVH (Bounding Volume Hierarchy) cache that accelerates SoShape::rayPick() from O(N) to O(log N) triangle intersection tests.

On the first pick for each shape, triangle data is collected during the brute-force generatePrimitives() pass via invokeTriangleCallbacks(). A BVH is then built using SAH-binned construction with a flat depth-first node layout. All subsequent picks use the cached BVH. Shapes with <64 triangles remain brute-force. The BVH is invalidated in SoShape::notify() when geometry changes.

This approach works for all SoShape subclasses — including FreeCAD's custom SoBrepFaceSet, SoBrepEdgeSet, etc. — because it collects from the generatePrimitives callback path rather than depending on SoPrimitiveVertexCache (which is only populated for shapes using Coin's standard GL render path).

Tracy profiling results (FreeCAD, macOS ARM, complex CAD model):

Before BVH:
Typical pick: 23-35 ms, worst case 417 ms
SoShape::rayPick: 231,816 calls/session, all brute-force

After BVH:
Typical pick: 10-21 ms, worst case 48 ms (excl. first-pick build)
BVH rayPick: 87 calls, 15.8 us mean, 1.4 ms total session
generatePrimitives: 5,190 calls (first-pick only, then cached)

Per-shape BVH (Bounding Volume Hierarchy) cache that accelerates
SoShape::rayPick() from O(N) to O(log N) triangle intersection tests.

On the first pick for each shape, triangle data is collected during the
brute-force generatePrimitives() pass via invokeTriangleCallbacks().
A BVH is then built using SAH-binned construction with a flat
depth-first node layout. All subsequent picks use the cached BVH.
Shapes with <64 triangles remain brute-force. The BVH is invalidated
in SoShape::notify() when geometry changes.

This approach works for all SoShape subclasses — including FreeCAD's
custom SoBrepFaceSet, SoBrepEdgeSet, etc. — because it collects from
the generatePrimitives callback path rather than depending on
SoPrimitiveVertexCache (which is only populated for shapes using
Coin's standard GL render path).

Tracy profiling results (FreeCAD, macOS ARM, complex CAD model):

  Before BVH:
    Typical pick: 23-35 ms, worst case 417 ms
    SoShape::rayPick: 231,816 calls/session, all brute-force

  After BVH:
    Typical pick: 10-21 ms, worst case 48 ms (excl. first-pick build)
    BVH rayPick: 87 calls, 15.8 us mean, 1.4 ms total session
    generatePrimitives: 5,190 calls (first-pick only, then cached)
When a SoSeparator has more than 8 children, rayPick now checks each
child SoSeparator's cached bounding box against the ray before
traversing it. Children whose bbox doesn't intersect the ray are
skipped entirely. This is safe because SoSeparator children are
state-isolated (push/pop), so skipping them doesn't affect siblings.

Non-separator children (SoTransform, SoMaterial, SoShape, etc.) are
always traversed to preserve state ordering.

Tracy profiling results (FreeCAD, macOS ARM, complex CAD model):

  Before child culling:
    SoShape::rayPick: 113,871 calls per session

  After child culling:
    SoShape::rayPick: 47,691 calls per session (58% reduction)
Add COIN_USE_TRACY cmake option for Tracy frame profiler support.
When enabled, adds profiling zones to SoHandleEventAction::doPick,
SoHandleEventAction::getPickedPointList, SoShape::rayPick, and
SoBVHCache::rayPick. Uses -undefined dynamic_lookup on macOS so
Coin shares the host application's TracyClient instance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@oursland oursland marked this pull request as draft March 26, 2026 04:09
@oursland
Copy link
Copy Markdown
Contributor Author

I've converted this back to draft. Experimentation has shown that this approach of small BVHs is a major improvement, but overall the whole scene must be inserted into a single global BVH to get the pick performance expected. I'm working on that change now.

Three-level BVH hierarchy for ray picking:

1. Scene-level BVH (SoSceneBVH): flat BVH over ALL shapes' world-space
   bounding boxes. Built lazily on first pick from shapes that register
   during traversal. Subsequent picks query the BVH to produce a
   candidate set — shapes not in the set skip immediately in rayPick().
   Reduces shapes visited per pick from ~5000 to ~2000.

2. Per-separator child BVH (SoChildBVH): BVH over direct children's
   bounding boxes within each SoSeparator. Uses affectsState() to
   ensure state nodes (transforms, materials) are always traversed.
   Threshold lowered to 4 children.

3. Per-shape triangle BVH (SoBVHCache): existing per-shape BVH over
   triangles, threshold lowered from 64 to 8 triangles.

Tracy profiling results (FreeCAD, macOS ARM, complex Assembly):

  Before all BVH work:
    doPick median: 28.5 ms, mean: 28.8 ms

  After scene-level BVH:
    doPick median: 11.7 ms, mean: 14.8 ms, p95: 17.5 ms
    Scene BVH build: 0.3 ms (once)
    Scene BVH query: 14 us/pick
    Shapes per pick: 2,017 (down from 5,268)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant