Weighted Blended OITの実装

今回はWeighted Blended OITと呼ばれる技術をUE4のエンジンコードを改造して実装する話です。

エンジンコード改造となりますので文字多めです。

・OIT(Order Independent Transparency)とは?

OITとはOrder Independent Transparencyの略です。

順序に依存しない半透明、というような意味ですが、ほぼそのままの意味です。

知っての通り、半透明を描画する際には描画順序が重要になります。

大抵のシステムではメッシュ単位などでZソートを行い、画面の奥のメッシュから順番に描画していきます。

この手法はほとんどどんなシーンでも有効に働きますが、一部のシーンではうまくいきません。

わかりやすいのは半透明メッシュが入れ子構造になっている場合です。

半透明のグラスの中にやはり半透明のウィスキーが入っている、という場合、正しい描画順序はグラスの奥→ウィスキー→グラスの手前となります。

しかしメッシュ単位での描画となるとウィスキー→グラス、もしくはウィスキー入りグラス1回で描画、という事にもなります。

後者はどうしようもないとして、前者はグラスの奥側を正しく描画できないので、グラスの奥は描画しないなどの措置がとられるのではないかと思います。

ゲーム中のちょっとした小物くらいならそれでもいいかもしれません。

しかし、昨今のゲームは大変映像が綺麗になっていて、カットシーンなどでも高解像度です。

また、ゲームエンジンを利用した建築ビジュアライゼーションなども需要が出てきています。

このような状況で半透明の順番を正しく考えなければならない、そういうデザインを心がけなければならないというが難しくなってきているのは否定できません。

OITを実装しているゲームはほとんどないのが現状ですが、非常に高い優先度とはいえないまでもあったら欲しい機能と考えられているのではないでしょうか?

・Weighted Blended OITとは?

OITの実装としてはLinkListを利用したものが最も有名なのではないかと思います。

手前味噌ですが、私も以前実装したサンプルをアップしています。

DirectXの話 第110回

この手法は新生トゥームレイダーララ・クロフトの髪の毛の表現で使用されていましたが、やはり速度面で苦労していたようです。

正確に表現しようとするとこの手法は正確な半透明の順番を守ることが出来ます。また、トゥームレイダーでは順番を完全に正確に守らずに高速化を図っていたと記憶しています。

ある程度のスケーラビリティを持っていると言っても重いものは重い。

実装も比較的面倒ですし、このサンプルを作成した段階ではRadeonのドライバのバグで正常に描画されないこともありましたしね…

今回紹介するWeighted Blended OITは比較的軽く、実装も容易です。

Weighted Blended OITのペーパー

しかしかなり大胆に近似している手法であるため、正確な半透明表現はできませんし、使用の際には注意が必要です。

概要を簡単に説明してしまうと、半透明が重なった際に、その色が最終的な映像にどの程度影響を及ぼすかを重み付けして最後に正規化するという手法です。

と言ってもわかりにくいと思いますので、実例で解説します。

不透明メッシュ描画完了時、フレームバッファにはCsというカラーが描かれていたとします。

ここに(C1, A1)という半透明カラーが描画されたとしましょう。なお、Cはカラー、Aはアルファです。

それよりも手前に今度は(C2, A2)という半透明カラーが描画されます。

終結Cfは以下のような計算になります。

Cf = C2 * A2 + (C1 * A1 + Cs * (1-A1)) * (1 - A2) = C2 * A2 + C1 * A1 * (1 - A2) + Cs * (1 - A1) * (1 - A2)

アルファ値は0.0~1.0で、アルファ値同士の乗算(A1 * (1-A2)(1 - A1) * (1 - A2))は1.0を超えることはありません。

アルファ値に着目すると、最終結果により強い影響を与えるのはアルファの乗算結果が大きいカラーということになります。

つまり、A2が大きければC2の影響が大きくなり、A1が大きければC1の影響が大きくなります。

両方小さければCsの影響が大きくなります。

今度はA1 = 0.5A2 = 0.5とした場合の結果を計算してみましょう。

Cf = 0.5 * C2 + 0.25 * C1 + 0.25 * Cs

アルファ値が固定の場合、最後に描画されたC2の影響が大きくなります。

より一般的に考えると、深度が手前の半透明カラーの方が影響を与えやすいということです。

つまり、アルファ値が大きければ大きくなる、深度が近ければ大きくなる、という感じの関数 w(a, z) を作成し、これによって各半透明カラーの影響度を求める、というのがこの技術です。

弱点としてはあくまでも各カラー単体でのブレンドウェイトしか求められないため、前後関係などを正確に表現できるというわけではない点です。

そのため、画像処理ソフトで求められたブレンドの結果はこの技術で再現することはほぼ不可能です。

また、ウェイト関数によってはカメラの移動に対して結果が安定しません。

安定させようとすると今度は小さな距離の差がほとんど無視される結果になります。

入れ子構造の半透明ではまともな結果が得られないでしょう。

と、このように制約も大きな技術ではありますが、限定的な使用方法なら用途もあるんじゃないでしょうか?

…あるかなぁ?

それはともかく、続きからでソースコードの改変を行っていきます。

今回の改造はSeparate Translucentを利用します。

この技術の実装にはフレームバッファではなく別のバッファに描画する必要がありますが、Separate Translucentはこの実装にちょうどいいのです。

しかし通常のSeparate TranslucentはFP16のRGBAバッファが1枚使われています。

WB OITの実装にはもう1チャンネル必要なので、もう1枚のバッファを用意します。

Engine/Source/Runtime/Renderer/Private/PostProcess/SceneRenderTargets.h

690行目付近

TRefCountPtr<IPooledRenderTarget> SeparateTranslucencyRT;

TRefCountPtr<IPooledRenderTarget> SeparateTranslucencyDepthRT;

TRefCountPtr<IPooledRenderTarget> SeparateTranslucencyAlphaRT;

これでバッファを増やせますが、取得命令や生成を行う必要があります。

326行目付近

TRefCountPtr<IPooledRenderTarget>& 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.AutoWritable = false;

GRenderTargetPool.FindFreeElement(RHICmdList, Desc, SeparateTranslucencyAlphaRT, TEXT("SeparateTranslucencyAlpha"));

}

return SeparateTranslucencyAlphaRT;

}

増やしたバッファの生成と取得を行う関数です。

274行目

void FreeSeparateTranslucency()

{

SeparateTranslucencyRT.SafeRelease();

SeparateTranslucencyAlphaRT.SafeRelease();

check(!SeparateTranslucencyRT);

check(!SeparateTranslucencyAlphaRT);

}

解放処理です。

もう1点、FP16のRGBAバッファをクリアする際の値はデフォルトでは(0, 0, 0, 1)なのですが、これを(0, 0, 0, 0)にする必要があります。

310行目付近

if (!SeparateTranslucencyRT || SeparateTranslucencyRT->GetDesc().Extent != Size)

{

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));

//FPooledRenderTargetDesc Desc(FPooledRenderTargetDesc::Create2DDesc(Size, PF_FloatRGBA, FClearValueBinding::Black, TexCreate_None, Flags, false));

Desc.AutoWritable = false;

GRenderTargetPool.FindFreeElement(RHICmdList, Desc, SeparateTranslucencyRT, TEXT("SeparateTranslucency"));

}

.cppも同様に対応します。

Engine/Source/Runtime/Renderer/Private/PostProcess/SceneRenderTargets.cpp

1300行目付近

SCOPED_DRAW_EVENT(RHICmdList, BeginSeparateTranslucency);

TRefCountPtr<IPooledRenderTarget>* SeparateTranslucency;

TRefCountPtr<IPooledRenderTarget>* SeparateTranslucencyAlpha;

if (bSnapshot)

{

check(SeparateTranslucencyRT.GetReference());

check(SeparateTranslucencyAlphaRT.GetReference());

SeparateTranslucency = &SeparateTranslucencyRT;

SeparateTranslucencyAlpha = &SeparateTranslucencyAlphaRT;

}

else

{

SeparateTranslucency = &GetSeparateTranslucency(RHICmdList, ScaledSize);

SeparateTranslucencyAlpha = &GetSeparateTranslucencyAlpha(RHICmdList, ScaledSize);

}

const FTexture2DRHIRef &SeparateTranslucencyDepth = Scale < 1.0f ? (const FTexture2DRHIRef&)GetSeparateTranslucencyDepth(RHICmdList, GetBufferSizeXY())->GetRenderTargetItem().TargetableTexture : GetSceneDepthSurface();

{

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);

}

1358行目付近

if(IsSeparateTranslucencyActive(View))

