Unity shader學(xué)習(xí)---屏幕后處理效果

一.什么是屏幕后處理

1.1.概念

屏幕后處理是指渲染完整個(gè)場(chǎng)景得到屏幕圖像后墨榄,再對(duì)屏幕圖像做處理,實(shí)現(xiàn)屏幕特效巴比。使用這種技術(shù)抠璃, 可以為游戲畫面添加更多的藝術(shù)效果站楚, 例如景深( Depth of Field)、 運(yùn)動(dòng)模糊( Motion Blur) 等搏嗡。
實(shí)現(xiàn)屏幕后處理效果的關(guān)鍵在于得到渲染后的屏幕圖像,Unity提供了對(duì)應(yīng)的接口 OnRenderImage ,其函數(shù)聲明

MonoBehaviour.OnRenderImage(RenderTexture scr,RenderTexture dest)

參數(shù) src:源紋理源请,用于存儲(chǔ)當(dāng)前渲染的得到的屏幕圖像
**參數(shù) dest **:目標(biāo)紋理,經(jīng)過一系列操作后彻况,用于顯示到屏幕的圖像

在OnRenderImage函數(shù)中,通常調(diào)用 Graphics.Blit函數(shù) 完成對(duì)渲染紋理的處理
Graphics.Blit是每幀渲染時(shí)執(zhí)行t, 有3個(gè)重載:

public Static void Blit(Texture src,RenderTexture dest)
public Static void Blit(Texture src,RenderTexture dest,Material mat,int pass = -1)
public Static void Blit(Texture src,Material mat,int pass = -1)

參數(shù) mat : 使用的材質(zhì)舅踪,該材質(zhì)使用的Unity Shader將會(huì)進(jìn)行各種屏幕后處理操作,src對(duì)應(yīng)的紋理會(huì)傳遞給Shader中_MainTex的紋理屬性
參數(shù) pass:默認(rèn)值為-1纽甘,表示依次調(diào)用Shader內(nèi)所有Pass,否則調(diào)用索引指定的Pass抽碌。
第三個(gè)重載:Graphics.Blit(原圖悍赢,目標(biāo)决瞳,材質(zhì),指定PASS索引)左权,第四個(gè)參數(shù)是指定shader中的第幾個(gè)Pass進(jìn)行處理皮胡,索引是0開始。如:0 就是第一個(gè)Pass赏迟。 即Shader當(dāng)前起作用的SubShader的第一個(gè)Pass進(jìn)行渲染屡贺,其他Pass都不會(huì)執(zhí)行!所以這就是可以控制每一個(gè)Pass的執(zhí)行順序锌杀;而如果這個(gè)值為-1甩栈,則會(huì)按順序執(zhí)行所有Pass。

默認(rèn)情況下糕再,OnRenderImage 函數(shù)會(huì)在所有不透明和透明Pass執(zhí)行完后被調(diào)用量没,若想在不透明Pass執(zhí)行完后調(diào)用,即不對(duì)透明物體產(chǎn)生影響突想,可以在OnRenderImage函數(shù)前添加ImageEffectOpaque的屬性實(shí)現(xiàn)殴蹄。

Unity中實(shí)現(xiàn)屏幕后處理出效果,通常步驟:
1.在攝像機(jī)添加屏幕后處理腳本猾担,該腳本中會(huì)實(shí)現(xiàn)OnRenderImage函數(shù)獲取當(dāng)前屏幕渲染紋袭灯。
2.調(diào)用Graphics.Blit函數(shù)使用特定Shader對(duì)當(dāng)前圖像進(jìn)行處理,再將最終目標(biāo)紋理渲染到屏幕上垒探。對(duì)于復(fù)雜的后處理特效妓蛮,需要多次調(diào)用Graphics.Blit函數(shù)

1.2.具體做法

在進(jìn)行屏幕后處理前,需要檢查是否滿足后處理?xiàng)l件圾叼,創(chuàng)建一個(gè)用于屏幕后處理效果的基類蛤克,在實(shí)現(xiàn)屏幕后處理效果時(shí),只需繼承該基類夷蚊,再實(shí)現(xiàn)派生類中的具體操作构挤,完整代碼:

//希望在編輯器狀態(tài)下也可以執(zhí)行該腳本來查看效果  
[ExecuteInEditMode]
//所有的屏幕后處理效果都需要綁定在某個(gè)攝像機(jī)上  
[RequireComponent(typeof(Camera)]
public class PostEffectsBase : MonoBehaviour {
protected void CheckResources() {
    bool isSupported = CheckSupport();
    if (isSupported == false) {
        NotSupport();
    }
}
 
protected bool CheckSupport() {
    if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) {
        return false;
    }
    return true;
}
 
protected void NotSupport() {
    enabled = false;
}
// Use this for initialization
void Start () {
    //檢查資源和條件是否支持屏幕后處理
    CheckResources();  
}
 
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
    if (shader == null) {
        return null;
    }
    if (shader.isSupported && material && material.shader == shader) {
        return material;
    }
    if (!shader.isSupported)
    {
        return null;
    }
    else {
        material = new Material(shader);
        material.hideFlags = HideFlags.DontSave;
        if (material)
            return material;
        else
            return null;
    }
}

