Ray Tracing in One Weekend(中文翻译)

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

文章目录

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 rgb [ 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ˉ+tbˉ 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
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值