小嶋秀樹 | 研究室
日本語 | English
openFrameworks 応用編(5)

ここではインタラクティブアートへの応用をめざして,2次元物理シミュレータである ofxBox2d の概要を紹介します.また,ofxOpenNI と ofxOpenCv を使った Kinect との連携についても扱います.

注意:準備作業が抜けている人をよく見かけます.Xcode を正しくインストールしてください.Apple の Developer サイトから無料で入手できます.openFrameworks は最新版をダウンロードしてください.ここでは Xcode 8.1 + openFrameworks 0.9.8 の利用を想定しています.

ofxBox2d 入門

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
    (... 変更なし ...)
}
ofxBox2d で雪を降らせる

ボールやポリゴンではなく「雪」を降らせ,地面に積もらせることを考えます.内部的にはボールを使いますが,描画時はボールの代わりに雪の画像を表示します.

【ボールの位置に雪を描く】

ここで扱う雪の画像は,ガウシアン分布で不透明度を与えた白とします.中心は完全な白で,周辺に行くにつれて透明度が上がるという 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 を組み込んだプロジェクトの生成手順は次のとおりです.

準備ができたので,上で扱った 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' を押すごとに,文字情報を表示・非表示するようにしています.