《Raytracing In One Weekend》学习笔记01 Chapter 1、2、3、4、5、6、7、8

根据师兄推荐,打算从 Peter Shirley 的《Ray Tracing in OneWeekend》等系列图书入门光线追踪,学习过程中记录了一些经验总结笔记。这些笔记中包含了学习过程中遇到的一些知识理解以及编程相关的问题,如今记录下来,总结经验,加深印象。

Chapter 1. Overview

第一章介绍了作者将会使用c++进行编码,可以使用轻量级的IDE,甚至包括Codeblocks,这里我使用了Clion(通过教育网邮箱登录可以免费使用),比较轻量,而且代码管理也很方便。安装好Clion后记得配置一下C++环境。
作者喜欢在敲代码的过程中学习这些知识,我也很赞同。但是当一些代码提供可用时,虽然不好理解,推荐大家可以毫不留情的测试使用,这也是学习的一个过程。

Chapter 2. Output an Image

这一章主要教大家如何从无到有输入一幅rgb图像。图像采用ppm格式(wiki百科上有具体介绍,很详细,推荐看一下) 存储,构造过程和输出过程都十分简单。
比较有用的一点是,这里推荐了另外一种输出图像的方式(任意格式的图像),那就是使用stb_image_write第三方库,方法十分简单,所用代码如下:

  • 将stb_image_write.h头文件源码文件放到当前目录下,然后声明头文件并编码输入:
#include "vec3.h"   // 双引号应用的是程序目录的相对路径中的头文件
#include <iostream> // 尖括号引用的是编译器类库路径中的头文件
#define STB_IMAGE_WRITE_IMPLEMENTATION // 使第三方库stb_image_write成为可执行的源码
#include "stb_image_write.h"

using namespace std;

int main() {
    int nx = 200;
    int ny = 100;
    int channels = 3; // 代表rgb三通道,若为4则代表rgba四通道
    unsigned char *data = new unsigned char[nx*ny*channels]; // 声明数组,用于存放像素rgb值
    for(int j = ny -1; j >= 0; j--){
        for(int i = 0; i < nx; i++){
            vec3 col(float(i) / float(nx), float(j) / float(ny), 0.2);
            int ir = int(255.99*col[0]);
            int ig = int(255.99*col[1]);
            int ib = int(255.99*col[2]);
            data[(ny - j - 1)*nx*3 + 3 * i] = ir; // 计算出二维图像中的像素在一维数组中的对应位置,从第一行第一列开始
            data[(ny - j - 1)*nx*3 + 3 * i + 1] = ig;
            data[(ny - j - 1)*nx*3 + 3 * i + 2] = ib;
        }
    }
    stbi_write_png("PNGOutput2.png", nx, ny, channels, data, 0); // 输出图像
    return 0;
}

Chapter 3. The vec3 Class

这部分创建了一个vec3向量类,用于创建、表示以及操作三维向量。

  • 新增了一个有意思的点是inline内联函数,这部分内容c++课本上多有描述,可以解决一些频繁调用的函数大量消耗栈空间(栈内存)的问题。
  • 此外还重载了c++中的运算符,用于向量的基本数学运算。如果你感觉心里没底最好的方式是找课本上的运算符重载实例练习一下,至少有个了解。
    难度不大,不用惊慌。
  • 关于c++编程有两点需要注意:
  • 一是定义类时,创建了.h文件还需要再创建.cpp文件吗?其实不创建.cpp文件完全不影响编译,创建.cpp文件的主要目的是实现成员函数声明和定义的分离,方便代码管理(个人理解),当前项目相对来说是很轻量的,只需要创建一个.h文件就足够了(虽然博主也创建了.cpp文件,但是推荐只创建.h文件即可)。
  • 二是可以通过条件编译实现再同一个项目中管理不同章节的代码,节省创建新项目的时间。这部分的内容有时间的话博主会另写一篇总结文章。

Chapter 4. Rays, a Simple Camera, and Background

