9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HoudiniAdvent Calendar 2024

Day 6

Multiparmのあれこれ [移動、コピー、複製、リファレンス]

Last updated at Posted at 2024-12-05

はじめに

この記事はHoudini Advent Calender 2024の6日目の記事です

Houdini 20.5から新しく追加されたHOMの関数でMultiparmの扱いが便利になったので
今回はそれらを使って、いい感じ! なMultiparm Blockを作っていきたいと思います!

使用バージョン : Houdini 20.5.332

MultiparmBlock 移動編

MultiparmBlock 入替え編

MultiparmBlock コピー・複製編

MultiparmBlock リファレンス編(番外)

関数

20.0.270〜
hou.Parm.moveMultiParmInstances(moves)
hou.Parm.moveDownMultiParmInstance(inst_index)
hou.Parm.moveUpMultiParmInstance(inst_index)

20.0.270から追加された関数でMultiparmの移動ができるようになっています
単純な移動であればこちらの関数を使うだけでBlockの移動ができますね

moveMultiParmInstancesに関しては正しく引数を与えても例外が発生するというバグがありましたが、20.5.340で修正されたみたいです

20.5〜
hou.Parm.multiParmInstancesAsData()
hou.Parm.setMultiParmInstancesFromData(data)

そして、Recipesの追加に伴って、20.5からDataを扱う関数が増えました!
今回はMultiparmに関するものだけを挙げていますが、他にもあるのでParmもご確認下さい


hou.Parm.multiParmInstancesAsData()の関数を実行すると、
各インスタンスの情報を含んだリストが返ってきます。

リストの各要素は辞書で構成されていて、
基本的にはキーにパラメータの名前値にパラメータの値が含まれます

multiParmInstancesAsData()
# return: [{<parm_name>_#: <parm_value>,...},
#          {<parm_name>_#: <parm_value>,...}...]

# e.g)
#  単一の値
[{'parm1_#': '0', 'parm2_#': '2'},
 {'parm1_#': '5', 'parm2_#': '3'}]

#  複数の値
[{'parm1_#': ['0','1','2'], 'parm2_#': ['1','2','3']},
 {'parm1_#': ['3','4','5'], 'parm2_#': ['2','3','1']}]

# エクスプレッション
[{'parm1_#': {'expression': 'ch("ref_parm")'}},
 {'parm1_#': {'expression': 'hou.pwd().evalParm("ref_parm")', 'language': 'Python'}}]

#  キーフレーム
[{'parm1_#':
  {'keyframes': [ {'frame': 1, 'value': 1.0, 'slope': 0.0,
                   'accel': 0.0, 'inaccel': 0.3, 'expression': 'bezier()'},
                  {'frame': 2, 'value': 0.5, 'slope': 0.0,
                   'accel': 0.0, 'inaccel': 0.3, 'expression': 'bezier()'}
                ]
  }
}]

MultiparmBlock 移動編


それではこちらを作っていきたいと思います!

まずはパラメータのテンプレートを作成していきましょう
Edit Parameter Interaceからこのようなパラメータを作成しました

Folder TypeはMultiparm Block (list)
Button IconにそれぞれBUTTONS_up、BUTTONS_downを設定しています



もちろんこのままでは何も動かないので、
矢印のボタンを押した時に移動するようにコールバックを書いていきます



直接だと書きにくいので、自分は別途エディタなどで書いてから貼り付けています
HDAや別途パラメータで関数を用意して、それを評価する方法もありますが今回はシンプルに

up_#
parent = kwargs["parm"].parentMultiParm();
idx    = int(kwargs["script_multiparm_index"]) - parent.multiParmStartOffset();
n      = parent.eval();
parent.moveDownMultiParmInstance(idx) if (n > 1) and (idx > 0) else None
down_#
parent = kwargs["parm"].parentMultiParm();
idx    = int(kwargs["script_multiparm_index"]) - parent.multiParmStartOffset();
n      = parent.eval();
parent.moveUpMultiParmInstance(idx) if (n > 1) and (idx < n-1) else None

Callback Scriptではスクリプトを一行にまとめないといけないため、
行の末尾に ; をつけて文の区切りとして使用しています

では、スクリプトで何をしているか見ていきましょう!

# はじめに自身の属しているMultiparmを取得します
parent = kwargs["parm"].parentMultiParm();

