文章目录

前言

一、Output an Image

1.The PPM Image Format

2.Creating an Image File

3.Adding a Progress Indicator

二、The vec3 Class

1.Variables and Methods

2.vec3 Utility Functions

三、Surface Normals and Multiple Objects

1.An Abstraction for Hittable Objects

2.A List of Hittable Objects

四、Diffuse Materials

1.Fixing Shadow Acne

五、Metal

1.An Abstract Class for Materials

六、Dielectrics

1.Snell's Law

2.Total Internal Reflection

七、Defocus Blur

1.A Thin Lens Approximation

总结


前言

一个周末系列的光线追踪书籍链接在这,这篇文章主要记录学习过程中C++的一些特性以及书中的一些比较难的知识。

一、Output an Image

1.The PPM Image Format

  • 重点

代码处理的颜色范围值是0.0~1.0,对应r、g、b。但是生成图像时的颜色范围要转换成0~255,对应ir、ig、ib。

for (int j = image_height-1; j >= 0; --j) {for (int i = 0; i < image_width; ++i) {auto r = double(i) / (image_width-1);auto g = double(j) / (image_height-1);auto 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);std::cout << ir << ' ' << ig << ' ' << ib << '\n';}}
  • 强制类型转换

type(expr)是C语言风格的强制类型转换,对应于代码中的double(i),在这里使用强制类型类型转换是为了保证运算过后rgb的数据类型是double。

static_cast<int>是C++风格的强制类型转换,在这里将255*r运算得到的double数据转换成int类型。注意,将一个较大的算术类型赋值给较小的算术类型可能会存在精度损失,例如将double类型转换成int类型。

2.Creating an Image File

  • 重点

这一小节的目标是保存一张ppm图像,这里给出的只是重点的代码片段,并不是完整的保存图片程序,在这里使用的是C++文件流保存图像。

std::ofstream openFile("image.ppm");//使用文件输入输出流读写文件
openFile << "P3\n" << image_width << " " << image_height << "\n255\n";void write_color(std::ofstream& openFile, color pixel_color, int samples_per_pixel) {//在pixel_color中的像素全是累加值,不能直接使用,获取像素累计值auto r = pixel_color.x();auto g = pixel_color.y();auto b = pixel_color.z();//除以采样数,得到图形学中的0到1像素,这里使用了gamma矫正auto scale = 1.0 / samples_per_pixel;r = sqrt(scale * r);g = sqrt(scale * g);b = sqrt(scale * b);//static_cast和double()都是强制类型转换,一种是C++风格,另一种是C风格openFile<< 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";
}
  • C++文件流

代码中使用的文件流对象是ofstream可以实现写文件的功能,写入文件的方式与cout类似,具体的文件操作见这篇文章。

3.Adding a Progress Indicator

此小节完整代码如下所示:

    for (int j = image_height-1; j >= 0; --j) {std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;for (int i = 0; i < image_width; ++i) {auto r = double(i) / (image_width-1);auto g = double(j) / (image_height-1);auto 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);std::cout << ir << ' ' << ig << ' ' << ib << '\n';}}std::cerr << "\nDone.\n";
  • C++错误流

std::cerr的作用是打印错误信息,cerr不经过缓冲而直接输出,一般用于迅速输出出错信息,是标准错误,默认情况下被关联到标准输出流,但它不被缓冲,也就说错误消息可以直接发送到显示器,而无需等到缓冲区或者新的换行符时,才被显示。

'\r' 回车,光标移到当前行的开头,不会换到下一行,如果接着输出的话,本行以前的内容会被逐一覆盖;\n’ 换行,光标移到下一行的开头;

std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
std::cerr << "\nDone.\n";

二、The vec3 Class

1.Variables and Methods

此小节的所有代码如下:

#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];
};// Type aliases for vec3
using point3 = vec3;   // 3D point
using color = vec3;    // RGB color#endif
  • C++头文件编写

这几个预处理语句确保头文件被多次包含仍然可以正常工作(头文件的循环包含是写程序时非常严重的错误,文章后面会提到),整个程序中的预处理变量包括头文件保护符必须唯一,通常的做法是基于头文件中类的名字来构建保护符的名字,以确保其唯一性,在这里vec是头文件的文件名,直接将VEC3_H作为保护符的名字,详细的头文件编写见这篇文章

