找回密码
 立即注册
查看: 417|回复: 0

从RenderDoc看UE 5.1和Unity URP后处理区别

[复制链接]
发表于 2023-1-12 08:02 | 显示全部楼层 |阅读模式
如果说后处理之前的pass干的事情是为了让画面丰富,那么后处理就是为了让画面变得好看。
由于刚从unity转ue不久,虚幻的工作流不熟悉,美术也是从unity转的,对虚幻一套tonemap包括bloom都不适应,为了说服砍掉不合理的需求,适用ue的参数调节,势必要把ue的后处理看一圈。
为了说服为什么和unity不同,又势必要把unity的看一圈(阳康之后看代码都气短胸闷了)
事情的起因还是ue的bloom泛白,不过我整了个方法给拉低了(解决Unreal的Bloom泛光时颜色过白问题 - 知乎 (zhihu.com)),但是始终不是一个完善的解决方案(或者说正常的操作,正常就应该替换tonemap曲线)。为了搞清楚整个事情边摸边看顺便学些了一下两种引擎的后处理的方式。
本文只是一个对自己摸索到东西的记录,大概对整个管线流程能有个大致的了解,但是细节很多看不懂的,所以对细节部分我只能记录自己摸索到的或者从大佬们问到的东西了。
首先我们从Renderdoc来看
depthprepass,basepass,lightpass,各种环境光计算pass,各种雾,raymarching和透明pass,最后到了后处理pass



带后处理材质的后处理管线



不带后处理材质的后处理管线

这里我们清晰的看到了整个后处理的pass。这里给出了两张图片,原因是我发现如果你在后处理挂上后处理材质的话,管线流程有些许变化(TSR变FXAA了,不过具体应该和后处理材质设置的那个before tonemap有关系)



官方对后处理材质混合方式的解释

TAA会混合对像素进行颜色混合,所以TAA会造成GBuffer的数据在后处理材质的失效。(在TAA之前干不就好了么,为啥换成FXAA我还是不清楚)
总之虽然有些许不同,但是大体差不多,还是以TSR开启为例好了。
整个后处理大概分为几个部分:TSR,Bloom,ColorLuts,Tonemap(还有localexposure等没有细看,因为暂时我觉得本身对其他pass相对独立)
第一部分: TSR
时域超分辨率,听佬们说基本就是AMD的FSR,我自己没有看具体代码(看不懂),但是我暂且理解为TAA变种(这位佬有详细解释,主流抗锯齿方案详解(二)TAA - 知乎 (zhihu.com))。简单来说对不透明物体(透明物体需要单独开启)在某一空间的前后帧变化的delta写入VelocityBuffer,根据这个数值去和上一帧的图像就行某种规则的blend。貌似根据这篇文章所说,以前的TAA会在tonemap的结果上进行blend来达到更好的效果,但是目前通过renderdoc看到的并没有这一步,可能是新版本算法改进了罢。TAA的位置和影响是比较大的我认为,因为TAA后面的pass基本等于对GBuffer无缘了(边缘都模糊了,gbuffer的标记边缘也会出现误差了,bloom可能例外,因为本身就要模糊的)。



TSR之前



TSR之后

这里就涉及到一个问题,假如自己写个pass进去,就需要绑定Gbuffer的velocity写入,否则就会出现异常抖动。这也是做描边时遇到的问题。
而unity默认URP是MSAA,且为前向渲染。
第二部分:Bloom
对bloom了解的童鞋知道,bloom的做法是先筛选bloom的部分然后对这部分进行高斯模糊,不过具体的做法UE和unity略有不同



unreal5.1



unity 2021.3.13 urp

UE首先有个BloomSetup pass,这个pass和urp的bloom prefilter的作用一样,就是筛选出需要泛光的区域



筛选之前



筛选之后

他们的做法略有不同。
half TotalLuminance = Luminance(LinearColor) * ExposureScale;
half BloomLuminance = TotalLuminance - BloomThreshold;
half BloomAmount = saturate(BloomLuminance * 0.5f);
return float4(BloomAmount * LinearColor, 0) * View_PreExposure;如代码所示,unreal的做法是算出区域明度,然后再那阈值以上部分作为强度。Lumiance本质就是计算灰度那个dot(rgb,float3(0.3,0.59,0.11))
而unity再bloom.shader里的FragPrefilter可以看到
// Thresholding
            half brightness = Max3(color.r, color.g, color.b);
            half softness = clamp(brightness - Threshold + ThresholdKnee, 0.0, 2.0 * ThresholdKnee);
            softness = (softness * softness) / (4.0 * ThresholdKnee + 1e-4);
            half multiplier = max(brightness - Threshold, softness) / max(brightness, 1e-4);
            color *= multiplier;粗略来看差别就是这里用max来计算明度(感觉ue更合理),还有softness等参数影响,阈值以上的部分不是直接记录,而是计算比例再去和color相乘。
不管怎么样都是干了同一件事。
之后都是进行模糊了,然而模糊的算法unity和ue的做法差别就比较大了,甚至感觉我们从中可以看出二者开发思路的差别。
首先是UE:


模糊部分可以分为两部分,一部分是降采样,把图片降采样之后的结果拿去做第二部分,高斯模糊。
数一下就可以看到一共降采样了5次,包括原来那张图片一共6张图片,正好对应高斯模糊的12个pass,(一张图片需要沿着X轴Y轴各进行一次卷积)










