タグ別アーカイブ: Box2D

Processing #001 LeapMotion x Box2D

Processing #001 LeapMotion x Box2D from Takepepe on Vimeo.

ご無沙汰してます、Takepepeです。
世間は梅雨、ついにクーラーをかけながらデモムービーを撮る季節に突入しました。
すっかり月1更新になってしまった今日このごろ。反省しないと!

さて、今日は動画のとおり、Leapを使ってお絵描きするインスタレーションをProcessingで作成しました。
今回のキモはLeapSDKをラップしているライブラリから提供される
標準のジェスチャーや座標を、意図した値に変換するところです。

使用しているライブラリは以下

コードはEclipseで書いていますので、このままProcessingのIDEにはっつけても動かないのでご注意を。
OPENGLの恩恵を受けていないコードですが、レンダリングモードをOPENGLにしています。
EclipseでProcessingを書く場合、OPENGLの使用にはcore.jarだけではなく
他にも入れないといけない.jarがありますので適当にググってください。

いつもの様に、順をおって解説していきます。

  1. LeapPointerP5の作成
  2. Box2Dの初期設定
  3. LeapPointerP5の座標をBox2Dに落とし込む
  4. 両手の状態で描画判定

1.LeapPointerP5の作成

P001っていうのはPAppletを継承したやつです。
コンストラクタに引数として渡して保持しておけば、
Processingの便利APIを外からでも使用できます。

update()は、そのP001からdraw()内で呼ばれるループ関数です。
このクラスは、冒頭で述べたキモにあたる部分を担っています。

執筆時現在の開発環境での話ですが、
前回解説したとおり、LeapはFrameオブジェクトからpointable(指か棒)を取得できますが、
これらをロストした時のカバーを自分でやらなければいけません。

今回のインスタレーションでは、ポインターを1つに限定しています。
getPointable()でそのポインターを固定する処理をして、
ポインターが属する手と反対側の手が「グー」なら「つかんでいる」状態というのを
publicなisGrabbed()から参照出来るようになっています。

LeapPointerP5.java


import com.leapmotion.leap.Frame;
import com.leapmotion.leap.Pointable;
import com.leapmotion.leap.Hand;
import com.onformative.leap.LeapMotionP5;

public class LeapPointerP5 {
	
	private P001 p;
	
	private LeapMotionP5 leap;
	private Frame frame;
	private Pointable pointable;
	private Hand currentHand;
	private Hand contraryHand;
	
	private boolean isDoubleHand = false;
	private boolean isGrabbing = false;
	private float offset = 6.0f;
	private float x;
	private float y;
	
	public LeapPointerP5(P001 p,LeapMotionP5 leap){
		this.p = p;
		this.leap = leap;
		this.pointable = leap.getFrame().pointables().get(0);
	}

	//------------------------------------------------------------
	// @ Loop
	
	public void update(){
		
		frame = leap.getFrame();
		pointable = getPointable();
		setHands();
		setPosition();

	}
	
	//------------------------------------------------------------
	// @ private 
	
	private Pointable getPointable() {
		
		// 前のフレームで取得したPointableがある場合、前フレームで取得したPointableを返す。無ければ新しいPointableを返す。
		
		Pointable _pointable = frame.pointables().get(0);
		int count = frame.pointables().count();
		for(int i=0 ; i<count ; i++){
			if(pointable.id() == frame.pointables().get(i).id()){
				_pointable = frame.pointables().get(i);
			}
		}
		return _pointable;
	}
	
	private void setHands(){
		
		// pointableの手を格納。手を2つ取得できた場合、反対側の手を格納
		
		if(pointable.isFinger()){
			currentHand = pointable.hand();
			if(frame.hands().count() == 2){
				contraryHand = frame.hands().get(1);
				if(currentHand == contraryHand){
					contraryHand = frame.hands().get(0);
				}
				isDoubleHand = true;
			}else{
				contraryHand = null;
				isDoubleHand = false;
			}
		}
	}
	
	private void setPosition(){
		
		// pointableから、pointerの座標を算出
		
		float px = pointable.tipPosition().getX()*offset;
		float py = pointable.tipPosition().getY()*offset;
		this.x += ((p.width/2 + px) - this.x)/10;
		this.y += ((p.height/0.6 - py) - this.y)/10;
	}
	
	
	//------------------------------------------------------------
	// @ getter
	
