[CG実習 > インタラクション]

インタラクション

CGでは,静止画を作るだけではありません. 一連の動画を表示するアニメーションを行うこともあります. また,キーを押したり,マウスを動かすのに反応して,表示を変えるようなインタラクションの仕組みもCGの重要な要素です. ここでは,まずOpenGLでインタラクションの機能をもったプログラムを作成する方法について説明します.

サンプルプログラム

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

これは,信号機を表示するプログラムです. [n]か[SPACE]か[TAB]のいずれかのキーを押すと,信号が順に変わります. マウスの左ボタンを押した場合も,信号が順に変わります. [q]あるいは[ESC]を押すとプログラムが終了します.


001  # coding: utf-8
002  require "opengl"
003  require "glu"
004  require "glut"
005  require "cg/mglutils"
006  
007  ##
008  ## データ(定数)
009  ##
010  WSIZE = 600  # ウインドウサイズ
011  
012  R = 0.2      # ランプ(円板)の半径
013  M = 0.1      # ランプとランプの間隔,周囲のマージン
014  W = 6*R+4*M  # フレームの横幅
015  H = 2*R+2*M  # フレームの高さ
016  D = 2*R+M    # ランプの中心間の距離
017  
018  YC = 0.5     # ランプの位置(高さ)
019  YT = YC+R+M  # フレームの上端
020  YB = YT-H    # フレームの下端
021  XL = -W/2    # フレームの左端
022  XR = XL+W    # フレームの右端
023  XRP = XL+M   # ポールの右端
024  YBP = -1.0   # ポールの下端(画面の下端)
025  
026  L_ON  = 1.00 # ランプ点灯状態の明るさ
027  L_OFF = 0.5  # ランプ消灯状態の明るさ
028  
029  ##
030  ## 信号状態変数
031  ## 0=GREEN,1=YELLOW,2=RED
032  ##
033  __lamp  = 0  
034  
035  ### 信号機の描画(描画コールバック) ########
036  display = Proc.new {
037  
038    GL.Clear(GL::COLOR_BUFFER_BIT) # 画面のクリア
039  
040    ## フレームとポールを描く
041    GL.Color(0.7,0.7,0.7)  # 色=グレイ
042    GL.Rect(XL,YT,XR,YB)   # フレーム(四角)を描く
043    GL.Rect(XL,YB,XRP,YBP) # ポール(四角)を描く
044  
045    ## ランプ(円板)を描く(それぞれ中心,半径を指定)
046    if __lamp == 0 
047      ### 緑点灯
048      GL.Color(0,L_ON,0);      MGLUtils.disc([-D,YC],R) # ON
049      GL.Color(L_OFF,L_OFF,0); MGLUtils.disc([ 0,YC],R) # OFF
050      GL.Color(L_OFF,0,0);     MGLUtils.disc([ D,YC],R) # OFF
051    elsif __lamp == 1
052      ### 黄点灯
053      GL.Color(0,L_OFF,0);     MGLUtils.disc([-D,YC],R) # OFF
054      GL.Color(L_ON,L_ON,0);   MGLUtils.disc([ 0,YC],R) # ON
055      GL.Color(L_OFF,0,0);     MGLUtils.disc([ D,YC],R) # OFF
056    else 
057      ### 赤点灯
058      GL.Color(0,L_OFF,0);     MGLUtils.disc([-D,YC],R) # OFF
059      GL.Color(L_OFF,L_OFF,0); MGLUtils.disc([ 0,YC],R) # OFF
060      GL.Color(L_ON,0,0);      MGLUtils.disc([ D,YC],R) # ON
061    end
062  
063    GL.Flush() # 描画実行
064  }
065  
066  ### キーボード入力コールバック ########
067  keyboard = Proc.new { |key,x,y| 
068    # <文字>.ord == その<文字>に対応する番号
069    # 印字できない文字はこのようにして扱う
070    # 印字できる文字でもordで番号と比較できる
071    # 文字の番号はirbを使って「ord」で調べられる
072    # $ irb
073    # irb(main):001:0> 'n'.ord [Enter]
074    # => 110
075    # irb(main):002:0> exit [Enter]
076    
077    # [n]か[SPACE]か[TAB]: 信号を順に切替える
078    if key == 'n' or key == ' ' or key.ord == 0x09
079      __lamp = (__lamp + 1) % 3 # 0 -> 1, 1 -> 2, 2 -> 0
080      GLUT.PostRedisplay()      # displayコールバックを(後で適宜)呼び出す.
081    # [q]か[ESC]: 終了する
082    elsif key == 'q' or key.ord == 0x1b
083      exit 0
084    end
085  }
086  
087  #### マウス入力コールバック ########
088  mouse = Proc.new { |button,state,x,y|
089    # 左ボタンが押されたら,信号を順に切替える
090    if button == GLUT::LEFT_BUTTON and state == GLUT::DOWN
091      __lamp = (__lamp + 1) % 3 # 0 -> 1, 1 -> 2, 2 -> 0
092      GLUT.PostRedisplay()      # displayコールバックを(後で適宜)呼び出す.
093    end
094  }
095  
096  ##############################################
097  # main
098  ##############################################
099  GLUT.Init()                         # OpenGLの初期化
100  GLUT.InitWindowSize(WSIZE,WSIZE)    # ウインドウサイズの指定
101  GLUT.CreateWindow("Traffic Signal") # ウインドウ作成
102  GLUT.DisplayFunc(display)           # 描画コールバックの登録
103  GLUT.KeyboardFunc(keyboard)         # キーボード入力コールバックの登録
104  GLUT.MouseFunc(mouse)               # マウス入力コールバックの登録
105  GL.ClearColor(0.4,0.4,1.0,1.0)      # 背景色の設定
106  GLUT.MainLoop()                     # イベントループ開始

Ruby/OpenGLでのコールバックの基本

OpenGLの基礎」でもみた通り,OpenGLのプログラムは,通常,さまざまなイベントに対してコールバックで処理を行うイベント駆動型になっています. イベントが発生すると,そのイベントに対応するコールバックが呼び出されて,コールバックの処理が実行されます. このときコールバックには,イベントに関連したデータが引数として渡されます. コールバックではこのデータを使って処理を行うことになります. どんなデータが渡されるかはコールバックの種類によって決まっています. たとえば,キーボード入力に対応するコールバックに対しては, どのキーが押されたのか,どこで押されたのかというデータが渡されます. データが何も渡されない場合もあります.

Ruby/OpenGLでは,コールバックは,Procオブジェクトとして与えられます. Procオブジェクトは,データとして扱うことのできるメソッドのようなものです. コールバックは,一般的に次のような形式で記述します.


callback_obj = Proc.new { |callback_data_list|

  コールバックでの処理内容

}

Proc.newにブロック({ ... })を与えて,このブロックにコールバックとしての処理を記述しておきます. Proc.newを評価した結果として,ブロックの処理内容をデータとするProcオブジェクトが生成されます. このオブジェクトをコールバックとして登録することになります.

コールバックに渡されるデータは,コールバックのブロックパラメタを介して受け取ることになります(この例では,「|callback_data_list|」として表現しています. なお,ブロックとブロックパラメタの扱いについては,Rubyの資料の「配列とイテレータ」を参照して下さい.

コールバックの登録

コールバックは記述しただけでは機能しません. イベントが発生したときにコールバックが呼び出されるようにするには, コールバックとして生成したProcオブジェクトを適切に登録する必要があります. どのイベントに対するコールバックとして登録するかによって,利用するメソッドは異なります. 登録メソッドは一般的に次のような形で用います. 引数としてコールバックのProcオブジェクトを渡します. 「****」の部分には,イベントを表すような名前が入ります (用意されているメソッドは決まっています.勝手に名前をつけられるわけではありません).


  GLUT.****Func(callback_obj)


102  GLUT.DisplayFunc(display)           # 描画コールバックの登録
103  GLUT.KeyboardFunc(keyboard)         # キーボード入力コールバックの登録
104  GLUT.MouseFunc(mouse)               # マウス入力コールバックの登録
 :  
106  GLUT.MainLoop()                     # イベントループ開始

なおコールバックを登録する順序には制限はありません.

入力を処理するコールバック

OpenGLの基礎」でみた通り, OpenGLのプログラムはイベント駆動型です. 各イベントに対するコールバックを登録しておいて,あとは,イベントを監視して,イベントが発生したら,そのイベントに対応するコールバックを実行するというイベントループを繰り返し実行するようになっています.

OpenGLには,プログラムにインタラクティブ性を持たせるため,ユーザからの入力をイベントとして扱い,それに対応するコールバックを登録できるような仕掛けが用意されています. キーボード入力を扱うコールバックはGLUT.KeyboardFunc, マウス入力を扱うコールバックはGLUT.MouseFuncで登録します. マウスに関しては,その動きを処理するためのコールバックとして, GLUT.MotionFuncおよび GLUT.PassiveMotionFuncを用いることができます. また,カーソルがウインドウに入った,あるいはウインドウから出たときの処理をGLUT.EntryFuncとして登録することもできます.

キーボード入力を処理するコールバック

まずキーボード入力を処理するコールバックについて詳しく見てみます. このコールバックでは,押されたキーによって,異なる処理を行います. 具体的には,[n]か[SPACE]か[TAB]のいずれかのキーが押されたときに,信号を切替えます. 切替えは別途定義しているsignal_nextというメソッドを使って実行します. またこのとき,GLUT.PostRedisplayというメソッドを呼び出しています. これは,信号が切り替わった状態を適切に再描画するために必要になります. 一般に,描画コールバック以外で画像を変更した場合には,このメソッドを呼び出します. [q]か[ESC]が押されたときには,プログラムを終了(exit)します.


066  ### キーボード入力コールバック ########
067  keyboard = Proc.new { |key,x,y| 
068    # <文字>.ord == その<文字>に対応する番号
069    # 印字できない文字はこのようにして扱う
070    # 印字できる文字でもordで番号と比較できる
071    # 文字の番号はirbを使って「ord」で調べられる
072    # $ irb
073    # irb(main):001:0> 'n'.ord [Enter]
074    # => 110
075    # irb(main):002:0> exit [Enter]
076    
077    # [n]か[SPACE]か[TAB]: 信号を順に切替える
078    if key == 'n' or key == ' ' or key.ord == 0x09
079      __lamp = (__lamp + 1) % 3 # 0 -> 1, 1 -> 2, 2 -> 0
080      GLUT.PostRedisplay()      # displayコールバックを(後で適宜)呼び出す.
081    # [q]か[ESC]: 終了する
082    elsif key == 'q' or key.ord == 0x1b
083      exit 0
084    end
085  }
 :
103  GLUT.KeyboardFunc(keyboard)      # キーボード入力コールバックの登録

キーボード入力に対応するコールバックに対しては,押されたキーとキーが押されたときのマウスの位置がデータとして渡されます. コールバックでは,それらのデータを利用した処理を行います.

キーボード入力のコールバックは,GLUT.KeyboardFuncを使って登録します. このメソッドに,コールバックとして生成されたProcオブジェクトを引数として渡します.

なお GLUT.GetModifiersというメソッドで, [Shift],[Ctrl],[Alt]キーが押されているかどうかをチェックできます. [Ctrl]+[x]などの二つのキーの組み合わせが入力されたかどうかをチェックする際に,このメソッドを用います.


  modifiers = GLUT.GetModifiers()
  if (modifiers & GLUT::ACTIVE_CTRL) != 0
    puts "Ctrl"
  end
  if (modifiers & GLUT::ACTIVE_ALT) != 0
    puts "Alt"
  end
  if (modifiers & GLUT::ACTIVE_SHIFT) != 0
    puts "Shift"
  end

特殊なキーの扱い

カーソルキーやファンクションキーなど特殊なキーについては, キーボードコールバック(KeyboardFunc)では扱えません. 次のような特殊キー用のコールバックを別途作成して,登録します.

# 特殊なキー入力をあつかうコールバック
special = Proc.new { |key,x,y|
  if key == GLUT::KEY_LEFT     # [←]
    # [←]が押されたときの処理
  elsif key == GLUT::KEY_RIGHT # [→]
    # [→]が押されたときの処理
  elsif key == GLUT::KEY_UP    # [↑]
    # [↑]が押されたときの処理
  elsif key == GLUT::KEY_DOWN  # [↓]
    # [↓]が押されたときの処理
  elsif key == GLUT::KEY_F1    # [F1]
    # [F1]が押されたときの処理    
  elsif key == GLUT::KEY_F2    # [F2]
    # [F2]が押されたときの処理    
  end
}

  :
  :


GLUT.SpecialFunc(special)         # 特殊キー用の入力コールバック登録

  :

GLUT.MainLoop()

特殊キーのコールバック(SpecialFunc)は通常のキー入力のコールバック(KeyboardFunc)と併用できます.

マウスのボタン入力を処理するコールバック

次にマウスのボタン入力を処理するコールバックを見てみます. このコールバックでは,左ボタンが押されたときに,信号を順に切替えます. キーボード入力処理の場合と同様に,signal_nextで切替える処理を行った後に,やはりGLUT.PostRedisplayを呼び出しています. マウスのボタン入力処理のコールバックは,GLUT.MouseFuncを使って登録します.


087  #### マウス入力コールバック ########
088  mouse = Proc.new { |button,state,x,y|
089    # 左ボタンが押されたら,信号を順に切替える
090    if button == GLUT::LEFT_BUTTON and state == GLUT::DOWN
091      __lamp = (__lamp + 1) % 3 # 0 -> 1, 1 -> 2, 2 -> 0
092      GLUT.PostRedisplay()      # displayコールバックを(後で適宜)呼び出す.
093    end
094  }
 :
104  GLUT.MouseFunc(mouse)            # マウス入力コールバックの登録

マウス入力のコールバックには,ボタンの種類,ボタンの状態,カーソルの座標が渡されます. ボタンの状態は,ボタンが押されたのか(GLUT::DOWN),あるいは離されたのか(GLUT::UP)のいずれかです. カーソルの座標とは,ボタンが押されたあるいは離されたときにカーソルが位置していた座標を意味します. この座標を調べることで,特定の場所がクリックされたかどうかをチェックできます. またボタンは,GLUT::LFET_BUTTON,GLUT::MIDDLE_BUTTON,GLUT::RIGHT_BUTTONのいずれかで指定します.

サンプルのコールバックでは, 「state == GLUT::DOWN」を条件にいれておくことで,ボタンが押されたときだけ処理を行うようにしています. こうしておかなければ,ボタンを押して離すたびに信号が二回変わってしまいます.

なお,すでに述べた通り,マウスについては,その動きを処理するためのコールバックとして, GLUT.MotionFuncおよび GLUT.PassiveMotionFuncを利用することができます. また,カーソルがウインドウに入った,あるいはウインドウから出たときの処理をGLUT.EntryFuncとして登録することもできます.

座標系に関する注意

GLとGLUTの座標系

OpenGLでの2次元座標系は,x軸は右向きでy軸は上向きです. ウインドウ内でのOpenGLの座標系はプログラムでの設定に依存して決まります. 一方GLUTライブラリではx軸は右向きでy軸は下向きです. GLUTの座標系の原点はウインドウの左上角です. またウインドウのサイズの単位と座標系の単位が一致しています. イベントはGLUTで処理しますので,コールバックに渡される座標はGLUTでの座標であることに注意して下さい. たとえばマウスボタンをクリックした位置(x,y)は,GLUT(ウインドウ)の座標系で与えられます.

描画コールバック

すでに「OpenGLの基礎」でも見た通り, 基本的なコールバックとして, 必要に応じて,画面を(再)描画する処理を記述したものがあります. たとえば,ウインドウ(の一部)が隠れた状態にあって,それが再度現われたときには,画面を再描画する必要があります. 描画コールバックは,GLUT.DisplayFuncを使って登録します.


035  ### 信号機の描画(描画コールバック) ########
036  display = Proc.new {
037  
038    GL.Clear(GL::COLOR_BUFFER_BIT) # 画面のクリア
039  
040    ## フレームとポールを描く
041    GL.Color(0.7,0.7,0.7)  # 色=グレイ
042    GL.Rect(XL,YT,XR,YB)   # フレーム(四角)を描く
043    GL.Rect(XL,YB,XRP,YBP) # ポール(四角)を描く
044  
045    ## ランプ(円板)を描く(それぞれ中心,半径を指定)
046    if __lamp == 0 
047      ### 緑点灯
048      GL.Color(0,L_ON,0);      MGLUtils.disc([-D,YC],R) # ON
049      GL.Color(L_OFF,L_OFF,0); MGLUtils.disc([ 0,YC],R) # OFF
050      GL.Color(L_OFF,0,0);     MGLUtils.disc([ D,YC],R) # OFF
051    elsif __lamp == 1
052      ### 黄点灯
053      GL.Color(0,L_OFF,0);     MGLUtils.disc([-D,YC],R) # OFF
054      GL.Color(L_ON,L_ON,0);   MGLUtils.disc([ 0,YC],R) # ON
055      GL.Color(L_OFF,0,0);     MGLUtils.disc([ D,YC],R) # OFF
056    else 
057      ### 赤点灯
058      GL.Color(0,L_OFF,0);     MGLUtils.disc([-D,YC],R) # OFF
059      GL.Color(L_OFF,L_OFF,0); MGLUtils.disc([ 0,YC],R) # OFF
060      GL.Color(L_ON,0,0);      MGLUtils.disc([ D,YC],R) # ON
061    end
062  
063    GL.Flush() # 描画実行
064  }
 : 
102  GLUT.DisplayFunc(display)        # 描画コールバックの登録

コールバックと状態変数

さて,しばしば,複数のコールバックの間で共通のデータを参照したり,変更したりすることが必要になる場合があります. 今回のサンプルプログラムでは,特定のキーあるいはマウスのボタンが押されたら,信号を切り替えるようにしています. キーやマウスのコールバックで信号の切り替えが行われたことが検知されたら, 描画コールバックで信号のランプを切り替えるようになっているわけです. これは,「現在の信号の状態」というデータをこれらのコールバックで共有しなければならないことを意味しています. ところがOpenGLでは,コールバックの間でデータを共有するような仕組みは用意されていません.

Ruby/OpenGLでは,コールバック間で状態変数を共有することで,この問題を解決することができます. コールバック(実体はProcオブジェクト)を定義する前に変数を用意しておくと, その変数をコールバックから利用することができます. そこで,必要な変数を予め定義しておくことで,それらをコールバックの間で共有することができます.

サンプルプログラムでは,このような状態変数として,「__lamp」を利用しています.


029  ##
030  ## 信号状態変数
031  ## 0=GREEN,1=YELLOW,2=RED
032  ##
033  __lamp  = 0  
 :
046    if __lamp == 0 
047      ### 緑点灯
 :
051    elsif __lamp == 1
052      ### 黄点灯
 :
056    else 
057      ### 赤点灯
 :
079      __lamp = (__lamp + 1) % 3 # 0 -> 1, 1 -> 2, 2 -> 0
 :
091      __lamp = (__lamp + 1) % 3 # 0 -> 1, 1 -> 2, 2 -> 0

なお,大域変数(global variable)は,(定義された後は)いつでもどこでも使えますが,ここでは利用しないことにします (ここでは,状態変数を,実質上,大域変数のように使っています).

キー操作,マウス操作以外のプログラムへのインタフェイスとして,メニューがよく利用されます. OpenGLにも簡単なメニューを作成する機能が用意されています. メニュー作成の手順は次のようになります.

  1. メニューコールバックを用意する
    メニューに加える各項目が選択されたときの処理を記述します. 各項目には番号をつけておきます. コールバックには,この番号が渡されることになります.
  2. メニューコールバックを登録する
    GLUT.CreateMenuを使います.
  3. メニューの各項目にタイトルをつける
    GLUT.AddMenuEntryを使って, メニューに加える各項目にタイトルをつけます. コールバックで項目につけた番号を利用します.
  4. メニューを呼び出すマウスボタンを設定する. GLUT.AttachMenuで,メニューを呼び出すマウスのボタンを指定します.

サンプルプログラムには,たとえば次のようにメニューを加えることができます(traffic_signal_menu.rb).



#### メニューコールバック ########
menu = Proc.new { |id|
  case id
  when 1
    __lamp = (__lamp + 1) % 3 # 0 -> 1, 1 -> 2, 2 -> 0
    GLUT.PostRedisplay()
  when 2
    exit 0
  end
}

#### メニューの設定 ########
def setup_menu(menu)
  GLUT.CreateMenu(menu)               # メニューコールバックの登録
  GLUT.AddMenuEntry("Next",1)         # 項目1のタイトル設定
  GLUT.AddMenuEntry("Exit",2)         # 項目2のタイトル設定
  GLUT.AttachMenu(GLUT::RIGHT_BUTTON) # メニュー呼び出しの設定
end


GLUT.Init()
GLUT.InitWindowSize(WSIZE,WSIZE) 
GLUT.CreateWindow("Traffic Signal") # ウインドウ作成
GLUT.DisplayFunc(display)        # 描画コールバックの登録
GLUT.KeyboardFunc(keyboard)      # キーボード入力コールバックの登録
GLUT.MouseFunc(mouse)            # マウス入力コールバックの登録
GL.ClearColor(0.4,0.4,1.0,0.0)   # 背景色の設定
setup_menu(menu)                 # メニューの設定
GLUT.MainLoop()

case式

ここでメニューコールバックで利用しているcase式は, if式のような機能を提供するRubyの制御構造です. このコールバックのcase式は,if式で次のように書き換えることができます.


  if id == 1
    __lamp = (__lamp + 1) % 3 # 0 -> 1, 1 -> 2, 2 -> 0
    GLUT.PostRedisplay()
  elsif id == 2
    exit 0
  end

case式の構造は次のようになります.


   case 式0
   when 式11,式12,...
    式0の値が式11,式12,...のいずれかの値と等しくなるときに実行する節
   when 式21,式22,...
    式0の値が式21,式22,...のいずれかの値と等しくなるときに実行する節
    :
   else
     以上のいずれでもないときに実行する節
   end

各whenの後に式を1個以上書きます. case式の実行手順はif式と同様です. 上から順に条件がチェックされます. いずれかの条件にあてはまった場合,対応する節を実行して, 最後の式の値をcase式の値として,case式の評価を終了します. if式と同様にどの条件にもあてはまらない場合に実行することがあれば,else節に記述します. else節はなくても構いません.

ウインドウサイズの変更

描画ウインドウのサイズを変更しても,ビューポートクリッピングウインドウとの関係は変わりません. したがって,描画ウインドウを拡大すれば,図形がそれだけ大きく表示されることになります. またウインドウの縦横の比が変われば,図形がそれだけ歪んだ形で表示されることになります. 描画ウインドウのサイズが変更されたときも図形の形状を保つには,クリッピングウインドウを描画ウインドウに合わせて変更する必要があります. OpenGLでは,ウインドウのサイズの変更に対応するコールバックを用意できます. 登録にはGLUT.ReshapeFuncを使います. ウインドウサイズ変更コールバックには, 新しいウインドウの幅と高さ(ピクセル単位)が渡されます. なお,このコールバックは,ウインドウが最初に生成されるときにも呼び出されます.



#### ウインドウサイズ変更コールバック ########
reshape = Proc.new { |w,h|
  GL.Viewport(0,0,w,h)
  GL.LoadIdentity()
  x=w.to_f/WSIZE  # w.to_f: 整数を浮動小数点数に変換
  y=h.to_f/WSIZE  # h.to_f: 整数を浮動小数点数に変換
  GLU.Ortho2D(-x,x,-y,y)
  __y = y         # 状態変数の更新
  GLUT.PostRedisplay()
}


GLUT.Init()
GLUT.InitWindowSize(WSIZE,WSIZE) 
GLUT.CreateWindow("Traffic Signal")
GLUT.DisplayFunc(display)        # 描画コールバックの登録
GLUT.KeyboardFunc(keyboard)      # キーボード入力コールバックの登録
GLUT.MouseFunc(mouse)            # マウス入力コールバックの登録
GLUT.ReshapeFunc(reshape)         # ウインドウサイズ変更コールバック登録
GL.ClearColor(0.4,0.4,1.0,1.0)   # 背景色の設定
GLUT.MainLoop()

ウインドウサイズ変更のコールバックを加えたサンプルプログラムtraffic_signal_reshape.rbを用意しました. このプログラムでは,ウインドウのサイズが変わっても信号機のサイズが変わりません. また信号機は画面の中央に位置して上下に動かないようになっています. さらに支柱部分が常に画像の下端に接するように変更を加えています.

[CG実習 > インタラクション]