0%

九:磨砂(下)

浮点数精度

看到标题,我们就知道上一章画面太暗的罪魁祸首是谁了吧,生成反射光线的起点是碰撞点,但是因为浮点数并不能精确的等于某个数,比如double d = 0;,实际的项目运行过程中,d不会精确等于0,而是会等于诸如-0.00000083之类的逼近0的小数。

浮点数精度问题会导致一部分光线的起点在球的内部,这样发射光线,光线会和球的内壁碰撞,然后在球内反复弹射,耗尽自己的一生。

虽然可以通过微移光线起点的方式来解决,但是这样会把问题复杂化。别忘了我们在写hit函数的时候,留有限制t的参数t_min和t_max。我们完全可以使用t_min的限制,让光线自动忽略那些和发射点很近的物体。

1
if (world.hit(r, 0.001, infinity, rec)) {...}

图片变得正常了!

image-20220309090857497

伽马矫正

人的眼睛并不是精准的机器,它对亮度的感知和实际能量的功率是不成线性函数关系的,而是幂函数关系,这个函数的指数通常为2.2,称为Gamma值。

也就是说,如果光线真的是每次碰撞到物体都衰减一半的能量,那对于百分之50功率的灰色,人眼实际感受到的亮度为$\sqrt[2.2]{0.5}=0.77297$,是一种偏向于白色的淡灰色。

而人眼中的中灰色,实际上是功率只有img

为了适应人眼去纠正光线的能量,让它符合人眼生物学中的颜色,这叫做伽马矫正。为了方便,我们并不需要那么精准,我们使用”Gamma 2”矫正,即直接对最后的颜色值开方,在write_color函数中,有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();

//Gamma矫正(Gamma = 2.0)。
auto scale = 1.0 / samples_per_pixel;
r = sqrt(scale * r);
g = sqrt(scale * g);
b = sqrt(scale * b);

out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}

这样的图像看起来会更亮了,也更接近真实世界我们看到的漫反射材质

image-20220309091636573

更真实的漫反射

之前的随机的向量更倾向于接近法线,并不能做到完全随机,接下来介绍一种单位球表面随机选点模型——又称真实兰伯特模型(true Lambertian),它是最贴合现实世界物理规律的漫反射材质的反射方向的模型。

很简单,我们只需要把体积的随机改成面积的随机即可,我们在单位球的表面随机选点。

image-20220309132029289

所以在vec3.h文件中加入全局函数:

1
2
3
4
5
6
inline vec3 random_in_unit_sphere() {
...
}
vec3 random_unit_vector() {
return unit_vector(random_in_unit_sphere());
}

修改ray_color函数中的代码:

1
2
3
4
5
6
7
8
9
10
11
color ray_color(const ray& r, const hittable& world, int depth) {

...

if (world.hit(r, 0.001, infinity, rec)) {
// 这次改为在球面上取点。
point3 target = rec.p + rec.normal + random_unit_vector();
...
}
...
}

就可以得到这张图片:

image-20220309132224913

半球选点模型

这一种最容易想到的模型(我脑海里也是第一时间想到这个),很多早期的光线追踪论文使用的是这样一种模型。

在以碰撞点P为球心的单位半球内找点,取点半球和表面法线在面的同侧。继续添加一个vec3.h的全局函数。

1
2
3
4
5
6
7
8
9
vec3 random_in_hemisphere(const vec3& normal) {
vec3 in_unit_sphere = random_in_unit_sphere();

// 如果该向量和法线夹角为锐角,即在面的同侧,接受它,否则取反。
if (dot(in_unit_sphere, normal) > 0.0)
return in_unit_sphere;
else
return -in_unit_sphere;
}

同时更改ray_color中的调用

1
point3 target = rec.p + random_in_hemisphere(rec.normal);

image-20220309092409576

拓展

  1. 利用这一节的代码加工一下我们就可以制作各种各样的有趣的图片了:
    1. 太极image-20220309204931529
    2. 炽日image-20220309204955574
    3. 蛋灯image-20220309205203309
  2. 了解光栅化光照模型是如何实现漫反射的。

参考文献

https://raytracing.github.io/books/RayTracingInOneWeekend.html

参考自《RayTracingInOneWeekend》第8.3节到8.6节。