[CG実習 > OpenGLの基礎]

OpenGLの基礎

OpenGLは,3次元グラフィクスプログラミングにつかわれる標準的なライブラリです. ライブラリとは,プログラムの部品セットであるといえます. ライブラリを利用することで,自分で0から書かなくても, ライブラリが提供する部品を組み合わせることでプログラムを作成することができます. ここでは,OpenGLの基礎を説明します.

OpenGLの概要

OpenGLは,3次元グラフィクスプログラミングにつかわれる標準的なライブラリで,SGI(Silicon Graphics, Inc.)がGLという同社のコンピュータ専用のグラフィクスライブラリをもとに開発したものです. OpenGL以外でよく用いられる3次元グラフィクスライブラリとしては, Microsoft社のDirect3D(現在はDirectX Graphicsの一部という位置づけ)が挙げられます. このDirect3DはWindows専用ですが, OpenGLは,LinuxなどのUnix系OS,Macintosh,Windowsなどさまざまな環境で利用することができます. つまり,Direct3Dで書いたプログラムはWindowsでは動きますが,他の環境では動作しません. 一方,OpenGLで書いたプログラムは, Unix,Macintosh,Windowsのいずれでも簡単に動かすことができます.

OpenGLは,CGで仮想的な3次元のシーン(あるいは2次元のシーン)を描画(レンダリング;rendering)するために必要な機能を提供します. 前期(CG実習A)に行ったように,CGでは最終的には画像データを生成します. 本来,3次元シーンを描くには,物体とカメラの関係から,物体がカメラにどのように写り込んで,その結果,それがどのような線分あるいは面として描けるかを計算して,最終的な画像データを生成していくことが必要となります. このとき,光源のデータ,物体表面の反射特性などから,物体の各点がどんな色に見えるかを計算することも必要ですし,物体全体がカメラにはとらえられず一部しか写らない場合や, 複数の物体があれば,前の物体が後の物体を覆い隠すような状況があることなども考慮にいれる必要があります. このような処理を行うことはもちろん可能ですが,全てを自分で実現するのはとても大変です.

OpenGLは,われわれの代わりにそのような面倒な計算を全て行ってくれます. プログラムの中で,カメラの位置や向き,描画する物体の形状,色あるいは模様および位置,あるいは光源の位置や特性など,3次元シーンを記述するデータさえ与えれば, それらのデータに基づいたシーンを実際にレンダリングする処理は,すべてOpenGLに任せることができます. また物体を動かしたり,変形させたりなどのアニメーションを行うことも可能です.

ところで,グラフィクスのプログラムでは,たとえば,物体を好きなように動かしていろいろな方向から見ることができる,色を変えることができるなど,ユーザの指示によって適切な反応を起こすようなインタラクションの機能も欠かすことができません. そのような機能は,OpenGLに付加されているGLUTライブラリで提供されます.

Ruby/OpenGL

一般的に使われているOpenGLは,プログラミング言語Cのライブラリとして提供されています. Cのプログラムは機械語にコンパイルして利用するため,高速に動作します. ただし,たとえばWindowsでコンパイルして作成したプログラムは,Windowsでしか動作しません. 他の環境で動作させるには,(コンパイルする前の)Cのソースプログラムを,その環境で再度コンパイルする必要があります. この意味で,Cで作成したOpenGLプログラムは,そのままでどこでも動くわけではありません.

この実習では,Cの代わりに,RubyのOpenGLライブラリ(Ruby/OpenGL)を用います. Rubyはインタプリタで動作するため, Ruby/OpenGLで作成したプログラムは,(RubyとRuby/OpenGLが使えれば)どこでもそのままで動作します. 一般にRubyのプログラムは,コンパイルされたプログラムよりも動作は遅くなりますが, (前期のGfcライブラリがそうだったように)Ruby/OpenGL自体はコンパイルされていて,実行時間の短縮がはかられています. プログラムが複雑すぎたり,あるいはプログラムを動かすコンピュータの性能が低すぎたりしなければ,Ruby/OpenGLで十分に実用的なプログラミングを行うことができます.

OpenGLプログラムの基本的な構造

まず最初に,次の簡単なOpenGL(正確には,OpenGLとGLUTを使った)サンプルプログラム(opengl_first.rb)を見てみましょう.


require "opengl"
require "glut"

# 描画コールバック
display = Proc.new {
   GL.Color(1.0,0.0,0.0)          # 描画色を赤に設定
   GL.Begin(GL::POLYGON)          # 図形プリミティブ(多角形)指定開始
      GL.Vertex(-0.5, 0.5)        #   頂点1
      GL.Vertex(-0.5,-0.5)        #   頂点2
      GL.Vertex( 0.5,-0.5)        #   頂点3
      GL.Vertex( 0.5, 0.5)        #   頂点4
   GL.End()                       # 図形プリミティブ(多角形)指定終了
   GL.Flush()                     # 描画強制実行
}

# キーボード入力コールバック
keyboard = Proc.new { |key,x,y|
  exit(0)                         # 何かキーが押されたらプログラム終了
}

GLUT.Init()                       # 初期化処理
GLUT.InitWindowSize(300,300)      # ウインドウの大きさの指定
GLUT.CreateWindow("OpenGL:test")  # ウインドウの作成
GLUT.DisplayFunc(display)         # 描画コールバック登録
GLUT.KeyboardFunc(keyboard)       # キーボード入力コールバック登録
GLUT.MainLoop()                   # イベントループ開始

このプログラムは,通常のrubyプログラムと全く同様に次のように実行します. 実行すると,中央に赤い正方形が描かれたウインドウが開かれます. ウインドウの上で何かキーを押すと,画面が消えて,プログラムが終了します.

$ ruby opengl_first.rb

イベント,コールバック,イベントループ

このサンプルでもみたとおり,OpenGLのプログラムでは,新しいウインドウをつくって,そこで描画を行います. さて,ここで一般的なウインドウの挙動を考えてみましょう. 私たちは,ウインドウ上のボタンをクリックしたり,ウインドウのバーをドラッグしたり,あるいは,ウインドウ上でキーを入力したりすることで,ウインドウのサイズを変えたり,ウインドウを動かしたりなどします.

このような動作がプログラムでどのように実現されているかを考えてみると,ウインドウの中でマウスがクリックされたら,クリックされた場所を調べて,たとえばボタンが押されたと分かったら,それに対応する処理を行う,あるいはキーが入力されたら,入力されたキーの種類に対応した処理を行うというようになっていることが想像できます. 実際,このようなプログラムは,「こういうことが起きたら,こう処理する」というように対処すべきケースとそれ対する処理を決めて,それらの関係を全て登録しておいて,あとは,何かが起きるのを待って,対処すべきケースが発生するたびに予め決められた処理を行うという一連の動作を繰り返すようにすれば実現できます.

イベントループ

プログラムで処理すべき「ケース」をイベント(event)と呼びます. そのイベントに対処するための処理をまとめたものをコールバック(callback)と呼びます. また,イベントの発生を監視して,対処すべきイベントが発生したら,コールバックを実行して,またイベントの発生を待つような繰り返しの処理をイベントループと呼びます. このようなイベント,コールバック,イベントループにより特徴づけられるプログラムをイベント駆動型(event-driven)といいます.

サンプルプログラムでは,次の二つのコールバックを使っています. これらは,それぞれ,画面の描画,キーボードの入力を処理するためのものです. コールバックは,プログラムの前半でRubyのProcオブジェクトとして与えられています (Procオブジェクトとは,ごく大雑把にいえば,データとして使えるメソッドのようなものです).


GLUT.DisplayFunc(display)         # 描画コールバック登録
GLUT.KeyboardFunc(keyboard)       # キーボード入力コールバック登録

さて,keyboardというコールバックは,キー入力があったときに行われる処理を記述したものです. この場合,どのキーが押されたかによらず,exitによりプログラムを終了させています. 次にdisplayというコールバックが何をしているのかを見てみるために,プログラムを実行して,ウインドウの形を変えてみて下さい. 正方形が歪んで,さらに正方形以外の部分がおかしな状態になったのではないでしょうか. displayは,必要に応じて画面を(再)描画する処理を記述したものです. たとえば,ウインドウの形が変わったときには,画面を再描画する必要があります. ここで詳細は述べませんが,このコールバックでは正方形しか描いていため,背景部分がキチンと再描画されなくなっています. displayを次のように変更すれば,背景も含めて再描画されることになります.


