本次光线追踪系列从基础重新开始,主要参照 Ray Tracing in One Weekend ,具体实现代码框架见 https://github.com/RayTracing/raytracing.github.io/。本文只是主要精炼光追相关理论,具体实现可参照原文。
一、动态相机
1.1 视野角度
相机的视野角度(fov)这个参数,可以用来表示屏幕的高度:
我们假设相机到屏幕的距离为1,则有h=tan(θ/2)。
再加上宽高aspect比这个参数,可以通过通过屏幕高度来求出屏幕宽度。
1.2 相机定位和定向
相机的四要素如下图:
计算方式可参照下图:
其中,vup=(0,1,0)。
计算方式如下:
通过以上分析我们总结出相机类:
class camera
{
public:
vec3 origin;
vec3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
//lookfrom为相机位置,lookat为观察位置,vup传(0,1,0),vfov为视野角度,aspect为屏幕宽高比
camera(vec3 lookfrom, vec3 lookat, vec3 vup, float vfov, float aspect) //new
{
vec3 u, v, w;
float theta = vfov * M_PI / 180;
float half_height = tan(theta / 2);
float half_width = aspect * half_height;
origin = lookfrom;
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
lower_left_corner = origin - half_width * u - half_height * v - w;
horizontal = 2 * half_width * u;
vertical = 2 * half_height * v;
}
ray get_ray(float u, float v)
{
return ray(origin, lower_left_corner + u * horizontal + v * vertical - origin);
}
};
1.3 放置相机
camera cam(vec3(-2, 2, 1), vec3(0, 0, -1), vec3(0, 1, 0), 30, float(nx) / float(ny));
camera cam(vec3(-2, 2, 1), vec3(0, 0, -1), vec3(0, 1, 0), 90, float(nx) / float(ny));
二、散焦模糊
2.1 原理
我们之前定义的相机,本质上是一个针孔相机。如下图所示,真正的针孔相机成像是倒立的,但根据三角形相似,在代码中可以将屏幕挪到相机的位置的前方,从而避免倒立的情况,并更直观地去定义射线的起点和方向,回忆上一节camera类的代码:camear类里面有一个origin,表示相机的位置(对应下图“我们相机的位置”),然后从相机的位置出发,对着屏幕(对应下图“我们的屏幕”)上的每一个像素发射射线进行采样。
下面介绍一下带镜头的相机和针孔相机的区别:
针孔相机中(假设孔径足够小),则从树的顶点P到屏幕,只能通过一束光来成像这个点。带镜头的相机中,光线不是透过一个点(或者说“孔”)传入到成像屏幕的,而是透过具有一定半径的透镜传入的,半径的长度对应光圈的大小。这就导致成像的光线不仅只有一束,而是多束。
下图中,相机位置依然跟上图一样。红色光线反映了针孔相机中,将树的顶点P和最低点Q,传入相机屏幕的情况。蓝色光线就是镜头相机的成像情况,对于顶点P,其传入到成像屏幕的范围,从之前的一条光线,扩大到L1到L2两条光线之间的部分,尽管采样的光线变多了,但并不影响这一棵树的清晰成像,因为目前这棵树到相机的距离,刚好是新的屏幕到相机的距离,即焦距。
接下来,请大家发挥想象力去理解两个场景:
- 将这颗树往相机的方向移动,原本能采样到树顶的像素颜色,变成了多条光线采样值的混合色,也即是树顶部下面一片区域的颜色,从而导致这个像素变模糊,越往前移动,越模糊,因为L1和L2的区间会扩大更多。
- 将这棵树高度稍微拉高一点,并将其往后面移动,延长光线L1和L2至树的纵切平面,则会采样天空和树头顶的颜色的混合色,同样实现模糊。越往后,L1和L2的区间将会扩大,从而越模糊。
因此,只要物体到相机的距离不等于焦距,就会出现模糊,光圈越大,采样射线的跨度越大,模糊效果越明显,从而实现景深这一效果。程序中,为了简化操作,可以将原来相机的位置,从一个点,变换到镜头所在圆盘内的某个点。因为会多重采样抗锯齿,所以会从圆盘内的多个点出发,发射射线并采样求平均,以模拟上述镜头相机的原理。圆盘内点的位置,实际上为相机原位置,加上光圈半径范围内的偏移量。
2.2 代码实现
通常,所有场景射线均来自该lookfrom点。为了实现散焦模糊,请生成从以该lookfrom点为中心的磁盘内部发出的随机场景射线。半径越大,散焦模糊越大。您可以认为我们的原始相机具有一个半径为零的散焦盘(完全没有模糊),因此所有光线都起源于盘中心(lookfrom)。
生成单位圆内的点的函数:
inline double random_double()
{
return rand() / (RAND_MAX + 1.0);
}
inline vec3 random_in_unit_disk() {
vec3 p;
do {
p = 2.0*vec3(random_double(),random_double(),0) - vec3(1,1,0);
} while (dot(p,p) >= 1.0);
return p;
}
更新相机类及全局常数:
int gFov = 20;
float M_PI = 3.1415926;
vec3 lookfrom(3, 3, 2);
vec3 lookat(0, 0, -1);
float dist_to_focus = (lookfrom - lookat).length();
class camera
{
public:
vec3 origin;
vec3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
vec3 u, v, w;
float lens_radius;
//lookfrom为相机位置,lookat为观察位置,vup传(0,1,0),vfov为视野角度,aspect为屏幕宽高比
//aperture为光圈大小,focus_dist为相机到观察点的距离
camera(vec3 lookfrom, vec3 lookat, vec3 vup, float vfov, float aspect, float aperture, float focus_dist)
{
lens_radius = aperture / 2;
float theta = vfov * M_PI / 180;
float half_height = tan(theta / 2);
float half_width = aspect * half_height;
origin = lookfrom;
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
lower_left_corner = origin - half_width * focus_dist * u - half_height * focus_dist * v - focus_dist * w;
horizontal = 2 * half_width * focus_dist * u;
vertical = 2 * half_height * focus_dist * v;
}
ray get_ray(float s, float t)
{
vec3 rd = lens_radius * random_in_unit_disk();
vec3 offset = u * rd.x() + v * rd.y();
return ray(origin + offset, lower_left_corner + s * horizontal + t * vertical - origin - offset);
}
};
2.3 实现模糊
float aperture = 1.0;
camera cam(lookfrom, lookat, vec3(0, 1, 0), gFov, float(nx) / float(ny), aperture, dist_to_focus);
float aperture = 2.5;
camera cam(lookfrom, lookat, vec3(0, 1, 0), gFov, float(nx) / float(ny), aperture, dist_to_focus);
三、最终场景
经过了前几部分的讲解,我们来实现 Ray Tracing in One Weekend最终的版本(增加随机场景函数,并修改相机参数):
vec3 lookfrom(13, 2, 3);
vec3 lookat(0, 0, 0);
float dist_to_focus = 10.0;
float aperture = 0.1;
hittable *random_scene() {
int n = 500;
hittable **list = new hittable*[n+1];
list[0] = new sphere(vec3(0,-1000,0), 1000, new lambertian(vec3(0.5, 0.5, 0.5)));
int i = 1;
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
float choose_mat = random_double();
vec3 center(a+0.9*random_double(),0.2,b+0.9*random_double());
if ((center-vec3(4,0.2,0)).length() > 0.9) {
if (choose_mat < 0.8) { // diffuse
list[i++] = new sphere(center, 0.2,
new lambertian(vec3(random_double()*random_double(),
random_double()*random_double(),
random_double()*random_double())
)
);
}
else if (choose_mat < 0.95) { // metal
list[i++] = new sphere(center, 0.2,
new metal(vec3(0.5*(1 + random_double()),
0.5*(1 + random_double()),
0.5*(1 + random_double())),
0.5*random_double()));
}
else { // glass
list[i++] = new sphere(center, 0.2, new dielectric(1.5));
}
}
}
}
list[i++] = new sphere(vec3(0, 1, 0), 1.0, new dielectric(1.5));
list[i++] = new sphere(vec3(-4, 1, 0), 1.0, new lambertian(vec3(0.4, 0.2, 0.1)));
list[i++] = new sphere(vec3(4, 1, 0), 1.0, new metal(vec3(0.7, 0.6, 0.5), 0.0));
return new hittable_list(list,i);
}