次の方法で共有



September 2009

Volume 24 Number 09

Windows と C++ - Direct2D による描画

Kenny Kerr | September 2009

MSDN Magazine 6 月号 (msdn.microsoft.com/ja-jp/magazine/dd861344.aspx) で Direct2D について紹介しました。Direct2D は、特に要件が厳しく、見た目に美しいデスクトップ アプリケーションのパフォーマンスを最大限に引き出す新しい 2D グラフィックス API です。そのコラムでは、Direct2D に適した Windows のさまざまなグラフィックス API、Direct2D のアーキテクチャ、およびその原理について説明しました。具体的には、信頼性が高く、効率のよい方法で Direct2D を使用して、ウィンドウ内部のレンダリングを行う基礎について詳しく説明しました。また、デバイス固有のリソースとデバイスに依存しないリソースの作成方法と、それぞれのリソースのライフサイクルについても触れました。この記事は Direct2D の基礎をなす部分を数多く扱っているため、まだお読みでなければ、先に進む前にお読みいただくことをお勧めします。

レンダリングと制御

Direct2D は、ハードウェア アクセラレータを使用する 2D レンダリング API と考えるとわかりやすいと思います。もちろん、ソフトウェア フォールバックはサポートされますが、ここで重要なのは Direct2D がレンダリングを行うという点です。Windows の他のグラフィックス API とは異なり、Direct2D はグラフィックスに対してコンポーネント化したアプローチを使用します。ビットマップのデコードとエンコード、テキスト レイアウト、フォント管理、アニメーション、3D などにそれぞれ専用の API を用意するのではなく、レンダリングやグラフィックス プロセッシング ユニット (GPU) を制御することに重点を置くと同時に、テキスト レイアウトやイメージングなどに目的を絞った他の API に対する非常に優れたフックを提供します。ただし、さまざまな種類のブラシ、単純な図形や複雑な図形を表すプリミティブ、つまり、あらゆる 2D のグラフィックス アプリケーションのビルド ブロックは提供します。

今回のコラムでは、Direct2D を使用して描画する方法を紹介します。まず、Direct2D の色構造について説明してから、
さまざまな種類のブラシを作成する方法について説明します。Windows に含まれている他のほとんどのグラフィックス API とは異なり、Direct2D には "ペン" を表すプリミティブはありません。そのため、あらゆる輪郭の描画やと塗りつぶしにはブラシを使用することになるので、ブラシがきわめて重要になります。前置きはこれぐらいにして、プリミティブ図形の描画方法について説明しましょう。

Direct2D では簡単な構造体を使って色の構成要素を浮動小数点値で指定して、色を表現します。D2D1_COLOR_F 型は、実際には、色の値を表すために Direct3D で使用される D3DCOLORVALUE 構造体の typedef です。これには、赤、緑、青、およびアルファ チャネルをそれぞれ浮動小数点値で指定します。値の範囲は 0.0 ~ 1.0 で、0.0 はカラーチャネルでは黒になり、アルファ チャネルでは完全な透明になります。

次に例を示します。

struct D3DCOLORVALUE
{
FLOAT r;
FLOAT g;
FLOAT b;
FLOAT a;
};

Direct2D には、D2D1 名前空間に D2D1_COLOR_F から継承する ColorF というヘルパークラスが用意され、一般的な色定数がいくつか定義されています。ただし、このヘルパー クラスが重要な点は、D2D1_COLOR_F 構造体を初期化する便利なコンストラクターがいくつか提供されることです。たとえば、次のように赤を定義できます。

const D2D1_COLOR_F red = D2D1::ColorF(1.0f, 0.0f, 0.0f);

もう 1 つのコンストラクターは、ひとまとめに表現された RGB 値を受け取って、個々のカラー チャネルに変換します。次のように、同じ赤を定義します。

D2D1::ColorF(0xFF0000)

これは、次の列挙型値を使用するのとまったく同じです。

D2D1::ColorF(D2D1::ColorF::Red)

ひとまとめにした RGB 値は表現としては簡潔ですが、カラー チャネルを個別に抽出して等価な浮動小数点値に変換しなければならないため、CPU サイクルが多く消費されるので注意して使用してください。

すべてのコンストラクターは、既定で 1.0 に設定されている (つまり、完全に不透明な) アルファ値を受け取ります。アルファ値は省略可能です。したがって、半透明の青は次のようにして表すことができます。

D2D1::ColorF(0.0f, 0.0f, 1.0f, 0.5f)

ただし、レンダー ターゲットの描画領域をクリアする場合は別にして、色を直接指定して実行できる処理はほとんどありません。そこで、ブラシが必要になります。

ブラシ

単純な色構造とは異なり、ブラシはインターフェイスを通じて公開されるリソースです。直線の描画、図形の描画と塗りつぶし、およびテキストの描画にブラシを使用します。ID2D1Brush から派生するインターフェイスは、Direct2D に用意されているさまざまな種類のブラシを表します。ID2D1Brush インターフェイスそのものを使用すると、ブラシで使用する色のアルファ チャネルを変更するのではなく、ブラシ全体の不透明度を制御できます。これは、より興味深いいくつかの種類のブラシを使用する際に特に役立ちます。

次に、ブラシの不透明度を 50% に設定する方法を示します。

CComPtr<ID2D1Brush> brush;
// Create brush here...
brush->SetOpacity(0.5f);

Windows Presentation Foundation (WPF) とは異なり、ブラシで使用する座標系は、描画する特定の図形の座標系ではなく、レンダー ターゲットの座標系なので、ID2D1Brush ではブラシに適用される変換を制御することもできます。

ブラシを作成するためのさまざまなレンダー ターゲットのメソッドはすべて、省略可能な D2D1_BRUSH_PROPERTIES 構造体を受け取ります。この構造体を使用して、初期の不透明度と変換を設定できます。

Direct2D で用意されているすべてのブラシは変更可能なので、その特性を効果的に変更できるため、異なる特性を備えた新しいブラシを作成する必要はありません。アプリケーションを設計する際は、このことを念頭に置いてください。また、ブラシはデバイスに依存するリソースなので、ブラシを作成したレンダー ターゲットの有効期間内しか使用できません。つまり、レンダー ターゲットの有効期間内であれば特定のブラシを再利用できますが、レンダー ターゲットを解放するときは、ブラシも解放しなければなりません。

名前のとおり、ID2D1SolidColorBrush インターフェイスは純色のブラシを表します。このインターフェイスは、ブラシで使用する色を制御するためのメソッドを追加します。純色ブラシは、次のように、レンダー ターゲットの CreateSolidColorBrush メソッドを使用して作成されます。

CComPtr<ID2D1RenderTarget> m_target;
// Create render target here...
const D2D1_COLOR_F color = D2D1::ColorF(D2D1::ColorF::Red);
CComPtr<ID2D1SolidColorBrush> m_brush;
HR(m_target->CreateSolidColorBrush(color, &m_brush));
This brush's initial color can also easily be changed using the SetColor method:
m_brush->SetColor(differentColor);

図形の詳細については次のテーマとして残しておきますが、確認のために、レンダー ターゲットのサイズに基づく四角形を使用する、ウィンドウのレンダー ターゲットを塗りつぶします。Direct2D ではデバイス非依存ピクセル (DIP) が使用されることに注意してください。その結果、特定のデバイス (デスクトップ ウィンドウのクライアント領域など) で通知されるサイズが、レンダー ターゲットのサイズと一致しないことがあります。さいわい、GetSize メソッドを使用してレンダー ターゲットのサイズを DIP 単位で取得するのはとても簡単です。GetSize は、width と height という 2 つの浮動小数点値でサイズを表す D2D1_SIZE_F 構造体を返します。その後、RectF ヘルパー関数を使用し、レンダー ターゲットによって通知されたサイズをプラグインすることによって、塗りつぶす領域を表す D2D1_RECT_F 変数を提供できます。最後に、レンダー ターゲットの FillRectangle メソッドを使用して実際の描画を行います。

