DirectX11 With Windows SDK--13 動手實現一個簡易Effects框架、陰影效果繪製

前言

到如今爲止,全部的教程項目都沒有使用Effects11框架類來管理資源。由於在D3DCompile API (#47)版本中,若是你嘗試編譯fx_5_0的效果文件,會收到這樣的警告:
X4717: Effects deprecated for D3DCompiler_47html

在將來的版本中,D3DCompiler可能會中止對FX11的支持,因此咱們須要自行去管理各類特效,並改用HLSL編譯器去編譯每個着色器。同時,在閱讀本章以前,你須要先學習本系列前面的一些重點章節再繼續:git

章節目錄
01 DirectX11初始化
02 頂點/像素着色器的建立、頂點緩衝區
03 索引緩衝區、常量緩衝區
09 紋理映射與採樣器狀態
11 混合狀態與光柵化狀態
12 深度/模板狀態、反射繪製

在DirectXTK中的Effects.h能夠看到它實現了一系列Effects管理類,相比Effects11框架庫,它缺乏了反射機制,而且使用的是它內部已經寫好、編譯好的着色器。DirectXTK的Effects也只不過是爲了簡化遊戲開發流程而設計出來的。固然,裏面的一部分源碼實現也值得咱們去學習。github

注意:這章經歷了一次十分大的改動,原先所使用的BasicEffect類由於在後續的章節中發現很難擴展,因此進行了一次大幅度重構。並會逐漸替換掉後面教程的項目源碼所使用的BasicEffect。數組

在這一章的學習事後,你將會理解Effects11的一部分運做機制是怎樣的。而關於它的反射機制、着色器編譯部分不會進行探討。app

這篇教程還會提到用深度/模板狀態去實現簡單的陰影效果,但不會深刻數學公式原理。框架

DirectX11 With Windows SDK完整目錄ide

Github項目源碼函數

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

回顧RenderStates類

目前的RenderStates類存放有比較經常使用的各類狀態,原來在Effects11框架下是能夠在fx文件初始化各類渲染狀態,並設置到Technique11中。但如今咱們只能在C++代碼層中一次性建立好各類所需的渲染狀態:學習

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

    static bool IsInit();

    static void InitAll(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<ID3D11DepthStencilState> DSSWriteStencil;     // 深度/模板狀態:寫入模板值
    static ComPtr<ID3D11DepthStencilState> DSSDrawWithStencil;  // 深度/模板狀態:對指定模板值的區域進行繪製
    static ComPtr<ID3D11DepthStencilState> DSSNoDoubleBlend;    // 深度/模板狀態:無二次混合區域
    static ComPtr<ID3D11DepthStencilState> DSSNoDepthTest;      // 深度/模板狀態:關閉深度測試
    static ComPtr<ID3D11DepthStencilState> DSSNoDepthWrite;     // 深度/模板狀態:僅深度測試,不寫入深度值
};

具體的設置能夠參照源碼或者上一章內容。

簡易Effects框架

該Effects框架支持的功能以下:

  1. 管理/修改常量緩衝區的內容,並應用(Apply)變動
  2. 編譯HLSL着色器而不是fx文件
  3. 管理/使用四種渲染狀態
  4. 切換渲染模式(涉及到渲染管線各類資源的綁定、切換)
  5. 僅更新修改的變量所對應的常量緩衝區塊

不過它也有這樣的缺陷:

  1. 一個特效類對應一套着色器和所使用的常量緩衝區,所屬着色器代碼的變更極可能會引發對框架類的修改,由於缺少反射機制而致使靈活性差。

此外,該框架內部會對矩陣進行轉置,所以在傳遞矩陣給Effects時只須要傳遞默認的行主矩陣便可。

文件結構

首先是文件結構:

其中可以暴露給程序使用的只有頭文件Effects.h,裏面能夠存放多套不一樣的特效框架類的聲明,而關於每一個框架類的實現部分都應當用一個獨立的源文件存放。而EffectHelper.h則是用來幫助管理常量緩衝區的,服務於各類框架類的實現部分以及所屬的源文件,所以不該該直接使用。

理論上它也是能夠作成靜態庫使用的,而後着色器代碼穩定後也不該當變更。在使用的時候只須要包含頭文件Effects.h便可。

EffectHelper.h

該頭文件包含了一些有用的東西,但它須要在包含特效類實現的源文件中使用,且必須晚於Effects.hd3dUtil.h包含。

在堆上進行類的內存對齊

有些類型須要在堆上按16字節對齊,好比XMVECTORXMMATRIX,雖說拿這些對象做爲類的成員不太合適,畢竟分配在堆上的話基本上沒法保證內存按16字節對齊了,但仍是但願可以作到。在VS的corecrt_malloc.h(只要有包含stdlib.h, malloc.h之一的頭文件均可以)中有這樣的一個函數:_aligned_malloc,它能夠指定須要分配的內存字節大小以及按多少字節對齊。其中對齊值必須爲2的整數次冪的字節數。

void * _aligned_malloc(  
    size_t size,        // [In]分配內存字節數
    size_t alignment    // [In]按多少字節內存來對齊
);

若一個類中包含有已經指定內存對齊的成員,則須要優先把這些成員放到最前。

而後與之對應的就是_aligned_free函數了,它能夠釋放以前由_aligned_malloc分配獲得的內存。

下面是類模板AlignedType的實現,讓須要內存對齊的類去繼承該類便可。它重載了operator newoperator delete的實現:

// 若類須要內存對齊,從該類派生
template<class DerivedType>
struct AlignedType
{
    static void* operator new(size_t size)
    {
        const size_t alignedSize = __alignof(DerivedType);

        static_assert(alignedSize > 8, "AlignedNew is only useful for types with > 8 byte alignment! Did you forget a __declspec(align) on DerivedType?");

        void* ptr = _aligned_malloc(size, alignedSize);

        if (!ptr)
            throw std::bad_alloc();

        return ptr;
    }

    static void operator delete(void * ptr)
    {
        _aligned_free(ptr);
    }
};

須要注意的是,繼承AlignedType的類或者其成員必須自己有__declspec(align)的標識。如果內部成員,在全部包含該標識的值中最大的align值 必須是2的整數次冪且必須大於8。

下面演示了正確的和錯誤的行爲:

// 錯誤!VertexPosColor按4字節對齊!
struct VertexPosColor : AlignedType<VertexPos>
{
    XMFLOAT3 pos;
    XMFLOAT4 color;
};

// 正確!Data按16字節對齊,由於pos自己是按16字節對齊的。
struct Data : AlignedType<VertexPos>
{
    XMVECTOR pos;
    int val;
};

// 正確!Vector類按16字節對齊
__declspec(align(16))
struct Vector : AlignedType<Vector>
{
    float x;
    float y;
    float z;
    float w;
};

這裏AlignedType<T>主要是用於BasicEffect::Impl類,由於其內部包含了XMVECTORXMMATRIX類型的成員,且該類須要分配在堆上。

常量緩衝區管理

一個常量緩衝區可能會被建立、更新或者綁定到管線。若常量緩衝區的值沒有發生變化,咱們不但願它進行無心義的更新。這裏可使用一個dirty標記,確認它是否被修改過。在Effects調用Apply後,若是常量緩衝區的任一內部成員發生修改的話,咱們就將數據更新到常量緩衝區並恢復該標記。

首先是抽象基類CBufferBase

struct CBufferBase
{
    template<class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    CBufferBase() : isDirty() {}
    ~CBufferBase() = default;

    BOOL isDirty;
    ComPtr<ID3D11Buffer> cBuffer;

    virtual HRESULT CreateBuffer(ID3D11Device * device) = 0;
    virtual void UpdateBuffer(ID3D11DeviceContext * deviceContext) = 0;
    virtual void BindVS(ID3D11DeviceContext * deviceContext) = 0;
    virtual void BindHS(ID3D11DeviceContext * deviceContext) = 0;
    virtual void BindDS(ID3D11DeviceContext * deviceContext) = 0;
    virtual void BindGS(ID3D11DeviceContext * deviceContext) = 0;
    virtual void BindCS(ID3D11DeviceContext * deviceContext) = 0;
    virtual void BindPS(ID3D11DeviceContext * deviceContext) = 0;
};

這麼作是爲了方便咱們放入數組進行遍歷。

而後是派生類CBufferObjectstartSlot指定了HLSL對應cbuffer的索引,T則是C++對應的結構體,存儲臨時數據:

template<UINT startSlot, class T>
struct CBufferObject : CBufferBase
{
    T data;

    CBufferObject() : CBufferBase(), data() {}

    HRESULT CreateBuffer(ID3D11Device * device) override
    {
        if (cBuffer != nullptr)
            return S_OK;
        D3D11_BUFFER_DESC cbd;
        ZeroMemory(&cbd, sizeof(cbd));
        cbd.Usage = D3D11_USAGE_DYNAMIC;
        cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
        cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
        cbd.ByteWidth = sizeof(T);
        return device->CreateBuffer(&cbd, nullptr, cBuffer.GetAddressOf());
    }

    void UpdateBuffer(ID3D11DeviceContext * deviceContext) override
    {
        if (isDirty)
        {
            isDirty = false;
            D3D11_MAPPED_SUBRESOURCE mappedData;
            deviceContext->Map(cBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData);
            memcpy_s(mappedData.pData, sizeof(T), &data, sizeof(T));
            deviceContext->Unmap(cBuffer.Get(), 0);
        }
    }

    void BindVS(ID3D11DeviceContext * deviceContext) override
    {
        deviceContext->VSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
    }

    void BindHS(ID3D11DeviceContext * deviceContext) override
    {
        deviceContext->HSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
    }

    void BindDS(ID3D11DeviceContext * deviceContext) override
    {
        deviceContext->DSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
    }

    void BindGS(ID3D11DeviceContext * deviceContext) override
    {
        deviceContext->GSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
    }

    void BindCS(ID3D11DeviceContext * deviceContext) override
    {
        deviceContext->CSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
    }

    void BindPS(ID3D11DeviceContext * deviceContext) override
    {
        deviceContext->PSSetConstantBuffers(startSlot, 1, cBuffer.GetAddressOf());
    }
};

關於常量緩衝區臨時變量的修改則在後續的內容。

BasicEffect類--管理對象繪製的資源

首先是抽象基類IEffects,它僅容許被移動,而且僅包含Apply方法。

class IEffect
{
public:
    // 使用模板別名(C++11)簡化類型名
    template <class T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

    IEffect() = default;

    // 不支持複製構造
    IEffect(const IEffect&) = delete;
    IEffect& operator=(const IEffect&) = delete;

    // 容許轉移
    IEffect(IEffect&& moveFrom) = default;
    IEffect& operator=(IEffect&& moveFrom) = default;

    virtual ~IEffect() = default;

    // 更新並綁定常量緩衝區
    virtual void Apply(ID3D11DeviceContext * deviceContext) = 0;
};

原來的ID3DX11EffectPass包含的方法Apply用於在各個着色器階段綁定所須要的常量緩衝區、紋理等資源,並更新以前有所修改的常量緩衝區。如今咱們實現Effects框架中的Apply方法也是這麼作的。

而後是派生類BasicEffect,從它的方法來看,包含了單例獲取、渲染狀態的切換、修改常量緩衝區某一成員的值、應用變動四個大塊:

class BasicEffect : public IEffect
{
public:

    BasicEffect();
    virtual ~BasicEffect() override;

    BasicEffect(BasicEffect&& moveFrom) noexcept;
    BasicEffect& operator=(BasicEffect&& moveFrom) noexcept;

    // 獲取單例
    static BasicEffect& Get();

    

    // 初始化Basic.hlsli所需資源並初始化渲染狀態
    bool InitAll(ID3D11Device * device);


    //
    // 渲染模式的變動
    //

    // 默認狀態來繪製
    void SetRenderDefault(ID3D11DeviceContext * deviceContext);
    // Alpha混合繪製
    void SetRenderAlphaBlend(ID3D11DeviceContext * deviceContext);
    // 無二次混合
    void SetRenderNoDoubleBlend(ID3D11DeviceContext * deviceContext, UINT stencilRef);
    // 僅寫入模板值
    void SetWriteStencilOnly(ID3D11DeviceContext * deviceContext, UINT stencilRef);
    // 對指定模板值的區域進行繪製,採用默認狀態
    void SetRenderDefaultWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef);
    // 對指定模板值的區域進行繪製,採用Alpha混合
    void SetRenderAlphaBlendWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef);
    // 2D默認狀態繪製
    void Set2DRenderDefault(ID3D11DeviceContext * deviceContext);
    // 2D混合繪製
    void Set2DRenderAlphaBlend(ID3D11DeviceContext * deviceContext);

    

    //
    // 矩陣設置
    //

    void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX W);
    void XM_CALLCONV SetViewMatrix(DirectX::FXMMATRIX V);
    void XM_CALLCONV SetProjMatrix(DirectX::FXMMATRIX P);

    void XM_CALLCONV SetReflectionMatrix(DirectX::FXMMATRIX R);
    void XM_CALLCONV SetShadowMatrix(DirectX::FXMMATRIX S);
    void XM_CALLCONV SetRefShadowMatrix(DirectX::FXMMATRIX RefS);
    
    //
    // 光照、材質和紋理相關設置
    //

    // 各類類型燈光容許的最大數目
    static const int maxLights = 5;

    void SetDirLight(size_t pos, const DirectionalLight& dirLight);
    void SetPointLight(size_t pos, const PointLight& pointLight);
    void SetSpotLight(size_t pos, const SpotLight& spotLight);

    void SetMaterial(const Material& material);

    void SetTexture(ID3D11ShaderResourceView * texture);

    void XM_CALLCONV SetEyePos(DirectX::FXMVECTOR eyePos);



    //
    // 狀態開關設置
    //

    void SetReflectionState(bool isOn);
    void SetShadowState(bool isOn);
    

    // 應用常量緩衝區和紋理資源的變動
    void Apply(ID3D11DeviceContext * deviceContext);
    
