2011/11/05

Processing : P3D モードのヘンなクセ

現行バージョンの Processing には、ソフトウェアレンダリングで 3D を描画できる『P3D モード』が備わっています。

これは、3次元的な情報を視覚化する際にたいへん便利なのですが、実はちょっとした癖もあるため、うまく使うにはそれなりのコツが必要となってきます。

というわけで今日は、P3D モードで遭遇しがちな不具合と、その回避策を紹介したいと思います。



【地面のグリッド表示がおかしくなる現象】

以下のように、地面をグリッドで描画したい場合があったとしましょう。


素直にコードを書くとこんな感じになります(展開してご覧ください)。

void setup() {
  size(800, 600, P3D);
}

void draw() {
  // カメラを適当に設定
  camera(0, -100, 500, 0, 0, 0, 0, 1, 0);
  
  background(255);

  /* ====================== */
  /* 地面の描画(てきとう) */
  /* ====================== */
  stroke(100);
  noFill();  // ←塗りつぶさない
  
  final int step = 20;  
  for(int i = -width; i < width; i += step) {
    beginShape(QUAD_STRIP);
    for(int j = -width; j <= width; j += step) {
      vertex(i, 0, j);
      vertex(i + step, 0, j);
    }
    endShape();
  }
}

上記のプログラムは、いっけん正しく動きそうですが、いざ実行してみるとこんなふうにぶっ飛んだ結果になってしまいます。


なぜこんな事になったのでしょうか。

この現象は、描画すべき線分の端点がカメラ視点の裏側に回り込んでしまった場合に発生します。原因は、Processing 側が 3 次元座標を射影変換する際に、いくつかの例外処理を省略してしまっているためと考えられます。

これを回避するための比較的簡単な方法は、noFill() の代わりに fill(0x00FFFFFF) を使う事です。下記のコードならば、意図した結果が得られるはずです。

void setup() {
  size(800, 600, P3D);
}

void draw() {
  // カメラを適当に設定
  camera(0, -100, 500, 0, 0, 0, 0, 1, 0);
  
  background(255);

  /* ====================== */
  /* 地面の描画(てきとう) */
  /* ====================== */
  stroke(100);
  fill(0x00FFFFFF);  // ←変更
  
  final int step = 20;  
  for(int i = -width; i < width; i += step) {
    beginShape(QUAD_STRIP);
    for(int j = -width; j <= width; j += step) {
      vertex(i, 0, j);
      vertex(i + step, 0, j);
    }
    endShape();
  }
}

基本的に、『塗りつぶし無しの 3D 図形』と『塗りつぶしつきの 3D 図形』では異なるレンダリングパイプラインを通ります。ちなみに、陰影付けやオクルージョンの判定などを必要とする事から、一般的には後者の方が高度な処理となります。

上記のプログラムでは、fill(0x00FFFFFF) によって塗りつぶし色を「完全に透明な白」に指定する事によって、塗りつぶし無しと等価の見た目で、なおかつ問題となっていた端点の座標飛びに対処しています。



【半透明テクスチャのレンダリング順序】

Processing では、透過 png 画像をテクスチャとして使用する事ができます。

これを使えば、以下のように半透明の物体を表現する事ができそうですね。



半透明のテクスチャを貼った四角形を並べるコードは以下のように書けますが、これを実行すると何かがおかしくなります。

void setup() {
  size(800, 600, P3D);
  noStroke();
}

void draw() {
  background(0);
  camera(500, -500, 700, 0, 0, 0, 0, 1, 0);
  
  final int size  = 200;   // 板のサイズ

  final int depth = 1000;  // 板を並べる際の奥行き量
  final int step  =  100;  // 板と板との間隔
  
  colorMode(HSB, depth / step, 100, 100);
  for(int z = depth / 2; z >= -depth / 2; z -= step) {
    
    int colour = color((z + depth/2) / step, 100, 100);  // 色を計算
    PImage tex = createTranslucentTex(colour);           // 指定した色でテクスチャ作成

    // 板の描画
    beginShape(QUADS);
    texture(tex);
    vertex(-size, -size, z, 0,         0);
    vertex(-size,  size, z, 0,         tex.height);
    vertex( size,  size, z, tex.width, tex.height);
    vertex( size, -size, z, tex.width, 0);
    endShape(QUADS);
  }
  colorMode(RGB, 0xFF);

}

