第-3-章-相机系统与投影模型

Course: I wrote a Ray Tracer from scratch… in a Year


🏛 适用环境:CPU 光线追踪 / SDL2 渲染 / Vulkan 摄像机系统

🎯 目标:掌握光线生成、视角(FOV)、成像投影、抗锯齿与景深效果(Depth of Field, DOF)的实现。


相机(Camera)模块是整个光线追踪管线的“起点”。

它负责将 像素坐标 → 射线方向 的变换,并决定视角、焦距、成像范围等参数。

在渲染循环中:

每个像素的第一条光线都由 Camera 决定。


Camera|相机系统

Projection|投影模型

Field of View|视场角

Aspect Ratio|宽高比

Ray Generation|光线生成

Depth of Field|景深

Antialiasing|抗锯齿


想让你的光线“看到世界”?那就给它一只眼睛。

相机是光线的母体,控制角度、景深、模糊与清晰。


在光线追踪中,相机通常位于原点,朝 -Z 方向。

像素所在的“成像平面(View Plane)”位于相机前方距离 focal_length 的位置。

每个像素对应一个方向向量:

D=normalize(L+uH+vVO) \mathbf{D} = \text{normalize}(\mathbf{L} + u \cdot \mathbf{H} + v \cdot \mathbf{V} - \mathbf{O})

其中:

  • [\mathbf{O}]:相机原点
  • [\mathbf{L}]:左下角视窗位置
  • [\mathbf{H}, \mathbf{V}]:水平、垂直跨度向量
  • [u,v \in (0,1)]:像素归一化坐标

垂直视场角 [\theta] 控制相机"张嘴"大小:

h=tan(θ2) h = \tan\left(\frac{\theta}{2}\right)

画面宽高比(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;

📦 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 指向该点的光线。

像素中心采样会导致硬边。

解决办法:对每个像素随机发射多条光线,取平均颜色。

📦 多采样伪代码

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;

效果:边缘更柔和、噪点均匀分布。


模拟真实相机焦距与模糊:

  • 将相机镜头视为圆形孔径。
  • 光线从镜头不同点射出,聚焦于焦平面上。
  • 超出焦距的物体模糊。

📦 含景深的相机实现

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(光圈半径)会增强模糊效果。