前回まで難しい話題が続いてしまったので,今回は少し気楽な話として,OCamlで3Dグラフィックスをやってみよう。一般に,3Dグラフィックスのライブラリとしては,「OpenGL」が有名だ。OCamlにも「LablGL」という,OpenGLのインタフェースがある。ちなみに,LablGLを開発したジャック・ガリグ氏(元京都大学,現名古屋大学)は,OCamlの主要開発者の一人でもある。

 LablGLは,京都大学関数型言語研究グループのサイト(またはミラー・サイト)で無償配布されている。Debianのように,ディストリビューションによっては最初からLablGLのパッケージが用意されている。その場合は,パッケージをインストールするだけで十分だ。パッケージが用意されていなければ,以下の手順に従って,手動でLablGLをインストールする必要がある。

 LablGLはあくまでOpenGLのインタフェースにすぎないので,まず,あらかじめOpenGLをインストールしなければならない。Windows XPや一部のUNIX環境では,標準のOpenGLライブラリが提供されている。それ以外のOSでは,Mesaなど,フリーのOpenGL互換ライブラリが必要になる。これは大抵のOSでパッケージによるインストールが可能だろう。ただし,ソフトウエアによるエミュレーションは速度に問題があるので,できればハードウエアによるサポートがある環境がよい。

 OpenGL本体だけでなく,GLUTという追加ライブラリも必要だ。UNIXの場合,GLUTのパッケージをインストールするか,先のMesaのサイトなどからソースコードをダウンロードしてコンパイルすればよい。Windowsの場合は,http://www.xmission.com/~nate/glut.html(リンク)からglut-3.7.6-bin.zipをダウンロードし,その中にあるglut32.dllをsystem32フォルダにコピーする。

 OpenGLとGLUTがインストールできたら,LablGLをインストールする。UNIXでは,LablGLのREADMEファイルに従えばよい。Windowsでは,先の京大のサイトにあるlablgl-1.02-win32.zipを,Objective Camlをインストールしたディレクトリ(C:\Program Files\Objective Camlなど)に上書きして展開する。次に,コマンドプロンプトでlablGLフォルダに移動し,「ocaml build.ml」を実行する。

 なお,Windowsでは,Visual Studioなどの開発環境がインストールされていないと,アセンブラやリンカがないためネイティブコードが生成できず,「Assembler error」といったエラーが表示されるかもしれない。ただ,バイトコードを使うだけならば支障はないので,無視していい。

 ちなみに,この記事では解説しないが,LablGLにはTcl/TkのTkライブラリを利用する機能もある。Tcl/Tkのソースコードはhttp://www.tcl.tk/からダウンロードできるが,大概のUNIX環境ではパッケージからインストールできるだろう。Windowsの場合は,http://downloads.activestate.com/ActiveTcl/Windows/8.3.5/ActiveTcl8.3.5.0-2-win32-ix86.exe(リンク)をダウンロードして実行すればよい。

 また,TkのかわりにGTK+を利用することも可能だ。詳細はGTK+のOCamlインタフェースであるLablGTKのサイト(またはミラー・サイト)を参照してほしい。

lablglutの起動と光源の設定

 LablGLがインストールできたら,「lablglut」というコマンドを起動してみよう(Windowsではコマンドプロンプトから実行する)。ここで,もし「DLLがない」「.cmaファイルが見つからない」などのエラー・メッセージが表示されたら,LablGLのインストールに失敗している。その場合は,インストールの手順やLablGLのREADMEファイルを確認してほしい。なお,lablglutコマンド自体は,適切な引数を指定してocamlを起動するだけの単純なスクリプトである。

> cat /usr/local/bin/lablglut
#!/bin/sh
# toplevel with lablGL and LablGlut
exec ocaml -I "/usr/local/lib/ocaml/lablGL" lablgl.cma lablglut.cma $*
> lablglut
        Objective Caml version 3.09.3

 #

 lablglutを起動したら,関数「Glut.init」に,コマンドライン引数を表す配列Sys.argvを渡し,初期化を行う。

# Glut.init Sys.argv ;;
- : string array =
[|"/usr/local/bin/ocaml"; "-I"; "/usr/local/lib/ocaml/lablGL"; "lablgl.cma";
  "lablglut.cma"|]

 次に,関数「Glut.createWindow」にタイトル文字列を与え,ウィンドウを作成する(ただし,まだ表示はされない)。

# Glut.createWindow "LablGL: ITpro" ;;
- : int = 1

 さらに,以下のように三つの光源を設定してみよう。

# Gl.enable `lighting (* ライトを有効化 *) ;;
- : unit = ()
# GlLight.light 1 (`ambient (1.0, 1.0, 1.0, 1.0))
  (* 1番のライトを白色の環境光に設定 *) ;;
