【DirectX11】第四篇 Effects框架

本文为转载文章,这里为原文链接。

本文索引:

一、 什么是Effects?

effect框架是一组用于管理着色器程序和渲染状态的工具代码。例如,你可能会使用不同的effect绘制水、云、金属物体和动画角色。每个effect至少要由一个顶点着色器、一个像素着色器和渲染状态组成。

在Direct3D 11中,effects框架已从D3DX库中移除,你必须包含一个单独的头文件(d3dx11Effect.h),链接一个单独的库文件(D3DX11Effects.lib用于release生成,而D3DX11EffectsD.lib用于debug生成)。

而且,在Direct3D 11中提供了effect库的完整源代码(DirectX SDK/Samples/C++/Effects11)。因此,你可以根据需要修改effect框架。本书中,我们只是使用、并不会修改effect框架。要使用这个库,首先需要生成Effects11项目的Release和Debug模式,用于获得D3DX11Effects.lib和D3DX11EffectsD.lib文件,除非effect框架进行了更新(例如,新版本的DirectX SDK可能会更新这些文件,这时就需要重新生成.lib文件),这个步骤只需进行一次。d3dx11Effect.h头文件可在DirectX SDK/Samples/C++/Effects11/Inc文件夹中找到。在示例代码中,我们将d3dx11Effect.h,D3DX11EffectsD.lib和D3DX11Effects.lib文件都放在Common文件夹中,这样所有的项目文件都能共享这些文件。

二、 Effect文件结构

我们已经讨论了顶点着色器、像素着色器,并对几何着色器、曲面细分着色器进行了简要概述。我们还讨论了常量缓冲,它可以用于存储由着色器访问的“全局”变量。这些代码通常保存在一个effect文件(.fx)中,它是一个纯文本文件中(就像是C++代码保存在.h和.cpp文件中一样)。除了着色器和常量缓冲之外,每个effect文件至少还要包含一个technique,而每个technique至少要包含一个pass。

(1) technique11

一个technique由一个或多个pass组成,用于创建一个渲染技术。每个pass实现一种不同的几何体渲染方式,按照某些方式将多个pass的渲染结果混合在一起就可以得到我们最终想要的渲染结果。例如,在地形渲染中我们将使用多通道纹理映射技术(multi-pass texturing technique)。注意,多通道技术通常会占用大量的系统资源,因为每个pass都要对几何体进行一次渲染;不过,要实现某些渲染效果,我们必须使用多通道技术。

(2) pass

一个pass由一个顶点着色器、一个可选的几何着色器、一个像素着色器和一些渲染状态组成。这些部分定义了pass的几何体渲染方式。像素着色器也是可选的(很罕见)。例如,若我们只想绘制深度缓冲,不想绘制后台缓冲,在这种情况下我们就不需要像素着色器计算像素的颜色。

注意:techniques也可以组合在一起成为effect组。如果你没有显式地定义一个effect组,那么编译器会创建一个匿名effect组,把所有technique包含在effect文件中。本书中,我们不显式地定义effect组。下面是本章演示程序使用的effect文件:

cbuffer cbPerObject { float4x4 gWorldViewProj; }; 

struct VertexIn 
{ 
    float3 PosL  : POSITION;    
    float4 Color : COLOR; 
}; 

struct VertexOut 
{ 
    float4 PosH  : SV_POSITION;    
    float4 Color : COLOR; 
}; 

VertexOut VS(VertexIn vin) 
{
    VertexOut vout;    
    // 转换到齐次剪裁空间    
    vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);    
    // 将顶点颜色直接传递到像素着色器    
    vout.Color = vin.Color;    
    return vout;
}

float4 PS(VertexOut pin) : SV_Target
{    
    return pin.Color;
}

technique11 ColorTech
{    
    pass P0    
    {        
        SetVertexShader( CompileShader( vs_5_0, VS() ) );        
        SetPixelShader( CompileShader( ps_5_0, PS() ) );    
    }
}

cbuffer cbPerObject
{    
    float4x4 gWorldViewProj;
};

struct VertexIn
{    
    float3 PosL  : POSITION;    
    float4 Color : COLOR;
};

struct VertexOut
{    
    float4 PosH  : SV_POSITION;    
    float4 Color : COLOR;
};

