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

Wedgeを中心にPDGの話まで

この記事はHoudini Advent Calender 2019の7日目の記事(の予定でしたが公開できずにクリスマスを迎えてしまった記事)です

毎年Houdiniアドカレを楽しみにしていらっしゃるHoudinistの方々、大変申し訳ございませんでした

はじめに

Houdiniには古くからWedgeROPというノードが存在しています
仕組みがわかればHDAに組み込んだり社内のパイプラインに流したりしやすいので
多少のカスタマイズはされているかもしれませんが、その考え方や機能に助けられた方は多いのではないのでしょうか。

今回の記事ではWedgeについての基本的な解説をしつつ
PDGがリリースされてから搭載されたWedgeTOPの使い方と従来の方法(WedgeROP)との違いを解説して
PDGの処理への理解を深める(メモリ効率、workitemの挙動)までまとめたいと思っています
(が、記事公開時 2019/12/24時点では途中までの公開になっています、重ね重ね申し訳ありません・・!年内公開予定です)

また、本記事の内容はQiitaで公開している性質上、Houdiniの操作方法等の解説は省略して
内部処理や概要の説明を中心にして、PG/TA向けの解説にしたいと思います。
よろしくお願いします。

Wedgeとは

Wedge(ウェッジ)を簡単に説明すると
指定したパラメータを 任意の回数 値を変更させながら実行させる機能です

例えば、引数によって形状が変化するSOPがあったとして
大量のバリエーションを作成したい時などにWedgeを使ってあげると簡単にデータが生成できます

または、新しいSolverを学習したり検証をする時に
パラメータ毎の変化を比較したい場合もWedgeを使うと結果が比較しやすくて助かります

わざわざ各状態のデータファイルを用意したり
処理を回すためにスクリプトを別に用意する必要が無い
のがHoudiniの便利なところですね

WedgeROPの中身と処理

そんなWedgeの処理ですが、どういった挙動をしているか
WedgeROPの中身を見ていきたいと思います

WedgeROPはコンパイルされたノードではなく中にShellノードが入っています。
つまり処理をしているのは実質このShellノードなのですが
そのShellノードもこのように設定されており
Pre-Frame Script (レンダリングフレームの開始前に) python moduleにある関数を実行するようになっています。

この関数の内容は次のようなものですが、簡単に説明すると
指定された引数を使ってパラメータの値を変更する という基本部分に加えて
環境変数 WEDGE に変更された値に応じた文字列をセットする
環境変数 WEDGENUM に実行されたWEDGEの番号をセットする
という処理が書かれています

単に値を変更するだけでは無く
この環境変数をセットするという仕組みがとてもHoudiniらしい良いポイントだと思うので
解説をしてきます

Pythonコードはこちら、長いので折りたたみました
from __future__ import division
from __future__ import print_function
from builtins import zip
from builtins import range
from past.utils import old_div
import random

def setenvvariable(var, val):
    hou.hscript("set %s = %s" % (var, val))
    hou.hscript("varchange")

def createwedge(channame,
                englishname,
                min, max,
                steps):
    if englishname == "":
        # Extract the raw channel name
        # to use as our name if the user
        # provided none
        englishname = channame.rpartition("/")[2]
    result = []
    for i in range(steps):
        if steps > 1:
            v = (old_div((max - min), (steps-1))) * i
        else:
            v = (max - min) / 2.0

        v += min;
        prefix = "_%s_%f" % (englishname, v)
        result.append( (channame, prefix, v) )
    return result

def mergewedge(allwedge, wedge):
    result = []
    for w in wedge:
        for a in allwedge[:]:
            a = a[:]
            a.append(w)
            result.append(a)
    return result

def getwedgenames(node):
    wedges, stashedparms, errormsg = getwedges(node)

    names = []
    for wedge in wedges:
        names.append(_calculatewedgename(node, wedge))

    return names

