500行代碼實現軟件渲染器 - 3.隱藏面消隱(Z緩衝區)

引入

您好,我來介紹一下我的朋友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)

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章