Wedgeを中心にPDGの話まで

この記事は[Houdini Advent Calender 2019](https://qiita.com/advent-calendar/2019/houdini)の７日目の記事（の予定でしたが公開できずにクリスマスを迎えてしまった記事）です

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


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

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

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

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

例えば、引数によって形状が変化するSOPがあったとして
**大量のバリエーションを作成したい時**などにWedgeを使ってあげると簡単にデータが生成できます
<img width="720" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153791/7821cab8-e689-5732-f804-ac4d3655f23c.png">

または、新しいSolverを学習したり検証をする時に
**パラメータ毎の変化を比較したい場合**もWedgeを使うと結果が比較しやすくて助かります
<img width="720" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153791/76ee67df-912f-11c3-df3f-0a5617f6033b.png">

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



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

WedgeROPはコンパイルされたノードではなく中にShellノードが入っています。
つまり処理をしているのは実質このShellノードなのですが
そのShellノードもこのように設定されており
**Pre-Frame Script (レンダリングフレームの開始前に）** python moduleにある関数を実行するようになっています。
<img width="720" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153791/636fc19c-fe6e-6b14-478a-357f518693e2.png">



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

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

<details><summary>Pythonコードはこちら、長いので折りたたみました</summary><div>

```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", "")
```

</div></details>


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

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

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

という構文が使われていますが、こちらを使用すると
**シーン内で共通で使える変数**がセットされます
Editメニュー　＞　Alias and Variables　を押すと表示されるダイアログで確認が出来ます
<img width="640" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153791/fc8c2194-9283-e956-6cf3-479f053c6ce6.png">

**シーン内で共通で使える**いうのがどういうことかと言うと
次のような形で、$（変数名）を使えば、**シーン内部に存在するどんなノードでも値が呼び出せる**ということになります
<img width="720" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/153791/ea9637af-aaf0-ae18-5889-ed1ca4c4816c.png">

通常、ノードで使える変数は**ノードが元々対応しているローカル変数のみ**なのですが
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 !!!!