二.調(diào)整亮度、飽和度和對(duì)比度

新建一個(gè)腳本惕鼓,名為BrightnessSaturationAndContrast.cs筋现。添加到攝像機(jī)上。

using UnityEngine;
using System.Collections;

public class BrightnessSaturationAndContrast : PostEffectsBase {

    public Shader briSatConShader;
    private Material briSatConMaterial;
    public Material material {  
        get {
            briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
            return briSatConMaterial;
        }  
    }

    [Range(0.0f, 3.0f)]
    public float brightness = 1.0f;

    [Range(0.0f, 3.0f)]
    public float saturation = 1.0f;

    [Range(0.0f, 3.0f)]
    public float contrast = 1.0f;

    void OnRenderImage(RenderTexture src, RenderTexture dest) {
        if (material != null) {
            material.SetFloat("_Brightness", brightness);
            material.SetFloat("_Saturation", saturation);
            material.SetFloat("_Contrast", contrast);
            //使用這個(gè)函數(shù)進(jìn)行將source傳入material的_MainTex(必須保證有這個(gè)字段)紋理進(jìn)行用material的著色器渲染
            //注意:它會(huì)執(zhí)行所有Pass箱歧,進(jìn)行渲染矾飞,相當(dāng)于第四個(gè)參數(shù)為-1
            Graphics.Blit(src, dest, material);//第三個(gè)參數(shù)是material材質(zhì),還用它身上的著色器shader進(jìn)行渲染
        } else {
           //否則呀邢,只是將原色source圖像輸出到屏幕,實(shí)際上是啥都沒干洒沦。因?yàn)闆]有著色器傳入(第三個(gè)參數(shù)material身上的著色器)
            Graphics.Blit(src, dest);
        }
    }
}

繼承自屏幕處理基類PostEffectsBase,指定shader价淌,并根據(jù)該shader創(chuàng)建新的材質(zhì)申眼,通過OnRenderImage方法和Graphics.Blit方法將參數(shù)傳遞到shader中瞒津,完成著色,shader代碼:

Shader "Custom/Chapter12_BrightnessSaturateAndContrast" {
Properties{
    _MainTex("Maintex",2D)="white"{}
    _Brightness("Brightness",Float)=1
    _Saturation("Saturation",Float)=1
    _Contrast("Contrast",Float)=1
 
    //Graphics.Blit(src,dest,material)會(huì)將第一個(gè)參數(shù)傳遞給Shader中名為_MainTex的屬性
}
SubShader{
    Pass{
        ZTest Always
        Cull Off
        ZWrite Off
 
        CGPROGRAM
        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"
 
        sampler2D _MainTex;
        float4 _MainTex_ST;
        half _Brightness;
        half _Saturation;
        half _Contrast;
 
        struct v2f{
            float4 pos:SV_POSITION;
            float2 uv:TEXCOORD0;
        };
 
        //appdata_img為Unity內(nèi)置的結(jié)構(gòu)體括尸,只包含圖像處理必須的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)
        v2f vert(appdata_img v){
            v2f o;
            o.pos=UnityObjectToClipPos(v.vertex);
            o.uv=v.texcoord; //屏幕后處理得到的紋理和要輸出的紋理坐標(biāo)是相同的
 
            return o;
        }

        //屏幕特效使用的頂點(diǎn)著色器代碼通常比較簡單巷蚪,我們只需要進(jìn)行必須的頂點(diǎn)變換  
        //更重要的是,我們需要把正確的紋理坐標(biāo)傳遞給片元著色器濒翻,以便對(duì)屏幕圖像進(jìn)行正確的采樣  
        //使用了內(nèi)置appdata_img 結(jié)構(gòu)體作為頂點(diǎn)著色器的輸入  
        //可以在 UnityCGxginc 中找到該結(jié)構(gòu)體的聲明屁柏, 它只包含了圖像處理時(shí)必需的頂點(diǎn)坐標(biāo)和紋理坐標(biāo)等變量
        fixed4 frag(v2f i):SV_Target{
                //采樣
                fixed4 renderTex = tex2D(_MainTex, i.uv);
                //首先原色RGB*亮度值 就拿到了亮度值影響后的顏色
                fixed3 finalColor = renderTex.rgb * _Brightness;                
                //用一些特殊的浮點(diǎn)數(shù)進(jìn)行乘以原圖的r,g,b值再相加 得到一個(gè)特殊的值
                fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
                //用這個(gè)特殊值構(gòu)成了一個(gè)飽和度為0的顏色
                fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
                //_Saturation是飽和度,為0時(shí)則是飽和度為0的顏色值肴焊,否則越接近上面處理后的顏色(亮度影響后的)
                finalColor = lerp(luminanceColor, finalColor, _Saturation);
 
                //對(duì)比度為0的顏色值
                fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
                //_Contrast為0時(shí)前联,拿到的是對(duì)比度為0的顏色值,否則越接近上面處理后的顏色(亮度影響后+飽和度影響后的)
                finalColor = lerp(avgColor, finalColor, _Contrast);
                
                //輸出,A通道采用原圖A通道值
                return fixed4(finalColor, renderTex.a);
        }
        ENDCG
    }
}
FallBack Off
} 

