Rig PoseノードからHDAへの大量のパラメータ追加を楽に行う

この記事は [Houdiniアドベントカレンダー2021](https://qiita.com/advent-calendar/2021/houdini) 9日目の記事です。

# 0. はじめに
Kinefxでリグを作成する際に複数のRig Poseのパラメータを全部promoteして分かりやすい名前に変更するのに地味に時間がかかるのでスクリプト化を試みました。また公式やフォーラムにパラメータ周りのクラスに関するまとまった情報がなかったため、他のパラメータ周りの自動化の一助にもなればと思い記事を作成しました！

# 1. 対象読者
Pythonの基本的な文法を知っている（簡単なクラスを作ったことがある程度）Houdiniが好きな方。

# 2. この記事で作るもの
この記事では選択したRig Pose ノードのパラメータたちを一斉に親HDAに追加（promote）するスクリプトを組んでいきます。下はParameter Interfaceが空の状態から本スクリプトを一つのノードに対して実行した結果になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/6739b46d-f679-cf44-0776-f7748edcded7.png)

# 3. どうやって作るのか
今回触れるパラメータ関連のクラスはParameter Interfaceとの対応によって以下のように整理できます。公式では[ここ](https://www.sidefx.com/docs/houdini/hom/hou/ParmTemplate.html)の説明が比較的詳しいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/78ca4771-f4d2-9604-ff0c-ea2464717ab2.png)


ここでは役割の違いを明確にするためクラスを便宜的にBlueprint側とValue側というように分けています。

Value側のParmTupleやParmはパラメータの実際の値を保持するクラスでParameter Interface上で表示されている値の情報などを持っており、Blueprint側のParmTemplateGroupやParmTemplateはHDAがどのようなパラメータをもっているか、またParameter Interface上でのパラメータのレイアウトに関する情報などを保持しています。

したがって、Value側は画面上のノード一つごとにインスタンスが存在しノードの状態と値が連動しており、Blueprint側はHDAの定義に使われているためノードの一つ一つの状態とは独立しており、設計図やそれこそテンプレートのような役割を果たします。

今回のゴールはよしなにパラメータを追加したParmTemplateGroupをHDADefinitionクラスのsetParmTemplateGroupという関数を使って自分たちのHDAにセットしてあげることでParameter Interfaceを更新することになります。

# 4. 作る

以下では実際にRig Pose Nodeのパラメータたちを一気にHDAに追加するスクリプトを書いていきます。

またここでは例として [Sidefx公式チュートリアル](https://www.sidefx.com/tutorials/kinefx-rigging-fur-dude/) で作ることができるFur Dudeを取り上げます。以下はパラメータをすべて削除したFur Dudeが入ったプロジェクト一式へのリンクになります。
https://www.dropbox.com/sh/1o9hp54iq7aexrf/AADptPvP2j9DoFUKjlulVw6Ia?dl=0

## 4.1 環境
- PC: Windows 10
- Houdini: 19.0.383 Python 3

## 4.2 Shelfの追加

シェルフを適当な名前で追加し、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/a59fb79c-d0e4-1952-22e3-0f55e1aa0be3.png)

新しいツールを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/c67aa1f1-64b5-b1cb-e62a-299ae0c0e018.png)

新しく作成したツールを右クリック>Edit Toolからツールの編集画面に飛び、Scriptタブに移ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/c9ec8341-2f37-9ff8-39f5-72998aa74588.png)
例えばここに

```python
print("hello")
```

と入力し、Acceptを押した後ツールをクリックし実行すると、、、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/12b2fa39-d209-350a-9078-bb42035b38f6.png)

きっとこうなります。以下でコードを実行するといった場合にはApplyを押した後ツールをクリックすることを指します。

またパッとコードの挙動を確認したい時などには Python Shellも便利です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/9af390cb-cf82-fb1f-3b14-9d384e9231f5.png)


## 4.3 選択したノードから情報を取得
PythonからHoudiniを触りたいときにはhouモジュールを使います。例えば選択中 (ノードが黄色くアウトラインされている状態)のノードの情報を取得したいときには、

