[CG実習 > シェーディング]

シェーディング

これまでは,描画する物体に人工的に色をつけていました. 写実的なCG画像を生成するには,この方法は適当ではありません. より自然な表現を行うには,より自然に近い状態を仮想的に実現しなければなりません. そのためにまず必要となるのが,光に関連する現象をモデル化して,画像に適切なシェーディング(shading;陰影)をつけることです. ここでは,OpenGLによって,シェーディングを行う方法について説明します.

光源と物体

われわれが目にする物体の色は,光源(light source)と物体表面の材質(material)によって決まります. 光源から発せられた光は,物体表面でその特性によりさまざまに反射あるいは吸収されます. 反射光は,さらに別の物体に届いて,再び反射あるいは吸収されます. また物体自身が発光して光源となる場合もあります. このような発光,反射,吸収の複雑な相互作用の結果として目に入る光が,われわれの見る物体の色となります. シェーディングの処理では,本来このような過程をすべてシミュレートする必要があります.

OpenGLでは,シェーディング処理は各面(ポリゴン)に関してそれぞれ独立に行われます. つまりOpenGLでは,物体間での光の相互反射は扱われません. このように簡単化を行っても,十分に現実感のある画像を生成することができます. また簡単化によって,高速な描画が可能になり,廉価なコンピュータでも実時間のアニメーションを行うことが容易になります.

光の反射モデル

光の反射モデル

点光源を考えます. 点光源が物体を照らしているとき,その物体表面の点での光の反射をモデル化するために,まず次の3要素を考えます.


ここで扱う反射モデルでは,これらの幾何的なデータと光源の特性と物体の材質の特性とを利用して,次の3種類の異なる反射を定義し,その組み合わせでさまざまな反射を表現します.

拡散反射

拡散反射は,ざらついた面での反射をシミュレートしたものです. ざらついた面では,ミクロなレベルの微小面がいろいろな方向を向いていることから, そこで光が乱反射を起こし,マクロな結果として,方向に無関係に光が反射されるように見えます.

完全な拡散反射面は,どこから見てもに同じ色に見えることになります. このように,反射のうちの拡散反射成分は,観測する方向によらず,入射光とその入射角度,反射率のみで決まります. 入射角度は,光源方向と法線方向を用いて決めることができます.

鏡面反射

鏡面反射は,滑らかな面での反射をシミュレートしたものです. 完全な鏡面では,光は,正反射方向のみに反射されます. この方向は,光源方向と法線方向によって決まります. 完全な鏡面でなくても滑らかであれば,光は,正反射方向を中心とした狭い範囲に反射されます.

鏡面反射の効果は,観測方向に大きく依存することになります. もちろん,入射光,反射率,さらに鏡面性の度合い(shininess)にも依存します.

環境光反射

物体間の光の相互反射の影響を簡易的に一定の強さの環境光として表現します. 環境光は方向性を持ちません. その強さと物体の反射率によってのみ,シェーディング処理での環境光の影響が決まります.

放射光

放射光とは,物体から発せられる光で,これは反射に基づくものではありません. 放射光は物体として描く光源を表現するためのものです. ただし,OpenGLでは,すでに述べた通り,物体間の相互作用は考慮されません. したがって,この放射光が他の物体を照らす効果は,シェーディング処理には組み込まれません.

サンプルプログラム

ここで,サンプルプログラム(shaded_objects.rb)を見て下さい. 各行の左端の数字は,説明のために付けた行の番号で,プログラムにはこの行番号は含まれません.

このプログラムでは,物体を原点を中心に配置します. また常に原点を向いたカメラが用意されています. キーを押すことで,カメラを原点を中心に動かせるようになっています. 操作方法は次の通りです.


