8915 字
45 分钟
计算机图形学笔记(四):光线追踪与全局光照

计算机图形学笔记(四):光线追踪与全局光照#

光线追踪绝对是图形学里最有意思的部分了!第一次看到自己写的代码渲染出带反射、折射的真实画面时,那种成就感真的无法言喻。虽然算法理解起来不难,但要写出高效的实现还是有很多细节需要注意的。

目录#

  1. 光线追踪基础理论 - 渲染方程与Whitted光线追踪
  2. 光线-几何体相交算法 - 数学推导与数值稳定性
  3. 空间加速数据结构 - BVH与KD-Tree构建算法
  4. 蒙特卡洛方法与积分 - 重要性采样与方差减少
  5. 全局光照与路径追踪 - 路径追踪算法实现

光线追踪基础理论#

16.1 光线追踪的物理与数学基础#

16.1.1 几何光学的数学框架#

几何光学的基本假设#

1. 直线传播假设: 在均匀介质中,光沿直线传播。这是光线追踪算法的核心假设。

2. 独立性假设: 不同光线之间不发生相互作用,可以独立计算每条光线的贡献。

3. 几何尺度假设: 光的波长 λ\lambda 远小于场景中物体的特征尺度 LL,即 λL\lambda \ll L

光线的参数化表示#

数学定义: 三维空间中的光线可以用参数方程表示: r(t)=o+td\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}

其中:

  • oR3\mathbf{o} \in \mathbb{R}^3:光线起点(origin)
  • dR3\mathbf{d} \in \mathbb{R}^3:光线方向向量(direction),通常为单位向量
  • t[tmin,tmax]t \in [t_{min}, t_{max}]:参数范围

工程实现

struct Ray {
Vector3f origin; // 起点 o
Vector3f direction; // 方向 d(单位向量)
float t_min = 1e-4f; // 最小参数(避免自相交)
float t_max = 1e30f; // 最大参数
// 计算光线上的点
Vector3f at(float t) const {
return origin + t * direction;
}
// 检查参数是否在有效范围内
bool is_valid_t(float t) const {
return t >= t_min && t <= t_max;
}
};

16.1.2 渲染方程的递归形式#

渲染方程的数学推导#

积分形式的渲染方程Lo(p,ωo)=Le(p,ωo)+Ωfr(p,ωi,ωo)Li(p,ωi)cosθidωiL_o(\mathbf{p}, \omega_o) = L_e(\mathbf{p}, \omega_o) + \int_{\Omega} f_r(\mathbf{p}, \omega_i, \omega_o) L_i(\mathbf{p}, \omega_i) \cos\theta_i \, d\omega_i

递归形式的推导: 入射辐射度 Li(p,ωi)L_i(\mathbf{p}, \omega_i) 实际上是从方向 ωi\omega_i 射向点 p\mathbf{p} 的光线在其起始点的出射辐射度。

设光线与场景的下一个交点为 p\mathbf{p}',则: Li(p,ωi)=Lo(p,ωi)L_i(\mathbf{p}, \omega_i) = L_o(\mathbf{p}', -\omega_i)

