今日收官之战,图形学之旅差不多到此为止了
可定位相机
我们的相机目前是固定视角的,比较单调。由于程序的复杂性,所以在这里我们最后实现它。首先,我们需要为我们的相机添加视场(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轴的旋转。但其实还有一种方法,我们保持lookfrom
和lookat
两点不变,然后定义一个向上的方向向量,作为我们的摄像方向。
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; 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
然后我们发现渲染出来的图片不是很清晰,我们可以通过修改垂直视野来放大
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 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; 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 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) { 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,到此为止,我们的相机终于完成了!