6. OpenGLプログラミング入門

【プログラミング序論3(2014年度)】 目次, 講義ページ, 授業科目, www.kameda-lab.org 2015/01/31c

ここからは、OpenGLを使った紙芝居風CGアニメーションプログラムの作成を想定しながら、プログラミング技術の習得を目指します。

OpenGLはCGをリアルタイム表示するためのプログラミングライブラリです。
皆さんには、数学ライブラリ以外の大規模なライブラリを使うおそらく初めての機会になります。

OpenGLライブラリはそれ自体よくできたライブラリなのですが、それだけでは実際に描画したりする部分のプログラミングが難しいという問題がありました。
そこで、OpenGLライブラリをより簡単に使えるようにOpenGL Utility (GLU)ライブラリが用意されています。
しかし、GLUライブラリの助けを借りてもまだウィンドウを表示してのレンダリングなどが難しいという問題があったため、さらにOpenGL Utility Toolkit (GLUT)ライブラリが用意されています。
(最初からGLUTレベルのものを作っておけば?というのは、正論ですけど、それは後から来た人たちの言う台詞ですね。)
(用意してくれたのは誰か、というと、そのライブラリを使っていた人達のうちの有志だったりします)

結局、三重にも積み重なったライブラリを利用することになります。

  1. OpenGL: CGを単に描き、かつハードウェアを有効に利用するためのライブラリ
  2. GLU: OpenGLの関数を組み合わせて、より抽象度の高い(使いやすい)関数を揃えたライブラリ
  3. GLUT: OpenGLからwindow作成・制御に伴う部分を簡単に使えるようにするためのライブラリ

ということで、単にOpenGLライブラリを使う、と言っても、実際には3層に重なったOpenGLライブラリ、GLUライブラリ、GLUTライブラリを使うことになります。
(当然、GLU使うにはGLが必要ですし、GLUTを使う場合にはGLとGLUの両方が必要ということになります。)

まずはOpenGLでのプログラミングの初歩を実習していきましょう。
(この先、単にOpenGLと呼びますが、実際にはGLUTまで込みのプログラミングです。)

参考リンク

OpenGL(ver.2.1)の各関数(小文字のglの2文字で始まる関数)の説明
OpenGLのProgramming Guide Book (ver1.1, 通称赤本)
GLUT - The OpenGL Utility Toolkit
The OpenGL Utility Toolkit (GLUT) Programming Interface API Version 3


06.01. はじめてのOpenGL - プログラムの枠だけ -

OpenGL/glutライブラリ(以後は単純にOpenGLと略します)を使ってCG描画のプログラムを作成するときは、glutライブラリが推奨するプログラミングスタイルに従う必要があります。
(従わずに独自のスタイルで書くことも不可能でありませんが、茨の道で間違いなし。)
(郷に入っては郷に従えと。)
glutライブラリを使ったプログラムでは、glutMainLoop()関数を中心としたイベント〜コールバック方式のプログラミングが推奨されています。

イベント処理ループ

ウィンドウを介してユーザとインタラクションするプログラムでは、しばしばイベント駆動型のアルゴリズムが使われます。
これまで学習してきた通常のプログラムは、main()関数の頭から順に処理して最後まで行って終了していました(フロー駆動型と呼ばれます)。

これに対して、イベント駆動型プログラミングによる実行では、イベント処理ループに入るまでに、まず、
「どのようなイベントが発生したらどんな処理をするか」
という約束事を、想定しうる様々なイベントに対して、全て決めておきます。

約束事の打ち合わせが済むと、プログラムはイベント待ちの無限ループに入ります。
無限ループを開始後は、イベントに応じて定められた動作をする、ということ以外一切しません。
glutライブラリによるプログラミングでは、この方式に沿ってプログラミングすることが前提となっています。
(それ以外のプログラミングスタイルも可能ではあるのですが、あまり想定されていないようです。)

プログラムの仕様

今回のプログラムの外部仕様は単純です。
1. 無限ループ開始前の準備
2. 無限ループ開始
(「どんなイベントにも反応したくない」ということですね。)

プログラムを以下に示します。
06-01-Preparation.c

ic2_BootWindow()関数で初期化をしています。
以後、本授業で定義する関数名はic2_で始まるように決めておきます。
(こういう約束を ネーミングスキーム/naming schemeと言います。大規模なプログラムを書いていく上で、単純な、でも重要な技術です)
(2013年度までは本授業は「計算機序論2(Introductin to Computing 2)」の一部でした。その名残でip2という接頭辞を使っています)
(こういうのが折り重なっていくのが文化です。受け継ぐ側が由来を知らないままでいると、「なぜ?」ということになるわけですが‥)
(例えば日本語でなんで感謝表現が「ありがとうございます」だったり別れの言葉が「さようなら」だったりするのでしょうね‥)
ic2_BootWindow()でもっとも重要な役割はglutCreateWindow()を使ってwindowを立ち上げることです。
windowには名前がつけられるので、コマンドラインでの第0引数(=argv[0]=コマンド名)を指定するようにしています。
他には、glutInitDisplayMode()で、CG描画のスタイルを指定しています。
GLUT_DOUBLEで、描画にバッファを2枚用意することを宣言しています。
(これをダブルバッファリングと言います。)
(舞台などで回転舞台を用意して、裏でいろいろ準備して整ったらぐるっと180度回転して一気に場面転換するのと概念は同じです)
(当然、舞台上で黒子たちがいろんな準備するのに要する時間より、舞台の回転にかかる時間のほうがずっと短いというのがこの方式を採用するための前提です)

