4、BVH加速结构:构建与遍历的精度-速度权衡

BVH,全称是包围体层次结构。说白了,就是把场景里的物体,用一层层盒子包起来。你想想看,一条光线射过来,如果先跟大盒子求交,大盒子没碰到,那里面小盒子就不用算了。这就是加速的核心思想。

我最早接触BVH是在做室内渲染的时候。场景里几千个物件,没有加速结构,一帧要算好几个小时。加了BVH之后,直接降到几分钟。嗯,这个差距就是这么夸张。

4.1 构建策略:怎么把盒子分好?

构建BVH,核心就一个问题:怎么把物体分到左右子树里。分得好,遍历就快;分得差,跟没加速差不多。

常见的策略有三种:

策略 核心思路 构建速度 遍历效率
等量分割 左右子树物体数量尽量相等 快 一般
表面积启发式(SAH) 最小化光线与包围盒相交的期望代价 慢 高
中位分割 按坐标中位数切分空间 最快 中等

我个人习惯,在离线渲染里用SAH。虽然构建慢一点,但遍历时省下的时间完全能补回来。实时渲染的话,我建议用等量分割或中位分割,构建快,帧率更稳。

关键指标:SAH的代价函数是 C = C_trav + p_L * N_L * C_intersect + p_R * N_R * C_intersect。其中p_L和p_R是光线穿过左右子节点的概率,近似等于子节点表面积与父节点表面积之比。

4.2 遍历方式:深度优先还是广度优先?

遍历BVH,说白了就是怎么在树里找交点。两种主流方式:

  • 深度优先(DFS):一条路走到黑,先探左子树,再探右子树。实现简单,栈空间小。
  • 广度优先(BFS):一层一层扫,用队列管理。适合GPU并行,但内存开销大。

我在项目中遇到过一个问题:用DFS遍历时,如果场景里有很多薄片物体,光线经常要来回回溯,效率反而下降。后来改成BFS,虽然内存多了点,但遍历次数少了30%。

小技巧:遍历时可以先算光线到左右子节点的距离,近的优先遍历。这样能更快找到交点,提前剪枝。

4.3 精度与速度的权衡点

你想想看,BVH的精度体现在哪里?其实就是包围盒跟物体的贴合程度。贴合得越紧,空测试越少,但构建代价越高。

这里有三个权衡点:

  1. 叶子节点大小:叶子节点包含的物体越多,树越浅,遍历快但求交慢。我一般控制在4-8个物体。
  2. 包围盒更新:动态场景里,物体动了,盒子要不要重建?重建精度高但慢,直接扩大盒子快但精度低。
  3. 分裂平面选择:SAH算得精细,但每层都要排序。中位分割随便选个轴,快但可能产生重叠。

避坑指南:我曾经在动态场景里每帧重建BVH,结果帧率直接掉到个位数。后来改用“先扩大包围盒+隔几帧重建一次”的策略,精度损失不到5%,速度提升了4倍。

4.4 BVH构建与遍历流程图

下面这张图,把BVH从构建到遍历的完整流程串起来了。你看一眼就明白。

BVH构建与遍历流程 场景物体列表 选择构建策略 等量分割 / SAH / 中位分割 递归构建左右子树 计算包围盒 → 分裂 → 分配物体 是否达到叶子条件? 物体数 ≤ 阈值 或 深度达上限 是 存储物体列表 否 继续递归分裂 光线遍历BVH(DFS/BFS) 精度-速度权衡:SAH构建慢但遍历快;中位分割构建快但遍历可能多走弯路 动态场景建议:先扩大包围盒,隔N帧重建一次

4.5 实战中的取舍建议

说了这么多理论,最后给点实在的。我根据项目经验,总结了几个场景下的推荐配置:

场景类型 构建策略 叶子大小 遍历方式 更新策略
静态场景(离线渲染) SAH 4个物体 DFS 一次性构建
动态场景(游戏) 等量分割 8个物体 BFS 扩大盒子+隔帧重建
大规模场景(百万级) 中位分割+SAH混合 6个物体 DFS 分层更新

嗯,这里要注意。没有银弹。每个场景都要自己调参。我建议你先把SAH跑通,然后对比等量分割,看看你的场景更适合哪个。

我的习惯:先跑一次SAH构建,记录遍历时间。然后换成等量分割,看遍历时间差了多少。如果差不到20%,我就用等量分割。毕竟构建快,迭代起来爽。