private:
    class Impl;
    std::unique_ptr<Impl> pImpl;
};

XM_CALLCONV即在第六章以前提到的__vectorcall__fastcall約定。

而後來到BasicEffect.cpp,首先包含了對應HLSL五個cbuffer的C++結構體:

#include "Effects.h"
#include "EffectHelper.h"
#include "Vertex.h"
#include <d3dcompiler.h>
#include <experimental/filesystem>
using namespace DirectX;
using namespace std::experimental;

EffectHelper.h須要放在Effects.h以後。

這5個結構體都放在源文件是由於這些結構體僅限於在該文件種使用。

BasicEffect::Impl類

以前在BasicEffect中聲明瞭Impl類,主要目的是爲了將類的成員和方法定義都轉移到源文件中,而且還包含了HLSL五個cbuffer的C++結構體。不只能夠減小BasicEffect類的壓力,還能夠避免暴露上面的五個結構體:

class BasicEffect::Impl : public AlignedType<BasicEffect::Impl>
{
public:

    //
    // 這些結構體對應HLSL的結構體。須要按16字節對齊
    //

    struct CBChangesEveryDrawing
    {
        DirectX::XMMATRIX world;
        DirectX::XMMATRIX worldInvTranspose;
        Material material;
    };

    struct CBDrawingStates
    {
        int isReflection;
        int isShadow;
        DirectX::XMINT2 pad;
    };

