Substance Automation Toolkitによる簡単自動化

先日、Substanceゆるゆる会にて某氏から聞かれた話。
Substance Designerでテンプレート的なグラフを作成し、特定フォルダ内のすべてのテクスチャに対してそのグラフを適用してテクスチャを生成したいんだけど、できませんか?と。

で、できらぁ!

ということで、Substance Automation Toolkitで上のようなことをやる方法について書いてみます。
今回のサンプルには以下の環境が必要です。

さて、既存のテクスチャに何らかの処理を行いたいという話は割とよくある話だと思いますが、Substanceらしいこんな話をでっち上げてみましょう。

あなたはあるCG映像会社の社員です。
ある日、今やるべきことは流行りのリアルタイムレンダリングだ!Unreal Engine 4で昔作ったムービーをリアルタイムレンダリングするぞ!と社長に言われました。

そこでまず昔のデータをUE4に乗っけてみたところ、ノーマルマップもない状態だしPBR対応もしてないしでモデルだけだしても結果はよくありません。
とはいえ期間もないので全部作り直すのは不可能。多くのリソース、特に背景は昔のものを手続き的にブラッシュアップするだけにとどめたい。
そこでSubstance Designerを使って昔のテクスチャを手続き的に処理、ノーマルやラフネスを出力すればなんとかなりそうだというところまでは検証できました。

しかしリソースは大量で、すべてのテクスチャに対してSDでグラフ作成→テクスチャ出力なんてやってられない。
どうにか自動化出来ないだろうか?

はい、身につまされた人はいませんね?
え、いますか?
大変ですね。

すでにこんな経験をしている人ならすでに自動化してるとは思いますが、今後こういう事態に陥る人向けにこの記事を書きます。

まずはSDでテンプレートとなるグラフを作成します。
これにはまず適当なテクスチャ相手に問題なく動作するグラフを作成してください。
作成できたら入力しているテクスチャを入力ノードに変換します。
最終的に作成されるグラフはこんなものになるはずです。

f:id:monsho:20190307161025p:plain
テンプレートグラフ
今回はサンプルとしてB2M LiteでDiffuseテクスチャから各マップを出力するだけのものを作成しています。
もちろんこれは一例に過ぎないので、自分たちに必要な形でテンプレートグラフを作成してください。

次にSubstance Automation Toolkitですが、これを利用する方法は2種類あります。
1つはBatchtoolsを使用する方法で、インストールフォルダ内にコンソールアプリケーションがいくつか存在していますのでこれを使います。
直接使用できるので、ぶっちゃけバッチファイルを書くだけでも使えます。

もう1つはPythonを使う方法です。
といっても、実際にはPythonを通してBatchtoolsを使用するだけなので実はやってることは同じだったりします。
今回はこちらのPythonを通してやる方法と採用しています。

まず、Substance Automation ToolkitのPython APIをインストールします。
(インストールフォルダ)/Python API/Pysbs-2018.3.0_install.bat を叩いてPython APIをインストールしてください。
Pythonがインストールされてパスが通っていれば正常にインストールされるはずです。
MacLinuxについてはよくわからないですが、pip使えれば大丈夫じゃないかと。

次にテキストエディタでコードを書きます。
今回のコードは全文をここに書いておきますが、今後Substance系のコードが増えたらGitHubリポジトリ作るかもです。

import os
import sys
import subprocess
import pysbs
from pysbs import batchtools
from pysbs import context

if __name__ == "__main__":
    aContext = context.Context()
    template_path = 'MyDocument/Allegorithmic/Substance Automation Toolkit/specialize_test/B2MTest.sbs'

    # generate specialized sbs.
    output_path = 'MyDocument/Allegorithmic/Substance Automation Toolkit/specialize_test/'
    output_name = 'spec_test'
    base_color_image = 'MyDocument/Allegorithmic/Substance Automation Toolkit/specialize_test/Bricks_Test_basecolor.tga'
    metallic_image = 'MyDocument/Allegorithmic/Substance Automation Toolkit/specialize_test/Bricks_Test_metallic.tga'
    base_color_image_connect = 'input_diffuse@path@' + base_color_image + '@format@JPEG'
    metallic_image_connect = 'input_metallic@path@' + metallic_image + '@format@JPEG'
    proc = batchtools.sbsmutator_edit(
        input=template_path,
        presets_path=aContext.getDefaultPackagePath(),
        output_name=output_name,
        output_path=output_path,
        connect_image=(base_color_image_connect, metallic_image_connect),
        stderr=subprocess.PIPE)
    (out, err) = proc.communicate()
    proc.wait()
    if err:
        print(err)
        sys.exit(1)

    # cook sbsar.
    proc = batchtools.sbscooker(
        inputs=os.path.join(output_path, output_name) + '.sbs',
        includes=aContext.getDefaultPackagePath(),
        size_limit=13,
        output_path=output_path)
    (out, err) = proc.communicate()
    proc.wait()
    if err:
        print(err)
        sys.exit(1)

    # render textures.
    output_size = 11
    proc = batchtools.sbsrender_render(
        inputs=os.path.join(output_path, output_name) + '.sbsar',
        output_name='{inputName}_{outputNodeName}',
        output_path=output_path,
        output_format='tga',
        set_value=('$outputsize@%s,%s' % (output_size,output_size)))
    (out, err) = proc.communicate()
    proc.wait()
    if err:
        print(err)
        sys.exit(1)

フォルダ名やファイル名は適当ですので、必要に応じて変換してください。
また、今回のサンプルは1つの特殊化しかしていません。フォルダ内のすべてのテクスチャ、という感じにする場合は別途ファイル列挙などが必要になります。

簡単に解説していきます。
まず、インストールしたSubstancePython APIをインポートします。

import os
import sys
import subprocess
import pysbs
from pysbs import batchtools
from pysbs import context

今回はbatchtoolsとcontextだけ利用します。

その後はSubstanceのコンテキストを取得します。
コンテキストはSubstancePython APIを使用する上で基本となるものですが、今回はライブラリフォルダを取得するためだけに使用します。

if __name__ == "__main__":
    aContext = context.Context()

最初に実行するツールはsbsmutatorというツールです。
このツールは.sbsファイルの情報を取得したり、パラメータ等を変更して特殊化したりするためのツールです。
このツールを使うことで、テンプレートグラフの入力イメージを変更した特殊なグラフを作成し、.sbsファイルとして保存します。
そのためには batchtools.sbsmutator_edit() 命令を使用します。
なお、batchtools以下の命令はほぼ全てsubprocess.Popen命令の戻り値を返します。

    proc = batchtools.sbsmutator_edit(
        input=template_path,
        presets_path=aContext.getDefaultPackagePath(),
        output_name=output_name,
        output_path=output_path,
        connect_image=(base_color_image_connect, metallic_image_connect),
        stderr=subprocess.PIPE)
    (out, err) = proc.communicate()
    proc.wait()

