2012/02/14

Processingできらきら3次元パーティクル

今日はこういうものを作ります。たぶんきれいですね。


このスケッチは、いくつかのテクニックを組み合わせて作ります。

恐らく、Processing ユーザにとっては馴染みの薄いものもあるかと思いますので、今日はそれらをひとつひとつ紹介しつつ完成形に持っていこうと考えています。



【ビルボードをつくる】

ビルボードとは、3次元空間中のカメラに対して常に正面を向くような2次元オブジェクトの事です。一昔前のゲームなどではよく応用されていた技術です(今もかな)。これ自体が独立したひとつのトピックなので、まずはここから始めてみましょう。

以下のサンプルは、テキスト(Hello, World)をビルボード上に表示するというものです。


【理論の解説】

ビルボードは、以下のような同次形のモデル・ビュー変換行列
\begin{align}
{}^{\Sigma_{\mathrm{View}}}\vect{M}_{\Sigma_{\mathrm{Model}_{i}}} =
\left(\!
\begin{array}{ccc|c}
m_{00} & m_{01} & m_{02} & m_{03} \\
m_{10} & m_{11} & m_{12} & m_{13} \\
m_{20} & m_{21} & m_{22} & m_{23} \\ \hline
m_{30} & m_{31} & m_{32} & m_{33}
\end{array}
\!\right)
\end{align}――のうち、左上の3×3の小行列を単位行列にした結果
\begin{align}
\left( \!
\begin{array}{ccc|c}
1 & 0 & 0 & m_{03} \\
0 & 1 & 0 & m_{13} \\
0 & 0 & 1 & m_{23} \\ \hline
m_{30} & m_{31} & m_{32} & m_{33}
\end{array}
\!\right)
\end{align}を、新たなモデル・ビュー行列として設定します。たったこれだけ。かんたんですね。

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

PFont font;
long  time;

void setup() {
  size(800, 600, OPENGL);
  
  // フォント生成(メイリオ)
  font = createFont("Meiryo", 48);
  
  // デプスソートを有効にする
  // これがないとテキスト表示がおかしくなる
  hint(ENABLE_DEPTH_SORT);
  
}

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

  /* ================ */
  /* ビルボーディング */
  /* ================ */
  pushMatrix();
  translate(100, 0, 0);  // 物体を並進(ローカル座標系)
  
  // モデル-ビュー行列を取得
  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);
  
  fill(255);
  stroke(0);
  textFont(font, 24);
  textAlign(CENTER);
  text("Hello, World!", 0, 0);

  popMatrix();

  /* ============== */
  /* 地面とかの描画 */
  /* ============== */
  pushStyle();
  // 地面の描画 
  stroke(0, 255, 0);
  for(int x = -1000; x <= 1000; x += 100)
    line(x, 0, -1000, x, 0, 1000); 
  for(int z = -1000; z <= 1000; z += 100)
    line(-1000, 0, z, 1000, 0, z);
  
  // 回転軸の描画
  stroke(255, 0, 0);
  line(0, -500, 0, 0, 500, 0);
  popStyle();
  
  time++;
}



【自然なランダムウォークをつくる】

自然なランダムウォークというと少しおかしな日本語ですが、あちこちに動きが飛ばず、なめらかな軌跡を描くような動きを作ります。

タネを明かせば Perlin ノイズを何のひねりもなく使っただけで、当たり判定とかは考慮していません。


【ソースコード(展開してご覧ください)

ここでは、動く個体を Agent クラスにまとめています。
import processing.opengl.*;

List<Agent> agentList;
long  time;

void setup() {
  size(800, 600, OPENGL);
  
  agentList = new ArrayList<Agent>();
  for(int i = 0; i < 200; ++i) {
    float x = random(-200, 200);
    float z = random(-200, 200);
    agentList.add(new Agent(new PVector(x, 0, z)));
  }
}

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

  pushStyle();
  noStroke();
  colorMode(HSB, agentList.size() -1, 100, 100);
  for(int i = 0; i < agentList.size(); ++i) {
    fill(i, 100, 100);
    agentList.get(i).update();
  }  
  popStyle();

  /* ============== */
  /* 地面とかの描画 */
  /* ============== */
  pushStyle();
  // 地面の描画 
  stroke(0, 255, 0);
  for(int x = -1000; x <= 1000; x += 100)
    line(x, 0, -1000, x, 0, 1000); 
  for(int z = -1000; z <= 1000; z += 100)
    line(-1000, 0, z, 1000, 0, z);
  
  // 回転軸の描画
  stroke(255, 0, 0);
  line(0, -500, 0, 0, 500, 0);
  popStyle();
  
  time++;
}

