GAMES101课程 作业6 源代码概览

2023-05-16

GAMES101课程 作业6 源代码概览

Written by PiscesAlpaca(双鱼座羊驼)

文章目录

  • GAMES101课程 作业6 源代码概览
    • 一、概述
    • 二、源码分析
      • 1 初始化
        • 1.1 场景初始化
        • 1.2 模型加载与三角片元生成
        • 1.3 BVH加速类实例化与生成时间记录
        • 1.4 灯光的加载
        • 1.5 递归化的BVH树生成
      • 2 渲染
        • 2.1 屏幕坐标与世界坐标的转换——获取眼睛朝各个像素看的方向
        • 2.2 真正的光线追踪过程
      • 3 生成
    • 三、结语

一、概述

本篇将从main函数为出发点,按照各cpp文件中函数的调用顺序和层级嵌套关系,简单分析本次作业代码的含义。鉴于本人是初学者,部分分析恐有偏颇,欢迎读者批评指正。

二、源码分析

1 初始化

1.1 场景初始化

main.cpp

Scene scene(1280, 960);

Scene.cpp

class Scene
{
public:
    // setting up options
    int width = 1280;
    int height = 960;

    Scene(int w, int h) : width(w), height(h)
    {} 
}

在main函数中,首先创建了一个场景,将场景的长和宽传入Scene类的构造函数中


1.2 模型加载与三角片元生成

main.cpp

MeshTriangle bunny("models/bunny/bunny.obj");

紧接着,在main函数中调用了加载obj模型文件的语句,我们跟进去看看里边做了什么

Triangle.hpp

    MeshTriangle(const std::string& filename)
    {
        objl::Loader loader;
        loader.LoadFile(filename); //根据文件路径加载obj文件

        assert(loader.LoadedMeshes.size() == 1);
        auto mesh = loader.LoadedMeshes[0]; //获取mesh

        Vector3f min_vert = Vector3f{std::numeric_limits<float>::infinity(),
                                     std::numeric_limits<float>::infinity(),
                                     std::numeric_limits<float>::infinity()};
        Vector3f max_vert = Vector3f{-std::numeric_limits<float>::infinity(),
                                     -std::numeric_limits<float>::infinity(),
                                     -std::numeric_limits<float>::infinity()};
        //上述两个语句分别创建了bounding_box的6个面的记录,这些记录使用最小点和最大点表示
        
        for (int i = 0; i < mesh.Vertices.size(); i += 3) {
            std::array<Vector3f, 3> face_vertices; //记录一个片元的三个顶点
            for (int j = 0; j < 3; j++) {
                auto vert = Vector3f(mesh.Vertices[i + j].Position.X,
                                     mesh.Vertices[i + j].Position.Y,
                                     mesh.Vertices[i + j].Position.Z) *
                            60.f;
                //对于每一个定点,都将其后续两个定点进行遍历,形成一个片元的记录,并将其放大60倍
                face_vertices[j] = vert;

                min_vert = Vector3f(std::min(min_vert.x, vert.x),
                                    std::min(min_vert.y, vert.y),
                                    std::min(min_vert.z, vert.z));
                max_vert = Vector3f(std::max(max_vert.x, vert.x),
                                    std::max(max_vert.y, vert.y),
                                    std::max(max_vert.z, vert.z));
                //在遍历中每次更新最大点和最小点
            }

            auto new_mat =
                new Material(MaterialType::DIFFUSE_AND_GLOSSY,
                             Vector3f(0.5, 0.5, 0.5), Vector3f(0, 0, 0));
            new_mat->Kd = 0.6;
            new_mat->Ks = 0.0;
            new_mat->specularExponent = 0;
			//创建一个材质,其具体系数将在下方介绍
            triangles.emplace_back(face_vertices[0], face_vertices[1],
                                   face_vertices[2], new_mat);
            //每个片元的三个顶点及其材质添加入triangles向量中,emplace_back与push_back有异曲同工之妙
        }

        bounding_box = Bounds3(min_vert, max_vert);//用两个最大点和最小点表示六个面

        std::vector<Object*> ptrs;
        for (auto& tri : triangles)
            ptrs.push_back(&tri);
        //浅拷贝一份triangels的vector

        bvh = new BVHAccel(ptrs); //创建BVH加速实例
    }

解释:

1、对于43行:triangles是Triangle类的vector,当调用emplace_back方法时,其实是调用了Triangle类的构造方法。方法的调用,确定了三角形片元的三个顶点,两条边,材质以及法向量。

