While Loopを利用したSymmetric Nearest Neighborフィルタ (Substance 3D Designer 13.0)

久しぶりのブログ更新

FunctionグラフのWhile Loop

つい先日のバージョンアップで待望の[While Loop]ノードが追加されました。
今はFunctionグラフでしか使用できませんが、個人的にはかなり大きな追加要素だと思っています。
今回の更新ではこのノードの使い方を解説し、ついでに以前作成したSymmetric Nearest Neighbor (SNN) フィルタを[While Loop]ノードを使って実装してみます。

While Loop解説

利用法

[While Loop]は前述の通り、Functionグラフノードとして提供されています。

入力コネクタは3つ、出力コネクタは1つです。
[Init]入力コネクタは[While Loop]に入る前の処理です。ループを行う前の初期化などを接続します。
[Exit Cond.]入力コネクタは[While Loop]を抜ける条件を指定します。C++のwhile命令はループを継続する条件を指定しますが、こちらは逆を指定する点に注意が必要です。
[Loop Body]は実際のループ処理そのものです。ここに接続した処理が指定条件を満たすまでループされます。
また、パラメータとして[Max Iterations]が存在します。これは無限ループを抑制するためのパラメータで、[Exit Cond.]が必ずfalseになるような条件であっても、このパラメータで指定された回数以上はループが回らないようになっています。

では実際に使ってみましょう。
わかりやすさ重視のため、[Value Processor]をグラフに配置し、[Edit]ボタンを押してFunctionグラフ内に入ります。
そして以下のようにノードを組んでみましょう。

[While Loop]を利用する際、一時変数を設定する[Set]ノードは非常に重要となります。
[Exit Cond.]の条件式内で一時変数を取得し、条件を調べたりする必要が出てくるためです。

まず、[Init]ノードには[Set]ノードで変数iを0で初期化します。
終了条件の[Exit Cond.]は[Get Float]ノードでiを取得し、この値が10以上になったら終了するようにしています。 [Loop Body]はiを取得して1を加算、それをiに再度設定します。つまりインクリメントしているだけです。
[While Loop]の唯一の出力は[Loop Body]の出力となりますので、これをグラフの出力として設定すると結果は最後にiに設定された値、つまりループ終了条件の10という数値になります。
実際に[Value Processor]の結果は"10"となっているはずです。

さて、次はもう少し複雑な処理を行いましょう。1~10までの整数を加算した合計を求めるだけのグラフです。
C/C++Pythonなどを使えば簡単に作れる処理ですが、グラフで再現しようとするとやや面倒です。
答えは以下のようになります。

ここで大切なのは[Sequence]ノードです。
このノードは複数の処理を別個に行うためのノードで、入力コネクタの[In]に最初の処理、[Last]に次の処理を接続します。出力は[Last]の入力結果となります。
Substance 3D DesignerではUEのBlueprintのような、実行コネクタは存在しません。
[Set]ノードを見るとわかりやすいですが、入力は設定する数値、出力は入力に接続した結果をそのまま返します。
ここにさらに[Set]ノードを配置しても、別の変数に同じ数値を入れることしかできず、別の数値を設定することができません。
そのため、[Init]コネクタに接続しているように、2つの[Set]ノードを別々に用意し、[Sequence]ノードで接続する必要があります。

この処理は[Loop Body]でも同様です。
ループで実行する処理は変数viを加算する処理(つまり v += i)と、ループカウントであるiをインクリメントする処理の2つです。
どちらも最終的に[Set]ノードで変数を設定するため、[Init]と同様に[Sequence]を利用する必要があります。

また、[While Loop]の先でも[Sequence]を利用します。
[While Loop]の出力は[Loop Body]への入力で、これはその前の[Sequence]の[Last]ですので、当然変数iの値となります。
最終的に欲しいのはvの値ですので、[Sequence]の[In]に[While Loop]の出力を接続し、[Last]で変数vを取得して接続します。

この結果は"55"となります。[Value Processor]の結果を確認してみてください。

注意

[While Loop]を使用する場合、[Loop Body]に接続したノードとその他の処理で利用するノードの共有に注意が必要です。
どうやら[Loop Body]と[Exit Cond.]は独立したブロックとして処理されなければならないようで、別の部分で使用しているノードを利用すると正常に処理されないようなのです。

以下の画像は先程の処理ですが、①と②と書かれたノードに注目してください。

①は定数である1を設定しているノードで、②は変数vを取得するノードです。
パッと見ではどちらも共有することが可能に見えますので、どちらか一方を削除してもう一方に接続してみてください。
①と②のどちらかだけでも共有してしまうと、[Value Processor]の結果は"0"となってしまいます。
どうやら処理が正常に実行されず、必ず"0"が入ってしまうようです。

ただし、[Loop Body]と[Exit Cond.]は同じブロック扱いのようで、[Exit Cond.]に接続している変数iを取得するノードを[Loop Body]で共有しても問題なく動作しました。 しかしながら、混乱の元になるのできちんとブロック分けはしておくことをおすすめします。

do whileを実現する

[Loop Body]と[Exit Cond.]が同一ブロック扱いなのであれば、[Loop Body]の内容を[Exit Cond.]で処理しても問題ないのでは?
と思ったのでやってみました。

これでも正常動作しました。
[While Loop]ノードは[Init]→[Exit Cond.]→[Loop Body]→[Exit Cond.]→[Loop Body]→...→ループ後処理という順序で処理をしているようです。
ですので、このような接続方法にすることで、C/C++のdo while的な処理も可能ということになります。
ただし、[Loop Body]に何も接続していないとループが回らないようですので、害のないノードを接続しておく必要はあります。

しかし、現在の仕様では可能な処理というだけで、今後も問題なく利用できるかどうかは未知数です。
通常であれば[Loop Body]内で繰り返し処理を行うべきなので、どうしてもという場合以外は使用しないことをおすすめします。

実例 Symmetric Nearest Neighbor

というわけで本題です。
Symmetric Nearest Neighbor (SNN) は絵画っぽい画像フィルタです。
解説とHLSL実装についてはもんしょの巣穴を参照してください。

sites.google.com

このフィルタは以前もSDで作成したのですが、ループ処理がなかったため5x5、7x7、9x9のカーネルサイズで頑張って1つ1つ丁寧に実装していました。
当時作成した9x9の[Pixel Processor]は以下です。

ノード数はそんなに多くないように見えますが、定数を変更して接続して…を繰り返して発狂しそうになりましたね。
しかもカーネルサイズが変更できないですし、これ以上のカーネルサイズは心が折れて実装できませんでした。

では、ループを使って実装したものを見てみましょう。

ノード数はそれなりに多く見えますが、処理の流れも接続もわかりやすいかと思います。
なにより、これだけでカーネルサイズを16まで変更できるようになっています。
なお、カーネルサイズは片側のサイズで指定する形ですので、1を指定すると3x3、16なら33x33で処理します。

[While Loop]は3つ利用しています。これは元のHLSL実装でもforループを3つ使っているためです。
リアルタイム実装でなければ2重ループで全ピクセルに対して処理してもさほど問題ないとは思いますが、慣れるために元実装と似たような処理にしています。
3つの内2つは2重ループで使用しています。2重ループは分かりにくくなりやすいので注意が必要だと思います。
解説を…と思ったのですが、特に解説するところがないので、気になる方は以下からダウンロードして試してみてください。

www.dropbox.com

ご自由にお使いください。
Substance 3D Designerの教科書』で紹介しているKuwaharaフィルタも同様に作成することができるでしょう。
興味のある方は練習がてらチャレンジしてみてもいいのではないでしょうか?