Games101第五次作业(Whitted光线追踪&代码框架分析)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、Whitted Ray-Tracing Whitted光线追踪介绍

1.1概念引入

Whitted-style光线追踪是由特纳惠特首次提出的,利用折射定律菲涅尔方程来计算光线与反射或透明物体表面相交时的反射和折射光线的方向,遵循光线路径来获得相交对象的颜色。

1.2为什么需要光线追踪

首先引入局部光照和全局光照的概念:
局部光照 (local illumination):
通常,点光源和平行光源这种理想光源发出光线到物体表面产生的光照我们叫做直接光照;而只考虑直接光照的光照效果我们叫做局部光照。
全局光照 (global illumination):
在真实世界中,我们不仅要考虑理想光源带来的光照效果,还有区域光的存在,光线在玻璃、镜子等物体表面之间还会发生弹射,这种现象也是组成光照的重要部分,我们称为间接光照。而在局部光照基础上考虑到间接光照的光照效果我们称为全局光照。

因为传统的Phone模型是局部的,无法考虑到全局的光照效果,为了提高渲染质量,所以有了光线追踪的出现。

1.3Whitted光线追踪的实现要点

1.3.1注意的实现细节

1.如果相交点处的表面不透明且具有漫反射特性,我们要做的就是使用光照模型(例如Phong模型)来计算相交点处对象的颜色。 此过程还涉及朝场景中的每个光源方向投射光线,以查找该点是否在阴影中。 这些射线称为阴影射线。
2:如果表面是类似镜子的表面,我们只需在相交点处跟踪另一条反射射线即可。
3:如果曲面是透明曲面,则在交点处投射一条反射射线和一条折射射线。

1.3.2实现步骤概括

在这里插入图片描述
1.创造主射线primary ray:从摄像机的每个像素发射一条主射线,将主射线通过像素平面;
2.投射射线交互:根据发射的主射线和场景对象进行交互;
3.获得相交对象的属性:如果主射线与物体表面有交点,则需要得到交点表面属性(法线、材质特性等);
4.递归光线跟踪:根据物体表面特性,判断是否要发射二次射线secondary ray(包括折射射线and反射射线),需要递归发射的两条新射线,进行跟踪计算。

二、框架流程分析

2.1main函数

int main()
{
    
    
    //屏幕分辨率设置and场景对象创建
    Scene scene(1280, 960);
    //智能指针指向一个球类
    auto sph1 = std::make_unique<Sphere>(Vector3f(-1, 0, -12), 2);
    //球类有个父类叫物体,里面有materialType材质类,材质类只有三种值:漫反射和自身发光、折射和反射
    //材质的定义
    sph1->materialType = DIFFUSE_AND_GLOSSY;
    //漫反射定义
    sph1->diffuseColor = Vector3f(0.6, 0.7, 0.8);

    //智能指针指向另一个球类
    auto sph2 = std::make_unique<Sphere>(Vector3f(0.5, -0.5, -8), 1.5);
    sph2->ior = 1.5;//折射率
    //材质的定义
    sph2->materialType = REFLECTION_AND_REFRACTION;
    //加载物体到场景
    scene.Add(std::move(sph1));
    scene.Add(std::move(sph2));

    //uint32_t类型是无符号32位
    Vector3f verts[4] = {
    
    {
    
    -5,-3,-6}, {
    
    5,-3,-6}, {
    
    5,-3,-16}, {
    
    -5,-3,-16}};
    uint32_t vertIndex[6] = {
    
    0, 1, 3, 1, 2, 3};
    Vector2f st[4] = {
    
    {
    
    0, 0}, {
    
    1, 0}, {
    
    1, 1}, {
    
    0, 1}};

    //MeshTriangle类型网格三角形
    auto mesh = std::make_unique<MeshTriangle>(verts, vertIndex, 2, st);
    //材质 漫反射和自身发光
    mesh->materialType = DIFFUSE_AND_GLOSSY;

    //加入对象到场景
    scene.Add(std::move(mesh));
    //加入光到场景
    scene.Add(std::make_unique<Light>(Vector3f(-20, 70, 20), 0.5));
    scene.Add(std::make_unique<Light>(Vector3f(30, 50, -12), 0.5));    

    //定义一个渲染对象
    Renderer r;
    r.Render(scene);//进行场景的渲染

    return 0;
}

主函数最后,就开始进行Whitted光线追踪。

2.2Whitted光线追踪的具体步骤分析

2.2.1创造主射线primary ray

2.2.1.1创建摄像机

把摄像机的中心定义在(0,0,0)处,即是定义人眼的位置,XYZ轴方向如下图。
这里是将摄像机与世界空间的坐标原点重合。
在这里插入图片描述

2.2.1.2计算主射线primary ray的方向

对于每条要计算的光线,其方向都是从摄像机(0,0,0)出发,通过某个像素中心(x,y,z)指向世界空间(两点确定一条直线),射线获得的最终的信息会记录在射线穿过的像素,对像素进行着色就可以得到一张渲染好的图片。具体的过程可参照下图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这其实是相当于是光栅化的逆过程!为什么呢?
首先回顾一下光栅化的过程:
1.模型空间->世界空间;
2.世界空间->摄像机空间;
3.摄像机空间->裁剪空间;
4.裁剪空间->屏幕空间;
5.光栅化。
1、2、3是MV变换,我们从4.裁剪空间->屏幕空间开始讲:
(1)首先通过投影和规范化缩放,我们得到一个xyz范围都是[-1,-1]的一个标准立方体,注意这里的1只是一个单位,不是像素的尺寸;
(2)为了将图像映射到屏幕空间,我们会将整个图像先处理到xyz范围为[0,1]内,做法如下:
先将xyz方向都加上1,得到[0,2]的范围,再除以2,就得到[0,1]的范围,此时是整个图像在屏幕空间[0,1]的范围。
(3)将整个图像拉伸成我们的屏幕尺寸,所以图像的尺寸要拉伸为:
在这里插入图片描述
就可以将图像变成和一张和我们屏幕一样大小的图了。
所以如果要进行光栅化的逆过程,那做法如下:

1.栅格空间->NDC规范化空间

(1).首先找到像素坐标(x,y),因为我们要找到像素中心,所以坐标记得变为(x+0.5f,y+0.5f);

扫描二维码关注公众号,回复: 14685763 查看本文章

(2).找到像素坐标后,因为之前光栅化过程中[0,1]时是一张图片的大小,所以我们现在逆过程,是要将一张图片从屏幕大小转变为[0,1]大小,对应的每个像素大小也要进行适当比例的缩小,所以像素中心也要进行适当比例的缩小,所以对像素中心坐标有:
[(x+0,5)/scene.width,(y+0.5)/scene.height],
将像素坐标变换到[0,1]范围之内,但是,栅格空间中,屏幕像素坐标,其实左上角才是坐标原点,所以y点的值在恢复到[0,1]就要进行变换(变换到[0,1]范围时的规范化NDC空间坐标原点就是左下角),如下图:
在这里插入图片描述
所以此时的坐标已经变成:
x=(x+0,5)/scene.width;
y=1-(y+0.5)/scene.height;

(3).此时变换后,我们的图是在[0,1]范围内,但由光栅化我们知道,我们是将图像从[-1,-1]映射到[0,1]的,所以逆过程的话我们要映射回去,所以正确对应像素中心坐标应该是:
x=2 * (x + 0.5f) / scene.width - 1)
y=2 * [1 - (y + 0.5f) / scene.height] - 1 = 1 - 2 * [(y + 0.5f) / scene.height]
具体就是将[]0,1]乘以2变到[0,2]空间,再同时减1变到[-1,1]空间,实现变化到正确投影后的[-1,1]立方体之内,
上面的式子就是原像素中心对应的在[-1,1]立方体内的坐标位置。

2.将压缩的x轴复原

接下来要注意的一个点,我们在前面得到的[-1,-1]是相当于规范化的范围,但是在光栅化的过程,是投影后还要缩放到[-1,-1],进行规范化的操作,所以逆光栅化还要将规范化[-1,1]变成宽高比例正确的比例大小,那要怎么做呢?其实很简单,先要考虑压缩x轴的复原。
做法如下:
此时得到的像其实是正方形,但是我们屏幕一般都不是正方形,所以代码中要定义一个宽高比,随时可以将变换的图像恢复到原来的宽高比例(不会导致像素压缩)

float imageAspectRatio = scene.width / (float)scene.height;//宽高比

一般都是宽比高要长一点,而正方形时宽高相同,所以x乘上宽高比则可以恢复比例:
x * imageAspectRatio
y值保持不变。

3.将位置转换到近平面上

Vector3f dir = Vector3f(x, y, -1); // Don't forget to normalize this direction!

这是将2维的图像位置映射到深度为-1的地方,因为之前只是定义的x、y值,所以这里对z值的定义可以将点的位置变成三维坐标。
一些思考:图像是从我们可以看到的最大屏幕尺寸变换而来的,通过我们的眼睛和投影变换就可以找出对应的在空间中对应的最大图像范围,所以说最终在空间的显示范围是由眼睛位置和图像大小决定的,但同时还要看视域角的影响,如下图:在这里插入图片描述
由视域角(deg2rad改为弧度制)得出:投影面一半高度/投影面深度tan(a/2),

float scale = std::tan(deg2rad(scene.fov * 0.5f));

规范化和伸缩恢复原来的图像宽高比例之后,高的范围不变,还是2(范围还是[-1,-1]),所以一半的高是1,由上图,按比例:
一半高度1*scale=此时图像深度,因为代码框架给的fov是90度,所以此时图像深度=-1,所以其实所在平面就是近平面,有了近平面其实就确定了光线的位置了(确定了原像素中心对应位于近平面的点),投影可以正确的将光线投射到场景的对应点。
Renderer.cpp->Render():

void Renderer::Render(const Scene& scene)
{
    
    
    //创建容器装入每一个像素
    std::vector<Vector3f> framebuffer(scene.width * scene.height);

    float scale = std::tan(deg2rad(scene.fov * 0.5f));
    //imageAspectRatio是宽高比,是原本的scene的宽高比
    float imageAspectRatio = scene.width / (float)scene.height;

    // Use this variable as the eye position to start your rays.
    //定义镜头位置为0,0,0,即人眼位置
    Vector3f eye_pos(0);
    //遍历每一个像素
    int m = 0;
    for (int j = 0; j < scene.height; ++j)
    {
    
    
        for (int i = 0; i < scene.width; ++i)
        {
    
    
            // generate primary ray direction,生成主光线的方向
            float x;
            float y;
            // TODO: Find the x and y positions of the current pixel to get the direction,找到像素的xy值确定光线的方向
            x = (2 * (i + 0.5) / scene.width - 1) * imageAspectRatio * scale;
            y = (1 - 2 * (j + 0.5) / scene.height) * scale ;
            // vector that passes through it.通过它的向量
            // Also, don't forget to multiply both of them with the variable *scale*, and
            // x (horizontal) variable with the *imageAspectRatio*            

            Vector3f dir = Vector3f(x, y, -1); // Don't forget to normalize this direction!
            dir = normalize(dir);
            framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
        }
        UpdateProgress(j / (float)scene.height);
    }

    // save framebuffer to file

    FILE* fp = fopen("binary.ppm", "wb");
    //文件写入
    (void)fprintf(fp, "P6\n%d %d\n255\n", scene.width, scene.height);
    for (auto i = 0; i < scene.height * scene.width; ++i) {
    
    
        static unsigned char color[3];
        color[0] = (char)(255 * clamp(0, 1, framebuffer[i].x));
        color[1] = (char)(255 * clamp(0, 1, framebuffer[i].y));
        color[2] = (char)(255 * clamp(0, 1, framebuffer[i].z));
        fwrite(color, 1, 3, fp);
    }
    fclose(fp);    
}

2.2.2投射射线交互、获得相交对象的属性

2.2.2.1投射射线交互

投射射线的交互,即是Renderer.cpp->Render()中实现的castRay()函数,下面会进行castRay函数的分析。
代码如下:

