轉載請注明出處: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中的知識。