VertexOut VS(VertexIn vin)
{    
    VertexOut vout;    
    // 转换到齐次剪裁空间    
    vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);    
    // 将顶点颜色直接传递到像素着色器    
    vout.Color = vin.Color;    
    return vout;
}

float4 PS(VertexOut pin) : SV_Target
{    
    return pin.Color;
}

technique11 ColorTech
{    
    pass P0    
    {        
        SetVertexShader( CompileShader( vs_5_0, VS() ) );        
        SetPixelShader( CompileShader( ps_5_0, PS() ) );    
    }
}

前面提到,pass可以包含渲染状态。也就是,状态块可以直接在effect文件中创建和指定。当effect需要特定的渲染状态时,这种方式非常实用;但是,当某些effect需要在运行过程中改变渲染状态时,我们更倾向于在应用程序层执行状态设定,因为这样进行状态切换更方便一些。下面的代码示范了如何在一个effect文件中创建和指定光栅化状态块。

RasterizerState Wireframe 
{
    FillMode = Wireframe;    
    CullMode = Back;    
    FrontCounterClockwise = false;    
    // 我们没有设置的属性使用默认值
};

technique11 ColorTech
{    
    pass P0    
    {        
        SetVertexShader( CompileShader( vs_5_0, VS() ) );        
        SetPixelShader( CompileShader( ps_5_0, PS() ) );        
        SetRasterizerState(Wireframe);    
    }
}

可以看到,在光栅化状态对象中定义的常量与C++中的枚举成员基本相同,只是省去了前缀而已(例如,D3D11_FILL_和D3D11_CULL_)。

注意:由于effect通常保存在扩展名为.fx的文件中,所以在修改effect代码之后,不必重新编译C++源代码。

三、 如何编译着色器?

创建一个effect的第一步是编译定义在.fx文件中的着色器程序,可以由下面的D3DX方法完成:

HRESULT D3DX11CompileFromFile(LPCTSTR pSrcFile, CONST D3D10_SHADE R_MACRO *pDefines, LPD3D10INCLUDE  pInclude, LPCSTR  pFunctionName, LPCSTR pProfile, UINT Flags 1, UINT Flags 2, ID3DX11ThreadPump *pPump, ID3D10Blob  **ppShader, ID3D10Blob  **ppErrorMsgs, HRESULT *pHResult);

1.pSrcFile:.fx文件名,该文件包含了我们所要编译的效果源代码。

2.pDefines:高级选项,我们不使用;请参阅SDK文档。

3.pInclude:高级选项,我们不使用;请参阅SDK文档。

4.pFunctionName:着色器入口函数的名字。只用于单独编译着色器程序的情况。当使用effect框架时设置为null,这是因为在effect文件中已经定义了入口点。

5.pProfile:用于指定着色器版本的字符串。对于Direct3D 11来说,我们使用的着色器版本为5.0(“fx_5_0”)。

6.Flags1:用于指定着色器代码编译方式的标志值。SDK文档列出了很多标志值,但本书只使用其中的2个:

  • D3D10_SHADER_DEBUG:以调试模式编译着色器。
  • D3D10_SHADER_SKIP_OPTIMIZATION:告诉编译器不做优化处理(用于进行调试)。

7.Flags2:高级选项,我们不使用;请参阅SDK文档。

8.pPump:指向线程泵的指针,多线程编程时使用,是高级选项,我们不使用;请参阅SDK文档。本书中这个值都设为null。

9.ppShader:返回一个指向ID3D10Blob数据对象的指针,这个数据对象保存了经过编译的代码。

10.ppErrorMsgs:返回一个指向ID3D10Blob数据对象的指针,这个数据对象存储了一个包含错误信息的字符串。

11.pHResult:在使用异步编译时,用于获得返回的错误代码。仅当使用pPump时才使用该参数;我们在本书中将该参数设为空值。

上面提到的ID3D10Blob类型只是一个通用内存块,它有两个方法:

(a)LPVOID GetBufferPointer:返回指向数据的一个void*,所以在使用时应该对它执行相应的类型转换(具有请参见下面的示例)。

(b)SIZE_T GetBufferSize:返回缓冲的大小,以字节为单位。

注意:

  • 除了可以编译在.fx文件内的着色器代码,这个方法也可以编译单独的着色器代码。有些程序不使用effect框架,它们会单独的定义和编译自己的着色器代码。
  • 方法中的指向“D3D10”的引用并不是打印错误。因为D3D11编译器是建立在D3D10的编译器之上的,所以Direct3D 11开发组就没有修改某些标识的名称。

四、 如何创建Effect对象?

编译完成后,我们就可以使用下面的方法创建一个effect(用ID3DXEffect11接口表示):

HRESULT D3DX11CreateEffectFromMemory(    void *pData,    SIZE_T DataLength,    UINT FXFlags ,    ID3D11Device *pDevice,    ID3DX11Effect **ppEffect);

1.pData:指向编译好的effect数据的指针。

2.DataLength:effect数据的长度,以字节为单位。

3.FXFlags:Effect标识必须与定义在D3DX11CompileFromFile方法中的Flags2匹配。

4.pDevice:指向Direct3D 11设备的指针。

5.ppEffect:指向创建好的effect的指针。

下面的代码演示了如何编译并创建一个effect:

DWORD shaderFlags = 0; 
#ifdefined(DEBUG) || defined(_DEBUG)    shaderFlags |= D3D10_SHADER_DEBUG;    
    shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION; 
#endifID3D10Blob * compiledShader = 0; 
ID3D10Blob * compilationMsgs = 0; 
HRESULT hr = D3DX11CompileFromFile(L"color.fx", 0, 0, 0, "fx_5_0", shaderFlags, 0, 0, &compiledShader, &compilationMsgs, 0);
//  compilationMsgs包含错误或警告的信息
if(compilationMsgs ! =  0)
{    
    MessageBoxA(0, (char*)compilationMsgs->GetBufferPointer(), 0, 0);    
    ReleaseCOM(compilationMsgs);
}

// 就算没有compilationMsgs,也需要确保没有其他错误
if(FAILED(hr))
{    
    DXTrace(__FILE__,(DWORD)__LINE__,hr,L"D3DX11Compile FromFile",true);
}
ID3DX11Effect*  mFX;
HR(D3DX11CreateEffectFromMemory(    compiledShader->Ge tBufferPointer(),    compiledShader->Ge tBufferSize(),    0, md3dDevice, &mFX));
// 编译完成释放资源
ReleaseCOM(compiledShader);

注意:创建Direct3D资源代价昂贵,尽量在初始化阶段完成,即创建输入布局、缓冲、渲染状态对象和Effect对象应该总在初始化阶段完成。

五、 如何使Effect对象与程序交互?

(1) 获得Effect变量

C++应用程序代码通常要与effect进行交互;尤其是C++应用程序经常要更新常量缓冲中的变量。例如,在一个effect文件中,我们有如下常量缓冲定义:

cbuffer cbPerObject
{ 
    float4x4 gWVP;    
    float4 gColor;    
    float gSize;    
    int gIndex;    
    bool gOptionOn; 
};

通过ID3D11Effect接口,我们可以获得指向常量缓冲变量的指针:

ID3D11EffectMatrixVariable* fxWVPVar; 
ID3D11EffectVectorVariable* fxColorVar; 
ID3D11EffectScalarVariable* fxSizeVar; 
ID3D11EffectScalarVariable* fxIndexVar; 
ID3D11EffectScalarVariable* fxOptionOnVar; 
fxWVPVar = mFX->GetVariableByName("gWVP")->AsMatrix(); 
fxColorVar = mFX->GetVariableByName("gColor")->AsVector(); 
fxSizeVar = mFX->GetVariableByName("gSize")->AsScalar(); 
fxIndexVar = mFX->GetVariableByName("gIndex")->AsScalar(); 
fxOptionOnVar = mFX->GetVariableByName("gOptionOn")->AsScalar();

ID3D11Effect::GetVariableByName方法返回一个ID3D11EffectVariable指针。它是一种通用effect变量类型;要获得指向特定类型变量的指针(例如,矩阵、向量、标量),你必须使用相应的As-方法(例如,AsMatrix、AsVector、AsScalar)。

(2) 更新Effect变量

一旦我们获得变量指针,我们就可以通过C++接口来更新它们了。下面是一些例子:

fxWVPVar->SetMatrix((float*)&M); 
// assume M is of type 
XMMATRIXfxColorVar->SetFloatVector( (float*)&v ); 
// assume v is of type 
XMVECTORfxSizeVar->>SetFloat( 5.0f );
fxIndexVar->SetInt( 77 );
fxOptionOnVar->SetBool( true );

注意,这些语句修改的只是effect对象在系统内存中的一个副本,它并没有传送到GPU内存中。所以在执行绘图操作时,我们必须使用Apply方法更新GPU内存。这样做的原因是为了提高效率,避免频繁地更新GPU内存。如果每修改一个变量就要更新一次GPU内存,那么效率会很低。

注意:effect变量不一定要被类型化。例如,可以有如下代码:

ID3D11EffectVariable* mfxEyePosVar; 
mfxEyePosVar = mFX->GetVariableByName("gEyePosW"); 
...
mfxEyePosVar->SetRawValue(&mEyePos, 0, sizeof(XMFLOAT3));

这种方式可以用来设置任意大小的变量(例如,普通结构体)。注意,ID3D11EffectVectorVariable接口使用4D向量。如果你希望使用3D向量的话,那应该像上面那样使用ID3D11EffectVariable接口。

(3) 获得指向technique对象的指针

除了常量缓冲变量之外,我们还需要获得指向technique对象的指针。实现方法如下:

ID3D11EffectTechnique* mTech;mTech = mFX->GetTechniqueByName("ColorTech");

该方法只包含一个用于指定technique名称的字符串参数。

六、 使用effect绘图

要使用technique来绘制几何体,我们只需要确保对常量缓冲中的变量进行实时更新。然后,使用循环语句来遍历technique 中的每个pass,使用pass来绘制几何体:

// 设置常量缓冲XMMATRIX 
world  = XMLoadFloat4x4(&mWorld);
XMMATRIX view = XMLoadFloat4x4(&mView);
XMMATRIX proj = XMLoadFloat4x4(&mProj);
XMMATRIX worldViewProj = world*view*proj;
mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(&worldViewProj));
D3DX11_TECHNIQUE_DESC techDesc;mTech->GetDesc(&techDesc);
for(UINT p = 0;p < techDesc.Passes;++p )
{    
    mTech->GetPassByIndex(p)->Apply(0,md3dImmediateContext);    
    //  绘制几何体    md3dImmediateContext->DrawIndexed(36, 0, 0);
}

当使用pass来绘制几何体时,Direct3D会启用在pass中指定的着色器和渲染状态。ID3D11EffectTechnique::GetPassByIndex方法返回一个指定索引的pass对象的ID3D11EffectPass接口指针。Apply方法更新存储在GPU内存中的常量缓冲、将着色器程序绑定到管线、并启用在pass中指定的各种渲染状态。在当前版本的Direct3D 11中,ID3D11EffectPass::Apply方法的第一个参数还未使用,应设置为0;第二个参数指向pass使用的设备上下文的指针。如果你需要在绘图调用之间改变常量缓冲中的变量值,那你必须在绘制几何体之前调用Apply方法:

for (UINT i = 0; i < techDesc.Passes; ++i) 
{
    ID3D11EffectPass* pass = mTech->GetPassByIndex(i);    
    //设置地面几何体的WVP组合矩阵    
    worldViewProj = mLandWorld*mView*mProj;    
    mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(& worldViewProj);    
    pass->Apply(0, md3dImmediateContext);    
    mLand.draw();    
    // 设置水波几何体的WVP组合矩阵    
    worldViewProj = mWavesWorld*mView*mProj;    
    mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(& worldViewProj);    
    pass->Apply(0 ,md3dImmediateContext);    
    mWaves.draw();
}

七、 编译期间生成Effect

我们已经介绍了如何在运行时通过D3DX11CompileFromFile方法编译一个effect。但这样做会带来一个小小的不便:如果你的effect文件有一个编译错误,直到程序运行时你才会发现这个错误。我们还可以使用DirectX SDK自带的fxc工具(位于DirectX SDK/Utilities/bin/x86)离线编译你的effect。而且,你还可以修改你的VC++项目,将调用fxc编译effect的过程作为生成过程的一部分。步骤如下:

1.确保路径DirectX SDK/Utilities/bin/x86位于你的项目的VC++目录的“可执行文件目录(Executable Directories)”之下。

2.在项目中添加effect文件。

3.在解决方案资源管理器中右击每个effect文件选择属性,添加自定义生成工具(见下图,在项目中添加自定义生成工具):
这里写图片描述

调试模式:

  • fxc /Fc /Od /Zi /T fx_5_0 /Fo ” % (RelativeDir)/% (Filename).fxo ” ” % (FuIIPath) “

