UE5.32抖动半透明兼容TAA时序超分算法改进

常规的抖动半透明使用噪声(Pattern)抖动Alpha Mask,通过镂空的方式,搭配TAA模拟半透明。

经常会出现一些Artifact:

  1. 空洞。

Image

  1. Pattern的Clip破坏了时序抖动序列的空域连续性,速度、深度和颜色都出现了断层,导致后续TAA经常无法识别出边界区域,极易产生拖尾和鬼影。

Image

  1. 镂空的区域通过快速抖动方式模拟填充,极易出现能量损失,让画面看起来黯淡不少,同时也能看到廉价的抖动模式。

Image

(其实不仅仅是TAA,包括DOF、运动模糊等依赖速度和深度的效果基本都会出错。)

改进的抖动半透明通过缓存历史帧并重建的做法,能完美避免这些问题:

Image

下面是一组对比图:

Image

渲染需要抖动半透的角色时,按照2x2的Pattern做时序抖动(使用2x2的Pattern可以帮助我们在两帧之间就能拿到全部的场景信息)。

float computeOpacity(float2 PixelPos)
{
    const int2 PosMod = int2(PixelPos) % 2;
    const bool bSpecialPos = (PosMod.x + PosMod.y == 1);
    return ((View.FrameNumber & 1u) == 0u) ? bSpecialPos : (!bSpecialPos);
}

然后将上一帧的渲染颜色缓存下来。此时我们有如下两张图:

Image

从中可以提取出前景色和背景色,然后我们将抖动区域的Alpha写到GBuffer或者GPUScene中,在Shader中读取后,就可以模拟出Alpha半透明混合了:

Image

如何判断是chessboard半透明区域?

读取上一帧相同位置的Alpha值,然后与当前帧的Alpha值比较,若存在一帧有Alpha那么就是Chessboard半透区域。

注:通常情况下抖动半透明是逐Primitive的Alpha改变,可以存储当前帧的Alpha和上一帧的Alpha到GPUScene中,再通过GBuffer里的PrimitiveID拿到两帧互补的Alpha做判断。

Texture2D CurrentSceneColorTexture;
Texture2D PrevSceneColorTexture;

shader
{
    // ...
    const bool bCurrentPixelNoCull = IsJitter(CurrentAlpha);
    const bool bPrevPixelNoCull    = IsJitter(PrevAlpha);

    // No pixel jittering so skip this area.
    if((!bCurrentPixelNoCull) && (!bPrevPixelNoCull))
    {
        out = CurrentColor;
        return;
    }
 
    // Load prev color.
    const float4 PrevColor = PrevSceneColorTexture.load();

    // Composition scene color.
    if (bCurrentPixelNoCull)
    {
        const float Alpha = RemapAlpha(CurrentAlpha);
        out = lerp(PrevColor, CurrentColor, Alpha);
    }
    else
    {
        const float Alpha = RemapAlpha(PrevAlpha);
        out = lerp(CurrentColor, PrevColor, Alpha);
    }
}

在填充完颜色后,也不要忘记对深度和速度做孔洞填充防止TAA鬼影(此处我们不使用上一帧的速度和速度来填充空洞,而是直接拿当前帧的速度和速度图,扩张到隔壁空洞去)。

由于Pattern固定,可以知道应该采样像素的位置:

float2 ChessboardOffsetPos(float2 PixelPos)
{
    const int2 PosMod = int2(PixelPos) % 2;
    
    // (0, 0) -> (1, 0) = ( 1, 0) 0.
    // (1, 0) -> (0, 0) = (-1, 0) 1.
    // (0, 1) -> (1, 1) = ( 1, 0) 2.
    // (1, 1) -> (0, 1) = (-1, 0) 3.

    static const float2 kOffset2x2[4] = 
    {
        float2( 1, 0),
        float2(-1, 0),
        float2( 1, 0),
        float2(-1, 0),
    };

    return kOffset2x2[PosMod.x + PosMod.y * 2];
}

速度和深度的孔洞填充如下:

Image

虽然有些拉伸,但提供给TAA或者其他时域超分算法,用来防止拖尾,已经是足够了。

此时,由于重建了完整的速度和速度,颜色也支持混合在了一起,角色抖动的行为类似于绘制了速度的半透明物体,能正常在超分、DOF和运动模糊中渲染。

PS: 这是一个Dead Tech。在发现了抖动半透明的问题后,我在一个空闲的业余时间,在太阳落山的傍晚完成了开发,但是因为种种原因,现在(并且未来也)没有任何人,任何地方会有使用上它,XD。

© - 2024 · 月光下的旅行。 禁止转载