修改虚幻引擎后处理管线,自定义Bloom
泛光是常见后处理效果,这次的修改主要是两个目标1.UE原版后处理Bloom效果的输出集中在阈值颜色区域,无法在不改变物体本身光照颜色的情况下去随意泛出更强的光晕
2.UE原版的Bloom无论在PC还是手机(只看了UE5.1),都有一个Setup阈值pass,和若干不同分辨率的高斯blur(每个blur,xy各一个pass),新的Bloom替换效率更高,不含阈值和高斯blur的pass,且可以通过自定义控制台修改downsample等pass的数量来适配不同的平台。
由于项目本身是二次元游戏,相比能量集中在阈值以上,更希望到处都泛一点。所以新的方法直接干掉bloomsetup也是合理的。就算不干掉我也觉得可以把他合并到其他pass里。
<hr/>前置知识:
UE和Unity两个引擎对Bloom的实现方法:从RenderDoc看UE 5.1和Unity URP后处理区别 - 知乎 (zhihu.com)
用代理委托把渲染主题和shader代码分离到插件:虚幻UE通过插件修改引擎自定义PostProcess - 知乎 (zhihu.com)
这次的Bloom方法来源:Custom Bloom Post-Process in Unreal Engine | Froyok - Léna Piquet
<hr/>首先看UE的PPT
和我之前的文章一样,是downsample然后分别guassianblur然后权重加起来
我们看看其他的做法:
使命召唤高级战争里提供了一个自定义13sample的降采样的filter
同时还用了一个3x3的filter去升采样。同时每次升采样会对结果进行高斯模糊。
但是实际上,升采样的同时就会把高亮的颜色浸染到其他像素上,其实本质上升采样和高斯模糊做了同一件事情。
这次这位歪果仁提供的方法相当于各种方法的结合(本来bloom就这几种操作)用COD的降采样filter,然后再用他的升采样filter,同时每次升采样的结果是上一次升采样结果和这次结果的lerp(这个lerp的方法和unity的方法类似,提供了一个radius值去控制泛光的光圈大小)
注意:这个图里还提到了Glare,Ghosts+halo。皆为Lensflare镜头光晕的部分,由于镜头光晕共享了downsample的结果,所以如果我们依旧需要镜头光晕的效果,我们也需要对其进行适配。(但这个留在以后再说,事实上如果要求不高可以砍一些ue的lensflare功能,退化成一张片来优化效率)
我们先来看效果,新Bloom下:
Radius = 0.2的结果
Radius = 0.6的结果
Radius = 0.9的结果
UE5在新自定义Bloom不同Radius
https://www.zhihu.com/video/1617175263149531136
原本的Bloom之中,还有对不同阶段的自定义Hint。我们也可以对应加上偏色的功能。
同时Renderdoc里
需要注意一点,降采样应该是8次,但是由于Scenecolor提供的已经是1/2分辨率的结果,所以这里只有7次
这里已经完全没有原来的bloom和lensflare了。只有简简单单的downsample和upsample。
<hr/>实现要点:
由于我们用了代理委托的方法,我们需要在原本的Postprocess里找出逻辑替换
我们找到这行,把里面逻辑做ifelse,完全替换成我们的委托函数
除此之外由于我们完全抛弃了Setuppass,所以我们的结果是通过原本的linearcolor和linearcolorwithbloom来进行lerp
LinearColor += CombinedBloom.rgb * (1.0 + BloomDirtMaskColor);原本的加法逻辑替换成lerp
float3 BloomMask = saturate( CustomBloomScale + BloomDirtMaskColor );
LinearColor = lerp( LinearColor, CombinedBloom, BloomMask );这里我们需要cpp里传入CustomBloomScale的shader变量。这里我用了bloomIntensity,因为这样可以让其适配不同PostProcessVolume的切换逻辑
由于用控制台来调试新bloom的效果和质量(radius和passCount)需要新建控制台变量
TAutoConsoleVariable<int32> CVarBloomPassAmount(
TEXT(&#34;r.CustomPP.BloomPassAmount&#34;),
8,
TEXT(&#34; Number of passes to render bloom&#34;),
ECVF_RenderThreadSafe);
TAutoConsoleVariable<float> CVarBloomRadius(
TEXT(&#34;r.CustomPP.BloomRadius&#34;),
0.85,
TEXT(&#34; Size/Scale of the Bloom&#34;),
ECVF_RenderThreadSafe);并在Render函数中控制
int32 PassAmount = CVarBloomPassAmount.GetValueOnRenderThread();
需要注意,由于我们看的时候肯定希望editor也显示正常,所以需要处理editor的逻辑
需要一个rescale的shader,很简单。
void RescalePS(
in noperspective float4 UVAndScreenPos : TEXCOORD0,
out float4 OutColor : SV_Target0 )
{
float2 UV = UVAndScreenPos.xy * InputViewportSize;
OutColor.rgb = Texture2DSample( InputTexture, InputSampler, UV ).rgb;
OutColor.a = 0;
}
除此之外,这里用了VS和PS的方式去Downsample,每个Pass都是画正方形,所以可以共享一个单独的VS
#include &#34;Shared.ush&#34;
void CustomScreenPassVS(
in float4 InPosition : ATTRIBUTE0,
in float2 InTexCoord : ATTRIBUTE1,
out noperspective float4 OutUVAndScreenPos : TEXCOORD0,
out float4 OutPosition : SV_POSITION )
{
DrawRectangle(InPosition, InTexCoord, OutPosition, OutUVAndScreenPos);
}
Downsample
float2 InputSize;
float3 Downsample( Texture2D Texture, SamplerState Sampler, float2 UV, float2 PixelSize )
{
const float2 Coords = {
float2( -1.0f,1.0f ), float2(1.0f,1.0f ),
float2( -1.0f, -1.0f ), float2(1.0f, -1.0f ),
float2(-2.0f, 2.0f), float2( 0.0f, 2.0f), float2( 2.0f, 2.0f),
float2(-2.0f, 0.0f), float2( 0.0f, 0.0f), float2( 2.0f, 0.0f),
float2(-2.0f,-2.0f), float2( 0.0f,-2.0f), float2( 2.0f,-2.0f)
};
const float Weights = {
// 4 samples
// (1 / 4) * 0.5f = 0.125f
0.125f, 0.125f,
0.125f, 0.125f,
// 9 samples
// (1 / 9) * 0.5f
0.0555555f, 0.0555555f, 0.0555555f,
0.0555555f, 0.0555555f, 0.0555555f,
0.0555555f, 0.0555555f, 0.0555555f
};
float3 OutColor = float3( 0.0f, 0.0f ,0.0f );
UNROLL
for( int i = 0; i < 13; i++ )
{
float2 CurrentUV = UV + Coords * PixelSize;
OutColor += Weights * Texture2DSample(Texture, Sampler, CurrentUV ).rgb;
}
return OutColor;
}
void DownsamplePS(
in noperspective float4 UVAndScreenPos : TEXCOORD0,
out float3 OutColor : SV_Target0 )
{
float2 InPixelSize = (1.0f / InputSize) * 0.5;
float2 UV = UVAndScreenPos.xy;
OutColor.rgb = Downsample( InputTexture, InputSampler, UV, InPixelSize );
}Upsample
Texture2D PreviousTexture;
float Radius;
float3 Upsample( Texture2D Texture, SamplerState Sampler, float2 UV, float2 PixelSize )
{
const float2 Coords = {
float2( -1.0f,1.0f ), float2(0.0f,1.0f ), float2(1.0f,1.0f ),
float2( -1.0f,0.0f ), float2(0.0f,0.0f ), float2(1.0f,0.0f ),
float2( -1.0f, -1.0f ), float2(0.0f, -1.0f ), float2(1.0f, -1.0f )
};
const float Weights = {
0.0625f, 0.125f, 0.0625f,
0.125f,0.25f,0.125f,
0.0625f, 0.125f, 0.0625f
};
float3 Color = float3( 0.0f, 0.0f, 0.0f );
UNROLL
for( int i = 0; i < 9; i++ )
{
float2 CurrentUV = UV + Coords * PixelSize;
Color += Weights * Texture2DSampleLevel(Texture, Sampler, CurrentUV, 0).rgb;
}
return Color;
}
void UpsampleCombinePS(
in noperspective float4 UVAndScreenPos : TEXCOORD0,
out float3 OutColor : SV_Target0 )
{
float2 InPixelSize = 1.0f / InputSize;
float2 UV = UVAndScreenPos.xy;
float3 CurrentColor = Texture2DSampleLevel( InputTexture, InputSampler, UV, 0).rgb;
float3 PreviousColor = Upsample( PreviousTexture, InputSampler, UV, InPixelSize );
OutColor.rgb = lerp(CurrentColor, PreviousColor, Radius);
}<hr/>到这里就结束了,总之这次的实现其实对于算法意义不是很大。因为有大佬支持ue4 mobile以前就用了这个方法。我做这件事的意义可能最大的是锻炼自己TA在UE4里的工程实现能力吧。
这次由于用了代理和控制台变量的方式,把对UE引擎本身的修改减少到了最低。大大减少debug渲染代码的时间,每次修改之后只需要10s左右编译时间(5950x,64G,Incredibuild加持),不过还是有很多蛋疼的。(比如传参法线传到mobile了,又编引擎又编工程)
页:
[1]