2012/02/28

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

昨日の続きです。ほんのちょっと進歩してマテリアルを反映できるようになりました。

こういうかっこいい戦車も……
Metasequoia にプリセットされている「Tank」
ちゃんと質感を保ったまま Processing に持ってくる事ができました。やったー!
Processing で表示した結果
※ というのは大ウソで、法線無視、フラットシェーディング一択という男らしい割り切り方をしています。P3D モードにおけるレンダリングのクオリティならばこのくらいばっさり行っても許容範囲な気がします(おい



ちなみにここだけの話、X ファイル内のジオメトリの頂点数とテクスチャのUV頂点数が一致していないと表示できないという問題点があるのですが、Metasequoia が吐く X ファイルでは特に目立った問題は起きていないため、思い切って無視しています。

それでも泥縄式に拡張しただけあって、いい感じにソースが汚くなって参りました。

【Xファイル解析関連のコード】
// -------------------------------------------------
// =================================================
// Xファイル形式で記述されたメッシュを表示するテスト
//                スタティックメッシュ篇やや不完全版
// =================================================
// -------------------------------------------------


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

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

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

// ポリゴン
class Polygon {
  List<PVector> vertices;
  List<PVector> textureCoords;
  Material      material;
  
  public Polygon() {
    vertices = new ArrayList<PVector>();
  }
  
  public void addVertex(PVector v) {
    vertices.add(v);
  }
  
  public void addTextureCoord(PVector t) {
    if(textureCoords == null) textureCoords = new ArrayList<PVector>();
    textureCoords.add(t);
  }
  
  public void setMaterial(Material m) {
    material = m;
  }
  
  public void render() {
    if(vertices == null) return;
    
    if(vertices.size() == 3)      beginShape(TRIANGLES);
    else if(vertices.size() == 4) beginShape(QUADS);
    else                          beginShape();
    
    pushStyle();
    
    // マテリアルの設定
    if(material != null) {
      noStroke();
      fill(material.faceColor);

      shininess(material.power);
      specular(material.specularColor);
      emissive(material.emissiveColor);      
      
      if(material.texture != null) {
        // テクスチャが存在する場合
        texture(material.texture);
      }
    }
    
    for(int i = 0; i < vertices.size(); ++i) {
      PVector v = vertices.get(i);
      // 実際の頂点とテクスチャのUVが一致していない場合は表示できないよ
      if(textureCoords != null && 
          textureCoords.size() == vertices.size() && 
            material != null &&
              material.texture != null) {
          
        PVector tex = textureCoords.get(i);
        textureMode(NORMALIZED);
        vertex(v.x, v.y, v.z, tex.x, tex.y);
      } else {
        vertex(v.x, v.y, v.z);
      }
    }
    popStyle();
    endShape();
  }
}

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

// マテリアル
class Material {
  int     faceColor;
  float   power;
  int     specularColor;
  int     emissiveColor;
  PImage  texture;
  
  public Material() {
    texture       = null;
  }
}

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

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

  // メッシュとマテリアルの連想配列
  private HashMap<String, Mesh>     meshMap;
  private HashMap<String, Material> materialMap;

  // ポリゴンごとの頂点インデックス格納用
  private List<List<Integer>> vertexIndices;

  // メッシュ
  private Mesh mesh;

  // ----------------------------------------
  public Loader() {
    data = new StringBuffer();
    
    vertexIndices = new ArrayList<List<Integer>>();
    
    meshMap      = new HashMap<String, Mesh>();
    materialMap  = new HashMap<String, Material>();
  }
  
  // ----------------------------------------
  // ファイル名を指定してメッシュを読み込み 
  public Mesh loadX(final String fileName) { 

    // 色々初期化
    mesh = null;

    vertexIndices.clear();
    meshMap.clear();
    materialMap.clear();
    
    index = 0;

    data.delete(0, data.length());
    String[] lines = loadStrings(fileName);
    for(String line : lines) {
      // コメント除去の処理
      data.append(line.replaceAll("(##|//).*", "") + "\n");
    }
    
    List<PVector> textureCoordsList = null;
    while(index < data.length()) {
      String token = getNextToken();
      
      // テンプレートは読み飛ばす
      if(token.equals("template")) skipBlock();

      // ====================
      // Material(前方宣言)
      // ====================
      else if(token.equals("Material")) loadMaterialData();

      // ====================
      // MESH
      // ====================
      else if(token.equals("Mesh")) mesh = loadMeshData();

      // ====================
      // MeshTextureCoords
      // ====================
      else if(token.equals("MeshTextureCoords")) loadTextureCoords();
      
      // ====================
      // MeshMaterialList
      // ====================
      else if(token.equals("MeshMaterialList")) loadMaterialListData();

    }
    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();
    }
  }
  
  // ----------------------------------------
  // Meshブロックを読む
  private Mesh loadMeshData() {
    Mesh mesh = new Mesh();

    String token = getNextToken();
    // トークンが { でない場合
    if(!token.equals("{")) {
      meshMap.put(token, mesh);
      getNextToken();
    }
        
    // 頂点リストを作成
    List<PVector> verticesList = new ArrayList<PVector>();
    vertexIndices.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();
      
      // ポリゴンごとに頂点インデックスリストを作る
      List<Integer> indices = new ArrayList<Integer>();
      
      // 1ポリゴンあたりの頂点数
      int nVerticesPerPolygon = Integer.parseInt(getNextToken());
      for(int j = 0; j < nVerticesPerPolygon; ++j) {
            
        // 頂点インデックスを取得
        int verticesIndex = Integer.parseInt(getNextToken());

        indices.add(verticesIndex);
        
        // 頂点インデックスから該当する頂点を取得してポリゴンに追加
        poly.addVertex(verticesList.get(verticesIndex));
      }
      
      vertexIndices.add(indices);
      mesh.add(poly);
    }
    return mesh;
  }


  // ----------------------------------------
  // Materialブロックを読む
  private Material loadMaterialData() {
    Material material = new Material();

    String token = getNextToken();
    // トークンが { でない場合
    if(!token.equals("{")) {
      materialMap.put(token, material);
      getNextToken();
    }
    
    int r = 0xFF & round(255 * Float.parseFloat(getNextToken()));
    int g = 0xFF & round(255 * Float.parseFloat(getNextToken()));
    int b = 0xFF & round(255 * Float.parseFloat(getNextToken()));
    int a = 0xFF & round(255 * Float.parseFloat(getNextToken()));
    
    material.faceColor = a << 24 | r << 16 | g << 8 | b;
    
    material.power = Float.parseFloat(getNextToken());

    r = 0xFF & round(255 * Float.parseFloat(getNextToken()));
    g = 0xFF & round(255 * Float.parseFloat(getNextToken()));
    b = 0xFF & round(255 * Float.parseFloat(getNextToken()));

    material.specularColor = 0xFF << 24 | r << 16 | g << 8 | b;

    r = 0xFF & round(255 * Float.parseFloat(getNextToken()));
    g = 0xFF & round(255 * Float.parseFloat(getNextToken()));
    b = 0xFF & round(255 * Float.parseFloat(getNextToken()));

    material.emissiveColor = 0xFF << 24 | r << 16 | g << 8 | b;

    if(getNextToken().equals("TextureFilename")) {
      
      getNextToken();  // トークン「{」
      String textureFileName = getNextToken();
      material.texture = loadImage(textureFileName);
    }
    return material;
  }

  // ----------------------------------------
  // MeshTextureCoordsブロックを読む
  private void loadTextureCoords() {
    getNextToken();  // 「{」
    int nCoords = Integer.parseInt(getNextToken());
    List<PVector> coordsList = new ArrayList<PVector>();
    
    if(mesh == null) return;
    
    for(int i = 0; i < nCoords; ++i) {
      float u = Float.parseFloat(getNextToken());
      float v = Float.parseFloat(getNextToken());
      
      PVector texCoords = new PVector(u, v);
      coordsList.add(texCoords);
    }
    
    
    for(int i = 0; i < vertexIndices.size(); ++i) {
      Polygon       poly    = mesh.getPolygon(i);
      List<Integer> indices = vertexIndices.get(i);
      for(Integer index : indices) {
        poly.addTextureCoord(coordsList.get(index));
      }
    }
    
  }

  // ----------------------------------------
  // MeshMaterialListブロックを読む
  private void loadMaterialListData() {
    getNextToken();  // 「{」
    
    int nMaterials = Integer.parseInt(getNextToken());
    int nPolygons  = Integer.parseInt(getNextToken());
    
    // マテリアルインデックスリストを作成
    List<Integer> materialIndicesList = new ArrayList<Integer>();
    for(int i = 0; i < nPolygons; ++i)
      materialIndicesList.add(Integer.parseInt(getNextToken()));

    // Xで定義されているマテリアルを読み込み    
    List<Material> materialList = new ArrayList<Material>();
    for(int i = 0; i < nMaterials; ++i) {
      String token = getNextToken();
      while(token.equals("{")) token = getNextToken();
      while(token.equals("}")) token = getNextToken();
      
      Material material;
      if(token.equals("Material")) material = loadMaterialData();
      else                         material = materialMap.get(token);
      materialList.add(material);
    }
    
    for(int i = 0; i < nPolygons; ++i) {
      Polygon poly = mesh.getPolygon(i);
      int index    = materialIndicesList.get(i);
      Material m   = materialList.get(index);
      
      poly.setMaterial(m);
    }
    
  }

  // ----------------------------------------
  // 次のトークンを得る
  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("3 テクスチャ付き.x");
  
}

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

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

