OpenTKは、C#からOpenGLをいじるためのほぼ唯一の選択肢となるツールです。私が作成しているSSHクライアント PoderosaではWindows版、Mac OSX版の両方でお世話になっているライブラリなので、ここで使い方の要点をまとめておきたいと思います。
OpenTKは、OpenGLのAPIをそのままC#に置き換えただけなので、OpenGLの「ハードウェアに直接命令している感じ」が良くも悪くもそのまま出ています。とても現代的なAPIとはいいがたいですが、グラフィックのパフォーマンスを引き出しつつ、Windows/OSX/iOS/androidのすべてでだいたい同じコードが動くのはすばらしいことです。
OpenTKを使うには当然OpenGL自体の知識が必要で、OpenGLはゲーム系のエンジニア以外は知る必要がないと思われがちですが、ハイパフォーマンスなグラフィックとマルチプラットフォーム、という利点は見逃せません。ビジネスアプリでもグラフィック重視の場合は有力な選択肢だと個人的には思っています。
Windowsから使う
Visual StudioからNuGetで入れるのが簡単確実です。派生版をリリースしている人もたくさんいるようですが、なるべく本家のを使ったほうがいいでしょうね。
実際のアプリで使うには、上の画像のようにOpenTKだけでなくGLControlも併用する手がいいようです。GLControlはWindows.Forms.Controlから派生しており、PaintイベントでOpenGLこを使った描画をします。
WPFアプリの場合は、Windows.Formsはできれば触りたくないところですが、僕が調べた範囲ではそれは無理なようでした。WPFのWindowsFormsHostでGLControlを包み込んで使うのがいいでしょう。Windows版Poderosaはこの方法で実装しています。
Xamarin.Macから使う
セットアップ手順はむしろWindowsより簡単です。まあ、どんなアプリでも、実装の手間を考えればライブラリの手間の大小はどうでもいいんですがね。Xamarin Studioでプロジェクトの参照設定を開き、このようにOpenTKにチェックを入れるだけでOKです。
問題はXamarin.Macの世界でどのように描画するかですが、このNeHe Lessonという一連のサンプル群が非常に役立ちました。
NeHe Lesson
OSX版Poderosaをやるかどうか検討していたとき、このサンプルコードを実際に動かしてみたときの感触が決断の大きな要因だったので、もしこれがなければOSX版Poderosaは着手しなかったかもしれません。もうNeHeのほうに足を向けて寝ることはできません。
Xamarin.Formsから使う
Xamarin.FormsはiOS/androidのアプリを作るためのものですが、この場合 OpenGLViewというものが用意されているので話はわりと単純だと思います。ただ、これは実際に動かした経験があるわけではありません。
ガーベジコレクタとの絡み
最強のハマりポイントなので解説が必要です。
2Dのテクスチャを描画する次のようなコードを例に出します。OpenGLの観点からはごく普通に見えますが、これは実際には**「きわめて低い確率ながら」「深刻なクラッシュを」**引き起こします。
public void DrawTexture(int textureID, PointF[] src, PointF[] dest) {
GL.VertexPointer<PointF>(2, VertexPointerType.Float, 0, dest); //(1)
GL.TexCoordPointer<PointF>(2, TexCoordPointerType.Float, 0, src); //(2)
GL.BindTexture(TextureTarget.Texture2D, textureID);
GL.DrawArrays(PrimitiveType.Quads, 0, 4); //(3)
}
描画が実行されるのは**(3)の時点ですが、そのための情報は(1)(2)で与えています**。このとき、与えているデータ型はPointF[]なので、その先に繋がっているOpenGLネイティブコードではその配列の先頭アドレスだけを記憶しているのでしょう。
ですがそのあと、(1)から、実際に描画をするまでの(3)までにガーベジコレクタが走ると(そういう頻度は非常に低いのですが必ずあります)、PointF[]はメモリ上別のアドレスに移動してしまいます。移動前のアドレスには別のデータが入っていたり、そもそもアクセス不可なアドレスになったりするので、クラッシュしてしまうのです。
その事情を踏まえ問題修正したのが次のコードです。C#のunsafeやfixedステートメントはまさにこのためにあります。fixedを使うと、そのスコープを抜けるまでの間、fixedで指定したオブジェクトはGCの動作に関わらず同一のアドレスに保持されます。
public unsafe void DrawTexture(int textureID, PointF[] src, PointF[] dest) {
fixed(PointF* fsrc = src) {
fixed (PointF* fdest = dest) {
GL.VertexPointer<PointF>(2, VertexPointerType.Float, 0, dest);
GL.TexCoordPointer<PointF>(2, TexCoordPointerType.Float, 0, src);
GL.BindTexture(TextureTarget.Texture2D, textureID);
GL.DrawArrays(PrimitiveType.Quads, 0, 4);
}
}
}
fixedで得ているアドレスfsrc, fdestは別にその後のコードでは使っていないことに注目してください。GCによるアドレス移動から守ることだけが目的です。
OpenGLのような低水準のAPIを使う場合にはこういう知識も必要になってくるので、知見を簡単ですがまとめてみました。この一連の問題の分析には、私がかつてJava VMを実装するコードを細かく見ていたのが役立ちました。あのときの経験があったから、このような得体の知れない不意なクラッシュにも「これはGC絡みだな!」と勘が働いた、というのはあります。
過去の経験というのはいつどういう局面で役立つのかは全くわからないものですね。