Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果

Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果

本系列为UnityShader入门精要读书笔记总结,
原作者博客链接:http://blog.csdn.net/candycat1992/article/
书籍链接:http://product.dangdang.com/23972910.html

第12章 屏幕后处理效果

屏幕后处理效果(screen post-processing effects ) 是游戏中实现屏幕特效的常见方法。 在本
章中, 我们将学习如何在 Unity 中利用渲染纹理来实现各种常见的屏幕后处理效果。我们首先会解释在 Unity 中实现屏幕后处理效果的原理, 并建立一个基本的屏幕后处理脚本系统。随后 我们会使用这个系统实现一个简单的调整画面亮度、 饱和度和对比度的屏幕特效。 接下来, 我们会接触到图像滤波的概念, 并利用 Sobel 算子在屏幕空间中对图像进行边缘检测, 实现描边效果。 在此基础上,将会介绍如何实现一个高斯模糊的屏幕特效。后期我们会分别介绍如何实现 Bloom 和运动模糊效果。

12.1 建立一个基本的屏幕后处理脚本系统

屏幕后处理, 顾名思义, 通常指的是在渲染完整个场景得到屏幕图像后, 再对这个图像进行一系列操作, 实现各种屏幕特效。 使用这种技术, 可以为游戏画面添加更多的艺术效果, 例如景深( Depth of Field)、 运动模糊( Motion Blur) 等。
因此,想要实现屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,而Unity为我们提供了这样一个方便的接口OnRenderImage函数。它的函数声明如下:

MonoBehaviour.OnRenderImage(RenderTexture src,RenderTexture dest)  

当我们再脚本中声明此函数后,Unity会把当前渲染得到的图像存储在第一个参数对应的源渲染纹理中,通过函数中的一系列操作后,再把目标渲染纹理,即第二个参数对应的渲染纹理显示到屏幕上。

在默认的情况下,OnRenderImage 函数会在所有的不透明和透明的Pass执行完毕后被调用,以便对场景中所有的游戏对象都产生影响。但有时,我们希望在不透明的Pass(即渲染队列小于等于2500 的Pass,内置的Background、Geometry 和 AlphaTest渲染队列均在此范围内)执行完毕后立即调用OnRenderImage 函数,从而不对透明物体产生任何影响。此时,我们可以在OnRenderImage 函数前添加ImageEffectOpaque 属性来实现这样的目的。

因此,要在Unity 中实现屏幕后处理效果,过程通常如下:
我们首先需要再摄像机中添加一个用于屏幕后处理的脚本。在这个脚本中,我们会实现OnRenderImage函数来获取当前屏幕的渲染纹理。
然后,再调用Graphics.Blit 函数使用特定的Unity Shader 来对当前图像进行处理,再把返回的渲染纹理显示到屏幕上。对于一些复杂的屏幕特效,我们可能需要多次调用Graphics.Blit 函数来对上一步的输出结果进行下一步处理。

但是,在进行屏幕后处理之前,我们需要检查一系列条件是否满足,例如当前平台是否支持渲染纹理和屏幕特效,是否支持当前使用的Unity Shader等,为此,我们创建了一个用于屏幕后处理效果的基类,在实现各种屏幕特效时,我们只需要继承自该基类,再实现派生类中不同的操作即可。
基类PostEffectBase.cs 的代码如下

//希望在编辑器状态下也可以执行该脚本来查看效果  
[ExecuteInEditMode]  
//所有的屏幕后处理效果都需要绑定在某个摄像机上  
[RequireComponent(typeof(Camera))]  
public class PostEffectsBase : MonoBehaviour {  

    protected void CheckResource(){  
        bool isSupported = CheckSupport();  
        if(isSupported == false){  
            NotSupported();  
        }  
    }  

    protected bool CheckSupport(){  
        if(SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false){  
            Debug.LogWarning("This platform does not support image effects or render textures.");  
            return false;  
        }  
        return true;  
    }  

    protected void NotSupported(){  
        enabled = false;  
    }  

    protected void Start(){  
        CheckResource();  
    }  

    //第一个参数指定了该特效需要使用的Shader,第二个参数则是用于后期处理的材质  
    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;  
        }  
        material = new Material(shader);  
        material.hideFlags = HideFlags.DontSave;  
        return material;  
    }  
}  

12.2 调整屏幕的亮度、饱和度和对比度

在上面,我们了解了实现屏幕后处理特效的技术原理。我们现在先来实现一个非常简单的屏幕特效——调整屏幕的亮度、饱和度和对比度。我们的效果图如下所示
Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果_第1张图片
新建一个脚本,名为BrightnessSaturationAndContrast.cs。添加到摄像机上。

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);  
            Graphics.Blit(src,dest,material);  
        }  
        else{  
            Graphics.Blit(src,dest);  
        }  
    }  
}  

然后我们再修改Shader 的代码。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/Chapter12-MyBrightnessSaturationAndContrast"
{
    Properties {  
        _MainTex ("Base (RGB)", 2D) = "white" {}  
        _Brightness ("Brightness", Float) = 1  
        _Saturation("Saturation", Float) = 1  
        _Contrast("Contrast", Float) = 1  
    }  
    SubShader {  
        Pass {    
            //屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片  
            //为了防止它对其他物体产生影响,我们需要设置相关的渲染状态。  
            //关闭深度写入,是为了防止它“挡住”在其后面被渲染的物体  
            ZTest Always Cull Off 
            ZWrite Off  

            CGPROGRAM    
            #pragma vertex vert    
            #pragma fragment frag    

            #include "UnityCG.cginc"    

            sampler2D _MainTex;    
            half _Brightness;  
            half _Saturation;  
            half _Contrast;  

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

            //屏幕特效使用的顶点着色器代码通常比较简单,我们只需要进行必须的顶点变换  
            //更重要的是,我们需要把正确的纹理坐标传递给片元着色器,以便对屏幕图像进行正确的采样  
            //使用了内置appdata_img 结构体作为顶点着色器的输入  
            //可以在 UnityCGxginc 中找到该结构体的声明, 它只包含了图像处理时必需的顶点坐标和纹理坐标等变量
            v2f vert(appdata_img v) {  
                v2f o;  
                o.pos = UnityObjectToClipPos(v.vertex);  
                o.uv = v.texcoord;  
                return o;  
            }  

            fixed4 frag(v2f i) : SV_Target {  
                fixed4 renderTex = tex2D(_MainTex, i.uv);    

                //调整亮度  
                fixed3 finalColor = renderTex.rgb * _Brightness;  

                //调整饱和度  
                fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;  
                fixed3 luminanceColor = fixed3(luminance, luminance, luminance);  
                finalColor = lerp(luminanceColor, finalColor, _Saturation);  

                //调整对比度  
                fixed3 avgColor = fixed3(0.5, 0.5, 0.5);  
                finalColor = lerp(avgColor, finalColor, _Contrast);  

                return fixed4(finalColor, renderTex.a);    
            }    

            ENDCG  
        }    
    }  

    Fallback Off  
}  

12.3 边缘检测

边缘检测是描边效果的一种实现方法。原理是利用一些边缘检测算子对图像进行卷积操作。
在图像处理中,卷积操作指的就是使用一个卷积和对一张图像中的每个像素进行一系列操作。卷积核通常是一个四方形网格结构,该区域内每个网格都有一个权重值。当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上,如下图所示,翻转核之后再一次计算核中的每个元素和覆盖的图像像素值的乘积并求和,得到的结果就是该位置的新像素值。
Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果_第2张图片
这样的计算过程虽然简单,但可以实现很多常见的图像处理效果,例如图像模糊、边缘检测等。例如,如果我们想要对图像进行均值模糊,可以使用一个3×3的卷积核,核内每个元素的值均为1/9。
卷积操作的神奇之处在于选择的卷积核。那么用于边缘检测的卷积核(也被称为边缘检测算)应该长什么样的呢?在回答这个问题之前,我们可以首先回想一下边到底是如何形成的。如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,我们就会认为它们之间应该有一条边界。这种相邻像素之间的差值可以用梯度来表示,可以想象得到,边缘处的梯度绝对值会比较大。基于这样的理解,有几种不同的边缘检测算子被先后提出来。
Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果_第3张图片
计算方式举例Sobel算子
Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果_第4张图片
关于相关算子的介绍,可以查看下边的连接
https://wenku.baidu.com/view/abe192dc28ea81c758f5786b.html?from=search

3种常见的边缘检测算子如上图所示,它们都包含了两个方向的卷积核,分别用于检测水平方向和竖直方向上的边缘信息。在进行边缘检测时,我们需要对每个像素分别进行一次卷积计算,得到两个方向上的梯度值G(x)和G(y),而整体的梯度可按下面的公式计算而得:
这里写图片描述
由于上述操作包含了开根号操作,处于性能的考虑,我们有时会使用绝对值操作来代替开根号的操作:
这里写图片描述
当得到梯度G后,我们就可以据此来判断哪些像素对应了边缘(梯度值越大,越有可能是边缘点)。、

我们使用Sobel算子进行边缘检测,实现描边效果。
新建一个脚本,名为EdgeDetectiont.cs。添加到摄像机上。

public class EdgeDetection : PostEffectsBase {  
    public Shader edgeDetectShader = null;  
    private Material edgeDetectMaterial = null;  
    public Material material{  
        get{  
            edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader,edgeDetectMaterial);  
            return edgeDetectMaterial;  
        }  
    }  
    [Range(0.0f,1.0f)]  
    //当edgesOnly为0时,边缘将会叠加在原渲染图像上,为1时,则会只显示边缘,不显示原渲染图像  
    public float edgesOnly = 0.0f;  
    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.SetFloat("_BackgroundColor",backgroundColor);  

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

新建一个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纹理对应的每个纹素的大小。  
            //例如一张512×512的纹理,该值大小为0.001953(即1/512)。由于卷积需要对相邻区域内的纹理  
            //进行采样,因此我们需要它来计算相邻区域的纹理坐标  
            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;  
                //我们在v2f结构体中定义了一个维数为9的纹理数组,对应了使用Sobel算子采样时需要的9个  
                //邻域纹理坐标。通过把计算采样纹理坐标的代码从片元着色器转移到顶点着色器中,可以减少  
                //运算,提供性能。由于从顶点着色器到片元着色器的插值是线性的,因此这样的转移不会影响  
                //纹理坐标的计算结果。  
                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算子计算梯度值  
            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 越小,表面该位置越可能是一个边缘点。  
                half edge = 1 - abs(edgeX) - abs(edgeY);  

                return edge;  
            }  

            fixed4 fragSobel(v2f i) : SV_Target {  
                half edge = Sobel(i);  

                fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);  
                fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);  
                return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);  
            }  

            ENDCG  
        }   
    }  
    FallBack Off  
}  

12.4 高斯模糊

模糊的实现由很多方法,例如均值模糊和中值模糊。均值模糊同样使用了卷积操作,它使用的卷积核中的各个元素值都相等,且相加等于1,也就是说,卷积后得到的像素值是其领域内各个像素值的平均值。而中值模糊则是选择领域内对所有像素排序后的中值替换掉原颜色。一个更高级的模糊方法是高斯模糊。我们可以得到类似下图的效果。
Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果_第5张图片
高斯模糊同样利用了卷积计算,它使用的卷积核名为高斯核。高斯核是一个正方形大小的滤波核,其中每个元素的计算都是基于下面的高斯方程:
这里写图片描述
其中σ 是标准方差(一般取值为1),x和y分别对应了当前位置到卷积核中心的整数距离。要构建一个高斯核,我们只需要计算高斯核中各个位置对应的高斯值。为了保证滤波后的图像不会变暗,我们需要对高斯核中的权重进行归一化,即让每个权重除以所有权重的和,这样可以保证所有权重的和为1。因此,高斯函数中e的前面的系数实际不会对结果又任何影响。下图显示了一个标准方差为1的5×5大小的高斯核。

高斯方程很好地模拟了邻域每个像素对当前处理像素的影响程度——距离越近,影响越大。高斯核的维数越高,模糊程度越大。使用一个N×N的高斯核对图像进行卷积滤波,就需要N×N×W×H(W和H分别是图像的宽和高)次纹理采样。当N的大小不断增加时,采样次数会变得非常巨大。幸运的是,我们可以把这个二维高斯函数拆分成两个一维函数。也就是说,我们可以使用两个一维的高斯核先后对图像进行滤波,它们得到的结果和直接使用二维高斯核是一样的,但采样次数只需要2×N×W×H.我们可以进一步观察到,两个一维高斯核中包含了很多重复的权重,对比一个大小为5的一维高斯核,我们实际只需要记录3个权重(前三个)即可。
Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果_第6张图片
我们将会使用上述5×5的高斯核对原图像进行高斯模糊。我们将先后调用两个Pass,第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass再使用水平方向的一维高斯核对图像进行滤波,得到最红的目标图像。在实现中,我们还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度。

新建一个脚本,名为GaussianBlur.cs。添加到摄像机上。

public class GaussianBlur : PostEffectsBase{  
    public Shader gaussianBlurShader;  
    private Material gaussianBlurMaterial;  
    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;  

    //第一个版本  
    void OnRenderImage(RenderTexture src,RenderTexture dest){  
        if(material == null){  
            int rtW = src.width;  
            int rtH = src.height;  
            //分配一个缓冲区  
            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);  
        }  
    }  

    //第二个版本  
    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;  
            Graphics.Blit(src,buffer,material,0);  
            Graphics.Blit(buffer,dest,material,1);  
            RenderTexture.ReleaseTemporary(buffer);  
        }  
        else{  
            Graphics.Blit(src,dest);  
        }  
    }  

    //第三个版本  
    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);  
            //临时渲染纹理的滤波模式设置为双线性  
            buffer.filterMode = FilterMode.Bilinear;  
            Graphics.Blit(src,buffer0);  
            //进行迭代模糊  
            for(int i=0;i"_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);  
        }  
    }  
}  

新建一个Unity Shader。

