unity屏幕特效综述1 高斯模糊

1.高斯模糊

高斯模糊是后面很多屏幕特效的基础部分,其原理也很容易。

在学习之前,必须要学会卷积的操作,

如图所示, 卷积操作指的是使用一个卷积核(左图的3x3矩阵)对待处理图像的每一个像素进行卷积操作,具体的做法是把3x3矩阵的中心点放到待卷积的像素上,然后对卷积核覆盖到的像素的值乘以卷积核的值,然后求和,就是该像素的最终结果。

例如,我们使用卷积核为3x3的矩阵,矩阵每一个值都是1/9,然后利用这个矩阵对图像进行卷积操作,那么我们得到的结果的每一个像素值都是以该像素为中心的周围九个像素的平均值,这种卷积的过程也叫作均值模糊。

高斯模糊的原理和均值模糊很是类似,但是又有一些不同,其不同之处就在于卷积核的选取方法的不同,高斯模糊还考虑到了不同像素的权重,也就是越近的像素其权重越大,越远的像素权重越小。

其每个元素的计算满足下面的公式

x和y表示当前位置到卷积核中心的整数距离,根据这公式,我们计算出来一个5X5的高斯模糊的卷积核,然后对其进行归一化

因此,利用这个卷积核来对图像操作就可以实现高斯模糊了。

具体的实现还是有优化的方法的,直接利用这个矩阵的计算太繁杂了,假设图像的大小为W*H,利用上面的卷积核进行计算的复杂度是W*H*N*N(N为卷积核的维度),我们可以把上面的卷积核拆分成两个一维的矩阵

横着的为横向的权值的和,竖着的为纵向权值的和,我们对图像处理两次,第一次是利用竖直方向的一维卷积核进行卷积,第二次是利用水平方向的高斯核对图像进行卷积,这样我们得到的结果和使用上面的矩阵是一样的,但是复杂度变成了W*H*N*2.

在实际的实现中,我们还添加了一些参数来控制最后高斯模糊的效果,主要有三个参数:

第一个参数是iteration,这个参数用来控制迭代次数,理解也很简单,对图像进行高斯模糊iteration次,理论上次数越大,图像越模糊。第二个参数是blurSpread,卷积操作的时候,我们是对相邻的像素进行采样的,加入了这个参数之后,我们采样范围会扩大一些,比如隔2个像素进行采样,如下图所示,理论上这个值越大图像也是越模糊

最后一个参数是downSample,这个参数的作用是直接把图像按downSample的比例进行缩小,缩小的好处是这样做可以直接提高模糊程度,而且缩小后的图像需要处理的像素也会变少了很多,但是过大的downsample也会造成图像的虚化。

下面就是高斯模糊视线的脚本部分:

首先我们生成一个临时的渲染纹理,大小就是经过downsample缩放后的大小,然后设置其滤波模式为双线性,这样可以使得该渲染纹理在放大或者缩小的时候能获得更好的效果。

Graphics.Blit函数会把第一个纹理利用第三个参数的shader变换到第二个纹理上面,如果第三个参数为空的话,就直接赋值到第二个纹理上面,并且会自适应大小的。此外,这个函数的第四个参数指的是调用对应shader的pass序号。

根据我们模糊的原理,直接迭代iteration次,然后每一次的模糊的半径都会依次增加,我们使用buffer1来获得buffer0经过shader中pass1得到的图像,然后清空buffer0,再把buffer1赋值给buffer0,然后同样的调用pass2,再返回最终的结果即可,pass1和pass2分别就是水平方向和垂直方向的高斯卷积。

       void OnRenderImage (RenderTexture src, RenderTexture dest) {
              if (material != null) {
                     int rtW = src.width/downSample;
                     int rtH = src.height/downSample;
                     RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH,  0);
                     buffer0.filterMode = FilterMode.Bilinear;
                     Graphics.Blit(src, buffer0);
                     for (int i = 0; i < iterations; i++) {
                           material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
                           RenderTexture buffer1 =  RenderTexture.GetTemporary(rtW, rtH, 0);
                           // Render the vertical pass
                           Graphics.Blit(buffer0, buffer1, material, 0);
                           RenderTexture.ReleaseTemporary(buffer0);
                           buffer0 = buffer1;
                           buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
                           // Render the horizontal pass
                           Graphics.Blit(buffer0, buffer1, material, 1);
                           RenderTexture.ReleaseTemporary(buffer0);
                           buffer0 = buffer1;
                     }
                     Graphics.Blit(buffer0, dest);
                     RenderTexture.ReleaseTemporary(buffer0);
              } else {
                     Graphics.Blit(src, dest);
              }
       }

接下来就是shader部分了。

unity shader如果要写的话,需要搞清楚一些问题,然后才能下的了手:

1.有哪些属性

2.需要什么输入给顶点着色器

3.需要什么输出给片元着色器

