包围盒(Bounding box)是加速光线追踪(Ray Tracing)的最简单方法,不一定将其视为加速结构,但这无疑是减少渲染时间的最简单方法。
推荐:用 NSDT设计器 快速搭建可编程3D场景。
使用包围盒来加速光线追踪的想法已经很古老了。 很难确切地知道这个想法是由谁以及何时提出的。 在 1980 年题为“复杂场景快速渲染的 3 维表示”的论文中,作者 Ruben 和 Whitted 已经谈到了使用此类边界体积的层次结构进行“快速”渲染的可能性(请参阅末尾的参考资料部分) 本课的内容)。
茶壶中的每个栅格都可能有一百多个三角形:栅格有 8 个分区,包含 128 个三角形,这需要对投射到场景中的每条光线进行类似数量的相交测试。 相反,我们可以做的是光线追踪一个包含栅格所有顶点的盒子。 这样的盒子称为包围盒:它是围绕栅格的最紧密的可能体块(在这种情况下是一个盒子,但它也可以是一个球体)。 图 1 显示了构成茶壶模型的 32 个贝塞尔曲线块中每一个的边界框。
图 2:测试边界框与射线的相交。 可以安全地拒绝未与射线相交的包围体所包含的几何图形。
我们可以首先测试光线是否与该盒子相交,如果没有相交,我们就可以确定它无法与该包围盒中包含的几何体相交(图 2)。 如果我们可以忽略此边界框中的三角形,则可以节省 128 次光线-三角形相交测试。
考虑到场景中的许多边界框也可能无法通过此测试,总的来说,我们可以节省对光线三角形例程的大量调用。 如果射线与边界框相交,则必须测试体积中包含的所有三角形是否与该射线相交。 在图 2 中,我们可以看到一条射线与构成茶壶模型的贝塞尔曲面片的边界框相交。 只有彩色框才会与射线相交,并且只有这些框包含的网格才会被测试与射线的相交。 可以安全地拒绝包含在未相交的框中的补丁。
下面的伪代码说明了这个过程:
for (each object in scene) {
if (intersect(ray, object->boundingBox) == true) {
for (each triangle in object) {
if (intersect(ray, triangle) == true) {
// the ray intersects the triangle
...
}
}
}
}
图 3:Ape 模型的边界框(由 Kenichi Nishida 提供)
计算对象的边界框非常简单。 我们可以循环组成网格的所有顶点,以找到每个点坐标(xyz 点位置)的最小值和最大值。 这些值形成了我们所说的对象的最小和最大范围,并定义了边界框的两个角(见图 3)。
对象的边界框通常在构建对象时计算(例如在多边形网格类的构造函数中)并存储在对象类的成员变量中(每个基元类型可能需要不同的方法来计算此边界框 . 例如,二次球体的包围体积可以根据其半径计算)。
光线追踪器中的以下代码片段显示了我们如何计算多边形对象的边界框(第 13 行)。 bbox 变量是 Object 基类的成员变量(代码可在本课最后一章中找到)。
class PolygonMesh : public Object
{
public:
PolygonMesh(
const Matrix44f &o2w,
const uint32_t &np, const uint32_t *nv, const uint32_t *v,
const Vec3f *pts, const Vec3f *nors = NULL) :
Object(o2w), ntris(0), tris(NULL), P(NULL), N(NULL)
{
...
for (uint32_t i = 0; i < maxVertIndex + 1; ++i) {
objectToWorld.multVecMatrix(pts[i], P[i]);
bbox.extendBy(P[i]);
}
...
}
};
template
class Box
{
public:
...
void extendBy(const Vec& pt)
{
for (size_t i = 0; i < D; ++i) {
if (pt[i] < bounds[0][i]) bounds[0][i] = pt[i];
if (pt[i] > bounds[1][i]) bounds[1][i] = pt[i];
}
}
...
};
其余的代码非常简单。 我们将使用我们在第 7 课(相交简单形状)中学习的射线盒相交例程:
uint64_t numRayBoxTests = 0;
template
bool Box::intersect(const Ray& r) const
{
__sync_fetch_and_add(&numRayBoxTests, 1);
...
return true;
}
我们使用与射线-三角形相交方法相同的方法来计算射线-盒相交方法被调用的次数。 每次调用 box intersect 方法时,都会使用原子添加操作来递增 box.cpp 文件中声明的全局变量 numRayBoxTests。 最终值与其他统计信息一起在渲染结束时打印出来。
我们还创建了一个 AccelerationStructure 基类,其行为与对象非常相似。 它拥有一个 intersect 方法,循环遍历场景中的所有对象(第 5 行)。 我们首先测试射线和当前对象的边界框之间的相交(第 7 行)。 如果测试成功,则将针对盒子中包含的对象测试射线(第 9 行)。 其余代码与第 11 课中的跟踪函数的实现非常相似。每次三角形相交时,我们都需要更新到相交点的最近距离(第 10-11 行)并保留指向相交点的指针 对象(第 12 行):
const Object* AccelerationStructure::intersect(const Ray& ray, IsectData& isectData) const
{
float tClosest = ray.tmax;
Object *hitObject = NULL;
for (size_t i = 0; i < rc->objects.size(); ++i) {
Ray r(ray);
if (rc->objects[i]->bbox.intersect(r)) {
IsectData isectDataCurrent;
if (rc->objects[i]->intersect(ray, isectDataCurrent)) {
if (isectDataCurrent.t < tClosest && isectDataCurrent.t > ray.tmin) {
isectData = isectDataCurrent;
hitObject = rc->objects[i];
tClosest = isectDataCurrent.t;
}
}
}
}
return (tClosest < ray.tmax) ? hitObject : NULL;
}
最后我们需要更新跟踪功能。 我们不需要循环场景中的所有对象,只需调用 AccelerationStructure 类中的 intersect 方法即可。 如果此方法返回指向场景中对象的有效指针,则发生相交,我们可以使用相交对象的颜色设置像素的颜色:
template
void trace(const Ray ray, const RenderContext *rc, Spectrum &s)
{
IsectData isectData;
const Object *hitObject = NULL;
if ((hitObject = rc->accelStruct->intersect(ray, isectData)) != NULL)
s.xyz = hitObject->color;
else
s.xyz = rc->backgroundColor;
}
如果我们渲染上一章中使用的场景,我们现在得到以下统计数据:
Render time : 3.96 (sec)
Total number of triangles : 16384
Total number of primary rays : 307200
Total number of ray-triangles tests : 129335296
Total number of ray-triangles intersections : 111889
Total number of ray-box tests : 9830400
渲染时间减少了约 34 倍。从逻辑上讲,使用和不使用此基本加速结构执行的光线三角形测试数量之间的比率大致相同:38。由于额外的时间,这两个数字并不完全相同 现在需要计算光线盒相交(983 万次)。 然而,我们从使用该方案中获得的好处很大程度上超过了这些测试所需的额外工作。
本章介绍的技术在茶壶的情况下效果很好,因为模型包含许多分辨率(就多边形数量而言)相当低的网格。 另一方面,猿模型非常复杂,并且由单个部件制成。 如果我们要渲染这个对象,只有当主光线不与其边界框相交时(也就是说,对于典型的帧,可能少于像素总数的三分之一),我们才会节省时间。 对于所有过度光线,我们仍然需要对构成模型的数十万个三角形执行相交测试。 在这种特殊情况下,与暴力方法相比,边界框方法只会带来很小的好处。 它实现起来简单且快速,但显然并不适合所有情况,因此它不被认为是一种非常好的加速方法。
原文链接:光线追踪加速机制- 包围盒 — BimAnt