    struct CBChangesEveryFrame
    {
        DirectX::XMMATRIX view;
        DirectX::XMVECTOR eyePos;
    };

    struct CBChangesOnResize
    {
        DirectX::XMMATRIX proj;
    };


    struct CBChangesRarely
    {
        DirectX::XMMATRIX reflection;
        DirectX::XMMATRIX shadow;
        DirectX::XMMATRIX refShadow;
        DirectionalLight dirLight[BasicEffect::maxLights];
        PointLight pointLight[BasicEffect::maxLights];
        SpotLight spotLight[BasicEffect::maxLights];
    };

public:
    // 必須顯式指定
    Impl() : m_IsDirty() {}
    ~Impl() = default;

public:
    // 須要16字節對齊的優先放在前面
    CBufferObject<0, CBChangesEveryDrawing> m_CBDrawing;        // 每次對象繪製的常量緩衝區
    CBufferObject<1, CBDrawingStates>       m_CBStates;         // 每次繪製狀態變動的常量緩衝區
    CBufferObject<2, CBChangesEveryFrame>   m_CBFrame;          // 每幀繪製的常量緩衝區
    CBufferObject<3, CBChangesOnResize>     m_CBOnResize;       // 每次窗口大小變動的常量緩衝區
    CBufferObject<4, CBChangesRarely>       m_CBRarely;         // 幾乎不會變動的常量緩衝區
    BOOL m_IsDirty;                                             // 是否有值變動
    std::vector<CBufferBase*> m_pCBuffers;                      // 統一管理上面全部的常量緩衝區


