GLUTによる「手抜き」OpenGL入門

和歌山大学 システム工学部 デザイン情報学科

床井浩平

この文書の位置づけ

この文書は学生実験のテーマ「VR実験」の参考資料の, GLUT を用いた OpenGL のチュートリアルです. 180 分× 2 日+αで実験部分に到達できると思います. ただし内容は不十分なので, 必要に応じて資料やオンラインマニュ アル等を参照してください. また間違いも含まれていると思います. コメントをお願いします. なお, このページはリンク&コピーフリーです. このディレクトリをまとめたものを ここ に用意していますので, ご自由にお使いください.

初版 1997/09/30, 最終更新 2014/12/11

目次

 1.はじめに
  1.1 なぜ GLUT か
  1.2 それ以前に, なぜ OpenGL か
 2.GLUT のインストール
  2.1 GLUT を入手する
  2.2 UNIX 系 OS にインストールする
  2.3 Windows 系 OS にインストールする
  2.4 Mac OS X にインストールする
 3.コンパイルの仕方
  3.1 UNIX 系 OS の場合
  3.2 Windows 系 OS (Visual Studio) の場合
  3.3 Mac OS X (Developer Tools) の場合
 4.ウィンドウを開く
  4.1 空のウィンドウを開く
  4.2 ウィンドウを塗りつぶす
 5.二次元図形を描く
  5.1 線を引く
  5.2 図形のタイプ
  5.3 線に色を付ける
  5.4 図形を塗りつぶす
  5.5 関数の命名法
 6.座標軸を設定する
  6.1 座標軸とビューポート
  6.2 位置やサイズを指定してウィンドウを開く
 7.マウスとキーボード
  7.1 マウスボタンをクリックする
  7.2 マウスをドラッグする
  7.3 キーボードから読み込む
 8.三次元図形を描く
  8.1 二次元と三次元
  8.2 線画を表示する
  8.3 透視投影する
  8.4 視点の位置を変更する
 9.アニメーション
  9.1 図形を動かす
  9.2 ダブルバッファリング
10.隠面消去処理
 10.1 多面体を塗りつぶす
 10.2 デプスバッファを使う
 10.3 カリング
11.陰影付け
 11.1 光を当ててみる
 11.2 光源を設定する
 11.3 材質を設定する
12.階層構造
実験1.基本実験
実験2.立体視の実験
実験3.自由課題

資料:

1.はじめに

1.1 なぜ GLUT か

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 ライブラリ, 一種の 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 (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). いくつかデモプログラムも入っています.

1.2 それ以前に, なぜ OpenGL か

シミュレーション結果の視覚化など, グラフィックスを専門としない人が グラフィックスプログラミングをしなければならないということは結構ありますよね. 私の記憶が正しければ, かつて (いつの話だ?) は 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 などの toolkit を使えば見栄えのいいものができると思います. でも, これらはあくまで「ユーザーインタフェース構築のための部品集」なので, これら自体はあまり「グラフィックスプログラミング」の役に立ちそうにありません.

OpenGL は三次元のグラフィックスライブラリですが, もちろん二次元の機能も持っています. なにより, これを使うと N88BASIC の LINE 文で図形を書いていた頃 (遠くなったなぁ) の気楽さで グラフィックスプログラミングができます (あくまで個人的な印象です). それでいて, (当たり前だけど) N88BASIC とは比較にならないほどいろんなことができます.

ということで, OpenGL と GLUT を組み合わせれば,

  1. UNIX 系 OS (Linux, FreeBSD 等を含む) と Windows と Mac のいずれでも動く,
  2. リアルタイムに三次元表示を行うプログラムが,
  3. とっても簡単に書けてしまう,

という三拍子そろったメリットが得られます.

iPhone OS や Android では, OpenGL から派生した OpenGL ES が使われています.

もちろん GLUT は, 本格的な GUI を持ったプログラムの開発には向いていません. しかし研究などで, 手早くグラフィックスのプログラムを仕上げないといけないという場合には, とても便利な組み合わせだと思います.

なお GUI については, オリジナルの GLUT 自体にも一応 mui (micro-UI) という toolkit が含まれています. このほか, GLUI という GLUT と C++ で書かれた toolkit がリリースされています. これについては, GLUIリファレンスマニュアル日本語版 に日本語で書かれた詳しい資料があります. また Clutter という OpenGL のほかに OpenGL ES でも使用できるマルチプラットホームの toolkit も開発されています.

