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

【計算機序論2・実習(2012年度)】 目次, 計算機序論2, 授業科目, www.kameda-lab.org 2012/11/07e

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

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

OpenGLライブラリはそれ自体よくできたライブラリなのですが、それだけでは実際に描画したりする部分のプログラミングが難しいという問題がありました。
そこで、OpenGLライブラリをより簡単に使えるようにOpenGL Utility (GLU)ライブラリが用意されています。
しかし、GLUライブラリの助けを借りてもまだウィンドウを表示してのレンダリングなどが難しいという問題があったため、さらにOpenGL Utility Toolkit (GLUT)ライブラリが用意されています。

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

ということで、簡単にOpenGLライブラリを使う、と言っても、実際には3層に重なったOpenGLライブラリ、GLUライブラリ、GLUTライブラリを使うことになります。

まずはOpenGLでのプログラミングの初歩を実習していきましょう。

参考リンク

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と言います。大規模なプログラムを書いていく上で、単純な、でも重要な技術です)
ic2_BootWindow()でもっとも重要な役割はglutCreateWindow()を使ってwindowを立ち上げることです。
windowには名前がつけられるので、コマンドラインでの第0引数(=argv[0]=コマンド名)を指定するようにしています。
他には、glutInitDisplayMode()で、CG描画のスタイルを指定しています。
GLUT_DOUBLEで、描画にバッファを2枚用意することを宣言しています。
(これをダブルバッファリングと言います。)
(舞台などで回転舞台を用意して、裏でいろいろ準備して整ったらぐるっと180度回転して一気に場面転換するのと概念は同じです)
(当然、舞台上で黒子たちがいろんな準備するのに要する時間より、舞台の回転にかかる時間のほうがずっと短いというのがこの方式を採用するための前提です)

glutMainLoop()というイベント待ち無限ループに入る前に、「イベント〜動作」のペアを指定する必要がありますが、このプログラムでは何も指定していません。
そのため、プログラムは何もしない(window用に確保したバッファをクリアすらしない)ので、windowの枠が現れるだけで無限ループに入ります。
次節でこれについて学習します。

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

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環境に沿ったものです。)

リンク時:
必要なのは libGLU と libglut という2つの外部ライブラリを明示的にリンクすることだけです。
gccの引数としては、 -lGLU と -lglut となります。
(あとはリンク時にリンカが依存関係を解析して、さらに必要な外部ライブラリを自動的に付け足してくれます)
(ここでの説明もこの授業で使う工学システム学類5階計算機室のlinux環境に沿ったもので、その環境でのみ有効です。)
(linuxのディストリビューションによっては、-lglutだけで済む場合もあります)
(gccが、実際には libGLU と libglut をリンクして使えるようにするためには他にどのようなライブラリが必要か調べて、それらも自動でリンクしてくれています)

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

[1] コマンドライン
単一ソースファイルでOpenGLのプログラムを書いているうちは、コマンドラインでの実行ファイル生成(=コンパイル&リンク)は次のようにしてできます。
4.3節を復習しておいてください。
作業はどこかの(eclipseが使っていない)ディレクトリを用意して実施してください。

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

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

06-03-A 新規プロジェクトの登録 (05-4-2-A参照)
1 メニュー:ファイル→新規→Managed Make C Project
2 New Project/Managed Make C Project:プロジェクト名:06-01 → 次へ
(「デフォルト・ロケーションの使用」にチェックが入っていることも確認してください)
(グレイアウトされているロケーションが .../workspace-ic2/06-01 になっていきます)
3 New Project/Select a type of project:→終了
(Project Type: Executable(Gnu), Configurations: Debug と Release の両方になってるはずです)
4 a. メニュー:ファイル→新規→Source file、続けて Source File: 06-01-Preparation.c とファイル名を指定して開いて、ソースコードを記述
b. 何らかの手段で ~/workspace-ic2/06-01/ に 06-01-Preparation.c をコピーしたあと、メニュー→ファイル→更新
(いずれにせよこの段階ではコンパイルが通りません)
5 左の「C/C++ Projects」で 06-01 の最上位階層をクリックして選択
6 メニュー:ファイル→プロパティ(「プロパティー:06-01」ウィンドウが起動)
7 左側で「C/C++ Build」選択
8 右側上のConfiguration: で「Debug」選択
9 右側下のConfiguration Settings: で「Tool Settings」パネル選択
10 「Tool Settings」パネル内左側の「GCC C Linker」の下位の「Libraries」を選択
11 その右の「Libraries」で「+」アイコンクリック
12 「Enter Value」ウィンドウで「GLU」と入力→OK
11 その右の「Libraries」で「+」アイコンクリック
12 「Enter Value」ウィンドウで「glut」と入力→OK
13 「プロパティー:06-01」全体の右下の「適用」→OK
14 元のビューに戻ると、コンパイル・リンクが再実行されていてエラーが消えているはず
(右下の「コンソールでコンパイル実行の様子を確認しておきましょう)
(以後、特に説明しませんが06-02以降も同様に行ってください)