コンソールアプリケーションであるsbsmutatorへの引数は、通常 "--" で始まるオプションを利用します。
例えば、入力イメージとしてファイルを指定する場合、"--connect-image" というオプションを利用します。
しかし、これはPythonの変数としては使用できない文字列ですので、上記の例の場合は "connect_image" を指定することになります。
先頭の "--" は削除、途中にある "-" は "_" に変更します。

また、複数の入力を指定する場合もあります。
"--connect-image" はオプション1つにおいて1つの入力を指定することになりますので、コンソールコマンドとしては以下のように指定することになります。

sbsmutator.exe --connect-image "input0@path@basecolor.png" --connect-image "input1@path@metallic.png"

Pythonの可変個引数で辞書を用いる場合、同じ引数名を複数指定はできませんので、1つの引数に対してタプルで複数指定すればOKです。

この命令で特殊化された.sbsファイルが出力されますので、今度はこれを.sbsarファイルに変換します。
このためにはやはりBatchtoolsのsbscookerを使用します。

    proc = batchtools.sbscooker(
        inputs=os.path.join(output_path, output_name) + '.sbs',
        includes=aContext.getDefaultPackagePath(),
        size_limit=13,
        output_path=output_path)

指定したパスに同名の.sbsarファイルが出力されます。
ちなみに、自分の環境ではちょっとした警告が出ていましたが、まあ特に問題はないんじゃないかなと思います。

最後に.sbsarファイルからテクスチャを出力します。
これにはsbsrendererを利用します。

    output_size = 11
    proc = batchtools.sbsrender_render(
        inputs=os.path.join(output_path, output_name) + '.sbsar',
        output_name='{inputName}_{outputNodeName}',
        output_path=output_path,
        output_format='tga',
        set_value=('$outputsize@%s,%s' % (output_size,output_size)))
    (out, err) = proc.communicate()
    proc.wait()

"--output-name" オプションにはSDで指定できるグラフ名、出力ノード名などが使用できます。
今回のサンプルでは使用する.sbsarファイルのファイル名と出力ノード名を利用していますが、もちろん固定の名前も可能です。
ただ、複数のイメージを出力する場合は出力ノード名は使うようにしましょう。名前がかぶります。

Substanceに渡す各種サイズですが、これは2のn乗のnの値を指定するようにしてください。
サンプルで使っている 11 なら2048になります。
この値に 2048 を直接指定したらひどい目にあいました。
画像ビューアで開けないサイズのテクスチャが出来たのですが、むしろレンダリングしてくれるんだなぁと感心してしまいました。

以上で一連の流れは完了です。
今回提示したソースコードはあくまでの一例でしかありませんし、実際の業務ではファイル名やフォルダ名を指定したり、列挙したファイルに対して処理をしたり、出力フォルダを作成したりといった処理が必要になります。
そのあたりも考慮に入れて参考にしていただけたらと思います。

Batchtoolsを使うと他にも.fbxからジオメトリ関連のマップ(ノーマルやAO、IDマップなど)をベイクし、IDマップに従って特定のマテリアルを割り当てるとかも可能です。
この手の自動化はうまくやれればかなりの効率化が可能ですので、是非いろいろ試してみてください。
そして、うちはこういう使い方したよ!みたいな情報があったらこっそり教えてくださいw

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で書いてみたいですね。
ノードで組むの面倒なので…

Weighted Blended OIT - UE 4.20.3対応

以前、UE4で簡単ではありますが Weighted Blended OIT を実装してみました。
monsho.hatenablog.com

この技術の詳細については上の記事と、そこにリンクしているペーパーをお読みください。
あまり使われていない技術だとは思うのですが、『Saints Row』シリーズでおなじみのVolitionの作品『Agents of Mayhem』で使用されていることがGDC 2018で発表されています。

www.gdcvault.com

こちらはウェイト関数や様々な問題に対して調整を行っていますので、もしこの技術を実装したいのであれば上のリンクを参考にしてみてください。

さて、今回は要望がありましたので、UE 4.12で実装していたものをUE 4.20.3に対応させてみました。
ほんとに単純に対応しただけなので、『Agents of Mayhem』くらいちゃんとやりたい!という人はこの修正を参考にしてきちんと作成してみましょう。

前回と同様にソースコードを修正しますので、お仕事で使用する場合は十分に注意してください。
では、各ソースコードの修正項目を見ていきます。

Engine/Source/Runtime/Renderer/Private/PostProcess/SceneRenderTargets.h
near by line 231

void FreeSeparateTranslucency()
{
    SeparateTranslucencyRT.SafeRelease();
    check(!SeparateTranslucencyRT);
    SeparateTranslucencyAlphaRT.SafeRelease(); // <- add
    check(!SeparateTranslucencyAlphaRT); // <- add
}

near by line 278

/** Separate translucency buffer can be downsampled or not (as it is used to store the AfterDOF translucency) */
TRefCountPtr<IPooledRenderTarget>& GetSeparateTranslucency(FRHICommandList& RHICmdList, FIntPoint Size);
TRefCountPtr<IPooledRenderTarget>& GetSeparateTranslucencyAlpha(FRHICommandList& RHICmdList, FIntPoint Size); // <- add

near by line 567

/** ONLY for snapshots!!! this is a copy of the SeparateTranslucencyRT from the view state. */
TRefCountPtr<IPooledRenderTarget> SeparateTranslucencyRT;
/** Downsampled depth used when rendering translucency in smaller resolution. */
TRefCountPtr<IPooledRenderTarget> DownsampledTranslucencyDepthRT;
TRefCountPtr<IPooledRenderTarget> SeparateTranslucencyAlphaRT; // <- add

Engine/Source/Runtime/Renderer/Private/PostProcess/SceneRenderTargets.cpp
near by line 1268

uint32 Flags = TexCreate_RenderTargetable;

// Create the SeparateTranslucency render target (alpha is needed to lerping)
FPooledRenderTargetDesc Desc(FPooledRenderTargetDesc::Create2DDesc(Size, PF_FloatRGBA, FClearValueBinding(), TexCreate_None, Flags, false)); // <- replace
//FPooledRenderTargetDesc Desc(FPooledRenderTargetDesc::Create2DDesc(Size, PF_FloatRGBA, FClearValueBinding::Black, TexCreate_None, Flags, false));
Desc.Flags |= GFastVRamConfig.SeparateTranslucency;

near by line 1279

