1036 字
5 分钟
光线追踪:Cornell Box

实例#

我们通过创建六个矩形组成的 hittable_list ,来编写一个返回盒子的函数:

inline shared_ptr<hittable_list> box(const point3& a, const point3& b, shared_ptr<material> mat)
{
// 用两个对角顶点 a、b 定义一个轴对齐长方体,并返回它的 6 个面。
// 返回类型是 hittable_list,是因为长方体本身不是单个基础图元,
// 而是由 front/right/back/left/top/bottom 六个 quad 共同组成。
auto sides = make_shared<hittable_list>();
// 调用者传入的 a、b 不一定按最小角和最大角排序。
// 这里先整理出每个坐标轴上的 min/max,保证后续构造面的方向稳定。
auto min = point3(std::fmin(a.x(),b.x()), std::fmin(a.y(),b.y()), std::fmin(a.z(),b.z()));
auto max = point3(std::fmax(a.x(),b.x()), std::fmax(a.y(),b.y()), std::fmax(a.z(),b.z()));
// 三个边向量分别表示长方体在 x/y/z 方向上的尺寸。
// 每个 quad 由一个起点 Q 和两条边向量定义,因此下面会复用 dx/dy/dz
// 或它们的反方向,拼出朝向正确的六个面。
auto dx = vec3(max.x() - min.x(), 0, 0);
auto dy = vec3(0, max.y() - min.y(), 0);
auto dz = vec3(0, 0, max.z() - min.z());
// 每一面都选一个角点作为 Q,再给出沿该面的两条边。
// 边向量顺序会影响 quad 的法线方向,下面的排列让法线整体朝向盒子外侧。
sides->add(make_shared<quad>(point3(min.x(), min.y(), max.z()), dx, dy, mat)); // 前面:z = max.z()
sides->add(make_shared<quad>(point3(max.x(), min.y(), max.z()), -dz, dy, mat)); // 右面:x = max.x()
sides->add(make_shared<quad>(point3(max.x(), min.y(), min.z()), -dx, dy, mat)); // 后面:z = min.z()
sides->add(make_shared<quad>(point3(min.x(), min.y(), min.z()), dz, dy, mat)); // 左面:x = min.x()
sides->add(make_shared<quad>(point3(min.x(), max.y(), max.z()), dx, -dz, mat)); // 顶面:y = max.y()
sides->add(make_shared<quad>(point3(min.x(), min.y(), min.z()), dx, dz, mat)); // 底面:y = min.y()
return sides;
}

实例平移#

推理方法是:先将入射光线反向移动偏移量,判断是否发生相交,再将相交点正向移动偏移量。

我们需要将相交点正向移动偏移量,这样才能确保相交点确实位于入射光线的路径上。如果忘记正向移动相交点,那么相交点就会落在偏移光线的路径上,这就不对了。现在让我们添加实现这个功能的代码。

代码思路:不真正移动物体,而是把光线反向移动来复用原物体的 hit 逻辑。例如物体被平移了 offset,检测命中时就让光线起点减去 offset,相当于在物体原来的位置上做求交;命中后,再把 rec.p 加回 offset,得到真实世界坐标下的交点。bbox = object->bounding_box() + offset 则保证包围盒也跟着物体一起平移。

class translate : public hittable {
public:
// translate 是一个“包装器”物体:它不改变原物体形状,只把原物体整体平移 offset。
translate(std::shared_ptr<hittable> object, const vec3& offset)
: object(object), offset(offset)
{
// 原物体的包围盒也要同步平移,方便 BVH 等加速结构继续使用正确的空间范围。
bbox = object->bounding_box() + offset;
}
bool hit(const ray& r, interval ray_t, hit_record& rec) const override {
// 把光线反向平移 offset,相当于把“平移后的物体”临时还原到原来的位置做求交。
ray offset_r(r.origin() - offset, r.direction());
// 在原物体的局部位置上检测 offset_r 是否命中。
if (!object->hit(offset_r, ray_t, rec))
return false;
// object->hit 得到的是原物体坐标下的交点,需要再平移回世界坐标。
rec.p += offset;
return true;
}
// 返回已经平移过的包围盒。
aabb bounding_box() const override { return bbox; }
private:
std::shared_ptr<hittable> object; // 被包装、被平移的原始物体。
vec3 offset; // 平移向量,表示原物体整体移动的方向和距离。
aabb bbox; // 平移后的轴对齐包围盒。
};

我们还需要记得对包围盒进行偏移,否则入射光线可能会定位到错误的位置,从而轻易地判定为不相交。上述的 object->bounding_box() + offset 表达式需要一些额外的支持。

class aabb {
...
};
aabb operator+(const aabb& bbox, const vec3& offset) {
return aabb(bbox.x + offset.x(), bbox.y + offset.y(), bbox.z + offset.z());
}
aabb operator+(const vec3& offset, const aabb& bbox) {
return bbox + offset;
}

由于 aabb 的每个维度都表示为一个区间,我们还需要为 interval 扩展一个加法运算符。

interval operator+(const interval& ival, double displacement) {
return interval(ival.min + displacement, ival.max + displacement);
}
interval operator+(double displacement, const interval& ival) {
return ival + displacement;
}

实例旋转#

光线追踪:Cornell Box
https://dingfengbo.vercel.app/posts/rtiow/cornell-box/
作者
Eureka
发布于
2026-05-30
许可协议
CC BY-NC-SA 4.0