06-3-B 「構成およびデバッグ」(05-4-5-A参照)
1 プロジェクトビューで実行時デバッグをしようと思っているプロジェクトが選択されていることを確認
2 メニュー:実行→構成およびデバッグ
3 構成およびデバッグ(左):C/C++ Local Applicationをダブルクリック
(eclipseに、今ここで新しくデバッグが構成可能できそうなプロジェクトを探させます)
4 構成およびデバッグ(左):C/C++ Local Applicationの1階層下に現在のプロジェクト名と同じ名前の構成要素(デバッグ構成)が新しく現れるのでそれをクリック(ここでは06-01ですね)
5 構成およびデバッグ(右):名前:これはこのデバッグ構成につける名前です。現在はプロジェクト名が暫定的に入っていますが、混乱の元なので別の名前にしておきましょう。ここでは 06-01-DebugRun とでもしておきます。
6 構成およびデバッグ(右):Mainタブ:C/C++ Application: が空欄になっているので、Search Projectをクリック
7 Program Selection:Binaries:の欄内に 06-01 があるので、それをクリック→OK
(これによって workspace-ic2/06-01/Debug/06-01 というデバッグ用実行ファイルを指定したことになります)
(「構成およびデバッグ(右)」のMainタブについていた赤い×印がこれで消えます)
8 構成およびデバッグ(右):全ての設定が終わったら、「適用」をクリックして設定を保存します。
9 構成およびデバッグ(右):全ての設定が終わったので、右下の「デバッグ」をクリックして実行時デバッグを開始します。

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

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

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

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

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

$  ldd 06-01

演習

06-01-ex1: windowに自分の名前をつけてみましょう。アルファベット表記すること。(解答例:06-01-ex1.c)
06-01-ex2: 01-2-HelloESYS-Better.c06-01-Preparation.cとについて、実行ファイルを生成し、用いられる動的ライブラリ群を明かにして、その違いの理由を説明せよ。
06-01-ex3: 06-3-Bのあとでステップ実行すると、glutMainLoop()以降ステップ実行ができなくなるが、その理由を述べよ。


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

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

プログラム解説

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使用率を見ながら、06.01節のプログラム06.02節のプログラムとを実行してみてください。
前者では全然使用率が上がりませんが、後者ではCPU使用率がほぼ100%にまで到達すると思います。
(このCPU使用率の上昇具合は環境によって大きく違います)

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

glutMainLoop()を使うプログラムでは、05-4-7-A/B, 05-4-9のようにデバッグ実行しようとすると、glutMainLoop()を実行し始めた瞬間から無限ループに入ってEclipse側に制御を取り戻せなくなります。
このとき、次にユーザプログラム側(ユーザがソースコードを書いた部分)に実行が移るのは、ユーザが仕掛けたイベント・コールバックに従って、設定されたコールバック関数が呼び出されたときです。
そこで、デバッグ実行の前に、ユーザ側で(希望する全ての)コールバック関数の入口にブレークポイントを設定します。
デバッグ実行は、ブレークポイントを発見すると必ずそこで一旦停止して、ユーザに制御権を戻します。

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

演習

06-02-ex1:06.01節のプログラムではCPU利用率が実行前と実行中で(ほぼ)変化しない理由を説明しなさい。
06-02-ex2:06.02節のプログラムではCPU利用率が実行前と実行中で大きく異なり,100%近くになる理由を説明しなさい。このプログラム実行中、CPUは何をしているのでしょうか?


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

CPU使用率が高いのに、ユーザに何も視覚的変化がないのは寂しいものです。
なんとか可視化を考えてみましょう。

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

プログラム解説

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

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

さて、期待としては緑→黒→黒→緑→黒→黒→緑→黒→黒→緑→黒→黒→という色変化が見えるはずですが‥?
んん?おそらく思い描いている通りに見えてないと思います。どうしてでしょうね?

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

演習

