2011/12/03

Processingでかんたん3Dアニメーション

P5 Advent Calendar 2011 3日目を担当しました。

本日のテーマは【かんたん! 3Dアニメーション】です。みんな大好き Processing で、こんなものを作ってみました。

段ボール製ロボット“ダンボー”が、ただただ歩き続けるだけのシンプルな作品です。



【どうやって作ったの?】

この作品は、たくさんの直方体パーツを時間の経過とともに少しずつ動かして『歩いているように』見せています。

とはいえ、単純に一つひとつのパーツを無秩序に動かせばよいというわけではありません。 パーツ間の幾何構造をきちんと連携させるためにはそれなりの工夫が必要です。

そこでこの記事では、 3 次元アニメーションをそれっぽく見せるためのささやかなテクニックをご紹介していこうかと思います。

記事の前半で技術解説を適当にやって、後半で全ソースコードリストを公開します(ソースだけ欲しい方は記事の最後まで一気にスクロールして下さい)。

技術解説は、3 次元を扱う上で基礎中の基礎となるお話から始まり、座標変換の話題からデータ構造の考察へと話が繋がります。高校レベルの簡単なベクトルや行列の概念を知っている方を想定していますが、本文中には数式が登場しませんので、理系以外の方々にもご理解いただけるかと思います。モーションアニメに限らず、考えを広げるヒントになるかも知れません。

また、OpenProcessing の当該ページからはプロジェクトを一括してダウンロードして頂けます。



【3次元オブジェクトを動かす、ということ】

突然ですが、何があっても形状が変化しないものを、物理用語で『剛体』と言います

作品中のダンボーはすべて剛体のパーツでできています。

さてさて。剛体の動かし方は以下の 2 種類に分けられます。
  • 並進移動 (物体のすべての頂点が、同じ方向に同じ量だけ移動すること)
  • 回転移動 (物体のすべての頂点が、同じ軸のまわりを同じ角度だけ回転すること)
※ 回転軸は必ずしも x, y, z いずれかに一致している必要はなく、任意の(単位)ベクトルを回転軸として設定する事ができます(下図参照)。
視覚化するとこんな感じです。

並進移動
回転移動
繰り返しになりますが、どんなに複雑な移動でも上記の 2 つの移動を合成する事で実現できます。

本質はたったこれだけなのですが、座標軸をどう設定するかによって移動に関する制御のしやすさが全然違ってきます。

次節では、そこらへんの処理を都合よく行うために 2 つの座標系を導入する事にしましょう。



【世界座標とローカル座標】

ざっくり言ってしまえば、『ローカル座標』とは移動する物体に常にくっついていく座標系であり、『世界座標』とは決して変化しない座標系の事です。

解釈としては、“(僕から見て)前後左右”のように極めて局所的で自己中心的な位置表現が『ローカル座標系』、“地球上の緯度経度”のような大局的で一意性のある位置表現が『世界座標系』といったところでしょうか。

したがって、頭や胴体などそれぞれのパーツが独自のローカル座標を持つのに対して、世界座標は一つの 3 次元空間に一つしかありません(下図は頭部パーツのローカル座標と世界座標の対比。面倒なので他のパーツは省略)。

 ↓
t 秒後)

上図からも判る通り、パーツがどれだけ並進や回転移動を繰り返しても、そのローカル座標系における x 軸は常に剛体の右側を向き続けますし、z 軸は剛体の後方向を向き続けます。したがって、ローカル座標系を使えばパーツ同士の相対的な位置関係を容易に定義する事ができるのです(たとえば、胴体が移動しても、右腕は常に胴体から見て“右側”に在り続ける事ができます)。

一方、世界座標系はパーツがどれだけ移動を繰り返しても何の影響も受けませんから、あらゆるパーツの絶対的な位置を定義する唯一の基準として使用する事ができます。

なお、ローカル座標と世界座標は相互変換する事ができます。その理論的な背景は 9 月 27 の日記で簡単に紹介していますので、興味のある方はご覧下さい。

先に述べた剛体の 2 つの移動のうち、並進に関しては translate() メソッドが用意されています。座標系に注意しさえすればさほど難しい事ではありませんので、特に説明は不要でしょう。

ただし、回転移動に関してはさらなる補足が必要かも知れません。次節でご紹介します。



【回転 - ヘディング・ピッチ・バンク】

3 次元剛体のあらゆる回転移動は、ローカル座標系における 3 軸(xyz 軸)まわりの回転に分解することができます。逆に言えば、3 軸まわりの回転の合成で、あらゆる回転移動を表す事ができるという事です。これを、『オイラー角表現』と呼びます。

たとえば、先ほど例示した回転移動 ――
―― は、以下のように分解できます(下図中の水色の破線は回転軸)。

Processing では、rotateX()rotateY()rotateZ() というメソッドが基本軸まわりの回転移動をサポートしており、それらの合成で大概の回転移動を実現する事が可能です。

注意点としては、たとえ各軸まわりの回転量が同じでも、合成する順序が変わると得られる結果は異なったものになってしまいます。数学的に解釈するならば、行列の積演算が交換法則を持たない事と一緒です。したがって、剛体の回転移動を一意に表すには各軸まわりの回転量だけでなく、それらを合成する際の順序も定義せねばなりません

各軸まわりの回転を合成する順序には、『ロールピッチヨー』や『ヘディングピッチバンク』など、代表的なものが幾つかあります。この作品では、回転の合成順序をヘディング → ピッチ → バンク(下図参照)で統一しています。

初期状態
ヘディング( y 軸まわり)
ピッチ( x 軸まわり)
バンク( z 軸まわり)
合成完了

先に述べた通りにローカル座標系が定義された場合、ヘディングは y 軸まわりの回転、ピッチは x 軸まわりの回転、そしてバンクは z 軸まわりの回転になります。



【ダンボーのためのデータ構造】

ここまでは、一つのパーツのみに着目して解説を行なってきました。

しかし、ダンボーは複数のパーツの組み合わせでできています。これらの部品はひとまとめにして扱いたいものですが、私たちがよく知っている配列ではダンボーの身体構造をうまく表現できません。
※ そもそも配列は、同類のオブジェクト群に番号(順序)をつけて管理するためのデータ構造です。オブジェクトごとに何らかの階層関係がある場合は、必ずしも適切なデータ構造とは言えません。
この作品では 2 種類のデータ構造を用いています。

1 つは各パーツの物理的な親子関係を表す『木構造』で、もう 1 つは『連想配列』です。

木構造における各ノードにはパーツの幾何形状や親子間の相対的な位置関係、および前述の角変位が格納されており、親ノードの位置が変わると子ノードもその影響を受けます(シーングラフに近い構造です)。

胴体をルートとした木構造の概念図: なんかこわくなった

このような構造を導入する利点は、Processing に備わっている行列スタックを巧妙に用いて再帰的にレンダリングを行える事や、一つひとつのパーツの位置を(それらの親ノードにおける)ローカル座標空間から見た位置で定義でき、扱いが容易になる事などが挙げられます。
※ 繰り返しになりますが、こうする事で『右腕は胴体から見て常に右側、左腕は常に左側』という相対記述が可能になります。
残念なことに Processing は木構造をサポートしていないため、自力で実装する必要があります。このような場合の常套的なテクニックは、木構造のノードを、子ノードの参照リストを持つ自己参照クラスとして定義することです(Node クラスのソースコード参照)。これにより、ルートノードから全ての子ノードに対する階層的な参照関係が生じますから、木構造の再現は容易であると言えるでしょう。

一方、連想配列は各パーツを階層関係から切り離して(つまり単体で)扱いやすくするために用意しました。たとえば、歩行時の腕の振りをプログラムしたい場合は、腕のみに注目したいですね。そこで、腕パーツをすぐさま参照できるように、パーツの(一意な)名称とパーツのデータを紐付けて連想配列に格納しておくのです。

連想配列の概念図: 識別用の名称から、ただちに対応するパーツを参照できる
連想配列は Processing で用意されている HashMap を利用すれば簡単です。

なお、描画すべきパーツは総て木構造に格納する必要があるのに対して、連想配列には必ずしもすべてのパーツを格納する必要はありません。また、木構造の“ノード”と連想配列の“要素”は、どちらも同じオブジェクトを参照しています。ですので、連想配列経由でアクセスしたオブジェクトに何らかの変更を加えたからといって、対応する木構造のノードと整合しなくなる心配はありません。



【最後のしあげ:足を地に着けるために】

胴体パーツを木構造のルートに設定してしまったため、他のパーツは胴体を中心に動く事になります。このままアニメーションさせるとまるで宙ぶらりんですので、きちんとダンボーの足を地に着けてやらねばなりません。

この作品では、レンダリング直前にダンボーの頂点を世界座標に変換した後、“最も下にある頂点”の値を調べ、それが 0 になるようにモデル全体を鉛直方向に並進させています。

地面にめり込んだダンボー
 ↓
