文章目录
- 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 <&l

本文档详细介绍了如何使用C++从零开始实现一个光线追踪器,涵盖了光线追踪的基础概念、PPM图片格式、vec3类的设计、光线与球体求交、抗锯齿处理、不同材质的模拟等内容。

5575

被折叠的 条评论
为什么被折叠?



