11. 电介质
类似水,玻璃和钻石等透明的材质都是电介质。当光线碰到它们时,会分成反射光线和折射(透射)光线。我们将在反射和折射之间随机选择来处理这个问题,每次交互只生成一条散射射线。
这里快速回顾一些术语,反射光线指的是光线入射表面,然后从新的方向“反弹”离开表面。
折射光线在从一种介质进入另一种介质(如玻璃或水)时方向会发生改变。这就是为什么铅笔部分插入水中时看起来会弯曲的原因。
折射光线弯曲程度是由介质的折射率决定的。一般地,这是一个用来描述从光线从真空中进入介质时,光线的弯曲程度的值。玻璃的折射率在1.5-1.7之间,钻石在2.4附近,而空气的折射率是1.000293。
当光线从一种透明介质嵌入到另一种透明介质中时,可以用相对折射率来描述折射现象:即使用包围介质的折射率除以被包围介质的折射率。例如,当想要渲染一个水下的玻璃球时,玻璃球的相对折射率是1.125。这是由水的折射率(1.333)除以玻璃的折射率(1.5)得到的。
关于大多数常见介质的折射率,可以通过网上搜索得到。
11.1. 折射
最难调试的部分是折射光线。如果存在折射光线,我通常会先让所有光线发生折射。在这个项目中,我尝试在场景中放置两个玻璃球,结果是这样的(我还没有告诉你这样做的对错,但很快就会告诉你!):
是这样吗?玻璃球在现实生活中看起来很奇怪。但不,这是不对的。世界应该是颠倒的,不应该有奇怪的黑色东西。我刚把射线从图像中间直接打印出来,结果明显不对。这通常就能解决问题。
11.2. 折射定律(Shell Law)
折射规律可以通过斯涅尔定律描述:
其中和分别是入射光线与法线的夹角以及折射光线与法线的夹角。和分别是第一介质和第二介质的折射率。(通常空气=1.0,玻璃=1.3-1.7,钻石=2.4)。光路图如下:
为了确定折射光线的方向,我们必须求解:
在接触表面折射的这一侧,有一条折射光线和法线,它们之间的角度是。我们可以把分解成平行于和垂直于的部分:
如果我们求解和,可得:*(译者注:这里有必要解释一下,是与法线垂直的部分。是与法线平行的部分。
如果你想的话,你可以自己证明这个等式,但我们将把它当作事实并继续。本书的其余部分不需要你理解这个证明。
我们知道右侧每个项的值,除了。众所周知,两个向量的点积可以用它们之间的夹角的余弦来解释:
如果我们限制和为单位向量,则:
我们现在可以用已知量重写的求解公式:
当我们将它们重新组合在一起时,我们可以编写一个函数来计算:
...
inline vec3 reflect(const vec3& v, const vec3& n) {
return v - 2*dot(v,n)*n;
}
inline vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) {
auto cos_theta = std::fmin(dot(-uv, n), 1.0);
vec3 r_out_perp = etai_over_etat * (uv + cos_theta*n);
vec3 r_out_parallel = -std::sqrt(std::fabs(1.0 - r_out_perp.length_squared())) * n;
return r_out_perp + r_out_parallel;
}
发生折射的材质可以实现如下:
...
class metal : public material {
...
};
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;
};
现在我们将更新场景,将左侧的球体更改为折射率为1.5的玻璃:
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_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
渲染结果如下:
11.3. 全反射
折射的一个麻烦的问题是,对于某些光线角度,使用斯涅尔定律无法求解。当光线以足够的入射角进入折射率较低的介质时,它可以以大于90°的角度进行折射。如果我们回顾一下斯涅尔定律和的推导:
如果光线在玻璃内,而外部介质是空气( 以及 )
而的值不能大于1.0
方程两边之间的相等性被打破,解不存在。如果不存在解决方案,则玻璃无法折射,因此必须反射光线
if (ri * sin_theta > 1.0) {
// Must Reflect
...
} else {
// Can Refract
...
}
在这里,所有的光都被反射了,因为在实践中,这通常在固体物体内部,所以它被称为全内反射。这就是为什么有时当您浸入水中时,水与空气的边界就像一面完美的镜子——如果你在水下仰望,你可以看到水面上的东西,但是当你靠近水面朝着与水面较为平行的方向看时,水面看起来像一面镜子。
我们可以使用三角函数的特性求解:
以及:
double cos_theta = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::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 = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::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;
};
衰减始终为1——玻璃材质不吸收任何光线。
如果我们使用新的dielectric::scatter()函数渲染前一个场景,我们会看到 ...没有变化。怎么回事呢?
嗯,事实证明,给定一个折射率大于空气的材质球体,无论是在射入球的入口点还是在射线出口处都不会发生全反射。这是由于球体的几何形状决定的,因为入射光线将始终弯曲到较小的角度,然后在出口时弯曲回原始角度。
因此,我们如何表现全反射呢?如果球体的折射率小于它所在的介质,那么我们可以用与入射平面较小角度的入射光线撞击它,获得全反射。这应该足以观察效果。
我们假设整个世界充满了水(折射率大约是1.33),改变球体的材质为空气(折射率为1.0)——Oh,一个泡泡!为了做到这一点,依据下面的公式修改左边球体的折射率:
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);
修改后渲染效果如下:
你可以看到或多或少的直达光线会折射,而与平面夹角较小的光线会反射。
11.4. Schlick近似
真正的玻璃具有随观察角度变化的反射率——以与观察表面较平行的方向看窗户时,它就会变成一面镜子。有一个大而丑陋的方程式可以描述这一特点,但几乎每个人都使用Christophe Schlick的一个性能较优且非常准确的多项式近似。基于此可以实现我们的玻璃材质:
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 = std::fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = std::sqrt(1.0 - cos_theta*cos_theta);
bool cannot_refract = ri * sin_theta > 1.0;
vec3 direction;
if (cannot_refract || reflectance(cos_theta, ri) > random_double())
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;
static double reflectance(double cosine, double refraction_index) {
// Use Schlick's approximation for reflectance.
auto r0 = (1 - refraction_index) / (1 + refraction_index);
r0 = r0*r0;
return r0 + (1-r0)*std::pow((1 - cosine),5);
}
};
11.5. 模拟空心玻璃球
下面让我们渲染一个空心玻璃球。这是一个具有一定厚度的球体,里面有另一个空气球体。如果考虑光线穿过此类对象的路径,它将撞击外部球体、折射、撞击内部球体(假设我们确实击中它)、第二次折射,并穿过内部的空气。然后它将继续前进,撞击内球体的内表面,折射回来,然后撞击外球体的内表面,最后折射并退出到场景大气中。
外球体使用标准玻璃球体建模,折射率约为 1.50(模拟从外部空气到玻璃中的折射)。内球体有点不同,因为它的折射率应该是相对于外球体的材质的,是玻璃到内空气的过渡。
这实际上很容易指定,因为介质的refraction_index参数可以解释为物体的折射率除以外包围介质的折射率之比。在这种情况下,内球体的折射率是空气(内球体材质)的折射率除以玻璃(外包围介质)的折射率,即。
下面是代码实现:
...
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));
...
渲染结果如下:
12. 可定位相机
相机和电介质一样难以调试,所以我总是逐步开发我自己的相机。首先,让我们与许一个可调整的视野(fov)。这是从渲染图像的某一边到对边的视角。由于我们的图像不是正方形的,因此fov在水平和垂直方向上是不同的。我总是使用垂直 fov。我通常也以度数为单位指定它,并在构造函数中更改为弧度——这纯属个人喜好。
12.1. 相机视锥的几何表示
首先,我们保持射线从原点发出,并朝向的平面。如下图所示,只要我们让它和成比例,我们可以朝任意平面发射光线。
如果,这意味着。相机的实现如下:
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 = std::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);
}
...
};
我们将会使用简单的场景来测试上面的代码,场景中有两个相切的球,并设置fov为90度。
int main() {
hittable_list world;
auto R = std::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);
}
渲染效果如下:
12.2. 相机的位置和朝向
为了获得一个任意的视点,让我们首先说出我们关心的点。我们将放置摄像机的位置称为lookfrom,将我们注视的点称为lookat。(后续可以定义为要看的方向,而不是要看的点)。
我们还需要一种指定相机的滚动或侧向倾斜的方法:围绕lookat-lookfrom轴的旋转。另一种思考方式是,即使你保持lookfrom和lookat不变,你仍然可以将头绕着鼻子旋转。我们需要一种方法来为相机指定“up”向量。
我们可以指定任何我们想要的上方向,只要它不平行于视图方向即可。将此上方向向量投影到与视线方向正交的平面上,以获得相对于摄像机的上方向向量。我使用的常见惯例将其命名为“view up”(vup)向量。经过一些叉积和向量归一化,我们现在有一个完整的正交基 来描述相机的方向。是指向相机右侧的单位向量,是指向相机向上的单位向量,是指向视图方向相反的单位向量(因为我们使用右侧坐标),相机中心位于原点。
和以前一样,当我们的固定相机面向时,我们的任意视图相机面向。请记住,我们可以,但不是必须使用world up(0,1,0)来指定vup。这很方便,并且会自然地使您的相机保持水平,直到您决定尝试疯狂的相机角度。
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 = std::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:
};
我们把场景改回去,并使用新的视角渲染:
int main() {
hittable_list world;
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), 1.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));
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.lookfrom = point3(-2,2,1);
cam.lookat = point3(0,0,-1);
cam.vup = vec3(0,1,0);
cam.render(world);
}
渲染效果如下:
修改FOV参数为20:
cam.vfov = 20;
渲染后得到:
13. 焦散模糊(Defocus Blur)
我们最后一个功能:焦散模糊。摄影师们通常将其称为“景深”,所以请确保只在学习光线追踪的朋友中使用“焦散模糊”的术语。
真实相机中出现散焦模糊的原因是因为它们需要一个比较大光圈(而不仅仅是一个针孔)来聚焦光线。大光圈会使所有物体失焦,但如果我们在胶片/传感器前面放一个镜头,在一定的距离内所有物体都会聚焦。位于该距离的物体将会清晰可见,距离越远,物体就会显得越模糊。您可以想象这样的镜头:来自焦距处特定点的所有光线(照射到镜头上)都会弯曲回到图像传感器上的某个点。
我们将相机中心与让所有物体完美对焦的平面之间的距离称为“焦距”。请注意,焦距通常与注视距离(focal length)不同——注视距离是相机中心与图像平面之间的距离。然而,对于我们的模型,这两个值将相同,因为我们将把像素网格平面放在焦平面上,与相机中心的距离为一个“焦距”。
在物理相机中,焦距由镜头和胶片/传感器之间的距离控制。这就是为什么当你改变焦点时,你会看到镜头相对于相机移动的原因(这在你的手机相机中也可能发生,但却是传感器在移动)。“光圈”是一个孔,用于控制镜头的有效大小。对于真正的相机,如果你需要更多的光线,你可以把光圈调大,这样远离焦距的物体就会变得更加模糊。对于我们的虚拟相机,我们可以有一个完美的传感器,并且永远不需要更多的光线,所以我们只在想要散焦模糊时使用光圈。
13.1. 薄镜片的近似
真正的相机有一个复杂的复合镜头。对于我们的代码,我们可以模拟以下顺序:传感器,然后是镜头,然后是光圈。然后我们可以找出将光线发送到哪里,并在计算后翻转图像(图像在胶片上是倒过来的)。然而,图形人员通常使用薄透镜近似:
我们不需要模拟相机内部的任何部分,因为那将带来不必要的复杂性,只需要关注相机外部需要渲染的图像。相反,我通常会从无限薄的圆形“透镜”开始发出光线,并将它们发送到位于焦平面上相应的像素(与透镜的距离为“focal_length”),3D世界中该平面上的所有内容都完美对焦。
实际上,我们通过将视口放置在焦平面上来实现这一点。把所有内容总结如下:
- 焦平面(forcus plane)与相机视线方向正交。
- 焦距(forcus distance)是相机中心与焦平面之间的距离。
- 视口位于焦平面上,以相机视线方向矢量为中心。
- 像素格子位于视口内(3D 世界中)
- 从当前像素位置周围的区域中随机选择样本位置
- 相机从镜头(lens)上的随机点发射射线,穿过当前图像采样位置。
译者注:焦散模糊是这么理解的,光线原本聚焦于相机内部一点,而焦散模糊的时候,光线打在相机内部一个平面上的(上图中的 lens )。
13.2. 生成采样光线
如果没有焦散模糊,所有场景光线都源自相机中心(或“lookfrom”)。为了实现焦散模糊,我们构建了一个以相机中心为中心的圆盘。半径越大,焦散模糊越大。您可以将我们的原始相机想象为具有半径为零的焦散圆盘(完全没有模糊),因此所有光线都源自圆盘中心(lookfrom)。
那么,焦散盘应该有多大?由于此盘的大小控制着我们获得的焦散模糊程度,因此它应该是相机类的一个参数。我们可以将盘的半径作为相机参数,但模糊会根据投影距离而变化。一个稍微简单的参数是指定锥体的角度,其顶点位于视口中心,底部(焦散盘)位于相机中心。当您改变给定镜头的焦距时,这应该会为您提供更一致的结果。
由于我们将从焦散盘中选择随机点,因此我们需要一个函数来执行此操作:random_in_unit_disk()。此函数使用的方法与我们在random_in_unit_sphere()中使用的方法相同,区别在于它只针对二维平面。
...
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;
}
}
...
现在让我们更新相机以从焦散盘发出光线:
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
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 = std::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 * std::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 {
...
}
};
使用大光圈:
int main() {
...
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.max_depth = 50;
cam.vfov = 20;
cam.lookfrom = point3(-2,2,1);
cam.lookat = point3(0,0,-1);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 10.0;
cam.focus_dist = 3.4;
cam.render(world);
}
渲染结果如下:
14. 那么接下来呢?
14.1. 最后的场景
让我们制作这本书封面上的图像——许多随机的球体。
int main() {
hittable_list world;
auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5));
world.add(make_shared<sphere>(point3(0,-1000,0), 1000, ground_material));
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
auto choose_mat = random_double();
point3 center(a + 0.9*random_double(), 0.2, b + 0.9*random_double());
if ((center - point3(4, 0.2, 0)).length() > 0.9) {
shared_ptr<material> sphere_material;
if (choose_mat < 0.8) {
// diffuse
auto albedo = color::random() * color::random();
sphere_material = make_shared<lambertian>(albedo);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
} else if (choose_mat < 0.95) {
// metal
auto albedo = color::random(0.5, 1);
auto fuzz = random_double(0, 0.5);
sphere_material = make_shared<metal>(albedo, fuzz);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
} else {
// glass
sphere_material = make_shared<dielectric>(1.5);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
}
}
}
}
auto material1 = make_shared<dielectric>(1.5);
world.add(make_shared<sphere>(point3(0, 1, 0), 1.0, material1));
auto material2 = make_shared<lambertian>(color(0.4, 0.2, 0.1));
world.add(make_shared<sphere>(point3(-4, 1, 0), 1.0, material2));
auto material3 = make_shared<metal>(color(0.7, 0.6, 0.5), 0.0);
world.add(make_shared<sphere>(point3(4, 1, 0), 1.0, material3));
camera cam;
cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 1200;
cam.samples_per_pixel = 500;
cam.max_depth = 50;
cam.vfov = 20;
cam.lookfrom = point3(13,2,3);
cam.lookat = point3(0,0,0);
cam.vup = vec3(0,1,0);
cam.defocus_angle = 0.6;
cam.focus_dist = 10.0;
cam.render(world);
}
(请注意,上述代码与项目示例代码略有不同:上面的 samples_per_pixel 设置为 500,以获得需要相当长一段时间才能渲染的高质量图像。项目源代码使用值10,以便在开发和验证时获得合理的运行时间。)
渲染结果如下:
你可能会注意到一件有趣的事情,那就是玻璃球实际上没有阴影,这使得它们看起来像是漂浮着的。这不是Bug——你在现实生活中很少看到玻璃球,它们看起来也有点奇怪,而且在阴天确实看起来像是漂浮着的。玻璃球下大球体上的点仍然有大量的光线照射到它,因为有大量光线仍然来自天空。
14.2. 下一步计划
现在,你拥有了一个很酷的光线追踪渲染器,那么接下来呢?
14.2.1. 第二本书: Ray Tracing: The Next Week
该系列的第二本书,将会基于这本书开发的光线追踪渲染器进行。将会包括以下新的功能实现:
- 运动模糊——真实地渲染运动中的物体
- BVH——加速渲染场景的速度
- 纹理映射——在物体表面放置纹理
- 柏林噪声——对于很对技术而言,一个随机的噪声生成器会很有用
- 四边形——除了球体之外,再渲染更多的其他物体。此外,它是实现磁盘、三角形、环或几乎任何其他二维图元的基础。
- 光照——往场景中增加光源
- Transforms——对于放置和旋转物体很有用
- 体渲染——渲染烟,云和其他气态的体积
14.2.2. 第三本书: Ray Tracing: The Rest of Your Life
这本书再次扩展第二本书的内容。这本书的很多内容都是关于提高渲染图像质量和渲染器性能,并侧重于生成正确的射线并适当地累积它们。
这本书适合那些对编写专业级光线跟踪非常感兴趣,和/或对实现次表面散射或嵌套电介质等高级效果的基础感兴趣的读者。
14.2.3. 其他指导
您可以从这里获取很多其他指导,包括我们在本系列中尚未介绍的技术。这些包括:
- 三角形——大多数酷炫的模型都是三角形网格。模型I/O是最糟糕的,几乎每个人都试图让别人的代码来做这件事。这还包括高效处理大型三角形网格的技术,这本身颇具挑战。
- 并行性——使用不同的随机种子在N个核心上运行N份代码。对N次运行取平均值。取平均值也可以分层进行,其中可以对 N/2 对图像取平均值以获得N/4个图像,然后对这些图像取平均值。这种并行方法应该可以很好地扩展到数千个核心,并且只需很少的编码。
- 阴影射线——当向光源发射光线时,您可以准确确定特定点的阴影情况。这样,您可以渲染清晰或柔和的阴影,为场景增添另一层真实感。
玩得开心,请将你的精彩图片发送给我!