class Triangle : public Object
{
public:
    Vector3f v0, v1, v2; // 顶点 A, B ,C , 逆时针方向
    Vector3f e1, e2;     // 2个边 v1-v0, v2-v0;
    Vector3f t0, t1, t2; // texture coords 纹理坐标
    Vector3f normal;//法向量
    Material* m;//材质
    
    //构造方法
	Triangle(Vector3f _v0, Vector3f _v1, Vector3f _v2, Material* _m = nullptr)
        : v0(_v0), v1(_v1), v2(_v2), m(_m)
    {
        e1 = v1 - v0;
        e2 = v2 - v0;
        normal = normalize(crossProduct(e1, e2)); //确定法向量
    }
}

2、bounding_box是对每个object模型物体的包装盒,使用两个点表示六个面(绝妙的表示方法)。Bounds3 bounding_box;它定义在Triangle.hpp文件中

3、objl::Loader是外部引入的加载器,在这里暂不做解读,后期有时间补上。


1.3 BVH加速类实例化与生成时间记录

BVH.hpp

    BVHAccel(std::vector<Object*> p, int maxPrimsInNode = 1, SplitMethod splitMethod = SplitMethod::NAIVE);

BVH.cpp

BVHAccel::BVHAccel(std::vector<Object*> p, int maxPrimsInNode,
                   SplitMethod splitMethod)
    : maxPrimsInNode(std::min(255, maxPrimsInNode)), splitMethod(splitMethod),
      primitives(std::move(p))
{
    time_t start, stop;
    time(&start);
    if (primitives.empty())
        return;

    root = recursiveBuild(primitives); //递归的构造BVH树

    time(&stop);
    double diff = difftime(stop, start);
    int hrs = (int)diff / 3600;
    int mins = ((int)diff / 60) - (hrs * 60);
    int secs = (int)diff - (hrs * 3600) - (mins * 60);

    printf(
        "\rBVH Generation complete: \nTime Taken: %i hrs, %i mins, %i secs\n\n",
        hrs, mins, secs);
}

上文中的最后一行代码创建了BVH加速实例,在这里,我们跟进这行代码,阅读一下构造函数的定义和实现。

1、在这里maxPrimsInNode表示最大片元,primitives 负责记录所有三角形片元的信息

2、在构造函数中,最为重要一句话是root = recursiveBuild(primitives);,我们将在下面详细解析,其余操作便是记录开始时间和结束时间,计算BVH加速总用时,并非代码核心,故不再展开。


1.4 灯光的加载

main.cpp

scene.Add(std::make_unique<Light>(Vector3f(-20, 70, 20), 1));
scene.Add(std::make_unique<Light>(Vector3f(20, 70, 20), 1));

Light.hpp

class Light
{
public:
    Light(const Vector3f &p, const Vector3f &i) : position(p), intensity(i) {}

    Vector3f position;
    Vector3f intensity;
};

在main函数中,我们可以看到灯光被添加到了场景中,Light的构造函数比较简单,仅仅是设置了位置和强度。


1.5 递归化的BVH树生成

BVH.cpp

接下来,我们详细的了解一下recursiveBuild函数到底做了什么,这里我们将这个函数分割成几段,逐一解析。

step1:遍历片元包装盒,生成所有片元的最大包装盒

(不过6-9行语句看起来并没有什么作用,但解释部分可以帮助我们了解调用过程,帮助后续程序理解)

