HoudiniによるプロシージャルUnityプロジェクト

この記事はHoudini Advent Calendar 2020 21日目の記事です

f:id:ttata:20201221225914p:plain

はじめに

HoudiniとUnityによる普通のワークフローでは,FBX等のリソースを書き出すたびにUnity上で行う作業が発生しがちです.マテリアルの割当てだったり,Prefabを構成するGameObjectの数が変わった際のPrefabの編集等,一つずつ職人の手によるぬくもりのある手作業が必要になります(UnityのEditor拡張である程度自動化できますが...).Houdiniで大量にデータを書き出せる分,Unityで扱うデータが増えて作業の時間もかかりますし,当然ヒトが手作業で頑張るのでヒューマンエラーの温床にもなります.

このあたりのツール間のギャップをどうにかしたいなと前から思っていたのでゴリっとどうにかしてみました.

(できたてほかほかのため,あらゆる部分がナイーブな実装になってます)

思いついたアプローチ

さしあたって次の選択肢を思いつきました.

  • HoudiniEngineでがんばる
  • HoudiniからUnityProject内のYAMLをゴリゴリ書き換える
  • HoudiniからUnityをどうにか操作する

HoudiniEngineでがんばる

www.sidefx.com

HoudiniEngine For Unity自体はコアのdllをUnityとの橋渡しを行うC#で包むような作りになっています.なので外側の処理を書き換えることで,あるいはイベント周りの設定で生成の度に任意の処理を実行することができそうです.ただどうしてもHoudiniそのものを用いるよりも制約が厳しいので今回は見送りです.あとHoudiniから一歩も外に出たくないので.

HoudiniからUnityProject内のYAMLをゴリゴリ書き換える

Unityの*.prefab*.unity*.meta*.asset等のアセット関係の主要なファイルはYAMLというフォーマットで記述されています. こちらをHoudini経由で書き換えていく方法もありかなと思いました.

github.com

python3で動くunity向けのYAML parserが存在します. 最近のHoudiniのPython3ビルドがそこそこ安定しているので使用できそうです.

また,アセットを新規に追加した後の*.metaの生成もHoudini経由からできます. unityをコマンドライン上で実行する際に,--batchmodeオプションを追加することでヘッドレスで起動できます.また--quitオプションによりスクリプトの実行を指定しない場合に即時に終了させることができます. HoudiniからFBXをUnityのProjectディレクトリに書き出した後に,以下のコマンドでunityを一瞬だけ立ち上げることで*.metaファイルを生成することができます.

$ Unity.exe -quit -batchmode -projectPath <ProjectPath>

このYAMLを扱う方法は,アセットデータという低レイヤを直接操作するため強力ではありますが,GUIDの取り扱いも自前でどうにかしなければいけないため今回は見送りです.

HoudiniからUnityをどうにか操作する

何らかのプロセス間通信によりHoudiniとUnityを操作する作戦です. Socketあたりが手軽で良いかなと思いました.

Socketによる方法ですとUnity上でアセット等を取り扱うことになるので,UnityEngineの機能の強力なサポートが期待できます.

できたもの

Client

f:id:ttata:20201221223556p:plain

unity_send_command ROP Houdini側のSocket Clientです. Unityで実行したい処理を表すコマンドを文字列で指定します.このROPを実行するとUnity上で動いているSocket Serverへメッセージを送信します. なお,送信するコマンドを書き換えることで,様々な処理をUnityで行うメッセージを飛ばすunity_send_command ROPを作成することができます.

Server

Unity側で動くSocket Serverです.UniTaskを使っていい感じに非同期で走らせています.

Houdini側のClientからメッセージを受け取り,メッセージで指定された任意のスクリプトを実行します.

使い方

下準備

  1. 実行したい処理を書いたscriptをUnity上に用意する
  2. Houdiniでunity_sened_command ROPを作成する
  3. unity_sened_command ROPのメッセージを入力するパラメータに,最初に用意したscriptのClassとMethodと引数を表す文字列を指定する

実行

  1. Unityでサーバーを立ち上げる
  2. Houdini側でunity_sened_command ROPを実行

動作確認も兼ねて試しにFBXの書き出し操作とPrefabの作成を行うROP Networkを作ってみました.

以下のような構成になっています. f:id:ttata:20201221203641p:plain

filmboxfbx_geo

通常のfilmboxfbx ROPはObject Nodeしか指定できないので,そこをSOPのパスを指定できるよう拡張したものです.(今回の話とはあんまり関係無いです) 今回はいつものpigheadを書き出します.

f:id:ttata:20201221205352p:plain

unity_asset_database_refresh

