Substance DesignerのPythonプラグインの基本

東ゲ部もくもく会で調べた内容と、非常に簡単なプラグインの実装についてです。

Substance DesignerのPythonプラグインは去年の夏のアップデートだかで入っていたのですが、この段階では残念ながら現在編集中のグラフに対してあれやこれや出来ることが少なかったですね。
出来ることというと、グラフの再計算、ノードの整列、編集しているグラフとは別のグラフを新規作成して中身を作成とか、まあ、そんな感じでした。

しかし、年末のアップデートでいろいろと機能が追加されているというアップデート情報がありました。
その調査をしていなかったので、重い腰を上げて調査した次第です。

プラグイン作成方法

プラグインの作成方法は簡単です。
まず、メインメニューの[ウィンドウ]→[Python Editor]を選択してエディタを立ち上げます。
エディタ内メニューの[File]→[New Plugin]を選択し、ウィザードに従ってフォルダ名やプラグイン名を設定して[OK]を押すだけです。

この段階でプラグインは読み込まれ、メインメニューの[Scripts]の下に作成した名前のプラグインが追加されていることでしょう。

プラグイン配置

最初に作成したプラグインは前述の通りにメインメニューの[Scripts]直下に配置されます。
しかし、すべてのプラグインを直下に置くのは整理しづらいので困りますね。
そんな場合は以下の方法で別の場所に配置することが出来ます。

コード内のプラグインを作成しているコード部分に、sdplugins.PluginDesc() という関数があります。
この関数の引数である aPluginLocation を変更することで配置場所を変更可能です。

[Scripts]メニュー以下にフォルダを作ってその中に配置
aPluginLocation=sdplugins.PluginLocationMenu(sdplugins.MenuId.Scripts, 'MyPlugins', 0)

この例では [MyPlugins] フォルダを作成して、その中に配置します。
f:id:monsho:20190114162605p:plain

メインツールバー
aPluginLocation=sdplugins.PluginLocationToolbar(sdplugins.ToolBarId.Main, 0)

f:id:monsho:20190114162725p:plain

グラフエディタツールバー内
aPluginLocation=sdplugins.PluginLocationToolbar(sdplugins.ToolBarId.SBSCompGraph, 0)

f:id:monsho:20190114162813p:plain

ちなみにですが、aIconFileAbsPath にアイコン画像のファイルのフルパスを入力するとアイコン画像を変更することが出来ます。

現在編集中のグラフ、及び選択中のノードの取得

ここから実際のコードになります。
処理コードを書く場合は、ウィザードで作成した init.py の "# Put your code here" とコメントされている部分に書いていきましょう。

また、APIリファレンスは [ヘルプ]→[Python API Documentation] から参照できます。

aLocCont = aContext.getSDApplication().getLocationContext()
aSelectNodes = aLocCont.getSelectedNodes()
aGraph = aLocCont.getCurrentGraph()

SDLocationContext を取得後、そこから getSelectedNodes() 命令で選択中のすべてのノードを、getCurrentGraph() 命令で編集中のグラフを取得します。

ノードの追加

ノードをグラフに追加する場合、アトミックノードを追加するかグラフノード(.sbsで作成されたノード)を追加するかで作成方法が異なります。

アトミックノードの追加
aUniformNode = aGraph.newNode('sbs::compositing::uniform')

アトミックノードを新規作成するのは簡単です。
ノードの名前は sbs::compositing::~ で ~ 部分についてはAPIリファレンスを参照してください。

グラフノードの追加
aPackageMgr = aContext.getSDApplication().getPackageMgr()
aPackage = aPackageMgr.loadUserPackage('D:/test/SubstanceAdventCalender.sbs')
aCompNode = aGraph.newInstanceNode(aPackage.findResourceFromUrl('Day1_WoolKnit'))

こちらはちょっと面倒です。
まず、SDApplication クラスから SDPackageMgr を取得します。

グラフノードを作成するには、このパッケージマネージャに.sbsファイルを管理させなければなりません。
そのため、loadUserPackage() 命令で.sbsファイルをロードさせます。ファイル名はフルパスで指定しなければいけません。

