Substance Automation Toolkit で Pixel Processor を作成する

前回、Substance Automation Toolkit (SAT) を使ってノードを接続する方法について解説しました。
しかしこの手法、普通にマテリアルを作成するには不向きです。
テンプレート的な処理を大量に作る分にはいいのですが、1点もののマテリアルを作るなら普通にSubstance Designerを使った方がよいでしょう。

しかし現在のSATには式グラフを比較的簡単に生成する機能が提供されています。
sbsMathというモジュールがそれです。
SATの標準モジュールだけではプログラムコードを使ってノードを繋げることしかできませんが、sbsMathを利用することでプログラムで数式を実装する感覚で式グラフを作成できます。
が、このモジュールはなぜかSATの標準モジュールとして提供されていません。
いや、正確には提供されているのですが、なぜか "MyDocument/Allegorithmic/Substance Automation Toolkit" 内にインストールされます。

sbsMathを利用しているサンプルは同じフォルダ内にある raytracer.py です。
このサンプルは Pixel Processor を利用して各ピクセルごとにレイトレースして球をライティングしてレンダリングするサンプルです。
複雑に見えますが、sbsMath の機能を一通り使っているのでサンプルとしては十分でしょう。
とは言え、いきなりサンプルを全部解説、というのもあれなので、とりあえずより簡易なサンプルで解説しましょう。

今回作成したサンプルは、2枚のノーマルマップの各ピクセル内積を取るものです。
何かに使えるのか?という話をされると…使い道は全くないんですが、これを Pixel Processor で作成しようとすると意外と面倒だったりします。

内積計算自体は式ノードに存在するのでそのまま使えばいいのですが、ノーマルマップをサンプルした値はそのままベクトルとして使用できません。
カラーマップは負の値を保存できないため、ノーマルマップに格納されている値は -1~1 を 0~1 に圧縮しています。
つまりベクトルとして利用するには 0~1 を -1~1 に伸張しなければなりません。
そのための計算式は以下の通りです。

v = color * 2 - 1

まあ、正直な話、この程度の処理ならやっぱりSDで作った方が早いような気もしますがね…

とにかく、コードを見ていきましょう。
まず、インポートするモジュールですが、sbsMathを追加する必要があります。

import sbsMath.sbsmath as sm
import sbsMath.sbsmath_tools as st

sbsmathとsbsmath_toolsをそれぞれ sm, st という名前空間で取り扱います。

次にノードグラフの作成部分ですが、基本的には前回と同じです。
作成するのはカラーの入力ノードを2つ、それぞれID normal_0, normal_1 とします。
出力ノードはグレイスケールです。
最後の1つは Pixel Processor です。

    # pixel proc
    pix_proc_node = sbsGraph.createCompFilterNode(aFilter = sbsenum.FilterEnum.PIXEL_PROCESSOR,
        aGUIPos      = input_normal_0.getOffsetPosition(xOffset),
        aParameters  = {sbsenum.CompNodeParamEnum.COLOR_MODE:sbsenum.ColorModeEnum.GRAYSCALE})

出力はグレイスケールにしておきます。

問題なのは Pixel Processor への入力です。
このノードは最初の段階では入力ピンが1つしかありませんが、そのピンへノードを繋ぐと次のピンが出現するという仕組みになっています。
そのため、あるノードを Pixel Processor の入力ピンへノードを接続する場合、名前を直接指定する必要があります。

    # connection
    sbsGraph.connectNodes(aLeftNode = input_normal_0, aRightNode = pix_proc_node,
        aRightNodeInput = 'input')
    sbsGraph.connectNodes(aLeftNode = input_normal_1, aRightNode = pix_proc_node,
        aRightNodeInput = 'input:1')

最初のピンは input という名前で接続できます。
1つ目のピンへの接続はピンの名前を指定しなくても勝手に接続されるのでさほど問題はありません。
これに対して2つ目のピンは input:1 という名前で指定する必要があります。
ここから先に接続する場合も input:n という形で順次接続できます。
接続順番も、かならず0番から進めてください。1つ以上飛ばして接続しようとすると失敗します。

では、Pixel Processor の式グラフの作成です。命令は簡単で、以下のようにします。

    # pixel proc function
    st.generate_function(NormalMapDot, sbsDoc, fn_node = pix_proc_node.getPixProcFunction())

sbsmath_tools.generate_function() を使うと、特定の式グラフに対して式ノード生成関数をを走らせます。
第1引数に渡している NormalMapDot という関数がノードの作成を行ってくれます。
実装は以下のようになっています。

def NormalMapDot(fn):
    pixel_pos = fn.variable('$pos', widget_type=sbsenum.WidgetEnum.SLIDER_FLOAT2)

    normal0 = sm.create_color_sampler(pixel_pos, 0)
    normal1 = sm.create_color_sampler(pixel_pos, 1)

    normal0 = sm.swizzle_float3(normal0, [0, 1, 2])
    normal1 = sm.swizzle_float3(normal1, [0, 1, 2])

    normal0 = normal0 * fn.constant([2.0, 2.0, 2.0]) - fn.constant([1.0, 1.0, 1.0])
    normal1 = normal1 * fn.constant([2.0, 2.0, 2.0]) - fn.constant([1.0, 1.0, 1.0])

    dot_p = sm.dot(normal0, normal1)
    return fn.generate(dot_p)

sbsmathモジュールには式グラフで使用可能なノードが登録されています。
ここではベクトル要素のスイズルを行う SwizzleFloat3 や内積演算の Dot を使っています。
ノードや計算式の結果は変数に登録することができ、最終出力をgenerate()関数に引き渡せば自動的にグラフが生成されるというわけです。
ね、簡単でしょ?

作成したグラフは以下のようになります。
f:id:monsho:20171115003858p:plain
そして Pixel Processor の中身は以下です。
f:id:monsho:20171115003952p:plain
割といい感じにノードの配置もしてくれてますね。
ノード数も少ないので、やはりこれくらいならSD上で作成した方が早いかもしれませんが、パッと見でどういう計算しているのかわかりにくいのもノードベースならではかなとも思います。

さらに複雑な計算になるとノードベースではやりたくなくなるかもしれませんので、特にエンジニアの方はPythonでの生成法も覚えておくとあとあと便利かもしれませんよ。