F#でOpenGLを使うことができたので、デモプログラムを移植してみました。

gears.gif

シリーズの記事です。

関連するコードをまとめたリポジトリです。

【注意】この記事のサンプルはレガシーなAPIを使っています。新しいAPIへの移行は機会を改めることにして、今回はレガシーなまま進めます。

移植

前回の記事で作ったOpenGLサポートに足りない機能を補いながら移植を進めました。

完成したコードを説明します。

※ GL7 という名前は適当で、7は七誌の七です。

コンテキスト

元のC言語の main() から初期化部を抜粋します。

gears.c
main(int argc, char *argv[])
{
  (略)
  glutCreateWindow("Gears");
  init();

GLUTではコンテキストが1つしかないため切り替える必要がありませんが、GLFormでは必要に応じて切り替える方針です。

init() ではOpenGLの関数を使用しています。

gears.c
static void
init(void)
{
  (略)
  glLightfv(GL_LIGHT0, GL_POSITION, pos);
  glEnable(GL_CULL_FACE);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glEnable(GL_DEPTH_TEST);

GLFormではPaintだけ特別扱いでコンテキストを切り替えています。必要に応じてハンドラを追加していてはきりがないため、Paint以外では必要に応じてコンテキストを切り替えるようにします。

スコープアウト時にコンテキストを手放すため、後片付け用のクラスを実装します。

RAIIは手法の略語です。

type RAII(dtor) =
    interface IDisposable with override x.Dispose() = dtor()

RAIIを使ってスコープ内だけでコンテキストを取得するメソッドを実装します。

    member x.MakeCurrent() =
        ignore <| wglMakeCurrent(hDC, hGLRC)
        new RAII(fun () -> ignore <| wglMakeCurrent(0n, 0n))

F#の初期化部でMakeCurrentを使います。GLUTでは起動直後にReshapeイベントが発生しますがWindows Formsでは発生しないため、明示的にハンドラを呼びます。

gears.fsx
    let f = new GLForm(Text = "Gears")
    f.Load.Add <| fun _ ->
        use raii = f.MakeCurrent()
        init()
        reshape f

イベントハンドリング

main() でGUIのイベントハンドラを設定しています。

gears.c
main(int argc, char *argv[])
{
  (略)
  glutDisplayFunc(draw);
  glutReshapeFunc(reshape);
  glutKeyboardFunc(key);
  glutSpecialFunc(special);
  glutVisibilityFunc(visible);

reshape() はウィンドウのリサイズを処理します。ここでもOpenGLの関数が使われています。

gears.c
/* new window size or exposure */
static void
reshape(int width, int height)
{
  GLfloat h = (GLfloat) height / (GLfloat) width;

  glViewport(0, 0, (GLint) width, (GLint) height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.0, 1.0, -h, h, 5.0, 60.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glTranslatef(0.0, 0.0, -40.0);
}

イベントハンドラで必要に応じてMakeCurrentでコンテキストを取得します。GLUTとはハンドラの引数が異なるため、引数を調整してコールバックします。

gears.fsx
    f.Paint.Add <| fun _ ->
        draw f
        idle f
    f.Resize.Add <| fun _ ->
        use raii = f.MakeCurrent()
        reshape f
    f.KeyPress.Add <| fun e ->
        key f e.KeyChar
    f.KeyDown.Add <| fun e ->
        special e.KeyCode

GLUTではVisibilityイベントで非表示時に再描画要求を止めています。Windows Formsでは非表示時にPaintイベントが発生しないため自然と止まるためVisibilityイベントは移植する必要がありません。

これでGLUTとGLFormのすり合わせは完了です。後は文法を書き替えれば移植できます。

float32

F#では数値型の自動キャストは行われないため、必要に応じてキャストを追加します。

gears.c
#ifndef M_PI
#define M_PI 3.14159265
#endif
gears.fsx
let M_PI = float32 Math.PI

そこに注意すれば後はほとんど機械的な書き替えです。

gears.c
static void
gear(GLfloat inner_radius, GLfloat outer_radius, GLfloat width,
  GLint teeth, GLfloat tooth_depth)
{
  (略)
  /* draw front face */
  glBegin(GL_QUAD_STRIP);
  for (i = 0; i <= teeth; i++) {
    angle = i * 2.0 * M_PI / teeth;
    glVertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5);
    glVertex3f(r1 * cos(angle), r1 * sin(angle), width * 0.5);
    glVertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5);
    glVertex3f(r1 * cos(angle + 3 * da), r1 * sin(angle + 3 * da), width * 0.5);
  }
  glEnd();
gears.fsx
let gear inner_radius outer_radius width teeth tooth_depth =
    (略)
    // draw front face
    glBegin(GL_QUAD_STRIP)
    for i = 0 to teeth do
        let angle = float32 i * 2.0f * M_PI / float32 teeth
        glVertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5f)
        glVertex3f(r1 * cos(angle), r1 * sin(angle), width * 0.5f)
        glVertex3f(r0 * cos(angle), r0 * sin(angle), width * 0.5f)
        glVertex3f(r1 * cos(angle + 3.f * da), r1 * sin(angle + 3.f * da), width * 0.5f)
    glEnd()

キーイベント

キーイベントで終了するようになっていますが、いきなり exit は行儀が悪いので、GLFormを渡して閉じるようにします。

gears.c
/* change view angle, exit upon ESC */
/* ARGSUSED1 */
static void
key(unsigned char k, int x, int y)
{
  switch (k) {
  case 'z':
    view_rotz += 5.0;
    break;
  case 'Z':
    view_rotz -= 5.0;
    break;
  case 27:  /* Escape */
    exit(0);
    break;
  default:
    return;
  }
  glutPostRedisplay();
}

別の所で再描画を呼ぶため glutPostRedisplay() は移植から省きます。

gears.fsx
// change view angle, exit upon ESC
let key (f:GLForm) = function
| 'z' -> view_rotz <- view_rotz + 5.0f
| 'Z' -> view_rotz <- view_rotz - 5.0f
| '\u001b' (* Escape *) -> f.Close()
| _ -> ()

F#の構文は縦に長くならないのですっきりしています。別の例も見ます。

gears.c
/* change view angle */
/* ARGSUSED1 */
static void
special(int k, int x, int y)
{
  switch (k) {
  case GLUT_KEY_UP:
    view_rotx += 5.0;
    break;
  case GLUT_KEY_DOWN:
    view_rotx -= 5.0;
    break;
  case GLUT_KEY_LEFT:
    view_roty += 5.0;
    break;
  case GLUT_KEY_RIGHT:
    view_roty -= 5.0;
    break;
  default:
    return;
  }
  glutPostRedisplay();
}
gears.fsx
// change view angle
let special = function
| Keys.Up    -> view_rotx <- view_rotx + 5.0f
| Keys.Down  -> view_rotx <- view_rotx - 5.0f
| Keys.Left  -> view_roty <- view_roty + 5.0f
| Keys.Right -> view_roty <- view_roty - 5.0f
| _ -> ()

アニメーション

画面描画後にすぐ再描画を要求することでアニメーションしています。

gears.fsx
    f.Paint .Add <| fun _ ->
        draw()
        idle f

オリジナルのgearsが開発されたときとはマシン性能が異なるため、元の回転角度では速過ぎます。そのため回転角度を修正します。

gears.c
static void
idle(void)
{
  angle += 2.0;
  glutPostRedisplay();
}
gears.fsx
let idle (f:GLForm) =
    angle <- angle + 0.1f  // 2.0f
    f.Invalidate()

移植時に注意した点は以上です。

感想

GLUTではなく慣れたWindows FormsをOpenGLと組み合わせるのはなかなか快適です。既存の非OpenGL資産との組み合わせも柔軟にできます。もっと早く知っていれば必要以上にGDI+で消耗しなかったのにとも思いましたが、まあ今からでも遅すぎることはないでしょう。

その他

定番の解説書Red Bookのサンプルをいくつか移植してみました。