光源と物体
われわれが目にする物体の色は,光源(light source)と物体の材質(material)によって決まります. 光源から物体に届く光は,物体の特性により表面で反射したり,物体内で吸収されたり,あるいは物体を透過します. 反射光,透過光は,別の物体に届いて,また反射したり,吸収されたり,透過したりします. このような光源の発光,物体における反射,吸収,透過の複雑な相互作用の結果として目に入る光が,われわれの見る物体の色となります. シェーディングの処理では,本来このような現象をすべて考慮する必要があります. しかしこれらの現象をすべて忠実にシミュレートすることは容易ではありません.
CGではしばしば簡易的なモデルのもとでシェーディング処理を行います. OpenGLでは,シェーディング処理は各面に関してそれぞれ独立に行われます. つまりOpenGLでは,物体間での光の相互反射は扱われません.また影も作られません. このように単純化しても,十分に現実感のある画像を生成できます. また単純化によって,高速な描画が可能になり,廉価なコンピュータでも実時間のアニメーションを行うことが容易になります.
光の反射モデル

点光源を考えます. 点光源が物体を照らしているとき,その物体表面の点での光の反射をモデル化するために,まず次の3要素を考えます.
- 光源の方向
- その点での(物体表面の)曲面の法線の方向
- カメラ(観測者)の方向
ここで扱う反射モデルでは,これらの幾何的なデータと光源の特性と物体の材質の特性とを利用して,次の3種類の異なる反射を定義し,その組み合わせでさまざまな反射を表現します.
- 鏡面反射(specular reflection)
- 拡散反射(diffuse reflection)
- 環境光反射(ambient refrection)
鏡面反射
鏡面反射は,滑らかな面での反射をシミュレートしたものです. 完全な鏡面では,光は正反射方向のみに反射されます. この方向は,光源の方向と面の法線方向によって決まります. 完全な鏡面でなくてもある程度滑らかであれば,光は正反射方向を中心とした狭い範囲に反射されます.
鏡面反射の効果は,観測方向に依存することになります. また入射光,反射率,さらに鏡面性の度合い(shininess)にも依存します.
拡散反射
拡散反射は,ざらついていて光沢のない面での反射をシミュレートしたものです. そのような面では,ミクロなレベルでは微小な面がいろいろな方向を向いていて,それぞれにおいての反射の結果として,いろいろな方向に光が反射されるように見えます. また物体の内部に進入した光が,複雑な過程を経て,再度,物体から出てくる場合もあります.
完全な拡散反射面は,どこから見てもに同じ色に見えることになります. このように,反射のうちの拡散反射成分は,観測する方向によらず,入射光とその入射角度,反射率のみで決まります. 入射角度は,光源方向と法線方向を用いて決めることができます.
環境光反射
物体間の光の相互反射の影響を簡易的に一定の強さの環境光として表現します. 環境光は方向性を持ちません. その強さと物体の反射率によってのみ,シェーディング処理での環境光の影響が決まります.
[参考] 放射光
シーンにおいて一般に光源が直接観測される場合があります. OpenGLでは,光源は点光源として設定して,光源のデータにしたがって,シェーディング処理が行われます. しかし光源はシーンで観測される位置にあったとしても,点光源として計算のために使われるだけで,光源自体は描かれません.
そのような場合,光源に対応する物体を配置して,その物体に放射光の属性を与えることによって光源を表現できます. ただしOpenGLでは,すでに述べた通り,物体間の相互作用は考慮されません. したがって,この放射光が他の物体を照らす効果は,シェーディング処理には組み込まれません.
サンプルプログラム
ここで,サンプルプログラム(shaded_objects.rb)を見て下さい. 各行の左端の数字は,説明のために付けた行の番号で,プログラムにはこの行番号は含まれません.
このプログラムでは,物体を原点を中心に配置します. また常に原点を向いたカメラが用意されています. キーを押すことで,カメラを原点を中心に動かせるようになっています. 操作方法は次の通りです.
- [j],[J]を押すことで,カメラ位置の経度が変わります.
- [k],[K]を押すことで,カメラ位置の緯度が変わります.
- [z],[Z]を押すことで,カメラの原点からの距離が変わります.
- [i],[I],[u],[U]を押すことで光源を動かすことができます.
- [r]を押すとカメラが最初の位置に戻ります.
- [R]を押すと光源が最初の位置に戻ります.
- [o],[O]を押すことで描画される物体が変わります.
- [m],[M]を押すことで物体の材質(material)が変わります.
- [q],[ESC]を押すとプログラムが終了します.
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::LIGHT1,GL::LIGHT2のように呼ばれます. 配置できる光源の数は,システムによって定められています.
光源には,拡散光,鏡面光,環境光という成分が定義されます.各成分の色がRGBで定義されます. 光源の拡散光,鏡面光,環境光は,それぞれ拡散反射,鏡面反射,環境光反射に関する光源の寄与を表します.
光源は他の物体と同様に世界座標系に配置されます. 光源がある方向の無限遠の彼方にあるかのように配置することもできます. それによって,太陽光のような平行光をもたらす光源を表現することができます.
無限遠の光源でない場合, スポットライトのように方向性をもった光源を指定することもできます(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の有効化