DirectX11--HLSL編譯著色器的三種方法
前言
本文不考慮Effects11(FX11),而是原始的HLSL語言。
該文章從教程02單獨抽離出來作為單獨的教程。
目前編譯與載入著色器的方法如下:
-
使用Visual Studio中的HLSL編譯器,隨專案編譯期間一同編譯,並生成
.cso
(Compiled Shader Object)物件檔案,在執行期間載入該檔案以讀取位元組碼。 -
使用Visual Studio中的HLSL編譯器,隨專案編譯期間一同編譯,並生成
.inc
或.h
的標頭檔案,著色器位元組碼在編譯期間就可以確定。 - 在程式執行期間編譯著色器程式碼,並讀取生成的位元組碼。
在個人的DX11專案中,使用的是方法1(優先)和方法3的混合形式。儘管方法2是最近了解到的,但個人目前並不考慮更換為該方法。
ofollow,noindex" target="_blank">DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裡彙報。
與著色器相關的副檔名
為了符合微軟的約定,需要為你的著色器程式碼使用下面的副檔名(有所修改):
-
副檔名為
.hlsl
的檔案用於編寫HLSL的原始碼,參與編譯 -
副檔名為
.hlsli
的檔案作為HLSL的標標頭檔案,不參與編譯 -
副檔名為
.cso
的檔案作為已編譯的著色器物件(Compiled Shader Object) -
副檔名為
.inc
或.h
的檔案是C++的標頭檔案,但它的內部包含了著色器的位元組碼,使用BYTE
陣列來記錄
編譯期產生物件檔案,並在執行期載入
現在以Rendering a Triangle專案為例,現在我們已經編寫好的著色器檔案有Triangle.hlsli
,Triangle_VS.hlsl
,Triangle_PS.hlsl
這三個,可以將它拉進專案當中。
其中Triangle.hlsli
作為HLSL的標頭檔案預設不參與專案的編譯過程。
而對於Triangle_VS.hlsl
和Triangle_PS.hlsl
,則在專案屬性要這樣設定:
生成專案後,需要留意在輸出視窗(生成)中是否出現了下面的內容:

只有出現了上述內容,才說明成功編譯出物件檔案,否則說明沒有被編譯出來。如果你之前已經編譯出物件檔案,再編譯時沒有出現該輸出結果,可能需要先刪除之前編譯出來的物件檔案再試一次。
D3DReadFileToBlob函式--讀取編譯好的著色器二進位制資訊
對著色器程式碼或檔案的相關操作位於標頭檔案d3dcompiler.h
接下來,我們使用下面的函式來讀取編譯好的著色器二進位制資訊:
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, mVertexShader.GetAddressOf())); // 建立頂點佈局 HR(md3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout), blob->GetBufferPointer(), blob->GetBufferSize(), mVertexLayout.GetAddressOf()));
然後就可以拿獲取到的ID3DBlob
來建立著色器了。建立著色器和頂點佈局的部分在本文不進行討論,請回到教程02繼續檢視。
該方法的特點是會在你的專案資料夾中產生編譯好的著色器二進位制檔案,並且需要你在程式執行的時候直接讀進來。
編譯器產生標頭檔案,並在專案中包含該檔案
對於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(md3dDevice->CreateVertexShader(gTriangle_VS, sizeof(gTriangle_VS), nullptr, mVertexShader.GetAddressOf())); // 建立並繫結頂點佈局 HR(md3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout), gTriangle_VS, sizeof(gTriangle_VS), mVertexLayout.GetAddressOf()));
接下來就可以生成整個專案了,需要留意是否有紅色部分的輸出,否則可能沒有成功編譯出.inc
檔案(這可能會在已有.inc
檔案再次編譯的時候導致出現問題,需要刪除原來的.inc
檔案)。
由於上述兩個標頭檔案的產生(即著色器的編譯)先於專案的編譯,在沒有產生這兩個標頭檔案的時候,你也可以忍著編譯錯誤先把上述程式碼新增進去,然後編譯的時候就一切正常了。
該方法的特點是所有的過程均在編譯期完成,著色器位元組碼鑲嵌在了你的應用程式內部,可能會導致應用程式變大。
執行期間編譯著色器程式碼,生成位元組碼
現在你需要了解這些函式
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
和標準庫的filesystem
:
// 該函式需要包含filesystem標頭檔案,並using namespace std::experimental;(C++11/14) // ------------------------------ // CreateShaderFromFile函式 // ------------------------------ // [In]objFileNameInOut 編譯好的著色器二進位制檔案(.cso),若有指定則優先尋找該檔案並讀取 // [In]hlslFileName著色器程式碼,若未找到著色器二進位制檔案則編譯著色器程式碼 // [In]entryPoint入口點(指定開始的函式) // [In]shaderModel著色器模型,格式為"*s_5_0",*可以為c,d,g,h,p,v之一 // [Out]ppBlobOut輸出著色器二進位制資訊 HRESULT CreateShaderFromFile(const WCHAR * objFileNameInOut, const WCHAR * hlslFileName, LPCSTR entryPoint, LPCSTR shaderModel, ID3DBlob ** ppBlobOut) { HRESULT hr = S_OK; // 尋找是否有已經編譯好的頂點著色器 if (objFileNameInOut && filesystem::exists(objFileNameInOut)) { HR(D3DReadFileToBlob(objFileNameInOut, ppBlobOut)); } else { DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS; #ifdef _DEBUG // 設定 D3DCOMPILE_DEBUG 標誌用於獲取著色器除錯資訊。該標誌可以提升除錯體驗, // 但仍然允許著色器進行優化操作 dwShaderFlags |= D3DCOMPILE_DEBUG; // 在Debug環境下禁用優化以避免出現一些不合理的情況 dwShaderFlags |= D3DCOMPILE_SKIP_OPTIMIZATION; #endif ComPtr<ID3DBlob> errorBlob = nullptr; hr = D3DCompileFromFile(hlslFileName, nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, entryPoint, shaderModel, dwShaderFlags, 0, ppBlobOut, errorBlob.GetAddressOf()); if (FAILED(hr)) { if (errorBlob != nullptr) { OutputDebugStringA(reinterpret_cast<const char*>(errorBlob->GetBufferPointer())); } return hr; } // 若指定了輸出檔名,則將著色器二進位制資訊輸出 if (objFileNameInOut) { HR(D3DWriteBlobToFile(*ppBlobOut, objFileNameInOut, FALSE)); } } return hr; }
使用方式如下:
// 建立頂點著色器 HR(CreateShaderFromFile(L"HLSL\\Triangle_VS.cso", L"HLSL\\Triangle_VS.hlsl", "VS", "vs_5_0", blob.ReleaseAndGetAddressOf())); HR(md3dDevice->CreateVertexShader(blob->GetBufferPointer(), blob->GetBufferSize(), nullptr, mVertexShader.GetAddressOf())); // 建立並繫結頂點佈局 HR(md3dDevice->CreateInputLayout(VertexPosColor::inputLayout, ARRAYSIZE(VertexPosColor::inputLayout), blob->GetBufferPointer(), blob->GetBufferSize(), mVertexLayout.GetAddressOf()));
參考文章:
DirectX11 With Windows SDK完整目錄