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のマテリアルのブレンドもおかしくなってしまうので、この点にも注意ですね。