- : unit = ()
# Gl.enable `light1 (* 1番のライトを有効化 *) ;;
- : unit = ()
# GlLight.light 2 (`diffuse (1.0, 0.0, 0.0, 1.0))
  (* 2番のライトを赤色の点光源に設定 *) ;;
- : unit = ()
# GlLight.light 2 (`position (2.0, 0.0, 0.0, 0.0))
  (* 2番のライトを右方に設置 *) ;;
- : unit = ()
# Gl.enable `light2 (* 2番のライトを有効化 *) ;;
- : unit = ()
# GlLight.light 3 (`diffuse (0.0, 0.0, 1.0, 1.0))
  (* 3番のライトを青色の点光源に設定 *) ;;
- : unit = ()
# GlLight.light 3 (`position (-2.0, 0.0, 0.0, 0.0))
  (* 3番のライトを左方に設置 *) ;;
- : unit = ()
# Gl.enable `light3 (* 3番のライトを有効化 *) ;;
- : unit = ()

 Gl.enableは,OpenGLの様々な機能を有効にする関数だ。ここでは引数`lightningに適用してライト効果を有効にしたり,引数`light1や`light2などに適用して個々のライトを有効にしている。

 また,GlLight.lightは,ライトの番号を指定し,その性質を設定する関数である。`ambientは環境光(およびその色)を,`diffuseは点光源(およびその色)を,`positionは光源の位置を表す。

OCamlの「多相バリアント」

 `lightningのように「`」(バッククォート)のついたOCamlの値は多相バリアントと呼ばれる。多相バリアントは,複数のケースから一つのケースを選択するような状況で使用する。

 例えば,Gl.enableの引数としては,ライト効果を有効にする`lightingや,個々のライトを有効にする`light1,`light2,`light3といったもの以外に,2次元テクスチャ効果を有効にする`texture_2d,ポリゴンのアンチエイリアシング効果を有効にする`polygon_smoothなど,多くのケースがある。

 多相バリアントは,C言語のマクロ定数やenum型の値と異なり,あらかじめ宣言する必要がなく,整数と混同されるおそれもない。また,場合分け(想定・列挙される処理)の漏れがないことも,コンパイル時に検査される。試しにGl.enableが処理できないようなケースを引数として与えてみると,確かに型エラーになる。

# Gl.enable `hoge (* Gl.enableが想定していない多相バリアント`hoge *) ;;
Characters 10-15:
  Gl.enable `hoge ;;
            ^^^^^
This expression has type [> `hoge ] but is here used with type Gl.cap

 多相バリアントは,先の`ambient (1.0, 1.0, 1.0, 1.0)や`diffuse (1.0, 0.0, 0.0, 1.0)のように,パラメータを取ることもできる。パラメータの数や型は,多相バリアントの型の一部としてチェックされる。したがって,もしパラメータの数や型を間違えたら,やはりコンパイル時に型エラーになる。

# GlLight.light 1 (`ambient (255, 255, 255))
  (* `ambientのパラメータとして四つのfloatが必要だが誤って三つのintを与えている *) ;;
Characters 16-42:
  GlLight.light 1 (`ambient (255, 255, 255))
  (* `ambientのパラメータとして四つのfloatが必要だが誤って三つのintを与えている *) ;;
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^
This expression has type [> `ambient of int * int * int ]
but is here used with type
  GlLight.light_param =
    [ `ambient of Gl.rgba
~ 略 ~
    | `spot_exponent of float ]
Type int * int * int is not compatible with type
  Gl.rgba = float * float * float * float 
Types for tag `ambient are incompatible

 なお,ライトなどのパラメータの詳細な意味は,OCamlやLablGLに固有の事項ではないので,この記事では省略する。詳しく知りたい方は,OpenGLのサイトや書籍を参照してほしい。OpenGLのリファレンス・マニュアルプログラミング・ガイドGLUTの仕様なども参考になるだろう。

 OpenGLの機能とLablGLの対応については,残念ながらLablGLのREADMEファイルぐらいしか文書が存在しないが,型や関数の名前から明らかなことも多い。LablGLの型や関数の一覧は,「ocamlbrowser -I +lablGL」を実行して,名前が「Gl」で始まるモジュールを見ればよい(ただし,ocamlbrowserの実行には,前述のTkライブラリが必要)。

ティーポットの描画と回転

 さて,光源を設定したら,いよいよ立体の描画だ。GLUTには「ティーポットを描画する」という,都合のよい関数が最初から用意されている。その関数を利用して,以下のような描画関数displayを定義・登録してみよう。

# let display () = (* 特に意味のないunit型の引数()を受け取る。C言語のvoidに相当 *)
    GlClear.color (0.0, 0.0, 0.0); (* 画面をクリアするときの色を黒に設定 *)
    GlClear.clear [`color]; (* 画面をクリア *)
    Glut.solidTeapot 0.5; (* サイズ0.5のティーポットを描画 *)
    Gl.flush () (* バッファをフラッシュ *) ;;
