Tile SamplerのVector Map Inputで正しい方向を向かない場合の対処法

第3回SubstanceDesignerゆるゆる会にて、Tile SamplerのVector Map Inputについての問題が提示されました。
ある形状のエッジ部分にエッジに垂直になるように形状を配置したいが、Tile SamplerのVector Map Inputだとたまにおかしな方向を向くというものでした。

Tile SamplerノードのVector Map Inputは、入力されたノーマルマップの勾配方向に向かって入力形状を回転する機能です。
これを利用すると、ある形状のワッペンのようなマテリアルを作成し、そのエッジ部分に縫い付けたような糸を想起されるような形状を作ることが出来ます。

では試してみましょう。

f:id:monsho:20190609010105p:plain

適当な形状を作り、これをワッペンの形状とします。
そのエッジを検出し、これをTile SamplerのMask Map Inputに入れます。
Tile Samplerの[Mask Map Threshold]を1.0にすれば与えられた形状の場所のみパターンが描画されます。

次にこのマスクにBlurを適用し、ノーマルマップを生成します。
これによりエッジに垂直な勾配が出来ますのでこれをVector Map Inputに接続します。
あとは[Vector Map Multiplier]のパラメータを1.0にすれば入力した細い線の形状がエッジに垂直に出てくるはずです。

その結果がこちら。

f:id:monsho:20190609011535p:plain

正しく出てる…ように見えて一部は正しく出ていません。
耳のあたりの一部の線がエッジに垂直ではなく、エッジに沿っているようになっているのが見受けられるかと思います。

このようになる理由はVector Map Inputに起因します。
Vector Map Inputの挙動は、入力したノーマルマップのRGの値から勾配の方向を調べ、アークタンジェントを使って回転角度を求めています。
この方法では正しく傾きが出ている部分はいいのですが、正しく出ていない部分、例えばノーマルの方向が上向きになっている部分では正しく勾配方向が求められないことになります。
そのような場合は向きがおかしくなってしまうわけで、これは計算上仕方がないです。

ではどうすればいいかというと、Mask Mapとして入力したものからノーマルを計算する必要は別にないわけです。
つまりこうすればいい。

f:id:monsho:20190609082200p:plain

Edge Detectする前のノードにEdge Detectをブレンドしてまずは膨らませます。
これでTile Samplerで配置される位置は全てカバーできます。

次にBevelで内側に破綻しない程度に大きく傾きを付けていきます。
これでエッジ部分には外向きの傾斜が出来ますので勾配方向はすべての場所においてエッジの外側に向きます。
これでほぼ完全に正しい方向に対応できるようになるはずです!

f:id:monsho:20190609082653p:plain

エッジに沿っていたものがなくなり、綺麗に並んでいるのがわかるでしょう。

この問題の対処方法は他にもいくつかノードの組み合わせがあるのですが、概ね”正しい方向を正しく向かせられるノーマルを作成する”という単純かつ正しい方法でほぼ全て解決できます。
逆に言えば、うまく行ってないのはノーマルが勾配方向を示すものとしては正しくなっていないからと言うことになります。
ただしパッと見ではその正しくなさがわかりにくいと思いますので、出来るだけ”正しくならざるを得ない”ようなノーマルをVector Map Inputに提供するようにすることが大事でしょう。

ハハッ

f:id:monsho:20190609085242p:plain

UE4のPythonを使ったFBXインポート

まだベータ版ではありますが、UE4Pythonが使用できるようになっています。
主な使いみちはEditorでの作業の簡略化でしょう。
多くのDCCツールがPythonを採用しているため、作業効率化を図りたい人には馴染み深い言語でもあります。

UE4ではUIを含めた拡張機能プラグインで作成することになっていましたが、Pythonの導入で簡単な作業であればプラグインを作らなくても実行できるようになりました。
簡単な作業、ちょっとしたことというのは開発中はとても多く、これらを効率化出来るかどうかはイテレーションの回数に影響します。
その点でPythonは覚えておいて損がない言語だと思いますので、非プログラマの方もぜひ触ってみてください。

さて、今回はそんなちょっとした機能としてFBXファイルのインポートをやってみます。
FBXのインポートは1つのファイルだけなら普通にエディタを使えばいいのですが、大量のFBXを一度にインポートしたい場合などはエディタでやるのは大変です。
また、インポート後にマテリアルを変更したり、テクスチャも一緒にインポートしたり、インポートしたテクスチャをマテリアルインスタンスに割り当てたりと、作業は地味ですが大量にあります。
多くの場合、これらの作業は決められたルールで行われているので自動化もしやすいはず。
しかし、プラグインを作るのは少々面倒で、もっと簡単に処理したいと思ってしまうでしょう。

そこでPythonの登場です!
今回のサンプルはPythonを使ってFBXをインポート、それぞれのマテリアルにマスターマテリアルを継承したマテリアルインスタンスを設定するというものです。
テクスチャのインポートや割当は行っていませんが、これも対応は難しくないと思います。ただ、今回はマテリアルインスタンスの設定までです。

最初にPythonを使える状態にします。
プロジェクトを開いたら[編集]メニューの[プラグイン]を選択し、プラグインウィンドウを表示します。
そして以下の2つのプラグインを有効にしましょう。

f:id:monsho:20190518121215p:plain

Pythonは言わずもがなですが、[Editor Scripting Utilities]も今回は使用します。
2つのプラグインをONにしたらエディタを再起動します。

再起動後はアウトプットログウィンドウのコマンド入力部分に以下のコマンドで.pyファイルを実行できます。

py ファイル名
例)
 py "D:\test.py"

とても簡単です。当然ですが、.pyファイルは自前で用意しましょう。
今回はサンプルとして以下のようなスクリプトを書いてみました。

