1. 程式人生 > >DirectX11 Without DirectX SDK--14 深度測試

DirectX11 Without DirectX SDK--14 深度測試

gif img inpu 取代 分享圖片 一個 basic 能力 方法

前言

當使用加法/減法/乘法顏色混合,或者使用透明混合的時候,在經過深度測試時可能會引發一些問題。例如現在我們需要使用加法混合來繪制一系列對象,而這些對象彼此之間不會相互阻擋。若我們仍使用原來的深度測試,就必須保證某一像素下的所有片元需要按照從遠到近的順序來進行繪制,但這很難做到,尤其在繪制一些幾何體的時候可能會出現在前面的像素片元擋住了後面的像素片元的情況。現在,我們有能力通過調整深度測試這一行為,來改變最終的顯示結果。

DirectX11 With Windows SDK完整目錄

Github項目源碼

交換律

加法/減法/乘法的混合運算是滿足交換律的(對C0到Cn-1來說),這說明我們繪制像素的先後順序應該是不會對結果產生影響的:

\(\mathbf{B'}=\mathbf{B}+\mathbf{C_0}+\mathbf{C_1}+...+\mathbf{C_{n-1}}\)
\(\mathbf{B'}=\mathbf{B}-\mathbf{C_0}-\mathbf{C_1}-...-\mathbf{C_{n-1}}\)
\(\mathbf{B'}=\mathbf{B} \otimes \mathbf{C_0} \otimes \mathbf{C_1} \otimes ... \otimes \mathbf{C_{n-1}}\)

但是混合的先後順序會對結果有影響,所以不滿足交換律(無論alpha值如何):

\(\mathbf{B'}= 0.5(0.5\mathbf{B} + 0.5\mathbf{C_0}) + 0.5\mathbf{C_1} = 0.25\mathbf{B} + 0.25\mathbf{C_0} + 0.5\mathbf{C_1}\)
\(\mathbf{B'}= 0.5(0.5\mathbf{B} + 0.5\mathbf{C_1}) + 0.5\mathbf{C_0} = 0.25\mathbf{B} + 0.25\mathbf{C_1} + 0.5\mathbf{C_0}\)

深度測試

關閉深度測試

在某一個階段關閉深度測試後,若某一像素位置有新的像素片元出現,那麽該像素片元就會直接不經過深度測試來到後面的混合階段。此時混合沒開的話,該像素片元就會直接取代後備緩沖區的對應像素,此時需要按從後到前的順序來繪制物體才能保證正確的顯示效果。但關閉深度測試的一個主要用途是繪制場景內的一系列透明物體。當然,前提是這一堆透明物體中間沒有不透明物體在阻擋,否則不透明物體後面的透明物也會被繪制出來。

技術分享圖片

開啟深度測試但關閉深度寫入

相比上面的方式,這是一種更為合理的做法。我們只需要在渲染的時候先繪制不透明物體,然後就可以按任意的順序來繪制透明物體。這是因為當繪制透明物體的時候,若它前面有不透明物體阻擋,則不會通過深度測試。所以十分適合用於處理透明物體和不透明物體相互交叉的情況。

技術分享圖片

利用深度測試和模板測試來繪制閃電特效與其鏡面

一個閃電動畫實際上是由60張按順序播放的位圖構成:
技術分享圖片

然後在繪制的時候,每秒繪制60幀閃電動畫,即1秒一個循環。切換下一幀的時候,只需要更換下一張紋理即可。

// 更新閃電動畫
mBoltAnim.SetTexture(mBoltSRVs[currBoltFrame].Get());
if (frameTime > 1.0f / 60)
{
    currBoltFrame = (currBoltFrame + 1) % 60;
    frameTime -= 1.0f / 60;
}
frameTime += dt;

RenderStates類的變化

現在RenderStates類有如下可用的狀態:

class RenderStates
{
public:
    template <class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    static void InitAll(const ComPtr<ID3D11Device>& device);
    // 使用ComPtr無需手工釋放

public:
    static ComPtr<ID3D11RasterizerState> RSWireframe;       // 光柵化器狀態:線框模式
    static ComPtr<ID3D11RasterizerState> RSNoCull;          // 光柵化器狀態:無背面裁剪模式
    static ComPtr<ID3D11RasterizerState> RSCullClockWise;   // 光柵化器狀態:順時針裁剪模式

    static ComPtr<ID3D11SamplerState> SSLinearWrap;         // 采樣器狀態:線性過濾
    static ComPtr<ID3D11SamplerState> SSAnistropicWrap;     // 采樣器狀態:各項異性過濾

    static ComPtr<ID3D11BlendState> BSNoColorWrite;     // 混合狀態:不寫入顏色
    static ComPtr<ID3D11BlendState> BSTransparent;      // 混合狀態:透明混合
    static ComPtr<ID3D11BlendState> BSAlphaToCoverage;  // 混合狀態:Alpha-To-Coverage
    static ComPtr<ID3D11BlendState> BSAdditive;         // 混合狀態:加法混合


    static ComPtr<ID3D11DepthStencilState> DSSWriteStencil;     // 深度/模板狀態:寫入模板值
    static ComPtr<ID3D11DepthStencilState> DSSDrawWithStencil;  // 深度/模板狀態:對指定模板值的區域進行繪制
    static ComPtr<ID3D11DepthStencilState> DSSNoDoubleBlend;    // 深度/模板狀態:無二次混合區域
    static ComPtr<ID3D11DepthStencilState> DSSNoDepthTest;      // 深度/模板狀態:關閉深度測試
    static ComPtr<ID3D11DepthStencilState> DSSNoDepthWrite;     // 深度/模板狀態:僅深度測試,不寫入深度值
    static ComPtr<ID3D11DepthStencilState> DSSNoDepthTestWithStencil;   // 深度/模板狀態:關閉深度測試,對指定模板值的區域進行繪制
    static ComPtr<ID3D11DepthStencilState> DSSNoDepthWriteWithStencil;  // 深度/模板狀態:僅深度測試,不寫入深度值,對指定模板值的區域進行繪制
};

加法混合

加法混合模式的創建如下:

D3D11_BLEND_DESC blendDesc;
ZeroMemory(&blendDesc, sizeof(blendDesc));
auto& rtDesc = blendDesc.RenderTarget[0];
blendDesc.AlphaToCoverageEnable = false;
blendDesc.IndependentBlendEnable = false;
rtDesc.BlendEnable = true;

// 加法混合模式
// Color = SrcColor + DestColor
// Alpha = SrcAlpha
rtDesc.SrcBlend = D3D11_BLEND_ONE;
rtDesc.DestBlend = D3D11_BLEND_ONE;
rtDesc.BlendOp = D3D11_BLEND_OP_ADD;
rtDesc.SrcBlendAlpha = D3D11_BLEND_ONE;
rtDesc.DestBlendAlpha = D3D11_BLEND_ZERO;
rtDesc.BlendOpAlpha = D3D11_BLEND_OP_ADD;

HR(device->CreateBlendState(&blendDesc, BSAdditive.ReleaseAndGetAddressOf()));

關閉深度測試

需要準備好默認情況下的繪制和指定模板值繪制兩種情況:

D3D11_DEPTH_STENCIL_DESC dsDesc;

// 關閉深度測試的深度/模板狀態
// 若繪制非透明物體,務必嚴格按照繪制順序
// 繪制透明物體則不需要擔心繪制順序
// 而默認情況下模板測試就是關閉的
dsDesc.DepthEnable = false;
dsDesc.StencilEnable = false;

HR(device->CreateDepthStencilState(&dsDesc, DSSNoDepthTest.ReleaseAndGetAddressOf()));

// 關閉深度測試
// 若繪制非透明物體,務必嚴格按照繪制順序
// 繪制透明物體則不需要擔心繪制順序
// 對滿足模板值條件的區域才進行繪制
dsDesc.StencilEnable = true;
dsDesc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK;
dsDesc.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;

dsDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// 對於背面的幾何體我們是不進行渲染的,所以這裏的設置無關緊要
dsDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;

HR(device->CreateDepthStencilState(&dsDesc, DSSNoDepthTestWithStencil.ReleaseAndGetAddressOf()));

允許深度測試,但不寫入深度值的狀態

同理也需要準備好默認繪制和指定模板值繪制兩種情況:

// 進行深度測試,但不寫入深度值的狀態
// 若繪制非透明物體時,應使用默認狀態
// 繪制透明物體時,使用該狀態可以有效確保混合狀態的進行
// 並且確保較前的非透明物體可以阻擋較後的一切物體
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS;
dsDesc.StencilEnable = false;

HR(device->CreateDepthStencilState(&dsDesc, DSSNoDepthWrite.ReleaseAndGetAddressOf()));

// 進行深度測試,但不寫入深度值的狀態
// 若繪制非透明物體時,應使用默認狀態
// 繪制透明物體時,使用該狀態可以有效確保混合狀態的進行
// 並且確保較前的非透明物體可以阻擋較後的一切物體
// 對滿足模板值條件的區域才進行繪制
dsDesc.StencilEnable = true;
dsDesc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK;
dsDesc.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;

dsDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
// 對於背面的幾何體我們是不進行渲染的,所以這裏的設置無關緊要
dsDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;

HR(device->CreateDepthStencilState(&dsDesc, DSSNoDepthWriteWithStencil.ReleaseAndGetAddressOf()));

BasicFX類的變化

本來打算利用ID3D11ShaderReflection來嘗試獲得更多信息的,但是發現要實現一個功能接近於Effects11的類還是特別復雜,並且沒辦法寫原來FX特有的HLSL代碼,在這裏只好先擱置。

下面四個方法都是專門用於繪制閃電動畫的,使用了加法混合。每一個都可以看做是一個Effects11中的Technique11

BasicFX::SetDrawBoltAnimNoDepthTest方法

該方法關閉了深度測試,用於繪制閃電動畫(但默認並不是使用這個,你需要自行修改代碼替換調用來查看區別)

void BasicFX::SetDrawBoltAnimNoDepthTest()
{
    md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get());
    md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
    md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSNoDepthTest.Get(), 0);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSAdditive.Get(), nullptr, 0xFFFFFFFF);
}

BasicFX::SetDrawBoltAnimNoDepthWrite方法

該方法允許深度測試,但關閉深度值寫入,用於繪制閃電動畫(在程序中默認使用這種模式)

void BasicFX::SetDrawBoltAnimNoDepthWrite()
{
    md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get());
    md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
    md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSNoDepthWrite.Get(), 0);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSAdditive.Get(), nullptr, 0xFFFFFFFF);
}

BasicFX::SetDrawBoltAnimNoDepthTestWithStencil方法

該方法關閉了深度測試,用於繪制鏡面區域的閃電動畫(默認不使用這種模式)

void BasicFX::SetDrawBoltAnimNoDepthTestWithStencil(UINT stencilRef)
{
    md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get());
    md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
    md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSNoDepthTestWithStencil.Get(), stencilRef);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSAdditive.Get(), nullptr, 0xFFFFFFFF);
}

BasicFX::SetDrawBoltAnimNoDepthWriteWithStencil方法

該方法開啟深度測試,但不允許寫入深度值,用於繪制鏡面區域的閃電動畫(默認使用這種模式)

void BasicFX::SetDrawBoltAnimNoDepthWriteWithStencil(UINT stencilRef)
{
    md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get());
    md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
    md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSNoDepthWriteWithStencil.Get(), stencilRef);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSAdditive.Get(), nullptr, 0xFFFFFFFF);
}