Vector3f castRay(
        const Vector3f &orig, const Vector3f &dir, const Scene& scene,
        int depth)//传入的参数有:光源起点orig,光线的照射方向:dir,场景对象:scene,最大的递归次数:depth
{
    
    
    if (depth > scene.maxDepth) {
    
    
        return Vector3f(0.0,0.0,0.0);//如果递归超过5次就返回黑色
    }
    //光线击中点的颜色
    Vector3f hitColor = scene.backgroundColor;//先定义击中颜色为背景颜色
    if (auto payload = trace(orig, dir, scene.get_objects()))//payload是有效荷载,记录了相关的碰撞处的信息,trace函数即是判断物体几何求交
    {
    
    
        //如果物体确实与光线相交,且已经得知交点的属性
        Vector3f hitPoint = orig + dir * payload->tNear;//光线击中点的位置(世界坐标位置:o+td表达式)
        Vector3f N; // normal,击中点法线
        Vector2f st; // st coordinates,击中点对应纹理坐标
        payload->hit_obj->getSurfaceProperties(hitPoint, dir, payload->index, payload->uv, N, st);//获取击中物体的表面属性
        //下面就是对不同的材质进行不同的计算
        switch (payload->hit_obj->materialType) {
    
    
            case REFLECTION_AND_REFRACTION:
            {
    
    
                Vector3f reflectionDirection = normalize(reflect(dir, N));
                Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                             hitPoint - N * scene.epsilon :
                                             hitPoint + N * scene.epsilon;
                Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
                                             hitPoint - N * scene.epsilon :
                                             hitPoint + N * scene.epsilon;
                Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
                Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);
                float kr = fresnel(dir, N, payload->hit_obj->ior);
                hitColor = reflectionColor * kr + refractionColor * (1 - kr);
                break;
            }
            case REFLECTION:
            {
    
    
                float kr = fresnel(dir, N, payload->hit_obj->ior);
                Vector3f reflectionDirection = reflect(dir, N);
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                             hitPoint + N * scene.epsilon :
                                             hitPoint - N * scene.epsilon;
                hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
                break;
            }
            default:
            {
    
    
                // [comment]
                // We use the Phong illumation model int the default case. The phong model
                // is composed of a diffuse and a specular reflection component.
                // [/comment]
                Vector3f lightAmt = 0, specularColor = 0;
                Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
                                           hitPoint + N * scene.epsilon :
                                           hitPoint - N * scene.epsilon;
                // [comment]
                // Loop over all lights in the scene and sum their contribution up
                // We also apply the lambert cosine law
                // [/comment]
                for (auto& light : scene.get_lights()) {
    
    
                    Vector3f lightDir = light->position - hitPoint;
                    // square of the distance between hitPoint and the light
                    float lightDistance2 = dotProduct(lightDir, lightDir);
                    lightDir = normalize(lightDir);
                    float LdotN = std::max(0.f, dotProduct(lightDir, N));
                    // is the point in shadow, and is the nearest occluding object closer to the object than the light itself?
                    auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
                    bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);

                    lightAmt += inShadow ? 0 : light->intensity * LdotN;
                    Vector3f reflectionDirection = reflect(-lightDir, N);

                    specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
                        payload->hit_obj->specularExponent) * light->intensity;
                }

                hitColor = lightAmt * payload->hit_obj->evalDiffuseColor(st) * payload->hit_obj->Kd + specularColor * payload->hit_obj->Ks;
                break;
            }
        }
    }

    return hitColor;
}

总体思路:
1.先是从传入的光源点、光线方向、场景对象和最大递归次数开始,因为我们求交的目的是要返回最终光线追踪到的场景中的点的颜色值hitColor,因为一开不知道光线碰撞点的颜色,所以默认为背景颜色;
2.对于每一束光线,如果递归次数未达到最大值,则进行是否有交点的判断(即使trace()函数!!),如果有交点,则将交点处的有效负载(payload,在trace()里定义的一个用来记录交点信息的对象!)返回,主要返回的是光源和击中点的距离t有效负载的已初始化对象(还没赋值,只是便于后面接受表面属性有已经定义的变量);
3.拜访有效负载的getSurfaceProperties()函数,得到当前物体在此交点可获得的属性(比如说球体就是返回当前球体上对应点的法线);
4.通过判断击中点的信息,来看是打在哪一种材料上(当前物体的材料),则进行不同操作(具体操作下面讲),最终通过不同材质得到交点的颜色值并返回。payload只是记录了相关交点的信息,它的作用是:最终将信息传进去payload里面的材质进行不同材质选取,才能通过这些信息和材质最终获取交点的颜色!
注意:
1.因为有效负载payload接收的内容是和当前物体有关,而所有物体都继承与Object类,所以payload定义:(在trace()函数里面)
std::optional<hit_payload> payload;//构造包含hit_payload类的optional类对象payload
hit_payload类是Renderer.hpp头文件里面的一个类:
在这里插入图片描述

2.2.2.2获得相交对象的属性

获得相交对象的属性是由trace()函数和getSurfaceProperties()函数共同完成的。
trace()函数:

