メッシュを.objにしてUnityから出す

画面写真をクリックするとサンプルの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に表示し、 コンポーネントの名前の上で右クリックをします。

f:id:hirasho0:20190902193432p:plain

Save .objなるメニューがありますので、押します。 すると、MeshFilterについているMeshが.objファイル形式で、 Assets/に吐き出され、インポートされます。

f:id:hirasho0:20190902193439p:plain

あとは、普通にFBXをつっこんだ時と同様です。プレハブになっていますので、 hierarchyにつっこめば絵が出ます。

projectから

メッシュアセットを選んで右クリックし、Save .objすれば同じ場所に.obj形式で吐かれます。

制限事項

  • 位置、UV一つ、法線、しか出ません。
  • サブメッシュが複数ある場合、バラバラに吐き出されます。
  • マテリアルは吐きません

これらの制限は、.obj形式を選んだことと、私の手抜きによります。 フォーマットの制限に関する問題は、 collada(.dae)あたりを選べば解決できるのですが、 そこまでやるなら既存のものを使いたいところです。

動機

やりたいことは二つあります。

  • 動的に作ったメッシュをアセットにしたい
  • 動的に作ったメッシュをUnityの外に出したい

前者ができれば、冒頭の画面写真にあるように、 計算で作ったメッシュをstaticにしてライトマップを貼ったりできます。 しかし、これは最初に述べたように、AssetDatabase.CreateAsset()で終わりです。

問題は後者の方です。 ステージクリア型の小規模なゲームを想像してみてください。 パズルゲームなんかがいいでしょうね。

ステージの寸法を決めたり、壁を置いたりするのは、 ゲームデザイナーの仕事です。ステージエディタなどで修正を繰り返しながら、 ゲームが面白くなる配置を作ります。

この段階ではデバグ描画か何かでゲームが作られていて、 単なる線だったりしていることでしょう。 このままでは売れないので、アーティストさんに渡して モデリングをしてもらうわけですが、 その時に「寸法が合っているメッシュデータ」 があれば、仕様書代わりになるでしょう。 エクセル方眼紙などの辛い方法で渡す必要がなくなります。

また、Macの場合Finderでファイルを選ぶだけで、中身が見えたりします。 .objはメジャーなフォーマットなので、結構いろんなもので中身が見られるのです。

f:id:hirasho0:20190902193436p:plain

そういうことを考えると、制限事項にひっかかりさえしなければ、 標準の.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を全部吐くのは私の用途ではむしろ困る。

参考文献