光线追踪 超详解《Ray Tracing in One Weekend》
今天我们开始进入正篇
Chapter 3: Rays, a simple camera, and background
对于所有的光线追踪器,基本都有一个光线类,计算沿光线看到的颜色。
我们的光线是一个矢量运算:
p(t) = a + t * b.
书中的向量用大写粗体字表示,但这里我遵循一般表示法:矩阵为粗体大写字母,向量为粗体小写字母,而标量为正常字体
P是光线指向的目标位置,a是光线的起点,b是光线指向的方向,t则是光线步长。
如图:
我们如下定义ray-class
/// ray.h // ----------------------------------------------------- // [author] lv // [begin ] 2018.12 // [brief ] the ray-class for the ray-tracing project // from the 《ray tracing in one week》 // ----------------------------------------------------- #ifndef RAY_H #define RAY_H #include <lvgm\type_vec\type_vec.h> //https://www.cnblogs.com/lv-anchoret/p/10163085.html class ray { public: using value_type = lvgm::precision; using vec_type = lvgm::vec3<value_type>; public: ray() :_a{vec_type()} , _b{vec_type()} { } ray(const vec_type& a, const vec_type& b) :_a(a) ,_b(b) { } ray(const ray& r) :_a(r._a) ,_b(r._b) { } inline vec_type origin()const { return _a; } inline vec_type direction()const { return _b; } inline vec_type go(const value_type t)const { return _a + t * _b; } private: vec_type _a; vec_type _b; }; #endif //ray_h
然后我们来看制作一个光线追踪器,它的核心在于,发送光线去穿过像素,进而计算出沿着光线方向,什么颜色被看到了
我们看到物体,是外部光照射到物体表面,经过表面反射之后,那部分反射进入眼睛的光被我们捕捉,从而看到了光来源位置的物体,那么,我们假设从眼睛发射一束光,它代表我们的视线,当它沿着某个方向一直向前,视线会与物体表面相交,那么,我们就捕捉到了一个像素。光线追踪器就是计算视线的一种形式。
为了更贴切,我们在此之后将光线统一称为视线。
它可以计算视线与表面交点以及交点处的颜色。
上述内容会在后续的案例讲解过程中逐步理解的。
现在,我们来了解一下本书采用的默认坐标系统
coord 1.1
坐标(0,0,0)处为我们的眼睛(或者相机),我们的绘图区域为蓝色框代表的矩形平面,也就是你观察图像的那个屏幕。
按照学OpenGL的老规矩,第一堂当然是线性插值
差值公式:
blended_value = (1-t) * start_value + t * end_value
解释一下上面的公式:
t 是一个系数,根据情况确定,将开始颜色和终止颜色进行比例混合,然后得到混合色
如果我们要做一个从白色到蓝色根据坐标位置确定混合比例进行插值的矩形彩图
我们该如何做呢?
第一步,我们需要确定分辨率,假定为400*200,就用coord1.1的坐标体系去做。
第二步,我们需要确定开始位置和终止位置,假定从左下角混合到右上角,混合颜色为白色和蓝色。
第三步,我们需要确定视线,即确定从眼睛出发到屏幕的向量
1. 确定在平面中的位置,从左下角开始每一个平面位置均由水平和垂直两个分向量叠加而成:
diagram 3-1
由坐标系统得知,lower-left的坐标为(-2,-1,-1)
若y =(0,0.5,0),x =(1,0,0),则pos =(-1,-0.5,-1)
2.当我们确定了pos之后,其实,视线已经确定好了,因为眼睛的坐标为(0,0,0),pos即为视线向量
第四步,我们需要从分辨率到屏幕做一个映射。
看图还记得我们的分辨率是400*200吗,而屏幕的范围是4*2的矩形
所以,我们需要做一个映射,和上次一样,我们可以将分辨率下的位置通过除法转换到标准坐标,然后再通过标准坐标转换到屏幕坐标
例如一个分辨率下的x的步长为388,首先通过388/400变为一个0~1的实数,然后乘以4,即可变为屏幕步长
然后通过diagram 3-1,确定屏幕中的位置
第五步,我们需要确定比例系数t,我们可以选择x或y方向的其中一个做映射。
因为屏幕坐标系不确定,我们采用的是4*2的,但不是铭文规定的,所以我们需要将视线向量(等同于pos坐标位置)进行单位化,这样的话就可以把每个坐标分量的长度控制在[-1, 1],如果我们把它+1再除以2,那么就完全转化到[0, 1]了,此时,每个坐标方向的分向量范数均为[0, 1],它们是由三个坐标基共同作用而成的独一无二的,所以,你可以采用x的单位化值作为t,也可以把y作为t。
第六步,经过上述一顿操作,我们终于得到了屏幕某个点对应的blend_value,颜色混合值(或称为插值)
至此,我们基于位置进行的颜色插值就讲解完了
下面是代码:
#define LOWPRECISION #include <fstream> #include "ray.h" using namespace lvgm; #define stds std:: ray::vec_type lerp(const ray& r) { ray::vec_type unit_dir = r.direction().ret_unitization(); //单位化 ray::value_type t = 0.5*(unit_dir.y() + 1.0); //将y分量映射到[0, 1] //插值公式 白色&蓝色 return (1.0 - t)*ray::vec_type(1.0, 1.0, 1.0) + t*ray::vec_type(0.0, 0.0, 1.0); } void build_3_1() { int X = 400, Y = 200; //分辨率 400*200 stds ofstream file("graph3-1.ppm"); if (file.is_open()) { file << "P3\n" << X << " " << Y << "\n255\n"; ray::vec_type left_bottom{ -2.0,-1.0,-1.0 }; //左下角作为开始位置 ray::vec_type horizontal{ 4.0,0,0 }; //屏幕水平宽度 ray::vec_type vertical{ 0,2.0,0 }; //屏幕垂直高度 ray::vec_type eye{ 0,0,0 }; //眼睛位置 for (int j = Y - 1; j >= 0; --j) for (int i = 0; i < X; ++i) { vec2<ray::value_type> para(ray::value_type(i) / X, ray::value_type(j) / Y); ray r(eye, left_bottom + para.u() * horizontal + para.v() * vertical); ray::vec_type color = lerp(r); //得到插值颜色(rgb) int ir = int(255.99*color.r()); int ig = int(255.99*color.g()); int ib = int(255.99*color.b()); file << ir << " " << ig << " " << ib << stds endl; } file.close(); } else stds cerr << "load file failed!" << stds endl; stds cout << "complished" << stds endl; } int main() { build_3_1(); }
下面是效果图(y分量作为插值公式系数)
当然你也可以用x分量作为插值公式系数 t
感谢您的阅读,生活愉快~