適切な高さに並進移動
完成
 各パーツのローカル座標から世界座標への変換は 9 月 27 の日記で述べた通りです。ただし、各頂点を鉛直軸(y軸)の要素のみで比較するために Comparator をカスタマイズしました(Node クラスのソースコード 99 行目 - 104 行目参照)。



【技術解説あとがき】

Processing Advent Calendar 2011 3日目はいかがでしたでしょうか。

実は、参加表明をしてからというもの、題材選びには大いに悩む日々が続きました。『銀河鉄道を走らせよう』とか『クリスマスイルミネーションを作ろう』とかのアイディアはことごとくボツになりました。

〆切直前にがんばって見つけ出した題材ですので、楽しんで頂けたら非常に嬉しいです。

さて、プログラム自体は一晩で書き上げたものの、ノウハウを文章化する事が殊のほか難しく、やむなく端折ってしまったトピックがいくつかあります。

特に回転に関してはいくら説明しても足りないくらいです。簡単のために本文中では言及を避けましたが、ある角変位を表現する式には一意性がありません。また、オイラー角表現は原理上、ジンバルロックという弱点を抱えています。

この作品を改造する場合は、念のためこうした技術上の問題がある点にご留意ください
※ ジンバルロック問題の解消には四元数という虚数のバケモノみたいなやつを使います。この話題はどんなに駆け足で説明しても長くなってしまうのでまたの機会に。
それでは皆様、Let's Enjoy Processing! ∩( ・ω・)∩



【おまちかねソースコード】

…といきたいところですが、今回の作品はソースコードをコピペしただけでは動きません

ダンボーのモデルにマッピングするテクスチャが必要です。以下のテクスチャ画像に、それぞれ【d_tab.jpg】 【d_face.jpg】 【d_coin.jpg】 【d_normal.jpg】 と名前をつけて、data フォルダに保存して下さい。

d_tab.jpg
d_face.jpg
d_coin.jpg
d_normal.jpg
これで準備ができました。あとは以下のプログラムをすべて Processing IDE にコピー & ペーストすれば、ダンボーが動き出します。たぶん。

【メイン】
final int BACKGROUND_COLOR = 0xFFFFEEBB; // 背景色
Danbo danbo;                             // ダンボー
float time;                              // 経過時間の制御用変数


// ========================================
// 初期化
// ========================================
void setup() {
  size(400, 300, P3D);  
  noStroke();  
  
  danbo = new Danbo(); 
}


// ========================================
// 描画
// ========================================
void draw() {
  // ----------------------------------------
  // ライトが常に正面から当たるように設定
  // ----------------------------------------
  camera(); lights();


  // ----------------------------------------
  // カウンタの更新
  // ----------------------------------------
  float angle = radians(time += .05);
  if(angle >= TWO_PI) angle = time = 0;
  
  
  // ----------------------------------------
  // カメラの設置
  // ----------------------------------------
  camera(700 * sin(angle), -600, -700 * cos(angle), 
         0,                -300,  0, 
         0,                 1,    0);
  
  background(BACKGROUND_COLOR);
  
  
  // ----------------------------------------
  // 歩行モーションの更新
  // ----------------------------------------
  danbo.update();
  
  
  // ----------------------------------------
  // リフレクション(おまけ)
  // ----------------------------------------
  pushMatrix();
  applyMatrix(1.0,  0.0, 0.0, 0.0,
              0.0, -1.0, 0.0, 0.0,
              0.0,  0.0, 1.0, 0.0,
              0.0,  0.0, 0.0, 1.0);
  danbo.render();


  camera();
  pushStyle();
  fill(0xBB << 24 | 0xFFFFFF & BACKGROUND_COLOR);
  rect(0, 0, width, height);
  popStyle();
  popMatrix();

  
  // ----------------------------------------
  // ダンボーの描画
  // ----------------------------------------
  fill(0xFFFFFFFF);
  danbo.render();
}

Node クラス】
class Node {
  public PVector pos;   // 親からの相対位置
  public PVector org;   // 回転の原点座標
  public PVector size;  // 幅、高さ、奥行き
  public PVector rot;   // 各軸まわりの回転角
  
  private List<Node>          child;        // 子ノードの参照
  private Map<String, PImage> texMap;       // テクスチャ
  private PVector[]           localCoords;  // 頂点のローカル座標
  private PVector[]           globalCoords; // 頂点のグローバル座標
  