std::optional<hit_payload> trace(const Vector3f &orig, const Vector3f &dir,const std::vector<std::unique_ptr<Object> > &objects)
{
    
    
    float tNear = kInfinity;//因为要比较出最小值,所以一开始都定义为可取的最大值(下面tNearK也一样),如果出现更小值很容易就储存为更小的那个值
                            //这是最终要求出来的离光源最近交点的距离
    std::optional<hit_payload> payload;//构造包含hit_payload类的optional类对象payload
    for (const auto & object : objects)//对所有物体进行循环
    {
    
    
        float tNearK = kInfinity;//在global里面定义的,返回float类型最大值,
        uint32_t indexK;
        Vector2f uvK;
        //tNear是经过遍历后目前这条光线遇见物体交点最近的位置

        //注意下面if条件语句里面的两个条件:object->intersect(orig, dir, tNearK, indexK, uvK)和tNearK < tNear
        if (object->intersect(orig, dir, tNearK, indexK, uvK) && tNearK < tNear)//这里就是判断是否与当前的物体的表面相交,在已经相交的情况下,
                                                                                //如果tNearK < tNear,则是说明此循环与光线相交的物体,
                                                                                //其与光线的最近交点比之前最小的tNear还要小,所以距离更新,
                                                                                //即下面的payload->tNear = tNearK,同时更新其他的所有属性
        {
    
    
            payload.emplace();//相当于对optional对象payload直接构造hit_payload类的对象,并在下面对此对象进行赋值
            payload->hit_obj = object.get();
            payload->tNear = tNearK;//传入intersect()的tNearK已经变成t0(与物体表面最近交点的距离)传出来了
            payload->index = indexK;
            payload->uv = uvK;
            tNear = tNearK;
        }
    }

    return payload;
}

intersect()函数:

//光线与球求交
    //判断ray是否与物体表面相交,与隐式球面表达式求交会有两个解
    //输入的光源点位置:orig,光线的方向:dir,光从光源点到物体表面的最小时间,相当于最小深度
    bool intersect(const Vector3f& orig, const Vector3f& dir, float& tnear, uint32_t&, Vector2f&) const override
    {
    
    
        // analytic solution
        Vector3f L = orig - center;
        float a = dotProduct(dir, dir);
        float b = 2 * dotProduct(dir, L);
        float c = dotProduct(L, L) - radius2;
        float t0, t1;//求根公式求的两个解
        if (!solveQuadratic(a, b, c, t0, t1))//求根公式,求出来的t0是小于t1的
            return false;
        //这两步其实是为了判断点光源在球体的哪个位置,排除掉球体在光源背面的情况
        if (t0 < 0)
            t0 = t1;
        if (t0 < 0)//这次判断的是t1了
            return false;
        tnear = t0;

        return true;
    }

分析:
castRay是将光线的光源和方向以及场景对象传进trace()函数,在此之前,光线并不知道要在哪里碰到物体,并没有得到距离t的确切值,所以需要遍历所有物体并不断更新最短的距离,而trace()函数通过创建有效负载payload,可以将计算的t赋给payload中的tNear,再返回整个payload对象到castRay()函数,之后进行对碰撞点其他属性的获取(即是getSurfaceProperties()函数)。而trace()函数中的intersect()函数,则是对不同物体进行求交的具体部分,上面展示了与球体的求交。(具体步骤见注释)在这里插入图片描述

在这里插入图片描述

注意:
注意if条件语句里面的两个条件:
object->intersect(orig, dir, tNearK, indexK, uvK)和tNearK < tNear
首先是要同时满足这两个条件,即遍历每个物体,光线与物体相交此次的tNearK < tNear
其次两个条件不能交换顺序,一开始其实tNeark和tNear都是一样大小(都返回float最大值),而intersect()函数是对物体进行具体求交计算的部分,会计算最短的tNearK值,只有在intersect()函数计算完tNearK值,才可以继续与tNear进行比较,如果此时tNeark值小于tNear值,就将tNear值换成tNeark的值。
getSurfaceProperties()函数:
只展示球体类里的getSurfaceProperties()函数:

void getSurfaceProperties(const Vector3f& P, const Vector3f&, const uint32_t&, const Vector2f&,
                              Vector3f& N, Vector2f&) const override
    {
    
    
        N = normalize(P - center);//即是获取物体表面点对应的法线,球体中心指向表面交点
    }

2.2.3递归光线跟踪

这一部分主要进行castRay()函数中对不同材质的具体递归算法分析。
在进行递归光线追踪时,先提一下反射、折射和菲涅尔现象。

2.2.3.1反射

由反射现象可知:入射角等于反射角,对于反射材质,我们只能利用现有的法线和入射光线的方向来计算反射光线的方向,所以进行下面的计算:
在这里插入图片描述

所以求反射光线的代码:

Vector3f reflect(const Vector3f &I, const Vector3f &N)
{
    
    
    return I - 2 * dotProduct(I, N) * N;
}

2.2.3.2折射

