[CG実習 >  CG実習 課題(2022) >  円に基づいた曲線・曲面の設計 ]

[課題10]: 円に基づいた曲線・曲面の設計

課題

以下の説明にしたがって,曲線あるいは曲面を構成して描画するプログラムを作成して,提出してください.

3次ベジェ曲線によって,中心角90度の円弧(以下,四分円弧)を精度よく近似できます. 下の図(a)で青く塗ってあるのは(細い)円環です. 円の定義を用いて描いています. 一方,円環の内部のオレンジの線は四分円弧を近似する3次ベジェ曲線を4つ配置した図形です. 円が精度よく近似できていることが見て取れるでしょう. そのような図形を本課題では「擬円」(quasi-circle)と呼ぶことにします.

3次ベジェ曲線による擬円 双3次ベジェ曲面による擬似回転面
(a) 擬円(b) 擬似回転面

この課題では,このような曲線を設計することを,まず最初の目標とします. つづいて,3次ベジェ曲線の制御点列をz=z0,z=z1,z=z2,z=z3の4つの平面上に配置し,計16個の制御点によって,「双3次ベジェ曲面」を設計してみることにします. 平面z=zi(i=0,1,2,3)上の制御点列は四分円弧を描くように配置することにします.ただし各平面で近似する円の半径はそれぞれ独立に設定することにします. そのような曲面に基づいて,上の図(b)のように回転面のように見える曲面を構成できます. そのような曲面を本課題では「擬似回転面」(surface of quasi-revolution)と呼ぶことにします.

目次

  1. 課題の進め方
  2. テンプレート(#1)
  3. ステージ0 --- 四分円弧を近似する3次ベジェ曲線QRの制御点の決定
  4. ステージ1 --- 擬円の描画
  5. テンプレート(#2)
  6. ステージ2 --- 擬似回転面の構成
  7. ステージ3 --- 擬似回転面を変形する機能の実現
  8. 技術要素など

課題の進め方

今回の課題はステージ0,1,2,3で構成しています.

ステージ0,1には必ず取り組んで下さい. ステージ1が完了したら,そこで課題への取り組みを終了して, プログラムを提出して構いません.その場合はテンプレート(#1)から作成したプログラムを提出してください.

可能であれば,さらにステージ2,3に取り組んでみて下さい. ステージ2までで提出しても,ステージ3までで提出しても構いません. ステージ3のプログラムにさらに拡張を加えることも歓迎します. ステージ2以降まで取り組んだ場合はテンプレート(#2)から作成したプログラムを提出してください.

いずれの場合も,提出するプログラムは1つとしてください. ステージ2以降に取り組んだ場合, ステージ1で作成したプログラムを提出する必要はありません.

テンプレート(#1)

「擬円」の設計に利用するテンプレートを示します. このテンプレートは本課題のステージ1で利用します.

テンプレートはそのまま実行可能です. 実行すると,平面z=0上に,原点中心の細い円環および3次ベジェ曲線を描きます. [q],[ESC]を押すとプログラムが終了します.

このテンプレートでは,次のように3次ベジェ曲線の制御点列(__ctrl_points)が定義されています(n次のベジェ曲線の制御点はn+1個).


    ### ベジェ曲線の制御点列: [P0,P1,P2,P3]
    # (z=0の平面上のx≧0,y≧0の領域に配置)
    # [課題] 正しく設定する必要がある
    # (次の設定では四分円弧を近似できていない)
    __ctrl_points = [
      # P0,         P1,         P2,       P3
      [0.0,0.0,0.0],[R,0.0,0.0],[R,R,0.0],[0.0,R,0.0]
    ]

これらの制御点P0,P1,P2,P3を,以下のステージ0で説明するように配置し直すことで, 平面z=0上の半径Rの円のx≧0かつy≧0の領域にある四分円弧を近似した曲線が得られます.その曲線を以下では曲線QRと呼ぶことにします. そのときプログラムを実行すると,最初の図に示したように曲線QRは円環の内部に収まります(擬円のうちx≧0,y≧0の領域に含まれる部分が曲線QRです).

ステージ0 --- 四分円弧を近似する3次ベジェ曲線QRの制御点の決定

ここでは平面z=0上の半径Rの円のx≧0かつy≧0の領域にある四分円弧を近似する3次ベジェ曲線QRについて,その制御点を導出することにします. 具体的には制御点P0,P1,P2,P3を決定します. Pi(i=0,1,2,3)はすべて平面z=0上の点です. つまりPi=(xi,yi,0)と書けます.

この問題については,ネットで検索すれば答えが得られるかと思いますが,せっかくですので,独力で計算してみてください.

まず曲線のパラメタをtとして,区間0≦t≦1において, 制御点P0,P1,P2,P3と, 3次のBernstein多項式B0(t),B1(t),B2(t),B3(t)を使って,曲線QR(t)は次のように定義されます.

  QR(t) = B0(t)・P0 + B1(t)・P1 + B2(t)・P2 + B3(t)・P3  (0 ≦ t ≦ 1)

ここでBernstein多項式B0(t),B1(t),B2(t),B3(t)の定義は次のとおりです.

  B0(t) = (1-t)3
  B1(t) = 3t(1-t)2
  B2(t) = 3t2(1-t)
  B3(t) = t3

このベジェ曲線の定義から次が分かります.

曲線QRの両端点は, x≧0かつy≧0の領域にある四分円弧の両端点と一致させて, さらにこれら2つの曲線について,両端点での接線方向も一致させることにします.

次にx≧0かつy≧0の領域にある四分円弧が直線y=xについて対称であることから, 曲線QR(0≦t≦1)において曲線上の点QR(t)と点QR(1-t)が直線y=xについて対称であるようにすると, 制御点P1とP2は直線y=xについて対称となることが分かります.また制御点P0(=QR(0))とP3(=QR(1))も直線y=xについて対称です.

さらにt=1/2のとき,点QR(1/2)は円弧上にあるとします.

以上を用いると,半径Rを用いて, 制御点P0,P1,P2,P3を決定できます.

ステージ1 --- 擬円の描画

ステージ0で導出した制御点P0,P1,P2,P3によって,テンプレート(#1)の制御点のデータを書き換えれば,曲線QRが描けます. さらに曲線QRと同じ形状の3次ベジェ曲線4本を適切に配置することで,擬円(円を近似する曲線)を描くことができます.


    ### ベジェ曲線の制御点列: [P0,P1,P2,P3]
    # (z=0の平面上のx≧0,y≧0の領域に配置)
    # [課題] 正しく設定する必要がある
    # (次の設定では四分円弧を近似できていない)
    __ctrl_points = [
      # P0,         P1,         P2,       P3
      [0.0,0.0,0.0],[R,0.0,0.0],[R,R,0.0],[0.0,R,0.0]
    ]

このステージでは,擬円を描くプログラムを作成してみて下さい. なおテンプレート(#1)では次の処理で曲線を構成して,描画しています.

  # ベジェ曲線の構成
  configure_curve_evaluator(__ctrl_points) 
  # ベジェ曲線の描画
  GL.Color(CURVE_COLOR)
  GL.EvalMesh1(GL::LINE,0,N_STEPS)

__ctrl_pointsを用いて,メソッドconfigure_curve_evaluatorでベジェ曲線を定義して, 曲線上に予め設置されたパラメタ区間[0,1]上のサンプル点(GL.MapGrid1dで生成)を用いて,GL.EvalMesh1でサンプル点での曲線の点の位置を計算して,曲線を区分的に近似して描画します. ベジェ曲線を構成する処理については,メソッドconfigure_curve_evaluatorの定義を参照してください.

テンプレート(#2)

「擬似回転面」の設計に利用するテンプレートを示します. このテンプレートは本課題のステージ2以降で利用します.

テンプレートはそのまま実行可能で,次のように操作します.

このテンプレートでは,次のようにベジェ曲面の制御点データ(__surf_points)が定義されています.

    ### 双3次ベジェ曲面の制御点データの構成
    # [[P00,P01,P02,P03],
    #  [P10,P11,P12,P13],
    #  [P20,P21,P22,P23],
    #  [P30,P31,P32,P33]]
    # Pij = [x_{ij},y_{ij},z_{ij}] (i,j)の位置の制御点の座標(i,j∈{0,1,2,3})
    # [課題] 設定は適宜変更することを想定している
    __surf_points=[
      [[0.0,0.0,0.0],[R,0.0,0.0],[R,R,0.0],[0.0,R,0.0]], # z==0.0
      [[0.0,0.0,1.0],[R,0.0,1.0],[R,R,1.0],[0.0,R,1.0]], # z==1.0
      [[0.0,0.0,2.0],[R,0.0,2.0],[R,R,2.0],[0.0,R,2.0]], # z==2.0
      [[0.0,0.0,3.0],[R,0.0,3.0],[R,R,3.0],[0.0,R,3.0]]  # z==3.0
    ]

テクスチャマッピングの併用

このテンプレートでは,画像を曲面に貼り付けられるようになっています. 画像はコマンドラインで指定します.

$ ruby soqr_template.rb  画像ファイル名

具体的にはたとえば「4arrows.png」を画像として使うとして,次のように指定します.

$ ruby soqr_template.rb  4arrows.png

なお画像を指定しないで実行することもできます.

$ ruby soqr_template.rb

ステージ2 --- 擬似回転面の構成

次に3次ベジェ曲線に基づいて16個の制御点によって「双3次ベジェ曲面」を設計してみることにします.

双3次ベジェ曲面とは, 次のように構成されます. まず制御点列Ci=(Pi0,Pi1,Pi2,Pi3)を4組用意します(i=0,1,2,3). 制御点列Ci(i=0,1,2,3)で構成される3次ベジェ曲線をQi(u)とします(0≦u≦1).ここでそれらの3次ベジェ曲線において,uを固定したときに得られる4つの点Q0(u),Q1(u),Q2(u),Q3(u) を制御点とすると,また3次ベジェ曲線が構成できます. その曲線をQu(v)とします(0≦v≦1). uを0から1まで動かしたときの曲線Qu(v)の軌跡で構成されるのが双3次ベジェ曲面です.

双3次ベジェ曲面の例として, テンプレート(#2)では,次のように制御点を配置しています.

    ### 双3次ベジェ曲面の制御点データの構成
    # [[P00,P01,P02,P03],
    #  [P10,P11,P12,P13],
    #  [P20,P21,P22,P23],
    #  [P30,P31,P32,P33]]
    # Pij = [x_{ij},y_{ij},z_{ij}] (i,j)の位置の制御点の座標(i,j∈{0,1,2,3})
    # [課題] 設定は適宜変更することを想定している
    __surf_points=[
      [[0.0,0.0,0.0],[R,0.0,0.0],[R,R,0.0],[0.0,R,0.0]], # z==0.0
      [[0.0,0.0,1.0],[R,0.0,1.0],[R,R,1.0],[0.0,R,1.0]], # z==1.0
      [[0.0,0.0,2.0],[R,0.0,2.0],[R,R,2.0],[0.0,R,2.0]], # z==2.0
      [[0.0,0.0,3.0],[R,0.0,3.0],[R,R,3.0],[0.0,R,3.0]]  # z==3.0
    ]

本課題では平面z=zi(i=0,1,2,3)上に制御点列Ci=(Pi0,Pi1,Pi2,Pi3)を配置することにします(上の例では,zi=iとしていますが,必ずしもこのように配置しなくても構いません). 各平面上では,ステージ0で定めたように,四分円弧を近似するように4つの制御点Pi0,Pi1,Pi2,Pi3を配置することにします. ただし各平面で近似する円の半径Riはそれぞれ独立に設定することにします.

そのような16個の制御点で構成される双3次ベジェ曲面4枚を適切に配置することで, 回転面のように見える曲面が得られます. すでに述べたように,そのような曲面を本課題では「擬似回転面」と呼びます.

このステージでは,擬似回転面を描くプログラムを作成してみて下さい. 曲面の形状は自由に決めてください.

なおテンプレート(#2)では次の処理で曲面を構成して,描画しています.

  # ベジェ曲面の構成
  configure_surface_evaluator(GL::MAP2_VERTEX_3,__surf_points) 
  # ベジェ曲面の描画
  GL::EvalMesh2(GL::FILL,0,N_STEPS,0,N_STEPS)

__surf_pointsを用いて,メソッドconfigure_surface_evaluatorでベジェ曲面を定義して, 曲面上に予め設置されたパラメタ空間[0,1]2内のサンプル点(GL.MapGrid2dで生成)を用いて,GL.EvalMesh2でサンプル点での曲面の点の位置を計算して,曲面を近似して描画します. ベジェ曲面を構成する処理については,メソッドconfigure_surface_evaluatorの定義を参照してください.

ステージ3 --- 擬似回転面を変形する機能の実現

ステージ2で用意した制御点の配置を変えることで,さまざまな擬似回転面を生成できます. ステージ3では,制御点列Ci(i=0,1,2,3)を配置する平面のz方向の位置(z=zi), および平面z=zi上で制御点を配置する基準とする円の半径Riを変更する仕組みを導入して, インタラクティブに擬似回転面を変形する機能を実現してください. 制御点を決めるパラメタ(zi,Ri)は画面に表示しておくとよいでしょう.

技術要素など

プログラム作成に利用する可能性のある技術要素やデータを示します.

修飾キー(Shift,Ctrl,Alt)の活用

キーボード入力コールバックにおいて,GLUT.GetModifiersというメソッドで, [Shift],[Ctrl],[Alt]キーが押されているかどうかをチェックできます.


  mod = GLUT.GetModifiers()
  if (mod & GLUT::ACTIVE_ALT)!=0
    #
    # [Alt]が押されている場合の処理
    #
  end

  if (mod & GLUT::ACTIVE_CTRL)!=0
    #
    # [Ctrl]が押されている場合の処理
    #
  end

  if (mod & GLUT::ACTIVE_SHIFT)!=0
    #
    # [Shift]が押されている場合の処理
    #
  end

なお[Shift],[Ctrl]を押しながらキーを押すとイベントとして発生するkeyが変化する場合がありますので注意してください. たとえば[Shift]+[a]の場合,keyは"A"に変わります(小文字→大文字). また[Ctrl]+[x]の場合,keyは「24番の文字」になります. これは「key.ord == 24」で判定できます(他の判定方法もあります).


  when key 
  case 'a'

    # [a]の場合

  case 'A'

    # [A](=[Shift]+[a])の場合

  case 'x'  

    # [x]の場合

  end

  when key.ord
  case 0x1b
    # [ESC]の場合
  case 0x18 # 0x18 == 24
    # [Ctrl]+[x]の場合
  end
  
  

(一時的に)キーのデータを端末に表示させてみれば,どういうイベントが発生しているのかを知ることができます.


keyboard = Proc.new { |key,x,y| 

  # keyとその番号(key.ord)を端末に表示する
  p [:key,key,:ord,key.ord]

}

文字の表示

画面に文字(英数字,記号)を表示するには,次のようにdrawStringメソッドを利用します. 残念ながら日本語を表示することはできません.


次のライブラリを利用する
require "cg/bitmapfont"

 :

display = Proc.new {

   # x,y:  表示位置(世界座標)
   #       (※ ここで指定するy座標は文字の上端ではないので注意)
   # str:  表示する文字列
   # font: フォント(以下のいずれかを指定する)
   #         GLUT::BITMAP_9_BY_15
   #         GLUT::BITMAP_8_BY_13
   #         GLUT::BITMAP_TIMES_ROMAN_10
   #         GLUT::BITMAP_TIMES_ROMAN_24
   #         GLUT::BITMAP_HELVETICA_10
   #         GLUT::BITMAP_HELVETICA_12
   #         GLUT::BITMAP_HELVETICA_18
   drawString(x,y,str,font)

   # 直前に書いた文字列の続きに別の文字列を表示する場合は次のメソッドを利用する
   drawStringCont(str2,font)

   # 例
   drawString(0.0,0.0,"message",GLUT::BITMAP_9_BY_15)
   drawStringCont(" another message",GLUT::BITMAP_9_BY_15)

   # __theta,__phiという(実数値)変数が定義されているとする.
   # それぞれを全体6桁で小数点以下2桁で取り込んだ文字列を作る.
   str = "(theta,phi)=(%6.2f %6.2f)" % [__theta,__phi]

   # 生成された文字列strを表示する
   drawString(-0.9,0.9,str,GLUT::BITMAP_TIMES_ROMAN_24)

}

数値を取り込んだ文字列を作る方法

上の例でも示している通り,(表示桁数を指定して)数値を取り込んだ文字列を作ることができます.



 # サンプルデータ
 x=1.234
 y=0.456
 z=-2

 # xの小数点以下を2桁までを文字列strを生成する
 # str ==> "1.23"
 str = "%.2f" % x 
 
 # xを(小数点をふくめて)全体で7桁,小数点以下3桁までで文字列str2を生成する
 # str2 ==> "  1.234"
 str2 = "%7.3f" % x 

 # xの小数点以下を2桁,yの小数点以下を3桁までを取り込んで文字列str3を作る
 # str3 ==> "Data: [X=1.23] [Y=0.456]"
 str3 = "Data: [X=%.2f] [Y=%.3f]" % [x,y]

 # x,y,zのそれぞれを小数点以下2桁まで取り込んで文字列str4を作る
 # str4 ==> "v=(1.23,0.46,-2.00)"
 str4 = "v=(%.2f,%.2f,%.2f)" % [x,y,z]

 # x,y,zのそれぞれを正負の符号付きで小数点以下2桁まで取り込んで文字列str5を作る
 # str5 ==> "v=(+1.23,+0.46,-2.00)"
 str5 = "v=(%+.2f,%+.2f,%+.2f)" % [x,y,z]


いずれの場合も文字列"..."の中の「%」で始まる部分で指定された形式で,数値が当てはめられます.このとき小数点以下は(必要に応じて)四捨五入されます. 「%」で始まる部分以外は文字がそのまま入ります.

上の例に示している通り,当てはめる値が一つの場合は,(文字列につづく「%」の後に)そのままデータを指定します. 2つ以上の値を取り込む場合,データは配列で指定します. このとき配列の要素数とあてはめる値の個数が一致している必要があります. 次の例も参考にしてください.

  
  ary = [1.2, 3, -4]
  # str6 ==> "(+1.200, +3.000, -4.000)"
  str6 = "(%+.3f, %+.3f, %+.3f)" % ary # ary自体が配列であることに注意

[注意] 文字表示のための追加設定

表示する文字にもモデル・ビュー変換と投影変換が適用されます. Zバッファによる処理も行われます. シェーディング処理を行っている場合には,その影響も受けます. またテクスチャマッピングも影響を及ぼします. これらの効果を考慮しないと思わぬ結果になりえます.

そこで文字の表示処理の前後で次のような処理を行って下さい. こうすることで文字は2次元平面上に配置するものと考えることができます. また文字色はGL.Colorで指定できます.



    GL.Disable(GL::DEPTH_TEST)    # ZバッファをOFFにする
    GL.Disable(GL::LIGHTING)      # シェーディングをOFFにする(GL.Colorで文字の色を決めるようにする)
    GL.Disable(GL::TEXTURE_2D)    # テクスチャマッピングをOFFにする(ONである場合のみ)
    GL.PushMatrix()               # 現在のモデル・ビュー変換行列の退避
    GL.LoadIdentity()             # 変換行列の初期化
    GL.MatrixMode(GL::PROJECTION) # 投影変換行列モードに設定
    GL.PushMatrix()               # 現在の投影変換行列の退避
    GL.LoadIdentity()             # 投影変換行列の初期化
    
     
  # ここで文字の表示を行う
  # -1 <= x <= 1,-1 <= y <= 1の範囲で二次元平面上に描くと考えればよい
    # 色はGL.Colorで指定する
    

    GL.PopMatrix()                # 退避しておいた投影変換行列に戻す
    GL.MatrixMode(GL::MODELVIEW)  # モデル・ビュー行列モードに戻す
    GL.PopMatrix()                # 退避しておいたモデル・ビュー変換行列に戻す
    GL.Enable(GL::TEXTURE_2D)     # テクスチャマッピングをONにする(上でOFFにした場合のみ)
    GL.Enable(GL::LIGHTING)       # シェーディングをONにする
    GL.Enable(GL::DEPTH_TEST)     # ZバッファをONにする


ここで使われているGL.PushMatrix,GL.PopMatrixについては「形状モデリング」で詳しく取り上げます.

[参考] 文字の表示位置の制御

drawStringdrawStringContメソッドは,この実習用に用意したもので,OpenGLで標準に使えるものではありません(Ruby/OpenGLにも含まれていません).

なおdrawStringにつづいてdrawStringContを使うときに, drawStringContの直前でGL.Colorで色を変えても反映されないという不具合が分かっています. そこで文字列の途中で色を変える場合にはdrawStringContを使う代わりに(スマートではありませんが)drawStringで表示する位置を調整して下さい.

上のように二次元平面上に文字を描くように設定をしたとき, 描画される文字列の世界座標系(2次元座標系)でのサイズは,次のrasterSizeメソッドで知ることができるようになっています.


  # rasterSize(obj,font,range,wsize,dir)
  #   obj:   描画する文字列あるいは文字数
  #   font:  描画に用いるフォント
  #   range: ウインドウの端から端までの世界座標系での長さ
  #   wsize: ウインドウの大きさ
  #   dir: BITMAP_DIM_WIDTH または BITMAP_DIM_HEIGHT

  WSIZE=800 # 以下の例で使っているウインドウサイズ 

  # 描画する文字列の世界座標系での横幅を求める(BITMAP_DIM_WIDTH)
  # "hello!"という文字列を「GLUT::BITMAP_9_BY_15」で描画する
  # ウインドウの左端から右端までの世界座標系での長さは2.0だとする(-1≦x≦1)
  # ウインドウの大きさ(横幅)はWSIZE(=800)とする
  # 得られた値をswに代入する
  sw = rasterSize("hello!",GLUT::BITMAP_9_BY_15,2.0,WSIZE,BITMAP_DIM_WIDTH)

  # 描画する文字列の世界座標系での横幅を求める(BITMAP_DIM_WIDTH)
  # 「GLUT::BITMAP_HELVETICA_18」で12文字分描画する場合を考える
  # ウインドウの左端から右端までの世界座標系での長さは2.0だとする(-1≦x≦1)
  # ウインドウの大きさ(横幅)はWSIZE(=800)とする
  # 得られた値をswに代入する
  sw = rasterSize(12,GLUT::BITMAP_HELVETICA_18,2.0,WSIZE,BITMAP_DIM_WIDTH)  

  # 描画する文字列の世界座標系での高さを求める(BITMAP_DIM_HEIGHT)
  # 「GLUT::BITMAP_HELVETICA_18」での縦方向で3文字分の高さを求める
  # ウインドウの上端から下端までの世界座標系での長さは2.0だとする(-1≦y≦1)
  # ウインドウの大きさ(高さ)はWSIZE(=800)とする
  # 得られた値をshに代入する
  sh = rasterSize(3,GLUT::BITMAP_HELVETICA_18,2.0,WSIZE,BITMAP_DIM_HEIGHT)  

数学の関数など

関数Rubyでの式備考
xyx**y
√xMath.sqrt(x)
sin xMath.sin(x)xはラジアン
cos xMath.cos(x)xはラジアン
tan xMath.tan(x)xはラジアン
log xMath.log(x)自然対数
log10xMath.log10(x)常用対数
exMath.exp(x)
πMath::PI「PI」は大文字のpi
xの絶対値x.abs

配列のコピー

配列のコピーは次のようにして作ることができます.


  # 数値のみのフラットな配列の場合
  src0 = [1,2,3,4]
  dst0 = src0.dup  # src0のコピー

  # 要素に配列を持つ配列の場合
  src1 = [[1,2],3,[4,5,[6,7]]]
  dst1 = Marshal.load(Marshal.dump(src)) # src1のコピー
  

代入を行っただけの場合には,コピーは作られません. 代入とは右辺の式の値に名前をつける処理です. 右辺が変数の場合は,その変数の値が右辺の式の値です. その値とは変数が指しているデータそのものです. そこで,すでに存在する配列に対して代入によって名前をつけると, 単純に別名がつけられるだけ(同一の配列を参照する名前が増えるだけ)の結果になります.


  a0 = [0,1,2,3]
  a1 = a0  # a1は「a0が指す配列」を指す→a0,a1は同一の配列を指すことになる(配列は1つしかない)

  a0[0] = 4  # a0が指す配列の最初の要素への代入
  a1[0] == 0 # ==> false
  a1[0] == 4 # ==> true

利用可能なテクスチャ画像

テクスチャ画像のサンプルをPandAの「リソース」から取得できるようにしています.

[CG実習 >  CG実習 課題(2022) >  円に基づいた曲線・曲面の設計 ]