29
4

HtoAを使ったAss/Alembicの利用方法

Last updated at Posted at 2019-12-03

はじめに

もう毎年高齢恒例の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ファイルの出力ができません:sob:(Houdini18.0.460以降で出力できるようになりました)

なので、Houdini Core/FX環境、Maya環境(学生版、ホームライセンスでも可)でassの出力をして勉強してください。

Mayaで構築したレンダーシーンをHoudini上でHoudiniレンダーシーンと共存させてレンダリングしたいのであれば、MtoAからassを書き出して、それをHtoAで読めばいいだけです。
MtoA上でArnoldメニュー→Export StandInを実行して、出力したい情報にチェックを付けてassファイルを書き出します。
image.png
HtoAの/objコンテキスト上でArnold ProceduralまたはArnold Includeを追加して、そのassファイルを指定するだけでHoudini上にMayaのArnoldシーンをそのまま取り込むことができます。
Maya固有のシェーダを使用しているシーンだとArnold ROPのPlugin PathにそのDLLのパスを指定する必要があるのですが、バージョンが上がる度にDCC固有シェーダから純粋Arnoldシェーダに切り替わるようになってきています。今後も期待。
image.png
Mayaのシーンをassファイル経由でHoudiniに取り込んで豚さんと共存させた例。
image.png
便利っすね。
ただ、別のDCCのオブジェクトをHoudni上で編集したいとなった場合、現状はAlembic/FBXなどの中間フォーマット(今後はUSD)でHoudiniに取り込んでArnoldシェーダを割り当ててレンダリングすることとなります。
その上で、Maya上で準備しておくと便利なことがあるので、それについて紹介したいと思います。

Houdiniのファイルの拡張子はhip
Arnoldのファイルの拡張子はass
hipとass!!!:thinking:
なんか相性良さそう:spy_tone1:

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を有効にすることです。
image.png
この方法は、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なんかも追加できるのでチャレンジしてみてください。

restとshop_materialpathを追加するPyMELスクリプト
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"])

実行すると、下図のようにカスタムアトリビュートが追加されます。
image.png
通常のアトリビュートだと数値/文字列フィールドの形式で値が表示されると思うのですが、追加した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文字列の値には、vtxfvrunivarを指定することができるみたいです。
それ以外の値または_AbcGeomScopeが設定されていない場合はconst扱い、つまりオブジェクト単位のアトリビュートになることがわかります。
今回は、Maya側から書き出されるAlembicがHoudiniでは、restアトリビュートをvarにしてHoudiniのPointアトリビュート、shop_materialpathをuniにしてHoudiniのPrimitiveアトリビュートとして認識されるようにしたかったので上記のコードがそのような指定になっています。

では、モデルを用意します。
ここでは、変形する球、シェーダをフェースアサインメントした球、地面の3つを用意しました。
そこにアトリビュートを追加してHoudiniへ書き出したいジオメトリを複数選択した状態で、
**Cacheメニュー→Alembic Cache→Export Selection to Alembic...**を実行します。
image.png
下図のように、Attributeを追加します。
image.png

_AbcGeomScopeはMayaのAlembicインポータ/エクスポータがアトリビュートの種類を判別する際に必要なだけなのでrestshop_materialpathだけでいいです。
File FormatOgawaが必須です。ArnoldはOgawaにしか対応してないです。
フェース単位のアトリビュートのアサインメントがされていればWrite Face Setsが必要になります。
Mayaから出力されたAlembicファイルをHoudiniにインポートすると、Rest PositionがrestPointアトリビュートとして取り込まれました:relaxed:
image.png
shop_materialpathPrimitiveアトリビュートも読み込まれています。
このパスにシェーダを配置することで、マテリアルの再アサインメントは不要になります。
image.png
/shopコンテキストにシェーダを配置します。
Arnold Python APIを使用すれば、Mayaから書き出したassファイルのシェーダ情報を分析して、それをHoudini上でシェーダグラフとして再現することができます。
その方法は申し訳ないですが公開できないです。後述のArnold Python APIを参考に各自挑戦してみるといいでしょう:japanese_ogre:

(2019年12月13日追記)HtoA5.0.0(Arnold6)からassファイルをシェーダグラフとして構築する機能が追加されました。
https://docs.arnoldrenderer.com/display/A5AFHUG/HtoA+5.0.0

シェーダを配置したらArnold ROPでレンダリングしてみると、Alembicを通常のHoudini Geometryとして読み込めばレンダリングされますが、パックされたままだとシェーダが反映されません。
image.png
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アトリビュートのパスを見てシェーダを割り当ててくれます。
image.png

(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を実行します。
FilenameMaterialXファイル(.mtlx)の出力先を、Look Name適当な名前を設定してExportするだけです。
image.png
すると、こんな感じのXMLファイルが生成されます。
image.png
xi:includeエレメントでシェーダスキーマを定義します。カスタムシェーダを追加した場合はここに新しくシェーダスキーマを追加します。
注目すべきなのは、<materialassign>エレメントのgeomアトリビュートの値です。
MaterialXは、このgeomアトリビュートで指定されたワイルドカードでPacked Alembicの内部ジオメトリ階層にアクセスすることができます。
標準のMaterialXが出力するこの値、、、不便です。今回はテキストエディタでgeom="/ごにょごにょ"の部分をgeom="*/ごにょごにょ"に書き換えてしまいましょう。
image.png
Arnold ROPにMaterialX ROPを接続して、SelectionにAlembicオブジェクトのパス、MaterialX FileにMaterialXファイルのパス、Lookにルック名を設定してレンダリングするだけでシェーダグラフとシェーダアサインメントが自動的に処理されます。
image.png
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とかにしておきます。
image.png

Houdini上で、/objコンテキストにArnold Includeオブジェクトを配置します。ここにShadersOnly.assファイルを指定します。
これによって、Mayaのシェーダグラフをインポートすることができます。
image.png

Arnold Includeで取り込まれたシェーダグラフを任意のオブジェクトに割り当てるには、/outコンテキスト内でaiOperatorのSet Parameter ROPを追加し、それをArnold ROPに接続します。
そして、Selectionにはシェーダを割り当てたいオブジェクトのパスAssignment_1パラメータにshader[0]="assファイルに記述されているシェーダ名"を記述します。
image.png
これでレンダリングしてみると、シェーダを割り当てることができましたね。
image.png
他のオブジェクトにも同様の操作でArnold ROPに接続していけば、シェーダを割り当てることができます。

え?:ear:
面倒くさいって?

まぁ、そうでしょうね。
なので、assファイルを解読してaiOperatorを自動でセットアップするスクリプトを作成しましょう。
ここで必要になるassファイルは、シェーダ情報とそのシェーダをバインドしているシェイプです。
Arnold Includeで読み込んだassファイルはシェーダ情報しか入っていなくて、どのシェーダをどのシェイプにバインドさせるのか情報がありません。
そのため、再度Export StandInを実行して、
Exportの項目で、ShapesShadersのチェックを付けてassファイルを書き出します。例えば、ShapesShaders.assとかにしておきます。
image.png

aiOperatorを自動セットアップするPythonスクリプト
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が作成されマージされています。
image.png
このサブネットをArnold ROPに接続します。
これでレンダリングできました。
image.png
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間でレンダリングができるよ

おしまい:hugging:

29
4
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
4