次の方法で共有


DirectX の構成要素

Direct2D 効果を使用して Z の限界を突破する

Charles Petzold

コード サンプルのダウンロード

Charles Petzold人は子供時代に、読み書きを覚えるよりも先に絵の描き方を覚えます。また、その経験からいくつかの教訓を学んでいることも間違いありません。たとえば、描く順序によってキャンバス上の絵の重なり方が変わることに気付きます。先に描いた絵は、後に描いた絵に部分的に隠れたり、見えにくくなったりします。

このような理由から、コンピューター グラフィックスのメカニズムをまったく知らない人でも、図 1 の画像がレンダリングされた順序を推測できるでしょう。当然ながら、最初に背景が灰色に着色されました。その後、青色、緑色の順に三角形が描画され、最後に、最も手前にある赤色の三角形が描画されました。背面から前面へ図形をレンダリングするプロセスが "画家のアルゴリズム" と呼ばれるのも納得できますね。

Three Overlapping Triangles
図 1 重なり合う 3 つの三角形

図 1 の 3 つの三角形は、積み重ねた色画用紙と考えることもできます。三角形をさらに重ねていくと、山のように積み重なり、最初は 2 次元平面だったものが 3 次元になります。

2D グラフィックスにも、Z 軸 (2 次元の画面またはキャンバスに直交する仮想空間) の初歩的な概念があります。平面 2D オブジェクトの重なり方は、図形の "Z オーダー" によって決まります。たとえば、XAML ベースの環境では、Canvas.ZIndex 添付プロパティによって、他の要素の上にあるように見える要素が決まります。しかし、実際には、要素が画面にレンダリングされる順序を制御しているにすぎません。

問題は、2D グラフィックスでは Z インデックスが必ず図形全体に適用されることです。このような Z オーダーを使用する場合、図 2 のような 3 つの図形は描画できません。この図では、1 つ目の図形が 2 つ目の図形の上に、2 つ目の図形が 3 つ目の図形の上に、3 つ目の図形が 1 つ目の図形の上に重なっています。

Mutually Overlapping Triangles
図 2 相互に重なり合う三角形

1 つの三角形の 1 つの角で生じた変化はわずかなように思えますが、この変化によって状況が大きく変わっています。図 2 の画像を画用紙で再現するのは簡単だとしても、絵の具や 2D グラフィックス プログラミングによる絵で再現するのは簡単ではありません。3 つのうち 1 つの三角形を、注意深く計算した座標で 2 つの部分に分けてレンダリングするか、残りの三角形のうち一方に基づくクリッピングを使用してレンダリングする必要があります。

効果と GPU

図 2 のレンダリングは、3D グラフィックスの世界の概念を借りると大幅に簡単になります。

このような図形には、統一した Z インデックスを設定できません。そのため、図形の面全体に可変 Z 座標を設定できるようにする必要があります。このようにすると、描画プロセスで、レンダリング サーフェスのすべてのピクセルを保持する Z 座標 (Z バッファーまたは深度バッファーと呼ばれる) を保持できます。各図形のレンダリング時に、図形の各ピクセルの Z 座標がこの深度バッファー内の対応する Z 座標と比較されます。ピクセルが深度バッファーの Z 座標より上にある場合、そのピクセルは描画され、新しい Z 座標が深度バッファーに格納されます。Z 座標より下にある場合、そのピクセルは無視されます。

この方法は、比較そのものについてだけでなく、各画像のすべてのピクセルのZ 座標の計算についても、処理の負荷が高いように思えます。実際、これは的確な評価です。そのため、このような処理は最新の GPU の並列計算機能に任せるのが理想的です。

図形の各ピクセルの Z 座標の計算は、図形が三角形の場合、理論上は非常に簡単です。また、すべての多角形は三角形に分解できます。必要な処理は、三角形の 3 つの頂点にそれぞれ 3D 座標点を設定することだけです。これで、三角形内部の任意の点を 3 つの頂点の座標の加重平均として計算できるようになります (加重平均には、ドイツ人の数学者 August Ferdinand Möbius が開発しコンピューター グラフィックスで使用される概念の 1 つ、重心座標も含まれます)。

同じ補間プロセスを利用して、三角形をシェーディングすることもできます。各頂点に特定の色を割り当てている場合、その三角形内部のピクセルは頂点に割り当てている 3 色の加重平均になります (図 3 参照)。

The ThreeTriangles Program Display
図 3 ThreeTriangles プログラムの表示

このような色のグラデーションは、三角形をシェーディングして曲面のように見せかける効果があることから、3D プログラミングの重要な機能でもあります。しかし、通常の 2D プログラミングでよく使用するグラデーションではありません。

図 3 の画像は、コード サンプルとしてダウンロードできる ThreeTriangles というプログラムで作成しました。このプログラムは、Windows 8.1 および Windows Phone 8.1 で実行できます (このソリューションは、Visual Studio 2013 Update 2 で、新しいユニバーサル アプリ テンプレートを使用して作成しました。ユニバーサル アプリ テンプレートを使用すると、Windows 8.1 と Windows Phone 8.1 で多くのコードを共有できます)。

ThreeTriangles プログラムのグラフィックスは、効果またはカスタム効果 (独自にコーディングする場合) と呼ばれる Direct2D の機能を使用して、Direct2D のみを利用してレンダリングされます。カスタム効果を使用すると、Direct2D だけを使用する場合よりもはるかに実際の 3D プログラミングに近い形でレンダリングできます。

Direct2D のカスタム効果を作成する場合、通常は 3D プログラマだけに許されている特権である、GPU 上で実行されるコードの記述が可能になります。このコードは、シェーダーと呼ばれる小さなプログラムの形式を取っています。シェーダーの作成には、上位レベル シェーダー言語 (HLSL: High Level Shader Language) という C に似た言語を使用します。作成したシェーダーは、通常のプロジェクト ビルド時に Visual Studio でコンパイル済みシェーダー オブジェクト (.cso) ファイルにコンパイルされ、プログラムの実行時に GPU 上で実行されます。

実のところ、Direct2D 効果は、シェーダーのラッパー以上の存在と表現されることもあります。カスタム効果は、Direct2D プログラミングの範ちゅうでシェーダーを使用して擬似 3D 画像を実現する、唯一の手段です。

Direct2D 効果で使用できるシェーダーには、次の 3 種類があります。

  • 頂点シェーダー: 頂点を操作します。各三角形には 3 つの頂点があり、頂点には必ず座標点が設定されています。また、色などの情報も頂点に設定できます。
  • ピクセル シェーダー: 三角形内部のピクセルを操作します。頂点と共に提供された情報は、ピクセル シェーダー用の準備として三角形平面に対して自動的に補間されます。
  • 計算シェーダー: GPU を使用して負荷の高い並列計算を行います。今回のコラムでは計算シェーダーについては説明しません。

Direct2D 効果に使用するシェーダーは、Direct3D プログラミングに使用するシェーダーとは若干要件が異なりますが、多くの概念は同じです。

組み込み効果とカスタム効果

Direct2D には、ぼかし、シャープ化、各種のカラー操作などのさまざまな画像処理操作をビットマップに対して実行する、約 40 種の事前定義された組み込み効果が用意されています。

各組み込み効果はクラス ID によって識別されます。このクラス ID を使用して、対応する種類の効果を作成します。たとえば、ビットマップの色を変更する変換を指定できる、カラー マトリックス効果を使用するとします。この場合、レンダリング クラスのプライベート フィールドとして ID2D1Effect 型のオブジェクトを宣言することが考えられます。

Microsoft::WRL::ComPtr<ID2D1Effect> m_colorMatrixEffect;

CreateDeviceDependentResources メソッドで、ドキュメントに記載のクラス ID を参照すると、この効果を作成できます。

d2dContext->CreateEffect(
  CLSID_D2D1ColorMatrix, &m_colorMatrixEffect);

この時点で、効果オブジェクトの SetInput を呼び出してビットマップを設定したり、SetValue を呼び出して変換行列を指定したりできます。色相をシフトしたこのビットマップをレンダリングするには、次のように呼び出します。

d2dContext->DrawImage(m_colorMatrixEffect.Get());

すべての組み込み効果には、ビットマップ入力を使用します。また、Direct2D 効果の機能の 1 つとして、効果を連結し、一連の効果をビットマップに適用することができます。

独自のカスタム効果の記述に関心がある方は、Direct2D Custom Image Effects Sample という便利な Windows 8.1 Visual Studio ソリューションをお勧めします。このソリューションには、Direct2D 効果に使用できる 3 種類のシェーダーを示す 3 つのプロジェクトが含まれています。3 つのプログラムはどれも、入力としてビットマップを必要とします。

そのため、Direct2D 効果では必ずビットマップ入力を操作すると皆さんに思われても無理はありませんが、これは本当ではありません。図 3 の画像を作成した ThreeTriangles プログラムには、ビットマップ入力が必要ありません。

また、Direct2D 効果には 1 種類のシェーダーしか含めることができないと思われるのも無理はありません。確かに、組み込み効果には頂点シェーダーかピクセル シェーダーのいずれかしか含まれず、両方は含まれていないようですが、この点も ThreeTriangles プログラムには当てはまりません。ThreeTriangles プログラムでは、頂点シェーダーとピクセル シェーダーの両方を使用するカスタム効果を定義しています。

登録、作成、描画

Direct2D 効果はクラス ID から事前に登録して作成するように設計されているので、カスタム効果にも Direct2D 効果と同じ機能を用意する必要があります。ThreeTriangles プログラムのカスタム効果は SimpleTriangleEffect という名前のクラスです。このクラスは、クラスを登録する静的メソッドを定義しています。このメソッドの呼び出し元は ThreeTrianglesRenderer クラスのコンストラクターですが、次のように、プログラム内の任意の場所で効果を登録できます。

SimpleTriangleEffect::RegisterEffectAsync(d2dFactory)

この登録メソッドは、コンパイル済みシェーダー ファイルに読み込む必要があるので非同期です。また、この目的のために DirectXHelper クラスに用意されているメソッドは、ReadDataAsync のみです。

組み込み効果を使用する場合と同様に、ThreeTrianglesRenderer クラスでは ID2D1Effect オブジェクトをヘッダー ファイルのプライベート フィールドとして宣言しています。

Microsoft::WRL::ComPtr<ID2D1Effect> m_simpleTriangleEffect;

CreateDeviceDependentResources メソッドは、組み込み効果と同じ方法でカスタム効果を作成します。

d2dContext->CreateEffect(
   CLSID_SimpleTriangleEffect, &m_simpleTriangleEffect)

先ほどカスタム効果を登録したので、このクラス ID は効果に関連付けられています。

SimpleTriangleEffect に入力はありません (これが一因となって、コードが "単純" になっています)。効果のレンダリング方法は、組み込み効果と同様です。

d2dContext->DrawImage(m_simpleTriangleEffect.Get());

このカスタム効果の使用方法が単純だった分、効果クラス自体の複雑さをいくらか感じていただけると思います。SimpleTriangleEffect などのカスタム効果には、ID2D1EffectImpl (効果実装) インターフェイスを実装する必要があります。効果は、変換と呼ばれる複数の段階で構成されています。各段階は、ID2D1DrawTransform の実装で表すのが一般的です。単一のクラスを両方のインターフェイスで使用する場合は (SimpleTriangleEffect がこの場合に該当します)、IUnknown (3 メソッド)、ID2D1EffectImpl (3 メソッド)、ID2D1TransformNode (1 メソッド)、ID2D1Transform (3 メソッド)、および ID2D1DrawTransform (1 メソッド) を実装する必要があります。

効果の初回登録時には、効果やその作成者を識別する XML に加えて、かなりのオーバーヘッドが発生します。さいわい、今回の例のように単純な効果については、効果メソッドの多くをかなり簡単に実装できます。効果クラスの最も重要な機能は、コンパイル済みシェーダー コードを読み込んで登録して (後で参照できるようにシェーダーを GUID に関連付けて)、頂点バッファーを定義することです。頂点バッファーも GUID に関連付ける必要があります。

頂点バッファーから…

頂点バッファーは、処理用にまとめられた頂点の集合です。各頂点には必ず 2D 座標点または 3D 座標点が設定されていますが、多くの場合は他の項目も設定されています。各頂点に関連付けられたデータと各頂点の配置は、頂点バッファーの "レイアウト" と呼ばれます。また、概して、ThreeTriangles プログラムではこの頂点レイアウトを記述するために 3 つの異なる (ただし同等の) データ型を定義しています。

この頂点データの 1 つ目の表現を図 4 に示します。これは、3D 座標点と RGB カラーが含まれた Vertex という名前の単純な構造体です。これらの構造体の配列によって、プログラムで表示する 3 つの三角形を定義します (この配列は、SimpleTriangleEffect クラスの Initialize 必須メソッド内でハードコードしています。実際のプログラムの場合は、効果クラスを使用して、頂点の配列を効果の入力に指定できます)。

図 4 SimpleTriangleEffect での頂点の定義

