シェーダーとシェーダー リソースを操作する

ここでは、Windows 8 用の Microsoft DirectX ゲームを開発する際にシェーダーとシェーダー リソースを操作する方法について説明します。 グラフィックス デバイスとリソースを設定する方法を見てきました。おそらく、そのパイプラインの変更も開始しました。 それでは、ピクセル シェーダーと頂点シェーダーを見てみましょう。

シェーダー言語に慣れていない場合は、簡単に説明します。 シェーダーは、グラフィックス パイプライン内の特定のステージでコンパイルおよび実行される、小規模で低レベルのプログラムです。 専門は非常に高速な浮動小数点演算です。 最も一般的なシェーダー プログラムは次のとおりです。

  • 頂点シェーダー - シーン内の各頂点に対して実行されます。 このシェーダーは、呼び出し元アプリによって提供される頂点バッファー要素に対して動作し、ピクセル位置にラスター化される 4 成分の位置ベクトルが最小限に抑えられます。
  • ピクセル シェーダー - レンダー ターゲット内の各ピクセルに対して実行されます。 このシェーダーは、前のシェーダー ステージ (最も単純なパイプラインでは頂点シェーダー) からラスター化された座標を受け取り、そのピクセル位置の色 (またはその他の 4 成分値) を返し、レンダー ターゲットに書き込まれます。

この例には、ジオメトリのみを描画する非常に基本的な頂点シェーダーとピクセル シェーダー、基本的な照明計算を追加するより複雑なシェーダーが含まれています。

シェーダー プログラムは、Microsoft High Level Shader Language (HLSL) で記述されています。 HLSL 構文は C とよく似ていますが、ポインターはありません。 シェーダー プログラムは非常にコンパクトで効率的である必要があります。 シェーダーがコンパイルして命令が多すぎる場合は、実行できず、エラーが返されます。 (許可されている命令の正確な数は、Direct3D 機能レベルの一部であることに注意してください)。

Direct3D では、シェーダーは実行時にコンパイルされません。これらは、プログラムの残りの部分がコンパイルされるときにコンパイルされます。 Microsoft Visual Studio 2013 でアプリをコンパイルすると、HLSL ファイルは CSO (.cso) ファイルにコンパイルされ、アプリは描画前に GPU メモリに読み込んで配置する必要があります。 パッケージ化するときに、これらの CSO ファイルをアプリに含めるようにしてください。これらはメッシュやテクスチャと同じようにアセットです。

HLSL セマンティクスについて

HLSL セマンティクスは、多くの場合、新しい Direct3D 開発者にとって混乱のポイントとなることが多いため、続行する前に少し時間を取って説明することが重要です。 HLSL セマンティクスは、アプリとシェーダー プログラムの間で渡される値を識別する文字列です。 可能な文字列はさまざまですが、 POSITION や使用法を示す COLOR などの文字列を使用することをおすすめします。 定数バッファーまたは入力レイアウトを構築するときに、これらのセマンティクスを割り当てます。 類似する値に別のレジスタを使うために、セマンティクスに 0 ~ 7 の範囲の数値を追加できます。 COLOR0、COLOR1、COLOR2 などです。

"SV_" というプレフィックスが付いたセマンティクスは、シェーダー プログラムによって書き込みが行われるシステム値のセマンティクスです。CPU 上で実行されているゲーム自体で変更することはできません。 通常、これらのセマンティクスには、グラフィックス パイプライン内の別のシェーダー ステージからの入力または出力、または GPU によって完全に生成される値が含まれます。

また、SV_ セマンティクスは、シェーダー ステージに対する入出力を指定するために使用される場合は、さまざまな動作を示します。 たとえば、SV_POSITION (出力) には、頂点シェーダー ステージで変換された頂点データが含まれ、SV_POSITION (入力) には、ラスター化ステージ中に GPU によって補間されたピクセル位置の値が含まれます。

一般的な HLSL セマンティクスをいくつか次に示します。

  • 頂点バッファー データのPOSITION(n) です。 SV_POSITION によって、ピクセル位置がピクセル シェーダーに指定されます。これをゲームで書き込むことはできません。
  • 頂点バッファーによって提供される通常のデータのNORMAL(n)。
  • シェーダーに提供されるテクスチャ UV 座標データの TEXCOORD(n)。
  • シェーダーに提供される RGBA カラー データのCOLOR(n)。 これは、ラスター化時の値の補間を含め、座標データと同じように扱われることに注意してください。セマンティックは、それがカラー データであることを識別するのに役立ちます。
  • ピクセル シェーダーからターゲット テクスチャまたは他のピクセル バッファーへの書き込みを表すSV_Target[n]。

