GAMES101课程 作业6 源代码概览

Written by PiscesAlpaca(双鱼座羊驼)

一、概述

本篇将从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={"request_id"%3A"162216944616780357298394"%2C"scm"%3A"20140713.130102334.pc_all."}&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)

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

  1. Web编程基础--HTML、CSS、JavaScript 学习之课程作业“仿360极速浏览器新标签页”

    Web编程基础--HTML.CSS.JavaScript 学习之课程作业"仿360极速浏览器新标签页" 背景: 作为一个中专网站建设出身,之前总是做静态的HTML+CSS+DIV没 ...

  2. 课程作业01:模仿JavaAppArguments.java示例,编写一个程序,此程序从命令行接收多个数字,求和之后输出结果。

    1.设计思想: 首先是从JavaAppArguments.java示例开始,此示例已打印参数,定义数字 之和和作为存储单位的整型,然后将输入参数的字符串转化为整型,之后求和即可. 2.程序流程图: 3 ...

  3. coursea机器学习课程作业

    coursea机器学习课程作业 一 关于此课程 课程地址 图片来自网络 1.官网课程地址传送 2.如果访问官网速度较慢可以上 B站课程地址 机器学习是一门研究在非特定编程条件下让计算机采取行动的学科. ...

  4. THE LAST ONE!! 2017《面向对象程序设计》课程作业八

    THE LAST ONE!! 2017<面向对象程序设计>课程作业八 031602230 卢恺翔 GitHub传送门 题目描述 1.时间匆匆,本学期的博客作业就要结束了,是否有点不舍,是否 ...

  5. Django项目:CRM(客户关系管理系统)--76--66PerfectCRM实现CRM课程作业排名

    # classtop_urls.py # ————————64PerfectCRM实现CRM课程排名详情———————— from django.conf.urls import url from b ...

  6. 【UE4】GAMES101 图形学作业2:光栅化和深度缓存

    总览 在上次作业中,虽然我们在屏幕上画出一个线框三角形,但这看起来并不是那么的有趣.所以这一次我们继续推进一步--在屏幕上画出一个实心三角形,换言之,栅格化一个三角形.上一次作业中,在视口变化之后,我 ...

  7. 团队作业Week14——源代码管理

    0. 在吹牛之前,先回答这个问题: 如果你的团队来了一个新队员,有一台全新的机器, 你们是否有一个文档,只要设置了相应的权限,她就可以根据文档,从头开始搭建环境,并成功地把最新.最稳定版本的软件编译出 ...

  8. 2017秋 FZU SDN 课程作业汇总

    课程: SDN课程上机作业:SDN上机作业 参考作业: deepYY SDN作业: SDN作业 faberry的博客:faberry peiqiaoWang的博客:peiqiaoWang 相关博客汇总 ...

  9. SDN课程作业总结

    SDN 期末作业总结 设计场景 我们采用参考场景一,实现负载均衡,拓扑图及端口示意如下: 演示视频 视频地址 关键代码 package loadBalance; import java.io.Buff ...

  10. 【UE4】GAMES101 图形学作业5:光线与物体相交(球、三角面)

    总览 在这部分的课程中,我们将专注于使用光线追踪来渲染图像.在光线追踪中最重要的操作之一就是找到光线与物体的交点.一旦找到光线与物体的交点,就可以执行着色并返回像素颜色. 在这次作业中,我们要实现两个 ...

随机推荐

  1. KingbaseES 开启事务提交跟踪

    KingbaseESV8R6有个参数 track_commit_timestamp,用来开启跟踪事务提交的时间戳. 配置 编辑kingbase.conf,添加配置如下: track_commit_ti ...

  2. KingbaseES 绑定变量与游标共享

    对于重复执行的SQL,需要使用绑定变量,避免SQL的重复解析.但是,并不是说使用了绑定变量,就一定能避免硬解析.具体可以参见:https://www.cnblogs.com/kingbase/p/16 ...

  3. 项目管理构建工具——Maven(基础篇)

    项目管理构建工具--Maven(基础篇) 在前面的内容中我们学习了JDBC并且接触到了jar包概念 在后面我们的实际开发中会接触到很多jar包,jar包的导入需要到互联网上进行就会导致操作繁琐 Mav ...

  4. Taurus.MVC 微服务框架 入门开发教程:项目集成:6、微服务间的调用方式:Rpc.StartTaskAsync。

    系统目录: 本系列分为项目集成.项目部署.架构演进三个方向,后续会根据情况调整文章目录. 开源地址:https://github.com/cyq1162/Taurus.MVC 本系列第一篇:Tauru ...

  5. PHP之旅---出发(php+apache+MySQL)

    @ 目录 前言 准备 php安装 Apache安装 MySQL安装 Navicat安装(附) Apache+php整合 验证Apache+php 前言 本文详细介绍php+apache+MySQL在w ...

  6. Latex中也能展示动态图?

    技术背景 在学术领域,很多文档是用Latex做的,甚至有很多人用Latex Beamer来做PPT演示文稿.虽然在易用性和美观等角度来说,Latex Beamer很大程度上不如PowerPoint,但 ...

  7. Python Web开发主流框架

    Web 开发是Python 语言应用领域的重要部分,也是工作岗位最多的领域.如果你对基于Python的Web 开发有兴趣,正打算使用Python 做Web 开发,或者已经是一个Web 开发者有工作需要 ...

  8. Kubernetes 控制器

    在实际使用的时候并不会直接使用 Pod,而是会使用各种控制器来满足我们的需求,Kubernetes 中运行了一系列控制器来确保集群的当前状态与期望状态保持一致,它们就是 Kubernetes 的大脑. ...

  9. haproxy + keeplived

    两台主机: 192.168.2.163 192.168.2.165 # yum安装haproxy yum install haproxy # cat /etc/haproxy/haproxy.cfg ...

  10. .NET6 使用 AutoFac (解析)

    一.Net 6环境下的.net core项目里如何使用Autofac实现依赖注入. 通常的,我们把其他服务注入到Controller时,使用.net core自带的依赖注入即可,但是如果我们要实现自定 ...