我将详细解析这段代码中的 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 实现,平衡了构建速度、遍历效率和内存使用。