うーむ
明日書こう
そうだ 明日書こう!
そう思っていったい何日経過したことか
そうだ、明日書こう!!!!!
しばらく更新しなくてごめんなさい
理由は、ちょっと別のプログラミングに興味を持ってやっていたというのと
冬はスノボーの季節なので土日が全部空いてないという残酷な状況だったからです
雪が無くなったら、ブログの更新を再開します
すっごくかわいい女の子の友達がいるんですよ。僕がまだ小さな時知り合った子です。
なんだかんだ、恋愛関係になることもなく、単に腐れ縁友達となって、今でも年に数回、二人で呑むことがあるんです。
しかしついこの間、呑みに行った帰りに、その子が、もっと遊ぼうって言ったんです。いつもなら呑んですぐに帰るんですけど。
僕が、どうしたの?って聞くと、今日は帰りたくないって、なんかドラマみたいな台詞を言うじゃないですか。
僕ももうドキドキで、ハアハアですよ。じ、時間も遅いし、ほ……ホテルでも行こうか、あ、ああもちろん何にもしないから!!
と言ったら、うん。って言うんですよ。ああ、やべーきたよこれキタ。とか思ってホテルにいく設定の夢を見ました。
今回は、前回の続き、ID2D1PathGeometryのAdd系関数についてみていこうと思います。
Add系関数は以下のようなものがありました。
/*円弧セグメントを追加する*/ AddArc /*ペジェ曲線を追加する*/ AddBezier /*ペジェ曲線(の配列)セグメントを追加する*/ AddBeziers /*簡易ペジェ曲線セグメントを追加する*/ AddQuadraticBezier /*簡易ペジェ曲線(の配列)セグメントを追加する*/ AddQuadraticBeziers /*直線セグメントを追加する*/ AddLine /*直線(の配列)セグメントを追加する*/ AddLines
このうち、後ろに「s」がついているやつは、一気にもそっと追加するバージョンなだけなので、はしょります。
ということで、覚えるべきAdd系関数はAddArc,AddBezier,AddQuadraticBezier,AddLineの4つです。
まずはAddLineについて。
void AddLine(D2D1_POINT_2F point);
言うことないですね。見たまんまです。
次にAddQuadraticBezierです。二次ペジェ曲線。
こいつは、簡単な曲線を書くためのものです。
void AddQuadraticBezier( const D2D1_QUADRATIC_BEZIER_SEGMENT &bezier );
D2D1_QUADRATIC_BEZIER_SEGMENTを引数にとっています。これがどうなっているかというと、
struct D2D1_QUADRATIC_BEZIER_SEGMENT { D2D1_POINT_2F point1; D2D1_POINT_2F point2; };
2つの点を取るだけです。
point1はコントロールポイント、いわゆる制御点というやつです。
point2は、エンドポイント、すなわち最終的に線が到達する場所、点です。
直前の点から、point2に指定した点まで線を引くが、point1の影響を受けて曲線になる、と言ったらわかりやすいでしょうか。
イメージとしては、直前の点から、point2までの線が、point1に指定したコントロールポイントに引っ張られたような曲線になるって感じです。
数学的な説明については、ここでかくのもあれだしネット上にたくさんの説明があるのでそちらを参照してください。
http://koujinz.cocolog-nifty.com/blog/2009/05/post-fd85.htmlなんかがわかりやすいです。
お次は、AddBezierです。三次ペジェ曲線。
これは、AddQuadraticBezierをもう少し複雑にしたもの、コントロールポイントが1つ増えたものです。
void AddBezier( const D2D1_BEZIER_SEGMENT &bezier );
形としてはAddQuadraticBezierとまったく同じですね。とる引数が違うだけです。
D2D1_BEZIER_SEGMENTがどうなっているかというと、想像通り……
struct D2D1_BEZIER_SEGMENT { D2D1_POINT_2F point1; D2D1_POINT_2F point2; D2D1_POINT_2F point3; };
点が1つ増えているだけです。
point1とpoint2がコントロールポイントとなり、point3をエンドポイントとした曲線を引きます。
イメージとしてもAddQuadraticBezierの時と同じで、直前の点とpoint3を結ぶ線を引くが、point1およびpoint2の方向に引っ張られたような曲線を描くことになります。
フォトショップやイラストレータを知っている人だったら、いわゆる「パスツール」である、といったらわかりやすいでしょうか。
上記したリンクに三次ペジェ曲線の数学的な解説も載っているので、一度目を通してみることをお勧めします。
(難しいものではないので、原理だけでも知っておけば、利用の手助けになると思います)
さて、最後にAddArcです。
これも曲線の一種です。名前からしてわかるとおり弧を描くためのものですが、これは今までのAdd系関数に比べて、ちょっとクセがあります。
もっとも基本的な動作としては、直前の点と、指定した点(エンドポイント)を直径とする半円が描かれます。
ただ、その他にもいろいろとパラメータをとることができ、この使い方にちょっとしたコツがいります。
まあとりあえず、プロトタイプを見てみましょう。
void AddArc( const D2D1_ARC_SEGMENT &arc );
AddArc自体は、他の関数と同じ、単純に対応する値を一つとるだけです。
D2D1_ARC_SEGMENTについてみてみましょう。
struct D2D1_ARC_SEGMENT { D2D1_POINT_2F point; D2D1_SIZE_F size; FLOAT rotationAngle; D2D1_SWEEP_DIRECTION sweepDirection; D2D1_ARC_SIZE arcSize; };
パラメータがいっぱいあります。眩暈がします。覚えたくないです。もうだめです。助けて……
しかし誰も助けてくれないので、理解するしかありません。だるい。
pointについては、まあわかると思いますが、エンドポイントです。最終的にこの点に線が到達します。
sizeは、xとyの半径を指定することができます。この値はちょっとクセがあります。
通常、直前の点からpointまでを直径とする正円を半分にぶった切った半円になるのですが、sizeに指定するx方向半径とy方向半径を調節することで、楕円っぽくすることができます。
しかしちょっと待った、直前の点と、pointで指定したエンドポイントが決められている以上、それら2つの点を必ず通過するような円にならなければなりません。
そうすっとsizeに指定するxとy(つまりx方向半径と、y方向半径)はどういう扱いになるのか?
これは、本当の半径以上の値を指定すれば、その値どおりになり、本当の半径以下の値を指定すると、比率になります。(みたいです。実験してわかったことです。)
つまり、例えば直前の点とエンドポイントの長さが100ある時、1,1を指定すると、直径を100とする正円を規則正しく半分にぶった切った半円が描かれることになります。
1,2を指定すれば、縦長になった楕円になります。
ちょっとわかりづらいです。(慣れる意味もこめて、少々値をいじくって実験してみるとよいです)
rotationAngleは、楕円の時に効果を発揮します。正円の時には効果は発揮しないようです。
楕円の時これを指定すると、時計回り方向に円がぐにゃってなります。ぐにゃって何?って言われても……
イメージとしては、風で飛ばされそうな風船みたいになります。
sweepDirectionは、半円の方向です。時計回りに書くか、反時計回りに書くか。
D2D1_SWEEP_DIRECTIONはenumで、以下の値をとり得ます。
typedef enum { D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISE, D2D1_SWEEP_DIRECTION_CLOCKWISE } D2D1_SWEEP_DIRECTION;
まあ、見れば明らかですが、D2D1_SWEEP_DIRECTION_COUNTER_CLOCKWISEは反時計回り、D2D1_SWEEP_DIRECTION_CLOCKWISEは時計回りです。
例えば、x軸に水平になるように直前の点とエンドポイントをとり、かつ直前の点のほうがエンドポイントよりxの値が小さい場合(つまり左から右に線を引く場合)場合、半円を書くとするなら上のほうに弧ができるように書くか、下のほうに弧ができるように書くか、2通りの書き方ができると思います。
上側に弧ができるように書きたいのなら、時計回りに弧を描きますよね。逆にした側に弧ができるように書きたいのなら、反時計回りです。そういうことです。
そして最後に、arcSizeについてですが、こいつはD2D1_ARC_SIZE_SMALL, D2D1_ARC_SIZE_LARGEをとり得ます。
コレが何の意味があるかということですが、まず前提として、どうやらこれはsizeで指定した値が比率ではないとき、つまり実際の半径より大きい値を指定したときに効果があるようです。
そして、sizeの指定により楕円(正円含む)になった状態に対して効果があります。
ここまでの解説では、半径ありきで考えてきましたが、その考えを捨て去ってください。
端的に説明してしまうと、直前に指定した点と、今回指定したpointを結ぶ線、この線分よりもでっかい直径をもつ円の円周に両端が接するように、ぽこっと入れたとすれば、その線分で2分割される円の片割れは、ちいさいのとでっかいの2つができますよね?
まるいホールケーキを半分に切る時に直径で切れば、きれいに真っ二つになって美しいですが、俺のほうがいっぱい食べたいから直径からわざとずらして、彼女のぶんのケーキの量を減らすことに似ています。