{

SCOPED_DRAW_EVENT(RHICmdList, FinishSeparateTranslucency);

FTextureRHIParamRef RenderTargets[2] = { NULL };

SetRenderTargets(RHICmdList, 2, RenderTargets, NULL, 0, NULL);

TRefCountPtr<IPooledRenderTarget>* SeparateTranslucency;

TRefCountPtr<IPooledRenderTarget>* SeparateTranslucencyAlpha;

TRefCountPtr<IPooledRenderTarget>* SeparateTranslucencyDepth;

if (bSnapshot)

{

check(SeparateTranslucencyRT.GetReference());

SeparateTranslucency = &SeparateTranslucencyRT;

SeparateTranslucencyAlpha = &SeparateTranslucencyAlphaRT;

SeparateTranslucencyDepth = &SeparateTranslucencyDepthRT;

}

else

{

FIntPoint ScaledSize;

float Scale = 1.0f;

GetSeparateTranslucencyDimensions(ScaledSize, Scale);

SeparateTranslucency = &GetSeparateTranslucency(RHICmdList, ScaledSize);

SeparateTranslucencyAlpha = &GetSeparateTranslucencyAlpha(RHICmdList, ScaledSize);

SeparateTranslucencyDepth = &GetSeparateTranslucencyDepth(RHICmdList, GetBufferSizeXY());

}

RHICmdList.CopyToResolveTarget((*SeparateTranslucency)->GetRenderTargetItem().TargetableTexture, (*SeparateTranslucency)->GetRenderTargetItem().ShaderResourceTexture, true, FResolveParams());

RHICmdList.CopyToResolveTarget((*SeparateTranslucencyAlpha)->GetRenderTargetItem().TargetableTexture, (*SeparateTranslucencyAlpha)->GetRenderTargetItem().ShaderResourceTexture, true, FResolveParams());

RHICmdList.CopyToResolveTarget((*SeparateTranslucencyDepth)->GetRenderTargetItem().TargetableTexture, (*SeparateTranslucencyDepth)->GetRenderTargetItem().ShaderResourceTexture, true, FResolveParams());

}

Separate Tranclucentのバッファへの描画は今までのブレンド方法ではダメなのでこちらも変更します。

Engine/Source/Runtime/Renderer/Private/BasePassRendering.h

855行目付近

case BLEND_Translucent:

// Alpha channel is only needed for SeparateTranslucency, before this was preserving the alpha channel but we no longer store depth in the alpha channel so it's no problem

RHICmdList.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());

break;

問題点としては、この方法を用いるとNon Separateの半透明もこのブレンドになってしまいます。

本気で実装する場合はSeparateとNon Separateで別のブレンドになるようにするか、Non Separateで半透明を使用しないかすべきです。

Separate Translucentバッファに描画する処理をシェーダに書きます。

Engine/Shaders/BasePassPixelShader.usf

685行目付近

// Two options control the render target bindings : OUTPUT_GBUFFER_VELOCITY and ALLOW_STATIC_LIGHTING

#if USES_GBUFFER

...

#if WRITES_PRECSHADOWFACTOR_TO_GBUFFER

,out float4 OutGBufferE : GBUFFER_E_TARGET

#endif

#else

,out float OutAlpha : SV_Target1

#endif // USES_GBUFFER

1045行目付近

Color += Emissive;

#if MATERIALBLENDING_TRANSLUCENT

OutColor = half4(Color * VertexFog.a + VertexFog.rgb, Opacity);

OutColor = RETURN_COLOR(OutColor);

{

// Blend weight function.

OutAlpha = OutColor.a;

float screenZ = SvPositionToScreenPosition(SvPosition).z;

float a = OutColor.a * OutColor.a;

float w = a * a * max(1e-2, min(3.0 * 1e3, 0.03 / (1e-5 + pow(SvPosition.w / 1000.0, 4.0))));

OutColor *= w;

}

#elif MATERIALBLENDING_ADDITIVE

OutColor = half4(Color * VertexFog.a * Opacity, 0.0f);

OutColor = RETURN_COLOR(OutColor);

OutAlpha = 0.0;

#elif MATERIALBLENDING_MODULATE

// RETURN_COLOR not needed with modulative blending

half3 FoggedColor = lerp(float3(1, 1, 1), Color, VertexFog.aaa * VertexFog.aaa);

OutColor = half4(FoggedColor, Opacity);

OutAlpha = 0.0;

Blend weight functionとコメントが打たれている部分が問題のブレンドウェイト関数です。

今回は距離が1000くらいまでの間で効果が出るような関数を用いています。

この関数はゲームに応じて変更した方がいいと思います。

元論文にを読みながらそれぞれのゲームに最適な関数を利用しましょう。

ここまででSeparate Translucentバッファへの描画は完了ですが、これをフレームバッファにマージする段階でまた処理を変更する必要があります。

