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的精度体现在哪里?其实就是包围盒跟物体的贴合程度。贴合得越紧,空测试越少,但构建代价越高。
这里有三个权衡点:
- 叶子节点大小:叶子节点包含的物体越多,树越浅,遍历快但求交慢。我一般控制在4-8个物体。
- 包围盒更新:动态场景里,物体动了,盒子要不要重建?重建精度高但慢,直接扩大盒子快但精度低。
- 分裂平面选择:SAH算得精细,但每层都要排序。中位分割随便选个轴,快但可能产生重叠。
避坑指南:我曾经在动态场景里每帧重建BVH,结果帧率直接掉到个位数。后来改用“先扩大包围盒+隔几帧重建一次”的策略,精度损失不到5%,速度提升了4倍。
4.4 BVH构建与遍历流程图
下面这张图,把BVH从构建到遍历的完整流程串起来了。你看一眼就明白。
4.5 实战中的取舍建议
说了这么多理论,最后给点实在的。我根据项目经验,总结了几个场景下的推荐配置:
| 场景类型 | 构建策略 | 叶子大小 | 遍历方式 | 更新策略 |
|---|---|---|---|---|
| 静态场景(离线渲染) | SAH | 4个物体 | DFS | 一次性构建 |
| 动态场景(游戏) | 等量分割 | 8个物体 | BFS | 扩大盒子+隔帧重建 |
| 大规模场景(百万级) | 中位分割+SAH混合 | 6个物体 | DFS | 分层更新 |
嗯,这里要注意。没有银弹。每个场景都要自己调参。我建议你先把SAH跑通,然后对比等量分割,看看你的场景更适合哪个。
我的习惯:先跑一次SAH构建,记录遍历时间。然后换成等量分割,看遍历时间差了多少。如果差不到20%,我就用等量分割。毕竟构建快,迭代起来爽。