06-03-ex1: 上記プログラムを変更して、5回に1回緑になるようにしなさい。
06-03-ex2: 上記プログラムを変更して、10回に1回赤になるようにしなさい。
06-03-ex3: 上記プログラムを変更して、緑→黒→黒→黒→白→黒→黒→黒にしてみなさい。
06-03-ex4: 現在使用しているデスクトップ環境で、モニタのリフレッシュレートを調べなさい。
06-03-ex5: 上記プログラムを変更して、ic2_DrawFrame()関数がex4のレート×100回実行されたら終了するように改良しなさい(60Hzなら6000回)。
06-03-ex6: ex5のプログラムを実施し、実行時間を秒単位で計測しなさい。なぜ実行に100秒かからないのか、説明しなさい。(ごく簡単な実行時間の計測は、bash上で date; ./06-03-ex5; date のようにすれば可能です)


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

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

また、この方式では描画時間間隔も一定しません。
(ic2_DrawFrame()関数の負荷が同じでも、OS上で他に重いジョブが走り始めたりすると1/60秒のうちに何回当該関数を実行できるかが変化してきます)

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

(1) まずglutDisplayFunc()で、(なんにせよ)「描画するならこの関数」ということを指定します。(これ自身はイベント〜動作の定義とは関係ありません)
(2) glutTimerFunc()で、「今からt[ms]後に起動したい関数(ここではic2_timerhandler()関数)を指定」します。
(3) その(2)で起動する(ic2_timerhandler())関数の中で、「次にチャンスがあったときには再描画させる」ことを、glutPostRedisplay()関数を呼び出すことで指示します。
(4) 実際には「次のチャンス」はイベント待ち無限ループにもどっ(てごく短時間経ってOSから「今からデスクトップを再描画するけどどうする?」と連絡を受け)たタイミングが、実際の再描画開始のタイミングとなります。
(一見面倒なこの仕組みの利点は、再描画してほしいというリクエストがごく短時間のうちに複数溜まったときに、それぞれで描画してしまうのではなくまとめて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()関数内では、「次にチャンスがあったときには間違いなく(ic2_timerhandler()関数を実行することによって)再描画をさせる」という要請を、glutPostRedisplay()関数を呼ぶことで明示します(上記(3))。
そのあと、これから250[ms]後にまた自分を(ic2_timerhandler()関数を)呼び出すよう予約しておきます(上記(2))。

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

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

演習

06-04-ex1: 上記プログラムが前節のプログラムよりCPU使用率は下がる傾向になりますが、処理量はどれぐらい削減されるのでしょうか?06-03-exの結果と合わせて考察するとさらによいでしょう。
06-04-ex2: 自分が今使用してるモニタのリフレッシュレートに合わせて、上記プログラムの描画時間間隔を変更しなさい。
06-04-ex3: 上記プログラムの描画時間間隔をいろいろと変えてみて、描画時間感覚とCPU負荷との関係で言えることを示しなさい。
06-04-ex4: このことから、タイマーハンドラを使わないプログラムの実行時に起きていたことと、そのときのCPU負荷率の高さの原因説明を試みなさい(06-03-ex6の考察と関連)。
06-04-ex5: 厳密には、上記のプログラムでは4Hz(250ms毎)での描画間隔になる保証はありません。その理由について述べてみなさい。
06-04-ex6: 厳密には、上記のプログラムでは4Hz(250ms毎)より小さくなる(間隔が長くなる)傾向があります。その理由について述べ、考えられる対策方法を示しなさい。
06-04-ex7: 上記プログラムを改良して、周期とbpmをコマンドラインで指定できるようにしなさい。bpmは1分当たりのフレーム切り替え回数(beat per minute)とします。


06.05. はじめてのOpenGL - 直交投影カメラ -

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

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

そのために、カメラの設定をします。
直交投影カメラによる3次元→2次元への変換を行います。

投影の概念

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

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

pcamera = P Pcamera

ここで、Pcameraは3要素、pcameraは2要素で本来は十分なのですが、計算構造の簡素化を考慮して、すべて4要素の斉次座標表現をします。
これによって、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=0の平面に正投影するものです。
当然、撮像面の大きさは物体の大きさだけ必要です。
(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次元ベクトルに変換する式を記述しなさい。
06-05-ex2: 上記ex1において、変換後のカメラ座標系ベクトル中の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: 自分で方眼紙を用意して、オリジナルのロゴを作りましょう。スケール調整可能にしておくこと。


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: 上記の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()関数 を使えば、システム時間をマイクロ秒オーダで調べることができます。

06-08-CountTime.c

演習

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


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

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

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


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