コードは次のようになります。

const D2D1_SIZE_F size = m_target->GetSize();
const D2D1_RECT_F rect = D2D1::RectF(0, 0, size.width, size.height);
m_target->FillRectangle(rect, m_brush);

図 1 に、純色の緑のブラシを使用して塗りつぶしたウィンドウのようすを示します。とてもいい感じです。

Direct2D には、2 種類のグラデーション ブラシも用意されています。グラデーション ブラシは、ある軸に沿って複数の色を混ぜながら領域を塗りつぶすブラシです。線形グラデーション ブラシは、軸を始点と終点を持つ直線として定義します。放射状グラデーション ブラシは、軸を楕円として定義するため、楕円の中心を基準とした点から外側に向かって色が放射状に広がります。

グラデーションは、0.0 ~ 1.0 の一連の相対位置として定義されます。それぞれの相対位置は、独自の色を持ちます。これは、グラデーション境界と呼ばれます。この範囲外の位置を使用して、さまざまな効果を生み出すことができます。グラデーション ブラシを作成するには、まず、グラデーション境界のコレクションを作成する必要があります。最初に、D2D1_GRADIENT_STOP 構造体の配列を定義します。

次に例を示します。

const D2D1_GRADIENT_STOP gradientStops[] =
{
{ 0.0f, color1 },
{ 0.2f, color2 },
{ 0.3f, color3 },
{ 1.0f, color4 }
};

次に、レンダー ターゲットの CreateGradientStopCollection メソッドを呼び出して、グラデーション境界の配列に基づいてコレクション オブジェクトを作成します (次のコード参照)。

CComPtr<ID2D1GradientStopCollection> gradientStopsCollection;

HR(m_target->CreateGradientStopCollection(gradientStops,
_countof(gradientStops),
&gradientStopsCollection));

1 つ目のパラメーターは配列へのポインターで、2 つ目のパラメーターは配列のサイズを指定します。ここでは、標準の _countof マクロを使用しています。CreateGradientStopCollection メソッドは新しいコレクションを返します。線形グラデーション ブラシを作成するには、D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES 構造体を指定して、グラデーションの軸の始点と終点を指定します。グラデーション境界の位置とは異なり、始点と終点はブラシの座標空間内にあります。通常、このブラシの座標空間は、ブラシに変換が設定されていない限り、レンダー ターゲットの座標空間になります。たとえば、次のようにして、レンダー ターゲットの左上隅から右下隅に引いた軸を使用するブラシを作成できます。

const D2D1_SIZE_F size = m_target->GetSize();
const D2D1_POINT_2F start = D2D1::Point2F(0.0f, 0.0f);
const D2D1_POINT_2F end = D2D1::Point2F(size.width, size.height);
const D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES properties = D2D1::LinearGradientBrushProperties(start, end);
HR(m_target->CreateLinearGradientBrush(properties,
gradientStopsCollection,
&m_brush));

LinearGradientBrushProperties は、D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES 構造体を初期化するために Direct2D に用意されているもう 1 つのヘルパー関数です。CreateLinearGradientBrush メソッドは、このヘルパー関数と共に前述のグラデーション境界のコレクションを受け取り、新しいブラシを表す ID2D1LinearGradientBrush インターフェイス ポインターを返します。

もちろん、ブラシは、レンダー ターゲットのサイズが変わるかどうかは認識しません。ウィンドウのサイズが変更されたときに軸の終点を変更する場合は、ブラシの SetEndPoint メソッドを使用して簡単に実行できます。同じように、SetStartPoint メソッドを使用して、軸の始点を変更できます。

描画コードは次のようになります。

const D2D1_SIZE_F size = m_target->GetSize();
const D2D1_RECT_F rect = D2D1::RectF(0, 0, size.width, size.height);

