ここではインタラクティブアートへの応用をめざして,2次元物理シミュレータである ofxBox2d の概要を紹介します.また,ofxOpenNI と ofxOpenCv を使った Kinect との連携についても扱います.
注意:準備作業が抜けている人をよく見かけます.Xcode を正しくインストールしてください.Apple の Developer サイトから無料で入手できます.openFrameworks は最新版をダウンロードしてください.ここでは Xcode 8.1 + openFrameworks 0.9.8 の利用を想定しています.
ofxBox2d は,多数の平面図形(丸・四角など)の相互作用をシミュレートするアドオンです.たとえば下図の例では,マウスポインタの位置に丸や四角といった図形を次々と生成し,仮想的な重力によって地面(画面下端)に落下・堆積させています.2次元平面(画面)上に限られますが,図形に質量(密度)を与え,重力やその他の外力を加えることで,動かすことができます.また,そうして動かされた図形たちは互いに衝突や摩擦など力を及ぼしあい,興味深い動きを見せてくれます.
ofxBox2d を使ったアプリケーションを開発するには,OF_PATH > projectGenerator_osx > projectGenerator.app を使います.これを起動し,プロジェクトの名前(たとえば box2dExample0)を決め,必要なアドオン(ofxBox2d)にチェックを入れ,最後に "GENERATE PROJECT" のボタンを押します.すると OF_PATH > apps > myApps の中に新しいプロジェクトのフォルダが用意され,その中に必要なファイルが生成されます.
ofApp.h には,世界(world2d)と,そこに登場するボール(ofxBox2dCircle)をそのポインタのベクタとして用意します.後者は ofBox2dCircle へのポインタ型 ofPtr のベクタとなることに注意してください.また,"<ofPtr<ofxBox2dCircle> >" の "> >" の部分に必ずスペースを入れるようにしてください.
#pragma once #include "ofMain.h" #include "ofxBox2d.h" class ofApp : public ofBaseApp { private: ofxBox2d world2d; vector <ofPtr<ofxBox2dCircle> > circles; public: (... 変更なし ...) };
ofApp::setup() では,世界(world2d)を初期化します.状態更新スピードを標準的な 60 回/秒に設定していますが,これは環境に応じて決めてください.ofApp::update() で世界の状態を更新したあと,ofApp::draw() で各ボールを描画しています.
#include "ofApp.h" void ofApp::setup() { // window ofBackground(0, 0, 0); ofSetWindowShape(640, 480); // box2d world2d.init(); world2d.setFPS(60.0); } void ofApp::update() { world2d.update(); } void ofApp::draw() { // draw each circle ofSetColor(255, 150, 150); for (int i = 0; i < circles.size(); i++) circles[i].get()->draw(); } void ofApp::keyPressed(int key) { // create a new circle ofPtr<ofxBox2dCircle> circle = ofPtr<ofxBox2dCircle>(new ofxBox2dCircle); // set attributes to this circle (density, bounce, friction) circle.get()->setPhysics(1.0, 0.5, 0.1); circle.get()->setup(world2d.getWorld(), mouseX, mouseY, ofRandom(10, 20)); // add this circle to "circles" vector circles.push_back(circle); } (... 以下変更なし ...)
この世界(world2d)に新しいボール(ofBox2dCircle)を生み出すコードが,ofApp::keyPressed() の中に埋め込まれています.ofxBox2dCircle オブジェクトを new で生成し,物理パラメタ(密度・弾性係数など)や所属する世界(ここでは world2d)をセットします.最後に,そのポインタ(circle)を circles ベクタに追加します.これで,いずれかのキーを押せば,マウスポインタの位置(mouseX, mouseY)にランダムな半径(10〜20)をもつボールを生成します.
ofxBox2d では,下向き 5 の重力加速度がデフォルトになっています.生成されたボールは自由落下しながら,画面下方へと消えていきます.
上の例では,自由落下するボールは,やがて画面下方へと消えていってしまいます.これでは面白くないので,画面全体を「箱」のようにして,その中にボールがとどまるように ofApp::setup() を変更してみます.画面の大きさ一杯の「箱」の中に,生成したボールがとどまるようになります.力学的に安定したボールは色が薄くなります.
void ofApp::setup() {
// window
ofBackground(0, 0, 0);
ofSetWindowShape(640, 480);
// box2d
world2d.init();
world2d.setFPS(60.0);
// rectangular bound
world2d.createBounds(0, 0, 640, 480);
}
つぎに,箱の中に溜まっているボールをマウスポインタで動かせるように改造します.これも ofApp::setup() に1行追加するだけです.ボールを1つマウスポインタで捕獲し,それを動かしたり,あるいは投げ上げるなどしてみてください.
void ofApp::setup() {
// window
ofBackground(0, 0, 0);
ofSetWindowShape(640, 480);
// box2d
world2d.init();
world2d.setFPS(60.0);
world2d.registerGrabbing();
// rectangular bound
world2d.createBounds(0, 0, 640, 480);
}
箱が世界では面白くないので,地平をつくり,その両端からボールが落ちていくようにしましょう.createBounds() の代わりに createGround() を使います.画面座標で (100, 400) から (540, 400) まで地平を引くと,生成したボールは地平上に溜まったあと,左右から落ちていきます.
#pragma once
#include "ofMain.h"
#include "ofxBox2d.h"
class ofApp : public ofBaseApp {
private:
ofxBox2d world2d;
ofPolyline groundLine;
vector <ofPtr<ofxBox2dCircle> > circles;
public:
(... 変更なし ...)
};
void ofApp::setup() { // window ofBackground(0, 0, 0); ofSetWindowShape(640, 480); // box2d world2d.init(); world2d.setFPS(60.0); world2d.registerGrabbing(); // ground world2d.createGround(100, 400, 540, 400); groundLine.addVertex(100, 400); groundLine.addVertex(540, 400); } void ofApp::draw() { // draw the ground ofSetColor(255, 255, 255); groundLine.draw(); // draw each circle ofSetColor(255, 150, 150); for (int i = 0; i < circles.size(); i++) circles[i].get()->draw(); }
ofPolyline は経由点を登録していくことで,折れ線や曲線を描くことができます.ここでは,始点と終点のみからなる線分を描くことに使っています.
ボールだけでなく四角もつくれます.ofxBox2dRect がそれです.ボールと同じように,ofApp.h にポインタのベクタとして用意します.次にように書き加えてください.キーボードの 'c' を押すとボールが,'b' を押すと四角が生成されるようにします.
#pragma once
#include "ofMain.h"
#include "ofxBox2d.h"
class ofApp : public ofBaseApp {
private:
ofxBox2d world2d;
ofPolyline groundLine;
vector <ofPtr<ofxBox2dCircle> > circles;
vector <ofPtr<ofxBox2dRect> > boxes;
public:
(... 変更なし ...)
};
(... 変更なし ...) void ofApp::draw() { // draw the ground ofSetColor(255, 255, 255); groundLine.draw(); // draw each circle ofSetColor(255, 150, 150); for (int i = 0; i < circles.size(); i++) circles[i].get()->draw(); // draw each box ofSetColor(150, 255, 150); for (int i = 0; i < boxes.size(); i++) boxes[i].get()->draw(); } void ofApp::keyPressed(int key) { if (key == 'c') { // create a new circle ofPtr<ofxBox2dCircle> circle = ofPtr<ofxBox2dCircle>(new ofxBox2dCircle); // set attributes to this circle (density, bounce, friction) circle.get()->setPhysics(1.0, 0.5, 0.1); circle.get()->setup(world2d.getWorld(), mouseX, mouseY, ofRandom(10, 20)); // add this circle to "circles" vector circles.push_back(circle); } else if (key == 'b') { // create a new box ofPtr<ofxBox2dRect> box = ofPtr<ofxBox2dRect>(new ofxBox2dRect); // set attributes to this box (density, bounce, friction) box.get()->setPhysics(1.0, 0.5, 0.1); box.get()->setup(world2d.getWorld(), mouseX, mouseY, ofRandom(20, 40), ofRandom(20, 40)); // add this circle to "circles" vector boxes.push_back(box); } } (... 変更なし ...)
四角だけでなく,任意の凸多角形も生成可能です.次のようにプログラムを変更することで,'p' を押したときに正六角形のポリゴン(ofxBox2dPolygon)を生成するようになります.ポリゴンの作り方は ofPolyline と同じです.最後に close() を呼び出すことで折れ線を閉じ,多角形にしています.
#pragma once
#include "ofMain.h"
#include "ofxBox2d.h"
class ofApp : public ofBaseApp {
private:
ofxBox2d world2d;
ofPolyline groundLine;
vector <ofPtr<ofxBox2dCircle> > circles;
vector <ofPtr<ofxBox2dRect> > boxes;
vector <ofPtr<ofxBox2dPolygon> > polies;
public:
(... 変更なし ...)
};
(... 変更なし ...) void ofApp::draw() { (... 変更なし ...) // draw each polygon ofSetColor(150, 150, 255); for (int i = 0; i < polies.size(); i++) polies[i].get()->draw(); } void ofApp::keyPressed(int key) { if (key == 'c') { (... 変更なし ...) } else if (key == 'b') { (... 変更なし ...) } else if (key == 'p') { // create a new polygon (hexagon) ofPtr<ofxBox2dPolygon> poly = ofPtr<ofxBox2dPolygon>(new ofxBox2dPolygon); float r = ofRandom(10, 20); float rsin30 = r * sin(M_PI / 6.0), rcos30 = r * cos(M_PI / 6.0); poly.get()->addVertex(mouseX - rsin30, mouseY - rcos30); poly.get()->addVertex(mouseX - r, mouseY); poly.get()->addVertex(mouseX - rsin30, mouseY + rcos30); poly.get()->addVertex(mouseX + rsin30, mouseY + rcos30); poly.get()->addVertex(mouseX + r, mouseY); poly.get()->addVertex(mouseX + rsin30, mouseY - rcos30); poly.get()->close(); poly.get()->setPhysics(1.0, 0.5, 0.1); poly.get()->create(world2d.getWorld()); polies.push_back(poly); } } (... 変更なし ...)
地平のように,世界に対して静止した物体を置くことができます.ここではコップを作ってみましょう.静止した物体の生成方法は,ボール・四角・ポリゴンなどと同様です.ただし密度を 0(または負数)にします.これで質量が無限大の物体となります.
#pragma once #include "ofMain.h" #include "ofxBox2d.h" class ofApp : public ofBaseApp { private: ofxBox2d world2d; ofPolyline groundLine, cupLine; vector <ofPtr<ofxBox2dCircle> > circles; vector <ofPtr<ofxBox2dRect> > boxes; vector <ofPtr<ofxBox2dPolygon> > polies; ofPtr<ofxBox2dPolygon> cup; public: (... 変更なし ...) };
void ofApp::setup() { (... 変更なし ...) // ground world2d.createGround(100, 400, 540, 400); groundLine.addVertex(100, 400); groundLine.addVertex(540, 400); // make a cup cupLine.addVertex(200, 200); cupLine.addVertex(210, 200); cupLine.addVertex(210, 390); cupLine.addVertex(430, 390); cupLine.addVertex(430, 200); cupLine.addVertex(440, 200); cupLine.addVertex(440, 400); cupLine.addVertex(200, 400); cupLine.close(); cup = ofPtr<ofxBox2dPolygon>(new ofxBox2dPolygon); cup.get()->addVertexes(cupLine); cup.get()->triangulatePoly(10); cup.get()->setPhysics(0.0, 0.5, 0.1); cup.get()->create(world2d.getWorld()); } void ofApp::draw() { // draw the ground and the cup ofSetColor(255, 255, 255); groundLine.draw(); cupLine.draw(); // draw each circle ofSetColor(255, 150, 150); for (int i = 0; i < circles.size(); i++) circles[i].get()->draw(); (... 変更なし ...) }
この例では,まず ofPolyline で閉じた輪郭 cupLine を描き,それを addVertexes()(vertex の複数形のつもり;本当は vertices だけど)でポリゴンを生成しています.setPhysics() で密度を 0 に設定していることに注意してください.
単体としてのポリゴンは凸多角形(凹みのない多角形)に限られますが,コップは凹多角形です.凹多角形を世界に置くには,複数の三角形(最もシンプルな凸多角形)に分割する必要があります.triangulatePoly() がそれを行うメソッドです.引数として与えている 10 は,長さ 10 を単位として三角形に分割することを意味します.およそ,ポリゴンの一番短い辺の長さとするとよいでしょう.三角形への分割を可視化したい場合は,つぎのように ofApp::draw() を変更してみてください.
void ofApp::draw() {
// draw the ground and the cup
ofSetColor(255, 255, 255);
groundLine.draw();
vector <TriangleShape> tries = cup.get()->triangles;
for (int i = 0; i < tries.size(); i++) {
ofSetColor((i * 64) % 256 + 63);
ofTriangle(tries[i].a, tries[i].b, tries[i].c);
}
// draw each circle
(... 変更なし ...)
}
ボールやポリゴンではなく「雪」を降らせ,地面に積もらせることを考えます.内部的にはボールを使いますが,描画時はボールの代わりに雪の画像を表示します.
ここで扱う雪の画像は,ガウシアン分布で不透明度を与えた白とします.中心は完全な白で,周辺に行くにつれて透明度が上がるという 200 ピクセル四方の画像です.黒い背景にこの画像を描くと,下図のようになります.
上で扱った例(ボールと地平)をベースとして,次のように新しいプログラム(たとえば box2dExample1)を作ります.画面下端が地平です.ボールを描くべき位置に,上の画像を描きます.snow.png をダウンロードして,bin/data の中に入れておいてください.
#pragma once
#include "ofMain.h"
#include "ofxBox2d.h"
class ofApp : public ofBaseApp {
private:
ofxBox2d world2d;
vector <ofPtr<ofxBox2dCircle> > circles;
ofImage snow;
void makeSnow(float x, float y, float size);
public:
(... 変更なし ...)
};
#include "ofApp.h" void ofApp::setup() { // window ofBackground(0, 0, 0); ofSetWindowShape(640, 480); // box2d world2d.init(); world2d.setFPS(60.0); world2d.setGravity(0, 1); // snow snow.loadImage("snow.png"); snow.setAnchorPercent(0.5, 0.5); // ground (invisible) world2d.createGround(0, 480, 640, 480); } void ofApp::makeSnow(float x, float y, float size) { // create a new circle ofPtr<ofxBox2dCircle> circle = ofPtr<ofxBox2dCircle>(new ofxBox2dCircle); // set attributes to this circle (density, bounce, friction) circle.get()->setPhysics(1.0, 0.5, 0.1); circle.get()->setup(world2d.getWorld(), x, y, size); // add this circle to "circles" vector circles.push_back(circle); } void ofApp::update() { if (ofRandom(0, 100) < 40) makeSnow(ofRandom(-40, 680), -40, ofRandom(5, 10)); world2d.update(); } void ofApp::draw() { // draw each circle ofSetColor(255, 255, 255); for (int i = 0; i < circles.size(); i++) { ofPoint pos = circles[i].get()->getPosition(); float size = circles[i].get()->getRadius() * 10.0; snow.draw(pos, size, size); } } (... 変更なし ...)
上のプログラムで,ofApp::makeSnow() は,位置と大きさを与えられてボールを生成するメソッドです.ofApp::update() から 40% の確率で ofApp::makeSnow() を呼び出し,雪を新しく生成しています.60 fps を想定しているので,毎秒 24 個前後の雪が生成されることになります.ofApp::setup() では,重力を通常の 1/5 に設定しています.雪をゆっくりと降らせるためです.また ofApp::draw() では,雪の画像(snow.png)はボールの大きさに比べて 10 倍に拡大して描画しています.これは隣り合った雪どうしの半透明の部分を重ね合わせるためです.
個々の雪粒に少しだけ個性を持たせましょう.ofApp::makeSnow() で雪を生成するとき,わずかな初速度をランダムに与えます.つぎのように変更してください.静止画ではあまり変化はわかりませんが,動きは少しリアルになったと思います.
void ofApp::makeSnow(float x, float y, float size) {
// create a new circle
ofPtr<ofxBox2dCircle> circle = ofPtr<ofxBox2dCircle>(new ofxBox2dCircle);
// set attributes to this circle (density, bounce, friction)
circle.get()->setPhysics(1.0, 0.4, 0.0);
circle.get()->setup(world2d.getWorld(), x, y, size);
circle.get()->setVelocity(ofRandom(-1.0, 1.0), ofRandom(-1.0, 1.0));
// add this circle to "circles" vector
circles.push_back(circle);
}
際限なく雪を生成していくとどうなるでしょうか.ここでは世界に存在する雪(=ボール)の数と,毎秒のフレーム数(fps: frames per second)を画面左上に表示するようにして,どこまで雪を増やせるか試してみます.
void ofApp::draw() {
// draw each snow
ofSetColor(255, 255, 255);
for (int i = 0; i < circles.size(); i++) {
ofPoint pos = circles[i].get()->getPosition();
float size = circles[i].get()->getRadius() * 10.0;
snow.draw(pos, size, size);
}
// draw the number of circles and fps
char buf[100];
sprintf(buf, "%ld circles", circles.size());
ofDrawBitmapString(buf, 20, 20);
sprintf(buf, "%5.2f fps", ofGetFrameRate());
ofDrawBitmapString(buf, 20, 40);
}
つぎつぎと雪が生成され,地平にたまり,その両端からこぼれ落ちていきます.fps は ofApp::setup() で 60 に設定してあり,実際の動作も 60 弱程度で安定していると思います.しかし,生成されたボールの数が多くなるにつれて,fps は徐々に下がっていきます.試してみてください.(実際の fps 値はパソコンの処理能力に依存します.)
なぜ fps が下がるのかというと,生成されたすべての雪が画面の外に落ちていった後も,世界の中に存在しつづけるからです.これを回避するには,画面の外に出ていった雪を世界から消していくしかありません.次のように,objectKiller() をつくり,ofApp::update() に仕込んでください.
bool objectKiller(ofPtr<ofxBox2dBaseShape> shape) { float yPos = shape.get()->getPosition().y; return (yPos >= 500); } void ofApp::update() { // make a new snow if (ofRandom(0, 100) < 40) makeSnow(ofRandom(-40, 680), -40, ofRandom(5, 10)); // box2d (remove spelt snows, and update) ofRemove(circles, objectKiller); world2d.update(); }
objectKiller() は,世界に存在するオブジェクト(ボール・四角・ポリゴンなどの親クラスである ofxBox2dBaseShape)を消すか消さないかを決める関数です.ここでは,Y 座標が 500 を超えた場合(つまり画面下端から少し下へ落ちていった場合)にそのオブジェクトを消す判断をしています.true を返すと「消す」,false なら「消さない」という意味です.ofApp::update() の中の ofRemove() によって,この関数をベクタの各要素(ボールなど)に適用し,true となった要素を消去しています.
これでボールを生成しつづけても,画面の外に出たものは消去されるため,ボールの総数は 400個程度に抑えられます.fps も 60 程度で安定して動作しつづけることができます.(実際の fps 値などはパソコンの処理能力に依存します.)
最後に背景を少しだけ藍色にしてみます.ofApp::setup() の中の ofBackground(0, 0, 0) を ofBackground(80, 80, 120) に変更します.眺めているだけでも楽しめる雪の情景となります.
雪が降り積もるなかで,その雪と遊べると楽しそうです.ここでは Kinect を使って人影を切り出し,雪とを相互作用させることを試みます.
ここからの解説は,諸般の事情で Mac のみに対応した新しい内容にしてあります.ご注意ください.
Kinect を使って人の輪郭や画像を切り出すことは,すでに「openFrameworks 応用編(3)」の後半で扱いました.この機能を,上で扱った雪の世界に組み込んでみます.ofxOpenNI だけでなく,ofxOpenCv による輪郭抽出の機能も使います.ofxBox2d, ofxOpenNI, ofxOpenCv を組み込んだプロジェクトの生成手順は次のとおりです.
- projectGenerator.app を起動し,プロジェクト名(box2dExample2)を新しく設定し,ofxBox2d と ofxOpenCv のアドオンを有効にして,プロジェクトファイル一式を生成("GENERATE PROJECT")します.
- すでに作成した ofxOpenNI プロジェクト(kinectExampleN など)の bin/data の中にある openni フォルダを,上で作成した box2dExample2 の bin/data の中にコピーします.既存の ofxOpenNI プロジェクトがない場合は「openFrameworks 応用編(3)」を参照して作業してください.
- Xcode で box2dExample2.xcodeproj を開き,ofxOpenNI をアドオンとしてプロジェクトに登録します.まず,左上のフォルダ型のアイコンをクリックし,kinectExample(ブルーのアイコン)の左側にある三角をクリックします.つぎに,その下に現れた addons を右クリックして「Add Files to "kinectExample"...」を選び,OF_PATH > addons > ofxOpenNI のフォルダを選択して "OK" を押します.これで Xcode ウィンドウ左側の addons の下に ofxOpenNI というフォルダが入るはずです.
準備ができたので,上で扱った box2dExample1 をベースに,次のようにプログラムしていきます.まずは背景にモノクロの人影を表示するようにします.minDist と maxDist には,人物と背景を区別できるように,適当な数値(単位は mm)を与えてください.
#pragma once #include "ofMain.h" #include "ofxBox2d.h" #include "ofxOpenNI.h" #include "ofxOpenCv.h" class ofApp : public ofBaseApp { private: // box2d ofxBox2d world2d; vector <ofPtr<ofxBox2dCircle> > circles; ofImage snow; void makeSnow(float x, float y, float size); // kinect + OpenCv ofxOpenNI kinect; unsigned int minDist, maxDist; ofxCvGrayscaleImage shadowImage, invertImage; public: (... 変更なし ...) };
#include "ofApp.h" void ofApp::setup() { // window ofBackground(80, 80, 120); ofSetWindowShape(640, 480); // box2d world2d.init(); world2d.setFPS(60.0); world2d.setGravity(0, 2); // snow snow.loadImage("snow.png"); snow.setAnchorPercent(0.5, 0.5); // ground (invisible) world2d.createGround(0, 480, 640, 480); // setup OpenNI (kinect) kinect.setup(); kinect.setRegister(true); kinect.setMirror(true); kinect.addDepthGenerator(); kinect.start(); minDist = 500; maxDist = 1500; // sensing range of distance [mm] // allocate OpenCv images shadowImage.allocate(640, 480); invertImage.allocate(640, 480); } void ofApp::makeSnow(float x, float y, float size) { // create a new circle ofPtr<ofxBox2dCircle> circle = ofPtr<ofxBox2dCircle>(new ofxBox2dCircle); // set attributes to this circle (density, bounce, friction) circle.get()->setPhysics(1.0, 0.5, 0.1); circle.get()->setup(world2d.getWorld(), x, y, size); circle.get()->setVelocity(ofRandom(-1.0, 1.0), ofRandom(-1.0, 1.0)); // add this circle to "circles" vector circles.push_back(circle); } bool objectKiller(ofPtr<ofxBox2dBaseShape> shape) { float yPos = shape.get()->getPosition().y; return (yPos >= 500); } void ofApp::update() { // shadow (OpenNI/OpenCv) kinect.update(); unsigned short *depthData = kinect.getDepthRawPixels().getData(); unsigned char *shadowData = shadowImage.getData(); for (int k = 0; k < 640*480; k++) shadowData[k] = (minDist < depthData[k] && depthData[k] < maxDist)? 255: 0; shadowImage.flagImageChanged(); // must call after pixel manipulation invertImage = shadowImage; invertImage.invert(); // make a new snow if (ofRandom(0, 100) < 40) makeSnow(ofRandom(-40, 680), -40, ofRandom(5, 10)); // box2d (remove spelt circles, and update) ofRemove(circles, objectKiller); // update the world world2d.update(); } void ofApp::draw() { // draw shadow/background ofSetColor(80, 80, 120); invertImage.draw(0, 0, 640, 480); // draw each circle ofSetColor(255, 255, 255); for (int i = 0; i < circles.size(); i++) { ofPoint pos = circles[i].get()->getPosition(); float size = circles[i].get()->getRadius() * 10.0; snow.draw(pos, size, size); } // draw the number of circles and fps ofSetColor(255, 100, 100); char buf[100]; sprintf(buf, "%ld circles", circles.size()); ofDrawBitmapString(buf, 20, 20); sprintf(buf, "%5.2f fps", ofGetFrameRate()); ofDrawBitmapString(buf, 20, 40); } (... 変更なし ...)
新しく導入したのは,Kinect の距離画像からの切り出しと,得られたシルエット(人影)を OpenCv のグレースケール画像(中身は白黒の2値)である shadowImage に格納し,さらに白黒を反転した invertImage を生成しているところです.ofApp::draw() では 反転画像(つまり人影が黒でそれ以外が白の画像)に紺色をつけて,雪景色の背景として描画しています.
Kinect で捉えた人影から輪郭(contour)を抽出します.ofxOpenCv の一部である ofxCvContourFinder を使います.ofApp.h と ofApp.cpp に以下の部分を書き加えてください.
#pragma once
#include "ofMain.h"
#include "ofxBox2d.h"
#include "ofxOpenNI.h"
#include "ofxOpenCv.h"
class ofApp : public ofBaseApp{
private:
(... 変更なし ...)
// kinect + OpenCv
ofxOpenNI kinect;
float minDist, maxDist;
ofxCvGrayscaleImage shadowImage, invertImage;
ofxCvContourFinder finder;
public:
(... 変更なし ...)
};
ofApp::update() の中にある finder.findContours() は,2値化されたグレースケール画像について,最小領域 1000 ピクセルで最大領域は画面の 1/4 となる連続領域(blob)から最大のものを 4 つまで取り出すというものです.最後の引数(false)は,blob の穴を解析するかどうかのフラグで,ここでは穴の解析は無効化しています.
(... 変更なし ...) void ofApp::update() { // shadow (OpenNI/OpenCv) (... 変更なし ...) invertImage = shadowImage; invertImage.invert(); // find contours finder.findContours(shadowImage, 1000, 640*480/4, 4, false); // make a new snow (... 変更なし ...) } void ofApp::draw() { // draw shadow/background ofSetColor(60, 60, 100); invertImage.draw(0, 0, 640, 480); // draw contours finder.draw(0, 0, 640, 480); // draw each circle (... 変更なし ...) } (... 変更なし ...)
このプログラムを実行すると,シルエット画像に水色の輪郭が描かれ,さらにシルエットを囲む長方形が赤色で描かれます.シルエットが2つあれば,それぞれに輪郭と長方形が描かれます.
さらに,Polyline を使って,この輪郭を好きな色,好きな太さで描いてみましょう.次のようにプログラムを変更します.
#pragma once #include "ofMain.h" #include "ofxBox2d.h" #include "ofxOpenNI.h" #include "ofxOpenCv.h" #define MAX_BLOBS 4 class ofApp : public ofBaseApp{ private: (... 変更なし ...) ofxCvContourFinder finder; int numShadows; ofPolyline shadowLines[MAX_BLOBS]; public: (... 変更なし ...) };
void ofApp::update() { (... 変更なし ...) // find contours finder.findContours(shadowImage, 1000, 640*480/4, MAX_BLOBS, false); numShadows = finder.nBlobs; for (int j = 0; j < MAX_BLOBS; j++) { shadowLines[j].clear(); if (j < numShadows) { // make a contour for (int k = 0; k < finder.blobs[j].pts.size(); k++) shadowLines[j].addVertex(finder.blobs[j].pts[k]); shadowLines[j].close(); shadowLines[j].simplify(); } } // make a new snow (... 変更なし ...) } void ofApp::draw() { // draw shadow/background ofSetColor(80, 80, 120); invertImage.draw(0, 0, 640, 480); // draw contours ofSetLineWidth(3); ofSetColor(140, 140, 200); for (int j = 0; j < numShadows; j++) shadowLines[j].draw(); // draw each circle (... 変更なし ...) }
このプログラムを実行すると,最大4つまでのシルエットについて,太さ3ピクセルの輪郭線を描いています.まだ描画しているだけで,雪との相互作用はありません.
最後に,シルエットと雪との相互作用を実現します.そのためには,上でコップをつくったように,シルエットごとにポリゴンに変換し,さらに三角形に分割することが必要となります.次のようにプログラムを変更してください.
#pragma once
#include "ofMain.h"
#include "ofxBox2d.h"
#include "ofxOpenNI.h"
#include "ofxOpenCv.h"
#define MAX_BLOBS 4
class ofApp : public ofBaseApp{
private:
(... 変更なし ...)
ofxCvContourFinder finder;
int numShadows;
ofPolyline shadowLines[MAX_BLOBS];
ofPtr<ofxBox2dPolygon> shadows[MAX_BLOBS];
public:
(... 変更なし ...)
};
void ofApp::setup() { (... 変更なし ...) // allocate OpenCv images shadowImage.allocate(640, 480); invertImage.allocate(640, 480); // shadow of a person for (int i = 0; i < MAX_BLOBS; i++) shadows[i] = ofPtr<ofxBox2dPolygon>(new ofxBox2dPolygon); } void ofApp::update() { (... 変更なし ...) // find contour finder.findContours(shadowImage, 1000, 640*480/4, MAX_BLOBS, false); numShadows = finder.nBlobs; for (int j = 0; j < MAX_BLOBS; j++) { shadowLines[j].clear(); shadows[j].get()->clear(); if (j < numShadows) { // make a contour for (int k = 0; k < finder.blobs[j].pts.size(); k++) shadowLines[j].addVertex(finder.blobs[j].pts[k]); shadowLines[j].close(); shadowLines[j].simplify(); // make a polygon shadows[j].get()->clear(); shadows[j].get()->addVertexes(shadowLines[j]); shadows[j].get()->triangulatePoly(30); shadows[j].get()->setPhysics(1.0, 0.5, 0.1); shadows[j].get()->create(world2d.getWorld()); } } // make a new snow (... 変更なし ...) }
これで,下の実行例のように,シルエットと雪との相互作用が可能となりました.ただし,いくつかの雪粒がシルエットの内部に見られます.シルエット内部を分割した三角形のスキマに雪粒が入ってしまったようです.
シルエット内部の雪粒を消すには,つぎのようにプログラムを変更します.各雪粒について,シルエット内部にある場合はそれを消去するというものです.この機能を,objectKiller の処理と一体化して,効率的な雪粒消去を行うようにします.
(... objectKiller() を削除 ...)
void ofApp::update() {
(... 変更なし ...)
// box2d (remove spelt circles, and update)
for (int i = 0; i < circles.size(); i++) {
ofPoint pos = circles[i].get()->getPosition();
if (pos.y > 500) {
circles[i].get()->destroy();
circles.erase(circles.begin() + i);
}
else {
for (int j = 0; j < MAX_BLOBS; j++) {
if (shadowLines[j].inside(pos)) {
circles[i].get()->destroy();
circles.erase(circles.begin() + i);
}
}
}
}
// update the world
world2d.update();
}
これでシルエット内部に雪粒が入り込むことはなくなりました.シルエットを素早く動かすと,雪は掻き分けられる代わりに,融けて消えるようになります.そっと動かせば雪は融けません.
プロジェクタでスクリーンに投影する場合など,フルスクリーン化する必要性が出てくる場合があります.openFrameworks では ofToggleFullscreen() を呼び出すたびにフルスクリーン表示と通常表示(ofSetWindowShape() で設定したウィンドウでの表示)を行き来することができます.'f' キーを押して切り替えるようにしましょう.
void ofApp::keyPressed(int key) {
if (key == 'f') {
ofToggleFullscreen();
}
}
'f' を押すと,フルスクリーン表示にはなりますが,下図のように,描画される諸要素は元のスケールのままです.
正しいスケールでフルスクリーン表示するために,次のように scale や offsetX, offsetY といった変数を用意し,ofApp::draw() の先頭部分で,ofScale() による描画スケールと ofTranslete() によるオフセット(描画位置)の設定を行うようにします.
#pragma once
#include "ofMain.h"
#include "ofxBox2d.h"
#include "ofxOpenNI.h"
#include "ofxOpenCv.h"
#define MAX_BLOBS 4
class ofApp : public ofBaseApp {
private:
(... 変更なし ...)
// display mode
bool debug;
float scale, offsetX, offsetY;
public:
(... 変更なし ...)
};
void ofApp::setup() { (... 変更なし ...) // display mode debug = false; scale = 1.0; offsetX = offsetY = 0; } void ofApp::update() { (... 変更なし ...) } void ofApp::draw() { // display mode ofTranslate(offsetX, offsetY); ofScale(scale, scale); // draw shadow/background (... 変更なし ...) // draw the number of circles and fps if (debug) { char buf[100]; sprintf(buf, "%ld circles", circles.size()); ofDrawBitmapString(buf, 20, 20); sprintf(buf, "%5.2f fps", ofGetFrameRate()); ofDrawBitmapString(buf, 20, 40); } } void ofApp::keyPressed(int key){ if (key == 'd') { debug = ! debug; } else if (key == 'f') { ofToggleFullscreen(); float w = ofGetWindowWidth(), h = ofGetWindowHeight(); scale = MIN(w / 640, h / 480); offsetX = (w - 640 * scale) / 2; offsetY = (h - 480 * scale) / 2; } }
'f' が押されるとフルスクリーン表示に移行して,画面の幅と高さを調べます.この幅と高さが,元の 640, 480 から何倍になっているのかを計算し,その小さい方を scale とします.offsetX, offsetY は,画面中央に描画されるようにするために,描画原点に与えるオフセットです.これらが設定されると,ofApp::draw() の中で,まずオフセット分だけ座標系を平行移動し,座標系を scale 倍に拡大します.4:3 よりも横長のディスプレーでは,画面下端の左右から雪粒が落ちていくようになってしまいますが,それほど違和感はないと思います.加えて,'d' を押すごとに,文字情報を表示・非表示するようにしています.