これらと同様にマルチプラットホームで使える toolkit として, Qt ("キュート" と読む) や FLTK (Fast Light Toolkit), GLFW, SDL (Simple Directmedia Layer) などがあります. Qt は最近のグラフィックスアプリケーションの GUI toolkit として非常に人気があります. 非常に高機能ですが, 非常に巨大です. FLTK は GUI に特化していますが, 非常に軽量です. GLFW はマウスやキーボードのほか, ジョイスティックも入力デバイスとして使えます. SDL はジョイスティックなどの入力機器に加えて, 音声も扱うことができます. 一方, Linux などで使われる GTK という toolkit をベースにしたものに, GtkGLAreaGtkGLExt があります. その他の OpenGL と組み合わせて使える toolkit については, OpenGL.org GLUT-like Windowing Toolkits に紹介があります.

2.GLUT をインストールする

2.1 GLUT を入手する

オリジナルの GLUT のソースファイルの場所は GLUT - The OpenGL Utility Toolkit のページからたどることができます (glut-3.7.tar.gz / glut37.zipglut_data-3.7.tar.gz / glut37data.zip). Windows 版は Nate Robins 氏のサイト Nate Robins - OpenGL - GLUT for Win32 からも入手できます. ただしオリジナルの GLUT は, 1998 年に発表された Version 3.7 以来, 長い間メンテナンスされていません. 代わりに freeglutOpenGLUT という互換の toolkit が開発されています.

2.2 UNIX / Linux 系 OS にインストールする

ほとんどの 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 または OpenGLUT をソースからコンパイルしてインストールすることもできます. 下記の "インストール先" には, 自分が書き込み可能なディレクトリを指定してください. "--prefix=インストール先" を省略した場合は, make install により /usr/local 以下にインストールされます.

$ tar xzf freeglut-X.Y.Z.tar.gz
$ cd freeglut-X.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 を用いて作成するプログラムと同じディレクトリか, 他の適当なディレクトリに置いてください.

Mesa について

Linux などメーカーによって OpenGL が移植されていない UNIX 系 OS では, Mesa と呼ばれる OpenGL 互換のライブラリが X Window に組み込まれています. このソースプログラムは, SourceForge の Mesa3D プロジェクトから入手できます.

SGI から Mark Kilgard 氏を含む大量のエンジニアが NVIDIA に移籍したと思ったら, 1999 年 2 月には SGI が GLX (X Window の OpenGL 拡張) をオープンソース化して, XFree86X.Org でも GLX が使えるようになりました. 現在 NVIDIAAMD (ATI) などのグラフィックスプロセッサメーカーが, OpenGL のハードウェアアクセラレーションが有効なドライバを提供しています. その他のメーカーも, それぞれにドライバを用意しているようです.
ただし, メーカーから提供されるドライバ (プロプライエタリドライバ) の中には, OS のカーネルを "汚染" するものがあるため, Linux のパッケージに標準的に含まれることはないようです. オープンソースのドライバについては, DRI (Direct Rendering Infrastracture) などを参照してください. また Utah-GLX にもいくつかのグラフィックスプロセッサのドライバがあります. ところで SGI は 2000 年の 1 月,ついに OpenGL のサンプルインプリメンテーションをオープンソース化してしまいました.

2.3 Windows 系 OS にインストールする

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 氏のサイト Nate Robins - OpenGL - GLUT for Win32 などにある Windows 用のバイナリファイル (glut-3.7.x-bin.zip) を使うと簡単ですが, ここでは freeglut をコンパイルして使う方法を説明します. 開発環境は Visual Studio 2008 を想定しています.

  1. まず, freeglut のソースファイルを入手します. これは .tar.gz 形式のアーカイブファイルなので, lhaplus などの適当なアーカイバを使って展開してください.
  2. 展開したフォルダには "VisualStudio2008" と "VisualStudio2008Static" という二つのフォルダが含まれています.
    VisualStudio2008
    ダイナミックリンクライブラリ (DLL) を作成します. これを使って作成したプログラムは実行するために freeglut.dll というファイルが必要になりますが, freeglut が更新された場合に freeglut.dll を入れ替えるだけですみます. 通常はこちらを使います.
    VisualStudio2008Static
    スタティックリンクライブラリを作成します. これを使って作成したプログラムは単体で動作します. freeglut が更新されたときは, 作成したプログラムをリンクしなおす必要があります.
  3. "VisualStudio2008" のフォルダを開き, その中にある freeglut.sln というファイル (ソリューションファイル) をダブルクリックしてください.
  4. Visual Studio 2008 が起動したら, 「ビルド」のメニューにある「構成マネージャ」を開きます.
  5. 「アクティブ ソリューション 構成」を "Release" に切り替えます. 別に "Debug" のままでもかまわないと思うのですが, freeglut 自体をデバックすることはないと思うので, "Release" でビルドすることにします.
  6. 「アクティブ ショリューション プラットフォーム」は, 32 ビット版のライブラリを作成するなら "Win32" のままにしてください. 64 ビット版のライブラリを作成するには, ここから "新規作成" を選び, 「新しいプラットフォームを入力または選択してください」のところで "x64" を選んでください. 詳しくは "方法 : Visual C++ プロジェクトを 64 ビット プラットフォーム用に設定する" を参照してください.
  7. メニューの「ビルド」から「freeglut のビルド」を選んでください. コンパイルエラーがなければ, ライブラリファイルが作成されます.
  8. "Release" フォルダ (64 ビット版の場合は "x86" フォルダの中の "Release" フォルダ) の中にある freeglut.lib と freeglut.dll というファイルを, それぞれ次の場所にコピーします.
    freeglut.lib
    32 ビット版は "C:\Program Files\Microsoft SDKs\Windows\v6.0A\Lib" (Visual Studio 2010)
    "C:\Program Files\Windows Kits\8.0\Lib\win8\um\x86" (Visual Studio 2012)
    "C:\Program Files\Windows Kits\8.1\Lib\win8\um\x86" (Visual Studio 2013)
    64 ビット版は "C:\Program Files\Microsoft SDKs\Windows\v6.0A\Lib\x64"
    "C:\Program Files (x86)\Windows Kits\8.0\Lib\win8\um\x64" (Visual Studio 2012)
    "C:\Program Files (x86)\Windows Kits\8.1\Lib\win8\um\x64" (Visual Studio 2013)
    freeglut.dll
    OS が 32 ビット版なら "C:\Windows\System32"
    64 ビット版の OS に 32 ビット版の DLL を入れるなら "C:\Windows\SysWOW64"
    64 ビット版の OS に 64 ビット版の DLL を入れるなら "C:\Windows\System32"
    (64 ビット版の OS の "C:\Windows\System32" には 64 ビット版の DLL を入れます)
  9. 展開した freeglut のフォルダにある "include" フォルダの中の "GL" フォルダにあるヘッダファイルを下記の場所にコピーします.
    glut.h freeglut.h freeglut_std.h freeglut_ext.h
    "C:\Program Files\Microsoft SDKs\Windows\v6.0A\Include\gl" (Visual Studio 2010)
    "C:\Program Files (x86)\Windows Kits\8.0\Include\um\gl" (Visual Studio 2012)
    "C:\Program Files (x86)\Windows Kits\8.1\Include\um\gl" (Visual Studio 2013)

NT 系の OS (Windows NT / 2000 / XP Professional) では, そのまま配置すると配置したユーザ (Administrator 等) しか読み取りや実行が行えなくなってしまうことがあります. 配置後にこれらのファイルのプロパティを見て (ファイルをマウスの右ボタンでクリック), 「セキュリティ」のタブで「グループ名またはユーザー名」に「Users」と「Power Users」を追加し, それぞれの「アクセス許可」の「読み取り」と「読み取りと実行」に チェックマークを入れておいてください.

なお, C++ Builder の場合は "えむっち" さんの へっぽこプログラマー日記が参考になります. Cygwin の場合は Cygwin で OpenGL / Glut を使う方法で詳しく解説されています. また, フリーの処理系の LCC-Win32 というのも使えるそうです (Using GLUT with LCC-Win32, 山本 秀一 先生ご教示ありがとうございました). この他, 埼玉大学の櫻井 先生が OpenGL と GLUT を Windows9*/NT で使う方法について OpenGL の部屋に詳しくおまとめになっています.

2.4 Mac OS X にインストールする

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 に含まれている X Window 上でも, OpenGL / GLUT を使うことができます. Mac OS X で X Window を使用するには, 「アプリケーション」フォルダの中の「ユーティリティ」フォルダにある X11 というアプリケーション (X11.app) を起動します. X11.app は, 10.3 (Panther) 以降には標準で含まれています (カスタムインストールする必要があります). また, X11.app のほかに, オープンソースで開発されている XQuartz も使用できます.

いずれも GLX (OpenGL Extension) はサポートされています. また Snow Leopard には X11 用の GLUT も含まれています. もし, それ以前の OS で X11 に GLUT が含まれていない場合は, 2.2と同様にソースからコンパイルしてください. freeglut の場合は configure 時に環境変数 CFLAGS-I/usr/X11R6/include, 環境変数 LDFLAGS-L/usr/X11R6/lib を設定しておいてください.

$ tar xzf freeglut-X.Y.Z.tar.gz
$ cd freeglut-X.Y.Z
$ ./configure CFLAGS=-I/usr/X11R6/include LDFLAGS=-L/usr/X11R6/lib
$ make
$ sudo make install

オリジナルの GLUT の場合は, コンパイルした後 glut.h を /usr/local/include/GL に, libglut.a を /usr/local/lib に置いてください.

$ sudo mkdir /usr/local/lib
$ sudo mkdir /usr/local/include
$ sudo mkdir /usr/local/include/GL
$ cd glut-3.7
$ sudo cp lib/glut/libglut.a /usr/local/lib
$ sudo cp include/GL/glut.h /usr/local/include/GL

3.コンパイルの仕方

3.1 UNIX / Linux 系 OS の場合

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 を作る

以下の内容の 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

3.2 Windows 系 OS (Visual Studio) の場合

プロジェクトを新規作成する際に, 「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 が含まれていることを確かめてください (無いことは無いと思いますが). もしなければ追加してください.

3.3 Mac OS X (Developer Tools) の場合

プログラムをコマンドラインでコンパイルする場合は, 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 のサンプルプログラムの (結合) プロジェクトにターゲットを追加するのが, 一番手っ取り早いように思います. 新たにプロジェクトを起こす場合は, 次のような手順になります.

  1. 新規プロジェクトとして空のプロジェクトを作成する.
  2. 新規ターゲットとしてアプリケーションを追加する.
  3. GLUT.framework および OpenGL.framework の二つのフレームワークを追加する.
  4. ソースファイルを作成あるいは追加する.
  5. ビルドする.

詳しくはここ (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

4.ウィンドウを開く

4.1 空のウィンドウを開く

いよいよプログラムの作成に入ります. ウィンドウを開くだけのプログラムは, 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)
これは無限ループです. この関数を呼び出すことで, プログラムはイベントの待ち受け状態になります.

見れば分かる通り, プログラムは,

  1. 初期化して,
  2. ウィンドウを開いて,
  3. そのウィンドウ内に絵を描く関数を決めて,
  4. 何かことが起こるのを待つ.

という順になります. C 言語の教科書なんかに良く出てくる 「標準入出力を使ったプログラム」なんかと違うところは, 中心となる処理 (この場合 display()) を実行するタイミングが, ソースプログラムを見ただけでは何時なのかわからない, というところでしょうか.

最初に display() が実行されるのは, 初めてウィンドウが開いたとき, すなわち, glutMainLoop() が glutCreateWindow() の指示を受けてウィンドウの生成を完了したときになります. また, その後も, このウィンドウがほかのウィンドウに隠され再び現れたときのように, ウィンドウの再描画が必要になったときに実行されます.

上のプログラムでは display() の中身に何も記述していないため, display() が呼び出されても何も仕事をしません. 試しにこのウィンドウを移動したり, 他のウィンドウで隠したりしてみてください. ウィンドウの中の表示はおかしなものになっていると思います.

このように複数の (オーバーラップ可能な) ウィンドウが使用できるウィンドウシステムに対応したプログラムでは, 処理の流れは時間軸に沿って「プログラムの始めから終りへ」ではなく, 何かこと (事象) が起るたびに「プログラムの各部がランダムに」実行されます. 従って, そのプログラミングスタイルも, 「事象」に対して, その「対処方法」を登録していくというものになります. ここではこの事象をイベントと呼び, 対処方法の手続きをハンドラと呼ぶことにします.

なお, このプログラムには「終了する方法」を組み込んでいないので, プログラムを終了するには実行したウィンドウで Ctrl-C をタイプするか, ウィンドウを閉じてください.

4.2 ウィンドウを塗りつぶす

