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