    ComPtr<ID3D11VertexShader> m_pVertexShader3D;               // 用於3D的頂點着色器
    ComPtr<ID3D11PixelShader>  m_pPixelShader3D;                // 用於3D的像素着色器
    ComPtr<ID3D11VertexShader> m_pVertexShader2D;               // 用於2D的頂點着色器
    ComPtr<ID3D11PixelShader>  m_pPixelShader2D;                // 用於2D的像素着色器

    ComPtr<ID3D11InputLayout>  m_pVertexLayout2D;               // 用於2D的頂點輸入佈局
    ComPtr<ID3D11InputLayout>  m_pVertexLayout3D;               // 用於3D的頂點輸入佈局

    ComPtr<ID3D11ShaderResourceView> m_pTexture;                // 用於繪製的紋理

};

構造/析構/單例

這裏用一個匿名空間保管單例對象的指針。當有一個實例被構造出來的時候就會給其賦值。後續就不容許再被實例化了,可使用Get方法獲取該單例。

namespace
{
    // BasicEffect單例
    static BasicEffect * g_pInstance = nullptr;
}

BasicEffect::BasicEffect()
{
    if (g_pInstance)
        throw std::exception("BasicEffect is a singleton!");
    g_pInstance = this;
    pImpl = std::make_unique<BasicEffect::Impl>();
}

