今回の記事では頂点シェーダとピクセルシェーダをDirectX11で使うまでの実装方法について説明してきます。
DirectX11で使用するシェーダの言語はHLSLです。
今回はこのHLSLのコンパイルも含めてランタイム(実行時)で行う方法について記述します。
前回の記事でも言いましたがDirectX11では固定パイプラインが廃止されました。
なので描画するにはシェーダに実装が必須となります。
では実装について順を追って見ていきましょう。
シェーダのコンパイル
DirectX11で使用するHLSLはGPUの動作を記述するプログラム言語です。
ps.hlsl
// 入力情報 struct PS_INPUT { float4 OutPosition : SV_POSITION; float4 Color : COLOR0; }; void PS(in PS_INPUT In, out float4 Out : SV_Target0) { Out = In.Color; }
これがHLSLでカラーを出力するだけのピクセルシェーダの実装です。
ただHLSLはテキスト形式なので、そのままではハードウェア側で理解ができません。
動作させるためにはコンパイルして側で理解できるバイナリコードにする必要があります。
では早速コンパイルするための処理を見ていきましょう。
// シェーダコンパイル周りの機能をインクルード #include <d3dcompiler.h> // シェーダコンパイル用の静的ライブラリをリンク #pragma comment(lib, "d3dcompiler.lib") ID3DBlob* pBlob; ID3DBlob* pErrorMsg; HRESULT hr = D3DCompileFromFile( "ps.hlsl", NULL, NULL, "PS", "ps_5_0", 0, 0, &pBlob, &pErrorMsg ); if (FAILED(hr)) { // シェーダのエラー内容を表示 MessageBox(NULL, (char*)pErrorMsg->GetBufferPointer(), "Compile Error", MB_OK); SafeRelease(pErrorMsg); } else { // コンパイル成功 }
シェーダのコンパイルを行うためには下記の2つが必要になります。
- d3dcompiler.hのインクルード
- d3dcompiler.libをリンク
これでピクセルシェーダをコンパイルすることができます。
ps.hlslをピクセルシェーダのシェーダモデル5.0としてコンパイルしています。
コンパイル時にエラーが出た場合はエラーメッセージが pErrorMsg で取得できます。
成功した場合は pBlob にシェーダバイナリが格納されます。
D3DCompileFromFile
pFileName | コンパイルするHLSLを記述したファイルへのパスを指定します。 |
---|---|
pDefines | HLSL上で事前に定義するdefineを指定できます。 指定しない場合はNULLも可能です。 |
pInclude | インクルードファイルを取り扱うためのID3DIncludeを指定できます。 includeを使用しない場合はNULLで問題ありません。D3D_COMPILE_STANDARD_FILE_INCLUDEを指定することで相対パスでのincludeを可能にします。 |
pEntrypoint | エントリーポイントとなる関数名を指定します。 |
pTarget | コンパイル時のターゲットとなるシェーダモデルを指定します。 |
Flags1 | コンパイルオプションを指定できます。 オプション内容については公式のドキュメントを参考にしてください。 |
Flags2 | エフェクトファイル用のコンパイルオプションを指定できます。 シェーダをコンパイルする場合は 0 を指定します。 |
ppCode | コンパイル結果のバイナリコードが格納されます。 |
ppErrorMsgs | コンパイルエラー時にエラー内容が格納されます。 |
この関数を使用してバイナリコードを生成して使用します。
※プログラム上の文字列から直接コンパイルしたい場合はD3DCOMPILE関数を使用します。
シェーダ管理クラス
では続いてバイナリコードから頂点シェーダとピクセルシェーダを生成・管理する実装を見ていきましょう。
今回はシェーダを管理する規定クラスを用意して作っていきます。
テンプレートクラスを使用するのでテンプレートの知識が必要になります。
Shader.h
// シェーダ管理用クラステンプレート template<class T> class Shader { public: Shader(void) : m_pShader(NULL) {} virtual ~Shader(void) { Finalize(); } // 初期化 virtual bool Initialize(DirectX11& directX, const char* pFilename, const char* pEntryPoint) = 0; // 終了処理 virtual void Finalize(void) { SafeRelease(m_pShader); } // シェーダの取得 T* GetShader(void) { return m_pShader; } protected: // コンパイル bool Compile(const char* pFilename, const char* pEntryPoint, const char* pShaderModel, ID3DBlob** ppBlob); protected: T* m_pShader; // シェーダ }; // VertexShaderクラス class VertexShader : public Shader<ID3D11VertexShader> { public: VertexShader(void) : m_pShaderCode(NULL) {} // 初期化 bool Initialize(DirectX11& directX, const char* pFilename, const char* pEntryPoint) override; // 終了処理 void Finalize(void) override; // シェーダコードの取得 ID3DBlob* GetShaderCode(void) { return m_pShaderCode; } private: ID3DBlob* m_pShaderCode; }; // PixelShaderクラス class PixelShader : public Shader<ID3D11PixelShader > { public: // 初期化 bool Initialize(DirectX11& directX, const char* pFilename, const char* pEntryPoint) override; };
今回はShaderという名前でクラステンプレートを用意しています。
これを継承することで重複するコードを削減しています。
機能は単純です。
- 初期化
- 解放
- シェーダの取得
- コンパイル
これだけです。
継承した先ではそれぞれコンパイルしたバイナリコードからシェーダを生成します。
Shader.cpp
include "Shader.h" // シェーダコンパイル周りの機能をインクルード #include <d3dcompiler.h> // シェーダコンパイル用の静的ライブラリをリンク #pragma comment(lib, "d3dcompiler.lib") // コンパイル template<class T> bool Shader<T>::Compile(const char* pFilename, const char* pEntryPoint, const char* pShaderModel, ID3DBlob** ppBlob) { WCHAR path[256]; size_t len = 0; mbstowcs_s(&len, path, 256, pFilename, _TRUNCATE); ID3DBlob* pErrorMsg; HRESULT hr = D3DCompileFromFile( path, NULL, D3D_COMPILE_STANDARD_FILE_INCLUDE, pEntryPoint, pShaderModel, 0, 0, ppBlob, &pErrorMsg ); if (FAILED(hr)) { // シェーダのエラー内容を表示 MessageBox(NULL, (char*)pErrorMsg->GetBufferPointer(), "Compile Error", MB_OK); SafeRelease(pErrorMsg); return false; } return true; } // 初期化 bool VertexShader::Initialize(DirectX11& directX, const char* pFilename, const char* pEntryPoint) { ID3DBlob* pBlob; if (!Compile(pFilename, pEntryPoint, "vs_5_0", &pBlob)) { return false; } auto pDevice = directX.GetDevice(); HRESULT hr; // 頂点シェーダの生成 hr = pDevice->CreateVertexShader( pBlob->GetBufferPointer(), pBlob->GetBufferSize(), NULL, &m_pShader ); if (FAILED(hr)) { SafeRelease(pBlob); return false; } m_pShaderCode = pBlob; return true; } // 終了処理 void VertexShader::Finalize(void) { SafeRelease(m_pShaderCode); Shader::Finalize(); } // 初期化 bool PixelShader::Initialize(DirectX11& directX, const char* pFilename, const char* pEntryPoint) { ID3DBlob* pBlob; if (!Compile(pFilename, pEntryPoint, "ps_5_0", &pBlob)) { return false; } auto pDevice = directX.GetDevice(); HRESULT hr; // ピクセルシェーダの生成 hr = pDevice->CreatePixelShader( pBlob->GetBufferPointer(), pBlob->GetBufferSize(), NULL, &m_pShader ); SafeRelease(pBlob); return SUCCEEDED(hr); }
基底クラスでは解放処理と最初のページで記載したコンパイル処理を記述しています。
コンパイル時に使用するシェーダモデルは種類毎に異なるので各シェーダで指定しています。
頂点シェーダとピクセルシェーダの実装の違いは下記です。
頂点シェーダ
- シェーダモデル:vs_5_0
- 生成処理:ID3DDevice::CreateVertexShader
- シェーダコードの保持
ピクセルシェーダ
- シェーダモデル:ps_5_0
- 生成処理:ID3DDevice::CreatePixelShader
各シェーダで違う部分はこれだけです。
他のシェーダも実装は同様なので基底クラスを使うことで全て簡略化できます。
※後述しますが頂点シェーダだけはシェーダバイナリを保持する必要があります。
頂点シェーダとピクセルシェーダ
プログラム上でシェーダを扱う機能は実装できました。
次は実際に使用するシェーダファイルを作成していきましょう。
今回は単純に頂点の値を入力してそのままピクセルシェーダで表示するだけの機能にします。
Shader.hlsl
// 頂点シェーダ入力情報 struct VS_INPUT { float3 Position : POSITION; float4 Color : COLOR; }; // 頂点シェーダ出力情報 struct VS_OUTPUT { float4 Position : SV_POSITION; float4 Color : COLOR0; }; // 頂点シェーダ void VS(in VS_INPUT In, out VS_OUTPUT Out) { Out.Position.xyz = In.Position.xyz; Out.Position.w = 1.0f; Out.Color = In.Color; } // ピクセルシェーダ入力情報 typedef VS_OUTPUT PS_INPUT; // ピクセルシェーダ void PS(in PS_INPUT In, out float4 OutColor : SV_Target0) { OutColor = In.Color; }
内容は単純です。
VS関数
これが頂点シェーダの本体となります。
先ずVS_INPUTで座標と頂点カラーを受け取ります。
そして受け取った内容をそのままVS_OUTPUTに渡して出力しています。
引数の頭に in、outとついていますが、これで入出力の内容を設定しています。
outは戻り値で指定することもできます。
VS_INPUTの変数の後ろに記述している POSITION や COLOR については任意の文字を指定できます。
次の項目で記述しますが、この部分は頂点バッファのどの値を関連付るかという設定をC++側で行います。
VS_OUTPUTの変数の後ろに記述している SV_POSITION や COLOR0 は使用できる名前が決まっています。
一般的に座標はSV_POSITION、カラーはCOLOR[n]、UV値はTEXCOORD[n]となります。
他にもありますが、詳しくは公式ドキュメントを参考にしてください。
PS関数
こちらはピクセルシェーダの本体です。
VS_OUTPUTで出力された内容と同じものが入力で入ってきます。
なのでVS_OUTPUTをtypedefでPS_INPUTとしています。
ピクセルシェーダの出力はSV_Targetを指定する必要があります。
今回はSV_Target0で 0 となっていますが、複数のレンダーターゲットに同時に出力する場合は1, 2, 3と末尾を変更します。
頂点入力レイアウト
これで描画に最低限必要なシェーダが実装出来ました。
ただ頂点シェーダを扱うには頂点バッファと頂点シェーダの入力情報を関連付る必要があります。
入力情報の関連付にはID3D11InputLayoutを使用します。
InputLayout.h
// InputLayoutクラス class InputLayout { public: InputLayout(void) : m_pInputLayout(NULL) {} ~InputLayout(void) { SafeRelease(m_pInputLayout); } // 初期化 bool Initialize(DirectX11& directX, VertexShader& shader, D3D11_INPUT_ELEMENT_DESC* pElements, UINT num); // 終了処理 void Finalize(void); public: // 頂点入力レイアウトの取得 ID3D11InputLayout* GetInputLayout(void) { return m_pInputLayout; } private: ID3D11InputLayout* m_pInputLayout; // 頂点入力レイアウト };
ID3D11InputLayoutではD3D11_INPUT_ELEMENT_DESCとシェーダコードから入力情報をひもづけます。
InputLayout.cpp
#include "InputLayout.h" // 初期化 bool InputLayout::Initialize(DirectX11& directX, VertexShader& shader, D3D11_INPUT_ELEMENT_DESC* pElements, UINT num) { auto pDevice = directX.GetDevice(); auto pCode = shader.GetShaderCode(); // 入力レイアウトの生成 if (FAILED(pDevice->CreateInputLayout( pElements, num, pCode->GetBufferPointer(), pCode->GetBufferSize(), &m_pInputLayout))) { return false; } return true; } // 終了処理 void InputLayout::Finalize(void) { SafeRelease(m_pInputLayout); }
頂点シェーダへの入力情報はID3DDevice::CreateInputLayoutで生成します。
ここでD3D11_INPUT_ELEMENT_DESCを使って頂点シェーダのVS_INPUTに指定する POSITION や COLOR といったSemantic(意味付け)を指定します。
今回は座標とカラーのみなので下記のような設定を渡します。
頂点入力情報の設定
// 頂点データ struct Vertex { float x, y, z; float r, g, b, a; }; // Vertexに対応する入力設定 D3D11_INPUT_ELEMENT_DESC elements[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 4 * 3, D3D11_INPUT_PER_VERTEX_DATA, 0 }, };
これで POSITION と COLOR にそれぞれ値の紐付けが行われます。
D3D11_INPUT_ELEMENT_DESC
SemanticName | シェーダの入力情報に意味付けを行います。 変数の後に記述する内容と一致している場合、この設定の値が紐付きます。 |
SemanticIndex | 同一のSemantic名を複数使用する場合はここで番号を指定できます。 SemanticNameをPOSITIONにした際に0, 1のIndexを使用する場合は次が使用できます。 ・POSITION0 ・POSITION1 |
Format | 入力する変数の型を指定できます。 渡すバッファのサイズに合わせてドキュメントを参考に指定してください。 |
InputSlot | 使用する頂点バッファのスロットを指定できます。 これにより複数の頂点バッファを使用して描画することもできます。 |
AlignedByteOffset; | 関連付ける値が頂点内のどの位置にあるかオフセットを指定します。 byte単位での指定となります。 |
InputSlotClass | 頂点バッファの1要素を進める条件を指定します。 基本的にD3D11_INPUT_PER_VERTEX_DATAで問題ありません。 |
InstanceDataStepRate | D3D11_INPUT_PER_VERTEX_DATAの場合は 0 を指定します。 D3D11_INPUT_PER_INSTANCE_DATAの場合は描画するインスタンス数を指定します。 |
色々とややこしい設定もありますが、今のところは複数頂点バッファやインスタンス描画は使用しませんので最低限の設定だけで問題ありません。
描画処理
ここまで来れば後はそれぞれのクラスを使って描画するだけです!
初期化から描画、解放までの流れはこんな感じになります。
VertexBuffer vertexBuffer; VertexShader vertexShader; PixelShader pixelShader; InputLayout inputLayout; // 頂点データ struct Vertex { float x, y, z; UINT color; }; // シェーダと頂点バッファの初期化 bool Initialize(DirectX11& directX11) { Vertex vertices[] = { { -0.5f, -0.5f, 0.0f, 0xffffffff }, { -0.5f, 0.5f, 0.0f, 0xffffffff }, { 0.5f, -0.5f, 0.0f, 0xffffffff }, { 0.5f, 0.5f, 0.0f, 0xffffffff }, }; if (!vertexBuffer.Initialize(directX11, sizeof(vertices), vertices)) return false; if (!vertexShader.Initialize(directX11, "Shader.hlsl", "VS")) return false; if (!pixelShader.Initialize(directX11, "Shader.hlsl", "PS")) return false; // 入力レイアウト D3D11_INPUT_ELEMENT_DESC elements[] = { { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, { "COLOR", 0, DXGI_FORMAT_R8G8B8A8_UNORM, 0, 4 * 3, D3D11_INPUT_PER_VERTEX_DATA, 0 }, }; UINT numElements = ARRAYSIZE(elements); if (!inputLayout.Initialize(directX11, vertexShader, elements, numElements)) return false; return true; } // 終了処理 void Finalize() { inputLayout.Finalize(); vertexBuffer.Finalize(); vertexShader.Finalize(); pixelShader.Finalize(); } // 描画 void Render(DirectX11& directX11) { ID3D11DeviceContext* pContext = directX11.GetContext(); ID3D11RenderTargetView* pTarget = directX11.GetRenderTargetView(); // ビューポート(描画範囲の設定) D3D11_VIEWPORT ViewPort; ViewPort.TopLeftX = 0; ViewPort.TopLeftY = 0; ViewPort.Width = 1280; ViewPort.Height = 720; ViewPort.MinDepth = 0.0f; ViewPort.MaxDepth = 1.0f; // 描画するターゲットの設定 pContext->OMSetRenderTargets(1, &pTarget, NULL); pContext->RSSetViewports(1, &ViewPort); // 使用するシェーダの設定 pContext->VSSetShader(vertexShader.GetShader(), NULL, 0); pContext->PSSetShader(pixelShader.GetShader(), NULL, 0); // 頂点バッファと頂点レイアウトの設定 UINT offset = 0; UINT stride = sizeof(Vertex); ID3D11Buffer* pVBuffer = vertexBuffer.GetBuffer(); pContext->IASetVertexBuffers(0, 1, &pVBuffer, &stride, &offset); pContext->IASetInputLayout(inputLayout.GetInputLayout()); // 描画 pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); pContext->Draw(4, 0); }
これでシェーダを使った描画が行えます。
DirectX11では必ず描画にシェーダが必要となるので少し難易度が高くなっています。
ただ描画できるところまで持っていければあとはシェーダを触ってダイレクトに見た目に影響を与えられるので少しずつ触っていけるようにしましょう!
今後はシェーダを使った様々な演出も記事にしていこうと思います。