	public float getX(){
		return this.x;
	}
	
	public float getY(){
		return this.y;
	}
	
	public boolean isPointed(){
		boolean res = false;
		if(frame.pointables().count() != 0){
			res = true;
		}
		return res;
	}
	
	public boolean isGrabbed(){
		
		// 描画している手と反対側の手が存在しつつ、その手がグーの時trueを返す。
		
		boolean res = false;
		if(isDoubleHand){
			if(!isGrabbing){
				if(contraryHand.fingers().count() == 0){
					res = true;
					isGrabbing = true;
				}
			}else{
				res = true;
				if(contraryHand.fingers().count() == 5){
					res = false;
					isGrabbing = false;
				}
			}
		}
		return res;
	}
	
}

2.Box2Dの初期設定

ここからはPAppletのコードです。
Box2Dはあんまり新しくはなく、日本語のドキュメントがかなり少ないため苦戦しました。
Physicsという、ProcessingでBox2Dを使用するためのインスタンスを生成し、
それに対して、jbox2dのクラス群を利用していく感じになります。
createPointerBody()ではLeapPointerP5で定義した座標をBox2D上に落とし込むためのインスタンスを生成しています。

P001.java(抜粋)

	private void createPysics(){
		float gravX = 0.0f;
		float gravY = -100.0f;
		float AABBWidth = 2*width;
		float AABBHeight = 2*height;
		float borderBoxWidth = width+11;
		float borderBoxHeight = height+11;
		physics = new Physics(this, width, height, gravX, gravY, AABBWidth, AABBHeight, borderBoxWidth, borderBoxHeight, pixelsPerMeter);
		physics.setDensity(110.1f);
		physics.setCustomRenderingMethod(this, "myCustomRenderer");
	}
	
	private void createPointerBody(){
		
		// LeapPointerP5の座標を用いたpointerBodyを生成。
		
		pointerDef = new BodyDef();
		pointerBody = physics.getWorld().createBody(pointerDef);
		pointerCircleDef = new CircleDef();
		pointerCircleDef.radius = 0.5f;
		pointerCircleDef.friction = physics.getFriction();
		pointerCircleDef.restitution = physics.getRestitution();
		pointerCircleDef.isSensor = physics.getSensor();
		pointerBody.createShape(pointerCircleDef);
		pointerBody.setMassFromShapes();
		updatePointerBody();
	}
	

3.LeapPointerP5の座標をBox2Dに落とし込む

以下のupdatePointerBody()はdraw()内でコールされている関数です。
getColor()は、フレームにあわせて虹色を返してくれる関数です。

P001.java(抜粋)

	private void updatePointerBody(){
		
		// 描画中は描画が始まった段階のpalletColorを使用。描画中でない時は随時描画色を更新。
		
		UserData data = new UserData();
		if(isDrawing){
			data.color = palletColor;
		}else{
			data.color = getColor();
		}
		pointerBody.setUserData(data);
		
		// Box2Dの座標系に変換しつつpointerBodyを移動。
		
		float x = (pointer.getX() - width/2)/pixelsPerMeter;
		float y = (-pointer.getY() + height/2)/pixelsPerMeter;
		pointerBody.setPosition(new Vec2(x,y));
		
	}

4.両手の状態で描画判定

LeapPointerP5で反対の手が「グー」と判定された場合、描画中になります。
まず、verticesというArrayListが生成され、
「グー」の間、ポインターの座標をどんどんverticesにつっこんでいき、
「パー」になった時、もしくは反対の手がなくなった時、
描画が終了し、verticesを使用したCircleが新しいbodyとして追加されます。

P001.java(抜粋)

	private void brushBody(){
		if(isDrawing != pointer.isGrabbed()){
			if(isDrawing){
				onEndDrawing();
			}else{
				onStartDrawing();
			}
		}
		if(isDrawing == pointer.isGrabbed() && isDrawing){
			onProcessDrawing();
		}

		// 描画中であれば、ProcessingのAPIでラインを描く。( verticesの数だけellipseを描く )
		
		if(isDrawing){
			this.fill(palletColor);
			for(int i=0 ; i<vertices.size() ; i++){
				this.ellipse(vertices.get(i).x*pixelsPerMeter+width/2, vertices.get(i).y*-pixelsPerMeter+height/2, 30 ,30);
			}
		}
	}
	
	private void onStartDrawing(){
		
		// 描画が開始した時の処理。 vertices に格納した座標を元に Box2D で body を生成。
		
		isDrawing = true;
		vertices = new ArrayList<Vec2>();
		palletColor = getColor();
	}
	
	private void onProcessDrawing(){
		
		// 描画中の処理。 vertices に格納した座標を格納。
		
		Vec2 point = physics.screenToWorld(pointer.getX(),pointer.getY());
		vertices.add(point);
	}
	
	private void onEndDrawing(){
		
		// 描画が終了した時の処理。verticesに格納した座標を元にBox2Dでbodyを生成。
		
		isDrawing = false;
		BodyDef bd = new BodyDef();
		Body body = physics.getWorld().createBody(bd);
		for(int i =0 ; i<vertices.size() ; i++){
			CircleDef cd = new CircleDef();
			cd.radius = 0.5f;
			cd.density = physics.getDensity();
			cd.friction = physics.getFriction();
			cd.restitution = physics.getRestitution();
			cd.isSensor = physics.getSensor();
			cd.localPosition.set(vertices.get(i));
			body.createShape(cd);
			body.setMassFromShapes();
		}
		
		// 描画したBodyに色を持たせる。必要なメンバはUserDataクラスで定義しておく。
		
		UserData data = new UserData();
		data.color = palletColor;
		body.setUserData(data);
		vertices = null;
	}

P001.java

import java.util.List;
import java.util.ArrayList;

import processing.core.PApplet;

import org.jbox2d.common.Vec2;
import org.jbox2d.dynamics.World;
import org.jbox2d.dynamics.Body;
import org.jbox2d.dynamics.BodyDef;
import org.jbox2d.collision.CircleShape;
import org.jbox2d.collision.CircleDef;
import org.jbox2d.collision.Shape;
import org.jbox2d.collision.ShapeType;
import org.jbox2d.p5.Physics;

import com.onformative.leap.LeapMotionP5;

public class P001 extends PApplet{
	private static final long serialVersionUID = 0;
	
	// @ Box2D (JBox2D + BoxWrap2d)
	// JBox2D : http://www.jbox2d.org/
	// BoxWrap2d : http://wiki.processing.org/w/BoxWrap2d
	private Physics physics;
	private List<Vec2> vertices;
	private float pixelsPerMeter = 30;
	private BodyDef pointerDef;
	private Body pointerBody;
	private CircleDef pointerCircleDef;
	
	//  LeapMotionP5
	//  https://github.com/mrzl/LeapMotionP5
	//  auther @ mrzl
	private LeapMotionP5 leap;
	
	//  LeapPointerP5
	//  auther @ Takeppe
	private LeapPointerP5 pointer;
	
	private int palletColor;
	private boolean isDrawing = false;
	
	//------------------------------------------------------------
	// @ Setup
		
	public void setup(){
		size(2560,1390,OPENGL);
		frameRate(60);
		colorMode(HSB, 360, 100, 100);
		noStroke();
		
		leap = new LeapMotionP5(this);
		pointer = new LeapPointerP5(this,leap);
		
		createPysics();
		createPointerBody();
	}
	
	//------------------------------------------------------------
	// @ Box2D 
	
	private void createPysics(){
		float gravX = 0.0f;
		float gravY = -100.0f;
		float AABBWidth = 2*width;
		float AABBHeight = 2*height;
		float borderBoxWidth = width+11;
		float borderBoxHeight = height+11;
		physics = new Physics(this, width, height, gravX, gravY, AABBWidth, AABBHeight, borderBoxWidth, borderBoxHeight, pixelsPerMeter);
		physics.setDensity(110.1f);
		physics.setCustomRenderingMethod(this, "myCustomRenderer");
	}
	
	private void createPointerBody(){
		
		// LeapPointerP5の座標を用いたpointerBodyを生成。
		
		pointerDef = new BodyDef();
		pointerBody = physics.getWorld().createBody(pointerDef);
		pointerCircleDef = new CircleDef();
		pointerCircleDef.radius = 0.5f;
		pointerCircleDef.friction = physics.getFriction();
		pointerCircleDef.restitution = physics.getRestitution();
		pointerCircleDef.isSensor = physics.getSensor();
		pointerBody.createShape(pointerCircleDef);
		pointerBody.setMassFromShapes();
		updatePointerBody();
	}
	
