DirectX11 With Windows SDK--12 深度/模板狀態

前言

深度/模板測試使用的是與後備緩衝區同等分辨率大小的緩衝區,每個元素的一部分連續位用於深度測試,其餘的則用作模板測試。兩個測試的目的都是爲了能夠根據深度/模板狀態需求的設置來選擇需要繪製的像素。

DirectX11 With Windows SDK完整目錄

Github項目源碼

深度/模板測試

深度測試、模板測試的執行是在混合操作之前執行的,具體的執行順序爲:模板測試→深度測試→混合操作。

深度測試

深度測試需要用到深度/模板緩衝區,對每個像素使用24位或32位來映射物體從世界到NDC座標系下z的值(即深度,範圍[0.0, 1.0])。0.0時達到攝像機的最近可視距離,而1.0時達到攝像機的最遠可視距離。若某一像素位置接收到多個像素片元,只有z值最小的像素纔會通過最終的深度測試。具體細化的話,就是現在有一個像素片元,已知它的深度值,然後需要跟深度緩衝區中的深度值進行比較,若小於深度緩衝區的深度值,則該像素片元將會覆蓋後備緩衝區原來的像素片元,並更新深度緩衝區中對應位置的深度值。

模板測試

除了深度測試以爲,我們還可以設定模板測試來阻擋某些特定的區域的像素通過後備緩衝區。而且模板測試在操作上自由度會比深度測試大。對於需要進行模板測試的像素,比較式如下:
(StencilRef & StencilReadMask) ⊴ (Value & StencilReadMask)

該表達式首先括號部分是兩個操作數進行與運算,然後通過⊴(用戶指定的運算符)對兩個結果進行比較。若該表達式的值爲真,則最終通過模板測試,並保留該像素進行後續的混合操作。

其中StencilReadMask則是應用程序所提供的掩碼值。

深度/模板格式

深度/模板緩衝區是一個2D數組(紋理),它必須經由確定的數據格式創建:

  1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT:每個元素佔64位,其中32位浮點數用於深度測試,8位無符號整數用於模板測試,剩餘24位僅用於填充。

  2. DXGI_FORMAT_D24_UNORM_S8_UINT:每個元素佔32位,其中24位無符號整數映射到深度值[0.0, 1.0]的區間,8位無符號整數用於模板測試。

ID3D11DeviceContext::ClearDepthStencilView方法–深度/模板緩衝區內容清空

方法原型如下:

void ID3D11DeviceContext::ClearDepthStencilView(
    ID3D11DepthStencilView *pDepthStencilView,  // [In]深度模板視圖
    UINT ClearFlags,     // [In]使用D3D11_CLEAR_FLAG枚舉類型決定需要清空的部分
    FLOAT Depth,         // [In]使用Depth值填充所有元素的深度部分
    UINT8 Stencil);      // [In]使用Stencil值填充所有元素的模板部分

其中D3D11_CLEAR_FLAG有如下枚舉值:

枚舉值 含義
D3D11_CLEAR_DEPTH 清空深度部分
D3D11_CLEAR_STENCIL 清空模板部分

可以使用位運算或來同時清理。

通常深度值會默認設爲1.0以確保任何在攝像機視野範圍內的物體都能被顯示出來

模板值則默認會設置爲0

ID3D11Device::CreateDepthStencilState方法–創建深度/模板狀態

要創建深度/模板狀態ID3D11DepthStencilState之前,首先需要填充D3D11_DEPTH_STENCIL_DESC結構體:

typedefstruct 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;

深度狀態設定

  1. DepthEnable:如果關閉了深度測試,則繪製的先後順序就十分重要了。對於不透明的物體,必須按照從後到前的順序進行繪製,否則最後繪製的內容會覆蓋之前的內容,看起來就像在最前面那樣。並且關閉深度測試會導致深度緩衝區的值會保持原樣,不再進行更新,此時DepthWriteMask也不會使用。

  2. D3D11_DEPTH_WRITE_MASK枚舉類型只有兩種枚舉值:

枚舉值 含義
D3D11_DEPTH_WRITE_MASK_ZERO 不寫入深度/模板緩衝區
D3D11_DEPTH_WRITE_MASK_ALL 允許寫入深度/模板緩衝區

但即便設置了D3D11_DEPTH_WRITE_MASK_ZERO,如果DepthEnable開着的話仍會取原來的深度值進行深度比較,只是不會更新深度緩衝區。

  1. DepthFunc:指定D3D11_COMPARISON_FUNC枚舉值來描述深度測試的比較操作,標準情況下是使用D3D11_COMPARISON_LESS來進行深度測試,當然你也可以自定義測試的比較方式。

枚舉類型D3D11_COMPARISON_FUNC的枚舉值如下:

枚舉值 含義
D3D11_COMPARISON_NEVER = 1 該比較函數一定返回false
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 該比較函數一定返回true

默認情況下,深度狀態的值如下:

DepthEnable = TRUE;
DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL
DepthFunc = D3D11_COMPARISION_LESS

模板狀態設定

  1. StencilEnable:若要使用模板測試,則指定爲true
  2. StencilReadMask:該掩碼用於指定StencilRef和深度/模板緩衝區的模板值Value中的某些特定位,默認使用的是下面宏常量:
    #define D3D11_DEFAULT_STENCIL_READ_MASK (0xff)
  3. StencilWriteMask:該掩碼指定待寫入的模板值的哪些位要寫入深度/模板緩衝區中,默認使用的是下面宏常量:
    #define D3D11_DEFAULT_STENCIL_WRITE_MASK (0xff)
  4. FrontFace:該結構體指定了不同測試結果下對模板值應做什麼樣的更新(對於正面朝向的三角形)
  5. BackFace:該結構體指定了不同測試結果下對模板值應做什麼樣的更新(對於背面朝向的三角形)

深度/模板操作描述結構體如下:

typedefstruct D3D11_DEPTH_STENCILOP_DESC {
    D3D11_STENCIL_OP StencilFailOp;      
    D3D11_STENCIL_OP StencilDepthFailOp; 
    D3D11_STENCIL_OP StencilPassOp;      
    D3D11_COMPARISON_FUNC StencilFunc;   
} D3D11_DEPTH_STENCILOP_DESC;
  1. StencilFailOp:若模板測試不通過對深度/模板緩衝區的模板值部分的操作
  2. StencilDepthFailOp:若模板測試通過,但深度測試不通過對深度/模板緩衝區的模板值部分的操作
  3. StencilPassOp:若模板/深度測試通過對深度/模板緩衝區的模板值部分的操作
  4. StencilFunc:模板測試所用的比較函數

枚舉類型D3D11_STENCIL_OP的枚舉值如下:

枚舉值 含義
D3D11_STENCIL_OP_KEEP 保持目標模板值不變
D3D11_STENCIL_OP_ZERO 保持目標模板值爲0
D3D11_STENCIL_OP_REPLACE 使用StencilRef的值替換模板模板值
D3D11_STENCIL_OP_INCR_SAT 對目標模板值加1,超過255的話將值保持在255
D3D11_STENCIL_OP_DECR_SAT 對目標模板值減1,低於0的話將保持在0
D3D11_STENCIL_OP_INVERT 對目標模板值的每個位進行翻轉
D3D11_STENCIL_OP_INCR 對目標模板值加1,超過255的話值將上溢變成0
D3D11_STENCIL_OP_DECR 對目標模板值減1,低於0的話將下溢變成255

默認情況下,模板狀態的值如下:

StencilEnable = FALSE;
StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK;
StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;

FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;

BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;

填充完上面一堆結構體信息後,就終於可以創建深度模板狀態了:

HRESULT ID3D11Device::CreateDepthStencilState(
  const D3D11_DEPTH_STENCIL_DESC *pDepthStencilDesc,      // [In]深度/模板狀態描述
  ID3D11DepthStencilState        **ppDepthStencilState    // [Out]輸出深度/模板狀態
);

ID3D11DeviceContext::OMSetDepthStencilState方法–輸出合併階段設置深度/模板狀態

創建好深度/模板狀態後,我們就可以將它綁定到渲染管線上:

void ID3D11DeviceContext::OMSetDepthStencilState(
    ID3D11DepthStencilState *pDepthStencilState,      // [In]深度/模板狀態,使用nullptr的話則是默認深度/模板狀態
    UINT StencilRef);                                 // [In]提供的模板值

如果要恢復到默認狀況,可以這樣調用:

md3dImmediateContext->OMSetDepthStencilState(nullptr, 0);

利用模板測試繪製平面鏡

要實現鏡面反射的效果,我們需要解決兩個問題:

  1. 如何計算出一個物體的所有頂點在任意平面的鏡面的反射位置
  2. 在鏡面位置只顯示鏡面本身和反射的物體的混合