  private final int NUM_VERTICES   = 8;  // 頂点数
  private final int NUM_PARTITIONS = 5;  // 分割数
  
  
  // ========================================
  // コンストラクタ
  // ========================================
  public Node() {
    pos          = new PVector(0, 0, 0);
    org          = new PVector(0, 0, 0);
    size         = new PVector(1, 1, 1);
    rot          = new PVector(0, 0, 0);
    
    child        = new ArrayList<Node>();
    texMap       = new HashMap<String, PImage>();
    
    localCoords  = new PVector[NUM_VERTICES];
    globalCoords = new PVector[NUM_VERTICES];
    
    for(int i = 0; i < NUM_VERTICES; ++i) {
      localCoords [i] = new PVector();
      globalCoords[i] = new PVector();
    }
  }
  
  
  // ========================================
  // 子要素追加
  // ========================================
  public void addChild(Node child) {
    this.child.add(child);
  }
  
  
  // ========================================
  // テクスチャ設定
  // ========================================
  public void applyTexture(String name, PImage tex) {
    this.texMap.put(name, tex);
  }
  
  
  // ========================================
  // 最も地面に近い頂点の y 座標値を得る
  // ========================================
  public float getGlobalMaxYCoord() {
    return getGlobalMaxYCoord(Float.MIN_VALUE);
  }
  private float getGlobalMaxYCoord(float maxY) {
    float ret = maxY;
    
    // 箱のローカル頂点をセット
    localCoords[0].set(-.5*size.x, -.5*size.y, -.5*size.z);
    localCoords[1].set(-.5*size.x, -.5*size.y,  .5*size.z);
    localCoords[2].set( .5*size.x, -.5*size.y,  .5*size.z);
    localCoords[3].set( .5*size.x, -.5*size.y, -.5*size.z);
    localCoords[4].set(-.5*size.x,  .5*size.y, -.5*size.z);
    localCoords[5].set(-.5*size.x,  .5*size.y,  .5*size.z);
    localCoords[6].set( .5*size.x,  .5*size.y,  .5*size.z);
    localCoords[7].set( .5*size.x,  .5*size.y, -.5*size.z);    
    
    // ----------------------------------------
    // ワールド行列の計算
    // ----------------------------------------
    pushMatrix();
    
    // 並進
    translate(pos.x, pos.y, pos.z);
    
    // 回転
    translate(org.x, org.y, org.z);
    rotateY(rot.y); rotateX(rot.x); rotateZ(rot.z);
    translate(-org.x, -org.y, -org.z);
    
    // ワールド行列を計算
    PMatrix3D worldMat = (PMatrix3D)getMatrix();
    worldMat.preApply(iViewMat);
    
    // ----------------------------------------
    // ローカル座標から世界座標に変換
    // ----------------------------------------
    for(int i = 0; i < NUM_VERTICES; i++)
      worldMat.mult(localCoords[i], globalCoords[i]);
    
    // ----------------------------------------
    // y座標値で世界座標配列をソート(降順)
    // ----------------------------------------
    Arrays.sort(globalCoords, new java.util.Comparator() {
      public int compare(Object p1, Object p2) {
        return ((PVector)p1).y == ((PVector)p2).y ?  0 :
               ((PVector)p1).y >  ((PVector)p2).y ? -1 : 1;
      }
    });
    // 配列の先頭には最もy座標の大きなベクトルが格納されている
    float tmp = globalCoords[0].y;
    
    if(ret < tmp) ret = tmp;
    
    // ----------------------------------------
    // 再帰的に木をトラバース
    // ----------------------------------------
    for(Node n : child) {
      tmp = n.getGlobalMaxYCoord(ret);
      if (ret < tmp) ret = tmp;
    }
    
    popMatrix();
    return ret;
  }
  
  
  // ========================================
  // ノードのレンダリング
  // ========================================
  void render() {
    pushMatrix();
    
    // 並進
    translate(pos.x, pos.y, pos.z);
    
    // 回転
    translate(org.x, org.y, org.z);
    rotateY(rot.y); rotateX(rot.x); rotateZ(rot.z);
    translate(-org.x, -org.y, -org.z);
    
    float dx = size.x / NUM_PARTITIONS;
    float dz = size.z / NUM_PARTITIONS;
    
    
    // ----------------------------------------
    // 前面の描画
    // ----------------------------------------
    PImage tex = texMap.get("FRONT");
    if(tex == null) tex = texMap.get("DEFAULT");
    
    beginShape(QUAD_STRIP);
    if(tex != null) {
      float texdx = (float)(tex.width-1) / NUM_PARTITIONS;
      texture(tex);
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(.5 * size.x - i * dx, -.5 * size.y, -.5 * size.z, texdx * i, 0);
        vertex(.5 * size.x - i * dx,  .5 * size.y, -.5 * size.z, texdx * i, tex.height-1);
      }
    } else {
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(.5 * size.x - i * dx, -.5 * size.y, -.5 * size.z);
        vertex(.5 * size.x - i * dx,  .5 * size.y, -.5 * size.z);
      }
    }
    endShape();
    
    
    // ----------------------------------------
    // 背面の描画
    // ----------------------------------------
    tex= texMap.get("BACK");
    if(tex == null) tex = texMap.get("DEFAULT");
    
    beginShape(QUAD_STRIP);
    if(tex != null) {
      float texdx = (float)(tex.width-1) / NUM_PARTITIONS;
      texture(tex);
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(-.5 * size.x + i * dx, -.5 * size.y, .5 * size.z, texdx * i, 0);
        vertex(-.5 * size.x + i * dx,  .5 * size.y, .5 * size.z, texdx * i, tex.height-1);
      }
    } else {
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(-.5 * size.x + i * dx, -.5 * size.y, .5 * size.z);
        vertex(-.5 * size.x + i * dx,  .5 * size.y, .5 * size.z);
      }
    }
    endShape();
    
    
    // ----------------------------------------
    // 天面の描画
    // ----------------------------------------
    tex= texMap.get("TOP");
    if(tex == null) tex = texMap.get("DEFAULT");
    
    beginShape(QUAD_STRIP);
    if(tex != null) {
      float texdx = (float)(tex.width-1) / NUM_PARTITIONS;
      texture(tex);
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(.5 * size.x - i * dx, -.5 * size.y,  .5 * size.z, texdx * i, 0);
        vertex(.5 * size.x - i * dx, -.5 * size.y, -.5 * size.z, texdx * i, tex.height-1);
      }
    } else {
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(.5 * size.x - i * dx, -.5 * size.y,  .5 * size.z);
        vertex(.5 * size.x - i * dx,  .5 * size.y, -.5 * size.z);
      }
    }
    endShape();
    
    
    // ----------------------------------------
    // 底面の描画
    // ----------------------------------------
    tex= texMap.get("BOTTOM");
    if(tex == null) tex = texMap.get("DEFAULT");
    
    beginShape(QUAD_STRIP);
    if(tex != null) {
      float texdx = (float)(tex.width-1) / NUM_PARTITIONS;
      texture(tex);
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(.5 * size.x - i * dx, .5 * size.y,  .5 * size.z, texdx * i, 0);
        vertex(.5 * size.x - i * dx, .5 * size.y, -.5 * size.z, texdx * i, tex.height-1);
      }
    } else {
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(.5 * size.x - i * dx, .5 * size.y,  .5 * size.z);
        vertex(.5 * size.x - i * dx, .5 * size.y, -.5 * size.z);
      }
    }
    endShape();
    
    
    // ----------------------------------------
    // 左面の描画
    // ----------------------------------------
    tex= texMap.get("LEFT");
    if(tex == null) tex = texMap.get("DEFAULT");
    
    beginShape(QUAD_STRIP);
    if(tex != null) {
      float texdx = (float)(tex.width-1) / NUM_PARTITIONS;
      texture(tex);
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(-.5 * size.x, -.5 * size.y, -.5 * size.z + i * dz, texdx * i, 0);
        vertex(-.5 * size.x,  .5 * size.y, -.5 * size.z + i * dz, texdx * i, tex.height-1);
      }
    } else {
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(-.5 * size.x, -.5 * size.y, -.5 * size.z + i * dz);
        vertex(-.5 * size.x,  .5 * size.y, -.5 * size.z + i * dz);
      }
    }
    endShape();
    
    
    // ----------------------------------------
    // 右面の描画
    // ----------------------------------------
    tex= texMap.get("RIGHT");
    if(tex == null) tex = texMap.get("DEFAULT");
    
    beginShape(QUAD_STRIP);
    if(tex != null) {
      float texdx = (float)(tex.width-1) / NUM_PARTITIONS;
      texture(tex);
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(.5 * size.x, -.5 * size.y, .5 * size.z - i * dz, texdx * i, 0);
        vertex(.5 * size.x,  .5 * size.y, .5 * size.z - i * dz, texdx * i, tex.height-1);
      }
    } else {
      for(int i = 0; i <= NUM_PARTITIONS; ++i) {
        vertex(.5 * size.x, -.5 * size.y, .5 * size.z - i * dz);
        vertex(.5 * size.x,  .5 * size.y, .5 * size.z - i * dz);
      }
    }
    endShape();
    
    
    // ----------------------------------------
    // 再帰的に木をトラバース
    // ----------------------------------------
    for(Node n : child) {
      n.render();
    }
    popMatrix();
  }
}

