この文書は学生実験のテーマ「VR実験」の参考資料の, GLUT を用いた OpenGL のチュートリアルです. 180 分× 2 日+αで実験部分に到達できると思います. ただし内容は不十分なので, 必要に応じて資料やオンラインマニュ アル等を参照してください. また間違いも含まれていると思います. コメントをお願いします. なお, このページはリンク&コピーフリーです. このディレクトリをまとめたものを ここ に用意していますので, ご自由にお使いください.
初版 1997/09/30,
最終更新 2016/07/22
この学生実験 (演習) は 2016 年度をもって終了しました. 18 年間 (演習で使用した期間) ありがとうございました.
目次
資料:
- 今までにあった質問
- リフレッシュレートの変更
- AUX 版, Indy 版, 書籍版
- 床井研究室 (OpenGL 関連記事)
- 柴山 健伸 先生 (システム工学部情報通信システム学科) の混沌としたサンプル
- 陳 謙 先生 (システム工学部デザイン情報学科) の Motif を使ったサンプル
- 中山 礼児 氏 (経済学部 2000 年卒) の Delphi についての解説
- The OpenGL WEB Site (OpenGL の総本山)
- GLUT - The OpenGL Utility Toolkit (OpenGL.org の GLUT のページ)
- OpenGL Code & Tutorial Listings (OpenGL.org のチュートリアル集)
- OpenGL Technical FAQ (OpenGL について良く聞かれる質問)
- OpenGL FAQ 日本語 (OpenGL について良く聞かれる質問の日本語版)
- GLUT Programming Interface API Version 3 (GLUT のマニュアル)
- GLUT (OpenGL Utility Toolkit) ガイド 日本語版 (上記の邦訳)
- GLUT FAQ (GLUT について良く聞かれる質問)
- GLUT for Mac OS X (Mac OS X の GLUT のソースプログラム)
- 株式会社エクサさんの OpenGL プログラミングコース (本格的な教材・公開に感謝)
- WisdomSoft (赤坂玲音) さんの OpenGL 入門 (系統立てて構成された優れたテキスト)
- 数学と計算 (OpenGL のほか, C/C++, LaTeX 等豊富なチュートリアルあり)
- sonson@Picture&Software (GLUT に頼らない OpenGL の使い方等詳しい解説がある)
- OpenGL de プログラミング (基礎から最新技術・周辺技術まで網羅的にまとまっている)
- ☆PROJECT ASURA☆ ([PROGRAM] のページに優れた解説とサンプルがたくさんある)
- コンピュータ・シミュレーション講座 OpenGL 入門 (natural science Laboratory)
- JOGL (Java 版 OpenGL) のチュートリアル (この「手抜き OpenGL」をもとに作成されました)
- OpenGL Mailing List (日本の OpenGL エキスパートが集う)
OpenGL は Silicon Graphics 社 (現 SGI 社, 以下 SGI) が開発した, OS に依存しない三次元のグラフィックスライブラリ (正確には Application Program Interface, API) です. でも, この「OS に依存しない」というところが実は曲者で, ウィンドウを開いたりマウスの操作を受け付けたりするところは, それぞれの OS の流儀に則って, OS やウィンドウシステムにお願いしないといけません. すなわち, OpenGL の機能を使えるようにするためには, Windows なら Windows の, X Window なら X Window のやり方で, あらかじめお膳立てをしてやる必要があるのです.
実はこれが結構面倒な作業なので, 教科書の OpenGL Programming Guide の第 1 版 では, 補助ライブラリ (AUX ライブラリ, 一種のツールキット) というのを導入して, その部分をとりあえず隠していました. つまり, 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 (The OpenGL Utilitiy Toolkit) だと言えます. これは SGI の Mark Kilgard 氏によって作成されました (今は NVIDIA に居るみたいですけど). またユタ大学の Nate Robins 氏 (この人も今は NVIDIA に居るらしい) という人によって, Windows にも移植されました. このため GLUT には AUX ライブラリのような問題?はありません. バージョン 3.6 以降では Windows 版と UNIX 版のソースコードが統合され, まとめて提供されています.
Linux や Macintosh では Mesa の上に AUX ライブラリや GLUT が移植されました. また Apple 自身もついに? Mac OS 8.1 から OpenGL を採用し, この上でも GLUT が使用可能になりました. 現在の Mac OS X ではグラフィックス機能の基盤として OpenGL を採用しており, Developer Tools (Xcode) には標準で GLUT が含まれています. ソースプログラムも公開されています (GLUT for Mac OS X). いくつかデモプログラムも入っています.
シミュレーション結果の視覚化など, グラフィックスを専門としない人が グラフィックスプログラミングをしなければならないということは結構ありますよね. 私の記憶が正しければ, かつて (いつの話だ?) は Calcomp のプロッタライブラリとか, Tektronix 4014 ターミナルのエスケープシーケンスとか, あるいは N88BASIC のグラフィックス (GLIO 呼び出しとか) なんかがそういう目的に使われてたように思います.
現在なら, そういう目的には何を使えばいいのでしょうか? Windows ならもちろん DirectX に含まれる Direct3D (D3D) が使えます. X Window なら Xlib で書くしかないのでしょうか? PEX はもう廃れましたよね? こういうものは, 使ったことがある人はわかると思いますが, 実際に絵を描き始めるまでに訳の分からない呪文をいっぱい並べないといけなかったりして, 結構煩わしいもんですよね. 特にグラフィックスとなると
本格的な GUI (Graphical User Interface) を持ったアプリケーションプログラムを作りたければ, Windows なら素直に Visual BASIC を使うか, MFC (Microsoft Foundation Class) あるいは .Net なんかを使うべきでしょう. X Window なら Motif や GTK などのツールキットを使えば見栄えのいいものができると思います. でも, これらはあくまで「ユーザーインタフェース構築のための部品集」なので, これら自体はあまり「グラフィックスプログラミング」の役に立ちそうにありません.
OpenGL は三次元のグラフィックスライブラリですが, もちろん二次元の機能も持っています. なにより, これを使うと N88BASIC の LINE 文で図形を書いていた頃 (遠くなったなぁ) の気楽さで グラフィックスプログラミングができます (あくまで個人的な印象です). それでいて, (当たり前だけど) N88BASIC とは比較にならないほどいろんなことができます.
ということで, OpenGL と GLUT を組み合わせれば,
という三拍子そろったメリットが得られます.
iPhone OS や Android では, OpenGL から派生した OpenGL ES が使われています.
もちろん GLUT は, 本格的な GUI を持ったプログラムの開発には向いていません. しかし研究などで, 手早くグラフィックスのプログラムを仕上げないといけないという場合には, とても便利な組み合わせだと思います.
なお GUI については, オリジナルの GLUT 自体にも一応 mui (micro-UI) というツールキットが含まれています. このほか, GLUI という GLUT と C++ で書かれたツールキットがリリースされています. これについては, GLUIリファレンスマニュアル日本語版 に日本語で書かれた詳しい資料があります. また Clutter という OpenGL のほかに OpenGL ES でも使用できるマルチプラットホームの ツールキットも開発されています.
これらと同様にマルチプラットホームで使えるツールキットとして, Qt ("キュート" と読む) や FLTK (Fast Light Toolkit), GLFW, SDL (Simple Directmedia Layer) などがあります. Qt は最近のグラフィックスアプリケーションの GUI ツールキットとして非常に人気があります. 非常に高機能ですが, 非常に巨大です. FLTK は GUI に特化していますが, 非常に軽量です. GLFW はマウスやキーボードのほか, ジョイスティックも入力デバイスとして使えます. SDL はジョイスティックなどの入力機器に加えて, 音声も扱うことができます. 一方, Linux などで使われる GTK というツールキットをベースにしたものに, GtkGLArea や GtkGLExt があります. その他の OpenGL と組み合わせて使えるツールキットについては, OpenGL.org の GLUT-like Windowing Toolkits に紹介があります.
オリジナルの GLUT のソースファイルの場所は GLUT - The OpenGL Utility Toolkit のページからたどることができます (glut-3.7.tar.gz / glut37.zip と glut_data-3.7.tar.gz / glut37data.zip). ただし, オリジナルの GLUT は, 1998 年に発表された Version 3.7 以来, 長い間メンテナンスされていません. 現在では, 代わりに freeglut という互換のツールキットが使用されることが多いようです (他に OpenGLUT というものもありますが, これも現在メンテナンスされていないようです).
ほとんどの Linux / FreeBSD 系の OS には, GLUT (おそらく freeglut) のパッケージが用意されていると思います. 使用する OS のパッケージマネージャを使ってインストールしてください.
(Vine) $ sudo apt-get install freeglut freeglut-devel (Debian, Ubuntu) $ sudo apt-get install freeglut3 freeglut3-dev (RedHat, Fedra) $ sudo yum install freeglut $ sudo yum install freeglut-devel
root の権限がないときは, freeglut のソースを入手して, 自分でコンパイルしてください. freeglut のバージョン 3 以降では, これに cmake を使います (渡辺 敏暢 様 情報ありがとうございました). cmake の使い方は, 陳 先生が「CMake の勧め」で説明されています (陳 先生 ありがとうございます). 下記の "インストール先" には, 自分が書き込み可能なディレクトリを指定してください. "-DCMAKE_INSTALL_PREFIX=インストール先" を省略した場合は, make install により /usr/local 以下にインストールされます.
$ tar xzf freeglut-3.Y.Z.tar.gz $ cd freeglut-3.Y.Z $ mkdir build $ cd build $ cmake -DCMAKE_INSTALL_PREFIX=インストール先 .. $ make $ make install
ここでもし "XInput.h が無い" というエラーでビルドに失敗するときは, freeglut-3.Y.Z/src/x11 にある fg_internal_x11.h の中で #include している X11/extensions/XInput.h を X11/extensions/XI.h に書き換えてください.
一方, freeglut のバージョン 2 では, ソースプログラムに含まれている configure スクリプトを使います. 下記の "インストール先" には, 自分が書き込み可能なディレクトリを指定してください. "--prefix=インストール先" を省略した場合は, make install により /usr/local 以下にインストールされます.
$ tar xzf freeglut-2.Y.Z.tar.gz $ cd freeglut-2.Y.Z $ ./configure --prefix=インストール先 $ make $ make install
オリジナルの GLUT をインストールする場合は, GLUT - The OpenGL Utility Toolkit のページからソースファイル (glut-3.7.tar.gz, glut_data-3.7.tar.gz) を取ってきてコンパイルしてください. ただし, このコンパイルに使用する xmkmf や imake は現在の X Window には含まれていないので, 別に入手してインストールしておく必要があります. また, その際は 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 $ xmkmf $ make Makefiles $ make includes $ make depend $ cd lib/glut $ make
上記の手順により glut-3.7/lib/glut/libglut.a が作成されます. これと glut-3.7/include/GL/glut.h を, GLUT を用いて作成するプログラムと同じディレクトリか, 他の適当なディレクトリに置いてください.
Linux などメーカーによって OpenGL が移植されていない UNIX 系 OS では, Mesa と呼ばれる OpenGL 互換のライブラリが X Window に組み込まれています. このソースプログラムは, SourceForge の Mesa3D プロジェクトから入手できます.
SGI から Mark Kilgard 氏を含む大量のエンジニアが NVIDIA に移籍したと思ったら, 1999 年 2 月には SGI が GLX (X Window の OpenGL 拡張) をオープンソース化して, XFree86 や X.Org でも GLX が使えるようになりました. 現在 NVIDIA や AMD (ATI) などのグラフィックスプロセッサメーカーが, OpenGL のハードウェアアクセラレーションが有効なドライバを提供しています. その他のメーカーも, それぞれにドライバを用意しているようです.
ただし, メーカーから提供されるドライバ (プロプライエタリドライバ) の中には, OS のカーネルを "汚染" するものがあるため, Linux のパッケージに標準的に含まれることはないようです. オープンソースのドライバについては, DRI (Direct Rendering Infrastracture) などを参照してください. また Utah-GLX にもいくつかのグラフィックスプロセッサのドライバがあります. ところで SGI は 2000 年の 1 月,ついに OpenGL のサンプルインプリメンテーションをオープンソース化してしまいました.
[New] NuGet を使えば,以下の手順でプロジェクトごとに freeglut を組み込むことができます.
この場合,以降の手順は不要です.OpenGL が使えるのは, OpenGL の DLL をインストールした Windows 95 および Windows 98 以降の OS です. Windows 95 の場合, OSR2 以降なら多分標準で入っているのではないかと思いますが, 無い場合は MS からダウンロードしてきてください. これは ftp://ftp.microsoft.com/softlib/MSLFILES/opengl95.exe にあります (いまさら Windows 95 を使うことは無いと思いますが).
GLUT を使えるようにするには, 以前は Nate Robins 氏が Windows 用のバイナリファイルを提供されていましたが, 現在はすでに提供を終了されているようです. ここでは freeglut をコンパイルして使う方法を説明します. 開発環境は Visual Studio 2013 を想定しています.
C++ Builder の場合は "えむっち" さんの へっぽこプログラマー日記が参考になります. Cygwin の場合は Cygwin で OpenGL / Glut を使う方法で詳しく解説されています. また, フリーの処理系の LCC-Win32 というのも使えるそうです (Using GLUT with LCC-Win32, 山本 秀一 先生 ご教示ありがとうございました). この他, 埼玉大学の櫻井 先生が OpenGL と GLUT を Windows9*/NT で使う方法について OpenGL の部屋に詳しくおまとめになっています.
Mac OS X では標準で OpenGL と GLUT が使えます. OS に標準で添付されている Developer Tools をインストールしてください. GLUT は Developer Tools に含まれています. ソースプログラムも公開されています (GLUT for Mac OS X). ただし, この Web ページのソースプログラムをそのままコンパイルできるようにするためには, /usr/local/include に GL というディレクトリを掘って, そこに GLUT のソースファイルに含まれている glut.h をコピーするか, そこから /System/Library/Frameworks/GLUT.framework/Headers/glut.h へのシンボリックリンクを張っておく必要があります.
$ sudo mkdir /usr/local/include $ sudo mkdir /usr/local/include/GL $ cd /usr/local/include/GL $ sudo ln -s /System/Library/Frameworks/GLUT.framework/Headers/glut.h .
この最後のシンボリックリンクを作成しない場合は, 以下のソースプログラムにおいて GL/glut.h ではなく GLUT/glut.h を #include するようにしてください (Mac OS X ではそうするのがスジでしょう).
ところで, Mac OS X 10.9 (Mavericks) / Xcode 5 以降で GLUT を使ったプログラムをコンパイルすると, "deplicated" (非推奨) という警告が出ます. このメッセージは以下の方法で抑制できます.
しかし, Mac OS X では OpenGL の Core Profile (OpenGL 3.2 より前の古い機能との互換性を維持しないモード) の使用を推奨しており, Compatibility Mode に依存した GLUT はもう使うべきではないのかも知れません. なお, GLUT の代替として使用できる GLFW は Mac OS X でも使用でき, Core Profile も選択できます.
このほか, Mac OS X 上の X Window 環境である XQuartz も GLX (OpenGL Extension) をサポートしており, この上でも OpenGL を使うことができます. GLUT も含まれています. freeglut を使用する場合は, 2.2と同様にソースからコンパイルしてください.
freeglut の場合は, cc (あるいは gcc) コマンドに -lglut -lGLU -lGL というオプションを追加するだけでコンパイルできます. この演習では, このほかに三角関数などの数学ライブラリも使用するので, さらに -lm というオプションも付ける必要があります.
$ cc program.c -lglut -lGLU -lGL -lm
オリジナルの GLUT の場合は, 以下のようなオプションを付ける必要があるかもしれません (使う可能性のあるライブラリを全部並べたので, 環境によっては不必要なものも含まれています).
$ cc -I/usr/X11R6/include program.c -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm -lpthread
GLUT のソースをコンパイルした場合は, 上のコマンドにおいて glut.h と libglut.a を置いた場所をオプションで指定してください. 仮に, これらを作成するプログラムと同じディレクトリに置いたとすれば, "-I. -L." というオプションを追加します.
コンパイルの度に長いコマンドを打つのは面倒だと感じたら, 楽をする方法を考えましょう. これにはいくつかの方法が考えられます.
あらかじめ以下のようなコマンドを実行しておきます.
$ function ccgl() { cc -I/usr/X11R6/include "$@" -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm -lpthread; }
そうすると, 以降は以下のコマンドでコンパイル (&リンク) が行えます.
$ ccgl program.c
これを .bashrc の中に書いておけば, ログインする度に alias コマンドを実行する手間が省けます.
以下のような内容のファイル ccgl を作成してください.
#!/bin/sh exec cc -I/usr/X11R6/include "$@" -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm -lpthread
そのあと chmod コマンドを実行して, このシェルスクリプトを実行可能にします.
$ chmod +x ccgl
以降はこの ccgl コマンドを使ってコンパイルできます.
$ ./ccgl program.c
以下の内容の Makefile というファイルを作ります. "--Tab-->" のところは, タブ (Tab) を使って字下げしてください.
CFLAGS = -I/usr/X11R6/include LDLIBS = -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm -lpthread a.out: program.c --Tab-->$(CC) $(CFLAGS) program.c $(LDLIBS)
Makefile のあるディレクトリで make コマンドを実行すると, program.c がコンパイルされて a.out という実行ファイルが生成されます. なお, $(CC) は cc (または gcc) コマンドに置き換わります. なお, この場合 $(CC) の行は, 実は書かなくても大丈夫です.
$ make
このコマンドは emacs の中からも M-x compile で起動できます.
Makefile にはファイルの「生成規則」を記述します. make は実行すると, Makefile 中の最初の生成規則を探します. 上のファイルの場合, a.out の行がそれになります. この行には a.out というターゲットを生成するのに program.c が必要だという依存関係を記述しており, その次の行に実際に a.out を生成するための手続きを記述しています (注). この行の行頭は Tab 文字にしてください.
ターゲットが複数あるときは以下のようにします.
CFLAGS = -I/usr/X11R6/include LDLIBS = -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm -lpthread all: prog1 prog2 prog1: prog1.c --Tab-->$(CC) $(CFLAGS) prog1.c -o prog1 $(LDLIBS) prog2: prog2.c --Tab-->$(CC) $(CFLAGS) prog2.c -o prog2 $(LDLIBS)
この最初の生成規則は all の行で, all を生成するには prog1 と prog2 が必要だという依存関係を記述しています. しかし all の生成方法は記述していないので, make は prog1 と prog2 の両方の生成だけが完了した時点で終了します. 特定のターゲットだけを生成したいときは, そのターゲット名を make の引数に指定します.
$ make prog1
プロジェクトを新規作成する際に「Win32 コンソール アプリケーション」を選び, 「空のプロジェクト」を作成してください. Visual Studio の使い方を知らない人は, まずこちら (Visual Studio 2008) かこちら (Visual Studio .NET 2003) あるいはこちら (Visual C++ 6.0) を見てください.
以降で示すプログラムは, UNIX / Linux 系 OS 上での実行を前提に作成しています. Windows 上では「Win32 コンソール アプリケーション」のプロジェクトにすることで, これらのプログラムをそのまま Visual Studio 2008 でコンパイルできるようになります. プログラムの実行時にコンソールウィンドウを開きたくない場合は, ソースプログラムの先頭に次の 1 行を入れてください.
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
freeglut あるいは GLUT 3.7.2 以降と Visual Studio の組合わせなら, ソースファイルで GL/glut.h を #include することで, 自動的に freeglut.lib または glut32.lib, glu32.lib, および opengl32.lib がリンクされます (山下 真 様 ご教示ありがとうございました). したがって, このままビルドすれば実行ファイルができあがるはずです.
もしうまくいかないようなら, プロジェクトのプロパティ (Alt+F7) を開き, 「構成プロパティ」の「リンカ」の「入力」を選んで, 「追加の依存ファイル」に freeglut.lib または glut32.lib, glu32.lib, および opengl32.lib の三つを追加してください. このほか, 「C/C++」の「プリプロセッサ」を選んで, 「プリプロセッサの定義」に WIN32 が含まれていることを確かめてください (無いことは無いと思いますが). もしなければ追加してください.
プログラムをコマンドラインでコンパイルする場合は, cc (あるいは gcc) コマンドに以下のようなオプションを付けてください (榎本 剛 様 ご教示ありがとうございました).
$ cc program.c -framework GLUT -framework OpenGL
Mac OS X 10.9 (Maverics) 以降では, GLUT を使用したプログラムはコンパイル時に警告が出ます. この警告を抑制するには, -mmacosx-version-min=10.8 というオプションを追加してください.
できた a.out を実行すれば, ウィンドウが開きます. メニューも付いているはずです (メニューからスクリーンショットの保存ができると思います).
Developer Tools に含まれる Xcode を使う場合は, 既に用意されている GLUT のサンプルプログラムの (結合) プロジェクトにターゲットを追加するのが, 一番手っ取り早いように思います. 新たにプロジェクトを起こす場合は, 次のような手順になります.
詳しくはここ (Xcode 3) かここ (Xcode), あるいはここ (Project Builder) を参照してください. なお, 2.4で示したように /usr/local/include/GL 等に glut.h を置いていない場合は, ヘッダファイルとして GL/glut.h ではなく GLUT/glut.h を #include してください.
上記の手順が面倒な場合は, このファイルを展開してできる "GLUT Application" というフォルダを /Developer/Library/Xcode/Project Templates/Application というフォルダに入れれば, Xcode 3 で新規プロジェクトを作成するときに, "Application" のところに "GLUT Application" が現れます. ただし, これは見よう見まねで作ったので, 正しいのかどうかわかりません.
Mac OS X 上で X Window (X11) 用にプログラムをコンパイルするには, 3.1で述べた UNIX 系 OS の場合に準じます. cc コマンドに以下のオプションを追加してください.
$ cc -I/usr/X11R6/include program.c -L/usr/X11R6/lib -lglut -lGLU -lGL
いよいよプログラムの作成に入ります. ウィンドウを開くだけのプログラムは, 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)
int glutCreateWindow(char *name)
void glutDisplayFunc(void (*func)(void))
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, 1.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)
void glClearColor(GLclampf R, GLclampf G, GLclampf B, GLclampf A)
void glClear(GLbitfield mask)
glFlush(void)
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)
void glVertex2d(GLdouble x, GLdouble y)
描かれる図形は, (-0.9, -0.9) と (0.9, 0.9) の 2 点を対角線とする正方形です. これがウィンドウに対して「一回り小さく」描かれます. このウィンドウの大きさと図形の大きさの比率は, ウィンドウを各台縮小しても変化しません. これはウィンドウの x 軸と y 軸の範囲が, ともに [-1, 1] に固定されているからです.
glBegin() の引数 mode に指定できる図形のタイプには以下のようなものがあります. 詳しくは man glBegin を参照してください.
GL_POINTS
GL_LINES
GL_LINE_STRIP
GL_LINE_LOOP
GL_TRIANGLES / GL_QUADS
GL_TRIANGLE_STRIP / GL_QUAD_STRIP
GL_TRIANGLE_FAN
GL_POLYGON
OpenGL を処理するハードウェアは, 実際には三角形しか塗り潰すことができません (モノによっては四角形もできるものもあります). このため GL_POLYGON の場合は, 多角形を三角形に分割してから処理します. 従って, もし描画速度が重要なら 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)
図形を塗りつぶしてみます. 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, 1.0); } int main(int argc, char *argv[]) { /* 変更なし */ }
色は頂点毎に指定することもできます. glBegin() の前の glColor3d() を消して, かわりに四つの glVertex2d() の前に glColor3d() を置きます. prog1.c を以下のように変更してください. コンパイルしてプログラムを実行すると, どういう色の付き方になったでしょうか?
#include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); 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)
void glLoadIdentity(void)
void glOrtho(GLdouble l, GLdouble r, GLdouble b, GLdouble t, GLdouble n, GLdouble f)
glutReshapeFunc(void (*func)(int w, int h))
resize() の処理によって, プログラムは glViewport() で指定した領域に glOrtho() で指定した領域内の図形を表示するようになります. ここで glOrtho() で指定するの領域の大きさをビューポートの大きさに比例するように設定すれば, 表示内容の大きさをビューポートの大きさにかかわらず一定に保つことができます. ここでビューポートの大きさは開いたウィンドウの大きさと一致させていますから, ウィンドウのリサイズしても表示内容の大きさを一定に保つことができます.
図形はワールド座標系と呼ばれる空間にあり, その 2 点 (l, b), (r, t) を結ぶ線分を対角線とする矩形領域を, 2 点 (-1, -1), (1, 1) を対角線とする矩形領域に投影します. この投影された座標系を正規化デバイス座標系 (あるいはクリッピング座標系) と呼びます.
この正規化デバイス座標系の正方形領域内の図形がデバイス座標系 (ディスプレイ上の表示領域の座標系) のビューポートに表示されますから, 結果的にワールド座標系から glOrtho() で指定した矩形領域を切り取ってビューポートに表示することになります.
ワールド座標系から切り取る領域は, "CG用語" 的には「ウィンドウ」と呼ばれ, ワールド座標系から正規化デバイス座標系への変換は 「ウィンドウイング変換」と呼ばれます. しかしウィンドウシステム (X Window, MS Windows 等) においては, 「ウィンドウ」はアプリケーションプログラムが ディスプレイ上に作成する表示領域のことを指すので, ここの説明ではこれを「座標軸」と呼んでいます. なお, 正規化デバイス座標系からデバイス座標系への変換はビューポート変換と呼ばれます.
glOrtho() では引数として l, r, t, b の他に n と f も指定する必要があります. 実は OpenGL は二次元図形の表示においても内部的に三次元の処理を行っており, ワールド座標系は奥行き (Z) 方向にも軸を持つ三次元空間になっています. n と f には, それぞれこの空間の前方面 (可視範囲の手前側の限界) と後方面 (可視範囲の遠方の限界) を指定します. n より手前にある面や f より遠方にある面は表示されません.
二次元図形は奥行き (Z) 方向が 0 の三次元図形として取り扱われるので, ここでは n (前方面, 可視範囲の手前の位置) を -1.0, f (後方面, 遠方の位置) を 1 にしています.
glOrtho() を使用しなければ変換行列は単位行列のままなので, ワールド座標系と正規化デバイス座標系は一致し, ワールド座標系の 2 点 (-1, -1), (1, 1) を対角線とする矩形領域がビューポートに表示されます. ビューポート内に表示する空間の座標軸が変化しないため, この状態でウィンドウのサイズを変化させると, それに応じて表示される図形のサイズも変わります. 初期状態はこのようになっています.
表示図形のサイズをビューポートの大きさにかかわらず一定にするには, glOrtho() で指定するの領域の大きさをビューポートの大きさに比例するように設定します. 例えばワールド座標系の座標軸が上記と同様に l, r, t, b, n, f で与えられており, もともとのウィンドウの大きさが W×H, リサイズ後のウィンドウの大きさが w×h なら, glOrtho(l * w / W, r * w / W, b * h / H, t * h / H, n, f) とします. 上のプログラムでは, ワールド座標系の 2 点 (-1, -1), (1, 1) を対角線とする矩形領域を 200×200 の大きさのウィンドウに表示した時の表示内容の大きさが 常に保たれるよう設定しています.
プログラムの起動時に開くウィンドウの位置やサイズを指定したいときは, 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)
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))
プログラムが変更できたら, コンパイルしてプログラムを実行してみてください. 開いたウィンドウの上でマウスのボタンをクリックしてみてください. 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)
前のプログラムでは, ウィンドウのサイズを変えたり ウインドウが他のウィンドウに隠されたあと再び表示される度に, ウィンドウの中身が消えてしまいます. やはり, この場合もちゃんと書き直してやる必要があるわけですが, そのためにはそれまでに表示した内容を記憶しておかなければなりません.
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)
マウスのボタンを押しながらマウスを動かす操作を, ドラッグと言います. ドラッグ中はマウスの位置を継続的に取得する必要がありますが, 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_COLOR_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_COLOR_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)
glDisable(GLenum cap)
glLogicOp(GLenum opcode)
glutMotionFunc(void (*func)(int x, int y))
ラバーバンドを実現する場合, マウスを動かしたときに直前に描いたラバーバンドを消す必要があります. また, ラバーバンドを描いたことによって ウィンドウに既に描かれていた内容が壊されてしまうので, その部分をもう一度描き直す必要があります. しかし, そのために画面全体を書き換えるのは, ちょっともったいない気がします.
そこでラバーバンドを描く際には, 線を背景とは異なる色で描く代わりに, 描こうとする線上の画素の色を反転するようにします. こうすればもう一度同じ線上の画素の色を反転することで, そこに描かれていた以前の線が消えてウィンドウに描かれた図形が元に戻ります. このために glLogicOp() を使用します. glLogicOp() で指定した論理演算は, glEnable(GL_LOGIC_OP)<白黒の場合>あるいは glEnable(GL_COLOR_LOGIC_OP)<カラーの場合>で有効になります (陳 先生 ご指摘ありがとうございました).
ただし, マウスのボタンを押した直後はまだラバーバンドは描かれていませんから, そのときだけラバーバンドの消去は行わないようにしなければなりません. このため rubberband なんていう変数を使ったちょっと泥臭いプログラムになっていますが, 我慢してください (もっとエレガントな方法もありますけど ).
glutMotionFunc(), glutPassiveMotionFunc() で指定した関数は, マウスの移動にともなって頻繁に実行されるので, この関数の中で時間のかかる処理を行うと, マウスの応答が悪くなってしまいます. これを避ける方法は9節以降で解説します.
OpenGL のアプリケーションプログラムが開いたウィンドウには, ターミナルウィンドウのようにキーボード入力を行うことができません. そのかわりマウスのボタン同様, キーをタイプするごとに実行する関数を指定できます. それには 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 mouse(int button, int state, int x, int y) { /* 変更なし */ } void motion(int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { switch (key) { case 'q': case 'Q': case '\033': /* '\033' は ESC の ASCII コード */ exit(0); 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); glutMotionFunc(motion); glutKeyboardFunc(keyboard); init(); glutMainLoop(); return 0; }
glutKeyboardFunc(void (*func)(unsigned char key, int x, int y))
ファンクションキーのような文字キー以外のタイプを検出するときは glutSpecialFunc(), Shift や Ctrl のようなモディファイア (修飾) キーを検出するには glutGetModifiers() を使います. 使い方はいずれも man コマンドで調べてください.
これまでは二次元の図形の表示を行ってきましたが, OpenGL の内部では実際には三次元の処理を行っています. すなわち画面表示に対して垂直に Z 軸が伸びており, これまではその三次元空間の xy 平面への平行投影像を表示していました.
試しに5.4節で作成したプログラム (prog2.c) において, 図形を y 軸中心に 25 度回転してみましょう.
#include <GL/glut.h> void display(void) { glClear(GL_COLOR_BUFFER_BIT); 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) { glClearColor(1.0, 1.0, 1.0, 1.0); } 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)
コンパイルしたプログラムを実行して, 描かれる図形を見てください. Y 軸中心に回転しているため, 以前に比べて少し縦長になっていると思います.
このウィンドウを最小化したり他のウィンドウを重ねたりして, 再描画をさせてみましょう. 再描画する度に図形の形が変わると思います. これは変換行列に glRotated() による回転の行列が積算されるからです. これを防ぐには描画の度に変換マトリクスを glLoadIdentity() で初期化するか, 後で述べる glPushMatrix() / glPopMatrix() を使って変換行列を保存します.
それでは, こんどは以下のような三次元の立方体を線画で描いてみましょう. 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() は, 引数に三つの要素を持つ 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)
前のプログラムでは, 立方体が画面に平行投影されるため, 正方形しか描かないと思います. そこで現実のカメラのように透視投影をしてみます. これには 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)
glTranslated(GLdouble x, GLdouble y, GLdouble z)
ウィンドウをリサイズしたときに表示図形がゆがまないようにするためには, 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,4,5) の位置から原点 (0,0,0) を眺めますから, 立方体の A (0,0,0) の頂点がウィンドウの中心に来ると思います.
なお, gluPerspective(), gluLookAt() 等, glu*() で始まる関数は GL Utility ライブラリ (-lGLU) の関数です.
ここまでできたら, 今度はこの立方体を回してみましょう. それにはちょっと工夫が必要です. アニメーションを行うには, 頻繁に画面の書き換えを行う必要があります. しかし glutMailLoop() は無限ループであり, glutDisplayFunc() で指定された関数は, ウィンドウを再描画するイベントが発生したときにしか呼び出されません.
したがってアニメーションを実現するには, このウィンドウの再描画イベントを連続的に発生させる必要があります. プログラム中でウィンドウの再描画イベントを発生させるには, glutPostRedisplay() 関数を用います. これをプログラムが「暇なとき」に繰り返し呼び出すことで, アニメーションが実現できます. プログラムが暇になったときに実行する関数は, glutIdleFunc() で指定します.
一つ注意しなければいけないことがあります. 繰り返し描画を行うには, 描画の度に座標変換の行列を設定する必要があります.
ところで座標変換のプロセスは,
という四つのステップで行われます. 今行おうとしている図形を回すという変換は, 「モデリング変換」に相当します.
これまではこれらを区別 せずに取り扱ってきました. すなわち, これらの投影を行う行列式を掛け合わせることで, 単一の行列式として取り扱ってきたのです.
しかし図形だけを動かす場合は, モデリング変換の行列だけを変更すればいいことになります. また, 後で述べる陰影付けは, 透視変換を行う前の座標系で計算する必要があります.
そこで OpenGL では, 「モデリング変換−ビューイング変換」の変換行列 (モデルビュー変換行列) と, 「透視変換」の変換行列を独立して取り扱う手段が提供されています. モデルビュー変換行列を設定する場合は glMatrixMode(GL_MODELVIEW), 透視変換行列を設定する場合は glMatrixMode(GL_PROJECTION) を実行します.
カメラの画角などのパラメータを変更しなければ, 透視変換行列を設定しなければならないのはウィンドウを開いたときだけなので, これは resize() で設定すればよいでしょう. あとは全てモデリング−ビューイング変換行列に対する操作なので, 直後に glMatrixMode(GL_MODELVIEW) を実行します.
カメラ (視点) の位置を動かすアニメーションを行う場合は, 描画のたびに gluLookAt() によるカメラの位置や方向の設定 (ビューイング変換行列の設定) を行う必要があります. 同様に物体が移動したり回転したりするアニメーションを行う場合も, 描画のたびに物体の位置や回転角の設定 (モデリング変換行列の設定) を 行う必要があります. したがって, これらは display() の中で設定します.
マウスの左ボタンをクリックしている間, 立方体が回転するようにします. ついでに右ボタンをクリックすると立方体が 1 ステップだけ回転し (関谷 先生 ご指摘ありがとうございました), 'q', 'Q', または ESC キーでプログラムが終了するようにします. 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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 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_DOWN) { /* アニメーション開始 */ glutIdleFunc(idle); } else { /* アニメーション停止 */ glutIdleFunc(0); } break; case GLUT_RIGHT_BUTTON: if (state == GLUT_DOWN) { /* コマ送り (1ステップだけ進める) */ glutPostRedisplay(); } break; default: break; } } void keyboard(unsigned char key, int x, int y) { switch (key) { case 'q': case 'Q': case '\033': /* '\033' は ESC の ASCII コード */ exit(0); 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); glutKeyboardFunc(keyboard); init(); glutMainLoop(); return 0; }
void glutPostRedisplay(void)
void glutIdleFunc(void (*func)(void))
void glMatrixMode(GLenum mode)
前のプログラムでは毎回画面を全部描き換えているため, 表示がちらついてしまいます. これを防ぐためには, ダブルバッファリングという方法を用います. これは画面を二つに分け, 一方を表示している間に (見えないところで) もう一方に図形を描き, それが完了したらこの二つの画面を入れ換える方法です.
GLUT でダブルバッファリングを使うには, glutInitDisplayMode() に GLUT_DOUBLE の指定を追加します. また, 図形の描画後に実行している glFlush() を glutSwapBuffers() に置き換えて, ここで二つの画面の入れ換えを行います.
それでは, 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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, 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); glutKeyboardFunc(keyboard); init(); glutMainLoop(); return 0; }
int glutSwapBuffers(void)
それでは, 次に立方体の面を塗りつぶしてみましょう. 面のデータは, 稜線とは別に以下のように用意します.
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 枚の四角形を描きます. 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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, 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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
void glColor3dv(const GLdouble *v)
でもこれだとなんか変な表示になるかもしれません. 前のプログラムではデータの順番で面を描いていますから, 先に描いたものが後に描いたもので塗りつぶされてしまいます. ちゃんとした立体を描くには隠面消去処理を行う必要があります.
隠面消去処理を行なうには glutInitDisplayMode() で GLUT_DEPTH を指定しておき, glEnable(GL_DEPTH_TEST) を実行します. こうすると, 描画のときにデプスバッファを使うようになります. このため, 画面 (フレームバッファ, カラーバッファ) を消去するときにデプスバッファも一緒に消去しておきます. それには glClear() の引数に GL_DEPTH_BUFFER_BIT を追加します.
デプスバッファを使うと, 使わないときより処理速度が低下します. そこで, 必要なときだけデプスバッファを使うようにします. デプスバッファを使う処理の前で 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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { glClearColor(1.0, 1.0, 1.0, 1.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); glutKeyboardFunc(keyboard); init(); glutMainLoop(); return 0; }
上のプログラムでは常にデプスバッファを使うので, 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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { glClearColor(1.0, 1.0, 1.0, 1.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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { glClearColor(1.0, 1.0, 1.0, 1.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) を実行する必要はありません.
それでは光源を二つにして, それぞれの位置と色を変えてみましょう. 最初の光源 (GL_LIGHT0) の位置を Z 軸方向の斜め上 (0, 3, 5) に, 二つ目の光源 (GL_LIGHT1) を x 軸方向の斜め上 (5, 3, 0) に置き, 二つ目の光源の色を緑 (0, 1, 0) にします. これらのデータはいずれも四つの要素を持つ GLfloat 型の配列に格納します. 四つ目の要素は 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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { glClearColor(1.0, 1.0, 1.0, 1.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); glLightfv(GL_LIGHT1, GL_SPECULAR, green); } int main(int argc, char *argv[]) { /* 変更なし */ }
void glLightfv(GLenum light, GLenum pname, const GLfloat *params)
陰影付けの計算はワールド座標系で行われるので, glLightfv() による光源の位置 (GL_POSITION) の設定は, 視点の位置を設定した後に行う必要があります. また, 上のプログラムの glRotate3d() より後でこれを設定すると, 光源もいっしょに回転してしまいます.
glLightfv() による光源の色の設定 (GL_DIFFUSE 等) は, 必ずしも display() 内に置く必要はありません. プログラムの実行中に光源の色を変更しないなら, glEnable(GL_DEPTH_TEST) や glEnable(GL_LIGHTING) 同様 init() の中で一度実行すれば十分です.
glLightf*() で設定可能なパラメータは, GL_POSITION や GL_DIFFUSE 以外にもたくさんあります. 光源を方向を持ったスポットライトとし, その方向や広がり, 減衰率なども設定することもできます. 詳しくは man glLightf を参照してください.
前の例では図形に色を付けていませんでしたから, 立方体はデフォルトの色 (白) で表示されたと思います. 今度はこの色を変えてみましょう. この場合も光源の時と同様に四つの要素を持つ GLfloat 型の配列を用意し, 個々の要素に色を R, G, B それに A の順に格納します. 四つ目の要素 (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_AMBIENT_AND_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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
void glMaterialfv(GLenum face, GLenum pname, const GLfloat *params)
図形に色を付けるということは, 図形の物理的な材質パラメータを設定することに他なりません. GL_DIFFUSE で設定する拡散反射係数が図形の色に相当します. GL_AMBIENT は環境光 (光源以外からの光) に対する反射係数で, 光の当たらない部分の明るさに影響を与えます. GL_DIFFUSE と GL_AMBIENT には同じ値を設定することが多いので, これらを同時に設定する GL_AMBIENT_AND_DIFFUSE が用意されています. GL_SPECULAR は光源に対する鏡面反射係数で, 図形表面の光源の映り込み (ハイライト) の強さです. GL_SHININESS はこの鏡面反射の細さを示し, 大きいほどハイライトの部分が小さくなります. この材質パラメータの要素は一つだけなので, 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_AMBIENT_AND_DIFFUSE, red); /* 図形の描画 */ cube(); /* モデルビュー変換行列の復帰 */ glPopMatrix(); glutSwapBuffers(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 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 keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
この図形に, もう一つ立方体を追加します. 二つ目の 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_AMBIENT_AND_DIFFUSE, red); /* 図形の描画 */ cube(); /* 二つ目の図形の描画 */ glPushMatrix(); glTranslated(1.0, 1.0, 1.0); cube(); glPopMatrix(); /* モデルビュー変換行列の復帰 */ glPopMatrix(); glutSwapBuffers(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
このように変換行列に変更を加えている部分を glPushMatrix() と glPopMatrix() の対ではさんで「入れ子構造」にすることによって, 動きの階層構造を表現できます.
なお, この例では二つ目の cube() より後ろでは何も描画しておらず, 最後の glPushMatrix() で最初に実行した glPushMatrix() で保存した内容を復帰しているため, この cube() をはさんでいる glPushMatrix() と glPopMatrix() は無くても結果は変わりません.
ではこの二つ目の cube() を, 一つ目の 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_AMBIENT_AND_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(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
この例では, 一つ目の glRotated() による回転が 両方の cube() に影響しているのに対し, 二つ目の glRotated() は二つ目の 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 }; 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_AMBIENT_AND_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_AMBIENT_AND_DIFFUSE, blue); cube(); glPopMatrix(); /* モデルビュー変換行列の復帰 */ glPopMatrix(); glutSwapBuffers(); /* 一周回ったら回転角を 0 に戻す */ if (++r >= 360) r = 0; } void resize(int w, int h) { /* 変更なし */ } void mouse(int button, int state, int x, int y) { /* 変更なし */ } void keyboard(unsigned char key, int x, int y) { /* 変更なし */ } void init(void) { /* 変更なし */ } int main(int argc, char *argv[]) { /* 変更なし */ }
ここからようやく実験の本題に入ります. 以下のテーマのうち, グループに割り当てられた課題を選んで実験してください. これまでのようにソースプログラムは明示しませんから, 自分で実装を考えてください.
人間が映像から立体感(奥行き)を知覚する要因には様々なものがありますが, その中のひとつに両眼視差があります. これは1つの対象を見るときでも右目と左目では見る角度が微妙に異なるため, それぞれの目の網膜に映る映像に違いが発生して, そこから奥行きを知覚する現象です.
これをテレビやコンピュータのディスプレイを使って再現するには, 右目と左目に別々の映像を見せる仕組みが必要になります. これにはヘッドマウンティッドディスプレイ (HMD) などのように右目と左目の直前に独立したディスプレイを置く方式や, 液晶シャッタメガネを使う時分割方式, 偏光メガネ方式, メガネを必要としない裸眼方式など, いくつかの方式があります.
実験1で作成したプログラムをもとにして, 両眼視差による立体視 (ステレオ表示) を行うプログラムを作成してください. 使用する実験機器に応じて, 以下のいずれかの方式でプログラムを作成してください.
OpenGL と GLUT を使って,立体視ディスプレイを活用したゲームなど(シューティング,レース,格闘,落ちもの等何でも)何か「動く」プログラムを作成してください.実験1・2のプログラムを拡張したものでも構いません.