Graphics.Blit(src,dest,material)會(huì)將第一個(gè)參數(shù)傳遞給Shader中名為_MainTex的屬性,然后用這個(gè)shader進(jìn)行渲染娶眷。


BrightnessSaturationAndContrast.cs掛在相機(jī)上

調(diào)整參數(shù)的效果

三.邊緣檢測(cè)

屏幕后處理中的邊緣檢測(cè)是利用一些邊緣檢測(cè)算子對(duì)圖像中的像素進(jìn)行卷積操作似嗤。

3.1.卷積概念

在圖像處理中,卷積操作指的就是使用一個(gè)卷積和對(duì)一張圖像中的每個(gè)像素進(jìn)行一系列操作届宠。卷積核通常是一個(gè)四方形網(wǎng)格結(jié)構(gòu)烁落,例如2x2,3x3的方形區(qū)域豌注,該區(qū)域內(nèi)每個(gè)網(wǎng)格都有一個(gè)權(quán)重值伤塌。當(dāng)對(duì)圖像中的某個(gè)像素進(jìn)行卷積時(shí),我們會(huì)把卷積核的中心放置于該像素上轧铁,如下圖所示每聪。


翻轉(zhuǎn)核之后再一次計(jì)算核中的每個(gè)元素和覆蓋的圖像像素值的乘積并求和,得到的結(jié)果就是該位置的新像素值

具體關(guān)于圖像處理中的卷積計(jì)算齿风,可以參考這篇博客(http://www.cnblogs.com/freeblues/p/5738987.html
這樣的計(jì)算過程雖然簡單药薯,但可以實(shí)現(xiàn)很多常見的圖像處理效果,例如圖像模糊救斑、邊緣檢測(cè)等童本。

3.2.邊緣檢測(cè)算子

卷積操作的神奇指出 在于選擇的卷積核,用于邊緣檢測(cè)的卷積核(邊緣檢測(cè)算子)是什么脸候? 首先想一下 如果相鄰像素之間存在差別明顯的顏色穷娱,亮度等屬性,那么他們之間應(yīng)該有一條邊界运沦。這種相鄰像素之間的差值可以用梯度來表示泵额,邊緣處的梯度絕對(duì)值比較大,所以携添,就出現(xiàn)下面幾種邊緣檢測(cè)算子:



邊緣檢測(cè)算子包含兩個(gè)方向的卷積核梯刚,用來計(jì)算水平方向和豎直方向的梯度值,得到兩個(gè)方向的梯度值薪寓,而整體的梯度值可以是兩個(gè)方向上的梯度值平方和開根亡资,為了節(jié)約性能可以是兩個(gè)方向梯度值的絕對(duì)值求和,整體梯度的值越大向叉,說明該像素點(diǎn)則越有可能是邊緣位置锥腻。

關(guān)于相關(guān)算子的介紹,可以查看下邊的連接
https://wenku.baidu.com/view/abe192dc28ea81c758f5786b.html?from=search

3.3.實(shí)現(xiàn)

使用Sobel算子進(jìn)行邊緣檢測(cè)母谎,實(shí)現(xiàn)描邊效果瘦黑。
新建一個(gè)腳本,名為EdgeDetectiont.cs奇唤。添加到攝像機(jī)上幸斥。

using UnityEngine;
using System.Collections;

public class EdgeDetection : PostEffectsBase {

    public Shader edgeDetectShader;
    private Material edgeDetectMaterial = null;
    public Material material {  
        get {
            edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
            return edgeDetectMaterial;
        }  
    }

    [Range(0.0f, 1.0f)]
    public float edgesOnly = 0.0f;//當(dāng)edgesOnly為0時(shí),邊緣將會(huì)疊加在原渲染圖像上咬扇,
//為1時(shí)甲葬,則會(huì)只顯示邊緣,不顯示原渲染圖像
    public Color edgeColor = Color.black;//邊緣顏色
    
    public Color backgroundColor = Color.white;//背景顏色

    void OnRenderImage (RenderTexture src, RenderTexture dest) {
        if (material != null) {
            material.SetFloat("_EdgeOnly", edgesOnly);
            material.SetColor("_EdgeColor", edgeColor);
            material.SetColor("_BackgroundColor", backgroundColor);

            Graphics.Blit(src, dest, material);
        } else {
            Graphics.Blit(src, dest);
        }
    }
}

新建一個(gè)Unity Shader懈贺。

Shader "Unlit/Chapter12-MyEdgeDetection"
{
    Properties {  
        _MainTex ("Base (RGB)", 2D) = "white" {}  
        _EdgeOnly ("Edge Only", Float) = 1.0  
        _EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)  
        _BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)  
    }  
    SubShader {  
        Pass {    
            ZTest Always Cull Off ZWrite Off  

            CGPROGRAM  

            #include "UnityCG.cginc"  

            #pragma vertex vert    
            #pragma fragment fragSobel  

            sampler2D _MainTex;  
            //xxx_TexelSize 是Unity為我們提供訪問xxx紋理對(duì)應(yīng)的每個(gè)紋素的大小经窖。  
            //例如一張512×512的紋理,該值大小為0.001953(即1/512)梭灿。由于卷積需要對(duì)相鄰區(qū)域內(nèi)的紋理  
            //進(jìn)行采樣画侣,因此我們需要它來計(jì)算相鄰區(qū)域的紋理坐標(biāo)  
            uniform half4 _MainTex_TexelSize;  
            fixed _EdgeOnly;  
            fixed4 _EdgeColor;  
            fixed4 _BackgroundColor;  

            struct v2f {  
                float4 pos : SV_POSITION;  
                half2 uv[9] : TEXCOORD0;  
            };  

            v2f vert(appdata_img v) {  
                v2f o;  
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  

                half2 uv = v.texcoord;  
                //我們?cè)趘2f結(jié)構(gòu)體中定義了一個(gè)維數(shù)為9的紋理數(shù)組,對(duì)應(yīng)了使用Sobel算子采樣時(shí)需要的9個(gè)  
                //鄰域紋理坐標(biāo)堡妒。通過把計(jì)算采樣紋理坐標(biāo)的代碼從片元著色器轉(zhuǎn)移到頂點(diǎn)著色器中配乱,可以減少  
                //運(yùn)算,提供性能。由于從頂點(diǎn)著色器到片元著色器的插值是線性的艰垂,因此這樣的轉(zhuǎn)移不會(huì)影響  
                //紋理坐標(biāo)的計(jì)算結(jié)果幌蚊。  
                o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);  
                o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);  
                o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);  
                o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);  
                o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);  
                o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);  
                o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);  
                o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);  
                o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);  

                return o;  
            }  

            fixed luminance(fixed4 color) {  
                return  0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;   
            }  

            //利用Sobel算子計(jì)算梯度值  
            half Sobel(v2f i) {  
                //水平方向卷積核  
                const half Gx[9] = {-1,  0,  1,  
                                    -2,  0,  2,  
                                    -1,  0,  1};  
                //豎直方向卷積核  
                const half Gy[9] = {-1, -2, -1,  
                                     0,  0,  0,  
                                     1,  2,  1};       

                half texColor;  
                half edgeX = 0;  
                half edgeY = 0;  
                for (int it = 0; it < 9; it++) {  
                    //采樣,得到亮度值  
                    texColor = luminance(tex2D(_MainTex, i.uv[it]));  
                    //水平方向上梯度  
                    edgeX += texColor * Gx[it];  
                    //豎直方向上梯度  
                    edgeY += texColor * Gy[it];  
                }  
                //edge 越小佑钾,表面該位置越可能是一個(gè)邊緣點(diǎn)。  
                half edge = 1 - abs(edgeX) - abs(edgeY);  

                return edge;  
            }  

            fixed4 fragSobel(v2f i) : SV_Target {  
               half edge = Sobel(i);
                //如果有看Sobel解釋的同學(xué)烦粒,可以看看這里的注釋)
                //1. 為什么 1 - 總體梯度值呢休溶?  因?yàn)楫?dāng) 當(dāng)前顏色 和 周圍相差越大時(shí),梯度越大扰她, 此時(shí)edge 越小兽掰,lerp插值得到的是邊緣顏色!
                // 如果徒役,你將lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);改為 lerp(tex2D(_MainTex, i.uv[4], _EdgeColor), edge);
                // 此時(shí)孽尽,edge 就等于 梯度值, 梯度值越大忧勿,說明當(dāng)前像素點(diǎn)顏色紙和周圍顏色相差越大杉女,說明是邊緣瞻讽!因此顯示出邊緣顏色!!
                // i.uv[4]是當(dāng)前像素點(diǎn)紋理坐標(biāo)
 
                //withEdgeColor:當(dāng)越接近邊緣時(shí),輸出的是邊緣顏色熏挎,否則輸出的是原圖顏色速勇,中間是這2個(gè)顏色的插值結(jié)果,以離邊緣的遠(yuǎn)近程度進(jìn)行插值運(yùn)算
                fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
                //onlyEdgeColor:當(dāng)離邊緣越近時(shí)坎拐,輸出邊緣顏色烦磁,否則輸出背景純色顏色,中間是這2個(gè)顏色的插值結(jié)果哼勇,都伪,以離邊緣的遠(yuǎn)近程度進(jìn)行插值運(yùn)算
                fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
                //將上方2個(gè)顏色進(jìn)行插值_EdgeOnly,當(dāng)為1時(shí),則只顯示出邊緣顏色+背景純色顏色(注意背景是自定義的純色圖积担,而不是屏幕背景哦)
                //否則為0時(shí)陨晶,則是顯示出邊緣+原圖
                return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
            }  

            ENDCG  
        }   
    }  
    FallBack Off  
}  

_EdgeOnly從0~1,邊緣顏色黑色磅轻,背景顏色白色

邊緣黃色

四.高斯模糊

模糊實(shí)現(xiàn)的方式有多種珍逸,有均值模糊,中值模糊聋溜。

從數(shù)字信號(hào)處理的角度看谆膳,圖像模糊的本質(zhì)一個(gè)過濾高頻信號(hào),保留低頻信號(hào)的過程撮躁。過濾高頻的信號(hào)的一個(gè)常見可選方法是卷積濾波漱病。從這個(gè)角度來說,圖像的高斯模糊過程即圖像與正態(tài)分布做卷積把曼。由于正態(tài)分布又叫作“高斯分布”杨帽,所以這項(xiàng)技術(shù)就叫作高斯模糊。而由于高斯函數(shù)的傅立葉變換是另外一個(gè)高斯函數(shù)嗤军,所以高斯模糊對(duì)于圖像來說就是一個(gè)低通濾波器注盈。

用于高斯模糊的高斯核(Gaussian Kernel)是一個(gè)正方形的像素陣列,其中像素值對(duì)應(yīng)于2D高斯曲線的值叙赚。
一個(gè)典型的高斯核

圖像中的每個(gè)像素被乘以高斯核老客,然后將所有這些值相加,得到輸出圖像中此處的值震叮。
N維空間高斯模糊方程可以表示為:
N維空間高斯模糊方程

二維空間

其中σ 是標(biāo)準(zhǔn)方差(一般取值為1)胧砰,x和y分別對(duì)應(yīng)了當(dāng)前位置到卷積核中心的整數(shù)距離。

要構(gòu)建一個(gè)高斯核苇瓣,只需要計(jì)算高斯核中各個(gè)位置對(duì)應(yīng)的高斯值尉间。為了保證濾波后的圖像不會(huì)變暗,需要對(duì)高斯核中的權(quán)重進(jìn)行歸一化,即讓每個(gè)權(quán)重除以所有權(quán)重的和哲嘲,這樣可以保證所有權(quán)重的和為1贪薪。因此,高斯函數(shù)中e的前面的系數(shù)實(shí)際不會(huì)對(duì)結(jié)果又任何影響眠副。

高斯核的維數(shù)越高古掏,處理后的圖像模糊程度會(huì)越高。當(dāng)使用一個(gè)NxN的高斯核對(duì)一個(gè)WxH的圖像進(jìn)行處理侦啸,那么對(duì)像素值的采樣結(jié)果為NxNxWxH次,N越大采樣的次數(shù)就越大丧枪。二維的高斯核可以拆成兩個(gè)一維的高斯核光涂,得到的結(jié)果和直接使用二維高斯核的結(jié)果是一樣的,這樣可以使采樣次數(shù)降低到2xNxWxH次拧烦。而兩個(gè)一維的高斯核中權(quán)重值有重復(fù)的權(quán)重值忘闻,例如一個(gè)5x5的一維高斯核只需要記錄三個(gè)權(quán)重值。


