光线追踪(五)——四边形与光源

Illustrated by 夜蓝星炫(Pixiv),Source

只有球体的渲染器可以展示反射、折射和纹理,却很难构造墙壁、地板、灯板与盒子。本篇继续参考 《Ray Tracing: The Next Week》:先加入平行四边形图元,再让普通几何通过自发光材质变成具有实际面积的光源。二者结合之后,就能搭建用于研究间接光照的经典 Cornell Box。

配套 Rust 程序位于 src/bin/chapter5.rs,生成四幅 1280×720 结果图。程序采用每像素 48 个样本,并对面积光源进行显式随机采样。

注 1.(Perlin 噪声为何不作为本篇重点)
Perlin 噪声改变的是纹理值随空间位置的变化方式;四边形求交、自发光和可见性测试都不依赖它。它可以像第四篇的棋盘纹理一样作为反照率或发光纹理使用,却不会改变本篇的几何与照明算法,因此这里不展开。后续讨论程序纹理时再系统介绍噪声、湍流与大理石纹理更合适。

一、四边形图元

1. 用角点和两条边定义平行四边形

这里的 Quad 并非任意四边形,而是由角点 𝑸 与两条不平行的边向量 𝒖,𝒗 定义的平行四边形:

𝑺(𝛼,𝛽)=𝑸+𝛼𝒖+𝛽𝒗,0≤𝛼≤1,0≤𝛽≤1.

四个顶点依次为 𝑸、𝑸+𝒖、𝑸+𝒗 和 𝑸+𝒖+𝒗。𝒖 与 𝒗 不必正交,因此这种表示也覆盖倾斜的平行四边形。

图 1 射线与四边形所在平面相交,再用 (𝛼,𝛽) 判断交点是否落在边界内
定义 1.(退化四边形)
若 𝒖×𝒗=𝟎,两条边平行,张成面积为零,无法定义唯一法线与二维坐标。构造器应拒绝这种输入,而不是让后续除法产生 NaN。

Rust 构造器预先缓存法线、平面常数和坐标变换所需的向量:

let n = u.cross(v);
assert!(n.length_squared() > 1e-16);
let normal = n.unit();
let d = normal.dot(q);
let w = n / n.length_squared();

2. 先与无限平面求交

四边形所在平面满足

𝒏⋅𝒙=𝐷,𝒏=𝒖×𝒗‖𝒖×𝒗‖,𝐷=𝒏⋅𝑸.

把射线 𝒓(𝑡)=𝑶+𝑡𝒅 代入,得到

𝑡=𝐷−𝒏⋅𝑶𝒏⋅𝒅.

当分母接近零时,射线与平面平行;否则还要检查 𝑡 是否落在当前有效区间。这里只有通过平面测试,才值得继续判断有限边界。

let denominator = self.normal.dot(ray.direction);
if denominator.abs() < 1e-9 { return None; }
let t = (self.d - self.normal.dot(ray.origin)) / denominator;
if t < t_min || t > t_max { return None; }

3. 平面坐标与边界判断

令交点为 𝑷,并记 𝒑=𝑷−𝑸。即使两条边并不正交,也可用

𝒘=𝒏𝒏⋅𝒏,𝒏=𝒖×𝒗,𝛼=𝒘⋅(𝒑×𝒗),𝛽=𝒘⋅(𝒖×𝒑)

恢复式 (1) 中的两个系数。随后只需检查 𝛼,𝛽∈[0,1]。这两个系数同时就是四边形的 UV 坐标,可以直接交给第四篇的图像纹理。

定理 1.(平面坐标公式的正确性)
若 𝒑=𝛼𝒖+𝛽𝒗 且 𝒖,𝒗 不平行,则式 (4) 恢复出的数值恰为 𝛼,𝛽。

证明. 对等式两边右叉乘 𝒗,含 𝛽𝒗×𝒗 的项为零,故

𝒑×𝒗=𝛼(𝒖×𝒗)=𝛼𝒏.
再与 𝒘=𝒏𝒏⋅𝒏 点乘便得到 𝛼。对等式左叉乘 𝒖,同理得到 𝛽。
□
let planar = point - self.q;
let alpha = self.w.dot(planar.cross(self.v));
let beta  = self.w.dot(self.u.cross(planar));
if !(0.0..=1.0).contains(&alpha)
    || !(0.0..=1.0).contains(&beta) {
    return None;
}

4. 包围盒与更多平面图元

四边形的 AABB 可由四个顶点逐分量取最小值和最大值。若四边形与某个坐标平面平行,其中一个轴的厚度会严格为零;为避免擦边射线被浮点比较意外丢弃,应沿零厚度方向加入很小的 padding。

更重要的是,平面相交和二维坐标恢复可以复用。只需替换 (𝛼,𝛽) 的内部测试,就能得到不同图元:

图 2 Rust 程序渲染的五个不同朝向与倾斜程度的平行四边形

二、光源与直接照明

1. 自发光材质

此前材质只有散射:射线命中表面后得到衰减颜色和下一条射线。光源还需要一个独立的发射项 𝐿𝑒。对普通材质,𝐿𝑒=𝟎;对自发光材质,命中表面就返回指定辐亮度,而且可以不再散射。

路径递归因此从单纯的“衰减乘以后继路径”变为

𝐿(𝒓)=𝐿𝑒+𝑓𝑟𝐿(𝒓′).

在完整路径追踪器中,第二项还包含方向余弦、概率密度和采样权重;Lambert 材质使用匹配的余弦加权采样时,这些因子可以部分约去。

match hit.material {
    Material::Light(emission) => emission,
    Material::Diffuse(albedo) => {
        // 估计直接光照,并继续追踪间接光
    }
}
注 2.(发光颜色可以大于一)
反照率表示反射比例,通常限制在 [0,1];发射辐亮度不是反射比例,可以明显大于 1。例如 Color::new(15.0, 13.5, 10.5) 表示一个偏暖且很亮的光源,最后再通过曝光或色调映射压缩到显示范围。

把 Light 材质赋给球体、四边形或任何可求交对象,它就成为可见光源。背景色在这一阶段应设为黑色,否则天空本身仍会提供环境照明,使局部光源的作用不易辨认。

2. 从点光源到面积光源

点光源容易计算,但没有面积,通常产生完全锐利的阴影。四边形恰好可以同时充当可见几何和面积光源。对光源上的随机点

𝒚=𝑸+𝜉1𝒖+𝜉2𝒗,𝜉1,𝜉2∈U(0,1),

从表面点 𝒙 向 𝒚 发射阴影射线。若中间没有物体,样本就对直接光照作出贡献。

图 3 面积光源采样:不同光源位置可能被遮挡,也可能对表面可见

均匀采样面积为 𝐴𝐿 的光源时,一次 Lambert 直接光照估计可写成

𝐿̂𝑑=𝜌𝐿𝑒𝐴𝐿𝜋max(0,𝒏𝑥⋅𝝎)max(0,𝒏𝑦⋅(−𝝎))‖𝒚−𝒙‖2𝑉(𝒙,𝒚),

其中 𝜌 是反照率,𝝎 从 𝒙 指向 𝒚,𝑉 是可见性函数。两个余弦分别描述接收面和发光面朝向,距离平方体现几何衰减,𝐴𝐿 来自均匀面积采样概率密度 𝑝𝐴=1𝐴𝐿。

定义 2.(可见性函数)
若连接 𝒙 与 𝒚 的开线段不与其他表面相交,则 𝑉(𝒙,𝒚)=1;否则为 0。实现时用 [𝜀,1−𝜀] 作为阴影射线参数区间,既避免表面自相交,也避免把采样到的光源端点误判成遮挡物。

3. 光源尺寸与软阴影

面积光源上的每个位置都像一个微小光源。若遮挡物只挡住其中一部分,表面仍会收到其余部分的光,形成半影;光源越大,从一个像素看见的光源位置差异越大,阴影边界通常越柔和。

图 4 较小面积光源:照度较低,阴影边界较集中
图 5 较大面积光源:覆盖立体角更大,阴影具有更宽的半影

这组对照保持光源表面辐亮度不变,因此增大面积也会增大总功率。若只想比较阴影软硬而保持总功率近似不变,应按面积的倒数缩放 𝐿𝑒。另一方面,每像素只取少量光源样本会产生明显噪点;增加样本、分层采样,或按光源对表面的贡献做重要性采样,都能降低方差。

三、用四边形搭建场景

1. 六个面组成长方体

给定轴对齐长方体的对角点 𝒂,𝒃,先逐分量求 min、max,再构造 𝒅𝑥,𝒅𝑦,𝒅𝑧 三条边。每个面都是一个起点加两条边向量,共六个 Quad。注意相对面的边向量顺序应反转,使外法线方向一致;即使命中记录会根据入射射线翻转法线,稳定的几何朝向对单面发光和背面剔除仍很重要。

let faces = [
    Quad::new(point(min.x, min.y, max.z),  dx,  dy, mat),
    Quad::new(point(max.x, min.y, max.z), -dz,  dy, mat),
    Quad::new(point(max.x, min.y, min.z), -dx,  dy, mat),
    // 左、顶、底三个面……
];

这些面仍是普通 Hittable,可以和球体一起放进第三篇的 BVH。这里无需为“盒子”重新设计求交公式;组合已有图元往往比增加特殊分支更容易验证。

2. Cornell Box

Cornell Box 用白色地板、顶板与后墙,加上红、绿侧墙和顶部面积光源,构造一个几何简单但光照关系丰富的封闭场景。彩色墙面对其他表面的间接反射会形成 color bleeding;顶部灯板则提供明确的面积光源和软阴影。

图 6 由四边形墙面、四边形面积光源、六面体和球体组成的 Cornell Box

本篇程序为便于快速演示,显式估计直接光照,并以少量漫反射递归近似间接光;它已经能够展示遮挡、半影和墙面染色,但并非最终的无偏积分器。若完全采用第一篇渲染方程的路径采样,必须记录每种采样策略的概率密度。下一阶段进一步加入“按光源采样”和多重重要性采样,才能同时稳定处理小光源与复杂材质。

四、实现检查清单

四边形补足了平面几何,自发光材质则让几何本身成为光源。渲染器从依赖天空背景的球体展示,迈向了能够组织室内空间、可见灯具、直接阴影与间接反射的完整场景。