Substance Automation Toolkit の紹介

Substance Designer 2017のライセンスを持っている方はSubstance Automation Toolkit(以下SAT)の使用が可能です。
f:id:monsho:20171103103811p:plain Allegorithmic公式サイトの自身のアカウントからDLできるはずです。

このツールキットはその名の通り、自動化を主な目的としたツール群です。
ドキュメントは以下です。

Automation Toolkit Home - Substance Automation ToolKit - Allegorithmic Documentation

Substance Designer 6以前のライセンスであればSubstance Batch Tools(以下SBT)というものが使えましたが、SATはSBTを引き継いで機能を追加したものと考えてください。

同梱されているコマンドラインツールは以下のものがあります。

sbsbaker

メッシュからAO、Normal等のメッシュ固有のマップを生成します。
ハイポリメッシュの指定もできるので、マップのベイクを一括で行いたい場合に使えます。

sbscooker

.sbsファイルから.sbsarファイルを生成するツールです。
SD作業者がSP作業者にマテリアルやフィルタを提供する場合、.sbsarで提供するのが基本です。
このツールを使えば自動的に更新された.sbsファイルから.sbsarファイルを生成するようにすることが可能です。

sbsmutator

.sbsファイルに修正をかけることができるツールのようです。
例えば.sbsで使用しているイメージデータを入れ替えたり、パラメータを変更したりして別の.sbsを生成することができるようです。

sbsrender

.sbsarからイメージをレンダリングするツールです。
SDからテクスチャイメージをエクスポートするのを自動化することができます。
入力イメージ、パラメータ変更もできるので、テクスチャエクスポートをしたいだけならsbsmutatorは不要です。

sbsupdater

.sbsの対応バージョンを更新するツールです。
SDが更新されると、以前作成した.sbsを読み込む際に更新処理が入ります。
SDで使用する際にいちいちそれらが出力されるのは面倒なので、このツールで一括更新してしまうとよいでしょう。

多分、sbsupdater以外はSBTにもあったツールだと思います。

SATではこれらのコマンドラインツールに加え、Python用のモジュールが追加されています。
PythonAPIによってこれらのツールと同じことができるようで、かなり便利になりました。

また、このモジュールによって.sbsファイルの生成もできるようになっています。
通常であればSDを立ち上げて作成する.sbsですが、PythonAPIによってノード追加、接続などができるわけです。
使い道は…これがなかなか難しい。
簡単な接続ならPythonでも簡単に行えますが、SDを使った方がより簡単です。
複雑なマテリアルを作成するならそれこそSD使った方がいいです。
あるとすると、2つのマテリアルを指定のマスクイメージでブレンドするというノード接続において、マテリアルをいろいろ変化させて様々な組み合わせのマテリアルを作成したい場合でしょうか。
C#などでUIを作り、自身のライブラリ内のマテリアルとマスクイメージを選択できるようにしてとっかえひっかえ組み合わせを試すとか?

まあ、イマイチ使い道が思い浮かばないのですが、簡単なPythonコードを紹介しましょう。

# coding: utf-8
import os
import sys
import pysbs

from pysbs import python_helpers
from pysbs import context
from pysbs import sbsenum
from pysbs import sbsgenerator