Danbo クラス】
Map<String, PImage> imgMap;    // 同じ画像を何度もロードしないようにハッシュで管理
PMatrix3D           iViewMat;  // カメラ逆行列


class Danbo {
  private Map<String, Node> bodyMap;   // ボディパーツ用のハッシュ
  private Node              bodyTree;  // ボディパーツ用の木構造(ルートノード)
  private int               time;      // 経過時間の管理用変数
  
  
  // ========================================
  // コンストラクタ
  // ========================================
  public Danbo() {
    createBody();
    time = 0;
  }
  
  
  // ========================================
  // 歩行モーションの更新
  // ----------------------------------------
  // パーツごとに回転量を再設定しているだけだよ
  // ========================================
  public void update() {
    float angle = radians(++time * 4);
    if(angle >= TWO_PI) angle = time = 0;
    
    float sinAngle = sin(angle);
    float cosAngle = cos(angle);

    // ----------------------------------------
    // 胴の振り
    // ----------------------------------------
    final float BODY_AMPLITUDE = radians(1);  // 振り幅の大きさ(胴を振る角度)

    Node body = bodyMap.get("BODY");
    body.rot.set(0, 0, BODY_AMPLITUDE * cosAngle);
    
    // ----------------------------------------
    // 頭の振り
    // ----------------------------------------
    final float HEAD_AMPLITUDE_X = radians(5);  // 振り幅の大きさ(頭を前後に振る角度)
    final float HEAD_AMPLITUDE_Y = radians(2);  //    〃   ( 〃 左右  〃  )

    Node head = bodyMap.get("HEAD");
    head.rot.set(HEAD_AMPLITUDE_X * abs(sinAngle), HEAD_AMPLITUDE_Y * sinAngle, 0);

    // ----------------------------------------
    // 腕の振り
    // ---------------------------------------
    final float ARM_AMPLITUDE = radians(40);  // 振り幅の大きさ(腕を振る角度)
    
    Node leftArm = bodyMap.get("LEFTARM");
    leftArm.rot.set(ARM_AMPLITUDE * sinAngle, 0, 0);
    
    Node rightArm = bodyMap.get("RIGHTARM");
    rightArm.rot.set(-ARM_AMPLITUDE * sinAngle, 0, 0);
  
    // ----------------------------------------
    // 脚の動き
    // ----------------------------------------    
    final float LEG_AMPLITUDE = radians(30);  // 振り幅の大きさ(脚を振る角度)
    
    Node leftLeg = bodyMap.get("LEFTLEG");
    leftLeg.rot.set(-LEG_AMPLITUDE * sinAngle, 0, 0);
    
    Node rightLeg = bodyMap.get("RIGHTLEG");
    rightLeg.rot.set(LEG_AMPLITUDE * sinAngle, 0, 0);
  }
  
  
  // ========================================
  // ダンボーのレンダリング
  // ========================================
  public void render() {
    // ----------------------------------------
    // カメラ行列を取得し、その逆行列を得る
    // ----------------------------------------
    iViewMat = (PMatrix3D)getMatrix();
    iViewMat.invert();
    
    // 脚が地面に着くよう、y 方向の並進量を求める
    float shiftY = bodyTree.getGlobalMaxYCoord();
    
    pushMatrix();
    translate(0, -shiftY, 0);
    
    bodyTree.render();
    
    popMatrix();
  }
  
  
  // ----------------------------------------
  
  
  // ========================================
  // 画像ファイルを一括ロードして、imgMapで一元管理します
  // これによって、同じ画像を何度もロードしなくてよくなります
  // ========================================
  private void createImageMap() {
    if(imgMap == null) imgMap = new HashMap<String, PImage>();
    
    if(!imgMap.containsKey("NORMAL")) imgMap.put("NORMAL",loadImage("d_normal.jpg"));
    if(!imgMap.containsKey("COIN"))   imgMap.put("COIN",  loadImage("d_coin.jpg"));
    if(!imgMap.containsKey("TAB"))    imgMap.put("TAB",   loadImage("d_tab.jpg"));
    if(!imgMap.containsKey("FACE"))   imgMap.put("FACE",  loadImage("d_face.jpg"));
  }
  
  
  // ========================================
  // ボディパーツ(幾何情報とマテリアル情報)を、
  // ハッシュ表と木構造に格納します。
  // ハッシュ表は身体部位を高速に検索でき、
  // また、木構造は階層的なアフィン変換を容易に実現できます
  // ========================================
  private void createBody() {
    if(bodyMap == null) bodyMap = new HashMap<String, Node>();
    else bodyMap.clear();
    
    createImageMap();
  
    final float MARGIN = 20;
  
    // ----------------------------------------
    // 胴体
    // ----------------------------------------
    final float BODY_WIDTH  = 130;
    final float BODY_HEIGHT = 155;
    final float BODY_DEPTH  = 130;
    
    // 幾何情報の設定
    Node body = new Node();
    body.size.set(BODY_WIDTH, BODY_HEIGHT, BODY_DEPTH);
    
    // テクスチャの設定
    body.applyTexture("DEFAULT", imgMap.get("NORMAL"));
    body.applyTexture("FRONT",   imgMap.get("COIN"));
    
    
    // ----------------------------------------
    // 頭
    // ----------------------------------------
    final float HEAD_WIDTH  = 275;
    final float HEAD_HEIGHT = 180;
    final float HEAD_DEPTH  = 180;
    
    Node head = new Node();
    head.pos.set (0,          -.5 * (BODY_HEIGHT + HEAD_HEIGHT), 0         );
    head.org.set (0,           .5 * HEAD_HEIGHT,                 0         );
    head.size.set(HEAD_WIDTH,  HEAD_HEIGHT,                      HEAD_DEPTH);
    
    head.applyTexture("TOP",     imgMap.get("TAB"));
    head.applyTexture("FRONT",   imgMap.get("FACE"));
    head.applyTexture("DEFAULT", imgMap.get("NORMAL"));
    
    
    // ----------------------------------------
    // 腕
    // ----------------------------------------
    final float ARM_WIDTH  =  45;
    final float ARM_HEIGHT = 180;
    final float ARM_DEPTH  =  50;
    
    Node leftArm = new Node();
    leftArm.pos.set(-.5 * (BODY_WIDTH + ARM_WIDTH + MARGIN),    // x
                    -.5 * (BODY_HEIGHT - ARM_HEIGHT) + MARGIN,  // y
                      0);                                       // z

    leftArm.org.set (0,         -.5 * ARM_HEIGHT, 0        );
    leftArm.size.set(ARM_WIDTH,   ARM_HEIGHT,     ARM_DEPTH);

    leftArm.applyTexture("DEFAULT", imgMap.get("NORMAL"));


    Node rightArm = new Node();
    rightArm.pos.set( .5 * (BODY_WIDTH + ARM_WIDTH + MARGIN),    // x
                     -.5 * (BODY_HEIGHT - ARM_HEIGHT) + MARGIN,  // y
                       0);                                       // z

    rightArm.org.set (0,         -.5 * ARM_HEIGHT, 0        );
    rightArm.size.set(ARM_WIDTH,  ARM_HEIGHT,      ARM_DEPTH);

    rightArm.applyTexture("DEFAULT", imgMap.get("NORMAL"));
    
    
    // ----------------------------------------
    // 脚
    // ----------------------------------------
    final float LEG_HEIGHT = 130;
    final float LEG_WIDTH  =  50;
    final float LEG_DEPTH  = 100;
    
    Node leftLeg = new Node();
    leftLeg.pos.set(-.5 * (LEG_WIDTH + MARGIN),        // x
                     .5 * (BODY_HEIGHT + LEG_HEIGHT),  // y
                     0);                               // z
                     
    leftLeg.org.set (0,         -.5 * LEG_HEIGHT, 0        );
    leftLeg.size.set(LEG_WIDTH,   LEG_HEIGHT,     LEG_DEPTH);
    
    leftLeg.applyTexture("DEFAULT", imgMap.get("NORMAL"));
  
  
    Node rightLeg = new Node();
    rightLeg.pos.set(.5 * (LEG_WIDTH + MARGIN),        // x
                     .5 * (BODY_HEIGHT + LEG_HEIGHT),  // y
                      0);                              // z
                     
    rightLeg.org.set (0,         -.5 * LEG_HEIGHT, 0        );
    rightLeg.size.set(LEG_WIDTH,   LEG_HEIGHT,     LEG_DEPTH);
    
    rightLeg.applyTexture("DEFAULT", imgMap.get("NORMAL"));
    
    
    // ----------------------------------------
    // スカート(?)
    // ----------------------------------------
    final float SKIRT_HEIGHT = .4 * BODY_DEPTH;
    final float SKIRT_ANGLE1 =  radians(30);
    final float SKIRT_ANGLE2 =  radians(20);
    
    Node backSkirt = new Node();
    
    backSkirt.pos.set( 0,                                 // x
                      .5 * (BODY_HEIGHT + SKIRT_HEIGHT),  // y
                      .5 * BODY_DEPTH);                   // z
                      
    backSkirt.org.set (0,            -.5 * SKIRT_HEIGHT, 0);
    backSkirt.rot.set (SKIRT_ANGLE1,  0,                 0);
    backSkirt.size.set(BODY_WIDTH,    SKIRT_HEIGHT,      1);
    
    backSkirt.applyTexture("DEFAULT", imgMap.get("NORMAL"));
    
    
    Node frontSkirt = new Node();

    frontSkirt.pos.set(  0,                                 // x
                        .5 * (BODY_HEIGHT + SKIRT_HEIGHT),  // y
                       -.5 * BODY_DEPTH);                   // z
                       
    frontSkirt.org.set ( 0,            -.5 * SKIRT_HEIGHT, 0);
    frontSkirt.rot.set (-SKIRT_ANGLE1,  0,                 0);
    frontSkirt.size.set( BODY_WIDTH,    SKIRT_HEIGHT,      1);
    
    frontSkirt.applyTexture("DEFAULT", imgMap.get("NORMAL"));
  
  
    Node leftSkirt = new Node();
    
    leftSkirt.pos.set(-.5 * BODY_WIDTH,                    // x
                       .5 * (BODY_HEIGHT + SKIRT_HEIGHT),  // y
                        0);                                // z
    
    leftSkirt.org.set (0, -.5 * SKIRT_HEIGHT, 0           );
    leftSkirt.rot.set (0,   0,                SKIRT_ANGLE2);
    leftSkirt.size.set(1,   SKIRT_HEIGHT,     BODY_DEPTH  );
    
    leftSkirt.applyTexture("DEFAULT", imgMap.get("NORMAL"));
  
  
    Node rightSkirt = new Node();
    
    rightSkirt.pos.set(.5 * BODY_WIDTH,                    // x
                       .5 * (BODY_HEIGHT + SKIRT_HEIGHT),  // y
                        0);                                // z
                       
    rightSkirt.org.set (0, -.5 * SKIRT_HEIGHT,  0           );
    rightSkirt.rot.set (0,   0,                -SKIRT_ANGLE2);
    rightSkirt.size.set(1,   SKIRT_HEIGHT,      BODY_DEPTH  );
    
    rightSkirt.applyTexture("DEFAULT", imgMap.get("NORMAL"));
    
    
    // ----------------------------------------
    // ハッシュ表の構築
    // ----------------------------------------
    bodyMap.put("HEAD",     head);
    bodyMap.put("BODY",     body);
    bodyMap.put("LEFTARM",  leftArm);
    bodyMap.put("RIGHTARM", rightArm);
    bodyMap.put("LEFTLEG",  leftLeg);
    bodyMap.put("RIGHTLEG", rightLeg);
    
    
    // ----------------------------------------
    // 木の構築
    // ----------------------------------------
    //
    //     頭
    //     |
    //     胴 ← Root
    //   /| |\ 
    // 右腕 右 左 左腕
    //    脚 足
    // ----------------------------------------
    body.addChild(leftLeg);
    body.addChild(rightLeg);
    body.addChild(head);
    body.addChild(leftArm);
    body.addChild(rightArm);
    bodyTree = body;
  
    // おまけ:スカートのようなもの
    // こちらは別にどうでもいいのでハッシュ表には入れない
    body.addChild(backSkirt);
    body.addChild(frontSkirt);
    body.addChild(leftSkirt);
    body.addChild(rightSkirt);
  }
}



【おまけ】

主催者からの紹介文


モーション、エフェクト、3D、パーティクル、計算幾何、とP5で幅広く活動していらっしゃる @tercel_s さんが、Processing Advent Calendar 2011 に参戦!! 12/3日担当予定 http://t.co/63T4x3C0 #p5advent2011年12月1日 15:15 via web

うわぁなにこのプレシャーΣ(・Д・ノ)ノ

2 件のコメント:

  1. 分かりやすい解説、ありがとうございます。
    プログラムのソースコードをダウンロードして実行してみたのですが、
    エラーが出てしまいます。
    「can not find a class or type named "Map"」と表示されます。
    当方、プログラミングは初心者なため原因が分かりません。
    教えていただけないでしょうか。

    返信削除
  2. コメントありがとうございます。

    お返事が遅れてしまい、申し訳ございません。

    お使いのProcessinのバージョンにもよりますが、以下をお試しいただけますでしょうか。

    ① Processingのモードを「Java」に切り替える
    ② プログラムの中のMapという単語を「HashMap」に置き換える

    返信削除

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