def _calculatewedgename(node, wedge_channels):
    wedge_name = node.parm("prefix").eval()

    for channel in wedge_channels:
        channel_prefix = channel[1]
        wedge_name += channel_prefix

    return wedge_name

def applyspecificwedge(node, wl):
    wedgestring = _calculatewedgename(node, wl)
    for wedge in wl:
        (cname, prefix, val) = wedge
        parm = node.parm(cname)
        if parm is not None:
            # Check if the parm is integer.
            ptype = parm.parmTemplate().type()
            if ptype == hou.parmTemplateType.Int:
                val = int(val)
            if ptype == hou.parmTemplateType.Menu:
                val = int(val)
            if ptype == hou.parmTemplateType.Toggle:
                val = int(val)
            parm.set(val)
    # Set the wedge environment variable
    setenvvariable("WEDGE", wedgestring)

def buildrandom(chanlist, namelist, rangelist):
    wedge = []

    # A sure sign I should have passed a list of tuples...
    for (chan, englishname, r) in zip(chanlist, namelist, rangelist):
        (a, b) = r
        v = random.uniform(a, b)
        if englishname == "":
            # Extract the raw channel name
            # to use as our name if the user
            # provided none
            englishname = chan.rpartition("/")[2]
        prefix = "_%s_%f" % (englishname, v)
        wedge.append( (chan, prefix, v) )

    return wedge

def getwedges(node):
    numparam = node.parm("wedgeparams").eval()

    stashedparms = []
    errormsg = ""

    if node.parm("wedgemethod").eval() == 0:
        if node.parm("random").eval() == 0:
            allwedge = [[]]
            for p in range(numparam):
                chan = node.parm("chan%d" % (p+1,)).eval()
                name = node.parm("name%d" % (p+1,)).eval()
                if chan != "":
                    parm = node.parm(chan)
                    if parm is None:
                        errormsg += "Cannot find channel %s\n" % (chan, )
                        continue

                    wedge = createwedge(
                        chan,
                        name,
                        node.parm("range%dx" % (p+1,)).eval(),
                        node.parm("range%dy" % (p+1,)).eval(),
                        node.parm("steps%d" % (p+1,)).eval())

                    stashedparms.append((chan, parm.eval()))

                    # more an outerproduct than a merge.
                    allwedge = mergewedge(allwedge, wedge)
        else:
            allwedge = []
            chanlist = []
            namelist = []
            rangelist = []
            for p in range(numparam):
                chan = node.parm("chan%d" % (p+1,)).eval()
                name = node.parm("name%d" % (p+1,)).eval()
                if chan != "":
                    parm = node.parm(chan)
                    if parm is None:
                        errormsg += "Cannot find channel %s\n" % (chan, )
                        continue

                    chanlist.append(chan)
                    namelist.append(name)
                    rangelist.append( (node.parm("range%dx" % (p+1,)).eval(),
                                       node.parm("range%dy" % (p+1,)).eval()) )
                    stashedparms.append((chan, parm.eval()))

            random.seed(node.parm("seed").eval())
            nsample = node.parm("numrandom").eval()
            for lvar in range(nsample):
                wedge = buildrandom(chanlist, namelist, rangelist)
                allwedge.append(wedge)
    else:   # Must be by take.
        allwedge = []
        rendernode = None
        if len(node.inputs()) > 0:
            rendernode = node.inputs()[0]
            if rendernode:
                renderpath = rendernode.path()
        if rendernode is None:
            renderpath = node.parm("driver").eval()
            rendernode = node.node(renderpath)
        basetake = node.parm("roottake").eval()
        (takelist, e) = hou.hscript("takels -i -q -p %s" % (basetake,))
        takelist = takelist.strip()
        if takelist == "":
            errormsg += "No takes found as child of \"%s\".\n" % (basetake,)
        takelist = takelist.split("\n")
        chan = renderpath + "/take"
        stashedparms.append((chan, node.parm(chan).eval()))
        for take in takelist:
            allwedge.append( [(chan, "_" + take, take)] )

    return allwedge, stashedparms, errormsg

