メディア芸術演習 VI - メディア・アート II
openFramewrorks – addonを使う 2:reacTIVision + ofxTUIO でタンジブルなインタフェイスを作る
reacTIVisionとは?
今日の授業では、reacTIVisionを使用してインタラクティブなアプリケーションを作成してみようと思います。
reacTIVisionとは、タンジブル(Tangible)なユーザインタフェイスを実現するための、オープンソースのツールキットです。タンジブルとは、形のない情報に直接触れることが出来るようなインターフェイスのことです。reacTIVisionは、このタンジブルな環境を実現するために、専用のマーカーを使用して複数のポイントの位置と角度を、素早く認識し解析することができます。
reacTIVisionを応用した有名な作品として、Reactableが挙げられます。Reactableは、テーブルの上に物理的にオブジェクトを配置していくことで、音響の生成や、音楽の演奏をすることができる、タンジブルな音楽創作環境です。
reacTIVisionを利用することで、このようなインタラクティブでタンジブルな作品を比較的容易に制作することが可能となります。
reacTIVisonを利用したシステムの概要
まず初めに、reacTIVisionを使用したシステムの全体像について理解しましょう。reacTIVisionは単体のアプリケーションとなっていて、コンピュータに接続したカメラで専用のマーカーの位置と角度を識別します。その解析結果は、TUIOという通信プロトコルを使用して送出されます。TUIOとはマルチタッチのインタフェイスやタンジブルなインタフェイスの情報の送受信に特化した、通信プロトコルです。TUIOはOSC(Open Sound Control)のプロトコルをベースにしてそれを拡張したものとなっています。
TUIOを情報を受信してどのようにインタフェイスを設計し表現するかという部分が、創作に関係するパートとなります。今回はこのTUIOを受信して表現する部分にopenFrameworksを用いることになります。(もちろんopenFrameworks以外のアプリケーションを使用することも可能です)。openFrameworksでTUIOを使用するにはofxTUIOというアドオンを用います。
reacTIVisonのセットアップ
まずは、マーカーを読みとる側のreacTIVisionを設定します。といっても、こちらはアプリケーションをダウンロードして実行するだけです。reacTIVisionのWebサイトのreactiから、最新のreacTIVison vision engineをダウンロードします。Zipファイルを解凍して、reacTIVisionというアプリケーションを実行します
reacTIVisonのマーカーはreacTIVisionのアプリケーションのフォルダ内のsymbolフォルダ内の「default.pdf」をプリントアウトして使用します。カメラの前でマーカーをかざすと、マーカーの位置と角度、マーカーのID番号が表示されます。正しく認識できたら、マーカーの読み取り側の環境は設定完了です。
openFrameworksのセットアップ
次にopenFrameworksの設定を行います。
openFrameworks側では、reacTIVision vision engineから送出されるTUIOプロトコルを受信して、その内容を解析します。その解析結果(マーカーの位置、角度、ID番号)を利用して、ビジュアルやサウンドをインタラクティブに操作できるようにしてみようと思います。
openFrameworksでTUIOを送受信できるようにするには、ofxTUIOというアドオンを利用します。またこのofxTUIOを使用するためには、併せてofxOSCというアドオンも必要となります。ofxOSCはFAT版のaddonsに最初から含まれていますので、ofxTUIOだけダウンロードすれば必要なシステムは整います。以下のリンクよりofxTUIOをダウンロードして、openFrameworksのaddonsフォルダ内に配置します。
このアドオンを使用するには、先週行ったofxBox2dと同様にプロジェクトにアドオンのソースとライブラリを追加する必要があります。新規にopenFrameworksのプロジェクトを作成し、ファイルリストにofxTUIOとofxOSCを追加します。追加が完了すると、ファイルリストは以下のような構成になるでしょう。
マーカーの位置を認識する
まず初めに認識したマーカーの位置と角度とIDを表示するだけの簡単なプログラムを作成してみましょう。このプログラムがofxTUIOを用いたアプリケーションのテンプレートとなります。
testApp.h
#ifndef _TEST_APP #define _TEST_APP #include "ofMain.h" #include "ofxTuio.h" //-------------------------------------------------------- class testApp : public ofSimpleApp{ public: void setup(); void update(); void draw(); void keyPressed (int key); void mouseMoved(int x, int y ); void mouseDragged(int x, int y, int button); void mousePressed(int x, int y, int button); void mouseReleased(); //TUIOの物体(マーカー)に関係するイベントを追加 //物体の追加を検知 void objectAdded(ofxTuioObject & tuioObject); //物体の削除を検知 void objectRemoved(ofxTuioObject & tuioObject); //物体の状態の更新を検知 void objectUpdated(ofxTuioObject & tuioObject); //TUIOのカーソル(タッチポイントなど)に関係するイベント //カーソルの追加を検知 void tuioAdded(ofxTuioCursor & tuioCursor); //カーソルの削除を検知 void tuioRemoved(ofxTuioCursor & tuioCursor); //カーソルの状態の更新を検知 void tuioUpdated(ofxTuioCursor & tuioCursor); private: //TUIOのクライアントのインスタンス化 myTuioClient tuio; //ログの出力用 string log; }; #endif
testApp.cpp
#include "testApp.h" void testApp::setup(){ //TUIOに関連するイベントリスナーの追加 ofAddListener(tuio.objectAdded,this,&testApp::objectAdded); ofAddListener(tuio.objectRemoved,this,&testApp::objectRemoved); ofAddListener(tuio.objectUpdated,this,&testApp::objectUpdated); ofAddListener(tuio.cursorAdded,this,&testApp::tuioAdded); ofAddListener(tuio.cursorRemoved,this,&testApp::tuioRemoved); ofAddListener(tuio.cursorUpdated,this,&testApp::tuioUpdated); //フレームレート設定 ofSetFrameRate(60); //背景を黒に ofBackground(0,0,0); //ポート番号3333で、TUIOの通信開始 tuio.start(3333); //ログのテキストを空に log=""; } void testApp::update(){ //TUIOのメッセージを受信 tuio.getMessage(); } void testApp::draw(){ //カーソルの状態を表示 tuio.drawCursors(); //オブジェクトの状態を表示 tuio.drawObjects(); //ログを表示 ofSetColor(0xffffff); ofDrawBitmapString(log, 20, 20); } void testApp::keyPressed (int key){ } void testApp::mouseMoved(int x, int y ){ } void testApp::mouseDragged(int x, int y, int button){ } void testApp::mousePressed(int x, int y, int button){ } void testApp::mouseReleased(){ } void testApp::objectAdded(ofxTuioObject & tuioObject){ //マーカー追加 log = " new object: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::objectRemoved(ofxTuioObject & tuioObject){ //マーカー削除 log = " object removed: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::objectUpdated(ofxTuioObject & tuioObject){ //マーカーの状態更新 log = " object updated: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::tuioAdded(ofxTuioCursor & tuioCursor){ //カーソル追加 log = " new cursor: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); } void testApp::tuioRemoved(ofxTuioCursor & tuioCursor){ //カーソル削除 log = " cursor removed: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); } void testApp::tuioUpdated(ofxTuioCursor & tuioCursor){ //カーソル状態更新 log = " cursor updated: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); }
ofxTUIOでは、TUIOの送受信をmyTuioClientというクラスを通して行います。myTuioClientは、TUIO通信の開始や終了、物体の状態の表示などを、複数のオブジェクトを一括した状態で情報の取得が可能となっています。myTuioClientの基本的なメソッドには次のようなものがあります。以下の例では、myTuioClientのインスタンスをtuioとした場合の例になります。
- tuio.start(ポート番号) – 設定したポート番号でTUIOの通信を開始
- tuio.stop() – 通信の有料
- tuio.getMessage() – TUIOのメッセージを受信
- tuio.drawCursors() – カーソルの状態を画面に表示
- tuio.drawObjects() – 物体(マーカー)の状態を画面に表示
このプログラムでは、testAppのsetup()内で、myTuioClientのstart()で通信を開始し、update()内では、getMessage()を繰り返し呼びだすことで、常に最新のTUIOメッセージを取得しています。その状態をもとに、drawObjects()とdrawCursors()を使用して画面状に現在の状態を描画しています。
また、ofxTUIOは、個別の物体やカーソルの状態の更新をTUIOのイベントという形で通知します。これは、マウスやキーボードからの入力を通知するイベント(keyPressed、mouseReleasedなど)と似ています。TUIOから通知されるイベントには下記のようなものがあります。
- objectAdded(ofxTuioObject & tuioObject) – 物体(tuioObject)が画面に追加された際に通知される
- objectRemoved(ofxTuioObject & tuioObject) – 物体(tuioObject)が画面から削除された際に通知される
- objectUpdated(ofxTuioObject & tuioObject) – 物体(tuioObject)の状態が更新された際に通知される
- tuioAdded(ofxTuioCursor & tuioCursor) – カーソル(tuioCursor)が画面に追加された際に通知される
- tuioRemoved(ofxTuioCursor & tuioCursor) – カーソル(tuioCursor)が画面から削除された際に通知される
- tuioUpdated(ofxTuioCursor & tuioCursor) – カーソル(tuioCursor)の状態が更新された際に通知される
イベントは、検出した物体のインスタンス、またはカーソルのインスタンスと一緒に送られてきます。このインスタンスに定義されたアクセサ(メソッド)を通して、物体の情報を取り出すことが可能となっています。物体の状態を知るためのアクセサとして以下のものがあります。
- tuioObject.getFiducialId() – 物体のマーカーID番号
- tuioObject.getX() – 物体のX座標
- tuioObject.getY() – 物体のY座標
- tuioObject.getAngleDegrees() – 物体の配置角度
サンプルプログラムでは、これらのTUIOに関するイベント情報を画面の左上に常に最新の状態で表示するようにしています。
オリジナルなマーカーを描画する
最初の例では、tuio.drawObjects()を利用してマーカーの状態を描画しましたが、この表示はあくまで状態の確認用のものであり、実際に作品を制作する際には、マーカーの状態(ID、座標、角度)にアクセスして、オリジナルの表現方法で描画していく必要があります。
複数のマーカーの情報を取得する際にも、myTuioClientのメソッドを利用します。myTuioClientのインスタンスをtuioとした場合、tuio.getTuioObjects() メソッドを実行すると、myTuioClientを通して全てのオブジェクトのリストを取得できます。データの形式は、ofxTuioObjectのポインタ型を格納したlistという形式になっています。このlistは、vectorとよく似た動的な配列型です。しかしvectorと違って、listはその配列内の要素にアクセスする際には必ずイテレータ(iterator)を用いなければならないという制約があります。
このサンプルプログラムでもfor文の継続条件にイテレータを使用して、個々のオブジェクト(ofxTuioObject)のポインタを取り出し、そこから、X座標、Y座標、角度を取得しています。例えば、ofxTuioObjectのポインタ型を*blobとした場合下記のようにして個々の情報にアクセスすることができます。
- blob->getFiducialId() – オブジェクトのID
- blob->getX() – X座標
- blob->getY() – Y座標
マーカーの状態をオリジナルな方法で描画するサンプルとして、まずは簡単な例として、画像ファイルを読み込んで、全てのマーカーの位置と角度の情報を取得して、その場所と角度を画像で再現するというプログラムを作成してみたいと思います。
testApp.h
#ifndef _TEST_APP #define _TEST_APP #include "ofMain.h" #include "ofxTuio.h" //-------------------------------------------------------- class testApp : public ofSimpleApp{ public: void setup(); void update(); void draw(); void keyPressed (int key); void mouseMoved(int x, int y ); void mouseDragged(int x, int y, int button); void mousePressed(int x, int y, int button); void mouseReleased(); //TUIOの物体(マーカー)に関係するイベントを追加 //物体の追加を検知 void objectAdded(ofxTuioObject & tuioObject); //物体の削除を検知 void objectRemoved(ofxTuioObject & tuioObject); //物体の状態の更新を検知 void objectUpdated(ofxTuioObject & tuioObject); //TUIOのカーソル(タッチポイントなど)に関係するイベント //カーソルの追加を検知 void tuioAdded(ofxTuioCursor & tuioCursor); //カーソルの削除を検知 void tuioRemoved(ofxTuioCursor & tuioCursor); //カーソルの状態の更新を検知 void tuioUpdated(ofxTuioCursor & tuioCursor); private: //TUIOのクライアントのインスタンス化 myTuioClient tuio; //ログの出力用 string log; //表示する画像 ofImage myImage; }; #endif
testApp.cpp
#include "testApp.h" void testApp::setup(){ //TUIOに関連するイベントリスナーの追加 ofAddListener(tuio.objectAdded,this,&testApp::objectAdded); ofAddListener(tuio.objectRemoved,this,&testApp::objectRemoved); ofAddListener(tuio.objectUpdated,this,&testApp::objectUpdated); ofAddListener(tuio.cursorAdded,this,&testApp::tuioAdded); ofAddListener(tuio.cursorRemoved,this,&testApp::tuioRemoved); ofAddListener(tuio.cursorUpdated,this,&testApp::tuioUpdated); //フレームレート設定 ofSetFrameRate(60); //背景を黒に ofBackground(0,0,0); //ポート番号3333で、TUIOの通信開始 tuio.start(3333); //ログのテキストを空に log=""; //画像ファイルを読み込み myImage.loadImage("images/photo.png"); } void testApp::update(){ //TUIOのメッセージを受信 tuio.getMessage(); } void testApp::draw(){ //オブジェクトのリストを取得 list<ofxTuioObject*> objectList = tuio.getTuioObjects(); //リスト操作のためのイテレータを準備 list<ofxTuioObject*>::iterator tobj; //全てのオブジェクトをイテレータで操作 for (tobj=objectList.begin(); tobj != objectList.end(); tobj++) { //物体を取得 ofxTuioObject *blob = (*tobj); //座標を記録 glPushMatrix(); //座標を移動 glTranslatef(blob->getX()*ofGetWidth(), blob->getY()*ofGetHeight(), 0.0); //ID番号を表示 ofDrawBitmapString("id = " + ofToString(blob->getFiducialId(), 0), -64, 80); //回転 glRotatef(blob->getAngleDegrees(), 0.0, 0.0, 1.0); ofSetColor(255, 255, 255); //画像を描画 myImage.draw(-64, -64); //座標を復帰 glPopMatrix(); } //ログを表示 ofSetColor(0xffffff); ofDrawBitmapString(log, 20, 20); } void testApp::keyPressed (int key){ } void testApp::mouseMoved(int x, int y ){ } void testApp::mouseDragged(int x, int y, int button){ } void testApp::mousePressed(int x, int y, int button){ } void testApp::mouseReleased(){ } void testApp::objectAdded(ofxTuioObject & tuioObject){ //マーカー追加 log = " new object: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::objectRemoved(ofxTuioObject & tuioObject){ //マーカー削除 log = " object removed: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::objectUpdated(ofxTuioObject & tuioObject){ //マーカーの状態更新 log = " object updated: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::tuioAdded(ofxTuioCursor & tuioCursor){ //カーソル追加 log = " new cursor: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); } void testApp::tuioRemoved(ofxTuioCursor & tuioCursor){ //カーソル削除 log = " cursor removed: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); } void testApp::tuioUpdated(ofxTuioCursor & tuioCursor){ //カーソル状態更新 log = " cursor updated: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); }
タンジブル・サウンドプレーヤー
応用例として、マーカーの位置や角度によって複数の音を同時再生するタンジブル・サウンドプレーヤーを作ってみましょう。
この例では、5つのofSoundPlayerを準備して、それぞれにサウンドファイルを読み込んでいます。そして、マーカーのX座標を音の左右の定位に、マーカーのY座標をオーディオの再生スピードに、マーカーの角度でサウンドのボリュームが変化するようにしています。
testApp.h
#ifndef _TEST_APP #define _TEST_APP #include "ofMain.h" #include "ofxTuio.h" //#include "CustomTuioClient.h" #define NUM 5 //-------------------------------------------------------- class testApp : public ofSimpleApp{ public: void setup(); void update(); void draw(); void keyPressed (int key); void mouseMoved(int x, int y ); void mouseDragged(int x, int y, int button); void mousePressed(int x, int y, int button); void mouseReleased(); void objectAdded(ofxTuioObject & tuioObject); void objectRemoved(ofxTuioObject & tuioObject); void objectUpdated(ofxTuioObject & tuioObject); void tuioAdded(ofxTuioCursor & tuioCursor); void tuioRemoved(ofxTuioCursor & tuioCursor); void tuioUpdated(ofxTuioCursor & tuioCursor); private: myTuioClient tuio; string log; ofSoundPlayer mySounds[NUM]; }; #endif
testApp.cpp
#include "testApp.h" void testApp::setup(){ //イベントリスナーの追加 ofAddListener(tuio.objectAdded,this,&testApp::objectAdded); ofAddListener(tuio.objectRemoved,this,&testApp::objectRemoved); ofAddListener(tuio.objectUpdated,this,&testApp::objectUpdated); ofAddListener(tuio.cursorAdded,this,&testApp::tuioAdded); ofAddListener(tuio.cursorRemoved,this,&testApp::tuioRemoved); ofAddListener(tuio.cursorUpdated,this,&testApp::tuioUpdated); //画面の基本設定 ofBackground(0,0,0); ofSetFrameRate(60); ofSetCircleResolution(32); ofEnableAlphaBlending(); ofSetVerticalSync(true); ofEnableSmoothing(); //サウンドファイルの読み込み for (int i = 0; i < NUM; i++) { mySounds[i].loadSound("sounds/rainstick.aif"); mySounds[i].setLoop(true); } //TUIO通信開始、ポート3333 tuio.start(3333); } void testApp::update(){ //TUIOメッセージの受信 tuio.getMessage(); //オブジェクトのリストを取得 list<ofxTuioObject*> objectList = tuio.getTuioObjects(); //イテレータの準備 list<ofxTuioObject*>::iterator tobj; //カウンター用変数 int i = 0; //表示されている物体の数だけくりかえし for (tobj=objectList.begin(); tobj != objectList.end(); tobj++) { //個別の物体を取り出し ofxTuioObject *blob = (*tobj); //IDを抽出 int id = blob->getFiducialId(); //サウンドが設定されたIDの範囲なら if (id >= 0 && id < NUM) { //X座標を左右の定位に適用 mySounds[id].setPan(blob->getX()); //Y座標を再生スピードの適用 mySounds[id].setSpeed((1.0f - blob->getY()) * 2.0); //角度をサウンドのボリュームに適用 mySounds[id].setVolume(blob->getAngleDegrees()/360.0); } } } void testApp::draw(){ //オリジナルの図形を描画 //半径を20に float radius = 20; //オブジェクトのリストを取得 list<ofxTuioObject*> objectList = tuio.getTuioObjects(); //イテレータの準備 list<ofxTuioObject*>::iterator tobj; //表示されている物体の数だけくりかえし for (tobj=objectList.begin(); tobj != objectList.end(); tobj++) { //個別の物体を取り出し ofxTuioObject *blob = (*tobj); //オリジナルの物体を描画 glPushMatrix(); glTranslatef(blob->getX()*ofGetWidth(), blob->getY()*ofGetHeight(), 0.0); glRotatef(blob->getAngleDegrees(), 0.0, 0.0, 1.0); ofFill(); ofSetColor(0, 0, 255, 127); //サウンドのレベルによって半径を変化させて青い円を描く float* spectrum; spectrum = ofSoundGetSpectrum(1); ofCircle(0, 0, spectrum[0] * radius * 100.0); ofSetColor(255, 255, 255); ofNoFill(); ofCircle(0, 0, radius); ofLine(0, -radius, 0, 0); glPopMatrix(); } //ログを表示 ofSetColor(0xffffff); ofDrawBitmapString(log, 20, 20); } void testApp::keyPressed (int key){ } void testApp::mouseMoved(int x, int y ){ } void testApp::mouseDragged(int x, int y, int button){ } void testApp::mousePressed(int x, int y, int button){ } void testApp::mouseReleased(){ } void testApp::objectAdded(ofxTuioObject & tuioObject){ int id = tuioObject.getFiducialId(); if (id >= 0 && id < NUM) { mySounds[tuioObject.getFiducialId()].play(); } //マーカー追加 log = " new object: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::objectRemoved(ofxTuioObject & tuioObject){ int id = tuioObject.getFiducialId(); if (id >= 0 && id < NUM) { mySounds[tuioObject.getFiducialId()].stop(); } //マーカー削除 log = " object removed: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::objectUpdated(ofxTuioObject & tuioObject){ //マーカーの状態更新 log = " object updated: " + ofToString(tuioObject.getFiducialId())+ " X: "+ofToString(tuioObject.getX())+ " Y: "+ofToString(tuioObject.getY())+ " angle: "+ofToString(tuioObject.getAngleDegrees()); } void testApp::tuioAdded(ofxTuioCursor & tuioCursor){ //カーソル追加 log = " new cursor: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); } void testApp::tuioRemoved(ofxTuioCursor & tuioCursor){ //カーソル削除 log = " cursor removed: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); } void testApp::tuioUpdated(ofxTuioCursor & tuioCursor){ //カーソル状態更新 log = " cursor updated: " + ofToString(tuioCursor.getFingerId())+ " X: "+ofToString(tuioCursor.getX())+ " Y: "+ofToString(tuioCursor.getY()); }
おまけ
いま、Microsoftのゲーム用センサーKinectをハックして作品に使用することが世界的なブームになっています。
ofxKinectというアドオンを使用すると、Kinectの入力をopenFrameworksで使用することが可能となります。Kinectを購入したので、実際にKinectで遊んでみましょう。
ofxKinect test from Atsushi Tadokoro on Vimeo.
ofxKinect Study #01 from Atsushi Tadokoro on Vimeo.
ofxKinect study #03 from Atsushi Tadokoro on Vimeo.
サンプルプログラムのダウンロード
今日とりあげたサンプルは下記からダウンロードしてください。