Shaderlab Notizen 16 Gaussian Blur(高斯模糊)特效

一、降采样与高斯模糊的原理
1.1 图像的降采样

降采样(Downsample)也称下采样(Subsample),按字面意思理解即是降低采样频率。对于一幅N*M的图像来说,如果降采样系数为k,则降采样即是在原图中每行每列每隔k个点取一个点组成一幅图像的一个过程。

降采样系数K值越大,则需要处理的像素点越少,运行速度越快。

1.2 高斯模糊原理

高斯模糊(Gaussian Blur),也叫高斯平滑,高斯滤波,其通常用它来减少图像噪声以及降低细节层次,常常也被用于对图像进行模糊。

通俗的讲,高斯模糊就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯模糊的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。

高斯分布的数学表示如下:

A73811A6-94BE-4920-90D8-701A8E0DC923.jpg

其中,x为到像素中心的距离,σ为标准差。

Paste_Image.png

高斯分布(正态分布曲线)

说明一下高斯模糊的几个要点:

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

二、高斯模糊特效在Unity的实现

Unity中的屏幕特效,通常分为两部分来实现:

  • Shader代码实现部分
  • C#/javascript代码实现部分

上述两者结合起来,便可以在Unity中实现具有很强可控性和灵活性的屏幕后期特效。
实现思路类似Standard Assets/Image Effect中的Blur

2.1 Shader部分
书写思路方面,采用了3个通道(Pass),分别是:

  • 通道0:降采样通道。
  • 通道1:垂直方向模糊处理通道。
  • 通道2:水平方向模糊处理通道。

而三个通道中共用的变量、函数和结构体的代码位于CGINCLUDE和ENDCG之间。

源码如下:

Shader "Shader/RapidBlurEffect"
{
    //-----------------------------------【属性 || Properties】------------------------------------------
    Properties
    {
        //主纹理
        _MainTex("Base (RGB)", 2D) = "white" {}
    }

    //----------------------------------【子着色器 || SubShader】---------------------------------------
    SubShader
    {
        ZWrite Off
        Blend Off

        //---------------------------------------【通道0 || Pass 0】------------------------------------
        //通道0:降采样通道 ||Pass 0: Down Sample Pass
        Pass
        {
            ZTest Off
            Cull Off

            CGPROGRAM

            //指定此通道的顶点着色器为vert_DownSmpl
            #pragma vertex vert_DownSmpl
            //指定此通道的像素着色器为frag_DownSmpl
            #pragma fragment frag_DownSmpl

            ENDCG

        }

        //---------------------------------------【通道1 || Pass 1】------------------------------------
        //通道1:垂直方向模糊处理通道 ||Pass 1: Vertical Pass
        Pass
        {
            ZTest Always
            Cull Off

            CGPROGRAM

            //指定此通道的顶点着色器为vert_BlurVertical
            #pragma vertex vert_BlurVertical
            //指定此通道的像素着色器为frag_Blur
            #pragma fragment frag_Blur

            ENDCG
        }

        //---------------------------------------【通道2 || Pass 2】------------------------------------
        //通道2:水平方向模糊处理通道 ||Pass 2: Horizontal Pass
        Pass
        {
            ZTest Always
            Cull Off

            CGPROGRAM

            //指定此通道的顶点着色器为vert_BlurHorizontal
            #pragma vertex vert_BlurHorizontal
            //指定此通道的像素着色器为frag_Blur
            #pragma fragment frag_Blur

            ENDCG
        }
    }

    //-------------------------CG着色语言声明部分 || Begin CG Include Part----------------------
    CGINCLUDE

    //【1】头文件包含 || include
    #include "UnityCG.cginc"

    //【2】变量声明 || Variable Declaration
    sampler2D _MainTex;
    //UnityCG.cginc中内置的变量,纹理中的单像素尺寸|| it is the size of a texel of the texture
    uniform half4 _MainTex_TexelSize;
    //C#脚本控制的变量 || Parameter
    uniform half _DownSampleValue;

    //【3】顶点输入结构体 || Vertex Input Struct
    struct VertexInput
    {
        //顶点位置坐标
        float4 vertex : POSITION;
        //一级纹理坐标
        half2 texcoord : TEXCOORD0;
    };

    //【4】降采样输出结构体 || Vertex Input Struct
    struct VertexOutput_DownSmpl
    {
        //像素位置坐标
        float4 pos : SV_POSITION;
        //一级纹理坐标(右上)
        half2 uv20 : TEXCOORD0;
        //二级纹理坐标(左下)
        half2 uv21 : TEXCOORD1;
        //三级纹理坐标(右下)
        half2 uv22 : TEXCOORD2;
        //四级纹理坐标(左上)
        half2 uv23 : TEXCOORD3;
    };

    //【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)
    };

    //【6】顶点着色函数 || Vertex Shader Function
    VertexOutput_DownSmpl vert_DownSmpl(VertexInput v)
    {
        //【6.1】实例化一个降采样输出结构
        VertexOutput_DownSmpl o;

        //【6.2】填充输出结构
        //将三维空间中的坐标投影到二维窗口
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
        //对图像的降采样:取像素上下左右周围的点,分别存于四级纹理坐标中
        o.uv20 = v.texcoord + _MainTex_TexelSize.xy* half2(0.5h, 0.5h);;
        o.uv21 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, -0.5h);
        o.uv22 = v.texcoord + _MainTex_TexelSize.xy * half2(0.5h, -0.5h);
        o.uv23 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, 0.5h);

        //【6.3】返回最终的输出结果
        return o;
    }

    //【7】片段着色函数 || Fragment Shader Function
    fixed4 frag_DownSmpl(VertexOutput_DownSmpl i) : SV_Target
    {
        //【7.1】定义一个临时的颜色值
        fixed4 color = (0,0,0,0);

        //【7.2】四个相邻像素点处的纹理值相加
        color += tex2D(_MainTex, i.uv20);
        color += tex2D(_MainTex, i.uv21);
        color += tex2D(_MainTex, i.uv22);
        color += tex2D(_MainTex, i.uv23);

        //【7.3】返回最终的平均值
        return color / 4;
    }

    //【8】顶点输入结构体 || Vertex Input Struct
    struct VertexOutput_Blur
    {
        //像素坐标
        float4 pos : SV_POSITION;
        //一级纹理(纹理坐标)
        half4 uv : TEXCOORD0;
        //二级纹理(偏移量)
        half2 offset : TEXCOORD1;
    };

    //【9】顶点着色函数 || Vertex Shader Function
    VertexOutput_Blur vert_BlurHorizontal(VertexInput v)
    {
        //【9.1】实例化一个输出结构
        VertexOutput_Blur o;

        //【9.2】填充输出结构
        //将三维空间中的坐标投影到二维窗口
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
        //纹理坐标
        o.uv = half4(v.texcoord.xy, 1, 1);
        //计算X方向的偏移量
        o.offset = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _DownSampleValue;

        //【9.3】返回最终的输出结果
        return o;
    }

    //【10】顶点着色函数 || Vertex Shader Function
    VertexOutput_Blur vert_BlurVertical(VertexInput v)
    {
        //【10.1】实例化一个输出结构
        VertexOutput_Blur o;

        //【10.2】填充输出结构
        //将三维空间中的坐标投影到二维窗口
        o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
        //纹理坐标
        o.uv = half4(v.texcoord.xy, 1, 1);
        //计算Y方向的偏移量
        o.offset = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _DownSampleValue;

        //【10.3】返回最终的输出结果
        return o;
    }

    //【11】片段着色函数 || Fragment Shader Function
    half4 frag_Blur(VertexOutput_Blur i) : SV_Target
    {
        //【11.1】获取原始的uv坐标
        half2 uv = i.uv.xy;

        //【11.2】获取偏移量
        half2 OffsetWidth = i.offset;
        //从中心点偏移3个间隔,从最左或最上开始加权累加
        half2 uv_withOffset = uv - OffsetWidth * 3.0;

        //【11.3】循环获取加权后的颜色值
        half4 color = 0;
        for (int j = 0; j< 7; j++)
        {
            //偏移后的像素纹理值
            half4 texCol = tex2D(_MainTex, uv_withOffset);
            //待输出颜色值+=偏移后的像素纹理值 x 高斯权重
            color += texCol * GaussWeight[j];
            //移到下一个像素处,准备下一次循环加权
            uv_withOffset += OffsetWidth;
        }

        //【11.4】返回最终的颜色值
        return color;
    }

    //-------------------结束CG着色语言声明部分  || End CG Programming Part------------------
    ENDCG

    FallBack Off
}

2.2 c#部分

源码如下:

using UnityEngine;
using System.Collections;

//设置在编辑模式下也执行该脚本
[ExecuteInEditMode]
//添加选项到菜单中
[AddComponentMenu("Shader/RapidBlurEffect")]
public class RapidBlurEffect : MonoBehaviour
{
    //-------------------变量声明部分-------------------
    #region Variables

    //指定Shader名称
    private string ShaderName = "Shader/RapidBlurEffect";

    //着色器和材质实例
    public Shader CurShader;
    private Material CurMaterial;

    //几个用于调节参数的中间变量
    public static int ChangeValue;
    public static float ChangeValue2;
    public static int ChangeValue3;

    //降采样次数
    [Range(0, 6), Tooltip("[降采样次数]向下采样的次数。此值越大,则采样间隔越大,需要处理的像素点越少,运行速度越快。")]
    public int DownSampleNum = 2;
    //模糊扩散度
    [Range(0.0f, 20.0f), Tooltip("[模糊扩散度]进行高斯模糊时,相邻像素点的间隔。此值越大相邻像素间隔越远,图像越模糊。但过大的值会导致失真。")]
    public float BlurSpreadSize = 3.0f;
    //迭代次数
    [Range(0, 8), Tooltip("[迭代次数]此值越大,则模糊操作的迭代次数越多,模糊效果越好,但消耗越大。")]
    public int BlurIterations = 3;

    #endregion

    //-------------------------材质的get&set----------------------------
    #region MaterialGetAndSet
    Material material
    {
        get
        {
            if (CurMaterial == null)
            {
                CurMaterial = new Material(CurShader);
                CurMaterial.hideFlags = HideFlags.HideAndDontSave;
            }
            return CurMaterial;
        }
    }
    #endregion

    #region Functions
    //-----------------------------------------【Start()函数】---------------------------------------------
    // 说明:此函数仅在Update函数第一次被调用前被调用
    //--------------------------------------------------------------------------------------------------------
    void Start()
    {
        //依次赋值
        ChangeValue = DownSampleNum;
        ChangeValue2 = BlurSpreadSize;
        ChangeValue3 = BlurIterations;

        //找到当前的Shader文件
        CurShader = Shader.Find(ShaderName);

        //判断当前设备是否支持屏幕特效
        if (!SystemInfo.supportsImageEffects)
        {
            enabled = false;
            return;
        }
    }

    //-------------------------------------【OnRenderImage()函数】------------------------------------
    // 说明:此函数在当完成所有渲染图片后被调用,用来渲染图片后期效果
    //--------------------------------------------------------------------------------------------------------
    void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
    {
        //着色器实例不为空,就进行参数设置
        if (CurShader != null)
        {
            //【0】参数准备
            //根据向下采样的次数确定宽度系数。用于控制降采样后相邻像素的间隔
            float widthMod = 1.0f / (1.0f * (1 << DownSampleNum));
            //Shader的降采样参数赋值
            material.SetFloat("_DownSampleValue", BlurSpreadSize * widthMod);
            //设置渲染模式:双线性
            sourceTexture.filterMode = FilterMode.Bilinear;
            //通过右移,准备长、宽参数值
            int renderWidth = sourceTexture.width >> DownSampleNum;
            int renderHeight = sourceTexture.height >> DownSampleNum;

            // 【1】处理Shader的通道0,用于降采样 ||Pass 0,for down sample
            //准备一个缓存renderBuffer,用于准备存放最终数据
            RenderTexture renderBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
            //设置渲染模式:双线性
            renderBuffer.filterMode = FilterMode.Bilinear;
            //拷贝sourceTexture中的渲染数据到renderBuffer,并仅绘制指定的pass0的纹理数据
            Graphics.Blit(sourceTexture, renderBuffer, material, 0);

            //【2】根据BlurIterations(迭代次数),来进行指定次数的迭代操作
            for (int i = 0; i < BlurIterations; i++)
            {
                //【2.1】Shader参数赋值
                //迭代偏移量参数
                float iterationOffs = (i * 1.0f);
                //Shader的降采样参数赋值
                material.SetFloat("_DownSampleValue", BlurSpreadSize * widthMod + iterationOffs);

                // 【2.2】处理Shader的通道1,垂直方向模糊处理 || Pass1,for vertical blur
                // 定义一个临时渲染的缓存tempBuffer
                RenderTexture tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
                // 拷贝renderBuffer中的渲染数据到tempBuffer,并仅绘制指定的pass1的纹理数据
                Graphics.Blit(renderBuffer, tempBuffer, material, 1);
                //  清空renderBuffer
                RenderTexture.ReleaseTemporary(renderBuffer);
                // 将tempBuffer赋给renderBuffer,此时renderBuffer里面pass0和pass1的数据已经准备好
                 renderBuffer = tempBuffer;

                // 【2.3】处理Shader的通道2,竖直方向模糊处理 || Pass2,for horizontal blur
                // 获取临时渲染纹理
                tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
                // 拷贝renderBuffer中的渲染数据到tempBuffer,并仅绘制指定的pass2的纹理数据
                Graphics.Blit(renderBuffer, tempBuffer, CurMaterial, 2);

                //【2.4】得到pass0、pass1和pass2的数据都已经准备好的renderBuffer
                // 再次清空renderBuffer
                RenderTexture.ReleaseTemporary(renderBuffer);
                // 再次将tempBuffer赋给renderBuffer,此时renderBuffer里面pass0、pass1和pass2的数据都已经准备好
                renderBuffer = tempBuffer;
            }

            //拷贝最终的renderBuffer到目标纹理,并绘制所有通道的纹理到屏幕
            Graphics.Blit(renderBuffer, destTexture);
            //清空renderBuffer
            RenderTexture.ReleaseTemporary(renderBuffer);

        }

        //着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的
        else
        {
            //直接拷贝源纹理到目标渲染纹理
            Graphics.Blit(sourceTexture, destTexture);
        }
    }

    //-----------------------------------------【OnValidate()函数】--------------------------------------
    // 说明:此函数在编辑器中该脚本的某个值发生了改变后被调用
    //--------------------------------------------------------------------------------------------------------
    void OnValidate()
    {
        //将编辑器中的值赋值回来,确保在编辑器中值的改变立刻让结果生效
        ChangeValue = DownSampleNum;
        ChangeValue2 = BlurSpreadSize;
        ChangeValue3 = BlurIterations;
    }

    //-----------------------------------------【Update()函数】--------------------------------------
    // 说明:此函数每帧都会被调用
    //--------------------------------------------------------------------------------------------------------
    void Update()
    {
        //若程序在运行,进行赋值
        if (Application.isPlaying)
        {
            //赋值
            DownSampleNum = ChangeValue;
            BlurSpreadSize = ChangeValue2;
            BlurIterations = ChangeValue3;
        }
        //若程序没有在运行,去寻找对应的Shader文件
#if UNITY_EDITOR
        if (Application.isPlaying != true)
        {
            CurShader = Shader.Find(ShaderName);
        }
#endif

    }

    //-----------------------------------------【OnDisable()函数】---------------------------------------
    // 说明:当对象变为不可用或非激活状态时此函数便被调用
    //--------------------------------------------------------------------------------------------------------
    void OnDisable()
    {
        if (CurMaterial)
        {
            //立即销毁材质实例
            DestroyImmediate(CurMaterial);
        }

    }

 #endregion

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容