dielectric
水、玻璃、鑽石等透明材料都是電介質。當光線照射到它們身上時,它會分裂成反射光線和折射(透射)光線。我們將透過在反射和折射之間隨機選擇來處理這個問題,每次相互作用只產生一個散射射線。
折射程度 : 是根據兩個介質折射率的差值決定的。
Refraction
Snell's Law
$$
\eta \cdot \sin{\theta} = {\eta}' \cdot \sin{{\theta}'}
$$
$$
\sin{{\theta}'} = \frac{\eta}{{\eta}'} \sin{\theta}
$$
折射光線 ${R}'$ ,折射表面的法線 ${n}'$ 折射對應的折射角 $\theta'$ 我們將 $R'$ 分解為和 $n'$ 垂直和平行的
$$
R' = R'{\perp} + R'
$$
$$
R'_{\perp} = \frac{\eta}{\eta'}(R+\cos{\theta}n)
$$
$$
R'{\parallel} = - \sqrt{ 1 - \left| R' \right|^2 }n
$$
$\cos{\theta}$ 可以透過點乘得到。
$$
R'_{\perp} = \frac{\eta}{\eta'}(R+ (-R \cdot n) n)
$$
inline vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) {
auto cos_theta = fmin(dot(-uv, n), 1.0);
vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n);
vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_perp.length_squared())) * n;
return r_out_perp + r_out_parallel;
}
the dieliectric class
class dielectric : public material {
public:
dielectric(double refraction_index) : refraction_index(refraction_index) {}
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0);
double ri = rec.front_face ? (1.0/refraction_index) : refraction_index;
vec3 unit_direction = unit_vector(r_in.direction());
vec3 refracted = refract(unit_direction, rec.normal, ri);
scattered = ray(rec.p, refracted);
return true;
}
private:
// Refractive index in vacuum or air, or the ratio of the material's refractive index over
// the refractive index of the enclosing media
double refraction_index;
};
全反射
有一些光線角無法用斯涅爾定律求解。當光線以足夠的掠射角進入折射率較低的介質時它能以大於90度的角度折射.
如果
$$
\sin{{\theta}'} = \frac{1.5}{1.0} \sin{\theta} >0
$$
因此 角度 會出現問題。所以我們要判斷一下是否能夠反射
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
if (ri * sin_theta > 1.0) {
// Must Reflect
...
} else {
// Can Refract
...
}
這裡所有的光都被反射,因為在實踐中,這通常是在固體物體內部,所以它被稱為全內反射。這就是為什麼有時候當你在水下時,水與空氣的邊界就像一面完美的鏡子如果你在水下向上看,你可以看到水面上的東西,但是當你靠近水面側身看時,水面看起來就像一面鏡子。
class dielectric : public material {
public:
dielectric(double refraction_index) : refraction_index(refraction_index) {}
bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered)
const override {
attenuation = color(1.0, 1.0, 1.0);
double ri = rec.front_face ? (1.0/refraction_index) : refraction_index;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta*cos_theta);
bool cannot_refract = ri * sin_theta > 1.0;
vec3 direction;
if (cannot_refract)
direction = reflect(unit_direction, rec.normal);
else
direction = refract(unit_direction, rec.normal, ri);
scattered = ray(rec.p, direction);
return true;
}
private:
// Refractive index in vacuum or air, or the ratio of the material's refractive index over
// the refractive index of the enclosing media
double refraction_index;
};
Attenuation is always 1 — the glass surface absorbs nothing.
事實證明,給定一個折射率大於空氣的材料球體,沒有任何入射角能產生完全的內反射無論是在射線球體的入口還是在射線球體的出口。這是由於球體的幾何形狀,因為入射光線總是彎曲到一個較小的角度,然後在出口時彎曲回原來的角度。
那麼我們如何說明全內反射呢?如果球體的折射率小於它所處的介質,那麼我們就可以用淺掠角撞擊它,得到全外反射。這應該足以觀察到效果。
我們將模擬一個充滿水的世界(折射率約為1.33),並將球體材料更改為空氣(折射率1.00)一個氣泡!要做到這一點,改變左邊球體材料的折射率為
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left = make_shared<dielectric>(1.00 / 1.33);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
Schlick 近似
用克里斯托弗·施裡克(Christophe Schlick)的一個便宜而驚人準確的多項式近似。這就產生了我們的全玻璃材料
讓我們建造一個空心玻璃球。這是一個厚度的球體,內部有另一個空氣。如果您想到射線穿過這樣的物體的路徑,它將擊中外部球體,折射,擊中內部球(假設我們確實擊中了它),第二次折射,然後穿過內部的空氣。然後,它將繼續前進,擊中內部球體的內部表面,向後折射,然後擊中外部球體的內表面,最後折射並退回到場景大氣中。
外球只是用標準玻璃球對其進行建模,其折射率約為1.50(將外部空氣從外部空氣中建模為玻璃)。內部球體有些不同,因為其折射率應相對於周圍外球的物質,從而建模從玻璃向內空氣的過渡。這實際上很容易指定,因為Refraction_Index引數與介電材料可以解釋為物件的折射率的比率除以封閉介質的折射率。在這種情況下,內部球體將在玻璃的折射率(封閉介質)或1.00/1.50 = 0.67上具有空氣(內部球體材料)的折射率(內部球體材料)或者 0.67
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left = make_shared<dielectric>(1.50);
auto material_bubble = make_shared<dielectric>(1.00 / 1.50);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 0.0);
world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.2), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.4, material_bubble));
world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right));
可移動攝像頭
光線總 origin 出發,指向 -z,同時 z = -2 就是平面
$h = \tan{\frac{\theta}{2}}$
class camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
int samples_per_pixel = 10; // Count of random samples for each pixel
int max_depth = 10; // Maximum number of ray bounces into scene
double vfov = 90; // Vertical view angle (field of view)
void render(const hittable& world) {
...
private:
...
void initialize() {
image_height = int(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
pixel_samples_scale = 1.0 / samples_per_pixel;
center = point3(0, 0, 0);
// Determine viewport dimensions.
auto focal_length = 1.0;
auto theta = degrees_to_radians(vfov);
auto h = tan(theta/2);
auto viewport_height = 2 * h * focal_length;
auto viewport_width = viewport_height * (double(image_width)/image_height);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
auto viewport_u = vec3(viewport_width, 0, 0);
auto viewport_v = vec3(0, -viewport_height, 0);
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
pixel_delta_u = viewport_u / image_width;
pixel_delta_v = viewport_v / image_height;
// Calculate the location of the upper left pixel.
auto viewport_upper_left =
center - vec3(0, 0, focal_length) - viewport_u/2 - viewport_v/2;
pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
}
...
};
int main() {
hittable_list world;
auto R = cos(pi/4);
auto material_left = make_shared<lambertian>(color(0,0,1));
auto material_right = make_shared<lambertian>(color(1,0,0));
world.add(make_shared<sphere>(point3(-R, 0, -1), R, material_left));
world.add(make_shared<sphere>(point3( R, 0, -1), R, material_right));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.vfov = 90;
cam.render(world);
}
定位相機
相機位置 : lookfrom , look at: lookat
我們還需要一種方法來指定相機的滾動或側向傾斜:圍繞look -look - from軸的旋轉。另一種思考方式是,即使你一直不停地看,你仍然可以在鼻子周圍旋轉你的頭。我們需要的是一種為相機指定向上向量的方法。
我們可以指定任何向上的向量,只要它不平行於檢視方向。將此向上向量投影到與檢視方向正交的平面上,以獲得與相機相關的向上向量。我使用將其命名為檢視向上(vup)向量的通用約定。經過一些叉乘和向量歸一化之後,我們現在有了一個完整的標準正交基 $(u,v,w)$ 用來描述相機的方向。
- u : 單位向量, point to right
- v : 單位向量, point to up
- w :單位向量,指向與視野方向相反的方向
class camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
int samples_per_pixel = 10; // Count of random samples for each pixel
int max_depth = 10; // Maximum number of ray bounces into scene
double vfov = 90; // Vertical view angle (field of view)
point3 lookfrom = point3(0,0,0); // Point camera is looking from
point3 lookat = point3(0,0,-1); // Point camera is looking at
vec3 vup = vec3(0,1,0); // Camera-relative "up" direction
...
private:
int image_height; // Rendered image height
double pixel_samples_scale; // Color scale factor for a sum of pixel samples
point3 center; // Camera center
point3 pixel00_loc; // Location of pixel 0, 0
vec3 pixel_delta_u; // Offset to pixel to the right
vec3 pixel_delta_v; // Offset to pixel below
vec3 u, v, w; // Camera frame basis vectors
void initialize() {
image_height = int(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
pixel_samples_scale = 1.0 / samples_per_pixel;
center = lookfrom;
// Determine viewport dimensions.
auto focal_length = (lookfrom - lookat).length();
auto theta = degrees_to_radians(vfov);
auto h = tan(theta/2);
auto viewport_height = 2 * h * focal_length;
auto viewport_width = viewport_height * (double(image_width)/image_height);
// Calculate the u,v,w unit basis vectors for the camera coordinate frame.
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge
vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge
// Calculate the horizontal and vertical delta vectors from pixel to pixel.
pixel_delta_u = viewport_u / image_width;
pixel_delta_v = viewport_v / image_height;
// Calculate the location of the upper left pixel.
auto viewport_upper_left = center - (focal_length * w) - viewport_u/2 - viewport_v/2;
pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
}
...
private:
};
散焦模糊
現在我們的最後一個功能:散焦模糊。注意,攝影師稱之為景深,所以一定要在你的光線追蹤朋友中只使用術語散焦模糊。
我們在真實相機中出現散焦模糊的原因是,它們需要一個大洞(而不僅僅是針孔)來收集光線。一個大洞會使所有東西散焦,但如果我們把鏡頭放在膠片/感測器前面,就會有一定的距離使所有東西都聚焦。放置在該距離處的物件將顯示在焦點上,並且距離該距離越遠,它們將線性地顯示得越模糊。你可以這樣想透鏡:所有來自焦距處特定點的光線——以及照射到透鏡上的光線——都會向後彎曲到影像感測器上的一個點。
我們把相機中心到物體完全對焦的平面之間的距離稱為焦距。請注意,焦距通常與焦距不同,焦距是相機中心和影像平面之間的距離。然而,對於我們的模型,這兩個將具有相同的值,因為我們將畫素網格放在焦平面上,焦平面是離相機中心的焦距。
在物理相機中,焦距由鏡頭和膠片/感測器之間的距離控制。這就是為什麼當你改變焦點時,你會看到鏡頭相對於相機移動(這也可能發生在你的手機相機中,但感測器會移動)。“光圈”是一個孔,用來有效地控制鏡頭的大小。對於真正的相機,如果你需要更多的光,你會使光圈更大,並且遠離焦距的物體會變得更模糊。對於我們的虛擬相機,我們可以有一個完美的感測器,而且永遠不需要更多的光,所以我們只在需要散焦模糊時使用光圈。
薄透鏡近似
對於我們的程式碼,我們可以模擬順序:感測器,然後鏡頭,然後光圈。然後我們就可以計算出光線的傳送位置,並在計算後翻轉影像(影像在膠片上倒過來投影)。然而,圖形人員通常使用薄透鏡近似
我們不需要為了渲染相機外部的影像而模擬相機內部的任何東西,那將是不必要的複雜性。相反,我通常從一個無限薄的圓形鏡頭開始光線,並將它們傳送到焦平面上感興趣的畫素(距鏡頭的焦距),在3D世界中,該平面上的所有東西都是完美聚焦的。
在實踐中,我們透過將視口放置在這個平面中來實現這一點。把所有東西放在一起
- 聚焦平面與相機視角方向垂直
- 聚焦距離是相機中心到聚焦平面之間的距離。
- 視口位於聚焦平面上,以相機檢視方向向量為中心。
- 畫素位置的網格位於視口中(位於3D世界中)。
- 從當前畫素位置周圍的區域中選擇隨機影像樣本位置。
- 相機從鏡頭上的隨機點發射光線,穿過當前影像樣本位置。
沒有散焦模糊,所有場景光線都來自相機中心(或從)。為了實現離焦模糊,我們在相機中心構造了一個圓盤。半徑越大,散焦模糊越大。你可以想象我們的原始相機有一個半徑為零的離焦磁碟(沒有模糊),所以所有的光線都來自磁碟中心(從上看)。
那麼,散磁碟應該多大?由於此磁碟的大小控制了我們得到多少散焦模糊,因此應該是相機類的引數。我們可以將磁碟的半徑作為攝像機引數,但模糊會根據投影距離而有所不同。一個稍微更容易的引數是在攝像頭中心指定視口中心和基座(Defocus磁碟)的錐角度的角度。當您改變給定鏡頭的重點距離時,這應該為您提供更一致的結果。
...
inline vec3 unit_vector(const vec3& u) {
return v / v.length();
}
// 僅僅是二維的
inline vec3 random_in_unit_disk() {
while (true) {
auto p = vec3(random_double(-1,1), random_double(-1,1), 0);
if (p.length_squared() < 1)
return p;
}
}
...
lass camera {
public:
double aspect_ratio = 1.0; // Ratio of image width over height
int image_width = 100; // Rendered image width in pixel count
int samples_per_pixel = 10; // Count of random samples for each pixel
int max_depth = 10; // Maximum number of ray bounces into scene
double vfov = 90; // Vertical view angle (field of view)
point3 lookfrom = point3(0,0,0); // Point camera is looking from
point3 lookat = point3(0,0,-1); // Point camera is looking at
vec3 vup = vec3(0,1,0); // Camera-relative "up" direction
double defocus_angle = 0; // Variation angle of rays through each pixel
double focus_dist = 10; // Distance from camera lookfrom point to plane of perfect focus
...
private:
int image_height; // Rendered image height
double pixel_samples_scale; // Color scale factor for a sum of pixel samples
point3 center; // Camera center
point3 pixel00_loc; // Location of pixel 0, 0
vec3 pixel_delta_u; // Offset to pixel to the right
vec3 pixel_delta_v; // Offset to pixel below
vec3 u, v, w; // Camera frame basis vectors
vec3 defocus_disk_u; // Defocus disk horizontal radius
vec3 defocus_disk_v; // Defocus disk vertical radius
void initialize() {
image_height = int(image_width / aspect_ratio);
image_height = (image_height < 1) ? 1 : image_height;
pixel_samples_scale = 1.0 / samples_per_pixel;
center = lookfrom;
// Determine viewport dimensions.
auto focal_length = (lookfrom - lookat).length();
auto theta = degrees_to_radians(vfov);
auto h = tan(theta/2);
auto viewport_height = 2 * h * focus_dist;
auto viewport_width = viewport_height * (double(image_width)/image_height);
// Calculate the u,v,w unit basis vectors for the camera coordinate frame.
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
// Calculate the vectors across the horizontal and down the vertical viewport edges.
vec3 viewport_u = viewport_width * u; // Vector across viewport horizontal edge
vec3 viewport_v = viewport_height * -v; // Vector down viewport vertical edge
// Calculate the horizontal and vertical delta vectors to the next pixel.
pixel_delta_u = viewport_u / image_width;
pixel_delta_v = viewport_v / image_height;
// Calculate the location of the upper left pixel.
auto viewport_upper_left = center - (focus_dist * w) - viewport_u/2 - viewport_v/2;
pixel00_loc = viewport_upper_left + 0.5 * (pixel_delta_u + pixel_delta_v);
// Calculate the camera defocus disk basis vectors.
auto defocus_radius = focus_dist * tan(degrees_to_radians(defocus_angle / 2));
defocus_disk_u = u * defocus_radius;
defocus_disk_v = v * defocus_radius;
}
ray get_ray(int i, int j) const {
// Construct a camera ray originating from the defocus disk and directed at a randomly
// sampled point around the pixel location i, j.
auto offset = sample_square();
auto pixel_sample = pixel00_loc
+ ((i + offset.x()) * pixel_delta_u)
+ ((j + offset.y()) * pixel_delta_v);
auto ray_origin = (defocus_angle <= 0) ? center : defocus_disk_sample();
auto ray_direction = pixel_sample - ray_origin;
return ray(ray_origin, ray_direction);
}
vec3 sample_square() const {
...
}
point3 defocus_disk_sample() const {
// Returns a random point in the camera defocus disk.
auto p = random_in_unit_disk();
return center + (p[0] * defocus_disk_u) + (p[1] * defocus_disk_v);
}
color ray_color(const ray& r, int depth, const hittable& world) const {
...
}
};