def applywedge(node):
    allwedge, stashedparms, errormsg = getwedges(node)

    rendernode = None
    if len(node.inputs()) > 0:
        rendernode = node.inputs()[0]
        if rendernode:
            renderpath = rendernode.path()
    if rendernode is None:
        renderpath = node.parm("driver").eval()
        rendernode = node.node(renderpath)

    if rendernode is None:
        errormsg += "Cannot find output driver %s\n" % (renderpath,)

    # Extract specified single wedge
    wedgenum = 0
    if node.parm("wrange").eval() == 1:
        wedgenum = node.parm("wedgenum").eval()
        if wedgenum >= len(allwedge):
            errormsg += "Requested wedge %d greater than total wedges %d.\n" % (wedgenum, len(allwedge))
        else:
            allwedge = [allwedge[wedgenum]]

    if errormsg != "":
        if hou.isUIAvailable():
            hou.ui.displayMessage("Errors:\n" + errormsg)
        else:
            print("Errors: " + errormsg)

        return

    # Disable background rendering
    fgparm = None
    if node.parm("blockbackground").eval() == 1:
        fgparm = rendernode.parm("soho_foreground")

    if fgparm is not None:
        oldfgval = fgparm.eval()
        fgparm.set(True)

    # apply individual wedge
    for wl in allwedge:
        setenvvariable("WEDGENUM", str(wedgenum))
        applyspecificwedge(node, wl)
        rendernode.render()
        wedgenum = wedgenum + 1

    # Restore background rendering
    if fgparm is not None:
        fgparm.set(oldfgval)

    # Restore our settings to pre-wedge states.
    for (chan, val) in stashedparms:
        parm = node.parm(chan)
        parm.set(val)
    # Clear out the environment
    setenvvariable("WEDGE", "")

環境変数をセットすると何が良いのか?
を説明します

環境変数、と書いていますが、Houdiniの場合はHIPに保存されるローカル変数と言ったほうが良いかもしれません
先ほどのPythonスクリプトの中で、環境変数のセットに

hou.hscript("set %s = %s" % (var, val))

という構文が使われていますが、こちらを使用すると
シーン内で共通で使える変数がセットされます
Editメニュー > Alias and Variables を押すと表示されるダイアログで確認が出来ます

シーン内で共通で使えるいうのがどういうことかと言うと
次のような形で、$(変数名)を使えば、シーン内部に存在するどんなノードでも値が呼び出せるということになります

通常、ノードで使える変数はノードが元々対応しているローカル変数のみなのですが
WedgeROPを通して設定されるシーン内環境変数などは、基本的にどのノードからでも呼び出せます
(ローカル変数はドキュメントに書かれています、たとえばTransformノードであればこちらのCEX,CEYなどなど。。
https://www.sidefx.com/ja/docs/houdini/nodes/sop/xform.html#locals
ノード毎に決まっているのですが、WEDGEやWEDNUMといった変数はどのノードでも使えるようになります

この性質を使うと、工夫次第で色々な事が出来るようになります

例えば、WedgROPでセットされるWEDGENUM変数が、1~5の範囲で5回行われるとします

この場合、Wedgeが実行される度に、WEDGENUMに1〜5の値が入るのですが

fit($WEDGENUM,0,5,10,30)

とすれば、0~5ではなく10〜30の値を与えることが出来ますし

rand($WEDGENUM)

とすれば、WEDGENM毎に異なる値が生成されるように出来ます

バリエーションを作るにあたってこの性質や考え方はとても重要で
なるべく最小の調整で最大の効果を得ようとするとこの方法がマッチします


より詳しく解説して、WedgeTOPやPDGの話に展開して行きたいのですがサンタクロースが来てしまった為
現時点ではここまでの公開となります・・
全体としてはまだ1/3程度のボリュームでしょうか・・・
数日以内に必ず追加分を公開しますのでもう少々お待ち頂ければと思います・・!すみません!!!

...to be continued !!!!

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
ユーザーは見つかりませんでした