3次元CG作成の概要
3次元CG画像を作成することは,仮想的な3次元シーンを仮想的なカメラで撮影することであるといえます. 3次元CG画像を作成するには,まず撮影する3次元シーンを構築する必要があります. また,シーンを撮影するカメラを配置します. つづいて,カメラにどのようにシーンが写り込むかを決めます. これは,画像面に3次元シーンをどのように投影するかを決めることに相当します. 準備が全て整ったら,最後に撮影(描画)を行います. この描画処理をレンダリングといいます.
3次元シーンを構築するには,最低限,各物体の形状を記述する必要があります. よりリアルなシーンを構築するためには,光源をおく,物体の反射特性を決めることなどが必要となります. また,シーンを撮影する際には,たとえば,ある物体が他の物体の一部を覆い隠している, 物体の一部だけが写るなどの状況を的確に処理する必要があります. さらに写実的にするには,物体によって影ができる,物体間での光の反射などの現象を考慮する必要もあります.
ここでは,OpenGLで3次元CG画像を作成するための基本として, カメラの位置,姿勢,投影方法の設定と,レンダリングの基本的処理である隠面消去について説明します.
右手系と左手系

3次元シーンは,3次元座標系に基づいて構築することになります. ところで3次元座標系には,右手系と左手系の二つが存在します. OpenGLでは右手系が採用されています.
サンプルプログラム
ここで,3次元CGの基本的な要素を用いたサンプルプログラム(rotate_eye.rb)を見て下さい. 各行の左端の数字は,説明のために付けた行の番号で,プログラムにはこの行番号は含まれません.
このプログラムでは,色をつけた立方体を原点を中心に配置しています. また原点を向いたカメラが用意されています. キーを押すことで,カメラが立方体の周りを回るようになっています. カメラは,最初z軸上にあります. カメラは常に原点を向くように設定されています. プログラムの操作方法は次の通りです.
[j]/[J] | カメラの経度を変える |
[k]/[K] | カメラの緯度を変える |
[z]/[Z] | カメラと原点の距離を変える |
[a]/[A] | 座標軸の描画ON/OFF |
[h],[r] | 最初の状態に戻す |
[q],[ESC] | プログラムを終了させる |
001 # coding: utf-8
002 require "opengl"
003 require "glu"
004 require "glut"
005 require "cg/colored_cube"
006 require "cg/rotate3d"
007
008 #### 定数
009 DT = 4 # 回転角単位
010 INIT_THETA = 45.0 # カメラの初期位置
011 INIT_PHI = -45.0 # カメラの初期位置
012
013 MIN_DIST = 4.0 # カメラの原点からの距離の最小値
014 INIT_DIST = 8.0 # カメラの原点からの距離の初期値
015 DD = 0.125 # カメラの原点からの距離変更の単位
016 MAX_DIST = 30.0 # 最大距離
017
018 FOV = 45.0 # 視野角
019 NEAR = 0.1 # 視点から手前のクリップ面までの距離
020 FAR = 2.0*MAX_DIST # 視点から奥のクリップ面までの距離
021
022 WSIZE = 600 # ウインドウサイズ
023
024 CUBE = ColoredCube.new(1.0,[0,0,0]) # 色つき立方体
025 AXIS = Axis.new(MAX_DIST*10.0) # 座標軸
026
027 #### 状態変数
028 __theta = INIT_THETA # カメラ位置の経度
029 __phi = INIT_PHI # カメラ位置の緯度
030 __dist = INIT_DIST # カメラ位置の原点からの距離
031 __draw_axis = false # 座標軸を描画するか
032
033 #### カメラの配置を決定する ########
034 def set_camera(theta,phi,dist)
035 # カメラ位置の決定
036 eye = [0.0,0.0,dist]
037 eye.rotate3d('x',phi)
038 eye.rotate3d('y',theta)
039
040 # カメラの上向きの方向を決定
041 up = [0.0,1.0,0.0]
042 up.rotate3d('x',phi)
043 up.rotate3d('y',theta)
044
045 # カメラの位置と姿勢の指定(カメラは常に原点を追いかけるものとする)
046 GL.LoadIdentity()
047 GLU.LookAt(eye[0],eye[1],eye[2],0.0,0.0,0.0,up[0],up[1],up[2])
048 end
049
050 #### 描画コールバック ########
051 display = Proc.new {
052 # 背景,Zバッファのクリア
053 GL.Clear(GL::COLOR_BUFFER_BIT|GL::DEPTH_BUFFER_BIT)
054 CUBE.draw # 色つき立方体を描画
055 AXIS.draw if __draw_axis # 座標軸を描画
056 GLUT.SwapBuffers()
057 }
058
059 #### キーボード入力コールバック ########
060 keyboard = Proc.new { |key,x,y|
061 # [j],[J]: 経度の正方向/逆方向にカメラを移動する
062 if key == 'j' or key == 'J'
063 dir = (key == 'j') ? 1 : -1 # (条件式) ? 真のときの値 : 偽のときの値
064 __theta = (__theta + dir*DT) % 360
065 # [k],[K]: 緯度の正方向/逆方向にカメラを移動する
066 elsif key == 'k' or key == 'K'
067 dir = (key == 'k') ? 1 : -1
068 __phi = (__phi + dir*DT) % 360
069 # [z],[Z]: zoom in/out
070 elsif key == 'z' or key == 'Z'
071 if key == 'z'
072 __dist += DD
073 __dist = MAX_DIST if __dist > MAX_DIST
074 else
075 __dist -= DD
076 __dist = MIN_DIST if __dist < MIN_DIST
077 end
078 # [a],[A]: 座標軸の描画ON/OFFの切替
079 elsif key == 'a' or key == 'A'
080 __draw_axis = (not __draw_axis)
081 # [r],[h]: 初期状態に戻す
082 elsif key == 'r' or key == 'h'
083 __theta = INIT_THETA; __phi = INIT_PHI
084 __dist = INIT_DIST
085 # [q],[ESC]: 終了する
086 elsif key == 'q' or key.ord == 0x1b
087 exit 0
088 end
089
090 set_camera(__theta,__phi,__dist) # カメラの配置
091 GLUT.PostRedisplay() # 再描画
092 }
093
094 #### ウインドウサイズ変更コールバック ########
095 reshape = Proc.new { |w,h|
096 GL.Viewport(0,0,w,h)
097
098 GL.MatrixMode(GL::PROJECTION)
099 GL.LoadIdentity()
100 # fov: 視空間の上下方向の視角(degree)
101 # aspect: 視空間の「幅/高さ」の値(w/h)
102 # near,far: 手前と奥のclip平面の視点からの距離
103 # GLU.Perspective(fov,aspect,near,far)
104 GLU.Perspective(FOV,w.to_f/h,NEAR,FAR)
105 GL.MatrixMode(GL::MODELVIEW)
106
107 GLUT.PostRedisplay()
108 }
109
110 ##### main ##############################################
111
112 GLUT.Init()
113 # ダブルバッファとZバッファを使うように設定する
114 GLUT.InitDisplayMode(GLUT::RGB|GLUT::DOUBLE|GLUT::DEPTH)
115 GLUT.InitWindowSize(WSIZE,WSIZE)
116 GLUT.CreateWindow("Colored Cube")
117 GLUT.DisplayFunc(display)
118 GLUT.KeyboardFunc(keyboard)
119 GLUT.ReshapeFunc(reshape)
120 GL.ClearColor(0.4,0.4,1.0,0.0)
121 GL.Enable(GL::DEPTH_TEST) # Zバッファ機能をONにする
122 set_camera(__theta,__phi,__dist) # カメラの配置
123 GLUT.MainLoop()
カメラの投影モデル
CGのカメラは,もちろん本物のカメラとは異なります. 本物のカメラでは,フィルムはレンズの後に置かれます(デジタルカメラのCCD面 も,レンズの後にあります). 一方,CGでは,画像面はカメラの前にあると考えられます.

