Processing x Android #001

Processing x Android #001 from Takepepe on Vimeo.

最近春一番が吹き始めましたよ。
シーズンも終わりに近づき、悲しい限りです…。

今回はProcessing for Android について触れたいと思います。
Processing for AndroidはProcessingで比較的簡単にアプリをつくれる開発環境です。
ご存知のとおり、スマホ・タブレットにはたくさのセンサーが搭載されており、自分が使用しているNexus7には以下のセンサーが搭載されています。

・GPS
・電子コンパス
・光センサ
・加速度センサ
・ジャイロスコープ
・NFC
・磁気センサ

アイディア次第で何でも出来そうでワクワクしますね。特にNFCは今のところiOS端末には無いセンサーなので気になる人も多いのでは。
Processing for Androidなら、少ない手順でこれらにアクセス出来ます。

上の動画を見てのとおり、今回作成したアプリは端末の画面を見て遊ぶものではなく、センサー値やタッチ情報をiMacに送り、デスクトップで稼働しているProcessingを操作するものです。おおまかな流れを書いていきます。なお、開発環境の準備については良記事がたくさんありますので、そちらを参照してください。

  1. 動作環境の準備
  2. センサー値の取得
  3. OSCによる通信
  4. デスクトップアプリのスケッチ

1.動作環境の準備

今回はWifi環境のもと、OSCでAndroid・Mac間の値のやり取りを行っています。
まずはMacのIPアドレスをメモしておきましょう。
あとは、同じネットワークにAndroid端末を接続しておきます。
MacからAndroidに値を送る場合はAndroidのIPアドレスもメモしておきましょう。

Processing for Androidで、Androidアプリが開発できる環境が整ったら、
必要なライブラリを開発環境にインストールします。

ketai

https://code.google.com/p/ketai/

Androidの各センサーや、イベント、OSの標準UI、SQLなど扱うことが出来るライブラリです。
多くの端末でテストしていないため動作保証は出来ませんが、自分のNexus7は問題なく使えました。
ライブラリの作者も以下の環境でテストしている様です。今回は「Ketai_v8」を使います。

Test platforms : Android 4.1, 4.1.2, 4.0.2
Test Hardware: Nexus 7, Galaxy 3s, Nexus S, Transformer Prime

しかしながら、携帯から来ているらしいこのライブラリ名、違和感ありですね w(ケタイって)

oscP5

http://www.sojamo.de/libraries/oscP5/

OSCで値をやり取りするためにoscP5を使用します。
OSCはOpenSound Controlの略で、ネットワーク経由でリアルタイム通信が出来る通信プロトコルです。 http://ja.wikipedia.org/wiki/OpenSound_Control

2.センサー値の取得

さて、ここからコードに入ります。
が、その前に、先ほどダウンロードしたファイルに「examples」がありますので、いくつか気になるものをコンパイルしてみてください。

今回は「KetaiGesture」「KetaiSensor」の2つに焦点を絞っています。
まずはセンサーを有効にし、センサーイベントを受け取る関数を書きます。
今回有効にするのは「加速度センサ(Accelerometer)、ジャイロスコープ(Gyroscope)、電子コンパス(Orientation)、光センサ(Light)」です。

Android.pde
void setup(){

	gesture = new KetaiGesture(this);
    sensor = new KetaiSensor(this);
    
    sensor.enableAccelerometer();
    sensor.enableGyroscope();
    sensor.enableOrientation();
    sensor.enableLight();
    sensor.start();
    
}

public boolean surfaceTouchEvent(android.view.MotionEvent event) {
  
  super.surfaceTouchEvent(event);
  return gesture.surfaceTouchEvent(event);
  
}

配列に値を入れるまえに、ローパスフィルターを通しています。
センサー値は手ぶれや様々な誤差から、思った通りのスムーズな値が得られません。
今回は以下の記事を参考にセンサー値を調整しています。

アンドロイドな日々 http://android.ohwada.jp/archives/334

ピンチイベント、光センサーイベントの関数も書いておきます。

Android.pde

void onAccelerometerEvent(float x, float y, float z){
	
    float AX = 0.9*GValue.get(0)+0.1*x;
    float AY = 0.9*GValue.get(1)+0.1*y;
    float AZ = 0.9*GValue.get(2)+0.1*z;
    
    AValue.set(0,AX);
    AValue.set(1,AY);
    AValue.set(2,AZ);
  
}

