2012/02/15

ProcessingとTwitterをくっつける

くろねこさんが、昨日のサンプルをさっそく fork して下さいました(→くろねこさんの記事)。

パーティクルの動きの改良に加え、ソースコードもリファクタリングされて品質が上がっています。さすが!



さてさて、今日は Processing を Twitter に組み合わせてへんなのを作りますよー∩( ・ω・)∩

なんか、テキスト領域に Twitter のユーザ名を入れると、フォローしているおともだちが3次元空間を漂うというなんとも愉快なスケッチです。


実は、ほとんど昨日のソースコードを流用しただけの手抜き更新というのは内緒だよ。



【Processing と GUI】

本日一発目のテーマは、Processing スケッチへの GUI コンポーネントの組み込みです。 とりあえず完成すると以下のようになります(少し見づらいですが、動画の下の方にある藍色の矩形がテキストフィールドです)。


Processing 公式サイトにはいくつかのサードパーティ製ライブラリが紹介されています。僕はその中から、ControlP5 というライブラリを選びました(現時点の最新バージョンは ControlP5 0.5.4)。

OpenGL レンダラの環境でテキストフィールドを使用するためのミニマルなサンプルコードは以下の通り。なお、外部ライブラリの設置方法に関しては、うえちょこさんの記事に詳しく書かれています。

【ソースコード(展開してご覧ください)
import processing.opengl.*;
import controlP5.*;

ControlP5 controlP5;
Textfield tfUserName;  // テキストフィールド


void setup() {
  size(400, 300, OPENGL);
  controlP5 = new ControlP5(this);
  
  tfUserName = controlP5.addTextfield("userName", 10, 10, 200, 20);
  tfUserName.setFocus(true);
}


void draw() {
  hint(ENABLE_DEPTH_TEST);
  background(0);

  /* ============================== */
  /* ここに描画用のコードを書きます */
  /* ============================== */
  
  // GUIコンポーネント描画のためにカメラ位置をリセット
  camera();
  hint(DISABLE_DEPTH_TEST);
}


// テキストが入力された際にコールバックされるメソッド
public void userName(String txt) {
  if(txt.trim().length() < 1) return;

  println("「" + txt + "」が入力されました");
}

デモ動画のソースコードは……適当に作ったため、あまり見せられる代物ではありませんが、たぶん昔の僕だったら「汚くてもいいからソースコードを知りたい」と思ったでしょうから、こっそり晒しておきますね。

【おまけソースコード(展開してご覧ください)
import processing.opengl.*;
import javax.media.opengl.*;  
import controlP5.*;

final int FIELD_SIZE = 1000;
final int FIELD_STEP =  100;

ControlP5 controlP5;
Textfield tfUserName;       // テキストフィールド

PFont  font;  
long   time;  

List<Message> messageList;  // メッセージリスト

void setup() {
  size(640, 480, OPENGL);
  
  messageList = new LinkedList<Message>();
  
  controlP5 = new ControlP5(this);
  
  tfUserName = controlP5.addTextfield("textfield", 10, 10, 200, 20);
  tfUserName.setFocus(true);
  
  font = createFont("Meiryo", 48);
}


void draw() {
  background(0);

  PGraphicsOpenGL pgl = (PGraphicsOpenGL)g;  

  // カメラの設定
  float angle = 0.5f * radians(time);      
  camera(300*cos(angle), -300*sin(angle), 300*sin(angle),   
         0,               0,              0,   
         0,               1,              0);
  
  // 地面の描画
  pushStyle();
  stroke(255, 0, 0);  
  for(int i = -FIELD_SIZE; i <= FIELD_SIZE; i += FIELD_STEP) { 
    line(          i, 0, -FIELD_SIZE,          i, 0, FIELD_SIZE);   
    line(-FIELD_SIZE, 0,           i, FIELD_SIZE, 0,          i);
  }
  popStyle();
  
  // α合成を有効にする
  GL gl = pgl.beginGL();
  gl.glEnable(GL.GL_BLEND);
  gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);  
  gl.glDisable(GL.GL_DEPTH_TEST);
  pgl.endGL();
  
  // メッセージの描画
  for(Iterator iter = messageList.iterator(); iter.hasNext();) {
    Message msg = (Message)iter.next();
    // 更新して死んでたらリストから削除
    if(!msg.update()) iter.remove(); 
  }
  time++;
  
  // GUI コンポーネント描画のための設定
  camera();
  gl = pgl.beginGL();
  gl.glDisable(GL.GL_DEPTH_TEST);  // 深度テストを無効化
  gl.glEnable(GL.GL_BLEND);
  gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);  
  pgl.endGL();
}