若一個有平面鏡的場景中包含透明和非透明物體,則實際的繪製順序爲:

  1. 只向鏡面區域的模板緩衝區寫入值1,而深度緩衝區和後備緩衝區的值都不應該寫入
  2. 將需要繪製的鏡面反射物體進行反射變換,然後僅在模板值爲1的區域先繪製不透明的反射物體到後備緩衝區
  3. 在模板值爲1的區域繪製透明的反射物體後,再繪製透明鏡面到後備緩衝區
  4. 繪製正常的非透明物體到後備緩衝區
  5. 繪製透明物體到後備緩衝區

在3D場景中,要繪製鏡面反射的物體,我們只需要將原本的物體(所有頂點位置)進行鏡面反射矩陣的變換即可得到。但是反射的物體僅可以在物體一側透過鏡面看到,在鏡面的另一邊是無法看到反射的物體的。通過模板測試,我們可以在攝像機僅與鏡面同側的時候標定鏡面區域,並繪製鏡面反射的物體。

image

我們可以使用XMMatrixReflection函數來創建反射矩陣,提供的參數爲平面向量\((\mathbf{n} ,d)\)

這裏簡單瞭解一下,平面可以表示爲:
\[\mathbf{n} \cdot \mathbf{p} + d = 0\]
n爲平面法向量,p爲平面一點,進行叉乘運算。

例如(0.0f, 0.0f, -1.0f, 10.0f)可以表示z = 10的平面

HLSL代碼的變化

Basic.fx中,添加了一個常量緩衝區用來控制反射開關,它的更新頻率僅次於每次繪製更新的緩衝區。並且由於鏡面是固定的,這裏將鏡面反射矩陣放在不會變化的常量緩衝區上:

cbuffer CBChangesEveryDrawing : register(b0)
{
    row_major matrix gWorld;
    row_major matrix gWorldInvTranspose;
    row_major matrix gTexTransform;
    Material gMaterial;
}

cbuffer CBDrawingState : register(b1)
{
    int gIsReflection;
}

cbuffer CBChangesEveryFrame : register(b2)
{
    row_major matrix gView;
    float3 gEyePosW;
}

cbuffer CBChangesOnResize : register(b3)
{
    row_major matrix gProj;
}

cbuffer CBNeverChange : register(b4)
{
    row_major matrix gReflection;
    DirectionalLight gDirLight[10];
    PointLight gPointLight[10];
    SpotLight gSpotLight[10];
    int gNumDirLight;
    int gNumPointLight;
    int gNumSpotLight;
    float gPad;
}

所以現在目前已經使用了5個常量緩衝區,可以說在管理上會非常複雜,其中頂點着色器需要用到所有的常量緩衝區,而像素着色器需要用到除了CBChangesOnResize外的所有常量緩衝區。

然後3D頂點着色器添加了是否需要乘上反射矩陣的判定:

// Basic_VS_3D.hlsl
#include "Basic.fx"

// 頂點着色器(3D)
VertexOut VS_3D(VertexIn pIn)
{
    VertexOut pOut;
    
    float4 posW = mul(float4(pIn.PosL, 1.0f), gWorld);
    // 若當前在繪製反射物體,先進行反射操作
    [flatten]
    if (gIsReflection)
    {
        posW = mul(posW, gReflection);
    }
    pOut.PosH = mul(mul(posW, gView), gProj);
    pOut.PosW = mul(float4(pIn.Pos, 1.0f), gWorld).xyz;
    pOut.NormalW = mul(pIn.Normal, (float3x3)gWorldInvTranspose);
    pOut.Tex = mul(float4(pIn.Tex, 0.0f, 1.0f), gTexTransform).xy;
    return pOut;
}

對於像素着色器來說,由於點光燈和聚光燈都可以看做是物體,所以也應該進行鏡面反射矩陣變換:

// Basic_PS_3D.hlsl
#include "Basic.fx"