Shader "Unlit/Chapter12-MyGaussianBlur"
{
     Properties {  
        _MainTex ("Base (RGB)", 2D) = "white" {}  
        _BlurSize ("Blur Size", Float) = 1.0  
    }  
    SubShader {  
        //在SubShader 块中利用CGINCLUDE 和 ENDCG 来定义一系列代码  
        //这些代码不需要包含在Pass语义块中,在使用时,我们只需要在Pass中指定需要  
        //使用的顶点着色器和片元着色器函数名即可。  
        //使用CGINCLUDE 来管理代码 可以避免我们编写两个完全一样的frag函数  
        //这里相当于只是定义 执行还是在下边的Pass中
        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"  
}  

12.5 Bloom效果

Bloom特效是游戏中常见的一种屏幕效果。这种特效可以模拟真实摄像机的一种图像效果,它让画面中较亮的区域“扩散”到周围的区域中,造成一种朦胧的效果。下图给出了这样一种Bloom的效果。
我们先来实现一个基本的Bloom特效,可以得到类似下图中的效果。
Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果_第7张图片
Bloom的实现原理非常简单:我们首先根据一个阈值提取出图像中较亮的区域,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合,得到最终的效果。
新建一个脚本,名为Bloom.cs。添加到摄像机上。

public class Bloom : PostEffectsBase{  
    public Shader bloomShader;  
    private Material blooMaterial = null;  
    public  Material material{  
        get{  
            blooMaterial = CheckShaderAndCreateMaterial(bloomShader,blooMaterial);  
            return blooMaterial;  
        }  
    }  
    [Range(0,4)]  
    public int iterations = 3;  

    [Range(0.2f,3.0f)]  
    public float blurSpread = 0.6f;  

    [Range(1,8)]  
    public int downSample = 2;  

    [Range(0.0f,4.0f)]  
    public float luminanceThreshold = 0.6f;  
    //阈值大小控制  我们开启HDR让像素的范围可以超过1

    void OnRenderImage(RenderTexture src, RenderTexture dest){  
        if(material != null){  
            material.SetFloat("_LuminanceTheshold",luminanceThreshold);  
            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, material, 0);  

            //高斯模糊
            for(int i=0;i"_BlurSize",1.0f + i*blurSpread);  
                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);  

                Graphics.Blit(buffer0,buffer1,material,1);  

                RenderTexture.ReleaseTemporary(buffer0);  
                buffer0 = buffer1;  
                buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);  

                Graphics.Blit(buffer0,buffer1,material,2);  
                buffer0 = buffer1;  
            }  

            material.SetTexture("_Bloom",buffer0);  
            Graphics.Blit(src,dest,material,3);  
            RenderTexture.ReleaseTemporary(buffer0);  
        }  
        else{  
            Graphics.Blit(src,dest);  
        }  
    }  
}  

新建一个Unity Shader。

Shader "Unlit/Chapter12-MyBloom"
{
    Properties {  
        _MainTex ("Base (RGB)", 2D) = "white" {}  
        //高斯模糊后的较亮的区域  
        _Bloom ("Bloom (RGB)", 2D) = "black" {}  
        //用于提取较亮区域使用的阈值  
        _LuminanceThreshold ("Luminance Threshold", Float) = 0.5  
        //控制不同迭代之间高斯模糊的模糊区域范围  
        _BlurSize ("Blur Size", Float) = 1.0  
    }  
    SubShader {  
        CGINCLUDE  

        #include "UnityCG.cginc"  

        sampler2D _MainTex;  
        half4 _MainTex_TexelSize;  
        sampler2D _Bloom;  
        float _LuminanceThreshold;  
        float _BlurSize;  

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

        v2f vertExtractBright(appdata_img v) {  
            v2f o;  

            o.pos = mul(UNITY_MATRIX_MVP, v.vertex);  

            o.uv = v.texcoord;  

            return o;  
        }  

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

        fixed4 fragExtractBright(v2f i) : SV_Target {  
            fixed4 c = tex2D(_MainTex, i.uv);  
            fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);  

            return c * val;  
        }  

        struct v2fBloom {  
            float4 pos : SV_POSITION;   
            half4 uv : TEXCOORD0;  
        };  

        v2fBloom vertBloom(appdata_img v) {  
            v2fBloom o;  

            o.pos = mul (UNITY_MATRIX_MVP, v.vertex);  
            o.uv.xy = v.texcoord;         
            o.uv.zw = v.texcoord;  

            #if UNITY_UV_STARTS_AT_TOP            
            if (_MainTex_TexelSize.y < 0.0)  
                o.uv.w = 1.0 - o.uv.w;  
            #endif  

            return o;   
        }  

        fixed4 fragBloom(v2fBloom i) : SV_Target {  
            return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);  
        }   

        ENDCG  

        ZTest Always Cull Off ZWrite Off  

        Pass {    
            CGPROGRAM    
            #pragma vertex vertExtractBright    
            #pragma fragment fragExtractBright    

            ENDCG    
        }  
         //这两个高斯模糊的Pass注意对应你自己的路径 最后的名字改为大写  
        UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"  

        UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"  

        Pass {    
            CGPROGRAM    
            #pragma vertex vertBloom    
            #pragma fragment fragBloom    

            ENDCG    
        }  
    }  
    FallBack Off  
}  

