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

形状モデリング

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

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

サンプルプログラム

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

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

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

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

階層的形状モデリング

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

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

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

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実習 > 形状モデリング]