相机,就像电介质一样,调试起来非常麻烦。所以我一直采用循序渐进的方式来开发。首先,让我们创造一个可调试的视角(fov)。这就是一个你通过视口看景象时的角度。我经常使用垂直fov。我也常用角度来指定它,并在构造函数中更改为弧度——这只是个人习惯的问题。
11.1. 相机视野几何结构
我们首先让光线从源点朝向z=-1平面。或者,我们可以使它朝向z=-2平面,只要我们把h设为这个距离的比例。如下图所示:
图14:相机视野几何结构
显而易见,h=tan(θ/2)。我们将添加代码,改写相机类:
代码62:[Camera.h]改写过后的相机类
我们将fov设置为90,宽高比为16:9,同时将场景中的物体简化为两个小球,来看看效果:
代码63:[main.cpp]添加一个广角相机:
结果:
结果17:广角镜头下的场景
11.2. 相机的定向和定位
我们给定了一个任意的视点,我们首先关心两点。我们将称之为放置摄像机视线的位置,以及我们看向的点。(稍后,如果需要,可以定义要查看的方向,而不是要查看的点。)
我们还需要一种方法来指定摄像机的滚动或侧向倾斜:围绕lookat-lookfrom轴旋转。另一种方法是可以保持lookfrom和lookat固定,让他绕该轴旋转。我们需要的是一种为摄像机指定“up”向量的方法。该up向量应位于与视图方向正交的平面内。
图15:相机和视野方向
实际上,我们可以使用我们想要的任何上向量,只需将其投影到这个平面上,就可以得到摄像机的上向量。我们使用约定俗成的名叫“view up”(vup) 向量。我们现在有了一个完整的正交基(u,v,w)来描述摄像机的方向。
图16:相机的vup方向
记住vup,v和w都位于同一平面内。与之前一样,当我们的固定摄像机朝向-Z时,我们的任意视图摄像机朝向-w。要知道我们可以这么做——但不是必须要这么做——使用(0,1,0)作为世界的上方向来规定vup。这很方便,在你决定尝试疯狂的相机角度之前,它会自然地保持你的相机水平。
代码64:[Camera.h]可以设定位置和朝向的相机
我们来使用新的视角,切换回之前的场景:
代码65:[main.cpp]使用新视角的场景
我们得到如下图:
结果18:一个远距离的视野
假如我们将距离改进一点:
代码66:[main.cpp] 改变了相机位置和视野
我们会得到:
结果19:聚焦
现在要介绍我们的最后一个功能:散焦模糊。请注意,所有摄影师都会将其称为“景深(depth of field)”,因此请注意在朋友之间只使用“散焦模糊”。
我们在真实相机中散焦模糊的原因是,它们需要一个大孔(而不仅仅是一个针孔)来收集光线。这将会使所有的物体发生焦散,但是如果我们将一个透镜放在这个洞中,在一定的距离内,这些物体都会聚焦。你也可以这样认为透镜的作用:所以的光线来自焦距上的一个特定的点——并且它们都击中了透镜——将它们都汇聚在了图像传感器上的一个点。
在物理相机中,焦距由透镜和感光元件之间的距离来决定的。这就是你为什么会看见当你调试焦距时镜头回伸缩(但是这在手机的相机里是相反的,移动的是感光元件)。“光圈”是一个用来有效控制镜头大小的孔。在真实的相机中,如果你需要更多的光线,就将光圈调大,当然后果是会更容易产生焦散。对于虚拟相机来说,我们可以有一个完美的感光元件,永远不需要更多的光,所以我们想要获得焦散模糊时只能依靠光圈。
12.1 模拟一个薄的透镜
真实的相机是有着复杂的复合透镜。在我们的代码中我们可以模拟这种顺序:感光元件,然后是透镜,光圈。之后我们可以找出光线的发送位置,并在计算后翻转图像(图像倒置投影在胶片上)。图形工作人员通常使用如下薄透镜的模拟:
图17:相机透镜模型
我们不需要模拟相机中的所有的内部情况。为了在摄像机外渲染图像,这些都不是必要的。取而代之的是,我通常从透镜开始光线,并将其发送到焦点平面(focuse_dist是镜片的距离),平面上的一切都完美的在焦点处。
图18:相机的焦点平面
12.2 生成采样光线
通常的,场景中所有的光线原点位于lookfrom点上。为了实现焦散模糊,场景中随机生成光线原点都位于这个镜片的中心lookfrom是。半径越大,焦散模糊就越严重。你可以把我们的原始相机想象成一个半径为零的散焦盘(不会产生模糊)。所以所有的光线都在圆盘的中心。
代码67:[Vec3.h]随机生成一个起点在圆盘上的向量
代码68:使用景深的相机(depth-of-field,dof)
在主函数中使用大光圈:
代码69:有景深的场景
之后我们会得到:
结果20:有景深效果的球
13.1 最后的渲染
首先让我来实现以下本文的封面——许许多多的球体:
代码70:最终的场景
经过一段时间的运行,我们会得到:
结果21:最终的场景
你可能会注意到一件有趣的事,那就是玻璃球实际上没有阴影,这使得它们看起来像是在漂浮。这不是一个bug——你在现实生活中很少看到玻璃球,通常它们确实看起来也有点奇怪,特别是在阴天看起来就像漂浮在空中一样。玻璃球下的大球体任然会反射大量的光线,因为天空的渲染顺序是没有被遮挡的。
13.2. 改进的空间
我们现在拥有了一个非常酷的光线追踪其!下一步是什么呢?
光线——你可以显式的进行,方法是将阴影光线发射到光源,亦或是隐式进行,即使一些物体发出光线,向其偏移散射光线,之后对光线权重降低来抵消偏移。两者都有效,我在偏爱少数人的第二种方法。
三角形——最酷的模型都是三角形组成的。模型I/O是最糟糕的,几乎每个人都试图让别人的代码来做这件事。
表面纹理——这可以让你像贴墙纸一样将图像贴到模型上,这很容易,也是一件很好的事。
固体纹理(Solid textures)——Ken Perlin的代码已在网络上公开。Andrew Kensler也有一些很棒的博客来介绍这个。
集成和媒体——这是非常棒的东西可以改变你的软件架构。我更喜欢使集成代码,让它们成为一个可被调用多次的接口。在进行渲染的时候可以直接调用它们。
并行——在具有N内核上运行代码的N个副本。平均运行N次。这种平均也可以分层进行,其中可以对N/2对图像进行平均以获得N/4个图像,并且可以对这些图像进行平均。这种并行方法应该可以很好地扩展到数千个核,只需要很少的编码。
祝你玩得开心,请把你的酷照片发给我(Peter Shirley)!
《一周学会光线追踪:理论和实战》系列文章到此已经全部完结!