Edited at

F#にOpenGLの歯車デモを移植

More than 1 year has passed since last update.

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のサンプルをいくつか移植してみました。