Maya の MeshSync を少し便利にする

# はじめに
これは [Maya Advent Calendar 2020](https://qiita.com/advent-calendar/2020/maya) 16日目の記事です。

小ネタかつ既知かもしれませんが、MeshSyncの導入や紹介の手引きをする上で気になっていた箇所がありましたのでまとめてみました。文中はかなりUnity色が強いです。ご了承ください。



## [MeshSync](https://github.com/unity3d-jp/MeshSync/blob/dev/Readme_JP.md) って何?
雑にまとめるとアーティスト作業にフォーカスしたプロセス間通信のUnity版です。アセットのクオリティはイテレーション回数に比例しますが、最近のスマホアプリ製作はUnity側で調整しながらアセット編集を行わずしてクオリティを上げることが出来ない要素が多分に増えたため、MeshSyncのようなリアルタイムプレビュー機能は必須です。

MeshSyncを用いた作業の導線を考えた時、
1. PlayMode中に修正したいオブジェクトを見つける
2. PrefabにあるFBXのパスを調べる
3. Mayaで上記のFBX(ma)を開いて、MeshSync関連コマンドの実行
の３ステップを踏む必要がアセット毎に毎回発生するので、これを1つにまとめたいと考えました。



## エディタ拡張スクリプト
Mayaの`userSetup.py`のような下準備を不要にしつつ、C#スクリプト1ファイルに機能をまとめてみます。一部ハードコードしている部分がありますので、ご自身の環境に読み替えてください。

ポイントとして以下の条件で処理を分岐させています。

- Mayaが起動していない
  - コマンドライン引数にMELを渡して起動＋更新
- Mayaが起動している
  - `commandport`に直接MELを渡して更新

余談として、MeshSyncコンポーネントのプロパティをMayaに送りたいのですが、それらメンバーは`internal`です。

アクセスする方法がなくもないのですが、それらを含めるとアドカレ内容の範囲を逸脱してUnityの方に書かないといけなくなりそうなので、ここでは省略します。

```c#
using System.Text;
using System.Diagnostics;
using System.Net.Sockets;
using UnityEditor;
using UnityEngine;

public class RemoteControll
{
    public static void Send(dynamic request)
    {
        using (var client = new TcpClient("127.0.0.1", 7001))
        {
            var data = Encoding.UTF8.GetBytes(request);
            using (var stream = client.GetStream())
            {
                stream.Write(data, 0, data.Length);
            }

            client.Dispose();
        }
    }
}

[InitializeOnLoad]
public static class SceneViewCameraTool
{
    static SceneViewCameraTool()
    {
        SceneView.onSceneGUIDelegate += (sceneView) =>
        {
            Handles.BeginGUI();
            using (new GUILayout.VerticalScope(GUI.skin.box, GUILayout.Width(100)))
            {
                GUILayout.Label("Tool");

                if (GUILayout.Button("MeshSync", GUILayout.Width(100)))
                {
                    var app = "/Applications/Autodesk/maya2017/Maya.app/Contents/bin/maya";
                    var port = "if(`commandPort -q \\\":7001\\\"` == false) commandPort -name \\\":7001\\\";";
                    var cmd = "UnityMeshSync_Export;";

                    if (Process.GetProcessesByName("maya").Length > 0)
                    {
                        RemoteControll.Send(
                            cmd +
                            $"viewPlace -p -an true -eye {sceneView.camera.transform.position.x} {sceneView.camera.transform.position.y} {sceneView.camera.transform.position.z} -fov {sceneView.camera.fieldOfView} `lookThru -q`;");
                        return;
                    }

                    var selection = Selection.activeGameObject;
                    if (selection != null)
                    {
                        var meshsync = GameObject.Find("MeshSyncServer");
                        if (meshsync == null)
                        {
                            EditorApplication.ExecuteMenuItem("GameObject/MeshSync/Create Server");
                            Selection.activeGameObject = selection;
                        }

                        var fbx = AssetDatabase.GetAssetPath(
                            PrefabUtility.GetCorrespondingObjectFromOriginalSource(selection));
                        fbx = Application.dataPath.Replace("Assets", "") + fbx;

                        var setup = $"evalDeferred \\\"UnityMeshSync_Settings -p 8080; {cmd}\\\" -lp;";
                        var args = $"-file \"{fbx}\" -hideConsole -nosplash -command \"{port} {setup}\"";

                        var process = new Process
                        {
                            StartInfo =
                            {
                                FileName = app, Arguments = args,
                                WindowStyle = ProcessWindowStyle.Hidden | ProcessWindowStyle.Minimized,
                                CreateNoWindow = true
                            }
                        };
                        process.Start();
                    }
                }
            }

            Handles.EndGUI();
        };
    }
}
```
普段はデュアルディスプレイで作業されている方も多いと思いますが、キャプチャの都合上1画面で収録しました。SceneView上での拡張に関しては完全に個人的な好みです。

![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48607/9b55d807-76a6-2cb2-8f91-90f2303b04dd.gif)

##### 補足事項
`UnityMeshSync_`~ コマンドはpluginがロードされた後になるので、`evalDeferred`で遅延実行します。

`viewPlace`コマンドはUnity側の画角を再現してみるために、おまけ程度に使ってみました。

Prefabから直接FBXのパスを参照したりしていますが、実際のところ各社各プロジェクトによって対象を書き換える必要があるかと思います。


### おわりに
内製エンジン環境だと（気の利く社内の開発者が作り込んでくれているので）あまり気にしていなかったことも、商用エンジンはそういう痒いところを自分で作って便利にするか、札束で叩いたアセットで解決するしかないです。

細かい処理でもめんどくさがらずにやっておくと、時間短縮になって後が楽になりますね。

---

#### TODO
1. Cameraの同期
2. Material参照
3. Component付替