// add this function
TRefCountPtr<IPooledRenderTarget>& FSceneRenderTargets::GetSeparateTranslucencyAlpha(FRHICommandList& RHICmdList, FIntPoint Size)
{
    if (!SeparateTranslucencyAlphaRT || SeparateTranslucencyAlphaRT->GetDesc().Extent != Size)
    {
        uint32 Flags = TexCreate_RenderTargetable;

        // Create the SeparateTranslucency render target (alpha is needed to lerping)
        FPooledRenderTargetDesc Desc(FPooledRenderTargetDesc::Create2DDesc(Size, PF_R16F, FClearValueBinding::White, TexCreate_None, Flags, false));
        Desc.Flags |= GFastVRamConfig.SeparateTranslucency;
        Desc.AutoWritable = false;
        Desc.NumSamples = GetNumSceneColorMSAASamples(CurrentFeatureLevel);
        GRenderTargetPool.FindFreeElement(RHICmdList, Desc, SeparateTranslucencyAlphaRT, TEXT("SeparateTranslucencyAlpha"));
    }
    return SeparateTranslucencyAlphaRT;
}

near by line 1355

TRefCountPtr<IPooledRenderTarget>* SeparateTranslucency;
TRefCountPtr<IPooledRenderTarget>* SeparateTranslucencyAlpha; // <- add
if (bSnapshot)
{
    check(SeparateTranslucencyRT.GetReference());
    check(SeparateTranslucencyAlphaRT.GetReference()); // <- add
    SeparateTranslucency = &SeparateTranslucencyRT;
    SeparateTranslucencyAlpha = &SeparateTranslucencyAlphaRT; // <- add
}
else
{
    SeparateTranslucency = &GetSeparateTranslucency(RHICmdList, SeparateTranslucencyBufferSize);
    SeparateTranslucencyAlpha = &GetSeparateTranslucencyAlpha(RHICmdList, SeparateTranslucencyBufferSize); // <- add
}
const FTexture2DRHIRef &SeparateTranslucencyDepth = SeparateTranslucencyScale < 1.0f ? (const FTexture2DRHIRef&)GetDownsampledTranslucencyDepth(RHICmdList, SeparateTranslucencyBufferSize)->GetRenderTargetItem().TargetableTexture : GetSceneDepthSurface();

    // replace clear and set render targets process.
#if 0
check((*SeparateTranslucency)->GetRenderTargetItem().TargetableTexture->GetClearColor() == FLinearColor::Black);
// clear the render target the first time, re-use afterwards
SetRenderTarget(RHICmdList, (*SeparateTranslucency)->GetRenderTargetItem().TargetableTexture, SeparateTranslucencyDepth,
   bFirstTimeThisFrame ? ESimpleRenderTargetMode::EClearColorExistingDepth : ESimpleRenderTargetMode::EExistingColorAndDepth, FExclusiveDepthStencil::DepthRead_StencilWrite);
#else
{
    ERenderTargetLoadAction ColorLoadAction = bFirstTimeThisFrame ? ERenderTargetLoadAction::EClear : ERenderTargetLoadAction::ELoad;
    ERenderTargetStoreAction ColorStoreAction = ERenderTargetStoreAction::EStore;
    ERenderTargetLoadAction DepthLoadAction = ERenderTargetLoadAction::ELoad;
    ERenderTargetStoreAction DepthStoreAction = ERenderTargetStoreAction::ENoAction;
    FRHIRenderTargetView ColorViews[2] = {
        FRHIRenderTargetView((*SeparateTranslucency)->GetRenderTargetItem().TargetableTexture, 0, -1, ColorLoadAction, ColorStoreAction),
        FRHIRenderTargetView((*SeparateTranslucencyAlpha)->GetRenderTargetItem().TargetableTexture, 0, -1, ColorLoadAction, ColorStoreAction)
    };
    FRHISetRenderTargetsInfo Info(2, ColorViews, FRHIDepthRenderTargetView(SeparateTranslucencyDepth, DepthLoadAction, DepthStoreAction, FExclusiveDepthStencil::DepthRead_StencilWrite));
    RHICmdList.SetRenderTargetsAndClear(Info);
}
#endif

near by line 1433

FTextureRHIParamRef RenderTargets[2]{}; // <- add
SetRenderTargets(RHICmdList, 2, RenderTargets, nullptr, 0, nullptr); // <- add

TRefCountPtr<IPooledRenderTarget>* SeparateTranslucency;
TRefCountPtr<IPooledRenderTarget>* SeparateTranslucencyAlpha; // <- add
TRefCountPtr<IPooledRenderTarget>* SeparateTranslucencyDepth;
if (bSnapshot)
{
    check(SeparateTranslucencyRT.GetReference());
    SeparateTranslucency = &SeparateTranslucencyRT;
    SeparateTranslucencyAlpha = &SeparateTranslucencyAlphaRT; // <- add
    SeparateTranslucencyDepth = SeparateTranslucencyScale < 1.f ? &DownsampledTranslucencyDepthRT : &SceneDepthZ;
}
else
{
    SeparateTranslucency = &GetSeparateTranslucency(RHICmdList, SeparateTranslucencyBufferSize);
    SeparateTranslucencyAlpha = &GetSeparateTranslucencyAlpha(RHICmdList, SeparateTranslucencyBufferSize); // <- add
    SeparateTranslucencyDepth = SeparateTranslucencyScale < 1.f ? &GetDownsampledTranslucencyDepth(RHICmdList, SeparateTranslucencyBufferSize) : &SceneDepthZ;
}

const FResolveRect SeparateResolveRect(
    View.ViewRect.Min.X * SeparateTranslucencyScale, 
    View.ViewRect.Min.Y * SeparateTranslucencyScale, 
    View.ViewRect.Max.X * SeparateTranslucencyScale, 
    View.ViewRect.Max.Y * SeparateTranslucencyScale
    );

RHICmdList.CopyToResolveTarget((*SeparateTranslucency)->GetRenderTargetItem().TargetableTexture, (*SeparateTranslucency)->GetRenderTargetItem().ShaderResourceTexture, SeparateResolveRect);
RHICmdList.CopyToResolveTarget((*SeparateTranslucencyAlpha)->GetRenderTargetItem().TargetableTexture, (*SeparateTranslucencyAlpha)->GetRenderTargetItem().ShaderResourceTexture, SeparateResolveRect); // <- add
RHICmdList.CopyToResolveTarget((*SeparateTranslucencyDepth)->GetRenderTargetItem().TargetableTexture, (*SeparateTranslucencyDepth)->GetRenderTargetItem().ShaderResourceTexture, SeparateResolveRect);

Engine/Source/Runtime/Renderer/Private/BasePassRendering.h
near by line 806

