Path Tracing 实现的伪代码

shade(p, wo)
	// 1. 直接光照
	sampleLight(inter , pdf_light)
	Get x, ws, NN, emit form inter
	Shoot a ray form p to x
	If the ray is not blocked in the middle
		L_dir = emit        // Emitssion
      * eval(wo, ws, N) // BRDF 描述光从 ws → wo 在表面如何反射
      * dot(ws, N)      // theta
      * dot(ws, NN)     // theta_prime
      / |x - p|^2       // r^2
      / pdf_light       // Monte Carlo
      
	// 2. 间接光照
	L_indir = 0.0
	// 用俄罗斯轮盘赌决定是否继续递归 (防止无线递归,保证最终结果能量无损失)
	Test Russain Roulette with probability RussainRoulette
	wi = sample(wo, N) // 方向采样(BRDF Sampling)
										 // 根据 BRDF 分布采样一个入射方向 wi, 比均匀采样效率高
	Trace a ray r(p, wi)
	If ray r hit a non-emitting object at q
		L_indir = shade(q, wi) * eval(ow, wi, N) * dot(wi, N) / pdf(wo, wi, N)
		 / RussainRoulette
		
		Return L_dir + L_indir

解析

shade(p, wo) 这个函数本质上是在数值求解渲染方程 $Lo(p,ωo)=L_{dir}+L_{indir}$
p 当前着色点(光线与物体的交点)
wo 从点 p 指向相机的出射方向(outgoing direction)

Task

本次实验中只需要修改一个函数 CastRay(const Ray ray, int depth) in scene.cpp

Final

  1. 结果图添加到 readme

    (我也要做一个酷酷的图)

    Snipaste_2026-01-06_01-25-21.png

  2. 多线程


check

// 光线与场景求交
  Intersection inter = Intersect(ray);

  if (!inter.happened)
    return Vector3f(0.0f);

  // 如果打到光源,直接返回光源的 emission
  // 也就是说我们直接看到光源本身的颜色
  if (inter.m->HasEmission())
    return inter.m->GetEmission();

  // ----------------------------------------------------------------------------: 直接光照
  Vector3f dir_light(0.0f);

  // 在光源上采样一个点
  Intersection light_pos;
  float light_pdf;
  SampleLight(light_pos, light_pdf);  // out parameters

  // 计算交点 p 到光源采样点的方向
  Vector3f p = inter.coords;
  Vector3f N = inter.normal;

  Vector3f light_dir = light_pos.coords - p;  // p 指向光源的向量
  float light_distance = light_dir.Norm();    // 距离
  light_dir = Normalize(light_dir);           // 单位向量 ws

  // 发射一条 shadow ray, 检查中间是否有遮挡物
  // 添加偏移避免自相交
  Ray shadow_ray(p + N * 0.0001f, light_dir);
  Intersection shadow_inter = Intersect(shadow_ray);

  // 如果没有遮挡,直接计算光照贡献
  // 注意:需要检查 shadow_inter.happened,否则 distance 可能未定义
  if (!shadow_inter.happened || shadow_inter.distance >= light_distance - 0.001f) {
    auto ws = light_dir;
    auto NN = light_pos.normal;  // 光源的法线
    auto wo = -ray.direction;    // 出射方向 (指向眼睛,相机)

    Vector3f brdf = inter.m->Eval(wo, ws, N);
    float cos_theta = std::max(0.0f, DotProduct(ws, N));          // 光线方向与交点法的夹角余弦
    float cos_theta_prime = std::max(0.0f, DotProduct(-ws, NN));  // 光线方向与光源法线的夹角余弦

    dir_light = light_pos.emit * brdf * cos_theta * cos_theta_prime / (light_distance * light_distance) / light_pdf;
  }

  // ----------------------------------------------------------------------------:  间接光照
  Vector3f light_indir(0.0f);

  // 俄罗斯轮盘赌决定是否继续追踪 (用随机数和 russian_roulette 0.8 比较)
  if (GetRandomFloat() < russian_roulette) {
    // 在加点处采样一个反射方向
    Vector3f wo = -ray.direction;
    Vector3f wi = inter.m->Sample(wo, N);  // 在半球上随机采样一个出射方向 wi

    // 发射发新的光线
    // 添加偏移避免自相交
    Ray new_ray(p + N * 0.0001f, wi);
    Intersection new_inter = Intersect(new_ray);

    // 如果打到了物体,且不是光源,计算间接光照
    if (new_inter.happened && !new_inter.m->HasEmission()) {
      float pdf = inter.m->Pdf(wo, wi, N);
      if (pdf > 0.001f) {  // 避免除以0
        Vector3f brdf = inter.m->Eval(wo, wi, N);
        float cos_theta = std::max(0.0f, DotProduct(wi, N));

        light_indir = CastRay(new_ray, depth + 1) * brdf * cos_theta / pdf / russian_roulette;
      }
    }
  } // end rr

  return dir_light + light_indir;