How the Modly UV Unwrapping System Uses BVH Acceleration Structures for Fast Triangle Queries
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, the BVHNode struct occupies 32 bytes with tight packing:
// 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 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:
// 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. This matches Modly's assumption that UV islands map to mesh faces requiring independent acceleration structures.
Per-Face BVH Construction
// 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:
// 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:
// 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:
bvhNodepointer 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:
- Island stitching: Multiple threads query separate BVHs simultaneously
- Seam detection: Parallel edge tests against face BVHs
- Texture baking: The same
bvh.himplementation acceleratesbaker.cpppoint-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—BVHclass declaration,BVHNodestruct,Triangleforward declarationapi/uv_unwrapper/uv_unwrapper/csrc/bvh.cpp—BVH::Subdivide,BVH::FindBestSplitPlane,BVH::UpdateNodeBounds,BVH::Intersectapi/uv_unwrapper/uv_unwrapper/csrc/unwrapper.cpp— BVH instantiation per face, integration with UV algorithm phasesapi/texture_baker/texture_baker/csrc/baker.cpp— Secondary usage for texture-space point queries
Summary
- The Modly UV unwrapping system constructs one
BVHobject per face usingbinned SAHsplitting inbvh.cpp - 32-byte
BVHNodestructs stored in flat arrays optimize cache performance during traversal - O(log N) intersection queries via
BVH::Intersectreplace 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →