7计算机图形学原理及实践:基础方法(第3版)

光线追踪

全局光照

光线追踪阴影反射折射

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第7章 光线追踪(Ray Tracing)

一、导读

1.1 本章学习目标

光线追踪是生成高质量真实感图像的核心技术。与光栅化渲染不同,光线追踪通过模拟光线在场景中的传播路径来生成图像,自然地产生了反射、折射、阴影、焦散等全局光照效果。从 1979 年 Turner Whitted 的经典论文到 2018 年 NVIDIA RTX 实现实时硬件光线追踪,这项技术经历了四十年的发展,正在深刻改变图形学的格局。

本章的核心学习目标包括:

  • 理解光线追踪的基本原理:掌握从相机发射光线、追踪光线路径、计算交点和着色的完整流程。
  • 实现光线-物体求交算法:掌握光线与平面、球体、三角形等基本图元的求交算法。
  • 理解阴影的生成机制:通过阴影射线(Shadow Ray)实现硬阴影和软阴影。
  • 掌握反射和折射的追踪:实现递归光线追踪,处理镜面反射和透明折射效果。
  • 理解路径追踪的基本原理:了解蒙特卡洛方法在全局光照中的应用,理解采样、噪声和收敛的关系。
  • 了解实时光线追踪技术:了解 BVH 加速结构、NVIDIA RTX 硬件架构、以及光栅化与光线追踪混合渲染的现代方案。

1.2 为什么本章如此重要

光线追踪代表了计算机图形学中"正确"的渲染方式。它直接模拟光的物理行为,因此可以自然地产生光栅化难以实现的效果:精确的反射、透明的折射、柔和的阴影、间接光照(颜色溢出)、焦散等。在电影和广告行业,光线追踪(及其扩展——路径追踪)已经是标准的渲染方法。

更重要的是,随着硬件的进步,光线追踪正在从离线渲染走向实时应用。NVIDIA RTX 系列 GPU 提供了专用的光线追踪加速核心(RT Core),使得在游戏中实时追踪数百万条光线成为可能。DirectX Raytracing(DXR)和 Vulkan Ray Tracing 等 API 为开发者提供了统一的接口。理解光线追踪的原理,是掌握下一代图形技术的基础。

即使在不完全使用光线追踪的场景中,光栅化渲染也经常借助光线追踪的思想:阴影贴图可以看作是一种从光源出发的可见性查询,环境光遮蔽本质上是对半球方向的采样——这些技术都与光线追踪有着深层的联系。

1.3 核心问题引导

在阅读本章时,请带着以下问题思考:

光线追踪为什么从相机发射光线而不是从光源发射?反向追踪的物理依据是什么?

光线与三角形的求交算法(Möller-Trumbore)是如何工作的?为什么它比先求平面再判断点在三角形内更高效?

递归光线追踪的终止条件是什么?如果不加限制地递归下去会怎样?如何平衡递归深度与图像质量?

路径追踪与 Whitted 光线追踪有什么本质区别?为什么路径追踪能产生更真实的效果(如颜色溢出、软阴影)?

蒙特卡洛积分的收敛速度是多少?为什么路径追踪的图像有噪声?如何加速收敛?

BVH(Bounding Volume Hierarchy)如何加速光线-场景求交?为什么它是实时光线追踪的核心数据结构?


二、核心概念详解

2.1 光线追踪基础

2.1.1 基本思想

光线追踪的核心思想是反向追踪光路:从相机(眼睛)出发,通过每个像素发射一条光线,追踪光线在场景中的传播路径,计算光线与物体的交点处的颜色。

为什么反向追踪?

正向追踪(从光源出发):
  光源 → 发射数十亿光子 → 大部分永远不会到达相机 → 极度浪费

反向追踪(从相机出发):
  相机 → 通过每个像素发射一条光线 → 追踪到光源 → 每条光线都有贡献

这是"伴随原理"(adjoint principle)的应用:
光路是可逆的,从 A 到 B 的路径与从 B 到 A 的路径相同
cpp
// 光线的基本定义
struct Ray {
  glm::vec3 origin;    // 光线起点
  glm::vec3 direction; // 光线方向(单位向量)

  glm::vec3 at(float t) const {
    return origin + t * direction;
  }
};

// 光线追踪渲染循环
void render(Image& image, const Scene& scene, const Camera& camera) {
  for (int y = 0; y < image.height; y++) {
    for (int x = 0; x < image.width; x++) {
      // 通过像素中心发射光线
      float u = (x + 0.5f) / image.width;
      float v = (y + 0.5f) / image.height;
      Ray ray = camera.generateRay(u, v);

      // 追踪光线
      glm::vec3 color = traceRay(ray, scene, MAX_DEPTH);
      image.setPixel(x, y, color);
    }
  }
}

2.1.2 相机模型

cpp
// 针孔相机模型
struct Camera {
  glm::vec3 origin;
  glm::vec3 lowerLeftCorner;
  glm::vec3 horizontal;
  glm::vec3 vertical;
  glm::vec3 u, v, w; // 相机坐标系的基向量

  Camera(glm::vec3 lookFrom, glm::vec3 lookAt, glm::vec3 vup,
         float fovDegrees, float aspect, float aperture, float focusDist) {
    float theta = glm::radians(fovDegrees);
    float halfHeight = std::tan(theta / 2.0f);
    float halfWidth = aspect * halfHeight;

    w = glm::normalize(lookFrom - lookAt);
    u = glm::normalize(glm::cross(vup, w));
    v = glm::cross(w, u);

    lowerLeftCorner = origin - halfWidth * focusDist * u -
                      halfHeight * focusDist * v - focusDist * w;
    horizontal = 2.0f * halfWidth * focusDist * u;
    vertical = 2.0f * halfHeight * focusDist * v;
  }

  Ray generateRay(float s, float t) const {
    glm::vec3 direction = glm::normalize(
      lowerLeftCorner + s * horizontal + t * vertical - origin
    );
    return Ray(origin, direction);
  }
};

2.2 光线-物体求交

2.2.1 光线与球体求交

球体方程:|P - C|² = R²,其中 C 是球心,R 是半径。

光线方程:P(t) = O + tD。

代入得:|O + tD - C|² = R²

展开为关于 t 的二次方程:at² + bt + c = 0

a = D · D
b = 2D · (O - C)
c = (O - C) · (O - C) - R²

判别式 Δ = b² - 4ac
Δ < 0: 无交点
Δ = 0: 相切(一个交点)
Δ > 0: 两个交点(取较小的正 t)
cpp
// 光线与球体求交
struct HitRecord {
  float t;
  glm::vec3 position;
  glm::vec3 normal;
  Material* material;
};

bool hitSphere(const Ray& ray, float tMin, float tMax,
               const Sphere& sphere, HitRecord& hit) {
  glm::vec3 oc = ray.origin - sphere.center;
  float a = glm::dot(ray.direction, ray.direction);
  float b = 2.0f * glm::dot(oc, ray.direction);
  float c = glm::dot(oc, oc) - sphere.radius * sphere.radius;
  float discriminant = b * b - 4.0f * a * c;

  if (discriminant < 0) return false;

  // 取较小的正根
  float t = (-b - std::sqrt(discriminant)) / (2.0f * a);
  if (t < tMin || t > tMax) {
    t = (-b + std::sqrt(discriminant)) / (2.0f * a);
    if (t < tMin || t > tMax) return false;
  }

  hit.t = t;
  hit.position = ray.at(t);
  hit.normal = (hit.position - sphere.center) / sphere.radius;
  hit.material = sphere.material;
  return true;
}

2.2.2 光线与三角形求交(Möller-Trumbore 算法)

Möller-Trumbore 算法是光线与三角形求交的最高效方法之一,它直接在参数空间中求解,避免了先求平面再判断点在三角形内的两步过程。

光线与三角形求交:
P(t) = O + tD (光线)
P(u,v) = (1-u-v)V₀ + uV₁ + vV₂ (三角形参数化)

令 P(t) = P(u,v):
O + tD = (1-u-v)V₀ + uV₁ + vV₂
O + tD = V₀ + u(V₁-V₀) + v(V₂-V₀)

整理为矩阵方程:
┌      ┐ ┌   ┐       ┌          ┐
│ -D   │ │ t │       │          │
│ E1   │ │ u │ = O - V0
│ E2   │ │ v │       │          │
└      ┘ └   ┘       └          ┘

其中 E1 = V₁-V₀, E2 = V₂-V₀

用 Cramer 法则求解
cpp
// Möller-Trumbore 算法
bool hitTriangle(const Ray& ray, float tMin, float tMax,
                 const Triangle& tri, HitRecord& hit) {
  const float EPSILON = 1e-7f;
  glm::vec3 edge1 = tri.v1 - tri.v0;
  glm::vec3 edge2 = tri.v2 - tri.v0;

  glm::vec3 h = glm::cross(ray.direction, edge2);
  float a = glm::dot(edge1, h);

  if (a > -EPSILON && a < EPSILON) return false; // 平行

  float f = 1.0f / a;
  glm::vec3 s = ray.origin - tri.v0;
  float u = f * glm::dot(s, h);

  if (u < 0.0f || u > 1.0f) return false;

  glm::vec3 q = glm::cross(s, edge1);
  float v = f * glm::dot(ray.direction, q);

  if (v < 0.0f || u + v > 1.0f) return false;

  float t = f * glm::dot(edge2, q);
  if (t < tMin || t > tMax) return false;

  hit.t = t;
  hit.position = ray.at(t);
  hit.normal = glm::normalize(glm::cross(edge1, edge2));
  hit.material = tri.material;
  return true;
}

2.2.3 加速结构:BVH

对复杂场景,逐物体求交的时间复杂度为 O(n),不可接受。BVH(Bounding Volume Hierarchy)通过层次化的包围体将求交复杂度降低到 O(log n)。

cpp
// BVH 节点
struct BVHNode {
  BoundingBox bounds;
  BVHNode* left;
  BVHNode* right;
  int primitiveStart;
  int primitiveCount;
  bool isLeaf;
};

// BVH 求交
bool hitBVH(const Ray& ray, float tMin, float tMax,
            const BVHNode* node, HitRecord& hit) {
  if (!node->bounds.hitByRay(ray, tMin, tMax)) {
    return false;
  }

  if (node->isLeaf) {
    bool hitAnything = false;
    for (int i = node->primitiveStart;
         i < node->primitiveStart + node->primitiveCount; i++) {
      if (primitives[i]->hit(ray, tMin, tMax, hit)) {
        tMax = hit.t; // 缩小搜索范围
        hitAnything = true;
      }
    }
    return hitAnything;
  }

  bool hitLeft = hitBVH(ray, tMin, tMax, node->left, hit);
  bool hitRight = hitBVH(ray, tMin, hitLeft ? hit.t : tMax,
                         node->right, hit);
  return hitLeft || hitRight;
}
BVH 结构示意:

           ┌──────────────┐
           │   Root Node   │
           │  ┌──────────┐ │
           │  │  AABB     │ │
           │  └──────────┘ │
           └──────┬───────┘
                  │
        ┌─────────┴─────────┐
        │                   │
   ┌────┴────┐        ┌────┴────┐
   │  Left   │        │  Right  │
   │ ┌─────┐ │        │ ┌─────┐ │
   │ │ AABB │ │        │ │ AABB │ │
   │ └─────┘ │        │ └─────┘ │
   └────┬────┘        └────┬────┘
        │                   │
   ┌────┴────┐        ┌────┴────┐
   │  │      │        │  │      │
  T1  T2    T3       T4  T5    T6

求交过程:
1. 测试根节点的 AABB → 命中
2. 测试左子节点 AABB → 未命中 → 跳过整个子树
3. 测试右子节点 AABB → 命中
4. 继续递归直到叶节点,测试实际图元

2.3 Whitted 光线追踪

2.3.1 递归光线追踪

Whitted 光线追踪(1979)是经典的光线追踪算法。在找到光线与物体的交点后,根据物体的材质属性,生成新的光线继续追踪:

  • 阴影射线:从交点向光源发射光线,检测是否被遮挡
  • 反射光线:如果表面是镜面的,沿反射方向继续追踪
  • 折射光线:如果表面是透明的,沿折射方向继续追踪
cpp
// Whitted 光线追踪核心
glm::vec3 traceRay(const Ray& ray, const Scene& scene, int depth) {
  if (depth <= 0) return glm::vec3(0.0f);

  HitRecord hit;
  if (!scene.hit(ray, 0.001f, MAX_FLOAT, hit)) {
    return scene.backgroundColor(ray.direction);
  }

  // 计算本地光照
  glm::vec3 color = hit.material->shade(hit.position, hit.normal,
                                         ray.direction, scene);

  // 反射光线
  if (hit.material->reflective > 0) {
    glm::vec3 reflectDir = glm::reflect(ray.direction, hit.normal);
    Ray reflectRay(hit.position + hit.normal * 0.001f, reflectDir);
    glm::vec3 reflectColor = traceRay(reflectRay, scene, depth - 1);
    color = glm::mix(color, reflectColor, hit.material->reflective);
  }

  // 折射光线
  if (hit.material->transparent > 0) {
    glm::vec3 refractDir = glm::refract(
      glm::normalize(ray.direction), hit.normal,
      1.0f / hit.material->ior  // 折射率比值
    );
    if (glm::length(refractDir) > 0.001f) {
      Ray refractRay(hit.position - hit.normal * 0.001f, refractDir);
      glm::vec3 refractColor = traceRay(refractRay, scene, depth - 1);
      color = glm::mix(color, refractColor, hit.material->transparent);
    }
  }

  return color;
}

2.3.2 阴影射线

cpp
// 阴影检测
bool isInShadow(const glm::vec3& point, const glm::vec3& lightPos,
                const Scene& scene) {
  glm::vec3 lightDir = glm::normalize(lightPos - point);
  float distance = glm::length(lightPos - point);
  Ray shadowRay(point + lightDir * 0.001f, lightDir);

  HitRecord hit;
  // 如果在到达光源之前碰到任何物体,则在阴影中
  return scene.hit(shadowRay, 0.001f, distance - 0.001f, hit);
}

2.3.3 菲涅尔效应与 Schlick 近似

反射和折射的比例不是固定的,而是取决于观察角度。掠射角度下反射率趋近 100%,这就是菲涅尔效应。

cpp
// Schlick 菲涅尔近似
float schlickFresnel(float cosTheta, float ior) {
  float r0 = (1.0f - ior) / (1.0f + ior);
  r0 = r0 * r0;
  return r0 + (1.0f - r0) * std::pow(1.0f - cosTheta, 5.0f);
}

// 在折射光线追踪中使用菲涅尔
float cosTheta = -glm::dot(ray.direction, hit.normal);
float reflectance = schlickFresnel(cosTheta, hit.material.ior);

// 按菲涅尔比例混合反射和折射
glm::vec3 reflectColor = traceRay(reflectRay, scene, depth - 1);
glm::vec3 refractColor = traceRay(refractRay, scene, depth - 1);
color = reflectance * reflectColor + (1.0f - reflectance) * refractColor;

2.4 路径追踪

2.4.1 从 Whitted 到路径追踪

Whitted 光线追踪只追踪了镜面反射和折射方向,忽略了漫反射表面的间接光照。路径追踪通过在半球方向上随机采样,完整地模拟了光在场景中的多次反弹。

Whitted 光线追踪:
  交点 → 只追踪镜面反射/折射方向 → 缺少间接光照

路径追踪:
  交点 → 在半球上随机采样方向 → 包含所有光照路径
  多次采样取平均 → 收敛到正确结果

2.4.2 蒙特卡洛积分

路径追踪基于蒙特卡洛积分:用随机采样来估计积分值。

渲染方程:
Lₒ(x, ωₒ) = Lₑ(x, ωₒ) + ∫Ω fᵣ(x, ωᵢ, ωₒ) Lᵢ(x, ωᵢ) (ωᵢ·n) dωᵢ

蒙特卡洛估计:
Lₒ ≈ (1/N) Σⱼ₌₁ᴺ fᵣ(x, ωⱼ, ωₒ) Lᵢ(x, ωⱼ) (ωⱼ·n) / p(ωⱼ)

N: 采样数
ωⱼ: 随机采样方向
p(ωⱼ): 采样的概率密度函数
cpp
// 简单路径追踪器
glm::vec3 pathTrace(const Ray& ray, const Scene& scene, int depth) {
  if (depth <= 0) return glm::vec3(0.0f);

  HitRecord hit;
  if (!scene.hit(ray, 0.001f, MAX_FLOAT, hit)) {
    return scene.backgroundColor(ray.direction);
  }

  // 如果是发光体,直接返回发光颜色
  if (hit.material->emissive) {
    return hit.material->emission;
  }

  // 随机采样半球方向
  glm::vec3 randomDir = sampleHemisphere(hit.normal);
  Ray nextRay(hit.position + hit.normal * 0.001f, randomDir);

  // 递归追踪
  glm::vec3 incoming = pathTrace(nextRay, scene, depth - 1);

  // BRDF × 入射光 × cos(θ) / PDF
  float cosTheta = glm::max(glm::dot(hit.normal, randomDir), 0.0f);
  float pdf = 1.0f / (2.0f * M_PI); // 均匀半球采样的 PDF
  glm::vec3 brdf = hit.material->albedo / M_PI; // Lambertian BRDF

  return brdf * incoming * cosTheta / pdf;
}

// 半球均匀采样
glm::vec3 sampleHemisphere(const glm::vec3& normal) {
  // 使用随机数生成半球方向
  float u1 = randomFloat();
  float u2 = randomFloat();
  float r = std::sqrt(1.0f - u1 * u1);
  float phi = 2.0f * M_PI * u2;

  // 在标准半球上采样
  glm::vec3 dir(r * std::cos(phi), std::sqrt(u1), r * std::sin(phi));

  // 变换到法线方向
  glm::vec3 up = std::abs(normal.y) < 0.999f ?
    glm::vec3(0, 1, 0) : glm::vec3(1, 0, 0);
  glm::vec3 tangent = glm::normalize(glm::cross(up, normal));
  glm::vec3 bitangent = glm::cross(normal, tangent);
  return tangent * dir.x + normal * dir.y + bitangent * dir.z;
}

2.4.3 重要性采样

均匀采样的收敛速度很慢。重要性采样通过让采样分布更接近被积函数来加速收敛。

cpp
// 余弦加权重要性采样(比均匀采样收敛更快)
glm::vec3 cosineWeightedSample(const glm::vec3& normal) {
  float u1 = randomFloat();
  float u2 = randomFloat();
  float r = std::sqrt(u1);
  float theta = 2.0f * M_PI * u2;

  float x = r * std::cos(theta);
  float y = std::sqrt(1.0f - u1);
  float z = r * std::sin(theta);

  glm::vec3 dir = localToWorld(glm::vec3(x, y, z), normal);
  return dir;
}

// 余弦加权采样的 PDF = cos(θ) / π
// 与 Lambertian BRDF 的 cos(θ)/π 匹配
// 因此 brdf * cosTheta / pdf = albedo / π * cosTheta / (cosTheta / π) = albedo
// 方差大幅降低!

2.5 实时光线追踪

2.5.1 混合渲染架构

现代实时渲染通常采用光栅化 + 光线追踪的混合方案:

  • 主渲染:使用光栅化渲染不透明物体(高效)
  • 光线追踪效果:用光线追踪实现特定效果

- 阴影(精确的硬阴影和软阴影)

- 反射(屏幕空间反射的补充)

- 环境光遮蔽(精确的 AO)

- 全局光照(间接光照)

混合渲染管线:

┌─────────────────────────────────────────────────┐
│ 光栅化阶段                                        │
│ ├── G-Buffer 渲染(位置、法线、Albedo、Metallic、Roughness)│
│ └── 直接光照                                       │
└───────────────────────┬─────────────────────────┘
                        │
┌───────────────────────┴─────────────────────────┐
│ 光线追踪阶段                                      │
│ ├── RT 阴影(从交点向光源追踪)                      │
│ ├── RT 反射(从相机追踪反射光线)                     │
│ ├── RT AO(从交点向半球追踪)                        │
│ └── RT GI(从交点追踪间接光照)                      │
└───────────────────────┬─────────────────────────┘
                        │
┌───────────────────────┴─────────────────────────┐
│ 合成阶段                                          │
│ ├── 合并光栅化和光线追踪结果                          │
│ ├── 降噪(Denoiser)                               │
│ └── 后处理(色调映射、Gamma 校正)                     │
└─────────────────────────────────────────────────┘

2.5.2 NVIDIA RTX 架构

NVIDIA RTX GPU 包含三种处理核心:

  • CUDA 核心:通用计算和着色
  • RT 核心:专用的光线-包围体求交加速
  • Tensor 核心:AI 降噪(DLSS)
cpp
// Vulkan 光线追踪着色器示例(简化)

// 光线生成(Ray Generation Shader)
#version 460
#extension GL_EXT_ray_tracing : require

layout(set = 0, binding = 0) uniform accelerationStructureEXT topLevelAS;
layout(set = 0, binding = 1, rgba8) uniform writeonly image2D outputImage;