// 指定した色で半透明のテクスチャを作成する
// 引数: 色
PImage createTranslucentTex(int colour) {
  PImage tex = createImage(128, 128, ARGB);
  
  for(int y = 0; y < tex.height; y++) {
    int alphaValue = 0xFF * y / tex.height;  // アルファ値
    for(int x = 0; x < tex.width; x++) {
      int index = y * tex.width + x;
      tex.pixels[index] = alphaValue << 24 | 0x00FFFFFF & colour;
    }
  }
  return tex;
}


何がおかしいか分かりますでしょうか。

奥の方の板が、なぜか手前の板を覆い隠しているように見えます(ちなみに、テクスチャが不透明であれば、このような問題は発生しません)。

Processing では、通常『Z バッファリング』と呼ばれる方式を用いてレンダリング処理を省力化しています。それはよいのですが、この方式には、『半透明の物体が必ずしも正しく描画されない』という大きな問題点があります。

これを解決するための手段として、『画家のアルゴリズム(Z ソート法)』というものがあります。アルゴリズムとはいえさして大仰なものではなく、カメラ奥の物体から手前の物体にかけて重ね塗りをするように描画していくという非常に直感的な仕組みです。

というわけで、描画方式をデフォルトのZ バッファリングから画家のアルゴリズムに変更するには、hint() メソッドに ENABLE_DEPTH_SORT を与えればよいそうです。

画家のアルゴリズムを有効にしたコードを以下の通りです。

void setup() {
  size(800, 600, P3D);
  noStroke();
  hint(ENABLE_DEPTH_SORT); // ←追加
}

void draw() {
  background(0);
  camera(500, -500, 700, 0, 0, 0, 0, 1, 0);
  lights();
  final int size  = 200;   // 板のサイズ

  final int depth = 1000;  // 板を並べる際の奥行き量
  final int step  =  100;  // 板と板との間隔

  colorMode(HSB, depth / step, 100, 100);
  for(int z = depth / 2; z >= - depth / 2; z -= step) {
    
    int colour = color((z + depth/2) / step, 100, 100);  // 色を計算
    PImage tex = createTranslucentTex(colour);           // 指定した色でテクスチャ作成

    // 板の描画
    beginShape(QUADS);
    texture(tex);
    vertex(-size, -size, z, 0,         0);
    vertex(-size,  size, z, 0,         tex.height);
    vertex( size,  size, z, tex.width, tex.height);
    vertex( size, -size, z, tex.width, 0);
    endShape(QUADS);
  }
  colorMode(RGB, 0xFF);
}

// 指定した色で半透明のテクスチャを作成する
// 引数: 色
PImage createTranslucentTex(int colour) {
  PImage tex = createImage(128, 128, ARGB);
  
  for(int y = 0; y < tex.height; y++) {
    int alphaValue = 0xFF * y / tex.height;  // アルファ値
    for(int x = 0; x < tex.width; x++) {
      int index = y * tex.width + x;
      tex.pixels[index] = alphaValue << 24 | 0x00FFFFFF & colour;
    }
  }
  return tex;
}

これで、恐らく期待した結果が得られるようになります(『恐らく』と書いたのは、リファレンスに “the algorithm is not yet perfect.” とあるためです)。



【あとがき・おまけ】

実は、オーロラにも半透明のポリゴンが大量に用いられています。ですが、こちらは画家のアルゴリズムを用いていないため、カメラ視点の位置によってはおかしな事になります。

『画家のアルゴリズム』の存在自体は知っていましたが、それを有効にするための方法が分からなかったので、なるべく違和感を払拭するため、ポリゴンの描画順を少しいじったり、視点を移動できる範囲を強く制限したりと涙ぐましい工夫が凝らされています。

というわけで、あまり大した事ではありませんが、知っていると少し幸せになれるかも知れない Tips でした。めでたし、めでたし。

0 件のコメント:

コメントを投稿

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