前言
本教程不考慮Effects11(FX11),而是基於原始的HLSL。
目前編譯與加載着色器的方法如下:
- 使用Visual Studio中的HLSL編譯器,隨項目編譯期間一同編譯,並生成
.cso
(Compiled Shader Object)對象文件,在運行期間加載該文件以讀取字節碼。 - 使用Visual Studio中的HLSL編譯器,隨項目編譯期間一同編譯,並生成
.inc
或.h
的頭文件,着色器字節碼在編譯期間就可以確定。 - 在程序運行期間編譯着色器代碼,並讀取生成的字節碼。
在個人的DX11項目中,使用的是方法1(優先)和方法3的混合形式。盡管方法2是最近了解到的,但個人目前並不考慮更換為該方法。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
與着色器相關的文件擴展名
為了符合微軟的約定,需要為你的着色器代碼使用下面的擴展名(有所修改):
- 擴展名為
.hlsl
的文件用於編寫HLSL的源代碼,參與編譯 - 擴展名為
.hlsli
的文件作為HLSL的標頭文件,不參與編譯 - 擴展名為
.cso
的文件作為已編譯的着色器對象(Compiled Shader Object) - 擴展名為
.inc
或.h
的文件是C++的頭文件,但它的內部包含了着色器的字節碼,使用BYTE
數組來記錄
方法1:編譯期產生對象文件,並在運行期加載
現在以Rendering a Triangle項目為例,現在我們已經編寫好的着色器文件有Triangle.hlsli
, Triangle_VS.hlsl
, Triangle_PS.hlsl
這三個,它們存放項目在HLSL文件夾內。現在你可以將它拉進項目當中。
其中Triangle.hlsli
作為HLSL的頭文件默認不參與項目的編譯過程。
而對於Triangle_VS.hlsl
和Triangle_PS.hlsl
,則在項目屬性要這樣設置:
其中入口點名稱指的是該着色器階段最先開始調用的函數名。比如在C/C++/新建的.hlsl文件中,默認的入口點名稱是main。而上面的例子中,我們希望讓頂點着色器從VS
函數開始運行,則需要指定入口點為VS
。
關於着色器模型,因為假定用戶的顯卡已經支持特性等級11.0,這里使用的是Shader Model 5.0
,如果你的顯卡不支持特性等級11.0,則需要將特性等級降為10.0/10.1,分別對應能使用的着色器模型為4.0/4.1
生成項目后,需要留意在輸出窗口(生成)中是否出現了下面的內容:
只有出現了上述內容,才說明成功編譯出對象文件,否則說明沒有被編譯出來。如果你之前已經編譯出對象文件,再編譯時沒有出現該輸出結果,可能需要先刪除之前編譯出來的對象文件再試一次。
D3DReadFileToBlob函數--讀取編譯好的着色器二進制信息
對着色器代碼或文件的相關操作位於頭文件d3dcompiler.h
,而且你還需要添加靜態庫d3dcompiler.lib
接下來,我們使用下面的函數來讀取編譯好的着色器二進制信息:
HRESULT D3DReadFileToBlob(LPCWSTR pFileName, // [In].cso文件名
ID3DBlob** ppContents); // [Out]獲取二進制大數據塊
注意:如果你的項目中不存在該函數,說明你可能預先包含了DX SDK,然而該教程使用的是Windows SDK,該函數位於D3DCompiler >= 46的版本,因此你需要剔除DX SDK的包含路徑和庫路徑。
使用方式也十分簡單(以創建頂點着色器和頂點布局為例):
ComPtr<ID3DBlob> blob;
HR(D3DReadFileToBlob(L"HLSL\\Triangle_VS.cso", blob.GetAddressOf()));
HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader.GetAddressOf()));
// 創建頂點布局
HR(md3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout.GetAddressOf()));
然后就可以拿獲取到的ID3DBlob
來創建着色器了。創建着色器和頂點布局的部分在本文不進行討論,請回到教程02繼續查看。
該方法的特點是會在你的項目文件夾中產生編譯好的着色器二進制文件,並且需要你在程序運行的時候直接讀進來。
方法2:編譯器產生頭文件,並在項目中包含該文件
對於Triangle_VS.hlsl
和Triangle_PS.hlsl
,在項目屬性要這樣設置:
這里關於頭文件的名稱以及內部的全局變量名可以自行決定。
頭文件 經過編譯后會在HLSL文件夾產生Triangle_VS.inc
和Triangle_PS.inc
兩個文件,觀察里面的代碼你可以發現里面有匯編部分(不會包含進代碼中)和一個全局變量,在Triangle_VS.inc
中產生的是全局變量gTriangle_VS
,而在Triangle_PS.inc
中產生的是全局變量gTriangle_PS
。這兩個變量都是BYTE
數組,里面的內容正是編譯好的字節碼。
現在需要在你需要編寫創建着色器相關代碼的源文件上面包含這兩個頭文件:
#include "HLSL/Triangle_VS.inc"
#include "HLSL/Triangle_PS.inc"
然后創建頂點着色器和頂點布局的代碼變成了這樣:
// 創建頂點着色器
HR(m_pd3dDevice->CreateVertexShader(gTriangle_VS, sizeof(gTriangle_VS), nullptr, m_pVertexShader.GetAddressOf()));
// 創建並綁定頂點布局
HR(m_pd3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout),
gTriangle_VS, sizeof(gTriangle_VS), m_pVertexLayout.GetAddressOf()));
接下來就可以生成整個項目了,需要留意是否有紅色部分的輸出,否則可能沒有成功編譯出.inc
文件(這可能會在已有.inc
文件再次編譯的時候導致出現問題,需要刪除原來的.inc
文件)。
由於上述兩個頭文件的產生(即着色器的編譯)先於項目的編譯,在沒有產生這兩個頭文件的時候,你也可以忍着編譯錯誤先把上述代碼添加進去,然后編譯的時候就一切正常了。
該方法的特點是所有的過程均在編譯期完成,着色器字節碼鑲嵌在了你的應用程序內部,可能會導致應用程序變大。
方法3:運行期間編譯着色器代碼,生成字節碼
現在你需要了解這些函數
D3DCompileFromFile函數--運行期編譯.hlsl文件
HRESULT D3DCompileFromFile(
LPCWSTR pFileName, // [In]要編譯的.hlsl文件
CONST D3D_SHADER_MACRO* pDefines, // [In_Opt]忽略
ID3DInclude* pInclude, // [In_Opt]如何應對#include宏
LPCSTR pEntrypoint, // [In]入口函數名
LPCSTR pTarget, // [In]使用的着色器模型
UINT Flags1, // [In]D3DCOMPILE系列宏
UINT Flags2, // [In]D3DCOMPILE_FLAGS2系列宏
ID3DBlob** ppCode, // [Out]獲得着色器的二進制塊
ID3DBlob** ppErrorMsgs); // [Out]可能會獲得錯誤信息的二進制塊
再次注意:如果你的項目中不存在該函數,說明你可能預先包含了DX SDK,然而該教程使用的是Windows SDK,該函數位於D3DCompiler >= 46的版本,因此你需要剔除DX SDK的包含路徑和庫路徑。
其中pInclude
用於決定如何處理包含文件。如果設為nullptr
,則編譯的着色器代碼包含#include
時會引發編譯器報錯。如果你需要使用#include
,可以傳遞D3D_COMPILE_STANDARD_FILE_INCLUDE
宏,這是一個默認的包含句柄,可以按該着色器代碼所處的相對路徑去搜索對應的頭文件並包含進來。
#define D3D_COMPILE_STANDARD_FILE_INCLUDE ((ID3DInclude*)(UINT_PTR)1)
D3DWriteBlobToFile函數--將編譯好的着色器二進制信息寫入文件
HRESULT D3DWriteBlobToFile(
ID3DBlob* pBlob, // [In]編譯好的着色器二進制塊
LPCWSTR pFileName, // [In]輸出文件名
BOOL bOverwrite); // [In]是否允許覆蓋
對於bOverwrite
來說,無論是TRUE
還是FALSE
都無關緊要,因為我們只有在檢測到沒有編譯好的着色器文件時才會啟動運行期編譯,然后再保存到文件。
具體用法已經集成在下面的CreateShaderFromFile
函數中了
CreateShaderFromFile函數的實現
下面是CreateShaderFromFile
函數的實現,現在該函數已經放到了d3dUtil.h中,需要依賴dxerr
,你也可以自己修改這個函數的實現:
// 安全COM組件釋放宏
#define SAFE_RELEASE(p) { if ((p)) { (p)->Release(); (p) = nullptr; } }
// ------------------------------
// CreateShaderFromFile函數
// ------------------------------
// [In]csoFileNameInOut 編譯好的着色器二進制文件(.cso),若有指定則優先尋找該文件並讀取
// [In]hlslFileName 着色器代碼,若未找到着色器二進制文件則編譯着色器代碼
// [In]entryPoint 入口點(指定開始的函數)
// [In]shaderModel 着色器模型,格式為"*s_5_0",*可以為c,d,g,h,p,v之一
// [Out]ppBlobOut 輸出着色器二進制信息
HRESULT CreateShaderFromFile(const WCHAR * csoFileNameInOut, const WCHAR * hlslFileName,
LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob ** ppBlobOut)
{
HRESULT hr = S_OK;
// 尋找是否有已經編譯好的頂點着色器
if (csoFileNameInOut && D3DReadFileToBlob(csoFileNameInOut, ppBlobOut) == S_OK)
{
return hr;
}
else
{
DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#ifdef _DEBUG
// 設置 D3DCOMPILE_DEBUG 標志用於獲取着色器調試信息。該標志可以提升調試體驗,
// 但仍然允許着色器進行優化操作
dwShaderFlags |= D3DCOMPILE_DEBUG;
// 在Debug環境下禁用優化以避免出現一些不合理的情況
dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
ID3DBlob* errorBlob = nullptr;
hr = D3DCompileFromFile(hlslFileName, nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entryPoint, shaderModel,
dwShaderFlags, 0, ppBlobOut, &errorBlob);
if (FAILED(hr))
{
if (errorBlob != nullptr)
{
OutputDebugStringA(reinterpret_cast<const char*>(errorBlob->GetBufferPointer()));
}
SAFE_RELEASE(errorBlob);
return hr;
}
// 若指定了輸出文件名,則將着色器二進制信息輸出
if (csoFileNameInOut)
{
return D3DWriteBlobToFile(*ppBlobOut, csoFileNameInOut, FALSE);
}
}
return hr;
}
使用方式如下:
ComPtr<ID3DBlob> blob;
// 創建頂點着色器
HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.cso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf()));
HR(m_pd3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, m_pVertexShader.GetAddressOf()));
// 創建並綁定頂點布局
HR(m_pd3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout),
blob->GetBufferPointer(), blob->GetBufferSize(), m_pVertexLayout.GetAddressOf()));
參考文章:
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。