#ifndef VEC3_H
#define VEC3_H#endif
  • C++类的构造函数

这两个构造函数的定义出现了新的部分,即冒号以及冒号和花括号之间的代码,其中花括号定义了空的函数体。新出现的部分叫做构造函数初始值列表,它负责为新创建的对象的一个或几个数据成员赋初值,构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开来。

vec3() : e{0, 0, 0} {}//列表初始化,这里是数组的初始化方法,和普通对象不太一样
vec3(double e0, double e1, double e2) : e{ e0, e1, e2 } {}
  • const成员函数

任何对类成员的直接访问都被看做this的隐式调用,也就是说当vec3定义的对象使用内部的成员函数时,它都会隐式地使用this指向的成员。因为this的目的总是指向这个对象,所以this时一个常量指针,我们不允许改变this中保存的地址。默认情况下,this的类型是指向类类型非常量版本的常量指针。意味着我们不能把this绑定到一个常量对象上。这一情况使得我们不能在一个常量对象上调用普通的成员函数。C++语言的做法是允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数。

double x() const { return e[0]; }
double y() const { return e[1]; }
double z() const { return e[2]; }

2.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/(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(vec3 v) {return v / v.length();
}

​​​​​​​三、Surface Normals and Multiple Objects

1.An Abstraction for Hittable Objects

完整代码如下所示:

#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

其中hit_record的作用是记录光线碰撞点的信息,碰撞到的点坐标,法线方向以及碰撞时间

struct hit_record {point3 p;vec3 normal;double t;
};
  • 抽象基类与纯虚函数

和普通的虚函数不一样,一个纯虚函数无需定义。我们通过在函数体的位置(即在申明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数。其中,=0只能出现在类内部的虚函数声明语句处。

注意,含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类的作用是负责定义接口,而后续的派生类可以覆盖此接口。我们无法直接创建一个抽象基类的对象,如果要定义一个继承了抽象基类的派生类对象,前提是派生类对纯虚函数进行了覆盖。

class hittable {public:virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};
  • 派生类对纯虚函数的覆盖

根据hittable定义一系列碰撞物体,在这里只定义了球体,具体代码如下所示:

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;
};

可见sphere类中的hit函数对hittable类中的hit函数进行了覆盖,在这里注意要在纯虚函数后面加上override关键字进行覆盖。hit函数的定义过程是在sphere类外操作的,如果在类外定义需要加上类的作用域符。类外定义的代码片段如下所示:

bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {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 false;auto sqrtd = sqrt(discriminant);// Find the nearest root that lies in the acceptable range.auto root = (-half_b - sqrtd) / a;if (root < t_min || t_max < root) {root = (-half_b + sqrtd) / a;if (root < t_min || t_max < root)return false;}rec.t = root;rec.p = r.at(rec.t);rec.normal = (rec.p - center) / radius;return true;
}

注意下面这个代码片段:

// Find the nearest root that lies in the acceptable range.
auto root = (-half_b - sqrtd) / a;
if (root < t_min || t_max < root) {root = (-half_b + sqrtd) / a;if (root < t_min || t_max < root)return false;

root代表的是碰撞的时间,auto root = (-b - sqrt(discriminant)) / (2.0 * a);是最小的碰撞时间,如果没有落在指定的时间范围内,则观察最大的碰撞时间,在这里t_min通常是光线出发的起始时间(如t_min= 0),如果最小碰撞时间小于t_min,则说明光线的起点在物体内部。t_max是光线打到最远的地方的时间(如t_max= infinite),如果t_max小于root说明整个物体都在光线起点的后面,是无法被碰撞到的。t_max在后续的代码中有寻找最近碰撞物体的作用,所以t_max的值也会改变,具体见后续的说明。

2.A List of Hittable Objects

下面这段代码主要涉及到了C++的智能指针

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 t_min, double t_max, hit_record& rec) const override;public:std::vector<shared_ptr<hittable>> objects;
};

静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态内存或栈内存的对象由编译器自动创建和销毁,对于栈对象,仅在其定义的函数块运行时才存在;static对象在使用之前分配,在程序结束时销毁。除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作堆。程序用堆来存储动态分配的对象,动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

为了更安全的使用动态内存,C++提供了shared_ptr智能指针,shared_ptr允许多个指针指向同一个对象。shared_ptr的详细使用见这篇文章

四、Diffuse Materials