	public void myCustomRenderer(World world){
		
		// BoxWrap2d のカスタムレンダラーを使用。参照は以下。
		// http://wiki.processing.org/w/BoxWrap2d#Using_a_custom_renderer
		
		Body body;
		for (body = world.getBodyList(); body != null; body = body.getNext()) {
			Shape shape;
			for (shape = body.getShapeList(); shape != null; shape = shape.getNext()) {
				ShapeType st = shape.getType();
				if (st == ShapeType.POLYGON_SHAPE) {
				}else if (st == ShapeType.CIRCLE_SHAPE) {
					UserData data = (UserData)shape.getBody().getUserData();
					if(data != null){
						fill(data.color);
					}
					CircleShape circle = (CircleShape) shape;
					Vec2 pos = physics.worldToScreen(body.getWorldPoint(circle.getLocalPosition()));
					float radius = physics.worldToScreen(circle.getRadius());
					this.ellipseMode(CENTER);
					this.ellipse(pos.x, pos.y, radius*2, radius*2);
				}
			}
		}
	}
	
	//------------------------------------------------------------
	// @ Loop
	
	@Override
	public void draw() {
		this.background(0);
		pointer.update();
		updatePointerBody();
		brushBody();
	}
	
	private void updatePointerBody(){
		
		// 描画中は描画が始まった段階のpalletColorを使用。描画中でない時は随時描画色を更新。
		
		UserData data = new UserData();
		if(isDrawing){
			data.color = palletColor;
		}else{
			data.color = getColor();
		}
		pointerBody.setUserData(data);
		
		// Box2Dの座標系に変換しつつpointerBodyを移動。
		
		float x = (pointer.getX() - width/2)/pixelsPerMeter;
		float y = (-pointer.getY() + height/2)/pixelsPerMeter;
		pointerBody.setPosition(new Vec2(x,y));
		
	}
	
	private void brushBody(){
		if(isDrawing != pointer.isGrabbed()){
			if(isDrawing){
				onEndDrawing();
			}else{
				onStartDrawing();
			}
		}
		if(isDrawing == pointer.isGrabbed() && isDrawing){
			onProcessDrawing();
		}

		// 描画中であれば、ProcessingのAPIでラインを描く。( verticesの数だけellipseを描く )
		
		if(isDrawing){
			this.fill(palletColor);
			for(int i=0 ; i<vertices.size() ; i++){
				this.ellipse(vertices.get(i).x*pixelsPerMeter+width/2, vertices.get(i).y*-pixelsPerMeter+height/2, 30 ,30);
			}
		}
	}
	
	private void onStartDrawing(){
		
		// 描画が開始した時の処理。 vertices に格納した座標を元に Box2D で body を生成。
		
		isDrawing = true;
		vertices = new ArrayList<Vec2>();
		palletColor = getColor();
	}
	
	private void onProcessDrawing(){
		
		// 描画中の処理。 vertices に格納した座標を格納。
		
		Vec2 point = physics.screenToWorld(pointer.getX(),pointer.getY());
		vertices.add(point);
	}
	
	private void onEndDrawing(){
		
		// 描画が終了した時の処理。verticesに格納した座標を元にBox2Dでbodyを生成。
		
		isDrawing = false;
		BodyDef bd = new BodyDef();
		Body body = physics.getWorld().createBody(bd);
		for(int i =0 ; i<vertices.size() ; i++){
			CircleDef cd = new CircleDef();
			cd.radius = 0.5f;
			cd.density = physics.getDensity();
			cd.friction = physics.getFriction();
			cd.restitution = physics.getRestitution();
			cd.isSensor = physics.getSensor();
			cd.localPosition.set(vertices.get(i));
			body.createShape(cd);
			body.setMassFromShapes();
		}
		
		// 描画したBodyに色を持たせる。必要なメンバはUserDataクラスで定義しておく。
		
		UserData data = new UserData();
		data.color = palletColor;
		body.setUserData(data);
		vertices = null;
	}
	
	private int getColor(){
		int hue = this.frameCount%360;
		return this.color(hue,100,100);
	}

	//------------------------------------------------------------
	// @ KeybordEvent
		
	@Override
	public void keyPressed(){
		
		// 初期化処理
		
		World world = physics.getWorld();
		if(keyCode == 32){
			Body body;
			for (body = world.getBodyList(); body != null; body = body.getNext()) {
				world.destroyBody(body);
			}
			physics.destroy();
			createPysics();
			createPointerBody();
		}
	}
	
}

余談ですが、先日Processingが2になりましたね!
LeapSDKもバージョンアップがあった様子ですが、ちゃんと見れてません。
(というか、ライブラリ頼みなのでバージョンあがっても対応できないという…むむむ)

openFrameworks x iOS #001

openFrameworks x iOS #001 from Takepepe on Vimeo.

最近めっきり暖かくなりましたね。
花粉が飛び始めたようですが、Takepepeまだ花粉症ではないのでどこ吹く風です。
(花粉症の方すみません;)

今回は上の動画の様なものを作りました。
openFrameworksで作成した複数のiOSアプリから、
openFrameworksで作成したDesktopアプリケーションを操作します。
通信には前回同様、OSCを使っています。

前回は1対1の一方通信でしたが、今回は1対複の通信になります。
動画上では2台のiPhoneのタッチ座標を取得し、その座標をつなぐ線が描画されていますが、
コード上はOSC接続している端末数に応じて点と線が増えていくものになっています。
iPadやiOSシュミレーターでも確認できます。
(引き出しに眠っているiPhone3GSが復活。捨てなくてよかった。)
物理演算処理にはBox2Dを使用しています。

全体の流れを詳しく説明していきます。

  1. iOS端末からDesktopアプリケーションにOSC通信
  2. iOS端末の数に応じたClientインスタンスの生成
  3. Box2Dのセッティングと円を降らす処理
  4. Clientメンバの座標間で線をひく

1.iOS端末からDesktopアプリケーションにOSC通信

まずはiOSアプリをopenFrameworksで作ります。
アプリ単体で見ると、タッチした場所に円が描かれるだけの簡単なものになっています。

oscS.h (iOS端末のコード)


#define HOST "192.168.10.191" // iMacのIPアドレス
#define PORT 8000 // 接続ポート

class oscS : public ofxiPhoneApp{
	
    public:
        void setup();
        void update();
        void draw();
        float getX(float mx);
        float getY(float my);
        void touchMoved(ofTouchEventArgs & touch);
    
    private:
        ofxOscSender sender;
};

oscS.mm (iOS端末のコード)


#include "oscS.h"
#include "ofxOsc.h"

//--------------------------------------------------------------
void oscS::setup(){
    
	ofxAccelerometer.setup();
    sender.setup( HOST, PORT );
	ofBackground(0, 0, 0);
    
}

//--------------------------------------------------------------
void oscS::update(){

}

//--------------------------------------------------------------
void oscS::draw(){
	ofSetColor(255, 255, 255);
    ofCircle(mouseX, mouseY, 10);
}

//--------------------------------------------------------------
float oscS::getX(float mx){
    int res = ofGetWidth();
    return mx/res;
}

//--------------------------------------------------------------
float oscS::getY(float my){
    int res = ofGetHeight();
    return my/res;
}

//--------------------------------------------------------------
void oscS::touchMoved(ofTouchEventArgs & touch){
    ofxOscMessage m;
    m.setAddress( "/mouse/position" );
    m.addFloatArg( getX(touch.x) );
    m.addFloatArg( getY(touch.y) );
    sender.sendMessage( m );
}

2.iOS端末の数に応じたClientインスタンスの生成

ここからはDesktopアプリケーションです。
まずは「Client」クラスを作成します。
このクラスはコンストラクタにiOS端末のipアドレスを引数で渡します。
update関数が呼ばれるたび、タッチ座標を変換・更新します。

Client.h

#include "ofxOsc.h"
class Client {
    public:
        Client(std::string ip);
        void update(ofxOscMessage message);
        void draw();
        ofPoint pos;
        string getIp();
    
    private:
        string ip;
};

Client.cpp

#include "ofxOsc.h"

//--------------------------------------------------------------
Client::Client(string _ip){
    ip = _ip;
}

//--------------------------------------------------------------
void Client::update(ofxOscMessage message){
    ofxOscMessage m = message;
    
    // Clientオブジェクトの更新
    if ( m.getAddress() == "/mouse/position" && ip == m.getRemoteIp()){
        
        // 画面比率にあわせてタッチ座標を変換
        pos.x = ofGetWidth() * m.getArgAsFloat(0);
        pos.y = ofGetHeight() * m.getArgAsFloat(1);
    }
}

