本項目參考自教程《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
其中,分別是入射角和折射角,分別是兩種介質的折射率。
已知入射光線向量和法向量,折射光線向量的計算公式如下:
其中,
至於這個公式的推導過程,有興趣可以參考:折射向量的推導
寫成代碼如下:
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近似法求反射比的近似解:
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));