Skip to content

Lecture 16: Ray Tracing 4(Monte Carlo Path Tracing)

Monte Carlo Integration

蒙特卡洛积分是一种通过随机采样来估计积分值的方法。对于一个函数\(f(x)\)在区间\([a, b]\)上的积分,在\(n\)次独立均匀采样下,蒙特卡洛积分的估计值为:

\[ I \approx \frac{b-a}{n} \sum_{i=1}^{n} f(x_i) \]

更泛用地,Monte Carlo estimator为:

\[ F_n = \frac{1}{n} \sum_{i=1}^{n} \frac{f(X_i)}{p(X_i)} \]

其中,\(X_i\)是根据概率密度函数\(p(x)\)采样得到的随机变量。

则:

\[ \int f(x) dx = \frac{1}{n} \sum_{i=1}^{n} \frac{f(X_i)}{p(X_i)} \]

Path Tracing

路径追踪(Path Tracing)是一种基于蒙特卡洛积分的全局光照计算方法。它通过模拟光线在场景中的传播路径,来估计每个像素的颜色值。

路径追踪用于解决老光线追踪无法处理的glossy material(半光泽材质)和diffuse material(漫反射材质)的问题。Whitted-Style Ray Tracing含有积分和递归,在实际计算时会截取递归深度。

A Simple Solution

根据渲染方程:

\[ L_o(p, \omega_o) = L_e(p, \omega_o) + \int_{\Omega^+} f_r(p, \omega_i, \omega_o) L_i(p, \omega_i) \cos\theta_i d\omega_i \]

再根据蒙特卡洛积分:

\[ f(x) = f_r(p, \omega_i, \omega_o) L_i(p, \omega_i) \cos\theta_i \]
\[ p(\omega_i) = \frac{1}{2\pi} \]

则有:

\[ L_o(p, \omega_o) =\int_{\Omega^+} f_r(p, \omega_i, \omega_o) L_i(p, \omega_i) \cos\theta_i d\omega_i \approx \frac{1}{N} \sum_{i=1}^{N} \frac{f_r(p, \omega_i, \omega_o) L_i(p, \omega_i) \cos\theta_i}{p(\omega_i)} \]

其中,\(N\)是采样数量,\(\omega_i\)是根据概率密度函数\(p(\omega_i)\)采样得到的入射方向。

根据以上可以以设计一个简单的路径追踪算法:

C++
shade(p, wo){
    Randomly choose N directions wi~pdf;
    L_o = 0;
    For each wi{
        Trace a ray r(p, wi);
        If (ray r hits light source){
            L_o += (1 / N) * L_i f_r * cosine / pdf(wi);
        }
    }
    return L_o;
}

以上算法可以解释为:随机选择N个入射方向,对每个方向追踪一条光线,如果光线击中光源,则计算该方向的贡献,计算的原理是基于渲染方程和蒙特卡洛积分,最后将所有方向的贡献累加,得到最终的出射辐射亮度。

以上只涉及了直接光照的计算,间接光照的计算需要递归地追踪反射和折射光线。

C++
shade(p, wo){
    Randomly choose N directions wi~pdf;
    L_o = 0;
    For each wi{
        Trace a ray r(p, wi);
        If (ray r hits light source){
            L_o += (1 / N) * L_i f_r * cosine / pdf(wi);
        }else if (ray r hits object){
            L_o += (1 / N) * shade(hitPoint, -wi) * f_r * cosine / pdf(wi);
        }
    }
    return L_o;
}

shade(hitPoint, -wi)表示在交点处递归地计算出射辐射亮度,-wi表示光线的反方向,即视角方向。

但是以上算法存在一些问题:当多次计算间接光源时,计算量会指数级爆炸。为了解决这个问题,在计算光照时,可以只随机选择一个方向进行追踪,而不是N个方向。

C++
shade(p, wo){
    Randomly choose 1 direction wi~pdf;
    Trace a ray r(p, wi);
    if (ray r hits light source){
        L_o = L_i * f_r * cosine / pdf(wi);
    }else if (ray r hits object){
        L_o = shade(hitPoint, -wi) * f_r * cosine / pdf(wi);
    }
    return L_o;
}

但是这种方法会导致很多噪音,因为每次只采样一个方向,可能会错过重要的光照贡献。为了解决这个问题,可以增加采样数量N,在每个像素上多次采样,然后对结果进行平均。

C++
ray_generation(camPos, pixel){
    Uniformly sample N times in the pixel;
    pixel_radiance = 0;
    For each sample{
        Shoot a ray r(camPos, pixel);
        if(ray hit the scene at p){
            pixel_radiance += 1 / N * shade(p, sample_to_cam);
        }
    }
    return pixel_radiance;
}

但以上还有严重问题,即可能会陷入无限递归。为了解决这个问题,可以设置最大递归深度,或者使用Russian Roulette方法随机终止路径。

假设使用Russian Roulette方法:

C++
shade(p, wo){
    Manually specify a probability P_RR;
    Randomly choose a number xi in [0, 1];
    if(xi > P_RR) return 0;
    Randomly choose 1 direction wi~pdf;
    Trace a ray r(p, wi);
    if (ray r hits light source){
        L_o = L_i * f_r * cosine / (pdf(wi) * P_RR);
    }else if (ray r hits object){
        L_o = shade(hitPoint, -wi) * f_r * cosine / (pdf(wi) * P_RR);
    }
    return L_o;
}

其中,\(P_{RR}\)是Russian Roulette的概率,决定了路径继续追踪的概率。通过这种方法,可以在保持无偏性的同时,减少计算量。

但这个方法在实际应用中仍然会有噪音问题(尤其是每像素样本数较少时)。为了解决噪音问题,可以只对光源采样,减少大量的不必要采样。(其实还可以用重要性采样Importance Sampling,甚至多重重要性采样Multiple Importance Sampling)

需要将\(d\omega\)转化为\(dA\)

\[ d\omega = \frac{dA \cos\theta_A}{r^2} \]

则:

\[ L_o(p, \omega_o) =\int_{A} f_r(p, \omega_i, \omega_o) L_i(p, \omega_i) \cos\theta_i \frac{\cos\theta_A}{r^2} dA \]

因此,最终的做法是,对光源进行均匀采样,对非光源进行Russian Roulette采样。

C++
shade(p, wo){
    Uniformly sample the light at x_light (pdf_light = 1 / A);
    L_dir = L_i * f_r * cosine_i * cosine_light / |p - x_light|^2 / pdf_light;

    L_indir = 0;
    Test Russian Roulette with probability P_RR;
    Uniformly sample the hemisphere toward wi (pdf_hemi = 1 / 2pi);
    Trace a ray r(p, wi);
    if (ray r hits non-emitting object){
        L_indir = shade(hitPoint, -wi) * f_r * cosine / pdf_hemi / P_RR;
    }
    return L_dir + L_indir;
}

以上还需要补充一个点,即光源采样时需要考虑遮挡问题。如果光源被其他物体遮挡,则该方向的贡献为零。

C++
shade(p, wo){
    Uniformly sample the light at x_light (pdf_light = 1 / A);
    Trace a shadow ray r(p, x_light);
    if (ray r is not blocked){
        L_dir = L_i * f_r * cosine_i * cosine_light / |p - x_light|^2 / pdf_light;
    }else{
        L_dir = 0;
    }

    L_indir = 0;
    Test Russian Roulette with probability P_RR;
    Uniformly sample the hemisphere toward wi (pdf_hemi = 1 / 2pi);
    Trace a ray r(p, wi);
    if (ray r hits non-emitting object){
        L_indir = shade(hitPoint, -wi) * f_r * cosine / pdf_hemi / P_RR;
    }
    return L_dir + L_indir;
}