F#でOpenGLを使うことができたので、デモプログラムを移植してみました。
シリーズの記事です。
- F#でOpenGL
- F#にOpenGLの歯車デモを移植 ← この記事
- OpenGLでオフスクリーンレンダリング
関連するコードをまとめたリポジトリです。
【注意】この記事のサンプルはレガシーなAPIを使っています。新しいAPIへの移行は機会を改めることにして、今回はレガシーなまま進めます。
移植
前回の記事で作ったOpenGLサポートに足りない機能を補いながら移植を進めました。
完成したコードを説明します。
※ GL7 という名前は適当で、7は七誌の七です。
コンテキスト
元のC言語の main()
から初期化部を抜粋します。
main(int argc, char *argv[])
{
(略)
glutCreateWindow("Gears");
init();
GLUTではコンテキストが1つしかないため切り替える必要がありませんが、GLFormでは必要に応じて切り替える方針です。
init()
ではOpenGLの関数を使用しています。
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では発生しないため、明示的にハンドラを呼びます。
let f = new GLForm(Text = "Gears")
f.Load.Add <| fun _ ->
use raii = f.MakeCurrent()
init()
reshape f
イベントハンドリング
main()
でGUIのイベントハンドラを設定しています。
main(int argc, char *argv[])
{
(略)
glutDisplayFunc(draw);
glutReshapeFunc(reshape);
glutKeyboardFunc(key);
glutSpecialFunc(special);
glutVisibilityFunc(visible);
reshape()
はウィンドウのリサイズを処理します。ここでもOpenGLの関数が使われています。
/* 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とはハンドラの引数が異なるため、引数を調整してコールバックします。
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#では数値型の自動キャストは行われないため、必要に応じてキャストを追加します。
# ifndef M_PI
# define M_PI 3.14159265
# endif
let M_PI = float32 Math.PI
そこに注意すれば後はほとんど機械的な書き替えです。
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();
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を渡して閉じるようにします。
/* 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()
は移植から省きます。
// 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#の構文は縦に長くならないのですっきりしています。別の例も見ます。
/* 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();
}
// 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
| _ -> ()
アニメーション
画面描画後にすぐ再描画を要求することでアニメーションしています。
f.Paint .Add <| fun _ ->
draw()
idle f
オリジナルのgearsが開発されたときとはマシン性能が異なるため、元の回転角度では速過ぎます。そのため回転角度を修正します。
static void
idle(void)
{
angle += 2.0;
glutPostRedisplay();
}
let idle (f:GLForm) =
angle <- angle + 0.1f // 2.0f
f.Invalidate()
移植時に注意した点は以上です。
感想
GLUTではなく慣れたWindows FormsをOpenGLと組み合わせるのはなかなか快適です。既存の非OpenGL資産との組み合わせも柔軟にできます。もっと早く知っていれば必要以上にGDI+で消耗しなかったのにとも思いましたが、まあ今からでも遅すぎることはないでしょう。
その他
定番の解説書Red Bookのサンプルをいくつか移植してみました。
簡単なサンプルを2つ移植しました。 https://t.co/VYySVpnzMn pic.twitter.com/9NKIu9qhPw
— 七誌 (@7shi) 2017年1月23日
FreeGLUTから必要な関数だけ移植して、球やトーラスを表示するサンプルを移植した。GLUTありきだと気付かなかったけど、こんなに手間が掛かるんだ… https://t.co/n3AnhQGvnG pic.twitter.com/DXHefpWnX6
— 七誌 (@7shi) 2017年1月27日
移植してみたけどちょっとシンプル過ぎる(教材目的で取っ掛かりを提供するだけなので、これで良いのだろうけど)。拡大して塗りつぶしをやめたら、ワイヤーフレームのトーラスが現れた。 https://t.co/5H2RjsfZ0r pic.twitter.com/Pn6cCXWvaq
— 七誌 (@7shi) 2017年1月30日