DirectX11 With Windows SDK--31 陰影映射

前言

陰影既暗示着光源相對於觀察者的位置關係,也從側面傳達了場景中各物體之間的相對位置。本章將起底最基礎的陰影映射算法,而像複雜如級聯陰影映射這樣的技術,也是在陰影映射的基礎上發展而來的。html

學習目標:git

  1. 掌握基本的陰影映射算法
  2. 熟悉投影紋理貼圖的工做原理
  3. 瞭解陰影圖走樣的問題並學習修正該問題的經常使用策略

DirectX11 With Windows SDK完整目錄github

Github項目源碼算法

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

核心思想

陰影映射技術的核心思想其實不復雜。對於場景中的一點,若是該點可以被攝像機觀察到,卻不能被光源定義的虛擬攝像機所觀察到,那麼場景中的這一點則能夠被斷定爲光源所照射不到的陰影區域。緩存

如下圖爲例,眼睛觀察到地面上最左邊的一點,而且從光源處觀察也能看到該點。所以該點不會產生陰影。ide

再看下面的圖,眼睛能夠觀察到地面上中間那一點,可是從光源處觀察不能看到該點。所以該點會產生陰影。函數

具體落實下來應該怎麼作呢?對於點光源來講,因爲它的光是朝全部方向四射散開的,但爲了方便,咱們能夠像攝像機那樣選取視錐體區域(使用一個觀察矩陣 + 透視投影矩陣來定義),而後通過正常的變換後就能計算出光源到區域內物體的深度值;而對於平行光(方向光)來講,咱們能夠採用正交投影的方式來選取一個矩形區域(使用一個觀察矩陣 + 正交投影矩陣定義)。和通常的渲染流程不一樣的是,咱們只須要記錄深度值到深度緩衝區,而不須要將顏色繪製到後備緩衝區。學習

陰影貼圖

陰影貼圖技術也是一種變相的「渲染到紋理」技術。它以光源的視角來渲染場景深度信息,即在光源處有一個虛擬攝像機,它將觀察到的物體的深度信息保存到深度緩衝區中。這樣咱們就能夠知道那些離光源最近的像素片元信息,同時這些點天然是不在陰影範圍之中。測試

一般該技術須要用到一個深度/模板緩衝區、一個與之對應的視口、針對該深度/模板緩衝區的着色器資源視圖(SRV)和深度/模板視圖(DSV),而用於陰影貼圖的那個深度/模板緩衝區也被稱爲陰影貼圖

光源的投影

在考慮點光源的投影和方向光的投影時可能會有些困難,但這兩個問題其實能夠轉化成虛擬攝像機的透視投影和正交投影。

對於透視投影來講,其實咱們也已經很是熟悉了。在這種作法下咱們只考慮虛擬攝像機的視錐體區域(即儘管點光源是朝任意方向照射的,但咱們只看點光源往該視錐體範圍內照射的區域),而後對物體慣例進行世界變換、以光源爲視角的觀察變換、光源的透視投影變換,這樣物體就被變換到了以光源爲視角的NDC空間。

而對於正交投影而言,咱們也是同樣的作法。正交投影的視景體是一個軸對齊於觀察座標系的長方體。儘管咱們很差描述一個方向光的光源,但爲了方便,咱們把光源定義在視景體xOy切面中心所處的那條直線上。這樣咱們就只須要給出視景體的寬度、高度、近平面、遠平面信息就能夠構造出一個正交投影矩陣了。

咱們能夠看到,正交投影的投影線均平行於觀察空間的z軸。

正交投影矩陣在第四章變換已經講過,就再也不贅述。

投影紋理座標

投影紋理貼圖技術可以將紋理投射到任意形狀的幾何體上,又由於其原理與投影機的工做方式比較類似,由此得名。例以下圖中,右邊的骷髏頭紋理被投射到左邊場景中的多個幾何體上。

投影紋理貼圖的關鍵在於爲每一個像素生成對應的投影紋理座標,從視覺上給人一種紋理被投射到幾何體上的感受。