この例を確認すると、HLSL セマンティクスのいくつかの例が表示されます。

定数バッファーからの読み取り

そのバッファーがリソースとしてステージにアタッチされている場合、シェーダーは定数バッファーから読み取ることができます。 この例では、頂点シェーダーにのみ定数バッファーが割り当てられます。

定数バッファーは、C++ コードと、それにアクセスする対応する HLSL ファイルの 2 つの場所で宣言されます。

C++ コードで定数バッファー構造体を宣言する方法を次に示します。

typedef struct _constantBufferStruct {
    DirectX::XMFLOAT4X4 world;
    DirectX::XMFLOAT4X4 view;
    DirectX::XMFLOAT4X4 projection;
} ConstantBufferStruct;

C++ コードで定数バッファーの構造体を宣言するときは、すべてのデータが 16 バイトの境界に沿って正しく配置されていることを確認します。 これを行う最も簡単な方法は、コード例に示すように、XMFLOAT4XMFLOAT4X4 などのDirectXMath 型を使用することです。 また、静的アサートを宣言することで、配置が間違ったバッファーから保護することもできます。

// Assert that the constant buffer remains 16-byte aligned.
static_assert((sizeof(ConstantBufferStruct) % 16) == 0, "Constant Buffer size must be 16-byte aligned");

ConstantBufferStruct が 16 バイトに配置されていない場合、このコード行ではコンパイル時にエラーが発生します。 定数バッファーの配置とパッキングの詳細については、「定数変数のパッキング規則」を参照してください。

次に、定数バッファーを頂点シェーダー HLSL で宣言する方法を示します。

cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix mWorld;      // world matrix for object
    matrix View;        // view matrix
    matrix Projection;  // projection matrix
};

すべてのバッファー (定数、テクスチャ、サンプラーなど) には、GPU がそれらにアクセスできるようにレジスタが定義されている必要があります。 各シェーダー ステージでは最大 15 個の定数バッファーを使用でき、各バッファーには最大 4,096 個の定数変数を保持できます。 レジスタ使用法宣言の構文は次のとおりです。

  • b*#*: 定数バッファー (cbuffer) のレジスタ。
  • t*#*: テクスチャ バッファー (tbuffer) のレジスタ。
  • s*#*: サンプラーのレジスタ。 (サンプラーは、テクスチャ リソース内のテクセルの参照動作を定義します)。

たとえば、ピクセル シェーダーの HLSL は、次のような宣言を含む入力としてテクスチャとサンプラーを受け取る場合があります。

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

レジスタに定数バッファーを割り当てるのはユーザーの役割です。パイプラインを設定するときは、HLSL ファイルで割り当てたのと同じスロットに定数バッファーをアタッチします。 たとえば、前のトピックでは、VSSetConstantBuffers の呼び出しは最初のパラメーターの '0' を示します。 これは、定数バッファー リソースをレジスタ 0 にアタッチするように Direct3D に指示します。これは、HLSL ファイル内の register(b0) へのバッファーの割り当てと一致します。

頂点バッファーからの読み取り

頂点バッファーは、シーン オブジェクトの三角形データを頂点シェーダーに提供します。 定数バッファーと同様に、頂点バッファー構造体は、同様のパッキング規則を使用して C++ コードで宣言されます。

typedef struct _vertexPositionColor
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 color;
} VertexPositionColor;

Direct3D 11 では、頂点データの標準形式はありません。 代わりに、記述子を使用して独自の頂点データ レイアウトを定義します。データ フィールドは、D3D11_INPUT_ELEMENT_DESC 構造体の配列を使用して定義されます。 ここでは、前の構造体と同じ頂点形式を記述する単純な入力レイアウトを示します。

D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayout
    );

サンプル コードを変更するときに頂点形式にデータを追加する場合は、入力レイアウトも必ず更新してください。そうしないと、シェーダーで解釈できなくなります。 頂点レイアウトは次のように変更できます。

typedef struct _vertexPositionColorTangent
{
    DirectX::XMFLOAT3 pos;
    DirectX::XMFLOAT3 normal;
    DirectX::XMFLOAT3 tangent;
} VertexPositionColorTangent;

その場合は、次のように入力レイアウト定義を変更します。

D3D11_INPUT_ELEMENT_DESC iaDescExtended[] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayoutExtended
    );

入力レイアウト要素の各定義には、"POSITION" や "NORMAL" などの文字列がプレフィックスとして付けられます。これは、このトピックで前に説明したセマンティックです。 これは、頂点を処理するときに GPU がその要素を識別するのに役立つハンドルのようなものです。 頂点要素の一般的でわかりやすい名前を選択します。

