2011/05/31

Processingで画面遷移効果(くだけちるエフェクト)

ゲームなどの画面遷移に使えそうな気がしたので、くだけちるエフェクトを簡単に実装してみた。

アプレット領域内でクリックすると、砕け散るエフェクトの後、次のシーンに遷移するサンプルである。

Applet対応のブラウザでご覧ください

破片の形状作成は Delaunay 分割を使いたいところだが、計算コストが少々大きいので今回は果てしなく原始的な方法にした。破片の頂点座標はランダムである。

ソースコードを晒すにはあまりにもお粗末なのだが、最近更新してなかったし何かしら書かなきゃヤバいだろうなーと思ったので一応公開しておく。



主な設計思想は、いつぞやの有限オートマトンとほぼ同じだ。

すなわち、様々な「状態」を抽象化したインタフェースを最初に作っておき、具体的な各状態クラスはそれを実装(implement)して作る。

インタフェースを実装したクラスのオブジェクトは、アップキャストする事でひとまとめに扱う事ができるため、わずらわしい状態管理が非常にラクになるのであった。

というわけで、今回の主要な(というか今回の肝である)遷移を司るクラスはこんな感じ。

// シーンを司るインタフェースクラス
// updateメソッドをオーバーライドして使う
// 戻り値は、次に遷移するシーンオブジェクト
// (遷移しない場合はthisポインタ)
interface Scene {
  Scene update();
  
  // 連鎖的にupdateメソッドを移譲するため、
  // 呼び出し先がnewを乱発しないための制御フラグを設定する
  void setEnable(boolean isEnable);  
}

// =======================================================
// 遷移用クラス
// =======================================================
class SceneFrag implements Scene {
  private PImage nextImage;  // 画面のイメージ
  private final int MAX_TIME = 100;
  private Fragments fragments;
  int time;
  Scene cur, next;
  
  SceneFrag(Scene cur, Scene next) {
    this.cur = cur;
    this.next = next;
    
    cur.setEnable(false);
    next.setEnable(false);
    
    nextImage = createImage(width, height, RGB);
    
    fragments = new Fragments(200);
    time = 0;
  }
  
  Scene update() { 
    noLights();
    noStroke();
    next.update();
    
    // ========================
    // 一度2次元に書き直す。
    // 3Dで、投影面よりオブジェクトが手前にある場合の副作用を軽減できる
    // ただし処理が重くなるので今回はコメントアウト
    // ========================
    /*
    loadPixels();
    for(int i = 0; i < pixels.length; ++i)
      nextImage.pixels[i] = pixels[i];
      
    updatePixels();
    noLights();
    
    background(0);
    image(nextImage, 0, 0);
    */
    
    fragments.update();
    
    fill(255, 255 * (MAX_TIME - time*2) / MAX_TIME);
    rect(0, 0, width, height);
    
    if(time++ < MAX_TIME) return this;
    
    next.setEnable(true);
    return next;
  }
  
  void setEnable(boolean isEnable) {
    // まぁ何もしない
  }
}

// 破片の集まりクラス
class Fragments {
  // テクスチャ
  PImage tex;
  Fragment[] f;
  
  Fragments(int num) {
    tex = createImage(width, height, RGB);
    noStroke();
    fill(255, 50);
    rect(0, 0, width, height);
    
    loadPixels();
    for(int i = 0; i < pixels.length; i++) {
      tex.pixels[i] = pixels[i];
    }
    f = new Fragment[num];
    PVector[] p;
    
    for(int i = 0; i < num; i++) {
      p = new PVector[3];
      
      // ここは感覚的に破片の座標を設定
      p[0] = new PVector(random(width), random(height));
      p[1] = new PVector(p[0].x + random(150)-75, p[0].y + random(150)-75);
      p[2] = new PVector(p[1].x + random(150)-75, p[1].y + random(150)-75);
      f[i] = new Fragment(p);
    }
  }
  
  void update() {
    stroke(0, 10);
    for(int i = 0; i < f.length; ++i) {
      f[i].update();  // 更新
      
      // 破片を描画
      textureMode(IMAGE);
      beginShape(TRIANGLES);
      texture(tex);
      for(int j = 0; j < f[i].vertices.length; ++j) {
        vertex(f[i].vertices[j].x, f[i].vertices[j].y, 
               f[i].texCoords[j].x, f[i].texCoords[j].y);
      }
      endShape();
    }
  }
}

// いっこあたりの破片を管理する
class Fragment {
  PVector[] texCoords;  // テクスチャ座標
  PVector[] vertices;   // 頂点座標
  PVector center;       // 重心
  