图片分别对应着从低采样到高采样的高斯模糊结果,由于采样率低的时候,所以模糊的卷积核对于整张图片占比就大,所以低采样的模糊的范围相对较大。同时,这个结果应当进行了mipmap(sorry,这是猜测,这个具体还没看代码)



模糊之后



猜测mipmap之后

然后这些不同大小的模糊结果按权重blend到目标rt



bloom的结果

BloomIntensity会影响采样的卷积核,这部分在引擎的cpp代码里,会将offset参数和intensity相乘之后再传入ps里
void MainPS(
        noperspective float2 InUV : TEXCOORD0,
        noperspective float4 InOffsetUVs[ (( 30 + 1) / 2) ] : TEXCOORD1,
        out  float4  OutColor : SV_Target0)
{
        float4 Color = 0;
        [unroll]  for (int SampleIndex = 0; SampleIndex <  30  - 1; SampleIndex += 2)
        {
                float4 UVUV = InOffsetUVs[SampleIndex / 2];
                Color += SampleFilterTexture(UVUV.xy) * SampleWeights[SampleIndex + 0];
                Color += SampleFilterTexture(UVUV.zw) * SampleWeights[SampleIndex + 1];
        }
        [flatten]  if ( 30  & 1)
        {
                float2 UV = InOffsetUVs[ (( 30 + 1) / 2)  - 1].xy;
                Color += SampleFilterTexture(UV) * SampleWeights[ 30  - 1];
        }
        OutColor = Color;
}所以最终后处理可以调整的参数只有threshold和intensity,分辨对应影响着bloomsetup和gaussianblur这两个pass
然而unity的做法要更有意思一些


我们通过framedebugger能看到整个bloom有19个pass


第一个pass就是之前的那个bloom prefilter




从第二个pass开始到第十三个pass全部是horizontal和vertical的模糊pass,
再观察他们的rendertarget,发现每次进行一遍高斯模糊之后,会把结果降采样,拿这个结果在进行一边高斯模糊。
这样会发生什么呢














我们可以看到每次模糊就相当于把颜色沿着边缘一圈进行2像素扩张(假如卷积核是5x5),然后由于单位像素占图片比重越来越大所以最后6次的叠加得到一张颜色几乎充满整张rt的图片。
这样有啥好处呢,我们继续看
14-19的pass开始叫Bloom Upsample
每次Upsample pass输入有两张RT


分别对应那几张不同分辨率的模糊结果。
  half3 Upsample(float2 uv)
        {
            half3 highMip = DecodeHDR(SAMPLE_TEXTURE2D_X(_SourceTex, sampler_LinearClamp, uv));

        #if _BLOOM_HQ && !defined(SHADER_API_GLES)
            half3 lowMip = DecodeHDR(SampleTexture2DBicubic(TEXTURE2D_X_ARGS(_SourceTexLowMip, sampler_LinearClamp), uv, _SourceTexLowMip_TexelSize.zwxy, (1.0).xx, unity_StereoEyeIndex));
        #else
            half3 lowMip = DecodeHDR(SAMPLE_TEXTURE2D_X(_SourceTexLowMip, sampler_LinearClamp, uv));
        #endif

            return lerp(highMip, lowMip, Scatter);
        }在shader里我们发现,这两张图片的结果通过一个Scatter的参数进行lerp
所以从14-19这些pass里,这样从最小采样的rt开始lerp到最大采样的那张rt来得到最终的bloom结果
而scatter越大,结果就越接近采样小的那张rt,也就意味着泛光的光圈会更大。




这样unity里就能随意控制整个泛光的范围且不改变物体本身的色相。ue却办不到
我的感受就是unity还是自定义比较友好,unreal相对追求物理正确。
第三部分: CombineLuts


首先我们能看到这是一张32个slice,没个slice32x32,也就是说生成的是一张32x32x32的3d纹理
slice 1-32颜色会越来越偏蓝。
所以这其实是一张颜色映射table,我们读shader会发现,他这个颜色映射包含了tonemap,colorgrading,whitebalance等等各种效果的一张颜色映射,里面涉及到的最重要的就是tonemap函数
tonemap是ue自己的film aces 曲线
float FilmSlope;
float FilmToe;
float FilmShoulder;
float FilmBlackClip;
float FilmWhiteClip;

half3 FilmToneMap( half3 LinearColor )
.......里面的内容是一系列矩阵相乘,这一套下来会让人感觉ue的tonemap会让高数值的颜色产生相当大的色差。
所以做NRP就建议吧这个tonemap替换掉emmmm,扯远。
然后这张图的储存做了log的处理,原因是高范围HDR颜色数值远大于1,这样log之后映射到0-1的区间再进行采样就能做颜色映射了。


当然URP也有叫ColorGradingLUT。
第四部分: Tonemap


最后这个Tonemap pass其实是一个把效果应用到scenecolor的pass,因为前面做的所有东西都是准备工作。这个pass虽然叫Tonemap,实际是吧bloom和tonemap结果与scenecolor进行处理。
另外这里还处理了其他postprocess例如胶卷颗粒等效果,LUTS是32x32x32的,颜色必定有丢失,所以还有对颜色的correct。
<hr/>未完待续~

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-27 20:34 , Processed in 0.092081 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表