BVHBuildNode* BVHAccel::recursiveBuild(std::vector<Object*> objects)
{
    BVHBuildNode* node = new BVHBuildNode(); //创建一棵树的根节点

    // Compute bounds of all primitives in BVH node
    Bounds3 bounds;
    for (int i = 0; i < objects.size(); ++i)
        bounds = Union(bounds, objects[i]->getBounds());
    

**解释:**上述代码对每一个三角片元进行bounds的合并,通过调用Bounds3.hpp中Union(const Bounds3 &b1, const Bounds3 &b2)方法实现,以下是该函数的实现:

inline Bounds3 Union(const Bounds3 &b1, const Bounds3 &b2)
{
    Bounds3 ret;
    ret.pMin = Vector3f::Min(b1.pMin, b2.pMin);
    ret.pMax = Vector3f::Max(b1.pMax, b2.pMax);
    return ret;
}

这段代码的大意是,将传入的两个Bounds(姑且称为包围盒),将两个包围盒中最小的顶点和最大的顶点找出来,将他们作为新的包围盒边界,从而达成了边界合并的效果,生成新的包围盒。应用于三角形片元中,我们可以知道,这是对上述提到过的object中所有三角形片元进行包围盒的合并。(包围盒的六个面依然使用最大点和最小点表示,对应了老师上课讲的Axis-Aligned形式)

对于objects[i]->getBounds()这段代码,我们可以在Triangle.hpp中找到其对Object类继承后方法的重载:

inline Bounds3 Triangle::getBounds() { return Union(Bounds3(v0, v1), v2); }

它实际上先后调用了构造函数Bounds3(const Vector3f p1, const Vector3f p2)和重载方法Union(const Bounds3 &b, const Vector3f &p),从而获取了每一个片元的包装盒。以下为源代码,我们可以在Bounds3.hpp中找到他们:

Bounds3(const Vector3f p1, const Vector3f p2)
{
    pMin = Vector3f(fmin(p1.x, p2.x), fmin(p1.y, p2.y), fmin(p1.z, p2.z));
    pMax = Vector3f(fmax(p1.x, p2.x), fmax(p1.y, p2.y), fmax(p1.z, p2.z));
}
inline Bounds3 Union(const Bounds3 &b, const Vector3f &p)
{
    Bounds3 ret;
    ret.pMin = Vector3f::Min(b.pMin, p);
    ret.pMax = Vector3f::Max(b.pMax, p);
    return ret;
}

step2:对于一个或两个片元的情况(叶子结点)

//如果仅有一个片元,则创建一个叶子结点    
	if (objects.size() == 1) { 
        // Create leaf _BVHBuildNode_
        node->bounds = objects[0]->getBounds();
        node->object = objects[0];
        node->left = nullptr;
        node->right = nullptr;
        return node;
    }
//如果有两个片元,则生成的节点分别记录指向的节点,且该节点实际记录的是两个子节点共同的大包装盒
    else if (objects.size() == 2) { 
        node->left = recursiveBuild(std::vector{objects[0]});
        node->right = recursiveBuild(std::vector{objects[1]});

        node->bounds = Union(node->left->bounds, node->right->bounds);
        return node;
    }

step3:获得所有片元质心的最大包装盒并按照质心分布重新排序

else {
    //以质心作为主要点,生成所有三角片元质心的大包装盒
    Bounds3 centroidBounds;
    for (int i = 0; i < objects.size(); ++i)
        centroidBounds =
            Union(centroidBounds, objects[i]->getBounds().Centroid());
    
    int dim = centroidBounds.maxExtent();

**解释:**这里我们看到了一个新的函数Centroid(),让我们来看看它做了什么。事实上,在Bounds3.hpp中,这个函数利用最小点和最大点的性质得到了片元的质心,通过union函数的不断调用,最终得到了包裹物体所有片元质心的最小点和最大点,即所有质心的包装盒

Vector3f Centroid() { return 0.5 * pMin + 0.5 * pMax; }

在第8行我们又看到了一个新的函数maxExtent(),同样它位于Bounds3.hpp中,以下是代码:

Vector3f Diagonal() const { return pMax - pMin; }
int maxExtent() const
{
    Vector3f d = Diagonal();
    if (d.x > d.y && d.x > d.z) //x分量最大
        return 0;
    else if (d.y > d.z) //y分量最大
        return 1;
    else //z分量最大
        return 2;
}

在这里,我们可以知道maxExtent()调用了Diagonal()函数获得了最小点和最大点的对角向量,由最小点指向最大点,当对角向量x分量最大,则返回0;y分量最大,则返回1;z分量最大,则返回2。

返回BVH.cpp,我们可以看到,实际上是根据整个物体质心在分量上的布局,对所有片原进行排序,方便后续构建BVH树.

std::sort函数,按照给定的方法中比较的策略对整个数组进行排布(从小到大)

    switch (dim) {
    case 0:
        std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
            return f1->getBounds().Centroid().x <
                   f2->getBounds().Centroid().x;
        });
        break;
    case 1:
        std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
            return f1->getBounds().Centroid().y <
                   f2->getBounds().Centroid().y;
        });
        break;
    case 2:
        std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
            return f1->getBounds().Centroid().z <
                   f2->getBounds().Centroid().z;
        });
        break;
    }


step4:划分初始的两个部分,递归的构建BVH树

    auto beginning = objects.begin(); //获取头指针
    auto middling = objects.begin() + (objects.size() / 2); //获取中间指针
    auto ending = objects.end(); //获取尾指针

    auto leftshapes = std::vector<Object*>(beginning, middling); //得到左侧区域
    auto rightshapes = std::vector<Object*>(middling, ending); //得到右侧区域

    assert(objects.size() == (leftshapes.size() + rightshapes.size()));

    node->left = recursiveBuild(leftshapes); //根节点的左子节点
    node->right = recursiveBuild(rightshapes); //根节点的右子节点

    node->bounds = Union(node->left->bounds, node->right->bounds); //最大包装盒
}