1.Fixing Shadow Acne

shadow acne是“阴影粉刺”问题,参考一位知乎答主的文章,便是从球面反射的光给球体自身造成了阴影,具体的现象如下图所示:

从球面反射的光不应该给球面自身造成阴影,所以我们不需要精确 t = 0 球面(否则这个会计算出来跟球面相交),而是要‘漂移’一点,所以在代码里:

if (world.hit(r, 0.001, infinity, rec)) {

这样我们的反射光是从 0.001 处反射出去。避免了‘粉刺’问题。

五、Metal

1.An Abstract Class for Materials

为什么定义在不同头文件中的类和结构体需要使用前向声明,如下所示:

#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
#include "rtweekend.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;}
};

第一段代码中class material前面需要声明struct hit_record,第二段代码中hit_record前面需要声明class material。这两段语句主要是解决循环依赖问题,material需要依赖hit_record,同时hit_record也需要依赖material,如果在两个代码中分别加上彼此所属的头文件则会出现头文件的循环包含,但是由于预定义的存在(前面已经讲过)头文件循环包含并不会造成任何问题,但是类与结构体的循环依赖问题依然存在,所以需要用到前向声明。更详细的介绍见文章1和文章2.。

六、Dielectrics

1.Snell's Law

在这一小节中原作者直接跳过了折射向量计算的推导,但是这个还蛮重要的,在这里我将给出自己的推导。

2.Total Internal Reflection

折射并不是在任意角度都存在,当角度过大时根据Snell‘s Law其实得不到折射光线方向,原因如下所示:

七、Defocus Blur

1.A Thin Lens Approximation

具体内容来源于这篇文章,首先,我们来了解一下散焦模糊,我们在真实相机中散焦模糊的原因是因为它们需要一个大圈(而不仅仅是一个针孔)来聚光。这会使所有东西都散焦,但是如果用小孔的话,那么通过前后调整相机镜头,就会使得一切景色都会聚焦到相机镜头中,也就是会汇聚到那个孔内。物体聚焦的那个平面的距离由镜头和胶片/传感器之间的距离控制。这就是为什么当你改变焦点时可以看到镜头相对于相机移动的原因。

光圈是一个可以有效控制镜头大小的孔。对于真正的相机,如果你需要更多光线,你可以使光圈更大,同时也会获得更多的散焦模糊。对于我们的虚拟相机,我们也需要一个光圈

真正的相机具有复杂的复合镜头。对于我们的代码,我们可以模拟顺序:传感器,然后是镜头,然后是光圈,并找出发送光线的位置并在计算后翻转图像(图像在胶片上倒置投影)。人们通常使用薄透镜模拟近似。

引用书上一张图(相机聚焦成像)

我们不需要这么复杂,我们通常从镜头表面开始射线,并将它们发送到虚拟胶片平面,方法是找到胶片在焦点平面上的投影(在距离focus_dist处)。

前面说了一大堆,看着比较复杂,其实并没有那么难

前言说了三件事情:

第一点,生活中的相机成像分为两个部分,inside和outside,涉及3个物:film(胶片)、lens(镜片)、focusPlane(焦点平面),而我们只需要outside部分

其次第二点,我们的眼睛(或者相机)不再是一个点而是眼睛所在的周围圆盘上的随机点,因为实际的相机是有摄像镜头的,摄像镜头是一个大光圈(很大一个镜片),并不是针孔类的东东,所以,我们要模拟镜头,就要随机采针孔周围的光圈点。

所以代码实际上就是在通过相机原点并处于xy平面的一个区域随机采样来模拟镜头的光圈,通过成像平面与镜头的距离来模拟实际相机的焦距。


总结

最终的渲染时间非常慢,大概书中给出的图片需要渲染一天半才可以出结果。