下圖是光源觀察的視野,其中點p是待渲染的一點,而紋理座標(u, v)則指定了應當被投射到3D點p上的紋素,而且座標(u, v)與投影到屏幕上的NDC座標有特定聯繫。咱們能夠將投影紋理座標的生成過程分爲以下步驟:

  1. 將3D空間中一點p投影到光源的投影窗口,並將其變換到NDC空間。
  2. 將投影座標從NDC空間變換到紋理空間,以此將它們轉換爲紋理座標

而步驟2中的變換過程則取決於下面的座標變換:

\[u=0.5x+0.5\\ v=-0.5y+0.5 \]

即從x, y∈[-1, 1]映射到u, v∈[0, 1]。(y軸和v軸是相反的)

這種線性變換能夠用矩陣表示:

\[\mathbf{T}=\begin{bmatrix} 0.5 & 0 & 0 & 0 \\ 0 & -0.5 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0.5 & 0.5 & 0 & 1 \\ \end{bmatrix}\\ \begin{bmatrix} x & y & 0 & 1 \end{bmatrix}\mathbf{T}=\begin{bmatrix} u & v & 0 & 1 \end{bmatrix} \]

那麼物體上的一點p從局部座標系到最終的紋理座標點t的變換過程爲:

\[\mathbf{p}\mathbf{W_{Obj}}\mathbf{V_{Light}}\mathbf{P_{Light}}\mathbf{T}=\mathbf{t} \]

這裏補上了世界變換矩陣,是由於這一步容易在後面的代碼實踐中被漏掉。但此時的t還須要通過透視除法,纔是咱們最終須要的紋理座標。

代碼實現

下面的HLSL代碼展現了頂點着色器計算投影紋理座標的過程:

// 頂點着色器
VertexPosHWNormalTexShadowPosH VS(VertexPosNormalTex vIn)
{
    VertexPosHWNormalTexShadowPosH vOut;
    
    matrix viewProj = mul(g_View, g_Proj);
    vector posW = mul(float4(vIn.PosL, 1.0f), g_World);

    vOut.PosW = posW.xyz;
    
    // ...
    
    // 把頂點變換到光源的投影空間
    vOut.ShadowPosH = mul(posW, g_ShadowTransform);
    return vOut;
}

// 像素着色器
float4 PS(VertexPosHWNormalTexShadowPosH pIn) : SV_Target
{
    // 透視除法
    pIn.ShadowPosH.xyz /= pIn.ShadowPosH.w;
    
    // NDC空間中的深度值
    float depth = pIn.ShadowPosH.z;
    
    // 經過投影紋理座標來對紋理採樣
    // 採樣出的r份量即爲光源觀察該點時的深度值
    float4 c = g_ShadowMap.Sample(g_Sam, pIn.ShadowPosH.xy);
    
    // ...
}

視錐體以外的點

在渲染管線中,位於視錐體以外的幾何體是要被裁剪掉的。可是,在咱們以光源設置的視角投影幾何體而爲之生成投影紋理座標時,並不須要執行裁剪操做——只須要簡單投影頂點便可。所以,位於視錐體以外的幾何體頂點會獲得[0, 1]區間以外的投影紋理座標。而後具體的採樣行爲則須要依賴於咱們設置的採樣器。

通常來講,咱們並不但願對位於視錐體外的幾何體頂點進行貼圖,由於這並無任何意義。考慮到可視深度在NDC空間的最大值爲1.0f,咱們能夠採用邊界深度值爲1.0f的邊框尋址模式

另外一種作法則是結合聚光燈的策略,使聚光燈照射範圍以外的部分不受光照,亦即不在陰影的計算範圍內。

透視除法與投影的其餘問題

來到正交投影,由於咱們依然是要計算出NDC座標,對於NDC空間範圍外的點,咱們依然能夠採用上面的尋址模式策略,但聚光燈的策略就不適用了。

此外,正交投影無需進行透視除法,由於正交投影后的座標w值老是1.0f。但保留透視除法可讓咱們的這套着色器能夠同時工做在正交投影和透視投影上。若是沒有透視除法,則只能在正交投影中工做。