这部分通过描述了射线的定义并创建了射线ray类,定义一个摄像机,以及创建一个颜色函数color来初步构建了一个光线追踪器,具有发射光线,计算光线与像素碰撞点的颜色的功能(demo中显示出了背景的颜色)。个人认为需要注意的有以下几点:

  • 射线的数学定义函数需要理解(博主认为大家应该比较容易理解,需要注意的是初始时只需要起点和方向就能定义一条射线;后续章节中由于涉及到射线与模型相交,所以又引入了参数t,用于表示射线相交后有效部分的长度);
  • 摄像机的位置很有意义(科普向),始终位于(0, 0, 0)原点处,向上与y轴正方向重合,向前朝向z轴的负方向(也就是看向z轴的负方向)。这里可以给大家普及一下,计算机图形学中一般是认为摄像机固定于上述的标准位置,那么可能有同学问了,现实世界中摄像机是会动的啊。这里需要解释一下,此处用到了物理的相对运动知识,也就是说,我们可以把摄像机的运动转换成模型的运动,对模型进行视图变换(也叫摄像机变换)从而得到运动后的模型状态信息,然后再进行渲染操作。变换过程中的数学知识很有趣,有兴趣的同学(你可能没时间看,但是我强烈推荐你在空闲看!),可以观看闫令琪GAMES101的第四课,视图变换部分内容(你很有可能会把前面的部分也看完😄)。
    偷了一张图
  • 射线是由摄像机位置发出的,模拟了由人眼发出的视线,有多少条呢?目前我们是在for循环中规定了一个像素点对应一条,也就是有200*100条(可以想象一下,如果博主没推错的话就是对了😄);
  • 射线与某物体(本章节只涉及背景)相交后,需要通过color函数计算一下颜色值,这就是该位置我们看到的颜色;
  • 颜色是通过射线的方向向量单位化后的y分量映射后并进行插值得到的。解释一下,就是射线有一个方向向量,作者将该向量单位化,所以三个分量的取值范围都是(-1, 1);然后将对应的y分量从(-1, 1)映射到(0, 1)上(为啥是开区间?博主暂时没有考虑;映射的计算过程以y分量为例,为:0.5*(y + 1.0)),为啥插值到(0, 1)区间上?因为插值参数的范围需要是(0, 1),这个y分量映射之后就是插值参数t;最后进行插值操作:
// 返回插值后的颜色,从浅蓝色到白色之间进行插值;从上到下渐变的颜色同时对应了y从大到小(for循环规定的)的位置特性
return (1.0 - t)*vec3(1.0, 1.0, 1.0) + t*vec3(0.5, 0.7, 1.0);  
// 特别注意,这个t和第五章中的t不是一回事;这里的t只代表插值参数,第五章中的t代表射线函数的中的一个变量(注意一下即可)

盗图王者

生成的背景图

Chapter 5. Adding a Sphere

这一部分主要是在场景中定义了一个球体,然后判断射线与球体是否相交,如果相交了,将球体对应的颜色设置为红色。你可能对以下几个问题感到疑惑:

  • 球体是怎么定义的?事实上,这里的球体只是在数学计算(射线与球体碰撞)中有所涉及,只是虚构了一个球体,仅有球心和半径的信息,目前并未创建球体实体(下一章将会创建球体类,充分利用c++面向对象的特点);
  • 射线与球体是怎么碰撞的? 实际上是通过数学计算进行判断的。因为我们知道了球心和半径,所以可以写出球面座标公式:
    友善的借用
    Cx/y/z代表了球心座标,R为球半径,都是已知的。
    实际上我们并不想用这个方程,我们想用的是球面座标的向量表示方式,即:
    友善的借用
    其中p为球面上的任意一个点(x, y, z),这个方程的数学含义是,球面上任一点与球心之差构成一个向量,这个向量和自己的点乘结果,是等于方程右侧式子的(单纯计算推出来的,dot表示点乘);而右侧的式子恰恰就是球面座标公式的一部分,它等于R的平方,也就是说,我们得到了球面上任意一点的向量表示方式。
    现在我们想计算球面有没有和射线发生碰撞,考虑到射线的参数表示为:p(t)=A+t∗B,p(t)表示射线上的任意一个点(x, y, z),A为射线起点座标,B为射线的方向向量,t为参数变量,可以认为A、B是已知的,仅t未知。
    所以要想计算球面和射线有没有相交,直接将射线带入球面方程即可,如果你思路很清晰的话,会发现代入后仅剩下一个变量,那就是t,带入后得到了关于t的一元二次方程。博主通过向量的结合率将射线方程代入球面方程后推导了一下,最终得出了下面的方程:
    这个方程不错
    这就是射线与球面的相交方程,仅含有t这一个未知数,所以可以通过计算相应的一元二次方程的判别式来判断有没有解,若有解,有几个解(1个or2个)。在这一章里,我们只是判断了有没有解的情况,若有解,代表有交点,然后我们将对应位置处的像素赋值为红色(问:怎么找到这个像素的呢?其实阅读源码不难发现这里有这么一个性质,那就是图像像素和射线存在一一对应的关系,也就是说,根据像素的u、v座标确定了一条射线,这条射线和模型如果相交,那么这个像素就要赋值红色;若未相交,赋值为计算得到的背景色;所以要赋值的这个像素根本就不需要找,它就是我们最初的图像上某个具体位置的像素啊);若无解,表示射线与模型没有交点,射线投向了背景,我们需要将对应位置处的像素赋值为插值计算出的背景色。
  • 其实这一章还有个重大缺陷,那就是判断出来的有解的情况,可能对应着球在射线的负方向上,即t<0的情况,很显然摄像机后面的球是看不见的,下一章对这个缺点进行了改进,排除了t<0的情况。

Chapter 6. Surface Normals and Multiple Objects

这一部分分两个片段分别介绍了一些内容。
第一个片段介绍了球面法线的定义,以及如何可视化法线(通过颜色标识)。

  • 球面法线定义为(射线与球面碰撞点 - 球心点),作者个人倾向于对其再进行单位化操作,因为这会对后续的着色提供方便。
  • 可视化法线:球面上分布了无数的法线,每一条法线的x/y/z分量都不一定相同(由于我们获取的法线是进行单位化的,所以每一分量的取值范围为(-1, 1)),所以可以利用这个特征,将每一条法线的x/y/z分量映射到(0, 1)(r/g/b)上 (映射方法很简单,可以参看第六章color函数的源码),对应着像素颜色r/g/b的取值范围,从而表示像素点的颜色,实现法线的可视化。

第二个片段实现了在场景中通过链表的形式创建多个物体,用到了c++面向对象的知识。

  • 这里构造了一个抽象类hittable,可以认为是一类被射线碰到的物体(这里作者认为会与面向对象中的对象混淆,所以换了一种命名方式,最后作者根据这个类中的虚函数hit(用来计算判断是否发生碰撞)命名了这个抽象类)。
  • 这里还构造了sphere类,继承自抽象类hittable,并实现了父类中的虚函数hit的定义(你最好了解一下抽象类虚函数的基础知识,不用太深入)。这样的好处就是把不同的碰撞细节定义在了不同的物体类中,如果再添加一个其它类别的物体,比如说立方体(实际这本书并未涉及),我们可以把对应的判断碰撞方法写到该类的hit函数里,充分面向对象。

当前章节中还用到了多态的思想。指的是,声明hittable类型的链表,然后为该链表赋值为sphere类型的内容。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作(摘抄自百度百科)。

关于最后的结果图:在最后的结果图中,你可能会问,**为啥大球是一片均匀的绿色的呢?**我的理解是大球太大了,导致我们看到的球面法线变化值不大,所以映射到颜色后,显示一片均匀的绿色。至于为啥是绿色,那是因为x/y/z对应的r/g/b值中,y分量比较大,所以g值大,显示绿色(个人理解)。

Chapter 7. Antialiasing