// テキストが入力された際にコールバックされるメソッド
public synchronized void textfield(String txt) {
  // 適当な初期位置を指定して、リストに新たなメッセージを追加する
  messageList.add(new Message(txt,
      new PVector(random(-100, 100), 0, random(-100, 100))));
}


// メッセージクラス
class Message {
  private final int   BASECOLOR = 0xFFFFFFFF;  // 基本色
  private final int   TTL       = 200;         // 生存時間
  private final float dY        = -0.5;        // y座標の増分

  private String  message;       // メッセージ
  private PVector pos;           // 位置
  private int     time;          // 経過時間  
  
  // コンストラクタ
  // -----------------------------
  // メッセージと初期位置を指定する
  Message(String msg, PVector pos) {
    this.message = msg;
    this.pos     = pos;
  }
  
  // update()
  // -----------------------------
  // 状態を更新し、生死状態を返す
  // true:  有効(生きている)
  // false: 無効(死んでいる)
  boolean update() {
    if(++time > TTL) return false;
    pos.y += dY;
    
    // ビルボーディング 
    pushMatrix();
    translate(pos.x, pos.y, pos.z);  // 物体を並進(ローカル座標系)  
      
    // モデル-ビュー行列を取得  
    PMatrix3D builboardMat = (PMatrix3D)g.getMatrix();  
    // 回転成分を単位行列に  
    builboardMat.m00 = builboardMat.m11 = builboardMat.m22 = 1;  
    builboardMat.m01 = builboardMat.m02 =   
    builboardMat.m10 = builboardMat.m12 =   
    builboardMat.m20 = builboardMat.m21 = 0;  
    
    resetMatrix();  
    applyMatrix(builboardMat);  

    // アルファ値を計算
    pushStyle();
    int a = 255 - 255 * time / TTL;
    fill(a << 24 | BASECOLOR & 0xFFFFFF);
    
    // フォントを設定
    textFont(font, 24);  
    textAlign(CENTER);
    
    // テキストを描画
    text(message, 0, 0);
    popStyle();    
       
    popMatrix();
    return true;
  }
}



【Processing と Twitter】

