へ～、Pythonの中からSOPが呼べるんですね～

殺人的な猛暑日の続く夏からいきなり冬になった感のある今日此の頃ですが、皆様如何お過ごしでしょうか？
Houdini触ってますか？

今日はPythonSOPについてお話したいと思います。

__wrangle超速い、Pythonめっちゃ遅い！__
__wrangle超短く書ける、Python超ゲロクソ長いコードになっちゃう～！__

そうお考えの方も多いと思います。
Pythonさん、実はいろいろ出来るんですよ。
器用な人なんです。

---

ある日ふとPythonSOPの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/a90d5b00-7165-f0d9-a2f4-ae54e6e00efa.png)
Pythonコードの右上の▼を押して眺めてたら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/9d4f1c9e-029b-5776-0584-ff774ab457a4.png)
あれ？下の２ついつの間にか増えてる？？？
なんだコレ？
Verbってことは挙動？SOPとして動いてくれるってこと？
ApplySubdivide SOP Verb にして上にBOXを繋いでみたら
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/4f8cd521-04cb-e2c4-81c3-16e31dc26076.png)
SubdivideSOPが無いのにPythonの中でSubdivideやってくれてる～！

じゃぁテキストのリスト読んでFileSOPで配置とか軽く出来そうだし
いろいろ生成したものを条件付けて吐き出すのも簡単そう！
やるぅ～！

このスニペットでは

```
verb = hou.sopNodeTypeCategory().nodeVerb("subdivide")

verb.setParms( { 'iterations' : 1 } )

geo.execute(verb)
```
SOPの指定、パラメータの指定、実行という流れで行っています。
大変わかりやすいですね。

分かりづらいのはここ

```
node.geometry().clear()
node.geometry().merge(geo)
```

ここを

```
geo.clear()
geo.merge(geo)
```
こう書き換えても動作しないんですよ。
動作しないというのはBOXの結果のまま変化が無いということです。

ちょっと紛らわしいんですが
geoの中にはSOPの計算結果の形状が入っています。
しかしまだ実際の形状としては反映されません。

```
node.geometry().clear()
```
この行で一旦ジオメトリを消して
```
node.geometry().merge(geo)
```
計算結果を実形状にマージして完成という流れになります。

geoはあくまでメモリ上のインスタンスということなんでしょうか。

私が紛らわしいと思った理由はあります。
別の上から二番目のPythonスニペットでこう書かれています。

```
# Move Points Up
node = hou.pwd()
geo = node.geometry()

# Add code to modify contents of geo.
for point in geo.points():
    pos = point.position()
    pos += hou.Vector3(0, 1, 0)
    point.setPosition(pos)
```
ぱっと読んですぐに分かると思いますがこれは全頂点をY+方向に1だけ動かす簡単な例です。
これは実際の形状の頂点を直接動かしています。

それに対して
```
geo.execute(verb)
```

は実際の形状にすぐには影響を与えません。

なので紛らわしいなと思った次第です。
簡単なスニペットで出来ることだけでなく扱い方の違いまで
考えが及ぶ様に用意されているのは有り難いことですね。

geoというインスタンスは

* 現在の形状を変更(点の移動)出来る、実形状とリンクしている状態

* geo.execute(verb)の結果は形状を変化させずに
メモリの中にだけSOPの計算結果を保持しておくにとどまり
SOPノード自体は存在せず実行だけを行う

ということがスニペットを通じて理解出来ました。

------

では軽く実践してみます。

https://3dnchu.com/archives/cg-test-models/

3dnchuさんのこの記事で紹介されているモデルで適当にうさぎなり龍なりを落としてきて

それをcsvファイルにリストしておいて

PythonSOP内で読み込みとID付けとパック化を行い

点群に割り当ててみることとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/9ab20d8d-ae9c-7eb5-233c-cd030fc1e82c.png)

まず名前とパスが書かれたcsvを用意します。
そしてこれらをHoudiniに読み込むためにPythonSOPを一個作ってその中に下記のコードを書きます。

```
import csv

models = {}
with open('d:/models/test/models.csv') as f:
    rd = csv.reader(f)
    for i in rd:
        models[i[0]] = i[1]
node = hou.pwd()

verb1 = hou.sopNodeTypeCategory().nodeVerb("file")
verb2 = hou.sopNodeTypeCategory().nodeVerb("xform")

for k,v in models.items():
    geo = node.geometry()
    # ファイル名指定、パック化、見た目フルジオメトリ
    verb1.setParms( { 'file' : v, 'loadtype' : 6 ,'viewportlod' : 0 } )
    geo.execute(verb1)

    bbox = geo.boundingBox().sizevec() # 大きさ取得
    minp = geo.boundingBox().minvec() # 位置調整
    # 接地させて大きさ調整
    verb2.setParms( {'xOrd':5, 'scale' : 1/min(bbox) , 't' : (0,-1*minp[1],0) } )
    geo.execute(verb2)
    
    geo.addAttrib(hou.attribType.Point,'name',k)
    pnt = geo.points()[0]
    pnt.setAttribValue('name',k) # 名前を付けておく
    node.geometry().merge(geo)

```
このコードを書いたPythonSOPにimport_assetsなどの名前を付けて名前アトリビュートの付いた点群に
CopyToPointsをするとこの様な結果になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/f0710911-47b8-1206-9b42-78052eced3e6.png)




１つ目のverbでFileSOPを指定します。
setParmsで使用されているパラメータ名の調べ方ですが
```
    verb.setParms( { 'file' : v, 'loadtype' : 6 ,'viewportlod' : 0 } )
```
fileの項目は言わずもがな２つ目はFileSOPを実際に出して
Loadのパラメータの上にカーソルを置くとバルーンが出てパラメータの詳細が出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/77130398-605a-53bf-e468-a47a5ea930c3.png)
Loadの下にParameter : loadtypeとありますのでそれがパラメータ名です。
値が６となっているのは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/1844f471-3a85-5a0c-2ae7-a59334f99a60.png)
Loadのプルダウンメニュー、Houdini風に言うとオーダードメニューの一番下の
PackedGeometryが上から数えて７番目で一番上を０として数えるので６になります。

３つ目のviewportlodはLoadをPacked系にした時にだけ現れるDisplay asのパラメータです。
パックした際の見た目を決定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/2aea6d7f-d395-5ade-d0be-a08f6e757624.png)
これは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/7e5ec95a-5ad0-cade-42d5-74b13f6690cd.png)
一番上のFullGeometryで良いので０でOKです。

```
    bbox = geo.boundingBox().sizevec() # 大きさ調整
    minp = geo.boundingBox().minvec() # 位置調整
```
boudingBox系のクラスのメソッドを使って情報を取っています。
この情報は次のTransformSOPで使います。
boudingBoxクラスのメソッドの詳細についてはこちらを御覧ください。
https://www.sidefx.com/docs/houdini/hom/hou/BoundingBox.html

次に読み込んだモデルの位置やサイズ調整をします。
```
    verb2 = hou.sopNodeTypeCategory().nodeVerb("xform")
    verb2.setParms( {'xOrd':5, 'scale' : 1/min(bbox) , 't' : (0,-1*minp[1],0) } )
    geo.execute(verb2)
```
おや？transformじゃなくxformになっているぞ…？
TransformSOPの名前は実はtransformじゃありません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/051f4bc5-d770-2fe3-a5e0-60385468c2fd.png)
何を馬鹿なことを言ってるんだTransformSOPの名前はtransformに決まってるじゃないか、
などと思われることでしょう。
実は違うんです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/c4775974-5072-86a1-2d85-586dcc2081bf.png)
メインメニューからAssets -> Asset Definition Toolbar -> Show Menu Always を設定して下さい。
すると・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/79b89b49-d5d3-df0a-5bf7-e23dddd1efae.png)
Asset Name のところに __xform__ と書かれているのが分かります。

罠ですね。

引っかからない様にしましょう、
こんな罠にいちいち引っかかっている様では生き残れませんよ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/f735f033-1914-2ef3-266e-57a06b1c78ee.png)
この辺も生存バイアス風味ではありますがフロムゲーよりはマシなので泣き言はほどほどにしときましょう。
俺達はフィーリングでHoudiniを触っている。

```
   verb2.setParms( {'xOrd':5, 'scale' : 1/min(bbox) , 't' : (0,-1*minp[1],0) } )
```
xOrdが5というのはTrans Rot Scaleの順で計算するということです。
おおむね一番サイズの小さい軸でスケールを合わせておけば良い感じだったので今回はUniformScaleで
`1/min(bbox)`
としました。
もし軸ごとにスケールを変えたい場合はsx,sy,szみたいな指定ではなく移動と同じ様に
sでベクターのタプルで指定してあげて下さい。
移動に関してはY座標の一番低いところをXZ平面に合わせる様にしています。

あとは名前のアトリビュートをポイントに設定してCopyToPointに渡すだけですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/8784d76b-1996-7282-a555-be489919b860.png)

Copy To Point SOP にはPiece Attributeを設定して下さい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/a24ad3e5-3f79-b51a-3841-3007e0fab3b3.png)

Copy To Point SOP の２つ目の入力のPoint Wrangleには下記の様に書いて
それぞれのPointにnameアトリビュートを設定しておくと良い感じにバラバラに配置できます。

```
string names[] = {"blub","bob","bunny","dragon","happy","spot"};

int rnd = int(rand(@ptnum+chi("seed"))*5.99999);

s@name = names[rnd];
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/67faea02-a14b-1c5b-1345-2523cffe3842.png)
Scatterの代わりに Scatter And Align を使っても便利です。
回転や大きさなどを簡単に良い感じにバラバラに出来ますし重ならないように
調整しやすいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153910/434bc738-33e2-ee43-7e37-04b6406c3e3b.png)

---
いかがでしたか？
もうFileSOPをモデルの数だけシーンの最初に置くとかやらなくていいんです
そうPythonならね。
何千回何万回と頂点に対して処理する様な処理は弱いですがSOPを扱ったり
外部ファイルとの連携、機械学習との連携などPythonならではのめちゃつよ成分がありますので是非とも使い込んで日々の業務の糧として下さい。
では急に寒くなったんで体調管理などお気を付けてお過ごし下さい。

---

配布物

hipは[ここ](https://drive.google.com/file/d/1iuOp7Nj08kIIa146aU5FhAddvwTJUURW/view?usp=sharing)です。(hiplc)