  float dx, dy;
  float ddy;
  float angle;  // 角速度
  
  
  Fragment(PVector[] vertices) {
    // 頂点座標とテクスチャ座標をセット
    this.vertices = vertices;
    
    this.texCoords = new PVector[this.vertices.length];
    for(int i = 0; i < this.vertices.length; i++) {
      this.texCoords[i] = new PVector(this.vertices[i].x, 
        this.vertices[i].y, this.vertices[i].z);
    }
    
    // (初)速度・加速度を設定
    dx = random(2)-1;
    dy  = -random(5);
    ddy = 0.2;
    angle = radians(random(8)-4);
    this.center = new PVector();
  }
  void update() {
    this.center.x = this.center.y = 0;
    
    for(int i = 0; i < vertices.length; ++i) {
      // 各頂点を足して…
      this.center.add(this.vertices[i]);
    }
    // 平均する(重心座標が求まる)
    this.center.div(vertices.length);
        
    for(int i = 0; i < vertices.length; ++i) {
      // 重心を原点に合わせて
      float tmpX = this.vertices[i].x - center.x;
      float tmpY = this.vertices[i].y - center.y;
      
      // 回転を施し、元の座標に戻す
      this.vertices[i].x = cos(angle)*tmpX - sin(angle)*tmpY + center.x;
      this.vertices[i].y = sin(angle)*tmpX + cos(angle)*tmpY + center.y;
      
      // 並進移動      
      this.vertices[i].x += dx;
      this.vertices[i].y += dy;
    }    
    
    // 速度更新
    dy += ddy;
  } 
}

からくりはというと、状態遷移前のスクリーンショットをそのままテクスチャにして、おびただしい3角ポリゴン(Fragmentオブジェクト)に貼りつけているだけ。

Fragmentは、破片の幾何学的な頂点座標と、テクスチャの (u, v) 座標を保持するクラスであり、それらを集成したものがFragmentsである。

リソース節減を考慮し、1枚のテクスチャを各破片に割り当てるために、テクスチャ用画像はFragmentsが管理している。

Fragmentsupdateメソッドは、砕け散った前状態と次状態とを巧妙にブレンドして表示するためのものだ。

updateが呼ばれるたびにタイマーtimeがカウントアップされ、MAX_TIMEに達すると次状態へと遷移する。



完全版はこちら。
Scene scene;

void setup() {
  size(400, 300, P3D);
  scene = new Scene1();
}
void draw() {
  background(255, 0, 0, 10);
  scene = scene.update();
}

// シーンを司るインタフェースクラス
// updateメソッドをオーバーライドして使う
// 戻り値は、次に遷移するシーンオブジェクト
// (遷移しない場合はthisポインタ)
interface Scene {
  Scene update();
  
  // 連鎖的にupdateメソッドを移譲するため、
  // 呼び出し先がnewを乱発しないための制御フラグを設定する
  void setEnable(boolean isEnable);  
}


// =======================================================
// デモ用の状態クラス
// =======================================================
class Scene1 implements Scene {
  float h, p, b;     // ヘディング・ピッチ・バンク回転量
  float dh, dp, db;  // 角速度
  int objColor, bgColor;
  boolean isEnable;
  
  PVector[] stars;
  float[] dx;

  Scene1() {
    // 色の付け方はてきとうです。
    objColor = color(random(200, 255), 
      random(200, 255), 
      random(100, 255));
    bgColor = color( red(objColor)/10, 
      green(objColor)/10, 
      blue(objColor)/10);
    
    stars = new PVector[100];
    dx = new float[stars.length];
    for(int i = 0; i < stars.length; i++) {
      stars[i] = new PVector(random(width), random(height));
      dx[i] = random(5) + 1;
    }
      
    // 回転量・並進量を設定
    h = random(0, 360);
    p = random(-90, 90);
    b = random(0, 360);
    dh = random(-1, 1);
    db = random(-1, 1);
    dp = random(-1, 1);
    
    isEnable = true;
  }
  
  Scene update() {
    camera();
    lights();
    
    background(bgColor);
    
    strokeWeight(1);
    stroke(255);
    for(int i = 0; i < stars.length; i++) {
      point(stars[i].x, stars[i].y);
      if((stars[i].x += dx[i]) > width) {
        stars[i].x = 0;
        dx[i] = random(5) + 1;
      }
    }
    noStroke();
    
    strokeWeight(3);
    stroke(objColor);
    noFill();
    
    // デモシーン:立方体を適当に…
    pushMatrix();
    translate(width/2, height/2, -width);
    rotateY(radians(h));
    rotateX(radians(p));
    rotateZ(radians(b));
    box(250);
    popMatrix();
    
    h += dh;
    p += dp;
    b += db;
    
    if (mousePressed && isEnable) {
      return new SceneFrag(this, new Scene1());
    } else {
      return this;
    }
  }
  