void onGyroscopeEvent(float x, float y, float z){
	
    float GX = 0.9*GValue.get(0)+0.1*x;
    float GY = 0.9*GValue.get(1)+0.1*y;
    float GZ = 0.9*GValue.get(2)+0.1*z;
    
    GValue.set(0,GX);
    GValue.set(1,GY);
    GValue.set(2,GZ);
  
}

void onOrientationEvent(float x, float y, float z){
	
    float OX = 0.9*OValue.get(0)+0.1*x;
    float OY = 0.9*OValue.get(1)+0.1*y;
    float OZ = 0.9*OValue.get(2)+0.1*z;
    
    OValue.set(0,OX);
    OValue.set(1,OY);
    OValue.set(2,OZ);
  
}

void onPinch(float x, float y, float d){
  
  pinchValue = d;
  
}

void onLightEvent(float v){
  
  lightValue = v;
  
}

3.OSCによる通信

今度はOSC通信に必要なインスタンスを準備します。まずはAndroid側。
1.でメモしておいた、iMacのネットワークアドレスをNetAddressインスタンスにセットします。
ポート番号は12000としておきます。Androidは12001に。
また、メニュー > Android > Sketch Permissions でINTERNETにチェックをいれておきます。

Android.pde

void setup(){
  
  oscP5 = new OscP5(this,12001);
  iMacAddress = new NetAddress("192.168.10.191",12000); // IPアドレスは適宜変更
  
}

ここで新たに、デスクトップで稼働するスケッチを新規作成します。
これが後のセンサー値を受け取るデスクトップアプリになります。
こちらのスケッチでも、先程と同様にOscP5インスタンスとNetAddressインスタンスを作成します。
Androidのスケッチとはとポート番号が入れ替えになります。

Desktop.pde

void setup(){
  
  oscP5 = new OscP5(this,12000);
  androidAddress = new NetAddress("192.168.10.221",12001); // IPアドレスは適宜変更
  
}


最後にAndroid側からOscMessageで値を送信し、
iMacで稼働しているアプリに値がちゃんと届いていることを確認します。

Android.pde

void draw(){
  
  OscMessage sendValue = new OscMessage("/AndroidOSC");
  
  // ピンチ値
  sendValue.add(pinchValue);
  
  // 光センサー
  sendValue.add(lightValue);
  
  // 加速度センサー
  sendValue.add(AValue.get(0));
  sendValue.add(AValue.get(1));
  sendValue.add(AValue.get(2));
  
  // ジャイロスコープセンサー
  sendValue.add(GValue.get(0));
  sendValue.add(GValue.get(1));
  sendValue.add(GValue.get(2));
  
  // 電子コンパス
  sendValue.add(OValue.get(0));
  sendValue.add(OValue.get(1));
  sendValue.add(OValue.get(2));
  
  // OscMessageをiMacに送信
  oscP5.send(sendValue, iMacAddress);
  
}

Desktop.pde

void oscEvent(OscMessage theOscMessage) {

	if(theOscMessage.checkAddrPattern("/AndroidOSC") == true) {
    	if(theOscMessage.checkTypetag("fffffffffff")) {
        	
            // ピンチ値
        	pinchValue = theOscMessage.get(0).floatValue();
            
            // 光センサー
            lightValue = theOscMessage.get(1).floatValue();
            
            // 加速度センサー
            AValue.set(0,theOscMessage.get(2).floatValue());
            AValue.set(1,theOscMessage.get(3).floatValue());
            AValue.set(2,theOscMessage.get(4).floatValue());
            
            // ジャイロスコープセンサー
            GValue.set(0,theOscMessage.get(5).floatValue());
            GValue.set(1,theOscMessage.get(6).floatValue());
            GValue.set(2,theOscMessage.get(7).floatValue());
            
            // 電子コンパス
            OValue.set(0,theOscMessage.get(8).floatValue());
            OValue.set(1,theOscMessage.get(9).floatValue());
            OValue.set(2,theOscMessage.get(10).floatValue());
            
    	}
	}
    
}

4.デスクトップアプリのスケッチ

ここまで来れば、もう完成間近です。
Processingでは3D表現も簡素なものであればすぐに出来ます。
あまり3Dソフトに慣れていないので、フリーのドロイド君を探し、Blendrでobj出力しました。
Processingに読み込む時、いろいろエラーが出た人は、書き出しオプションにチェックを入れて対処しましょう。
冒頭動画のソースコード全文は以下の様になっています。

Desktop.pde

import saito.objloader.*;
import oscP5.*;
import netP5.*;

int W = 180;
int WIDTH = 720-W;
int HEIGHT = 405;
int NUM = 128;

OscP5 oscP5;
OBJModel objm;

float lightValue;
float scaleValue;
ArrayList<Float> AValue = new ArrayList<Float>();
ArrayList<Float> GValue = new ArrayList<Float>();
ArrayList<Float> OValue = new ArrayList<Float>();

void setup() {
  
    size(720, 405, OPENGL);
    noStroke();
    
    scaleValue = 40;
    for(int i = 0; i < 3; i++){
      AValue.add(new Float(0));
      GValue.add(new Float(0));
      OValue.add(new Float(0));
    }
    oscP5 = new OscP5(this,12000);
    
    objm = new OBJModel(this);
    objm.load("Android3D.obj");
    setConsole();
    
}

void draw() {
  
  fill(0);
  noStroke();
  rect(0,0,WIDTH,HEIGHT);
  
  String info = "";
  info += "scaleValue:"+scaleValue+"\n";
  info += "lightValue:"+lightValue+"\n";
  info += "AccelerometerX:"+AValue.get(0)+"\n";
  info += "AccelerometerY:"+AValue.get(1)+"\n";
  info += "AccelerometerZ:"+AValue.get(2)+"\n";
  info += "GyroscopeX:"+GValue.get(0)+"\n";
  info += "GyroscopeY:"+GValue.get(1)+"\n";
  info += "GyroscopeZ:"+GValue.get(2)+"\n";
  info += "OrientationX:"+OValue.get(0)+"\n";
  info += "OrientationY:"+OValue.get(1)+"\n";
  info += "OrientationZ:"+OValue.get(2)+"\n";
  drawConsole(info);
  
  println(lightValue);
  ambientLight(200,100,100);
  directionalLight(255,255,100,-1,0,0);
  pointLight(100,200,255, 100, 100, 100);
  spotLight(lightValue,lightValue,lightValue, 100, 100, 1000, 0, 0, -1, PI, 2);
  
  translate(WIDTH/2,HEIGHT/2,1);
  rotateX(GValue.get(0)*-1);
  rotateY(GValue.get(1));
  rotateZ(GValue.get(2)*-1);
  scale(scaleValue);
  objm.draw();
  
}

void oscEvent(OscMessage theOscMessage) {
  if(theOscMessage.checkAddrPattern("/AndroidOSC") == true) {
    if(theOscMessage.checkTypetag("fffffffffff")) {
      float pinchValue = theOscMessage.get(0).floatValue();
      if(pinchValue>2){
        scaleValue++;
      }
      if(pinchValue < -2){
        scaleValue--;
      }
      if(scaleValue < 1){
        scaleValue = 1;
      }
      lightValue = theOscMessage.get(1).floatValue();
      AValue.set(0,theOscMessage.get(2).floatValue());
      AValue.set(1,theOscMessage.get(3).floatValue());
      AValue.set(2,theOscMessage.get(4).floatValue());
      GValue.set(0,theOscMessage.get(5).floatValue());
      GValue.set(1,theOscMessage.get(6).floatValue());
      GValue.set(2,theOscMessage.get(7).floatValue());
      OValue.set(0,theOscMessage.get(8).floatValue());
      OValue.set(1,theOscMessage.get(9).floatValue());
      OValue.set(2,theOscMessage.get(10).floatValue());
    }
  }
}

Android.pde
import ketai.net.*;
import ketai.ui.*;
import ketai.sensors.*;
import oscP5.*;
import netP5.*;

KetaiGesture gesture;
KetaiSensor sensor;
public OscP5 oscP5;
NetAddress iMacAddress;

float pinchValue;
float lightValue;
ArrayList<Float> AValue = new ArrayList<Float>();
ArrayList<Float> GValue = new ArrayList<Float>();
ArrayList<Float> OValue = new ArrayList<Float>();

