本次光线追踪系列从基础重新开始,主要参照 Ray Tracing in One Weekend ,具体实现代码框架见 https://github.com/RayTracing/raytracing.github.io/。本文只是主要精炼光追相关理论,具体实现可参照原文。
相机的视野角度(fov)这个参数,可以用来表示屏幕的高度: 我们假设相机到屏幕的距离为1,则有h=tan(θ/2)。 再加上宽高aspect比这个参数,可以通过通过屏幕高度来求出屏幕宽度。
相机的四要素如下图: 计算方式可参照下图: 其中,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); } };我们之前定义的相机,本质上是一个针孔相机。如下图所示,真正的针孔相机成像是倒立的,但根据三角形相似,在代码中可以将屏幕挪到相机的位置的前方,从而避免倒立的情况,并更直观地去定义射线的起点和方向,回忆上一节camera类的代码:camear类里面有一个origin,表示相机的位置(对应下图“我们相机的位置”),然后从相机的位置出发,对着屏幕(对应下图“我们的屏幕”)上的每一个像素发射射线进行采样。
下面介绍一下带镜头的相机和针孔相机的区别:
针孔相机中(假设孔径足够小),则从树的顶点P到屏幕,只能通过一束光来成像这个点。带镜头的相机中,光线不是透过一个点(或者说“孔”)传入到成像屏幕的,而是透过具有一定半径的透镜传入的,半径的长度对应光圈的大小。这就导致成像的光线不仅只有一束,而是多束。
下图中,相机位置依然跟上图一样。红色光线反映了针孔相机中,将树的顶点P和最低点Q,传入相机屏幕的情况。蓝色光线就是镜头相机的成像情况,对于顶点P,其传入到成像屏幕的范围,从之前的一条光线,扩大到L1到L2两条光线之间的部分,尽管采样的光线变多了,但并不影响这一棵树的清晰成像,因为目前这棵树到相机的距离,刚好是新的屏幕到相机的距离,即焦距。
接下来,请大家发挥想象力去理解两个场景:
将这颗树往相机的方向移动,原本能采样到树顶的像素颜色,变成了多条光线采样值的混合色,也即是树顶部下面一片区域的颜色,从而导致这个像素变模糊,越往前移动,越模糊,因为L1和L2的区间会扩大更多。将这棵树高度稍微拉高一点,并将其往后面移动,延长光线L1和L2至树的纵切平面,则会采样天空和树头顶的颜色的混合色,同样实现模糊。越往后,L1和L2的区间将会扩大,从而越模糊。因此,只要物体到相机的距离不等于焦距,就会出现模糊,光圈越大,采样射线的跨度越大,模糊效果越明显,从而实现景深这一效果。程序中,为了简化操作,可以将原来相机的位置,从一个点,变换到镜头所在圆盘内的某个点。因为会多重采样抗锯齿,所以会从圆盘内的多个点出发,发射射线并采样求平均,以模拟上述镜头相机的原理。圆盘内点的位置,实际上为相机原位置,加上光圈半径范围内的偏移量。
通常,所有场景射线均来自该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); } };经过了前几部分的讲解,我们来实现 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); }