12.6 运动模糊 

运动模糊是真实世界中的摄像机的一种效果。如果在摄像机曝光时,拍摄场景发生了变化,就会产生模糊的画面。运动模糊在我们的日常生活中是非常常见的,只要留心观察,就可以发现无论是体育报道还是各个电影里,都有运动模糊的身影。运动模糊效果可以让物体运动看起来更加真实平滑,但在计算机产生的图像中,由于不存在曝光这一物理现象,渲染出来的图像往往都棱角分明,缺少运动模糊。在一些诸如赛车类型的游戏中,为画面添加运动模糊是一种常见的处理方法。
我们可以得到类似下图的效果。
Unity Shader入门精要学习笔记 - 第12章 屏幕后处理效果_第8张图片

运动模糊的实现方法有很多种。一种实现方法是利用一块累积缓存来混合多张连续的图像。当物体快速移动产生多张图像后,我们取它们之间的平均值作为最后的运动模糊图像。然而,这种暴力的方法对性能的消耗很大,因为想要获取多张帧图像往往意味着我们需要在同一帧里渲染多次场景。另一种应用广泛的方法是创建和使用速度缓存,这个缓存中存储了各个像素当前的运动速度,然后利用该值来决定模糊的方向和大小。
我们使用类似上述第一种方法的实现来模拟运动模糊的效果。我们不需要再一帧中把场景渲染多次,但需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。这种方法与原始的利用累计缓存的方法相比性能更好,但模糊效果可能会略有影响。

在摄像机上新建脚本MotionBlur.cs

public class MotionBlur : PostEffectsBase {  

    public Shader motionBlurShader;  
    private Material motionBlurMaterial = null;  

    public Material material {    
        get {  
            motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);  
            return motionBlurMaterial;  
        }    
    }  

    [Range(0.0f, 0.9f)]  
    public float blurAmount = 0.5f;  
    //blurAmount 的值越大, 运动拖尾的效果就越明显, 为了防止拖尾效果完全替代当前帧的渲染
    //结果, 我们把它的值截取在 0.0-0.9 范围内。  
    private RenderTexture accumulationTexture;  

    void OnDisable() {  
        DestroyImmediate(accumulationTexture);  
    }  

    void OnRenderImage (RenderTexture src, RenderTexture dest) {  
        if (material != null) {  
            // Create the accumulation texture  
            if (accumulationTexture == null || accumulationTexture.width != src.width || accumulationTexture.height != src.height) {  
                DestroyImmediate(accumulationTexture);  
                accumulationTexture = new RenderTexture(src.width, src.height, 0);  
                accumulationTexture.hideFlags = HideFlags.HideAndDontSave;  
                Graphics.Blit(src, accumulationTexture);  
            }  

            // We are accumulating motion over frames without clear/discard  
            // by design, so silence any performance warnings from Unity  
            accumulationTexture.MarkRestoreExpected();  

            material.SetFloat("_BlurAmount", 1.0f - blurAmount);  

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

新建一个Unity Shader。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unlit/Chapter12-MyMotionBlur"
{
    Properties {  
        _MainTex ("Base (RGB)", 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 = mul(UNITY_MATRIX_MVP, v.vertex);  

            o.uv = v.texcoord;  

            return o;  
        }  

        fixed4 fragRGB (v2f i) : SV_Target {  
            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  

        Pass {  
            Blend SrcAlpha OneMinusSrcAlpha  
            ColorMask RGB  

            CGPROGRAM  

            #pragma vertex vert    
            #pragma fragment fragRGB    

            ENDCG  
        }  

        Pass {     
            Blend One Zero  
            ColorMask A  

            CGPROGRAM    

            #pragma vertex vert    
            #pragma fragment fragA  

            ENDCG  
        }  
    }  
    FallBack Off  
}  

你可能感兴趣的:(Unity)