发布模式:

  • fxc /T fx_5_0 /Fo ” o/o (RelativeDir)/% (Filename).fxo” ” % (FuIIPath) “

你可以在SDK文档中找到fxc完整的编译参数说明。在调试模式中我们使用了以下三个参数,“/Fc /Od /Zi”分别对应输出汇编指令,禁用优化,开启调试信息。

在编译阶段获取错误信息要比运行时获取方便得多。现在我们在生成过程中编译effect文件(.fxo),再也不需要在运行时进行这个操作了(即,我们无须再调用D3DX11CompileFromFile方法了)。但是,我们仍需要从.fxo文件中加载编译过的shader,并将它们传递给D3DX11CreateEffectFromMemory方法。这个工作可以通过使用C++的文件输入功能实现:

std::ifstream fin("fx/color.fxo", std::ios::binary); 
fin.seekg(0, std::ios_base::end); 
int size = (int)fin.tellg(); 
fin.seekg(0, std::ios_base::beg); 
std::vector<char> compiledShader(size); 
fin.read(&compiledShader[0], size); 
fin.close(); 
HR(D3DX11CreateEffectFromMemory(&compiledShader[0], size, 0, md3dDevice, &mFX));

除了在颜色立方体演示程序中我们在运行时编译了shader之外,本书的其他示例都是在生成过程中编译了所有shader。

八、 避免动态分支语句

在shader中使用动态分支语句代价不菲,所以只在必要时才使用它们。其实我们真正想要的是一个条件编译,它可以生成不同的shader代码,但又不使用分支指令。幸运的是,effect框架提供了一个方法可以解决这个问题。下面是具体实现:

// 省略常量缓冲,顶点结构等...
VertexOut VS(VertexIn vin) 
{
    /* 省略代码细节 */
}

#define LowQuality 0
#define MediumQuality 1
#define HighQuality 2
float4 PS(VertexOut pin, uniform int gQuality) : SV_Target
{    
    /* Do work common to all quality levels */    
    if(gQuality == LowQuality)    
    {       
        /* Do low quality specific stuff */    
    }    
    else if(gQuality == MediumQuality)    
    {        
        /* Do medium quality specific stuff */    
    }    
    else    
    {        
        /* Do high quality specific stuff */    
    }    
    /* Do more work common to all quality levels */
}

technique11 ShadowsLow
{    
    pass P0    
    {        
        SetVertexShader(CompileShader(vs_5_0,VS()));        
        SetPixeIShader(CompileShader(ps_5_0, PS(LowQuality)));    
    }
}

technique11 ShadowsMedium
{   
    pass P0    
    {        
        SetVertexShader(CompileShader(vs_5_0,VS()));        
        SetPixeIShader(CompileShader(ps_5_0, PS(MediumQuality)));    
    }
}

techniquell ShadowsHigh
{   
    pass P0    
    {        
        SetVertexShader(CompileShader(vs_5_0,VS()));        
        SetPixeIShader(CompileShader(ps_5_0, PS(HighQuality)));    
    }
}

我们在像素着色器中添加了一个额外的uniform参数(uniform译为不变的,相当于常量),用来表示阴影质量等级。这个参数值是不同的,但对每个像素来说却是不变的,当你使用 uniform/constant的时候。此外,我们不会在运行时改变它,就像常量缓存一样。我们是在编译时设置这些参数的,而且这些值在编译时就是已知的,所以effect框架会基于这个值生成不同的shader代码。这样,我们不用复制代码(effect框架帮我们在编译时复制了这些代码)就可以生成低、中、高三种不同阴影质量的shader代码,而且没有用到条件分支语句。

下面的两个例子是使用shader生成器的常见情景:

  • 是否需要纹理?有个应用程序需要在一些物体上施加纹理,而另一些物体不使用纹理。一个解决方法是创建两个像素着色器,一个提供纹理而另一个不提供。或者我们也可以使用shader生成技巧创建两个像素着色器,然后在C++程序中选择期望的technique。
  • 使用多少个光源?一个游戏关卡可能会支持1至4个光源。光源越多,光照计算就越慢。我们可以基于光源数量设计不同的顶点着色器,或者也可以使用shader生成技巧创建四个顶点着色器,然后在C++程序中根据当前激活的光源数量选择期望的technique。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片