1. 程式人生 > >OpenGL學習腳印: 反走樣初步(Anti-aliasing basic)

OpenGL學習腳印: 反走樣初步(Anti-aliasing basic)

寫在前面
目前,我們繪製的圖形中存在瑕疵的,觀察下面這個立方體:

走樣的立方體
仔細看,立方體的邊緣部分存在折線,如果我們放大了看,則可以看到這種瑕疵更明顯:

邊緣走樣

這種繪製的物體邊緣部分出現鋸齒的現象稱之為走樣(aliasing)。反走樣(Anti-aliasing)是減輕這種現象的方法。反走樣本身也是一個比較複雜的主題,深入瞭解需要有訊號處理中的背景知識,例如訊號取樣、訊號重構、濾波等知識,本節作為一個初步探討,我們不深入反走樣演算法的細節。主要以OpenGL中實現的MSAA(Multi-Sampled Anti-Aliasing)為重點,介紹在OpenGL中反走樣的處理。本節示例程式碼均可以

在我的github下載

通過本節可以瞭解到:

  • 走樣和反走樣的基本概念
  • 多采樣的反走樣方法
  • 使用多采樣的FBO
  • 使用多采樣FBO的紋理

走樣和反走樣

我們要繪製的圖形在理論上是連續的,例如直線,但是螢幕顯示裝置卻是由畫素組成的離散形式,當在二維螢幕上顯示連續的物件時,總是會出現失真現象,這稱之為走樣(aliasing)。用於減輕或者消除這種圖形失真現象的技術稱之為反走樣(Anti-aliasing)。例如下面的直線(圖中a,來自Rasterization lecture),當採用某種直線繪製演算法顯示到螢幕裝置上時就產生了走樣現象(圖中b):

line-jaggy

要比較好地理解這個現象,我們有必要了解下一些基礎知識。

畫素和解析度

畫素(pixel, 是picture element的簡稱)是圖形的最小表示單位,一個圖形的大小通常用包含的畫素個數來表示,如下圖所示:

pixel

畫素所在的座標系是螢幕座標系,通常用畫素所在的行列來表示,例如下圖(來自Pixel Coordinates):

pixel-coordinates

上圖中左右都是12x8大小的畫素塊,左邊一個的行從上到下計算,右邊一個的行從下往上計算,圖中位置(3,5)所表示的畫素有區別。畫素一般在示例時用點或者方塊表示說明問題,實際上畫素既不是點,也不是方塊,它是不可見的沒有區域大小的取樣點。(可參考Sampling, Aliasing, & Mipmaps)。畫素的值,表示的是亮度或者顏色的強度,例如常見的8位顏色影象,其中每個畫素包含RGB顏色。

解析度(resolution)是顯式裝置的引數,一般用包含的總畫素個數說明,用列數(對應顯示器寬)x行數(對應顯示器高)來說明,例如1024 x768,這裡寬高比(aspect ratio)為4:3。

光柵化(raterization)

光柵化是將輸入的圖元轉換為螢幕座標對齊的片元(fragment)的過程。我們看下圖的渲染管線(來自3D Graphics with OpenGLBasic Theory):
渲染管線

這裡首先處理的是輸入的頂點以及由gl命令指定繪製的圖元型別(點、直線、三角形等),這一階段由頂點著色器完成,頂點著色器輸出經過座標變換後的頂點。下一階段光柵處理,運用光柵掃描轉換演算法(scan-conversion)將輸入的圖元轉換為片元(fragment)。下一階段,片元著色器處理輸入的片元,決定了最終螢幕上每個畫素的顏色。這裡需要注意的是,片元一般也可以對應畫素,但是當多采樣時一個畫素的顏色可能是多個片元共同決定的。畫素是二維的(x,y),片元卻是三維的(x,y,z),其中z表示深度座標,用於進行深度測試。

在這個流水線中,光柵化的過程就是處理輸入的圖元,並最終決定產生哪些片元的過程,如下圖所示(來自Graphics and Rendering):

光柵掃描

有了上面的基礎後,我們可以明白,當把一條連續的直線,通過光柵掃描演算法(例如Bresenham演算法,具體可以參考直線段的掃描轉換演算法)轉換為片元時,由於直線時連續的,而片元的座標對齊螢幕上畫素,取樣整數座標,不得不對浮點數座標取整,導致掃描轉換後確定的片元和原先要表達的直線存在差距,這種差距引起了圖形的走樣。

通過上面,我們對走樣有了一個基本概念,當然這只是直觀上的理解,如果需要深入瞭解,還需要從訊號處理的角度看問題,在此不再深入,如果感興趣,可以自行參考Antialiasing

下面熟悉兩種反走樣的方法。

SuperSampling

超級取樣反走樣方法(Super-Sampled Anti-Aliasing ,SSAA),是通過以更高的解析度來取樣圖形,然後再顯示在低解析度的裝置上,從而減少失真的方法。例如下面的圖形表示了增加解析度後,繪製直線的差別:

增加解析度

通過將高解析度的影象,顯示在低解析度的裝置上,確實能有效減輕走樣現象,但是存在的弊端就是,要為這些多出來的畫素,進行更多的計算,以及記憶體開銷很大。這是一種比較傳統的方法。

MultiSampling

多采樣技術(Multi-Sampled Anti-Aliasing, MSAA)是對SSAA的改進,改進之處在於執行片元著色器的次數並沒有明顯增加,對邊緣部分卻進行了很好的反走樣。多采樣相對於單取樣,單取樣在一個畫素上,以畫素中心為標準,當光柵化時,如果圖元覆蓋了這個中心點,那麼就產生這個畫素對應的片元,否則不影響這個畫素;而多采樣,則在每個畫素上進行細分,分出更多的子取樣點(sub-sample),如下圖所示(來自Anti-Aliasing):

多采樣

當圖元的一部分覆蓋了畫素中的子取樣點時,則會執行片元著色器,片元著色器的執行不是以子取樣點為單位,也就是說不管有多個子取樣點,對於每個圖元,這個畫素只執行一次片元著色器。當顏色緩衝中這些子取樣點,填充了所有繪製的圖元的顏色時,將取平均值計算最終這個畫素的顏色,這個顏色將是唯一值。

例如上面的圖中,三角形圖元覆蓋了2個取樣點,那麼這個畫素的最終顏色由三角形覆蓋的2個取樣點的顏色和另外兩個取樣點的顏色(例如這個顏色可能是glClearColor指定的顏色)混合後的均值決定。

下面的圖是單取樣對應的光柵化過程(來自Anti-Aliasing):

單取樣

通過多采樣,繪製的三角形的邊緣部分,因為有了和背景顏色的混合,而減輕了走樣現象,如下圖所示(來自Anti-Aliasing):

多采樣效果

使用多采樣方法時,由於每個畫素包含更多的取樣點,而這些取樣也需要儲存顏色、深度值,以及進行模板測試,因而需要更大的顏色緩衝區、深度緩衝區和模板緩衝區。

上面直觀上介紹了MSAA方法,實現細節沒有探討,感興趣地可以自行參考演算法實現。在OpenGL中內建了多采樣反走樣方法,因此下面我們將介紹在OpenGL中的使用。

啟用多采樣

要啟用多采樣,我們需要建立一個支援多采樣的緩衝區(multisample buffer),一般由視窗系統為我們生成。我們這裡使用GLFW的windowHint來讓GLFW庫幫我們生成這個多采樣緩衝區:

  glfwWindowHint(GLFW_SAMPLES, 4);

上面第二個引數告訴GLFW建立的多采樣中每個畫素包含的子取樣點個數,一般填寫4就好。如果填寫太大,將影響圖形繪製效能。注意,引數不要寫成了:

   glfwWindowHint(GL_SAMPLES, 4); // 應該是GLFW引數,錯誤

同時需要確保開啟了OpenGL多采樣,這個引數預設是開啟的:

   glEnable(GL_MULTISAMPLE);  

通過開啟多采樣,我們繪製的立方體效果如下:

反走樣的立方體

你可以通過O和F開啟和關閉反走樣,對比效果。
放大這個反走樣的立方體,我們發現邊緣部分的鋸齒有了明顯改善:

反走樣的邊緣

使用多采樣的FBO離屏反走樣

上面開啟了全域性的多采樣,如果需要為部分內容開啟多采樣,可以使用FBO實現離屏渲染的反走樣圖形,然後將這個離屏渲染的圖形賦值到預設的FBO中。在前面FBO一節已經介紹了FBO的基本使用,如果對這個概念不熟悉,可以回過頭去檢視。當使用多采樣FBO時需要做些調整。

建立多采樣的紋理附加物件

這裡建立一個附加FBO的textures時,附加的紋理不再是GL_TEXTURE_2D,而是GL_TEXTURE_2D_MULTISAMPLE:

  static GLuint makeMAAttachmentTexture(GLint samplesNum = 4, GLint internalFormat = GL_RGB,
        GLsizei width = 800, GLsizei height = 600)
{
GLuint textId;
glGenTextures(1, &textId);
// 注意修改target為GL_TEXTURE_2D_MULTISAMPLE
glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, textId); 
glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samplesNum, internalFormat,width, height, GL_TRUE); 
    glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
return textId;
}

建立多采樣的RBO

與之前建立RBO不同之處在於預分配記憶體時,需要指定多采樣的子取樣點個數,是下面的第二個引數:

glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, 
   GL_DEPTH24_STENCIL8,WINDOW_WIDTH, WINDOW_HEIGHT); // 預分配記憶體

