文章目录
- 1. 写在前面
- 2. Output an Image (输出一张图片)
- 3. The vec3 Class (vec3类)
- 4. Rays, a Simple Camera, and Background (光、摄像机、背景)
- 5. Adding a Sphere (添加球体)
- 6. Surface Normals and Multiple Objects (表面法线和多个对象)
- 6.1 Shading with Surface Normals (表面法线与着色)
- 6.2 Simplifying the Ray-Sphere Intersection Code (简化球体求交代码)
- 6.3 An Abstraction for Hittable Objects (可命中对象的抽象)
- 6.4 Front Faces Versus Back Faces (正面与背面)
- 6.5 A List of Hittable Objects (可命中对象的列表)
- 6.6 Some New C++ Features (一些新特性)
- 6.7 Common Constants and Utility Functions (常用常量和实用函数)
- 7. Antialiasing (抗锯齿)
- 8. Diffuse Materials (漫反射材质)
- 9. Metal (金属)
- 10. Dielectrics (电解质)
- 11. Positionable Camera (可定位的相机)
- 12.Defocus Blur (离焦模糊)
- 13.Where Next?
1. 写在前面
下文的内容大部分来自于
g
i
t
h
u
b
github
github上的一个光线追踪的项目:教你在一周的时间内利用
C
+
+
C++
C++从
0
0
0开始实现一个光线追踪器。因为我对光线追踪比较感兴趣,同时缺乏
C
+
+
C++
C++项目经验,所以决定动手做一下这个项目,通过博客记录一下遇到的问题。废话不多说,直接开始。
项目地址:https://github.com/RayTracing/raytracing.github.io/
开发环境:
V
S
2019
VS2019
VS2019
t
i
p
s
:
tips:
tips:在项目的开发过程中,难免需要修改之前定义过的函数/类,建议您参照
G
i
t
H
u
b
GitHub
GitHub原文修改,因为原文会对新增/改动的代码块进行标注。
2. Output an Image (输出一张图片)
2.1 The PPM Image Format (ppm图片格式)
首先介绍一下
P
P
M
PPM
PPM格式,我们将使用它来存储图片的信息,因为它非常简单。
我们可以写一个
C
+
+
C++
C++程序
(
m
a
i
n
.
c
c
)
(main.cc)
(main.cc)来生成这样的文件。
#include<iostream>
using std::cin;
using std::cout;
int main() {
const int image_width = 256;
const int image_height = 256;
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) {
double r = double(i) / (image_width - 1);
double g = double(j) / (image_height - 1);
double 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);
cout << ir << ' ' << ig << ' ' << ib << '\n';
}
}
}
像素按照从左至右、从上至下的顺序打印,由于计算出的 r 、 g 、 b r、g、b r、g、b在 [ 0 , 1 ] [0,1] [0,1]之间,所以输出时应当将它们扩展至 [ 0 , 255 ] [0,255] [0,255]。
2.2 Creating an Image File (创建图片文件)
编译运行后,在项目文件夹下找到对应的
e
x
e
exe
exe文件:
通过命令行运行并将其输出重定向到文件中:
通过
X
n
V
i
e
w
XnView
XnView查看
i
m
a
g
e
.
p
p
m
image.ppm
image.ppm:
3. The vec3 Class (vec3类)
几乎所有图形程序都有一些用于存储几何矢量和颜色的类。 在许多系统中,这些向量是 4 D 4D 4D( 3 D 3D 3D加上几何的齐次坐标,而 R G B RGB RGB加上颜色的 a l p h a alpha alpha透明通道)。 就我们的目的而言, 3 D 3D 3D就足够了。 我们将对颜色,位置,方向,偏移量等使用相同的 v e c 3 vec3 vec3类。
3.1 Variables and Methods (变量和方法)
头文件 v e c 3. h vec3.h vec3.h的第一部分。
#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; //3D point
using color = vec3; //RGB color
3.2 vec3 Utility Functions (vec3实用函数)
头文件的第二部分。
// vec3 Utility Functions
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 unit_vector(const vec3& v) {
return v / v.length();
}
3.3 Color Utility Functions (color实用函数)
使用新的 v e c 3 vec3 vec3类,我们将创建一个函数,将单个像素的颜色写到标准输出流中 c o l o r . h color.h color.h。
#ifndef COLOR_H
#define COLOR_H
#include"vec3.h"
#include<iostream>
void write_color(std::ostream& out, color pixel_color) {
//写下每个颜色分量转换后的值[0,255]
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
那么可以将 m a i n . c c main.cc main.cc修改如下:
#include<iostream>
#include"vec3.h"
#include"color.h"
using std::cin;
using std::cout;
int main() {
const int image_width = 256;
const int image_height = 256;
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) {
double r = double(i) / (image_width - 1);
double g = double(j) / (image_height - 1);
double b = 0.25;
color pixel_color(r, g, b);
write_color(cout, pixel_color);
}
}
}
4. Rays, a Simple Camera, and Background (光、摄像机、背景)
4.1 The ray Class (光线类)
所有的光线追踪器都以
r
a
y
ray
ray类为基础,它还包含颜色(沿着光所能看到的颜色)。我们可以把光想象成函数:
P
(
t
)
=
A
ˉ
+
t
∗
b
ˉ
P(t)=\bar{A}+t*\bar{b}
P(t)=Aˉ+t∗bˉ,
A
ˉ
\bar{A}
Aˉ是光线的起点,
b
ˉ
\bar{b}
bˉ是光线的方向,
t
t
t是一个实数,当它取不同的值时,就对应光线上不同的点。
r
a
y
.
h
:
ray.h:
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; }
point3 at(double t) const {
return orig + t * dir;
}
public:
point3 orig;
vec3 dir;
};
#endif // !RAY_H
4.2 Sending Rays Into the Scene (在场景中添加光线)
光线追踪器的核心是使得光线穿过每个像素,并计算沿着对应方向所能看到的颜色。这包含三个步骤:
1.
1.
1.计算从眼睛到像素的光线。
2.
2.
2.求出与光线相交的物体。
3.
3.
3.计算出交点处的颜色。下面的例子中我们将渲染长方形图片,并且我们还需要设置一个虚拟视口,以使场景射线通过。视口的宽高比应当与渲染的图像相同。我们选择一个高度为
2
2
2个单位的视口,并将投影平面和投影点之间的距离设置为
1
1
1个单位,这被称为
f
o
c
a
l
l
e
n
g
t
h
focal\ length
focal length,注意不要与
f
o
c
u
s
d
i
s
t
a
n
c
e
focus\ distance
focus distance混淆。我将眼睛(摄像机)放在
(
0
,
0
,
0
)
(0,0,0)
(0,0,0),同时使用右手坐标系:
下面修改
m
a
i
n
.
c
c
:
main.cc:
main.cc:
#include<iostream>
#include"vec3.h"
#include"color.h"
#include"ray.h"
using std::cin;
using std::cout;
//实现渐变色
color ray_color(const ray& r) {
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
int main() {
//Image
const double aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
//Camera
double viewport_height = 2.0;
double viewport_width = aspect_ratio * viewport_height;
double focal_length = 1.0;
point3 origin = point3(0, 0, 0);
vec3 horizontal = vec3(viewport_width, 0, 0);
vec3 vertical = vec3(0, viewport_height, 0);
//视口左下角的坐标
point3 lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3(0, 0, focal_length);
//Render
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) {
double u = double(i) / (image_width - 1);
double 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(cout, pixel_color);
}
}
}
中间计算
l
o
w
e
r
_
l
e
f
t
_
c
o
r
n
e
r
lower\_left\_corner
lower_left_corner的部分可能有点难懂,其实就是计算视口左下角的坐标,建议结合上面的图理解。
r
a
y
_
c
o
l
o
r
ray\_color
ray_color其实实现了渐变色效果,经过转换把
t
t
t的范围限定在
[
0
,
1
]
[0,1]
[0,1]内,那么套公式就好了(线性插值):
查看生成的
p
p
m
ppm
ppm文件,应该如下图所示:
5. Adding a Sphere (添加球体)
现在我们准备给场景添加一个球体。
5.1 Ray-Sphere Intersection (光线球体求交)
上面是球体求交的推导过程,比较简单就不多解释了。可以看到我们最后得到了一个一元二次方程,那么利用判别式和求根公式,很容易计算出交点。
5.2 Creating Our First Raytraced Image (第一个光追图像)
更改 m a i n . c c main.cc main.cc中的 r a y _ c o l o r ray\_color ray_color函数逻辑,可以画出一个红色的球。
#include<iostream>
#include"vec3.h"
#include"color.h"
#include"ray.h"
using std::cin;
using std::cout;
//判断光线是否与某个球相交
bool hit_sphere(const point3& center, double radius, const ray& r) {
vec3 oc = r.origin() - center;
double a = dot(r.direction(), r.direction());
double b = 2.0 * dot(r.direction(), oc);
double c = dot(oc, oc) - radius * radius;
double discriminant = b * b - 4 * a * c;
return discriminant > 0;
}
color ray_color(const ray& r) {
if (hit_sphere(point3(0, 0, -1), 0.5, r))
return color(1, 0, 0);
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
int main() {
//Image
const double aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
//Camera
double viewport_height = 2.0;
double viewport_width = aspect_ratio * viewport_height;
double focal_length = 1.0;
point3 origin = point3(0, 0, 0);
vec3 horizontal = vec3(viewport_width, 0, 0);
vec3 vertical = vec3(0, viewport_height, 0);
//视口左下角的坐标
point3 lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3(0, 0, focal_length);
//Render
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) {
double u = double(i) / (image_width - 1);
double 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(cout, pixel_color);
}
}
}
结果如下图所示:
t
i
p
s
:
tips:
tips:大家可以自己修改球体的中心和半径,看看图片会怎么变化~
到目前为止,我们的追踪器还缺少很多东西,比如阴影、反射、更多的物体,而且这个球体求交的函数还有小
b
u
g
bug
bug,把球体中心坐标的
z
z
z改为
1
1
1后会得到和上图一样的结果,这说明你看到了身后的物体!显然这是错误的,后面我们将会修改这一
b
u
g
bug
bug。
6. Surface Normals and Multiple Objects (表面法线和多个对象)
6.1 Shading with Surface Normals (表面法线与着色)
为了方便着色,我们需要获得表面的法线。对于球体来说,很容易得到某个点的法向量。
由于目前还没有灯光,所以我们先通过简单的映射来为球体表面着色,由于交点
P
P
P的法向量为
N
N
N,且
N
N
N为单位向量,那么将其三个分量的值加
1
1
1再乘
0.5
0.5
0.5即可将范围限定在
[
0
,
1
]
[0,1]
[0,1]内,再把它们映射到
R
G
B
RGB
RGB即可。现在让我们来修改球体求交的代码吧,之前只是简单的判断了判别式,并没有计算出
t
1
、
t
2
t_1、t_2
t1、t2的值,显然当
t
1
、
t
2
t_1、t_2
t1、t2小于
0
0
0时也是没有交点的,否则返回最小的那个解。
m
a
i
n
.
c
c
:
main.cc:
main.cc:
#include<iostream>
#include"vec3.h"
#include"color.h"
#include"ray.h"
using std::cin;
using std::cout;
//判断光线是否与某个球相交
double hit_sphere(const point3& center, double radius, const ray& r) {
vec3 oc = r.origin() - center;
double a = dot(r.direction(), r.direction());
double b = 2.0 * dot(r.direction(), oc);
double c = dot(oc, oc) - radius * radius;
double discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return -1.0;
}
else {
double t1 = (-b - sqrt(discriminant)) / (2.0 * a);
if (t1 > 0)
return t1;
double t2 = (-b + sqrt(discriminant)) / (2.0 * a);
if (t2 > 0)
return t2;
return -1.0;
}
}
color ray_color(const ray& r) {
double t = hit_sphere(point3(0, 0, -1), 0.5, r);
if (t > 0) {
vec3 N = unit_vector(r.at(t) - point3(0, 0, -1));
return 0.5 * color(N.x() + 1, N.y() + 1, N.z() + 1);
}
vec3 unit_direction = unit_vector(r.direction());
t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
结果应该如下图所示:
6.2 Simplifying the Ray-Sphere Intersection Code (简化球体求交代码)
简化球体求交代码。
令
b
=
2
∗
h
b=2*h
b=2∗h,即可进一步化简求根公式。
double hit_sphere(const point3& center, double radius, const ray& r) {
vec3 oc = r.origin() - center;
double a = dot(r.direction(), r.direction());
double half_b = dot(r.direction(), oc);
double c = oc.length_squared() - radius * radius;
double discriminant = half_b * half_b - a * c;
if (discriminant < 0) {
return -1.0;
}
else {
double t1 = (-half_b - sqrt(discriminant)) / a;
if (t1 > 0)
return t1;
double t2 = (-half_b + sqrt(discriminant)) / a;
if (t2 > 0)
return t2;
return -1.0;
}
}
6.3 An Abstraction for Hittable Objects (可命中对象的抽象)
当场景中存在多个球时,上面这种设计会使得代码非常冗余。比较好的设计是创建一个
h
i
t
t
a
b
l
e
hittable
hittable抽象基类,需要求交点功能的类就去继承这个抽象基类,并实现对应的纯虚函数。同时我们会限定
t
t
t的范围,当且仅当
t
m
i
n
<
t
<
t
m
a
x
t_{min}<t<t_{max}
tmin<t<tmax时才认为命中(有交点)。
h
i
t
t
a
b
l
e
.
h
:
hittable.h:
hittable.h:
#ifndef HITTABLE_H
#define HITTABLE_H
#include"ray.h"
struct hit_record {
point3 p; //交点
vec3 normal; //法向量
double t; //距离
};
class hittable {
public:
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};
#endif // ! HITTABLE_H
s p h e r e . h : sphere.h: sphere.h:
#ifndef SPHERE_H
#define SPHERE_H
#include"hittable.h"
#include"vec3.h"
class sphere :public hittable {
public:
sphere() { }
sphere(point3 cen, double r) :center(cen), radius(r) { }
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const override;
public:
point3 center;
double radius;
};
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
vec3 oc = r.origin() - center;
double a = r.direction().length_squared();
double half_b = dot(r.direction(), oc);
double c = oc.length_squared() - radius * radius;
double discriminant = half_b * half_b - a * c;
if (discriminant > 0) {
double root = sqrt(discriminant);
double t = (-half_b - root) / a;
if (t > t_min&& t < t_max) {
rec.t = t;
rec.p = r.at(t);
rec.normal = (rec.p - center) / radius;
return true;
}
t = (-half_b + root) / a;
if (t > t_min&& t < t_max) {
rec.t = t;
rec.p = r.at(t);
rec.normal = (rec.p - center) / radius;
return true;
}
}
return false;
}
#endif // ! SPHERE_H
6.4 Front Faces Versus Back Faces (正面与背面)
关于法线还有非常重要的一点,就是它的方向。目前的做法是让它垂直平面朝外,当射线在外部与球体相交时没有问题,但是当射线在内部与球体相交时,我们需要翻转法线的方向。也就是说,我们应该始终使得法线的方向与射线的方向相反。
对于那些需要双面着色、或者内部和外部颜色不一致的物体来说,这一点非常重要。
但是翻转之后就不能通过点积来判断了,所以我们需要变量来记录一下。
h
i
t
t
a
b
l
e
.
h
:
hittable.h:
hittable.h:
#ifndef HITTABLE_H
#define HITTABLE_H
#include"ray.h"
struct hit_record {
point3 p; //交点
vec3 normal; //法向量
double t; //距离
bool front_face;
inline void set_face_normal(const ray& r, const vec3& outward_normal) {
front_face = dot(r.direction(), outward_normal) < 0;
normal = front_face ? outward_normal : -outward_normal;
}
};
class hittable {
public:
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};
#endif // ! HITTABLE_H
s p h e r e . h : sphere.h: sphere.h:
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
vec3 oc = r.origin() - center;
double a = r.direction().length_squared();
double half_b = dot(r.direction(), oc);
double c = oc.length_squared() - radius * radius;
double discriminant = half_b * half_b - a * c;
if (discriminant > 0) {
double root = sqrt(discriminant);
double t = (-half_b - root) / a;
if (t > t_min&& t < t_max) {
rec.t = t;
rec.p = r.at(t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
return true;
}
t = (-half_b + root) / a;
if (t > t_min&& t < t_max) {
rec.t = t;
rec.p = r.at(t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
return true;
}
}
return false;
}
6.5 A List of Hittable Objects (可命中对象的列表)
现在,我们添加一个存储 h i t t a b l e hittable hittable列表的类:
h i t t a b l e _ l i s t . h : hittable\_list.h: hittable_list.h:
#ifndef HITTABLE_LIST_H
#define HITTABLE_LIST_H
#include"hittable.h"
#include<memory>
#include<vector>
using std::shared_ptr;
using std::make_shared;
using std::vector;
class hittable_list :public hittable {
public:
hittable_list(){ }
hittable_list(shared_ptr<hittable> object) { add(object); }
void clear() { objects.clear(); }
void add(shared_ptr<hittable> object) { objects.push_back(object); }
virtual bool hit(const ray& r, double tmin, double tmax, hit_record& rec) const override;
public:
vector<shared_ptr<hittable>> objects;
};
bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
hit_record temp_rec;
bool hit_anything = false;
double closest_so_far = t_max;
for (const auto& object : objects) {
if (object->hit(r, t_min, closest_so_far, temp_rec)) {
hit_anything = true;
closest_so_far = temp_rec.t;
rec = temp_rec;
}
}
return hit_anything;
}
#endif // !HITTABLE_LIST_H
6.6 Some New C++ Features (一些新特性)
h
i
t
t
a
b
l
e
_
l
i
s
t
hittable\_list
hittable_list使用了
v
e
c
t
o
r
vector
vector和
s
h
a
r
e
d
_
p
t
r
shared\_ptr
shared_ptr,在这里解释一下。
s
h
a
r
e
d
_
p
t
r
<
t
y
p
e
>
shared\_ptr <type>
shared_ptr<type>是具有引用计数语义的指向某些已分配类型的指针。 每次将其值分配给另一个共享指针时(通常是简单分配),引用计数就会增加。 当共享指针超出范围时(例如,在块或函数的末尾),引用计数将减少。 一旦计数变为零,该对象将被删除。
m
a
k
e
_
s
h
a
r
e
d
<
t
h
i
n
g
>
(
t
h
i
n
g
_
c
o
n
s
t
r
u
c
t
o
r
_
p
a
r
a
m
s
.
.
.
)
make\_shared <thing>(thing\_constructor\_params ...)
make_shared<thing>(thing_constructor_params...)使用构造函数参数分配
t
h
i
n
g
thing
thing类型的新实例。 它返回一个
s
h
a
r
e
d
_
p
t
r
<
t
h
i
n
g
>
shared\_ptr <thing>
shared_ptr<thing>。由于可以通过
m
a
k
e
_
s
h
a
r
e
d
<
t
y
p
e
>
(
.
.
)
make\_shared <type>(..)
make_shared<type>(..)的返回类型自动推导该类型,因此可以使用
a
u
t
o
auto
auto类型说明符。
我们将在代码中使用共享指针,因为它允许多个几何共享一个公共实例(例如,一堆都使用相同纹理贴图材质的球体),而且它可以自动管理内存且易于推理 。
v
e
c
t
o
r
vector
vector可以简单的理解成动态数组。
6.7 Common Constants and Utility Functions (常用常量和实用函数)
我们需要一些数学常量和函数,现在来定义他们并放到
r
t
w
e
e
k
e
n
d
.
h
rtweekend.h
rtweekend.h中。
r
t
w
e
e
k
e
n
d
.
h
:
rtweekend.h:
rtweekend.h:
#ifndef RTWEEKEND_H
#define RTWEEKEND_H
#include<cmath>
#include<cstdlib>
#include<limits>
#include<memory>
using std::shared_ptr;
using std::make_shared;
using std::sqrt;
//常量
const double infinity = std::numeric_limits<double>::infinity(); //无穷大
const double pi = 3.1415926535897932385;
//函数
inline double degrees_to_radians(double degrees) { //度数到弧度
return degrees * pi / 180.0;
}
//公共头文件
#include"ray.h"
#include"vec3.h"
#endif // !RTWEEKEND_H
现在修改 m a i n . c c main.cc main.cc如下:
#include<iostream>
#include"rtweekend.h"
#include"color.h"
#include"hittable_list.h"
#include"sphere.h"
using std::cin;
using std::cout;
color ray_color(const ray& r, const hittable& world) {
hit_record rec;
if (world.hit(r, 0, infinity, rec)) {
return 0.5 * (rec.normal + color(1, 1, 1));
}
vec3 unit_direction = unit_vector(r.direction());
double t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
int main() {
//Image
const double aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
//World
hittable_list world;
world.add(make_shared<sphere>(point3(0, 0, -1), 0.5));
world.add(make_shared<sphere>(point3(0, -100.5, -1), 100));
//Camera
double viewport_height = 2.0;
double viewport_width = aspect_ratio * viewport_height;
double focal_length = 1.0;
point3 origin = point3(0, 0, 0);
vec3 horizontal = vec3(viewport_width, 0, 0);
vec3 vertical = vec3(0, viewport_height, 0);
//视口左下角的坐标
point3 lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3(0, 0, focal_length);
//Render
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) {
double u = double(i) / (image_width - 1);
double v = double(j) / (image_height - 1);
ray r(origin, lower_left_corner + u * horizontal + v * vertical - origin);
color pixel_color = ray_color(r,world);
write_color(cout, pixel_color);
}
}
}
结果应如下图所示:
这样产生的图片实际上只是球体及其表面法线位置的可视化图像。 这通常是查看模型中缺陷和特征的好方法。
7. Antialiasing (抗锯齿)
这一章将会学习抗锯齿,同时我们会对相机类进行抽象。
7.1 Some Random Number Utilities (随机数生成器)
我们需要随机数生成器,它的值应该在
[
0
,
1
)
[0,1)
[0,1)内。
r
t
w
e
e
k
e
n
d
.
h
:
rtweekend.h:
rtweekend.h:
inline double random_double() {
//[0,1)
return rand() / (RAND_MAX + 1.0);
}
inline double random_double(double min, double max) {
//[min,max)
return min + (max - min) * random_double();
}
另外一种实现:
7.2 Generating Pixels with Multiple Samples (用多个样本生成像素)
我们在给定的像素内选定几个样本,对每个样本都发射光线,然后平均这些光线的颜色。稍后可以看到,我们的计算方式是先对颜色求和,最后再取平均值。
现在是时候创建一个摄像机类来管理我们的虚拟摄像机和场景漫游的相关任务了。 下列类使用之前的轴对齐相机实现了一个简单相机:
c
a
m
e
r
a
.
h
:
camera.h:
camera.h:
#ifndef CAMERA_H
#define CAMERA_H
#include"rtweekend.h"
class camera {
public:
camera() {
auto aspect_ratio = 16.0 / 9.0;
auto viewport_height = 2.0;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.0;
origin = point3(0, 0, 0);
horizontal = vec3(viewport_width, 0.0, 0.0);
vertical = vec3(0.0, viewport_height, 0.0);
//视口左下角的坐标
lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3(0, 0, focal_length);
}
ray get_ray(double u, double v) const {
return ray(origin, lower_left_corner + u * horizontal + v * vertical - origin);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};
#endif // !CAMERA_H
再给 r t w e e k e n d . h rtweekend.h rtweekend.h内添加一个函数:
inline double clamp(double x, double min, double max) {
if (x < min) return min;
if (x > max) return max;
return x;
}
现在来修改我们的
w
r
i
t
e
_
c
o
l
o
r
(
)
write\_color()
write_color()函数。
c
o
l
o
r
.
h
:
color.h:
color.h:
void write_color(std::ostream& out, color pixel_color,int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
//根据样本数对颜色取平均值
auto scale = 1.0 / samples_per_pixel;
r *= scale;
g *= scale;
b *= scale;
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}
然后修改 m a i n . c c : main.cc: main.cc:
#include<iostream>
#include"camera.h"
#include"rtweekend.h"
#include"color.h"
#include"hittable_list.h"
#include"sphere.h"
using std::cin;
using std::cout;
color ray_color(const ray& r, const hittable& world) {
hit_record rec;
if (world.hit(r, 0, infinity, rec)) {
return 0.5 * (rec.normal + color(1, 1, 1));
}
vec3 unit_direction = unit_vector(r.direction());
double t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
int main() {
//Image
const double aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
//World
hittable_list world;
world.add(make_shared<sphere>(point3(0, 0, -1), 0.5));
world.add(make_shared<sphere>(point3(0, -100.5, -1), 100));
//Camera
camera cam;
//Render
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) {
color pixel_color(0, 0, 0);//求和
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width - 1);
auto v = (j + random_double()) / (image_height - 1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world);
}
write_color(cout, pixel_color, samples_per_pixel);//取平均值
}
}
}
结果如下图所示:
可以看到边缘更加平滑了。
8. Diffuse Materials (漫反射材质)
现在我们有了对象,每个像素会发射多条光线,我们可以制作一些逼真的材料。 我们将从漫反射(无光泽)材料开始。 一个问题是我们是否混合并匹配几何和材料(因此我们可以将一个材料分配给多个球体,反之亦然),或者几何和材料是否紧密结合(这对于将几何和材料连接在一起的程序对象很有用)。我们将采用分离的策略(在大多数渲染器中通常如此),但请注意这一限制。
8.1 A Simple Diffuse Material (一种简单的漫反射材质)
不发光的漫射对象仅具有周围环境的颜色,但它们会使用自身的固有颜色对其进行调制。 从散射表面反射的光的方向是随机的。 因此,如果我们将三束光线发送到两个扩散表面之间的裂缝中,它们将具有不同的随机行为:
它们也可能被吸收而不是被反射。表面越黑,吸收的可能性就越大(这就是为什么它很暗的原因)。实际上,任何将方向随机化的算法都会产生看起来粗糙的表面。这里作者使用了一个
l
a
z
y
h
a
c
k
lazy\ hack
lazy hack,在数学上接近理想的
L
a
m
b
e
r
t
i
a
n
Lambertian
Lambertian。
读者
V
a
s
s
i
l
l
e
n
C
h
i
z
h
o
v
Vassillen\ Chizhov
Vassillen Chizhov证明了
l
a
z
y
h
a
c
k
lazy\ hack
lazy hack确实只是一个
l
a
z
y
h
a
c
k
lazy\ hack
lazy hack,而且是不准确的。理想的
L
a
m
b
e
r
t
i
a
n
Lambertian
Lambertian的正确表示并没有太多工作,将在本章最后介绍。
两个单位球体相切于
P
P
P点。这两个球的中心为
(
P
ˉ
+
n
ˉ
)
(\bar{P} + \bar{n})
(Pˉ+nˉ)和
(
P
ˉ
−
n
ˉ
)
(\bar{P} - \bar{n})
(Pˉ−nˉ),其中
n
ˉ
\bar{n}
nˉ为曲面的法线。以
(
P
ˉ
−
n
)
(\bar{P}-n)
(Pˉ−n)为中心的球体被视为曲面的内部,而以
(
P
ˉ
+
n
ˉ
)
(\bar{P} + \bar{n})
(Pˉ+nˉ)为中心的球体被视为曲面的外部。选择与射线原点位于曲面同一侧的球体,在此单位球体内选取一个随机点
S
S
S,然后将射线从命中点
P
P
P发送到随机点
S
S
S:
我们需要一种在单位球体中选择随机点的方法。 我们将使用最简单的算法:拒绝算法。 首先,在单位立方体中选择一个随机点,
x
、
y
、
z
x、y、z
x、y、z的范围都在
[
−
1
,
1
]
[-1,1]
[−1,1]之间,如果该点在球体外部则拒绝该点。
v
e
c
3.
h
:
vec3.h:
vec3.h:
//成员函数
static vec3 random() {
return vec3(random_double(), random_double(), random_double());
}
static vec3 random(double min, double max) {
return vec3(random_double(min, max), random_double(min, max), random_double(min, max));
}
//普通函数
vec3 random_in_unit_sphere() {
while (true) {
vec3 p = vec3::random(-1, 1);
if (p.length_squared() >= 1)
continue;
return p;
}
}
修改一下
r
a
y
_
c
o
l
o
r
(
)
ray\_color()
ray_color()函数:
m
a
i
n
.
c
c
:
main.cc:
main.cc:
color ray_color(const ray& r, const hittable& world) {
hit_record rec;
if (world.hit(r, 0, infinity, rec)) {
point3 target = rec.p + rec.normal + random_in_unit_sphere();//随机点S
return 0.5 * ray_color(ray(rec.p, target - rec.p), world);
}
vec3 unit_direction = unit_vector(r.direction());
double t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
8.2 Limiting the Number of Child Rays (限制递归深度)
你可能已经发现
r
a
y
_
c
o
l
o
r
ray\_color
ray_color中存在的问题了,它是一个递归函数,什么时候终止递归?当射线和球体没有交点时。但是如果不加一个递归深度的限制,它可能会一直递归下去直到
s
t
a
c
k
b
o
o
m
stack\ boom
stack boom。
m
a
i
n
.
c
c
:
main.cc:
main.cc:
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
if (depth <= 0)
return color(0, 0, 0);
if (world.hit(r, 0, infinity, rec)) {
point3 target = rec.p + rec.normal + random_in_unit_sphere();//随机点S
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth - 1);
}
vec3 unit_direction = unit_vector(r.direction());
double t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
const int max_depth = 50;
结果如下图所示:
8.3 Using Gamma Correction for Accurate Color Intensity (使用Gamma校正获得准确的颜色强度)
这些球体看起来应该很亮(在现实生活中为浅灰色)。 但是图片却很暗,这是因为几乎所有图像查看器都假定图像是经过“伽马校正”的,这意味着
[
0
,
1
]
[0,1]
[0,1]的值在存储为字节之前会有一些变换。 初步近似,我们可以使用“伽玛2”,这意味着将颜色提升为
(
1
/
G
a
m
m
a
)
(1/Gamma)
(1/Gamma)的幂,即
(
R
G
B
)
½
(RGB)^{½}
(RGB)½。
c
o
l
o
r
.
h
:
color.h:
color.h:
void write_color(std::ostream& out, color pixel_color,int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
//根据样本数对颜色取平均值
auto scale = 1.0 / samples_per_pixel;
r = sqrt(r * scale);
g = sqrt(g * scale);
b = sqrt(b * scale);
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}
结果正如我们所期望的:
8.4 Fixing Shadow Acne (修复暗疮问题)
还有一个细微的错误。 某些反射光线不是在精确的
t
=
0
t = 0
t=0处撞击到它们所反射的对象,而是在
t
=
−
0.0000001
t = -0.0000001
t=−0.0000001或
t
=
0.00000001
t = 0.00000001
t=0.00000001或球体相交给我们的任何浮点近似值处。 因此,我们需要忽略非常接近零的值。这样就可以修正
S
h
a
d
o
w
A
c
n
e
Shadow \ Acne
Shadow Acne(暗疮问题,仔细看会发现上图中有很多黑色的点)。
m
a
i
n
.
c
c
:
main.cc:
main.cc:
if (world.hit(r, 0.001, infinity, rec)) {
效果如下图所示,大家可以对比一下:
8.5 True Lambertian Reflection (真正的朗伯反射)
这里介绍了一下真正的
L
a
m
b
e
r
t
i
a
n
R
e
f
l
e
c
t
i
o
n
Lambertian\ Reflection
Lambertian Reflection,我们之前所用的方法其实不够均匀,感兴趣的可以看一下。
v
e
c
3.
h
:
vec3.h:
vec3.h:
vec3 random_unit_vector() {
auto a = random_double(0, 2 * pi);
auto z = random_double(-1, 1);
auto r = sqrt(1 - z * z);
return vec3(r * cos(a), r * sin(a), z);
}
它是
r
a
n
d
o
m
_
i
n
_
u
n
i
t
_
s
p
h
e
r
e
(
)
random\_in\_unit\_sphere()
random_in_unit_sphere()的替代函数。
m
a
i
n
.
c
c
:
main.cc:
main.cc:
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
if (depth <= 0)
return color(0, 0, 0);
if (world.hit(r, 0.001, infinity, rec)) {
point3 target = rec.p + rec.normal + random_unit_vector();//随机点S
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth - 1);
}
vec3 unit_direction = unit_vector(r.direction());
double t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
因为我们的场景太过简单,所以很难说出这两种方法的区别,但是可以明显的看出两点:
1.
1.
1.更改后阴影不那么明显;
2.
2.
2.更改后两个球体都更加明亮了。这两个变化都是由于光线的散射更加均匀,朝法线散射的光线更少。 这意味着对于散射的物体,它们会显得更亮,因为更多的光会朝着相机反射。 对于阴影,较少的光直接向上反射,因此,较小球体下方较大球体的那一部分变得更亮了。
8.6 An Alternative Diffuse Formulation (另一种漫反射公式)
作者这里还是在讨论自己的
l
a
z
y
h
a
c
k
lazy\ hack
lazy hack和
L
a
m
b
e
r
t
i
a
n
R
e
f
l
e
c
t
i
o
n
Lambertian\ Reflection
Lambertian Reflection,并提出了一种新的近似方式:对于远离交点的所有角度都具有统一的散射方向,而不依赖于与法线的角度。许多第一批射线追踪论文都使用这种扩散方法(在采用朗伯散射之前)。
v
e
c
3.
h
:
vec3.h:
vec3.h:
vec3 random_in_hemisphere(const vec3& normal) {
vec3 in_unit_sphere = random_in_unit_sphere();
if (dot(in_unit_sphere, normal) > 0.0)// In the same hemisphere as the normal
return in_unit_sphere;
else
return -in_unit_sphere;
}
修改 m a i n . c c main.cc main.cc中的 r a y _ c o l o r ( ) ray\_color() ray_color()。
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
if (depth <= 0)
return color(0, 0, 0);
if (world.hit(r, 0.001, infinity, rec)) {
//point3 target = rec.p + rec.normal + random_unit_vector();//随机点S
point3 target = rec.p + random_in_hemisphere(rec.normal);
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth - 1);
}
vec3 unit_direction = unit_vector(r.direction());
double t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
在本书中,场景将会变得更加复杂。作者在这里鼓励读者切换漫反射器并观察它们的区别。通过了解不同漫反射方法对场景照明的影响,您可以获得有价值的见解。
9. Metal (金属)
9.1 An Abstract Class for Materials (材质的抽象)
如果我们希望不同的物体使用不同的材料,则需要进行设计决策。 我们可以创建一个具有很多参数的通用材料,而不同的材料类型只是将其中一些参数归零。 这不是一个坏方法;或者我们可以用一个抽象的材料类来封装行为。 我是后一种方法的粉丝。 对于我们的程序,材料需要做两件事:
1.
1.
1.产生散射射线(或说它吸收了入射射线);
2.
2.
2.如果发生了散射,应该将射线衰减多少。
m
a
t
e
r
i
a
l
.
h
:
material.h:
material.h:
#ifndef MATERIAL_H
#define MATERIAL_H
#include"rtweekend.h"
struct hit_record;
class material {
public:
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0;
};
#endif // ! MATERIAL_H
9.2 A Data Structure to Describe Ray-Object Intersections (描述交点的数据结构)
我们使用
h
i
t
_
r
e
c
o
r
d
hit\_record
hit_record是为了避免函数需要过多的参数,当然你也可以采用后者的办法。由于
H
i
t
t
a
b
l
e
s
Hittables
Hittables和
m
a
t
e
r
i
a
l
s
materials
materials需要相互了解,所以我们需要修改一下
h
i
t
_
r
e
c
o
r
d
hit\_record
hit_record。
h
i
t
t
a
b
l
e
.
h
:
hittable.h:
hittable.h:
class material;
struct hit_record {
point3 p; //交点
vec3 normal; //法向量
shared_ptr<material> mat_ptr;
double t; //距离
bool front_face;
inline void set_face_normal(const ray& r, const vec3& outward_normal) {
front_face = dot(r.direction(), outward_normal) < 0;
normal = front_face ? outward_normal : -outward_normal;
}
};
当光线撞击表面(例如特定的球体)时,
h
i
t
_
r
e
c
o
r
d
hit\_record
hit_record中的材质指针将设置为在
m
a
i
n
(
)
main()
main()中设置该球体时所给定的材质指针。所以我们需要修改
s
p
h
e
r
e
sphere
sphere的代码,为其添加一个材质指针。
s
p
h
e
r
e
.
h
:
sphere.h:
sphere.h:
class sphere :public hittable {
public:
sphere() { }
sphere(point3 cen, double r, shared_ptr<material> m) :center(cen), radius(r), mat_ptr(m) { }
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const override;
public:
point3 center;
double radius;
shared_ptr<material> mat_ptr;
};
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
vec3 oc = r.origin() - center;
double a = r.direction().length_squared();
double half_b = dot(r.direction(), oc);
double c = oc.length_squared() - radius * radius;
double discriminant = half_b * half_b - a * c;
if (discriminant > 0) {
double root = sqrt(discriminant);
double t = (-half_b - root) / a;
if (t > t_min&& t < t_max) {
rec.t = t;
rec.p = r.at(t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
rec.mat_ptr = mat_ptr;
return true;
}
t = (-half_b + root) / a;
if (t > t_min&& t < t_max) {
rec.t = t;
rec.p = r.at(t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
rec.mat_ptr = mat_ptr;
return true;
}
}
return false;
}
9.3 Modeling Light Scatter and Reflectance
对于我们已经存在的朗伯(漫反射)情况,它既可以始终散射并通过其反射率
R
R
R进行衰减,也可以不衰减地散射但需要吸收
1
−
R
1-R
1−R部分的光线,或者可以混合使用这些策略 。 对于朗伯材料,我们得到以下简单类:
m
a
t
e
r
i
a
l
.
h
:
material.h:
material.h:
#include"hittable.h"
class lambertian :public material {
public:
lambertian(const color& a) :albedo(a) { }
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override {
vec3 scatter_direction = rec.normal + random_unit_vector();
scattered = ray(rec.p, scatter_direction);
attenuation = albedo;
return true;
}
public:
color albedo;
};
9.4 Mirrored Light Reflection (镜面反射)
对于光滑的金属,射线不会被随机散射,但是会被反射。
红色的反射射线为
v
ˉ
+
2
∗
b
ˉ
\bar{v} + 2*\bar{b}
vˉ+2∗bˉ。 在我们的设计中,
n
ˉ
\bar{n}
nˉ是单位矢量,但
v
ˉ
\bar{v}
vˉ可能不是。
b
ˉ
\bar{b}
bˉ的长度应为
v
ˉ
⋅
n
ˉ
\bar{v}⋅\bar{n}
vˉ⋅nˉ。 考虑到
v
ˉ
\bar{v}
vˉ的指向,公式应该为:
v
e
c
3.
h
:
vec3.h:
vec3.h:
vec3 reflect(const vec3& v, const vec3& n) {
return v - 2 * dot(v, n) * n;
}
m a t e r i a l . h : material.h: material.h:
class metal :public material {
public:
metal(const color& a) :albedo(a) { }
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected);
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
public:
color albedo;
};
m a i n . c c : main.cc: main.cc:
#include"material.h"
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
if (depth <= 0)
return color(0, 0, 0);
if (world.hit(r, 0.001, infinity, rec)) {
ray scattered;
color attenuation;
if (rec.mat_ptr->scatter(r, rec, attenuation, scattered))
return attenuation * ray_color(scattered, world, depth - 1);
return color(0, 0, 0);
}
vec3 unit_direction = unit_vector(r.direction());
double t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
9.5 A Scene with Metal Spheres (添加金属球体)
现在让我们给场景中添加一些金属球体吧。
m
a
i
n
.
c
c
:
main.cc:
main.cc:
//World
hittable_list world;
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.7, 0.3, 0.3));
auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8));
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2));
world.add(make_shared<sphere>(point3(0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3(0.0, 0.0, -1.0), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3(1.0, 0.0, -1.0), 0.5, material_right));
结果如下图所示:
9.6 Fuzzy Reflection (模糊反射)
我们还可以随机化反射方向。
球体越大,反射将变得越模糊。 建议添加一个模糊度参数,该参数仅是球体的半径。 要注意的是,对于大球体或掠食性射线,我们可能会散射到表面以下, 我们可以让表面吸收那些。
m
a
t
e
r
i
a
l
.
h
:
material.h:
material.h:
class metal :public material {
public:
metal(const color& a, double f) :albedo(a), fuzz(f < 1 ? f : 1) { }
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected + fuzz * random_in_unit_sphere());
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
public:
color albedo;
double fuzz;
};
我们可以通过在金属上添加模糊度
0.3
0.3
0.3和
1.0
1.0
1.0来进行尝试:
m
a
i
n
.
c
c
:
main.cc:
main.cc:
auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8), 0.3);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
结果应如下图所示:
10. Dielectrics (电解质)
水、玻璃和钻石等透明材料是电介质。 当光线射到它们上时,它分裂为反射射线和折射(透射)射线。 我们将通过在反射和折射之间随机选择,并且每次交互仅生成一条散射射线来解决这一问题。
10.1 Refraction (折射)
最难调试的部分是折射射线。 为了调试,我通常会将所有光线折射。 对于这个项目,我尝试在场景中放置两个玻璃球:
这张图片是正确的吗?显然不是,世界应该颠倒过来,并且没有奇怪的黑色东西。
10.2 Snell’s Law (斯涅尔/折射定律)
斯涅尔定律:
那么有:
设折射光线为
R
′
ˉ
\bar{R'}
R′ˉ,与它位于同一边的法线为
n
ˉ
\bar{n}
nˉ,则可以将
R
′
R'
R′分成两部分,一部分平行于
n
ˉ
\bar{n}
nˉ,另一部分垂直于
n
ˉ
\bar{n}
nˉ。
不难计算得到(推导过程略去):
替换
c
o
s
θ
cosθ
cosθ可得:
v
e
c
3.
h
:
vec3.h:
vec3.h:
vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat) {
auto cos_theta = dot(-uv, n);
vec3 r_out_perp = etai_over_etat * (uv + cos_theta * n);
vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_perp.length_squared())) * n;
return r_out_perp + r_out_parallel;
}
m a t e r i a l . h : material.h: material.h:
class dielectric :public material {
public:
dielectric(double ri) :ref_idx(ri) { }
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override {
attenuation = color(1.0, 1.0, 1.0);
//判断交点是在外部还是内部 front_face为true时-->外部
double etai_over_etat = rec.front_face ? (1.0 / ref_idx) : ref_idx;
vec3 unit_direction = unit_vector(r_in.direction());
vec3 refracted = refract(unit_direction, rec.normal, etai_over_etat);
scattered = ray(rec.p, refracted);
return true;
}
public:
double ref_idx;
};
m a i n . c c : main.cc: main.cc:
//World
hittable_list world;
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<dielectric>(1.5);
auto material_left = make_shared<dielectric>(1.5);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
结果如下图所示:
10.3 Total Internal Reflection (全内反射)
之前的计算过程有些问题:我们没有考虑全内反射的情况。
显然,当右边的值
>
1.0
>1.0
>1.0的时候,必然不可能发生折射。
m
a
t
e
r
i
a
l
.
h
:
material.h:
material.h:
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override {
attenuation = color(1.0, 1.0, 1.0);
//判断交点是在外部还是内部 front_face为true时-->外部
double etai_over_etat = rec.front_face ? (1.0 / ref_idx) : ref_idx;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
if (etai_over_etat * sin_theta > 1.0) { //全内反射
vec3 reflected = reflect(unit_direction, rec.normal);
scattered = ray(rec.p, reflected);
return true;
}
vec3 refracted = refract(unit_direction, rec.normal, etai_over_etat);
scattered = ray(rec.p, refracted);
return true;
}
衰减始终为1,因为玻璃表面不吸收任何东西。
m
a
i
n
.
c
c
:
main.cc:
main.cc:
//World
hittable_list world; auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left = make_shared<dielectric>(1.5);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 0.0);
10.4 Schlick Approximation (Schlick近似)
真实的玻璃具有随角度变化的反射率—以陡峭的角度看窗户,它将变成一面镜子。 有一个复杂的方程可以帮我们实现这种效果,但几乎每个人都不会使用它,而是使用
C
h
r
i
s
t
o
p
h
e
S
c
h
l
i
c
k
Christophe\ Schlick
Christophe Schlick的多项式近似。
m a t e r i a l . h : material.h: material.h:
double schlick(double cosine, double ref_idx) {
auto r0 = (1 - ref_idx) / (1 + ref_idx);
r0 *= r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override {
attenuation = color(1.0, 1.0, 1.0);
//判断交点是在外部还是内部 front_face为true时-->外部
double etai_over_etat = rec.front_face ? (1.0 / ref_idx) : ref_idx;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
if (etai_over_etat * sin_theta > 1.0) { //全内反射
vec3 reflected = reflect(unit_direction, rec.normal);
scattered = ray(rec.p, reflected);
return true;
}
double reflect_prob = schlick(cos_theta, etai_over_etat);//反射率
if (random_double() < reflect_prob) {
vec3 reflected = reflect(unit_direction, rec.normal);
scattered = ray(rec.p, reflected);
return true;
}
vec3 refracted = refract(unit_direction, rec.normal, etai_over_etat);
scattered = ray(rec.p, refracted);
return true;
}
10.5 Modeling a Hollow Glass Sphere (空心玻璃球)
使用介质球的一个有趣且简单的技巧是使用负半径,它的几何形状不会受到影响,但表面法线会指向内部。可以用来制作空心玻璃球。
m
a
i
n
.
c
c
:
main.cc:
main.cc:
world.add(make_shared<sphere>(point3(0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3(0.0, 0.0, -1.0), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), -0.4, material_left));
world.add(make_shared<sphere>(point3(1.0, 0.0, -1.0), 0.5, material_right));
11. Positionable Camera (可定位的相机)
相机和电介质一样,调试起来很麻烦。所以我总是循序渐进。首先,让我们允许一个可调视野(
f
o
v
fov
fov)。这是你透过入口看到的角度。因为我们的图像不是方形的,所以视场在水平和垂直方向上是不同的。我总是用垂直视野。我通常也会用度数来指定它,并在构造函数中变成弧度——这是个人喜好的问题。
11.1 Camera Viewing Geometry (摄像机观察几何)
之前我们使光线从原点发出并前进到
z
=
−
1
z = -1
z=−1平面,其实可以让他前进到任何一个平面。
当
f
o
c
a
l
_
l
e
n
g
t
h
=
1
focal\_length=1
focal_length=1时,显然有
h
=
t
a
n
(
θ
/
2
)
h=tan(θ/2)
h=tan(θ/2)。
c
a
m
e
r
a
.
h
:
camera.h:
camera.h:
class camera {
public:
camera(double vfov, double aspect_ratio) {
auto theta = degrees_to_radians(vfov);
auto h = tan(theta / 2);
auto viewport_height = 2.0 * h;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.0;
origin = point3(0, 0, 0);
horizontal = vec3(viewport_width, 0.0, 0.0);
vertical = vec3(0.0, viewport_height, 0.0);
//视口左下角的坐标
lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3(0, 0, focal_length);
}
ray get_ray(double u, double v) const {
return ray(origin, lower_left_corner + u * horizontal + v * vertical - origin);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};
m a i n . c c : main.cc: main.cc:
//World
auto R = cos(pi / 4);
hittable_list world;
auto material_left = make_shared<lambertian>(color(0, 0, 1));
auto material_right = make_shared<lambertian>(color(1, 0, 0));
world.add(make_shared<sphere>(point3(-R, 0, -1), R, material_left));
world.add(make_shared<sphere>(point3(R, 0, -1), R, material_right));
//Camera
camera cam(90, aspect_ratio);
结果如下:
11.2 Positioning and Orienting the Camera (摄像头的定位和定向)
为了可以随意设定观察点,我们需要定义两个变量。
l
o
o
k
f
r
o
m
look\ from
look from代表摄像机的位置,
l
o
o
k
a
t
look\ at
look at代表正在观察的点。相机还可以进行旋转,所以我们还需要一个向量
u
p
up
up,它位于与视图垂直的平面上。
你可以使用任意的
u
p
up
up,将其映射到相机所在的平面上即可得到针对相机的
u
p
up
up,那么我们很容易构造出一个正交基础
(
u
,
v
,
w
)
(u,v,w)
(u,v,w)来描述相机的位置,如下图所示:
c
a
m
e
r
a
.
h
:
camera.h:
camera.h:
class camera {
public:
camera(point3 lookfrom, point3 lookat, vec3 vup, double vfov, double aspect_ratio) {
auto theta = degrees_to_radians(vfov);
auto h = tan(theta / 2);
auto viewport_height = 2.0 * h;
auto viewport_width = aspect_ratio * viewport_height;
auto w = unit_vector(lookfrom - lookat);
auto u = unit_vector(cross(vup, w));
auto v = cross(w, u);
origin = lookfrom;
horizontal = viewport_width * u;
vertical = viewport_height * v;
lower_left_corner = origin - horizontal / 2 - vertical / 2 - w;
}
ray get_ray(double u, double v) const {
return ray(origin, lower_left_corner + u * horizontal + v * vertical - origin);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};
m a i n . c c : main.cc: main.cc:
//World
hittable_list world;
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.1, 0.2, 0.5));
auto material_left = make_shared<dielectric>(1.5);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 0.0);
world.add(make_shared<sphere>(point3(0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3(0.0, 0.0, -1.0), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), -0.45, material_left));
world.add(make_shared<sphere>(point3(1.0, 0.0, -1.0), 0.5, material_right));
//Camera
camera cam(point3(-2, 2, 1), point3(0, 0, -1), vec3(0, 1, 0), 90, aspect_ratio);
结果:
再次修改摄像机:
camera cam(point3(-2, 2, 1), point3(0, 0, -1), vec3(0, 1, 0), 20, aspect_ratio);
结果:
12.Defocus Blur (离焦模糊)
我们最终要实现的功能是散焦模糊,上面讲了原因。
12.1 A Thin Lens Approximation
真实的相机具有复杂的复合镜头。 我们可以在代码中模拟:传感器,然后是镜头,然后是光圈。 然后,我们可以找出光线应该发送到哪里,并在计算出图像后翻转图像(图像上下颠倒地投影在胶片上)。 但是,图形学中通常使用薄透镜近似值:
我们不需要模拟相机的内部。 因为在相机外部渲染图像,模拟内部既复杂又没有必要。 取而代之的是,我通常从镜头发出光线,然后将它们发送到聚焦平面(距离镜片
f
o
c
u
s
_
d
i
s
t
focus\_dist
focus_dist远)。
12.2 Generating Sample Rays (生成采样光线)
通常,所有场景射线均来自于视点。 为了实现散焦模糊,我们需要在视点周围()随机生成射线。 半径越大,散焦模糊越大。 您可以认为我们的原始相机具有一个半径为零的散焦盘(完全没有模糊),因此所有光线都起源于盘中心(
l
o
o
k
f
r
o
m
lookfrom
lookfrom)。
v
e
c
3.
h
:
vec3.h:
vec3.h:
vec3 random_in_unit_disk() {
while (true) {
auto p = vec3(random_double(-1, 1), random_double(-1, 1), 0);
if (p.length_squared() >= 1)
continue;
return p;
}
}
c a m e r a . h : camera.h: camera.h:
#ifndef CAMERA_H
#define CAMERA_H
#include"rtweekend.h"
class camera {
public:
camera(point3 lookfrom, point3 lookat, vec3 vup, double vfov, double aspect_ratio,double aperture,double focus_dist) {
auto theta = degrees_to_radians(vfov);
auto h = tan(theta / 2);
auto viewport_height = 2.0 * h;
auto viewport_width = aspect_ratio * viewport_height;
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
origin = lookfrom;
horizontal = focus_dist * viewport_width * u;
vertical = focus_dist * viewport_height * v;
lower_left_corner = origin - horizontal / 2 - vertical / 2 - focus_dist * w;
lens_radius = aperture / 2;
}
ray get_ray(double s, double t) const {
vec3 rd = lens_radius * random_in_unit_disk();
vec3 offset = u * rd.x() + v * rd.y();
return ray(origin + offset, lower_left_corner + s * horizontal + t * vertical - origin - offset);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
vec3 u, v, w;
double lens_radius;
};
#endif // !CAMERA_H
m a i n . c c : main.cc: main.cc:
//Camera
point3 lookfrom(3, 3, 2);
point3 lookat(0, 0, -1);
vec3 vup(0, 1, 0);
auto dist_to_focus = (lookfrom - lookat).length();
auto aperture = 2.0;
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus);
效果:
13.Where Next?
13.1 A Final Render
首先,让我们向场景中添加球体!
m
a
i
n
.
c
c
:
main.cc:
main.cc:
#include<iostream>
#include"camera.h"
#include"rtweekend.h"
#include"color.h"
#include"hittable_list.h"
#include"sphere.h"
#include"material.h"
using std::cin;
using std::cout;
hittable_list random_scene() {
hittable_list world;
auto ground_material = make_shared<lambertian>(color(0.5, 0.5, 0.5));//地面
world.add(make_shared<sphere>(point3(0, -1000, 0), 1000, ground_material));
for (int a = -11; a < 11; a++) {
for (int b = -11; b < 11; b++) {
auto choose_mat = random_double(); //随机材质
point3 center(a + 0.9 * random_double(), 0.2, b + 0.9 * random_double()); //随机中心
if ((center - point3(4, 0.2, 0)).length() > 0.9) {
shared_ptr<material> sphere_material;
if (choose_mat < 0.8) {
// diffuse 漫反射
auto albedo = color::random() * color::random();
sphere_material = make_shared<lambertian>(albedo);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
}
else if (choose_mat < 0.95) {
// metal 金属
auto albedo = color::random(0.5, 1);
auto fuzz = random_double(0, 0.5);
sphere_material = make_shared<metal>(albedo, fuzz);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
}
else {
// glass 玻璃
sphere_material = make_shared<dielectric>(1.5);
world.add(make_shared<sphere>(center, 0.2, sphere_material));
}
}
}
}
auto material1 = make_shared<dielectric>(1.5);
world.add(make_shared<sphere>(point3(0, 1, 0), 1.0, material1));
auto material2 = make_shared<lambertian>(color(0.4, 0.2, 0.1));
world.add(make_shared<sphere>(point3(-4, 1, 0), 1.0, material2));
auto material3 = make_shared<metal>(color(0.7, 0.6, 0.5), 0.0);
world.add(make_shared<sphere>(point3(4, 1, 0), 1.0, material3));
return world;
}
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
if (depth <= 0)
return color(0, 0, 0);
if (world.hit(r, 0.001, infinity, rec)) {
ray scattered;
color attenuation;
if (rec.mat_ptr->scatter(r, rec, attenuation, scattered))
return attenuation * ray_color(scattered, world, depth - 1);
return color(0, 0, 0);
}
vec3 unit_direction = unit_vector(r.direction());
double t = 0.5 * (unit_direction.y() + 1.0);
//线性插值
return (1.0 - t) * color(1.0, 1.0, 1.0) + t * color(0.5, 0.7, 1.0);
}
int main() {
//Image
const double aspect_ratio = 3.0 / 2.0;
const int image_width = 1200;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 500;
const int max_depth = 50;
//World
auto world = random_scene();
//Camera
point3 lookfrom(12, 2, 3);
point3 lookat(0, 0, 0);
vec3 vup(0, 1, 0);
auto dist_to_focus = 10.0;
auto aperture = 0.1;
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus);
//Render
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) {
color pixel_color(0, 0, 0);//求和
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width - 1);
auto v = (j + random_double()) / (image_height - 1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world, max_depth);
}
write_color(cout, pixel_color, samples_per_pixel);//取平均值
}
}
}
效果(这需要花费相当长的时间去渲染,你可以减少递归深度和取样点的个数):
您可能会注意到的一件有趣的事情是,玻璃球实际上没有阴影,这使它们看起来像是漂浮的—这不是 b u g bug bug。
13.2 Next Steps
您现在有了一个很酷的光线追踪器!接下来是什么?