return node;
}

**小结:**可以看出,构建BVH树是以质心作为依据递归的划分区域的,非叶子结点仅仅存放bounds的范围,叶子结点会存放每个三角片元的bounds和片元指针,这与上课所讲的是一致的。至此初始化工作结束,接下来我们看到第二篇章,渲染。

2 渲染

2.1 屏幕坐标与世界坐标的转换——获取眼睛朝各个像素看的方向

Renderer.cpp

void Renderer::Render(const Scene& scene)
{
    std::vector<Vector3f> framebuffer(scene.width * scene.height);

    float scale = tan(deg2rad(scene.fov * 0.5));
    float imageAspectRatio = scene.width / (float)scene.height;
    Vector3f eye_pos(-1, 5, 10);
    int m = 0;
    for (uint32_t j = 0; j < scene.height; ++j) {
        for (uint32_t i = 0; i < scene.width; ++i) {
            // generate primary ray direction
            // 这里仅仅是通过将相机坐标转化为一个归一化的世界坐标,并假设相机在0,0,0点,从而求出眼睛看各个像素的方向向量,eye_pos才是世界坐标中眼睛真正的位置
            float x = (2 * (i + 0.5) / (float)scene.width - 1) * imageAspectRatio * scale;
            float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;
            // TODO: Find the x and y positions of the current pixel to get the
            // direction
            //  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*

            // Don't forget to normalize this direction!
            Vector3f dir = Vector3f(x, y, -1); // 实际上减去了相机0,0,0的坐标,归一化后是方向向量
            dir = normalize(dir);
            Ray r(eye_pos, dir); //此时传播时间未定,t实际上是0
            framebuffer[m++] = scene.castRay(r, 0);

        }
        UpdateProgress(j / (float)scene.height);
    }
    UpdateProgress(1.f);

**解释:**重要语句的基本含义已经在注释中体现,这里再进行一些小结。

1、上述代码利用双重循环对图片区域每个像素进行遍历,对每个像素取得其中点,并转换为归一化的世界坐标,从而获取眼睛所看到的方向(利用了向量的自由移动的性质)。

2、第13、14行,是栅格空间和世界坐标的转换过程,具体推导可参阅以下文章:

https://blog.csdn.net/dong89801033/article/details/114834898?ops_request_misc=%7B%22request%5Fid%22%3A%22162216944616780357298394%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=162216944616780357298394&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-2-114834898.pc_search_result_cache&utm_term=games101%E4%BD%9C%E4%B8%9A5&spm=1018.2226.3001.4187

3、在23行,仅仅是假设相机与可视平面的距离为1,利用缩放的性质得到方向向量,进而进行归一化操作,实际的眼睛起点依然是第7行的坐标;而相机坐标为(0,0,0)因此各个像素在可视平面上的坐标即为眼睛看到的方向。


2.2 真正的光线追踪过程

step1:在castRay函数中判断光的最大深度是否超出场景的最大深度

Scene.cpp

if (depth > this->maxDepth) { //光的最大深度超出场景的最大深度,则不会被渲染直接返回0,0,0 黑色
    return Vector3f(0.0,0.0,0.0);
}

step2:算出眼睛与物体最近的交点

这里调用了Intersection intersection = Scene::intersect(ray);一条语句,实际上这是光线追踪过程中嵌套调用最复杂的一个语句,让我们跟进它来看一看。

Scene.cpp

Intersection Scene::intersect(const Ray &ray) const
{
    return this->bvh->Intersect(ray);
}

BVH.cpp

Intersection BVHAccel::Intersect(const Ray& ray) const
{
    Intersection isect;
    if (!root) //如果bvh树根节点是空的
        return isect;
    isect = BVHAccel::getIntersection(root, ray);
    return isect;
}

BVH.cpp

Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
    // TODO Traverse the BVH to find intersection
    Intersection inter;
    //lights direction
    float x = ray.direction.x;
    float y = ray.direction.y;
    float z = ray.direction.z;
    //define lights direction whether is negtive 判断光线是否反向
    std::array<int, 3> dirIsNeg = { int(x<0),int(y<0),int(z<0) };
    //if bounds crash the ray
    if (node->bounds.IntersectP(ray, ray.direction_inv, dirIsNeg)) {
        // condition1: leaf node
        if(node->left == nullptr && node->right == nullptr) {
            inter = node->object->getIntersection((ray));
            return inter;
        } else {
            Intersection left = getIntersection(node->left, ray);
            Intersection right = getIntersection(node->right, ray);

            Intersection result;
            left.distance < right.distance ? result = left : result = right;     
            return result;   
        }
    }
    return inter;
}