建立完了附加紋理和RBO後,我們就構建了一個狀態合法的FBO了,完整建立過程如下:

   bool prepareMSFBO(const int samplesCnt, GLuint& colorTextId, GLuint& fboId)
{
    glGenFramebuffers(1, &fboId);
    glBindFramebuffer(GL_FRAMEBUFFER, fboId);
    // 附加 color attachment
    colorTextId = TextureHelper::makeMAAttachmentTexture(samplesCnt, GL_RGB); // 建立FBO中的多采樣紋理
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, colorTextId, 0);
    // 附加 depth stencil RBO attachment
    GLuint rboId;
    glGenRenderbuffers(1, &rboId);
    glBindRenderbuffer(GL_RENDERBUFFER, rboId);
    glRenderbufferStorageMultisample(GL_RENDERBUFFER, samplesCnt, GL_DEPTH24_STENCIL8,
        WINDOW_WIDTH, WINDOW_HEIGHT); // 預分配記憶體
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboId);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    {
        return false;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    return true;
}

有了合法的FBO後,我們分兩個階段繪製場景:

    // 第一遍 繪製到多采樣的FBO中
    glBindFramebuffer(GL_FRAMEBUFFER, MSFBOId);
    glClearColor(0.18f, 0.04f, 0.14f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    // 省略繪製場景程式碼
    // 第二遍 將多采樣的FBO紋理複製到預設FBO
        glBindFramebuffer(GL_READ_FRAMEBUFFER, MSFBOId);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);  // 這裡0表示使用預設的FBO
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 
        0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);

注意第二遍,複製多采樣的FBO到預設FBO時,我們使用了函式glBlitFramebuffer,這個函式從讀buffer拷貝內容到寫buffer,具體引數如下:

API void glBlitFramebuffer( GLint srcX0,
GLint srcY0,
GLint srcX1,
GLint srcY1,
GLint dstX0,
GLint dstY0,
GLint dstX1,
GLint dstY1,
GLbitfield mask,
GLenum filter);
1.srcX0, srcY0, srcX1, srcY1指定了讀取buffer的區域.
2.dstX0, dstY0, dstX1, dstY1指定了寫入buffer的區域
3.mask指定了從讀buffer複製到寫buffer的內容,允許的值為L_COLOR_BUFFER_BIT,GL_DEPTH_BUFFER_BIT 和GL_STENCIL_BUFFER_BIT.
4.當圖形被拉伸時的插值選項,允許值為GL_NEAREST or GL_LINEAR.

在使用這個函式之前,我們需要將多采樣紋理繫結到讀buffer,而預設的FBO置為寫buffer。經過兩個步驟的繪製,最終得到和上面使用MSAA效果相同的結果,但是這裡我們是執行的離屏渲染,而不是全域性採用反走樣。

多采樣FBO紋理的後處理

如果需要使用上述多采樣的FBO的紋理做一些後處理效果(postProcessing),直接使用這個多采樣的紋理是不行的,因為他和預設的FBO的子取樣點個數不相同,無法直接在著色器中取樣。這裡的技巧是,藉助一箇中間FBO,分三次繪製:

1.渲染到多采樣FBO。
2.多采樣FBO渲染到中間FBO,這個紋理可供我們正常在著色器中取樣了。
3.在著色器中取樣中間FBO的紋理,渲染到一個矩形上,作為場景最終效果。

首先,我麼準備多采樣FBO和一個只包含顏色附加的中間FBO;然後分三次繪製場景,流程如下:

   // 建立多采樣的FBO
   GLuint MSTextId, MSFBOId;
   prepareMSFBO(SAMPLES_COUNT, MSTextId, MSFBOId);
   // 建立一箇中間FBO
   GLuint screenTextId, intermediateFBOId;
   prepareIntermediateFBO(screenTextId, intermediateFBOId);
   // 第一遍 繪製到多采樣的FBO中
  glBindFramebuffer(GL_FRAMEBUFFER, MSFBOId);
  // 第二遍 將多采樣的FBO紋理複製到中間FBO
        glBindFramebuffer(GL_READ_FRAMEBUFFER, MSFBOId);
        glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBOId);
glBlitFramebuffer(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, 
            0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
// 第三遍 利用中間FBO的紋理 繪製到預設FBO中
glBindTexture(GL_TEXTURE_2D, screenTextId);
glDrawArrays(GL_TRIANGLES, 0, 6);

使用多采樣FBO的紋理,我們經過模糊效果處理的立方體,如下圖所示:

多采樣FBO紋理的使用

最後的說明

本節初步學習了反走樣這個主題,熟悉了OpenGL中使用MSAA方法處理反走樣。反走樣本身是一個比較複雜的主題,存在眾多的反走樣方法,而且涉及到訊號處理的背景知識,要學習好這個主題需要付出一定的努力。有需要深入瞭解的可以檢視A Quick Overview of MSAA,以及東南大學反走樣課件