0%

34:初始图形学(7)

上一章完成了相机类的实现,对之前所学的内容进行了封装与整理,现在要学习新的内容。

抗锯齿

我们放大之前渲染的图片,往往会发现我们渲染的图像边缘有尖锐的”阶梯”性质。这种阶梯状被称为”锯齿”。当真实的相机拍照时,边缘通常没有锯齿,这是因为真实的边缘时前景和背景颜色的混合,而不是简单的二值化。而且真实的世界是连续的,而我们渲染的图像是离散的,也就是说真实世界具有无限的分辨率 ,而我们的=图像的分辨率是有限的。我们可以通过对每个像素进行多次采样并取平均值,来近似实现此效果。

我们目前采用的采样方式被称为”点采样“,即在每个像素的中心发射一条射线来进行采样,但是同时也面临一个问题,当我们对较远的地方的图像进行采样时,可能会出现”非黑即白”的情况。但实际上我们应该看到的是灰色,而不是黑白分明的点。这是我们的眼睛很自然的对远处的图形进行了处理,而这种处理正是我们想要的效果。

所以为了解决这个问题,我们采用”多采样”的方式,来对我们的图片实现采样。我们需要对像素周围的光线进行采样,然后对采样的结果进行整合,以近似真实世界的连续效果。

为了实现这种效果,我们采用最为简单的方式,我们对以像素为中心的正方形区域进行采样,这个正方形区域会延伸到每个相邻的像素的一般位置。这个方法可能效果一般,但是便于实现,具体的内容可以参考文献像素不是一个小方块,下面是一个多采样草图

image.png

数学工具:随机数生成

为了实现函数的多采样,我们需要一个能够返回真实随机数的随机数生成器。这个函数为我们返回一个(0,1)的随机数,我们可以使用标准库<cstdlib>中的std::rand()函数,这个函数会返回一个[0,RAND_MAX]之间的随机整数。我们通过以下改动,可以得到真正的随机数函数,我们写在rtweekend.h中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <limits>
#include <memory>
...
//实用函数
inline double degree_to_radius(double degrees){
return degrees * pi / 180.0;
}
inline double random_double(){
//返回[0,1)的数
return std::rand() / (RAND_MAX + 1.0);
}
inline double random_double(double min,double max){
//返回[min,max)的数
return min + (max - min)*random_double();
}

不过由于rand()具有随机性较差等特点,所以在C++11标准下有其他的随机数函数写法:

1
2
3
4
5
6
7
8
9
...
#include <random>
...
inline double random_double(){
static std::uniform_real_distribution<double> distribution(0.0,1.0);
static std::mt19937 generator;
return distribution(generator);
}
...

不过看不太懂就是了

使用多采样式生成像素

对于由多个样本组成的像素,我们将从像素周围的区域选择样本,并将颜色(光值)平均在一起

我们需要更新我们的write_color()函数以考虑我们的样本数量,不过在此之前,我们需要添加一个用于区间判断的辅助函数interval::clamp(x),以确保最终的结果的颜色分量保持在正确的[0,1]范围:

1
2
3
4
5
6
7
8
9
class interval{
public:
...
//包含
double clamp(double x) const{
if(x < min) return min;
if(x > max) return max;
return x;
}

接下来我们更新write_color()函数,其包含区间的限制功能:

1
2
3
4
5
6
7
8
9
10
11
  void write_color(std::ostream& out,const color& pixel_color){
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
//使用区间RGB[0,1]计算RGB值
static const interval intensity(0.000,0.999);
int rbyte = int (256*intensity.clamp(r));
int gbyte = int (256*intensity.clamp(g));
int bbyte = int (256*intensity.clamp(b));
out << rbyte << ' ' << gbyte << ' ' << bbyte << '\n';
}

这样保证了我们的的rgb分量不会超出[0,1]的范围,这样更加安全。

接下来我们需要更新相机类,以定义和使用一个新的camera::get_ray(i,j)函数,该函数将为每个像素生成不同的样本。这个函数将使用一个新的辅助函数sample_squred(),该函数在以原点为中心的正方形内生成一个随机样本点。然后我们将这个正方形中的随机样本转换为我们当前正在采样的特定像素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

#ifndef RENDER_C___CAMERA_H
#define RENDER_C___CAMERA_H

#include "hittable.h"

class camera{
public:
double aspect_radio = 1.0; //图像的宽高比
int image_width = 100; //图像宽度的像素数
int samples_per_pixel = 10; //每个像素的采样次数

void 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*pixel_samples_scale);
}
}
std::clog << "\rDone. \n";
}

private:
int image_height; //渲染图像的高度
double pixel_samples_scale; //每次采样的颜色权重
point3 camera_center; //相机的中心
point3 pixel00_loc; //像素(0,0)的位置
vec3 pixel_delta_u; //向右的偏移值
vec3 pixel_delta_v; //向下的偏移值

void initialize(){
image_height = int(image_width/aspect_radio);
image_height = (image_height < 1) ? 1 : image_height;

pixel_samples_scale = 1.0 / samples_per_pixel;

camera_center = point3 (0,0,0);

//确认视窗的设置
auto focal_length = 1.0; //焦距设置
auto viewport_height = 2.0;
auto viewport_width = viewport_height*(double (image_width)/image_height);

//视图边缘的向量计算
auto viewport_u = vec3(viewport_width,0,0);
auto viewport_v = vec3(0,-viewport_height,0);
//计算视图的像素间的水平竖直增量
pixel_delta_u = viewport_u/image_width;
pixel_delta_v = viewport_v/image_height;

//计算左上角第一个像素中心的坐标
auto viewport_upper_left = camera_center - vec3(0,0,focal_length) - viewport_v/2 - viewport_u/2;
pixel00_loc = viewport_upper_left + 0.5*(pixel_delta_u+pixel_delta_v);
}

ray get_ray(int i,int j){
//构造一个从原点开始的随机采样射线,指向(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 = camera_center;
auto ray_direction = pixel_sample - ray_origin;

return ray(ray_origin,ray_direction);
}

vec3 sample_square() const {
//返回一个一个随机的点,在[-0.5,-0.5]~[+0.5,+0.5]的单位矩阵内
return {random_double() - 0.5, random_double() - 0.5,0};
}

color ray_color(ray & r,const hittable& world){
hit_record rec;
if(world.hit(r,interval(0,infinity),rec)){
return 0.5*(rec.normal + color(1,1,1));
}

vec3 unit_direction = unit_vector(r.direction());
auto a = 0.5*(unit_direction.y()+1.0);
return (1.0 - a)*color(1.0,1.0,1.0) + a*color(0.5,0.7,1.0);
}
};

#endif //RENDER_C___CAMERA_H

这是新的camera,我们更新了get_ray()sample_square(),还有新的公有私有属性

接下来设置一下主函数的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

int main(){
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 cam;

cam.aspect_radio = 16.0/9.0;
cam.image_width = 400;
cam.samples_per_pixel = 100; // 设置采样次数

cam.render(world);
}
image.png

这里可以看到左图的锯齿明显减少了,我们的抗锯齿设置的十分成功,今天就到此为止了