// 像素着色器(3D)
float4 PS_3D(VertexOut pIn) : SV_Target
{
    // 提前進行裁剪,對不符合要求的像素可以避免後續運算
    float4 texColor = tex.Sample(samLinear, pIn.Tex);
    clip(texColor.a - 0.1f);

    // 標準化法向量
    pIn.NormalW = normalize(pIn.NormalW);

    // 頂點指向眼睛的向量
    float3 toEyeW = normalize(gEyePosW - pIn.PosW);

    // 初始化爲0 
    float4 ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 A = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 D = float4(0.0f, 0.0f, 0.0f, 0.0f);
    float4 S = float4(0.0f, 0.0f, 0.0f, 0.0f);
    int i;


    // 強制展開循環以減少指令數
    [unroll]
    for (i = 0; i < gNumDirLight; ++i)
    {
        ComputeDirectionalLight(gMaterial, gDirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    
    [unroll]
    for (i = 0; i < gNumPointLight; ++i)
    {
        PointLight pointLight = gPointLight[i];
        // 若當前在繪製反射物體,需要對光照進行反射矩陣變換
        [flatten]
        if (gIsReflection)
        {
            pointLight.Position = (float3) mul(float4(pointLight.Position, 1.0f), gReflection);
        }

        ComputePointLight(gMaterial, pointLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    
    [unroll]
    for (i = 0; i < gNumSpotLight; ++i)
    {
        SpotLight spotLight = gSpotLight[i];
        // 若當前在繪製反射物體,需要對光照進行反射矩陣變換
        [flatten]
        if (gIsReflection)
        {
            spotLight.Position = (float3) mul(float4(spotLight.Position, 1.0f), gReflection);
        }

        ComputeSpotLight(gMaterial, spotLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
    

    
    float4 litColor = texColor * (ambient + diffuse) + spec;
    litColor.a = texColor.a * gMaterial.Diffuse.a;
    return litColor;
}

RenderStates類的變化

RenderStates類變化如下:

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

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

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

    static ComPtr<ID3D11SamplerState> SSLinear;         // 採樣器狀態:線性過濾
    static ComPtr<ID3D11SamplerState> SSAnistropic;     // 採樣器狀態:各項異性過濾

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

    static ComPtr<ID3D11DepthStencilState> DSSMarkMirror;       // 深度/模板狀態:標記鏡面區域
    static ComPtr<ID3D11DepthStencilState> DSSDrawReflection;   // 深度/模板狀態:繪製反射區域
    static ComPtr<ID3D11DepthStencilState> DSSNoDoubleBlend;    // 深度/模板狀態:無二次混合區域
    static ComPtr<ID3D11DepthStencilState> DSSNoDepthTest;      // 深度/模板狀態:關閉深度測試
    static ComPtr<ID3D11DepthStencilState> DSSNoDepthWrite;     // 深度/模板狀態:僅深度測試,不寫入深度值
};

新增的渲染狀態的定義如下:

void RenderStates::InitAll(ComPtr<ID3D11Device> device)
{
    // ***********初始化光柵化器狀態***********
    D3D11_RASTERIZER_DESC rasterizerDesc;
    ZeroMemory(&rasterizerDesc, sizeof(rasterizerDesc));

    // ...

    // 順時針剔除模式
    rasterizerDesc.FillMode = D3D11_FILL_SOLID;
    rasterizerDesc.CullMode = D3D11_CULL_BACK;
    rasterizerDesc.FrontCounterClockwise = true;
    rasterizerDesc.DepthClipEnable = true;
    HR(device->CreateRasterizerState(&rasterizerDesc, &RSCullClockWise));

    
    // ***********初始化採樣器狀態***********
    // ...
    
    // ***********初始化混合狀態***********
    // ...
    
    // ***********初始化深度/模板狀態***********
    D3D11_DEPTH_STENCIL_DESC dsDesc;

    // 鏡面標記深度/模板狀態
    // 這裏不寫入深度信息
    // 無論是正面還是背面,原來指定的區域的模板值都會被寫入StencilRef
    dsDesc.DepthEnable = true;
    dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
    dsDesc.DepthFunc = D3D11_COMPARISON_LESS;

    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_REPLACE;
    dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
    // 對於背面的幾何體我們是不進行渲染的,所以這裏的設置無關緊要
    dsDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
    dsDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
    dsDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
    dsDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;

    HR(device->CreateDepthStencilState(&dsDesc, DSSMarkMirror.GetAddressOf()));

    // 反射繪製深度/模板狀態
    // 由於要繪製反射鏡面,需要更新深度
    // 僅當鏡面標記模板值和當前設置模板值相等時纔會進行繪製
    dsDesc.DepthEnable = true;
    dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
    dsDesc.DepthFunc = D3D11_COMPARISON_LESS;

    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, DSSDrawReflection.GetAddressOf()));

    // 無二次混合深度/模板狀態
    // 允許默認深度測試
    // 通過自遞增使得原來StencilRef的值只能使用一次,實現僅一次混合
    dsDesc.DepthEnable = true;
    dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
    dsDesc.DepthFunc = D3D11_COMPARISON_LESS;

    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_INCR;
    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_INCR;
    dsDesc.BackFace.StencilFunc = D3D11_COMPARISON_EQUAL;

    HR(device->CreateDepthStencilState(&dsDesc, DSSNoDoubleBlend.GetAddressOf()));

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

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


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

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

}

場景繪製

現在場景內有四面牆,一個平面鏡,一面地板,一個籬笆盒和水面。

開始繪製前,我們需要清空深度/模板緩衝區和渲染目標視圖:

md3dImmediateContext->ClearRenderTargetView(mRenderTargetView.Get(), reinterpret_cast<const float*>(&Colors::Black));
md3dImmediateContext->ClearDepthStencilView(mDepthStencilView.Get(), D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

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

這一步通過對鏡面所在區域寫入模板值1來標定鏡面繪製區域。

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

// 裁剪掉背面三角形
// 標記鏡面區域的模板值爲1
// 不寫入像素顏色
md3dImmediateContext->RSSetState(nullptr);
md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSMarkMirror.Get(), 1);
md3dImmediateContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);

mMirror.Draw(md3dImmediateContext);

通過VS圖形調試器可以看到模板值爲1的區域

第2步:繪製不透明的鏡面反射物體

理論上會有三面牆和地板可能會透過鏡面看到,這裏都需要繪製,但要注意在對頂點位置做反射變換時,並沒有對法向量做反射變換。並且原來按順時針排布的三角形頂點也變成了逆時針排布。所以需要對順時針排布的頂點做裁剪處理。

image

在做模板測試的時候,我們僅對模板值爲1的像素點通過測試,這樣保證限定繪製區域在鏡面上。

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

// 開啓反射繪製
XMINT4 reflectionState = { 1, 0, 0, 0 };
md3dImmediateContext->UpdateSubresource(mConstantBuffers[1].Get(), 0, nullptr, &reflectionState, 0, 0);
    
// 繪製不透明物體,需要順時針裁剪
// 僅對模板值爲1的鏡面區域繪製
md3dImmediateContext->RSSetState(RenderStates::RSCullClockWise.Get());
md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawReflection.Get(), 1);
md3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
    
mWalls[2].Draw(md3dImmediateContext);
mWalls[3].Draw(md3dImmediateContext);
mWalls[4].Draw(md3dImmediateContext);
mFloor.Draw(md3dImmediateContext);

到這時候繪製效果如下:

第3步:繪製透明的鏡面反射物體

這一步需要繪製的透明反射物體有籬笆盒以及水面,繪製了這些透明物體後就可以連同鏡面一起混合繪製了。其中籬笆盒要優於水面先行繪製:

// ***********************
// 3. 繪製透明的反射物體
//

// 關閉順逆時針裁剪
// 僅對模板值爲1的鏡面區域繪製
// 透明混合
md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawReflection.Get(), 1);
md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);