在折射中,有著名的Snell’s law公式:
![在这里插入图片描述](https://img-blog.csdnimg.cn/d49f89bdd81c42a799b0fbff03f29991.png
在这里插入图片描述
在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/a0533640595743ae819d36951c6d64d7.jpeg
所以折射光线的代码:

Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior)
{
    
    
    float cosi = clamp(-1, 1, dotProduct(I, N));
    float etai = 1, etat = ior;//定义空气折射率(为1)和物体材质折射率
    Vector3f n = N;
    if (cosi < 0) //要判断光线是从外部射入还是内部射出
    {
    
     cosi = -cosi; } 
    else {
    
     std::swap(etai, etat); n= -N; }
    float eta = etai / etat;
    float k = 1 - eta * eta * (1 - cosi * cosi);
    //k<0则不满足折射条件,说明是发生了全反射,此时相当于没有折射
    return k < 0 ? 0 : eta * I + (eta * cosi - sqrtf(k)) * n;
}

clamp()函数的作用:
在这里插入图片描述
注意
1.代码中用clamp()函数是将求出的在这里插入图片描述进行一个范围的约束;
2.注意要考虑光线是从外部射入还是内部射出:
代码如下:

//要判断光线是从外部射入还是内部射出
if (cosi < 0) 
    {
    
     cosi = -cosi; } //外部射入
    else {
    
     std::swap(etai, etat); n= -N; }//内部射出

在这里插入图片描述
正常的法线都是从球的表面指向球外部,如图,如果入射的方向变成从内部到外部,那按照公式推算时,要保证入射角是正的同时要使法线的方向相反且交换折射率,这样子才可以继续用原来的公式进行计算,所以代码中如果入射角大于90度(入射光线和法线点乘大于0),则要进行取反,同时法线取反,折射率交换;
3.注意全反射的情况:
在这里插入图片描述

2.2.3.3菲涅尔现象

透明物体(例如玻璃或水)既折射又反射。它们反射的光量与透射的光量实际上取决于入射角。当入射角减小时,透射的光量增加。并且由于按照能量守恒定律,反射光量加上折射光量必须等于入射光总量(不考虑光损失),所以可以推断出,当入射角增大时,反射光量会增加,角度接近90度时最高可达100%。从技术上讲,玻璃球的边缘是100%反射的。但是在球体的中心,该球体仅反射约6%的入射光。可以通过两幅图来体现菲涅尔现象:
在这里插入图片描述
从图中可以看出,离我们越近的水面,透射的现象越明显,反射现象越不明显;相反,离我们越远的水面,透射现象不明显,反射现象反而更加明显,在一些3D软件的材质中的菲涅尔,是说明某种材质在不同距离下呈现不同的反射效果。
在这里插入图片描述
菲涅尔现象展现出:光线随距离的远近产生不同的反射效果。

光由两个垂直波组成,我们称其为平行偏振光和垂直偏振光。我们需要使用两个不同的方程式计算这两个波的反射光比率,并对结果求平均值。两个菲涅耳方程为:
在这里插入图片描述

![在这里插入图片描述](https://img-blog.csdnimg.cn/e6a15e2c293c4fdcab74ce9d8e4063e2.png

其中的Rs或Rp是两种偏振下的反射率,反射率越大,进入眼睛的光越强。
通过计算平均值,就可以得到反射的比率:
在这里插入图片描述
项η1,η2是两种介质的折射率,项cosθi和cosθt分别是入射角和折射角。由于能量守恒,折射光的比率可以简单地计算为:
在这里插入图片描述
所以菲涅尔现象代码:

float fresnel(const Vector3f &I, const Vector3f &N, const float &ior)
{
    
    
    float cosi = clamp(-1, 1, dotProduct(I, N));//I和N的点乘,还不是入射角
    float etai = 1, etat = ior;//定义空气中的折射率1和物体折射率
    Vector3f n = N;
    if (cosi < 0)
    {
    
    
        cosi = fabsf(cosi);//处理float类型的绝对值,这里是
    }
    else //如果光线是从物体内部射向外面,则要交换折射率,为了计算公式的正确性 
    {
    
    
        std::swap(etai, etat);
        n = -N;
    }
   
    float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));//由上面折射公式(Snell's law)推导可知,这里求出是反射角的sin值

    //考虑全反射
    if (sint >= 1)//如果是符合折射公式推导的反射角,其sin值不会到达1 
    {
    
    
        return 1;//反射率为1
    }
    else {
    
    
        float cost = sqrtf(std::max(0.f, 1 - sint * sint));
        float Rs = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
        float Rp = ((etai * cost) - (etat * cosi)) / ((etai * cost) + (etat * cosi));
        return (Rs * Rs + Rp * Rp) / 2;
    }
}

