屏幕后处理·运动模糊

《Unity Shader入门精要》第十二章屏幕后处理技巧中介绍了运动模糊的实现,因为涉及的知识点较多,因此单记一篇笔记,如有错误的理解,望各位指正。感谢乐乐女神~

透明度测试 与 透明度混合

实现透明效果主要有两种方法,一种是透明度测试,一种是透明度混合。透明度测试即在shader中加入clip()函数,裁剪掉小于透明度阈值的片元,无法实现半透明的效果。透明度用来实现半透明的效果,更加复杂。

使用透明度混合,必须要添加ZWrite Off与Blend语义,告诉Unity不将透明物体的深度写入深度缓冲,并且要混合当前片元与颜色缓冲中的片元RGB值,利用Shader中frag返回的alpha通道值混合。混合的模式有多种方式:

1
2
3
4
Blend Off //不混合,直接用当前片元覆盖颜色缓冲区
Blend SrcFactor DstFactor //利用当前片元的透明度混合 颜色缓冲区的rgb与当前片元的rgb
Blend SrcFactor DstFactor,SrcFactorA DstFactorA //赋予新的透明度因子SrcFactorA DstFactorA进行混合
BlendOp BlendOperation //指定别的方式BlendOperation进行混合

其中第二种混合方式公式如下(当SrcFactor为SrcAlpha ,DstFactor为OneMinusSrcalpha时):
$$
DstColor_{new}=SrcAlpha \times SrcColor + (1-SrcAlpha) \times DstColor_{old}
$$
其中$DstColor$为颜色缓冲区中的值。

ColorMask A指的是只对alpha通道进行写值。

屏幕后处理脚本系统

屏幕后处理,就是对执行完所有透明与不透明的Pass后的场景进行抓取,将抓取到的屏幕存在纹理中,对抓取的纹理进行一些处理,将变化后的纹理再显示再屏幕中。unity已经提供了抓取屏幕的接口—–OnRenderImage,函数声明如下:

1
MonoBehaviour.OnRenderImage(RenderTexture source,RenderTexture destination);

当在脚本中声明此函数时,Unity会将当前渲染得到的屏幕图像存储在参数source中,此函数中的实现对source的操作,最终处理后的屏幕图像保存在destination中,Unity会将destination绘制在屏幕中。在OnRenderImage中我们通常用Graphics.Blit函数实现对纹理的变换,Blit函数声明如下:

1
2
public static void Blit(Texture src,RnederTexture dest);
public static void Blit(Texture src,RnederTexture dest,Material mat,int pass=-1);

Unity设计Blit函数帮助我们变换屏幕纹理,Blit中会将src传给mat中的“_MainTex”,利用mat进行修改,将结果返回给dest,pass默认为-1表示会依次执行mat中所有的pass,否则就会调用指定的Pass。

运动模糊法1

该效果主要思路就是:当前帧的rgb值取决于上一帧与实际当前帧的混合。

让画面运动起来

设置相机运动脚本,让画面动起来,再混合每一帧造成运动模糊的效果。在书中,想要获得相机绕固定点旋转的视角,改变相机的transform,lookAt矩阵即可。像机脚本的关键代码如下:

1
2
3
4
5
6
7
8
9
void Update () {
transform.position = Vector3.Slerp(transform.position, curEndPoint, Time.deltaTime * speed);//利用Slerp球形插值函数,随着Time而移动
transform.LookAt(lookAt);//改变像机视角矩阵 View
if (pingpong) {
if (Vector3.Distance(transform.position, curEndPoint) < 0.001f) {
curEndPoint = Vector3.Distance(curEndPoint, endPoint) < Vector3.Distance(curEndPoint, startPoint) ? startPoint : endPoint;
}
}
}

shader

在shader中开启ZWrite Off与Blend语义,以混合颜色缓冲区中上一帧的RGB与当前渲染帧的RGB以达到模糊的效果。设置两个Pass,Pass1往颜色缓冲中写RGB值,并且利用_BlurAmount混合前后帧率,Pass2往颜色缓冲区中写入原有屏幕的Alpha值。

1
2
3
fixed4 fragRGB (v2f i) : SV_Target {
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}

运动模糊法2

在书的第三章中介绍了另外一种实现运动模糊的方法。是作者受《GPU GEMS3》速度映射图启发的方法。利用深度图来得到屏幕所有像素的世界坐标(经过VP逆矩阵的转换),再用上一帧的VP矩阵得到当前像素点在上一帧的像素位置。在屏幕空间坐标系中计算速度,沿着速度路径平均采样。关键代码如下:

在后处理脚本中,需要拿到当前帧VP矩阵,和上一帧的VP逆矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_BlurSize", blurSize);

material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;//VP矩阵
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;//逆矩阵
material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
previousViewProjectionMatrix = currentViewProjectionMatrix;

Graphics.Blit (src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}

shader中关键代码在fragment shader中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fixed4 frag(v2f i) : SV_Target {
//从深度图缓冲中得到当前采样点的深度值
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
//H是映射到-1~1坐标系中的坐标
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
//利用VP逆矩阵得到世界空间中坐标
float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
//齐次除法
float4 worldPos = D / D.w;

//当前坐标
float4 currentPos = H;
//因为书中的所有物体的世界空间坐标不会变化,变得只是摄像机的世界空间位置与朝向, 因此可以根据VP矩阵计算出准确的上一帧屏幕像素坐标
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
//齐次除法
previousPos /= previousPos.w;

//利用-1~1坐标系中的位置计算速度
float2 velocity = (currentPos.xy - previousPos.xy)/2.0f;

//沿着速度方向进行均值采样
float2 uv = i.uv;
float4 c = tex2D(_MainTex, uv);
uv += velocity * _BlurSize;
for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
float4 currentColor = tex2D(_MainTex, uv);
c += currentColor;
}
c /= 3;

return fixed4(c.rgb, 1.0);
}

参考文献

Unity Shader 入门精要