//--------------------------------------------------------------
void Client::draw(){
    ofCircle(pos.x, pos.y, 5);
}

//--------------------------------------------------------------
string Client::getIp(){
    return ip;
}

メインスレッドでOSCメッセージを受信した際、そのメッセージが新しい端末からの場合、
Clientインスタンスが生成されるようになっています。

oscR.h (抜粋)

#include "Client.h"
#define PORT 8000
class oscR : public ofBaseApp{
    public:
        void setup();
        void update();
        void draw();
    
    private:
        void updateClient();
        ofxOscReceiver  receiver;
        vector <Client *> clients;

}

oscR.cpp (抜粋)

#include "oscR.h"
#include "ofxOsc.h"

//--------------------------------------------------------------
void oscR::setup(){

    receiver.setup( PORT );
    
}
//--------------------------------------------------------------
void oscR::update(){
    
    // OSCクライアントの更新
    updateClient();

}
//--------------------------------------------------------------
void oscR::updateClient(){
    while( receiver.hasWaitingMessages() ){
        ofxOscMessage m;
        receiver.getNextMessage( &m );
        
        // 1つ目のOSCクライアント
        if(clients.size() == 0){
            clients.push_back(new Client(m.getRemoteIp()));
        }
        
        // 新しいOSCクライアントからのメッセージかどうか
        int flag = 1;
        for(int i=0 ; i< clients.size() ; i++){
            if(clients[i]->getIp() == m.getRemoteIp()){
                flag = 0;
            }
        }
        
        // 新しいOSCクライアントだったら
        if(flag){
            clients.push_back(new Client(m.getRemoteIp()));
        }
        
        // Clientオブジェクトの更新
        int size = clients.size();
        for(int i=0 ; i< size ; i++){
            clients[i]->update(m);
        }
    }
}
//--------------------------------------------------------------
void oscR::draw(){

    ofSetColor(50,200,255);
    ofFill();
    for(int i=0; i<clients.size(); i++) {
        clients[i]->draw();
    }

}

3.Box2Dのセッティングと円を降らす処理

次にBox2Dの実装です。
updateでカウントアップし、circlesの数が100に達したら
新しいofxBox2dCircleを作り、古いものは削除します。

oscR.h (抜粋)

#include "ofxBox2d.h"
class oscR : public ofBaseApp{
    public:
        void setup();
        void update();
        void draw();
    
    private:
        void updateCircles();
        int count;
    
        ofxBox2d box2d;
        vector <ofxBox2dCircle *> circles;
    
};

oscR.cpp (抜粋)

#include "ofxBox2d.h"

//--------------------------------------------------------------
void oscR::setup(){

    count = 0;
    
    // Box2D設定
    box2d.init();
    box2d.setGravity(0, 0.1);
    box2d.createBounds(0, 0, ofGetWidth(), ofGetHeight());
    box2d.setFPS(10.0);
    box2d.setIterations(5, 5);
    
}

//--------------------------------------------------------------
void oscR::update(){
    
    // ofxBox2dCircleを更新
    updateCircles();
    box2d.update();

}

//--------------------------------------------------------------
void oscR::updateCircles(){
    count++;
    if(count > 100){
        
        // ofxBox2dCircleの追加
        ofPoint pos;
        int r = ofRandom(10, 30);
        int x = ofRandom(r, ofGetWidth());
        pos.set(x,r);
        
        ofxBox2dCircle *c = new ofxBox2dCircle();
        c->setPhysics(1.0, 0.2, 0.2);
        c->setup(box2d.getWorld(),x,r,r);
        circles.push_back(c);
        
        // 100個以上になったら先頭のofxBox2dCircleを削除
        if(circles.size() > 100){
            vector <ofxBox2dCircle *>::iterator it = circles.begin();
            (*it)->destroy();
            delete *it;
            it = circles.erase(it);
        }
        
        count = 0;
    }
}

//--------------------------------------------------------------
void oscR::draw(){

    ofNoFill();
    ofSetColor(255,255,255);
    for(int i=0; i<circles.size(); i++) {
        circles[i]->draw();
    }

}

4.Clientメンバの座標間で線をひく

最後にiOS端末から取得した座標でofxBox2dPolygonの線をひきます。
ofPolylineに座標を追加し、ofxBox2dPolygonに代入します。
画面上で線が見える様、draw関数ではofPolylineを描画しています。 続きを読む