ちなみにエラー処理とかはあまりしていないので、X ファイルが規格外だったりテクスチャファイルが見つからなかったりすると平気で RuntimeException をぶん投げると思いますがそこはご愛嬌という事で。



【MESH GURUのサンプルに挑戦】

鎌田茂雄 著 『MESH GURU』 の第 8 章で使用されているサンプルのうち、アニメーションを含まない X ファイルをパースしてみる事にします。以下では、Processing における実行結果と、使用した X ファイルをそのまま掲載しています。

『MESH GURU』 のサンプルは Metasequoia が吐く X ファイルとはじゃっかん構造が違い、冒頭で名前つきの Material を定義して、以降は識別子で Material を参照する形になっています。なんとも意地の悪い書き方ではありますが、可能な限り対応する事にしました。



【世界最小Xファイル】

xof 0303txt 0032

///////////////////メッシュ//////////////////////////
Mesh Mesh_Triangle
{
  //////頂点データ部////////////
   3;
  -1.0;-1.0;0.0;,
  -1.0;1.0;0.0;,
  1.0;-1.0;0.0;;
  //////ポリゴンデータ部///////
  1;
  3;0,1,2;; 
}



【マテリアル付き】

xof 0303txt 0032
//////////////////////////マテリアル/////////////////////
Material Triangle_Blue
{
  0.0;0.0;1.0;1.0;;
  51.2;
  0.0;0.0;0.0;;
  0.0;0.0;0.0;;
}
///////////////////メッシュ//////////////////////////
Mesh Mesh_Triangle
{
   3;
  -1.0;-1.0;0.0;,
  -1.0;1.0;0.0;,
  1.0;-1.0;0.0;;
  1;
  3;0,1,2;;
  ///////////////////マテリアルリスト////////////
  MeshMaterialList 
  {
    1;
    1;
    0;
    { Triangle_Blue }
  }
}




