這是games101 現代計算機圖形學 作業06 by coolwx
感謝https://blog.csdn.net/miyu1994/article/details/107006010/ 這位大佬的文章,因為我一開始真的寫不出來SAH(啟發式搜索),看了這位大神的算法,我最終才理解了他的算法,其實還是比較簡單的,關於SAH,我應該會寫一篇筆記,來記錄我的感想,再次感謝以上這位大神的算法,因為我一開始真的想不出這個啟發式算法,所以代碼我會使用他的代碼,然后添上我的注釋,便於理解(哎,還是太菜,沒寫出來)。
作業要求:
其他,這次作業里面有兩個坑。
一個是castRay方法現在在Scene.cpp里面,然后castRay函數發生了一點變化,ray作為參數會傳進去,其實ray也就定義了direction和origin兩個參數。
細讀這次的所有代碼,也算是對整個光線追蹤有了更深刻的理解吧,可惜我本人還是太菜了。
第二個坑是我一開始沒畫出來我佛了,后來也是參考了這位大神的代碼,發現我對於Bounds包圍盒中的求解中,沒有考慮方向中有負數的情況,直接佛了。。
首先進入Triangle.h
修改getInsection函數,需要注意的是,這里需要理解以下參數
coords:光線經過的那一點的坐標,這里使用ray(double)(注意,我們在Ray.h中重載了(),用於計算t時刻的光線坐標)
happend:光線和三角形是否相交?
m:材質
normal:交點處三角形的法線
distance:光線經過的時間,也就是我們算出來的t_tmp。
object:事實上你進入Objects.h,就會發現基類Object(接口,里面一堆純虛函數),這里作為一個指針,指向本對象,也就是this(框架的編程還是很漂亮的),指代我們的三角形。
(注意我們作業5中我自己寫的求交,感覺low爆了,還是框架的寫的好看)
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);//s1 double det = dotProduct(e1, pvec); if (fabs(det) < EPSILON) return inter;//no solution double det_inv = 1. / det; Vector3f tvec = ray.origin - v0;//s u = dotProduct(tvec, pvec) * det_inv; if (u < 0 || u > 1) return inter; Vector3f qvec = crossProduct(tvec, e1); v = dotProduct(ray.direction, qvec) * det_inv; 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); return inter; }
然后進入Renderer.cpp,修改Render函數,注意這里,我們上面兩行代碼計算x和y就是作業5的答案,然后下面對比作業5的代碼出現了變化,因為框架修改了
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 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* Vector3f dir = Vector3f(x, y, -1); dir = normalize(dir); Ray ray(eye_pos,dir); framebuffer[m++] = scene.castRay(ray,0); // Don't forget to normalize this direction! } UpdateProgress(j / (float)scene.height); } UpdateProgress(1.f); // 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); } fclose(fp); }
進入Bounds3.cpp ,修改IntersectP函數,此函數用於求包圍盒是否和光線相交,這里特別要注意負數的情況,一開始我想后面這個數組有什么用,然后一直沒用到,最后卡住了,佛了,其實這個就是判斷大小用的,算最大最小的時候注意負數的情況
inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir, const std::array<int, 3>& dirIsNeg) const { float t_Min_x = (pMin.x - ray.origin.x)*invDir[0]; float t_Min_y = (pMin.y - ray.origin.y)*invDir[1]; float t_Min_z = (pMin.z - ray.origin.z)*invDir[2]; float t_Max_x = (pMax.x - ray.origin.x)*invDir[0]; float t_Max_y = (pMax.y - ray.origin.y)*invDir[1]; float t_Max_z = (pMax.z - ray.origin.z)*invDir[2]; if(!dirIsNeg[0]) { float t = t_Min_x; t_Min_x = t_Max_x; t_Max_x = t; } if(!dirIsNeg[1]) { float t = t_Min_y; t_Min_y = t_Max_y; t_Max_y = t; } if(!dirIsNeg[2]) { float t = t_Min_z; t_Min_z = t_Max_z; t_Max_z = t; } float t_enter = std::max(t_Min_x,std::max(t_Min_y,t_Min_z)); float t_exit = std::min(t_Max_x,std::min(t_Max_y,t_Max_z)); if(t_enter<t_exit&&t_exit>=0) return true; else return false; // 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 }
最后進入BVH.cpp,修改我們的getInsection,注意,我們這里是為了求交,根據閆神所講的,肯定要到葉子節點才算求交,而中間如果和包圍盒都沒交點,那么和三角形一定無交點,簡單二叉樹遍歷就行。
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const { Intersection intersect; Vector3f invdir(1./ray.direction.x,1./ray.direction.y,1./ray.direction.z); std::array<int, 3> dirIsNeg; dirIsNeg[0] = ray.direction.x>0; dirIsNeg[1] = ray.direction.y>0; dirIsNeg[2] = ray.direction.z>0; if(!node->bounds.IntersectP(ray,invdir,dirIsNeg)) { return intersect; } if(node->left == nullptr && node->right==nullptr) { return node->object->getIntersection(ray); } Intersection h1 = getIntersection(node->left,ray); Intersection h2 = getIntersection(node->right,ray); return h1.distance<h2.distance?h1:h2; return intersect; // TODO Traverse the BVH to find intersection }
其實到這里,我們的任務已經完成了,效果如下:
但是其實還沒完,注意我們的作業提高要求:

告訴我們是一個啟發性算法,這里的思想是如果有兩個包圍盒,B包圍A,那么光線過B也過A的概率就是SA/SB,
注意,我們在這一次作業的時候,觀察前面的rebuild方法,是遞歸地將objects數組分成左右兩個部分,那么SA可以說代表左邊的objects形成的總包圍盒的面積,而SB可以說是代表右邊的划分總的包圍盒的面積,SN代表我們的最大的包圍盒的面積,我們的想法是啟發式搜索(哇本人看到啟發式搜索就頭疼),這里Ctrav表示當前划分一次的花費,這里取1/8(這個值應該是可以版的,具體看https://blog.csdn.net/qq_39300235/article/details/106999514,這篇文章說的很好)
我們的目標是分割這兩個數組(搜索),所花費的cost越少越好。(注意,我們一開始划分數組的時候,方法是就是取最大的跨度維度,然后從中間划分)
但是啟發式搜索目的是不要讓我們從中間划分,而是划分的時候讓cost(也就是上面的式子C越小越好),找出這種划分方法就行。
https://blog.csdn.net/qq_39300235/article/details/106999514這篇文章其實很清楚,但是里面一堆桶的說法很麻煩,記住,我們的目的就是找出某個划分方法,然后讓cost最小,這樣形成的BVH結構不會有很大的深度。
我截取這篇文章的核心思想就是在每一個維度(x,y,z)分別計算一種划分(左,右),目的是求左右之間的中間值
這道題坑的地方是其實他 在bound3s中寫了一個offset(),這個東西是就是用來求上面那篇文章的桶的標號的。
還有,這里面有一個bid = bid -1 ,其實是我們分了12個桶(數組最大數11),但是根據offset是可以算出12的(12棵樹中間的間隔其實有13個,那么就把12的地方換成11就行,也就是桶的大小最后一個桶會大一點)
感謝https://blog.csdn.net/miyu1994/article/details/107006010/大神的代碼,我這里直接用了他的代碼,不過標記了一點注釋,這樣比較容易理解。
1 BVHBuildNode* BVHAccel::recursiveBuild(std::vector<Object*> objects) 2 { 3 BVHBuildNode* node = new BVHBuildNode(); 4 5 // Compute bounds of all primitives in BVH node 6 Bounds3 bounds; 7 for (int i = 0; i < objects.size(); ++i) 8 bounds = Union(bounds, objects[i]->getBounds()); 9 if (objects.size() == 1) { 10 // Create leaf _BVHBuildNode_ 11 node->bounds = objects[0]->getBounds(); 12 node->object = objects[0]; 13 node->left = nullptr; 14 node->right = nullptr; 15 return node; 16 } 17 else if (objects.size() == 2) { 18 node->left = recursiveBuild(std::vector{objects[0]}); 19 node->right = recursiveBuild(std::vector{objects[1]}); 20 21 node->bounds = Union(node->left->bounds, node->right->bounds); 22 return node; 23 } 24 else 25 { 26 Bounds3 centroidBounds; 27 //算出最大的包圍盒(通用的,因為兩個方法都要用到) 28 for (int i = 0; i < objects.size(); ++i) 29 centroidBounds = 30 Union(centroidBounds, objects[i]->getBounds().Centroid()); 31 32 std::vector<Object*> leftshapes; 33 std::vector<Object*> rightshapes; 34 35 switch (splitMethod)//這里注意了在BVH.h里面有個枚舉類,構造函數中的初始將決定是普通方法,還是SAH 36 { 37 case SplitMethod::NAIVE: 38 { 39 int dim = centroidBounds.maxExtent();//算出最大的跨度對應的值,x為0,y為1,z為2 40 int index = objects.size() / 2; 41 switch (dim) 42 //排序,針對最大的跨度排序 43 { 44 case 0: 45 std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) { 46 return f1->getBounds().Centroid().x < 47 f2->getBounds().Centroid().x; 48 }); 49 break; 50 case 1: 51 std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) { 52 return f1->getBounds().Centroid().y < 53 f2->getBounds().Centroid().y; 54 }); 55 break; 56 case 2: 57 std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) { 58 return f1->getBounds().Centroid().z < 59 f2->getBounds().Centroid().z; 60 }); 61 break; 62 } 63 64 auto beginning = objects.begin(); 65 auto middling = objects.begin() + index; 66 auto ending = objects.end(); 67 //遞歸算法,樞軸是中間元素。 68 leftshapes = std::vector<Object *>(beginning, middling); 69 rightshapes = std::vector<Object *>(middling, ending); 70 } 71 break; 72 case SplitMethod::SAH: 73 { 74 float nArea = centroidBounds.SurfaceArea();//算出最大的 75 76 int minCostCoor = 0; 77 int mincostIndex = 0; 78 float minCost = std::numeric_limits<float>::infinity(); 79 std::map<int, std::map<int, int>> indexMap; 80 //indexmap用於記錄x,y,z(前一個int代表x,y,z,后一個map代表那個軸對應的map) 81 //遍歷x,y,z的划分 82 for(int i = 0; i < 3; i++) 83 { 84 int bucketCount = 12;//桶的個數,這里定了12個桶,就是在某一個軸上面划分了12個區域 85 std::vector<Bounds3> boundsBuckets; 86 std::vector<int> countBucket; 87 for(int j = 0; j < bucketCount; j++) 88 { 89 boundsBuckets.push_back(Bounds3()); 90 countBucket.push_back(0); 91 } 92 93 std::map<int, int> objMap; 94 95 for(int j = 0; j < objects.size(); j++) 96 { 97 int bid = bucketCount * (centroidBounds.Offset(objects[j]->getBounds().Centroid()))[i];//算出對應x,y。z上的id值,這里【i】代表x,y,z 98 if(bid > bucketCount - 1)//實質是可以划分13個區域的,將最后一個區域合並。 99 { 100 bid = bucketCount - 1; 101 } 102 Bounds3 b = boundsBuckets[bid]; 103 b = Union(b, objects[j]->getBounds().Centroid()); 104 boundsBuckets[bid] = b; 105 countBucket[bid] = countBucket[bid] + 1; 106 objMap.insert(std::make_pair(j, bid)); 107 } 108 109 indexMap.insert(std::make_pair(i, objMap)); 110 //對於每一個划分,計算他所對應的花費,方法是對於桶中的每一個面積,計算他的花費,最后進行計算 111 //找出這個划分。 112 for(int j = 1; j < boundsBuckets.size(); j++) 113 { 114 Bounds3 A; 115 Bounds3 B; 116 int countA = 0; 117 int countB = 0; 118 for(int k = 0; k < j; k++) 119 { 120 A = Union(A, boundsBuckets[k]); 121 countA += countBucket[k]; 122 } 123 124 for(int k = j; k < boundsBuckets.size(); k++) 125 { 126 B = Union(B, boundsBuckets[k]); 127 countB += countBucket[k]; 128 } 129 130 float cost = 1 + (countA * A.SurfaceArea() + countB * B.SurfaceArea()) / nArea;//計算花費 131 //找出這個花費。 132 if(cost < minCost) 133 { 134 minCost = cost; 135 mincostIndex = j; 136 minCostCoor = i; 137 } 138 } 139 } 140 //加入左右數組,這里很重要,具體還是看那篇博客 141 for(int i = 0; i < objects.size(); i++) 142 { 143 if(indexMap[minCostCoor][i] < mincostIndex) 144 { 145 leftshapes.push_back(objects[i]); 146 } 147 else 148 { 149 rightshapes.push_back(objects[i]); 150 } 151 } 152 } 153 break; 154 dafault: 155 break; 156 } 157 158 assert(objects.size() == (leftshapes.size() + rightshapes.size())); 159 //遞歸計算,同普通方法 160 node->left = recursiveBuild(leftshapes); 161 node->right = recursiveBuild(rightshapes); 162 163 node->bounds = Union(node->left->bounds, node->right->bounds); 164 } 165 166 return node; 167 }
最后終於完成了。
這一次還是很有挑戰性的,里面涉及啟發式搜索,BVH,bounds求交