**解释:**对于这一求交点的过程,程序先后调用了多个获取交点的函数,已经按照调用顺序在上方代码块给出。最为重要的是BVHAccel::getIntersection函数,它的主要思想是按照眼睛可视方向的x y z方向的分量和预先生成好的每个节点的bounds,以二叉树深度遍历的方式遍历BVH树,并在子节点判断是否与三角形片元相交,并返回交点的各个属性。

1、判断包装盒是否与眼睛可视方向相交

我们跟进第12行的函数,这是在作业中自行实现的方法,const Vector3f &invDir参数实际是光线向量矩阵的逆矩阵,在这里仅仅为了加快程序计算速度(注释里也说了乘法比除法快),可以理解x y z分量为方向,也可以理解为速度。

我们在这里获取每条光线各个分量与包装盒射入和射出时的时间,(pMin - ray.origin)为路程,invDir为速度分之一,则6个float为具体的时间。

当光线是从远离坐标原点方向射向坐标原点时,此时射入的时间会记录到max中,射出的时间会记录到min中,因此需要调换顺序。

在这之后就是对包装盒原理的运用,具体可参阅文章:

https://blog.csdn.net/weixin_44518102/article/details/122074548

inline bool Bounds3::IntersectP(const Ray &ray, const Vector3f &invDir,
                                const std::array<int, 3> &dirIsNeg) const
{
    // invDir: ray direction(x,y,z), invDir=(1.0/x,1.0/y,1.0/z), use this because Multiply is faster that Division
    // dirIsNeg: ray direction(x,y,z), dirIsNeg=[int(x>0),int(y>0),int(z>0)], use this to simplify your logic
    // TODO test if ray bound intersects
    float x_min = (pMin.x - ray.origin.x) * invDir.x; //invDir可以理解为方向,可以理解为速度
    float x_max = (pMax.x - ray.origin.x) * invDir.x;
    float y_min = (pMin.y - ray.origin.y) * invDir.y;
    float y_max = (pMax.y - ray.origin.y) * invDir.y;
    float z_min = (pMin.z - ray.origin.z) * invDir.z;
    float z_max = (pMax.z - ray.origin.z) * invDir.z;

    if (dirIsNeg[0])
    {
        std::swap(x_min, x_max);
    }
    if (dirIsNeg[1])
    {
        std::swap(y_min, y_max);
    }
    if (dirIsNeg[2])
    {
        std::swap(z_min, z_max);
    }

    float max = std::min(x_max, std::min(y_max, z_max));
    float min = std::max(x_min, std::max(y_min, z_min));

    if(min < max && max >= 0) return true;
    return false;
}

2、对叶子节点的处理

BVH.cpp

