SOP、DOP、WrangleあたりはネタがかぶりそうだったのでPythonネタを書きたいと思います。
バージョンはHoudini15.5時点での話です。
Houdiniがノードベースなシステムであることは、言い換えればビジュアルプログラミングができるということなので、他の3D CGソフトウェアよりはPythonによるスクリプティングの機会は少ないのではないかと思います。
では、どこでPythonを使用するんだ!って話になるんですが、やはりパイプライン開発がメインになります。
例えばHoudini標準のディスパッチャーであるHQueueは、標準のままつかったとしたら、全フレームのシミュレーションが終わった後にレンダリングジョブを別クライアントに渡すという流れになりますが、Pythonを使えば、1フレームのシミュレーションが終わったら別のクライアントにそのレンダリングジョブを投げると言ったことが可能になります。
また、Mantraコマンドの-PオプションでPythonスクリプトファイルを指定することができるので、既に書き出したIFDのレンダリングプロパティ(例えば、サブピクセル)を上書きしてレンダリングすることができるので、サブピクセルを変更して再度IFDを出力するといった手間を省くことができます。
他にもAlembicファイルを読み込んだ時にその中のオブジェクトトランスフォーム情報を読み込むといったこともPythonを使うことで可能です。
HQueue自体はフリーですがHoudini Indie以上のライセンスの有無をチェックします(ライセンスは掴まない)、IFDの書き出しは商用版Houdini/Engineでないとだめなので、ApprenticeユーザーでもIndieユーザーでも役立つ情報としてPythonによるAlembicの情報取得の方法を紹介したいと思います。
HoudiniのPythonには、_alembic_hom_extensionsと呼ばれるモジュールが用意されています。
通常ではPythonでHoudiniを制御する時はhouモジュールから始めていくわけなのですが、この**_alembic_hom_extensionsはhou**モジュールとは独立しています。つまり、このモジュールを使用するだけであればライセンス不要。
でも、HoudiniにAlembicの情報を渡して処理することがほとんどなのであまり意味ない
このモジュールを使用することで、主に指定したAlmembicファイルのオブジェクト階層、オブジェクトトランスフォーム、可視性を取得することができます。
他にもオブジェクトのユーザープロパティの値(Houdiniから出すAlembicならカメラオブジェクトに追加設定されている解像度がそれですね)やオブジェクトアニメーションの時間範囲を取得することができます。
このモジュールが持っている関数は以下のとおりです。
読み流す程度で次の説明にいきましょう。
alembicArbGeometry(abcPath, objectPath, name, sampleTime) → (value, isConstant, scope)
Noneまたは(value, isConstant, scope)のタプルを返します。このタプルの中身は、アトリビュートの値、そのアトリビュートが時間軸で一定値かどうかを示したブール値、スコープ('varying', 'vertex', 'facevarying', 'uniform', 'constant', 'unknown')です。
alembicClearArchiveCache()
Alembicファイルの内部キャッシュをクリアします。
alembicGetArchiveMaxCacheSize()
Alembicファイルキャッシュサイズを返します。
alembicGetCameraDict(abcPath, objectPath, sampleTime)
指定したオブジェクトのカメラパラメータの辞書を返します。
alembicGetCameraResolution(abcPath, objectPath, sampleTime)
Noneまたは2個のfloatを含んだタプルを返します。1番目の値は、Houdini CameraのX解像度です。2番目の値は、Houdini CameraのY解像度です。なにかしらのカメラ(例えばMaya Camera)には解像度がないので、この場合にはNoneが返されます。
alembicGetObjectPathListForMenu(abcPath)
メニューコールバックに必要な形式の文字列をタプルで返します。
alembicGetSceneHierarchy(abcPath, objectPath) → (object_name, object_type, (children))
3つのタプルを返します。各タプルは、以下の構成になっています:
(object_name, object_type, (children))
(children)は、子ノードを含んだタプルです。
object_typeは以下のどれかが含まれます(他のタイプを含む場合もあります):
- cxform 一定トランスフォームノード
- xform アニメーショントランスフォームノード
- camera カメラノード
- polymesh Polygon Meshシェイプノード
- subdmesh Subdivision Surface Meshシェイプノード
- faceset Face Setシェイプノード
- curves Curvesシェイプノード
- points Pointsシェイプノード
- nupatch NuPatchシェイプノード
- unknown 不明なノード
alembicHasUserProperties(abcPath, objectPath)
オブジェクトにユーザープロパティがなければNoneを返します。ある場合は、そのユーザープロパティが時間軸で一定かどうかを返します。
alembicSetArchiveMaxCacheSize(size)
一度にキャッシュ化されるAlembicファイルの最大数を設定します。
alembicTimeRange(abcPath, [objectPath=None]) → (start_time, end_time)
Noneまたは(start_time, end_time)のタプルを返します。このタプルは、Alembicアーカイブ内のFPS情報によるそのアーカイブのグローバル開始/終了時間を含んでいます。objectPathを指定すれば、そのオブジェクトの開始/終了時間を計算します。そのアーカイブが一定であればNoneを返します。
alembicUserProperty(abcPath, objectPath, name, sampleTime) → (value, isConstant)
Noneまたは(value,isConstant)のタプルを返します。このタプルは、アトリビュートの値、そのアトリビュートが時間軸で一定値かどうかを示したブール値を含んでいます。
alembicUserPropertyMetadata(abcPath, objectPath, sampleTime)
NoneまたはJSON辞書を返します。このJSON辞書は、ユーザープロパティ名 --> ユーザープロパティメタデータのマップを含んでいます。
alembicUserPropertyDictionary(abcPath, objectPath, sampleTime)
NoneまたはJSON辞書を返します。このJSON辞書は、ユーザープロパティ名 --> ユーザープロパティ値のマップを含んでいます。
alembicUserPropertyValuesAndMetadata(abcPath, objectPath, sampleTime)
Noneまたはタプルを返します。このタプルは2つのJSON辞書を含んでいます。1番目の辞書は、ユーザープロパティと値のマップを含んでいます。2番目の辞書は、ユーザープロパティと1番目の辞書の解釈に使用されるメタデータのマップを含んでいます。
alembicVisibility(abcPath, objectPath, sampleTime, [check_ancestor=False]) → (value, isConstant)
Noneまたは(value,isConstant)のタプルを返します。このタプルは、オブジェクトの可視性、その可視性が時間軸で一定かどうかを示したブール値を含んでいます。可視性の戻り値の0は非表示、1は可視、-1はディファー(親の可視性に依存)を意味します。
getLocalXform(abcPath, objectPath, sampleTime) → (xform, isConstant, inherit)
(xform, isConstant, inherits)のタプルを返します。このタプルは、ローカルトランスフォーム、そのトランスフォームが時間軸で一定かどうかを示したブール値、ノードが親のトランスフォームを継承しているか(またはトランスフォーム階層と繋がっているか)を示したブール値を含んでいます。
getWorldXform(abcPath, objectPath, sampleTime) → (xform, isConstant, inherit)
(xform, isConstant, inherits)のタプルを返します。このタプルは、ワールドトランスフォーム、そのトランスフォームが時間軸で一定かどうかを示したブール値、ノードが親のトランスフォームを継承しているか(またはトランスフォーム階層と繋がっているか)を示したブール値を含んでいます。
実際には上記のすべての関数を使用するわけではないので、注目すべき関数だけにスポットを当てたいと思います。
alembicGetSceneHierarchy(abcPath, objectPath)
alembicTimeRange(abcPath, [objectPath=None])
getWorldXform(abcPath, objectPath, sampleTime)
かなり少なくなりました。この3つの関数で何ができるのかを見てみましょう。
cam1、Toy、Subnet(この中にPig、ShaderBall)を作成しました。
ShaderBall以外にはオブジェクトレベルのアニメーションを設定しています。
このシーンを丸ごとAlembicに出力し、それを新規シーンに読み込んでみます。
Houdiniと同じオブジェクト階層でデータが取り込まれており、アニメーションもします。
でもアニメーションするcam1オブジェクトを選択してもAnimation Editorには何もアニメーションカーブが表示されません。
CHOPに詳しい人であればObject CHOPを使えばMotion FX Viewにてサンプリングされたアニメーションカーブが表示されるじゃん?って思われるかと思います。元のAlembicのアニメーションデータがベイクされていて線形補間されているものであればそれでいいんですが、そうじゃないアニメーションの場合は浮動小数点フレームの値もちゃんと取得しておきたいものです。
なので、やっぱりアニメーションカーブを取得したいですよね?
そこで登場するのが**_alembic_hom_extensions**モジュールです。
大まかな流れとしては、
- Alembic内のオブジェクト階層を取得し、どのオブジェクトにアニメーション情報が付いているのか判断する
- そのオブジェクトのアニメーション範囲を調べる
- オブジェクトトランスフォーム情報を取得
で色々とパイプライン開発ができます。
####Alembicのオブジェクト階層とアニメーションタイプを取得する方法
コードは以下のとおりです。
import _alembic_hom_extensions as abc
abcPath = "C:/data/alembicFile.abc"
def expandChild(root,child,objectHierarchy,objectType):
objectHierarchy.append(root+child[0])
objectType.append(child[1])
if len(child[2])==0:
return
else:
return expandChild(root+child[0]+"/",child[2][0],objectHierarchy,objectType)
objectHierarchy=[]
objectType=[]
childNodes = abc.alembicGetSceneHierarchy(abcPath, "/")[2]
for eachChildNode in childNodes:
expandChild("/",eachChildNode,objectHierarchy,objectType)
print objectHierarchy
print objectType
#出力結果
['/Toy', '/Toy/testgeometry_rubbertoy1', '/cam1', '/cam1/cameraProperties', '/Subnet', '/Subnet/Pig', '/Subnet/Pig/testgeometry_pighead1', '/Subnet/Pig/testgeometry_pighead1/PigFace']
['xform', 'polymesh', 'xform', 'camera', 'cxform', 'xform', 'polymesh', 'faceset']
xformは、そのオブジェクトにアニメーション情報を持っていることを意味し、cxformは、アニメーション情報を持っていないことがわかります。
####アニメーション範囲を取得する方法
では、cam1のアニメーション範囲を調べてみましょう。
import _alembic_hom_extensions as abc
abcPath = "C:/data/alembicFile.abc"
objectPath = "/cam1"
print abc.alembicTimeRange(abcPath, objectPath)
#出力結果
(0.041666666666666664, 2.0)
この関数で出力されるアニメーション範囲の単位は秒です。
このカメラはアニメーション範囲が1フレームから48フレームでFPS=24で取り込んでいるので
(1.0/24.0,48.0/24.0)の結果が表示されています。
####現行フレームにおけるカメラの移動、回転、スケールの値を取得する方法
では、オブジェクトのトランスフォームを調べてみましょう。
import _alembic_hom_extensions as abc
abcPath = "C:/data/alembicFile.abc"
objectPath = "/cam1"
sampleTime = hou.frame()/hou.fps()
#getWorldXformで返されたタプルの1番目の要素は16個のfloatタプル
xform = abc.getWorldXform(abcPath, objectPath, sampleTime)[0]
#マトリックス形式に変換するためにhou.Matrix4を使用する
xformMatrix = hou.Matrix4(xform)
#移動、回転、スケールを抽出する
explodedDictionary = xformMatrix.explode()
print "translate:",explodedDictionary["translate"]
print "rotate",explodedDictionary["rotate"]
print "scale",explodedDictionary["scale"]
#出力結果
translate: [5.75595, 3.34021, 10.2852]
rotate [-11.6801, 28.4249, -1.25354e-06]
scale [1, 1, 1]
```
上記の単一スクリプトで利用するのもありですが、**HoudiniのエクスプレッションはHScriptだけでなくPythonも使用することができます**。
なので、次はAlembicで取り込んだカメラノードに以下のようにユーザーパラメータ(HoudiniではSpareパラメータと言います)を追加してみましょう。
ここでは、Float Vector3を2つ追加し、TranslateパラメータとRotateパラメータを用意しました。
そして、エクスプレッションのタイプをPythonに変更します。
![UserParms.png](https://qiita-image-store.s3.amazonaws.com/0/154191/3c170758-182d-f349-6414-f89ccfb60957.png)
Alembic Xformノードでは、既にFile Name、Object Path、Frame、Frameのパラメータが用意されているので、これらのパラメータの値を利用して、カメラの移動、回転の情報を取得してみたいと思います。
パラメータ上でAlt+Eキーで表示されるEdit Expressionダイアログによって複数行のPythonコードを入力することができますが、よく使用するコードをいちいち入力するのは面倒ですよね。
よく使用するコードは、カスタム関数として登録しておくのが便利です。
カスタム関数を登録する方法は、特定のディレクトリにPythonファイルを配置して、それをパスに通しておく方法がありますが、それだと環境依存のシーンファイルになってしまいます。
それは回避したい!
なので、カスタム関数をシーンファイル内で定義することにします。
####カスタム関数をシーンファイルに登録する方法
カスタム関数は、WindowsメニューのPython Source Editorで登録することができます。
![PythonSourceEditor.png](https://qiita-image-store.s3.amazonaws.com/0/154191/6022a0b5-fa4d-97ad-e550-6a4fe3141500.png)
そして、以下のようにコードを入力します。
![PythonSourceEditorCode.png](https://qiita-image-store.s3.amazonaws.com/0/154191/6285343b-318f-656a-d176-36201529f123.png)
このコードは、Alembic Xformノード上のパラメータで動作し、そのノードのトランスフォーム情報を返す関数です。
```python:
#modeが"translate"なら位置情報、"rotate"なら回転情報、"scale"ならスケール情報を返します
#indexが0ならX成分、1ならY成分、2ならZ成分
def getAlembicTransform(mode="translate",index=0):
import _alembic_hom_extensions as abc
currentNode = hou.pwd()
abcPath = currentNode.parm('fileName').eval()
objectPath = currentNode.parm('objectPath').eval()
sampleTime = currentNode.parm('frame').eval()/currentNode.parm('fps').eval()
xform = abc.getWorldXform(abcPath, objectPath, sampleTime)[0]
xformMatrix = hou.Matrix4(xform)
explodedDictionary = xformMatrix.explode()
return explodedDictionary[mode][index]
```
Python Source Editorウィンドウでコードを登録したら、
以下のように"getAlembicTransform"カスタム関数を呼び出すことができるようになります。
![ParameterExpression.png](https://qiita-image-store.s3.amazonaws.com/0/154191/380f944f-fa7b-6f00-98e3-c6ffd7d9344a.png)
パラメータ名を左クリックして、値がちゃんと入っているのかを確認します。
![ParameterExpressionEval.png](https://qiita-image-store.s3.amazonaws.com/0/154191/c8e5cc41-04e2-4412-55ac-3d802a64680b.png)
これで、AlembicオブジェクトのアニメーションカーブをAnimation Editorで確認できるようになります。
![AnimationCurve.png](https://qiita-image-store.s3.amazonaws.com/0/154191/e4697ace-c750-d3b1-7f2a-64ff75d139f7.png)
これでAlembicのトランスフォーム情報をパラメータ値に入れることができました。
おしまい。:hugging: