UnityShader入门精要——高斯模糊

均值模糊:它使用的卷积核中的各个元素值都相等,且相加等于1,即卷积后得到的像素值是其邻域内各个像素值的平均值。

中值模糊:选择邻域内对所有像素排序后的中值替换掉原颜色。

高斯模糊:使用的卷积核名为高斯核,是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:

G(x,y)=\frac{1}{2\pi \sigma ^{2 }}e^{\frac{x^{2}+y^{2}}{2\sigma ^{2}}}

其中,σ是标准方差(一般取值为1),x和y分别对应了当前位置到卷积核中心的整数距离。

高斯方程很好地模拟了邻域每个像素对当前处理像素的影响程度一距离越近,影响越大。高斯核的维数越高,模糊程度越大。

使用一个NxN的高斯核对图像进行卷积滤波,就需要NxNxWxH(W和H分别是图像的宽和高)次纹理采样。当N的大小不断增加时,采样次数会变得非常巨大。我们可以把这个二维高斯函数拆分成两个一维函数。也就是说,我们可以使用两个一维的高斯核先后对图像进行滤波,它们得到的结果和直接使用二维高斯核是一样的,但采样次数只需要2xNxWxH。我们可以进一步观察到,两个一维高斯核中包含了很多重复的权重。对于一个大小为5的一维高斯核,我们实际只需要记录3个权重值即可。

后处理代码: 

using UnityEngine;
using System.Collections;

public class GaussianBlur : PostEffectsBase {

	public Shader gaussianBlurShader;
	private Material gaussianBlurMaterial = null;

	public Material material {  
		get {
			gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
			return gaussianBlurMaterial;
		}  
	}

	// 高斯模糊迭代次数
	[Range(0, 4)]
	public int iterations = 3;
	
	// 模糊范围
	[Range(0.2f, 3.0f)]
	public float blurSpread = 0.6f;
	
    // 缩放系数的参数
	[Range(1, 8)]
	public int downSample = 2;
	
	/// 1st edition: just apply blur
	***

	/// 2nd edition: scale the render texture
    ***

	/// 3rd edition: use iterations for larger blur
    ***
}

blurSpread和downSample都是出于性能的考虑。在高斯核维数不变的情况下,_BlurSize 越大,模糊程度越高,但采样数却不会受到影响,但过大的_BlurSize 值会造成虚影。而downSample越大,需要处理的像素数越少,同时也能进一步提高模糊程度,但过大的downSample可能会使图像像素化。

(1)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.Release Temporary来释放之前分配的缓存。

(2) 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);
		}
	}

与第一个版本代码不同的是,我们在声明缓冲区的大小时,使用了小于原屏幕分辨率的尺寸,并将该临时渲染纹理的滤波模式设置为双线性(buffer.filterMode = FilterMode.Bilinear)。这样,在调用第一个 Pass时,我们需要处理的像素个数就是原来的几分之一。对图像进行降采样不仅可以减少需要处理的像素个数,提高性能,而且适当的降采样往往还可以得到更好的模糊效果。尽管downSample值越大,性能越好,但过大的downSample可能会造成图像像素化。

(3)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);

                // 空出buffer1为下一次循环准备
				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代码如下:

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 = UnityObjectToClipPos(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 = UnityObjectToClipPos(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语义定义了名字,可以在其他Shader中直接通过它们的名字来使用该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"
}

本次在SubShader中利用CGINCLUDE...ENDCG中定义一系列代码,这些代码不需要包含在任何Pass 语义块中,在使用时,我们只需要在Pass中直接指定需要使用的顶点着色器和片元着色器函数名即可。CGINCLUDE类似于C++中头文件的功能。由于高斯模糊需要定义两个Pass,但它们使用的片元着色器代码是完全相同的,使用CGINCLUDE可以避免我们编写两个完全一样的 frag函数。

我们为两个Pass 使用NAME语义定义名字,为Pass定义名字,可以在其他.Shader中直接通过它们的名字来使用该Pass,而不需要再重复编写代码。

在本节中我们会利用5x5大小的高斯核对原图像进行高斯模糊,而一个5x5的二维高斯核可以拆分成两个大小为5的一维高斯核,因此我们只需要计算5个纹理坐标即可。为此,我们在v2f结构体中定义了一个5维的纹理坐标数组。数组的第一个坐标存储了当前的采样纹理,而剩余的四个坐标则是高斯模糊中对邻域采样时使用的纹理坐标。我们还和属性_ BlurSize相乘来控制采样距离。

struct v2f {
			float4 pos : SV_POSITION;
			half2 uv[5]: TEXCOORD0;
		};
		  
v2f vertBlurVertical(appdata_img v) {
	v2f o;
	o.pos = UnityObjectToClipPos(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;
}

在高斯核维数不变的情况下,_BlurSize 越大,模糊程度越高,但采样数却不会受到影响。但过大的BlurSize 值会造成虚影,这可能并不是我们希望的。通过把计算采样纹理坐标的代码从片元着色器中转移到顶点着色器中,可以减少运算,提高性能。由于从顶点着色器到片元着色器的插值是线性的,因此这样的转移并不会影响纹理坐标的计算结果

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。

猜你喜欢

转载自blog.csdn.net/weixin_51327051/article/details/123031360