我們將會(huì)使用上述5×5的高斯核對(duì)原圖像進(jìn)行高斯模糊恋博。我們將先后調(diào)用兩個(gè)Pass齐佳,第一個(gè)Pass將會(huì)使用豎直方向的一維高斯核對(duì)圖像進(jìn)行濾波,第二個(gè)Pass再使用水平方向的一維高斯核對(duì)圖像進(jìn)行濾波债沮,得到最紅的目標(biāo)圖像炼吴。在實(shí)現(xiàn)中,還將利用圖像縮放來進(jìn)一步提高性能疫衩,并通過調(diào)整高斯濾波的應(yīng)用次數(shù)來控制模糊程度硅蹦。

新建一個(gè)腳本,名為GaussianBlur.cs闷煤。添加到攝像機(jī)上童芹。

public class GaussianBlur : PostEffectsBase{  
    public Shader gaussianBlurShader;  
    private Material gaussianBlurMaterial;  
    public Material material{  
        get{  
            gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader,gaussianBlurMaterial);  
            return gaussianBlurMaterial;  
        }  
    }  

    //迭代次數(shù)  
    [Range(0,4)]  
    public int iterations = 3;  
    //模糊范圍  
    [Range(0.2f,3.0f)]  
    public float blurSpread = 0.6f;  
    //縮放系數(shù)  
    [Range(1, 8)]  
    public int downSample = 2;  

    //第一個(gè)版本,最簡單的處理
    /*void OnRenderImage(RenderTexture src,RenderTexture dest){  
        if(material == null){  
            int rtW = src.width;  
            int rtH = src.height;  
            //分配一個(gè)緩沖區(qū)  
            RenderTexture buffer = RenderTexture.GetTemporary(rtW,rtH,0);  
            Graphics.Blit(src,buffer,material,0);  
            Graphics.Blit(buffer,dest,material,1);  
            RenderTexture.ReleaseTemporary(buffer);  
        }  
        else{  
            Graphics.Blit(src,dest);  
        }  
    }  */

    //第二個(gè)版本 鲤拿,增加降采樣的處理
    /*void OnRenderImage(RenderTexture src,RenderTexture dest){  
        if(material == null){  
            //使用了小于原屏幕分辨率的尺寸  
            int rtW = src.width/downSample;  
            int rtH = src.height/downSample;  
            //分配一個(gè)緩沖區(qū)  
            RenderTexture buffer = RenderTexture.GetTemporary(rtW,rtH,0);  
            //臨時(shí)渲染紋理的濾波模式設(shè)置為雙線性  
            buffer.filterMode = FilterMode.Bilinear;  
            Graphics.Blit(src,buffer,material,0);  
            Graphics.Blit(buffer,dest,material,1);  
            RenderTexture.ReleaseTemporary(buffer);  
        }  
        else{  
            Graphics.Blit(src,dest);  
        }  
    }  */

    //第三個(gè)版本假褪,增加降采樣處理及迭代的影響
    void OnRenderImage(RenderTexture src,RenderTexture dest){  
        if(material == null){  
            //使用了小于原屏幕分辨率的尺寸  
            int rtW = src.width/downSample;  
            int rtH = src.height/downSample;  
            //分配一個(gè)緩沖區(qū)  
            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0);  
            //臨時(shí)渲染紋理的濾波模式設(shè)置為雙線性  
            buffer.filterMode = FilterMode.Bilinear;  
            Graphics.Blit(src,buffer0);  
            //進(jìn)行迭代模糊  
            for(int i=0;i<iterations;i++){  
                material.SetFloat("_BlurSoze",1.0f+i*blurSpread);  
                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);  
                Graphics.Blit(buffer0,buffer1,material,0);  
                RenderTexture.ReleaseTemporary(buffer0);  

                buffer0 = buffer1;  
                buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);  
                Graphics.Blit(buffer0,buffer1,material,1);  
                RenderTexture.ReleaseTemporary(buffer0);  
                buffer0 = buffer1;  
            }  
            Graphics.Blit(buffer0,dest);  
            RenderTexture.ReleaseTemporary(buffer0);  
        }  
        else{  
            Graphics.Blit(src,dest);  
        }  
    }  
}  

新建一個(gè)Unity Shader。

