RayTracingInOneWeekend是一个挺不错的实现光追效果的文章,可以通过C++代码实现一个简单的光追渲染,这里记录了一些我在编写的时候遇到的一些知识点,比较个人,建议自己实操后再来查看此文会有更深的体会。

C++输出PPM格式图片:

文章中实现渲染效果是通过C++语言,其中实现的方式是通过C++语言自定义各种类(比如向量,颜色==),通过光线追踪的基本理念,即追踪光线,代码实现逻辑,通过cout语句输出以及cerr语句追踪进度,用CMD对编译后的程序进行操作生成PPM格式的图片,再用PS(个人是通过该软件的,文章中并不是)查看效果,其实按照实现流程来说,使用其他语言也完全可以。
控制台输出PPM图片

关于PPM格式

PPM可以理解为把每一个像素信息都存储的图片文件格式,格式简单,没有压缩,在百度和维基百科中查找时是找不到的,需要通过查找PBM格式才能找到,更加详细的信息可以自行百度,这里会把主要使用的PPM格式的书写规范放出:

P3           #"P3"意味是通过ASCLL格式的RGB颜色
3 2          #"3"和"2"分别代表的是图片的宽度和高度,单位像素
255          #"255"是每个颜色的最大值
#上面是颜色格式的规定
#下面是颜色详细定义,当然只需要一个空格就可以分开,我这里为了好看
255   0   0  # red
  0 255   0  # green
  0   0 255  # blue
255 255   0  # yellow
255 255 255  # white
  0   0   0  # black

上述格式生成的PPM格式则是:

PPM格式的图片颜色会从左到右,从上到下,十分的清楚明了,很适合学习
PPM格式图片示例

关于C++特殊语法--shared_ptr

由于我对于C++比较高级的语法并不是很了解,所以这个也列进来了
C++的动态内存的管理是通过new()和delete()实现的,但是这种方式并不利于我们要多次引用相同材质或者贴图的情况,所以文中使用了智能指针来管理动态对象,其中shared_ptr如其名,允许多个指针指向同一个对象,使用它会更安全,更方便

float精度出现的问题

文章中出现了黑斑的问题,如下图:
黑斑问题
首先讲述一下文章中的逻辑,文中的摄像机射出的射线与碰撞体会发生碰撞,并得到点的深度信息,文中该碰撞函数还会舍弃一定范围深度的点,而这个范围一开始的最小值为0,最大值为无穷,当成功检测到碰撞点时,则替换最大值范围,这样就巧妙地进行了深度覆盖检测,下方为实现的部分代码:

bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const
{
    hit_record temp_rec;
    bool hit_anything = false;
    auto closest_so_far = t_max;
    //对世界中所有物体与光线进行碰撞检测
    for (const auto& object : objects) {
        //如检测成功,则替换最大深度值
        if (object->hit(r, t_min, closest_so_far, temp_rec)) {
            hit_anything = true;
            closest_so_far = temp_rec.t;
            rec = temp_rec;
        }
    }
    return hit_anything;
}

下图从二维图片的示例来更加清晰地讲解,原点作为摄像机的坐标,往右上角的黑色射线即摄像机射出的射线,假设射线先与A圆的1,2点相交,再与B圆3,4点相交,很明显得出1点的深度值并替换上述函数中的closest_so_far数值,此值小于3,4点的深度值,所以是无法检测成功,则自然地舍去了B圆的两点。
代码逻辑图解
而这里的近切面就引入了精度问题,单精度数值是七位小数,双精度数值是十五位小数,当超过精度后,如0.省略n个零1(n>16),则直接会当做为0来处理,所以这样就会导致在有一些点被忽略从而造成画面一定错误,所以可以把近切面的阈值改为0.001就可以了
黑斑问题解决后效果

关于光线追踪的理念

从字面意义上就是通过追踪光线,通过光线的一些反馈获取颜色,而其中追踪的光线实际上是根据摄像机向视口依次放出射线,获取每一个像素的颜色信息,再渲染出来,得出这种方法可行的原因是光线追踪算法依赖的几个假设:
1)光线沿直线传播
2)光线之间不发生碰撞
3)光路可逆
通过这几个假设就可以推断出上面的方法是可行的,虽然在现实中的光线并不是假设的样子。
其中文章中的实现方法有些取巧特殊,以下是部分代码,我将进行一定的解释。

color ray_color(const ray& r, const hittable& world, int depth) {
    hit_record rec;
    // 通过设置深度值来判断是否退出递归
    if (depth <= 0)
        return color(0, 0, 0);
    //判断是否与场景的物体发生碰撞,获取碰撞信息
    if (world.hit(r, 0.001, infinity, rec)) {
        ray scattered;
        color attenuation;
        //根据碰撞信息的材质的函数来判断是否有其他光线射出
        if (rec.mat_ptr->scatter(r, rec, attenuation, scattered))
            //这里的颜色的乘法专业术语叫颜色的非等比缩小
            return attenuation * ray_color(scattered, world, depth - 1);
        return color(0, 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);
}

可以看到文章中这段代码是通过光线与场景中的物体进行碰撞检测,如果碰撞则再根据碰撞体的材质来判断是否发射新的光线(包括反射射线以及折射射线),通过递归来实现对光线的反射以及折射的追踪,为了避免无限递归则通过设置深度值来撤出递归,最终会获得一个颜色值。这里面的最后一段代码算是简化了整个光线追踪的流程,就是将没有与场景中物体碰撞的光线直接通过一个简易的天空盒赋值,就避免了设计光源,也就是取巧的地方。

关于材质

文章中实现了三个材质:漫反射材质金属电介质(玻璃之类的),这里就涵盖了漫反射反射折射的实现方法。

  • 漫反射的实现是比较依赖文章中抗锯齿的实现方式,文章中抗锯齿的实现方式是通过对一个像素进行多次随机采样(给予视口窗口某点一定的偏移值,再与摄像机连线),获得的多个颜色值求和取平均,而漫反射材质实现则是往随机一个方向射出自身的颜色,就契合了之前多次采样射出的光线。
  • 金属材质中实现反射模糊是通过设置一个fuzz变量(double类型)来决定模糊程度,该变量乘以一个随机方向的单位向量与反射向量末位置相加实现偏移,也就实现了反射模糊的效果,fuzz越大,模糊程度越大。
    原理图解:

原理
金属球效果,右边的模糊度比左边大:
金属球材质效果

  • 电介质材质实现不仅需要考虑折射的现象,还要考虑全反射的现象,这里判断两个现象中采取哪个是通过角度的计算以及Christophe Schlick的多项式近似,这个多项式解决的是在不同的角度观察电介质材质,会有不同的反射率,举个例子,从比较陡的角度观察玻璃,玻璃就变成了镜子。
    下面第一张图就是只有折射效果的电介质材质,第二张是通过多项式近似来模拟折射和全反射的电介质材质:

只有折射效果的电介质材质
通过多项式近似实现的电介质材质
可以看出采用了这个多项式近似后的质感更加真实,但Christophe Schlick的多项式近似只是近似地模拟了折射率的变化。