2.2.3.4递归光线追踪

下面是castRay()函数里面材质选择的代码:

//下面就是对不同的材质进行不同的计算
        switch (payload->hit_obj->materialType) {
    
    
            case REFLECTION_AND_REFRACTION:
            {
    
    
                Vector3f reflectionDirection = normalize(reflect(dir, N));
                Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                             hitPoint - N * scene.epsilon :
                                             hitPoint + N * scene.epsilon;
                Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
                                             hitPoint - N * scene.epsilon :
                                             hitPoint + N * scene.epsilon;
                Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);
                Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);
                float kr = fresnel(dir, N, payload->hit_obj->ior);
                hitColor = reflectionColor * kr + refractionColor * (1 - kr);
                break;
            }
            case REFLECTION:
            {
    
    
                float kr = fresnel(dir, N, payload->hit_obj->ior);
                Vector3f reflectionDirection = reflect(dir, N);
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                             hitPoint + N * scene.epsilon :
                                             hitPoint - N * scene.epsilon;
                hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
                break;
            }
            default:
            {
    
    
                // [comment]
                // We use the Phong illumation model int the default case. The phong model
                // is composed of a diffuse and a specular reflection component.
                // [/comment]
                Vector3f lightAmt = 0, specularColor = 0;
                Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?
                                           hitPoint + N * scene.epsilon :
                                           hitPoint - N * scene.epsilon;
                // [comment]
                // Loop over all lights in the scene and sum their contribution up
                // We also apply the lambert cosine law
                // [/comment]
                for (auto& light : scene.get_lights()) {
    
    
                    Vector3f lightDir = light->position - hitPoint;
                    // square of the distance between hitPoint and the light
                    float lightDistance2 = dotProduct(lightDir, lightDir);
                    lightDir = normalize(lightDir);
                    float LdotN = std::max(0.f, dotProduct(lightDir, N));
                    // is the point in shadow, and is the nearest occluding object closer to the object than the light itself?
                    auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
                    bool inShadow = shadow_res && (shadow_res->tNear * shadow_res->tNear < lightDistance2);

                    lightAmt += inShadow ? 0 : light->intensity * LdotN;
                    Vector3f reflectionDirection = reflect(-lightDir, N);

                    specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
                        payload->hit_obj->specularExponent) * light->intensity;
                }

                hitColor = lightAmt * payload->hit_obj->evalDiffuseColor(st) * payload->hit_obj->Kd + specularColor * payload->hit_obj->Ks;
                break;
            }
        }
2.2.3.4.1玻璃材质(有反射也有折射)
case REFLECTION_AND_REFRACTION:
            {
    
    
                Vector3f reflectionDirection = normalize(reflect(dir, N));//反射光线的方向(记得归一化)
                Vector3f refractionDirection = normalize(refract(dir, N, payload->hit_obj->ior));//折射光线的方向(记得归一化)
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?//判断反射光线的起点在物体内还是物体外
                                             hitPoint - N * scene.epsilon :
                                             hitPoint + N * scene.epsilon;
                Vector3f refractionRayOrig = (dotProduct(refractionDirection, N) < 0) ?
                                             hitPoint - N * scene.epsilon :
                                             hitPoint + N * scene.epsilon;
                Vector3f reflectionColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1);//对反射的光线进行递归,计算与其他物体相交点的颜色
                Vector3f refractionColor = castRay(refractionRayOrig, refractionDirection, scene, depth + 1);//对折射的光线进行递归,计算与其他物体相交点的颜色
                float kr = fresnel(dir, N, payload->hit_obj->ior);//用菲涅尔按权重计算反射和折射光线返回原弹射点的颜色值
                hitColor = reflectionColor * kr + refractionColor * (1 - kr);
                break;
            }

注意:
在进行光线递归的时候,我们是把下一条光线的起点设置在上一条光线与物体的交点处,但事实上,由于float精度的问题,在计算第一条光线的距离t的时候就已经不是完全精确的了,而第一条光线与物体的交点可能在物体内部或者在物体外部。
如果没有对交点进行处理,则如果交点在物体内部,那可能反射的光线就会直接打在物体内表面,会导致光追结果出现错误。
在这里插入图片描述
在这里插入图片描述