場景繪制

現在的場景繪制可以說算是比較復雜的了,需要同時處理透明物體、非透明物體的繪制,以及繪制鏡面和陰影的效果。因此嚴格的按照正確順序去繪制在這裏就變得十分重要。

第1步: 鏡面區域寫入模板緩沖區

和之前一樣,先標記好鏡面區域:

// *********************
// 1. 給鏡面反射區域寫入值1到模板緩沖區
// 

mBasicFX.SetWriteStencilOnly(1);
mMirror.Draw(md3dImmediateContext);

第2步:繪制不透明的反射物體

// ***********************
// 2. 繪制不透明的反射物體
//

// 開啟反射繪制
mDrawingState.isReflection = 1;     // 反射開啟
mBasicFX.UpdateConstantBuffer(mDrawingState);
mBasicFX.SetRenderDefaultWithStencil(1);

mWalls[2].Draw(md3dImmediateContext);
mWalls[3].Draw(md3dImmediateContext);
mWalls[4].Draw(md3dImmediateContext);
mFloor.Draw(md3dImmediateContext);
    
mWoodCrate.Draw(md3dImmediateContext);

技術分享圖片

第3步:繪制不透明反射物體的陰影

// ***********************
// 3. 繪制不透明反射物體的陰影
//

mWoodCrate.SetMaterial(mShadowMat);
mDrawingState.isShadow = 1;         // 反射開啟,陰影開啟
mBasicFX.UpdateConstantBuffer(mDrawingState);
mBasicFX.SetRenderNoDoubleBlend(1);

