[CG実習 > 3次元CGの基礎]

3次元CGの基礎

ここでは,3次元CG画像を生成するための基本について説明します.

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)と呼ぶことにします. なお,平行投影の場合,視空間は直方体となります.

視空間とは,定義の通り,画像にとらえられる三次元空間の領域を表します. 原語では,これを"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つの設定が必要となります.

サンプルプログラムでは,次のように設定を行っています.


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()

[CG実習 > 3次元CGの基礎]