第-3-章-相机系统与投影模型
Course: I wrote a Ray Tracer from scratch… in a Year
🎮 第 3 章:相机系统与投影模型
🏛 适用环境:CPU 光线追踪 / SDL2 渲染 / Vulkan 摄像机系统
🎯 目标:掌握光线生成、视角(FOV)、成像投影、抗锯齿与景深效果(Depth of Field, DOF)的实现。
🧭 引擎定位
相机(Camera)模块是整个光线追踪管线的“起点”。
它负责将 像素坐标 → 射线方向 的变换,并决定视角、焦距、成像范围等参数。
在渲染循环中:
每个像素的第一条光线都由 Camera 决定。
🏷 关键词速记
Camera|相机系统
Projection|投影模型
Field of View|视场角
Aspect Ratio|宽高比
Ray Generation|光线生成
Depth of Field|景深
Antialiasing|抗锯齿
🎮 30 秒玩法摘要
想让你的光线“看到世界”?那就给它一只眼睛。
相机是光线的母体,控制角度、景深、模糊与清晰。
📚 核心原理讲解
🔹 1. 投影几何基础
在光线追踪中,相机通常位于原点,朝 -Z 方向。
像素所在的“成像平面(View Plane)”位于相机前方距离 focal_length 的位置。
每个像素对应一个方向向量:
其中:
- [\mathbf{O}]:相机原点
- [\mathbf{L}]:左下角视窗位置
- [\mathbf{H}, \mathbf{V}]:水平、垂直跨度向量
- [u,v \in (0,1)]:像素归一化坐标
🔹 2. 视角(FOV)与画面比例(Aspect Ratio)
垂直视场角 [\theta] 控制相机"张嘴"大小:
画面宽高比(aspect ratio = width / height)用于计算视窗宽度。
📦 计算视窗范围
float aspect_ratio = float(image_width) / image_height;
float viewport_height = 2.0f * tan(fov / 2.0f);
float viewport_width = aspect_ratio * viewport_height;🔹 3. 光线生成(Ray Generation)
📦 C++ 相机类核心代码
class Camera {
public:
Vec3 origin;
Vec3 horizontal, vertical;
Vec3 lower_left_corner;
Camera(float fov, float aspect) {
float theta = radians(fov);
float h = tan(theta / 2);
float viewport_height = 2.0f * h;
float viewport_width = aspect * viewport_height;
origin = Vec3(0, 0, 0);
horizontal = Vec3(viewport_width, 0, 0);
vertical = Vec3(0, viewport_height, 0);
lower_left_corner = origin - horizontal/2 - vertical/2 - Vec3(0, 0, 1);
}
Ray getRay(float u, float v) const {
return Ray(origin, lower_left_corner + u*horizontal + v*vertical - origin);
}
};解释:
lower_left_corner是屏幕左下角在世界空间的位置。- 每个像素 (u,v) 都发出一条从
origin指向该点的光线。
🔹 4. 抗锯齿(Anti-Aliasing)
像素中心采样会导致硬边。
解决办法:对每个像素随机发射多条光线,取平均颜色。
📦 多采样伪代码
Color pixelColor = Color(0,0,0);
for (int s=0; s < samples_per_pixel; ++s) {
float u = (x + randomFloat()) / image_width;
float v = (y + randomFloat()) / image_height;
Ray ray = camera.getRay(u, v);
pixelColor += trace(ray);
}
pixelColor /= samples_per_pixel;效果:边缘更柔和、噪点均匀分布。
🔹 5. 景深(Depth of Field)
模拟真实相机焦距与模糊:
- 将相机镜头视为圆形孔径。
- 光线从镜头不同点射出,聚焦于焦平面上。
- 超出焦距的物体模糊。
📦 含景深的相机实现
class CameraDOF {
public:
Vec3 origin, u, v, w;
float lens_radius;
Vec3 lower_left_corner, horizontal, vertical;
CameraDOF(Vec3 lookFrom, Vec3 lookAt, Vec3 vup,
float vfov, float aspect, float aperture, float focus_dist) {
float theta = radians(vfov);
float h = tan(theta/2);
float viewport_height = 2.0f * h;
float viewport_width = aspect * viewport_height;
w = normalize(lookFrom - lookAt);
u = normalize(cross(vup, w));
v = cross(w, u);
origin = lookFrom;
horizontal = focus_dist * viewport_width * u;
vertical = focus_dist * viewport_height * v;
lower_left_corner = origin - horizontal/2 - vertical/2 - focus_dist*w;
lens_radius = aperture / 2;
}
Ray getRay(float s, float t) const {
Vec3 rd = lens_radius * randomInUnitDisk();
Vec3 offset = u * rd.x + v * rd.y;
return Ray(origin + offset,
lower_left_corner + s*horizontal + t*vertical - origin - offset);
}
};🎯 焦距控制清晰范围,aperture 决定模糊强度。
🧪 实验与验证步骤
1️⃣ 相机移动
- 改变
lookFrom观察不同角度。
2️⃣ 抗锯齿验证
- 渲染棋盘格球体,对比单样本与 50 样本的边缘平滑度。
3️⃣ 景深验证
- 让两个球体位于不同深度,调整 aperture 与 focus_dist。
🪄 创作与实现建议
- 将
Camera设计为可热切换模块,便于添加多相机视角。 - 渲染调试时可启用 视野可视化(在屏幕上画出相机 frustum)。
- 用不同的随机数种子生成多帧,可实现时间抗锯齿(TAA)。
- 景深实现推荐使用随机 disk 采样 + 多线程加速。
🧠 记忆口诀
“像素生光眼为源,视锥定界景为圆;
多采一抹锯齿平,焦外模糊景深现。”
✅ 实践题
Q1:相机中 lower_left_corner 的作用是什么?
🅰️ 确定成像平面在世界空间中的基准点。
Q2:为什么抗锯齿要随机采样?
🅰️ 随机采样能均匀平滑像素间边界,减少阶梯效应。
Q3:如何控制景深强度?
🅰️ 增大 aperture(光圈半径)会增强模糊效果。