为什么要重建法线图

在后处理中会丢失场景中的顶点信息和法线信息,但在描边或做一些屏幕效果时,都需要用到法线信息

如何重建法线图

主要思路是使用叉乘,因为法线是垂直于面的向量,所以需要在求得某个面的两个向量。但仅仅是这种方法,会存在瑕疵,本篇将介绍如何对其进行优化

实现

方法一

第一种方法即是最简单的叉乘,只需采样三个点

float GetDeviceDepth(float2 uv)
{
    return _CameraDepthTexture.SampleLevel(sampler_LinearClamp, uv, 0).r;
}

// 需要注意UNITY_MATRIX_I_P看向z轴负方向,重建得到的值应大部分为蓝色
float3 ReBuildPosVS(float2 positionVP, float depth)
{
    float3 positionNDC = float3(positionVP * 2.f - 1.f, depth);
    #if defined (UNITY_UV_STARTS_AT_TOP)
    positionNDC.y = - positionNDC.y;
    #endif

    float4 positionWS = mul(UNITY_MATRIX_I_P, float4(positionNDC, 1.f));
    positionWS.xyz /= positionWS.w;

    return positionWS;
}

float3 ReBuildNormalVS_Low(float2 uv)
{
    float depth = GetDeviceDepth(uv);
    float3 positionVS = ReBuildPosVS(uv, depth);

    return SafeNormalize(cross(ddx(positionVS), ddy(positionVS)));
}

不难得到下图:

方法二

不难看出,方法一得到的法线图,在边缘有很多瑕疵且存在artifacts ,这是因为在边缘计算时,可能采样的点的深度差较大,得到的效果不够理想

为了避免这一问题,需要选取深度差更小的点

float3 ReBuildNormalVS_Medium(float2 uv)
{
    float3 posVS_C = ReBuildPosVS(positionVP);
    float3 posVS_T = ReBuildPosVS(positionVP + float2(0, 1 ));
    float3 posVS_B = ReBuildPosVS(positionVP + float2(0, -1));
    float3 posVS_R = ReBuildPosVS(positionVP + float2(1, 0 ));
    float3 posVS_L = ReBuildPosVS(positionVP + float2(-1, 0));

    float3 depthDiffL = posVS_C - posVS_L;
    float3 depthDiffR = posVS_R - posVS_C;
    float3 depthDiffT = posVS_T - posVS_C;
    float3 depthDiffB = posVS_C - posVS_B;

    float3 horizionVec = abs(depthDiffL.z) < abs(depthDiffR.z) ? depthDiffL : depthDiffR;
    float3 verticalVec = abs(depthDiffB.z) < abs(depthDiffT.z) ? depthDiffB : depthDiffT;

    return SafeNormalize(cross(horizionVec, verticalVec));
}

可以得到如下结果:

方法三

方法二得到的效果挺不错的,对于边缘的处理是很不错的,但在某些地方仍然存在artifacts ,接下来介绍一种方法可以得到完美的法线,但消耗也随之增高

首先来解释一下为什么方法二会有部分地方仍存在artifacts ,如下图所示:

左图中,当采样的中心点为c点时,不会出现artifacts,这是因为|d - c|<|c - b|,c点是很有可能在向量ed上,而非ab

但右图中,就出现问题了,虽然|d - c| > |c - b|,但c点是在向量ed上,实际上使用方法二计算会选取bc向量,这就导致artifacts的出现

要想得到绝对正确的法线,就需要额外判断c点在向量ab上还是ed上。具体做法如下图所示:

  1. 延长线段ab到点c,得到点c_1
  2. 延迟线段ed到点c,得到点c_2
  3. 如果|c_1 - c|<|c_2 - c|,点c在向量ab上,否则在向量de上

另外需要注意的是,经过透视变换后,深度是非线性的,对于屏幕上等距分布的三个点ABC,当他们在世界空间中处于同一条直线时,深度值大小的关系是2 Depth_B = Depth_A + Depth_C

float3 ReBuildNormalVS_High(float2 uv)
{
    float3 posVS_C = ReBuildPosVS(positionVP);
    float3 posVS_T = ReBuildPosVS(positionVP + float2(0, 1 ));
    float3 posVS_B = ReBuildPosVS(positionVP + float2(0, -1));
    float3 posVS_R = ReBuildPosVS(positionVP + float2(1, 0 ));
    float3 posVS_L = ReBuildPosVS(positionVP + float2(-1, 0));

    float3 depthDiff_L = posVS_C - posVS_L;
    float3 depthDiff_R = posVS_R - posVS_C;
    float3 depthDiff_T = posVS_T - posVS_C;
    float3 depthDiff_B = posVS_C - posVS_B;

    float centerDepth = GetDeviceDepth(positionVP * _ViewSize.zw);
    float4 horizionDepth = float4(
        GetDeviceDepth(positionVP + float2(-1.f, 0.f) * _ViewSize.zw),
        GetDeviceDepth(positionVP + float2(1.f, 0.f) * _ViewSize.zw),
        GetDeviceDepth(positionVP + float2(-2.f, 0.f) * _ViewSize.zw),
        GetDeviceDepth(positionVP + float2(2.f, 0.f) * _ViewSize.zw)
    );
    float4 verticalDepth = float4(
        GetDeviceDepth(positionVP + float2(0.f, -1.f) * _ViewSize.zw),
        GetDeviceDepth(positionVP + float2(0.f, 1.f) * _ViewSize.zw),
        GetDeviceDepth(positionVP + float2(0.f, -2.f) * _ViewSize.zw),
        GetDeviceDepth(positionVP + float2(0.f, 2.f) * _ViewSize.zw)
    );

    float2 horizionDepthDiff = abs(horizionDepth.xy * 2 - horizionDepth.zw - centerDepth);
    float2 verticalDepthDiff = abs(verticalDepth.xy * 2 - verticalDepth.zw - centerDepth);

    float3 horizionVec = horizionDepthDiff.x < horizionDepthDiff.y ? depthDiff_L : depthDiff_R;
    float3 verticalVec = verticalDepthDiff.x < verticalDepthDiff.y ? depthDiff_B : depthDiff_T;

    float3 normalVS = SafeNormalize(cross(horizionVec, verticalVec));

    return normalVS;
}

总结

总的来说,方法二和方法三效果不会特别大,在做后处理时,可以针对这三种重建法线的方式进行画质分级

代码仓库

https://github.com/chenglixue/Unity-RebuildNomralVS/tree/main

Reference

Improved normal reconstruction from depth

Accurate Normal Reconstruction from Depth Buffer


他们曾如此骄傲的活过,贯彻始终