为什么要重建法线图
在后处理中会丢失场景中的顶点信息和法线信息,但在描边或做一些屏幕效果时,都需要用到法线信息
如何重建法线图
主要思路是使用叉乘,因为法线是垂直于面的向量,所以需要在求得某个面的两个向量。但仅仅是这种方法,会存在瑕疵,本篇将介绍如何对其进行优化
实现
方法一
第一种方法即是最简单的叉乘,只需采样三个点
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上
。具体做法如下图所示:
- 延长线段ab到点c,得到点c_1
- 延迟线段ed到点c,得到点c_2
- 如果|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
Comments | NOTHING