1. 程式人生 > >Stenciling技術實現鏡子效果 圖形學1

Stenciling技術實現鏡子效果 圖形學1

本人花時間整理的渲染鏡子的技術,有需要的朋友可以參照一下。

簡介:

模板緩衝(The stencil buffer)(或者翻譯為矇蔽層緩衝吧)屬於是一個後臺緩衝,不過它跟back buffer不一樣,它主要用來完成一些特效。比如:鏡子效果,陰影效果等。

它的緩衝大小是和一般的後臺緩衝(back buffer)和深度緩衝(depth buffer)一樣的。

模板緩衝的工作原來就是用來阻擋後臺緩衝的特定區域來完成特效的。

如果渲染的時候開啟了深度緩衝,那麼模板緩衝的計算時間基本上為零,速度很快。因為模板緩衝和深度緩衝是同時生成的,他們共同用一個32bits的緩衝。例如下面的四種:depth/stencil緩衝:

D3DFMT_D24S8      深度緩衝24 bits每畫素 模板緩衝8bits每畫素。

D3DFMT_D24X4S4

D3DFMT_D15S1

D3DFMT_D32          a 32-bit depth buffer only

thestencil test模板緩衝測試

決定什麼畫素可以顯示在螢幕上是有模板緩衝測試決定的,例如運用下面邏輯:,

IF ref & mask (comparation operation) value& mask= = true THEN accept pixel

ELSE reject pixel

ref 和mask的值都是程式設計師定義的,value是畫素的某值,通過comparation operation(也是程式設計師定義的函式)之後,比較如果為真就顯示,不為真就不顯示。

例如Direct3D9中就用下面函式來修改Stencil Reference Value(ref)為1.

gd3dDevice->SetRenderState(D3DRS_STENCILREF, 0x1);

預設的mask的值為0xffffffff, 沒有矇蔽任何位畫素

下面程式碼修改為矇蔽搞16位:

gd3dDevice->SetRenderState(D3DRS_STENCILMASK, 0x0000ffff);

Stencil Value 就是當前測試的模板緩衝的畫素值,我們可以用模板渲染狀態(stencil render states)來控制什麼可以寫在模板緩衝裡

Comparison Operation包括如下:

typedef enum _D3DCMPFUNC {
    D3DCMP_NEVER = 1,
    D3DCMP_LESS = 2,
    D3DCMP_EQUAL = 3,
    D3DCMP_LESSEQUAL = 4,
    D3DCMP_GREATER = 5,
    D3DCMP_NOTEQUAL = 6,
    D3DCMP_GREATEREQUAL = 7,
    D3DCMP_ALWAYS = 8,
    D3DCMP_FORCE_DWORD = 0x7fffffff
} D3DCMPFUNC;

最後到正題了:如何做鏡子效果?

要程式設計實現鏡子效果需要首先解決兩個問題:

1. 要知道如何鏡子是如何映射出物體的,我們就利用發射原理來正確地畫出鏡子裡面的物體。

2.  要矇蔽不是鏡子的區域,讓物體只在鏡子裡顯示。

第一個問題就是用幾何學解決,第二個問題是用我們上面講到的模板技術。

第一個問題如果基礎不好的,趕緊去惡補平面幾何知識。道理就是:知道了原物體的向量,然後知道平面的公式,利用這兩個已知量來計算對映物體的位置,就可以畫出對映物體了,DirectX可以呼叫函式如下:

D3DXMATRIX *D3DXMatrixReflect(    D3DXMATRIX *pout,       // The resulting reflection matrix.    CONST D3DXPLANE *pPlane // The plane to reflect about.);

其實我們一直都有在渲染鏡子裡面的物體的,不過是加了矇蔽層效果,所以只會在適當角度的時候,你才能看到鏡子裡面的物體罷了。

渲染步驟如下:

1. 正常地渲染整個場面,但是暫時不渲染鏡子裡面的物體。

2. 把鏡子位置渲染出來(或類似鏡子可以對映物體的物體),設定模板測試為通過,通過的話設定鏡子區域的畫素為1. 其他非鏡子區域的畫素都為0.

3. 然後把需要對映的物體渲染到後臺緩衝和模板緩衝中去。正如前面所介紹的這些畫素通過模板測試的才可以顯示,就是才可以渲染到後臺緩衝中去。從邏輯上解析就是做了與運算,需要渲染的畫素與畫素為1的與運算之後保持原來畫素,所以正常渲染;如果與畫素為0的與運算之後就會為0,所以沒有渲染。

以下是部分示例程式碼:

1. 首先設定好初始的渲染狀態:

gd3dDevice->SetRenderState(D3DRS_STENCILENABLE, true);
gd3dDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS);//鏡子區域的渲染都總是通過stencil測試,所以為always,當然鏡子的區域可能會隨著鏡頭的轉變而轉變的。
gd3dDevice->SetRenderState(D3DRS_STENCILREF, 0x1); //設定ref為1
gd3dDevice->SetRenderState(D3DRS_STENCILMASK, 0xffffffff);
gd3dDevice->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff);
gd3dDevice->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP);//stencil測試失敗,則保持stencil buffer原有的畫素,這裡為0
gd3dDevice->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP);
gd3dDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE);//用上面的ref值1來代替stencil的當前值