# 描画コールバック
display = Proc.new {
   GL.Clear(GL::COLOR_BUFFER_BIT) # 背景のクリア
   GL.Color(1.0,0.0,0.0)          # 描画色を赤に設定
   GL.Begin(GL::POLYGON)          # 図形プリミティブ(多角形)指定開始
      GL.Vertex(-0.5, 0.5)        #   頂点1
      GL.Vertex(-0.5,-0.5)        #   頂点2
      GL.Vertex( 0.5,-0.5)        #   頂点3
      GL.Vertex( 0.5, 0.5)        #   頂点4
   GL.End()                       # 図形プリミティブ(多角形)指定終了
   GL.Flush()                     # 描画強制実行
}

なおウインドウを変形すると描画される図形が歪むのは,ウインドウ内にとらえられている座標の範囲が(そのように設定しない限り)変わらないためです.

背景色は次のように指定することができます. メソッドGL.ClearColorの引数は, 背景色のRGBA値を示します.それぞれ0.0〜1.0で指定します. ここでA値とは透明度を表します.通常はα値といいます. なお,このプログラムではα値は意味を持ちません(指定しても無視されます.α値を使うには,いくつかの設定が必要です).


GLUT.Init()                       # 初期化処理
GLUT.InitWindowSize(300,300)      # ウインドウの大きさの指定
GLUT.CreateWindow("OpenGL:test")  # ウインドウの作成
GLUT.DisplayFunc(display)         # 描画コールバック登録
GLUT.KeyboardFunc(keyboard)       # キーボード入力コールバック登録
GL.ClearColor(0.0, 1.0, 0.0, 0.0) # 背景色の設定
GLUT.MainLoop()                   # イベントループ開始

2次元図形の描画

次に,2次元の図形を描画する方法を見てみます. すでに見ている通り,サンプルプログラムでは,displayコールバックで正方形を描いています.


# 描画コールバック
display = Proc.new {
   GL.Color(1.0,0.0,0.0)          # 描画色を赤に設定
   GL.Begin(GL::POLYGON)          # 図形プリミティブ(多角形)指定開始
      GL.Vertex(-0.5, 0.5)        #   頂点1
      GL.Vertex(-0.5,-0.5)        #   頂点2
      GL.Vertex( 0.5,-0.5)        #   頂点3
      GL.Vertex( 0.5, 0.5)        #   頂点4
   GL.End()                       # 図形プリミティブ(多角形)指定終了
   GL.Flush()                     # 描画強制実行
}

図形プリミティブ

OpenGLでは,描画する対象の一つ一つをプリミティブ(primitive)といいます. 図形プリミティブの描画を行うには,少なくとも,その図形の形状データを与える必要があります. 形状データの指定は次のように行います.


   GL.Begin(プリミティブの種類)
      GL.Vertex(点の座標)
             :
   GL.End()

プリミティブは一つの図形か,同種の図形の集まりになっています. 一つのプログラムで複数のプリミティブを描画することができます.

基本的な図形プリミティブには次のようなものがあります.

GL::POINTS 点の集まり 描画する点を順に指定する
線分 GL::LINES 線分の集まり 線分の両端点を順に指定する
GL::LINE_STRIP 折れ線 点を順に指定する
GL::LINE_LOOP 閉じた折れ線 点を順に指定する.最初の点と最後の点がつなげられる
GL::POLYGON 単純な凸多角形 頂点を順に指定する
GL::QUADS 四角形の集まり 頂点を4つずつ指定する
GL::TRIANGLES 三角形の集まり 頂点を3つずつ指定する
GL::TRIANGLE_STRIP 三角形の帯 最初の3点で1番目の三角形を指定する.あとは点を一つずつ加えて,直前に指定した2点と合わせて2番目以降の三角形を指定していく
GL::QUAD_STRIP 四角形の帯 最初の4点で1番目の四角形を指定する.あとは点を二つずつ加えて,直前に指定した2点と合わせて2番目以降の四角形を指定していく
GL::TRIANGLE_FAN 三角形を集めた扇型 中心となる点から始めて最初の3点で1番目の三角形を指定する.あとは点を一つずつ加えて,最初の1点,さらに直前に指定した1点と合わせて2番目以降の三角形を指定していく

図形データを描画するには,座標系の設定などが必要となります. OpenGLの座標系はx軸が右向き,y軸が上向きとなります. デフォルト(何も指定しない状態)では,x,yともに[-1.0,1.0]の範囲が描画されます. ウインドウのサイズが変更されても,この関係は変わりません. ウインドウが拡大あるいは縮小されたときの挙動を変えるには,専用のコールバックを用意する必要があります. 座標系の設定法などについては後で述べます.

属性

面図形には色(光の反射特性)や模様,線分なら色や太さなど, 図形プリミティブに対しては,その属性(attribute)を指定することができます.

たとえば,サンプルプログラムでは,GL.Colorで正方形の色を指定しています. GL.Colorは描画色を指定するもので,その引数はRGB値を示します(RGBAも指定できます). RGB値の指定方法は,GL.ClearColorの場合と同様です.

面図形の色を頂点毎に指定することもできます. この場合,デフォルトでは,面の色は頂点の色の線形補間で決められます. たとえば,サンプルプログラムのdisplayを次のように変更してみて下さい. 変更後に実行すると,グラデーション付きの三角形が表示されるはずです.


display = Proc.new {
  GL.Clear(GL::COLOR_BUFFER_BIT) # 背景のクリア
  GL.Begin(GL::TRIANGLES)        # 図形プリミティブ(三角形)指定開始
    GL.Color(1.0,0.0,0.0)        #   頂点1の色(赤)
    GL.Vertex(-0.5, 0.5)         #   頂点1
    GL.Color(0.0,1.0,0.0)        #   頂点2の色(緑)
    GL.Vertex(-0.5,-0.5)         #   頂点2
    GL.Color(0.0,0.0,1.0)        #   頂点3の色(青)
    GL.Vertex( 0.5,-0.5)         #   頂点3
  GL.End()                       # 図形プリミティブ(三角形)指定終了
  GL.Flush()                     # 描画強制実行
}

また次のように書くと線分の太さを指定できます.



  GL.LineWidth(3.0) # 太さの指定(これ以降に適用される)
  GL.Begin(GL::LINES)

  GL.End()
  


2次元ビューの設定

図形を描画するには,座標系の設定などが必要となります. さて,OpenGLでは,図形データは世界座標系(world coordinate system)で記述されます. つまり,図形は仮想的な世界に存在すると考えるわけです. また,その図形をカメラでとらえたものがウインドウに描画されていると考えます. このとき,図形をどこからどのように見るか,つまりカメラの位置や姿勢などによって図形のビュー(view;見え方)は変わります. カメラと世界座標系の関係によって,ウインドウに図形がどのように描かれるかが決まるわけです. 描画ウインドウには,スクリーン座標系(screen coordinate system)が設定されます. スクリーン座標系の値は,ピクセルを単位とします. 以上をまとめると,プログラムでは, 世界座標系とスクリーン座標系との関係を記述することで, ビューをコントロールするということができます.

2次元の世界では,描画ウインドウが世界座標系のどの部分をとらえるかを記述することで,ビューを決めます. これは,ちょうど世界座標系に枠を設定して,その中を描画ウインドウに当てはめることに相当します. この枠のことをクリッピングウインドウ(clipping window)といいます. なお,クリッピングウインドウの外にはみ出したものは描画されません. サンプルプログラムを次のように変更してみて下さい.


GLUT.Init()                       # 初期化処理
GLUT.InitWindowSize(300,300)      # ウインドウの大きさの指定
GLUT.CreateWindow("OpenGL:test")  # ウインドウの作成
GLUT.DisplayFunc(display)         # 描画コールバック登録
GLUT.KeyboardFunc(keyboard)       # キーボード入力コールバック登録
GLU.Ortho2D(-2.0,2.0,-2.0,2.0)    # クリッピングウインドウの設定(左,右,下,上の境界)
GLUT.MainLoop()                   # イベントループ開始

GLU.Ortho2Dによって,クリッピングウインドウを設定します. 引数は順に,クリッピングウインドウの左,右,下,上の境界線の座標を表します.

ビューポート

ビューポートとは,表示されるウインドウのうちのどこに描画領域を設定するかを示すものです. クリッピングウインドウの内部は,ビューポートに写されることになります. デフォルトではウインドウ全体がビューポートになります.

ビューポートの指定には,GL.Viewportを用います. GL.Viewportは引数を4個とります.それらは順に, ビューポートの(ウインドウでの)左下の位置(x,y)と,サイズ(幅,高さ)を表します. 値は全てピクセル単位です. なお,ビューポートの設定は通常ウインドウサイズ変更のコールバックの中で行います.

[CG実習 > OpenGLの基礎]