【Shader】屏幕后处理笔记

字数 1353阅读 432

是什么?

顾名思义就是在得到屏幕渲染图后我们在对这个图做一定的处理,这个处理就差不多我们平时p图的哪些操作,改亮度,对比度,模糊啥的。

怎么做?

这里unity给我们一个接口-OnRenderImage

//src为我们得到屏幕渲染图 dest为我们处理后的一定结果
MonoBehaviour.OnRenderImage (RenderTexture src,RenderTexture dest)

在这个方法中我们一般使用Graphics.Blit来做融合处理。

public static void Blit(Texture source, RenderTexture dest, Material mat, [Internal.DefaultValue("-1")] int pass);
public static void Blit(Texture source, RenderTexture dest, Material mat);
public static void Blit(Texture source, Material mat);

简单的原理就是建一个材质把这个材质的效果叠加到source图上去,所以我们的所写的Shader中必须有一个叫_MainTex的属性。

都说是后处理了,最后渲染得到的图是通过摄像机得到的,所以我们需要把这些处理的脚本放在跟摄像机一个GameObject上。首先我们需要判断当前平台是否支持,然后需要创建一个材质。这里我们有一个基类PostEffectBase。如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class PostEffectBase : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {
        CheckResoures();
    }
    //在最开始调用
    protected void CheckResoures()
    {
        var isSupported = CheckSupport();
        if (!isSupported)
        {
            NoSupport();
        }
    }
    //检查是否支持
    protected bool CheckSupport()
    {
        if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false)
        {
            Debug.LogWarning("此平台不支持图片效果后处理");
            return false;
        }
        return true;
    }

    //取消该脚本效果
    protected void NoSupport()
    {
        enabled = false;
    }

    //检查shader是否存在并创建该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;
        }else
        {
            material=new Material(shader);
            material.hideFlags=HideFlags.DontSave;
            if (material)
            {
                return material;
            }else
            {
                return null;
            }
        }
    }
}

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

要做到这个三个效果我们只需要在片元方法中,调节其中颜色值即可。首先获得屏幕:

fixed4 renderTex = tex2D(_MainTex, i.uv); 

首先我们来看如何调整亮度:

fixed3 finalCol = renderTex.rgb * _Brightness; 

在调整饱和度:

//这个应该是一个经验公式
fixed luminance = 0.2125 * renderTex.r + 0.7254 * renderTex.g + 0.0721 * renderTex.b; 
fixed3 luminaceColor = fixed3(luminance, luminance, luminance); 
finalCol == lerp(luminaceColor, finalCol, _Saturation); 

最后设置对比度即可:

fixed3 avgColor = fixed3(0.5, 0.5, 0.5); 
finalCol = lerp(avgColor,finalCol,_Contrast); 
return fixed4(finalCol,renderTex.a); 

我们在OnRenderImage中设置这三个参数即可:

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

亮度的效果还是可以用来做开场动画和结束动画,挺好看的。

image

边缘检测

卷积: 是指使用卷积核对一张图像中的每个像素进行一系列操作。卷积核一般是一个正方形网格结构(3x3或者是2x2),每正方形网格中的格子都会有一个数值,称为权重值。既然是积就需要相乘,比如我们需要计算某一个像素的卷积,就把一个卷核的中心格子放在哪个像素上,然后依次计算核中每个元素和其覆盖的图像像素值得乘积并求和,得到该像素的卷积。

卷积示意图

这个我看了几次,才看明白哦。这个就很像乘以矩阵啥的,我们可以把卷积核理解成一个常量。对,就是相当于扫雷那种感觉我们在排一个点是否是雷就看周围的数字嘛,这个周围的就可以理解为卷积核。我们这个时候已知周围的数字,然后把周围的字与那个格子的颜色相乘然后,最后把所有相乘的值相加就得到了卷积。

那我们现在获得了卷积值,卷积值越大就越可能是边缘点。我们知道边缘为什么是边缘?就是因为他与其他地方的颜色差别很。当然这个卷积核肯定是特定的,这样就可以检测出来颜色大小。

3种常见的边缘检测算子

边缘检测cs代码

边缘检测shader代码

我这里没有实现这个功能,说的是我的shader不支持平台。。。。

高斯模糊

使用的原理与求边缘的原理差不多,也是使用一个卷积核,但是这里叫做高斯核,这里有一个求高斯核的权重的公式,


公式

然后我们可以跟求边缘一样求得卷积。这里我们做了一个优化,把二维的正方形高斯核转化成两个一维的高斯核,就是转化中间的横竖那一列和行。

把一个二维5x5的高斯核转化为两个一维高斯核
效果

高斯模糊cs代码
高斯模糊shader代码


Bloom效果

这个效果是做到一种高光部分过爆的效果,带有朦胧效果。
原理就是先生成高亮部分的图,然后对这个图做高斯模糊最终获得一张图(模拟光线图),然后把模糊图与原图做一个混合即可。
所以这用了4个pass块,几次来处理。

效果图

Bloom效果cs代码
Bloom效果shader代码


运动模糊

做运动模糊一般有很多种方式:
1.累计缓存:把每一帧的图像记录下来,然后把记录下来的图片连续混合起来显示,这样就可以做出动态模糊同时也可以做出虚影的效果。这个消耗很大。
2.速度缓存:缓存中存储了各个像素当前的运动速度,然后利用该值来决定模糊的方向大小。

这里作者使用得的第一个方式的优化来实现,在得到之前渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。

在OnRenderImage中,我们就把当前渲染得到得的图不断存入acumulationTexture中。

void OnRenderImage(RenderTexture src, RenderTexture dest)
{
    if (material != null)
    {
        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);
        }
        //恢复操作 发生在渲染到纹理而该纹理又没有被提前清空或销毁的情况
        accumulationTexture.MarkRestoreExpected();

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

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

我们在shader中需要分别处理透明通道和颜色通道在两个pass块里,因为我们在获得模糊虚影时,需要设置虚影的长度,这个长度就是我们的设置的模糊参数。

实现效果

看起来还是很酷炫得的。

Bloom效果cs代码
Bloom效果shader代码

推荐阅读更多精彩内容