パッケージの読み込みに成功したら findResourceFromUrl() 命令で追加したいグラフ名を指定します。

この手法は自前のグラフノードだけでなく、Substance Designer標準のグラフノードでも同じ作法が必要のようです。

ノードのプロパティを変更する

aPPNode = aGraph.newNode('sbs::compositing::pixelprocessor')
aPPNode.setInputPropertyInheritanceMethodFromId('$tiling', SDPropertyInheritanceMethod.Absolute)
aPPNode.setInputPropertyValueFromId('$tiling', SDValueEnum.sFromValueId('sbs::compositing::tiling', 'no_tiling'))
aPPNode.setInputPropertyValueFromId('colorswitch', SDValueBool.sNew(False))

ノードのプロパティを変更するには setInputPropertyValueFromId()setPropertyValue() を使います。
前者はプロパティの名前から値を設定、後者はプロパティオブジェクトに直接設定です。

プロパティオブジェクトは生成時に取得するか、getProperties()getPropertyFromId() で取得できます。

また、一部の標準プロパティは親ノードやグラフの設定を受け継ぎますので、その場合は設定が無効になるものも多いです。
そのようなプロパティは setInputPropertyInheritanceMethodFromId() 命令で継承方法を変更することが可能です。

ノードを接続する

aUniformOutput = aUniformNode.getProperties(SDPropertyCategory.Output)[0]
aUniformNode.newPropertyConnectionFromId(aUniformOutput.getId(), aPPNode, 'input.connector')

ノードの接続は newPropertyConnectionFromId() 命令か、newPropertyConnection() を使います。
これらの命令も前者はID指定、後者はプロパティオブジェクト指定です。

接続する場合は、接続元のノードに対して命令を発行し、引数として、出力ピンのプロパティ、接続先ノード、接続先入力ピンのプロパティとなります。

グラフにプロパティを追加して、値を設定する

aProp = aGraph.getPropertyFromId("test_value", SDPropertyCategory.Input)
if aProp is not None:
    aGraph.deleteProperty(aProp)
aProp = aGraph.newProperty("test_value", SDTypeInt.sNew(), SDPropertyCategory.Input)
aGraph.setPropertyValue(aProp, SDValueInt.sNew(0))

newProperty() 命令によって入力パラメータを追加できます。
setPropertyValue() はデフォルト値を設定することが出来ます。

ただ、何も考えずにプロパティを追加してしまうと、同一名のプロパティが存在した場合に自動的に末尾に番号をつけられます。
そのため、上記の例では deleteProperty() 命令で最初に同名のプロパティを削除しています。

サンプル

        # Put your code here
        selNodes = aContext.getSDApplication().getLocationContext().getSelectedNodes()
        aGraph = aContext.getSDApplication().getLocationContext().getCurrentGraph()
        for n in selNodes:
            if n.getDefinition().getId() == 'sbs::compositing::blend':
                srcProp = n.getPropertyFromId('source.connector', SDPropertyCategory.Input)
                dstProp = n.getPropertyFromId('destination.connector', SDPropertyCategory.Input)
                srcCnts = n.getPropertyConnections(srcProp)
                dstCnts = n.getPropertyConnections(dstProp)
                if srcCnts.getSize() > 0 and dstCnts.getSize() > 0:
                    n0 = srcCnts[0].getInputPropertyNode()
                    p0 = srcCnts[0].getInputProperty()
                    n1 = dstCnts[0].getInputPropertyNode()
                    p1 = dstCnts[0].getInputProperty()
                    n.deletePropertyConnections(srcProp)
                    n.deletePropertyConnections(dstProp)
                    n0.newPropertyConnection(p0, n, dstProp)
                    n1.newPropertyConnection(p1, n, srcProp)
        pass

とりあえず run() 関数の内部だけをコピペしました。
どんなプラグインかというと、選択中のBlendノードでForegroundとBackgroundのピンの入力を逆転するだけです。
ForegroundをBackgroundに、BackgroundをForegroundにします。
ボタン1つで簡単に切り替えられる便利機能!というほど便利でもないですが、とりあえずの練習用ということで。

今後はPixelProcessorの関数をPythonで書いてみたいですね。
ノードで組むの面倒なので…