路径追踪
蒙特卡洛路径追踪是一种全局光照算法,通过求解渲染方程产生逼真的光照效果。
渲染方程
路径追踪的核心是求解 Kajiya 渲染方程:
$$L_o(x, \omega_o) = L_e(x, \omega_o) + \int_{\Omega} f_r(x, \omega_i, \omega_o) L_i(x, \omega_i) (\omega_i \cdot n) d\omega_i$$
其中:
- $L_o$ 是出射辐射亮度
- $L_e$ 是自发光
- $f_r$ 是 BRDF
- $L_i$ 是入射辐射亮度
- $\omega_i \cdot n$ 是余弦项
蒙特卡洛积分
使用蒙特卡洛方法估计积分:
$$\int f(x) dx \approx \frac{1}{N} \sum_{i=1}^{N} \frac{f(X_i)}{p(X_i)}$$
应用到渲染方程:
$$L_o \approx L_e + \frac{1}{N} \sum_{i=1}^{N} \frac{f_r L_i (\omega_i \cdot n)}{p(\omega_i)}$$
余弦加权采样
对于 Lambertian 材质,使用余弦加权半球采样:
$$p(\omega) = \frac{\cos\theta}{\pi}$$
采样方向生成:
cpp
__device__ vec3 cosine_weighted_hemisphere(curandState* state) {
float r1 = curand_uniform(state);
float r2 = curand_uniform(state);
float phi = 2.0f * M_PI * r1;
float sqrt_r2 = sqrtf(r2);
float x = cosf(phi) * sqrt_r2;
float y = sinf(phi) * sqrt_r2;
float z = sqrtf(1.0f - r2);
return vec3(x, y, z);
}俄罗斯轮盘赌
为避免无限递归,使用俄罗斯轮盘赌终止:
$$L = \frac{L_{direct} + p_{rr} \cdot L_{indirect}}{p_{rr}}$$
当路径长度 > 3 时,以概率 $p_{rr} = 0.8$ 继续追踪:
cpp
__device__ bool russian_roulette(curandState* state, int depth) {
if (depth <= 3) return true;
return curand_uniform(state) < 0.8f;
}完整路径追踪 kernel
cpp
__device__ vec3 trace_path(
const Ray& ray,
const Scene& scene,
curandState* state,
int max_depth
) {
vec3 radiance = vec3(0.0f);
vec3 throughput = vec3(1.0f);
Ray current_ray = ray;
for (int depth = 0; depth < max_depth; ++depth) {
HitRecord hit;
if (!scene.intersect(current_ray, hit)) {
radiance += throughput * scene.background(current_ray);
break;
}
// 发光材质
radiance += throughput * hit.material->emitted();
// 俄罗斯轮盘赌
if (depth > 3 && !russian_roulette(state, depth)) break;
// 采样 BSDF
vec3 wi;
float pdf;
vec3 f = hit.material->sample(state, hit, wi, pdf);
// 更新吞吐量
throughput *= f * fabsf(dot(wi, hit.normal)) / pdf;
// 更新光线
current_ray = Ray(hit.p, wi);
}
return radiance;
}降噪
高采样数收敛慢,可使用降噪技术:
- 时域累积
- 空间滤波
- AI 降噪(OptiX Denoiser)
参考资料
- [Kajiya 1986] "The Rendering Equation"
- [Pharr 2016] "Physically Based Rendering"