はじめに
この記事はQualiArts Advent Calendar 2020の18日目の記事になります。
去年のアドカレでは「テクニカルアーティストが3Dのワークフローを改善した話」と題してQualiArtsにおけるテクニカルアーティスト(TA)の業務に関してお話しましたが、今年なんと新たに「テクニカルアーティスト(TA)室」という組織が新設されました。
20年度入社の新卒TAも生まれ、TA組織の規模と業務範囲を広げながら、引き続きプロジェクト横断で活動しています。
このTA室の業務の一環として、Unity公式で開発されている「Python for Unity」について調査したので、本稿ではこちらを紹介します。
PythonはMaya、Houdini、Blenderなど多くの3Dツールで使用でき、事実上TA、TD(テクニカルディレクター)の標準言語となっています。
「Python for Unity」もこの流れに則ってパイプラインTAのために開発されています。
(UE4も同様の理由でプラグインがリリースされていますね)
そのため、実際のゲームにおいてPythonスクリプトによる制御をすることはスコープに入っていません。
Python for Unityをインストールする
Package Managerでインストール…と言いたいところですが、まだ出てこないので、導入したいUnityプロジェクト直下のPackagesディレクトリにあるmanifest.jsonを開き、dependenciesに以下を追記します。
{
"dependencies": {
(略)
"com.unity.scripting.python": "2.1.1-preview.1"
}
}
追記して保存したらUnityを開けばWindow>Generalに「Python Console」が追加されます。
また、PackageManagerにも表示されるようになり、こちらからはサンプルをインポートすることもできます。
次に使用するPythonの実行ファイルを指定します。
Edit>Project Settings>Python for Unityを選択
「…」をクリックしてpythonの実行ファイルを指定します。
Mayaを使用している場合、「mayapy.exe」を指定すれば楽してPySideも使用可能になります。
詳しくは以下をご覧ください。
Python for Unity - Installation
あとは指示に従ってUnityを再起動すればインストールは完了です。
Python Consoleを使ってみる
上部がログ表示エリア、下部がコード記述エリアになります。
import UnityEditor
import UnityEngine
guids = UnityEditor.AssetDatabase.FindAssets("t:prefab", None)
for guid in guids:
path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid)
print(str(path))
UnityEngine.Debug.Log(path)
このようなコードを書いて「Execute」を実行、もしくはCtrl+Enterをすると…
Python Consoleの上部のログ表示エリアとConsoleにログが表示されました。
この通り、UnityEditorやUnityEngineというモジュールをimportすることで、普段書いているC#のコードをPythonに置き換えてUnityを操作することができます。
次にこれをPythonを書けない人でも簡単に実行できるようにしましょう。
Python Consoleで書いたコードを簡単に実行できるようにする
Python Console上部の「Save & Create Shortcut」をクリックします。
ダイアログが表示されるので、メニューバーに表示するラベルを「/」区切りで記述します。
スクリプトの保存場所を選択するダイアログが表示されるので、適当な場所を指定して完了です。
メニューバーに指定したラベルでメニューが追加されました。かんたんですね!
さて、これはどのように実現しているのでしょうか。
保存したディレクトリを見てみると…
保存したPythonファイルと同名でC#ファイルが作成されています。
中身を見てみると…
using UnityEditor;
using UnityEditor.Scripting.Python;
public class MenuItem_SearchPrefabs_Class
{
[MenuItem("Python Scripts/SearchPrefabs")]
public static void SearchPrefabs()
{
PythonRunner.RunFile("Assets/Scripts/search_prefabs.py");
}
};
普通にエディタ拡張でメニューを作るのと同様に「MenuItem」を使用したコードが自動生成されています。
作成したpythonファイルは「PythonRunner.RunFile」でパスを指定する形で実行されていることがわかります。
というわけで、Python Consoleから保存せずとも、自身でお好みのエディタでPythonスクリプトを作成し、それを「PythonRunner.RunFile」を用いて実行することで同様の挙動が得られそうですね!
PySide Camera Selectorを読んでみる
Package Managerの「PySide Camera Selector」の「Import into Project」をクリックすると、PySideのサンプルがImportされます。
ImportされるファイルはAssemblyDefinition、C#スクリプト、Pythonスクリプト、PySide用UIファイルです。
Python>Examples>PySide Exampleから起動できます。
挙動としては以下のような感じです。
- Hierarchyに存在するカメラをリスト表示
- 「Use Camera」をクリックするとリストでアクティブなカメラを選択し、「GameObject/Align View to Selected」を実行
- Hierarchyの変更を検知してリストを自動更新
PySideExample.cs
[MenuItem("Python/Examples/PySide Example")]
public static void OnMenuClick()
{
PythonRunner.SpawnClient(
file: $"{__DIR__()}/PySideExample.py",
wantLogging: true);
}
こんな感じでPythonを実行しています。
先程と違い、SpawnClientを使用することで新しいPythonインタプリタを作成します。
なので、ファイルパスもプロジェクトルートからの相対パスではなくフルパスになっています。
引数などに関しては以下をご参照ください。
SpawnClient
このスクリプトのもう1つの役割はイベントのハンドリングです。
static public void Subscribe()
{
EditorApplication.hierarchyChanged -= OnHierarchyChanged;
EditorApplication.hierarchyChanged += OnHierarchyChanged;
}
public static void OnHierarchyChanged()
{
PythonRunner.CallAsyncServiceOnClient("PySide Example", "on_hierarchy_changed");
}
hierarchyChangedにこれをフックすることで、Python側のコードを実行しています。
第一引数はクライアントの名前、第二引数はサービスの名前になるのですが、詳しくはこのあとで説明します。
PySideExample.py
Python側には以下の2つのクラスがあります。
- PySideTestClientService
- PySideTestUI
Unityとの連携部分以外はPySideを利用した見慣れた感じの構成です。
Unityとの連携には以下のモジュールを使用します。
import unity_python.client.unity_client as unity_client
基本的には unity_client.UnityClientService
を継承したクラスを作成し、このクラスでUnityと接続してやりとりを行う形になります。
PySideTestClientService
というわけで unity_client.UnityClientService
を継承したこのクラスを見ていきます。
def exposed_client_name(self):
return "PySide Example"
def exposed_on_hierarchy_changed(self):
_PYSIDE_UI.populate_camera_list()
先頭にこんな感じのコードが記述されていますが、「PySideExample.cs」のコードを改めて見てみましょう。
PythonRunner.CallAsyncServiceOnClient("PySide Example", "on_hierarchy_changed");
CallAsyncServiceOnClient
の第一引数であるクライアント名は exposed_client_name
が返す値に相当します。
したがって、docstringにも書いてありますが、この値は他と被らない一意なものを指定する必要があります。
次にCallAsyncServiceOnClient
の第二引数ですが、こちらに対して「exposed_」接頭辞をつけたメソッドが実行されます。
したがって、 on_hierarchy_changed
を指定した場合、このサンプルの通り exposed_on_hierarchy_changed
を実装する形になります。
PySideTestUI
これはいわずもがな、UIを制御するクラスです。
PySideに関しては本題から逸れるので特に触れず、Unity連携部分を見ていきます。
ざっと読んでいくとUnity側のコード実行の方法が大きく2パターンあることがわかります。
まずパターン1、serviceを使用する方法です。
camera = self.service.UnityEngine.GameObject.Find('{}'.format(selected_items[0].text()))
このserviceは上の PySideTestClientService
のインスタンスです。
これを経由してUnityEngineやUnityEditorの機能を使用することができます。
次にパターン2、connectionを使用する方法です。
self.connection.root.execute(inspect.cleandoc(
"""
import UnityEditor
UnityEditor.EditorApplication.ExecuteMenuItem('GameObject/Align View to Selected')
"""))
Python Consoleと同じ書き方でコードを埋め込んで実行する形になります。
コメントを見る限り、serviceを使用する方法よりこちらのほうが高速で動作するようです。
第2引数を指定すれば指定コード内で使用した変数を辞書で取得することができます。
vars = self.connection.root.dict()
self.connection.root.execute(inspect.cleandoc(
"""
import UnityEngine
cameras = [x.name for x in UnityEngine.Camera.allCameras]
"""), vars)
camera_list = vars["cameras"]
ちなみにC#側のSubscribeメソッドもconnection経由でpythonから実行しています。
PySide Camera Selectorまとめ
というわけで、公式のPySideのサンプルは
- メニューバーから実行するとC#でPythonを実行
- 実行されたPythonの中でC#のSubscribeメソッドを実行
- Unityのイベント発行時にPythonのメソッドを実行
というような流れになっていることがわかりました。
Unity側からの実行とUnityのイベント発行時のPython実行が不要あれば、Unity側に特にコードを用意する必要もなさそうです。
MayaからUnityの情報にアクセスしてみる
なんとなく構成がわかったところでMayaからUnityの情報にアクセスする方法について考えてみます。
unity_pythonにパスを通す
早速コードを書いていきたいところですが、unity_pythonモジュールはどこにあるんでしょうか。
調べてみると、どうやらPython for Unityを導入したプロジェクトの Library/PackageCache
以下にあるようです。
プロジェクトが C:/Users/{ユーザー名}/Documents/dev/unity/python-test
で com.unity.scripting.python@2.1.1-preview.1
を導入した場合、具体的には以下になります。
C:/Users/{ユーザー名}/Documents/dev/unity/python-test/Library/PackageCache/com.unity.scripting.python@2.1.1-preview.1/Python~/site-packages
これをIDEのパスに追加することである程度のオートコンプリートもきいて書きやすくなりました。
参考:Python for Unity first impression and VSCode setup.
コードを書いていく
import unity_python.client.unity_client as unity_client
class MayaUnityClientService(unity_client.UnityClientService):
def exposed_client_name(self):
# 一意な名前を返す
return "MayaUnityClient"
def search_cameras(self):
cameras = [x.name for x in service.UnityEngine.Camera.allCameras]
print(cameras)
service = MayaUnityClientService()
client = unity_client.connect(service)
service.search_cameras()
# 終わったら切断
client.close()
前述の通り UnityClientService
を継承したクラスを用意し、 exposed_client_name
で一意な名前を返します。
search_cameras
というHierarchy上に存在するカメラを取得して表示するだけのメソッドを用意。
あとはこのインスタンスを生成してUnityに接続して search_cameras
を実行したのち切断するだけのかんたんな実装です。
Mayaで実行する
Mayaで実行する際ももちろんunity_pythonにパスを通す必要があります。
既に何らかの自前のツールがある場合はそちらでパスを通すようにしてもよいですが、今回は手っ取り早くスクリプトエディタで実行する形にします。
というわけで以下のようなコードを用意してパスを通します。
import sys
script_path = "C:/Users/{ユーザー名}/Documents/dev/unity/python-test/Library/PackageCache/com.unity.scripting.python@2.1.1-preview.1/Python~/site-packages"
if script_path not in sys.path:
sys.path.insert(0, script_path)
その上で先程用意したコードを実行すればMayaからUnityの情報にアクセスすることができます。
無事 [u'Main Camera', u'UICamera3', u'Camera']
と表示されました!
おわり
まだExperimentalではありますが、UnityをPythonで操作することができました。
今回はMayaからUnityの情報にアクセスする方法を紹介しましたが、mayapyを使用することでその逆も可能です。
UnityからMayaシーンにアクセスして再エクスポートの実行などもできそうで、夢が広がりますね!
また、Unityは標準で小さなコードを実行する術がスクリプトファイルの共有以外にないので、Python Consoleによってコードを各々の環境でコピペすることで簡単にコードが実行できるというのも嬉しいシーンがありそうです。
Python for Unityに関する情報は以下のフォーラムをご覧ください。
External Tools Previews
Python for Unityを活用したShotgunとの連携を行うプラグインに関するスレッドもあるので、非常に実装の参考になると思います。
正式リリースに期待しつつ、引き続き活用方法を考えていく予定なので、また知見がたまったら共有できればと思います。