《Unity Shader入门精要》第十二章屏幕后处理技巧中介绍了运动模糊的实现,因为涉及的知识点较多,因此单记一篇笔记,如有错误的理解,望各位指正。感谢乐乐女神~
透明度测试 与 透明度混合
实现透明效果主要有两种方法,一种是透明度测试,一种是透明度混合。透明度测试即在shader中加入clip()函数,裁剪掉小于透明度阈值的片元,无法实现半透明的效果。透明度用来实现半透明的效果,更加复杂。
使用透明度混合,必须要添加ZWrite Off与Blend语义,告诉Unity不将透明物体的深度写入深度缓冲,并且要混合当前片元与颜色缓冲中的片元RGB值,利用Shader中frag返回的alpha通道值混合。混合的模式有多种方式:
1 2 3 4
| Blend Off Blend SrcFactor DstFactor Blend SrcFactor DstFactor,SrcFactorA DstFactorA BlendOp 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); transform.LookAt(lookAt); 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; 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); float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1); float4 D = mul(_CurrentViewProjectionInverseMatrix, H); float4 worldPos = D / D.w; float4 currentPos = H; float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos); previousPos /= previousPos.w; 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 入门精要