2012/02/27

ProcessingでXファイルを解析(スタティックメッシュ篇その1)

今日は、DirectX で標準的に使用されていた X ファイルを読み込んで、Processing で表示してみたいと思います。 すなわち ——
Metasequoia にプリセットされている「日本橋麒麟像」
こんなふうに任意のモデリングソフトで作成したデータを、

上図のように、Processing でそのまま使えるようにしてしまおうという試みです。

しかし僕がアホすぎるせいで、まだ静止した 3D モデルの幾何形状だけしか読めていませんごめんなさい(アニメーションはおろか物体の色付けやテクスチャも無し)。



この課題の一番のネックは X ファイルのパーサです。

X のフォーマットはそれなりに自由度が高く、使用するモデリングソフトによって構造が変わったりします。 すべての状況に適応できる汎用性の高いコードを本気で書こうとすると、割と本格的な構文解析器を実装する必要があるような気がして心が折れます。

というわけで、個人的にはひとまず姑息的なパーサを手っ取り早く作り、なんか問題が起きたら適宜設計を見直しつつ修正していこうかと思います。



とりあえずXファイル読み込み関連をまとめたプログラム(長いので折りたたんであります)。
// -------------------------------------------------
// =================================================
// Xファイル形式で記述されたメッシュを表示するテスト
//                    スタティックメッシュ篇不完全版
// =================================================
// -------------------------------------------------


// =================================================

// メッシュ
class Mesh {
  List<Polygon> polygons;
  
  public Mesh() {
    polygons = new ArrayList<Polygon>();
  }
  
  public void add(Polygon p) {
    polygons.add(p);
  }
  
  public void render() {
    if(polygons == null) return;
    
    for(Polygon p : polygons) p.render();
  }
}

// =================================================

// ポリゴン
class Polygon {
  List<PVector> vertices;
  
  public Polygon() {
    vertices = new ArrayList<PVector>();
  }
  
  public void add(PVector v) {
    vertices.add(v);
  }
  
  public void render() {
    if(vertices == null) return;
    
    if(vertices.size() == 3)      beginShape(TRIANGLES);
    else if(vertices.size() == 4) beginShape(QUADS);
    else                          beginShape();
    for(PVector v : vertices) vertex(v.x, v.y, v.z);
    endShape();
  }
}

// =================================================

// Xファイルローダクラス
class Loader {
  // Xファイルの文字列データが一括して格納される
  private StringBuffer data;
  
  // Xファイル内の部分文字列の先頭を指すインデックス
  private int          index;

  // 頂点リスト
  List<PVector> verticesList;
  
  // ----------------------------------------
  public Loader() {
    data = new StringBuffer();
    verticesList = new ArrayList<PVector>();
  }
  
  // ----------------------------------------
  // ファイル名を指定してメッシュをロード 
  public Mesh loadX(final String fileName) { 
    Mesh mesh = new Mesh();
    
    index = 0;
    data.delete(0, data.length());
    String[] lines = loadStrings(fileName);
    for(String line : lines) {
      // ここで本来はコメント除去の処理が必要
      
      data.append(line + "\n");
    }
    while(index < data.length()) {
      String token = getNextToken();
      
      // テンプレートは読み飛ばす
      if(token.equals("template")) skipBlock();

      // ====================
      // MESH
      // ====================
      else if(token.equals("Mesh")) {
        token = getNextToken();
        // トークンが { でない場合
        if(!token.equals("{")) {
          getNextToken();
        } 
        /*
        else {
          // 名前つきマップに入れたい
        }*/
        
        // 頂点リストを作成
        verticesList.clear();
        
        // 全頂点数を取得
        int nVertices = Integer.parseInt(getNextToken());
        
        // 各頂点の座標を取得
        for(int i = 0; i < nVertices; ++i) {
          float x = Float.parseFloat(getNextToken());
          float y = Float.parseFloat(getNextToken());
          float z = Float.parseFloat(getNextToken());          
          verticesList.add(new PVector(x, y, z));
        }
        
        // ポリゴン数を取得
        int nPolygons = Integer.parseInt(getNextToken());
        for(int i = 0; i < nPolygons; ++i) {
          Polygon poly = new Polygon();
          
          // 1ポリゴンあたりの頂点数
          int nVerticesPerPolygon = Integer.parseInt(getNextToken());
          for(int j = 0; j < nVerticesPerPolygon; ++j) {
            
            // 頂点インデックスを取得
            int verticesIndex = Integer.parseInt(getNextToken());
            
            // 頂点インデックスから該当する頂点を取得してポリゴンに追加
            poly.add(verticesList.get(verticesIndex));
          }

          mesh.add(poly);
        }
      }
    }
    return mesh;
  }
  
  // ----------------------------------------
  // { } で囲まれたブロックを読み飛ばす
  private void skipBlock() {
    try {
      // はじめの { まで読み飛ばす 
      while(!(getNextToken().equals("{")) && index < data.length()) ;
      
      int n = 1;
      while(n > 0 && index < data.length()) {
        String token = getNextToken();
        if(token.equals("{")) ++n;
        if(token.equals("}")) --n;
      }
    } catch (RuntimeException ex) {
      ex.printStackTrace();
    }
  }
  
  // ----------------------------------------
  // 次のトークンを得る
  private String getNextToken() {
    // 最初にインデックスの範囲チェック
    if(!(index < data.length())) return "";  // ファイルの末尾に達した場合は""を返す
    
    // デリミタ(区切り文字)の読み飛ばし
    String delimiters = " \t\r\n,;\"";
    while(index < data.length() && 
          strChr(delimiters, data.charAt(index)))  
      ++index;

    if(!(index < data.length())) return "";  // ファイルの末尾
 
    // 中括弧の検出
    char[] c = { data.charAt(index) };
    if(c[0] == '{' || c[0] == '}') {
      ++index;
      return new String(c);
    }
    
    // それ以外のトークン検出
    delimiters += "{}";
    int startIndex = index;
    int endIndex   = index;
    while(endIndex < data.length() &&
          !strChr(delimiters, data.charAt(endIndex)))
      ++endIndex;
    index = endIndex;
    return data.substring(startIndex, endIndex);
  }
  
  // ----------------------------------------
  // 文字列シーケンスsの中に、cが含まれているかどうかをチェック
  private boolean strChr(final CharSequence s, final char c) {
    for(int i = 0; i < s.length(); ++i)
      if(s.charAt(i) == c) return true;
    return false;
  }
}

で、使い方はこんな感じ。

Mesh mesh;

void setup() {
  size(640, 480, P3D);
  
  // ファイル名を指定してXをロード  
  mesh = new Loader().loadX("nihonbasikirin.x");
  
}

void draw() {
  background(0);
  camera();
  lights();
  noStroke();

  camera(500, 500, 500, 0, 300, 0, 0, -1, 0);
    
  scale(300);
  
  // メッシュの描画
  mesh.render();
}



ちなみに、現在の制約では、一つの X ファイルに複数のメッシュ情報が定義されている場合、うまく読み込む事ができません(対応は簡単ですが、それが必要な例を見た事がないのでとりあえずこのままでいいや的な……)。

ただ、視覚的には複数のように見えるメッシュであっても、内部的には一つのメッシュにまとめられているような場合は、正しくロードする事ができます(下図参照)。

Metasequoia にプリセットされている「魔道少女」
これを Processing で表示させると、ちゃんと 2 つのモデルが表示されます。


これからいろいろ忙しくなりますが、暇を見つけて『色付け』と『アニメーション』に対応していけたらなぁ……と考えています。 うーん……。

0 件のコメント:

コメントを投稿

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