case BLEND_Translucent:
    // Note: alpha channel used by separate translucency, storing how much of the background should be added when doing the final composite
    // The Alpha channel is also used by non-separate translucency when rendering to scene captures, which store the final opacity
    DrawRenderState.SetBlendState(TStaticBlendState<
        CW_RGBA, BO_Add, BF_One, BF_One, BO_Add, BF_One, BF_One,
        CW_RGBA, BO_Add, BF_Zero, BF_InverseSourceColor>::GetRHI()); // <- replace
    //DrawRenderState.SetBlendState(TStaticBlendState<CW_RGBA, BO_Add, BF_SourceAlpha, BF_InverseSourceAlpha, BO_Add, BF_Zero, BF_InverseSourceAlpha>::GetRHI());
    break;

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessBokehDOFRecombine.h
near by line 17

// derives from TRenderingCompositePassBase<InputCount, OutputCount> 
// ePId_Input0: Full res scene color
// ePId_Input1: optional output from the BokehDOF (two blurred images, for in front and behind the focal plane)
// ePId_Input2: optional SeparateTranslucency
// ePId_Input3: optional SeparateTranslucencyAlpha // <- add
class FRCPassPostProcessBokehDOFRecombine : public TRenderingCompositePassBase<4, 1> // <- replace
//class FRCPassPostProcessBokehDOFRecombine : public TRenderingCompositePassBase<3, 1>

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessing.cpp
near by line 514

static void AddPostProcessDepthOfFieldBokeh(FPostprocessContext& Context, FRenderingCompositeOutputRef& SeparateTranslucency, FRenderingCompositeOutputRef& SeparateTranslucencyAlpha, FRenderingCompositeOutputRef& VelocityInput) // <- replace
//static void AddPostProcessDepthOfFieldBokeh(FPostprocessContext& Context, FRenderingCompositeOutputRef& SeparateTranslucency, FRenderingCompositeOutputRef& VelocityInput)

near by line 555

FRenderingCompositePass* NodeRecombined = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessBokehDOFRecombine(bIsComputePass));
NodeRecombined->SetInput(ePId_Input0, Context.FinalOutput);
NodeRecombined->SetInput(ePId_Input1, NodeBlurred);
NodeRecombined->SetInput(ePId_Input2, SeparateTranslucency);
NodeRecombined->SetInput(ePId_Input3, SeparateTranslucencyAlpha); // <- add

near by line 1406

// not always valid
FRenderingCompositeOutputRef SeparateTranslucency;
FRenderingCompositeOutputRef SeparateTranslucencyAlpha; // <- add
// optional
FRenderingCompositeOutputRef BloomOutputCombined;

near by line 1427

if (FSceneRenderTargets::Get(RHICmdList).SeparateTranslucencyRT)
{
    FRenderingCompositePass* NodeSeparateTranslucency = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessInput(FSceneRenderTargets::Get(RHICmdList).SeparateTranslucencyRT));
    SeparateTranslucency = FRenderingCompositeOutputRef(NodeSeparateTranslucency);
    FRenderingCompositePass* NodeSeparateTranslucencyAlpha = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessInput(FSceneRenderTargets::Get(RHICmdList).SeparateTranslucencyAlphaRT)); // <- add
    SeparateTranslucencyAlpha = FRenderingCompositeOutputRef(NodeSeparateTranslucencyAlpha); // <- add

near by line 1531

if(bBokehDOF)
{
    if (FPostProcessing::HasAlphaChannelSupport())
    {
        UE_LOG(LogRenderer, Log, TEXT("Boked depth of field does not have alpha channel support. Only Circle DOF has."));
    }
    if(VelocityInput.IsValid())
    {
        //AddPostProcessDepthOfFieldBokeh(Context, SeparateTranslucency, VelocityInput);
        AddPostProcessDepthOfFieldBokeh(Context, SeparateTranslucency, SeparateTranslucencyAlpha, VelocityInput); // <- replace
    }
    else
    {
        // todo: black/white default is a compositing graph feature, no need to hook up a node

        // black is how we clear the velocity buffer so this means no velocity
        FRenderingCompositePass* NoVelocity = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessInput(GSystemTextures.BlackDummy));
        FRenderingCompositeOutputRef NoVelocityRef(NoVelocity);
        //AddPostProcessDepthOfFieldBokeh(Context, SeparateTranslucency, NoVelocityRef);
        AddPostProcessDepthOfFieldBokeh(Context, SeparateTranslucency, SeparateTranslucencyAlpha, NoVelocityRef); // <- replace
    }
    bSepTransWasApplied = true;
}

if(SeparateTranslucency.IsValid() && !bSepTransWasApplied)
{
    checkf(!FPostProcessing::HasAlphaChannelSupport(), TEXT("Separate translucency was supposed to be disabled automatically."));
    const bool bIsComputePass = ShouldDoComputePostProcessing(Context.View);
    // separate translucency is done here or in AddPostProcessDepthOfFieldBokeh()
    FRenderingCompositePass* NodeRecombined = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessBokehDOFRecombine(bIsComputePass));
    NodeRecombined->SetInput(ePId_Input0, Context.FinalOutput);
    NodeRecombined->SetInput(ePId_Input2, SeparateTranslucency);
    NodeRecombined->SetInput(ePId_Input3, SeparateTranslucencyAlpha); // <- add

    Context.FinalOutput = FRenderingCompositeOutputRef(NodeRecombined);
}

Engine/Shaders/Private/BasePassPixelShader.usf
near by line 1073

#elif MATERIALBLENDING_TRANSLUCENT
    Out.MRT[0] = half4(Color * Fogging.a + Fogging.rgb, Opacity);
    Out.MRT[0] = RETURN_COLOR(Out.MRT[0]);
    // this block is weight function.
    {
        // Blend weight function.
        Out.MRT[1] = Out.MRT[0].aaaa;
        float screenZ = SvPositionToScreenPosition(MaterialParameters.SvPosition).z;
        float z = MaterialParameters.SvPosition.z / MaterialParameters.SvPosition.w;
        float a = Out.MRT[0].a * Out.MRT[0].a;
        float w = a * a * max(1e-2, min(3.0 * 1e3, 0.03 / (1e-5 + pow(MaterialParameters.SvPosition.w / 1000.0, 4.0))));
        //float w = a * a * max(1e-2, 3.0 * 1e4 * pow((1.0 - z), 3.0));
        Out.MRT[0] *= w;
    }
#elif MATERIALBLENDING_ADDITIVE
    Out.MRT[0] = half4(Color * Fogging.a * Opacity, 0.0f);
    Out.MRT[0] = RETURN_COLOR(Out.MRT[0]);
    Out.MRT[1] = half4(0, 0, 0, 0); // <- add
