第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 的路径相同// 光线的基本定义
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 相机模型
// 针孔相机模型
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)// 光线与球体求交
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 法则求解// 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)。
// 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)是经典的光线追踪算法。在找到光线与物体的交点后,根据物体的材质属性,生成新的光线继续追踪:
- 阴影射线:从交点向光源发射光线,检测是否被遮挡
- 反射光线:如果表面是镜面的,沿反射方向继续追踪
- 折射光线:如果表面是透明的,沿折射方向继续追踪
// 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 阴影射线
// 阴影检测
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%,这就是菲涅尔效应。
// 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(ωⱼ): 采样的概率密度函数// 简单路径追踪器
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 重要性采样
均匀采样的收敛速度很慢。重要性采样通过让采样分布更接近被积函数来加速收敛。
// 余弦加权重要性采样(比均匀采样收敛更快)
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)
// 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 光线追踪器,渲染包含球体的场景:
#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 路径追踪收敛演示
// 路径追踪:逐步增加采样数观察收敛过程
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 时的渲染速度差异。测试不同场景复杂度下的性能表现。