(ところで、なぜ私はスラスラと「OpenGLでのプログラミングではこの関数をこう使って‥という知識が出てくるのでしょうね?)
(もちろん私はOpenGLの開発者でもなければGLU/GLUTの開発者でもありません。そういう人たちにお会いしたこともありません。)
(開発者の人たちが書き残した、「こう使ってほしい」という文書を一通り読んで知識をつけているわけです。)
(06.章の最初に記したリンクの数々がその文書に相当します。)
(その上で、要点だけをかいつまんで皆さんに教えています。)
(将来、皆さんが何か新しいライブラリを使うことがあれば、そのときは皆さん自身でそういう文書を読み通して、どう書くべきかの知識をつけていくことが必要になります。)

glutMainLoop()というイベント待ち無限ループに入る前に、「イベント〜動作」のペアを指定する必要がありますが、このプログラムでは何も指定していません。
そのため、プログラムは何もしない(window用に確保したバッファをクリアすらしない)ので、windowの枠が現れるだけで無限ループに入ります。
(ほんとに何もしないので、Window内を黒く塗りつぶしたりすることすらしません。塗りつぶすのだってちょっとは手間がかかるのです。)
(結果として、現在のデスクトップ表示がそのまま引き継がれたような形になります〔居抜き状態ですね〕。)
(もちろん、このウィンドウの中で綺麗にCGを描くのが目標なので、次節以降はそれについて学習していきます。)

コンパイルとリンクの概念

OpenGLのライブラリは実は大規模なもので、コンパイルとリンクには本来注意が必要です。
ただ、よく使われるライブラリでもあるので、整理が進んでいます。

コンパイル時:
必要なのは #include <GL/glut.h> としてGL/glut.hをインクルードすることだけです。
(実際には /usr/include/GL/glut.h に存在します。)
(試しに /usr/include/GL/glut.h の中を覗くと、/usr/include/GL/freeglut_std.h をインクルードしなさいと書いてあります。)
(さらに /usr/include/GL/freeglut_std.h を覗くと、いろいろあったり、/usr/include/GL/gl.hや/usr/include/GL/glu.hをインクルードしなさいと書いてあったりします。)
(さらに /usr/include/GL/gl.h を覗くと、‥以下略しますがコンパイラはコンパイル時にこの作業をすべてします。)
(なお、この説明内容はOpenGLのライブラリのインストール方法に依って変わります。)
(ここでの説明はこの授業で使う工学システム学類5階計算機室の2011年度でのlinux環境に沿ったものです。)

リンク時:
本来は OpenGL, GLU, GLUT やそれらが使うライブラリ全てを指定する必要があるのですが、実際には、libglut という外部ライブラリ1つを明示的にリンク時に示すだけで済みます。
つまり、gccの引数としては、 -lglut だけが必要ということになります。
(こんなに単純なのは、リンク作業をするgccに、いろんな予備知識をつけてあげてあるからですね。)
(「予備知識」と簡単に言ってますが、実際には皆さんが使っているlinuxシステムにOpenGL/GLU/GLUTのライブラリを導入するときに、gccリンカに対して「どこに」ヘッダファイル群を配置して、「どこに」ライブラリファイル群を配置したかを伝えておいてあるのです)
(その知識を用いて、リンク時にリンカとしてのgccが依存関係を解析して、必要な外部ライブラリを自動的に付け足してくれます。)
(ここでの説明は、この授業で使う工学システム学類5階計算機室のlinux環境に沿ったもので、その環境でのみ有効です。)
(何故こんな断りを入れるかというと、『予備知識のつけてあげかた』はOpenGL/GLU//GLUTの計算機システムへの導入方法が違えば、当然異なる可能性があるからです。)
(例えば、2012年度までの5階計算機システムでは、-lglut の他に -lGLU の指定が必要でした。前のシステムでは「glut使うなら当然GLUも使うだろ?」というちょっとした知識が足りてなかったんですね。)

コンパイルとリンクの作業

[1] コマンドライン
単一ソースファイルでOpenGLのプログラムを書いているうちは、コマンドラインでの実行ファイル生成(=コンパイル&リンク)は次のようにしてできます。
04.03.節を復習しておいてください。
作業は ~/workspace-ip3/06-01のディレクトリを用意して実施しましょう。

$ gcc -Wall -o 06-01 06-01-Preparation.c -lglut
$ ./06-01

[2] eclipse
前節との違いは、リンクの引数をeclipseに伝える必要があることです。
プロジェクト名は 06-01 としましょう。

06-01-A 新規プロジェクトの登録 (05-04-02-A参照)
1 メニュー:ファイル→新規→C プロジェクト
2 <C プロジェクト>
プロジェクト名:06-01
プロジェクト・タイプ:実行可能[展開]→空のプロジェクト
ツール・チェーン:Linux GCC
→完了
(「デフォルト・ロケーションの使用」にチェックが入っていることも確認してください)
(グレイアウトされているロケーションが .../workspace-ip3/06-01 になっていきます)
("Directory with specified name already exists."と警告されますが、確信犯なので無視して進みます)
3 "06-01"プロジェクトが選択されていることを確認してリフレッシュ(F5)
4 メニュー:ファイル→プロパティ
5 <06-01のプロパティー>ウィンドウ
左:C/C++ビルド[展開]→設定
右:ツール設定→GCC C Linker[展開]→ライブラリー
右:ライブラリー(-l)に「glut」を追加
→OK

06-01-B 「構成およびデバッグ」(05-04-05-A参照)
1 プロジェクトビューで実行時デバッグをしようと思っているプロジェクトが選択されていることを確認
2 メニュー:実行→デバッグの構成
3 <デバッグ構成>
左:C/C++ Applicatinをダブルクリック
左:C/C++ Applicatinの下に増える"06-01 Debug"を選択
右:環境→設定する環境変数で新規
4 <新規環境変数>
名前:DISPLAY
値::0.0
→OK
5 <デバッグ構成>
→適用
→デバッグ

このあと、05-04-07-A同様にステップ実行すると、順調に進むのですが、glutMainLoop()に到達した時点で進まなくなります。

06-03-C オンラインデバッグの強制停止
1 a. メニュー:実行→終了
b. 左上のアイコン並びのあたりの停止ボタンをクリック

停止させたあとは、05-04-08-Aと同じ手順で実行時インスタンスを削除しておきましょう。

実行ファイルと動的ライブラリとの関係

指定しなかった(リンカの自動解析に任せた)外部ライブラリの多くは動的ライブラリとして実行ファイルに結合されます。
(GL, GLU, glut をはじめ、X windowシステムと連携するためのライブラリとかが山のようにくっついていることがわかります)
(4章までのプログラムが依存する動的ライブラリの規模と比べてみてください)
(コマンドラインで作成した実行ファイルとeclipseで作成した実行ファイルは何か違いがあるでしょうか?)

$ cd ~/workspace-ip3/06-01
$ ls -l 06-01 Debug/06-01
$ ldd 06-01
$ ldd Debug/06-01

演習

06-01-ex1: windowに自分の名前をつけてみましょう。アルファベット表記すること。(解答例:06-01-Preparation.c)

06-01-ex2: 01-02-HelloESYS-Better.c06-01-Preparation.cとについて、実行ファイルを生成し、用いられる動的ライブラリ群をリストにして、その違いを明らかにしなさい。また、その差の理由について、説明を試みなさい。

06-01-ex3: 06-03-Bのあとでステップ実行すると、glutMainLoop()以降ステップ実行ができなくなりますが、その理由を考えてみてください。

06-01-ex4: イベント駆動型プログラミングのプログラミングスタイルについて説明しなさい。

06-01-ex5: ダブルバッファリングがCG描画に有効な仕組みであることを原理説明を交えながら示しなさい。

06-01-ex6: ダブルバッファリングに代えて、トリプルバッファリングがもしあればそれはCG描画に有効でしょうか。考察とともに示しなさい。

06-01-ex7: プログラム中の GLUT_DOUBLE が実際に定義されているインクルードファイルとその中の行を示しなさい。

06-01-ex8: glutInitDisplayMode()関数の引数はなぜマクロを|でつなぐ形式で指定するのでしょうか。その理由を説明しなさい。
(ヒント:マクロの定義をヘッダファイルの中で探してみましょう)


06.02. はじめてのOpenGL - バッファを切り替えよう -

続いて、「イベント〜動作」を指定します。

なお、本節以降はeclipse上のプロジェクト名を特に記述しませんが、全て節番号にします。
本節であれば"06-02"がプロジェクト名になります。
ソースファイルも同名のディレクトリ内に展開してください。

プログラム解説

06-02-SwapBuffers.c
06-01からの差分

ic2_BootWindow()の中で、windowを開いたあと、このwindowに対して「イベント〜動作」を指定します。
と言っても、ここでは glutIdleFunc() という関数を使って、「暇だったら ic2_DrawFrame()という関数を呼び出して仕事しなさい」と指定するだけです。
(ここのidleは、車のアイドリングと同じ意味ですね)
このように、glutライブラリでは、「イベント〜動作」の動作指定の時には関数名を指定します。
このプログラムでは他に何のイベントも受け付ける予定がない(登録してない)ので、実際には常に暇になります。
つまり、イベント待ち無限ループ開始→暇→すぐic2_DrawFrame()実行→実行終わってイベント待ち無限ループに戻る→やっぱり暇→ic2_DrawFrame()実行→実行終わってイベント待ち無限ループに戻る→やっぱり暇→‥となり、実質的には常にic2_DrawFrame()を実行し続けている形になります。

ic2_DrawFrame()で最終的にすべきことは、1フレーム分のCG描画です。
まだCG物体の描画とかまでプログラムが進んでないので、ここではとりあえずバッファの下塗り(初期化)だけしておきます。
あと、glutSwapBuffers()で、それまで描画していたバッファと表示していたバッファとを瞬間的に切り替えます。

実行してみれば分かりますが、windowの中が黒くなっただけで、何かしてるの?という感じがするでしょう。
しかし、システムの負担は違ってきます。
前者では全然CPU使用率が上がりませんが、後者ではCPU使用率が上昇します。
デスクトップ→システム→システム管理→システム・モニタを立ち上げて、CPU使用率を見ながら、06.01.節のプログラム06.02.節のプログラムとを実行してみてください。
わかりにくければ、システムモニターの「プロセス」ペインか、別のターミナルで top というコマンドを実行してみてください。
(どちらでも内容は概ね同じです。システム内のプロセスの実行状況を、システムに負荷をかけているもの順に表示してくれます)
(このCPU使用率の上昇具合は環境によって大きく違います)
(2013年からのシステムは高速になった上にOSが賢くなったので違いがわかりにくくなりました‥)

イベントループ方式のプログラムでのデバッグ実行のコツ

glutMainLoop()を使うプログラムでは、05-04-07-A/B, 05-04-09のようにデバッグ実行しようとすると、glutMainLoop()を実行し始めた瞬間から無限ループに入ってEclipse側では何も変化が現れないようになり、かつ制御を取り戻せなくなります。
(厳密には、eclipse/gdb側からすると、glutMainLoop()が掛かれているソースファイルが見当たらない〔私たちが書いてないのでソースファイルもないのは当たり前〕ので、eclipseとしては現在地の表示のしようがないのですね)
このとき、次にユーザプログラム側(ユーザがソースコードを書いた部分)に実行が移るのは、ユーザが仕掛けたイベント・コールバックに従って、設定されたコールバック関数が呼び出されるときです。
そこで、デバッグ実行の前に、ユーザ側で(希望する全ての)コールバック関数の入口にブレークポイントを設定します。
デバッグ実行は、ブレークポイントを発見すると必ずそこで一旦停止して、ユーザに制御権を戻します。

06-02-C ブレークポイントの設定と実行
1 設定したい関数の行の左端(行番号の部分)をクリック
2 そのまま右クリック→ブレークポイントの切り替え
(チェックマークと青丸が付きます)
(チェックマークはこの行にブレークポイントが付いていることを示します)
(青丸はこのブレークポイントで止まる予定ということを示します)
他にも一時停止したい行があれば同様にしてブレークポイントを設定します。
2 a. F8で再開
b. F5/F6などでステップ実行
3 該当するブレークポイントまで到達すると停止します。
(該当行の先頭に矢印が来るのですぐわかります)

演習

06-02-ex1:06.01.節のプログラムではCPU利用率が実行前と実行中で(ほぼ)変化しない理由を説明しなさい。

06-02-ex2:06.02.節のプログラムではCPU利用率が実行前に比べて実行中はCPU利用率が上昇します。その理由を説明しなさい。このプログラム実行中、CPUは何をしているのでしょうか?
(余談ですが、2012年までのシステムでは1スレッドが100%近くまで上昇していました。2013年からのシステムは上昇具合が小さくなり数%程度のようですね。コンパイラとOSが進化してハードウェア利用効率が上昇していることが見て取れます。)

06-02-ex3:CPU負荷の監視中、先頭に現れる(システムに最も負荷をかけている)プロセスは何でしょうか?調べて、それが何をするプロセスであるか報告しなさい。

06-02-ex4:glutIdleFunc()は引数に「関数名」を渡します。C言語においてなぜこのようなことが可能か、"関数に通常の変数を引き渡す場合の説明"に関する知識を演繹して説明を試みなさい。
(ヒント:C言語では、関数自体も変数として扱われているのです!)


06.03. はじめてのOpenGL - バッファ切替の確認 -

CPU使用率が高いのに、ユーザに何も視覚的変化がないのは寂しいものです。
なんとか動いている様子がわかるようにしてみましょう。

ここでは、塗りつぶす色を時々変えることを考えます。
3回に1回は緑色に、残り2回は従来通り黒色にしてみます。

プログラム解説

06-03-SwapBuffersCheck.c
06-02からの差分

ic2_DrawFrame()内にstatic整数変数を用意して、今が何回目の描画かを数えて記憶しています。

さて、期待としては緑→黒→黒→緑→黒→黒→緑→黒→黒→緑→黒→黒→という色変化が見えるはずですが‥?
んん?見えてますか?そうでもない?どうでしょうね?

【注意:下記の作業は、2012年度までの工学システム学類5階計算機システムでかなり高い確率でシステムをフリーズさせるようです。実施する場合は少なくともeclipse等エディタは終了させておきましょう】
(2013年度からのシステムで動作確認するの忘れてました‥大丈夫かな?)
動体視力に自信のない人は、コマンドラインで実行して、Ctrl-z と fg とを繰り返してみましょう。
(新しく生成したウィンドウではなく、プログラムを起動した端末ウィンドウ上でキーを入力してください)
Ctrl-zでプログラムが一時停止します。
(Ctrlキーを押しながらzキーですよ、よもやとは思いますが念のため)
fgと打つと、一時停止していたプログラムの実行が再開します。
(確率的には33%で緑画面を見れるはず。目押しでCtrl-z出来る人がいたら自慢していいでしょう)
(終了させるにはもちろんCtrl-cですね)

演習

06-03-ex1: 上記プログラムを変更して、5回に1回緑になるようにしなさい。

06-03-ex2: 上記プログラムを変更して、10回に1回赤になるようにしなさい。

06-03-ex3: 上記プログラムを変更して、緑→黒→黒→黒→白→黒→黒→黒にしてみなさい。

06-03-ex4: 現在使用しているデスクトップ環境で、モニタのリフレッシュレート(frame per second, fps)を調べなさい。

06-03-ex5: 上記プログラムを変更して、ic2_DrawFrame()関数が06-03-ex4のレート×100回実行されたら終了するように改良しなさい(60Hzなら6000回)。

06-03-ex6: 06-03-ex5のプログラムを実施し、実行時間を秒単位で計測しなさい。(ごく簡単な実行時間の計測は、bash上で date; ./06-03-ex5; date のようにすれば可能です)実行には100秒かかると予想されますが、なぜ100秒と予想するのか説明しなさい。

06-03-ex7: 06-03-ex6の計測結果がもし100秒ではない場合、その原因について説明を試みなさい。


06.04. はじめてのOpenGL - バッファを時間で切り替え -

前節では、glutに「暇だったらic2_DrawFrame()」と指定していましたが、この「暇だったら」が実は曲者です。
linuxで使っているXサーバは(環境にもよりますが)デスクトップ全体について、60Hzぐらいの頻度で描画し直しています。
(Xサーバとは、皆さんに対してGUIを提供しているlinux上のprocessのことです)
(皆さんが実行しているOpenGLのサンプルプログラムは、実際には描画をするときにはXサーバに表示依頼をしています)
今回のように、ic2_DrawFrame()での作業量が小さい場合はその実行に時間がかかりません。
ということは、「暇なのでic2_DrawFrame()を実行してみた」らまだ1/60秒も経ってなかった。そこでまた「暇なので(さらにもう一回)ic2_DrawFrame()を実行してみた」ということが1/60秒が経つまでに発生する可能性があります(もしかするとさらにもう一回?二回?)。

また、この方式では描画時間間隔も一定しません。
(ic2_DrawFrame()関数の負荷が同じでも、OS上で他に重いジョブが走り始めたりすると1/60秒のうちに何回当該関数を実行できるかが変化してきます)
(細かいことを言えば、上記と逆で、「次に暇」になってみたら1/60秒以上経っていた、という可能性はもちろんあります。いわゆる処理落ちという状態ですね。残念ながらデスクトップ描画のスピードは一定なので、1/60秒内に間に合わなければ次は2/60秒後が次の描画切り替えのチャンスということになります)
(5階の計算機は本講義で1フレームで描いてもらおうとする分量ぐらいなら瞬時に〔1/60秒よりはるかにはるかに短い時間で〕終えてしまうので、当面処理落ちの方を考える必要はありません)

そこで、指定するイベントの種類を時間ベース(タイマー方式)のものに変更します。
glutライブラリの推奨流儀(書き方)に従って、以下のようにします。

(1) まずglutDisplayFunc()で、(なんにせよ)「描画するならこの関数」ということを指定します。(これ自身はイベント〜動作の定義とは関係ありません)
(2) glutTimerFunc()で、「今からt[ms]後に起動したい関数(ここではic2_timerhandler()関数)を指定」します。
(「時間が来たら」というのも立派なイベントの一種です)
(3) その(2)で起動する(ic2_timerhandler())関数の中で、「次にチャンスがあったときには再描画させる」ことを、glutPostRedisplay()関数を呼び出すことで指示します。
(ここで気をつけてほしいのは、この関数内部では描画は一切しないまま処理を終えてしまうことです。ここはあくまで「次はいつにどうするか」を決めているだけです)
(4) 実際には「次のチャンス」とは、イベント待ち無限ループにもどってループを1週して、OSが「glutPostRedisplay()によってついさっき描画依頼を受けてたし描き直すぜ」と作業を始めるタイミングのことです。
(一見面倒なこの仕組みの利点は、再描画してほしいという(3)のリクエストがごく短時間のうちに複数溜まったときに、それぞれで描画してしまうのではなくまとめて(4)の1回の描画だけに抑え込めることと、描画する関数をプログラミング上1つだけ用意しておけばいいようにしておけること、の2つですね)
(「リクエストがごく短時間のうちに複数溜まる(かもしれない)」例は、そのうちに出てくることでしょう)

プログラム解説

06-04-SwapByTimer.c
06-03からの差分

glutDisplayFunc(ic2_DrawFrame());によって、バッファに描画する場合は(原則として)ic2_DrawFrame()を使うという宣言をします(上記(1))。
glutTimerFunc(250, ic2_timerhandler, 0);によって、その時点から250[ms]後に ic2_timerhandler() 関数を(引数を0として)呼び出すよう予約しておきます(上記(2))。

イベント待ち無限ループに入った後、250[ms]後にic2_timerhandler()が実行開始されます。
ic2_timerhandler()関数内では、「次にチャンスがあったときには間違いなく再描画をさせる」という要請を、glutPostRedisplay()関数を呼ぶことで明示します(上記(3))。
そのあと、これから250[ms]後にまた自分を(ic2_timerhandler()関数を)呼び出すよう予約しておきます(上記(2))。

ic2_timerhandler()の実行が済んでイベント待ち無限ループに戻ってきた後、描画できるタイミングが来た瞬間に(多くの場合これはすぐに来ます)ic2_DrawFrame()が実行され、結果的に描画とバッファの切り替えが行われます。

今回は結果として250[ms]間隔で描画が更新されるので、バッファの色変化が観察しやすくなっていることでしょう。
また、前節のプログラムよりCPU使用率もずっと低くなります。

演習

06-04-ex1: CPUコストの削減
上記プログラムが前節のプログラムよりCPU使用率は下がる傾向になりますが、処理量はどれぐらい削減されるのでしょうか?06-02,06-03のプログラムの実行結果と合わせて考察するとさらによいでしょう。

06-04-ex2: モニタに合わせた描画間隔の変更
自分が今使用してるモニタのリフレッシュレートに合わせて、上記プログラムの描画時間間隔を変更しなさい。

06-04-ex3: 描画時間間隔とCPU負荷との関係
上記プログラムの描画時間間隔をいろいろと変えてみて、描画時間間隔とCPU負荷との関係で言えることを示しなさい。特に間隔を狭める方はどこまで可能なのでしょうか(意味があるのでしょうか)?

06-04-ex4: タイマー割り込みの意義
このことから、タイマーハンドラを使わないプログラムの実行時に起きていたことと、そのときのCPU負荷率の高さの原因説明を試みなさい(06-03-ex5から06-03-ex7で実行結果が予想通りでなかった場合)。

06-04-ex5: タイマー割り込みの精度限界
厳密には、上記のプログラムでは4Hz(250ms毎)での描画間隔になる保証はありません。その理由について述べてみなさい。

06-04-ex6: 描画時間間隔の漸近的変更
上記プログラムを改良して、描画時間間隔がだんだん広がるプログラムを作成しなさい。

06-04-ex7: 描画時間間隔の指定
上記プログラムを改良して、周期をbpmの形でコマンドラインから指定できるようにしなさい。bpmは1分当たりのフレーム切り替え回数(beat per minute)とします。

06-04-ex8: 背景のグラデーション
本節のプログラムを改良して、(1)滑らかにロゴが拡大(最小から最大まで20分割以上)させ、かつ(2)3周期かけて背景を黒から白まで変化させなさい。このとき、さらにロゴが見えにくくならないよう何らかの配慮をしなさい。


06.05. はじめてのOpenGL - 平行投影カメラ -

ここまででやっとバッファと表示の問題が片付いたので、次はいよいよCGを実際に描画したいところです。
しかし、CGの描画には、「カメラ・光源・物体(面の位置と法線と反射率)」の全てが揃う必要があります。

本節では、幾何的な問題だけでも手をつけて、形状だけでも見えるようにしましょう。

そのために、カメラの設定をします。
平行投影カメラ(直交投影、正投影とも呼ばれます)による3次元から2次元への射影変換を行います。

投影の概念

まずは数学的な表現の学習です。

平面への射影が行われることになる3次元世界を表現するための座標系をカメラ(3次元)座標系と呼び、その座標をPcameraで表記します。
投影先の平面を撮像面と呼び、その2次元座標系を撮像面(2次元)座標系と呼び、その座標をpcameraで表記します。
3次元から2次元への投影は、射影行列Pで表現します。

pcamera = P Pcamera

ここで、Pcameraは3要素、pcameraは2要素で本来は十分なのですが、計算構造の簡素化を考慮して、すべて4要素の同次座標表現をします。
(もとが3要素の場合は1つ、もとが2要素の場合は2つ要素を増やして、いつでも4次元ベクトルにしてしまうようにします)
(増やす要素にどのような値を入れるべきか?これについては決まりがあります。あとでプログラムの実際を見ればわかりますが、本授業の内容程度ではそれを知る必要はありません〔OpenGLライブラリにお任せできるため〕。)
これによって、Pは4x4の行列となります。

数学的には上記で済むのですが、実際に3次元世界を2次元平面に射影するときには、幾つかの細かい、しかし絶対に必要な項目を定めておかなくてはいけません。
ここではOpenGLでの約束に沿って説明します。

・OpenGLでは3次元座標系に右手系を採用しています。
・カメラ3次元座標系の中で、カメラの焦点は原点に固定されます。
・カメラ3次元座標系の中で、カメラの光軸は(0,0,-1)の方向ベクトルに向けられます。
・カメラ3次元座標系の中で、カメラの縦方向はY軸で上方が正、カメラの横方向はX軸で右方が正です。

参考:Red Book Chapter 3, Figure 3-9: Object and Viewpoint at the Origin

平行投影

平行投影は、空間上の座標を、Z値が一定の平面(XY平面に平行な面)に正投影するものです。
当然、撮像面の大きさは物体の大きさだけ必要です。
(170cmの身長の人間を平行投影によって像を得るのなら、上下に170cmの大きさのある撮像面(!)を用意する必要があります)
また、原理的には任意のZ値の座標点を投影することが可能ですが、実際には投影できる点の座標のZ値の範囲を限定します。
(OpenGLハードウェアが通常使っているZ-buffer法という遠近判定法をするときに、範囲が広すぎると遠近判定の精度が低下してしまうからです)

参考:Red Book Chapter 3, Orthograhic Projection

プログラム解説

06-05-OrthogonalCamera.c
06-04からの差分

カメラの設定は、1フレーム分の描画を行うic2_DrawFrame()の最初に行います。
そこでic2_SetUpCamera_Ortho()を実行して、直交投影用の4x4行列を求めます。

glOrtho(-1, 1, -1, 1, -1, 1)
このプログラムでは、横(X座標) -1.0〜1.0、縦(Y座標) -1.0〜1.0が直交投影の範囲としています。(第1〜4引数)
このプログラムでは、奥行(Z座標) -1.0〜1.0を直交投影の範囲としています。(第5〜6引数)

参考:Red Book Appendix F. Orthographic Projection

注意点

glOrtho()で与える撮像面の縦横比と、実際に表示しているwindowの縦横比とは一致している必要があります。
そうしないと、歪んだ(幾何的に正しくない)描画となってしまいます。
glutCreateWindow()ではデフォルトで正方なwindowが用意されるので、このプログラムではglOrtho()のアスペクト比が1.0の正方になるように指定しています。
実は、残念ながら、正しく実行されても見た目は何も以前と変わりません‥物体を描画してないからです。それでは次節へ。

発展(この部分ははじめのうちは読み飛ばしても構いません)
OpenGLでは、CG描画を計算する際には、基本的に4x4の行列演算を行います。
OpenGLで扱う4x4の行列は大別して4種類に別れ、そのうちこの授業で扱うのは MODELVIEW行列とPROJECION行列の2つです。
OpenGLでは、これらの行列に関しては、特にスタック構造を用いて様々な操作が出来るように工夫されています。
実際に描画に当たって使用されるのはそれぞれの行列スタックのトップにある行列だけです。

4種類の行列のうち、3次元物体を2次元平面に射影するPROJECTION行列が、カメラモデルに相当します。
そこで、まず glMatrixMode(GL_PROJECTION) によってまずPROJECTION行列スタックに用があることを示します。
続いて,glLoadIdentity() で現在操作中の(=PROJECTION行列の)スタックのトップを単位行列化します。
参考: Red Book Chapter 3, Figure 3-1, 3-2

演習

06-05-ex1: 直交投影の行列式
平行投影カメラについて、OpenGLの直交変換(glOrtho()関数)前の世界座標系での3次元ベクトルを、変換後のカメラ座標系での3次元ベクトルに投影する式を、行列演算による形で記述しなさい。ここでは投影先はZ=0の平面とします。

06-05-ex2: Z値要素の意味
上記06-05-ex1において、変換後のカメラ座標系ベクトル中のZ値要素はどのような意味を持つか説明しなさい。

06-05-ex3: Z軸
OpenGLの規約に従うと、Z軸は皆さんが見ている画面に対してどの方向が正になるか説明しなさい。

06-05-ex4: glOrtho()関数
資料を調べてglOrtho(-1, 1, -1, 1, -1, 1)によって用意されるMODELVIEW行列のプログラム実行時の実際の値を示しなさい。その値の導出家庭も示すこと。4x4の全要素を数値で示すこと。
参考: Red Book Chapter 3, Orthographic projection
参考: Red Book Appendix F. Orthographic projection

06-05-ex5: 実際の投影面
本節のプログラムのglOrtho()の設定では、投影面は実際にはどこになりますか。資料を調べて理由を挙げて説明しなさい。
参考: Red Book Chapter 3, Orthographic projection
参考: Red Book Appendix F. Orthographic projection

06-05-ex6: Z軸方向の描画範囲
直交投影では、カメラ投影の原理的な説明からすると矛盾するようなZ軸描画範囲(正の部分空間)をセットすることができます。なぜでしょうか。
(本節のプログラムはまさにそのような例になっています)


06.06. はじめてのOpenGL - 線画物体を描画 -

直交投影のカメラまで用意したので、あとは物体の形状を与えれば、幾何的に正しい描画が可能になります。
(光源も、物体の反射率もまだ与えてないので、レンダリングとして正しい色を描画できるようになるのはもっと後です)

06.05節のプログラムでは、描画できる空間はX,Y,Z軸とも -1.0 〜 1.0 です。
そこで、この範囲に入る線画物体の描画を用意しましょう。

プログラム解説

06-06-LogoOpenGL.c
06-05からの差分

ic2_OpenGLLogo()関数は、Z=0の平面上のX値 -1.0 〜 1.0 、Y値 -1.0 〜 1.0 の範囲内でOpenGLを線画で描画します。
あとで表示サイズが変えられるように、スケールファクタを引数として取れるようにします。

プログラム解説(線の描画)

OpenGLでは、何種類かの典型的なCGオブジェクトを描くための関数群が用意されています。
ここでは、「GL_LINES」という、点列を繋いで線分を描画する形式を利用します。

GL_LINES object
glDisable(GL_LIGHTING);   // 以後RGBの色指定があれば値域を0.0〜1.0と仮定して着色
glColor3f(1.0, 1.0, 1.0); // 順にR,G,B
glLineWidth(1.0); // 線の太さ(単位は[画素])

glBegin(GL_LINES); // 描画開始 (線分AB, 線分BCを描画)
glVertex3f(s * -0.8, s *  0.8, 0.0); glVertex3f(s * -0.8, s *  0.2, 0.0); // 線分AB
glVertex3f(s * -0.8, s *  0.2, 0.0); glVertex3f(s * -0.8, s *  0.2, 0.0); // 線分BC
glEnd(); // 描画終了

(ちなみに点群を繋いで線分を描いていくときは GL_LINE_STRIP を使います)

参考:Red Book Chapter 2, Describing Points, Lines, and Polygons → Lines

演習

06-06-ex1: 線の太さ
線の太さを2倍にしてみましょう。

06-06-ex2: 座標系
現在表示しているウィンドウに対して、上記のようなソースコードでの3次元座標はどう配置されてますか。原点、X軸、Y軸、Z軸をそれぞれ示しなさい。また、単位(各軸1単位)がどこに相当するか説明しなさい。

06-06-ex3: オリジナルロゴ
自分で方眼紙状のワークシートを用意して、オリジナルのロゴを作りましょう。スケール調整可能にしておくこと。


06.07. はじめてのOpenGL - 周期的動作 -

せっかくOpenGL LOGO を描いているので、こちらも何か動きが欲しいところです。
ic2_OpenGLLogo()関数の呼び出し時に指定するスケールファクタを、背景の色切替と同期して変化させるようにします。

プログラム解説

06-07-Periodic.c
06-06からの差分

演習

06-07-ex1: 上記プログラムを改造して、書割変数splitnumber(正整数)を導入して、なめらかにロゴが拡大しているように見えるCGを実現しなさい。loopcounterの数値と、タイマーハンドラの待ち時間の操作が重要です。
06-07-Periodic.cは周期2500[ms]でsplitnumber=10に相当)