001  # coding: utf-8
002  require 'opengl'
003  require 'glu'
004  require 'glut'
005  require 'cg/gl_objects'
006  require 'cg/camera'
007  
008  #### 定数
009  INIT_THETA =  45.0  # カメラの初期位置
010  INIT_PHI   = -45.0  # カメラの初期位置
011  INIT_DIST  = 10.0   # カメラの原点からの距離の初期値
012  L_INIT_PHI = 45.0   # 光源の初期位置
013  L_INIT_PSI = 45.0   # 光源の初期位置
014  
015  DT = 4              # 回転角単位
016  DD = 0.125          # カメラの原点からの距離変更の単位
017  SZ = 2.0            # 面オブジェクトのサイズのパラメタ
018  
019  WSIZE = 600         # ウインドウサイズ
020  
021  DEG2RAD = Math::PI/180.0
022  
023  # マテリアルの定義
024  DEFAULT_AMBIENT  = [0.3,0.3,0.2]
025  DEFAULT_DIFFUSE  = [0.9,0.9,0.6]
026  DEFAULT_SPECULAR = [0.1,0.1,0.1]
027  DEFAULT_SHININESS = 64.0
028  
029  #### 状態変数(カメラ)
030  __camera = Camera.new(INIT_THETA,INIT_PHI,INIT_DIST)
031  __lightphi = L_INIT_PHI
032  __lightpsi = L_INIT_PSI
033  
034  #### 図形セット
035  __gl_objs = GLObject.new
036  
037  #### 光源の位置
038  def set_light_position(phi,psi)
039    # 無限遠の光源(平行光線)
040    phi *= DEG2RAD;  psi *= DEG2RAD; 
041    z = Math.cos(phi); r = Math.sin(phi)
042    x = r*Math.cos(psi); y = r*Math.sin(psi)
043    GL.Light(GL::LIGHT0,GL::POSITION,[x,y,z,0.0]) 
044    GL.Light(GL::LIGHT1,GL::POSITION,[0.0,-1.0,-1.0,0.0])
045  end
046  
047  # 法線を表す棒付きの面オブジェクト(plane)を生成する
048  plane = Proc.new {
049    # 現在の色,シェーディング設定などの属性データの退避
050    GL.PushAttrib(GL::CURRENT_BIT|GL::LIGHTING_BIT) 
051  
052    # 材質の設定(表) 
053    GL.Material(GL::FRONT,GL::AMBIENT,  [0.1,0.1,0.0])
054    GL.Material(GL::FRONT,GL::DIFFUSE,  [0.7,0.5,0.1])
055    GL.Material(GL::FRONT,GL::SPECULAR, [0.5,0.7,0.5])
056    GL.Material(GL::FRONT,GL::SHININESS,60.0)
057  
058    # 材質の設定(裏) → 「GL.LightModel」「光源1の有効化」の行を有効にすること
059    #GL.Material(GL::BACK,GL::AMBIENT,  [0.0,0.0,0.0])
060    #GL.Material(GL::BACK,GL::DIFFUSE,  [1.0,0.0,1.0])
061    #GL.Material(GL::BACK,GL::SPECULAR, [0.0,0.0,0.5])
062    #GL.Material(GL::BACK,GL::SHININESS,120.0)
063    
064    # 面データ
065    GL.Begin(GL::POLYGON)
066      GL.Normal(0.0,0.0,1.0) # 法線の設定
067      GL.Vertex( SZ, SZ,0.0)
068      GL.Vertex(-SZ, SZ,0.0)
069      GL.Vertex(-SZ,-SZ,0.0)
070      GL.Vertex( SZ,-SZ,0.0)
071    GL.End()
072  
073    GL.Disable(GL::LIGHTING) # シェーディングを一旦OFFにする
074  
075    # 法線を表す棒
076    GL.Color(1.0,0.0,0.0)
077    GL.Begin(GL::LINES)
078      GL.Vertex(0.0,0.0,0.0)
079      GL.Vertex(0.0,0.0,1.0)
080    GL.End()
081  
082    GL.Enable(GL::LIGHTING)  # シェーディングを再度ONにする
083  
084    # 退避しておいた属性データをもとに戻す
085    GL.PopAttrib()
086  }
087  # 生成したplaneを描画するオブジェクトの一覧に登録しておく
088  __gl_objs.register(plane)
089  
090  #### 描画コールバック ########
091  display = Proc.new {
092    GL.Clear(GL::COLOR_BUFFER_BIT|GL::DEPTH_BUFFER_BIT)
093    set_light_position(__lightphi,__lightpsi) # 光源の配置
094  
095    ### マテリアルの設定とオブジェクトの描画
096    GL.Material(GL::FRONT,GL::AMBIENT,  DEFAULT_AMBIENT)
097    GL.Material(GL::FRONT,GL::DIFFUSE,  DEFAULT_DIFFUSE)
098    GL.Material(GL::FRONT,GL::SPECULAR, DEFAULT_SPECULAR)
099    GL.Material(GL::FRONT,GL::SHININESS,DEFAULT_SHININESS)
100    
101    __gl_objs.draw        
102  
103    # [マテリアル設定に関する補足] 
104    #
105    # __gl_objs.drawでの描画処理において,上で定義しているマテリアルは
106    # 利用される場合もあれば利用されない場合もある.
107    # とくに設定を変えない限りは上のマテリアルを使うようになっている.
108    # __gl_objsには内部で予め定義されているマテリアルのリストがあり,
109    # 上で定義しているマテリアルはそのリストの1要素として扱われる.
110    # 利用するマテリアルは順次変更できるようになっていて,リストの
111    # 要素となっているマテリアルが巡回的に選択されて適用される.
112    # (keyboardコールバックを参照のこと).
113  
114    GLUT.SwapBuffers()
115  }
116  
117  #### キーボード入力コールバック ########
118  keyboard = Proc.new { |key,x,y| 
119    case key
120    # [j],[J]: 経度の正方向/逆方向にカメラを移動する
121    when 'j','J'
122      __camera.move((key == 'j') ? DT : -DT,0)
123    # [k],[K]: 緯度の正方向/逆方向にカメラを移動する
124    when 'k','K'
125      __camera.move(0,(key == 'k') ? DT : -DT)
126    # [z],[Z]: zoom in/out
127    when 'z','Z'
128      __camera.zoom((key == 'z') ? DD : -DD)
129    # [o],[O]: オブジェクトの変更(正順,逆順)
130    when 'o','O'
131      dir = (key == 'o') ? GLObject::NEXT : GLObject::PREV
132      __gl_objs.switch(GLObject::OBJECT,dir)
133    # [m],[M]: マテリアルの変更(正順,逆順)
134    when 'm','M'
135      dir = (key == 'm') ? GLObject::NEXT : GLObject::PREV
136      __gl_objs.switch(GLObject::MATERIAL,dir)
137    # [i],[I]: 光源0の位置を変更する
138    when 'i','I'
139      dp = (key == 'i') ? DT : -DT
140      __lightpsi = (__lightpsi + dp) % 360
141    # [u],[U]: 光源0の位置を変更する
142    when 'u','U'
143      dp = (key == 'u') ? DT : -DT
144      __lightphi = (__lightphi + dp) % 360
145    # [r]: カメラを初期状態に戻す
146    when 'r'
147      __camera.reset
148    # [R]: 光源を初期状態に戻す
149    when 'R'
150      __lightphi = L_INIT_PHI
151      __lightpsi = L_INIT_PSI
152    # [q]: 終了する
153    when 'q'
154      exit 0
155    end
156    exit 0 if key.ord == 0x1b
157    
158    GLUT.PostRedisplay()
159  }
160  
161  #### ウインドウサイズ変更コールバック ########
162  reshape = Proc.new { |w,h|
163    GL.Viewport(0,0,w,h)
164    __camera.projection(w,h) 
165    GLUT.PostRedisplay()
166  }
167  
168  #### シェーディングの設定 ########
169  def init_shading
170    # 光源0(環境光,拡散,鏡面成分) 白色
171    GL.Light(GL::LIGHT0,GL::AMBIENT, [0.1,0.1,0.1])
172    GL.Light(GL::LIGHT0,GL::DIFFUSE, [1.0,1.0,1.0])
173    GL.Light(GL::LIGHT0,GL::SPECULAR,[1.0,1.0,1.0])
174  
175    # 光源1(環境光,拡散,鏡面成分) 黄色
176    GL.Light(GL::LIGHT1,GL::AMBIENT, [0.1,0.1,0.1])
177    GL.Light(GL::LIGHT1,GL::DIFFUSE, [1.0,1.0,0.2])
178    GL.Light(GL::LIGHT1,GL::SPECULAR,[1.0,1.0,1.0])
179    
180    GL.Enable(GL::LIGHTING)  # シェーディング処理ON
181    GL.Enable(GL::LIGHT0)    # 光源0の有効化
182    # GL.Enable(GL::LIGHT1)   # 光源1の有効化
183    GL.Enable(GL::NORMALIZE) # 法線の自動単位ベクトル化
184  
185    ## フラットシェーディングモードにする
186    # GL.ShadeModel(GL::FLAT)
187  
188    ## 両面照明のON(BACKを有効にする)
189    ## GL.LightModel(GL::LIGHT_MODEL_TWO_SIDE,GL::TRUE)
190  end
191  
192  ##### main ##############################################
193  
194  GLUT.Init()
195  GLUT.InitDisplayMode(GLUT::RGB|GLUT::DOUBLE|GLUT::DEPTH)
196  GLUT.InitWindowSize(WSIZE,WSIZE) 
197  GLUT.CreateWindow('Shading Test')
198  GLUT.DisplayFunc(display)        
199  GLUT.KeyboardFunc(keyboard)      
200  GLUT.ReshapeFunc(reshape)
201  GL.Enable(GL::DEPTH_TEST)
202  GL.LineWidth(2.0) # 線を描画する幅(法線の描画に利用する)
203  init_shading()    # シェーディングの設定
204  __camera.set      # カメラを配置する
205  GLUT.MainLoop()

