Direct2D (5) ID2D1Geometry を使う

GPU の活用によりラスタライズはたいしたウエイトを占めておらず、CPU の仕事は
与えるデータの構築だけとなりました。それゆえ Direct2D はただの描画命令の
集まりにはなっていません。
ベクター描画の途中に介入し、ベクトルの情報のままデータを扱うことが可能です。
Direct2D が単なる GDI の置き換えではなく、可能性を秘めているのはこの Geometry
ではないかと思っています。

実際に ID2D1Geometry がどのようなものか興味あるので、機能を 1つ 1つ追いかけて
みました。

● Path の作成

Path の作成は下記の通り。

ID2D1Factory*	iFactory;
ID2D1PathGeometry*	iPath;
iFactory->CreatePathGeometry( &iPath );

ID2D1GeometrySink*	isink;
iPath->Open( &isink );
isink->SetFillMode( D2D1_FILL_MODE_WINDING );
isink->BeginFigure( startpos, D2D1_FIGURE_BEGIN_FILLED ); // = SVG M
isink->AddLine( pos1 );	    // = SVG L
isink->AddBezier( D2D1::BezierSegment( c2, c3, c4 ) );	 // = SVG C
 ...
isink->EndFigure( D2D1_FIGURE_END_CLOSED );   // = SVG Z
isink->Close();

ID2D1GeometrySink::Close() (ID2D1SimplifiedGeometrySink::Close()) は
SVG 等の Z コマンド相当かと思っていたら EndFigure( D2D1_FIGURE_END_CLOSED )
の方でした。

D2D1_FIGURE_END_OPEN     そのまま
D2D1_FIGURE_END_CLOSED   BeginFigure() の位置に直線で接続(閉じたパスにする)

ID2D1GeometrySink::SetFillMode() では塗りつぶしのルールを決定します。
SVG 等の vector graphics にある evenodd / nonzero に相当します。

・D2D1_FILL_MODE_ALTERNATE
 ”evenodd” 相当。外からのレイが交差した回数で決定します。
 交差回数が偶数なら外、奇数は中。パスの回転方向は影響しません。

・D2D1_FILL_MODE_WINDING
 ”nonzero” 相当。外からのレイが交差するとき、交わった線の方向を考慮します。
 たとえば閉じているパスが仮に右回りなら +1、左回りなら -1、結果が 0 なら外。
 つまりパスの回転方向を見ており、逆回転はくり抜き。

● Sink

Sink は値の追加、データをためることだけが出来るバッファです。
Add 系命令のみで Get はありません。
ID2D1Geometry では各オペレーションに対して受け皿である Sink を渡します。

最初にただの box を作ってみます。

ID2D1RectangleGeometry*	iRect;
iFactory->CreateRectangleGeometry(
	D2D1::RectF( 0.0f, 0.0f, 60.0f, 50.0f ), &iRect );

この box を元に、線を太らせる処理を行います。
もともとは描画時に太い幅 (strokeWidth) の線で描画するための変換ですが、
描画前に Path として受け取ることが出来るわけです。

// 変換結果を格納する ID2D1PathGeometry を作る
ID2D1PathGeometry*	iPath2;
iFactory->CreatePathGeometry( &iPath2 );

// ID2D1PathGeometry の sink に変換結果を受け取る
ID2D1GeometrySink*	isink;
iPath2->Open( &isink );
// iRect の変換結果を isink へ
iRect->Widen( 10.0f, NULL, NULL, isink ); // strokeWidth= 10.0f
isink->Close();

実際に描画したのがこちら。

Direct2D sample

↑左から順に
 ・元の Box
 ・元の Box を幅 10 の線で描画したもの
 ・幅 10 の描画を Path で受け取ったもの
 ・幅 10 で Path 化したものをさらに 幅 4 の線で描画したもの

// 描画コード
iRenderTarget->SetTransform( D2D1::Matrix3x2F::Translation( 20.5f, 20.5f ) );
iRenderTarget->DrawGeometry( iRect, iBrush0 );

iRenderTarget->SetTransform( D2D1::Matrix3x2F::Translation( 120.5f, 20.5f ) );
iRenderTarget->DrawGeometry( iRect, iBrush0, 10.0f );

iRenderTarget->SetTransform( D2D1::Matrix3x2F::Translation( 220.5f, 20.5f ) );
iRenderTarget->DrawGeometry( iPath2, iBrush0 );

iRenderTarget->SetTransform( D2D1::Matrix3x2F::Translation( 320.5f, 20.5f ) );
iRenderTarget->DrawGeometry( iPath2, iBrush0, 4.0f );

RenderTarget には直接描画可能な DrawRectangle() 命令があります。
Direct2D では内部的には全く同じように

(1) CreateRectangleGeometry() 等でプリミティブを作成
(2) Widen() など与えたオプションによって必要な変換を行う
(3) Tessellate() で ID2D1Mesh 化
(4) 最終的にすべて ID2D1RenderTarget::FillMesh()

という手順で描画しているだけかもしれません。この場合一番低レベルな描画は
FillMesh() になります。
ただし現在 FillMesh() での直接描画が成功しておらず、これらは予想の範囲を
超えておりません。

もし予想が合っているのなら、Geometry を扱うことでより柔軟なデータ管理ができます。
たとえば比較的 Mesh に近い状態のままキャッシュすることで、高速化や再利用が
可能となるでしょう。毎回 stroke を指定して Draw するよりも Geometry か Mesh
化しておいた方が高速だと思います。

●変換形状の読み出し

Geometry の中身は隠蔽されており、パスの座標値などデータを直接さわることが
できなくなっています。
ところが SDK には、テセレートされたアウトラインフォントの形状を取り出して立体化し、
3D ポリゴンとして描画するサンプルプログラム Interactive3dTextSample があります。

その仕組みはこうです。

(1) Sink 互換のインターフェースを自分で定義しておく
(2) Geometry の Tessellate() 等に渡して変換結果を受け取る

たとえば ID2D1Geometry::Tessellate() であれば、変換後の結果を格納するために
内部で ID2D1TessellationSink::AddTriangles() を呼び出しています。
互換インターフェースで AddTriangles() を定義しておけばトラップ可能となるわけです。

このように、Tessellate() など Geometry で変換した座標値をアプリケーションで
使うことができます。

関連エントリ
Direct2D (4) Direct2D の描画
Direct2D (3) 互換性の検証と Vista で Direct2D
Direct2D その(2) インターフェース
Direct2D と Direct3D10.1 の下位互換