#elif MATERIALBLENDING_MODULATE
    // RETURN_COLOR not needed with modulative blending
    half3 FoggedColor = lerp(float3(1, 1, 1), Color, Fogging.aaa * Fogging.aaa);
    Out.MRT[0] = half4(FoggedColor, Opacity);
    Out.MRT[1] = half4(0, 0, 0, 0); // <- add

ここにウェイト関数があります。
調整はここで行うと良いでしょう。

near by line 1185

// the following needs to match to the code in FSceneRenderTargets::GetGBufferRenderTargets()
#define PIXELSHADEROUTPUT_BASEPASS 1
#define PIXELSHADEROUTPUT_MRT0 (!USES_GBUFFER || !SELECTIVE_BASEPASS_OUTPUTS || NEEDS_BASEPASS_VERTEX_FOGGING || USES_EMISSIVE_COLOR || ALLOW_STATIC_LIGHTING)
//#define PIXELSHADEROUTPUT_MRT1 (USES_GBUFFER && (!SELECTIVE_BASEPASS_OUTPUTS || !MATERIAL_SHADINGMODEL_UNLIT))
#define PIXELSHADEROUTPUT_MRT1 (USES_GBUFFER && (!SELECTIVE_BASEPASS_OUTPUTS || !MATERIAL_SHADINGMODEL_UNLIT)) || (MATERIALBLENDING_TRANSLUCENT || MATERIALBLENDING_ADDITIVE || MATERIALBLENDING_MODULATE) // <- replace

Engine/Shaders/Private/PostProcessBokehDOF.usf
near by line 455

#if RECOMBINE_METHOD == 2 || RECOMBINE_METHOD == 3
    // replace separate translucent composition.
#if 0
   float4 SeparateTranslucency = UpsampleSeparateTranslucency(SvPosition.xy, FullResUV, PostprocessInput2, PostprocessInput2Size.zw);

   // add RGB, darken by A (this allows to represent translucent and additive blending)
   OutColor.rgb = OutColor.rgb * SeparateTranslucency.a + SeparateTranslucency.rgb;
#else
    float4 WeightedColor;
    float SeparateAlpha;
    UpsampleSeparateTranslucency(WeightedColor, SeparateAlpha, SvPosition.xy, FullResUV, PostprocessInput2, PostprocessInput3, PostprocessInput2Size.zw);

    if (SeparateAlpha < 1.0f)
    {
        float3 averageColor = WeightedColor.rgb / max(WeightedColor.a, 1e-4);
        OutColor.rgb = averageColor * (1.0 - SeparateAlpha) + OutColor.rgb * SeparateAlpha;
    }
#endif
#endif

Engine/Shaders/Private/SeparateTranslucency.ush
near by line 455

// add functions for weighted blended OIT.
// don't replace BilinearUpsampling() and NearestDepthNeighborUpsampling() functions.
void BilinearUpsamplingForWeightedBlendedOIT(out float4 WeightedColor, out float Alpha, float2 UV, Texture2D LowResTex, Texture2D LowResAlphaTex)
{
    WeightedColor = Texture2DSampleLevel(LowResTex, BilinearClampedSampler, UV, 0);
    Alpha = Texture2DSampleLevel(LowResAlphaTex, BilinearClampedSampler, UV, 0).r;
}

void NearestDepthNeighborUpsamplingForWeightedBlendedOIT(out float4 WeightedColor, out float Alpha, float2 Position, float2 UV, Texture2D LowResTex, Texture2D LowResAlphaTex, float2 LowResTexelSize)
{
//@todo - support for all platforms, just skip the GatherRed optimization where not supported
#if (SM5_PROFILE && !(METAL_SM5_PROFILE || METAL_SM5_NOTESS_PROFILE || METAL_MRT_PROFILE || PS4_PROFILE))

    // The relative depth comparison breaks down at larger distances and incorrectly causes point sampling on the skybox pixels
    float MaxOperationDepth = 2000000.0f;

    // Note: this upsample is specialized for half res to full res
    float4 LowResDepthBuffer = LowResDepthTexture.GatherRed(BilinearClampedSampler, UV);
    float4 LowResDepth = min(float4(ConvertFromDeviceZ(LowResDepthBuffer.x), ConvertFromDeviceZ(LowResDepthBuffer.y), ConvertFromDeviceZ(LowResDepthBuffer.z), ConvertFromDeviceZ(LowResDepthBuffer.w)), MaxOperationDepth.xxxx);
    float FullResDepth = min(ConvertFromDeviceZ(SceneTexturesStruct.SceneDepthTexture[uint2(Position.xy)].x), MaxOperationDepth);

    float RelativeDepthThreshold = .1f;

    // Search for the UV of the low res neighbor whose depth is closest to the full res depth
    float MinDist = 1.e8f;

    float2 UV00 = UV - 0.5f * LowResTexelSize;
    float2 NearestUV = UV00;
    UpdateNearestSample(LowResDepth.w, UV00, FullResDepth, MinDist, NearestUV);

    float2 UV10 = float2(UV00.x + LowResTexelSize.x, UV00.y);
    UpdateNearestSample(LowResDepth.z, UV10, FullResDepth, MinDist, NearestUV);

    float2 UV01 = float2(UV00.x, UV00.y + LowResTexelSize.y);
    UpdateNearestSample(LowResDepth.x, UV01, FullResDepth, MinDist, NearestUV);

    float2 UV11 = float2(UV00.x + LowResTexelSize.x, UV00.y + LowResTexelSize.y);
    UpdateNearestSample(LowResDepth.y, UV11, FullResDepth, MinDist, NearestUV);
     
    float4 Output = 0.0f;
    float InvFullResDepth = 1.0f / FullResDepth;

    BRANCH
    if (abs(LowResDepth.w - FullResDepth) * InvFullResDepth < RelativeDepthThreshold 
        && abs(LowResDepth.z - FullResDepth) * InvFullResDepth < RelativeDepthThreshold 
        && abs(LowResDepth.x - FullResDepth) * InvFullResDepth < RelativeDepthThreshold
        && abs(LowResDepth.y - FullResDepth) * InvFullResDepth < RelativeDepthThreshold)
    {
        WeightedColor = Texture2DSampleLevel(LowResTex, BilinearClampedSampler, UV, 0);
        Alpha = Texture2DSampleLevel(LowResAlphaTex, BilinearClampedSampler, UV, 0).r;
    }
    else
    {
        WeightedColor = Texture2DSampleLevel(LowResTex, PointClampedSampler, UV, 0);
        Alpha = Texture2DSampleLevel(LowResAlphaTex, PointClampedSampler, UV, 0).r;
    }
    
#else

    WeightedColor = Texture2DSampleLevel(LowResTex, BilinearClampedSampler, UV, 0);
    Alpha = Texture2DSampleLevel(LowResAlphaTex, BilinearClampedSampler, UV, 0).r;

#endif
}

