対応が遅くなりましたが、Wavefront .OBJファイルのインポート機能を実装しました。
OBJファイルはCG分野で使用されることが多く、重複頂点もない点とマテリアルが設定できるのが利点です。
IO処理はもちろん非同期処理で読み書きしています。
宣言部分
public static partial class OBJFile { /// <summary> /// OBJファイルの読み込み /// </summary> /// <param name="filePath">ファイルパス</param> /// <returns>成功したら三角形ファセット群のデータを返し、失敗したらnullを返す</returns> public static async Task<TriangleMesh> LoadAsync(string filePath)
OBJファイルフォーマット
OBJファイルフォーマットは以下のような構成になります。
# コメント
mtllib マテリアルファイル名
g グループ名
usemtl マテリアル名
v x成分値 y成分値 z成分値
v x成分値 y成分値 z成分値
v x成分値 y成分値 z成分値
…(省略)…
vt x成分値 y成分値
vt x成分値 y成分値
vt x成分値 y成分値
…(省略)…
vn x成分値 y成分値 z成分値
vn x成分値 y成分値 z成分値
vn x成分値 y成分値 z成分値
…(省略)…
f 頂点座標値番号/テクスチャ座標値番号/頂点法線ベクトル番号 (多角形の頂点の数だけ続く)
f 頂点座標値番号/テクスチャ座標値番号/頂点法線ベクトル番号 (多角形の頂点の数だけ続く)
f 頂点座標値番号/テクスチャ座標値番号/頂点法線ベクトル番号 (多角形の頂点の数だけ続く)
…(省略)…
#はコメントなので読み飛ばします。
※参考文献は以下になります。
また、以下のようなWavefront .OBJファイルのデータ集等も活用すると面白いと思います。
ソースコード
今回は頂点座標と面の頂点インデックスを使用するので、vの行とfから始まる行の頂点座標値番号のみ読み込んでモデルを描画します。
public static partial class OBJFile { /// <summary> /// OBJファイルの読み込み /// todo:頂点共有の処理 /// </summary> /// <param name="filePath">ファイルパス</param> /// <returns>成功したら三角形ファセット群のデータを返し、失敗したらnullを返す</returns> public static async Task<TriangleMesh> LoadAsync(string filePath) { // テキストファイルを開く using(var sr = File.OpenText(filePath)) { string line; var vertices = new List<Point>(); var vertexIndices = new List<Tuple<int, int, int>>(); string lowerToken; // 1行ずつ読み込む while ((line = await sr.ReadLineAsync()) != null) { // スペース区切りのトークンに分ける IEnumerable<string> tokens = line.Trim().Split(' ', '/'); // トークンを小文字に変更する lowerToken = tokens.First().ToLower(); // トークンに対して然るべき処理を行う。 // それなりに厳密に書式を見ていくことにする if (lowerToken == "#") { // コメント continue; } else if (lowerToken == "mtllib") { // マテリアル情報、今のところ何もしない continue; } else if (lowerToken == "g") { // 何もしない continue; } else if (lowerToken == "usemtl") { // 何もしない continue; } else if (lowerToken == "v") { // 頂点座標値 double value = double.NaN; double value1 = double.NaN; double value2 = double.NaN; if (double.TryParse(tokens.ElementAt(1), out value) && double.TryParse(tokens.ElementAt(2), out value1) && double.TryParse(tokens.ElementAt(3), out value2)) { vertices.Add(new Point(value, value1, value2)); } } else if (lowerToken == "vt") { // 頂点テクスチャ番号、今のところ何もしない continue; } else if (lowerToken == "vn") { // 頂点法線ベクトル、今のところ何もしない continue; } else if (lowerToken == "f") { // 面情報 int value; int value1; int value2; // 頂点番号 if (int.TryParse(tokens.ElementAt(1), out value) && int.TryParse(tokens.ElementAt(2), out value1) && int.TryParse(tokens.ElementAt(3), out value2)) { vertexIndices.Add(new Tuple<int, int, int>(value - 1, value1 - 1, value2 - 1)); } } else if (lowerToken == string.Empty) { // 何もしない } else { throw new Exception("無効な書式です。"); } } if (vertices.Count >= 3 && vertexIndices.Count > 0) { return new TriangleMesh(vertices, vertexIndices); } else { return null; } } } }
コードの書き方自体が古典的すぎますが、単純にvの行のスペースで区切られている値3つを頂点座標とし、fの行スペースで区切られている最初の値3つを頂点インデックスとして三角形メッシュの構造にするのみです。
フレームワークのViewModel側からファイル読み込みコマンド実行時のコードを以下のように書き換えました。
Inport = new RelayCommand( () => { MessengerInstance.Send( new FileOpenMessage( async path => { var lowerPath = path.ToLower(); var token = lowerPath.Split('.'); switch (token.GetValue(token.Length - 1)) { case "stl": var triangleMesh = await STLFile.LoadAsync(path); モデル.モデルを追加する(new TriangleMesh(triangleMesh.Vertices, triangleMesh.VertexIndices, triangleMesh.VertexNormals)); break; case "obj": triangleMesh = await OBJFile.LoadAsync(path); モデル.モデルを追加する(new TriangleMesh(triangleMesh.Vertices, triangleMesh.VertexIndices, triangleMesh.VertexNormals)); break; } })); });
実行結果
フレームワーク内で拡張子判別しているのですがもっと良い方法がある筈なので後ほどリファクタリングすると思います。
サンプルのティーポットの読み込み結果が以下になります。
実装時間30分くらいでしたがなんでこんなに間が開いてしまったのか自分でも理解できません(汗。
3ds Maxのファイルフォーマット対応で多少幅が広がったと思います。