[index]

1. OpenGL 入門

1.1 OpenGL 概要

◆ 標準3次元グラフィックスライブラリ

OpenGL はシリコングラフィックス社が開発した3次元グラフィックスライブラリです.約150種類のコマンドから構成されています.OpenGL を用いると,3次元立体や2次元画像によって構成される,高品質な仮想空間を表現することができ,OpenGL は業界標準となっています.多くの CG アニメーションは,OpenGL をベースにした開発環境で制作されています.

◆ 名前の由来

OpenGL という名前は,Open な Graphics Library というところからきています.以前(かなり前)には,IRIS GL というのがありまして,それはシリコングラフィックス社製の特別なマシン上でしか動かなかったのですが,open になって,Windows や MacOS など,Linuxなどの OSでも動かせるようになりました.

◆ 基本機能

OpenGL の基本的な機能には,次のようなものがあります.

この演習では,これらのような機能をどのように使えばいいのか,を学んでいきます.

◆ OpenGLのデータ処理過程

次の図は,OpenGL のデータの処理過程を表したブロックダイアグラムです.左からコマンドが入力され,パイプラインを流れるように順に処理されます.

OpenGL アーキテクチャ

コマンド には,幾何学的オブジェクトを指定するものや,処理ステージにおけるオブジェクトの取り扱い方法を制御するものなどがあります.全てのコマンドをパイプラインに通して直ちに実行するとは限らず, ディスプレイリスト にコマンドを一旦保持してから,後で処理することも可能です.

エバリュエータ では,多項式近似の曲線と曲面といった幾何学形状を処理します. OpenGL は,幾何学的プリミティブを頂点の集合として処理するのですが,それは, 頂点単位の演算とプリミティブの形成 というステージで行われます.この二つのステージは,幾何学的な処理をする部分です.

続いて,ラスタライズフラグメント単位の演算 では,フレームバッファ(ラスタスキャン型CRTの場合に,画像の情報を一時的に収めておく場所)にためる値に関係する計算をします.ここでの演算には,フレームバッファのzバッファ値の条件つき更新や,入力された色ともともと保存されていた色との混合,マスク処理などが含まれます.

入力データは頂点データに限らず,ビットマップ形式のような画像も入力可能で,そういう形式のデータは,テクスチャマッピングに利用する画像とみなされ,前述のステージは通らず, ピクセルの演算 ステージでピクセルとして処理されます.この結果は,テクスチャメモリ にラスタライズ用に保存,または,ただちにラスタライズされます.その後は,あたかも幾何学的データから生成されたものであるかのように,フレームバッファに結合されます.

◆ OpenGLの関連ツール: GLUT

OpenGL は OS やウィンドウシステムに依存しています.そのため,OpenGL だけを利用してプログラムを書く場合,コンピュータの画面に,OpenGL の画像を表示するためのウィンドウを開くための処理やマウスやキーボードからの入力の処理などは,環境が変わるごとに書き換える必要がありますが,それはとても不便です.そこで,一般的に,GLUT(OpenGL Utility Toolkit)という OpenGL の初期化,ウィンドウ操作,イベント処理を行う,フリーのライブラリを利用して,環境の違いを気にしないでプログラミングできるようにします.

◆ GLUT を使った,OpenGL プログラムの基本的な流れ

  1. glut ライブラリを初期化する
    glutInit(&argc, argv);

  2. ディスプレイモードを設定する
    OpenGLの画面を,フルカラーで描くのか, それとも,もともと用意した色だけ,すなわち, インデックスカラーで描くのか,とか,陰面消去するのかどうか, などを設定します.
    glutInitDisplayMode(GLUT_RGBA|GLUT_SINGLE);

  3. ウィンドウの開く位置や大きさを指定する(時には、マシンの環境によります)
    glutInitWindowPosition(x0, y0);
    glutInitWindowSize(width, height);

  4. 名前をつけて,ウィンドウを開く
    glutCreateWindow(title);

  5. 画面をクリアする色を指定する
    glClearColor(red, green, blue, alpha);

  6. 画面をクリアする
    glClear();

  7. コマンドバッファにたまっている未実行の命令を実行する
    glFlush();

  8. 入力まち状態の無限ループ
    glutMainLoop(NULL);

以上が大まかな流れですが,やってみないとよくわからないと思いますので,早速サンプルプログラムを書いて,コンパイルして,実行してみましょう.

[index]


1.2 コンパイルの準備

まず,この演習用のディレクトリを作成しましょう.ホームディレクトリ(/usr/people/s0x10xx/ など)にファイルがバラバラと置いてあるのは,一般的に,あまり感心されません.ディレクトリ名は何でもいいですが,次の例では Graphics Science Seminar の頭文字をとって,gss という名前にしています.

% mkdir gss
では,作成したディレクトリへ移動しましょう.
% cd gss
OpenGL と GLUT のライブラリを利用してコンパイルするには,cc(あるいはgcc) または CC コマンドに以下のようなオプションをつけて実行します.
% cc program.c -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm
でも,毎回,こんなに長いコマンドを入力するのは面倒なので,以下のどれかの方法で,ラクをしてください.

◆ alias する

各自のホームディレクトリの .cshrc ファイルの最後に,次の1行を書き加えて,改行します.
alias ccgl 'cc \!* -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm'
そうすると,以降ログインした時には,以下のコマンドを入力するだけですみます.
% ccgl program.c
なお,書き加えた直後だけ,以下のコマンドを実行し,書き加えた alias を有効にしてください.
% source ~/.cshrc
この方法は,カレントディレクトリを気にしないで,どこででも実行できるので便利です.

◆ シェルスクリプトを書く

以下のような内容のファイル ccgl を作成します.
#!/bin/sh
exec cc "$@" -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm
そのあと chmod コマンドで,ファイルのモードを変更して,実行可能にします.
% chmod +x ccgl
以降はこの ccgl コマンドを使ってコンパイルできます。
% ccgl program.c
ただし,ccgl ファイルが,カレントディレクトリ,または,コマンドパスに記述されているディレクトリに存在しない場合は,パスをちゃんと入力する必要があります.

◆ Makefile を作る

以下の内容の Makefile というファイルを作ります。 --Tab-->のところは、タブ (Tab) キーを使って字下げしてください。
LIBS = -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm
a.out: program.c
--Tab-->cc program.c $(LIBS)
このような記述の Makefile のあるディレクトリで make コマンドを実行すると, program.c がコンパイルされて a.out という実行ファイルが生成されます.
% make
このコマンドは emacs の中からも M-x compile で起動できます.

Makefile には,ファイルの生成規則を記述します. make は実行すると,Makefile 中の最初の生成規則を探します.上のファイルの場合,a.out の行がそれになります.この行には a.out というターゲットを生成するのに program.c が必要だという依存関係を記述しており,その次の行に実際に a.out を生成するための手続きを記述しています.

ターゲットが複数あるときは以下のように記述します.

LIBS = -L/usr/X11R6/lib -lglut -lGLU -lGL -lXmu -lXi -lXext -lX11 -lm
all: prog1 prog2
prog1: prog1.c
--Tab-->cc prog1.c -o prog1 $(LIBS)
prog2: prog2.c
--Tab-->cc prog2.c -o prog2 $(LIBS)
この場合の最初の生成規則は all の行で, all を生成するには prog1 と prog2 が必要だという依存関係を記述しています. make は prog1 と prog2 の両方の生成が完了した時点で終了します.特定のターゲットだけを生成したいときは,そのターゲット名を make の引数に指定します.
% make prog1
Makefile を使うと,プログラムファイル名を変更するたびに,2行ほど追加記述しないといけないように思えますが, make コマンドにはあらかじめ約束事があり,それに従う処理の記述は省略できます.詳しくは,床井先生が書かれた Makefile の補足を参照してください.

[index]


1.3 ウィンドウを開く

GLUT を利用した OpenGL プログラムの最も基本的なものは,ウィンドウを開くだけ,の,次のようなプログラムです.このソースプログラムを適当なファイル名(たとえば,prog01.c)で作成し,コンパイルして出来上がった実行プログラム(a.out)を実行してみましょう.

/* prog01.c */
/* 空のウィンドウを開く */

#include <GL/glut.h>

/* 描画 */
void display(void)
{
}

int main(int argc, char** argv)
{
  /* 初期化 */
  glutInit(&argc, argv);

  /* ウィンドウの生成 */
  glutCreateWindow(argv[0]);

  /* 描画ルーチンの設定 */
  glutDisplayFunc(display);

  /* 無限ループ */
  glutMainLoop();
  return 0;
}

ウィンドウのサイズと位置
#include <GL/glut.h>
GLUT ライブラリを利用する場合のインクルードファイルです.環境に応じて,glut.h が他に必要なヘッダーファイルをインクルードしてくれるので,ソースコードは異なる環境で共通に利用できます.
glutInit(int *argcp, char **argv)
GLUT および OpenGL 環境を初期化します. argcp には main 関数の引数を示す argc のアドレスを渡します. argv は main 関数の argv そのものです. GLUT で解釈された引数は配列から消去され, X Window で使われるオプション -display, -geometry などはここで処理されます.プログラム自身で処理すべき引数があるときは,この後で処理します.
int glutCreateWindow(char *titleString)
OpenGL による図形等を描画するための台紙(?)となるウィンドウをこの関数で開きます.引数 titleString によって,そのウィンドウのタイトルバーなどに表示されるウィンドウの名前の文字列を指定します. なお,戻り値は開いたウィンドウの識別子です.
void glutDisplayFunc(void (*func)(void))
引数 func は開いたウィンドウ内に描画する関数へのポインタです.ウィンドウが開かれたり,他のウィンドウによって隠されたウィンドウが再び現れたりして,ウィンドウを再描画する必要があるときにこの関数が実行されます.この関数内で図形などの描画を行います.
void glutMainLoop(void)
これは無限ループです.この関数を呼び出すことで,プログラムはイベントの待ち受け状態になります.

このように,プログラムは,

  1. 初期化して,
  2. ウィンドウを開いて,
  3. そのウィンドウ内に絵を描く関数を決めて,
  4. 何かことが起こるのを待つ.
という流れになっています.

最初に display() が実行されるのは,初めてウィンドウが開いたとき,すなわち, glutMainLoop() が glutCreateWindow() の指示を受けてウィンドウの生成を完了したときになります.また,その後も,このウィンドウがほかのウィンドウに隠され再び現れたときのように,ウィンドウの再描画が必要になったときに実行されます.

上のプログラムでは display() の中身として何も記述していないので, display() が呼び出されても何も仕事をしません.試しにこのウィンドウを移動したり,他のウィンドウで隠したりしてみてください.ウィンドウの中の表示はおかしなものになっていると思います.

このように複数の(オーバーラップ可能な)ウィンドウが使用できるウィンドウシステム環境に対応したプログラムでは,処理の流れは時間軸に沿って「プログラムの始めから終りへ」ではなく,何かこと(事象)が起るたびに「プログラムの各部がランダムに」実行されます.従って,このプログラムのスタイルも,「事象」に対して,その「対処方法」を登録していくというものになります.一般に,この事象をイベント と呼び,対処方法の手続きをハンドラ, イベントハンドラと呼びます.

このプログラムには,「プログラムを終了する方法」を組み込んでいないので,終了するには,実行したウィンドウで Ctrl-C をタイプするか, OpenGL ウィンドウのタイトルバーの左のボタンをクリックして「閉じる」か「中止」を選んでください.

prog01.c では,関数 display() に何も記述していなかったので,ウィンドウの枠だけ出現し,ウィンドウの中身はでたらめだったと思います.「基本的な流れ」に登場する残りの関数を使って,ウィンドウを塗りつぶしてみましょう. prog01.c に太字のところを追加し,コンパイルしてプログラムを実行してください.

/* prog01.c */
/* 青く塗りつぶしたウィンドウを開く */

#include <GL/glut.h>

/* 描画 */
void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glFlush();
}

void init(void)
{
  glClearColor(0.0, 0.0, 1.0, 0.0);
}