シェーディング処理に必要な設定

OpenGLでシェーディング処理を行うには次のような設定が必要となります.

光源の設定

OpenGLの光源は,大きさをもたない点光源として扱われます. 光源は複数配置できます. 各光源は,予め決められた名前をもっています. 最初の光源は,GL::LIGHT0と呼ばれ,あとは順にGL::LIGHT1GL::LIGHT2のように呼ばれます. 配置できる光源の数は,システムによって定められています.

光源には,拡散光,鏡面光,環境光という成分が定義されます.各成分の色がRGBで定義されます. 光源の拡散光,鏡面光,環境光は,それぞれ拡散反射,鏡面反射,環境光反射に関する光源の寄与を表します.

実は光源にはRGBA成分を指定することになっていますが,Ruby/OpenGLでは,A成分は通常指定しなくてもかまいません.

光源は他の物体と同様に世界座標系に配置されます. 光源がある方向の無限遠の彼方にあるかのように配置することもできます. それによって,太陽光のような平行光をもたらす光源を表現することができます.

無限遠の光源でない場合, スポットライトのように方向性をもった光源を指定することもできます(GL::SPOT_DIRECTION,GL::SPOT_CUTOFF). 無限遠にない点光源については,光の減衰をシミュレートすることもできます.

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


170    # 光源0(環境光,拡散,鏡面成分) 白色
171    GL.Light(GL::LIGHT0,GL::AMBIENT, [0.1,0.1,0.1])
172    GL.Light(GL::LIGHT0,GL::DIFFUSE, [1.0,1.0,1.0])
173    GL.Light(GL::LIGHT0,GL::SPECULAR,[1.0,1.0,1.0])