Shader "Unlit/Chapter12-MyGaussianBlur"
{
     Properties {  
        _MainTex ("Base (RGB)", 2D) = "white" {}  
        _BlurSize ("Blur Size", Float) = 1.0  
    }  
    SubShader {  
        //在SubShader 塊中利用CGINCLUDE 和 ENDCG 來定義一系列代碼  
        //這些代碼不需要包含在Pass語義塊中近顷,在使用時(shí)生音,我們只需要在Pass中指定需要  
        //使用的頂點(diǎn)著色器和片元著色器函數(shù)名即可。  
        //使用CGINCLUDE 來管理代碼 可以避免我們編寫兩個(gè)完全一樣的frag函數(shù)  
        //這里相當(dāng)于只是定義 執(zhí)行還是在下邊的Pass中
        //使用時(shí)幕庐,在Pass中直接指定需要使用的著色器函數(shù)久锥,避免編寫完一樣的片元著色器函數(shù)
        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"  
}  

實(shí)例效果:
原圖
模糊后的效果

五.運(yùn)動(dòng)模糊

運(yùn)動(dòng)模糊是真實(shí)世界中的攝像機(jī)的一種效果。如果攝像機(jī)在曝光時(shí)异剥,場(chǎng)景發(fā)生變化瑟由,就會(huì)產(chǎn)生模糊的畫面。計(jì)算機(jī)產(chǎn)生的圖像由于不存在曝光,渲染出來的圖像往往是線條清晰歹苦,缺少運(yùn)動(dòng)模糊青伤。
運(yùn)動(dòng)模糊的實(shí)現(xiàn)有多種方式
1.利用積累緩存混合多張連續(xù)圖片
當(dāng)物體移動(dòng)產(chǎn)生多張圖片后,取這些圖片的平均值作為最后的運(yùn)動(dòng)模糊圖像殴瘦。這種方式對(duì)性能消耗有較大影響狠角,獲取多張幀圖像需要在同一幀內(nèi)多次進(jìn)行場(chǎng)景渲染。
2.使用速度緩存
這個(gè)緩存中存儲(chǔ)各個(gè)像素的運(yùn)動(dòng)速度蚪腋,使用該值決定模糊的方向和大小丰歌。

這里使用類似上述第一種方法的實(shí)現(xiàn)來模擬運(yùn)動(dòng)模糊的效果。我們不需要再一幀中把場(chǎng)景渲染多次屉凯,但需要保存之前的渲染結(jié)果立帖,不斷把當(dāng)前的渲染圖像疊加到之前的渲染圖像中,從而產(chǎn)生一種運(yùn)動(dòng)軌跡的視覺效果悠砚。這種方法與原始的利用累計(jì)緩存的方法相比性能更好晓勇,但模糊效果可能會(huì)略有影響。
實(shí)例代碼:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class MotionBlur : PostEffectsBase{
    public Shader motionBlurShader;
    private Material motionBlurMaterial = null;
    public Material material
    {
        get
        {
            motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
            return motionBlurMaterial;
        }
    }
 
    [Range(0f, 0.9f)]
    public float blurAmount = 0.5f;
    //blurAmount 的值越大灌旧, 運(yùn)動(dòng)拖尾的效果就越明顯绑咱, 為了防止拖尾效果完全替代當(dāng)前幀的渲染
    //結(jié)果, 我們把它的值截取在 0.0-0.9 范圍內(nèi)枢泰。  
    private RenderTexture accumulationTexture;
 
    private void OnDisable()
    {
        DestroyImmediate(accumulationTexture);//銷毀臨時(shí)紋理
    }
 
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (material != null)
        {
            //確保臨時(shí)渲染紋理得大小是和屏幕一樣得,注意:這個(gè)臨時(shí)渲染紋理是不需要清空的描融,只有在禁用掉組件時(shí)才會(huì)銷毀
            if(accumulationTexture == null || accumulationTexture.width != source.width || 
                accumulationTexture.height != source.height)
            {
                DestroyImmediate(accumulationTexture);
                accumulationTexture = new RenderTexture(source.width, source.height, 0);
                //不保存和不顯示在Hierarchy中
                accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
                //將屏幕圖像渲染到臨時(shí)紋理上
                Graphics.Blit(source, accumulationTexture);
            }
            //進(jìn)行一個(gè)渲染紋理的恢復(fù)操作:恢復(fù)操作發(fā)生在渲染到紋理而該紋理又沒有被提前清空或銷毀的前提下!這個(gè)操作就是我在shader注釋所說的
            //進(jìn)行一個(gè)恢復(fù)操作,不然上一幀的顏色會(huì)一直逗留在屏幕中 你可以試試注釋掉行代碼
            accumulationTexture.MarkRestoreExpected();
 
            //當(dāng)blurAmount越大 傳入的值越小宗苍,運(yùn)動(dòng)模糊效果越顯著稼稿,因?yàn)樯弦粠念伾煌耆A粝聛砹?
            //具體看shader代碼有詳細(xì)解釋。讳窟。让歼。
            material.SetFloat("_BlurAmount", 1.0f - blurAmount);
 
            //將渲染的結(jié)果疊加到臨時(shí)紋理,注意丽啡,還沒疊加之前臨時(shí)紋理還保留著上一幀的渲染結(jié)果谋右,然后疊加是指Shader的一個(gè)透明度混合操作進(jìn)行的
            //shader代碼用了2個(gè)PASS,第一個(gè)PASS是只混合RGB通道补箍,A通道不會(huì)進(jìn)行寫入到緩沖區(qū)改执,第二個(gè)PASS是將原始(最開始的)那個(gè)A通道,寫入到緩沖區(qū)
            Graphics.Blit(source, accumulationTexture, material);
            //臨時(shí)紋理輸出到屏幕
            Graphics.Blit(accumulationTexture, destination);
        }
        else
        {
            Graphics.Blit(source, destination);
        }
    }
}