```python
import hou

node = hou.selectedNodes()[0]
print(node.name())
```
を実行すると名前を取得することができます。（Python Shellを開いていた場合Python Shellに結果が出力されます。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/0db30376-c258-8cf5-b3d1-e6b7fde0eee1.png)

(開いていなかった場合ポップアップで出力されます。)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/e65d5a3b-03f3-0c79-41f4-3cc72c644ae9.png)

## 4.3 HDAにパラメータを追加
以下ではまず例として transform1 のTranslateをHDAのパラメータとして追加します。
これを行うにはtransform1からTranslateをParmTupleとして取り出し対応するParmTemplateを取得、これをtransform1の親ノードに当たるfur_dude_rig_anim_1のHDAのParmTemplateGroupに追加する必要があります。(適時ページ上部のクラスの関係を表した図を参照してください \_　\_ ) 

```python
import hou

node = hou.selectedNodes()[0]

# 親ノードをたどりHDAの定義などを取得
hdaDef = node.parent().type().definition()
parmTempGroup = hdaDef.parmTemplateGroup()

# 選択されたノードからパラメータ定義の取得
parmTuple = node.parmTuple("t")
parmTemp = parmTuple.parmTemplate()

# HDAに新しいパラメータを追加
parmTempGroup.append(parmTemp)

hdaDef.setParmTemplateGroup(parmTempGroup)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/3af2d1b5-3673-1e9c-e654-2989a28931e8.png)
一方でこのままではHDAのパラメータとtransform1のパラメータが連携できていないため、transform1のtx, ty, tzからコードで追加したパラメータを参照する必要があります。また名前の重複によるエラーを避けるため以降コードを実行する前に毎度Type Propertiesからパラメータをすべて削除してください。

```python
import hou

node = hou.selectedNodes()[0]

hdaDef = node.parent().type().definition()
parmTempGroup = hdaDef.parmTemplateGroup()

parmTuple = node.parmTuple("t")
parmTemp = parmTuple.parmTemplate()

parmTempGroup.append(parmTemp)

# Translateの各パラメータtx, ty, tzそれぞれから対応するHDAのパラメータを参照する。
for parm in parmTuple:
    hdaParmName = parm.name() # transform1とHDAのパラメータの名前が一緒なのでこれで行ける
    parm.setExpression(f'ch("../{hdaParmName}")')

hdaDef.setParmTemplateGroup(parmTempGroup)
```

これで一つのパラメータに関しては無事追加できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/d33809c2-dbcf-a6b5-c50f-1acb69ddc2fd.png)


## 4.4 HDAに複数パラメータを同時に追加
ここでより本題に近づいて rigpose_testの全てのTransformationsのTranslate, Rotate, Scaleを一斉にHDA追加します。
TransformationsはMultiparm Block という種類のフォルダで、eval関数を通して現在のフォルダー内のパラメータ集合の数を知ることができます。そのほかにも [公式ドキュメントのこのページ](https://www.sidefx.com/docs/houdini/hom/hou/Parm.html)のMultiparmsセクション下の関数を使って情報を取得できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/0447579a-18b4-5fc4-1d2c-ef9bab01ac32.png)
Multiparm Block内部のパラメータは名前にindexが含まれている(以下画像では1)ため、この性質を利用して全パラメータを走査していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/752394c2-6c28-a5e6-a1ad-80aaeee94be9.png)



```python
import hou

node = hou.selectedNodes()[0]

hdaDef = node.parent().type().definition()
parmTempGroup = hdaDef.parmTemplateGroup()

prefixes = ['t', 'r', 's'] # 今回追加したいパラメータたちの名前
folder_len = node.parm("transformations").eval() # これでMultiparm Blockのパラメータ集合数を取得できる


for i in range(folder_len): # Multiparmの数だけ実行
    for prefix in prefixes:
        parmTuple = node.parmTuple(f"{prefix}{i}")
        parmTemp = parmTuple.parmTemplate()
        
        parmTempGroup.append(parmTemp)
        
        for parm in parmTuple:
            hdaParmName = parm.name()
            parm.setExpression(f'ch("../{hdaParmName}")')
        