今までは関数 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)
ディスプレイの表示モードを設定します. mode に GLUT_RGBA を指定した場合は, 色の指定を RGB (赤緑青, 光の三原色) で行えるようにします. 他にインデックスカラーモード (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 が最も明るく, この三つに (0, 0, 0) を指定すれば黒色, (1, 1, 1) を指定すれば白色になります. 上の例ではウィンドウは青色で塗りつぶされます. 最後の A はα値と呼ばれ, OpenGL では不透明度として扱われます (0 で透明, 1 で不透明). ここではとりあえず 1 にしておいてください.
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 です.

5.二次元図形を描く

5.1 線を引く

ウィンドウ内に線を引いてみます. 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() は二次元の座標値を設定するのに使います. 引数の型は GLdouble (double と等価) です. 引数が float 型のときは glVertex2f(), int 型のときは glVertex2i() を使います.

描かれる図形は, (-0.9, -0.9) と (0.9, 0.9) の 2 点を対角線とする正方形です. これがウィンドウに対して「一回り小さく」描かれます. このウィンドウの大きさと図形の大きさの比率は, ウィンドウを各台縮小しても変化しません. これはウィンドウの x 軸と y 軸の範囲が, ともに [-1, 1] に固定されているからです.

ウィンドウの拡大縮小

5.2 図形のタイプ

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 を処理するハードウェアは, 実際には三角形しか塗り潰すことができません (モノによっては四角形もできるものもあります). このため GL_POLYGON の場合は, 多角形を三角形に分割してから処理します. 従って, もし描画速度が重要なら GL_TRIANGLE_STRIP や GL_TRIANGLE_FAN を使うよう プログラムを工夫してみてください. また GL_QUADS も GL_POLYGON より高速です.

5.3 線に色を付ける

線に色を付けてみます. 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() を使います.

5.4 図形を塗りつぶす

図形を塗りつぶしてみます. 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

5.5 関数の命名法

glVertex*() や glColor*() のような関数の * の部分は, 引数の型や数などを示しています. 詳しくは man glVertex2d や man glColor3d を参照してください.

関数の命名法概略図

6.座標軸を設定する

6.1 座標軸とビューポート

ウィンドウ内に表示する図形の座標軸は, そのウィンドウ自体の大きさと図形表示を行う "空間" との関係で決定します. 開いたウィンドウの位置や大きさはマウスを使って変更することができますが, その情報はウィンドウマネージャを通じて, イベントとしてプログラムに伝えられます.

これまでのプログラムでは, ウィンドウのサイズを変更すると表示内容もそれにつれて拡大縮小していました. これを表示内容の大きさを変えずに表示領域のみを広げるようにします.

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) を結ぶ線分を対角線とする矩形領域がここに表示されます. 最初の二つの引数 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() で指定するの領域の大きさをビューポートの大きさに比例するように設定すれば, 表示内容の大きさをビューポートの大きさにかかわらず一定に保つことができます. ここでビューポートの大きさは開いたウィンドウの大きさと一致させていますから, ウィンドウのリサイズしても表示内容の大きさを一定に保つことができます.

ウィンドウ−ビューポート変換

図形はワールド座標系と呼ばれる空間にあり, その 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 の大きさのウィンドウに表示した時の表示内容の大きさが 常に保たれるよう設定しています.

6.2 位置やサイズを指定してウィンドウを開く

プログラムの起動時に開くウィンドウの位置やサイズを指定したいときは, 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() より前に置き, 無効にするには後に置きます.

7.マウスとキーボード

7.1 マウスボタンをクリックする

マウスのボタンを押したことを知るには, 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() と同様に二次元の座標値を設定しますが, 引数の型が 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() と同様に二次元の座標値を設定しますが, 引数 v には 2 個の要素をもつ GLint 型 (int と等価) の配列を指定します. v[0] には x 座標値, v[1] には y 座標値を格納します. この例のように, 複数の点の座標を指定する場合に便利です.

7.2 マウスをドラッグする

マウスのボタンを押しながらマウスを動かす操作を, ドラッグと言います. ドラッグ中はマウスの位置を継続的に取得する必要がありますが, 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)
引数 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() を使用します. glLogicOp() で指定した論理演算は, glEnable(GL_LOGIC_OP)<白黒の場合>あるいは glEnable(GL_COLOR_LOGIC_OP)<カラーの場合>で有効になります (陳 先生ご指摘ありがとうございました).

ただし, マウスのボタンを押した直後はまだラバーバンドは描かれていませんから, そのときだけラバーバンドの消去は行わないようにしなければなりません. このため rubberband なんていう変数を使ったちょっと泥臭いプログラムになっていますが, 我慢してください (もっとエレガントな方法もありますけど…).

glutMotionFunc(), glutPassiveMotionFunc() で指定した関数は, マウスの移動にともなって頻繁に実行されるので, この関数の中で時間のかかる処理を行うと, マウスの応答が悪くなってしまいます. これを避ける方法は9節以降で解説します.

7.3 キーボードから読み込む

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))
引数 func には, キーがタイプされたときに実行する関数のポインタを与えます. この関数の引数 key にはタイプされたキーの ASCII コードが渡されます. また x と y にはキーがタイプされたときのマウスの位置が渡されます.

ファンクションキーのような文字キー以外のタイプを検出するときは glutSpecialFunc(), Shift や Ctrl のようなモディファイア (修飾) キーを検出するには glutGetModifiers() を使います. 使い方はいずれも man コマンドで調べてください.

8.三次元図形を描く

8.1 二次元と三次元

これまでは二次元の図形の表示を行ってきましたが, 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)
変換行列に回転の行列を乗じます. 引数はいずれも GLdouble 型 (double と等価) で, 一つ目の引数 angle は回転角, 残りの三つの引数 x, y, z は回転軸の方向ベクトルです. 引数が float 型なら glRotatef() を使います. 原点を通らない軸で回転させたい場合は, glTranslated() を使って一旦軸が原点を通るように図形を移動し, 回転後に元の位置に戻します.

コンパイルしたプログラムを実行して, 描かれる図形を見てください. Y 軸中心に回転しているため, 以前に比べて少し縦長になっていると思います.

このウィンドウを最小化したり他のウィンドウを重ねたりして, 再描画をさせてみましょう. 再描画する度に図形の形が変わると思います. これは変換行列に glRotated() による回転の行列が積算されるからです. これを防ぐには描画の度に変換マトリクスを glLoadIdentity() で初期化するか, 後で述べる glPushMatrix() / glPopMatrix() を使って変換行列を保存します.

8.2 線画を表示する

それでは, こんどは以下のような三次元の立方体を線画で描いてみましょう. 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)
glVertex3dv() は三次元の座標値を指定するのに使います. 引数 v は 3 個の要素を持つ GLdouble 型 (double と等価) 配列を指定します. v[0] には x 座標値, v[1] には y 座標値, v[2] には z 座標値を格納します.

8.3 透視投影する

前のプログラムでは, 立方体が画面に平行投影されるため, 正方形しか描かないと思います. そこで現実のカメラのように透視投影をしてみます. これには 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 はカメラの画角であり, 度で表します. これが大きいほどワイドレンズ (透視が強くなり, 絵が小さくなります) になり, 小さいほど望遠レンズになります. 二つ目の引数 aspect は画面のアスペクト比 (縦横比) であり, 1 であればビューポートに表示される図形の x 方向と y 方向のスケールが等しくなります. 三つ目の引数 zNear と四つ目の引数 zFar は表示を行う奥行き方向の範囲で, zNear は手前 (前方面), zFar は後方 (後方面) の位置を示します. この間にある図形が描画されます.
透視変換の視野
glTranslated(GLdouble x, GLdouble y, GLdouble z)
変換行列に平行移動の行列を乗じます. 引数はいずれも GLdouble 型 (double と等価) で, 三つの引数 x, y, z には現在の位置からの相対的な移動量を指定します. 引数が float 型なら glTranslatef() を使います.

ウィンドウをリサイズしたときに表示図形がゆがまないようにするためには, gluPerspective() で設定するアスペクト比 aspect を, glViewport() で指定したビューポートの縦横比 (w/h) と一致させます.

上のプログラムのように, リサイズ後のウィンドウのサイズをそのままビューポートに設定している場合, 仮に aspect が定数であれば, ウィンドウのリサイズに伴って表示図形が伸縮するようになります. したがって, ウィンドウをリサイズしても表示図形の縦横比が変わらないようにするために, ここでは aspect をビューポートの縦横比に設定しています.

8.4 視点の位置を変更する

前のプログラムのように, 視点の位置を移動するには, 図形の方を 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)
この最初の三つの引数 ex, ey, ez は視点の位置, 次の三つの引数 cx, cy, cz は目標の位置, 最後の三つの引数 ux, uy, uz は, ウィンドウに表示される画像の「上」の方向を示すベクトルです.

この例では (3,4,5) の位置から原点 (0,0,0) を眺めますから, 立方体の A (0,0,0) の頂点がウィンドウの中心に来ると思います.

なお, gluPerspective(), gluLookAt() 等, glu*() で始まる関数は GL Utility ライブラリ (-lGLU) の関数です.

