(十一)unity Shader之——————屏幕后处理效果(高斯模糊)
前一篇文章中讲了卷积的概念,并利用卷积实现了一个简单的边缘检测效果。下面将学习卷积的另一个常见应用——高斯模糊。模糊的实现方法有很多,例如均值模糊和中值模糊,均值模糊同样使用了卷积操作,它使用的卷积核中的各个元素值都相等,且相加等于1,也就是说,卷积后得到的像素值是其领域内各个像素值的平均值。而中值模糊则是选择领域内的所有像素排序后的中值替换掉原颜色。一个更高级的模糊方法是高斯模糊。
下图是原效果:

下图是高斯模糊效果:
一、高斯滤波
高斯模糊同样利用了卷积计算,它使用的卷积核名为高斯核。高斯核是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:

其中, σ为标准方差(一般为1)。x,y分别对应了当前位置到卷积核中心的整数距离。要构建一个高斯核,需要对高斯核中的权重进行归一化,既让每个权重除以所有权重的和,这样可以保证所有权重的和为1.因此高斯函数中e前面的系数实际不会对结果有任何影响。
高斯方程很好地模拟了领域每个像素对当前处理像素的影响程度——距离越近,影响越大。高斯核的维度越高,模糊程度越大。下面会使用5X5的高斯核对原图像进行高斯模糊,先后调用两个pass,第一个pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个pass再使用水平方向的一维高斯核对图像进行滤波,得到最终的目标图像。在实现中,还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。
二、实现
新建脚本GaussianBlur.cs
1.继承前面的文章中写的基类:
public class GaussianBlur : PostEffectsBase {
2.声明效果需要的shader,并据此创建相应的材质:
public Shader gaussianBlurShader;
private Material gaussianBlurMaterial = null;
public Material material {
get {
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
3.提供了调整高斯模糊迭代次数、模糊范围和缩放系数的参数:
// Blur iterations - larger number means more blur.
[Range(0, 4)]
public int iterations = 3;
// Blur spread for each iteration - larger value means more blur
[Range(0.2f, 3.0f)]
public float blurSpread = 0.6f;
[Range(1, 8)]
public int downSample = 2;
blurSpread和downSample都是出于性能的考虑,在高斯核维数不变的情况下,_BlurSize越大,模糊程度越高,但采样数却不会受到影响。但过大的_BlurSize值会造成虚影,这可能并不是我们希望的,而downSample越大,需要处理的像素越少,同时也能进一步提高模糊程度,但过大的downSample可能会使图像像素化。
4.最后需要定义关键的OnRenderImage函数,首先看第一个版本:
//1st edition: just apply blur
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
int rtW = src.width;
int rtH = src.height;
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(src, buffer, material, 0);
// Render the horizontal pass
Graphics.Blit(buffer, dest, material, 1);
RenderTexture.ReleaseTemporary(buffer);
}
else
{
Graphics.Blit(src, dest);
}
}
与前面的实现不同,这里利用RenderTexture.GetTemporary函数分配了一块与屏幕图像大小相同的缓冲区,这是因为高斯模糊需要调用两个pass,需要使用一块中间缓存来存储第一个pass执行完毕后得到的模糊结果。如代码所示,首先调用Graphics.Blit(src,buffer,material,0),使用shader中第一个Pass(即使用竖直方向的一维高斯核进行滤波)对src进行处理,并将结果存储在了buffer中,然后在调用 Graphics.Blit(buffer, dest, material, 1),使用shader中第二个pass对buffer进行处理,返回最终的屏幕图像,最后我们还需要调用RenderTexture.ReleaseTemporary来释放之前分配的缓存。
5.下面实现第二个版本的OnRenderImage函数,这个版本中我们将利用缩放对图像进行降采样,从而减少需要处理的像素个数,提高性能。
// 2nd edition: scale the render texture
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (material != null)
{
int rtW = src.width / downSample;
int rtH = src.height / downSample;
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer.filterMode = FilterMode.Bilinear;
// Render the vertical pass
Graphics.Blit(src, buffer, material, 0);
// Render the horizontal pass
Graphics.Blit(buffer, dest, material, 1);
RenderTexture.ReleaseTemporary(buffer);
}
else
{
Graphics.Blit(src, dest);
}
}
与第一个版本代码不同的是,我们在声明缓冲区的大小时,使用了小于原屏幕分辨率的尺寸,并将该临时渲染纹理的滤波模式设置为双线性。这样在调用第一个pass时,我们需要处理的像素个数就是原来的几分之一。对图像进行降采样不仅可以减少需要处理的像素个数,提高性能,而且适当的降采样往往还可以得到更好的模糊效果,尽管downSample值越大,性能越好,但过大的downSample可能会造成图像像素化。
6.最后一个版本的代码还考虑了高斯模糊的迭代次数:
/// 3rd edition: use iterations for larger blur
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);
}
}
上面的代码显示了如何利用两个临时缓存在迭代之间进行交替的过程。在迭代开始前,我们首先定义了第一个缓存buffer0,并把src中的图像缩放后存储到buffer0中。在迭代的过程中,我们又定义了第二个缓存buffer1.在执行第一个pass时,输入是buffer0,输出是buffer1,完毕后首先把buffer0释放,再把结果值buffer1存储到buffer0中,重新分配buffer1,然后在调用第二个pass,重复上述过程。在迭代完成后,buffer0将存储最终的图像,再利用Graphics.Blit(buffer0, dest);把结果显示到屏幕上,并释放缓存。
下面实现shader部分:
1.首先声明属性:
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
2.我们将使用CGINCLUDE来组织代码。我们在Subshader块中利用CGINCLUDE和ENDCG语义定义代码。这些代码不需要包含任何Pass语义块,使用时只需要在pass块中直接指定需要使用的顶点着色器和片元着色器函数名即可。CGINCLUDE类似C++中头文件的功能。由于高斯模糊需要定义两个Pass块,它们使用的片元着色器代码是完全相同的,使用它可以避免我们编写两个完全一样的frag函数。
3.在CG代码块定义属性对应变量:
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
由于要得到相邻像素的纹理坐标,使用unity提供的_MainTex_TexelSize变量,以机损相邻像素的纹理坐标偏移量。
4.分别定义两个Pass使用的顶点着色器,下面是竖直方向的顶点着色器代码:
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;
}
我们利用5X5大小的高斯核对原图像进行高斯模糊,一个5X5二维高斯核可以拆分为两个大小为5的一维高斯核,因此只需要计算5个纹理坐标。定义一个5维的纹理坐标数组,数组第一个坐标存储了当前的采样纹理,而剩余四个坐标则是高斯模糊中对邻域采样时使用的纹理坐标。我们还和属性_BlurSize相乘来控制采样距离。在高斯核维数不变的情况下,_BlurSize越大,模糊程度越高,但采样数不会受到影响。但过大_BlurSize值会造成虚影。通过把计算采样纹理坐标代码从片元着色器中转移到顶点着色器中,可以减少运算,提高性能,由于从顶点着色器到片元着色器的插值是线性的,因此这样转移并不会影响纹理坐标的计算结果。
水平方向的也类似,只是在计算4个纹理坐标时使用了水平方向的纹素大小进行纹理偏移:
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;
}
5.定义两个Pass公共的片元着色器:
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);
}
一个5X5二维高斯核可以拆分为两个大小为5的一维高斯核,并且由于它的对称性,只需要记录3个高斯权重,也就是weight变量,首先声明各个相邻像素对应权重weight,然后将结果sum初始化为当前的像素值乘它的权重值,根据对称性,进行两次迭代,每次迭代包含了两次纹理采样,并把像素值和权重相乘后的结果叠加到sum中,最后函数返回滤波结果sum。
6.然后定义了高斯模糊使用的两个pass:
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
}
首先设置了渲染状态,设置Pass的名字,因为高斯模糊是非常常见的图像处理操作,很多屏幕后处理特效都是建立在它的基础上的,例如bloom效果,可以在其他shader中直接通过他们名字来使用该pass,不需要重复写代码。
更多推荐



所有评论(0)