3次元の点Pは,透視投影により画像面に写されます. レンズの焦点に相当する投影中心点と3次元の点Pを結んだ直線と画像面との交点が点Pの像となります. 画像面と投影中心点との距離により,生成される画像は変わります. この距離が無限大になると,透視投影は平行投影になります. OpenGLでは,透視投影あるいは平行投影を利用することができます.

画像面のサイズは有限ですので,投影中心点からみて,画像面から奥にある全ての空間が画像にとらえられるわけではありません. 画像面の四隅と投影中心点を結ぶ直線で決まる四角錐の外側は,画像には写りません. この四角錐のうち,原理的には,画像面から奥にある全ての空間が見えることになりますが, 通常は,この奥行方向についても,画像に写る範囲を限定します. 具体的には,画像面に平行な2枚のクリッピング平面を用意して,その間の空間だけが画像にとらえられるものとします. したがって,画像に写る3次元空間の範囲は,四角錐と奥行方向に関する2枚の平面で区切られる四角錐台となります. ここでは,その領域を視空間(原語ではviewing volume)と呼ぶことにします. なお,平行投影の場合,視空間は直方体となります.
カメラの位置と姿勢
カメラをどこにどのような向きで置くかによって,シーンの見え方は変わります. このようなカメラの配置はどのように記述できるのでしょうか.

