Help us understand the problem. What is going on with this article?

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

はじめに

この記事はHoudini Advent Calender 2019の10日目の記事です。

先月の終わりにHoudini18.0がリリースされました!

もうすでに使ってる方も多いと思いますが、新機能ムービーで紹介されたノード以外にも細かい更新がされているようです。

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

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

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

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

この条件を元にPythonで追加、更新されたノードをネットワークエディタ状に全て作成したいと思います。

ちなみに比較するバージョンは17.5.39118.0.287を使用しています。

スクリプトの実行結果

まずスクリプトを実行した結果をご覧ください。
Get New Nodes

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

18.0で追加されたSopノード
New Sop

ちなみに追加されたパラメータにはconstant()というエクスプレッションが入っているので、パラメータのフィルターからParameters with Non-Default Valuesを選ぶと追加されたパラメータのみ表示出来るようになっています。
Parameter Filter

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

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

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

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個のパラメータが追加されてます。

これを見るとLopHoudini17.5の時点ですでに入ってたんですね。そしてLOPSopに限らず、TopDopもかなり更新されてるようです。

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

CSVデータ

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

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

  • CSVをダウンロード

  • エクセルで開いた図。ノードごとにカテゴリ、ノード名、ノードラベルで分けています。
    Houdini18.0にしか存在しないノード

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

  • CSVをダウンロード

  • エクセルで開いた図。パラメータごとにカテゴリ、ノード名、ノードラベル、パラメータ名、パラメータラベルで分けています。
    パラメータが追加されたノード

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

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

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

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

  • インストールディレクトリの取得
    Houdiniは起動時にHFSという環境変数にインストールディレクトリを登録するので、osモジュールで取得します。
import os
hfs = os.getenv('HFS')
#C:/PROGRA~1/SIDEEF~1/HOUDIN~1.287
  • 全てのバージョンの取得
    ここから全てのバージョンを取得するには一つ上のディレクトリに行き、そのフォルダにある全てのフォルダ名を取得します。
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で取得したパスから省けるんですが、HFSHOUDIN~1.287のように略字で返ってくるので、下記の方法で省きました。
import hou
current_version = hou.applicationVersionString()
versions.remove('Houdini ' + current_version)
  • 取得したバージョンをUIに表示
    取得したバージョンをUIに表示させてユーザーが選べるようにします。
    UIPySideで作るのが一般的ですが、面倒なのでhou.uiモジュールのselectFromList関数を使用しました。
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()でキーに各カテゴリ名、値にhou.NodeTypeCategoryオブジェクトを持った辞書が返ってきます。
Node Categories
そしてhou.NodeTypeCategoryオブジェクトのnodeTypes関数を実行することで、キーにノードタイプ名、値にhou.NodeTypeオブジェクトを持った辞書が返ってきます。

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

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

全てのパラメータを取得

全てのパラメータを取得するにはhou.NodeTypeオブジェクトのparmTemplates()関数を実行することで、基本的に全てのパラメーターのhou.parmTemplateオブジェクトを取得することが出来ます。
しかしMultiparm Blockに入ったパラメータは含まれないため、Group PromoteなどパラメータがMultiparm Blockに入ってる物は取得することが出来ません。

Multi Parm

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

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

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

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

# -*- 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の箇所のみ表示)。

"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の引数に指定します。

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モジュールのインポートに失敗してるようです。

エラーの原因

なぜエラーが出るかと言うと、Hythonhouモジュールをインポートする時に$HFS$HFS/houdini/python2.7libsを見に行きます。
つまり18.0からHythonを実行すると18.0用のHFSPYTHONPATHしか認識しないため、インポートエラーが出るという事です。

解決方法

これを解決するにはHythonを実行する前に下記コードを実行して、そのバージョン用の設定に変更する必要があります(安全のため、Hython実行後は元の設定に戻します)。

import sys

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

コード全体

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

コード全体を見る
# -*- 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()

まとめ

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした