8. GLUTを用いたCGの移動と回転

【プログラミング序論3】 目次, 講義ページ, 授業科目, www.kameda-lab.org 2016/01/18a

OpenGLでのCG描画をいよいよ行います。
ただ、光と反射から色を決定する方法をまだ説明していないので、色についてはRGB値を直接指定します。

参考リンク

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


08.01. OpenGLライブラリが使う行列

OpenGLが主に用いる行列には、GL_PROJECTION行列とGL_MODELVIEW行列があります。
いま、ここでは GL_PROJECTION行列を、GL_MODELVIEW行列をと表記します。
ある空間中の1点の位置ベクトルは、4次元ベクトルaで表します。
当面、a = (X, Y, Z, 1)Tとし、第4要素は1に限定しておくことにします。

GL_PROJECTION行列
参考:Projection Transformation
カメラ座標系の位置ベクトルbを画像平面上に射影します。
射影後の点aも4次元ベクトルで表現します。
現時点では、射影後はa = (u, v, f, 1)Tとし、第4要素は1にしておいてください。
ここでは、06.05節の正投影に対応する投影行列 ortho を考えます。
この場合は、射影後はa = (X, Y, f, 1)Tとなります。
a = ortho b

GL_MODELVIEW行列
参考:Viewing and Modeling Transformations
物体の移動や回転などで見ためを変化させるのがGL_MODELVIEW行列の役割です。
現在はまだ何も移動や回転など、モデルを動かしてその見かけ(Model view)を変化させてないですね。
(今はまだ「モデルをカメラの前で動かす」という表現を使ってます)
ここではGL_MODELVIEW行列をで表記します。
移動前の位置ベクトルをc、移動後の位置ベクトルをbとします。
b = c

行列の演算
上の2つの演算をまとめると、次のようになります。
a = ortho b = ortho ( c) = ortho c

行列の確認
GL_PROJECTION行列とGL_MODELVIEW行列とが実際にどのような値になっているか、プログラムで確認してみましょう。
OpenGLでは、4x4の行列を表現するのに、float の1次元配列(要素数16)を用います。(08-01-ex3)
GL_PROJECTION行列とGL_MODELVIEW行列とが実際にどのような値になっているか、プログラムで確認してみましょう。
実は、GL_PROJECTION行列とGL_MODELVIEW行列とは、OpenCVライブラリの中で直接参照することができません。
(それなりの理由があってのことなのですがここでは書きません)
代わりに、glGetFloatv()を使って自分の用意した変数に代入させます。
(sscanf()関数と似た考え方ですね)

glGetFloatv()を使って、GL_PROJECTION行列とGL_MODELVIEW行列とをまとめて表示する関数の作成します。
これは今までのどの関数グループとも近くないので、ソースファイルをこれのために分けて用意します。

この関数を使ってどこの状態の行列を知るべきでしょうか。
それは物体をCGとして描画し始める直前です。このことについては次節で説明します。

【プログラム08-01】(灰色分は変更無し)

  1. ic2-CommonHeaders.h (07-05からの差分)
  2. 08-01-Rendering.c (07-05からの差分)
  3. 08-01-GLTools.c (新規)
  4. 07-03-EmbededObjects.c, 07-03-Projection.c, 07-04-Initialization.c, 07-05-Callback.c, 07-05-MainFunction.c

eclipseのプロジェクト設定について
07節以降、特にこの08節以降は、書き換える必要のないファイルが増えてきます。
(書き換える必要のないファイルが完全に確定してしまえるのならライブラリを作ってしまうところですが、本授業ではそこまではやりません)
(ちょこちょことあちこちのファイルをいじるので‥)
そこで今後は以下のようにして各節用の新規プロジェクトを用意してください。
1. まずプロジェクト(08-01)を新規作成します。
2. 必要な新しいソースファイルをそのプロジェクトディレクトリ(~/workspace-ic2/08-01/)に置きます。
3. 書き換えずに前のプロジェクトから引き継ぐソースファイル(ここでいれば07.05.節のソースファイル群のうち、新しく書き直さなかったソースファイル)(~/workspace-ic2/07-05/*.[ch])をプロジェクトディレクトリ(~/workspace-ic2/08-01/)にコピーする。この操作はeclipse外で行ってください。
(eclipse内でできないこともないですが‥)
4. プロジェクトを「更新」します。
5. リンクするライブラリの設定を忘れないように。
6. (引き継いできた)ソースファイル名が古いままだと気になる場合は、プロジェクトツリーの変更したいファイル名の上で右クリックして、「名前変更」を選び、適切な名前にしてください。
(eclipseはファイル名が変わっても自動的にコンパイル方法などを対応させてくれます)

eclipse
オンラインデバッグ開始前に、ブレークポイントを調べたい関数のところで設置するように。

こちらは2012年度以前の情報なので無視してください。

eclipseのフォント設定
5階計算機室の環境では、等幅フォントで全てのソースを表示してくれません。
気になる人は、とりあえず下記の対策をして、半角英数だけですが等幅にしてみてください。
フォント設定変更は、ソースコードの部分とターミナルの部分の2箇所です。
1. ウィンドウ→設定
2.「設定」左側:一般→(展開)→外観→(展開)→色とフォント
3.「設定」右側:色とフォント:C/C++→(展開)→Editor→(展開)→C/C++ Editor Text Font→変更
4. ファミリ:文泉*等寛微米黒 , スタイル:Regular , サイズ 10 →OK
(とにかく下のプレビューで等幅かどうかを確認しましょう)
5.「設定」右側:色とフォント:C/C++→(展開)→C-Build Console Text Font→変更
6 ファミリ:文泉*等寛微米黒 , スタイル:Regular , サイズ 10 →OK

eclipseのトラブルシューティング(本当に不具合になった人だけ)
なにかの拍子に、設定が不良になってeclipseが全く起動できなくなるときがあります。
~/workspace-ic2/.metadata/ の下のファイルがおかしい、と言われた場合がこれに該当します。
0. eclipseを終了させてください。
1. bash上で、rm -rf ~/workspace-ic2/.metadata/ として、全て消してください。
2. eclipseを起動します。
3. メニュー:ファイル→インポート
4. 一般→(展開)→既存プロジェクトをワークスペースへ→次へ
5. ルートディレクトリの選択→参照(ubuntu/workspace-ic2が選択されてるはず)
6. ずらっと過去のプロジェクトが並んでいるはずなので、「すべて選択」して「終了」します。
7. 成功すれば、全てのプロジェクトが復活します。

演習

08-01-ex1: 投影行列の予想
06.05節の正投影の設定のときのorthoの行列をプログラム作成前に予想しなさい。

08-01-ex2: GL_MODELVIEW行列
GL_MODELVIEW行列は初期状態でどのような行列であるべきか、理由と共に説明しなさい。

08-01-ex3: OpenGLでの4x4行列の配置
OpenGLでfloat m[16];の16要素は4x4行列にどう対応するか示しなさい。特に添字には注意して説明すること。

08-01-ex4: glGetFloatv()関数の説明
glGetFloatv()関数の説明のあるページを、「OpenGLの各関数の説明」の中から探してURLとその解説部分を示しなさい。(単にhttp://www.opengl.org/sdk/docs/man/だけでは不正解です。)

08-01-ex5: 新しいOpenGLライブラリ関数
今回新しく導入されたOpenGLライブラリ関数を全てリストアップし、とその使い方を簡単に説明しなさい。

08-01-ex6: オリジナルロゴ へ交換
ic2_OpenGLLogo()の内容を、06-06-ex3で作成したオリジナルロゴに入れ替えなさい。

08-01-ex7: ic2_ShowMATRIX()のprintf()
ic2_ShowMATRIX()について、printf()に続いてffush()を加えて実行しなさい。


08.02. 初めての移動

参考:Viewing and Modelin Transformations

まだ私たちが手にしているCG物体はic2_OpenGLLogo()で描画する物体だけです。
この物体を右に0.5移動させることにします。(08-02-ex1)
そうしたいのですが、まずは「カメラ座標系のX軸に沿って+0.01だけ移動」してみましょう。
(0.5と言っておきながら0.01とはなぜ?と思うかもしれませんが、後で出てくるようにちゃんと理由があります。)

移動の概念
今回の移動は、GL_MODELVIEW行列moveを操作することで実現します。
a = ortho move c
移動を表す関数には glTranslatef() を用います。(08-02-ex2)
glTranslatef(X,Y,Z) の3引数が移動量を示します。

おそらく、物体の移動をまず考えると、「物体がカメラ座標系の中で移動すること」で実現することにしたくなります。
ところが、今回のOpenGLのプログラミング上では、ic2_OpenGLLogo()関数の中の座標値をいじりません。
(「移動」させるなら、各頂点の座標をX方向に0.5ずつ足したものを用意するべきですよね。でもそのようにプログラムソースを書くのはとても面倒です。ic2_OpenGLLogo()関数内には座標がいっぱいあるので、書き換えは気が遠くなりそうです)
厳密には、概念として、ic2_OpenGLLogo()関数の中では、CGは「モデル座標系」に従って座標値を設定し描画します。
(ロゴマークを設計するときに使った方眼紙の横軸と縦軸がこの「モデル座標系」に相当します)
OpenGLでは、プログラム上でモデル座標系に頂点など(c)が描画がされる瞬間、内部の演算によってモデル座標系での座標値をカメラ座標系に変換(b)し、それを画像平面に射影して描画(a)を実際に行います。
a = ortho b = ortho (move c) = ortho move c

移動を表す行列
glTranslatef(X,Y,Z) を実行すると、4x4行列の単位行列に加えて1,2,3行の4列にX,Y,Zがそれぞれ配置された行列が生成されます。
これがさきほどのmoveに相当します。

移動の実装
先ほどの説明に従えば、glTranslatef(X,Y,Z)は、ic2_OpenGLLogo()で実際の描画が始まる前なら、どこでもいいことになります。
ここでは、ic2_OpenGLLogo()の呼び出し直前に操作してみましょう。
OpenGLでは、glTranslatef()のように行列を操作する関数の利用時には、必ずOpenGLの4種の行列のどれに操作するか宣言しなくていけません。
そのために、glTranslatef()の直前に glMatrixMode(GL_MODELVIEW) を実行して、以降の操作がGL_MODELVIEW行列であることを示します。

【プログラム08-02】(灰色分は変更無し)

  1. 08-02-Rendering.c (08-01からの差分)
  2. 07-03-EmbededObjects.c, 07-03-Projection.c, 07-04-Initialization.c, 07-05-Callback.c, 07-05-MainFunction.c, 08-01-GLTools.c, ic2-CommandHeaders.h

実行

表示されるGL_MODELVIEW行列に注目しましょう‥て、あれれ?どうして?

演習

08-02-ex1: 単位
上記説明中の、「0.01」の単位は何ですか。説明しなさい。

08-02-ex2: glTranslatef()関数の説明
ページを、「OpenGLの各関数の説明」の中から探してURLを示しなさい。URLだけでは指定しきれない場合は、URL+手順でも可。
「https://www.opengl.org/sdk/docs/man2/」だけでは不正解です。

08-02-ex3: 移動変量のテスト
移動変量の(0.01, 0, 0)をいろいろ変更して実行してみなさい。特にZ値を変更するとどうなるか注意しなさい。その結果の図をキャプチャして文章でも説明しなさい。

08-02-ex4: 想定外の移動の確認
プログラムの進行に伴って、ウィンドウ内のロゴが移動していく様子を確認しなさい。


08.03. OpenGLでの行列操作の実際

参考:Viewing and Modelin Transformations

前節では図らずもアニメーションを実現してしまいました。
(わーいアニメーションだー、とどうしてそうなったかもわからずに喜ぶのではプログラマ失格です)
(普通のプログラマなら、例えたまたま良い結果に出くわしても、それが想定外であるのなら用心してちゃんとそれが発生しないように「バグ」として潰します)
(上級のプログラマ(?)なら、たまたまでくわした良い結果をもたらした原因(バグ)を究明し、今後に役立てます)
どうしてこんなことが起こったのでしょうか?
実は、OpenGLでの行列操作には1つの特徴があります。
それは、行列操作を「それまでの演算結果の上で行う」ことです。

行列の蓄積
OpenGLが起動したとき、08.01.で見た通り、GL_MODELVIEW行列stは単位行列です。
st
glTranslatef()を最初に実行する際、移動行列をここで便宜上で表すと、st
stst
となります。実際には右辺の st の中身は なので、左辺の値は となります。
次にglTranslatef()をまた実行する段になると、stは初期化されてないので、
stst
によって左辺は T T、つまり移動量2倍の状態になります。
あとは同じ要領で、3回目には3倍、4回目には4倍になります。
(注意すべきは、
st st ではないことです。一緒じゃないかって?勘違いして覚えると次節で大変なことに‥)

行列の蓄積の解消
ではどうすればこの不本意なアニメーションを止められるでしょうか?
演算の前に、stを明示的に初期化(単位行列化)すればよいのです。
st
stst
この1行目の操作を実現するのが glLoadIdentity() 関数です。
ここでは、glTranslatef()の実行直前に導入します。

プログラミング
プログラミングで、動作を確認してみましょう。
確認の為に、ic2_showMATRIX()を3箇所に埋め込んでみます。
(そこの君、プログラムの実行結果がかえってつまんなくなった‥とか言わない)

【プログラム08-03】(灰色分は変更無し)

  1. 08-03-Rendering.c (08-02からの差分)
  2. 07-03-EmbededObjects.c, 07-03-Projection.c, 07-04-Initialization.c, 07-05-Callback.c, 07-05-MainFunction.c, 08-01-GLTools.c, ic2-CommandHeaders.h

演習

08-03-ex1: X変量の変更
X変量「0.01」を0.03のようにもう少し大きい値にして実行してみなさい。

08-03-ex2: 移動行列
08.01.節でも説明した通り、OpenGLでは、3次元空間座標はt(X,Y,Z,1)で表されます。
移動はglTranslate*(X,Y,Z)関数によって移動行列が用意されます。このとき、X,Y,Zは移動量を示しています。
MODELVIEW行列上でこの移動行列がどう表現されるかを説明しなさい。
08-02-ex2で調べたURLも使って構いません。
参考 http://glprogramming.com/red/appendixf.html

08-03-ex3: 回転行列
回転はglRotate*(a,X,Y,Z)関数によって回転行列が用意されます。このとき、aは回転角度、X,Y,Zは回転軸ベクトルを示しています。
MODELVIEW行列上でこの回転行列がどう表現されるかを調べなさい。
参考 http://glprogramming.com/red/appendixf.html

08-03-ex4: 直交行列
回転行列は線形代数的に言えば直交行列になります。直交行列の性質を覚えている限りすべて示しなさい。
(プログラミングするときに有用・便利な性質を有しています)

08-03-ex5: 逆の回転行列
今、軸(X,Y,Z)の周りをaだけ回転する行列に対して、同じ軸周りを-aだけ回転する逆回転行列を考えます。
これはOpenGLの関数ではそれぞれどのように書くべきでしょうか。
また、そうして得られる二つの行列の間にはどのような関係があるでしょうか。

08-03-ex6: 行列積の演算法則
一般的な演算法則の「結合法則」「交換法則」「分配法則」を説明しなさい。
この3つのうち、行列積において不成立なのはどの法則か示しなさい。

08-03-ex7: 行列積の演算法則の例外
一般的には行列積において不成立なはずの演算法則が、特定の条件下では成立します。
その演算法則を上げ,成立する特定の条件を考えて説明しなさい。

08-03-ex8: GL_MODELVIEW行列の様子の確認
X変量「0.01」がプログラム進行に連れてGL_MODELVIEW行列に蓄積されていく様子を確認しなさい。
(本授業のシステムでは、Eclipse上で実行に連れて表示を更新していくには05.08.節のようにfflush()関数を設定する必要があります)


08.04. 多段階の行列操作

OpenGLでは、移動の操作を多段階の行列操作で表現することで複雑な操作を可能にします。
主たる操作には、移動、回転、拡大縮小の3種類があります。
このいずれも行列で表現します。(08-04-ex1)

ここでは、Viewing and Modelin TransformationsのFigure-3.4の操作を実現してみましょう。
Z軸反時計回りに45°回転させたあと、「カメラ座標系X軸」に沿って0.7移動させます()。
回転行列をrot、移動行列をmoveとすると、
st move rot
でよいことになります。
行列の並びが説明の並びと反転していることに注意してください。
(逆ではいけません!)
これは操作対象である位置ベクトルがこの右側に配置されて、右側の行列から適用されていくためです。

OpenGLの実装では、前節で述べたように、後から演算される行列は、その前の行列に対して右から演算されます。
ということは、説明語順に対して、逆順に行列操作関数を並べていくこと(式の上では左から右へ順に)になります。

実際にプログラミングする前に,RT,TRのどちらが正しいか考えてみてください。
Rは回転行列を、Tは移動行列を示します。
(位置ベクトルは行列の右から掛け合わせることに注意。)
行列の演算は一般に交換不能(結合法則と分配法則は成立)であることに注意してください。
【RT】の実装
glRotatef(45, 0, 0, 1);
glTranslatef(0.7, 0, 0);
【TR】の実装
glTranslatef(0.7, 0, 0);
glRotatef(45, 0, 0, 1);

なお、回転行列を表す glRotatef() 関数は、第1引数が回転角度(degreeで指定)、第2〜4引数で回転軸の方向ベクトルを示します。(08-03-ex3)

【プログラム08-04】(灰色分は変更無し)

  1. 08-04-Rendering.c (08-03からの差分)
  2. 07-03-EmbededObjects.c, 07-03-Projection.c, 07-04-Initialization.c, 07-05-Callback.c, 07-05-MainFunction.c, 08-01-GLTools.c, ic2-CommandHeaders.h

わかりにくいときは、's'/'S'キーも利用してみましょう。
(logoscale変数による変化は ic2_OpenGLLogo() の中の描画座標値を直接いじっているので、行列操作に無関係ですね)
('s'のスケールを0.5よりもっと小さい 0.1 とかにするとわかりやすいかもしれません)

演習

08-04-ex1: 行列演算
RT, TRのどちらも実装・実行して、違いを確認しなさい。

08-04-ex2: glRotatef()・時計回り
glRotatef()だけを使って、ロゴが中心で時計回りにゆっくり回転するプログラムを作成しなさい。

08-04-ex3: glRotatef()・Y軸回り
glRotatef()だけを使って、ロゴがY軸回りに(右側が最初に手前に来る形で)ゆっくり回転するプログラムを作成しなさい。

08-04-ex4: 08-04-ex3の違和感
ex3が直感的な期待と見た目が異なる点と、その理由について説明しなさい.

08-04-ex5: glRotatef()関数でのX軸の回転方向
glRotatef()関数において、X軸ベクトル(1,0,0)に対して正回転値を与えた時の回転方向を図示しなさい。カメラの視線方向も記入すること。

08-04-ex6: glRotatef()関数でのY軸の回転方向
glRotatef()関数において、Y軸ベクトル(0,1,0)に対して正回転値を与えた時の回転方向を図示しなさい。カメラの視線方向も記入すること。

08-04-ex7: glRotatef()関数でのZ軸の回転方向
glRotatef()関数において、Z軸ベクトル(0,0,1)に対して正回転値を与えた時の回転方向を図示しなさい。カメラの視線方向も記入すること。

08-04-ex8: ロゴの回転
小さいロゴがある円周に沿って時計回りに運動するプログラムを作成しなさい(秒針の先にロゴが張り付いているイメージです)。

08-04-ex9: 方向を維持したロゴの回転
小さいロゴがある円周に沿って時計回りに運動するプログラムを作成しなさい。ただしロゴは回転させないこと(観覧車を横から見るようなイメージです)。


kameda[at]iit.tsukuba.ac.jp