まずカメラと物体は一つの三次元空間の中にあると考えられます. この三次元空間の座標系を世界座標系(world coordinate system)といいます. カメラの位置はこの世界座標系の1点で与えられます. その点を視点といいます. 視点は透視投影の投影中心点に対応します. カメラの姿勢を定めるには,カメラが向いている正面方向とカメラでの上向きの方向を与えれば十分です. これは,視点を中心とした3次元座標系の軸の方向を設定するのと同じことです. その座標系をカメラ座標系(camera coordinate system)といいます.
変換行列 --- モデル・ビュー変換と投影変換
物体の配置,カメラの配置,投影方法の設定により,物体上の各点が画像面のどこに写されるかが決定されます. 3次元の点を画像に写す過程は,数学的には,行列で表現される変換を使って記述できます. この変換行列は,世界座標系でのカメラの配置を表す行列,カメラの投影方法を表す行列などの積によって表現されます. 物体についても,その配置,つまり位置や向きなどを行列で表現できます.
OpenGLでは,物体あるいはカメラの配置に関わるモデル・ビュー変換と カメラの投影方法に関わる投影変換を指定することで,画像を生成する変換を表します(細かく言えば,ビューポートを決める変換などもあります). 具体的には,それぞれの変換を表す行列が一つずつ用意され,それを操作することで変換を決めることになります. GL.MatrixMode(GL::MODELVIEW)かGL.MatrixMode(GL::PROJECTION)のいずれかの行列モードを指定することによって,モデル・ビュー変換,投影変換のどちらの行列を操作するかを決めることができます.
モデル・ビュー変換 | GL.MatrixMode(GL::MODELVIEW) |
投影変換 | GL.MatrixMode(GL::PROJECTION) |
ところで行列を操作するとはいっても,基本的に行列を直接記述する必要はありません. たとえば,行列を初期化する,つまり単位行列にするには,(行列モードを設定した上で)メソッドGL.LoadIdentityを使います.
なお,物体とカメラの配置が一つの行列(モデル・ビュー変換)として表せるのは,物体の動きとカメラの動きが表裏一体の関係にあるためです. たとえば,物体の周りをぐるっと一周して撮影を行うのと,物体をくるっと一回転させて撮影を行ったときに,まったく同じ映像が得られることも考えられます. そのような二つの状況は,画像からだけでは区別がつかないわけです. 逆にいえば,同じ効果を得るのに,カメラを動かしたと考えても,物体を動かしたと考えてもいいわけです. このことからも,物体とカメラの配置をモデル・ビュー変換として一括で扱えることが分かります.
OpenGLでのカメラの配置と投影変換
すでに説明したように,カメラの配置は,視点位置,カメラが向いている正面方向,カメラでの上向きの方向を指定することで決められます. OpenGLでは,GLU.LookAtによってカメラの配置を指定します. GLU.LookAtは引数を9個とり,前から3個ずつで,それぞれ 視点,注視点,上向きの方向のベクトルを表します. 視点と注視点によって,正面方向が決められます. なお,GLU.LookAtを適用するには,予め,GL.MatrixMode(GL::MODELVIEW)により,モデル・ビュー変換の行列を操作するように設定しておく必要があります.
サンプルプログラムでは,プログラムの実行開始時とキー入力によってカメラ位置が変わったときにset_camera()というメソッドを呼び出しています. その中で,まずGL.LoadIdentityを使って,モデル・ビュー変換行列を初期化したのち,GLU.LookAtでカメラの配置を決定しています.
033 #### カメラの配置を決定する ########
034 def set_camera(theta,phi,dist)
035 # カメラ位置の決定
036 eye = [0.0,0.0,dist]
037 eye.rotate3d('x',phi)
038 eye.rotate3d('y',theta)
039
040 # カメラの上向きの方向を決定
041 up = [0.0,1.0,0.0]
042 up.rotate3d('x',phi)
043 up.rotate3d('y',theta)
044
045 # カメラの位置と姿勢の指定(カメラは常に原点を追いかけるものとする)
046 GL.LoadIdentity()
047 GLU.LookAt(eye[0],eye[1],eye[2],0.0,0.0,0.0,up[0],up[1],up[2])
048 end
:
090 set_camera(__theta,__phi,__dist) # カメラの配置
:
122 set_camera(__theta,__phi,__dist) # カメラの配置
投影変換は,透視投影GLU.Perspective,あるいは平行投影GL.Orthoで与えます. サンプルプログラムでは,透視投影をつかっています.
094 #### ウインドウサイズ変更コールバック ########
095 reshape = Proc.new { |w,h|
096 GL.Viewport(0,0,w,h)
097
098 GL.MatrixMode(GL::PROJECTION)
099 GL.LoadIdentity()
100 # fov: 視空間の上下方向の視角(degree)
101 # aspect: 視空間の「幅/高さ」の値(w/h)
102 # near,far: 手前と奥のclip平面の視点からの距離
103 # GLU.Perspective(fov,aspect,near,far)
104 GLU.Perspective(FOV,w.to_f/h,NEAR,FAR)
105 GL.MatrixMode(GL::MODELVIEW)
106
107 GLUT.PostRedisplay()
108 }
透視投影では,四角錐台の視空間を次の四つのパラメタで指定します. まず一つめにカメラ座標での上下方向の視角(fov)を与えます. また手前と奥の2枚のクリッピング平面の位置(near,far)を与えます. さらに,手前と奥の2枚のクリッピング平面から四角錐が切り取る面の縦横比(aspect)を与えます. GLU.Perspectiveは4個の引数,fov,aspect,near,farをこの順でとります.
# fov: 視空間の上下方向の視角(degree) # aspect: 視空間の「幅/高さ」の値(w/h) # near,far: 手前と奥のclip平面の視点からの距離 GLU.Perspective(fov,aspect,near,far)
一方,平行投影では,視空間となる直方体の6枚の面のデータをカメラ座標で指定します. 直方体の各面は,カメラ座標のいずれかの軸と平行に置かれることになります. GL.Orthoは6個の引数をとり,それらは順に,直方体の左面,右面,底面,上面,手前面,奥面の位置を表します. 手前面をカメラの裏側に置くことも許されます. なお手前面と奥面は,カメラからの距離で指定します. これらの面をカメラの裏側に置く場合,負の値を指定することになります.
# left,right,bottom,top,near,far: 各clip平面の位置 GL.Ortho(left,right,bottom,top,near,far)
以下に平行投影を使ったサンプルプログラムを示します.
なお,これらの投影変換を適用するには,予め,GL.MatrixMode(GL::PROJECTION)により,投影変換の行列を操作するように設定しておく必要があります.
隠面消去
立体図形を正しく表示するには,カメラに見えている面だけを表示して,カメラから見えない面は表示しないようにしなければなりません. また同様に,お互いに重なり合う物体を表示するには,前の物体が後の物体(の一部)を覆い隠している状況を正しく把握して,カメラに見えている部分だけを表示する必要があります. このようにカメラから見える面だけを表示して,そうでない面は表示しないようにする問題を隠面消去(hidden surface removal)といいます.
隠面消去の方法はいくつかありますが,代表的な方法として,Zバッファ法(あるいはデプスバッファ法)があります. この方法では,画像を生成するバッファに加えて,各ピクセルでの奥行き情報を保持するバッファを使います. これをZバッファ(あるいはデプスバッファ)といいます 投影変換により,各ピクセルに点のデータを描き込む際に,その色を画像バッファに格納するとともに,カメラからの距離をZバッファに記録します. つぎに,同じピクセルに描かれることになる点があったとき,Zバッファの値と新しく描き込む点のカメラからの距離を比較すれば,新しい点が手前にくるのか,あるいは奥にくるのかが分かります. 手前にくると分かったときだけ,バッファ(画像バッファとZバッファ)を書き換えるようにすれば,物体の前後関係を正しく取扱うことができます. なお,半透明物体を扱う際には状況はもっと複雑になるのですが,ここでは触れないことにします.
OpenGLでZバッファを用いるには,次の3つの設定が必要となります.
- GLUT.InitDisplayModeにGLUT::DEPTHを設定しておく
RGBモードでZバッファを用いてアニメーションを行うには, GLUT.InitDisplayMode(GLUT::RGB|GLUT::DOUBLE|GLUT::DEPTH)を指定することになります.
- GL.Enable(GL::DEPTH_TEST)で,Zバッファを使用可能にしておく
ウインドウの設定をした後,実際にZバッファを使う前にこの設定を行います.
- GL.Clear(GL::COLOR_BUFFER_BIT|GL::DEPTH_BUFFER_BIT)でクリアを行う
これまで,画面をクリアする際には,色(だけ)を背景色でクリアしていましたが, Zバッファを使う場合には,これも同時にクリアしておく必要があります.
サンプルプログラムでは,次のように設定を行っています.
052 # 背景,Zバッファのクリア
053 GL.Clear(GL::COLOR_BUFFER_BIT|GL::DEPTH_BUFFER_BIT)
:
112 GLUT.Init()
113 # ダブルバッファとZバッファを使うように設定する
114 GLUT.InitDisplayMode(GLUT::RGB|GLUT::DOUBLE|GLUT::DEPTH)
115 GLUT.InitWindowSize(WSIZE,WSIZE)
116 GLUT.CreateWindow("Colored Cube")
117 GLUT.DisplayFunc(display)
118 GLUT.KeyboardFunc(keyboard)
119 GLUT.ReshapeFunc(reshape)
120 GL.ClearColor(0.4,0.4,1.0,0.0)
121 GL.Enable(GL::DEPTH_TEST) # Zバッファ機能をONにする
122 set_camera(__theta,__phi,__dist) # カメラの配置
123 GLUT.MainLoop()