class Agent {
  private final float TERRITORY_SIZE = 2000;
  private final float NOISE_SCALE    = 0.001f;
  private final float AGENT_SIZE     = 10; 
  
  private PVector center;  // 縄張りの中心座標
  private PVector offset;  // 中心からのオフセット
  private PVector pos;     // 座標
  
  private long time;      // 経過時間
  float xOffset, zOffset; // ノイズ生成用
  
  Agent(PVector center) {
    this.center = center;
    pos         = new PVector();
    offset      = new PVector();
    xOffset     = random(TERRITORY_SIZE);
    zOffset     = random(TERRITORY_SIZE);
  }
  
  PVector getPos() {
    return pos;
  }
  
  void update() {
    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 - AGENT_SIZE * 0.5, center.z + offset.z);
    translate(pos.x, pos.y, pos.z);
    
    box(AGENT_SIZE);
        
    popMatrix();
  }
}



【合体させる】

あとは、この2つのテクニックをてきとうに合体させるだけです。

下図(左)のように、とりあえずランダムに動き回るビルボードを山のように作って、 それにテクスチャを貼れば完成。意外と簡単ですね。

左: ビルボードの輪郭 / 右:テクスチャを貼った結果
ここで、glBlendFunc メソッドを使って加算合成モードにすると、ビルボード同士が重なった際に輝度が上がります。完成形のソースコードを以下に示します。

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

PGraphics   tex;
List<Agent> agentList;
long        time;

void setup() {
  size(800, 600, OPENGL);
 
  // テクスチャ生成
  tex = createTexture();
  
  agentList = new ArrayList<Agent>();
  for(int i = 0; i < 1000; ++i) {
    float x = random(-1000, 1000);
    float z = random(-1000, 1000);
    agentList.add(new Agent(new PVector(x, 0, z)));
  }
}

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.glDisable(GL.GL_DEPTH_TEST);
  gl.glEnable(GL.GL_BLEND);
  gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE);
  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 x = -1000; x <= 1000; x += 100)
    line(x, 0, -1000, x, 0, 1000); 
  for(int z = -1000; z <= 1000; z += 100)
    line(-1000, 0, z, 1000, 0, z);
  
  popStyle();
  
  time++;
}

PGraphics createTexture() {
  PGraphics tex = createGraphics(100, 100, P2D);
  float r = tex.width * 2 / 3;
  
  tex.beginDraw();
  tex.background(0);
  tex.noStroke();
  tex.fill(100, 150, 255);
  tex.ellipse(tex.width/2, tex.height/2, r, r);
  tex.filter(BLUR, 10.0);
  tex.endDraw();
  return tex;
}

class Agent {
  private final float TERRITORY_SIZE = 2000;
  private final float NOISE_SCALE    = 0.001f;
  private final float AGENT_SIZE     = random(50); 
  
  private PVector center;  // 縄張りの中心座標
  private PVector offset;  // 中心からのオフセット
  private PVector pos;     // 座標
  
  private long time;      // 経過時間
  float xOffset, zOffset; // ノイズ生成用
  
  Agent(PVector center) {
    this.center = center;
    pos         = new PVector();
    offset      = new PVector();
    xOffset     = random(TERRITORY_SIZE);
    zOffset     = random(TERRITORY_SIZE);
  }
  
  PVector getPos() {
    return pos;
  }
  
  void update() {
    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();
  }
}

1 件のコメント:

  1. 名前は聞いたことがったのですが、
    パーティクルってこんな感じで実装できるのかと
    初めて知りました!
    さっそく使って見ようと思います。

    返信削除

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