// replace function.
#if 0

float4 UpsampleSeparateTranslucency(float2 Position, float2 UV, Texture2D LowResTex, float2 LowResTexelSize)
{
#if NEAREST_DEPTH_NEIGHBOR_UPSAMPLE
   return NearestDepthNeighborUpsampling(Position, UV, LowResTex, LowResTexelSize);
#else
   return BilinearUpsampling(UV, LowResTex);
#endif
}

#else

// Weighted blended OIT version
void UpsampleSeparateTranslucency(out float4 WeightedColor, out float Alpha, float2 Position, float2 UV, Texture2D LowResTex, Texture2D LowResAlphaTex, float2 LowResTexelSize)
{
#if NEAREST_DEPTH_NEIGHBOR_UPSAMPLE
    NearestDepthNeighborUpsamplingForWeightedBlendedOIT(WeightedColor, Alpha, Position, UV, LowResTex, LowResAlphaTex, LowResTexelSize);
#else
    BilinearUpsamplingForWeightedBlendedOIT(WeightedColor, Alpha, UV, LowResTex, LowResAlphaTex);
#endif
}

#endif

以上です。
私の手元ではビルドが通っていますが、通らなかった人がいた場合は…まあ、頑張って修正してね!

ウェイト関数を変更したいだけなら1箇所変更すれば対応できるはずです。
また、より細かな調整をしたい場合も今回修正した部分に手を入れるだけでほとんどの場合で対応できるのではないかと思います。

ただ、通常のアルファブレンドとOITを同時に使用したい、という場合はかなり面倒な修正が必要になると思いますのでご注意ください。
また、Separate Translucency がOFFのマテリアルのブレンドもおかしくなってしまうので、この点にも注意ですね。

Flood Fillのエラーについて

Substance Designerの便利ノード [Flood Fill] はアイランドに分かれている形状にランダム性をもたせるのに重宝します。

f:id:monsho:20180906215410p:plain

このノードには3種の処理方法があり、[Safety/Speed trade-off] パラメータで変更が可能です。

  • Simple or small shapes:高速ですがエラーが発生しやすく、オススメできません
  • Complex or big shapes:デフォルトです。多くの場合で正常に処理できますが、失敗もあります
  • No failure mode:最も遅いですが、失敗はほぼありません

デフォルトの [Complex or big shapes] を選択し、[Flood Fill to Random Color/Grayscale] に接続した場合、多くの場合で成功するのですが、一部で失敗が発生します。

f:id:monsho:20180906220339p:plain

赤枠で囲った部分に横線が入っているのが見えるでしょう。
このようなエラーを回避するには [No failure mode] を選択するのが最も正しいやり方ですが、私の環境ですと、このモードを選択すると20msほど余分に処理がかかってしまいます。
大きな問題になる時間ではないとはいえ、軽く処理できるならそれに越したことはないわけで、そもそもこのようなエラーが何故発生するのか疑問に思いました。
そこで、ちょっと調べてみた結果を記事として公開することにした、という次第です。

さて、そもそも [Flood Fill] ノードから出力されているカラーの値は一体どのような値なのでしょうか?
これは、出力ピンにマウスドラッグすると判明します。

f:id:monsho:20180906220905p:plain

RGには各形状のUV値が格納されます。
このUV値は、各形状の軸並行境界ボックス(AABB)の左上を0、右下を1としたUV座標が入っています。
BAはこのAABBのサイズです。サイズ1.0はテクスチャ全体を示しています。

例えばRGの値を利用すれば各形状にテクスチャを貼り付けるようなことも可能です。

f:id:monsho:20180906221718p:plain

さて、[Flood Fill to Random Color/Grayscale] についてですが、これらはRGの値とBAの値を利用して、形状ごとに一意の値を生成、ここからランダムに色やグレースケールを求めるという手法を採っています。
とりあえず、[~ to Random Grayscale] の中身を見てみましょう。

f:id:monsho:20180906222054p:plain

[Pixel Processor] を2つ使っていますが、左下のノイズが表示されているものは [White Noise Fast] と近い処理を行っています。
重要なのはもう1つの方。こちらの中身を見てみましょう。

f:id:monsho:20180906223226p:plain

形状ごとに一意の値を求める、と前述しましたが、その部分は前半部分です。コメントで「upper-left corner」と書かれている部分までがその処理です。
あるピクセルの値を取得した際、そのピクセルが所属する形状のサイズ(BA)はその形状内では全て同一の値です。
違いがあるのはUV値となるRGの値ですが、RG * BAの計算で何が求められるかというと、そのAABBの左上からそのピクセルまでのXY軸の距離となります。
このピクセルの座標からこの値を引き算すると、求められるのはAABBの左上の座標、ということになります。
つまり、コメントどおりにこの段階で所属する形状のAABBの左上座標という、形状ごとに一意の値が求まることになるわけです。

ここで一意の値が求まっているわけですので、ノイズからサンプリングする座標をこのAABBの左上座標にしてみましょう。

f:id:monsho:20180906224837p:plain

左がオリジナルの [Flood Fill to Random Grayscale]、右がAABBの左上座標からノイズサンプリングをしたバージョンです。
サンプリングする座標が変わっているため、各形状のグレースケール値は変化していますが、縞模様は出ていません。
形状のサイズを様々に変更してみましたが、この手法で縞模様は出ませんでした。
つまり、後半の計算が縞模様を出す原因になっていると言えるわけです。
なお、[Flood Fill to Random Color] も同様の処理を行っていますが、やはり同じ改造で縞模様を消せます。

ここからはあくまでも私の想像ですが、デフォルトの [Flood Fill] 処理ではUVの値、もしくはAABBのサイズ、もしくは両方に微量の誤差が出ているのではないかと思います。
AABBの左上を求める計算でもその誤差が出ていて、同じ形状内だけど小数点以下の小さな値に微量の誤差が埋まっているのだろうと思われます。
そのままの座標をノイズサンプリングに利用した場合は誤差は切り捨てられて無視できるわけですが、65535 という大きな値をかけてしまったために誤差が顕在化しているのではないでしょうか?
[Flood Fill] ノードの中身を追ってみれば正確な原因もつかめるのかもしれませんが、軽く開いて絶望したので追いません。
なので、これらはあくまで予想ですが、そう外れてもいないんじゃないかと思っています。

というわけで、[Flood Fill] のエラー対策としては以下の手法のどれかを選ぶのがおすすめです。

  • No failure mode を使用する(簡単だけど処理時間大)
  • AABBの左上座標を用いてノイズサンプリングする(ノード化しておけばコストは安い)
  • to Random ~ の後に [Flood Fill to Color/Grayscale] を用いる(コスト安め)
  • [Flood Fill to Color/Grayscale] にノイズテクスチャを刺す(コスト安め)

3つ目と4つ目について簡単に解説します。

[Flood Fill to Color/Grayscale] は形状のAABBの中心座標の色を [Color/Grayscale Input] に刺したイメージからサンプリングします。
to Random ~ の後にこのノードを利用することで、AABB中心の色をサンプリングできます。
縞模様になっている形状の場合は縞のどの部分が選択されるか微妙ではありますが、縞模様になっていない形状についてはほぼ同じ色が取得できます。

f:id:monsho:20180906233814p:plain

ほぼ、と書いたのは、複雑な形状のためにAABB中心がその形状の外に行ってしまっているという稀有な状態の場合にうまくいかないという問題があるためです。

4つ目の手法を用いる場合の注意点としては、to Random ~は形状の境界部分が0、それ以外は0以上の値が入るように設定されている、という点です。
つまり、形状部分と境界部分が明確にされなければ、下手すると形状と境界が合体してしまうことがあるということです。
ノイズ生成時には注意しましょう。

Substance DesignerのPython Editorについて

新バージョンのSubstance DesignerにはPython Editorが搭載されているというのは前にも書きました。
これによっていろいろできるんじゃないか?と思って調べてみたことを今回は書きます。

まあ、ぶっちゃけてしまうと

今のところ使えねぇ!

です。

Python Editor

Substance DesignerのPython Editorは、メニューの[Window]→[Python Editor]から起動することが出来ます。
MayaのScriptウィンドウのような画面が出てくるでしょう。
Mayaと違うのは、上が編集画面、下がコンソール画面ということでしょうか。

ドキュメント

Python Editorの基本的な使い方、プラグインの作成方法はこちらにあります。

Python Editor - Substance Designer - Allegorithmic Documentation

また、APIのドキュメントはインストールフォルダ内にあります。
Python Editorウィンドウの[Help]→[Python API Documentation]をクリックすると開けます。

モジュール

Substance Designer特有のモジュールは sd です。
import sd でインポートできます。
あとはAPIドキュメントと先のSDのドキュメントを読めばとりあえずの機能実装はできるでしょう。

なお、Substance Automation Toolkitで使用できるPythonモジュールは pysbs です。
SDのモジュールとは別物ですね。
この時点で嫌な予感がしませんか?

プラグイン作成方法についてドキュメントを読む

最初はPython Editorの説明、操作方法等です。
How Toからプラグインの作成方法なので、そちらを見てみましょう。

Pythonプラグインの置き場所を設定したらメニューの [File]→[New Plugin]を選択してプラグイン作成ウィザードを立ち上げましょう。
プラグインの名前を決めて保存フォルダなんかも決定したらOKを押すと自動的に基本コードが生成されます。
しかもこの段階でプラグインとして登録されます。
メニューの[Scripts]→[作成したプラグイン名]で実行することが可能です。

APIドキュメントを読む

何ができるか知りたい場合、まずAPIドキュメントに目を通すのがいいでしょう。
作成したプラグインコードには run() 関数が存在し、ここに自前のコードを入力していくことになります。
ここの引数で aContext とありますが、これは sd.context 型です。

この型にはgetter以外の関数は存在しません。
ここで getSDApplication() 命令を実行し、sd.api.sdapplication を取得ます。
そして、getLocationContext()で sd.api.sdlocationcontext を取得すれば、ここから現在編集中のグラフや、選択しているノードの配列を取得することが出来ます。

さて、ここから何をやりましょうか?
というか、何ができるのでしょうか?
APIドキュメントに目を通してみましょう。

…getterがほとんど、設定したり作成したりはほぼ出来ない。
sd.tools.exportには指定したグラフの出力を行う命令がついていますが、出力するグラフと出力ディレクトリくらいしか指定できません。
他になにかないのか!と探しても特に見つからないでしょう。
まあ、つまり、そういうことなんです。

pysbsは使えない?

基本、使えません。
普通にpysbsをpipでインストールした場合、インストール先はSDとは別のPythonになるはずです。
SDにはインストールフォルダにPythonが存在するので、こちらで使えるように手動でインストールすればあるいは使えるかもしれません。

ただ、使えたとしても pysbs でグラフを作成→ファイルで保存→sdモジュールで読み込み、が関の山です。
残念ながら、sdモジュールのコンテキストやグラフに対して使用することは出来ません。

まとめ

今回のバージョンで追加された、ノードを整列する機能は、実はこのPythonプラグインとして作られていたりします。
他にもサンプルがいくつか存在してもいます。
が、現在はそれらのサンプルでできることくらいしか出来ないため、すぐになにか効率よくできる、というわけではないようです。
実はドキュメントのバージョンが古いだけでもっといろんな事ができる、という可能性を否定もしませんが。

もし、ドキュメント以上の機能が見つけた方がいましたら教えていただけるとありがたいです。

Substance Designer 2018 Summerの新ノード

というわけで、新しく追加されたノードも調べてみたので、適当に紹介してみます。

Gradient系

f:id:monsho:20180721102119p:plain

グレースケールのグラデーションを作成するノードが追加されてます。
以前はLinearだけでしたが、今回はポイントを指定してグラデーションを作成するノードになっています。
多分、Subsntace Shareでユーザが公開していたものを取り込んだのではないかと思います。

[Gradient Axial]と[Gradient Axial Reflected]は指定した2点を補間するようにグラデーションを作成します。
Reflected の方は中心で折り返す感じですね。

[Gradient Circular]は見ての通り角度に対してグラデーションがかかるようになっています。
円グラフ的なメーターのようなUIを作成するときに重宝するでしょう。
[Gradient Radial]は円状にグラデーションを作成できます。
この2つは中心ともう1点を指定することでグラデーションを変更することができます。

Cube 3D GBuffers

f:id:monsho:20180721102850p:plain

以前からあった[Cube 3D GBuffers]ノードはですが、UVの出力が追加されています。
使い道は今の所考えられていないのですが、使えばこんな事もできます。

f:id:monsho:20180721103508p:plain

だからなんだって感じですが…

Shape Extrude

f:id:monsho:20180721104439p:plain

名前の通り、形状の押し出しを行うノードです。
サンプルとして薬莢がばらまかれたようなマテリアルが示されていましたが、たしかにその手の使い方ができるノードですね。
これまでだと、そのような立体感のあるマテリアルを作成するのが少し難しかったので。
ただし、かなり重いので多用は厳禁ですかね。

Flood Fill

[Flood Fill]ノードの性能がアップ!待ってた!
これまでの[Flood Fill]は凹形に弱かったのですが、新バージョンでは凹形でも綺麗に処理してくれます。
以下のようなものを旧バージョンで試してもらえば一目瞭然です。

