制作光线
光线是什么?你或许觉得要某个数学概念来定义光线,实在是太过于不能理解:“我怎么在程序中创造光线,难不成我先抽象出一个太阳不成?”
换一种说法你就明白了——射线。这下子你的脑海中是不是就有一些数学概念了?没错,一个点和一个方向会组成一根射线,我们顺着这个思路往下想。我们需要一个原点A和一个方向向量b,接下来我可以给出一个公式来描述光线:
A表示光线的原点(光源位置),b为一个单位向量表示一个方向,t则表示单位时间,通过给t取不同的值,我们可以得到沿路上所有的点的三维坐标,当然,这个值一般来说不会为负数,因为射线只有一个方向,看下图:
懂得光线的基本概念之后,我们就开始用代码描述它了,下面创建一个ray.h文件:
#ifndef RAY_H
#define RAY_H
#include "vec3.h"
class ray {
public:
//空构造。
ray() {}
//带参构造,显然我们需要一个原点和一个方向。
ray(const point3& origin, const vec3& direction)
: orig(origin), dir(direction)
{}
//通过这个函数拿取原点值。
point3 origin() const { return orig; }
//通过这个函数拿取方向值。
vec3 direction() const { return dir; }
//这个函数就对应了上方数学公式中的P(t),通过传入一个时间t,能得到当前光线传播到的坐标位置。
point3 at(double t) const {
return orig + t*dir;
}
public:
point3 orig;
vec3 dir;
};
#endif
它多简单啊!甚至都没有什么可以说的。完完全全按照我们上面的数学公式,就可以敲完这个代码,多提一嘴,在有参构造中我们的参数是point3和vec3,这里你就可以看到我们对vec3类取了很多别名的好处了,有别名之后,世界是多么清晰!<br />下面的部分是本系列的第一个**难点**,我们要怎么射出光线呢?
逐像素发射光线
如果你关注过游戏引擎或者任何3D建模软件,你都应该知道有一个摄像机的概念。我们拖动这个摄像机在世界空间中移动,就可以看到在这个位置下的相机“照片”。
在本项目中我们也需要类似的概念,但是先不要这么复杂,我们把“相机”固定在一个位置,并且固定一下它观察的方向。
现在轮到我们刚刚定义的“光线”出场了:
- 光线从哪里射出呢?相机位置。你可能会很奇怪,按照常理来说,太阳发出的光线从物体上弹射了多次,最终会被摄像机(或者人眼)捕捉。相机位置应该是光线的终点才对啊,怎么会是起点呢?原因是我们需要逆光路取色,这是路径追踪的经典光线模型,你只需要记住,我们的光线和现实中的光线是反过来的,等做完这个项目你就会明白,如果正向光路进行光线追踪会极其困难,几乎寸步难行。
- 光线朝哪个方向射出呢?这就要引出一个“虚拟视口”的概念。想象一下相机对着的那个方向,难道那个方向上的所有物件都能被你拍到吗?显然不是这样,如果是这样,你会得到一张无穷大的照片。我们假设相机前方摆了一个虚拟的框框,它是方形的,且正好和我们要生成的图片的长宽比相似,接下来,我们用密集的光线射满这个框框。正确的来说,我们是按照行优先的顺序,从左上角开始,一排一排的射出光线,射出光线的数目就是像素的数目,换句话说,我们对每一个像素都会射出一根光线。
感觉很抽象?没事,看看下面这张图。我们假设相机放在(0,0,0)位置上且相机镜头中心对准Z轴负方向,然后这个虚拟视口的位置在距离相机1个单位长度的地方,且它的长宽是4和2。我们来看看在空间中发生了什么事情。
假设图中的虚拟视口上有800个像素,每个像素长宽都是0.1,你想最终得到一张4020的图片,你要怎么射出光线?假设我们要瞄准每一个像素的中心。
按照行优先左上角开始,我们发射的第一根光线应该是从(0,0,0)射向(-2+0.05,1-0.05,-1)方向。我们不必要求这个方向向量是一个单位向量,保持方向向量是单位向量并不能给我们的项目提供更多便利。
那第二个呢?是(-2+0.15,1-0.05,-1)方向。
第四十个呢?是(-2+3.95,1-0.05,-1)方向。
第八百个呢?是(-2+3.95,1-1.95,-1)方向。
对,按照上面我们给出的虚拟视口长宽以及像素数量,我们可以射出这样的八百个光线,这些光线将分别为这八百个像素决定最终的颜色,形成一幅4020的精致小巧的图片。
如果你理解了上述的模型,你理解下面这些代码就不难了,我们改造main函数,并且为其所在的文件添加一个全局函数,如下:
#include "color.h"
#include "ray.h"
#include "vec3.h"
#include <iostream>
//这是一个简单的决定光线所带回颜色的函数。
color ray_color(const ray& r) {
//我们先把这个光线的方向向量单位化。
vec3 unit_direction = unit_vector(r.direction());
//再根据这个单位化向量的y分量给他设定颜色,注意我们得保证t在[0,1]之间。
auto t = 0.5*(unit_direction.y() + 1.0);
// 嘿,你认得它吗?一个插值函数,t靠近0它就越靠近白色,越靠近1它就越靠近一种蓝色。
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
int main() {
//图片数据,这次我们换一种角度去定义图片的长宽,我们定义一个长宽比,再把它的宽度定义出来。
//长度就可以通过简单的计算得到。
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
//虚拟视口数据,我们保持它的高度(宽度)为2,长度同样通过长宽比得到。
//之前介绍过,我们要保持视口和实际图片的长宽比一致。
auto viewport_height = 2.0;
auto viewport_width = aspect_ratio * viewport_height;
//这是视口离相机的距离,保持为1就好,我们暂时把它写死。
auto focal_length = 1.0;
//相机位置
auto origin = point3(0, 0, 0);
//相机水平方向,即X轴正方向。
auto horizontal = vec3(viewport_width, 0, 0);
//相机头顶方向,即Y轴正方向。
auto vertical = vec3(0, viewport_height, 0);
//这个是虚拟视口左下角所在位置的坐标,在上面那个图片例子里,它就是(-2,-1,-1)。
//注意因为长宽比不是2/1而是16/9,所以本例子里这个值和图片中的值不同。
auto lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length);
// 渲染循环(render loop)
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
//这个uv就是当前像素位置的横纵坐标偏移。
auto u = double(i) / (image_width-1);
auto v = double(j) / (image_height-1);
//创造射线。
ray r(origin, lower_left_corner + u*horizontal + v*vertical - origin);
//通过全局函数取到本像素颜色。
color pixel_color = ray_color(r);
//写颜色到输出流。
write_color(std::cout, pixel_color);
}
}
std::cerr << "\nDone.\n";
}
看,蓝天!
我留下几个问题来考验你是否真的理解上面的代码:
(1)lower_left_corner的值在上面的代码中应该是多少呢?
(2)渲染循环中射出的光线是瞄准哪里的呢?还是每个像素的中心吗?
(3)更改ray_color函数如下,你觉得会输出一张什么样的图片,再实际运行一下,看看是否和你想象的一样?
color ray_color(const ray& r) {
auto vec = r.direction() * 1 - r.origin();
auto absVec = vec3(std::abs(vec.x()), std::abs(vec.y()), std::abs(vec.z()));
return unit_vector(absVec);
}
参考文献
https://raytracing.github.io/books/RayTracingInOneWeekend.html
参考自《RayTracingInOneWeekend》第4节