# インデックスを取得
# ただ、そのままだとパラメータのインデックスがオフセットされている場合があるので、
# 0から開始されるように調整
idx = int(kwargs["script_multiparm_index"]) - parent.multiParmStartOffset();

# インスタンスの総数を取得
n = parent.eval();

# 三項演算子を使って、条件に一致する場合のみ関数を実行
# up_#:   インスタンスの数が2つ以上かつ、インデックスの値が最下位ではない場合のみ上に移動
parent.moveDownMultiParmInstance(idx) if (n > 1) and (idx > 0)   else None
# down_#: インスタンスの数が2つ以上かつ、インデックスの値が最上位ではない場合のみ下に移動
parent.moveUpMultiParmInstance(idx)   if (n > 1) and (idx < n-1) else None
三項演算子
x if condition else y

この書き方は三項演算子と呼ばれ、if-else 文を1行で書くことができます

  • condition は評価される条件式
  • x は、condition が 真(True)であれば選ばれる値
  • y は、condition が 偽(False)であれば選ばれる値

up_#、down_#それぞれのCallback Scriptにスクリプトを設定し、Acceptを押したら
移動ボタンの付いたMultiparmの完成です!

MultiparmBlock 入替え編

先程のMultiparmだと、繰り上げ、繰り下げしかできないので
もう少し拡張して、任意の場所と入替えられるようにして行きましょう

QMenuでメニューを作ろう

早速、準備していくのですが、
どのMultiparmと入替えるのかを選択できるようにしたいので、

Qtを使用してメニューを作成していきます

Action Button

先程はButtonパラメータのCallback Scriptでスクリプトを書いていきましたが
一行では何かと難しいので、今回はAction Buttonを使って実装していきたいと思います。

Edit Parameter Interfaceを見てみると、パラメーターのタブの中にAction Buttonというタブがあります。
ここにPythonスクリプトを書き込むとパラメーターの横にアイコンが表示されるので、それをクリックすると書き込んだスクリプトを実行する事が出来ます。

pitch

シンプルなメニュー

Floatなど適当なパラメータを用意して、Action Buttonに以下を書き込みます

Action Button
from hutil.Qt.QtGui     import QCursor
from hutil.Qt.QtWidgets import QMenu, QAction
# NOTE: QActionはQt6からQtGuiに属するので、今後修正が必要になるかも

def test():
    hou.ui.displayMessage("Hello!")

menu    = QMenu("Hoge")                   # メニューの作成
submenu = QMenu("Fuga",   parent=menu)    # サブメニューの作成
action  = QAction("Fugo", parent=submenu) # アクションの作成

action.triggered.connect(test) # アクションをクリックした時の関数を登録

submenu.addAction(action) # アクションの追加
menu.addMenu(submenu)     # サブメニューの追加

# UIの見え方をHoudini仕様にするために、スタイルシートを設定
menu.setStyleSheet(hou.qt.styleSheet())

# マウスカーソルの位置を取得
pos = QCursor().pos()

# 実行
menu.exec_(pos)

これを実行すると

簡単なメニューが作れました
それではこれらを踏まえてMultiparmを拡張していきましょう!

実装


移動編でのパラメーターにDataパラメータを追加しました。
こちらのパラメータタイプは本来の使い方ではないと思うのですが
Action Buttonだけを使いたかったので、こちらを使用しています
※これにより処理が増えてしまいましたっ...
パラメータタイプ

Action Button
from hutil.Qt.QtGui     import QCursor
from hutil.Qt.QtWidgets import QMenu, QAction
# NOTE: QActionはQt6からQtGuiに属するので、今後修正が必要になるかも

# 引数を含んだ関数を与えたいのでpartialをimport
from functools import partial

# geometryが空の状態だとhou.Parm.setMultiParmInstancesFromDataを実行した時に
# エラーを引き起こすので、それを回避するための関数
def checkNoneGeometry(data):
    for d in data:
        for k in d.keys():
            if isinstance(d[k], dict) and ("geometry" in d[k]) and (d[k]["geometry"] is None):
                d[k].pop('geometry')
    return data

# Multiparmを入れ替えるための関数
def swapBlock(multiparm, idx, target_idx):
    # Multiparmのデータを取得
    data = multiparm.multiParmInstancesAsData()
    # geometryにNoneが含まれていないか確認
    checkNoneGeometry(data)
    # 順番の入替え
    data[idx],data[target_idx] = data[target_idx],data[idx]
    # Multiparmのセット
    multiparm.setMultiParmInstancesFromData(data)