if(node->left == nullptr && node->right == nullptr) {
    inter = node->object->getIntersection((ray));
    return inter;

如上述代码所示,这是对叶子结点的操作,我们跟进getIntersection()函数。注意:这是对三角形片元的求交操作,故调用的是Triangle类的方法。

Triangle.hpp

inline Intersection Triangle::getIntersection(Ray ray) //为了计算传播时间,计算重心坐标是否在三角形内
{
    Intersection inter;

    if (dotProduct(ray.direction, normal) > 0) //此时说明可视方向与片元朝向相同,眼睛看不到
        return inter;
    double u, v, t_tmp = 0;
    Vector3f pvec = crossProduct(ray.direction, e2);
    double det = dotProduct(e1, pvec);
    if (fabs(det) < EPSILON)
        return inter;

    double det_inv = 1. / det;
    Vector3f tvec = ray.origin - v0;
    u = dotProduct(tvec, pvec) * det_inv; //b1
    if (u < 0 || u > 1)
        return inter;
    Vector3f qvec = crossProduct(tvec, e1);
    v = dotProduct(ray.direction, qvec) * det_inv; //b2
    if (v < 0 || u + v > 1)
        return inter;
    t_tmp = dotProduct(e2, qvec) * det_inv;

    // TODO find ray triangle intersection
    if (t_tmp < 0)
        return inter;

    inter.distance = t_tmp; //点到眼睛的传播时间
    inter.happened = true;
    inter.m = m; //点的材质就是三角形的材质
    inter.obj = this; //点所在的物体就是该片元的物体
    inter.normal = normal; //点的法线是三角形片元的法线
    inter.coords = ray(t_tmp); //实际的交点坐标 origin+direction*t

    return inter;
}

事实上,这段函数就是对Möller-Trumbore 算法的运用,其最终求出了眼睛可视方向射线与三角形片元相交的时间,并且利用u v变量作为公式中的b1 b2参数判断了交点是否位于三角形内(运用重心坐标),主要注释已在上方给出。

对于Möller-Trumbore 算法的详细推导,可以参阅文章:

https://blog.csdn.net/zhanxi1992/article/details/109903792

3、对非叶子节点的处理

对非叶子节点的处理便是简单的递归的调用左右两个子节点,直到遇到叶子节点位置。每次从子节点返回,便比较两子节点的distance值(时间),取最小的值所属的点为最终眼睛所见的交点

step3:获取片元属性并判断是否相交

让我们在上述多级的嵌套中回过神,继续回到Scene::castRay函数中。此时我们已经获得了

Intersection intersection = Scene::intersect(ray); //算出眼睛与物体最近的交点

这一语句的返回结果,接下来的步骤是对返回结果交点实例的属性的获取,大致包括物体、法向量、交点坐标、传播时间等属性,已经详细的列举在下方代码块中:

    Material *m = intersection.m;
    Object *hitObject = intersection.obj; //三角形片元的物体
    Vector3f hitColor = this->backgroundColor;
//    float tnear = kInfinity;
    Vector2f uv;
    uint32_t index = 0;
    if(intersection.happened) { //说明交点有效,与物体相交了

        Vector3f hitPoint = intersection.coords; //实际的交点坐标
        Vector3f N = intersection.normal; // normal 法向量
        Vector2f st; // st coordinates
        hitObject->getSurfaceProperties(hitPoint, ray.direction, index, uv, N, st);

step4:材质类型的选择与光照模型的应用

我们可以在函数中看到这句话:

switch (m->getType())

这便是对我们刚刚得到的交点中属性材质的筛选语句,由于本次实验中采用的材质类型是DIFFUSE_AND_GLOSSY,因此在本品文章中仅对这部分材质的代码块进行解析。

关于材质的类型,它们被定义在Material.hpp中

Material.hpp

enum MaterialType { DIFFUSE_AND_GLOSSY, REFLECTION_AND_REFRACTION, REFLECTION };

以下为本次实验所运用的Phone光照模型的实现:

default: //DIFFUSE_AND_GLOSSY
{
    // [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]
    
    // 环境光Ambient 高光specular
    Vector3f lightAmt = 0, specularColor = 0;
    Vector3f shadowPointOrig = (dotProduct(ray.direction, N) < 0) ?
                               hitPoint + N * EPSILON :
                               hitPoint - N * EPSILON;
    //判断眼睛观看方向与法线的夹角,如果夹角在0-90度之间,则说明光线照射方向相同;否则光线照射相反                        
    // [comment]
    // Loop over all lights in the scene and sum their contribution up
    // We also apply the lambert cosine law
    // [/comment]
    for (uint32_t i = 0; i < get_lights().size(); ++i)
    {
        //区域光(无意义)
        auto area_ptr = dynamic_cast<AreaLight*>(this->get_lights()[i].get());
        if (area_ptr)
        {
            // Do nothing for this assignment
        }
        else
        {
            Vector3f lightDir = get_lights()[i]->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)); //只有照射在表面才有意义
            Object *shadowHitObject = nullptr;//(无意义)
            float tNearShadow = kInfinity;//(无意义)
            // is the point in shadow, and is the nearest occluding object closer to the object than the light itself?
            //判断阴影点沿着光的逆方向是否能与其他片元相交,如果能相交则此处必定是阴影,如果不能相交,此处不是阴影
            bool inShadow = bvh->Intersect(Ray(shadowPointOrig, lightDir)).happened;
            lightAmt += (1 - inShadow) * get_lights()[i]->intensity * LdotN;
            Vector3f reflectionDirection = reflect(-lightDir, N); //获取平面反射情况下的反射光
            specularColor += powf(std::max(0.f, -dotProduct(reflectionDirection, ray.direction)), m->specularExponent) * get_lights()[i]->intensity;
        }
    }
    hitColor = lightAmt * (hitObject->evalDiffuseColor(st) * m->Kd + specularColor * m->Ks);
    break;
}

**解释:**其主要步骤如下

1、判断眼睛观看方向与法线的夹角,如果夹角在0-90度之间,则说明光线照射方向相同,否则光线照射相反 。如果相同则说明照射的是三角片元的背面,阴影点理应向与法线相反方向移动一定距离;如果光线照射相反,则说明照射的是三角片元的正面,阴影点理应向与法线相同方向移动一定距离。(代码8-13行)

