筆者介紹:姜雪偉,IT公司技術合夥人,IT高級講師,CSDN社區專家,特邀編輯,暢銷書做者,國家專利發明人;已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。html
CSDN視頻網址:http://edu.csdn.net/lecturer/144算法
抗鋸齒問題在遊戲中一直存在的,尤爲是體如今3D模型上的材質或者遊戲UI界面上,因爲如今引擎都很是完善,而且引擎都提供了抗鋸齒功能,咱們經過引擎提供的參數界面設置一下就能夠消除。可是不少讀者並不明白爲什麼設置一下就能夠消除掉,或者根本沒有去研究,本篇博客給讀者揭祕這些技術原理。架構
鋸齒邊(Jagged Edge)出現的緣由是由頂點數據像素化以後成爲片斷的方式所引發的。下面是一個簡單的立方體,它體現了鋸齒邊的效果:函數
也許不是當即可見的,若是你更近的看看立方體的邊,你就會發現鋸齒了。若是咱們放大就會看到下面的情境:
post
這固然不是咱們在最終版本的應用裏想要的效果。這個效果,很明顯能看到邊是由像素所構成的,這種現象叫作走樣(Aliasing)。有不少技術可以減小走樣,產生更平滑的邊緣,這些技術叫作抗鋸齒技術(Anti-aliasing,也被稱爲反走樣技術)。性能
首先,咱們有一個叫作超級採樣抗鋸齒技術(Super Sample Anti-aliasing, SSAA),它暫時使用一個更高的解析度(以超級採樣方式)來渲染場景,當視頻輸出在幀緩衝中被更新時,解析度便降回原來的普通解析度。這個額外的解析度被用來防止鋸齒邊。雖然它確實爲咱們提供了一種解決走樣問題的方案,但卻因爲必須繪製比平時更多的片斷而下降了性能。因此這個技術只流行了一段時間。測試
這個技術的基礎上誕生了更爲現代的技術,叫作多采樣抗鋸齒(Multisample Anti-aliasing)或叫MSAA,雖然它借用了SSAA的理念,但卻以更加高效的方式實現了它。本篇博客咱們會展開討論這個MSAA技術,它是OpenGL內建的。ui
爲了理解什麼是多重採樣(Multisampling),以及它是如何解決鋸齒問題的,咱們先要更深刻了解一個OpenGL光柵化的工做方式。如今引擎都是跨平臺的,因此引擎的底層技術也是使用OpenGL實現的,咱們如今討論的技術也是 基於OpenGL的。spa
光柵化是你最終經處理的頂點和片斷着色器之間的全部算法和處理的集合。光柵化將屬於一個基本圖形的全部頂點轉化爲一系列片斷。頂點座標理論上能夠含有任何座標,但片斷卻不是這樣,這是由於它們與你的窗口的解析度有關。幾乎永遠都不會有頂點座標和片斷的一對一映射,因此光柵化必須以某種方式決定每一個特定頂點最終結束於哪一個片斷/屏幕座標上。.net
這裏咱們看到一個屏幕像素網格,每一個像素中心包含一個採樣點(sample point),它被用來決定一個像素是否被三角形所覆蓋。紅色的採樣點若是被三角形覆蓋,那麼就會爲這個被覆蓋像(屏幕)素生成一個片斷。即便三角形覆蓋了部分屏幕像素,可是採樣點沒被覆蓋,這個像素仍然不會受到任何片斷着色器影響到。
你可能已經明白走樣的緣由來自何處了。三角形渲染後的版本最後在你的屏幕上是這樣的:
因爲屏幕像素總量的限制,有些邊上的像素能被渲染出來,而有些則不會。結果就是咱們渲染出的基本圖形的非光滑邊緣產生了上圖的鋸齒邊。
多采樣所作的正是再也不使用單一採樣點來決定三角形的覆蓋範圍,而是採用多個採樣點。咱們再也不使用每一個像素中心的採樣點,取而代之的是4個子樣本(subsample),用它們來決定像素的覆蓋率。這意味着顏色緩衝的大小也因爲每一個像素的子樣本的增長而增長了。
左側的圖顯示了咱們普通決定一個三角形的覆蓋範圍的方式。這個像素並不會運行一個片斷着色器(這就仍保持空白),由於它的採樣點沒有被三角形所覆蓋。右邊的圖展現了多采樣的版本,每一個像素包含4個採樣點。這裏咱們能夠看到只有2個採樣點被三角形覆蓋。
採樣點的數量是任意的,更多的採樣點能帶來更精確的覆蓋率。
多采樣開始變得有趣了。2個子樣本被三角覆蓋,下一步是決定這個像素的顏色。咱們原來猜想,咱們會爲每一個被覆蓋的子樣本運行片斷着色器,而後對每一個像素的子樣本的顏色進行平均化。例子的那種狀況,咱們在插值的頂點數據的每一個子樣本上運行片斷着色器,而後將這些採樣點的最終顏色儲存起來。幸虧,它不是這麼運做的,由於這等於說咱們必須運行更多的片斷着色器,會明顯下降性能。
MSAA的真正工做方式是,每一個像素只運行一次片斷着色器,不管多少子樣本被三角形所覆蓋。片斷着色器運行着插值到像素中心的頂點數據,最後顏色被儲存近每一個被覆蓋的子樣本中,每一個像素的全部顏色接着將平均化,每一個像素最終有了一個惟一顏色。在前面的圖片中4個樣本中只有2個被覆蓋,像素的顏色將以三角形的顏色進行平均化,顏色同時也被儲存到其餘2個採樣點,最後生成的是一種淺藍色。
結果是,顏色緩衝中全部基本圖形的邊都生成了更加平滑的樣式。讓咱們看看當再次決定前面的三角形覆蓋範圍時多樣本看起來是這樣的:
這裏每一個像素包含着4個子樣本(不相關的已被隱藏)藍色的子樣本是被三角形覆蓋了的,灰色的沒有被覆蓋。三角形內部區域中的全部像素都會運行一次片斷着色器,它輸出的顏色被儲存到全部4個子樣本中。三角形的邊緣並非全部的子樣本都會被覆蓋,因此片斷着色器的結果僅儲存在部分子樣本中。根據被覆蓋子樣本的數量,最終的像素顏色由三角形顏色和其餘子樣本所儲存的顏色所決定。
大體上來講,若是更多的採樣點被覆蓋,那麼像素的顏色就會更接近於三角形。若是咱們用早期使用的三角形的顏色填充像素,咱們會得到這樣的結果:
對於每一個像素來講,被三角形覆蓋的子樣本越少,像素受到三角形的顏色的影響也越少。如今三角形的硬邊被比實際顏色淺一些的顏色所包圍,所以觀察者從遠處看上去就比較平滑了。
不只顏色值被多采樣影響,深度和模板測試也一樣使用了多采樣點。好比深度測試,頂點的深度值在運行深度測試前被插值到每一個子樣本中,對於模板測試,咱們爲每一個子樣本儲存模板值,而不是每一個像素。這意味着深度和模板緩衝的大小隨着像素子樣本的增長也增長了。
到目前爲止咱們所討論的不過是多采樣發走樣工做的方式。光柵化背後實際的邏輯要比咱們討論的複雜,但你如今能夠理解多采樣抗鋸齒背後的概念和邏輯了。
若是讀者打算在OpenGL中使用MSAA,那麼咱們必須使用一個能夠爲每一個像素儲存一個以上的顏色值的顏色緩衝(由於多采樣須要咱們爲每一個採樣點儲存一個顏色)。咱們這就須要一個新的緩衝類型,它能夠儲存要求數量的多重採樣樣本,它叫作多樣本緩衝(Multisample Buffer)。
多數窗口系統能夠爲咱們提供一個多樣本緩衝,以代替默認的顏色緩衝。GLFW一樣給了咱們這個功能,咱們所要做的就是提示GLFW,咱們但願使用一個帶有N個樣本的多樣本緩衝,而不是普通的顏色緩衝,這要在建立窗口前調用glfwWindowHint
來完成:
- glfwWindowHint(GLFW_SAMPLES, 4);
當咱們如今調用glfwCreateWindow
,用於渲染的窗口就被建立了,此次每一個屏幕座標使用一個包含4個子樣本的顏色緩衝。這意味着全部緩衝的大小都增加4倍。
如今咱們請求GLFW提供了多樣本緩衝,咱們還要調用glEnable
來開啓多采樣,參數是 GL_MULTISAMPLE
。大多數OpenGL驅動,多采樣默認是開啓的,因此這個調用有點多餘,但一般記得開啓它是個好主意。這樣全部OpenGL實現的多采樣都開啓了。
- glEnable(GL_MULTISAMPLE);
當默認幀緩衝有了多采樣緩衝附件的時候,咱們所要作的所有就是調用 glEnable
開啓多采樣。由於實際的多采樣算法在OpenGL驅動光柵化裏已經實現了,因此咱們無需再作什麼了。若是咱們如今來渲染教程開頭的那個綠色立方體,咱們會看到邊緣變得平滑了:
這個箱子看起來平滑多了,在場景中繪製任何物體均可以利用這個技術,核心代碼以下所示:
- glViewport(0, 0, screenWidth, screenHeight);
-
- // Setup OpenGL options
- glEnable(GL_MULTISAMPLE); // Enabled by default on some drivers, but not all so always enable to make sure
- glEnable(GL_DEPTH_TEST);
由於GLFW負責建立多采樣緩衝,開啓MSAA很是簡單。若是咱們打算使用咱們本身的幀緩衝,來進行離屏渲染,那麼咱們就必須本身生成多采樣緩衝了;如今咱們須要本身負責建立多采樣緩衝。
有兩種方式能夠建立多采樣緩衝,並使其成爲幀緩衝的附件:紋理附件和渲染緩衝附件。
爲了建立一個支持儲存多采樣點的紋理,咱們使用 glTexImage2DMultisample
來替代 glTexImage2D
,它的紋理目標是GL_TEXTURE_2D_MULTISAMPLE
:
- glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, tex);
- glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, width, height, GL_TRUE);
- glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
第二個參數如今設置了咱們打算讓紋理擁有的樣本數。若是最後一個參數等於 GL_TRUE
,圖像上的每個紋理像素(texel)將會使用相同的樣本位置,以及一樣的子樣本數量。
爲將多采樣紋理附加到幀緩衝上,咱們使用glFramebufferTexture2D
,不過此次紋理類型是GL_TEXTURE_2D_MULTISAMPLE
:
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, tex, 0);
當前綁定的幀緩衝如今有了一個紋理圖像形式的多采樣顏色緩衝。
和紋理同樣,建立一個多采樣渲染緩衝對象(Multisampled Renderbuffer Objects)不難。並且還很簡單,由於咱們所要作的所有就是當咱們指定渲染緩衝的內存的時候將glRenderbuffeStorage
改成glRenderbufferStorageMuiltisample
:
- glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height);
有同樣東西在這裏有變化,就是緩衝目標後面那個額外的參數,咱們將其設置爲樣本數量,當前的例子中應該是4.
渲染到多采樣幀緩衝對象是自動的。當咱們繪製任何東西時,幀緩衝對象就綁定了,光柵化會對負責全部多采樣操做。咱們接着獲得了一個多采樣顏色緩衝,以及深度和模板緩衝。由於多采樣緩衝有點特別,咱們不能爲其餘操做直接使用它們的緩衝圖像,好比在着色器中進行採樣。
一個多采樣圖像包含了比普通圖像更多的信息,因此咱們須要作的是壓縮或還原圖像。還原一個多采樣幀緩衝,一般用glBlitFramebuffer
來完成,它從一個幀緩衝中複製一個區域粘貼另外一個裏面,同時也將任何多采樣緩衝還原。
glBlitFramebuffer
把一個4屏幕座標源區域傳遞到一個也是4空間座標的目標區域。你可能還記得幀緩衝教程中,若是咱們綁定到GL_FRAMEBUFFER
,咱們實際上就同時綁定到了讀和寫的幀緩衝目標。咱們還能夠經過GL_READ_FRAMEBUFFER
和GL_DRAW_FRAMEBUFFER
綁定到各自的目標上。glBlitFramebuffer
函數從這兩個目標讀取,並決定哪個是源哪個是目標幀緩衝。接着咱們就能夠經過把圖像位塊傳送(Blitting)到默認幀緩衝裏,將多采樣幀緩衝輸出傳遞到實際的屏幕了:
- glBindFramebuffer(GL_READ_FRAMEBUFFER, multisampledFBO);
- glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
- glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
若是咱們渲染應用,咱們將獲得和沒用幀緩衝同樣的結果:一個綠色立方體,它使用MSAA顯示出來,但邊緣鋸齒明顯少了:
核心源代碼以下所示:
- // Setup OpenGL options
- glEnable(GL_MULTISAMPLE); // Enabled by default on some drivers, but not all so always enable to make sure
- glEnable(GL_DEPTH_TEST);
- <span style="color:#e0e2e4;"><span style="rgb(40, 43, 46);"></span></span><pre code_snippet_id="2217685" snippet_file_name="blog_20170220_9_6895735" name="code" class="html">//</pre><span style="color:#222222;">// Setup cube VAO GLuint cubeVAO, cubeVBO; glGenVertexArrays(1, &cubeVAO); glGenBuffers(1, &cubeVBO); glBindVertexArray(cubeVAO); glBindBuffer(GL_ARRAY_BUFFER, cubeVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), &cubeVertices, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); glBindVertexArray(0); #pragma endregion // Framebuffers GLuint framebuffer; glGenFramebuffers(1, &framebuffer); glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); // Create a multisampled color attachment texture GLuint textureColorBufferMultiSampled = <strong>generateMultiSampleTexture</strong>(4); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, textureColorBufferMultiSampled, 0); // Create a renderbuffer object for depth and stencil attachments GLuint rbo; glGenRenderbuffers(1, &rbo); glBindRenderbuffer(GL_RENDERBUFFER, rbo); glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, screenWidth, screenHeight); glBindRenderbuffer(GL_RENDERBUFFER, 0); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo); if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl; glBindFramebuffer(GL_FRAMEBUFFER, 0);</span>
其中上文中調用的函數實現以下所示:
- GLuint generateMultiSampleTexture(GLuint samples)
- {
- GLuint texture;
- glGenTextures(1, &texture);
-
- glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
- glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, screenWidth, screenHeight, GL_TRUE);
- glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
-
- return texture;
- }
可是若是咱們打算使用一個多采樣幀緩衝的紋理結果來作這件事,就像後處理同樣會怎樣?咱們不能在片斷着色器中直接使用多采樣紋理。咱們能夠作的事情是把多緩衝位塊傳送(Blit)到另外一個帶有非多采樣紋理附件的FBO中。以後咱們使用這個普通的顏色附件紋理進行後處理,經過多采樣來對一個圖像渲染進行後處理效率很高。這意味着咱們必須生成一個新的FBO,它僅做爲一個將多采樣緩衝還原爲一個咱們能夠在片斷着色器中使用的普通2D紋理中介。僞代碼是這樣的:
- GLuint msFBO = CreateFBOWithMultiSampledAttachments();
- // Then create another FBO with a normal texture color attachment
- ...
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
- ...
- while(!glfwWindowShouldClose(window))
- {
- ...
-
- glBindFramebuffer(msFBO);
- ClearFrameBuffer();
- DrawScene();
- // Now resolve multisampled buffer(s) into intermediate FBO
- glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);
- glBindFramebuffer(GL_DRAW_FRAMEBUFFER, intermediateFBO);
- glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
- // Now scene is stored as 2D texture image, so use that image for post-processing
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
- ClearFramebuffer();
- glBindTexture(GL_TEXTURE_2D, screenTexture);
- DrawPostProcessingQuad();
-
- ...
- }
若是咱們實現幀緩衝教程中講的後處理代碼,咱們就能創造出沒有鋸齒邊的全部效果很酷的後處理特效。使用模糊kernel過濾器,看起來會像這樣:
核心源代碼以下所示:
- // Setup cube VAO
- GLuint cubeVAO, cubeVBO;
- glGenVertexArrays(1, &cubeVAO);
- glGenBuffers(1, &cubeVBO);
- glBindVertexArray(cubeVAO);
- glBindBuffer(GL_ARRAY_BUFFER, cubeVBO);
- glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), &cubeVertices, GL_STATIC_DRAW);
- glEnableVertexAttribArray(0);
- glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
- glBindVertexArray(0);
- // Setup screen VAO
- GLuint quadVAO, quadVBO;
- glGenVertexArrays(1, &quadVAO);
- glGenBuffers(1, &quadVBO);
- glBindVertexArray(quadVAO);
- glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
- glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
- glEnableVertexAttribArray(0);
- glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
- glEnableVertexAttribArray(1);
- glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)(2 * sizeof(GLfloat)));
- glBindVertexArray(0);
- #pragma endregion
-
-
- // Framebuffers
- GLuint framebuffer;
- glGenFramebuffers(1, &framebuffer);
- glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
- // Create a multisampled color attachment texture
- GLuint textureColorBufferMultiSampled = generateMultiSampleTexture(4);
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D_MULTISAMPLE, textureColorBufferMultiSampled, 0);
- // Create a renderbuffer object for depth and stencil attachments
- GLuint rbo;
- glGenRenderbuffers(1, &rbo);
- glBindRenderbuffer(GL_RENDERBUFFER, rbo);
- glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, screenWidth, screenHeight);
- glBindRenderbuffer(GL_RENDERBUFFER, 0);
- glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
-
- if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
- cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
-
- // second framebuffer
- GLuint intermediateFBO;
- GLuint screenTexture = generateAttachmentTexture(false, false);
- glGenFramebuffers(1, &intermediateFBO);
- glBindFramebuffer(GL_FRAMEBUFFER, intermediateFBO);
- glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0); // We only need a color buffer
-
- if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
- cout << "ERROR::FRAMEBUFFER:: Intermediate framebuffer is not complete!" << endl;
- glBindFramebuffer(GL_FRAMEBUFFER, 0);
其中調用的函數代碼以下所示:
- GLuint generateMultiSampleTexture(GLuint samples)
- {
- GLuint texture;
- glGenTextures(1, &texture);
-
- glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, texture);
- glTexImage2DMultisample(GL_TEXTURE_2D_MULTISAMPLE, samples, GL_RGB, screenWidth, screenHeight, GL_TRUE);
- glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, 0);
-
- return texture;
- }
-
- // Generates a texture that is suited for attachments to a framebuffer
- GLuint generateAttachmentTexture(GLboolean depth, GLboolean stencil)
- {
- // What enum to use?
- GLenum attachment_type;
- if(!depth && !stencil)
- attachment_type = GL_RGB;
- else if(depth && !stencil)
- attachment_type = GL_DEPTH_COMPONENT;
- else if(!depth && stencil)
- attachment_type = GL_STENCIL_INDEX;
-
- //Generate texture ID and load texture data
- GLuint textureID;
- glGenTextures(1, &textureID);
- glBindTexture(GL_TEXTURE_2D, textureID);
- if(!depth && !stencil)
- glTexImage2D(GL_TEXTURE_2D, 0, attachment_type, screenWidth, screenHeight, 0, attachment_type, GL_UNSIGNED_BYTE, NULL);
- else // Using both a stencil and depth test, needs special format arguments
- glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, screenWidth, screenHeight, 0, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- glBindTexture(GL_TEXTURE_2D, 0);
-
- return textureID;
- }
當咱們但願將多采樣和離屏渲染結合起來時,咱們須要本身負責一些細節。全部細節都是值得付出這些額外努力的,由於多采樣能夠明顯提高場景視頻輸出的質量。要注意,開啓多采樣會明顯下降性能,樣本越多越明顯。