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函数到底做了什么,这里咱们将这个函数宰割成几段,逐个解析。
<u>step1:</u>遍历片元包装盒,生成所有片元的最大包装盒
(不过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;}
<u>step2:</u>对于一个或两个片元的状况(叶子结点)
//如果仅有一个片元,则创立一个叶子结点 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; }
<u>step3:</u>取得所有片元质心的最大包装盒并依照质心散布从新排序
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; }
<u>step4:</u>划分初始的两个局部,递归的构建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/dong898...
3、在23行,仅仅是假如相机与可视立体的间隔为1,利用缩放的性质失去方向向量,进而进行归一化操作,理论的眼睛终点仍然是第7行的坐标;而相机坐标为(0,0,0)因而各个像素在可视立体上的坐标即为眼睛看到的方向。
2.2 真正的光线追踪过程
<u>step1:</u>在castRay函数中判断光的最大深度是否超出场景的最大深度
Scene.cpp
if (depth > this->maxDepth) { //光的最大深度超出场景的最大深度,则不会被渲染间接返回0,0,0 彩色 return Vector3f(0.0,0.0,0.0);}
<u>step2:</u>算出眼睛与物体最近的交点
这里调用了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_...
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/zhanxi1...
3、对非叶子节点的解决
对非叶子节点的解决便是简略的递归的调用左右两个子节点,直到遇到叶子节点地位。每次从子节点返回,便比拟两子节点的distance值(工夫),取最小的值所属的点为最终眼睛所见的交点
<u>step3</u>:获取片元属性并判断是否相交
让咱们在上述多级的嵌套中回过神,持续回到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);
<u>step4</u>:材质类型的抉择与光照模型的利用
咱们能够在函数中看到这句话:
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...
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/kinghzk...
三、结语
至此,源代码整体调用过程解读到此结束了。
十分荣幸可能参加GAMES101课程,这使得我对图形学中的光栅化和光线追踪有了粗疏的理解。
本篇文章的撰写略显仓促,源码浏览破费的工夫也较少,兴许在整体的了解和细节的把握上有失偏颇,欢送宽广网友批评指正。
欢送指出谬误和有余~
转载请注明出处!
本篇公布在以下博客或网站:
双鱼座羊驼 - 知乎 (zhihu.com)
pisces365的博客_CSDN博客
双鱼座羊驼 - SegmentFault 思否
双鱼座羊驼 的个人主页 - 动静 - 掘金 (juejin.cn)
双鱼座羊驼 - 博客园 (cnblogs.com)