告别Whitted-Style!用Python从零实现一个简单的路径追踪器(附蒙特卡洛采样与RR算法代码)

张开发
2026/4/11 10:22:42 15 分钟阅读

分享文章

告别Whitted-Style!用Python从零实现一个简单的路径追踪器(附蒙特卡洛采样与RR算法代码)
用Python构建路径追踪器从蒙特卡洛采样到俄罗斯轮盘赌的实战指南在计算机图形学领域路径追踪Path Tracing作为光线追踪家族的重要成员已经成为实现照片级真实感渲染的核心技术。与传统的Whitted-Style光线追踪相比路径追踪通过蒙特卡洛方法模拟光线在场景中的随机传播过程能够更准确地表现漫反射、全局光照和材质间的相互影响。本文将带领读者用Python从零实现一个简化但完整的路径追踪器通过可运行的代码示例深入理解蒙特卡洛积分、俄罗斯轮盘赌RR等关键算法。1. 为什么需要路径追踪Whitted-Style光线追踪作为早期经典算法存在两个根本性缺陷镜面反射假设问题强制所有反射均为完美镜面反射无法处理粗糙材质Glossy材质的真实散射行为漫反射终止问题在漫反射表面停止光线追踪忽略了漫反射物体间的光线交互即color bleeding现象# Whitted-Style光线追踪的典型伪代码 def whitted_style_trace(ray, depth): if depth MAX_DEPTH: return BLACK hit scene.intersect(ray) if hit is None: return background_color color hit.material.emissive if hit.material.is_specular: reflected compute_reflection(ray, hit) color whitted_style_trace(reflected, depth1) if hit.material.is_transparent: refracted compute_refraction(ray, hit) color whitted_style_trace(refracted, depth1) return color路径追踪通过以下创新解决这些问题蒙特卡洛积分随机采样求解渲染方程中的半球积分递归终止控制俄罗斯轮盘赌概率性地终止光线路径重要性采样优先采样对最终结果贡献大的方向如直接光源2. 蒙特卡洛积分实战实现蒙特卡洛积分的核心思想是通过随机采样近似计算定积分。对于渲染方程中的半球积分$$ L_o(p,\omega_o) \int_{\Omega} L_i(p,\omega_i) f_r(\omega_i,\omega_o) \cos\theta_i d\omega_i $$我们可以用蒙特卡洛估计量$$ \frac{1}{N} \sum_{k1}^N \frac{L_i(p,\omega_k) f_r(\omega_k,\omega_o) \cos\theta_k}{p(\omega_k)} $$import numpy as np def monte_carlo_integrate(f, pdf, sample_count100): 蒙特卡洛积分通用实现 :param f: 被积函数 :param pdf: 概率密度函数 :param sample_count: 采样数 :return: 积分估计值 total 0.0 for _ in range(sample_count): # 按pdf采样方向 xi np.random.uniform(0, 1, 2) omega_i uniform_sample_hemisphere(xi) # 计算被积函数值 fr eval_brdf(omega_i, omega_o) cos_theta np.dot(omega_i, hit.normal) Li trace_ray(Ray(hit.position, omega_i)) total Li * fr * cos_theta / pdf(omega_i) return total / sample_count def uniform_sample_hemisphere(u): 均匀半球面采样 phi 2 * np.pi * u[0] theta np.arccos(u[1]) return np.array([ np.sin(theta) * np.cos(phi), np.sin(theta) * np.sin(phi), np.cos(theta) ])注意采样数N越大结果越精确但计算成本越高。实践中需要权衡质量与性能。3. 路径追踪核心算法实现完整的路径追踪算法需要考虑直接光照、间接光照和递归终止。以下是关键实现步骤3.1 基础路径追踪框架def path_trace(ray, scene, depth0): if depth MAX_DEPTH: return np.zeros(3) # 黑色 # 求交测试 hit scene.intersect(ray) if hit is None: return scene.background # 自发光贡献 radiance hit.material.emission # 直接光照采样 light_sample sample_light(hit.position, scene.lights) if light_sample: light_dir light_sample.position - hit.position light_dist np.linalg.norm(light_dir) light_dir / light_dist # 可见性测试 shadow_ray Ray(hit.position hit.normal*EPSILON, light_dir) shadow_hit scene.intersect(shadow_ray) if shadow_hit is None or shadow_hit.distance light_dist: # 计算直接光照贡献 brdf hit.material.eval_brdf(-ray.direction, light_dir) cos_theta max(0, np.dot(hit.normal, light_dir)) radiance light_sample.radiance * brdf * cos_theta / light_sample.pdf # 俄罗斯轮盘赌决定是否继续追踪 if depth RR_DEPTH: survival_prob 0.8 # 继续追踪概率 if np.random.random() survival_prob: return radiance radiance / survival_prob # 能量补偿 # 间接光照采样 omega_i sample_brdf(-ray.direction, hit.material) pdf hit.material.pdf(-ray.direction, omega_i) if pdf EPSILON: cos_theta max(0, np.dot(hit.normal, omega_i)) brdf_val hit.material.eval_brdf(-ray.direction, omega_i) indirect_ray Ray(hit.position hit.normal*EPSILON, omega_i) radiance path_trace(indirect_ray, scene, depth1) * brdf_val * cos_theta / pdf return radiance3.2 俄罗斯轮盘赌实现俄罗斯轮盘赌RR通过概率控制递归深度避免无限递归同时保持无偏估计def russian_roulette(radiance, depth): 俄罗斯轮盘赌递归终止控制 if depth 3: # 前3次反射强制追踪 return radiance, False rr_prob 0.7 # 继续追踪概率 if np.random.random() rr_prob: return np.zeros(3), True # 终止 # 补偿能量以保持无偏 return radiance / rr_prob, False3.3 重要性采样优化均匀采样效率低下采用BRDF重要性采样可大幅减少噪声def cosine_weighted_sample(u): 余弦加权半球采样 phi 2 * np.pi * u[0] r np.sqrt(u[1]) return np.array([ r * np.cos(phi), r * np.sin(phi), np.sqrt(1 - u[1]) ]) def ggx_sample(u, roughness): GGX法线分布采样 a roughness * roughness phi 2 * np.pi * u[0] cos_theta np.sqrt((1 - u[1]) / (1 (a*a - 1) * u[1])) sin_theta np.sqrt(1 - cos_theta*cos_theta) return np.array([ sin_theta * np.cos(phi), sin_theta * np.sin(phi), cos_theta ])4. 完整渲染管线实现将各组件整合成完整渲染器class PathTracer: def __init__(self, width, height): self.width width self.height height self.samples_per_pixel 16 self.max_depth 5 self.rr_prob 0.8 def render(self, scene): image np.zeros((self.height, self.width, 3)) for y in range(self.height): for x in range(self.width): radiance np.zeros(3) # 多重采样抗锯齿 for _ in range(self.samples_per_pixel): # 生成相机光线 u (x np.random.random()) / self.width v (y np.random.random()) / self.height ray scene.camera.generate_ray(u, v) # 路径追踪 radiance self.trace(ray, scene) # 求平均并应用gamma校正 radiance / self.samples_per_pixel image[y,x] np.power(np.clip(radiance, 0, 1), 1/2.2) return image def trace(self, ray, scene, depth0): if depth self.max_depth: return np.zeros(3) hit scene.intersect(ray) if hit is None: return scene.background # 自发光贡献 radiance hit.material.emission # 直接光照采样 light_radiance self.sample_direct_lighting(hit, scene) radiance light_radiance # 俄罗斯轮盘赌 if depth 2: if np.random.random() self.rr_prob: return radiance radiance / self.rr_prob # 间接光照采样 omega_i, pdf hit.material.sample_brdf(-ray.direction) if pdf 0: cos_theta max(0, np.dot(hit.normal, omega_i)) brdf hit.material.eval_brdf(-ray.direction, omega_i) indirect_ray Ray(hit.position hit.normal*EPSILON, omega_i) indirect_radiance self.trace(indirect_ray, scene, depth1) radiance indirect_radiance * brdf * cos_theta / pdf return radiance def sample_direct_lighting(self, hit, scene): if not scene.lights: return np.zeros(3) # 随机选择一个光源 light np.random.choice(scene.lights) sample light.sample(hit.position) # 计算光照方向 light_dir sample.position - hit.position light_distance np.linalg.norm(light_dir) light_dir / light_distance # 阴影测试 shadow_ray Ray(hit.position hit.normal*EPSILON, light_dir) shadow_hit scene.intersect(shadow_ray) if shadow_hit and shadow_hit.distance light_distance: return np.zeros(3) # 计算直接光照贡献 brdf hit.material.eval_brdf(-ray.direction, light_dir) cos_theta max(0, np.dot(hit.normal, light_dir)) light_pdf sample.pdf * len(scene.lights) return sample.radiance * brdf * cos_theta / light_pdf5. 性能优化与进阶技巧5.1 分层采样降噪def stratified_samples(n): 分层采样生成更均匀的随机数 samples np.stack([ np.random.permutation(np.linspace(0, 1, n, endpointFalse)), np.random.permutation(np.linspace(0, 1, n, endpointFalse)) ], axis1) return samples np.random.random((n, 2)) / n5.2 多重重要性采样结合光源采样和BRDF采样的优势def mis_weights(pdf_a, pdf_b, beta2): 多重重要性采样权重 weight_a (pdf_a ** beta) / (pdf_a ** beta pdf_b ** beta) weight_b (pdf_b ** beta) / (pdf_a ** beta pdf_b ** beta) return weight_a, weight_b5.3 渐进式渲染实现class ProgressiveRenderer: def __init__(self, width, height): self.accum_buffer np.zeros((height, width, 3)) self.sample_count 0 def update(self, new_samples): self.accum_buffer new_samples self.sample_count 1 return self.accum_buffer / self.sample_count路径追踪的实现过程中调试和优化往往占据大部分时间。建议从简单场景开始如Cornell Box逐步验证各组件正确性。通过可视化中间结果如首次命中、直接光照、间接光照分量可以快速定位问题。

更多文章