# パラメータの情報を取得
parm      = kwargs["parmtuple"][0]
multiparm = parm.parentMultiParm()
offset    = multiparm.multiParmStartOffset()
idx       = int(kwargs["script_multiparm_index"]) - offset
n         = multiparm.eval()

# メインメニューの作成
menu = QMenu()

# サブメニューの作成
submenu = QMenu("Swap",                                 # Label
                icon   = hou.qt.Icon("SOP_attribswap"), # Icon
                parent = menu)                          # Parent
for i in range(n):
    # クリックしたインデックスの場合はスキップ
    if i == idx:
        continue
    # アクションの作成
    action = QAction(hou.qt.Icon("SOP_enumerate"),                # Icon
                     "%s_%d"%(multiparm.description(), i+offset), # Label
                     parent=submenu)                              # Parent
    # アクションをクリックした時の関数を登録
    # NOTE: lambdaを使う事もできますが、イテレーションの中で設定すると
    #       iなどの値が先にキャプションされてしまい意図しない結果となるので
    #       functoolsのpartialを使用
    action.triggered.connect(partial(swapBlock,multiparm,idx,i))
    # サブメニューにアクションを追加
    submenu.addAction(action)

# サブメニューをメニューに追加
menu.addMenu(submenu)

# UIの見え方をHoudini仕様にするために、スタイルシートを設定
menu.setStyleSheet(hou.qt.styleSheet())

# マウスカーソルの位置を取得
pos = QCursor().pos()

# 実行
menu.exec_(pos)
checkNoneGeometry()

Data、Geometry Dataタイプのパラメータは、ジオメトリを格納できるのですが、
その値が空の場合、hou.Parm.setMultiParmInstancesFromDataエラーを引き起こすため、それを回避するために処理をいれています

今回アイコンも追加しています、詳しくはこちらを hou.qt.Icon

これを適用すると入替えメニューの付いたMultiparmの完成です!

MultiparmBlock コピー・複製編

さぁどんどん、いきましょう
次はコピー、複製できるように拡張していきます

入替え編のコードを少し調整します

Action Button
from hutil.Qt.QtGui     import QCursor
from hutil.Qt.QtWidgets import QMenu, QAction
# NOTE: QActionはQt6からQtGuiに属するので、今後修正が必要になるかも

# 引数を含んだ関数を与えたいのでpartialをimport
from functools import partial

# geometryが空の状態だとhou.Parm.setMultiParmInstancesFromDataを実行した時に
# エラーを引き起こすので、それを回避するための関数
def checkNoneGeometry(data):
    for d in data:
        for k in d.keys():
            if isinstance(d[k], dict) and ("geometry" in d[k]) and (d[k]["geometry"] is None):
                d[k].pop('geometry')
    return data

# Multiparmを操作するための関数
def processBlock(multiparm, idx, target_idx, method):
    # Multiparmのデータを取得
    data = multiparm.multiParmInstancesAsData()
    # geometryにNoneが含まれていないか確認
    checkNoneGeometry(data)
    # 順番の入替え
    if method == "swap":
        data[idx],data[target_idx] = data[target_idx],data[idx]
    # コピー
    elif method == "copy":
        data[idx] = data[target_idx]
    # 複製
    elif method == "dup":
        data.insert(target_idx, data[idx])
    # Multiparmのセット
    multiparm.setMultiParmInstancesFromData(data)

# パラメータの情報を取得
parm      = kwargs["parmtuple"][0]
multiparm = parm.parentMultiParm()
offset    = multiparm.multiParmStartOffset()
idx       = int(kwargs["script_multiparm_index"]) - offset
n         = multiparm.eval()

# メインメニューの作成
menu = QMenu()

# サブメニューのリストを作成
submenu_list = []
submenu_list.append(QMenu("Swap",              icon=hou.qt.createIcon("SOP_attribswap"), parent=menu, objectName="swap"))
submenu_list.append(QMenu("Copy\tfrom",        icon=hou.qt.createIcon("BUTTONS_copy"),   parent=menu, objectName="copy"))
submenu_list.append(QMenu("Duplicate\tbefore", icon=hou.qt.createIcon("SOP_duplicate"),  parent=menu, objectName="dup"))
for submenu in submenu_list:
    method = submenu.objectName()
    for i in range(n):
        # 複製以外で、クリックしたインデックスの場合はスキップ
        if (i == idx) and (method != "dup"):
            continue
        # アクションの作成
        action = QAction(hou.qt.createIcon("SOP_enumerate"),          # Icon
                         "%s_%d"%(multiparm.description(), i+offset), # Label
                         parent=submenu)                              # Parent
        # アクションをクリックした時の関数を登録
        # NOTE: lambdaを使う事もできますが、イテレーションの中で設定すると
        #       iなどの値が先にキャプションされてしまい意図しない結果となるので
        #       functoolsのpartialを使用
        action.triggered.connect(partial(processBlock,multiparm,idx,i,method))
        # サブメニューにアクションを追加
        submenu.addAction(action)
    # サブメニューをメニューに追加
    menu.addMenu(submenu)