2. 渲染鏡子:

注意這裡是重點,要做出鏡子效果就必須理解好如何渲染鏡子和為什麼這樣渲染鏡子的。

這裡只是把鏡子渲染到模板緩衝中去,並沒有更新後臺緩衝,因為鏡子並不需要顯示在螢幕上,只是我們設定的一堵對映平面。

注意:畫鏡子的時候會自動同時更新back buffer和depthbuffer的,自動更新的演算法應該是用或運算,把模板緩衝的畫素和back buffer的畫素做或運算。

這時候利用blending技術,把將要寫入的物體畫素全部設定為零,就是全透明瞭,那麼就不會更新back buffer和depth buffer了。

// Disable writes to the depth and back buffers
gd3dDevice->SetRenderState(D3DRS_ZWRITEENABLE, false);//關閉depth buffer寫入
gd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, true);//開啟blending
gd3dDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);//設定源畫素因子(就是將要寫入的畫素)為0,那麼與畫素值與之後,結果為零。
gd3dDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);//目標(原來在back buffer的)畫素因子為1, 與目標畫素與之後,結果為目標畫素值
 
// Draw mirror to stencil only.
drawMirror();

// Re-enable depth writes
HR(gd3dDevice->SetRenderState( D3DRS_ZWRITEENABLE, true));

3.設定對映渲染需要的狀態:

// Only draw reflected teapot to the pixels where the mirror was drawn.
gd3dDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL);
gd3dDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP);

這時候如果stencil測試通過對的話,就保持需要畫的物體的畫素。測試函式為D3DCMP_EQUAL,而鏡子的stencil值為1,測試的值也為1,mask的值為0Xffffffff,所以鏡子區域的畫素都為通過狀態,就是說這個狀態下,物體就能在鏡子映射出來。

4.下面就是正式畫鏡子物體的步驟了,以畫一個茶壺為例:

// Build reflection transformation.
D3DXMATRIX R;
D3DXPLANE plane(0.0f, 0.0f, 1.0f, 0.0f); // xy-plane
D3DXMatrixReflect(&R, &plane);

// Save the original teapot world matrix.
D3DXMATRIX oldTeapotWorld = mTeapotWorld;

// Add reflection transform.
mTeapotWorld = mTeapotWorld * R;

// Reflect light vector also.
D3DXVECTOR3 oldLightVecW = mLightVecW;
D3DXVec3TransformNormal(&mLightVecW, &mLightVecW, &R);
mFX->SetValue(mhLightVecW, &mLightVecW, sizeof(D3DXVECTOR3));


這裡是座標變換的知識,利用原有的茶壺位置和反射平面的位置計算出需要對映茶壺的位置,然後渲染之。

5. 最後一步了

如果我們現在馬上渲染茶壺的話,那反射鏡子裡面不會顯示茶壺的。為什麼?因為鏡子離鏡頭更加近,所以把茶壺擋住了,這時候需要關閉深度測試。

注意:關閉深度測試的時候,畫物體的先後順序會很重要,畫後的物體會遮蔽畫先的物體,就是相當於把先畫在back buffer的畫素複寫了。所以這裡一定要畫完鏡子,然後再畫鏡子裡面的物體,否則,鏡子就擋住了裡面的物體了。

// Disable depth buffer and render the reflected teapot. We also
// disable alpha blending since we are done with it.
gd3dDevice->SetRenderState(D3DRS_ZENABLE, false);
gd3dDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, false);

gd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW);
drawTeapot();

用D3DCULL_CW,意思就是順時針為正面,directx預設是逆時針為正面的, 因為在鏡子裡面是反射世界,所以正面和反面都調轉過來了。

6.還有就是還原為預設渲染狀態的程式碼了:

// Restore original teapot world matrix and light vector.
mTeapotWorld = oldTeapotWorld;
mLightVecW   = oldLightVecW;

// Restore render states.
gd3dDevice->SetRenderState(D3DRS_ZENABLE, true);
gd3dDevice->SetRenderState( D3DRS_STENCILENABLE, false);
gd3dDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW);


總結:

這裡的渲染知識要一定基礎,包括:

1. 座標轉換

2.幾何知識

3.渲染過程一定了解

DirectX感覺還是挺方便的了,如果要更好的理解也許學習一下最基本的一些圖形學知識會更加好吧。比如一些圖形學最低層的演算法,如何操作畫素,一些畫直線的演算法和眾多插值演算法(這是很博大精深的演算法),這些東西很低層,不過對於理解高層的應用還是非常有用的。