サンプルプログラム
まずサンプルプログラム(windmill_3d.rb)を見て下さい. 各行の左端の数字は,説明のために付けた行の番号で,プログラムにはこの行番号は含まれません.
このプログラムは,風力発電の風車を模したものです. ただし,それらしく見えるように形状を作ってみただけで,工学的な設計はまったく行っていません. 操作方法は次の通りです.
- [j]/[J],[k]/[K],[l]/[L]で風車の姿勢をさまざまに変えることができます.
- [c]/[C]で風車の頭部の向きを変えることができます.
- [r]で風車を回転/停止できます.回転中も風車の姿勢などを変えることができます.
- [q],[ESC]でプログラムを終了します.
1 require "opengl"
2 require "glut"
3 require "camera"
4
5 #### カメラ,光源,ウインドウに関する定数定義
6 INIT_THETA = 0.0 # カメラの初期位置
7 INIT_PHI = 0.0 # カメラの初期位置
8 INIT_DIST = 12.0 # カメラの原点からの距離の初期値
9 L_INIT_PHI = 45.0 # 光源の初期位置
10 L_INIT_PSI = 45.0 # 光源の初期位置
11
12 DT = 3 # 回転角単位
13 DD = 0.125 # カメラの原点からの距離変更の単位
14
15 WSIZE = 600
16 DEG2RAD = Math::PI/180.0
17
18 #### 光源の位置
19 def set_light_position(phi,psi)
20 # 無限遠の光源(平行光線)
21 phi *= DEG2RAD; psi *= DEG2RAD;
22 z = Math.cos(phi); r = Math.sin(phi)
23 x = r*Math.cos(psi); y = r*Math.sin(psi)
24 GL.Light(GL::LIGHT0,GL::POSITION,[x,y,z,0.0])
25 GL.Light(GL::LIGHT1,GL::POSITION,[0.0,-1.0,-1.0,0.0])
26 end
27
28 #### u×vの方向の単位ベクトルを求めるメソッド
29 def unit_cross_product(u,v)
30 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]]
31 l = Math.sqrt(y[0]*y[0] + y[1]*y[1] + y[2]*y[2])
32 y.collect! {|c| c/l}
33 end
34
35 #============================================================#
36 #======================== 風車の定義 ========================#
37 #============================================================#
38
39 # 風車の構成
40 # 羽根 x 3,回転軸,支柱,基台
41
42 ## 羽根の形状パラメタ
43 BLADE_LENGTH = 3.0
44 BLADE_LENGTH2 = 0.6
45 BLADE_WIDTH = 0.3
46 BLADE_DEPTH = 0.2
47 BLADE_NORMAL0 = [0.0,0.0,-1.0]
48 BLADE_NORMAL1 = unit_cross_product([BLADE_LENGTH2,-BLADE_WIDTH,0.0],[0.0,0.0,1.0])
49 BLADE_NORMAL2 = [0.0,1.0,0.0]
50 BLADE_NORMAL3 = unit_cross_product([BLADE_LENGTH2,-BLADE_WIDTH,-BLADE_DEPTH],
51 [BLADE_LENGTH,0.0,-BLADE_DEPTH])
52
53 ## 回転軸の形状パラメタ
54 AXLE_BOXSIZE = 0.46
55 AXLE_SPHERE_SLICES = 12
56 AXLE_SPHERE_STACKS = AXLE_SPHERE_SLICES
57 AXLE_SCALE = 1.75
58 AXLE_HEIGHT = AXLE_BOXSIZE*AXLE_SCALE*0.75
59
60 ## 支柱の形状パラメタ
61 ROD_TOP_RADIUS = 0.37*AXLE_BOXSIZE
62 ROD_BASE_RADIUS = ROD_TOP_RADIUS
63 ROD_HEIGHT = 5
64 ROD_SLICES = 12
65 ROD_STACKS = 6
66
67 ## 基台の形状パラメタ
68 BASE_TOP_RADIUS = ROD_BASE_RADIUS
69 BASE_BASE_RADIUS = 1.5*BASE_TOP_RADIUS
70 BASE_HEIGHT = 0.2
71 BASE_SLICES = 12
72 BASE_STACKS = 6
73 BASE_RINGS = 6
74
75 ## 風車
76 def windmill(quad,blade_angle,head_angle)
77 # マテリアル
78 GL.Material(GL::FRONT_AND_BACK,GL::AMBIENT, [0.2,0.2,0.2])
79 GL.Material(GL::FRONT_AND_BACK,GL::DIFFUSE, [0.8,0.8,0.8])
80 GL.Material(GL::FRONT_AND_BACK,GL::SPECULAR, [0.0,0.0,0.0])
81 GL.Material(GL::FRONT_AND_BACK,GL::SHININESS,0.0)
82
83 # 回転軸
84 GL.PushMatrix()
85 GL.Translate(0.0,BASE_HEIGHT+ROD_HEIGHT,0.0)
86 GL.Rotate(head_angle,0.0,1.0,0.0) # 頭部の回転角
87 axle(quad)
88
89 # 羽根
90 GL.Translate(0.0,0.0,AXLE_HEIGHT*0.75) # 回転軸に合わせて羽根の位置を調整する
91 GL.Rotate(blade_angle,0.0,0.0,1.0) # 羽根の現在の回転角
92 3.times do |i|
93 blade()
94 GL.Rotate(120,0.0,0.0,1.0) # 3枚の羽根の配置
95 end
96 GL.PopMatrix()
97
98 GL.PushMatrix()
99 # 台と支柱の中心軸をy軸とするために回転
100 # (定義時はz軸が中心軸となっている)
101 GL.Rotate(-90.0,1.0,0.0,0.0)
102
103 # 基台
104 base(quad)
105
106 # 支柱
107 GL.Translate(0.0,0.0,BASE_HEIGHT)
108 rod(quad)
109 GL.PopMatrix()
110 end
111
112 # 羽根(三角錐;三角形4面で構成)
113 def blade
114 GL.Begin(GL::TRIANGLES)
115 #
116 GL.Normal(BLADE_NORMAL0)
117 GL.Vertex(0.0,0.0,0.0) # v0
118 GL.Vertex(BLADE_LENGTH,0.0,0.0) # v1
119 GL.Vertex(BLADE_LENGTH2,-BLADE_WIDTH,0.0) # v2
120 #
121 GL.Normal(BLADE_NORMAL1)
122 GL.Vertex(0.0,0.0,0.0) # v0
123 GL.Vertex(BLADE_LENGTH2,-BLADE_WIDTH,0.0) # v2
124 GL.Vertex(0.0,0.0,BLADE_DEPTH) # v3
125 #
126 GL.Normal(BLADE_NORMAL2)
127 GL.Vertex(0.0,0.0,0.0) # v0
128 GL.Vertex(0.0,0.0,BLADE_DEPTH) # v3
129 GL.Vertex(BLADE_LENGTH,0.0,0.0) # v1
130 #
131 GL.Normal(BLADE_NORMAL3)
132 GL.Vertex(BLADE_LENGTH,0.0,0.0) # v1
133 GL.Vertex(0.0,0.0,BLADE_DEPTH) # v3
134 GL.Vertex(BLADE_LENGTH2,-BLADE_WIDTH,0.0) # v2
135 GL.End()
136 end
137
138 # 回転軸(半球と立方体を組み合わせて前後に引き延ばした図形)
139 def axle(quad)
140 s = AXLE_BOXSIZE/2
141 GL.PushMatrix()
142 # 前後に引き延ばす
143 GL.Scale(1.0,1.0,AXLE_SCALE)
144 # 後部(ボックス)
145 GLUT.SolidCube(AXLE_BOXSIZE)
146 # 前部(楕円球)
147 GL.PushMatrix()
148 GL.Translate(0.0,0.0,s)
149 GLU.Sphere(quad,s,AXLE_SPHERE_SLICES,AXLE_SPHERE_STACKS)
150 GL.PopMatrix()
151 GL.PopMatrix()
152 end
153
154 # 支柱(円柱面)
155 def rod(quad)
156 # 側面(z=0を底面,z軸を中心軸とする円柱面)
157 GLU.Cylinder(quad,
158 ROD_BASE_RADIUS,ROD_TOP_RADIUS,ROD_HEIGHT,
159 ROD_SLICES,ROD_STACKS)
160 end
161
162 # 基台(円錐台)
163 def base(quad)
164 # 側面(z=0を底面,z軸を中心軸とする円錐台の側面)
165 GLU.Cylinder(quad,
166 BASE_BASE_RADIUS,BASE_TOP_RADIUS,BASE_HEIGHT,
167 BASE_SLICES,BASE_STACKS)
168 # 底面(円)
169 GLU.Disk(quad,0,BASE_BASE_RADIUS,BASE_SLICES,BASE_RINGS)
170 end
171
172 #============================================================#
173 #======================== 地面の定義 ========================#
174 #============================================================#
175
176 GROUND_SIZE=BASE_BASE_RADIUS*6.0 # 地面の広さ
177
178 def ground
179 # マテリアル
180 GL.Material(GL::FRONT_AND_BACK,GL::AMBIENT, [0.0,0.0,0.0])
181 GL.Material(GL::FRONT_AND_BACK,GL::DIFFUSE, [0.194,0.049,0.049])
182 GL.Material(GL::FRONT_AND_BACK,GL::SPECULAR, [0.0,0.0,0.0])
183 GL.Material(GL::FRONT_AND_BACK,GL::SHININESS,0.0)
184
185 ## 立方体を平たく変形して「地面」を作る
186 ## y = 0が上面になるように位置調整する.
187 GL.PushMatrix()
188 GL.Translate(0.0,-0.1*GROUND_SIZE,0.0)
189 GL.Scale(1.0,0.1,1.0)
190 GLUT.SolidCube(2.0*GROUND_SIZE)
191 GL.PopMatrix()
192 end
193
194 #============================================================#
195
196 ## 状態変数
197 __theta = 0 # y軸回りの風車の回転角
198 __phi = 0 # x軸回りの風車の回転角
199 __psi = 0 # z軸回りの風車の回転角
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.Rotate(__theta,0.0,1.0,0.0)
218 GL.Rotate(__phi,1.0,0.0,0.0)
219 GL.Rotate(__psi,0.0,0.0,1.0)
220 GL.Translate(0.0,-3.5,0.0)
221 ground()
222 windmill(__quad,__blade_angle,__head_angle)
223 GL.PopMatrix()
224 GLUT.SwapBuffers()
225 }
226
227 #### アイドルコールバック ########
228 idle = Proc.new {
229 __blade_angle = (__blade_angle + DT) % 360 # 羽根の角度の更新
230 GLUT.PostRedisplay()
231 }
232
233 #### キーボード入力コールバック ########
234 keyboard = Proc.new { |key,x,y|
235 case key
236 # [j],[J]: 風車のy軸まわりの回転角の増減
237 when ?j,?J
238 dir = (key == ?j) ? 1 : -1
239 __theta = (__theta + dir*DT) % 360
240 # [k],[K]: 風車のx軸まわりの回転角の増減
241 when ?k,?K
242 dir = (key == ?k) ? 1 : -1
243 __phi = (__phi + dir*DT) % 360
244 # [l],[L]: 風車のz軸まわりの回転角の増減
245 when ?l,?L
246 __psi = (__psi + dir*DT) % 360
247 # [c],[C]: 風車頭部の回転角の増減
248 when ?c,?C
249 dir = (key == ?c) ? 1 : -1
250 __head_angle = (__head_angle + dir*DT) % 360
251 # [r]: 風車の姿勢を初期状態にリセット
252 when ?r
253 __theta = __phi = __psi = 0
254 __blade_angle = 90
255 __head_angle = 0
256 __lightphi = L_INIT_PHI
257 __lightpsi = L_INIT_PSI
258 __camera.reset
259 # [z],[Z]: zoom in/out
260 when ?z,?Z
261 __camera.zoom((key == ?z) ? DD : -DD)
262 # [i],[I]: 光源0の位置を変更する
263 when ?i,?I
264 dp = (key == ?i) ? DT : -DT
265 __lightpsi = (__lightpsi + dp) % 360
266 # [u],[U]: 光源0の位置を変更する
267 when ?u,?U
268 dp = (key == ?u) ? DT : -DT
269 __lightphi = (__lightphi + dp) % 360
270 # [a]: アニメーション開始/停止
271 when ?a
272 if __anim_on
273 __anim_on = false
274 GLUT.IdleFunc(nil)
275 else
276 __anim_on = true
277 GLUT.IdleFunc(idle)
278 end
279 # [q],[ESC]: 終了する
280 when ?q, 0x1b
281 exit 0
282 end
283
284 GLUT.PostRedisplay()
285 }
286
287 #### ウインドウサイズ変更コールバック ########
288 reshape = Proc.new { |w,h|
289 GL.Viewport(0,0,w,h)
290 __camera.projection(w,h)
291 GLUT.PostRedisplay()
292 }
293
294 #### シェーディングの設定 ########
295 def init_shading
296 # 光源の環境光,拡散,鏡面成分と位置の設定
297 GL.Light(GL::LIGHT0,GL::AMBIENT, [0.1,0.1,0.1])
298 GL.Light(GL::LIGHT0,GL::DIFFUSE, [1.0,1.0,1.0])
299 GL.Light(GL::LIGHT0,GL::SPECULAR,[1.0,1.0,1.0])
300 GL.Light(GL::LIGHT0,GL::POSITION,[0.0,1.0,1.0,0.0]) # 無限遠の光源(平行光線)
301
302 # シェーディング処理ON,光源(No.0)の配置
303 GL.Enable(GL::LIGHTING)
304 GL.Enable(GL::LIGHT0)
305 end
306
307 ##### main ##############################################
308 GLUT.Init()
309 GLUT.InitDisplayMode(GLUT::RGB|GLUT::DOUBLE|GLUT::DEPTH)
310 GLUT.InitWindowSize(WSIZE,WSIZE)
311 GLUT.InitWindowPosition(300,200)
312 GLUT.CreateWindow("Windmill(3D)")
313 GLUT.DisplayFunc(display)
314 GLUT.KeyboardFunc(keyboard)
315 GLUT.ReshapeFunc(reshape)
316 GL.Enable(GL::DEPTH_TEST)
317 init_shading() # シェーディングの設定
318 __camera.set # カメラを配置する
319 GL.ClearColor(0.4,0.4,1.0,0.0)
320 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でくくっておいた方がよいでしょう.