m_brush->SetEndPoint(D2D1::Point2F(size.width, size.height));
m_target->FillRectangle(rect, m_brush);

図 2 は、線形グラデーション ブラシを使用したときのウィンドウのようすを示しています。

放射状グラデーション ブラシを作成するには、D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES 構造体を指定する必要があります。この構造体は、楕円と、楕円の中心の相対的なオフセットを定義します。楕円の中心は、グラデーション境界のコレクションに基づいて、ブラシが色を "放射" する元となる原点に相当します。次の例は、レンダー ターゲットの中心に配置され、レンダー ターゲットの半径に相当する半径 X と Y を持ち、右下隅との中間に原点がある楕円を作成します。

const D2D1_SIZE_F size = m_target->GetSize();

const D2D1_POINT_2F center = D2D1::Point2F(size.width / 2.0f, size.height / 2.0f);
const D2D1_POINT_2F offset = D2D1::Point2F(size.width * 0.25f, size.height * 0.25f);
const float radiusX = size.width / 2.0f;
const float radiusY = size.height / 2.0f;

const D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES properties = D2D1::RadialGradientBrushProperties(center,

offset,

radiusX,

radiusY);

HR(m_target->CreateRadialGradientBrush(properties,
gradientStopsCollection,
&m_brush));

RadialGradientBrushProperties は、D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES 構造体を初期化するために Direct2D に用意されているもう 1 つのヘルパー関数です。CreateRadialGradientBrush メソッドは、このヘルパー関数と共に前述のグラデーション境界のコレクションを受け取り、新しいブラシを表す ID2D1RadialGradientBrush インターフェイス ポインターを返します。

次のようにして、ブラシの楕円と原点をいつでも変更できます。

m_brush->SetCenter(center);
m_brush->SetGradientOriginOffset(offset);
m_brush->SetRadiusX(radiusX);
m_brush->SetRadiusY(radiusY);

図 3 に、実行中の放射状グラデーション ブラシを示します。

Direct2D にはビットマップ ブラシも用意されていますが、Direct2D ビットマップの詳細については、今後のコラムのテーマとして残しておきます。

図形

これまで、ブラシを中心に説明するために、四角形を塗りつぶす方法のみを話題にしてきましたが、Direct2D では、単純な四角形を超えるはるかに複雑な図形を描画できます。手始めとして、四角形、角の丸い四角形、および楕円のプリミティブが用意されています。プリミティブとは、これらの各図形の単純なデータ構造を指します。

D2D1_RECT_F は、浮動小数点値で座標を指定する四角形を表します。D2D1_RECT_F そのものは、D2D_RECT_F の typedef です。

struct D2D_RECT_F
{
FLOAT left;
FLOAT top;
FLOAT right;
FLOAT bottom;
};

D2D1_ROUNDED_RECT は、角の丸い四角形を表します。これは、丸い角を描画するために使用する 1/4 楕円の半径を次のように定義します。

struct D2D1_ROUNDED_RECT
{
D2D1_RECT_F rect;
FLOAT radiusX;
FLOAT radiusY;
};

D2D1_ELLIPSE は楕円を表し、中心点と半径によって定義します。

struct D2D1_ELLIPSE
{
D2D1_POINT_2F point;
FLOAT radiusX;
FLOAT radiusY;
};

これらの構造体はそれほど魅力的に思えないかもしれませんが、これらについて触れる理由が 2 つあります。まず、より高度なジオメトリ オブジェクトを作成する際に、これらのプリミティブを使用します。次に、これらのプリミティブの 1 つを塗りつぶしたり描画したりするだけであれば、一般に、プリミティブを使用するほうが、ジオメトリ オブジェクトを使用するよりも高いパフォーマンスが得られるためです。

