想看原书可以看本系列的第一篇《Ray Tracing in a Weekend》学习笔记01
7. Antialiasing
这章要解决边缘锯齿的问题。
7.2 Some Random Number Utilities
首先需要的是一个返回真实随机数的随机数生成器。制作一个返回规范随机数的函数,该函数通常会返回0<=r<1范围内的随机实数。
//rtweekend.h random_double() functions
#include <cstdlib>
...
inline double random_double() {
// Returns a random real in [0,1).
return rand() / (RAND_MAX + 1.0);
}
inline double random_double(double min, double max) {
// Returns a random real in [min,max).
return min + (max-min)*random_double();
}
C ++传统上没有标准的随机数生成器,但是较新版本的C ++已使用标头解决了此问题。 如果要使用此函数,则可以按照以下条件获得随机数:
//rtweekend.h random_double(), alternate implemenation
#include <random>
inline double random_double() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}
7.2 Generating Pixels with Multiple Samples
上次的运行结果能很明显的看到球面的边缘很锐利。我们一直都是发射一条射线去扫描单个像素,以达到采样的目的,很明显这是不够的。为了让边缘更平滑,可以向同一个像素用更多的射线去采样。红色框表示一个像素,用更多的射线去扫描该像素,原先只是选择一个射线,扫描的是像素的中心位置进行计算,得到像素值,用它啦代替整个方框的颜色。现在就是让方框中出现有更多的点来决定方框的颜色。
现在需要一个摄像机类来管理摄像机和场景漫游的任务。下面这个类使用之前的轴对齐相机实现了一个简单相机。
//camera.h The camera class
#ifndef CAMERA_H
#define CAMERA_H
#include "rtweekend.h"
class camera {
public:
camera() {
auto aspect_ratio = 16.0 / 9.0;
auto viewport_height = 2.0;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.0;
origin = point3(0, 0, 0);
horizontal = vec3(viewport_width, 0.0, 0.0);
vertical = vec3(0.0, viewport_height, 0.0);
lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length);
}
ray get_ray(double u, double v) const {
return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};
#endif
有了camera.h就可以控制多条射线。为了处理多次采样的颜色计算,需要更新write_color()函数。在每次迭代的过程中增加全色,最后根据采样的样本数做一次除法,来写入颜色。在rtweeken.h中添加一个函数clamp(x,min,max),将x的值限制在(min,max)中。
//rtweekend.h The clamp() utility function
inline double clamp(double x, double min, double max) {
if (x < min) return min;
if (x > max) return max;
return x;
}
//color.h The multi-sample write_color() function
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();
// Divide the color by the number of samples.
auto scale = 1.0 / samples_per_pixel;
r *= scale;
g *= scale;
b *= scale;
// Write the translated [0,255] value of each color component.
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';
}
此时,main.c也需要修改:
//main.cc Rendering with multi-sampled pixels
#include "camera.h"
...
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
// World
hittable_list world;
world.add(make_shared<sphere>(point3(0,0,-1), 0.5));
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100));
// Camera
camera cam;
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
color pixel_color(0, 0, 0);
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width-1);
auto v = (j + random_double()) / (image_height-1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}
很明显可以看到变化,边缘不再那么锐利,边缘处的像素显得更为模糊,达到了抗锯齿的效果。
8 Diffuse Materials
我们现在已经有对象,并且每个像素都有多条光线采样。现在可以通过光线追踪制作一些逼真的材质。从漫反射材料开始。
8.1 A Simple Diffuse Material
不发光的漫反射对线仅具有周围环境的颜色,但会使用其自身的固有颜色来调和这些颜色。漫反射表面反射的光是随机的。比如我们将三条光线发送到一个漫反射表面,它们反射回来的方向都是随机的。
它们也可能是被吸收而不是被反射。表面越暗,吸收的可能性就越大。这也是为什么它很暗。任何随机化方向的算法都会产生看起来很粗糙的表面。 最简单的方法之一是理想的漫反射表面。
①从摄像机位置(eye)发射一条射线,交球面与p点。在p点做两个与球面相切的单位球体,这两个单位球体的球心就在P+n和P-n(其中P表示eye->p向量,n表示p点的法向量)。以P-n为球心的单位球体在表面内部,P+n在外部。选择与射线方向在同一侧的单位球体,在其中随机选择一个点s,然后将一条光线从命中点p发送到s(用向量表示为S-P)。
选择s点的方法,使用最简单的拒绝方法。在单位立方体里面选择一个随机点,其中x,y,z都控制在(-1,1)中。如果该点在单位球体之外拒绝该点。
//vec3.h vec3 random utility functions
class vec3 {
public:
...
inline static vec3 random() {
return vec3(random_double(), random_double(), random_double());
}
inline static vec3 random(double min, double max) {
return vec3(random_double(min,max), random_double(min,max), random_double(min,max));
}
//vec3.h The random_in_unit_sphere() function
vec3 random_in_unit_sphere() {
while (true) {
auto p = vec3::random(-1,1);
if (p.length_squared() >= 1) continue;
return p;
}
}
更新ray_color()函数来使用新的随机方向生产器:
//main.c ray_color() using a random ray direction
color ray_color(const ray& r, const hittable& world) {
hit_record rec;
if (world.hit(r, 0, infinity, rec)) {
point3 target = rec.p + rec.normal + random_in_unit_sphere();
return 0.5 * ray_color(ray(rec.p, target - rec.p), world);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
8.2 Limiting the Number of Child Rays
ray_color()是递归的。当射线无法击中任何物体时,要停止递归。但可能会递归很长时间,直到破坏堆栈。为了防止这种事情发生,我们需要限制最大的递归深度,在最大深度不返回任何光源。
//main.c ray_color() with depth limiting
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0, infinity, rec)) {
point3 target = rec.p + rec.normal + random_in_unit_sphere();
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
...
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
const int max_depth = 50;
...
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
color pixel_color(0, 0, 0);
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width-1);
auto v = (j + random_double()) / (image_height-1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world, max_depth);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}