Separate TranslucentバッファのマージはBokehDOFの処理時に行われますのでこちらを改造します。

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessBokehDOFRecombine.h

11行目付近

// derives from TRenderingCompositePassBase 

// 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

class FRCPassPostProcessBokehDOFRecombine : public TRenderingCompositePassBase<4, 1>

使用する入力バッファを3から4に増やします。

Engine/Source/Runtime/Renderer/Private/PostProcess/PostProcessing.cpp

423行目付近

static void AddPostProcessDepthOfFieldBokeh(FPostprocessContext& Context, FRenderingCompositeOutputRef& SeparateTranslucency, FRenderingCompositeOutputRef& SeparateTranslucencyAlpha, FRenderingCompositeOutputRef& VelocityInput)

{

...

FRenderingCompositePass* NodeRecombined = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessBokehDOFRecombine());

NodeRecombined->SetInput(ePId_Input0, Context.FinalOutput);

NodeRecombined->SetInput(ePId_Input1, NodeBlurred);

NodeRecombined->SetInput(ePId_Input2, SeparateTranslucency);

NodeRecombined->SetInput(ePId_Input3, SeparateTranslucencyAlpha);

Context.FinalOutput = FRenderingCompositeOutputRef(NodeRecombined);

}

1192行目付近

// not always valid

FRenderingCompositeOutputRef SeparateTranslucency;

FRenderingCompositeOutputRef SeparateTranslucencyAlpha;

// optional

FRenderingCompositeOutputRef BloomOutputCombined;

1220行目付近

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));

SeparateTranslucencyAlpha = FRenderingCompositeOutputRef(NodeSeparateTranslucencyAlpha);

1330行目付近

if(bBokehDOF)

{

if(VelocityInput.IsValid())

{

AddPostProcessDepthOfFieldBokeh(Context, SeparateTranslucency, SeparateTranslucencyAlpha, VelocityInput);

}

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, SeparateTranslucencyAlpha, NoVelocityRef);

}

bSepTransWasApplied = true;

}

if(SeparateTranslucency.IsValid() && !bSepTransWasApplied)

{

// separate translucency is done here or in AddPostProcessDepthOfFieldBokeh()

FRenderingCompositePass* NodeRecombined = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessBokehDOFRecombine());

NodeRecombined->SetInput(ePId_Input0, Context.FinalOutput);

NodeRecombined->SetInput(ePId_Input2, SeparateTranslucency);

NodeRecombined->SetInput(ePId_Input3, SeparateTranslucencyAlpha);

Context.FinalOutput = FRenderingCompositeOutputRef(NodeRecombined);

}

大変長くなっていますが、あと少しです。

Engine/Shaders/SeparateTranslucency.usf

24行目付近

void UpsampleSeparateTranslucency(out float4 WeightedColor, out float Alpha, float2 Position, float2 UV)

{

...

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(PostprocessInput2, PostprocessInput2Sampler, UV, 0);

Alpha = Texture2DSampleLevel(PostprocessInput3, PostprocessInput3Sampler, UV, 0).r;

}

else

{

WeightedColor = Texture2DSampleLevel(PostprocessInput2, GetPointClampedSampler(), NearestUV, 0);

Alpha = Texture2DSampleLevel(PostprocessInput3, GetPointClampedSampler(), NearestUV, 0).r;

}

#else

WeightedColor = Texture2DSample(PostprocessInput2, PostprocessInput2Sampler, UV);

Alpha = Texture2DSample(PostprocessInput3, PostprocessInput3Sampler, UV);

#endif

}

Engine/Shaders/PostProcessBokehDOF.usf

440行目付近

#if RECOMBINE_METHOD == 2 || RECOMBINE_METHOD == 3

float4 WeightedColor;

float SeparateAlpha;

UpsampleSeparateTranslucency(WeightedColor, SeparateAlpha, SvPosition.xy, FullResUV);

if (SeparateAlpha < 1.0f)

{

float3 averageColor = WeightedColor.rgb / max(WeightedColor.a, 1e-4);

OutColor.rgb = averageColor * (1.0 - SeparateAlpha) + OutColor.rgb * SeparateAlpha;

}

#endif

以上です。多分。これで修正分全部・・・のはず。

結果はTwitterにアップしたものをそのまま掲載します。

実装結果を色々試してみたい方はエンジンの改造を行ってください。

うまくいかない場合はどこかのコードを掲載忘れていますので、コメントかTwitterで報告していただければと思います。