新建一個(gè)Unity Shader坑雅。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
 
Shader "MilkShader/Twently/M_MotionBlur"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _BlurAmount ("Blur Amount", Float) = 1.0
    }
    SubShader
    {
        CGINCLUDE
        #include "UnityCG.cginc"
 
        sampler2D _MainTex;
        fixed _BlurAmount;
 
        struct v2f{
            float4 pos : SV_POSITION;
            half2 uv : TEXCOORD0;
        };
 
        v2f vert(appdata_img v){
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord;
            return o;
        }
        fixed4 fragRGB(v2f i) : SV_Target{
            //使用_BlurAmount作為這個(gè)片元的輸出顏色A通道,在第一個(gè)PASS中 我們開啟了混合,_BlurAmount就是作為混合因子的
            //即 SrcAlpha 為 _BlurAmount
            // OneMinusSrcAlpha = 1 - _BlurAmount
            // outcolor = sourcecolor * SrcAlpha + destiColor * OneMinusSrcAlpha = sourcecolo * _BlurAmount + destiColor * (1-_BlurAmount)
            //當(dāng)_BlurAmount越小時(shí)辈挂,完全保留顏色緩沖區(qū)的顏色值,也就是上一幀(A幀)的屏幕顏色被完全保留下來裹粤,當(dāng)然幀(B幀)的顏色會(huì)在下一幀進(jìn)行輸出到屏幕终蒂。
            //在下一幀(C幀)時(shí)的時(shí),UNITY會(huì)清空當(dāng)前幀的上一幀(A幀)的屏幕顏色值或衰減顏色值等處理
            //所以_BlurAmount越小時(shí),越能感覺到運(yùn)動(dòng)模糊效果拇泣,否則上一幀的顏色紙得不到保留噪叙,那就完全沒有運(yùn)動(dòng)模糊效果
            return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
        }
 
        half4 fragA(v2f i): SV_Target {
            return tex2D(_MainTex, i.uv);
        }
        ENDCG
 
        ZTest Always Cull Off ZWrite Off
        //這里將RGB和A通道分開,是由于在做混合時(shí)霉翔,按照_BlurAmount參數(shù)值將源 圖像和目標(biāo)圖像進(jìn)行混合
        //而同時(shí)不讓其紋理受到A通道值的影響睁蕾,只是用來做混合,不改變其透明度
        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            //只輸出RGB到屏幕上
            ColorMask RGB
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragRGB
            ENDCG
        }
        Pass
        {
            Blend One Zero
            //只輸出A到屏幕上
            ColorMask A
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragA
            ENDCG
        }
    }
    Fallback Off
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末债朵,一起剝皮案震驚了整個(gè)濱河市子眶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌序芦,老刑警劉巖壹店,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異芝加,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)射窒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門藏杖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脉顿,你說我怎么就攤上這事蝌麸。” “怎么了艾疟?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵来吩,是天一觀的道長。 經(jīng)常有香客問我蔽莱,道長弟疆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任盗冷,我火速辦了婚禮怠苔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仪糖。我一直安慰自己柑司,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布锅劝。 她就那樣靜靜地躺著攒驰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪故爵。 梳的紋絲不亂的頭發(fā)上玻粪,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼奶段。 笑死饥瓷,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的痹籍。 我是一名探鬼主播呢铆,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蹲缠!你這毒婦竟也來了棺克?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤线定,失蹤者是張志新(化名)和其女友劉穎娜谊,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體斤讥,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纱皆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了芭商。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片派草。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖铛楣,靈堂內(nèi)的尸體忽然破棺而出近迁,到底是詐尸還是另有隱情,我是刑警寧澤簸州,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布鉴竭,位于F島的核電站,受9級(jí)特大地震影響岸浑,放射性物質(zhì)發(fā)生泄漏搏存。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一矢洲、第九天 我趴在偏房一處隱蔽的房頂上張望祭埂。 院中可真熱鬧,春花似錦兵钮、人聲如沸蛆橡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泰演。三九已至,卻和暖如春葱轩,著一層夾襖步出監(jiān)牢的瞬間睦焕,已是汗流浹背藐握。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留垃喊,地道東北人猾普。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像本谜,于是被迫代替她去往敵國和親初家。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容