BasicEffect::~BasicEffect()
{
}

BasicEffect::BasicEffect(BasicEffect && moveFrom) noexcept
{
    pImpl.swap(moveFrom.pImpl);
}

BasicEffect & BasicEffect::operator=(BasicEffect && moveFrom) noexcept
{
    pImpl.swap(moveFrom.pImpl);
    return *this;
}

BasicEffect & BasicEffect::Get()
{
    if (!g_pInstance)
        throw std::exception("BasicEffect needs an instance!");
    return *g_pInstance;
}

BasicEffect::InitAll方法

BasicEffect::InitAll方法負責建立出全部的着色器和常量緩衝區,以及全部的渲染狀態:

bool BasicEffect::InitAll(ID3D11Device * device)
{
    if (!device)
        return false;

    if (!pImpl->m_pCBuffers.empty())
        return true;

    if (!RenderStates::IsInit())
        throw std::exception("RenderStates need to be initialized first!");

    ComPtr<ID3DBlob> blob;

    // 建立頂點着色器(2D)
    HR(CreateShaderFromFile(L"HLSL\\Basic_VS_2D.cso", L"HLSL\\Basic_VS_2D.hlsl", "VS_2D", "vs_5_0", blob.GetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pVertexShader2D.GetAddressOf()));
    // 建立頂點佈局(2D)
    HR(device->CreateInputLayout(VertexPosTex::inputLayout, ARRAYSIZE(VertexPosTex::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexLayout2D.GetAddressOf()));

    // 建立像素着色器(2D)
    HR(CreateShaderFromFile(L"HLSL\\Basic_PS_2D.cso", L"HLSL\\Basic_PS_2D.hlsl", "PS_2D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pPixelShader2D.GetAddressOf()));

    // 建立頂點着色器(3D)
    HR(CreateShaderFromFile(L"HLSL\\Basic_VS_3D.cso", L"HLSL\\Basic_VS_3D.hlsl", "VS_3D", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pVertexShader3D.GetAddressOf()));
    // 建立頂點佈局(3D)
    HR(device->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->m_pVertexLayout3D.GetAddressOf()));

    // 建立像素着色器(3D)
    HR(CreateShaderFromFile(L"HLSL\\Basic_PS_3D.cso", L"HLSL\\Basic_PS_3D.hlsl", "PS_3D", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->m_pPixelShader3D.GetAddressOf()));


    pImpl->m_pCBuffers.assign({
        &pImpl->m_CBDrawing, 
        &pImpl->m_CBFrame, 
        &pImpl->m_CBStates, 
        &pImpl->m_CBOnResize, 
        &pImpl->m_CBRarely});

    // 建立常量緩衝區
    for (auto& pBuffer : pImpl->m_pCBuffers)
    {
        HR(pBuffer->CreateBuffer(device));
    }

    return true;
}

各類渲染狀態的切換

下面全部的渲染模式使用的是線性Wrap採樣器。

BasicEffect::SetRenderDefault方法--默認渲染

BasicEffect::SetRenderDefault方法使用了默認的3D像素着色器和頂點着色器,而且其他各狀態都保留使用默認狀態:

void BasicEffect::SetRenderDefault(ID3D11DeviceContext * deviceContext)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
    deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
    deviceContext->RSSetState(nullptr);
    deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
    deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    deviceContext->OMSetDepthStencilState(nullptr, 0);
    deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

BasicEffect::SetRenderAlphaBlend方法--Alpha透明混合渲染

該繪製模式關閉了光柵化裁剪,並採用透明混合方式。

void BasicEffect::SetRenderAlphaBlend(ID3D11DeviceContext * deviceContext)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
    deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
    deviceContext->RSSetState(RenderStates::RSNoCull.Get());
    deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
    deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    deviceContext->OMSetDepthStencilState(nullptr, 0);
    deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicEffect::SetRenderNoDoubleBlend方法--無重複混合(單次混合)

該繪製模式用於繪製陰影,防止過分混合。須要指定繪製區域的模板值。

void BasicEffect::SetRenderNoDoubleBlend(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
    deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
    deviceContext->RSSetState(RenderStates::RSNoCull.Get());
    deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
    deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    deviceContext->OMSetDepthStencilState(RenderStates::DSSNoDoubleBlend.Get(), stencilRef);
    deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicEffect::SetWriteStencilOnly方法--僅寫入模板值

該模式用於向模板緩衝區寫入用戶指定的模板值,而且不寫入到深度緩衝區和後備緩衝區。

void BasicEffect::SetWriteStencilOnly(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
    deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
    deviceContext->RSSetState(nullptr);
    deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
    deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    deviceContext->OMSetDepthStencilState(RenderStates::DSSWriteStencil.Get(), stencilRef);
    deviceContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);
}

BasicEffect::SetRenderDefaultWithStencil方法--對指定模板值區域進行常規繪製

該模式下,僅對模板緩衝區的模板值和用戶指定的相等的區域進行常規繪製。

void BasicEffect::SetRenderDefaultWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
    deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
    deviceContext->RSSetState(RenderStates::RSCullClockWise.Get());
    deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
    deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    deviceContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef);
    deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

BasicEffect::SetRenderAlphaBlendWithStencil方法--對指定模板值區域進行Alpha透明混合繪製

該模式下,僅對模板緩衝區的模板值和用戶指定的相等的區域進行Alpha透明混合繪製。

void BasicEffect::SetRenderAlphaBlendWithStencil(ID3D11DeviceContext * deviceContext, UINT stencilRef)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexLayout3D.Get());
    deviceContext->VSSetShader(pImpl->m_pVertexShader3D.Get(), nullptr, 0);
    deviceContext->RSSetState(RenderStates::RSNoCull.Get());
    deviceContext->PSSetShader(pImpl->m_pPixelShader3D.Get(), nullptr, 0);
    deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    deviceContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef);
    deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicEffect::Set2DRenderDefault方法--2D默認繪製