void main() {
  ivec2 pixel = ivec2(gl_LaunchIDEXT.xy);
  vec2 uv = (vec2(pixel) + 0.5) / vec2(gl_LaunchSizeEXT.xy);

  vec3 origin = cameraPos;
  vec3 direction = getRayDirection(uv);

  // 发射光线
  gl_RayFlagsEXT = gl_RayFlagOpaqueEXT;
  traceRayEXT(topLevelAS, gl_RayFlagsEXT, 0xFF, 0, 1, 0,
              origin, 0.001, direction, 1000.0, 0);

  // 读取命中结果
  vec3 color = payloadColor;
  imageStore(outputImage, pixel, vec4(color, 1.0));
}

// 最近命中着色器(Closest Hit Shader)
#version 460
#extension GL_EXT_ray_tracing : require

layout(location = 0) rayPayloadInEXT vec3 payloadColor;
hitAttributeEXT vec2 attribs;

void main() {
  // 计算重心坐标
  vec3 barycentrics = vec3(1.0 - attribs.x - attribs.y,
                           attribs.x, attribs.y);

  // 插值法线和纹理坐标
  vec3 normal = interpolateAttribute(barycentrics, normals);
  vec2 texCoord = interpolateAttribute(barycentrics, texCoords);

  // 计算颜色
  payloadColor = computeLighting(normal, texCoord);
}

三、实战案例

3.1 CPU 光线追踪器

以下实现一个完整的 CPU 光线追踪器,渲染包含球体的场景:

cpp
#include <glm/glm.hpp>
#include <vector>
#include <cmath>
#include <random>

struct Sphere {
  glm::vec3 center;
  float radius;
  glm::vec3 albedo;
  float metallic;
  float roughness;
};

struct Scene {
  std::vector<Sphere> spheres;
  glm::vec3 lightPos;
  glm::vec3 lightColor;
};

// 随机数生成
std::mt19937 rng(42);
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
float randomFloat() { return dist(rng); }

// 光线追踪渲染
glm::vec3 renderPixel(int x, int y, int width, int height,
                      const Scene& scene, glm::vec3 cameraPos) {
  float u = (x + 0.5f) / width;
  float v = (y + 0.5f) / height;

  // 简单的针孔相机
  float fov = 45.0f;
  float scale = std::tan(glm::radians(fov) * 0.5f);
  float aspect = (float)width / height;

  glm::vec3 dir = glm::normalize(glm::vec3(
    (2.0f * u - 1.0f) * scale * aspect,
    (1.0f - 2.0f * v) * scale,
    -1.0f
  ));

  Ray ray(cameraPos, dir);
  return traceRay(ray, scene, 5);
}

glm::vec3 traceRay(const Ray& ray, const Scene& scene, int depth) {
  if (depth <= 0) return glm::vec3(0);

  float closestT = 1e10f;
  int hitSphere = -1;

  // 找最近交点
  for (int i = 0; i < scene.spheres.size(); i++) {
    float t;
    if (hitSphereTest(ray, scene.spheres[i], t) && t < closestT) {
      closestT = t;
      hitSphere = i;
    }
  }

  if (hitSphere < 0) {
    // 天空颜色
    glm::vec3 unitDir = glm::normalize(ray.direction);
    float t = 0.5f * (unitDir.y + 1.0f);
    return (1.0f - t) * glm::vec3(1) + t * glm::vec3(0.5f, 0.7f, 1.0f);
  }

  const Sphere& sphere = scene.spheres[hitSphere];
  glm::vec3 hitPos = ray.at(closestT);
  glm::vec3 normal = glm::normalize(hitPos - sphere.center);

  // 阴影检测
  glm::vec3 lightDir = glm::normalize(scene.lightPos - hitPos);
  Ray shadowRay(hitPos + normal * 0.001f, lightDir);
  bool inShadow = false;
  for (const auto& s : scene.spheres) {
    float t;
    if (hitSphereTest(shadowRay, s, t)) {
      inShadow = true;
      break;
    }
  }

  // 光照计算
  glm::vec3 color(0);
  if (!inShadow) {
    float diff = std::max(glm::dot(normal, lightDir), 0.0f);
    color = sphere.albedo * diff * scene.lightColor;

    // 镜面反射
    glm::vec3 viewDir = glm::normalize(-ray.direction);
    glm::vec3 halfDir = glm::normalize(lightDir + viewDir);
    float spec = std::pow(std::max(glm::dot(normal, halfDir), 0.0f), 32.0f);
    color += spec * scene.lightColor * 0.5f;
  }

  // 环境光
  color += sphere.albedo * 0.05f;

  // 递归反射
  if (sphere.metallic > 0 && depth > 1) {
    glm::vec3 reflectDir = glm::reflect(ray.direction, normal);
    Ray reflectRay(hitPos + normal * 0.001f, reflectDir);
    glm::vec3 reflectColor = traceRay(reflectRay, scene, depth - 1);
    color = glm::mix(color, reflectColor, sphere.metallic);
  }

  return color;
}