f:id:monsho:20180721110054p:plain

また、[Flood Fill]系のノードが2つ追加されて、1つに変更が入ってます。

f:id:monsho:20180721110955p:plain

[Flood Fill to Gradient]には角度とスロープのイメージ入力が追加されています。
[Tile Sampler]と同じように、入力したグレースケールイメージから角度とスロープのパラメータを取得して適用することができます。
[Flood Fill to Grayscale]と[Flood Fill to Color]も入力イメージからグレースケールとカラーを取得して適用するノードです。

Quad Transform、Trapezoid Transform

f:id:monsho:20180721111717p:plain

形状変形ノードが2種類追加されています。
[Quad Transform (Grayscale)]は四角ポリゴンの4点を自由に移動することで形状変形が可能です。
これまで以上に自由な形状生成が可能になったのでかなり便利なノードだと思います。
また、このノードはタイリングされないようです。

[Trapezoid Transform (Grayscale)]は台形変形です。上部と下部のサイズのみ指定可能です。
[Quad Transform]の簡易版に近い印象ですが、こちらはタイリングされます。

Shape Splatter

今回の目玉ノードがこちらになるのかなと思います。[Shape Splatter]です。
名前から[Splatter]や[Splatter Cirsular]の仲間と思われるかもしれませんが、どちらかというと[Tile Sampler]の亜種といった感じです。

f:id:monsho:20180721113317p:plain

入力ピンを見てもらえれば[Tile Sampler]に近いことがわかりますね。
なお、処理の重さも近いです。というか、こっちのほうが重いかも?

簡単な使い方としては地面に沿って何かを配置するなどです。
以下のようにするとわかりやすいでしょうか。

f:id:monsho:20180721113956p:plain

しかし、同じことは[Tile Sampler]と[Blend]の加算でも可能です。
では、こちらはどう違うのか?というと、いろんな機能が追加されています。
例えば、地面に沿って方向を変更したり、地面に平行に配置したりが可能です。

f:id:monsho:20180721114537p:plain

スロープの方向に回転していたり、平行に配置されたりしてるのがわかるでしょうか?

また、出力ピンの[Splatter Data 1/2]にはパターンのID、インデックス、UV、バウンディングボックスサイズなどが入っています。
これを利用することで後で情報を追加したりすることができます。
[Shape Splatter Data Extract]を使うとそれらの情報へのアクセスも容易になります。

f:id:monsho:20180721115140p:plain

使い道はいろいろありそうなので、面白い使い方を見つけた人は是非教えてください。

その他

[Auto Levels]に修正が入ったようですが、イマイチ差がわからず。

[Material Transform]で法線の回転がサポートされています。
使ったことがないのでわからないのですが、以前は法線方向が正常に回転しなかったっぽいですね。
これに関連して、[Normal Transform]ノードと[Normal Vector Rotation]ノードが追加されています。

f:id:monsho:20180721120036p:plain

[Normal Transform]は法線方向を形状ごと回転する、[Material Transform]との法線回転と同様のものになります。
[Normal Vector Rotation]は形状はそのままに、法線の方向だけを回転するようです。
使い道あるのでしょうか、これ?

[Shape]ノードにHemisphere、つまり半球が追加されています。
そういえばなかったな、この形状…

というわけで追加、修正ノードのまとめでした。
抜けがあるかもしれませんが、あったら教えてください。

Substance Designer 2018 Summerの新機能

唐突にバージョンアップがあったのですが、とりあえず調べてみた新機能等をサクッと紹介してみようと思います。

UIの変更

少し前のSubstance PainterのアップデートでUIが変わりましたが、Substance Designerも同様の変更が施されました。
正直な話、前の方がよか…げふんげふん。

f:id:monsho:20180721005811p:plain

特に変わったのはグラフエディタの上部のAtomicノードボタン。
以前はアイコンというよりノード名のイニシャルだったのが、アイコンに変化しました。
なれるまでに少し時間がかかりそうです。

f:id:monsho:20180721005851p:plain

ノードの整列

これまでなかったノードの整列機能が追加されました。
選択したノードを水平に並べるか、垂直に並べるかを選べます。
また、グリッドにスナップしていないノードを自動的に近いグリッドにスナップさせる機能も追加されています。

ボタン操作はグラフエディタの上部にあります。

f:id:monsho:20180721011016p:plain

左から水平に整列、垂直に整列、グリッドにスナップです。
ショートカットキーはそれぞれ H, V, S となっています。

整列機能は機械的に水平・垂直に整列するだけなので、過剰な期待は禁物です。
また、複数ノード選択→H→Vとやると、すべてのノードを1箇所にまとめることができます。
完全なクソノード配置なので止めましょう。

Input/Outputの色空間指定

Input/OutputノードのUsageに色空間指定項目が追加されています。

f:id:monsho:20180721012124p:plain

ここを変更することで出力される結果が変わるかというと、そんなこともなかったです。
指定することで良いことあるのかは今のところ不明ですね。

読み込みオンリーグラフのPixel Processor

これまでも読み込みオンリーのグラフは "Open Reference" メニューで見ることができました。
しかし、実際に見られるのは表側だけで、Pixel Processor の中身を見ることができませんでした。
今回のバージョンからは普通に内部まで覗けるようになりました。

まあ、これまでもノードをコピペして読み書き可能なグラフに持っていけば中身を見られたので、それに慣れてる人にはさほど嬉しくない機能かもしれません。

Pythonスクリプトが使えるようになった?

メニューに [Scripts] メニューが追加されています。

f:id:monsho:20180721013454p:plain

現在はサンプルがいくつか入っているだけのようですが、自分たちでも追加できるようになっているかもしれません。

追記

Pythonを使用してスクリプトを行う場合、自身のプロジェクトを設定することが可能です。

f:id:monsho:20180721095802p:plain

ここにディレクトリを設定すればOKっぽいですが、URLなのでウェブ上のものでも可能っぽいですね。
加えて、Pythonエディタが存在しています。

f:id:monsho:20180721095959p:plain

メニューの [Windows]→[Python Editor] で開けます。
開いたWindowはこんな感じ。

f:id:monsho:20180721100405p:plain

Mayaとかと同じような感じの基本的なエディタですね。
ちゃんとコンソールにエラー表示とかもされるので、最低限のエディットは可能じゃないでしょうか。

使い道としてはグラフを作成したりノードを追加したりが可能だと思うのですが、普通にテンプレートグラフ作っとけとかグラフ化しておけという感じで対応できるものではあるので、イマイチ使い道に困りそう。
なにかアイデアあったら教えてください。

次の記事では新規に追加されたノードの紹介をしていこうと思います。