代入渲染方程得到递归形式: Lo(p,ωo)=Le(p,ωo)+Ωfr(p,ωi,ωo)Lo(p,ωi)cosθidωiL_o(\mathbf{p}, \omega_o) = L_e(\mathbf{p}, \omega_o) + \int_{\Omega} f_r(\mathbf{p}, \omega_i, \omega_o) L_o(\mathbf{p}', -\omega_i) \cos\theta_i \, d\omega_i

光线追踪的数学本质#

积分方程的求解: 渲染方程是一个Fredholm积分方程,光线追踪通过递归求解这个积分方程。

Neumann级数展开: 渲染方程可以展开为无穷级数: L=Le+TLe+T2Le+T3Le+L = L_e + TL_e + T^2L_e + T^3L_e + \cdots

其中 TT 是传输算子: [TL](p,ωo)=Ωfr(p,ωi,ωo)L(p,ωi)cosθidωi[TL](\mathbf{p}, \omega_o) = \int_{\Omega} f_r(\mathbf{p}, \omega_i, \omega_o) L(\mathbf{p}', -\omega_i) \cos\theta_i \, d\omega_i

物理解释

  • LeL_e:直接光照(0次反射)
  • TLeTL_e:一次反射光照
  • T2LeT^2L_e:二次反射光照
  • \vdots

光线追踪的收敛性#

收敛条件: 当传输算子 TT 的谱半径小于1时,Neumann级数收敛: ρ(T)<1\rho(T) < 1

这在物理上对应于能量守恒条件:反射的总能量小于入射能量。

16.2 Whitted光线追踪算法#

16.2.1 Whitted算法的数学模型#

算法的理论基础#

Whitted模型的简化假设

  1. 完美镜面反射:只考虑镜面方向的反射
  2. 完美透射:只考虑折射方向的透射
  3. 点光源:光源被建模为点光源
  4. 递归终止:通过深度限制终止递归

反射定律的数学推导#

反射向量计算: 给定入射向量 d\mathbf{d} 和表面法向量 n\mathbf{n},反射向量 r\mathbf{r} 为: r=d2(dn)n\mathbf{r} = \mathbf{d} - 2(\mathbf{d} \cdot \mathbf{n})\mathbf{n}

推导过程: 设入射向量为 d\mathbf{d},分解为法向分量和切向分量:

  • 法向分量:d=(dn)n\mathbf{d}_{\parallel} = (\mathbf{d} \cdot \mathbf{n})\mathbf{n}
  • 切向分量:d=dd\mathbf{d}_{\perp} = \mathbf{d} - \mathbf{d}_{\parallel}

反射时切向分量不变,法向分量反向: r=dd=d2d=d2(dn)n\mathbf{r} = \mathbf{d}_{\perp} - \mathbf{d}_{\parallel} = \mathbf{d} - 2\mathbf{d}_{\parallel} = \mathbf{d} - 2(\mathbf{d} \cdot \mathbf{n})\mathbf{n}

Snell定律与折射#

Snell定律n1sinθ1=n2sinθ2n_1 \sin\theta_1 = n_2 \sin\theta_2

其中 n1,n2n_1, n_2 是两种介质的折射率,θ1,θ2\theta_1, \theta_2 是入射角和折射角。

折射向量的向量形式t=n1n2d+(n1n2cosθ1cosθ2)n\mathbf{t} = \frac{n_1}{n_2}\mathbf{d} + \left(\frac{n_1}{n_2}\cos\theta_1 - \cos\theta_2\right)\mathbf{n}

其中: cosθ1=dn\cos\theta_1 = -\mathbf{d} \cdot \mathbf{n} cosθ2=1(n1n2)2(1cos2θ1)\cos\theta_2 = \sqrt{1 - \left(\frac{n_1}{n_2}\right)^2(1 - \cos^2\theta_1)}

全内反射条件: 当 (n1n2)2(1cos2θ1)>1\left(\frac{n_1}{n_2}\right)^2(1 - \cos^2\theta_1) > 1 时发生全内反射。

Whitted算法的完整实现#

Vector3f whitted_ray_tracing(const Ray& ray, const Scene& scene, int depth) {
// 递归终止条件
if (depth <= 0) return Vector3f::Zero();
// 光线与场景求交
Intersection hit = scene.intersect(ray);
if (!hit.happened) return scene.background_color;
// 初始化颜色为自发光
Vector3f color = hit.material->emission;
// 直接光照计算
for (const auto& light : scene.lights) {
Vector3f light_dir = light->sample_direction(hit.position);
// 阴影测试
Ray shadow_ray(hit.position + EPSILON * hit.normal, light_dir);
if (!scene.is_occluded(shadow_ray, light->distance(hit.position))) {
// 计算直接光照贡献
Vector3f light_color = light->intensity / light->distance_squared(hit.position);
Vector3f brdf_value = hit.material->evaluate_brdf(-ray.direction, light_dir, hit.normal);
float cos_theta = std::max(0.0f, hit.normal.dot(light_dir));
color += brdf_value * light_color * cos_theta;
}
}
// 镜面反射
if (hit.material->is_reflective() && depth > 0) {
Vector3f reflect_dir = reflect(ray.direction, hit.normal);
Ray reflect_ray(hit.position + EPSILON * hit.normal, reflect_dir);
Vector3f reflected_color = whitted_ray_tracing(reflect_ray, scene, depth - 1);
color += hit.material->reflectance * reflected_color;
}
// 折射/透射
if (hit.material->is_transparent() && depth > 0) {
float eta = hit.front_face ? (1.0f / hit.material->ior) : hit.material->ior;
Vector3f refract_dir = refract(ray.direction, hit.normal, eta);
if (refract_dir.squaredNorm() > 0) { // 检查是否发生全内反射
Vector3f offset = hit.front_face ? (-EPSILON * hit.normal) : (EPSILON * hit.normal);
Ray refract_ray(hit.position + offset, refract_dir);
Vector3f refracted_color = whitted_ray_tracing(refract_ray, scene, depth - 1);
color += hit.material->transmittance * refracted_color;
}
}
return color;
}
// 反射向量计算
Vector3f reflect(const Vector3f& incident, const Vector3f& normal) {
return incident - 2.0f * incident.dot(normal) * normal;
}
// 折射向量计算
Vector3f refract(const Vector3f& incident, const Vector3f& normal, float eta) {
float cos_i = -incident.dot(normal);
float sin_t2 = eta * eta * (1.0f - cos_i * cos_i);
if (sin_t2 >= 1.0f) {
return Vector3f::Zero(); // 全内反射
}
float cos_t = std::sqrt(1.0f - sin_t2);
return eta * incident + (eta * cos_i - cos_t) * normal;
}
### 16.2.2 项目中的实现框架
**基于GAMES101 Assignment5-6的结构**:
```cpp
Vector3f Scene::castRay(const Ray &ray, int depth) const {
if (depth > this->maxDepth) {
return Vector3f(0.0, 0.0, 0.0);
}
Intersection intersection = Scene::intersect(ray);
if (!intersection.happened) {
return this->backgroundColor;
}
Material *m = intersection.m;
Object *hitObject = intersection.obj;
Vector3f hitColor = this->backgroundColor;
Vector3f hitPoint = intersection.coords;
Vector3f N = intersection.normal;
Vector2f uv = intersection.uv;
switch (m->getType()) {
case REFLECTION_AND_REFRACTION: {
Vector3f reflectionDirection = reflect(ray.direction, N);
Vector3f refractionDirection = refract(ray.direction, N, m->ior);
Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
hitPoint - N * EPSILON : hitPoint + N * EPSILON;
Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
hitPoint - N * EPSILON : hitPoint + N * EPSILON;
Vector3f reflectionColor = castRay(Ray(reflectionRayOrig, reflectionDirection), depth + 1);
Vector3f refractionColor = castRay(Ray(refractionRayOrig, refractionDirection), depth + 1);
float kr = fresnel(ray.direction, N, m->ior);
hitColor = reflectionColor * kr + refractionColor * (1 - kr);
break;
}
case REFLECTION: {
float kr = fresnel(ray.direction, N, m->ior);
Vector3f reflectionDirection = reflect(ray.direction, N);
Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
hitPoint - N * EPSILON : hitPoint + N * EPSILON;
hitColor = castRay(Ray(reflectionRayOrig, reflectionDirection), depth + 1) * kr;
break;
}
default: {
// 漫反射材质的直接光照计算
Vector3f lightAmt = 0, specularColor = 0;
Vector3f shadowPointOrig = (dotProduct(ray.direction, N) < 0) ?
hitPoint + N * EPSILON : hitPoint - N * EPSILON;
for (auto& light : this->get_lights()) {
Vector3f lightDir = light->position - hitPoint;
float lightDistance2 = dotProduct(lightDir, lightDir);
lightDir = normalize(lightDir);
float LdotN = std::max(0.f, dotProduct(lightDir, N));
// 阴影测试
auto shadow_res = trace(Ray(shadowPointOrig, lightDir), this->get_objects());
bool inShadow = shadow_res && (shadow_res->t * shadow_res->t < lightDistance2);
lightAmt += inShadow ? 0 : light->intensity * LdotN;
Vector3f reflectionDirection = reflect(-lightDir, N);
specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, ray.direction)),
m->specularExponent) * light->intensity;
}
hitColor = lightAmt * m->Kd * m->diffuseColor + specularColor * m->Ks;
break;
}
}
return hitColor;
}

16.3 光线-物体相交算法#

16.3.1 光线-球体相交#

数学推导: 球体方程:pc2=r2||p - c||^2 = r^2 光线方程:p=o+tdp = o + td

代入得到:o+tdc2=r2||o + td - c||^2 = r^2

展开:(oc+td)(oc+td)=r2(o - c + td) \cdot (o - c + td) = r^2

整理得到二次方程:at2+bt+c=0at^2 + bt + c = 0

其中:

  • a=dd=1a = d \cdot d = 1(假设d是单位向量)
  • b=2d(oc)b = 2d \cdot (o - c)
  • c=(oc)(oc)r2c = (o - c) \cdot (o - c) - r^2

代码实现

bool intersect_sphere(const Ray& ray, const Vector3f& center, float radius,
float& t_near, float& t_far) {
Vector3f oc = ray.origin - center;
float a = ray.direction.dot(ray.direction);
float b = 2.0f * oc.dot(ray.direction);
float c = oc.dot(oc) - radius * radius;
float discriminant = b * b - 4 * a * c;
if (discriminant < 0) return false;
float sqrt_discriminant = std::sqrt(discriminant);
t_near = (-b - sqrt_discriminant) / (2.0f * a);
t_far = (-b + sqrt_discriminant) / (2.0f * a);
if (t_near > t_far) std::swap(t_near, t_far);
return t_near >= ray.t_min && t_near <= ray.t_max;
}

16.3.2 光线-三角形相交#

Möller-Trumbore算法

bool intersect_triangle(const Ray& ray, const Vector3f& v0, const Vector3f& v1, const Vector3f& v2,
float& t, float& u, float& v) {
Vector3f edge1 = v1 - v0;
Vector3f edge2 = v2 - v0;
Vector3f h = ray.direction.cross(edge2);
float a = edge1.dot(h);
if (a > -EPSILON && a < EPSILON) return false; // 光线平行于三角形
float f = 1.0f / a;
Vector3f s = ray.origin - v0;
u = f * s.dot(h);
if (u < 0.0f || u > 1.0f) return false;
Vector3f q = s.cross(edge1);
v = f * ray.direction.dot(q);
if (v < 0.0f || u + v > 1.0f) return false;
t = f * edge2.dot(q);
return t > EPSILON && t >= ray.t_min && t <= ray.t_max;
}

16.3.3 光线-平面相交#

数学推导: 平面方程:n(pp0)=0\mathbf{n} \cdot (\mathbf{p} - \mathbf{p}_0) = 0 光线方程:p=o+td\mathbf{p} = \mathbf{o} + t\mathbf{d}

代入:n(o+tdp0)=0\mathbf{n} \cdot (\mathbf{o} + t\mathbf{d} - \mathbf{p}_0) = 0 解得:t=n(p0o)ndt = \frac{\mathbf{n} \cdot (\mathbf{p}_0 - \mathbf{o})}{\mathbf{n} \cdot \mathbf{d}}

代码实现

bool intersect_plane(const Ray& ray, const Vector3f& point, const Vector3f& normal,
float& t) {
float denom = normal.dot(ray.direction);
if (std::abs(denom) < EPSILON) return false; // 光线平行于平面
t = normal.dot(point - ray.origin) / denom;
return t >= ray.t_min && t <= ray.t_max;
}

光线-几何体相交算法#

17.1 包围盒相交算法#

17.1.1 轴对齐包围盒(AABB)的数学理论#

AABB的数学定义#

轴对齐包围盒: AABB是一个与坐标轴平行的长方体,可以用两个对角顶点定义: AABB={(x,y,z):xminxxmax,yminyymax,zminzzmax}\text{AABB} = \{(x,y,z) : x_{min} \leq x \leq x_{max}, y_{min} \leq y \leq y_{max}, z_{min} \leq z \leq z_{max}\}

光线-AABB相交的数学推导#

光线参数方程r(t)=o+td\mathbf{r}(t) = \mathbf{o} + t\mathbf{d}

平面相交计算: 对于每个坐标轴 ii,光线与两个平面 xi=xminx_i = x_{min}xi=xmaxx_i = x_{max} 的相交参数为: t1i=xminoidi,t2i=xmaxoidit_{1i} = \frac{x_{min} - o_i}{d_i}, \quad t_{2i} = \frac{x_{max} - o_i}{d_i}

近远平面确定tnear,i=min(t1i,t2i),tfar,i=max(t1i,t2i)t_{near,i} = \min(t_{1i}, t_{2i}), \quad t_{far,i} = \max(t_{1i}, t_{2i})

相交条件: 光线与AABB相交当且仅当: tenter=max(tnear,x,tnear,y,tnear,z)texit=min(tfar,x,tfar,y,tfar,z)t_{enter} = \max(t_{near,x}, t_{near,y}, t_{near,z}) \leq t_{exit} = \min(t_{far,x}, t_{far,y}, t_{far,z})

数值稳定性考虑#

除零处理: 当 di=0d_i = 0 时,光线平行于第 ii 轴:

  • 如果 oi<xmino_i < x_{min}oi>xmaxo_i > x_{max},则无相交
  • 否则,tnear,i=t_{near,i} = -\inftytfar,i=+t_{far,i} = +\infty

优化的AABB相交实现#

struct AABB {
Vector3f min_point, max_point;
AABB() : min_point(Vector3f::Constant(INFINITY)),
max_point(Vector3f::Constant(-INFINITY)) {}
AABB(const Vector3f& min_p, const Vector3f& max_p)
: min_point(min_p), max_point(max_p) {}
// 计算表面积(用于SAH)
float surface_area() const {
Vector3f extent = max_point - min_point;
return 2.0f * (extent.x() * extent.y() +
extent.y() * extent.z() +
extent.z() * extent.x());
}
// 计算体积
float volume() const {
Vector3f extent = max_point - min_point;
return extent.x() * extent.y() * extent.z();
}
void expand(const Vector3f& point) {
min_point = min_point.cwiseMin(point);
max_point = max_point.cwiseMax(point);
}
};
bool intersect_aabb_robust(const Ray& ray, const AABB& box, float& t_min, float& t_max) {
t_min = ray.t_min;
t_max = ray.t_max;
for (int i = 0; i < 3; ++i) {
if (std::abs(ray.direction[i]) < 1e-8f) {
// 光线平行于第i轴
if (ray.origin[i] < box.min_point[i] || ray.origin[i] > box.max_point[i]) {
return false;
}
} else {
float inv_dir = 1.0f / ray.direction[i];
float t1 = (box.min_point[i] - ray.origin[i]) * inv_dir;
float t2 = (box.max_point[i] - ray.origin[i]) * inv_dir;
if (t1 > t2) std::swap(t1, t2);
t_min = std::max(t_min, t1);
t_max = std::min(t_max, t2);
if (t_min > t_max) return false;
}
}
return true;
}

17.1.2 有向包围盒(OBB)#

OBB定义

struct OBB {
Vector3f center;
Vector3f axes[3]; // 三个正交轴
Vector3f half_extents; // 沿各轴的半长度
AABB to_aabb() const {
Vector3f extent = Vector3f::Zero();
for (int i = 0; i < 3; ++i) {
extent += axes[i].cwiseAbs() * half_extents[i];
}
return AABB(center - extent, center + extent);
}
};

17.2 复杂几何体相交#

17.2.1 光线-网格相交#

网格相交算法

class TriangleMesh {
private:
std::vector<Vector3f> vertices;
std::vector<Vector3i> triangles;
AABB bounding_box;
public:
bool intersect(const Ray& ray, Intersection& hit) {
if (!intersect_aabb(ray, bounding_box)) return false;
bool hit_found = false;
float closest_t = ray.t_max;
for (const auto& triangle : triangles) {
Vector3f v0 = vertices[triangle[0]];
Vector3f v1 = vertices[triangle[1]];
Vector3f v2 = vertices[triangle[2]];
float t, u, v;
if (intersect_triangle(ray, v0, v1, v2, t, u, v) && t < closest_t) {
closest_t = t;
hit.t = t;
hit.position = ray.at(t);
hit.normal = (v1 - v0).cross(v2 - v0).normalized();
hit.uv = Vector2f(u, v);
hit_found = true;
}
}
return hit_found;
}
};

17.2.2 光线-隐式曲面相交#

隐式曲面定义:f(x, y, z) = 0

牛顿迭代法求交

bool intersect_implicit_surface(const Ray& ray,
std::function<float(Vector3f)> f,
std::function<Vector3f(Vector3f)> gradient,
float& t) {
float t_current = ray.t_min;
const int max_iterations = 100;
const float tolerance = 1e-6f;
for (int i = 0; i < max_iterations; ++i) {
Vector3f point = ray.at(t_current);
float value = f(point);
if (std::abs(value) < tolerance) {
t = t_current;
return true;
}
Vector3f grad = gradient(point);
float denominator = grad.dot(ray.direction);
if (std::abs(denominator) < tolerance) break;
t_current -= value / denominator;
if (t_current < ray.t_min || t_current > ray.t_max) break;
}
return false;
}

空间加速数据结构#

18.1 BVH(层次包围盒)#

18.1.1 BVH的数学理论基础#

层次包围盒的数学原理#

核心思想: BVH(Bounding Volume Hierarchy)通过递归地将几何对象分组并用包围盒包围,构建一个二叉树结构,从而实现对光线-场景相交测试的加速。

数学基础: 设场景中有 nn 个几何对象 {O1,O2,,On}\{O_1, O_2, \ldots, O_n\},BVH将其组织成一个二叉树 TT,满足:

  1. 包围性质:每个内部节点的包围盒包含其所有子节点的包围盒
  2. 分离性质:叶子节点包含的几何对象在空间上相对集中
  3. 平衡性质:树的深度尽可能小,理想情况下为 O(logn)O(\log n)

相交测试的复杂度分析#

朴素方法: 对于每条光线,需要与所有 nn 个对象进行相交测试,时间复杂度为 O(n)O(n)

BVH加速: 利用包围盒的层次结构,平均时间复杂度降低到 O(logn)O(\log n)

  • 如果光线与节点的包围盒不相交,则可以跳过整个子树
  • 只有当光线与包围盒相交时,才需要递归测试子节点

BVH节点定义

struct BVHBuildNode {
AABB bounds;
BVHBuildNode* left;
BVHBuildNode* right;
Object* object; // 叶子节点存储对象
int split_axis; // 分割轴
int first_prim_offset; // 第一个图元的偏移
int n_primitives; // 图元数量
void init_leaf(int first, int n, const AABB& b) {
first_prim_offset = first;
n_primitives = n;
bounds = b;
left = right = nullptr;
}
void init_interior(int axis, BVHBuildNode* c0, BVHBuildNode* c1) {
left = c0;
right = c1;
bounds = AABB();
bounds.expand(c0->bounds);
bounds.expand(c1->bounds);
split_axis = axis;
n_primitives = 0;
}
};

18.1.2 BVH构建算法的数学理论#

分割策略的数学分析#

1. 中点分割(Midpoint Split): 选择最长轴的中点进行分割: xsplit=xmin+xmax2x_{split} = \frac{x_{min} + x_{max}}{2}

优点:简单快速,保证树的平衡性 缺点:可能产生空的子树或不均匀的分布

2. 表面积启发式(SAH - Surface Area Heuristic): 基于期望相交测试次数的最优化分割策略。

SAH的数学推导#

期望相交测试次数: 对于一个包围盒 BB,其子节点 BLB_LBRB_R,光线与该节点相交的期望测试次数为: C(B)=Ctraversal+P(BLB)C(BL)+P(BRB)C(BR)C(B) = C_{traversal} + P(B_L|B) \cdot C(B_L) + P(B_R|B) \cdot C(B_R)

其中:

  • CtraversalC_{traversal} 是遍历节点的固定开销
  • P(BLB)P(B_L|B) 是光线与 BB 相交时也与 BLB_L 相交的条件概率
  • C(BL)C(B_L) 是左子树的期望测试次数

几何概率假设: 假设光线方向均匀分布,则: P(BLB)=SA(BL)SA(B)P(B_L|B) = \frac{SA(B_L)}{SA(B)}

其中 SA(B)SA(B) 是包围盒 BB 的表面积。

SAH代价函数SAH(split)=Ctraversal+SA(BL)SA(B)NLCintersect+SA(BR)SA(B)NRCintersectSAH(split) = C_{traversal} + \frac{SA(B_L)}{SA(B)} \cdot N_L \cdot C_{intersect} + \frac{SA(B_R)}{SA(B)} \cdot N_R \cdot C_{intersect}

其中 NLN_LNRN_R 分别是左右子树的图元数量。

基于GAMES101项目的实现#

BVHBuildNode* BVHAccel::recursiveBuild(std::vector<Object*> objects) {
BVHBuildNode* node = new BVHBuildNode();
// 计算所有对象的包围盒
AABB bounds;
for (int i = 0; i < objects.size(); ++i) {
bounds.expand(objects[i]->getBounds());
}
if (objects.size() == 1) {
// 创建叶子节点
node->bounds = objects[0]->getBounds();
node->object = objects[0];
node->left = nullptr;
node->right = nullptr;
return node;
} else if (objects.size() == 2) {
node->left = recursiveBuild(std::vector{objects[0]});
node->right = recursiveBuild(std::vector{objects[1]});
node->bounds = AABB();
node->bounds.expand(node->left->bounds);
node->bounds.expand(node->right->bounds);
return node;
} else {
// 选择分割轴(最长轴)
AABB centroid_bounds;
for (int i = 0; i < objects.size(); ++i) {
centroid_bounds.expand(objects[i]->getBounds().centroid());
}
int dim = centroid_bounds.max_extent();
switch (splitMethod) {
case SplitMethod::NAIVE: {
// 中点分割
std::sort(objects.begin(), objects.end(), [dim](const auto& a, const auto& b) {
return a->getBounds().centroid()[dim] < b->getBounds().centroid()[dim];
});
auto beginning = objects.begin();
auto middling = objects.begin() + (objects.size() / 2);
auto ending = objects.end();
auto leftshapes = std::vector<Object*>(beginning, middling);
auto rightshapes = std::vector<Object*>(middling, ending);
assert(objects.size() == (leftshapes.size() + rightshapes.size()));
node->left = recursiveBuild(leftshapes);
node->right = recursiveBuild(rightshapes);
break;
}
case SplitMethod::SAH: {
// SAH分割(表面积启发式)
// 实现SAH算法...
break;
}
}
node->bounds = AABB();
node->bounds.expand(node->left->bounds);
node->bounds.expand(node->right->bounds);
}
return node;
}

18.1.3 BVH遍历算法#

Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const {
Intersection isect;
if (!node) return isect;
// 检查光线是否与节点包围盒相交
Vector3f inv_dir = Vector3f(1.0f / ray.direction.x(),
1.0f / ray.direction.y(),
1.0f / ray.direction.z());
std::array<int, 3> dir_is_neg = {
ray.direction.x() > 0 ? 0 : 1,
ray.direction.y() > 0 ? 0 : 1,
ray.direction.z() > 0 ? 0 : 1
};
if (!node->bounds.intersect_p(ray, inv_dir, dir_is_neg)) {
return isect;
}
// 叶子节点:与对象求交
if (node->left == nullptr && node->right == nullptr) {
return node->object->getIntersection(ray);
}
// 内部节点:递归遍历子节点
Intersection hit1 = getIntersection(node->left, ray);
Intersection hit2 = getIntersection(node->right, ray);
return hit1.distance < hit2.distance ? hit1 : hit2;
}

18.2 SAH(表面积启发式)#

18.2.1 SAH理论基础#

成本函数

Cost = C_trav + P_left × N_left × C_isect + P_right × N_right × C_isect

其中:

  • C_trav:遍历成本
  • P_left/P_right:光线击中左/右子树的概率
  • N_left/N_right:左/右子树的图元数量
  • C_isect:相交测试成本

概率计算

P_left = SA_left / SA_parent
P_right = SA_right / SA_parent

18.2.2 SAH实现#

struct SAHBucket {
int count = 0;
AABB bounds;
};
float evaluate_sah(const std::vector<Object*>& objects, int split_axis, float split_pos) {
AABB left_bounds, right_bounds;
int left_count = 0, right_count = 0;
for (const auto& obj : objects) {
Vector3f centroid = obj->getBounds().centroid();
if (centroid[split_axis] < split_pos) {
left_bounds.expand(obj->getBounds());
left_count++;
} else {
right_bounds.expand(obj->getBounds());
right_count++;
}
}
float cost = 1.0f + // 遍历成本
(left_count * left_bounds.surface_area() +
right_count * right_bounds.surface_area()) /
parent_bounds.surface_area();
return cost;
}
int find_best_split_sah(const std::vector<Object*>& objects) {
const int n_buckets = 12;
float min_cost = INFINITY;
int best_split = -1;
for (int dim = 0; dim < 3; ++dim) {
// 创建桶
std::vector<SAHBucket> buckets(n_buckets);
// 将对象分配到桶中
for (const auto& obj : objects) {
int bucket = n_buckets * centroid_bounds.offset(obj->getBounds().centroid())[dim];
bucket = std::min(bucket, n_buckets - 1);
buckets[bucket].count++;
buckets[bucket].bounds.expand(obj->getBounds());
}
// 计算每个分割位置的成本
for (int i = 0; i < n_buckets - 1; ++i) {
AABB b0, b1;
int count0 = 0, count1 = 0;
for (int j = 0; j <= i; ++j) {
b0.expand(buckets[j].bounds);
count0 += buckets[j].count;
}
for (int j = i + 1; j < n_buckets; ++j) {
b1.expand(buckets[j].bounds);
count1 += buckets[j].count;
}
float cost = 1.0f + (count0 * b0.surface_area() + count1 * b1.surface_area()) /
bounds.surface_area();
if (cost < min_cost) {
min_cost = cost;
best_split = i;
}
}
}
return best_split;
}

18.3 其他加速结构#

18.3.1 八叉树(Octree)#

基本原理:递归地将3D空间分割为8个子立方体

数据结构定义

class Octree {
private:
struct OctreeNode {
AABB bounds;
std::vector<Object*> objects;
std::array<std::unique_ptr<OctreeNode>, 8> children;
bool is_leaf;
OctreeNode(const AABB& b) : bounds(b), is_leaf(true) {}
};
std::unique_ptr<OctreeNode> root;
int max_depth;
int max_objects_per_node;
public:
void build(const std::vector<Object*>& objects, const AABB& scene_bounds) {
root = std::make_unique<OctreeNode>(scene_bounds);
build_recursive(root.get(), objects, 0);
}
private:
void build_recursive(OctreeNode* node, const std::vector<Object*>& objects, int depth) {
if (depth >= max_depth || objects.size() <= max_objects_per_node) {
node->objects = objects;
return;
}
// 创建8个子节点
Vector3f center = node->bounds.center();
Vector3f min_pt = node->bounds.min_point;
Vector3f max_pt = node->bounds.max_point;
// 8个子立方体的包围盒
std::array<AABB, 8> child_bounds = {
AABB(Vector3f(min_pt.x(), min_pt.y(), min_pt.z()),
Vector3f(center.x(), center.y(), center.z())), // 000
AABB(Vector3f(center.x(), min_pt.y(), min_pt.z()),
Vector3f(max_pt.x(), center.y(), center.z())), // 100
AABB(Vector3f(min_pt.x(), center.y(), min_pt.z()),
Vector3f(center.x(), max_pt.y(), center.z())), // 010
AABB(Vector3f(center.x(), center.y(), min_pt.z()),
Vector3f(max_pt.x(), max_pt.y(), center.z())), // 110
AABB(Vector3f(min_pt.x(), min_pt.y(), center.z()),
Vector3f(center.x(), center.y(), max_pt.z())), // 001
AABB(Vector3f(center.x(), min_pt.y(), center.z()),
Vector3f(max_pt.x(), center.y(), max_pt.z())), // 101
AABB(Vector3f(min_pt.x(), center.y(), center.z()),
Vector3f(center.x(), max_pt.y(), max_pt.z())), // 011
AABB(Vector3f(center.x(), center.y(), center.z()),
Vector3f(max_pt.x(), max_pt.y(), max_pt.z())) // 111
};
// 将对象分配到子节点
std::array<std::vector<Object*>, 8> child_objects;
for (const auto& obj : objects) {
for (int i = 0; i < 8; ++i) {
if (child_bounds[i].intersects(obj->getBounds())) {
child_objects[i].push_back(obj);
}
}
}
// 递归构建非空子节点
node->is_leaf = false;
for (int i = 0; i < 8; ++i) {
if (!child_objects[i].empty()) {
node->children[i] = std::make_unique<OctreeNode>(child_bounds[i]);
build_recursive(node->children[i].get(), child_objects[i], depth + 1);
}
}
}
public:
Intersection intersect(const Ray& ray) const {
return intersect_recursive(root.get(), ray);
}
private:
Intersection intersect_recursive(OctreeNode* node, const Ray& ray) const {
if (!node || !node->bounds.intersect(ray)) {
return Intersection();
}
if (node->is_leaf) {
Intersection closest_hit;
float closest_t = INFINITY;
for (const auto& obj : node->objects) {
Intersection hit = obj->getIntersection(ray);
if (hit.happened && hit.distance < closest_t) {
closest_hit = hit;
closest_t = hit.distance;
}
}
return closest_hit;
}
// 遍历子节点
Intersection closest_hit;
float closest_t = INFINITY;
for (const auto& child : node->children) {
if (child) {
Intersection hit = intersect_recursive(child.get(), ray);
if (hit.happened && hit.distance < closest_t) {
closest_hit = hit;
closest_t = hit.distance;
}
}
}
return closest_hit;
}
};

优缺点分析

  • 优点:空间分割均匀,易于理解和实现
  • 缺点:对象可能跨越多个节点,存储冗余

18.3.2 kD树(k-Dimensional Tree)#

基本原理:沿坐标轴交替分割空间

数据结构

struct KDNode {
AABB bounds;
int split_axis; // 分割轴:0=x, 1=y, 2=z
float split_value; // 分割位置
std::unique_ptr<KDNode> left; // 小于split_value的子树
std::unique_ptr<KDNode> right; // 大于等于split_value的子树
std::vector<Object*> objects; // 叶子节点存储的对象
bool is_leaf;
};
class KDTree {
private:
std::unique_ptr<KDNode> root;
int max_depth;
int max_objects_per_leaf;
public:
void build(const std::vector<Object*>& objects, const AABB& bounds) {
root = build_recursive(objects, bounds, 0);
}
private:
std::unique_ptr<KDNode> build_recursive(const std::vector<Object*>& objects,
const AABB& bounds, int depth) {
auto node = std::make_unique<KDNode>();
node->bounds = bounds;
// 终止条件
if (depth >= max_depth || objects.size() <= max_objects_per_leaf) {
node->is_leaf = true;
node->objects = objects;
return node;
}
// 选择分割轴(循环选择或基于最长轴)
int axis = depth % 3; // 或者选择bounds的最长轴
// 计算分割位置(中位数或中点)
std::vector<float> centroids;
for (const auto& obj : objects) {
centroids.push_back(obj->getBounds().center()[axis]);
}
std::sort(centroids.begin(), centroids.end());
float split_pos = centroids[centroids.size() / 2]; // 中位数
node->split_axis = axis;
node->split_value = split_pos;
node->is_leaf = false;
// 分割对象
std::vector<Object*> left_objects, right_objects;
for (const auto& obj : objects) {
float centroid = obj->getBounds().center()[axis];
if (centroid < split_pos) {
left_objects.push_back(obj);
} else {
right_objects.push_back(obj);
}
}
// 计算子节点包围盒
AABB left_bounds = bounds, right_bounds = bounds;
left_bounds.max_point[axis] = split_pos;
right_bounds.min_point[axis] = split_pos;
// 递归构建子树
if (!left_objects.empty()) {
node->left = build_recursive(left_objects, left_bounds, depth + 1);
}
if (!right_objects.empty()) {
node->right = build_recursive(right_objects, right_bounds, depth + 1);
}
return node;
}
};

遍历算法

Intersection traverse_kd_tree(KDNode* node, const Ray& ray) {
if (!node || !node->bounds.intersect(ray)) {
return Intersection();
}
if (node->is_leaf) {
// 与叶子节点中的所有对象求交
Intersection closest_hit;
float closest_t = INFINITY;
for (const auto& obj : node->objects) {
Intersection hit = obj->getIntersection(ray);
if (hit.happened && hit.distance < closest_t) {
closest_hit = hit;
closest_t = hit.distance;
}
}
return closest_hit;
}
// 确定光线与分割平面的关系
float t_split = (node->split_value - ray.origin[node->split_axis]) /
ray.direction[node->split_axis];
KDNode* first_child, *second_child;
if (ray.origin[node->split_axis] < node->split_value) {
first_child = node->left.get();
second_child = node->right.get();
} else {
first_child = node->right.get();
second_child = node->left.get();
}
// 遍历第一个子节点
Intersection hit = traverse_kd_tree(first_child, ray);
// 如果找到交点且在分割平面之前,直接返回
if (hit.happened && hit.distance < t_split) {
return hit;
}
// 否则遍历第二个子节点
Intersection hit2 = traverse_kd_tree(second_child, ray);
// 返回最近的交点
if (hit2.happened && (!hit.happened || hit2.distance < hit.distance)) {
return hit2;
}
return hit;
}

18.3.3 加速结构性能对比#

理论复杂度

结构构建时间内存消耗查询时间适用场景
BVHO(n log n)O(n)O(log n)通用,动态场景
OctreeO(n log n)O(n)O(log n)均匀分布的场景
kD-TreeO(n log n)O(n)O(log n)静态场景,点查询
GridO(n)O(n + cells)O(1)均匀密度场景

实际性能测试

struct PerformanceTest {
std::string structure_name;
double build_time;
double query_time;
size_t memory_usage;
int ray_count;
void print_results() const {
std::cout << structure_name << ":\n";
std::cout << " 构建时间: " << build_time << "ms\n";
std::cout << " 平均查询时间: " << query_time / ray_count << "ms\n";
std::cout << " 内存使用: " << memory_usage / 1024.0 / 1024.0 << "MB\n";
}
};
void benchmark_acceleration_structures(const std::vector<Object*>& objects,
const std::vector<Ray>& test_rays) {
std::vector<PerformanceTest> results;
// 测试BVH
{
auto start = std::chrono::high_resolution_clock::now();
BVHAccel bvh(objects);
auto build_end = std::chrono::high_resolution_clock::now();
auto query_start = std::chrono::high_resolution_clock::now();
for (const auto& ray : test_rays) {
bvh.Intersect(ray);
}
auto query_end = std::chrono::high_resolution_clock::now();
PerformanceTest test;
test.structure_name = "BVH";
test.build_time = std::chrono::duration<double, std::milli>(build_end - start).count();
test.query_time = std::chrono::duration<double, std::milli>(query_end - query_start).count();
test.ray_count = test_rays.size();
results.push_back(test);
}
// 类似地测试其他结构...
// 输出结果
for (const auto& result : results) {
result.print_results();
}
}

蒙特卡洛方法与积分#

19.1 蒙特卡洛积分理论#

19.1.1 蒙特卡洛积分的数学基础#

基本蒙特卡洛估计#

积分估计公式: 对于一维积分 abf(x)dx\int_a^b f(x) dx,蒙特卡洛估计为: abf(x)dxbaNi=1Nf(Xi)\int_a^b f(x) dx \approx \frac{b-a}{N} \sum_{i=1}^N f(X_i)

其中 XiX_i 是在区间 [a,b][a,b] 上均匀分布的随机样本。

多维积分推广: 对于 dd 维积分: Ωf(x)dxΩNi=1Nf(Xi)\int_{\Omega} f(\mathbf{x}) d\mathbf{x} \approx \frac{|\Omega|}{N} \sum_{i=1}^N f(\mathbf{X}_i)

其中 Ω|\Omega| 是积分域的体积。

理论保证#

强大数定律limN1Ni=1Nf(Xi)=E[f(X)]=f(x)p(x)dxa.s.\lim_{N \to \infty} \frac{1}{N} \sum_{i=1}^N f(X_i) = E[f(X)] = \int f(x) p(x) dx \quad \text{a.s.}

中心极限定理N(1Ni=1Nf(Xi)E[f(X)])dN(0,σ2)\sqrt{N}\left(\frac{1}{N} \sum_{i=1}^N f(X_i) - E[f(X)]\right) \xrightarrow{d} \mathcal{N}(0, \sigma^2)

其中 σ2=Var[f(X)]\sigma^2 = \text{Var}[f(X)]

收敛速度: 标准误差为 O(N1/2)O(N^{-1/2})与维度无关,这是蒙特卡洛方法的重要优势。

19.1.2 重要性采样理论#

重要性采样的数学推导#

基本变换f(x)dx=f(x)p(x)p(x)dx=E[f(X)p(X)]\int f(\mathbf{x}) d\mathbf{x} = \int \frac{f(\mathbf{x})}{p(\mathbf{x})} p(\mathbf{x}) d\mathbf{x} = E\left[\frac{f(\mathbf{X})}{p(\mathbf{X})}\right]

其中 p(x)p(\mathbf{x}) 是概率密度函数,Xp(x)\mathbf{X} \sim p(\mathbf{x})

蒙特卡洛估计f(x)dx1Ni=1Nf(Xi)p(Xi)\int f(\mathbf{x}) d\mathbf{x} \approx \frac{1}{N} \sum_{i=1}^N \frac{f(\mathbf{X}_i)}{p(\mathbf{X}_i)}

方差分析与最优采样#

估计量的方差Var[f(X)p(X)]=f2(x)p(x)dx(f(x)dx)2\text{Var}\left[\frac{f(\mathbf{X})}{p(\mathbf{X})}\right] = \int \frac{f^2(\mathbf{x})}{p(\mathbf{x})} d\mathbf{x} - \left(\int f(\mathbf{x}) d\mathbf{x}\right)^2

最优概率密度函数: 使方差最小的最优采样密度为: p(x)=f(x)f(y)dyp^*(\mathbf{x}) = \frac{|f(\mathbf{x})|}{\int |f(\mathbf{y})| d\mathbf{y}}

此时方差为: Var=(f(x)dx)2(f(x)dx)2\text{Var}^* = \left(\int |f(\mathbf{x})| d\mathbf{x}\right)^2 - \left(\int f(\mathbf{x}) d\mathbf{x}\right)^2

实用采样策略: 在实际应用中,选择 p(x)f(x)p(\mathbf{x}) \propto |f(\mathbf{x})| 可以显著减少方差。

代码实现

class ImportanceSampler {
private:
std::function<float(float)> pdf; // 概率密度函数
std::function<float(float)> inverse_cdf; // 累积分布函数的逆
public:
float sample() {
float u = random_float(); // [0,1)均匀随机数
return inverse_cdf(u);
}
float evaluate_pdf(float x) {
return pdf(x);
}
};
// 余弦加权半球采样(用于漫反射)
Vector3f cosine_weighted_hemisphere_sample() {
float u1 = random_float();
float u2 = random_float();
float cos_theta = std::sqrt(u1);
float sin_theta = std::sqrt(1.0f - u1);
float phi = 2.0f * M_PI * u2;
return Vector3f(sin_theta * std::cos(phi),
sin_theta * std::sin(phi),
cos_theta);
}
float cosine_hemisphere_pdf(float cos_theta) {
return cos_theta / M_PI;
}

19.1.3 方差减少技术的数学理论#

分层采样(Stratified Sampling)#

基本原理: 将积分域分割成 MM 个不相交的子域 Ωj\Omega_j,在每个子域内独立采样: Ωf(x)dx=j=1MΩjf(x)dx\int_{\Omega} f(\mathbf{x}) d\mathbf{x} = \sum_{j=1}^M \int_{\Omega_j} f(\mathbf{x}) d\mathbf{x}

方差减少效果: 分层采样的方差为: Varstrat=j=1MΩj2njσj2\text{Var}_{strat} = \sum_{j=1}^M \frac{|\Omega_j|^2}{n_j} \sigma_j^2

其中 σj2\sigma_j^2 是第 jj 层内的方差,通常 VarstratVaruniform\text{Var}_{strat} \leq \text{Var}_{uniform}

多重重要性采样(MIS)#

问题背景: 当有多个采样策略时,如何组合它们以获得最优结果?

平衡启发式(Balance Heuristic): 对于 nn 个采样策略,权重函数为: wi(x)=nipi(x)j=1nnjpj(x)w_i(\mathbf{x}) = \frac{n_i p_i(\mathbf{x})}{\sum_{j=1}^n n_j p_j(\mathbf{x})}

其中 nin_i 是策略 ii 的样本数,pi(x)p_i(\mathbf{x}) 是对应的概率密度函数。

幂启发式(Power Heuristic)wi(x)=(nipi(x))βj=1n(njpj(x))βw_i(\mathbf{x}) = \frac{(n_i p_i(\mathbf{x}))^\beta}{\sum_{j=1}^n (n_j p_j(\mathbf{x}))^\beta}

通常取 β=2\beta = 2

MIS估计量I^MIS=i=1n1nij=1niwi(Xi,j)f(Xi,j)pi(Xi,j)\hat{I}_{MIS} = \sum_{i=1}^n \frac{1}{n_i} \sum_{j=1}^{n_i} w_i(\mathbf{X}_{i,j}) \frac{f(\mathbf{X}_{i,j})}{p_i(\mathbf{X}_{i,j})}

工程实现#

// 分层采样实现
class StratifiedSampler {
public:
std::vector<Vector2f> generate_2d_samples(int sqrt_samples) {
std::vector<Vector2f> samples;
float inv_sqrt = 1.0f / sqrt_samples;
for (int i = 0; i < sqrt_samples; ++i) {
for (int j = 0; j < sqrt_samples; ++j) {
float jitter_x = random_float();
float jitter_y = random_float();
float x = (i + jitter_x) * inv_sqrt;
float y = (j + jitter_y) * inv_sqrt;
samples.emplace_back(x, y);
}
}
// 随机打乱以避免相关性
std::shuffle(samples.begin(), samples.end(), rng);
return samples;
}
};
// 多重重要性采样实现
class MISIntegrator {
public:
static float power_heuristic(float pdf_a, float pdf_b, int beta = 2) {
float weight_a = std::pow(pdf_a, beta);
float weight_b = std::pow(pdf_b, beta);
return weight_a / (weight_a + weight_b);
}
Vector3f estimate_direct_lighting(const Intersection& hit) {
Vector3f L(0, 0, 0);
// 策略1:BRDF采样
Vector3f brdf_dir = sample_brdf(hit);
float brdf_pdf = evaluate_brdf_pdf(hit, brdf_dir);
float light_pdf = evaluate_light_pdf(hit, brdf_dir);
if (brdf_pdf > 0) {
float weight = power_heuristic(brdf_pdf, light_pdf);
Vector3f brdf_value = evaluate_brdf(hit, brdf_dir);
Vector3f incoming = trace_ray(hit.position, brdf_dir);
L += weight * brdf_value * incoming / brdf_pdf;
}
// 策略2:光源采样
Vector3f light_dir = sample_light(hit);
light_pdf = evaluate_light_pdf(hit, light_dir);
brdf_pdf = evaluate_brdf_pdf(hit, light_dir);
if (light_pdf > 0) {
float weight = power_heuristic(light_pdf, brdf_pdf);
Vector3f brdf_value = evaluate_brdf(hit, light_dir);
Vector3f incoming = trace_ray(hit.position, light_dir);
L += weight * brdf_value * incoming / light_pdf;
}
return L;
}
};
trace_ray(hit.position, brdf_dir) *
weight_brdf / brdf_pdf;
// 光源采样
Vector3f light_dir = sample_light(hit);
light_pdf = evaluate_light_pdf(hit, light_dir);
brdf_pdf = evaluate_brdf_pdf(hit, light_dir);
float weight_light = mis_weight(light_pdf, brdf_pdf);
Vector3f light_contribution = evaluate_brdf(hit, light_dir) *
trace_ray(hit.position, light_dir) *
weight_light / light_pdf;
return brdf_contribution + light_contribution;
}

19.2 渲染中的蒙特卡洛应用#

19.2.1 直接光照的蒙特卡洛估计#

渲染方程的直接光照部分

$$L_{direct} = \int_\Omega f_r(\omega_i, \omega_o) L_i(\omega_i) \cos \theta_i d\omega_i$$

蒙特卡洛估计

Vector3f estimate_direct_lighting(const Intersection& hit, const Vector3f& wo) {
Vector3f direct_lighting(0, 0, 0);
int samples = 64;
for (int i = 0; i < samples; ++i) {
// 采样光源
Intersection light_sample;
float light_pdf;
scene.sample_light(light_sample, light_pdf);
Vector3f light_dir = (light_sample.coords - hit.coords).normalized();
float distance = (light_sample.coords - hit.coords).norm();
// 阴影测试
Ray shadow_ray(hit.coords + EPSILON * hit.normal, light_dir);
shadow_ray.t_max = distance - EPSILON;
if (!scene.intersect(shadow_ray).happened) {
// 计算BRDF
Vector3f brdf = hit.m->eval(light_dir, wo, hit.normal);
// 计算贡献
float cos_theta = std::max(0.0f, hit.normal.dot(light_dir));
Vector3f contribution = brdf * light_sample.emit * cos_theta / light_pdf;
direct_lighting += contribution;
}
}
return direct_lighting / samples;
}

19.2.2 全局光照的路径追踪#

路径追踪算法

Vector3f path_tracing(const Ray& ray, int depth) {
if (depth <= 0) return Vector3f(0, 0, 0);
Intersection hit = scene.intersect(ray);
if (!hit.happened) return scene.background_color;
Vector3f color(0, 0, 0);
// 自发光
color += hit.m->getEmission();
// 直接光照
color += estimate_direct_lighting(hit, -ray.direction);
// 间接光照(俄罗斯轮盘赌)
float russian_roulette = 0.8f;
if (random_float() < russian_roulette) {
// 采样BRDF
Vector3f wi = sample_hemisphere(hit.normal);
float pdf = 1.0f / (2.0f * M_PI); // 均匀半球采样
Vector3f brdf = hit.m->eval(wi, -ray.direction, hit.normal);
float cos_theta = std::max(0.0f, hit.normal.dot(wi));
Ray indirect_ray(hit.coords + EPSILON * hit.normal, wi);
Vector3f indirect = path_tracing(indirect_ray, depth - 1);
color += brdf * indirect * cos_theta / (pdf * russian_roulette);
}
return color;
}

19.2.3 双向路径追踪#

基本思想:同时从光源和摄像机追踪路径,在中间连接

算法框架

Vector3f bidirectional_path_tracing(const Ray& camera_ray) {
// 从摄像机追踪路径
std::vector<PathVertex> camera_path;
trace_path_from_camera(camera_ray, camera_path);
// 从光源追踪路径
std::vector<PathVertex> light_path;
trace_path_from_light(light_path);
Vector3f color(0, 0, 0);
// 尝试所有可能的连接
for (int i = 0; i < camera_path.size(); ++i) {
for (int j = 0; j < light_path.size(); ++j) {
Vector3f contribution = connect_paths(camera_path, i, light_path, j);
color += contribution;
}
}
return color;
}
struct PathVertex {
Vector3f position;
Vector3f normal;
Vector3f wi, wo; // 入射和出射方向
Material* material;
float pdf_forward, pdf_backward;
Vector3f throughput; // 路径权重
};
Vector3f connect_paths(const std::vector<PathVertex>& camera_path, int cam_idx,
const std::vector<PathVertex>& light_path, int light_idx) {
if (cam_idx >= camera_path.size() || light_idx >= light_path.size()) {
return Vector3f(0, 0, 0);
}
const PathVertex& cam_vertex = camera_path[cam_idx];
const PathVertex& light_vertex = light_path[light_idx];
// 检查连接的可见性
Vector3f connection_dir = (light_vertex.position - cam_vertex.position).normalized();
float distance = (light_vertex.position - cam_vertex.position).norm();
Ray visibility_ray(cam_vertex.position + EPSILON * cam_vertex.normal, connection_dir);
visibility_ray.t_max = distance - EPSILON;
if (scene.intersect(visibility_ray).happened) {
return Vector3f(0, 0, 0); // 被遮挡
}
// 计算连接的贡献
Vector3f brdf_cam = cam_vertex.material->eval(connection_dir, cam_vertex.wo, cam_vertex.normal);
Vector3f brdf_light = light_vertex.material->eval(-connection_dir, light_vertex.wi, light_vertex.normal);
float cos_cam = std::max(0.0f, cam_vertex.normal.dot(connection_dir));
float cos_light = std::max(0.0f, light_vertex.normal.dot(-connection_dir));
Vector3f geometry_term = Vector3f(cos_cam * cos_light / (distance * distance));
return cam_vertex.throughput * brdf_cam * geometry_term * brdf_light * light_vertex.throughput;
}

全局光照与路径追踪#

20.1 渲染方程深度解析#

20.1.1 渲染方程的数学推导与物理意义#

渲染方程的完整数学形式#

积分形式的渲染方程Lo(p,ωo)=Le(p,ωo)+Ωfr(p,ωi,ωo)Li(p,ωi)cosθidωiL_o(\mathbf{p}, \omega_o) = L_e(\mathbf{p}, \omega_o) + \int_{\Omega} f_r(\mathbf{p}, \omega_i, \omega_o) L_i(\mathbf{p}, \omega_i) \cos \theta_i \, d\omega_i

各项的物理意义与数学定义#

1. 出射辐射度 Lo(p,ωo)L_o(\mathbf{p}, \omega_o): 从表面点 p\mathbf{p} 沿方向 ωo\omega_o 的出射辐射度,单位为 Wm2sr1\text{W} \cdot \text{m}^{-2} \cdot \text{sr}^{-1}

2. 自发光项 Le(p,ωo)L_e(\mathbf{p}, \omega_o): 表面点 p\mathbf{p} 自身发出的辐射度(对于光源)

3. BRDF fr(p,ωi,ωo)f_r(\mathbf{p}, \omega_i, \omega_o): 双向反射分布函数,定义为: fr(p,ωi,ωo)=dLo(p,ωo)dEi(p,ωi)=dLo(p,ωo)Li(p,ωi)cosθidωif_r(\mathbf{p}, \omega_i, \omega_o) = \frac{dL_o(\mathbf{p}, \omega_o)}{dE_i(\mathbf{p}, \omega_i)} = \frac{dL_o(\mathbf{p}, \omega_o)}{L_i(\mathbf{p}, \omega_i) \cos \theta_i \, d\omega_i}

4. 入射辐射度 Li(p,ωi)L_i(\mathbf{p}, \omega_i): 从方向 ωi\omega_i 入射到点 p\mathbf{p} 的辐射度

5. 余弦项 cosθi\cos \theta_i: Lambert余弦定律,其中 θi\theta_i 是入射方向与表面法向量的夹角

6. 立体角积分域 Ω\Omega: 以点 p\mathbf{p} 为中心的上半球面,Ω=2π\Omega = 2\pi 立体角

递归形式与光传输#

递归渲染方程Lo(p,ωo)=Le(p,ωo)+Ωfr(p,ωi,ωo)Lo(p,ωi)cosθidωiL_o(\mathbf{p}, \omega_o) = L_e(\mathbf{p}, \omega_o) + \int_{\Omega} f_r(\mathbf{p}, \omega_i, \omega_o) L_o(\mathbf{p}', -\omega_i) \cos \theta_i \, d\omega_i

其中 p\mathbf{p}' 是沿方向 ωi\omega_i 的光线与场景的下一个交点: p=p+tωi\mathbf{p}' = \mathbf{p} + t \omega_i

这个递归形式体现了光在场景中的多次弹射传播。

20.1.2 光传输算子的数学理论#

算子形式的渲染方程#

线性算子表示L=E+TL\mathbf{L} = \mathbf{E} + \mathbf{T}\mathbf{L}

其中:

  • L\mathbf{L}:辐射度函数(在所有表面点和方向上的分布)
  • E\mathbf{E}:自发光项
  • T\mathbf{T}:光传输算子

光传输算子的定义(TL)(p,ωo)=Ωfr(p,ωi,ωo)L(p,ωi)cosθidωi(\mathbf{T}\mathbf{L})(\mathbf{p}, \omega_o) = \int_{\Omega} f_r(\mathbf{p}, \omega_i, \omega_o) \mathbf{L}(\mathbf{p}', -\omega_i) \cos \theta_i \, d\omega_i

Neumann级数展开#

级数解: 从算子方程 L=E+TL\mathbf{L} = \mathbf{E} + \mathbf{T}\mathbf{L} 可得: L=(IT)1E=n=0TnE\mathbf{L} = (\mathbf{I} - \mathbf{T})^{-1}\mathbf{E} = \sum_{n=0}^{\infty} \mathbf{T}^n \mathbf{E}

各项的物理意义

光线弹射次数与光照贡献:

直接光照(0次弹射): T0E=E\mathbf{T}^0\mathbf{E} = \mathbf{E}

一次间接光照(1次弹射): T1E=TE\mathbf{T}^1\mathbf{E} = \mathbf{T}\mathbf{E}

二次间接光照(2次弹射): T2E=T2E\mathbf{T}^2\mathbf{E} = \mathbf{T}^2\mathbf{E}

n次间接光照(n次弹射): TnE\mathbf{T}^n\mathbf{E}

收敛性分析#

收敛条件: 级数收敛当且仅当 T<1\|\mathbf{T}\| < 1,即光传输算子的谱半径小于1。

物理意义: 这对应于能量守恒定律——每次反射都会损失一部分能量,因此无限次弹射后总能量收敛。

实际应用: 在路径追踪中,通过俄罗斯轮盘赌(Russian Roulette)来无偏地截断无限级数。

20.2 路径追踪算法详解#

20.2.1 基础路径追踪#

算法原理:从摄像机发射光线,在每个交点随机选择一个方向继续追踪

基于GAMES101 Assignment7的实现

Vector3f Scene::castRay(const Ray &ray, int depth) const {
if (depth > this->maxDepth) {
return Vector3f(0.0, 0.0, 0.0);
}
Intersection intersection = Scene::intersect(ray);
if (!intersection.happened) {
return this->backgroundColor;
}
Material *m = intersection.m;
Object *hitObject = intersection.obj;
Vector3f hitColor = this->backgroundColor;
Vector3f hitPoint = intersection.coords;
Vector3f N = intersection.normal;
Vector2f uv = intersection.uv;
switch (m->getType()) {
case DIFFUSE: {
// 直接光照采样
Vector3f L_dir(0, 0, 0);
// 对光源进行采样
Intersection light_pos;
float light_pdf = 0.0f;
sampleLight(light_pos, light_pdf);
Vector3f obj2light = light_pos.coords - hitPoint;
Vector3f obj2lightdir = obj2light.normalized();
float distance = obj2light.norm();
// 发射阴影光线
Ray shadowRay(hitPoint, obj2lightdir);
Intersection shadowIntersection = intersect(shadowRay);
// 检查是否被遮挡
if (shadowIntersection.distance - distance > -EPSILON) {
Vector3f f_r = m->eval(obj2lightdir, -ray.direction, N);
L_dir = light_pos.emit * f_r * dotProduct(obj2lightdir, N) / light_pdf / distance / distance;
}
// 间接光照采样(俄罗斯轮盘赌)
Vector3f L_indir(0, 0, 0);
float ksi = get_random_float();
if (ksi < RussianRoulette) {
Vector3f wi = toWorld(sampleHemisphere(uv), N);
Ray indirectRay(hitPoint, wi);
Intersection indirectIntersection = intersect(indirectRay);
if (indirectIntersection.happened && !indirectIntersection.obj->hasEmit()) {
Vector3f f_r = m->eval(wi, -ray.direction, N);
float pdf = 1.0f / (2.0f * M_PI); // 均匀半球采样
L_indir = castRay(indirectRay, depth + 1) * f_r * dotProduct(wi, N) / pdf / RussianRoulette;
}
}
hitColor = L_dir + L_indir;
break;
}
}
return hitColor;
}

20.2.2 重要性采样优化#

BRDF重要性采样

// 余弦加权采样(适用于Lambert材质)
Vector3f cosine_sample_hemisphere(const Vector2f& u) {
float cos_theta = std::sqrt(u[0]);
float sin_theta = std::sqrt(1.0f - u[0]);
float phi = 2.0f * M_PI * u[1];
return Vector3f(sin_theta * std::cos(phi),
sin_theta * std::sin(phi),
cos_theta);
}
float cosine_hemisphere_pdf(float cos_theta) {
return cos_theta / M_PI;
}
// 镜面反射采样
Vector3f sample_specular_reflection(const Vector3f& wi, const Vector3f& normal) {
return wi - 2.0f * wi.dot(normal) * normal;
}
// 微表面模型采样(GGX分布)
Vector3f sample_ggx_distribution(const Vector2f& u, float alpha) {
float cos_theta = std::sqrt((1.0f - u[0]) / (1.0f + (alpha * alpha - 1.0f) * u[0]));
float sin_theta = std::sqrt(1.0f - cos_theta * cos_theta);
float phi = 2.0f * M_PI * u[1];
return Vector3f(sin_theta * std::cos(phi),
sin_theta * std::sin(phi),
cos_theta);
}

20.2.3 多重重要性采样#

结合BRDF采样和光源采样

Vector3f estimate_direct_lighting_mis(const Intersection& hit, const Vector3f& wo) {
Vector3f L(0, 0, 0);
// 光源采样
Intersection light_sample;
float light_pdf;
scene.sample_light(light_sample, light_pdf);
if (light_pdf > 0) {
Vector3f wi = (light_sample.coords - hit.coords).normalized();
// 检查可见性
if (!scene.occluded(hit.coords, light_sample.coords)) {
Vector3f f = hit.material->eval(wi, wo, hit.normal);
float brdf_pdf = hit.material->pdf(wi, wo, hit.normal);
if (brdf_pdf > 0) {
float weight = power_heuristic(light_pdf, brdf_pdf);
float cos_theta = std::max(0.0f, hit.normal.dot(wi));
L += f * light_sample.emit * cos_theta * weight / light_pdf;
}
}
}
// BRDF采样
Vector3f wi;
float brdf_pdf;
Vector3f f = hit.material->sample_f(wo, wi, hit.normal, brdf_pdf);
if (brdf_pdf > 0) {
Ray ray(hit.coords + EPSILON * hit.normal, wi);
Intersection light_hit = scene.intersect(ray);
if (light_hit.happened && light_hit.obj->hasEmit()) {
float light_pdf_value = scene.pdf_light(hit.coords, light_hit.coords);
float weight = power_heuristic(brdf_pdf, light_pdf_value);
float cos_theta = std::max(0.0f, hit.normal.dot(wi));
L += f * light_hit.obj->getEmission() * cos_theta * weight / brdf_pdf;
}
}
return L;
}
// Power启发式权重函数
float power_heuristic(float pdf_a, float pdf_b, int beta = 2) {
float a = std::pow(pdf_a, beta);
float b = std::pow(pdf_b, beta);
return a / (a + b);
}

20.3 高级全局光照技术#

20.3.1 光子映射#

基本思想

  1. 光子发射:从光源发射光子,记录其在场景中的交互
  2. 光子存储:将光子存储在空间数据结构中
  3. 密度估计:在渲染时查询附近光子估计辐射度

光子结构

struct Photon {
Vector3f position; // 光子位置
Vector3f direction; // 入射方向
Vector3f power; // 光子能量
short plane; // kD树分割平面
};
class PhotonMap {
private:
std::vector<Photon> photons;
int stored_photons;
int max_photons;
public:
void store(const Photon& photon) {
if (stored_photons < max_photons) {
photons[stored_photons] = photon;
stored_photons++;
}
}
Vector3f irradiance_estimate(const Vector3f& position, const Vector3f& normal,
float max_distance, int max_photons) {
// 查找最近的光子
std::vector<std::pair<float, int>> nearest_photons;
find_nearest_photons(position, max_distance, max_photons, nearest_photons);
if (nearest_photons.empty()) return Vector3f(0, 0, 0);
Vector3f irradiance(0, 0, 0);
float max_dist_sqr = nearest_photons.back().first;
for (const auto& [dist_sqr, idx] : nearest_photons) {
const Photon& photon = photons[idx];
// 检查光子方向与表面法向量的关系
if (photon.direction.dot(normal) < 0) {
irradiance += photon.power;
}
}
// 密度估计:$irradiance = \frac{power}{area}$
return irradiance / (M_PI * max_dist_sqr);
}
};

光子追踪过程

void trace_photon(const Ray& ray, const Vector3f& power, int depth) {
if (depth <= 0 || power.norm() < EPSILON) return;
Intersection hit = scene.intersect(ray);
if (!hit.happened) return;
// 存储光子(除了第一次交互)
if (depth < max_depth) {
Photon photon;
photon.position = hit.coords;
photon.direction = ray.direction;
photon.power = power;
photon_map.store(photon);
}
// 俄罗斯轮盘赌决定是否继续
float survival_prob = std::min(0.9f, power.maxCoeff());
if (random_float() > survival_prob) return;
// 采样新方向
Vector3f new_direction;
Vector3f brdf = hit.material->sample_f(-ray.direction, new_direction, hit.normal);
Vector3f new_power = power * brdf * hit.normal.dot(new_direction) / survival_prob;
Ray new_ray(hit.coords + EPSILON * hit.normal, new_direction);
trace_photon(new_ray, new_power, depth - 1);
}

20.3.2 实时全局光照近似#

屏幕空间环境光遮蔽(SSAO)

float calculate_ssao(const Vector2f& screen_pos, const Vector3f& position,
const Vector3f& normal, const Texture& depth_buffer) {
float occlusion = 0.0f;
int samples = 64;
float radius = 0.5f;
for (int i = 0; i < samples; ++i) {
// 在法向量半球内采样
Vector3f sample_dir = generate_hemisphere_sample(normal);
Vector3f sample_pos = position + radius * sample_dir;
// 投影到屏幕空间
Vector2f sample_screen = world_to_screen(sample_pos);
// 采样深度缓冲
float sample_depth = depth_buffer.sample(sample_screen);
float actual_depth = world_to_depth(sample_pos);
// 比较深度
if (actual_depth > sample_depth + bias) {
occlusion += 1.0f;
}
}
return 1.0f - (occlusion / samples);
}

光传播体积(Light Propagation Volumes)

class LightPropagationVolume {
private:
struct VoxelGrid {
std::vector<Vector3f> red_coeffs; // 红色通道球谐系数
std::vector<Vector3f> green_coeffs; // 绿色通道球谐系数
std::vector<Vector3f> blue_coeffs; // 蓝色通道球谐系数
int resolution;
};
VoxelGrid grid;
public:
void inject_light(const std::vector<VirtualPointLight>& vpls) {
// 将虚拟点光源注入体素网格
for (const auto& vpl : vpls) {
Vector3i voxel_coord = world_to_voxel(vpl.position);
// 计算球谐系数
Vector3f sh_coeffs = compute_spherical_harmonics(vpl.direction, vpl.intensity);
grid.red_coeffs[voxel_index(voxel_coord)] += sh_coeffs * vpl.color.r;
grid.green_coeffs[voxel_index(voxel_coord)] += sh_coeffs * vpl.color.g;
grid.blue_coeffs[voxel_index(voxel_coord)] += sh_coeffs * vpl.color.b;
}
}
void propagate_light() {
// 迭代传播光照
for (int iteration = 0; iteration < 4; ++iteration) {
VoxelGrid new_grid = grid;
for (int z = 0; z < grid.resolution; ++z) {
for (int y = 0; y < grid.resolution; ++y) {
for (int x = 0; x < grid.resolution; ++x) {
Vector3i coord(x, y, z);
// 从6个邻居传播光照
propagate_from_neighbors(coord, new_grid);
}
}
}
grid = new_grid;
}
}
Vector3f sample_indirect_lighting(const Vector3f& position, const Vector3f& normal) {
Vector3i voxel_coord = world_to_voxel(position);
// 三线性插值采样球谐系数
Vector3f red_sh = trilinear_sample(grid.red_coeffs, voxel_coord);
Vector3f green_sh = trilinear_sample(grid.green_coeffs, voxel_coord);
Vector3f blue_sh = trilinear_sample(grid.blue_coeffs, voxel_coord);
// 计算法向量方向的辐射度
float sh_basis = evaluate_spherical_harmonics(normal);
return Vector3f(red_sh.dot(Vector3f(sh_basis)),
green_sh.dot(Vector3f(sh_basis)),
blue_sh.dot(Vector3f(sh_basis)));
}
};

计算机图形学笔记(四):光线追踪与全局光照
https://kyc001.github.io/posts/计算机图形学笔记四/
作者
kyc001
发布于
2025-07-23
许可协议
CC BY-NC-SA 4.0