转载请注明出处:http://www.cnblogs.com/Ray1024
一、概述
Effects框架是一组用于管理着色器程序和渲染状态的工具代码。例如,你可能会使用不同的effect绘制水、云、金属物体和动画角色。每个effect至少要由一个顶点着色器、一个像素着色器和渲染状态组成。
在Direct3D11中,Effects框架已从D3DX库中移除,需要我们自己配置effect库,配置过程请参考《Direct3D11学习:(一)开发环境配置》。
上一篇文章介绍了D3D11的绘图基础过程,并绘制了一个彩色立方体,当中使用了Effects框架,只是简单的介绍了一下。我们这篇文章来详细讲解一下Effects框架。
二、Effects
2.1 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() ) );
}
}
注意:点和向量可以在许多不同的空间中描述(例如,局部空间、世界空间、观察空间、齐次裁剪空间)。当阅读代码时,有时很难看出点和向量的坐标系是相对于哪个坐标系的。所以,我们经常使用下面的后缀来表示空间:L(局部空间)、W(世界空间)、V(观察空间)、H(齐次裁剪空间)。下面是一些例子:
float3 iPosL; // local space
float3 gEyePosW; // world space
float3 normalV; // view space
float4 posH; // homogeneous clip space
前面提到,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++源代码。
2.2 在生成期间编译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文件选择属性,添加自定义生成工具,在自定义生成工具中,命令行(Debug模式)写fxc /Fc /Od /Zi /T fx_5_0 /Fo "%(RelativeDir)\%(Filename).fxo" "%(FullPath)",命令行(Release模式)写fxc /T fx_5_0 /Fo "%(RelativeDir)\%(Filename).fxo" "%(FullPath)",说明写fxc compile for debug: %(FullPath),输出写%(RelativeDir)\%(Filename).fxo,如下图:
这样,每当我们Build我们的项目时,IDE会自动帮我们编译Effect并生成相应的输出文件,在C++程序中直接读取二进制文件即可。而且IDE在帮我们编译Effect文件时,如果有来自于fxc的编译警告或错误,会在调试输出窗口显示相关信息。在编译阶段获取错误信息要比运行时获取方便得多。
现在我们在生成过程中编译effect文件(.fxo),再也不需要在运行时进行这个操作了(即,我们无须再调用D3DX11CompileFromFile方法了)。但是,我们仍需要从.fxo文件中加载编译过的shader,并将它们传递给D3DX11CreateEffectFromMemory方法。这个工作可以通过使用C++的文件输入功能实现:
// 从.fxo文件中加载编译过的shader
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(); // 创建Effect
HR(D3DX11CreateEffectFromMemory(&compiledShader[0], size, 0, m_pD3DDevice, &m_pFX));
我在这篇文章的示例代码中使用了上篇文章中绘制彩色立方体的代码,只是将Effect文件的编译工作放在了生成时,我们这个系列的其他示例都是在生成过程中编译了所有shader。
2.3 在C++应用程序中与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)。
一旦我们获得变量指针,我们就可以通过C++接口来更新它们了。下面是一些例子:
fxWVPVar->SetMatrix( (float*)&M ); // assume M is of type XMMATRIX
fxColorVar->SetFloatVector( (float*)&v ); // assume v is of type XMVECTOR
fxSizeVar->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接口。
除了常量缓冲变量之外,我们还需要获得指向technique对象的指针。实现方法如下:
ID3D11EffectTechnique* mTech;
mTech = mFX->GetTechniqueByName("ColorTech");
该方法只包含一个用于指定technique名称的字符串参数。
2.4 使用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();
}
2.5 将effect框架作为“着色器生成器”
我们一开始提到过一个effect可以包含多个technique。那为什么我们要使用多个technique呢?下面我们用阴影绘制为例子解释一下这个问题,但不会讨论实现阴影的细节内容。显然,阴影质量越高,要求的资源就越多。为了支持用户不同等级的显卡,我们可能会提供低、中、高不同质量的阴影技术。因此,即使只有一个阴影效果,我们也会使用多个technique去实现它。我们的阴影effect文件如下所示:
// 省略了常量缓冲,顶点结构等代码...
VertexOut VS(Vertexln vin) {/* Omit implementation details */} float4 LowQualityPS(VertexOut pin) : SV_Target
{
/* Do work common to all quality levels */
/* Do low quality specific stuff */
/* Do more work common to all quality levels */
} float4 MediumQualityPS(VertexOut pin) : SV_Target
{
/* Do work common to all quality levels */
/* Do medium quality specific stuff */
/* Do more work common to all quality levels */
} float4 HighQualityPS(VertexOut pin) : SV_Target
{
/* Do work common to all quality levels */
/* 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, LowQualityPS()));
}
} technique11 ShadowsMedium
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0,VS()));
SetPixeIShader(CompileShader(ps_5_0, MediumQualityPS()));
}
} technique11 ShadowsHigh
pass P0
{
SetVertexShader(CompileShader(vs_5_0,VS()));
SetPixeIShader(CompileShader(ps_5_0, HighQualityPS ()));
}
}
C++应用程序会侦测玩家的显卡等级,选择最合适的technique进行渲染。
注意:前面的代码假设三个不同的阴影technique只在像素着色器中有所区别,所有的technique共享相同的顶点着色器。但是,每个technique都有不同的顶点着色器也是有可能的。
前面的实现中还有一个问题:即使像素着色器的代码是不同的,但是还是有一些通用的代码是重复的。建议使用条件分支语句解决这个问题。在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 */
}
elseif(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参数,用来表示阴影质量等级。这个参数值是不同的,但对每个像素来说却是不变的,but is instead uniform/constant。Moreover,we do not change it at runtime either,like we change constant buffer variables。我们是在编译时设置这些参数的,而且这些值在编译时就是已知的,所以effect框架会基于这个值生成不同的shader变量。这样,我们不用复制代码(effect框架帮我们在编译时复制了这些代码)就可以生成低、中、高三种不同阴影质量的shader代码,而且没有用到条件分支语句。
下面的两个例子是使用shader生成器的常见情景:
1.是否需要纹理?有个应用程序需要在一些物体上施加纹理,而另一些物体不使用纹理。一个解决方法是创建两个像素着色器,一个提供纹理而另一个不提供。或者我们也可以使用shader生成技巧创建两个像素着色器,然后在C++程序中选择期望的technique。
float4 PS(VertexOut pin, uniform bool gApplyTexture) : SV_Target
{
/* Do common work */
if(gApplyTexture)
{
/* Apply texture */
}
/* Do more common work */
} technique11 BasicTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0,VS()));
SetPixelShader(CompileShader(ps_5_0, PS(false)));
}
} technique11 TextureTech
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0,VS()));
SetPixelShader(CompileShader(ps_5_0,PS(true)));
}
}
2.使用多少个光源?一个游戏关卡可能会支持1至4个光源。光源越多,光照计算就越慢。我们可以基于光源数量设计不同的顶点着色器,或者也可以使用shader生成技巧创建四个顶点着色器,然后在C++程序中根据当前激活的光源数量选择期望的technique:
VertexOut VS(VertexOut pin, uniform int gLightCount)
{
/* Do common work */
for(int i = 0; i< gLightCount; ++i)
{
/* do lighting work */
}
/* Do more common work */
} technique11 Light1
{
P0
{
SetVertexShader(CompileShader(vs_5_0, VS(1)));
SetPixeIShader(CompileShader(ps_5_0, PS()));
}
} technique11 Light2
{
P0
{
SetVertexShader(CompileShader(vs_5_0, VS(2)));
SetPixeIShader(CompileShader(ps_5_0, PS()));
}
}
technique11 Light3
{
P0
{
SetVertexShader(CompileShader(vs_5_0, VS(3)));
SetPixeIShader(CompileShader(ps_5_0, PS()));
}
}
technique11 Light4
{
P0
{
SetVertexShader(CompileShader(vs_5_0, VS(4)));
SetPixeIShader(CompileShader(ps_5_0, PS()));
}
}
参数也可以不止一个。要将阴影质量,纹理和多个光源组合在一起,我们可以使用以下的顶点和像素着色器:
VertexOut VS(VertexOut pin, uniform int gLightCount)
{...}
float4 PS(VertexOut pin,uniform int gQuality,uniform bool gApplyTexture) : SV_Target
{...}
要创建一个使用低质量阴影,两个光源,不使用纹理的technique,我们可以这样写代码:
technique11 LowShadowsTwoLightsNoTextures
{
pass P0
{
SetVertexShader(CompileShader(vs_5_0, VS(2)));
SetPixeIShader(CompileShader(ps_5_0,PS(LowQuality,false)));
}
}
三、结语
在这篇文章中我们详细地讲解了Effects框架,在之后的学习中我们会经常用到effect中的知识。