DirectX11 With Windows SDK--13 动手实现一个简易Effects框架、阴影效果绘制

前言

到现在为止,所有的教程项目都没有使用Effects11框架类来管理资源。因为在D3DCompile API (#47)版本中,如果你尝试编译fx_5_0的效果文件,会收到这样的警告:
X4717: Effects deprecated for D3DCompiler_47

在未来的版本中,D3DCompiler可能会停止对FX11的支持,所以我们需要自行去管理各种特效,并改用HLSL编译器去编译每一个着色器。同时,在阅读本章之前,你需要先学习本系列前面的一些重点章节再继续:

主题 版次 创建时间 修改时间
01 DirectX11初始化 第5版 2018/5/12 2018/8/18
02 顶点/像素着色器的创建、顶点缓冲区 第10版 2018/5/13 2018/8/30
03 索引缓冲区、常量缓冲区 第7版 2018/5/13 2018/8/20
09 纹理映射与采样器状态 第7版 2018/7/12 2018/8/11
11 混合状态与光栅化状态 第3版 2018/7/21 2018/8/8
12 深度/模板状态、反射绘制 第5版 2018/7/28 2018/9/16

在DirectXTK中的Effects.h可以看到它实现了一系列Effects管理类,相比Effects11框架库,它缺少了反射机制,并且使用的是它内部已经写好、编译好的着色器。DirectXTK的Effects也只不过是为了简化游戏开发流程而设计出来的。当然,里面的一部分源码实现也值得我们去学习。

注意:这章经历了一次十分大的改动,原先所使用的BasicFX类因为在后续的章节中发现很难扩展,所以进行了一次大幅度重构。并会逐渐替换掉后面教程的项目源码所使用的BasicFX。

在这一章的学习过后,你将会理解Effects11的一部分运作机制是怎样的。而关于它的反射机制、着色器编译部分不会进行探讨。

这篇教程还会提到用深度/模板状态去实现简单的阴影效果,但不会深入数学公式原理。

DirectX11 With Windows SDK完整目录

Github项目源码

回顾RenderStates类

目前的RenderStates类存放有比较常用的各种状态,原来在Effects11框架下是可以在fx文件初始化各种渲染状态,并设置到Technique11中。但现在我们只能在C++代码层中一次性创建好各种所需的渲染状态:

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

    static bool IsInit();

    static void InitAll(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<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.h,里面可以存放多套不同的特效框架类的声明,而关于每个框架类的实现部分都应当用一个独立的源文件存放。而EffectHelper.h则是用来帮助管理常量缓冲区的,服务于各种框架类的实现部分以及所属的源文件,因此不应该直接使用。

理论上它也是可以做成静态库使用的,然后着色器代码稳定后也不应当变动。在使用的时候只需要包含头文件Effects.h即可。

EffectHelper.h

该头文件包含了一些有用的东西,但它需要在包含特效类实现的源文件中使用,且必须晚于Effects.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>主要是用于BasicObjectFX::Impl类,因为其内部包含了XMVECTORXMMATRIX类型的成员,且该类需要分配在堆上。

常量缓冲区管理

一个常量缓冲区可能会被创建、更新或者绑定到管线。若常量缓冲区的值没有发生变化,我们不希望它进行无意义的更新。我们可以使用一个dirty标记,确认它是否被修改过。常量缓冲区的任一内部成员发生修改的话,我们就将数据更新到常量缓冲区并恢复该标记。

首先是抽象基类CBufferBase

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

    bool isDirty;
    ComPtr<ID3D11Buffer> cBuffer;

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

这么做是为了方便我们放入数组进行遍历。

然后是派生类CBufferObjectstartSlot指定了HLSL对应cbuffer的索引,T则是C++对应的结构体,存储临时数据:

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

    void CreateBuffer(ComPtr<ID3D11Device> device) override
    {
        if (cBuffer != nullptr)
            return;
        D3D11_BUFFER_DESC cbd;
        ZeroMemory(&cbd, sizeof(cbd));
        cbd.Usage = D3D11_USAGE_DEFAULT;
        cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
        cbd.CPUAccessFlags = 0;
        cbd.ByteWidth = sizeof(T);
        HR(device->CreateBuffer(&cbd, nullptr, cBuffer.GetAddressOf()));
    }

    void UpdateBuffer(ComPtr<ID3D11DeviceContext> deviceContext) override
    {
        if (isDirty)
        {
            isDirty = false;
            deviceContext->UpdateSubresource(cBuffer.Get(), 0, nullptr, &data, 0, 0);
        }
    }

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

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

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

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

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

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

关于常量缓冲区临时变量的修改则在后续的内容。

BasicObjectFX类--管理对象绘制的资源

首先是抽象基类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(ComPtr<ID3D11DeviceContext> deviceContext) = 0;
};

原来的ID3DX11EffectPass包含的方法Apply用于在各个着色器阶段绑定所需要的常量缓冲区、纹理等资源,并更新之前有所修改的常量缓冲区。现在我们实现Effects框架中的Apply方法也是这么做的。

然后是派生类BasicObjectFX,从它的方法来看,包含了单例获取、渲染状态的切换、修改常量缓冲区某一成员的值、应用变更四个大块:

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

    BasicObjectFX();
    virtual ~BasicObjectFX() override;

    BasicObjectFX(BasicObjectFX&& moveFrom);
    BasicObjectFX& operator=(BasicObjectFX&& moveFrom);

    // 获取单例
    static BasicObjectFX& Get();

    

    // 初始化Basix.fx所需资源并初始化渲染状态
    bool InitAll(ComPtr<ID3D11Device> device);


    //
    // 渲染模式的变更
    //

    // 默认状态来绘制
    void SetRenderDefault(ComPtr<ID3D11DeviceContext> deviceContext);
    // Alpha混合绘制
    void SetRenderAlphaBlend(ComPtr<ID3D11DeviceContext> deviceContext);
    // 无二次混合
    void SetRenderNoDoubleBlend(ComPtr<ID3D11DeviceContext> deviceContext, UINT stencilRef);
    // 仅写入模板值
    void SetWriteStencilOnly(ComPtr<ID3D11DeviceContext> deviceContext, UINT stencilRef);
    // 对指定模板值的区域进行绘制,采用默认状态
    void SetRenderDefaultWithStencil(ComPtr<ID3D11DeviceContext> deviceContext, UINT stencilRef);
    // 对指定模板值的区域进行绘制,采用Alpha混合
    void SetRenderAlphaBlendWithStencil(ComPtr<ID3D11DeviceContext> deviceContext, UINT stencilRef);
    // 2D默认状态绘制
    void Set2DRenderDefault(ComPtr<ID3D11DeviceContext> deviceContext);
    // 2D混合绘制
    void Set2DRenderAlphaBlend(ComPtr<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 SetWorldViewProjMatrix(DirectX::FXMMATRIX W, DirectX::CXMMATRIX V, DirectX::CXMMATRIX P);

    void XM_CALLCONV SetTexTransformMatrix(DirectX::FXMMATRIX W);

    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(ComPtr<ID3D11ShaderResourceView> texture);

    void XM_CALLCONV SetEyePos(DirectX::FXMVECTOR eyePos);



    //
    // 状态开关设置
    //

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

    // 应用常量缓冲区和纹理资源的变更
    void Apply(ComPtr<ID3D11DeviceContext> deviceContext);
    
private:
    class Impl;
    std::unique_ptr<Impl> pImpl;
};

XM_CALLCONV即在第六章之前提到的__vectorcall__fastcall约定。

然后来到BasicObjectFX.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;

//
// 这些结构体对应HLSL的结构体,仅供该文件使用。需要按16字节对齐
//

struct CBChangesEveryDrawing
{
    DirectX::XMMATRIX world;
    DirectX::XMMATRIX worldInvTranspose;
    DirectX::XMMATRIX texTransform;
    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[BasicObjectFX::maxLights];
    PointLight pointLight[BasicObjectFX::maxLights];
    SpotLight spotLight[BasicObjectFX::maxLights];
};

EffectHelper.h需要放在Effects.h之后。

这5个结构体都放在源文件是因为这些结构体仅限于在该文件种使用。

BasicObjectFX::Impl类

之前在BasicObjectFX中声明了Impl类,主要目的是为了将类的成员和方法定义都转移到源文件中。不仅可以减少BasicObjectFX类的压力,还可以避免暴露上面的五个结构体。

BasicObjectFX::Impl类包含一切所需资源,以及一个编译着色器的方法:

//
// BasicObjectFX::Impl 需要先于BasicObjectFX的定义
//

class BasicObjectFX::Impl : public AlignedType<BasicObjectFX::Impl>
{
public:
    // 必须显式指定
    Impl() = default;
    ~Impl() = default;

    // objFileNameInOut为编译好的着色器二进制文件(.*so),若有指定则优先寻找该文件并读取
    // hlslFileName为着色器代码,若未找到着色器二进制文件则编译着色器代码
    // 编译成功后,若指定了objFileNameInOut,则保存编译好的着色器二进制信息到该文件
    // ppBlobOut输出着色器二进制信息
    HRESULT CreateShaderFromFile(const WCHAR* objFileNameInOut, const WCHAR* hlslFileName, LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob** ppBlobOut);

public:
    // 需要16字节对齐的优先放在前面
    CBufferObject<0, CBChangesEveryDrawing> cbDrawing;      // 每次对象绘制的常量缓冲区
    CBufferObject<1, CBDrawingStates>       cbStates;       // 每次绘制状态变更的常量缓冲区
    CBufferObject<2, CBChangesEveryFrame>   cbFrame;        // 每帧绘制的常量缓冲区
    CBufferObject<3, CBChangesOnResize>     cbOnResize;     // 每次窗口大小变更的常量缓冲区
    CBufferObject<4, CBChangesRarely>       cbRarely;       // 几乎不会变更的常量缓冲区
    BOOL isDirty;                                           // 是否有值变更
    std::vector<CBufferBase*> cBufferPtrs;                  // 统一管理下面所有的常量缓冲区


    ComPtr<ID3D11VertexShader> vertexShader3D;              // 用于3D的顶点着色器
    ComPtr<ID3D11PixelShader>  pixelShader3D;               // 用于3D的像素着色器
    ComPtr<ID3D11VertexShader> vertexShader2D;              // 用于2D的顶点着色器
    ComPtr<ID3D11PixelShader>  pixelShader2D;               // 用于2D的像素着色器

    ComPtr<ID3D11InputLayout>  vertexLayout2D;              // 用于2D的顶点输入布局
    ComPtr<ID3D11InputLayout>  vertexLayout3D;              // 用于3D的顶点输入布局

    ComPtr<ID3D11ShaderResourceView> texture;               // 用于绘制的纹理

};

着色器的编译方法这里不再赘述。

构造/析构/单例

这里用一个匿名空间保管单例对象的指针。当有一个实例被构造出来的时候就会给其赋值。后续就不允许再被实例化了,可以使用Get方法获取该单例。

namespace
{
    // BasicObjectFX单例
    static BasicObjectFX * pInstance = nullptr;
}

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

BasicObjectFX::~BasicObjectFX()
{
}

BasicObjectFX::BasicObjectFX(BasicObjectFX && moveFrom)
{
    pImpl.swap(moveFrom.pImpl);
}

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

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

BasicObjectFX::InitAll方法

BasicObjectFX::InitAll方法负责创建出所有的着色器和常量缓冲区,以及所有的渲染状态:

bool BasicObjectFX::InitAll(ComPtr<ID3D11Device> device)
{
    if (!device)
        return false;

    ComPtr<ID3DBlob> blob;

    // 创建顶点着色器(2D)
    HR(pImpl->CreateShaderFromFile(L"HLSL\\BasicObject_VS_2D.vso", L"HLSL\\BasicObject_VS_2D.hlsl", "VS", "vs_5_0", blob.GetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->vertexShader2D.GetAddressOf()));
    // 创建顶点布局(2D)
    HR(device->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->vertexLayout2D.GetAddressOf()));

    // 创建像素着色器(2D)
    HR(pImpl->CreateShaderFromFile(L"HLSL\\BasicObject_PS_2D.pso", L"HLSL\\BasicObject_PS_2D.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->pixelShader2D.GetAddressOf()));

    // 创建顶点着色器(3D)
    HR(pImpl->CreateShaderFromFile(L"HLSL\\BasicObject_VS_3D.vso", L"HLSL\\BasicObject_VS_3D.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->vertexShader3D.GetAddressOf()));
    // 创建顶点布局(3D)
    HR(device->CreateInputLayout(VertexPosNormalTex::inputLayout, ARRAYSIZE(VertexPosNormalTex::inputLayout),
        blob->GetBufferPointer(), blob->GetBufferSize(), pImpl->vertexLayout3D.GetAddressOf()));

    // 创建像素着色器(3D)
    HR(pImpl->CreateShaderFromFile(L"HLSL\\BasicObject_PS_3D.pso", L"HLSL\\BasicObject_PS_3D.hlsl", "PS", "ps_5_0", blob.ReleaseAndGetAddressOf()));
    HR(device->CreatePixelShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, pImpl->pixelShader3D.GetAddressOf()));


    // 初始化
    RenderStates::InitAll(device);

    pImpl->cBufferPtrs.assign({
        &pImpl->cbDrawing, 
        &pImpl->cbFrame, 
        &pImpl->cbStates, 
        &pImpl->cbOnResize, 
        &pImpl->cbRarely});

    // 创建常量缓冲区
    for (auto& pBuffer : pImpl->cBufferPtrs)
    {
        pBuffer->CreateBuffer(device);
    }

    return true;
}

各种渲染状态的切换

下面所有的渲染模式使用的是线性Wrap采样器。

BasicFX::SetRenderDefault方法--默认渲染

BasicObjectFX::SetRenderDefault方法使用了默认的3D像素着色器和顶点着色器,并且其余各状态都保留使用默认状态:

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

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

该绘制模式关闭了光栅化裁剪,并采用透明混合方式。

void BasicObjectFX::SetRenderAlphaBlend()
{
    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(nullptr, 0);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicObjectFX::SetRenderNoDoubleBlend方法--无重复混合(单次混合)

该绘制模式用于绘制阴影,防止过度混合。需要指定绘制区域的模板值。

void BasicObjectFX::SetRenderNoDoubleBlend(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::DSSNoDoubleBlend.Get(), stencilRef);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicObjectFX::SetWriteStencilOnly方法--仅写入模板值

该模式用于向模板缓冲区写入用户指定的模板值,并且不写入到深度缓冲区和后备缓冲区。

void BasicObjectFX::SetWriteStencilOnly(UINT stencilRef)
{
    md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get());
    md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSWriteStencil.Get(), stencilRef);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSNoColorWrite.Get(), nullptr, 0xFFFFFFFF);
}

BasicObjectFX::SetRenderDefaultWithStencil方法--对指定模板值区域进行常规绘制

该模式下,仅对模板缓冲区的模板值和用户指定的相等的区域进行常规绘制。

void BasicObjectFX::SetRenderDefaultWithStencil(UINT stencilRef)
{
    md3dImmediateContext->IASetInputLayout(mVertexLayout3D.Get());
    md3dImmediateContext->VSSetShader(mVertexShader3D.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(RenderStates::RSCullClockWise.Get());
    md3dImmediateContext->PSSetShader(mPixelShader3D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(RenderStates::DSSDrawWithStencil.Get(), stencilRef);
    md3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

BasicObjectFX::SetRenderAlphaBlendWithStencil方法--对指定模板值区域进行Alpha透明混合绘制

该模式下,仅对模板缓冲区的模板值和用户指定的相等的区域进行Alpha透明混合绘制。

void BasicObjectFX::SetRenderAlphaBlendWithStencil(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::DSSDrawWithStencil.Get(), stencilRef);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

BasicObjectFX::Set2DRenderDefault方法--2D默认绘制

该模式使用的是2D顶点着色器和像素着色器,并修改为2D输入布局。

void BasicObjectFX::Set2DRenderDefault()
{
    md3dImmediateContext->IASetInputLayout(mVertexLayout2D.Get());
    md3dImmediateContext->VSSetShader(mVertexShader2D.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(nullptr);
    md3dImmediateContext->PSSetShader(mPixelShader2D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
    md3dImmediateContext->OMSetBlendState(nullptr, nullptr, 0xFFFFFFFF);
}

BasicObjectFX::Set2DRenderAlphaBlend方法--2D透明混合绘制

相比上面,多了透明混合状态。

void BasicObjectFX::Set2DRenderAlphaBlend()
{
    md3dImmediateContext->IASetInputLayout(mVertexLayout2D.Get());
    md3dImmediateContext->VSSetShader(mVertexShader2D.Get(), nullptr, 0);
    md3dImmediateContext->RSSetState(RenderStates::RSNoCull.Get());
    md3dImmediateContext->PSSetShader(mPixelShader2D.Get(), nullptr, 0);
    md3dImmediateContext->PSSetSamplers(0, 1, RenderStates::SSLinearWrap.GetAddressOf());
    md3dImmediateContext->OMSetDepthStencilState(nullptr, 0);
    md3dImmediateContext->OMSetBlendState(RenderStates::BSTransparent.Get(), nullptr, 0xFFFFFFFF);
}

更新常量缓冲区

下面这些所有的方法会更新CBufferObject中的临时数据,数据脏标记被设为true

void XM_CALLCONV BasicObjectFX::SetWorldMatrix(DirectX::FXMMATRIX W)
{
    auto& cBuffer = pImpl->cbDrawing;
    cBuffer.data.world = W;
    cBuffer.data.worldInvTranspose = XMMatrixTranspose(XMMatrixInverse(nullptr, W));
    pImpl->isDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicObjectFX::SetViewMatrix(FXMMATRIX V)
{
    auto& cBuffer = pImpl->cbFrame;
    cBuffer.data.view = V;
    pImpl->isDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicObjectFX::SetProjMatrix(FXMMATRIX P)
{
    auto& cBuffer = pImpl->cbOnResize;
    cBuffer.data.proj = P;
    pImpl->isDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicObjectFX::SetWorldViewProjMatrix(FXMMATRIX W, CXMMATRIX V, CXMMATRIX P)
{
    pImpl->cbDrawing.data.world = W;
    pImpl->cbDrawing.data.worldInvTranspose = XMMatrixTranspose(XMMatrixInverse(nullptr, W));
    pImpl->cbFrame.data.view = V;
    pImpl->cbOnResize.data.proj = P;

    auto& pCBuffers = pImpl->cBufferPtrs;
    pCBuffers[0]->isDirty = pCBuffers[1]->isDirty = pCBuffers[3]->isDirty = true;
    pImpl->isDirty = true;
}

void XM_CALLCONV BasicObjectFX::SetTexTransformMatrix(FXMMATRIX W)
{
    auto& cBuffer = pImpl->cbDrawing;
    cBuffer.data.texTransform = W;
    pImpl->isDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicObjectFX::SetReflectionMatrix(FXMMATRIX R)
{
    auto& cBuffer = pImpl->cbRarely;
    cBuffer.data.reflection = R;
    pImpl->isDirty = cBuffer.isDirty = true;
}

void XM_CALLCONV BasicObjectFX::SetShadowMatrix(FXMMATRIX S)
{
    auto& cBuffer = pImpl->cbRarely;
    cBuffer.data.shadow = S;
    pImpl->isDirty = cBuffer.isDirty = true;
}

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

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

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

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

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

void BasicObjectFX::SetTexture(ComPtr<ID3D11ShaderResourceView> texture)
{
    pImpl->texture = texture;
}

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

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

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

BasicObjectFX::Apply方法--应用缓冲区、纹理资源并进行更新

BasicObjectFX::Apply首先将所需要用到的缓冲区绑定到渲染管线上,并设置纹理,然后才是视情况更新常量缓冲区。

下面的缓冲区数组索引值同时也对应了之前编译期指定的startSlot值。

首先检验总的脏标记是否为true,若有任意数据被修改,则检验每个常量缓冲区的脏标记,并根据该标记决定是否要更新常量缓冲区。

void BasicObjectFX::Apply(ComPtr<ID3D11DeviceContext> deviceContext)
{
    auto& pCBuffers = pImpl->cBufferPtrs;
    // 将缓冲区绑定到渲染管线上
    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->texture.GetAddressOf());

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

当然,目前BasicFX能做的事情还是比较有限的,并且还需要随着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.fx"

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

Basic_PS_3D.hlsl文件变化如下:

#include "Basic.fx"

// 像素着色器(3D)
float4 PS_3D(Vertex3DOut pIn) : SV_Target
{
    // 提前进行裁剪,对不符合要求的像素可以避免后续运算
    float4 texColor = tex.Sample(sam, 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;
}

Basic_VS_2D.hlsl变化如下:

#include "Basic.fx"

// 顶点着色器(2D)
Vertex2DOut VS_2D(Vertex2DIn pIn)
{
    Vertex2DOut pOut;
    pOut.PosH = float4(pIn.Pos, 1.0f);
    pOut.Tex = mul(float4(pIn.Tex, 0.0f, 1.0f), gTexTransform).xy;
    return pOut;
}

Basic_VS_3D.hlsl变化如下:

#include "Basic.fx"

// 顶点着色器(3D)
Vertex3DOut VS_3D(Vertex3DIn pIn)
{
    Vertex3DOut pOut;
    
    float4 posW = mul(float4(pIn.PosL, 1.0f), gWorld);
    // 若当前在绘制反射物体,先进行反射操作
    [flatten]
    if (gIsReflection)
    {
        posW = mul(posW, gReflection);
    }
    // 若当前在绘制阴影,先进行投影操作
    [flatten]
    if (gIsShadow)
    {
        posW = (gIsReflection ? mul(posW, gRefShadow) : mul(posW, gShadow));
    }

    pOut.PosH = mul(mul(posW, gView), gProj);
    pOut.PosW = mul(float4(pIn.Pos, 1.0f), gWorld).xyz;
    pOut.NormalW = mul(pIn.NormalL, (float3x3) gWorldInvTranspose);
    pOut.Tex = mul(float4(pIn.Tex, 0.0f, 1.0f), gTexTransform).xy;
    return pOut;
}

GameObject类与BasicObjectFX类的对接

由于GameObject类也承担了绘制方法,那么最后的Apply也需要交给游戏对象来调用。因此GameObject::Draw方法变更如下:

void GameObject::Draw(ComPtr<ID3D11DeviceContext> deviceContext, BasicObjectFX& effect)
{
    // 设置顶点/索引缓冲区
    UINT strides = sizeof(VertexPosNormalTex);
    UINT offsets = 0;
    deviceContext->IASetVertexBuffers(0, 1, mVertexBuffer.GetAddressOf(), &strides, &offsets);
    deviceContext->IASetIndexBuffer(mIndexBuffer.Get(), DXGI_FORMAT_R16_UINT, 0);

    // 更新数据并应用
    effect.SetWorldMatrix(XMLoadFloat4x4(&mWorldMatrix));
    effect.SetTexTransformMatrix(XMLoadFloat4x4(&mTexTransform));
    effect.SetTexture(mTexture);
    effect.SetMaterial(mMaterial);
    effect.Apply(deviceContext);

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

场景绘制

现在场景只有墙体、地板、木箱和镜面。

第1步: 镜面区域写入模板缓冲区

// *********************
// 1. 给镜面反射区域写入值1到模板缓冲区
// 

mBasicObjectFX.SetWriteStencilOnly(md3dImmediateContext, 1);
mMirror.Draw(md3dImmediateContext, mBasicObjectFX);

第2步: 绘制不透明的反射物体

// ***********************
// 2. 绘制不透明的反射物体
//

// 开启反射绘制
mBasicObjectFX.SetReflectionState(true);
mBasicObjectFX.SetRenderDefaultWithStencil(md3dImmediateContext, 1);

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

第3步: 绘制不透明反射物体的阴影

// ***********************
// 3. 绘制不透明反射物体的阴影
//

mWoodCrate.SetMaterial(mShadowMat);
mBasicObjectFX.SetShadowState(true);    // 反射开启,阴影开启            
mBasicObjectFX.SetRenderNoDoubleBlend(md3dImmediateContext, 1);

mWoodCrate.Draw(md3dImmediateContext, mBasicObjectFX);

// 恢复到原来的状态
mBasicObjectFX.SetShadowState(false);
mWoodCrate.SetMaterial(mWoodCrateMat);

第4步: 绘制透明镜面

// ***********************
// 4. 绘制透明镜面
//

// 关闭反射绘制
mBasicObjectFX.SetReflectionState(false);
mBasicObjectFX.SetRenderAlphaBlendWithStencil(md3dImmediateContext, 1);

mMirror.Draw(md3dImmediateContext, mBasicObjectFX);

第5步:绘制不透明的正常物体

// ************************
// 5. 绘制不透明的正常物体
//
mBasicObjectFX.SetRenderDefault(md3dImmediateContext);

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

第6步:绘制不透明正常物体的阴影

// ************************
// 6. 绘制不透明正常物体的阴影
//
mWoodCrate.SetMaterial(mShadowMat);
mBasicObjectFX.SetShadowState(true);    // 反射关闭,阴影开启
mBasicObjectFX.SetRenderNoDoubleBlend(md3dImmediateContext, 0);

mWoodCrate.Draw(md3dImmediateContext, mBasicObjectFX);

mBasicObjectFX.SetShadowState(false);       // 阴影关闭
mWoodCrate.SetMaterial(mWoodCrateMat);

最终绘制效果如下:

注意该样例只生成点光灯到地板的阴影。你可以用各种摄像机模式来进行测试。

2018/9/17:该教程后面14, 15, 17, 19章还没有及时更换为新的框架,需要一段时间进行替换。并且篇幅庞大难免有遗漏错误之处,望谅解。

DirectX11 With Windows SDK完整目录

Github项目源码

猜你喜欢

转载自www.cnblogs.com/X-Jun/p/9665452.html
今日推荐