光线追踪(四)——运动模糊与纹理映射
前三篇中的射线只携带空间信息,材质也只有一个固定反照率。本篇沿着 《Ray Tracing: The Next Week》 的路线扩展这两个限制:首先把射线提升到时空查询,使不同样本观察到运动物体在快门期间的不同位置;然后把“颜色”抽象为一个可查询函数,让材质随空间位置或表面坐标变化。
配套 Rust 程序位于 src/bin/chapter4.rs。本篇四幅结果均由该程序以 分辨率、每像素 个样本生成。
一、运动模糊
1. 从瞬时成像到快门时间积分
此前默认所有像素都在同一瞬间拍摄。真实相机的快门会在区间 内保持开启,因此一个像素记录的是空间采样与时间采样的共同平均。若暂时只写出时间维度,像素值可以表示为
蒙特卡洛方法不需要显式求这个积分:对每条摄像机射线独立采样 ,再取平均
证明. 均匀分布的概率密度为 ,因此

2. 带时间的射线与摄像机
带时间的射线由起点 、方向 和采样时刻 构成。其空间参数仍为
Rust 中保留原来的双参数构造器,使旧代码默认在 渲染;需要运动模糊时再显式指定时间:
pub struct Ray {
pub origin: Point3,
pub direction: Vec3,
pub time: f64,
}
impl Ray {
pub const fn new(origin: Point3, direction: Vec3) -> Self {
Self::with_time(origin, direction, 0.0)
}
pub const fn with_time(origin: Point3, direction: Vec3, time: f64) -> Self {
Self { origin, direction, time }
}
}摄像机保存 shutter_open 与 shutter_close。生成射线时,除像素抖动和孔径圆盘采样外,再增加一次时间采样:
let time = if self.shutter_open == self.shutter_close {
self.shutter_open
} else {
rng.range(self.shutter_open, self.shutter_close)
};
Ray::with_time(origin, direction, time)incoming.time;若每次反弹重新抽取时间,单条路径会在运动场景的多个状态之间跳跃,得到没有物理意义的几何组合。3. 线性运动球体
令球心在 时为 、在 时为 。最简单的运动模型是线性插值:
射线—球体求交的二次方程完全不变,只需把固定球心替换为 。也就是说,时间维度被封装在“查询当前球心”这一步中。
pub fn center(&self, time: f64) -> Point3 {
self.center0
+ ((time - self.time0) / (self.time1 - self.time0))
* (self.center1 - self.center0)
}
let center = self.center(ray.time);
let oc = ray.origin - center;运动图元仍要进入 BVH。节点包围盒必须覆盖整个快门区间;对于线性运动球,只需合并两个端点球的 AABB:
4. 静止快门与运动模糊对照
下面两幅图使用完全相同的五个线性运动球。第一幅把快门固定在 ,所有样本观察同一个瞬间,因此轮廓清晰:

第二幅在 内均匀采样。每个像素累计了物体沿轨迹处于不同位置时的贡献,于是运动方向被拉成连续的模糊带:

运动模糊并不是对最终二维图片做滤镜。遮挡、阴影、反射和材质查询都发生在各自采样时刻;这正是把时间放进射线而不是在渲染后处理的价值。
二、纹理映射
1. 把颜色提升为纹理函数
固定反照率可以写成常函数 。更一般地,纹理是一个查询:
保留 与三维位置 两组输入,可以同时表达表面纹理和空间纹理。
一个三维棋盘纹理可令
根据 的奇偶选择两种颜色。这里必须使用向下取整而非向零截断,否则原点两侧的格子会不对称。
let x = (point.x / scale).floor() as i64;
let y = (point.y / scale).floor() as i64;
let z = (point.z / scale).floor() as i64;
if (x + y + z).rem_euclid(2) == 0 { even } else { odd }
棋盘线条在两个不同球面上连续弯曲,说明颜色来自同一个三维函数,而不是分别贴上的二维图片。
2. 球面 UV 坐标
图像纹理需要把单位球面点 转成二维坐标。按照本系列采用的球面朝向,定义
于是 。 沿纬线方向绕球一周, 从南极走向北极。

3. 从 UV 到图像像素
设纹理图宽高为 。最近邻采样先把 UV 限制在单位区间,再把坐标换成整数像素:
式中翻转 ,是因为数学纹理坐标通常向上增长,而图像数组的行号通常从顶部向下增长。
let u = u.clamp(0.0, 1.0);
let v = 1.0 - v.clamp(0.0, 1.0);
let i = ((u * width as f64) as usize).min(width - 1);
let j = ((v * height as f64) as usize).min(height - 1);
pixels[j * width + i]配套程序不引入图像依赖,而是先用 Rust 生成一幅 的 PPM 经纬测试图,再由 ImageTexture::load_ppm 读回像素。源纹理如下:

映射到球面后,粗黑线标出主要经纬区块,细白线帮助观察极点压缩、球面弯曲和接缝位置:

最近邻实现最容易验证,但放大时会出现块状边缘。若要使用双线性过滤,可读取 周围四个像素并做两次线性插值;其数学形式与第一篇的单线性插值相同,只是分别沿 、 两个方向执行。
三、实现检查清单
- 所有旧构造的射线默认时间为 ,保证静态场景兼容。
- 摄像机只在快门区间采样一次时间;整条反弹路径始终保留该时间。
- 运动物体求交使用
ray.time,运动 AABB 覆盖完整快门区间。 - 命中记录同时保存三维点、法向量和二维 UV。
- 空间纹理查询 ,图像纹理查询 ;材质不需要知道纹理的具体类型。
- 图像采样要限制边界并处理 方向翻转。
- 本系列所有 Rust 渲染输出现统一为至少 。
完成这些改动后,路径追踪器不再只能渲染静态、纯色的球:时间成为场景查询的一部分,颜色也成为可组合的函数。下一步可以自然扩展到 Perlin 噪声、凹凸映射、发光纹理与更复杂的动画变换。
Illustrated by