Unity上でAssetDatabase.Refresh()を実行するメッセージを飛ばすunity_send_message ROPです. AssetDatabase.RefreshはUnity上でのアセットの情報の更新を行います. これを実行することで,上流でUnityのプロジェクト内に書き出されたFBXを読み込み,*.metaファイルを生成,Unityのアセットとして扱えるようになります. ちなみに上記のunityをbatchmodeで一瞬だけ立ち上げる方法に比べて処理時間が圧倒的に短いです.それはそう.

cf. AssetDatabase-Refresh - Unity スクリプトリファレンス

unity_create_prefab_from_fbx

Unity上で,指定したパスのFBXからPrefabを作成する処理を実行するメッセージを飛ばすunity_send_message ROPです. ROPのパラメータにはUnityプロジェクト内のFBXを示すパスを設定します. なおUnity側の処理についてはこちらのスクリプトをお借りしてます.

unity_exit

Unity上で動いているServerを終了するunity_send_message ROPです. メッセージのパラメータには"exit"を指定しています.

実行してみる

実際に書き出し処理を行ってみます.Unity上でServerが実行されていることを確認し,Houdiniで一連のROPを実行します.

f:id:ttata:20201221205854p:plain

無事にFBXが書き出され,Prefabも一緒に生成されました.やったー.

実装

ところどころピックアップして解説します

Client

unity_send_command rop Houdini側のSocket Clientです.ごくごく普通にpythonのsocketを使って実装しています.

なおこのClientから飛ばすペイロードは,Unityで動かしたい処理を表す次のフォーマットの文字列になっています.

<TypeName>.<MethodName>:<Args>

また,Serverを終了したい場合はexitを飛ばします.

Server

Unity側で動くSocket Serverです.適当に書きましたが今の所うまく動いているのでセーフです.

[MenuItem("Tools/StartServerTask")]
static public async UniTaskVoid StartServerTask(){
    System.Net.Sockets.TcpListener listener;
    System.Net.Sockets.TcpClient client;
    var ipStr = "127.0.0.1";
    System.Net.IPAddress ip = System.Net.IPAddress.Parse(ipStr);
    var port = int.Parse("28820");
    listener = new System.Net.Sockets.TcpListener(ip, port);
    listener.Start();

    await UniTask.Run(async() =>
    {
        do
        {
            // Clientと接続
            client = listener.AcceptTcpClient();
            var ep = (System.Net.IPEndPoint)client.Client.RemoteEndPoint;
            Debug.Log(string.Format("Connect Client: ip: {0} port: {1})", ep.Address, ep.Port));

            System.Net.Sockets.NetworkStream ns = client.GetStream();

            ns.ReadTimeout = 10000;
            ns.WriteTimeout = 10000;
            System.Text.Encoding enc = System.Text.Encoding.UTF8;

            // Messageの受け取り
            var message = ReceiveMessage(ns, enc);
            Debug.Log(message);
            if (message == "exit") break;

            // Threadを一時的に切り替えてMessageが示す処理を実行
            await UniTask.Yield();
            var result = EvaluateMessage(message);
            await UniTask.SwitchToTaskPool();

            // 事後処理
            ns.Close();
            client.Close();
        } while (true);
    });
    listener.Stop();
}

以下のループでClientからのメッセージを処理します.

  1. Clientと接続します
  2. 受け取ったペイロードの文字列<TypeName>.<MethodName>:<Args>をパースし,実行したい処理とその引数に分解します
  3. EvaluateMessage(string message)でばらした情報を元にリフレクションで実行します.ここの処理については,Unity APIはMain Thread以外からは呼び出せないため前後でThreadを切り替えています.
  4. Client側から受け取った文字列が"exit"であればサーバーが終了します.
  5. 最初に戻ります

なおリフレクションについては以下のような形で文字列からクラス名,メソッド名,引数を指定して実行しています.

Type.GetType(typeName).GetMethod(methodName).Invoke(null, new object[] { argsRawStr });

cf. C#リフレクションTIPS 55連発 - Qiita

まとめ

Unityでの作業がつらかったので,UnityとHoudiniをSocketで接続し,HoudiniのROPからUnityの任意の処理を実行するしくみを作りました. ちょっと乱暴なタイトル伏線回収ですが,処理を走らせるたびにHoudiniからUnityのアセットを全部消して,再度Houdiniから生成処理を行えば,事実上,プロシージャルUnityプロジェクトの生成,みたいなこともできるなーと思います.

今後は以下のようなことができればいいなと考えてます.

  • Unity側での主要な操作を行うscriptの整備
  • Houdiniで,Unity側からの処理の返り値等の情報を受取るしくみの実装
  • Socketまわりの例外処理
  • TOPによるUnityを絡めたパイプライン構築