算法思路

  1. 從光源的視角將場景深度以「渲染到紋理」的形式繪製到名爲陰影貼圖的深度緩衝區中
  2. 從玩家攝像機的視角渲染場景,計算出該點在光源視角下NDC座標,其中z值爲深度值,記爲d(p)
  3. 上面算出的NDC座標的xy份量變換爲陰影貼圖的紋理座標uv,而後進行深度值採樣,獲得s(p)
  4. 當d(p) > s(p)時, 像素p位於陰影範圍以內;天然相反地,當d(p) <= s(p)時,像素p位於陰影範圍以外(至於爲何還有<,後面會提到)

改進TextureRender

既然陰影貼圖和RTT有着許多類似的地方,那何不把它也放到TextureRender裏面共用呢?只要添加一個開關控制該RTT是否用做陰影貼圖便可。

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

    TextureRender() = default;
    ~TextureRender() = default;
    // 不容許拷貝,容許移動
    TextureRender(const TextureRender&) = delete;
    TextureRender& operator=(const TextureRender&) = delete;
    TextureRender(TextureRender&&) = default;
    TextureRender& operator=(TextureRender&&) = default;


    HRESULT InitResource(ID3D11Device* device,
        int texWidth,
        int texHeight,
        bool shadowMap = false,
        bool generateMips = false);

    // 開始對當前紋理進行渲染
    // 陰影貼圖無需提供背景色
    void Begin(ID3D11DeviceContext* deviceContext, const FLOAT backgroundColor[4]);
    // 結束對當前紋理的渲染,還原狀態
    void End(ID3D11DeviceContext * deviceContext);
    // 獲取渲染好的紋理的着色器資源視圖
    // 陰影貼圖返回的是深度緩衝區
    // 引用數不增長,僅用於傳參
    ID3D11ShaderResourceView* GetOutputTexture();

    // 設置調試對象名
    void SetDebugObjectName(const std::string& name);

private:
    ComPtr<ID3D11ShaderResourceView>        m_pOutputTextureSRV;          // 輸出的紋理(或陰影貼圖)對應的着色器資源視圖
    ComPtr<ID3D11RenderTargetView>          m_pOutputTextureRTV;          // 輸出的紋理對應的渲染目標視圖
    ComPtr<ID3D11DepthStencilView>          m_pOutputTextureDSV;          // 輸出紋理所用的深度/模板視圖(或陰影貼圖)
    D3D11_VIEWPORT                          m_OutputViewPort = {};        // 輸出所用的視口

    ComPtr<ID3D11RenderTargetView>          m_pCacheRTV;                  // 臨時緩存的後備緩衝區
    ComPtr<ID3D11DepthStencilView>          m_pCacheDSV;                  // 臨時緩存的深度/模板緩衝區
    D3D11_VIEWPORT                          m_CacheViewPort = {};         // 臨時緩存的視口

    bool                                    m_GenerateMips = false;       // 是否生成mipmap鏈
    bool                                    m_ShadowMap = false;          // 是否爲陰影貼圖

};

在做爲RTT時,須要建立紋理與它的SRV和RTV、深度/模板緩衝區和它的DSV、視口

而做爲陰影貼圖時,須要建立深度緩衝區與它的SRV和DSV、視口

下面的代碼只關注建立陰影貼圖的部分:

HRESULT TextureRender::InitResource(ID3D11Device* device, int texWidth, int texHeight, bool shadowMap, bool generateMips)
{
    // 防止重複初始化形成內存泄漏
    m_pOutputTextureSRV.Reset();
    m_pOutputTextureRTV.Reset();
    m_pOutputTextureDSV.Reset();
    m_pCacheRTV.Reset();
    m_pCacheDSV.Reset();

    m_ShadowMap = shadowMap;
    m_GenerateMips = false;
    HRESULT hr;
    
    // ...
    
    // ******************
    // 建立與紋理等寬高的深度/模板緩衝區或陰影貼圖,以及對應的視圖
    //
    CD3D11_TEXTURE2D_DESC texDesc((m_ShadowMap ? DXGI_FORMAT_R24G8_TYPELESS : DXGI_FORMAT_D24_UNORM_S8_UINT),
        texWidth, texHeight, 1, 1,
        D3D11_BIND_DEPTH_STENCIL | (m_ShadowMap ? D3D11_BIND_SHADER_RESOURCE : 0));

    ComPtr<ID3D11Texture2D> depthTex;
    hr = device->CreateTexture2D(&texDesc, nullptr, depthTex.GetAddressOf());
    if (FAILED(hr))
        return hr;

    CD3D11_DEPTH_STENCIL_VIEW_DESC dsvDesc(depthTex.Get(), D3D11_DSV_DIMENSION_TEXTURE2D, DXGI_FORMAT_D24_UNORM_S8_UINT);

    hr = device->CreateDepthStencilView(depthTex.Get(), &dsvDesc,
        m_pOutputTextureDSV.GetAddressOf());
    if (FAILED(hr))
        return hr;

    if (m_ShadowMap)
    {
        // 陰影貼圖的SRV
        CD3D11_SHADER_RESOURCE_VIEW_DESC srvDesc(depthTex.Get(), D3D11_SRV_DIMENSION_TEXTURE2D, DXGI_FORMAT_R24_UNORM_X8_TYPELESS);

        hr = device->CreateShaderResourceView(depthTex.Get(), &srvDesc,
            m_pOutputTextureSRV.GetAddressOf());
        if (FAILED(hr))
            return hr;
    }

    // ******************
    // 初始化視口
    //
    m_OutputViewPort.TopLeftX = 0.0f;
    m_OutputViewPort.TopLeftY = 0.0f;
    m_OutputViewPort.Width = static_cast<float>(texWidth);
    m_OutputViewPort.Height = static_cast<float>(texHeight);
    m_OutputViewPort.MinDepth = 0.0f;
    m_OutputViewPort.MaxDepth = 1.0f;

    return S_OK;
}

須要注意的是,在建立深度緩衝區時,若是還想爲他建立SRV,就不能將DXGI格式定義成DXGI_FORMAT_D24_UNORM_S8_UINT這些帶D的類型,而應該是DXGI_FORMAT_R24G8_TYPELESS

而後在建立陰影貼圖的SRV時,則須要指定爲DXGI_FORMAT_R24_UNORM_X8_TYPELESS

開始陰影貼圖的渲染前,不須要設置RTV,只須要綁定DSV。

void TextureRender::Begin(ID3D11DeviceContext* deviceContext, const FLOAT backgroundColor[4])
{
    // 緩存渲染目標和深度模板視圖
    deviceContext->OMGetRenderTargets(1, m_pCacheRTV.GetAddressOf(), m_pCacheDSV.GetAddressOf());
    // 緩存視口
    UINT num_Viewports = 1;
    deviceContext->RSGetViewports(&num_Viewports, &m_CacheViewPort);

    // 清空緩衝區
    // ... 
    deviceContext->ClearDepthStencilView(m_pOutputTextureDSV.Get(), D3D11_CLEAR_DEPTH | (m_ShadowMap ? 0 : D3D11_CLEAR_STENCIL), 1.0f, 0);
    
    // 設置渲染目標和深度模板視圖
    deviceContext->OMSetRenderTargets((m_ShadowMap ? 0 : 1), 
        (m_ShadowMap ? nullptr : m_pOutputTextureRTV.GetAddressOf()), 
        m_pOutputTextureDSV.Get());
    // 設置視口
    deviceContext->RSSetViewports(1, &m_OutputViewPort);
}

渲染完成後,和往常同樣還原便可。

偏移與走樣

陰影圖存儲的是距離光源最近的可視像素深度值,可是它的分辨率有限,致使每個陰影圖紋素都要表示場景中的一片區域。所以,陰影圖只是以光源視角針對場景深度進行的離散採樣,這將會致使所謂的陰影粉刺等圖像走樣問題。以下圖所示(注意圖中地面上光影之間輪流交替的「階梯狀」條紋):

而下圖則簡單展現了爲何會發生陰影粉刺這種現象。因爲陰影圖的分辨率有限,因此每一個陰影圖紋素要對應於長江中的一塊區域(而不是點對點的關係,一個坡面表明陰影圖中一個紋素的對應範圍)。從觀察點E查看場景中的兩個點p1與p2,它們分別對應於兩個不一樣的屏幕像素。可是,從光源的觀察角度來看,它們卻都有着相同的陰影圖紋素(即s(p1)=s(p2)=s,因爲分辨率的緣由)。當咱們在執行陰影圖檢測時,會獲得d(p1) > s 及 d(p2) <= s這兩個測試結果,這樣一來,p1將會被繪製爲如同它在陰影中的顏色,p2將被渲染爲好似它在陰影以外的顏色,從而致使陰影粉刺。

所以,咱們能夠經過偏移陰影圖中的深度值來防止出現錯誤的陰影效果。此時咱們就能夠保證d(p1) <= s 及 d(p2) <= s。可是尋找合適的深度偏移須要反覆嘗試。

偏移量過大會致使名爲peter-panning(彼得·潘,即小飛俠,他曾在一次逃跑時弄丟了本身的影子)的失真效果,使得陰影看起來與物體相分離。

然而,並無哪種固定的偏移量能夠正確地運用於全部幾何體的陰影繪製。特別是下圖那種(從光源的角度來看)有着極大斜率的三角形,這時候就須要選取更大的偏移量。可是,若是試圖經過一個過大的深度偏移量來處理全部的斜邊,則又會形成peter-panning問題。

所以,咱們繪製陰影的方式就是先以光源視角度量多邊形斜面的斜率,併爲斜率較大的多邊形應用更大的偏移量。而圖形硬件內部對此有相關技術的支持,咱們經過名爲斜率縮放偏移的光柵化狀態屬性就可以輕鬆實現。

typedef struct D3D11_RASTERIZER_DESC {
    // ...
    INT             DepthBias;
    FLOAT           DepthBiasClamp;
    FLOAT           SlopeScaledDepthBias;
    BOOL            DepthClipEnable;
    // ...
} D3D11_RASTERIZER_DESC;
  1. DepthBias:一個固定的應用偏移量。
  2. DepthBiasClamp:所容許的最大深度偏移量。以此來設置深度偏移量的上限。不難想象,及其陡峭的傾斜度會致使斜率縮放偏移量過大,從而形成peter-panning失真
  3. SlopeScaledDepthBias:根據多邊形的斜率來控制偏移程度的縮放因子。

注意,在將場景渲染至陰影貼圖時,便會應用該斜率縮放偏移量。這是因爲咱們但願以光源的視角基於多邊形的斜率而進行偏移操做,從而避免陰影失真。所以,咱們就會對陰影圖中的數值進行偏移計算(即由硬件將像素的深度值與偏移值相加)。在本Demo中採用的具體數值以下:

// [出自MSDN]
// 若是當前的深度緩衝區採用UNORM格式而且綁定在輸出合併階段,或深度緩衝區尚未被綁定
// 則偏移量的計算過程以下:
//
// Bias = (float)DepthBias * r + SlopeScaledDepthBias * MaxDepthSlope;
//
// 這裏的r是在深度緩衝區格式轉換爲float32類型後,其深度值可取到大於0的最小可表示的值
// MaxDepthSlope則是像素在水平方向和豎直方向上的深度斜率的最大值
// [結束MSDN引用]
//
// 對於一個24位的深度緩衝區來講, r = 1 / 2^24
//
// 例如:DepthBias = 100000 ==> 實際的DepthBias = 100000/2^24 = .006
//
// 本Demo中的方向光始終與地面法線呈45度夾角,故取斜率爲1.0f
// 如下數據極其依賴於實際場景,所以咱們須要對特定場景反覆嘗試才能找到最合適
rsDesc.DepthBias = 100000;
rsDesc.DepthBiasClamp = 0.0f;
rsDesc.SlopeScaledDepthBias = 1.0f

注意:深度偏移發生在光柵化期間(裁剪以後),所以不會對幾何體裁剪形成影響。

RenderStates中咱們添加了這樣一個光柵化狀態:

// 深度偏移模式
rasterizerDesc.FillMode = D3D11_FILL_SOLID;
rasterizerDesc.CullMode = D3D11_CULL_BACK;
rasterizerDesc.FrontCounterClockwise = false;
rasterizerDesc.DepthClipEnable = true;
rasterizerDesc.DepthBias = 100000;
rasterizerDesc.DepthBiasClamp = 0.0f;
rasterizerDesc.SlopeScaledDepthBias = 1.0f;
HR(device->CreateRasterizerState(&rasterizerDesc, RSDepth.GetAddressOf()));