void setup(){
  size(displayWidth, displayHeight);
  orientation(LANDSCAPE);
  
  oscP5 = new OscP5(this,12001);
  iMacAddress = new NetAddress("192.168.10.191",12000);
  
  pinchValue = 0;
  lightValue = 0;
  for(int i = 0; i < 3; i++){
    AValue.add(new Float(0));
    GValue.add(new Float(0));
    OValue.add(new Float(0));
  }
  
  gesture = new KetaiGesture(this);
  sensor = new KetaiSensor(this);
  sensor.enableLight();
  sensor.enableAccelerometer();
  sensor.enableGyroscope();
  sensor.enableOrientation();
  sensor.start();
  
}

public boolean surfaceTouchEvent(android.view.MotionEvent event) {
  
  super.surfaceTouchEvent(event);
  return gesture.surfaceTouchEvent(event);
  
}

void draw(){
  
  pinchValue += (1-pinchValue)/5;
  
  String info = "";
  info += "AccelerometerX:"+AValue.get(0)+"\n";
  info += "AccelerometerY:"+AValue.get(1)+"\n";
  info += "AccelerometerZ:"+AValue.get(2)+"\n";
  info += "GyroscopeX:"+GValue.get(0)+"\n";
  info += "GyroscopeY:"+GValue.get(1)+"\n";
  info += "GyroscopeZ:"+GValue.get(2)+"\n";
  info += "OrientationX:"+OValue.get(0)+"\n";
  info += "OrientationY:"+OValue.get(1)+"\n";
  info += "OrientationZ:"+OValue.get(2)+"\n";
  
  OscMessage sendValue = new OscMessage("/AndroidOSC");
  sendValue.add(pinchValue);
  sendValue.add(lightValue);
  sendValue.add(AValue.get(0));
  sendValue.add(AValue.get(1));
  sendValue.add(AValue.get(2));
  sendValue.add(GValue.get(0));
  sendValue.add(GValue.get(1));
  sendValue.add(GValue.get(2));
  sendValue.add(OValue.get(0));
  sendValue.add(OValue.get(1));
  sendValue.add(OValue.get(2));
  oscP5.send(sendValue, iMacAddress);
  
  background(0);
  textSize(20);
  fill(255);
  text(info,50,50);
  
}

void onAccelerometerEvent(float x, float y, float z){
  
  float AX = 0.9*GValue.get(0)+0.1*x;
  float AY = 0.9*GValue.get(1)+0.1*y;
  float AZ = 0.9*GValue.get(2)+0.1*z;
  
  AValue.set(0,AX);
  AValue.set(1,AY);
  AValue.set(2,AZ);
  
}

void onGyroscopeEvent(float x, float y, float z){
  
  float GX = 0.9*GValue.get(0)+0.1*x;
  float GY = 0.9*GValue.get(1)+0.1*y;
  float GZ = 0.9*GValue.get(2)+0.1*z;
  
  GValue.set(0,GX);
  GValue.set(1,GY);
  GValue.set(2,GZ);
  
}

void onOrientationEvent(float x, float y, float z){
  
  float OX = 0.9*OValue.get(0)+0.1*x;
  float OY = 0.9*OValue.get(1)+0.1*y;
  float OZ = 0.9*OValue.get(2)+0.1*z;
  
  OValue.set(0,OX);
  OValue.set(1,OY);
  OValue.set(2,OZ);
  
}

void onLightEvent(float v){
  
  lightValue = v;
  
}

void onPinch(float x, float y, float d){
  
  pinchValue = d;
  
}

Console.pde

import processing.video.*;
Capture capture;
PFont font;

void setConsole(){
  font = loadFont("Helvetica-12.vlw");
  capture = new Capture(this, 640,480,30);
  capture.start();
}

void drawConsole(String str){
  float H = W*0.75;
  int ts = 11;
  float X = width-W;
  float Y = height-H;
  if (capture.available()){
    capture.read();
    image(capture, X, Y, W, H);
  }
  
  fill(17);
  rect(X,0,W,Y);
  
  stroke(51);
  line(X,0,X,height);
  noStroke();
  
  fill(255);
  textSize(ts);
  textFont(font, ts);
  text(str,X+5,ts+5);
  text(str,X+5,ts+5);
}