【テクスチャ付き】

xof 0303txt 0032
//////////////////////////マテリアル/////////////////////
Material Triangle_Tex
{
  1.0;1.0;1.0;1.0;;
  51.2;
  0.0;0.0;0.0;;
  0.0;0.0;0.0;;
  TextureFilename
  {
    "コンクリート.bmp";
  }
}
///////////////////メッシュ//////////////////////////
Mesh Mesh_Triangle
{
   3;
  -1.0;-1.0;0.0;,
  -1.0;1.0;0.0;,
  1.0;-1.0;0.0;;
  1;
  3;0,1,2;;
  ///////////////////テクスチャ座標///////////////
  MeshTextureCoords
  {
    3;
    0.0;1.0;,
    0.0;0.0;,
    1.0;1.0;;
  }
  ///////////////////マテリアルリスト////////////
  MeshMaterialList 
  {
    1;
    1;
    0;
    { Triangle_Tex }
  }
}

※ 文字コードが UTF-8 以外の場合、日本語のテクスチャを読み込もうとした瞬間に死にます。



【おまけ:魔道少女】




【おまけその2:どせいさん】

こちらのサイトから拝借した「どせいさん」を表示してみました。なお、左右が反転しているのは、座標軸の取り方によるものです。

0 件のコメント:

コメントを投稿

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