def CreateHelloWorld(destFileAbsPath):
  aContext = context.Context()

  sbsDoc = sbsgenerator.createSBSDocument(aContext,
    aFileAbsPath = destFileAbsPath,
    aGraphIdentifier = 'TestMaterial')

  sbsGraph = sbsDoc.getSBSGraph('TestMaterial')

  startPos = [48, 48, 0]
  xOffset = [192, 0, 0]
  yOffset = [0, 192, 0]
  xyOffset = [192, 96, 0]

  # input nodes
  input_0 = sbsGraph.createInputNode(aIdentifier = 'input_0',
    aGUIPos = startPos,
    aColorMode = sbsenum.ColorModeEnum.COLOR,
    aAttributes = {sbsenum.AttributesEnum.Label: 'Input0'})
  input_1 = sbsGraph.createInputNode(aIdentifier = 'input_1',
    aGUIPos = input_0.getOffsetPosition(yOffset),
    aColorMode = sbsenum.ColorModeEnum.COLOR,
    aAttributes = {sbsenum.AttributesEnum.Label: 'Input1'})

  # noise
  noise_node = sbsGraph.createCompInstanceNodeFromPath(aSBSDocument = sbsDoc,
    aPath = 'sbs://crystal_1.sbs/crystal_1',
    aGUIPos = input_1.getOffsetPosition(yOffset))

  # blend
  blend_node = sbsGraph.createCompFilterNode(aFilter = sbsenum.FilterEnum.BLEND,
    aGUIPos = input_1.getOffsetPosition(xOffset),
    aParameters = {sbsenum.CompNodeParamEnum.BLENDING_MODE: sbsenum.BlendBlendingModeEnum.COPY,
    sbsenum.CompNodeParamEnum.OPACITY: 0.8},
    aInheritance = {sbsenum.CompNodeParamEnum.OUTPUT_SIZE: sbsenum.ParamInheritanceEnum.PARENT})

  # output nodes
  output_node = sbsGraph.createOutputNode(aIdentifier = 'BlendResult',
    aGUIPos = blend_node.getOffsetPosition(xOffset),
    aOutputFormat = sbsenum.TextureFormatEnum.DEFAULT_FORMAT,
    aAttributes = {sbsenum.AttributesEnum.Description: 'noise masked blending result.'})

  # connection
  sbsGraph.connectNodes(aLeftNode = input_0, aRightNode = blend_node,
    aRightNodeInput = sbsenum.InputEnum.SOURCE)
  sbsGraph.connectNodes(aLeftNode = input_1, aRightNode = blend_node,
    aRightNodeInput = sbsenum.InputEnum.DESTINATION)
  sbsGraph.connectNodes(aLeftNode = noise_node, aRightNode = blend_node,
    aRightNodeInput = sbsenum.InputEnum.OPACITY)
  sbsGraph.connectNodes(aLeftNode = blend_node, aRightNode = output_node)

  sbsDoc.writeDoc()

  return True

if __name__ == "__main__":
  destFileAbsPath = python_helpers.getAbsPathFromModule(sys.modules[__name__], 'sample/TestMaterial.sbs')
  CreateHelloWorld(destFileAbsPath)

まずpysbsからコンテキストを取得し、これを元にして.sbsファイルを生成します。これはドキュメントと呼ばれます。
ドキュメントには複数のグラフを作成することができます。これはそのままSubstanceのグラフです。
今回は TestMaterial.sbs というドキュメントに TestMaterial というグラフを作成しました。

作成したグラフ内に各ノードを作成していきます。
まずは入力ノードを2つ作成します。createInputNode()関数を用いています。
なお、ノードの位置は直接指定することもできれば、あるノードからの相対位置も指定できます。

次に適当なノイズノードを持ってきます。
createCompInstanceNodeFromPath() 関数を使用することで、特定のパスにある.sbsから特定のノードを取得できます。
SDの標準ノードについては "sbs://" から始まるパスでOKのようです。

次はブレンドノード。
組み込みノードについては createCompFilterNode() 関数で生成できますが、一部のノード(入力や出力など)は特定の関数があります。

ノードの最後は出力ノードです。Descriptionにコメントを残すことも可能です。

コメントと言えば、フレームも作成できるみたいですね。

配置したノードはそれだけでは置かれているだけなので、connectNodes() 命令で接続を行います。
左と右のノードと、それぞれどこのピンに接続するかを指定しますが、ピンが1つだけの場合は省略可能です。
なお、存在しないピンに入出力しようとすると例外を発生させます。入出力のどちらが悪いのかは教えてくれません。うぼぁぁ

最後にドキュメントを writeDoc() すれば.sbsが出力されます。
出力された.sbsがこちらです。

f:id:monsho:20171103125009p:plain

ちゃんと接続されてますね。
まあ、正直な話、これだけコード書くよりノード接続した方が速いです。
なので、こういうことをしたいのであればPythonを使う理由はほとんどありません。
しかし、ノード接続では面倒なものもありまして…
次回はその辺の、ノード接続では面倒なものを簡単に作成する方法について書きます。