06-07-ex2: スケールファクタ(ic2_OpenGLLogo()の引数s)と、フレームカウント(loopcounter)の値との関係をプログラムを読んで正確に解析しなさい。loopcounterは0から値を取って解析すること。loopcounterのそれぞれの値に対してのsの値は求めること。
(もとの06-07-Periodic.cに対してでも、06-07-ex1のプログラムに対してでも、どちらでもよい。採用したほうがどちらかは明記すること)

06-07-ex3: 上記の06-07-ex2で調べたように、loopcounterとsとの関係は線形である。この線形関係を式で表記しなさい。

06-07-ex4: loopcounterとsとが正比例だとロゴが一定スピードで近づいてきているようにみえない(遠くのうちは速く、近づくと遅く移動しているように見える)。その理由を投影幾何の式を用いて説明しなさい。

06-07-ex5: 時間経過(フレームカウント)に対してロゴが見かけ上、等速で近づいてくるようにするための、フレームカウントからスケールファクタを求める式を導出しなさい。

06-07-ex6: 上記のex5の式に基づいてプログラムを修正して、その動作を主観的に確認しなさい。

06-07-ex7: 06-07-Periodic.cでは緑色バッググラウンドになったときに、ロゴが描画できていません。その時にロゴが1.0倍で描画されるように変更しなさい。


06.08. はじめてのOpenGL - マイクロ秒オーダの計測 -

unixのシステム関数である gettimeofday()関数 を使えば、システム時間をマイクロ秒オーダで調べることができます。
(この節の内容は、OpenGLとは関係ないので、他のlinuxプログラム一般に適用できます)

06-08-CountTime.c

演習

06-08-ex1: 06-04-ex7のプログラムをさらに改造して、周期が正確に設定されているか確認できるプログラムにしなさい。

06-08-ex2: OpenGLのプログラミングにおいては、実はここで示した方法は計時方法として推奨されていません。どうしてか理由を考えてみなさい。
(代わりにglutGet(GLUT_ELAPSED_TIME)関数などの利用が推奨されています)


06.09. はじめてのOpenGL - コードクリーニング -

次節へ向けて、背景がちらちらすると見にくいので整理しておきましょう。

06-09-LogoOpenGL.c
06-06からの差分

演習

06-09-ex1: 06.07節までのプログラムで「背景がちらちらとする」理由を説明しなさい。


kameda[at]iit.tsukuba.ac.jp.