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もバージョンアップがあった様子ですが、ちゃんと見れてません。
(というか、ライブラリ頼みなのでバージョンあがっても対応できないという…むむむ)