# UIの見え方をHoudini仕様にするために、スタイルシートを設定
menu.setStyleSheet(hou.qt.styleSheet())

# マウスカーソルの位置を取得
pos = QCursor().pos()

# 実行
menu.exec_(pos)

サブメニューの追加と、それらを処理する関数に条件をつけています
少し調整を加えるだけで実装できました!

コピー・複製機能の付いたMultiparmの完成です!

MultiparmBlock リファレンス編(番外)


こちらなんですが、リファレンスになるとリストの編集だけではなく、
データの中身を少し書き換える必要があるのと、ネストされたMultiparmやRampなどの処理が複雑になるため、詳しい解説は省略します

コードは貼り付けておくので、ご興味あればご覧ください!

Script
Action Button
from hutil.Qt.QtGui     import QCursor
from hutil.Qt.QtWidgets import QMenu, QAction
# NOTE: QActionはQt6からQtGuiに属するので、今後修正が必要になるかも

import re

# 引数を含んだ関数を与えたいのでpartialをimport
from functools import partial
# 辞書を子階層までコピーしたいのでdeepcopyをimport
from copy      import deepcopy

# geometryが空の状態だとhou.Parm.setMultiParmInstancesFromDataを実行した時に
# エラーを引き起こすので、それを回避するための関数
def checkNoneGeometry(data):
    for d in data:
        for k in d.keys():
            if isinstance(d[k], dict) and ("geometry" in d[k]) and (d[k]["geometry"] is None):
                d[k].pop('geometry')
    return data
    
# Rampのリンクが残っている場合があるのでそれらを解除
def unlinkMultiparm(node):
    path  = node.path()
    links = re.findall("'(.*?)'",hou.hscript("opmultiparm %s"%(path))[0])
    [hou.hscript("opmultiparm '%s' '%s' ''"%(path,link)) for link in links]

# 再帰的にdataをリファレンスへと書き換え
def toReferenceData(node, data, target_idx, offset, indices=[], depth=0):
    for i,parm_set in enumerate(data):
        if depth == 0 and target_idx != i:
            continue
        current_indices = indices[:] + [i+offset,]
        
        for parm_name,parm_value in parm_set.items():
            resolved_parm_name = parm_name.replace("#","{}").format(*current_indices)
            expr     = 'ch("%s")'%(resolved_parm_name)
            expr_str = 'chs("%s")'%(resolved_parm_name)
            
            # Get Template
            template_group = node.parmTemplateGroup()
            template = template_group.find(parm_name)
            if template:
                type_    = template.type()
                datatype = template.dataType()
                
                # Ramp
                if datatype == hou.parmData.Ramp:
                    ramp_offset = int(template.tags().get('multistartoffset',1))
                    parm_set[parm_name] = {'value' : {'expression': expr}, 'points' : parm_set[parm_name]}
                    for point_idx,point in enumerate(parm_set[parm_name]['points']):
                        for point_name, point_value in point.items():
                            if isinstance(point_value, list):
                                point[point_name] = [{'expression': 'ch("%s_%d%s")'%(resolved_parm_name,point_idx+ramp_offset,channel)}
                                                      for channel in ["cr","cg","cb"]]
                            else:
                                point[point_name] =  {'expression': 'ch("%s_%d%s")'%(resolved_parm_name,point_idx+ramp_offset,point_name)}
                                
                # MultiparmBlock       
                elif type_ in (hou.parmTemplateType.Folder, hou.parmTemplateType.FolderSet):
                    if template.folderType() in (
                        hou.folderType.MultiparmBlock,
                        hou.folderType.ScrollingMultiparmBlock,
                        hou.folderType.TabbedMultiparmBlock
                    ):
                        offset = int(template.tags().get('multistartoffset',1))
                        if isinstance(parm_set[parm_name], list):
                            parm_set[parm_name] = {
                                'multiparm_links': {},
                                'multiparms'     : parm_set[parm_name][:],
                                'value'          : {'expression': expr}
                            }
                            parm_value = parm_set[parm_name]['multiparms']
                        elif isinstance(parm_set[parm_name], dict):
                            parm_value = parm_set[parm_name].get('multiparms')
                            parm_set[parm_name]['value'] = {'expression': expr}
                        else:
                            parm_set[parm_name] = {'expression': expr}
                            
                # String / Float
                else:
                    expr = expr_str if datatype == hou.parmData.String else expr
                    parm_set[parm_name] = {'expression': expr}
                    
                # List
                if isinstance(parm_value, list):
                    toReferenceData(node, parm_value, target_idx, offset, current_indices, depth+1)
    if depth == 0:
        return data[target_idx]

