はじめに
この記事はHoudini Advent Calender 2019の10日目の記事です。
先月の終わりにHoudini18.0がリリースされました!
もうすでに使ってる方も多いと思いますが、新機能ムービーで紹介されたノード以外にも細かい更新がされているようです。
そこで、この記事ではHoudini18.0で追加、更新されたノードを取得、作成する方法を紹介します。
追加、更新されたノードの定義
調べるために追加、更新の定義を考えます。定義したのは下記の2つです。
- Houdini17.5のノード全てと比較してHoudini18.0にしか存在しない物
- ノードは存在するが新しくパラメータが追加された物
この条件を元にPythonで追加、更新されたノードをネットワークエディタ状に全て作成したいと思います。
ちなみに比較するバージョンは17.5.391と18.0.287を使用しています。
スクリプトの実行結果
スクリプトを実行すると、どのバージョンと比較するかを選ぶウィンドウが出てきて、選ぶとノードの比較処理が走ります。
処理が終わると、ノード自体が追加された物はNewNodes、パラメータが追加されたノードはNewParmNodesというSubnetが作成され、中にSopなどカテゴリごとにノードが出来ています。
ちなみに追加されたパラメータにはconstant()というエクスプレッションが入っているので、パラメータのフィルターからParameters with Non-Default Valuesを選ぶと追加されたパラメータのみ表示出来るようになっています。
スクリプトは下記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個のパラメータが追加されてます。
これを見るとLopはHoudini17.5の時点ですでに入ってたんですね。そしてLOP、Sopに限らず、Top、Dopもかなり更新されてるようです。
もう追える気がしません。
CSVデータ
ノード名やパラメータ名の一覧も見たいという方の為に下記にCSVファイルをアップしました。
Houdini18.0にしか存在しないノード
パラメータが追加されたノード
追加、更新されたノードを取得する方法
ここからは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で取得したパスから省けるんですが、HFSはHOUDIN~1.287
のように略字で返ってくるので、下記の方法で省きました。
import hou
current_version = hou.applicationVersionString()
versions.remove('Houdini ' + current_version)
-
取得したバージョンをUIに表示
取得したバージョンをUIに表示させてユーザーが選べるようにします。
UIはPySideで作るのが一般的ですが、面倒なので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オブジェクトを持った辞書が返ってきます。
そして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に入ってる物は取得することが出来ません。
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モジュールのインポートに失敗してるようです。
エラーの原因
なぜエラーが出るかと言うと、Hythonはhouモジュールをインポートする時に**$HFSと$HFS/houdini/python2.7libsを見に行きます。
つまり18.0からHythonを実行すると18.0用のHFSとPYTHONPATH**しか認識しないため、インポートエラーが出るという事です。
解決方法
これを解決するには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()
まとめ
以上、新しいバージョンで追加、更新されたノードの調べ方でした。
このスクリプトを使う事で新しいバージョンが出る度にいち早く更新されたノードを確認する事が出来ますね!
もし記事内で間違いや不明点等あれば書き込んでいただけると幸いです。
最後までお読みいただき、ありがとうございました。