9.アニメーション

9.1 図形を動かす

ここまでできたら, 今度はこの立方体を回してみましょう. それにはちょっと工夫が必要です. アニメーションを行うには, 頻繁に画面の書き換えを行う必要があります. しかし glutMailLoop() は無限ループであり, glutDisplayFunc() で指定された関数は, ウィンドウを再描画するイベントが発生したときにしか呼び出されません.

したがってアニメーションを実現するには, このウィンドウの再描画イベントを連続的に発生させる必要があります. プログラム中でウィンドウの再描画イベントを発生させるには, glutPostRedisplay() 関数を用います. これをプログラムが「暇なとき」に繰り返し呼び出すことで, アニメーションが実現できます. プログラムが暇になったときに実行する関数は, glutIdleFunc() で指定します.

一つ注意しなければいけないことがあります. 繰り返し描画を行うには, 描画の度に座標変換の行列を設定する必要があります.

ところで座標変換のプロセスは,

  1. 図形の空間中での位置を決める「モデリング変換」
  2. その空間を視点から見た空間に直す「ビューイング (視野) 変換」
  3. その空間をコンピュータ内の空間にあるスクリーンに投影する「透視変換」
  4. スクリーン上の図形をディスプレイ上の表示領域に切り出す「ビューポート変換」

という四つのステップで行われます. 今行おうとしている図形を回すという変換は, 「モデリング変換」に相当します.

これまではこれらを区別 せずに取り扱ってきました. すなわち, これらの投影を行う行列式を掛け合わせることで, 単一の行列式として取り扱ってきたのです.

しかし図形だけを動かす場合は, モデリング変換の行列だけを変更すればいいことになります. また, 後で述べる陰影付けは, 透視変換を行う前の座標系で計算する必要があります.

そこで 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)
再描画イベントを発生させます. このイベントの発生が発生すると, glutDisplayFunc() で指定されている描画関数が実行されます. なお, 再描画が開始されるまでの間にこのイベントが複数回発生しても, この描画関数は一度だけ実行されます.
void glutIdleFunc(void (*func)(void))
引数 func には, このプログラムが暇な (何もすることがない) ときに実行する関数のポインタを指定します. 引数の関数はプログラムが「暇になる」たびに繰り返し実行されます. この関数を指定すると, プログラムが止まっているように見えてもコンピュータの負荷は増大します. したがって glutIdleFunc() による関数の指定は必要になった時点で行い, 不要になれば glutIdleFunc() の引数に 0 または NULL を指定して関数の指定を解除してやる必要があります.
void glMatrixMode(GLenum mode)
設定する変換行列を指定します. 引数 mode が GL_MODELVIEW ならモデルビュー変換行列, GL_PROJECTION なら透視変換行列を指定します.

9.2 ダブルバッファリング

前のプログラムでは毎回画面を全部描き換えているため, 表示がちらついてしまいます. これを防ぐためには, ダブルバッファリングという方法を用います. これは画面を二つに分け, 一方を表示している間に (見えないところで) もう一方に図形を描き, それが完了したらこの二つの画面を入れ換える方法です.

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)
ダブルバッファリングの二つのバッファを交換します. glFlush() は自動的に実行されます. このプログラムでこれを使うとずいぶん遅くなるように見えますが, これはディスプレイのバッファの交換の時のちらつきを防ぐために, ディスプレイの表示タイミング (帰線消去期間) を待っているためです. ディスプレイのリフレッシュレートが 60Hz であれば, バッファの交換は 1/60 秒ごとに行われます. このプログラムは一周で 360 回再表示を行いますから, この場合一周するのに最短でも 6 秒かかることになります.

10.隠面消去処理

10.1 多面体をぬりつぶす

それでは, 次に立方体の面を塗りつぶしてみましょう. 面のデータは, 稜線とは別に以下のように用意します.

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)
glColor3dv() は glColor3d() と同様にこれから描画するものの色を指定します. 引数 v は三つの要素を持った GLdouble 型 (double と等価) の配列で, v[0] には赤 (R), v[1] には緑 (G), v[2] には青 (B) の強さを, 0〜1 の範囲で指定します.

でもこれだとなんか変な表示になるかもしれません. 前のプログラムではデータの順番で面を描いていますから, 先に描いたものが後に描いたもので塗りつぶされてしまいます. ちゃんとした立体を描くには隠面消去処理を行う必要があります.