2、生成了一个与光照方向相反的向量,用它来判断光是否照射到了表面。由于是相反的,则当大于0时,实际的光照其实是能够照射到表面的。(代码28-32行)

3、对于眼睛所看到的每一个像素,遍历场景中生成的所有光线,为照射到的交点生成环境光lightAmt和高光specular。

**[important]**其中稍微难以理解的是37行的代码,其实际上使用了和 判断眼睛能够看见的最近的交点 这一过程所使用到的相同的一系列函数。只不过这里由于shadowPointOrig是与光照方向相反的向量,我们可以理解为从交点射出一条光线,判断其在传播过程中是否会与物体中其他片元相交,只要能够相交,便能使这一交点实例中happened变量变为true,那么就说明当前的点会被其他交点遮挡,此时为它生成阴影即可。

反过来想,如果从光线传播方向正向判断,实际上是较为困难的事情,这一点的处理是很巧妙地。不过我认为直接调用bvh->Intersect方法未免影响效率,毕竟最终得到的还是最近距离的点,这需要再次遍历整个二叉树,不如改为只要遇到交点就返回,可以提高一定的效率。

对于环境光lightAmt和高光specular的计算便是38、40和43行的代码,其运用了Phone模型的公式,但该程序解法(evalDiffuseColor(st))函数与公式有所不同,这里不再仔细研究,可参阅:

https://blog.csdn.net/qjh5606/article/details/89761955

4、上述函数包含一些无意义的变量,不知是否是课程组无意放置的。


至此,castRay函数所有执行和调用便结束了,我们得以返回到Renderer::Render最初的地方。

**小结:**上述过程主要是先判断包装盒是否与眼睛可视方向相交,并进一步判断是否与片元相交,最终返回这一交点,并按照Phone模型生成最终光照的颜色,返回并存入framebuffer当中。

3 生成

    // 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] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].x));
        color[1] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].y));
        color[2] = (unsigned char)(255 * clamp(0, 1, framebuffer[i].z));
        fwrite(color, 1, 3, fp); //此函数将从指定流中写入n个大小为size的对象
    }
    fclose(fp);  

最终的帧缓存输出过程如上述代码所示,这一过程使用了ppm文件格式,第3行实际上是ppm文件的文件头,以P6开始,声明图像长宽,并设置最大像素;6-8行控制了RGB三种颜色的数值,确保它们在0-255之间。

若想了解ppm文件结构,可以参阅:

https://blog.csdn.net/kinghzkingkkk/article/details/70226214


三、结语

至此,源代码整体调用过程解读到此结束了。

非常荣幸能够参与GAMES101课程,这使得我对图形学中的光栅化和光线追踪有了细致的了解。

本篇文章的撰写略显仓促,源码阅读花费的时间也较少,也许在整体的理解和细节的把握上有失偏颇,欢迎广大网友批评指正。


欢迎指出错误和不足~

转载请注明出处!

本篇发布在以下博客或网站:

双鱼座羊驼 - 知乎 (zhihu.com)

pisces365的博客_CSDN博客

双鱼座羊驼 - SegmentFault 思否

双鱼座羊驼 的个人主页 - 动态 - 掘金 (juejin.cn)

双鱼座羊驼 - 博客园 (cnblogs.com)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

GAMES101课程 作业6 源代码概览 的相关文章

