Unity Shader - Bloom(光暈、泛光)

前言

Bloom(光暈)是一種計算機圖形效果,用於視頻遊戲,演示和高動態範圍渲染(HDRR)中,以再現真實相機的成像僞像。該效果會產生從圖像中明亮區域的邊界延伸的條紋(或羽毛),從而造成超亮的光使攝像機或眼睛捕捉場景的幻覺。

效果對比如下:

左邊是原圖, 右邊Bloom處理後的

在這裏插入圖片描述

原理:

Bloom的實現原理非常簡單,大致分爲三步:

  1. 對需要處理的圖像經過亮度提取, 並且通過一個闕值來控制亮度
  2. 對經過亮度提取後的圖像進行模糊處理(這裏是採用高斯模糊)
  3. 最後再疊加原圖和模糊處理後的圖像,輸出即可

實現

下面我們來逐步的通過代碼實現一下。

既然是後期特效, 那麼基本框架和之前的後期處理一樣, 首先在Camera上掛載一個C#腳本來捕捉攝像機渲染後的圖像。

一 C#實現

新建一個Bloom.cs
PostEffectsBase 基類可以在這裏獲取(來自《Unity Shader入門精要》)

using System.Collections;
using UnityEngine;
// ---------------------------【Bloom 全屏泛光後期】---------------------------
//編輯狀態下也運行  
[ExecuteInEditMode]
public class Bloom : PostEffectsBase
{
    public Shader bloomShader;
    private Material mMaterial;
    //bloom處理的shader
    public Material material
    {
        get
        {
            mMaterial = CheckShaderAndCreateMaterial(bloomShader, mMaterial);
            return mMaterial;
        }
    }
    //迭代次數
    [Range(0, 4)]
    public int iterations = 3;

    //模糊擴散範圍
    [Range(0.2f, 3.0f)]
    public float blurSpread = 0.6f;
    // 降頻
    private int downSample = 1;

    // 亮度闕值
    [Range(-1.0f, 1.0f)]
    public float luminanceThreshold = 0.6f;
    // bloom 強度
    [Range(0.0f, 5.0f)]
    public float bloomFactor = 1;
    // bloom 顏色值
    public Color bloomColor = new Color(1, 1, 1, 1);

    void Awake()
    {
        bloomShader = Shader.Find("lcl/screenEffect/Bloom");
    }

    //-------------------------------------【OnRenderImage函數】------------------------------------    
    // 說明:此函數在當完成所有渲染圖片後被調用,用來渲染圖片後期效果
    //--------------------------------------------------------------------------------------------------------  
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (material)
        {
            int rtW = source.width >> downSample;
            int rtH = source.height >> downSample;
            RenderTexture texture1 = RenderTexture.GetTemporary(rtW, rtH, 0);
            RenderTexture texture2 = RenderTexture.GetTemporary(rtW, rtH, 0);
            // 亮度提取 - 通道0
            material.SetFloat("_LuminanceThreshold", luminanceThreshold);
            Graphics.Blit(source, texture1, material, 0);

            // 高斯模糊 - 通道1
            for (int i = 0; i < iterations; i++)
            {
                //垂直高斯模糊
                material.SetVector("_offsets", new Vector4(0, 1.0f + i * blurSpread, 0, 0));
                Graphics.Blit(texture1, texture2, material, 1);
                //水平高斯模糊
                material.SetVector("_offsets", new Vector4(1.0f + i * blurSpread, 0, 0, 0));
                Graphics.Blit(texture2, texture1, material, 1);
            }
            //用模糊圖和原始圖計算出輪廓圖  - 通道2
            material.SetColor("_BloomColor", bloomColor);
            material.SetFloat("_BloomFactor", bloomFactor);
            material.SetTexture("_BlurTex", texture1);
            Graphics.Blit(source, destination, material, 2);
        }
    }
}

二 Shader實現

下面我們重點來看看Shader是如何實現的:

1.首先我們對圖像的亮度的提取

亮度的提取非常簡單, 通過一個公式即可提取: 0.2125 * r + 0.7154 * g + 0.0721 * b , 這裏的原理就不細說了, 感興趣的可以去Google一下。
亮度提取成功之後我們可以通過一個闕值來控制,並且把值限制在0-1範圍內

關鍵shader代碼如下:

// 亮度提取
fixed luminance(fixed4 color) {
    return  0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; 
}
// 片元着色器
fixed4 fragExtractBright(v2fExtBright i) : SV_Target {
    fixed4 color = tex2D(_MainTex, i.uv);
    // clamp 約束到 0 - 1 區間
    fixed val = clamp(luminance(color) - _LuminanceThreshold, 0.0, 1.0);
    
    return color * val;
}

呈現效果如下:

在這裏插入圖片描述

2.模糊處理

然後我們再對以上得到的圖像經過高斯模糊處理一下, 這裏也可以採用簡單的均值模糊處理。
這裏的模糊處理教程也不細說了,篇幅太長,可以參考我之前的一篇文章:Unity Shader - 均值模糊和高斯模糊

關鍵代碼:

 // ---------------------------【高斯模糊 - start】---------------------------
struct v2fBlur  
{  
    float4 pos : SV_POSITION;   //頂點位置  
    float2 uv  : TEXCOORD0;     //紋理座標  
    float4 uv01 : TEXCOORD1;    //一個vector4存儲兩個紋理座標  
    float4 uv23 : TEXCOORD2;    //一個vector4存儲兩個紋理座標  
};  

//高斯模糊頂點着色器
v2fBlur vertBlur(appdata_img v)  
{  
    v2fBlur o;  
    o.pos = UnityObjectToClipPos(v.vertex);  
    //uv座標  
    o.uv = v.texcoord.xy;  
    
    //計算一個偏移值,offset可能是(1,0,0,0)也可能是(0,1,0,0)這樣就表示了橫向或者豎向取像素周圍的點  
    _offsets *= _MainTex_TexelSize.xyxy;  
    
    //由於uv可以存儲4個值,所以一個uv保存兩個vector座標,_offsets.xyxy * float4(1,1,-1,-1)可能表示(0,1,0-1),表示像素上下兩個  
    //座標,也可能是(1,0,-1,0),表示像素左右兩個像素點的座標,下面*2.0,*3.0同理  
    o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
    o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;  
    return o;  
}  

//高斯模糊片段着色器
fixed4 fragBlur(v2fBlur i) : SV_Target  
{  
    fixed4 color = fixed4(0,0,0,0);  
    color += 0.4026 * tex2D(_MainTex, i.uv);  
    color += 0.2442 * tex2D(_MainTex, i.uv01.xy);  
    color += 0.2442 * tex2D(_MainTex, i.uv01.zw);  
    color += 0.0545 * tex2D(_MainTex, i.uv23.xy);  
    color += 0.0545 * tex2D(_MainTex, i.uv23.zw);  
    return color;  
}
// ---------------------------【高斯模糊 - end】---------------------------

處理後的效果如下:

在這裏插入圖片描述

3.最後疊加原圖和模糊處理後的圖像

關鍵代碼如下

// 片元着色器
fixed4 fragBloom(v2fBloom i) : SV_Target {
    //對原圖進行uv採樣
    fixed4 mainColor = tex2D(_MainTex, i.uv);
    //對模糊處理後的圖進行uv採樣
    fixed4 blurColor = tex2D(_BlurTex, i.uv);
    //輸出 = 原始圖像 + 模糊圖像 * bloom顏色 * bloom權值
    fixed4 resColor = mainColor + blurColor * _BloomColor * _BloomFactor
    return resColor;
} 

原圖和Bloom的對比效果如下(左原圖,右bloom):

在這裏插入圖片描述

最終的完整Shader代碼:

// ---------------------------【泛光 Bloom】---------------------------
Shader "lcl/screenEffect/Bloom"  
{
    // ---------------------------【屬性】---------------------------
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    // ---------------------------【子着色器】---------------------------
    SubShader
    {
        //後處理效果一般都是這幾個狀態  
        ZTest Always  
        Cull Off  
        ZWrite Off  
        Fog{ Mode Off } 

        CGINCLUDE
        #include "UnityCG.cginc"
        
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        sampler2D _BlurTex;
        float4 _offsets;
        float _LuminanceThreshold;
        fixed4 _BloomColor;
        float _BloomFactor;

        // ---------------------------【亮度提取 - start】---------------------------
        struct v2fExtBright {
            float4 pos : SV_POSITION; 
            half2 uv : TEXCOORD0;
        };
        // 頂點着色器
        v2fExtBright vertExtractBright(appdata_img v) {
            v2fExtBright 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(v2fExtBright i) : SV_Target {
            fixed4 color = tex2D(_MainTex, i.uv);
            // clamp 約束到 0 - 1 區間
            fixed val = clamp(luminance(color) - _LuminanceThreshold, 0.0, 1.0);
            
            return color * val;
        }
        // ---------------------------【亮度提取 - end】---------------------------


        
        // ---------------------------【高斯模糊 - start】---------------------------
        struct v2fBlur  
        {  
            float4 pos : SV_POSITION;   //頂點位置  
            float2 uv  : TEXCOORD0;     //紋理座標  
            float4 uv01 : TEXCOORD1;    //一個vector4存儲兩個紋理座標  
            float4 uv23 : TEXCOORD2;    //一個vector4存儲兩個紋理座標  
        };  

        //高斯模糊頂點着色器
        v2fBlur vertBlur(appdata_img v)  
        {  
            v2fBlur o;  
            o.pos = UnityObjectToClipPos(v.vertex);  
            //uv座標  
            o.uv = v.texcoord.xy;  
            
            //計算一個偏移值,offset可能是(1,0,0,0)也可能是(0,1,0,0)這樣就表示了橫向或者豎向取像素周圍的點  
            _offsets *= _MainTex_TexelSize.xyxy;  
            
            //由於uv可以存儲4個值,所以一個uv保存兩個vector座標,_offsets.xyxy * float4(1,1,-1,-1)可能表示(0,1,0-1),表示像素上下兩個  
            //座標,也可能是(1,0,-1,0),表示像素左右兩個像素點的座標,下面*2.0,*3.0同理  
            o.uv01 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1);  
            o.uv23 = v.texcoord.xyxy + _offsets.xyxy * float4(1, 1, -1, -1) * 2.0;  
            return o;  
        }  
        
        //高斯模糊片段着色器
        fixed4 fragBlur(v2fBlur i) : SV_Target  
        {  
            fixed4 color = fixed4(0,0,0,0);  
            color += 0.4026 * tex2D(_MainTex, i.uv);  
            color += 0.2442 * tex2D(_MainTex, i.uv01.xy);  
            color += 0.2442 * tex2D(_MainTex, i.uv01.zw);  
            color += 0.0545 * tex2D(_MainTex, i.uv23.xy);  
            color += 0.0545 * tex2D(_MainTex, i.uv23.zw);  
            return color;  
        }
        // ---------------------------【高斯模糊 - end】---------------------------

        // ---------------------------【Bloom(高斯模糊和原圖疊加) - start】---------------------------
        struct v2fBloom {
            float4 pos : SV_POSITION; 
            half2 uv : TEXCOORD0;
        };
        // 頂點着色器
        v2fBloom vertBloom(appdata_img v) {
            v2fBloom o;
            
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord;
            return o;
        }
        
        // 片元着色器
        fixed4 fragBloom(v2fBloom i) : SV_Target {
            //對原圖進行uv採樣
            fixed4 mainColor = tex2D(_MainTex, i.uv);
            //對模糊處理後的圖進行uv採樣
            fixed4 blurColor = tex2D(_BlurTex, i.uv);
            //輸出 = 原始圖像 + 模糊圖像 * bloom顏色 * bloom權值
            fixed4 resColor = mainColor + blurColor * _BloomColor * _BloomFactor;
            return resColor;
        } 
        // ---------------------------【Bloom - end】---------------------------

        ENDCG

        // 亮度提取
        Pass {  
            CGPROGRAM  
            #pragma vertex vertExtractBright  
            #pragma fragment fragExtractBright  
            ENDCG  
        }

        //高斯模糊
        Pass {
            CGPROGRAM
            #pragma vertex vertBlur  
            #pragma fragment fragBlur
            ENDCG  
        }

        // Bloom
        Pass {
            CGPROGRAM
            #pragma vertex vertBloom  
            #pragma fragment fragBloom
            ENDCG  
        }

    }
}

最後

有興趣的小夥伴可以來我的GitHub逛逛, 歡迎Star,謝謝!
裏面有我平時學習unity shader過程中實現的一些特效demo。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章