MSDN文檔Depth Bias講述了該技術相關的所有規則,而且介紹瞭如何使用浮點深度緩衝區進行工做。

百分比漸近過濾(PCF)

在使用投影紋理座標(u, v)對陰影圖進行採樣時,每每不會命中陰影圖中紋素的準確位置,而是一般位於陰影圖中的4個紋素之間。然而,咱們不該該對深度值採用雙線性插值法,由於4個紋素之間的深度值不必定知足線性過渡,插值出來的深度值跟實際的深度值有誤差,這樣可能會致使把像素錯誤標入陰影中這樣的錯誤結果(所以咱們也不能爲陰影圖生成mipmap)。

出於這樣的緣由,咱們應該對採樣的結果進行插值,而不是對深度值進行插值。這種作法稱爲——百分比漸近過濾。即咱們以點過濾(MIN_MAG_MIP_POINT)的方式在座標(u, v)、(u+△x, v)、(u, v+△x)、(u+△x, v+△x)處對紋理進行採樣,其中△x=1/SHADOW_MAP_SIZE(除以的是引用貼圖的寬高)。因爲是點採樣,這4個採樣點分別命中的是圍繞座標(u, v)最近的4個陰影圖紋素s0、s一、s二、s3,以下圖所示。

接下來,咱們會對這些採集的深度值進行陰影圖檢測,並對測試的結果展開雙線性插值。

static const float SMAP_SIZE = 2048.0f;
static const float SMAP_DX = 1.0f / SMAP_SIZE;

// ...

//
// 採樣操做
//

// 對陰影圖進行採樣以獲取離光源最近的深度值
float s0 = g_ShadowMap.Sample(g_SamShadow, tex.xy).r;
float s1 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(SMAP_DX, 0)).r;
float s2 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(0, SMAP_DX)).r;
float s3 = g_ShadowMap.Sample(g_SamShadow, tex.xy + float2(SMAP_DX, SMAP_DX)).r;

// 該像素的深度值是否小於等於陰影圖中的深度值
float r0 = (depth <= s0);
float r1 = (depth <= s1);
float r2 = (depth <= s2);
float r3 = (depth <= s3);

//
// 雙線性插值操做
//

// 變換到紋素空間
float2 texelPos = SMAP_SIZE * tex.xy;

// 肯定插值係數(frac()返回浮點數的小數部分)
float2 t = frac(texelPos);

// 對比較結果進行雙線性插值
return lerp(lerp(r0, r1, t.x), lerp(r2, r3, t.x), t.y);

若採用這種計算方法,則一個像素就可能局部處於陰影之中,而不是非0即1.例如,如有4個樣本,三個在陰影中,一個在陰影外,那麼該像素有75%處於陰影之中。這就讓陰影內外的像素之間有了更加平滑的過渡,而不是棱角分明。

但這種過濾方法產生的陰影看起來仍然很是生硬,且鋸齒失真問題的最終處理效果仍是不能使人十分滿意。PCF的主要缺點是須要4個紋理樣本,而紋理採樣自己就是現代GPU代價較高的操做之一,由於存儲器的帶寬與延遲並無隨着GPU計算能力的劇增而獲得相近程度的巨大改良。幸運的是,Direct3D 11+版本的圖形硬件對PCF技術已經有了內部支持,上面的一大堆代碼能夠用SampleCmpLevelZero函數來替代。

float percentage = g_ShadowMap.SampleCmpLevelZero(g_SamShadow, shadowPosH.xy, depth).r;

方法中的LevelZero部分意味着它只能在最高的mipmap層級中進行採樣。另外,該方法使用的並不是通常的採樣器對象,而是比較採樣器。這使得硬件可以執行陰影圖的比較測試,而且須要在過濾採樣結果以前完成。對於PCF技術來講,咱們須要使用的是D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT過濾器,並將比較函數設置爲LESS_EQUAL(因爲對深度值進行了偏移,因此也要用到LESS比較函數)。

