20.3K Views
November 14, 24
スライド概要
本講演へのアンケートにご協力をお願いします:
https://docs.google.com/forms/d/e/1FAIpQLSeeq3O53ausrSoHXHluKskiabOlucYgXBajAhVQ2IbL__Km7Q/viewform
Youtube URL:
講演内容:
普段DCCツールをサポートしているテクニカルアーティストの方向けにUEでPythonを活用し、ツール作成からリリースまでの一連の流れをご紹介します。
ツール作成には、アセットをUEへシームレスにインポートするアセットエクスポーターを例にUEにおける基本的な全アセットのインポート設定や手法を説明し、リリースに必要なGUIやメニュー追加、テスト方法などについても解説します。
講演者:
山城 拓巳(デザイナー部テクニカルアーティストチーム シニア(株式会社Cygames))
Cygamesについてはこちら:
https://www.cygames.co.jp/
UNREAL FEST 2024 TOKYO 公式サイト:
https://unrealengine.jp/unrealfest/2024/
Unreal Engineを開発・提供しているエピック ゲームズ ジャパンによる公式アカウントです。 勉強会や配信などで行った講演資料を公開しています。 公式サイトはこちら https://www.unrealengine.com/ja/
テクニカルアーティスト向け Unreal Python スタート ガイド 株式会社 Cygames デザイナー部 テクニカルアーティストチーム シニア 山城 拓巳
自己紹介 株式会社 Cygames テクニカルアーティストチーム/シニア 山城 拓巳 ● アーティスト向けのDCCツールやゲームエンジ ン向けのツール開発を担当。 CEDEDC 2022 ツール保守コスト大幅削減!テクニカルアーティストによ るツールログサービスの開発と運用事例 2/96
このセッションの環境 ● Windows 11 ● Unreal Engine 5.4 ● Pythonバージョンは 3.11 ※ 内容的にはUnreal Engine5系であれば内容に細かいバージョン縛りはなし ● DCC ● Maya 2023 ● Substance 3DPainter 10.1 3/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 4/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 5/96
Unreal Pythonの導入方法 ● プラグインを有効化するだけ ● 最近はデフォルトでOn 6/96
Unreal Python 導入後追加されるもの 1. 出力ログにPythonのコード実行とREPL(Read-Eval-Print Loop)が追加 REPL(Read-Eval-Print Loop) 7/96
Unreal Python 導入後追加されるもの 2. コマンドレット実行にPython実行用のコマンドレットが追加 (コマンドレット・・Unreal Engineをコマンドライン実行するもの) エディター起動あり UnrealEditor-Cmd.exe “uprojectパス” –ExecutePythonScript=“Pythonパス” エディター起動なし UnrealEditor-Cmd.exe "uprojectパス" -run=pythonscript -script="Pythonパス” 8/96
Unreal Pythonの Hello World ● UE内で実行可能な「unreal」モジュールが存在 import unreal # Mayaでいうcmdsみたいなもの unreal.log("Hello World!") 9/96
Unreal Pythonの設定(型情報) ● エディター環境設定のDeveloper モード この機能がOnになってるとそのプロジェクト内で利用可能な関数の型情報を生成してくれる プロジェクトフォルダ/Intermediate/PythonStub/unreal.py 10/96
Unreal Pythonの仕組み Blueprint向けに公開された関数がPythonにも「ほぼ」公開される 共通 ⇐ 共通 前述したデベロッパーモードをOnにして生成された型情報の中でも関数を確認できる。 この仕組みのおかげで、C++経由で足りない機能なども追加が可能。 11/96
Unreal Pythonの設定(予約パス) ● プロジェクトフォルダ/Content/Python ● Unreal Engineフォルダ/Content/Python ● 有効にした各プラグインフォルダ/Content/Python ● Documents/UnrealEngine/Pythonフォルダ ● 例) C:/Users/Username/Documents/UnrealEngine/Python 12/96
Unreal Pythonの設定(追加パス) ● プロジェクト設定のPython Plugin設定画面から追加可能。 任意のフォルダにpip install先を作ってそのパスを通せば、pipライブラリも読み込める。 上記はContentと同階層にsite-packagesフォルダを作った例。 13/96
Unreal Pythonの設定(スタートアップ) ● init_unreal.pyという名前で予約パス、追加パスに配置すると起動時実行可能。 14/96
Unreal Pythonの設定(スタートアップ) ● 任意のスタートアップファイル指定も可能。 15/96
Unreal Pythonの設定(ブラウザ統合) ● この講演では便宜上、「コンテンツブラウザ統合を有効化」をOnにする。 この設定を行うとPythonファイルをコンテンツブラウザで表示して、実行できる。 16/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 17/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 2.1. スタティックメッシュ 2.2. スケルタルメッシュ 2.3. アニメーションシーケンス 2.4. テクスチャ 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 18/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 2.1. スタティックメッシュ ← 一番簡単なものから 2.2. スケルタルメッシュ 2.3. アニメーションシーケンス 2.4. テクスチャ 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 19/96
スタティックメッシュの手動手順整理 ● DCCからFBXを書き出す ===== UE ===== ● FBX をドラッグする ● インポート設定を編集する ● 「インポート」を押す 20/96
スタティックメッシュの手動手順を整理 ● DCCからFBXを書き出す ● FBX をドラッグする ● インポート設定を編集する 3つの工程をまとめて自動化 ● 「インポート」を押す ※ DCCからFbxを書き出す部分はDCC側なので、割愛 21/96
ここまでのデモ
3工程をまとめて自動化する import unreal task = unreal.AssetImportTask() source_file_path = "インポートしたいFBX パス" ue_file_dir = "インポートしたいUEファイルルート" # ダイアログ非表示 task.set_editor_property("automated", True) task.set_editor_property("filename", source_file_path) task.set_editor_property("destination_path", ue_file_dir) # 上書き許可 task.set_editor_property("replace_existing", True) # 完了後、保存する task.set_editor_property("save", True) ⇐ FBXをドラッグする # FBX設定 次ページで説明 import_ui = unreal.FbxImportUI() task.options = import_ui ⇐インポート設定 # 実行する unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) ⇐「インポート」を押す 23/96
FBX Import UIを設定する import_ui = unreal.FbxImportUI() Import_ui.reset_to_default() import_ui.import_mesh = True # UEの中にどのタイプでインポートするか Import_ui.mesh_type_to_import = unreal.FBXImportType.FBXIT_STATIC_MESH # FBXのオリジナルタイプ。スタティックメッシュタイプを強制的にスケルタルメッシュにできたりできる Import_ui.original_import_type = unreal.FBXImportType.FBXIT_STATIC_MESH # FBXIT_STATIC_MESH # アニメーションのインポート有無 Import_ui.import_animations = False # スケルトンのインポート有無 Import_ui.import_as_skeletal = False # マテリアルのインポート有無 import_ui.import_materials = False # テクスチャのインポート有無 import_ui.import_textures = False # メッシュのインポート有無 import_ui.import_mesh = True 24/96
より詳細な設定については・・ ● unreal.pyのFbxImportUIを参照 スタティックメッシュの詳細設定はFbxStaticMeshImportDataを指定したり、 スケルタルメッシュではFbxSkeletalMeshImportDataを指定したりする。 各アセットごとの詳細設定ごとに設定クラスが分かれて指定する形式 25/96
現時点での気になる点 1. Asset Import Taskで取得する置き場所が決まっている import unreal task = unreal.AssetImportTask() source_file_path = “ここがDCCの実行と分離されてるので、取り扱いしづらい" ue_file_dir = "インポートしたいUEファイルルート" 2. 実行が2段階になっている 1. DCCでFbx書き出す 2. UEでAssetImportTaskを呼び出し、インポートする 26/96
DCCからリモートで実行可能
Remote 設定 ● Python Pluginにはリモート実行可能な設定が存在 28/96
Remote 接続実装 ● Python Pluginのフォルダ内にRemote実行のサンプルファイルが存在 PythonScriptPlugin¥Content¥Python¥remote_execution.py 29/96
remote_execution.pyを利用する ● Unreal Engine外から以下を送信する import time from remote_execution import RemoteExecution remote_execute = RemoteExecution() remote_execute.start() ⇐ サンプルファイルのクラス リモート実行セッションを開始する # 即確認すると接続される前に完了してしまうため、1秒待機する。 time.sleep(1) # UEが存在したら、一番最初に見つけたUEで実行する if remote_execute.remote_nodes: remote_execute.open_command_connection(remote_execute.remote_nodes[0]) remote_execute.run_command(“sample_call.py unreal_fest 2024”) remote_execute.stop() sample_call.pyをunreal_fest, 2024という引数で実行する 30/96
実行先のsample_call.pyと実行結果 Unreal側で実行するsample_call.py import sys import unreal unreal.log(f"Hello, {sys.argv}") リモートで実行された結果 31/96
これらを踏まえ、 スタティックメッシュ書き出しへ
DCC側から呼び出すコード
import time
from remote_execution import RemoteExecution
使いやすい様にクラス化する
class UERemoteExecution:
@classmethod
def execute_file(cls, file_path: str, *args) -> None | dict:
remote_execute = RemoteExecution()
remote_execute.start()
time.sleep(1)
if remote_execute.remote_nodes:
remote_execute.open_command_connection(remote_execute.remote_nodes[0])
remote_execute.run_command(f"{file_path} {" ".join(args)}")
remote_execute.stop()
UERemoteExecution.execute_file(“import_static_mesh.py”, “FBXパス“, “UEディレクトリ”)
インポート実装コードを指定
書きだしたFbxパスとUEディ
レクトリを引数にする
33/96
UEで実行するimport_static_mesh.py import unreal import sys fbx_path = sys.argv[1] ue_dir = sys.argv[2] ⇐ 変更点。定数から引数で来る想定に変更。 task = unreal.AssetImportTask() task.set_editor_property('automated', True) task.set_editor_property('filename', f'{fbx_path}') task.set_editor_property('destination_path', f'{ue_dir}') task.set_editor_property('save', True) import_ui = unreal.FbxImportUI() # スタティックメッシュに施したい設定を追加する task.option = import_ui unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) 34/96
改めてデモ
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 2.1. スタティックメッシュ 2.2. スケルタルメッシュ 2.3. アニメーションシーケンス 2.4. テクスチャ 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 36/96
スタティックメッシュとの相違点 ● スケルトンという概念がある 新規で作成したり、既存のスケルトンで共有したり 逆にエクスポーターで気を付ける点はほぼこれだけ 37/96
デモ
DCC側から呼び出すコード ● 先ほど紹介した UERemoteExecution クラスをそのまま利用する ● 実行ファイルをImport_skeletal_mesh.pyに変更 UERemoteExecution.execute_file(“import_skeletal_mesh.py”, ⇐ スケルタルメッシュ “FBXパス”, インポートコードに変更 “UEディレクトリ”, “スケルトンパス”) スケルトンパスを追加 39/96
UEで実行する「import_skeletal_mesh.py」 import unreal import sys fbx_path = sys.argv[1] ue_dir = sys.argv[2] skeleton_path = sys.argv[3] task = unreal.AssetImportTask() # タスクの設定は前述してるので省略 import_ui = unreal.FbxImportUI() # FbxImport設定は前述してるので省略 ⇐ 追加点。スケルトンパスを指定する。 例) /Game/Meshes/sample_skeletal_mesh_Skeleton スケルトン設定 loaded_skeleton = unreal.load_asset(skeleton_path) # Game/**の形式 if loaded_skeleton: # 存在してるならそれを利用する import_ui.import_as_skeletal = False import_ui.skeleton = loaded_skeleton else: # ない場合は新規作成 import_ui.import_as_skeletal = True unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) 40/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 2.1. スタティックメッシュ 2.2. スケルタルメッシュ 2.3. アニメーションシーケンス 2.4. テクスチャ 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 41/96
スケルタルメッシュとの相違点 ● スケルトン指定が必須 アニメーションシーケンスには骨がないとエラーになる。 42/96
デモ ● TODO: モーションビルダーからUEへのデモ
UERemoteExecution の変更内容
● 骨がない場合は、明示的に失敗させたいので、リザルトを返す
class UERemoteExecution:
@classmethod
def execute_file(cls, file_path: Path, *args) -> dict:
remote_execute = RemoteExecution()
remote_execute.start()
time.sleep(1)
戻り値を追加
res = None # 追加
if remote_execute.remote_nodes:
remote_execute.open_command_connection(remote_execute.remote_nodes[0])
res = remote_execute.run_command(f”{file_path.as_posix()} {“ “.join(args)}”)
remote_execute.stop()
return res # 追加
44/96
DCC側からの実行コード ● 骨が必須という関係で、見つからない場合は、DCC側で通知したい ● execute_fileに結果情報を返す機能を追記する from remote import UERemoteExecution res = UERemoteExecution.execute_file( “import_animation.py”, “FBXパス”, “UEディレクトリ”, “スケルトンパス” ) if res["success"] != True: pass ⇐ ここでDCC側でダイアログを出し、気づかせる 45/96
UEで実行するimport_animation.py import unreal import sys fbx_path = sys.argv[1] ue_dir = sys.argv[2] skeleton_path = sys.argv[3] # Task設定は前述してるので省略 task = unreal.AssetImportTask() # FbxImport設定は前述してるので省略 import_ui = unreal.FbxImportUI() loaded_skeleton = unreal.load_asset(skeleton_path) # Game/**の形式 if loaded_skeleton: import_ui.import_as_skeletal = False import_ui.skeleton = loaded_skeleton else: raise ValueError("Skeleton not found.") ⇐ 変更点。ない場合はエラーにする unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) 46/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 2.1. スタティックメッシュ 2.2. スケルタルメッシュ 2.3. アニメーションシーケンス 2.4. テクスチャ 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 47/96
今までのアセットとの違い ● インポート時にダイアログが表示されない 48/96
今までのアセットとの違い ● テクスチャの種類によって設定が変わる Albedo, Normalで圧縮法設定が変わったりする 49/96
相違点に対する対応方針 ● インポート時にダイアログが表示されない ● テクスチャをインポート後に後から設定を変更する ● テクスチャの種類によって設定が変わる ● テクスチャによって設定がサフィックスで設定を分岐する ● アルベドなら_D、ノーマルなら_Nなど 50/96
デモ ● TODO: Substance Painterからリモート実行するデモ
DCC側からの実行コード ● 今まで利用していたUERemoteExecutionを利用する ● 実行ファイルをimport_texture.pyに変更 from remote import UERemoteExecution UERemoteExecution.execute_file(“import_texture.py”, “テクスチャパス”, “UEディレクトリ”) 変更 変更 52/96
UEで実行するimport_texture.py ● FbxImportUIの設定を削除 import unreal Import sys fbx_path = sys.argv[1] Ue_dir = sys.argv[2] task = unreal.AssetImportTask() Task.set_editor_property(‘automated’, True) Task.set_editor_property(‘filename’, f’{fbx_path}’) Task.set_editor_property(‘destination_path’, f’{ue_dir}’) Task.set_editor_property(‘save', True) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) 次のページでテクスチャの設定変更コードに続く 53/96
UEで実行するimport_texture.py # unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) インポートしたテクスチャを取得する texture_path = task.imported_object_paths[0] asset_registry = unreal.AssetRegistryHelpers.get_asset_registry() texture = asset_registry.get_asset_by_object_path(texture_path).get_asset() # テクスチャグループを設定 texture.lod_group = unreal.TextureGroup.TEXTUREGROUP_CHARACTER texture_name = texture.get_name() サフィックスで設定変更する if texture_name.endswith("_D"): texture.srgb = True elif texture_name.endswith("_N"): texture.srgb = False texture.compression_settings = unreal.TextureCompressionSettings.TC_NORMALMAP # 変更点あれば保存 unreal.EditorAssetLibrary.save_loaded_asset(texture, only_if_is_dirty=True) 54/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 55/96
オススメのGUIは2つ ● PySide6 ● Mayaなどでよく使われている ● QtをPythonで使用できる様にしたもの。 ● Qt自体はC++製 ● Editor Utility Widget (EUW) ● UE標準のEditor用GUI機能 ● 普段UEでUI作る時と同じエディターを利 用できる 56/96
オススメのGUIは2つ ● PySide6 ● Mayaなどでよく使われている ● QtをPythonで使用できる様にしたもの。 ● Qt自体はC++製 ● Editor Utility Widget (EUW) ● UE標準のEditor用GUI機能 ● 普段UEでUI作る時と同じエディターを利 用できる 57/96
PySide6をUEで利用するまでの流れ ● site-packagesでPySide6を追加する ● python –m pip install PySide6 –t “UEが確認できるsite-packagesフォルダ” ● Python Plugin設定の追加パスにsite-packagesを追加する ● 導入完了 58/96
基本的なQMainWindowのサンプル import sys from PySide6 import QtWidgets class SampleWindow(QtWidgets.QMainWindow): def __init__(self): super(SampleWindow, self).__init__() self.setWindowTitle("Sample Window") self.resize(640, 480) layout = QtWidgets.QVBoxLayout() # ボタンを作成 self.button = QtWidgets.QPushButton("Print Hello") layout.addWidget(self.button) central_widget = QtWidgets.QWidget() central_widget.setLayout(layout) self.setCentralWidget(central_widget) self.button.clicked.connect(self.print_hello) def print_hello(self): print("Hello") サンプルで表示されるWindow 59/96
UIを表示する import unreal import sys from sample import SampleWindow if not QtWidgets.QApplication.instance(): app = QtWidgets.QApplication(sys.argv) else: app = QtWidgets.QApplication.instance() window = SampleWindow() window.show() 裏側対策 unreal.parent_external_window_to_slate( window.winId(), unreal.SlateParentWindowSearchMethod.ACTIVE_WINDOW ) 60/96
PySide6デモ
オススメのGUIは2つ ● PySide6 (PySide2) ● Mayaなどでよく使われている ● QtをPythonで使用できる様にしたもの。 ● Qt自体はC++製 ● Editor Utility Widget (EUW) ● UE標準のEditor用GUI機能 ● 普段UEでUI作る時と同じエディターを利 用できる 62/96
Editor Utility Widget (EUW) ● 通常はUIにBP側のイベントをcallbackとして登録して利用する。 ● 今回はPython経由で全てバインドする方式でご紹介する。 ● EUWをViewとして分離でき、色々と都合がよい。 63/96
EUWデモ
EUW表示のコード紹介 import unreal EUW_PATH = “EUWまでのパス" class EUWSample: @classmethod def launch(cls, *args, **kwargs): euw = unreal.load_asset(EUW_PATH) eus = unreal.get_editor_subsystem(unreal.EditorUtilitySubsystem) euw_instance = eus.spawn_and_register_tab(euw) EUWを生成・表示 ボタンを取得し、関数をバインドする button: unreal.Button = euw_instance.get_editor_property("sample_call") button.on_clicked.add_callable_unique(cls.clicked_sample) @classmethod def clicked_sample(cls): unreal.log("Hello") EUWSample.launch() 65/96
EUWの注意点 ● Level再読み込み時にバインド先が消える ● EUWが再生成される仕様のため ● Level開いた時に再度接続しなおすコードが必要 66/96
GUI比較 ● PySide6 (PySide2) ● メリット ● DCCで培ったGUI資源を利用できる ● 全部Pythonで完結できる ● デメリット ● テーマをUEと揃えるのは難しい ● 非公式で存在 ● ドッキングできない ● Editor Utility Widget (EUW) ● メリット ● テーマが標準機能と揃う ● ドッキング可能で、UE ライクな挙動に なる ● デメリット ● 既存のWidget資源が使えない ● Level を開きなおすとPythonとの連携 が切れる 67/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 68/96
メニュー周りの用語 Tool Menus メニュー全体を管理するクラス Tool Menu 各メニューの場所ごとの管理クラス Menu 各メニュー。Sectionを持つ Section 各Menu Entryごとのグループ。Menu Entryをもつ Menu Entry メニュー項目。ここにPythonの実行コマンドとかEUWの表示を追加する 69/96
メニュー周りの仕組みについて 厳密ではないが、既存のメニューに当てはめてみる Tool Menus Tool Menu Menu Section Menu Entry Menu Entry 70/96
デモ ● TODO: 実際の動作デモ
メニュー追加の実装 ● メニュー定義用JSONファイルと実装コードに分ける ● JSONに分けておくと、簡単にメニューを追加しやすい ● 実装コード側だけ各プロジェクトで共通化しやすい メニュー定義(JSON) メニュー実装(python) 72/96
メニュー定義用JSONファイルを用意する { } “name”: “カスタムメニュー”, ⇐ メニュー名 “menus”: [ { Menu "label": "サンプルメニュー1",⇐ サブメニュー名 "type": "submenu", "children": [ { "label": "サンプルセクション1", Section "type": "section", "children": [ { Menu Entry "label": "ツール1", "type": "python", "annotation": "アノテーション1", "command": "print('call tool1')" }, { "label": "ツール2", "type": "widget", "annotation": "アノテーション2", "command": "print('call tool2')" } ] } ] } ] Jsonを読み込むと上記ができる 73/96
メニュー実装(メニューを作成する) def launch_menu(): MENU_PATH = os.path.join(SCRIPT_DIR, "menu.json") JSONをロードする json_data = None with open(MENU_PATH, "r", encoding="utf-8") as f: json_data = json.load(f) # メニューの管理クラス取得 tool_menus: unreal.ToolMenus = unreal.ToolMenus.get() ⇐ メニュー全体を管理するクラスを取得する # メインメニューの上部を取得 main_menu = tool_menus.find_menu("LevelEditor.MainMenu") ⇐ UEのトップメニューを取得する menu_name = json_data["name"] カスタムメニューを作成する custom_menu = main_menu.add_sub_menu( main_menu.get_name(), menu_name, menu_name, menu_name) 以下サブメニュー作成に続く 現状の状態イメージ 74/96
メニュー実装(メニューを作成する) サンプルメニュー1を作成する for sub_menu_data in json_data["menus"]: sub_menu_name = sub_menu_data["label"] custom_sub_menu = custom_menu.add_sub_menu( custom_menu.get_name(), sub_menu_name, sub_menu_name, sub_menu_name) サンプルセクション1を作成する for section_data in sub_menu_data["children"]: custom_sub_menu.add_section(section_data["label"], section_data["label"]) Menu Entryを作成する for menu_entry_data in section_data["children"]: add_entry(custom_sub_menu, menu_entry_data, section_data["label"]) # メニューリフレッシュ tool_menus.refresh_all_widgets() 現状の状態イメージ ⇒ 75/96
メニュー実装(SectionとMenu Entry) def add_entry(menu: unreal.ToolMenu, data: dict, include_session): name = data.get("label", "") menu_type = data.get("type") annotation = data.get("annotation", "") command = data.get("command", "") if menu_type == "python": entry = unreal.ToolMenuEntry( name=name, type=unreal.MultiBlockType.MENU_ENTRY, ) entry.set_label(name) entry.set_tool_tip(annotation) entry.set_string_command( type=unreal.ToolMenuStringCommandType.PYTHON, custom_type="", string=f"{command}", ) menu.add_menu_entry(include_session, entry) 現状の状態イメージ ⇒ Menu Entryの情報取得 Python 用Menu Entryを作成する 76/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 77/96
テストとは ● テストとは? ● 実装コードが正しいかどうか確認するための手法 ● 色々なテストあるが、今回はユニットテストのみを対象。 ● 機能テストとかホワイトボックステストとか色々ある。 78/96
どんな事する? ● 実装コードに対応した、テストコードを書く 実装コード テストコード class Sample: def increment(self, number: int) -> int: return number + 1 from sample import Sample class TestSample: def test_increment(self): result = Sample().increment(4) assert result == 5 このコードをテストライブラリで実行 コードが正しいかを確認する 79/96
テストを行う理由は? ● Unreal Engineのアップデート時に非常に恩恵がある。 ● エンジンアップデート時にUnreal Pythonの関数が結構変わってしまう。 ● ツールを実行するまでエラーがわからない状態になる ● 全ツールを動作確認すれば問題ないが現実的ではない テストコードを用意し、自動実行しておけば問題範囲をすぐ確認できる 80/96
やらなくてよいときもある? ● Pythonでツールを作る量が少なければ ● テストコードを書くコスト > 確認するコストになる ● 一般的にテストは色々なメリットがあると言われているが・・ ● 当然コストもかかる為、Unreal Pythonで開発 = 絶対やろうというわけではない 本講演では Unreal Engineアップデートにツール保守を迅速に対応させるという気持ち 81/96
利用するライブラリ ● Pythonのテストフレームワークとして有名なpytestを例に紹介 82/96
pytest導入方法 ● site-packagesでpytestを追加する ● python –m pip install pytest –t “UEが確認できるsite-packagesフォルダ” ● Python Plugin設定の追加パスにsite-packagesを追加する 83/96
pytestの構成 ● test-sample ● src ● __init__.py ● sample ・・・ モジュール ● __init__.py ● main.py ・・・ 実装 ● tests ● __init__.py ● test_main.py ・・・ テストコード srcフォルダへのパスを通しておく 84/96
実装とテストコード ● 先ほどのサンプルを利用する 実装コード(main.py) テストコード (test_main.py) class Sample: def increment(self, number: int) -> int: return number + 1 from sample import Sample class TestSample: def test_increment(self): result = Sample().increment(4) assert result == 5 85/96
テスト実行するexecute_test.py
● Unreal Engine上で実行できるようにコードを実装する
import pytest
from pathlib import Path
import sys
SCRIPT_DIR = Path(__file__).parent ⇐ srcフォルダパス
TEST_DIR = SCRIPT_DIR / “tests" ⇐ testsフォルダパス
hit_modules = []
for module_name in sys.modules.keys():
if module_name.startswith(“sample"):
hit_modules.append(module_name)
for module_name in hit_modules:
del sys.modules[module_name]
リロード対策
pytest.main([TEST_DIR.as_posix(), "-v", "-s", f"--rootdir={SCRIPT_DIR.as_posix()}"])
86/96
デモ ● Unreal Engine上でexecute_test.pyを実行する 87/96
わざとテストコードを失敗させてみる ● Unreal Engine上でexecute_test.pyを実行する from sample import Sample class TestSample: def test_increment(self): result = Sample().increment(4) assert result == 6 ⇐ 4をインクリメントしても絶対に6にならないので、エラーになる 88/96
Unreal Pythonでのテスト実行方法 ● コマンドラインでPythonコマンドレットを使用する ● Python Pluginを有効化すると追加されるコマンドライン実行機能 UnrealEditor-Cmd.exe “UProjectパス” -run=pythonscript -script=“execute_test.py“ -FullStdOutLogOutput ● JenkinsなどのCI環境向け 89/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 90/96
配布方法と配布物について ● Version Control管理のフォルダに配布物を配置し、同期する方式をとる techart Content ツール用 ┗ tools ┗ Python ┗ exporter ┗ init_unreal.py スタートアップ。Menu.pyのメニュー起動 ┗ __init__.py を追加しておく。 ┗ app.py ┗ menus メニュー ┗ menu.py ┗ menu.yaml テスト用 ┗ tests ┗ exporter ┗ test_app.py ┗ site-packages ⇐ 外部ライブラリ。Pysideやpytestなど 91/96
アジェンダ 1. Unreal Pythonの導入方法と各種設定 2. 基本アセットのエクスポーター作成 3. ツールに使うGUIについて 4. メニューへの追加 5. テスト実行について 6. 配布方法 7. まとめ 92/96
Unreal Pythonのメリット ● 自由度の高さ ● ほぼBlueprint関数が利用できるのでかなり自由にできることがわかる ● 豊富なライブラリによる、目的達成の速さ ● Python はパッケージマネージャーが広く普及しており、簡単に導入し、利用できる ● 学習難易度が低い = 利用者を増やしやすい ● C++クラスに詳しくなる ● Unreal PythonのAPIは実質的にC++のクラスなので、エディター用のC++クラスに詳しくなる。 93/96
Unreal Pythonのデメリット ● 実行速度の遅さ ● BPとC++に比べると明らかに実行速度は遅い。 ● 実行時にしかエラーがわからない ● 静的解析が充実してきたが、完全ではない。 ● エクスポーターなどアセットパイプラインを担う部分ではいつの間にか動かなくなるのは問題あり。 ● 前述したEngineアップデート時にも問題となりやすい。 ● テストコードなどで補強する必要がある。 ● 自由度は高いが完全ではない ● かなりの範囲が公開されているが、Editor 機能全てにアクセスできる訳ではない ● アクセスできない部分は、C++経由で公開する必要がある 94/96
C++でツールを作った方が良いケース ● アーティストのワークフローをサポートするエンジニアが多く居る場合 ● 現実、中々難しいが・・・。 ● 速度が要求される箇所 ● Unreal Pythonのデメリットでも記載した通り、速度が要求されるところは苦手。 ● Blueprintと混ぜて使いたい場合 ● 具体的なケースでBlueprint内の関数をpythonで定義するなど。 ● 読み込み順の問題でBlueprintがコンパイルエラーになることがあった。 ● ModuleのLoading Phaseを変更することで、回避は可能そう (未検証) ● 元々C++からBlueprint、Pythonに公開するという主従関係になってるので使い方が違う。 ● => 素直にC++で定義するかBlueprintで定義する。 95/96
おわりに ● Unreal Pythonの導入方法から自動化事例と続き、リリース方法までの一連の 流れをご紹介させていただきました。 ● 本講演でUnreal Python利用で参考になるものがあれば幸いです。 ご清聴ありがとうございました! 96/96
Q&A
アンケートへのご協力をよろしくお願いします アンケートQRコード (後日ご支給いたします) https://docs.google.com/forms/d/e/1FAIpQLSeeq3O53ausrSoHXHl uKskiabOlucYgXBajAhVQ2IbL__Km7Q/viewform 98/96