【D3D11遊戲編程】學習筆記十八:模板緩衝區的使用、鏡子的實現

       (注:【D3D11遊戲編程】學習筆記系列由CSDN作者BonChoix所寫,轉載請註明出處:http://blog.csdn.net/BonChoix,謝謝~)

 

      模板緩衝區(Stencil Buffer)是一個與後緩衝區(Back Buffer)尺寸一樣的離屏緩衝區(Off-Screen Buffer),主要用於實現一些特效。模板緩衝區中的每一個像素Pi,j,與後緩衝區中的像素Pi,j是一一對應的。在功能上,與深度緩衝區類似,都是用來控制一個片段能否通過3D渲染管線相應的階段,以被進一步處理。不同之處在於,模板緩衝區與深度緩衝區用於控制片段是否通過所使用的判斷依據不一樣。對於深度緩衝區,它通過比較每個片段與當前緩衝區中對應像素處的深度值來判斷,如果小於該深度值則通過,否則丟棄該片段 ;模板緩衝區使用其他的判斷依據,我們稍後會詳細介紹。

       1. 模板緩衝區相關數據格式

       實際上,模板緩衝區與深度緩衝區是共用一個“物理緩衝區”的,即真正存在的只有一個緩衝區,該緩衝區中任一像素處保存了兩種信息:深度值與模板值。比如一個像素佔用4個字節(32位),那麼深度值可能佔用前面幾位,模板值佔用後面幾位。在D3D11中針對該緩衝區,定義瞭如下幾種數據格式:

       DXGI_FORMAT_D32_FLOAT_S8X24_UINT:該格式中,每個像素爲8字節(64位),其中深度值佔32位,爲float型。模板值爲8位,爲位於[0,255]中的整型,後面24位無任何用途,純對齊用;

       DXGI_FORMAT_D24_UNORM_S8_UINT:該格式中,每個像素爲4字節(32位),其中深度值佔24位,並映射到[0,1]之間。模板值爲8位,爲位於[0,255]中的整型;

       在大多數情況下,我們使用第二種格式,在我們前面所有的示例程序中,使用的正是這種格式。在初始化D3D時,我們需要在創建深度/模板緩衝區時爲它指定相應的格式,對應代碼如下(對應 dsDesc.Format部分):

	D3D11_TEXTURE2D_DESC dsDesc;
	dsDesc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;
	dsDesc.Width = m_clientWidth;
	dsDesc.Height = m_clientHeight;
	dsDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
	dsDesc.MipLevels = 1;
	dsDesc.ArraySize = 1;
	dsDesc.CPUAccessFlags = 0;
	dsDesc.SampleDesc.Count = g_x4MsaaQuality<1?1:4;
	dsDesc.SampleDesc.Quality = g_x4MsaaQuality<1?0:g_x4MsaaQuality-1;
	dsDesc.MiscFlags = 0;
	dsDesc.Usage = D3D11_USAGE_DEFAULT;
	hr = m_d3dDevice->CreateTexture2D(&dsDesc,0,&m_depthStencilBuffer);
	if(FAILED(hr))
	{
		MessageBox(NULL,_T("Create depth stencil buffer failed!"),_T("ERROR"),MB_OK);
		return false;
	}
	hr = m_d3dDevice->CreateDepthStencilView(m_depthStencilBuffer,0,&m_depthStencilView);
	if(FAILED(hr))
	{
		MessageBox(NULL,_T("Create depth stencil view failed!"),_T("ERROR"),MB_OK);
		return false;
	}
	m_deviceContext->OMSetRenderTargets(1,&m_renderTargetView,m_depthStencilView);

 

       2. 模板測試判斷依據

       正如深度緩衝區使用片段的深度值作爲判斷該片段是否通過的依據,模板緩衝區也有它獨特的判斷依據,如以下公式所示:

      

       該判斷主要包含兩部分:COMPARISON左邊部分和右邊部分。

       StencilRef爲程序員設定的一個參考值,StencilReadMask爲模板值讀取掩碼,與參考值“按位與”作爲式子中的左邊的結果;一般情況下,我們設定該掩碼爲0xFF,即接位與的結果就是模板參考值本身;

       Value爲模板緩衝區中對應位置處的當前值,同樣與掩碼按位與後作爲右邊的結果值;

       式子中左、右兩部分的結果通過中間的比較操作“COMPARISON”來決定決斷的結果,在D3D11中,比較操作定義爲如下枚舉類型:

typedef enum D3D11_COMPARISON_FUNC {
  D3D11_COMPARISON_NEVER           = 1,
  D3D11_COMPARISON_LESS            = 2,
  D3D11_COMPARISON_EQUAL           = 3,
  D3D11_COMPARISON_LESS_EQUAL      = 4,
  D3D11_COMPARISON_GREATER         = 5,
  D3D11_COMPARISON_NOT_EQUAL       = 6,
  D3D11_COMPARISON_GREATER_EQUAL   = 7,
  D3D11_COMPARISON_ALWAYS          = 8 
} D3D11_COMPARISON_FUNC;

       通過名字即很容易想到其意義,我們忽略前綴:

       NEVER:判斷操作永遠失敗,即片段全部不通過模板測試;

       LESS:該判斷爲“<"操作,即左邊<右邊時測試通過;

       EQUAL:“=”操作,即當左、右兩邊相等時測試通過;

       LESS_EQUAL:“<="操作,當左邊<=右邊時測試通過;

       GREATER:">"操作,當左邊>右邊時測試通過;

       NOT_EQUAL:"!="操作,當左、右兩邊不相等時測試通過;

       GREATER_EQUAL:">="操作,當左邊>=右邊時測試通過;

       ALWAYS:永遠通過,即不管左右兩邊的值,恆通過。

       這裏的枚舉類型同樣適應於深度緩衝區中比較操作的設定。

       舉個例子說明下模板測試的過程:

       比如我們設定模板參考值爲1,掩碼值爲0xffffffff,針對某個片段,如果模板緩衝區中對應的當前模板值爲0,則按上述公式,

       左邊 = 1 & 0xFF = 1;

       右邊 = 0 & 0xFF = 0;

       1. 如果比較操作我們設定爲ALWAYS,則該片段的模板測試通過(不管左右兩邊什麼值);

       2. 如果比較操作我們設定爲LESS,則由於1 < 0是錯誤的,因此該片段的模板測試失敗,片段被丟棄;

       3. 如果比較操作我們設定爲GREATER,由於1 > 0正確,因此模板測試成功,片段通過。

       其他比較操作依次類推,很簡單。

       3. 模板緩衝區的更新

       在上一步驟中的模板測試之後,不管片段是否通過測試,都要對模板緩衝區進行相應的更新。至於怎麼更新,取決於程序員的設定。D3D11中針對模板緩衝區的更新操作定義瞭如下枚舉類型:

typedef enum D3D11_STENCIL_OP {
  D3D11_STENCIL_OP_KEEP       = 1,
  D3D11_STENCIL_OP_ZERO       = 2,
  D3D11_STENCIL_OP_REPLACE    = 3,
  D3D11_STENCIL_OP_INCR_SAT   = 4,
  D3D11_STENCIL_OP_DECR_SAT   = 5,
  D3D11_STENCIL_OP_INVERT     = 6,
  D3D11_STENCIL_OP_INCR       = 7,
  D3D11_STENCIL_OP_DECR       = 8 
} D3D11_STENCIL_OP;

       同樣,我們忽略前綴:

       KEEP:保持當前值 不變,比如測試前模板值爲0,則繼續爲0不變;

       ZERO:把模板緩衝區對應位置的模板值設爲0;

       REPLACE:"replace"即替換的意思,即使用模板參考值替換模板緩衝區中對應的當前值;

       INCR_SAT:"INCR"即increase,自增的意思,"SAT"爲saturate,用於限制自增的範圍。即把當前的模板值加1。如果值超過了255(因爲我們的模板緩衝區爲8位,因此255即爲最大值),則保持在255。

       DECR_SAT:同上,DECR爲"decrease",自減的意思,即把當前值自減1,如果值低於0,則保持在0;

       INVERT:把當前模板值按位取反。比如對於0xffffffff,更新後的結果爲0x00000000;

       INCR:同上面的INCR一樣,也是把當前模板值自增1,但如果值超過255,則返回到0,之後繼續自增;

       DECR:同上面的DECR一樣,也是把當前模板值自減1,但如果值低於0,則爲255,之後繼續自減。

 

       4. D3D11中針對模板緩衝區的操作

       之前我們使用過混合,在使用混合,我們首先要創建一個BlendState,然後通過SetBlendState來使用它。同樣,這裏我們要使用模板緩衝區,也是首先創建相應的DepthStencilState,然後SetDepthStencilState。在D3D11中對應的函數爲:

HRESULT CreateDepthStencilState(
  [in]   const D3D11_DEPTH_STENCIL_DESC *pDepthStencilDesc,
  [out]  ID3D11DepthStencilState **ppDepthStencilState
);

       第一個參數爲對應的深度/模板緩衝區狀態描述,我們應該已經很習慣這種步驟了。無論是創建紋理、緩衝區、還是各種渲染狀態,第一步都是通過給出其描述開始;

       第二個參數爲我們要創建的狀態接口的地址。

       狀態描述結果定義如下:

typedef struct D3D11_DEPTH_STENCIL_DESC {
  BOOL                       DepthEnable;
  D3D11_DEPTH_WRITE_MASK     DepthWriteMask;
  D3D11_COMPARISON_FUNC      DepthFunc;
  BOOL                       StencilEnable;
  UINT8                      StencilReadMask;
  UINT8                      StencilWriteMask;
  D3D11_DEPTH_STENCILOP_DESC FrontFace;
  D3D11_DEPTH_STENCILOP_DESC BackFace;
} D3D11_DEPTH_STENCIL_DESC;

       該結構中前幾個用於描述深度緩衝區,後面用於描述模板緩衝區。(畢竟該兩個緩衝區位於一起嘛~)

       DepthEnable:是否使用深度緩衝區,顯然大多數數情況下爲true;

       DepthWriteMask:深度值寫入掩碼值,大多數情況下我們把整個深度值完整寫入,因此掩碼值爲D3D11_DEPTH_WRITE_MASK_ALL;

       DepthFunc:深度判斷值,即本文第二部分中提到的比較函數,大多數情況下我們使用LESS,即更小的深度(更靠前)通過測試;

       StencilEnable:是否使用模板緩衝區,我們就是要開啓模板緩衝區,因此爲true;

       StencilReadMask:模板值讀取掩碼,大多數情況下我們使用0xff;

       StencilWriteMask:模板值寫入掩碼,同樣爲0xff;

       FrontFace:針對渲染物體的正面,所使用的模板更新操作;

       BackFace:針對渲染物體的背面,所使用的模板更新操作。

       這裏的FrontFace和BackFace對應的結果定義如下:

typedef struct D3D11_DEPTH_STENCILOP_DESC {
  D3D11_STENCIL_OP      StencilFailOp;
  D3D11_STENCIL_OP      StencilDepthFailOp;
  D3D11_STENCIL_OP      StencilPassOp;
  D3D11_COMPARISON_FUNC StencilFunc;
} D3D11_DEPTH_STENCILOP_DESC;

       這裏前三個操作就是本文第三部分所提到的模板更新操作。

       StencilFailOp:模板測試失敗後的操作,比如我們想設置爲如果失敗,則保持不變,則爲KEEP;

       StencilDepthFailOp:深度測試失敗後的操作;

       StencilPassOp:模板測試通過後的操作,比如我們在通過測試後更新爲參考值,則爲REPLACE;

       StencilFunc:比較操作,即本文第二部分中提到的比較操作:LESS、GREATER、ALWAYS等等。

       一般情況下,我們只渲染物體的正面,背面是剔除的,因此在上面的結構中,對於BackFace的設置是無關緊要的。

 

       到現在爲此,你可能會有疑問:在模板測試中使用的模板參考值怎麼設定? 沒錯,上面我們只是說是程序員設定的,但到目前爲止還沒提到如何設定這個值。其實該模板參考值正是通過SetDepthStencilState函數設定的。該函數原型如下:

void OMSetDepthStencilState(
  [in]  ID3D11DepthStencilState *pDepthStencilState,
  [in]  UINT StencilRef
);

       第一個參數就是我們剛剛創建的深度/模板狀態接口;

       第二個參數即指定模板參考值,爲UINT類型。

       好了,有關模板緩衝區的使用就這些,下面來個小結:

       1. 使用模板緩衝區時最重要的兩個值:緩衝區中的當前值value,模板參考值ref;

       2. 模板測試的本質即對該兩個值使用特定的比較操作:NEVER, ALWAYS, LESS, EQUAL, GREATER等等;

       3. 模板測試後要對模板緩衝區進行相應的更新,更新操作包括:KEEP, REPLACE, INCR_SAT, INCR, DECR, DECR_SAT等等;

       4. 模板測試後針對不同結果可以使用不同的更新操作,包括測試成功操作(StencilPassOp),測試失敗操作(StencilFailOp),深度測試失敗操作(DepthFailOp).

 

       有關模板緩衝區的使用,理論知識就是這些。但是學習模板緩衝區,最好的方法就是研究實際的例子。下面我們就通過一個平面鏡子反射的例子來進一步掌握模板緩衝區的使用。

       5. 實際例子:平面鏡的實現

          5.1 要解決的兩個關鍵問題

          要實現平面鏡反射效果,主要有兩大關鍵問題要解決。

          首先是平面鏡反射的變換操作。對於一個要繪製的物體,如何得到它在鏡子中的影子的表示。在3D中,任何變換都是通過矩陣來實現的。這裏也一樣,這時我們就需要一個反射變換矩陣。反射變換矩陣可以惟一地通過一個平面給確定,該平面就是反射平面。在3D中,平面的數學表示爲一個4維的向量[Nx, Ny, Nz, d]。此外,XNAMath也提供了相應的函數來等到反射變換矩陣:

XMMATRIX XMMatrixReflect(
         XMVECTOR ReflectionPlane
)

       這裏面惟一的參數即我們的反射平面。

       由於篇幅限制,在本文中暫時不對任意平面的3D數學表示進行解釋,後面的示例程序中我們對於使用的平面直接給出其表示形式。如果大家對3D平面的數學表示有困惑,可以參考專門的3D數學方面的書籍。也歡迎向我提出,如有必要我後面會專門寫篇文章來解釋3D平面表示的推導。

          第二個問題,也是核心問題,即:當我們的觀察點在空間中任意移動時,在移動到特定範圍之外時,我們將看不到鏡子中的物體。我想這種情況在現實當中很容易想到吧。下面我通過幾張圖來說明下這種情景(這些圖來自本文對應的示例程序截圖,大家可以自由下載參考源代碼):

上面這張圖是正常情況下我們在鏡子中看到物體的情形。

當我們在空間移動時,由於鏡子尺寸的限制,整個物體(或部分)可能會移動到鏡子範圍之外。正如以上圖片所示,這時我們應該只能看到一部分物體了。這種情況也正是我們最終期待看到的情況。 但這就涉及到一個問題:如何讓程序知道哪些部分位於鏡子當中,以正確繪製它;而哪些部分位於鏡子之外,從而不繪製呢?

這時就是“模板緩衝區”大顯身手的時候啦!

在繼續介紹之前,我們先看一下如果沒有模板緩衝區,將是什麼樣的情況:針對上幅圖中的視角,以下是不使用模板緩衝區時的渲染結果:

物體竟然顯示到鏡子外面去啦!這顯然是不允許的!還好有模板緩衝區的存在~

下面我們來詳細地看下如何作用模板緩衝區來實現第二張圖中的效果。

          5.2 繪製過程

          在這個場景中,主要有如下幾部分:牆面、地面、箱子、鏡子、鏡中的箱子。

          1. 我們按正常情況繪製牆面、地面和箱子。這些物體的繪製與模板緩衝區無任何關係。

          2. 第二步我們就要針對鏡子區域,使用模板緩衝區來進行限制了。

              爲了告訴計算機,哪些區域是鏡子所在區域,哪些區域是鏡子之外的部分,我們需要使用模板緩衝區來標記給它看。因此在這一步當中,我們只針對模板緩衝區進行操作,而不更改場景中的顏色值。

              爲了實現這種效果,我們首先要禁止顏色的寫入。在之前文章介紹“混合”的使用時,提到過禁止顏色寫入的實現,即通過把D3D11_RENDER_TARGET_BLEND_DESC中對應的RenderTargetWriteMask設置爲0即可。如有疑問,可以參考這裏。 相應的狀態我已經在源代碼框架中的RenderStates中定義好了,即NoColorWrite狀態,詳細情況請參考源代碼。

              其次就是模板緩衝區的操作了。還記得不,在每開始繪製一幀時,我們要做的第一件事就是清屏,包括後緩衝區以及深度/模板緩衝區。清屏時我們指定了默認的模板值,比如0,也是我們之前一直做的。如下語句,最後一個參數就是清屏時設定的模板值:

	m_deviceContext->ClearDepthStencilView(m_depthStencilView,D3D11_CLEAR_DEPTH|D3D11_CLEAR_STENCIL,1.f,0);

因此在經過第一步繪製後,模板緩衝區中所有的模板值依然爲0,因爲我們沒打開模板操作。爲了惟一地標記鏡子所在區域,我們需要修改該區域對應的模板值,以區別於其他區域的模板值。比如把該區域的模板值設爲參考值。

              要修改模板緩衝區,我們就需要調用繪製函數。因此我們只需要更新鏡子所在區域的模板值,因此這一步中我們只繪製鏡子,由於禁止顏色寫入,因此場景並不會被改變,僅僅是用於修改模板緩衝區。爲了把鏡子所在區域全部更新,我們使用的模板比較函數爲ALWAYS即只要繪製該區域,總能夠通過測試而修改模板值。對於模板更新操作,正如剛纔所言,我們使用REPLACE操作,把對應區域設爲參考值,即針對StencilPass的。因爲現在模板測試總是通過,因此StencilFail的操作就無所謂的。對於DepthFail後的更新操作,我們設爲KEEP,即即使模板測試通過,但深度測試不通過的,依然不改變對應的模板值(設想下,如果箱子擋住了一部分鏡子,對於這一部分,即使位於鏡子範圍之內,我們也看不到鏡子的物體吧。這就是深度測試失敗導致的。因此這時我們就不需要修改對應的模板值了。當然,就算修改了,後面真正繪製鏡子物體時,還會因爲深度測試失敗而丟棄)

       爲了形象地表示這兩步中後緩衝區和模板緩衝區相應的改變,請看以下幾張圖:

這張圖是第一步繪製完牆面、地面、箱子後對應的後緩衝區和模板緩衝區。我們重點關注模板緩衝區,圖中一致地爲灰色,代表默認的0.

 這張圖爲第二步把鏡子區域模板值修改爲參考值後的情形。顯然,後緩衝區沒有任何改變。而模板緩衝區,我們看到鏡子區域的改變,而其他地方保持不變。這樣我們就惟一地鏡子區域標記出來了。

          3. 繪製鏡中的物體

          現在,我們開始繪製箱子經反射後在鏡子中的部分。 這裏有兩大事情要做:

          1. 生成相應的變換矩陣,注意光源的方向也要經過鏡子的反射

          在該程序中,我們的鏡子所在平面爲與z軸垂直且位於z = 5的平面,正面指向z軸負方向。這裏我直接給出其數學表示, 爲【0,0,-1, 5】。通過它生成反射矩陣,應用於箱子,並且相應地修改光源的方向,代碼如下:

	//首先計算反射矩陣:反射面爲牆面
	XMVECTOR refPlane = XMVectorSet(0.f,0.f,-1.f,5.f);			//牆面的數學表示:[0,0,-1,5]
	XMMATRIX R = XMMatrixReflect(refPlane);

	//更新鏡中物品相應的變換矩陣
	XMMATRIX view = XMLoadFloat4x4(&m_view);
	XMMATRIX proj = XMLoadFloat4x4(&m_proj);
	XMMATRIX worldBox = XMLoadFloat4x4(&m_worldBox)*R;
	XMMATRIX worldInvTranspose = InverseTranspose(worldBox);
	XMMATRIX wvp = worldBox * view * proj;
	XMMATRIX texTrans = XMMatrixIdentity();

	//注意也要對光照方向進行相應的反射變換
	XMFLOAT3 oldDirs[3];
	for(UINT i=0; i<3; ++i)
	{
		oldDirs[i] = m_dirLights[i].dir;
		XMVECTOR dir = XMVectorSet(oldDirs[i].x,oldDirs[i].y,oldDirs[i].z,1.f);
		XMVECTOR rdir = XMVector3TransformNormal(dir,R);
		XMStoreFloat3(&m_dirLights[i].dir,rdir);
	}

          還有一點要注意,原物體中規定頂點以順時針爲正面,但經過鏡面反射後,對應的正面會變成逆時針方向,因此我們還需要相應地設置渲染狀態,規定逆時針爲正面。否則鏡子中的物體會被作爲背面而剔除掉。

          第二件大事爲設置相應的模板緩衝區狀態。

          爲了使用之前標記好的區域,我們規定,只要當對應的模板值爲參考值時才通過模板測試,否則失敗。即這時的模板比較函數爲EQUAL。顯然,只有鏡子中區域模板值才爲參考值,其他區域都不會通過的。因此保證了物體只能被繪製在鏡子範圍之內。這個對應的模板狀態在源代碼框架中RenderStates中也定義好了,爲DrawReflectionDSS。

現在設置好渲染狀態就可以繪製鏡子中的箱子了,渲染狀態有如下兩個關鍵點:

	//由於反射前順時針順序的頂點在反射後變爲逆時針,因此暫時需要讓逆時針爲正面來渲染
	m_deviceContext->RSSetState(RenderStates::CounterClockFrontRS);
	//設置好相應的模板緩衝區狀態
	m_deviceContext->OMSetDepthStencilState(RenderStates::DrawReflectionDSS,0x1);

          當然,繪製完還要記得把光源方向改回來。

	for(UINT i=0; i<3; ++i)
	{
		m_dirLights[i].dir = oldDirs[i];
	}

          4. 最後一步,渲染鏡子本身

          現在只差最後的鏡子了。由於我們既要看到鏡子本身,也要看到裏面的物體,因此對於鏡子的渲染,我們要開啓混合,使用“透明”效果把鏡子和裏面的物體混合起來。這一步很簡單了,不需要模板緩衝區的操作,只要設置好“透明”狀態即可。透明狀態我們在之前的例子中已經定義好了,我們直接使用。如下:

	//開啓透明狀態
	m_deviceContext->OMSetBlendState(RenderStates::TransparentBS,blendFactor,0xffffffff);

 

          OK, 整個渲染過程就結束了。小結下:

          1. 正常繪製牆面、地面、箱子

          2. 設置“禁止顏色寫入”狀態,只渲染鏡子,以利用模板緩衝區標記鏡子區域(ALWAYS比較操作,通過模板測試的使用REPLACE更新模板值)(不僅限於REPLACE,比如INCR、INCR_SAT也是可以的)

          3. 使用相應的反射變換矩陣,渲染鏡子中物體(光源的方向也需要相應的改變),設置相應的模板狀態,只在標記區域內渲染(EQUAL比較操作)

          4. 開啓“透明”效果,渲染鏡子本身。

 

       6. 總結

       模板緩衝區的介紹到這兒就結束了。學習模板緩衝區的關鍵在於例子的學習,這節通過一個平面鏡的反射效果實現初步演示了模板的操作。此外,模板還可以用於其他很多特效中,比如陰影的實現。總之,模板緩衝區的使用是非常靈活的,大家可以在今後結合例子逐步的體會它的使用,並不斷地總結,以慢慢地學會自己使用模板來解決其他相關問題。

       最後是本文對應的示例程序源代碼

 

       本文完

 

 

 

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