这部分我们将使用 C++ 从零开始实现一个光线追踪器。首先需要实现一些在简单的光线追踪器中要用到的基本工具类,包含三维向量类、光线类,以及一些之后可能频繁用到的工具函数。
1 三维向量类 尽管在正规的光线追踪器中使用更多的是四维向量,比如齐次坐标和带有 A 通道的颜色,但是对于我们这个简化的光线追踪器来说,三维向量已经足够了,我们可以使用三维向量表示位置、方向、颜色等,为了区分这三种表示,我们给三维向量类 vec3
起两个别名 point3
和 color
以方便之后代码的编写和阅读,但他们实质上没有任何区别,我们没有为了不同的表示构造一个专门的类,这意味着一个颜色和一个位置可以进行相加,这在正常的光线追踪工程实现中是不被允许的,但我们毕竟是简化版本,只要在使用的时候注意即可。
我们首先在该类中实现一些基本的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #pragma once #ifndef VEC3_H #define VEC3_H #include <cmath> #include <iostream> using std::sqrt;class vec3 { public : double e[3 ]; public : vec3 () : e{ 0 ,0 ,0 } {} vec3 (double e0, double e1, double e2) : e{ e0, e1, e2 } {} double x () const { return e[0 ]; } double y () const { return e[1 ]; } double z () const { return e[2 ]; } vec3 operator -() const { return vec3 (-e[0 ], -e[1 ], -e[2 ]); } double operator [](int i) const { return e[i]; } double & operator [](int i) { return e[i]; } vec3& operator +=(const vec3& v) { e[0 ] += v[0 ]; e[1 ] += v[1 ]; e[2 ] += v[2 ]; return *this ; } vec3& operator *=(const double t) { e[0 ] *= t; e[1 ] *= t; e[2 ] *= t; return *this ; } vec3& operator /=(const double t) { return *this *= 1 /t; } double length_squared () const { return e[0 ] * e[0 ] + e[1 ] * e[1 ] + e[2 ] * e[2 ]; } double length () const { return sqrt (length_squared ()); } }; using point3 = vec3;using color = vec3;#endif
在工程实践中,文件开头的 #pragma once
是必须的,否则可能因为头文件重复包含导致编译错误,定义类时的判断:
1 2 3 4 5 6 #ifndef VEC3_H #define VEC3_H ... #endif
也是必须的,同样是为了防止重复编译。
然后实现一些对向量操作的工具函数,大部分是运算符重载,以让我们可以按照之前的习惯操作三维向量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 inline std::ostream& operator <<(std::ostream& out, const vec3& v) { return out << v.e[0 ] << ' ' << v.e[1 ] << ' ' << v.e[2 ]; } inline vec3 operator +(const vec3& u, const vec3& v) { return vec3 (u.e[0 ] + v.e[0 ], u.e[1 ] + v.e[1 ], u.e[2 ] + v.e[2 ]); } inline vec3 operator -(const vec3& u, const vec3& v) { return vec3 (u.e[0 ] - v.e[0 ], u.e[1 ] - v.e[1 ], u.e[2 ] - v.e[2 ]); } inline vec3 operator *(const vec3& u, const vec3& v) { return vec3 (u.e[0 ] * v.e[0 ], u.e[1 ] * v.e[1 ], u.e[2 ] * v.e[2 ]); } inline vec3 operator *(double t, const vec3& v) { return vec3 (t * v.e[0 ], t * v.e[1 ], t * v.e[2 ]); } inline vec3 operator *(const vec3& v, double t) { return t * v; } inline vec3 operator /(const vec3& v, double t) { return (1 / t) * v; } inline double dot (const vec3& u, const vec3& v) { return u.e[0 ] * v.e[0 ] + u.e[1 ] * v.e[1 ] + u.e[2 ] * v.e[2 ]; } inline vec3 cross (const vec3& u, const vec3& v) { return vec3 (u.e[1 ] * v.e[2 ] - u.e[2 ] * v.e[1 ], u.e[2 ] * v.e[0 ] - u.e[0 ] * v.e[2 ], u.e[0 ] * v.e[1 ] - u.e[1 ] * v.e[0 ]); } inline vec3 normalize (vec3 v) { return v / v.length (); } void write_color (unsigned char *& p, color pixel_color) { *p++ = (unsigned char )(255.999 * pixel_color.x ()); *p++ = (unsigned char )(255.999 * pixel_color.y ()); *p++ = (unsigned char )(255.999 * pixel_color.z ()); }
2 光线类 光线类是基于三维向量类的,我们按照图形学中射线的定义来实现一个光线类,给定空间中一个位置和一个方向就可以确定一条射线,再给定时间 t,就可以得到射线上任意一点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #pragma once #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 () { return orig; } vec3 direction () { return dir; } point3 at (double t) const { return orig + t * dir; } public : point3 orig; vec3 dir; }; #endif
3 图像输出 我们所有渲染结果最终都要输出到一张图片中,我们可以选择任何输出格式,这里使用了非常方便的开源的图像工具 stb_image ,将结果保存到数组中然后使用 stbi_write_png
函数保存为 PNG 格式图片。我们用一个生成渐变图片的例子来了解图像输出流程。
首先定义一个 ray_color
函数,根据给定的方向机型简单的线性插值,以混合两种不同的颜色:
1 2 3 4 5 6 7 8 color ray_color (ray& r) { vec3 unit_dir = normalize (r.direction ()); auto t = 0.5 * (unit_dir.y () + 1.0 ); return (1.0 - t) * color (1.0 , 1.0 , 1.0 ) + t * color (0.5 , 0.7 , 1.0 ); }
我们定义相机所在位置为 (0, 0, 0),屏幕(输出的图片)比例为 16 : 9,因此我们的视口比例也要和最终的图片比例一致,然后从屏幕左上角开始遍历,从相机向屏幕投射光线,根据该光线方向通过 ray_color
函数计算插值颜色显示到屏幕上。场景大致如下:
下面实现主函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 #include <iostream> #include <string> #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" #include "vec3.h" #include "ray.h" 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); const int channel = 3 ; auto viewport_height = 2.0 ; auto viewport_width = aspect_ratio * viewport_height; auto focal_length = 1.0 ; auto origin = point3 (0 , 0 , 0 ); auto horizontal = vec3 (viewport_width, 0 , 0 ); auto vertical = vec3 (0 , viewport_height, 0 ); auto lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3 (0 , 0 , focal_length); std::string SavePath = "D:\\TechStack\\ComputerGraphics\\Ray Tracing in One Weekend Series\\Results\\" ; std::string filename = "BlendColor.png" ; std::string filepath = SavePath + filename; unsigned char * odata = (unsigned char *)malloc (image_width * image_height * channel); unsigned char * p = odata; for (int j = image_height - 1 ; j >= 0 ; --j) { std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush; for (int i = 0 ; i < image_width; ++i) { 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 (p, pixel_color); } } stbi_write_png (filepath.c_str (), image_width, image_height, channel, odata, 0 ); std::cerr << "\nDone.\n" ; }
得到的结果如下: