■ 2011年11月29日 [OpenGL][ゼミ] ゴムシミュレータ(1)
忙しい
まあ, 他の先生の忙しさを見ていたら大学に張り付いている私なんぞの忙しさなど屁みたいなもんだとは思いますが, それにしても自分的には結構限界に近いところまで来ている気がします. 元来コンテキストスイッチに時間がかかる質なのに, なんかバラバラのことが同時に進んでいて本当にキツいです. そんな中, 課題というか仕事を投げてほったらかしになってる学生さんのプロジェクトが, どれも自発的に動いてくれて本当に助かります. みんないい子すぐる. てゆうか, 私が下手に手を出さない方が何事もうまくいくんだよな orz
力学シミュレーションのチュートリアル
ということで, また (別の) 学生さん向けにチュートリアルを書きます. かなり昔 (うわ, もう7年前だ) の学生さんが「糸シミュレータ」を作るところから始めて「ゴム網シミュレータ」に拡張するという方法で勉強していたのですが, それをもう一度 (可能なら Transform Feedback を使って) 再現しようと思います. 本当はプログラムを自分でスクラッチから書き始めて欲しいところですが, 本質ではないところで堂々巡りしてても時間がもったいないので, あらかじめ骨格になるプログラムを提供します. 学生さんの本当のスキルを見切れていないのでとっても基本的なところから始めてしまいますけど, 復習だと思ってやってみてください.
スケルトンプログラム
下記に骨格になるプログラムを示します. 例によって GLUT を使っています. 最小限のユーザインタフェースを実現するための骨組みは作ってありますので, それに追加する形でプログラムを完成させてください. ここではさらに「オレオレ補助ライブラリ (Gg.h/Gg.cpp)」を使っています. チュートリアルの最初がこの使い方の説明になってしまっているのは本意ではありませんし, こういうのを使うと実際の処理手順が隠されてしまう (理解の妨げになる) と思うのですが, 使わないとこれまた読めないプログラムになってしまいそうなので我慢してください. 必要ならライブラリのソースを追っかけてください.
#include <cstdlib>
#include <cmath>
// 補助ライブラリ
#include "Gg.h"
using namespace gg;
// 後始末
static void cleanup(void)
{
}
// 初期化処理
static void init(void)
{
// ゲームグラフィックス特論の都合にもとづく初期化
ggInit();
// OpenGL の初期設定
glClearColor(1.0, 1.0, 1.0, 1.0);
// 後始末
atexit(cleanup);
}
// 画面に図形を描画する
static void display(void)
{
// 画面クリア
glClear(GL_COLOR_BUFFER_BIT);
// ダブルバッファリング
glutSwapBuffers();
}
// ウィンドウのオープン・リサイズ時の処理
static void resize(int w, int h)
{
// ウィンドウ全体をビューポート(表示領域)にする
glViewport(0, 0, w, h);
}
// 押されたマウスボタン
static int pressed;
// マウスのボタンを押したときの処理
static void mouse(int button, int state, int x, int y)
{
// 押されたボタンを覚えておく
pressed = button;
switch (button)
{
case GLUT_LEFT_BUTTON:
if (state == GLUT_DOWN)
{
// 左ボタンを押したときの処理
}
else
{
// 左ボタンを離したときの処理
}
break;
case GLUT_MIDDLE_BUTTON:
if (state == GLUT_DOWN)
{
// 中ボタンを押したときの処理
}
else
{
// 中ボタンを離したときの処理
}
break;
case GLUT_RIGHT_BUTTON:
if (state == GLUT_DOWN)
{
// 右ボタンを押したときの処理
}
else
{
// 右ボタンを離したときの処理
}
break;
default:
break;
}
}
// マウスのドラッグ中の処理
static void motion(int x, int y)
{
switch (pressed)
{
case GLUT_LEFT_BUTTON:
// 左ボタンでドラッグ中の処理
break;
case GLUT_MIDDLE_BUTTON:
// 中ボタンでドラッグ中の処理
break;
case GLUT_RIGHT_BUTTON:
// 右ボタンでドラッグ中の処理
break;
default:
break;
}
}
// キーボードをタイプしたときの処理
static void keyboard(unsigned char key, int x, int y)
{
switch (key) {
case '\033':
case 'q':
case 'Q':
// ESC キー, q, Q をタイプしたら終了する
exit(0);
default:
break;
}
}
// メインプログラム
int main(int argc, char *argv[])
{
glutInitWindowSize(500, 500);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
glutCreateWindow("physics");
glutDisplayFunc(display);
glutReshapeFunc(resize);
glutMouseFunc(mouse);
glutMotionFunc(motion);
glutKeyboardFunc(keyboard);
init();
glutMainLoop();
return 0;
}
点を描く
最初に, こういう点を描く手続きを追加します.
とりあえず点の数は 20 個, 点の大きさは 5 くらいにします. またクリッピング空間の大きさが -1≤x,y,z≤1 なので, 両端の点の位置を (-0.9, 0, 0), (0.9, 0. 0) として, この区間に等間隔に点を描くことにします.
#include <cstdlib>
#include <cmath>
// 補助ライブラリ
#include "Gg.h"
using namespace gg;
// 頂点データ
static const int pointn = 20; // 点の数
static const GLfloat pointSize = 5.0f; // 点の大きさ
static const GLfloat point0[] = { -0.9f, 0.0f, 0.0f }; // 始点の位置
static const GLfloat point1[] = { 0.9f, 0.0f, 0.0f }; // 終点の位置
点のデータは「オレオレ補助ライブラリ (Gg.h/Gg.cpp)」で定義しているクラス GgBuffer<T> を使って頂点バッファオブジェクト (VBO) を作ることにします. このクラスは T 型の頂点バッファオブジェクトを glGenBuffers() により一つ作ります. このクラスのメソッド load(target, n, data, usage) はこのバッファオブジェクトを glBindBuffer() で結合したあと, glBufferData() で target のバッファに data に格納されている n 個の頂点属性を送ります. usage は glBufferData() の第4引数で, 頂点バッファオブジェクトの使われ方を指定します. また data は glBufferData() の第3引数に与えるデータのポインタで, ここに 0 を指定したときは VBO のメモリは確保されますが, データの転送は行いません.
// 点データ
static class PointBuffer
{
// 頂点バッファオブジェクト
GgBuffer<GLfloat[4]> position;
public:
// デストラクタ
~PointBuffer(void) {}
// コンストラクタ
// n: 頂点数, pos: 頂点の位置
PointBuffer(unsigned int n)
{
// 頂点バッファオブジェクトのメモリを確保する
position.load(GL_ARRAY_BUFFER, n, 0, GL_DYNAMIC_COPY);
この load() メソッドが呼ばれた直後では glBindBuffer() が呼ばれ glBufferData() により VBO のメモリを確保した状態になっていますので, ここで glMapBuffer() を使って VBO をホストコンピュータのメインメモリにマップします. そして, これに対して点の位置を設定します. 値の設定が終わったら glUnmapBuffer() を呼び出して, VBO をメインメモリから分離します.
// 作成した頂点バッファオブジェクトに初期値を設定する
GLfloat (*p)[4] = (GLfloat (*)[4])glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
for (unsigned int i = 0; i < n; ++i)
{
GLfloat t = (GLfloat)i / (GLfloat)(pointn - 1);
p[i][0] = point0[0] * (1.0f - t) + point1[0] * t;
p[i][1] = point0[1] * (1.0f - t) + point1[1] * t;
p[i][2] = point0[2] * (1.0f - t) + point1[2] * t;
p[i][3] = 1.0f;
}
glUnmapBuffer(GL_ARRAY_BUFFER);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
また, この頂点バッファオブジェクトのデータを描画するメソッド draw(pvLoc, mode) を定義します. 引数の pvLoc はシェーダのプログラムオブジェクトから得られる attribute 変数の場所, mode は描画ずる図形プリミティブを指定します. また GgBuffer クラスのメソッド buf() は頂点バッファオブジェクトの名前 (番号), num() はデータ数 (頂点数) を返します.
// 描画
void draw(GLint pvLoc, GLenum mode)
{
glBindBuffer(GL_ARRAY_BUFFER, position.buf());
glEnableVertexAttribArray(pvLoc);
glVertexAttribPointer(pvLoc, 4, GL_FLOAT, GL_FALSE, 0, 0);
glDrawArrays(mode, 0, position.num());
glDisableVertexAttribArray(pvLoc);
}
} *pointBuffer = 0; // 頂点バッファオブジェクト
同様に, この図形を描画するためのシェーダを作ります. 点を描く空間はクリッピング空間 (-1≤x,y,z≤1) そのままにするので, バーテックスシェーダでは頂点の座標値の attribute 変数 position をそのまま gl_Position に代入します.
#version 150
in vec3 position;
void main(void)
{
gl_Position = vec4(position, 1.0);
}
フラグメントシェーダでは, 現在のフラグメントの色を uniform 変数 color にします. したがって color の値をそのまま gl_FragColor に代入します.
#version 150
uniform vec4 color;
out vec4 FragColor;
void main(void)
{
FragColor = color;
}
これらのシェーダのソースプログラムを読み込んで, シェーダのプログラムオブジェクトを作ります.「オレオレ補助ライブラリ (Gg.h/Gg.cpp)」で定義しているクラス GgShader を派生して作ります. このクラスではシェーダで使用する attribute 変数や uniform 変数の場所をメンバとして保持します.
// シェーダ
static class PointShader
: public GgShader
{
// attribute 変数の場所
GLint positionLoc;
// uniform 変数の場所
GLint colorLoc;
コンストラクタはシェーダのソースファイル名と, その中で使用している attribute 変数や uniform 変数名, およびフラグメントシェーダの出力変数名を引数にします. シェーダのソースファイル名を指定して基底クラス GgShader のコンストラクタを呼び出せば, ソースプログラムがコンパイルされてプログラムオブジェクトが作成されます. そして引数の変数名の場所を探して, メンバ変数を初期化します. フラグメントシェーダの出力変数名は glBindFragDataLocation() で指定します.
public:
// デストラクタ
~PointShader(void) {}
// コンストラクタ
PointShader(
const char *vert = "point.vert",
const char *frag = "point.frag",
const char *position = "position",
const char *color = "color",
const char *fragcolor = "FragColor"
)
: GgShader(vert, frag)
, positionLoc(glGetAttribLocation(getProgram(), position))
, colorLoc(glGetUniformLocation(getProgram(), color))
{
glBindFragDataLocation(getProgram(), 0, fragcolor);
}
このほかに, いくつかのインタフェースを用意しておきます. attribute 変数の場所は, 図形を描画する際に必要になります.
// 色を設定する
void setColor(GLfloat r, GLfloat g, GLfloat b, GLfloat a = 1.0f) const
{
glUniform4f(colorLoc, r, g, b, a);
}
// attribute 変数 pos の場所を得る
GLint getPositionLoc(void) const
{
return positionLoc;
}
} *pointShader = 0;
プログラムの起動時に, これらのオブジェクトを生成します. また終了時に削除するようにします.
// 後始末
static void cleanup(void)
{
// 頂点バッファオブジェクトを削除する
delete pointBuffer;
// プログラムオブジェクトを削除する
delete pointShader;
}
// 初期化処理
static void init(void)
{
// ゲームグラフィックス特論の都合にもとづく初期化
ggInit();
// 頂点バッファオブジェクトを作成する
pointBuffer = new PointBuffer(pointn);
// プログラムオブジェクトを作成する
pointShader = new PointShader();
// OpenGL の初期設定
glClearColor(1.0, 1.0, 1.0, 1.0);
// 後始末
atexit(cleanup);
}
そしてシェーダを指定して点を描画します. use() は PointShader の基底クラス GgShader から継承したメソッドで, glUseProgram() を呼んでシェーダプログラムを有効にします. また draw() の第1引数に頂点バッファオブジェクトのデータを渡す attribute 変数の場所, 第2引数に図形プリミティブを指定します. なお, このプログラムでは glPointSize(pointSize) がとても気色悪いところに入っているので, 気になるなら自分で draw() メソッドの定義の中とかに移動してください.
// 画面に図形を描画する
static void display(void)
{
// 画面クリア
glClear(GL_COLOR_BUFFER_BIT);
// 点の描画
pointShader->use();
pointShader->setColor(1.0f, 0.0f, 0.0f);
glPointSize(pointSize);
pointBuffer->draw(pointShader->getPositionLoc(), GL_POINTS);
// ダブルバッファリング
glutSwapBuffers();
}
draw() メソッドでは点 (GL_POINTS) と同じデータで折れ線 (GL_LINE_STRIP) を描くことができるので, 以下の2行を追加すれば点と点の間を線分で結ぶことができます.
// 画面に図形を描画する
static void display(void)
{
// 画面クリア
glClear(GL_COLOR_BUFFER_BIT);
// 点の描画
pointShader->use();
pointShader->setColor(0.0f, 0.0f, 0.0f);
pointBuffer->draw(pointShader->getPositionLoc(), GL_LINE_STRIP);
pointShader->setColor(1.0f, 0.0f, 0.0f);
glPointSize(pointSize);
pointBuffer->draw(pointShader->getPositionLoc(), GL_POINTS);
// ダブルバッファリング
glutSwapBuffers();
}
点の位置をマウスで動かす
この図形の点の位置をマウスでドラッグして動かせるようにします. そのために, 点データのクラス PointBuffer にマウスでクリックした位置に近い点を見つけるメソッドと, マウスのドラッグにともなって頂点バッファオブジェクトに格納されているその点の位置を変更するメソッドを追加します.
// 点データ
static class PointBuffer
{
// 頂点バッファオブジェクト
GgBuffer<GLfloat[4]> position;
public:
...
pick() メソッドは引数 (x, y) に近い点を探します. (x±dx, y±dy) にある点のうち, 最初に見つかったもののインデックスを返します. pbuf() は基底クラス GgPoints が保持する頂点バッファオブジェクトの名前 (番号) を返します. 見つからなければ -1 を返します. glMapBuffer() を使って頂点バッファオブジェクトをホストコンピュータのメインメモリにマップし, その個々の要素と (x, y) を比較します.
// バッファ中の点から (x, y) に近いものを探す
int pick(GLfloat x, GLfloat y, GLfloat dx, GLfloat dy) const
{
glBindBuffer(GL_ARRAY_BUFFER, position.buf());
GLfloat (*p)[4] = (GLfloat (*)[4])glMapBuffer(GL_ARRAY_BUFFER, GL_READ_ONLY);
for (unsigned int i = 0; i < position.num(); ++i)
{
if (fabs(p[i][0] - x) <= dx && fabs(p[i][1] - y) <= dy)
{
glUnmapBuffer(GL_ARRAY_BUFFER);
return i;
}
}
glUnmapBuffer(GL_ARRAY_BUFFER);
return -1;
}
move() メソッドは頂点のインデックスを指定して, その内容を書き換えます. これにも glMapBuffer() が使えますが, 一個しか書き換えないので, ここでは glBufferSubData() を使います.
// バッファ中の i 番目の点の位置を (x, y) に設定する
void move(int i, GLfloat x, GLfloat y) const
{
glBindBuffer(GL_ARRAY_BUFFER, position.buf());
GLfloat p[] = { x, y };
glBufferSubData(GL_ARRAY_BUFFER, sizeof (GLfloat[4]) * i, sizeof p, p);
}
} *pointBuffer = 0; // 頂点バッファオブジェクト
外部変数 cx, cy を用意し, それにウィンドウの中心位置を求めておきます.
// ウィンドウの中心位置
static GLfloat cx, cy;
// ウィンドウのオープン・リサイズ時の処理
static void resize(int w, int h)
{
// ウィンドウ全体をビューポート(表示領域)にする
glViewport(0, 0, w, h);
// ウィンドウの幅と高さを覚えておく
cx = w * 0.5f;
cy = h * 0.5f;
}
マウスでクリック・ドラッグする点の番号を格納する変数 hit を用意し, 初期値として何もドラッグしていないことを示す -1 を格納しておきます. また, ドラッグ中の図形の挙動をアニメーションするために, 何もすることがなかったときに実行する (すなわち glutIdleFunc() で指定する) 関数 idle() を宣言し, そこで画面の再描画を促す glutPostRedisplay() を呼び出すようにします.
// 押されたマウスボタン
static int pressed;
// ドラッグされている点
static int hit = -1;
// 何もすることがなくなった時の処理
static void idle(void)
{
// 画面の再描画を行う
glutPostRedisplay();
}
// マウスのボタンを押したときの処理
static void mouse(int button, int state, int x, int y)
{
// 押されたボタンを覚えておく
pressed = button;
そして, マウスの左ボタンを押したときには, 現在の画面上のマウスの位置から点の座標系 (クリッピング空間) の位置を求めます. 同時に, 点の大きさ pointSize をもとに, クリック位置を判定する領域の大きさ (dx, dy) を求めます. これらを引数に指定して pick() メソッドを呼び出し, 返り値を hit に格納します. また近い点があれば, glutIdleFunc(idle) を呼び出してアニメーションを開始します.
switch (button)
{
case GLUT_LEFT_BUTTON:
if (state == GLUT_DOWN)
{
// 左ボタンを押したときの処理
hit = pointBuffer->pick((GLfloat)x / cx - 1.0f, 1.0f - (GLfloat)y / cy, pointSize / cx, pointSize / cy);
if (hit >= 0) glutIdleFunc(idle);
}
マウスの左ボタンを離した時は, その位置をドラッグ中の点に設定します. そして hit に -1 を入れて点の選択を解除し, glutIdleFunc(0) を呼び出してアニメーションを停止します.
else
{
// 左ボタンを離したときの処理
pointBuffer->move(hit, (GLfloat)x / cx - 1.0f, 1.0f - (GLfloat)y / cy);
hit = -1;
glutIdleFunc(0);
}
break;
...
マウスのドラッグ中は, hit が 0 以上なら, move() メソッドを呼び出して, その頂点の位置を変更します.
// マウスのドラッグ中の処理
static void motion(int x, int y)
{
switch (pressed)
{
case GLUT_LEFT_BUTTON:
// 左ボタンでドラッグ中の処理
if (hit >= 0)
{
pointBuffer->move(hit, (GLfloat)x / cx - 1.0f, 1.0f - (GLfloat)y / cy);
}
break;
...
点をドラッグして移動できるでしょうか.
タイトル修正した.
後の都合により OpenGL 3.2 / GLSL 1.5 に書き換え.Mac OS Lion の GLUT 非対応.