画面写真をクリックするとサンプルのWebGLビルドに飛びます。が、本題の「アセットにする」部分は エディタでしか動かないので、あまり意味はありません。
こんにちは。技術部平山です。
今回は、MeshをUnity外に持ち出せる形(.obj)で保存するお話をします。
ソースコードはgithubに置いてありますが、 同じことをする実績のあるコードがありますので、 それを使う方が良いでしょう。
単にアセットにできればいい場合
本題に入る前に、「単にアセットにできればいい場合」の方法を書いておきます。 計算で作ったメッシュをプレハブに入れたいとか、シーンに置いてライトマップを焼きたい、 とかいう場合ですね。 この場合、AssetDatabase.CreateAsset() にメッシュを渡しておしまいです。
以下のコードをどこかに用意しておけば、 MeshFilterで右クリックして、差さっているMeshをセーブできるようになります。
[MenuItem("CONTEXT/MeshFilter/Save .asset")] public static void SaveFromInspector(MenuCommand menuCommand) { var meshFilter = menuCommand.context as MeshFilter; if (meshFilter != null) { var mesh = meshFilter.sharedMesh; if (mesh != null) { var path = string.Format("Assets/{0}.asset", mesh.name); AssetDatabase.CreateAsset(mesh, path); AssetDatabase.SaveAssets(); // これしないと空のメッシュだけできて中身が消えます } } }
後述する実装の手間も制限もありません。
ただし、標準で入っているSphereやCube、あるいは元々アセットとして存在しているメッシュに 対して呼ぶとエラーになります。「あの標準で入ってる球のメッシュが欲しい」 と思っても、このやり方では取れないわけです。
使い方
まず、ObjFileWriter.cs を持っていってプロジェクトに入れます。
hierarchyから
動的に生成したMeshがついているMeshFilterコンポーネントをInspectorに表示し、 コンポーネントの名前の上で右クリックをします。
Save .obj
なるメニューがありますので、押します。
すると、MeshFilterについているMeshが.objファイル形式で、
Assets/
に吐き出され、インポートされます。
あとは、普通にFBXをつっこんだ時と同様です。プレハブになっていますので、 hierarchyにつっこめば絵が出ます。
projectから
メッシュアセットを選んで右クリックし、Save .obj
すれば同じ場所に.obj形式で吐かれます。
制限事項
- 位置、UV一つ、法線、しか出ません。
- サブメッシュが複数ある場合、バラバラに吐き出されます。
- マテリアルは吐きません
これらの制限は、.obj形式を選んだことと、私の手抜きによります。 フォーマットの制限に関する問題は、 collada(.dae)あたりを選べば解決できるのですが、 そこまでやるなら既存のものを使いたいところです。
動機
やりたいことは二つあります。
- 動的に作ったメッシュをアセットにしたい
- 動的に作ったメッシュをUnityの外に出したい
前者ができれば、冒頭の画面写真にあるように、
計算で作ったメッシュをstaticにしてライトマップを貼ったりできます。
しかし、これは最初に述べたように、AssetDatabase.CreateAsset()
で終わりです。
問題は後者の方です。 ステージクリア型の小規模なゲームを想像してみてください。 パズルゲームなんかがいいでしょうね。
ステージの寸法を決めたり、壁を置いたりするのは、 ゲームデザイナーの仕事です。ステージエディタなどで修正を繰り返しながら、 ゲームが面白くなる配置を作ります。
この段階ではデバグ描画か何かでゲームが作られていて、 単なる線だったりしていることでしょう。 このままでは売れないので、アーティストさんに渡して モデリングをしてもらうわけですが、 その時に「寸法が合っているメッシュデータ」 があれば、仕様書代わりになるでしょう。 エクセル方眼紙などの辛い方法で渡す必要がなくなります。
また、Macの場合Finderでファイルを選ぶだけで、中身が見えたりします。 .objはメジャーなフォーマットなので、結構いろんなもので中身が見られるのです。
そういうことを考えると、制限事項にひっかかりさえしなければ、 標準の.assetであるよりも、むしろ.objの方が便利な気すらしてきます。
ちなみに、.assetで吐くと素のmeshなので、hierarchyに放りこむと
マテリアルがなくてピンクになります。
.objで吐くとt: model
でひっかかる、何かMeshではない型(prefabではない)
になるので、デフォルトのマテリアルもついていて、放りこむだけで一応の絵が出ます。
.objファイルについて少し
.objファイルはこれ以上なく簡単なファイルフォーマットです。 プログラムで生成した正四面体のデータをご覧ください。
Generated by Kayac.ObjFileWriter. 6 vertices, 8 faces. # positions v -0.50000000 0.00000000 0.00000000 v 0.00000000 -0.50000000 0.00000000 v 0.00000000 0.00000000 -0.50000000 v 0.00000000 0.00000000 0.50000000 v 0.00000000 0.50000000 0.00000000 v 0.50000000 0.00000000 0.00000000 # normals vn -0.50000000 0.00000000 0.00000000 vn 0.00000000 -0.50000000 0.00000000 vn 0.00000000 0.00000000 -0.50000000 vn 0.00000000 0.00000000 0.50000000 vn 0.00000000 0.50000000 0.00000000 vn 0.50000000 0.00000000 0.00000000 # triangle faces f 1//1 3//3 2//2 f 1//1 5//5 3//3 f 2//2 4//4 1//1 f 1//1 4//4 5//5 f 2//2 6//6 4//4 f 4//4 6//6 5//5 f 2//2 3//3 6//6 f 3//3 5//5 6//6
説明する必要もないほど簡単ですね。まして書き出しだけなら実装も一瞬です。 vが頂点座標、vnが法線です。 この例にはありませんがvtはUVです。あとはfで、頂点、UV、法線の番号を 組にして面を定義します。気をつけることがあるとすれば、番号が1始まりなことくらいですね。
コンテキストメニューについて少し
コンポーネントで右クリックするとメニューが出る、というのは、 以下のようにすれば実装できます。
[MenuItem("CONTEXT/MeshFilter/Save .obj")] public static void SaveFromInspector(MenuCommand menuCommand) { var meshFilter = menuCommand.context as MeshFilter; if (meshFilter != null) { var mesh = meshFilter.sharedMesh; // meshにしちゃうとコピー走るよ if (mesh != null) { Write("Assets", mesh, importImmediately: true); } } }
MenuItem属性の引数をCONTEXT/
から始め、コンポーネントの型名(ここではMeshFilter)を書いて、
その後にメニューに出る項目名を並べます。
MenuCommand型が引数にもらえ、ここからコンポーネントのインスタンスが取れますので、
めでたくMeshがゲットできるわけです。
終わりに
実は私は、AssetDatabase.CreateAsset()
の存在を知る前に、
.objの吐き出しを書いてしまっていましたし、
既存実装を知ったのも書き終えてからです。
この記事を書く時に改めて調べたらいろいろ出てきて焦った、
という経緯があります。
事前に調べていたはずなのに、何故出てこなかったのか不思議で仕方ありません。 人は見たいものしか見ないと言いますが、もしかしたら自作したい気持ちが 私の目を節穴にしたのかもしれませんね。
...こんな面白くないものを書きたいと思ってるはずはないんですが。
なお、終わってから振りかえってみれば、自分で実装しても悪くなかった気はします。
- 欲しい機能は限られていて、既存実装よりも小さいもので十分だった。
- 既存実装ではfloat.ToString()を精度指定なしで呼んでおり、10進6桁では劣化が気になる。
- 既存実装ではMeshFilterを引数に取る作りだが、私は素のVector配列とint配列から吐きたかった。
- subMeshを全部吐くのは私の用途ではむしろ困る。