# How the Modly UV Unwrapping System Uses BVH Acceleration Structures for Fast Triangle Queries

> Learn how the Modly UV unwrapping system uses BVH acceleration structures to speed up ray triangle intersection tests from O(N) to O(log N) for faster UV operations.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: internals
- Published: 2026-08-15

---

**The Modly UV unwrapping system builds a Bounding-Volume-Hierarchy (BVH) for each face's UV triangles to accelerate ray-triangle intersection tests from O(N) to O(log N), dramatically speeding up island stitching, seam detection, and projection operations.**

Modly's open-source 3D pipeline implements a high-performance UV unwrapping module written in C++. At its core, the system relies on **BVH acceleration structures** to handle the geometric queries required for quality UV generation. This article examines the BVH implementation in `lightningpixel/modly`, covering construction, traversal, and integration with the unwrapper.

## BVH Architecture in Modly

The BVH system follows a classic top-down construction pattern optimized for UV-space triangle queries. Three components work together: the `BVH` class manages the hierarchy, `BVHNode` stores spatial partitions, and `Triangle` represents the leaf geometry.

### Core Data Structures

Two files define the BVH interface. In [`api/uv_unwrapper/uv_unwrapper/csrc/bvh.h`](https://github.com/lightningpixel/modly/blob/main/api/uv_unwrapper/uv_unwrapper/csrc/bvh.h), the `BVHNode` struct occupies 32 bytes with tight packing:

```cpp
// bvh.h - 32-byte aligned node for cache efficiency
struct BVHNode {
    float min[3];
    float max[3];
    union {
        struct { unsigned int left; unsigned int right; } inner;  // 8 bytes
        unsigned int triangleIndex;                               // 4 bytes
    };
    unsigned int isLeaf;
    // padding to 32 bytes total
};

```

The `BVH` class constructor in [`bvh.cpp`](https://github.com/lightningpixel/modly/blob/main/bvh.cpp) pre-allocates a flat buffer using `new BVHNode[triCount * 2 + 64]`. This contiguous layout prevents pointer chasing and enables SIMD-friendly traversal.

### Building the Hierarchy with Binned SAH

Construction uses **Surface-Area-Heuristic binning** rather than exhaustive evaluation. The `BVH::FindBestSplitPlane` method tests candidate splits along each axis:

```cpp
// bvh.cpp - SAH split evaluation
void BVH::FindBestSplitPlane(BVHNode& node, int& axis, float& splitPos, float& bestCost) {
    bestCost = 1e30f;
    for (int a = 0; a < 3; a++) {           // test x, y, z axes
        float boundsMin = node.min[a];
        float boundsMax = node.max[a];
        if (boundsMin == boundsMax) continue; // degenerate dimension
        
        // binning: 8-16 bins typically used
        const int NUM_BINS = 8;
        // ... bin triangle centroids, evaluate SAH cost per bin boundary
    }
}

```

The `BVH::Subdivide` method recursively partitions until fewer than 3 triangles remain per node or SAH predicts higher cost for splitting. Each split triggers `BVH::UpdateNodeBounds` to recompute child AABBs.

## Integrating BVH with the UV Unwrapper

The unwrapper instantiates one BVH per face in [`unwrapper.cpp`](https://github.com/lightningpixel/modly/blob/main/unwrapper.cpp). This matches Modly's assumption that UV islands map to mesh faces requiring independent acceleration structures.

### Per-Face BVH Construction

```cpp
// unwrapper.cpp - creating BVH instances for UV triangle sets
#include "bvh.h"

// triangles_per_face: array of Triangle* per face
// indices_per_face: corresponding index buffers
// triangle_counts: number of triangles per face

BVH* bvhs = new BVH[face_count];           // one BVH per face

for (int face_idx = 0; face_idx < face_count; ++face_idx) {
    bvhs[face_idx] = BVH(
        triangles_per_face[face_idx],      // Triangle* data
        actual_indices[face_idx],          // int* index remapping
        triangle_counts[face_idx]          // size_t triangle count
    );
    // constructor builds full hierarchy via Subdivide()
}

```

The `BVH` constructor signature enforces type safety:

```cpp
// bvh.h - explicit constructor prevents misuse
BVH(Triangle* tri, int* actual_idx, const size_t& num_indices);

```

### Intersection Queries for UV Operations

The `BVH::Intersect` method implements iterative depth-first traversal using an explicit stack. This avoids recursion overhead and enables predictable memory behavior:

```cpp
// bvh.cpp - BVH traversal for triangle intersection
std::vector<int> BVH::Intersect(Triangle& tri_intersect) {
    std::vector<int> hits;
    unsigned int stack[64];                // small stack, sufficient for log2(N) depth
    unsigned int stack_ptr = 0;
    stack[stack_ptr++] = rootNodeIdx;      // push root
    
    while (stack_ptr > 0) {
        unsigned int node_idx = stack[--stack_ptr];
        BVHNode& node = bvhNode[node_idx];
        
        // AABB culling: skip branch if query triangle misses bounds
        if (!AABBIntersects(node.min, node.max, tri_intersect)) continue;
        
        if (node.isLeaf) {
            if (TriangleIntersect(triangles[node.triangleIndex], tri_intersect)) {
                hits.push_back(node.triangleIndex);
            }
        } else {
            stack[stack_ptr++] = node.inner.left;
            stack[stack_ptr++] = node.inner.right;
        }
    }
    return hits;
}

```

The query reduces triangle tests from **O(N)** for brute-force to **O(log N)** for balanced hierarchies. For a 10,000-triangle UV island, this typically means ~14 node tests versus 10,000 triangle tests.

## Performance Characteristics and Design Decisions

### Memory Layout Optimizations

Modly's BVH makes several cache-conscious choices:

- **32-byte nodes**: Exactly one cache line on x86_64, eliminating false sharing
- **Flat array storage**: `bvhNode` pointer to contiguous memory, not tree of pointers
- **Morton-order consideration**: Implicit spatial locality though not full Z-curve

### Parallel Usage Patterns

The BVH structure is **read-only after construction**, enabling safe concurrent queries. The unwrapper exploits this during:

1. **Island stitching**: Multiple threads query separate BVHs simultaneously
2. **Seam detection**: Parallel edge tests against face BVHs
3. **Texture baking**: The same [`bvh.h`](https://github.com/lightningpixel/modly/blob/main/bvh.h) implementation accelerates [`baker.cpp`](https://github.com/lightningpixel/modly/blob/main/baker.cpp) point-triangle lookups

### Trade-offs and Limitations

| Aspect | Implementation Choice | Rationale |
|--------|----------------------|-----------|
| **Rebuild cost** | Full rebuild per unwrap | Expected mesh changes; no incremental update |
| **Split heuristic** | Binned SAH (8 bins) | Balance between quality and build speed |
| **Traversal stack** | Fixed 64-element array | Prevents allocation; handles 2^64 triangles theoretically |
| **Leaf threshold** | 2-3 triangles | Tuned for UV triangle size distribution |

## Code Paths and File References

All BVH functionality resides in the UV unwrapper's C++ source:

- [`api/uv_unwrapper/uv_unwrapper/csrc/bvh.h`](https://github.com/lightningpixel/modly/blob/main/api/uv_unwrapper/uv_unwrapper/csrc/bvh.h) — `BVH` class declaration, `BVHNode` struct, `Triangle` forward declaration
- [`api/uv_unwrapper/uv_unwrapper/csrc/bvh.cpp`](https://github.com/lightningpixel/modly/blob/main/api/uv_unwrapper/uv_unwrapper/csrc/bvh.cpp) — `BVH::Subdivide`, `BVH::FindBestSplitPlane`, `BVH::UpdateNodeBounds`, `BVH::Intersect`
- [`api/uv_unwrapper/uv_unwrapper/csrc/unwrapper.cpp`](https://github.com/lightningpixel/modly/blob/main/api/uv_unwrapper/uv_unwrapper/csrc/unwrapper.cpp) — BVH instantiation per face, integration with UV algorithm phases
- [`api/texture_baker/texture_baker/csrc/baker.cpp`](https://github.com/lightningpixel/modly/blob/main/api/texture_baker/texture_baker/csrc/baker.cpp) — Secondary usage for texture-space point queries

## Summary

- The **Modly UV unwrapping system** constructs one `BVH` object per face using `binned SAH` splitting in [`bvh.cpp`](https://github.com/lightningpixel/modly/blob/main/bvh.cpp)
- **32-byte `BVHNode` structs** stored in flat arrays optimize cache performance during traversal
- **O(log N) intersection queries** via `BVH::Intersect` replace brute-force O(N) triangle testing
- **Read-only post-construction** design permits thread-safe parallel queries across island stitching and seam detection
- **Identical BVH code** accelerates both UV unwrapping and texture baking pipelines

## Frequently Asked Questions

### How does the BVH handle degenerate UV triangles?

The `BVH::FindBestSplitPlane` method skips axes where `boundsMin == boundsMax`, preventing division by zero in binning. Degenerate triangles are still inserted into leaf nodes; intersection tests in `TriangleIntersect` include epsilon-tolerant edge checks.

### Can the BVH be reused across multiple unwrap operations?

Yes. The `BVH` destructor frees node memory, but the class supports move semantics. For static meshes, users could maintain persistent BVH instances, though [`unwrapper.cpp`](https://github.com/lightningpixel/modly/blob/main/unwrapper.cpp) currently rebuilds per unwrap to handle input changes.

### What is the typical memory overhead of the BVH?

Memory usage is approximately `64 bytes × triangle count` — the node array allocates `2N + 64` nodes at 32 bytes each, plus the original triangle array. For a 100,000-triangle mesh, expect roughly 6-7 MB total BVH overhead.

### Why does Modly use one BVH per face instead of one global BVH?

Per-face BVHs match the UV island structure where topological connectivity matters more than global proximity. This also enables **embarrassingly parallel** processing: each face's unwrapping runs independently on its own BVH without synchronization.