// Define Vertex for simple initialization
struct Vertex
{
  float x;
  float y;
  float z;
  float r;
  float g;
  float b;
};
// Each triangle has three points and three colors
static Vertex vertices [] =
{
  // Triangle 1
  {    0, -1000, 0.0f, 1, 0, 0 },
  {  985,  -174, 0.5f, 0, 1, 0 },
  {  342,   940, 1.0f, 0, 0, 1 },
  // Triangle 2
  {  866,   500, 0.0f, 1, 0, 0 },
  { -342,   940, 0.5f, 0, 1, 0 },
  { -985,  -174, 1.0f, 0, 0, 1 },
  // Triangle 3
  { -866,   500, 0.0f, 1, 0, 0 },
  { -643,  -766, 0.5f, 0, 1, 0 },
  {  643,  -766, 1.0f, 0, 0, 1 }
};
// Define layout for the effect
static const D2D1_INPUT_ELEMENT_DESC vertexLayout [] =
{
  { "MESH_POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0 },
  { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12 },
};

x と y の値は、半径が 1,000 で 40 度ずつ増加する角の正弦と余弦に基づいて設定しています。ただし、z 座標は必ず 0 ~ 1 になるので、赤の頂点の z 値は 1、緑の頂点の z 値は 0.5、青の頂点の z 値は 0 に設定します。詳細については、後ほど説明します。

この配列の後には、別の小さな配列があります。この配列は、頂点バッファーの作成と登録に必要な頂点情報を形式的な方法で定義しています。

頂点バッファーと頂点シェーダーは、どちらも SimpleTriangleEffect の SetDrawInfo メソッド内で参照します。効果をレンダリングするたびに、これらの 9 個の頂点を頂点シェーダーに渡します。

…頂点シェーダーを経由して…

図 5 に、SimpleTriangleEffect の頂点シェーダーを示します。これは、3 つの構造体と、main という関数で構成されています。main 関数は、頂点バッファーのすべての頂点に対して呼び出します。この場合の頂点は 9 個だけですが、さらに多い場合もよくあります。

図 5 SimpleTriangleEffectVertexShader.hlsl ファイル

// 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;
};
// Information provided for Direct2D vertex shaders
cbuffer ClipSpaceTransforms : register(b0)
{
  float2x1 sceneToOutputX;
  float2x1 sceneToOutputY;
}
// Called for each vertex
VertexShaderOutput main(VertexShaderInput input)
{
  // Output structure
  VertexShaderOutput output;
  // Append a 'w' value of 1 to the 3D input position
  output.sceneSpaceOutput = float4(input.position.xyz, 1);
  // Standard calculations
  output.clipSpaceOutput.x =
    output.sceneSpaceOutput.x * sceneToOutputX[0] +
    output.sceneSpaceOutput.w * sceneToOutputX[1];
  output.clipSpaceOutput.y =
    output.sceneSpaceOutput.y * sceneToOutputY[0] +
    output.sceneSpaceOutput.w * sceneToOutputY[1];
  output.clipSpaceOutput.z = output.sceneSpaceOutput.z;
  output.clipSpaceOutput.w = output.sceneSpaceOutput.w;
  // Transfer the color
  output.color = input.color;
  return output;
}

3 つの構造体それぞれには、HLSL データ型、メンバー名、および (各フィールドの役割を識別する) 大文字セマンティクスで識別されるフィールドが含まれています。

VertexShaderInput という名前の構造体は、main 関数への入力です。この構造体は先ほど示した頂点バッファーのレイアウトと同じ構成ですが、3D 位置と RGB カラーの HLSL データ型を格納します。

VertexShaderOutput という名前の構造体は、main の出力を定義しています。最初の 2 つのフィールドは、Direct2D 効果の場合に必須です (3 つ目の必須フィールドは効果に入力ビットマップが関係する場合に指定します)。sceneSpaceOutput というフィールドは、入力座標に基づいています。一部の効果ではこの座標を変更しますが、この例の効果では変更せず、3D 入力座標を w の値が 1 の 4D 同次座標に変換するだけです。

output.sceneSpaceOutput = float4(input.position.xyz, 1);

頂点シェーダー出力には、入力した色から設定するだけの、color という必須ではないフィールドも含まれています。

output.color = input.color;

