刚刚…好像有什么东西过去了。
阅读注意:
10.8.1
当前版本URP的运动模糊是比较清晰简单的,其大体思路与GPU Gems3中提到的方法大致相同。首先记录前一帧的观察矩阵和投影矩阵,然后计算出相同位置前一帧和当前帧的位置差,得出速度方向,最后沿着速度方向上多次采样进行模糊。
顶点着色器
我们直接来康康顶点部分:
VaryingsCMB VertCMB(Attributes input)
{
VaryingsCMB output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
#if _USE_DRAW_PROCEDURAL
GetProceduralQuad(input.vertexID, output.positionCS, output.uv.xy);
#else
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv.xy = input.uv;
#endif
float4 projPos = output.positionCS * 0.5;
projPos.xy = projPos.xy + projPos.w;
output.uv.zw = projPos.xy;
return output;
}
这段除了基本的坐标转换,就是计算NDC坐标小连招了(_USE_DRAW_PROCEDURAL
的内容用于处理全屏面片)。
float4 projPos = output.positionCS * 0.5;
其中,positionCS
的w分量为 w w w,x、y范围为 [ − w , w ] [-w,w] [−w,w]。经过计算后,projPos
的x、y范围为 [ − 0.5 w , 0.5 w ] [-0.5w,0.5w] [−0.5w,0.5w],w的值为 0.5 w 0.5w 0.5w。
projPos.xy = projPos.xy + projPos.w;
projPos.xy
范围映射为 [ 0 , w ] [0,w] [0,w]。之后只需要进行齐次除法和重新映射到 [ − 1 , 1 ] [-1,1] [−1,1]就可以当NDC坐标用了。 因为后面会用这个坐标会乘一个投影逆矩阵unity_CameraInvProjection
,来还原观察空间中的坐标,其中本身就包含平台信息,所以上面操作中的y分量没有必要乘以_ProjectionParams.x
来消除平台差异。
片元着色器
half4 Frag(VaryingsCMB input) : SV_Target
{
return DoMotionBlur(input, 2);// 2为迭代次数
}
URP的运动模糊根据迭代次数的不同,低中高三种品质,其迭代次数分别为2、3和4。
half4 DoMotionBlur(VaryingsCMB input, int iterations)
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
float2 uv = UnityStereoTransformScreenSpaceTex(input.uv.xy);// 你可以当作直接返回UV
float2 velocity = GetCameraVelocity(float4(uv, input.uv.zw)) * _Intensity;
float randomVal = InterleavedGradientNoise(uv * _SourceSize.xy, 0);
float invSampleCount = rcp(iterations * 2.0);
half3 color = 0.0;
UNITY_UNROLL
for (int i = 0; i < iterations; i++)
{
color += GatherSample(i, velocity, invSampleCount, uv, randomVal, -1.0);
color += GatherSample(i, velocity, invSampleCount, uv, randomVal, 1.0);
}
return half4(color * invSampleCount, 1.0);
}
我们先来看看速度是怎么计算的
float2 GetCameraVelocity(float4 uv)
{
float depth = SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_PointClamp, uv.xy).r;
#if UNITY_REVERSED_Z
depth = 1.0 - depth;
#endif
depth = 2.0 * depth - 1.0;
float3 viewPos = ComputeViewSpacePosition(uv.zw, depth, unity_CameraInvProjection);
float4 worldPos = float4(mul(unity_CameraToWorld, float4(viewPos, 1.0)).xyz, 1.0);
float4 prevPos = worldPos;
float4 prevClipPos = mul(_PrevViewProjM, prevPos);
float4 curClipPos = mul(_ViewProjM, worldPos);
float2 prevPosCS = prevClipPos.xy / prevClipPos.w;
float2 curPosCS = curClipPos.xy / curClipPos.w;
return ClampVelocity(prevPosCS - curPosCS, _Clamp);
}
_PrevViewProjM
和_ViewProjM
分别为前一帧和当前的VP矩阵我们采样了深度贴图_CameraDepthTexture
,并把它映射到了 [ − 1 , 1 ] [-1,1] [−1,1]。并根据这个深度和NDC信息还原观察空间的坐标。
float3 viewPos = ComputeViewSpacePosition(uv.zw, depth, unity_CameraInvProjection);
值得注意的是,uv.zw
没有进行齐次除法(映射到 [ − 1 , 1 ] [-1,1] [−1,1]的部分在函数内部进行),在这里直接进行使用。这是因为后处理运动模糊是通过Bilt
命令进行的,它会重置观察、投影矩阵,所以齐次除法的 w w w实际上是1,也就没必要除了。
得到观察空间坐标,继续还原到世界空间坐标。然后,通过前后两帧的VP矩阵,计算裁剪空间的坐标,并进行齐次除法。prevPosCS - curPosCS
即为相机的运动方向。
float2 ClampVelocity(float2 velocity, float maxVelocity)
{
float len = length(velocity);
return (len > 0.0) ? min(len, maxVelocity) * (velocity * rcp(len)) : 0.0;
}
ClampVelocity
函数限制了一下速度的大小,并交由_Clamp
进行调节。
回到DoMotionBlur
函数:
float randomVal = InterleavedGradientNoise(uv * _SourceSize.xy, 0);
这里通过一个交叉梯度噪音函数生成了一个随机值。
//From Next Generation Post Processing in Call of Duty: Advanced Warfare [Jimenez 2014]
// http://advances.realtimerendering.com/s2014/index.html
float InterleavedGradientNoise(float2 pixCoord, int frameCount)
{
const float3 magic = float3(0.06711056f, 0.00583715f, 52.9829189f);
float2 frameMagicScale = float2(2.083f, 4.867f);
pixCoord += frameCount * frameMagicScale;
return frac(magic.z * frac(dot(pixCoord, magic.xy)));
}
这个算法源于Next Generation Post Processing in Call of Duty
。我们可以用工具将这个噪声图像生成出来(输入uv * _SourceSize.xy
):
噪声拥有丰富的值范围,有着交错的梯度,这能让我们后续的模糊采样效果更加平滑。
后续的采样部分:
float invSampleCount = rcp(iterations * 2.0);
half3 color = 0.0;
UNITY_UNROLL
for (int i = 0; i < iterations; i++)
{
color += GatherSample(i, velocity, invSampleCount, uv, randomVal, -1.0);
color += GatherSample(i, velocity, invSampleCount, uv, randomVal, 1.0);
}
return half4(color * invSampleCount, 1.0);
其中GatherSample
部分:
float3 GatherSample(float sampleNumber, float2 velocity, float invSampleCount, float2 centerUV, float randomVal, float velocitySign)
{
// offsetLength 范围为[sampleNumber, sampleNumber + 1]
float offsetLength = (sampleNumber + 0.5) + (velocitySign * (randomVal - 0.5));
float2 sampleUV = centerUV + (offsetLength * invSampleCount) * velocity * velocitySign;
return SAMPLE_TEXTURE2D_X(_SourceTex, sampler_PointClamp, sampleUV).xyz;
}
我们沿着速度方向的两边进行随机采样,制造运动模糊。以2次迭代为例(红点为当前位置):
白点是invSampleCount * velocity
的倍数。
运动模糊效果如下:
水平有限,如有错误,请多包涵 (〃‘▽’〃)