无标题
我将详细解析这段代码中的 BVH(Bounding Volume Hierarchy)加速结构 的构建过程,结合图例说明其结构和构建方式。
一、BVH 结构概览
1. BVH 节点结构(BVHNode)
struct BVHNode {
glm::vec3 min; // 包围盒最小值
float pad0; // 填充
glm::vec3 max; // 包围盒最大值
float pad1; // 填充
int leftChild = -1; // 左子节点索引
int firstPrim = 0; // 图元起始索引(叶子节点)或右子节点索引(内部节点)
int primCount = 0; // 图元数量(叶子节点)
int pad2; // 填充
};
设计特点: - 使用 紧凑的
AABB(轴对齐包围盒) 表示 - 支持
32字节对齐(便于 GPU 访问) -
复用字段:firstPrim
在叶子节点中存储图元索引,在内部节点中存储右子节点索引
2. 构建数据结构
struct BuildPrimitive {
AABB bounds; // 三角形包围盒
glm::vec3 center; // 三角形中心点
int triangleIndex; // 对应三角形索引
int pad0;
};
二、BVH 构建流程
步骤 1:数据准备
graph TD
A[输入网格 Mesh] --> B[提取三角形]
B --> C[应用变换矩阵]
C --> D[创建 BuildPrimitive 数组]
D --> E[每个三角形计算包围盒和中心]
步骤 2:递归构建(核心算法)
int BuildRecursive(std::vector<BuildPrimitive>& prims, int start, int end, int depth)
构建流程图:
graph TD
A[开始 prims【start:end】] --> B{图元数量 ≤ 2?}
B -->|是| C[创建叶子节点]
B -->|否| D[计算包围盒和分割轴]
D --> E[分割图元数组]
E --> F[递归构建左子树]
E --> G[递归构建右子树]
F --> H[合并子节点包围盒]
G --> H
C --> I[计算叶子包围盒]
H --> J[返回节点索引]
I --> J
subgraph "叶子节点"
C -->|设置| K[primCount = 图元数]
C -->|设置| L[firstPrim = start]
C -->|设置| M[leftChild = -1]
end
subgraph "内部节点"
H -->|设置| N[primCount = 0]
H -->|设置| O[firstPrim = 右子节点索引]
H -->|设置| P[leftChild = 左子节点索引]
end
步骤 3:图元重排(关键优化)
// 在 Build() 函数中,构建完成后:
std::vector<Triangle> sortedTriangles;
for (const auto& prim : prims) {
sortedTriangles.push_back(triangles[prim.triangleIndex]);
}
triangles = std::move(sortedTriangles);
为什么需要重排? - 构建过程中 prims
数组被 std::partition 重新排列 -
重排后,同一叶子节点内的三角形在内存中连续存储 - 显著提高
缓存命中率,提升遍历性能
三、BVH 树结构示例
假设有 8 个三角形,构建后的 BVH 树可能如下:
Level 0: [根节点 0]
/ \
Level 1: [内部节点 1] [内部节点 2]
/ \ / \
Level 2: [叶子节点 3] [叶子节点 4] [叶子节点 5] [叶子节点 6]
(三角形0-1) (三角形2-3) (三角形4-5) (三角形6-7)
内存布局:
nodes 数组: [节点0, 节点1, 节点2, 节点3, 节点4, 节点5, 节点6]
triangles 数组: [三角形0, 三角形1, 三角形2, 三角形3, 三角形4, 三角形5, 三角形6, 三角形7]
↑叶子3的三角形↑ ↑叶子4的三角形↑ ↑叶子5的三角形↑ ↑叶子6的三角形↑
四、分割策略(SplitNode 函数)
分割流程:
graph LR
A[开始] --> B[计算所有图元中心的包围盒]
B --> C{选择最长轴 XYZ}
C --> D[计算分割点 = 中点]
D --> E[使用 std::partition 分割]
E --> F{分割是否平衡?}
F -->|是| G[返回成功]
F -->|否| H[强制中点分割]
H --> G
分割准则: 1. 轴选择:选择图元中心点分布最分散的轴(X、Y 或 Z) 2. 分割点:选择该轴的中点 3. 平衡检查:确保两边都有图元,否则强制平分
五、遍历算法(IntersectRecursive)
bool IntersectRecursive(int nodeIndex, const Ray& ray, float tMin, float tMax, HitRecord& rec) const
遍历流程:
graph TD
A[开始遍历] --> B[获取当前节点]
B --> C{与节点AABB相交?}
C -->|否| D[返回 false]
C -->|是| E{是否为叶子节点?}
E -->|是| F[遍历所有三角形]
F --> G{有命中?}
G -->|是| H[更新最近命中]
G -->|否| I[继续]
E -->|否| J[递归遍历左子树]
J --> K[递归遍历右子树]
K --> L{比较左右命中结果}
L --> M[返回最近命中]
六、GPU 友好设计
1. 数据结构对齐
#pragma pack(push, 16) // 强制 16 字节对齐
struct BVHNode { ... };
struct Triangle { ... };
struct GPUMaterial { ... };
#pragma pack(pop)
2. 内存布局优化
- 节点数组:连续存储,索引访问
- 三角形数组:按 BVH 顺序重排,提高缓存一致性
- 材质数组:独立存储,支持纹理索引
七、性能优化要点
1. 构建阶段优化:
- 使用
std::partition快速分割 - 限制叶子节点大小(≤ 2 个三角形)
- 记录最大深度用于分析
2. 遍历阶段优化:
- 早期退出:不相交的节点立即跳过
- 最近命中更新:维护当前最近命中距离
tMax - 堆栈优化:递归实现简单,但可以考虑迭代+栈优化
3. 内存优化:
- 紧凑存储:32字节/节点,16字节/三角形
- 重排数据:提高空间局部性
- 填充字段:确保 GPU 内存对齐
八、混合渲染应用
在 RayTracer::RenderShadows() 和
RenderReflections() 中:
// 共享相同的 BVH 数据
bvhNodesBuffer.Bind(4); // BVH 节点缓冲区
trianglesBuffer.Bind(5); // 三角形缓冲区
materialsBuffer.Bind(6); // 材质缓冲区
优势: - 一次构建,多次使用 - 支持阴影光线、反射光线、主光线等不同查询类型 - GPU 计算着色器直接访问结构化数据
总结
这个 BVH 实现采用了 自顶向下递归构建 + SAH(Surface Area Heuristic)近似分割 的策略。虽然代码中使用了简单的 中点分割(而非完整的 SAH),但通过图元重排、紧凑存储和 GPU 友好设计,实现了高效的射线相交加速。
关键创新点: 1.
字段复用设计:firstPrim
在叶子/内部节点的不同用途 2.
构建后重排:优化内存访问模式 3. GPU
对齐布局:便于计算着色器直接访问 4.
混合渲染共享:同一 BVH 支持多种光线追踪任务
这是一个实用的、面向实时渲染的 BVH 实现,平衡了构建速度、遍历效率和内存使用。