Weighted Blended OIT - UE 4.20.3対応
以前、UE4で簡単ではありますが Weighted Blended OIT を実装してみました。
monsho.hatenablog.com
この技術の詳細については上の記事と、そこにリンクしているペーパーをお読みください。
あまり使われていない技術だとは思うのですが、『Saints Row』シリーズでおなじみのVolitionの作品『Agents of Mayhem』で使用されていることがGDC 2018で発表されています。
こちらはウェイト関数や様々な問題に対して調整を行っていますので、もしこの技術を実装したいのであれば上のリンクを参考にしてみてください。
さて、今回は要望がありましたので、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のマテリアルのブレンドもおかしくなってしまうので、この点にも注意ですね。