DirectX の構成要素
頂点シェーダーと変換
1975 年、芸術家の Richard Haas は、マンハッタンのソーホー地区にあるビルの壁面に、古典的キャストアイロン (鋳鉄) 建築のファサードに似せて絵を描きました。窓には猫も描かれています。今はこのような絵画にお目にかかることは少なくなりましたが、当時は非常にリアルに受け止められ、多くの人の目を欺きました。
このような絵画はトロンプ・ルイユ (「騙し絵」) と呼ばれ、陰影を使って 2 次元平面を 3 次元に見せかけます。人間の目はこのような錯覚にだまされやすいものですが、同時に、仕掛けを見破ることで自らを満足させたいとも考えます。簡単に見破る方法の 1 つは、別の角度から絵を眺めて、同じに見えるかどうか確認することです。
2D と 3D の境界にまたがるようなグラフィックがコンピューター プログラムによって表示された場合にも同じような心理効果が働きます。本当に 3D なのか、巧妙に重ね合わせて陰影を付けた単なる 2D なのか、その真相は頭を左右に動かしてもわかりません。しかし、プログラムに命じてグラフィックを回転させると、何が行われているかを確認できる可能性があります。
図 1 の画像は、前回のコラム (msdn.microsoft.com/magazine/dn768854) で取り上げた ThreeTriangles プログラムで表示した画面にとてもよく似ています。しかし、今回の ThreeRotatingTriangles というコード サンプルでは、組み合わせた 3 つの三角形を回転させます。これは、視覚的に非常に興味深い効果です。3 つの三角形が相関して動くと、実際にプログラムは 3D グラフィック処理を進行しているのだと感じます。しかし、コードをみると、記述しているのは Direct3D ではなく完全な Direct2D であり、Direct2D 効果の強力な機能が使用されているのが分かります。
図 1 ThreeRotatingTriangles プログラムの表示
データに効果を施す
前回の ThreeTriangles プログラムの全体構造を、ThreeRotatingTriangles でも保持しています。Direct2D 効果の実装を提供するクラスは、SimpleTriangleEffect ではなく RotatingTriangleEffect という名前に変わっていますが、引き続き ID2D1EffectImpl (「効果の実装」) と ID2D1DrawTransform インターフェイスを実装しています。
前回の SimpleTriangleEffect は汎用性が高いとはいえませんでした。3 つの重なり合う三角形を表示する頂点をハードコードしていたためです。RotatingTriangleEffect では、頂点をクラス外部から定義できるようにし、効果の実装と頂点シェーダーの強化により行列変換を可能にしています。
一般に、RotatingTriangleEffect のような効果の実装には、RegisterEffectFromString を呼び出して自身をクラス ID に関連付けることにより、自身を登録する静的メソッドが含まれています。ThreeRotatingTriangles プログラムでは、ThreeRotatingTrianglesRenderer クラスによってコンストラクター内のこの静的メソッドを呼び出して、効果を登録します。
また、ThreeRotatingTrianglesRenderer は、ID2D1Effect 型のオブジェクトをプライベート フィールドとして定義します。
Microsoft::WRL::ComPtr<ID2D1Effect>
m_rotatingTriangleEffect;
効果を使用するために、プログラムでは CreateEffect への呼び出し内で効果のクラス ID を参照して,このオブジェクトを作成する必要があります。ThreeRotatingTrianglesRenderer クラスでは、CreateDeviceDependentResources メソッド内でこれを行っています。
d2dContext->CreateEffect(
CLSID_RotatingTriangleEffect, &m_rotatingTriangleEffect);
その後、効果は DrawImage メソッドの呼び出しによってレンダリングされます。Render メソッド内では次のように ThreeRotatingTrianglesRenderer を呼び出します。
d2dContext->DrawImage(m_rotatingTriangleEffect.Get());
しかし、この 2 つの呼び出しの間にできることはこれだけではありません。ID2D1Effect は ID2D1Properties から派生しています。ID2D1Properties には SetValue と GetValue という、プログラムで効果にプロパティを設定できるメソッドがあります。設定できるプロパティは、ブール型フラグとして公開される単純な効果オプションから、大きなデータ バッファーまでさまざまです。ただし、SetValue と GetValue はあまり使用しません。これらのメソッドでは特定のプロパティをインデックスで特定する必要があること、および SetValueByName メソッドと GetValueByName メソッドを使用する方がプログラムがわかりやすくなることが理由です。
この SetValueByName メソッドと GetValueByName メソッドは ID2D1Effect オブジェクトの一部で、ID2D1Effect オブジェクトは CreateEffect 呼び出しから返されるオブジェクトです。ID2D1Effect はこれらのプロパティの値を、開発者が作成した効果の実装クラス (ID2D1EffectImpl インターフェイスと ID2D1DrawTransform インターフェイスを実装したクラス) に渡します。回りくどいようですが、このような方法を取ることで、効果を実装したクラスにアクセスすることなく登録された効果を使用できるようになります。
しかし、このことは、RotatingTriangleEffect クラスなどの効果の実装は、さまざまな型のプロパティを受け取ることができることを示す必要があり、また、それらのプロパティを設定および取得するメソッドを提供する必要があることを意味します。
プロパティの情報は、効果の実装が RegisterEffectFromString を使用して自身を登録する際に提供します。この呼び出しで必要なのは、効果の実装がサポートするさまざまなプロパティの名前と型を含む XML です。RotatingTriangleEffect は、次に示す名前とデータ型を持つ 4 つのプロパティをサポートします。
- VertexData、blob 型 (バイト ポインターによって参照されるメモリ バッファー)
- ModelMatrix、matrix4x4 型
- ViewMatrix、matrix4x4 型
- ProjectionMatrix、matrix4x4 型
これらのデータ型の名前は、Direct2D 効果固有のものです。プロパティをサポートする効果を登録する際に、効果の実装は D2D1_VALUE_TYPE_BINDING オブジェクトの配列も提供する必要があります。この配列の各オブジェクトが、名前付きプロパティ (VertexData など) と効果の実装内でデータ設定と取得を行う 2 つのメソッドを関連付けます。VertexData の場合、2 つのメソッドは SetVertexData と GetVertexData という名前です (このような Get メソッドを定義する場合には、const キーワードを含めないと、奇妙なテンプレート エラーが発生して非常に悩むことになります)。
同じようにして、RotatingTriangleEffect クラスでは SetModelMatrix と GetModelMatrix という名前のメソッドを定義します。他のクラスでも同じように定義します。これらのメソッドは、アプリケーション プログラムから呼び出されることはありません。実際、これらのメソッドは RotatingTriangleEffect に対してプライベートです。代わりに、プログラムでは ID2D1Effect オブジェクトの SetValueByName メソッドと GetValueByName メソッドを呼び出します。その後、ID2D1Effect オブジェクトから効果の実装の Set メソッドや Get メソッドが呼び出されます。
頂点バッファー
ThreeRotatingTrianglesRenderer クラスではコンストラクターが RotatingTriangleEffect を登録し、Render メソッドが効果をレンダリングします。しかし、この 2 つの呼び出しの間に、レンダラー クラスから ID2D1Effect オブジェクトの SetValueByName が呼び出され、効果の実装にデータが渡されます。
前述の 4 つの効果プロパティの 1 つ目は VertexData です。このプロパティは、頂点バッファーの定義に使用する頂点のコレクションです。Direct2D 効果の場合、頂点バッファーに含まれる項目数は 3 の倍数で、三角形にグループ化されている必要があります。ThreeRotatingTriangles プログラムは 3 つの頂点が設定された 3 つの三角形しか表示しませんが、この効果はこれより大きいバッファーを処理することができます。
RotatingTriangleEffect が想定する頂点バッファーの形式は、次のように RotatingTriangleEffect.h の構造体で定義されています。
struct PositionColorVertex
{
DirectX::XMFLOAT3 position;
DirectX::XMFLOAT3 color;
};
これは、SimpleTriangleEffect が使用する形式と同じですが、定義の方法が少し異なります。図 2 は、効果を作成後に、ThreeRotatingTrianglesRenderer の CreateDeviceDependentResources メソッドが頂点配列を効果に転送する方法を示しています。
図 2 効果を作成して頂点バッファーを設定する
void ThreeRotatingTrianglesRenderer::CreateDeviceDependentResources()
{
ID2D1DeviceContext1* d2dContext =
m_deviceResources->GetD2DDeviceContext();
// Create the effect
DX::ThrowIfFailed(d2dContext->CreateEffect(
CLSID_RotatingTriangleEffect,
&m_rotatingTriangleEffect)
);
// Set the vertices
std::vector<PositionColorVertex> vertices =
{
// Triangle 1
{ XMFLOAT3(0, -1000, -1000), XMFLOAT3(1, 0, 0) },
{ XMFLOAT3(985, -174, 0), XMFLOAT3(0, 1, 0) },
{ XMFLOAT3(342, 940, 1000), XMFLOAT3(0, 0, 1) },
// Triangle 2
{ XMFLOAT3(866, 500, -1000), XMFLOAT3(1, 0, 0) },
{ XMFLOAT3(-342, 940, 0), XMFLOAT3(0, 1, 0) },
{ XMFLOAT3(-985, -174, 1000), XMFLOAT3(0, 0, 1) },
// Triangle 3
{ XMFLOAT3(-866, 500, -1000), XMFLOAT3(1, 0, 0) },
{ XMFLOAT3(-643, -766, 0), XMFLOAT3(0, 1, 0) },
{ XMFLOAT3(643, -766, 1000), XMFLOAT3(0, 0, 1) }
};
DX::ThrowIfFailed(
m_rotatingTriangleEffect->SetValueByName(L"VertexData",
(byte *) &vertices)
);
// Ready to render!
m_readyToRender = true;
}
X 座標と Y 座標は半径が 1,000 で 40 度ずつ増加する角の正弦と余弦に基づいて設定しています。Z 座標は、前景を –1,000 として背景を 1,000 とする範囲です。前回の SimpleTriangleEffect は、3D 出力のクリッピングに使用した名前付け規則のために Z を 0 ~ 1 の間に設定するよう非常に気を使いました。今回は、実際のカメラの変換が頂点に適用されるため、気を使う必要はありません。
VertexData の名前を指定して SetValueByName を呼び出すことにより、ID2D1Effect オブジェクトは RotatingTriangleEffect の SetVertexBuffer メソッドを呼び出し、データを渡します。このメソッドはバイト ポインターを元の型にキャストし、CreateVertexBuffer を呼び出して、頂点シェーダーに渡せるような方法で情報を保存します。
変換の適用
図 3 に、3 つの行列変換を計算して SetValueByName を 3 回呼び出す、ThreeRotatingTrianglesRenderer の Update メソッドを示します。コードは、正常な範囲から逸脱した HRESULT 戻り値のチェックを省略して少し簡略化しました。
図 3 3 つの変換行列を設定する
void ThreeRotatingTrianglesRenderer::Update(DX::StepTimer const& timer)
{
if (!m_readyToRender)
return;
// Apply model matrix to rotate vertices
float angle = float(XM_PIDIV4 * timer.GetTotalSeconds());
XMMATRIX matrix = XMMatrixRotationY(angle);
XMFLOAT4X4 float4x4;
XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
m_rotatingTriangleEffect->SetValueByName(L"ModelMatrix", float4x4);
// Apply view matrix
matrix = XMMatrixLookAtRH(XMVectorSet(0, 0, -2000, 0),
XMVectorSet(0, 0, 0, 0),
XMVectorSet(0, 1, 0, 0));
XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
m_rotatingTriangleEffect->SetValueByName(L"ViewMatrix", float4x4);
// Base view width and height on coordinates of model
float width = 2000;
float height = 2000;
// Adjust width and height for landscape and portrait modes
Windows::Foundation::Size logicalSize =
m_deviceResources->GetLogicalSize();
if (logicalSize.Width > logicalSize.Height)
width *= logicalSize.Width / logicalSize.Height;
else
height *= logicalSize.Height / logicalSize.Width;
// Apply projection matrix
matrix = XMMatrixOrthographicRH(width, height, 500, 4000);
XMStoreFloat4x4(&float4x4, XMMatrixTranspose(matrix));
m_rotatingTriangleEffect->SetValueByName(L"ProjectionMatrix", float4x4);
}
通常どおり、Update はビデオ ディスプレイのフレーム レートで呼び出されます。最初に計算する行列は、Y 軸に沿って回転させるように頂点に適用します。2 つ目の行列は標準的なカメラ ビューの変換です。ビューアーが 3 次元座標系の原点に位置して Z 軸を正面にとらえるようにシーンが移動します。3 つ目は標準的な射影行列です。X 座標と Y 座標を –1 ~ 1 の値の範囲に正規化し、Z 座標を 0 ~ 1 の範囲に正規化します。
これらの行列は、頂点座標に適用する必要があります。それでは、CreateDeviceDependentResources メソッドに定義した頂点の配列で乗算して RotatingTriangleEffect で新しい頂点バッファーを設定しないのはなぜでしょう。
確かに、ビデオ ディスプレイのフレーム レートで変化する動的頂点バッファーを定義することは可能で、場合によってはその必要があります。しかし、頂点を行列変換で修正するだけの場合、動的頂点バッファーは、プログラム全体で同じバッファーを維持して後からパイプラインで変換を適用するほど効率的ではありません。ビデオ GPU で実行されている頂点シェーダーの場合はなおさらです。
つまり、頂点シェーダーでは、ビデオ ディスプレイのすべてのフレームに新しい行列変換が必要です。また、効果の実装がどのように頂点シェーダーにデータを格納するかという、別の問題も発生します。
シェーダー定数バッファー
データは、定数バッファーと呼ばれるメカニズムを通じてアプリケーション コードからシェーダーに転送します。定数バッファーという名前から、プログラム全体を通じてコンテンツが定数のままであるとは思い込まないでください。まったくの間違いです。ほとんどの場合、定数バッファーはビデオ ディスプレイのすべてのフレームで変化します。ただし、定数バッファーのコンテンツは、各フレームのすべての頂点に対しては一定です。また、定数バッファーの形式は、コンパイル時にプログラムによって固定されます。
頂点シェーダー定数バッファーの形式は、2 か所 (C++ コード内と頂点シェーダー内) で定義します。効果の実装内では、次のように定義します。
struct VertexShaderConstantBuffer
{
DirectX::XMFLOAT4X4 modelMatrix;
DirectX::XMFLOAT4X4 viewMatrix;
DirectX::XMFLOAT4X4 projectionMatrix;
} m_vertexShaderConstantBuffer;
レンダラーの Update メソッドが SetValueByName を呼び出して行列の 1 つを設定すると、ID2D1Effect オブジェクトが RotatingTriangleEffect クラスの適切な Set メソッドを呼び出します。呼び出すメソッドは、SetModelMatrix、SetViewMatrix、および SetProjectionMatrix という名前で、m_vertexShaderConstantBuffer オブジェクトの適切なフィールドに行列を転送します。
ID2D1Effect オブジェクトは、SetValueByName に対する呼び出しのすべてで、グラフィック出力に対して対応する変化が含まれると考えられる効果に変更が行われると想定するため、効果の実装の PrepareForRender メソッドを呼び出します。ここで、効果の実装は SetVertexShaderConstantBuffer を呼び出して VertexShaderConstantBuffer のコンテンツを頂点シェーダーに転送することができます。
新しい頂点シェーダー
最後に、上位レベル シェーダー言語 (HLSL: High Level Shader Language) コードを見てみましょう。このコードは、3D 空間での 3 つの三角形の頂点の回転と向きの調整の処理の大部分を担います。これは強化された新しい頂点シェーダーです。図 4 にその全体を示します。
図 4 三角形を回転させる効果の頂点シェーダー
// Per-vertex data input to the vertex shader
struct VertexShaderInput
{
float3 position : MESH_POSITION;
float3 color : COLOR0;
};
// Per-vertex data output from the vertex shader
struct VertexShaderOutput
{
float4 clipSpaceOutput : SV_POSITION;
float4 sceneSpaceOutput : SCENE_POSITION;
float3 color : COLOR0;
};
// Constant buffer provided by effect.
cbuffer VertexShaderConstantBuffer : register(b1)
{
float4x4 modelMatrix;
float4x4 viewMatrix;
float4x4 projectionMatrix;
};
// Called for each vertex.
VertexShaderOutput main(VertexShaderInput input)
{
// Output structure
VertexShaderOutput output;
// Get the input vertex, and include a W coordinates
float4 pos = float4(input.position.xyz, 1.0f);
// Pass through the resultant scene space output value
output.sceneSpaceOutput = pos;
// Apply transforms to that vertex
pos = mul(pos, modelMatrix);
pos = mul(pos, viewMatrix);
pos = mul(pos, projectionMatrix);
// The result is clip space output
output.clipSpaceOutput = pos;
// Transfer the color
output.color = input.color;
return output;
}
構造体の定義方法に注目してください。シェーダーの VertexShaderInput は C++ ヘッダー ファイルに定義されている PositionColorVertex 構造体と同じ形式です。VertexShaderConstantBuffer は C++ コードの同名の構造体と同じ形式です。VertexShaderOutput 構造体はピクセル シェーダーの PixelShaderInput 構造体に一致します。
前回の SimpleTriangleEffect に関連付けられている頂点シェーダー内の ClipSpaceTransforms バッファーは、自動的に提供されてシーン空間 (三角形の頂点に使用されるピクセル座標) からクリップ空間への変換を行います。クリップ空間には –1 ~ 1 の範囲の正規化された X 座標と Y 座標および 0 ~ 1 の範囲の Z 座標が含まれます。
これはもう必要ないので削除しました。削除したことで不都合は起こりませんでした。代わりに、射影行列が同等の機能を果たします。メイン関数では 3 つの行列に入力頂点位置を適用し、その結果を出力構造体の clipSpaceOuput フィールドに設定しています。
この clipSpaceOutput フィールドは必須です。このように、深度バッファーは管理され、結果が表示画面にマッピングされます。ただし、VertexShaderOutput 構造体の sceneSpaceOutput フィールドは必須ではありません。このフィールドを削除しても (さらにピクセル シェーダーの PixelShaderInput 構造体からフィールドを削除しても)、プログラムは同じように実行されます。
行優先と列優先
シェーダーでは、行列を使用して 3 つの位置の乗算を実行します。
pos = mul(pos, modelMatrix);
pos = mul(pos, viewMatrix);
pos = mul(pos, projectionMatrix);
数学的に表記すると、乗算は次のようになります。
m11 m12 m13 m14
m21 m22 m23 m24
m31 m32 m33 m34
m41 m42 m43 m44
|x y z w| ×
この乗算を実行すると、点 (x, y, z, w) を表す 4 つの数字に、行列の最初の列の 4 つの数字 (m11, m21, m31, m41) が乗算され、その 4 つの積が合計されます。その後、この処理が 2 つ目、3 つ目、および 4 つ目の列を使用して行われます。
ベクトル (x, y, z, w) は、隣接するメモリに格納された 4 つの数字から構成されます。行列乗算の並列処理を実装しているハードウェアを考えてください。各列の数字も隣接するメモリに格納されている場合、この 4 つの乗算を並列で実行すると速度が速くなると思いますか。速くなる可能性は高いでしょう。これは、m11、m21、m31、m41、m12、m22 ... という順序が、行列の値をメモリに格納する最適な方法であることを意味しています。
これを「列優先順」と呼びます。メモリ ブロックは行列の最初の列から始まり、2 列目、3 列目、4 列目と続きます。この順序が、頂点シェーダーで行列の乗算を実行する際に想定されている行列の構造です。
しかし、これは DirectX でメモリに行列を保存する通常の方法ではありません。DirectX Math ライブラリの XMMATRIX 構造体と XMFLOAT4X4 構造体では、行列を行優先 (m11、m12、m13、m14、m21、m22 ...) 順で格納します。行優先順は、横に読み進めた後に次の行に移るという、文章を読む順序と同じになるため、多くの人にとって自然な順序に感じられます。
いずれにしても、DirectX とシェーダー コードには互換性がありません。図 3 のコードを見ると、すべての行列が XMMatrixTranspose 呼び出しを適用されてから頂点シェーダーに送信されているのが分かります。XMMatrixTranspose 関数では行優先の行列を列優先の行列に変換し、必要に応じて再変換します。
これが、この問題に対する最も一般的なソリューションですが、ソリューションはこれだけではありません。シェーダーを行優先順にコンパイルするフラグを指定する方法や、行列を入れ替えずに乗算の際に行列とベクトルの順序を切り替える方法もあります。
pos = mul(viewMatrix, pos);
最後の手順
3 つの三角形のシェーディングによって、各三角形の表面に頂点の色が補間されることは確かに示されますが、結果はかなりおおざっぱです。もちろん、この強力なシェーディング ツールを使用し、光の反射を模倣して結果を洗練することができます。それが、さまざまな用途に使用できる Direct2D の世界に飛び込むための最後の手順となります。
Charles Petzoldは MSDN マガジンの記事を長期にわたって担当しており、Windows 8 向けのアプリケーション開発についての書籍『Programming Windows, 6th Edition』(Microsoft Press、2013 年) の著者でもあります。彼の Web サイトは charlespetzold.com (英語) です。
この記事のレビューに協力してくれたマイクロソフト技術スタッフの Doug Erickson に心より感謝いたします。