0%

39:初始图形学(11)

今日收官之战,图形学之旅差不多到此为止了

可定位相机

我们的相机目前是固定视角的,比较单调。由于程序的复杂性,所以在这里我们最后实现它。首先,我们需要为我们的相机添加视场(fov)效果。这是从渲染图像一边到另外一边的视觉角度。由于我们的图像不是正方形视图,所以水平和垂直的fov是不同的。这里我们选择开发垂直fov。我们用度数来指定它,然后再构造函数中将其转换为弧度。

计算机视几何

我们继续保持从原点发出的光线,指向z轴上的平面,然后使h与这个距离的比例保持一致:

image.png

其中theta是我们的视野,即fov,这里表示h = tan(theta/2)

我们将其应用到我们的相机类中:

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
class camera{
public:
double aspect_radio = 1.0; //图像的宽高比
int image_width = 100; //图像宽度的像素数
int samples_per_pixel = 10; //每个像素的采样次数
int max_depth = 10; //光线追踪的最大递归深度

double vfov = 90; //垂直视角(视场)
...
private:
...
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 theta = degree_to_radius(vfov);
auto h = std::tan(theta/2);
auto viewport_height = 2*h*focal_length;
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);
}
...
};

这个原理就是实现广角,图像的宽高比不变,但是视口的大小改变了,使得图像有拉伸压缩的视觉效果

我们用广角来渲染一下我们之前的图片试试,发现渲染出来的效果和之前是一样的,这是为什么呢? 因为之前设置的视口高度为2.0,而焦距为1.0,所以计算可以得到当时的视角也是90°,自然和现在是一样的了,同理我们可以通过缩小垂直视野来看到更远处的东西,这就是放大缩小的原理

摄像机的定位和定向

如果我们想要任意摆放,获得任意的视角,我们首先需要确定两个点,一个是我们放置计算机的点lookfrom,还有一个是我们想要看到点lookat

然后我们还需要定义一个方向作为摄像机的倾斜角度,即lookat-lookfrom轴的旋转。但其实还有一种方法,我们保持lookfromlookat两点不变,然后定义一个向上的方向向量,作为我们的摄像方向。

image.png

我们可以指定任何我们想要的向上向量,只要它不与视图方向平行。将这个向上向量投影到与视图方向垂直的平面上,以获得相对于摄像机的向上向量。我们将其命名为vup即向上向量。我们可以通过叉乘和向量的单位化,得到一个完整的正交归一基,然后我们就可以用(u,v,w)来描述摄像机的方向了。

这里u指的是摄像机右侧的单位向量,v指的是摄像机向上的单位向量,w指的是视图方向相反的单位向量,摄像机中心位于原点

image.png

之前我们需要让-Z和-w在同一水平面,以实现水平摄像叫角度,现在我们只需要指定我们的向上向量vup指向(0,1,0)就可以保持摄像机的水平了

我们将这些功能加入相机类中:

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
class camera{
public:
double aspect_radio = 1.0; //图像的宽高比
int image_width = 100; //图像宽度的像素数
int samples_per_pixel = 10; //每个像素的采样次数
int max_depth = 10; //光线追踪的最大递归深度

double vfov = 90; //垂直视角(视场)
point3 lookfrom = point3 (0,0,0); //相机位置
point3 lookat = point3 (0,0,-1); //观察点
vec3 vup = vec3(0,1,0); //相机相对向上的位置
...
private:
int image_height; //渲染图像的高度
double pixel_samples_scale; //每次采样的颜色权重
point3 camera_center; //相机的中心
point3 pixel00_loc; //像素(0,0)的位置
vec3 pixel_delta_u; //向右的偏移值
vec3 pixel_delta_v; //向下的偏移值
vec3 u,v,w; //相机的相对坐标系

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 = lookfrom;

//确认视窗的设置
auto focal_length = (lookfrom - lookat).length(); //焦距设置
auto theta = degree_to_radius(vfov);
auto h = std::tan(theta/2);
auto viewport_height = 2*h*focal_length;
auto viewport_width = viewport_height*(double (image_width)/image_height);

//计算摄像机的相对基底
w = unit_vector(lookfrom-lookat);
u = unit_vector(cross(vup,w));
v = cross(w,u);;

//视图边缘的向量计算
auto viewport_u = viewport_width * u;
auto viewport_v = viewport_height * -v;
//计算视图的像素间的水平竖直增量
pixel_delta_u = viewport_u/image_width;
pixel_delta_v = viewport_v/image_height;

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

然后我们在main函数中调整我们的视角:

1
2
3
4
5
6
7
8
int main(){
...
cam.vfov = 90;
cam.lookfrom = point3 (-2,2,1);
cam.lookat = point3 (0,0,1);
cam.vup = vec3 (0,1,0);
...
}
image.png

然后我们发现渲染出来的图片不是很清晰,我们可以通过修改垂直视野来放大

1
cam.vfov = 20;
image.png

太好看了

散焦模糊

最后,我们还需要实现摄像机的一个特性:散焦模糊。在摄影中,我们把这种效果称之为景深。

在真实相机中,散焦模糊的产生是因为相机需要一个较大的孔(光圈)来收集光线,而不是一个针孔。如果只有一个针孔,那么所有物体都会清晰地成像,但进入相机的光线会非常少,导致曝光不足。所以相机会在胶片/传感器前面加一个镜头,那么会有一个特定的距离,在这个距离上看到的物体都是清晰的,然后离这个距离越远,图像就越模糊。你可以这么理解镜头:所有从焦点距离的特定点发出的光线——并且击中镜头——都会弯曲回图像传感器上的一个单一点。

在这里我们需要区分两个概念:

