目标:
使用vscodeIDE编写代码,这是我的配置
学习这个教程,完成一个简易的光线追踪器开发
1·输出PPM图像
在不使用 opengl (渲染图像)/ std_image.h(加载图像)等库的情况下,怎样通过代码自定义图像?
有一种PPM格式的图像,可以通过定义数据生成简单的图像
首先p3表明颜色是ASCII码格式,接着指定分辨率,和最大颜色值之后下面的都是RGB颜色值 ,
我们可以通过代码设置这些数据:
首先指定p3和分辨率(200宽100高)和255最大颜色值,然后设置每个像素颜色值,首先在0---1之间,然后转为0---255的颜色
对于PPM来说从左往右,从上到下写入的,因此首先输出的数据是左上,最后输出的是右下
#include <iostream>
int main()
{
const int image_width = 200;
const int image_height = 100;
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;
auto g = double(j) / image_height;
auto b = 0.2;
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';
}
}
}
我们已经有了所有数据,通过新建终端,然后执行命令
我们可以运行exe,并将cout输出的内容写入到PPM格式的文件中
加载ppm文件的网站
最终效果:
2·VEC3文件
用vec3`类来储存所有的颜色, 位置, 方向, 偏移等
它是一个有3个double类型数据的数组,重载了+加-减*乘/除,dot()点乘cross()叉乘,unit_vector()标准化等矢量运算
构造函数会接收3个数据,作为向量
write_color()函数,可以让值转为0---255之间
像素颜色设置,通过调用自定义的API,还是之前显示结果
vec3 color(double(i) / image_width, double(j) / image_height, 0.2);
color.write_color(std::cout);
3·ray类
光线是一个射线,a原点,t ,double值,b方向
vec3 at(double t) const:返回t时间下,ray的向量
在main中,控制的从每个像素发射的ray方向为x(-1--1)y(-2----2)
vec3 lower_left_corner(-2.0, -1.0, -1.0);
vec3 horizontal(4.0, 0.0, 0.0);
vec3 vertical(0.0, 2.0, 0.0);
for (int j = image_height - 1; j >= 0; --j)
{
for (int i = 0; i < image_width; ++i){
auto u = double(i) / image_width;//0--1
auto v = double(j) / image_height;//1--0
//u*horizontal: 0--4
//v*vertical: 2--0
//lower_left_corner + :x:-2--2, y:1-- -1
ray r(origin, lower_left_corner + u*horizontal + v*vertical);
}
}
ray_color(ray)函数根据y值将蓝白做了个线性插值的混合
vec3 ray_color(const ray& r) {/* 根据ray的y值,返回渐变的蓝白色 */
vec3 unit_direction = unit_vector(r.direction());/* 单位化光线 */
auto t = 0.5*(unit_direction.y() + 1.0);/* 1---0 */
return (1.0-t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);/* 从上到下,从蓝到白 */
}
完成效果:
4·渲染球体
隐式表示:
这是球心在0,0,0位置的方程,所有球面的点xyz都满足=r^2,如果<r^2则点在内部,否则在外部
如果球心在cx,cy,cz,则
其中r可以写为点p和球心c的向量形式(等价) ,最终的球面方程:
那么光线和球体相交,两个方程都满足,即光线为t时间时的坐标,作为点p满足在球体表面
唯一的未知数就是t,即关于t的一个一元二次方程
,通过求根公式判断交点个数、
我们创建一个球心在vec3(0, 0, -1),半径为0.5的球体,根据hit_sphere()光线和球体 求交,如果相交,返回颜色值return vec3(1, 0, 0);,否则仍然是原来的背景
bool hit_sphere(const vec3 ¢er, double radius, const ray &r)
{
vec3 oc = r.origin() - center;
auto a = dot(r.direction(), r.direction());
auto b = 2.0 * dot(oc, r.direction());
auto c = dot(oc, oc) - radius * radius;
auto discriminant = b * b - 4 * a * c;/* 当 b2−4ac>=0 时,表示二次函数与x轴有至少一个交点 */
return (discriminant > 0);
}
最终效果:
5·着色
对于hit_sphere()我们返回一个double值,如果光线物体没有交点,返回-1,否则返回t的结果(光线在t时间与球体相交)
if (discriminant < 0) {
return -1.0;
}
else {
return (-b - sqrt(discriminant) ) / (2.0*a);
}
我们求解物体的法线(光线 - 球心),然后根据法线向量,简单的为球体着色
auto t = hit_sphere(vec3(0,0,-1), 0.5, r);
if (t > 0.0) {
vec3 N = unit_vector(r.at(t) - vec3(0,0,-1));
return 0.5*vec3(N.x()+1, N.y()+1, N.z()+1);
}
结果:
6·优化代码
项1:求根公式
我们可以把求根公式中的b替换为2h,经过展开移项后
因此,简化后的代码
double hit_sphere(const vec3 ¢er, double radius, const ray &r)
{
vec3 oc = r.origin() - center;
auto a = r.direction().length_squared();
auto half_b = dot(oc, r.direction());
auto c = oc.length_squared() - radius*radius;
auto discriminant = half_b*half_b - a*c;
if (discriminant < 0) {
return -1.0;
} else {
return (-half_b - sqrt(discriminant) ) / a;
}
}
项2:物体继承体系
我们建立一个物体基类叫做hittable,其中STRUCT hit_record记录交点的信息,hit()虚函数用来判断是否有交点
建立一个sphere派生类继承hittable
项3:法线方向
我们想要物体在内外方向都有法线,因此如果光线和法线dot>0即同方向即光线从球体内部照射,则法线反转,否则不反转
在物体基类STRUCT hit_record中引入了,和新建了set_face_normal()函数,用来反转法线,并在求交时加入射入面的判别
double t;
bool front_face;//是否是正面
项4:物体列表
我们新建一个hittable_list物体列表类继承hittable,维护一个std::vector<shared_ptr<hittable>>物体列表
并且重写了hit函数,其中会对for (const auto &object : objects)物体列表的所有物体依次求交
并且维护了t_min, closest_so_far参数,负责深度测试
项5:常用函数和工具
rtweekend.h类
7·两个球体
在main函数中,创建两个球体,大的球体作为地板
hittable_list world;
world.add(make_shared<sphere>(vec3(0, 0, -1), 0.5));
world.add(make_shared<sphere>(vec3(0, -100.5, -1), 100));
然后调用hittable_list类中的hit函数,求交点信息更新像素颜色
结果:
8·反走样/抗锯齿
MSAA:每个像素增加到samples_per_pixel个采样点,每个采样点的颜色值为 1 / samples_per_pixel的权重
首先在rtweekend.h添加随机数生成器和clamp()函数(负责将值限制在min和max之间)
然后抽象出轴对齐摄像机类,也就是屏幕像素
最后结果与对比: