降采样

降采样(Downsample)也称下采样(Subsample),按字面意思理解即是降低采样频率。对于一幅N*M的图像来说,如果降采样系数为k,则降采样即是在原图中每行每列每隔k个点取一个点组成一幅图像的一个过程。不难得出,降采样系数K值越大,则需要处理的像素点越少,运行速度越快。

高斯模糊原理

高斯模糊(Gaussian Blur),也叫高斯平滑,高斯滤波,其通常用它来减少图像噪声以及降低细节层次,常常也被用于对图像进行模糊。通俗的讲,高斯模糊就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯模糊的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。

  • 从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积。
  • 由于正态分布又叫作高斯分布,所以这项技术就叫作高斯模糊。
  • 高斯模糊能够把某一点周围的像素色值按高斯曲线统计起来,采用数学上加权平均的计算方法得到这条曲线的色值
  • 所谓"模糊",可以理解成每一个像素都取周边像素的平均值。
  • 图像与圆形方框模糊做卷积将会生成更加精确的焦外成像效果。由于高斯函数的傅立叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波器。
简化的一维高斯分布函数

一维函数
一维图像

简化的二维高斯分布函数

二维函数
二维图像

一维表示二维变换函数

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

需要特别注意的地方,由于对于图像的处理高斯正态分布是二维的,如果按照正常情况下计算的话,该算法的计算时间复杂度非常高,假设屏幕分辨率是MN,我们的高斯核大小是mn,那么进行一次后处理的时间复杂度为O(MNmn)!此时我们可以把拆分为两个一维高斯公式来处理,即横线和纵向分别做处理,那么我们在横向方向需要Mmn次采样操作,而在竖向方向需要Nm*n次采样操作,总共的时间复杂度就是O((M+N)mn)
对于变量x有f(x)=0.39894*exp(-0.5*x*x/(0.20*sigma))/sigma*sigma

具体实现(二维)

纹理平铺和偏移

材质通常具有其纹理属性的 Tiling 和 Offset 字段。此信息将传递到着色器中的 float4 ​*{TextureName}*​_ST 属性:

例如,如果着色器包含名为 _MainTex 的纹理,则平铺信息将位于 _MainTex_ST 矢量中。

纹理大小

​*{TextureName}*​_TexelSize - float4 属性包含纹理大小信息:

二维一次计算

//这一步其实可以用计算出来的常量来替代,不需要在循环中每一步计算
float GetGaussWeight(float x, float y, float sigma)
{
	float sigma2 = pow(sigma, 2.0f);
	float left = 1 / (2 * sigma2 * 3.1415926f);
	float right = exp(-(x*x+y*y)/(2*sigma2));
	return left * right;
}

fixed4 GaussBlur(float2 uv)
{
	//因为高斯函数中3σ以外的点的权重已经很小了,因此σ取半径r/3的值
	float sigma = (float)_BlurRadius / 3.0f;
	float4 col = float4(0, 0, 0, 0);
	for (int x = - _BlurRadius; x <= _BlurRadius; ++x)
	{
		for (int y = - _BlurRadius; y <= _BlurRadius; ++y)
		{
			//获取周围像素的颜色
			//因为uv是0-1的一个值,而像素坐标是整形,我们要取材质对应位置上的颜色,需要将整形的像素坐标
			//转为uv上的坐标值
			float4 color = tex2D(_MainTex, uv + float2(x * _MainTex_TexelSize.x, y * _MainTex_TexelSize.y));
			//获取此像素的权重
			float weight = GetGaussWeight(x, y, sigma);
			//计算此点的最终颜色
			col += color * weight;
						
		}
	}
	return col;
}

水平模糊与垂直模糊二次计算

// 计算高斯权重
float computerBluGauss(float x,float sigma) 
{
       return 0.3989*exp(-0.5*x*x/(sigma*sigma))/sigma*sigma;
}

//计算一个偏移值
// offset(1,0,0,0)代表水平方向
// offset(0,1,0,0)表示垂直方向  
_offsets *= _MainTex_TexelSize.xyxy;  

//由于uv可以存储4个值,所以一个uv保存两个vector坐标
//_offsets.xyxy * float4(1,1,-1,-1)可能表示(0,1,0-1),表示像素上下两坐标
// 也可能是(1,0,-1,0),表示像素左右两个像素点的坐标,下面*2.0,同理
o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;

float w0 = computerBluGauss(0,5);
float w1 = computerBluGauss(1,5);
float w2 = computerBluGauss(2,5);
float sum = w0+w1*2+w2*2; // 5个点归一化
color += w0/sum * tex2D(_MainTex, i.uv);  
color += w1/sum * tex2D(_MainTex, i.uv01.xy);  
color += w1/sum * tex2D(_MainTex, i.uv01.zw);  
color += w2/sum * tex2D(_MainTex, i.uv23.xy);  
color += w2/sum * tex2D(_MainTex, i.uv23.zw);

完整代码

GaussianBlur
Shader "URP/GaussianBlur"
{
    Properties
    {
        [MainTexture]_MainTex ("Texture", 2D) = "white" {}
        [MainColor]_Color ("Color",Color) = (1,1,1,1)
        [IntRange]_BlurRadius ("BlurRadius", Range(1,15)) = 5
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Transparent"
            "RenderPipeline"="UniversalPipeline"
            "IgnoreProjector"="true"
            "Queue"="Transparent" // Transparent AlphaTest Geometry Background
        }
        LOD 100
        Blend SrcAlpha OneMinusSrcAlpha

        HLSLINCLUDE

        
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        

        TEXTURE2D_X(_MainTex);
        SAMPLER(sampler_MainTex);

        CBUFFER_START(UnityPerMaterial)
        half4 _Color;
        half4 _MainTex_ST;
        half4 _MainTex_TexelSize;
        int _BlurRadius;
        CBUFFER_END

        
        struct Attributes
        {
            half4 position : POSITION;
            half2 uv : TEXCOORD0;
        };


        struct Varyings
        {
            half4 position_sv : SV_POSITION;
            half2 uv : TEXCOORD0;
        	half4 uv01 : TEXCOORD1;
        	half4 uv23 : TEXCOORD2;
        };

		/***  方式1 ***/
        //这一步其实可以用计算出来的常量来替代,不需要在循环中每一步计算
		half GetGaussWeight(half x, half y, half sigma)
		{
			half sigma2 = pow(sigma, 2.0f);
			half left = 1 / (2 * sigma2 * 3.1415926f);
			half right = exp(-(x*x+y*y)/(2*sigma2));
			return left * right;
		}

        half4 GaussBlur(float2 uv)
		{
			//因为高斯函数中3σ以外的点的权重已经很小了,因此σ取半径r/3的值
			half sigma = _BlurRadius / 3.0h;
			half4 col = half4(0, 0, 0, 0);
			for (int x = - _BlurRadius; x <= _BlurRadius; ++x)
			{
				for (int y = - _BlurRadius; y <= _BlurRadius; ++y)
				{
					//获取周围像素的颜色
					//因为uv是0-1的一个值,而像素坐标是整形,我们要取材质对应位置上的颜色,需要将整形的像素坐标
					//转为uv上的坐标值
					half4 color = SAMPLE_TEXTURE2D_X(_MainTex,sampler_MainTex,uv + half2(x * _MainTex_TexelSize.x, y * _MainTex_TexelSize.y));
					//获取此像素的权重
					half weight = GetGaussWeight(x,y, sigma);
					//计算此点的最终颜色
					col += color * weight;
					
				}
			}
			return col;
		}
        

        /*** 方式2 ***/
        //【5】准备高斯模糊权重矩阵参数7x4的矩阵 ||  Gauss Weight
		static const half4 GaussWeight[7] =
		{
			half4(0.0205,0.0205,0.0205,0),
			half4(0.0855,0.0855,0.0855,0),
			half4(0.232,0.232,0.232,0),
			half4(0.324,0.324,0.324,1),
			half4(0.232,0.232,0.232,0),
			half4(0.0855,0.0855,0.0855,0),
			half4(0.0205,0.0205,0.0205,0)
		};

        // 水平一次 垂直一次
        half4 GaussBlur2(half2 uv,half2 offset)
		{
			//【11.2】获取偏移量
			half2 OffsetWidth = _MainTex_TexelSize.xy * offset;
			//从中心点偏移3个间隔,从最左或最上开始加权累加
			half2 uv_withOffset = uv - OffsetWidth * 3.0;
	 
			//【11.3】循环获取加权后的颜色值
			half4 color = 0;
			for (int j = 0; j < 7; j++)
			{
				//偏移后的像素纹理值
				half4 texCol = SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex,uv_withOffset);
				//待输出颜色值+=偏移后的像素纹理值 x 高斯权重
				color += texCol * GaussWeight[j];
				//移到下一个像素处,准备下一次循环加权
				uv_withOffset += OffsetWidth;
			}
	 
			//【11.4】返回最终的颜色值
			return color;
		}

        
        /*** 方式3 ***/
        half GetGaussWeight3(half x,half sigma)
		{
			half sigma2 = pow(sigma, 2.0h);
			half left = 1 / (sigma * sqrt(2*PI));
			half right = exp(-pow(x,2.0h)/2*sigma2);
			return left * right;
		}

        half4 GaussBlur3(Varyings IN)
        {
        	half4 color = 0;
        	half sigma = 1;
	        half w0 = GetGaussWeight3(0,sigma);
            half w1 = GetGaussWeight3(1,sigma);
            half w2 = GetGaussWeight3(2,sigma);
            half sum = w0+w1*2+w2*2;
            color += w0/sum * SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex,IN.uv);  
            color += w1/sum * SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex,IN.uv01.xy);  
            color += w1/sum * SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex,IN.uv01.zw);  
            color += w2/sum * SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex,IN.uv23.xy);  
            color += w2/sum * SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex,IN.uv23.zw);

        	return color;
        }
        
        
        ENDHLSL
        
        
        Pass
        {
            HLSLPROGRAM

            #pragma vertex Vert
			#pragma fragment Frag
            
            Varyings Vert(Attributes IN)
            {
                Varyings OUT;
                OUT.position_sv = TransformObjectToHClip(IN.position);
                OUT.uv = TRANSFORM_TEX(IN.uv,_MainTex);


        		// 方案3使用
        		half4 _offsets = half4(3,0,0,0);
                _offsets *= _MainTex_TexelSize.xyxy;  
                OUT.uv01 = IN.uv.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
                OUT.uv23 = IN.uv.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;  
        	
                return OUT;
            }



            half4 Frag(Varyings IN) : SV_Target
            {
                return GaussBlur3(IN);
            }
            
            
            ENDHLSL
        }
        

    }




}

参考链接

Markdown数学公式语法
Unity Shader 均值模糊和高斯模糊
Unity Shader 图片的高斯模糊
屏幕高斯模糊(Gaussian Blur)后期特效的实现高斯模糊shader