1. 程式人生 > >【圖形學與遊戲程式設計】開發筆記-基礎篇6:緩衝區與混合

【圖形學與遊戲程式設計】開發筆記-基礎篇6:緩衝區與混合

(本系列文章由pancy12138編寫,轉載請註明出處:http://blog.csdn.net/pancy12138)

上一次的教程大家瞭解了最簡單的著色方法,那麼這一節我們來講解一些與著色無關的渲染管線流程。雖然在上一次的教程中我們成功的展示瞭如何給一個球體的表面進行光照著色。但是並不是說我們以往所有的知識就足以完美的展示出一個3D場景了。之所以我們能夠看到那個球體而沒有產生任何不適的感覺,主要是因為我們不知不覺的已經用到了一些緩衝區的知識。當然,這在基礎篇一開始的時候就有涉及了,只是當初我們並沒有過多的去關注這些知識。那麼今天我們就要來仔細的分析緩衝區這一在圖形管線中極為重要的演算法。

首先,神馬是緩衝區,這個名詞大家應該不會陌生,所謂緩衝區就是一個和螢幕大小相當的陣列。這在我們入門篇講解雙緩衝抗閃屏的時候就已經講到了。當初我們在講解交換鏈的時候第一次提及到了後臺(顏色)緩衝區。那麼事實上,為了保證繪製過程的完美,整個繪製過程中我們需要很多緩衝區來實現各種各樣的演算法。那麼今天我們要講的緩衝區包括:深度緩衝區,模板緩衝區,顏色緩衝區。然後,還有一個配合顏色緩衝區的alpha混合。注意,這裡所有的緩衝區都只是一個和螢幕一樣大的陣列,所以他們都是工作於光柵化之後的,並且他們只會對每個螢幕畫素保留一個值。使用的時候也是如此。大家不要習慣性的認為這些緩衝區能夠存貯整個3D場景的所有資訊。

首先是深度緩衝區,這個緩衝區的作用非常的明顯,就是為了判斷物體之間的“遮擋”關係。比如說,當你把一個蘋果放在一個桌子前面的時候,蘋果就會把桌子的一部分擋住。但是我們之前講的3D->2D的投影操作以及向量->光柵的光柵化操作都不能得到這個效果,這就會在之後得到一些錯誤的影象,比如如果先畫蘋果後畫桌子,就會發現桌子把蘋果給覆蓋了,這很明顯和我們當初的想法是不符的。於是,無論是directx還是opegl都預設提供了一個深度緩衝區,這個緩衝區將會在光柵化完畢之後對每一個點進行一次“深度測試”,看看這個點究竟是不是離攝像機最近的點,如果是,那麼久蓋住之前的點,如果不是,那麼就不允許它繪製。用最簡單的程式語言來描述就是:

float depth_buffer[wind_width][wind_height];
void clear_buffer
{
     for(int i = 0; i < wind_width; ++i)
     {
          for(int j = 0; j < wind_height; ++j)
         {
             depth_buffer[i][j] = 1.0f;
         }
     }
}
void depth_check(float depth,int width,int height)
{
     if(depth < depth_buffer[width][height])
    {
        draw_point(width,height);
        depth_buffer[width][height] = depth;
    }
}

也就是跟大家平常寫的找一個數組裡的最大值差不多。每一幀開始的時候把緩衝區的每個畫素都清空成最遠(注意這裡使用投影之後的點的z座標作為距離攝像機的距離,所以最遠是1.0,最近是0.0)。然後,每次繪製物體的時候就呼叫檢驗函式來進行深度測試。當然深度測試函式肯定不能像上面我寫的那樣,在圖形庫裡面基本上都是呼叫的GPU加速的演算法,並且屬於內建演算法之一,執行速度是非常快的。這裡也許有些人會有一些疑問,為什麼物體距離攝像機的深度可以用它的z座標來表示?如果換了觀察視角怎麼辦。事實上這個問題我們在之前的教程中已經講解過了。無論是directx還是opengl都是不允許你更改觀察視角的,也就是說那個視角其實是固定的。如果我們需要觀察視角變更的話,只要把物體挪動就好了。也就是說你想看一個物體的背面,並不需要走到他後面去,只需要把他轉一下把背面露給你就好了。深度緩衝區的用法事實上是很簡單的。directx裡面對於深度緩衝區是使用ID3D11depthstencilview來進行管理和使用的,而由於深度緩衝區的本質是一個二維圖片陣列。而二維圖片陣列本身是屬於“紋理”範疇的。所以,他的基本資源是一個texture2D資源,而管理器資源是ID3D11depthstencilview。大笑講到這裡大家估計會比較暈。博主你到底在說啥呢?這裡我稍微提及一下directx的資源管理方案:

首先,我們要知道,所有的大型資源(幾何體,紋理圖片,緩衝區等等)都必須儲存在視訊記憶體當中。但是我們控制繪製呼叫是用的CPU來呼叫的。因此,為了避免CPU和GPU的資料交換,directx為每一種不同的資源提供了不同的訪問方法,比如說shaderresourceview(SRV)用於訪問紋理,unorderdaccessview(UAV)用於訪問GPU可讀寫資源,depthstencilview(DSV)用於訪問深度模板緩衝區資源,這些資源訪問類我們可以理解成定義在cpu的指標,指向GPU的指標。當然這是一種更高階的訪問方式。而這些指標只擁有訪問資源的許可權,並不能建立資源。如果要建立資源的時候,我們對於不同的資源會有不同的類來進行,比如說Id3d11buffer,或者texture2D這些。這些類都是繼承了id3d11resource類,其功能就是建立以及修改視訊記憶體中儲存的各種大型資源。由於視訊記憶體和記憶體之間進行資料交換非常的緩慢,所以我們一般只有在程式初始化的時候用這些類來進行視訊記憶體的建立工作,一旦視訊記憶體建立完畢,我們就可以只使用前面的那些GPU訪問類來對視訊記憶體資源進行呼叫。這樣就可以讓程式變的非常的流暢。而openGL也是以類似的思想來進行資源管理的(opengl1.0除外),只不過opengl裡面並沒有所謂的訪問指標,建立指標等分得很細的東西,我們在cpu上用一個int變數就可以代表視訊記憶體上相應的資源,由於所有的操作都封裝在OpenGL的狀態機裡面,所有我們既可以用這個int進行視訊記憶體的建立,也可以用它進行視訊記憶體的訪問。這種方法的話有好處也有壞處,好處是使用起來簡單,壞處是資源交換以及繫結就不簡單以及不太好封裝起來。不過這都不是重點,大家程式寫多了自然就都習慣了,所以對於這種資源管理的知識,大家要多練習,這樣才能掌握的比較好。

D3D11_TEXTURE2D_DESC dsDesc;
	dsDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
	dsDesc.Width = wind_width;
	dsDesc.Height = wind_height;
	dsDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
	dsDesc.MipLevels = 1;
 	dsDesc.CPUAccessFlags = 0;
	dsDesc.MiscFlags = 0;
	dsDesc.Usage = D3D11_USAGE_DEFAULT;
	dsDesc.SampleDesc.Count = 4;
	dsDesc.SampleDesc.Quality = 0;

	ID3D11Texture2D* depthStencilBuffer;
	device_pancy->CreateTexture2D(&dsDesc, 0, &depthStencilBuffer);
	device_pancy->CreateDepthStencilView(depthStencilBuffer, 0, &depthStencilView);
	depthStencilBuffer->Release();
上述就是一個深度緩衝區的建立過程,這裡要注意的是深度模板緩衝區的msaa抗鋸齒倍數一定要和渲染目標,也就是後臺緩衝區的渲染倍數相同。不能出現比如後臺緩衝區4倍抗鋸齒,深度緩衝區8倍或者不開的這種情況。

這裡我們發現,深度緩衝區並不是一個單獨的緩衝區,他還附帶了一個模板緩衝區,一般一個32位的緩衝區會被分為兩部分,其中24位作為深度緩衝區,8位作為模板緩衝區。那麼神馬又是模板緩衝區呢,這個緩衝區又有什麼作用呢?接下來我們就要來講解這一部分的知識。
由於深度緩衝區在directx與opengl中屬於功能不可更改的緩衝區(只能工作於深度比較)。但是其比對的思想又非常的好用。所以我們有時候會希望達到類似的畫素測試效果,但是不希望用深度作為標準,比如說我們希望x座標小於1的畫素點不要顯示在螢幕上。那麼這個時候我們就需要一個自定義的緩衝區,這也就是模板緩衝區的作用,我們在使用模板緩衝區的時候,先要給緩衝區設定一個比較函式。然後再給模板緩衝區寫入一個用於比較的區域,然後再進行正式的渲染。這個時候就可以進行模板測試,凡是不能符合測試的畫素點都不允許通過。這樣幹說理論可能大家比較暈。我舉一個最簡單的例子,比如說鏡面效果,就是通過鏡子能夠看到物體的映象這一效果:


上圖就展示了模板緩衝區的使用方法,首先我們先把鏡子渲染一遍,藉助這一過程把鏡子所在的畫素點全部標記成可訪問的模式(白色區域),得到一張下面所示的模板緩衝區的資料。然後第二遍正式渲染的時候我們就可以開啟模板測試,只要最終渲染的畫素座標對應的模板顏色不是白色的(也就是不可訪問),那就不能顯示,最終就可以使得我們渲染的模型的影子只出現在鏡面所在的位置。

當然,這只是最簡單的一個模板緩衝區的使用方式,模板緩衝區的應用還是比較多的,最經典的比如說shadow volume演算法,這個會在之後講解全域性陰影的時候來進行講解。現在可以大概的瞭解模板緩衝區的作用就好了,因為這一緩衝區在大部分傳統演算法中還是很少用到的,等到真正用到的時候再仔細學習也不算遲。

最後我們來講一個比較常用的東西,後臺(顏色)緩衝區以及alpha混合。顏色緩衝區以及其對應的顏色測試演算法屬於最後一個測試演算法。這個測試演算法主要是用來遮蔽一些顏色輸出以及進行最終的顏色處理工作。我們在一開始講解後臺緩衝區的時候只是提到他是為了防止閃屏而存在的。不過經過多年的優化,他也在進行影象累計的過程中同時做了很多的顏色處理工作。首先是顏色遮蔽。如果我們的程式需要最終渲染的顏色的某個通道(RGBA)被遮蔽掉的話就可以開啟這個顏色遮蔽,這樣就可以達到一些比較特殊的效果,比如說我的部落格譯文裡面的毛髮渲染,就是在pass1遮蔽了RGB通道來記錄最終的alpha效果。除了直接遮蔽通道以外,我們還可以根據自己的想法來進行顏色剔除工作,這個也很簡單,就是在pixel shader裡面,使用clip()函式來裁剪掉我們不想要的畫素點。

然後是alpha混合,我們知道很多時候我們用於渲染的圖片並不是全部都是我們想要的,比如說一個角色的原畫,周圍的一些黑乎乎的東西我們就不希望他出現在渲染結果裡面:


比如說上面這張圖片,我們希望把角色顯示出來,但是不要顯示周圍的黑乎乎的背景。在ps中我們一般會選擇摳圖來解決。換算到shader裡面就是我們說的clip(color.r < 0.001f && color.g <=0.001f && color.b <=0.001f)這種類似的方式。但是這種硬生生的摳圖演算法會使得影象的邊緣非常的不平整,也就是不夠平滑。這是其一。其二就是對於一些“半透明”的物體,比如說水晶啊,水域啊這些的,我們希望透過它看到一些東西的時候就不能簡單地靠摳圖來進行解決。這個時候就需要alpha混合演算法來進行。所謂的alpha混合就是指在進行顏色合成的時候,我們新渲染的物體的顏色會和之前渲染的物體的顏色進行合成。合成的公式有很多,最簡單的就是a.rgb*a.a + b.rgb*(1-a.a)這樣得到一個新的顏色效果,這種做法即可以平滑的處理“摳圖”的效果,也可以很好的處理“透過a看到b”這一現象。alpha混合在很多演算法裡面都有涉及,其應用範圍可以說是非常的廣。不過目前來說我們基礎篇的程式尚且沒有用到這一功能。所以大家先了解這一概念就可以了。之後再在講解相關的渲染演算法的時候我們會經常提到這一概念。

ok,那麼這一次的教程到這裡就算是結束了。這一次的教程我們並沒有講解任何程式相關的東西,只是給大家普及了緩衝區以及alpha混合的概念。這些東西暫時還是用處比較小的,但是他們是確實存在於渲染管線當中,並且在以後的演算法設計過程中會體現出非常巨大的效果。所以大家可以先了解了解這些知識。這樣之後在瞭解更為複雜的演算法的時候就會更加的得心應手。