Unity PostProcess Shader - 14 - InnerGlow - 基于后效的内轮廓效果,类似 PhotoShop 图层的内发光

141 篇文章 33 订阅
49 篇文章 0 订阅


之前写的一个类似Photoshop中的图层的 发光 效果


管线:built-in 管线
unity 版本:2019.4.30f1


思路

  • 得到内轮廓边缘:只要某个像素的 kernal size 范围内的,如 5x5 的 kernal size,只要某个 pixel(x,y) 像素不是全黑,并且 kernal size 核内的 sum 值小于 kernal size 平方,都算是边缘
  • 将内轮廓边缘模糊
  • 将模糊的边缘与 _SrcTex 做像素交集才显示

Shader

// jave.lin 2021/02/25
Shader "PP/InnerGlowPP"
{
    CGINCLUDE
        #include "UnityCG.cginc"

        // pure col
        float4 vert_pure_col(float4 vertex : POSITION) : SV_POSITION
        {
            return UnityObjectToClipPos(vertex);
        }
        fixed4 frag_pure_col() : SV_Target
        {
            return 1;
        }

        // inner glow
        Sampler2D _InnerGlowOrginTex;
        float4 _InnerGlowOrginTex_TexelSize;
        F _InnerGlowKernelSize;
        F _InnerGlowSize;
        struct a2v_inner_glow
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
        };
        struct v2f_inner_glow
        {
            float4 vertex : SV_POSITION;
            float2 uv : TEXCOORD0;
        };
        v2f_inner_glow vert_inner_glow (a2v_inner_glow v) {
            v2f_inner_glow o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = v.uv;
            return o;
        }
        fixed4 frag_inner_glow(v2f_inner_glow i) : SV_Target
        {
            fixed originV = 0;
            F sum = 0;
            int start = -_InnerGlowKernelSize;
            int count = _InnerGlowKernelSize + 1;
            int delta = count - start;
            int threhold = delta * delta;

            for (int x = start; x < count; x++)
            {
                for (int y = start; y < count; y++)
                {
                    float2 temp_uv = i.uv + float2(x, y) * _InnerGlowOrginTex_TexelSize.xy * _InnerGlowSize;
                    fixed v = tex2D(_InnerGlowOrginTex, temp_uv);
                    if (x == 0 && y == 0) originV = v;
                    sum += v;
                }
            }
            
            return originV > 0 && sum < threhold ? 1 : 0;
        }

        /// blur /
        struct a2v_blur
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
        };
        struct v2f_blur
        {
            float4 vertex : SV_POSITION;
            float2 uv : TEXCOORD0;
            float4 uv01 : TEXCOORD1;
            float4 uv23 : TEXCOORD2;
        };
        Sampler2D _BlurOrginTex;
        float4 _BlurOrginTex_TexelSize;
        F _BlurSize;
        v2f_blur vert_blur_h (a2v_blur v) {
            v2f_blur o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = v.uv;
            float2 ts = _BlurOrginTex_TexelSize.xy;
            float2 offset1 = float2(1, 0);
            float2 offset2 = float2(2, 0);
            o.uv01.xy = v.uv + offset1 *  ts * _BlurSize; // 左1
            o.uv01.zw = v.uv + offset1 * -ts * _BlurSize; // 右1
            o.uv23.xy = v.uv + offset2 *  ts * _BlurSize; // 左2
            o.uv23.zw = v.uv + offset2 * -ts * _BlurSize; // 右2
            return o;
        }
        v2f_blur vert_blur_v (a2v_blur v) {
            v2f_blur o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = v.uv;
            float2 ts = _BlurOrginTex_TexelSize.xy;
            float2 offset1 = float2(0, 1);
            float2 offset2 = float2(0, 2);
            o.uv01.xy = v.uv + offset1 *  ts * _BlurSize; // 上1
            o.uv01.zw = v.uv + offset1 * -ts * _BlurSize; // 下1
            o.uv23.xy = v.uv + offset2 *  ts * _BlurSize; // 上2
            o.uv23.zw = v.uv + offset2 * -ts * _BlurSize; // 下2
            return o;
        }
        fixed4 frag_blur (v2f_blur i) : SV_Target {
            fixed4 sum = tex2D(_BlurOrginTex, i.uv) * 0.4026;
            sum += tex2D(_BlurOrginTex, i.uv01.xy) * 0.2442; // 左1 | 上1
            sum += tex2D(_BlurOrginTex, i.uv01.zw) * 0.2442; // 右1 | 下1
            sum += tex2D(_BlurOrginTex, i.uv23.xy) * 0.0545; // 左2 | 上2
            sum += tex2D(_BlurOrginTex, i.uv23.zw) * 0.0545; // 右2 | 下2
            return sum;
        }

        /// final /
        Sampler2D _MaskTex;
        Sampler2D _BlurTex;
        Sampler2D _SrcTex;
        fixed4 _InnerGlowColor;
        struct a2v_final
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;
        };
        struct v2f_final
        {
            float4 vertex : SV_POSITION;
            float2 uv : TEXCOORD0;
        };
        v2f_final vert_final (a2v_final v) {
            v2f_final o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = v.uv;
            return o;
        }
        fixed4 frag_final (v2f_final i) : SV_Target {
            fixed blur_col = tex2D(_BlurTex, i.uv).r;
            fixed mask_col = tex2D(_MaskTex, i.uv).r;
            blur_col *= (mask_col > 0 ? 1 : 0);
            fixed4 combinedCol = saturate(tex2D(_SrcTex, i.uv));
            combinedCol.rgb += (blur_col * _InnerGlowColor.a) * _InnerGlowColor.rgb;
            return combinedCol;
        }
    ENDCG
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always
        Pass // pure col 0
        {
            
            ColorMask R
            CGPROGRAM
            #pragma vertex vert_pure_col
            #pragma fragment frag_pure_col
            ENDCG
        }
        Pass // inner glow 1
        {
            ColorMask R
            CGPROGRAM
            #pragma vertex vert_inner_glow
            #pragma fragment frag_inner_glow
            ENDCG
        }
        Pass // blur h 2
        {
            ColorMask R
            CGPROGRAM
            #pragma vertex vert_blur_h
            #pragma fragment frag_blur
            ENDCG
        }
        Pass // blur v 3
        {
            ColorMask R
            CGPROGRAM
            #pragma vertex vert_blur_v
            #pragma fragment frag_blur
            ENDCG
        }
        Pass // final 4
        {
            CGPROGRAM
            #pragma vertex vert_final
            #pragma fragment frag_final
            ENDCG
        }
    }
}


CSharp


Camera PP 脚本

// jave.lin 2021/02/25
// 绘制外发光后效
// 临时效果,时间关系,未优化
using UnityEngine;
using UnityEngine.Rendering;

public class InnerGlowPP : PostEffectBasic
{
    private static int _InnerGlowKernelSize_hash = Shader.PropertyToID("_InnerGlowKernelSize");
    private static int _InnerGlowSize_hash = Shader.PropertyToID("_InnerGlowSize");
    private static int _InnerGlowColor_hash = Shader.PropertyToID("_InnerGlowColor");
    private static int _InnerGlowOrginTex_hash = Shader.PropertyToID("_InnerGlowOrginTex");
    private static int _BlurOrginTex_hash = Shader.PropertyToID("_BlurOrginTex");
    private static int _BlurSize_hash = Shader.PropertyToID("_BlurSize");
    private static int _BlurTex_hash = Shader.PropertyToID("_BlurTex");
    private static int _MaskTex_hash = Shader.PropertyToID("_MaskTex");
    private static int _SrcTex_hash = Shader.PropertyToID("_SrcTex");

    [Header("绘制材质")]
    public Material mat;

    [Header("DownSample 降采等级")]
    public int down_sample_level = 4;

    [Range(1, 4)]
    [Header("高斯模糊的次数")]
    public int iterations = 4;

    [Header("核大小")]
    [Range(0.0f, 2.0f)]
    public int innerGlowKernelSize = 1;

    [Header("内发光大小")]
    [Range(0.0f, 10.0f)]
    public float innerGlowSize = 2.0f;

    [Header("模糊边界大小:每次模糊采样纹素距离的缩放因数")]
    [Range(0.2f, 3.0f)]
    public float blur_size = 0.2f;

    [Header("内发光颜色")]
    public Color glow_color = Color.red;

    private CommandBuffer cmdBuffer;
    private Camera cam;

    protected override void Start()
    {
        base.Start();
        cmdBuffer = new CommandBuffer();
        cmdBuffer.name = "InnerGlowPPCmdBuffer";
        cam = GetComponent<Camera>();
    }

    private void OnDestroy()
    {
        if (cmdBuffer != null)
        {
            cmdBuffer.Clear();
            cmdBuffer.Dispose();
            cmdBuffer = null;
        }
        cam.targetTexture = null;
        cam = null;
    }

    protected override void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (!IsSupported || InnerGlowManager.instance.Count == 0)
        {
            GraphicsUtil.SkipOnImage(src, dest);
            return;
        }

        if (mat == null)
        {
            Debug.LogError("InnerGlowPP.mat == null");
            GraphicsUtil.SkipOnImage(src, dest);
            return;
        }

        // clamp down_sample_level
        if (down_sample_level <= 0) down_sample_level = 1;

        var sw = Screen.width;
        var sh = Screen.height;
        var rw = sw / down_sample_level;
        var rh = sh / down_sample_level;

        ShowGlow(src, dest, sw, sh, rw, rh, mat);
    }

    private void ShowGlow(RenderTexture src, RenderTexture dest, int sw, int sh, int rw, int rh, Material usingMat)
    {
        // create RT
        var inner_glow_mask_rt = RenderTexture.GetTemporary(sw, sh, 0, RenderTextureFormat.R8);
        inner_glow_mask_rt.filterMode = FilterMode.Bilinear;
        inner_glow_mask_rt.name = $"InnerGlowPP.inner_glow_mask_rt_{sw}x{sh}";

        cmdBuffer.Clear();
        cmdBuffer.SetRenderTarget(inner_glow_mask_rt);
        cmdBuffer.ClearRenderTarget(false, true, Color.black);

        InnerGlowManager.instance.Update2CmdBuffer2Draw(cmdBuffer, usingMat, 0);
        // execute cmd buffer, Draw To RT
        Graphics.ExecuteCommandBuffer(cmdBuffer);

        // inner glow
        var inner_glow_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
        inner_glow_rt.filterMode = FilterMode.Bilinear;
        inner_glow_rt.name = $"InnerGlowPP.inner_glow_rt_{rw}x{rh}";

        usingMat.SetInt(_InnerGlowKernelSize_hash, innerGlowKernelSize);
        usingMat.SetFloat(_InnerGlowSize_hash, innerGlowSize);
        usingMat.SetTexture(_InnerGlowOrginTex_hash, inner_glow_mask_rt);
        Graphics.Blit(null, inner_glow_rt, usingMat, 1);

        // blur
        var blur_rt = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
        blur_rt.filterMode = FilterMode.Bilinear;
        blur_rt.name = $"InnerGlowPP.blur_rt_{rw}x{rh}";

        var rt0 = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
        rt0.filterMode = FilterMode.Bilinear;
        rt0.name = $"InnerGlowPP.rt0_{rw}x{rh}";

        // 先将远 blur_rt 复制到rt0
        // references : https://blog.csdn.net/linjf520/article/details/104940213
        Graphics.Blit(inner_glow_rt, rt0);

        for (int i = 0; i < iterations; i++)
        {
            usingMat.SetFloat(_BlurSize_hash, 1 + i * blur_size);

            var rt1 = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
            rt1.filterMode = FilterMode.Bilinear;
            rt1.name = $"InnerGlowPP.rt1.1_{rw}x{rh}";

            usingMat.SetTexture(_BlurOrginTex_hash, rt0);
            // horizontal blur
            Graphics.Blit(null, rt1, usingMat, 2);

            RenderTexture.ReleaseTemporary(rt0);
            rt0 = rt1;
            rt1 = RenderTexture.GetTemporary(rw, rh, 0, RenderTextureFormat.R8);
            rt1.filterMode = FilterMode.Bilinear;
            rt1.name = $"InnerGlowPP.rt1.2_{rw}x{rh}";

            // vertical blur
            usingMat.SetTexture(_BlurOrginTex_hash, rt0);
            Graphics.Blit(null, rt1, usingMat, 3);
            RenderTexture.ReleaseTemporary(rt0);
            rt0 = rt1;
        }

        Graphics.Blit(rt0, blur_rt);
        RenderTexture.ReleaseTemporary(rt0);

        // final
        usingMat.SetTexture(_MaskTex_hash, inner_glow_mask_rt);
        usingMat.SetTexture(_BlurTex_hash, blur_rt);
        usingMat.SetTexture(_SrcTex_hash, src);
        usingMat.SetColor(_InnerGlowColor_hash, glow_color);
        Graphics.Blit(null, dest, usingMat, 4);

        RenderTexture.ReleaseTemporary(inner_glow_mask_rt);
        RenderTexture.ReleaseTemporary(inner_glow_rt);
        RenderTexture.ReleaseTemporary(blur_rt);
    }
}


Outline Renderer 提取器

挂载在对应的 Root 下,会自动提取底下所有的 renderer

// jave.lin 2021/09/24
// InnerGlow 后效绘制的 Renderer 提取组件

using System.Collections.Generic;
using UnityEngine;

public class InnerGlowRendererExtractor : MonoBehaviour
{
    public bool extractEveryFrame = false;

    //private List<Renderer> renderers;
    public List<Renderer> renderers; // 暂时 public 便于 inspector 中查看
    private int instID;

    private void Awake()
    {
        instID = GetInstanceID();
        renderers = ListPoolUtil<Renderer>.FromPool();
    }
    private void Start()
    {
        UpdateRenderers();
    }
    private void Update()
    {
        if (extractEveryFrame)
        {
            UpdateRenderers();
        }
    }
    private void OnDestroy()
    {
        if (renderers != null)
        {
            ListPoolUtil<Renderer>.ToPool(renderers);
            renderers = null;
        }
    }
    private void UpdateRenderers()
    {
        var mrList = ListPoolUtil<MeshRenderer>.FromPool();
        gameObject.GetComponentsInChildren<MeshRenderer>(false, mrList);

        var smrList = ListPoolUtil<SkinnedMeshRenderer>.FromPool();
        gameObject.GetComponentsInChildren<SkinnedMeshRenderer>(false, smrList);

        renderers.Clear();
        renderers.AddRange(mrList);
        renderers.AddRange(smrList);

        ListPoolUtil<MeshRenderer>.ToPool(mrList);
        ListPoolUtil<SkinnedMeshRenderer>.ToPool(smrList);

        InnerGlowManager.instance.Remove(instID);
        InnerGlowManager.instance.Add(instID, renderers);
    }
}

OutlineManager

// jave.lin 2021/02/25
// 内发光的管理
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class InnerGlowManager : MonoSingleton<InnerGlowManager>
{
    private Stack<List<Renderer>> listPool = new Stack<List<Renderer>>();
    private Stack<InnerGlowElement> innerGlowElementPool = new Stack<InnerGlowElement>();

    private List<InnerGlowElement> innerGlowElementList = new List<InnerGlowElement>();
    private Dictionary<int, InnerGlowElement> innerGlowElementDict_key_instid = new Dictionary<int, InnerGlowElement>();
    private Dictionary<GameObject, InnerGlowElement> innerGlowElementDict_key_go = new Dictionary<GameObject, InnerGlowElement>();


    public int Count => innerGlowElementList.Count;

    private void OnDestroy()
    {
        if (listPool != null)
        {
            foreach (var item in listPool)
            {
                item.Clear();
            }
            listPool.Clear();
            listPool = null;
        }
        if (innerGlowElementPool != null)
        {
            innerGlowElementPool.Clear();
            innerGlowElementPool = null;
        }
        if (listPool != null)
        {
            listPool.Clear();
            listPool = null;
        }
    }
    public bool Contains(GameObject go)
    {
        foreach (var glowElement in innerGlowElementList)
        {
            if (glowElement.go == go)
            {
                return true;
            }
        }
        return false;
    }
    public void Add(int instID, List<Renderer> renderers)
    {
        InnerGlowElement e = innerGlowElementPool.Count > 0 ? innerGlowElementPool.Pop() : null;

        if (e == null)
        {
            e = new InnerGlowElement 
            { 
                optType = eInnerGlowOptType.SpecialRenderers, 
                instID = instID, 
                go = null, 
                renderers = new List<Renderer>(renderers), 
                ignoreActive = false 
            };
        }
        else
        {
            e.optType = eInnerGlowOptType.SpecialRenderers;
            e.instID = instID;
            e.go = null;
            e.renderers.AddRange(renderers);
            e.ignoreActive = false;
        }

        innerGlowElementDict_key_instid[instID] = e;
        innerGlowElementList.Add(e);
        //Log.logError($"InnerGlowManager.Add instID:{instID}");
    }
    public void Add(GameObject go, bool ignoreActive = false, int ignoreLayer = 0)
    {
        if (ignoreLayer == -1)
        {
            // culling everything
            return;
        }
        if (go == null)
        {
            return;
        }

        if (Contains(go))
        {
            return;
        }

        var list = listPool.Count > 0 ? listPool.Pop() : new List<Renderer>();
        go.GetComponentsInChildren<Renderer>(false, list);

        InnerGlowElement e = innerGlowElementPool.Count > 0 ? innerGlowElementPool.Pop() : null;

        if (ignoreLayer != 0)
        {
            var count = list.Count;
            for (int i = 0; i < count; i++)
            {
                if (list[i].gameObject.layer == ignoreLayer)
                {
                    list.RemoveAt(i);
                    --i;
                    --count;
                    continue;
                }
            }
        }

        if (e == null)
        {
            e = new InnerGlowElement { optType = eInnerGlowOptType.SpecialGO, instID = -1, go = go, renderers = list, ignoreActive = ignoreActive };
        }
        else
        {
            e.optType = eInnerGlowOptType.SpecialGO;
            e.instID = -1;
            e.go = go;
            e.renderers = list;
            e.ignoreActive = ignoreActive;
        }
        innerGlowElementDict_key_go[go] = e;
        innerGlowElementList.Add(e);
    }
    public void Remove(GameObject go)
    {
        if (go == null)
        {
            return;
        }

        for (int i = 0; i < innerGlowElementList.Count; i++)
        {
            var e = innerGlowElementList[i];
            if (e.go == null)
            {
                _Reclyle(e);
                innerGlowElementList.RemoveAt(i);
                continue;
            }
            if (e.go == go)
            {
                _Reclyle(e);
                innerGlowElementList.RemoveAt(i);
                return;
            }
        }
        innerGlowElementDict_key_go.Remove(go);
    }
    public void Remove(int instID)
    {
        if (innerGlowElementDict_key_instid.Remove(instID))
        {
            for (int i = 0; i < innerGlowElementList.Count; i++)
            {
                var e = innerGlowElementList[i];
                if (e.instID == instID)
                {
                    _Reclyle(e);
                    innerGlowElementList.RemoveAt(i);
                    break;
                }
            }
        }
        
        //Log.logError($"InnerGlowManager.Remove instID:{instID}");
    }
    public void Clear()
    {
        if (innerGlowElementList.Count > 0)
        {
            foreach (var e in innerGlowElementList)
            {
                _Reclyle(e);
            }
            innerGlowElementList.Clear();
        }
        innerGlowElementList.Clear();
        innerGlowElementDict_key_go.Clear();
    }
    public void Update2CmdBuffer2Draw(CommandBuffer cmdBuffer, Material material, int pass = -1)
    {
        for (int i = innerGlowElementList.Count - 1; i > -1; i--)
        {
            var e = innerGlowElementList[i];
            switch (e.optType)
            {
                case eInnerGlowOptType.SpecialGO:
                    if (e.go == null)
                    {
                        _Reclyle(e);
                        innerGlowElementList.RemoveAt(i);
                        continue;
                    }
                    if (!e.ignoreActive)
                    {
                        if (!e.go.activeInHierarchy)
                        {
                            continue;
                        }
                    }

                    for (int j = 0; j < e.renderers.Count; j++)
                    {
                        var r = e.renderers[j];
                        var draw = r != null;
                        if (draw && !e.ignoreActive)
                        {
                            draw = r.enabled && r.gameObject.activeInHierarchy;
                        }
                        if (draw)
                        {
                            cmdBuffer.DrawRenderer(r, material, 0, pass);
                        }
                    }
                    break;
                case eInnerGlowOptType.SpecialRenderers:
                    for (int j = 0; j < e.renderers.Count; j++)
                    {
                        var r = e.renderers[j];
                        if (r == null || r.gameObject == null)
                        {
                            _Reclyle(e);
                            innerGlowElementList.RemoveAt(i);
                            break;
                        }
                        var draw = true;
                        if (!e.ignoreActive)
                        {
                            draw = r.enabled && r.gameObject.activeInHierarchy;
                        }
                        if (draw)
                        {
                            cmdBuffer.DrawRenderer(r, material, 0, pass);
                        }
                    }
                    break;
                default:
                    Debug.LogError($"Unimplements GlowOptType : {e.optType}");
                    break;
            }
        }
    }
    private void _Reclyle(InnerGlowElement e)
    {
        if (e.optType == eInnerGlowOptType.SpecialGO)
        {
            innerGlowElementDict_key_go.Remove(e.go);
        }
        else
        {
            innerGlowElementDict_key_instid.Remove(e.instID);
        }
        e.renderers.Clear();
        listPool.Push(e.renderers);
        innerGlowElementPool.Push(e);
    }
}

public enum eInnerGlowOptType
{
    SpecialGO,
    SpecialRenderers,
}

public class InnerGlowElement
{
    public eInnerGlowOptType optType;
    public int instID;      // key, when optType == Special Instance ID
    public GameObject go;   // key, when optType == Special GO
    public List<Renderer> renderers;
    public bool ignoreActive;
}


效果

原图
在这里插入图片描述
加上外发光效果
在这里插入图片描述

GIF,颜色可以调整外发光大小,还有透明度
在这里插入图片描述

下面是 外发光 + 内发光 的效果
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值