mWireFence.Draw(md3dImmediateContext);
mWater.Draw(md3dImmediateContext);
mMirror.Draw(md3dImmediateContext);
    
// 關閉反射繪製
reflectionState.x = 0;
md3dImmediateContext->UpdateSubresource(mConstantBuffers[1].Get(), 0, nullptr, &reflectionState, 0, 0);

繪製完後效果如下:

第4步:繪製不透明的正常物體

這一步僅有牆體和地板需要繪製:

// ************************
// 4. 繪製不透明的正常物體
//

md3dImmediateContext->RSSetState(nullptr);
md3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
md3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);

for (auto& wall : mWalls)
    wall.Draw(md3dImmediateContext);
mFloor.Draw(md3dImmediateContext);

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

// ***********************
// 5. 繪製透明的正常物體
//

// 關閉順逆時針裁剪
// 透明混合
md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
md3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);

mWireFence.Draw(md3dImmediateContext);
mWater.Draw(md3dImmediateContext);

完成所有繪製後,顯示效果如下:

先繪製鏡面場景還是繪製主場景?

一開始我是根據龍書的順序先繪製主場景,再繪製鏡面場景的。但是在繪製帶有透明物體的場景時,會得到下面的結果:

可以看到鏡面下面的部分有黑邊,是因爲在繪製主場景的時候,黑色背景和水面產生了混合,並且改寫了深度值,導致在繪製鏡面後面的物體(主要是地板部分)時水面以下的部分沒有通過深度測試,地板也就沒有被繪製。

DirectX11 With Windows SDK完整目錄

Github項目源碼

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