多摩美 – メディア芸術演習 VI – メディア・アート II 2014
第3回: openFrameworks基本 – プログラムの構造をつくる、OOP入門
Githubでのサンプル配布
今回から講義内で使用するサンプルプログラムをGithubというWebサービスを使用して、配布していきます。
Githubの仕組みを理解するためには、まずGitというバージョン管理システムについて簡単に知っておく必要があります。
バージョン管理システムとは、コンピュータ上のファイルの変更履歴を管理するためのシステムで、主にプログラムのソースコードやテキストファイルなどの管理に用いられます。プログラミングをしていて、更新した内容がうまく作動せず、過去の特定の時点まで状態を戻す必要になる場合があります。そのために、毎回変更を加える前に手動でバックアップするのは面倒ですし、ファイル容量も無駄に使用してしまいます。現在のソフトウェア開発の現場では、バージョン管理システムを使用して効率的に作業履歴を管理、運用していく方法が主流となっています。
Gitはバージョン管理システムのひとつで、現在主流となっているシステムの一つです。その特徴は「分散型バージョン管理システム」という構造で、これは「リポジトリ」と呼ばれる更新の全履歴を含んだデータを複製することができて、複数の場所で分散して管理することが可能となっているものです。
まず、Gitについての基本的な概念と操作方法を、下記のサイトを参考にしながら解説します。
この講義で配布したサンプルは、毎週更新されていく予定です。ですので、それぞれの履修者は、自分のPC上に授業サンプルのリポジトリのクローンを作成し、毎週差分データをPullして更新していくようにしましょう。以下のような手順になります。
- Githubにアカウントを作成
- Githubのクライアントアプリをインストール(Mac / Win)
- Githubから、授業のリポジトリをクローン
- リポジトリを変更して自分のオリジナルのサンプルにする場合には、ブランチを作成
- 毎週の講義でサンプルの更新があった場合には、リポジトリをPullして差分を更新
前回の復習
では、早速Githubのリポジトリを参考にしながら、前回までの復習をしてきましょう。
まず、ランダムに静止した丸を画面上に描くプログラムを作成しました。乱数の生成 ofRandom() を ofApp::setup() 内で行うのがポイントです。また、位置を記録するにあたり、2次元のベクトルデータを扱うofVec2fを使用して、x座標とy座標をまとめて「位置ベクトル」として管理している点にも注意してください。
つぎに、この仕組みを応用して、一気に大量の静止する円を生成しました。ポイントは、位置ベクトルを保存するofVec2fを配列(Array)として用意して、値をロッカーに入れるようなイメージで次々と格納しているところです。
次にアニメーションについて考えます。位置をあらわすベクトルpositionに加えて、速度を記録するためのベクトルvelocityをofVec2fとして新規に準備します。velocityもまずsetup()内でランダムに値を生成して格納し、update()内で現在の位置に速度を加算します。そのことで次のフレームの位置が算出されます。あとは、新規に計算された位置に点を描くだけで、全ての点が独立してアニメーションします。
現在のフレームの位置ベクトル + 速度ベクトル = 次のフレームの位置ベクトル
position += velocity;
また、このサンプルでは画面の上下左右の端にくるとバウンドするような工夫をしています。点の位置をマイフレーム常に監視しておき、画面の上下もしくは左右の端にきた瞬間にvelocityに-1をかけ算しています。このことはちょうど力が反転することを意味します。つまりこの計算がバウンドを生みだしているのです。
if (position.x < 0 || position.x > ofGetWidth()) { velocity.x *= -1; } if (position.y < 0 || position.y > ofGetHeight()) { velocity.y *= -1; }
これで、壁にあたって跳ね返る動きが1つの点が作成できました。次にこの点を大量に複製してみましょう。考え型は、静止したランダムな場所の点と同様です。位置ベクトルの配列position[]に加えて、速度ベクトルの配列velocity[]を作成します。これをfor文をつかって点の数だけ演算していけば良いのです。
これで大量に動く点が作成できました。次にこれらの一つ一つの点に摩擦力(friction)が加わるようにしてみたいと思います。摩擦力とは例えば空気抵抗や接地面の抵抗などと考えてください。この摩擦力は常に速度ベクトルと反対の方向に働きます。図示すると以下のように考えられるでしょう。
この摩擦力を実装するにあたり、色々方法はあるのですが、速度ベクトルのかけ算と考えると実装が楽になります。例えば摩擦力が0.1だとすると、最初1だった速度ベクトルは0.9になります。さらに次のフレームでは0.9 x 0.9、その次には、0.9 x 0.9 x 0.9… というように指数的に減速していきます。
これらの力の関係をあらわすため、位置ベクトルpositon、速度ベクトルvelocityに加えて、力を計算するためのベクトルforceを導入します。この3つのofVec2fの配列を用いて摩擦力を加味した次のフレームの位置を算出します。
for (int i = 0; i < CIRCLE_NUM; i++) { // 力をリセット force[i].set(0, 0); // 摩擦力の計算 force[i] -= velocity[i] * friction; // 力から速度を算出 velocity[i] += force[i]; // 速度から位置を算出 position[i] += velocity[i]; ...(省略) }
今後のプログラムの拡張を見据えて、プログラムを役割ごとに整理していきましょう。現在のupdate()の部分をその内容によって、以下のように別々の関数にしていきます。
- setup()
- setInit() : 初期設定
- update()
- resetForce() : 力をリセット
- updateForce() : 力を更新
- updatePos() : 位置の更新
- checkBounds() : 画面からはみ出たらバウンドさせる
これを実装すると以下のようになります。
次に重力加えてみましょう。重力は常に一定方向にかかり続ける力と考えられます。摩擦力と違って重力は速度ベクトルの大きさに関わらず、つねに一定の力が掛り続けます。
この機能を実現するために、それぞれの点に力を加えるaddForce()という関数を実装します。これは、引数にofVec2fをとり、全ての点に対して指定した力のベクトルを加えるというものです。
void ofApp::addForce(ofVec2f _force){ // 力を加える for (int i = 0; i < CIRCLE_NUM; i++) { force[i] += _force; } }
この関数を使用して重力を加えた処理をします。今回は下向きに重力をかけています。update()内は以下のようになります。
void ofApp::update(){ resetForce(); // 力をリセット addForce(ofVec2f(0, 0.5)); // 重力を加える updateForce(); // 力の更新 (摩擦) updatePos(); // 円の座標を全て更新 // 画面からはみ出たらバウンドさせる checkBounds(0, 0, ofGetWidth(), ofGetHeight()); // 枠内に収める constrain(0, 0, ofGetWidth(), ofGetHeight()); }
基本の動きが決まれば、あとは数を膨大に増やしたり、マウスのクリックで再度繰り返すなどいろいろな工夫が可能となります。また、初期化の際の速度ベクトルの乱数の計算を工夫することで、きれいに円形に拡がるようにすることが可能です。角度と距離をランダムに生成して、三角関数を使用して座標にしています。
for (int i = 0; i < CIRCLE_NUM; i++) { position[i].x = initPos.x; position[i].y = initPos.y; float length = ofRandom(20); float angle = ofRandom(PI * 2); velocity[i].x = cos(angle) * length; velocity[i].y = sin(angle) * length; force[i].set(0, 0); }
ここまでで、以下の映像のような動きが作成できました。今回はさらにこうした動きについて理解を深めるとともに、プログラムの構造についても考えていきます。
openFrameworks examples for Tamabi ma2 from Atsushi Tadokoro on Vimeo.
プログラムを構造化する – オブジェクト指向プログラミング(OOP)
ここまでのプログラムは全てofApp.hとofApp.cppに変更を加えていきました。現在のような単一の機能のプロジェクトであれば、これでもあまり問題はありません。しかし、このプロジェクトに様々な機能を加えていこうとすると、徐々にofAppが肥大化していきプログラム全体の把握が徐々に困難になってきます。例えばエラーが起こった際も、ofAppのどの部分がおかしいのか不具合の発生箇所を切り出すことが難しく、また機能ごとの更新も手間取るようになってしまいます。
プロジェクトを機能ごとに分割して、より見通しの良い構造にしていきたいと思います。openFrameworksは、C++というプログラミング言語がベースになっています。C++ではその構造の根底に「オブジェクト指向プログラミング(Object Oriented Programming)」という考えかたがあります。
オブジェクト指向とは、プログラムを「オブジェクト(Object)」という小さなプログラムの集合として構成するプログラミングの手法です。このオブジェクトはそれぞれが自律しています。そして、お互いにメッセージを送受信して全体の機能を支えます。
次に、この一つ一つのオブジェクトに注目します。オブジェクトは、その挙動を2つのカテゴリーにわけて定義しています。一つはそのオブジェクトの「Property(属性)」です。Propertyは、例えば色、サイズ、形、数など定まった性質を定義します。もう一つは、オブジェクトの「Method(手続)」です。Methodは、一連の処理をまとめたものと考えてください。
- Property: 属性、状態、データ
- Method: 操作、手続、行為
では、先程の重力と摩擦力を適用したひとつひとつの点を、オブジェクトとして考えたらどうなるでしょう? Property(属性)とMethod(手続)という観点から例えば以下のように整理できないでしょうか。
オブジェクト名 | Perticle |
---|---|
Property | 位置 速度 力 摩擦係数 |
Method | 初期設定 力をリセット 力を加える 力を更新 位置の更新 バウンドさせる 枠内に収める |
PropetyとMethodの内容が、ここまでで作成してきたプログラムの構造と密接に関連していることに注目してください。よくみると、以下のような関連が見えてきます。
- Property: 変数
- ofVec2f position
- ofVec2f velocity
- ofVec2f force
- Method: 関数
- void setInit(ofVec2f initPos);
- void resetForce();
- void addForce(ofVec2f force);
- void updateForce();
- void updatePos();
- void checkBounds(float xmin, float ymin, float xmax, float ymax);
- void constrain(float xmin, float ymin, float xmax, float ymax);
つまり、オブジェクトとは変数と関数によって記述された、プログラムの構成単位と考えられます。
では、変数と関数を表に整理してみます。
Perticle |
---|
+ position : ofVec2f + velocity : ofVec2f + force : ofVec2f |
+ setInit(ofVec2f initPos) : void + setInit(ofVec2f initPos) : void + resetForce() : void + addForce(ofVec2f force) : void + updateForce() : void + checkBounds(float xmin, float ymin, float xmax, float ymax) : void + constrain(float xmin, float ymin, float xmax, float ymax) : void |
この設計図に従って、ひとつひとつの点をオブジェクトにしてみましょう。C++ではオブジェクトを生成するには、まずその設計図を書くことから始めます。このオブジェクトの設計図のことを「クラス(Class)」と呼びます。まずクラスを記述して、これを実体化(インスタンス化)することによって、オブジェクトが生まれます。金属のパーツを作るのに、まず金型を作成し、金型が完成した後で金属を流し込んで実際のパーツを生成するようなイメージです。一度金型(クラス)を生成してしまえば、何度でもくりかえしパーツを生成することが可能です。
クラスの記述は、ヘッダーファイル(.h)と実装ファイル(.cpp)にわけて記述します。この方法はofApp.hとofApp.cppを思い出すかもしれません。実は、ofAppもクラスです。ofAppクラスは、main.cppで実体化(インスタンス化)されています。
では、一つ一つの点をParticleクラスと名付けて、それぞれ「Particle.h」と「Particle.cpp」として記述してみます。クラスの記述には様々なルールがあります。実際のクラスを作成しながら、Xcodeでの操作方法、記述の細かなルールなどを確認していきます。
クラスを追加する
クラスは独立した新規のファイルとしてプロジェクトに追加します。Xcodeでは以下のような手順でファイルを追加します。
- ファイルのリストの「src」フォルダを右クリックして、リストから「New File…」を選択
- Mac OS X > C and C++ > C++ File を選択
- 「Particle」という名前で「src」フォルダに保存、各種設定は、そのままで
この手順でクラスがファイルとしてプロジェクトに追加されます。Xcodeの左側のコラムのファイルツリーは、以下のようになります。
このParticle.hとParticle.cppに、点の運動の1粒分を記述します。
openFrameworksでは、まず始めにmain.cppが実行されます。main.cppは、ofAppクラスをインスタンス化して実行します。ですので、ParticleクラスはofAppに読み込んでインスタンス化するという構造になっています。図示すると以下のような形になります。
この構造を踏まえて、ofApp.hとofApp.cppを作成してみましょう。
これで、先週の復習で作成したプログラムと同じ動きが、複数のクラスによって実現できました。では、この作成したParticleクラスを活用して、いろいろ表現してみましょう。
無限に増殖、全体数を変更可能にする
マウスをドラッグしている間は、パーティクルが増殖し続けるようにしてみたいと思います。ここで問題となるのは、ここまでのやり方ではまず始めにパーティクルの最大数(CIRCLE_NUM)が決められてしまっている点です。このため、後から想定した数よりも多くのパーティクルが必要になっても配列に格納することができません。
こうした際にとても便利な仕組みがあります。これまで使用してきた最大数が固定された配列はarrayと呼ばれます。それに対して、C++では必要に応じて要素の数を変更できる可変長の配列が存在します。可変長配列は何種類かあるのですが、ここでは、vectorという仕組みを使用してみます。vectorはarrayと違って、自動的に領域の拡張が行われます。
vectorの定義は以下のように行います。
vector< 型の種類> 変数名
例えば、Particleクラスをvectorをつかって可変長の配列にしたいのであれば、以下のようになります。
vector particles;
vectorを使用するには、まず対象となるクラスを個別にインスタンス化し、必要であればプロパティの初期値を設定した後で、push_back()というメソッドを用いてvectorの末尾にインスタンスを追加します。今回のParticleの例でいうと、以下のようにして新規に一粒ぶんを追加しています。
// 一時格納用にParticleのインスタンスpを生成 Particle p; // 摩擦係数を設定 p.friction = 0.01; // 重力は0に p.gravity.set(0, 0); // 初期位置を設定 p.setInit(ofVec2f(x, y)); // 初期速度を設定 float length = ofRandom(3.0); float angle = ofRandom(PI * 2); p.velocity.x = cos(angle) * length; p.velocity.y = sin(angle) * length; // Vectorに追加 particles.push_back(p);
push_back()の他にもvectorには様々な機能があります。代表的なものを以下にまとめます。
名前 | 説明 |
---|---|
push_back | 末尾へ要素追加 |
pop_back | 末尾から要素削除 |
insert | 要素の挿入 |
erase | 要素の削除 |
clear | 全要素削除 |
size | 要素の数 |
では、実際にプログラムしてみましょう。
このプログラムで、ドラッグし続けると無限にパーティクルが増殖するプログラムが完成しました。
曲線で結ぶ
では、全ての点を出現順に線で結んだらどうなるでしょう? 実際に試してみましょう。
たくさんの点を曲線で結ぶには、ofCurveVertex()を使用します。使用方法は以下のようにofBeginShape()とofEndShaper()で上下を囲んで、その中で頂点を指定していくという方法で全ての座標を指定します。
ofBeginShape(); ofCurveVertex(x0, y0); ofCurveVertex(x1, y1); ofCurveVertex(x2, y2); ... ofEndShape();
このプログラムを実行すると、全ての点が曲線で結ばれて、線が自動的に拡散していくような不思議な効果が生まれます。
応用: 高速化の工夫 – VBO Meshをつかう
ここまでで、大量の点を一気に描画することができるようになりました。この点が数千個単位の数でしたら問題ありません。しかし、点の数が数万、数十万と増えていくにつれ、徐々に描画が追い付かなくなりコマ落ちするようになってきます。
ここで高速化の工夫をしてみたいと思います。大量の点を高速に描くための方法はいろいろありますが、ここでは、VBO Meshというものを使用してみます。これは、「頂点バッファーオブジェクト(Vertex Buffer Object = VBO)」という方法を用いて、大量の頂点の座標メモリに保存して処理するのではなく、GPU(ビデオカード)のRAMに予めデータを置いておきPC本体から毎回データを転送しなくても良いようにするOpenGLの仕組みです。この方法により大量の頂点を使用するようなプログラムの描画の高速化が期待できます。
このプログラムでは下記のようなコードでofVboMeshという仕組みを用いて高速描画を実現しています。
// VBO Meshの作成 ofVboMesh mesh; // 頂点を点で描画 mesh.setMode(OF_PRIMITIVE_POINTS); // メッシュに格納していた頂点情報をクリア mesh.clear(); // パーティクルの位置をVBO Meshに頂点の座標として記録 for (int i = 0; i < particles.size(); i++) { mesh.addVertex(ofVec3f(particles[i].position.x, particles[i].position.y, 0)); } // メッシュを描画 mesh.draw();
マシンの性能にもよりますが、数万から数十万単位のパーティクルもコマ落ちすることなく描画可能です。
応用: 画面をフェードさせる
最後にちょっとしたエフェクトを追加してみましょう。setup()で以下の命令を実行すると、毎回の画面の更新が止まります。つまり、物体が移動した全ての軌跡が残ることになります。
//画面の更新をOFFに ofSetBackgroundAuto(false);
これに加えて、draw()で描画する前に、画面全体を半透明の四角形で塗り潰します。これによって、徐々にフェードアウトするような効果が生まれます。四角形の透明度を調整することでフェードアウトする時間が調整することが可能です。
// 画面をフェード ofSetColor(0, 0, 0, 15); ofRect(0, 0, ofGetWidth(), ofGetHeight());
ofVboMesh particle test from Atsushi Tadokoro on Vimeo.