この記事は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 !!!!