目前为止,如果你放大渲染出的图像,可能会注意到图像边缘的明显“阶梯状”效果。这种阶梯效果通常被称为“走样”或“锯齿”。当真实相机拍摄图片时,边缘通常没有锯齿,因为边缘像素是一些前景和一些背景的混合。请考虑,与我们渲染的图像不同,真实世界的图像是连续的。换句话说,世界(及其真实图像)具有无限的分辨率。我们可以通过对每个像素进行多次采样来获得相同的效果。
通过每个像素中心传递单一光线,我们正在进行常见的点采样。点采样的问题可以通过远处渲染一个小的棋盘格来说明。如果该棋盘由一个8×8的黑白格子组成,但只有四条光线射到它上面,那么这四条光线可能只会与白色格子相交,或只与黑色格子相交,或者是一些奇怪的组合。在真实世界中,当我们用眼睛远处观察一个棋盘时,我们会感知到它是灰色的,而不是黑白的尖点。那是因为我们的眼睛自然而然地完成了我们希望光线追踪完成的任务:将落在所渲染图像的特定(离散的)区域上的(连续函数的)光线进行积分。
显然,通过多次对像素中心进行相同光线的重新采样,并不能带来任何好处——我们每次都会得到相同的结果。相反,我们想要采样像素周围的光线,然后对这些采样进行积分以近似真正的连续结果。那么,我们如何积分像素周围的光线呢?
我们将采用最简单的模型:采样以像素为中心、向四个相邻像素各自延伸一半距离的正方形区域。这并不是最优的方法,但它是最直接的方法。(请参阅 A Pixel is Not a Little Square 以深入了解此主题。)
Figure 8: Pixel samples
Some Random Number Utilities
我们需要一个能返回真正随机数的随机数生成器。这个函数应该返回一个经典的随机数,按照惯例应在0≤n<1的范围内。其中“小于”符号在1之前很重要,因为有时我们会利用这一点。
实现这个的一个简单方法是使用在 <cstdlib> 中找到的 rand() 函数,它返回0到RAND_MAX 之间的随机整数。因此,我们可以使用以下代码片段将其转换为所需的真正随机数,并将其添加到 rtweekend.h 中:
#include <cmath>
#include <cstdlib>
#include <limits>
#include <memory>
...
// Utility Functions
inline double degrees_to_radians(double degrees) {
return degrees * pi / 180.0;
}
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();
}
Listing 36: [rtweekend.h] random_double() functions
传统上,C++ 并没有一个标准的随机数生成器,但是较新版本的 C++ 通过 <random> 头文件解决了这个问题(尽管根据一些专家的说法,它并不完美)。如果你想使用它,你可以按照以下方式获取满足我们需求的随机数:
#include <random>
inline double random_double() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}
Listing 37: [rtweekend.h] random_double(), alternate implemenation
8.2 Generating Pixels with Multiple Samples
对于由多个样本组成的单个像素,我们将从像素周围的区域选择样本,并将得到的光(颜色)值进行平均。
首先,我们将更新write_color()函数以考虑我们使用的样本数:我们需要找到所有取样的平均值。为此,我们将在每次迭代中添加完整的颜色,然后在最后进行一次除法(除以样本数),然后再写入颜色。为了确保最终结果的颜色分量保持在正确的[0,1]范围内,我们将添加并使用一个小的辅助函数:interval::clamp(x),用于限制值x在指定范围内。
class interval {public:...bool surrounds(double x) const {return min < x && x < max;}
double clamp(double x) const {if (x < min) return min;if (x > max) return max;return x;}
...
};
Listing 38: [interval.h] The interval::clamp() utility function
以下是更新后的write_color()函数,它接受像素的所有光的总和和涉及的样本数量:
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.
static const interval intensity(0.000, 0.999);
out << static_cast<int>(256 * intensity.clamp(r))<<' '
<< static_cast<int>(256 * intensity.clamp(g))<<' '
<<static_cast<int>(256 * intensity.clamp(b))<<'\n';
}
Listing 39: [color.h] The multi-sample write_color() function
现在让我们更新相机类,定义并使用一个新的camera::get_ray(i,j)函数,该函数将为每个像素生成不同的样本。该函数将使用一个新的辅助函数pixel_sample_square(),该函数生成一个随机样本点,在以原点为中心的单位正方形内。然后,我们将这个随机样本从理想的正方形转换回当前正在采样的特定像素。
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 pixelvoid render(const hittable& world) {initialize();std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";for (int j = 0; j < image_height; ++j) {std::clog << "\rScanlines remaining: " << (image_height - j) << ' '<< std::flush;for (int i = 0; i < image_width; ++i) {color pixel_color(0,0,0);for (int sample = 0; sample < samples_per_pixel; ++sample){ray r = get_ray(i, j);pixel_color += ray_color(r, world);}write_color(std::cout, pixel_color, samples_per_pixel );}}std::clog << "\rDone. \n";}
...
private:
...
void initialize() {...
}ray get_ray(int i, int j) const {// Get a randomly sampled camera ray for the pixel at location i,j.auto pixel_center = pixel00_loc + (i * pixel_delta_u) + (j * pixel_delta_v);auto pixel_sample = pixel_center + pixel_sample_square();auto ray_origin = center;auto ray_direction = pixel_sample - ray_origin;return ray(ray_origin, ray_direction);}vec3 pixel_sample_square() const {// Returns a random point in the square surrounding a pixel at the origin.auto px = -0.5 + random_double();auto py = -0.5 + random_double();return (px * pixel_delta_u) + (py * pixel_delta_v);}...
};#endif
Listing 40: [camera.h] Camera with samples-per-pixel parameter
(除了上面的新的pixel_sample_square()函数之外,在Github源代码中还可以找到pixel_sample_disk()函数。这是为了在非正方形像素上进行实验而包含的,但在本书中我们不会使用它。pixel_sample_disk()依赖于稍后定义的random_in_unit_disk()函数。)
更新Main函数以设置新的相机参数。
int main() {
...
camera cam;cam.aspect_ratio = 16.0 / 9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100;
cam.render(world);
}
Listing 41: [main.cc] Setting the new samples-per-pixel parameter
放大图像后,我们可以看到边缘像素的差异。
Image 6:抗锯齿前后对比。