Facebookからのコメントテスト
facebookのコメント機能と統合してみた。うまくいくかテスト。
facebookのコメント機能と統合してみた。うまくいくかテスト。
今日の授業は、年末の課題として出題したPicasa Webアルバムにアップロードした画像をFlashから扱う方法について学んでいきます。今回の授業内容は、最終課題に直結しますので、しっかりと理解するようにしましょう。
Picasa Webアルバムは、API (Application Program Interface – 開発の際に使用できる命令や関数の集合のこと) を用いてWeb経由で情報を取得したり検索したりといった様々な操作が可能となっています。Picasaで使用できるAPIの詳細な資料は下記のページに掲載されていて、この内容を理解することで、Piacasaに掲載された写真の情報を利用して様々なアプリケーションやWebサービスの開発が可能となります。
しかし、このAPIをそのままFlashで使用するには、ある程度のプログラムの知識が必要となり、必ずしもFlashに最適化されたAPIとはいえません。
FlashからPicasa WebアルバムAPIを操作するのに便利なように、AS3で書かれたライブラリが開発されています。このAS3のライブラリを利用することで、より簡易にPicasaの情報をFlash(AS3)から利用できるになります。今回は以下のライブラリを利用します。
このページの「Dowlnloads」から「picasaflashapi-r421.swc」をダウンロードしてファイルを保存しておきます。
これからPicasaにアップロードしたWebアルバムの情報を取得するために、PicasaのアカウントとアルバムIDが必要となります。
まず写真をアップロードしたPicasa WebアルバムをWebブラウザで開きます。アルバムの一覧から使用するアルバムを選択し、アルバムの画面を開きます。
画面の左側に「RSS」というリンクが表示されるので、そのリンクをクリックしてRSSフィードを開きます。このRSSフィードのURLから、picasaのユーザIDとアルバムIDがわかります。例えば、RSSのURLが
の場合は、ユーザIDは「tadokoro」、アルバムIDは「5560743918853662497」になります。つまりこのURLは
となっています。このユーザIDとアルバムIDを記録しておきます。
まずは、APIを利用してPicasa Webアルバムの情報が取得できるか試してみます。以下の手順で制作します。
これらの手順を終えたら、「Main.as」に以下の記述を記入します。
[sourcecode language=”as3″]
package {
import sk.prasa.webapis.picasa.PicasaResponder;
import sk.prasa.webapis.picasa.PicasaService;
import sk.prasa.webapis.picasa.events.PicasaDataEvent;
import flash.system.Security;
import flash.display.*;
import flash.events.*;
import flash.net.*;
public class Main extends Sprite {
//ユーザIDとアルバムIDを指定
private var userID:String=’tadokoro’;
private var alubumID:String=’5560743918853662497′;
public function Main() {
//クロスドメインポリシーファイルの読み込み
Security.loadPolicyFile("http://photos.googleapis.com/data/crossdomain.xml");
init();
}
private function init():void {
//Picasa APIのサービスを新規に生成
var service : PicasaService = new PicasaService();
//使用可能な画像サイズ一覧 (Pixel)
//32, 48, 64, 72, 144, 160, 200, 288, 320, 400, 512, 576,
//640, 720, 800, 912, 1024, 1152, 1280, 1440, 1600
//最大の幅
service.imgmax="912";
//サムネイルのサイズ
service.thumbsize="64";
//最大読込枚数
service.max_results=100;
//ユーザIDとアルバムIDを指定して、情報を読み込み
var responder:PicasaResponder=service.photos.list(userID,alubumID);
//情報の読込みが完了した際に発生するイベントリスナーの登録
responder.addEventListener(PicasaDataEvent.DATA, onCompleteHandler);
}
//情報を読込み
private function onCompleteHandler(picsData:PicasaDataEvent):void {
trace(‘写真データ :’ + picsData.data );
trace(‘エントリー :’ + picsData.data.entries);
trace(‘写真総数 :’ + picsData.data.entries.length);
//個別の写真のデータを取得する
for (var i:int = 0; i < picsData.data.entries.length; i++) {
trace(‘——————————————————-‘);
trace(‘写真NO : ‘ + i);
trace(‘写真ID : ‘ + picsData.data.entries[i].id);
trace(‘写真のURL : ‘ + picsData.data.entries[i].media.content.url);
trace(‘サムネイルURL : ‘ + picsData.data.entries[i].media.thumbnails[0].url);
trace(‘写真の幅 : ‘ + picsData.data.entries[i].gphoto.width);
trace(‘写真の高さ : ‘ + picsData.data.entries[i].gphoto.height);
}
}
}
}
[/sourcecode]
Flashの「出力」画面にアルバムにアップロードした写真の情報が取得していることを確認できます。
[sourcecode language=”as3″]
(GET) http://photos.googleapis.com/data/feed/api/user/tadokoro/albumid/5560743918853662497
写真データ :[object AtomFeed]
エントリー :[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry],[object PhotoEntry]
写真総数 :22
——————————————————-
写真NO : 0
写真ID : http://photos.googleapis.com/data/entry/api/user/tadokoro/albumid/5560743918853662497/photoid/5560743958909231746
写真のURL : http://lh6.ggpht.com/_S9tCNvdm3tU/TSu7KyWvIoI/AAAAAAAAE3Q/6r-SUDLf76s/s912/DSC_0044.jpg
サムネイルURL : http://lh6.ggpht.com/_S9tCNvdm3tU/TSu7KyWvIoI/AAAAAAAAE3Q/6r-SUDLf76s/s64-c/DSC_0044.jpg
写真の幅 : 4928
写真の高さ : 3264
——————————————————-
写真NO : 1
写真ID : http://photos.googleapis.com/data/entry/api/user/tadokoro/albumid/5560743918853662497/photoid/5560744066139826450
写真のURL : http://lh4.ggpht.com/_S9tCNvdm3tU/TSu7RB0gKRI/AAAAAAAAE3U/WKxbZjpigdw/s912/DSC_0082.jpg
サムネイルURL : http://lh4.ggpht.com/_S9tCNvdm3tU/TSu7RB0gKRI/AAAAAAAAE3U/WKxbZjpigdw/s64-c/DSC_0082.jpg
写真の幅 : 4928
写真の高さ : 3264
——————————————————-
写真NO : 2
写真ID : http://photos.googleapis.com/data/entry/api/user/tadokoro/albumid/5560743918853662497/photoid/5560744129672119666
写真のURL : http://lh4.ggpht.com/_S9tCNvdm3tU/TSu7UufyBXI/AAAAAAAAE3Y/Vzg6F-NCPLw/s912/DSC_0087.jpg
サムネイルURL : http://lh4.ggpht.com/_S9tCNvdm3tU/TSu7UufyBXI/AAAAAAAAE3Y/Vzg6F-NCPLw/s64-c/DSC_0087.jpg
写真の幅 : 4928
写真の高さ : 3264
——————————————————-
写真NO : 3
写真ID : http://photos.googleapis.com/data/entry/api/user/tadokoro/albumid/5560743918853662497/photoid/5560744232728263106
写真のURL : http://lh3.ggpht.com/_S9tCNvdm3tU/TSu7auaR-cI/AAAAAAAAE3c/YBL6f5z83PI/s912/DSC_0132.jpg
サムネイルURL : http://lh3.ggpht.com/_S9tCNvdm3tU/TSu7auaR-cI/AAAAAAAAE3c/YBL6f5z83PI/s64-c/DSC_0132.jpg
写真の幅 : 4928
写真の高さ : 3264
…
[/sourcecode]
今回は、外部のWebサービスと連携して、作成したWebページに動画や写真、音声などを掲載する方法について学びます。また、最終課題への準備として、Picasa Webアルバムを使用する環境設定を行います。
この授業では、最終課題としてオンライン・ポートフォリオを作成します。これまでの授業の中でWebページの作成方法について様々なトピックをとりあげてきましたが、最終的にこうした内容をふまえて自分自身のオンラインポートフォリオを完成させてください。
展覧会のページのために、各自1つ自分のオンラインポートフォリオのバナーを作成します。バナーは以下のフォーマットで作成するようにしてください
ファイルは、IDDのWebサーバーにアップロードすること。最後に必ず以下のアドレスから閲覧できることを確認。
オンラインポートフォリオを課題として扱うのはこの授業で終わりですが、オンラインポートフォリオ自体はこれから卒業まで(卒業後も?)、スタジオの選択や卒業制作、就職活動などで重要な資料となります。この授業が終わったら放置してしまうことなく、絶えず更新をし続けて、iddでの活動がオンラインポートフォリオを閲覧すると全てわかるようなものを目指して活用していくようにしてください。
今回は、ネットワークを使用したopenFrameworks同士の連携と、openFrameworksと他のアプリケーションとの連携について取り上げます。openFrameworksでネットワークを利用す方法はいくつかありますが、今回はその中で比較的扱い易いプロトコルである、Open Sound Control (OSC) についてとりあげたいと思います。
Open Sound Controlは、カリフォルニア大学バークレー校にある CNMAT(The Center for New Music and Audio Technologies)が開発した、通信プロトコルです。その特徴について、CNMATのページではOSCについて下記のように要約されています。
Open Sound Control (OSC) is a protocol for communication among computers, sound synthesizers, and other multimedia devices that is optimized for modern networking technology. Bringing the benefits of modern networking technology to the world of electronic musical instruments, OSC’s advantages include interoperability, accuracy, flexibility, and enhanced organization and documentation.
(訳) Open Sound Control (OSC) は、コンピュータ、シンセサイザー、その他のマルチメディアデバイス同士でコミュニケーションするための通信プロトコルです。現代のネットワーク技術の成果を電子楽器の世界に適用することで、OSCは、相互運用性、正確さ、柔軟さ、また、拡張性に優れた性能を持ちます。
この説明からわかるように、OSCは当初は電子楽器を連携する目的で開発されました。すっかり古くなってしまったMIDI(Musical Instrument Digital Interface)の次世代を担うプロトコルとなることを狙いとしています。その通信の仕組みとして、インターネットで用いられている通信方式(TCP/IP、UDP/IP)を活用することで、柔軟な連携を可能にしています。
現在では、OSCはその簡易さと柔軟性から電子楽器やコンピュータ音楽の範囲を越えて、様々なアプリケーションやハードウェアに実装されています。主なものだけでも以下のリストのように数多くのアプリケーションやハードウェアで実装されています。
OSCのプロトコルは大きくわけて2つのパートに分けられます。一つは、OSCメッセージ(OSC Message)、もう一つはOSC引数(OSC Arguments)です。
OSCメッセージは、送受信するOSCの情報内容をラベリングしたものです。つまり、ここでOSCの値が何を意味してるのかを表しています。OSCメッセージは、WWWなどのインターネットアプリケーションで用いられる、URL (Uniform Resorce Locator) と良く似ています。メッセージは階層構造を持つことが可能で、その階層をスラッシュ「/」で区切って表現します。例えば、マウスの状態をOSCで送受信する際に、その座標と、マウスボタンの状態の2つのメッセージを送りたい場合には、その両者を「mouse」というメッセージでまとめて、その下位メッセージとしって「position」と「button」というメッセージがあると考えます。すると、マウスの座標は「/mouse/position」マウスのボタンの状態は「/mouse/button」というメッセージとして両者を階層的に表現できます。このOSCメッセージはアプリケーションでOSCを使用する目的に応じて自由にデザインしていくことが可能となっています。
OSC引数 (OSC Argument) は実際の値を送信します。値は、整数(int32)、実数(float)、文字列(string)など様々な型を送受信することが可能です。また、複数の値を一度に送受信することも可能となっています。
OSCプロトコルの中身
OSCメッセージのネームスペースの例
では、まず初めにopenFrameworksのアプリケーション同士で、OSCを送受信してみましょう。まずは簡単なサンプルとして、マウスの位置とマウスボタンの状態をOSCで表現してみましょう。
OSCは、マウスの位置と、マウスボタンの状態で2種類使用することにします。それぞれ以下のようにメッセージをわりふります。
openFrameworksのプログラムは、送信側(Sender)と受信側(Receiver)の2種類で構成されます。それぞれのプログラムでOSCを使用するためには、ofxOscアドオンをプロジェクトに追加する必要があります。追加した状態は下記のようになります。
それでは、受信側=サーバー(oscReceiveExample)、送信側=クライアント(oscSenderExample)それぞれをプログラムしていきましょう。
testApp.h
[sourcecode language=”cpp”]
#ifndef _TEST_APP
#define _TEST_APP
#include "ofMain.h"
#include "ofxOsc.h"
//ポート番号を設定
#define PORT 8000
//——————————————————–
class testApp : public ofBaseApp{
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(int x, int y, int button);
void windowResized(int w, int h);
void dumpOSC(ofxOscMessage m); //OSCメッセージを出力
private:
//OSCメッセージを受信するインスタンス
ofxOscReceiver receiver;
//マウス座標
int remoteMouseX, remoteMouseY;
//マウスボタンの状態 ("up", "down")
string mouseButtonState;
//string oscString;
};
#endif
[/sourcecode]
testApp.cpp
[sourcecode language=”cpp”]
#include "testApp.h"
void testApp::setup(){
//指定したポートで接続
receiver.setup( PORT );
//値を初期化
mouseX = 0;
mouseY = 0;
mouseButtonState = "";
ofBackground(0, 0, 0);
}
void testApp::update(){
//現在順番待ちのOSCメッセージがあるか確認
while( receiver.hasWaitingMessages() )
{
//次のメッセージを取得
ofxOscMessage m;
receiver.getNextMessage( &m );
//マウスの位置を取得
if ( m.getAddress() == "/mouse/position" ){
remoteMouseX = m.getArgAsInt32( 0 );
remoteMouseY = m.getArgAsInt32( 1 );
}
//マウスボタンの状態を取得
else if ( m.getAddress() == "/mouse/button" ) {
mouseButtonState = m.getArgAsString( 0 ) ;
}
//OSCメッセージをそのままコンソールに出力
dumpOSC(m);
}
}
//OSCメッセージをコンソールに出力する関数
void testApp::dumpOSC(ofxOscMessage m) {
string msg_string;
msg_string = m.getAddress();
for (int i=0; i<m.getNumArgs(); i++ ) {
msg_string += " ";
if(m.getArgType(i) == OFXOSC_TYPE_INT32)
msg_string += ofToString( m.getArgAsInt32(i));
else if(m.getArgType(i) == OFXOSC_TYPE_FLOAT)
msg_string += ofToString( m.getArgAsFloat(i));
else if(m.getArgType(i) == OFXOSC_TYPE_STRING)
msg_string += m.getArgAsString(i);
}
cout << msg_string << endl;
}
void testApp::draw(){
int radius;
if (mouseButtonState == "down") {
//マウスボタンが押されていたら、赤い円を描画
radius = 20;
ofSetColor(255, 127, 0);
} else {
//マウスボタンが押されていなければ、青い円を描画
radius = 10;
ofSetColor(0, 127, 255);
}
ofCircle(remoteMouseX, remoteMouseY, radius);
}
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(int x, int y, int button){}
void testApp::windowResized(int w, int h){}
[/sourcecode]
testApp.h
[sourcecode language=”cpp”]
#ifndef _TEST_APP
#define _TEST_APP
#include "ofMain.h"
#include "ofxOsc.h"
#define HOST "localhost" //送信先ホストのIPを設定
#define PORT 8000 //送信先のポート番号を設定
//——————————————————–
class testApp : public ofBaseApp{
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(int x, int y, int button);
void windowResized(int w, int h);
private:
//OSCメッセージの送信者
ofxOscSender sender;
};
#endif
[/sourcecode]
testApp.cpp
[sourcecode language=”cpp”]
#include "testApp.h"
void testApp::setup(){
ofBackground(0, 0, 0);
//指定したIPアドレスとポート番号でサーバーに接続
sender.setup( HOST, PORT );
}
void testApp::update(){
}
void testApp::draw(){
//現在のマウスの場所に円を描画
ofSetColor(255, 255, 255);
ofCircle(mouseX, mouseY, 10);
}
void testApp::keyPressed (int key){}
void testApp::mouseMoved(int x, int y ){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/position" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
//メッセージを送信
sender.sendMessage( m );
}
void testApp::mouseDragged(int x, int y, int button){}
void testApp::mousePressed(int x, int y, int button){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/button" );
//OSC引数として、マウス状態"down"を送信
m.addStringArg( "down" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
sender.sendMessage( m );
}
void testApp::mouseReleased(int x, int y, int button){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/button" );
//OSC引数として、マウス状態"up"を送信
m.addStringArg( "up" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
sender.sendMessage( m );
}
void testApp::windowResized(int w, int h){}
[/sourcecode]
送信用プログラムと受信用プログラムを起動して、送信側のウィンドウの上でマウスポインタを動かすと、受信側のプログラムで同じ動きが再現されます。
このプログラムは1台のコンピュータの中だけでなく、ネットワークに繋がったコンピュータ同士でOSCをやりとりすることが可能です。試しに隣の席のコンピュータのIPを調べて、送信側(oscSenderExample)のtestApp.hの7行目に記述してあるIP「”localhost”」を例えば「”192.168.1.10″」のように隣の席のIP番号で置き換えてみましょう。こうすることでネットワークを介して繋がったコンピュータ同士でOSCメッセージが送られていることを確認できます。
受信側のプログラムは、通信の状態をコンソールから確認することができます。XCodeメニューから「実行」→「コンソール」を選択してコンソール画面を表示した状態で実行すると、下記のようにOSCメッセージが受信されていることを確認できます。
/mouse/position 550 430 /mouse/position 547 430 /mouse/position 544 431 /mouse/position 543 431 /mouse/position 541 431 /mouse/position 540 432 /mouse/button down 540 432 /mouse/button up 540 432 /mouse/position 541 432 /mouse/position 543 431 /mouse/position 545 430 /mouse/position 547 429 ...
次にもう少し複雑なサンプルとして、一つのサーバーに複数のクライアントからメッセージを送ることのできるアプリケーションを作成してみましょう。
クライアント側で画面をクリックすると、サーバー側の同じ場所から輪が波紋のように拡がっていくアプリケーションを作成してみましょう。拡がっていく円は別のクラスRingとして作成しています。クライアント側の参加者全員が同じIP番号を指定することで、サーバに向って複数のクライアントから同時にOSCメッセージを送ることが可能です。
testApp.h
[sourcecode language=”cpp”]
#ifndef _TEST_APP
#define _TEST_APP
#include "ofMain.h"
#include "ofxOsc.h"
#include "Ring.h"
//ポート番号を設定
#define PORT 8000
class testApp : public ofBaseApp{
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(int x, int y, int button);
void windowResized(int w, int h);
void dumpOSC(ofxOscMessage m);
vector <Ring *> rings; //拡大する輪"Ring"の配列
private:
ofxOscReceiver receiver;
int remoteMouseX, remoteMouseY;
string mouseButtonState;
string oscString;
};
#endif
[/sourcecode]
testApp.cpp
[sourcecode language=”cpp”]
#include "testApp.h"
void testApp::setup(){
//指定したポートで接続
receiver.setup( PORT );
ofSetFrameRate(60);
//値を初期化
mouseX = 0;
mouseY = 0;
mouseButtonState = "";
ofBackground(0, 0, 0);
}
void testApp::update(){
//現在順番待ちのOSCメッセージがあるか確認
while( receiver.hasWaitingMessages() )
{
//次のメッセージを取得
ofxOscMessage m;
oscString = m.getAddress();
receiver.getNextMessage( &m );
//マウスの位置を取得
if ( m.getAddress() == "/mouse/position" ){
remoteMouseX = m.getArgAsInt32( 0 );
remoteMouseY = m.getArgAsInt32( 1 );
}
//マウスボタンの状態を取得
else if ( m.getAddress() == "/mouse/button" ) {
mouseButtonState = m.getArgAsString( 0 ) ;
remoteMouseX = m.getArgAsInt32( 1 );
remoteMouseY = m.getArgAsInt32( 2 );
}
//OSCメッセージをそのままコンソールに出力
dumpOSC(m);
}
//マウスアップされたら、新規にRingを追加
if(mouseButtonState == "up"){
rings.push_back(new Ring(ofPoint(remoteMouseX, remoteMouseY)));
mouseButtonState = "";
}
//Ring更新
for(vector <Ring *>::iterator it = rings.begin(); it != rings.end();){
(*it)->update();
if ((*it)->dead) {
delete (*it);
it = rings.erase(it);
} else {
++it;
}
}
}
//OSCメッセージをコンソールに出力する関数
void testApp::dumpOSC(ofxOscMessage m) {
string msg_string;
msg_string = m.getAddress();
for (int i=0; i<m.getNumArgs(); i++ ) {
msg_string += " ";
if(m.getArgType(i) == OFXOSC_TYPE_INT32)
msg_string += ofToString( m.getArgAsInt32(i));
else if(m.getArgType(i) == OFXOSC_TYPE_FLOAT)
msg_string += ofToString( m.getArgAsFloat(i));
else if(m.getArgType(i) == OFXOSC_TYPE_STRING)
msg_string += m.getArgAsString(i);
}
cout << msg_string << endl;
}
void testApp::draw(){
//Ringを描画
for(vector <Ring *>::iterator it = rings.begin(); it != rings.end(); ++it){
(*it)->draw();
}
}
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(int x, int y, int button){
rings.push_back(new Ring(ofPoint(x, y)));
}
void testApp::windowResized(int w, int h){}
[/sourcecode]
Ring.h
[sourcecode language=”cpp”]
#ifndef _RING
#define _RING
#include "ofMain.h"
class Ring {
public:
Ring(ofPoint pos); //コンストラクタ
void update();
void draw();
ofPoint pos; //輪の中心位置
float radius; //輪の半径
float radiusSpeed; //輪の拡大スピード
bool dead; //生死の判定
private:
};
#endif
[/sourcecode]
Ring.cpp
[sourcecode language=”cpp”]
#include "Ring.h"
Ring::Ring(ofPoint _pos)
{
pos = _pos;
radius = 0;
radiusSpeed = 0.5;
dead = false;
}
void Ring::update()
{
radius += radiusSpeed;
if (radius > ofGetWidth()) {
dead = true;
}
}
void Ring::draw()
{
ofSetCircleResolution(64);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
ofEnableSmoothing();
ofSetLineWidth(1);
ofPushMatrix();
ofTranslate(pos.x, pos.y);
ofNoFill();
ofSetColor(63, 127, 255, 127 – radius * 127 / ofGetHeight());
ofCircle(0, 0, radius);
ofFill();
ofSetColor(63, 127, 255, 31 – radius * 31 / ofGetHeight());
ofCircle(0, 0, radius);
ofPopMatrix();
}
[/sourcecode]
testApp.h
[sourcecode language=”cpp”]
#ifndef _TEST_APP
#define _TEST_APP
#include "ofMain.h"
#include "ofxOsc.h"
#include "Ring.h"
#define HOST "192.168.1.10" //サーバのIPアドレスを指定する
#define PORT 8000
//——————————————————–
class testApp : public ofBaseApp{
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(int x, int y, int button);
void windowResized(int w, int h);
vector <Ring *> rings; //輪の動的配列
private:
//OSCメッセージの送信者
ofxOscSender sender;
};
#endif
[/sourcecode]
testApp.cpp
[sourcecode language=”cpp”]
#include "testApp.h"
void testApp::setup(){
ofBackground(0, 0, 0);
ofSetFrameRate(60);
//指定したIPアドレスとポート番号でサーバーに接続
sender.setup( HOST, PORT );
}
void testApp::update(){
//Ring更新
for(vector <Ring *>::iterator it = rings.begin(); it != rings.end();){
(*it)->update();
if ((*it)->dead) {
delete (*it);
it = rings.erase(it);
} else {
++it;
}
}
}
void testApp::draw(){
//Ringを描画
for(vector <Ring *>::iterator it = rings.begin(); it != rings.end(); ++it){
(*it)->draw();
}
}
void testApp::keyPressed (int key){}
void testApp::mouseMoved(int x, int y ){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/position" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
//メッセージを送信
sender.sendMessage( m );
}
void testApp::mouseDragged(int x, int y, int button){}
void testApp::mousePressed(int x, int y, int button){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/button" );
//OSC引数として、マウス状態"down"を送信
m.addStringArg( "down" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
sender.sendMessage( m );
}
void testApp::mouseReleased(int x, int y, int button){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/button" );
//OSC引数として、マウス状態"up"を送信
m.addStringArg( "up" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
sender.sendMessage( m );
//Ringを追加
rings.push_back(new Ring(ofPoint(x, y)));
}
void testApp::windowResized(int w, int h){}
[/sourcecode]
※ Ring.hとRing.cppに関しては、サーバ側(oscRingReceiver)と同じ。
「Open Sound Control とは」のセクションで解説したように、OSCはopenFrameworks以外にも多くのアプリケーションで実装され利用可能です。異なるアプリケーション同士を連携させることで、そのアプリケーションで得意な分野に特化させて、苦手な部分は他のアプリケーションに任せることが可能となります。例えばopenFrameworksで複雑な音響生成をするのはあまり向いていません。そこで、音響合成用のアプリケーションやDSP用の言語を用いて音響合成を行い、openFrameworksでは得意とする高速のグラフィック処理に専念するということが可能となります。
OSCが利用可能なアプリケーションは、Max/MSP、Pure Data、Csound、SuperCollider、ChucKなど数多く存在しています。今回は、Pure Data(pd)とSuperColliderをOpenFrameworksと連携させてみようと思います。
pdには、MillerPacketの開発した基本機能だけを収録したバージョン(pd-vanilla)と、様々なサードパーティーの機能拡張を含んだバージョン(pd-extended)があります。
pdでOSCを扱いたい際は、拡張機能を収録したpd-extendedを使用する必要があります。最新版のpd-extendedは、下記からダウンロードしてください。
PdでのOSCの受信は、とてもシンプルです。まず、「dumpOSC」オブジェクトを用いて、OSCを受信します。その際に第1引数としてポート番号を指定します。次に受信したOSCメッセージを、「OSCroute」オブジェクトで分解していきます。例えば、「/mouse/position」というOSCメッセージを取り出したい場合には、「OSCroute /mouse」として/mouse以下のメッセージを取り出した上で、「OSCroute /position」として/position以下の値を抽出します。あとは、パラメータの数に応じて「unpack」オブジェクトでOSC引数を分解します。
では、pdで簡単なFMシンセサイザーのパッチを作成してみましょう。そして、モジュレーターのオシレータの周波数とインデックスをそれぞれ、マウスのX座標とY座標でコントロールできるよう、OSCの受信側の設定をしています。マウスの座標は「/mouse/position」メッセージで受信するようになっています。
このpdのパッチにOSCメッセージを送出するopenFrameworksのプログラムは、openFrameworks同士でOSCを送受信していたものと全く同じやり方で、実現可能です。
testApp.h
[sourcecode language=”cpp”]
#ifndef _TEST_APP
#define _TEST_APP
#include "ofMain.h"
#include "ofxOsc.h"
#define HOST "localhost" //IPアドレスを入力
#define PORT 8000
//——————————————————–
class testApp : public ofBaseApp{
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(int x, int y, int button);
void windowResized(int w, int h);
private:
//OSCメッセージの送信者
ofxOscSender sender;
};
#endif
[/sourcecode]
testApp.cpp
[sourcecode language=”cpp”]
#include "testApp.h"
void testApp::setup(){
ofBackground(0, 0, 0);
//指定したIPアドレスとポート番号でサーバーに接続
sender.setup( HOST, PORT );
}
void testApp::update(){}
void testApp::draw(){
//現在のマウスの場所に円を描画
ofSetColor(255, 255, 255);
ofCircle(mouseX, mouseY, 10);
}
void testApp::keyPressed (int key){}
void testApp::mouseMoved(int x, int y ){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/position" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
//メッセージを送信
sender.sendMessage( m );
}
void testApp::mouseDragged(int x, int y, int button){}
void testApp::mousePressed(int x, int y, int button){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/button" );
//OSC引数として、マウス状態"down"を送信
m.addStringArg( "down" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
sender.sendMessage( m );
}
void testApp::mouseReleased(int x, int y, int button){
//OSCメッセージの準備
ofxOscMessage m;
//OSCアドレスの指定
m.setAddress( "/mouse/button" );
//OSC引数として、マウス状態"up"を送信
m.addStringArg( "up" );
//OSC引数として、現在のマウスの座標(x, y)を送信
m.addIntArg( x );
m.addIntArg( y );
sender.sendMessage( m );
}
void testApp::windowResized(int w, int h){}
[/sourcecode]
まず、Pd側のプログラムを起動してDSPをONにした状態で、openFrameworks側のプログラムを実行すると、マウスの位置によって音色が変化するFM合成された音響が生成されます。openFramworks同士の連携と同様、IPアドレスを設定することで、別のマシンから遠隔操作することも可能です。
openFrameworksとPdをOSCを介して組み合わせる方法として、より便利なアドオンofxPdが開発されています。このアドオンを使用すると、Pd側のパッチでは、dumpOSCやOSCrouteなどを使用することなく、シンプルに「r パラメータ名」という形式で値を取得することが可能となります。
また、ofxPdを使用すると、Pdのプログラムを実行することなくopenFrameworksのプログラム内部にPdの音響合成機能を内包することが可能となります。ofxPdでpdのパッチを使用するには、作成したパッチをopenFrameworksのプロジェクトフォルダの「bin/data/」フォルダ内に格納します。
では、ofxPdで使用できるように、先程作成したFM合成のパッチに変更を加えます。変更点は、パッチを開いて200ミリ秒後に自動的に音響合成を開始できるようにしている部分と、パラメータを「dumpOSC」を経由することなく「r」オブジェクトから直接読み込むようにしている部分です。
openFrameworks側では、ofxOSCに加えてofxPdとofxThredをアドオンとしてプロジェクトに追加する必要があります。
アドオンの設定が完了したら、下記のプログラムを作成します。
testApp.h
[sourcecode language=”cpp”]
#ifndef _TEST_APP
#define _TEST_APP
#include "ofMain.h"
#include "ofxPd.h"
#include "ofxOsc.h"
class testApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
void keyPressed (int key);
void keyReleased(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(int x, int y, int button);
void windowResized(int w, int h);
void audioRequested(float * output, int bufferSize, int nChannels);
void audioReceived(float * input, int bufferSize, int nChannels );
float * audioInputData;
// Pdオブジェクト
ofxPd pd;
// Pdと通信するためのOSC
ofxOscSender osc_sender;
};
#endif
[/sourcecode]
testApp.cpp
[sourcecode language=”cpp”]
#include "testApp.h"
void testApp::setup(){
ofSetFrameRate(60);
ofBackground(0, 0, 0);
//OSCの初期化
static const string HOST = "localhost";
static const int PORT = 8000;
osc_sender.setup( "localhost", PORT);
// pdのための定数を定義
// 出力と入力のチャンネル数
static const int NUM_OUT_CHANNELS = 2;
static const int NUM_IN_CHANNELS = 2;
// サンプリングレイト
static const int BITRATE = 44100;
// バッファーサイズ
static const int BUFFER_SIZE = 256;
// 使用するバッファーの数
static const int NUM_BUFFERS = 4;
// Pdのブロックサイズ
static const int PD_BLOCK_SIZE = 64;
// Pdを初期化
pd.setup( "", NUM_OUT_CHANNELS, NUM_IN_CHANNELS, BITRATE, PD_BLOCK_SIZE );
// Pdファイルを読み込み
pd.addOpenFile( "simple_fm.pd" );
// Pdを開始
pd.start();
// オーディオ入力を定義
audioInputData = new float[BUFFER_SIZE*NUM_IN_CHANNELS];
// サウンド出力を初期化
ofSoundStreamSetup( NUM_OUT_CHANNELS, NUM_IN_CHANNELS, this, BITRATE, BUFFER_SIZE, NUM_BUFFERS );
// Pdから音を出力
pd.startDSP();
}
void testApp::update(){}
void testApp::draw(){
ofSetColor(0, 127, 255);
ofCircle(mouseX, mouseY, 20);
}
void testApp::keyPressed (int key){
}
void testApp::keyReleased (int key){
}
void testApp::mouseMoved(int x, int y ){
// マウスの座標をPdのパラメータとして送出
pd.sendFloat( "modulator_freq", x );
pd.sendFloat( "modulator_index", y );
}
void testApp::mouseDragged(int x, int y, int button){}
void testApp::mousePressed(int x, int y, int button){}
void testApp::mouseReleased(int x, int y, int button){}
void testApp::windowResized(int w, int h){}
void testApp::audioRequested(float * output, int bufferSize, int nChannels){
//Pdの音を計算
pd.renderAudio( audioInputData, output, bufferSize, nChannels );
}
void testApp::audioReceived(float * input, int bufferSize, int nChannels){
memcpy(audioInputData, input, bufferSize*nChannels*sizeof(float));
}
[/sourcecode]
openFramwroksのプログラムを実行すると、Pdを起動することなく音響が合成されます。openFrameworksのプログラムの中にPdが内包された状態になっています。このようにopenFrameworks単体に機能を統合することで、実際に作品内で使用する際にセットアップを容易で確実に行うことが可能となるでしょう。
次にopenFrameworksとSuperColliderを連携させてみましょう。
SuperColliderは、言語部分と音響合成部分が分離していて、その相互の連携にOSCを用いています。ですので、その仕組み自体がとてもOSCを用いた他のプログラムとの連携に適しています。具体的には、SuperColliderで楽器を定義する「SynthDef」という関数を使用して、楽器定義ををSuperColliderの音響合成サーバに格納します。あとは、openFrameworksからSuperColliderで行うのと同じ方法で、OSCのメッセージを送出することで音響を合成することが可能となります。
openFrameworksとSuperColliderの連携は、ofxOSCを用いてOSCの素のメッセージを送信しても可能ですが、SuperColliderとの連携に特化したアドオンofxSuperColliderを用いるとより簡単に連携を行うことが可能です。ofxSuperColliderを使用するには、ofxSuperColliderの他に、ofxOscが必要となります。
以下のサンプルは、画面上でマウスをクリックすると、SuperColliderで定義した楽器”newRing”を生成して演奏します。また、生成された音響全体にリバーブをかける楽器”reverb”も同時に用いています。
SuperColliderの楽器定義 (sc_inst.scd)
[sourcecode]
// SynthDef
(
SynthDef("reverb", {
arg wet=1.0;
var in, fx;
in = In.ar(0, 2);
fx = in;
fx = GVerb.ar(fx, 80);
ReplaceOut.ar(0, fx);
}).store;
SynthDef("baseSound", {
arg note=40, amp=0.1, fadein=12.0;
var env, out;
env = EnvGen.kr(Env.new([0, amp], [fadein]));
out = RLPF.ar(LFPulse.ar([note, note+7].midicps, 0.15), SinOsc.kr(0.1, 0, 10, 72).midicps, 0.1, 0.1);
Out.ar(0, out*env);
}).store;
SynthDef("newRing", {
arg note=40, amp=0.5, pan = 0.0, decay=4.0;
var env1, out1, env2, out2, mix;
out1 = RLPF.ar(LFPulse.ar([note, note+7].midicps, 0.15), SinOsc.kr(0.1, 0, 10, 72).midicps, 0.1, 0.1);
out2 = SinOsc.ar([(note+48).midicps, (note+55).midicps]);
env1 = EnvGen.kr(Env.perc(decay/4.0, decay/4.0*3.0, amp, -4), doneAction: 2);
env2 = EnvGen.kr(Env.adsr(0.001, 0.4, 0.0, decay, amp*0.1, -4));
mix = (out1 * env1) + (out2 * env2);
mix = CombN.ar(mix, 0.31, 0.31, 2, 0.5, mix);
Out.ar(0, mix);
}).store;
)
// test
s.sendMsg("/s_new", "reverb", x = s.nextNodeID, 1, 1);
s.sendMsg("/s_new", "baseSound", x = s.nextNodeID, 1, 1);
s.sendMsg("/s_new", "newRing", x = s.nextNodeID, 1, 1, "note", 42);
[/sourcecode]
testApp.h
[sourcecode language=”cpp”]
#ifndef _TEST_APP
#define _TEST_APP
#include "ofMain.h"
#include "ofxSuperCollider.h"
#include "Ring.h"
class testApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
void keyPressed (int key);
void keyReleased(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(int x, int y, int button);
void windowResized(int w, int h);
ofxSCSynth *reverb; // SC楽器 "reverb"
ofxSCSynth *newRing; // SC楽器 "newRing"
ofxSCSynth *baseSound; // SC楽器 "baseSound"
vector <Ring *> rings; //拡大する輪"Ring"の配列
};
#endif
[/sourcecode]
testApp.cpp
[sourcecode language=”cpp”]
#include "testApp.h"
void testApp::setup(){
ofSetFrameRate(60);
ofBackground(0, 0, 0);
reverb = new ofxSCSynth("reverb");
reverb->create();
baseSound = new ofxSCSynth("baseSound");
baseSound->create();
}
void testApp::update(){
//Ring更新
for(vector <Ring *>::iterator it = rings.begin(); it != rings.end();){
(*it)->update();
if ((*it)->dead) {
delete (*it);
it = rings.erase(it);
} else {
++it;
}
}
}
void testApp::draw(){
//Ringを描画
for(vector <Ring *>::iterator it = rings.begin(); it != rings.end(); ++it){
(*it)->draw();
}
}
void testApp::keyPressed(int key){}
void testApp::keyReleased(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(int x, int y, int button){
//newRingの楽器を新規に生成し演奏
int note[8] = {28, 35, 40, 47, 52, 59, 64, 71};
newRing = new ofxSCSynth("newRing");
newRing->set("note", note[int(ofRandom(0, 8))]);
newRing->set("pan", (x – ofGetWidth() / 2.0) / ofGetWidth() * 2.0);
newRing->create();
//Ringを追加
rings.push_back(new Ring(ofPoint(x, y)));
}
void testApp::windowResized(int w, int h){}
[/sourcecode]
Ring.h
[sourcecode language=”cpp”]
#ifndef _RING
#define _RING
#include "ofMain.h"
class Ring {
public:
Ring(ofPoint pos);
void update();
void draw();
ofPoint pos;
float radius;
float radiusSpeed;
bool dead;
private:
};
#endif
[/sourcecode]
Ring.cpp
[sourcecode language=”cpp”]
#include "Ring.h"
Ring::Ring(ofPoint _pos)
{
//初期設定
pos = _pos;
radius = 0;
radiusSpeed = 0.5;
dead = false;
}
void Ring::update()
{
//輪の半径を拡大
radius += radiusSpeed;
//もし画面の幅より半径が大きくなったら、死亡
if (radius > ofGetWidth()) {
dead = true;
}
}
void Ring::draw()
{
//輪を描く
ofSetCircleResolution(64);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
ofEnableSmoothing();
ofSetLineWidth(2);
ofPushMatrix();
ofTranslate(pos.x, pos.y);
ofNoFill();
ofSetColor(63, 127, 255, 200);
ofCircle(0, 0, radius);
ofPopMatrix();
}
[/sourcecode]
このプログラムで音を生成するには、まずSuperColliderを起動し、サーバーを起動します。その上で楽器の定義を選択して「Enter」キーを押して楽器定義をサーバーに格納します。その後、openFrameworks側でプログラムを実行し、画面をクリックすると、拡がる輪とともに、SCで生成した音響が生成されます。
SuperColliderの楽器もまた、openFrameworksのプログラムに内包することが可能です。SuperColliderをopenFrameworksに内包するには、ofxSuperColliderServerというアドオンを使用します。このアドオンは楽器定義ファイルをopenFrameworksのプログラムに直接読み込んでSuperColliderによる音響合成を可能とするとても便利なアドオンです。使用方法もとても簡単で、ofxSuperColliderServerのアドオンを読み込んだ後初期化を行うだけで、あとはofxSuperCllider単体で行った際と同じ方法でプログラムします。
先程のプログラムを書き換えて、openFramework単体で音響合成が可能なようにしてみましょう。
testApp.h
[sourcecode language=”cpp”]
#ifndef _TEST_APP
#define _TEST_APP
#include "ofMain.h"
#include "ofxOsc.h"
#include "Ring.h"
#include "ofxSuperCollider.h"
#include "ofxSuperColliderServer.h"
//ポート番号を設定
#define PORT 8000
//——————————————————–
class testApp : public ofBaseApp{
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(int x, int y, int button);
void windowResized(int w, int h);
void dumpOSC(ofxOscMessage m);
vector <Ring *> rings; //拡大する輪"Ring"の配列
ofxSCSynth *reverb; // SC楽器 "reverb"
ofxSCSynth *newRing; // SC楽器 "newRing"
ofxSCSynth *baseSound; // SC楽器 "baseSound"
private:
ofxOscReceiver receiver;
int remoteMouseX, remoteMouseY;
string mouseButtonState;
string oscString;
};
#endif
[/sourcecode]
testApp.cpp
[sourcecode language=”cpp”]
#include "testApp.h"
void testApp::setup(){
//指定したポートで接続
receiver.setup( PORT );
ofSetFrameRate(60);
//値を初期化
mouseX = 0;
mouseY = 0;
mouseButtonState = "";
ofBackground(0, 0, 0);
ofxSuperColliderServer::init();
reverb = new ofxSCSynth("reverb");
reverb->create();
baseSound = new ofxSCSynth("baseSound");
baseSound->create();
}
void testApp::update(){
//現在順番待ちのOSCメッセージがあるか確認
while( receiver.hasWaitingMessages() )
{
//次のメッセージを取得
ofxOscMessage m;
oscString = m.getAddress();
receiver.getNextMessage( &m );
//マウスの位置を取得
if ( m.getAddress() == "/mouse/position" ){
remoteMouseX = m.getArgAsInt32( 0 );
remoteMouseY = m.getArgAsInt32( 1 );
}
//マウスボタンの状態を取得
else if ( m.getAddress() == "/mouse/button" ) {
mouseButtonState = m.getArgAsString( 0 ) ;
remoteMouseX = m.getArgAsInt32( 1 );
remoteMouseY = m.getArgAsInt32( 2 );
}
//OSCメッセージをそのままコンソールに出力
dumpOSC(m);
}
//マウスアップされたら、新規にRingを追加
if(mouseButtonState == "up"){
rings.push_back(new Ring(ofPoint(remoteMouseX, remoteMouseY)));
mouseButtonState = "";
//SCで音を鳴らす
int note[8] = {28, 35, 40, 47, 52, 59, 64, 71};
newRing = new ofxSCSynth("newRing");
newRing->set("note", note[int(ofRandom(0, 8))]);
newRing->set("pan", (remoteMouseX – ofGetWidth() / 2.0) / ofGetWidth() * 2.0);
newRing->create();
}
//Ring更新
for(vector <Ring *>::iterator it = rings.begin(); it != rings.end();){
(*it)->update();
if ((*it)->dead) {
delete (*it);
it = rings.erase(it);
} else {
++it;
}
}
}
//OSCメッセージをコンソールに出力する関数
void testApp::dumpOSC(ofxOscMessage m) {
string msg_string;
msg_string = m.getAddress();
for (int i=0; i<m.getNumArgs(); i++ ) {
msg_string += " ";
if(m.getArgType(i) == OFXOSC_TYPE_INT32)
msg_string += ofToString( m.getArgAsInt32(i));
else if(m.getArgType(i) == OFXOSC_TYPE_FLOAT)
msg_string += ofToString( m.getArgAsFloat(i));
else if(m.getArgType(i) == OFXOSC_TYPE_STRING)
msg_string += m.getArgAsString(i);
}
cout << msg_string << endl;
}
void testApp::draw(){
//Ringを描画
for(vector <Ring *>::iterator it = rings.begin(); it != rings.end(); ++it){
(*it)->draw();
}
}
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(int x, int y, int button){
rings.push_back(new Ring(ofPoint(x, y)));
int note[8] = {28, 35, 40, 47, 52, 59, 64, 71};
newRing = new ofxSCSynth("newRing");
newRing->set("note", note[int(ofRandom(0, 8))]);
newRing->set("pan", (x – ofGetWidth() / 2.0) / ofGetWidth() * 2.0);
newRing->create();
}
void testApp::windowResized(int w, int h){}
[/sourcecode]
今回紹介した全てのプログラムは、下記からダウンロード可能です。
先週に引き続き、マウスを利用したインタラクションについて取り上げます。
今回は、マウスの位置だけでなく、より詳細なマウスの状態を取得する方法について紹介します。
マウスからの入力は、先週とりあつかったマウスポインタの位置だけでなく、マウスのボタンの状態や対象との関係によって様々な状態を表現することが可能です。ActionScript 3では、マウスの状態をイベントとして受けとることができます。マウスのイベントを処理するイベントリスナーを生成することで、きめ細かなマウスの入力の処理が可能となります。
ActionScript 3で取り扱うことのできるイベントは下記の通りです。
これらのイベントがどのような時に発生するのか、また、これらのイベントを活用することでどのようなことが出来るのかを探っていきましょう。
今日の授業で使用するスクリプトのテンプレートです。まず、AS3形式のFlashファイルを用意し、ドキュメントクラスを「Main」に指定します。その上で下記のコードを新規に作成したActionScriptファイルにペーストして、ファイル名を「Main.as」にしてFlaファイルと同じ場所に保存してください。
[sourcecode language=”as3″]
package {
import flash.display.*;
import flash.events.*;
public class Main extends Sprite {
//ここにグローバルな変数を記入
public function Main() {
//初期設定の項目を記入
//フレーム更新のイベントリスナーの登録
this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
}
//イベントハンドラ
function enterFrameHandler(event:Event):void {
}
}
}
[/sourcecode]
まず初めに、マウスイベントを取得して、trace() 関数を使用して状態を出力するサンプルを作成してみましょう。またいつものように、AS3形式でFLAファイルを作成し、ドキュメントクラスを「Main」にします。その上で同じ場所に「Main.as」というファイル名でActionScript形式のファイルを作成し、下記のスクリプトを記入します。
[sourcecode language=”as3″]
package {
import flash.display.*;
import flash.events.*;
public class Main extends Sprite {
//ここにグローバルな変数を記入
public function Main() {
//初期設定の項目を記入
//マウスイベントリスナー
stage.addEventListener(MouseEvent.CLICK, clickHandler);
stage.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
stage.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
stage.addEventListener(MouseEvent.MOUSE_MOVE, mouseMoveHandler);
}
//クリック
private function clickHandler(event:MouseEvent):void {
trace("clickHandler");
}
//マウスボタンを押した瞬間
private function mouseDownHandler(event:MouseEvent):void {
trace("mouseDownHandler");
}
//マウスボタンを離した瞬間
private function mouseUpHandler(event:MouseEvent):void {
trace("mouseUpHandler");
}
//マウスボタンを動かした瞬間
private function mouseMoveHandler(event:MouseEvent):void {
trace("mouseMoveHandler");
}
}
}
[/sourcecode]
/wp-content/uploads/2010/12/Main1.swf, 550, 400
ムービーを実行すると、マウスの動作に対応して、「出力」画面にメッセージが表示されると思います。マウスボタンを何も押さずにマウスポインタを動かしたときには、MouseEvent.MOUSE_MOVEのメッセージ。マウスボタンを押した瞬間に、MouseEvent.MOUSE_DOWN。そして、押していたマウスボタンを離した瞬間にMouseEvent.MOUSE_UPと、MouseEvent.CLICKが出力されるのがわかります。
先程の例は、ステージ上でのマウスの動作を取得していました。AS3のマウスイベントでは、ステージ上に配置したムービークリップとの関係性を表現するために、MOUSE_DOWNやMOUSE_UPなどの他に、様々なイベントが用意されています。
ムービークリップとの関連を調べるために、ステージ上に「MyButton」というクラス名でリンケージを設定してボタンの図形を描きます。これをステージ中央に配置してマウスでのインタラクションを試してみましょう。
[sourcecode language=”as3″]
package {
import flash.display.*;
import flash.events.*;
public class Main extends Sprite {
//ここにグローバルな変数を記入
var myButton:MyButton = new MyButton();
public function Main() {
//初期設定の項目を記入
//画面中心にボタンを配置
myButton.x=stage.stageWidth/2;
myButton.y=stage.stageHeight/2;
this.addChild(myButton);
//ボタン(myButton)に対するマウスイベントリスナー
myButton.addEventListener(MouseEvent.CLICK, clickHandler);
myButton.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
myButton.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
myButton.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
myButton.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
}
//クリック
private function clickHandler(event:MouseEvent):void {
trace("clickHandler");
}
//マウスボタンを押した瞬間
private function mouseDownHandler(event:MouseEvent):void {
trace("mouseDownHandler");
myButton.scaleX = myButton.scaleY = 0.9;
}
//マウスボタンを離した瞬間
private function mouseUpHandler(event:MouseEvent):void {
trace("mouseUpHandler");
myButton.scaleX = myButton.scaleY = 1.0;
}
//ロールオーバー
private function rollOverHandler(event:MouseEvent):void {
trace("rollOverHandler");
myButton.alpha=0.5;
}
//ロールアウト
private function rollOutHandler(event:MouseEvent):void {
trace("rollOutHandler");
myButton.alpha=1.0;
}
}
}
[/sourcecode]
/wp-content/uploads/2010/12/Main2.swf, 550, 400
この例ではステージ状に配置した、MyButton(インスタンス名はmyButton)というムービークリップシンボルと、マウスの状態を表示しています。注意深く観察すると、MouseEvent.CLICK、MouseEvent.MOUSE_UP、MouseEvent.MOUSE_DOWN、MouseEvent.ROLL_OVER、MouseEvent.ROLL_OUT という合計5つのイベントが、それぞれ独自のタイミングで発生していることがわかると思います。
この中で、MOUSE_UPとCLICKの違いがわかりずらいかもしれません。両者は共にマウスボタンを離した瞬間に発生するのですが、1つだけ違いがあります。対象とするムービークリップの外でマウスボタンを押し(= MouseEvent.MOUSE_DOWN)、そのままボタンを押した状態でムービークリップまで移動してきて、マウスボタンを離します。その際には、CLICKイベントは発生するのですが、MOUSE_UPイベントは発生しません。
では、もう少し高度な例として、ムービークリップをマウスポインタで掴んで移動できるようなインタラクションを作成してみましょう。
マウスで対象のムービークリップを掴んで移動するには、MOUSE_DOWNイベントとMOUSE_UPイベントをうまく使い分ける必要があります。MOUSE_DOWNイベントが発生した際に、物体を掴んだと判断し、MOUSE_UPイベントが発生した瞬間に掴んでいた物体を離したと解釈すると自然なインタラクションとなります。
マウスでムービークリップを移動したように見せる方法は、マウスの座標を取得して常にその座標とムービークリップの座標が一致するようにすることで実現可能ですが、もう少し簡易な方法が存在します。例えば、ムービークリップシンボルが「hoo」だとすると以下のメソッドでマウスのドラッグによる移動の開始/終了を設定可能です。
この機能を活用して、以下のようにプログラムしてみましょう。
[sourcecode language=”as3″]
package {
import flash.display.*;
import flash.events.*;
public class Main extends Sprite {
//ここにグローバルな変数を記入
var myObject:MyObject = new MyObject();
public function Main() {
//初期設定の項目を記入
//画面中心にボタンを配置
myObject.x=stage.stageWidth/2;
myObject.y=stage.stageHeight/2;
this.addChild(myObject);
//フレーム更新のイベントリスナーの登録
this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
//マウスイベントリスナー
myObject.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
myObject.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
myObject.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
myObject.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
}
//イベントハンドラ
function enterFrameHandler(event:Event):void {
}
//マウスボタンを押した瞬間
private function mouseDownHandler(event:MouseEvent):void {
event.target.startDrag();
}
//マウスボタンを離した瞬間
private function mouseUpHandler(event:MouseEvent):void {
event.target.stopDrag();
}
//ロールオーバー
private function rollOverHandler(event:MouseEvent):void {
event.target.alpha=0.5;
}
//ロールアウト
private function rollOutHandler(event:MouseEvent):void {
event.target.alpha=1.0;
}
}
}
[/sourcecode]
/wp-content/uploads/2010/12/Main3.swf, 550, 400
最後に複数の物体をそれぞれマウスで掴んで移動できるような例を紹介します。
目や鼻、口などを画像で作成して、それをマウスで移動可能にすることで「ふくわらい」を作成してみましょう。
[sourcecode language=”as3″]
package {
import flash.display.*;
import flash.events.*;
public class Main extends Sprite {
//ここにグローバルな変数を記入
var eyeR:EyeR = new EyeR();
var eyeL:EyeL = new EyeL();
var nose:Nose = new Nose();
var mouth:Mouth = new Mouth();
public function Main() {
//初期設定の項目を記入
//ランダムに顔のパーツを配置
eyeR.x=Math.random()*stage.stageWidth;
eyeR.y=Math.random()*stage.stageHeight;
this.addChild(eyeR);
eyeL.x=Math.random()*stage.stageWidth;
eyeL.y=Math.random()*stage.stageHeight;
this.addChild(eyeL);
nose.x=Math.random()*stage.stageWidth;
nose.y=Math.random()*stage.stageHeight;
this.addChild(nose);
mouth.x=Math.random()*stage.stageWidth;
mouth.y=Math.random()*stage.stageHeight;
this.addChild(mouth);
//マウスイベントリスナー
eyeR.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
eyeR.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
eyeR.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
eyeR.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
eyeL.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
eyeL.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
eyeL.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
eyeL.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
nose.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
nose.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
nose.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
nose.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
mouth.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
mouth.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler);
mouth.addEventListener(MouseEvent.ROLL_OVER, rollOverHandler);
mouth.addEventListener(MouseEvent.ROLL_OUT, rollOutHandler);
}
//マウスボタンを押した瞬間
private function mouseDownHandler(event:MouseEvent):void {
event.target.startDrag();
}
//マウスボタンを離した瞬間
private function mouseUpHandler(event:MouseEvent):void {
event.target.stopDrag();
}
//ロールオーバー
private function rollOverHandler(event:MouseEvent):void {
event.target.alpha=0.5;
}
//ロールアウト
private function rollOutHandler(event:MouseEvent):void {
event.target.alpha=1.0;
}
}
}
[/sourcecode]
/wp-content/uploads/2010/12/Main4.swf, 550, 400
今回は、CSSを使用して、二段組、三段組みなどページ全体のレイアウトを行う方法、またそれぞれのレイアウトに使用されるパーツの位置や大きさを指定する方法について解説していきます。
今回は、OpenCVという映像解析の技術を応用して、ライブ映像を用いたインタラクティブな表現に挑戦します。
「OpenCV」とは、「Open Computer Vision Library」の略で、オープンソースでコンピュータビジョンの技術を利用可能なライブラリです。米Intel社で開発され、画像処理・画像認識用のC/C++言語のライブラリとして配布されています。商用・非商用を問わず無料で使用することが可能です(BSDライセンス)。
OpenCVのコアとなる技術「コンピュータビジョン」とは、ひとことで言うと「ロボットの目」を作るという研究分野です。「コンピュータビジョン」の実現のために、画像のセンシングのためのハードウェアの研究から情報を認識するための人工知能の研究まで、広範囲な分野の研究が行われています。
OpenCVは、この「コンピュータビジョン」の研究の中でソフトウェアを用いて画像処理を行う用途に使用されています。映像をリアルタムに解析した結果からその構造を解析し、モーション検知や物体追跡、さらには機械学習(顔認識)など、様々な応用が可能となっています。
openFrameworksでは、ofxOpenCvというアドオンを使用することで、簡単にOpenCVの機能を活用することが可能です。
OpenCVによる画像解析を用いてインタラクションを実現したメディアアート作品は、近年、数多く発表されています。ここ数年のメディアアートのメインストリームの一つといっても過言ではないかもしれません。現在活発に情報がやりとりされている「Kinect Hack」に関しても、こうしたリアルタイムの映像解析を用いたインタラクティブアートの一つの進化形とも考えられます。
OpenCVを分析のエンジンとして使用した過去の有名な作品をいくつか紹介します。
Knee Deep – Cinekid 2009 from Theo Watson on Vimeo.
Boards Interactive Magazine – Walkthrough from Theo Watson on Vimeo.
では、実際にOpenCVによる映像解析を用いて、移動する物体を識別する方法について探っていきましょう。OpenCVのライブラリを使用することで比較的簡単に映像ソースから変化した部分だけを識別し、さらにその輪郭を抽出することが可能です。このデータを応用することで、様々なインタラクティブなメディアアート作品を作成していくことが可能となります。まずは、輪郭抽出までの手順を順番に理解していきましょう。
カラーの映像ソースから、移動する物体の輪郭を抽出するまでには、いくつかの手順を踏む必要があります。おおまかな流れは以下のような順番になっています。
この手順に従って、実際にopenFrameworksでプログラミングしていきましょう。
まず、輪郭検出までの手順を、そのまま順番に実装した素直なプログラムを作ってみましょう。
ソース映像や、それぞれの解析過程の映像のためのメモリ領域を確保した上で、ofxOpenCvのメソッドを活用しながら、徐々に輪郭を抽出しています。
openFrameworksからofxOpenCvの機能を使用するには、addonsの中にofxOpenCvを加える必要があります。addonsフォルダ内が下記のようになるようファイルを追加してください。
アドオンが追加できたら、下記のソースを入力します。
test.h
#ifndef _TEST_APP #define _TEST_APP #include "ofMain.h" #include "ofxOpenCv.h" //あらかじめ録画したビデオを使用する場合には、ここをコメントアウト #define _USE_LIVE_VIDEO class testApp : public ofBaseApp { public: void setup(); void update(); void draw(); void keyPressed (int key); void keyReleased(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(int x, int y, int button); void windowResized(int w, int h); #ifdef _USE_LIVE_VIDEO //ライブカメラを使用する際には、カメラ入力を準備 ofVideoGrabber vidGrabber; #else //あらかじめ録画した映像を使用する際には、ビデオプレイヤーを準備 ofVideoPlayer vidPlayer; #endif ofxCvColorImage colorImg; //オリジナルのカラー映像 ofxCvGrayscaleImage grayImage; //グレースケールに変換後 ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像 ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分 ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス bool bLearnBakground; //背景画像を登録したか否か bool showCvAnalysis; //解析結果を表示するか int threshold; //2値化の際の敷居値 int videoMode; //表示する画像を選択 }; #endif
test.cpp
#include "testApp.h" void testApp::setup(){ //画面の基本設定 ofBackground(0, 0, 0); ofEnableAlphaBlending(); ofSetFrameRate(60); //カメラ使用の場合 #ifdef _USE_LIVE_VIDEO //カメラから映像を取り込んで表示 vidGrabber.setVerbose(true); vidGrabber.initGrabber(320,240); #else //カメラ不使用の場合ムービーファイルを読み込んで再生 vidPlayer.loadMovie("fingers.mov"); vidPlayer.play(); #endif //使用する画像の領域を確保 colorImg.allocate(320,240); grayImage.allocate(320,240); grayBg.allocate(320,240); grayDiff.allocate(320,240); //変数の初期化 bLearnBakground = true; showCvAnalysis = false; threshold = 20; videoMode = 0; } void testApp::update(){ //新規フレームの取り込みをリセット bool bNewFrame = false; #ifdef _USE_LIVE_VIDEO //カメラ使用の場合はカメラから新規フレーム取り込み vidGrabber.grabFrame(); //新規にフレームが切り替わったか判定 bNewFrame = vidGrabber.isFrameNew(); #else //カメラ不使用の場合は、ビデオプレーヤーから新規フレーム取り込み vidPlayer.idleMovie(); //新規にフレームが切り替わったか判定 bNewFrame = vidPlayer.isFrameNew(); #endif //フレームが切り替わった際のみ画像を解析 if (bNewFrame){ #ifdef _USE_LIVE_VIDEO //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidGrabber.getPixels(), 320,240); //左右反転 colorImg.mirror(false, true); #else //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidPlayer.getPixels(), 320,240); #endif //カラーのイメージをグレースケールに変換 grayImage = colorImg; //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする if (bLearnBakground == true){ grayBg = grayImage; bLearnBakground = false; } //グレースケールのイメージと取り込んだ背景画像との差分を算出 grayDiff.absDiff(grayBg, grayImage); //画像を2値化(白と黒だけに)する grayDiff.threshold(threshold); //2値化した画像から輪郭を抽出する contourFinder.findContours(grayDiff, 25, grayDiff.width * grayDiff.height, 10, false, false); } } void testApp::draw(){ //現在のモードに応じて、表示する映像を切り替え switch (videoMode) { case 1: //グレースケール映像 grayImage.draw(0, 0, ofGetWidth(), ofGetHeight()); break; case 2: //背景画像 grayBg.draw(0, 0, ofGetWidth(), ofGetHeight()); break; case 3: //2値化された差分映像 grayDiff.draw(0, 0, ofGetWidth(), ofGetHeight()); break; default: //カラー映像 colorImg.draw(0, 0, ofGetWidth(), ofGetHeight()); break; } //画面に対する映像の比率を計算 float ratioX = ofGetWidth()/320; float ratioY = ofGetHeight()/240; //解析結果を表示する場合 if (showCvAnalysis) { //検出した解析結果を表示 for (int i = 0; i < contourFinder.nBlobs; i++){ ofPushMatrix(); //画面サイズいっぱいに表示されるようリスケール glScalef((float)ofGetWidth() / (float)grayDiff.width, (float)ofGetHeight() / (float)grayDiff.height, 1.0f); contourFinder.blobs[i].draw(0,0); ofFill(); ofSetColor(255, 0, 0); ofEllipse(contourFinder.blobs[i].centroid.x, contourFinder.blobs[i].centroid.y, 4, 4); ofPopMatrix(); } } //ログと操作説明を表示 ofSetColor(255, 255, 255); ofDrawBitmapString("FPS: "+ofToString(ofGetFrameRate()), 20, 20); ofDrawBitmapString("Threshold: "+ofToString(threshold), 20, 35); ofDrawBitmapString("Number of Blobs: "+ofToString(contourFinder.nBlobs), 20, 50); ofDrawBitmapString("[0] Show original video", 20, 65); ofDrawBitmapString("[1] Show grayscale video", 20, 80); ofDrawBitmapString("[2] Show captured background", 20, 95); ofDrawBitmapString("[3] Show difference from background", 20, 110); ofDrawBitmapString("[space] Captuer background", 20, 125); ofDrawBitmapString("[a] Analysis on / off", 20, 140); } void testApp::keyPressed (int key){ //キー入力でモード切り替え switch (key){ case '0': //カラー映像表示 videoMode = 0; break; case '1': //グレースケール映像表示 videoMode = 1; break; case '2': //背景画像表示 videoMode = 2; break; case '3': //2値化した差分映像 videoMode = 3; break; case 'a': //解析結果の表示の on / off showCvAnalysis ? showCvAnalysis=false : showCvAnalysis=true; break; case 'f': //フルスクリーンに ofSetFullscreen(true); break; case ' ': //背景画像を新規に取り込む bLearnBakground = true; break; case '+': //2値化の閾値を増加 threshold ++; if (threshold > 255) threshold = 255; break; case '-': //2値化の閾値を減少 threshold --; if (threshold < 0) threshold = 0; break; } } void testApp::keyReleased(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(int x, int y, int button){ } void testApp::windowResized(int w, int h){ }
このプログラムはキーボード入力によってモードを切り替えられる仕組みになっています。実際にキー入力をしてみて、処理の過程を実感してみましょう。
映像からモーションを検出し輪郭を抽出するためには、様々な画像処理が行われています。実際には処理を行う対象、周囲の環境、カメラの性能、明るさなど様々な要因の影響を受けるため、実際に分析結果をみながら細かくパラメータを調整していく必要があります。パラメータには下記のようなものがあります。
これらの値を、実際に検出結果を確かめながら適切にチューニングすることで、より正確な検出や計算量の軽減などを行うことが可能となります。
次に、この輪郭抽出のプログラムをより実用的なものにしていきたいと思います。先程解説したように、正確な輪郭の抽出や、計算量を抑えた分析のためには、適切なパラメータのチューニングが必要となります。しかし、調整の際に毎回プログラムを修正してビルドをやりなおすのは効率が良くありません。
このように、状況に応じて細かなパラメータを調整した際に便利なアドオンを紹介します。ofxSimpleGuiTooというアドオンは、プログラム内で用いられているパラメータやbool値のtrue/falseの切り替えなどを、簡単なGUIを通してプログラムを作動させながらリアルタイムに行うことが可能です。さらに、分析中の画像を複数枚同時に小窓の中に表示することができるので、今回のようなOpenCVを用いた画像解析のプログラムにはうってつけです。
パラメータを設定した結果は、最終的にXML形式の外部ファイルとして保存され、次回プログラムを起動した際には調整したパラメータを再度読み込んで前回の状態を復元する機能もあります。この機能を活用することで、展示する場所の状況を記録しおくことが可能となり、作品制作の際の強力なサポートとなるでしょう。
ofxSimpleGuiTooを使用するためには、同時に付随するアドオンをいくつか読み込む必要があります。下記にofxSimpleGuiTooを作動させるためのアドオンを列挙します。
これに加えて、ofxOpenCvも必要となるので、「グループとファイル」の中のaddonsフォルダは下記のようになります。
アドオンの設定が完了したら、以下のプログラムを入力します。
testApp.h
#ifndef _TEST_APP #define _TEST_APP #include "ofMain.h" #include "ofxOpenCv.h" #include "ofxSimpleGuiToo.h" //あらかじめ録画したビデオを使用する場合には、ここをコメントアウト //#define _USE_LIVE_VIDEO class testApp : public ofBaseApp { public: void setup(); void update(); void draw(); void keyPressed (int key); void keyReleased(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(int x, int y, int button); void windowResized(int w, int h); //ライブカメラを使用する際には、カメラ入力を準備 ofVideoGrabber vidGrabber; ofVideoPlayer vidPlayer; ofxCvColorImage colorImg; //オリジナルのカラー映像 ofxCvGrayscaleImage grayImage; //グレースケールに変換後 ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像 ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分 ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス bool bLearnBakground; //背景画像を登録したか否か bool showCvAnalysis; //解析結果を表示するか int threshold; //2値化の際の閾値 bool liveVideo; //カメラ入力を使用するか否か //輪郭検出時のパラメータ float minBlobSize; //物体の最小サイズ float maxBlobSize; //物体の最大サイズ int maxNumBlobs; //認識する物体の最大数 bool findHoles; //穴を検出するか bool useApproximation; //近似値を使用するか //GUI ofxSimpleGuiToo gui; }; #endif
testApp.cpp
#include "testApp.h" void testApp::setup(){ //画面の基本設定 ofBackground(0, 0, 0); ofEnableAlphaBlending(); ofSetFrameRate(60); //カメラから映像を取り込んで表示 vidGrabber.setVerbose(true); vidGrabber.initGrabber(320,240); vidPlayer.loadMovie("fingers.mov"); vidPlayer.play(); //使用する画像の領域を確保 colorImg.allocate(320,240); grayImage.allocate(320,240); grayBg.allocate(320,240); grayDiff.allocate(320,240); //変数の初期化 bLearnBakground = false; showCvAnalysis = false; //GUIを設定 gui.setup(); gui.config->gridSize.x = 250; gui.addContent("grayImage", grayImage); //グレーに変換した映像 gui.addContent("grayBg", grayBg); //キャプチャーした背景 gui.addContent("grayDiff", grayDiff); //2値化した差分画像 gui.addFPSCounter(); //FPS gui.addSlider("threshold", threshold, 0, 400); //2値化の閾値 gui.addToggle("use live video", liveVideo); //カメラを使うかどうか gui.addToggle("findHoles", findHoles); //穴を検出するか gui.addToggle("useApproximation", useApproximation); //近似法を使うか gui.addSlider("minBlobSize", minBlobSize, 0, 1); //検出する物体の最小サイズ gui.addSlider("maxBlobSize", maxBlobSize, 0, 1); //検出する物体の最大サイズ gui.addSlider("maxNumBlobs", maxNumBlobs, 1, 100); //検出する物体の最大数 gui.loadFromXML(); } void testApp::update(){ //新規フレームの取り込みをリセット bool bNewFrame = false; if (liveVideo) { //カメラ使用の場合はカメラから新規フレーム取り込み vidGrabber.grabFrame(); //新規にフレームが切り替わったか判定 bNewFrame = vidGrabber.isFrameNew(); } else { vidPlayer.idleMovie(); //新規にフレームが切り替わったか判定 bNewFrame = vidPlayer.isFrameNew(); } //フレームが切り替わった際のみ画像を解析 if (bNewFrame){ if (liveVideo) { //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidGrabber.getPixels(), 320,240); //左右反転 colorImg.mirror(false, true); } else { //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidPlayer.getPixels(), 320,240); } //カラーのイメージをグレースケールに変換 grayImage = colorImg; //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする if (bLearnBakground == true){ grayBg = grayImage; bLearnBakground = false; } //グレースケールのイメージと取り込んだ背景画像との差分を算出 grayDiff.absDiff(grayBg, grayImage); //画像を2値化(白と黒だけに)する grayDiff.threshold(threshold); //2値化した画像から輪郭を抽出する contourFinder.findContours(grayDiff, minBlobSize * minBlobSize * grayDiff.getWidth() * grayDiff.getHeight(), maxBlobSize * maxBlobSize * grayDiff.getWidth() * grayDiff.getHeight(), maxNumBlobs, findHoles, useApproximation); } } void testApp::draw(){ //解析結果を表示 contourFinder.draw(0, 0, ofGetWidth(), ofGetHeight()); //GUIを表示 gui.draw(); } void testApp::keyPressed (int key){ //キー入力でモード切り替え switch (key){ case 'f': //フルスクリーンのon/off ofToggleFullscreen(); case 'g': //Gui表示のon/off gui.toggleDraw(); break; case ' ': //背景画像を新規に取り込む bLearnBakground = true; break; } } void testApp::keyReleased(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(int x, int y, int button){ } void testApp::windowResized(int w, int h){ }
[g]キーでGUIの表示/非表示の切り替えができます。GUIを表示した解析のパラメータを細かく設定し、完了したらGUIを隠して使用します。
では次に、この輪郭を検出した物体の情報を活用してみましょう。検出した物体(Blob)は、その図形をとりかこむ長方形の座標と重心点の座標を取得することが可能です。この重心の点を利用して、そこからパーティクルが湧き出てくるような表現に挑戦してみましょう。
パーティクル表現の基本原理は、以前の講義「openFrameworksで、オブジェクト指向プログラミング(OOP) 後編」で作成したパーティクルを活用しています。
testApp.h
#ifndef _TEST_APP #define _TEST_APP #include "ofMain.h" #include "MyCircle.h" #include "ofxOpenCv.h" #include "ofxSimpleGuiToo.h" class testApp : public ofBaseApp { public: void setup(); void update(); void draw(); void keyPressed (int key); void keyReleased(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(int x, int y, int button); void windowResized(int w, int h); //ライブカメラを使用する際には、カメラ入力を準備 ofVideoGrabber vidGrabber; ofVideoPlayer vidPlayer; ofxCvColorImage colorImg; //オリジナルのカラー映像 ofxCvGrayscaleImage grayImage; //グレースケールに変換後 ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像 ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分 ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス bool bLearnBakground; //背景画像を登録したか否か bool showCvAnalysis; //解析結果を表示するか int threshold; //2値化の際の敷居値 //輪郭検出時のパラメータ float minBlobSize; float maxBlobSize; int maxNumBlobs; bool findHoles; bool useApproximation; bool liveVideo; //GUI ofxSimpleGuiToo gui; //パーティクルの動的配列 vectorcircles; }; #endif
testApp.cpp
#include "testApp.h" void testApp::setup(){ //画面の基本設定 ofBackground(0, 0, 0); ofEnableAlphaBlending(); ofSetFrameRate(60); //カメラから映像を取り込んで表示 vidGrabber.setVerbose(true); vidGrabber.initGrabber(320,240); vidPlayer.loadMovie("fingers.mov"); vidPlayer.play(); //使用する画像の領域を確保 colorImg.allocate(320,240); grayImage.allocate(320,240); grayBg.allocate(320,240); grayDiff.allocate(320,240); //変数の初期化 bLearnBakground = false; showCvAnalysis = false; //GUIを設定 gui.setup(); gui.config->gridSize.x = 250; gui.addContent("grayImage", grayImage); gui.addContent("grayDiff", grayDiff); gui.addContent("findConter", contourFinder); gui.addFPSCounter(); gui.addSlider("threshold", threshold, 0, 400); gui.addToggle("use live video", liveVideo); gui.addToggle("findHoles", findHoles); gui.addToggle("useApproximation", useApproximation); gui.addSlider("minBlobSize", minBlobSize, 0, 1); gui.addSlider("maxBlobSize", maxBlobSize, 0, 1); gui.addSlider("maxNumBlobs", maxNumBlobs, 1, 100); gui.loadFromXML(); } void testApp::update(){ //新規フレームの取り込みをリセット bool bNewFrame = false; if (liveVideo) { //カメラ使用の場合はカメラから新規フレーム取り込み vidGrabber.grabFrame(); //新規にフレームが切り替わったか判定 bNewFrame = vidGrabber.isFrameNew(); } else { vidPlayer.idleMovie(); //新規にフレームが切り替わったか判定 bNewFrame = vidPlayer.isFrameNew(); } //フレームが切り替わった際のみ画像を解析 if (bNewFrame){ if (liveVideo) { //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidGrabber.getPixels(), 320,240); //左右反転 colorImg.mirror(false, true); } else { //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidPlayer.getPixels(), 320,240); } //カラーのイメージをグレースケールに変換 grayImage = colorImg; //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする if (bLearnBakground == true){ grayBg = grayImage; bLearnBakground = false; } //グレースケールのイメージと取り込んだ背景画像との差分を算出 grayDiff.absDiff(grayBg, grayImage); //画像を2値化(白と黒だけに)する grayDiff.threshold(threshold); //2値化した画像から輪郭を抽出する contourFinder.findContours(grayDiff, minBlobSize * minBlobSize * grayDiff.getWidth() * grayDiff.getHeight(), maxBlobSize * maxBlobSize * grayDiff.getWidth() * grayDiff.getHeight(), maxNumBlobs, findHoles, useApproximation); //検出した解析結果からBlobの中心位置を求め //中心位置にパーティクルを追加 for (int i = 0; i < contourFinder.nBlobs; i++){ circles.push_back(new MyCircle(ofPoint(contourFinder.blobs[i].centroid.x, contourFinder.blobs[i].centroid.y), ofRandom(0.5, 4.0), 0.4, 0.1, 10)); } } //パーティクル更新 for(vector::iterator it = circles.begin(); it != circles.end();){ (*it)->update(); if ((*it)->dead) { delete (*it); it = circles.erase(it); } else { ++it; } } } void testApp::draw(){ ofFill(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE); ofPushMatrix(); //画面サイズいっぱいに表示されるようリスケール glScalef((float)ofGetWidth() / (float)grayDiff.width, (float)ofGetHeight() / (float)grayDiff.height, 1.0f); //ビデオ映像表示 ofSetColor(255, 255, 255, 127); colorImg.draw(0, 0); //パーティクルを表示 for(vector ::iterator it = circles.begin(); it != circles.end(); ++it){ (*it)->draw(); } ofPopMatrix(); //GUIを表示 gui.draw(); } void testApp::keyPressed (int key){ //キー入力でモード切り替え switch (key){ case 'f': //フルスクリーンのon/off ofToggleFullscreen(); case 'g': //Gui表示のon/off gui.toggleDraw(); break; case ' ': //背景画像を新規に取り込む bLearnBakground = true; break; } } void testApp::keyReleased(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(int x, int y, int button){ } void testApp::windowResized(int w, int h){ }
CustomCircle.h
#ifndef _MY_CIRCLE #define _MY_CIRCLE #include "ofMain.h" class MyCircle { public: //コンストラクタ MyCircle(ofPoint pos, float radius, float maxSpeed = 0.4, float phaseSpeed = 0.06, float lifeLength = 20.0); //デストラクタ ~MyCircle(); //半径の更新 void update(); //円を描く void draw(); //アクセサ void setPos(ofPoint pos); void setRadius(float radius); //円の移動スピード ofPoint speed; //移動スピードの最大値 float maxSpeed; //収縮を制御する正弦波の位相 float phase; //収縮スピード float phaseSpeed; //生死の判定 bool dead; //寿命 float lifeLength; private: //プロパティはprivateで宣言 //円の位置 ofPoint pos; //円の半径 float radius; }; #endif
CustomCircle.cpp
#include "MyCircle.h" //コンストラクタ MyCircle::MyCircle(ofPoint _pos, float _radius, float _maxSpeed, float _phaseSpeed, float _lifeLength) { pos = _pos; radius = _radius; phaseSpeed = _phaseSpeed; maxSpeed = _maxSpeed; lifeLength = _lifeLength; //スピードを設定 speed.x = ofRandom(-maxSpeed, maxSpeed); speed.y = ofRandom(-maxSpeed, maxSpeed); //初期位相 phase = ofRandom(0, PI*2); //生死 dead = false; } //デストラクタ MyCircle::~MyCircle(){} void MyCircle::update() { //円の半径の伸び縮みの位相を更新 phase += phaseSpeed; if (phase > lifeLength) { dead = true; } //位置を更新 pos += speed; } void MyCircle::draw() { //パーティクルを描く float r = sin(phase)*radius; ofSetColor(127, 255, 255, 11); ofCircle(pos.x, pos.y, radius*2.0); ofSetColor(31, 127, 255, 127); ofCircle(pos.x, pos.y, r); ofSetColor(255, 255, 255); ofCircle(pos.x, pos.y, r*0.25); } void MyCircle::setPos(ofPoint _pos) { pos = _pos; } void MyCircle::setRadius(float _radius) { radius = _radius; }
次に、単純な座標の情報だけを活用するのではなく、映像全体の動きや物体の移動する流れにより密接に絡みあった表現を追求していきたと思います。
ここで、ベクトル場(Vector Field)という概念を導入したいと思います。ベクトル場とは、空間内のベクトル的な量の分布を表したものです。例えば、流体の速さと向きや、磁力や重力の強さと向きなどが空間内にどのように分布しているかを表現するために用いられます。
OpenCVで解析した映像の変化を、このベクトル場としてとらえ、空間内でのベクトルの量と向きに変換してみます。すると、単純な座標ではなく映像の動きを流れとして感じさせるような表現が可能となってきます。
このプログラムのvectorFieldクラスは、映像の差分情報からベクトル場を計算しています。画面全体のベクトル場を算出して、その中にパーティクルをランダムに配置し、ベクトル場の流れをパーティクル一つ一つに力として伝えています。その結果、パーティクルは映像の動きに押し流されるように、空間内を漂います。
testApp.h
#ifndef _TEST_APP #define _TEST_APP #include "ofMain.h" #include "particle.h" #include "vectorField.h" #include "ofxOpenCv.h" #include "ofxSimpleGuiToo.h" //あらかじめ録画したビデオを使用する場合には、ここをコメントアウト //#define _USE_LIVE_VIDEO class testApp : public ofBaseApp { public: void setup(); void update(); void draw(); void keyPressed (int key); void keyReleased(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(int x, int y, int button); void windowResized(int w, int h); //ライブカメラを使用する際には、カメラ入力を準備 ofVideoGrabber vidGrabber; ofVideoPlayer vidPlayer; ofxCvColorImage colorImg; //オリジナルのカラー映像 ofxCvGrayscaleImage grayImage; //グレースケールに変換後 ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像 ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分 ofxCvGrayscaleImage grayDiffSmall; //縮小した差分イメージ ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス bool bLearnBakground; //背景画像を登録したか否か bool showCvAnalysis; //解析結果を表示するか int threshold; //2値化の際の敷居値 bool liveVideo; //カメラ入力を使用するか bool drawColorImg; //ビデオ表示のon/off bool drawVectorFirld; //VectorField表示のon/off bool bForceInward; //重力の向き //GUI ofxSimpleGuiToo gui; //ベクトル場 vectorField VF; //パーティクル vectorparticles; int particleNum; }; #endif
testApp.cpp
#include "testApp.h" void testApp::setup(){ //画面の基本設定 ofBackground(0, 0, 0); ofSetFrameRate(60); //カメラから映像を取り込んで表示 vidGrabber.setVerbose(true); vidGrabber.initGrabber(320,240); vidPlayer.loadMovie("fingers.mov"); vidPlayer.play(); //使用する画像の領域を確保 colorImg.allocate(320,240); grayImage.allocate(320,240); grayBg.allocate(320,240); grayDiff.allocate(320,240); grayDiffSmall.allocate(60, 40); //変数の初期化 bLearnBakground = true; showCvAnalysis = false; //ベクトル場の初期化 VF.setupField(60, 40, ofGetWidth(), ofGetHeight()); //パーティクル生成 particleNum = 8000; for (int i = 0; i < particleNum; i++) { particle* p = new particle(); p->setInitialCondition(ofRandom(0,ofGetWidth()),ofRandom(0,ofGetHeight()),0,0); particles.push_back(p); } //GUIの初期設定 gui.setup(); gui.config->gridSize.x = 250; gui.addContent("grayImage", grayImage); gui.addContent("grayDiff", grayDiff); gui.addContent("grayDiffSmall", grayDiffSmall); gui.addFPSCounter(); gui.addSlider("threshold", threshold, 0, 400); gui.addToggle("use live video", liveVideo); gui.addToggle("draw video", drawColorImg); gui.addToggle("draw vector field", drawVectorFirld); gui.addToggle("force inward", bForceInward); gui.loadFromXML(); } void testApp::update(){ //新規フレームの取り込みをリセット bool bNewFrame = false; if (liveVideo) { vidPlayer.stop(); //カメラ使用の場合はカメラから新規フレーム取り込み vidGrabber.grabFrame(); //新規にフレームが切り替わったか判定 bNewFrame = vidGrabber.isFrameNew(); } else { vidPlayer.play(); vidPlayer.idleMovie(); //新規にフレームが切り替わったか判定 bNewFrame = vidPlayer.isFrameNew(); } //フレームが切り替わった際のみ画像を解析 if (bNewFrame){ if (liveVideo) { //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidGrabber.getPixels(), 320,240); //左右反転 colorImg.mirror(false, true); } else { //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidPlayer.getPixels(), 320,240); } //カラーのイメージをグレースケールに変換 grayImage = colorImg; //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする if (bLearnBakground == true){ grayBg = grayImage; bLearnBakground = false; } //グレースケールのイメージと取り込んだ背景画像との差分を算出 grayDiff.absDiff(grayBg, grayImage); //画像を2値化(白と黒だけに)する grayDiff.threshold(threshold); //縮小した差分イメージに画像をコピー grayDiffSmall.scaleIntoMe(grayDiff); //ぼかし grayDiffSmall.blur(5); //ベクトル場に差分イメージを適用 VF.setFromPixels(grayDiffSmall.getPixels(), bForceInward, 0.05f); } //ベクトル場に発生した力を計算し、パーティクルにかかる力を算出 for(vector::iterator it = particles.begin(); it != particles.end(); ++it){ (*it)->resetForce(); ofxVec2f frc; frc = VF.getForceFromPos((*it)->pos.x, (*it)->pos.y); (*it)->addForce(frc.x, frc.y); (*it)->addDampingForce(); (*it)->update(); } } void testApp::draw(){ //ofEnableAlphaBlending(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE); if (drawColorImg) { //カラーイメージを描く ofSetColor(255, 255, 255, 63); colorImg.draw(0, 0, ofGetWidth(), ofGetHeight()); } if (drawVectorFirld) { //ベクトル場を描く ofSetColor(255, 255, 255, 127); VF.draw(); } //パーティクルを描く ofFill(); ofSetColor(63, 127, 255, 200); for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){ (*it)->draw(); } //GUIを描く gui.draw(); } void testApp::keyPressed (int key){ //キー入力でモード切り替え switch (key){ case 'f': //フルスクリーンのon/off ofToggleFullscreen(); case 'g': //Gui表示のon/off gui.toggleDraw(); break; case 't': //重力を反転 bForceInward = !bForceInward; break; case ' ': //背景画像を新規に取り込む bLearnBakground = true; //particleをリセット for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){ (*it)->setInitialCondition(ofRandom(0,ofGetWidth()), ofRandom(0,ofGetHeight()), 0,0); } break; } } void testApp::keyReleased(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(int x, int y, int button){ } void testApp::windowResized(int w, int h){ }
vectorField.h
#include "testApp.h" void testApp::setup(){ //画面の基本設定 ofBackground(0, 0, 0); ofSetFrameRate(60); //カメラから映像を取り込んで表示 vidGrabber.setVerbose(true); vidGrabber.initGrabber(320,240); vidPlayer.loadMovie("fingers.mov"); vidPlayer.play(); //使用する画像の領域を確保 colorImg.allocate(320,240); grayImage.allocate(320,240); grayBg.allocate(320,240); grayDiff.allocate(320,240); grayDiffSmall.allocate(60, 40); //変数の初期化 bLearnBakground = true; showCvAnalysis = false; //ベクトル場の初期化 VF.setupField(60, 40, ofGetWidth(), ofGetHeight()); //パーティクル生成 particleNum = 8000; for (int i = 0; i < particleNum; i++) { particle* p = new particle(); p->setInitialCondition(ofRandom(0,ofGetWidth()),ofRandom(0,ofGetHeight()),0,0); particles.push_back(p); } //GUIの初期設定 gui.setup(); gui.config->gridSize.x = 250; gui.addContent("grayImage", grayImage); gui.addContent("grayDiff", grayDiff); gui.addContent("grayDiffSmall", grayDiffSmall); gui.addFPSCounter(); gui.addSlider("threshold", threshold, 0, 400); gui.addToggle("use live video", liveVideo); gui.addToggle("draw video", drawColorImg); gui.addToggle("draw vector field", drawVectorFirld); gui.addToggle("force inward", bForceInward); gui.loadFromXML(); } void testApp::update(){ //新規フレームの取り込みをリセット bool bNewFrame = false; if (liveVideo) { vidPlayer.stop(); //カメラ使用の場合はカメラから新規フレーム取り込み vidGrabber.grabFrame(); //新規にフレームが切り替わったか判定 bNewFrame = vidGrabber.isFrameNew(); } else { vidPlayer.play(); vidPlayer.idleMovie(); //新規にフレームが切り替わったか判定 bNewFrame = vidPlayer.isFrameNew(); } //フレームが切り替わった際のみ画像を解析 if (bNewFrame){ if (liveVideo) { //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidGrabber.getPixels(), 320,240); //左右反転 colorImg.mirror(false, true); } else { //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidPlayer.getPixels(), 320,240); } //カラーのイメージをグレースケールに変換 grayImage = colorImg; //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする if (bLearnBakground == true){ grayBg = grayImage; bLearnBakground = false; } //グレースケールのイメージと取り込んだ背景画像との差分を算出 grayDiff.absDiff(grayBg, grayImage); //画像を2値化(白と黒だけに)する grayDiff.threshold(threshold); //縮小した差分イメージに画像をコピー grayDiffSmall.scaleIntoMe(grayDiff); //ぼかし grayDiffSmall.blur(5); //ベクトル場に差分イメージを適用 VF.setFromPixels(grayDiffSmall.getPixels(), bForceInward, 0.05f); } //ベクトル場に発生した力を計算し、パーティクルにかかる力を算出 for(vector::iterator it = particles.begin(); it != particles.end(); ++it){ (*it)->resetForce(); ofxVec2f frc; frc = VF.getForceFromPos((*it)->pos.x, (*it)->pos.y); (*it)->addForce(frc.x, frc.y); (*it)->addDampingForce(); (*it)->update(); } } void testApp::draw(){ //ofEnableAlphaBlending(); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE); if (drawColorImg) { //カラーイメージを描く ofSetColor(255, 255, 255, 63); colorImg.draw(0, 0, ofGetWidth(), ofGetHeight()); } if (drawVectorFirld) { //ベクトル場を描く ofSetColor(255, 255, 255, 127); VF.draw(); } //パーティクルを描く ofFill(); ofSetColor(63, 127, 255, 200); for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){ (*it)->draw(); } //GUIを描く gui.draw(); } void testApp::keyPressed (int key){ //キー入力でモード切り替え switch (key){ case 'f': //フルスクリーンのon/off ofToggleFullscreen(); case 'g': //Gui表示のon/off gui.toggleDraw(); break; case 't': //重力を反転 bForceInward = !bForceInward; break; case ' ': //背景画像を新規に取り込む bLearnBakground = true; //particleをリセット for(vector ::iterator it = particles.begin(); it != particles.end(); ++it){ (*it)->setInitialCondition(ofRandom(0,ofGetWidth()), ofRandom(0,ofGetHeight()), 0,0); } break; } } void testApp::keyReleased(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(int x, int y, int button){ } void testApp::windowResized(int w, int h){ }
vectorField.cpp
#include "vectorField.h" //------------------------------------------------------------------------------------ vectorField::vectorField(){ /*fieldWidth = 60; fieldHeight = 40; */ } //------------------------------------------------------------------------------------ void vectorField::setupField(int innerW, int innerH, int outerW, int outerH){ fieldWidth = innerW; fieldHeight = innerH; externalWidth = outerW; externalHeight = outerH; field.clear(); fieldSize = fieldWidth * fieldHeight; for (int i = 0; i < fieldSize; i++){ ofxVec2f pt; pt.set(0,0); field.push_back(pt); } } //------------------------------------------------------------------------------------ void vectorField::clear(){ for (int i = 0; i < fieldSize; i++){ field[i].set(0,0); } } //------------------------------------------------------------------------------------ void vectorField::fadeField(float fadeAmount){ for (int i = 0; i < fieldSize; i++){ field[i].set(field[i].x*fadeAmount,field[i].y*fadeAmount); } } //------------------------------------------------------------------------------------ void vectorField::randomizeField(float scale){ for (int i = 0; i < fieldSize; i++){ // random between -1 and 1 float x = (float)(ofRandom(-1,1)) * scale; float y = (float)(ofRandom(-1,1)) * scale; field[i].set(x,y); } } //------------------------------------------------------------------------------------ void vectorField::draw(){ float scalex = (float)externalWidth / (float)fieldWidth; float scaley = (float)externalHeight / (float)fieldHeight; for (int i = 0; i < fieldWidth; i++){ for (int j = 0; j < fieldHeight; j++){ // pos in array int pos = j * fieldWidth + i; // pos externally float px = i * scalex; float py = j * scaley; float px2 = px + field[pos].x * 5; float py2 = py + field[pos].y * 5; ofLine(px,py, px2, py2); // draw an baseline to show direction // get the line as vector, calculate length, then normalize. // rotate and draw based on length ofxVec2f line; line.set(px2-px, py2-py); float length = line.length(); line.normalize(); line.rotate(90); // these are angles in degrees ofLine(px - line.x*length*0.2, py - line.y*length*0.2, px + line.x*length*0.2, py + line.y*length*0.2); } } } //------------------------------------------------------------------------------------ ofxVec2f vectorField::getForceFromPos(float xpos, float ypos){ ofxVec2f frc; frc.set(0,0); // convert xpos and ypos into pcts = float xPct = xpos / (float)externalWidth; float yPct = ypos / (float)externalHeight; // if we are less then 0 or greater then 1 in x or y, return no force. if ((xPct < 0 || xPct > 1) || (yPct < 0 || yPct > 1)){ return frc; } // where are we in the array int fieldPosX = (int)(xPct * fieldWidth); int fieldPosY = (int)(yPct * fieldHeight); // saftey :) fieldPosX = MAX(0, MIN(fieldPosX, fieldWidth-1)); fieldPosY = MAX(0, MIN(fieldPosY, fieldHeight-1)); // pos in array int pos = fieldPosY * fieldWidth + fieldPosX; frc.set(field[pos].x * 0.1, field[pos].y * 0.1 ); // scale here as values are pretty large. return frc; } //------------------------------------------------------------------------------------ void vectorField::addInwardCircle(float x, float y, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; float unit_px = ( fieldPosX - i) / distance; float unit_py = ( fieldPosY - j) / distance; field[pos].x += unit_px * strongness; field[pos].y += unit_py * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::addOutwardCircle(float x, float y, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; float unit_px = ( fieldPosX - i) / distance; float unit_py = ( fieldPosY - j) / distance; field[pos].x -= unit_px * strongness; field[pos].y -= unit_py * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::addClockwiseCircle(float x, float y, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; float unit_px = ( fieldPosX - i) / distance; float unit_py = ( fieldPosY - j) / distance; field[pos].x += unit_py * strongness; /// Note: px and py switched, for perpendicular field[pos].y -= unit_px * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::addCounterClockwiseCircle(float x, float y, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; float unit_px = ( fieldPosX - i) / distance; float unit_py = ( fieldPosY - j) / distance; field[pos].x -= unit_py * strongness; /// Note: px and py switched, for perpendicular field[pos].y += unit_px * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::addVectorCircle(float x, float y, float vx, float vy, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; field[pos].x += vx * strongness; field[pos].y += vy * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::setFromPixels(unsigned char * pixels, bool bMoveTowardsWhite, float strength){ clear(); for (int i = 1; i < fieldWidth-1; i++){ for (int j = 1; j < fieldHeight-1; j++){ int pos = j * fieldWidth + i; // calc the difference in horiz and vert int nw = pixels[ (j-1) * fieldWidth + (i-1) ]; int n_ = pixels[ (j-1) * fieldWidth + (i ) ]; int ne = pixels[ (j-1) * fieldWidth + (i+1) ]; int _e = pixels[ (j ) * fieldWidth + (i-1) ]; int _w = pixels[ (j ) * fieldWidth + (i+1) ]; int sw = pixels[ (j+1) * fieldWidth + (i-1) ]; int s_ = pixels[ (j+1) * fieldWidth + (i ) ]; int se = pixels[ (j+1) * fieldWidth + (i+1) ]; float diffx = (nw + _w + sw) - (ne + _e + se); float diffy = (nw + n_ + ne) - (sw + s_ + se); if (bMoveTowardsWhite){ diffx *= -1; diffy *= -1; } field[pos].x = diffx * strength; field[pos].y = diffy * strength; } } } //------------------------------------------------------------------------------------ vectorField::~vectorField(){ }
particle.h
#ifndef PARTICLE_H #define PARTICLE_H #include "ofMain.h" #include "ofxVectorMath.h" class particle { public: ofxVec2f pos; ofxVec2f vel; ofxVec2f frc; // frc is also know as acceleration (newton says "f=ma") particle(); virtual ~particle(){}; void resetForce(); void addForce(float x, float y); void addDampingForce(); void setInitialCondition(float px, float py, float vx, float vy); void update(); void draw(); void bounceOffWalls(); float damping; protected: private: }; #endif // PARTICLE_H
particle.cpp
#include "particle.h" #include "ofMain.h" //------------------------------------------------------------ particle::particle(){ setInitialCondition(0,0,0,0); damping = 0.08f; } //------------------------------------------------------------ void particle::resetForce(){ // we reset the forces every frame frc.set(0,0); } //------------------------------------------------------------ void particle::addForce(float x, float y){ // add in a force in X and Y for this frame. frc.x = frc.x + x; frc.y = frc.y + y; } //------------------------------------------------------------ void particle::addDampingForce(){ // the usual way to write this is vel *= 0.99 // basically, subtract some part of the velocity // damping is a force operating in the oposite direction of the // velocity vector frc.x = frc.x - vel.x * damping; frc.y = frc.y - vel.y * damping; } //------------------------------------------------------------ void particle::setInitialCondition(float px, float py, float vx, float vy){ pos.set(px,py); vel.set(vx,vy); } //------------------------------------------------------------ void particle::update(){ vel = vel + frc; pos = pos + vel; } //------------------------------------------------------------ void particle::draw(){ ofCircle(pos.x, pos.y, 3); } //------------------------------------------------------------ void particle::bounceOffWalls(){ // sometimes it makes sense to damped, when we hit bool bDampedOnCollision = true; bool bDidICollide = false; // what are the walls float minx = 0; float miny = 0; float maxx = ofGetWidth(); float maxy = ofGetHeight(); if (pos.x > maxx){ pos.x = maxx; // move to the edge, (important!) vel.x *= -1; bDidICollide = true; } else if (pos.x < minx){ pos.x = minx; // move to the edge, (important!) vel.x *= -1; bDidICollide = true; } if (pos.y > maxy){ pos.y = maxy; // move to the edge, (important!) vel.y *= -1; bDidICollide = true; } else if (pos.y < miny){ pos.y = miny; // move to the edge, (important!) vel.y *= -1; bDidICollide = true; } if (bDidICollide == true && bDampedOnCollision == true){ vel *= 0.3; } }
最後にこのベクトル場を、より厳密な物理世界に適用してみましょう。
今度のプログラムは、一つ一つのパーティクルが単純なものではなく、物理演算を適用した物体に変更しています。物理演算には前回使用したBox2Dを活用しています。ベクトル場に配置されたパーティクルは、その力の影響を受けながら、重力やパーティクル同士の衝突などの物理法則が適用されています。このことにより、とてもリアルな粒の動きが再現されています。
testApp.h
#ifndef _TEST_APP #define _TEST_APP #include "ofMain.h" #include "vectorField.h" #include "CustomCircle.h" #include "ofxOpenCv.h" #include "ofxSimpleGuiToo.h" #include "ofxBox2d.h" class testApp : public ofBaseApp { public: void setup(); void update(); void draw(); void keyPressed (int key); void keyReleased(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(int x, int y, int button); void windowResized(int w, int h); //カメラ入力を準備 ofVideoGrabber vidGrabber; ofxCvColorImage colorImg; //オリジナルのカラー映像 ofxCvGrayscaleImage grayImage; //グレースケールに変換後 ofxCvGrayscaleImage grayBg; //キャプチャーした背景画像 ofxCvGrayscaleImage grayDiff; //現在の画像と背景との差分 ofxCvGrayscaleImage grayDiffSmall; //縮小した差分イメージ ofxCvContourFinder contourFinder; //輪郭抽出のためのクラス bool bLearnBakground; //背景画像を登録したか否か bool showCvAnalysis; //解析結果を表示するか int threshold; //2値化の際の敷居値 bool liveVideo; //カメラ入力を使用するか bool drawColorImg; //ビデオ表示のon/off bool drawVectorFirld; //VectorField表示のon/off bool bForceInward; //重力の向き //GUI ofxSimpleGuiToo gui; //ベクトル場 vectorField VF; //box2d ofxBox2d box2d; float gravity; //重力 float force; //押し出す力 float vectorThreshold; //力を適用する閾値 //ofxBox2dCircleを継承したクラス listparticles; int particleNum; }; #endif
testApp.cpp
#include "testApp.h" void testApp::setup(){ //画面の基本設定 ofBackground(0, 0, 0); ofSetFrameRate(60); //カメラから映像を取り込んで表示 vidGrabber.setVerbose(true); vidGrabber.initGrabber(320,240); //使用する画像の領域を確保 colorImg.allocate(320,240); grayImage.allocate(320,240); grayBg.allocate(320,240); grayDiff.allocate(320,240); grayDiffSmall.allocate(60, 40); //変数の初期化 bLearnBakground = true; showCvAnalysis = false; //ベクトル場の初期化 VF.setupField(60, 40, ofGetWidth(), ofGetHeight()); //GUIの初期設定 gui.setup(); gui.config->gridSize.x = 250; gui.addContent("grayImage", grayImage); gui.addContent("grayDiff", grayDiff); gui.addContent("grayDiffSmall", grayDiffSmall); gui.addFPSCounter(); gui.addSlider("threshold", threshold, 0, 400); gui.addSlider("gravity", gravity, 0.0, 1.0); gui.addSlider("force", force, 0.0, 20.0); gui.addSlider("vector threshold", vectorThreshold, 0.0, 2.0); gui.addToggle("use live video", liveVideo); gui.addToggle("draw video", drawColorImg); gui.addToggle("draw vector field", drawVectorFirld); gui.addToggle("force inward", bForceInward); gui.loadFromXML(); //Box2D初期設定 box2d.init(); box2d.setGravity(0, gravity); box2d.createBounds(); box2d.setFPS(8); //パーティクル生成 particleNum = 500; for (int i = 0; i < particleNum; i++) { CustomCircle* p = new CustomCircle(); p->setPhysics(1.0, 0.0, 0.2); p->setup(box2d.getWorld(), ofRandom(0, ofGetWidth()), ofRandom(0, ofGetHeight()), ofRandom(7, 14), false); p->disableCollistion(); particles.push_back(p); } } void testApp::update(){ box2d.setGravity(0, gravity); box2d.update(); //新規フレームの取り込みをリセット bool bNewFrame = false; //カメラから新規フレーム取り込み vidGrabber.grabFrame(); //新規にフレームが切り替わったか判定 bNewFrame = vidGrabber.isFrameNew(); //フレームが切り替わった際のみ画像を解析 if (bNewFrame){ //取り込んだフレームを画像としてキャプチャ colorImg.setFromPixels(vidGrabber.getPixels(), 320,240); //左右反転 colorImg.mirror(false, true); //カラーのイメージをグレースケールに変換 grayImage = colorImg; //まだ背景画像が記録されていなければ、現在のフレームを背景画像とする if (bLearnBakground == true){ grayBg = grayImage; bLearnBakground = false; } //グレースケールのイメージと取り込んだ背景画像との差分を算出 grayDiff.absDiff(grayBg, grayImage); //画像を2値化(白と黒だけに)する grayDiff.threshold(threshold); //縮小した差分イメージに画像をコピー grayDiffSmall.scaleIntoMe(grayDiff); //ぼかし grayDiffSmall.blur(5); //ベクトル場に差分イメージを適用 VF.setFromPixels(grayDiffSmall.getPixels(), bForceInward, 0.05f); //ベクトル場に発生した力を計算し、パーティクルにかかる力を算出 for(list::iterator it = particles.begin(); it != particles.end(); ++it){ ofxVec2f frc; frc = VF.getForceFromPos((*it)->getPosition().x, (*it)->getPosition().y); //設定した閾値を越えたら、VFの力を加える if (frc.length() > vectorThreshold) { (*it)->addForce(ofPoint(frc.x * force, frc.y * force), 1.0); } (*it)->update(); } } } void testApp::draw(){ glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE); if (drawColorImg) { //カラーイメージを描く ofSetColor(255, 255, 255, 127); colorImg.draw(0, 0, ofGetWidth(), ofGetHeight()); } if (drawVectorFirld) { //ベクトル場を描く ofSetColor(255, 255, 255, 127); VF.draw(); } //パーティクルを描く for(list ::iterator it = particles.begin(); it != particles.end(); ++it){ (*it)->draw(); } //GUIを描く gui.draw(); } void testApp::keyPressed (int key){ //キー入力でモード切り替え switch (key){ case 'f': //フルスクリーンのon/off ofToggleFullscreen(); case 'g': //Gui表示のon/off gui.toggleDraw(); break; case 't': //重力を反転 bForceInward = !bForceInward; break; case ' ': //背景画像を新規に取り込む bLearnBakground = true; break; } } void testApp::keyReleased(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(int x, int y, int button){ } void testApp::windowResized(int w, int h){ }
vectorField.h
#ifndef VECTORFIELD_H #define VECTORFIELD_H #include "ofMain.h" #include "ofxVectorMath.h" class vectorField { public: // the internal dimensions of the field: (ie, 60x40, etc) int fieldWidth; int fieldHeight; int fieldSize; // total number of "pixels", ie w * h // the external dimensions of the field: (ie, 1024x768) int externalWidth; int externalHeight; vectorfield; vectorField(); virtual ~vectorField(); void setupField(int innerW, int innerH, int outerW, int outerH); // pass in internal dimensions and outer dimensions void clear(); void fadeField(float fadeAmount); void randomizeField(float scale); void draw(); ofxVec2f getForceFromPos(float xpos, float ypos); void addOutwardCircle(float x, float y, float radius, float strength); void addInwardCircle(float x, float y, float radius, float strength); void addClockwiseCircle(float x, float y, float radius, float strength); void addCounterClockwiseCircle(float x, float y, float radius, float strength); void addVectorCircle(float x, float y, float vx, float vy, float radius, float strength); void setFromPixels(unsigned char * pixels, bool bMoveTowardsWhite, float strength); protected: private: }; #endif // VECTORFIELD_H
vectorField.cpp
#include "vectorField.h" //------------------------------------------------------------------------------------ vectorField::vectorField(){ /*fieldWidth = 60; fieldHeight = 40; */ } //------------------------------------------------------------------------------------ void vectorField::setupField(int innerW, int innerH, int outerW, int outerH){ fieldWidth = innerW; fieldHeight = innerH; externalWidth = outerW; externalHeight = outerH; field.clear(); fieldSize = fieldWidth * fieldHeight; for (int i = 0; i < fieldSize; i++){ ofxVec2f pt; pt.set(0,0); field.push_back(pt); } } //------------------------------------------------------------------------------------ void vectorField::clear(){ for (int i = 0; i < fieldSize; i++){ field[i].set(0,0); } } //------------------------------------------------------------------------------------ void vectorField::fadeField(float fadeAmount){ for (int i = 0; i < fieldSize; i++){ field[i].set(field[i].x*fadeAmount,field[i].y*fadeAmount); } } //------------------------------------------------------------------------------------ void vectorField::randomizeField(float scale){ for (int i = 0; i < fieldSize; i++){ // random between -1 and 1 float x = (float)(ofRandom(-1,1)) * scale; float y = (float)(ofRandom(-1,1)) * scale; field[i].set(x,y); } } //------------------------------------------------------------------------------------ void vectorField::draw(){ float scalex = (float)externalWidth / (float)fieldWidth; float scaley = (float)externalHeight / (float)fieldHeight; for (int i = 0; i < fieldWidth; i++){ for (int j = 0; j < fieldHeight; j++){ // pos in array int pos = j * fieldWidth + i; // pos externally float px = i * scalex; float py = j * scaley; float px2 = px + field[pos].x * 5; float py2 = py + field[pos].y * 5; ofLine(px,py, px2, py2); // draw an baseline to show direction // get the line as vector, calculate length, then normalize. // rotate and draw based on length ofxVec2f line; line.set(px2-px, py2-py); float length = line.length(); line.normalize(); line.rotate(90); // these are angles in degrees ofLine(px - line.x*length*0.2, py - line.y*length*0.2, px + line.x*length*0.2, py + line.y*length*0.2); } } } //------------------------------------------------------------------------------------ ofxVec2f vectorField::getForceFromPos(float xpos, float ypos){ ofxVec2f frc; frc.set(0,0); // convert xpos and ypos into pcts = float xPct = xpos / (float)externalWidth; float yPct = ypos / (float)externalHeight; // if we are less then 0 or greater then 1 in x or y, return no force. if ((xPct < 0 || xPct > 1) || (yPct < 0 || yPct > 1)){ return frc; } // where are we in the array int fieldPosX = (int)(xPct * fieldWidth); int fieldPosY = (int)(yPct * fieldHeight); // saftey :) fieldPosX = MAX(0, MIN(fieldPosX, fieldWidth-1)); fieldPosY = MAX(0, MIN(fieldPosY, fieldHeight-1)); // pos in array int pos = fieldPosY * fieldWidth + fieldPosX; frc.set(field[pos].x * 0.1, field[pos].y * 0.1 ); // scale here as values are pretty large. return frc; } //------------------------------------------------------------------------------------ void vectorField::addInwardCircle(float x, float y, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; float unit_px = ( fieldPosX - i) / distance; float unit_py = ( fieldPosY - j) / distance; field[pos].x += unit_px * strongness; field[pos].y += unit_py * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::addOutwardCircle(float x, float y, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; float unit_px = ( fieldPosX - i) / distance; float unit_py = ( fieldPosY - j) / distance; field[pos].x -= unit_px * strongness; field[pos].y -= unit_py * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::addClockwiseCircle(float x, float y, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; float unit_px = ( fieldPosX - i) / distance; float unit_py = ( fieldPosY - j) / distance; field[pos].x += unit_py * strongness; /// Note: px and py switched, for perpendicular field[pos].y -= unit_px * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::addCounterClockwiseCircle(float x, float y, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; float unit_px = ( fieldPosX - i) / distance; float unit_py = ( fieldPosY - j) / distance; field[pos].x -= unit_py * strongness; /// Note: px and py switched, for perpendicular field[pos].y += unit_px * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::addVectorCircle(float x, float y, float vx, float vy, float radius, float strength){ // x y and radius are in external dimensions. Let's put them into internal dimensions: // first convert to pct: float pctx = x / (float)externalWidth; float pcty = y / (float)externalHeight; float radiusPct = radius / (float)externalWidth; // then, use them here: int fieldPosX = (int)(pctx * (float)fieldWidth); int fieldPosY = (int)(pcty * (float)fieldHeight); float fieldRadius = (float)(radiusPct * fieldWidth); // we used to do this search through every pixel, ie: // for (int i = 0; i < fieldWidth; i++){ // for (int j = 0; j < fieldHeight; j++){ // but we can be smarter :) // now, as we search, we can reduce the "pixels" we look at by // using the x y +/- radius. // use min and max to make sure we don't look over the edges int startx = MAX(fieldPosX - fieldRadius, 0); int starty = MAX(fieldPosY - fieldRadius, 0); int endx = MIN(fieldPosX + fieldRadius, fieldWidth); int endy = MIN(fieldPosY + fieldRadius, fieldHeight); for (int i = startx; i < endx; i++){ for (int j = starty; j < endy; j++){ int pos = j * fieldWidth + i; float distance = (float)sqrt((fieldPosX-i)*(fieldPosX-i) + (fieldPosY-j)*(fieldPosY-j)); if (distance < 0.0001) distance = 0.0001; // since we divide by distance, do some checking here, devide by 0 is BADDDD if (distance < fieldRadius){ float pct = 1.0f - (distance / fieldRadius); float strongness = strength * pct; field[pos].x += vx * strongness; field[pos].y += vy * strongness; } } } } //------------------------------------------------------------------------------------ void vectorField::setFromPixels(unsigned char * pixels, bool bMoveTowardsWhite, float strength){ clear(); for (int i = 1; i < fieldWidth-1; i++){ for (int j = 1; j < fieldHeight-1; j++){ int pos = j * fieldWidth + i; // calc the difference in horiz and vert int nw = pixels[ (j-1) * fieldWidth + (i-1) ]; int n_ = pixels[ (j-1) * fieldWidth + (i ) ]; int ne = pixels[ (j-1) * fieldWidth + (i+1) ]; int _e = pixels[ (j ) * fieldWidth + (i-1) ]; int _w = pixels[ (j ) * fieldWidth + (i+1) ]; int sw = pixels[ (j+1) * fieldWidth + (i-1) ]; int s_ = pixels[ (j+1) * fieldWidth + (i ) ]; int se = pixels[ (j+1) * fieldWidth + (i+1) ]; float diffx = (nw + _w + sw) - (ne + _e + se); float diffy = (nw + n_ + ne) - (sw + s_ + se); if (bMoveTowardsWhite){ diffx *= -1; diffy *= -1; } field[pos].x = diffx * strength; field[pos].y = diffy * strength; } } } //------------------------------------------------------------------------------------ vectorField::~vectorField(){ }
CustomCircle.h
#include "ofxVectorMath.h" #include "ofxBox2d.h" //ofxBox2dCircleを継承したクラスCustomCircleを定義 class CustomCircle : public ofxBox2dCircle { public: void draw(); //円を描画する };
CustomCircle.cpp
#include "CustomCircle.h" //ofxBox2dCircleを継承した、オリジナルの円を描く void CustomCircle::draw() { float radius = getRadius(); //半径を取得 glPushMatrix(); //座標を変更 glTranslatef(getPosition().x, getPosition().y, 0); //物体の位置に座標を移動 //円を描く ofFill(); ofSetColor(127, 255, 255, 11); ofCircle(0, 0, radius*2.0); ofSetColor(31, 127, 255, 127); ofCircle(0, 0, radius); ofSetColor(255, 255, 255, 63); ofCircle(0, 0, radius*0.5); glPopMatrix(); //座標を元に戻す }
openFrameworks, vector field study from Atsushi Tadokoro on Vimeo.
今日とりあげた全てのプログラムは下記からダウンロード可能です。
今回は、CSSでよりきめ細かくスタイルの適用範囲を指定する方法を学んでいきます。
これまでのサンプルは、プログラムを実行すると、静止画であってもアニメーションであっても、ユーザがプログラムを終了するまではプログラムされた通りに自動的に再生されていました。言い換えると、このプログラムにユーザが介入する要素は、プログラムの起動と終了のみと言えるでしょう。
Flashでは、様々な手段でプログラムを実行している最中にも、ユーザがプログラムに対して介入することが可能です。ユーザがアクションを起こし、それに反応してプログラムは何らかのリアクションをする。ユーザはさらにその反応を見て、別のアクションを起こす。そうした、2つ以上の要素(この場合は、人間とコンピュータ上で動いているプログラム)の相互作用のことを、インタラクション(interaction)と呼びます。
プログラムにインタラクションを付加することで、より意外性に富んだ豊かな表現が可能となります。また、インタラクションをデザインしていくことは、これまでの伝統的なデザインの枠を越えた、新たな領域と言えるでしょう。どのようにしたら、効果的なインタラクションを実現できるのか、どのようにデザインすれば自然なインタラクションが生まれるのか、といったようにまだまだ未知の領域が残された刺激的な世界が拡がっています。
一口にマウスによるインタラクションといっても、マウスの位置とマウスボタンが押されているorいない、という組合せによって、様々なユーザの動作が存在します。このそれぞれの動作とFlashの状態を組み合わせることで、様々なインタラクションが実現できます。Flashで用いられるマウスの動作には以下のものがあげられます。
今日はこれらのマウスに関する状態を検知する機能から、マウスの位置を知るためのmouseX、mouseYに注目して学んでいきます。
今日の授業で使用するスクリプトのテンプレートです。まず、AS3形式のFlashファイルを用意し、ドキュメントクラスを「Main」に指定します。その上で下記のコードを新規に作成したActionScriptファイルにペーストして、ファイル名を「Main.as」にしてFlaファイルと同じ場所に保存してください。
[sourcecode language=”as3″]
package {
import flash.display.*;
import flash.events.*;
public class Main extends Sprite {
//ここにグローバルな変数を記入
public function Main() {
//初期設定の項目を記入
//フレーム更新のイベントリスナーの登録
this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
}
//イベントハンドラ
function enterFrameHandler(event:Event):void {
}
}
}
[/sourcecode]
まず、一番簡単なマウスを利用したインタラクションとして、マウスの位置の検出を試してみましょう。まず、Flaファイル側でムービークリップシンボルを生成し、クラス名「Ball」という名前でリンケージを作成します。
では、まずはこのムービークリップBallの位置を、マウスカーソルの位置で移動してみましょう。マウスカーソルの位置を検知するには、mouseXとmouseYというプロパティを用います。画面全体(stage)から見たマウスカーソルの位置は、以下のように指定します。
このプロパティを画面に配置したムービークリップの(x, y)座標に代入すると、マウスカーソルの位置にムービクリップが移動します。
[sourcecode language=”as3″]
package
{
import flash.display.*;
import flash.events.*;
public class Main extends Sprite
{
//ここにグローバルな変数を記入
var ball:Ball = new Ball();
public function Main()
{
//初期設定の項目を記入
this.addChild(ball);
//フレーム更新のイベントリスナーの登録
this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
}
//イベントハンドラ
function enterFrameHandler(event:Event):void
{
ball.x = stage.mouseX;
ball.y = stage.mouseY;
}
}
}
[/sourcecode]
/wp-content/uploads/2010/11/webmov101201_01.swf, 400, 300
単純にマウスカーソルの位置とムービークリップの位置を一致させるだけでなく、ムービクリップの別のプロパティとマウスカーソルの位置を関連付けてみましょう。例えば、マウスカーソルのX座標の位置はムービクリップのX座標のままで、マウスカーソルのY座標はムービークリップのサイズに適用するように変化させてみましょう。
また、ムービクリップをもう1つ配置して、X座標を画面の中心で反転するように配置してみましょう。
[sourcecode language=”as3″]
package
{
import flash.display.*;
import flash.events.*;
public class Main extends Sprite
{
//ここにグローバルな変数を記入
var ball1:Ball = new Ball();
var ball2:Ball = new Ball();
public function Main()
{
//初期設定の項目を記入
this.addChild(ball1);
this.addChild(ball2);
//フレーム更新のイベントリスナーの登録
this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
}
//イベントハンドラ
function enterFrameHandler(event:Event):void
{
ball1.x = stage.mouseX;
ball2.x = stage.stageWidth – stage.mouseX;
ball1.y = stage.stageHeight / 2;
ball2.y = stage.stageHeight / 2;
ball1.scaleX = ball1.scaleY = stage.mouseY / stage.stageHeight;
ball2.scaleX = ball2.scaleY = 1.0 – stage.mouseY / stage.stageHeight;
}
}
}
[/sourcecode]
/wp-content/uploads/2010/11/webmov101201_02.swf, 400, 300
マウスカーソルの位置を利用したインタラクションの応用例として、マウスカーソルの位置にすぐに移動するのではなく、すこし遅れたタイミングでなめらかな動きでカーソルを追い掛ける動きをつくってみましょう。
なめらかな動きを実現するために、マウスカーソルの座標と、現在のムービークリップの座標の差分をとって、常に一定の数値で割り算した値だけ移動するようにします。すると、物体は徐々に減速しながら限りなくカーソルの場所に近付いていくという動きになります。割り算する数値を調整すると、減速するスピードを変化させることが可能です。この動きは簡単な数式で効果的なので、多くのFlashサイトで見掛けます。
[sourcecode language=”as3″]
package
{
import flash.display.*;
import flash.events.*;
import flashx.textLayout.formats.Float;
public class Main extends Sprite
{
//ここにグローバルな変数を記入
var ball:Ball = new Ball();
var interpolate:Number;
public function Main()
{
//初期設定の項目を記入
this.addChild(ball);
interpolate = 0.1;
//フレーム更新のイベントリスナーの登録
this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
}
//イベントハンドラ
function enterFrameHandler(event:Event):void
{
ball.x += (stage.mouseX – ball.x) * interpolate;
ball.y += (stage.mouseY – ball.y) * interpolate;
}
}
}
[/sourcecode]
/wp-content/uploads/2010/11/webmov101201_03.swf, 400, 300
応用例として、先週学んだ配列を利用して、カーソルをなめらかに追い掛ける動きを、たくさんの物体でやってみましょう。
[sourcecode language=”as3″]
package
{
import flash.display.*;
import flash.events.*;
import flashx.textLayout.formats.Float;
public class Main extends Sprite
{
//ここにグローバルな変数を記入
var balls:Array = new Array();
var ballNum:Number;
var interpolate:Number;
public function Main()
{
//20コのボールを生成
ballNum = 20;
//ボールのパラメータを設定
for (var i:int = 0; i < ballNum; i++)
{
var b:Ball = new Ball();
b.alpha = 0.1;
b.scaleX = b.scaleY = 3.0 – i / ballNum * 2.0;
this.addChild(b);
balls.push(b);
}
//フレーム更新のイベントリスナーの登録
this.addEventListener(Event.ENTER_FRAME, enterFrameHandler);
}
//イベントハンドラ
function enterFrameHandler(event:Event):void
{
//20コのボールが補完しながらカーソルを追う
for (var i:int = 0; i < ballNum; i++)
{
balls[i].x += (stage.mouseX – balls[i].x) * 0.02 * (i + 1);
balls[i].y += (stage.mouseY – balls[i].y) * 0.02 * (i + 1);
}
}
}
}
[/sourcecode]
/wp-content/uploads/2010/11/webmov101201_04.swf, 400, 300