新しいバージョンで追加、更新されたノードを取得、作成する

# はじめに
この記事は[**Houdini Advent Calender 2019**](https://qiita.com/advent-calendar/2019/houdini)の10日目の記事です。

先月の終わりに[**Houdini18.0**](https://www.sidefx.com/community/houdini-18-released/)がリリースされました！

もうすでに使ってる方も多いと思いますが、[**新機能ムービー**](https://www.sidefx.com/products/houdini/whats-new-in-18/)で紹介されたノード以外にも細かい更新がされているようです。

そこで、この記事ではHoudini18.0で追加、更新されたノードを取得、作成する方法を紹介します。

# 追加、更新されたノードの定義

調べるために追加、更新の定義を考えます。定義したのは下記の2つです。

- **Houdini17.5のノード全てと比較してHoudini18.0にしか存在しない物**
- **ノードは存在するが新しくパラメータが追加された物**

この条件を元にPythonで追加、更新されたノードをネットワークエディタ状に全て作成したいと思います。  
ちなみに比較するバージョンは**17.5.391**と**18.0.287**を使用しています。

# スクリプトの実行結果

まずスクリプトを実行した結果をご覧ください。
![Get New Nodes](https://dl.dropboxusercontent.com/s/01q7a13djx3165n/NodeCompare.gif?dl=0)

スクリプトを実行すると、どのバージョンと比較するかを選ぶウィンドウが出てきて、選ぶとノードの比較処理が走ります。
処理が終わると、ノード自体が追加された物は**NewNodes**、パラメータが追加されたノードは**NewParmNodes**という**Subnet**が作成され、中に**Sop**などカテゴリごとにノードが出来ています。

**18.0で追加されたSopノード**
![New Sop](https://dl.dropboxusercontent.com/s/exs9g6r51nj4hhx/NewSop.jpg?dl=0)

ちなみに追加されたパラメータには**constant()**というエクスプレッションが入っているので、パラメータのフィルターから**Parameters with Non-Default Values**を選ぶと追加されたパラメータのみ表示出来るようになっています。
![Parameter Filter](https://dl.dropboxusercontent.com/s/9d7p5n2jmr7dmpx/ParmFilter.jpg?dl=0)

スクリプトは下記**GitHub**のリポジトリからダウンロードして使用することが出来ます。

- [**Houdini Create Update Node**](https://github.com/TakashiKoide/HoudiniCreateUpdateNode)

# 追加、更新されたノード一覧

最初は追加、更新されたノードを全て載せようかと思ったんですが、余りにも多かったので、カテゴリ別に追加、更新されたノードの数をまとめました。

## Houdini18.0にしか存在しないノードの数

| Category | Node Count |
|----------|------------|
| Shop     | 1          |
| Chop     | 2          |
| Top      | 12         |
| Object   | 3          |
| Driver   | 6          |
| Cop2     | 1          |
| Sop      | 62         |
| Lop      | 51         |
| Dop      | 12         |
| TopNet   | 2          |

全体で**152個**のノードが追加、更新されています。

新しく追加された**LOP**だけでなく、**SOP**だけでも**62個**のノードが追加、更新されています。

では次にパラメータが追加されたノードの数をご覧ください。


## パラメータが追加されたノードと追加されたパラメータの数

| Category | Node Count | Parameter Count |
|----------|------------|-----------------|
| Top      | 44         | 175             |
| Object   | 1          | 3               |
| Driver   | 1          | 3               |
| Chop     | 1          | 4               |
| Sop      | 43         | 226             |
| Lop      | 24         | 241             |
| Vop      | 10         | 41              |
| Dop      | 63         | 361             |

こちらでは全体で**187個**のノードにパラメータが追加されており、パラメータ数でいうと**1054個**のパラメータが追加されてます。

これを見ると**Lop**は**Houdini17.5**の時点ですでに入ってたんですね。そして**LOP**、**Sop**に限らず、**Top**、**Dop**もかなり更新されてるようです。

もう追える気がしません。

# CSVデータ

ノード名やパラメータ名の一覧も見たいという方の為に下記にCSVファイルをアップしました。
## Houdini18.0にしか存在しないノード

- [CSVをダウンロード](https://dl.dropboxusercontent.com/s/p0jmdyw3f8ai163/NewNodes.csv?dl=0)

- エクセルで開いた図。ノードごとにカテゴリ、ノード名、ノードラベルで分けています。
![Houdini18.0にしか存在しないノード](https://dl.dropboxusercontent.com/s/8fsqx41fp0839lb/NewNodes.jpg?dl=0)

## パラメータが追加されたノード

- [CSVをダウンロード](https://dl.dropboxusercontent.com/s/oznqz53vxpr9q6n/NewParmNodes.csv?dl=0)

- エクセルで開いた図。パラメータごとにカテゴリ、ノード名、ノードラベル、パラメータ名、パラメータラベルで分けています。
![パラメータが追加されたノード](https://dl.dropboxusercontent.com/s/g7jjh7f9d1m1w24/NewParmNodes.jpg?dl=0)

# 追加、更新されたノードを取得する方法

ここからはPythonの解説です。全てを書くのは厳しいので、ポイントになる箇所だけ解説していきます。

## インストールしてるHoudiniのバージョンの取得

まず比較したいバージョンを指定したいので、インストールしてるHoudiniのバージョンを全て取得します。

- **インストールディレクトリの取得**  
Houdiniは起動時に**HFS**という環境変数にインストールディレクトリを登録するので、**os**モジュールで取得します。

```Python
import os
hfs = os.getenv('HFS')
#C:/PROGRA~1/SIDEEF~1/HOUDIN~1.287
```

- **全てのバージョンの取得**  
ここから全てのバージョンを取得するには一つ上のディレクトリに行き、そのフォルダにある全てのフォルダ名を取得します。

```Python
root = os.path.dirname(hfs)
versions = os.listdir(root)
#['Houdini 16.5.268', 'Houdini 17.0.376', 'Houdini 17.5.258', 'Houdini 17.5.360','Houdini 17.5.391', 'Houdini 18.0.287']
```

- **起動してるHoudiniのバージョンを省く**  
これで全バージョンが取れましたが、起動してるHoudiniのバージョンは必要ないので、省きます。  
本来なら**HFS**で取得したパスから省けるんですが、**HFS**は`HOUDIN~1.287`のように略字で返ってくるので、下記の方法で省きました。

```Python
import hou
current_version = hou.applicationVersionString()
versions.remove('Houdini ' + current_version)
```

- **取得したバージョンをUIに表示**  
取得したバージョンを**UI**に表示させてユーザーが選べるようにします。  
**UI**は**PySide**で作るのが一般的ですが、面倒なので[**hou.ui**](https://www.sidefx.com/ja/docs/houdini/hom/hou/ui.html)モジュールの**selectFromList**関数を使用しました。

```Python
sel_version = hou.ui.selectFromList(
    versions, exclusive=True, title='Select Compare Version',
    column_header='Versions', width=240, height=240
)
if not sel_version:
    return
version = versions[sel_version[0]]
version_path = '{}/{}'.format(root, version)
#C:/PROGRA~1/SIDEEF~1/Houdini 17.5.391
```

## 全てのノードタイプの取得

比較する為に全てのノードタイプを取得する必要があります。  

全てのノードタイプを取得するには[**hou.nodeTypeCategories()**](https://www.sidefx.com/ja/docs/houdini/hom/hou/nodeTypeCategories.html)でキーに各カテゴリ名、値に[**hou.NodeTypeCategory**](https://www.sidefx.com/ja/docs/houdini/hom/hou/NodeTypeCategory.html)オブジェクトを持った辞書が返ってきます。
![Node Categories](https://dl.dropboxusercontent.com/s/ddlc8lp12fci76m/NodeCategories.jpg?dl=0)
そして[**hou.NodeTypeCategory**](https://www.sidefx.com/ja/docs/houdini/hom/hou/NodeTypeCategory.html)オブジェクトの[**nodeTypes**](https://www.sidefx.com/ja/docs/houdini/hom/hou/NodeTypeCategory.html#nodeTypes)関数を実行することで、キーにノードタイプ名、値に[**hou.NodeType**](https://www.sidefx.com/ja/docs/houdini/hom/hou/NodeType.html)オブジェクトを持った辞書が返ってきます。

例えばSOPのノードタイプ名を全て取得するには下記コードを実行します。

```Python
import hou
categories = hou.nodeTypeCategories()
sop_category = categories['Sop']
sop_data = sop_category.nodeTypes()
sop_nodes = sop_data.keys()
```

## 全てのパラメータを取得

全てのパラメータを取得するには[**hou.NodeType**](https://www.sidefx.com/ja/docs/houdini/hom/hou/NodeType.html)オブジェクトの[**parmTemplates()**](https://www.sidefx.com/ja/docs/houdini/hom/hou/NodeType.html#parmTemplates)関数を実行することで、基本的に全てのパラメーターの[**hou.parmTemplate**](https://www.sidefx.com/ja/docs/houdini17.5/hom/hou/ParmTemplate.html)オブジェクトを取得することが出来ます。
しかし**Multiparm Block**に入ったパラメータは含まれないため、**Group Promote**などパラメータが**Multiparm Block**に入ってる物は取得することが出来ません。

![Multi Parm](https://dl.dropboxusercontent.com/s/uyg9j4ecvdk3nqb/MultiParm.jpg?dl=0)

**Multiparm Block**に含まれるパラメータも取得するには、下記のように再帰関数を使用する事で取得可能です。

```Python
def get_all_parm_templates(all_parms, node_type):
    parms = node_type.parmTemplates()
    for parm in parms:
        if parm.type() == hou.parmTemplateType.Folder:
            get_all_parm_templates(all_parms, parm)
        elif parm.type() != hou.parmTemplateType.FolderSet:
            all_parms.append(parm)
    return all_parms
```

## ノードタイプとパラメータを全て取得

以上を踏まえてノードタイプとパラメータを全て取得します。

```Python
# -*- coding: utf-8 -*-
import hou

def get_all_parm_templates(all_parms, node_type):
    parms = node_type.parmTemplates()
    for parm in parms:
        if parm.type() == hou.parmTemplateType.Folder:
            get_all_parm_templates(all_parms, parm)
        elif parm.type() != hou.parmTemplateType.FolderSet:
            all_parms.append(parm)
    return all_parms

def main():
    node_data = {}
    categories = hou.nodeTypeCategories()
    for category_name, category in categories.items():
        category_data = []
        nodes = category.nodeTypes()
        for node_name, node_type in nodes.items():
            node_info = {}
            node_info['node_name'] = node_name
            node_info['node_label'] = node_type.description()
            all_parms = get_all_parm_templates([], node_type)
            node_info['parms'] = [parm.name() for parm in all_parms]
            category_data.append(node_info)
        node_data[category_name] = category_data

    return node_data
```

上記コードを実行すると下記のようなカテゴリごとにノード名、ノードラベル、パラメータ名を持つ辞書が返ってきます(PolySplitSopの箇所のみ表示)。

```JSON
"Sop": [
    {
        "node_label": "PolySplit", 
        "parms": [
            "splitloc", 
            "pathtype", 
            "override", 
            "newt", 
            "updatenorms", 
            "close", 
            "tolerance"
        ], 
        "node_name": "polysplit"
    }, 
```

ただこれを実行しても今起動してるバージョンのノード情報を取得出来るだけで指定したバージョンのノード情報を取得出来る訳ではありません。

## 指定したバージョンのノード情報を取得

指定したバージョンのノード情報を取得するために**Hython**を使います。  
**Hython**とは**$HFS/bin**内に置かれてる**Pythonシェル**で、各バージョンごとに存在します。  
起動時に自動で**hou**モジュールを読み込んでくれるため、**Houdiniを起動せずにHoudini独自の処理を実行することが出来ます。**

指定したバージョンの**Hython**でスクリプトを実行するには先ほどのコードを**.py**ファイルで保存し、**subprocess**を使用して**Hython**の引数に指定します。

```Python
import subprocess
from subprocess import PIPE

hython = 'Hythonまでのパス'
script = '実行するスクリプトのパス'
p = subprocess.Popen([hython, script], shell=True, stdout=PIPE, stderr=PIPE)
#スクリプトからの戻り値を取得
stdout, stderr = p.communicate()
#戻ってくる値は文字列なので、evalで辞書に変換
node_data = eval(stdout)
```

しかしこれを実行しても**stdout**には何も返ってこず、**stderr**には次の文字列が返ってきました。

```
'EnvControl: HOUDINI_USER_PREF_DIR missing __HVER__, ignored.\r\nTraceback (most rec
ent call last):\n  File "<string>", line 8, in <module>\n  File "C:/PROGRA~1/SIDEEF~
1/HOUDIN~1.287/houdini/python2.7libs\\hou.py", line 19, in <module>\n    import _hou
\nImportError: DLL load failed: \x8ew\x92\xe8\x82\xb3\x82\xea\x82\xbd\x83v\x83\x8d\x
83V\x81[\x83W\x83\x83\x82\xaa\x8c\xa9\x82\xc2\x82\xa9\x82\xe8\x82\xdc\x82\xb9\x82\xf
1\x81B\nTraceback (most recent call last):\r\n  File "D:\\create_update_node\\get_node_data.py", lin
e 2, in <module>\r\n    import hou\r\n  File "C:/PROGRA~1/SIDEEF~1/HOUDIN~1.287/houd
ini/python2.7libs\\hou.py", line 19, in <module>\r\n    import _hou\r\nImportError: 
DLL load failed: \x8ew\x92\xe8\x82\xb3\x82\xea\x82\xbd\x83v\x83\x8d\x83V\x81[\x83W\x
83\x83\x82\xaa\x8c\xa9\x82\xc2\x82\xa9\x82\xe8\x82\xdc\x82\xb9\x82\xf1\x81B\r\n'
```

エラーを見るとどうやら**hou**モジュールのインポートに失敗してるようです。
### エラーの原因
なぜエラーが出るかと言うと、**Hython**は**hou**モジュールをインポートする時に**$HFS**と**$HFS/houdini/python2.7libs**を見に行きます。
つまり**18.0**から**Hython**を実行すると**18.0**用の**HFS**と**PYTHONPATH**しか認識しないため、インポートエラーが出るという事です。
### 解決方法
これを解決するには**Hython**を実行する前に下記コードを実行して、そのバージョン用の設定に変更する必要があります(安全のため、Hython実行後は元の設定に戻します)。

```Python
import sys

#HFSを取得した比較バージョンのパスで上書き
os.putenv('HFS') = version_path
#houモジュールのインポート時にDLLもロードするため、PATH環境変数も書き換えます。
path = '{}/bin;{}'.format(version_path, os.getenv('PATH'))
os.putenv('PATH', path)
```

## コード全体

コード全体は下記のようになりました(長いので折りたたんでます)。
スクリプトの実行はmain関数を呼び出します。
<details><summary>コード全体を見る</summary><div>

```Python:
# -*- coding: utf-8 -*-
import hou
import os
import subprocess
from subprocess import PIPE

from .get_node_data import get_all_parm_templates

def get_compare_version(hfs):
    version_root = os.path.dirname(hfs)
    versions = os.listdir(version_root)
    current_version = 'Houdini ' + hou.applicationVersionString()
    if current_version in versions:
        versions.remove(current_version)
    #UIのオプション用辞書
    kwargs = {
        'exclusive': True,
        'title': 'Select Compare Version',
        'column_header': 'Versions',
        'width': 240,
        'height': 240
    }
    #バージョンを選択用のリストビューを表示
    sel_version = hou.ui.selectFromList(versions, **kwargs)
    if not sel_version:
        return
    version = versions[sel_version[0]]
    return version

def get_env_from_version(version, hfs, pref_dir):
    old_hfs = '{}/{}'.format(os.path.dirname(hfs), version)
    old_pref_dir = '{}/{}'.format(
        os.path.dirname(pref_dir),
        '.'.join(version.replace('Houdini ', 'houdini').split('.')[:2])
    )
    return old_hfs, old_pref_dir

def set_base_env(path, hfs, pref_dir):
    #環境変数とPythonパスを設定する
    os.putenv('PATH', path)
    os.putenv('HFS', hfs)
    os.putenv('HOUDINI_USER_PREF_DIR', pref_dir)

def get_old_node_data(old_hfs, old_pref_dir):
    script_root = os.path.dirname(__file__)
    script = os.path.normpath(script_root + "/get_node_data.py")
    hython = os.path.normpath(old_hfs + '/bin/hython.exe')
    #hythonに投げる前に必要な環境変数とPythonのパスを通す
    path = '{}/bin;{}'.format(old_hfs, os.getenv('PATH'))
    set_base_env(path, old_hfs, old_pref_dir)
    #hythonでスクリプトを実行する
    p = subprocess.Popen([hython, script], shell=True, stdout=PIPE, stderr=PIPE)
    #スクリプトからの戻り値を取得
    stdout, stderr = p.communicate()
    if stderr:
        hou.ui.displayMessage('Script Error', severity=hou.severityType.Error)
        return
    #戻ってくる値は文字列なので、evalで辞書に変換
    old_node_data = eval(stdout)
    return old_node_data

def get_node_info(node_name, node_label):
    node_info = {}
    node_info['Node Name'] = node_name
    node_info['Node Label'] = node_label
    return node_info

def compare(old_node_data):
    new_node_data = {}
    new_parm_node_data = {}
    categories = hou.nodeTypeCategories()
    for category, type_category in categories.items():
        new_nodes = []
        new_parm_nodes = []
        nodes = type_category.nodeTypes()
        old_nodes = old_node_data.get(category)
        #カテゴリ自体が存在しない場合の処理
        if not old_nodes:
            for node_name, node_type in sorted(nodes.items()):
                node_label = node_type.description()
                node_info = get_node_info(node_name, node_label)
                new_nodes.append(node_info)
            if new_nodes:
                new_node_data[category] = new_nodes
            continue
        #カテゴリは存在する場合
        old_node_names = [node_info['node_name'] for node_info in old_nodes]
        for node_name, node_type in sorted(nodes.items()):
            node_label = node_type.description()
            node_info = get_node_info(node_name, node_label)
            if node_name in old_node_names:
                all_parms = get_all_parm_templates([], node_type)
                index = old_node_names.index(node_name)
                parm_sets = set(old_nodes[index]['parms'])
                new_parms = [parm.name() for parm in all_parms if not parm.name() in parm_sets]
                if new_parms:
                    node_info['parms'] = new_parms
                    new_parm_nodes.append(node_info)
            else:
                new_nodes.append(node_info)
        if new_nodes:
            new_node_data[category] = new_nodes
        if new_parm_nodes:
            new_parm_node_data[category] = new_parm_nodes
    return new_node_data, new_parm_node_data

def create_nodes(node_data, root_node):
    for category, nodes in node_data.items():
        #ノードを作成するためのカテゴリに合わせた親ノードを作る
        if category == 'Object':
            parent_node = root_node.createNode('subnet', category)
        elif category == 'Driver':
            parent_node = root_node.createNode('ropnet', category)
        elif category == 'Sop':
            parent_node = root_node.createNode('geo', category)
        elif category == 'Vop':
            parent_node = root_node.createNode('matnet', category)
        elif not 'Net' in category:
            try:
                parent_node = root_node.createNode(
                    category.lower() + 'net', category, run_init_scripts=False)
            except:
                continue
        else:
            parent_node = root_node.createNode(category.lower(), category)
        #ノードの作成
        for node_info in nodes:
            #ノードの名前を取得して作成
            node_name = node_info['Node Name']
            try:
                new_node = parent_node.createNode(node_name)
            except:
                continue
            #パラメータの取得
            parms = node_info.get('parms')
            if not parms:
                continue
            #パラメータにエクスプレッションを設定
            for parm_name in parms:
                try:
                    if parm_name[-1] == '#':
                        parm_name = parm_name[:-1] + '1'
                    parm_tuple = new_node.parmTuple(parm_name)
                    if not parm_tuple:
                        continue
                    for parm in parm_tuple:
                        parm.setExpression('constant()')
                except:
                    pass
        #ノード整理
        parent_node.layoutChildren()
    root_node.layoutChildren()

def create_new_nodes(new_node_data):
    root_node = hou.node('/obj').createNode('subnet', 'NewNodes')
    create_nodes(new_node_data, root_node)

def create_new_parm_nodes(new_parm_node_data):
    root_node = hou.node('/obj').createNode('subnet', 'NewParmNodes')
    create_nodes(new_parm_node_data, root_node)

def main():
    hfs = os.getenv('HFS')
    #比較するバージョンを取得
    version = get_compare_version(hfs)
    if not version:
        return
    pref_dir = os.getenv('HOUDINI_USER_PREF_DIR')
    path = os.getenv('PATH')
    #比較するバージョンの環境変数を取得
    old_hfs, old_pref_dir = get_env_from_version(
        version, hfs, pref_dir)
    #比較するバージョンのノード情報を取得
    old_node_data = get_old_node_data(old_hfs, old_pref_dir)
    if not old_node_data:
        return
    #hython用にセットした環境変数を元に戻す
    set_base_env(path, hfs, pref_dir)
    #現在のバージョンにあるノードと比較してノードの情報を取得
    new_node_data, new_parm_node_data = compare(old_node_data)
    #現在のバージョンのみに存在するノードを作成
    create_new_nodes(new_node_data)
    #現在のバージョンでパラメータが追加されたノードを作成
    create_new_parm_nodes(new_parm_node_data)
    #ノードの整理
    hou.node('/obj').layoutChildren()
```
</div></details>

# まとめ
以上、新しいバージョンで追加、更新されたノードの調べ方でした。
このスクリプトを使う事で新しいバージョンが出る度にいち早く更新されたノードを確認する事が出来ますね！

もし記事内で間違いや不明点等あれば書き込んでいただけると幸いです。
最後までお読みいただき、ありがとうございました。