《用兩天學習光線追蹤》8.折射向量和電介質

本項目參考自教程《Ray Tracing in One Weekend》,在跑通了所有例子之後,加上了自己的理解寫成筆記,項目使用CPU多線程提速,並增加了GUI進度顯示。
項目鏈接:https://github.com/maijiaquan/ray-tracing-with-imgui

目錄:
《用兩天學習光線追蹤》1.項目介紹和ppm圖片輸出
《用兩天學習光線追蹤》2.射線、簡單相機和背景輸出
《用兩天學習光線追蹤》3.球體和表面法向量
《用兩天學習光線追蹤》4.封裝成類
《用兩天學習光線追蹤》5.抗鋸齒
《用兩天學習光線追蹤》6.漫反射材質
《用兩天學習光線追蹤》7.反射向量和金屬材質
《用兩天學習光線追蹤》8.折射向量和電介質
《用兩天學習光線追蹤》9.可放置相機
《用兩天學習光線追蹤》10.散焦模糊

《用一週學習光線追蹤》1.動態模糊
《用一週學習光線追蹤》2.BVH樹、AABB相交檢測
《用一週學習光線追蹤》3.純色紋理和棋盤紋理
《用一週學習光線追蹤》4.柏林噪聲
《用一週學習光線追蹤》5.球面紋理貼圖
《用一週學習光線追蹤》6.光照和軸對齊矩形
《用一週學習光線追蹤》7.長方體和平移旋轉


本節內容

本節將會實現折射向量和電介質材質的光線追蹤,例如玻璃材質。

本節代碼:main8.cpp


折射向量

對於折射,有個著名的公式:Snell’s law

sinθ1sinθ2=η2η1 \dfrac{\sin\theta_1}{\sin\theta_2} = \dfrac{\eta_2}{\eta_1}
其中,θ1,θ2\theta_1,\theta_2分別是入射角和折射角,η1,η2\eta_1, \eta_2分別是兩種介質的折射率。

已知入射光線向量II和法向量NN,折射光線向量TT的計算公式如下:
T=η(I+c1N)Nc2 T = \eta(I + c_1 N) - N c_2

其中,
η=η1η2c1=NIc2=1η2(1c12) \begin{array}{l} \eta = \dfrac{\eta_1}{\eta_2}\\ c_1 = N \cdot I\\ c_2 = \sqrt{1 - \eta^2 (1 - c_1^2)} \end{array}
至於這個公式的推導過程,有興趣可以參考:折射向量的推導
寫成代碼如下:

bool refract(const vec3& v, const vec3& n, float ni_over_nt, vec3& refracted) {
    vec3 uv = unit_vector(v);
    float dt = dot(uv, n);
    float discriminant = 1.0 - ni_over_nt*ni_over_nt*(1-dt*dt);
    if (discriminant > 0) {
        refracted = ni_over_nt*(uv - n*dt) - n*sqrt(discriminant);
        return true;
    }
    else
        return false;
}

電介質

玻璃、水、鑽石都是電介質,當光線通過它們內部,會發生折射(refraction),同時也會發生反射(reflection)。

例如當我們遠處看玻璃窗戶的時候,基本就是透明的,但當我們貼着窗戶看(視線和玻璃平面夾角很小)的時候,窗戶能夠像鏡子一樣反射一部分光。
如何計算反射和折射光強的比重呢?菲涅耳方程(Fresnel Formula)可以用來計算描述光在不同介質之間的反射比和透射比,但菲涅爾方程相當複雜,我們可以使用Fresnel-Schlick近似法求反射比的近似解:
R(θ)=R0+(1R0)(1cosθ)5 R(\theta) = R_0 + (1 - R_0) ( 1 - \cos\theta)^5

R0=(n1n2n1+n2)2R_0 = (\frac{n_1-n_2}{n_1+n_2})^2

Schlick近似函數如下:

float schlick(float cosine, float n1_over_n2) {
    float r0 = (1-n1_over_n2) / (1+n1_over_n2);
    r0 = r0*r0;
    return r0 + (1-r0)*pow((1 - cosine),5);
}

實現電介質類

如果是從裏到外入射,即入射向量在法向量另外一側的情況:

則要對法向量取反,並顛倒裏外的折射率之比:

class dielectric : public material
{
public:
    dielectric(float ri) : ref_idx(ri) {} //n2/n1
    virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const
    {
        vec3 outward_normal;
        vec3 reflected = reflect(r_in.direction(), rec.normal);
        float ni_over_nt;
        attenuation = vec3(1.0, 1.0, 1.0);
        vec3 refracted;

        float reflect_prob; //反射概率
        float cosine;

        if (dot(r_in.direction(), rec.normal) > 0) //從裏到外,即入射向量在法向量另外一側的情況
        {
            outward_normal = -rec.normal;   //對法向量取反
            ni_over_nt = ref_idx;           
            cosine = ref_idx * dot(r_in.direction(), rec.normal) / r_in.direction().length();
            //不知道爲什麼這裏要乘一個ref_idx
        }
        else    //從外到裏,即入射向量在法向量同一側
        {
            outward_normal = rec.normal;    //法向量不變
            ni_over_nt = 1.0 / ref_idx;     
            cosine = -dot(r_in.direction(), rec.normal) / r_in.direction().length();
        }

        if (refract(r_in.direction(), outward_normal, ni_over_nt, refracted))
        {
            reflect_prob = schlick(cosine, ref_idx);
        }
        else    //若無折射,則全反射
        {
            reflect_prob = 1.0;
        }

        if (random_double() < reflect_prob)
        {
            scattered = ray(rec.p, reflected);
        }
        else
        {
            scattered = ray(rec.p, refracted);
        }

        return true;
    }

    float ref_idx;
};

修改入口函數

list[0] = new sphere(vec3(0, 0, -1), 0.5, new lambertian(vec3(0.1, 0.2, 0.5)));
...
list[3] = new sphere(vec3(-1, 0, -1), 0.5, new dielectric(1.5));

參考資料:《Ray Tracing in One Weekend》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章