所以我们可以通过对交点法线进行一个微小的偏移(朝着法线所在直线上偏移),来比较好的解决精度缺失的问题,但对于反射和折射,法线偏移的方向是有些不同的。
在这里插入图片描述
首先说反射,要分为光线从外部射入和内部射出两种情况考虑:
(注意法线的定义:交点向量减掉球体中心向量并进行归一化;还有,反射光线是由我们定义计算的!!)
光线外部射入:
在这里插入图片描述
光线内部射出:
在这里插入图片描述
接下来说一下折射:
同理,都是为了防止折射光线的自相交,所以要保证折射光线一定进入另一种介质里,才能获得正确的结果。
光线外部射入:
在这里插入图片描述
光线内部射出:
在这里插入图片描述

2.2.3.4.2只有反射的材质
case REFLECTION:
            {
    
    
                float kr = fresnel(dir, N, payload->hit_obj->ior);
                Vector3f reflectionDirection = reflect(dir, N);
                Vector3f reflectionRayOrig = (dotProduct(reflectionDirection, N) < 0) ?
                                             hitPoint + N * scene.epsilon :
                                             hitPoint - N * scene.epsilon;
                hitColor = castRay(reflectionRayOrig, reflectionDirection, scene, depth + 1) * kr;
                break;
            }
2.2.3.4.3默认材质

默认材质只设置有漫反射和镜面反射属性,用Phone光照模型来处理:
回顾一下Phone光照模型:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
注意:
1.对于相交对象的颜色,用Phong-shading模型来计算;
2.需要判断场景中的点是否在阴影中,要从相交对象向场景中的光源投射一个阴影射线(shadow ray)来判断这个相交对象是否在阴影中;
3.经典的whited-style光线追踪遇到漫反射表面会直接利用blinn-phong模型计算颜色值返回,而不再递归下去;
4.这是对默认材质的处理方法,对其他材质是用其他的做法。

default:
            {
    
          
                Vector3f lightAmt = 0, specularColor = 0;
                Vector3f shadowPointOrig = (dotProduct(dir, N) < 0) ?//反射光线,这里同样是为了递归做了交点偏移
                                           hitPoint + N * scene.epsilon :
                                           hitPoint - N * scene.epsilon;
                for (auto& light : scene.get_lights())//遍历场景内的光线,对每束光线都进行phone模型的光照着色 
                {
    
    
                    Vector3f lightDir = light->position - hitPoint;//人眼光路和物体的交点指向场景光源位置的向量
                    //交点和光源的距离平方
                    float lightDistance2 = dotProduct(lightDir, lightDir);
                    lightDir = normalize(lightDir);//归一化
                    float LdotN = std::max(0.f, dotProduct(lightDir, N));//用于漫反射的夹角
                    //这里是判断从人眼光路和某个物体的交点出发,向着场景光源处的光线是否有和其他物体相交(有相交说明此光线被其他物体遮挡了)
                    auto shadow_res = trace(shadowPointOrig, lightDir, scene.get_objects());
                    //判断是否在阴影里面
                    //满足两个条件:
                    //1.人眼可以看到阴影(所以人眼光路一定与产生阴影点的物体有交点hitPoint),同时阴影处向着场景光源发射的光线一定会被其他物体挡住;
                    //2.场景光源看不到阴影,所以交点与场景光源的距离一定大于场景光源与物体的最小距离tNear
                    bool inShadow = shadow_res && (powf(shadow_res->tNear,2) < lightDistance2);
                    //阴影怎么设置?用环境光系数来定义,此时的阴影点,实际上如果没有来自其他物体的反射光,就完全是黑色,                 
                    //直接可以被场景光线照到的点,直接进行漫反射操作即可。
                    lightAmt += inShadow ? 0 : light->intensity * LdotN;

                    Vector3f reflectionDirection = reflect(-lightDir, N);//得到反射光线(场景中的光源在点产生的反射光线)
                    //高光的着色
                    //这里可以不用求半程向量,因为有人眼的光路方向dir和反射光方向,高光越明显,两个向量方向越趋近于相反!!越趋近于>=0,则是完全不往人眼照射
                    specularColor += light->intensity * powf(std::max(0.f, -dotProduct(reflectionDirection, dir)),
                        payload->hit_obj->specularExponent);//specularExponent是相当于高光的p系数
                }
                //计算漫反射和高光的综合作用,将颜色返回
                hitColor = lightAmt * payload->hit_obj->evalDiffuseColor(st) * payload->hit_obj->Kd + specularColor * payload->hit_obj->Ks;//st是击中点对应纹理坐标
                break;
            }

最后,展示一下渲染的结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Phantom1516/article/details/127991761