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

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

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

Substance Painter用フィルタの作り方(応用編)

前回の実践編ではフィルタを適用したレイヤーの各イメージを法則に従って変換するという、比較的簡単なフィルタの実装を紹介しました。
この手のフィルタはSubstance Designer上でもそのまま使える便利なフィルタですが、今回紹介するのはSubstance Painterのペイント機能を主に利用したちょっと特殊なフィルタの作り方です。
作るものはこんな感じのものです。

f:id:monsho:20180616010721p:plain

ちょっと前にTwitterにも上げましたが、上のレイヤーのペイントが剥げて下のレイヤーが見える、というようなフィルタです。
剥げる部分はSPのペイント機能を使ってペイントして指定します。

このような処理はSPの機能でも実現できるか?というと、可能ではありますが少々面倒です。
下のレイヤーが見えるようにするにはマスクを使えばいいので、ペイントと各フィルタで対応できます。
しかし、剥げた部分の周囲のペイントがめくれ上がる、というのはそれほど簡単ではないと思います。
今回のフィルタはそのようなことが1つのフィルタと1つのレイヤーで実現することができます。

ただ、注意として、あまり出来は良くありません。
実用的なものにするにはもっと詰める必要はあると思いますが、そこはまあ、自分なりに応用してみてください。

今回やること

今回新しく使用している手法は以下のものとなります。

  • User定義マップを使用して他のマップに迷惑をかけないペイントを行う
  • ペイント部分を取り出して効果を適用する
  • 下のレイヤーが見えるように疑似マスクを利用する

実際に作成しながら1つ1つ見ていきましょう。

User定義マップ

User定義マップはSPで使用できるマップの一種ですが、特に用途が決まっていないのでユーザが適当に用途を決めていいマップです。
これらは何らかの情報として出力することもできますし、今回のようにフィルタの起点とすることもできます。
使い方を決めるのはユーザなので、SP上では特に何かが起こったりしないマップチャンネルです。

SPのテクスチャセット設定でチャンネルを追加できますが、ここでUserマップの追加が可能です。

f:id:monsho:20180616011410p:plain

User定義マップは複数種類が使用できますが、今回は "User0" を使用することにしましょう。

SDでフィルタを作成するときは、入力のIDに user0 を指定します。
今回はまず "Painter Filter (specific)" のテンプレートからグラフを作成し、[Input] ノードを1つ追加します。
Attributes の設定は以下のようにします。

f:id:monsho:20180616011829p:plain

Usage の項目には実は User0 という項目がドロップダウンリストには存在しませんが、直入力で設定してください。
SPで User0 チャンネルを追加してそこにペイントしたりするとこの [Input] ノードに情報が入ってくるという寸法です。

ペイント部分を取り出す

SPで User0 のチャンネルにペイントが施されていると想定して話を進めます。
前述の通り、ペイントした結果は SD の [Input] ノードに入ってきますので、これをタネとして処理を作っていきましょう。

しかし、ペイントが剥げたような処理を1から実装するのは面倒なので、以前Substance Shareにアップしたグラフを使用します。

share.allegorithmic.com

このグラフはペイントが剥げた際のペイント部の高さ、ノーマル、ペイントが剥がれている部分のマスクを出力してくれます。
これをダウンロードしたら、作成したフィルタにリンクしてください。
配置しましたら、まず以下のように組んでみましょう。

f:id:monsho:20180616021905p:plain

User0の [Input] ノードをリンクした [Paint Peeling Mask] ノードに接続します。
途中で [Histogram Scan] ノードを経由していますが、[Paint Peeling Mask] が0/1の2値画像を必要としているためです。
2値画像でなくても動作はしますが、結果を保証できないために採用しています。

[Paint Peeling Mask] の出力にあるHeightは元のHeightチャンネルに加算しておきましょう。
このノードにはペイントの基本高さを設定するパラメータがありますが、特に変更する必要は感じませんでした。

下のレイヤーが見えるようにする

SP で下のレイヤーを見えるようにする最も良い手段はマスクを利用することですが、レイヤーのカラーチャンネルに適用されたフィルタからマスクチャンネルを設定する手段はありません。

ではどうすればいいかというと、実はフィルタの出力にはアルファ値を含めることができます。
アルファ値が含まれている [Output] ノードの場合、そのチャンネルのブレンド時にアルファ値が考慮されます。
つまり、同じマスクをすべてのチャンネルに対してアルファ値として出力することで、マスクと同じ結果を生み出すことができるというわけです。

それを考慮した最終結果は以下のようになります。

f:id:monsho:20180616030646p:plain

[Paint Peeling Mask] ノードから出力されるマスクは、ペイントが剥がれている部分が1.0、ペイント部分が0.0で出力されるので、[Invert Grayscale] ノードで反転してからアルファ値として適用します。
Base Color はRGBを出力する必要があるので [Alpha Merge] ノードを利用していますが、Roughness、Metallic、Heightは1チャンネルなので [RGBA Merge] ノードを利用しています。

最後に [Paint Peeling Mask] のパラメータをExposeして終了です。
Exposeしたいパラメータは好きなものを選択しましょう。ただ、今回はノーマルを利用していないので Normal Intensity パラメータは不要でしょう。

ここで1つ注意点を。
SDで設定される入力パラメータは最大値と最小値を設定することができます。
Clamp の項目を True にしない限り、最大・最小値はスライドバーの変動可能範囲を示すだけで、数値の直接入力を行うと最大値を大きく超えて設定することができます。

しかし、SPではこの方法が不可能になっているようです。
SDで設定した最大・最小値の間でスライダーを動かすことができ、数値の直接入力でも最大・最小値の値に収めるためにClampされてしまいます。
現在はまだ回避方法はなさそうなので、十分広い範囲を指定しておく方がよいと思います。

SPでペイントする

SPではまずテクスチャセット設定ウィンドウでUser0のチャンネルを追加しましょう。
そして、ペイントマテリアルのレイヤーを上に、下地のレイヤーを下にして、ペイントマテリアルのレイヤーにフィルタを以下のように設定します。

f:id:monsho:20180616032124p:plain

"Add Paint" でペイントを追加してから "Add Filter" で Paint Peeling Filter を指定します。
あとはこの追加ペイントでUser0のみにペイントすればペイントが剥がれたような結果を確認できるでしょう。
なお、モノクロ画像をUser0に適用すればよかったりするので、ペイントではなく塗りつぶしを使ってもよかったりします。

さて、全3回でSubstance Painter用のフィルタの作成方法を紹介しました。
今回作ったものは実用しようと思うともう少し詰めないと厳しい部分はありますが、作り方はわかったのではないでしょうか。
あまり日本語情報が多くない分野だなと思っていたので、人によっては役に立つ情報なのかなと思います。