学习实现简单的光线追踪

本文最后更新于: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(){
//image
const int image_width = 256;
const int image_height = 256;
//render
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){
//progess indicator
std::cerr << "rScanlines remaining: " << j << ' ' << std::flush;
……
std::cout << ir << ' ' << ig << ' ' << ib << '\n';
}
}
//progess ends.
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];
};
//alias
using point3 = vec3;
using color = vec3;
#endif // !VEC3_H

这里的代码中使用了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) {
//write the mapped color component
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 // !COLOR_H

现在,通过调用这些代码,把之前的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
不过接下来需要了解一些理论知识,以更好地理解代码是如何编写和运行的。

光线,摄像机,和背景


学习实现简单的光线追踪
https://minatoai.github.io/2023/02/12/学习实现简单的光线追踪/
作者
minatoAI
发布于
2023年2月12日
许可协议