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

# はじめに
もう毎年~~高齢~~恒例の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++だけでなく**O**pen **S**hading **L**anguage(OSL)でシェーダを書くことができたり、MaterialX(後述で触れます)にも早く対応していたり、次はUSDですかね。楽しみです。
Houdini標準のMantraレンダラーはHoudiniユーザーには無償で好きなだけ使える太っ腹仕様で、シェーダも馴染みのあるVEXで書きやすいので私は気に入ってるのですが、Houdiniでしか動かないというデメリットが辛いです。
Arnoldは、標準化されたシーン記述フォーマットであるass(**A**rnold **S**cene **S**ource)ファイルでレンダリング(これを**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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/8e214dcb-9322-8184-b683-4b3e46e22fde.png)
HtoAの`/obj`コンテキスト上で**Arnold Procedural**または**Arnold Include**を追加して、そのassファイルを指定するだけでHoudini上にMayaのArnoldシーンをそのまま取り込むことができます。
Maya固有のシェーダを使用しているシーンだとArnold ROPの`Plugin Path`にそのDLLのパスを指定する必要があるのですが、バージョンが上がる度にDCC固有シェーダから純粋Arnoldシェーダに切り替わるようになってきています。今後も期待。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/2f6fbf6f-9328-5239-ae4f-1d657637083d.png)
Mayaのシーンをassファイル経由でHoudiniに取り込んで豚さんと共存させた例。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/7136a6fc-38e4-78e2-0954-72d2bb09e430.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/ced5880f-419e-83bb-afc1-33087ccb43d7.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なんかも追加できるのでチャレンジしてみてください。

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/28346d16-ddb6-eb53-8eb3-8d4fe23221bd.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`文字列の値には、`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...**を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/c48e7294-c80a-0563-1524-f41b4c4e1bf0.png)
下図のように、Attributeを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/bfebe1a1-56da-5615-08b2-d3960e3c1e77.png)

`_AbcGeomScope`はMayaのAlembicインポータ/エクスポータがアトリビュートの種類を判別する際に必要なだけなので`rest`と`shop_materialpath`だけでいいです。
**File Format**は**Ogawa**が必須です。Arnoldは**Ogawa**にしか対応してないです。
フェース単位のアトリビュートのアサインメントがされていれば**Write Face Sets**が必要になります。
Mayaから出力されたAlembicファイルをHoudiniにインポートすると、Rest Positionが`rest`Pointアトリビュートとして取り込まれました:relaxed:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/687f7aff-35f0-e071-c50f-06c0613b3b20.png)
`shop_materialpath`Primitiveアトリビュートも読み込まれています。
このパスにシェーダを配置することで、マテリアルの再アサインメントは不要になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/d16bccc7-0817-d7fc-0820-370c2b34406c.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/8e2b99a4-03d7-c99b-0250-ef9951b98689.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/507e4ef5-2551-3a87-babb-d82f0ee80e31.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**を実行します。
**Filename**に`MaterialXファイル(`.mtlx`)の出力先`を、**Look Name**に`適当な名前`を設定して**Export**するだけです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/bc624776-735f-762a-f092-643a05fb4829.png)
すると、こんな感じのXMLファイルが生成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/03a8da63-ff1c-2d80-4e65-58b75bc0a3a7.png)
`xi:include`エレメントでシェーダスキーマを定義します。カスタムシェーダを追加した場合はここに新しくシェーダスキーマを追加します。
**注目すべきなのは、`<materialassign>`エレメントの`geom`アトリビュートの値です。**
MaterialXは、この`geom`アトリビュートで指定された*ワイルドカード*でPacked Alembicの内部ジオメトリ階層にアクセスすることができます。
標準のMaterialXが出力するこの値、、、不便です。今回はテキストエディタで`geom="/ごにょごにょ"`の部分を`geom="*/ごにょごにょ"`に書き換えてしまいましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/5cff774b-fff9-f00f-49d9-7e3da695d235.png)
Arnold ROPにMaterialX ROPを接続して、SelectionにAlembicオブジェクトのパス、MaterialX FileにMaterialXファイルのパス、Lookにルック名を設定してレンダリングするだけでシェーダグラフとシェーダアサインメントが自動的に処理されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/2e12cdeb-790b-1688-217e-f9622f9e64e6.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/13fb0179-84af-efa8-e690-c13c1f656b3f.png)

Houdini上で、`/obj`コンテキストに**Arnold Include**オブジェクトを配置します。ここに`ShadersOnly.ass`ファイルを指定します。
これによって、Mayaのシェーダグラフをインポートすることができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/5089fcc8-fa68-8600-5236-4eccc176b270.png)

**Arnold Include**で取り込まれたシェーダグラフを任意のオブジェクトに割り当てるには、`/out`コンテキスト内でaiOperatorの**Set Parameter ROP**を追加し、それを**Arnold ROP**に接続します。
そして、**Selection**には`シェーダを割り当てたいオブジェクトのパス`、**Assignment_1**パラメータに`shader[0]="assファイルに記述されているシェーダ名"`を記述します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/3dd4b146-5a31-5f6d-9dfb-0acf2fcee30f.png)
これでレンダリングしてみると、シェーダを割り当てることができましたね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/d9a730a5-b234-b93c-9b2d-464892364859.png)
他のオブジェクトにも同様の操作でArnold ROPに接続していけば、シェーダを割り当てることができます。


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

まぁ、そうでしょうね。
なので、assファイルを解読して**aiOperator**を自動でセットアップするスクリプトを作成しましょう。
ここで必要になるassファイルは、シェーダ情報とそのシェーダをバインドしているシェイプです。
**Arnold Include**で読み込んだassファイルはシェーダ情報しか入っていなくて、どのシェーダをどのシェイプにバインドさせるのか情報がありません。
そのため、再度**Export StandIn**を実行して、
Exportの項目で、`Shapes`と`Shaders`のチェックを付けてassファイルを書き出します。例えば、`ShapesShaders.ass`とかにしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/1ecb9e31-8526-024f-f476-f8ec674136ea.png)

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/58c9d2c2-e181-cc60-65c8-66c9cc5b613f.png)
このサブネットをArnold ROPに接続します。
これでレンダリングできました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154191/81f76f68-4844-5d65-42af-8147fe32d26e.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: