サンプルプログラム
まずサンプルプログラム(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として登録することもできます.
座標系に関する注意

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にも簡単なメニューを作成する機能が用意されています. メニュー作成の手順は次のようになります.
- メニューコールバックを用意する
メニューに加える各項目が選択されたときの処理を記述します. 各項目には番号をつけておきます. コールバックには,この番号が渡されることになります. - メニューコールバックを登録する
GLUT.CreateMenuを使います. - メニューの各項目にタイトルをつける
GLUT.AddMenuEntryを使って, メニューに加える各項目にタイトルをつけます. コールバックで項目につけた番号を利用します. - メニューを呼び出すマウスボタンを設定する. 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を用意しました. このプログラムでは,ウインドウのサイズが変わっても信号機のサイズが変わりません. また信号機は画面の中央に位置して上下に動かないようになっています. さらに支柱部分が常に画像の下端に接するように変更を加えています.