Processing #001 LeapMotion x Box2D from Takepepe on Vimeo.
ご無沙汰してます、Takepepeです。
世間は梅雨、ついにクーラーをかけながらデモムービーを撮る季節に突入しました。
すっかり月1更新になってしまった今日このごろ。反省しないと!
さて、今日は動画のとおり、Leapを使ってお絵描きするインスタレーションをProcessingで作成しました。
今回のキモはLeapSDKをラップしているライブラリから提供される
標準のジェスチャーや座標を、意図した値に変換するところです。
使用しているライブラリは以下
BoxWrap2d : http://wiki.processing.org/w/BoxWrap2d
LeapMotionP5 : https://github.com/mrzl/LeapMotionP5
コードはEclipseで書いていますので、このままProcessingのIDEにはっつけても動かないのでご注意を。
OPENGLの恩恵を受けていないコードですが、レンダリングモードをOPENGLにしています。
EclipseでProcessingを書く場合、OPENGLの使用にはcore.jarだけではなく
他にも入れないといけない.jarがありますので適当にググってください。
いつもの様に、順をおって解説していきます。
- LeapPointerP5の作成
- Box2Dの初期設定
- LeapPointerP5の座標をBox2Dに落とし込む
- 両手の状態で描画判定
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もバージョンアップがあった様子ですが、ちゃんと見れてません。
(というか、ライブラリ頼みなのでバージョンあがっても対応できないという…むむむ)