DirectX11--HLSL編譯着色器的三種方法

前言

本教程不考慮Effects11(FX11),而是基於原始的HLSL。

目前編譯與加載着色器的方法如下:

  1. 使用Visual Studio中的HLSL編譯器,隨項目編譯期間一同編譯,並生成.cso(Compiled Shader Object)對象文件,在運行期間加載該文件以讀取字節碼。
  2. 使用Visual Studio中的HLSL編譯器,隨項目編譯期間一同編譯,並生成.inc.h的頭文件,着色器字節碼在編譯期間就可以確定。
  3. 在程序運行期間編譯着色器代碼,並讀取生成的字節碼。

在個人的DX11項目中,使用的是方法1(優先)和方法3的混合形式。儘管方法2是最近瞭解到的,但個人目前並不考慮更換爲該方法。

DirectX11 With Windows SDK完整目錄

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

與着色器相關的文件擴展名

爲了符合微軟的約定,需要爲你的着色器代碼使用下面的擴展名(有所修改):

  1. 擴展名爲.hlsl的文件用於編寫HLSL的源代碼,參與編譯
  2. 擴展名爲.hlsli的文件作爲HLSL的標頭文件,不參與編譯
  3. 擴展名爲.cso的文件作爲已編譯的着色器對象(Compiled Shader Object)
  4. 擴展名爲.inc.h的文件是C++的頭文件,但它的內部包含了着色器的字節碼,使用BYTE數組來記錄

方法1:編譯期產生對象文件,並在運行期加載

現在以Rendering a Triangle項目爲例,現在我們已經編寫好的着色器文件有Triangle.hlsli, Triangle_VS.hlsl, Triangle_PS.hlsl這三個,可以將它拉進項目當中。

其中Triangle.hlsli作爲HLSL的頭文件默認不參與項目的編譯過程。

而對於Triangle_VS.hlslTriangle_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繼續查看。

該方法的特點是會在你的項目文件夾中產生編譯好的着色器二進制文件,並且需要你在程序運行的時候直接讀進來。

方法2:編譯器產生頭文件,並在項目中包含該文件

對於Triangle_VS.hlslTriangle_PS.hlsl,在項目屬性要這樣設置:

這裏關於頭文件的名稱以及內部的全局變量名可以自行決定。

頭文件 經過編譯後會在HLSL文件夾產生Triangle_VS.incTriangle_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文件)。

由於上述兩個頭文件的產生(即着色器的編譯)先於項目的編譯,在沒有產生這兩個頭文件的時候,你也可以忍着編譯錯誤先把上述代碼添加進去,然後編譯的時候就一切正常了。

該方法的特點是所有的過程均在編譯期完成,着色器字節碼鑲嵌在了你的應用程序內部,可能會導致應用程序變大。

方法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和標準庫的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()));

參考文章:

Compiling Shaders

How To: Compile a Shader

DirectX11 With Windows SDK完整目錄

歡迎加入QQ羣: 727623616 可以一起探討DX11,以及有什麼問題也可以在這裏彙報。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章