DirectX11 With Windows SDK--12 深度/模板狀態、平面鏡反射繪製

前言

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

DirectX11 With Windows SDK完整目錄git

Github項目源碼github

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。數組

深度/模板測試

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

深度測試

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

模板測試

除了深度測試覺得,咱們還能夠設定模板測試來阻擋某些特定的區域的像素經過後備緩衝區。並且模板測試在操做上自由度會比深度測試大。對於須要進行模板測試的像素,比較式以下:
(StencilRef & StencilReadMask) ⊴ (Value & StencilReadMask)測試

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

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

深度/模板格式

深度/模板緩衝區是一個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結構體:

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;

深度狀態設定

  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爲平面一點,進行叉乘運算。

d是一個有向距離值

上面的式子展開後就是咱們高數見到的平面方程:
\[Ax + By + Cz + D = 0\]

這至關於我

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

HLSL代碼的變化

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

cbuffer CBChangesEveryDrawing : register(b0)
{
    matrix g_World;
    matrix g_WorldInvTranspose;
    Material g_Material;
}

cbuffer CBDrawingStates : register(b1)
{
    int g_IsReflection;
    float3 g_Pad1;
}

cbuffer CBChangesEveryFrame : register(b2)
{
    matrix g_View;
    float3 g_EyePosW;
}

cbuffer CBChangesOnResize : register(b3)
{
    matrix g_Proj;
}

cbuffer CBChangesRarely : register(b4)
{
    matrix g_Reflection;
    DirectionalLight g_DirLight[10];
    PointLight g_PointLight[10];
    SpotLight g_SpotLight[10];
    int g_NumDirLight;
    int g_NumPointLight;
    int g_NumSpotLight;
    float g_Pad2;
}

因此如今目前已經使用了5個常量緩衝區,能夠說在管理上會很是複雜,其中頂點着色器須要用到全部的常量緩衝區,而像素着色器須要用到除了CBChangesOnResize外的全部常量緩衝區。

而後3D頂點着色器添加了是否須要乘上反射矩陣的斷定:

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

// 頂點着色器(3D)
VertexPosHWNormalTex VS_3D(VertexPosNormalTex vIn)
{
    VertexPosHWNormalTex vOut;
    
    matrix viewProj = mul(g_View, g_Proj);
    float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);
    float3 normalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
    // 若當前在繪製反射物體,先進行反射操做
    [flatten]
    if (g_IsReflection)
    {
        posW = mul(posW, g_Reflection);
        normalW = mul(normalW, (float3x3) g_Reflection);
    }
    vOut.PosH = mul(posW, viewProj);
    vOut.PosW = posW.xyz;
    vOut.NormalW = normalW;
    vOut.Tex = vIn.Tex;
    return vOut;
}

對於像素着色器來講,因爲點光燈和聚光燈均可以看作是物體,因此也應該進行鏡面反射矩陣變換(主要反射光的方向和位置):

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

// 像素着色器(3D)
float4 PS_3D(VertexPosHWNormalTex pIn) : SV_Target
{
    // 提早進行裁剪,對不符合要求的像素能夠避免後續運算
    float4 texColor = g_Tex.Sample(g_SamLinear, pIn.Tex);
    clip(texColor.a - 0.1f);

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

    // 頂點指向眼睛的向量
    float3 toEyeW = normalize(g_EyePosW - 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 < 5; ++i)
    {
        DirectionalLight dirLight = g_DirLight[i];
        [flatten]
        if (g_IsReflection)
        {
            dirLight.Direction = mul(dirLight.Direction, (float3x3) (g_Reflection));
        }
        ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
        
    

    
    // 若當前在繪製反射物體,須要對光照進行反射矩陣變換
    PointLight pointLight;
    [unroll]
    for (i = 0; i < 5; ++i)
    {
        pointLight = g_PointLight[i];
        [flatten]
        if (g_IsReflection)
        {
            pointLight.Position = (float3) mul(float4(pointLight.Position, 1.0f), g_Reflection);
        }
        ComputePointLight(g_Material, pointLight, pIn.PosW, pIn.NormalW, toEyeW, A, D, S);
        ambient += A;
        diffuse += D;
        spec += S;
    }
        
    
    
    SpotLight spotLight;
    // 若當前在繪製反射物體,須要對光照進行反射矩陣變換
    [unroll]
    for (i = 0; i < 5; ++i)
    {
        spotLight = g_SpotLight[i];
        [flatten]
        if (g_IsReflection)
        {
            spotLight.Position = (float3) mul(float4(spotLight.Position, 1.0f), g_Reflection);
            spotLight.Direction = mul(spotLight.Direction, (float3x3) g_Reflection);
        }
        ComputeSpotLight(g_Material, 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 * g_Material.Diffuse.a;
    return litColor;
}

RenderStates類的變化

RenderStates類變化以下:

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

    static void InitAll(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(ID3D11Device * device)
{
    // 先前初始化過的話就不必重來了
    if (IsInit())
        return;

    // ***********初始化光柵化器狀態***********
    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
// 不寫入像素顏色
m_pd3dImmediateContext->RSSetState(nullptr);
m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSMarkMirror.Get(), 1);
m_pd3dImmediateContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);

m_Mirror.Draw(m_pd3dImmediateContext.Get());

經過VS圖形調試器能夠看到模板值爲1的區域

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

理論上會有三面牆和地板可能會透過鏡面看到,這裏都須要繪製,但要注意在對頂點位置作反射變換時,原來平面向外的法向量變成了平面向內部,所以還須要額外對法向量作反射變換(龍書缺乏了對法向量的反射變換)。而且原來按順時針排布的三角形頂點也變成了逆時針排布。因此須要對順時針排布的頂點作裁剪處理。

image

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

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

// 開啓反射繪製
m_CBStates.isReflection = true;
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(CBDrawingStates), &m_CBStates, sizeof(CBDrawingStates));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0);  
        
// 繪製不透明物體,須要順時針裁剪
// 僅對模板值爲1的鏡面區域繪製
m_pd3dImmediateContext->RSSetState(RenderStates::RSCullClockWise.Get());
m_pd3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawReflection.Get(), 1);
m_pd3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
    
m_Walls[2].Draw(m_pd3dImmediateContext.Get());
m_Walls[3].Draw(m_pd3dImmediateContext.Get());
m_Walls[4].Draw(m_pd3dImmediateContext.Get());
m_Floor.Draw(m_pd3dImmediateContext.Get());

到這時候繪製效果以下:

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

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

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

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

m_WireFence.Draw(m_pd3dImmediateContext.Get());
m_Water.Draw(m_pd3dImmediateContext.Get());
m_Mirror.Draw(m_pd3dImmediateContext.Get());
    
// 關閉反射繪製
m_CBStates.isReflection = false;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(CBDrawingStates), &m_CBStates, sizeof(CBDrawingStates));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0);

繪製完後效果以下:

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

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

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

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

for (auto& wall : m_Walls)
    wall.Draw(m_pd3dImmediateContext.Get());
m_Floor.Draw(m_pd3dImmediateContext.Get());

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

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

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

m_WireFence.Draw(m_pd3dImmediateContext.Get());
m_Water.Draw(m_pd3dImmediateContext.Get());

完成全部繪製後,顯示效果以下:

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

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

能夠看到鏡面下面的部分有黑邊,是由於在繪製主場景的時候,黑色背景和水面產生了混合,而且改寫了深度值,致使在繪製鏡面後面的物體(主要是地板部分)時水面如下的部分沒有經過深度測試,地板也就沒有被繪製。

DirectX11 With Windows SDK完整目錄

Github項目源碼

歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。

相關文章
相關標籤/搜索