  void setEnable(boolean isEnable) {
    this.isEnable = isEnable;
  }
}


// =======================================================
// 遷移用クラス(ここからが本編)
// =======================================================
class SceneFrag implements Scene {
  private PImage nextImage;  // 画面のイメージ
  private final int MAX_TIME = 100;
  private Fragments fragments;
  int time;
  Scene cur, next;
  
  SceneFrag(Scene cur, Scene next) {
    this.cur = cur;
    this.next = next;
    
    cur.setEnable(false);
    next.setEnable(false);
    
    nextImage = createImage(width, height, RGB);
    
    fragments = new Fragments(200);
    time = 0;
  }
  
  Scene update() { 
    noLights();
    noStroke();
    next.update();
    
    // ========================
    // 一度2次元に書き直す。
    // 3Dで、投影面よりオブジェクトが手前にある場合の副作用を軽減できる
    // ただし処理が重くなるので今回はコメントアウト
    // ========================
    /*
    loadPixels();
    for(int i = 0; i < pixels.length; ++i)
      nextImage.pixels[i] = pixels[i];
      
    updatePixels();
    noLights();
    
    background(0);
    image(nextImage, 0, 0);
    */
    
    fragments.update();
    
    fill(255, 255 * (MAX_TIME - time*2) / MAX_TIME);
    rect(0, 0, width, height);
    
    if(time++ < MAX_TIME) return this;
    
    next.setEnable(true);
    return next;
  }
  
  void setEnable(boolean isEnable) {
    // まぁ何もしない
  }
}

// 破片の集まりクラス
class Fragments {
  // テクスチャ
  PImage tex;
  Fragment[] f;
  
  Fragments(int num) {
    tex = createImage(width, height, RGB);
    noStroke();
    fill(255, 50);
    rect(0, 0, width, height);
    
    loadPixels();
    for(int i = 0; i < pixels.length; i++) {
      tex.pixels[i] = pixels[i];
    }
    f = new Fragment[num];
    PVector[] p;
    
    for(int i = 0; i < num; i++) {
      p = new PVector[3];
      
      // ここは感覚的に破片の座標を設定
      p[0] = new PVector(random(width), random(height));
      p[1] = new PVector(p[0].x + random(150)-75, p[0].y + random(150)-75);
      p[2] = new PVector(p[1].x + random(150)-75, p[1].y + random(150)-75);
      f[i] = new Fragment(p);
    }
  }
  
  void update() {
    stroke(0, 10);
    for(int i = 0; i < f.length; ++i) {
      f[i].update();  // 更新
      
      // 破片を描画
      textureMode(IMAGE);
      beginShape(TRIANGLES);
      texture(tex);
      for(int j = 0; j < f[i].vertices.length; ++j) {
        vertex(f[i].vertices[j].x, f[i].vertices[j].y, 
               f[i].texCoords[j].x, f[i].texCoords[j].y);
      }
      endShape();
    }
  }
}

// いっこあたりの破片を管理する
class Fragment {
  PVector[] texCoords;  // テクスチャ座標
  PVector[] vertices;   // 頂点座標
  PVector center;       // 重心
  
  float dx, dy;
  float ddy;
  float angle;  // 角速度
  
  
  Fragment(PVector[] vertices) {
    // 頂点座標とテクスチャ座標をセット
    this.vertices = vertices;
    
    this.texCoords = new PVector[this.vertices.length];
    for(int i = 0; i < this.vertices.length; i++) {
      this.texCoords[i] = new PVector(this.vertices[i].x, 
        this.vertices[i].y, this.vertices[i].z);
    }
    
    // (初)速度・加速度を設定
    dx = random(2)-1;
    dy  = -random(5);
    ddy = 0.2;
    angle = radians(random(8)-4);
    this.center = new PVector();
  }
  void update() {
    this.center.x = this.center.y = 0;
    
    for(int i = 0; i < vertices.length; ++i) {
      // 各頂点を足して…
      this.center.add(this.vertices[i]);
    }
    // 平均する(重心座標が求まる)
    this.center.div(vertices.length);
        
    for(int i = 0; i < vertices.length; ++i) {
      // 重心を原点に合わせて
      float tmpX = this.vertices[i].x - center.x;
      float tmpY = this.vertices[i].y - center.y;
      
      // 回転を施し、元の座標に戻す
      this.vertices[i].x = cos(angle)*tmpX - sin(angle)*tmpY + center.x;
      this.vertices[i].y = sin(angle)*tmpX + cos(angle)*tmpY + center.y;
      
      // 並進移動      
      this.vertices[i].x += dx;
      this.vertices[i].y += dy;
    }    
    
    // 速度更新
    dy += ddy;
  } 
}



【6月6日追記】

ちょっと改良

0 件のコメント:

コメントを投稿

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