hdaDef.setParmTemplateGroup(parmTempGroup)
```

これで無事一度に複数のパラメータを追加できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/c1ee850d-cb25-9426-db79-31391e20eb01.png)

## 4.5 ロックを考慮したりちゃんと名前をつけたり
最後に本題の rigpose1 の全てのTransformationsを一斉にHDAに追加します。
現在のスクリプトをそのまま実行すると以下のようなエラーが表示されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/971c4f61-fa52-2c0e-fc06-2f113f4917c4.png)
これはロックされているパラメータを編集しようとするために発生します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/ef24462a-93ca-251f-23e4-e75bba7dce32.png)
パラメータがロックされているかどうかは `parm.isLocked()` で取得できるため、これを使ってロックされているパラメータに関しては処理をスキップします。

また現状パラメータのラベルがすべてTranslateやRotateと一般的な名前がついていてとても分かりづらいので、ボーンの名前(name attribute)をもとにいい感じの名前に変えてあげます。Rig Poseの各パラメータにはすでにボーンの名前に基づいてAliasという別名的な何かが設定されているので、これを使います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/7bb1cabd-1964-f193-ca75-99d43ea39276.png)

```python
import hou
import re

# ヘルパー関数
def snakeCaseToSpaced(s):
    ''' snakeCaseをspace区切りに変換する
    '''
    words = s.split("_")
    words = list(map(lambda x: x.title(), words))
    return " ".join(words)
    

node = hou.selectedNodes()[0]

hdaDef = node.parent().type().definition()
parmTempGroup = hdaDef.parmTemplateGroup()

prefixes = ['t', 'r', 's']
suffixes = ['x', 'y', 'z']
folder_len = node.parm("transformations").eval()

for i in range(folder_len):
    for prefix in prefixes:
        parmTuple = node.parmTuple(f"{prefix}{i}")
        parmTemp = parmTuple.parmTemplate()
        
        # ロックされているパラメータがあった場合は以降の処理をスキップする
        lock_count = 0
        for parm in parmTuple:
            lock_count += 1 if parm.isLocked() else 0
        if lock_count > 0:
            continue
        
        # GroupのAliasを整形してパラメータの名前の一部として使えるようにする
        groupAlias = node.parm(f"group{i}").alias() # aliasの取得
        groupAlias = re.sub("_group", "", groupAlias) # _余計な文字列の削除
        groupAlias = snakeCaseToSpaced(groupAlias) # よりラベルっぽくsnakecaseをスペース区切りに変換
        
        # 名前とラベルの付け替え
        parmTemp.setName(f"{parmTemp.name()}_{node.name()}") # HDAを通して一意になるように変換
        parmTemp.setLabel(f"{groupAlias} {parmTemp.label()}") # わかりやすいラベルに変換
        
        parmTempGroup.append(parmTemp)
        
        for parm, suffix in zip(parmTuple, suffixes):
            hdaParmName = f"{parmTemp.name()}{suffix}" # パラメータ名を上で変換しているので、一致するように注意する
            parm.setExpression(f'ch("../{hdaParmName}")')
        
hdaDef.setParmTemplateGroup(parmTempGroup)
```

これで無事いい感じの名前を付けながら一度に複数のパラメータを追加できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/54ecab7f-c5e4-3222-faa9-cb4af872842f.png)
ちゃんとリグとして動きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/6739b46d-f679-cf44-0776-f7748edcded7.png)

# 5. おわりに
今回のスクリプトはこのファイルのrigpose_feetのようにParmTupleの中のParmのうち1つだけロックがかかっていない場合などには対応していませんが、以下リポジトリにこういったエッジケースにも対応した実装 （/scripts/python/dabinPromoteParms.py、筆者作）があるので気になる場合はのぞいてみてください。
https://github.com/daisea3e1203/dabinTools
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382078/90c051ee-4f67-f898-a26e-e199fb63b691.png)

ほぼ個人用のパッケージのリポジトリのためこれといったドキュメントはありませんが、他にもいくつかのツールが入っているのでもしよかったら中身をのぞいてみてください。

もしもっとうまいやり方があるよという場合はぜひ教えていただけますと！