# Multiparmを入れ替えるための関数
def processBlock(multiparm, idx, target_idx, method):
    # Multiparmのデータを取得
    data = multiparm.multiParmInstancesAsData()
    # geometryにNoneが含まれていないか確認
    checkNoneGeometry(data)
    # 順番の入替え
    if method == "swap":
        data[idx],data[target_idx] = data[target_idx],data[idx]
    # コピー
    elif method == "copy":
        data[idx] = data[target_idx]
    # 複製
    elif method == "dup":
        data.insert(target_idx, data[idx])
    # リファレンス
    elif method == "ref":
        # リンクを解除
        unlinkMultiparm(multiparm.node())
        # 一度パラメータをリセット
        multiparm.removeMultiParmInstance(idx)
        multiparm.insertMultiParmInstance(idx)
        # リファレンスに書き換え
        data[idx] = toReferenceData(multiparm.node(),
                                    deepcopy(data),
                                    target_idx,
                                    multiparm.multiParmStartOffset())
    # Multiparmのセット
    multiparm.setMultiParmInstancesFromData(data)

# パラメータの情報を取得
parm      = kwargs["parmtuple"][0]
multiparm = parm.parentMultiParm()
offset    = multiparm.multiParmStartOffset()
idx       = int(kwargs["script_multiparm_index"]) - offset
n         = multiparm.eval()

# メインメニューの作成
menu = QMenu()

# サブメニューのリストを作成
submenu_list = []
submenu_list.append(QMenu("Swap",              icon=hou.qt.createIcon("SOP_attribswap"), parent=menu, objectName="swap"))
submenu_list.append(QMenu("Copy\tfrom",        icon=hou.qt.createIcon("BUTTONS_copy"),   parent=menu, objectName="copy"))
submenu_list.append(QMenu("Duplicate\tbefore", icon=hou.qt.createIcon("SOP_duplicate"),  parent=menu, objectName="dup"))
submenu_list.append(QMenu("Reference\tfrom",   icon=hou.qt.createIcon("LOP_reference"),  parent=menu, objectName="ref"))
for submenu in submenu_list:
    method = submenu.objectName()
    for i in range(n):
        # 複製以外で、クリックしたインデックスの場合はスキップ
        if (i == idx) and method != "dup":
            continue
        # アクションの作成
        action = QAction(hou.qt.createIcon("SOP_enumerate"),          # Icon
                         "%s_%d"%(multiparm.description(), i+offset), # Label
                         parent=submenu)                              # Parent
        # アクションをクリックした時の関数を登録
        # NOTE: lambdaを使う事もできますが、イテレーションの中で設定すると
        #       iなどの値が先にキャプションされてしまい意図しない結果となるので
        #       functoolsのpartialを使用
        action.triggered.connect(partial(processBlock,multiparm,idx,i,method))
        # サブメニューにアクションを追加
        submenu.addAction(action)
    # サブメニューをメニューに追加
    menu.addMenu(submenu)

# UIの見え方をHoudini仕様にするために、スタイルシートを設定
menu.setStyleSheet(hou.qt.styleSheet())

# マウスカーソルの位置を取得
pos = QCursor().pos()

# 実行
menu.exec_(pos)

おわりに

新しく追加された関数で、Multiparmを簡単に扱うことができました
今までは、複製するのにも結構大変だったので個人的には嬉しいところです

また、QMenuを使ったカスタムのメニューもさらに書き加えていけば、
独自のWidgetを表示したりと出来ることが増えるので是非色々と試してみて下さい!

ありがとうございました!

9
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?