天天看點

DirectX11 EffectsEffects

Effects

1. 什麼是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檔案夾中,這樣所有的項目檔案都能共享這些檔案。

2. 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, ), 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++源代碼。

3. 如何編譯着色器?

建立一個effect的第一步是編譯定義在.fx檔案中的着色器程式,可以由下面的D3DX方法完成:

HRESULT D3DX11CompileFromFile (
    LPCTSTR pSrcFile ,
    CONST D3D10_SHADE R_MACRO *pDefines,
    LPD3D10INCLUDE  pInclude ,
    LPCSTR  pFunctionName ,
    LPCSTR pProfile,
    UINT Flags ,
    UINT Flags ,
    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:傳回緩沖的大小,以位元組為機關。

注意:

(1).除了可以編譯在.fx檔案内的着色器代碼,這個方法也可以編譯單獨的着色器代碼。有些程式不使用effect架構,它們會單獨的定義和編譯自己的着色器代碼。

(2).方法中的指向“D3D10”的引用并不是列印錯誤。因為D3D11編譯器是建立在D3D10的編譯器之上的,是以Direct3D 11開發組就沒有修改某些辨別的名稱。

4. 如何建立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 = ;
#ifdefined (DEBUG)||defined(_DEBUG)
    shaderFlags |= D3D10_SHADER_DEBUG;
    shaderFlags |= D3D10_SHADER_SKIP_OPTIMIZATION ;
#endif
ID3D10Blob * compiledShader = ;
ID3D10Blob * compilationMsgs = ;
HRESULT hr = D3DX11CompileFromFile(L"color.fx", ,
    , , "fx_5_0", shaderFlags,
    , , &compiledShader, &compilationMsgs, );

//  compilationMsgs包含錯誤或警告的資訊
if(compilationMsgs ! =  )
{
    MessageBoxA(, (char*)compilationMsgs->GetBufferPointer(), , );
    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(),
    , md3dDevice, &mFX));

// 編譯完成釋放資源
ReleaseCOM(compiledShader);
           

注意:建立Direct3D資源代價昂貴,盡量在初始化階段完成,即建立輸入布局、緩沖、渲染狀态對象和Effect對象應該總在初始化階段完成。

5. 如何使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 XMMATRIX
fxColorVar->SetFloatVector( (float*)&v ); // assume v is of type XMVECTOR
fxSizeVar->>SetFloat( f );
fxIndexVar->SetInt(  );
fxOptionOnVar->SetBool( true );
           

注意,這些語句修改的隻是effect對象在系統記憶體中的一個副本,它并沒有傳送到GPU記憶體中。是以在執行繪圖操作時,我們必須使用Apply方法更新GPU記憶體。這樣做的原因是為了提高效率,避免頻繁地更新GPU記憶體。如果每修改一個變量就要更新一次GPU記憶體,那麼效率會很低。

注意:effect變量不一定要被類型化。例如,可以有如下代碼:

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

這種方式可以用來設定任意大小的變量(例如,普通結構體)。注意,ID3D11EffectVectorVariable接口使用4D向量。如果你希望使用3D向量的話,那應該像上面那樣使用ID3D11EffectVariable接口。

(3). 獲得指向technique對象的指針

除了常量緩沖變量之外,我們還需要獲得指向technique對象的指針。實作方法如下:

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

該方法隻包含一個用于指定technique名稱的字元串參數。

6. 使用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*>(&worldViewPr
oj));

D3DX11_TECHNIQUE_DESC techDesc;
mTech->GetDesc(&techDesc);
for(UINT p = ;p < techDesc.Passes;++p )
{
    mTech->GetPassByIndex(p)->Apply(,md3dImmediateContext);
    //  繪制幾何體
    md3dImmediateContext->DrawIndexed(, , );
}
           

當使用pass來繪制幾何體時,Direct3D會啟用在pass中指定的着色器和渲染狀态。ID3D11EffectTechnique::GetPassByIndex方法傳回一個指定索引的pass對象的ID3D11EffectPass接口指針。Apply方法更新存儲在GPU記憶體中的常量緩沖、将着色器程式綁定到管線、并啟用在pass中指定的各種渲染狀态。在目前版本的Direct3D 11中,ID3D11EffectPass::Apply方法的第一個參數還未使用,應設定為0;第二個參數指向pass使用的裝置上下文的指針。如果你需要在繪圖調用之間改變常量緩沖中的變量值,那你必須在繪制幾何體之前調用Apply方法:

for(UINT i = ; i < techDesc.Passes; ++i)
{
    ID3D11EffectPass* pass = mTech->GetPassByIndex(i);

    //設定地面幾何體的WVP組合矩陣
    worldViewProj = mLandWorld*mView*mProj;
    mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(& worldViewProj);
    pass->Apply(, md3dImmediateContext);
    mLand.draw();

    // 設定水波幾何體的WVP組合矩陣
    worldViewProj = mWavesWorld*mView*mProj;
    mfxWorldViewProj->SetMatrix(reinterpret_cast<float*>(& worldViewProj);
    pass->Apply( ,md3dImmediateContext);
    mWaves.draw();
}
           

7. 編譯期間生成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檔案選擇屬性,添加自定義生成工具(見下圖,在項目中添加自定義生成工具):

DirectX11 EffectsEffects

調試模式:

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(, std::ios_base::end);
int size = (int)fin.tellg();
fin.seekg(, std::ios_base::beg);
std::vector<char> compiledShader(size);

fin.read(&compiledShader[],size);
fin.close();

HR(D3DX11CreateEffectFromMemory(&compiledShader[],size,
    , md3dDevice, &mFX));
           

除了在顔色立方體示範程式中我們在運作時編譯了shader之外,本書的其他示例都是在生成過程中編譯了所有shader。

8. 避免動态分支語句

在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參數(uniform譯為不變的,相當于常量),用來表示陰影品質等級。這個參數值是不同的,但對每個像素來說卻是不變的,當你使用 uniform/constant的時候。此外,我們不會在運作時改變它,就像常量緩存一樣。我們是在編譯時設定這些參數的,而且這些值在編譯時就是已知的,是以effect架構會基于這個值生成不同的shader代碼。這樣,我們不用複制代碼(effect架構幫我們在編譯時複制了這些代碼)就可以生成低、中、高三種不同陰影品質的shader代碼,而且沒有用到條件分支語句。

下面的兩個例子是使用shader生成器的常見情景:

1.是否需要紋理?有個應用程式需要在一些物體上施加紋理,而另一些物體不使用紋理。一個解決方法是建立兩個像素着色器,一個提供紋理而另一個不提供。或者我們也可以使用shader生成技巧建立兩個像素着色器,然後在C++程式中選擇期望的technique。

2.使用多少個光源?一個遊戲關卡可能會支援1至4個光源。光源越多,光照計算就越慢。我們可以基于光源數量設計不同的頂點着色器,或者也可以使用shader生成技巧建立四個頂點着色器,然後在C++程式中根據目前激活的光源數量選擇期望的technique。

繼續閱讀