Ray Tracing in One Weekend详细总结相关推荐

  1. 总结《Ray Tracing from the Ground Up》

    之前已经学习过<Ray Tracing in One Weekend>和<An Introduction to Ray Tracing>的一些内容,相关总结文档链接如下: 总结 ...

  2. 总结《An Introduction to Ray Tracing》

    在学习完<Ray Tracing in One Weekend>之后,对Ray Tracing的概念及其涉及的主要方面有了大概的了解.同时,在熟悉了<Ray Tracing in O ...

  3. 计算机图形学 读书笔记(八) 光线跟踪加速Ray Tracing Acceleration

    写个读书笔记,一来作为字典以后可以查,二来记录自己的理解. 并没有对每个知识点的详细解释,大部分只有主观的定性的解释. 光线跟踪受到的限制: 1.时间复杂度和空间复杂度都很高. 2.主要时间用在了可见 ...

  4. Ray Tracing in One Weekend从零实现一个简单的光线追踪渲染器

    Ray Tracing in One Weekend学习笔记 1.Overview 从零开始实现一个简单的光线追踪渲染器,能够实现漫反射材质.金属材质.透明材质的渲染,此外还实现了摄像机的自由移动和焦 ...

  5. 计算机图形学学习笔记——Whitted-Style Ray Tracing(GAMES101作业5讲解)

    计算机图形学学习笔记--Whitted-Style Ray Tracing GAMES101作业5讲解 遍历所有的像素生成光线 光线与平面求交 遍历所有的像素生成光线 关于作业五中如何遍历所有的像素, ...

  6. 计算机图形学六:光线追踪-Ray Tracing

    文章目录 阴影映射(Shadow Mapping) Whitted-Style 光线追踪 原理 光线与物体求交 光线的表示方法 光线与隐式曲面求交 光线与显示曲面求交 如何加速 轴对齐包围盒(Axis ...

  7. 光线追踪(ray tracing)介绍与细节推导

    背景 最近因为找到关于光线追踪相关不错的教程,所以边学习边做记录并希望将相关资料进行分享. 光线追踪作为计算机图形学中一种可以获得良好的效果的渲染算法,有着非常广泛的应用.历史背景相关的介绍可参考百度 ...

  8. Ray Tracing,Ray Casting,Path Tracing,Ray Marching 的区别?

    作者:洛城 链接:https://www.zhihu.com/question/29863225/answer/70728387 来源:知乎 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请 ...

  9. 《Ray Tracing in One Weekend》、《Ray Tracing from the Ground Up》读后感以及光线追踪学习推荐...

    <Ray Tracing in One Weekend> 优点: 相对简单易懂 渲染效果相当好 代码简短,只看书上的代码就可以写出完整的程序,而且Github上的代码是将基类与之类写在一起 ...

最新文章

  1. Top 15 不起眼却有大作用的 .NET功能集
  2. rhino-java中调用javascript
  3. amf java_java – 不支持的AMF版本
  4. 【图灵有聊】说好的安全呢?
  5. 实现Windows non-Unicode设置批量修改
  6. c语言试题 改错题,精选二级C++试题 – 改错题
  7. java跟python对比_【多年的Java程序员总结Java与Python的对比 】
  8. 代码质量第 5 层 - 只是实现了功能
  9. Windows10最新MySQL8.0.23安装教程(超级详细)
  10. 技术专题:厦门9月30日限制路由(网络尖冰),WAYOS或ROS解决方案
  11. 玩转jquery插件之flexigrid 【转】
  12. 昆仑通态复制的程序可以用吗_昆仑通态触摸屏如何做时间记录
  13. c语言转换为python语言_使用C语言中的数据缓冲区和NumPy数组之间的转换来为Python接口打包C程序的最佳方法是什么?...
  14. 设计模式之GOF23组合模式
  15. 尼得科与日本电产三协共同研发出一款搭载有“Zignear®”的AC伺服电机
  16. 802.11ax分析1---IEEE 802.11ax和IEEE 802.11ac性能对比
  17. 高精度计算Π的值(C语言)
  18. 德州仪器TI芯片实时监控自动抢购
  19. GUI小工具-网盘搜索器
  20. CSS中font-family属性值中文和英文的问题

热门文章

  1. .tar.gz与.tar.bz2解压
  2. 电商代购系统;海外代购系统;代购程序,代购系统源码PHP前端源码
  3. 大学计算机模拟考试常见试题与解析
  4. android按钮设置下划线,Android开发如何给textView设置下划线或中划线
  5. 董宇辉的解答一位父亲请教如何让子女从心里喜欢上英语
  6. Dubbo的原理和机制(详解)
  7. c++ opencv数字图像处理:频率域滤波--低通滤波--理想低通滤波
  8. RERAN:安卓系统的定时和点击的录制和回放——(4)
  9. 360随身wifi原理,功能特点,使用说明
  10. 【opencv学习笔记】018之Sobel算子与Scharr算子