  • 焦距:相机中心到视口的距离,焦距决定了视场的大小。焦距越长,视场越窄。
  • 焦点距离:焦点距离是相机中心到焦点平面的距离,在焦点平面上的所有物体看起来都是清晰的

不过这里为了简化模型,我们将焦点平面和视口平面重合。

“光圈”是一个孔,用于控制镜头的有效大小。对于真正的摄像机,如果你需要更多的光线,你会使光圈变大,这将导致远离焦距的物体产生更多的模糊。在我们的虚拟摄像机中,我们可以拥有一个完美的传感器,永远不需要更多的光线,所以我们只在想要产生失焦模糊时使用光圈。

薄透镜近似

真实的相机镜头十分复杂,有传感器,镜头,光圈,胶片等…

image.png

实际上,我们不需要模拟相机内的任何一个部分,这些对于我们而言太过复杂,我们可以简化这个过程。我们将其简化成:我们从一近似平面的圆形”透镜”发出光线,并将它们发送的焦点平面的对应点上(距离透镜focal_length),在这个平面上的3D世界中的所有物体都处于完美的焦点中。

image.png

我们将这个过程展示出来:

  • 焦平面和相机视向垂直
  • 焦距是相机中心与焦点平面之间的距离
  • 视口位于焦点平面上,位于相机视角方向向量为中心
  • 像素位置的网格位于视场内
  • 从当前像素位置周围的区域随机采样(抗锯齿)
  • 相机从镜头上的随机点发射光线,通过图像样本位置

生成样本光线

没有散焦模糊时,所有的场景光线都来自相机中心(lookfrom)。为了实现散焦模糊,我们在相机中心构造一个圆盘。半径越大,散焦模糊越明显。你可以把我们的原始相机想想象成一个半径为0的散焦圆盘,所以完全不模糊。

所以我们将散焦盘的设置作为相机类的一个参数。我们将其半径作为相机系数,同时还需注意一点,相机的焦点距离也会影响散焦模糊的效果。此时,为了控制散焦模糊的程度,可以选择以下两种方式:

  • 散焦圆盘的半径:但是散焦模糊的效果会随着焦点距离的改变而被影响
  • 锥角:指定一个锥角,锥的顶点位于视口中心,底面位于相机中心,我们可以通过计算得到相应的底面半径

由于我们将从失焦盘中选择随机点,我们需要一个函数来完成这个任务random_in_unit_disk()这个和我们在random_in_unit_sphere()用到方法一样,只不过这个是二维的:

1
2
3
4
5
6
7
8
//vec3.h
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;
}
}

现在我们更新相机,加入失焦模糊的功能:

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
class camera{
public:
double aspect_ratio = 1.0; //图像的宽高比
int image_width = 100; //图像宽度的像素数
int samples_per_pixel = 10; //每个像素的采样次数
int max_depth = 10; //光线追踪的最大递归深度

double vfov = 90; //垂直视角(视场)
point3 lookfrom = point3 (0,0,0); //相机位置
point3 lookat = point3 (0,0,-1); //观察点
vec3 vup = vec3(0,1,0); //相机相对向上的位置

double defocus_angle = 0; //锥角
double focus_dist = 0; //从相机中心到焦点平面中心的距离
...
private:
int image_height; //渲染图像的高度
double pixel_samples_scale; //每次采样的颜色权重
point3 camera_center; //相机的中心
point3 pixel00_loc; //像素(0,0)的位置
vec3 pixel_delta_u; //向右的偏移值
vec3 pixel_delta_v; //向下的偏移值
vec3 u,v,w; //相机的相对坐标系
vec3 defocus_disk_u; //散焦圆盘水平向量
vec3 defocus_disk_v; //散焦圆盘垂直向量

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;

camera_center = lookfrom;

//确认视窗的设置
// auto focal_length = (lookfrom - lookat).length(); //焦距设置
auto theta = degree_to_radius(vfov);
auto h = std::tan(theta/2);
auto viewport_height = 2*h*focus_dist; //确保视口和焦点平面重合
auto viewport_width = viewport_height*(double (image_width)/image_height);

//计算摄像机的相对基底
w = unit_vector(lookfrom-lookat);
u = unit_vector(cross(vup,w));
v = cross(w,u);

//视图边缘的向量计算
auto viewport_u = viewport_width * u;
auto viewport_v = viewport_height * -v;
//计算视图的像素间的水平竖直增量
pixel_delta_u = viewport_u/image_width;
pixel_delta_v = viewport_v/image_height;

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

//计算相机散焦圆盘的基向量
auto defocus_radius = focus_dist * std::tan(degree_to_radius(defocus_angle/2));
defocus_disk_u = u*defocus_radius;
defocus_disk_v = v*defocus_radius;
}

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 = (defocus_angle <= 0) ? camera_center :defocus_disk_sample();
auto ray_direction = pixel_sample - ray_origin;

return ray(ray_origin,ray_direction);
}
...
point3 defocus_disk_sample() const {
// 返回散焦盘的上的随机点
auto p = random_in_unit_disk();
return camera_center + (p[0]*defocus_disk_u) + (p[1]*defocus_disk_v);
}
...
};

现在我们的相机具备了景深的效果,然我们来渲染试试效果吧:

image.png

Good,到此为止,我们的相机终于完成了!