mWoodCrate.Draw(md3dImmediateContext);

// 恢復到原來的狀態
mDrawingState.isShadow = 0;
mBasicFX.UpdateConstantBuffer(mDrawingState);
mWoodCrate.SetMaterial(mWoodCrateMat);

技術分享圖片

第4步:繪制需要混合的反射閃電動畫和透明物體

// ***********************
// 4. 繪制需要混合的反射閃電動畫和透明物體
//

mBasicFX.SetDrawBoltAnimNoDepthWriteWithStencil(1);
mBoltAnim.Draw(md3dImmediateContext);

mBasicFX.SetRenderAlphaBlendWithStencil(1);
mMirror.Draw(md3dImmediateContext);

技術分享圖片

第5步:繪制不透明的正常物體

// ************************
// 5. 繪制不透明的正常物體
//
mBasicFX.SetRenderDefault();
mDrawingState.isReflection = 0;     // 反射關閉
mBasicFX.UpdateConstantBuffer(mDrawingState);
    
for (auto& wall : mWalls)
    wall.Draw(md3dImmediateContext);
mFloor.Draw(md3dImmediateContext);
mWoodCrate.Draw(md3dImmediateContext);

技術分享圖片

第6步:繪制不透明正常物體的陰影

// ************************
// 6. 繪制不透明正常物體的陰影
//
mWoodCrate.SetMaterial(mShadowMat);
mDrawingState.isShadow = 1;         // 反射關閉,陰影開啟
mBasicFX.UpdateConstantBuffer(mDrawingState);
mBasicFX.SetRenderNoDoubleBlend(0);

mWoodCrate.Draw(md3dImmediateContext);

mDrawingState.isShadow = 0;         // 陰影關閉
mBasicFX.UpdateConstantBuffer(mDrawingState);
mWoodCrate.SetMaterial(mWoodCrateMat);

技術分享圖片

第7步:繪制需要混合的閃電動畫

// ************************
// 7. 繪制需要混合的閃電動畫
mBasicFX.SetDrawBoltAnimNoDepthWrite();
mBoltAnim.Draw(md3dImmediateContext);

技術分享圖片

最終動畫效果如下:

技術分享圖片

DirectX11 With Windows SDK完整目錄

Github項目源碼

DirectX11 Without DirectX SDK--14 深度測試