はじめに
もう毎年高齢恒例のHoudini Advent Calendar。
さて何を書こうかと考えたけど、ボクは今年はArnoldばかりさわってるのでHtoA(Arnold for Houdini)で参考になりそうな情報を書きたいと思います。
Arnoldとはレンダラーの1つで、
- Houdini(HtoA)
- Maya(MtoA)
- 3dsMax(MaxtoA)
- Cinema4D(C4DtoA)
- Softimage(SitoA)
- Katana(KtoA)
- Gaffer(GafferArnold)
- Blender(BtoA)
- 他にもGolaem、Massive、NukeなどでもArnoldに対応していたりします。
つまり、メジャーどころのソフトがたいてい対応しているレンダラーで、映像業界では事実上のデファクトスタンダードと言えます。
しかも、バッチレンダーライセンス1本でどのDCCでもレンダリングできるというコスト面でも優しいです。
さらには、C++だけでなくOpen Shading Language(OSL)でシェーダを書くことができたり、MaterialX(後述で触れます)にも早く対応していたり、次はUSDですかね。楽しみです。
Houdini標準のMantraレンダラーはHoudiniユーザーには無償で好きなだけ使える太っ腹仕様で、シェーダも馴染みのあるVEXで書きやすいので私は気に入ってるのですが、Houdiniでしか動かないというデメリットが辛いです。
Arnoldは、標準化されたシーン記述フォーマットであるass(Arnold Scene Source)ファイルでレンダリング(これをKick Ass)することができます。
つまり、どのDCCでもassに書き出してしまえば、どのDCCでもレンダリングすることができます。
Arnoldを理解するなら、assを勉強するのが良いです。
参考になるおすすめページは、ここです。
https://arnoldsupport.com/category/ass/
尚、HoudiniでArnoldのようなサードパティ製レンダラーを使用する場合はHoudini Indie、Houdini Core、Houdini FXのどれかのライセンスが必要となります。
また、Houdini Indie版ではassファイルの出力ができません(Houdini18.0.460以降で出力できるようになりました)
なので、Houdini Core/FX環境、Maya環境(学生版、ホームライセンスでも可)でassの出力をして勉強してください。
Mayaで構築したレンダーシーンをHoudini上でHoudiniレンダーシーンと共存させてレンダリングしたいのであれば、MtoAからassを書き出して、それをHtoAで読めばいいだけです。
MtoA上でArnoldメニュー→Export StandInを実行して、出力したい情報にチェックを付けてassファイルを書き出します。
HtoAの/obj
コンテキスト上でArnold ProceduralまたはArnold Includeを追加して、そのassファイルを指定するだけでHoudini上にMayaのArnoldシーンをそのまま取り込むことができます。
Maya固有のシェーダを使用しているシーンだとArnold ROPのPlugin Path
にそのDLLのパスを指定する必要があるのですが、バージョンが上がる度にDCC固有シェーダから純粋Arnoldシェーダに切り替わるようになってきています。今後も期待。
Mayaのシーンをassファイル経由でHoudiniに取り込んで豚さんと共存させた例。
便利っすね。
ただ、別のDCCのオブジェクトをHoudni上で編集したいとなった場合、現状はAlembic/FBXなどの中間フォーマット(今後はUSD)でHoudiniに取り込んでArnoldシェーダを割り当ててレンダリングすることとなります。
その上で、Maya上で準備しておくと便利なことがあるので、それについて紹介したいと思います。
Houdiniのファイルの拡張子はhip
Arnoldのファイルの拡張子はass
hipとass!!!
なんか相性良さそう
Alembic経由でMayaからHoudiniへ任意のアトリビュートを渡す
MayaのシェイプにHoudini用Point/Primitiveアトリビュートを追加する
HoudiniではRest Positionをよく使用します。
Rest Positionとは、日本語で"静止位置"のことです。
ArnoldだとPref(Position Reference)、MayaだとReference Objectとか指すかと思います。
なぜ必要なのかというと、ジオメトリにテクスチャを2D/3D投影する方法(例えばカメラプロジェクション)でシェーダを組んだ場合に、そのジオメトリが変形すれば投影されたテクスチャは滑ったように見えます。
この場合、変形前のジオメトリに2D/3D投影した結果を変形後のジオメトリに戻すというテクニックを使用します。
よくわからない人は、ここ読んでね。 https://houdini.prisms.xyz/wiki/index.php?title=Rest_Attrib
MayaでRest Positionを設定する方法は、TexturingメニューのCreate Texture Reference Objectを実行し、Arnoldでそれを反映させるためにMayaシェイプのArnoldプロパティにあるExport Reference Positionsを有効にすることです。
この方法は、Maya特有でジオメトリをリファレンス形式で参照していてごちゃごちゃして嫌いです。そして、純粋にAlembicでアトリビュートとして出せない。
理想は、ジオメトリに頂点アトリビュートとしてRest Positionを追加し、そのアトリビュートをAlembicに書き出したいです。
Mayaのシェイプに任意で頂点アトリビュート/フェースアトリビュートを追加することは、MELを組むとできるので、その方法を紹介します。
一般的にはMaya頂点アトリビュート<->Houdiniポイントアトリビュート、Mayaフェースアトリビュート<->Houdiniプリミティブアトリビュートという認識で良いと思います。
今回は、Rest Positionを頂点アトリビュートとして追加したいのですが、ついでに、フェース単位のシェーダ名が入ったフェースアトリビュートも追加して、頂点/フェースアトリビュートの追加方法について説明したいと思います。
Pymelコードは以下のとおりです。
これは、SGにArnoldシェーダが割り当てられているシェイプに対してRest Positionとshop_materialpathを追加するMaya用スクリプトです。
頂点座標をワールド空間で取得しているので、Alembicに書き出したいオブジェクトのトランスフォームがフリーズされていることを前提にしています。
- 現行フレームのジオメトリの位置情報を格納した
rest
という名前の頂点アトリビュート - ジオメトリに割り当てられているシェーダ名を
/shop/maya/シェーダ名
という文字列を格納したshop_materialpath
という名前のフェースアトリビュート
を追加することができます。
これを応用すればVelocityとかCdなんかも追加できるのでチャレンジしてみてください。
import pymel.core as pm
#キーがシェイプ名、値がそのシェイプのフェースアトリビュート値のリストのペアである辞書を宣言
dictAttributes = {}
context = "/shop/maya/"
for eachSG in pm.ls(type="shadingEngine"):
members = pm.sets(eachSG,q=True,nodesOnly=False)
#SGにオブジェクト/コンポーネントが割り当てられていなければスキップする
if len(members)==0:
continue
#SGにシェーダが割り当てられていなければスキップする。
#SGのaiSurfaceShaderの入力コネクタがArnoldで優先されるサーフェスシェーダ。なければsurfaceShaderの入力コネクタのサーフェスシェーダが使用される。
shader = (pm.listConnections(eachSG+".aiSurfaceShader",p=False,c=False,s=True,d=False) or [""])[0]
if shader == "":
shader = (pm.listConnections(eachSG+".surfaceShader",p=False,c=False,s=True,d=False) or [""])[0]
if shader == "":
continue
shaderName = shader.name()
#オブジェクトレベルの割り当てなのかコンポーネントレベルの割り当てなのかに基づいて処理を分岐させる。
#オブジェクトレベルの場合は、すべてのフェースの数だけのサイズのリストを用意して、リストのすべてのアイテムにシェーダ名を書き込む
#コンポーネントレベルの場合は、そのフェースのインデックスにシェーダ名の情報を書き込む
for eachMember in members:
#SGがオブジェクトに紐付けられている場合の処理
if type(eachMember) == pm.nodetypes.Mesh:
dictAttributes[eachMember]={"shader":[],"rest":[]}
dictAttributes[eachMember]["shader"] = [context+shaderName]*eachMember.numFaces()
for vtxIndex in range(eachMember.numVertices()):
position = pm.pointPosition(eachMember+".vtx["+str(vtxIndex)+"]",world=True).get()
dictAttributes[eachMember]["rest"].append( [position[0],position[1],position[2]] )
#SGがフェースに紐付けられている場合の処理
elif eachMember._ComponentLabel__ == "f":
shape = eachMember._node
if shape not in dictAttributes:
dictAttributes[shape]={"shader":[],"rest":[]}
dictAttributes[shape]["shader"]= [""]*eachMember.totalSize()
for vtxIndex in range(shape.numVertices()):
dictAttributes[shape]["rest"].append( pm.pointPosition(shape+".vtx["+str(vtxIndex)+"]",world=True) )
#該当するリストのインデックスにシェーダ名を書き込む
for index in eachMember.indices():
dictAttributes[shape]["shader"][index] = context+shaderName
restAttributeName = "rest"
shaderAttributeName = "shop_materialpath"
for eachShape in dictAttributes:
#Rest Positionアトリビュートを追加
if not pm.attributeQuery(restAttributeName+"_AbcGeomScope",node=eachShape,exists=True):
pm.addAttr(eachShape, dataType="string", longName=restAttributeName+"_AbcGeomScope")
pm.setAttr(eachShape+"."+restAttributeName+"_AbcGeomScope", "var")
if not pm.attributeQuery(restAttributeName,node=eachShape,exists=True):
pm.addAttr(eachShape,dataType="vectorArray",longName=restAttributeName)
pm.setAttr(eachShape+"."+restAttributeName,dictAttributes[eachShape]["rest"])
#shop_materialpathアトリビュートを追加
if not pm.attributeQuery(shaderAttributeName+"_AbcGeomScope",node=eachShape,exists=True):
pm.addAttr(eachShape, dataType="string", longName=shaderAttributeName+"_AbcGeomScope")
pm.setAttr(eachShape+"."+shaderAttributeName+"_AbcGeomScope", "uni")
if not pm.attributeQuery(shaderAttributeName,node=eachShape,exists=True):
pm.addAttr(eachShape,dataType="stringArray",longName=shaderAttributeName)
pm.setAttr(eachShape+"."+shaderAttributeName,dictAttributes[eachShape]["shader"])
実行すると、下図のようにカスタムアトリビュートが追加されます。
通常のアトリビュートだと数値/文字列フィールドの形式で値が表示されると思うのですが、追加したrest/shop_materialpathは頂点/フェース単位の配列値なので値をGUIから確認することができません。Component Editorで確認できたらいいのですが。。。
ここで、アトリビュート名_AbcGeomScope
という名前のアトリビュート名を意図的に追加しているのですが、これには意味があります。
シェイプに配列値のアトリビュートを追加した時、それだけでは情報が足らないことにお気づきでしょうか?
それがオブジェクト単位の配列アトリビュートなのか?頂点単位のアトリビュートなのか?フェース単位のアトリビュートなのか?
それを明示的に指定する必要があるのです。
HoudiniでもWrangle SOPを触った時に"Run Over"パラメータで意図的にPoint/Vertex/Primitive/Detailのどのアトリビュートなのかを指定すると思います。
Mayaはシェイプ上でアトリビュートを追加している以上は、補足情報として頂点、フェース、オブジェクトのどのアトリビュートなのか明示しなければなりません。
MayaのAlembicインポータ/エクスポータでは、その役割をしているメタデータとして_AbcGeomScope
接尾辞が決められています。
その情報はAlembicのGithubで公開されているコード内に記述されています。
https://github.com/alembic/alembic/blob/master/maya/AbcExport/AttributesWriter.cpp
_AbcGeomScope
文字列の値には、vtx
、fvr
、uni
、var
を指定することができるみたいです。
それ以外の値または_AbcGeomScope
が設定されていない場合はconst
扱い、つまりオブジェクト単位のアトリビュートになることがわかります。
今回は、Maya側から書き出されるAlembicがHoudiniでは、restアトリビュートをvar
にしてHoudiniのPointアトリビュート、shop_materialpathをuni
にしてHoudiniのPrimitiveアトリビュートとして認識されるようにしたかったので上記のコードがそのような指定になっています。
では、モデルを用意します。
ここでは、変形する球、シェーダをフェースアサインメントした球、地面の3つを用意しました。
そこにアトリビュートを追加してHoudiniへ書き出したいジオメトリを複数選択した状態で、
**Cacheメニュー→Alembic Cache→Export Selection to Alembic...**を実行します。
下図のように、Attributeを追加します。
_AbcGeomScope
はMayaのAlembicインポータ/エクスポータがアトリビュートの種類を判別する際に必要なだけなのでrest
とshop_materialpath
だけでいいです。
File FormatはOgawaが必須です。ArnoldはOgawaにしか対応してないです。
フェース単位のアトリビュートのアサインメントがされていればWrite Face Setsが必要になります。
Mayaから出力されたAlembicファイルをHoudiniにインポートすると、Rest Positionがrest
Pointアトリビュートとして取り込まれました
shop_materialpath
Primitiveアトリビュートも読み込まれています。
このパスにシェーダを配置することで、マテリアルの再アサインメントは不要になります。
/shop
コンテキストにシェーダを配置します。
Arnold Python APIを使用すれば、Mayaから書き出したassファイルのシェーダ情報を分析して、それをHoudini上でシェーダグラフとして再現することができます。
その方法は申し訳ないですが公開できないです。後述のArnold Python APIを参考に各自挑戦してみるといいでしょう
(2019年12月13日追記)HtoA5.0.0(Arnold6)からassファイルをシェーダグラフとして構築する機能が追加されました。
https://docs.arnoldrenderer.com/display/A5AFHUG/HtoA+5.0.0
シェーダを配置したらArnold ROPでレンダリングしてみると、Alembicを通常のHoudini Geometryとして読み込めばレンダリングされますが、パックされたままだとシェーダが反映されません。
Packed Alembicに対してシェーダを割り当てる方法は、
- material_attributeを利用する
- MaterialXを使用する
- aiOperatorを使用する
これらのどれかを使うことで可能です。
MaterialXとaiOperatorを使った方法は、現在のところオブジェクトレベルのアサインメントしかできないです。
material_attributeを利用したシェーダアサインメント
material_attribute機能は、Packed Alembic内部に定義されているshop_materialpathを認識してシェーダをアサインメントすることができます。
これは前々からボクがやりたかったことなのですが、今年にリリースされたHtoA4.2.0から可能になりました。
https://docs.arnoldrenderer.com/display/A5AFHUG/HtoA+4.2.0
このリリースノートに、
Add per primitive shop_materialpath translation in the alembic procedural
と書かれています。
そう、Mayaでshop_materialpath
アトリビュートを追加した理由がこれです。
Arnold ROPのExport Referenced Materials
にチェックを付けるだけで、Packed Alembic内部のshop_materialpath
アトリビュートのパスを見てシェーダを割り当ててくれます。
(2023年10月4日追記)
HtoA6.2.4.0で長らく放置されていたバグが修正されました。
HTOA-1852 - Fixed wrong shader assignments with per-primitive shop_materialpath and alembic procedural
MaterialXを利用したシェーダアサインメント
MtoAでもHtoAでもMaterialXエクスポータがあります。あまりにもしょぼいのでボクは独自でエクスポータを作っていますが、ここでは標準のMaterialXエクスポータを使ってPacked Alembicにシェーダアサインメントする方法について説明します。
MaterialXって何?って人は、 https://www.materialx.org/ を参考にください。
仕様書の日本語化も実はボクが翻訳しています。 https://materialx.prisms.xyz/
Maya上でMaterialXに出力したいジオメトリを複数選択した状態で、ArnoldメニューのUtilities→Export Selection to MaterialXを実行します。
FilenameにMaterialXファイル(
.mtlx)の出力先
を、Look Nameに適当な名前
を設定してExportするだけです。
すると、こんな感じのXMLファイルが生成されます。
xi:include
エレメントでシェーダスキーマを定義します。カスタムシェーダを追加した場合はここに新しくシェーダスキーマを追加します。
注目すべきなのは、<materialassign>
エレメントのgeom
アトリビュートの値です。
MaterialXは、このgeom
アトリビュートで指定されたワイルドカードでPacked Alembicの内部ジオメトリ階層にアクセスすることができます。
標準のMaterialXが出力するこの値、、、不便です。今回はテキストエディタでgeom="/ごにょごにょ"
の部分をgeom="*/ごにょごにょ"
に書き換えてしまいましょう。
Arnold ROPにMaterialX ROPを接続して、SelectionにAlembicオブジェクトのパス、MaterialX FileにMaterialXファイルのパス、Lookにルック名を設定してレンダリングするだけでシェーダグラフとシェーダアサインメントが自動的に処理されます。
MaterialXの良いところは、シェーダグラフをHtoA上で再構築する必要がないし、シェーダアサインメントをワイルドカードでできるので階層を意識する必要がないことです。
レンダリング結果からわかるように、フェース単位のシェーダアサインメントができないです。
aiOperatorを利用したシェーダアサインメント
material_attributeならフェース単位のアサインメントができるものの、実際のシェーダグラフを/shopコンテキストで構築する必要があり、そのハードルは高いように感じます。
MaterialXは手軽ではあるものの、アーティストがそのXMLを編集するのは辛いでしょう。
aiOperatorを利用すれば、少し勉強すればアーティストでも編集できるし、Mayaのシェーダグラフをassファイルで取り込んで制御することができます。
Arnoldを触っていると、assファイルって便利だなって思うんですね。
assファイルは主にシーン記述をするのに使用しますが、それだけじゃなくて、シェーダグラフを定義したファイルとして用意したり、ライトを定義したファイルとして用意したり、レンダリング設定を定義したファイルとして用意しても構わないのです。
aiOperatorを使って、Mayaのシェーダグラフをassファイル経由で取り込む方法について説明します。
まずは、Maya上で、シェーダだけのassファイルを出力します。
Arnoldメニュー→StandIn→Export StandInを実行します。
Exportの項目でShaders
のみをチェックしてassファイルを書き出します。
このファイル名は、例えばShadersOnly.ass
とかにしておきます。
Houdini上で、/obj
コンテキストにArnold Includeオブジェクトを配置します。ここにShadersOnly.ass
ファイルを指定します。
これによって、Mayaのシェーダグラフをインポートすることができます。
Arnold Includeで取り込まれたシェーダグラフを任意のオブジェクトに割り当てるには、/out
コンテキスト内でaiOperatorのSet Parameter ROPを追加し、それをArnold ROPに接続します。
そして、Selectionにはシェーダを割り当てたいオブジェクトのパス
、Assignment_1パラメータにshader[0]="assファイルに記述されているシェーダ名"
を記述します。
これでレンダリングしてみると、シェーダを割り当てることができましたね。
他のオブジェクトにも同様の操作でArnold ROPに接続していけば、シェーダを割り当てることができます。
え?
面倒くさいって?
まぁ、そうでしょうね。
なので、assファイルを解読してaiOperatorを自動でセットアップするスクリプトを作成しましょう。
ここで必要になるassファイルは、シェーダ情報とそのシェーダをバインドしているシェイプです。
Arnold Includeで読み込んだassファイルはシェーダ情報しか入っていなくて、どのシェーダをどのシェイプにバインドさせるのか情報がありません。
そのため、再度Export StandInを実行して、
Exportの項目で、Shapes
とShaders
のチェックを付けてassファイルを書き出します。例えば、ShapesShaders.ass
とかにしておきます。
import arnold
arnold.AiBegin()
assFilePath = "F:/AdventCalendar2019/ShapesShaders.ass"
dicShapes={}
universe = AiUniverse()
result = arnold.AiASSLoad(universe,assFilePath)
nodeIter = arnold.AiUniverseGetNodeIterator(universe,arnold.AI_NODE_SHAPE)
while not arnold.AiNodeIteratorFinished(nodeIter):
node = arnold.AiNodeIteratorGetNext(nodeIter)
nodeName = arnold.AiNodeGetName(node)
nodeEntry = arnold.AiNodeGetNodeEntry(node)
if nodeName == "root" or nodeName == "":
continue
dicShapes[nodeName]={}
parmIter = arnold.AiNodeEntryGetParamIterator(nodeEntry)
while not arnold.AiParamIteratorFinished(parmIter):
parm = arnold.AiParamIteratorGetNext(parmIter)
parmName = arnold.AiParamGetName(parm)
if parmName == "shader":
dicShapes[nodeName][parmName] = []
parmArray = arnold.AiNodeGetArray(node,parmName)
for arrayIndex in range(arnold.AiArrayGetNumElements(parmArray)):
dicShapes[nodeName][parmName].append(arnold.AiNodeGetName(arnold.AiArrayGetPtr(parmArray, arrayIndex)))
elif parmName == "visibility":
dicShapes[nodeName][parmName] = arnold.AiNodeGetInt(node,parmName)
arnold.AiParamIteratorDestroy(parmIter)
arnold.AiNodeIteratorDestroy(nodeIter)
subnet = hou.node("/out").createNode("subnet","setParms")
merge = subnet.createNode("merge","Merge")
for shapeIndex,eachShape in enumerate(dicShapes):
setParameterNode = subnet.createNode("set_parameter",eachShape)
merge.setInput(shapeIndex,setParameterNode)
setParameterNode.parm("selection").set("*/"+eachShape)
setParameterNode.parm("assignment").set(1)
setParameterNode.parm("assignment_1").set("visibility="+str(dicShapes[eachShape]["visibility"]))
for shaderIndex,eachShader in enumerate(dicShapes[eachShape]["shader"]):
setParameterNode.parm("assignment").set(shaderIndex+2)
setParameterNode.parm("assignment_"+str(shaderIndex+2)).set("shader["+str(shaderIndex)+"]=\""+str(dicShapes[eachShape]["shader"][shaderIndex])+"\"")
このスクリプトを実行してみると、/out
コンテキスト内にsetParmsサブネットが生成され、その中にassファイルで定義されたオブジェクトの数だけSet Parameter ROPが作成されマージされています。
このサブネットをArnold ROPに接続します。
これでレンダリングできました。
MaterialXと同様にaiOperatorはオブジェクトレベルのシェーダアサインメントしか対応していないので、このような結果となります。
とはいえ、フェース単位のシェーダアサインメントをしていないモデルであれば、assファイルを利用してArnold Python APIを使用することで、MayaでなくともHoudiniでLookDevすることができます。
検証環境:
Maya2019
MtoA3.3.0.2
Houdini17.5.425
HtoA4.4.1
まとめ
- Mayaでも頂点/フェースアトリビュートを追加することができるので、それをAlembic経由でHoudiniに取り込むことができるよ
- Arnoldを理解するならassを理解するといいよ
- Arnold Python APIの使い方を教えたよ
- assでパイプラインを構築すれば複数のDCC間でレンダリングができるよ
おしまい