光源の位置は,4次元の座標(x,y,z,w)で指定します. 無限遠の位置の光源の場合は,w = 0.0とします. (x,y,z)が光源の方向を表します. 一方,無限遠でない場合の光源はw ≠ 0.0とします. そのとき(x/w,y/w,z/w)が光源の位置を表します.

039    # 無限遠の光源(平行光線)
040    phi *= DEG2RAD;  psi *= DEG2RAD; 
041    z = Math.cos(phi); r = Math.sin(phi)
042    x = r*Math.cos(psi); y = r*Math.sin(psi)
043    GL.Light(GL::LIGHT0,GL::POSITION,[x,y,z,0.0]) 

光源の位置はモデル・ビュー変換行列に影響を受けます.

w ≠ 0.0の場合,光が円錐形に拡がるようなスポットライトの効果をもたせることができます. このときGL::SPOT_CUTOFFで光の拡がる角度を指定できます. 円錐の中心軸と母線のなす角度を指定します(0-90). このとき中心軸の方向も指定します(GL::SPOT_DIRECTION). なおGL::SPOT_CUTOFF=180に設定すると,全方位に光を放つ光源となります(w ≠ 0.0).

  GL.Light(GL::LIGHT0,GL::POSITION,[x,y,z,1.0]) 
  GL.Light(GL::LIGHT0,GL::SPOT_DIRECTION,[0.0,0.0,-1.0])
  GL.Light(GL::LIGHT0,GL::SPOT_CUTOFF,45.0) 

材質の設定

物体の材質は,拡散,鏡面,環境光に関する反射率,放射光とshininessで定義されます. 反射率,放射光はRGBの各成分ごとに指定します. 透過光の処理を行う場合には,A成分も指定します. 反射率,放射光の値の範囲は[0.0,1.0]です. shininessは鏡面反射の集中度を表すパラメタです. この値が大きいほど,特定の方向から明るく見えるということになります. shininessの値の範囲は[0.0,128.0]です.