函數中傳入的depth將會出如今比較運算符的左邊,即:

depth <= sampleDepth

RenderStates中咱們添加了這樣一個採樣器:

ComPtr<ID3D11SamplerState> RenderStates::SSShadow = nullptr;

// 採樣器狀態:深度比較與Border模式
ZeroMemory(&sampDesc, sizeof(sampDesc));
sampDesc.Filter = D3D11_FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT;
sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_BORDER;
sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_BORDER;
sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_BORDER;
sampDesc.ComparisonFunc = D3D11_COMPARISON_LESS_EQUAL;
sampDesc.BorderColor[0] = { 1.0f };
sampDesc.MinLOD = 0;
sampDesc.MaxLOD = D3D11_FLOAT32_MAX;
HR(device->CreateSamplerState(&sampDesc, SSShadow.GetAddressOf()));

注意:根據SDK文檔所述,只有R32_FLOAT_X8X24_TYPELESSR32_FLOATR24_UNORM_X8_TYPELESSR16_UNORM格式才能用於比較過濾器。

在PCF的基礎上進行均值濾波

到目前爲止,咱們在本節中一直使用的是4-tap PCF核(輸入4個樣原本執行的PCF)。PCF核越大,陰影的邊緣輪廓也就越豐滿、越平滑,固然,花費在SampleCmpLevelZero函數上的開銷也就越大。在本Demo中,咱們是按3x3正方形的均值濾波方式來執行PCF。因爲每次調用SampleCmpLevelZero函數實際所執行的都是4-tap PCF,因此一共採樣了36次,其中有4x4個獨立採樣點。此外,採用過大的濾波核還會致使以前所述的陰影粉刺問題,但本章不打算講述,有興趣能夠回到龍書閱讀(過大的PCF核)。

顯然,PCF技術通常來講只需在陰影的邊緣進行,由於陰影內外兩部分並不涉及混合操做(只有陰影邊緣纔是漸變的)。基於此,只要能對陰影邊緣的PCF設計相應的處理方案就行了。但這種作法通常要求咱們所用的PCF核足夠大(5x5及更大)時才划算(由於動態分支也有開銷)。不過最終是要效率仍是要畫質仍是取決於你本身。

注意:實際工程中所用的PCF核不必定是方形的過濾柵格。很多文獻也指出,隨機的拾取點也能夠做爲PCF核。

考慮到在作比較時,若是處於陰影外的值爲1,在陰影內的值爲0,在採用SampleCmpLevelZero和均值濾波後,咱們用範圍值0~1來表示處於陰影外的程度。隨着值的增長,該點也變得越亮。咱們可使用下面的函數來計算3x3正方形的均值濾波下的陰影因子:

float CalcShadowFactor(SamplerComparisonState samShadow, Texture2D shadowMap, float4 shadowPosH)
{
	// 透視除法
    shadowPosH.xyz /= shadowPosH.w;
	
	// NDC空間的深度值
    float depth = shadowPosH.z;

	// 紋素在紋理座標下的寬高
    const float dx = SMAP_DX;

    float percentLit = 0.0f;
    const float2 offsets[9] =
    {
        float2(-dx, -dx), float2(0.0f, -dx), float2(dx, -dx),
		float2(-dx, 0.0f), float2(0.0f, 0.0f), float2(dx, 0.0f),
		float2(-dx, +dx), float2(0.0f, +dx), float2(dx, +dx)
    };
                      
	[unroll]
    for (int i = 0; i < 9; ++i)
    {
        percentLit += shadowMap.SampleCmpLevelZero(samShadow,
			shadowPosH.xy + offsets[i], depth).r;
    }
    
    return percentLit /= 9.0f;
}

而後在咱們的光照模型中,只有第一個方向光才參與到陰影的計算,而且陰影因子將與直接光照(漫反射和鏡面反射光)項相乘。

// ...
float shadow[5] = { 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
 
// 僅第一個方向光用於計算陰影
shadow[0] = CalcShadowFactor(g_SamShadow, g_ShadowMap, pIn.ShadowPosH);
    
[unroll]
for (i = 0; i < 5; ++i)
{
    ComputeDirectionalLight(g_Material, g_DirLight[i], pIn.NormalW, toEyeW, A, D, S);
    ambient += A;
    diffuse += shadow[i] * D;
    spec += shadow[i] * S;
}

// ...

因爲環境光是間接光,因此陰影因子不受影響。而且,陰影因子也不會對來自環境映射的反射光構成影響。

C++端代碼實現

EffectHelper的引入

本章開始的代碼引入了EffectHelper來管理着色器所需的資源(咱們能夠無需手動建立並交給它來託管),並應用在了全部的Effect類當中。除了IEffect接口類,目前還引入了IEffectTransform接口類來統一變換的設置。隨着抽象類的增長,像GameObject這樣的類就能夠對IEffect接口類對象查詢是否有某一特定接口類或具體類來執行額外的複雜操做。

此外,SkyRender類也所以有了輕微的變更。具體想了解仍是去源碼翻閱,這裏不展開。

構建陰影貼圖與更新

首先咱們要在GameApp::InitResource中建立一副2048x2048的陰影貼圖:

m_pShadowMap = std::make_unique<TextureRender>();
HR(m_pShadowMap->InitResource(m_pd3dDevice.Get(), 2048, 2048, true));

在本Demo中,光照方向每幀都在變更,咱們但願讓投影立方體與光照所屬的變換軸對齊,而且中心可以坐落在原點。所以在GameApp::UpdateScene能夠這麼作:

//
// 投影區域爲正方體,以原點爲中心,以方向光爲+Z朝向
//
XMMATRIX LightView = XMMatrixLookAtLH(dirVec * 20.0f * (-2.0f), g_XMZero, g_XMIdentityR1);
m_pShadowEffect->SetViewMatrix(LightView);

// 將NDC空間 [-1, +1]^2 變換到紋理座標空間 [0, 1]^2
static XMMATRIX T(
    0.5f, 0.0f, 0.0f, 0.0f,
    0.0f, -0.5f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f, 0.0f,
    0.5f, 0.5f, 0.0f, 1.0f);
// S = V * P * T
m_pBasicEffect->SetShadowTransformMatrix(LightView * XMMatrixOrthographicLH(40.0f, 40.0f, 20.0f, 60.0f) * T);

至於繪製部分,本Demo將和陰影有聯繫的場景對象放入了另外一個重載函數DrawScene中(具體實現不在這給出),整體狀況以下:

void GameApp::DrawScene()
{
    // ...

    // ******************
    // 繪製到陰影貼圖

    m_pShadowMap->Begin(m_pd3dImmediateContext.Get(), nullptr);
    {
        DrawScene(true);
    }
    m_pShadowMap->End(m_pd3dImmediateContext.Get());

    // ******************
    // 正常繪製場景
    m_pBasicEffect->SetTextureShadowMap(m_pShadowMap->GetOutputTexture());
    DrawScene(false, m_EnableNormalMap);

    // 繪製天空盒
    m_pDesert->Draw(m_pd3dImmediateContext.Get(), *m_pSkyEffect, *m_pCamera);

    // 解除深度緩衝區綁定
    m_pBasicEffect->SetTextureShadowMap(nullptr);
    m_pBasicEffect->Apply(m_pd3dImmediateContext.Get());

    // ...

}

演示

本Demo提供了5種斜率下的方向光,對應主鍵盤數字鍵1-5,Q鍵開關法線貼圖,E鍵開關陰影貼圖的顯示,G鍵切換陰影貼圖的顯示模式。

練習題

  1. 嘗試4096x409六、1024x102四、512x5十二、256x256這幾種不一樣分辨率的陰影貼圖
  2. 嘗試以單次點採樣陰影檢測來修改本演示程序(即不採用PCF)。咱們將欣賞到硬陰影與鋸齒狀的陰影邊緣
  3. 關閉斜率縮放偏移來觀察陰影粉刺
  4. 將斜率縮放偏移值放大10倍,觀察peter panning失真的效果
  5. 實現單點光源下的陰影(必要時能夠考慮像CubeMap那樣使用6個正方形貼圖)

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

相關文章
相關標籤/搜索