定数バッファーと同様に、頂点シェーダーには、受信頂点要素に対応するバッファー定義があります。 (そのため、入力レイアウトを作成するときに頂点シェーダー リソースへの参照を提供しました。Direct3D では、シェーダーの入力構造体を使用して頂点ごとのデータ レイアウトが検証されます)。入力レイアウト定義とこの HLSL バッファー宣言の間でセマンティクスがどのように一致するかに注意してください。 ただし、COLOR には "0" が追加されています。 レイアウト内で COLOR 要素が 1 つだけ宣言されている場合は、0 を追加する必要はありませんが、将来さらに色要素を追加する場合に備えて、0 を追加することをおすすめします。

struct VS_INPUT
{
    float3 vPos   : POSITION;
    float3 vColor : COLOR0;
};

シェーダー間でデータを渡す

シェーダーは入力型を受け取り、実行時にメイン関数から出力型を返します。 前のセクションで定義した頂点シェーダーの場合、入力の種類は VS_INPUT 構造体であり、一致する入力レイアウトと C++ 構造体を定義しました。 この構造体の配列は、CreateCube メソッドで頂点バッファーを作成するために使用されます。

頂点シェーダーは PS_INPUT 構造体を返します。この構造体には、4 成分 (float4) の最終的な頂点位置を最小限に抑える必要があります。 この位置の値には、GPU が次の描画手順を実行するために必要なデータを保持できるように、システム値のセマンティック SV_POSITION が宣言されている必要があります。 頂点シェーダー出力とピクセル シェーダー入力の間に 1 対 1 の対応は存在しないことに注意してください。頂点シェーダーは、指定された頂点ごとに 1 つの構造体を返しますが、ピクセル シェーダーはピクセルごとに 1 回実行されます。 これは、頂点ごとのデータが最初にラスター化ステージを通過するためです。 このステージでは、描画するジオメトリを "カバー" するピクセルを決定し、各ピクセルの頂点ごとの補間データを計算し、それらのピクセルごとにピクセル シェーダーを 1 回呼び出します。 補間は、出力値をラスター化するときの既定の動作であり、出力ベクター データ (光ベクトル、頂点ごとの法線と接線など) の正しい処理に特に不可欠です。

struct PS_INPUT
{
    float4 Position : SV_POSITION;  // interpolated vertex position (system value)
    float4 Color    : COLOR0;       // interpolated diffuse color
};

頂点シェーダーを確認する

頂点シェーダーの例は非常に単純です。頂点 (位置と色) を取り込み、モデル座標から遠近投影座標に位置を変換し、それを (色と共に) ラスタライザーに返します。 頂点シェーダーがカラー値に対して計算を実行しなかった場合でも、カラー値が位置データと共に補間され、ピクセルごとに異なる値が提供されていることに注意してください。

VS_OUTPUT main(VS_INPUT input) // main is the default function name
{
    VS_OUTPUT Output;

    float4 pos = float4(input.vPos, 1.0f);

    // Transform the position from object space to homogeneous projection space
    pos = mul(pos, mWorld);
    pos = mul(pos, View);
    pos = mul(pos, Projection);
    Output.Position = pos;

    // Just pass through the color data
    Output.Color = float4(input.vColor, 1.0f);

    return Output;
}

フォン シェーディング用にオブジェクトの頂点を設定する頂点シェーダーなど、より複雑な頂点シェーダーは、次のようになります。 この場合、ベクトルと法線が補間され、滑らかに見えるサーフェスを近似するという事実を利用しています。

// A constant buffer that stores the three basic column-major matrices for composing geometry.
cbuffer ModelViewProjectionConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
};

cbuffer LightConstantBuffer : register(b1)
{
    float4 lightPos;
};

struct VertexShaderInput
{
    float3 pos : POSITION;
    float3 normal : NORMAL;
};

// Per-pixel color data passed through the pixel shader.

struct PixelShaderInput
{
    float4 position : SV_POSITION; 
    float3 outVec : POSITION0;
    float3 outNormal : NORMAL0;
    float3 outLightVec : POSITION1;
};

PixelShaderInput main(VertexShaderInput input)
{
    // Inefficient -- doing this only for instruction. Normally, you would
 // premultiply them on the CPU and place them in the cbuffer.
    matrix mvMatrix = mul(model, view);
    matrix mvpMatrix = mul(mvMatrix, projection);

    PixelShaderInput output;

    float4 pos = float4(input.pos, 1.0f);
    float4 normal = float4(input.normal, 1.0f);
    float4 light = float4(lightPos.xyz, 1.0f);

    // 
    float4 eye = float4(0.0f, 0.0f, -2.0f, 1.0f);

    // Transform the vertex position into projected space.
    output.gl_Position = mul(pos, mvpMatrix);
    output.outNormal = mul(normal, mvMatrix).xyz;
    output.outVec = -(eye - mul(pos, mvMatrix)).xyz;
    output.outLightVec = mul(light, mvMatrix).xyz;

    return output;
}

ピクセル シェーダーを確認する

この例のこのピクセル シェーダーは、おそらく、ピクセル シェーダーで使用できるコードの絶対最小量です。 ラスタライズ中に生成された補間ピクセル カラー データを受け取り、出力として返します。ここで、レンダリング ターゲットに書き込まれます。 なんて退屈なのでしょう。

PS_OUTPUT main(PS_INPUT In)
{
    PS_OUTPUT Output;

    Output.RGBColor = In.Color;

    return Output;
}

重要な部分は、戻り値の SV_TARGET システム値セマンティックです。 これは、出力がプライマリ レンダー ターゲットに書き込まれることを示します。これは、ディスプレイ用にスワップ チェーンに提供されるテクスチャ バッファーです。 これはピクセル シェーダーに必要です。ピクセル シェーダーのカラー データがなければ、Direct3D には何も表示されません。

フォン シェーディングを実行するより複雑なピクセル シェーダーの例は、次のようになります。 ベクトルと法線は補間されているため、ピクセル単位で計算する必要はありません。 ただし、補間のしくみにより、それらを再正規化する必要があります。概念的には、ベクトルを頂点 A の方向から頂点 B の方向に徐々に "スピン" し、その長さを維持しながら、代わりに 2 つのベクター エンドポイント間の直線を横切ってカットする必要があります。

cbuffer MaterialConstantBuffer : register(b2)
{
    float4 lightColor;
    float4 Ka;
    float4 Kd;
    float4 Ks;
    float4 shininess;
};

struct PixelShaderInput
{
    float4 position : SV_POSITION;
    float3 outVec : POSITION0;
    float3 normal : NORMAL0;
    float3 light : POSITION1;
};

float4 main(PixelShaderInput input) : SV_TARGET
{
    float3 L = normalize(input.light);
    float3 V = normalize(input.outVec);
    float3 R = normalize(reflect(L, input.normal));

    float4 diffuse = Ka + (lightColor * Kd * max(dot(input.normal, L), 0.0f));
    diffuse = saturate(diffuse);

    float4 specular = Ks * pow(max(dot(R, V), 0.0f), shininess.x - 50.0f);
    specular = saturate(specular);

    float4 finalColor = diffuse + specular;

    return finalColor;
}

別の例では、ピクセル シェーダーは、ライトとマテリアルの情報を含む独自の定数バッファーを受け取ります。 頂点シェーダーの入力レイアウトは標準データを含むように拡張され、その頂点シェーダーからの出力には、頂点、ライト、頂点法線の変換されたベクトルがビュー座標系に含まれると予想されます。

割り当てられたレジスタ (ts) を持つテクスチャ バッファーとサンプラーがある場合は、ピクセル シェーダーでもアクセスできます。

Texture2D simpleTexture : register(t0);
SamplerState simpleSampler : register(s0);

struct PixelShaderInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
    float3 lightDirection = normalize(float3(1, -1, 0));
    float4 texelColor = simpleTexture.Sample(simpleSampler, input.tex);
    float lightMagnitude = 0.8f * saturate(dot(input.norm, -lightDirection)) + 0.2f;
    return texelColor * lightMagnitude;
}

シェーダーは、シャドウ マップやノイズ テクスチャなどの手続き型リソースを生成するために使用できる非常に強力なツールです。 実際、高度な手法では、テクスチャを視覚的要素としてではなくバッファーとしてより抽象的に考える必要があります。 高さ情報などのデータや、最終的なピクセル シェーダー パスまたはその特定のフレームでマルチステージ エフェクト パスの一部としてサンプリングできるその他のデータが保持されます。 マルチサンプリングは強力なツールであり、多くの最新の視覚効果のバックボーンです。

次のステップ

この時点で DirectX 11 に慣れており、プロジェクトの作業を開始する準備ができていることを願います。 DirectX と C++ を使用した開発に関するその他の質問への回答に役立つリンクを次に示します。

DirectX デバイス リソースの操作

Direct3D 11 レンダリング パイプラインについて