本文最后更新于:2023年2月14日 上午
用C++实现简单的路径追踪渲染
参考链接/原文:https://raytracing.github.io/books/RayTracingInOneWeekend.html
概述
该代码不依赖其他图形API,实现了间接光照,其简单易懂,并附带一些调试方法。
使用C++编程语言。
需要一定的数学知识,比如向量的计算。
输出第一个图像
第一份代码
教程使用了PPM图像格式,是一种位图格式。后面生成的图像就是这种格式。
新建一个main.cpp
源码文件。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> int main () { const int image_width = 256 ; const int image_height = 256 ; std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n" ; for (int j = image_height-1 ; j >= 0 ; --j){ for (int i = 0 ; i < image_width; ++i){ auto r = double (i)/(image_width-1 ); auto g = double (j)/(image_height-1 ); auto b = 0.25 ; int ir = static_cast <int >(255.999 * r); int ig = static_cast <int >(255.999 * g); int ib = static_cast <int >(255.999 * b); std::cout << ir << ' ' << ig << ' ' << ib << '\n' ; } } }
细节:
行内的像素从左到右输出。
每行像素从上到下输出。
三原色分量的范围是0.0到1.0。代码中将颜色范围映射到[0,1]。
代码将图片中红色的含量设置为从左到右增加,绿色含量从下到上增加,正常情况下输出的图像右上角是黄色。
生成一个图像
因为代码结果直接输出到终端,所以需要通过一个操作:输出重定向,将代码的结果生成一个PPM图像文件。
如果使用visual studio,打开窗口菜单的视图-终端
,将终端的路径移动到编译结果目录下,执行指令:
1 编译出的程序.exe | out-file image.ppm -encoding utf8
该指令将程序的输出结果重定向到一个文件,即生成了一个文件去保存输出结果。
因为visual studio的终端是powershell,默认输出文件的编码格式为UTF-16,会导致PPM文件不正常
所以需要使用额外参数指定输出文件的编码格式为UTF-8。
预期结果如下:
这就是本教程的第一个成果,按照惯例可以叫做"hello world"。
打开PPM图像可能需要一些额外软件支持,例如honeyview。
或者使用某些在线转换文件格式的网站,将其转换为更常见的JPG、PNG等。
如果图片看起来不是这样,可以用文本编辑器将这个PPM图片文件打开,正常来说是这样的:
1 2 3 4 5 6 7 8 9 P3 256 256 255 0 255 63 1 255 63 2 255 63 3 255 63 4 255 63 ……
通过文本编辑器查看这个PPM文件,可以检查是否哪里有问题。
添加一个进度提示
为了便于观察渲染的进度,我们可以添加一个进度提示。这个提示也可以帮助分析渲染的问题。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 …… for (int j = image_height-1 ; j >= 0 ; --j){ std::cerr << "rScanlines remaining: " << j << ' ' << std::flush; …… std::cout << ir << ' ' << ig << ' ' << ib << '\n' ; } } std::cerr << "\nDone.\n" ; }
vec3,向量类
向量类型在图形学中用途广泛,例如存储几何向量和颜色信息。
最常用的向量是四维的,在几何计算中可以存储三个位置量和一个齐次坐标,或者在保存颜色信息时存储RGB值和一个透明度通道。
在本教程中,我们选择了三维向量,尽管三维向量在实际使用时不够好,但让这些简单的代码运行是足够的。
vec3的变量和方法
根据理解,我们可以定义一个三维向量的类,并用不同的别名以区分含义和用途。
新建一个vec3.h
头文件,第一段代码如下:
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 #ifndef VEC3_H #define VEC3_H #include <cmath> #include <iostream> using std::sqrt;class vec3 {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.e[0 ]; e[1 ] += v.e[1 ]; e[2 ] += v.e[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 () const { return sqrt (length_squared ()); } double length_squared () const { return e[0 ] * e[0 ] + e[1 ] * e[1 ] + e[2 ] * e[2 ]; }public : double e[3 ]; };using point3 = vec3;using color = vec3;#endif
这里的代码中使用了double类型,也可以使用float类型,二者皆可。
vec3的实用函数
在vec3.h
接着加一些实用函数到#ifndef...#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 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 /(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 unit_vector (vec3 v) { return (v / v.length ()); }
这些函数定义了相关的计算规则。
颜色的实用函数
使用vec3
类,我们可以编写一个函数,用来生成一个像素的颜色,并输出到终端。
新建一个color.h
头文件,写入代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 #ifndef COLOR_H #define COLOR_H #include "vec3.h" #include <iostream> void write_color (std::ostream& out, color pixel_color) { out << static_cast <int >(255.999 * pixel_color.x ()) << ' ' << static_cast <int >(255.999 * pixel_color.y ()) << ' ' << static_cast <int >(255.999 * pixel_color.z ()) << ' ' << '\n' ; }#endif
现在,通过调用这些代码,把之前的main.cpp
改写一下。
1 2 3 4 5 6 7 8 9 #include "color.h" #include "vec3.h" #include <iostream> …… for (int i = 0 ; i < image_width; ++i) { color pixel_color (double (i) / (image_width - 1 ), double (j) / (image_height - 1 ), 0.25 ) ; write_color (std::cout, pixel_color); } ……
只展示了改写的部分,开头部分添加两行#include
,并改写for
循环内的执行语句,用到了我们编写的函数。
当然,之前的准备不只是为了重写hello world
。
不过接下来需要了解一些理论知识,以更好地理解代码是如何编写和运行的。
光线,摄像机,和背景