随机推荐

  • It is a distutils installed project

    方法1 xff1a 方法2 xff1a 直接全局搜索对应文件 xff0c 具体包括 34 package name 34 文件夹 和 34 package name 34 egg info xff0c 找到后直接删除即可 eg sudo f
  • git将当前修改文件生成patch

    应用场景 xff1a 只想将当前文件夹中改动的文件打包成一个patch xff0c 并不想提交相应的代码 执行命令 xff1a git diff nova api openstack compute servers py nova cmd
  • OpenStack--虚拟机操作(挂起、暂停和搁置)

    1 OpenStack虚拟机三种操作 1 1 挂起和恢复 xff08 suspend和resume xff09 挂起 xff1a openstack中的挂起 xff08 suspend xff09 操作是将虚拟机的内存保存到磁盘中 xff0
  • eMMC 里 DDR52 HS200 HS400 等的含义

    eMMC 里 DDR52 HS200 HS400 这些名词指的是不同的速度 DDR52就是最高 52M clock xff0c 数据速率就是 52 x 2 61 104 HS200 就是最高 200M clock xff0c 单通道 xff
  • Libvirt的job机制3--基本原理

    在Job机制简介中介绍了在libvirt中调用接口时如何使用Job机制 xff1b 在条件变量基本原理中介绍了条件变量的一些基本原理 xff0c 这个是Job机制的基础 本文主要介绍Job机制中用到的一些函数 结构体等变量 xff0c 分析
  • libvirt初始化流程分析

    libvirtd daemon初始化流程比较复杂 xff0c 本文对代码流程进行初步分析 xff0c 可能有不对的地方 xff0c 随时会进行更新和补充 总体代码流程 main remote remote deamon c daemonCo
  • OpenStack调试方法总结

    1 利用remote pdb进行调试 下载链接remote pdb 2 0 0 py2 py3 none any whl 虚拟化文档类资源 CSDN下载 from remote pdb import RemotePdb RemotePdb
  • Compute通过Conductor访问数据库方法

    目录 方法注册 实例序列化与反序列化 conductor与数据库交互 Instance类对象与ComputeManager类对象 compute服务的worker数量 这里以instance save函数为例进行说明 方法注册 nova组件
  • oslo_messaging学习系列之二《OpenStack中使用消息队列》

    目录 消息驱动 oslo messaging模块详解 Transport Target xff08 oslo messaging target Target xff09 Server RPC Client RPC Client参数传递 消息
  • PX4日志文件分析

    FlightPlot画图软件 xff1a http www pixhawk com dev flightplot 支持的日志格式 xff1a PX4自动驾驶仪日志 xff08 px4log 由 sdlog2 生成 xff09 APM二进制日
  • 固定翼航模及四旋翼无人机基础

    目录 一 机架 二 电机参数 1 电机的尺寸和KV值 2 KV值与力 桨的关系 三 桨叶参数 1 桨叶的直径和螺距 2 桨叶选配 3 电机与桨叶搭配表 四 电池与电调 五 航模电子设备连线图解 1 固定翼整机 2 多旋翼整机 3 接收机 4
  • FreeRTOS 之五种内存分配方案(概括总结)

    一 FreeRTOS 之五种内存分配方案 Heap 1 1 在此方案中 xff0c 实现了基础版本的pcPortMalloc xff0c 而没有实现vPortFree xff0c 因此它适用于从来不会删除任务的应用 2 动态分配的内存总是确
  • android9.0以上当切换程序后,service在后台运行一分钟左右就自动停止的解决方案

    service作为安卓四大组件之一 xff0c 在开发项目中很多人都会用到 xff0c 不知道大家有没有发现 xff0c 在android9 0以上的系统 xff0c 如果你在app启动了一个service xff0c 当你切换到另一个ap
  • ubuntu设置当前文件夹路径下快捷键打开终端的办法

    转发 地址链接 xff1a 添加链接描述
  • 移动网络支持

    名词介绍 IMSI 指国际移动用户识别码 xff08 IMSI xff1a International Mobile Subscriber Identification Number xff09 是区别移动用户的标志 xff0c 储存在SI
  • 2021-09-07NVIDIA Jetson Xavier NX载板 RTSO-6002使用TF(MicroSD)卡说明

    NVIDIA Jetson Xavier NX载板RTSO 6002使用TF卡说明 一 将根文件拷贝到格式化好的TF卡中二 开发主机下载对应安装包三 烧录环境搭建四 进入Recovery模式五 最关键的一步 xff0c 烧录 xff08 回
  • Jetson xavier nx | NVIDIA 安装ROS教程

    确认ubuntu软件设置 安装源 sudo sh c 39 echo 34 deb http packages ros org ros ubuntu lsb release sc main 34 gt etc apt sources lis
  • 操作系统课程设计pintos project1实验摘记

    前言 xff1a 本篇意在记录本学期结束的操作系统课程设计pintos project1实验报告和实现过程 整个实验参考了多篇文章也查阅了一些代码 xff0c 其中部分内容或与其他文章相同 xff0c 还请见谅 同时 xff0c 也为了测试
  • redis安装与基本使用

    redis安装与基本使用 文章目录 redis安装与基本使用一 redis在Ubuntu中的安装二 redis常用命令三 redis基本类型1 String2 List3 set4 Hash5 Zset有序集合6 geospatical 地
  • GAMES101课程 作业6 源代码概览

    GAMES101课程 作业6 源代码概览 Written by PiscesAlpaca xff08 双鱼座羊驼 xff09 文章目录 GAMES101课程 作业6 源代码概览一 概述二 源码分析1 初始化1 1 场景初始化1 2 模型加载