clipSpaceOutput という必須出力フィールドは、3D で使用される正規化座標として各頂点の座標を表します。これらの座標は、先月のこのコラムで説明した、カメラの投影変換から生成する座標と同じです。これらの clipSpaceOutput 座標では、x 値は画面の左端を –1 とし右端を 1 とする範囲、y 値は下端を –1 とし上端を 1 とする範囲、z 値はユーザーに最も近い座標を 0 とし最も遠い座標を 1 とする範囲です。フィールドの名前が示すように、これらの正規化座標は、3D シーンを画面に合わせてクリッピングするために使用します。

これらの clipSpaceOutput 座標を計算しやすいように、ClipSpaceTransforms という 3 つ目の構造体を自動的に提供します。これらの座標は、画面のピクセル幅と高さに基づく 4 つの数字と、DrawImage で効果をレンダリングする際に適用される任意のデバイス コンテキスト変換です。

しかし、x 座標と y 座標に対する変換だけを提供しているので、元の頂点バッファーでは z 座標の値が 0 ~ 1 になるように定義しています。別のアプローチとしては、頂点シェーダーで実際のカメラ投影変換を使用する方法もあります (これについては、今後のコラムで説明します)。

これらの z 値も深度バッファーで自動的に使用される結果、小さい z 座標の値を持つピクセルによって大きい z 座標の値を持つピクセルが隠されます。しかし、これが発生するのは、効果クラスの SetDrawInfo メソッドから、D2D1_VERTEX_OPTIONS_USE_DEPTH_BUFFER フラグが設定された SetVertexProcessing を呼び出す場合のみです (この呼び出しを実行すると、プログラムの実行中に Visual Studio の出力ウィンドウに COM エラーが表示される場合もありますが、このエラーはマイクロソフトによる Direct2D 効果サンプル コードでも発生します)。

…ピクセル シェーダーへ

効果をレンダリングするたびに (一般的には、ビデオ ディスプレイのフレーム レートの頻度で)、頂点バッファーのすべての頂点に対して (この場合は 9 回) 頂点シェーダーが呼び出されます。

頂点シェーダーからの出力は、ピクセル シェーダーへの入力と同じ形式です。図 6 のピクセル シェーダーに示すように、PixelShaderInput 構造体は、頂点シェーダーの VertexShaderOutput と同じです。

図 6 SimpleTriangleEffectPixelShader.hlsl ファイル

// Per-pixel data input to the pixel shader
struct PixelShaderInput
{
  float4 clipSpaceOutput : SV_POSITION;
  float4 sceneSpaceOutput : SCENE_POSITION;
  float3 color : COLOR0;
};
// Called for each pixel
float4 main(PixelShaderInput input) : SV_TARGET
{
  // Simply return color with opacity of 1
  return float4(input.color, 1);
}

ただし、ピクセル シェーダーは三角形のすべてのピクセルに対して呼び出され、構造体のすべてのフィールドがその三角形の平面に対して補間されます。ピクセル シェーダーの main 関数は、不透明度を含む 4 つの構成要素から成る色を返す必要があるので、補間した RGB カラーに不透明度フィールドを追加して変更します。この色がディスプレイに出力されます。

ピクセル シェーダーの興味深いバリエーションを紹介しましょう。sceneSpaceOutput フィールドの z 座標の範囲は 0 ~ 1 です。そのため、この z 座標を使用して灰色のシェーディングを作成し、main メソッドから返すと、各三角形の深度を視覚化できます。

float z = input.sceneSpaceOutput.z;
return float4(z, z, z, 1);

機能強化の可能性

SimpleTriangleEffect ではいくらか機能を省略しています。頂点の入力を設定するメソッドを実装すれば、このコードの用途がさらに広がります。他の機能を追加してもよいでしょう。行列演算は GPU で実行されるので、頂点シェーダーは行列変換 (回転、カメラの変換など) の実行に最適です。

コード強化の実装という誘惑に抗えるプログラマはほとんどいないでしょう。特に、静的画像をアニメーション画像に変換する強化は魅力的です。

Charles Petzold は MSDN マガジンの記事を長期にわたって担当しており、Windows 8 向けのアプリケーション開発についての書籍『Programming Windows, 6th Edition』(Microsoft Press、2013 年) の著者でもあります。彼の Web サイトは charlespetzold.com (英語) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Doug Erickson に心より感謝いたします。
Doug Erickson は、Microsoft の OSG 開発者ドキュメンテーション チームのための主任プログラミング作成者です。DirectX グラフィックスのコードとコンテンツを作成および開発しているとき以外は、Charles Petzold などの記事を読んでいます。彼は、余暇でもこんな調子なのです。後は、単車に乗るのも好きです。