【问题标题】:Traversal of Bounding Volume Hierachy in Shaders着色器中包围体层次的遍历
【发布时间】:2019-08-24 01:39:51
【问题描述】:

我正在使用 vulkan 计算着色器开发路径跟踪器。我实现了一个代表bounding volume hierachy 的树。 BVH 的想法是尽量减少需要执行光线相交测试的对象数量。


#1 朴素实施

我的第一个实现非常快,它将树向下遍历到 BVH 树的一个单个叶。但是,光线可能与 多个 叶子相交。然后这段代码会导致一些三角形没有被渲染(尽管它们应该)。

int box_index = -1;

for (int i = 0; i < boxes_count; i++) {
    // the first box has no parent, boxes[0].parent is set to -1
    if (boxes[i].parent == box_index) {
        if (intersect_box(boxes[i], ray)) {
            box_index = i;
        }
    }
}

if (box_index > -1) {
    uint a = boxes[box_index].ids_offset;
    uint b = a + boxes[box_index].ids_count;

    for (uint j = a; j < b; j++) {
        uint triangle_id = triangle_references[j];
        // triangle intersection code ...
    }
}

#2 多叶实现

我的第二个实现说明了多个叶子可能相交的事实。然而,这个实现比实现 #1 慢 36x(好吧,我错过了 #1 中的一些交叉测试,但仍然......)。

bool[boxes.length()] hits;
hits[0] = intersect_box(boxes[0], ray);

for (int i = 1; i < boxes_count; i++) {
    if (hits[boxes[i].parent]) {
        hits[i] = intersect_box(boxes[i], ray);
    } else {
        hits[i] = false;
    }
}

for (int i = 0; i < boxes_count; i++) {
    if (!hits[i]) {
        continue;
    }

    // only leaves have ids_offset and ids_count defined (not set to -1)
    if (boxes[i].ids_offset < 0) {
        continue;
    }

    uint a = boxes[i].ids_offset;
    uint b = a + boxes[i].ids_count;

    for (uint j = a; j < b; j++) {
        uint triangle_id = triangle_references[j];
        // triangle intersection code ...
    }
}

这种性能差异让我发疯。似乎只有一个像if(dynamically_modified_array[some_index]) 这样的语句会对性能产生巨大影响。我怀疑 SPIR-V 或 GPU 编译器不再能够发挥其优化魔力?所以这是我的问题:

  1. 这真的是一个优化问题吗?

  2. 如果是,我可以将实现#2 转换为更好地优化吗? 我能以某种方式给出优化提示吗?

  3. 是否有在着色器中实现 BVH 树查询的标准方法?

【问题讨论】:

    标签: glsl compiler-optimization vulkan raytracing bounding-box


    【解决方案1】:

    经过一番挖掘,我找到了解决方案。需要了解的重要一点是,BVH 树并不排除需要评估所有个叶子的可能性。

    下面的实现#3,使用命中和未命中链接。需要以一种在最坏的情况下以正确的顺序查询所有框的方式对框进行排序(因此单个循环就足够了)。但是,链接用于跳过不需要评估的节点。当前节点为叶子节点时,进行实际的三角形相交。

    • 点击链接~在点击的情况下跳转到哪个节点(下图绿色)
    • 未命中链接~如果未命中要跳转到哪个节点(下图红色)

    图片取自here。相关论文和源代码也在 Toshiya Hachisuka 教授的page 上。 this paper referenced in the slides 中也描述了相同的概念。


    #3 带有命中和未命中链接的 BVH 树

    我必须扩展通过链接推送到着色器的数据。还需要一些离线摆弄才能正确存储树。起初我尝试使用一个while循环(循环直到box_index_next为-1),这再次导致了疯狂的减速。无论如何,以下工作相当快:

    int box_index_next = 0;
    
    for (int box_index = 0; box_index < boxes_count; box_index++) {
        if (box_index != box_index_next) {
            continue;
        }
    
        bool hit = intersect_box(boxes[box_index], ray);
        bool leaf = boxes[box_index].ids_count > 0;
    
        if (hit) {
            box_index_next = boxes[box_index].links.x; // hit link
        } else {
            box_index_next = boxes[box_index].links.y; // miss link
        }
    
        if (hit && leaf) {
            uint a = boxes[box_index].ids_offset;
            uint b = a + boxes[box_index].ids_count;
    
            for (uint j = a; j < b; j++) {
                uint triangle_id = triangle_references[j];
                // triangle intersection code ...
            }
        }
    }
    

    此代码比快速但有缺陷的实现 #1 慢约 3 倍。这有点意料之中,现在速度取决于实际的树,而不是 gpu 优化。例如,考虑三角形沿轴对齐的退化情况:同一方向的射线可能与所有三角形相交,然后需要评估所有树叶。

    教授。 Toshiya Hachisuka 提出了针对此类情况的进一步优化in his sildes(第 36 页及以后):一个存储 BVH 树的多个版本,在空间上沿 x、-x、y、-y、z 和 -z 排序。对于遍历,需要根据射线选择正确的版本。然后,只要与叶子中的三角形相交,就可以停止遍历,因为要访问的所有剩余节点将在空间上位于该节点的后面(从射线的角度来看)。


    一旦构建了 BVH 树,查找链接就非常简单了(下面是一些 python 代码):

    class NodeAABB(object):
    
        def __init__(self, obj_bounds, obj_ids):
            self.children = [None, None]
            self.obj_bounds = obj_bounds
            self.obj_ids = obj_ids
    
        def split(self):
            # split recursively and create children here
            raise NotImplementedError()
    
        def is_leaf(self):
            return set(self.children) == {None}
    
        def build_links(self, next_right_node=None):
            if not self.is_leaf():
                child1, child2 = self.children
    
                self.hit_node = child1
                self.miss_node = next_right_node
    
                child1.build_links(next_right_node=child2)
                child2.build_links(next_right_node=next_right_node)
    
            else:
                self.hit_node = next_right_node
                self.miss_node = self.hit_node
    
        def collect(self):
            # retrieve in depth first fashion for correct order
            yield self
            if not self.is_leaf():
                child1, child2 = self.children
                yield from child1.collect()
                yield from child2.collect()
    

    将所有 AABB 存储在一个数组中(将被发送到 GPU)后,您可以使用 hit_nodemiss_node 查找链接的索引并将它们也存储起来。

    【讨论】:

    • 你能告诉我在树的构建过程中你是如何计算links.x和links.y的吗?
    • 我添加了一些示例代码,希望对您有所帮助。在我的情况下,链接是 ivec2。
    • 非常感谢!!
    • 在定义节点的next_right_node的情况下,如何确定在数组中的位置?我看不到它在哪里。
    • 我没有在上面的代码中写这个。在 build_links 中,各个节点对象作为参考存储。完成此操作后,使用 collect() 将所有节点放入一个数组中,然后您可以确定索引,例如:all_my_nodes.index(some_node.hit_node)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-08-24
    相关资源
    最近更新 更多