引入
您好,我來介紹一下我的朋友z緩衝區,一個黑色的夥計。 他將幫助我們避免上一課中隱藏面移除的視覺效果。
順便說一句,我想提一下,我在課程中大量使用的這個模型是由Vidar Rapp創建的。 他授予了我使用許可,以便我可以教授關於渲染的基礎知識。雖然我對它進行了破壞,但我保證我會把眼睛還給那個人。
好吧,回到主題,理論上我們可以繪製所有三角形而不丟棄任何一個。 如果我們正確地從後到前開始繪製三角形,前面將擦除後面。,這被稱爲畫家算法。 不幸的是,它伴隨很高的計算成本:對於每一次攝像機的運動,我們需要對所有場景進行重新排序。 而且還有動態場景,複雜程度就更不必說了......這甚至還不是主要問題。 主要問題是我們並不總是能夠確定正確的順序。
我們嘗試渲染一個簡單的場景
想象一下由三個三角形組成的簡單場景:攝像機從上到下看,我們將彩色三角形投影到白色屏幕上:
渲染結果應該如下所示:
藍色小平面 - 它是紅色背後還是前方呢?可見, 畫家的算法在這裏不起作用。 可以將藍色小平面分成兩個(一個在紅色小平面前面,一個在後面)。 然後紅色前面的那個被分成兩個 - 一個在綠色三角形前面,一個在後面......我想你可能意識到了問題:在有數百萬個三角形的場景中,計算起來真的代價很高。 可以使用BSP樹來解決它。 順便說一下,這個數據結構對於移動相機來說是不變的,但它確實非常混亂。 生命太短暫,不能讓它變得凌亂。
簡化問題:丟棄一個維度。Y緩衝區。
讓我們暫時丟棄一個維度並沿着黃色平面切割上面的場景:
我的意思是,現在我們的場景由三個線段組成(黃色平面和每個三角形的交線),最終渲染具有正常寬度但是1個像素高度:
與往常一樣,提交代碼在此。 我們的場景是二維的,所以使用我們在第一課中編寫的line()函數就可以輕鬆繪製它。
{ // just dumping the 2d scene (yay we have enough dimensions!)
TGAImage scene(width, height, TGAImage::RGB);
// scene "2d mesh"
line(Vec2i(20, 34), Vec2i(744, 400), scene, red);
line(Vec2i(120, 434), Vec2i(444, 400), scene, green);
line(Vec2i(330, 463), Vec2i(594, 200), scene, blue);
// screen line
line(Vec2i(10, 10), Vec2i(790, 10), scene, white);
scene.flip_vertically(); // i want to have the origin at the left bottom corner of the image
scene.write_tga_file("scene.tga");
}
如果我們從側面看,我們2D場景的是這樣的:
讓我們渲染一下。 回想一下,渲染的是1像素高度。 在我的源代碼中,我創建了16像素高的圖像,以便於在高分辨率屏幕上閱讀。 rasterize()函數僅在圖像的第一行中寫入。
TGAImage render(width, 16, TGAImage::RGB);
int ybuffer[width];
for (int i=0; i<width; i++) {
ybuffer[i] = std::numeric_limits<int>::min();
}
rasterize(Vec2i(20, 34), Vec2i(744, 400), render, red, ybuffer);
rasterize(Vec2i(120, 434), Vec2i(444, 400), render, green, ybuffer);
rasterize(Vec2i(330, 463), Vec2i(594, 200), render, blue, ybuffer);
所以,我聲明瞭一個數組ybuffer,尺寸爲(width,1)。 該數組初始化爲負無窮大。 然後我調用rasterize()函數,並將數組和圖像render作爲參數。這個函數如下:
void rasterize(Vec2i p0, Vec2i p1, TGAImage &image, TGAColor color, int ybuffer[]) {
if (p0.x>p1.x) {
std::swap(p0, p1);
}
for (int x=p0.x; x<=p1.x; x++) {
float t = (x-p0.x)/(float)(p1.x-p0.x);
int y = p0.y*(1.-t) + p1.y*t;
if (ybuffer[x]<y) {
ybuffer[x] = y;
image.set(x, 0, color);
}
}
}
其實非常簡單。我遍歷了從p0.x到p1.x之間的所有x座標,並計算了線段對應的y座標。然後我檢查了ybuffer數組中序號爲x的值。如果當前y值比ybuffer中的值更接近相機,那麼我將繪製當前的像素,並更新對應ybuffer的值。
讓我們一步步來看。在第一個紅色線段調用rasterlize()之後,我們的內存是這樣的。
屏幕:
ybuffer:
在這裏,品紅色表示無窮小,那些堤防表示我們無法觸及的屏幕。其他的表示爲灰色:灰度值比較低的堤防更接近相機,灰度值更高的地方離相機遠。
然後我們繪製綠色線段。
屏幕:
ybuffer:
最後繪製藍色線段。
屏幕:
ybuffer:
恭喜,我們在一維屏幕上繪製了二維場景。讓我們再次欣賞我們的渲染結果:
回到三維
對於在二維屏幕上繪製三維場景,我們需要一個二維的Z緩衝區z-buffer。
int *zbuffer = new int[width*height];
我將二維緩衝區打包成一維的,轉換方式也是極簡單的:
int idx = x + y*width;
z緩衝序號到x、y座標的轉換如下:
int x = idx % width;
int y = idx / width;
然後在代碼中我簡單地遍歷所有三角形,並使用當前三角形和對z緩衝區的引用作爲參數調用光柵化器函數。唯一的困難是如何計算我們想要繪製的像素的z值。 讓我們回想一下我們如何計算之前y緩衝區示例中的y值:
int y = p0.y*(1.-t) + p1.y*t;
變量t的本質是什麼? 其實,(1-t,t)是點(x,y)相對於區段p0的重心座標,p1:(x,y)= p0 *(1-t)+ p1 * t。 所以我們的想法是,採用三角柵格化的重心座標版本,對於我們想要繪製的每個像素,只需將其重心座標乘以我們柵格化的三角形頂點的z值:
triangle(screen_coords, float *zbuffer, image, TGAColor(intensity*255, intensity*255, intensity*255, 255));
[...]
void triangle(Vec3f *pts, float *zbuffer, TGAImage &image, TGAColor color) {
Vec2f bboxmin( std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
Vec2f bboxmax(-std::numeric_limits<float>::max(), -std::numeric_limits<float>::max());
Vec2f clamp(image.get_width()-1, image.get_height()-1);
for (int i=0; i<3; i++) {
for (int j=0; j<2; j++) {
bboxmin[j] = std::max(0.f, std::min(bboxmin[j], pts[i][j]));
bboxmax[j] = std::min(clamp[j], std::max(bboxmax[j], pts[i][j]));
}
}
Vec3f P;
for (P.x=bboxmin.x; P.x<=bboxmax.x; P.x++) {
for (P.y=bboxmin.y; P.y<=bboxmax.y; P.y++) {
Vec3f bc_screen = barycentric(pts[0], pts[1], pts[2], P);
if (bc_screen.x<0 || bc_screen.y<0 || bc_screen.z<0) continue;
P.z = 0;
for (int i=0; i<3; i++) P.z += pts[i][2]*bc_screen[i];
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值,還有什麼可以做的嗎?
答案是添加紋理! 這將是我們的家庭作業。
在.obj文件中,我們有以“vt u v”開頭的行,它們給出了一組紋理座標。 “f x / x / x x / x / x x / x / x”中間(斜線之間)的數字是該三角形的該頂點的紋理座標。 將其插入三角形內部,乘以紋理圖像的寬度 - 高度,您就能獲得需要在渲染中使用的顏色。
漫反射紋理可以在這裏獲得。
這是我期望你渲染出來的效果:
感謝原作者Dmitry V. Sokolov的授權,原文鏈接:https://github.com/ssloy/tinyrenderer/wiki/Lesson-3:-Hidden-faces-removal-(z-buffer)