サンプルプログラム
まずサンプルプログラム(windmill_3d.rb)を見て下さい. 各行の左端の数字は,説明のために付けた行の番号で,プログラムにはこの行番号は含まれません.
このプログラムは,風力発電の風車を模したものです. ただし,それらしく見えるように形状を作ってみただけで,工学的な設計はまったく行っていません. 操作方法は次の通りです.
- [j]/[J],[k]/[K],[l]/[L]で風車の姿勢をさまざまに変えることができます.
- [c]/[C]で風車の頭部の向きを変えることができます.
- [r]で風車を回転/停止できます.回転中も風車の姿勢などを変えることができます.
- [q],[ESC]でプログラムを終了します.
001 # coding: utf-8
002 require 'opengl'
003 require 'glu'
004 require 'glut'
005 require 'cg/camera'
006
007 #### カメラ,光源,ウインドウに関する定数定義
008 INIT_THETA = 0.0 # カメラの初期位置
009 INIT_PHI = 0.0 # カメラの初期位置
010 INIT_DIST = 12.0 # カメラの原点からの距離の初期値
011 L_INIT_PHI = 45.0 # 光源の初期位置
012 L_INIT_PSI = 45.0 # 光源の初期位置
013
014 DT = 3 # 回転角単位
015 DD = 0.125 # カメラの原点からの距離変更の単位
016
017 WSIZE = 900
018 DEG2RAD = Math::PI/180.0
019 SLEEP_TIME=0.05
020
021 #### 光源の位置
022 def set_light_position(phi,psi)
023 # 無限遠の光源(平行光線)
024 phi *= DEG2RAD; psi *= DEG2RAD;
025 z = Math.cos(phi); r = Math.sin(phi)
026 x = r*Math.cos(psi); y = r*Math.sin(psi)
027 GL.Light(GL::LIGHT0,GL::POSITION,[x,y,z,0.0])
028 GL.Light(GL::LIGHT1,GL::POSITION,[0.0,-1.0,-1.0,0.0])
029 end
030
031 #### u×vの方向の単位ベクトルを求めるメソッド
032 def unit_cross_product(u,v)
033 y = [u[1]*v[2]-u[2]*v[1],u[2]*v[0]-u[0]*v[2],u[0]*v[1]-u[1]*v[0]]
034 l = Math.sqrt(y[0]*y[0] + y[1]*y[1] + y[2]*y[2])
035 y.collect! {|c| c/l}
036 end
037
038 #============================================================#
039 #======================== 風車の定義 ========================#
040 #============================================================#
041
042 # 風車の構成
043 # 羽根 x 3,回転軸,支柱,基台
044
045 ## 羽根の形状パラメタ
046 BLADE_LENGTH = 3.0
047 BLADE_LENGTH2 = 0.6
048 BLADE_WIDTH = 0.3
049 BLADE_DEPTH = 0.2
050 BLADE_NORMAL0 = [0.0,0.0,-1.0]
051 BLADE_NORMAL1 = unit_cross_product([BLADE_LENGTH2,-BLADE_WIDTH,0.0],[0.0,0.0,1.0])
052 BLADE_NORMAL2 = [0.0,1.0,0.0]
053 BLADE_NORMAL3 = unit_cross_product([BLADE_LENGTH2,-BLADE_WIDTH,-BLADE_DEPTH],
054 [BLADE_LENGTH,0.0,-BLADE_DEPTH])
055
056 ## 回転軸の形状パラメタ
057 AXLE_BOXSIZE = 0.46
058 AXLE_SPHERE_SLICES = 12
059 AXLE_SPHERE_STACKS = AXLE_SPHERE_SLICES
060 AXLE_SCALE = 1.75
061 AXLE_HEIGHT = AXLE_BOXSIZE*AXLE_SCALE*0.75
062
063 ## 支柱の形状パラメタ
064 ROD_TOP_RADIUS = 0.37*AXLE_BOXSIZE
065 ROD_BASE_RADIUS = ROD_TOP_RADIUS
066 ROD_HEIGHT = 5
067 ROD_SLICES = 12
068 ROD_STACKS = 6
069
070 ## 基台の形状パラメタ
071 BASE_TOP_RADIUS = ROD_BASE_RADIUS
072 BASE_BASE_RADIUS = 1.5*BASE_TOP_RADIUS
073 BASE_HEIGHT = 0.2
074 BASE_SLICES = 12
075 BASE_STACKS = 6
076 BASE_RINGS = 6
077
078 ## 風車
079 def windmill(quad,blade_angle,head_angle)
080 # マテリアル
081 GL.Material(GL::FRONT,GL::AMBIENT, [0.2,0.2,0.2])
082 GL.Material(GL::FRONT,GL::DIFFUSE, [0.8,0.8,0.8])
083 GL.Material(GL::FRONT,GL::SPECULAR, [0.0,0.0,0.0])
084 GL.Material(GL::FRONT,GL::SHININESS,0.0)
085
086 # 回転軸
087 GL.PushMatrix()
088 GL.Translate(0.0,BASE_HEIGHT+ROD_HEIGHT,0.0)
089 GL.Rotate(head_angle,0.0,1.0,0.0) # 頭部の回転角
090 axle(quad)
091
092 # 羽根
093 GL.Translate(0.0,0.0,AXLE_HEIGHT*0.75) # 回転軸に合わせて羽根の位置を調整する
094 GL.Rotate(blade_angle,0.0,0.0,1.0) # 羽根の現在の回転角
095 3.times do |i|
096 blade()
097 GL.Rotate(120,0.0,0.0,1.0) # 3枚の羽根の配置
098 end
099 GL.PopMatrix()
100
101 GL.PushMatrix()
102 # 台と支柱の中心軸をy軸とするために回転
103 # (定義時はz軸が中心軸となっている)
104 GL.Rotate(-90.0,1.0,0.0,0.0)
105
106 # 基台
107 base(quad)
108
109 # 支柱
110 GL.Translate(0.0,0.0,BASE_HEIGHT)
111 rod(quad)
112 GL.PopMatrix()
113 end
114
115 # 羽根(三角錐;三角形4面で構成)
116 def blade
117 GL.Begin(GL::TRIANGLES)
118 #
119 GL.Normal(BLADE_NORMAL0)
120 GL.Vertex(0.0,0.0,0.0) # v0
121 GL.Vertex(BLADE_LENGTH,0.0,0.0) # v1
122 GL.Vertex(BLADE_LENGTH2,-BLADE_WIDTH,0.0) # v2
123 #
124 GL.Normal(BLADE_NORMAL1)
125 GL.Vertex(0.0,0.0,0.0) # v0
126 GL.Vertex(BLADE_LENGTH2,-BLADE_WIDTH,0.0) # v2
127 GL.Vertex(0.0,0.0,BLADE_DEPTH) # v3
128 #
129 GL.Normal(BLADE_NORMAL2)
130 GL.Vertex(0.0,0.0,0.0) # v0
131 GL.Vertex(0.0,0.0,BLADE_DEPTH) # v3
132 GL.Vertex(BLADE_LENGTH,0.0,0.0) # v1
133 #
134 GL.Normal(BLADE_NORMAL3)
135 GL.Vertex(BLADE_LENGTH,0.0,0.0) # v1
136 GL.Vertex(0.0,0.0,BLADE_DEPTH) # v3
137 GL.Vertex(BLADE_LENGTH2,-BLADE_WIDTH,0.0) # v2
138 GL.End()
139 end
140
141 # 回転軸(半球と立方体を組み合わせて前後に引き延ばした図形)
142 def axle(quad)
143 s = AXLE_BOXSIZE/2
144 GL.PushMatrix()
145 # 前後に引き延ばす
146 GL.Scale(1.0,1.0,AXLE_SCALE)
147 # 後部(ボックス)
148 GLUT.SolidCube(AXLE_BOXSIZE)
149 # 前部(楕円球)
150 GL.PushMatrix()
151 GL.Translate(0.0,0.0,s)
152 GLU.Sphere(quad,s,AXLE_SPHERE_SLICES,AXLE_SPHERE_STACKS)
153 GL.PopMatrix()
154 GL.PopMatrix()
155 end
156
157 # 支柱(円柱面)
158 def rod(quad)
159 # 側面(z=0を底面,z軸を中心軸とする円柱面)
160 GLU.Cylinder(quad,
161 ROD_BASE_RADIUS,ROD_TOP_RADIUS,ROD_HEIGHT,
162 ROD_SLICES,ROD_STACKS)
163 end
164
165 # 基台(円錐台)
166 def base(quad)
167 # 側面(z=0を底面,z軸を中心軸とする円錐台の側面)
168 GLU.Cylinder(quad,
169 BASE_BASE_RADIUS,BASE_TOP_RADIUS,BASE_HEIGHT,
170 BASE_SLICES,BASE_STACKS)
171 # 底面(円)
172 GLU.Disk(quad,0,BASE_BASE_RADIUS,BASE_SLICES,BASE_RINGS)
173 end
174
175 #============================================================#
176 #======================== 地面の定義 ========================#
177 #============================================================#
178
179 GROUND_SIZE=BASE_BASE_RADIUS*6.0 # 地面の広さ
180
181 def ground
182 # マテリアル
183 GL.Material(GL::FRONT,GL::AMBIENT, [0.0,0.0,0.0])
184 GL.Material(GL::FRONT,GL::DIFFUSE, [0.194,0.049,0.049])
185 GL.Material(GL::FRONT,GL::SPECULAR, [0.0,0.0,0.0])
186 GL.Material(GL::FRONT,GL::SHININESS,0.0)
187
188 ## 立方体を平たく変形して「地面」を作る
189 ## y = 0が上面になるように位置調整する.
190 GL.PushMatrix()
191 GL.Translate(0.0,-0.1*GROUND_SIZE,0.0)
192 GL.Scale(1.0,0.1,1.0)
193 GLUT.SolidCube(2.0*GROUND_SIZE)
194 GL.PopMatrix()
195 end
196
197 #============================================================#
198
199 ## 状態変数
200 __blade_angle=90 # 羽根の回転角
201 __head_angle=0 # 風車頭部の回転角
202 __anim_on = false # 風車回転アニメーションのON/OFF
203 __camera = Camera.new(INIT_THETA,INIT_PHI,INIT_DIST)
204 __lightphi = L_INIT_PHI
205 __lightpsi = L_INIT_PSI
206
207 # 二次曲面の生成に必要なオブジェクトの生成とパラメタ設定
208 __quad = GLU.NewQuadric()
209 GLU.QuadricDrawStyle(__quad,GLU::FILL)
210 GLU.QuadricNormals(__quad,GLU::SMOOTH)
211
212 #### 描画コールバック ########
213 display = Proc.new {
214 GL.Clear(GL::COLOR_BUFFER_BIT|GL::DEPTH_BUFFER_BIT)
215 set_light_position(__lightphi,__lightpsi)
216 GL.PushMatrix()
217 GL.Translate(0.0,-3.5,0.0)
218 ground()
219 windmill(__quad,__blade_angle,__head_angle)
220 GL.PopMatrix()
221 GLUT.SwapBuffers()
222 }
223
224 #### アイドルコールバック ########
225 idle = Proc.new {
226 __blade_angle = (__blade_angle + DT) % 360 # 羽根の角度の更新
227 sleep(SLEEP_TIME)
228 GLUT.PostRedisplay()
229 }
230
231 #### キーボード入力コールバック ########
232 keyboard = Proc.new { |key,x,y|
233 case key
234 # [SPACE]: アニメーション開始/停止
235 when ' '
236 if __anim_on
237 __anim_on = false
238 GLUT.IdleFunc(nil)
239 else
240 __anim_on = true
241 GLUT.IdleFunc(idle)
242 end
243 # [j],[J]: 経度の正方向/逆方向にカメラを移動する
244 when 'j','J'
245 __camera.move((key == 'j') ? DT : -DT,0,0)
246 # [k],[K]: 緯度の正方向/逆方向にカメラを移動する
247 when 'k','K'
248 __camera.move(0,(key == 'k') ? DT : -DT,0)
249 # [l],[L]:
250 when 'l','L'
251 __camera.move(0,0,(key == 'l') ? DT : -DT)
252 # [z],[Z]: zoom in/out
253 when 'z','Z'
254 __camera.zoom((key == 'z') ? DD : -DD)
255 # [c],[C]: 風車頭部の回転角の増減
256 when 'c','C'
257 dir = (key == 'c') ? 1 : -1
258 __head_angle = (__head_angle + dir*DT) % 360
259 # [r]: 風車の姿勢を初期状態にリセット
260 when 'r'
261 __blade_angle = 90
262 __head_angle = 0
263 __lightphi = L_INIT_PHI
264 __lightpsi = L_INIT_PSI
265 __camera.reset
266 # [i],[I]: 光源0の位置を変更する
267 when 'i','I'
268 dp = (key == 'i') ? DT : -DT
269 __lightpsi = (__lightpsi + dp) % 360
270 # [u],[U]: 光源0の位置を変更する
271 when 'u','U'
272 dp = (key == 'u') ? DT : -DT
273 __lightphi = (__lightphi + dp) % 360
274 # [q],[ESC]: 終了する
275 when 'q'
276 exit 0
277 end
278 exit 0 if key.ord == 0x1b
279
280 GLUT.PostRedisplay()
281 }
282
283 #### ウインドウサイズ変更コールバック ########
284 reshape = Proc.new { |w,h|
285 GL.Viewport(0,0,w,h)
286 __camera.projection(w,h)
287 GLUT.PostRedisplay()
288 }
289
290 #### シェーディングの設定 ########
291 def init_shading
292 # 光源の環境光,拡散,鏡面成分と位置の設定
293 GL.Light(GL::LIGHT0,GL::AMBIENT, [0.1,0.1,0.1])
294 GL.Light(GL::LIGHT0,GL::DIFFUSE, [1.0,1.0,1.0])
295 GL.Light(GL::LIGHT0,GL::SPECULAR,[1.0,1.0,1.0])
296
297 # シェーディング処理ON,光源(No.0)の配置
298 GL.Enable(GL::LIGHTING)
299 GL.Enable(GL::LIGHT0)
300 GL.Enable(GL::NORMALIZE) # 法線の自動単位ベクトル化
301 end
302
303 ##### main ##############################################
304 GLUT.Init()
305 GLUT.InitDisplayMode(GLUT::RGB|GLUT::DOUBLE|GLUT::DEPTH)
306 GLUT.InitWindowSize(WSIZE,WSIZE)
307 GLUT.CreateWindow('Windmill (3D)')
308 GLUT.DisplayFunc(display)
309 GLUT.KeyboardFunc(keyboard)
310 GLUT.ReshapeFunc(reshape)
311 GL.Enable(GL::DEPTH_TEST)
312 init_shading() # シェーディングの設定
313 __camera.set # カメラを配置する
314 GL.ClearColor(0.4,0.4,1.0,0.0)
315 GLUT.MainLoop()
図形の変換
基本的な図形に変換を施すことで図形を変形して,一つの図形をさまざまな部品に仕立て上げることができます. ここでは,平行移動,回転,スケーリングの3種類の変換とそれらの合成変換について,OpenGLに基づいて説明します.
これらの変換はすべて同次座標(homogeneous coordinates)に基づく変換行列によって表現できます. OpenGLでは,モデル・ビュー変換用の行列を設定して,それにより図形に施す変換を定めます.
平行移動
平行移動は,単純に指定した量だけ図形を動かす変換です. 移動量は,世界座標系(右手系)の3次元ベクトルで表されます. OpenGLでは,平行移動はGL.Translate(dx,dy,dz)で実行します.
回転
回転は,1本の直線を軸として,指定した角度だけ図形を回す変換です. OpenGLでは,回転軸は常に原点を通る直線で与えられます. つまり軸は3次元ベクトル一つで表現されます. この軸を表すベクトルの方向に右ネジが進むとき,そのネジの回転方向が回転の正方向と定義されます. 回転はGL.Rotate(angle,lx,ly,lz)で実行できます. angleは度(degree)で指定します(radianではありません).
スケーリング
スケーリングは,指定した割合で拡大縮小を行う変換です. OpenGLでは,スケーリングは原点を中心として行われます. スケーリング係数(拡大縮小率)は,x,y,zの各軸に関して別々に設定できます. スケーリングは,GL.Scale(sx,sy,sz)で実行できます.
合成変換
OpenGLでは,回転は常に原点まわりの回転です. では,別の点Cを通る回転軸のまわりの回転を行うにはどうすればよいのでしょうか. 少し考えれば,そのような回転は,点Cを原点に移す平行移動,原点まわりの回転,原点を点Cに移す平行移動の組み合わせ,つまり合成変換で表現できることが分かります.
合成変換は,変換を順に並べることで表現できます. たとえば,点Cを通る回転軸まわりの回転は,次のように実現できます.
# C = (cx,cy,cz)を通る軸での回転
GL.Translate(cx,cy,cz)
GL.Rotate(theta,lx,ly,lz)
GL.Translate(-cx,-cy,-cz)
# その回転をかけてポリゴンを描く
GL.Begin(GL::POLYGON)
:
:
GL.End()
このとき,変換の順番に注意して下さい. このコードでは,まず原点を点Cに移す平行移動,次に回転,そして最後に点Cを原点に移す平行移動の順に変換を適用しているかのように見えますが,実際の適用は逆順になります. すなわち後に記述した変換から先に適用されるわけです. 描画する図形の記述(上の例では,GL.Begin(GL::POLYGON)...GL.End())に近い方から,順に適用されると考えるとよいかもしれません.
階層的形状モデリング
サンプルプログラムの風車は,次のような部品から構成されています. これらの部品をお互いに関連付けることで物体全体の形状が作り上げられています.
- 基台
- 支柱
- 回転軸
- 羽根(3枚)
このように複数の部品を組み合わせた物体は,あるベースとなる部品に対して, 別の部品を接続して,それにさらにまた別の部品を接続してというようにして構成され,階層的な構造をもつことになります.
サンプルプログラムでは,風車の全体を回転させることができます. また,それとは別に風車の頭部の向きを変えることができます. 風車の羽根は回転軸に対して固定されていることから,回転軸部分と同時に羽根も同じように向きを変えます. つまり,羽根の姿勢は,回転軸部分の姿勢に影響を受けるわけです. さらに回転軸部分は支柱に接続されていて,支柱の姿勢が変われば,回転軸部分の姿勢が変わり,羽根の姿勢も変わります. これは,羽根の姿勢が間接的に支柱の姿勢に影響を受けることを意味しています. このように,階層的な構造をもつ物体では,よりベースに近い部品の挙動が,より末端に位置するの部品の挙動に影響を与えます.
OpenGLで,このような階層的な構造をもつ物体をモデリングするには,まず基本となる各部品を設計します. このとき部品自体を基準とした座標系で,形状のみを設計します. 部品を基準とする座標系で形状を記述することで設計が容易になります. あとは,設計した部品を移動,回転,あるいはスケーリングすることで,部品を適宜配置あるいは変形して全体の形状を設計していくことになります. すでに見た通り,これらの処理は,GL.Translate(),GL.Rotate,GL.Scale(),あるいはこれらの合成変換で実現できます.
変換によって部品を組み合わせて形状を設計する際,ベースとなる部品に対する変換に,さらにもう一段階変換を追加してかけることで部品を組み合わせるような場合と,他の部品とは独立して,ある一つの部品にだけ変換をかけたいような場合があります. たとえば風車の例では,風車の頭部は向きを変えることができますが,その姿勢は,支柱の姿勢を決める全体的な変換に,さらに支柱に対する頭部の向きを決める局所的な変換をかけることで決められます. また,回転軸部分は,半球と立方体を組み合わせた物体を前後に引き延ばすことで作られています. このスケーリング処理は,他の部品には適用しません.
このように変換を適宜合成,あるいは別個に適用するような仕組みは,変換行列をスタックで扱うことで実現することができます.
行列スタック
スタック(stack)とは,一般には,データを管理するための基本的な仕組み(データ構造)の一つです. スタックに新しいデータを入れていくと,データはスタックの中で順に縦に積まれていきます. 先に入れておいたデータは,スタックの奥に押しこまれます スタックから取り出せるのは,常に一番上のデータだけです. データを一つ取り出すと,もともと上から2番目にあったデータが一番上になりますので,こんどはそのデータを取り出すことが可能になります. データをスタックの一番上(top)に新たに積む操作をpush,スタックの一番上のデータ(top)を取り出す操作をpopといいます. 後に入れたデータが先に出てくるという性質から,スタックを後入れ先出し型(LIFO;Last In First Out)のデータ構造といいます.
「3次元CGの基礎」の資料の中で既に示した通り, OpenGLでは,描画する3次元シーンの設計とシーンの撮影方法の設定を,モデル・ビュー変換を表す行列と投影変換を表す行列の二つによって記述します. これらの二つの行列は,それぞれスタックで管理されます. スタックの一番上の行列が実際に変換に利用されることになります. スタックを操作する基本的なメソッドには次のようなものがあります.
- GL.MatrixMode()
モデル・ビュー変換行列(GL::MODELVIEW),投影変換行列(GL::PROJECTION)のどちらを操作するかを指定します. 形状モデリングには,モデル・ビュー変換を利用します.
- GL.PushMatrix()
現在のスタックの一番上の行列をコピーしてpushする. これにより,現在の変換行列を保存しつつ,それに別の変換を施して,新しい変換行列を合成することができます. GL.PopMatrixでもとの変換行列に戻すことができます.
- GL.PopMatrix()
現在のスタックの一番上の行列をpopして捨てる. GL.PushMatrixを使って新たな変換行列を作って利用した後に, もとの変換行列に戻す際にGL.PopMatrixを使います. このようにGL.PushMatrixとGL.PopMatrixはペアで使います.
- GL.LoadIdentity()
現在のスタックの一番上の行列を単位行列にします.
階層的な構造をもつ物体を設計する際に,ある部品(の集合)に独自に変換をかけたい場合は,GL.PushMatrixとGL.PopMatrixとをペアにして,その間で必要な変換を施し,部品(の集合)を描画することになります. GL.PushMatrixにより,その時点での変換行列を保持したまま新しい変換行列を作ることが可能となり,GL.PopMatrixにより元の変換行列を復元できますので,部品に施した変換が他の部品には影響を与えることはありません.
サンプルプログラムでも,風車の設計に随所で行列のPushとPopを利用しています. なお一度設計した物体を別途再利用する可能性を考えれば,物体にかかる変換の全体をGL.PushMatrixとGL.PopMatrixでくくっておいた方がよいでしょう.