今回はWeighted Blended OITと呼ばれる技術をUE4のエンジンコードを改造して実装する話です。
エンジンコード改造となりますので文字多めです。
・OIT(Order Independent Transparency)とは?
OITとはOrder Independent Transparencyの略です。
順序に依存しない半透明、というような意味ですが、ほぼそのままの意味です。
知っての通り、半透明を描画する際には描画順序が重要になります。
大抵のシステムではメッシュ単位などでZソートを行い、画面の奥のメッシュから順番に描画していきます。
この手法はほとんどどんなシーンでも有効に働きますが、一部のシーンではうまくいきません。
わかりやすいのは半透明メッシュが入れ子構造になっている場合です。
半透明のグラスの中にやはり半透明のウィスキーが入っている、という場合、正しい描画順序はグラスの奥→ウィスキー→グラスの手前となります。
しかしメッシュ単位での描画となるとウィスキー→グラス、もしくはウィスキー入りグラス1回で描画、という事にもなります。
後者はどうしようもないとして、前者はグラスの奥側を正しく描画できないので、グラスの奥は描画しないなどの措置がとられるのではないかと思います。
ゲーム中のちょっとした小物くらいならそれでもいいかもしれません。
しかし、昨今のゲームは大変映像が綺麗になっていて、カットシーンなどでも高解像度です。
また、ゲームエンジンを利用した建築ビジュアライゼーションなども需要が出てきています。
このような状況で半透明の順番を正しく考えなければならない、そういうデザインを心がけなければならないというが難しくなってきているのは否定できません。
OITを実装しているゲームはほとんどないのが現状ですが、非常に高い優先度とはいえないまでもあったら欲しい機能と考えられているのではないでしょうか?
・Weighted Blended OITとは?
OITの実装としてはLinkListを利用したものが最も有名なのではないかと思います。
手前味噌ですが、私も以前実装したサンプルをアップしています。
この手法は新生トゥームレイダーでララ・クロフトの髪の毛の表現で使用されていましたが、やはり速度面で苦労していたようです。
正確に表現しようとするとこの手法は正確な半透明の順番を守ることが出来ます。また、トゥームレイダーでは順番を完全に正確に守らずに高速化を図っていたと記憶しています。
ある程度のスケーラビリティを持っていると言っても重いものは重い。
実装も比較的面倒ですし、このサンプルを作成した段階ではRadeonのドライバのバグで正常に描画されないこともありましたしね…
今回紹介する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.5、A2 = 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 validFRenderingCompositeOutputRef 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 == 3float4 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にアップしたものをそのまま掲載します。
ブレンドウェイト関数を修正、距離1000くらいまでを考えたものに変更した結果がこちら。 pic.twitter.com/gL3llbT6iB
— もんしょ (@monsho1977) 2016年6月9日
もう1つの難点はカメラ距離によって安定感が失われます。カメラから離れるとどんどん位置関係が怪しくなりますね。 pic.twitter.com/h4MnlVpoRF
— もんしょ (@monsho1977) 2016年6月9日
WB OITを使って同じパーティクルを表示した結果がこちら。パーティクルに対してはなかなか効果あるかも。 pic.twitter.com/UapwcD6pcx
— もんしょ (@monsho1977) 2016年6月11日
実装結果を色々試してみたい方はエンジンの改造を行ってください。
うまくいかない場合はどこかのコードを掲載忘れていますので、コメントかTwitterで報告していただければと思います。