[CG実習 > 形状モデリング]

形状モデリング

ここでは,基本的な図形を部品として組み合わせて,より複雑な形状の物体を設計する形状モデリングの方法について説明します. 部品を組み合わせることで,一定の形状をもつ物体だけでなく,変形する物体,たとえば関節で動くような物体も設計できます. また,複数の物体を一つの系としてまとめて扱うこともできます.

さて,部品を組み合わせるといっても,実際にそれらを貼り合わせたり,ネジどめしたりするわけではありません. 基本となる部品を移動させたり,回転させたり,あるいはスケーリング(拡大縮小)したりすることで,部品を適宜配置して全体の形状を設計していくことになります. 移動,回転,スケーリングなどの操作は行列による変換で表現できます. そこで,まず最初にそれらの変換について説明します. その後,それらの変換を利用して,形状モデリングを行う方法について説明します.

サンプルプログラム

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

このプログラムは,風力発電の風車を模したものです. ただし,それらしく見えるように形状を作ってみただけで,工学的な設計はまったく行っていません. 操作方法は次の通りです.

風車を回転させる際に,羽根は回転させてますが,じつは回転軸は回転させていません. 回転軸の形状は回転対称ですので,実際には回転させなくても見た目には問題になりません.

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())に近い方から,順に適用されると考えるとよいかもしれません.

階層的形状モデリング

サンプルプログラムの風車は,次のような部品から構成されています. これらの部品をお互いに関連付けることで物体全体の形状が作り上げられています.

このように複数の部品を組み合わせた物体は,あるベースとなる部品に対して, 別の部品を接続して,それにさらにまた別の部品を接続してというようにして構成され,階層的な構造をもつことになります.

サンプルプログラムでは,風車の全体を回転させることができます. また,それとは別に風車の頭部の向きを変えることができます. 風車の羽根は回転軸に対して固定されていることから,回転軸部分と同時に羽根も同じように向きを変えます. つまり,羽根の姿勢は,回転軸部分の姿勢に影響を受けるわけです. さらに回転軸部分は支柱に接続されていて,支柱の姿勢が変われば,回転軸部分の姿勢が変わり,羽根の姿勢も変わります. これは,羽根の姿勢が間接的に支柱の姿勢に影響を受けることを意味しています. このように,階層的な構造をもつ物体では,よりベースに近い部品の挙動が,より末端に位置するの部品の挙動に影響を与えます.

OpenGLで,このような階層的な構造をもつ物体をモデリングするには,まず基本となる各部品を設計します. このとき部品自体を基準とした座標系で,形状のみを設計します. 部品を基準とする座標系で形状を記述することで設計が容易になります. あとは,設計した部品を移動,回転,あるいはスケーリングすることで,部品を適宜配置あるいは変形して全体の形状を設計していくことになります. すでに見た通り,これらの処理は,GL.Translate(),GL.Rotate,GL.Scale(),あるいはこれらの合成変換で実現できます.

変換によって部品を組み合わせて形状を設計する際,ベースとなる部品に対する変換に,さらにもう一段階変換を追加してかけることで部品を組み合わせるような場合と,他の部品とは独立して,ある一つの部品にだけ変換をかけたいような場合があります. たとえば風車の例では,風車の頭部は向きを変えることができますが,その姿勢は,支柱の姿勢を決める全体的な変換に,さらに支柱に対する頭部の向きを決める局所的な変換をかけることで決められます. また,回転軸部分は,半球と立方体を組み合わせた物体を前後に引き延ばすことで作られています. このスケーリング処理は,他の部品には適用しません.

このように変換を適宜合成,あるいは別個に適用するような仕組みは,変換行列をスタックで扱うことで実現することができます.

行列スタック

スタック(stack)とは,一般には,データを管理するための基本的な仕組み(データ構造)の一つです. スタックに新しいデータを入れていくと,データはスタックの中で順に縦に積まれていきます. 先に入れておいたデータは,スタックの奥に押しこまれます スタックから取り出せるのは,常に一番上のデータだけです. データを一つ取り出すと,もともと上から2番目にあったデータが一番上になりますので,こんどはそのデータを取り出すことが可能になります. データをスタックの一番上(top)に新たに積む操作をpush,スタックの一番上のデータ(top)を取り出す操作をpopといいます. 後に入れたデータが先に出てくるという性質から,スタックを後入れ先出し型(LIFO;Last In First Out)のデータ構造といいます.

3次元CGの基礎」の資料の中で既に示した通り, OpenGLでは,描画する3次元シーンの設計とシーンの撮影方法の設定を,モデル・ビュー変換を表す行列と投影変換を表す行列の二つによって記述します. これらの二つの行列は,それぞれスタックで管理されます. スタックの一番上の行列が実際に変換に利用されることになります. スタックを操作する基本的なメソッドには次のようなものがあります.

階層的な構造をもつ物体を設計する際に,ある部品(の集合)に独自に変換をかけたい場合は,GL.PushMatrixGL.PopMatrixとをペアにして,その間で必要な変換を施し,部品(の集合)を描画することになります. GL.PushMatrixにより,その時点での変換行列を保持したまま新しい変換行列を作ることが可能となり,GL.PopMatrixにより元の変換行列を復元できますので,部品に施した変換が他の部品には影響を与えることはありません.

サンプルプログラムでも,風車の設計に随所で行列のPushとPopを利用しています. なお一度設計した物体を別途再利用する可能性を考えれば,物体にかかる変換の全体をGL.PushMatrixとGL.PopMatrixでくくっておいた方がよいでしょう.

[CG実習 > 形状モデリング]