該模式使用的是2D頂點着色器和像素着色器,並修改成2D輸入佈局。

void BasicEffect::Set2DRenderDefault(ID3D11DeviceContext * deviceContext)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexLayout2D.Get());
    deviceContext->VSSetShader(pImpl->m_pVertexShader2D.Get(), nullptr, 0);
    deviceContext->RSSetState(nullptr);
    deviceContext->PSSetShader(pImpl->m_pPixelShader2D.Get(), nullptr, 0);
    deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    deviceContext->OMSetDepthStencilState(nullptr, 0);
    deviceContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

BasicEffect::Set2DRenderAlphaBlend方法--2D透明混合繪製

相比上面,多了透明混合狀態。

void BasicEffect::Set2DRenderAlphaBlend(ID3D11DeviceContext * deviceContext)
{
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    deviceContext->IASetInputLayout(pImpl->m_pVertexLayout2D.Get());
    deviceContext->VSSetShader(pImpl->m_pVertexShader2D.Get(), nullptr, 0);
    deviceContext->RSSetState(RenderStates::RSNoCull.Get());
    deviceContext->PSSetShader(pImpl->m_pPixelShader2D.Get(), nullptr, 0);
    deviceContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    deviceContext->OMSetDepthStencilState(nullptr, 0);
    deviceContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

更新常量緩衝區

下面這些全部的方法會更新CBufferObject中的臨時數據,數據髒標記被設爲true

void XM_CALLCONV BasicEffect::SetWorldMatrix(DirectX::FXMMATRIX W)
{
    auto& cBuffer = pImpl->m_CBDrawing;
    cBuffer.data.world = XMMatrixTranspose(W);
    cBuffer.data.worldInvTranspose = XMMatrixInverse(nullptr, W);   // 兩次轉置抵消
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetViewMatrix(FXMMATRIX V)
{
    auto& cBuffer = pImpl->m_CBFrame;
    cBuffer.data.view = XMMatrixTranspose(V);
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetProjMatrix(FXMMATRIX P)
{
    auto& cBuffer = pImpl->m_CBOnResize;
    cBuffer.data.proj = XMMatrixTranspose(P);
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetReflectionMatrix(FXMMATRIX R)
{
    auto& cBuffer = pImpl->m_CBRarely;
    cBuffer.data.reflection = XMMatrixTranspose(R);
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetShadowMatrix(FXMMATRIX S)
{
    auto& cBuffer = pImpl->m_CBRarely;
    cBuffer.data.shadow = XMMatrixTranspose(S);
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicEffect::SetRefShadowMatrix(DirectX::FXMMATRIX RefS)
{
    auto& cBuffer = pImpl->m_CBRarely;
    cBuffer.data.refShadow = XMMatrixTranspose(RefS);
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetDirLight(size_t pos, const DirectionalLight & dirLight)
{
    auto& cBuffer = pImpl->m_CBRarely;
    cBuffer.data.dirLight[pos] = dirLight;
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetPointLight(size_t pos, const PointLight & pointLight)
{
    auto& cBuffer = pImpl->m_CBRarely;
    cBuffer.data.pointLight[pos] = pointLight;
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetSpotLight(size_t pos, const SpotLight & spotLight)
{
    auto& cBuffer = pImpl->m_CBRarely;
    cBuffer.data.spotLight[pos] = spotLight;
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetMaterial(const Material & material)
{
    auto& cBuffer = pImpl->m_CBDrawing;
    cBuffer.data.material = material;
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetTexture(ID3D11ShaderResourceView * m_pTexture)
{
    pImpl->m_pTexture = m_pTexture;
}

void XM_CALLCONV BasicEffect::SetEyePos(FXMVECTOR eyePos)
{
    auto& cBuffer = pImpl->m_CBFrame;
    cBuffer.data.eyePos = eyePos;
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetReflectionState(bool isOn)
{
    auto& cBuffer = pImpl->m_CBStates;
    cBuffer.data.isReflection = isOn;
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

void BasicEffect::SetShadowState(bool isOn)
{
    auto& cBuffer = pImpl->m_CBStates;
    cBuffer.data.isShadow = isOn;
    pImpl->m_IsDirty = cBuffer.isDirty = true;
}

BasicEffect::Apply方法--應用緩衝區、紋理資源並進行更新

BasicEffect::Apply首先將所須要用到的緩衝區綁定到渲染管線上,並設置紋理,而後纔是視狀況更新常量緩衝區。

下面的緩衝區數組索引值同時也對應了以前編譯期指定的startSlot值。

首先檢驗總的髒標記是否爲true,如有任意數據被修改,則檢驗每一個常量緩衝區的髒標記,並根據該標記決定是否要更新常量緩衝區。

void BasicEffect::Apply(ID3D11DeviceContext * deviceContext)
{
    auto& pCBuffers = pImpl->m_pCBuffers;
    // 將緩衝區綁定到渲染管線上
    pCBuffers[0]->BindVS(deviceContext);
    pCBuffers[1]->BindVS(deviceContext);
    pCBuffers[2]->BindVS(deviceContext);
    pCBuffers[3]->BindVS(deviceContext);
    pCBuffers[4]->BindVS(deviceContext);

    pCBuffers[0]->BindPS(deviceContext);
    pCBuffers[1]->BindPS(deviceContext);
    pCBuffers[2]->BindPS(deviceContext);
    pCBuffers[4]->BindPS(deviceContext);

    // 設置紋理
    deviceContext->PSSetShaderResources(0, 1, pImpl->m_pTexture.GetAddressOf());

    if (pImpl->m_IsDirty)
    {
        pImpl->m_IsDirty = false;
        for (auto& pCBuffer : pCBuffers)
        {
            pCBuffer->UpdateBuffer(deviceContext);
        }
    }
}

固然,目前BasicEffect能作的事情仍是比較有限的,而且還須要隨着HLSL代碼的變更而隨之調整。更多的功能會在後續教程中實現。

繪製平面陰影

使用XMMatrixShadow能夠生成陰影矩陣,根據光照類型和位置對幾何體投影到平面上的。

XMMATRIX XMMatrixShadow(
    FXMVECTOR ShadowPlane,      // 平面向量(nx, ny, nz, d)
    FXMVECTOR LightPosition);   // w = 0時表示平行光方向, w = 1時表示光源位置

一般指定的平面會稍微比實際平面高那麼一點點,以免深度緩衝區資源爭奪致使陰影顯示有問題。

使用模板緩衝區防止過分混合

一個物體投影到平面上時,投影區域的某些位置可能位於多個三角形以內,這會致使這些位置會有多個像素經過測試並進行混合操做,渲染的次數越多,顯示的顏色會越黑。

咱們可使用模板緩衝區來解決這個問題。

  1. 在以前的例子中,咱們用模板值爲0的區域表示非鏡面反射區,模板值爲1的區域表示爲鏡面反射區;
  2. 使用RenderStates::DSSNoDoubleBlend的深度模板狀態,當給定的模板值和深度/模板緩衝區的模板值一致時,經過模板測試並對模板值加1,繪製該像素的混合,而後下一次因爲給定的模板值比深度/模板緩衝區的模板值小1,不會再經過模板測試,也就阻擋了後續像素的繪製;
  3. 應當先繪製鏡面的陰影區域,再繪製正常的陰影區域。

着色器代碼的變化

Basic_PS_2D.hlsl文件變化以下:

#include "Basic.hlsli"

// 像素着色器(2D)
float4 PS_2D(VertexPosHTex pIn) : SV_Target
{
    float4 color = g_Tex.Sample(g_Sam, pIn.Tex);
    clip(color.a - 0.1f);
    return color;
}

Basic_PS_3D.hlsl文件變化以下:

#include "Basic.hlsli"

// 像素着色器(3D)
// 像素着色器(3D)
float4 PS_3D(VertexPosHWNormalTex pIn) : SV_Target
{
    // 提早進行裁剪,對不符合要求的像素能夠避免後續運算
    float4 texColor = g_Tex.Sample(g_Sam, 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;
}

Basic_VS_2D.hlsl變化以下:

#include "Basic.hlsli"

// 頂點着色器(2D)
VertexPosHTex VS_2D(VertexPosTex vIn)
{
    VertexPosHTex vOut;
    vOut.PosH = float4(vIn.PosL, 1.0f);
    vOut.Tex = vIn.Tex;
    return vOut;
}

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);
    }
    // 若當前在繪製陰影,先進行投影操做
    [flatten]
    if (g_IsShadow)
    {
        posW = (g_IsReflection ? mul(posW, g_RefShadow) : mul(posW, g_Shadow));
    }

    vOut.PosH = mul(posW, viewProj);
    vOut.PosW = posW.xyz;
    vOut.NormalW = normalW;
    vOut.Tex = vIn.Tex;
    return vOut;
}

GameObject類與BasicEffect類的對接

因爲GameObject類也承擔了繪製方法,那麼最後的Apply也須要交給遊戲對象來調用。所以GameObject::Draw方法變動以下:

void GameObject::Draw(ID3D11DeviceContext * deviceContext, BasicEffect& effect)
{
    // 設置頂點/索引緩衝區
    UINT strides = m_VertexStride;
    UINT offsets = 0;
    deviceContext->IASetVertexBuffers(0, 1, m_pVertexBuffer.GetAddressOf(), &strides, &offsets);
    deviceContext->IASetIndexBuffer(m_pIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);

    // 更新數據並應用
    effect.SetWorldMatrix(XMLoadFloat4x4(&m_WorldMatrix));
    effect.SetTexture(m_pTexture);
    effect.SetMaterial(m_Material);
    effect.Apply(deviceContext);

    deviceContext->DrawIndexed(m_IndexCount, 0, 0);
}

場景繪製

如今場景只有牆體、地板、木箱和鏡面。

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

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

m_BasicEffect.SetWriteStencilOnly(m_pd3dImmediateContext.Get(), 1);
m_Mirror.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

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

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

// 開啓反射繪製
m_BasicEffect.SetReflectionState(true);
m_BasicEffect.SetRenderDefaultWithStencil(m_pd3dImmediateContext.Get(), 1);

m_Walls[2].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Walls[3].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Walls[4].Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_Floor.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);
m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

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

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

m_WoodCrate.SetMaterial(m_ShadowMat);
m_BasicEffect.SetShadowState(true); // 反射開啓,陰影開啓            
m_BasicEffect.SetRenderNoDoubleBlend(m_pd3dImmediateContext.Get(), 1);

m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

// 恢復到原來的狀態
m_BasicEffect.SetShadowState(false);
m_WoodCrate.SetMaterial(m_WoodCrateMat);

第4步: 繪製透明鏡面

// ***********************
// 4. 繪製透明鏡面
//

// 關閉反射繪製
m_BasicEffect.SetReflectionState(false);
m_BasicEffect.SetRenderAlphaBlendWithStencil(m_pd3dImmediateContext.Get(), 1);

m_Mirror.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

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

// ************************
// 5. 繪製不透明的正常物體
//
m_BasicEffect.SetRenderDefault(m_pd3dImmediateContext.Get());

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

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

// ************************
// 6. 繪製不透明正常物體的陰影
//
m_WoodCrate.SetMaterial(m_ShadowMat);
m_BasicEffect.SetShadowState(true); // 反射關閉,陰影開啓
m_BasicEffect.SetRenderNoDoubleBlend(m_pd3dImmediateContext.Get(), 0);

m_WoodCrate.Draw(m_pd3dImmediateContext.Get(), m_BasicEffect);

m_BasicEffect.SetShadowState(false);        // 陰影關閉
m_WoodCrate.SetMaterial(m_WoodCrateMat);

最終繪製效果以下:

注意該樣例只生成點光燈到地板的陰影。你能夠用各類攝像機模式來進行測試。

DirectX11 With Windows SDK完整目錄

Github項目源碼

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

相關文章
相關標籤/搜索