この文書は学生実験のテーマ「VR実験」の参考資料の、 GLUT を用いた OpenGL のチュートリアルです。 180 分× 2 日+αで実験部分に到達できると思います。 ただし内容は不十分なので、 必要に応じて資料やオンラインマニュ アル等を参照してください。 また間違いも含まれていると思います。 コメントをお願いします。
このページをつくった本人は予想していなかったのですが、 もし、このページを授業等でご利用頂いているのであれば、 その時にネットワークの到達性に問題が出ないとも限りませんので、 必要に応じてこのページをローカルサーバにコピーしてお使いください。 その際、ご連絡等は不要です。 また内容も必要に応じて書き換えて頂いて結構ですし、 著者名等も外して頂いて結構です。 このディレクトリをまとめたものを ここ に用意してあります。
初版 1997/09/30, 最終更新
目次
- 資料:
- 今までにあった質問
- 以前に書いた AUX ライブラリ版
- 柴山健伸先生の混沌としたサンプル
- 陳謙先生の Motif を使ったサンプル
- 中山礼児氏の Delphi についての解説
- The OpenGL WEB Site(OpenGL の総本山)
- About GLUT(OpenGL.org の GLUT のページ)
- Coding & Tutorials(OpenGL.org のチュートリアルリンク集)
- OpenGL Technical FAQ(OpenGL について良く聞かれる質問)
- OpenGL FAQ 日本語(OpenGL について良く聞かれる質問の日本語版)
- GLUT Programming Interface API Version 3(GLUT のマニュアル)
- GLUT ガイド日本語版 (GLUT Programming Interface API Version 3 の訳)
- GLUT FAQ (GLUT について良く聞かれる質問)
- OpenGL プログラミングコース(これが最初からあれば苦労しなかったのに)
- OpenGL 入門講座(関数の解説が丁寧)
- 数学と計算(OpenGL のほか、C/C++、LaTeX 等豊富なチュートリアルあり)
OpenGL はシリコングラフィックス社(以下 SGI)が開発した、 OS に依存しない3次元のグラフィックスライブラリ (API) です。 でも、この「OS に依存しない」というところが実は曲者で、 ウィンドウを開いたりウィンドウマネージャと通信したりするところは、 ちゃんとそれぞれの流儀に則って、 OS やウィンドウシステムにお願いしないといけません。 すなわち、OpenGL の機能が使えるように、 Windows なら Windows のやり方で、 X なら X のやり方で、 お膳立てをしてやる必要があるのです。
実はこれが結構面倒な作業なので、 教科書の OpenGL Programming Guide の第1版 では、 補助ライブラリ(AUX ライブラリ、一種の toolkit)というのを導入して、 その部分をとりあえず隠していました。 つまり、AUX ライブラリに OS に依存する処理を任せることで、 読者は OpenGL そのものの学習に専念できるようになっていたのです。
OpenGL Programming Guide の第2版では、AUX ライブラリに代えて GLUT を使うようになりました。
ところで、Microsoft 社(以下 MS)が SGI から OpenGL のライセンスを買って自分のところの OS に載っけたので、 OpenGL は一気にグラフィックスライブラリの“業界標準”の地位に登り詰めました。 その際、この AUX ライブラリも Windows (NT/95) に移植されました。 この結果、図らずも?この AUX ライブラリを使って書いたソースプログラムは、 UNIX と Windows のどちらでもコンパイルできるという 便利な仕組みができ上がりました。
しかし、AUX ライブラリはもともと学習用であり、 ちゃんとしたアプリケーションを書こうとすると機能に不足を感じます。 それに MS による AUX ライブラリの移植はやはり MS の流儀で行われていて、 例えば、イベントのハンドラには CALLBACK という型を付けないといけないとか、 やっぱり気色の悪い部分があったりします。
そこで AUX ライブラリを、 多少なりともまともなアプリケーションが作れるように改良したものが GLUT と言えます。 これは SGI の Mark Kilgard によって作成されました (今は nVIDIA に居るみたいですけど)。 またユタ大学の Nate Robins(この人も今は nVIDIA に居るのかも)という人によって、 Windows にも移植されました。 このため GLUT には AUX ライブラリのような問題?はありません。 バージョン 3.6 以降では Windows 版と UNIX 版のソースコードが統合され、 まとめて提供されています。
Macintosh でも Mesa の上に AUX ライブラリや GLUT が移植されています。また Apple 自身もついに? OpenGL を採用し、この上でも GLUT は使用可能です。 MacOS X の Developer Tools には標準で GLUT が含まれています。
なお、GLUT には C/C++ 用の他、Fortran や Ada 用のインタフェースライブラリも用意されています。
シミュレーション結果の視覚化など、 グラフィックスを専門としない人が グラフィックスプログラミングをしなければならないということは結構ありますよね。 かつて(いつの話だ?)は Calcomp のプロッタライブラリとか、 Tektronix 4014 ターミナルのエスケープシーケンスとか、 あるいは N88BASIC のグラフィックス(GLIO 呼び出しとか) なんかがそういう目的に使われてたと思います。
今ならそういう目的には何を使います? Windows なら GDI で描きますか? やはり DirectX でしょうか? X なら Xlib? それとも PEX? こういうのは、使ったことがある人はわかると思いますが、 実際に絵を描き始めるまでに なんか訳の分からない呪文をいっぱい並べないといけなくて、 結構煩わしいもんですよね。 特にグラフィックスとなると…
本格的な GUI (Graphical User Interface) を持ったアプリケーションプログラムを作りたいのなら、 Windows なら素直に Visual BASIC や Visual C++ に付いている MFC (Microsoft Foundation Class) を使うべきでしょうし、 X Window なら Motif などの toolkit を使えば、見栄えのいいものができるでしょう。 しかしこれらはあくまで 「ユーザーインタフェース構築のためのフレームワーク」 であって、「グラフィックスプログラミング」そのものには、 あんまり役に立ちそうにありません。
OpenGL は3次元のグラフィックスライブラリですが、 もちろん2次元の機能も持っています。 なにより、これを使うと N88BASIC の LINE 文で図形を書いていた頃の気楽さで グラフィックスプログラミングができます(あくまで個人的な印象です)。 それでいて、(当たり前だけど) N88BASIC とは比較にならないほどいろんなことができます。
ということで、GLUT と OpenGL を組み合わせれば、
という三拍子そろったメリットが得られます。
もちろん GLUT は、 本格的な GUI を持ったプログラムの開発には向きません。 しかし研究などで、 手早くグラフィックスのプログラムを仕上げないといけないという場合には、 とても便利な組み合わせだと思います。
なお GUI については、GLUT 自身に一応 MUI - micro-UI という簡単なユーザインタフェース作成用のツールキットが付いています。 また GLUI という C++ で書かれたツールキットもリリースされているようです。 これについては、 GLUIによるOpenGLダイアログ に日本語で書かれた分かりやすいチュートリアルがあります。 その他のツールキットについては、 http://www.vogue.is.uec.ac.jp/~zetaka/Public/hobby/opengl/index.html に詳しくまとめられています。
IRIX 用の GLUT は SGI の freeware のページにあります。 また Windows (95/98/98SE/Me/NT/2000/XP) 版の最新版は http://www.xmission.com/~nate/glut.html から得られます。 このほか、IRIX なら /usr/share/src/OpenGL/toolkits/libglut にも GLUT のソースプログラムがあります。 ただし IRIX 6.2 の場合、これは Version 2 のものでした。
IRIX 用のバイナリ (glut_dev.tardist) は、 IRIX の Web ブラウザでダウンロードするとインストーラ (swmgr) が自動的に起動します。tardist ファイルを取ってきて tardist コマンドでインストールすることもできます。
% tardist glut_dev.tardist
これで swmgr が起動します。 swmgr を使えば、 システム標準のヘッダファイル/ライブラリのパスにインストールできます。 ただし、 それには root の権限が必要です。
tardist ファイルは tar で展開できます。その場合は “swmgr -f 展開したディレクトリ”あるいは “inst -f 展開したディレクトリ”を実行してください。
IRIX 以外の UNIX や IRIX で root の権限がないときは、 GLUT Specification のページからソースファイル (glut-3.7.tar.gz と glut_data-3.7.tar.gz) を取ってきてコンパイルしてください。 その際は glut-3.7/lib/glut に cd して make したほうがいいでしょう。 glut-3.7 で make すると サンプルプログラムから何からコンパイルするので、 すごく時間がかかります (非常に参考になるサンプルプログラムなので、 目を通しておくことを勧めます)。
% gunzip -d -c glut-3.7.tar.gz | tar xf - % cd glut-3.7/lib/glut % make
でき上がった glut-3.7/lib/glut/libglut.a と glut-3.7/include/GL/glut.h を、 適当なディレクトリに移動します。 例えばホームディレクトリ直下に GLUT というディレクトリを作り、 ~/GLUT/GL/glut.h と~/GLUT/libglut.a として置けばいいでしょう。
% mkdir ~/GLUT % cp libglut.a ~/GLUT % mkdir ~/GLUT/GL % cp ../../include/GL/glut.h ~/GLUT/GL
OpenGL が移植されていない UNIX や Macintosh では、 Mesa と呼ばれる OpenGL 互換の無料のライブラリが使用できます。 これは http://www.mesa3d.org/ から入手できます。
SGI から Mark Kilgard を含む大量のエンジニアが nVIDIA に移籍したと思ったら、 SGI が GLX (X Window の OpenGL 拡張)をオープンソース化して、 XFree86 でも GLX が使えるようになりました。 現在 nVIDIA は RIVA128 / RIVA128ZX / RIVA TNT / RIVA TNT2 / Vanta それに GeForce 256 / GeForce2 GTS/Ultra/MX / GeForce3 用に、ハードウェアアクセラレーションが効く XFree86 4.x 用のドライバを用意しています。 その他のカードについては、 XFree86 4.x では DRI (Direct Rendering Infrastracture) のページを参照してください。 この日本語の解説が XFree86 4.0(DRI)のインストール にあります。また 3.3.6 あたりについては Utah-GLX のページを参照してください。 この日本語の解説が(上と同じ方が書かれた) Utah-GLX のインストールにあります。 ところで、ついに SGI が OpenGL のサンプルインプリメンテーションをオープンソース化してしまいました。
OpenGL が使えるのは、OpenGL の DLL をインストールした Windows 95 および Windows 98/98SE/Me、Windows NT 3.5/4.0/2000、そして Windows XP です。 Windows 95 の場合、 OSR2 以降なら多分標準で入っているのではないかと思いますが、 無い場合は MS からダウンロードしてきてください。 これは ftp://ftp.microsoft.com/softlib/MSLFILES/opengl95.exe にあります。
開発環境には Visual C++ 5.0 を想定しています。 Windows 用のバイナリ (glutdlls.zip) を展開した後、 各ファイルを以下のように配置してください。
README.win には glutwin32.mak を使うとか書いてありますが、 これは glutdlls.zip には入っていません (ソースファイル glut-3.7.tar.gz あるいは glut37.zip には入っています)。 *.lib、*.dll には、 ファイル名に 32 の付いたものと付いていないものがあります。 ファイル名に 32 が付いているものは、 opengl32.dll すなわち MS 版の OpenGL に対応しており、 付いていないものは SGI 版の OpenGL に対応しています。
なお、C++ Builder の場合は“えむっち”さんの
へっぽこプログラマー日記が参考になります。
Cygwin の場合は
cygwin を使って OpenGL を使ったプログラムを書く話
で詳しく解説されています。また、フリーの処理系の
LCC-Win32
というのも使えるそうです(
Using GLUT with LCC-Win32、山本秀一先生ありがとう)。
LCC-Win の使い方については
無料(タダ)で始める!!
に分かりやすい解説があります。
この他、埼玉大学の櫻井先生が OpenGL と GLUT を Windows9*/NT で使う方法について
OpenGL の部屋
に詳しくおまとめになっています。
GLUT を swmgr でインストールした場合は、 cc コマンドに以下のようなオプションを付けてください。
% cc program.c -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm
swmgr を使わずにソースをコンパイルした場合は、 上のコマンドで glut.h と libglut.a を置いた場所を 指定してください。 これらをホームディレクトリ直下の GLUT にインストールした場合は -I ~/GLUT -L ~/GLUT を追加してください。 また Linux の場合はたいてい -L/usr/X11R6/lib を追加するといいようです。
コンパイルの度にこんなに長いコマンドを打つのは面倒ですから、 楽をする方法を考えましょう。これにはいくつか方法が考えられます。
% alias ccgl 'cc \!* -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm' |
% ccgl program.c |
#!/bin/sh exec cc "$@" -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm |
% chmod +x ccgl |
% ccgl program.c |
LIBS = -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm a.out: program.c --Tab-->cc program.c $(LIBS) |
% make |
Makefile にはファイルの「生成規則」を記述します。 make は実行すると、Makefile 中の最初の生成規則を探します。 上のファイルの場合、a.out の行がそれになります。 この行には a.out というターゲットを生成するのに program.c が必要だという依存関係を記述しており、 その次の行に実際に a.out を生成するための手続きを記述しています (注)。 この行の行頭は Tab 文字にしてください。
ターゲットが複数あるときは以下のようにします。
LIBS = -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm all: prog1 prog2 prog1: prog1.c --Tab-->cc prog1.c -o prog1 $(LIBS) prog2: prog2.c --Tab-->cc prog2.c -o prog2 $(LIBS)
この最初の生成規則は all の行で、all を生成するには prog1 と prog2 が必要だという依存関係を記述しています。 しかし all の生成方法は記述していないので、make は prog1 と prog2 の両方の生成だけが完了した時点で終了します。 特定のターゲットだけを生成したいときは、 そのターゲット名を make の引数に指定します。
% make prog1
まず、新規作成で 「Win32 Console Application」のプロジェクト を作成してください。
以降で示すプログラムは、UNIX (IRIX) 上での実行を前提に作成しています。 Windows 上では「Win32 Console Application」のプロジェクトにすることで、 これらのプログラムをそのまま VC++ でコンパイルできるようになります。 コンソールウィンドウを開きたくない場合は GLUT FAQ の Q36 を参考にしてください。
GLUT 3.7.2 以降と Visual C++ 6.0 の組合わせなら、ソースファイルに GL/glut.h が include していれば自動的に glut32.lib glu32.lib opengl32.lib を組み込んでくれます(山下真さんありがとう)。 したがって、普通にビルドすれば実行ファイルができあがるはずです。
もし、うまく行かないようなら、 プロジェクトの設定 (Alt+F7) のリンクのタブで、 オブジェクト/ライブラリモジュールに glut32.lib glu32.lib opengl32.lib の3つを追加してください。 SGI の OpenGL を使用する場合は、 代わりに glut.lib glu.lib opengl.lib を追加してください。 あと、C/C++ のタブでプリプロセッサの定義に WIN32 があることを確かめてください(無いことは無いと思いますが)。 もしなければ追加してください。
いよいよプログラムの作成に入ります。 ウィンドウを開くだけのプログラムは、 GLUT を使うとこんな風になります。 このソースプログラムを prog1.c とというファイル名で作成し、 コンパイルして出来上がった実行プログラム (a.out) を実行してみてください。
#include <GL/glut.h> void display(void) { } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutMainLoop(); return 0; }
- void glutInit(int *argcp, char **argv)
- GLUT および OpenGL 環境を初期化します。 引数には main の引数をそのまま渡します。 X Window で使われるオプション -display などはここで処理されます。 この関数によって引数の内容が変更されます。 プログラム自身で処理すべき引数があるときは、 この後で処理します。
- int glutCreateWindow(char *name)
- ウィンドウを開きます。 引数 name はそのウィンドウの名前の文字列で、 タイトルバーなどに表示されます。 以降の OpenGL による図形の描画等は、 開いたウィンドウに対して行われます。 なお、戻り値は開いたウィンドウの識別子です。
- void glutDisplayFunc(void (*func)(void))
- 引数 func は開いたウィンドウ内に描画する関数へのポインタです。 ウィンドウが開かれたり、 他のウィンドウによって隠されたウィンドウが再び現れたりして、 ウィンドウを再描画する必要があるときに、 この関数が実行されます。 したがって、この関数内で図形表示を行います。
- void glutMainLoop(void)
- これは無限ループです。 この関数を呼び出すことで、 プログラムはイベントの待ち受け状態になります。
見れば分かる通り、プログラムは、
という順になります。 C 言語の教科書なんかに良く出てくる 「標準入出力を使ったプログラム」なんかと違うところは、 中心となる処理(この場合 display())を実行するタイミングが、 ソースプログラムを見ただけでは何時なのかわからない、 というところでしょうか。
最初に display() が実行されるのは、 初めてウィンドウが開いたとき、すなわち、 glutMainLoop() が glutCreateWindow() の指示を受けてウィンドウの生成を完了したときになります。 また、その後も、 このウィンドウがほかのウィンドウに隠され再び現れたときのように、 ウィンドウの再描画が必要になったときに実行されます。
なお上のプログラムでは display() の中身に何も記述していないため、 display() が呼び出されても何も仕事をしません。 試しにこのウィンドウを移動したり、 他のウィンドウで隠したりしてみてください。 ウィンドウの中の表示はおかしなものになっていると思います。
このように複数の (オーバーラップ可能な) ウィンドウが使用できるウィンドウシステムに対応したプログラムでは、 処理の流れは時間軸に沿って「プログラムの始めから終りへ」ではなく、 何かこと(事象)が起るたびに「プログラムの各部がランダムに」実行されます。 従って、そのプログラミングスタイルも、 「事象」に対して、その「対処方法」を登録していくというものになります。 ここではこの事象をイベントと呼び、 対処方法の手続きをハンドラと呼ぶことにします。
なお、このプログラムには「終了する方法」を組み込んでいないので、 プログラムを終了するには実行したウィンドウで Ctrl-C をタイプするか、 ウィンドウのタイトルバーの左のボタンをクリックして 「閉じる」か「中止」を選んでください。
今までは関数 display() の中に何も記述していなかったので、 ウィンドウの中身はでたらめ (おそらく、そのウィンドウの位置に以前に描かれていた内容の残骸) だと思います。 そこで、今度は開いたウィンドウを塗りつぶしてみます。 prog1.c に太字のところを追加し、 もう一度コンパイルしてプログラムを実行してみてください。
#include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); glFlush(); } void init(void) { glClearColor(0.0, 0.0, 1.0, 0.0); } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); init(); glutMainLoop(); return 0; }
- void glutInitDisplayMode(unsigned int mode)
- ディスプレイの表示モードを設定します。 mode に GLUT_RGBA を指定した場合は、 色の指定を RGB(赤緑青、光の3原色)で行えるようにします。 他にインデックスカラーモード (GLUT_INDEX) も指定できます。 後者はうまく使えば効率の良い表示が行えますが、 それなりに面倒なので、 ここではお任せで使える RGBA モードを使います。
- void glClearColor(GLclampf R, GLclampf G, GLclampf B, GLclampf A)
- glClear(GL_COLOR_BUFFER_BIT) でウィンドウを塗りつぶす際の色を指定します。 R,G,B はそれぞれ赤、緑、青色の成分の強さを示す GLclampf 型 (float 型と等価)の値で、 0〜1 の間の値を持ちます。 1 が最も明るく、この3つに (0, 0, 0) を指定すれば黒色、(1, 1, 1) を指定すれば白色になります。 上の例ではウィンドウは青色で塗りつぶされます。 最後の A はα値と呼ばれ、OpenGL では不透明度として扱われます (0 で透明、1 で不透明)。ここではとりあえず 0 にしておいてください。
- void glClear(GLbitfield mask)
- ウィンドウを塗りつぶします。 mask には塗りつぶすバッファを指定します。 OpenGL が管理する画面上のバッファ(メモリ)には、 色を格納するカラーバッファの他、 隠面消去に使うデプスバッファ、 凝ったことをするときに使うステンシルバッファ、 カラーバッファの上に重ねて表示されるオーバーレイバッファなど、 いくつかのものがあり、 これらが一つのウィンドウに重なって存在しています。 mask に GL_COLOR_BUFFER_BIT を指定したときは、 カラーバッファだけが塗りつぶされます。
- glFlush(void)
- glFlush() はまだ実行されていない OpenGL の命令を全部実行します。 OpenGL は関数呼び出しによって生成される OpenGL の命令をその都度実行するのではなく、 いくつか溜め込んでおいてまとめて実行します。 このため、ある程度命令が溜まらないと 関数を呼び出しても実行が開始されない場合があります。 glFlush() はそういう状況で まだ実行されていない残りの命令の実行を開始します。 ひんぱんに glFlush() を呼び出すと、かえって描画速度が低下します。
glClearColor() は、 プログラムの実行中に背景色を変更することがなければ、 最初に一度だけ設定すれば十分です。 そこでこのような初期化処理を行う関数は、 glMainLoop() の前に実行する関数 init() にまとめて置くことにします。
glFlush() のかわりに glFinish() を使う場合もあります。これは、 glFlush() がまだ実行されていない OpenGL の命令の実行開始を促すのに加えて、 glFinish() はそれがすべて完了するのを待ちます。
gl*() で始まる(glu*() や glut*() で始まらない)関数が、 OpenGL の API です。
ウィンドウ内に線を引いてみます。 prog1.c を以下のように変更し、 コンパイルしてプログラムを実行してください。
#include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); glBegin(GL_LINE_LOOP); glVertex2d(-0.9, -0.9); glVertex2d(0.9, -0.9); glVertex2d(0.9, 0.9); glVertex2d(-0.9, 0.9); glEnd(); glFlush(); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
- void glBegin(GLnum mode)
- void glEnd(void)
- 図形を描くには、 glBegin()〜glEnd() の間にその図形の各頂点の座標値を設定する関数を置きます。 glBegin() の引数 mode には描画する図形のタイプを指定します。
- void glVertex2d(GLdouble x, GLdouble y)
- glVertex2d() は2次元の座標値を設定するのに使います。 引数の型は GLdouble (double と等価)です。 引数が float 型のときは glVertex2f()、 int 型のときは glVertex2i() を使います。
glBegin() の引数 mode に指定できる図形のタイプには以下のようなものがあります。 詳しくは man glBegin を参照してください。
- GL_POINTS
- 点を打ちます。
- GL_LINES
- 2点を対にして、その間を直線で結びます。
- GL_LINE_STRIP
- 折れ線を描きます。
- GL_LINE_LOOP
- 折れ線を描きます。始点と終点の間も結ばれます。
- GL_TRIANGLES / GL_QUADS
- 3/4点を組にして、三角形/四角形を描きます。
- GL_TRIANGLE_STRIP / GL_QUAD_STRIP
- 一辺を共有しながら帯状に三角形/四角形を描きます。
- GL_TRIANGLE_FAN
- 一辺を共有しながら扇状に三角形を描きます。
- GL_POLYGON
- 凸多角形を描きます。
OpenGL を処理するハードウェアは、 実際には3角形しか塗り潰すことができません (モノによっては4角形もできるものもあります)。 このため GL_POLYGON の場合は、 多角形を3角形に分割してから処理します。 従って、もし描画速度が重要なら GL_TRIANGLE_STRIP や GL_TRIANGLE_FAN を使うよう プログラムを工夫してみてください。 また GL_QUADS も GL_POLYGON より高速です。
線に色を付けてみます。 prog1.c を以下のように変更し、コンパイルしてください。 プログラムを実行したら線は何色で表示されたでしょうか?
#include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3d(1.0, 0.0, 0.0); glBegin(GL_LINE_LOOP); glVertex2d(-0.9, -0.9); glVertex2d(0.9, -0.9); glVertex2d(0.9, 0.9); glVertex2d(-0.9, 0.9); glEnd(); glFlush(); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
- void glColor3d(GLdouble r, GLdouble g, GLdouble b)
- glColor3d() はこれから描画するものの色を指定します。 引数の型は GLdouble 型(double と等価)で、 r,g,b にはそれぞれ赤、緑、青の強さを 0〜1 の範囲で指定します。 引数が float 型のときは glColor3f()、 int 型のときは glColor3i() を使います。
図形を塗りつぶしてみます。 GL_LINE_LOOP を GL_POLYGON に変更し、 ついでに背景も白色に変更しましょう。 変更したプログラムをコンパイルして実行してください。
#include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3d(1.0, 0.0, 0.0); glBegin(GL_POLYGON); glVertex2d(-0.9, -0.9); glVertex2d(0.9, -0.9); glVertex2d(0.9, 0.9); glVertex2d(-0.9, 0.9); glEnd(); glFlush(); } void init(void) { glClearColor(1.0, 1.0, 1.0, 0.0); } int main(int argc, char *argv[]) { /* 変更なし */ }
色は頂点毎に指定することもできます。 prog1.c を以下のように変更してください。 コンパイルしてプログラムを実行すると、 どういう色の付き方になったでしょうか?
#include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3d(1.0, 0.0, 0.0); glBegin(GL_POLYGON); glColor3d(1.0, 0.0, 0.0); /* 赤 */ glVertex2d(-0.9, -0.9); glColor3d(0.0, 1.0, 0.0); /* 緑 */ glVertex2d(0.9, -0.9); glColor3d(0.0, 0.0, 1.0); /* 青 */ glVertex2d(0.9, 0.9); glColor3d(1.0, 1.0, 0.0); /* 黄 */ glVertex2d(-0.9, 0.9); glEnd(); glFlush(); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
多分、 多角形の内部は頂点の色から補間した色で塗りつぶされたと思います。 このプログラムは後で使用するので、 prog2.c というコピーを作っておいてください。
% cp prog1.c prog2.c
glVertex*() や glColor*() のような関数の * の部分は、 引数の型や数などを示しています。 詳しくは man glVertex2d や man glColor3d を参照してください。
ウィンドウ内に表示する図形の座標軸は、 そのウィンドウ自体の大きさと図形表示を行う“空間”との関係で決定します。 開いたウィンドウの位置や大きさはマウスを使って変更することができますが、 その情報はウィンドウマネージャを通じて、 イベントとしてプログラムに伝えられます。
これまでのプログラムでは、 ウィンドウのサイズを変更すると表示内容もそれにつれて拡大縮小していました。 これを表示内容の大きを変えずに表示領域のみを広げるようにします。
prog1.c に以下のように resize() という関数を追加し、 glutReshapeFunc() を使って それをウィンドウのリサイズ(拡大縮小)のイベントに対するハンドラに指定します。 プログラムが変更できたらコンパイルしてプログラムを実行し、 開いたウィンドウを拡大縮小してみてください。
#include <GL/glut.h> void display(void) { /* 変更なし */ } void resize(int w, int h) { /* ウィンドウ全体をビューポートにする */ glViewport(0, 0, w, h); /* 変換行列の初期化 */ glLoadIdentity(); /* スクリーン上の表示領域をビューポートの大きさに比例させる */ glOrtho(-w / 200.0, w / 200.0, -h / 200.0, h / 200.0, -1.0, 1.0); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); init(); glutMainLoop(); return 0; }
- void glViewport(GLint x, GLint y, GLsizei w, GLsizei h)
- ビューポートを設定します。 ビューポートとは、開いたウィンドウの中で描画が行われる領域で、 正規化デバイス座標系の2点 (-1, -1), (1, 1) を結ぶ線分を対角線とする矩形領域がここに表示されます。 最初の2つの引数 x、y にはその領域の左下隅の位置、 w には幅、h には高さをデバイス座標系、 すなわちディスプレ以上の画素数で指定します。 関数 resize() の引数 w、h にはそれぞれウィンドウの幅と高さが入っていますから、 glViewport(0, 0, w, h) はリサイズ後のウィンドウの全面を表示領域に使うことになります。
- void glLoadIdentity(void)
- これは変換行列を初期化します。 座標変換の合成は行列の積であらわされますから、 変換行列に初期値として単位行列を設定します。
- void glOrtho(GLdouble l, GLdouble r, GLdouble b, GLdouble t, GLdouble n, GLdouble f)
- glOrtho() はワールド座標系を正規化デバイス座標系に平行投影 (orthographic projection : 正射影) する行列を変換行列に乗じます。 引数には左から、 l に表示領域の左端 (left) の位置、 r に右端 (right) の位置、 b に下端 (bottom) の位置、 t に上端 (top) の位置、 n に前方面 (near) の位置、 f に後方面 (far) の位置を指定します。 これは、ビューポートに表示される空間の座標軸を設定します。
- glutReshapeFunc(void (*func)(int w, int h))
- 引数 func には、 ウィンドウがリサイズされたときに実行する関数のポインタを与えます。 この関数の引数にはリサイズ後のウィンドウの幅と高さが渡されます。
resize() の処理によって、プログラムは glViewport() で指定した領域に glOrtho() で指定した領域内の図形を表示するようになります。 ここで glOrtho() で指定するの領域の大きさを resize() の引数で得られる領域の大きさに比例するように設定すれば、 図形の空間のディスプレイ上での“密度”を一定に保つことができます。
図形はワールド座標系と呼ばれる空間にあり、 その2点 (l, b), (r, t) を結ぶ線分を対角線とする矩形領域を、 2点 (-1, -1), (1, 1) を対角線とする矩形領域に投影します。 この投影された座標系を正規化デバイス座標系と呼びます。
この正規化デバイス座標系の正方形領域内の図形がデバイス座標系 (ディスプレイ上のウィンドウ)のビューポートに表示されますから、 結果的にワールド座標系から glOrtho() で指定した矩形領域を切り取ってビューポートに表示することになります。
ワールド座標系から切り取る領域は、 “CG用語的”には「ウィンドウ」と呼ばれ、 ワールド座標系から正規化デバイス座標系への変換は 「ウィンドウイング変換」と呼ばれます。 しかしウィンドウシステム(X Window, MS Windows 等)においては、 「ウィンドウ」はアプリケーションプログラムが ディスプレイ上に作成する表示領域のことを指すので、 ここの説明ではこれを「座標軸」と呼んでいます。 なお、正規化デバイス座標系からデバイス座標系への変換は ビューポート変換と呼ばれます。
glOrtho() では引数として l, r, t, b の他に n と f も指定する必要があります。 実は OpenGL は2次元図形の表示においても内部的に3次元の処理を行っており、 ワールド座標系は奥行き (Z) 方向にも軸を持つ3次元空間になっています。 n と f には、 それぞれこの空間の前方面(可視範囲の手前側の限界) と後方面(可視範囲の遠方の限界)を指定します。 n より手前にある面や f より遠方にある面は表示されません。
2次元図形は奥行き (Z) 方向が 0 の3次元図形として取り扱われるので、 ここでは n(前方面、可視範囲の手前の位置)を -1.0、 f (後方面、遠方の位置)を 1 にしています。
glOrtho() を使用しなければ変換行列は単位行列のままなので、 ワールド座標系と正規化デバイス座標系は一致し、 ワールド座標系の2点 (-1, -1), (1, 1) を対角線とする矩形領域がビューポートに表示されます。 ビューポート内に表示する空間の座標軸が変化しないため、 この状態でウィンドウのサイズを変化させると、 それに応じて表示される図形のサイズも変わります。 初期状態はこのようになっています。
そこで上のプログラムでは、 実際のウィンドウのサイズの 1/100 をワールド座標系の矩形領域に設定します。 こうするとウィンドウのサイズの変化に比例して ワールド座標系の矩形領域の大きさが設定されるため、 ウィンドウ内に表示される図形の大きさが不変になります。
プログラムの起動時に開くウィンドウの位置やサイズを指定したいときは、 glutInitWindowPosition() および glutInitWindowSize() を使います。 これらを使用しなければ、 プログラムが起動したときに開かれるウィンドウのサイズは ウィンドウマネージャの設定に従います。 prog1.c に試しに太字の部分を追加してみてください。
#include <GL/glut.h> void display(void) { /* 変更なし */ } void resize(int w, int h) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInitWindowPosition(100, 100); glutInitWindowSize(320, 240); glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); init(); glutMainLoop(); return 0; }
- void glutInitWindowSize(int w, int h)
- 新たに開くウィンドウの幅と高さを指定します。 これを指定しないときは、300×300 のウィンドウを開きます。
- void glutInitWindowPosition(int x, int y)
- 新たに開くウィンドウの位置を指定します。 これを指定しないときは、 ウィンドウマネージャによってウィンドウを開く位置を決定します。
X Window の場合、 -geometry オプションによって コマンドラインからウィンドウを開く位置やサイズを指定できます。 これは glutInit() によって処理されるので、 -geometry オプションを有効にするには glutInitWindowPosition() と glutInitWindowSize() を glutInit() より前に置き、無効にするには後に置きます。
マウスのボタンを押したことを知るには、 glutMouseFunc() という関数で マウスのボタンを操作したときに呼び出す関数を指定します。 prog1.c を以下のように変更してください。
#include <stdio.h> #include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); /* 途中削除 */ glFlush(); } void resize(int w, int h) { /* ウィンドウ全体をビューポートにする */ glViewport(0, 0, w, h); /* 変換行列の初期化 */ glLoadIdentity(); /* 以下削除 */ } void mouse(int button, int state, int x, int y) { switch (button) { case GLUT_LEFT_BUTTON: printf("left"); break; case GLUT_MIDDLE_BUTTON: printf("middle"); break; case GLUT_RIGHT_BUTTON: printf("right"); break; default: break; } printf(" button is "); switch (state) { case GLUT_UP: printf("up"); break; case GLUT_DOWN: printf("down"); break; default: break; } printf(" at (%d, %d)\n", x, y); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInitWindowPosition(100, 100); glutInitWindowSize(320, 240); glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); glutMouseFunc(mouse); init(); glutMainLoop(); return 0; }
- glutMouseFunc(void (*func)(int button, int state, int x, int y))
- 引数 func には、 マウスのボタンが押されたときに実行する関数のポインタを与えます。 この関数の引数 button には押されたボタン (GLUT_LEFT_BUTTON, GLUT_MIDDLE_BUTTON, GLUT_RIGHT_BUTTON)、 state には「押した (GLUT_DOWN)」のか「離した (GLUT_UP)」のか、 x と y にはその位置が渡されます。
プログラムが変更できたら、コンパイルしてプログラムを実行してみてください。 開いたウィンドウの上でマウスのボタンをクリックしてみてください。
x と y に渡される座標は、 ウィンドウの左上隅を原点 (0, 0) とした画面上の画素の位置になります。 デバイス座標系とは上下が反転している ので気をつけてください。
マウスの位置をもとに図形を描く場合は、
マウスの位置からウィンドウ上の座標値を求めなければなりません。
ここではちょっと手を抜いて、
ワールド座標系がこのマウスの座標系に一致するよう
glOrtho() を設定します(右図)。
またウィンドウの上下も反転します(prog1.c の下線部)。
prog1.c を以下のように変更してください。
#include <stdio.h> #include <GL/glut.h> void display(void) { /* 変更なし */ } void resize(int w, int h) { /* ウィンドウ全体をビューポートにする */ glViewport(0, 0, w, h); /* 変換行列の初期化 */ glLoadIdentity(); /* スクリーン上の座標系をマウスの座標系に一致させる */ glOrtho(-0.5, (GLdouble)w - 0.5, (GLdouble)h - 0.5, -0.5, -1.0, 1.0); } void mouse(int button, int state, int x, int y) { static int x0, y0; switch (button) { case GLUT_LEFT_BUTTON: if (state == GLUT_UP) { /* ボタンを押した位置から離した位置まで線を引く */ glColor3d(0.0, 0.0, 0.0); glBegin(GL_LINES); glVertex2i(x0, y0); glVertex2i(x, y); glEnd(); glFlush(); } else { /* ボタンを押した位置を覚える */ x0 = x; y0 = y; } break; case GLUT_MIDDLE_BUTTON: /* 削除 */ break; case GLUT_RIGHT_BUTTON: /* 削除 */ break; default: break; } /* 以下削除 */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
- glVertex2i(GLint, GLint)
- この関数は glVertex2d() と同様に2次元の座標値を設定しますが、 引数の型が GLint 型(int 型と等価)です。
前のプログラムでは、 ウィンドウのサイズを変えたり ウインドウが他のウィンドウに隠されたあと再び表示される度に、 ウィンドウの中身が消えてしまいます。 やはり、この場合もちゃんと書き直してやる必要があるわけですが、 そのためにはそれまでに表示した内容を記憶しておかなければなりません。
mouse() が実行されたときに、 配列に現在の位置を記憶しておき、 display() が実行されたときに、それをまとめて描画するようにします。 prog1.c を以下のように変更してください。
#include <stdio.h> #include <GL/glut.h> #define MAXPOINTS 100 GLint point[MAXPOINTS][2]; /* 座標を記憶する配列 */ int pointnum = 0; /* 記憶した座標の数 */ void display(void) { int i; glClear(GL_COLOR_BUFFER_BIT); /* 記録したデータで線を描く */ if (pointnum > 1) { glColor3d(0.0, 0.0, 0.0); glBegin(GL_LINES); for (i = 0; i < pointnum; i++) { glVertex2iv(point[i]); } glEnd(); } glFlush(); } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 削除 */ switch (button) { case GLUT_LEFT_BUTTON: /* ボタンを操作した位置を記録する */ point[pointnum][0] = x; point[pointnum][1] = y; if (state == GLUT_UP) { /* ボタンを押した位置から離した位置まで線を引く */ glColor3d(0.0, 0.0, 0.0); glBegin(GL_LINES); glVertex2iv(point[pointnum - 1]); /* 一つ前は押した位置 */ glVertex2iv(point[pointnum]); /* 今の位置は離した位置 */ glEnd(); glFlush(); } else { /* 削除 */ } if (pointnum < MAXPOINTS - 1) pointnum++; break; case GLUT_MIDDLE_BUTTON: break; case GLUT_RIGHT_BUTTON: break; default: break; } } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
- glVertex2iv(const GLint *v)
- この関数は glVertex2i() と同様に2次元の座標値を設定しますが、 引数 v には2個の要素をもつ GLint 型(int と等価)の配列を指定します。 v[0] には x 座標値、v[1] には y 座標値を格納します。 この例のように、複数の点の座標を指定する場合に便利です。
マウスのボタンを押しながらマウスを動かす操作を、 ドラッグと言います。 ドラッグ中はマウスの位置を継続的に取得する必要がありますが、 glutMouseFunc() で指定するハンドラはボタンを押したときにしか実行されないので、 この目的には使用できません。
マウスを動かしたときに実行する関数を指定するには、 glutMotionfunc() または glutPassiveMotionFunc() を使用します。 glutMotionfunc() で指定した関数は、 マウスのボタンを押しながらマウスを動かしたときに実行されます。 glutPassiveMotionFunc() で指定した関数は、 マウスのボタンを押さずにマウスを動かしたときに実行されます。
前のプログラムでは、 マウスの左ボタンを押してから離すまでウィンドウには何も表示されませんでした。 これを、マウスのドラッグ中は線分をマウスに追従して描くようにします。 このような効果をラバーバンド(輪ゴム) と言います。このために glutMotionFunc() を使って、 マウスのドラッグ中にラバーバンドを表示するようにします (大川様ありがとうございました)。
#include <stdio.h> #include <GL/glut.h> #define MAXPOINTS 100 GLint point[MAXPOINTS][2]; /* 座標を記憶する配列 */ int pointnum = 0; /* 記憶した座標の数 */ int rubberband = 0; /* ラバーバンドの消去 */ void display(void) { /* 変更なし */ } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { switch (button) { case GLUT_LEFT_BUTTON: /* ボタンを操作した位置を記録する */ point[pointnum][0] = x; point[pointnum][1] = y; if (state == GLUT_UP) { /* ボタンを押した位置から離した位置まで線を引く */ glColor3d(0.0, 0.0, 0.0); glBegin(GL_LINES); glVertex2iv(point[pointnum - 1]); /* 一つ前は押した位置 */ glVertex2iv(point[pointnum]); /* 今の位置は離した位置 */ glEnd(); glFlush(); rubberband = 0; } else { } if (pointnum < MAXPOINTS) pointnum++; break; case GLUT_MIDDLE_BUTTON: break; case GLUT_RIGHT_BUTTON: break; default: break; } } void motion(int x, int y) { static GLint savepoint[2]; /* 以前のラバーバンドの端点 */ /* 論理演算機能 ON */ glEnable(GL_LOGIC_OP); glLogicOp(GL_INVERT); glBegin(GL_LINES); if (rubberband) { /* 以前のラバーバンドを消す */ glVertex2iv(point[pointnum - 1]); glVertex2iv(savepoint); } /* 新しいラバーバンドを描く */ glVertex2iv(point[pointnum - 1]); glVertex2i(x, y); glEnd(); glFlush(); /* 論理演算機能 OFF */ glLogicOp(GL_COPY); glDisable(GL_LOGIC_OP); /* 今描いたラバーバンドの端点を保存 */ savepoint[0] = x; savepoint[1] = y; rubberband = 1; } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInitWindowPosition(100, 100); glutInitWindowSize(320, 240); glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); glutMouseFunc(mouse); glutMotionFunc(motion); init(); glutMainLoop(); return 0; }
- glEnable(GLenum cap)
- 引数 cap に指定した機能を使用可能にします。 GL_LOGIC_OP(もしくは GL_COLOR_LOGIC_OP)は図形の描画の際に、 ウィンドウに既に描かれている内容と、 これから描こうとする内容の間で論理演算を行うことができるようにします。
- glDisable(GLenum cap)
- 引数 cap に指定した機能を使用不可にします。
- glLogicOp(GLenum opcode)
- 引数 opcode にはウィンドウに描かれている内容と、 これから描こうとする内容との間で行う論理演算のタイプを指定します。 GL_COPY はこれから描こうとする内容をそのままウィンドウ内に描きます。 GL_INVERT はウィンドウに描かれている内容の、 これから描こうとする図形の領域を反転します。 詳しくは man glLogicOp を参照してください。
- glutMotionFunc(void (*func)(int x, int y))
- 引数 func には、 マウスのいずれかのボタンを押しながらマウスを動かしたときに 実行する関数のポインタを与えます。 この関数の引数 x と y には、現在のマウスの位置が渡されます。 この設定を解除するには、引数に 0(ヌルポインタ)を指定します (stdio.h 等の中で定義されている記号定数 NULL を使用しても良い)。
ラバーバンドを実現する場合、 マウスを動かしたときに直前に描いたラバーバンドを消す必要があります。 また、ラバーバンドを描いたことによって ウィンドウに既に描かれていた内容が壊されてしまうので、 その部分をもう一度描き直す必要があります。 しかし、そのために画面全体を書き換えるのは、 ちょっともったいない気がします。
そこでラバーバンドを描く際には、 線を背景とは異なる色で描く代わりに、 描こうとする線上の画素の色を反転するようにします。 こうすればもう一度同じ線上の画素の色を反転することで、 そこに描かれていた以前の線が消えてウィンドウに描かれた図形が元に戻ります。 このために glLogicOp() を使用します。 なお、ラバーバンドが消えずに残ってしまう場合は、glEnable() / glDisable() において GL_LOGIC_OP の代りに GL_COLOR_LOGIC_OP を使ってみてください (陳先生ご指摘ありがとう)。
ただし、マウスのボタンを押した直後はまだラバーバンドは描かれていませんから、 そのときだけラバーバンドの消去は行わないようにしなければなりません。 このため rubberband なんていう変数を使ったちょっと泥臭いプログラムになっていますが、 我慢してください(もっとエレガントな方法もありますけど…)。
glutMotionFunc(), glutPassiveMotionFunc() で指定した関数は、 マウスの移動にともなって頻繁に実行されるので、 この関数の中で時間のかかる処理を行うと、 マウスの応答が悪くなってしまいます。 これを避ける方法は9節以降で解説します。
OpenGL のアプリケーションプログラムが開いたウィンドウには、 コンソールや xwsh のようなキーボード入力を行うことができません。 そのかわりマウスのボタン同様、 キーをタイプするごとに実行する関数を指定できます。 それには glutKeyboardFunc() を使います。
これまで作ったプログラムは、 プログラムを終了する方法を組み込んでいませんでした。 そこで q のキーや ESC キーをタイプしたときに exit() を呼び出して、プログラムが終了するようにします。 また exit() を使うために stdlib.h も include します。 prog1.c を以下のように変更してください。
#include <stdio.h> #include <stdlib.h> #include <GL/glut.h> #define MAXPOINTS 100 GLint point[MAXPOINTS][2]; /* 座標を記憶する配列 */ int pointnum = 0; /* 記憶した座標の数 */ int rubberband = 0; /* ラバーバンドの消去 */ void display(void) { /* 変更なし */ } void resize(int w, int h) { /* 変更なし */ } void motion(int x, int y) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { switch (key) { case 'q': case 'Q': case '\033': exit(0); /* '\033' は ESC の ASCII コード */ default: break; } } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInitWindowPosition(100, 100); glutInitWindowSize(320, 240); glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); glutMouseFunc(mouse); glutKeyboardFunc(keyboard); init(); glutMainLoop(); return 0; }
- glutKeyboardFunc(void (*func)(unsigned char key, int x, int y))
- 引数 func には、 キーがタイプされたときに実行する関数のポインタを与えます。 この関数の引数 key にはタイプされたキーの ASCII コードが渡されます。 また x と y にはキーがタイプされたときのマウスの位置が渡されます。
ファンクションキーのような文字キー以外のタイプを検出するときは glutSpecialFunc()、Shift や Ctrl のようなモディファイア(修飾)キーを検出するには glutGetModifiers() を使います。 使い方はいずれも man コマンドで調べてください。
これまでは2次元の図形の表示を行ってきましたが、 OpenGL の内部では実際には3次元の処理を行っています。 すなわち画面表示に対して垂直に Z 軸が伸びており、 これまではその3次元空間の XY 平面への平行投影像を表示していました。
試しに5.4節で作成したプログラム (prog2.c) において、 図形を Y 軸中心に 25 度回転してみましょう。
#include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); glColor3d(1.0, 0.0, 0.0); glRotated(25.0, 0.0, 1.0, 0.0); glBegin(GL_POLYGON); glColor3d(1.0, 0.0, 0.0); /* 赤 */ glVertex2d(-0.9, -0.9); glColor3d(0.0, 1.0, 0.0); /* 緑 */ glVertex2d(0.9, -0.9); glColor3d(0.0, 0.0, 1.0); /* 青 */ glVertex2d(0.9, 0.9); glColor3d(1.0, 1.0, 0.0); /* 黄 */ glVertex2d(-0.9, 0.9); glEnd(); glFlush(); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); init(); glutMainLoop(); return 0; }
- glRotated(GLdouble angle, GLdouble x, GLdouble y, GLdouble z)
- 変換行列に回転の行列を乗じます。 引数はいずれも GLdouble 型(double と等価)で、1つ目の引数 angle は回転角、 残りの3つの引数 x, y, z は回転軸の方向ベクトルです。 引数が float 型なら glRotatef() を使います。 原点を通らない軸で回転させたい場合は、 glTranslated() を使って一旦軸が原点を通るように図形を移動し、 回転後に元の位置に戻します。
コンパイルしたプログラムを実行して、描かれる図形を見てください。 Y 軸中心に回転しているため、 以前に比べて少し縦長になっていると思います。
このウィンドウを最小化したり他のウィンドウを重ねたりして、 再描画をさせてみましょう。再描画する度に図形の形が変わると思います。 これは変換行列に glRotated() による回転の行列が積算されるからです。 これを防ぐには描画の度に変換マトリクスを glLoadIdentity() で初期化するか、 後で述べる glPushMatrix() / glPopMatrix() を使って変換行列を保存します。
それでは、こんどは以下のような3次元の立方体を線画で描いてみましょう。 glut には glutWireCube() など、 いくつか基本的な立体を描く関数があるのですが、 ここでは自分で形状を定義してみたいと思います。
この図形は8個の点を12本の線分で結びます。 点の位置(幾何情報)と線分(位相情報)を別々にデータにします。
GLdouble vertex[][3] = { { 0.0, 0.0, 0.0 }, /* A */ { 1.0, 0.0, 0.0 }, /* B */ { 1.0, 1.0, 0.0 }, /* C */ { 0.0, 1.0, 0.0 }, /* D */ { 0.0, 0.0, 1.0 }, /* E */ { 1.0, 0.0, 1.0 }, /* F */ { 1.0, 1.0, 1.0 }, /* G */ { 0.0, 1.0, 1.0 } /* H */ }; int edge[][2] = { { 0, 1 }, /* ア (A-B) */ { 1, 2 }, /* イ (B-C) */ { 2, 3 }, /* ウ (C-D) */ { 3, 0 }, /* エ (D-A) */ { 4, 5 }, /* オ (E-F) */ { 5, 6 }, /* カ (F-G) */ { 6, 7 }, /* キ (G-H) */ { 7, 4 }, /* ク (H-E) */ { 0, 4 }, /* ケ (A-E) */ { 1, 5 }, /* コ (B-F) */ { 2, 6 }, /* サ (C-G) */ { 3, 7 } /* シ (D-H) */ };
この場合、例えば“点 C”(1,1,0) と“点 D”(0,1,0) を結ぶ線分“ウ”は、以下のようにして描画できます。 glVertex3dv() は、 引数に3つの要素を持つ GLdouble 型(double と等価)の配列のポインタを与えて、 頂点を指定します。
glBegin(GL_LINES); glVertex3dv(vertex[edge[2][0]]); /* 線分“ウ”の一つ目の端点“C”*/ glVertex3dv(vertex[edge[2][1]]); /* 線分“ウ”の二つ目の端点“D”*/ glEnd();
従って立方体全部を描くプログラムは以下のようになります。 なお、立方体がウィンドウからはみ出ないように、 glOrtho() で表示する座標系を (-2,-2)〜(2,2) にしています。 prog2.c を以下のように変更してください。
#include <GL/glut.h> GLdouble vertex[][3] = { { 0.0, 0.0, 0.0 }, { 1.0, 0.0, 0.0 }, { 1.0, 1.0, 0.0 }, { 0.0, 1.0, 0.0 }, { 0.0, 0.0, 1.0 }, { 1.0, 0.0, 1.0 }, { 1.0, 1.0, 1.0 }, { 0.0, 1.0, 1.0 } }; int edge[][2] = { { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 0 }, { 4, 5 }, { 5, 6 }, { 6, 7 }, { 7, 4 }, { 0, 4 }, { 1, 5 }, { 2, 6 }, { 3, 7 } }; void display(void) { int i; glClear(GL_COLOR_BUFFER_BIT); /* 図形の描画 */ glColor3d(0.0, 0.0, 0.0); glBegin(GL_LINES); for (i = 0; i < 12; i++) { glVertex3dv(vertex[edge[i][0]]); glVertex3dv(vertex[edge[i][1]]); } glEnd(); glFlush(); } void resize(int w, int h) { glViewport(0, 0, w, h); glLoadIdentity(); glOrtho(-2.0, 2.0, -2.0, 2.0, -2.0, 2.0); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); init(); glutMainLoop(); return 0; }
- glVertex3dv(const GLdouble *v)
- glVertex3dv() は3次元の座標値を指定するのに使います。 引数 v は3個の要素を持つ GLdouble 型(double と等価)配列を指定します。 v[0] には x 座標値、v[1] には y 座標値、v[2] には z 座標値を格納します。
前のプログラムでは、立方体が画面に平行投影されるため、 正方形しか描かないと思います。 そこで現実のカメラのように透視投影をしてみます。 これには glOrtho() の代わりに gluPerspective() を使います。
gluPerspective() は座標軸の代わりに、 カメラの画角やスクリーンのアスペクト比(縦横比)を用いて表示領域を指定します。 また glOrtho() 同様、前方面や後方面の位置の指定も行います。
視点の位置の初期値は原点なので、 このままでは立方体が視点に重なってしまいます。 そこで glTranslated() を使って立方体の位置を少し奥にずらしておきます。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int edge[][2] = { /* 変更なし */ }; void display(void) { /* 変更なし */ } void resize(int w, int h) { glViewport(0, 0, w, h); glLoadIdentity(); gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0); glTranslated(0.0, 0.0, -5.0); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
- gluPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar)
- 変換行列に透視変換の行列を乗じます。 最初の引数 fovy はカメラの画角であり、度で表します。 これが大きいほどワイドレンズ(透視が強くなり、絵が小さくなります)になり、 小さいほど望遠レンズになります。 2つ目の引数 aspect は画面のアスペクト比(縦横比)であり、 1 であればビューポートに表示される図形の X 方向と Y 方向のスケールが等しくなります。 3つ目の引数 zNear と4つ目の引数 zFar は表示を行う奥行き方向の範囲で、 zNear は手前(前方面)、zFar は後方(後方面)の位置を示します。 この間にある図形が描画されます。
- glTranslated(GLdouble x, GLdouble y, GLdouble z)
- 変換行列に平行移動の行列を乗じます。 引数はいずれも GLdouble 型 (double と等価)で、 3つの引数 x, y, z には現在の位置からの相対的な移動量を指定します。 引数が float 型なら glTranslatef() を使います。
ウィンドウをリサイズしたときに表示図形がゆがまないようにするためには、 gluPerspective() で設定するアスペクト比 aspect を、 glViewport() で指定したビューポートの縦横比 (w/h) と一致させます。
上のプログラムのように、 リサイズ後のウィンドウのサイズをそのままビューポートに設定している場合、 仮に aspect が定数であれば、 ウィンドウのリサイズに伴って表示図形が伸縮するようになります。したがって、 ウィンドウをリサイズしても表示図形の縦横比が変わらないようにするために、 ここでは aspect をビューポートの縦横比に設定しています。
前のプログラムのように、 視点の位置を移動するには、図形の方を glTranslated() や glRotated() を用いて逆方向に移動することで実現できます。 しかし、視点を任意の位置に指定したいときには gluLookAt() を使うと便利です。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int edge[][2] = { /* 変更なし */ }; void display(void) { /* 変更なし */ } void resize(int w, int h) { glViewport(0, 0, w, h); glLoadIdentity(); gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0); gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
- void gluLookAt(GLdouble ex, GLdouble ey, GLdouble ez, GLdouble cx, GLdouble cy, GLdouble cz, GLdouble ux, GLdouble uy, GLdouble uz)
- この最初の3つの引数 ex, ey, ez は視点の位置、 次の3つの引数 cx, cy, cz は目標の位置、 最後の3つの引数 ux, uy, uz は、 ウィンドウに表示される画像の「上」の方向を示すベクトルです。
この例では (3,4,5) の位置から原点 (0,0,0) を眺めますから、 立方体の A (0,0,0) の頂点がウィンドウの中心に来ると思います。
なお、gluPerspective(), gluLookAt() 等、 glu*() で始まる関数は GL Utility ライブラリ (-lGLU) の関数です。
ここまでできたら、今度はこの立方体を回してみましょう。 それにはちょっと工夫が必要です。アニメーションを行うには、 頻繁に画面の書き換えを行う必要があります。 しかし glutMailLoop() は無限ループであり、 glutDisplayFunc() で指定された関数は、 ウィンドウを再描画するイベントが発生したときにしか呼び出されません。
したがってアニメーションを実現するには、 このウィンドウの再描画イベントを連続的に発生させる必要があります。 プログラム中でウィンドウの再描画イベントを発生させるには、 glutPostRedisplay() 関数を用います。 これをプログラムが「暇なとき」に繰り返し呼び出すことで、 アニメーションが実現できます。 プログラムが暇になったときに実行する関数は、 glutIdleFunc() で指定します。
一つ注意しなければいけないことがあります。 繰り返し描画を行うには、 描画の度に座標変換の行列を設定する必要があります。
ところで座標変換のプロセスは、
という4つのステップで行われます。 今行おうとしている図形を回すという変換は、 「モデリング変換」に相当します。
これまではこれらを区別 せずに取り扱ってきました。 すなわち、これらの投影を行う行列式を掛け合わせることで、 単一の行列式として取り扱ってきたのです。
しかし図形だけを動かす場合は、 モデリング変換の行列だけを変更すればいいことになります。 また、後で述べる陰影付けは、 透視変換を行う前の座標系で計算する必要があります。
そこで OpenGL では、 「モデリング変換−ビューイング変換」の変換行列(モデルビュー変換行列)と、 「透視変換」の変換行列を独立して取り扱う手段が提供されています。 モデルビュー変換行列を設定する場合は glMatrixMode(GL_MODELVIEW)、 透視変換行列を設定する場合は glMatrixMode(GL_PROJECTION) を実行します。
カメラの画角などのパラメータを変更しなければ、 透視変換行列を設定しなければならないのはウィンドウを開いたときだけなので、 これは resize() で設定すればよいでしょう。 あとは全てモデリング−ビューイング変換行列に対する操作なので、 直後に glMatrixMode(GL_MODELVIEW) を実行します。
カメラ(視点)の位置を動かすアニメーションを行う場合は、 描画のたびに gluLookAt() によるカメラの位置や方向の設定 (ビューイング変換行列の設定) を行う必要があります。 同様に物体が移動したり回転したりするアニメーションを行う場合も、 描画のたびに物体の位置や回転角の設定(モデリング変換行列の設定)を 行う必要があります。 したがって、これらは display() の中で設定します。
マウスの左ボタンをクリックする度に、 立方体が1回転するようにします。 ついでに中央ボタンをクリックすると立方体が1ステップだけ回転し (関谷先生 ありがとうございました)、 右ボタンをクリックするとプログラムが終了するようにします。 prog2.c を以下のように変更してください。
#include <stdlib.h> #include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int edge[][2] = { /* 変更なし */ }; void idle(void) { glutPostRedisplay(); } void display(void) { int i; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の描画 */ glColor3d(0.0, 0.0, 0.0); glBegin(GL_LINES); for (i = 0; i < 12; i++) { glVertex3dv(vertex[edge[i][0]]); glVertex3dv(vertex[edge[i][1]]); } glEnd(); glFlush(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { glViewport(0, 0, w, h); /* 透視変換行列の設定 */ glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0); /* モデルビュー変換行列の設定 */ glMatrixMode(GL_MODELVIEW); } void mouse(int button, int state, int x, int y) { switch (button) { case GLUT_LEFT_BUTTON: /* アニメーション開始 */ if (state == GLUT_UP) glutIdleFunc(idle); break; case GLUT_MIDDLE_BUTTON: /* コマ送り */ if (state == GLUT_UP) { /* 表示イベントの無限ループを止める */ glutIdleFunc(0); /* 1ステップだけ進める */ glutPostRedisplay(); } break; case GLUT_RIGHT_BUTTON: /* プログラム終了 */ if (state == GLUT_UP) exit(0); break; default: break; } } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); glutMouseFunc(mouse); init(); glutMainLoop(); return 0; }
- int glutLayerGet(GLenum info)
- glClear()の解説でも述べましたが、 一つのウィンドウは幾つかのメモリが層(レイヤ)状に重なって構成されています。 この関数は処理対象のウィンドウの関係するレイヤの状態を調べます。 引数 info に GLUT_NORMAL_DAMAGED を指定すると、 そのウィンドウの表示が壊されている (他のウィンドウに隠されたあと再表示されたなど)場合に、 戻り値が真(非0)になります。 display() が実行されたときの再表示イベントには、 glutPostRedisplay() によるものとウィンドウマネージャからの本当の再表示イベントがありますから、 これらを区別するためにこの関数を使用しています。
- void glutPostRedisplay(void)
- 再描画イベントを発生させます。 このイベントの発生が発生すると、 glutDisplayFunc() で指定されている描画関数が実行されます。 なお、再描画が開始されるまでの間にこのイベントが複数回発生しても、 この描画関数は1度だけ実行されます。 また、この関数によって発生した再描画イベントでは、 glutLayerGet(GLUT_NORMAL_DAMAGED) は真になりません。
- void glutIdleFunc(void (*func)(void))
- 引数 func には、 プログラムが「何もすることがない」ときに実行する関数のポインタを指定します。 引数の関数はプログラムが「暇なとき」に繰り返し実行されます。 この関数を指定すると、 プログラムが止まっているように見えてもコンピュータの負荷は増大します。 したがって glutIdleFunc() による関数の指定は必要になった時点で行い、 不要になれば glutIdleFunc() の引数に 0 または NULL を指定して関数の指定を解除してやる必要があります。
- void glMatrixMode(GLenum mode)
- 設定する変換行列を指定します。 引数 mode が GL_MODELVIEW ならモデルビュー変換行列、 GL_PROJECTION なら透視変換行列を指定します。
前のプログラムでは毎回画面を全部描き換えているため、 表示がちらついてしまいます。 これを防ぐためには、ダブルバッファリングという方法を用います。 これは画面を2つに分け、 一方を表示している間に(見えないところで)もう一方に図形を描き、 それが完了したらこの2つの画面を入れ換える方法です。
GLUT でダブルバッファリングを使うには、 glutInitDisplayMode() に GLUT_DOUBLE の指定を追加します。 また、図形の描画後 glFlush() の代わりに glutSwapBuffers() を呼び出して、2つの画面の入れ換えを行います。
それでは、prog2.c でダブルバッファリングを行うようにしてみましょう。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int edge[][2] = { /* 変更なし */ }; void idle(void) { /* 変更なし */ } void display(void) { int i; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の描画 */ glColor3d(0.0, 0.0, 0.0); glBegin(GL_LINES); for (i = 0; i < 12; i++) { glVertex3dv(vertex[edge[i][0]]); glVertex3dv(vertex[edge[i][1]]); } glEnd(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); glutMouseFunc(mouse); init(); glutMainLoop(); return 0; }
- int glutSwapBuffers(void)
- ダブルバッファリングの2つのバッファを交換します。 glFlush() は自動的に実行されます。 このプログラムでこれを使うとずいぶん遅くなるように見えますが、 これはディスプレイのバッファの交換の時のちらつきを防ぐために、 ディスプレイの表示タイミング(帰線消去時間)を待っているためです。 Indy だとリフレッシュレートが 60Hz(デフォルト)なので、 バッファの交換は 1/60 秒ごとに行われます。 このプログラムは1周で 360 回再表示を行いますから、 最短でも 6 秒かかることになります。
それでは、次に立方体の面を塗りつぶしてみましょう。 面のデータは、稜線とは別に以下のように用意します。
int face[][4] = { { 0, 1, 2, 3 }, /* A-B-C-D を結ぶ面 */ { 1, 5, 6, 2 }, /* B-F-G-C を結ぶ面 */ { 5, 4, 7, 6 }, /* F-E-H-G を結ぶ面 */ { 4, 0, 3, 7 }, /* E-A-D-H を結ぶ面 */ { 4, 5, 1, 0 }, /* E-F-B-A を結ぶ面 */ { 3, 2, 6, 7 } /* D-C-G-H を結ぶ面 */ };
このデータを使って、線を引く代わりに6枚の4角形を描きます。 prog2.c を以下のように変更してください。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { { 0, 1, 2, 3 }, { 1, 5, 6, 2 }, { 5, 4, 7, 6 }, { 4, 0, 3, 7 }, { 4, 5, 1, 0 }, { 3, 2, 6, 7 } }; void idle(void) { /* 変更なし */ } void display(void) { int i; int j; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の描画 */ glColor3d(0.0, 0.0, 0.0); glBegin(GL_QUADS); for (j = 0; j < 6; j++) { for (i = 0; i < 4; i++) { glVertex3dv(vertex[face[j][i]]); } } glEnd(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
でもこれだと真っ黒で何もわからないので、 面ごとに色を変えてみましょう。 色のデータは以下のように作ってみます。
GLdouble color[][3] = { { 1.0, 0.0, 0.0 }, /* 赤 */ { 0.0, 1.0, 0.0 }, /* 緑 */ { 0.0, 0.0, 1.0 }, /* 青 */ { 1.0, 1.0, 0.0 }, /* 黄 */ { 1.0, 0.0, 1.0 }, /* マゼンタ */ { 0.0, 1.0, 1.0 } /* シアン */ };
一つの面を描く度に、この色を設定してやります。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble color[][3] = { { 1.0, 0.0, 0.0 }, { 0.0, 1.0, 0.0 }, { 0.0, 0.0, 1.0 }, { 1.0, 1.0, 0.0 }, { 1.0, 0.0, 1.0 }, { 0.0, 1.0, 1.0 } }; void idle(void) { /* 変更なし */ } void display(void) { int i; int j; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の描画 */ glBegin(GL_QUADS); for (j = 0; j < 6; j++) { glColor3dv(color[j]); for (i = 0; i < 4; i++) { glVertex3dv(vertex[face[j][i]]); } } glEnd(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
- void glColor3dv(const GLdouble *v)
- glColor3dv() は glColor3d() と同様にこれから描画するものの色を指定します。 引数 v は3つの要素を持った GLdouble 型(double と等価)の配列で、 v[0] には赤 (R)、v[1] には緑 (G)、v[2] には青 (B) の強さを、 0〜1 の範囲で指定します。
でもこれだとなんか変な表示になるかもしれません。 前のプログラムではデータの順番で面を描いていますから、 先に描いたものが後に描いたもので塗りつぶされてしまいます。 ちゃんとした立体を描くには隠面消去を行う必要があります。
隠面消去を行なうには glutInitDisplayMode() で GLUT_DEPTH を指定しておき、 glEnable(GL_DEPTH_TEST) を実行します。
こうすると、描画のときに Z バッファ(デプスバッファ)を使うようになります。 したがって、画面を消去するときは Z バッファも消去する必要があります。 それには glClear() で GL_DEPTH_BUFFER_BIT を指定します。
Z バッファを使うと、使わないときより処理速度が低下します。 そこで、必要なときだけ Z バッファを使うようにします。 Z バッファを使う処理の前で glEnable(GL_DEPTH_TEST) を実行し、 使い終わったら glDisable(GL_DEPTH_TEST) を実行します。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble color[][3] = { /* 変更なし */ }; void idle(void) { /* 変更なし */ } void display(void) { int i; int j; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の描画 */ glBegin(GL_QUADS); for (j = 0; j < 6; j++) { glColor3dv(color[j]); for (i = 0; i < 4; i++) { glVertex3dv(vertex[face[j][i]]); } } glEnd(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { glClearColor(1.0, 1.0, 1.0, 0.0); glEnable(GL_DEPTH_TEST); } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); glutMouseFunc(mouse); init(); glutMainLoop(); return 0; }
上のプログラムでは常に Z バッファを使うので、init() の中で glEnable(GL_DEPTH_TEST) を一度だけ実行し、glDisable(GL_DEPTH_TEST) の実行を省略しています。
立方体のように閉じた立体の場合、裏側にある面、 すなわち視点に対して裏を向いている面は見ることはできません。 そういう面をあらかじめ取り除いておくことで、 隠面消去処理の効率を上げることができます。
視点に対して裏を向いている面を表示しないようにするには glCullFace(GL_BACK)、 表を向いている面を表示しないようにするには glCullFace(GL_FRONT)、 両方とも表示しないようにするには glCullFace(GL_FRONT_AND_BACK) を実行します。ただし、この状態でも点や線などは描画されます。
また、glCullFace() を有効にするには glEnable(GL_CULL_FACE)、 無効にするには glDisable(GL_CULL_FACE) を実行します。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble color[][3] = { /* 変更なし */ }; void idle(void) { /* 変更なし */ } void display(void) { int i; int j; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の描画 */ glBegin(GL_QUADS); for (j = 0; j < 6; j++) { glColor3dv(color[j]); for (i = 0; i < 4; i++) { glVertex3dv(vertex[face[j][i]]); } } glEnd(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { glClearColor(1.0, 1.0, 1.0, 0.0); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); } int main(int argc, char *argv[]) { /* 変更なし */ }
このプログラムも、 多分妙な表示になります。 裏側の面を表示しないはずなのに、 実際は表側の面が削除されています。 実は、面の表裏は頂点をたどる順番で決定しています。 配列 face[] ではこれを右回り(時計回り)で結んでいます。 ところが OpenGL では、 標準では視点から見て頂点が左回りになっているとき、 その面を表として扱います。 試しに glCullFace(GL_FRONT) としてみてください。 あるいは、face[] において頂点を右回りにたどるようにしてみてください。
なお、頂点が右回りになっているときを表として扱いたいときは、 glFrontFace(GL_CW) を実行します。 左回りに戻すには glFrontFace(GL_CCW) を実行します。
一般にカリングはクリッピングや隠面消去の効率を上げるために、 視野外にある図形など見えないことが分かっているものを事前に取り除いておいて、 隠面消去(可視判定)の対象から外しておくことを言います。 これには様々な方法が考えられますが、glCullFace() による方法はそのもっとも基本的なものです。
次は面ごとに色を付けるかわりに、光を当ててみましょう。 陰影付け(光源の処理)の計算を行うためには、 面ごとの色の代わりに法線ベクトルを与えます。 glColor3dv() のかわりに glNormal3dv() を使います。
GLdouble normal[][3] = { { 0.0, 0.0,-1.0 }, { 1.0, 0.0, 0.0 }, { 0.0, 0.0, 1.0 }, {-1.0, 0.0, 0.0 }, { 0.0,-1.0, 0.0 }, { 0.0, 1.0, 0.0 } };
光を当てるためには、もちろん光源も設定する必要があります。 OpenGL には、最初からいくつかの光源が用意されています。 いくつの光源が用意されているかはシステムによって異なります。 0番目の光源(GL_LIGHT0 - 必ず用意されている)を有効にする (点灯する)には glEnable(GL_LIGHT0)、 無効にする(消灯する)には glDisable(GL_LIGHT0) を実行します。
陰影付けを行うと、陰影付けを行わないより処理速度は低下します。 陰影付けを有効にするには glEnable(GL_LIGHTING)、 無効にするには glDisable(GL_LIGHTING) を実行します。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble normal[][3] = { { 0.0, 0.0,-1.0 }, { 1.0, 0.0, 0.0 }, { 0.0, 0.0, 1.0 }, {-1.0, 0.0, 0.0 }, { 0.0,-1.0, 0.0 }, { 0.0, 1.0, 0.0 } }; void idle(void) { /* 変更なし */ } void display(void) { int i; int j; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の描画 */ glBegin(GL_QUADS); for (j = 0; j < 6; j++) { glNormal3dv(normal[j]); for (i = 0; i < 4; i++) { glVertex3dv(vertex[face[j][i]]); } } glEnd(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { glClearColor(1.0, 1.0, 1.0, 0.0); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_FRONT); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); } int main(int argc, char *argv[]) { /* 変更なし */ }
なお、陰影付けが有効になっているときは、 glColor3d() などによる色指定は無視されます。 glColor3d() などで色を付けたいときは、一旦 glDisable(GL_LIGHTING) を実行して陰影付けを行わないようにする必要があります。 一方、上のプログラムのように常に陰影付けを行う場合や、 光源を点灯したままにしておく場合は、 glEnable(GL_DEPTH_TEST) 同様 glEnalbe(GL_LIGHTING) や glEnable(GL_LIGHTn) を init() の中で一度実行するだけで十分です。 また、このときは glDisable(GL_LIGHTING) や glDisable(GL_LIGHTn) を実行する必要はありません。
それでは光源を2つにして、 それぞれの位置と色を変えてみましょう。 最初の光源 (GL_LIGHT0) の位置を Z 軸方向の斜め上 (0, 3, 5) に、2つ目の光源 (GL_LIGHT1) を X 軸方向の斜め上 (5, 3, 0) に置き、2つ目の光源の色を緑 (0, 1, 0) にします。 これらのデータはいずれも4つの要素を持つ GLfloat 型の配列に格納します。 4つ目の要素は 1 にしておいてください。
GLfloat light0pos[] = { 0.0, 3.0, 5.0, 1.0 }; GLfloat light1pos[] = { 5.0, 3.0, 0.0, 1.0 }; GLfloat green[] = { 0.0, 1.0, 0.0, 1.0 };
これらを glLightfv() を使ってそれぞれの光源に設定します。 prog2.c を以下のように変更してください。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble normal[][3] = { /* 変更なし */ }; GLfloat light0pos[] = { 0.0, 3.0, 5.0, 1.0 }; GLfloat light1pos[] = { 5.0, 3.0, 0.0, 1.0 }; GLfloat green[] = { 0.0, 1.0, 0.0, 1.0 }; void idle(void) { /* 変更なし */ } void display(void) { int i; int j; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 光源の位置設定 */ glLightfv(GL_LIGHT0, GL_POSITION, light0pos); glLightfv(GL_LIGHT1, GL_POSITION, light1pos); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の描画 */ glBegin(GL_QUADS); for (j = 0; j < 6; j++) { glNormal3dv(normal[j]); for (i = 0; i < 4; i++) { glVertex3dv(vertex[face[j][i]]); } } glEnd(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { glClearColor(1.0, 1.0, 1.0, 0.0); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_FRONT); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glEnable(GL_LIGHT1); glLightfv(GL_LIGHT1, GL_DIFFUSE, green); } int main(int argc, char *argv[]) { /* 変更なし */ }
- void glLightfv(GLenum light, GLenum pname, const GLfloat *params)
- 光源のパラメータを設定します。最初の引数 light には設定する光源の番号 (GL_LIGHT0〜GL_LIGHTn、n はシステムによって異なります)です。 2つ目の引数 pname は設定するパラメータの種類です。 ここに GL_POSITION を指定すると光源の位置を設定します。 また GL_DIFFUSE を指定すると光源の拡散反射光強度(色)を設定します。 最後の引数 params は、pname に指定したパラメータの種類に設定する値です。 pname が GL_POSITION あるいは GL_DIFFUSE のときは、 params は4つの要素を持つ GLfloat 型の配列で、 それぞれ光源の位置および拡散反射光強度を指定します。 光源が (x, y, z) の位置にあるとき、 params の各要素には (x/w, y/w, z/w, w) を設定します。 通常 w = 1 として点光源の位置を設定しますが、w = 0 であれば (x, y, z) 方向の平行光線の設定になります。 また光源の拡散反射光強度が (R, G, B) なら params の各要素には (R, G, B, 1) を設定します。 なお、この初期値は (1 1 1 1) ですが、RGB には 1 を越えた値を設定できます。
陰影付けの計算は視点座標系で行われるので、 glLightfv() による光源の位置 (GL_POSITION) の設定は、 視点の位置を設定した後に行う必要があります。 また、上のプログラムの glRotate3d() より後でこれを設定すると、 光源もいっしょに回転してしまいます。
座標変換のプロセスは “モデリング変換→ビューイング変換→透視変換→…” という順に行われると書きましたが、 プログラムのコーディング上は、これらの設定が 逆順になる ことに注意してください。
- glLoadIdentity() でモデルビュー変換行列を初期化
- gluLookAt() 等でビューイング変換を設定
- glTranslated() や glRotated() 等でモデリング変換を設定
- glBegin()〜glEnd() 等による描画
1-2 の間で光源の位置を設定した場合は、 光源は視点と一緒に移動します。 このとき、光源の方向を (0, 0, 1, 0)、すなわち Z 軸方向に設定すれば、 自動車のヘッドライトのような効果を得ることができます。2-3 の間で光源の位置を設定した場合は、 光源の位置は視点や図形の位置によらず固定になります。 通常はここで光源の位置を設定します。3-4 の間で光源の位置を設定した場合は、 光源の位置は図形と一緒に移動します。
glLightfv() による光源の色の設定 (GL_DIFFUSE 等) は、 必ずしも display() 内に置く必要はありません。 プログラムの実行中に光源の色を変更しないなら、 glEnable(GL_DEPTH_TEST) や glEnable(GL_LIGHTING) 同様 init() の中で一度実行すれば十分です。
glLightf*() で設定可能なパラメータは、 GL_POSITION や GL_DIFFUSE 以外にもたくさんあります。 光源を方向を持ったスポットライトとし、 その方向や広がり、減衰率なども設定することもできます。 詳しくは man glLightf を参照してください。
前の例では図形に色を付けていませんでしたから、 立方体はデフォルトの色(白)で表示されたと思います。 今度はこの色を変えてみましょう。 この場合も光源の時と同様に4つの要素を持つ GLfloat 型の配列を用意し、 個々の要素に色を R、G、B それに A の順に格納します。 4つ目の要素 (A) は、ここではとりあえず 1 にしておいてください。
GLfloat red[] = { 0.8, 0.2, 0.2, 1.0 };
glColor*() で色を付けるときと同様、 図形を描く前に glMaterialfv() を使ってこの色を図形の色に指定します。 prog2.c を以下のように変更してください。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble normal[][3] = { /* 変更なし */ }; GLfloat light0pos[] = { 0.0, 3.0, 5.0, 1.0 }; GLfloat light1pos[] = { 5.0, 3.0, 0.0, 1.0 }; GLfloat green[] = { 0.0, 1.0, 0.0, 1.0 }; GLfloat red[] = { 0.8, 0.2, 0.2, 1.0 }; void idle(void) { /* 変更なし */ } void display(void) { int i; int j; static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); /* 視点位置と視線方向 */ gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); /* 光源の位置設定 */ glLightfv(GL_LIGHT0, GL_POSITION, light0pos); glLightfv(GL_LIGHT1, GL_POSITION, light1pos); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の色(赤) */ glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, red); /* 図形の描画 */ glBegin(GL_QUADS); for (j = 0; j < 6; j++) { glNormal3dv(normal[j]); for (i = 0; i < 4; i++) { glVertex3dv(vertex[face[j][i]]); } } glEnd(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
- void glMaterialfv(GLenum face, GLenum pname, const GLfloat *params)
- glMaterialfv() は図形の材質パラメータを設定します。 引数 face には GL_FRONT、GL_BACK および GL_FRONT_AND_BACK が指定でき、 それぞれ面の表、裏、あるいは両面に材質パラメータを設定します。 設定できる材質 pname には GL_AMBIENT(環境光に対する反射係数)、 GL_DIFFUSE(拡散反射係数)、GL_SPECULAR(鏡面反射係数)、 GL_EMISSION(発光係数)、GL_SHININESS(ハイライトの輝き)、 あるいは GL_AMBIENT_AND_DIFFUSE(拡散反射係数と鏡面反射係数の両方) があります。他にインデックスカラーモード (GLUT_INDEX) であれば GL_COLOR_INDEXES も使用できますが、 この資料では使用していません。 引数 params は1つまたは4つの要素を持つ GLfloat 型(float と等価)の配列で、 4つの要素を持つ場合(GL_SHININESS、GL_COLOR_INDEXES 以外)は、 色の成分 RGB および A に対する係数を指定します。 この初期値は (0.8, 0.8, 0.8, 1) ですが、 1 を越える値も設定できます。
図形に色を付けるということは、 図形の物理的な材質パラメータを設定することに他なりません。 GL_DIFFUSE で設定する拡散反射率が図形の色に相当します。 GL_AMBIENT は環境光(光源以外からの光)に対する反射率で、 光の当たらない部分の明るさになります。 GL_SPECULAR は光源に対する鏡面反射率で、 図形表面の光源の映り込み(ハイライト)の強さです。 GL_SHININESS はこの鏡面反射の細さを示し、 大きいほどハイライトの部分が小さくなります。 この材質パラメータの要素は1つだけなので、 glMaterialf() を使って設定することもできます。
GL_DIFFUSE 以外のパラメータを設定することによって、 図形の質感を制御できます。 たとえば GL_SPECULAR(鏡面反射係数)を白 (1 1 1 1) に設定して GL_SHININESS を大きく(10〜40 とか/最大 128)すれば つややかなプラスチックのようになりますし、 GL_SPECULAR(鏡面反射係数)を GL_DIFFUSE と同じにして GL_AMBIENT を 0 に近づければ金属的な質感になります。 ただし GL_SPECULAR や GL_AMBIENT を操作するときは、 glLightfv() で光源のこれらのパラメータも設定してやる必要があります。
次に図形の階層構造を表現してみます。 これまでのプログラムで実際に立方体を描いている部分を、 独立した関数 cube() として抜き出します。
また、 視点の位置や画角などは変更しないので、 これをウィンドウを開いたりサイズが変更されたときに設定するようにします。 こうすると変換行列は glRotated() で変更されたあと元に戻されないため、 このままでは次に描画するときにはおかしくなってしまいます。 そこで、glRoatated() を使う前に、 そのときの変換行列の内容を保存しておき、 あとでその内容を戻します。 これには glPushMatrix() と glPopMatrix() を使います。
prog2.c を以下のように変更してください。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble normal[][3] = { /* 変更なし */ }; GLfloat light0pos[] = { 0.0, 3.0, 5.0, 1.0 }; GLfloat light1pos[] = { 5.0, 3.0, 0.0, 1.0 }; GLfloat green[] = { 0.0, 1.0, 0.0, 1.0 }; GLfloat red[] = { 0.8, 0.2, 0.2, 1.0 }; void cube(void) { int i; int j; glBegin(GL_QUADS); for (j = 0; j < 6; j++) { glNormal3dv(normal[j]); for (i = 0; i < 4; i++) { glVertex3dv(vertex[face[j][i]]); } } glEnd(); } void idle(void) { /* 変更なし */ } void display(void) { static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* 光源の位置設定 */ glLightfv(GL_LIGHT0, GL_POSITION, light0pos); glLightfv(GL_LIGHT1, GL_POSITION, light1pos); /* モデルビュー変換行列の保存 */ glPushMatrix(); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の色(赤) */ glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, red); /* 図形の描画 */ cube(); /* モデルビュー変換行列の復帰 */ glPopMatrix(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { glViewport(0, 0, w, h); /* 透視変換行列の設定 */ glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0); /* モデルビュー変換行列の設定 */ glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(3.0, 4.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
この図形に、もう一つ立方体を追加します。 2つ目の cube() を実行する前に glTranslated() を実行して、 最初の cube() の位置から少しずらします。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble normal[][3] = { /* 変更なし */ }; GLfloat light0pos[] = { 0.0, 3.0, 5.0, 1.0 }; GLfloat light1pos[] = { 5.0, 3.0, 0.0, 1.0 }; GLfloat green[] = { 0.0, 1.0, 0.0, 1.0 }; GLfloat red[] = { 0.8, 0.2, 0.2, 1.0 }; void cube(void) { /* 変更なし */ } void idle(void) { /* 変更なし */ } void display(void) { static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* 光源の位置設定 */ glLightfv(GL_LIGHT0, GL_POSITION, light0pos); glLightfv(GL_LIGHT1, GL_POSITION, light1pos); /* モデルビュー変換行列の保存 */ glPushMatrix(); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の色(赤)*/ glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, red); /* 図形の描画 */ cube(); /* 二つ目の図形の描画 */ glPushMatrix(); glTranslated(1.0, 1.0, 1.0); cube(); glPopMatrix(); /* モデルビュー変換行列の復帰 */ glPopMatrix(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
本当はこの2つ目の cube() をはさんでいる glPushMatrix()、glPopMatrix() は不要なのですが、説明をわかりやすくするために付けています。
ではこの2つ目の cube() を、 1つ目の cube() の倍の速度で回転させてみましょう。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble normal[][3] = { /* 変更なし */ }; GLfloat light0pos[] = { 0.0, 3.0, 5.0, 1.0 }; GLfloat light1pos[] = { 5.0, 3.0, 0.0, 1.0 }; GLfloat green[] = { 0.0, 1.0, 0.0, 1.0 }; GLfloat red[] = { 0.8, 0.2, 0.2, 1.0 }; void cube(void) { /* 変更なし */ } void idle(void) { /* 変更なし */ } void display(void) { static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* 光源の位置設定 */ glLightfv(GL_LIGHT0, GL_POSITION, light0pos); glLightfv(GL_LIGHT1, GL_POSITION, light1pos); /* モデルビュー変換行列の保存 */ glPushMatrix(); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の色(赤)*/ glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, red); /* 図形の描画 */ cube(); /* 二つ目の図形の描画 */ glPushMatrix(); glTranslated(1.0, 1.0, 1.0); glRotated((double)(2 * r), 0.0, 1.0, 0.0); cube(); glPopMatrix(); /* モデルビュー変換行列の復帰 */ glPopMatrix(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
この例では、1つ目の glRotated() による回転が 両方の cube() に影響しているのに対し、 2つ目の glRotated() は2つ目の cube() にしか影響していません。 これによって、図形の動きの階層構造を表現できます。 では最後に、この2つの立方体の色を変えてみましょう。
#include <GL/glut.h> GLdouble vertex[][3] = { /* 変更なし */ }; int face[][4] = { /* 変更なし */ }; GLdouble normal[][3] = { /* 変更なし */ }; GLfloat light0pos[] = { 0.0, 3.0, 5.0, 1.0 }; GLfloat light1pos[] = { 5.0, 3.0, 0.0, 1.0 }; GLfloat green[] = { 0.0, 1.0, 0.0, 1.0 }; GLfloat red[] = { 0.8, 0.2, 0.2, 1.0 }; GLfloat blue[] = { 0.2, 0.2, 0.8, 1.0 }; void cube(void) { /* 変更なし */ } void idle(void) { /* 変更なし */ } void display(void) { static int r = 0; /* 回転角 */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* 光源の位置設定 */ glLightfv(GL_LIGHT0, GL_POSITION, light0pos); glLightfv(GL_LIGHT1, GL_POSITION, light1pos); /* モデルビュー変換行列の保存 */ glPushMatrix(); /* 図形の回転 */ glRotated((double)r, 0.0, 1.0, 0.0); /* 図形の色(赤)*/ glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, red); /* 図形の描画 */ cube(); /* 二つ目の図形の描画 */ glPushMatrix(); glTranslated(1.0, 1.0, 1.0); glRotated((double)(2 * r), 0.0, 1.0, 0.0); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, blue); cube(); glPopMatrix(); /* モデルビュー変換行列の復帰 */ glPopMatrix(); glutSwapBuffers(); /* 回転の制御 */ if (glutLayerGet(GLUT_NORMAL_DAMAGED) == 0) { /* glutPostRedisplay() による再描画 */ if (++r >= 360) { /* 一周回ったらアニメーションを止める */ r = 0; glutIdleFunc(0); } } } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
ここからようやく実験の本題に入ります。 これまでのようにソースプログラムは明示しませんから、 自分で実装を考えてください。
ウォークスルーは3次元CGシーンの中を歩き回る効果のことを言います。 これは視点の移動によるアニメーションです。 マウスで視点の位置を動かすプログラムを作ってみましょう。 これまでに作ったプログラムをベースを改造するのが手っ取り早いと思いますが、 うまく行かなければ下の手順を参考にしてください。 ソースファイル名は prog3.c としてください。
下のプログラムは球を一つだけ静止画で表示します。 球の表示には glutSolidSphere() という関数を用いています。 ただし、このプログラムでは、これを display() ではなく、別の関数 scene() の中で実行しています。
glNewList()〜glEndList() の間に挟まれた OpenGL の命令は、 glNewList() の引数に GL_COMPLIE を指定しているときにはすぐには表示されず、 OpenGL のサーバ側に保存されます。 これを実際に表示するには glCallList() を使います。 これはディスプレイリストと呼ばれ、 同じ OpenGL のコマンドを繰り返し実行する場合は、 そのコマンドをサーバ側に転送する手間が省けるために表示速度が向上します。 なお、使用可能なディスプレイリスト番号を得るには glGenLists() を用います。
#include <GL/glut.h> GLuint objects; /* ディスプレイリスト番号 */ void display(void) { static double ex = 0.0, ez = 0.0; /* 視点の位置 */ static double r = 0.0; /* 視点の向き */ /* 画面クリア */ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); /* モデルビュー変換行列の初期化 */ glLoadIdentity(); /* 視点の移動 */ glRotated(r, 0.0, 1.0, 0.0); glTranslated(ex, 0.0, ez); /* シーンの描画 */ glCallList(objects); glFlush(); } void resize(int w, int h) { /* ウィンドウ全体をビューポートにする */ glViewport(0, 0, w, h); /* 透視変換行列を設定する */ glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(30.0, (double)w / (double)h, 1.0, 100.0); /* モデルビュー変換行列を指定しておく */ glMatrixMode(GL_MODELVIEW); } void init(void) { /* 初期設定 */ glClearColor(1.0, 1.0, 1.0, 0.0); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glEnable(GL_LIGHT0); } void scene(void) { static GLfloat red[] = { 0.8, 0.2, 0.2, 1.0 }; static GLfloat green[] = { 0.2, 0.8, 0.2, 1.0 }; static GLfloat blue[] = { 0.2, 0.2, 0.8, 1.0 }; static GLfloat yellow[] = { 0.8, 0.8, 0.2, 1.0 }; int i; /* 図形をディスプレイリストに登録 */ objects = glGenLists(1); glNewList(objects, GL_COMPILE); /* 陰影付けを ON にする */ glEnable(GL_LIGHTING); /* 赤い球 */ glPushMatrix(); glTranslated(0.0, 0.0, -5.0); glMaterialfv(GL_FRONT, GL_DIFFUSE, red); glutSolidSphere(0.5, 10, 5); glPopMatrix(); /* 緑の球 */ glPushMatrix(); glTranslated(0.0, 0.0, 5.0); glMaterialfv(GL_FRONT, GL_DIFFUSE, green); glutSolidSphere(0.5, 10, 5); glPopMatrix(); /* 青い球 */ glPushMatrix(); glTranslated(-5.0, 0.0, 0.0); glMaterialfv(GL_FRONT, GL_DIFFUSE, blue); glutSolidSphere(0.5, 10, 5); glPopMatrix(); /* 黄色い球 */ glPushMatrix(); glTranslated(5.0, 0.0, 0.0); glMaterialfv(GL_FRONT, GL_DIFFUSE, yellow); glutSolidSphere(0.5, 10, 5); glPopMatrix(); /* 陰影付けを OFF にする */ glDisable(GL_LIGHTING); /* 地面を線画で描く */ glColor3d(0.0, 0.0, 0.0); glBegin(GL_LINES); for (i = -10; i <= 10; i++) { glVertex3d((GLdouble)i, -0.5, -10.0); glVertex3d((GLdouble)i, -0.5, 10.0); glVertex3d(-10.0, -0.5, (GLdouble)i); glVertex3d( 10.0, -0.5, (GLdouble)i); } glEnd(); glEndList(); } int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_RGBA | GLUT_DEPTH); glutCreateWindow(argv[0]); glutDisplayFunc(display); glutReshapeFunc(resize); init(); scene(); glutMainLoop(); return 0; }
これを、マウスを前後にドラッグしたときに、 それにつれて視点が前後に移動するようにしてください。 マウスの位置を「速度」として扱い、 マウスがウィンドウの中心にあるときに静止(速度0)、 中心から離れるにつれて速く動くようになるようにしましょう。
上のプログラムでは、視点を移動するかわりに 物体の方を逆方向に動かして います。このため ex, ez および r の値は、 進行方向とは逆に設定する必要がありますので、注意してください。
scene() の図形は自分なりに色々変えてみてください。 ただし、あんまり複雑なものを描くと表示が遅くなります。 部品には glutSolidSphere() のほか、glutSolidCube(), glutSolidCone(), glutSolidDodecahedron(), glutSolidOctahedron(), glutSolidIcosahedron(), glutSolidTetrahedron(), glutSolidTorus(), それに glutSolidTeapot() などが用意されています。詳しくは man コマンドや GLUT ガイド (日本語版) を参照してください。
glutSolidTetrahedron() などはサイズを指定することができませんから、 これらの大きさを変えたいときは glScaled() を使ってください。
- glScaled(GLdouble x, GLdouble y, GLdouble z)
- 変換行列に拡大縮小の行列を乗じます。 引数はいずれも GLdouble 型 (double と等価)で、 3つの引数 x, y, z には拡大係数を指定します。 引数が float 型なら glScalef() を使います。
マウスで前後移動ができるようになったら、 今度はマウスの左右の動きを視点の回転の角速度に反映して、 進行方向を変えられるようにしてください。 この場合もウィンドウの中心で角速度が0になるようにしましょう。 もちろん、見ている方向に前後移動できるようにしてください。
視点の現在位置が (ex, ez) にあり、r の方向を向いているとき、 次に画面表示を行なう時の視点の位置 (ex', ez') と視線の方向 r' を、マウスの位置から得られる速度と角速度から求めます。
なお、これはあくまで「一例」に過ぎません。 自分の思う方法で歩き回り方を考えてくれれば結構です。 ただし、 ちゃんと物体の背後に「回り込む」ことができるようにして下さい。
両眼視差による立体視は右眼用と左眼用の画像を別々に生成すれば実現できます。 一つのディスプレイでこの2つの画像を見ることができるように、 実験室の Indy には液晶シャッタ眼鏡 (Crystal Eyes) が接続されています。
ディスプレイの表示が1秒間に何回も書き替えられていることは、 みなさん良くご存じのことと思います (テレビの前で手を振ったことないですか?)。 Indy の場合、デフォルトではリフレッシュレートが 60Hz なので、 この書き換えは1秒間に 60 回行われています。
そこで、この書き換えの時に右眼用の画像と左眼用の画像を交互に表示して、 液晶シャッタ眼鏡で右眼用の画像が表示されているときは左目を閉じ、 左眼用の画像が表示されているときは右目を閉じてしまいます。 こうして一つのディスプレイで右目と左目に別々の絵を見せることができます。
ところで GLUT のマニュアルを見ると、 ディスプレイにこのような方法で画像を表示させるには glutInitDisplayMode() で GLUT_STEREO 指定すれば良いように思えます。 しかし、これが可能なのは Onyx などの高性能な機種において、 あらかじめステレオバッファを使えるように設定している場合に限られ、 実験室の Indy ではこれを指定してもエラーになってしまいます。
じゃあどうするのかというと、/usr/gfx/setmon という外部コマンドを使います。
#include <stdlib.h> ... void keyboard(unsigned char key, int x, int y) { switch (key) { case 'q': case 'Q': case '\033': /* プログラムの実行終了時に元のディスプレイモードに戻す */ system("/usr/gfx/setmon -n 60HZ"); exit(0); /* '\033' は ESC の ASCII コード */ default: break; } } ... int main(int argc, char *argv[]) { /* プログラムの実行開始時にステレオモードに変更 */ system("/usr/gfx/setmon -n STR_RECT"); ... /* ESC をタイプしたら終了するようにする */ glutKeyboardFunc(keyboard); /* GLUT を使ってフルスクリーンモードで表示 */ glutFullScreen(); ... return 0; }
/usr/gfx/setmon -n STR_RECT を実行すると、 ディスプレイのリフレッシュレートがそれまでの倍の 120Hz になる一方で、 走査線の数が半分になります (同時に Crystal Eyes のトランスミッタの LED が発光します)。 そして、元の画面の下半分 (0, 0)-(1279, 491) と上半分 (0, 532)-(1279, 1023) が、それぞれ交互にディスプレイ全体に表示されるようになります。 フルスクリーンモードにするのは、 立体視を行うアプリケーション以外が画面に表示されるのを防ぐためです。
O2 の場合、画面の下半分は (0, 0)-(1279, 491)、上半分は (0, 512)-(1279, 1003) になります。またこの範囲を越えるビューポートを設定して描画を行うと、 O2 の画面表示がおかしくなってしまいます。
なお、フルスクリーンモードで動かすと ウィンドウマネージャなどの操作ができなくなります。 Alt を押しながら ESC をタイプすればウィンドウを切り替えることができますが、 それでもステレオモードなのでマウスやコマンド等の操作は難しいでしょう。 したがってこのプログラムは、 終了時に必ずディスプレイのモードを元に戻す仕組みを組み込んでおいてください (7.3節参照)。
フルスクリーンモードのアスペクト比は 1.3 程度です (ウィンドウのリサイズは行えないので、アスペクト比は定数で構いません)。 こうしてビューポートをディスプレイの下半分と上半分に切り替えながら、 それぞれに右眼用の画像と左眼用の画像を表示します。
... /* 右眼のビューポート */ glViewport(0, 0, 1280, 492); /* モデルビュー変換行列の初期化 */ glLoadIdentity(); /* 右眼の位置と方向 */ gluLookAt(/* ここは自分で考えてください */); /* 視点の移動 */ glRotated(r, 0.0, 1.0, 0.0); glTranslated(ex, 0.0, ez); /* シーンの描画 */ glCallList(objects); /* 左眼のビューポート,O2 の場合は 0, 512, 1280, 492 */ glViewport(0, 532, 1280, 492); /* モデルビュー変換行列の初期化 */ glLoadIdentity(); /* 左眼の位置と方向 */ gluLookAt(/* ここは自分で考えてください */); /* 視点の移動 */ glRotated(r, 0.0, 1.0, 0.0); glTranslated(ex, 0.0, ez); /* シーンの描画 */ glCallList(objects); ...
二つの視点の間隔とそれぞれの位置・方向、 および fovy(実験1のサンプルでは 30 に設定されています)は、 ディスプレイの表示面の高さ、表示面との距離、 自分の両目の間隔の実測値から割り出してください。
本当は gluLookAt() を使わずに、 gluPerspective() を glFrustum() に置き換えて左右の目の視野をずらしたほうが厳密なんですが、 それだとプログラムが少しややこしくなりますし、 glFrustum() の説明もしなきゃいけなくなるので手を抜きます。
ディスプレイをのぞき穴に見立てて、 その向こう側に仮想的な部屋があるものとして画像を生成します。 観測者の頭に位置センサ (IsoTrak) を取り付け、 その情報を元に画像を生成することにより、 運動視差による奥行き感を実現します。 IsoTrak からのデータの読み込みについては、 IsoTrak II の資料を参照してください。
これも gluLookAt() を使わずに、 gluPerspective() を glFrustum() に置き換えて視野をずらしたほうが厳密なんですが、 視点の位置を gluLookAt() で動かす方法を採って構いません。
SuperGrove から得られるデータを元に、 CG指人形を動かします。 SuperGrove からのデータの読み込みについては、 SupreGlove Jr. の資料を参照してください。 なお、この実験は表示形状を変化させることになるので、 ディスプレイリストは使わない方が簡単だと思います。
Crystal Eys を装着し、IsoTrak のセンサを握り締めて、 パンチングボールを殴ります。 IsoTrak からのデータの読み込みについては、 IsoTrak II の資料を参照してください。