这部分讲的是如何实现抗锯齿。

  • 主要原理很简单,那就是通过计算周围像素的颜色平均值来得到当前像素的颜色,比如说在边界处具有很明显的前景色和后景色,要想获得平滑的边界,可以把前景色和背景色混合得到一个边界色,作为过渡,由于像素很小,所以不放大看的话,像素的锯齿感就消失了。作者在这里并没有考虑多层颜色的影响,给出的理由是对画面效果的提升不大。
  • 实际编程过程中作者随机采样(这也是为啥用到随机函数的原因)了200个周围的像素点,最后取了一下平均值得到当前像素的颜色值。这就相当于在计算每一个像素点的颜色值时,在其周围随机发射了200条射线,并计算交点颜色,最后取平均。这就与先前章节中介绍的一个像素对应一条射线不一样了,读者需要注意。

Chapter 8. Diffuse Materials

Diffuse Materials 漫反射材质 让模型的视觉效果更逼真。
这部分开头作者做了一下声明,那就是作者将几何形状和材质分开了,而非一一对应,这会导致一些局限性。此外,漫反射材质本身不发光,只会反射周围的环境光,同时会将环境光混合调制成本身的色彩光线在漫反射表面上的反射方向是随机的,如下图:
漫反射材质的随机反射

漫反射材质的随机反射

当然有些光线也可能会被吸收,表面越黑,吸收光线的能力越强。这也是黑色物体为啥黑的原因——因为它吸收光线,极少反射光线。

废话不多说,我们看看代码部分有哪些需要注意的。

  • 如何模拟射线的随机反射?
  • 先搞清楚为啥要模拟随机反射:因为根据漫反射材质随机反射光线的性质,光线照射到漫反射材质表面上时,会被表面吸收(当前章节不考虑)或随机反射到一个方向上,而且有可能会反射后再次反射(当然强度会衰减),如果这样考虑的话,渲染效果将会更加逼真,这是由真实世界中的物理规律决定的。
  • 具体怎么做才能获取随机反射射线呢?作者在实现过程的一个步骤中采用了一个取巧的办法,叫舍弃法(理解起来很简单,博主就不多说了,但是博主认为这个方法有很大缺陷)。整体步骤描述如下:首先我们已经计算得到了射线与物体(这里主要是sphere)之间的交点,并且知道交点处物体表面的法线(是一个单位向量),所以如果我们在原点(0, 0, 0)处获取一个随机点(这个随机点位于以原点为球心的单位球体内,实际上demo程序中只计算了x/y/z各分量都为正的情况),那么,将该随机点(其实是以原点为起点的向量)与已知的交点座标和法线向量相加,就可以得到一个新的随机点,这个点位于一个球体中,这个球体的半径为1,球心座标为射线与sphere交点加上法线向量。也就是说,我们最终获得了位於单位半径球体中的一个随机点,这个随机点与射线和sphere交点之差,便是新的反射射线的方向,而交点就是反射射线的起点。至此,一条随机反射射线便构造出来了。
    绝对原创图片
构造随机反射射线
  • 上面提到过,随机反射射线如果和物体碰撞还可以再次进行反射,但是不会无线反射下去,因为射线每反射一次,其强度便会衰弱一次(这里我们用衰弱系数描述),所以最后要么最后碰撞到背景,要么因为强度太弱无法继续反射。体现在demo程序中便是一个递归函数,上述两种中止情况便是递归的返回条件。
  • 最后还需要注意两个问题:
  • 颜色问题,生成的图片偏黑,矫正一下,理想的效果应该是浅灰色,为此我们可以对像素颜色分量分别进行开方处理,变相的增强了颜色的亮度(因为颜色分量是小数),前后对比效果图如下👇
  • 在这里插入图片描述在这里插入图片描述
颜色矫正
  • 编程问题,是浮点类型的t造成的,因为二进制计算机在判断相等时,是用范围来判断的。举个例子,对于float数据类型的实数,-0.0000001和0.0000001都被计算机认为是0,因此我们应该舍去负值(对应射线与球体交点在视线背面的情况),这样的操作去除了“阴影粉刺”现象(的确是这么称呼的。。。),效果图如下👇
    在这里插入图片描述
编程误差矫正
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章