欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-10-05 08:59:10
前言 到现在为止,所有的教程项目都没有使用Effects11框架类来管理资源。因为在D3DCompile API ( 47)版本中,如果你尝试编译fx_5_0的效果文件,会收到这样的警告: 在未来的版本中,D3DCompiler可能会停止对FX11的支持,所以我们需要自行去管理各种特效,并改用HLS ......

前言

到现在为止,所有的教程项目都没有使用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. 一个特效类对应一套着色器和所使用的常量缓冲区,所属着色器代码的变动很可能会引起对框架类的修改,因为缺乏反射机制而导致灵活性差。

文件结构

首先是文件结构:
DirectX11 With Windows SDK--13 动手实现一个简易Effects框架、阴影效果绘制

其中能够暴露给程序使用的只有头文件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时表示光源位置

通常指定的平面会稍微比实际平面高那么一点点,以避免深度缓冲区资源争夺导致阴影显示有问题。

使用模板缓冲区防止过度混合

一个物体投影到平面上时,投影区域的某些位置可能位于多个三角形之内,这会导致这些位置会有多个像素通过测试并进行混合操作,渲染的次数越多,显示的颜色会越黑。

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

我们可以使用模板缓冲区来解决这个问题。

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

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

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

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

mwoodcrate.setmaterial(mshadowmat);
mbasicobjectfx.setshadowstate(true);    // 反射开启,阴影开启            
mbasicobjectfx.setrendernodoubleblend(md3dimmediatecontext, 1);

mwoodcrate.draw(md3dimmediatecontext, mbasicobjectfx);

// 恢复到原来的状态
mbasicobjectfx.setshadowstate(false);
mwoodcrate.setmaterial(mwoodcratemat);

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

第4步: 绘制透明镜面

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

// 关闭反射绘制
mbasicobjectfx.setreflectionstate(false);
mbasicobjectfx.setrenderalphablendwithstencil(md3dimmediatecontext, 1);

mmirror.draw(md3dimmediatecontext, mbasicobjectfx);

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

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

// ************************
// 5. 绘制不透明的正常物体
//
mbasicobjectfx.setrenderdefault(md3dimmediatecontext);

for (auto& wall : mwalls)
    wall.draw(md3dimmediatecontext, mbasicobjectfx);
mfloor.draw(md3dimmediatecontext, mbasicobjectfx);
mwoodcrate.draw(md3dimmediatecontext, mbasicobjectfx);

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

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

// ************************
// 6. 绘制不透明正常物体的阴影
//
mwoodcrate.setmaterial(mshadowmat);
mbasicobjectfx.setshadowstate(true);    // 反射关闭,阴影开启
mbasicobjectfx.setrendernodoubleblend(md3dimmediatecontext, 0);

mwoodcrate.draw(md3dimmediatecontext, mbasicobjectfx);

mbasicobjectfx.setshadowstate(false);       // 阴影关闭
mwoodcrate.setmaterial(mwoodcratemat);

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

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

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

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

directx11 with windows sdk完整目录

github项目源码