Unity Shader 屏幕后处理-荧光效果(Bloom)

2021-03-13  本文已影响0人  洒一地阳光_217d

Unity Shader系列文章:Unity Shader目录-初级篇

Unity Shader系列文章:Unity Shader目录-中级篇

效果:
Bloom效果。光线透过门扩散到了周围较暗的区域中 左图:原效果。右图:Bloom处理后的效果
原理:

先根据一个阙值提取出图像中的较亮区域把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合得到最终的效果。

ScreenPostEffectsBase基类代码:

using UnityEngine;

/// <summary>
/// 屏幕后处理效果基类
/// </summary>
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class ScreenPostEffectsBase : MonoBehaviour
{
    public Shader Shader;
    public Material Material
    {
        get
        {
            return CheckAndCreateMaterial();
        }
    }
    private Material _material;

    protected void Start()
    {
        CheckResources();
    }

    /// <summary>
    /// 检查资源
    /// </summary>
    protected void CheckResources()
    {
        if (!CheckSupport())
        {
            NotSupported();
        }
    }

    /// <summary>
    /// 检查支持
    /// </summary>
    /// <returns></returns>
    protected bool CheckSupport()
    {
        bool isSupported = SystemInfo.supportsImageEffects;
        return isSupported;
    }

    /// <summary>
    /// 不支持
    /// </summary>
    protected void NotSupported()
    {
        enabled = false;
    }

    /// <summary>
    /// 检查和创建Material
    /// </summary>
    /// <returns></returns>
    protected Material CheckAndCreateMaterial()
    {
        if (!Shader || !Shader.isSupported)
        {
            return null;
        }

        if (_material && _material.shader == Shader)
        {
            return _material;
        }

        _material = new Material(Shader);
        _material.hideFlags = HideFlags.DontSave;
        return _material;
    }
}

ScreenBloom派生类代码:

using UnityEngine;

/// <summary>
/// 屏幕后处理:荧光效果(Bloom)
/// </summary>
public class ScreenBloom : ScreenPostEffectsBase
{
    /// <summary>
    /// 迭代次数
    /// </summary>
    [Range(0, 4)]
    public int Iterations = 3;

    /// <summary>
    /// 模糊扩散
    /// </summary>
    [Range(0.2f, 3)]
    public float BlurSpread = 0.6f;

    /// <summary>
    /// 缩放系数
    /// </summary>
    [Range(1, 8)]
    public int DownSample = 2;

    /// <summary>
    /// 控制亮度阈值大小
    /// </summary>
    [Range(0.0f, 4.0f)]
    public float LuminanceThreshold = 0.6f;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (Material != null)
        {
            Material.SetFloat("_LuminanceThreshold", LuminanceThreshold);

            int rtW = src.width / DownSample;
            int rtH = src.height / DownSample;

            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
            buffer0.filterMode = FilterMode.Bilinear;

            // 来使用Shader中的第1个Pass提取图像中的较亮区域,将结果存储在buffer0中
            Graphics.Blit(src, buffer0, Material, 0);

            for (var i = 0; i < Iterations; i++)
            {
                Material.SetFloat("_BlurSize", 1 + i * BlurSpread);

                // 定义了第二个缓存 bufferl
                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                // 执行第2个 Pass 时,输入 buffer0, 输出是 bufferl
                Graphics.Blit(buffer0, buffer1, Material, 1);

                // 完毕后首先把 buffer0释放,再把结果值 buffer 存储到 buffer0中,重新分配 bufferl
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
                buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

                // 调用第3个Pass, 重复上述过程。模糊后的较亮区域将会存储在buffer0中
                Graphics.Blit(buffer0, buffer1, Material, 2);
                // 完毕后再把 buffer0释放,再把结果值buffer存储到buffer0中
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
            }

            // 再把 buffer0 传递给材质中的_Bloom纹理属性
            Material.SetTexture("_Bloom", buffer0);
            // 调用第4个Pass来进行最后的混合,将结果存储在目标渲染纹理中
            Graphics.Blit(src, dest, Material, 3);

            RenderTexture.ReleaseTemporary(buffer0);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

Shader代码:

// 荧光效果
Shader "Custom/Bloom"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" { }
        _Bloom ("Bloom", 2D) = "black" { }// 模糊后的较亮区域
        _LuminanceThreshold ("LuminanceThreshold", Float) = 0.5 // 控制亮度阈值大小
        _BlurSize ("BlurSIze", Float) = 1 // BlurSize 越大,模糊程度越高
    }

    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 = UnityObjectToClipPos(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);

            // 将采样得到的亮度值减去阐值Luminance Threshold, 并把结果截取到 0-1 范围内。
            // 然后,我们把该值和原像素值相乘,得到提取后的亮部区域。
            fixed val = clamp(luminance(c) - _LuminanceThreshold, 0, 1);

            return c * val;
        }

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

        // 顶点着色
        v2fBloom vertBloom(appdata_img v)
        {
            v2fBloom o;

            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv.xy = v.texcoord;
            o.uv.zw = v.texcoord;

            // 下面要对这个纹理坐标进行平台差异化处理,因为OpenGL,(0, 0)点对应了屏幕的左下角,DirectX中对应了屏幕左上角
            // 大多数时候这都无关紧要,除了渲染到渲染纹理时。在此情况下,Unity 渲染到 Direct3D 上的纹理时,在内部翻转渲染,以便平台之间的惯例匹配。
            // 如果我们做的屏幕后期特效简单(一次处理一个纹理),这无关紧要,因为 Graphics.Blit 方法会自动进行处理。
            // 然而,如果在屏幕后期特效中同时处理一个以上的 RenderTexture,它们很可能会在不同的垂直方向出现(仅在类似 Direct3D 的平台上,并且仅在使用抗锯齿选项时)
            
            // UNITY_UV_STARTS_AT_TOP,纹理的坐标系原点在纹理顶部的平台上值:Direct3D类似平台是1;OpenGL类似平台是0
            #if UNITY_UV_STARTS_AT_TOP
                // 在Direct3D平台下,如果我们开启了抗锯齿,则xxx_TexelSize.y 会变成负值,好让我们能够正确的进行采样。
                // 所以if (_MainTex_TexelSize.y < 0)的作用就是判断我们当前是否开启了抗锯齿。
                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

        }

        // 通过UsePass 语义指明它们的 Pass 名来实现的 (Pass代码详见之前文章《Unity Shader 屏幕后处理-高斯模糊》)
        // 由于 Unity 部会把所有 Pass Name转换成大写字母表示 因此在使用 UsePass 命令时我们必须使用大写形式的名字。
        UsePass "Custom/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"

        UsePass "Custom/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"

        Pass
        {
            CGPROGRAM

            #pragma vertex vertBloom
            #pragma fragment fragBloom

            ENDCG

        }
    }

    Fallback Off
}

上一篇下一篇

猜你喜欢

热点阅读