4.怎么写顶点着色器

5.怎么写片元着色器

想明白了,然后着色器的框架就出来了。

1.这里需要两个输入,第一个是_MainTex,这个变量必须,也只能叫_MainTex,Graphics.Blit会把其第一个参数传递到_MainTex变量,第二个参数是_BlurSize,上面的脚本中也涉及到了这个变量,用来控制采样的范围的。

2.顶点着色器的输入直接使用了appdata_img结构体,一般的屏幕后处理都会直接使用这个输入结构体 ,结构体的内容可以在UnityCG.cginc源码中找到,如下所示

struct appdata_img

{

    float4 vertex : POSITION;

    half2 texcoord : TEXCOORD0;

};

只包含了一个顶点坐标和纹理坐标。

3.输出的话我们需要想一想高斯模糊的原理,texcoord只能获得采样点的中心坐标,但是其周围的坐标并不能获得,因此,我们需要uv[5]来保存其上下或者左右的采样坐标,在片元着色器中对这些坐标采样得到纹理值与卷积核对应位置的值相乘求和得到最终的像素结果。此外,还必须要有裁剪空间的定点坐标,这是每一个untiy shader都必须要包括的,其实我搞不懂既然这样为什么不直接把这个信息作为内置信息,省的开发人员每次都要手动敲一下。

4、顶点着色器的目的就是填充好片段着色器的输入结构体就行了。裁剪空间的顶点坐标很容易得到,uv[5]的信息可以通过中心点以及_BlurSize进行偏移即可,如下所示:

o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;  

这个是对垂直方向卷积的uv值,水平方向也很容易,不赘述了,可以看后面的源码。

5.片段着色器的内容在大部分情况之下都是最重要的内容,这里就是我们高斯模糊的精髓所在了,我们已经得到了五个点的采样坐标,而且,我们也知道了每个采样坐标纹理信息所占的比例,只需要相乘求和就行了。

float weight[3] = {0.4026, 0.2442, 0.0545};                   
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];               
for (int it = 1; it < 3; it++) {
       sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
       sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}           

其实也是很容易理解的。

最后的源码包含两个pass,分别进行垂直和水平方向上的卷积。

Shader "Unity Shaders Book/Chapter 12/Gaussian Blur" {
       Properties {
              _MainTex ("Base (RGB)", 2D) = "white" {}
              _BlurSize ("Blur Size", Float) = 1.0
       }
       SubShader {
              CGINCLUDE            
              #include "UnityCG.cginc"          
              sampler2D _MainTex;  
              half4 _MainTex_TexelSize;
              float _BlurSize;             
              struct v2f {
                     float4 pos : SV_POSITION;
                     half2 uv[5]: TEXCOORD0;
              };              
              v2f vertBlurVertical(appdata_img v) {
                     v2f o;
                     o.pos = mul(UNITY_MATRIX_MVP, v.vertex);               
                     half2 uv = v.texcoord;                   
                     o.uv[0] = uv;
                     o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) *  _BlurSize;
                     o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) *  _BlurSize;
                     o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) *  _BlurSize;
                     o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) *  _BlurSize;                                
                     return o;
              }             
              v2f vertBlurHorizontal(appdata_img v) {
                     v2f o;
                     o.pos = mul(UNITY_MATRIX_MVP, v.vertex);               
                     half2 uv = v.texcoord;                   
                     o.uv[0] = uv;
                     o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) *  _BlurSize;
                     o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) *  _BlurSize;
                     o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) *  _BlurSize;
                     o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) *  _BlurSize;                                
                     return o;
              }             
              fixed4 fragBlur(v2f i) : SV_Target {
                     float weight[3] = {0.4026, 0.2442, 0.0545};                   
                     fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];               
                     for (int it = 1; it < 3; it++) {
                           sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
                           sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
                     }                    
                     return fixed4(sum, 1.0);
              }                 
              ENDCG         
              ZTest Always Cull Off ZWrite Off         
              Pass {
                     NAME "GAUSSIAN_BLUR_VERTICAL"                   
                     CGPROGRAM                    
                     #pragma vertex vertBlurVertical  
                     #pragma fragment fragBlur                  
                     ENDCG  
              }             
              Pass {  
                     NAME "GAUSSIAN_BLUR_HORIZONTAL"                 
                     CGPROGRAM                   
                     #pragma vertex vertBlurHorizontal  
                     #pragma fragment fragBlur                
                     ENDCG
              }
       }
       FallBack "Diffuse"
}

下面的三个图分别是原图,高斯模糊得到的图,最后一张图是使用均值模糊在同样的参数下得到的图(也就是把frag着色器里面的矩阵参数变成0.2,0.2,0.2),可以发现均值模糊是要比高斯模糊模糊的,具体哪个好那个坏,我不做评价,那是艺术家该干的事情。

发布了31 篇原创文章 · 获赞 4 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_43813453/article/details/100867562