import unreal

# FBXインポート時の設定
op = unreal.FbxImportUI()
op.import_materials = True # マテリアルもインポート
op.static_mesh_import_data.combine_meshes = True # メッシュを1つにまとめる

# FBXインポートのタスクを生成
task = unreal.AssetImportTask()
task.automated = True
task.destination_name = 'TestMesh' # UE4上のアセット名
task.destination_path = '/Game/Test/' # アセットを保存するフォルダ
task.filename = "D:/test.fbx" # 読み込みたいFBXファイル名を指定する
task.options = op

tasks = [task]

# タスクを実行
# FBXとマテリアルがインポートされる
atool = unreal.AssetToolsHelpers.get_asset_tools()
atool.import_asset_tasks(tasks)

# インポートされたマテリアルを削除して、同名のマテリアルインスタンスを作成
# このインスタンスをインポートしたメッシュに割り当てる
mesh_data = unreal.EditorAssetLibrary.find_asset_data(task.destination_path + task.destination_name)
master_mat = unreal.EditorAssetLibrary.find_asset_data('/Game/Test/M_Master_base')
mesh = mesh_data.get_asset()
for i in range(mesh.get_num_sections(0)):
    mat = mesh.get_material(i)
    mat_path = unreal.EditorAssetLibrary.get_path_name_for_loaded_asset(mat)
    mat_name = mat_path[mat_path.rfind('/') + 1:mat_path.rfind('.')]
    mat_path = mat_path[:mat_path.rfind('/') + 1]
    unreal.EditorAssetLibrary.delete_loaded_asset(mat)
    mat_inst = atool.create_asset(mat_name, mat_path, unreal.MaterialInstanceConstant, None)
    unreal.MaterialEditingLibrary.set_material_instance_parent(mat_inst, master_mat.get_asset())
    mesh.set_material(i, mat_inst)

やっていることは簡単です。少しずつ見ていきましょう。

import unreal

UE4上のPythonの機能を使う場合はこのモジュールをインポートします。

op = unreal.FbxImportUI()

メッシュインポート時の設定情報を作成します。これは unreal.FbxImportUI クラスで作成し、必要なデータを設定しましょう。

task = unreal.AssetImportTask()
# ry
tasks = [task]

アセットのインポートを行う際には unreal.AssetImportTask クラスを作成します。
インポートするファイル1つにつき1つのオブジェクを使います。

atool = unreal.AssetToolsHelpers.get_asset_tools()
atool.import_asset_tasks(tasks)

アセットのインポートは unreal.AssetTools を利用しますが、このオブジェクトは unreal.AssetToolsHelpers.get_asset_tools() 関数を用います。
リストの先頭から順番に処理されるので複数のタスクを用意しても構いませんが、今回は1つだけです。

mesh_data = unreal.EditorAssetLibrary.find_asset_data(task.destination_path + task.destination_name)

unreal.EditorAssetLibrary を使用する場合に Editor Scripting Utilities プラグインが必要になります。
find_asset_data() 関数は指定パスのアセットを検索して1つだけ返します。
返り値の AssetData オブジェクトから get_asset() 関数を使うと、実際のアセットへのアクセサーを取得できます。
その後の forループで回すのはメッシュのセクション数、すなわちマテリアル数となります。

    mat = mesh.get_material(i)
    mat_path = unreal.EditorAssetLibrary.get_path_name_for_loaded_asset(mat)
    mat_name = mat_path[mat_path.rfind('/') + 1:mat_path.rfind('.')]
    mat_path = mat_path[:mat_path.rfind('/') + 1]
    unreal.EditorAssetLibrary.delete_loaded_asset(mat)
    mat_inst = atool.create_asset(mat_name, mat_path, unreal.MaterialInstanceConstant, None)
    unreal.MaterialEditingLibrary.set_material_instance_parent(mat_inst, master_mat.get_asset())
    mesh.set_material(i, mat_inst)

for文の中身です。
まず、マテリアルをインポートしているので、FBX内のマテリアル名と同一名のマテリアルアセットが作成されています。
マテリアルスロットに割り当てられているマテリアルからアセットパスを取得、これをパスとアセット名に分けています。

そしておもむろにマテリアルを削除します。同名のマテリアルインスタンスを作成するためです。
マテリアルインスタンスは create_asset() 関数で MaterialInstanceConstant を作成します。
このクラスのオブジェクトがコンテンツブラウザ上でのマテリアルインスタンスです。

このマテリアルインスタンスの親マテリアルとして取得済みのマスターマテリアルを設定します。
マテリアル名に何らかの識別子を用意し、名前に応じて使用するマスターマテリアルを変更させる、なんていうのも作業効率化になりそうですね。
親マテリアルを設定したらメッシュの対象番号のマテリアルとして設定します。

これだけ。 ちなみに、この状態だとアセットの保存ができてませんが、これも EditorAssetLibrary に存在しますので、メッシュとマテリアルインスタンスを保存すると良いでしょう。
ソースコード中のFBXファイル名を正しいファイル名に変更すれば動作するはずです。(UE4.22.1)

UE4Pythonは2.7だそうです。3系ではないので記述方法やモジュールに注意が必要です。
しかもUE4自身がPythonの実行ファイルを持っているので、必要なモジュールがない場合のインストールには注意が必要です。

とまあ、こんな感じです。
あとは自前のルールに合わせてコードを書き換えれば一括インポートは簡単になるのではないかと思います。

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プラグインとして作られていたりします。
他にもサンプルがいくつか存在してもいます。
が、現在はそれらのサンプルでできることくらいしか出来ないため、すぐになにか効率よくできる、というわけではないようです。
実はドキュメントのバージョンが古いだけでもっといろんな事ができる、という可能性を否定もしませんが。

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