val display : unit -> unit = <fun>
# Glut.displayFunc display (* 描画関数displayを登録 *) ;;
- : unit = ()

 これだけでも描画はできるのだが,せっかくの3Dグラフィックスなので,少しくらいは動きを入れたい。そこで,キーボードから入力があったら回転する,という関数も定義・登録しよう。

# let keyboard ~key:k ~x:x ~y:y = (* 入力されたキーkとマウスの座標x,yを引数とする *)
    if k = int_of_char 'q' then exit 0; (* 入力された文字がqだったら終了 *)
    GlMat.rotate ~angle:2.0 ~x:0.25 ~y:0.5 ~z:1.0 (); (* そうでなければ回転 *)
    display () (* 描画関数displayを呼び出して,全画面を再描画 *) ;;
val keyboard : key:int -> x:'a -> y:'b -> unit = <fun>
# Glut.keyboardFunc keyboard (* キーボード処理関数keyboardを登録 *) ;;
- : unit = ()

 最後に,GLUTのメイン・ループを呼び出すと,ウィンドウが表示される。そのウィンドウでスペースキーなどを押せば,実際にティーポットが回転するはずだ。

# Glut.mainLoop () (* この関数は呼び出すと返ってこない *) ;;

 qキーを押せば,lablglut全体が終了する。また,lablglutでCtrl-cを押せば,lablglut自体は終了せず,GLUTのメイン・ループだけが一時中断する(Windowsの場合は,lablglutを実行しているコマンドプロンプトでCtrl-cを押してから,ティーポットが表示されているウィンドウでスペースキーなどを押す)。

# Glut.mainLoop () (* EnterキーのあとにCtrl-cを押す *) ;;
Interrupted.
# Glut.mainLoop () (* GLUTのメインループを再び呼び出すこともできる *) ;;

OCamlの「ラベル付き引数」

 先のkeyboard関数では,引数として単に「k」「x」「y」ではなく,「~key:k」「~x:x」「~y:y」を指定していた。回転関数GlMat.rotateの引数「~angle:2.0」「~x:0.25」なども同様だ。これらはラベル付き引数と呼ばれる,OCamlの機能の一つだ。

 OpenGLのような大きなライブラリでは,一つの関数に多くのintやfloat(C言語ではdouble)の引数があるため,どの引数が何なのか,呼び出す側からわかりにくい。例えば,GlMat.rotate(C言語ではglRotatedないしglRotatef)だったら,角度(angle)が先なのか,ベクトルの要素(x,y,z)が先なのかがわからない。もしラベル付き引数がなかったら,マニュアルを参照するか,順番を暗記しておくしかないない。

 OCamlでは,「~key」「~x」などのラベルをつけることにより,任意の順番で引数を指定できる。例えば,先の

GlMat.rotate ~angle:2.0 ~x:0.25 ~y:0.5 ~z:1.0 ();

は,

GlMat.rotate ~x:0.25 ~y:0.5 ~z:1.0 ~angle:2.0 ();

と書いても,全く構わない。必要であれば,一部の引数だけ先に与えて,他の引数はあとから指定することもできる。その場合も引数の順番は自由だ。

# let my_rotate = GlMat.rotate ~x:0.25 ~y:0.5 ~z:1.0 (* x,y,zを先に与える *) ;;
val my_rotate : angle:float -> unit -> unit = <fun>
# my_rotate ~angle:2.0 (* 後からangleを与える *) ;;
- : unit -> unit = <fun>
# let my_rotate2 = GlMat.rotate ~angle:2.0 (* angleを先に与える *) ;;
val my_rotate2 : ?x:float -> ?y:float -> ?z:float -> unit -> unit = <fun>
# my_rotate2 ~x:0.25 ~y:0.5 ~z:1.0 (* 後からx,y,zを与える *) ;;
- : unit -> unit = <fun>

 なお,上の例で,ラベルについている「?」(クエスチョン・マーク)は,C++のデフォルト引数のように,デフォルト値があって省略できる引数を表している。ラベル付き引数や多相バリアントの詳細については,OCamlマニュアルの第1部第4章や,ディディエ・レミー氏によるOCamlチュートリアルの付録Bを参照してほしい。

 ラベル付き引数や多相バリアントは,OCamlの特徴的な機能だ。同じ関数型言語であるHaskellやStandard MLは,少なくとも標準で組み込まれている機能としては,こうした機能を持っていない。OpenGLのようにとかく複雑になりがちなライブラリのインタフェースを明快にして,プログラミングを簡単にする効果は大きいと思う。

著者紹介 住井 英二郎

 東北大学 大学院 情報科学研究科 助教授。今回はライブラリを紹介しつつOCamlの機能も解説しました。ひょっとしたらOpenGLや3Dグラフィックスについては,お恥ずかしい間違いや不正確なところがあったかもしれません。その際はコメントなどをいただければ幸いです。