10.2 デプスバッファを使用する

隠面消去処理を行なうには 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) の実行を省略しています.

10.3 カリング

立方体のように閉じた立体の場合, 裏側にある面, すなわち視点に対して背を向けている面は見ることはできません. そういう面をあらかじめ取り除いておくことで, 隠面消去処理の効率を上げることができます.

視点に対して背を向けている面を表示しないようにするには 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() による方法 (背面ポリゴンの除去) は, そのもっとも基本的なものです.

11.陰影付け

11.1 光を当ててみる

次は面ごとに色を付けるかわりに, 光を当ててみましょう. 陰影付け (光源の処理) の計算を行うためには, 面ごとの色の代わりに法線ベクトルを与えます. 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) を実行する必要はありません.

11.2 光源を設定する

それでは光源を二つにして, それぞれの位置と色を変えてみましょう. 最初の光源 (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)
光源のパラメータを設定します. 最初の引数 light には設定する光源の番号 (GL_LIGHT0〜GL_LIGHTn, n はシステムによって異なります) です. 二つ目の引数 pname は設定するパラメータの種類です. ここに GL_POSITION を指定すると光源の位置を設定します. また GL_DIFFUSE を指定すると光源の拡散反射光強度 (色) を設定します. 最後の引数 params は, pname に指定したパラメータの種類に設定する値です. pname が GL_POSITION あるいは GL_DIFFUSE のときは, params は四つの要素を持つ 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() より後でこれを設定すると, 光源もいっしょに回転してしまいます.

glLightfv() による光源の色の設定 (GL_DIFFUSE 等) は, 必ずしも display() 内に置く必要はありません. プログラムの実行中に光源の色を変更しないなら, glEnable(GL_DEPTH_TEST)glEnable(GL_LIGHTING) 同様 init() の中で一度実行すれば十分です.

glLightf*() で設定可能なパラメータは, GL_POSITION や GL_DIFFUSE 以外にもたくさんあります. 光源を方向を持ったスポットライトとし, その方向や広がり, 減衰率なども設定することもできます. 詳しくは man glLightf を参照してください.

11.3 材質を設定する

前の例では図形に色を付けていませんでしたから, 立方体はデフォルトの色 (白) で表示されたと思います. 今度はこの色を変えてみましょう. この場合も光源の時と同様に四つの要素を持つ 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)
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 は一つまたは四つの要素を持つ GLfloat 型 (float と等価) の配列で, 四つの要素を持つ場合 (GL_SHININESS, GL_COLOR_INDEXES 以外) は, 色の成分 RGB および A に対する係数を指定します. この初期値は (0.8, 0.8, 0.8, 1) ですが, 1 を越える値も設定できます.

図形に色を付けるということは, 図形の物理的な材質パラメータを設定することに他なりません. 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() で光源のこれらのパラメータも設定してやる必要があります.

12.階層構造

次に図形の階層構造を表現してみます. これまでのプログラムで実際に立方体を描いている部分を, 独立した関数 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.基本実験

ここからようやく実験の本題に入ります. 以下のテーマのうち, グループに割り当てられた課題を選んで実験してください. これまでのようにソースプログラムは明示しませんから, 自分で実装を考えてください.

実験2.立体視の実験

立体視のメカニズム

人間が映像から立体感(奥行き)を知覚する要因には様々なものがありますが, その中のひとつに両眼視差があります. これは1つの対象を見るときでも右目と左目では見る角度が微妙に異なるため, それぞれの目の網膜に映る映像に違いが発生して, そこから奥行きを知覚する現象です.

視差による網膜像の違い

これをテレビやコンピュータのディスプレイを使って再現するには, 右目と左目に別々の映像を見せる仕組みが必要になります. これにはヘッドマウンティッドディスプレイ (HMD) などのように右目と左目の直前に独立したディスプレイを置く方式や, 液晶シャッタメガネを使う時分割方式, 偏光メガネ方式, メガネを必要としない裸眼方式など, いくつかの方式があります.

実験1で作成したプログラムをもとにして, 両眼視差による立体視 (ステレオ表示) を行うプログラムを作成してください. 使用する実験機器に応じて, 以下のいずれかの方式でプログラムを作成してください.

実験3.自由課題

OpenGL と GLUT を使って,立体視ディスプレイを活用したゲームなど(シューティング,レース,格闘,落ちもの等何でも)何か「動く」プログラムを作成してください.実験1・2のプログラムを拡張したものでも構いません.