Yurlungur is 何?
ゲーム・CG業界でスタンダードになっている Maya・Houdini・Unreal のスクリプトAPIを、HOMライクなAPIに統一することを目指す、テクニカル・アーティスト(以下TA)向けのPythonライブラリです。
その中身は、maya.cmds / hou / unreal モジュールのメタプロラッパーです。
本業の待ち時間と休日を使って少しずつ作りました。
pip でインストール出来ます。
pip install yurlungur
GitHub でソースを公開しています。MITライセンスです。
https://github.com/sho7noka/Yurlungur
Web のドキュメントもあります。
https://sho7noka.github.io/Yurlungur/
なぜ作ったのか?
ゲーム・CG業界で使われている3Dアプリケーションは、複雑さを増す多人数分業を支援するために Python が広く組み込まれています。ハイエンド・モバイルに関わらず、現場仕様のカスタマイズは必須です。
しかし、それぞれの3DアプリケーションのAPIデザインはまるで統一されていません。現在のコンテンツ制作において1つのアプリケーションで全工程を賄うのは不可能(あるいは非効率)になっているにも関わらず、それぞれのパイプラインを整備するために、TAやツールプログラマがそのAPIの「お作法」を毎回学ぶのは非効率だと考えていました。ツールの書き方も私が知る限りルールがありません。
それに加え、いざという時の アプリケーションの乗り換えには多大なコストがかかります。教育コスト、心理コストはもちろん、特にインハウスツールの全面的な書き直し作業が原因で、規模が大きい会社ほど簡単には移行できません。(どことか何のアプリとかは言いません)
海外に目を向ければ、PixarUSDやIECoreなどC++で設計したシーングラフをそれぞれのアプリケーションAPIにadaptして扱う例はあります。共通のツールプログラミング環境として Fabric Engine という先見性のあるライブラリもありました。(開発は終了しています)
上記のような理由から、個人で出来る規模で各DCCツールのAPIを共通化したライブラリの必要性を感じていました。
その結果、Houdiniのオブジェクトモデルをベースにアレンジを加えて作ったラッパーモジュールが、Yurlungur です。
ちなみに Unreal は、v4.1.9のbeta版でPythonをサポートしました。
UnityはBooとJavaScriptがオミットされ、ランタイムとエディター両方でC#のみサポートなので対応は後です。
Yurlungurで出来ること
Yurlungur には大きく分けて二つの機能があります。
- コマンドラッパー
- コマンドラインインターフェース
1. コマンドラッパー
オブジェクトベースのスクリプトAPIです。YObject, YNode, YAttr などYで始まるオブジェクト内に、そのアプリケーションが適合するオブジェクトをラッピングしています。
実際の現場で個々のローカル環境にpipインストールは大変なので、sys.pathに通してしまうのが良いでしょう。
import sys
sys.path.append("yurlungurがインストールされたパス")
import yurlungur as yr
クラスについて説明します。
YObject (オブジェクト)
YObject は全アプリケーション共通の基底クラスです。シーン中のオブジェクトを使ってインスタンス化します。
obj = yr.YObject("sphere")
rename_obj = obj("my sphere")
print rename_obj.name
Pythonはオーバーロードをサポートしませんし、getter/setter を推奨しない文化なので、__call__
経由で rename プロシージャを呼び出しています。
YNode (ノード)
YNode は YObject のサブクラスです。name/property/id などの YObject のプロパティに加え、ノード生成とコネクション情報を持つアプリケーションの基本となるオブジェクトです。
node = yr.YNode.create("gpuCache")
print node.inputs()
print node.name
YObjectと差別化するために __add__
や __sub__
でオペレーターオーバーロードを実装して、YNode + YNode
みたいな書き方も良いかなと考えています。
YAttr (アトリビュート)
YAttr はHoudiniのAPIの影響を強く受けたアトリビュートオブジェクトです。特に maya.cmds はアトリビュートの getAttr と setAttr が使いにくいので、HOMに寄せた使い方にデザインしています。
width = yr.YObject("defaultResolution").attr("width")
width.set(1000)
PyMELのAttributesクラスと同じようなアクセサーも用意しました。
上記と同じ動作になります。
reso = yr.YObject("defaultResolution")
reso.width.set(1000)
ノードとアトリビュートの共通APIオブジェクト導入を検討しています。データベース化したORマッパー的な仕組みが良いかなと考えています。
メタオブジェクト
これでMELとサヨナラできるやったー!みたいにならないのが、ラッピングライブラリを作っていく上での宿命です。使用頻度が少なくラップされていないメソッドやプラグインを介したプロシージャを使いたい場合、それを Yurlungur からコールする方法を用意しています。これはyr.meta
モジュールと environment
モジュールを使うことで実現できます。
if yr.Maya():
yr.meta.ls(sl=1) # ls -sl が呼ばれる
yr.meta
モジュールは、ゴーストメソッドを介したYMObjectのエイリアスです。environment モジュール内のMaya関数は、実行環境の判断で各コマンドのモンキーパッチ制御に使っている内部実装向けモジュールです。
デコレーターに対応しているので、関数ブロック単位でコマンド実行の制御が出来ます。
@yr.Maya
def createCache():
node = yr.YNode.create("gpuCache")
print node.inputs()
print node.name
@yr.Houdini
def createSOP():
node = yr.YNode.create("geometry")
print node.inputs()
print node.name
if __name__ == "__main__":
# Maya 実行環境下のみコール
createCache()
# Houdini 実行環境下のみコール
createSOP()
2. コマンドラインインターフェース
DCCアプリケーションはスタンドアロンアプリケーションと並行してmayapyやhythonなどのコマンドラインツールを実装しています。
特にMayaを扱うプロダクションは複数のアプリを使うので、それらを共通のAPIでやりとり出来れば便利だと思い実装しました。パイプラインを意識したファイルのやり取りに毎回アプリケーションを開き直す必要がないので便利です。
HoudiniEngine や LiveLink 機能で同じ事を実現できますが、主にインハウスツールやテキストエディタを仲介する組み込み向けの機能です。
python yurlungur -h
__main__.py
をモジュールに入れると、pythonはそこをエントリーポイントとして見なします。PyQtかPySideがローカルにインストールされていれば、スタンドアロンで起動します。
ここからは、少し実装の中身を紹介させてください。
メタプログラミングのむずかしさ
ライブラリを設計するには、普段見る必要のない世界を覗かないといけません。
Pythonは、実装する用途に絞れば簡単でモジュールも充実したプログラミング言語ですが、ライブラリを読んだり作るために特殊メソッドをはじめとした、普段のツール開発で目にしない言語仕様を掘り下げる必要があります。
ドキュメントが理解できてもユーザに見えない部分の実装というのは、使うイメージがなかなか湧きません。
同時にクラスを全て手入力するのは大変なので、メタプログラミングを介する必要性を直感的に感じていました。これは、PyMELを参考に実装を進める事にしました。
PyMELを手がかりにして感触を掴む
メタクラスの設計の例として、PyMELのcoreにその形跡が見えます。
Cygwin や git bash で下記コマンドを入力して見てください。
$ cd C:/Program Files/Autodesk/Maya2017/Python/Lib/site-packages/pymel
$ grep -r -n __metaclass__ .
./core/datatypes.py:290: __metaclass__ = MetaMayaArrayTypeWrapper
./core/datatypes.py:1429: __metaclass__ = _factories.MetaMayaTypeWrapper
./core/datatypes.py:1550: __metaclass__ = MetaMayaArrayTypeWrapper
./core/datatypes.py:2248: __metaclass__ = MetaMayaArrayTypeWrapper
./core/datatypes.py:2802: __metaclass__ = _factories.MetaMayaTypeWrapper
./core/general.py:2754: __metaclass__ = _factories.MetaMayaTypeWrapper
./core/general.py:4061: __metaclass__ = _factories.MetaMayaComponentWrapper
./core/general.py:6076: __metaclass__ = _factories.MetaMayaTypeWrapper
./core/general.py:6135: __metaclass__ = _util.Singleton
./core/language.py:277: __metaclass__ = util.Singleton
./core/language.py:303: __metaclass__ = util.metaStatic
./core/language.py:467: __metaclass__ = util.Singleton
./core/language.py:607: __metaclass__ = util.Singleton
./core/language.py:1095: __metaclass__ = util.Singleton
./core/language.py:1133: __metaclass__ = util.Singleton
./core/nodetypes.py:45: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:757: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:761: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:766: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:776: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:1678: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:1692: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:1759: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2239: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2246: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2384: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2474: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2758: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2866: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2893: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2899: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:2913: __metaclass__ = _factories.MetaMayaTypeWrapper
./core/nodetypes.py:3229: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:3513: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:3534: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:3552: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:3640: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:3644: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:3651: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:3654: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/nodetypes.py:3657: __metaclass__ = _factories.MetaMayaNodeWrapper
./core/system.py:630: __metaclass__ = _util.Singleton
./core/uitypes.py:470: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:572: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:619: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:741: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:744: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:768: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:812: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:815: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:831: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:860: __metaclass__ = _factories.MetaMayaUIWrapper
./core/uitypes.py:1041: __metaclass__ = AELoader
./internal/factories.py:1849: __metaclass__ = util.Singleton
./internal/parsers.py:272: __metaclass__ = util.Singleton
./tools/mel2py/melparse.py:1060: __metaclass__ = util.Singleton
./util/arrays.py:1099: __metaclass__ = metaReadOnlyAttr
./util/objectParser.py:940: __metaclass__ = metaStatic
./util/objectParser.py:949: __metaclass__ = metaStatic
./util/trees.py:1441: __metaclass__ = MetaTree
./util/trees.py:1446: __metaclass__ = MetaTree
./util/trees.py:1451: __metaclass__ = MetaTree
./util/trees.py:1456: __metaclass__ = MetaTree
./util/utilitytypes.py:17: __metaclass__ = Singleton
./util/utilitytypes.py:31: __metaclass__ = Singleton
./util/utilitytypes.py:98: __metaclass__ = metaStatic
./util/utilitytypes.py:800: __metaclass__ = TestMetaClass
デザインパターンが使われて居るのだなと思ってもらえれば良いです。将来的な環境移行やBlenderにも対応させたかったので、__metaclass__を使わずにtype関数のもう1つの機能を用いて実装します。
#type(クラス名、親クラス、メンバー属性)
_YVector = type('_YVector', (yr.meta.Vector3,), dict())
このtype関数でスーパークラスを動的に変更することで、OpenMaya の MVector クラスと HOM の hou.Vector3 クラス、unreal の Vector クラスをダイナミックに継承することが出来るようになります。特に計算速度が気になるコンテナはPythonで自前実装せずに、ホストアプリケーションのオブジェクトをそのまま使うように工夫しています。
課題と未来
プロユースの信頼を目指すにはライブラリの安定性と互換性の確保が必要ですが、いくつか課題があります。
ノード操作やアプリケーション専用uiなどの editor 機能は、独自アーキテクチャに左右されるので抽象化が進められていません。ダイアログやボタンなどQtで共通化できるものはラップせず、キーフレームなど必要最低限に絞って実装したいと考えています。ポリゴンやパーティクルなどの builder 機能はアプリケーションAPIを一つ一つ読み込んで行かないといけないので、徐々に作っていきたいと思っています。
GUIはQt, APIはYurlungurに準拠すれば、LightWeightなマルチプラットフォームツール作りが出来ます。
最後に
ライセンス確保やテストの都合上、ひとりで全てのDCCアプリケーションの共通化を進めるのはキビしく、Unrealが殆ど進んでいないので片手落ち感満載なプロジェクトです。3dsMax、Blender、motionbuilderあたりはラッピングするだけなので、複数アプリを使わざるを得ない悩みをお持ちなスポンサーをお待ちしています(笑)
また、志を共有して頂けるTAの方(存在すれば)をお待ちしています。