なお,各物体について,必ずしもここで挙げたすべての項目を指定する必要はありません. 指定しない項目については,それまでに指定した値が使われます. 最初から何も指定しない場合には,デフォルトの値が使われることになります. たとえば,デフォルトでは放射光は0に設定されています. したがって,明示的に設定しない限り,物体が自ら光を放つことはありません.

サンプルプログラムでは,面オブジェクトに独自の材質を設定しています.


052    # 材質の設定(表) 
053    GL.Material(GL::FRONT,GL::AMBIENT,  [0.1,0.1,0.0])
054    GL.Material(GL::FRONT,GL::DIFFUSE,  [0.7,0.5,0.1])
055    GL.Material(GL::FRONT,GL::SPECULAR, [0.5,0.7,0.5])
056    GL.Material(GL::FRONT,GL::SHININESS,60.0)

法線方向の設定

最初に述べた通り,物体上のある点での光の反射をシミュレートするには,その点での面の法線方向,入射光の方向,観測方向を定める必要があります. 入射光の方向は光源の位置で決まり,観測方向はカメラの位置で決まります. 面の法線方向については,原則として各頂点ごとに指定することになります. 同じ法線をもつ頂点が複数あれば,それらの頂点については,法線方向の設定は,最初に一度行うだけで構いません.

サンプルプログラムでは,面オブジェクトを作成して,そのオブジェクトについて法線を設定しています.


064    # 面データ
065    GL.Begin(GL::POLYGON)
066      GL.Normal(0.0,0.0,1.0) # 法線の設定
067      GL.Vertex( SZ, SZ,0.0)
068      GL.Vertex(-SZ, SZ,0.0)
069      GL.Vertex(-SZ,-SZ,0.0)
070      GL.Vertex( SZ,-SZ,0.0)
071    GL.End()

観測者の方向に関する設定について

物体上のある点に関して,観測者の方向はカメラ位置で決まります. この方向は,点ごとに異なることになります. そこで本来は,描画する全ての点について,観測者の方向を計算しなければならないことになります.

しかしこの処理は負担がかかるため,初期状態では,処理を簡略化して描画する各面について観測者の方向を一定としています. 実際,観測者が物体から遠く離れていれば,この仮定は妥当なものとなります. 次の設定を行えば,描画する面の各頂点で観測者の方向を計算することになります.


  GL.LightModel(GL::LIGHT_MODEL_LOCAL_VIEWER,GL::TRUE)

GL::TRUEの代わりにGL::FALSEを指定すれば,上で述べたように処理を簡略化します.

シェーディング処理と光源のON/OFF

シェーディング処理を実際に行うには,シェーディング処理を有効にすること, および光源を点灯することが必要です. シェーディング処理を有効にするには,GL.Enable(GL::LIGHTING)を実行します. また光源については,各光源ごとに別々に設定を行います. たとえば,光源0(LIGHT0)を点灯するには,GL.Enable(GL::LIGHT0)とします. なお,シェーディング処理を有効にしている場合,物体の色は材質によって決まり,GL.Colorで指定した色は無視されます.

シェーディング処理を行わない,あるいは光源を消したい場合には, GL.Enableの代わりに GL.Disableを実行します. GL.Disableでシェーディング処理を無効にすれば,GL.Colorで色をつけて描画を行うことができます.

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


073    GL.Disable(GL::LIGHTING) # シェーディングを一旦OFFにする
074  
075    # 法線を表す棒
076    GL.Color(1.0,0.0,0.0)
077    GL.Begin(GL::LINES)
078      GL.Vertex(0.0,0.0,0.0)
079      GL.Vertex(0.0,0.0,1.0)
080    GL.End()
081  
082    GL.Enable(GL::LIGHTING)  # シェーディングを再度ONにする
 :
168  #### シェーディングの設定 ########
169  def init_shading
170    # 光源0(環境光,拡散,鏡面成分) 白色
171    GL.Light(GL::LIGHT0,GL::AMBIENT, [0.1,0.1,0.1])
172    GL.Light(GL::LIGHT0,GL::DIFFUSE, [1.0,1.0,1.0])
173    GL.Light(GL::LIGHT0,GL::SPECULAR,[1.0,1.0,1.0])
 :
180    GL.Enable(GL::LIGHTING)  # シェーディング処理ON
181    GL.Enable(GL::LIGHT0)    # 光源0の有効化

[CG実習 > シェーディング]