非常著名的光线追踪教程。
先看画出一个类似于天空的图像需要哪些内容。
具体的解析都在注释里:
//vec3.h
//vec3 类的头文件,它定义了一个三维向量类 vec3。这个类有三个成员变量 e0、e1 和 e2 表示向量的三个分量,
//即向量在三个坐标轴上的投影。同时还定义了一些向量运算符,如向量加减法、点乘、叉乘、标量乘法和除法等,
//以及一些向量的操作函数,如向量的长度、单位向量等。这些函数和运算符可以对 vec3 类对象进行操作,返回一个
//新的 vec3 类对象或修改原有的 vec3 类对象。同时,这个头文件还定义了一些别名,如 point3 表示三维空间中的一个点,color 表示颜色。
#ifndef VEC3_H
#define VEC3_H
#include<cmath>
#include<iostream>
using std::sqrt;
class vec3
{
public:
vec3() : e{0,0,0}{}
vec3(float e0, float e1, float e2) : e{ e0,e1,e2 } {}
float x() const { return e[0]; }
float y() const { return e[1]; }
float z() const { return e[2]; }
vec3 operator~() const { return vec3(-e[0], -e[1], -e[2]); }
float operator[](int i) const { return e[i]; }//返回向量的某个分量的值
float& operator[](int i) { return e[i]; }//返回向量的某个分量的引用,可以修改这个分量的值
//将对当前向量对象进行修改,并返回修改后的对象
//将另一个向量 v 的分量加到当前向量对象 e 中,实现了向量加法的功能
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 float t)
{
e[0] *= t;
e[1] *= t;
e[2] *= t;
return *this;
}
vec3& operator/=(const float t)
{
return *this *= 1 / t;
}
float length() const
{
return sqrt(length_squared());
}
float length_squared() const
{
return e[0] * e[0] + e[1] * e[1] + e[2] * e[2];
}
public:
float e[3];
};
//type aliases for vec3
using point3 = vec3;
using color = 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 * (float t, const vec3& v)
{
return vec3(t * v.e[0], t * v.e[1], t * v.e[2]);
}
inline vec3 operator * (const vec3& v, float t)
{
return t * v;
}
inline vec3 operator / (vec3& u, float t)
{
return (1 / t) * u;
}
inline float 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();
}
#endif // ! VEC3_H
//color.h
#pragma once
#ifndef COLOR_H
#define COLOR_H
#include"vec3.h"
#include<iostream>
void write_color(std::ostream& out, color pixel_color)
{
//write the translated[0,255] value of each color component;
//在渲染图像时,我们通常需要将颜色值从浮点数格式转换为整数格式,并将其存储为图像文件中每个像素的 RGB 值。
//而 C++ 中的 int 类型也是有符号的 32 位整数,因此在写入图像时,我们需要将浮点数值缩放到 0 到 255 的整数
//范围内,并将其强制转换为 int 类型。在此处,我们选择将颜色乘以 255.999,而不是 255,这是为了解决浮点数精
//度损失的问题,因为 255.999 在 IEEE 754 浮点数规范下可以精确表示。
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
//ray.h
#pragma once
#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; }
//光线可以看做是P(t) A + tb的方程,P是沿着这条线的3d位置,A是光线原点,b是光线方向
point3 at(float t) const
{
return orig + t * dir;
}
public:
point3 orig;
vec3 dir;
};
#endif // !RAY_H
//main.cpp
#include"vec3.h"
#include"ray.h"
#include"color.h"
#include<iostream>
color ray_color(const ray& r)
{
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5f * (unit_direction.y() + 1.0f);
//(1−t)×c1 + t×c2 线性插值
return (1.f - t) * color(1.f, 1.f, 1.f) + t * color(0.5f, 0.7f, 1.f);
}
int main()
{
//定义了一个关于图像和相机的基本参数:
//图像部分:定义了图像的宽高比,以及宽度和高度。宽高比是在之前的代码中定义的,宽度为400像素,高度根据宽高比计算得出。
//相机部分:定义了视口的宽高以及焦距。视口是指相机能够看到的范围,这里的视口宽度是根据宽高比和视口高度计算得出的;
// 焦距表示了相机的聚焦程度,这里设为1。还定义了相机的位置,以及一个水平方向向量、垂直方向向量和左下角的位置,这些都是
// 计算相机拍摄视角时需要用到的基本参数。
//image
const auto aspect_ratio = 16.f / 9.f;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
//camera
auto viewport_height = 2.f;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.f;
auto origin = point3(0, 0, 0);
auto horizontal = vec3(viewport_width, 0, 0);
auto vertical = vec3(0, viewport_height, 0);
auto lower_left_corner = origin - horizontal / 2 - vertical / 2 - vec3(0, 0, focal_length);
//render
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
//由于PPM文件格式规定像素行是从文件的底部开始写入的,而每一行的像素是从左向右依次排列的,
//因此在遍历时需要先从最后一行开始循环遍历,再往上逐行遍历,以保证输出的像素数据符合PPM文件格式的要求
for (int i = image_height - 1; i >= 0; --i)
{
//通过一个外层循环,从图像的最后一行开始,往前逐行扫描。
//在内层循环中,对于每行中的每个像素,计算出其在图像中的位置(即相对于左下角的水平和垂直方向上的偏移量),从而确定了该像素对应的射线方向。
//使用 ray_color() 函数计算出该射线的颜色,即该像素的颜色。
//将该像素的颜色输出到标准输出流中,以便最终能够将其保存为图像文件。
std::cerr << "\r Scanlines remaining: " << i << ' ' << std::flush;
for (int j = 0; j < image_width; ++j)
{
auto u = float(j) / (image_width - 1);
auto v = float(i) / (image_height - 1);
ray r(origin, lower_left_corner + u * horizontal + v * vertical - origin);
color pixel_color = ray_color(r);
write_color(std::cout, pixel_color);
}
}
std::cerr << "\nDone.\n";
}
最好在Release下运行,生成了exe后,在exe所在的文件夹唤出cmd,并输入
[文件名] > image.ppm
通过查看图像的软件打开ppm文件,会得到如下图:
这个渐变效果得益于ray_color函数中的插值计算。