GUI は一旦おいといて(つ´∀`)つ

今度は Twitter との連繋を考えてみます。ここでのゴールは、ひとまず Twitter でフォローしている方々のアイコンを 3次元空間中に散りばめる事です。


Twitter API を直叩きするのも面倒なので、Twitter4J という便利なライブラリをちょこっと使います。ライブラリは公式サイト あるいは GitHub から入手できます。今回は最新安定版である twitter4j-2.2.5.zip を選択しました。

今日はもう疲れ始めたので、OAuth 認証の要らないフォロー/フォロワー一覧を取得するところまでやろうかと思います。

JavaDoc などを参照しつつ、フレンド(自分がフォローしているアカウント) を調べるプログラムを Processing で書いてみました。

【ソースコード(展開してご覧ください)
import twitter4j.*;

final String USER_NAME = "tercel_s";

Twitter twitter;

void setup() {
  twitter = new TwitterFactory().getInstance();
  
  try {
    
    // 指定したユーザがフォローしているアカウント一覧を取得
    IDs friends = twitter.getFriendsIDs(USER_NAME, -1);
    long[] friendsIDs = friends.getIDs();
    
    // Twitter ID からユーザ情報を逐次取得して表示
    for(long id : friendsIDs) {
      User user = twitter.showUser(id);
      println(user.getName() + " (" + user.getScreenName() + ")");
    }
    
  } catch (TwitterException ex) {
    if(ex.isCausedByNetworkIssue()) {
      // 何らかの事情でネットワークに接続できませんでした
    } else {
      int statusCode = ex.getStatusCode();
      switch(statusCode) {
        case 400:  // Bad Request
          // リクエストに不正があるかレートリミットに達しました
          break;
        case 401:  // Unauthorized
          // 認証情報に間違いがあります
          break;
        case 403:  // Forbidden
          // リクエストが拒否されました
          break;
        case 404:  // Not Found
          // 存在しないリソースにアクセスしました
          break;
        case 500:  // Server Internal Error
          // サーバ障害が発生しています
          break;
        case 502:  // Bad Gateway
          // Twitterサービスに障害が発生しています
          break;
        case 503:  // Service Unavailable
          // Twitterが過負荷状態です
          break;
      }
    }
  }
  
  noLoop();
}

void draw() {
  // 
}

実行すると、コンソールにフレンド一覧が列挙されます。

ただこれ、けっこう時間がかかります。ですので、フォロー一覧の読み込み処理は別スレッドに移した方がよいでしょう。適当にマルチスレッド化するとこうなります(本当に適当なので、よい子はマネしないでね)。

【ソースコード(展開してご覧ください)
import twitter4j.*;

final String USER_NAME = "tercel_s";

Twitter twitter;
boolean isLoading;

void setup() {
  twitter = new TwitterFactory().getInstance();
  
  // マルチスレッドだよ∩( ・ω・)∩
  new Thread() {
    public synchronized void run() {
      isLoading = true;
      try {
        // 指定したユーザがフォローしているアカウント一覧を取得
        IDs friends = twitter.getFriendsIDs(USER_NAME, -1);
        long[] friendsIDs = friends.getIDs();
        
        // Twitter ID からユーザ情報を逐次取得して表示
        for(long id : friendsIDs) {
          User user = twitter.showUser(id);
          println(user.getName() + " (" + user.getScreenName() + ")");
        }
        isLoading = false;
      } catch (TwitterException ex) {
        /* 省略 */
      } finally {
        isLoading = false;  // デッドロック回避用
      }
    }
  }.start();
}

void draw() { 
  // 
}

これさえできれば、デモ動画の作品を作るのは簡単です。ほとんど昨日のパーティクルをユーザのアイコンに差し換えただけです。

【おまけソースコード(展開してご覧ください)
import processing.opengl.*;
import javax.media.opengl.*;
import twitter4j.*;

final int FIELD_SIZE = 1000;
final int FIELD_STEP =  100;

final String USER_NAME = "tercel_s";  
boolean      isLoading;

List<Agent> agentList;  // フレンドアイコンのリスト

long time;

void setup() {
  size(800, 600, OPENGL);  
  
  agentList = new ArrayList<Agent>();

  // マルチスレッドでフレンドを読み込む
  new Thread(new FriendLoader()).start();
  hint(ENABLE_DEPTH_SORT);
}

void draw() { 
  background(0);
  
  /* ================== */
  /* カメラを適当に設定 */
  /* ================== */
  float angle = 0.05f * radians(time);
  
  camera(800, -200,   0, 
           0,    0,   0, 
           0,    1,   0);
  rotateY(angle);  // ちょっと回す

  pushStyle();
  
  // テクスチャの加算合成を有効にする
  PGraphicsOpenGL pgl = (PGraphicsOpenGL)g;
  GL gl = pgl.beginGL();
  
  gl.glEnable(GL.GL_BLEND);
  gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
  pgl.endGL();  
  
  noStroke();
  fill(0);
  
  // フレンド一覧を表示
  for(int i = 0; i < agentList.size(); ++i) {
    agentList.get(i).update();
  }

  popStyle();

  /* ============== */
  /* 地面とかの描画 */
  /* ============== */
  pushStyle();
  // 地面の描画 
  stroke(0, 255, 0, 100);
  for(int i = -FIELD_SIZE; i <= FIELD_SIZE; i += FIELD_STEP) { 
    line(          i, 0, -FIELD_SIZE,          i, 0, FIELD_SIZE);   
    line(-FIELD_SIZE, 0,           i, FIELD_SIZE, 0,          i);
  }
  
  popStyle();
  
  time++;
  mm.addFrame();
  if(time > 60 * 60) {
    mm.finish();
    background(0);
    noLoop();
  }
}


// フレンドを読み込むためのクラス
class FriendLoader implements Runnable {
  public void run() {
    isLoading = true;
    Twitter twitter = new TwitterFactory().getInstance();
    try {
      // まずは自分自身をリストに追加
      User me = twitter.showUser(USER_NAME);
      PImage myIcon = loadImage(me.getProfileImageURL().toString());
      myIcon.resize(100, 100);
      agentList.add(new Agent(myIcon, new PVector()));
            
      // 自分がフォローしているアカウント一覧を取得
      IDs friends = twitter.getFriendsIDs(USER_NAME, -1);
      long[] friendsIDs = friends.getIDs();
        
      // Twitter ID からユーザ情報を逐次取得
      for(long id : friendsIDs) {
        User user = twitter.showUser(id);
          
        // アイコン取得とリサイズ
        PImage profileImage = loadImage(user.getProfileImageURL().toString());
        if(profileImage == null) continue;
        
        profileImage.resize(100, 100);

        // 位置の設定
        float x = random(-1000, 1000);
        float z = random(-1000, 1000);
        agentList.add(new Agent(profileImage, new PVector(x, 0, z)));
      }
      
      isLoading = false;
    } catch (TwitterException ex) {
      /* 省略 */
    } finally {
      isLoading = false;  // デッドロック回避用
    }
  }
}


// エージェント
class Agent {
  private final float TERRITORY_SIZE = 2000;
  private final float NOISE_SCALE    = 0.001f;
  private final float AGENT_SIZE     = 50; 
  
  private PImage  tex;     // テクスチャ
  
  private PVector center;  // 縄張りの中心座標
  private PVector offset;  // 中心からのオフセット
  private PVector pos;     // 座標
  
  private long time;      // 経過時間
  float xOffset, zOffset; // ノイズ生成用
  
  Agent(PImage tex, PVector center) {
    this.center = center;
    this.tex    = tex;
    pos         = new PVector();
    offset      = new PVector();
    xOffset     = random(TERRITORY_SIZE);
    zOffset     = random(TERRITORY_SIZE);
  }
  
  PVector getPos() {
    return pos;
  }
  
  void update() {
    if (tex == null) return;
    time++;
    
    offset.x = (noise((time + xOffset) * NOISE_SCALE) - 0.5f) * TERRITORY_SIZE;
    offset.z = (noise((time + zOffset) * NOISE_SCALE) - 0.5f) * TERRITORY_SIZE;
    
    pushMatrix();
    pos.set(center.x + offset.x, center.y, center.z + offset.z);
    translate(pos.x, pos.y, pos.z);
    
    // モデル-ビュー行列を取得
    PMatrix3D builboardMat = (PMatrix3D)g.getMatrix();
    // 回転成分を単位行列に
    builboardMat.m00 = builboardMat.m11 = builboardMat.m22 = 1;
    builboardMat.m01 = builboardMat.m02 = 
    builboardMat.m10 = builboardMat.m12 = 
    builboardMat.m20 = builboardMat.m21 = 0;
  
    resetMatrix();
    applyMatrix(builboardMat);
        
    beginShape(QUADS);
    texture(tex);
    vertex(-0.5f * AGENT_SIZE, -AGENT_SIZE, 0, 0,         0);
    vertex(-0.5f * AGENT_SIZE,           0, 0, 0,         tex.height-1);
    vertex( 0.5f * AGENT_SIZE,           0, 0, tex.width, tex.height-1);
    vertex( 0.5f * AGENT_SIZE, -AGENT_SIZE, 0, tex.width, 0);    
    endShape();
        
    popMatrix();
  }
}

あとはこの 2 つを適当に組み合わせれば、冒頭の動画のようなものが作れると思います。

なしくずしてきにめでたしめでたし。



そんなこんなで久しぶりに Twitter ネタを書きましたが、このプログラムにはまだまだアラがあります。

まず、Twitter の API 呼び出しには制限が設けられており、このプログラムを試しているとあっという間に API 上限に引っかかってしまいます。フォローしているアカウントの情報を一人ずつ取得していくと、アホみたいなスピードで限界に近付いていきます。

次に、計算機資源の問題があります。今はとりあえずフレンド情報を全読みしていますが、それこそ何十万人もフォローしているような人が動かしたら、あっという間にメモリを喰い尽くして死ぬでしょう。

それでもまぁ、とりあえず Twitter と P5 をくっつけてそれっぽい事ができたので今日はよしとします。ふぅ、つかれた(´-ω-`)

2 件のコメント:

ひとことどうぞφ(・ω・,,)