bool hitSphereTest(const Ray& ray, const Sphere& sphere, float& t) {
  glm::vec3 oc = ray.origin - sphere.center;
  float a = glm::dot(ray.direction, ray.direction);
  float b = 2.0f * glm::dot(oc, ray.direction);
  float c = glm::dot(oc, oc) - sphere.radius * sphere.radius;
  float disc = b * b - 4 * a * c;
  if (disc < 0) return false;
  t = (-b - std::sqrt(disc)) / (2.0f * a);
  return t > 0.001f;
}

3.2 路径追踪收敛演示

cpp
// 路径追踪:逐步增加采样数观察收敛过程
void progressivePathTrace(Image& image, const Scene& scene,
                          int maxSamples) {
  Image accumulated(image.width, image.height, glm::vec3(0));

  for (int sample = 0; sample < maxSamples; sample++) {
    for (int y = 0; y < image.height; y++) {
      for (int x = 0; x < image.width; x++) {
        // 加入子像素抖动(抗锯齿)
        float jitterX = randomFloat();
        float jitterY = randomFloat();
        float u = (x + jitterX) / image.width;
        float v = (y + jitterY) / image.height;

        Ray ray = camera.generateRay(u, v);
        glm::vec3 color = pathTrace(ray, scene, 8);

        // 累加
        accumulated.addPixel(x, y, color);

        // 计算平均值
        glm::vec3 avg = accumulated.getPixel(x, y) / (sample + 1.0f);
        image.setPixel(x, y, avg);
      }
    }
    // 每 10 个采样保存一帧,观察收敛过程
    if (sample % 10 == 0) {
      saveImage(image, "progressive_" + std::to_string(sample) + ".ppm");
    }
  }
}

四、本章小结

本章系统讲解了光线追踪从基础到前沿的完整知识:

光线追踪通过反向追踪光路,从相机发射光线追踪场景中的传播路径,自然地产生反射、折射、阴影等效果。

光线-物体求交是光线追踪的核心计算。Möller-Trumbore 算法高效地解决了光线与三角形的求交问题。

BVH 加速结构通过层次化的包围体将求交复杂度从 O(n) 降低到 O(log n),是处理复杂场景的关键。

Whitted 光线追踪通过递归追踪反射和折射光线,实现了镜面反射和透明效果,但缺少漫反射间接光照。

路径追踪基于蒙特卡洛积分,通过在半球上随机采样方向,完整地模拟了全局光照。收敛速度慢但结果物理正确。

实时光线追踪采用光栅化 + 光线追踪的混合架构,利用专用硬件加速,在实时应用中实现高质量的光照效果。


五、思考与练习

思考题

为什么路径追踪的图像有噪声?噪声的方差与采样数有什么关系?如何将采样数减半使噪声减半?

BVH 的构建策略对性能有什么影响?SAH(Surface Area Heuristic)是如何优化 BVH 构建的?

俄罗斯轮盘赌(Russian Roulette)是如何在路径追踪中无偏地终止光线的?为什么它比固定深度截断更好?

焦散效果(如玻璃球在地面上产生的光斑)为什么很难用路径追踪渲染?双向路径追踪如何解决这个问题?

实时光线追踪中的降噪器(Denoiser)是如何工作的?为什么少量的光线采样配合降噪器就能产生干净的图像?

编程练习

基础光线追踪器:实现一个 CPU 光线追踪器,渲染包含多个球体的场景,支持阴影、反射和折射。

路径追踪器:实现一个路径追踪器,使用余弦加权重要性采样。逐步增加采样数,观察图像从噪声到清晰的收敛过程。

BVH 加速:实现 BVH 加速结构,对比有 BVH 和无 BVH 时的渲染速度差异。测试不同场景复杂度下的性能表现。


7/8光线追踪
0%