int main(int argc, char** argv)
{
  /* 初期化 */
  glutInit(&argc, argv);

  /* ウィンドウの生成 */
  glutInitDisplayMode(GLUT_RGBA);
  glutCreateWindow(argv[0]);

  /* OpenGL 初期化ルーチンの呼出し */
  init();

  /* 描画ルーチンの設定 */
  glutDisplayFunc(display);

  /* 無限ループ */
  glutMainLoop();
  return 0;
}
void glutInitDisplayMode(unsigned int mode)
ディスプレイの表示モードを設定します. mode として,次のような値を指定します.
  • RGBモードか,カラーインデックスモードか (GLUT_RGBS or GLUT_INDEX)
  • ダブルバッファを使うかどうか (GLUT_SINGLE or GLUT_DOUBLE)
    これは,アニメーションをさせるときに重要なポイントです.
  • 隠面消去をするかどうか (GLUT_DEPTH)
例えば,RGBモードで隠面消去してアニメーションをするときは,
glutInitDisplayMode(GLUT_RGBA | GLUT_DOBULE | GLUT_DEPTH);
というように指定します.このような設定は,演習をすすめていくうちにでてきますが,ここでは,GLUT_RGBA モードだけを指定します.mode に GLUT_RGBA を指定した場合は,色の指定を RGB(赤緑青,光の3原色)で行えます.色の話は,別の機会にもう少し詳しく説明します.
void glClearColor(GLclampf red, GLclampf green, GLclampf blue, GLclampf alpha)
glClear(GL_COLOR_BUFFER_BIT) でウィンドウを初期化する際の色を指定します. red,green,blue はそれぞれ赤,緑,青色の成分の強さを示す GLclampf 型(float 型と等価)の値で,0〜1 の間の値を持ちます. 1.0 が最も明るく,この3つに (0.0, 0.0, 0.0)を指定すれば黒色, (1.0, 1.0, 1.0) を指定すれば白色になります.上の例ではウィンドウは青色で塗りつぶされます。最後の alpha はα値と呼ばれ,OpenGL では不透明度として扱われます(0.0 で透明,1.0 で不透明).ここではとりあえず 0.0 にしておいてください。

void glClear(GLbitfield mask)
ウィンドウを塗りつぶします. mask には塗りつぶすバッファを指定します. OpenGL が管理する画面上のバッファ(メモリ)には,色を格納するカラーバッファの他,隠面消去に使うデプスバッファ,凝ったことをするときに使うステンシルバッファ,カラーバッファの上に重ねて表示されるオーバーレイバッファなど,いくつかのものがあり,これらが一つのウィンドウに重なって存在しています. mask に GL_COLOR_BUFFER_BIT を指定したときは,カラーバッファだけが塗りつぶされます.
glFlush(void)
glFlush() はまだ実行されていない OpenGL の命令を全て実行します. OpenGL は関数呼び出しによって生成される OpenGL の命令をその都度実行するのではなく,いくつか溜め込んでおいてまとめて実行します.このため,ある程度命令が溜まらないと関数を呼び出しても実行が開始されない場合があります. glFlush() はそういう状況でまだ実行されていない残りの命令の実行を開始します.頻繁に glFlush() を呼び出すと,描画速度が低下することもあります.

1.1 で示した「基本的な流れ」と,prog01.c で呼ばれている関数が実行される順とが一致していることを確認してください.

1.4 線を引く

ウィンドウ内に線を引いてみます。 prog1.c を以下のように変更し、コンパイルしてプログラムを実行してください。

#include <GL/glut.h>

void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glBegin(GL_LINE_LOOP);
  glVertex2d(-0.9, -0.9);
  glVertex2d(0.9, -0.9);
  glVertex2d(0.9, 0.9);
  glVertex2d(-0.9, 0.9);
  glEnd();
  glFlush();
}

void init(void)
{
  /* 変更なし */
}

int main(int argc, char *argv[])
{
  /* 変更なし */
}
void glBegin(GLnum mode)
void glEnd(void)
図形を描くには、 glBegin()〜glEnd() の間にその図形の各頂点の座標値を設定する関数を置きます。 glBegin() の引数 mode には描画する図形のタイプを指定します。
void glVertex2d(GLdouble x, GLdouble y)
glVertex2d() は2次元の座標値を設定するのに使います。引数の型は GLdouble (double と等価)です。引数が float 型のときは glVertex2f()、 int 型のときは glVertex2i() を使います。

1.5 図形のタイプ

glBegin() の引数 mode に指定できる図形のタイプには以下のようなものがあります。詳しくは man glBegin を参照してください。

GL_POINTS
点を打ちます。
GL_LINES
2点を対にして、その間を直線で結びます。
GL_LINE_STRIP
折れ線を描きます。
GL_LINE_LOOP
折れ線を描きます。始点と終点の間も結ばれます。
GL_TRIANGLES / GL_QUADS
3/4点を組にして、三角形/四角形を描きます。
GL_TRIANGLE_STRIP / GL_QUAD_STRIP
一辺を共有しながら帯状に三角形/四角形を描きます。
GL_TRIANGLE_FAN
一辺を共有しながら扇状に三角形を描きます。
GL_POLYGON
凸多角形を描きます。
図形プリミティブ一覧

OpenGL を処理するハードウェアは、実際には3角形しか塗り潰すことができません(モノによっては4角形もできるものもあります)。このため GL_POLYGON の場合は、多角形を3角形に分割してから処理します。従って、もし描画速度が重要なら GL_TRIANGLE_STRIP や GL_TRIANGLE_FAN を使うようプログラムを工夫してみてください。また GL_QUADS も GL_POLYGON より高速です。

1.6 線に色を付ける

線に色を付けてみます。 prog1.c を以下のように変更し、コンパイルしてください。プログラムを実行したら線は何色で表示されたでしょうか?

#include <GL/glut.h>

void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glColor3d(1.0, 0.0, 0.0);
  glBegin(GL_LINE_LOOP);
  glVertex2d(-0.9, -0.9);
  glVertex2d(0.9, -0.9);
  glVertex2d(0.9, 0.9);
  glVertex2d(-0.9, 0.9);
  glEnd();
  glFlush();
}

void init(void)
{
  /* 変更なし */
}

int main(int argc, char *argv[])
{
  /* 変更なし */
}
void glColor3d(GLdouble r, GLdouble g, GLdouble b)
glColor3d() はこれから描画するものの色を指定します。引数の型は GLdouble 型(double と等価)で、 r,g,b にはそれぞれ赤、緑、青の強さを 0〜1 の範囲で指定します。引数が float 型のときは glColor3f()、 int 型のときは glColor3i() を使います。

1.7  図形を塗りつぶす

図形を塗りつぶしてみます。 GL_LINE_LOOP を GL_POLYGON に変更し、ついでに背景も白色に変更しましょう。変更したプログラムをコンパイルして実行してください。

#include <GL/glut.h>

void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glColor3d(1.0, 0.0, 0.0);
  glBegin(GL_POLYGON);
  glVertex2d(-0.9, -0.9);
  glVertex2d(0.9, -0.9);
  glVertex2d(0.9, 0.9);
  glVertex2d(-0.9, 0.9);
  glEnd();
  glFlush();
}

void init(void)
{
  glClearColor(1.0, 1.0, 1.0, 0.0);
}

int main(int argc, char *argv[])
{
  /* 変更なし */
}

色は頂点毎に指定することもできます。 prog1.c を以下のように変更してください。コンパイルしてプログラムを実行すると、どういう色の付き方になったでしょうか?

#include <GL/glut.h>

void display(void)
{
  glClear(GL_COLOR_BUFFER_BIT);
  glColor3d(1.0, 0.0, 0.0);
  glBegin(GL_POLYGON);
  glColor3d(1.0, 0.0, 0.0); /* 赤 */
  glVertex2d(-0.9, -0.9);
  glColor3d(0.0, 1.0, 0.0); /* 緑 */
  glVertex2d(0.9, -0.9);
  glColor3d(0.0, 0.0, 1.0); /* 青 */
  glVertex2d(0.9, 0.9);
  glColor3d(1.0, 1.0, 0.0); /* 黄 */
  glVertex2d(-0.9, 0.9);
  glEnd();
  glFlush();
}

void init(void)
{
  /* 変更なし */
}

int main(int argc, char *argv[])
{
  /* 変更なし */
}

多分、多角形の内部は頂点の色から補間した色で塗りつぶされたと思います。このプログラムは後で使用するので、 prog2.c というコピーを作っておいてください。

% cp prog1.c prog2.c


次回以降,このような基本的なプログラムへ,さまざまな関数を呼び出す記述を追加していくことで,CG のプログラムを発展させていきます.

[index]