前回まで難しい話題が続いてしまったので,今回は少し気楽な話として,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グラフィックスについては,お恥ずかしい間違いや不正確なところがあったかもしれません。その際はコメントなどをいただければ幸いです。 |