レンダー ターゲットには、これらのプリミティブを塗りつぶしたり輪郭を描画したりするための、一連の Fill- メソッドと Draw- メソッドが用意されています。これまで、FillRectangle メソッドの使用方法について説明してきました。FillRoundedRectangle メソッドと FillEllipse メソッドはまったく同じように機能するので、これらの例を説明することで読者をうんざりさせるのはやめます。Fill- メソッドとは対照的に、Draw- メソッドは特定の図形の輪郭を描画するのに使用でき、非常に多用途です。Fill- メソッドと同様、Draw- メソッドにもまったく同じように機能する 3 つのプリミティブがあるため、ここでは DrawRectangle メソッドについてのみ取り上げます。

最も単純な形式では、次のようにして、四角形の輪郭を描画できます。

m_target->DrawRectangle(rect,
m_brush,
20.0f);

3 つ目のパラメーターは、四角形の輪郭を描くストロークの幅を指定します。ストローク自体は四角形の中心に配置されるので、同じ四角形を塗りつぶした場合は、塗りつぶしとストロークが 10.0 DIP 分だけ重なり合って表示されることになります。省略可能な 4 つ目のパラメーターは、描画するストロークのスタイルを制御するために指定します。これは、破線を描画する場合や、図形の頂点を結ぶ線の種類を制御する場合に便利です。

ストロークのスタイル情報は、ID2D1StrokeStyle インターフェイスによって表されます。ストロークのスタイル オブジェクトはデバイスに依存しないリソースなので、レンダー ターゲットが無効化されても再作成する必要がありません。この場合は、Direct2D ファクトリ オブジェクトの CreateStrokeStyle メソッドを使用して、新しいストロークのスタイル オブジェクトを作成します。

ストロークで特定の種類の結線を使用する場合は、次のようにして、ストロークのスタイルを作成できます。

D2D1_STROKE_STYLE_PROPERTIES properties = D2D1::StrokeStyleProperties();
properties.lineJoin = D2D1_LINE_JOIN_BEVEL;

HR(m_factory->CreateStrokeStyle(properties,
0, // dashes
0, // dash count
&m_strokeStyle));

D2D1_STROKE_STYLE_PROPERTIES 構造体には、ストロークのスタイルの多様な側面を制御するさまざまなメンバーが用意されています。たとえば、輪郭や破線の各終端の図形 (キャップ) や、破線自体のスタイルなどがあります。CreateStrokeStyle メソッドの 2 つ目と 3 つ目のパラメーターは省略可能です。独自に破線のストロークのスタイルを定義する場合のみ、これらのパラメーターを指定する必要があります。破線のストロークのスタイルを定義するには、破線の構成要素をペアで指定します。各ペアの 1 つ目の要素は破線の描画部分の長さを指定し、2 つ目の要素は次の描画部分までの空白部分の長さを指定します。値自体にはストロークの幅が乗算されます。目的のパターンを作成するのに必要なだけの数のペアを指定できます。以下に例を示します。

D2D1_STROKE_STYLE_PROPERTIES properties = D2D1::StrokeStyleProperties();
properties.dashStyle = D2D1_DASH_STYLE_CUSTOM;

float dashes[] =
{
2.0f, 1.0f,
3.0f, 1.0f,
};

HR(m_factory->CreateStrokeStyle(properties,
dashes,
_countof(dashes),
&m_strokeStyle));

自分で定義するのではなく、D2D1_DASH_STYLE 列挙型からさまざまな破線のスタイルをいくつか選択できます。図 4 は、さまざまなスタイルのストロークでの実行結果を示しています。よく見てみると、Direct2D が、プリミティブごとにアンチエイリアシングを行い、アルファ ブレンドを自動的に実行し、最終的に最も美しく表示されるようにしていることがわかります。

Direct2D で自動的に行われる処理には、複雑なジオメトリや変換から、テキストおよびビットマップの描画まで他にももっとたくさんあります。今後のコラムでは、これらの機能について取り上げる予定です。

Kenny Kerr は、Windows のソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は weblogs.asp.net/kennykerr (英語) です。