??如果你喜歡我寫的文章,可以把我的公眾號設為星標 ??,這樣每次有更新就可以及時推送給你啦
在第三天的學習中,我們學會了如何利用重心坐標演算法畫三角形,并運用三角形繪制演算法把人頭模型畫了出來,雖然最后的渲染結果能看出來這是個腦袋,但是嘴巴處有很明顯的穿幫,這一天我們就學習一下,如何利用 Z-buffering(深度緩沖)來解決層疊問題,
??本文原始碼 ??:toyRenderer-day04-Z-buffering
1.畫家演算法
在正式講解 Z-buffering 問題之前,我們先來了解一下畫家演算法,這個演算法的思想極其簡單,我們可以結合下圖簡單分析一下:

如果要畫一個有山有草有樹林的風景畫,一個初學者畫家可以按以下繪制順序畫畫:
首先畫最遠處的山 然后畫次遠處的草原 最后畫最近的樹木
或者我們用更程式員的方式描述一下:
首先畫 z-index=1處的山然后畫 z-index=2處的草原最后畫 z-index=3的樹木
在現代主流的 UI 渲染引擎中,各個元素的先后層級順序基本上都是用「畫家演算法」這種思路決定的:
網頁通過 CSS 的 z-index控制層級順序iOS 通過 layer.zPosition控制層級順序Android 通過 index控制層級順序
平常畫 UI 時,我們可以簡單粗暴的把各個 View 理解為一個一個的二維盒子,每個盒子在 z 軸上都是互相獨立的,這樣我們就可以方便的用 z-index 動態控制盒子的層級;但是在渲染三維物體時,三維模型在 z 軸上是連續的,并且三維模型間還會互相組合交錯,這種通過 z-index 控制層級的方案很難奏效,
舉個最簡單的例子,下圖中三個互相交錯的三角形,只使用 z-index 是無法區分層級的,更不要說繪制了:

??注:Newell 演算法可以解決多邊形重疊導致排序困難的問題,感興趣的同學可以自行查閱學習
為了解決這個問題,2020 年獲得「圖靈獎」的計算機圖形學大佬——艾德文·卡特姆,提出了一個著名的演算法——Z-buffering,
2.Z-buffering
Z-buffering,中文名又為「深度圖」「深度緩沖」,它是通過記錄比較每個像素的深度資訊來解決層級問題,
Z-buffering 演算法理解起來其實是非常直觀的,我們這里借用《虎書 4》里的一張插圖(可以關注???號「鹵蛋實驗室」后臺回復「圖形學」領取本書)來講解一下 Z-buffering 的作業原理,

首先我們假設要在一個 8*8 的螢屏上渲染兩個互相遮擋的三角形,我們在正式渲染前先開辟一塊兒 8*8 的二維記憶體空間,這個空間的默認值均為 -∞,
假設我們已知兩個三角形的每個像素的深度資訊,紅三角形的深度均為 5,紫三角形的深度區間為 [3, 8],
我們先遍歷紅色三角形的所有像素,和 Z-buffering 的默認值 -∞ 比較,哪個值大,就保留哪個值,經過第一輪比較后,我們就記錄了紅色三角形的深度資訊,
然后我們遍歷紫色三角形的所有像素,和最新的 Z-buffering 逐像素比較,哪個值大,就保留哪個值,第二輪比較后我們就又記錄了紫色三角形的深度資訊,
最后我們就得到了一份深度緩沖,它記錄了這張圖片的層級順序,最終渲染時我們按這個深度緩沖逐像素渲染三角形即可,
上面的思路寫成偽代碼就是這樣的:
// 首先假設深度默認值都是負無窮 -∞(這里可以是無窮大,也可以是無窮小,依坐標系而定)
for (each triangle T) // 遍歷每個三角形
for (each sample (x,y,z) in T) // 遍歷三角形里的每個像素
if (z > zbuffer[x,y]) // 如果深度大于已有的值,
framebuffer[x,y] = rgb; // 則更新顏色,
zbuffer[x,y] = z; // 并更新 zbuffer
else
// do nothing // 小于已有的值,就說明這個像素點被遮擋不需要繪制了
3.代碼實作
理解了上面的偽代碼,現成真正的代碼就很容易了,
首先我們定義一下 Z-buffering 的資料結構,按道理來說,我們直接定義成一個二維陣列是最符合渲染場景的,第一維表示列,第二維表示行:
// [[1, 2, 3],
// [4, 5, 6],
// [7, 8, 9]]
但是我們并不需要這樣寫,我們可以把二維陣列拍平,然后通過偏移量進行訪問(可以聯想一下回圈佇列和最大堆這兩種資料結構的底層實作):
// [[1, 2, 3], [1, 2, 3,
// [4, 5, 6], => 4, 5, 6,
// [7, 8, 9]] 7, 8, 9],
定義好結構后,我們給 Z-buffering 的每個子元素都賦上 -∞ 的默認值:
float *zbuffer = new float[width * height];
for (int i=0; i < width * height; i++) {
zbuffer[i] = -std::numeric_limits<float>::max();
}
最后把上面的偽代碼翻譯為正常的 cpp 代碼就可以了:
//......
Vec3f P;
for (P.x = boxmin.x; P.x <= boxmax.x; P.x++) {
for (P.y = boxmin.y; P.y <= boxmax.y; P.y++) {
Vec3f bc_screen = barycentric(pts, P); // bc 是 Barycentric Coordinates 的縮寫
//......
// 計算當前像素的 zbuffer
P.z = 0;
for (int i = 0; i < 3; i++) {
P.z += pts[i][2] * bc_screen[i];
}
// 更新總的 zbuffer 并繪制
if(zbuffer[int(P.x + P.y * width)] < P.z) {
zbuffer[int(P.x + P.y * width)] = P.z;
image.set(P.x, P.y, color);
}
}
}
//......
加入 Z-buffering 計算后,我們渲染的模型就完全正常了:

相應的,如果把 Z-buffering 渲染為一張圖,則是下面這樣的:

個人認為 Z-buffering 的概念還是很簡單的,理論了解清楚后代碼很容易寫出來,在實際應用中,Z-buffering 其實還有很多的問題,例如因為精度問題引起的 z-fighting,相應的也有一些解決方案,因為本系列教程目標只是構建一個最小功能的軟渲染器,這些相對深入的問題就不探討了,感興趣的同學可以自行搜索學習,
??如果你喜歡我的文章,希望點贊?? 收藏 ?? 在看 ?? 三連關注一下
歡迎大家關注我的微信公眾號:鹵蛋實驗室,目前專注前端技術,對圖形學也有一些微小研究,
也可以加我的微信 egg_labs,歡迎大家來撩,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/258330.html
標籤:其他
上一篇:計算機網路與通信網路
下一篇:如何用阿里云ECS搭建網站