このとき、でっかいほうのケーキを要求するのがD2D1_ARC_SIZE_LARGEで、小さいほうのケーキで我慢するのがD2D1_ARC_SIZE_SMALLです。
ま、僕は甘いものなんて好きじゃないから、小さいほうでいいよ。っていって彼女に大きいほうを上げて、喜ばせて、だけど彼女はケーキを食べ切れなくて、余ったケーキを、しょうがないなあ、といいながら食べてあげて、結局僕のほうがいっぱいケーキ食べることになるってことはあらかじめ計算していてあえて大きいほうをあげておき、さらに彼女は僕のさりげない(計算された)優しさを知ることになりそのあとふたりは(ry
というのが、arcSizeとなります。
Add系関数は、概念としては非常に簡単で、ただ点を追加していくだけ、ただその途中にどういうふうに線を曲線とするか、という程度のものなので、いくつか図形をつくって描画してみて、感覚をつかんでみてください。
次回は、PathGeometry(GeometrySink)を使う上での、いくらかの細かいフラグ関係についてみてみます。
ええ。雑記です。
Direct2Dとはなんの関係もありません。
Direct2D入門ってサイトなのに、それはどうよ?
そう思うかもしれません。しかし書かずにはいられませんでした。
今は反省していません。
ASIO(Audio Streaming Input Output)でオーディオを低レイテンシで入力するプログラムを趣味で作ろうと思ってASIO SDKをダウンロードしてきて、SDKはコードが全部あるのでざっと読んだのですが、これちょっとひどいですね。
ASIO対応のオーディオインターフェイスはそこそこのお値段しますが、その中でも、まあそこそこな機器を持ってるですよ。昔Cubase SXでDTMやろうと興奮気味に買ったのですが、3曲くらいつくって飽きて、いまや押入れに眠るようになってしまった幻の機器が。
ひさしぶりに(エロ本を探すため)押し入れをあさっていたら発見して、これはもったいない、ちょっとこいつで遊んでみようとおもって手を出したのです。
WindowsにおけるASIOプログラミングは、SDKの内部的にはレジストリにあるASIOドライバのエントリを列挙して、そこにかいてあるCLSIDからCOMで提供されているASIOドライバへアクセスするインターフェイスをロードする形になってるみたいですが、SDKにおけるその使い方というかライブラリがひでえ。
コードは簡素で読みやすいのですが、お行儀が悪すぎる。
グローバル変数つかいまくりんぐなのはまだよしとしても、newしたオブジェクトをdeleteしないとか、クラスのコンストラクタでCOMの初期化を勝手にするとか。
ドライバロードするのに、SDK中のcppにある関数のプロトタイプがヘッダに書いてないから、自分で使いたい場面でプロトタイプ宣言して使うとか、グローバル変数をexternして使うとか。
マルチスレッドのことまったく考慮してないのはいいにしてもCOMが内部に隠蔽された状態で勝手にSTAで初期化されるから、ASIO SDKをスレッドセーフに使ったとしてもCOM的に不正になるとか。
それ以外にもコードを追っていくと判明する数々の柔軟性の無さにファーーーーーーーック!!!って叫んでしまいそうになります
C言語とC++が絶妙に混ざり合ったなんともいえない仕上がりになっています。まあ、そもそもこのSDKってサポートすらしないってものらしいので、ようはSDKという名のサンプルコードなんでしょうね。
じゃあ最初からサンプルコードってことで公開しろよ……
かといってASIO自体は魅力的なので、しょうがないからSDKをまるごと作り直しています。
本当ならこんなめんどくさいことはしたくないのですが、どうしょうもないから、やるしかありません。
理不尽なことはやめて、C++ベースにして、newしたらdeleteするようにして(笑)、マルチスレッドでもCOMを扱えるようにして……
まるごと作り直しです。修正というレベルではなく、完全に作り直し。
普通SDKと名乗るならこれくらいやっておいてくれてもいいのにさ。
ASIOドライバを使ってるソフトウェアを作ってる会社(や人)は、みんなつくってるんだろうな……
まあ救いはASIOドライバの低レベルなインターフェイスはいたってシンプルなことです。
それでもめんどくさい……
でもまあ、そこも含めて、人があまりやらないニッチなジャンルに手を出すときの醍醐味かなとおもって納得しています
なんにせよ、初めてやる分野については、オラ、わくわくすっぞ!
なので、楽しいです。
でもめんどくさい……
みなさんよくコマンドライン使いますよね。しかしコマンドラインて卑猥な気がするのは私だけでしょうか?
コーマン・ド・ライン
ですよ。激しく卑猥です。コーマンだけならいざしらず、ラインまで入ってます。すごい。
コーマンのラインと言ってしまうとあまりに直球なのでコーマンドラインと、外国語っぽくしてみたのでしょうか。
コーマンドラインにいろいろ入力して出力を得る……ゴクリ……
さて今回は、コーマンのラインを描くことも可能なID2D1PathGeometryについてです。
うんちゃらGeometryというクラスは前回のエントリで解説したとおり、いくつかあるのですが、このID2D1PathGeometryがもっとも基本形であり、もっとも難しいです。
後のうんちゃらGeometryは、このID2D1PathGeometryを便利に使うためにあるといっても過言ではありません。(しかし過言かもしれません)
今までのこのサイトというかブログの進行を見ていれば(読んでいれば)わかるとおり、基本的に用法が難しくても、それが後で使うことになったり基本になったりするようなことはもっとも最初にやってしまうという方針です。
なので、他の楽勝なうんちゃらGeometryからやってしまうのも手ですが、いきなりID2D1PathGeometryをやってみようとおもいます。
これがわかれば、後のうんちゃらGeometryはカスです。
とはいっても、ID2D1PathGeometryはそんなに難しいのか!といったらそんなことはありません。
まあ四角形を描くというだけ、に比べたら多少覚えなきゃいけないことが多いくらいかな、程度で、特に対して難しいわけではありませんので、特に身構えず、さらっとやってしまいましょう。
まず第一に、ジオメトリを描画する場合、レンダーターゲットのもつメソッドであるDrawGeometry(線の描画)or FillGeometry(塗りつぶし)によって行います。
このDrawうんちゃらとか、Fillうんちゃらは図形描画の項目でやったと思います。
この図形に相当する部分がジオメトリになっただけなので、これらの関数についてはここでは詳しく触れません。
今回のエントリでは、ID2D1PathGeometryをどのように構築するか、ということをやってみたいと思います。
では、早速ID2D1PathGeometryの構築についてです。
前回のエントリでも軽く触れましたが、ID2D1PathGeometryをつくって、内部に図形を保持させるには、以下のような流れになります。
・ID2D1PathGeometryを生成する
・ID2D1PathGeometryからID2D1GeometrySinkをオープンする
・ID2D1GeometrySinkにたいしていろいろパスを追加したり、別のジオメトリと結合したりとかそういう操作を行う
・ID2D1GeometrySinkをクローズする
・ID2D1PathGeometryが完成
ID2D1GeometrySinkは、ID2D1PathGeometryに図形を流し込むためのものでした。
本来ID2D1PathGeometryがもっていてもおかしくない機能を、UMLでいうところの委譲した形になっています。
前回のエントリでも解説しましたが、うんちゃらGeometryは、一度構築すると、それを修正するようなことはできません。
ID2D1PathGeometryもご他聞にもれず、一度ID2D1GeometrySinkをオープンして図形を流し込み、クローズすることで図形が確定した後は、もうID2D1GeometrySinkを再度オープンすることはできません。
もっかいオープンしようとすると失敗します。
なので、通常は、ID2D1PathGeometryを作成したら、直後にID2D1PathGeometryからID2D1GeometrySinkをオープンし、なんかごにょごにょしたあとに、それをクローズして、図形を確定させ、あとはconstな変数みたいな感じで使うだけとなります。
シンプルといえばシンプルな構造ですが、僕はクラスの関数(この場合オープン)が何度も呼び出せるようになっているのに失敗するというような設計は、基本的に嫌いなので(そもそもオープンできない設計のほうが望ましい)、このID2D1PathGeometryとID2D1GeometrySinkの関係については、すごく違和感がありますが、まあ、こうなってるんだから、このまま使うしかないよね、しょうがない、と諦めています。
さて、ID2D1PathGeometryの生成についてです。
ID2D1PathGeometryはファクトリのCreatePathGeometryによって行います。
(例によってエラー処理ははしょります。自分のプログラムなどではFAILEDマクロなどを使って戻り値のHRESULTをチェックするようにして下さい。)
CComQIPtr<ID2D1PathGeometry> geometry; factory->CreatePathGeometry(&geometry);
先ほど説明したとおり、生成しただけでは、ID2D1PathGeometryの中の図形は確定していませんから、ID2D1GeometrySinkをオープンしてやる必要があります。
CComQIPtr<ID2D1GeometrySink> sink; geometry->Open(&sink);
ここまでで利用した関数は、ファクトリのCreatePathGeometryと、ID2D1PathGeometryのOpenですが、これらは見た瞬間に何を要求していて何をするのか明らかだと思うので、関数のプロトタイプについては記述しなくてもいいですよね???
まあ、とりあえずこれで、ID2D1PathGeometryのオブジェクトをつくり、ID2D1GeometrySinkをオープンすることができました。
あとは、ID2D1GeometrySinkの中に、いろいろな直線や曲線を追加していって、図形を完成させ、クローズすることで確定(=流し込み)してやることでID2D1PathGeometryが描画リソースとして利用できるようになります。
なので、ここからしばらくはID2D1GeometrySinkについての解説です。
ID2D1GeometrySinkは、ID2D1SimplifiedGeometrySinkを継承しています。
ID2D1GeometrySinkは、実際にはID2D1SimplifiedGeometrySinkを、若干使いやすくしたようなもんです。
ただ、ID2D1PathGeometryを構築するには、ID2D1GeometrySinkだけを見ていれば特に問題は無いため(というか継承してるんだからID2D1SimplifiedGeometrySinkのもってるメソッドは使えるし)、そういうことにしておきましょう。
ID2D1GeometrySinkが持っているメソッドは以下のとおりです。
/*ID2D1GeometrySinkを閉じる*/ Close /*図形を開始する*/ BeginFigure /*図形を終了する*/ EndFigure /*塗りつぶしモードを指定する*/ SetFillMode /*セグメント(AddXXXで追加したひとつひとつのもの)についてのなんかのフラグを教師ビンビン物語*/ SetSegmentFlags /*円弧セグメントを追加する*/ AddArc /*ペジェ曲線を追加する*/ AddBezier /*ペジェ曲線(の配列)セグメントを追加する*/ AddBeziers /*簡易ペジェ曲線セグメントを追加する*/ AddQuadraticBezier /*簡易ペジェ曲線(の配列)セグメントを追加する*/ AddQuadraticBeziers /*直線セグメントを追加する AddLine /*直線(の配列)セグメントを追加する*/ AddLines
AddXXXsのような「s」がついてるやつは、単純に、その配列をガバッっと追加するものです。それ以上でもそれ以下でもありません。
んでば、詳しく見ていきましょう。それぞれの詳細については後でやります。今はとにかくこれらを使う手順を追ってみます。
まず、ID2D1GeometrySinkは、何個もの図形を流し込むことができます。
といってもよくわからないと思うので具体例を述べてみると、例えば単純にAddLineを呼び出したとすると、前に追加した点(というかAddXXXによって構成された最後の点)とAddLineで指定した点(これはAddLineという名前ですが、点を追加するだけです)を結ぶ直線セグメントを追加することになります。
つまり、単純にAddXXXを呼び出し続けると、全部繋がります。
なので、つながりを切断して、新しく図形を始めるためにBeginFigureとEndFigureがあります。
このBeginFigureとEndFigureにかこまれている間に追加したAddXXX郡で1つの図形が構成され、また次のBeginFigure、EndFigureの間に追加したAddXXX郡で1つの図形が構成され、といった具合です。
このような構成になっている以上、BeginFigureとEndFigureの間以外でAddXXXを呼び出すと失敗します。
もうひとつ、セグメントという言葉が何度もでてきていますが、このセグメントというのは、例えば図形中の直線1つ、ペジェ曲線1つ、のことです。
ようするに、AddXXXで追加したなんらかの図形構成要素の一つのことをセグメントと言っています。
ここまで説明すればお分かりのとおり、基本的な方針としては
・ID2D1GeometrySinkがオープンされる
・BeginFigureを呼ぶ
・Add系関数を何度か読んで好きな図形を構築する
・EndFigureを呼ぶ
・さらに図形を作りたい場合BeginFigure~EndFigureの流れを何度も繰り返す
・最後にClose
ということになります。
その他、いくつかのフラグなどがありますが、今回のエントリでは、そういったものはあまり考えず、とにかく図形を構築して描画、というところまでやりたいと思います。
あまり複雑な図形をつくるのもめんどくさいので、三角形を作ってみます。
factory->CreatePathGeometry(&geometry); CComQIPtr<ID2D1GeometrySink> sink; geometry->Open(&sink); sink->BeginFigure(D2D1::Point2F(300,300), D2D1_FIGURE_BEGIN_FILLED); sink->AddLine(D2D1::Point2F(500, 500)); sink->AddLine(D2D1::Point2F(100, 500)); sink->EndFigure(D2D1_FIGURE_END_CLOSED); sink->Close();
上記のコードで、三角形を作ることができました。
D2D1_FIGURE_BEGIN_FILLEDとか、よくわからんものが出てきていますが、とりあえず今は気にしないことにしましょう。全体の流れを見てください。
BeginFigureで、開始点(最初の点)を指定して、そこからAddLineで、次の点を指定しています。つまりBeginFigureで指定した点とAddLineで指定した点を結ぶ直線ができます。
同様に、次のAddLineによって、直前の点と、指定した点によって直線ができます。
EndFigureで図形を確定し、Closeによってその図形をID2D1PathGeometryのものとして流し込み、確定させます。
あれ、簡単じゃね?なんかこれは序の口で、もっと難しくなるの?
といえば、そんなことはありません。これが基本形で、これがすべてみたいなものです。
あとは、ちょっと細かいフラグを覚えて、曲線とか、Arcなどがどういうものなのかというのを覚えれば終わりです。
ためしに、ここでつくったgeometryを、FillGeometryで塗りつぶし描画してみましょう。
target->FillGeometry(geometry, brush);
どうでしょう、三角形が表示されましたでしょうか?
次回は、他のAdd系関数について詳しく見てみたり、フラグ関係について整理してみたいと思います。